An In-depth Look at C++ Keyword: static
大多数 C++ 关键字使用起来都是比较简单的,但也有少数相对复杂,static 便是其中的一个代表。
标准往往会避免为语言增加新的关键字,而是复用已有的。这使得 static 如今已存在十几种不同的意思,可以修饰全局,也可以修饰局部;可以修饰函数,也可以修饰变量;还可以和 inline、const、constexpr、constinit 等关键字组合起来使用。
许多 C++ devs 对其都只处于一个浅层次的理解,不全面也不深入,用来不明所以。通过本文能够弥补此部分知识点。
1
内存布局
程序由指令和数据构成,指令和数据又存储在不同的段内,我们首先要了解执行程序的内存布局,才能从一个更底层的视角来理解 static。
常见的程序分段如表格所示:
Sections | Meaning |
---|---|
.code /.text | 代码段,存储编译后的机器指令 |
.data | 数据段,通常存储全局变量、静态局部变量和常量 |
.bss | Block Started by Symbol的缩写,用来存储未初始化的全局变量和静态局部变量,实际并不占据空间,仅是请求加载器在程序加载到内存时,预留一些空间 |
.rodata | read-only data的缩写,通常存储静态常量 |
程序分段有利于区分指令和数据,指令通常是只读的,数据是可读写或只读的,分到不同的段内,设置不同的权限,可以防止程序指令被无意修改。
一个程序编译后的内存布局图可能如下图所示:
可以看到,程序被分成了六个主要的内存区域。因为代码指令、数据段等往往是固定大小的,所以处于低地址;堆栈可能会在程序执行时动态增长,所以处于高地址。
同时,不同段的数据还代表着不同的存储时期。
2
存储时期
C++中不同的数据存在四个存储时期,分别为 automatic, static, thread 和 dynamic。
automatic 主要指的就是栈上的数据,它能够在进入某个作用域时自动申请内存,并在离开时自动释放内存。
static 指的主要是 .data/.bss/.rodata 段的数据,这些数据在程序执行时就申请内存,等到程序结束时才释放。
而 thread 存储时期是 C++11 才有的,只有 thread_local 修饰的数据才属此类,它们在线程开始时申请内存,线程结束时释放内存。
dynamic 则表示堆上的数据,也就是使用 new/malloc 申请的内存,这些内存必须手动释放。 当然,通过智能指针这些数据的生命周期也能够自动管理。
不要把这些存储时期与编译期/运行期混淆,它们是编译原理的概念,按那种尺度来划分,则存在编译期-加载期-运行期三个不同的时期。
编译期指的是编译器处理代码的时期,该时期的数据符号地址被翻译成绝对地址,是最早就确定的数据。constexpr/constint 所修饰的数据,一般就是在这一时期分配的内存。
编译后的程序存储在硬盘上,准备执行时操作系统需要将它们读取到 RAM 中,这个时期就叫加载期。.data/.rodata 段的数据就是在这一时期分配内存的,一个常见的误区就是认为 static 数据是处于编译期。
运行期是程序已经运行,指令已经开始被CPU处理了。一些额外的内存需要分配给现在才存在的数据,比如 .bss 和堆栈数据就属于这一时期。
内存布局和存储时期搞清楚了,下面需要理解链接的基本概念。
3
链接
不同的数据作用范围也不尽相同,如何调整数据的作用范围?就是通过那些变量修饰符,它们能改变数据的链接方式。
每个 .cpp/.cc/.cxx... 文件称为一个 TU(Translation Units,翻译单元),有些数据只在当前 TU 使用,有些数据还需要在其他 TUs 使用,前者称为内部链接,后者称为外部链接。
每个 TU 就是一个模块,链接就是将这些模块组合起来,同时保证其中所引用的各种符号都在正确的位置上。
只有在当前 TU 中使用的数据才需要内部链接,局部的那些数据属于无链接。我们需要关注的是一个变量的内部链接和外部链接是如何指定的。
先说内部链接,这些名称能够在整个 TU 使用,以下是其规则:
命名空间下以 static 修饰的变量、变量模板、函数和函数模板;
命名空间下以 const 修饰的变量;
匿名 union 的数据成员;
匿名空间下的所有名称。
再说外部链接,这些名称能够在不同 TUs 间使用,外部链接的名称甚至能够和其他语言生成的 TUs 链接。规则如下:
命名空间下没有以 static 修饰的函数,没有额外修饰的变量和以 extern 修饰的变量;
命名空间下的枚举名称;
命名空间下的类名称,包含它们的成员函数、(const) static 数据成员、嵌套类/枚举、首次引入的友元函数;
命名空间下以 static 修饰的非函数模板;
首次在block scope 下的函数名、以 extern 修饰的变量名。
暂时先留个大概印象,后面还会再次以具体例子介绍有些规则。
4
以 static 修饰变量
前置概念介绍完了,下面从不同方面来进行讨论 static 关键字。本节关注于修饰变量,这又有全局和局部之分。
4.1
以 static 修饰全局变量
全局变量处于 static 存储时期,也对应于加载期,在 main() 执行之前就已为这些变量分配了内存。
如果一个全局变量被 extern 修饰,则它具有外部链接,能够被其他 TUs 使用。相反,如果一个全局变量被 static 修饰,它具有内部链接,只能在当前 TU 使用。
一个例子:
// tu-one.cpp
extern int var_1 = 42; // external linkage
static int var_2 = 24; // internal linkage
// tu-two.cpp
#include <iostream>
// refers to the var_1 defined in the tu-one.cpp
extern int var_1;
int main() {
std::cout << var_1 << "\n"; // prints 42
}
若是再考虑组合 const 进行修饰,情况则又不相同。
如果一个全局变量没有使用 const 修饰,那么它默认就有 extern 链接,无需多此一举再加上 extern 修饰。而对于这样一个变量,如何改变它的外部链接方式?只需使用 static 修饰,就可以将它变成内部链接。
如果一个全局变量使用了 const/constexpr 修饰,则它默认就有了 static 链接。此时如果再加上 static 修饰,也是多此一举。其他文件此时将无法访问该全局变量,如何改变呢?前面加上 extern 修饰,就可以让它变成外部链接。
以上内容的一个例子:
// tu-one.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.
4.2
以 static 修饰局部变量
局部变量也要分情况讨论一下,先说函数中的局部变量。
函数中局部变量的存储时期为 automatic,此类变量无链接,使用时在栈上自动分配内存,离开作用域时自动释放,只能在当前作用域使用。
如果为这样的局部变量加上 static,就将其存储时期由 automatic 改变成了 static,生命周期遍及整个程序的生命周期。这种变量实际是先在 .bss 段预留了空间,等到首次进入该函数,才真正为其分配内存,此时初始化的时机就不是加载期,而且延迟到了运行期,所以这种方式也叫惰性初始化。
这种局部静态变量就相当于全局变量,不同之处在于它是无链接,可见性仅在当前函数,而且可以延迟初始化。
4.3
以 static 修饰成员变量
如果一个类成员变量以 static 修饰,那么该变量只是一个声明,需要额外提供定义,类的所有对象共享此类变量。
class S {
static int x; // declaration
};
int S::x = 0; // definition,initialize outside the class
为什么需要在外部定义呢?
因为 static 对象必须满足 ODR(One Definition Rule),而类一般是在头文件中声明,该头文件可能会被多个 TUs 包含,每个对象必须具备唯一的定义,否则在编译链接时会出现问题。所以将它作为一个声明,定义在类外部单独指定。
但是在某些时候也可以不用定义,比如:
// Example from cppref
struct S
{
static const int x = 0; // static data member
// a definition outside of class is required if it is odr-used
};
const int& f(const int& r);
int n = b ? (1, S::x) // S::x is not odr-used here
: f(S::x); // S::x is odr-used here: a definition is required
只有在 ODR-used 时才必须要提供定义,在不需要 lvalue 的表达式中,它可以直接使用 S::x 的值,此时经历了 lvalue-to-rvalue 的隐式转换。相反,在需要 lvalue 的表达式中,则必须提供定义。
注:ODR-used 是标准用来指必须为实体提供定义的术语,因为它不是必须的,需要依赖情境讨论,所以不单独使用 used 来描述。比如一个虚函数是非 ODR-used,而一个纯虚函数是 ODR-used,使用时必须提供定义。模板只在使用时才实例化,这里的使用准确的描述也应该是 ODR-used。
如果嫌在外部定义麻烦,在 C++17 可以采用 inline 来光明正大地违背 ODR,它能够告诉链接器,我想在多个 TUs 之间拥有相同的定义。
class S {
// since C++17
inline static int x = 42;
};
在 C++20,由于 constexpr 会隐式 inline,所以还可以这么写:
class S {
// since C++20
static constexpr int x = 42;
};
另外,在 C++98,如果以 static const 修饰一个整型成员数据,那么也可以在类内直接初始化,并且可以保证初始化是在编译期完成的。
// C++98
struct S {
static const int x = 42; // OK
const int y = 42; // since C++11, default member initializer
};
对于非 static 数据成员,自 C++11 开始支持 default member initializer,于是也可以直接在类内直接初始化。
5
以 static 修饰函数
函数也分全局函数和成员函数,以 static 修饰时也要分别讨论。
5.1
以 static 修饰全局函数
正常情况下,全局函数默认是外部链接,可以通过前置声明在多个 TUs 使用。
如果以 static 修饰全局函数,则将其链接方式变为内部链接,只能在当前 TU 使用。
一个小例子:
// tu-one.cpp
#include <iostream>
static void foo() {
std::cout << "internal linkage\n";
};
void bar() {
std::cout << "external linkage\n";
}
// tu-two.cpp
extern void foo();
extern void bar(); // refers to the bar() defined in the tu-one.cpp
int main() {
foo(); // Error, undefined reference to 'foo()'
bar(); // OK
}
5.2
以 static 修饰成员函数
之前在【洞悉C++函数重载决议】里已经讲解过本节内容。
以 static 修饰的成员函数增加了一个隐式对象参数,它并不是 this 指针,而是为了重载决议能够正常运行所定义的一个可以匹配任何参数的对象参数。这样的成员函数无法访问其他的非静态成员名称,因为那些名称都与对象绑定。
当时编写的一个示例:
struct S {
void f(long) {
std::cout << "member version\n";
}
static void f(int) {
std::cout << "static member version\n";
}
};
int main() {
S s;
s.f(1); // static member version
}
6
static 修饰变量对 Lambdas 捕获参数的影响
如果是全局变量,那么 Lambdas 无需捕获便可以直接使用:
int x = 42;
int main() {
// you don't need to capture a global variable
[] { return x; }();
}
但如果是局部变量,由于它的存储时期为 automatic,就必须捕获才能使用:
int main() {
int x = 42;
// you have to capture a local variable
[&x] { return x; }();
}
但如果使用 static 修饰该局部变量,就无需再进行捕获:
int main() {
static int x = 42;
// OK
[] { return x; }();
}
同理,const/constexpr/constinit 修饰的变量在某些时候也无需再进行捕获:
constinit int m = 42;
int main() {
constexpr int x = 42;
const int n = 42;
// OK
[] { return m + x + n; }();
}
7
static constexpr, static constinit
请大家注意我上节最后一句的用词,是「在某些时候」也无需进行捕获,使用那些词修饰并非一定是可以无需捕获。
准确地说,非 ODR-used 的数据无需捕获,它可以直接把那个常量隐式捕获。
int main() {
constexpr std::string_view x = "foo";
[] { x; }(); // OK, x is not odr-used
[] { x.data(); }(); // error: x.data() is odr-used
}
此时就可以借助 static constexpr,就可以强保证 Lambdas 可以隐式捕获该数据:
int main() {
static constexpr std::string_view x = "foo";
[] { x; }(); // OK, x is not odr-used
[] { x.data(); }; // OK, x.data() is not odr-used
}
可以理解为此时捕获的不是 lvalue,而是经由 lvalue-to-rvalue 的那个值。
static constinit 也是同理,事实上 constinit 在局部使用必须添加 static,它只能修饰静态存储期或是线程存储期的数据。
在类中使用 static constexpr 修饰数据,可以保持数据既是编译期,又能够所有对象共享一份数据。
template<int N>
struct S {
static constexpr int x = N;
};
constinit 和 constexpr 大多时候都是同理,只是前者是可读可写,后者是只读,除非有不同的意义,否则讨论的 constexpr 用法也适用于 constinit。后文不再提及。
static constexpr 的另一个用处是「强保证」发生于编译期。constexpr 本身只是「弱保证」,它并不一定发生于编译期。
它们的其他用处见第9节。
8
static const vs constexpr
前面讲过,C++ 以 const 修饰全局变量会默认为内部链接,所以 static 可以省略不写。但是局部变量不可省,因为 static 修饰局部变量时的意义是改变局部变量的存储时期,此时的 static const 必须完整写出。
全局变量本身的存储时期就是 static,加上 const 表示只读,此时以 static 修饰的意义是指定其链接方式。
局部变量本身的存储时期是 automatic,无链接,加上 const 依旧表示只读,此时以 static 修饰的意义是指定其存储时期。
所以对于全局 (static) const 数据来说,是在加载期就分配内存了(如存储时期那节所强调,不要误以为它发生在编译期)。而对于局部 static const 数据来说,它实际分配内存是在首次使用时,实际可能发生于运行期。
constexpr 修饰的变量则不同,它们发生于编译期,这其实是要早于 static const 修饰的变量。
但是经过优化,它们展现的效果是差不多的,相对来说,更推荐使用 constexpr。
int main() {
static const int m = 42; // since C++98
constexpr int n = 42; // since C++11
return m + n;
}
9
Solving the "Static Initialization Order Fiasco"
SIOF是存储时期为 static 的这类数据在跨 TUs 时相互依赖所导致的问题,因为不同 TUs 中的这些数据初始化顺序没有规定,引用的数据可能还没初始化。
因此全局变量、静态变量、静态成员变量都可能会引起这个问题。
看如下例子:
// tu-one.cpp
auto get_value(int val) -> int {
return val * 2;
}
auto tu_one_x = get_value(42);
// tu-two.cpp
#include <iostream>
extern int tu_one_x;
auto tu_two_x = tu_one_x;
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
tu_one_x 跨 TUs 使用,tu_two_x 依赖于它。在编译时初始化 tu_two_x,如果此时 tu_one_x 还未初始化,那么结果就偏离预期。
这依赖于编译顺序,你只有50%的几率获得预期结果。
解决策略一是使用局部静态变量替代全局变量。前面讲过,局部静态变量相当于全局变量,而且可以延迟初始化。
// tu-one.cpp
auto get_value(int val) -> int {
return val * 2;
}
// auto tu_one_x = get_value(42);
auto get_x() -> int& {
static auto x = get_value(42);
return x;
}
// tu-two.cpp
#include <iostream>
auto get_x() -> int&;
auto tu_two_x = get_x();
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
局部静态静态会在首次访问时初始化,因此在初始化 tu_two_x 之前,就先把 tu_one_x 初始化了。于是不会再有 SOIF:
解决策略二是借助 constinit。前面依旧讲过,constinit 发生于编译期,而存储时期为 static 的数据实际发生于加载期,SOIF 只是加载期的问题,只要将初始化时期错开,体现一前一后,就能够指定顺序,从而解决该问题。
// tu-one.cpp
constexpr auto get_value(int val) -> int {
return val * 2;
}
constinit auto tu_one_x = get_value(42);
// tu-two.cpp
#include <iostream>
extern constinit int tu_one_x;
auto tu_two_x = tu_one_x;
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
此时 tu_one_x 初始化于编译期,而 tu_two_x 初始化于加载期,所以也不会存在SOIF。
但是你无法使用 extern constexpr,像如下这样写会编译失败:
// tu-two.cpp
extern constexpr int tu_one_x; // error: not a definition
auto tu_two_x = tu_one_x;
因为 constinit 修饰的数据是可读可写的,而 constexpr 修饰的数据是只读的,定义时必须要给初值。这里这种写法被视为只是一个声明。
虽然无法使用 extern constexpr,但也是可以借助 constexpr 来解决 SOIF 的,只不过要把所有的实现全部放到头文件,然后在另一个实现文件中包含该头文件。本节最后有一个相关例子。
使用 static 修饰的变量与全局变量同理,也提供一个例子:
// tu-one.h
struct S {
static int tu_one_x; // declaration
};
// tu-one.cpp
#include "tu-one.h"
auto get_value(int val) -> int {
return val * 2;
}
// definition
int S::tu_one_x = get_value(42);
// tu-two.cpp
#include "tu-one.h"
#include <iostream>
static auto tu_two_x = S::tu_one_x;
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
它们的存储时期也是 static,所以也会产生 SOIF。tu_two_x 的初始化依赖于 S::tu_one_x,因此你也有 50% 的几率得到正确结果。
通过使用 static constinit,也得以解决此问题。
// tu-one.h
struct S {
static constinit int tu_one_x; // declaration
};
// tu-one.cpp
#include "tu-one.h"
constexpr auto get_value(int val) -> int {
return val * 2;
}
// definition
int S::tu_one_x = get_value(42);
// tu-two.cpp
#include "tu-one.h"
#include <iostream>
static auto tu_two_x = S::tu_one_x;
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
使用 constinit,所有相关操作也都得是编译期完成,所以 get_value() 也加上了 constexpr 修饰。那么 static 在此时主要指的是所修饰数据在所有对象之间共享,constinit 将它的初始化时间提前到了编译期。
但是你不能把 tu_two_x 也以 static constinit 修饰,因为编译期的值发生在链接之间,在编译期就得确定,而 tu_two_x 的值又来自于另一个文件,编译时根本就不知道所依赖的那个常量值。
同理这里也可以使用 static constexpr,但是 constexpr 没有 constinit 灵活,它是 const 的,所以定义时就必须跟着初始化。
// tu-one.h
constexpr auto get_value(int val) -> int {
return val * 2;
}
struct S {
static constexpr int tu_one_x = get_value(42); // definition
};
// tu-two.cpp
#include "tu-one.h"
#include <iostream>
static auto tu_two_x = S::tu_one_x;
int main() {
std::cout << "tu_two_x: " << tu_two_x << "\n";
}
结果和使用 static constinit 完全相同。
10
static inline
static 能够指示编译器数据只在单个 TU 使用,即内部链接;与之相反,inline 能够指示编译器数据需要在多个 TUs 使用,此时即使违背了 ODR,也不应该报错,属于外部链接。
那如果它们组合使用,会有怎样的效果?
让我们写个例子来对比一下不同的情况编译之后到底产生了什么内容。
//// This example is adapted from https://gist.github.com/htfy96/50308afc11678d2e3766a36aa60d5f75
// header.hpp
inline int only_inline() { return 42; }
static int only_static() { return 42; }
static inline int static_inline() { return 42; }
// tu-one.cpp
#include "header.hpp"
auto get_value_one() -> int {
return static_inline() + only_inline() + only_static();
}
// tu-two.cpp
#include "header.hpp"
auto get_value_one() -> int;
auto get_value_two() -> int {
return static_inline() + only_inline() + only_static();
}
auto main() -> int {
return get_value_one() + get_value_two();
}
先编译 tu-one.cpp,并查看生成的目标文件的符号表:
lkimuk@cppmore:~/Desktop/demo$ g++ -c tu-one.cpp
lkimuk@cppmore:~/Desktop/demo$ readelf -sW tu-one.o | c++filt -t
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS tu-one.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .text._Z11only_inlinev
4: 0000000000000000 11 FUNC LOCAL DEFAULT 2 only_static()
5: 000000000000000b 11 FUNC LOCAL DEFAULT 2 static_inline()
6: 0000000000000000 11 FUNC WEAK DEFAULT 6 only_inline()
7: 0000000000000016 36 FUNC GLOBAL DEFAULT 2 get_value_one()
readelf 用来查看 Linux 上可执行文件的结构,-s 表示显示符号表,-W 表示以宽格式显示。c++filt 是 gnu 提供的反 Name Demangling 工具,可以显示未经修饰的函数名称。
可以看到,only_static() 和 static_inline() 的绑定方式都是 LOCAL,表示仅在当前文件可见;only_inline() 的绑定方式为 WEAK,表示该符号可被覆盖,所以在其他文件中也是可见的;get_value_one() 的绑定方式是 GLOBAL,也表示在所有文件中可见。
再来编译 tu-two.cpp,也查看其目标文件的符号表:
lkimuk@cppmore:~/Desktop/demo$ g++ -c tu-two.cpp
lkimuk@cppmore:~/Desktop/demo$ readelf -sW tu-two.o | c++filt -t
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS tu-two.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .text._Z11only_inlinev
4: 0000000000000000 11 FUNC LOCAL DEFAULT 2 only_static()
5: 000000000000000b 11 FUNC LOCAL DEFAULT 2 static_inline()
6: 0000000000000000 11 FUNC WEAK DEFAULT 6 only_inline()
7: 0000000000000016 36 FUNC GLOBAL DEFAULT 2 get_value_two()
8: 000000000000003a 29 FUNC GLOBAL DEFAULT 2 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND get_value_one()
和 tu-two.o 的情况相似,这里不再赘述。
具体来看链接之后的情况:
lkimuk@cppmore:~/Desktop/demo$ g++ tu-one.o tu-two.o -o main
lkimuk@cppmore:~/Desktop/demo$ readelf -sW main | c++filt -t
Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34 (3)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 43 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS Scrt1.o
2: 0000000000000358 32 OBJECT LOCAL DEFAULT 3 __abi_tag
3: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
4: 0000000000001070 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
5: 00000000000010a0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
6: 00000000000010e0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
7: 0000000000004028 1 OBJECT LOCAL DEFAULT 25 completed.0
8: de0 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fini_array_entry
9: 0000000000001120 0 FUNC LOCAL DEFAULT 14 frame_dummy
10: dd8 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_entry
11: 0000000000000000 0 FILE LOCAL DEFAULT ABS tu-one.cpp
12: 0000000000001129 11 FUNC LOCAL DEFAULT 14 only_static()
13: 0000000000001134 11 FUNC LOCAL DEFAULT 14 static_inline()
14: 0000000000000000 0 FILE LOCAL DEFAULT ABS tu-two.cpp
15: 000000000000116e 11 FUNC LOCAL DEFAULT 14 only_static()
16: 0000000000001179 11 FUNC LOCAL DEFAULT 14 static_inline()
17: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
18: 00000000000021d8 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
19: 0000000000000000 0 FILE LOCAL DEFAULT ABS
20: 0000000000002004 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
21: de8 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
22: 0000000000004000 0 OBJECT LOCAL DEFAULT 23 _GLOBAL_OFFSET_TABLE_
23: 0000000000004028 0 NOTYPE GLOBAL DEFAULT 24 _edata
24: 0000000000004018 0 NOTYPE WEAK DEFAULT 24 data_start
25: 0000000000002000 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
26: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5
27: 00000000000011a8 29 FUNC GLOBAL DEFAULT 14 main
28: 0000000000004020 0 OBJECT GLOBAL HIDDEN 24 __dso_handle
29: 00000000000011c8 0 FUNC GLOBAL HIDDEN 15 _fini
30: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34
31: 0000000000001163 11 FUNC WEAK DEFAULT 14 only_inline()
32: 0000000000001040 38 FUNC GLOBAL DEFAULT 14 _start
33: 0000000000001000 0 FUNC GLOBAL HIDDEN 11 _init
34: 0000000000004028 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
35: 000000000000113f 36 FUNC GLOBAL DEFAULT 14 get_value_one()
36: 0000000000001184 36 FUNC GLOBAL DEFAULT 14 get_value_two()
37: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 24 __data_start
38: 0000000000004030 0 NOTYPE GLOBAL DEFAULT 25 _end
39: 0000000000004028 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
40: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
41: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
42: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
请注意观察,链接之后 only_static() 和 static_inline() 存在两份拷贝,说明它在每个 TU 都存在一份拷贝。而 only_inline() 只存在一份拷贝,说明 inline 的名称的确能够跨 TUs。
全局变量也只存在一份,说明外部链接是起作用的。
于是能够得出结论,static 是内部链接,inline 是外部链接,static inline 和 static 效果一样,此时加上 inline,仅仅是告诉编译器,可以尝试内联一下代码。
11
总结
本文深入全面系统地介绍了 static 关键字的方方面面,涉及内容又多又杂,又广又深。
static 是 C++ 中最复杂的关键字之一,有多达十几种不同的意思,而且涉及编译知识,许多使用形式意思非常细微。
所有相关内容几乎都包含在本文当中,具体总结大家就得自己归纳一下了。
若有未提及的用法,或是错误之处,请评论区留言。