C++面向对象程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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;
}