《C++PrimerPlus 6th Edition》第9章 内存模型与名称空间 要点记录
本章内容
- 单独编译
- 存储持续性、作用域和链接性
- 定位(placement) new 运算符
- 名称空间
9.1 单独编译
- 程序拆分:①头文件:包含结构声明和使用这些结构的函数原型;②源代码头文件:包含与结构有关的函数的代码;③源代码文件:包含与结构相关的函数调用;
- 头文件包含的内容:①函数原型;②使用#define或const定义的符号常量;③结构声明;④类声明;⑤模板声明;⑥内联函数;
- 在同一个文件中只能同一个头文件包含一次,一种防护(guarding)方案如下:
#ifndef NAME //如果没有定义过NAME,则执行ifndef与endif之间的代码 #define NAME header file contents //头文件中的内容放在这里 #endif
【注意】
- 不要将函数定义或变量声明放在头文件中;
- 模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义;
- 包含自己的头文件时,应使用引号而不是尖括号;
- 讨论单独编译时,C++使用术语翻译单元(translation unit)而不是文件,因为文件并不是计算机组织信息时的唯一方式;
- 在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成的,这是由于不同编译器将为同一个函数生成不同的修饰名称,名称的不同将使链接器无法将一个编译器生成的函数调用与另一编译器生成的函数定义匹配。
9.2 存储连续性、作用域和链接性
【名词解释】
- 作用域(scope):描述名称在翻译单元的多大范围内可见;自动变量的作用域为局部,静态变量的作用域是全局还是局部取决于它是如何被定义的。例如,在函数原型作用域中使用的名称只在包含参数列表的括号内可用(这也是为什么这些名称是什么以及是否出现都不重要的原因);
- 链接性(linkage):描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享;
-
C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域是局部,则只对它自己是可见的,因此不能被其他函数调用。这样的函数将无法运行);
-
C++三种存储方案(包括C++11有四种,多了一种线程存储持续性)对比:
存储方案类别 描述 要点 自动存储持续性 在函数定义中声明的变量(包括函数参数)的存储持续性为自动的 ①C++11之后的auto再无"标记变量为自动存储"的功能;
②关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量;自C++11后,它只是显式地指出变量是自动的,现在仍使用它的原因是:指出程序员想使用一个自动变量,这个变量的名称可能与外部变量相同,保留它的重要原因——避免使用了该关键字的现有代码非法静态存储持续性 在函数定义外定义的变量和使用关键static定义的变量的存储持续性为都为静态 ①如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构的每一个元素或成员的所有位都设置为0;
②静态变量存储方式存储描述 作用域 链接性 如何声明 静态,无链接性 代码块 无 在代码块中使用关键字static 静态,外部链接性 文件 外部 不在任何函数内 静态,内部链接性 文件 内部 不在任何函数内,使用关键字static
③单定义规则(One Definition Rule):变量只能有一次定义;由此C++提供了两种变量声明:定义声明(给变量分配存储空间)和引用声明(引用已有的变量,使用关键字extern,且不进行初始化);
④如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它
⑤可使用外部变量在多文件程序中的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据;
⑥在代码块中使用static时,将导致局部变量的存储持续性为静态的,这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在;
⑦如果初始化了静态局部变量,则程序只在启动时进行一次初始化,以后再调用函数时,将不再初始化线程存储持续性(C++11) 当前,多核处理器很常见,这些CPU可同时处理多个执行任务,这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长(本书不探讨并行编程) 动态存储持续性 用new运算符分配的内存将一直存在,知道使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap) ①编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,另外一块用于动态存储;
②在程序结束时,由new分配的内存并不总是被释放,在某些情况下,请求大型内存块将导致该代码块在程序结束时不会被自动释放;
③new失败时,起初C++在这种情况下让new返回空指针,但现在将引发异常std::bad_alloc;
④分配函数和释放函数(位于全局名称空间中,且可以对它们进行替换和自定义):
void* operator new(std::size_t);
void* operator new[] (std::size_t);
⑤new定位运算符(后面会附相应习题作为示例):
char buffer[512];
double* pd;
pd = new (buffer) double[5];
pd = new (buffer + 5 * sizeof(double) ) double[5]; -
c-v限定符:
①关键字const:在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义就像使用了static说明符一样;如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性;鉴于单个const在多个文件之间共享,因此只有一个文件可对其进行初始化;
②关键字volatile:即使程序代码没有对内存单元进行修改,其值也可能发生变化。该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。因此,使用该关键字是为了告诉编译器不要进行这种优化; -
关键字mutable:即使结构(或类)变量为const,其某个成员也可以被修改。如下示例中变量
accesses
被声明为mutable
,因此即便变量veep
为const
修饰,其成员accesses
同样能修改。struct data{ char name[30]; mutable int accesses; }; const data veep = { "PPGod", 0}; strcpy(veep.name, "God Sheng"); // not allowed veep.accesses++; //allowed
-
函数和链接性:①和C一样,C++不允许在一个函数中定义另外一个函数,因此所有函数存储持续性都自动为静态的;②在默认情况下,函数的链接性为外部的,即可在文件间共享;③可在函数原型中使用关键字extern来指出函数是在另一个文件中定义,不过这是可选的;还可使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用,必须同时在函数原型和函数定义中使用该关键字;④内联函数由于其机制的原因,可以把函数定义放在头文件中,这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。
-
C++查找函数的原则:
- 如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找;
- 如果找到两个定义,则编译器将发出错误消息,因为每个外部函数只能有一个定义;
- 如果在程序文件中没找到,编译器将在库中搜索。
9.3 名称空间
- 搞清楚using声明与using编译的区别:如
using std::cout; //声明 using namespace std //编译;
- 名称空间可以嵌套,可以创建别名:
namespace my_very_favorite_things{...}; namespace mvft = my_very_favorite_things;
- using编译指令是可以传递的;
namespace A{ using namespace B; } //下面两种写法等价,体现了传递性 using namespace A; <===> using namespace A; using namespace B;
- 未命名的名称空间类似全局变量(从声明点到声明区域末尾),但其由于没有名称,故不能使用using编译或using声明来使用它,故提供了链接性为内部的静态变量的替代品;
static int counts; //1 //1的等价写法 namespace{ int counts; // static storage, internal linkage }
- 如果函数被重载,则一个using声明将导入所有版本;
using nspc_name::func; //func若有多个重载版本,则以上语句将会使func的所有版本全部导入
- 名称空间使用的指导原则:
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量;
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量;
- 如果开发了个函数库或类库,将其放在名称空间中;
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计;
- 不要在头文件中使用using编译指令;
- 导入名称时,首先使用作用域解析运算符或using声明的方法;
- 对于using声明,首先将其作用域设置为局部而不是全局;
习题
9-3 定位运算符new的使用
#include<iostream>
#include<cstring>
#include<new> //似乎有无该库导入,都可以正常使用定位运算符new
const int BUF = 512;
char buffers[BUF];
struct chaff{
char dross[20];
int slag;
};
int main(){
using namespace std;
chaff* cf1, *cf2;
cf1 = new (buffers) chaff[2]; // use buffers array
cf2 = new chaff[2]; //use heap
cout<<"&buffers = "<<&buffers<<endl;
cout<<"&cf1[0] = "<<cf1<<endl;
strcpy(cf1[0].dross, "Fuck you ");
cf1[0].slag = 777;
cout<<"&cf1[1] = "<<cf1+1<<endl;
strcpy(cf1[1].dross, "Leatherman");
cf1[1].slag = 4396;
cout<<"&cf1[1] - &cf1[0] = "<<&cf1[1] - &cf1[0]<<endl; //1,以数据元素为单位
cout<<"&cf2 = "<<cf2<<endl;
strcpy(cf2[0].dross, "Do you like ");
cf2[0].slag = 2800;
strcpy(cf2[1].dross, "what you see?");
cf2[1].slag = 1557;
cout<<"Data for cf1:\n";
for (int i=0; i<2; ++i){
cout<<"chaff #"<<i+1<<": "<<endl;
cout<<"dross: "<<cf1[i].dross<<endl;
cout<<"slag: "<<cf1[i].slag<<endl;
}
cout<<"Data for cf2:\n";
for (int i=0; i<2; ++i){
cout<<"chaff #"<<i+1<<": "<<endl;
cout<<"dross: "<<cf2[i].dross<<endl;
cout<<"slag: "<<cf2[i].slag<<endl;
}
delete[] cf2;
return 0;
}
【疑问】 似乎不管头文件new的有与无,都可以使用new作为定位运算符,在没有导入new头文件时我试过,没有问题,和导入new头文件时的结果没有区别,但书上说需要有new头文件的引入。请问各位大佬们,这是怎么回事?(注:本人用的是devC++,编译器支持C++11,版本为TDM-GCC 4.9.2 64-bit Debug)
2020-11-30
习题参考代码见我的github(上传后会把链接打上)。
欢迎各位大佬们于评论区进行批评指正~