首页 C++ 基础知识总结
文章
X

C++ 基础知识总结

C++ 在 C语言的基础上添加的类代表的面向对象语言、C++ 模板支持的泛型编程。从 C 过渡到 C++ 的学习量就像从头学习 C 语言一样大。而且需要摒弃一些 编程习惯。C 提供了低级硬件访问, OOP 提供了高级抽象。

C 语言编程的思想

面向过程、结构化编程、自顶向下进行设计、任务分解。其中面向过程需要处理两个概念,即数据和算法数据是程序使用和处理的信息,而算法是程序使用这些 信息的方法。这种结构化编程理念提高了程序的清晰度、可靠性,并使之便于维护,但面对编写大程序时,还是面临很大的挑战。

编译运行

忠告

OOP(面向对象)

OOP 提供了一种新方法。与强调算法的过程性编程不同的是,OOP 强调的是数据。OOP 不像过程性编程语言,试图使问题满足语言的过程性方法,而是试图 让语言来满足问题的要求。其理念是设计与问题的本质特性相对应的数据格式(类)。在 C++ 中是一种规范,它描述了这种新型数据格式对象是根据 这种规范构造的特定的数据结构

OOP 程序设计方法首先设计类,它们准确地表示了程序要处理的东西。但不仅仅是将数据和方法合并为类定义。还有比如信息隐藏、类扩展继承等。C++ 真正的 优点之一就是,可以方便地重用和修改现有的、经过仔细测试的代码。

泛型编程

它与 OOP 的目标相同,都使得重用代码和抽象通用概念的技术更简单。不过 OOP 强调的是编程的数据方面,而泛型编程强调的是独立于特定 数据类型,它们的侧重点不同。 OOP 是一个管理大型项目的工具,而泛型编程提供了执行常见任务(如数据排序)的工具。术语泛型指的是创建独立于类型 的代码。

C++ 的基本元件

理解并掌握下面的基本元件,有助于将 C++ 内化,也有利于对其他的 OOP 语言的学习。

语句和数据类型

语句实际上是计算机的一条或多条机器指令的合集。数据类型是一种比较小的临时存储空间,这种空间就有首地址和存储大小,而且首地址由数据类型对应的 变量指认,存储大小则由数据类型确定(如int、long所占空间不一样),同时数据类型绑定了运算(如int由四则运算,而string就没有)。

1
2
3
C++ 中,任何值或任何有效的值与运算符的组合都是表达式。

语句是表达式后面紧随分好(;),但语句去掉分号不一定是表达式,如 int count;

需要注意的是,当使用不同编译器或跨平台时,编译器或其他操作系统定义的数据类型的长度不一样,会导致预想的表示范围有出入,很有可能导致越界处理 失败,为了防止类似错误,可以使用sizeof 测试出关心的数据类型的长度。为了跨平台,建议使用比如,#define INT int 之类的方法,这样需要修改时, 只用改一条语句就可以了(比如改成 #define INT long 以弥补表示范围的不同)

数据类型中 int 是计算机中“最自然”的类型,没有特殊需要建议使用 int ,当大量数组出现时,能满足需要的情况下建议使用 short(因为跨平台时,若使用 int 可能导致需要的内存加倍),只需要一个字节的数据,建议使用 char ,因为可以最大限度节约存储空间。

还需要注意的是,常数后面最好使用后缀,如 const int n = 10u;,在使用浮点运算时,一定要注意计算机精度是有限的,此时必须知道浮点数在计算机中的表示。

c++ 中存在隐性数据类型转换,不过其自动转换的原则是:保持数据的最大精度(小字节往大字节装,启用临时空间),最后结果再强制类型转换。 所以在自己强制转换时需要考虑精度损失。其中强制类型转换有两种方式:

  • (int)dScore;
  • static_cast(dScore);

精神损失

1
在国际编程一般使用宽字节类型 wchar_t, char16_t, char32_t

数组

数组可以看做同类型变量的集合,这些变量可以通过数组下标找到。c++ 11 提供了新特性。初始化可以省略 = ,如果不初始化,则编译器会自动 初始化 0

1
用指针遍历数组

指针能遍历数组是因为:数组实质上是同类型的变量集合,也就是说有独立的地址,而且这些变量连续存储,间隔该类型的字节数, 这样通过指针加减就可以引用数组中的变量了。

1
2
【注意】 指针加 1 ,指的是下一个类型单元(如 int))。
类型不同,一个类型单元代表的字节数一般也不同。

使用指针引用数组时,要明确数组名是数组的首地址,指针名是地址,*指针名是指针指向的变量(内容)。

1
数组的替代品
  • 模板类 vector

vector 是使用 new 创建动态数组的替代品。实际上,vector 类确实使用 new 和 delete 来管理内存,但这种工作是自动完成的。 vector 示例

  • 模板类 array

array 对象的长度和数组一样也是固定,也使用栈,但它使用起来更方便更安全(该类实现了一些基本的数组操作,如排序、整体赋值等)

使用 typedef 简化声明

typedef 让你能够创建类型别名,然后使用别名来简化代码。示例如下:

typedef

字符串

单引号('')和双引号("")是不一样的,单引号表示字符,双引号表示字符串(系统自动添加'\0')。

语句实际上是计算机的一条或多条机器指令的合集。数据类型是一种比较小的临时存储空间,这种空间就有首地址和存储大小,而且首地址由数据类型对应的 变量指认,存储大小则由数据类型确定(如int、long所占空间不一样),同时数据类型绑定了运算(如int由四则运算,而string就没有)。

1
计算机程序在存储数据时必须跟踪的3种基本属性,如下:
  • 信息存储在何处
  • 存储的值为多少
  • 存储的信息是什么类型

指针的危险

在 C++ 中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据内存,为数据提供空间是一个独立的步骤。 如果没有指定指针指向的数据空间,则指向的空间是随机的(该指针单元里的内容可能是以前遗留下的,然后被解读为地址)。 指针警告

指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上看,指针与整数是截然不同的类型,整数是可以执行四则运算等的数字, 而指针描述的是位置,将两个地址相乘没有任何意义,可见,不能简单地将整数赋给指针,必须进行强制类型转化,如:

1
2
int * pt = null;
pt = (int *)0xB800000;

我们知道,不论什么类型的指针,其本身所占的字节数是相等的,那为什么还必须声明指针所指向的类型呢?原因如下:

1
2
地址(指针)本身只指出了数据存储地址的开始,而没有指出其类型(使用的字节数),
这样就无法知道该存储空间的结束位置。

指针和 const

1
指针有点通配符的意味!

关键字 const 用于指针有两种不同的方式:

  • 让指针指向一个常量对象,以防止使用该指针来修改所指向的值。如下例:
    1
    2
    
      int iAge = 20;//iAge 可被修改
      const int* p_iAge = &iAge;// *p_iAge 不可修改
    

【注意】 常规指针不能指向常规常量地址!如下图: 常规指针不可指向常量地址 函数参数 尽可能使用 const

  • 将指针本身声明为常量,以防止改变指针指向的位置。示例如下:

const 指针

上图中,在最后一个声明中,关键字 const 的位置与以前不同。这种声明格式使得 finger 只能指向 sloth,但允许使用 finger 来修改 sloth 的 值。中间的声明不允许使用 ps 来修改 sloth 的值,但允许将 ps 指向另一个位置。简而言之,finger 和 *ps 都是 const,而 *finger 和 ps 不是

1
如何区分这两种情况:
  • 根据优先级。* 的优先级高,在这里作为分割符
  • * 右边是指针名和指针类型(判断指针类型时,把指针名去掉),左边是指向的变量的类型
  • 左边变量类型*右边指针类型,相当于变量 <- 指针<-表示指向,即*

注释

注释有//行注释/*注释*/。注释的目的是增加程序可读性,不参与编译运行等程序操作。

转义序列

转义序列是为了输入在键盘上没有的特殊字符或者是那些已经被赋予特定含义的字符,当需要输入这些键盘上没有的字符或需要赋予其非特定意义时,就需要 转义序列协助。

转义序列

函数

函数是一个模块,它描述了与其他模块的交互接口,包括入口(函数名、参数)和出口(返回值),同时实现了入口到出口的处理逻辑(即函数体)。它实际上 是面相过程语言对过程的一种封装,也是被引用的一种功能单元或内存区块。其中内存区块的首地址由函数名指认,区块大小由函数体所占字节确定,而且函数体 的花括号制定了区块的边界。该区块一旦被调用之后就会失去动态特性,即函数体申请的空间都被释放而不能返回(因为其生命周期已经结束)

函数 函数调用 函数调用方法

1
2
不要混淆函数原型和函数定义。函数原型(函数头并加之后随分号)只是描述了函数接口,
而函数定义则包含了函数的代码。

要使用 C++ 函数,必须完成如下工作:

  • 提供函数定义
  • 提供函数原型
  • 调用函数

为什么需要函数原型?原型描述了函数到编译器的接口。原型协助编译器高效捕获少参数或函数不存在等错误。有人会说,为什么不让编译器 自己在源文件搜索,这样就知道了(因为函数原型可以认为是函数头尾随分号形成)。不过这样既低效,编译器在搜索文件的剩余部分时将 必须停止对 main() 的编译。一个更严重的问题的是:函数甚至可能并不在文件中。C++ 允许讲一个程序放在多个文件中,单独编译这些文件, 然后再将它们组合起来(这也是库函数为什么编译好,然后调用库函数要先包含对应的头文件—-函数原型已经写在里面了)。在这种情况下, 编译器在编译 main() 时,可能无权访问函数代码(如库函数无源代码)。

  • 避免使用函数原型的唯一方法是:在首次使用函数之前定义它。(但破坏了自顶向下的设计理念)
  • 避免重复声明函数原型的方法:把函数原型写在不同的头文件中,然后包含进来。(没有用到的函数会被编译器过滤掉)

库函数

库函数是已经定义和编译好的函数,同时可以使用标准库投稳价提供其原型,因此只需正确地调用这种函数即可。

递归函数

如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调用链的内容,通常的方法将递归调用 放在 if 语句中。

递归调用

函数参数和按值传递

函数参数 上图告知,函数参数属于函数的局部变量,可以与其他函数中的变量重名(但作用域可以识别)。

局部变量

1
2
3
当函数参数为数组时,可以用数组名指出该参数是数组,在指明数组的长度。
如:int sum_arr(int arr[], int n);//传递的是数组的首地址
再如:int sum_arr(int *arr, int n);// arr 为数组名

在用指针操作数组时,要记住下面两个恒等式:

  • arr[i] == *(ar + i) // ar 为指向 arr 的指针
  • &arr[i] == ar + i

参数为数组 【注意】:接收数组名参数的函数访问的是原始数组,而不是其副本(只不过用来接收数组名的指针是副本而已)。

1
2
3
void show_array(const int array[], int n);
//加 const 禁止该函数对数组 array 进行修改
void show_array(const int* begin, const int* end);

函数返回值

注意函数返回时只是拷贝了函数内部变量的值,并将其复制到新建的临时变量中,如果不是指针时可以得到想要的内容的,但如果返回的 是指针:

  • 指针指示的内存在栈区(如函数内部的临时数组),那么函数返回时只是复制了一份地址,但该地址指示的存储空间可能找不到或被 系统自动释放或被覆盖。这样就得不到想要的结果了。
  • 指针指示的内存在非堆区(如堆区、全局区等),那么是可以找到相应的存储位置的,返回结果有效。

C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型—整数、指针等,甚至可以是结构和对象(即在函数内部 定义的结构体等也可以返回,不过此时使用的是副本,所以但结构等比较大时,不建议这样做,因为此时复制成副本开销大)!有趣的是, 虽然 C++ 函数不能直接返回数组,但可以将数组作为结构或对象成员部分来返回。

1
2
当结构体等较大时,建议传递地址而不是结构体,以提高效率。
但此时是对```原结构体等```进行操作!

结构

所谓结构指的是程序语句的组织逻辑。一般包括:顺序结构、选择结构、循环结构。这些结构都是可以相互嵌套的!

循环结构

循环实际上和函数类似,也是一种功能代码块,也有入口和出口及内部处理逻辑。只不过其入口和出口在循环块前面和后面。需要注意的, 在循环内部声明的变量在循环块外部是不可用的,和函数内部声明的变量同属于局部变量,受制于花括号(一种作用域界定符)。

  • for 循环

for 循环为执行重复的操作提供了循序渐进的步骤。这些步骤如下:

1
2
3
4
1、设置初始值;
2、执行测试、看看循环是否应当继续进行;
3、执行循环操作;
4、更新用于测试的值(可以借助步长)。

for

1
基于范围的 for 循环

这种基于范围的 for 循环简化了一种常见的循坏任务:对数组(或容器类、如 vector 何 array)的每个元素执行相同操作。示例代码如下:

1
2
3
double prices[5] = {4.99, 10.99, 6.87, 7.88, 8.68};
for (double x : prices)
    cout << x << std::endl;
  • while 循环

while

  • do-while 循环

do while

选择结构

  • if

if

  • if-else

if-else

  • switch

switch

1
为提高可读性,可以结合枚举类型做 case 。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

enum {red, orange, yellow, green, blue, violet, indigo};

int main()
{
    using namespace std;
    cout << "Enter color code (0-6): ";
    int code;
    cin >> code;
    while (code >= red && code <= indigo)
    {
        switch (code)
        {
            case red: cout << "red!\n"; break;
            case orange: cout << "orange!\n"; break;
            case yellow: cout << "yellow!\n"; break;
            case green:  cout << "green!\n"; break;
            case blue: cout << "blue!\n"; break;
            case violet: cout << "violet!\n"; break;
            case indigo: cout << "indigo!\n"; break;
        }
    cout << "Enter color code (0-6): ";
    cin >> code;
    }
    cout << "Bye\n";
    return 0;
}

break 和 continue 语句

Break 和 continue 语句都能够跳过部分代码。

  • 可以在 switch 语句或任何循环中使用 break 语句,使程序跳到 switch 或循环后面的语句处执行。
  • continue 语句用于循环中,让程序跳过循环体重余下的代码,并开始新一轮循环

break 和 continue 上图程序中的 if 语句实际上相当于控制循环入口或出口的测试语句。

1
由上图可知,break 和 continue 的区别。
  • 都是跳过循环体中其后的代码
  • break 跳过之后不继续(即终止)循环,而 continue 则继续新的循环。

头文件

头文件可以看做是支持一组特定功能的工具,它会被包含在源文件或其他头文件中。编译时会原样替换#include指令所在的位置,链接时会查询包含该头文件 的源文件中涉及到的库函数,如果没有包含头文件会编译或链接错误。需要注意的是自己写的头文件要用预处理指令防止被重复包含,否则会出现错误 (可能出现重定义等,主要是作用域导致的)。

头文件

之所以在 C++ 中要使用头文件,最主要的原因是 C++ 的同一个项目可能有多个源代码文件,要命的是这些源代码是分别单独编译的。 也就是说,在编译其中一个文件时,编译器并不知道其它文件中定义的内容,如类、全局变量等。 这就要求我们必须在要使用某个类、函数或变量的每个文件中声明它,否则 C++ 是无法找到它的。

很多文件可能都需要使用同一个函数。假设有一个文件 b.cpp 需要使用这个函数,那么,它必须先声明它,虽然不需要再重写。 如果有很多文件都要使用这个函数,那么这会变得麻烦,特别的,如果你写了一个类,那么你需要维护大量的声明(对于每一个 public 对象), 并且如果你的类的定义发生了改变,你可能不得不改变无数个声明。

所以,C++ 语言提出了头文件的概念。你只需要在头文件中声明一次,在实现文件中定义一次,在所有需要用的文件中,就只需要引用这个头文件,相当于每个文件都包含了一个声明。 为了防止头文件的重复包含,通常应该使用预处理指令 #define (定义符号)、#ifndef(如果没有定义)、#endif(结束判断)来书写头文件的内容。 请理解如下的例子,它是对上个笔记中的 Xiao 类的改进。

命名(名称)空间

为了组织重用代码而引入的。前面我们谈到了头文件,可能出现你包含的两个头文件或源文件中含有同名的 public 方法,那么就产生二义性了,如果你 想都用(可能这同名的方法功能不一样),那怎么办呢?最好的办法就是在方法前加不同的前缀,这样就可以区分了。这样就不怕重用来源不同的代码时出现 命名冲突了。

C++ 中用 using namespace 这个编译指令来实现命名空间的使用。如 using namespace std; 不过若只使用该命名空间的少量方法时,比如使用cout、cin等, 可以:

  • using namespace std::cout;
  • using namespace std::cin;

这样就可以直接使用cout,cin了,否则必须std::cout,std::cin。如果按前面 using namespace std; 这样的话,就会失去命名空间的意义了(无法避免冲突)。

1
自定义命名空间的方法如下:
1
2
3
4
5
namespace nsdebug     //名字空间nsdebug是在别的文件里定义的
{
     int GetStringWidth(char* s);
     int GetCellValue(int x,int y);
}

假设不同命名空间有相同的方法,而且要在不同程序段使用。这样可以推荐两种方法:

  • 命名空间::方法进行区别使用
  • using namespace 命名空间 1 ;//使用命名空间 1 中的方法之后再 using namespace 命名空间 2 ;使用命名空间 2 中的方法

名称空间

变量声明和变量命名规则

声明通常指出了要存储的数据类型和程序对存储在这里的数据使用的名称。事实上,使用声明可以防止拼写错误,一旦拼写错误, 由于该“错误拼写成的变量”没有声明就使用了,编译器会报错,从而提醒编程者出现了拼写错误。

1
变量名命名时一定要做到见名知意,而且要遵守以下规则:
  • 名称中只能含有字符、数字和下划线
  • 名称第一个字符不能是数字
  • 区分大小写字符
  • 不能使用 C++ 关键字用作变量名
  • 以两个下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)使用,以一个下划线开头的名称被保留给实现,用作全局标识符。
  • 名称不易过长,过长会导致输入量变大、而且有些平台对长度有限制。
  • 最好使用前缀表明该变量的类型(如int iCount)

变量名命名时一定要做到见名知意,包括该变量的类型和意义。

1
下面是具体的前缀规则:
数据类型前缀例子
intiint iCount;
shortsshout sValue;
unsigneduunsigned int uiAge;
longllong lValue;
floatffloat fScore;
doubleddouble dValue;
charchchar chChar;
char []szchar szName[30];
stringstrstring strName;
boolbbool bPass;
一级指针p_int *p_iValue;
二级指针pp_int **p2_iValue;
一维数组a_int a_iArray[20]
二级数组a2_int a2_iArray[2][4]
bytebybyte byInfo;
类成员变量m_int m_iValue;
全局变量g_ 
静态变量s_ 
vectorv_vector v_iValue;
enumemenum emWeek;
bitbtbit btBit
structstsruct stStudent;
函数fnvoid fnProc(void);
常量全部大写const int iMAX=100;
指针数组ap_int *p[2];
数组指针pa_int (*p)[2];

类是用户定义的一种数据类型,要定义类,就需要描述它能够表示什么信息和可对数据执行哪些操作。换句话说,类描述了一种数据类型的全部(静态和动态)属性 (包括对这些属性执行的所有操作),对象是根据这些描述创建的实体。

类和对象的关系,就像数据类型和变量的关系。数据类型定义了数据格式,但是没有数据,当然也就不能执行数据操作,可见数据类型是静态的,变量是动态的。

内存空间的划分

  • 栈区(stack,系统提供):

由编译器自动分配释放(如 auto 声明的变量) ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  • 堆区(heap,函数库提供):

一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

1
2
int * pa_iSome = new int [10]; // get a block of 10 ints
delete [] pa_iSome;

当程序使用完 new 分配的内存块时,应使用 delete释放它们。然而,对于 new 创建的数组,应使用另一种格式来 释放(见上面的例子)。方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。

使用 new 的原则

  • 全局区(静态static区):

全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。该内存在程序编译时就已经分配好,并在程序 的整个运行期间都存在。

  • 文字常量区:

常量字符串就是放在这里的。 程序结束后由系统释放。

  • 程序代码区:

存放函数体的二进制代码

1
下面给出识别存储区域的例子:
  • int a=1; a在栈区
  • char s[]=”123”; s在栈区,“123”在栈区,其值可以被修改
  • char *s=”123”; s在栈区,“123”在常量区,其值不能被修改
  • int *p=new int; p在栈区,申请的空间在堆区(p指向的区域)
  • int *p=(int *)malloc(sizeof(int)); p在栈区,p指向的空间在堆区
  • static int b=0; b在静态区

static

1
static 用来控制变量的存储方式和可见性

我们知道存储空间有以下规则:

  • 所属存储区域(如堆、栈等)
  • 访问权限控制(如公有、私有等)
  • 访问方式(如可读、可写)
  • 存储空间大小(定义该空间的边界)
  • 存储空间的首地址(保证可以找到该空间)
  • 存储的内容类型(如整型、字符串等)
  • 初始化方式(如只可初始化一次)。

静态数据成员要在程序一开始运行时就必须存在,因为函数在程序运行中被调用,所以静态数据成员不能再任何函数内分配空间和初始化。 这样,它的空间分配有三个可能的地方:

  • 是作为类的外部接口的头文件,那里有类声明;
  • 是类定义的内部实现,那里有类的成员函数定义;
  • 是应用程序的main()函数前的全局数据声明和定义处。

静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配, 所以在类声 明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时, 要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。

1
2
static函数与普通函数有什么区别:
static函数与普通函数作用域不同,只在定义该变量的源文件内有效;

变量作用域

变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的开括号配对的第一个闭括号。也就是说,作用域由变量所在的最近 一对括号确定。需要提醒的是,文件的首末可以认为是一对花括号。

  • 全局变量(本程序中共享):

全局变量是在所有函数体的外部定义的,程序的所在部分(甚至其它文件中的代码)都可以使用。全局变量不受作用域的影响 (也就是说,全局变量的生命期一直到程序的结束)。如果在一个文件中使用extern关键字来声明另一个文件中存在的全局变量, 那么这个文件可以使用这个数据。

  • 局部变量(作用域为闭花括号为界):

局部变量出现在一个作用域内,它们是局限于一个函数或代码块(用{}包起来,如 for 循环)。局部变量经常被称为自动变量。 它的作用域是从定义开始到最近的闭花括号结束。 变量隐藏

1
2
3
在函数原型作用域中:
1、只在包含参数列表的括号内可用
2、所以这些名称是什么以及是否出现都不重要
  • 寄存器变量:

寄存器变量是一种局部变量。关键字register告诉编译器“尽可能快地访问这个变量”。加快访问速度取决于现实,但是, 正如名字所暗示的那样,这经常是通过在寄存器中放置变量来做到的。这并不能保证将变置在寄存器中,甚至也不能保证提高访问速度。 这只是对编译器的一个暗示。

1
使用 register 变量是有限制的:
  • 不可能得到或计算register 变量的地址;
  • register 变量只能在一个块中声明(不可能有全局的或静态的 register 变量)。然而可以在一个函数中(即在参数表中) 使用 register 变量作为一个形式参数。

一般地,不应当推测编译器的优化器,因为它可能比我们做得更好。因此,最好避免使用关键字 register。

  • 静态变量(只初始化一次,下一次依据上一次结果值):

通常,函数中定义局部变量在函数中作用域结束时消失。当再次调用这个函数时,会重新创建变量的存储空间,其值会被重新初始化。 关键字static有一些独特的意义。

1
静态局部变量

如果想使局部变量的值在程序的整个生命期里仍然存在,我们可以定义函数的局部变量为static(静态的),并给它一个初始化。初始化只在函数第一次调用时执行, 函数调用之间变量的值保持不变,这种方式,函数可以“记住”函数调用之间的一些信息片断。这也就是所谓的静态局部变量,具有局部作用域, 它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的, 而静态局部变量只在定义自己的函数体内始终可见。

static局部变量的优点是在函数范围之外它是不可用的,所以它不可能被轻易改变。这会使错误局部化。

1
静态全局变量(仅本文件中可见)

具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里, 即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。 当 static 全局变量放在头文件中,并且两个源文件都包含了该头文件,那么该 static 全局变量的作用域分别是这两个源文件 (而全局变量将为这两个源文件共享)。

  • 外部变量

extern 告诉编译器存在着一个变量和函数(不论是全局的还是静态全局的),即使编译器在当前的文件中没有看到它。这个变量或函数 可能在一个文件或者在当前文件的后面定义。例如 extern int i; 编译器会知道 i 肯定作为全局变量(或者静态全局)存在于某处。 当编译器看到变量 i 的定义时,并没有看到别的声明,所以知道它在文件的前面已经找到了同样声明的 i 。

1
2
3
外部变量的作用域(决定于它引用的变量作用域):
1、如该变量是全局变量(extern 后不能加 static),则为本程序
2、如该变量是静态全局变量(extern 后可以省略 static),则为本文件
  • const 变量(只读,所以必须定义时初始化)

const 告诉编译器这个名字表示常量,不管是内部的还是用户定义的数据类型都可以定义为 const。如果定义了某对象为常量, 然后试图改变它,编译器将会产生错误。在 C++ 中一个 const 必须有初始值。

const 变量的作用域:决定于它的定义位置,参照“局部变量”“全局变量”

1
2
3
mutable关键字的作用:
可以用它来指出,即使结构体(或类)变量为 const,
用 mutable 关键字修饰的变量的值也是可以改变的。

各类型变量的比较

1
各类型变量(B)的比较,列表如下:
 全局变量静态全局B局部变量静态局部B
静态存储区YYN (栈区)Y
存储位置数一处一文件一处 局部一处
作用域本工程本文件定义->闭}同局部B
定义处初始化 一次 一次
与extern结合Y(不能加static)Y(可省static)NN
默认初始化YYNY
值是否可修改YYYY

我要评论

本文由作者按照 CC BY 4.0 进行授权