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

StackOverflow https://stackoverflow.com/questions/1812348

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

现在请查看此代码段:

#include<stdio.h>

int main(void)
{
  union a
  {
     int i;
     char ch[2];
  };

  union a u;
  u.ch[0] = 3;
  u.ch[1] = 2;

  printf("%d %d %d\n", u.ch[0], u.ch[1], u.i);

  return 0;
}

输出:

3 2 515

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

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

感谢。

有帮助吗?

解决方案

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

实施例

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

  1. 因为我的机器具有32位小端架构,使用 -O0 ,我最终将两个最低有效字节初始化为2和3,两个最高有效字节未初始化。所以联盟的内存看起来像这样: {3,2,garbage,garbage}

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

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

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

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

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

其他提示

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

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

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

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

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

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

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

输出背后的原因是在您的机器上整数存储在 little-endian format:首先存储最不重要的字节。因此字节序列 [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

2 3

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

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

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

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

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top