动态内存管理是C语言进阶的核心难点,也是笔试面试的高频考点。本文将结合实战案例,系统梳理动态内存的常见错误、柔性数组的优势,以及内存分区的底层逻辑,帮你夯实底层编程能力。
一、为什么要有动态内存分配
已掌握的内存开辟方式
cpp
// 在栈空间开辟4字节
int val = 20;
// 在栈空间开辟10字节连续空间
char arr[10] = {0};
静态开辟内存的缺点
• 空间大小固定:数组在声明时必须指定长度,运行时无法调整
• 无法满足动态需求:程序运行时才知道所需空间大小的场景无法处理
动态内存分配的意义
让程序可以在运行时申请和释放内存,更灵活地管理内存资源。
二、malloc 和 free
2.1 malloc 函数
cpp
void* malloc(size_t size);
• 功能:在堆区申请一块连续可用的内存空间
• 参数:size - 要分配的内存字节数
• 返回值:
成功:返回指向内存起始地址的 void* 指针
失败:返回 NULL(必须检查返回值)
• 注意:
返回的 void* 需根据实际使用类型强制转换
size 为 0 时行为未定义,取决于编译器
2.2 free 函数
cpp
void free(void* ptr);
• 功能:释放动态内存分配函数(malloc/calloc/realloc)申请的空间,malloc 和 free 都声明在 stdlib.h 头文件中
• 参数:ptr - 指向要释放内存的指针
• 注意:
不能释放非动态开辟的内存
ptr 为 NULL 时函数不执行任何操作
释放后应将指针置为 NULL,避免野指针
示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int* ptr = NULL;
// 动态申请num个int大小的空间
ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr) // 检查malloc是否成功
{
int i = 0;
for (i = 0; i < num; i++)
{
*(ptr + i) = 0;
}
}
else
{
perror("malloc");
return 1;
}
free(ptr); // 释放动态内存
ptr = NULL; // 将指针置空,避免野指针
return 0;
}
三、calloc 和 realloc
3.1 calloc 函数
cpp
void* calloc(size_t num, size_t size);
• 功能:为 num 个大小为 size 的元素开辟空间,并将每个字节初始化为 0
• 与 malloc 的区别:calloc 会自动初始化内存为全 0,而 malloc 不会
• 适用场景:需要初始化内存的场景
示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // 输出全0
}
}
free(p);
p = NULL;
return 0;
}
3.2 realloc 函数
cpp
void* realloc(void* ptr, size_t size);
• 功能:调整已分配内存块的大小,可在保留原有数据的情况下扩大或缩小内存
• 参数:
ptr:指向要调整的内存起始地址(ptr为NULL 时,realloc功能等同于 malloc)
size:调整后的内存大小(字节)
• 返回值:
成功:返回新的内存地址(可能与原地址相同或不同)
失败:返回 NULL,原内存保持不变
内存调整的两种情况
-
原有空间后有足够空间:直接在原内存后追加空间,返回原地址
-
原有空间后无足够空间:在堆区寻找新空间,拷贝原数据到新空间,释放原空间,返回新地址
安全使用方式
cpp
int* p = (int*)malloc(100);
if (p != NULL)
{
// 业务处理
}
else
{
return 1;
}
// 扩展容量
int* temp = (int*)realloc(p, 1000);
if (temp != NULL)
{
p = temp;
}
free(p);
p = NULL;
四、常见的动态内存错误
4.1 对 NULL 指针的解引用
cpp
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20; // 如果malloc失败,p为NULL,会导致程序崩溃
free(p);
}
✅ 解决:必须检查 malloc/calloc/realloc 的返回值
4.2 对动态开辟空间的越界访问
cpp
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
return;
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i; // 当i=10时越界访问
}
free(p);
}
✅ 解决:确保访问不超过申请的内存范围
4.3 对非动态开辟内存使用 free
cpp
int main() {
int a = 10; // 栈区局部变量
int* p = &a;
free(p); // 错误:释放栈区内存,程序崩溃
p = NULL;
return 0;
}
错误原因:free函数仅能释放堆区由malloc/calloc/realloc开辟的动态内存,若对栈区局部变量、全局变量、常量区内存调用free,会触发段错误,因为这些内存的管理由编译器/操作系统负责,并非堆区。
✅ 解决:free 只能用于动态开辟的内存,明确free的使用范围:仅当指针指向堆区动态内存时,才能调用free,栈区/全局区/常量区内存无需手动释放(编译器自动回收)。
4.4 使用 free 释放一块动态内存的一部分
cpp
void test()
{
int* p = (int*)malloc(100);
p++;
free(p); // p不再指向动态内存的起始位置
}
错误原因:动态内存申请后,指针被非法偏移,导致free时仅释放了堆区的一部分内存,剩余未释放的内存成为内存碎片,程序运行期间无法再被使用,最终造成内存泄漏(堆内存被占用,直至程序退出才会被系统回收)。
✅ 解决:必须保持指针指向动态内存的起始地址
4.5 对同一块动态内存多次释放
cpp
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p); // 重复释放,导致未定义行为
}
错误原因:同一块堆区动态内存,调用多次free,会破坏堆区的内存管理链表,导致程序崩溃,属于未定义行为。
✅ 解决:释放后将指针置为 NULL(free(NULL) 是安全的)
4.6 动态开辟内存忘记释放(内存泄漏)
cpp
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
错误原因:最常见的动态内存错误,申请堆内存后,程序运行期间未调用free释放,且指向该内存的指针丢失,导致堆内存被永久占用,程序不退出则内存无法回收。
• 短期小程序:影响可忽略,程序退出后系统会回收所有内存;
• 长期运行程序(如服务器、嵌入式程序):内存泄漏会持续占用堆空间,最终导致内存耗尽,程序崩溃。
✅ 解决:动态开辟的内存必须释放,且正确释放
五、经典笔试题分析
题目 1:函数传参错误(无法修改主函数指针)
cpp
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果:程序会崩溃(段错误),并存在内存泄漏。
问题分析
-
指针传参本质是值传递:GetMemory 的形参 p 是 str 的临时拷贝,函数内对 p 的赋值不会影响主函数的 str,所以 str 依然是 NULL。
-
NULL指针解引用:strcpy(str, "hello world") 中 str 为 NULL,直接解引用会触发段错误。
-
内存泄漏:GetMemory 中 malloc 申请的100字节内存,因为临时指针 p 被销毁,没有任何指针指向它,无法释放。
修复方案(两种方式)
方式1:使用二级指针传参
cpp
void GetMemory(char **p)
{
*p = (char *)malloc(100);
if (*p == NULL) { // 新增:malloc后判空
perror("malloc fail");
return;
}
}
void Test(void)
{
char *str = NULL;
GetMemory(&str); // 传入指针的地址
if (str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str); // 释放内存
str = NULL;
}
}
方式2:函数返回动态内存的指针
cpp
char* GetMemory()
{
char* p = (char *)malloc(100);
if (p == NULL) {
perror("malloc fail");
return NULL;
}
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
if (str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
}
题目 2:返回栈区指针(野指针经典问题)
cpp
#include <stdio.h>
#include <stdlib.h>
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果:程序会出现乱码或崩溃,属于未定义行为。
问题分析
-
返回栈区局部变量地址:GetMemory 中的数组 p 是栈区局部变量,函数执行结束后栈帧被销毁,p 占用的内存被回收。
-
野指针访问:Test 中的 str 接收了已失效的栈区地址,成为野指针,此时访问 str 属于非法内存操作。
修复方案(三种方式)
方式1:返回堆区动态内存
cpp
char *GetMemory(void)
{
char* p = (char *)malloc(12); // 存"hello world"+'\0'
if (p == NULL) {
perror("malloc fail");
return NULL;
}
strcpy(p, "hello world");
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
if (str != NULL) {
printf(str);
free(str);
str = NULL;
}
}
方式2:返回常量区字符串(字符串常量存于常量区,只读)
cpp
char *GetMemory(void)
{
return "hello world"; // 常量区字符串,不可修改
}
方式3:使用全局变量
cpp
char p[] = "hello world"; // 全局区内存
char *GetMemory(void)
{
return p;
}
核心考点
内存分区的生命周期:
• 栈区:函数进栈创建,出栈销毁,局部变量存于此,不可返回栈区地址;
• 全局/静态区:程序启动时分配,退出时释放;
• 常量区:存字符串常量等,只读,程序运行期间一直存在;
• 堆区:malloc申请,free释放,手动管理生命周期。
题目 3:malloc后未判空
cpp
#include <stdio.h>
#include <stdlib.h>
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果
• 程序会正常打印出 hello,但存在内存泄漏。
问题分析
-
传参逻辑正确:GetMemory 接收二级指针 **p,可以成功修改主调函数 Test 里的指针 str,让它指向动态分配的100字节堆内存。
-
缺少判空检查:malloc 有可能失败返回 NULL,如果不检查,后续的 strcpy 会解引用 NULL 导致程序崩溃。
-
内存泄漏:Test 函数中申请的堆内存没有调用 free 释放,程序退出前这块内存会一直被占用。
修复方案
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
// 新增:malloc后必须判空
if (*p == NULL)
{
perror("malloc fail");
return;
}
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
if (str != NULL) // 判空后再操作
{
strcpy(str, "hello");
printf(str);
// 新增:使用完释放内存
free(str);
str = NULL; // 释放后置空,避免野指针
}
}
int main()
{
Test();
return 0;
}
题目 4:野指针
cpp
#include <stdio.h>
#include <stdlib.h>
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
运行结果
• 这是未定义行为,可能打印 world,也可能程序崩溃或出现乱码。
问题分析
-
野指针访问:free(str) 释放了 str 指向的堆内存,但 str 本身没有被置为 NULL,此时 str 是一个野指针(指向已被回收的无效地址)。
-
非法内存操作:if(str != NULL) 的判断结果为真,会执行 strcpy(str, "world"),这是对已释放内存的非法写入,会破坏堆区的内存管理结构,导致程序异常。
修复方案
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
char *str = (char *) malloc(100);
if (str == NULL) // 新增:malloc后判空
{
perror("malloc fail");
return;
}
strcpy(str, "hello");
free(str);
str = NULL; // 新增:释放后置空
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
六、柔性数组
6.1 定义与语法
柔性数组是C99标准引入的特性,指结构体的最后一个成员是未知大小的数组。
cpp
// 标准写法
struct st_type {
int i;
int a[0]; // 柔性数组成员
};
// 兼容某些编译器的写法
struct st_type {
int i;
int a[]; // 省略大小
};
6.2 核心特点
• 前置成员要求:柔性数组成员前必须至少有一个其他成员。
• sizeof计算规则:sizeof(struct st_type) 的结果不包含柔性数组的内存(例如上面结构体大小为4,仅包含int i)。
• 动态分配要求:必须用 malloc 一次性分配结构体和柔性数组的内存,且分配的总大小要大于结构体本身的大小。
cpp
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a)); // 输出的是4
return 0;
}
6.3 使用示例
代码1
cpp
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
}type_a;
int main()
{
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
// 业务处理
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
6.4 柔性数组的优势
上述的 type_a 结构也可以设计为下面的结构,也能完成同样的效果。
代码2
cpp
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int *p_a;
}type_a;
int main()
{
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
// 业务处理
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
// 释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
上述代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)
- 优势对比(与"结构体+指针"方案)
|-------|------------------|----------------------|
| 特性 | 柔性数组方案 | 结构体+指针方案 |
| 内存分配 | 一次分配,内存连续 | 两次分配,内存碎片化 |
| 内存释放 | 一次free即可 | 需分别释放指针指向的内存和结构体,易泄漏 |
| 访问效率 | 连续内存,缓存友好,访问速度更快 | 指针间接访问,多一次内存寻址 |
| 代码复杂度 | 更简洁,不易出错 | 需管理两次内存分配,代码繁琐 |
七、C/C++程序内存区域划分
程序运行时,内存会被划分为不同区域,各自负责存储不同类型的数据,生命周期也不同。
- 栈区(Stack)
• 存储内容:函数的局部变量、参数、返回地址等。
• 管理方式:由编译器自动分配和释放,函数执行结束时栈帧自动销毁。
• 特点:内存大小固定、速度快,但空间有限;地址向下增长。
- 堆区(Heap)
• 存储内容:动态分配的内存(malloc/calloc/realloc 申请的内存)。
• 管理方式:由程序员手动分配和释放,若忘记释放会导致内存泄漏,程序结束后由操作系统回收。
• 特点:空间大、灵活;地址向上增长。
- 数据段(静态区)
• 存储内容:全局变量、静态变量(static修饰的变量)。
• 管理方式:程序启动时分配,退出时由操作系统释放。
• 特点:生命周期与程序一致。
- 代码段(常量区)
• 存储内容:可执行代码、字符串常量等只读数据。
• 管理方式:由操作系统加载,程序运行期间只读。
• 特点:内存不可修改。
- 内核空间
• 存储内容:操作系统内核代码和数据。
• 特点:用户程序无法直接访问。
动态内存管理与内存分区的知识,不仅是C语言进阶的必经之路,更是写出高性能、稳定代码的基石。掌握柔性数组的设计思想,避开动态内存的常见陷阱,理解内存分区的底层逻辑,这些能力会让你在复杂场景下的编程更加游刃有余。希望本文的梳理与实战案例,能帮你建立起清晰的内存管理思维,在未来的开发与面试中都能从容应对。