一个包含柔性数组成员的结构体是一种可以声明变量并用 sizeof
进行计算大小的类型,这导致以下程序出现了异常行为。
文件 fam1.c
:
#include <stdio.h>
#include <stddef.h>
struct s {
char c;
char t[]; };
extern struct s x;
size_t s_of_x(void);
int main(void) {
printf("size of x: %zu\n", sizeof x);
printf("size of x: %zu\n", s_of_x());
}
文件 fam2.c
:
#include <stddef.h>
struct s {
char c;
char t[2]; };
struct s x;
size_t s_of_x(void) {
return sizeof x;
}
当编译并运行该程序时,它会输出一些令人惊讶的结果:
$ clang -std=c17 -pedantic -Wall fam1.c fam2.c
$ ./a.out
size of x: 1
size of x: 3
请注意,您还可以将“
extern
”移至fam2.c中,这样如果访问x.t
则程序的行为会更加出乎意料。需要明确的是,我不知道这种变体是否符合C17标准的定义,但我很确定大多数编译器生成的目标文件在链接在一起时会产生一个失效的二进制文件。我不确定C17标准的意图是否使由fam1.c和fam2.c组成的程序不被定义,但我没有看到哪些条款会使其如此。人们可能会想到C17的6.2.7:1和6.2.7:2条款,但是如果您仔细阅读它们,它们似乎恰好允许fam1.c和fam2.c正在做的事情:
“兼容类型”和“复合类型”的规定在C17第6.2.7条中描述。 6.2.7:1两种类型具有兼容类型,如果它们的类型相同。用于确定两种类型是否兼容的其他规则在6.7.2中描述了类型说明符,在6.7.3中描述了类型限定符, 在6.7.6中描述了声明符。此外,如果在单独的翻译单位中声明了两个结构体、联合体或枚举类型,则它们的标签和成员满足以下要求时兼容: 如果其中一个使用标签声明,则另一个必须使用相同的标签声明。如果两个都在各自的翻译单位中的任何位置完成, 则还需要满足以下附加要求:应存在一对一的对应关系,这些对应关系是它们的成员,每个对应成员对都用兼容类型声明; 如果一对成员用对齐说明符声明,则另一对成员用等效的对齐说明符声明;如果一对成员用名称声明,则另一对成员使用 相同的名称声明。对于两个结构体,对应的成员应按相同的顺序声明。对于两个结构体或联合体,对应的位域应具有相同的宽度。 对于两个枚举类型,对应的成员应具有相同的值。
6.2.7:2所有引用同一对象或函数的声明必须具有兼容类型;否则,行为未定义。
参考文献,fam1.c和fam2.c中的伸缩性数组成员在6.7.2.1:18中描述:
6.7.2.1:18作为特例,具有多个命名成员的结构的最后一个元素可以具有不完全数组类型;这称为柔性数组成员。在大多数情况下,柔性数组成员将被忽略。特别地,在结构的大小方面,柔性数组成员的大小就像省略了该成员一样,除非它可能具有更多的填充尾随。然而,当左操作数(或->)是(指向)具有柔性数组成员的结构的指针,并且右操作数命名该成员时,它的行为就好像该成员被替换为最长的数组(具有相同的元素类型),使得该数组不会比所访问的对象更大;即使这将不同于替换数组的偏移量,该数组的偏移量也应保持柔性数组成员的偏移量。如果该数组没有元素,则其行为就像它有一个元素,但如果试图访问该元素或生成一个超过该元素的指针,则行为是未定义的。
我是否遗漏了一些内容,包括C17中的6.2.7还是其他地方,使得fam1.c + fam2.c是未定义的?或者根据C17标准,它是一个定义良好的C程序,那么,如果
extern
位于非FAM版本的结构体上并且在同一编译单元中访问x.t
是否定义良好,是出于同样的原因?(这是一个离题,但我认为我可以解释为什么6.2.7:1写成这样。目的可能是允许在一个编译单元中使用
struct s { int (*m)[]; } x;
,而在另一个编译单元中使用struct s { int (*m)[2]; } x;
)
sizeof
违反了规则。 - Agent_L