2.12 作用域和生命期
作用域是指标识符在程序中的有效范围。生命期是指标识符在程序中的生存周期,也就是在程序运行过程中,标识符在内存中存在的时间。这里的标识符包括变量名、函数名、常量名、对象名、语句标号、宏名等。
2.12.1 作用域
C++的作用域大致可以分为全局作用域、局部作用域和文件作用域三种。还有一种更细的分法,按照作用域范围,作用域从大到小分为程序作用域、文件作用域、类作用域、函数作用域和块作用域5种类型。
(1)程序作用域
程序作用域是指一个标识符在整个程序范围内有效。若一个程序由多个文件组成,具有这种作用域的标识符可以在该程序的各个文件中应用。具有程序作用域的标识符只能在一个文件中定义1次,但要在使用它的文件中用extern声明。例如,如果有10个文件都要用到某个变量,这个变量也只能在1个文件中定义,在另外9个文件中必须用extern声明后才能使用。
(2)文件作用域
文件作用域是指在一个文件中所有函数定义之外定义的名字(包括函数名),其有效范围为从定义它的语句位置开始,直到文件结束。具有文件作用域的名字只能在定义它的文件中使用,但不能在组成同一程序的其他文件中使用。
(3)函数作用域
函数作用域是指在函数范围内的任何语句位置都有效。语句标号是唯一具有函数作用域的标识符。
(4)块作用域
写在{}内的一条或多条语句就构成了一个语句块,在其中定义的标识符就只能在这对“{ }”中使用,而且只在定义(或声明)它的语句位置到离它最近的“}”之间有效,即只能在这段代码区域内引用它,这就是块作用域。
在C++中,任何在“{ }”中定义或声明的标识符都具有块作用域。局限在一个函数内部的标识符都具有块作用域,包括在函数内部定义的变量或对象、函数的形式参数等。
(5)作用域限定符::
在函数中,若局部变量和某个全局变量同名,可用作用域限定符::存取全局变量的值。用法非常简单,就是在变量名前面加上作用域限定符::。
【例2-20】 块作用域及作用域限定符的应用。
//Eg2-20.cpp int i; //L0 int f(){ int i; //L1 i=1; //修改L1定义的i ::i=0; //修改L0定义的i { int j=0; //L2 static k; //L3 i=2; //修改L1定义的i ::i=3; //修改L0定义的i } //j,k的作用域到此结束 j=2; //错误,j已无定义 return k; //错误,k已失去作用域 }
在本例中,最外层的i和函数名f具有文件作用域,可在整个文件中应用。f()内层的i、j、k具有块作用域,只能在包含它的最近一对“{ }”内有效。
if、switch、for以及while之类的复合语句也是一种块语句,在其中(包括在其条件测试语句中)定义的名字具有块作用域,其有效范围是该语句本身。
【例2-21】 下面的程序说明在if语句中定义的变量的作用域。假设在if之前没有i和p的任何说明和定义。
//Eg2-21.cpp if(int i=5) { //i作用域自此开始 int p=0; //p的作用域自此开始 } //p的作用域到此结束 else { i=1; p=2; //错误,p无定义 } //i的作用域到此结束
下面的例子说明switch中定义的变量的作用域。
void f(int i){ switch ( int j=i) { //j的作用域开始于此 case 1: j=j+1; case 2: …… case 3: cout<<j; } //j的作用域到此结束 cout<<j<<endl; //错误,j已无定义 }
对于for和while循环语句,标准C++规定在其循环测试条件中定义的变量,其作用域也限于循环本身,即结束于循环体结束的“}”。按此标准,下面的程序存在错误:
void f1(int z){ for (int i=0;i<z;i++){ int j=i; cout<<i*j<<endl; } //i的作用域到此结束 cout<<i<<endl; //错误,i已无定义 }
但在许多C++编译器中,这段程序能够正确编译和运行,原因是,在标准C++之前,上面的for循环是按如下方式处理的:
int i=0; for (;i<z;i++){ …… }
现在的许多编译器仍按这种方式处理for循环,如Visual C++就是这样的。
2.12.2 变量类型及生命期
根据变量的作用域范围,变量可分为全局变量和局部变量两大类。在函数内部定义的变量就是局部变量(包括函数参数),它们只能在定义它的函数中使用;在函数之外定义的变量(不属于任何函数)就是全局变量,其有效范围从其在文件中的定义位置开始到文件结束。
变量的生命期是指变量在内存中存在的时间,生命期与变量所在的内存区域有关。为了更清楚地理解这个问题,先看看运行程序对内存的应用情况。
一个程序在其运行期间,它的程序代码和数据会被分别存储在4个不同的内存区域,如图2-3所示。
程序代码区:程序代码(即程序的各函数代码)存放在此区域中。
全局数据区:程序的全局数据(如全局变量)和静态数据(static)存放在此区域中。
图2-3 内存区域
栈区:程序的局部数据(在函数中定义的数据)存放在此区域中。
堆区:程序的动态数据(new、malloc就在此区域中分配存储空间)存放在此区域中。
全局数据区中的数据由C++编译器建立,对于定义时没有初始化的变量,系统会自动将其初始化为0。这个区域中的数据一直保存,直到程序结束时才由系统负责回收。
堆区的数据由程序员管理,程序员可用new或malloc分配其中的存储单元给指针变量,用完之后,由程序员用delete或free将其归还系统,以便其他程序使用。
在函数中定义的局部变量(除了static类型的局部变量外,static类型的变量在全局数据区中),只有当函数被调用时,系统才在栈区中为它们分配存储空间,并且不会对分配的存储单元做初始化工作。一旦函数调用完成,系统就会回收这些变量在栈区中的存储单元。
全局变量和静态变量存储在全局数据区中,它们具有较长的生命期。非静态的局部变量存储在栈区中,其生命期很短,只在函数调用期间有效。
静态变量可分为静态全部变量和静态局部变量,前者的作用域是整个程序范围,后者的作用域局限于定义它的语句块。静态局部变量的作用域与普通局部变量的作用域是相同的,但它与全局变量有着同样长的生命期,即程序结束时它才会被释放。普通局部变量的生命期只有函数调用期间才存在,函数调用完成后就结束了。
【例2-22】 静态变量的生存期长于其作用域的例子。
// Eg2-22.cpp #include <iostream.h> static int n; //n被初始化为0 void f(){ static int i; //i被初始化为0 int j=0; i+=2; j+=2; cout<<"i="<<i<<", "; cout<<"j="<<j<<endl; } void main(){ n+=5; f(); //输出i=2,j=2; i=2; //错误,i虽然为static,但其作用域为函数f()内部 f(); //输出i=4,j=2; } //i,n的生命期到此才结束
第1次调用函数f()后,因为i为静态变量,虽然失去了作用域(这就是i=2错误的原因),但却未失去其生存期(即它占据的内存未被系统回收),第2次调用函数f()时,将直接在i对应的存储器中加2,所以结果是4。而j是普通局部变量,第1次调用函数f()后,其作用域和生存期都结束了,第2次调用又重新开始,所以两次调用函数f(),j的输出都是2。
2.12.3 变量初始化
变量初始化的基本原则是:如果定义变量时提供了初始值表达式,系统就用这个表达式的值作为变量的初值;如果定义变量时没有为它提供初值,则全局数据区中的变量将被系统自动初始化为0,栈和堆中的变量不被初始化。
全局变量、命名空间的变量、静态变量会被保存在全局数据区中,所以它们会被系统自动初始化为0;局部变量(也叫自动变量)被存储在栈区中,动态分配的变量(用malloc和new建立)被存储在堆区中,它们都不会被系统用默认值初始化。
【例2-23】 全局变量、静态变量、局部变量的初始化。
//Eg2-23.cpp #include <iostream.h> int n; //初始化为0 void f(){ static int i; //初始化为0 int j; //不被初始化,j值未知 cout<<"i="<<i<<", "; cout<<"j="<<j<<endl; } int *p1; //p1被初始为0 void main(){ int *p2; //p2不被初始化,值未知 int m; //m不被初始化,值未知 f(); //输出i=0,j=?,?表示不确定值 cout<<"n="<<n<<endl; //输出n=0 cout<<"m="<<m<<endl; //输出m=?,?表示不确定值 if(p1) cout<<"p1="<<p1<<endl; //p1=0,无输出 if(p2) cout<<"p2="<<p2<<endl; //输出p2=?,?表示不确定地址 }
2.12.4 局部变量与函数返回地址
弄清楚了局部变量的存储方式和生命期之后,当用指针或引用从函数中返回一个地址时就要小心了,一定不要返回局部变量的指针或引用。
【例2-24】 返回引用的函数。
//Eg2-24.cpp #include<iostream> using namespace std; int &f1(int x){ int temp=x; return temp; } void main(){ int &i=f1(3); cout<<i<<endl; cout<<i<<endl; }
虽然在两条输出i的语句间没有其他语句,但两次输出的结果仍然可能不一致。下面是在Visual C++ 6.0环境下的输出结果:
3 4200045
第2次输出的4200045只是一个随机值而已,就算是其他值也是可以理解的。原因很简单,函数f1()返回了局部变量的temp的地址,函数调用结束后,这个地址就无效了,会再次把这个存储区域分配给谁,它会怎样改写这个存储区域中的内容,这些都不得而知。
同样,如果函数的返回类型是指针,也切忌返回局部变量的地址,它会引发与本例同样的错误。下面的f1()函数就存在这样的问题。
int *f1(){ int temp=1; return &temp; }