Kodewick
第 19 课,共 30 课目录

栈和堆

第 19 课,共 30 课 · 第 4 部分 · 内存 · 约 15 分钟

程序跑起来以后,变量放在内存里。C 程序的内存里有两块地方你要分清:栈和堆。到现在为止,我们写的变量都在栈上。点单步,代码下面的内存画面分成「栈」和「堆」两块:

main.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 课见过一个指向局部变量的指针,函数一返回就悬空了。数组也一样:想让函数做好一个数组交出来,直接返回局部数组,是行不通的:

main.c
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 把它还回去:

main.c
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}

活得比函数久

堆上的块不归哪个函数管,函数返回了,它还在。所以要让函数做出一组数交给调用它的地方,就在堆上做:

main.c
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]。可很多时候,要放多少个数得等程序跑起来才知道:比如从一组成绩里挑出及格的,挑完才知道有几个。这时就按算出来的个数到堆上去要:

main.c
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,一个函数里要是声明一个几百万个元素的数组,就可能把栈撑爆,程序直接崩溃。堆可以大得多,能用到电脑的大部分内存。所以很大的数组也放到堆上:

main.c
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,放在栈上很危险,放在堆上没问题。(这段程序循环一百万圈,别点单步一圈一圈地看,点运行就好。)

什么时候用堆,记住三种情况:

  • 要的个数等程序跑起来才知道;
  • 这块内存要比要它的函数活得久;
  • 东西很大,栈上放不下。

除此以外,用普通的局部变量就好:不用自己还,也就不会忘了还。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

堆上的价格

用 malloc 在堆上要一个 double,把 19.5 放进去,显示 价格:19.50,最后 free 它。

要显示
价格:19.50
exercise1.c
1#include <stdio.h>2#include <stdlib.h>3 4int main(void) {5 6    return 0;7}
输出
点「运行」或按 Ctrl+Enter
练习 2

平方表

写好函数 make_squares:在堆上要 n 个 int,第 i 个放 (i + 1) * (i + 1),返回这块内存的地址。程序要显示 1 4 9 16 25 ,然后换行。

要显示
1 4 9 16 25 
exercise2.c
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它

make_prices 返回了一个局部数组的地址,程序一用就出事。改成在堆上要内存,让程序显示 12 8 25,并在 main 里用完后 free。

要显示
12 8 25
exercise3.c
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 函数的参数和局部变量在栈上:调用时多一个框,返回时框就没了,里面的变量也没了。
  • 不要返回局部变量的地址:函数一返回,那里就不是你的了。
  • 堆上的内存用 malloc 要、用 free 还;函数返回了,它还在。
  • 指针本身可以在栈上,它指着的块在堆上。
  • 三种情况用堆:个数要跑起来才知道、要活得比函数久、东西太大栈上放不下。