38

我正在从 K&R 阅读 C 中的 union,据我了解,union 中的单个变量可以保存多种类型中的任何一种,如果某些内容作为一种类型存储并作为另一种类型提取,则结果纯粹是实现定义的。

现在请检查此代码段:

#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.chu.i

我知道对于大多数其他人来说,这可能看起来很初学者,但我无法弄清楚该输出背后的原因。

谢谢。

4

6 回答 6

30

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

例子

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

  1. 因为我的机器有 32 位 little-endian 架构,-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优化调用时,它选择了零作为可能未初始化的联合的随机值。

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

于 2009-11-28T12:00:11.000 回答
19

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

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

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

请注意,尝试这样做仍然会导致未定义的行为。如果您读取的值恰好对于您读取的类型无效(所谓的“陷阱表示”),那么行为仍然是未定义的。否则,您读取的值是实现定义的。

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

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

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

于 2009-11-28T16:24:17.423 回答
9

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

这个结果取决于具体的实现和平台。

于 2009-11-28T12:05:26.573 回答
5

它依赖于实现,结果可能在不同的平台/编译器上有所不同,但似乎这就是正在发生的事情:

二进制的 515 是

1000000011

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

0000001000000011

这两个字节是:

00000010 and 00000011

哪个是23

希望有人解释为什么它们被反转 - 我的猜测是字符没有反转,但 int 是小端。

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

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

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

于 2009-11-28T12:00:46.397 回答
5

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

如果您在大端系统上执行此操作,您将(可能)获得 770(假设 16 位整数)或 50462720(假设 32 位整数)。

于 2009-11-28T12:04:27.967 回答
3

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

假设您在一个具有 16 位整数的系统上,那么您所做的仍然是实现定义的。如果您的系统是 little endian,则 u.ch[0] 将对应于 ui 的最低有效字节,而 u.ch 1将是最高有效字节。在大端系统上,情况正好相反。此外,C 标准不强制实现使用二进制补码来表示有符号整数值,尽管二进制补码是最常见的。显然,整数的大小也是实现定义的。

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

于 2009-11-28T12:25:56.117 回答