动态内存管理
目录
- 为什么要有动态内存分配
- [malloc 和 free](#malloc 和 free)
- [calloc 和 realloc](#calloc 和 realloc)
- 常见的动态内存的错误
- 动态内存经典笔试题分析
- 柔性数组
- [总结 C/C++ 中程序内存区域划分](#总结 C/C++ 中程序内存区域划分)
1. 为什么要有动态内存分配
我们已经掌握的内存开辟方式有:
c
int val = 20; // 在栈空间上开辟四个字节(固定的向内存申请4个字节)
char arr[10] = {0}; // 在栈空间上开辟10个字节的连续空间(申请连续的一块空间,40字节)
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。
有时候我们需要的空间大小在程序运行的时候才能知道 ,那数组的编译时开辟空间的方式就不能满足了。
C 语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
示例:为什么需要动态内存
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入需要存储的学生人数:");
scanf("%d", &n);
// ❌ 错误做法:大小在编译时必须是常量(C89标准不支持VLA)
// int arr[n]; // 某些编译器不支持
// ✅ 正确做法:运行时动态申请
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("内存申请失败!\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
printf("%d ", arr[i]);
}
printf("\n");
free(arr); // 使用完毕后释放
arr = NULL;
return 0;
}
2. malloc 和 free
2.1 malloc
C 语言提供了一个动态内存开辟的函数:
c
void* malloc(size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败 ,则返回一个
NULL指针,因此 malloc 的返回值一定要做检查。 - 返回值的类型是
void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size为 0,malloc 的行为是标准未定义的,取决于编译器。
2.2 free
C 语言提供了另外一个函数 free,专门用来做动态内存的释放和回收的:
c
void free(void* ptr);
- 如果参数
ptr指向的空间不是动态开辟的,那 free 函数的行为是未定义的。 - 如果参数
ptr是NULL指针,则函数什么事都不做。 malloc和free都声明在stdlib.h头文件中。
2.3 malloc 与 free 使用示例
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
// int arr[num] = {0}; // 变长数组,不推荐(C89不支持)
int *ptr = NULL;
ptr = (int *)malloc(num * sizeof(int)); // 动态申请 num 个 int 大小的空间
if (NULL != ptr) // 判断 ptr 指针是否为空
{
int i = 0;
for (i = 0; i < num; i++)
{
*(ptr + i) = i; // 等价于 ptr[i] = i;
printf("%d ", ptr[i]);
}
printf("\n");
}
else
{
printf("内存申请失败!\n");
return 1;
}
free(ptr); // 释放 ptr 所指向的动态内存
ptr = NULL; // 将指针置为 NULL,避免野指针(有必要!)
return 0;
}
注意:
free(ptr)之后,ptr仍然保存着原来的地址(野指针),因此要手动置为NULL。
3. calloc 和 realloc
3.1 calloc
C 语言还提供了一个函数叫 calloc,也用来动态内存分配:
c
void* calloc(size_t num, size_t size);
- 函数的功能是为
num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为 0。 - 与函数
malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全 0。
示例
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int *)calloc(10, sizeof(int)); // 申请10个int,自动初始化为0
if (NULL != p)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i)); // 输出:0 0 0 0 0 0 0 0 0 0
}
printf("\n");
}
free(p);
p = NULL;
return 0;
}
输出结果:
0 0 0 0 0 0 0 0 0 0
如果对申请的内存空间的内容要求初始化,可以使用
calloc函数来完成任务。
malloc vs calloc 对比
| 函数 | 初始化 | 参数 | 用途 |
|---|---|---|---|
malloc(size) |
不初始化(随机值) | 总字节数 | 快速申请内存 |
calloc(num, size) |
全部初始化为 0 | 元素个数 + 单元大小 | 需要初始值为0时使用 |
c
// 等价写法对比
int *p1 = (int *)malloc(10 * sizeof(int)); // 不初始化
int *p2 = (int *)calloc(10, sizeof(int)); // 自动初始化为0
// 若要用 malloc 实现 calloc 的效果,需要额外调用 memset
memset(p1, 0, 10 * sizeof(int));
3.2 realloc
realloc 函数可以对动态开辟内存的大小进行灵活调整:
c
void* realloc(void* ptr, size_t size);
ptr是要调整的内存地址size是调整之后的新大小- 返回值为调整之后的内存起始位置
- 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc 的两种情况
情况1:原有空间之后有足够大的空间
ptr ──→ [ 已分配 20byte ][ 尚未分配(空闲) ]
扩容后: [ 已分配 20byte ][ 新增空间 ]
(直接追加,ptr 不变,数据不发生变化)
情况2:原有空间之后没有足够大的空间
ptr ──→ [ 已分配 20byte ][ 已分配(被占用) ]
在堆的其他位置另找空间:
newptr ──→ [ 复制的20byte + 新增空间 ]
(函数返回新地址,原空间自动释放)
realloc 正确使用方式
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int *)malloc(100); // 申请100字节
if (ptr != NULL)
{
// 业务处理...
}
else
{
return 1;
}
// ❌ 代码1:直接将 realloc 的返回值放到 ptr 中(有风险!)
// ptr = (int *)realloc(ptr, 1000);
// 如果申请失败返回 NULL,ptr 变为 NULL,原来的内存地址丢失,造成内存泄漏!
// ✅ 代码2:先用临时变量保存返回值,判断后再赋值
int *p = NULL;
p = (int *)realloc(ptr, 1000);
if (p != NULL)
{
ptr = p; // 确认申请成功后再更新 ptr
}
else
{
printf("扩容失败,原数据仍然可用!\n");
}
// 业务处理...
free(ptr);
ptr = NULL;
return 0;
}
realloc 特殊用法:当 ptr 为 NULL 时,等同于 malloc
c
// 当第一个参数为 NULL 时,realloc 等同于 malloc
int *p = (int *)realloc(NULL, 100 * sizeof(int));
// 等价于
int *p = (int *)malloc(100 * sizeof(int));
4. 常见的动态内存的错误
4.1 对 NULL 指针的解引用操作
c
void test()
{
int *p = (int *)malloc(INT_MAX / 4); // 申请极大空间,极可能失败返回 NULL
*p = 20; // ❌ 如果 p 的值是 NULL,就会有问题(崩溃)
free(p);
}
// ✅ 正确做法:
void test_correct()
{
int *p = (int *)malloc(INT_MAX / 4);
if (p == NULL) {
perror("malloc failed");
return;
}
*p = 20;
free(p);
p = NULL;
}
4.2 对动态开辟空间的越界访问
c
void test()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int)); // 申请10个int
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++) // ❌ i <= 10,当 i == 10 时越界访问
{
*(p + i) = i;
}
free(p);
}
// ✅ 正确做法:
void test_correct()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int));
if (NULL == p) exit(EXIT_FAILURE);
for (i = 0; i < 10; i++) // ✅ 严格 < 10
{
*(p + i) = i;
}
free(p);
p = NULL;
}
4.3 对非动态开辟内存使用 free 释放
c
void test()
{
int a = 10;
int *p = &a; // p 指向栈上的变量
free(p); // ❌ 对非堆内存调用 free,行为未定义(崩溃)
}
// ✅ 只能 free 由 malloc/calloc/realloc 申请的内存
4.4 使用 free 释放一块动态开辟内存的一部分
c
void test()
{
int *p = (int *)malloc(100);
p++; // p 指针向后偏移了一个 int 的大小
free(p); // ❌ p 不再指向动态内存的起始位置,行为未定义
}
// ✅ 正确做法:
void test_correct()
{
int *p = (int *)malloc(100);
int *start = p; // 保存起始地址
p++; // 可以移动 p 进行操作
// ...
free(start); // 释放起始地址
start = NULL;
}
4.5 对同一块动态内存多次释放
c
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); // ❌ 重复释放,行为未定义(可能崩溃)
}
// ✅ 正确做法:free 后立即置 NULL
void test_correct()
{
int *p = (int *)malloc(100);
free(p);
p = NULL; // 置为 NULL 后,再次 free(NULL) 是安全的(什么都不做)
free(p); // 安全,free(NULL) 不会出错
}
4.6 动态开辟内存忘记释放(内存泄漏)
c
void test()
{
int *p = (int *)malloc(100); // 申请了内存
if (NULL != p)
{
*p = 20;
}
// ❌ 函数结束,没有调用 free(p)!
// p 是局部变量,函数结束后 p 消失,但堆上的内存没有被释放
}
int main()
{
test();
while (1); // 程序持续运行,内存始终无法回收 → 内存泄漏
}
// ✅ 正确做法:
void test_correct()
{
int *p = (int *)malloc(100);
if (NULL != p)
{
*p = 20;
// 使用完毕后释放
free(p);
p = NULL;
}
}
切记:动态开辟的空间一定要释放,并且正确释放。
5. 动态内存经典笔试题分析
5.1 题目1
c
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world"); // ❌ str 仍为 NULL,程序崩溃
printf(str);
}
分析:
GetMemory 函数接收的是 str 的值 (即 NULL),p 是 str 的拷贝,函数内部修改 p 不影响 str。
函数执行完后,str 依然是 NULL,strcpy 对 NULL 指针解引用导致程序崩溃 。
另外,在函数内部 malloc 的内存没有 free,造成内存泄漏。
修正方案一:传二级指针
c
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str); // 传地址
if (str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str); // 记得释放
str = NULL;
}
}
修正方案二:返回值传递
c
char *GetMemory(void)
{
char *p = (char *)malloc(100);
return p;
}
void Test(void)
{
char *str = GetMemory(); // 通过返回值获取地址
if (str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
}
5.2 题目2
c
char *GetMemory(void)
{
char p[] = "hello world"; // p 是栈上的局部数组
return p; // ❌ 返回栈上局部变量的地址
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str); // ❌ 打印的是"野指针",行为未定义(可能乱码,可能崩溃)
}
分析:
p 是局部数组,存在于 GetMemory 函数的栈帧上。函数返回后,栈帧被销毁,p 数组的内存可能被其他内容覆盖。
str 拿到的是一个野指针 ,printf 的结果是未定义行为(乱码或崩溃)。
修正:使用 static 或 malloc
c
// 方案1:static 修饰(存于静态区,生命周期是整个程序)
char *GetMemory(void)
{
static char p[] = "hello world";
return p;
}
// 方案2:malloc(存于堆区)
char *GetMemory(void)
{
char *p = (char *)malloc(100);
strcpy(p, "hello world");
return p; // 调用者负责 free
}
5.3 题目3
c
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num); // 通过二级指针修改调用者的指针
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str); // 输出:hello
// ❌ 没有 free,内存泄漏
}
分析:
此处传入的是 str 的地址(&str),所以 *p = malloc(num) 等于 str = malloc(100),正确地将申请到的内存地址赋给了 str。
程序可以正常打印 hello,但忘记 free,存在内存泄漏。
修正:
c
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str); // ✅ 释放内存
str = NULL;
}
5.4 题目4
c
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str); // 释放了内存
if (str != NULL) // ⚠️ str 不为 NULL(free 不会改变指针值)
{
strcpy(str, "world"); // ❌ 访问已释放的内存(野指针),行为未定义
printf(str);
}
}
分析:
free(str) 释放了内存,但 str 的值并没有变成 NULL,仍然保存着原来的地址(此时是野指针)。
if (str != NULL) 条件成立,继续对野指针操作,行为未定义(可能崩溃,可能输出乱码)。
修正:free 后立即置 NULL
c
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL; // ✅ free 后立即置 NULL
if (str != NULL) // 此时条件不成立,不会进入 if
{
strcpy(str, "world");
printf(str);
}
}
6. 柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组 ,这就叫做「柔性数组」成员。
c
struct st_type
{
int i;
int a[0]; // 柔性数组成员(有些编译器可能报错)
};
// 有些编译器需要写成:
struct st_type
{
int i;
int a[]; // 柔性数组成员(更常用写法)
};
6.1 柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其他成员。
sizeof返回的这种结构大小不包括柔性数组的内存。- 包含柔性数组成员的结构用
malloc()函数进行内存的动态分配,分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
c
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[]; // 柔性数组成员
} type_a;
int main()
{
printf("%d\n", (int)sizeof(type_a)); // 输出:4(只算 int i 的大小)
return 0;
}
6.2 柔性数组的使用(代码1)
c
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[]; // 柔性数组成员
} type_a;
int main()
{
int j = 0;
// 申请结构体大小 + 100个int的空间
type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int));
if (p == NULL) {
perror("malloc");
return 1;
}
p->i = 100;
for (j = 0; j < 100; j++)
{
p->a[j] = j; // 柔性数组成员 a 相当于获得了100个整型元素的连续空间
}
printf("i = %d\n", p->i);
printf("a[0]=%d, a[99]=%d\n", p->a[0], p->a[99]);
free(p); // 只需要 free 一次
p = NULL;
return 0;
}
6.3 柔性数组的优势
上述 type_a 结构也可以设计为下面的结构,也能完成同样的效果(代码2):
c
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int *p_a; // 用指针代替柔性数组
} type_a;
int main()
{
int j = 0;
type_a *p = (type_a *)malloc(sizeof(type_a));
if (p == NULL) return 1;
p->i = 100;
p->p_a = (int *)malloc(p->i * sizeof(int)); // 第二次 malloc
if (p->p_a == NULL) {
free(p);
return 1;
}
for (j = 0; j < 100; j++)
{
p->p_a[j] = j;
}
// 释放空间(需要两次 free)
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
柔性数组(代码1)相比指针方案(代码2)的两个好处
好处1:方便内存释放
代码1(柔性数组):只需 free(p) 一次
代码2(指针): 需要先 free(p->p_a),再 free(p),顺序不能错
如果将结构体返回给调用者,调用者只需调用一次 free 即可释放所有内存,不需要了解内部结构。
好处2:有利于访问速度
代码1:结构体和数组数据在连续的内存中
[ i | a[0] | a[1] | ... | a[99] ]
└──────────── 连续内存 ──────────┘
代码2:结构体和数组数据分散在两块内存中
[ i | p_a ──────────────────→ [ a[0] | a[1] | ... | a[99] ] ]
└── 一块 ──┘ └───────── 另一块 ────────────┘
连续的内存有益于提高访问速度,也有益于减少内存碎片(减少缓存 miss)。
7. 总结 C/C++ 中程序内存区域划分
内存分区示意图
高地址
┌─────────────────────────────┐
│ 内核空间 │ ← 用户代码不能读写
├─────────────────────────────┤
│ 栈区(stack) ↓ │ ← 向低地址增长
│ │
│ (空闲区域) │
│ │
│ 堆区(heap) ↑ │ ← 向高地址增长
├─────────────────────────────┤
│ 内存映射段(mmap) │ ← 文件映射、动态库、匿名映射
├─────────────────────────────┤
│ 数据段(静态区) │ ← 全局变量、静态变量
├─────────────────────────────┤
│ 代码段(只读) │ ← 函数二进制代码、只读常量
└─────────────────────────────┘
低地址
各区域说明
| 区域 | 内容 | 管理方式 | 生命周期 |
|---|---|---|---|
| 栈区 (stack) | 局部变量、函数参数、返回地址 | 自动管理(编译器) | 函数调用时创建,返回时销毁 |
| 堆区 (heap) | malloc/calloc/realloc 申请的内存 |
程序员手动管理 | 直到 free 或程序结束 |
| 数据段 (静态区) | 全局变量、static 变量 |
操作系统 | 程序整个生命周期 |
| 代码段 | 函数体的二进制代码、字符串常量 | 只读 | 程序整个生命周期 |
代码示例:不同变量位于不同区域
c
#include <stdio.h>
#include <stdlib.h>
int globalVar = 1; // 数据段(全局初始化变量)
static int staticGlobalVar = 1; // 数据段(静态全局变量)
void test()
{
static int staticVar = 1; // 数据段(静态局部变量,只初始化一次)
int localVar = 1; // 栈区(局部变量)
int num1[10] = {1, 2, 3, 4};// 栈区(局部数组)
char char2[] = "abcd"; // 栈区("abcd"复制到栈上的数组中)
char *pChar3 = "abcd"; // pChar3 在栈区,"abcd" 字符串在代码段(只读)
int *ptr1 = (int *)malloc(4 * sizeof(int)); // ptr1 在栈,指向堆区
int *ptr2 = (int *)calloc(4, sizeof(int)); // ptr2 在栈,指向堆区
int *ptr3 = (int *)realloc(ptr2, 4 * sizeof(int)); // ptr3 在栈,指向堆区
free(ptr1); // 释放堆内存
free(ptr3); // 释放堆内存(ptr2 已由 realloc 处理)
}
int main()
{
test();
return 0;
}
关键区别:栈 vs 堆
c
// 栈上数组:大小必须在编译期确定,自动释放,速度快,空间有限(约1-8MB)
int arr[10]; // ✅ 编译期大小确定
// int arr[n]; // ⚠️ VLA,C99支持,但不推荐
// 堆上内存:大小可以在运行期确定,手动释放,空间大(受物理内存限制)
int *arr = (int *)malloc(n * sizeof(int)); // ✅ 运行期确定大小
// 使用后必须 free
free(arr);
arr = NULL;
总结
| 函数 | 头文件 | 功能 | 初始化 | 注意事项 |
|---|---|---|---|---|
malloc(size) |
stdlib.h |
申请指定字节数的内存 | 不初始化 | 返回值需判空 |
free(ptr) |
stdlib.h |
释放动态申请的内存 | --- | free后置NULL |
calloc(n, size) |
stdlib.h |
申请n个元素内存 | 初始化为0 | 返回值需判空 |
realloc(ptr, size) |
stdlib.h |
调整已申请内存大小 | 不初始化新增部分 | 用临时变量接收返回值 |
动态内存使用六大禁忌:
- 对 NULL 指针解引用
- 越界访问动态数组
- free 非堆内存
- free 非起始地址
- 重复 free
- 忘记 free(内存泄漏)