内存泄漏和悬空指针
堆上的内存自己要、自己还。这一课看「还」出了错会怎样:忘了还、还了还在用、还了两次。先看忘了还:
1#include <stdio.h>2#include <stdlib.h>3 4int *make_week(int base) {5 int *steps = malloc(7 * sizeof(int));6 for (int i = 0; i < 7; i++) {7 steps[i] = base + i * 100;8 }9 return steps;10}11 12int main(void) {13 for (int week = 1; week <= 3; week++) {14 int *steps = make_week(week * 1000);15 printf("第 %d 周第一天:%d 步\n", week, steps[0]);16 }17 return 0;18}
第 1 周第一天:1000 步 第 2 周第一天:2000 步 第 3 周第一天:3000 步
输出看不出任何问题,程序也正常结束了。点单步走到最后:堆上有三个「28 字节 · 第 5 行」的块,循环每转一圈多一个,一个也没还。程序结束时它们还在,每个都标着红色的「内存泄漏」。
忘了 free
要了内存却没有还,叫内存泄漏。每一圈,main 里的 steps 都指着一个新块,旧的那个就再也没有指针指着了:没人能用它,也没人能还它。
这个程序跑一下就结束了,结束时系统会把它的内存都收回去,所以看不出害处。可是一个一直开着的程序(服务器、浏览器、游戏)要是每处理一次就漏一点,它占的内存就越来越多,最后慢得不行,甚至被系统关掉。
真的 C 程序漏了内存,什么也不会说,输出和没漏一模一样。我们这里运行时也不会提醒;要看出来,就点单步走到最后,看堆上还剩着哪些块。
丢了指针
不光是忘了写 free。把指针改去指别的地方,原来的块也就找不回来了:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *prices = malloc(3 * sizeof(int));6 prices[0] = 12;7 prices = malloc(5 * sizeof(int));8 prices[0] = 30;9 printf("%d\n", prices[0]);10 free(prices);11 return 0;12}
30
点单步:走过第 7 行,prices 的箭头从「12 字节」的块转到了新的「20 字节」的块上,旧块那里一根箭头也没有了。第 10 行的 free 还的是新块。走到最后,旧块标着「内存泄漏」。
修好泄漏
规矩很简单:每个要来的块,在最后一个指着它的指针丢掉之前,还回去。第一个例子里,用完这一周就 free:
1#include <stdio.h>2#include <stdlib.h>3 4int *make_week(int base) {5 int *steps = malloc(7 * sizeof(int));6 for (int i = 0; i < 7; i++) {7 steps[i] = base + i * 100;8 }9 return steps;10}11 12int main(void) {13 for (int week = 1; week <= 3; week++) {14 int *steps = make_week(week * 1000);15 printf("第 %d 周第一天:%d 步\n", week, steps[0]);16 free(steps);17 }18 return 0;19}
第 1 周第一天:1000 步 第 2 周第一天:2000 步 第 3 周第一天:3000 步
再点单步走到最后:三个块都是灰色的「已释放」,没有红色的了。
小技巧: 写下一个 malloc 的时候,马上想好:谁来 free 它,在哪一行。函数返回一块堆内存时,写一句注释,说清楚调用它的人要负责 free。
还了还在用
反过来的错更危险:块已经还了,指针还指着那里,接着用。这样的指针叫悬空指针。
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *balance = malloc(sizeof(int));6 *balance = 500;7 free(balance);8 printf("余额:%d\n", *balance);9 return 0;10}
heap-use-after-free发生了什么: 停在第 8 行:这块内存已经 free 了(还回去了),不能再用。
free 以后,balance 里的地址没有变,可那块内存已经不是你的了,系统随时可能把它分给别人。真的 C 程序可能照样显示 500,可能显示一个乱数,也可能过了很久才在别的地方出事。我们的引擎在第 8 行停下来。点「看看是怎么出错的」:块已经变灰,balance 的箭头变成了红色虚线。
常见的情况是两个指针指着同一块,用一个还了,另一个还在用:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *scores = malloc(3 * sizeof(int));6 scores[0] = 88;7 int *best = scores;8 free(scores);9 printf("最高分:%d\n", best[0]);10 return 0;11}
heap-use-after-free发生了什么: 停在第 9 行:这块内存已经 free 了(还回去了),不能再用。
best = scores 复制的只是地址,两根箭头指着同一个块。还了一次,两个指针就都悬空了。
free 两次
同一块还两次也不行。还是那两个指着同一块的指针,各 free 一次:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *list = malloc(4 * sizeof(int));6 int *copy = list;7 list[0] = 7;8 copy[1] = 9;9 free(list);10 free(copy);11 return 0;12}
attempting double-free发生了什么: 停在第 10 行:这块内存已经 free 过了,不能再 free 一次。
copy 和 list 是同一个块,第 9 行已经还了,第 10 行又还一次。真的 C 程序常常在这里崩溃,有时还会把堆弄乱、让后面的 malloc 出错。我们的引擎在第 10 行停下。
还有一种:free 了一个不是 malloc 来的地址。普通变量在栈上,函数结束自己就没了,不归你还:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int count = 3;6 int *p = &count;7 *p = 4;8 free(p);9 return 0;10}
main.c: In function 'main': main.c:8:5: warning: 'free' called on unallocated object 'count' [-Wfree-nonheap-object] 8 | free(p); | ^~~~~~~ main.c:5:9: note: declared here 5 | int count = 3; | ^~~~~ attempting free on address which was not malloc()-ed
发生了什么: 停在第 8 行:「count」不是 malloc 来的,不能 free。free 只能用在 malloc / calloc / realloc 给的指针上。
这次 gcc 编译时就看出来了,给了警告;可警告不会拦住程序。我们的引擎在第 8 行停下:count 不是 malloc 来的。
free 以后设成 NULL
一个好习惯:free 以后,马上把指针设成 NULL。这样它就不再悬空,用之前查一下 != NULL 就知道它还能不能用;不小心再 free 一次也没事,因为 free(NULL) 什么也不做。
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *cart = malloc(2 * sizeof(int));6 cart[0] = 15;7 cart[1] = 40;8 printf("合计:%d\n", cart[0] + cart[1]);9 free(cart);10 cart = NULL;11 if (cart != NULL) {12 printf("购物车还在\n");13 } else {14 printf("购物车已经清空\n");15 }16 free(cart);17 return 0;18}
合计:55 购物车已经清空
点单步:走过第 10 行,cart 的箭头没了,变成 NULL。
这段程序会怎样?
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *a = malloc(sizeof(int));6 int *b = a;7 free(a);8 a = NULL;9 free(b);10 return 0;11}
a = NULL; 只改了 a,b 里还是原来那个地址。第 9 行的 free(b) 还的是已经还过的块,是 free 两次。设成 NULL 是个好习惯,但它只管得了这一个指针,管不了别的指着同一块的指针。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
还早了
✓这段程序 free 得太早,后面还在用。挪一挪那一行,让它显示 第一名:95 和 第二名:88,并且程序结束前把内存还掉。
第一名:95 第二名:88
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *top = malloc(2 * sizeof(int));6 top[0] = 95;7 top[1] = 88;8 printf("第一名:%d\n", top[0]);9 free(top);10 printf("第二名:%d\n", top[1]);11 return 0;12}
只还一次
✓orders 和 today 指着同一块内存,程序把它还了两次。删掉多余的那一句,让程序显示 今天 39 单,并且正常结束。
今天 39 单
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *orders = malloc(3 * sizeof(int));6 orders[0] = 12;7 orders[1] = 7;8 orders[2] = 20;9 int *today = orders;10 printf("今天 %d 单\n", today[0] + today[1] + today[2]);11 free(orders);12 free(today);13 return 0;14}
安全地还
✓写好函数 release:free 参数 p,然后返回 NULL。main 写的是 data = release(data);,所以调用两次也不会出错。程序要显示 data 是 NULL。
data 是 NULL
1#include <stdio.h>2#include <stdlib.h>3 4int *release(int *p) {5 6}7 8int main(void) {9 int *data = malloc(4 * sizeof(int));10 data = release(data);11 data = release(data);12 if (data == NULL) {13 printf("data 是 NULL\n");14 }15 return 0;16}
这一课学会了
- 要了不还,叫内存泄漏:输出看不出来,程序越跑占的内存越多。单步走到最后,没还的块标着「内存泄漏」。
- 把指向一个块的最后一个指针改掉或丢掉之前,先
free它。 free以后接着用(悬空指针)、同一块free两次、free不是malloc来的地址,都是未定义行为,我们的引擎会停下说明。- 两个指针指着同一块时,
free一次,两个都悬空了;只能还一次。 free以后把指针设成NULL:用之前查得出来,再free也没事;但别的指着同一块的指针不会跟着变。