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

In a structure, is it legal to use one array field to access another one?

本文关键字:一个 另一个 访问 是否 字段 结构 数组      更新时间:2023-10-16

例如,考虑以下结构:

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

s.a[6]并期望它等于s.b[2]是否合法?就我个人而言,我觉得它一定是C++中的UB,而我不确定C。然而,我在C和C++语言的标准中找不到任何相关的东西。


更新

有几个答案提出了确保没有填充的方法字段之间,以使代码可靠地工作。我想强调一下如果这样的代码是UB,那么缺少填充是不够的。如果是UB,则编译器可以自由地假设对CCD_ 3和CCD_重叠,并且编译器可以自由地对这种存储器访问进行重新排序。例如,

int x = s.b[2];
s.a[6] = 2;
return x;

可以转换为

s.a[6] = 2;
int x = s.b[2];
return x;

其总是返回CCD_ 5。

写s.a[6]并期望它等于s.b[2]是否合法?

。因为访问超出绑定的数组调用了C和C++中的未定义行为

C11 J.2未定义的行为

  • 将指针添加到数组对象或刚好超出数组对象和整数类型,会产生指向刚好超出数组的结果数组对象,用作一元*运算符的操作数评估(6.5.6)。

  • 数组下标超出范围,即使对象显然可以使用给定的下标访问(如在左值表达式中a[1][7]给出声明inta[4][5])(6.5.6)。

C++标准草案第5.7节加法运算符第5段说:

当将具有整型的表达式添加到或减去时从指针中,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向从原始元素,使得所得数组元素和原始数组元素等于积分表达式。[…]如果指针操作数和结果都指向元素相同数组对象的,或数组最后一个元素之后的一个对象,评估不应产生溢出;否则行为未定义

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

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

第二填充可能与struct object的对齐一样是a的对齐,这与b的对齐相同,但是C语言也没有强制第二填充不存在。

ab是两个不同的数组,a被定义为包含4元素。因此,a[6]访问数组超出了界限,因此是未定义的行为。请注意,数组下标a[6]被定义为*(a+6),因此UB的证明实际上是由"加法运算符"一节和"指针"一节给出的。请参阅C11标准(例如本在线草案版本)中描述这一方面的以下部分:

6.5.6加法运算符

将具有整数类型的表达式添加到或减去时从指针中,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向从原始元素,使得结果数组元素和原始数组元素等于整数表达式。换句话说,如果表达式P指向数组对象,表达式(P)+N(相当于N+(P))和(P)-N(其中N具有值N)分别指向第i+N个和数组对象的第i-n个元素,前提是它们存在。此外,如果表达式P指向数组对象的最后一个元素表达式(P)+1指向数组对象的最后一个元素之后的一个,并且如果表达式Q指向数组的最后一个元素之后的一个对象,表达式(Q)-1指向数组的最后一个元素对象如果指针操作数和结果都指向元素相同数组对象的,或数组最后一个元素之后的一个对象,评估不应产生溢出;否则行为未定义。如果结果指向最后一个元素之后的一个对于数组对象,它不应用作一元的操作数*运算符。

相同的参数适用于C++(尽管此处未引用)。

此外,尽管由于超出了a的数组边界,这显然是未定义的行为,但请注意,编译器可能会在成员ab之间引入填充,因此即使允许这种指针算法,a+6也不一定会产生与b+2相同的地址。

合法吗?不。正如其他人提到的,它调用未定义的行为

它行得通吗?这取决于编译器。这就是未定义的行为:它是未定义的

在许多C和C++编译器上,结构的布局会使b在内存中紧跟在a之后,并且不会进行边界检查。因此,访问a[6]将有效地与b[2]相同,并且不会引起任何类型的异常。

给定

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

假设没有额外的填充,该结构实际上只是查看包含8个整数的内存块的一种方式。您可以将其强制转换为(int*),并且((int*)s)[6]将指向与S.a[i]0相同的内存。

你应该依赖这种行为吗?绝对不是未定义意味着编译器不必支持此功能。编译器可以自由地填充结构,该结构可以呈现&(s.b[2])==&(s.a[6])不正确。编译器还可以在数组访问上添加边界检查(尽管启用编译器优化可能会禁用这种检查)。

我过去也经历过这种影响。像这样的结构是很常见的

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

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

正如@MartinJames在评论中提到的那样,如果您需要保证ab在连续内存中(或者至少可以这样处理,(编辑)除非您的体系结构/编译器使用了不寻常的内存块大小/偏移量和强制对齐,需要添加填充),那么您需要使用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访问b(例如,a[6],如您所要求的),但您可以使用all访问ab的元素(例如S.b[j]0指与b[2]相同的内存位置)。

(编辑:您可以将上面代码中的84分别替换为2*sizeof(int)sizeof(int),以更有可能匹配体系结构的对齐方式,特别是如果代码需要更具可移植性,但您必须小心,避免对aball中的字节数做出任何假设。然而,这将适用于最常见的情况(1、2和4字节)内存对齐。)

这里有一个简单的例子:

#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] = ''; /* null terminator */
testing.b[0] = 'e';
testing.b[1] = 'f';
testing.b[2] = 'g';
testing.b[3] = ''; /* null terminator */
printf("a=%sn",testing.a); /* output: a=abc */
printf("b=%sn",testing.b); /* output: b=efg */
printf("all=%sn",testing.all); /* output: all=abc */
testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
printf("a=%sn",testing.a); /* output: a=abcdefg */
printf("b=%sn",testing.b); /* output: b=efg */
printf("all=%sn",testing.all); /* output: all=abcdefg */
return 0;
}

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

简短回答:不你在一片行为不明确的土地上。

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

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

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

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

但该强制转换违反了严格的混叠,因此只有使用g++ -fno-strict-aliasing才是安全的。您可以将结构指针强制转换为指向第一个成员的指针,但随后您又回到了UB船上,因为您正在访问第一个成员之外的内容。

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

此外,当我们在做这件事的时候,为什么不使用std::vector呢?它不是傻瓜式的,但在后端,它有保护措施来防止这种不良行为。

附录:

如果你真的关心性能:

假设您正在访问两个类型相同的指针。编译器很可能会假设两个指针都有机会干扰,并会实例化额外的逻辑来保护您不做愚蠢的事情。

如果你郑重地向编译器保证你没有试图别名,编译器会给你丰厚的奖励:restrict关键字是否在gcc/g++中提供了显著的好处

结论:不要作恶;和编译器将感谢您。

Jed Schaff的答案是正确的,但并不完全正确。如果编译器在20和b之间插入填充,他的解决方案仍然会失败。然而,如果您声明:

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))来获得s.b的地址,无论编译器如何布局结构。offsetof()宏在<stddef.h>中声明。

表达式sizeof(s_t)是一个常量表达式,在C和C++中的数组声明中都是合法的。它不会给出可变长度的数组。(为之前误读C标准道歉。我认为这听起来不对。)

然而,在现实世界中,一个结构中int的两个连续阵列将按照您期望的方式进行布局。(您可以通过将a的边界设置为3或5而不是4,然后让编译器将ab在16字节的边界上对齐,来设计一个非常做作的反例,例如CCD_ 61。如果编译器正在做一些会破坏程序的事情,只要你不在断言本身中触发UB,这些操作就不会增加运行时开销,并且会失败。

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

当程序试图在一个结构字段中使用越界数组下标来访问另一个结构域的成员时,标准没有对实现必须执行的操作施加任何限制。因此,在严格符合程序中,越界访问是"非法的">,并且使用这种访问的程序不能同时100%可移植且没有错误。另一方面,许多实现确实定义了这样的代码的行为,并且仅针对这样的实现的程序可以利用这样的行为。

这种代码有三个问题:

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

  2. 给定以下内容:

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

    对于编译器来说,假设structPtr->array1[x]的使用将产生与前面在"if"条件中使用的值相同的值通常是有用的,即使它会改变依赖于两个数组之间的混叠的代码行为。

  3. 如果array1[]有例如4个元素,则编译器会给出类似以下内容:

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

可以得出结论,由于没有定义x不小于4的情况,因此它可以无条件地调用foo(x)

不幸的是,虽然程序可以使用sizeofoffsetof来确保结构布局不会有任何意外,但它们无法测试编译器是否承诺避免对类型#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范围内时才会定义行为,但由于该表达式中的p->数组类型为char*(由于衰减),因此不清楚使用y的访问中的强制转换是否会产生任何影响。另一方面,由于将指向结构的第一元素的指针转换为char*应该产生与将结构指针转换为char*相同的结果,并且转换后的结构指针应该可用于访问其中的所有字节,使用x的访问似乎应该被定义为(至少)x=0..7[如果array2的偏移量大于4,它将影响命中array2的成员所需的x的值,但是x的一些值可以通过定义的行为来实现]。

IMHO,一个很好的补救方法是以不涉及指针衰减的方式在数组类型上定义下标运算符。在这种情况下,表达式p->array[x]&(p->array1[x])可以邀请编译器假设x是0..3,但p->array+x*(p->array+x)将需要编译器考虑其他值的可能性。我不知道是否有编译器能做到这一点,但标准并不需要

相关文章: