Menu

C 语言 malloc:动态内存分配、free 和 sizeof

堆为什么存在,malloc 如何给你一块在运行时才确定大小的内存,p = malloc(n * sizeof *p) 惯用写法,检查 NULL,只释放一次,以及一个从头到尾完整实现的动态数组。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

到目前为止,你写过的每个数组,大小都是在编译时就定死的:int scores[10]; 会预留十个位置,不管你需要的是三个还是三百个。只要大小在程序运行时才能知道,比如一个文件有多少行、用户会输入多少条记录,这种写法就行不通了。动态内存解决了这个问题:在需要的时候申请需要的字节数,用完后再还回去。

相关的工具都在 <stdlib.h> 中:

#include <stdlib.h>

为什么需要堆

C 语言给程序提供了两块主要的内存区域。局部变量位于自动存储区(栈):它们的大小由编译器决定,所在函数一返回就会消失。动态分配的内存位于堆:大小由你在运行时决定,这块内存会一直有效,直到你显式释放它。

第二个特性才是使用 malloc 的真正原因。函数可以分配一块内存,返回指针,而这块内存在调用者那里依然有效,局部数组永远做不到这一点。两块区域的详细对比见栈与堆。

malloc:申请字节

malloc 接收一个字节数,返回一个 void *,指向这么多字节的未初始化内存;如果请求失败,就返回 NULL。

这段短小的程序里有四个要点,它们就是全部的规矩:

  1. 大小表达式是 n * sizeof *scores。 把它读作“n 个 scores 所指向的东西”。
  2. 在任何使用之前,先检查结果是否为 NULL。
  3. 这块内存的用法和数组完全一样:scores[i] 之所以可行,是因为下标运算本质上就是指针运算。
  4. 只释放一次,然后把指针设为 NULL。

注意这里没有对返回值做强制类型转换。在 C 语言中,void * 会自动转换为任何对象指针类型,写成 (int *)malloc(...) 只会增加干扰,还会掩盖遗漏 <stdlib.h> 头文件的问题。(C++ 要求这个转换,C 不需要。)

sizeof 惯用写法

为什么用 sizeof *scores 而不是 sizeof(int)?因为它永远不会过时。

int  *a = malloc(n * sizeof *a);     /* n ints, whatever int is here */
long *b = malloc(n * sizeof *b);     /* same line shape, right size */

/* the fragile form */
long *c = malloc(n * sizeof(int));   /* compiles, allocates too little */

最后一行是一个真实存在的 bug,而且没有任何编译器会警告:c 是 long *,分配的大小却是按 int 计算的,后半部分的每次写入都会越界。用 sizeof *c,大小就会自动跟随声明。

sizeof *p 并不会解引用 p:sizeof 在编译时只根据类型求值,所以即使 p 未初始化或为 NULL 也是安全的。

未初始化的内存

malloc 不会清空它给你的内存。这些字节里保存的是之前留下的任意内容。

如果你希望内存自动清零,calloc 可以一步完成,参见 calloc 和 realloc。

free:归还内存

free(p) 把这块内存还给分配器。三条规则:

  • 传入 malloc 返回的原始指针,而不是移动过的指针。free(p + 1) 是未定义行为。
  • 每块内存只释放一次。释放两次就是重复释放(double free),会破坏分配器自己的记录。
  • 释放之后,指针就失效了。继续使用它就是释放后使用(use-after-free)。
int *p = malloc(sizeof *p);
free(p);
*p = 5;        /* use-after-free - undefined behavior */
free(p);       /* double free - undefined behavior */

这两行都不一定会立即崩溃,这正是它们危险的地方:程序可能运行好几分钟,然后在毫不相关的地方出错。最省事的防御办法是把指针清空:

free(p);
p = NULL;

C 标准明确规定 free(NULL) 什么也不做,所以之后再调用一次 free(p) 也无害,而 *p 会在真正出错的位置立即崩溃,而不是悄悄破坏数据。

一个完整的动态数组

这就是真实分配的样子:在运行时读取数量,按它确定内存大小,使用,然后释放。

当大小变大时,把数量转换为 size_t 就很重要了:如果 n 是普通的 int,n * sizeof *values 可能在乘积传给 malloc 之前就溢出,得到一块比预期小得多的内存。用 size_t 做乘法可以避免这个问题。

在函数内部分配

正是堆的生命周期规则让下面的写法合法,同时也让返回局部数组变得非法:

len + 1 为空终止符留出了位置,这和 字符串 中各处的大小规则相同。再注意函数上方的注释:当一个函数返回已分配的内存时,由谁来释放它是函数约定的一部分。把这一点写下来不是走形式,它是防止内存泄漏的唯一保障。

堆上的结构体

同样的惯用写法适用于任何类型:

注意第二个失败分支在返回之前释放了 p。一旦函数持有不止一块已分配的内存,每条错误路径都必须释放已经申请到的那些,这是内存泄漏中描述的泄漏最常见的来源。

检查清单

  • 第一次使用之前,始终检查 malloc 是否返回 NULL。
  • 用 n * sizeof *p 计算大小,并以 size_t 做乘法。
  • 在 C 语言中不要对返回值做强制类型转换。
  • malloc 分配的内存未初始化;先写再读。
  • 用原始指针只释放一次,然后把它设为 NULL。
  • 分配之后的每条错误路径,都必须释放已经持有的内存。

常见问题

C 语言中 malloc 是做什么的?

malloc(n) 向堆申请 n 字节的内存,并返回指向这块内存起始位置的指针;如果请求无法满足,就返回 NULL。这块内存会一直存在,直到你把同一个指针传给 free。和局部变量不同,函数返回时它不会被释放。

C 语言中 malloc 和 free 怎么用?

用 int *p = malloc(n * sizeof *p); 分配内存,在使用之前先检查 if (p == NULL),然后像数组一样使用它,用完后调用 free(p);,并且只调用一次。之后再写上 p = NULL,这样以后不小心再用到它时,会直接崩溃,而不是悄悄破坏数据。

为什么 malloc(n * sizeof *p) 比 malloc(n * sizeof(int)) 更好?

因为 sizeof *p 会自动跟随指针的类型。如果 p 后来改成了 long * 或 struct Point *,分配的大小会自动更新;而直接写 sizeof(int),大小就会变得不对,编译器也不会提示。

C 语言中不释放内存会怎样?

这块内存会在整个进程的生命周期内一直被占用,这就是内存泄漏。短小的程序可以侥幸没事,因为操作系统在程序退出时会回收所有内存;但长时间运行的程序每执行一轮就泄漏一点,最终会耗尽内存。参见 内存泄漏。

Coddy 编程语言插图

和 Coddy 一起学编程

开始