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

AJM*_*eld 6 c struct pointers language-lawyer c11

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

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);
Run Code Online (Sandbox Code Playgroud)

第6.7.2.1段第14段似乎表明这应该是实施定义的:

结构或联合对象的每个非位字段成员以适合其类型的实现定义方式对齐.

然后继续说:

结构对象中可能存在未命名的填充,但不是在其开头.

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

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);
Run Code Online (Sandbox Code Playgroud)

标准似乎保证foo3.arr是相同的&foo3.a,并且没有任何意义,指的是一种方式是合法的而另一种方式不是,但同样没有意义的是,添加外部联合与数组应该突然(&foo3.a)[1]法律.

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

  1. foo3.arr 保证与...相同 &foo.a
  2. foo3.arr + 1&foo3.b指向相同的内存位置
  3. &foo3.a + 1并且&foo3.b因此必须指向相同的存储器位置(从图1和2)
  4. 结构布局要求是一致的,所以&foo1.a&foo1.b应当设置完全一样,&foo3.a&foo3.b
  5. &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;
Run Code Online (Sandbox Code Playgroud)

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

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

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

M.M*_*M.M 10

简介:这个领域的标准是不够的,关于这个主题的争论有几十年的历史,而且严格的别名没有令人信服的解决方案或建议.

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


首先:通常认为第一个代码示例中的代码是未定义的行为,因为通过直接指针算法访问数组的边界.

规则是C11 6.5.6/8.它说从指针索引必须保持在"数组对象"(或一个结束)之内.它没有说明哪个数组对象,但是通常同意在这种情况下int *p = &foo.a;"数组对象"是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]
Run Code Online (Sandbox Code Playgroud)

要么

((int *)((uintptr_t)&foo.a + sizeof(int)))[0]
Run Code Online (Sandbox Code Playgroud)

相关链接:,


不是一般约定是否((int *)&foo)[1]有效.有人说它与你的第一个代码基本相同,因为标准说"指向对象的指针,适当转换,指向元素的第一个对象".其他人说它与我(char *)上面的例子基本相同,因为它遵循指针转换的规范.一些人甚至声称它是严格的别名违规,因为它将结构别名为数组.

也许相关的是N2090 - Pointer来源提案.这并不直接解决这个问题,也没有提议废除6.5.6/8.

  • 在C99之前,已被广泛认可用于各种目的的编译器将需要支持超出所有编译器所要求的标准的类型使用模式,并且对此类模式的支持被视为实现质量问题。在旨在或仅用于高端数字运算的编译器中适当的限制将使编译器无法用于处理低级内存管理代码。没有意愿去认识不同种类的实现,任何试图解决一套规则的尝试…… (3认同)
  • ...因为几乎可以保证一切都会破坏很多代码,而不必要地损害许多优化。标准的一部分*允许*编译器以任意方式处理其行为将在其他地方定义的动作,这并不意味着任何判断*旨在*用于特定目的的高质量编译器都应该这样做。 (3认同)