在 C 语言开发中,栈上固定大小的数组和变量,很难应对运行时才确定空间大小的场景。动态内存管理(malloc/free、calloc、realloc、柔性数组)正是解决这一痛点的核心能力,也是面试高频考点。本文从原理、用法、避坑、笔试题到内存布局,全面总结动态内存管理的核心知识。
一、为什么需要动态内存分配?
我们常规的内存开辟方式有两个致命局限:
- 大小固定:栈上变量、数组的空间在编译时就确定,运行时无法修改;
- 数组长度受限:声明数组必须指定固定长度,无法适配动态需求。
而动态内存分配在堆区申请空间,运行时确定大小、可灵活调整、手动释放,完美解决上述问题,适配数组扩容、变长结构体等场景。
二、核心函数:malloc、free、calloc、realloc
所有动态内存函数均在 <stdlib.h> 头文件中声明,操作的是堆区内存。
1. malloc:申请未初始化的连续空间
- 函数原型:void* malloc(size_t size);
- 功能:向堆区申请size 字节的连续空间,返回空间起始地址;
- 返回值:成功返回void*地址(需强制类型转换),失败返回NULL(必须判空);
- 注意:申请的内存未初始化,值为随机垃圾数据;size=0行为未定义。
2. free:释放动态内存
- 函数原型:void free(void* ptr);
- 功能:释放ptr指向的堆区动态内存;
- 注意:
- ptr必须是malloc/calloc/realloc返回的地址,否则行为未定义;
- ptr=NULL时,free无任何操作;
- 释放后建议将ptr置为NULL,避免野指针。
3. calloc:申请并初始化的连续空间
- 函数原型:void* calloc(size_t num, size_t size);
- 功能:申请num 个、每个 size 字节的连续空间,自动初始化为 0;
- 与 malloc 区别:多了一步全 0 初始化,适合需要初始值为 0 的场景。
4. realloc:调整动态内存大小
- 函数原型:void* realloc(void* ptr, size_t size);
- 功能:调整ptr指向的内存为size字节,保留原数据;
- 两种扩容场景:
- 原空间后有足够空间:直接追加,返回原地址;
- 原空间后无足够空间:开辟新空间、拷贝原数据、释放旧空间,返回新地址;
- 注意:
- ptr=NULL时,等价于malloc(size);
- 失败返回NULL,原内存不变,切勿直接用原指针接收返回值(会导致内存泄漏)。
三、动态内存的 6 大常见错误(高频踩坑点)
1. 对 NULL 指针解引用
malloc 失败返回 NULL,直接解引用会导致程序崩溃。
-

-
2. 越界访问
访问超出申请范围的内存,破坏堆结构,导致未定义行为。
c
int *p = (int*)malloc(10*sizeof(int));
for(int i=0; i<=10; i++)
*(p+i)=i; // 错误:i=10时越界3. free 非动态内存
free 只能释放堆区内存,释放栈区变量会报错。
int a=10;
int *p=&a;
free(p); // 错误:a在栈区,非动态内存4. free 动态内存的一部分
指针偏移后指向非起始地址,free 失败。
int *p=(int*)malloc(100);
p++;
free(p); // 错误:p不再指向起始地址5. 重复释放
同一块内存多次 free,破坏堆结构。
int *p=(int*)malloc(100);
free(p);
free(p); // 错误:重复释放6. 内存泄漏
忘记释放不再使用的动态内存,占用系统资源,程序长期运行会崩溃。
void test()
{
int *p=(int*)malloc(100);
*p=20;
} // 错误:未free,内存泄漏四、经典笔试题解析(必掌握)
题目 1:传值调用,malloc 无效
void GetMemory(char *p)
{
p=(char*)malloc(100);
}
void Test()
{
char *str=NULL;
GetMemory(str);
strcpy(str,"hello world"); // 崩溃
}结果:程序崩溃。
-
原因:传值调用,函数内修改的是形参 p,实参 str 仍为 NULL,解引用崩溃。
-
题目 2:返回栈区地址,野指针
char* GetMemory(void)
{
char p[]="hello world";
return p;
}
void Test()
{
char *str=GetMemory();
printf(str); // 乱码/崩溃
}结果:输出乱码或崩溃。 原因:p 是栈区数组,函数结束后栈帧销毁,返回的地址为野指针。
-
题目 3:传址调用,正常运行
-
void GetMemory(char **p, int num)
{
*p=(char*)malloc(num);
}
void Test()
{
char *str=NULL;
GetMemory(&str,100);
strcpy(str,"hello");
printf(str); // 输出hello
free(str); // 正确释放
}结果:正常输出hello。
-
原因:传二级指针,修改实参 str 指向堆区内存,可正常使用和释放。
-
题目 4:释放后野指针,非法访问
void Test()
{
char *str=(char*)malloc(100);
strcpy(str,"hello");
free(str); // 释放内存
if(str!=NULL)
{
strcpy(str,"world"); // 非法访问
printf(str);
}
}结果:输出world(侥幸),但属于未定义行为。
-
原因:free 后 str 为野指针,值非 NULL,访问已释放内存,风险极高。
-
五、柔性数组:结构体变长的优雅方案
1. 定义
C99 标准规定:结构体最后一个成员可以是未知大小的数组,称为柔性数组。
// 写法1:部分编译器支持
struct st
{
int i;
int a[0]; // 柔性数组
};
// 写法2:通用
struct st
{
int i;
int a[]; // 柔性数组
};2. 特点
- 柔性数组前至少有一个成员;
- sizeof(结构体)不包含柔性数组(如上结构体大小为 4);
- 需用malloc一次性分配:结构体大小 + 柔性数组空间。
-
优势:
- 释放简单:一次 free 释放所有内存,避免用户漏释放;
- 访问高效:内存连续,减少内存碎片,提升访问速度。
-
六、C/C++ 程序内存区域划分
程序运行时内存分为 4 大区域,动态内存管理核心在堆区:
- 栈区(stack):存放局部变量、函数参数、返回地址;自动分配释放,容量小、效率高;
- 堆区(heap):存放动态内存(malloc/free);手动分配释放,容量大、灵活;
- 数据段(静态区):存放全局变量、静态变量;程序结束由系统释放;
- 代码段:存放函数二进制代码;只读,程序运行期间不变。
-
七、总结
动态内存管理是 C 语言的核心技能,核心要点:
- 动态内存操作堆区,需手动malloc申请、free释放;
- malloc判空、realloc用临时指针接收、释放后置NULL;
- 避开 6 大常见错误,掌握 4 道经典笔试题逻辑;
- 柔性数组是结构体变长的最优方案,兼顾效率和安全性;
- 理清内存布局,区分栈、堆、数据段、代码段的特性。



