- C++变量声明上下文中的存储类是一种类型说明符,该说明符控制对象的生存期、链接和内存位置。
- 给定对象只能有一个存储类
- 在块内定义的变量具有自动存储,除非使用
extern、static 、thread_local
说明符进行了指定 - 自动对象和变量不具有链接;它们对于块外部的代码是不可见的
mutable
关键字可以视为存储类说明符。但是它们值存在类定义的成员课表中
作用
存储类说明符与名字的作用域一同,控制名字的两个独立性质:其“存储期”与其“连接”。
连接
指代对象、引用、函数、类型、模板、命名空间或值的名字,可具有连接。
- 若某个名字具有连接,则其所指代的实体与另一作用域中的声明所引入的相同名字指代相同的实体。
- 若变量、函数或其他实体声明于数个作用域但没有足够的连接,则将生成该实体的多个实例。
以下各种连接可以被识别:
无连接
名字只能从其所在的作用域使用。声明于块作用域的下列名字均无连接
- 未显式声明为 extern 的变量(不管有没有 static 修饰符);
- 局部类及其成员函数;
- 声明于块作用域的其他名字,例如 typedef、枚举及枚举项。
未指定为拥有外部、模块 (C++20 起)或内部连接的名字亦无连接,这与其声明所处的作用域无关。
内部连接
名字可从当前翻译单元中的所有作用域中使用(内部链接意味着,名称在用于声明变量的文件的外部是不可见的)。声明于命名空间作用域的下列任何名字均具有内部连接:
- 声明为 static 的变量、变量模板 (C++14 起)、函数或函数模板;
- 未声明为 extern 且先前未声明为具有外部连接的非 volatile 非模板 (C++14 起)非 inline (C++17 起) 且未被导出 (C++20 起)的 const 限定的变量(包含 constexpr);
- 匿名联合体的数据成员。
另外,(C++11 起)所有声明于无名命名空间或无名命名空间内的命名空间中的名字,即使显式声明为 extern,均拥有内部连接。
外部连接
名字能从其他翻译单元中的作用域使用(外部链接意味着,变量的名称在用于声明变量的文件的外部是可见的。)。具有外部连接的变量和函数也具有语言连接,这使得以不同编程语言编写的翻译单元可以互相连接。 除了后述注解,声明于命名空间作用域的下列任何名字均具有外部连接:
- 以上未列出的变量与函数(即未声明为 static 的函数、命名空间作用域内未声明为 static 的非 const 变量,和所有声明为extern 的变量);
- 枚举;
- 类以及其成员函数、静态数据成员(不论是否 const)、嵌套类及枚举,及首次以类体内的 friend
- 声明引入的函数的名字; 所有未列于上的模板名(即未声明为 static 的函数模板)。
任何首次声明于块作用域的下列名称拥有外部连接:
- 声明为 extern 的变量名;
- 函数名。
然而,若名字声明于无名命名空间或内嵌于无名命名空间的命名空间,则该名字拥有内部连接。若名字声明于具名模块且未被导出,则该名字拥有模块连接。 (C++20 起)
模块连接
- C++20起引入
- 名字只能从同一模块单元或同一具名模块中的其他翻译单元的作用域指代。
- 声明于命名空间作用域的名字,若它们声明于具名模块,未被导出且无内部连接,则拥有模块链接。
存储期
C++中有种不同的方案来存储数据,这些方案的区别在于数据保留在内存中的时间(也叫做存储期)。程序中的所有对象都具有下列存储期之一:
- 自动存储(automatic)连续性:
- 未声明为 static、extern 或 thread_local 的所有局部对象(包括函数参数)均拥有此存储期
- 它们在程序开始执行时其所属的函数或者代码块时被创建,执行完函数或者代码块时,它们使用的内存被释放
- 作用域为局部,没有链接性。没有链接性的意思是指两个函数之间的变量名可以相同
- 作用域的起点是声明位置
- C++是如何处理自动变量的:由于自动变量的数目随函数的开始和结束而增加,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,将其视为栈,以管理变量的增减。程序使用两个指针来跟踪栈,一个指针指向栈底—栈的开始位置,另一个指针指向堆顶—下一个可用的内存单元。 当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数调用前的值,从而释放新变量使用的内存
- 静态存储连续性:
- 在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。
- 所有声明于命名空间(包含全局命名空间)作用域的对象,加上声明带有 static 或 extern 的对象均拥有此存储期。
- 它们在程序整个运行过程中都存在。这类对象的存储在程序开始时分配,并在程序结束时解分配。
- 这类对象只存在一个实例。
- 有关拥有此存储期的对象的初始化的细节,见非局部变量与静态局部变量。
- 线程存储持续性:
- 动态存储持续性:
- 使用new分配的内存将一直存在,直到使用delete将其释放或者程序结束。这种内存的存储持续性为动态,有时被称为自由存储或者堆
- 也就是说,这类对象的存储是通过使用动态内存分配函数来按请求进行分配和解分配的。
- 关于具有此存储期的对象的初始化的细节,见 new 表达式。
静态持续变量
C++为静态存储持续性提供了三种链接性:
- 外部链接性(可在其他文件中访问)
- 内部链接性(只能在当前文件中访问)
- 无链接性(只能在当前函数或者代码块中访问)
这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。
int global; // 静态存储变量,外部链接
static int one_file; // 静态存储变量,内部链接
int func1(int n){
static int count = 0; // 静态持续变量,无链接
}
- global:整个程序运行期间一直存在,因为是外部链接,所以其他文件也可以访问这个变量
- one_file:整个程序运行期间一直存在,因为是内部链接,所以其他文件不可以访问这个变量,只能在本文件内使用
- count :整个程序运行期间一直存在,因为是无链接,所以它的作用域是局部,只能在func1函数内使用。 这个叫做静态局部变量
由于静态变量的数组在程序运行期间是不变的,因此程序不需要使用特殊的装置(比如栈)来管理它们。编译器将分配固定的内存块在存储所有的静态变量,这些变量在整个程序执行期间一直存在。
静态变量的初始化
- 零初始化的:
- 如果没有显示的初始化静态变量,编译器将把它们设置为0。
- 在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。
- 除了零初始化外,还可以对静态变量进行常量表达式初始化和动态初始化
零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译)单元式初始化变量。动态初始化意味着变量将在编译后初始化。
那么初始化形式由什么因素决定呢?
- 首先,所有静态变量都被零初始化,而不管程序员是否显式的初始化了它。
- 接下来,如果使用了常量表达式初始化了变量,而且编译器仅根据文件内容(包含被包括的头文件)就可计算表达式,编译器将执行常量表达式初始化
- 必要时,编译器将执行简单计算
- 如果没有足够的信息,变量将动态初始化。
int x; // 零初始化
int y = 5; // 静态初始化
int z = 13 * 13; //静态初始化
int enougt = sizeof(long) * 2 + 1 ; //静态初始化
const double pi = 4.0 * atan(10.1); // 动态初始化
最后一个因为调用atan,这需要等到该函数被链接而且程序执行时。
静态持续性,外部链接性
默认情况下,在全局命名空间中定义的对象或者变量具有静态持续时间和外部链接
- 静态持续时间意味着,在程序启动时分配对象或者变量,并在程序结束时释放对象或者变量
- 外部链接意味着,变量的名称在用于声明变量的文件的外部是可见的
全局变量具有静态持续性,外部链接性
- 链接性为外部的变量通常称为外部变量,它们的存储期为静态,作用域是整个文件。
- 外部变量是在函数外部定义的,因此对所有函数而言是外部的。比如,可以在mian前面或者头文件种定义它们。
- 可以在文件种位于外部变量定义后面的任何函数种使用它们,因此外部变量也叫做全局变量
单定义规则
一方面,在每个使用外部变量的文件中,都必须声明它。
另一方面,C++中有单定义规则ORD,即变量只能由一次定义。为此,C++提供了两种变量声明:
- 定义声明(简称定义):给变量分配存储空间
- 引用声明(简称声明):不给变量分配存储空间,因为它引用已有的变量。
引用声明使用extern,而且不进行初始化,否则,就是定义,导致分配存储空间:
double up; // 定义,零初始化
extern int blem; // 声明
extern char gz = 'z'; // 定义
如果要在多个文件中使用外部变量,只需要在一个文件中包含改变了的定义(但定义规则),但在使用该变量的其他所有文件中,都必须使用extern声明它
静态持续性,内部链接性
将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。内部链接变量只能在其所属文件中使用
这种做法将识别,因为errorrs不能有两个定义(ORD规则)
可以的,因为static指出了errors为内部链接。静态变量将隐藏外部变量
静态存储持续性、无链接性(静态局部变量)
无链接性的局部变量:
- 创建方法:static 局部变量
- 这种变量只能在该代码中使用,但它在代码块不活动时仍然存储。因此在两次函数调用期间,静态局部变量的值将保持不变
- 如果初始化了静态局部变量,则持续只在启动时进行一次初始化,以后再调用函数时,将不会被再次初始化
静态局部变量
- 声明于块作用域且带有 static 或 thread_local (C++11 起) 说明符的变量拥有静态或线程 (C++11 起)存储期
- 静态局部变量在控制首次经过其声明时才会被初始化(除非其初始化是零初始化或常量初始化,这可以在首次进入块前进行)。在其后所有的调用中,声明均被跳过。
- 若初始化抛出异常,则不认为变量被初始化,且控制下次经过该声明时将再次尝试初始化。
- 若初始化递归地进入正在初始化的变量的块,则行为未定义。
- C++11起,若多个线程试图同时初始化同一静态局部变量,则初始化严格发生一次(类似的行为也可对任意函数以 std::call_once 来达成)。
- 注意:此功能特性的通常实现均使用双检查锁定模式的变体,这使得对已初始化的局部静态变量检查的运行时开销减少为单次非原子的布尔比较。
- 块作用域静态变量的析构函数在初始化已成功的情况下在程序退出时被调用。
- 相同内联函数(可以是隐式内联)的所有定义中,函数局域的静态对象均指代定义于一个翻译单元中的同一对象。
存储方案和动态分配
- 使用
new
/malloc
分配的内存叫做动态内存 - 动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。
- 与自动内存不同,动态内存不是FIFO,其分配和释放顺序要却决于new和delete要以何种方式被使用。
- 通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另一块用于动态变量
存储类说明符
存储类说明符如下关键字:
- register:自动存储期
- 此关键词的存在可用于提示优化器将此变量的值存储于 CPU 寄存器
- 此关键词已于 C++17 被弃用。
- auto:(C++11 前)自动存储期。
- 此关键词的含义已于 C++11 更改。C++11 起,auto 不再是存储类说明符;它被用于指示类型推导。
- mutable : 不影响存储期或连接。解释见 const/volatile 。
声明中只可以出现一个存储类说明符,但 thread_local 可以与 static 或 extern 结合 (C++11 起)。
cv限定符
cv-限定符:
- const
- 内存被初始化后,程序就不能再对它进行修改
- volatile:
- 即使程序代码没有对内存单元进行修改,其值也可能发生变化。
- 举个例子: 一个指针指向某个硬件位置,这时硬件可能修改其中的内容(而不是程序)
- 该关键字的作用:改善编译器的的优化能力
- 假如编译器发现,程序再几条语句中两次使用了某个变量,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。
- 这种优化假设变量的值在这两次使用之间不会变化。
- 如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。
mutable
用它来指出,即使将结构或者类变量为const,其某个成员也可以被修改:
struct data{
char name[30];
mutable int assesses;
};
int main(){
const data veep = {"ayicds d", 0};
strcpy(veep.name, "aaaa"); // not allowed;
veep.assesses++; // ok
}
veep的const限定符禁止修改veep的成员,但是assess成员的mutable说明符使得assess不受这种限制。
extern
变量
声明为extern的对象和变量是在另一个翻译单元中定义的,或者在包含外部链接的封闭范围中定义的。
- extern:静态或线程存储期和外部连接。
- 仅允许搭配变量声明和函数声明(除了类成员或函数形参)
- 它指定外部连接,而且技术上不影响存储期,但它不能用来定义自动存储期的对象,故所有 extern 对象都具有静态或线程存储期
- 另外,使用 extern 且无初始化器的声明不是定义。
语言链接性
链接程序要求每个不同的函数都有不同的符号名。
- 在C语言中,一个名称只对应一个函数,因此很容器实现。为了满足内部需要,C语言编译器可能将spiff这样的函数翻译成_spiff,这种方法被称为C语言链接性
- 在C++中,同一个名称可能对应多个函数,必须将这些函数翻译成不同的符号名称,因此,C++编译器执行名称矫正或者名称修饰,为重载函数生成不同的符号名称。比如spiff(int)转换成
_spiff_i
,spiff(double, double)转换成spiff_d_d
,这种方法就叫做C++语言链接。
链接程序寻找与C++函数调用匹配的函数时,使用的方法和C语言不同。但如果在C++程序中使用C库编译的函数,将出现什么情况呢?
比如:
spiff(2); //
在C库中的符号是_spiff,但是对C++查找的是_spiff_i
。找不到,为了解决这个问题,可以用函数原型来指出要使用的约定:
extern "C" void spiff(int); // C语言链接性名字查找
extern void spoff(int); //默认C++
extern "c++" void spaff(int); // 显式C++语言链接性
static
static
:静态或线程存储期和内部连接。
- 仅允许搭配(函数形参列表外的)对象声明、(块作用域外的)函数声明及匿名联合体声明
- 当用于声明对象时,它指定静态存储期(除非与 thread_local 协同出现)
static关键字可以用于如下情况:
- 在文件范围(全局范围/命名空间范围)声明变量或者函数时,
static
指定变量或者函数具有内部链接。在声明变量时,变量具有静态持续时间,并且除非您指定另一个值,否则编译器会将变量初始化为0 - 当用于声明类成员变量时,它会声明一个静态成员变量。它有如下特性:
- 这个成员的一个副本由类的所有实例共享。
- 必须在文件范围内定义静态数据成员。
- 声明为
const static
的整型数据成员可以有初始值设定项。
#include <iostream>
using namespace std;
class MyClass{
public:
static int m_i;
};
int MyClass::m_i = 0;
MyClass myObject1;
MyClass myObject2;
int main()
{
cout << myObject1.m_i << "\t" << myObject2.m_i << endl;
myObject1.m_i = 1;
cout << myObject1.m_i << "\t" << myObject2.m_i << endl;
myObject2.m_i = 2;
cout << myObject1.m_i << "\t" << myObject2.m_i << endl;
MyClass::m_i = 3;
cout << myObject1.m_i << "\t" << myObject2.m_i << endl;
}
- 当用于声明类成员函数时,它会声明一个静态成员函数。它有如下特性:
- 该函数由类的所有实例共享。
- 静态成员函数不能访问实例成员,它不关联到任何对象,因为该函数没有隐式
this
指针。如果要访问实例成员,请使用作为实例指针或引用的参数来声明函数。 - 静态成员函数不能为virtual、const 或 volatile
- 静态成员函数的地址不能存储在常规的函数指针中,但不能存储在成员函数指针中
- 不能将联合体声明为静态的。 但是,全局声明的匿名联合必须是显式声明的 static 。
- 在函数中声明变量时,
static
关键字指定该变量在调用该函数时保持其状态。比如下面例子:
#include <iostream>
void showstat(int curr){
static int nStatic;
nStatic += curr;
std::cout << "nStatic is " << nStatic << "\n";
}
int main()
{
for ( int i = 0; i < 5; i++ )
showstat( i );
}
下面示例显示了 static 成员函数中声明的局部变量。 static 变量可用于整个程序; 该类型的所有实例共享该变量的相同副本 static:
#include <iostream>
using namespace std;
struct C{
void Test(int value){
static int var = 0;
if (var == value)
cout << "var == value" << endl;
else
cout << "var != value" << endl;
var = value;
}
};
int main()
{
C c1;
C c2;
c1.Test(100);
c2.Test(100);
}
thread_local (线程存储期)
-
C++11起引入的
-
仅允许搭配声明于命名空间作用域的对象、声明于块作用域的对象及静态数据成员。不能用于函数声明或者定义
-
thread_local指示对象具有线程存储期:
- 使用说明符声明的变量
thread_local
只能在创建它的线程上访问。 - 变量在创建线程时创建,销毁线程时销毁。
- 每个线程都有自己的副本。
- 使用说明符声明的变量
-
thread_local 能与 static 或 extern 结合,以分别指定内部或外部连接(但静态数据成员始终拥有外部链接),但额外的 static 不影响存储期
-
从不同作用域指代的且带内部或外部连接的 thread_local 变量的名字可能指代相同或不同的实例,这取决于代码执行于相同还是不同的线程。
thread_local float f = 42.0; // Global namespace. Not implicitly static.
struct S // cannot be applied to type definition
{
thread_local int i; // Illegal. The member must be static.
thread_local static char buf[10]; // OK
};
void DoSomething()
{
// Apply thread_local to a local variable.
// Implicitly "thread_local static S my_struct".
thread_local S my_struct;
}