前言
存储期和链接涉及关键字 static、extern、auto、register(寄存器)、thread_local。其中register在C++17后弃用,thread_local使用频率很低。故本文主要描述static和extern。
使用角度的定义
static
- 修饰局部变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。静态成员变量的初始化必须在函数声明体之外。
- 修饰全局变量,使全局变量的作用域限制在cpp内部。当头文件定义static全局变量后,会在每个cpp生成一个常量区变量地址。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。功能与匿名命名空间类似,但符号上不同。
以上符号从上到下依次为:普通函数、静态函数、静态函数(另一个cpp中),匿名命名空间函数,匿名命名空间的静态函数 - 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
extern
使用在多文件之间需要共享某些代码时。具体使用场景如下:
- 声明一个变量,使用extern区分声明和定义。当extern后的“声明”具备复制,那这个表达式将等同于定义。注意可具有多个声明,但只能存在一个定义
- 多文件中共享const对象。
原理角度的定义
zh.cppreference.com/w/cpp/langu…
static 用于表示内部链接。static 说明符只能搭配(函数形参列表外的)对象声明、(块作用域外的)函数声明及匿名联合体声明。当用于声明类成员时,它会声明一个静态成员。当用于声明对象时,它指定静态存储期(除非与 thread_local 协同出现)。在命名空间作用域内声明时,它指定内部链接。
extern 用于表示外部链接。extern 声明符只能搭配变量声明和函数声明(除了类成员或函数形参)。它指定外部链接,而且技术上不影响存储期,但它不能用来定义自动存储期的对象,故所有 extern 对象都具有静态或线程存储期。另外,使用 extern 且没有初始化器的声明不是定义。
变量的初始化
zh.cppreference.com/w/cpp/langu…
粗略得将变量(variable)分为:
- 非局部变量、局部变量
- 静态变量、非静态变量
| 静态变量 | 非静态变量 | |
|---|---|---|
| 局部变量 | 见下文 局部静态变量的初始化 | 初始化具有auto存储期,在作用域开始前初始化结束时释放 |
| 非局部变量 | 见下文 非局部静态变量的初始化 |
非静态变量
初始化具有auto存储期,在作用域开始前初始化结束时释放
局部静态变量的初始化
首次经过它的声明时才会被初始化,后续调用被调过。如果初始化递归地进入正在初始化的变量的块,那么行为未定义。块作用域静态变量的析构函数在初始化已成功的情况下在程序退出时被调用。
非局部静态变量的初始化
所有具有静态存储期的非局部变量的初始化会作为程序启动的一部分在 main 函数的执行之前进行(除非被延迟。
虽然都是静态变量,但是其初始化也分为静态初始化和动态初始化。首选的是静态初始化,其发生在编译阶段,存储在常量区。当变量的初始化动作无法在编译器解决(如使用非平凡的析构的变量类型)时,会发生静态变量的动态初始化。
非局部静态变量的静态初始化
适用场景:
是能够被常量初始化的变量类型(说人话、不严谨的理解:const/constexpr 对象,且无需触发栈操作的构造)”zh.cppreference.com/w/cpp/langu…
动作行为:
当定义常量初始化,编译期会调用此初始化。否则,会执行零初始化(字面意思
使用方式:
- constexpr声明静态初始化的同时声明const属性,强制编译器选择静态初始化
- const声明,由编译器自行选择是否静态初始化
备注:
- 常量初始化通常在编译期进行。预先被计算的对象表示会作为程序映像的一部分存储下来。如果编译器没有这样做,那么它仍然必须保证该初始化发生早于任何动态初始化。
- 零初始化的变量将被置于程序映像的 .bss 段,它不占据磁盘空间,并在加载程序时由操作系统以零填充。
非局部静态变量的动态初始化
如静态变量初始化定义的那样,通常来说动态初始化都位于main函数执行之前进行。但这里涉及到一个棘手的问题,动态初始化顺序。
不严格的理解:
- 初始化顺序在同一个编译单元(同一个cpp)中按照代码定义出现的顺序进行初始化,释放顺序相反。
- 初始化顺序在跨编译单元中未定义。
问题解决:
- 重构整个代码,取消跨编译单元依赖静态变量
- 借助局部静态变量的调用前构造的特性,将非局部静态变量封装成函数对局部静态变量的调用。
实现示例:C++11后的单例
类静态成员的初始化
理论上类的静态成员属于非局部静态变量的范畴。书写上去全局变量有差异。
- 常量(定义见上文)静态成员变量的初始化可直接写在类声明中。此变量的初始化在编译期进行,定义位置跟类声明无关。
class MyClass {
public:
static const int I = 1;
static constexpr int L = 1;
};
- 非常量静态成员变量的定义不应直接存在于类声明中。这是因为非常量静态成员的初始化位于main函数前不在类初始化时。
class MyClass {
public:
inline static int Y = 1; // C++17 后支持
static int Z;
};
int MyClass::Z = 100;