C++ Prime Plus 知识点整理 - 第九章 内存模型和命名空间

第九章 内存模型和命名空间

1. 单独编译

  • 程序可以分为如下三部分,可见这与oop方法一致,声明和定义分开;
  1. 头文件:包含结构的声明和使用这些结构的函数的原型;
  2. 源代码文件:包含与结构有关的函数的代码;
  3. 源代码文件;包含调用与结构相关的函数的代码;
  • 如果头文件里定义函数,除非是内联函数,否则将出错;如下内容可以放到头文件中:
  1. 函数原型;
  2. 使用#define或const定义的符号常量
  3. 结构声明
  4. 类声明
  5. 模板声明
  6. 内联函数
  • 包含头文件时,尖括号与双引号的区别:尖括号在存储标准头文件的文件系统中查找,双引号先在当前目录或源代码目录(或其他目录,取决于编译器)查找,如果没有再去标准头文件存储的位置查找;
  • #ifdef…#endif技术,使用这个方法可以防止重定义错误
#ifndef XXX
...
#endif

#ifndef XXX
#define XXX
...
#endif

#ifdef XXX1
#elif XXX2
...
#endif
  • 目前套路的都是编译文件,然后C++标准使用属于翻译单元(translation unit),而不是文件,因为文件并不是计算机组织信息时的唯一方式;不同编译器对函数生成不同的修饰名称,所以编译之前请确保都是使用同一个编译器编译;

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

  • 一共有四种存储持续性:自动存储持续性、静态存储持续性、线程存储持续性(c++11新增)、动态存储持续性;

自动存储持续性:函数定义中声明的变量(包括函数参数)的存储持续性为自动的。程序开始执行其所属的函数或代码块时被创建,执行完函数或代码块时,它们使用的内存被释放C++有2种存储持续性为自动的变量
静态存储持续性:函数定义外定义的变量或使用关键字static定义的变量的存储持续性为静态。它们在程序整个运行过程中都存在C++有3种存储持续性为静态的变量
线程存储持续性:对于多核处理器,CPU可以同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果使用关键字thread_local声明的,则其生命周期与所属的线程一样长
动态存储持续性用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。

  • 作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见
  • 链接性(linkage)描述了名称如何在不同单元间共享
  • 不同的C++存储方式是通过存储持续性、作用域和链接性来描述的;
2.1 自动存储持续性
  • 函数中的参数和函数中声明的变量存储持续性为自动,作用域为局部,无链接性
  • C语言以前的auto用于指定变量为自动存储类型,C++用于自动类型推断;
  • 自动变量的数目随函数的开始和结束而增减,因此程序在运行时必须进行自动变量的管理,常用方法是留出一段内存,将其视为栈,来管理变量;栈的默认长度取决于实现,但编译器可以更改栈的长度;具体操作:程序使用两个指针来跟踪栈,一个指针指向栈底,一个指向下一个可用栈空间,当函数被调用时,通过入栈操作分配参数内存并存入数值,当函数结束时,栈顶指针直接重置为函数被调用前的值,从而释放自动变量的内存;
  • 寄存器变量使用关键字register,最初由C语言引入,它建议编译器使用CPU寄存器来存储自动变量,旨在提高访问变量的速度,然而C++11中关键字register用于显示地指出变量时自动的,这与C语言的auto变量用途一致;之所以保留register是为了避免使用了改关键字的现有代码非法;
2.2 静态持续持续性
2.2.1 静态持续变量
  • C++静态存储持续行变量有3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问);由于静态变量的数目在运行时不改变,因此不需要进行管理;

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

  • 所有的静态持续变量都有下属初始化特征,未被初始化的静态变量所有位置都被设置为0,这种变量被称为零初始化的(zero-initialized);

  • 零初始化和常量表达式初始化被统称为静态初始化,即编译器处理文件(翻译单元)时初始化变量动态初始化指变量将在编译后初始化;所有静态变量都会先被零初始化(即静态初始化),再看是否需要动态初始化;

#include <cmath>
int x;						// 静态初始化(零初始化)
int y = 1*2; 				// 静态初始化(零初始化、常量表达式初始化)
const int z = 4 * atan(1.0);// 动态初始化(零初始化、动态初始化)
  • C++11 新增了官架子constexpr,增加了创建常量表达式的方式;
2.2.2 静态持续性、外部链接性
  • 链接性为外部的变量通常简称为外部变量,存储持续性为静态,作用域为整个文件,由于外部定义的变量其后的任何函数中使用它,因此外部变量也全局变量;
  • C++的单定义规则指出,变量只能有一次定义,因此C++提供了两种边拉你个声明,一种是定义声明(defining declaration)或简称为定义,它给变量分配存储空间;另一种是引用声明(referencing declaration)或简称声明,它不给变量分配内存空间,因为它引用已有的变量;单定义规则并不意味着不能有多个相同名称的变量;
  • 引用声明使用关键字extern,且不能进行初始化,否则变为定义声明;如果局部变量和全局变量重名,则使用作用域解析运算符::可以在局部使用同名的全局变量
  • 全局变量容易导致程序不可靠,(程序越能避免对数据进行不必要的访问,越能保持数据的完整性?);
2.2.3 静态持续性、内部链接性
  • 将限定符static用于作用域为整个文件的变量时,该变量的链接性将为内部的,该变量只能在所属的文件中使用,因此不同文件可以定义名称相同的内部链接的全局变量;
2.2.4 静态持续性、无链接性
  • 将static限定符作用于代码块中定义的变量,该局部变量的存储持续性为静态的,该变量只能在该代码块中使用,且该代码块不处于活动状态时仍然存在;
2.2.5 总结

在这里插入图片描述

2.3 说明符和限定符
  • 存储说明符(storage class specifier),thread_local可与static或extern结合使用,其他的只能有一个
  1. auto(c++11中不再是说明符)
  2. register
  3. static
  4. extern
  5. thread_local(c++11新增)
  6. mutable:指出即使结构或类变量为const,其中某个成员也是可被修改的
  • cv-限定符,
  1. const
  2. volatile:关键字volatile表示即使程序代码没有对内存单元进行修改,其值也可能发生变化,每次读取必须读取内存而不是读取寄存器中的缓存;该关键字用于改善编译器的优化能力;
  • const变量默认情况下链接性为内部的,如果要定义链接性为外部的常量,需要使用extern关键字来定义
2.4 函数的链接性
  • 函数链接性默认为外部,可以使用static定义函数的链接性为内部,使之只能在一个文件中使用,但必须在原型和函数定义中同时使用该关键字;同变量一样,静态函数将覆盖外部定义,单定义规则也适用于非内联函数,内联函数不受这项规则的约束,但同一个函数的所有内联定义都必须相同;
  • 如果文件中声明了静态函数,则先在此文件中找函数定义,否则编译器会在整个程序文件中查找函数定义,如果没找到,则将在库中进行搜索,这意味着定义了与库函数同名的函数,则优先调用定义的函数;
2.5 语言的链接性
  • C++执行名称矫正(名称修饰),这种方法叫做C++语言的链接;要在C++中使用C语言的程序需要使用函数原型来指出要使用的约定:
extern "C" void test(int);	// 使用C语言链接性
extern void test(int);		// 使用C++语言链接性,默认方式
extern "C++" void test(int);// 使用C++语言链接性
2.6 存储方案和动态分配
  • 前面的5种方案不适合使用new(或c的malloc)分配的内存,这种成为动态内存;编译器使用三块独立的内存:一块用于静态变量、一块用于自动变量、一块用于动态存储
  • 如果使用new为内置的标量类型分配存储空间并初始化使用小括号,如#1所示,此方法也可用于有合适构造函数的类;如果要初始化常规结构或数组,应该使用大括号的列表初始化,如#2所示,此要求编译器支持C++11;如果new分配内存失败,将抛出std::bad_alloc的异常;
// #1
int *pi = new int(6);
// #2
struct where{double x, double y, double z};
where *one = new where{2.5, 1.5, 3.7};
int *ar = new int[4] = {1, 3, 5, 7};
  • 运算符new和new[]分别调用如下函数,这些函数被称为分配函数,同样也有delete和delete[];std::size_t对应于合适的整型; 如下面的举例,C++将这些函数成为可替换的,因此可以根据需要手动为new和delete提供替换函数
void * operator new(std::size_t);
void * operator new[](std::size_t);
void * operator delete(std::size_t);
void * operator delete[](std::size_t);
// 举例:
int *pi = new int;// 等价为 int *pi = new(sizeof(int));
  • new负责在堆中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让你能够指定要使用的位置,可以使用这种特性来设置内存管理规程、处理需要通过特定地址访问的硬件或在特定位置创建对象;使用定位new运算符需要包含头文件new,使用方法如下;由于定位new的特性,可以让其与初始化结合,直接映射硬件地址要注意,不可以使用delete释放定位new分配的内存,因为delete用于指向常规new分配的内存
// XType为某自定义类型
char buffer1[100];
XType * p1;
p1 = new (buffer1)XType; // 将p1分配到指定的buffer1里
  • 和常规new运算符一样,定位new运算符也是一个函数,但定位new函数不可替换,但可重载,至少接受两个参数,第一个总是std::size_t;
int *p1 = new (buffer1)int; // 上面那句话等价为p1 = new(sizeof(int), buffer1);

3. 名称空间

  • 声明区域是可以在其中进行声明的区域潜在作用域一个变量或函数理论上起作用开始的位置和结束的位置,潜在作用域比声明区域小,因为先定义再使用;变量并非在其潜在作用域内的任何位置都是可见的,局部变量可能会隐藏原来的同名变量;变量对程序而言可见的范围称为作用域

在这里插入图片描述
在这里插入图片描述

  • C++使用一种新的声明区域,来创建命名的名称空间,称为名称空间;名称空间可以使全局的,也可以是另一个名称空间中,但不能位于代码块中,因此名称空间连接性为外部性(除非引用了常量);除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间,它对应于文件级声明区域,因此所说的全局变量都位于全局名称空间中
  • 不同命名空间的名称不会产生冲突,名称空间是开放的,即同一个名称空间可以分成多份来编写,如下所示;要访问名称空间的名称,使用作用域解析运算符::,使用名称空间来限定;名称空间的开放也意味着很那确定在那个地方添加了某个名称;
//head.h
namespace TEST{
	int a = 23;
	void abc(void);
}
namespace PASS{
	int a = 44;// 不会与TEST空间的a冲突
}
//src.h
#include "head.h"
namespace TEST{
	void abc(void)// 在一个地方声明,在另一个地方定义
	{
		std::cout<<123<<std::endl;
	}
}
//main.c
#include "head.h"
int main(){
	TEST::abc(); // 名称空间::要访问的名称
	return 0;
}
  • 为了简化访问操作,可以使用using声明或者using编译指令;using声明使特定的标识符可用,using编译指令使整个名称空间都可用
using namespace std;// std名称空间所有名称可用
using std::cout;	// std名称空间的cout可用
  • 使用using编译指令引入已经在函数中声名的相同名称将会隐藏名称空间的名称,不过仍可以使用作用域解析运算符访问,但如果使用的是using声明则会出错;因此使用using声明更安全,因为如果重名编译器会发出错误警告,而using编译指令不会发出任何警告;
  • 名称空间可以嵌套,也可以使用using编译指令和using声明;
  • 通过省略名称空间的名称来创建未命名的名称空间此名称空间只用于该文件,者提供了链接性为内部的静态变量的替代品
  • 命名空间的使用原则
  1. 使用已命名的名称空间中声明的变量,而不是使用外部全局变量;
  2. 使用已命名的名称空间中声明的变量,而不是使用静态全局变量;
  3. 如果开发了一个函数库或类库,应该放在一个名称空间中;
  4. 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计;
  5. 不要在头文件中使用using编译指令;这样做掩盖了要让哪些名称可用,另外包含头文件的顺序可能影响程序的行为。如果非要用using编译指令,应该将其放在所有预处理器编译指令#include之后;
  6. 导入名称时,首选使用作用域解析运算符或using声明的方法;
  7. 对于using声明,首选将其作用域设置为局部而不是全局;
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值