目录标题

  • 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

为什么要“刻意”分两块?

  1. 有非零初始值的变量(比如 int x = 123;):

    • 可执行文件必须真的存储这些字节,否则程序启动时 OS 没法知道该填什么内容;
    • 所以 .data 段里放的是“真实数据”。
  2. 默认 0 或全部为 0 的变量(比如 int buf[1<<20]; 未初始化):

    • 我们只需要“启动时是全 0 的 1MB 内存”,根本没必要在文件里放 1MB 的 0x00
    • 因此 .bss 在 ELF 里只写“大小”和布局信息,不写那堆 0
    • 程序运行时由 OS 分配页、以“零页”(demand-zero)方式保证它们初值为 0。

因此“未初始化的静态变量可以节省 ELF 大小”——节省的是文件体积和加载 I/O,而不是运行时的 RAM


1.3 ELF 加载时 .data / .bss 实际发生了什么?

简化版加载过程可以理解为:

  1. 内核读取 ELF 的 Program Header:

    • 找到“可读+可写”的数据段(含 .data.bss);
  2. .data

    • 把文件里存放的那一段字节映射/拷贝到进程的虚拟地址空间;
  3. .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;
}

它涉及三个完全不同的维度:

  1. 存储期(lifetime)

    • x 是静态存储期:从程序启动到结束一直存在;
  2. 段位置(.data/.bss)

    • x 有非零常量初值 123 → 典型放在 .data
  3. 初始化时机(什么时候写入 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();(非常量表达式)

这时编译器会做两件事:

  1. 存储分配

    • y 仍然具有静态存储期;
    • 通常作为一个静态对象,放在 .bss 或类似的静态区中,初始为 0 或处于“未初始化标记状态”。
  2. 初始化逻辑延迟到“第一次执行到这行代码”
    编译器会生成类似伪代码(忽略线程安全版的简化版):

    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++:能做复杂的“动态初始化”,因此需要“首次调用时执行一次”的语义和线程安全保证。

如果你脑子里以后能自动把这三层拆开,就不会再被 .data / .bss / “第一次调用才初始化”这些表面现象绕晕了。


结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

最后,想特别推荐一下我出版的书籍——《C++编程之禅:从理论到实践》。这是对博主C++ 系列博客内容的系统整理与升华,无论你是初学者还是有经验的开发者,都能在书中找到适合自己的成长路径。从C语言基础到C++20前沿特性,从设计哲学到实际案例,内容全面且兼具深度,更加入了心理学和禅宗哲理,帮助你用更好的心态面对编程挑战。
本书目前已在京东、当当等平台发售,推荐前往“清华大学出版社京东自营官方旗舰店”选购,支持纸质与电子书双版本。希望这本书能陪伴你在C++学习和成长的路上,不断精进,探索更多可能!感谢大家一路以来的支持和关注,期待与你在书中相见。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐