# C++基础9-内存模型和名称空间

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢!

# 内存模型

存储类别如何影响信息在文件间的共享,C++使用不同的方案来存储数据,他们的区别在于数据保留在内存中的时间。

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

# 作用域和链接

作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。作用域为局部的变量只在定义它的代码块(由花括号括起来的)中可用,比如函数内定义的变量只能在该函数内使用。作用域为全局的变量从定义位置到文件结尾之间都可用。还有一些其他变量比如函数原型中使用的名称只在形参列表之间的括号内可用。在类中声明的成员的作用域为整个类,在名称空间中声明的变量的作用域为整个名称空间。C++函数的作用域可以是整个类或整个名称空间,但不能是局部的。
链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的变量只能由一个文件中的函数共享。自动变量没有链接性,因为它们不能共享。

# 自动存储变量

自动变量定义时没有指定初始值则其值是不确定的。函数中的形参是用栈实现的。register在c++11中用途是想指出变量是自动的,在c++17中被弃用。

# 静态持续变量

静态变量有3种链接性:外部链接,内部链接和无链接。这3中链接在整个程序执行期间都存在,与自动变量相比,寿命更长,它们不使用栈来管理内存,编译器将分配固定的内存块来存储所有的静态变量。如果没有显式地初始化静态变量,编译器将设置为0,默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。

存储描述 持续性 作用域 链接性 声明方式
自动 自动 代码块 在代码块中
寄存器 自动 代码块 在代码块中,使用关键字register
静态,无链接性 静态 代码块 在代码块中,使用关键字static
静态,外部链接性 静态 文件 外部 在代码块外面声明
静态,内部链接性 静态 文件 内部 在代码块外面声明,使用关键字static

# 静态持续性,外部链接性

C++有单定义规则(One Definition Rule,ODR),即变量只能有一次定义。对于要使用外部变量的文件,都必须先声明它。C++提供了两种变量声明:

  1. 定义声明(defining declaration)或简称定义(definition),给变量分配存储空间;
  2. 引用声明(referencing declaration)或简称声明(declaration),不给变量分配存储空间,因为引用已有的变量。使用关键字extern。
file1.cpp

extern double abcExtern1 = 50;
double abcExtern2 = 100;
double abcExtern3 = 150;

file2.cpp

extern double abcExtern1;
static double abcExtern2=110;
extern double abcExtern3;

int main(int argc, char *argv[])
{
double abcExtern3=10; //内部变量覆盖外部变量
    cout << abcExtern1 << endl;
    cout << abcExtern2 << endl;
    cout << abcExtern3 << endl << ::abcExtern3 <<endl;
}

最后输出为:
50
110
10
150

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 静态持续性,内部链接性

上面的abcExtern2便是内部链接的静态变量。如果该文件存在与一个静态外部变量相同名字的静态内部变量,内部变量将隐藏外部变量。

# 静态持续性,无链接性

用static限定符在代码块中定义,但是它在代码块不存在时依然可用:

void sum() {
    static int i = 0;
    i++;
    cout << i << endl;
}

int main(int argc, char *argv[])
{
    for (int i = 0; i < 3; i++) {
        sum();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 说明符和限定符

# 存储说明符(storage class specifier)关键词:

auto register static extern thread_local mutable

其中auto在c++11中不再是,register在c++17中弃用,thread_local是c++11新增的。

# cv限定符:

const volatile

const说明变量为常量,volatile则是改善编译器的优化能力,如果编译器发现程序在几条语句中两次使用了某个变量的值,则编译器不会让程序查找这个值两次,而是将值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。

mutable则是指出,即使结构变量或类为const,其某个成员也可以被修改。

struct personInfo {
    mutable int age=18;
    int expr = 2;
};

int main(int argc, char *argv[])
{
    personInfo const p1;
    cout << p1.age << endl;
    p1.age = 20;
    cout << p1.age << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12

# const变量

在C++中const限定符对默认存储类型也有影响,在默认情况下全局变量的链接性是外部的,但const全局变量的链接性为内部的。在C++看来,全局const定义就像static说明符一样:

file1.cpp

//const double abc1 = 10;//错误,无法解析的外部符号
extern const double abc1 = 10;

file2.cpp

extern const double abc1;

int main(int argc, char *argv[])
{
    cout << abc1 << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

内部链接性意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量,每个定义都是所属文件私有的,这就是能够将常量定义放在头文件的原因。只要两个cpp文件中包含同一个头文件,将获得同一组常量。

如果想要const修饰的常量是外部的,则需要在文件中使用extern关键字来声明,而常规外部变量要不要extern都可以的。

# 函数的链接性

C++不允许在一个函数中定义另一个函数,因此所有函数的存储持续性都自动为静态的,即整个程序执行期间都一直存在。默认情况下,函数的链接性为外部的,即可以在文件间共享。

# 语言的链接性

语言链接性的不同会对函数有影响,C语言中一个名称对应一个函数(c language linking),C++中一个函数对应多个函数,会执行名称矫正或名称修饰(c++ language linking),因此C语言和C++对函数的内部链接是不一样的。假如在C++中要使用C库预编译的函数,可以使用指定语法

extern "C" 函数原型
extern "C++" 函数原型

# 动态内存

使用C++运算符new或(C函数malloc())分配的内存,这种内存被称为动态内存。动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因为可以在一个函数中分配,在另一个函数中释放。通常编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,还有一块用于动态存储。

# 使用new运算符初始化

为内置的标量类型分配存储空间并初始化时:

    int *intArr = new int[6];
    int *intOne = new int(6);
    int *intOne1 = new int{ 10 };
1
2
3

new失败时会引发异常std::bad_alloc

# 分配函数,释放函数和替换函数

void * operator new(std::size_t);
void * operator new[](std::size_t);

void operator delete(void *);
void operator delete[](void *);
1
2
3
4
5

注意这些函数时可替换的,也就是可以使用原来的语法来调用您自己定义的new和delete函数。

# 定位new运算符

通常new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种用法就是定位(placement)new运算符,它让您能够指定要使用的位置。

    int * p1 = new int[30];
    double * p2 = new (p1) double[5];
    double * p3 = new (p1) double[5];
    double * p4 = new (p1+2) double[5];

    cout << p1 << ":" << p2 << ":" << p3 << ":" << p4 << endl;
    p2[3] = 4.0;
    p3[3] = 5.0;
    p4[2] = 6.0;
    cout << p2[3] << endl;
1
2
3
4
5
6
7
8
9
10

# 名称空间

随着项目的增大,名称互相冲突的可能性将增加,为了解决名称冲突,C++提供了名称空间工具。

# 传统C++名称空间

声明区域(declaration region):可以在其中进行声明的区域。比如全局变量声明区域是声明所在的文件,函数中的变量声明区域是所在代码块。

潜在作用域(potential scope):变量的潜在作用域从声明点开始,到其声明区域的结尾。它比声明区域小,这是由于变量必须定义后才能使用。

变量并非在其潜在作用域内的任何位置都是可见的,可能会被嵌套的同名变量隐藏,变量对程序而言可见的范围被称为作用域(scope)。

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

# 新的名称空间特性

C++新增了一种功能,通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。

  1. 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。在默认情况下,名称空间中声明的名称的链接性为外部的,除非引用了常量。
  2. 除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。
  3. 名称空间是开放的(open),即可以把名称加入到已有的名称空间中。

# using 编译指令和声明的比较

使用using声明时就好像声明了相应的名称一样,如果某个名称已经在函数中声明了,就不能用using声明导入相同的名称。如果局部有相同名字的变量,则会出错。
使用using编译指令时,进行名称解析,就像是在此处引入了命名空间中的变量,但是如果局部有相同名字的变量,将隐藏命名空间中的变量。
使用using声明要比using编译指令更安全,由于只导入指定的名称,如果该名称与局部名称冲突,编译器将发出指示。但是如果使用using编译指令的话,局部名称将覆盖名称空间版本,而编译器不会发出警告。名称空间开放性意味着名称空间的名称可能分散在多个地方,使得难以知道都添加了哪些名称。

实例:

namespace test1 {
    int a1 = 0;
}

namespace test2 {
    int a2 = 1;
}

namespace test1 {
    int a2 = 2;
}

int a2 = 3;

int main(int argc, char *argv[])
{
    using test1::a2;
    using namespace test2;
    cout << a2 << endl;
    //unqualified name 未限定名称
    cout << test2::a2 << endl;
    //qualified name 限定的名称
    cout << ::a2 << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 名称空间的其他特性

名称空间可以嵌套,可以创建别名,可以传递变量。省略未命名的名称空间可以将其内部的变量都变为内部静态变量。实例如下:

namespace test1 {
    int a1 = 1;
    namespace test2 {
        int a2 = 2;
    }
}

namespace test3 {
    using namespace test1;
    int a3 = 3;
}

namespace {
    int a4 = 4;
    int a5 = 5;
}

int main(int argc, char *argv[])
{
    namespace t2 = test1::test2;
    using namespace t2;
    cout << a2 << endl;
    using namespace test3;
    cout << a1 << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 名称空间使用经验

  1. 文件模块化的程序,可以在头文件和实现体中引入命名空间。
  2. 使用已命名的名称空间中的声明变量,不要使用外部全局变量和静态全局变量。
  3. 原来C的头文件没有名称空间,C++的头文件做了相应转换,一般头文件都没有后缀.h。比如C中的math.h对应C++中的cmath。但是并非所有的编译器都完成了这种过度。
  4. 仅把使用using编译指令当成将旧代码转换为使用名称空间的权宜之计。
  5. 不要在头文件使用using编译指令,这样做掩盖了要让哪些名称可用,另外包含头文件的顺序可能影响程序的行为。如果非要使用,应将其放在所有#include之后。
  6. 导入名称时,首选使用作用域解析运算符或using声明方法。
  7. 对于using声明,首选将其作用域设置为局部而不是全局。