栈和堆
程序跑起来以后,变量放在内存里。C 程序的内存里有两块地方你要分清:栈和堆。到现在为止,我们写的变量都在栈上。点单步,代码下面的内存画面分成「栈」和「堆」两块:
1#include <stdio.h>2 3int add(int a, int b) {4 int sum = a + b;5 return sum;6}7 8int main(void) {9 int price = 30;10 int total = add(price, 12);11 printf("合计:%d\n", total);12 return 0;13}
合计:42
一开始栈上只有 main 的框,里面是 price 和 total。调用 add 时,上面多了一个框,放着 a、b、sum;add 一返回,这个框就没了。堆那一块一直是空的。
| 栈 | 堆 | |
|---|---|---|
| 放什么 | 函数的参数和局部变量 | 用 malloc 要来的内存 |
| 谁来管 | 自动:调用时有,返回时没 | 你自己:自己要,自己还 |
| 活多久 | 到函数结束 | 到你 free 它 |
栈:函数结束就没了
每调用一次函数,栈上就多一个框(第 8 课叫它栈帧),函数返回,框就拿掉。所以局部变量只活到函数结束。第 18 课见过一个指向局部变量的指针,函数一返回就悬空了。数组也一样:想让函数做好一个数组交出来,直接返回局部数组,是行不通的:
1#include <stdio.h>2 3int *make_scores(void) {4 int scores[3] = {78, 85, 90};5 return scores;6}7 8int main(void) {9 int *p = make_scores();10 printf("%d\n", p[0]);11 return 0;12}
main.c: In function 'make_scores': main.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr] 5 | return scores; | ^~~~~~ load of null pointer
发生了什么: 停在第 10 行:这个指针是 NULL(空指针),不能通过它读写。
gcc 先给了一个警告:函数返回了局部变量的地址。它还是编译了,而且因为这个地址拿出去也没法用,gcc 干脆让 make_scores 返回 NULL。所以第 10 行通过 p 去读时,我们的引擎停下了:p 是 NULL。点「看看是怎么出错的」,一步一步看:make_scores 的框一拿掉,框里的数组也跟着没了。
堆:自己要,自己还
想要一块不会跟着函数消失的内存,就到堆上去要。malloc 在 <stdlib.h> 里,你告诉它要多少字节,它在堆上找出这么大的一块,把地址交给你;用完了,用 free 把它还回去:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int *p = malloc(sizeof(int));6 *p = 42;7 printf("堆上的数:%d\n", *p);8 free(p);9 return 0;10}
堆上的数:42
点单步,走过第 5 行:堆里出现了一个块,标着「4 字节 · 第 5 行」,也就是它有多大、是哪一行要来的。p 自己在栈上,是 main 框里的一个指针,一根箭头从它指向堆上的块。走过第 8 行的 free,块变成灰色,标着「已释放」:这块内存还回去了,不能再用。malloc 的细节(要多少字节、要不到怎么办)下一课再讲。
变量 price 本身放在哪里?
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 double *price = malloc(sizeof(double));6 *price = 9.5;7 printf("%.1f\n", *price);8 free(price);9 return 0;10}
price 是 main 的局部变量,和别的局部变量一样在栈上。它里面存的是一个地址,那个地址上的 double(9.5)才在堆上。单步时看:price 在 main 的框里,箭头指到堆里的块。
活得比函数久
堆上的块不归哪个函数管,函数返回了,它还在。所以要让函数做出一组数交给调用它的地方,就在堆上做:
1#include <stdio.h>2#include <stdlib.h>3 4int *make_scores(void) {5 int *scores = malloc(3 * sizeof(int));6 scores[0] = 78;7 scores[1] = 85;8 scores[2] = 90;9 return scores;10}11 12int main(void) {13 int *p = make_scores();14 printf("%d %d %d\n", p[0], p[1], p[2]);15 free(p);16 return 0;17}
78 85 90
和上一节的程序几乎一样,只是数组换成了堆上的块。点单步看:make_scores 返回时,它的框没了,框里的 scores 指针也没了,可堆上那个「12 字节」的块还在,main 里的 p 接着指着它。块是 make_scores 要来的,最后由 main 还回去:谁还都行,只要还一次。
大小要等跑起来才知道
这门课里的数组,大小都是写代码时就定下的常量,比如 int a[7]。可很多时候,要放多少个数得等程序跑起来才知道:比如从一组成绩里挑出及格的,挑完才知道有几个。这时就按算出来的个数到堆上去要:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int scores[6] = {58, 72, 91, 45, 60, 83};6 int count = 0;7 for (int i = 0; i < 6; i++) {8 if (scores[i] >= 60) {9 count++;10 }11 }12 int *passed = malloc(count * sizeof(int));13 int k = 0;14 for (int i = 0; i < 6; i++) {15 if (scores[i] >= 60) {16 passed[k] = scores[i];17 k++;18 }19 }20 printf("及格 %d 人:", count);21 for (int i = 0; i < count; i++) {22 printf("%d ", passed[i]);23 }24 printf("\n");25 free(passed);26 return 0;27}
及格 4 人:72 91 60 83
count 是 4,所以堆上的块是 4 × 4 = 16 字节,不多不少。
小技巧: C 也允许 int a[n] 这种大小是变量的数组(叫变长数组),但它放在栈上,n 太大就会出事,C11 起编译器也可以不支持它。这个页面不支持它。要的个数是变量时,用 malloc。
栈不大,堆大
栈的空间不大,在常见的电脑上一般只有几 MB,一个函数里要是声明一个几百万个元素的数组,就可能把栈撑爆,程序直接崩溃。堆可以大得多,能用到电脑的大部分内存。所以很大的数组也放到堆上:
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 int count = 1000000;6 int *steps = malloc(count * sizeof(int));7 for (int i = 0; i < count; i++) {8 steps[i] = 5000;9 }10 printf("一共 %d 天,第一天 %d 步\n", count, steps[0]);11 free(steps);12 return 0;13}
一共 1000000 天,第一天 5000 步
一百万个 int 是 4000000 字节,差不多 4 MB,放在栈上很危险,放在堆上没问题。(这段程序循环一百万圈,别点单步一圈一圈地看,点运行就好。)
什么时候用堆,记住三种情况:
- 要的个数等程序跑起来才知道;
- 这块内存要比要它的函数活得久;
- 东西很大,栈上放不下。
除此以外,用普通的局部变量就好:不用自己还,也就不会忘了还。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
堆上的价格
✓用 malloc 在堆上要一个 double,把 19.5 放进去,显示 价格:19.50,最后 free 它。
价格:19.50
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 6 return 0;7}
平方表
✓写好函数 make_squares:在堆上要 n 个 int,第 i 个放 (i + 1) * (i + 1),返回这块内存的地址。程序要显示 1 4 9 16 25 ,然后换行。
1 4 9 16 25
1#include <stdio.h>2#include <stdlib.h>3 4int *make_squares(int n) {5 6}7 8int main(void) {9 int *squares = make_squares(5);10 for (int i = 0; i < 5; i++) {11 printf("%d ", squares[i]);12 }13 printf("\n");14 free(squares);15 return 0;16}
修好它
✓make_prices 返回了一个局部数组的地址,程序一用就出事。改成在堆上要内存,让程序显示 12 8 25,并在 main 里用完后 free。
12 8 25
1#include <stdio.h>2#include <stdlib.h>3 4int *make_prices(void) {5 int prices[3] = {12, 8, 25};6 return prices;7}8 9int main(void) {10 int *p = make_prices();11 printf("%d %d %d\n", p[0], p[1], p[2]);12 return 0;13}
这一课学会了
- 函数的参数和局部变量在栈上:调用时多一个框,返回时框就没了,里面的变量也没了。
- 不要返回局部变量的地址:函数一返回,那里就不是你的了。
- 堆上的内存用
malloc要、用free还;函数返回了,它还在。 - 指针本身可以在栈上,它指着的块在堆上。
- 三种情况用堆:个数要跑起来才知道、要活得比函数久、东西太大栈上放不下。