数组存放一种类型的多个值。struct 恰好相反:它存放几个不同类型的值,并把它们当作一个整体。一个点由 x 和 y 组成。一名员工有名字、编号和薪水。没有结构体,你就只能把它们当作三个零散的变量带来带去,祈祷永远不会把谁的薪水搞混。
声明结构体类型
结构体声明会命名一个标签,并在花括号里列出成员:
struct Point {
int x;
int y;
};
右花括号后面的分号是必需的。忘了它,C 初学者会遇到最让人困惑的错误信息之一,因为编译器会把错误报在下一行。
这声明的是一个类型,而不是变量。在 C 中,这个类型的完整名称是 struct Point,包括关键字在内:
成员用点运算符读写:p.x。结构体变量就是普通变量:它存放在栈上,可以用 = 赋值给另一个同类型的结构体,并在作用域结束时消失。
到处写 struct Point 很累,所以几乎所有实际的 C 代码都会用 typedef 包装声明,让类型直接叫 Point。
初始化结构体
可以在声明结构体的同时给它赋值。按位置的写法按声明顺序列出各个值:
struct Point p = {3, 4}; // x = 3, y = 4
指定初始化器则给每个成员命名,更清晰也更安全:
有三点值得注意:
- 指定的写法可以按任意顺序列出成员,所以以后添加成员或调整顺序时,值不会悄悄地跑到错误的位置。
- 没有列出的成员都会被初始化为零,
{0}是把整个结构体清零的惯用写法。 char name[32]成员是结构体内部的一个真正的数组,所以字符串就存放在结构体本身里,而不是别处。这也意味着事后写a.name = "Ada";是非法的,你需要用strcpy(见字符串函数)。
没有初始化的结构体是未初始化的,在写入成员之前读取它们是未定义行为。= {0} 没有任何开销,却能消除这一整类 bug。
赋值与比较
结构体赋值会复制每个成员:
a 没有被改动:b 得到了自己的副本。但要注意 C 没有提供的东西:结构体没有 ==。if (a == b) 无法通过编译。比较结构体就是逐个比较成员:
if (a.x == b.x && a.y == b.y) { /* equal */ }
不要拿 memcmp 当捷径。编译器会为了对齐在成员之间插入看不见的填充字节,这些字节里可能是垃圾值,所以成员完全相同的两个结构体,逐字节比较时可能不相等。
结构体数组
结构体和其他类型一样是一种类型,所以可以创建结构体数组,这是存放一张记录表的标准方式。
staff[i].salary 的意思是“第 i 个元素,然后取它的 salary 成员”。下标运算的优先级比点运算高,所以不需要括号。
结构体传入函数时会被复制
这条规则常常让人中招。把结构体传给函数时,C 会复制它。函数操作的是它自己的私有副本:
p 仍然是 (3, 4)。像 distance_from_origin 那样按值读取结构体完全没问题,对小结构体来说往往是最清晰的选择。但如果函数必须修改调用者的结构体,或者结构体大到复制很浪费,就改为传递指针。这是结构体与指针一页的主题。
按值返回结构体的道理相同,而且完全合法:
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
嵌套结构体
结构体的成员本身也可以是结构体。把点连起来就能访问内部成员:
b.published.year 一次深入一层。嵌套是在 C 中构建真实数据模型的方式,而且内部结构体是存放在外部结构体里面的,而不是用指针指向,所以整个 Book 是一块连续的内存。
大小、填充与成员顺序
sizeof 告诉你结构体有多大,而结果往往比所有成员大小之和更大:
在典型的机器上,Wasteful 是 12 字节,Tidy 是 8 字节,尽管两者包含相同的三个成员。编译器会插入填充,让每个成员位于其类型偏好的地址上,int 通常要求地址是 4 的倍数。把较大的成员放在较小的成员前面,往往排列得更紧凑。
这一点很少需要在意(百万元素的数组、网络数据包的布局),也从不影响你自己代码的正确性。但它能解释为什么 sizeof 会出乎你的意料,以及为什么对结构体用 memcmp 不是个好主意。
常见问题
C 语言中如何声明结构体?
写 struct,后面跟一个标签名和用花括号括起来的成员列表:struct Point { int x; int y; };。这声明的是一个类型,而不是变量。要创建变量,还得写 struct Point p;。在 C 中 struct 这个词是类型名的一部分,所以大多数代码会搭配 typedef 使用。
结构体的 . 和 -> 有什么区别?
拿到的是结构体本身时用 .(p.x),拿到的是指向它的指针时用 ->(ptr->x)。箭头是 (*ptr).x 的简写。详细内容见结构体与指针。
C 语言中结构体是按值传递还是按引用传递?
始终按值传递。把结构体传给函数会复制每个成员,所以函数内部的修改不会影响调用者的变量。要修改原来的结构体,或者避免复制大结构体,就改为传递指针。
C 语言中如何初始化结构体?
可以按位置初始化:struct Point p = {3, 4};,也可以用指定初始化器:struct Point p = {.y = 4, .x = 3};。指定的写法给每个成员命名,所以即使以后有人调整了结构体成员的顺序也不受影响,没有命名的成员会被置零。