🧠 简介: 在C语言中,内存布局是理解程序行为的关键。 它决定了变量如何存储、函数如何调用、内存如何分配与释放。
当一个C程序加载到内存后,操作系统会为其划分不同区域,每个区域负责特定的任务:
| 内存区域 | 描述 |
|---|---|
| 代码段 (Text Segment) | 存放编译后的机器指令,只读、可共享 |
| 已初始化数据段 (Data Segment) | 保存初始化的全局变量与静态变量 |
| 未初始化数据段 (BSS Segment) | 存放未初始化的全局/静态变量,系统自动清零 |
| 堆 (Heap) | 动态内存分配区域,需手动管理 |
| 栈 (Stack) | 存储函数调用、局部变量,自动回收 |
📘 提示:从内存地址上看,栈区通常在高地址向下增长,而堆区则在低地址向上增长。
代码段(Text Segment)是程序中用于存放可执行指令的区域。 当我们编写的C源代码经过编译和链接后,函数体、控制语句等被转换为机器码,这些指令被集中放入代码段中。 在程序运行时,CPU会从该区域中逐条读取并执行指令。
代码段通常具有以下几个特性:
例如,下面的代码中,函数 hello() 和 main() 的机器指令都位于代码段中:
#include <stdio.h>
void hello() {
printf("Hello, World!\\n");
}
int main() {
hello();
return 0;
}
你可以通过 objdump -h 或 readelf -S 命令查看可执行文件中 .text 段的大小与起始地址,例如:
gcc code_demo.c -o code_demo
readelf -S code_demo | grep .text
输出结果中 .text 段即对应代码段区域。
该区域只包含函数指令本身,不包含任何变量数据。
💡 提示: 若程序错误地尝试修改代码段内容(如通过函数指针写入指令区),系统会触发段错误(Segmentation Fault)。
已初始化数据段(Data Segment)用于存放所有在程序开始前已被显式赋初值的全局变量与静态变量。 该区域的数据在程序启动时从可执行文件加载到内存中,并保持其初始值不变,直到程序结束或被修改。
特点如下:
main() 函数执行前,操作系统的加载器(Loader)就已将这些变量加载到内存中;.data 节。示例程序:
#include <stdio.h>
int global_var = 42; // 已初始化全局变量,位于 .data 段
int main() {
static int static_var = 99; // 已初始化静态变量,位于 .data 段
printf("Global: %d, Static: %d\\n", global_var, static_var);
return 0;
}
编译后,你可以通过命令观察 .data 段内容:
readelf -S a.out | grep .data
输出示例:
[12] .data PROGBITS 0000000000601040 0x1040 000010 WA 0 0 8
这里的 WA 标志表示该段是 可写 (Writeable) 和 已分配 (Allocated) 的。
注意,若你将变量初始化为 0,它将不会进入数据段,而会被编译器优化进 BSS 段。
📘 小结:
✅ 已初始化的全局变量与静态变量 → 存放于 Data 段;
✅ 未初始化的(或初始化为 0)变量 → 存放于 BSS 段;
✅ 局部变量 → 存放于栈(Stack);
✅ 动态分配变量(malloc/new) → 存放于堆(Heap)。
BSS(Block Started by Symbol)段用于存放未显式初始化的全局变量和静态变量。 在程序加载到内存时,操作系统会自动将该区域清零,确保所有变量在程序开始运行前处于确定的初始状态(即值为0)。 这意味着,无论你是否显式地写出“=0”,效果都是一样的。
BSS 段的一个显著特性是:它不占用可执行文件的实际存储空间。 因为未初始化变量的内容全为零,编译器只在可执行文件的符号表中记录其名称与大小,而不会真的为其分配文件中的空间。 加载程序时,操作系统根据这些符号信息动态分配内存并初始化为 0。
#include <stdio.h>
int uninit_global; // 存放于 BSS 段
int global_init = 5; // 存放于 Data 段
int main() {
static int uninit_static; // 也位于 BSS 段
static int init_static = 10; // 位于 Data 段
printf("Uninit Global: %d, Uninit Static: %d\\n",
uninit_global, uninit_static);
printf("Init Global: %d, Init Static: %d\\n",
global_init, init_static);
return 0;
}
可以通过以下命令观察可执行文件中各段的分布情况(以 命令行 为例):
gcc bss_demo.c -o bss_demo
size bss_demo
输出示例:
text data bss dec hex filename
1024 32 12 1068 42c bss_demo
从结果可见,data 段保存初始化数据,而 bss 段记录未初始化变量的总大小。
局部变量(非静态)存储在栈(Stack)中,而栈的特性是“重复利用内存空间”——当函数调用结束后,其栈帧(包含局部变量、参数等)会被释放,但内存中的数据不会被主动清除。因此,未初始化的局部变量的值,本质上与栈的“历史使用记录”相关,具体取决于以下因素:
下面的代码可以直观展示这一特性:
#include <stdio.h>
// 第一个函数:在栈上写入数据
void func1() {
int a = 100; // 栈上分配的局部变量
int b = 200; // 栈上另一个局部变量
printf("func1 中:a=%d, b=%d(栈上写入数据)\n", a, b);
}
// 第二个函数:读取未初始化局部变量(栈上遗留数据)
void func2() {
int x; // 未初始化局部变量(栈上)
int y; // 未初始化局部变量(栈上)
printf("func2 中:x=%d, y=%d(栈上遗留数据)\n", x, y);
}
int main() {
// 首次调用func2:栈上无历史数据,值为随机垃圾
printf("=== 首次调用func2 ===\n");
func2();
// 调用func1:在栈上写入数据
printf("\n=== 调用func1 ===\n");
func1();
// 再次调用func2:可能读取到func1遗留的栈数据
printf("\n=== 再次调用func2 ===\n");
func2();
return 0;
}
在多数编译器(如GCC)下的输出可能类似:
=== 首次调用func2 ===
func2 中:x=32767, y=-12345(栈上无历史数据,值随机)
=== 调用func1 ===
func1 中:a=100, b=200(栈上写入数据)
=== 再次调用func2 ===
func2 中:x=200, y=100(读取到func1遗留的栈数据)
原因解析:
func1调用时,变量a和b被压入栈;当func1返回后,栈帧释放但数据未清除。
func2调用时,其局部变量x和y恰好占用了func1中b和a的栈空间,因此读取到了遗留值。
⚠️ 注意: 这种“读取栈遗留数据”的行为属于未定义行为(Undefined Behavior)——编译器不保证其一致性,不同编译选项(如优化等级)、不同系统或不同调用顺序都可能导致结果完全不同。永远不要依赖未初始化局部变量的值!
📘 小结:
✅ BSS 段存放未初始化的全局或静态变量;
✅ 程序加载时由系统清零,确保安全初始值;
✅ 不占用可执行文件磁盘空间,只占运行时内存;
✅ 局部变量若未初始化,则存放在栈中,其内容与栈的历史使用记录相关(如之前函数的遗留数据),属于未定义值;
✅ 最佳实践:始终显式初始化局部变量(如int x = 0;),避免依赖未定义行为。
堆区(Heap)是用于动态内存分配的区域,其大小仅受系统可用内存限制。
在C语言中,堆内存通过 malloc()、calloc()、realloc() 等函数进行分配,并且必须使用 free() 手动释放,否则会导致内存泄漏。
堆区的特点:
示例程序:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = malloc(sizeof(int) * 10); // 动态分配10个int空间
if(ptr == NULL) { // 分配失败检查
fprintf(stderr, "Memory allocation failed\\n");
return 1;
}
for (int i = 0; i < 10; i++) ptr[i] = i * 10; // 初始化堆数组
for (int i = 0; i < 10; i++) printf("%d ", ptr[i]);
printf("\\n");
free(ptr); // 释放堆内存
ptr = NULL; // 避免野指针
}
💡 提示:堆内存不能自动回收,每次malloc或calloc都应对应free,否则长时间运行的程序可能出现 内存泄漏。
栈区(Stack)由操作系统自动管理,通常用于存储函数调用帧、局部变量和返回地址。 当函数调用时,会为该函数分配栈帧;函数返回时,栈帧自动销毁。
栈区特点:
#include <stdio.h>
void func() {
int local = 10; // 局部变量存放于栈中
printf("Local variable: %d\\n", local);
}
int main() {
func();
}
💡 注意:栈变量的生命周期仅限于函数执行期间,函数返回后变量会被释放,指向栈内存的指针若在函数外使用,会成为野指针。
下面的示例展示了 代码段、数据段、BSS、堆、栈 的综合使用和内存地址观察方法:
#include <stdio.h>
#include <stdlib.h>
int global_init = 100; // 已初始化全局变量,位于数据段
int global_uninit; // 未初始化全局变量,位于BSS段
void func() {
static int static_var = 200; // 静态局部变量,位于数据段
int local = 10; // 局部变量,位于栈
int *heap = malloc(sizeof(int)); // 动态分配,位于堆
*heap = 300;
printf("Static: %d @%p\\n", static_var, &static_var);
printf("Local: %d @%p\\n", local, &local);
printf("Heap: %d @%p\\n", *heap, heap);
free(heap); // 释放堆内存
}
int main() {
printf("Global Init: %d @%p\\n", global_init, &global_init);
printf("Global Uninit: %d @%p\\n", global_uninit, &global_uninit);
func();
}
从输出中可以观察到:
global_init 和静态变量 static_var 地址一般在低地址区域;global_uninit 地址紧随数据段;heap 地址在较低的高地址区域,由 malloc 管理;local 地址在高地址区域,随函数调用压入栈顶。| 内存区域 | 示例变量 | 说明 |
|---|---|---|
| 代码段 | 函数代码 | 只读,共享 |
| 数据段 | global_init, static_var | 初始化全局/静态变量 |
| BSS段 | global_uninit | 未初始化变量,初始为0 |
| 堆 | heap | 动态内存,手动分配/释放 |
| 栈 | local | 局部变量与函数帧,自动管理 |
嵌入式系统中通常无操作系统内存管理,开发者需自行定义段布局:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
volatile unsigned int CONTROL;
volatile unsigned int STATUS;
volatile unsigned int DATA;
} UART_Reg;
int main() {
UART_Reg *UART1 = malloc(sizeof(UART_Reg));
UART1->CONTROL = 0x01;
UART1->STATUS = 0x00;
UART1->DATA = 0x55;
printf("UART1 DATA: 0x%X\\n", UART1->DATA);
free(UART1);
}
可使用 Valgrind 或 AddressSanitizer 进行动态内存检测。
#include <stdlib.h>
void leak() {
int *p = malloc(10 * sizeof(int)); // 未释放
}
int main() { leak(); }
#define POOL_SIZE 1024
typedef struct { char pool[POOL_SIZE]; size_t offset; } MemoryPool;
void pool_init(MemoryPool *mp){ mp->offset = 0; }
void* pool_alloc(MemoryPool *mp, size_t sz){
if(mp->offset + sz > POOL_SIZE) return NULL;
void* p = mp->pool + mp->offset; mp->offset += sz; return p;
}
void pool_free(MemoryPool *mp){ mp->offset = 0; }
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int[]> arr(new int[10]);
for(int i=0;i<10;i++) arr[i]=i;
for(int i=0;i<10;i++) std::cout << arr[i] << " ";
}