7.9 持有二进制位

因为C是一种旨在“接近硬件”的语言,但很多人都发现一个令人沮丧的现象,那就是对于数字没有一种固有的二进制的表示方法。当然,有十进制和十六进制(还可以容忍,仅仅因为它们能较容易地在你的头脑中形成一组二进制位),但是八进制呢?哎呀!每当你阅读正在尝试对其进行编程的芯片的说明书时,这些说明书不会使用八进制甚至十六进制来描述芯片的寄存器—他们使用二进制。然而,C不让用0b0101101这样的表示方法,很明显,对于接近硬件的语言来说,这才是最好的解决方案。

虽然在C++中仍然没有固有的表示二进制的方法,由于两个类的增加:二进制位集合bitset和逻辑向量vector<bool>而使得情况有所好转,它们都被设计用来操纵一组开/关值。[1]这些类型之间主要的不同是:

·每个bitset持有一个固定位数的二进制位(bit)。用户在bitset的模板参数中建立二进制位的位数。像正常vector一样,vector<bool>可以动态地扩展为持有任意数目的bool值。

·bitset模板是为了在操纵二进制位时提高性能的目的而设计,因此并不是一个“正常的”STL容器。因此,它没有迭代器。作为一个模板参数,二进制位的位数目在编译时就已经知道了,并且允许将底层的整型数组存储在运行时的栈上。另一方面,vector<bool>容器是vector的一个特化,所以有一个普通vector的所有操作—该特化只是被设计用来提高bool数据的空间使用率。

在bitset和vector<bool>之间没有琐碎的转换,这意味着它们就是为了完全不同的目的而设计的。此外,它们都不是传统的“STL容器”。bitset模板类拥有一个面向二进制位层次的操作接口,绝不与到目前为止本教材中所讨论的STL容器类似。vector的特化vector<bool>类似于类-STL容器,但与将要在下面讨论的内容也是不同的。

7.9.1 bitset<n>

bitset作为模板接受一个无符号整型模板参数,该参数用来表示二进制位的位数。因此,bitset<10>与bitset<20>相比就是两种不同的类型,不能在它们两个之间进行比较、赋值等操作。

一个bitset以有效的形式提供了最一般的用于二进制位操作的方式。然而,每个bitset通过合理地将一组二进制位封装到一个整型数组中来实现(典型的如unsigned long,它至少包含32个二进制位)。另外,从一个bitset到一个数的惟一转化就是将其转化为一个unsigned long(通过函数to ulong())。

下面的例子测试了几乎所有bitset的功能(这里未介绍那些多余的或不重要的操作)。读者可以在每个打印输出的右边看到对bitset输出的描述,因此,可以将这些输出描述与它们原来的值进行比较。如果读者到现在为止还不了解二进制位操作方式的话,运行这个程序将会很有帮助。

7.9 持有二进制位 - 图1

7.9 持有二进制位 - 图2

7.9 持有二进制位 - 图3

为产生有趣的随机bitset,在程序中创建了函数randBitset()。该函数将每16个随机二进制位向左移动,直到bitset(其尺寸大小在函数中已经被模板化了)被填满为止,以此来演示operator<<=的使用。用operator|=将产生的数字和每组新的16位二进制数结合起来。

main()函数首先显示了一个bitset单元的大小。如果它小于32位,sizeof就产生4(4字节=32位,其最大实现是一个long型的大小。如果它在32到64之间,则需要两个long型数,大于64需要3个long型,等等。因此,为了最有效地利用空间,所使用的二进制位数量应在适宜个数的固有long型数表示的范围中。然而,要注意的是,对该对象不存在额外的开销——就像是在为一个long型数进行手工译码一样。

虽然除了to_ulong()之外没有其他的从bitset进行数字转换的函数,但是有一个流插入器stream inserter,它产生一个包含1和0的string,这个字符串可以和实际的bitset一样长。

虽然仍然没有用于表示二进制数的基本的格式,但是bitset支持最贴近的二进制表示形式:由1和0与在右边的最低有效位(least-significant bit, lsb)一起组成的一个string。3个构造函数演示创造一个完整的string、在第2个字符开始的string以及从第2个字符开始到第11个字符结束的string、可以使用operator<<从一个bitset写到一个输出流ostream,它以1和0的方式出现。也可以使用operator>>从一个输入流istream中读入到bitset(在这里没有显示)。

必须注意,bitset仅有3个非成员运算符:与(&)、或(|)和异或(^)。其中的每个都创建一个新的bitset作为其返回值。在没有创建暂时值的地方,全部选择更有效率的&=、|=等形式的成员运算符。然而,这些形式改变了bitset的值(这个值在上面例子的大多数检测中即a)。为避免发生这种情况,通过调用a的拷贝构造函数创建一个作为左值的临时对象;这就是为什么BS(a)的形式如此。每次测试的结果都显示出来,有时候a被重新打印出来从而更容易以它进行参照。

在程序运行的时候,例子的其余部分有自我解释;如果没有,读者可以在自己使用的编译器的文档中或者在本章较早提到的其他文档中查找有关细节。

7.9.2 vector<booI>

容器vector<bool>是vector模板的一个特化。一个标准的bool变量至少需要一个字节,但是因为一个bool型只有两个状态,所以理想的vector<bool>实现是这样的,每一个bool值仅需一个二进制位来表示。因为典型的库实现将一组二进制位封装进整型数组之内,所以迭代器必须特殊定义并且不能是一个指向bool型的指针。

用于vector<bool>的位操纵函数比bitset的那些函数受到更多的限制。在vector中已有的这些成员函数基础上添加的惟一成员函数就是flip(),用于使所有的位取反。它没有bitset中的set()或reset()。当使用operator[]时,就会送回一个vector<bool>:reference类型的对象,该对象也有一个用于对个别的位取反的成员函数flip()。

7.9 持有二进制位 - 图4

这个例子中的最后一部分创造了一个vector<bool>,通过先将它转换成一个仅包含0和1的string,再转换成为一个bitset。这里必须在编译时就知道bitset的大小。可以看出来,这个转换并不是基于常规的那种操作。

某些其他容器保证提供的功能不见了,vector<bool>特化给人的感觉是一种“有缺陷的”STL容器。比如,在其他的容器持有如下关系:

7.9 持有二进制位 - 图5

对于所有其他的容器,front()函数产生一个左值(某个对象能获得一个指向它的非常量引用),函数begin()必须产生某个对象的解析,并且得到其地址。因为二进制位是不可寻址的,所以上面的两个函数不可能用于处理持有二进制位的容器。vector<bool>和bitset两者都使用一个代理类(嵌套的reference引用类,之前提到过)在必要的时候读取和设置二进制位。

[1]Chuck设计并提供了最初的关于bitset或bitstring、以及vector<bool>最早的参考实现,当时,即20世纪90年代早期,他就是C++标准委员会中的一个活跃的成员。