结构体指针
上一课说到,结构体传给函数传的是副本,改不了外面的。第 16 课我们用指针解决过同样的问题:把地址交给函数。结构体也一样,先看看指向结构体的指针:
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 是个指针,没有成员:
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 完全一样。
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 里的那个:
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}
top 返回的是 &list[1],也就是 group[1] 的地址,不是副本。p->score = 100 改的就是 group[1]。返回数组元素的地址没问题:数组在 main 里,函数结束后它还在。
在堆上建一个结构体
结构体也能放在堆上(第 19、20 课):malloc(sizeof(struct product)) 要一块正好放得下一个结构体的内存,拿到的是指向它的指针,所以接下来都用 ->:
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 课)表示函数不会改它。
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 却还指着那里:
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:
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)。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
换成箭头
✓这段程序编译不过。把 p 后面的点号都改成箭头,让它显示 台灯 89.00。
台灯 89.00
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}
加分
✓写函数 add_points,参数是指向 struct player 的指针和要加的分数,把分数加到这个选手身上。程序要显示 小周:35 分。
小周:35 分
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}
堆上的一本书
✓用 malloc 在堆上建一个 struct book,书名设成 "活着",页数设成 191,显示 活着:191 页,最后 free 掉。
活着:191 页
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}
这一课学会了
struct student *p = &s;是指向结构体的指针,*p就是s。p->score就是(*p).score:手里是结构体用.,是指针用->。- 函数参数写成结构体指针、调用时传
&acc,函数就能改外面的结构体,也省得复制。 - 函数可以返回数组里某个元素的地址,通过它改的就是数组里的那一个。
malloc(sizeof(struct product))在堆上建结构体;可以写一个函数建好再返回指针,调用的人用完要free。free之后别再用;指针可能是NULL时,先检查再用->。