堆栈和内存分配
这篇笔记我们介绍可执行程序的内存模型和C语言中和内存分配管理相关的用法。
程序的内存模型
可执行文件内部的分段
在Linux下,我们可以使用size命令查看一个可执行文件的区域划分信息。

我们可以看到,在可执行程序加载到内存之前,其实已经划分了一些区域。
代码段:存放CPU执行的机器指令,是一段只读的数据区域。代码段是可共享的,通常在内存中只会存在一份。
数据段:包括初始化的数据段和未初始化的数据段,前者包括程序编译时就已规划好的内存区域,主要存放全局数据、常量等;后者也叫BSS段,用于存储未初始化的全局变量和静态变量。与初始化的数据段不同,BSS段中的变量在程序加载时不占据实际的存储空间,而是在运行时分配。在可执行文件中,BSS段仅记录变量的大小和位置,而不保存具体的初始值。在程序运行之前,操作系统会将BSS段中的变量初始化为零。数据段在加载到内存后也可以叫静态区。
程序运行时内存模型
程序在加载到内存前,代码段和数据段的大小就已经固定了,程序运行期间代码区和静态区的分配也不能改变,而堆和栈则是运行时分配的内存区域。
栈:函数执行时,局部变量会在栈上创建,函数返回时自动销毁。栈内存分配一般都有对应的处理器指令,效率较高,但为了保证执行效率,栈内存空间不会很大,Linux默认为8MiB。如果错误的使用递归函数,可能导致栈内存不足,这种bug被称为栈溢出。
堆:程序运行时可以用malloc和free函数动态申请和释放的内存,堆内存比较充足,但需要我们自己管理,如果不小心分配后忘记释放,程序占用的内存越来越大直到系统崩溃,这种bug叫做堆内存泄漏。
静态区内存除非程序结束否则无需释放;栈内存能够自动管理,也不需要手动释放;而堆内存需要手动进行管理。
栈内存管理
栈内存由系统自动管理,主要存放函数的参数及局部变量。函数执行完成后,系统自动释放栈内存,不需要程序员手动释放,这部分可以参考汇编语言相关的章节。如果对栈的理解不深可能偶然写出一些错误的代码,下面例子代码中,func()函数返回时s指向的栈内存已经释放,因此打印它的内容是未定义行为。
#include <stdio.h>
char *func()
{
// 典型错误写法,s存储在栈区,函数返回时内存已释放
char *s = "abc123";
return s;
}
int main(void)
{
char *s = func();
// 此处将输出乱码
printf("%s\n", &s);
return 0;
}
如果要在函数中返回字符串,我们可以申请堆内存实现,或者传入一个指针参数,函数内将结果拷贝到指定的内存地址。
堆内存管理
C语言标准库中提供了一些函数用来管理堆内存。
#include <stdlib.h>
void *malloc(int n);
void *calloc(int n, int size);
void *realloc(void *p, int n);
void free(void *p);
malloc分配n个字节的内存,并返回指向这段内存的指针。calloc和malloc相同,只不过参数有些区别,calloc会分配n*size字节的内存,此外还会初始化内存数据为0。realloc会重新分配n字节内存,并复制p指向的原有内容到新分配的内存上。free用于释放指针p指向的内存。
这里注意calloc会初始化新分配的内存数据为0,而malloc不会。malloc分配的内存如果需要初始化操作,需要配合memset函数使用。
memset函数能够初始化p指向的内存中的前n个字节,将其初始化为c字符。
#include <string.h>
void *memset(void *p, int c, int n);
这里要注意的一点是如果传入的指针不是char *类型,只能初始化为0,c使用其它字符会造成错误;而char *类型则可以指定任意c参数字符。
静态区内存使用
静态区存储全局变量、静态变量和常量。静态区内的变量在编译阶段就已经分配好内存布局并初始化,这块内存在程序运行期间也将一直存在。
注意:字符串字面值存储在静态区的常量区域,这也是字符串字面值不可修改的原因。
#include <stdio.h>
int i = 0; // 全局变量,存储在静态区
const int j = 1; // 常量,存储在静态区
int k; // 全局变量,存储在静态区,默认初始化位0
int main(void)
{
return 0;
}