第10章 名字控制
创建名字是程序设计过程中一项最基本的活动,当一个项目很大时,它会不可避免地包含大量的名字。
C++允许我们对名字的产生和名字的可见性进行控制,包括这些名字的存储位置以及名字的连接。
static这个关键字早在人们知道“重载”这个词的含义之前就在C语言中被重载了,并且在C++中又增加了另外的含义。关于static的所有使用最基本的概念是指“位置不变的某个东西”(如“静电”),不管这里是指在内存中的物理位置还是指在文件中的可见性。
在本章里,我们将看到static如何控制存储和可见性,还将看到一种通过C++的名字空间特征来控制访问名字的改进方法。我们还将发现怎样使用已经采用C语言编写和编译过的函数。
10.1 来自C语言中的静态元素
在C和C++中,static都有两种基本的含义,并且这两种含义经常是互相冲突的:
1)在固定的地址上进行存储分配,也就是说对象是在一个特殊的静态数据区(static data area)上创建的,而不是每次函数调用时在堆栈上产生的。这也是静态存储的概念。
2)对一个特定的编译单位来说是局部的(就像在后面将要看到的,这在C++中局限于类的范围)。这样,static控制名字的可见性(visibility),所以这个名字在这个单元或类之外是不可见的。这也描述了连接的概念,它决定连接器将看到哪些名字。
本节将着重讨论static的这两个含义,这些都是从C中继承来的。
10.1.1 函数内部的静态变量
通常,在函数体内定义一个局部变量时,编译器在每次函数调用时使堆栈的指针向下移一个适当的位置,为这些局部变量分配内存。如果这个变量有一个初始化表达式,那么每当程序运行到此处,初始化就被执行。
然而,有时想在两次函数调用之间保留一个变量的值,可以通过定义一个全局变量来实现,但这样一来,这个变量就不仅仅只受这个函数的控制。C和C++都允许在函数内部定义一个static对象,这个对象将存储在程序的静态数据区中,而不是在堆栈中。这个对象只在函数第一次调用时初始化一次,以后它将在两次函数调用之间保持它的值。比如,下面的函数每次调用时都返回一个字符串中的下一个字符。
static chars在每次onechar()调用时保留它的值,因为它存放在程序的静态数据区而不是存储在函数的堆栈中。当用一个字符指针作参数(char)调用oneChar()时,参数值被赋给s,然后返回字符串的第一个字符。以后每次调用oneChar()都不用带参数,函数将使用默认参数charArray的默认值0,函数就会继续用以前初始化的s值取字符,直到它到达字符串的结尾标志—空字符为止,到这时,字符指针就不会再增加了,这样,指针不会越过字符串的末尾。
但是,如果调用oneChar()时没有参数而且s以前也没有初始化,那会怎样呢?也许会在定义s时提供一个初始值:
但如果没有为一个内建类型的静态变量提供一个初始值的话,编译器也会确保在程序开始时它被初始化为零(转化为适当的类型),所以在oneChar()中,函数第一次调用时s将被赋值为零,这样if(!s)后面的程序就会被执行。
上例中s的初始化是很简单的,其实对一个静态对象的初始化(与其他对象的初始化一样)可以是任意的常量表达式,常量表达式中可以出现常量及在此之前已声明过的变量和函数。
应该知道:上面的函数很容易产生多线程问题;无论什么时候设计一个包含静态变量的函数时,都应该记住多线程问题。
10.1.1.1 函数内部的静态对象
关于一般的静态变量的规则同样适用于用户自定义的静态对象,而且它同样也必须有初始化操作。但是,零赋值只对内建类型有效,用户自定义类型必须用构造函数来初始化。因此,如果在定义一个静态对象时没有指定构造函数参数,这个类就必须有默认的构造函数。请看下例:
在函数f()内部定义一个静态的X类型的对象,它可以用带参数的构造函数来初始化,也可以用默认构造函数。程序控制第一次转到对象的定义点时,而且只有第一次时,才需要执行构造函数。
10.1.1.2 静态对象的析构函数
静态对象的析构函数(包括静态存储的所有对象,不仅仅是上例中的局部静态对象)在程序从main()中退出时,或者标准的C库函数exit()被调用时才被调用。多数情况下main()函数的结尾也是调用exit()来结束程序的。这意味着在析构函数内部使用exit()是很危险的,因为这样导致了无穷的递归调用。但如果用标准的C库函数abort()来退出程序,静态对象的析构函数并不会被调用。
可以用标准C库函数atexit()来指定当程序跳出main()(或调用exit())时应执行的操作。在这种情况下,在跳出main()或调用exit()之前,用atexit()注册的函数可以在所有对象的析构函数之前被调用。
同普通对象的销毁一样,静态对象的销毁也是按与初始化时相反的顺序进行的。当然只有那些已经被创建的对象才会被销毁。幸运的是,开发工具会记录对象初始化的顺序和那些已被创建的对象。全局对象总是在main()执行之前被创建,在退出main()时销毁。如果一个包含局部静态对象的函数从未被调用过,那么这个对象的构造函数也就不会执行,这样自然也不会执行析构函数。请看下例:
在Obj中,char c的作用就像一个标识符,构造函数和析构函数就可以通过c显示出当前正在操作的对象信息。而Obj a是一个全局的Obj类的对象,所以构造函数总是在main()函数之前就被调用。但函数f()内的Obj类的静态对象b和函数g()内的静态对象c的构造函数只在这些函数被调用时才起作用。
为了说明哪些构造函数与析构函数被调用,在main()中只调用了f(),程序的输出结果为:
在执行main()函数之前,对象a的构造函数即被调用,而b的构造函数只是因为f()的调用而调用。当退出main()函数时,所有被创建的对象的析构函数按创建时相反的顺序被调用。这意味着如果g()被调用,对象b和c的析构函数的调用顺序依赖于g()和f()的调用顺序。
注意跟踪文件ofstream的对象out也是一个静态对象,因为它定义在所有函数之外,位于静态存储区。它的定义(因为不用extern定义)应该出现在文件的一开始,在out的任何可能的使用出现之前,这一点很重要,否则就可能在一个对象初始化之前使用它。
在C++中,全局静态对象的构造函数是在main()之前调用的,所以现在有了一个在进入main()之前执行一段代码的简单的、可移植的方法,并且可以在退出main()之后用析构函数执行代码。在C中要做到这一点,就显得很繁琐,我们将不得不熟悉编译器开发商的汇编语言的开始代码。