一、存储持续性回顾

在第一篇中我们讨论了C++的内存方案,即存储类别如何影响信息在文件间的共享。

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

  • 自动存储持续性。在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。

  • 静态存储持续性。在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3中存储持续性为静态的变量。

  • 线程存储持续性(C++11)。当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量时使用关键字thread_local声明的,则其生命周期与所属的线程一样长。(本系列不讨论并行编程)。

  • 动态存储持续性。用new运算符分配的内存将一直存在,直到使用delele运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。

二、作用域和链接性

作用域描述了名称在文件(翻译单元)的多大范围内可见。

例如,函数中定义的变量可在函数中使用,但不能在其他函数中使用;而在文件中的函数定义之前定义的变量则可在所有函数中使用。

C++变量的作用域有多种。

  1. 作用域为局部的变量只在定义它们的代码块中可用。
  2. 作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。
  3. 自动变量的作用域为局部,静态变量的作用域为全局还是局部取决于它是如何被定义的。
  4. 在函数原型作用域中使用的名称只在包含参数列表的括号内可用(这就是为什么函数声明时,参数名称是什么以及是否出现都不重要的原因)。
  5. 在类中声明的成员的作用域为整个类。
  6. 在命名空间中声明的变量的作用域为整个名称空间(由于名称空间已经引入到C++语言中,因此全局作用域是名称空间作用域的特例)。

链接性描述了名称如何在不同单元间共享。

  • 链接性为外部的名称可在文件间共享;
  • 链接性为内部的名称只能由一个文件中的函数共享;
  • 自动变量的名称没有链接性,因为它们不能共享。

不同的C++存储方式是通过存储持续性作用域链接性来描述的。下面来看看各种C++存储方式的这些特征。首先介绍引入名称空间之前的情况,然后看一看名称空间带来的影响。

三、自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性

当程序开始执行这些变量所属的代码块时,将为其分配内存;当函数结束时,这些变量都将消失。

注意: 执行到代码块时,将为变量分配内存, 但其作用域的起点为其声明位置。

int main() 
{							// teledeli分配内存
	int teledeli = 5; 		// teledeli作用域开始
	{							// websight分配内存
		cout << "Hello\n";
		int websight = -2; 		// websight作用域开始
		cout << websight << ' ' << teledeli << endl;
	}							// websight作用域结束
	cout << teledeli << endl;
	...
} 							// teledeli作用域结束

自动变量和栈
我们知道自动变量就是存储在栈内存中的,随着函数调用栈的长度增增减减,就不多说了。

两个关键字 autoregister

  • auto在C++11中,用于自动类型推断,但在之前以及C语言中,它用于显式地指出变量为自动存储。
  • register最初是C语言引入的,建议编译器使用CPU寄存器来存储自动变量,旨在提高访问变量的速度。C++11中,register取代了之前auto的用法,只是显式地指出变量是自动的。

四、静态持续变量

和C语言一样,C++也为静态存储持续性变量提供了3中链接性:

  • 外部链接性(可在其他文件中访问)
  • 内部链接性(只能在当前文件中访问)
  • 无链接性(只能在当前函数或代码块中访问)。

这3中链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如栈)来管理它们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0.在默认情况下,静态数组和结构体将每个元素或成员的所有位都设置为0.

下面介绍如何创建这3种静态持续变量,然后介绍它们的特点。

  1. 要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;
  2. 要想创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;
  3. 要想创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。

下面的代码片段说明这3中变量:

...
int global = 1000;				// 静态存储持续性、外部链接性、作用域为全局(整个文件)
static int one_file = 50; 		// 静态存储持续性、内部链接性、作用域为全局(整个文件)
int main() 
{
...
}

void funct1(int n) 
{
	static int count = 0; 		// 静态存储持续性,无链接性、作用域为局部
		int llama = 0;		// 单纯的自动存储持续性、无链接性、作用域为局部
	...
}

void funct2(int q) 
{
	...
}

由于one_file的链接性为内部,因此只能在包含上述代码的文件中使用它;由于global的链接性为外部,因此可以在程序的其他文件中使用它。

所有的静态持续变量都有下述初始化特征:

  • 未被初始化的静态变量的所有位都被设置为0。这种变量被称为零初始化的。

下表总结了引入名称空间之前使用的存储特性,并指出了关键字static的两种用法,但含义有些不同:

  • 用于局部声明,以指出变量时无链接性的静态变量时,static表示的是存储持续性
  • 而用于代码块外的声明时,static表示内部链接性,而变量已经是静态持续性了。

有人称之为关键字重载,即关键字的含义取决于上下文。

存储描述 持续性 作用域 链接性 如何声明
自动 自动 局部(代码块) 在代码块中
寄存器 自动 局部(代码块) 在代码块中,使用关键字register
静态、无链接性 静态 局部(代码块) 在代码块中,使用关键字static
静态、外部链接性 静态 全局(文件) 外部 不在任何函数内
静态、内部链接性 静态 全局(文件) 内部 不在任何函数内,使用关键字static

下面详细介绍各种静态持续性

4.1 静态持续性、外部链接性

链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变量是在函数外部定义的,因此对所有函数而言都是外部的。例如,可以在main()前面或头文件中定义它们。可以在文件中位于外部变量定义后面的任何函数中使用它,因此外部变量也称全局变量(相对于局部的自动变量)。

1. 单定义规则

一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,C++有“单定义规则”(One Definition Rule, ODR),该规则指出,变量只能有一次定义。

为满足这种需求,C++提供了两种变量声明:

  • 一种是定义声明(defining declaration)或简称定义(definition),它给变量分配存储空间;
  • 另一种是引用声明(reference declaration)或简称声明(declaration),它不给变量分配空间,因为它引用已有的变量。

引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间:

double up;					// 定义声明,并进行零初始化
extern int blem;			// 引用声明, blem在其他地方定义
extern char gr = 'z'; 		// 定义声明,因为初始化了

如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它:

// file01.cpp
extern int cats = 20;	// 因为初始化了,所以是定义
int dogs = 22;			// 没有关键字extern  是定义
int fleas;				// 也是定义

...
// file02.cpp
// 使用文件file01.cpp中的cats、dogs
extern int cats; 		// 引用声明
extern int dogs;		// 引用声明

...
// file03.cpp
// 使用文件file01.cpp中的cats、dogs和fleas
extern int cats;
extern int dogs;
extern int fleas;

全局变量和局部变量该选谁?
首先,全局变量很有吸引力——因为所有的函数能访问全局变量,因此不用传递参数。但易于访问的代价很大——程序不可靠。

计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下,应使用局部变量,应该在需要知晓时才传递数据,而不应该不加区分地使用全局变量来使数据可用。

在之后面向对象编程的C++系列,我们将会看到OOP(面向对象编程)在数据隔离方面又向前迈了一步。

然而,全局变量也有它们的用处。例如,可以让多个函数可以使用同一个数据块(如月份名数组或原子量数组)。外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。

4.2 静态持续性、内部链接性

static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。在多文件程序中,外部链接性和内部链接性之间的差别很有意义。链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即可以在其他文件中使用,如前面的示例所示。

那如果要在其他文件中使用相同的名称来表示其他变量,该如何办呢?——答案是,是用static限定符,将该变量的链接性表示为内部。

// file1
int errors = 20;   	// 外部变量

// file2
static int errors = 5; // 只在file2内部可见
  • 可使用外部变量在多文件程序的不同部分之间共享数据;
  • 可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方法)。

另外,如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。

4.3 静态持续性、无链接性

至此,介绍了链接性分别为内部和外部、作用域为整个文件的变量。接下来介绍静态持续家族中的第三个成员——无链接性的局部变量。

static限定符用于在代码块中定义的变量,将使局部变量的存储持续性为静态的。

这意味着虽然改变量只在该代码块中可用,但它在改代码块不处于活动状态时仍然存在。因此在两次函数调用之间,静态局部变量的值将保持不变。

三、说明符和限定符

有些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。

1. cv-限定符

const

const已经很熟悉了,它表明,内存被初始化后,程序便不能再对它进行修改。

默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。内部链接性还意味着,每个文件都有自己的一组常量,而不是所以文件共享一组常量。每个定义都是其所属文件私有的,这就是能够将常量定义放在头文件中的原因。这样,只要在两个源代码文件中包括同一个头文件,则它们将获得同一组常量。

如果出于某种原因,程序员希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性。
extern const int states = 50; // 使得const全局变量外部链接性

volatile

关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。(按时编译器不要做相关方面的代码优化)。

例如,可以将一个指针指向某个硬件位置,其中包含了来自串口的时间或信息,在这种情况下,硬件(而不是程序)可能修改其中的内容;或者两个程序可能互相影响,共享数据。

该关键字的作用格式为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

2. 说明符

  • auto(在C++11中不再是说明符);
  • register
  • static
  • extern
  • thread_local(C++11新增的);
  • mutable

其中大部分已经介绍过了。在同一个声明中不能使用多个说明符,但thread_local除外,它可与staticextern结合使用。

mutable

可以用它来指出,及时结构体(或类)变量为const,其某个成员也可以被修改。
例如:

struct data
{
	char name[30];
	mutable int accesses;
	...
};

const data veep = {"nwpu", 0, ...};
strcpy{veep.name, "sysu");	// 不允许
veep.accesses++; 		// 允许

accessesmutable说明符使得accesses不受这种限制。

四、函数和链接性

和变量一样,函数也有链接性,虽然可选择的范围比较小。

和C语言一样,C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都是静态的,即在整个程序执行期间都一直存在

实际上,可以再函数原型中使用关键字extern来指出函数是在另一个文件中定义的。 还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字。

static int private(double x);
...
static int private(double x) {
	...
}

这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的函数。

内联函数不受单定义规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。

五、语言链接性

另一种形式的链接性——称为语言链接性也对函数有影响。

1. 背景知识

首先介绍一些背景知识。连接程序要求每个不同的函数都有不同的符号名。在C语言中,一个名称只对应一个函数,因此这很容易实现。为满足内部需要,C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性。但在C++中,同一个名称可能对应多个函数(函数重载),必须将这些函数翻译为不同的符号名称。因此,C++编译器执行名称校正或名称修饰,为重载函数生成不同的符号名称。例如,可能将spiff(int)转换为_spiff_i,而将spiff(double, double)转换为_spiff_d_d。这种方法被称为C++语言链接性

链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同。但如果要在C++程序中使用C库中预编译的函数,将出现什么情况呢?例如,假设有下面的代码:
spiff(22); // want spiff(int) from a C library
它在C库文件的符号名称为_spiff,但对于我们假设的链接程序来说,C++查询约定是查找符号名称_spiff_i。为解决这种问题,可以用函数原型来指出要使用的约定:

extern "C" void spiff(int);  	// 使用C语言方法进行名称查找
extern void spoff(int);			// 使用C++方法进行名称查找
extern "C++" void spaff(int);	// 使用C++方法进行名称查找

六、存储方案和动态分配

动态内存由new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。

通常,编译器使用三块独立内存:一块用于静态变量(可能再细分);一块用于自动变量(栈);另一块用于动态存储(堆)。

虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。例如,假设在一个函数中包含下面的语句:

float * p_fees = new float[20];

我们可以通过作用域和链接性规则来控制p_fees,从而选择何时释放new申请的80字节动态内存。

1. 定位new运算符

通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位new运算符,它让我们能够制定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

要使用定位new特性,首先要包含头文件new,他提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除要指定参数外,句法与常规new运算符相同。具体地说,使用定位new运算符时,变量后面可以有方括号,也可以没有。

下面的代码段演示了new运算符的4中用法:

#include <new>
struct chaff
{
	char dross[20];
	int slag;
};

char buffer1[50];
char buffer2[50];

int main()
{
	chaff *p1, *p2;
	int *p3, *p4;
	// 首先,用常规的new, 两个都在堆内存
	p1 = new chaff; 
	p3 = new int[20];
	// 现在,用定位new运算符
	p2 = new (buffer1) chaff; 	// 把结构体放在buffer1
	p4 = new (buffer2) int[20]; // 把int数组放在buffer2
}

七、命名空间

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

using声明使特定的标识符可用。
using std::cout;

using编译指令使整个命名空间可用。
using namespace std;

一般来说,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有的名称,包括可能不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加量哪些名称。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐