关于C – union中的union作为一种类型并且作为另一种类型读取的问题 – 是否已实现定义?

我正在阅读K&R中关于C的联合,据我所知,联合中的单个变量可以包含几种类型中的任何一种,如果某些东西存储为一种类型并且提取为另一种类型,则结果纯粹是实现定义的。

现在请检查以下代码段:

#include int main(void) { union a { int i; char ch[2]; }; union au; u.ch[0] = 3; u.ch[1] = 2; printf("%d %d %d\n", u.ch[0], u.ch[1], ui); return 0; } 

输出:

 3 2 515 

在这里,我在u.ch分配值,但是从u.chui检索。 它是实现定义的吗? 或者我做的事情真的很傻?

我知道这对其他大多数人来说似乎很初学,但我无法弄清楚输出背后的原因。

谢谢。

这是未定义的行为。 uiu.ch位于相同的内存地址。 因此,写入一个并从另一个读取的结果取决于编译器,平台,体系结构,有时甚至是编译器的优化级别。 因此, ui的输出可能不总是515

例如,我的机器上的gcc-O0-O2产生两个不同的答案。

  1. 因为我的机器具有32位小端架构,因此-O0最终将两个最低有效字节初始化为2和3,两个最重要的字节未初始化。 所以联盟的记忆如下: {3, 2, garbage, garbage}

    因此我得到的输出类似于3 2 -1216937469

  2. 使用-O2 ,我得到3 2 515的输出,这使得联合内存{3, 2, 0, 0} 。 会发生什么是gcc使用实际值优化对printf的调用,因此程序集输出看起来像是等效于:

     #include  int main() { printf("%d %d %d\n", 3, 2, 515); return 0; } 

    值515可以如在该问题的其他答案中解释的那样获得。 实质上,这意味着当gcc优化了调用时,它选择了零作为未初始化联合的随机值。

写入一个联盟成员并从另一个联盟成员阅读通常没有多大意义,但有时它可能对使用严格别名编译的程序有用 。

这个问题的答案取决于历史背景,因为语言的规范随着时间而变化。 而这件事恰好是受变化影响的人。

你说你正在读K&R。 该书的最新版本(截至目前)描述了C语言的第一个标准化版本 – C89 / 90。 在那个版本的C语言中,写一个联合成员并读取另一个成员是未定义的行为 。 没有实现定义 (这是一个不同的东西),但未定义的行为。 在这种情况下,语言标准的相关部分是6.5 / 7。

现在,在C的演变的某个后期(应用技术勘误3的C99语言规范版本),使用联合进行类型惩罚突然变得合法,即写一个联盟成员然后读另一个成员。

请注意,尝试执行此操作仍可能导致未定义的行为。 如果您读取的值对于您通读的类型无效(所谓的“陷阱表示”),则行为仍未定义。 否则,您读取的值是实现定义的。

您的具体示例对于从intchar[2]数组的类型惩罚相对安全。 在C语言中,将任何对象的内容重新解释为char数组总是合法的(同样,6.5 / 7)。

然而,反之则不然。 将数据写入union的char[2]数组成员,然后将其作为int读取可能会创建陷阱表示并导致未定义的行为 。 即使您的char数组有足够的长度来覆盖整个int也存在潜在的危险。

但是在你的特定情况下,如果int恰好大于char[2] ,你读取的int将覆盖数组末尾之外的未初始化区域,这又会导致未定义的行为。

输出背后的原因是,在您的机器上,整数以小端格式存储:首先存储最不重要的字节。 因此,字节序列[3,2,0,0]表示整数3 + 2 * 256 = 515。

此结果取决于具体实现和平台。

此类代码的输出将取决于您的平台和C编译器实现。 您的输出让我觉得您在litte-endian系统(可能是x86)上运行此代码。 如果您将515放入i并在调试器中查看它,您会看到最低位的字节为3,而内存中的下一个字节为2,它与您在ch中放置的完全相同。

如果你在big-endian系统上这样做,你可能(可能)得到770(假设16位整数)或50462720(假设32位整数)。

它取决于实现,结果可能因不同的平台/编译器而异,但似乎正是这样:

二进制515是

 1000000011 

填充零使其成为两个字节(假设16位为int):

 0000001000000011 

这两个字节是:

 00000010 and 00000011 

这是23

希望有人解释为什么他们被逆转 – 我的猜测是,字符不会被颠倒,但int是小端。

分配给联合的内存量等于存储最大成员所需的内存量。 在这种情况下,你有一个长度为2的int和一个char数组。假设int是16位而char是8位,两者都需要相同的空间,因此union被分配了两个字节。

将三个(00000011)和两个(00000010)分配给char数组时,union的状态为0000001100000010 。 当您从此联合中读取int时,它会将整个事物转换为整数。 假设LSB存储在最低地址的little-endian表示,从union读取的int将是0000001000000011 ,这是515的二进制。

注意:即使int是32位也是如此 – 检查Amnon的答案

如果您使用的是32位系统,那么int是4个字节,但您只需初始化2个字节。 访问未初始化的数据是未定义的行为。

假设您使用的是16位整数的系统,那么您所做的仍然是实现定义的。 如果你的系统是小端,那么u.ch [0]将与ui的最低有效字节对应,而u.ch 1将是最重要的字节。 在大端系统上,它是另一种方式。 此外,C标准并不强制实现使用二进制补码来表示有符号整数值,尽管两个补码是最常见的。 显然,整数的大小也是实现定义的。

提示:如果使用hex值,则更容易看到发生了什么。 在小端系统上,hex的结果将是0x0203。