6.7.2 静态存储(编译器预分配)

    每个C++程序对应着一块静态数据区(也称全局数据区)。在源程序编译时,编译器就为某些程序实体(某些变量及所有的常量)预分配存储地址和内存空间,程序一开始执行,这些程序实体就被创建,一直到程序结束才被撤销,并释放对应的内存空间,因此称其为“永久存储”。静态存储的程序实体的数目在程序运行过程中是不变的,因此无须使用特殊的机制(如堆、栈)来管理它们,编译器将分配固定的内存块来存储所有的静态存储实体。

    常量已经在第2章进行了详细的介绍,下面对静态存储的变量进行讨论,根据用法不同,静态存储的变量可分为全局变量和静态变量。

    1.extern关键字

    前面已经提及,extern是C++提供的存储类声明符,用extern声明的变量称为全局变量。从字面上看,这意味着该变量可以在程序的任意位置使用,全局变量是在函数和类外定义的,所以也称为外部变量。

    与自动变量不同的是,全局变量的声明有两种形式,即定义性声明和引用性声明。定义性声明用于创建变量,基本形式如下所示。


    extern类型变量名=初始化表达式;此时,初始化表达式不可省略,此指令通知编译器在静态数据区中开辟一块指定类型大小的内存区域,用于存储该变量,下列语句创建了一个初始值为10的double型全局变量total。 extern double total=10;存储类extern说明符也可以省略,只要是在外部(不属于任何一个函数或类)定义的变量,编译器就将其当做全局变量。在外部定义时上述代码等价于下面的代码。 double total=10;

    注意

    只有当在定义性声明中省略了extern时,初始化表达式才可省略,系统默认将其初始化为0,对于定义的全局数组或结构,编译器将其中的每个元素或成员的所有位都初始化为0。

    在使用全局变量之前,需要对其进行引用性声明,这和函数的用法有一点类似,引用性声明只有一种格式,如下所示。


    extern类型变量名;

    请注意,这里没有初始化语句,引用性声明用于告诉编译器变量是全局的这一特征,引用性声明不只局限在外部,只要在使用全局变量之前对其进行声明即可。

    注意

    同函数一样,如果定义在前,而使用在后,且定义和使用在同一文件(注意程序和文件的差别)中,引用性声明可以省略。

    定义性声明和引用性声明主要有以下3点区别。

    ❑定义性声明只能有一次,而引用性声明可以有多次。

    ❑定义性声明一定是在外部,而引用性声明的位置没有限制,只要在使用前对全局变量声明使其可见即可,关于可见性的内容,请参考本章稍后的介绍。

    ❑定义性声明有两种形式:带extern的形式(不可缺少初始化语句)和省略extern的形式(可省略初始化语句,编译器默认初始化),而引用性声明只有带extern但不带初始化表达式一种形式。

    代码6.15演示了全局变量的定义性声明和引用性声明。

    代码6.15 全局变量的定义性声明和引用性声明GlobalVariable


    <————————————-文件名:example615.cpp——————————————-> 01 #include<iostream> 02 using namespace std; 03 int main() 04 { 05 extern int num1;//引用性声明 06 cout<<"num1:"<<num1<<endl; 07 void change();//函数声明 08 change();//函数使用 09 cout<<"num1:"<<num1<<endl; 10 return 0; 11 } 12 int num1;//定义性声明 <————————————-文件名:func.cpp—————————————————> 13 extern int num1;//引用性声明 14 void change()//函数定义 15 { 16 num1+=3; 17 }

    输出结果如下所示。


    num1:0 num1:3

    【代码解析】代码由两个程序文件组成,代码第12行,定义全局变量num1,其放在example615.cpp中,func.cpp中函数change()对num1进行了加3处理,在使用num1前,func.cpp的第一句便外部引用声明了全局变量num1,这样,便可以在func.cpp内的任何位置使用num1。

    num1的定义性声明省略了extern和初始化语句,编译器自动将其初始化为0,在main()函数内,访问num1之前同样要对其进行引用声明,可以看出,没有传递任何参数,change()函数便实现了对num1的修改和访问,这也提供了一种在程序文件间进行数据通信的途径。注意,对全局变量引用声明的时候,不能给初值。例如,在代码6.15中,如果将func.cpp中的引用性声明改为如下所示:

    extern int num1=10;

    这便不再是引用性声明语句,而成了int型全局变量的定义,编译器会为其在静态数据区分配内存。实际上,example615.cpp中已经定义了全局变量num1,编译器会给出变量重复定义的错误。

    注意

    首先,全局变量虽然方便了数据的共享,但其破坏了程序的封装型,使程序读起来比较困难。其次,全局变量占据的空间无法释放,一定程度上浪费了内存资源。最后,滥用全局变量容易造成名字冲突和程序错误。因此,应尽量少用全局变量。

    2.static关键字

    static也是C++提供的存储类声明符,使用static声明的变量称为静态变量,同全局变量一样,静态变量也是永久存储的。

    静态变量与全局变量的区别在于以下两点。

    ❑没有定义性声明和引用性声明之分,只有声明语句。静态变量应在使用前进行声明,编译器会根据声明语句,在静态数据区为静态变量分配内存空间。

    ❑既可以在外部声明,又可以在内部声明。当在外部声明时(外部静态变量),所声明的标识符只限于一个文件中的函数共享;当在内部声明时(内部静态变量),所声明的标识符仅限于所在的代码块内使用,内部静态变量与自动变量的唯一区别在于内部静态变量具有永久生存期。

    静态变量声明的基本形式如下所示。


    static类型变量名=[初始化表达式];

    注意

    初始化表达式可以省略,此时系统默认将其初始化为0;对于定义的全局数组或结构,编译器将其中的每个元素或成员的所有位都初始化为0。

    先来看一个例子,见代码6.16。

    代码6.16 静态变量的应用StaticVariable1


    <—————————————文件名:example616.cpp——————————————> 01 #include<iostream> 02 using namespace std; 03 static int num1=12;//声明 04 int main() 05 { 06 cout<<"num1:"<<num1<<endl; 07 void change();//函数声明 08 change();//函数使用 09 cout<<"num1:"<<num1<<endl; 10 return 0; 11 } <—————————————-文件名:hs.cpp—————————————————-> 12 #include<iostream> 13 using namespace std; 14 void change()//函数定义 15 { 16 static int num1;//声明 17 num1+=3; 18 cout<<"num1:"<<num1<<endl; 19 }

    输出结果如下所示。


    num1:12 num1:3 num1:12

    【代码解析】在example616.cpp第3行,声明的静态变量num1的作用范围仅限于本文件,代码第16行,即hs.cpp中的“static int num1;”相当于声明了另一个静态变量num1,其作用范围仅限于change函数内。两个静态变量都位于静态存储区,编译器通过绝对地址的不同对其加以区分。

    试试看将change函数定义在main()函数后,编译输出会是什么结果呢?

    可以发现,将change()函数定义在main()函数后,输出结果并没有发生变化。这是因为change()函数声明的内部静态变量屏蔽了外部静态变量,这是由变量的可见性决定的,在稍后的章节中将介绍可见性的相关内容。

    对内部静态变量而言,在函数两次调用之间,静态变量的值是能保持的(保存的),代码6.17是内部静态变量的使用范例。

    代码6.17 内部静态变量的应用StaticVariable2


    <———————————-文件名:example617.cpp———————————————-> 01 #include<iostream> 02 using namespace std; 03 int main() 04 { 05 int num1=5; 06 void add(int); 07 add(num1); 08 add(num1); 09 add(num1); 10 return 0; 11 } 12 void add(int n) 13 { 14 static int m=1;//add()函数内部定义的静态变量m,在函数两次调用的间隔保持其值 15 m+=n; 16 cout<<"m:"<<m<<endl; 17 }

    输出结果如下所示。


    m:6 m:11 m:16

    【代码解析】在代码第14行,用static修饰的内部变量m在函数add()调用的间隔里保持着数据,换句话说,对静态变量的初始化在程序刚开始执行的时候已然完成,程序中对该变量的写入和访问都会一直存储在静态存储区中。

    静态变量不像全局变量那样有定义性声明和应用性声明之分,凡是声明语句都将在静态数据区为其分配空间。因此,应避免同一可见域的重复声明。

    注意

    对于外部静态变量,推荐声明在文件前面(#include语句后),而对内部静态变量,推荐声明在代码块的最前面。

    静态变量的名字不影响其他文件中的同名变量,其提供了隐藏程序实体的一种手段,程序员在设计不同的文件时,不必担心它会和其他文件中的标识符发生冲突。

    3.全局变量和静态变量的初始化

    对于全局变量和静态变量,允许在其创建时对它进行显式初始化,系统默认将其初始化为0。对于定义的全局数组或结构,编译器将其中的每个元素或成员的所有位都初始化为0。

    需要注意的是,只能使用常量表达式来初始化全局变量和静态变量。常量表达式包括第2章中介绍的直接常量、const常量、枚举常量和sizeof()运算符。下面的初始化代码都是合法的。


    int num;//编译器自动将num初始化为0 int num1=20;//直接常量 const int x=10; int num2=x;//const常量 int num3=sizeof(double);//sizeof运算符

    不能使用变量来初始化全局变量和静态变量,因为全局变量和静态变量的内存空间是在程序刚开始执行就开辟的,初始化也是在这时完成的,此时,变量的值是未知的,变量的内存空间甚至还没有被分配。

    试试看能否用一个全局变量(或静态变量)对另一个全局变量(或静态变量)赋值呢?

    C++允许全局变量和静态变量间的相互赋值,前提是该变量可见。