顾名思义,按位运算符允许按照位来操作整型变量。可以把按位运算符应用于任意signed和unsigned整型,包括char类型。但是,它们通常应用于不带符号的整型。
这些运算符的一个常见应用是在整型变量中使用单个的位存储信息。例如标记,它用于描述二进制状态指示符。可以使用一个位来描述有两个状态的值:开或关、男或女,真或假。
也可以使用按位运算符处理存储在一个变量中的几个信息项。例如,颜色值常常记录为三个八位值,分别存储颜色中红、绿和蓝的强度。这些常常保存到四字节变量中的三个字节。第四个字节也不会浪费,包含表示颜色透明度的值。显然,要处理各个颜色成分,需要从变量中分离出各个字节,按位运算符就可以做到这一点。
再看另外一个例子,假定需要记录字体的信息,那么,只要存储每种字体的样式和字号,以及字体是黑体还是斜体,就可以把这些信息都存储在一个二字节的整型变量中,如图3-1所示。
图3-1 把字体数据存储在2个字节中
可以使用一位来记录字体是否为斜体—— 1表示斜体,0表示一般。同样,用另一位来指定字体是否为黑体。使用一个字节可以从多达256种不同的样式中选择一个,再用另外5位记录最多32磅的字号。因此,在一个16位的字中,可以记录四个不同的数据项。按位运算符提供了访问和修改整数中单个位和一组位的便利方式,能方便地组合和分解一个16位的字。
3.3.1 移位运算符
移位运算符可以把整型变量中的内容向左或向右移动指定的位数。移位运算符和其他按位运算符一起使用,可以获得前面描述的结果。>>运算符把位向右移动,<<运算符把位向左移动,移出变量两端的位被舍弃。
所有的按位操作都可以处理任何类型的整数,但本章的例子使用16位的变量,使例子较为简单。用下面的语句声明并初始化一个变量number:
unsigned short number=16387U;
如第2章所述,不带符号的字面量应在数字的最后添加字母U或u。
在下面的语句中,对这个变量的内容进行移位,并存储结果:
unsigned short result = number <<2; //Shift left two bit positions
移位运算符的左操作数是要移位的值,右操作数指定要移动的位数。图3-2列出了该操作的过程。
图3-2 移位运算
从图3-2可以看出,把数值16387向左移动两位,得到数值12。数值的这种剧烈变化是舍弃高位数字的结果。
把数值向右移动,可以使用下面的语句:
result = number >>2; //Shift right two bit positions
把数值16387向右移动两位,得到数值4096。向右移动两位相当于使该数值除以4。
只要没有舍弃位,向左移动n位就相当于把该数值乘以2的n次方。换言之,就等于把该数值乘以2n。同样,向右移动n位就相当于把该数值除以2的n次方。但要注意,变量number向左移位时,如果舍弃了重要的位,结果就不是我们希望的那样了。可是,这与乘法运算并没有不同。如果把2字节的数值乘以4,就会得到相同的结果,所以向左移位和相乘仍是等价的。出现问题的原因是相乘的结果超出了2字节整数的取值范围。
如果需要修改原来的值,可以使用op= 赋值运算符。在这种情况下,可以使用>>=或<<=运算符。例如:
number >>= 2; // Shift contents of number two positions to the right
这等价于:
number =number >> 2; // Shift contents of number two positions to the right
这些移位运算符跟前面用于输入输出的插入和提取运算符有可能搞混。从编译器的角度来看,其含义一般可以从上下文中判断出来。否则,编译器就会生成一个消息,但用户需要非常小心。例如,如果输出变量number向左移动两位的结果,就应编写下面的代码:
cout << (number << 2);
其中,括号是必不可少的。没有括号,编译器就会把移位运算符解释为流插入运算符,从而得不到想要的结果。
按位移动带符号的整数
可以把移位运算符应用于带符号和不带符号的整型数。但是,向右移位运算符在带符号整数类型的操作随系统的不同而不同,这取决于编译器的实现。在一些情况下,向右移位运算符会在左边空出来的位上填充0。在其他情况下,符号位向右移动,在左边空出来的位上填充1。
移动符号位的原因是为了保持向右移位和除法运算的一致性。可以用char类型的变量来说明这一点,解释其工作原理。假定把value定义为char类型,其初始值为–104(十进制):
signed char value=–104;
其二进制表示为10011000。使用下面的操作把它向右移动两位:
value >>= 2; //Result 11100110
注释中显示了其二进制结果。右边溢出了两个0,因为符号位是1,就在左边空出来的位上填充1。该结果的十进制表示是–26,这正好是value的值除以4的结果。当然,对于不带符号的整数类型的操作,符号位不移动,在左边空出来的位上填充0。
前面说过,在向右移位负整数时,其操作是已定义好的,所以实现该操作时不一定要依赖它。因为在大多数情况下,使用这些运算符是为了进行按位操作,此时维护位模式的完整性是非常重要的。所以,应总是使用不带符号的整数,以确保避免高阶位的移位。
3.3.2 位模式下的逻辑运算
修改整数值中的位时,可以使用4个按位运算符,如表3-1所示。
表3-1 按位运算符
运 算 符 |
说 明 |
~ |
这是按位求反运算符。它是一个一元运算符,可以反转操作数中的位,即1变成0,0变成1 |
& |
这是按位与运算符,它对操作数中相应的位进行与运算。如果相应的位都是1,结果位就是1,否则就是0 |
^ |
这是按位异或运算符,它对操作数中相应的位进行异或运算。如果相应的位各不相同,例如一个位是1,另一个位是0,结果位就是1。如果相应的位相同,结果位就是0 |
| |
这是按位或运算符,它对操作数中相应的位进行或运算。如果两个对应的位中有一个是1,结果位就是1。如果两个位都是0,结果就是0 |
表3-1中的运算符按照其优先级排列,在这个集合中,按位求反运算符的优先级最高,按位或运算符的优先级最低。在附录D的运算符优先级表中,按位移动运算符<<和>>具有相同的优先级,它们位于~运算符的下面,&运算符的上面。
如果以前没有见过这些运算符,就会问“这非常有趣,但这是为什么?”。下面就将它们用于实践。
1. 使用按位与运算符
按位与运算符一般用于选择整数值中特定的一个位或一组位。为了说明这句话的含义,下面再次使用本节开头的例子,利用一个16位整数存储字体的特性。
假定声明并初始化一个变量,指定一种12磅字号、斜体、样式为6的字体。实际上,就是图3-1中的字体。样式的二进制值是00000110,斜体位是1,黑体位是0,字号是01100。还有一个没有使用的位,需要把font变量的值初始化为二进制数0000 0110 0100 1100。
由于4位二进制数对应于一个16进制数,因此最简单的方法是以十六进制方式指定初 始值:
unsigned short font=0x064C; // Style 6, italic, 12 point
注释:
在建立像这样的位模式时,十六进制表示法要比十进制表示法更合适。
要使用字号,需要从font变量中提取它,这可以使用按位与运算符来实现。只有当两个位都是1时,按位与运算符才会产生1,所以可以定义一个值,在将定义字号的位和font执行按位与操作时选择该位。为此,只需定义一个值,该值在我们感兴趣的位上包含1,在其他位上包含0。这种值称为掩码,用下面的语句定义它:
unsigned short size_mask=0x1F; //Mask is 0000 0000 0001 1111
//to select size
font变量的5个低位表示其字号,把这些位设置为1,剩余的位设置为0,这样它们就会被舍弃(二进制数0000 0000 0001 1111可转换为十六进制数1F)。
现在可以用下面的语句提取font中的字号了:
unsigned short size=font & size_mask;
在&操作中,当两个对应的位是1时,结果位就是1。任何其他组合起来的结果就是0。因此组合起来的值如下:
font 0000 0110 0100 1100
size_mask 0000 0000 0001 1111
font & size_mask 0000 0000 0000 1100
把二进制值分解为4位一组的形式并不是很重要,这只是易于表示对应的十六进制数,看出其中有多少位。掩码的作用是把最右边的5位分隔出来,这5位表示点数(即字号)。
可以使用同样的方法选择字体的样式,只是还需要使用按位移动运算符把样式值向右移动。可以用下面的语句定义一个掩码,选择左边的8位,如下所示:
unsigned short style_mask=0xFF00; //Mask is 1111 1111 0000 0000
//for style
用下面的语句获取样式值:
unsigned short style=(font & style_mask) >> 8; //Extract the style
该语句的结果如下:
font 0000 0110 0100 1100
style_mask 1111 1111 0000 0000
font & style_mask 0000 0110 0000 0000
(font & style_mask) >> 8 0000 0000 0000 0110
为表示斜体和黑体的位定义掩码,并把相应的位设置为1,就很容易把它们分隔出来。当然,还需要一种方式来测试得到的位,这部分内容详见第4章。
按位与运算符的另一个用途是关闭位。前面介绍的是掩码中为0的位在结果中也将输出0。例如,为了关闭表示斜体的位,其他的位不变,只需定义一个掩码,使该掩码中的斜体位为0,其他位为1,再对font变量和该掩码进行按位与操作即可。实现此操作的代码将在按位或运算符一节中介绍。
2. 使用按位或运算符
可以使用按位或运算符设置一个或多个位。继续操作前面的font变量,现在需要设置斜体和黑体位。用下面的语句可以定义掩码,选择这些位:
unsigned short italic=0x40U; //Seventh bit from the right
unsigned short bold=0x20U; //Sixth bit from the right
用下面的语句设置黑体位:
font |= bold; // Set bold
位的组合如下:
font 0000 0110 0100 1100
bold 0000 0000 0010 0000
font | bold 0000 0110 0110 1100
现在,font变量指定它表示的字体是黑体和斜体。注意这个操作会设置位,而不考虑以前的状态。如果以前位的状态是开,则现在仍保持开的状态。
也可以对掩码执行按位或操作,设置多个位。下面的语句就同时设置了黑体和斜体:
font |= bold | italic; //Set bold and italic
该语言很容易让人选择错误的运算符。“设置斜体和黑体”很容易让人觉得应使用&运算符,而这是错误的。对两个掩码执行按位与操作会得到一个所有位都是0的值,这不会改变字体的任何属性。
如上一节最后所述,可以使用&运算符关闭位。也就是定义一个掩码,把其中要关闭的位设置为0,其他位设置为1。但如何指定这样的掩码?如果要显式指定它,就需要知道变量中有多少个字节,如果希望程序可以任何方式移植,这就不很方便。可是,在通常用于打开位的掩码上使用按位求反运算符,就可以得到这样的掩码。在bold掩码上关闭黑体位,就可以得到该掩码:
bold 0000 0000 0010 0000
~bold 1111 1111 1101 1111
按位求反运算符的作用是反转原数值中的每一位,使0变成1,1变成0。无论bold变量占用2个字节、4个字节还是8个字节,这都会生成我们期望的结果。
提示:
按位求反运算符有时称为NOT运算符,因为对于它操作的每个位,都会得到跟开始不同的值。
因此,在关闭黑体位时,只需对掩码bold的反码和font变量执行按位与操作,可用的语句如下所示:
font &= ~bold; //Turn bold off
还可以使用&运算符把几个掩码组合起来,再对结果跟要修改的变量执行按位与操作,将多个位设置为0。例如:
font &= ~bold & ~italic; //Turn bold and italic off
这个语句把font变量中的斜体和黑体位设置为0。注意这里不需要括号,因为~运算符的优先级高于&运算符。但是,如果不清楚运算符的优先级,就应加上括号,表示希望执行的操作。这肯定是无害的,在需要括号时还可以正常发挥作用。
3. 使用按位异或运算符
按位异或运算符的使用频率远远低于&和 | 运算符,有关它的使用例子也比较少。但它的一个重要应用是图形编程。在屏幕中创建动画的一种方式是绘制一个对象,删除它,再在一个新位置重新绘制。如果要求动画很平滑,这个过程就需要重复得很快,其中删除是一个重要的部分。我们并不想删除和重新绘制整个屏幕,因为这非常费时,屏幕也会出现闪烁。最理想的是,只删除屏幕上要移动的对象。使用所谓的异或模式就可以做到这一点,得到非常平滑的 动画。
异或模式的理念是,在屏幕上用给定的颜色绘制对象,如果接着用背景色重新绘制它,它就会消失。如图3-3所示。
图3-3 用异或模式绘图
以异或模式在屏幕上绘制对象时,每次绘制对象的颜色会自动在为对象所选的颜色和背景色之间来回变化。得到这一效果的关键是使用按位异或运算符快速而自动地改变颜色。它使用异或运算符的一个特性,即如果对两个值进行异或操作,再对所得的结果和一个原始值执行异或操作,就会得到另一个值。这听起来很复杂,下面就用一个例子来说明。
假定要在前景色(这里使用红色)和背景色(白色)之间来回切换。颜色通常用3个8位值来表示,分别对应于红、蓝、绿的亮度,存储在一个4字节的整数中。通过改变颜色中的红、蓝和绿的比例,就可以获得大约1600万种不同的颜色,包括从白色到黑色之间的所有颜色。纯红色是0xFF0000,这时红色成分设置为其最大值,其他两种颜色即蓝色和绿色的成分设置为0。在相同颜色模式下,绿色就是0xFF00,蓝色是0xFF。在白色中,红、蓝、绿的成分具有相同的最大值,即0xFFFFFF。
可以用下面的语句定义表示红色和白色的变量:
unsigned long red=0XFF0000UL; //Color red
unsigned long white=0XFFFFFFUL; //Color white-RGB all maximum
接着创建一个掩码,用于在红色和白色之间来回切换,并把包含绘图颜色的变量初始化为红色:
unsigned long mask=red ^ white; //Mask for switching colors
unsigned long draw_color=red; //Drawing color
变量mask初始化为要切换的两种颜色的按位异或操作结果,因此:
red 1111 1111 0000 0000 0000 0000
white 1111 1111 1111 1111 1111 1111
mask(即red ^ white) 0000 0000 1111 1111 1111 1111
如果对mask和red执行异或操作,就会得到white,如果对mask和white执行异或操作,就会得到red。因此,使用draw_color中的颜色绘制对象,就可以通过下面的语句切换颜色:
draw_color ^= mask; //Switch the drawing color
当draw_color包含red时,其执行过程如下:
draw_color 1111 1111 0000 0000 0000 0000
mask 0000 0000 1111 1111 1111 1111
draw_color ^ mask 1111 1111 1111 1111 1111 1111
显然,draw_color的值从红色变为白色。再次执行这个语句,就会把颜色改回为红色:
draw_color^=mask; //Switch the drawing color
其执行过程如下:
draw_color 1111 1111 1111 1111 1111 1111
mask 0000 0000 1111 1111 1111 1111
draw_color ^ mask 1111 1111 0000 0000 0000 0000
draw_color又变成了红色。这个技术适用于任意两种颜色,当然它实际上与特定颜色没有一点关系,可以把它用于切换任意一对整型数值。
程序示例3.4—— 使用按位运算符
下面用一个例子来试验按位运算符,看看它们如何一起工作。本例还演示了如何使用异或运算符在两个值之间切换,以及如何使用掩码来选择和设置各个位。代码如下:
//Program 3.4 Using the bitwise operators
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setfill;
using std::setw;
int main() {
unsigned long red=0xFF0000UL; //Color red
unsigned long white=0xFFFFFFUL; //Color white - RGB all maximum
cout << std::hex; //Set hexadecimal output format
cout << setfill('0'); //Set fill character for output
cout << "\nTry out bitwise AND and OR operators.";
cout << "\nInitial value red = "<< setw(8) << red;
cout << "\nComplement ~red = "<< setw(8) <<~ red;
cout << "\nInitial value white = "<< setw(8) << white;
cout << "\nComplement ~ white = "<< setw(8) <<~ white;
cout << "\n Bitwise AND red & white = " << setw(8) << (red & white);
cout << "\n Bitwise OR red | white = " << setw(8) << (red | white);
cout << "\n\nNow we can try out successive exclusive OR operations.";
unsigned long mask=red ^ white;
cout << "\n mask= red ^ white = " << setw(8) << mask;
cout << "\n mask ^ red = " << setw(8) << (mask ^ red);
cout << "\n mask ^ white = " << setw(8) << (mask ^ white);
unsigned long flags=0xFF; //Flags variable
unsigned long bit1mask=0x1; //Selects bit 1
unsigned long bit6mask=0x20; //Selects bit 6
unsigned long bit20mask=0x80000; //Selects bit 20
cout << "\n\nNow use masks to select or set a particular flag bit.";
cout << "\nSelect bit 1 from flags : " << setw(8) << (flags & bit1mask);
cout << "\nSelect bit 6 from flags : " << setw(8) << (flags & bit6mask);
cout << "\nSwitch off bit 6 in flags : " << setw(8) << (flags &= ~bit6mask);
cout << "\nSwitch on bit 20 in flags : " << setw(8) << (flags |= bit20mask);
cout <<endl;
return 0;
}
该例子的输出如下:
Try out bitwise AND and OR operators.
Initial value red = 00ff0000
Complement ~red = ff00ffff
Initial value white = 00ffffff
Complement ~ white = ff000000
Bitwise AND red & white = 00ff0000
Bitwise OR red | white = 00ffffff
Now we can try out successive exclusive OR operations.
mask= red ^ white =0000ffff
mask ^ red = 00ffffff
mask ^ white =00ff0000
Now use masks to select or set a particular flag bit.
Select bit 1 from flags : 00000001
Select bit 6 from flags : 00000020
Switch off bit 6 in flags : 000000df
Switch on bit 20 in flags : 000800df
例子的说明
本例中添加了对标准头文件<iomanip>的#include指令,这个头文件在第2章介绍过,因为代码将使用操纵程序控制输出的格式。首先,定义两个整数变量,它们包含的值表示要用于后续按位运算的颜色:
unsigned long red=0xFF0000UL; //Color red
unsigned long white=0xFFFFFFUL; //Color white - RGB all maximum
为了把数据显示为十六进制值,可用下面的语句指定:
cout << std::hex; //Set hexadecimal output format
其中hex是一个操纵程序,它把整数值的输出表示为十六进制。注意,这是模式化的,该程序以后在标准输出流中的所有整数输出都采用十六进制格式。不需要把hex发送给输出流cout。如果需要,可以用下面的语句把输出格式改回为十进制:
cout << std::dec; //Set decimal output format
这个语句使用dec操纵程序,把整数输出重新设置为默认的十进制表示。注意,把输出格式设置为十六进制,仅影响整数值。浮点数值会继续显示为一般的十进制。
如果在输出整数时加上前导0,就会使结果更清晰易懂。用下面的语句设置这种模式:
cout << setfill('0'); //Set fill character for output
其中setfill()是一个操纵程序,它把填充字符设置为括号中的字符。这也是模式化的,这样,以后的所有整数输出都会在需要时使用这个填充字符。它对十进制和十六进制输出都起作用。如果要用星号代替该填充字符,则可以使用下面的语句:
cout << setfill('*'); //Set fill character for output
要把填充字符设置回原来的默认值,只需在括号中使用空格:
cout << setfill(' '); //Set fill character for output
下面的语句显示red的值及其反码:
cout << "\nInitial value red = "<< setw(8) << red;
cout << "\nComplement ~red = "<< setw(8) <<~ red;
这里使用第2章介绍的setw()操纵程序,把输出字段宽度设置为8。如果所有的输出值都采用相同的字段宽度,就很容易比较它们。设置宽度不是模式化的,它只应用于跟在字段宽度设置点后面的下一条语句的输出。在red和white的输出中,~运算符反转了其操作数的位。
下面的语句使用按位与以及按位或运算符来合并red和white:
cout << "\n Bitwise AND red & white = " << setw(8) << (red & white);
cout << "\n Bitwise OR red | white = " << setw(8) << (red | white);
注意输出中表达式的括号。它们是必需的,因为<<的优先级高于&和|。没有括号,该语句就不会编译。如果查看一下输出,就会看出它跟这里讨论的相同。若对两个值都为1的位执行按位与操作,就会得到1,否则结果就是0。在对两个位执行按位或操作时,除非两个位都是0,否则结果就是1。
然后,创建一个掩码,在通过按位异或运算符组合两个值时,该掩码用于反转red和white的值。
unsigned long mask=red ^ white;
如果查看一下mask值的输出,就会发现在两个位的值不同时,对两个位执行异或操作的结果是1,在两个位的值相同时,该操作的结果是0。利用异或运算符把mask和两个颜色值中的一个组合起来,就会得到另一个颜色值。这可以用下面的语句来说明:
cout << "\n mask ^ red = " << setw(8) << (mask ^ red);
cout << "\n mask ^ white = " << setw(8) << (mask ^ white);
最后一组语句演示了如何使用掩码从一组标记位中选择一个位。选择某个位的掩码必须使该位的值为1,其他位的值为0。因此,从一个32位long变量中选择第1、6和20位的掩码定义如下:
unsigned long bit1mask=0x1; //Selects bit 1
unsigned long bit6mask=0x20; //Selects bit 6
unsigned long bit20mask=0x80000; //Selects bit 20
要从flags中选择一个位,只需对相应的掩码和flages的值执行按位与操作。例如:
cout << "\nSelect bit 6 from flags : " << setw(8) << (flags & bit6mask);
从输出中可以看到,表达式(flags & bit6mask)的结果是只设置了第6位的整数。当然,如果flages中的第6位为0,该表达式的结果就是0。
要关闭一个位,需要对flages变量和一个掩码执行按位与操作。在该掩码中,要关闭的那个位是0,其他位是1。对掩码和对应的位执行按位求反操作,也可以关闭该位。bit6mask就是这样的一个掩码。下面的语句把flags中的第6位关闭,并显示结果:
cout << "\nSwitch off bit 6 in flags : " << setw(8) << (flags &= ~bit6mask);
当然,如果第6位已经是0,该位就保持不变。要打开一个位,只需对flages和一个掩码执行按位或操作,在该掩码中,要打开的那个位是1:
cout << "\nSwitch on bit 20 in flags : " << setw(8) << (flags |= bit20mask);
这个语句把flags中的第20位设置为1,并显示结果。如果这个位已经是1,该位将保持不变。
4. 输出操纵程序
把第2章介绍的也算在内,到目前为止我们已介绍了5个模式化输出操纵程序,它们都是在<iostream>头文件中定义的:scientific、fixed、dec、hex和oct。表3-2列出所有的其他相似的操纵程序。目前不介绍后两项中的bool值,它将在第4章讨论。
表3-2 输出操纵程序
操 纵 程 序 |
执行的动作 |
dec |
把整数值格式化为十进制。这是默认的表示法 |
hex |
把整数值格式化为十六进制 |
oct |
把整数值格式化为八进制 |
left |
使输出字段中的值左对齐,其右端用填充字符填充。默认的填充字符是空格 |
right |
使输出字段中的值右对齐,其左端用填充字符填充。这是默认的对齐方式 |
fixed |
以固定点表示法输出浮点数值,即不带指数 |
scientific |
以科学表示法输出浮点数值,即尾数加指数的方式。默认的模式根据要显示的数值,选择fixed或scientific表示法 |
showpoint |
给浮点数值显示小数点和尾部的0 |
noshowpoint |
与上一个操纵程序相反。这是默认的 |
showbase |
在八进制的输出前面加上前导0,在十六进制的输出前面加上前导0x或0X |
noshowbase |
八进制和十六进制的输出中不显示前缀。这是默认的 |
showpos |
正数前面加上+号 |
noshowpos |
正数前面不显示+号,这是默认的 |
uppercase |
在以十六进制格式输出整数时,给十六进制数字显示大写字母A到F。如果设置了showbase,还要显示0X。在以科学计数法输出数值时,给指数显示E,而不是使用小写字母e |
nouppercase |
对上述项使用小写字母,这是默认的 |
boolalpha |
把bool值显示为true和false |
noboolalpha |
把bool值显示为1和0 |
可以一次设置多种模式,方法是在流中插入多个操纵程序。例如,如果要把整型数据输出为十六进制值,且在输出字段中左对齐,就可以使用下面的语句:
std::cout << std::hex << std::left << value;
这个语句会把value(以及程序中后续的所有整数,除非改变了设置)输出为左对齐的十六进制数值。
表3-3列出了需要提供参数值的输出操纵程序。
表3-3 需要参数的输出操纵程序