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申请的内存,全在这里。
堆的特点:
- 大小不固定,可以增长或缩小
- 生命周期由程序员控制——你申请,你释放
- 分配效率比栈低
我见过太多新手在堆上栽跟头。最常见的两个问题:
- 内存泄漏——申请了不释放
- 野指针——释放了还继续用
警告:在嵌入式系统中,堆的使用要格外小心。频繁的malloc/free会导致内存碎片,最终可能分配失败。我曾经在一个长期运行的设备上,因为堆碎片化严重,系统跑了两个月后突然崩溃。从那以后,我养成了「能不用堆就不用堆」的习惯。
1.5 栈(Stack)
栈是函数调用的舞台。局部变量、函数参数、返回地址,全压在栈上。
栈的特点:
- 自动分配、自动释放——函数进去压栈,出来弹栈
- 速度极快——比堆快一个数量级
- 大小有限——通常只有几KB到几MB
void func() {
int local_var = 5; // 这个在栈上
char buffer[1024]; // 这个也在栈上
}
嗯,这里要注意:栈空间是有限的。你在函数里定义一个超大数组,比如 char big[1024*1024],很可能直接栈溢出。我调试过一个bug,就是有人在中断服务函数里定义了一个大数组,结果一触发中断就死机。查了半天,原来是栈不够用。
1.6 内存布局全景图
说了这么多,咱们用一张图把整个内存模型串起来。下面是我手绘的SVG结构图,你一看就明白。
这张图里有个细节你注意一下:栈和堆是相向而长的。栈从高地址往低地址走,堆从低地址往高地址走。它们中间是空闲区域。如果两者撞上了,那就是栈溢出或者堆溢出——程序大概率直接崩溃。
1.7 各段对比一览
| 内存区域 | 存储内容 | 生命周期 | 分配方式 | 大小 |
|---|---|---|---|---|
| 代码段 | 机器指令 | 程序运行期间 | 编译时固定 | 固定 |
| 数据段 | 已初始化全局/静态变量 | 程序运行期间 | 编译时固定 | 固定 |
| BSS段 | 未初始化全局/静态变量 | 程序运行期间 | 编译时固定 | 固定 |
| 堆 | 动态分配的内存 | 从malloc到free | 运行时手动 | 动态增长 |
| 栈 | 局部变量、函数参数 | 函数调用期间 | 运行时自动 | 固定(通常较小) |
1.8 避坑指南
最后,我把自己这些年踩过的坑总结一下,你记好了:
- 不要返回局部变量的地址。函数退出后栈空间被回收,那个地址就成了野指针。我曾经在代码里看到有人返回局部数组的地址,还信誓旦旦说没问题——结果跑起来数据全乱套。
- 全局变量不是越多越好。数据段和BSS段的大小是固定的,但全局变量多了,代码的可维护性会直线下降。我见过一个项目,全局变量上千个,改一个变量要翻遍整个工程。
- 堆分配一定要检查返回值。malloc返回NULL的情况虽然少见,但在嵌入式系统里,内存紧张时真的会发生。不检查就直接用,解引用空指针,程序直接崩。
- 栈大小要心里有数。尤其是中断服务函数和递归调用。我曾经在一个RTOS项目里,任务栈设小了,系统跑着跑着就莫名其妙复位。后来用工具一查,栈溢出了。
一句话总结:代码段只读别乱改,数据段BSS段全局静态放,堆要手动管好,栈自动但空间有限。搞清楚了这五个区域,C语言内存管理你就入门了。
嗯,这一章就到这里。内容不多,但都是干货。你把这几个段的关系理清楚,后面讲指针、讲动态内存、讲内存对齐,你才能听得明白。