通过来自其他结构成员的偏移指针访问结构成员是否合法?

Is it legal to access struct members via offset pointers from other struct members?

在这两个示例中,通过偏移其他成员的指针来访问结构的成员是否会导致未定义/未指定/实现定义的行为?

struct {
  int a;
  int b;
} foo1 = {0, 0};

(&foo1.a)[1] = 1;
printf("%d", foo1.b);


struct {
  int arr[1];
  int b;
} foo2 = {{0}, 0};

foo2.arr[1] = 1;
printf("%d", foo2.b);

C11 § 6.7.2.1 第 14 段似乎表明这应该由实现定义:

Each non-bit-field member of a structure or union object is aligned in an implementation-defined manner appropriate to its type.

后来又说:

There may be unnamed padding within a structure object, but not at its beginning.

但是,像下面这样的代码似乎很常见:

union {
  int arr[2];
  struct {
    int a;
    int b;
  };
} foo3 = {{0, 0}};

foo3.arr[1] = 1;
printf("%d", foo3.b);

(&foo3.a)[1] = 2; // appears to be illegal despite foo3.arr == &foo3.a
printf("%d", foo3.b);

该标准似乎保证 foo3.arr&foo3.a 相同,并且以一种方式引用它是合法的而另一种方式不合法是没有意义的,但同样它不' 有意义的是,将外部联合与数组相加应该突然使 (&foo3.a)[1] 合法。

因此,我思考第一个例子的推理也必须是合法的:

  1. foo3.arr保证和&foo.a
  2. 一样
  3. foo3.arr + 1&foo3.b指向同一个内存位置
  4. &foo3.a + 1&foo3.b 因此必须指向相同的内存位置(从 1 和 2)
  5. struct布局需要保持一致,所以&foo1.a&foo1.b的布局应该和&foo3.a&foo3.b
  6. 完全一样
  7. &foo1.a + 1&foo1.b 因此必须指向相同的内存位置(从 3 和 4)

我发现一些外部消息来源表明 foo3.arr[1](&foo3.a)[1] 示例都是非法的,但是我无法在标准中找到具体的声明做到这一点。 即使它们都是非法的,也可以使用灵活的数组指针构造相同的场景,据我所知,确实具有标准定义的行为。

union {
  struct {
    int x;
    int arr[];
  };
  struct {
    int y;
    int a;
    int b;
  };
} foo4;

原始应用程序正在考虑从一个结构字段到另一个结构字段的缓冲区溢出是否严格按照标准定义:

struct {
  char buffer[8];
  char overflow[8];
} buf;
strcpy(buf.buffer, "Hello world!");
println(buf.overflow);

我希望这会在几乎任何现实世界的编译器上输出 "rld!",但是 这种行为是否由标准保证,或者它是未定义的还是实现定义的行为?

简介:这方面的标准是不充分的,关于这个话题和严格的别名有几十年的争论历史,没有令人信服的解决方案或建议来解决。

这个答案反映了我的观点,而不是标准的强加。


首先:人们普遍认为您的第一个代码示例中的代码是未定义行为,因为通过直接指针算法访问数组边界之外。

规则是C11 6.5.6/8。它说指针的索引必须保持在 "the array object" 内(或结束后的一个)。它没有说 哪个 数组对象,但普遍认为在 int *p = &foo.a; 的情况下 "the array object" 是 foo.a,而不是任何更大的对象其中 foo.a 是一个子对象。

相关链接: , .


其次:人们普遍认为您的两个 union 示例都是正确的。该标准明确表示可以读取联合体的任何成员;并且相关内存位置的任何内容都被解释为正在读取的联合成员的类型。


您建议 union 正确意味着第一个代码也应该正确,但事实并非如此。问题不在于指定读取的内存位置;问题在于我们如何得出指定该内存位置的表达式。

即使我们知道 &foo.a + 1&foo.b 是相同的内存地址,但是访问 int 是有效的,而访问 int 是无效的] 通过第一。

人们普遍同意,您可以通过不违反 6.5.6/8 规则的其他方式计算其地址来访问 int,例如:

((int *)((char *)&foo + offsetof(foo, b))[0]

((int *)((uintptr_t)&foo.a + sizeof(int)))[0]

相关链接:one, two


不是普遍认同((int *)&foo)[1]是否有效。有人说它与您的第一个代码基本相同,因为标准说 "a pointer to an object, suitably converted, points to the element's first object"。其他人说它与我上面的 (char *) 示例基本相同,因为它遵循指针转换规范。一些人甚至声称这是一个严格的别名违规行为,因为它将结构别名为数组。

可能相关的是 N2090 - Pointer provenance proposal。这并不直接解决问题,也不建议废除 6.5.6/8。

根据 C11 草案 N1570 6.5p7,尝试使用除字符类型左值、结构或联合类型或 包含的任何其他内容访问结构或联合对象的存储值 struct 或 union 类型,调用 UB,即使行为将由标准的其他部分完全描述。本节不包含允许使用非字符成员类型(或任何非字符数字类型)的左值来访问结构或联合的存储值的规定。

然而,根据已发布的基本原理文档,标准的作者认识到,在标准未施加要求的情况下,不同的实现提供了不同的行为保证,并认为这样 "popular extensions" 是一件好事和有用的事情.他们认为,市场应该比委员会更好地回答何时以及如何支持此类扩展的问题。虽然标准允许迟钝的编译器忽略 someStruct.array[i] 可能影响 someStruct 的存储值的可能性似乎很奇怪,但标准的作者认识到任何编译器的作者都不是故意的无论标准是否强制要求,obtuse 都将支持这样的构造,并且任何试图从 obtusely 设计的编译器强制执行任何类型的有用行为的尝试都是徒劳的。

因此,编译器对任何与结构或联合有关的东西 的支持水平本质上是一个实现质量问题。专注于与范围广泛的程序兼容的编译器编写者将支持范围广泛的结构。那些专注于最大化只需要那些语言将完全无用的结构的代码的性能,将支持更窄的集合。然而,该标准缺乏对此类问题的指导。

PS--配置为与 MSVC 样式 volatile 语义兼容的编译器会将限定符解释为指示对指针的访问可能具有与地址已被占用且不受 restrict 保护的对象,无论是否有任何其他理由预期这种可能性。在以 "unusual" 方式访问存储时使用这样的限定符可能会让人类读者更明显地看出代码同时在做某事 "weird" 因为它将确保与任何使用这种方式的编译器兼容语义,即使这样的编译器不会以其他方式识别该访问模式。不幸的是,一些编译器作者拒绝在除优化级别 0 之外的任何其他条件下支持此类语义,除非程序要求它使用非标准语法。