【C++ 基础知识 】C++函数内static:存储期与初始化时机详解
目录标题
- 1. 静态存储期与段布局:从变量到 `.data` / `.bss`
-
- 1.1 静态存储期变量到底是什么?
- 1.2 为什么要分 `.data` 与 `.bss` 两个段?
- 1.3 ELF 加载时 `.data` / `.bss` 实际发生了什么?
- 2. `.data` vs `.bss`:为何“未初始化反而更省”?
-
- 2.1 编译器如何看待“有/无初始化”?
- 2.2 文件体积 vs 运行时内存:不同维度的“节省”对比
- 2.3 ASLR 与“地址是否固定”的小顺带
- 3. 函数内 `static`:存储期、段位置与“第一次调用初始化”
-
- 3.1 一定要分清三个层次
- 3.2 C 语言中的函数内 `static`:本质上“程序一启动就准备好”
- 3.3 C++ 中的函数内 `static`:存储期 vs 初始化时机的真正分离
-
- 3.3.1 `static int x = 123;`(常量表达式)
- 3.3.2 `static int y = bar();`(非常量表达式)
- 3.4 用表再对比一下 C / C++ 对函数内 `static` 的处理
- 3.5 函数内 `static` 与全局 / 文件内 `static` 的本质统一
- 结语

1. 静态存储期与段布局:从变量到 .data / .bss
在聊函数内 static 之前,先把它的“家底”搞清楚:所有静态存储期变量(全局变量、static 全局/局部变量)本质上都属于“静态区”,只是被编译器和链接器分流到了 .data、.bss 等不同段。理解这一层之后,很多“第一次调用才初始化”的疑惑都会自然消失——就像罗素说“清晰本身就是一种善意”,把层次分清,人也就少纠结。
1.1 静态存储期变量到底是什么?
从语言层面来看,下面这些都属于静态存储期变量:
int g1 = 42; // 全局
int g2; // 全局,未显式初始化 => 默认 0
static int sg1 = 42; // 文件内静态
static int sg2; // 文件内静态,默认 0
void foo(void) {
static int x1 = 42; // 函数内静态
static int x2; // 函数内静态,默认 0
}
共同特征:
-
生命周期:从程序启动到程序结束,一直存在;
-
存储位置:不会放在栈上,也不是按需在堆上
malloc,而是在可执行文件描述的静态区(映射到虚拟内存); -
初始化规则:
- C / C++ 都规定:未显式初始化 → 默认“零初始化”;
- 显式初始化 → 用你给的值初始化。
在“存储期”这件事上:全局、文件内 static、函数内 static 是同一类。
1.2 为什么要分 .data 与 .bss 两个段?
从链接和加载角度,静态区又被再细分为:
.data:有非零初始值、且编译期就已知的静态变量;.bss:零初始化或未显式初始化的静态变量。
用一张表先概览:
| 变量写法 | 初始语义 | 常见所在段 |
|---|---|---|
int a = 42; |
非零常量 | .data |
int b; |
默认 0 | .bss |
static int c; |
默认 0 | .bss |
static int d = 0; |
等价默认 0 | 通常 .bss |
static int e = 123; |
非零常量 | .data |
为什么要“刻意”分两块?
-
有非零初始值的变量(比如
int x = 123;):- 可执行文件必须真的存储这些字节,否则程序启动时 OS 没法知道该填什么内容;
- 所以
.data段里放的是“真实数据”。
-
默认 0 或全部为 0 的变量(比如
int buf[1<<20];未初始化):- 我们只需要“启动时是全 0 的 1MB 内存”,根本没必要在文件里放 1MB 的
0x00; - 因此
.bss在 ELF 里只写“大小”和布局信息,不写那堆 0; - 程序运行时由 OS 分配页、以“零页”(demand-zero)方式保证它们初值为 0。
- 我们只需要“启动时是全 0 的 1MB 内存”,根本没必要在文件里放 1MB 的
因此“未初始化的静态变量可以节省 ELF 大小”——节省的是文件体积和加载 I/O,而不是运行时的 RAM。
1.3 ELF 加载时 .data / .bss 实际发生了什么?
简化版加载过程可以理解为:
-
内核读取 ELF 的 Program Header:
- 找到“可读+可写”的数据段(含
.data和.bss);
- 找到“可读+可写”的数据段(含
-
对
.data:- 把文件里存放的那一段字节映射/拷贝到进程的虚拟地址空间;
-
对
.bss:- 根据“需要 N 字节”这个信息,为进程分配相应长度的匿名虚拟内存;
- 用“零页”的策略保证这些页在第一次访问时看到的是全 0(硬件页错误 + OS 分配零页)。
2. .data vs .bss:为何“未初始化反而更省”?
很多人第一次听到“未初始化的静态变量更省文件空间”都会本能困惑:我只是少打了个 = 0,为啥可执行文件能小很多?这里其实包含了几个层面的优化理念——有点像心理学里说的“延迟满足”:并不是所有东西都要立刻在文件里固化,能拖到运行时再生成就不要提前占资源。
2.1 编译器如何看待“有/无初始化”?
从编译器视角:
-
如果初始化是编译期常量,且不是全 0:
- 编译器会生成对应字节,放入
.data;
- 编译器会生成对应字节,放入
-
如果初始化是“没有写”或者“写的是 0”:
- 编译器只需要告诉链接器:
“这里有 N 字节,启动时请帮我保证它是 0。”
- 编译器只需要告诉链接器:
举个极端例子:
// 两个 1MB 的全局数组
int a[1024 * 1024]; // 未初始化,默认 0
int b[1024 * 1024] = {1}; // 第一个元素为 1,后面很多 0
a:完全可以放.bss,ELF 里只记“大小 = 4MB”;b:第一个元素是1,整个段不能简单看作“全 0”,通常会进.data,
哪怕后面大部分仍然是 0,也需要把完整字节序列写进文件(或至少写入一段含非零数据)。
2.2 文件体积 vs 运行时内存:不同维度的“节省”对比
用表格看会更直观:
| 维度 | .data(已初始化) |
.bss(零初始化 / 未初始化) |
|---|---|---|
| ELF 中是否存字节 | ✅ 是,存放真实初始值 | ❌ 不存 0,只存“大小+布局” |
| ELF 文件体积影响 | 与变量总大小成正比增长 | 几乎只加一点元数据,大小基本不变 |
| 加载 I/O 开销 | 需要从磁盘读取这部分数据 | 不用读“0 字节”,只需建立映射 |
| 运行时占用内存 | 都要占用(内存页大小取决于段总大小) | 都要占用(只是页初次映射时自动为 0) |
所以当你写:
static int huge[10 * 1024 * 1024]; // 40MB
- 如果没显式初始化 →
.bss,不会让你的 ELF 立刻胖 40MB; - 如果写了一个复杂的非零初始值数组,理论上 ELF 就得把这些内容全带上。
2.3 ASLR 与“地址是否固定”的小顺带
- 全局 / 静态变量的地址在 ELF 里是相对可执行文件布局固定的偏移;
- 但运行时,进程的加载基址会因为 ASLR(地址空间布局随机化) 而变化;
- 所以每次运行
&g的虚拟地址可能不同,尽管“在段中的偏移”是固定的。
这点有助于你理解一个核心事实:
.data/.bss决定的是“它在静态区的布局方式和初始内容”,
不直接等价于“运行时虚拟地址是否固定”。
3. 函数内 static:存储期、段位置与“第一次调用初始化”
“函数中的静态变量,理论上第一次调用函数才会用到,可是我们又说编译后已经决定在 .data/.bss 了,这不矛盾吗?”
其实一点不矛盾——关键是分清**“这块内存什么时候存在”和“初始化代码什么时候执行”**,这有点像卡尼曼说的 System 1 / System 2:存储布局是“慢系统”的一次性设计,而初始化时机则是“快系统”在运行时做出的某次决定。
3.1 一定要分清三个层次
以这个例子为主角:
void f(void) {
static int x = 123;
}
它涉及三个完全不同的维度:
-
存储期(lifetime)
x是静态存储期:从程序启动到结束一直存在;
-
段位置(.data/.bss)
x有非零常量初值123→ 典型放在.data;
-
初始化时机(什么时候写入 123)
- C:通常在启动时就完成(静态初始化);
- C++:如果是常量表达式,也可以在静态阶段完成;
如果是非常量表达式,则在第一次执行到这行代码时初始化。
这三件事不能混为一谈。
3.2 C 语言中的函数内 static:本质上“程序一启动就准备好”
在 C 中,函数内 static 的初始化表达式必须是编译期常量:
void f(void) {
static int x = 123; // OK
// static int y = rand(); // ❌ rand() 不是常量表达式
}
所以:
- 编译器知道
x初始值就是 123; - 会把
x对应的 4 字节写进.data; - 程序加载时
.data已经被映射/填好,x一开始就是 123;
语义上可以说“它只初始化一次”,
但实现上几乎总是“随 .data 在进程启动时一次性初始化完毕”,和“第一次调用”和没什么必然关系。
3.3 C++ 中的函数内 static:存储期 vs 初始化时机的真正分离
C++ 把局部 static “玩得更花”——允许非常量表达式初始化:
int bar();
void f() {
static int x = 123; // 常量表达式初始化
static int y = bar(); // 动态初始化(需要在运行时调用 bar())
}
这里需要分两种情况:
3.3.1 static int x = 123;(常量表达式)
- 和 C 类似:编译器能在编译期算出初值;
- 放
.data,启动时已是 123; - C++ 标准把这种叫作“constant initialization”,通常在静态初始化阶段完成。
3.3.2 static int y = bar();(非常量表达式)
这时编译器会做两件事:
-
存储分配
y仍然具有静态存储期;- 通常作为一个静态对象,放在
.bss或类似的静态区中,初始为 0 或处于“未初始化标记状态”。
-
初始化逻辑延迟到“第一次执行到这行代码”
编译器会生成类似伪代码(忽略线程安全版的简化版):void f() { static bool y_inited = false; // 也在静态区 static int y; // 存在 .bss / 静态区 if (!y_inited) { y = bar(); // 真正的初始化动作(只执行一次) y_inited = true; } // 使用 y ... }
关键点:
- 内存块本身(y 的那块静态空间)从程序一开始就存在;
- 真正“写入数据”的动作(调用
bar()并给y赋值)被延迟到第一次执行f()里那行代码的时候; - 段布局(在
.bss还是.data)是在链接时就确定了,不会在第一次调用时动态改变。
3.4 用表再对比一下 C / C++ 对函数内 static 的处理
| 语言 / 情况 | 段位置(.data/.bss) | 内存存在时间 | 初始化代码何时执行 |
|---|---|---|---|
C:static int x = 123; |
.data(非零常量) |
程序启动到结束 | 启动时随 .data 一起初始化 |
C:static int x; / = 0 |
.bss |
程序启动到结束 | 启动时保证为 0(零页 / 清零) |
C++:static int x = 123; |
通常 .data |
程序启动到结束 | 静态初始化阶段(与 C 类似) |
C++:static int y = bar(); |
y 在 .bss 或静态区 |
程序启动到结束 | 第一次执行到这行代码时(动态初始化 + 只一次) |
所以可以归纳为一句话:
“第一次调用才初始化”指的是“初始化表达式那段代码的执行时机”,
而不是“那时候才分配内存”或“那时候才决定放入.data/.bss”。
3.5 函数内 static 与全局 / 文件内 static 的本质统一
最后再把视角拉高一点:
-
存储期:都是静态存储期;
-
段位置:由“初始值是否为编译期已知非零常量”决定;
-
语言语义:
- C:函数内
static在初始化能力上比较弱(必须常量表达式); - C++:能做复杂的“动态初始化”,因此需要“首次调用时执行一次”的语义和线程安全保证。
- C:函数内
如果你脑子里以后能自动把这三层拆开,就不会再被 .data / .bss / “第一次调用才初始化”这些表面现象绕晕了。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。
最后,想特别推荐一下我出版的书籍——《C++编程之禅:从理论到实践》。这是对博主C++ 系列博客内容的系统整理与升华,无论你是初学者还是有经验的开发者,都能在书中找到适合自己的成长路径。从C语言基础到C++20前沿特性,从设计哲学到实际案例,内容全面且兼具深度,更加入了心理学和禅宗哲理,帮助你用更好的心态面对编程挑战。
本书目前已在京东、当当等平台发售,推荐前往“清华大学出版社京东自营官方旗舰店”选购,支持纸质与电子书双版本。希望这本书能陪伴你在C++学习和成长的路上,不断精进,探索更多可能!感谢大家一路以来的支持和关注,期待与你在书中相见。
阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
更多推荐


所有评论(0)