1. C语言内存模型

咱们直接切入正题。C语言程序跑起来之后,内存到底长什么样?

我刚开始学嵌入式那会儿,总觉得内存就是个黑盒子。变量往里一扔,指针指来指去,出bug了也不知道为啥。后来被一个野指针折磨了三天,才老老实实把内存模型啃了一遍。

说白了,一个C程序在内存里被分成了五个区域。每个区域各司其职,谁也别越界。咱们一个一个看。

1.1 代码段(Text Segment)

代码段存的是啥?就是你的机器指令。你写的if、while、函数调用,编译链接之后全变成二进制指令,放在这里。

这个区域有个特点:只读。你想想看,程序跑起来之后,指令要是能被随便改,那不乱套了?我见过有人试图通过指针修改代码段里的内容,结果直接段错误。嗯,这是保护机制在起作用。

关键点:代码段是只读的,大小在编译时就固定了。你写再多代码,它也不会在运行时变大。

1.2 数据段(Data Segment)

数据段存的是已初始化的全局变量和静态变量。注意关键词——「已初始化」。

举个例子:

int global_var = 42;        // 这个放在数据段
static int static_var = 10; // 这个也放在数据段

这些变量在程序启动前就已经有值了。我个人习惯把数据段叫做「明牌区」——值都摆在那,清清楚楚。

我在项目中遇到过一个问题:一个嵌入式设备,数据段太大,导致编译出来的镜像文件比预期大了好几倍。后来发现是有人定义了一个巨大的全局数组,还给了初始值。嗯,这种坑踩过一次就记住了。

1.3 BSS段(Block Started by Symbol)

BSS段存的是未初始化的全局变量和静态变量。说白了,就是那些你声明了但没给初值的家伙。

int global_uninit;          // 放在BSS段
static int static_uninit;   // 也放在BSS段

你可能会问:这些变量没初始化,那它们初始值是多少?答案是0。BSS段在程序加载时会被系统清零。

小技巧:如果你有一大块缓冲区,不需要特定初始值,用未初始化的全局变量或静态变量。它们不占镜像文件空间,只在运行时分配内存。这在嵌入式开发中特别有用——省Flash空间。

我曾经在一个资源受限的MCU项目里,把一个大数组从数据段挪到BSS段,镜像文件直接小了200KB。你想想看,这在某些场景下就是能不能烧进去的区别。

1.4 堆(Heap)

堆是动态内存分配的地盘。你用malloc、calloc、realloc申请的内存,全在这里。

堆的特点:

  • 大小不固定,可以增长或缩小
  • 生命周期由程序员控制——你申请,你释放
  • 分配效率比栈低

我见过太多新手在堆上栽跟头。最常见的两个问题:

  1. 内存泄漏——申请了不释放
  2. 野指针——释放了还继续用

警告:在嵌入式系统中,堆的使用要格外小心。频繁的malloc/free会导致内存碎片,最终可能分配失败。我曾经在一个长期运行的设备上,因为堆碎片化严重,系统跑了两个月后突然崩溃。从那以后,我养成了「能不用堆就不用堆」的习惯。

1.5 栈(Stack)

栈是函数调用的舞台。局部变量、函数参数、返回地址,全压在栈上。

栈的特点:

  • 自动分配、自动释放——函数进去压栈,出来弹栈
  • 速度极快——比堆快一个数量级
  • 大小有限——通常只有几KB到几MB
void func() {
    int local_var = 5;  // 这个在栈上
    char buffer[1024];  // 这个也在栈上
}

嗯,这里要注意:栈空间是有限的。你在函数里定义一个超大数组,比如 char big[1024*1024],很可能直接栈溢出。我调试过一个bug,就是有人在中断服务函数里定义了一个大数组,结果一触发中断就死机。查了半天,原来是栈不够用。

1.6 内存布局全景图

说了这么多,咱们用一张图把整个内存模型串起来。下面是我手绘的SVG结构图,你一看就明白。

C语言内存模型布局 高地址 栈(Stack) 局部变量、函数参数、返回地址 向下增长(高地址→低地址) 堆(Heap) 动态分配(malloc/calloc/realloc) 向上增长(低地址→高地址) BSS段 未初始化全局/静态变量(初始为0) 数据段 已初始化全局/静态变量 代码段(只读) 低地址

这张图里有个细节你注意一下:栈和堆是相向而长的。栈从高地址往低地址走,堆从低地址往高地址走。它们中间是空闲区域。如果两者撞上了,那就是栈溢出或者堆溢出——程序大概率直接崩溃。

1.7 各段对比一览

内存区域 存储内容 生命周期 分配方式 大小
代码段 机器指令 程序运行期间 编译时固定 固定
数据段 已初始化全局/静态变量 程序运行期间 编译时固定 固定
BSS段 未初始化全局/静态变量 程序运行期间 编译时固定 固定
堆 动态分配的内存 从malloc到free 运行时手动 动态增长
栈 局部变量、函数参数 函数调用期间 运行时自动 固定(通常较小)

1.8 避坑指南

最后,我把自己这些年踩过的坑总结一下,你记好了:

  • 不要返回局部变量的地址。函数退出后栈空间被回收,那个地址就成了野指针。我曾经在代码里看到有人返回局部数组的地址,还信誓旦旦说没问题——结果跑起来数据全乱套。
  • 全局变量不是越多越好。数据段和BSS段的大小是固定的,但全局变量多了,代码的可维护性会直线下降。我见过一个项目,全局变量上千个,改一个变量要翻遍整个工程。
  • 堆分配一定要检查返回值。malloc返回NULL的情况虽然少见,但在嵌入式系统里,内存紧张时真的会发生。不检查就直接用,解引用空指针,程序直接崩。
  • 栈大小要心里有数。尤其是中断服务函数和递归调用。我曾经在一个RTOS项目里,任务栈设小了,系统跑着跑着就莫名其妙复位。后来用工具一查,栈溢出了。

一句话总结:代码段只读别乱改,数据段BSS段全局静态放,堆要手动管好,栈自动但空间有限。搞清楚了这五个区域,C语言内存管理你就入门了。

嗯,这一章就到这里。内容不多,但都是干货。你把这几个段的关系理清楚,后面讲指针、讲动态内存、讲内存对齐,你才能听得明白。