这篇笔记这么慢主要是因为中间隔了一堆寒假作业。下一篇得开学之后写了,不知道会咕到什么时候233。
本篇内容包括:
- 单独编译
- 存储持续性、作用域和链接性
- 定位(
placement
)new
运算符- 名称空间
第9章 内存模型和名称空间
9.1 单独编译
一种非常有用的组织程序的策略:
- 头文件:包含结构声明和使用这些结构的函数的原型。
- 源代码文件:引用头文件,包含与结构有关的函数的代码。
- 源代码文件:引用头文件,包含调用与结构相关的函数的代码。
下面列出了头文件中常包含的内容
- 函数原型。
- 使用
#define
或const
定义的符号常量。 - 结构声明。
- 类声明。
- 模板声明。
- 内联函数。
不要使用
#include
来包含源代码文件,这样做将导致多重声明防止头文件被多次包含:
1
2
3
4
/* details omitted */虽然我们讨论的是根据文件进行单独编译,但为保持通用性,C++ 标准使用了术语翻译单元(
translation unit
),而不是文件;文件并不是计算机组织信息时的唯一方式
9.2 存储持续性、作用域和链接性
C++ 使用四种不同的方案来存储数据:
- 自动存储持续性
- 静态存储持续性
- 线程存储持续性(C++11 引入)
- 动态存储持续性(有时被称为自由存储(
free store
)或堆(heap
))
9.2.1 作用域和链接
- 链接性(
linkage
)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享 - 注意,一个编译单元包括源文件内代码和它包含的库文件
9.2.2 自动存储持续性
- 自动变量储存在栈中
- 在 C++11 中,关键字
register
的作用只是显式地指出变量是自动的
9.2.3 静态持续变量
- 由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量。所有静态持续变量在整个程序执行期间都存在
- 所有静态持续变量都是零初始化的(
zero-initialized
) - C++ 也为静态存储持续性变量提供了3种链接性:
- 外部链接性(可在其他文件中访问)
- 内部链接性(只能在当前文件中访问)
- 无链接性(只能在当前函数或代码块中访问)
- 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用
static
限定符;要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static
限定符1
2
3
4
5
6int global = 1000; // static duration, external linkage
static int one_file = 50; // static duration, internal linkage
void foo() {
static int ret = 0; // static duration, no linkage
/* details omitted */
} - 由上可知:用于局部声明,以指出变量是无链接性的静态变量时,
static
表示的是存储持续性;而用于代码块外的声明时,static
表示内部链接性,而变量已经是静态持续性了。有人称之为关键字重载 - 零初始化意味着将变量设置为零。对于标量类型,零将被强制转换为合适的类型。例如,在 C++ 代码中,空指针用
0
表示,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,且填充位都被设置为零 - 零初始化和常量表达式初始化被统称为
静态初始化
,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化
意味着变量将在编译后初始化 - C++11 新增了关键字
constexpr
,这增加了创建常量表达式的方式。但本书不会更详细地介绍 C++11 新增的这项新功能(???
9.2.4 静态持续性、外部链接性
- “单定义规则”(
One Definition Rule(ODR)
)- 引用声明(
referencing declaration
)- 引用声明使用关键字
extern
,且不进行初始化;否则,声明为定义,导致分配存储空间
- 引用声明使用关键字
- 引用声明(
- 作用域解析运算符(
::
)- 放在变量名前面时,该运算符表示使用变量的全局版本
- 从清晰和避免错误的角度说,相对于依赖于作用域规则,使用
::
是更好的选择,也更安全
9.2.5 静态持续性、内部链接性
- 静态外部变量优先于常规外部变量:
1
2
3
4
5
6
7
8
9// file1
int foo = 20; // external declaration
...
-----------------------------------------
// file2
static int foo = 5; // known to file2 only
void bar() {
std::cout << foo; // uses foo defined in file 2
}
9.2.6 静态存储持续性、无链接性
- 无链接性的局部变量
- 被
static
限定符修饰的且在代码块中定义的变量 - 储存持续性为静态
- 被
9.2.7 说明符和限定符
- 储存说明符:
auto
(在 C++11 中不再是说明符)- C++11 之前:指出变量为自动变量
- C++11 及之后:自动类型推断
register
- C++11 之前:指示寄存器存储
- C++11 及之后:显式地指出变量是自动的
static
- 作用域为整个文件的声明:表示内部链接性
- 局部声明:表示局部变量的存储持续性为静态的
extern
- 引用声明
thread_local
(C++11 新增的)- 变量的持续性与其所属线程的持续性相同
thread_local
变量之于线程,犹如常规静态变量之于整个程序
mutable
- 即使结构(或类)变量为
const
,其某个成员也可以被修改1
2
3
4
5
6
7struct data {
char name[30];
mutable int accesses;
};
const data veep = {"Claybourne Clodde", 0};
std::cout << veep.name;
veep.accesses++;
- 即使结构(或类)变量为
- cv-限定符
volatile
- 即使程序代码没有对内存单元进行修改,其值也可能发生变化
- e.g. 可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息。在这种情况下,硬件(而不是程序)可能修改其中的内容
- 这个声明是为了防止编译器进行一些程序员不希望出现的优化
- 即使程序代码没有对内存单元进行修改,其值也可能发生变化
const
- 在默认情况下全局变量的链接性为外部的,但
const
全局变量的链接性为内部的。也就是说,全局const
定义就像使用了static
说明符一样- 因此,
const
全局变量可以存在于头文件之中(多多考虑头文件被多个文件包含情况)
- 因此,
- 如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用
extern
关键字来覆盖默认的内部链接性。此时,鉴于单个const
在多个文件之间共享,因此只有一个文件可对其进行初始化1
extern const int states = 50;
- 在默认情况下全局变量的链接性为外部的,但
9.2.8 函数和链接性
- 和变量一样,函数也有链接性
- C++ 不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的
- 在默认情况下,函数的链接性为外部的,即可以在文件间共享
- 可以在函数原型中使用关键字
extern
来指出函数是在另一个文件中定义的 - 可以使用关键字
static
将函数的链接性设置为内部的,使之只能在一个文件中使用。(必须同时在原型和函数定义中使用该关键字:) - 单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。
- 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++ 要求同一个函数的所有内联定义都必须相同。
9.2.9 语言链接性
- 同一个名称可能对应多个函数,因此,编译器执行名称矫正或名称修饰(参见第8章),为重载函数生成不同的符号名称。这被称为 C++语言链接(
C++ language linkage
)。 - 如果要在 C++ 程序中使用 C 库中预编译的函数,可以用函数原型来指出要使用的约定:
1
2
3extern "C" void foo(int); // use C protocol for name look-up
extern void foo(int); // use C++ protocol for name look-up
extern "C++" void foo(int); // use C++ protocol for name look-up
9.2.10 存储方案和动态分配
- 编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。
- 初始化动态分配的变量:
- 内置的标量类型或者有合适构造函数的类,可用括号扩起
1
double *pi = new double(3.14);
- 常规结构和数组,可用列表初始化(C++11)第一种也可使用列表初始化(C++11)
1
2struct foo {int a, b;};
foo *bar = new foo{1, 2};1
double *pi = new double{3.14};
- 内置的标量类型或者有合适构造函数的类,可用括号扩起
new
失败时:在最初的10年中,C++ 在这种情况下让new
返回空指针,但现在将引发异常std::bad_alloc
。第15章通过一些简单的示例演示了这两种方法的工作原理new
:运算符、函数和替换函数new
和delete
会分别调用如下函数(它们位于全局名称空间中)- 分配函数(
alloction function
)1
2void * operator new(std::size_t); // used by new
void * operator new[](std::size_t); // used by new[] - 释放函数(
deallocation function
)1
2void operator delete(void *);
void operator delete[](void *); - 陈国师上课指出:
delete
会调用析构函数,但free
不会
- 分配函数(
- C++ 将这些函数称为可替换的(
replaceable
),即可根据需要对其进行定制
- 定位
new
运算符new
负责在堆(heap
)中找到一个足以能够满足要求的内存块。new
运算符还有另一种变体,被称为定位(placement
)new
运算符,它让您能够指定要使用的位置。定位new
运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。- e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct foo {
char alex[20];
int bob;
};
char buffer[50];
int main() {
foo *p1, *p2;
p1 = new (buffer) foo;
p2 = new (buffer + sizeof(foo)) foo;
/* details omitted */
return 0;
} buffer
指定的内存是静态内存,而delete
只能用于这样的指针:指向常规new
运算符分配的堆内存。也就是说,下面的语句将引发运行阶段错误1
delete p1;
- 定位
new
运算符的工作原理:基本上,它只是返回传递给它的地址,并将其强制转换为void *
,以便能够赋给任何指针类型。将定位new
运算符用于类对象时,情况将更复杂,这将在第12章介绍。1
2int *p3 = new (buffer) int [40];
// invokes new(40 * sizeof(int), buffer) - C++允许程序员重载定位
new
函数。
9.3 名称空间
9.3.1 传统的 C++ 名称空间
- 声明区域(
declaration region
):可以在其中进行声明的区域 - 潜在作用域(
potential scope
):从声明点开始,到其声明区域的结尾 - 作用域(
scope
):可以直接使用变量的区域(没被别的同名变量隐藏的潜在作用域)
9.3.2 新的名称空间特性
- 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)
- 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间
(global namespace
)。它对应于文件级声明区域,因此前面所说的全局变量现在
被描述为位于全局名称空间中 - 名称空间是开放的(
open
),即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称goose
添加到Jill
中已有的名称列表中:1
2
3namespace Jill {
char *goose(const char *);
} - 未被装饰的名称称为未限定的名称(
unqualified name
),包含名称空间的名称称为限定的名称(qualified name
)。 using
声明和using
编译指令using
声明将特定的名称添加到它所属的声明区域中。例如main()
中的using
声明Jill::goose
将goose
添加到main()
定义的声明区域中由于1
2
3
4
5int main() {
using Jill::goose;
/* details omitted */
return 0;
}using
声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为goose
。using
编译指令使所有的名称都可用。如:1
using namespace std;
- 不同之处:
- 如果某个名称已经在函数中声明了,则不能用
using
声明导入相同的名称。 - 如果使用
using
编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
- 如果某个名称已经在函数中声明了,则不能用
- 名称空间的其他特性
- 如果函数被重载,则一个
using
声明将导入所有的版本。 - 可以将名称空间声明进行嵌套
- 可以在名称空间中使用
using
编译指令和using
声明 - 可以给名称空间创建别名
1
2namespace foo { /* details omitted */ }
namespace bar = foo; - 可以通过省略名称空间的名称来创建未命名的名称空间。这提供了链接性为内部的静态变量的替代品。例如下面两段代码等价
1
2static int counts; // static storage, internal linkage
int main() { /* details omitted */ }1
2
3
4namespace {
int counts; // static storage, internal linkage
}
int main() { /* details omitted */ }
- 如果函数被重载,则一个
9.3.4 名称空间及其前途
随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++ 当前提倡将标准函数放在名称空间
std
中,这种做法扩展到了来自 C 语言中的函数。- 仅将编译指令
using
作为一种将旧代码转换为使用名称空间的权宜之计。- 不要在头文件中使用
using
编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含文件的顺序可能影响程序的行为。如果非要使用编译指令using
,应将其放在所有预处理器编译指令#include
之后。- 导入名称时,首选使用作用域解析运算符或
using
声明的方法。- 对于
using
声明,首选将其作用域设置为局部而不是全局。