# C++深入理解static的弯弯绕绕 ## 前言 大多数 C++ 关键字使用起来都是比较简单的,但也有少数相对复杂,`static` 便是其中的一个代表。标准往往会避免为语言增加新的关键字,而是复用已有的。这使得 `static` 如今已存在多种不同的意思,可以修饰全局,也可以修饰局部;可以修饰函数,也可以修饰变量;还可以和 `inline`、`const`、`constexpr`、`constinit` 等关键字组合起来使用。 ## 数据的链接方式 不同的数据作用范围也不尽相同,如何调整数据的作用范围?就是通过那些变量修饰符,它们能改变数据的链接属性。每个 `.cpp/.cc/.cxx...` 文件称为一个 TU(Translation Units,翻译单元),有些数据只在当前 TU 使用,有些数据还需要在其他 TUs 使用,前者称为内部链接,后者称为外部链接。每个 TU 就是一个模块,链接就是将这些模块组合起来,同时保证其中所引用的各种符号都在正确的位置上。只有在当前 TU 中使用的数据才需要内部链接,局部的那些数据属于无链接。我们需要关注的是一个变量的内部链接和外部链接是如何指定的。 ## static修饰局部变量 函数中局部变量的存储时期为 `automatic`,此类变量无链接,使用时在栈上自动分配内存,离开作用域时自动释放,只能在当前作用域使用。 但如果为这样的局部变量加上 `static`,就将其存储时期由 `automatic` 改变成了 `static`,生命周期遍及整个程序的生命周期。这种变量的‌内存空间‌在程序加载时就在.data或者.bss段预留好了。但是,其‌逻辑初始化‌(特别是涉及构造函数调用时)是‌惰性‌的,仅在程序执行流第一次到达该声明语句时才发生。这种机制保证了初始化的线程安全性(C++11起)和按需执行。这种局部静态变量就相当于全局变量,不同之处在于它是无链接,可见性仅在当前函数,而且可以延迟初始化,并且在程序结束时销毁。 ```cpp class Test { public: Test() { std::cout << "Test() " << this << std::endl; } ~Test() { std::cout << "~Test() " << this << std::endl; } int index = 0; }; void test() { static Test a; a.index++; std::cout << "a.index = " << a.index << std::endl; } int main(int argc, char *argv[]) { std::cout << "test() start" << std::endl; test(); test(); std::cout << "test() end" << std::endl; return 0; } ``` ## static修饰全局变量 全局变量处于 `static` 存储时期,也对应于**加载期**,在 `main()` 执行之前就已为这些变量分配了内存并初始化。 ```cpp static Test a; void test() { a.index++; std::cout << "a.index = " << a.index << std::endl; } ``` 如果一个全局变量被 `extern` 修饰,则它具有外部链接,能够被其他 TUs 使用。相反,如果一个全局变量被 `static` 修饰,它具有内部链接,只能在当前 TU 使用。 ```cpp test1.h #include void test1(); test1.cpp #include "test1.h" extern int var1 = 1; // external linkage int var2 = 2; // external linkage(同var1,extern可省略) static int var3 = 3; // internal linkage void test1() { std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; std::cout << "var2 value:" << var2 << " address:" << &var2 << std::endl; std::cout << "var3 value:" << var3 << " address:" << &var3 << std::endl; } main.cpp #include "test1.h" extern int var1; // refers to the var1 defined in the test1.cpp extern int var2; // refers to the var2 defined in the test1.cpp // extern static int var3; // error:报错,语法上不允许指定多个存储类 static int var3 = 3; // 内部链接,跟test1.cpp中的var3只是名字一样, // 但不是同一个变量 int main(int argc, char *argv[]) { test1(); std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; std::cout << "var2 value:" << var2 << " address:" << &var2 << std::endl; std::cout << "var3 value:" << var3 << " address:" << &var3 << std::endl; return 0; } ``` 结果可以看到两个文件中对 `var1`、`var2` 的引用是同一个,而 `var3` 却不是同一个。 如果将 `var1`、`var2` 两个外部链接的变量放到头文件`test1.h` 中,`test1.h` 同时被 `test.cpp` 与 `main.cpp` 引用,将会报错多重定义的符号,那么如何解决呢?答案就是在头文件中声明为 `extern`,在源文件中定义并赋值: ```cpp test1.h extern int var1; extern int var2; void test1(); test1.cpp #include “test1.h” int var1 = 1; extern int var2 = 2; void test1() { std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; std::cout << "var2 value:" << var2 << " address:" << &var2 << std::endl; } main.cpp #include “test1.h” int main(int argc, char *argv[]) { test1(); std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; std::cout << "var2 value:" << var2 << " address:" << &var2 << std::endl; return 0; } ``` 跨动态库的情况下要使用 `var1`、`var2` 两个外部链接的变量,则需要导出/导入操作: ```cpp test1.h MY_DLL_EXPORT extern int var1; MY_DLL_EXPORT extern int var2; ``` 而 `static` 修改的内部链接变量将会在两个编译单元分别分配内存,成为两个独立的变量。因为程序在编译时,会将头文件展开到源文件中进行编译,所有就相当于 `test.cpp` 与 `main.cpp` 都定义了内部链接变量 `var3`,互不影响。 ```cpp test1.h static int var3 = 3; void test1(); test1.cpp #include “test1.h” void test1() { std::cout << "var3 value:" << var3 << " address:" << &var3 << std::endl; } main.cpp #include “test1.h” int main(int argc, char *argv[]) { test1(); std::cout << "var3 value:" << var3 << " address:" << &var3 << std::endl; return 0; } ``` 无名的命名空间,意味着命名空间中的标识符只能在本 TU 访问,相当于给这个标识符加上了 `static`,使其成为内部链接。 ```cpp namespace { int var1 = 1; } ``` 若是再考虑组合 `const/constexpr` 进行修饰,情况则又不相同。 - 如果一个全局变量**没有使用** `const/constexpr` 修饰,那么它默认具有外部链接属性,无需多此一举再加上 `extern` 修饰。而对于这样一个变量,只需使用 `static` 修饰,就可以将它变成内部链接。 - 如果一个全局变量**使用**了 `const/constexpr` 修饰,则它默认就有了内部链接属性。此时如果再加上 `static` 修饰,也是多此一举。其他源文件此时将无法访问该全局变量。 - 如果在头文件中全局变量**使用**了 `const/constexpr` 修饰,跟 `static` 一样,将会生成多份实例。如何改变呢?前面加上 `extern` 修饰,就可以让它变成外部链接属性。 ```cpp int var_1 = 42; // external linkage by default extern int var_2 = 42; // same as var_1, but it's redundant. static int var_3 = 42; // internal linkage const int var_4 = 42; // internal linkage by default static const int var_5 = 42; // same as var_4, but it's redundant. extern const int var_6 = 42; // external linkage constexpr int var_7 = 42; // internal linkage by default static constexpr int var_8 = 42; // same as var_7, but it's redundant. ``` ## static修饰成员变量 如果一个类成员变量以 `static` 修饰,那么该变量只是一个声明,需要额外提供定义,类的所有对象共享此类变量。 ```cpp class Test { static int x; // declaration }; int Test::x = 0; // definition,initialize outside the class ``` 为什么需要在外部定义呢? 因为 `static` 对象必须满足 ODR(One Definition Rule),而类一般是在头文件中声明,该头文件可能会被多个 TUs 包含,每个对象必须具备唯一的定义,否则在编译链接时会出现问题。所以将它作为一个声明,定义在类外部单独指定。 正常情况下,头文件类中声明,源文件中定义并初始化赋值,跨dll使用需要导入导出,所有使用 `static` 成员变量的地方都是同一个对象。 ```cpp // test1.h class Test { public: void testFun() { std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; } static int var1; }; // test1.cpp #include "test.h" int Test::var1 = 1; // main.cpp int main(int argc, char *argv[]) { Test test; test.testFun(); std::cout << "var1 value:" << test.var1 << " address:" << &test.var1 << std::endl; } ``` 头文件类中声明,头文件中定义并初始化赋值,在同一个模块(指 exe/dll)中当有多个源文件同时包含此头文件时,将会出现重复定义报错。 ```cpp // test1.h class Test { public: void testFun() { std::cout << "var1 value:" << var1 << " address:" << &var1 << std::endl; } static int var1; }; int Test::var1 = 1; // test2.cpp #incldue "test1.h" // main.cpp #incldue "test1.h" ``` 在不同模块中,将会生成多段内存,在自己的模块中使用自己的独立变量,**注意此时仍然不能出现同一模块多个源文件同时引用**。 **注:`static` 静态变量不允许被导出,因为它们默认是 `internal` 链接方式** ## static修饰全局函数 正常情况下,全局函数默认是外部链接,可以通过前置声明在多个 TUs 使用。 如果以 `static` 修饰全局函数,则将其链接方式变为内部链接,只能在当前 TU 使用。 ```cpp // test1.cpp #include static void foo() { std::cout << "internal linkage\n"; } void bar() { std::cout << "external linkage\n"; } // main.cpp extern void foo(); extern void bar(); // refers to the bar() defined in the test1.cpp int main() { foo(); // Error, undefined reference to 'foo()' bar(); // OK } ``` ## static修饰成员函数 以 `static` 修饰的成员函数属于‌类本身‌,而不是类的某个具体对象实例。因此,编译器‌不会‌为静态成员函数生成 `this` 指针。这样的 `static` 成员函数无法访问其他的非静态成员(包括变量和函数),因为那些名称都与对象绑定(但可以通过将对象作为入参的方式来达到目的)。由于 `static` 成员函数没有 `this` 指针,以下修饰符对其‌无效且会导致编译错误: 1. const:const修饰的是 `this` 指针指向的对象不可变,既然没有 `this`,就不能声明为 `static void func() const;`。 2. `virtual`‌:虚函数机制依赖对象的虚表指针(通常在对象头部),静态函数不绑定对象,因此不能是虚函数。 ```cpp class Test { public: static void testFun() { // std::cout << index << std::endl; // 出错,对非静态成员“Test::index”的非法引用 std::cout << "static member fun" << std::endl; } private: int index = 0; }; int main() { Test::testFun(); Test t; t.testFun(); } ``` ## static修饰变量对Lambdas捕获参数的影响 1. 如果是全局变量(包括 `const` 和 `constexpr` 修饰的),那么 `Lambdas` 无需捕获便可以直接使用: ```cpp int x = 42; int main() { // you don't need to capture a global variable [] { return x; }; } ``` 1. 如果是局部变量(包括 `const` 和 `constexpr` 修饰的,但排除 `static` 修饰的静态局部变量),由于它的存储时期为 `automatic`,就必须捕获才能使用: ```cpp int main() { int x = 42; // you have to capture a local variable [&x] { return x; }; } ``` 1. 如果使用 `static` 修饰的变量(包括全局静态变量、类静态成员变量、函数内的静态局部变量)具有‌静态存储期‌。Lambda 表达式可以直接访问这些变量,无需将其列入捕获列表: ```cpp int main() { static int x = 42; // OK [] { return x; }; } ```