在一个结构中,使用一个数组字段访问另一个数组是合法的吗?

例如,考虑以下结构:

struct S { int a[4]; int b[4]; } s; 

sa[6] ,期望它等于sb[2]是合法的吗? 就我个人而言,我觉得它必须是C ++的UB,而我不确定C的含义。然而,我没有发现任何与C和C ++语言标准相关的东西。


更新

有几个答案build议如何确保字段之间没有填充,以使代码可靠地工作。 我想强调的是,如果这样的代码是UB,那么填充的缺乏是不够的。 如果是UB,那么编译器可以自由地假定对Sa[i]Sb[j]不重叠,并且编译器可以自由地对这样的存储器访问进行重新sorting。 例如,

  int x = sb[2]; sa[6] = 2; return x; 

可以转化为

  sa[6] = 2; int x = sb[2]; return x; 

总是返回2

写一个sa [6],期望它等于sb [2]是合法的吗?

,因为访问数组越界会在C和C ++中调用未定义的行为

C11 J.2未定义的行为

  • 将指针join或减去数组对象和整数types会产生一个结果,该结果仅指向数组对象之外,并用作评估(6.5.6)的一元*运算符的操作数。

  • 一个数组下标超出范围,即使一个对象明显可以用给定的下标访问(如左值expression式a[1][7]给定了声明int a[4][5]) (6.5.6)。

C ++标准草案第5.7节添加操作符第5节说:

当具有整数types的expression式被添加到指针或从指针中减去时,结果具有指针操作数的types。 如果指针操作数指向数组对象的一个​​元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果数组元素和原始数组元素的下标之差等于整数expression式。 […] 如果指针操作数和结果指向同一个数组对象的元素,或者一个超过了数组对象的最后一个元素,则评估不会产生溢出; 否则,行为是不确定的。

除了@rsp的回答( Undefined behavior for an array subscript that is out of range ),我可以补充说,通过a来访问b是不合法的,因为C语言没有指定在结束之间可以有多less填充空间分配给a的区域和b的开始,所以即使你可以在特定的实现上运行它,它也是不可移植的。

 instance of struct: +-----------+----------------+-----------+---------------+ | array a | maybe padding | array b | maybe padding | +-----------+----------------+-----------+---------------+ 

第二个填充可能会丢失以及struct object的alignment方式是a的alignment方式与b的alignment方式相同,但是C语言也不会强制第二个填充不在那里。

ab是两个不同的数组, a被定义为包含4元素。 因此, a[6]访问数组越界,因此是不确定的行为。 请注意,数组下标a[6]定义为*(a+6) ,所以UB的certificate实际上是由“加法运算符”和“指针”一起给出的。参见C11标准的下一节在线草稿版本)描述这方面:

6.5.6添加操作符

当具有整数types的expression式被添加到指针或从指针中减去时,结果具有指针操作数的types。 如果指针操作数指向数组对象的一个​​元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果数组元素和原始数组元素的下标之差等于整数expression式。 换句话说,如果expression式P指向数组对象的第i个元素,则expression式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)到数组对象的第i + n个元素和第n个元素,只要它们存在。 此外,如果expression式P指向数组对象的最后一个元素,则expression式(P)+1将指向数组对象的最后一个元素,如果expression式Q指向数组对象的最后一个元素之后,expression式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果指向相同数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为是不确定的 。 如果结果指向一个超过数组对象的最后一个元素,则不应将其用作所评估的一元运算符的操作数。

相同的论点适用于C ++(尽pipe这里没有引用)。

另外,虽然由于超出了a数组范围这一事实显然是未定义的行为,但是请注意,编译器可能会在成员ab之间引入填充,即使这样的指针算术被允许, a+6也不一定会产生与b+2相同的地址。

这是合法吗? 正如其他人所提到的,它会调用未定义的行为

它会起作用吗? 这取决于你的编译器。 这是关于未定义的行为的事情:它是未定义的

在许多C和C ++编译器中,结构将被布置为使得b将立即跟随内存并且将不存在边界检查。 所以访问一个[6]实际上和b [2]是一样的,不会引起任何exception。

特定

 struct S { int a[4]; int b[4]; } s 

假设没有额外的填充 ,结构实际上只是一个查看包含8个整数的内存块的方式。 你可以将它转换为(int*)((int*)s)[6]指向与sb[2]相同的内存。

你应该依靠这种行为吗? 绝对不。 未定义意味着编译器不必支持这一点。 编译器可以自由地填充可能导致&(sb [2])==&(sa [6])不正确的假设。 编译器还可以在数组访问上添加边界检查(虽然启用编译器优化可能会禁用这种检查)。

过去我已经体验过这种影响。 像这样的结构是相当普遍的

 struct Bob { char name[16]; char whatever[64]; } bob; strcpy(bob.name, "some name longer than 16 characters"); 

现在bob.whatever将是“超过16个字符”。 (这就是为什么你应该总是使用strncpy,BTW)

正如@MartinJames在评论中提到的那样,如果你需要保证ab在连续的内存中(或者至less可以这样处理,(编辑)),除非你的架构/编译器使用一个不寻常的内存块大小/偏移量并强制需要添加填充的alignment),则需要使用union

 union overlap { char all[8]; /* all the bytes in sequence */ struct { /* (anonymous struct so its members can be accessed directly) */ char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */ char b[4]; }; }; 

你不能直接从a (例如a[6] )访问b ,但是你可以通过使用all 访问ab的元素(例如, all[6]指向与b[2] )。

(编辑:你可以分别用2*sizeof(int)sizeof(int)replace上面代码中的84 ,以便更有可能匹配架构的alignment方式,特别是如果代码需要更加便携,您必须小心避免对aballa字节数做任何假设,但是这可能是最常见的(1,2和4字节)内存alignment方式。 )

这是一个简单的例子:

 #include <stdio.h> union overlap { char all[2*sizeof(int)]; /* all the bytes in sequence */ struct { /* anonymous struct so its members can be accessed directly */ char a[sizeof(int)]; /* low word */ char b[sizeof(int)]; /* high word */ }; }; int main() { union overlap testing; testing.a[0] = 'a'; testing.a[1] = 'b'; testing.a[2] = 'c'; testing.a[3] = '\0'; /* null terminator */ testing.b[0] = 'e'; testing.b[1] = 'f'; testing.b[2] = 'g'; testing.b[3] = '\0'; /* null terminator */ printf("a=%s\n",testing.a); /* output: a=abc */ printf("b=%s\n",testing.b); /* output: b=efg */ printf("all=%s\n",testing.all); /* output: all=abc */ testing.a[3] = 'd'; /* makes printf keep reading past the end of a */ printf("a=%s\n",testing.a); /* output: a=abcdefg */ printf("b=%s\n",testing.b); /* output: b=efg */ printf("all=%s\n",testing.all); /* output: all=abcdefg */ return 0; } 

,因为在C和C ++中访问数组越界会调用未定义的行为

杰德·沙夫的回答是正确的,但不是很正确。 如果编译器在ab之间插入填充,他的解决scheme将仍然失败。 但是,如果您声明:

 typedef struct { int a[4]; int b[4]; } s_t; typedef union { char bytes[sizeof(s_t)]; s_t s; } u_t; 

无论编译器如何布局结构,你现在都可以访问(int*)(bytes + offsetof(s_t, b))来获取sb的地址。 offsetof()macros在<stddef.h>声明。

expression式sizeof(s_t)是一个常量expression式,在C和C ++的数组声明中都是合法的。 它不会给一个可变长度的数组。 (之前我误解了C标准,我以为听起来不对)。

然而在现实世界中,结构中的两个连续的int数组将按照您期望的方式进行布局。 (你可以devise一个非常人为的反例,把a的界限设置为3或5而不是4,然后让编译器在16字节的边界上对ab进行alignment。)而不是复杂的方法来尝试得到一个没有任何假设的程序,除了标准的严格措辞之外,你需要某种防御性编码,比如static assert(&both_arrays[4] == &s.b[0], ""); 。 这些不会增加运行时间的开销,如果你的编译器正在做一些会破坏你的程序的东西,只要不在触发器本身中触发UB就会失败。

如果您想要一种可移植的方式来保证这两个子数组都被打包到一个连续的内存区域中,或者以另一种方式分割一块内存,则可以使用memcpy()来复制它们。

简答: 不,你在不确定的行为之地。

长答案: 不。但这并不意味着你不能以其他粗略的方式访问数据…如果你使用GCC,你可以做如下的事情(dwillis的回答详述):

 struct __attribute__((packed,aligned(4))) Bad_Access { int arr1[3]; int arr2[3]; }; 

然后你可以通过( Godbolt source + asm )访问:

 int x = ((int*)ba_pointer)[4]; 

但是这个投影违反了严格的锯齿,所以只有用g++ -fno-strict-aliasing才是安全的。 您可以将一个结构指针转换为指向第一个成员的指针,但是您又回到了UB船上,因为您正在访问第一个成员之外。

或者,不要那样做。 保存一个未来的程序员(可能是你自己)那混乱的心痛。

另外,虽然我们在这,为什么不使用std :: vector? 这不是傻瓜,但在后端有防范这种不良行为的防范。

附录:

如果你真的关心性能:

假设你有两个相同types的指针,你正在访问。 编译器很可能会假设两个指针都有机会干涉,并且会实例化额外的逻辑来保护你免于做一些愚蠢的事情。

如果您向编译器郑重声明您不想别名,那么编译器将会给予您很大的回报: restrict关键字在gcc / g ++中提供了巨大的好处

结论:不要做坏事; 你将来的自我, 编译器会感谢你。

当一个程序试图在一个结构域中使用一个越界数组下标来访问另一个结构域的成员时,标准对于实现必须执行的操作没有任何限制。 因此, 在严格遵守程序的情况下 ,越界访问是“非法的” 使用这种访​​问的程序不能同时100%携带和无错误。 另一方面,许多实现确实定义了这种代码的行为,而仅仅针对这种实现的程序可能会利用这种行为。

这样的代码有三个问题:

  1. 虽然许多实现以可预测的方式布局结构,但是标准允许实现在除第一个以外的任何结构成员之前添加任意填充。 代码可以使用sizeofoffsetof来确保结构成员按预期方式放置,但是另外两个问题将保留。

  2. 给定类似于:

     if (structPtr->array1[x]) structPtr->array2[y]++; return structPtr->array1[x]; 

    对于编译器来说,假定在“if”条件下使用structPtr->array1[x]将产生与上述用法相同的值,即使它会改变依赖于别名的代码的行为在两个arrays之间。

  3. 如果array1[]具有例如4个元素,则编译器给出类似于:

     if (x < 4) foo(x); structPtr->array1[x]=1; 

可能得出这样的结论:由于没有定义x不小于4的情况,所以可以无条件地调用foo(x)

不幸的是,虽然程序可以使用sizeofoffsetof来确保结构布局没有任何意外,但是他们没有办法testing编译器是否承诺避免优化types#2或#3。 此外,该标准对于如下情况下的含义有点模糊:

 struct foo {char array1[4],array2[4]; }; int test(struct foo *p, int i, int x, int y, int z) { if (p->array2[x]) { ((char*)p)[x]++; ((char*)(p->array1))[y]++; p->array1[z]++; } return p->array2[x]; } 

该标准很清楚,只有当z的范围是0..3时,才会定义行为,但是由于expression式中的p->数组的types是char *(由于衰减),因此不能清除访问中的强制转换用y会有什么作用。 另一方面,由于将指针转换为char*的第一个元素应该产生与将结构指针转换为char*相同的结果,并且转换的结构指针应该可用于访问其中的所有字节,这似乎是使用x访问应该定义为(至less)x = 0..7 [如果array2的偏移量大于4,则会影响到碰到array2成员所需的x的值,但是x某个值可以这样做与定义的行为]。

恕我直言,一个很好的补救办法是以不涉及指针衰减的方式定义数组types的下标操作符。 在这种情况下,expression式p->array[x]&(p->array1[x])可以邀请编译器假定x是0..3,但是p->array+x*(p->array+x)将需要一个编译器来允许其他值的可能性。 我不知道是否有编译器这样做,但标准并不需要它。