C++ Primer Plus 学习笔记(第 9 章 内存模型和名称空间)

C++ Primer Plus 学习笔记

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

单独编译

和 C 语言一样,C++ 也允许甚至鼓励程序员将组件函数放在独立的文件中。单独编译这些文件,然后将它们链接成可执行的程序。(通常,C++ 编译器既编译程序,也管理链接器。)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。另外,大多数 C++ 环境都提供了其他工具来帮助管理。例如,UNIX 和 Linux 系统都具有 make 程序,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行 make 时,如果它检测到上次编译后修改了源文件,make 将记住重新构建程序所需的步骤。大多数集成开发环境(包括Embarcadero C++ Builder、Microsoft Visual C++、Apple Xcode 和 Freescale CodeWarrior)都在 Project 菜单中提供了类似的工具。
一个文件(头文件)包含了用户定义类型的定义:另一个文件包含操纵用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序中。这种组织方式也与 OPP 方法一致。
请不要将函数定义或变量声明放到头文件中。这样做对于简单的情况可能是可行的,但通常会引来麻
烦。例如,如果在头文件包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。下面列出了头文件中常包含的内容。

  • 函数原型。
  • 使用#defineconst定义的符号常量。
  • 结构声明。
  • 类声明。
  • 模板声明。
  • 内联函数。

将结构声明放在头文件中是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,
告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性(稍后将介绍),因此可以将其放在头文件中,而不会引起问题。
注意,如果文件名包含在尖括号中,则 C++ 编译器将在存储标准头文件的主机系统的文件系统中查找;如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,这取决于编译器)。如果没有在那里找到头文件,则将在标准位置查找。因此在包含自己的类文件时,应使用引号而不是尖括号。
在UNX系统中将多个源代码文件组合起来,只需执行编译命令 C C即可,其他步骤将自动完成。g++ 和 gp p命令行编译器以及 Borland C++ 命令行编译器(bcc32.exe)的行为类似。Apple
Xcode、Embarcadero C++ Builderr 和 Microsoft Visual C++ 基本上执行同样的步骤,但
启动这个过程的方式不同 —— 使用能够创建项目并将其与源代码文件关联起来的菜单。注意,只需将源代码文件加入到项目中,而不用加入头文件。这是因为#include指令管理头文件。另外,不要使用#include来包含源代码文件,这样做将导致多重声明。
警告:在 IDE 中,不要将头文件加入到项目列表中,也不要在源代码文件中使用#include来包含其他源代码文件。

// coordin.h -- structure templates and function prototypes
// structure templates
#ifndef COORDIN_H
#define COORDIN_H

struct polar
{
   double distance;   // distance from origin
   double angle;      // distance from origin
};

struct rect
{
   double x;   // horizontal distance form origin
   double y;   // vertical distance form origin
};

// prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif
头文件管理

在同一个文件中只能将同一个头文件包含一次。记住这个规则很容易,但很可能在不知情的情况下将头
文件包含多次。例如,可能使用包含了另外一个头文件的头文件。有一种标准的 C/C++ 技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即 if not defined)的。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H时,才处理#ifndef#endif之间的语句:

#ifndef COORDIN_H
...
#endif

通常,使用#define语句来创建符号常量,但只要将#define用于名称,就足以完成该名称的定义,如下所示:

#define MAXIMUM 4096
#define COORDIN_H

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

#ifndef COORDIN_H
#define COORDIN_H
// place include file contents here
#endif

编译器首次遇到该文件时,名称COORDIN_H没有定义(我们根据include文件名来选择名称,并加上一些下划线,以创建一个在其他地方不太可能被定义的名称)。在这种情况下,编译器将查看#ifndef#endif之间的内容(这正是我们希望的),并读取定义COORDIN_H的一行。如果在同一个文件中遇到其他包含coordin.h的代码,编译器将知道COORDIN_H已经被定义了,从而跳到#endif后面的一行上。注意,这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。大多数标准 C 和 C++ 头文件都使用这种防护(guarding)方案。否则,可能在一个文件中定义同一个结构两次,这将导致编译错误。
顺便说一句,虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++ 标准使用了术语翻译
单元(translation unit),而不是文件;文件并不是计算机组织信息时的唯一方式。出于简化的的,本书使用术语文件,您可将其解释为翻译单元。

多个库的链接

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

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

C++ 使用三种(在 C++11 中是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。

  • 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程
    序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。
    C++ 有两种存储持续性为自动的变量。
  • 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静
    态。它们在程序整个运行过程中都存在。C++ 有 3 种存储持续性为静态的变量。
  • 线程存储持续性(C++11):当前,多核处理器很常见,一这些 CPU 可同时处理多个执行任务。这
    让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread local声明的,则
    其生命周期与所属的线程一样长。本书不探讨并行编程。
  • 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(freestore)或堆(heap)。
作用域和链接

作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。例如,函数中定义的变量可在该
函数中使用,但不能在其他函数中使用;而在文件中的函数定义之前定义的变量则可在所有函数中使用。链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。
C++ 变量的作用域有多种。作用域为局部的变量只在定义它的代码块中可用。代码块是由花括号括起
的一系列语句。例如函数体就是代码块,但可以在函数体中嵌入其他代码块。作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数原型作用域(function prototype scope)中使用的名称只在包含参数列表的括号内可用(这就是为什么这些名称是什么以及是否出现都不重要的原因)。在类中声明的成员的作用域为整个类(参见第10章)。在名称空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到 C++ 语言中,因此全局作用域是名称空间作用域的特例)。
C++ 函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己是可见的,因此不能被其他函数调用。这样的函数将无法运行)。
不同的 C++ 存储方式是通过存储持续性、作用域和链接性来描述的。

自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
另外,当程序开始执行这些变量所属的代码块时,将为其分配内存;当函数结束时,这些变量都将消失(注意,执行到代码块时,将为变量分配内存,但其作用域的起点为其声明位置)。
如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。它的作用域是从定义它的位置到该代码块的结尾。
然而,如果有两个同名的变量(一个位于外部代码块中,另一个位于内部代码块中),新的定义隐藏(hide)以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义又重新可见。

使用 C++11 中的 auto

在 C++11 ,关键字auto用于自动类型推断。但在 C 语言和以前的 C++ 版本中,auto的含义截然不同,它用于显式地指出变量为自动存储。
由于只能将关键字auto用于默认为自动的变量,因此程序员几乎不使用它。它的主要用途是指出当前变量为局部自动变量。
在 C+11 中,这种用法不再合法。制定标准的人不愿引入新关键字,因为这样做可能导致将该关键字用于其他目的的代码非法。考虑到auto的老用法很少使用,因此赋予其新含义比引入新关键字是更好的选择。

自动变量的初始化

可以使用任何在声明进其值为已知的表达式为初始化自动变量。

自动变量和栈

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

寄存器变量

关键字register最初是由 C 语言引入的,它建议编译器使用 CPU 寄存器来存储自动变量。
在 C++11 之前,这个关键字在 C++ 中的用法始终未变,只是随着硬件和编译器变得越来越复杂,这种提示表明变量用得很多,编译器可对其做特殊处理。在 C++11 中,这种提示作用也失去了,关键字register只是显式地指出变量是自动的。鉴于关键字register只能用于原本就是自动的变量,使用它的唯一原因是,指出程序员想使用一个自动变量,这个变量的名称可能与外部变量相同。这与auto以前的用途完全相同。然而,保留关键字register的重要原因是,避免使用了该关键字的现有代码非法。

静态持续持续变量

和 C 语言一样,C++ 也为静态存储持续性变量提供了 3 种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这 3 种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0 。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0
注意:传统的 K&RC 不允许初始化自动数组和结构,但允许初始化静态数组和结构。ANSI C 和 C++ 允许对这两种数组和结构进行初始化,但有些旧的 C++ 翻译器使用与 ANSI_C 不完全兼容的 C 编译器。如果使用的是这样的实现,则可能需要使用这 3 种静态存储类型之一,以初始化数组和结构。
要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。
所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为0。这种变量
被称为零初始化的(zero-initialized)。

关键字static有两种用法,含义有些不同:用于局部声明,以指出变量是无链接性的静态变量时,static表示的是存储持续性;而用于代码块外的声明时,static表示内部链接性,而变量已经是
静态持续性了。有人称之为关键字重载,即关键字的含义取决于上下文。

引入名称空间之前的 5 种变量储存方式
存储描述持续性作用域链接性如何声明
自动自动代码块在代码块中
寄存器自动代码块在代码块中,使用关键字register
静态,无链接性静态代码块在代码块中,使用关键定static
静态,外部链接性静态文件外部不在任何函数内
静态,内部链接性静态文件内部不在任何函数内,使用关键字static
静态变量的初始化

除默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。零初始化意味着将变量设置为零。对于标量类型,零将被强制转换为合适的类型。例如,在 C++ 代码中,空指针用0表示,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,且填充位都被设置为零。
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始
化变量。动态初始化意味着变量将在编译后初始化。
那么初始化形式由什么因素决定呢?首先,所有静态变量都被零初始化,而不管程序员是否显式地初
始化了它。接下来,如果使用常量表达式初始化了变量,且编译器仅根据文件内容(包括被包含的头文件)就可计算表达式,编译器将执行常量表达式初始化。必要时,编译器将执行简单计算。如果没有足够的信息,变量将被动态初始化。
常量表达式并非只能是使用字面常量的算术表达式。例如,它还可使用sizeof运算符,C++11 新增了关键字constexpr,这增加了创建常量表达式的方式。但本书不会更详细地介绍C++11 新增的这项新功能。

静态持续性、外部链接性

链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变量是
在函数外部定义的,因此对所有函数而言都是外部的。例如,可以在main()前面或头文件中定义它们。可以在文件中位于外部变量定义后面的任何函数中使用它,因此外部变量也称全局变量(相对于局部的自动变量)。

单定义规则

一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,C++ 有“单定义规则”(One definition Rul,ODR),该规则指出,变量只能有一次定义。为满足这种需求,C++ 提供了两种变量声明。一种是定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。
引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间:

double up;              // definition,up is 0
extern int blem;        // blem defined elsewhere
extern char gr ='z';    // definition because initialized

如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该
变量的其他所有文件中,都必须使用关键字extern声明它。
请注意,单定义规则并非意味着不能有多个变量的名称相同。例如,在不同函数中声明的同名自动变量是彼此独立的,它们都有自己的地址。另外,局部变量可能隐藏同名的全局变量。然而,虽然程序中可包含多个同名的变量,但每个变量都只有一个定义。
如果在函数中声明了一个与外部变量同名的变量,这种声明将被视为一个自动变量的定义,当程序执行自动变量所属的函数时,该变量将位于作用域内。
C++ 比 C 语言更进了一步 —— 它提供了作用域解析运算符::。放在变量名前面时,该运算符表示使用变量的全局版本。

全局变量和局部变量

既然可以选择使用全局变量或局部变量,那么到底应使用哪种呢?首先,全局变量很有吸引力 —— 因为所有的函数能访问全局变量,因此不用传递参数。但易于访问的代价很大 —— 程序不可靠。计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下,应使用局部变量,应在需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。OOP 在数据隔离方面又向前迈进了一步。
然而,全局变量也有它们的用处。例如,可以让多个函数可以使用同一个数据块(如月份名数组或原
子量数组)。外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。

const char * const months [12] =
{
    "January", "February", "March", "April", "May",
    "June", "July", "August", "September", "October",
    "November", "December"
};

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

静态持续性、内部链接性

static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。在多文件程序中,内部链接性和外部链接性之间的差别很有意义。链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即可以在其他文件中使用。
如果要在其他文件中使用相同的名称来表示其他变量,该如何办呢?只需省略关键字extern即可吗?
这种做法将失败,因为它违反了单定义规则。
但如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件
中,静态变量将隐藏常规外部变量,这没有违反单定义规则,因为关键字static指出链接性为内部,因此并非要提供外部定义。
注意:在多文件程序中,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。
可使用外部变量在多文件程序的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方法)。另外,如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。

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

无链接性的局部变量是这样创建的,将static限定符用于在代码块中定义的变量。在代码块中使
static时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码映中可用,但它在该代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。(静态变量适用于再生 —— 可以用它们将瑞士银行的秘密账号传递到下一个要去的地方)。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。

cout <"Enter a line:\n";
cin.get (input, Arsize);
while (cin)
{
    cin.get(next);
    while (next !'\n')    // string didn't fit!
        cin.get (next);   // dispose of remainder
    ...
    cout <<"Enter next line (empty line to quit):\n";
    cin.get(input,Arsize);
}
cout << "Bye\n";

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

说明符和限定符

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

  • auto(在 C++11 中不再是说明符);
  • register;
  • static;
  • extern:
  • thread_local(C++11 上新增的);
  • mutable。

在同一个声明中不能使用多个说明符,但thread local除外,它可与staticextern结合使用。在 C+11之前,可以在声明中使用关键字auto指出变量为自动变量;但在 C+1l 中,auto用于自动类型推断。关键字register用于在声明中指示寄存器存储,而在 C++11 中,它只是显式地指出变量是自动的。关键字static被用在作用域为整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性为静态的。关键字extern表明是引用声明,即声明引用在其他地方定义的变量。关键字thread_local指出变量的持续性与其所属线程的持续性相同。thread_local变量之于线程,犹如常规静态变量之于整个程序。关键字mutable的含义将根据const来解释,因此先来介绍 cv-限定符,然后再解释它。

cv-限定符

下面就是cv-限定符:

  • const;
  • volatile。

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

mutable

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

struct data
{
    char name[30];
    mutable int accesses;
    ...
};
const data veep = {"claybourne clodde", 0, ...};
strcpy(veep.name,"Joye'Joux");    // not allowed
veep.accesses++;                  // allowed

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

再谈const

在 C++(但不是在 C 语言)中,const限定符对默认存储类型稍有影响。在默情况下全局变量的链
接性为外部的,但const全局变量的链接性为内部的。也就是说,在 C++ 看来,全局const定义就像使用了static说明符一样。
C++ 修改了常量类型的规则,让程序员更轻松。例如,假设将一组常量放在头文件中,并在同一个程
序的多个文作中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含相同的const声明语句,如果全局const声明的链接性像常规变量那样是外部的,则根据单定义规则,这将出错。也就是说,只能有一个文件可以包含const声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有未使用extern关键字的声明才能进行初始化,因此,需要为某个文件使用一组定义,而其他文件使用另一组声明。然而,由于外部定义的const数据的链接性为内部的,因此可以在所有文件中使用相同的声明。
内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是
其所属文件私有的,这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。
如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性,在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。然而,请记住,鉴于单个const在多个文作之间共享,因此只有一个文件可对其进行初始化。
在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量
才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。

函数和链接性

和变量一样,函数也有链接性,虽然可选择的范围比变量小。和 C 语言一样,C++ 不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字extern来指出函数是在另一个文作中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字。
这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使
用静态函数。
单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为
外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是您提供的)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头
文件的每个文件都有内联函数的定义。然而,C++ 要求同一个函数的所有内联定义都必须相同。

C++ 在哪里查找函数

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

语言链接性

另一种形式的链接性 —— 称为语言链接性(language linking)也对函数有影响。首先介绍一些背景知识。链接程序要求每个不同的函数都有不同的符号名。在 C 语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,C 语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为 C 语言链接性(C language linkage)。但在 C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称。因此,C++ 编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。例如,可能将spiff(int)转换为_spoff_i,而将spiff(double,double)转换为_spiff_d_d。这种方法被称为 C++ 语言链接(C++ language linkage)。
链接程序寻找与 C++ 函数调用匹配的函数时,使用的方法与 C 语言不同。但如果要在 C++ 程序中使用 C 库中预编译的函数,它在C库文件中的符号名称为_spiff,但对于我们假设的链接程序来说,C++ 查询约定是查找符号名称_spiff_i。为解决这种问题,可以用函数原型来指出要使用的约定:

extern "C" void spiff(int);    // use C protocol for name look-up
extern void spoff(int);        // use C++ protocol for name look-up
extern "C++" void spaff(int);  // use C++ protocol for name look-up

第一个原型使用 C 语言链接性;而后面的两个使用 C++ 语言链接性。第二个原型是通过默认方式指出这一点的,而第三个显式地指出了这一点。
C 和 C++ 链接性是 C++ 标准指定的说明符,但实现可提供其他语言链接性说明符。

存储方案和动态分配

前面介绍 C++ 用来为变量(包括数组和结构)分配内存的 5 种方案(线程内存除外),它们不适用于使用 C++ 运算符new(或 C 函数malloc())分配的内存,这种内存被称为动态内存。动态内存由运算符newdelete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是 FLFO,其分配和释放顺序要取决于newdelete在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假
设在一个函数中包含下面的语句:

float * p_fees = new float [20];

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

extern float * p_fees;

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

使用 new 运算符初始化

如果要初始化动态分配的变量,在 C++98 中,有时候可以这样做,C++11 增加了其他可
能性。下面先来看看 C++98 提供的可能性。
如果要为内置的标量类型(如intdouble)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起:

int *pi = new int (6);    // *pi set to 6
double * pd = new double (99.99);    // *pd set to 99.99

这种括号语法也可用于有合适构造函数的类,这将在本书后面介绍。
然而,要初始化常规结构或数组,需要使用大括号的列表初始化,这要求编译器支持 C++11。C++11 允许您这样做:

struct where {double x; double y; double z;};
where one = new where {2.5, 5.3, 7.2};    // C++11
int * ar = new int [4] {2, 4, 6, 7};      // C++11

在 C++11 中,还可将列表初始化用于单值变量:

int *pin = new int {6}        // *pi set to 6
double * pdo = new double {99.99};  // *pdo set to 99.99
new 失败时

new可能找不到请求的内存量。在最初的10年中,C++ 在这种情况下让new返回空指针,但现在将引发异常std::bad_alloc

new:运算符、函数和替换函数

运算符newnew[]分别调用如下函数:

void operator new(std::size_t);    // used by new
void operator new[](std::size_t);  // used by new[]

这些函数被称为分配函数(alloction function),它们位于全局名称空间中。同样,也有由deletedelete[]调用的释放函数(deallocation function):

void operator delete(void *);
void operator delete[](void *);

它们使用第I1章将讨论的运算符重载语法。std::size_t是一个typedef,对应于合适的整型。对于下面这样的基本语句:

int * pi = new int;
int * pa = new int[40];

将被转换为下面这样:

int * pi = new(sizeof(int));
int * pa = new(40 * sizeof(int));

使用运算符new的语句也可包含初始值,因此,使用new运算符时,可能不仅仅是调用new()函数。
同样,下面的语句:

delete pi;

将转换为如下函数调用:

delete (pi);

有趣的是,C++ 将这些函数称为可替换的(replaceable)。这意味着如果您有足够的知识和意愿,可为newdelete提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new运算符,但它将调用您定义的new()函数。

定位 new 运算符

通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后
new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。下面的代码段演示了new运算符的4种用法:

#include <new>
struct chaff
{
    char dross [20];
    int slag;
};
char buffer1[5o];
char buffer2[500];
int main()
{
    chaff *pl, *p2;
    int *p3, *p4;
// first, the regular forms of new
    pl = new chaff;                 // place structure in heap
    p3 = new int [20];              // place int array in heap
// now, the two forms of placement new
    p2 = new (buffer1) chaff;      // place structure in bufferl
    p4 = new (buffer2) int [20];   // place int array in buffer2
    ...

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

// newplace.cpp -- using placement new 
#include <iostream>
#include <new>            // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF];        // chunk of memory

int main()
{
    using namespace std;
    double *pd1, *pd2;
    int i;
    cout << "Calling new and placement new:\n";
    pd1 = new double[N];            // use heap
    pd2 = new (buffer) double[N];    // use buffer array
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 20.0 * i;
    cout << "Memory addresses;\n" << " heap:" << pd1;
    cout << " static: " << (void *)buffer  << endl;
    cout << "Memory contents:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }
    
    cout << "\nCalling new and placement new a second time:\n";
    double *pd3, *pd4;
    pd3 = new double[N];            // find new address
    pd4 = new (buffer) double[N];    // overwrite old data
    for (i = 0; i < N; i++)
        pd4[i] = pd3[i] = 1000 + 40.0 *i;
    cout << "Memory contents:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd3[i] << " at " << &pd3[i] << "; ";
        cout << pd4[i] << " at " << &pd4[i] << endl; 
    }
    
    cout << "\nCalling new and placement new a third time:\n";
    delete [] pd1;
    pd1 = new double[N];
    pd2 = new (buffer + N * sizeof(double)) double[N];
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 60.0 * i;
    cout << "Memory conterts:\n";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }
    delete [] pd1;
    delete [] pd3;
    return 0;
}

// 下面是该程序在某个系统上运行时的输出:
Calling new and placement new:
Memory addresses:
heap:006E4ABO
static:00FD9138
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9138
1020 a t006E4AB8; 1020 at 00FD9140
1040 at 006E4AC0; 1040 at 00FD9148
1060 at 006E4AC8; 1060 at 00FD9150
1080 at 006E4AD0; 1080 at 00FD9158
Calling new and placement new a second time:
Memory contents:
1000 at 006E4B68; 1000 at 00FD9138
1040 at 006E4B70; 1040 at 00FD9140
1080 at 006E4B78; 1080 at 00FD9148
1120 at 006E4B80; 1120 at 00FD9150
1160 at 006E4B88;1160 at 00FD9158
Calling new and placement new a third time:
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9160
1060 at 006E4AB8; 1060 at 00FD9168
1120 at 006E4AC0; 1120 at 00FD9170
1180 at 006E4AC8; 1180 at 00FD9178
1240 at 006E4AD0; 1240 at 00FD9180

首先要指出的一点是,定位new运算符确实将数组p2放在了数组buffer中,p2buffer的地址都是O0FD9138。然而,它们的类型不同,pl是double指针,而bufferchar指针(顺便说一句,这也是程序使用(void*)buffer进行强制转换的原因,如果不这样做,cout将显示一个字符串)。同时,常规new将数组pl放在很远的地方,其地址为O06E4AB0,位于动态管理的堆中。
需要指出的第二点是,第二个常规new运算符查找一个新的内存块,其起始地址为006E4B68;但第二个定位new运算符分配与以前相同的内存块:起始地址为00FD9138的内存块。定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。例如,在第三次调用定位new运算符时,提供了一个从数组buffer开头算起的偏移量,因此将分配新的内存。
第三点差别是,是否使用delete来释放内存。对于常规new运算符,delete [] pd1;释放起始地址为006E4AB0的内存块,因此接下来再次调用new运算符时,该内存块是可用的。
然而,没有使用delete来释放使用定位new运算符分配的内存。事实上,在这个例子中不能这样做。buffer指定的内存是静态内存,而delete只能用于这样的指针:指向常规new运算符分配的堆内存。也就是说,数组buffer位于delete的管辖区域之外。
另一方面,如果buffer是使用常规new运算符创建的,便可以使用常规delete运算符来释放整个内存块。
定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处。
定位new运算符的工作原理,基本上,它只是返回传递给它的地址,并将其强制转换为void*,以便能够赋给任何指针类型。但这说的是默认定位new函数,C++ 允许程序员重载定位new函数。
将定位new运算符用于类对象时,情况将更复杂,这将在第12章介绍。

定位 new 的其他形式

就像常规new调用一个接收一个参数的new()函数一样,标准定位new调用一个接收两个参数的new()函数:

int pi = new int;                 // invokes new(sizeof(int))
int p2 = new (buffer) int;        // invokes new(sizeof(int), buffer)
int p3 = new (buffer) int [40];   // invokes new(40 * sizeof(int), buffer)

定位new函数不可替换,但可重载。它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。

名称空间

在 C++ 中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大,名称
相互冲突的可能性也将增加。使用多个厂商的类库时,可能导致名称冲突。这种冲突被称为名称空间问题。
C++ 标准提供了名称空间工具,以便更好地控制名称的作用域。经过了一段时间后,编译器才支持名
称空间,但现在这种支持很普遍。

传统的 C++ 名称空间

介绍 C++ 中新增的名称空间特性之前,先复习一下 C++ 中己有的名称空间属性,并介绍一些术语,让读者熟悉名称空间的概念。
第一个需要知道的术语是声明区域(declaration region)。声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。
第二个需要知道的术语是潜在作用域(potential scope)。变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中
声明的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。变量对程序而言可见的范围被称为作用域(scope),前面正是以这种方式使用该术语的。
C++ 关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

新的名称空间特性

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

namespace Jack {
    double pail;            // variable declaration
    void fetch();           // function prototype
    int pal;                // variable declaration
    struct Well {...};      // structure declaration
  }
namespace Jill {
    double bucket (double n){...}   // function definition
    double fetch;                   // variable declaration
    int pal;                        // variable declaration
    struct Hill{...};               // structure declaration
}

名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,
在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
除了用户定义的名称空间外,还存在另一个名称空间 —— 全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。
任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此,Jack中的fetch可以与Jill中的fetch共存,Jill中的Hill可以与外部Hill共存。名称空间中的声明和定义规则同全局声明和定义规则相同。
名称空间是开放的(open),即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称goose添加到Jill中已有的名称列表中:

namespace Jill {
    char goose(const char *)
}

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

namespace Jack {
    void fetch()
    {
        ...
    }
}

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

Jack::pail = 2.34;   // use a variable
Jill::Hill mole;    // create a type Hill structure
Jack::fetch()       // use a function

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

using 声明和 using 编译指令

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

using Jill::fetch;  // a using declaration

using声明将特定的名称添加到它所属的声明区域中。例如main()中的using声明Jill::fetchfetch添加到main()定义的声明区域中。完成该声明后,便可以使用名称fetch代替Jill::fetch
由于using声明将名称添加到局部声明区域中,因此不能将另一个局部变量也命名为fetch。另外,和其他局部变量一样,fetch也将覆盖同名的全局变量。
在函数的外面使用using声明时,将把名称添加到全局名称空间中。

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

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

在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用。
在函数中使用using编译指令,将使其中的名称在该函数中可用。
有关using编译指令和using声明,需要记住的点是,它们增加了名称冲突的可能性。也就是说,如果有名称空间jackjill,并在代码中使用作用域解析运算符,则不会存在二义性;然而,如果使用using声明,情况将发生变化:

jack::pal = 3;
jill::pal = 10;

using jack::pal;
using jill::pal;
pal = 4;            // which one? now have a conflict

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

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

使用using编译指令导入一个名称空间中所有的名称与使用多个using声明就不一样的,而更像是大量使用作用域解析运算符。使用using声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而,使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。在下面的示例中,名称空间为全局的。如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。不过仍可以使用作用域解析运算符。
需要指出的另一点是,虽然函数中的using编译指令将名称空间的名称视为在函数之外声明的,但它
不会使得该文件中的其他函数能够使用这些名称。
注意:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
一般说来,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

名称空间的其他特性

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

namespace elements
{
    namespace fire
    {
        int flame;
        ...
    }
    float water;
}

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

using namespace elements::fire;

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

namespace myth
{
    using Jill::fetch;
    using namespace elements;
    using std::cout;
    using std::cin;
}

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

std::cin >> myth::fetch;

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

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

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

using namespace myth;
cin >> fetch;               // really std::cin and Jill::fetch

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

using namespace myth;
// 这条编译指令与下面两条编译指令等价:
using namespace myth;
using namespace elements;

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

namespace my_very_favorite_things {...}
// 则可以使用下面的语句让 mvft 成为 my_very_favorite_things 的别名:
namespace mvft = my_very_favorite_things;
// 可以使用这种技术来简化对嵌套名称空间的使用:
namespace MEF = myth::elements::fire;
using MEF::flame;
未命名的空间

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

namespace           // unnamed namespace
{
    int ice;
    int bandycoot;
}

这就像后面跟着using编译指令一样,也就是说,在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。从这个方面看,它们与全局变量相似。然而,由于这种名称空间没有名称,因此不能显式地使用using编译指令或using声明来使它在其他位置都可用。具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。例如,假设有这样的代码:

static int counts:      // static storage, internal linkage
int other();
int main()
{
    ...
}
int other()
{
    ...
}
// 采用名称空间的方法如下:
namespace
{
    int counts;         // static storage, internal linkage
}
int other();
int main()
{
    ...
}
int other()
{
    ...
}
名称空间及其前途

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

  • 使用在己命名的名称空间中声明的变量,而不是使用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++ 当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自 C 语言中的函数。例如,头文件math.h是与 C 语言兼容的,没有使用名称空间,但 C++ 头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。
  • 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
  • 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令include之后。
  • 导入名称时,首选使用作用域解析运算符或using声明的方法。
  • 对于using声明,首选将其作用域设置为局部而不是全局。

别忘了,使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用using编译指令并非什么大逆不道的事。
正如前面指出的,头文件名的变化反映了这些变化。老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用了std名称空间。

第 9 章 总结

C++ 鼓励程序员在开发程序时使用多个文件。一种有效的组织策略是,使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型;并将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。最后,将main()和其他使用这些函数的函数放在第三个文件中。
C++ 的存储方案决定了变量保留在内存中的时间(储存持续性)以及程序的哪一部分可以访问它(作用域和链接性)。自动变量是在代码块(如函数体或函数体中的代码块)中定义的变量,仅当程序执行到包含定义的代码块时,它们才存在,并且可见。自动变量可以通过使用存储类型说明符register或根本不使用说明符来声明,没有使用说明符时,变量将默认为自动的。register说明符提示编译器,该变量的使用频率很高,但 C++11 摒弃了这种用法。
静态变量在整个程序执行期间都存在。对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它(文件作用域),并可在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用extern关键字来声明它。对于文件间共享的变量,应在一个文件中包含其定义声明(无需使用extern,但如果同时进行初始化,也可使用它),并在其他文件中包含引用声明(使用extern且不初始化)。在函数的外面使用关键字static定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)。在代码块中使用关键字static定义的变量被限制在该代码块内(局部作用域、无链接性),但在整个程序执行期间,它都一直存在并且保持原值。
在默认情况下,C++ 函数的链接性为外部,因此可在文件间共享;但使用关键字static限定的函数的链接性为内部的,被限制在定义它的文件中。
动态内存分配和释放是使用newdelete进行的,它使用自由存储区或堆来存储数据。调用new占用内存,而调用delete释放内存。程序使用指针来跟踪这些内存单元。
名称空间允许定义一个可在其中声明标识符的命名区域。这样做的目的是减少名称冲突,尤其当程序非常大,并使用多个厂商的代码时。可以通过使用作用域解析运算符、using声明或using编译指令,来使名称空间中的标识符可用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值