Kodewick
第 23 课,共 30 课目录

结构体

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

一个学生有名字,也有分数。到现在为止,我们只能用两个变量分开存:name 一个,score 一个。学生一多,就要两个数组,还得小心让它们一一对上。结构体(struct)把几样相关的数据装进一个变量里:

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 = { "小林", 95 };10    printf("%s:%d 分\n", s.name, s.score);11    return 0;12}
输出
小林:95 分

点单步看看:s 在内存里画成一个大框,里面是 name 和 score 两个小格。它们是一个变量的两个部分,总是在一起。

定义一个结构体

struct student { ... }; 定义了一种新的类型,说明这种变量里有什么。里面的每一项叫成员:

部分在上面的例子里是什么
关键字struct说明下面是一个结构体
名字student这种结构体叫什么,用的时候写 struct student
成员char name[20]; int score;每一项都写类型和名字,和声明变量一样
结尾};花括号后面要有分号,很容易忘

定义只是说明「长什么样」,不占内存;写 struct student s; 才真的有了一个变量。用点号 . 拿到它的成员,读写都行。成员可以一项一项地赋值,字符串成员用 strcpy(第 14 课):

main.c
1#include <stdio.h>2#include <string.h>3 4struct product {5    char name[20];6    double price;7    int stock;8};9 10int main(void) {11    struct product p;12    strcpy(p.name, "笔记本");13    p.price = 12.5;14    p.stock = 40;15    p.stock = p.stock - 3;16    printf("%s %.2f 元,还剩 %d 本\n", p.name, p.price, p.stock);17    return 0;18}
输出
笔记本 12.50 元,还剩 37 本

p.stock 用起来就是一个普通的 int:可以赋值、可以参加运算。

初始化

声明时可以用花括号一次给全:值按成员定义的顺序一个对一个。给的值不够,剩下的成员是 0(字符串就是空的):

main.c
1#include <stdio.h>2 3struct product {4    char name[20];5    double price;6    int stock;7};8 9int main(void) {10    struct product a = { "铅笔", 1.5, 120 };11    struct product b = { "橡皮", 2.0 };12    printf("%s %.2f %d\n", a.name, a.price, a.stock);13    printf("%s %.2f %d\n", b.name, b.price, b.stock);14    return 0;15}
输出
铅笔 1.50 120
橡皮 2.00 0

顺序很要紧:写成 { 1.5, "铅笔", 120 } 就对不上了,编译器会报错或者给警告。

赋值是复制

数组不能用 = 整个赋值,结构体可以。b = a; 把 a 的每个成员都复制给 b,连里面的字符数组也一起复制。复制完它们就是两个互不相干的变量:

先猜一猜

这段程序显示什么?

1#include <stdio.h>2 3struct student {4    char name[20];5    int score;6};7 8int main(void) {9    struct student a = { "小林", 80 };10    struct student b = a;11    b.score = 100;12    printf("%d %d\n", a.score, b.score);13    return 0;14}

可是两个结构体不能用 == 比较,编译器不认:

main.c
1#include <stdio.h>2 3struct point {4    int x;5    int y;6};7 8int main(void) {9    struct point a = { 1, 2 };10    struct point b = { 1, 2 };11    if (a == b) {12        printf("一样\n");13    }14    return 0;15}
输出
main.c: In function 'main':
main.c:11:11: error: invalid operands to binary == (have 'struct point' and 'struct point')
   11 |     if (a == b) {
      |           ^~

要比,就一个成员一个成员地比:a.x == b.x && a.y == b.y。

结构体数组

结构体也能放进数组。songs[i] 是一首歌,songs[i].seconds 是这首歌的秒数。一个歌单:

main.c
1#include <stdio.h>2 3struct song {4    char title[30];5    int seconds;6};7 8int main(void) {9    struct song songs[3] = {10        { "清晨", 269 },11        { "海边", 226 },12        { "回家的路", 223 }13    };14    int total = 0;15    for (int i = 0; i < 3; i++) {16        printf("%d. %s(%d:%02d)\n", i + 1, songs[i].title, songs[i].seconds / 60, songs[i].seconds % 60);17        total += songs[i].seconds;18    }19    printf("一共 %d 分 %d 秒\n", total / 60, total % 60);20    return 0;21}
输出
1. 清晨(4:29)
2. 海边(3:46)
3. 回家的路(3:43)
一共 11 分 58 秒

每首歌的名字和长度放在一起,不会对错行。%02d 让秒数不够两位时前面补 0,4:05 不会显示成 4:5。

传给函数

结构体可以当参数。和 int 一样,传的是值:参数是一份完整的副本(第 8 课)。所以函数里改它,外面看不到:

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    printf("函数里:%d\n", a.balance);11}12 13int main(void) {14    struct account acc = { "小林", 100 };15    deposit(acc, 50);16    printf("main 里:%d\n", acc.balance);17    return 0;18}
输出
函数里:150
main 里:100

点单步,走进 deposit 时,它的栈帧里多出一个完整的 a:owner 和 balance 都复制过去了。变的是这个副本,函数结束它就没了。

一个办法是让函数把改好的结构体返回来,由调用的地方存回去:

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

小技巧: 结构体大了,每次调用都复制一遍很浪费。更常见的办法是传它的地址,函数通过指针直接改,下一课就讲。

练一练

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

练习 1

一本书

定义 struct book,成员是书名 title(char 数组,20 个字节)和页数 pages(int)。声明一本书 { "三体", 302 },显示 三体:302 页。

要显示
三体:302 页
exercise1.c
1#include <stdio.h>2 3// 在这里定义 struct book4 5int main(void) {6 7    return 0;8}
输出
点「运行」或按 Ctrl+Enter
练习 2

最便宜的

在 items 里找出价格最低的商品,显示 最便宜:牙刷 3.50。

要显示
最便宜:牙刷 3.50
exercise2.c
1#include <stdio.h>2 3struct product {4    char name[20];5    double price;6};7 8int main(void) {9    struct product items[4] = {10        { "牙膏", 8.90 },11        { "毛巾", 12.00 },12        { "牙刷", 3.50 },13        { "香皂", 4.20 }14    };15 16    return 0;17}
输出
点「运行」或按 Ctrl+Enter
练习 3

没打上折

half_price 想把价格减半,可显示的还是原价。改好它:让函数返回改好的商品,main 把它存回 p。程序要显示 毛巾 6.00。

要显示
毛巾 6.00
exercise3.c
1#include <stdio.h>2 3struct product {4    char name[20];5    double price;6};7 8void half_price(struct product p) {9    p.price = p.price / 2;10}11 12int main(void) {13    struct product p = { "毛巾", 12.00 };14    half_price(p);15    printf("%s %.2f\n", p.name, p.price);16    return 0;17}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • struct 名字 { 成员; ... }; 定义一种新类型,把几样相关的数据放在一起;结尾要分号。
  • 声明时用 { ... } 按顺序初始化,没给的成员是 0;也可以一项一项赋值,字符串成员用 strcpy。
  • 用点号 . 读写成员,s.score 用起来就是一个普通变量。
  • b = a; 复制整个结构体,之后两个互不相干;结构体不能用 == 比较。
  • 结构体数组把一组记录放在一起:songs[i].title。
  • 结构体传给函数传的是副本;要改外面的,就把新的结构体返回来(或者传指针,下一课讲)。