前面用的变量和数组,大小都是写代码时就确定的,它们存在**栈(stack)**上,函数结束就自动释放。
但有时候数组大小要到运行时才知道,这就需要在**堆(heap)**上手动分配内存。这篇讲的就是 C 语言的手动内存管理。
栈内存与堆内存
先搞清楚这两种内存的区别:
| 栈(Stack) | 堆(Heap) | |
|---|---|---|
| 分配方式 | 自动(声明变量时) | 手动(调用 malloc) |
| 释放方式 | 自动(函数返回时) | 手动(调用 free) |
| 大小限制 | 较小(通常几 MB) | 较大(取决于系统内存) |
| 速度 | 快 | 稍慢 |
| 典型用途 | 局部变量、函数参数 | 运行时动态大小的数组 |
一个简单的例子:
void foo() {
int a = 10; // 栈上,函数结束自动释放
int arr[100]; // 栈上,函数结束自动释放
int* p = malloc(100 * sizeof(int)); // 堆上,需要手动 free
free(p); // 手动释放
}为什么需要堆内存?两个常见场景:
- 数组大小运行时才知道:比如先读入
n,再创建大小为n的数组 - 数据需要跨函数使用:栈上的变量在函数返回后就没了,堆上的不会
malloc 与 free
malloc(memory allocate)在堆上分配指定字节数的内存,返回一个指针;用完必须用 free 释放:
sizeof(int) 返回一个 int 占多少字节(通常是 4),n * sizeof(int) 就是 n 个 int 一共需要多少字节。
calloc
calloc 和 malloc 类似,但有两点不同:参数形式不一样,而且会自动把内存初始化为 0:
在刷算法题时,calloc 比 malloc 更方便,因为很多时候我们需要数组初始值为 0。
realloc
realloc 用来调整已分配内存的大小,相当于「扩容」。一个典型场景:不知道输入有多少个数,需要边读边扩容:
realloc 会保留原有数据,如果新大小更大,多出来的部分是未初始化的。上面这个「容量不够就翻倍」的模式,其实就是动态数组(如 C++ 的 vector)的底层实现原理。
memset 与 memcpy
这两个函数在算法题中非常常用:
注意:memset 是按字节设置的,对 int 数组只有设 0 和 -1 是安全的。设成其他值(比如 memset(arr, 1, ...))不会得到你想要的结果,因为它会把每个字节都设为 1,而不是把每个 int 设为 1。
动态分配二维数组
二维数组的动态分配需要两步:先分配行指针数组(int**),再逐行分配内存:
释放顺序很重要:先释放每一行,再释放行指针数组。如果先释放了行指针数组,每一行的地址就丢了,那些内存就再也释放不了了(内存泄漏)。
常见内存错误
手动管理内存容易出错,这里列几个最常踩的坑:
内存泄漏:malloc 了没 free
void leak() {
int* p = (int*)malloc(100 * sizeof(int));
// 函数结束,指针 p 被销毁,但它指向的那块堆内存没人释放
// 这块内存就泄漏了,程序再也无法回收它
}规则:每个 malloc/calloc 都必须有对应的 free。
野指针:free 了还在用
int* p = (int*)malloc(sizeof(int));
*p = 42;
free(p); // 内存已释放
printf("%d", *p); // 危险!p 现在是野指针free 后内存被回收了,但 p 里还存着那个地址。通过它访问已释放的内存是未定义行为,可能崩溃,也可能读到垃圾值。
好习惯:free(p) 之后立即 p = NULL;。这样后续如果不小心用到,至少能通过 if (p != NULL) 拦住。
重复释放
int* p = (int*)malloc(sizeof(int));
free(p);
free(p); // 重复释放!未定义行为,通常导致崩溃同一块内存只能 free 一次。free 后置 NULL 也能防止这个问题(free(NULL) 是安全的,什么都不做)。
越界访问
int* arr = (int*)malloc(5 * sizeof(int));
arr[5] = 100; // 越界!只分配了下标 0~4
free(arr);越界访问不一定立即崩溃,但可能悄悄破坏其他数据,导致非常难排查的 bug。
小结
| 函数 | 作用 | 特点 |
|---|---|---|
malloc(size) | 分配 size 字节 | 内容未初始化(垃圾值) |
calloc(n, size) | 分配 n × size 字节 | 自动初始化为 0 |
realloc(ptr, size) | 调整已分配内存大小 | 保留原有数据 |
free(ptr) | 释放内存 | 必须与 malloc/calloc 配对 |
memset(ptr, val, size) | 按字节填充内存 | 对 int 只有 0 和 -1 安全 |
memcpy(dest, src, size) | 复制 size 字节 | dest 和 src 不能重叠 |
黄金法则:有 malloc 就有 free,先释放子元素再释放父元素,free 之后置 NULL。
下一篇我们来学 C 语言中常用的数据结构,包括数组操作、字符串处理、结构体,以及用数组模拟栈和队列。