这个涉及到使用可变数组成员的两个结构体定义的C程序是否已经被定义?

8

一个包含柔性数组成员的结构体是一种可以声明变量并用 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;

3
能否认识到结构体有两个定义,每个编译单元中各一个。一个具有变长数组成员变量,另一个具有固定大小的数组……尝试将这两个结构体定义放入共享头文件中。'extern'是向编译器承诺,在fam2.o中会找到包含VLA数组的结构体…您正在打破这个承诺。 - Fe2O3
2
@Fe2O3 我可能需要重申一下,问题并不是普通 C 编译器能否在分离编译的情况下处理我所提到的情况 - 显然它不能,或者程序应该如何编写 - 没有理由在某些编译单元中为 FAMs 提供大小,它们不是用于这样的目的。问题是C标准是否存在疏漏,导致我的示例意外定义。 - Pascal Cuoq
2
如果带有FAM的结构体是一个完整类型,则使用两个冲突的定义违反了规则(因为只有不完整类型可以在其他地方完成)。如果带有FAM的结构体是不完整类型,则使用sizeof违反了规则。 - Agent_L
3
@user3386109 的意图可能是这样或那样。我不会过度思考任何事情,我写了一个程序并且想知道它是否符合C标准。这是一个简单的问题,但“你想太多了”似乎并不是一个答案。 - Pascal Cuoq
2
我必须承认。6.7.6.2:6中似乎存在一个漏洞 - 它依赖于6.7.6.2:4,但不完整性的要求并没有明确说明。而你已经用FAM规避了不完整性。如果我们回到FAM的史前时期,最后一个成员被声明为大小为零,显然这就是FAM定义的意图。 - Agent_L
显示剩余19条评论
1个回答

7
如问题所述,C 2018 6.78.2.1 18表示:
大多数情况下,忽略灵活数组成员...
我们可以认为除非另有说明或必要性要求,否则将忽略灵活数组成员。 (对于后者,我考虑对齐要求。标准明确指出具有灵活数组成员的结构可能具有比没有该成员时更多的尾随填充,但省略了它可能具有更高的对齐要求的事实。但显然,如果其元素的要求高于结构的其他成员,则灵活数组成员可能会施加更高的对齐要求。)
由于在确定兼容性的目的上未说明忽略灵活数组成员的任何例外情况,因此我们应该在确定兼容性的目的上忽略灵活数组成员(但不忽略其额外的填充和对齐要求)。 然后,应用6.7.2.1 1中的规则,我们发现问题中的两个struct s声明没有“成员之间的一对一对应关系”,因为一个在结尾处具有数组成员,而另一个在忽略灵活数组成员时则没有。
此外,我认为6.7.2.1 1中未提及潜在的附加填充(以及未提及附加对齐要求)是委员会未能充分考虑灵活数组成员对6.7.2.1 1中兼容性语句的影响的证据,因此6.7.2.1 1不完整。
上述试图从人类不完美的文字中取得意义的尝试留下了这样一种可能性:具有灵活数组成员的结构类型将被视为与具有相同对齐要求(因此具有相同尾随填充)但没有灵活数组成员的结构类型兼容。 这可能是一个无意的后果,但不会引起任何问题 - 只有在单独的翻译单位中声明时才认为这两种类型是兼容的,并且在赋值和其他操作中将表现相同,只是在一个翻译单位中可以访问灵活数组成员,在另一个翻译单位中不可以。

请注意C11(草案)的6.7.2.1p18:“如果该数组没有元素,则其行为就像它有一个元素,但如果尝试访问该元素或生成一个指向该元素之后的指针,则行为是未定义的。” 这对我来说似乎使得此处的使用明确地成为未定义的行为,但我会听从您的解释。 - Andrew Henle
1
如果他们采用更传统的语言,比如“除非另有说明,否则忽略”,那么所有内容都会读起来更好。 “大多数情况下被忽略”不是良好规范的语言。我会说,类型对齐并填充,就像大小有一些正整数值和大小声明为T x [1]减去sizeof(T)。我们知道这是意图,但我认为本主题中的共识是它并没有完全表达出来,或者最多也没有表达得很好。 - Persixty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接