结构和其他数据形式
掌握结构体声明与布局、嵌套和数组、函数传递、联合体判别、枚举、typedef与复合字面量
学习目标
- 能定义、嵌套并指定初始化结构体,解释 padding、结构数组步长和复合字面量生存期
- 能比较结构体按值传递、返回与 const 指针借用,并为函数接口选择点号或箭头访问
- 能实现 enum 判别的 union,说明读取非当前成员为何不是数值转换且可能遇到陷阱表示
机制总览
结构和其他数据形式:机制路径
- 1
为什么要把多种数据「绑」在一起?
程序里常要描述 一个整体 :一本书有书名、价格、页数;一名学生有学号、姓名、成绩。若用三个互不相关的变量,容易传参漏项、排序时对不齐。
- 2
定义 struct 与成员访问
struct book novel = .title = "C Primer Plus", .price = 29.9f, .pages = 720 ;
- 3
结构体数组
若 sizeof(struct book) 为 52,则 shelf[1] 的起始地址比 shelf[0] 大 52 字节。这与「数组元素等宽连续」的规则一致,只是元素类型变「宽」了。
章级决策实验
结构和其他数据形式:机制与证据
切换《结构和其他数据形式》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么要把多种数据「绑」在一起?
程序里常要描述 一个整体 :一本书有书名、价格、页数;一名学生有学号、姓名、成绩。若用三个互不相关的变量,容易传参漏项、排序时对不齐。
可核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么要把多种数据「绑」在一起?」的实际行为。
学完《结构和其他数据形式》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
结构和其他数据形式:失效与核验
为什么要把多种数据「绑」在一起?
典型失效
若只记语法而忽略「为什么要把多种数据「绑」在一起?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么要把多种数据「绑」在一起?」的实际行为。
定义 struct 与成员访问
典型失效
若只记语法而忽略「定义 struct 与成员访问」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「定义 struct 与成员访问」的实际行为。
结构体数组
典型失效
若只记语法而忽略「结构体数组」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「结构体数组」的实际行为。
为什么要把多种数据「绑」在一起?
程序里常要描述一个整体:一本书有书名、价格、页数;一名学生有学号、姓名、成绩。若用三个互不相关的变量,容易传参漏项、排序时对不齐。
想象一张表格的一行:列与列属于同一条记录。C 的 ↡把多个不同类型的成员组合成一种用户自定义类型;成员按声明顺序存储(含对齐 padding)。 就是给「一行数据」起名字,让编译器把各列打包进一块连续内存。
没有 struct,你只能传一长串参数或平行数组——维护成本高,也谈不上类型安全。这一章还介绍 typedef(起别名)、union(省内存的「多选一」存储)和 enum(可读常量)。
定义 struct 与成员访问
struct book {
char title[40];
float price;
int pages;
};
struct book novel = {
.title = "C Primer Plus",
.price = 29.9f,
.pages = 720
};↡结构体或联合体内部的具名字段,通过对象点号成员或指针箭头成员访问。成员类型须满足完整对象类型等声明约束。 可以是标量、数组或另一个完整结构体等对象类型;结构体最后还可按规则使用一个柔性数组成员。初始化可用 C99 指定初始化:
struct book gift = { .title = "Demo", .price = 0.0f, .pages = 120 };实现为满足 ↡对象类型对存储地址倍数的实现要求;实现可在结构成员之间和末尾插入填充以满足成员及结构数组元素的对齐。 可以在成员间和结构尾部插入 padding。结构体开头不会有未命名填充,非位字段成员地址按声明顺序递增,但具体偏移与 sizeof(struct book) 由实现决定。序列化不能把字段大小简单相加来猜布局。
结构体数组
数组元素可以是 struct,元素在内存中一个接一个:
struct book shelf[3];
shelf[0].price = 19.9;
shelf[1].pages = 400;若 sizeof(struct book) 为 52,则 shelf[1] 的起始地址比 shelf[0] 大 52 字节。这与「数组元素等宽连续」的规则一致,只是元素类型变「宽」了。
嵌套结构:成员本身也是完整记录
↡把一个完整结构体对象声明为另一个结构体的成员;外层对象按值拥有内层对象,并用连续点号逐层访问。 适合把可复用子记录组合起来:
struct date {
int year, month, day;
};
struct loan {
struct book item;
struct date due;
};
struct loan current = {
.item = { .title = "C11", .price = 39.0f, .pages = 320 },
.due = { .year = 2026, .month = 7, .day = 31 }
};访问到期月份写 current.due.month。内层结构按值属于外层对象,不是自动分配的指针;复制 current 会连同 item 与 due 的成员一起复制。
结构指针:. 与 ->
结构体可以按值传入函数,语义上得到独立副本;是否产生实际内存拷贝由优化器决定。需要观察同一对象或避免在接口中复制大记录时,可传 ↡指向 struct 实例的指针;只有指向生存期内有效且满足对齐的对象时,才能通过箭头或先解引用后点号访问。:
void show_price(const struct book *pb) {
printf("%.2f\n", pb->price);
}
struct book b = { "Demo", 25.0f, 200 };
show_price(&b);第一步:变量与点号
对结构体变量用 .:b.price。左操作数必须是 struct 类型,不是指针。
函数与结构:值传递、返回值和指针借用
↡结构体对象可像标量一样作为实参按值传递、由函数返回或赋值;语义上复制成员值,源对象与目标对象随后独立。 适合小型值对象:
struct point { double x, y; };
struct point translated(struct point value, double dx, double dy) {
value.x += dx;
value.y += dy;
return value;
}
int main(void) {
struct point origin = {0.0, 0.0};
struct point moved = translated(origin, 2.0, 3.0);
return origin.x != 0.0 || moved.x != 2.0;
}结构体赋值和传值包含数组成员的值,但 padding 字节不属于可移植值语义,不能用 memcmp 代替逐成员相等比较。对于借用接口,const struct book * 表示函数不经该指针修改对象;调用者仍负责保证指针非空(若契约要求)、对齐正确且在整个调用期间指向生存期内对象。需要函数修改调用方结构时,才传非 const 指针并在接口中说明修改哪些成员。
typedef:给类型起别名
struct book 每次都要写两个单词。可用 ↡为已有类型创建别名;不创建新类型,只是语法糖。如 typedef struct book Book; 简化:
typedef struct book {
char title[40];
float price;
int pages;
} Book;
Book novel; /* 等价 struct book novel; */
Book *ptr = &novel;C11 也允许:
typedef struct book Book;
struct book { ... };对 enum 同样适用:typedef enum { RED, GREEN } Color;。别名不改变内存布局,只让声明更短、可读性更好。
结构体复合字面量:在表达式里创建对象
C99 的 ↡以括号类型名加初始化列表创建未命名结构体或联合体对象;块作用域对象具有自动存储期,可在包含块结束前取址使用。 可以直接作为函数实参:
void show_book(const struct book *value);
show_book(&(struct book){
.title = "Temporary",
.price = 12.5f,
.pages = 96
});这不是“只活到分号”的临时值:块作用域复合字面量对象生存到包含块结束,文件作用域复合字面量则具有静态存储期。但保存其地址并在对应生存期结束后使用仍会悬空。
联合体 union:同一块内存,多种解释
↡所有成员重叠于同一存储并具有相同起始地址;大小足以容纳最大成员且还可能因对齐含尾部填充。 与 struct 相反——成员重叠而非顺排:
union hold {
int i;
float f;
char c;
};
union hold u;
u.i = 65;
printf("%d\n", u.i); /* 65 */写入 u.i 后再读 u.f 不是把整数数值转换为浮点数,而是让另一成员类型解释相应对象表示。结果依赖 int 与 float 表示;若形成陷阱表示,求值可能产生未定义行为。只有联合体内若干结构共享的“共同初始序列”等标准特例可跨当前成员可靠观察。需要数值转换应写强制转换,需要可移植字节编码应定义格式并逐字段处理。
联合体本身不记录“当前是哪种含义”,程序应使用 enum 判别字段构成 tagged union,并在写 payload 时同步更新标签、在读 payload 前先检查标签。
典型用途:嵌入式寄存器位域、网络协议里多种 payload 互斥、在 int/float 间复用 4 字节以省内存。
枚举 enum:命名整数常量
↡声明一组具名枚举常量并形成枚举类型;C11中各枚举常量具有int类型,枚举类型与char或某个有符号或无符号整数类型兼容,具体由实现选择。 替代魔法数字:
enum color { RED, GREEN, BLUE, YELLOW };
enum color c = GREEN;
switch (c) {
case RED: puts("红"); break;
case GREEN: puts("绿"); break;
default: break;
}可显式赋值:enum { JAN = 1, FEB, MAR }; 则 FEB=2。C11 要求枚举常量可表示为 int;枚举对象的兼容整数类型由实现选择。不要依赖 sizeof(enum color) == sizeof(int),也不要把任意 int 都当成合法业务状态;进入 switch 前仍应验证外部数据。
代码详解:图书管理小片段
定义与 typedef
typedef struct {
char isbn[14];
float price;
int stock;
} Item;
Item inv = { "978-013", 59.0f, 10 };结构体数组与指针传参
void restock(Item *p, int add) {
if (p != NULL)
p->stock += add;
}
Item warehouse[100];
warehouse[0].stock = 5;
restock(&warehouse[0], 20);union 与 enum 配合( tagged union 思路)
enum ValueKind { VK_INT, VK_FLOAT };
struct Value {
enum ValueKind kind;
union {
int i;
float f;
} data;
};
struct Value v;
v.kind = VK_INT;
v.data.i = 42;
/* 读 v.data.i 前应先检查 v.kind == VK_INT */容易踩的坑
小结
- struct 把相关成员打包;
.访问变量成员;数组元素连续存放 - 嵌套结构按值拥有子记录;结构体可赋值、传值和返回,padding 不参与可移植相等语义
- const 结构体指针表达只读借用;
->访问指针成员且要求有效生存期 - typedef 为 struct/enum 起别名,不新造类型
- 复合字面量创建有明确存储期的未命名对象,不一定只活到当前表达式
- union 成员重叠但不自带标签;enum 判别字段决定当前允许读取的 payload
- C11 枚举常量是 int,枚举类型的兼容整数类型由实现选择
练习
问题 1(问答型) struct { char a; double b; } 的 sizeof 为什么往往大于 9?
问题 2(改代码型) 下面哪里错了?如何改?
struct Point { int x; int y; };
struct Point *pp;
pp.x = 1;问题 3(综合实现型) 用 typedef 定义 Student { char name[32]; int id; },声明含 3 个元素的数组,初始化第二名 id=1002,写函数 void print_id(const Student *s) 打印 id。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 结构体(struct)
用户自定义类型,包含多个成员;成员顺序存储,可能有 padding。见 StructMemoryLayoutDiagram。
- 成员(member)
struct 或 union 内部的具名字段;用点号或箭头访问。
- 对齐(alignment)
类型对对象地址倍数的实现要求。实现可在结构成员之间和末尾插入 padding,以满足成员及结构数组元素的对齐。
- 结构体指针
指向 struct 的指针;函数传参避免拷贝;用 -> 访问成员。
- typedef
为已有类型创建别名,如 typedef struct book Book; 简化声明。
- 联合体(union)
成员共享起始地址与存储,大小足以容纳最大成员且可能含尾部填充。业务代码通常用枚举标签记录当前允许读取的成员。
- 枚举(enum)
命名枚举常量集合。C11 枚举常量具有 int 类型,枚举类型与某种整数类型兼容,具体选择由实现决定。见 EnumDiagram。
- 嵌套结构
一个结构体对象作为另一个结构体的成员。外层按值拥有内层记录,使用连续点号逐层访问,复制外层也复制内层成员值。
- 结构体值传递
结构体可作为实参按值传递、作为返回值或用于赋值,语义上复制成员值。padding 字节不应参与可移植相等比较。
- 结构体复合字面量
以
(struct 类型){初始化项}创建未命名对象。块作用域对象生存到包含块结束,文件作用域对象具有静态存储期。
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
结构指针:机制、边界与证据
在《结构和其他数据形式》的官方单元 cpr-14 中,结构指针连接本章第 3 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。