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

Nik*_*lai 51 c c++ arrays struct

例如,请考虑以下结构:

struct S {
  int a[4];
  int b[4];
} s;
Run Code Online (Sandbox Code Playgroud)

写作s.a[6]并期望它等于是合法的s.b[2]吗?就个人而言,我觉得它必须是C++中的UB,而我不确定C.但是,我没有找到任何与C和C++语言相关的内容.


更新

有几个答案建议确保字段之间没有填充以使代码可靠地工作的方法.我想强调的是,如果这样的代码是UB,那么填充缺失是不够的.如果它是UB,那么编译器可以自由地假设访问S.a[i]S.b[j]不重叠,并且编译器可以自由地重新排序这样的存储器访问.例如,

    int x = s.b[2];
    s.a[6] = 2;
    return x;
Run Code Online (Sandbox Code Playgroud)

可以转化为

    s.a[6] = 2;
    int x = s.b[2];
    return x;
Run Code Online (Sandbox Code Playgroud)

总是回来2.

msc*_*msc 60

写sa [6]并期望它等于sb [2]是否合法?

.因为在C和C++中访问一个超出范围的数组调用了未定义的行为.

C11 J.2未定义的行为

  • 将指针加到或减去数组对象和整数类型会产生一个指向数组对象之外的结果,并用作*被计算的一元运算符的操作数(6.5.6).

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

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

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

  • @Lundin:当我们关心规范性文本时,我们有一个[语言 - 律师]标签.附件J足以进行非律师活动. (15认同)
  • 附件J也与引用无关,因为它是提供信息的,它不是规范性文本.引用ISO标准不仅仅是在pdf中进行一些文本搜索并复制/粘贴随机文本.这里的相关部分是6.5.6 - 附件J指出了.附件J仅仅是对标准中其他地方所有形式不明确的行为的方便总结. (7认同)
  • @Nikolai:这是**扩展**:编译器可以提供比标准更多的保证. (6认同)
  • @Nikolai灵活的数组成员必须是结构的最后一个成员,因此你不能拥有允许访问`b`元素的灵活`a`. (6认同)
  • 但是它与"灵活的阵列成员"有何关系,这是关于定义明确的越界访问? (3认同)

ali*_*oar 33

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

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+
Run Code Online (Sandbox Code Playgroud)

第二个填充可能会丢失,并且对齐的对齐struct object方式与对齐方式a相同,b但C语言也不会强制第二个填充不在那里.

  • @Nikolai标准不禁止填充存在,即使对齐是相同的. (19认同)
  • 在@ Nikolai的例子中,*可能*没有填充.但是如果`a`是`char a [3]`,编译器可能会也可能不会决定在`b`之前放置一个填充字节. (3认同)
  • @Panzercrisis:ISO C++将联合类型惩罚留作UB,因此您的问题仅与C99/C11或GNU C++或其他方言/实现相关,其中定义了一个成员然后读取另一个成员.但是确定,在'union {int a [10],b [5]; ``明确定义`b []`的元素与`a []`的早期元素对齐.工会与成员重叠,因此填充不会进入. (2认同)

Ste*_*ner 11

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

6.5.6加法运算符

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

相同的参数适用于C++(虽然这里没有引用).

此外,虽然这明显是不确定的行为,由于超出数组边界的事实a,请注意,编译器可能会成员之间引入填充ab,这样-即使被允许这样的指针算术- a+6不一定会产生相同的地址b+2.


dwi*_*iss 6

这合法吗?不会.正如其他人提到的,它会调用Undefined Behavior.

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

在许多C和C++编译器中,结构将被布置为使得b将紧跟在内存中并且将不存在边界检查.因此,访问[6]实际上与b [2]相同,不会导致任何异常.

特定

struct S {
  int a[4];
  int b[4];
} s
Run Code Online (Sandbox Code Playgroud)

并且假设没有额外的填充,该结构实际​​上只是一种查看包含8个整数的内存块的方法.你可以把它投射到(int*)((int*)s)[6]指向相同的内存s.b[2].

你应该依靠这种行为吗?绝对不. 未定义意味着编译器不必支持此功能.编译器可以自由填充结构,这可能会假设&(sb [2])==&(sa [6])不正确.编译器还可以添加对数组访问的边界检查(尽管启用编译器优化可能会禁用此类检查).

我过去曾经历过这种影响.拥有这样的结构是很常见的

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

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

  • 它甚至不一定会访问某些内容,允许编译器假定未发生未定义的行为,因此如果它可以证明表达式会产生未定义的行为,则允许编译器假定该代码永远不会到达并相应地进行优化. (4认同)

Jed*_*aaf 5

正如@MartinJames在评论中提到的,如果你需要保证a并且b在连续的内存中(或者至少能够被视为这样,(编辑),除非你的架构/编译器使用一个不寻常的内存块大小/偏移和强制对齐,需要添加填充),你需要使用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];
    };
};
Run Code Online (Sandbox Code Playgroud)

你不能直接访问ba(例如a[6],像你这样的要求),但你可以同时访问的内容ab使用all(例如,all[6]指的是相同的存储单元b[2]).

(编辑:您可以取代8,并4在上面的代码2*sizeof(int),并sizeof(int)分别,更可能匹配架构的调整,特别是如果代码需要更便携,但你必须要小心,以避免对如何做任何假设许多字节都在a,ball.但是,这将适用于最常见的(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] = '\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;
}
Run Code Online (Sandbox Code Playgroud)