内存管理

前面用的变量和数组,大小都是写代码时就确定的,它们存在**栈(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);                             // 手动释放
}

为什么需要堆内存?两个常见场景:

  1. 数组大小运行时才知道:比如先读入 n,再创建大小为 n 的数组
  2. 数据需要跨函数使用:栈上的变量在函数返回后就没了,堆上的不会

malloc 与 free

malloc(memory allocate)在堆上分配指定字节数的内存,返回一个指针;用完必须用 free 释放:

sizeof(int) 返回一个 int 占多少字节(通常是 4),n * sizeof(int) 就是 nint 一共需要多少字节。

calloc

callocmalloc 类似,但有两点不同:参数形式不一样,而且会自动把内存初始化为 0

在刷算法题时,callocmalloc 更方便,因为很多时候我们需要数组初始值为 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 语言中常用的数据结构,包括数组操作、字符串处理、结构体,以及用数组模拟栈和队列。