该书提到它在通信中使用,其中需要设置相同大小的标志;在一个谷歌网站上,它可以消除奇数大小的内存块...但在现代的非嵌入式内存空间中有用吗?
你能否在CPU寄存器中使用它进行某些巧妙的操作?它是否只是早期编程时代的遗物?或者它是否像臭名昭著的goto一样,仍然具有一些强大的用途(可能在紧凑的内存空间中),使其值得保留?
嗯,你几乎回答了自己的问题:内存。
早些时候,内存非常有限,即使能节省几个k字节也是很有用的。
但即使是今天,仍有一些场景可以使用联合体。例如,如果你想实现某种variant
数据类型,最好的方法就是使用联合体。
这听起来不像什么大不了的事,但假设你想使用一个变量,它可以存储4个字符的字符串(如ID)或4字节的数字(可能是某个哈希值或实际上只是一个数字)。
如果使用经典的struct
,这将占用8个字节的空间(至少,如果不够幸运,还会有填充字节)。使用union
只需4个字节。因此,你可以节省50%的内存,单个实例看起来并不多,但想象一下拥有百万个这样的实例。
虽然可以通过转换或子类化来实现类似的功能,但联合体仍然是最简单的方法。
联合的一个用途是让两个变量占用同一空间,结构体中的第二个变量可以决定您要将其视为哪种数据类型。
例如,您可以拥有一个布尔值“isDouble”和一个联合“doubleOrLong”,其中既有双精度浮点数又有长整型。如果isDouble == true,则将联合解释为double,否则将其解释为long。
联合的另一个用途是访问以不同表示形式表示的数据类型。例如,如果您知道如何在内存中布置双精度浮点数,则可以将双精度浮点数放入联合中,将其作为不同的数据类型(如长整型)访问,直接访问其位、尾数、符号、指数等,并对其进行直接操作。
现在由于内存价格便宜,您实际上并不需要这个功能,但在嵌入式系统中它仍然有其用处。
Windows API 在很多情况下都使用联合体。LARGE_INTEGER 就是这样一种用法的例子。基本上,如果编译器支持 64 位整数,就使用 QuadPart
成员;否则,手动设置低 DWORD 和高 DWORD。
这并不是一个过时的做法,因为C语言是在1972年创建的,当时内存是一个真正的问题。
你可以说,在现代的非嵌入式空间中,你可能不想使用C作为编程语言。如果你选择C作为实现的语言,那么你要利用C的优点:它高效、接近底层,从而产生紧凑、快速的二进制文件。
因此,当选择使用C时,仍然希望利用它的优点,包括内存空间效率。在这方面,联合非常有效;允许你具有一定程度的类型安全性,同时强制执行可用的最小内存占用。
我看到它被用在了《毁灭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;
这段代码获取浮点数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
理论上,在SSE2机器上,可以通过单个寄存器访问此内容;但实际上,我不确定是否有任何编译器会这样做。在我看来,这仍然比早期Quake版本中的转换代码更加简洁。