6.7.3.2 结构体和联合体说明符
IMPORTANT
这一节同时规定了普通成员、位字段、匿名结构体/联合体,以及柔性数组成员。网页端阅读时,最好把它们看成四套不同规则,而不是一套规则的附带细节。
语法
struct/union说明符有两种基本形式:- 带成员声明列表的定义形式;
- 只有关键字加标记的引用形式。
成员声明可以是:
- 普通成员声明;
- 位字段声明;
static_assert声明。
约束
除非该成员声明声明的是匿名结构体或匿名联合体,否则成员声明必须带成员声明符列表。
结构体或联合体成员不能是不完整类型,也不能是函数类型。
例外是:拥有超过一个具名成员的结构体,其最后一个成员可以是不完整数组类型,这就是柔性数组成员。
含有这种结构体成员的结构体,或者递归地含有这种成员的联合体,不能再作为另一个结构体成员,也不能成为数组元素。
位字段宽度表达式必须是非负整数常量表达式,并且宽度不能超过省略
: width后该类型对象本来可拥有的位宽。若位字段宽度为
0,则该声明不能带声明符。位字段类型必须是以下之一:
boolsigned intunsigned int- 位精确整数类型
- 其他由实现定义的类型
位字段是否允许原子类型,由实现定义。
语义总则
结构体由按顺序分配存储的一串成员组成。
联合体由共享同一片存储的一串成员组成。
写在
struct/union说明符上的属性,附着到该结构体或联合体类型本身。写在成员声明前面的属性,附着到该成员声明里声明出的每个成员。
写在说明符-限定符列表里的属性,附着到该成员声明或类型名里形成的类型。
成员与位字段
成员列表如果完全没有具名成员,无论是直接没有,还是通过匿名结构体/联合体递归展开后仍然没有,行为都未定义。
结构体或联合体成员可以是任意完整对象类型,但不能是变长修改类型。
位字段的值域由声明类型和指定位宽共同决定。
对非零宽度的
bool位字段,若存入0或1,之后比较时必须得到相同布尔值。实现可以自由选择位字段所用的可寻址存储单元,也可以决定:
- 位字段在单元内是从高位到低位分配还是反过来;
- 放不下时跨单元还是转入下一单元;
- 存储单元的对齐。
无名位字段常用于补齐布局。
宽度为
0的无名位字段表示:后续位字段不得继续塞进前一个位字段所在的那个存储单元。
匿名结构体与匿名联合体
类型说明符是无标记结构体说明符的无名成员,称为匿名结构体。
类型说明符是无标记联合体说明符的无名成员,称为匿名联合体。
匿名结构体/联合体中的成员,会被视为包含它的结构体或联合体的成员。
这种规则会递归传播。
例如:
struct v {
union {
struct { int i, j; };
struct { long k, l; } w;
};
int m;
} v1;2
3
4
5
6
7
这里 v1.i 有效,但 v1.k 无效,因为 k 在具名成员 w 中。
布局与地址
结构体中,非位字段成员和装位字段的存储单元,其地址按声明顺序递增。
指向结构体对象的指针,适当转换后,会指向它的首成员;反过来也成立。
结构体内部可以有无名填充,但开头不能有。
联合体大小足以容纳最大成员。
指向联合体对象的指针,适当转换后,可以视为指向其任一成员。
联合体成员彼此重叠;把它们都转换成字符类型指针时,会指向同一字节起点。
结构体和联合体尾部都可以有无名填充。
柔性数组成员
柔性数组成员只能出现在“至少有两个具名成员的结构体”的最后一个位置。
在大多数场景中,柔性数组成员会被忽略;例如
sizeof(struct s)时,效果好像它不存在,只是尾部可能多出额外填充。但当你对带柔性数组成员的结构体对象使用
.或->访问该成员时,语义会近似为:- 把它看成“尽可能长、但又不超过当前对象实际大小”的数组。
若按这个规则推出来的数组长度为
0,标准仍把它当成“像是有 1 个元素”,但访问那个元素或生成越过它的指针都属于未定义行为。
典型用法
struct s { int n; double d[]; };
int m = 8;
struct s *p = malloc(sizeof(struct s) + sizeof(double[m]));2
3
若分配成功,那么 p->d 在多数语义下可看作长度为 m 的 double 数组。