【问题标题】:What is the point behind unions in C?C中联合背后的意义是什么?
【发布时间】:2013-03-27 23:55:59
【问题描述】:

我正在阅读 O'Reilly 的 Practical C Programming 一书,并阅读了关于 C 编程语言的 K&R 书籍,但我真的很难理解联合背后的概念。

它们占用构成它们的最大数据类型的大小...最近分配的一个会覆盖其余的...但是为什么不根据需要使用/释放内存呢?

书中提到它用于通信,需要设置相同大小的标志;在一个谷歌网站上,它可以消除奇数大小的内存块......但它在现代非嵌入式内存空间中是否有用?

你可以用它和 CPU 寄存器做一些狡猾的事情吗?它只是早期编程时代的延续吗?还是像臭名昭著的 goto 一样,它仍然有一些强大的用途(可能在紧张的内存空间中)值得保留?

【问题讨论】:

标签: c unions


【解决方案1】:

嗯,你几乎回答了你的问题:内存。 过去内存相当低,即使节省几 KB 也很有用。

但即使在今天,在某些情况下,工会也会很有用。例如,如果您想实现某种variant 数据类型。最好的方法是使用联合。

这听起来并不多,但让我们假设您想使用一个变量来存储一个 4 字符串(如 ID)或 4 字节数字(可能是一些散列或实际上只是一个数字)。

如果您使用经典的struct,这将是 8 个字节长(至少,如果您不走运,还有填充字节)。使用 union 它只有 4 个字节。因此,您节省了 50% 的内存,这对于一个实例来说并不算多,但想象一下拥有一百万个这样的内存。

虽然您可以通过强制转换或子类化联合来实现类似的事情,但仍然是最简单的方法。

【讨论】:

    【解决方案2】:

    联合的一个用途是让两个变量占据相同的空间,结构中的第二个变量决定你想读取它的数据类型。

    例如你可以有一个布尔值'isDouble'和一个联合'doubleOrLong',它有一个double和一个long。如果 isDouble == true 将联合解释为双精度,否则将其解释为长整数。

    联合的另一个用途是访问不同表示形式的数据类型。例如,如果您知道双精度在内存中的布局方式,您可以将双精度放在联合中,将其作为不同的数据类型(如 long)访问,直接访问其位、尾数、符号、指数等,并对其进行一些直接操作。

    现在你真的不需要这个了,因为内存很便宜,但在嵌入式系统中它有它的用途。

    【讨论】:

    • 即使在具有大量千兆字节 RAM 的现代机器中,根据您的场景节省内存也不会受到影响,例如将某些内容保存到磁盘时,通过网络发送某些内容或仅根据您的数据集数量。
    • @Mario 同意。内存很便宜而且你不需要关心结构大小的论点对我来说总是很懒惰的设计。假设你想在内存中存储一​​亿条记录,这些记录有 32 个不同的布尔标志。您使用 32 个布尔值还是 32 位 XOR'd 整数。不考虑填充,相差大约 3GB。
    【解决方案3】:

    Windows API 大量使用联合。 LARGE_INTEGER 就是这种用法的一个例子。基本上,如果编译器支持 64 位整数,则使用 QuadPart 成员;否则,请手动设置低 DWORD 和高 DWORD。

    【讨论】:

      【解决方案4】:

      这并不是真正的拖延,因为 C 语言是在 1972 年创建的,当时内存是一个真正的问题。

      您可以提出这样的论点,即在现代非嵌入式空间中,您可能不想一开始就使用 C 作为编程语言。如果您选择 C ​​作为实现语言的选择,那么您正在寻求利用 C 的优势:它高效、接近金属,从而产生紧凑、快速的二进制文件。

      因此,在选择使用 C 时,您仍然希望利用它的优势,其中包括内存空间效率。对此,联盟运作良好;允许您具有一定程度的类型安全性,同时强制使用最小的内存占用空间。

      【讨论】:

      • 实际上,我之所以回顾 C,然后是 C++,然后是 C++ 中的模板,然后是 CLR,然后是 C++/CLI,是因为我试图创建 64 位(或更高)的索引升级对于 .Net 列表/字典类和关联的类/命名空间。我很想抓挠,所以我只想“修复”我认为这里有问题的地方。
      【解决方案5】:

      我在 Doom 3/idTech 4 Fast Inverse Square Root 实现中看到它使用的一个地方。

      对于不熟悉该算法的人,它本质上需要将浮点数视为整数。旧的 Quake(和更早)版本的代码通过以下方式执行此操作:

      float y = 2.0f;
      
      // treat the bits of y as an integer
      long i  = * ( long * ) &y;
      
      // do some stuff with i
      
      // treat the bits of i as a float
      y = * ( float * ) &i;
      

      original source on GitHub

      此代码获取浮点数y 的地址,将其转换为指向long 的指针(即,在地震期间为32 位整数),并将其解引用为i。然后它做了一些令人难以置信的奇怪的小玩意儿,反之亦然。

      这样做有两个缺点。一个是复杂的地址转换、强制转换、取消引用过程强制从内存中读取y 的值,而不是从寄存器1 中读取,返回时也是如此。然而,在 Quake 时代的计算机上,浮点寄存器和整数寄存器是完全分开的,因此您几乎必须将其推送到内存并返回以处理此限制。

      第二个是,至少在 C++ 中,进行这种强制转换是非常不受欢迎的,即使是在执行诸如此函数之类的相当于巫术的事情时也是如此。我敢肯定还有更令人信服的论点,但我不确定它们是什么:)

      因此,在 Doom 3 中,id 在他们的新实现中包含了以下位(它使用了不同的位旋转,但类似的想法):

      union _flint {
              dword                   i;
              float                   f;
      };
      
      ...
      union _flint seed;
      seed.i = /* look up some tables to get this */;
      double r = seed.f; // <- access the bits of seed.i as a floating point number
      

      original source on GitHub

      理论上,在 SSE2 机器上,这可以通过单个寄存器访问;我不确定在实践中是否有任何编译器会这样做。在我看来,它的代码仍然比早期 Quake 版本中的铸造游戏更简洁。


      1 - 忽略“足够高级的编译器”参数

      【讨论】:

        猜你喜欢
        • 2020-05-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-19
        • 1970-01-01
        相关资源
        最近更新 更多