C程序的内存到底怎么分的?栈、堆、全局区一张图讲清楚

C程序的内存到底怎么分的?栈、堆、全局区一张图讲清楚

你有没有想过一个问题:你写的每一个变量,到底存在内存的哪个位置?

int a = 10; 存在哪?malloc 分配的内存存在哪?static int count; 又存在哪?

很多人写了好几年 C 代码,对这些还是一笔糊涂账。结果就是:栈溢出了不知道为什么,内存泄漏了找不到在哪,全局变量改了那个文件出了 bug 更想不通。

今天把 C 程序的内存布局彻底拆清楚。看完这篇,以后遇到内存问题,你至少知道该往哪个方向查。


C程序内存的5大区

一个 C 程序运行时,内存被分成 5 个区域。从低地址到高地址依次是:

arduino 复制代码
┌─────────────────┐ 高地址
│     栈区 Stack   │  ↓ 向下增长
│       ...        │
│       空闲       │
│       ...        │
│     堆区 Heap    │  ↑ 向上增长
│  .bss(未初始化) │
│  .data(已初始化)│
│  代码段 .text     │
└─────────────────┘ 低地址

每张区域存什么?一句话总结:

区域 存什么 谁管理 生命周期
代码段 机器指令 系统 程序运行期间
全局区(.data/.bss) 全局变量、static变量、字符串常量 系统 程序运行期间
堆区 malloc/calloc 分配的内存 程序员手动 手动 free 前
栈区 局部变量、函数参数 系统自动 函数调用期间
(栈帧之间) 函数返回地址、寄存器保存 系统自动 函数调用期间

💡 最关键的区别 :栈和全局区是系统自动管理,堆是你自己管理。C 语言不帮你擦屁股,忘了 free 就是泄漏。


栈区------最快也最小的内存

栈的特点:自动分配、自动回收、速度极快、但空间很小。

c 复制代码
void func(int a) {
    int b = 10;        // 栈
    char buf[1024];    // 栈
    double arr[100];   // 栈
}  // 函数结束,a、b、buf、arr 全部自动释放

栈的空间有多大?

  • Linux :默认 8MB(ulimit -s 查看)
  • Windows:默认 1MB

听起来不大,但对大多数函数来说足够了。问题出在两种情况:

情况一:局部数组太大

c 复制代码
// ❌ 危险:大数组直接撑爆栈
void bad_function() {
    int arr[1000000];  // 4MB,在 Windows 上直接栈溢出
}

正确做法:大数组放堆上

c 复制代码
// ✅ 大内存分配用 malloc
void good_function() {
    int *arr = (int *)malloc(1000000 * sizeof(int));
    if (arr == NULL) return;
    // 使用 arr...
    free(arr);
}

情况二:递归太深

c 复制代码
// ❌ 无限递归 → 栈溢出
void infinite() {
    int temp[100];  // 每次调用都占 400 字节栈空间
    infinite();      // 永远不返回,栈指针一直下移
}

💡 记住:栈空间是固定的,不会动态扩展。局部变量小、函数调用浅 → 栈够用;大数组、深递归 → 换堆。


堆区------你自己管的大仓库

堆的特点:手动分配、手动释放、空间很大、但有碎片风险。

c 复制代码
// 分配
int *p = (int *)malloc(100 * sizeof(int));  // 400 字节
if (p == NULL) {
    perror("分配失败");
    return;
}

// 使用
for (int i = 0; i < 100; i++) {
    p[i] = i;
}

// 释放------忘了就是泄漏
free(p);
p = NULL;  // 好习惯:释放后置空

malloc vs calloc vs realloc

c 复制代码
// malloc:分配,内容不初始化(随机值)
int *a = (int *)malloc(10 * sizeof(int));

// calloc:分配 + 清零
int *b = (int *)calloc(10, sizeof(int));  // 10个int,全部为0

// realloc:调整大小(可能搬移数据)
int *c = (int *)realloc(b, 20 * sizeof(int));  // 扩到20个int
// ⚠️ realloc 可能失败,失败时原指针 b 仍有效
// 安全写法:
int *temp = (int *)realloc(b, 20 * sizeof(int));
if (temp != NULL) {
    b = temp;  // 成功后才更新指针
} else {
    // 失败处理,b 还能用
}

💡 堆的铁律

  1. 每次 malloc 必须检查 NULL
  2. 每次 malloc 必须对应一次 free
  3. free 之后立刻置 NULL
  4. 不要 free 两次(double free = 崩溃)

全局区------static 和全局变量的家

全局区分为 .data(已初始化)和 .bss(未初始化),程序运行期间一直存在。

c 复制代码
int g1 = 100;           // .data 段(已初始化)
int g2;                 // .bss 段(未初始化,系统自动清零为0)
static int g3 = 200;    // .data 段,但只在本文件可见
const char *str = "hello";  // str 在 .data,"hello" 在只读数据段

void counter() {
    static int count = 0;  // .data 段,只初始化一次!
    count++;
    printf("第 %d 次调用\n", count);
}

static 局部变量的"记忆"效果

c 复制代码
void test() {
    int a = 0;           // 每次调用都重新初始化为 0
    static int b = 0;    // 只初始化一次,之后保留上次的值
    a++;
    b++;
    printf("a=%d, b=%d\n", a, b);
}

int main() {
    test();  // a=1, b=1
    test();  // a=1, b=2  ← b 记住了!
    test();  // a=1, b=3
}

💡 static 的本质:把变量从栈区搬到全局区。函数结束了,全局区的变量不会消失,所以能"记住"上次的值。


代码段------你写的代码存在这

代码段存放编译后的机器指令,只读,多个进程可以共享同一份代码。

c 复制代码
// 以下所有代码,编译后都存在代码段
int add(int a, int b) {
    return a + b;
}

int main() {
    for (int i = 0; i < 10; i++) {
        printf("%d\n", add(i, 1));
    }
    return 0;
}

💡 代码段是只读的 。所以你不能修改常量字符串

c 复制代码
char *str = "hello";
str[0] = 'H';  // 段错误!"hello" 存在只读数据段

但数组可以:

c 复制代码
char str[] = "hello";  // "hello" 被拷贝到栈上
str[0] = 'H';           // 没问题,栈上的副本可以改

实战:一个程序看清所有区域

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int global_var = 100;        // 全局区 .data
int global_uninit;            // 全局区 .bss(自动为0)
static int static_global = 50; // 全局区 .data(本文件可见)
const char *g_str = "hello";  // g_str 在 .data,"hello" 在只读段

void demo(int param) {        // param 在栈区
    int local = 42;           // 栈区
    static int static_local = 0; // 全局区 .data(只初始化一次)
    
    int *heap_var = (int *)malloc(sizeof(int)); // 堆区
    *heap_var = 999;
    
    printf("--- 内存分布 ---\n");
    printf("代码段(函数地址): %p\n", demo);
    printf("全局区 .data:    %p (global_var=%d)\n", &global_var, global_var);
    printf("全局区 .bss:     %p (global_uninit=%d)\n", &global_uninit, global_uninit);
    printf("只读数据段:      %p (str=%s)\n", g_str, g_str);
    printf("堆区:            %p (*heap=%d)\n", heap_var, *heap_var);
    printf("栈区:            %p (local=%d)\n", &local, local);
    printf("栈区(参数):      %p (param=%d)\n", &param, param);
    
    free(heap_var);
}

int main() {
    demo(77);
    return 0;
}

运行后你会看到:

  • 代码段地址最低(函数本身的位置)
  • 全局区在中间
  • 堆区在更高地址
  • 栈区在最高地址

💡 这个程序跑一遍,5 个区全看到了。 比看十遍教科书管用。


一张表记住所有区别

特性 全局区 代码段
存什么 局部变量、参数 malloc 分配的数据 全局/static/常量 机器指令
谁管理 系统自动 程序员手动 系统自动 系统
速度 极快 较慢 ---
大小 1~8MB 几乎不限(受内存制约) 编译时确定 编译时确定
增长方向 高→低 低→高 固定 固定
生命周期 函数调用期间 手动 free 前 程序运行期间 程序运行期间
常见坑 栈溢出、返回局部地址 内存泄漏、double free 全局变量命名冲突 修改常量字符串

写在最后

C 语言的内存就 5 个区,每个区都有自己的规则:

  • :快但小,函数结束自动回收,别放太大的东西
  • :大但麻烦,malloc 和 free 必须成对出现
  • 全局区:static 变量的家,程序结束才释放
  • 代码段:只读的,别碰常量字符串
  • 只读数据段:字符串常量的归宿,修改就崩

选择原则很简单:

  • 小且短命 → 栈
  • 大或变长或跨函数 → 堆
  • 需要全程存在 → 全局区/static

理解了内存布局,很多"莫名其妙"的 bug 就有了方向:段错误 → 看是不是碰了只读区;数据莫名被改 → 看是不是栈溢出覆盖了;内存越来越多 → 看是不是 malloc 没 free。

C 语言不会帮你管内存,但你可以帮自己搞清楚。

相关推荐
天空'之城2 小时前
C 语言工业级通用组件手写 08:精准软件消抖
c语言·嵌入式开发·工业级组件·软件消抖·按键防抖
Legendary_0083 小时前
LDR6500 设计指南:Type-C 音频快充二合一配件 PD 取电与插拔时序控制方案
c语言·开发语言·音视频
会飞的小新4 小时前
C 标准库之 <fenv.h> 详解与深度解析
c语言·开发语言·microsoft
爱刷碗的苏泓舒5 小时前
C 语言 if-else 与 switch-case 分支语句对比
c语言·开发语言
无相求码5 小时前
指针的5层境界,从入门到劝退:C语言指针进阶全解析
c语言
C++ 老炮儿的技术栈6 小时前
基于MFC+原生GDI手动自绘仪表盘控件
c语言·c++·visual studio·控件·工业控制·gdi·自绘
小肝一下7 小时前
3. 单链表
c语言·数据结构·c++·算法·leetcode·链表·dijkstra
炸膛坦客16 小时前
单片机/C/C++八股:(二十六)IIC 专题(I²C)---- 上集
c语言·c++·单片机
程序员Rock1 天前
上位机开发-MODBUS面试常见问题
c语言·c++·面试·职场和发展·上位机