导图社区 C++ 之第九章:内存模型和命名空间
《C++ Primer Plus》第九章同步思维导图,介绍C++有关内存与命名空间的知识,属于C++的特性。内容详细充实,值得一看。
编辑于2021-01-02 19:24:33第9章 内存模型和命名空间
9.1 单独编译
三大部分
· 头文件
包含结构声明和使用这些结构的函数原型
· 源代码文件
包含于结构有关的函数的代码
· 源代码文件
包含调用与结构相关的函数的代码
头文件通常包含的内容
不要将函数定义或变 量声明放到头文件中。
· 函数原型
· 使用#define或const定义的符号常量
· 结构声明
· 类声明
· 模板声明
· 内联函数
头文件管理
C++规定,在同一个文件中只能将同一个头文件包含一次。为了避免重复包含,我们使用以下的方法: #ifndef COORDIN_H_ #define COORDIN_H_ ... #endif 编译器首次遇到该文件时,名称COORDIN_H_没有被定义。在这种情况下编译器将查看#ifndef和#endif之间的内容,读取并定义COODIN_H_的一行。如果编译器在同一个文件中遇到其他包含coordin.h的代码,编译器将知道COORDIN_H_已经被定义了,从而调到#endif后面的一行上。注意,这种方法不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
9.2 存储持续性、作用域和链接性
概述
存储持续性复习
· 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量 · 静态存储持续性:在函数外面定义的常量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量 · 线程存储持续性(C++11):当前,多核处理器很常见,这CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。 · 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)
9.2.1 作用域和链接
作用域(scope):描述了名称在文件(翻译单元)的多大范围内可见 链接性(linkage):描述了名称如何在不同单元间共享。自动变量的名称没有链接性,因为它们不可共享
9.2.2 自动存储持续性
默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。作用域起点为其声明位置,在程序开始执行这些变量所属的代码块时,为其分配内存;当函数结束时,变量都自动消失 如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。如: int main() { int a = 5; { int b = 0; } return 0; } a、b在代码块内部都是可见的,都是可以使用的。但是在代码块外部,a是可见的,而b不可见,因为b在外部不存在。 如果代码块中也定义了一个int变量a。那么这种情况下在程序执行内部代码块时,将a解释为局部代码块变量。我们说,新的定义隐藏了以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义又重新可见
1. 自动变量的初始化
2. 自动变量和栈
程序管理自动变量的常用方法是留出一段内存,并将其视为栈,以管理变量的增减。之所以被称之为栈,是由于新数据象征性地放在原有数据的上面(也就是说,在相邻的内存单元中,而不是在同一个内存单元中)。当程序使用完后,将其从栈中删除
3. 寄存器变量
关键字register(只能用于自动变量) 它建议编译器使用CPU寄存器来存储自动变量,旨在提高访问变量的速度: register int count_fast; // request for a register variable 这种提示表明变量用的很多,编译器可以对其做特殊处理
9.2.3 静态持续变量
· 创建链接性为外部的静态持续变量:在代码块的外面声明 · 创建链接性为内部的静态持续变量:在代码块的外面声明,并使用static限定符 · 创建没有链接性的静态持续变量:在代码块内部声明,并使用static限定符 如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素成员的所有位都设置为0
静态变量的初始化
零初始化和常量表达式初始化统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着将变量在编译后初始化。
首先,所有静态变量都被零初始化,而不管程序员是否显式地初始化了它。接下来,如果使用常量表达式初始化了变量,且编译器仅根据文件内容(包括头文件)就可计算表达式,编译器将执行常量表达式初始化。必要时,编译器将执行简单计算。如果没有足够的信息,变量将动态初始化: #include<cmath> int x; int y = 5; long z = 13 * 13; const double ppi = 4.0 * atan(1.0); 首先,x、y、z和pi都被零初始化,然后编译器计算常量表达式,并将y和z分别初始化为5和169。但要初始化pi,必须调用函数atan(),这需要等待该函数被链接且程序执行时。
9.2.4 静态持续性、外部链接性
链接性为外部的变量通常称为外部变量,它们的储存持续性为静态,作用域为整个文件。也称全局变量。
1. 单定义规则
· 在每使用外部变量的文件中,都必须声明它 · 变量只能有一次定义。
C++提供两种变量声明: (1)定义声明(简称为定义):给变量分配储存空间 (2)引用声明(简称为声明):不给变量分配空间,因为它引用已有的变量 引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配储存空间。如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则)但在使用该变量的其他所有文件中,都必须使用关键字extern来声明它: // file01.cpp extern int cats = 20; // definition because of initialization int dogs = 22; // also a definition int fleas; // also a definition // file02.cpp extern int cats; extern int dogs;
2. 程序说明
C++提供了作用域解析运算符(::)。放在变量名前面时,该运算符表示使用变量的全局版本。
9.2.5 静态持续性、内部链接性
将static限定符用于作用域为整个文件的变量时,该变量的链接性将变为内部的,即只能在所属文件中使用。
9.2.6 静态存储持续性、无链接性
将static限定符用于在代码块中定义的变量,该变量便无链接性,只能在该代码块中使用,但在该代码块不处于活动状态时依然存在,值将保持不变。另外,如果初始化了静态局部变量,则程序只将在启动时进行一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。
while (cin) { cin.get(next); while (next != '\n') cin.get(next); ... } 该代码实现了整行数据处理,并且告诉我们,试图使用cin.get(char*, int)读取空行将导致cin为false
9.2.7 说明符和限定符
1. 存储说明符: · auto:自动类型判断 · register:指示寄存器存储 · static:表示内部连接性或静态存储持续性 · extern:引用声明 · thread_local:指出变量的持续性与其所属线程的持续性相同 · 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
2. cv-限定符: · const:设置变量为常量,不能被修改 · volatile:声明告诉编译器,即使程序代码没有对内存单元进行修改,其值也可能发生变化(如其他程序的影响等)。 例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将其缓存到寄存器中。这种优化假设变量的值在两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化。
3. 再谈const (1)在C++(不是C)语言中,const限定符对默认储存类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局定义const变量就像使用了static一样。也因如此,每个const定义都是其所属文件私有的,这就是能够将const常量放在头文件中的原因。 (2)如果想要将某个常量的链接性设为外部,让其他文件也可以共享它,则可以使用extern关键字来覆盖默认的内部连接性: extern const int states = 50; // definition with external linkage 在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它,而只在其中一个文件里对其初始化。
9.2.8 函数和链接性
所有函数的存储持续性都自动为静态的,即整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以再文件之间共享。实际上,可以在函数原型中使用关键字extern来指出函数是在另一个文件中定义的,不过这也是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,后者是由链接程序搜索的库文件)。还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。但必须同时在原型和函数定义中都是用static。
9.2.9 语言链接性
C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性。 C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。如,可能将spiff(int)转换为_spiff_i,而将spiff(double, double)转换为_spiff_d_d。这种方法被称为C++语言链接。
9.2.10 储存方案和动态分配
通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分);一块用于自动变量;另一块用于动态存储。
1. 使用new运算符初始化
(1)初始化内置的标量类型: int* pi = new int (6); double* pd = new double (99.99); (2)要初始化常规数组或结构,需要使用大括号的列表初始化(仅C++11支持): struct where { double x; double y; double z; }; where* one = new where {2.5, 5.3, 7.2}; (3)也可用列表进行单值变量的初始化: int* pin = new int {}; double* pdo = new double {99.99};
2. new失败时
在最初的10年中,C++让new返回空指针,但现在是引发异常std::bad_alloc。
3. new:运算符、函数和替换函数
运算符new和new[]分别调用如下函数: void* operator new(std::size_t); // used by new void* operator new[](std::size_t); // used by new[] 同样,也有由delete和delete[]调用的释放函数: void operator delete(void*); void operator delete[](void*); 它们都位于全局名称空间中。因此对于下面的基本语句: int* pi = new int; 被转换为下列语句: int* pi = new(sizeof(int)); 而下面的语句: int* pa = new int (40); 被转换为下面这样: int* pa = new(40 * sizeof(int)); 同样,下面的语句: delete pi; 被转换为下列语句调用: delete (pi);
4. 定位new运算符
new运算符还有一种变体,被称为定位new运算符,它能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
5. 程序说明
部分程序: char buffer[512]; int main() { double* pd1, *pd2; pd1 = new double[5]; pd2 = new (buffer) double[5]; ... }
· 定位new运算符确实将数组p2放在了数组buffer中 · 定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。 · buffer指定的内存是静态内存,而delete只适用于这样的指针:指向常规new运算符分配的堆内存。也就是说,数组buffer位于delete的管辖区之外,因此下面的语句将发生错误: delete[] pd2; // won't work
6. 定位new的其他形式
标准定位new调用一个接收两个参数的new()函数: int* p1 = 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)
9.3 名称空间
9.3.1 传统的C++名称空间
· 声明区域:可以在其中声明的区域 · 潜在作用域:从声明点开始,到声明区域结尾
9.3.2 新的名称空间特性
一个名称空间的名称不会与另外一个名称空间的相同名称发生冲突,同时,允许程序的其他部分使用该名称空间中声明的东西。例如,使用关键字namespace创建名称空间Jack: namespace Jack { double pail; void fetch(); int pal; }
1. using声明和using编译指令
· using声明:由被限定的名称和它前面的关键字using组成。 using Jack::pail; 完成声明后,便可以用pail代替Jack::pail。
· using编译指令:使名称空间的所有名称可用。 using namespace std;
2. using编译指令和using声明之比较
一般来说,using声明比使用using编译指令更安全
3.名称空间的其它特性
(1)嵌套使用
(2)创建别名
假设有以下的命名空间: namespace my_very_favorite_things { .. }; 则可以使用下面的语句让mvft称为my_very_favorite_things的别名: namespace mvft = my_very_favorite_things; 可以使用这种技术来化简对嵌套名称空间的使用: namespace MEF = myth::elements::fire; using MEF::flame;
4. 未命名的名称空间
不能在未命名的名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品: static int counts; // static storage, internal linkage int other(); int main() { ... } 用未命名的名称空间的方法如下,效果一样: namespace { int counts; // static storage, internal linkage } int other(); int main() { ... }
9.3.3 名称空间示例
9.3.4 名称空间及其用途
指导原则
· 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
· 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
· 如果开发了一个函数库或类库,将其放在一个名称空间中。C++提倡将标准库函数放在名称空间std中
· 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计
· 不要在头文件中使用using编译指令,这样做掩盖了要让哪些名称可用
· 如果非要在头文件中使用编译指令using,应将其放在所有预处理器编译指令#include之后
· 导入名称时,首选使用作用域解析运算符或using声明的方法
· 对于using声明,首选将其作用域设置为局部而不是全局