Menu

C 语言结构体 struct:定义、初始化与使用

如何用 C 语言的 struct 把相关的值组合成一个类型:声明结构体,用点运算符访问成员,指定初始化器,结构体数组,嵌套,以及让所有人都意外的传参复制规则。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

数组存放一种类型的多个值。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};。指定的写法给每个成员命名,所以即使以后有人调整了结构体成员的顺序也不受影响,没有命名的成员会被置零。

Coddy 编程语言插图

和 Coddy 一起学编程

开始