Chapter9 内存模型和名称空间
9.1 单独编译
- 如果只修改了一个文件,则可以只重新编译该文件,然后将其与其他文件的编译版本链接
- 与其将结构声明加入到每一个文件中,步入将其放在头文件中,然后在每一个文件中包含头文件
- 可以将程序分为三部分:
- 头文件:结构声明,使用结构的函数原型
- 源代码文件:结构相关函数代码
- 调用文件:调用函数的代码
- 不能再头文件中包含函数定义,再在其他两个源文件中包含该头文件
- 头文件中应该包含的内容:
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数
- 可以将结构声明放在头文件里,因为不会创建变量,只是告诉编译器如何创建变量;模板声明不是将被编译的代码,而是指示编译器如何生成函数定义;被声明为const的函数和内联函数有特殊的链接属性。
- 保护方案:
#ifndef C #define C #endif - C++标准允许每个编译器设计人员实现不同的名称修饰,因此不用编译器生成的对象文件可能无法正确链接
9.2 存储持续性、作用域和链接性
- C++中有四种方案存储数据,区别在于数据保留在内存里的持续时间
- 自动存储持续性(在栈里):在函数定义中声明的变量和函数参数,在开始执行代码块时创建,结束执行时释放
- 静态存储持续性:在函数外定义的变量,使用static定义的变量,在程序的整个运行过程都存在
- 线程存储持续性:用关键词thread local声明,生命周期和所属线程一样长
- 动态存储持续性(在堆里):用new分配的内存直到delete都不会被删除
9.2.1 作用域和链接
9.2.2 自动存储持续性
- 关键字register最初是由C语言引入的,建议编译器使用CPU寄存器来存储自动变量来提高访问变量的速度。在C++11中,只是显式地指出变量是自动的
9.2.3 静态持续变量
- C++为静态存储持续性变量提供了3种链接性,这3种链接型都能在程序执行期间存在
- 外部链接性:可在其他文件中访问
- 内部链接性:只在当前文件中访问
- 无链接性:只能在当前函数或代码块中访问
- 由于静态变量的数目在程序运行期间是不变的,不需要使用特殊的结构管理;编译器将分配固定的内存块来存储所有的静态变量
- 如果没有显式初始化静态变量,编译器将把它们设置为0.静态数组和结构每个元素或成员变量都设置为0。
int glabal = 1000; //静态持续性,外部链接性
static int one_file; //静态持续性,内部链接性
void function(int n){
static int count = 0; //静态持续性,无链接性
}
//即使在func1没有被执行时,count也留在内存中
变量的5种存储方式
存储描述 | 持续性 | 作用域 | 链接性 | 声明方式 |
---|
自动 | 自动 | 代码块 | 无 | 在代码块中 | 寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register | 静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static | 静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数中 | 静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数中,使用关键字static |
- 在C++代码中,空指针用0表示,但内部可能用非零表示,因此指针变量将被初始化相应的内部表示
- 零初始化和常量表达式初始化统称为静态初始化,这意味着编译器在处理文件时初始化变量,而动态初始化意味着在编译后初始化
- 首先所有静态变量被0初始化,接下来如果使用常量表达式初始了变量则编译器将根据文件内容计算表达式(如 int enough = 2sizeof(long)+1),否则将采用动态初始化(如:const double pi = 4.0atan(1.0))
9.2.4 静态持续性,外部链接性
- C++有两种声明:定义声明(defining declaration)和引用声明(referencing declaration)
- 定义声明,简称为定义,为变量分配内存空间
- 引用声明,简称为声明,不为变量存储空间,因为是引用已有的变量:使用关键字extern,且不进行初始化
double up; //定义声明
extern int blem; //引用声明
extern char gr = 'z' //定义声明
- 如果要在多个文件使用外部变量,只需在一个文件中包含该变量的定义声明,但在使用该变量的其他所有文件中,都必须使用extern声明它
- 作用域解释符::放在变量名前面时,该运算符表示使用变量的全局版本
- 全局变量可以被所用函数访问,但程序会不可靠:解决方法是使用const避免不该的修改
const char* const months[12] = {
"Jan",....
}
//第一个const防止字符串被修改,第二个const确保数组中的每一个指针始终指向它最初指向的字符串
9.2.5 静态持续性,内部链接性
9.2.6 静态持续性,无链接性
- 它在两次函数调用之间仍然存在,值保持不变
- 只在启动时进行一次初始化,以后调用时不执行初始化
9.2.7 说明符和限定符
- 关键字thread_local指出变量的持续性与其线程的持续性相同。thread_local变量之于线程正如静态变量之于程序
- const限定符
- 内存被初始化后,程序不能对其进行修改
- 默认情况下全局变量的链接性为外部的,但const全局变量的链接性是内部的(就像使用了static说明符一样)——这是因为通常把const放在头文件里。
- 内部链接性还意味着每个文件都有自己的一组常量
- 如果希望某个常量的链接性为外部的,可以用extern来覆盖内部链接性:extern const int states = 50;且必须在使用变量的外部文件中使用extern声明。
- 在函数或代码块中声明const时,其作用域为代码块
- mutable
- 指出即使结构或类变量为const,其某个成员也可以声明为mutable,从而可被修改
- volatile限定符
- 即使程序没有对其修改,其值也可能发生变化(by硬件)。
- 告诉编译器不要执行对这种类型执行调到寄存器的相关优化
9.2.8 函数和链接性
- 所有函数的持续性都是静态的
- 默认情况下函数的链接性为外部的,可以使用关键字static将函数的链接性设为内部的,必须同时在原型和函数定义中使用该关键字
- 对于每一个非内联函数,程序只能包含一个定义
- 对于链接性为外部的函数来说,在多文件程序中只能有一个文件包含该函数的定义,但使用函数的每个文件都该包含其函数原型
- 内联函数不受单定义规则约束,因此程序员可以将内联函数的定义放在头文件中:不过同一个函数的所有内联定义都必须相同
- C++查找函数定义的顺序:
- 如果函数原型指出函数是静态的,则只在该文件中查找函数定义;否则将在所有的程序文件中查找
- 如果找到两个定义,编译器将发出错误信息
- 如果在文件中没有找到,将在库中搜索:这意味着文件中定义的函数会覆盖同名库函数
9.2.9 语言链接性
- 由于C++有名称修饰,当使用C函数时可以用函数原型来指出:
extern "C" void spiff(int); //用C的protocol来找函数
extern void spiff(int); //用C++的protocol来找函数
extern "C++" void spiff(int); //用C++的protocol来找函数
9.2.10 存储方案和动态分配
- 动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制:因此可以在一个函数中分配动态内存,而在另一个函数中释放
- 编译器使用三块独立的内存空间:用于自动变量,用于静态变量,用于动态存储
- 动态存储分配的内存返回地址赋给指针变量p,如果是外部链接性的需要在使用的地方声明: extern float* p;
- 在程序结束时由new分配的内存通常将被释放,但在某些情况下请求大型内存块将导致程序结束不会自动释放
int *pi = new int (6); //*p 设为 6
double* pd = new double (99.99) //*pd 设为99.99
C++11支持:
struct where {double x; double y; double z};
where *one = new where {2.5, 6.3, 4.2};
int* ar = new int [5] {26,7,3,5,6};
9.3.1 传统的C++名称空间(补图9.6)
- 几个基本概念:
- 声明区域:可以在其中声明的区域
- 潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾
- 作用域:变量对于程序而言可见的区域
- 变量并非在潜在作用域都可见,可能被隐藏
p267 图9.6
9.3.2 新的名称空间特性
- 一个名称空间中的名称不会与另一个名称空间的相同名称冲突
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能在代码块中
- 名称空间中声明的名称的链接性默认为外部的(除非它引用了常量)
- 全局名称空间,对应于文件级声明区域,全局变量位于全局名称空间中
- using声明
- 例using Jill::fetch可以用fetch代替Jill::fetch
- 在函数外面使用using声明,将把名称添加到全局名称空间
- 不能在函数中同时存在using Jill::fetch和局部的fetch;这比using编译指令更安全,后者会直接让局部的来覆盖
- using编译指令
- using namespace Jill可以使用名称空间中所有名称
- 局部声明的函数名fetch将隐藏内部using namespace Jill的fetch;换句话说就是不能用fetch代替Jill::fetch了,需要显式使用Jill::fetch
9.3.4 名称空间及其前途
一些使用名称空间的建议:
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量或静态全局变量
- 不要再头文件中使用using编译指令,如果非要使用应该放在所有#include之后
- 导入名称时首选使用作用域解析运算符或using声明
- 对于using声明,首选将其作用域设为局部而不是全局
Chapter12 类和动态内存分配
12.1 动态内存和类
12.1.2 特殊成员函数
-
如果用户没有自己定义,C++自动提供了以下成员函数:
-
隐式地址运算符返回调用对象的地址(即this指针的值) -
C++11提供了另外两个特殊成员函数:移动构造函数、移动赋值运算符
12.1.4 Stringbad的其他问题:赋值运算符
- C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的:
- ClassName & ClassName::operator= (const ClassName&)
- 赋值运算符的隐式实现也是堆成员逐个复制,这也可能导致浅复制的问题
12.2.1 修改后的默认构造函数
12.2.3 使用中括号表示访问字符
- 可以使用方法operator来重载访问运算符
- 在city[0]中,city是第一个操作数,[]是操作符, 0是第二个操作数
- 对于const对象,应该重载[]运算符,否则将不可写
12.2.4 静态类成员函数
- 可以将成员函数声明为静态的,不能通过对象调用静态成员函数
- 如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用
- 静态成员函数只能使用静态数据成员
12.3 在构造函数中使用new时应该注意的事项
- 如果有多个构造函数,必须用相同的方法使用new,因为只有一个析构函数,必须和它兼容
- 复制构造函数应该:
- 做到深复制,分配新空间复制数据
- 更新所有受影响的静态
- 赋值运算符应该
- 做到深复制:检查自我赋值的情况,释放成员指针以前指向的内存,返回一个指向调用对象的引用
12.4.1 返回指向const对象的引用
- 返回对象将调用复制构造函数,而返回引用不会
- 引用指向的对象应该在调用函数执行时存在
12.4.2 返回指向非const对象的引用
- 常见的返回非const对象引用的情景是:
- 重载赋值运算符:用于连续赋值
- 重载与cout一起使用的<<运算符:用于串接输出
12.4.3 返回对象
- 如果被返回的对象是被调用函数中的局部变量,则不应该按引用的方式返回他,因为函数返回前将调用局部对象的析构函数
12.4.4 返回const对象
- 如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回这种对象的引用
12.5 使用指向对象的指针
12.5.2 指针和对象小结
- 使用对象指针时:
- 可以将指针初始化为指向已有的对象
- 可以使用new来初始化指针,这将创建一个新的对象
- 对类使用new将调用相应的类构造函数来初始化新创建的对象
- 用->访问类方法,用解除引用运算符*来获得对象
12.5.3 再谈定位new运算符
- 对于使用定位new运算符创建的对象,应该以与创建顺序相反的顺序进行删除:因为晚创建的对象可能依赖于早创建的对象
|