这一章在讲什么?
一本书有书名、作者、价格,用 7 个零散数组存藏书信息?改一次排序就要同步改 7 个数组,必乱无疑。C 的结构(structure)把一组相关数据打包成一个整体——这正是”记录”思想在 C 里的化身。本章还引出联合(union)、枚举(enum)、typedef 与函数指针,构成 C 数据表示的进阶武器库。
14.1 问题示例:创建书籍清单
Gwen 想给藏书做清单,每本书要记书名、作者、价值。与其维护三个平行数组,不如定义一个”书的模板”,让一个变量同时装下三样东西。清单 14.1 book.c 是最小示例:
/* book.c -- 一本书的清单 */
#include <stdio.h>
#define MAXTITL 41 /* 书名最大长度 + 1 */
#define MAXAUTL 31 /* 作者名最大长度 + 1 */
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
}; /* 结构模板结束 */
int main(void)
{
struct book library; /* 声明 library 为一个 book 变量 */
printf("Please enter the book title.\n");
gets(library.title); /* 访问 title 成员 */
printf("Now enter the author.\n");
gets(library.author);
printf("Now enter the value.\n");
scanf("%f", &library.value);
printf("%s by %s: $%.2f\n", library.title, library.author, library.value);
printf("%s: \"%s\" ($%.2f)\n", library.author, library.title, library.value);
printf("Done.\n");
return 0;
}你需要掌握三个基本功:建立结构布局、按布局声明变量、访问变量的各个成员。
建立结构声明
模板声明(即清单 14.1 开头的 struct book {...};)中:struct 关键字表明后面是一个结构;book 是标记(tag),可用来日后引用这个设计;大括号里是成员列表(member list),每个成员有自己的声明和分号,成员可以是任意 C 类型——甚至另一个结构;末尾大括号后的分号不能漏,它结束整个模板定义。模板只是图纸,不分配内存:声明放在函数外,其后所有函数都能用;放在函数内,只对该函数可见。
定义结构变量与初始化
struct book library; 这行声明让编译器按 book 图纸分配空间:两个字符数组加一个 float,统一挂在 library 名下:

声明与定义可以合二为一(此时可省略标记);但模板要复用就该留标记。初始化语法和数组一样,用大括号包住逗号分隔的初始值,按成员顺序一一对应:
struct book library = {
"The Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};注意:具有静态存储期(static storage duration)的结构,初始化值必须是常量表达式。
访问成员:点运算符
用结构成员运算符 .(点号):library.value 就像普通 float 一样用,library.title 就像普通 char 数组一样用。scanf("%f", &library.value) 能成立,是因为 . 优先级高于 &,表达式相当于 &(library.value)。
C99 还给结构引入了指定初始化器(designated initializer),用点号加成员名,顺序随意:
struct book surprise = { .value = 10.99 };
struct book gift = { .value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad" };14.2 结构数组
一本书一个结构变量显然不够,把结构装进数组即可——清单 14.2 manybook.c:
/* manybook.c -- 多本书的清单(book 模板与 14.1 相同,此处省略) */
#include <stdio.h>
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 100 /* 藏书上限 */
int main(void)
{
struct book library[MAXBKS]; /* book 结构的数组 */
int count = 0;
int index;
printf("Please enter the book title.\n");
printf("Press [enter] at the start of a line to stop.\n");
while (count < MAXBKS && gets(library[count].title) != NULL && library[count].title[0] != '\0')
{
printf("Now enter the author.\n");
gets(library[count].author);
printf("Now enter the value.\n");
scanf("%f", &library[count++].value);
while (getchar() != '\n') continue; /* 清空输入行 */
if (count < MAXBKS)
printf("Enter the next title.\n");
}
if (count > 0)
{
printf("Here is the list of your books:\n");
for (index = 0; index < count; index++)
printf("%s by %s: $%.2f\n", library[index].title,
library[index].author, library[index].value);
}
else
printf("No books? Too bad.\n");
return 0;
}library 是数组名,library[2] 才是一个结构变量,所以下标要挂在 library 身上:library[2].value 对,library.value[2] 错。下标还能叠加——library[2].title[4] 是第三本书书名里的第 5 个字符。规律:点号左边的下标作用于结构数组,右边的下标作用于成员内部。
![/图:结构数组 library[MAXBKS] 的内存布局](../images/cpr/1e96a3e96dd90629d1d1ded6564ce3f5af5eaee825ec6639d243ec8519f43326.jpg)
程序里 while (getchar() != '\n') continue; 是为了清掉 scanf() 留在输入队列里的换行符——否则下一次 gets() 会读到残留的 \n,误判为空行而提前终止。
100 个结构可能撑爆栈
这个数组是自动存储类对象,通常放在栈(stack)上。100 本 × 80 多字节要占好几千字节,某些编译器默认栈很小,会报 stack overflow。解决办法:声明成 static 或外部(不占栈)、调大栈选项,或缩小数组。
14.3 嵌套结构
结构成员本身可以是另一个结构。比如”朋友”结构里嵌一个”姓名”结构:
struct names { // 第一个结构
char first[LEN];
char last[LEN];
};
struct guy { // 第二个结构
struct names handle; // 嵌套结构
char favfood[LEN];
char job[LEN];
float income;
};
struct guy fellow = {
{ "Ewen", "Villard" }, // 嵌套结构的初始化
"grilled salmon",
"personality coach",
58112.00
};初始化时嵌套部分用内层大括号;访问时连用两个点号:fellow.handle.first 即 (fellow.handle).first——先找 fellow,再找其中的 handle,再找 handle 里的 first。
14.4 指向结构的指针
用指针操作结构至少有三大好处:指针比整个结构好挪动(比如排序)、老式编译器只支持传结构指针、链式数据结构全靠结构里的指针互相勾连。
/* friends.c -- 使用指向结构的指针(names、guy 模板与 14.3 相同) */
#define LEN 20
int main(void)
{
struct guy fellow[2] = {
{ { "Ewen", "Villard" }, "grilled salmon",
"personality coach", 58112.00 },
{ { "Rodney", "Swillbelly" }, "tripe",
"tabloid editor", 232400.00 }
};
struct guy * him; /* 指向结构的指针 */
printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);
him = &fellow[0]; /* 告诉指针指向哪里 */
printf("pointer #1: %p #2: %p\n", him, him + 1);
printf("him->income is $%.2f: (*him).income is $%.2f\n", him->income, (*him).income);
him++; /* 指向下一个结构 */
printf("him->favfood is %s: him->handle.last is %s\n", him->favfood, him->handle.last);
return 0;
}要点:
- 声明
struct guy * him;与其他指针声明如出一辙。结构名不是结构的地址(这点与数组不同),取地址必须用&。 him + 1地址增加了 84(十六进制 54)——一个 guy 结构占 84 字节。有时结构会大于各成员之和,因为系统按对齐要求插入”空洞”。- 用指针访问成员有两种写法:间接成员运算符
->(him->income),或解引用加点((*him).income,括号不能省,因为.优先级高于*)。三者等价:barney.income == (*him).income == him->income(假设him == &barney)。
14.5 把结构告诉函数
三种传法
以”算两个账户总额”为例,ANSI C 有三种传法:
1. 传成员——只传函数需要的成员(清单 14.5 funds1.c 的思路):sum(stan.bankfund, stan.savefund),sum() 根本不知道也不关心参数来自结构。
2. 传结构地址(清单 14.6 funds2.c):原型 double sum(const struct funds * money);,函数体为 return (money->bankfund + money->savefund);,调用 sum(&stan)。函数直接操作原结构,加 const 防止误改。
3. 传结构本身(清单 14.7 funds3.c):形参换成 struct funds moolah,调用 sum(stan)。调用时自动创建副本结构、成员逐个拷贝,函数操作副本,用点号访问(moolah.bankfund)。
两种风格对比(清单 14.8 / 14.9)
同一任务”统计姓名字母总数”,指针版(names1.c)全程只维护一个 person 结构,三个函数都拿 &person 干活;传值版(names2.c)里每个函数各造一份副本,靠返回值传回:person = getinfo();、person = makeinfo(person);(在副本上计算,再 return info; 传回)、showinfo(person);。
传结构是"整包复制",小结构才划算
按值传结构要把所有成员整体拷贝一遍,结构一大,时间空间双重浪费——尤其函数只用其中一两个成员时。惯例是:默认传指针(配 const 保护数据),小型结构才按值传。传值的好处是操作副本更安全,代码也更自然。
成员用字符数组还是字符指针?
对比 struct names { char first[LEN]; char last[LEN]; };(数组,安全)与 struct pnames { char * first; char * last; };(指针,有陷阱):
初始化 struct pnames treas = {"Brad", "Fallingjaw"} 没问题——字符串常量本身有存储处,结构里只存两个地址。但 scanf("%s", attorney.last) 就危险了:attorney.last 是未初始化的指针,指向哪里天知道,程序可能崩溃,更糟的是偶尔”碰巧能跑”——那意味着埋了一颗雷。想让结构自己存字符串,就用数组。
结构、指针与 malloc()
指针成员的正确打开方式:先 malloc() 分配空间再存地址。清单 14.10 names3.c 的 getinfo() 先读进临时数组再按需分配:
void getinfo (struct namect * pst)
{
char temp[81];
printf("Please enter your first name.\n");
gets(temp);
pst->fname = (char *) malloc(strlen(temp) + 1); // 分配内存
strcpy(pst->fname, temp); // 拷贝过去
printf("Please enter your last name.\n");
gets(temp);
pst->lname = (char *) malloc(strlen(temp) + 1);
strcpy(pst->lname, temp);
}字符串本体活在 malloc() 的内存块里,结构只存地址;程序末尾配 cleanup() 函数 free 掉两块内存,malloc/free 收支平衡。
C99 新玩意:复合字面量与柔性数组
复合字面量(compound literal)能凭空造一个结构当实参:rect_area((struct rect) {10.5, 20.0}),也能取其地址传给要指针的函数。
柔性数组成员(flexible array member)让结构的最后一个成员是”可伸缩”的数组:
struct flex {
int count;
double average;
double scores[]; // 柔性数组:先不占空间
};规则:必须是最后一个成员、至少还有一个其他成员、方括号为空。使用时不声明结构变量,而是声明指针并 malloc 出”结构 + 数组”的整块空间:
pf = malloc(sizeof(struct flex) + 5 * sizeof(double));
pf->count = 5;
pf->scores[2] = 18.5; // 像普通数组一样用传结构数组
数组名即首元素地址,直接传即可(清单 14.13 funds4.c):sum(jones, N),形参 const struct funds money[],函数内用 money[i].bankfund 访问第 i 个结构,const 保护原数据。
14.6 把结构内容保存到文件
结构是天然的数据库构件:一个结构是一条记录(record),一个成员是一个字段(field)。用 fprintf() 逐字段存既啰嗦又难解析,正确姿势是 fread()/fwrite() 整块搬运——清单 14.14 booksave.c:
/* booksave.c -- 把结构内容保存到文件(book 模板与 14.1 相同,此处省略) */
#include <stdio.h>
#include <stdlib.h>
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 10 /* 藏书上限 */
int main(void)
{
struct book library[MAXBKS]; /* 结构数组 */
int count = 0;
int index, filecount;
FILE * pbooks;
int size = sizeof (struct book);
if ((pbooks = fopen("book.dat", "a+b")) == NULL)
{
fputs("Can't open book.dat file\n", stderr);
exit(1);
}
rewind(pbooks); /* 定位到文件开头 */
while (count < MAXBKS && fread(&library[count], size, 1, pbooks) == 1)
{
if (count == 0)
puts("Current contents of book.dat:");
printf("%s by %s: $%.2f\n", library[count].title,
library[count].author, library[count].value);
count++;
}
filecount = count;
if (count == MAXBKS)
{
fputs("The book.dat file is full.", stderr);
exit(2);
}
puts("Please add new book titles.");
puts("Press [enter] at the start of a line to stop.");
while (count < MAXBKS && gets(library[count].title) != NULL && library[count].title[0] != '\0')
{
puts("Now enter the author.");
gets(library[count].author);
puts("Now enter the value.");
scanf("%f", &library[count++].value);
while (getchar() != '\n') continue; /* 清空输入行 */
if (count < MAXBKS)
puts("Enter the next title.");
}
if (count > 0)
{
puts("Here is the list of your books:");
for (index = 0; index < count; index++)
printf("%s by %s: $%.2f\n", library[index].title,
library[index].author, library[index].value);
fwrite(&library[filecount], size, count - filecount, pbooks);
}
else
puts("No books? Too bad.\n");
puts("Bye.\n");
fclose(pbooks);
return 0;
}程序脉络:
- “a+b” 模式:能读全文件 + 追加写 + 二进制格式。文本部分用编辑器看得懂,value 的二进制部分看不懂——这正是要的效果。
- 第一次 while 循环:用 fread() 一块一块(每块一个结构)读进数组并显示旧记录,filecount 记住旧记录条数;第二次 while 循环接收新书记录,追加到数组末尾。
- 一次写完:
fwrite(&library[filecount], size, count - filecount, pbooks)把新增的”结构个数 = count - filecount”块一口气写进文件,不必逐条循环。
fread/fwrite 让"记录"真正落地
一条 fwrite 存下整个结构,一条 fread 整条取回——不必管结构里有多少字段、字符串多长。每条记录大小相同,随机访问也极简单:第 n 条记录的位置就是 n × sizeof(struct book)。这是最朴素也最实用的”数据库”。
代价是浪费:书名只有 10 个字符也得占 40 字节,未用空间一并存盘。变长记录要靠链式结构与动态分配,那是后续章节的话题。
14.7 结构之后:链式数据形式
结构最激动人心的用途是发明新数据形式:队列、二叉树、哈希表、图……它们的共同套路是——每个结构(节点)除了数据,还携带指向同类型结构的指针,把节点串成链。下图是一棵二叉树,10 层可存 1023 个词,若按规律排列,最多 9 步就能找到任意一个词;而数组最坏要扫全部 1023 个元素。

14.8 联合(union)
联合与结构语法几乎一样,区别只有一字:结构能同时存 int、double、char;联合同一时刻只能存其中之一,所有成员共享同一块内存(按最大成员分配):
union hold {
int digit;
double bigfl;
char letter;
};
union hold fit; // 8 字节(按最大的 double 分配)
fit.digit = 23; // 存 23,用 2 字节
fit.bigfl = 2.0; // 23 被清掉,存 2.0,用 8 字节
fit.letter = 'h'; // 2.0 被清掉,存 h,用 1 字节点号决定当前用哪个成员,记住里面装的是什么类型是你的责任。典型应用:结构里一个 status 成员标记车型是自有还是租赁,联合成员据此决定存车主信息还是租赁公司信息——空间不浪费。
联合里"躺着"的是上次写入的那个成员
写入 digit 后去读 bigfl,得到的不是”另一种解释”,而是无效数据。联合不是类型转换工具,正确用法永远是”用什么成员写入,就用什么成员读出”。
14.9 枚举(enum)
enum 用符号名代表整型常量,纯粹为了可读性——color = blue 比 color = 4 友好:
enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;- 默认从 0 开始编号:red=0, orange=1, …… 也可手动指定,未指定的接着顺延:
enum feline {cat, lynx = 10, puma, tiger};则 puma=11, tiger=12。 - 枚举常量本质是 int,可用在数组大小、switch 标签等任何整型常量的位置;但输入输出时要自己转换(用户输入 “orange” 字符串,程序查表换成枚举值)。清单 14.15 enum.c 的骨架:字符串数组 colors[] 的下标恰好用枚举值,strcmp 找到匹配后用 switch(color) 输出对应诗句——枚举把”下标”和”标签”优雅地串了起来。
顺带一提命名空间(namespace):结构/联合/枚举标记共享一个命名空间,与普通变量互不冲突,所以同一作用域里 struct rect {...}; 和 int rect; 合法——但容易困惑,不建议这么起名。
14.10 typedef 快览
typedef 给类型起别名(#define 是给预处理器的文本替换,typedef 由编译器处理且只作用于类型):
typedef unsigned char BYTE;
BYTE x, y[10], * z; // 等价于 unsigned char ...
typedef char * STRING; // STRING 即 char*
STRING name, sign; // 两个都是指针对比 #define STRING char *:那会让 STRING name, sign; 变成 char * name, sign;——只有 name 是指针,sign 只是 char。typedef 优势尽显。typedef 还常配合结构免写 struct 前缀(typedef struct {double x; double y;} rect;),也是 size_t、time_t 这类”平台自适应类型”的实现机制。注意:typedef 不创造新类型,只是贴标签。
14.11 函数与指针
函数指针
函数也有地址——机器码加载进内存的起始位置。声明函数指针的诀窍:先写出该类型的函数原型,把函数名换成 (*pf)。对比 void (*pf)(char *);(指向函数的指针)与 void *pf(char *);(返回指针的函数,完全不同!)。赋值时函数名即地址(不要加括号):pf = ToUpper; 合法,pf = round; 非法(类型不匹配),pf = ToLower(); 非法(那是调用)。调用有两种等价语法:(*pf)(mis); 和 pf(mis);——ANSI C 都接受。
函数指针最常见的去处是当参数,让通用函数按需选择具体操作。C 库的 qsort() 就是这样实现对任意类型数组排序的。清单 14.16 func_ptr.c 用菜单选择大小写变换,核心结构:
void (*pfun)(char *); // 声明函数指针
switch (choice) // 菜单选择决定指针指向
{
case 'u': pfun = ToUpper; break;
case 'l': pfun = ToLower; break;
case 't': pfun = Transpose; break;
case 'o': pfun = Dummy; break;
}
show(pfun, copy); // 把选中的函数传给 show()
void show(void (* fp)(char *), char * str)
{
(*fp)(str); // 用传入的函数处理 str
puts(str); // 显示结果
}还可以把函数指针排成数组(不能有”函数数组”,但可以有”函数指针数组”):V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dummy};,菜单序号直接当下标,switch 都省了。
花式声明速读
读复杂声明的两条规则:[] 和 () 优先级高于 *;它们彼此左结合。于是 int * risks[10]; 是 10 个指针组成的数组,int (* rusks)[10]; 是指向”10 个 int 的数组”的指针,char (* flump[3])(); 是 3 个函数指针组成的数组。
一套心智模型贯穿全章
结构 = 打包数据;结构数组 = 批量记录;结构套结构 = 层次化信息;结构指针 = 高效传参与链式结构;fread/fwrite = 记录落盘;union/enum/typedef = 换着花样描述数据。组合起来就能从零搭出队列、树、哈希表——第 17 章将正式动手。
关键概念与小结
- 结构把多个(通常不同类型的)数据项装进一个对象;标记声明模板,
.和->访问成员。 - 结构名不是地址;向函数传结构可选传指针(高效,配 const)、传值(安全,开销大)、传成员。
- 现代还允许结构赋值、结构做返回值;C99 补充了指定初始化器、复合字面量与柔性数组。
- 联合成员共享内存,一次只活一个;枚举造符号整型常量;typedef 给类型起别名。
- 函数名即函数地址,函数指针让”把函数当参数传”成为现实。
自测题(点击展开)
1. 下面的模板声明错在哪里?
structure { char itable; int num[20]; char * togs }两处错误:关键字应为struct而非 structure;成员声明char * togs后缺分号,且大括号后必须有分号结束整个定义。2.
struct house fruzt = {1560.0, 6, 1, "22 Spiffo Road"}; struct house *sign = &fruzt;之后,fruzt.rooms、sign->stories各是多少? fruzt.rooms 是 6,sign->stories 是 1。注意 sign 必须先&fruzt取地址——结构名本身不是地址。3. 如何初始化一个结构,只给 value 成员赋 10.99? 用 C99 指定初始化器
struct book surprise = { .value = 10.99 };,其余成员自动置零,成员顺序可任意排列。4. 向函数传递结构时,传指针和传结构本身各有什么优缺点? 传指针只拷贝一个地址,快且省空间,能直接修改原结构(加 const 可禁止);传结构要整包复制,大结构时开销明显,但操作副本更安全。惯例:默认传指针 + const,小结构才按值传。
5.
struct guy * him; him = &fellow[0];之后,him->income、(*him).income、fellow[0].income三者的关系? 三者完全等价,都是 fellow[0] 的 income 成员。-> 是指针专用的间接成员运算符;(*him) 把指针解引用回结构,因.优先级高于*,括号不可省。