1. 初识结构体struct
结构体(Structures)是一种用户可以自定义的类型,用来储存结构化的数据并实现一些功能。
结构体定义时必须以struct开头,后面是自定义的结构体名,大括号内部是这个结构体所包含的成员,这些成员可以是常见的任意基本数据类型,各个成员之间用;隔开。
例如一个日期的结构体定义:
1 | struct Date { |
这个结构体中包含了三个成员,可以将年份、月份、天数统一结构化地存储在这个结构体中,方便管理和使用。
结构体和其他数据类型一样,定义好后可以初始化使用,可以在初始化时指定每种成员变量的初始值。
结构体初始化同样需要以struct开头,后面是想要初始化的结构体名,然后写任意自定义的初始化结构体名称。初始化的各个成员之间用,隔开。
例如日期结构体的初始化:
1 | struct Date today = { 2026, 1, 1 }; |
初始化值的各个成员需要与定义时的成员一一对应。
需要注意的是,只有初始化结构体的时候才可以整体赋值,在非初始化时只能逐个成员赋值,不能整体赋值。
1
2
3
4
5 struct Date today = { 2026, 1, 1 }; // 正确:定义时初始化
struct Date today;
//today = { 2026, 1, 1 }; // 错误:不能这样赋值
today = (struct Date){ 2026, 1, 1 }; // 正确:使用复合字面值赋值
2. 创建结构体变量与访问方式
一般来说,创建和定义结构体时必须写struct关键字,表示这是一个结构体,不过也可以使用typedef来为这个结构体创建一个别名,例如:
1 | // 使用typedef为结构体类型定义别名为Date |
这样就可以直接使用Date对结构体进行初始化或赋值等操作:
1 | struct Date today = { 2026, 1, 1 }; // 正确:使用 struct 关键字 |
结构体一般会定义在全局,或直接将所有结构体放在一个单独的文件中,方便所有的函数使用。
访问结构体中成员的值有两种方式:
通过成员访问(.)
可以使用“自定义的结构体名.结构体中的某个成员”的方式访问结构体中成员的数据。例如:
1 | Date today = { 2026, 1, 1 }; |
通过指针访问(->)
当定义一个指向结构体的指针时,可以使用“自定义的指向结构体的指针->结构体中的某个成员”的方式访问结构体中成员的数据。例如:
1 | Date today = { 2026, 1, 1 }; |
3. 匿名结构体
匿名结构体没有它自己的结构体标签,只有一个别名。
对于较为简单的结构体,可以使用匿名结构体,并用typedef为其定义一个别名,例如:
1 | typedef struct { |
这种定义方式较为简洁,但也有一些缺点。
这样定义时,因为是匿名结构体,在初始化或赋值时只能使用别名:
1 | //struct Date today = { 2026, 1, 1 }; // 错误:struct Date 未定义 |
同样的,匿名结构体由于没有结构体标签,因此无法自引用,无法实现结构体链表。例如在有结构体标签的结构体中,可以实现引用自身:
1 | typedef struct Node { // ← 必须有标签名 |
而匿名结构体中无法实现。
此外,当一个结构体的声明和定义分别在不同的文件中时,也不可以使用匿名结构体。例如:
1 | // 头文件 a.h |
4. 函数参数为结构体
函数的参数也可以为结构体,可以在函数中实现读取或修改结构体中的成员变量的功能。
要求:一个简单的学生分数管理工具,学生的信息包括:姓名、ID号、分数,可以实现:
- 更新学生的分数值;
- 打印学生的所有信息。
在函数中更新结构体中成员变量值时,需要向函数中传递结构体和一个更新值。
与向函数中传递参数类似,此时函数中的所有参数都是局部变量,作用域只有函数内部,函数结束后即被销毁,因此直接在这个函数中修改结构体的值时,修改的是原结构体的一个副本,对原值无影响。
所以想要在外部函数中修改结构体中成员的值,需要向函数中传递一个结构体指针,同时使用结构体的指针访问的形式(->)来修改值,这时的值即可被成功修改。
代码实现:
1 |
|
同样的,打印学生信息的函数也可以改为使用指针向函数中传递结构体,这样可以避免每次调用打印函数时复制整个结构体,避免浪费性能和栈空间。
5. 值语义初始化结构体变量
函数的返回值也可以是一个结构体类型,因此可以通过一个函数对结构体进行初始化,例如:
1 |
|
这个程序使用了值语义初始化结构体,获取坐标时操作对象的值本身,而不是操作指向对象的引用或指针。
这种方法保证了传递出去的是最初始的值的一个副本,而最初始的值是一个局部变量,在函数结束之后即被销毁,不会将指针传递出去,避免外部程序通过其指针(悬垂指针)修改原始值,更为安全。
此外,使用统一的函数初始化值时,便于程序逻辑与内存分配的统一管理。
6. 结构体数组
结构体定义之后,也可以创建一个包含多个结构体的数组。
1 |
|
7. 嵌套结构体
结构体的成员也可以是结构体,被称为结构体的嵌套。
通过成员访问嵌套结构体中的成员时,可以使用多个.来逐级访问;通过指针访问嵌套结构体,第一层结构体需要使用->访问成员,成员中的结构体同样使用.访问即可。
例如定义一个人所包含的信息结构体,这个结构体中嵌套一个包含地址信息的结构体:
1 |
|
8. Enumeration枚举
枚举与结构体类似,也是可以自定义的一种数据类型。
枚举的作用类似于#define,可以将多个常量统一定义,便于后期读取和维护。
1 |
|
枚举的成员默认从0开始对应,依次递增。
也可以手动指定枚举所对应的值,之后的对应值会从指定值的基础上递增。
1 | typedef enum { |
当手动指定值的成员不是第一个时,前面没有被指定的成员对应的值依然会从0开始依次递增,指定值后的成员会从指定值开始逐个加1。
1
2
3
4
5
6
7
8
9 typedef enum {
MONDAY, // 0
TUESDAY, // 1
WEDNESDAY = 5, // 5
THURSDAY, // 6
FRIDAY, // 7
SATURDAY, // 8
SUNDAY // 9
} Weekday;
由于枚举和define类似,对应的值只是一个数字,因此想要输出字符串时只能定义新函数。
1 | const char* get_weekday_string(Weekday day) { |
此外,当定义多个枚举类型时,如果不手动指定,每个枚举类型中的第一个成员对应的值默认都会从0开始,这就会导致一个数字与不同枚举类型的多个成员对应,此时编译器也不会报错,因为在编译器眼中不同成员对应的都是一个数字,容易导致错误。
要避免这类型问题,需要手动指定枚举类型成员的起始值,避免不同枚举类型的值出现重叠,同时可以在使用枚举类型之前做范围检查,确保使用的是预期的枚举类型。
9. Union联合
联合与结构体、枚举类似,也是一种可以自定义的特殊的数据类型。
它允许在相同的内存位置储存不同的数据类型,也就是所有不同类型的成员共享同一块内存空间,大小等于占用内存空间最大的成员的大小。
因此在任一时刻,联合体只能储存一个成员的值。
一个变量可能存储多种类型的数据,但在一个给定时刻,只使用其中的一种数据类型,可以节省内存空间。
使用这种特性,可以结合枚举和结构体,自定义一种可以储存任意数据类型的结构体,可以通过枚举成员的值判断数据类型并打印。
1 |
|
10. 游戏设计:结构体、枚举、联合与多文件编程
对于较为复杂的程序设计,不可能在一个文件中包含所有的代码,必须涉及到多文件编程。
要求:使用多文件编程设计一个简单的游戏,包括:
- 玩家和敌人的结构体定义,包含玩家的姓名、等级、职业、能力等,包含敌人的种类、等级、职业、能力等;
- 多种游戏相关的函数,包括游戏初始化、生成玩家、生成敌人、玩家和敌人的战斗、打印玩家或敌人信息等。
这个简单游戏的代码示例同时也会演示标准注释的撰写方法。
game_types.h:游戏类型定义
需要先在“头文件”目录下创建一个game_types.h文件,用于存放玩家和敌人的角色种类的枚举类型:
1 | /** |
这里头文件的头和尾是一种固定的格式,防止头文件被多重包含。
game_abilities.h:游戏能力定义
创建一个game_abilities.h文件,使用联合类型定义玩家或敌人在某些职业下的能力:
1 | /** |
game_structs.h:游戏结构体定义
创建一个game_structs.h文件,用于定义玩家和敌人的结构体类型,成员中包含前面定义的类别和能力:
在这个文件中使用其他文件中定义的内容,必须使用
include引入。与引用官方库不同的是,引用官方库需要使用尖括号(
<>),引用自己定义的库使用的是双引号(“”)。
1 | /** |
至此,玩家和敌人的基本属性定义完成,可以开始写一些游戏的功能函数。
game_functions.h:游戏函数声明
可以创建一个game_functions.h文件,用于声明一些游戏功能函数:
.h文件用来声明所有的函数,.c文件用来定义函数的具体功能。其他文件中需要使用函数时,直接包含
.h文件即可。
1 | /** |
game_functions.c:实现功能函数
接着就可以在“源文件”的目录下创建一个game_functions.c文件,对声明的功能函数做具体的定义和实现,这些函数就是游戏的核心功能:
1 | /** |
最后,在主函数文件main.c中包含功能头文件,并调用一个初始化函数即可:
1 |
|
这个案例只是一个简单的游戏逻辑,还有很多可以改进和优化的地方,例如增加游戏循环逻辑、增加升级后的属性提升系统等,有兴趣的朋友可以在此基础上进一步修改和完善。
11. 第九章结束语
结构体、枚举、联合的重点是如何根据想要实现的功能,去设计一个自定义的类型。
要想清楚自定义的类型中应该包含哪些成员,是否需要结构体和枚举、联合的嵌套。
理解、消化、训练,都是必不可少的。
本文链接: https://hanqingjiang.com/2025/12/28/20251228_C_struct/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
