【c++】【基础】【primer_plus】【第九章】内存模型与作用域

前言

本章主要介绍c++的内存模型,包括存储的持续性、作用域、链接性等方面。除此之外 ,还介绍了动态内存分配的方法,及以名称空间的概念。

可以从内存分配方面了解c++程序的在单文件之外的源码组织形式。

存储持续性

存储持续性表示变量在内存中存在的时间,即生命周期,大致分为以下几种。

自动存储持续性

在执行其所属代码块或函数时创建,执行完后其内存被释放。

常见的有在函数中声明的变量,包括函数参数等,以及在代码块中被声明的变量,这些变量通常称为局部变量。

静态存储持续性

在程序整个运行过程中都存在。

常见的为在函数和代码块外定义的变量,以及在函数内使用static定义的变量,这些变量通常称为全局变量。

线程存储持续性(c++11)

生存周期同所属的线程一样长,一般使用local_thread声明。

动态存储持续性

使用new分配内存的变量,会直到使用delete释放,或者程序运行结束,才会释放其内存。

作用域

描述名称在文件(翻译单元)的可见范围,大致分为以下几种。

局部作用域 -- 只在定义它的代码块内可用,如自动变量。

全局作用域 -- 在定义位置到文件结尾都可用,如静态变量。

函数原型作用域 -- 在函数原型作用域中使用的名称只在包含参数表的括号内可用,且可省略。

类作用域 -- 在定义它的类中可用。

名称空间作用域 -- 在定义它的名称空间中可用,全局作用域为其一特例。

链接性

描述名称在不同单元之间共享的方式。

外部链接性 -- 可在文件之间共享。

内部链接性 -- 只能由其所在文件中的函数共享。

无链接性 -- 不能共享,如自动变量。

自动变量

自动变量一般都是作为局部的变量,且没有链接性。

auto关键字原用于显式地指出变量为自动存储,c++11已经取消此用法。

局部变量可隐藏外部的或外层的变量,全局变量若不想被隐藏,可使用域解析符(::)。

可使用任何在声明时已知的表达式来初始化自动变量。

在语言底层,自动变量使用栈来简化实现。

寄存器变量

使用register关键字声明的变量,它建议编译器使用寄存器来存储自动变量,旨在提高速度,目前已经淘汰,register关键字目前只显式说明变量是自动的。

静态持续变量的链接性

外部链接性

静态变量直接在代码块外声明,即可获得外部链接性,有两种声明方式。

定义声明 -- 会给变量分配内存,直接声明。

引用声明 -- 不给变量分配内存,而是引用已有变量,多为其它文件中的已有变量。

其中引用声明,需要__使用extern关键字且不初始化__,若初始化了,则还是定义声明。

一般只在一个文件中使用定义声明,而在其他文件中使用引用声明。

内部链接性

静态变量直接在代码块外声明并在前面加上static关键字,即可获得内部链接性。

此时static关键字表示为变量的内部链接性,变量一开始就已经是静态的了。

名称相同时,内部链接性的静态变量将隐藏常规的外部变量。

无链接性

变量在代码块内声明,并加个static关键字,即可获得无链接性。

此时static关键字表示为变量的存储持续性为静态,变量一开始已经是无链接性的了。

这种在不同情况下关键字的含义不同的现象,又叫做关键字重载。

限定符和说明符

c-v限定符

const

const限定符表示不能修改的内存。

const对全局变量的链接性有影响,它会使全局变量的链接性变为内部的。

这也是const变量能写在头文件而没有多重定义的原因。

若想使用链接性为外部的const变量,则可以再在前面加extern进行定义,但其他文件使用此变量必须也要用extern关键字进行引用声明。

1
extern const int maxn = 100;

volatile

volatile用来声明易变的变量。

即使程序代码没有对内存单元进行修改,其值也可能发生变化(可能受硬件影响)。这是volatile导致的易变性。

如果不使用volatile变量,在两次访问之间编译器可能会认为值没有变化,从而进行优化(优化为常量)。使用volatile变量相当于告诉编译器,不要进行这种优化。这是volatile导致的不可优化性。

存储说明符

auto -- c++11后不再是说明符。

register -- c++11后表示为显式说明为自动变量。

static -- 内部链接性 or 静态。

extern -- 外部链接性变量的引用声明。

thread_local -- (c++11)线程的静态变量。

mutable -- 可用它来指出,即使结构体(或类)变量为const,其某个成员也是可以被修改的。

1
2
3
4
5
6
7
8
struct node {
int x;
mutable int y;
};

const node n { 1, 2};
n.x = 3 // 错误, 不可修改
n.y = 4; // 正确, y为mutable变量

函数的链接性

函数默认为外部静态的,即默认是extern的。

可使用static关键字将其链接性设置为内部的。但必须同时在原型和定义中使用static

内部静态函数将覆盖外部函数。

外部链接性的函数名矫正约定

c语言对函数名的矫正使用的约定与c++不同,可用函数原型指出其使用的约定。

1
2
3
extern "C" void fun(int);           // 使用c语言的约定
extern void fun(int); // 使用c++的约定
extern "C++" void fun(int); // 使用c++的约定 -- 显式指出

动态内存分配

new运算符

new运算符用于使用堆空间给变量动态分配内存。

初始化内置的标量类型可以加括号分配内存并初始化,也适用于有合适构造函数的类。

1
int* p = new int(5);                    // 初始化(*p)的值为5

初始化结构体或数组,使用大括号的初始化列表进行初始化(c++11)

1
2
node* p = new node{1.0, 2.0};           // node为结构体
int* p = new int[4]{1, 2, 3, 4}; // 初始化数组

new失败时,会返回空指针,并引发 std::bad-alloc异常(第15章)

new运算符调用如下分配函数。

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

delete也有相应的释放函数。

定位new运算符

new负责在堆上找一块满足要求的内存块,但无法指定要使用的位置。

定位new能指定要使用的位置。从而可以设置内存管理规程,处理需要通过特定地址进行访问的硬件,在特定位置创建对象等。

1
2
3
#include <new>
char buff[50];
int* p = new (buff) int[20]; // 从buff中分配一个包含20个int的数组

注意,不能使用delete来释放使用定位new分配的内存。定位new相当于将分配内存的重任交给了程序员。

定位new的分配函数如下。

1
void* operator new(std::size_t, void* position);

名称空间

概述

声明区域

声明区域指可以在其中声明的区域。

全局变量的声明区域为其声明所在的文件,局部变量为其代码块。

潜在作用域

从声明位置点开始,到其声明区域的结尾。

作用域

变量对程序而言可见的的范围。

潜在作用域可能会被嵌套的声明区域隐藏,如局部变量隐藏全局变量。

可命名的名称空间

名称空间可以是全局的, 也可以位于另一个名称空间中,但不能位于代码块中。

默认在名称空间中声明的名称的链接性为外部的(除了const)。

全局名称空间,对应于文件级声明区域,即之前的全局变量所在空间。

名称空间是开放的,即可在不同的地方声明同一个名称空间,整个名称空间为累计起来的结果。

访问名称空间中名称的方法为,使用作用域解析运算符::

1
2
3
4
5
namespace CE {
int x;
double y;
}
cout << CE::x << endl;

using声明 & using编译指令

using声明和using编译指令都可以用来简化名称空间的使用。

using声明

using声明使特定的标识符可用。

1
using CE::x;

其实质是将特定的名称添加到它所属的声明区域中。如果该声明区域中已经存在同名变量,则会报错。

using 编译指令

using编译指令使该名称空间的所有名称都可用。

1
2
3
4
using namespace CE;
// 可在全局使用,也可在代码块中使用
// 使用此编译指令时,局部名称会隐藏名称空间名称
// < iostream > 与 < iostream.h > 的区别是前者支持名称空间
1
2
3
4
using namespace CE;         // 如果使用using CE::x;则会报错
int x = 2;
cout << x << endl; // 局部变量x -- 将隐藏CE中的x
cout << CE::x << endl; // 名称空间中的x

其它特性

名称空间是可嵌套的。

using编译指令是可传递的。

可以给名称空间创建别名,常用于简化嵌套名称空间的使用。

1
namespace cloud_engine = CE;

可以使用未命名的名称空间,其作用域为整个文件,使用时不用加域解析运算符。相当于内部链接的全局静态变量(使用static关键字的全局变量)。

使用名称空间的指导规则

使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。

使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。

如果开发了一个函数库或类库,将其放在一个名称空间中。

仅将using编译指令作为一各路将旧代码转换为使用名称空间的权宜之计。

不要在头文件中使用using编译指令,它掩盖了要让哪些名称可用。

包含头文件的顺序可能影响程序的行为,using应放在所有的#include之后。

导入名称时,首选使用作用域解析运算符或using声明的方法。

对于using声明,首选将其作用域设置为局部而不是全局。