一、动态内存分配
1. 为什么需要动态内存分配?
之前学的栈上开辟空间(比如int val = 20;、char arr[10]; )有两个硬伤:
- 空间大小固定,编译时就必须确定。
- 数组长度一旦声明就不能修改 ,无法应对 "运行时才知道需要多大空间" 的场景。
动态内存分配(在堆区操作)的核心优势 :运行时灵活申请 / 释放空间,大小可调整,解决了固定大小数组的局限性。
2. malloc 和 free 详解
2.1 malloc(相当于在图书馆借书)
- 函数原型 :
void* malloc(size_t size); 需要头文件#include<stdlib.h> - 核心功能 :在堆区申请一块连续 的内存空间,返回其起始地址。
- 关键细节 :
size参数:要分配的字节数 ,不是元素个数,所以常配合sizeof(类型)使用,比如malloc(num * sizeof(int))。- 返回值:
- 申请成功 :返回
void*类型的起始地址 ,使用时需要强转成目标类型 (比如(int*)malloc(...))。 - 申请失败 :返回
NULL,所以必须做判空处理,否则会触发空指针访问错误。
- 申请成功 :返回
- 特殊情况:
size为 0 时,行为未定义,不同编译器实现不同,不建议这么写。
2.2 free(相当于还之前借的书)
- 函数原型 :
void free(void* ptr); 需要头文件#include<stdlib.h> - 核心功能 :释放 之前通过**
malloc/calloc/realloc申请的堆内存。** - 关键细节 :
ptr必须是动态分配的地址,否则行为未定义。ptr为NULL时,free什么都不做,不会报错。- 释放后,建议手动将
ptr = NULL;,避免出现 "野指针"(指向已释放内存的指针)。
3. 代码示例逐行解析
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num); // 运行时输入需要的元素个数
// int arr[num]; // 部分编译器支持变长数组,但不是C标准特性,兼容性差
int* ptr = NULL;
ptr = (int*)malloc(num * sizeof(int)); // 申请num个int大小的堆空间
if (NULL != ptr) // 必须判空,防止malloc失败返回NULL
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0; // 给每个元素赋值为0
}
}
else
{
perror("malloc"); // 打印malloc失败的错误信息
return 1; // 异常退出程序
}
free(ptr); // 释放申请的堆内存,避免内存泄漏
ptr = NULL; // 关键:置空指针,防止野指针
return 0;
}
- 这里
ptr = NULL;是必要的 :free只是释放了内存 ,ptr里还存着原来的地址 ,不置空的话,后续如果不小心使用ptr,就会访问已经释放的内存 ,导致程序崩溃或未定义行为。
二、calloc和realloc
2.1 calloc 详解
函数原型
void* calloc(size_t num, size_t size);
- 参数 :
num:元素个数size:单个元素的字节大小
- 功能 :在堆区申请一块连续内存,空间大小为
num * size字节 ,并且会自动把每个字节初始化为 0。 - 和
malloc的核心区别 :calloc会初始化内存为 0, 而**malloc** 申请的内存里是随机值。
示例解析
int *p = (int*)calloc(10, sizeof(int));
-
这段代码等价于:
int *p = (int*)malloc(10 * sizeof(int)); memset(p, 0, 10 * sizeof(int)); // 手动初始化为0 -
输出结果是 10 个
0,就是因为**calloc自动完成了初始化。**
💡 适用场景:如果你需要申请内存后立刻用 0 初始化,用 calloc 会比 malloc + memset 更方便、代码更简洁。
2.2 realloc 详解
函数原型
void* realloc(void* ptr, size_t size);
- 功能 :调整之前通过
malloc/calloc/realloc申请的堆内存大小,可扩容也可缩容 ,并且会保留原数据。 - 参数 :
ptr:要调整的内存起始地址 ,如果是NULL,realloc****行为和malloc完全一样。size:调整后的新大小 (字节数)。
- 返回值 :
- 成功:返回调整后内存的起始地址(可能和原地址相同,也可能不同)。
- 失败:返回
NULL,原内存保持不变,不会被释放。
扩容的两种核心情况
| 情况 | 条件 | 结果 |
|---|---|---|
| 情况 1:原地扩容 | 原内存后面有足够的空闲空间 | 直接在原地址后面追加空间 ,原地址不变 ,数据保留 |
| 情况 2:异地扩容 | 原内存后面没有足够空间 | 在堆区找一块新的足够大的空间,把原数据拷贝过去 ,释放旧空间,返回新地址 |
关键易错点:realloc 的正确用法
❌ 错误写法(代码 1)
ptr = (int*)realloc(ptr, 1000);
- 问题:如果
realloc申请失败 ,会返回**NULL** ,直接赋值给ptr会导致:ptr变成空指针,丢失了原来内存的地址- 原内存无法再被
free,造成内存泄漏
✅ 正确写法(代码 2)
int* tmp = realloc(ptr, 1000);
if (tmp != NULL)
{
ptr = tmp;
}
else
{
// 处理扩容失败的情况,比如打印错误、释放原内存
perror("realloc");
free(ptr);
ptr = NULL;
return 1;
}
- 要点:先用临时变量接收
realloc的返回值 ,判空成功后再赋值给原指针 ,避免丢失原内存地址。
补充注意事项
- 缩容也会有两种情况 :
- 原地缩容:直接截断后面的空间,原地址不变
- 部分实现也可能会异地缩容(少见),所以同样建议用临时变量接收返回值
realloc(ptr, 0)的行为:标准未定义 ,不同编译器实现不同,不建议使用- 调整后的内存如果是异地扩容 ,原内存会被自动释放,不需要你手动
free原地址
动态内存函数对比表
表格
| 函数 | 初始化 | 调整大小 | 适用场景 |
|---|---|---|---|
malloc |
不初始化(随机值) | 不能 | 不需要初始化、固定大小的动态内存 |
calloc |
初始化为 0 | 不能 | 需要申请后立刻用 0 初始化的场景 |
realloc |
不初始化(新空间部分是随机值) | 可以扩容 / 缩容 | 运行时需要调整内存大小的场景 |
💡 小提示:动态内存的核心是谁申请谁释放 ,不管用哪个函数申请的内存,都要记得 free ;释放后要把指针置为 NULL,避免野指针问题。
三、常见的动态内存的错误
1. 对 NULL 指针的解引用操作
错误代码:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20; // 如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
问题分析: malloc 申请内存失败 时,会返回 NULL 。如果不检查 p 是否为 NULL ,直接解引用 *p ,会导致程序崩溃(空指针访问错误)。
修正方法:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
if (p == NULL) // 必须先检查申请是否成功
{
perror("malloc failed"); // 打印错误信息
return;
}
*p = 20;
free(p);
p = NULL;
}
2. 对动态开辟空间的越界访问
错误代码:
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 1;
}
for(i = 0; i <= 10; i++)
{
*(p+i) = i; // 当i是10的时候越界访问
}
free(p);
p = NULL;
}
问题分析: 申请了 10个int 大小的空间,合法下标是 0~9,但循环条件 i <= 10 会访问到 p[10] ,超出了分配的内存范围,属于越界访问,会导致未定义行为(崩溃、数据错乱等)。
修正方法:
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 1;
}
for(i = 0; i < 10; i++) // 改为i < 10,避免越界
{
*(p+i) = i;
}
free(p);
p = NULL;
}
3. 对非动态开辟内存使用 free 释放
错误代码:
void test()
{
int a = 10;
int *p = &a;
free(p); // 错误!
}
问题分析:free 只能释放 由 malloc/calloc/realloc 动态申请的堆内存。a 是栈上的局部变量 ,用 free 释放栈地址会导致程序崩溃。
修正方法: 不要对栈变量的指针使用 free,free 只用于堆内存释放。
4. 使用 free 释放一块动态开辟内存的一部分
错误代码:
void test()
{
int *p = (int *)malloc(100);
p++;
free(p); // p不再指向动态内存的起始位置
}
问题分析: malloc 返回的是内存块的起始地址 ,free 必须接收这个起始地址 才能正确释放整个内存块。p++ 后指针偏移了 ,指向内存块中间 ,此时 free(p) 会因为找不到内存块的元数据而崩溃。
修正方法:
void test()
{
int *p = (int *)malloc(100);
int *q = p; // 保存起始地址
p++; // 操作指针偏移
free(q); // 用起始地址释放
q = NULL;
}
5. 对同一块动态内存多次释放
错误代码:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); // 重复释放
}
问题分析:同一块堆内存只能被 free 一次 。重复释放 会破坏内存管理的元数据,导致程序崩溃或内存损坏。
修正方法:释放后立即将指针置为 NULL,避免误操作重复释放:
void test()
{
int *p = (int *)malloc(100);
free(p);
p = NULL; // 释放后置空
// free(p); // 此时free(NULL)是安全的(标准规定free(NULL)不做任何操作)
}
6. 动态开辟内存忘记释放(内存泄漏)
错误代码:
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
return 0;
}
问题分析:test 函数中申请了内存 ,但没有调用 free 释放 ,且程序一直运行 ,导致这部分内存永远无法被回收,造成内存泄漏 。长时间运行会耗尽系统内存。
修正方法: 申请的内存,使用完毕后必须在所有分支路径都调用 free 释放:
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
free(p); // 用完就释放
p = NULL;
}
}
int main()
{
test();
return 0;
}
💡 总结一下动态内存的核心原则:
- 申请必检查 :
malloc后必须判断返回值是否为NULL。 - 访问不越界:严格按照申请的大小访问内存。
- 释放要正确:只能释放堆内存,且必须用起始地址释放。
- 释放仅一次:释放后立即置空,避免重复释放。
- 用完必释放:所有分支路径都要确保内存被释放,防止泄漏。
四、四道经典动态内存笔试题
题目 1:值传递导致的内存泄漏与空指针访问
代码
#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;
}
运行结果
程序崩溃,大概率触发段错误(Segmentation Fault)。
错误分析
- 值传递的问题 :
GetMemory函数的参数p是str的值拷贝 ,函数内修改的只是形参p的副本,无法改变实参str的值。 - 空指针访问 :调用
GetMemory后,str仍然是NULL,后续strcpy(str, "hello world")是对空指针的解引用,直接触发崩溃。 - 内存泄漏 :
malloc申请的 100 字节内存,函数结束后没有任何指针指向它 ,也没有被释放 ,造成内存泄漏。
修正方案(传指针的指针)
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);
if (str != NULL)
{
strcpy(str, "hello world");
printf("%s\n", str);
free(str);
str = NULL;
}
}
题目 2:返回栈地址导致的野指针问题
代码
#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;
}
运行结果
程序输出乱码 ,或直接崩溃,属于未定义行为。
错误分析
char p[] = "hello world";是GetMemory函数内的局部栈数组 ,函数执行结束后,栈帧被销毁,p指向的内存空间也被回收。- 函数返回的
p是一个野指针,指向已经失效的栈内存,后续访问这块内存属于未定义行为,内容不可控。
修正方案(使用静态变量 / 动态内存 / 字符串常量)
// 方案1:用static静态变量(内存位于静态区,函数结束后不销毁)
char *GetMemory(void)
{
static char p[] = "hello world";
return p;
}
// 方案2:用动态内存(malloc在堆上申请)
char *GetMemory(void)
{
char *p = (char *)malloc(12);
strcpy(p, "hello world");
return p;
}
// 方案3:直接返回字符串常量(常量区,函数结束后不销毁)
char *GetMemory(void)
{
return "hello world";
}
题目 3:二级指针传递,内存申请成功但未释放
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.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 ,但存在内存泄漏。
分析
- 正确点 :通过二级指针
char **p传递,*p = malloc(num)能正确修改str的值 ,使其指向堆上的 100 字节内存,strcpy和printf都能正常执行。 - 问题点 :
malloc申请的 100 字节内存,在**Test函数结束后没有调用free释放** ,造成内存泄漏。
修正方案(添加 free 释放)
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
if (str != NULL)
{
strcpy(str, "hello");
printf("%s\n", str);
free(str); // 释放动态内存
str = NULL;
}
}
题目 4:释放后未置空,导致野指针访问
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.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;
}
运行结果
**程序大概率崩溃,或输出不可控的乱码,**属于未定义行为。
错误分析
free(str)执行后 ,str指向的堆内存已经被释放,这块内存不再属于程序,可能被系统回收或分配给其他变量。- 但
str本身的值没有被修改,仍然指向原来的地址 ,此时str成为野指针。 if(str != NULL)判断为真 ,执行strcpy(str, "world")是对已释放内存的非法访问,触发未定义行为。
修正方案(释放后置空)
void Test(void)
{
char *str = (char *)malloc(100);
if (str != NULL)
{
strcpy(str, "hello");
free(str);
str = NULL; // 释放后立即置空,避免野指针
}
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
💡 这 4 道题的核心考点总结:
| 题目 | 核心问题 | 关键知识点 |
|---|---|---|
| 1 | 函数传参时指针的值传递 | 指针的传递方式(值传递 vs 地址传递) |
| 2 | 返回栈上的局部变量地址 | 栈内存的生命周期(函数结束即销毁) |
| 3 | 动态内存未释放 | malloc 与 free 必须成对使用 |
| 4 | 释放后未置空导致野指针 | 释放后立即置空的重要性 |
五、柔性数组(结构体+动态内存管理)
1. 什么是柔性数组?
在 C99 标准中,允许结构体的最后一个成员是未知大小的数组 ,这就叫柔性数组成员。它有两种常见写法:
// 写法1:部分编译器支持
struct st_type
{
int i;
int a[0]; // 柔性数组,大小为0
};
// 写法2:标准写法(兼容性更好)
struct st_type
{
int i;
int a[]; // 柔性数组,不指定大小
};
注意:柔性数组必须是结构体的最后一个成员 ,且前面至少有一个其他成员。
2. 柔性数组的核心特点
-
sizeof不包含柔性数组的大小 比如上面的struct st_type,sizeof(struct st_type)的结果是4(仅包含int i的大小),柔性数组本身不占结构体的内存空间。 -
必须用
malloc动态分配内存 柔性数组的大小是在运行时确定的 ,因此结构体必须用malloc一次性分配足够的内存 ,内存大小 = 结构体本身大小 + 柔性数组需要的额外空间。
cpp
#define _CRT_SECURE_NO_WARNINGS 1
struct S
{
int n;
int arr[];//柔性数组成员
};
int main()
{ //希望有柔性数组存放五个整型
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = 1 + i;
}
return 0;
}
3. 柔性数组的使用示例(本质是让数组可变)
1. 基础用法(代码 1)
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[0]; // 柔性数组
} type_a;
int main()
{
// 一次性分配:结构体大小 + 100个int的空间
type_a *p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
if (p == NULL)
{
perror("malloc failed");
return 1;
}
p->i = 100;
for(int i = 0; i < 100; i++)
{
p->a[i] = i; // 直接像普通数组一样访问
}
free(p); // 一次free就释放所有内存
p = NULL;
return 0;
}
- 这里**
p->a就相当于一个大小为 100 的数组** ,和结构体的内存是连续的。 - 释放时只需要**
free(p)一次**,就能释放结构体和柔性数组的所有内存。
2. 对比传统指针写法(代码 2)
不用柔性数组 ,也可以用结构体里嵌套指针的方式实现同样的功能:
#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));
if (p == NULL)
{
perror("malloc failed");
return 1;
}
p->i = 100;
// 额外给指针分配100个int的空间
p->p_a = (int *)malloc(p->i * sizeof(int));
if (p->p_a == NULL)
{
perror("malloc failed");
free(p);
p = NULL;
return 1;
}
for(int i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
// 释放时必须先释放指针指向的内存,再释放结构体本身
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
4. 柔性数组的两大优势
和上面的指针写法相比 ,柔性数组 有两个明显的好处:
1. ✅ 内存释放更简单
- 柔性数组:一次
free就能释放所有内存 (结构体和数组的内存是连续的,都在同一块malloc出来的空间里)。 - 指针写法:需要两次
free,如果用户只释放了结构体指针,忘记释放p->p_a,就会造成内存泄漏。
2. ✅ 访问速度更快,内存碎片更少
- 柔性数组的内存是连续的 ,访问数组元素时,只需要在结构体指针的基础上做偏移即可,缓存命中率更高。
- 指针写法需要两次内存分配,可能导致内存碎片化,而且访问数组时需要先解引用指针 ,再访问元素,多了一层寻址操作。
5. 注意事项
- 柔性数组不能定义在栈上 ,只能用
malloc动态分配。 - 分配的内存大小 必须大于结构体本身的大小 ,否则柔性数组没有可用空间。
- 柔性数组不能单独使用 ,必须作为结构体的最后一个成员存在。
💡 一句话总结:柔性数组就是一种在结构体末尾动态扩展连续内存的技巧,用它能让内存分配和释放更简单、高效,避免内存泄漏和碎片化问题。
六、总结C/C++中程序内存区域划分

1. 四大内存区域详解(对应图里的文字说明)
1.1. 栈区(Stack)
- 特点:由编译器自动分配释放,函数执行结束时自动回收,向下增长(从高地址往低地址扩展),空间有限。
- 存放内容 :
- 函数内的局部变量 (非
static) - 函数参数 、返回值 、返回地址
- 函数调用时的栈帧信息
- 函数内的局部变量 (非
- 对应图里的变量 :
int localVar = 1;int num1[10] = {1,2,3,4};char char2[] = "abcd";(注意:这里是数组,字符串内容会拷贝到栈上)
1.2 堆区(Heap)
- 特点 :由程序员手动分配释放 (
malloc/calloc/realloc/free),如果不释放,程序结束时可能由操作系统回收,向上增长(从低地址往高地址扩展),空间大且灵活。 - 存放内容 :动态分配的内存块
- 对应图里的变量 :
(int*)malloc(sizeof(int*4));(int*)calloc(4, sizeof(int));(int*)realloc(ptr2, sizeof(int)*4);
1.3 数据段(静态区 / 全局区)
- 特点 :存放全局变量和静态变量 ,程序结束后由系统释放。
- 细分 :
- 已初始化数据段:存放初始化过的全局 / 静态变量
- 未初始化数据段:存放未初始化的全局 / 静态变量
- 对应图里的变量 :
int globalVar = 1;(全局变量)static int staticGlobalVar = 1;(静态全局变量)static int staticVar = 1;(静态局部变量)
1.4 代码段(只读段)
- 特点:存放可执行的机器指令,还有只读常量(比如字符串常量),权限是只读,防止程序意外修改指令。
- 对应图里的变量 :
char *pChar3 = "abcd";(这里的"abcd"是字符串常量,存在代码段;pChar3本身是局部指针变量,存在栈上)
2. 图里的变量分区对照表
| 变量 | 所在内存区域 | 关键说明 |
|---|---|---|
int globalVar = 1; |
数据段 | 全局变量 |
static int staticGlobalVar = 1; |
数据段 | 静态全局变量 |
static int staticVar = 1; |
数据段 | 静态局部变量 |
int localVar = 1; |
栈区 | 普通局部变量 |
int num1[10] = {1,2,3,4}; |
栈区 | 局部数组 |
char char2[] = "abcd"; |
栈区 | 数组内容拷贝到栈上 |
char *pChar3 = "abcd"; |
pChar3 在栈区 ,"abcd" 在代码段 |
指针本身在栈,指向的字符串常量在代码段 |
int *ptr1 = malloc(...); |
ptr1 在栈区,指向的内存块在堆区 |
动态分配的内存都在堆上 |
int *ptr2 = calloc(...); |
ptr2 在栈区,指向的内存块在堆区 |
同上 |
int *ptr3 = realloc(...); |
ptr3 在栈区,指向的内存块在堆区 |
同上 |