本文记录了C++中与内存模型和命名空间相关的容易遗忘的一些知识。
存储周期、作用域和链接性
在无命名空间时变量存储类别特性
| Storage Description | Duration | Scope | Linkage | How Declared |
|---|---|---|---|---|
| Automatic | Automatic | Block | None | In a block |
| Register | Automatic | Block | None | In a block with the keyword register |
| Static with no linkage | Static | Block | None | In a block with the keyword static |
| Static with external linkage | Static | File | External | Outside all functions |
| Static with internal linkage | Static | File | Internal | Outside all functions with the keyword static |
语言链接性
链接器需要为每个不同的函数分配一个不同的符号名称。在C语言中,这很容易实现,因为对于一个给定的名称,只能有一个C函数。因此,出于内部目的,C编译器可能会将诸如spiff之类的C函数名翻译为_spiff。C语言的处理方式称为C语言链接。然而,C++中可以有多个具有相同名称的函数,这些函数必须被转换为不同的符号名称。因此,C++编译器会进行名称修饰(或称为名称改编)过程,为重载函数生成不同的符号名称。例如,它可能会将spiff(int)转换为诸如_spiff_i之类的名称,将spiff(double, double)转换为_spiff_d_d。这种C++的处理方式称为C++语言链接。
当链接器查找与C++函数调用相匹配的函数时,它所使用的查找方法与查找匹配C函数调用的方法不同。但假设你想在C++程序中使用来自C库的预编译函数,该怎么办呢?例如,假设你有这样的代码:
spiff(22); // want spiff(int) from a C library
它在C库文件中的假设符号名称是_spiff,但对于我们假设的链接程序来说,C++的查找约定是查找符号名称_spiff_i。为了解决这个问题,可以使用函数原型来指明要使用的协议:
extern "C" void spiff(int); // use C protocol for name look-up
extern void spoff(int); // use C++ protocol for name look-up
extern "C++" void spaff(int); // use C++ protocol for name look-up
这里的第一个示例使用C语言链接。第二个和第三个示例使用C++语言链接;第二个示例默认采用C++语言链接,第三个示例则显式采用C++语言链接。
new:运算符、函数和替换函数
new和new[]运算符会调用两个函数:
void * operator new(std::size_t); // 被new使用
void * operator new[](std::size_t); // 被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));
有趣的是,C++将这些函数称为可替换的。这意味着如果你有足够的专业知识和需求,你可以提供new和delete的替换函数,并根据自己的特定要求进行定制。例如,一种选择是在类作用域中定义替换函数,以便它们能满足特定类的分配需求。你的代码会像往常一样使用new运算符,但此时new运算符会调用替换的new()函数。
定位new运算符
要使用定位new特性,首先需要包含 <new>,该头文件提供了这种版本new的原型。然后,使用带参数的new,该参数提供预期的地址。除了这个参数外,其语法与常规new相同。特别是,定位new既可以不带括号使用,也可以带括号使用。
就像常规的new调用带一个参数的new函数一样,标准的定位new会调用带两个参数的new函数:
char buffer[BUF]; // chunk of memory
int * pi = 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)
定位new函数不可替换,但可以重载。它至少需要两个参数,第一个参数始终是一个std::size_t参数,用于指定请求的字节数。任何此类重载函数都被称为定位new,即使额外的参数没有指定位置也是如此。
命名空间
命名空间可以位于全局级别,也可以位于其他命名空间内部,但不能放在块中。因此,在命名空间中声明的名称默认具有外部链接(除非它指向常量,常量自动具有内部链接)。
除了用户定义的命名空间外,还有一个命名空间,即全局命名空间。它对应于文件级别的声明区域,因此以前被称为全局变量的东西现在被描述为全局命名空间的一部分。
using 声明和 using 指令
#include <iostream>using namespace std; // using 指令using std::cout; // using 声明
using std::cin;
using std::endl;
[!NOTE]
假设一个命名空间和一个声明区域都定义了相同的名称。如果尝试使用using声明将该命名空间名称引入到声明区域中,这两个名称会产生冲突,从而导致错误。如果使用using指令将该命名空间名称引入到声明区域中,则该名称的局部版本会隐藏命名空间版本(using指令会将命名空间名称视为在函数外部声明的,但这并不会使这些名称对文件中的其他函数可用)。
未命名命名空间
namespace // unnamed namespace
{int ice;int bandycoot;
}
不能在包含未命名命名空间声明的文件以外的其他文件中使用该未命名命名空间中的名称。这提供了一种替代方案,可用于替代具有内部链接的静态变量。
命名空间惯用法
- 在命名空间中使用变量,而不是使用外部全局变量。
- 使用未命名命名空间中的变量,而不是使用静态全局变量。
- 如果你开发一个函数或类的库,请将它们放在命名空间中。
- 仅将using指令用作将旧代码转换为命名空间使用方式的临时手段。
- 不要在头文件中使用using指令;一方面,这样做会隐藏哪些名称被公开了。此外,头文件的顺序可能会影响程序的行为。如果要使用using指令,请将其放在所有预处理
#include指令之后。 - 通过使用作用域解析运算符
::或using声明来优先导入名称。 - 使用using声明时,优先使用局部作用域而非全局作用域。