【C语言】内存布局

—— 堆、栈

🧠 简介: 在C语言中,内存布局是理解程序行为的关键。 它决定了变量如何存储、函数如何调用、内存如何分配与释放。

1. 内存布局概述

当一个C程序加载到内存后,操作系统会为其划分不同区域,每个区域负责特定的任务:

内存区域描述
代码段 (Text Segment)存放编译后的机器指令,只读、可共享
已初始化数据段 (Data Segment)保存初始化的全局变量与静态变量
未初始化数据段 (BSS Segment)存放未初始化的全局/静态变量,系统自动清零
堆 (Heap)动态内存分配区域,需手动管理
栈 (Stack)存储函数调用、局部变量,自动回收
📘 提示:从内存地址上看,栈区通常在高地址向下增长,而堆区则在低地址向上增长

2. 代码段 (Text Segment)

代码段(Text Segment)是程序中用于存放可执行指令的区域。 当我们编写的C源代码经过编译和链接后,函数体、控制语句等被转换为机器码,这些指令被集中放入代码段中。 在程序运行时,CPU会从该区域中逐条读取并执行指令。

代码段通常具有以下几个特性:

例如,下面的代码中,函数 hello()main() 的机器指令都位于代码段中:

#include <stdio.h>
void hello() {
    printf("Hello, World!\\n");
}
int main() {
    hello();
    return 0;
}

你可以通过 objdump -hreadelf -S 命令查看可执行文件中 .text 段的大小与起始地址,例如:


gcc code_demo.c -o code_demo
readelf -S code_demo | grep .text

输出结果中 .text 段即对应代码段区域。 该区域只包含函数指令本身,不包含任何变量数据。

💡 提示: 若程序错误地尝试修改代码段内容(如通过函数指针写入指令区),系统会触发段错误(Segmentation Fault)。

3. 已初始化数据段 (Data Segment)

已初始化数据段(Data Segment)用于存放所有在程序开始前已被显式赋初值的全局变量与静态变量。 该区域的数据在程序启动时从可执行文件加载到内存中,并保持其初始值不变,直到程序结束或被修改。

特点如下:

示例程序:

#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)。

4. 未初始化数据段 (BSS)

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调用时,变量ab被压入栈;当func1返回后,栈帧释放但数据未清除。 func2调用时,其局部变量xy恰好占用了func1中ba的栈空间,因此读取到了遗留值。

⚠️ 注意: 这种“读取栈遗留数据”的行为属于未定义行为(Undefined Behavior)——编译器不保证其一致性,不同编译选项(如优化等级)、不同系统或不同调用顺序都可能导致结果完全不同。永远不要依赖未初始化局部变量的值!
📘 小结:
✅ BSS 段存放未初始化的全局或静态变量;
✅ 程序加载时由系统清零,确保安全初始值;
✅ 不占用可执行文件磁盘空间,只占运行时内存;
✅ 局部变量若未初始化,则存放在栈中,其内容与栈的历史使用记录相关(如之前函数的遗留数据),属于未定义值;
✅ 最佳实践:始终显式初始化局部变量(如int x = 0;),避免依赖未定义行为。

5. 堆 (Heap)

堆区(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; // 避免野指针
}
💡 提示:堆内存不能自动回收,每次 malloccalloc 都应对应 free,否则长时间运行的程序可能出现 内存泄漏

6. 栈 (Stack)

栈区(Stack)由操作系统自动管理,通常用于存储函数调用帧、局部变量和返回地址。 当函数调用时,会为该函数分配栈帧;函数返回时,栈帧自动销毁。

栈区特点:

#include <stdio.h>

void func() {
    int local = 10; // 局部变量存放于栈中
    printf("Local variable: %d\\n", local);
}

int main() {
    func();
}
💡 注意:栈变量的生命周期仅限于函数执行期间,函数返回后变量会被释放,指向栈内存的指针若在函数外使用,会成为野指针。

7. 内存布局综合示例

下面的示例展示了 代码段、数据段、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初始化全局/静态变量
BSS段global_uninit未初始化变量,初始为0
heap动态内存,手动分配/释放
local局部变量与函数帧,自动管理

8. 嵌入式中的内存布局

嵌入式系统中通常无操作系统内存管理,开发者需自行定义段布局:

#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);
}

9. 内存管理进阶

9.1 内存泄漏检测

可使用 ValgrindAddressSanitizer 进行动态内存检测。

#include <stdlib.h>
void leak() {
    int *p = malloc(10 * sizeof(int)); // 未释放
}
int main() { leak(); }

9.2 简易内存池

#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; }

9.3 智能指针(C++)

#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] << " ";
}