一、写在前面
上一篇讲了指针和结构体,这一篇我们聊动态内存管理。
为什么数据结构离不开动态内存?因为大部分数据结构的大小不是固定的。一个链表,你无法提前知道用户要存多少个数据,只能在程序运行时根据需要动态申请内存。
C语言里,动态内存的申请和释放全靠四个函数:malloc、calloc、realloc、free。用不好,轻则内存泄漏,重则程序崩溃。
二、四个核心函数
这四个函数的头文件都是 <stdlib.h>。
2.1 malloc:申请内存
c
void *malloc(size_t size);
-
参数:要申请的字节数
-
返回值:申请到的内存地址(void*类型),失败返回NULL
c
int *p = (int*)malloc(5 * sizeof(int));
if (p == NULL) {
printf("内存申请失败\n");
return -1;
}
// 现在可以用p了,就像一个长度为5的int数组
p[0] = 10;
p[1] = 20;
注意:
-
malloc申请的内存不会初始化,里面存的是垃圾值
-
要自己计算字节数,
5 * sizeof(int)才是5个int需要的空间 -
一定要判断返回值是否为NULL
2.2 calloc:申请并清零
c
void *calloc(size_t nmemb, size_t size);
-
参数1:元素个数
-
参数2:每个元素的大小
-
返回值:申请到的内存地址,失败返回NULL
c
int *p = (int*)calloc(5, sizeof(int));
// 等价于 malloc(5 * sizeof(int)),但内存会被初始化为0
// p[0]到p[4]都是0
calloc 比 malloc 多了一步清零操作。如果你需要初始化为0,用 calloc 更方便。如果不需要清零,malloc 更快。
2.3 realloc:调整内存大小
c
void *realloc(void *ptr, size_t new_size);
-
参数1:之前申请的内存地址
-
参数2:新的字节数
-
返回值:新内存的地址(可能和原来不同),失败返回NULL
这个函数很常用,比如动态数组扩容的时候。
c
int *p = (int*)malloc(5 * sizeof(int));
// 想扩容到10个int
int *temp = (int*)realloc(p, 10 * sizeof(int));
if (temp == NULL) {
// 扩容失败,原来的内存还在,要小心处理
free(p);
return -1;
}
p = temp;
// 现在p有10个int的空间
关键点:
-
realloc 可能移动内存(如果原位置后面空间不够),所以要用新指针接收返回值
-
扩容失败时返回NULL,原来的内存不会被释放,所以要先用临时变量接,判断成功再赋值
-
如果
ptr是NULL,realloc 等价于 malloc
2.4 free:释放内存
c
void free(void *ptr);
申请的动态内存,不用了必须释放。
c
int *p = (int*)malloc(10 * sizeof(int));
// 使用p...
free(p); // 释放
p = NULL; // 建议置NULL,防止野指针
为什么一定要 free?
-
程序不会自动释放动态内存(除非程序结束)
-
不释放会导致内存泄漏,长时间运行的程序(比如服务器)会耗尽内存
释放后的注意事项:
-
free 之后,指针变量还保存着原来的地址,但这个地址已经不能用了(野指针)
-
再次使用这个指针会导致未定义行为,可能崩溃
-
习惯:free 之后立即置 NULL
三、常见错误与内存泄漏
3.1 内存泄漏示例
c
void func() {
int *p = (int*)malloc(100 * sizeof(int));
// 忘记写 free(p);
return; // p被销毁了,但内存没释放,泄漏了
}
每次调用这个函数,就漏掉400字节(假设int4字节)。调用一万次,漏4MB。
3.2 重复释放
c
free(p);
free(p); // 错误!重复释放,程序可能崩溃
3.3 使用已释放的内存
c
free(p);
*p = 10; // 错误!p指向的内存已经还给系统了
3.4 内存泄漏检测
写数据结构代码时,可以用一个简单的方法:写一个函数打印当前已分配但未释放的内存数量。或者用 Valgrind 工具(Linux下):
bash
valgrind --leak-check=full ./你的程序
它会告诉你哪里申请的内存没有释放。
四、typedef 深入:简化结构体定义
上一篇简单提了 typedef,这一篇详细说说它在数据结构里的用法。
4.1 基本用法
c
// 定义结构体
typedef struct Node {
int data;
struct Node *next;
} Node;
// 现在可以这样用
Node n1; // 不用写 struct Node
Node *p = &n1;
4.2 同时定义指针类型
c
typedef struct Node {
int data;
struct Node *next;
} Node, *PNode;
// Node 是结构体类型
// PNode 是 Node* 类型
Node n1;
PNode p = &n1; // p 是 Node* 类型
这样写的好处是代码更简洁,尤其是函数参数:
c
// 不用typedef
void insert(struct Node *head, int data);
// 用typedef
void insert(PNode head, int data);
4.3 在数据结构代码中的标准写法
后面写链表、栈、队列时,我通常这样写:
c
// 节点类型
typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域
} Node, *PNode;
// 链表结构(如果需要)
typedef struct LinkedList {
PNode head; // 头指针
int size; // 节点个数
} LinkedList, *PList;
这样做的好处:
-
代码清晰,一眼看出类型含义
-
减少了
struct关键字的重复 -
后续修改结构体内部时,接口不变
五、综合例子:动态数组的简单实现
把 malloc 和 realloc 结合起来,实现一个可以自动扩容的数组:
c
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data; // 数据指针
int size; // 当前元素个数
int capacity; // 总容量
} DynamicArray;
// 初始化
void initArray(DynamicArray *arr, int initCapacity) {
arr->data = (int*)malloc(initCapacity * sizeof(int));
arr->size = 0;
arr->capacity = initCapacity;
}
// 添加元素
void push(DynamicArray *arr, int value) {
// 如果满了,扩容
if (arr->size >= arr->capacity) {
int newCapacity = arr->capacity * 2;
int *temp = (int*)realloc(arr->data, newCapacity * sizeof(int));
if (temp == NULL) {
printf("扩容失败\n");
return;
}
arr->data = temp;
arr->capacity = newCapacity;
printf("扩容到 %d\n", arr->capacity);
}
arr->data[arr->size] = value;
arr->size++;
}
// 打印
void printArray(DynamicArray *arr) {
printf("size=%d, capacity=%d, data=", arr->size, arr->capacity);
for (int i = 0; i < arr->size; i++) {
printf("%d ", arr->data[i]);
}
printf("\n");
}
// 释放内存
void destroyArray(DynamicArray *arr) {
free(arr->data);
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
}
int main() {
DynamicArray arr;
initArray(&arr, 3);
// 添加6个元素,会触发扩容
for (int i = 1; i <= 6; i++) {
push(&arr, i * 10);
printArray(&arr);
}
destroyArray(&arr);
return 0;
}
输出:
text
size=1, capacity=3, data=10
size=2, capacity=3, data=10 20
size=3, capacity=3, data=10 20 30
扩容到 6
size=4, capacity=6, data=10 20 30 40
size=5, capacity=6, data=10 20 30 40 50
size=6, capacity=6, data=10 20 30 40 50 60
这个例子演示了:
-
malloc 初始分配
-
realloc 动态扩容
-
free 释放内存
-
typedef 简化结构体定义
六、小结
| 函数 | 作用 | 注意事项 |
|---|---|---|
| malloc | 申请内存 | 不初始化,要判断NULL |
| calloc | 申请并清零 | 比malloc多一步清零 |
| realloc | 调整内存大小 | 用临时变量接返回值,失败原内存还在 |
| free | 释放内存 | 释放后置NULL,避免重复释放 |
动态内存管理的核心原则:
-
谁申请,谁释放
-
释放后指针置NULL
-
每次malloc/realloc都要判断返回值
-
用工具检测内存泄漏
七、思考题
- 下面的代码有什么问题?
c
int *p = (int*)malloc(10 * sizeof(int));
p = (int*)realloc(p, 20 * sizeof(int));
free(p);
-
为什么链表节点要用 malloc 动态创建,不能直接定义局部变量?
-
写一个函数,接收一个整数指针和长度,给数组动态扩容到原来的2倍。
欢迎在评论区讨论你的答案。