读取不是最近在 GCC 中编写的成员是未定义的行为吗?

Is reading a member that wasn't the most recently written in GCC undefined behavior?

本文关键字:未定义 成员 最近 GCC 读取      更新时间:2023-10-16

c++参考资料对联合有如下解释,这个问题有趣的部分用粗体表示:

联合的大小仅与容纳其最大数据成员所需的大小相同。其他数据成员分配的字节数与最大成员的一部分相同。分配的细节是由实现定义的,从最近没有写入的联合成员中读取是未定义的行为。作为非标准语言扩展,许多编译器实现了读取联合的非活动成员的能力。

现在,如果我在Linux Mint 18上用g++ -std=c++11编译以下代码,我得到以下输出(由printf语句旁边的注释给出):

#include <cstdio>
using namespace std;
union myUnion {
    int var1; // 32 bits
    long int var2; // 64 bits
    char var3; // 8 bits
}; // union size is 64 bits (size of largest member)
int main()
{
    myUnion a;
    a.var1 = 10;
    printf("a is %ld bits and has value %dn",sizeof(a)*8,a.var1); // ...has value 10
    a.var2 = 123456789;
    printf("a is %ld bits and has value %ldn",sizeof(a)*8,a.var2); // ...has value 123456789
    a.var3 = 'y';
    printf("a is %ld bits and has value %cn",sizeof(a)*8,a.var3); // ...has value y
    printf("a is %ld bits and has value %ldn",sizeof(a)*8,a.var2); //... has value 123456789, why???
    return 0;
}

return 0之前的行上,读取a.var2给出的不是'y'字符的ASCII小数(这是我所期望的,我对联合不熟悉),而是它最初定义的值。基于上面引用的cppreference.com,我是否应该理解这是未定义的行为,因为它不是标准的,而是GCC的特定实现?

编辑

正如下面伟大的答案所指出的那样,我在printf语句之后的评论中犯了一个复制错误,就在return 0语句之前。正确的版本是:

 printf("a is %ld bits and has value %ldn",sizeof(a)*8,a.var2); //... has value 123456889, why???

。7变成了8,因为前8位被'y'字符的ASCII值覆盖,即121(二进制的0111 1001)。我将保留上面代码中的内容,以便与由此产生的重要讨论保持一致。

关于未定义行为的有趣之处在于,它与"随机"行为非常不同。当处理未定义的行为时,编译器会有自己决定使用的行为,并且每次都倾向于显示相同的行为。

例如:IDEOne对这段代码有自己的解释:http://ideone.com/HO5id6
a is 32 bits and has value 10
a is 32 bits and has value 123456789
a is 32 bits and has value y
a is 32 bits and has value 123456889

你可能会注意到一些有趣的事情发生在那里(撇开事实,对于IDEOne的编译器,long int是32位而不是64位)。它仍然显示第4行读取与第2行读取类似,但是值实际上略有变化。似乎发生的情况是,'y'char值在联合中被设置,但它没有改变任何其他位。当我把它切换到long long int而不是long int时,我得到了类似的行为。

您可能想要检查,在您的示例中,第4行与之前的是否完全相同。我有点怀疑这是不是真的。

无论如何,为了回答您的特定问题,TL;DR是在GCC中,写入联合只更改与您要写入的特定成员相关的位,并且不能保证更改/清除所有其他位。当然,与任何与ub相关的东西一样,不要假设任何其他编译器(甚至是同一编译器的后续版本!)的行为相同。

您打印的只是同一内存区域的一部分:

myUnion a;
a.var2 = -1;
printf("a is %ld bits and has value %ld = 0x%lxn",
    sizeof(a)*8, a.var2, a.var2);
a.var3 = 'y';
printf("a is %ld bits and has value %c = 0x%xn",
    sizeof(a)*8, a.var3, a.var3);
printf("a is %ld bits and has value %ld = 0x%lxn",
    sizeof(a)*8, a.var2, a.var2);

示例输出

a is 64 bits and has value -1 = 0xffffffffffffffff
a is 64 bits and has value y = 0x79
a is 64 bits and has value -135 = 0xffffffffffffff79

为了清晰起见,我已经将您的123456789替换为最大值。这同样适用于你的号码:

a is 64 bits and has value 123456789 = 0x75bcd15
a is 64 bits and has value y = 0x79
a is 64 bits and has value 123456889 = 0x75bcd79

同样,原始值的第一个字节(特别是0x15)被0x79 (y字符)替换,因此原始数字被修改。

显然,a.var2被强制转换为整个内存区域的long int, a.var3 -被强制转换为char,即仅仅是联合内存的第一个字节。

可视化:

           long int (64)        = (long int) u
           ****************************************************************
           int (32) = a.var2    = (int) u
           ********************************
           char (1) = a.var1    = (char) u
           *
Byte no.:  0 ........................................................... 63
           ^
          ('y' = 0x79) (0xcd) (0x5b) (0x07)

文档中的行实际上意味着对联合成员的最后赋值指定了联合的值,其余的内存被认为是垃圾。尽管如此,我们通常可以观察到为联合分配的内存中的剩余部分。

你确定你写的是什么?

在ubuntu 64位的GCC 5.4.0中,我得到:

a is 64 bits and has value 10
a is 64 bits and has value 123456789
a is 64 bits and has value y
a is 64 bits and has value 123456889

var2是64位大小,通过改变var3的值,你修改了var2的最后一个字节。当您使用%x:

打印时,会更清楚。
a is 64 bits and var1 has value a
a is 64 bits and var2 has value 75bcd15
a is 64 bits and var3 has value 79
a is 64 bits and var2 has value 75bcd79

var1, var2和var3具有相同的内存方向,并且对于大多数计算机(Intel/Amd)来说,您的体系结构是Little Endian,修改var3会改变var2和var1的较低重要字节,因为它们共享相同的内存地址。

值得注意的是,C11标准§6.5.2.3,注释95(第83页)说:

如果用于读取联合对象内容的成员与最后用于在对象中存储值的成员不同,则该值的对象表示的适当部分将被重新解释为6.2.6中描述的新类型的对象表示(此过程有时称为"类型双关语")。这可能是一个陷阱。

这就是我所看到的,即使编译为c++ 11(与Apple LLVM版本8.0.0 (clang-800.0.38)):

a is 64 bits and has value 10
a is 64 bits and has value 123456789
a is 64 bits and has value y
a is 64 bits and has value 123456889
注意最后一个值不是 123456789,而是123456889,因为最低有效位字节被 覆盖了
a.var3 = 'y';

0x15替换为0x79 (== 'y')