存储类关键字
1 说明
存储类关键字是名称声明语法的decl-specifier-seq的一部分。和名称的作用域一起,控制着名称的两个独立属性,自动存储期和链接属性。
- auto 自动存储期。(C++11之前适用)
- register 自动存储期。另外,提醒编译器把对象放入处理器的寄存器中。(C++17之前适用,现已被废弃)
- static 静态或线程存储期,内部链接属性。
- extern 静态或线程存储期,外部链接属性。
- thread_local线程存储期。(C++11之后适用)
一次只能一个存储类关键字出现在声明语句中,thread_local是个例外,需要与static和或者extern结合使用。(C++11之后适用)
2 解释
- 关键字auto只被声明在块作用域或函数参数列表中的对象允许。它指示着,这类声明默认是自动存储期。在C++11中,这个关键字的意义被改变。
- 关键字register也是被声明在块作用域或函数参数列表中的对象允许。它指示着,这类声明默认是自动存储期。另外,这个关键字提示代码优化器保存该变量的值在CPU寄存器里。C++11放弃了这个关键字。
- 关键字static,被允许在对象的声明(除了函数列表),函数的声明(除在块作用域)和不具名联合体声明里使用。当用在类成员上时,它声明了一个静态成员。当用在对象声明上时,它指定了静态存储期(如果和thread_local联合使用除外)。当用在命名空间作用域内时,它指定了内部链接属性。
- 关键字extern只被允许用在变量和函数的声明上(除了类成员或函数参数)。它指定了外部链接属性,且不会影响存储期,但是它不能被用在一个具有自动存储期的对象身上,所以,所有的extern对象具有static或thread存储周期。另外,使用了extern且没有初始化的变量声明不是一个定义。
- 关键字thread_local被允许用在声明在命名空间范围和块作用域的对象,及静态数据成员。它指明,对象具有线程存储周期。可以和static或extern关键字一起使用,指明内部或者外部链接属性(除了static数据成员,其余的总是具有外部链接属性),但是添加的static关键字不会影响其存储周期。
3 存储期
所有的对象都具有下面这些存储类型中的一种:
- automatic
对象在代码块开始时被分配,离开时收回分配的存储空间。所有的局部对象都有这种存储周期,除非,它们被声明为static,extern或thread_local。 - static
当程序开始运行时分配对象的存储空间,程序结束时收回对象的存储空间。只允许一个对象实例存在。所有声明在命名空间的对象(包括全局命名空间),前面加上static,或者extern关键字的都有这种存储周期。 - thread
线程开始分配对象,线程结束收回分配给对象的存储空间。每个线程拥有这个对象唯一的实例。只有使用关键字thread_local声明的对象才有这种存储周期。关键字thread_local 可以和static或extern结合使用,以调整链接属性。 - dynamic 当使用动态内存分配函数请求分配或者回收对象的存储空间时才会使用。
4 链接
名称意指对象,引用,函数,类型,模板,命名空间,或数值(枚举器),都可以有链接属性。如果一个名称具有链接属性,那么在另一个作用域内声明而引入的相同名称就会被引用为同一个实体。如果在几个作用域内声明了具有相同名称的变量,函数,或另一个实体,但是又没有有效的链接属性,那么就会产生几个实体实例。
链接类型可以被分为下面三种:
(1)无链接。
这种方式适用于名称在它的作用域内的情况。下面的几种情况具有非链接属性:
- 名称没有显式地使用extern关键字声明(无关static修饰符);
- 局部类和它的成员函数;
- 块作用域内声明的其它名称,例如typedef,enum等声明的名称;
(2)内部链接。
在当前的编译单元里能够被所有的作用域引用的名称。命名空间作用范围内声明的下面中的任何一种名称都具有内部链接属性:
- static声明的变量,函数,和函数模板;
- 没有使用extern声明或者之前也没有被声明为具有外部链接属性的非易失性(non-volatile)非内嵌常量限定的变量(包括constexpr);
- 不具名联合体的数据成员;
另外,在不具名命名空间或者不具名命名空间内部的命名空间里声明的所有变量,即使明确使用extern声明,也是内部链接属性。
(3)外部链接
可被其它编译单元参考引用的名称,就具有外部链接属性。具有外部链接属性的变量和函数也具有语言链接属性,这使得链接不同程序语言写的编译单元成为可能。
任何在命名空间里声明的下列变量都具有外部链接属性,除非,命名空间是不具名的或者被一个不具名命名空间包含(C++11之后)。
- 上面没有列出的变量和函数(也就是说,没有被声明为static函数,命名空间范围内的非const变量没有被声明为static,任何声明为extern的变量)
- 枚举和枚举器
- 类名,它们的成员函数,静态数据成员(const与否),嵌套类和枚举类型变量,类体内首次使用友邻声明的函数
- 上面没有列出的所有模板变量(就是说,声明为static的非函数模板)
首次声明在块作用域内的下列变量中任何一种都有外部链接属性:
- 声明为extern的变量
- 函数变量
5 静态局部变量
使用限定符static声明在块作用域内的变量具有static存储期,只有当第一次执行经过它们的声明时被初始化(除非它们的初始化是0或常量初始化,这种初始化可以在进入块作用域之前就已经完成)。在所有后面的调用中,声明都会被跳过,不执行。
如果初始化过程出现异常,那么不认为变量被初始化,再一次尝试控制经过声明语句时,还会初始化。
如果多个线程同时尝试初始化相同的静态变量,初始化也只会进行一次(对于使用std::cal_once的任意函数都能获得相似的行为)。
注意:这个功能的通常实现就是使用双重检查锁定模式,它可以减少已经初始化为局部静态和单个非原子boolean的比较产生的系统开销。
当程序exit时,调用块作用域的析构函数,但前提是初始化成功。
对于同一个内嵌函数(也许是隐含内嵌)的所有定义里的局部静态对象,都会被一个编译单元定义的相同的对象引用。
6 注意
在C语言中,在顶层命名空间作用域(相当于C的文件范围)内的,是const且没有extern修饰的名称具有外部属性,但是在C++中却是内部链接属性。
在C里,寄存器变量的地址不能获取,但是在C++中,变量声明为register和没有任何存储类关键字修饰是没有什么区别的。(C++11之前)
C++中,不像C,变量不能声明为register。(C++17之后)
具有内部或外部链接属性的thread_local型变量的名称可以被不同的实例引用,依赖于代码是否在同一个或者不同的线程中执行。
关键字extern也可以指定语言链接属性和明确的模板实例声明,但是它不是存储类限定符(除非,声明被直接包含在语言链接指定中,在这种情况时,声明被像包含extern限定符一样对待)。
在C++语法中,关键字mutable是存储类限定符,尽管它不会影响存储周期或链接属性。
现在这段是不完整的,因为在同一个编译单元重新声明的规则。
存储类限定符,对于thread_local是个例外,不允许明确的指定和明确的实例。
template <class T> struct S {
thread_local static int tlm;
};
template <> thread_local int S<float>::tlm = 0; // "static" 没有出现在这里
7 关键字
auto, register, static, extern, thread_local
8举例
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
thread_local unsigned int rage = 1;
std::mutex cout_mutex;
void increase_rage(const std::string& thread_name)
{
++rage; // 锁外修改是没问题的;这是一个thread-local 变量
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Rage counter for " << thread_name << ": " << rage << '\n';
}
int main()
{
std::thread a(increase_rage, "a"), b(increase_rage, "b");
{
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "Rage counter for main: " << rage << '\n';
}
a.join();
b.join();
}
可能的输出:
Rage counter for a: 2
Rage counter for main: 1
Rage counter for b: 2