在C语言编程中,栈上开辟的 int、char、定长数组——这些空间在编译时就拍死了,大小固定,运行中没法调整。但实际开发里,很多场景要到程序运行时才能确定到底要多大内存,还得能动态扩容或缩容。这时候,动态内存管理就成了必须啃下的硬骨头。
一、为什么需要动态分配内存?
int vsl = 20; 在栈空间上开辟4字节
char arr[10] = {0}; 在栈空间上开辟10字节
之前学的变量创建都是在栈区,根据类型大小分配固定空间。这种方式有两个硬伤:
- 空间大小固定:编译时就确定了,跑起来改不了。
- 数组长度不可变:声明时必须写死长度,一旦定义,既不能扩容也不能缩容。
现实需求往往是动态的——比如用户输入的数据量不确定,列表需要随时增删元素。C语言为此引入了堆区的动态内存分配,由程序员手动申请、手动释放,灵活可控,完美绕过栈区的限制。
二、核心动态开辟函数
1. malloc:申请指定大小的连续内存
函数原型 void* malloc(size_t size);
- 头文件:
#include - 功能:向堆区申请
size字节的连续可用内存,返回起始地址。 - 参数:
size就是要申请的字节数。 - 返回值:成功返回
void*类型的起始地址(用的时候需要强制类型转换);失败返回NULL(比如内存不够时),所以返回值必须判空。 - 注意:申请回来的内存没有初始化,里面是随机值,谁也不知道里面藏着什么。

2. free:释放动态申请的内存
函数原型 void free(void* ptr);
- 头文件:
#include - 功能:释放
ptr指向的堆区动态内存。 - 参数:
ptr必须是动态内存的起始地址。 - 注意:如果
ptr指向的空间不是动态开辟的(比如栈上的变量),那free的行为就是未定义的,后果不可预测;如果ptr是NULL,那函数啥也不做,安全。

3. calloc:申请并初始化内存
函数原型 void* calloc(size_t num, size_t size);
- 功能:申请
num个、每个size字节的连续内存,并且自动初始化为 0。 - 与 malloc 的区别:
calloc会帮你把内存清零,malloc则留下随机值。 - 返回值:和
malloc一样,成功返回地址,失败返回NULL。


4. realloc:动态调整内存大小
realloc的出现让动态内存管理更灵活了。- 有时候发现之前申请的空间太小,有时候又觉得太大浪费——为了合理使用内存,肯定要能灵活调整。
realloc就是干这个的。
函数原型 void* realloc(void* ptr, size_t size);
- 功能:调整
ptr指向的动态内存大小为size字节,原有数据会保留。 - 参数:
ptr是原内存地址,如果传NULL,那就等价于malloc;size是调整后的新字节数。 - 返回值:成功返回新地址(注意可能和原地址不同);失败返回
NULL,原内存不动,也不会被释放。 - 扩容的两种情况:
- 原内存后面有足够空间:直接在后面追加,返回原地址。
- 原内存后面没空间了:重新找一块更大的新空间,拷贝原数据过去,释放旧空间,最后返回新地址。


三、动态内存常见的6大错误
1. 对 NULL 指针解引用
malloc、realloc、calloc 都有可能返回 NULL,直接拿来解引用必崩。

2. 对动态开辟空间的越界访问
申请了10个 int,偏要去访问第11个——越界,行为未定义。

3. 对非动态开辟内存使用 free 释放
对栈区变量调用 free,结果不可预料。

4. 使用 free 释放一块动态开辟内存的一部分
指针偏移后再 free,行为未定义。

5. 对同一块动态内存多次释放
同一块内存被 free 两次,程序直接崩溃。


6. 动态开辟内存未释放(内存泄漏)
申请了动态内存却不释放,程序跑着跑着内存越占越多,最后系统扛不住。

四、动态内存经典笔试题分析
1. 传值调用,内存泄漏


- 结果:程序崩溃,而且内存泄漏。
- 原因:
GetMemory是传值调用,形参p是实参str的临时拷贝,修改p不影响外部的str,所以str仍然是NULL,解引用直接崩;而malloc出来的内存没人能释放,泄漏了。
改进方法1:使用二级指针作为参数。


改进方法2:函数返回指针。


2. 返回栈区地址,野指针

- 结果:打印出随机值,因为返回的是野指针。
- 原因:
p是栈上的局部数组,函数结束后栈帧销毁,p的地址就失效了,str指向了一块已经被回收的内存。
改进方法:使用静态区内存

3. 传址调用,内存泄漏

- 结果:能正常打印"hello",但内存泄漏了,然后程序崩溃。
- 原因:二级指针传址,
GetMemory直接修改了str本身,让它指向堆区内存,所以打印没问题。但malloc出来的内存一直没释放,泄漏了。
4. free 后野指针

- 结果:程序崩了,或者打印出随机值。
- 原因:
free之后str没有置空,它仍然指向那块已经释放的内存——变成了野指针,后续再访问就是非法操作。
五、柔性数组:结构体的动态数组
1. 柔性数组的定义
C99 规定:结构体的最后一个成员可以是未知大小的数组,这就是柔性数组。
// 写法1:常用
struct S
{
int n;
int arr[]; // 柔性数组成员
};
// 写法2:部分编译器支持
struct S
{
int n;
int arr[0];
};
2. 柔性数组的特点
- 结构体中至少有一个其他成员(不能只有柔性数组)。
sizeof(结构体)不包含柔性数组的内存。- 必须用
malloc一次性分配结构体 + 柔性数组的内存。
struct S
{
int n;
char c;
int arr[];
};
int main()
{
printf("%zu\n", sizeof(struct S)); // 输出 8
return 0;
}
3. 柔性数组 vs 指针成员
// 柔性数组版本
struct S
{
int n;
int arr[]; // 柔性数组成员
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
// 1. 一次 malloc,同时包含结构体 n + arr 数组空间
if (p == NULL)
{
perror("malloc");
return 1;
}
p->n = 100;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i + 1;
}
free(p); // 一次释放
p = NULL;
return 0;
}
// 指针成员版本
struct S
{
int n;
int* arr; // 指针变量
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));
// 1. 只开了 int n + int* arr 的大小,arr 是野指针
if (p == NULL)
{
perror("malloc");
return 1;
}
p->n = 100;
int* ptr = (int*)malloc(10 * sizeof(int));
// 2. 单独创建 arr 指向的数组空间
p->arr = ptr;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i + 1;
}
free(p->arr); // 3. 先释放数组数据空间
free(p); // 4. 再释放结构体空间
p = NULL;
return 0;
}
(1)内存布局
- 柔性数组:一整块连续内存,
n与arr紧紧挨在一起。 - 指针成员:两块分散内存,结构体和数组内存相互独立。
(2)释放区别(最大优势)
- 柔性数组:一次
free全部释放,不会漏,不容易内存泄漏。 - 指针成员:必须先后两次
free,任何一次遗漏都会造成内存泄漏。
(3)效率
- 柔性数组内存连续,CPU 缓存命中率高,访问速度更快,内存碎片更少。
- 指针成员两块内存分散,效率略低,容易产生内存碎片。
(4)使用场景总结
- 追求简洁、稳定、高性能:优先用柔性数组(结构体动态数组的首选)。
- 如果数组需要中途单独扩容、单独销毁:则选用指针成员。
总结:柔性数组 = 一体连续内存,一次释放;指针成员 = 分体内存,两次释放。
六、C/C++ 程序内存区域划分

- 栈区(stack):存放局部变量、函数参数、返回地址;自动分配释放,向下增长,空间小。
- 堆区(heap):存放动态内存(
malloc/calloc/realloc),手动分配释放,向上增长,空间大。 - 数据段(静态区):存放全局变量、静态变量;程序结束时由系统释放。
- 代码段:存放可执行代码、只读常量;只读,防止篡改。
- 内核空间:操作系统占用,用户代码不可读写。
