第五章 结构体
本章要点
先想一个具体的问题:如何用程序来描述和存储一个学生的信息?
一个学生,通常包含哪些基本信息?班级、姓名、学号——这三样是最常见的。班级可能是一个编号(整数),姓名是一段文字(字符串),学号也可能是一串数字。也就是说,描述一个学生,需要把不同类型的数据组合在一起。
回顾一下我们目前学过的工具:int 和 float 只能存单个数值,char 只能存单个字符,数组要求所有元素类型相同,字符串本质上也是字符数组。有没有哪个东西能同时容纳“一个整数 + 一个字符串 + 一个整数“?没有。我们似乎只能这样写:
int class = 3; // 班级
char name[20] = "小明"; // 姓名
int student_id = 20240001; // 学号
三个变量,分别代表班级、姓名和学号。这能工作——但你能明显感觉到不对劲:这三个变量在逻辑上属于同一个学生,但在代码中却是三个互不相关的独立变量。问题出在三个层面:
- 分散:如果要处理第二个学生,就得再声明三个变量(
class2、name2、student_id2),数据多一个,变量翻一倍 - 传参混乱:如果要传给函数,就得写一长串参数,顺序稍错就出 bug
- 无约束:没有任何机制保证“
class和name是同一个人的“——全靠变量命名和程序员的自觉
这个问题的本质是:缺少一种能把多个不同类型的数据打包在一起的东西。
这个问题的本质是:缺少一种能把多个不同类型的数据打包在一起的东西。 C 语言为此提供了结构体(struct)。本章将系统学习结构体的用法。具体涵盖以下内容:
- 结构体的定义与变量声明
- 结构体成员的初始化与访问(点运算符
.) - 结构体数组——批量管理多条结构相同的记录
- 结构体与函数的配合:传参、返回值
- 字符串成员的赋值方法(
strcpy) - 结构体的嵌套——在结构体中包含另一个结构体
typedef关键字——给类型起别名,让代码更简洁
结构体是 C 语言从“基本类型“迈向“自定义类型“的关键一步。掌握了它,你才真正开始用 C 语言建模现实世界中的事物。后续章节中的指针、动态内存分配、链表等内容,都离不开结构体作为数据载体。
一、结构体基础
1.1 结构体是什么
回到上面的问题:班级、姓名、学号这三个数据,在逻辑上属于同一个学生,但在代码中却是三个分离的变量。结构体就是解决这个问题的——它允许你将多个不同类型的变量打包成一个整体,给这个整体起一个名字,然后把它当作一个统一的单位来使用。
你可以把结构体想象成一个快递包裹:里面放进不同类型的东西(一本书、一支笔、一个U盘),贴上标签,然后把这个包裹作为一个整体来传递。需要取出包裹里的某件东西时,打开包裹拿即可。
用结构体来描述“一个学生“,代码就变成了这样:
struct Student
{
int class; // 班级
char name[20]; // 姓名
int student_id; // 学号
};
struct Student 是一个自定义的类型——就像 int 和 float 一样,只不过它不是 C 语言内置的,而是你自己根据需求定义出来的。有了这个类型之后,就可以像声明普通变量一样声明“学生变量“:
struct Student stu1; // stu1 是一个"学生"变量,内部包含班级、姓名、学号
struct Student stu2; // stu2 是另一个"学生"变量
stu1 和 stu2 各自都拥有 class、name、student_id 这三个成员。它们互不影响——修改 stu1 的 class 不会改变 stu2 的 class。原本需要三个分离变量才能描述的一个学生,现在一个变量就够了;原本要靠命名约定来维持的关联,现在由类型本身来保证。
1.2 结构体的定义
结构体是一种自定义的复合数据类型。它允许你把多个不同类型的变量打包在一起,组成一个新的类型。使用结构体分两步走:先定义结构体类型(设计“模板“),再声明结构体变量(根据模板创建具体的“实例“)。
第一步:定义结构体类型。 这一步的作用是告诉编译器这个新类型“长什么样“——包含哪些成员,各自是什么类型。语法如下:
struct 结构体名
{
数据类型 成员名1;
数据类型 成员名2;
数据类型 成员名3;
...
};
其中 struct 是关键字,意思是“接下来定义的是一个结构体类型“。结构体名是你给这个新类型起的名字,命名规则和变量名一样。花括号 { } 中是成员列表,每个成员和普通变量声明一样,以分号结尾。最后的分号 ; 不能忘记——这是初学者常犯的错误,因为花括号后面跟分号不符合多数其他语境的习惯。
第二步:声明结构体变量。 定义好类型之后,就可以像使用 int 或 float 一样用它来声明变量:
struct 结构体名 变量名;
将以上两步放在一起看一个完整示例:
// 第一步:定义结构体类型(设计模板)
struct Student
{
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
}; // 注意这个分号!
// 第二步:声明结构体变量(创建实例)
struct Student stu1; // stu1 是一个 Student 类型的变量
struct Student stu2; // stu2 是另一个 Student 类型的变量
这样,stu1 和 stu2 各自都拥有了 name、age、score 这三个成员,就像两个独立的快递包裹,每个包裹里都有这三样东西。它们互不影响——修改 stu1 的 age 不会改变 stu2 的 age。
1.3 结构体变量的初始化
有了结构体变量之后,第一件事自然是给它赋初始值。结构体支持在声明的同时用花括号 { } 按成员定义的顺序列出初始值:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
int main(void)
{
// 声明并初始化结构体变量
struct Student stu1 = {"小明", 18, 92.5};
// 也可以先声明,再逐个赋值
struct Student stu2;
(void)stu2; // 本节只演示声明,明确暂不读取未初始化成员
// stu2.name = "小红"; // 错误!字符数组不能这样赋值
// 后面会学到用 strcpy 给字符串成员赋值
// 输出结构体成员
printf("姓名:%s\n", stu1.name);
printf("年龄:%d\n", stu1.age);
printf("成绩:%.1f\n", stu1.score);
return 0;
}
输出:
姓名:小明
年龄:18
成绩:92.5
关于初始化字符串成员:在声明时初始化,可以用
char name[20] = "小明"这种形式。但如果已经声明了结构体变量,再想修改字符串成员,就不能直接用=赋值了,需要用strcpy函数。本章第二节会详细讨论这个问题。
补充:指定初始化器——按成员名赋值
上面展示的是按顺序初始化——花括号里的值必须严格按照结构体定义中成员的先后顺序排列。这种方式的缺点是:你必须记住每个成员的位置;如果结构体有十几个成员而你只想初始化其中三个,中间那些你不需要的成员也得在花括号里留出位置,非常不方便。
C 语言从 C99 标准开始,提供了一种更灵活的写法:指定初始化器(Designated Initializer)。它的语法是 .成员名 = 值,直接在花括号里点名赋值:
struct Student stu1 = {.name = "小明", .age = 18, .score = 92.5};
和按顺序初始化相比,指定初始化器有四个明显的好处:
(1)不依赖顺序。 你可以按任意顺序列出成员,代码的含义完全不受影响:
// 下面两种写法完全等价
struct Student stu1 = {.name = "小明", .age = 18, .score = 92.5};
struct Student stu2 = {.score = 92.5, .name = "小明", .age = 18}; // 顺序不同,结果一样
(2)自文档化。 看到 {.score = 92.5},读者不需要翻到结构体定义去数“第三个成员是什么“——成员名已经直接写在了值旁边。这在结构体成员较多(十几个甚至更多)时尤其有价值。
(3)可以只初始化部分成员。 未指定的成员会被自动初始化为 0(数值类型)或空(字符数组)。这个特性在只需要关心少数几个字段时非常实用:
struct Student stu3 = {.name = "小红", .score = 88.0};
// age 自动初始化为 0
注意:这种“未指定成员自动归零“的特性只在使用了指定初始化器时才成立。如果完全不用花括号初始化,局部结构体变量的成员值是未定义的(垃圾值)。
(4)更安全。 当结构体定义发生变更时——比如在中间插入一个新成员——按顺序初始化的代码可能出现“值填错了位置“的隐蔽 bug;而指定初始化器通过成员名精确对应,不受成员排列顺序变化的影响。
下面是一个完整的对比示例:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
int main(void)
{
// 风格一:按顺序初始化(传统方式)
struct Student stu1 = {"小明", 18, 92.5};
// 风格二:指定初始化器(推荐方式)
struct Student stu2 = {.name = "小明", .age = 18, .score = 92.5};
// 指定初始化器:只初始化部分成员
struct Student stu3 = {.name = "小红", .score = 88.0};
// age 未指定,自动初始化为 0
printf("stu1:%s, %d 岁, %.1f 分\n", stu1.name, stu1.age, stu1.score);
printf("stu2:%s, %d 岁, %.1f 分\n", stu2.name, stu2.age, stu2.score);
printf("stu3:%s, %d 岁, %.1f 分\n", stu3.name, stu3.age, stu3.score);
return 0;
}
输出:
stu1:小明, 18 岁, 92.5 分
stu2:小明, 18 岁, 92.5 分
stu3:小红, 0 岁, 88.0 分
选择建议:对于只有两三个成员的小结构体,按顺序初始化写起来更短;但对于成员较多、或者你只关心其中几个成员的结构体,指定初始化器明显更清晰、更安全。在实际项目中,指定初始化器越来越成为主流选择——它让代码的可读性和可维护性都上了一个台阶。
1.4 访问结构体成员:点运算符 .
定义了结构体变量并初始化之后,接下来的问题就是如何读取或修改其中的某个具体成员。答案是使用点运算符 .:
结构体变量名.成员名
点运算符就像一把钥匙,用来打开结构体这个“包裹“,取出里面的某个具体东西。通过它,你可以对结构体的任意成员进行读值和赋值操作:
struct Student stu1;
stu1.age = 19; // 给 age 成员赋值
stu1.score = 88.5; // 给 score 成员赋值
printf("%d\n", stu1.age); // 读取 age 成员的值
下面看一个完整的示例,演示成员的访问和修改:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
int main(void)
{
struct Student stu1 = {"小明", 18, 92.5};
// 使用点运算符访问成员
printf("姓名:%s\n", stu1.name);
printf("年龄:%d\n", stu1.age);
printf("成绩:%.1f\n", stu1.score);
// 修改成员的值
stu1.age = 19;
stu1.score = 95.0;
printf("\n--- 修改后 ---\n");
printf("年龄:%d\n", stu1.age);
printf("成绩:%.1f\n", stu1.score);
return 0;
}
输出:
姓名:小明
年龄:18
成绩:92.5
--- 修改后 ---
年龄:19
成绩:95.0
二、结构体进阶
上一节我们学会了定义单个结构体变量,并访问它的成员。这解决了“把相关属性打包在一起“的基本问题。但在实际编程中,我们很少只处理一条记录——一个班级有几十个学生,一个书架上有几百本书,一个系统中有成千上万个用户。因此,结构体必须和数组、函数配合使用,才能发挥真正的威力。
本节讨论结构体的四种进阶用法:
- 结构体数组:批量管理多条结构相同的记录
- 结构体与函数:传参、返回值的配合
- 字符串成员的正确赋值方式
- 结构体的嵌套:结构体里面放结构体
2.1 结构体数组——批量管理复杂数据
结构体和数组可以组合使用。结构体数组就是“一组结构体“,每个数组元素都是一个完整的结构体变量。这种组合非常适合管理多条结构相同的记录——比如一个班的所有学生:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
int main(void)
{
// 结构体数组:3 个学生
struct Student students[3] = {
{"小明", 18, 92.5},
{"小红", 19, 88.0},
{"小刚", 18, 95.5}
};
// 用循环遍历所有学生
for (int i = 0; i < 3; i++)
{
printf("学生 %d:%s,年龄 %d,成绩 %.1f\n",
i + 1, students[i].name, students[i].age, students[i].score);
}
return 0;
}
输出:
学生 1:小明,年龄 18,成绩 92.5
学生 2:小红,年龄 19,成绩 88.0
学生 3:小刚,年龄 18,成绩 95.5
访问方式分析:
students→ 数组名,代表一整排储物柜students[i]→ 第 i 个抽屉,里面是一个完整的结构体变量(整个学生信息包裹)students[i].name→ 打开第 i 个抽屉,拿出里面的name成员
通过 for 循环遍历数组下标,就能批量处理每一条记录。
提示:结构体数组同样可以使用指定初始化器。当每条记录的字段较多时,用
.成员名 = 值能让每个元素的内容一目了然:struct Student students[3] = { {.name = "小明", .age = 18, .score = 92.5}, {.name = "小红", .age = 19, .score = 88.0}, {.name = "小刚", .age = 18, .score = 95.5} };
2.2 结构体与函数
结构体数组解决了“多条记录“的管理问题,但实际程序还需要对记录执行各种操作——打印、排序、筛选、计算统计值等等。这些操作通常应该封装在函数中,而不是全部写在 main 里。这自然引出了一个问题:结构体变量能否像普通变量一样在函数之间传递?
答案是肯定的。
2.2.1 把结构体传递给函数
结构体变量可以作为函数参数传递,用法与 int、float 等基本类型完全一致:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
// 函数:打印学生信息(接收结构体参数)
void print_student(struct Student stu)
{
printf("姓名:%s,年龄:%d,成绩:%.1f\n", stu.name, stu.age, stu.score);
}
int main(void)
{
struct Student stu1 = {"小明", 18, 92.5};
struct Student stu2 = {"小红", 19, 88.0};
print_student(stu1); // 传递整个结构体
print_student(stu2);
return 0;
}
输出:
姓名:小明,年龄:18,成绩:92.5
姓名:小红,年龄:19,成绩:88.0
注意:上面的代码中,结构体是以传值的方式传递的。这意味着函数内部拿到的是一个副本——如果函数修改了
stu的成员,不会影响调用处的原始变量。这和普通变量的传值是一样的道理。当结构体较大时,复制整个结构体会消耗时间和内存;这个问题将在指针章节中得到解决。
2.2.2 用结构体作返回值
函数不仅可以接收结构体参数,也可以返回一个结构体。这在需要“创建并返回一条新记录“的场景中非常自然:
#include <stdio.h>
struct Student
{
char name[20];
int age;
float score;
};
void print_student(struct Student stu)
{
printf("姓名:%s,年龄:%d,成绩:%.1f\n", stu.name, stu.age, stu.score);
}
// 函数:创建一个学生并返回
struct Student create_student(void)
{
struct Student new_stu = {"新同学", 18, 80.0};
return new_stu; // 返回结构体
}
int main(void)
{
struct Student stu = create_student();
print_student(stu); // 姓名:新同学,年龄:18,成绩:80.0
return 0;
}
2.3 给字符串成员赋值:strcpy 的使用
在结构体与函数配合使用的过程中,有一个细节此前被刻意跳过了:字符串成员的赋值问题。前面提到,结构体声明时可以直接用花括号初始化字符串成员。但声明之后,如果要修改字符串成员,就不能直接用 = 了:
struct Student stu1;
stu1.name = "小明"; // 错误!字符数组不能被赋值
这是因为 C 语言中的字符数组是一个固定内存区域,数组对象不是可修改的左值(lvalue)——你不能把一个字符串常量直接“覆盖“给它。正确的做法是使用 strcpy 函数(需要 #include <string.h>):
#include <stdio.h>
#include <string.h>
struct Student
{
char name[20];
int age;
float score;
};
int main(void)
{
struct Student stu1;
// 用 strcpy 给字符串成员赋值
strcpy(stu1.name, "小明");
stu1.age = 18;
stu1.score = 92.5;
printf("姓名:%s,年龄:%d,成绩:%.1f\n", stu1.name, stu1.age, stu1.score);
// 修改名字
strcpy(stu1.name, "大明");
printf("修改后姓名:%s\n", stu1.name);
return 0;
}
strcpy 函数说明:
strcpy(目标字符数组, 源字符串);
- 把“源字符串“的内容(包括末尾的
'\0')复制到“目标字符数组“中 - 需要
#include <string.h> - 和直接操作数组一样,要确保目标数组足够大,否则会发生越界
2.4 结构体的嵌套——结构体里放结构体
到目前为止,结构体的成员都是基本类型(int、float、char[] 等)。但现实世界中的数据往往是分层的:一个学生不仅有姓名和成绩,还有一个出生日期,而日期本身又由年、月、日组成;一本书不仅有书名,还有一个出版社信息,而出版社又有名称和地址。这种层次结构自然要求结构体的嵌套——一个结构体的成员可以是另一个结构体。
下面以学生和出生日期为例来演示:
#include <stdio.h>
// 日期结构体
struct Date
{
int year;
int month;
int day;
};
// 学生结构体(嵌套了日期结构体)
struct Student
{
char name[20];
int age;
float score;
struct Date birthday; // 嵌套结构体成员
};
int main(void)
{
struct Student stu1 = {"小明", 18, 92.5, {2008, 3, 15}};
printf("姓名:%s\n", stu1.name);
printf("出生日期:%d 年 %d 月 %d 日\n",
stu1.birthday.year, // 多级点运算符
stu1.birthday.month,
stu1.birthday.day);
return 0;
}
输出:
姓名:小明
出生日期:2008 年 3 月 15 日
多级访问分析:stu1.birthday 拿到的是 birthday 这个成员,它本身是一个 struct Date 类型的变量;再通过 .year 从这个日期变量中拿到 year 成员。这就像打开一个大包裹,里面还有一个小包裹,需要再打开一次。嵌套的层数没有限制,但层次过深会使代码的可读性下降——通常在两层以内是合理的。
嵌套结构体的指定初始化器。对于嵌套结构体,指定初始化器的优势更加突出——它不需要记住内层成员的排列顺序,而且一眼就能看出每个值属于哪个字段:
// 按顺序初始化(原始写法)——需要记住 {年, 月, 日} 的顺序
struct Student stu1 = {"小明", 18, 92.5, {2008, 3, 15}};
// 指定初始化器——每个值都标注了所属成员,层次关系一目了然
struct Student stu2 = {
.name = "小明",
.age = 18,
.score = 92.5,
.birthday = {.year = 2008, .month = 3, .day = 15}
};
// 也可以只初始化部分嵌套成员,未指定的自动为 0
struct Student stu3 = {
.name = "小红",
.birthday = {.year = 2009, .month = 6}
// birthday.day 自动为 0,age 和 score 也自动为 0
};
可以看到,指定初始化器的 . 层层嵌套,和访问成员时的 stu1.birthday.year 形成了完美的对称——初始化的写法和读取的写法在结构上是一致的。这种对称感让代码的意图非常清晰,阅读时几乎不需要思考。
三、typedef —— 给类型起别名
前面的两节,我们一直在和各种类型打交道——int、float、char、struct Student……每一次声明变量,都要把类型的完整名字写出来。对于基本类型来说,这倒不算什么负担;但对于结构体,每次声明变量都要在前面加一个 struct 关键字,写久了难免觉得啰嗦。写 struct Student stu1; 总不如写 Student stu1; 来得直接。
这就引出了本节的主题——typedef。它的作用是给已有的类型起一个新名字(别名)。有了它,你可以把冗长的类型名简化,也可以给抽象的类型赋予一个更具可读性的名称。
关键理解:
typedef不创建任何新类型——它只是给现有的类型贴上另一张标签。就像一个人既有本名又有绰号,叫哪个名字指的都是同一个人。
3.1 基本类型别名
先从一个最简单的例子入手。假如你正在写一个和年龄相关的程序,用 int 来存储年龄。但代码中到处都是 int age,光看类型名并不能立刻传达“这是一个年龄值“的语义。你可以用 typedef 给它取一个更有意义的名字:
typedef int Age;
Age student_age = 18; // 等价于 int student_age = 18;
Age teacher_age = 35; // 等价于 int teacher_age = 35;
经过 typedef int Age; 之后,Age 就成了 int 的别名。此后你用 Age 声明变量,和用 int 声明变量完全等价——编译器会把它们当作同一个类型来处理。
如何阅读 typedef 声明? typedef 的语法乍看有些别扭,但它有一个简单的阅读法则:
把
typedef去掉,剩下的部分就是一个普通的变量声明;而这个声明的变量名,就是新类型的别名。
以 typedef int Age; 为例:
- 去掉
typedef,得到int Age;→ 一个普通变量声明,表示“Age是一个int类型的变量“ - 加上
typedef→ 含义变为“Age是int类型的别名“,不再是变量
换句话说,typedef 把本该是变量名的位置,变成了类型名。
再看几个例子巩固这个阅读法则:
typedef unsigned long size_t; // unsigned long 的别名
typedef char String[100]; // 长度为 100 的字符数组的别名
typedef int* IntPtr; // int* 的别名
按照阅读法则:去掉 typedef,char String[100] 声明了一个长度为 100 的字符数组变量 String;加上 typedef,String 就成了“长度为 100 的字符数组“这一类型的别名。之后写 String name; 就等价于写 char name[100];。同样,IntPtr 就是 int* 的别名,之后写 IntPtr p; 等价于 int* p;。
3.2 typedef 与结构体——让代码告别 struct 前缀
typedef 最频繁的应用场景,就是和结构体搭配使用。回顾一下前面写的所有代码,每次声明结构体变量都要写完整的 struct Student:
struct Student stu1;
struct Student stu2;
struct Student create_student(void);
void print_student(struct Student stu);
这几个字反复出现,写起来相当繁琐。如果你接触过 C++ 或 Java,你会发现那些语言中声明结构体或类完全不需要 struct 前缀——直接写类型名就行了。C 语言虽然做不到这一点,但借助 typedef 可以达到同样的效果。
使用 typedef 给结构体起别名的做法有两种风格。
方式一:分开写。 先定义结构体类型,再用 typedef 给它起别名:
struct Student
{
char name[20];
int age;
float score;
};
typedef struct Student Student; // 给 struct Student 起别名为 Student
此后,你可以直接用 Student 来代替 struct Student:
Student stu1 = {.name = "小明", .age = 18, .score = 92.5}; // 等价于 struct Student stu1 = ...
Student students[3] = {{...}, {...}, {...}}; // 等价于 struct Student students[3] = ...
void print_student(Student stu) { ... } // 等价于 struct Student stu
这种写法的好处是思路清晰:定义类型归定义类型,起别名归起别名。而且 struct Student 这个名字依然可用——两种写法都可以,你可以在新旧代码之间平滑过渡。
方式二:合在一起写(最常见)。 C 语言允许在定义结构体的同时使用 typedef,一步到位:
typedef struct
{
char name[20];
int age;
float score;
} Student;
注意这里花括号前面没有结构体名(也可以有,但写了也用不大到),花括号后面紧跟的是别名。这一句同时做了三件事:定义了一个匿名的结构体类型,给它起别名为 Student,之后就能直接用 Student 声明变量。这是实际项目中最常见的写法,几乎每一个 C 程序员都会这样写:
Student stu1 = {.name = "小明", .age = 18, .score = 92.5};
Student stu2;
void print_student(Student stu)
{
printf("姓名:%s,年龄:%d,成绩:%.1f\n", stu.name, stu.age, stu.score);
}
没有了 struct 前缀的干扰,代码读起来清爽了许多。
两种方式的对比与取舍:
| 方式 | 写法 | 优点 | 缺点 |
|---|---|---|---|
| 分开写 | struct Student { ... };typedef struct Student Student; | 有显式的结构体标签名,可以在结构体内部引用自身(如链表的自引用) | 需要两行,略啰嗦 |
| 合一起 | typedef struct { ... } Student; | 简洁,一步到位 | 无标签名,不能自引用 |
在后面的指针和链表章节中,你会遇到需要在结构体内部包含一个指向自身类型的指针的情况。这时必须使用方式一(保留结构体标签名),因为 typedef 创建的别名在结构体定义完成之前还不可用:
typedef struct Node
{
int data;
struct Node* next; // 这里只能用 struct Node,别名 Node 还没生效
} Node;
现阶段你暂时不需要关心自引用的问题——在学习链表之前,放心使用方式二的合写形式即可,它会让你的代码干净很多。
四、动手练习
#include <stdio.h>
#include <string.h>
// 用 typedef 定义图书结构体(合写方式)
typedef struct
{
char title[50];
char author[30];
int pages;
float price;
} Book;
int main(void)
{
// 练习1:结构体数组存储图书信息(指定初始化器写法,字段含义一目了然)
Book books[3] = {
{.title = "C语言入门", .author = "张三", .pages = 320, .price = 45.5},
{.title = "数据结构", .author = "李四", .pages = 450, .price = 59.0},
{.title = "算法导论", .author = "王五", .pages = 600, .price = 79.5}
};
printf("===== 图书信息表 =====\n");
for (int i = 0; i < 3; i++)
{
printf("书名:%s\n", books[i].title);
printf("作者:%s\n", books[i].author);
printf("页数:%d\n", books[i].pages);
printf("价格:%.1f 元\n", books[i].price);
printf("----------------------\n");
}
// 练习2:找出最贵的书
int max_index = 0;
for (int i = 1; i < 3; i++)
{
if (books[i].price > books[max_index].price)
{
max_index = i;
}
}
printf("最贵的书是:《%s》,价格 %.1f 元\n\n",
books[max_index].title, books[max_index].price);
// 练习3:嵌套结构体——学生与地址(使用 typedef)
typedef struct
{
char city[20];
char street[50];
} Address;
typedef struct
{
char name[20];
int age;
Address addr; // 注意:用别名声明,不需要 struct 前缀
} Person;
// 练习3:嵌套结构体的指定初始化器——层次关系非常清晰
Person person1 = {
.name = "小明",
.age = 20,
.addr = {.city = "上海", .street = "南京路100号"}
};
printf("姓名:%s\n", person1.name);
printf("年龄:%d\n", person1.age);
printf("城市:%s\n", person1.addr.city);
printf("街道:%s\n", person1.addr.street);
return 0;
}