写C、C++代码的小伙伴一定在头疼变量的作用域、生存期、存储类别问题。什么静态、外部、寄存器、局部、全局搞得一头雾水。今天咱们就来梳理一下他们的变态关系(什么不得了的事情???)
1、变量的作用域
说白了,作用域就是一个”代码块“,也就是大括号包裹的那一段东西。包括函数体、控制语句块这些。大家应该都有所耳闻。
#include
int x = 5; // 全局变量
int main() {
printf("%d ",x);
int x = 6; // 局部变量
printf("%d ",x); // 输出结果是5还是6?
return 0;
}
这段代码算很经典了。它展示了不同定义位置的变量的作用域。
首先一个输出肯定是5,毫无悬念。但是下面那个就不同了,因为在main函数中又出来一个同名的局部变量。C++遵循向上覆盖原则,在一个代码块的子块中定义的变量,覆盖掉原块中定义的变量。所以,下面那个输出应该是6。一旦在子块中定义了同名变量,这个块外部的变量就不可见了。所以,实际编程要避免这种情况。
但C++提供了一种叫做“作用域运算符”的东西,也就是::
。这个运算符可以使得全局变量重新可见。比如我在x = 6定以后想再用全局变量的值,就可以这样写:printf("%d", ::x);
这样输出就是5了。这个运算符大家应该都见过,在类和namespace中经常使用。但不管怎么说,除非是在class和namespace中,其它的情况应该严格避免内外作用域的变量重名。
说了这么多,作用域的概念给大家:作用域,就是从变量定义开始到所在代码块或文件结束为止,对编译器可见的范围。
如果对上面这些概念不理解,就往下看,局部变量和全局变量的概念。
2、局部变量和全局变量
局部变量,是指在一个函数内部定义的变量。它的作用域从定义(或声明)开始,到函数结束。它只对函数本身可见,对函数外部不可见。任何手段都无法访问函数内部的变量。因为除了main函数之外,其它函数都不是程序实体,它只提供了一个模块运行的模板,里面的变量只是为了这个函数服务,函数外部引用它没有意义。
为了方便往下讲,先说一下生存期的概念。作用域是个静态概念,表示变量的可见范围,是对编译器而言的,而生存期是动态概念,表示变量在内存中的创建、使用、销毁过程,是运行时概念。生存期是指变量从创建到被操作系统回收的这一段时间。缩句的话,作用域是代码范围,生存期是时间范围。
回到正题,局部变量的生存期,就是从函数创建它开始,到函数调用完毕,被操作系统回收这一段时间。
我们知道,一个进程有一段栈空间,函数调用的过程是用栈管理的。栈保存函数的参数、局部变量、返回地址。当一个函数被调用时,CPU首先把当前执行地址保存到一个特殊寄存器中,作为这一函数的返回地址。然后,CPU跳转到函数入口地址,栈顶指针扩展一帧,栈空间随之增长。把刚才的返回地址保存到栈中,并从栈中加载参数。之后就是执行过程。执行完毕后,从栈中取出返回地址,CPU跳回这一地址。最后栈顶指针回退一帧,这一函数的栈空间将被释放,局部变量也就随之销毁。
上面这一段看不懂也没关系,如果你们学过汇编语言和操作系统,就会明白这是一个怎样的过程。总之,需要记住局部变量是在函数调用并执行到定义语句时创建,函数返回时销毁。
全局变量,是指不在任何函数内定义的变量。它定义在文件的顶层,对任何函数都可见(我们从现在起,假设任何函数中没有和全局变量重名的局部变量)。
全局变量的作用域是从定义开始到这个文件结束。其实这种说法不精确,因为全局变量可以被其他文件调用,这就是外部变量,我们稍后再讲。
和局部变量不同,全局变量是在进程创建(注意进程创建和main函数调用是两个概念,进程创建包括代码加载、数据加载、内存空间分配过程,是操作系统完成的,而main函数是进程调用的)时同时创建的。所以,全局变量在main函数调用之前就已经存在了。一个进程的地址空间分为代码段、数据段、用户段,代码段就是机器指令(参看冯诺依曼体系结构),数据段就是我们所说的全局变量,而用户段是供进程运行过程中动态分配内存的。我们刚才说的栈空间就在用户段。所以说,全局变量的存储位置和局部变量完全不同。
全局变量又分为已初始化的和未初始化的。已初始化的全局变量,操作系统会自动为其初始化值,放在数据段前部,而未初始化的全局变量,则会放在数据段的后部,并自动清零。所以,你看到未初始化的全局变量初始值都是0。
全局变量的生存期从进程创建开始,一直到进程运行完毕,所有内存被操作系统回收位置为止。
另外,在不是函数的代码块中创建的局部变量,也是类似的。比如
int s = 0;
for (int i = 0; i < 10; i++) {
s += i;
int t = s;
}
printf("%d %d %d", i, s, t); // 错误,i和t只对for循环体可见
3、变量存储类别
说完了生存期、作用域的概念,我们再来看变量存储类别。
变量的存储类别分为自动(auto),静态(static),外部(extern)和寄存器(register)四种。
3.1、自动变量
注意,虽然叫auto变量,但是,auto关键字在C++11中已经不再是“自动变量”的意思,而是“自动类型推断”。所以,不要试图用auto关键字来创建自动变量。
比如:
auto a = 1;
printf("%d\n", a); // a的类型自动推断为整型
auto int b = 2; // 错误,auto是指自动类型推断,不可以与类型标识符连用
printf("%d\n", b);
我们刚才所说的变量,都是自动变量。所谓自动变量,就是按照操作系统的内存分配和回收规则来管理的变量,不需要程序员干预,动态管理。如果是局部变量,则由栈空间保存,全局变量则由数据段保存。
3.2、静态变量和外部变量
这个是难点中的难点,大家一定要仔细看。
3.2.1、静态局部变量
我们刚才说的局部变量,是自动局部变量,也就是说作用域和生存期是捆绑的,一旦出了作用域,则销毁,不再可见。而静态局部变量则不同。它和全局变量一样,是放在数据段中的,只初始化一次,下次调用这个函数时,保留原来的值,继续使用。如下:
int count() {
static int cnt = 0;
return ++cnt;
}
如果cnt是个自动局部变量,则每次调用函数的时候,都要初始化为0,所以每次的返回值都是1。但定义成静态局部变量就不同了,它在函数调用结束后并不销毁,保留原来的值,下一次调用时,初始化语句是不起作用的,所以它返回的是函数被调用的次数。
本质上,静态局部变量和全局变量的生存期完全相同,只是作用域不同。刚才说了,作用域是相对于编译器来说的,所以静态局部变量编译器提供的“语法糖”,为避免全局变量重名造成干扰而引入的机制。
3.2.2、外部变量和静态全局变量
在讲静态全局变量之前,我们来先讲一下外部变量。
我们刚才说的全局变量,仅仅是对本文件可见吗?No,它对其他文件也可见。我们编译C++程序的时候,往往不止一个源文件,如果1.cpp要调用2.cpp的某个全局变量,怎么办呢?
答案就是,使用extern声明。比如main.cpp的内容:
#include
int a[20];
void operate(); // 函数声明
int main() {
operate();
for (int i = 0; i < 20; i++) {
printf("%d ",a[i]);
}
}
operate.cpp中内容:
extern int a[20]; // extern声明
void operate() {
a[0] = 0;
a[1] = 1;
for (int i = 2; i < 20; i++) {
a[i] = a[i-1] + a[i-2];
}
}
编译命令:g++ main.cpp operate.cpp -o main.exe
运行结果:0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
是不是很神奇?就像一个文件调用其他文件定义的函数一样,其他文件的全局变量也是可以使用的,只不过需要声明为extern。所以,外部变量是这样一个概念。把它类比成函数声明就行。
说完了外部变量,我们来说静态全局变量。刚才说到,全局变量可以被其他文件所引用,如果我们不想让它被其他文件引用,怎么办?答案就是,把这个全局变量加上static修饰。这时,static已经不再是静态的概念,而是阻止其它文件调用的意思。所以,静态全局变量这个称呼太过直译,有点误导。正确的叫法是文件内变量。
最初C++团队想增加一个intern关键字来表示这种变量,但是遭到广大程序员强烈反对,本着关键字能少即少的原则,就对static进行了“重载”,赋予了这么一个功能。其实我觉得还不如直接叫intern,static总是让人误会。
static也可以修饰函数,这表示,其他文件不可调用这个函数。如果把operate.cpp中的operate()
定义成static void operate()
,则编译报错。
3.3、寄存器变量
说完了最难的,咱们来放松一下。寄存器是CPU的一些存储部件,它用来存放立即就要参加CPU运算的数据。它的读写速度是纳秒级别,比内存、缓存都快至少2~3个数量级。
用register关键字声明的变量,叫做寄存器变量。表示,通知编译器把这个变量直接放在寄存器中而不是内存中,对一些频繁访问的变量,这样可以加快速度。但是,仅仅是通知编译器这样做,而编译器可能不会理会你的声明,而仍然把变量放在内存中。因为寄存器个数非常少,我们PC机的64位x86 CPU,只有8个通用寄存器,其中还有两个不是存放普通数据的。即使是寄存器较多的MIPS CPU,也只有32个。
另外需要注意的一点,就是寄存器变量不可以使用取地址运算符&,也不可以用指针指向它。因为寄存器没有地址,指针只能保存内存地址值,而不能保存寄存器。
4、类中的static
4.1、静态字段
类中的字段分为普通字段和静态字段。普通字段在类对象创建时被创建,它的生存期和类对象是捆绑的,同生共死。而静态字段,不依赖于对象创建,它被保存在数据段中。引用静态字段,可以用成员运算符,也可以用作用域运算符。
class Point {
private:
int x;
int y;
public:
static int cnt; // 其实这是不安全的,容易被篡改,应该设为private。但是作为一个例子来讲解静态字段
Point(int xx, int yy) {
cnt++; // 对当前拥有的对象个数计数
x = xx;
y = yy;
}
~Point() {
cnt--;
}
};
static int Point::cnt = 0; // 静态变量初始化必须在类外进行,除非它是const的
int main() {
printf("%d ", Point::cnt); // 类名访问
Point p(3, 4);
printf("%d ", p.cnt); // 对象名访问
Point *pp = new Point(5, 6);
printf("%d ", pp->cnt); // 指针访问
delete pp;
printf("%d ", Point::cnt);
return 0;
}
4.2、静态方法
静态方法和普通方法不同,它也是所有类对象共享的。可以通过类名调用,也可以通过对象名调用。把刚才的类改一下:
class Point {
private:
int x;
int y;
static int cnt;
public:
Point(int xx, int yy) {
cnt++; // 对当前拥有的对象个数计数
x = xx;
y = yy;
}
static int getCnt() {
return cnt;
}
/*
static int getX() {
return x;
}
*/ // 这个函数是错误的,静态方法不可引用非静态字段
~Point() {
cnt--;
}
};
static int Point::cnt = 0;
int main() {
printf("%d ", Point::getCnt()); // 类名访问
Point p(3, 4);
printf("%d ", p.getCnt()); // 对象名访问
Point *pp = new Point(5, 6);
printf("%d ", pp->getCnt()); // 指针访问
delete pp;
printf("%d ", Point::getCnt());
return 0;
}
注意,静态方法不可访问非静态字段,也不可调用非静态方法。
4.3、静态内部类
内部类分为普通内部类和静态内部类两种。它们唯一的不同就是,普通内部类是外部类的“奴隶”,它不仅受制于外部类,而且一切字段不能对外部类隐藏,即使是private。而静态内部类则不同,相当于放在某个类内部的外部类,private字段是不可访问的。所以静态内部类也叫嵌套类。和Java是一样的。
比如:
class aaa {
private:
class bbb {
private:
int a;
};
static class ccc {
private:
int a; // a对外不可见
};
public:
bbb bb;
ccc cc;
int bbbb() {
return bb.a; // 正确
}
int cccc() {
return cc.a; // 错误
}
};