Chapter9 内存模型和名称空间

Chapter9 内存模型和名称空间

9.1 单独编译

不应将函数定义或变量声明放到头文件中,这样做可能会引发麻烦。例如,如果在头文件中包含一个函数定义,然后再其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。

使用#include包含头文件时,如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;但如果文件名包含在双引号中,则编译器将首先查找当前的工作目录或源代码目录。

在同一个文件中只能将同一个头文件包含一次。可以利用基于预处理器编译指令#ifndef的技术避免多次包含同一个头文件。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句:

1
2
3
4
#ifndef COORDIN_H_
#define COORDIN_H_
...
#endif

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

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

  • 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放
  • 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在
  • 线程存储持续性(C++11):如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长
  • 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止,这种内存的存储持续性为动态,又是被称为自由存储(free store)或堆(heap)。

9.2.1 作用域和链接

作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能有一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

作用域为局部的变量只在定义它的代码块中可用。作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。在函数原型作用域(function prototype scope)中使用的名称旨在包含参数列表的括号内可用。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间。(全局作用域是名称空间作用域的特例

9.2.2 自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。

在C++11中,关键字auto用于自动类型推断,而在C语言和以前的C++版本中,auto用于显式地指出变量为自动存储。

  • 自动变量的初始化

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

  • 自动变量和栈

    由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是路i出一段内存,并将其视为栈,以管理变量的增减。(新数据被象征性地放在原有数据的上面,即数据位于相邻的内存单元中,当程序使用完后,将其从栈中删除)。

    image-20201019180018372
  • 寄存器变量
    关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量以提高访问变量的速度。

9.2.3 静态持续变量

C++为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这三种链接性都在整个程序执行期间存在,即静态变量在整个程序执行期间一直存在。

要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。

链接性 static限定符 位置
外部 代码块外
内部 代码块外
代码块内

所有静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为0.这种变量被称为零初始化的(zero-initializated)。

零初始化和常量表达式初始化统称为静态初始化,这意味着在编译器处理文件时初始化变量。

image-20201019181408033

9.2.4 静态持续性、外部链接性

链接性位外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。

  • 单定义规则

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

    引用声明使用关键字extern,且不进行初始化。如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单变量规则),但在使用该变量的其他所有文件种,都必须使用关键字extern声明它。如:

    image-20201019183133490

    虽然程序中可能包含多个同名的变量,但每个变量都只有一个定义。

    C++提供了作用域解析运算符(::),将它放在变量名前面时,该运算符表示使用变量的全局版本

9.2.5 静态持续性、内部链接性

将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。

如果文件定义了一个静态外部变量,其名称与另一个文件中的生命的常规外部变量先沟通,则在该文件中,静态变量将隐藏常规外部变量。可使用外部变量在多文件程序的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据

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

在两次函数调用之间,静态局部变量的值将保持不变。如果初始化了静态局部变量,则程序只在启动时进行依次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。

9.2.7 说明符和限定符

在同一个声明种不能使用多个说明符,但thread_local除外,它可与static或extern结合使用。

  • cv-限定符

    1. const
      它表明内存被初始化后,程序便不能再对它进行修改
    2. volatile
      它表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。例如,假设编译器发现,程序再几条语句种两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将执行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。
  • mutable

    可以用该关键字指出,即使结构(或类)变量为const,其某个成员也可以被修改。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct data
    {
    char name[30];
    mutable int accesses;
    ...
    };
    const data veep = {"Claybourne Clodde", 0, ...};
    strcpy(veep.name, "Joye Joux"); //not allowed
    veep.accesses++; // allowed
  • const

    在C++种,const限定符对默认存储类型稍有影响,在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。

9.2.8 函数和链接性

所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。

在默认情况下,函数的链接性为外部的,即可以在文件间共享。(可以在函数原型中使用extern来指出函数是在另一个文件中定义的)。

可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用(必须同时在原型和函数定义中使用该关键字)

9.2.9 存储方案和动态分配

动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。通常,编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,一块用于动态存储。

new如果找不到请求的内存量,将会引发异常std::bad_alloc.

通常new运算符负责在堆中找到一个能够满足要求的内存块,定位运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。

delete只能用于指向常规new运算符分配的对内存的指针。

9.3 名称空间

生命区域(declaration region):可以在其中进行声明的区域。
潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。

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

全局名称空间对应于文件及声明区域,之前所说的全局变量现在被描述为位于全局名称空间中。

名称空间是开放的,即可以把名称加入到已有的名称空间中。可以通过作用域解析运算符(::)来访问给定名称空间中的名臣给。

using声明使特定的标识符可用(将特定的名称添加到它所属的声明区域中),using编译指令使整个名称空间可用。

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

可以使用namespace给名称空间创建别名,如:

1
2
namespace my_very_favorite_things{...};
namespace mvft = my_very_favorite_things;//使mvft称为my_very_favorite_things的别名

可以通过省略名称空间的名称来创建未命名的名称空间,但不能在未命名的名称空间所属的文件之外的其他文件中使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。

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