C语言中的联合有什么作用?

7
我正在阅读O'Reilly的《实用C编程》一书,已经读过了C编程语言的K&R书籍,但我仍然很难理解union背后的概念。它们采用构成它们的最大数据类型的大小...并且最近分配的一个会覆盖其余部分...但为什么不只是根据需要使用/释放内存呢?
该书提到它在通信中使用,其中需要设置相同大小的标志;在一个谷歌网站上,它可以消除奇数大小的内存块...但在现代的非嵌入式内存空间中有用吗?
你能否在CPU寄存器中使用它进行某些巧妙的操作?它是否只是早期编程时代的遗物?或者它是否像臭名昭著的goto一样,仍然具有一些强大的用途(可能在紧凑的内存空间中),使其值得保留?

1
请查看 Type-Punning - Mysticial
在需要将结构体转换为其二进制值(例如char [])而不使用指针技巧的情况下,它可能非常有用。 - Richard J. Ross III
5个回答

5

嗯,你几乎回答了自己的问题:内存。

早些时候,内存非常有限,即使能节省几个k字节也是很有用的。

但即使是今天,仍有一些场景可以使用联合体。例如,如果你想实现某种variant数据类型,最好的方法就是使用联合体。

这听起来不像什么大不了的事,但假设你想使用一个变量,它可以存储4个字符的字符串(如ID)或4字节的数字(可能是某个哈希值或实际上只是一个数字)。

如果使用经典的struct,这将占用8个字节的空间(至少,如果不够幸运,还会有填充字节)。使用union只需4个字节。因此,你可以节省50%的内存,单个实例看起来并不多,但想象一下拥有百万个这样的实例。

虽然可以通过转换或子类化来实现类似的功能,但联合体仍然是最简单的方法。


1

联合的一个用途是让两个变量占用同一空间,结构体中的第二个变量可以决定您要将其视为哪种数据类型。

例如,您可以拥有一个布尔值“isDouble”和一个联合“doubleOrLong”,其中既有双精度浮点数又有长整型。如果isDouble == true,则将联合解释为double,否则将其解释为long。

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

现在由于内存价格便宜,您实际上并不需要这个功能,但在嵌入式系统中它仍然有其用处。


1
即使在具有数千兆字节RAM的现代计算机中,根据您的情况保存内存也不会有害,例如在将某些内容保存到磁盘、通过网络发送某些内容或仅基于数据集数量时。 - Mario
1
@Mario 同意。对我来说,认为内存便宜,不需要关心结构大小的论点总是让人感觉设计懒惰。假设你想在内存中存储一亿条记录,并且这些记录有32个不同的布尔标志。你会使用32个布尔值还是一个32位的异或整数?不考虑填充,这相差约3GB。 - Anthony

0

Windows API 在很多情况下都使用联合体。LARGE_INTEGER 就是这样一种用法的例子。基本上,如果编译器支持 64 位整数,就使用 QuadPart 成员;否则,手动设置低 DWORD 和高 DWORD。


0

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

你可以说,在现代的非嵌入式空间中,你可能不想使用C作为编程语言。如果你选择C作为实现的语言,那么你要利用C的优点:它高效、接近底层,从而产生紧凑、快速的二进制文件。

因此,当选择使用C时,仍然希望利用它的优点,包括内存空间效率。在这方面,联合非常有效;允许你具有一定程度的类型安全性,同时强制执行可用的最小内存占用。


实际上,我重新审查C,然后是C ++,然后是C ++中的模板,然后是CLR,然后是C ++ / CLI的原因是因为我正在尝试为.Net List / Dictionary类及其相关类/命名空间创建64位(或更高)索引升级。我有一个需要解决的问题,所以我只是要“修复”我认为有问题的地方。 - user978122

0

我看到它被用在了《毁灭3 / idTech 4》的快速反平方根算法实现中。

对于那些不熟悉此算法的人,它基本上要求将浮点数视为整数来处理。旧版的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;

GitHub上的原始来源

这段代码获取浮点数y的地址,将其转换为指向长整型(即Quake时代的32位整数)的指针,并将其解引用为i。然后它执行一些极其奇怪的位操作,再进行反向操作。

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

第二个缺点是,在C++中,即使像这个函数所做的那样进行巫术般的操作,进行这样的类型转换也是非常不被赞同的。我相信还有更有说服力的论据,但我不确定它们是什么 :)

在《毁灭战士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

GitHub上的原始来源

理论上,在SSE2机器上,可以通过单个寄存器访问此内容;但实际上,我不确定是否有任何编译器会这样做。在我看来,这仍然比早期Quake版本中的转换代码更加简洁。


1 - 忽略“足够先进的编译器”争议


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接