存储类说明符

来自cppreference.com
< cpp‎ | language

存储类说明符是一个名字的声明语法声明说明符序列的一部分。它与名字的作用域一同控制名字的两个独立性质:它的“存储期”和它的“链接”。

  • auto (C++11 前)无说明符 - 自动 存储期。
  • register - 自动 存储期,另提示编译器将此对象置于处理器的寄存器。(弃用)
(C++17 前)
  • static - 静态 或线程 存储期和内部 链接。
  • extern - 静态 或线程 存储期和外部 链接。
  • thread_local - 线程 存储期。
(C++11 起)


声明中只能出现一个存储类说明符,但 thread_local 可以与 staticextern 结合 (C++11 起)

解释

1) auto 说明符只能搭配在块作用域或函数形参列表中声明的对象。它指示自动存储期,即这种声明的默认情况。此关键词的含义在 C++11 有变更。
(C++11 前)
2) register 说明符只能搭配在块作用域或函数形参列表中声明的对象。它指示自动存储期,即这种声明的默认情况。另外,此关键词的存在可以用来提示优化器将此变量的值存储到 CPU 寄存器。此关键词已经被弃用。
(C++17 前)
3) static 说明符只能搭配(函数形参列表外的)对象声明、(块作用域外的)函数声明及匿名联合体声明。当用于声明类成员时,它会声明一个静态成员。当用于声明对象时,它指定静态存储期(除非与 thread_local 协同出现)。在命名空间作用域内声明时,它指定内部链接。
4) extern 声明符只能搭配变量声明和函数声明(除了类成员或函数形参)。它指定外部链接,而且技术上不影响存储期,但它不能用来定义自动存储期的对象,故所有 extern 对象都具有静态或线程存储期。另外,使用 extern 且没有初始化器的声明不是定义
5) thread_local 关键词只能搭配在命名空间作用域声明的对象、在块作用域声明的对象及静态数据成员。它指示对象具有线程存储期。如果对块作用域变量只应用了 thread_local 这一个存储类说明符,那么同时也意味着应用了 static。它能与 staticextern 结合,以分别指定内部或外部链接(但静态数据成员始终拥有外部链接)。
(C++11 起)

存储期

程序中的所有对象都具有下列存储期之一:

  • 自动(automatic)存储期。这类对象的存储在外围代码块开始时分配,并在结束时解分配。未声明为 thread_local (C++11 起)staticextern 的所有局部对象均拥有此存储期。
  • 静态(static)存储期。这类对象的存储在程序开始时分配,并在程序结束时解分配。这类对象只存在一个实例。所有在命名空间(包含全局命名空间)作用域声明的对象,加上声明带有 staticextern 的对象均拥有此存储期。有关拥有此存储期的对象的初始化的细节,见非局部变量静态局部变量
  • 线程(thread)存储期。这类对象的存储在线程开始时分配,并在线程结束时解分配。每个线程拥有它自身的对象实例。只有声明为 thread_local 的对象拥有此存储期。thread_local 能与 staticextern 一同出现,它们用于调整链接。关于具有此存储期的对象的初始化的细节,见非局部变量静态局部变量
(C++11 起)
  • 动态(dynamic)存储期。这类对象的存储是通过使用动态内存分配函数来按请求进行分配和解分配的。关于具有此存储期的对象的初始化的细节,见 new 表达式

子对象和引用成员的存储期与它们的完整对象一致。

链接

指代对象、引用、函数、类型、模板、命名空间或值的名字,可具有链接。如果某个名字具有链接,那么它所指代的实体与另一作用域中的声明所引入的相同名字指代相同的实体。如果有变量、函数或其他实体在数个作用域声明但没有足够的链接,那么就会生成该实体的多个实例。

以下各种链接可以被识别:

无链接

名字只能从它所在的作用域使用。 在块作用域声明的下列任何名字均无链接:

  • 未显式声明为 extern 的变量(不管有没有 static 修饰符);
  • 局部类和它的成员函数;
  • 在块作用域声明的其他名字,例如 typedef、枚举及枚举项。

未指定为拥有外部、模块 (C++20 起)或内部链接的名字同样无链接,这与它的声明所处的作用域无关。

内部链接

名字可从当前翻译单元中的所有作用域使用。 在命名空间作用域声明的下列任何名字均具有内部链接;

  • 类型具有 const 限定 但没有 volatile 限定的非模板 (C++14 起)变量,除非:
(C++17 起)
(C++20 起)
  • 它显式声明为 extern,或
  • 它已在先前声明,而且先前的声明不具有内部链接;

另外,所有在无名命名空间或无名命名空间内的命名空间中声明的名字,即使显式声明为 extern 也均拥有内部链接。

(C++11 起)
外部链接

名字能从其他翻译单元中的作用域使用。具有外部链接的变量和函数也具有语言链接,这使得以不同编程语言编写的翻译单元可以互相链接。 在命名空间作用域声明的下列任何名字均具有外部链接,除非这些名字在无名命名空间内声明或它们在具名模块声明且未被导出 (C++20 起)

  • 以上未列出的变量与函数(即未声明为 static 的函数、命名空间作用域内未声明为 static 的非 const 变量,和所有声明为 extern 的变量);
  • 枚举;
  • 类以及其成员函数、静态数据成员(不论是否 const)、嵌套类及枚举,及首次以类体内的 friend 声明引入的函数的名字;
  • 所有未列于上的模板名(即未声明为 static 的函数模板)。

任何首次在块作用域声明的下列名称拥有外部链接:

  • 声明为 extern 的变量名;
  • 函数名。
模块链接

名字只能从同一模块单元或同一具名模块中的其他翻译单元的作用域指代。

如果在命名空间作用域声明的名字附着到具名模块,未被导出且无内部链接,那么它拥有模块链接。

(C++20 起)

静态局部变量

在块作用域声明且带有 staticthread_local (C++11 起) 说明符的变量拥有静态或线程 (C++11 起)存储期,但在控制首次经过它的声明时才会被初始化(除非它被零初始化常量初始化,这可以在首次进入块前进行)。在其后所有的调用中,声明都会被跳过。

如果初始化抛出异常,那么不认为变量被初始化,且控制下次经过该声明时将再次尝试初始化。

如果初始化递归地进入正在初始化的变量的块,那么行为未定义。

如果多个线程试图同时初始化同一静态局部变量,那么初始化严格发生一次(类似的行为也可对任意函数以 std::call_once 来达成)。

注意:此功能特性的通常实现均使用双检查锁定模式的变体,这使得对已初始化的局部静态变量检查的运行时开销减少为单次非原子的布尔比较。

(C++11 起)

块作用域静态变量的析构函数在初始化已成功的情况下在程序退出时被调用。

在相同内联函数(可以是隐式内联)的所有定义中,函数局部的静态对象均指代在一个翻译单元中定义的同一对象,只要函数拥有外部链接。

注解

位于顶层命名空间作用域(C 中的文件作用域),且是 const 而非 extern 的名字在 C 中具有外部链接,但在 C++ 中具有内部链接。

C++11 起,auto 不再是存储类说明符;它被用于指示类型推导。

在 C 中,不能取 register 变量的地址,但在 C++ 中,声明为 register 的对象与声明不带任何存储类说明符的变量在语义上没有区别。

(C++17 前)

与 C 不同,在 C++ 中不能将变量声明为 register

(C++17 起)

从不同作用域指代的且带内部或外部链接的 thread_local 变量的名字可能指代相同或不同的实例,这取决于代码在相同还是不同的线程执行。

extern 关键词也能用来指定语言链接显式模板实例化声明,但它在这些情况中不是存储类说明符(但当声明直接在语言链接说明中所包含时将声明当做如同它含 extern 说明符)。

关键词 mutable 在 C++ 语言的语法中是存储类说明符,尽管它并不影响存储期或链接。

thread_local 以外的存储类说明符都不能显式特化显式实例化中使用:

template<class T>
struct S
{
    thread_local static int tlm;
};
 
template<>
thread_local int S<float>::tlm = 0; // "static" 不在此出现

关键词

auto, register, static, extern, thread_local, mutable

示例

#include <iostream>
#include <mutex>
#include <string>
#include <thread>
 
thread_local unsigned int rage = 1; 
std::mutex cout_mutex;
 
void increase_rage(const std::string& thread_name)
{
    ++rage; // 在锁外修改 OK;这是线程局部变量
    std::lock_guard<std::mutex> lock(cout_mutex);
    std::cout << 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 << "main 的愤怒计数:" << rage << '\n';
    }
 
    a.join();
    b.join();
}

可能的输出:

a 的愤怒计数:2
main 的愤怒计数:1
b 的愤怒计数:2

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 216 C++98 无名类和无名枚举在类作用域和命名空间作用域中的链接不同 它们在这些作用域都有外部链接
CWG 389 C++98 无链接的名字不能用来声明有链接的实体 无链接的类型不能作为有链接的变量或函数的
类型,除非该变量或函数已经有 C 语言链接
CWG 426 C++98 可以在同一个翻译单元中对某个实体
同时进行内部链接声明和外部链接声明
此时程序非良构
CWG 527 C++98 CWG 问题 389 的解决方案引入的类型也适用于
无法在自己所在的翻译单元外被命名的变量或函数
解除对这些变量或函数(即无链接或具有
内部链接,或在无名命名空间内声明)的限制
CWG 809 C++98 register 的功能极为有限 将其弃用
CWG 1648 C++11 thread_local 即使与 extern
一起使用也意味着应用了 static
只有在单独使用 thread_local
才意味着应用了 static
CWG 1686 C++98
C++11
在命名空间作用域声明的非静态变量只有在显式声明为
const(C++98)或 constexpr(C++11)时才具有内部链接
只需要类型有 const 限定
CWG 2019 C++98 未指明引用成员的存储期 与它们的完整对象一致
CWG 2387 C++14 不明确有 const 限定的变量模板是否默认有内部链接 const 限定符不影响变量
模板或它的实例的链接
P2788R0 C++20 即使在模块单元中,在命名空间中声明
具有 const 限定的变量也会给予它内部链接
不给予内部链接

参阅