zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

C++ 学习笔记——五、内存模型和名称空间

2023-04-18 14:25:37 时间

目录:点我

一、单独编译

通常程序会分为三个部分:

  • 头文件:包含结构声明和使用这些结构的函数的原型:
    • 函数原型;
    • 使用 #define 或 const 定义的符号常量;
    • 结构声明;
    • 类声明;
    • 模板声明;
    • 内联函数;
  • 源代码文件:包含与结构有关的函数的代码;
  • 源代码文件:包含调用与结构相关的函数的代码。
    1

头文件管理:在同一个文件中只能将同一个头文件包含一次,为防止意外,可以采用基于预处理器编译指令 #ifndef ,该指令将在没有执行内部指令的情况下才对其进行执行,即有选择的忽略内容:

#ifndef COORDIN_H_
...
#endif

多个库的链接:由于不同编译器所编译文件的名称修饰不同,可能导致链接器无法将一个编译器生成的函数调用与另一个编译器生成的函数定义匹配,因此在链接编译模块时,应确保所有对象文件或库都是由同一个编译器生成的。

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

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

  • 自动存储持续性:在函数定义中声明的变量,其存储持续性为自动的,生命周期只在函数执行过程中,共有 2 种存储持续性为自动的变量;
  • 静态存储持续性:在函数定义外定义的变量和使用关键字 static 定义的变量的存储持续性都为静态,在程序整个运行过程中都存在,共有 3 种存储持续性为静态的变量;
  • 线程存储持续性(C++ 11):由于多核处理器可以同时处理多个任务,这些程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字 thread_local 声明的,则其生命周期与所属的线程一样长;
  • 动态存储持续性:用 new 运算符分配的内存将一直存在,直到利用 delete 运算符将其释放或程序结束为止,这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。

1. 作用域和链接

作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见,如全局变量和局部变量。

链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

C++ 函数的作用域可以是整个类(类内成员函数)或整个名称空间(包括全局的),但不能是局部的(与 Python 不同,C++ 不能再代码块内定义函数)。

Ⅰ. 自动存储持续性

由于自动变量随着程序运行而增减,因此编译器通常留出一段内存,并将其视为栈来管理这些变量,之所以被称为栈,是因为新数据被象征性的放在原有数据的上面,也就是说在相邻的内存单元中,而不是再同一个内存单元中。

栈的默认长度取决于实现,但编译器通常使用两个指针来跟踪栈,一个指针指向栈底,另一个指针指向栈顶。当函数被调用时,其自动变量入栈,函数结束后,将其出栈。因此便可以理解为什么在函数内外可以使用相同命名的变量,这是因为在函数内部的变量先被访问到,因此就不会影响到更靠近栈底的变量。

对于寄存器变量,关键字 register 最初由 C 语言引入,用于建议编译器使用 CPU 寄存器来存储自动变量,旨在提高访问变量的速度。

在 C++ 11 中,该关键字只是显示地指出变量是自动的,鉴于该关键字只能用于自动变量,因此使用它的唯一原因是指出程序员想使用一个自动变量,而这个变量可能与外部变量名相同,这与以前 auto 关键字的用于相同。通常来说,保留该关键字只是为了避免该关键字的现有代码非法,而并无实际用处。

Ⅱ. 静态持续变量

静态存储持续性提供了 3 种链接性:

  • 外部链接性:又称为全局变量,可在其他文件中访问,必须在代码块的外部声明;
  • 内部链接性:只能在当前文件中访问,必须在代码块的外部声明,并使用 static 限定符;
  • 无链接性:只能在当前函数或代码块中访问,必须在代码块内声明,并使用 static 限定符,该变量即使函数未被调用仍存在。

这 3 种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的结构来进行管理,编译器将分配固定的内存块来存储所有的静态变量,默认值为 0 。

在每个使用外部变量的文件中,都必须进行声明,但因为 C++ 有单定义规则(One Definition Rule, ODR),因此提供了两种变量声明:

  • 定义声明:简称为定义,它给变量分配内存空间;
  • 引用声明:简称为声明,它不给变量分配空间或初始化,只是进行引用,使用关键字 extern 。

也就是说,在多文件中使用同一个全局变量时,只能在一个文件进行定义,在其他文件进行声明。

无链接性的静态变量只在第一次定义时进行初始化,后续将自动略过该步骤。

2. 说明符和限定符

存储说明符:

  • auto(C++ 11 中不再是说明符):自动类型推断;
  • register:指示寄存器存储;
  • static:静态变量声明;
  • extern:引用声明;
  • thread_local(C++ 11 新增):可与 static 和 extern 结合使用;
  • mutable:若某个结构或类变量为 const ,使用该说明符表示其某个成员可以被修改,也就是说该说明符具有去 const 的功能。

cv 限定符:

  • const:防止更改,默认情况下全局变量为外部链接性,而 const 限定符将其视作内部链接性,若要进行多文件声明,需在所有使用位置采用 extern 关键字,包括定义的时候,但仍然只能进行一次初始化;
  • volatile:若定义,则系统不会将多条语句所调用的某个可能为固定值的变量存储在寄存器来进行访问,若不定义,则可能进行这种优化。

3. 函数和链接性

函数默认是外部链接性的,但是可以通过 static 和 extern 显示的指明其链接性,必须在原型和函数定义中使用关键字。

单定义规则也适用于非内联函数,而内联函数不受约束,因此允许将其定义放入头文件,这样包含有文件的每个文件都有内联函数的定义,但是要求同一个函数的所有内联定义都必须相同。

4. 语言链接性

语言链接性(language linking)也对函数有影响。链接程序要求每个不同的函数都有不同的符号名,C 语言中一个名称只对应一个函数,但在 C++ 中,同一个名称可能对应多个函数,必须为这些函数翻译不同的符号名称,因此编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。这种方法被称为 C++ 语言链接:

// c
spiff(int)  // _spiff

// c++
spiff(int)  // _spiff_i
spiff(double, double)  // _spiff_d_d

那么如何在 C++ 中使用 C 库中预编译的函数?可以使用函数原型来指出要使用的约定:

extern "C" void spiff(int);  // c
extern void spoff(int);  // c++
extern "C++" void spaff(int);  // c++

5. 存储方案和动态分配

虽然动态变量与作用域无关,只与 new 和 delete 有关,但是其声明的变量仍然遵循存储方案的限定,例如在函数中声明的指针变量,虽然它指向的空间一直存在,但是该变量将在函数运行结束后被销毁,因此通常函数需要返回该指针来标记其占用的空间地址。

通常,new 负责在堆中找到一个足以能够满足要求的内存块,但其存在另一个变体,被称为(placement)new 运算符,能够指定要使用的位置,从而设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象,需要包含头文件 new 。

三、名称空间

在 C++ 中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。由于项目不断增大,不同厂商的名称可能存在冲突,因此 C++ 标准提供了名称空间工具,一边更好地控制名称的作用域。

1. 传统的名称空间

  • 声明区域(declaration region):可以在其中进行声明的区域,例如全局变量的声明区域为其声明所在的文件;局部变量的声明区域为其声明所在的代码块;
  • 潜在作用域(potiential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾,因此潜在作用域比声明区域小。

C++ 关于全局变量和局部变量的规则定义了一种名称空间层次,每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

说人话就是对于一个同名变量,函数内部的那个会覆盖函数外部的那个,循环内的会覆盖函数内的,互相之间不会产生冲突。

2. 新的名称空间特性

C++ 新增了一种功能,通过定义一个新的声明区域来创建命名的名称空间,这样不同名称空间之间不会产生影响,例如:

namespace Jack {
	double pail;
	void fetch();
	int pal;
	struct Well{...};
}
namespace Jill {
	double bucket(double n){...};
	double fetch;
	int pal;
	struct Hill {...};
}

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

除了用户定义的名称空间,还存在全局名称空间,对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。

名称空间是开放的,即可以把名称加入到已有的名称空间中,还可以为原来的名称空间中的函数原型提供具体代码:

namespace Jill {
	char * goose(const char *);  // 把名称goose加入到Jill中已有的名称列表中
}
namespace Jack{
	void fetch(){...};  // 为fetch原型提供代码
}

要访问名称空间可以使用作用域解析运算符 :: ,例如:

Jack::pail = 12.34;

未被装饰的名称(pail)称为未限定的名称,包含名称空间的名称(Jack::pail)称为限定的名称。

也可使用 using 声明和 using 编译指令,例如:

char fetch;
int main() {
	using Jill::fetch;  // 声明
	double fetch;  // 错误,因为已经存在fetch
	cout << fetch;  // 输出的是Jill空间中的fetch
	cout << ::fetch;  // 输出的是全局名称空间中定义的fetch,即char类型的fetch
}
char fetch;
int main() {
	using namespace Jill;  // 编译指令
	double fetch;  // 正确,将Jill中的fetch隐藏了
	cout << fetch;  // 输出刚定义的fetch
	cout << ::fetch;  // 输出全局fetch
	cout << Jill::fetch;  // 输出Jill中的fetch
}

名称空间可以嵌套使用:

namespace elements {
	namespace fire {
		int flame;
	}
	float water;
}
cout << elements::fire::flame;

using namespace emements::fire;
cout << flame;

名称空间可以使用 using :

namespace myth {
	using Jill::fetch;
	using namespace elements;
}
cout << myth::fetch;
cout << myth::water;

可以给名称空间创建别名:

namespace my_very_favorite_things {...};  // 已存在的名称空间
namespace mvft = my_very_favorite_things;  // 创建mvft别名

可以使用省略名称空间的名称来创建未命名的名称空间:

namespace {
	int ice;
	int bandycoot;
}

此时在该名称空间中声明的名称空间的潜在作用域为从声明点到该声明区域的末尾,与全局变量相似。由于没有名称,不能在未命名名称空间所属文件之外的其他文件中使用。

3. 指导原则

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量;
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量;
  • 应将开发的函数库或类库放在一个名称空间中;
  • 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计;
  • 不要再头文件中使用 using 编译指令;
  • 导入名称时,首选使用作用域解析运算符或 using 声明的方法;
  • 对于 using 声明,首选将其作用域设置为局部而不是全局。