malloc 和 free
上一课在堆上要过内存。这一课把 malloc 和 free 讲清楚:要多少字节、要不到怎么办、拿到的内存里有什么、什么时候还。先看一个完整的例子:在堆上放 5 天的气温。
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *temps = malloc(5 * sizeof(int));6 if (temps == NULL) {7 printf("内存不够\n");8 return 1;9 }10 for (int i = 0; i < 5; i++) {11 temps[i] = 20 + i;12 }13 printf("第一天 %d 度,最后一天 %d 度\n", temps[0], temps[4]);14 free(temps);15 temps = NULL;16 return 0;17}
第一天 20 度,最后一天 24 度
用堆上的内存,总是这四步:
| 步骤 | 这里的写法 |
|---|---|
要:算好字节数,调用 malloc | malloc(5 * sizeof(int)) |
查:看看是不是 NULL | if (temps == NULL) |
| 用:像数组一样用下标 | temps[i] = 20 + i; |
还:用完 free | free(temps); |
点单步:走过第 5 行,堆里出现一个「20 字节」的块,从 temps 画过去一根箭头;循环每转一圈,块里多一个数;走过 free,块变成灰色,标着「已释放」。最后一行把 temps 设成 NULL,箭头也没了,下一课讲为什么要这样做。
要多少字节
malloc 只认字节数,不知道你要放什么。要放 n 个某种类型的值,就写 n * sizeof(类型)。sizeof(int) 在这里是 4,sizeof(double) 是 8:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int count = 3;6 double *prices = malloc(count * sizeof(double));7 prices[0] = 4.5;8 prices[1] = 12.0;9 prices[2] = 3.25;10 double total = 0;11 for (int i = 0; i < count; i++) {12 total += prices[i];13 }14 printf("%zu 字节,合计 %.2f\n", count * sizeof(double), total);15 free(prices);16 return 0;17}
24 字节,合计 19.75
(这一节和下一节的几个例子为了短,没写检查 NULL;自己的程序里要写。)
只写个数、忘了乘 sizeof,是很常见的错:malloc(3) 只要了 3 个字节,连一个 int 都放不下。
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *steps = malloc(3);6 steps[0] = 4200;7 steps[1] = 6100;8 printf("%d\n", steps[1]);9 free(steps);10 return 0;11}
heap-buffer-overflow发生了什么: 停在第 6 行:越过了 malloc 来的这一块(3 个字节)的末尾(越界了)。
真的 C 不会提醒你,多写的字节会盖到别的东西上,程序可能过一会儿才出怪事。我们的引擎在第 6 行就停下了:这一块只有 3 个字节,放一个 4 字节的 int 已经越界。点「看看是怎么出错的」,看堆上那个「3 字节」的块。
小技巧: 也可以写 malloc(3 * sizeof *steps):*steps 是一个 int,sizeof *steps 就是 4。以后把 int 改成 double,这一行不用跟着改。
单步走过第 5 行,堆上的块标着多少字节?
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 double *weights = malloc(4 * sizeof(double));6 for (int i = 0; i < 4; i++) {7 weights[i] = 60.5 + i;8 }9 printf("%.1f\n", weights[3]);10 free(weights);11 return 0;12}
一个 double 占 8 个字节,要 4 个,就是 4 × 8 = 32 字节。malloc 拿到的只是 32 这个数,它不知道你要放 4 个 double。
检查 NULL
内存不是无限的。malloc 要不到那么多时,返回 NULL。拿到以后先检查,再用:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 char *big = malloc(100000000000);6 if (big == NULL) {7 printf("要不到 100 GB\n");8 return 1;9 }10 big[0] = 'A';11 free(big);12 return 0;13}
要不到 100 GB
一次要 100 GB,一般的电脑给不了,malloc 返回了 NULL,程序显示一句话就结束了。不检查的话,big[0] = 'A'; 就是往 NULL 里写,程序会崩溃。return 1; 告诉系统:程序没有顺利结束。
malloc 返回的类型是 void *,意思是「一个地址,不说里面放什么」。在 C 里,它可以直接存进 int *、double *、char * 这些指针,不用写别的。
拿到的内存里有什么
malloc 只管找一块地方,不管里面是什么。块里原来是什么就还是什么,和没赋值的变量一样(第 2 课):
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *counts = malloc(3 * sizeof(int));6 counts[0] = 5;7 printf("%d %d\n", counts[0], counts[1]);8 free(counts);9 return 0;10}
use of uninitialized value发生了什么: 停在第 7 行:这块内存还没有赋值就被用了。真 C 里这会是一个乱数(每次运行都可能不一样)。
counts[1] 还没赋过值。真的 C 会显示一个说不准的数,每次运行都可能不一样;我们的引擎在第 7 行停下来告诉你。
calloc:要来就是 0
要一块全是 0 的内存,用 calloc。它要两个参数:几个、每个多少字节。它会把整块清成 0:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int votes[8] = {2, 0, 1, 2, 2, 0, 1, 2};6 int *counts = calloc(3, sizeof(int));7 for (int i = 0; i < 8; i++) {8 counts[votes[i]]++;9 }10 for (int i = 0; i < 3; i++) {11 printf("%d 号:%d 票\n", i, counts[i]);12 }13 free(counts);14 return 0;15}
0 号:2 票 1 号:2 票 2 号:4 票
计数要从 0 开始,calloc 正合适。点单步,走过第 6 行,堆上的块是「12 字节」,三格都是 0;之后每投一票,就有一格加 1。
用完就 free
free(p) 把 p 指着的块还回去,以后这块内存可能分给别人用。几条规矩:
- 每个
malloc或calloc来的块,最后都要free一次,只能一次; free的必须是malloc或calloc给你的那个地址,不能是普通变量的地址;free以后,块里的东西不能再碰;free(NULL)什么也不做,是安全的。
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *a = malloc(2 * sizeof(int));6 int *b = malloc(4 * sizeof(int));7 a[0] = 1;8 b[0] = 2;9 printf("%d %d\n", a[0], b[0]);10 free(a);11 free(b);12 return 0;13}
1 2
点单步:堆上先后出现两个块,一个「8 字节 · 第 5 行」,一个「16 字节 · 第 6 行」。走过第 10 行,第一个块变灰;走过第 11 行,第二个也变灰。程序结束时,堆上没有还没还的块。违反上面这些规矩会怎样,下一课一个一个看。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
平均分
✓在堆上要 4 个 int,检查是不是 NULL,放进 78、85、90、77,显示 平均分:82.50,最后 free。
平均分:82.50
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 6 return 0;7}
数骰子
✓rolls 里是 10 次掷骰子的结果(1 到 6)。用 calloc 要 7 个 int 当计数器(下标 1 到 6 用来计数),数完显示每个点数出现了几次,格式是 1:2,每个一行,最后 free。
1:2 2:1 3:2 4:1 5:1 6:3
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int rolls[10] = {3, 6, 1, 3, 5, 6, 6, 2, 1, 4};6 7 return 0;8}
修好它
✓这段程序要了内存,可是一用就越界。改好 malloc 那一行,让它显示 9.5 7.0 8.5。
9.5 7.0 8.5
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 double *ratings = malloc(3);6 if (ratings == NULL) {7 return 1;8 }9 ratings[0] = 9.5;10 ratings[1] = 7.0;11 ratings[2] = 8.5;12 printf("%.1f %.1f %.1f\n", ratings[0], ratings[1], ratings[2]);13 free(ratings);14 return 0;15}
这一课学会了
malloc(n * sizeof(类型))在堆上要n个值的地方,返回块的地址;malloc只认字节数。- 要不到时
malloc返回NULL:拿到先检查,再用。 malloc给的内存没有清零,先赋值再用;要全是 0,用calloc(个数, 每个的字节数)。- 用完
free:每块一次,只能freemalloc/calloc给的地址,free(NULL)什么也不做。 - 单步时堆上每个块都标着字节数和是哪一行要来的;
free以后它变灰,标着「已释放」。