第9章 内存模型和名称空间

本章内容包括:

● 单独编译。

● 存储持续性、作用域和链接性。

● 布局(placement) new操作符。

● 名称空间。

C++为在内存中存储数据方面提供了多种选择。可以选择数据保留在内存中的时间长度(存储持续性)以及程序的哪一部分可以访问数据(作用域和链接)等。可以使用new来动态地分配内存,而布局new操作符提供了这种技术的一种变种。C++名称空间是另一种控制访问权的方式。通常,大型程序都由多个源代码文件组成,这些文件可能共享一些数据。这样的程序涉及到程序文件的单独编译,本章将首先介绍这个主题。

9.1 单独编译

和C语言一样,C++也允许甚至鼓励程序员将组件函数放在独立的文件中。第1章介绍过,可以单独编译这些文件,然后将它们链接成可执行的程序(通常,C++编译器既编译程序,也管理链接器)。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。另外,大多数C++环境都提供了其他工具来帮助管理。例如,UNIX和Linux系统都具有make程序,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行make时,如果它检测到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。Borland C++和Microsoft Visual C++和Metrowerks CodeWarrior IDE (integrated development enviroment,集成开发环境)在Project菜单中提供了类似的工具。

现在看一个简单的范例。我们不是要从中了解编译的细节(这取决于实现),而是要重点介绍更通用的方面,如设计。

例如,假设程序员决定分解程序清单7.12中的程序,将支持函数放在一个独立的文件中。清单7.12将直角坐标转换为极坐标,然后显示结果。不能简单地以main()之后的虚线为界,将原来的文件分为两个。问题在于,main()和其他两个函数使用了同一个结构声明,因此两个文件都应包含该声明。简单地将它们输入进去无疑是自找麻烦。即使正确地复制了结构声明,如果以后要作修改,则必须记住对这两组声明都进行修改。简而言之,将一个程序放在多个文件中将引出新的问题。

谁希望出现更多的问题呢?C和C++的开发人员都不希望,因此他们提供了#include来处理这种情况。与其将结构声明加入到每一个文件中,不如将其放在头文件中,然后在每一个源代码文件中包含该头文件。这样,要修改结构声明时,只需在头文件中做一次改动即可。另外,也可以将函数原型放在头文件中。因此,可以将原来的程序分成三部分:

● 头文件:包含结构声明和使用这些结构的函数的原型。

● 源代码文件:包含与结构有关的函数的代码。

● 源代码文件:包含调用与结构相关的函数的代码。

这是一种非常有用的组织程序的策略。例如,如果编写另一个程序时,也需要使用这些函数,则只需包含头文件,并将函数文件添加到工程列表或make列表中即可。另外,这种组织方法也反映了OOP方法。一个文件(头文件)包含了用户定义类型的定义;另一个文件包含操纵用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序中。

请不要将函数定义或变量声明放到头文件中。这样做对于简单的情况可能是可行的,但通常会引来麻烦。例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。下面列出了头文件中常包含的内容:

● 函数原型。

● 使用#define或const定义的符号常量。

● 结构声明。

● 类声明。

● 模板声明。

● 内联函数。

将结构声明放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性(稍后将介绍),因此可以将其放在头文件中,而不会引起问题。

程序清单9.1、9.2和9.3是将程序清单7.12分成几个独立部分后得到的结果。注意,在包含头文件时,我们使用“coordin.h”,而不是<coodin.h>。如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的头文件时,应使用引号而不是尖括号。

图9.1简要地说明了在UNIX系统中将该程序组合起来的步骤。注意,只需执行编译命令CC即可,其他步骤将自动完成。g++和gpp命令行编译器以及Borland C++命令行编译器(bcc32.exe)的行为类似。Borland C++、Turbo C++、Metrowerks CodeWarrior和Microsoft Visual C++基本上执行同样的步骤,但正如第1章介绍的,启动这个过程的方式不同——使用能够创建工程并将其与源代码文件关联起来的菜单。注意,只需将源代码文件加入到工程中,而不用加入头文件。这是因为#include指令管理头文件。另外,不要使用#include来包含源代码文件,这样做将导致多重声明。

警告:在IDE中,不要将头文件加入到工程列表中,也不要在源代码文件中使用#include来包含其他源代码文件。

程序清单9.1 coordin.cpp

image280_1

image281_1

image281_2

图9.1 在UNIX系统中编译由多个文件组成的C++程序

头文件管理

在同一个文件中只能将同一个头文件包含一次。记住这个规则很容易,但很可能在不知情的情况下将头文件包含多次。例如,可能使用包含了另外一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即if not defined)的。下面的代码片段:

image281_3

意味着仅当以前没有使用预处理器编译指令#define定义名称COORDINH时,才处理#ifndef和#endif之间的语句。

通常,使用#define语句来创建符号常量,如下所示:

define MAXIMUM 4096

但只要#define用于名称,就足以完成该名称的定义,如下所示:#define COORDINH

程序清单9.1使用这种技术是为了将文件内容包含在#ifndef中:

image281_4

编译器首次遇到该文件时,名称COORDINH没有定义(我们根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称)。在这种情况下,编译器将查看#ifndef和#endif之间的内容(这正是我们希望的),并读取定义COORDINH的一行。如果在同一个文件中遇到其他包含coordin.h的代码,编译器将知道COORDINH已经被定义了,从而跳到#endfi后面的一行上。注意,这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。大多数标准C和C++头文件都使用这种防护(guarding)方案。

程序清单9.2 file1.cpp

image282_1

程序清单9.3 file2.cpp

image282_2

将这两个源代码文件和新的头文件一起进行编译和链接,将生成一个可执行程序。下面是该程序的运行情况:

image283_1

顺便说一句,虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++标准使用了术语翻译单元(translation unit),而不是文件;文件并不是计算机组织信息时的惟一方式。

多个库的链接

C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰(参见第8章),因此由不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。也就是说,两个编译器将为同一个函数生成不同的修饰名称。名称的不同将使链接器无法将一个编译器生成的函数调用与另一个编译器生成的函数定义匹配。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。

9.2 存储持续性、作用域和链接性

介绍过多文件程序后,接下来扩展第4章对内存方案的讨论,即存储类别如何影响信息在文件间的共享。现在读者阅读第4章已经有一段时间了,因此先复习一下有关内存的知识。C++使用3种不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。

● 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。

● 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。

● 动态存储持续性:用new操作符分配的内存将一直存在,直到使用delete操作符将其释放或程序结束为止。这种内存的存储持续性为动态,有时又称为自由存储(free store)。

下面介绍其他内容,包括关于各种变量何时在作用域内或可见(可被程序使用)以及链接性的细节。链接性决定了哪些信息可在文件间共享。

9.2.1 作用域和链接

作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。例如,函数中定义的变量可在该函数中使用,但不能在其他函数中使用;而在文件中的函数定义之前定义的变量则可在所有函数中使用。链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

C++变量的作用域有多种。作用域为局部的变量只在定义它的代码块中可用。代码块是由花括号括起的一系列语句。例如函数体就是代码块,但可以在函数体中嵌入其他代码块。作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数原型作用域(function prototype scope)中使用的名称只在包含参数列表的括号内可用(这就是为什么这些名称是什么以及是否出现都不重要的原因)。在类中声明的成员的作用域为整个类(参见第10章)。在名称空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例)。

C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己是可见的,因此不能被其他函数调用。这样的函数将无法运行)。

不同的C++存储方式是通过存储持续性、作用域和链接性来描述的。下面来看看各种C++存储方式的这些特征。首先介绍引入名称空间之前的情况,然后看一看名称空间带来的影响。

9.2.2 自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。也就是说,如果在main()中声明了一个名为texas的变量,并在函数oil()中也声明了一个名为texas变量,则创建了两个独立的变量——只有在定义它们的函数中才能使用它们。对oil()中的texas执行的任何操作都不会影响main()中的texas,反之亦然。另外,当程序开始执行这些变量所属的代码块时,将为其分配内存;当函数结束时,这些变量都将消失(注意,执行到代码块时,将为变量分配内存,但其作用域的起点为其声明位置)。

如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。例如,假设在main()的开头定义了一个名为teledeli的变量,然后在main()中开始一个新的代码块,并在其中定义了一个新的变量websight,则teledeli在内部代码块和外部代码块中都是可见的,而websight就只在内部代码块中可见,它的作用域是从定义它的位置到该代码块的结尾:

image284_1

然而,如果将内部代码块中的变量命名为teledeli,而不是websight,使得有两个同名的变量(一个位于外部代码块中,另一个位于内部代码块中),情况将如何呢?在这种情况下,程序执行内部代码块中的语句时,将teledeli解释为局部代码块变量。我们说,新的定义隐藏了(hide)以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义又重新可见(参见图9.2)。

程序清单9.4表明,自动变量只在包含它们的函数或代码块中可见。

程序清单9.4 auto.cpp

image284_2

image285_1

image285_2

图9.2 代码块和作用域

下面是该程序的输出:

image285_3

在程序清单9.4中,3个texas变量的地址各不相同,而程序使用当前可见的那个变量,因此将113赋给oil()中的内部代码块中的texas,对其他同名变量没有影响。同样,实际的地址值和地址格式随系统而异。

现在总结一下整个过程。执行到main()时,程序为texas和year分配空间,使得这些变量可见。当程序调用oil()时,这些变量仍留在内存中,但不可见。为两个新变量(x和texas)分配内存,从而使它们可见。在程序执行到oil()中的内部代码块时,新的texas将不可见,它被一个更新的定义代替。不过,变量x仍然可见,这是因为该代码块没有定义x变量。当程序流程离开该代码块时,将释放最新的texas使用的内存,而第二个texas再次可见。当oil()函数结束时,texas和x都将过期,而最初的texas和year再次变得可见。

可以使用C++(和C)关键字auto来显式地指出存储类别:

image286_1

由于只能将关键字auto用于默认状态下为自动的变量,因此程序员几乎不使用它。有时候,它被用来向读者澄清代码的含义。例如,可以借助于表明,程序中特意创建了一个自动变量,它将覆盖全局定义(如本章后面的“静态持续性、外部连接性”一节将讨论的变量)。

1.自动变量的初始化

可以使用任何在声明时其值为已知的表达式来初始化自动变量,下面的范例初始化变量x、yz

image286_2

2.自动变量和堆栈

了解典型的C++编译器如何实现自动变量有助于更深入地了解自动变量。由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,并将其视为堆栈,以管理变量的增减。之所以被称为堆栈,是由于新数据被象征性地放在原有数据的上面(也就是说,在相邻的内存单元中,而不是在同一个内存单元中),当程序使用完后,将其从堆栈中删除。堆栈的默认长度取决于实现,但编译器通常提供改变堆栈长度的选项。程序使用两个指针来跟踪堆栈,一个指针指向栈底——堆栈的开始位置,另一个指针指向堆顶——下一个可用内存单元。当函数被调用时,其自动变量将被加入到堆栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

堆栈是LIFO(后进先出)的,即最后加入到堆栈中的变量首先被弹出。这种设计简化了参数传递。函数调用将其参数的值放在栈顶,然后重新设置栈顶指针。被调用的函数根据其形参描述来确定每个参数的地址。例如,图9.3表明,函数fib()被调用时,传递一个2字节的int和一个4字节的long。这些值被加入到堆栈中。当fib()开始执行时,它将名称real和tell同这两个值关联起来。当fib()结束时,栈顶指针重新指向以前的位置。新值没有被删除,但不再被标记,它们所占据的空间将被下一个将值加入到堆栈中的函数调用所使用(图9.3做了简化,因为函数调用可能传递其他信息,如返回地址)。

3.寄存器变量

和C语言一样,C++也支持使用register关键字来声明局部变量。寄存器变量是另一种形式的自动变量,因此其存储持续性为自动,作用域为局部,但没有链接性。关键字register提醒编译器,用户希望它通过使用CPU寄存器,而不是堆栈来处理特定的变量,从而提供对变量的快速访问。这里的理念是,CPU访问寄存器中的值的速度比访问堆栈中内存快。要声明寄存器变量,请在类型前加上关键字register:

register int count_fast: // request for a register variable

image287_1

图9.3 使用堆栈传递参数

读者可能注意到了,这里使用了“提醒”和“请求”。编译器并不一定会满足上述请求。例如,寄存器可能已经被占用,或者寄存器无法存储所请求的类型。许多程序员觉得,现代的编译器已经足够聪明,不需要提醒。例如,编写for循环时,编译器可能自动使用寄存器来存储循环计数。

如果变量被存储在寄存器中,则没有内存地址,因此不能将地址操作符用于寄存器变量。在下面的代码中,获取变量x的地址是可行的,但获取变量y的地址是错误的:

image287_2

在声明中使用register后,这种限制便会生效,即使编译器实际上并没有使用寄存器来存储该变量。

简而言之,常规局部变量、使用auto声明的局部变量以及使用register声明的局部变量的存储持续性都是自动的,作用域都是局部的,也都没有链接性。下面的代码说明了这3种情况:

image287_3

声明局部变量时,如果没有使用说明符,则与使用auto声明变量等效。通常,处理这种变量的方式是将其放置到内存堆栈中。使用register说明符指出该变量将被频繁使用,编译器可能会选择使用内存堆栈之外的其他方式(如使用CPU寄存器)来存储它。

9.2.3 静态持续变量

和C语言一样,C++也为静态存储持续性变量提供了3种链接性:外部链接性、内部链接性和无链接性。这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如堆栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器就将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。

注意:传统的K&R C不允许初始化自动数组和结构,但允许初始化静态数组和结构。ANSI C和C++允许对这两种数组和结构进行初始化,但有些旧的C++翻译器使用与ANSI C不完全兼容的C编译器。如果使用的是这样的实现,则可能需要使用这3种静态存储类型之一,以初始化数组和结构。

下面介绍如何创建这3种静态持续变量,然后介绍它们的特点。要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。下面的代码片段说明这3种变量:

image288_1

正如前面指出的,所有静态持续变量(上述范例中的global、one_file和count)在整个程序执行期间都存在。在funct1()中声明的变量count的作用域为局部,没有链接性,这意味着只能在funct1()函数中使用它,就像自动变量llama一样。但是,与llama不同的是,即使在funct1()函数没有被执行时,count也留在内存中。global和one_file的作用域都为整个文件,即在从声明位置到文件结尾的范围内都可以被使用。具体地说,可以在main()、funct1()和funct2()中使用它们。由于one_file的链接性为内部,因此只能在包含上述代码的文件中使用它;由于global的链接性为外部,因此可以在程序的其他文件中使用它。

所有的静态持续变量都有下面的两个初始化特征:

● 未被初始化的静态变量的所有位都被设置为0。

● 只能使用常量表达式来初始化静态变量。

常量表达式可以使用字面值常量、const常量和enum常量以及sizeof操作符。下面的代码片段说明了这几点:

image288_2

表9.1总结了引入名称空间之前使用的存储特性。下面详细介绍各种静态持续变量。

表9.1 5种变量储存方式

image289_1

1.静态持续性、外部链接性

链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变量是在函数外部定义的,因此对所有函数而言都是外部的。例如,可以在main()函数的前面定义它们。可以在文件中位于外部变量定义后面的任何函数中使用它,因此外部变量也称全局变量(相对于局部的自动变量)。不过,如果定义了与外部变量同名的自动变量,则当程序执行其所属的函数时,该自动变量将隐藏同名的外部变量。程序清单9.5说明了这几点,它还演示了如何使用关键字extern来重新声明以前定义过的外部变量,以及如何使用C++的作用域解析操作符来访问被隐藏的外部变量。由于该程序只包含一个文件,因此无法说明外部链接性;本章后面的范例将说明外部链接性。

程序清单9.5 extemal.cpp

image289_2

image290_1

2.程序说明

程序清单9.5中程序的输出表明,main()和update()都可以访问外部变量warming。注意,update()修改了warming,这种变化在接下来使用该变量时显现出来。

update()函数使用关键字extern对warming变量进行重新声明。该关键字的意思是“通过以前被外部定义的名称使用该变量”。由于即使省略该声明,update()的功能也相同,因此该声明是可选的。它指出该函数被设计成使用外部变量。原来的声明:

double warming = 0.3;

称为定义声明(defining declaration)或简称为定义(definition)。它给该变量分配存储空间。重新声明:

extern double warming;

称为引用声明(referencing declaration),或简称为声明(declaration)。它不给变量分配存储空间,因为它引用已有的变量。只能在引用其他地方(或函数)定义的变量的声明中使用关键字extern。基本上,该声明指出,使用外部定义的变量warming。在引用声明中指定的类型应与定义声明中相同;另外,不能在引用声明中初始化变量:

extern double warming = 0.5; // INVALID

仅当声明将为变量分配存储空间时(即定义声明),才能在声明中初始化变量。毕竟,初始化指的是在分配内存单元时给它赋值。

local()函数表明,定义与全局变量同名的局部变量后,局部变量将隐藏全局变量。例如,local()函数显示warming的值时,将使用warming的局部定义。

C++比C语言进了一步——它提供了作用域解析操作符(::)。当放在变量名称前面时,该操作符表示使用变量的全局版本。因此,local()将warming显示为0.8,但将::warming显示为0.4。后面介绍名称空间和类时,将再次介绍该操作符。

全局变量和局部变量

既然可以选择使用全局变量或局部变量,那么到底应使用哪种呢?首先,全局变量很有吸引力——因为所有的函数能访问全局变量,因此不用传递参数。但易于访问的代价很大——程序不可靠。计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下,应使用局部变量,应在需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。读者将会看到,OOP在数据隔离方面又向前迈进了一步。

不过,全局变量也有它们的用处。例如,可以让多个函数使用同一个数据块(如月份名数组或原子量数组)。外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。

image290_2

在上述范例中,第一个const防止字符串被修改,第二个const确保数组中每个指针始终指向它最初指向的字符串。

3.静态持续性、内部链接性

将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。在多文件程序中,内部链接性和外部链接性之间的差别很有意义。链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即可以在其他文件中使用。对于外部链接性变量,有且只有一个文件中包含了该变量的外部定义。其他文件要使用该变量,必须在引用声明中使用关键字extern(参见图9.4)。

image291_1

图9.4 定义声明和引用声明

如果文件没有提供变量的extern声明,则不能使用在其他文件中定义的外部变量:

image291_2

如果文件试图定义另一个同名的外部变量将出错:

image291_3

image292_1

但如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量:

image292_2

记住:在多文件程序中,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。

应使用外部变量在多文件程序的不同部分之间共享数据;应使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方法,C++标准指出,使用static来创建内部链接性的方法将逐步被淘汰)。另外,如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。

程序清单9.6和9.7演示了C++如何处理链接性为外部和内部的变量。程序清单9.6(twofile1.cpp)定义了外部变量torn和dick以及静态外部变量harry。这个文件中的main()函数显示这3个变量的地址,然后调用remote_access()函数,该函数是在另一个文件中定义的。程序清单9.7 (twofile2.cpp)列出了该文件。除了定义remote_access()之外,该文件还使用extern关键字来与第一个文件共享tom。接下来,该文件定义一个名为dick的静态变量。static限定符使该变量被限制在这个文件内,并覆盖相应的全局定义。然后,该文件定义了一个名为harry的外部变量,这不会与第一个文件中的harry发生冲突,因为后者的链接性为内部的。随后,remote-access()函数显示这3个变量的地址,以便于将它们与第一个文件中相应变量的地址进行比较。别忘了编译这两个文件,并将它们链接起来,以得到完整的程序。

程序清单9.6 twofile1.cpp

image292_3

程序清单9.7 twofile2.cpp

image293_1

下面是编译程序清单9.6和9.7生成的程序的输出:

image293_2

从上述地址可知,这两个文件使用了同一个tom变量,但使用了不同的dick和harry变量。具体的地址和格式可能随系统而异,但两个tom变量的地址将相同,而两个dick和harry变量的地址不同。

4.静态存储持续性、无链接性

至此,我们介绍了链接性分别为内部和外部、作用域为整个文件的变量。接下来介绍静态持续家族中的第三个成员——无链接性的局部变量。这种变量是这样创建的,将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变(静态变量适用于再生——可以用它们将瑞士银行的秘密账号传递到下一个要去的地方)。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。程序清单9.8说明了这几点。

该程序演示了一种处理行输入可能长于目标数组的方法。前面讲过,cin.get (input, ArSize)方法将一直读取输入,直到到达行尾或读取了ArSize-1个字符为止。它把换行符留在输入队列中。该程序读取行输入后的字符。如果是换行符,则说明整行被读取;否则说明行中还有字符没有被读取。随后,程序使用一个循环来丢弃余下的字符,不过读者可以修改代码,让下一轮输入读取行中余下的字符。该程序还利用了这样一个事实,即试图使用get(char*, int)读取空行将导致cin为false。

程序清单9.8 static.cpp

image293_3

image294_1

注意:早期的一些编译器没有实现这样的功能,即当cin.get(char*, int)读取一个空行后,将设置错误标记failbit,从而导致cin测试为false。在这种情况下,可以将测试:

while (cin)

替换为:

while (input[0])

或者提供一个可同时用于新旧实现的测试:

while (cin && input[0])

下面是该程序的输出:

image294_2

注意,由于数组长度为10,因此程序从每行读取的字符数都不超过9个。另外还需要注意的是,每次函数被调用时,自动变量count都被重置为0。不过,静态变量total只在程序运行时被设置为0,以后在两次函数调用之间,其值将保持不变,因此能够记录读取的字符总数。

9.2.4 说明符和限定符

一些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。下面是存储说明符:

● auto。

● register。

● static。

● extern。

● mutable。

其中的大部分已经介绍过了,在同一个声明中不能使用多个说明符。前面讲过,可以在声明中使用关键字auto将变量声明为自动变量;关键字register用于在声明中指示寄存器存储类型。关键字static被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。关键字extern表明是引用声明,即声明引用在其他地方定义的变量。关键字mutable的含义将根据const来解释,因此先来介绍cv-限定符,然后再解释它。

下面就是cv限定符:

● const。

● volatile。

(读者可能猜到了,cv表示const和volatile)其中最常用的是const,而读者已经知道其用途。它表明,内存被初始化后,程序便不能再对它进行修改。我们稍后再回过头来介绍它。

volatile关键字表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。听起来似乎很神秘,实际上并非如此。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容。或者两个程序可能互相影响,共享数据。该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

现在回到mutable。可以用它来指出,即使结构(或类)变量为const,其某个成员也可以被修改。例如,请看下面的代码:

image295_1

veep的const限定符禁止程序修改veep的成员,但access成员的mutable说明符使得access不受这种限制。

本书不使用volatile或mutable,但将进一步介绍const。

再谈const

在C++(但不是在C语言)中,const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义(如下述代码段所示)就像使用了static说明符一样。

image295_2

C++修改了常量类型的规则,让程序员更轻松。例如,假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面这样的定义:

image296_1

如果全局const声明的链接性像常规变量那样是外部的,则这将出错,因为不能在多个文件中定义同一个全局变量。也就是说,只能有一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有未使用extern关键字的声明才能进行初始化:

image296_2

因此,需要为某个文件使用一组定义,而其他文件使用另一组声明。然而,由于外部定义的const数据的链接性为内部的,因此可以在所有文件中使用相同的声明。

内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。

如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性:

extern const int states = 50; // external linkage

在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。另外,与常规变量不同的是,可以初始化extern const变量。实际上,必须这样做,因为const数据必须初始化。

在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。

9.2.5 函数和链接性

和变量一样,函数也有链接性,虽然可选择的范围比变量小。和C语言一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字extern来指出函数是在另一个文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字:

image296_3

这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。

C++有一个“单定义规则”,即对于每个非内联函数,程序中只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。

内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。不过,C++要求同一个函数的所有内联定义都必须相同。

C++在哪里查找函数

假设在程序的某个文件中调用一个函数,C++将到哪里去寻找该函数的定义呢?如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误消息,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数(不过,C++保留了标准库函数的名称,即程序员不应使用它们)。一些编译器-链接程序要求显式地指出要搜索哪些库。

9.2.6 语言链接性

另一种形式的链接性——称为语言链接性(language linking)也对函数有影响。首先介绍一些背景知识。链接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名称只对应一个函数,因此这很容易实现。因此,为满足内部需要,C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性(C language linkage)。但在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称矫正或名称修饰(参见第8章),为重载函数生成不同的符号名称。例如,可能将spiff (int)转换为_spoff_i,而将spiff (double, double)转换为_spiff_d_d。这种方法被称为C++语言链接(C++ language linkage)。

链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同。但如果要在C++程序中使用C库中预编译的函数,将出现什么情况呢?例如,假设有下面的代码:

spiff (22); // want spiff (int) from a C library

它在C库文件中的符号名称为_spiff,但对于我们假设的链接程序来说,C++查询约定是查找符号名称_spiff_i。为解决这种问题,可以用函数原型来指出要使用的约定:

image297_1

第一个原型使用C语言链接性;而后面的两个使用C++语言链接性。第二个原型是通过默认方式指出这一点的,而第三个显式地指出了这一点。

9.2.7 存储方案和动态分配

前面介绍C++用来为变量(包括数组和结构)分配内存的5种方案,它们不适用于使用C++操作符new(或C函数malloc())分配的内存,这种内存被称为动态内存。第4章介绍过,动态内存由操作符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。通常,编译器使用3块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。

虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:

float * p_fees = new float [20];

由new分配的80个字节(假设float为4个字节)的内存将一直保留在内存中,直到使用delete操作符将其释放。但当包含该声明的函数结束时,p_fees指针将消失。如果希望另一个函数能够使用这80个字节中的内容,则必须将其地址传递或返回给该函数。另一方面,如果将p_fees的链接性声明为外部的,则文件中位于该声明后面的所有函数都可以使用它。另外,通过在另一个文件中使用:

extern float * p_fees;

可以在这个文件中使用该指针。不过,请注意,使用new来设置p_fees的语句必须位于函数中(如下面的代码段所示),这是因为只能使用常量表达式来初始化静态存储变量:

image297_2

image298_1

注意:在程序结束时,由new分配的内存通常都将被释放,不过情况也并不总是这样。例如,在不那么健壮的操作系统中,在某些情况下,请求大型内存块将导致该代码块在程序结束不会被自动释放。最佳的做法是,使用delete来释放new分配的内存。

9.3 布局new操作符

通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new操作符还有另一种变体,被称为布局(placement) new操作符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程或处理需要通过特定地址进行访问的硬件。

要使用布局new特性,首先需要包含头文件new,它提供了这种版本的new操作符的原型;然后将new操作符用于提供了所需地址的参数。除需要指定参数外,句法与常规new操作符相同。具体地说,使用布局new操作符时,变量后面可以有方括号,也可以没有。下面的代码段演示了new操作符的4种用法:

image298_2

出于简化的目的,这个范例使用两个静态数组来为布局new操作符提供内存空间。因此,上述代码从bufferl中分配空间给结构chaff,从buffer2中分配空间给一个包含20个元素的int数组。

读者了解布局new操作符后,来看一个范例程序。程序清单9.9使用常规new操作符和布局new操作符创建动态分配的数组。该程序说明了常规new操作符和布局new操作符之间的一些重要差别,在查看该程序的输出后,我们将对此进行讨论。

程序清单9.9 newplace.cpp

image298_3

image299_1

下面是该程序在某个系统上运行时的输出:

image299_2

image300_1

程序说明

有关程序清单9.9,首先要指出的一点是,布局new操作符确实将数组p2放在了数组buffer中,p2和buffer的地址都是0x42e10。同时,常规new操作将数组p1放在很远的地方,其地址为0xc9d34,位于动态管理的堆中。

需要指出的第二点是,第二个常规new操作符查找一个新的内存块,其起始地址为0xc9d64;但第二个布局new操作符分配与以前相同的内存块:起始地址为0x42e10的内存块。布局new操作符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。例如,在第三次调用布局new操作符时,提供了一个从数组buffer开头算起的偏移量,因此将分配新的内存:

pd2 = new (buffer + N * sizeof(double)) double[N]; // offset of 40 bytes

第三点差别是,是否使用delete来释放内存。对于常规new操作符,下面的语句:

delete [] pd1;

释放起始地址为0xc9d34的内存块,因此接下来再次调用new操作符时,该内存块是可用的。然而,程序清单9.9中的程序没有使用delete来释放使用布局new操作符分配的内存。事实上,在这个例子中不能这样做。buffer指定的内存是静态内存,而delete只能用于这样的指针:指向常规new操作符分配的堆内存。也就是说,数组buffer位于delete的管辖区域之外,下面的语句:

delete [] pd2;

将引发运行阶段错误。另一方面,如果buffer是使用常规new操作符创建的,便可以使用常规delete操作符来释放整个内存块。

将布局new操作符用于类对象时,情况将更复杂,这将在第12章介绍。

9.4 名称空间

在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着工程的增大,名称相互冲突的可能性也将增加。使用多个厂商的类库时,可能导致名称冲突。例如,两个库可能都定义了名为List、Tree和Node的类,但定义的方式不兼容。用户可能希望使用一个库的List类,而使用另一个库的Tree类。这种冲突被称为名称空间问题。

C++标准提供了名称空间工具,以便更好地控制名称的作用域。经过了一段时间后,编译器才支持名称空间,但现在这种支持很普遍。

9.4.1 传统的C++名称空间

介绍C++中新增的名称空间特性之前,先复习一下C++中已有的名称空间属性,并介绍一些术语,让读者熟悉名称空间的概念。

第一个需要知道的术语是声明区域(declaration region)。声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。

第二个需要知道的术语是潜在作用域(potential scope)。变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。

然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。变量对程序而言可见的范围被称为作用域(scope),前面正是以这种方式使用该术语的。图9.5和图9.6对术语声明区域、潜在作用域和作用域进行了说明。

image301_1

图9.5 声明区域

image301_2

图9.6 潜在作用域和作用域

C++关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

9.4.2 新的名称空间特性

C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。例如,下面的代码使用新的关键字namespace创建了两个名称空间:Jack和Jill。

image302_1

名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。

除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。

任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此,Jack中的fetch可以与Jill中的fetch共存,Jill中的Hill可以与外部Hill共存。名称空间中的声明和定义规则同全局声明和定义规则相同。

名称空间是开放的(open),即可以把名称加入到已有的名称空间中。例如,下面这条语句:

image302_2

将名称goose添加到Jill中已有的名称列表中。

同样,原来的Jack名称空间为fetch()函数提供了原型。可以在该文件后面(或另外一个文件中)再次使用Jack名称空间来提供该函数的代码:

image302_3

当然,需要有一种方法来访问给定名称空间中的名称。最简单的方法是,通过作用域解析操作符::,使用名称空间来限定该名称:

image302_4

未被装饰的名称(如pail)被称为未限定的名称(unqualified name);包含名称空间的名称(如Jack::pail)被称为限定的名称(qualified name)。

1.using声明和using编译指令

我们并不希望每次使用名称时都对它进行限定,因此C++提供了两种机制(using声明和using编译指令)来简化对名称空间中名称的使用。using声明使特定的标识符可用,using编译指令使整个名称空间可用。

using声明由被限定的名称和它前面的关键字using组成:

using Jill::fetch; // a using declaration

using声明将特定的名称添加到它所属的声明区域中。例如main()中的using声明Jill::fetch将fetch添加到main()定义的声明区域中。完成该声明后,便可以使用名称fetch代替Jill::fetch。下面的代码段说明了这几点:

image303_1

由于using声明将名称添加到局部声明区域中,因此这个范例避免了将另一个局部变量也命名为fetch。另外,和其他局部变量一样,fetch也将覆盖同名的全局变量。

在函数的外面使用using声明时,将把名称添加到全局名称空间中:

image303_2

using声明使一个名称可用,而using编译指令使所有的名称都可用。using编译指令由名称空间名和它前面的关键字using namespace组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析操作符:

using namespace Jack; // make all the names in Jack available

在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用。这种情况已出现过多次:

image303_3

在函数中使用using编译指令,将使其中的名称在该函数中可用,下面是一个例子:

image303_4

在本书中,经常将这种格式用于名称空间std。

有关using编译指令和using声明,需要记住的一点是,它们增加了名称冲突的可能性。也就是说,如果有名称空间jack和jill,并在代码中使用作用域解析操作符,则不会存在二义性:

image304_1

变量jack::pal和jill::pal是不同的标识符,表示不同的内存单元。然而,如果使用using声明,情况将发生变化:

image304_2

事实上,编译器将不允许您同时使用上述两个using声明,因为这将导致二义性。

2.using编译指令和using声明之比较

使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析操作符。使用using声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而,使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。在下面的范例中,名称空间为全局的。如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。不过仍可以像下面的范例中那样使用作用域解析操作符:

image304_3

在main()中,名称Jill::fetch被放在局部名称空间中,但其作用域不是局部的,因此不会覆盖全局的fetch。然而,局部声明的fetch将隐藏Jill::fetch和全局fetch。不过,如果使用作用域解析操作符,则后两个fetch变量都是可用的。读者应将这个范例与前面使用using声明的范例进行比较。

需要指出的另一点是,虽然函数中的using编译指令将名称空间的名称视为在函数之外声明的,但它不会使得该文件中的其他函数能够使用这些名称。因此,在前一个例子中,foom()函数不能使用未限定的Hill标识符。

记住:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。

一般说来,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

下面是本书的大部分范例采用的方法:

image305_1

首先,#include语句将头文件iostream放到名称空间std中。然后,using编译指令是该名称空间在main()函数中可用。有些范例采取下述方式:

image305_2

这将名称空间std中的所有内容导出到全局名称空间中。使用这种方法的主要原因是方便。它完成起来比较简单,同时如果系统不支持名称空间,可以将前两行替换为:

#include &lt;iostream.h&gt;

不过,名称空间的支持者希望有更多的选择,既可以使用解析操作符,也可以使用using声明。也就是说,不要这样做:

using namespace std; // avoid as too indiscriminate

而应这样做:

image305_3

或者这样做:

image305_4

可以用嵌套式名称空间(将在下一节介绍)来创建一个包含常用using声明的名称空间。

3.名称空间的其他特性

可以将名称空间声明进行嵌套:

image305_5

这里,flame指的是element::fire::flame。同样,可以使用下面的using编译指令使内部的名称可用:

using namespace elements::fire;

另外,也可以在名称空间中使用using编译指令和using声明,如下所示:

image305_6

假设要访问Jill::fetch。由于Jill::fetch现在位于名称空间myth(在这里,它被叫作fetch)中,因此可以这样访问它:

std::cin >> myth::fetch;

当然,由于它也位于Jill名称空间中,因此仍然可以称作Jill::fetch:

std::cout << Jill::fetch; // display value read into myth::fetch

如果没有与之冲突的局部变量,则也可以这样做:

image306_1

现在考虑将using编译指令用于myth名称空间的情况。using编译指令是可传递的。如果A op B且B op C,则A op C,则我们说操作op是可传递的。例如,>操作符是可传递的(也就是说,如果A>B且B>C,则A>C)。在这个情况下,下面的语句:

using namespace myth;

将添加elements名称空间(也是通过一个using编译指令),因此它和下面的语句等价:

image306_2

可以给名称空间创建别名。例如,假设有下面的名称空间:

namespace my_very_favorite_things { … };

则可以使用下面的语句让mvft成为my_very_favorite_things的别名:

namespace mvft = my_very_favorite_things;

可以使用这种技术来简化对嵌套名称空间的使用:

image306_3

4.未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间:

image306_4

这就像后面跟着using编译指令一样,也就是说,在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。从这个方面看,它们与全局变量相似。不过,由于这种名称空间没有名称,因此不能显式地使用using编译指令或using声明来使它在其他位置都可用。具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称,因此这种方法可以替代链接性为内部的静态变量。实际上,C++标准不赞成在名称空间和全局作用域中使用关键字static(该标准使用“不赞成”表明,这种做法目前合法,但以后修订标准时,很可能将其视为非法)。例如,假设有这样的代码:

image306_5

image307_1

9.4.3 名称空间范例

现在来看一个多文件范例,该范例说明了名称空间的一些特性。该程序的第一个文件(参见程序清单9.10)是头文件,其中包含头文件中常包含的内容:常量、结构定义和函数原型。在这个例子中,这些内容被放在两个名称空间中。第一个名称空间叫pers,其中包含Person结构的定义和两个函数的原型——一个函数用人名填充结构,另一个函数显示结构的内容;第二个名称空间叫作debts,它定义了一个结构,该结构用来存储人名和金额。该结构使用了Person结构,因此,debts名称空间使用一条using编译指令,让pers中的名称在debts名称空间可用。debts名称空间也包含一些原型。

程序清单9.10 namesp.h

image307_2

第二个文件(见程序清单9.11)是源代码文件,它提供了头文件中的函数原型对应的定义。在名称空间中声明的函数名的作用域为整个名称空间,因此定义和声明必须位于同一个名称空间中。这正是名称空间的开放性发挥作用的地方。通过包含namesp.h(参见程序清单9.10)导入了原来的名称空间。然后该文件将函数定义添加入到两个名称空间中,如程序清单9.11所示。另外,文件names.cpp演示了如何使用using声明和作用域解析操作符来使名称空间std中的元素可用。

程序清单9.11 namesp.cpp

image307_3

image308_1

最后,该程序的第三个文件(参见程序清单9.12)是一个源代码文件,它使用了名称空间中声明和定义的结构和函数。程序清单9.12演示了多种使名称空间标识符可用的方法。

程序清单9.12 namessp.cpp

image308_2

image309_1

在程序清单9.12中,main()函数首先使用了两个using声明:

image309_2

注意,using声明只使用了名称,例如,第二个using声明没有描述showDebt的返回类型或函数特征标,而只给出了名称;因此,如果函数被重载,则一个using声明将导入所有的版本。另外,虽然Debt和showDebt都使用了Person类型,但不必导入任何Person名称,因为debt名称空间有一条包含pers名称空间的using编译指令。

接下来,other()函数采用了一种不太好的方法,即使用一条using编译指令导入整个名称空间:

using namespace debts; // make all debts and pers names available to other()

由于debts中的using编译指令导入了pers名称空间,因此other()函数可以使用Person类型和showPerson()函数。

最后,another()函数使用using声明和作用域解析操作符来访问具体的名称:

image309_3

下面是程序清单9.10〜9.12组成的程序的运行情况:

image309_4

image310_1

9.4.4 名称空间及其前途

随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则:

● 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。

● 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。

● 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的,没有使用名称空间,但C++头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。

● 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。

● 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。

● 导入名称时,首选使用作用域解析操作符或using声明的方法。

● 对于using声明,首选将其作用域设置为局部而不是全局。

别忘了,使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用using编译指令并非什么大逆不道的事。

正如前面指出的,头文件名的变化反映了这些变化。老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用了std名称空间。

9.5 总结

C++鼓励程序员在开发程序时使用多个文件。一种有效的组织策略是,使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型;并将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。最后,将main()和其他使用这些函数的函数放在第三个文件中。

C++的存储方案决定了变量保留在内存中的时间(储存持续性)以及程序的哪一部分可以访问它(作用域和链接性)。自动变量是在代码块(如函数体或函数体中的代码块)中定义的变量,仅当程序执行到包含定义的代码块时,它们才存在,并且可见。自动变量可以通过使用存储类型说明符auto或register或者根本不使用说明符(与使用auto相同)来声明。register说明符提示编译器,该变量的使用频率很高。

静态变量在整个程序执行期间都存在。对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它(文件作用域),并可在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用extern关键字来声明它。对于文件间共享的变量,应在一个文件中包含其定义声明(不使用extern),并在其他文件中包含引用声明(使用extern)。在函数的外面使用关键字static定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)。在代码块中使用关键字static定义的变量被限制在该代码块内(局部作用域、无链接性),但在整个程序执行期间,它都一直存在并且保持原值。

在默认情况下,C++函数的链接性为外部,因此可在文件间共享;但使用关键字static限定的函数的链接性为内部的,被限制在定义它的文件中。

名称空间允许定义一个可在其中声明标识符的命名区域。这样做的目的是减少名称冲突,尤其当程序非常大,并使用多个厂商的代码时。可以通过使用作用域解析操作符、using声明或using编译指令,来使名称空间中的标识符可用。

9.6 复习题

1.对于下面的情况,应使用哪种存储方案?

a.homer是函数的形参。

b.secret变量由两个文件共享。

c.topsecret变量由一个文件中的所有函数共享,但对于其他文件来说是隐藏的。

d.beencalled记录包含它的函数被调用的次数。

2.using声明和using编译指令之间有何区别?

3.重新编写下面的代码,使之不使用using声明和using编译指令。

image311_1

4.重新编写下面的代码,使之使用using声明,而不是using编译指令。

image311_2

5.在一个文件中调用average (3, 6)函数时,它返回两个int参数的平均值(int),在同一个程序的另一个文件中调用时,它返回两个int参数的平均值(double)。应如何实现?

6.下面的程序由两个文件组成,该程序显示什么内容?

image311_3

image312_1

7.下面的代码将显示什么内容?

image312_2

image313_1

9.7 编程练习

1.下面是一个头文件:

image313_2

setgolf()被重载,可以这样使用其第一个版本:

image313_3

上述函数调用提供了存储在ann结构中的信息。可以这样使用其第二个版本:

image313_4

上述函数将提示用户输入姓名和等级,并将它们存储在andy结构中。这个函数可以(但是不一定必须)在内部使用第一个版本。

根据这个头文件,创建一个多文件程序。其中的一个文件名为golf.cpp,它提供了与头文件中的原型匹配的函数定义;另一个文件应包含main(),并演示原型化函数的所有特性。例如,包含一个让用户输入的循环,并使用输入的数据来填充一个由golf结构组成的数组,数组被填满或用户将高尔夫选手的姓名设置为空字符串时,循环将结束。main()函数只使用头文件中原型化的函数来访问golf结构。

2.修改程序清单9.8:用string对象代替字符数组。这样,该程序将不再需要检查输入的字符串是否过长,同时可以将输入字符串同字符串""进行比较,以判断是否为空行。

3.下面是一个结构声明:

image314_1

编写一个程序,使用布局new操作符将一个包含两个这种结构的数组放在一个缓冲区中。然后,给结构的成员赋值(对于char数组,使用函数strcpy()),并使用一个循环来显示内容。方法1是像程序清单9.9那样将一个静态数组用作缓冲区;方法2是使用常规new操作符来分配缓冲区。

4.请基于下面这个名称空间编写一个由3个文件组成的程序:

image314_2

第一个文件是一个头文件,其中包含名称空间。第二个文件是一个源代码文件,它对这个名称空间进行扩展,以提供这3个函数的定义。第三个文件声明两个Sales对象,并使用setSales()的交互式版本为一个结构提供值,然后使用setSales()的非交互式版本为另一个结构提供值。另外它还使用showSales()来显示这两个结构的内容。