外观
【科普】为什么C语言中这些全局变量与静态变量默认值是0?
约 2348 字大约 8 分钟
2025-11-12
大家好,我是李述铜,一名专注于嵌入式系统与底层开发的工程师,我的主要工作是制作课程带大家从零手写操作系统、TCP/IP协议栈、文件系统等核心系统,从实现的视角理解计算机底层原理。
这篇文章继续写C语言相关的话题,内容主要涉及全局变量和静态变量的初始值从何而来?
我们知道,对于C程序中的全局变量和静态变量,如果不给定初始化值,则其默认值是0。
那么,这个默认值设置为0的工作,究竟是谁来完成的?是编译器吗,还是操作系统,抑或是程序本身?
而如果程序中有很多这种变量,他们是逐个被初始化为0,还是通过其他方法来实现?
在这篇文章中,我将以ARM和RISC-V上的程序代码为例,介绍其中的相关原理。
现象示例:一段简单的程序
为了方便介绍后续内容,这里给出一段简单的C代码,具体如下:
#include <stdio.h>
int g; // 全局变量
int main() {
static int s; // 静态变量
printf("%d %d\n", g, s);
s = 1;
return 0;
}在上述代码中,即使我们没有初始化变量g和s,程序总是输出0 0。
这段程序,无论是芯片上电复位,还是仅按下芯片的复位管脚,其输出总是不变的。
特别芯片被复位后,整个系统并未重新上电,s所处的存储空间最开始依然保留上次程序的赋值,即使程序重启运行后,打印的结果依然是0 0.
这就意味着:
在main()函数运行之前,一定有某种机制对g和s对应的存储空间进行了清0操作。
这背后倒底发生了什么?
为了讲清楚这个问题,我们必须先了解C程序的存储结构。
02 C程序是怎么被存储的?
2.1 程序被划分为不同区域
我们编译一个 C 程序,最终会变成可执行文件(ELF/HEX/BIN)。 其内部数据会被划分为多个区域(Section):
| 区域 | 内容 | 举例 |
|---|---|---|
.text | 可执行机器代码 | 函数 |
.rodata | 只读常量 | const char[]、字符串 |
.data | 已初始化的全局/静态变量 | int a = 10; |
.bss | 未初始化的全局/静态变量 | int a; |
| stack | 局部变量、调用栈 | 函数内部变量 |
| heap | 动态分配空间 | malloc |
本问题主角就是 .data 与 .bss。
03 .data vs .bss 的区别是什么?
我们看下面几类变量:
int a = 19; // 有初值 → .data
static int b = 5; // 有初值 → .data
int c; // 无初值 → .bss
static int d; // 无初值 → .bss
static int e = 0; // 初值为 0 → .bss✅ 所有没有显式初始化,或初始化为 0 的全局/静态变量都进入 .bss
✅ 有非零初值,进入 .data
✅ .data
- 保存已经显式初始化的变量
- 需要在程序中放入初始值
- 运行前必须将这些初始值从 Flash 拷贝到 RAM
✅ .bss
- 保存未初始化或初始化为 0 的变量
- 不需要在可执行文件中存储实际数据
- 只需要知道长度
- 运行时统一清零即可
也就是说,.bss 占用 RAM,但节约 Flash,因为无需存储初值。
04 C 语言为什么要求默认初始化为 0?
根据 C 标准:
未初始化的全局变量与静态变量,必须默认为 0。
这是语言层面的要求。 为什么要这样设计?
✅ 原因1:保证可预测性
全局/静态变量通常用于长期存储; 随机值可能引起不可控行为,因此标准要求初始化为 0。
✅ 原因2:方便编译器与启动代码优化
统一默认值,在启动阶段很好处理,只需清零 .bss 区。
05 真相:谁帮它们变成 0 的?
许多人以为是 “编译器” 帮我们初始化。 其实,变量是在程序启动时由**启动代码(startup code)**完成的。
流程是:
编译 → 链接 → 下载 → 启动(清零/拷贝)→ main()
✅ 5.1 程序烧录到 Flash
最终生成的 Flash 中数据大致如下:
| Section | 是否存储在 Flash |
|---|---|
| .text | ✅ |
| .rodata | ✅ |
| .data | ✅(包含初值) |
| .bss | ❌(不存初值,只记录大小) |
✅ 5.2 MCU 上电 → 执行启动代码
MCU 启动后执行 Startup(也叫 crt0),它会做两件事:
✅ (1) 拷贝 .data 到 RAM
Flash → RAM 因为 .data 需要带初值。
✅ (2) 清零 .bss
Startup 会做类似:
memset(bss_start, 0, bss_size);因此 .bss 中的所有变量自然变为 0。
✅ 所以:不是编译器自动赋值,而是启动代码在运行时统一清 0
✅ 5.3 调用 main()
完成 .data/.bss 初始化后,才进入 main()。 因此我们在 main() 看到的全局/静态变量已经是正确初值了。
这就是为什么它们 “自动是 0”。
06 为什么 .bss 单独管理可以节省空间?
如果程序中未初始化的变量叫:
static int buffer[1024];如果把 1024 * 4 = 4KB 数据全部写入 Flash 作为初值,那岂不是浪费?
.bss 区只需要:
只记录大小,不存储实际内容
Startup 时统一清零即可。
这节约了: ✅ Flash 空间 ✅ 二进制文件体积
程序启动时,也只需用 memset 统一清零即可完成。
07 对比一下局部变量
局部变量位于 栈(stack):
void foo() {
int a;
printf("%d\n", a); // 随机值
}栈空间不保证为 0,因此局部变量不做默认初始化。
这与全局/静态变量的“默认初始化”形成区别。
08 小总结
| 类型 | 是否默认初始化 | 默认值 | 存放位置 |
|---|---|---|---|
| 全局变量 | ✅ | 0 | .data / .bss |
| 静态变量 | ✅ | 0 | .data / .bss |
| 局部变量 | ❌ | 不确定 | stack |
✅
.data= 已初始化 ✅.bss= 未初始化或初值为 0
✅ 清零由 启动代码 完成 ✅
.bss不占 Flash,节省空间
09 想更深地玩:深入 Section、ELF、Linker Script
搞清 .data/.bss 只是理解编译器和 ARM 系统运行机制的第一步:
如果你:
- 想理解 ELF 是如何组织的?
- 想修改链接脚本自定义内存分布?
- 想知道优化
.bss/.data带来的性能收益? - 想理解 C 程序从源码 → 二进制 → RAM 的完整过程?
- 想学习 ARM 架构、编译器与运行机制?
那么我诚挚推荐你学习:
《ARM体系结构课程 —— 编译器使用指南》
在课程中,你会学到:
✅ ELF 文件结构 ✅ Section / Segment ✅ .data/.bss/.text 深度讲解 ✅ 链接脚本 Linker Script ✅ ARM 启动流程 boot & crt0 ✅ 变量是怎样映射到物理地址 ✅ 裸机启动中如何初始化 RAM ✅ 编译器生成的数据布局规律
不仅讲原理,还带你:
- 实操编译
- 观察 ELF
- 解析 Section
- 自己写 Linker Script
- 分析启动代码
从“会写 C”迈向“真正理解程序运行”。
10 结语
当你读懂 .data、.bss 和启动初始化流程时,你会发现:
编程语言背后,是一整套系统工程。
这也是为什么:
- 全局/静态变量默认是 0
- 为什么
.bss不占 Flash - 为什么局部变量不会自动初始化
这些知识会提升你对 C 程序、本地存储布局、嵌入式系统启动流程的理解,让你在编写底层代码、诊断问题、优化性能时得心应手。
如果你希望进一步深入 欢迎加入—— 《ARM体系结构课程 —— 编译器使用指南》 带你理解程序世界的底层逻辑。
如需,我还可以为本篇文章: ✅ 完善排版(带 emoji) ✅ 生成标题/封面文案 ✅ 制作 PPT 大纲 ✅ 做短视频脚本
欢迎继续交流!
历史文章
- 作为嵌入式开发者,有必要手写一个RTOS吗
- 别再只用malloc了!嵌入式C的栈上动态数组分配:变长数组
- 告别固定大小:利用C语言伸缩型数组,实现动态结构体
- 写给嵌入式C程序员:我们为什么终于不用自己定义UINT8了
- C结构体的初始化你还在按顺序写?试试这个C99神操作!
- C语言居然也有布尔类型?!
- 介绍一个C语言编程技巧:处理超长字符串显示
课程推荐
作者介绍
李述铜,嵌入式系统与底层架构领域讲师,专注于操作系统、CPU 架构的教学与研究。 出版作品《从0手写x86计算机操作系统》。主讲课程包括:《从0手写嵌入式操作系统》《从0手写TCP/IP协议栈》等。
欢迎关注我的微信公众号【李述铜的底层修炼】,以便及时获取我的更多文章!-> lishutong1024.cn
