Kodewick
第 24 课,共 30 课目录

结构体指针

第 24 课,共 30 课 · 第 5 部分 · 自己的类型 · 约 15 分钟

上一课说到,结构体传给函数传的是副本,改不了外面的。第 16 课我们用指针解决过同样的问题:把地址交给函数。结构体也一样,先看看指向结构体的指针:

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "小林", 80 };10    struct student *p = &s;11    (*p).score = 90;12    printf("%s:%d 分\n", (*p).name, s.score);13    return 0;14}
输出
小林:90 分

struct student *p 是一个指针,指向一个 struct student。*p 就是 s 本身,所以 (*p).score = 90 改的就是 s.score。点单步看看:p 是一个小格,一个箭头从它指到 s 的大框上。

箭头 ->

(*p).score 里的括号不能省。点号 . 比 * 先算,不加括号,*p.score 就成了「先拿 p 的成员 score」,可 p 是个指针,没有成员:

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "小林", 80 };10    struct student *p = &s;11    *p.score = 90;12    printf("%d\n", s.score);13    return 0;14}
输出
main.c: In function 'main':
main.c:11:7: error: 'p' is a pointer; did you mean to use '->'?
   11 |     *p.score = 90;
      |       ^
      |       ->

编译器都猜到你想干什么了:did you mean to use '->'?。每次写 (*p). 太麻烦,所以 C 有一个专门的写法,箭头 ->:p->score 和 (*p).score 完全一样。

main.c
1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student s = { "小林", 80 };10    struct student *p = &s;11    p->score = 90;12    p->score += 5;13    printf("%s:%d 分\n", p->name, p->score);14    return 0;15}
输出
小林:95 分

记法很简单:手里是结构体,用 .;手里是指向结构体的指针,用 ->。

函数通过指针改结构体

上一课的 deposit 存不进钱,因为它拿到的是副本。把参数改成指针,调用时传地址 &acc,函数就能直接改 main 里的那个:

main.c
1#include <stdio.h>2 3struct account {4    char owner[20];5    int balance;6};7 8void deposit(struct account *a, int amount) {9    a->balance += amount;10}11 12int main(void) {13    struct account acc = { "小林", 100 };14    deposit(&acc, 50);15    deposit(&acc, 30);16    printf("%s的余额:%d\n", acc.owner, acc.balance);17    return 0;18}
输出
小林的余额:180

点单步,走进 deposit:它的栈帧里不再有一份完整的副本,只有一个指针 a,箭头指回 main 栈帧里的 acc。a->balance += amount 改的就是那边的数。传指针还有一个好处:不管结构体多大,复制的只是一个地址。

小技巧: 函数只读不改时,参数写成 const struct account *a。这样既不用复制,函数里不小心改了它,编译器也会报错。

指向数组里的一个

指针也可以指向结构体数组里的某一个元素。下面的 top 在数组里找分数最高的学生,返回的是那个元素的地址:

先猜一猜

这段程序显示什么?

1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8struct student *top(struct student list[], int n) {9    struct student *best = &list[0];10    for (int i = 1; i < n; i++) {11        if (list[i].score > best->score) {12            best = &list[i];13        }14    }15    return best;16}17 18int main(void) {19    struct student group[3] = { { "小林", 82 }, { "小周", 91 }, { "小陈", 77 } };20    struct student *p = top(group, 3);21    p->score = 100;22    printf("%d\n", group[1].score);23    return 0;24}

在堆上建一个结构体

结构体也能放在堆上(第 19、20 课):malloc(sizeof(struct product)) 要一块正好放得下一个结构体的内存,拿到的是指向它的指针,所以接下来都用 ->:

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10int main(void) {11    struct product *p = malloc(sizeof(struct product));12    if (p == NULL) {13        printf("内存不够\n");14        return 1;15    }16    strcpy(p->name, "保温杯");17    p->price = 59.0;18    printf("%s %.2f\n", p->name, p->price);19    free(p);20    return 0;21}
输出
保温杯 59.00

点单步,看堆上出现一个框,里面是 name 和 price,main 里的 p 用箭头指着它;free(p) 之后,这个框变成灰色虚线,标着「已释放」:这块内存已经还回去了。

函数建,调用的人还

在堆上建结构体,常常写成一个函数:它要内存、填好成员、把指针交回来。堆上的东西函数结束后还在,所以这样做没问题(如果返回的是局部变量的地址就不行了,第 18 课)。代价是:调用的人用完要记得 free。参数 const char *name 是函数接收一个只读字符串的常见写法:传进来的是第一个字符的地址(第 17 课),const(第 18 课)表示函数不会改它。

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10struct product *new_product(const char *name, double price) {11    struct product *p = malloc(sizeof(struct product));12    if (p != NULL) {13        strcpy(p->name, name);14        p->price = price;15    }16    return p;17}18 19int main(void) {20    struct product *a = new_product("保温杯", 59.0);21    struct product *b = new_product("雨伞", 35.5);22    if (a == NULL || b == NULL) {23        return 1;24    }25    printf("%s %.2f\n", a->name, a->price);26    printf("%s %.2f\n", b->name, b->price);27    printf("一共 %.2f\n", a->price + b->price);28    free(a);29    free(b);30    return 0;31}
输出
保温杯 59.00
雨伞 35.50
一共 94.50

两个常见的错

第一个:free 之后又去用它。框已经还回去了,p 却还指着那里:

main.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct product {6    char name[20];7    double price;8};9 10int main(void) {11    struct product *p = malloc(sizeof(struct product));12    if (p == NULL) {13        return 1;14    }15    strcpy(p->name, "雨伞");16    p->price = 35.5;17    free(p);18    printf("%s %.2f\n", p->name, p->price);19    return 0;20}
输出
heap-use-after-free

发生了什么: 停在第 18 行:这块内存已经 free 了(还回去了),不能再用。

真正的 C 不一定会出事,可能还显示原来的值,也可能显示乱七八糟的东西。我们的引擎在这一行停下来。点「看看是怎么出错的」,看 free 之后那个框已经变灰,p 的箭头变成了红色虚线。

第二个:指针是 NULL,却用 -> 去拿成员。比如一个函数找不到东西时返回 NULL,调用的人又忘了检查,就会这样。下面直接把 p 设成 NULL:

main.c
1#include <stdio.h>2#include <string.h>3 4struct student {5    char name[20];6    int score;7};8 9int main(void) {10    struct student *p = NULL;11    strcpy(p->name, "小林");12    printf("结束\n");13    return 0;14}
输出
store to null pointer

发生了什么: 停在第 11 行:这个指针是 NULL(空指针),不能通过它读写。

NULL 不指向任何东西,p->name 没地方可去。在真正的电脑上,这样的程序一般会直接崩溃。用 -> 之前,只要指针可能是 NULL,就先 if (p != NULL)。

练一练

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

练习 1

换成箭头

这段程序编译不过。把 p 后面的点号都改成箭头,让它显示 台灯 89.00。

要显示
台灯 89.00
exercise1.c
1#include <stdio.h>2 3struct product {4    char name[20];5    double price;6};7 8int main(void) {9    struct product item = { "台灯", 99.0 };10    struct product *p = &item;11    p.price = p.price - 10;12    printf("%s %.2f\n", p.name, p.price);13    return 0;14}
输出
点「运行」或按 Ctrl+Enter
练习 2

加分

写函数 add_points,参数是指向 struct player 的指针和要加的分数,把分数加到这个选手身上。程序要显示 小周:35 分。

要显示
小周:35 分
exercise2.c
1#include <stdio.h>2 3struct player {4    char name[20];5    int points;6};7 8// 在这里写 add_points9 10int main(void) {11    struct player pl = { "小周", 20 };12    add_points(&pl, 10);13    add_points(&pl, 5);14    printf("%s:%d 分\n", pl.name, pl.points);15    return 0;16}
输出
点「运行」或按 Ctrl+Enter
练习 3

堆上的一本书

用 malloc 在堆上建一个 struct book,书名设成 "活着",页数设成 191,显示 活着:191 页,最后 free 掉。

要显示
活着:191 页
exercise3.c
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4 5struct book {6    char title[20];7    int pages;8};9 10int main(void) {11 12    return 0;13}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • struct student *p = &s; 是指向结构体的指针,*p 就是 s。
  • p->score 就是 (*p).score:手里是结构体用 .,是指针用 ->。
  • 函数参数写成结构体指针、调用时传 &acc,函数就能改外面的结构体,也省得复制。
  • 函数可以返回数组里某个元素的地址,通过它改的就是数组里的那一个。
  • malloc(sizeof(struct product)) 在堆上建结构体;可以写一个函数建好再返回指针,调用的人用完要 free。
  • free 之后别再用;指针可能是 NULL 时,先检查再用 ->。