动态内存管理是 C 语言中一项核心能力,它允许程序在运行时向操作系统申请内存,而不是在编译时就固定数组大小。掌握它,意味着你能写出更灵活、更高效的程序。
一、为什么需要动态内存?
假设你要写一个程序,让用户输入学生人数,然后录入成绩:
int n; scanf("%d", &n); int scores[n]; // C99 支持变长数组,但...变长数组(VLA)虽然语法简洁,却有局限性:大小受栈空间限制,生命周期局限于所在作用域。当数据量很大(比如百万级别),或者需要在函数间传递大块数据时,堆(Heap)上的动态内存才是正确选择。
二、四大核心函数
C 标准库<stdlib.h>提供了四个关键函数:
| 函数 | 作用 |
|---|---|
malloc(size) | 申请size字节的未初始化内存 |
calloc(n, size) | 申请n * size字节,并清零 |
realloc(ptr, new_size) | 调整已申请内存的大小 |
free(ptr) | 释放内存,归还操作系统 |
三、malloc 与 free 的基本用法
#include <stdio.h> #include <stdlib.h> int main() { int n; printf("请输入学生人数: "); scanf("%d", &n); // 申请能放 n 个 int 的内存 int *scores = (int *)malloc(n * sizeof(int)); if (scores == NULL) { printf("内存申请失败!\n"); return 1; } // 输入成绩 for (int i = 0; i < n; i++) { printf("第 %d 个学生成绩: ", i + 1); scanf("%d", &scores[i]); } // 计算平均分 int sum = 0; for (int i = 0; i < n; i++) { sum += scores[i]; } printf("平均分: %.2f\n", (double)sum / n); // 释放内存 free(scores); scores = NULL; // 好习惯:防止野指针 return 0; }关键点:
malloc返回void*,通常强制转换为所需类型指针。- 必须检查返回值是否为
NULL,申请失败时不能继续使用。 free后把指针置为NULL,避免后续误用造成难以定位的崩溃。
四、calloc:带清零的内存申请
如果你需要一块全为 0 的内存,calloc比malloc更方便:
int *arr = (int *)calloc(100, sizeof(int)); // 等价于 malloc(100 * sizeof(int)) + memset(arr, 0, 100 * sizeof(int))calloc的两个参数分别是元素个数和每个元素的大小,语义更清晰,且自动清零,适合初始化数组或结构体。
五、realloc:灵活调整大小
动态内存的优势在于"动态"。如果数组满了,可以用realloc扩容:
#include <stdio.h> #include <stdlib.h> int main() { int capacity = 4; // 初始容量 int count = 0; // 当前元素个数 int *arr = (int *)malloc(capacity * sizeof(int)); int num; printf("输入数字(-1 结束):\n"); while (scanf("%d", &num) == 1 && num != -1) { if (count >= capacity) { capacity *= 2; // 容量翻倍 int *tmp = (int *)realloc(arr, capacity * sizeof(int)); if (tmp == NULL) { printf("扩容失败!\n"); free(arr); return 1; } arr = tmp; printf("扩容至 %d\n", capacity); } arr[count++] = num; } printf("共录入 %d 个数: ", count); for (int i = 0; i < count; i++) { printf("%d ", arr[i]); } printf("\n"); free(arr); return 0; }注意:realloc可能将数据迁移到新地址,所以要用临时指针接收返回值,确认非空后再赋值给原指针。否则一旦realloc失败,原内存地址会丢失,造成内存泄漏。
六、常见错误与避坑指南
1. 内存泄漏:申请了却不释放
void leak() { int *p = malloc(100); // 函数结束,p 被销毁,但 100 字节仍在堆上,无法访问也无法释放 }解决:确保每条malloc/calloc/realloc路径都有对应的free。
2. 使用已释放的内存(Use-After-Free)
free(p); printf("%d\n", *p); // 危险!p 指向的内存已归还系统解决:free后立即将指针置NULL。
3. 重复释放(Double Free)
free(p); free(p); // 未定义行为,可能崩溃解决:同样,释放后置NULL,free(NULL)是安全的,什么都不会发生。
4.malloc与free不匹配
不要用free释放非动态申请的内存(如栈上的局部数组),也不要用delete(那是 C++ 的)。
七、二维数组的动态分配
有时你需要动态的矩阵。一种直观的方式是指针的指针:
#include <stdio.h> #include <stdlib.h> int main() { int rows = 3, cols = 4; // 申请行指针数组 int **matrix = (int **)malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) { matrix[i] = (int *)malloc(cols * sizeof(int)); } // 使用 matrix[1][2] = 100; printf("matrix[1][2] = %d\n", matrix[1][2]); // 释放:先释放每行,再释放行指针数组 for (int i = 0; i < rows; i++) { free(matrix[i]); } free(matrix); return 0; }这种方式每行内存不一定连续。如果需要物理上连续的一块内存(利于缓存友好),可以一次性申请rows * cols大小,然后通过宏或手动计算索引访问。
八、总结
| 要点 | 说明 |
|---|---|
malloc | 申请未初始化内存,记得检查NULL |
calloc | 申请并清零,适合数组初始化 |
realloc | 扩容/缩容,用临时指针接收结果 |
free | 配对释放,释放后置NULL |
| 核心原则 | 谁申请,谁释放;申请必检查,释放防野指针 |
动态内存管理是 C 语言强大的根源之一,也是最容易出错的地方。理解堆与栈的区别,养成"申请-检查-使用-释放"的完整闭环习惯,你的程序会更加健壮。