结构和其他数据形式

掌握结构体声明与布局、嵌套和数组、函数传递、联合体判别、枚举、typedef与复合字面量

学习目标

  • 能定义、嵌套并指定初始化结构体,解释 padding、结构数组步长和复合字面量生存期
  • 能比较结构体按值传递、返回与 const 指针借用,并为函数接口选择点号或箭头访问
  • 能实现 enum 判别的 union,说明读取非当前成员为何不是数值转换且可能遇到陷阱表示

机制总览

结构和其他数据形式:机制路径

  1. 1

    为什么要把多种数据「绑」在一起?

    程序里常要描述 一个整体 :一本书有书名、价格、页数;一名学生有学号、姓名、成绩。若用三个互不相关的变量,容易传参漏项、排序时对不齐。

  2. 2

    定义 struct 与成员访问

    struct book novel = .title = "C Primer Plus", .price = 29.9f, .pages = 720 ;

  3. 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 的 就是给「一行数据」起名字,让编译器把各列打包进一块连续内存。

没有 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 的成员一起复制。

结构指针:.->

结构体可以按值传入函数,语义上得到独立副本;是否产生实际内存拷贝由优化器决定。需要观察同一对象或避免在接口中复制大记录时,可传

void show_price(const struct book *pb) {
    printf("%.2f\n", pb->price);
}
 
struct book b = { "Demo", 25.0f, 200 };
show_price(&b);
. 与 -> 访问结构体成员① 结构体变量struct Book b;b.price = 29.9;变量名 . 成员② 取地址struct Book *pb;pb = &b;pb 指向 b 的首地址③ 箭头访问pb->price = 19.9;(*pb).price 等价指针名 -> 成员struct Book b 在内存中titlepricepagespb →pb->price 是 (*pb).price 的语法糖;函数参数常用 struct T * 避免拷贝
结构体变量用点号访问成员;指向结构体的指针用箭头(或先解引用再点号)。传 struct 指针进函数可避免整结构体拷贝。
分步1 / 3

第一步:变量与点号

. 与 -> 访问结构体成员① 结构体变量struct Book b;b.price = 29.9;变量名 . 成员② 取地址struct Book *pb;pb = &b;pb 指向 b 的首地址③ 箭头访问pb->price = 19.9;(*pb).price 等价指针名 -> 成员struct Book b 在内存中titlepricepagesb.price:直接通过变量名访问成员
结构体变量用点号访问成员;指向结构体的指针用箭头(或先解引用再点号)。传 struct 指针进函数可避免整结构体拷贝。

结构体变量.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 {
    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 */
union Data { int i; float f; char c; }所有成员从同一地址开始;sizeof(union) = 最大成员的大小共享内存块(4 字节示意)u.i = 0x41424344u.f(同一地址,不同解释)u.c写入 intu.i = 65;此时有效的是 i再读 float?x = u.f;位模式被当 float 解释使用规则· 同一时刻只应通过「最后写入的那种成员类型」来读· 典型用途:节省内存(多种表示互斥)、协议字段复用、与硬件寄存器布局对应· C11 起可用 tagged union 模式:另存 enum 标签记录当前有效成员(编译器不自动检查)
联合体的成员重叠在同一块内存上;写入一种类型后再用另一种类型读取,得到的是同一串字节的另一种解释——通常不是有意义的数值。

写入 u.i 后再读 u.f 不是把整数数值转换为浮点数,而是让另一成员类型解释相应对象表示。结果依赖 int 与 float 表示;若形成陷阱表示,求值可能产生未定义行为。只有联合体内若干结构共享的“共同初始序列”等标准特例可跨当前成员可靠观察。需要数值转换应写强制转换,需要可移植字节编码应定义格式并逐字段处理。

联合体本身不记录“当前是哪种含义”,程序应使用 enum 判别字段构成 tagged union,并在写 payload 时同步更新标签、在读 payload 前先检查标签。

典型用途:嵌入式寄存器位域、网络协议里多种 payload 互斥、在 int/float 间复用 4 字节以省内存。

枚举 enum:命名整数常量

替代魔法数字:

enum color { RED, GREEN, BLUE, YELLOW };
enum color c = GREEN;
 
switch (c) {
    case RED:   puts("红"); break;
    case GREEN: puts("绿"); break;
    default:    break;
}
enum color { RED, GREEN, BLUE, YELLOW }未赋值的枚举常从 0 起递增;也可显式指定如 BLUE = 10enum color c = GREEN; /* c 的值是 1 */标识符整数值RED0GREEN1BLUE2YELLOW3switch (c) 可分支处理;printf 用 %d 打印枚举的整数值typedef enum { ... } Color; 可像 struct 一样起别名
枚举把一组相关的命名常量映射为整数,替代魔法数字,让 switch 与函数参数更易读;底层类型通常是 int。

可显式赋值: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 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

讨论

评论区加载中…