5.3. 位操作符
位操作符(表5-3)使用整型的操作数。位操作符将其整型操作数视为二进制位的集合,为每一位提供检验和设置的功能。另外,这类操作符还可用于bitset 类型(第 3.5 节)的操作数,该类型具有这里所描述的整型操作数的行为。
表 5.3. 位操作符
操作符 功能 用法
~ bitwise NOT(位求反)~expr
<< left shift(左移) expr1 << expr2
>> right shift(右移) expr1 >> expr2
& bitwise AND(位与) expr1 & expr2
^ bitwise XOR(位异或)expr1 ^ expr2
| bitwise OR(位或)expr1 | expr2
位操作符操纵的整数的类型可以是有符号的也可以是无符号的。如果操作数为负数,则位操作符如何处理其操作数的符号位依赖于机器。于是它们的应用可能不同:在一个应用环境中实现的程序可能无法用于另一应用环境。
对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用unsigned整型操作数。
在下面的例子中,假设 unsigned char 类型有 8 位。位求反操作符(~)的功能类似于bitset 的 flip 操作(第 3.5.2 节):将操作数的每一个二进制位取反:将 1 设置为 0、0 设置为 1,生成一个新值:
unsigned char bits = 0227;
bits = ~bits;
<< 和 >>操作符提供移位操作,其右操作数标志要移动的位数。这两种操作符将其左操作数的各个位向左(<<)或向右(>>)移动若干个位(移动的位数由其右操作数指定),从而产生新的值,并丢弃移出去的位。
unsigned char bits = 1;
bits << 1; // left shift
bits << 2; // left shift
bits >> 3; // right shift
左移操作符(<<)在右边插入 0 以补充空位。对于右移操作符(>>),如果其操作数是无符号数,则从左边开始插入 0;如果操作数是有符号数,则插入符号位的副本或者 0 值,如何选择需依据具体的实现而定。移位操作的右操作数不可以是负数,而且必须是严格小于左操作数位数的值。否则,操作的效果未定义。
位与操作(&)需要两个整型操作数,在每个位的位置,如果两个操作数对应的位都为 1,则操作结果中该位为 1,否则为 0。
常犯的错误是把位与操作(&)和逻辑与操作(&&)(第 5.2 节)混淆了。同样地,位或操作(|)和逻辑或操作(||)也很容易搞混。
下面我们用图解的方法说明两个 unsigned char 类型值的位与操作,这两个
操作数均用八进制字面常量初始化:
unsigned char b1 = 0145;
unsigned char b2 = 0257;
unsigned char result = b1 & b2;
位异或(互斥或,exclusive or)操作符(^)也需要两个整型操作数。在每个位的位置,如果两个操作数对应的位只有一个(不是两个)为1,则操作结果中该位为 1,否则为 0。
result = b1 ^ b2;
位或(包含或,inclusive or)操作符(|)需要两个整型操作数。在每个位的位置,如果两个操作数对应的位有一个或者两个都为 1,则操作结果中该位为 1,否则为 0。
result = b1 | b2;
5.3.1. bitset 对象或整型值的使用
bitset 类比整型值上的低级位操作更容易使用。观察下面简单的例子,了解如何使用 bitset 类型或者位操作来解决问题。假设某老师带了一个班,班中有 30 个学生,每个星期在班上做一次测验,只有及格和不及格两种测验成绩,对每个学生用一个二进制位来记录一次测试及格或不及格,以方便我们跟踪每次测验的结果,这样就可以用一个bitset 对象或整数值来代表一次测验:
bitset<30> bitset_quiz1; // bitsetsolution
unsigned long int_quiz1 = 0; // simulatedcollection of bits
使用 bitset 类型时,可根据所需要的大小明确地定义 bitset_quiz1,它的每一个位都默认设置为 0 值。如果使用内置类型来存放测验成绩,则应将变量 int_quiz1 定义为 unsigned long 类型,这种数据类型在所有机器上都至少拥有32 位的长度。最后,显式地初始化 int_quiz1 以保证该变量在使用前具有明确定义的值。
老师可以设置和检查每个位。例如,假设第27 位所表示的学生及格了,则可以使用下面的语句适当地设置对应的位:
bitset_quiz1.set(27); // indicate studentnumber 27 passed
int_quiz1 |= 1UL<<27; // indicatestudent number 27 passed
如果使用 bitset 实现,可直接传递要置位的位给 set 函数。而用unsigned long 实现时实现的方法则比较复杂。设置指定位的方法是:将测验数据与一个整数做位或操作,该整数只有一个指定的位为 1。也就是说,我们需要一个只有第 27 位为 1 其他位都为0 的无符号长整数(unsignedlong),这样的整数可用左移操作符和整型常量 1 生成:
1UL << 27; // generate a value withonly bit number 27 set
然后让这个整数与 int_quiz1 做位或操作,操作后,除了第 27 位外其他所有位的值都保持不变,而第 27 位则被设置为 1。这里,使用复合赋值操作(第1.4.1 节)将位或操作的结果赋给 int_quiz1,该操作符 |= 操作的方法与 +=相同。于是,上述功能等效于下面更详细的形式:
// following assignment is equivalent toint_quiz1 |= 1UL << 27;
int_quiz1 = int_quiz1 | 1UL << 27;
如果老师重新复核测验成绩,发现第 27 个学生实际上在该次测验中不及
格,这时老师应把第 27 位设置为 0:
bitset_quiz1.reset(27); // student number27 failed
int_quiz1 &= ~(1UL<<27); //student number 27 failed
使用 bitset 的版本可直接实现该功能,只要复位(reset)指定的位即可。而对于另一种情况,则需通过反转左移操作后的结果来实现设置:此时,我们需要一个只有第 27 位为 0 而其他位都为 1 的整数。然后将这个整数与测验数据做位与操作,把指定的位设置为 0。位求反操作使得除了第 27 位外其他位都设置为 1,然后此值和 int_quiz1 做位与操作,保证了除第 27 位外所有的位都保持不变。
最后,可通过以下代码获知第 27 个学生是否及格:
bool status;
status = bitset_quiz1[27]; // how didstudent number 27 do?
status = int_quiz1 & (1UL<<27);// how did student number 27 do?
使用 bitset 的版本中,可直接读取其值判断他是否及格。使用 unsignedlong 时,首先要把一个整数的第 27 位设置为 1,然后用该整数和 int_quiz1做位与操作,如果 int_quiz1 的第 27 位为1,则结果为非零值,否则,结果为零。
一般而言,标准库提供的 bitset 操作更直接、更容易阅读和书写、正确使用的可能性更高。而且,bitset 对象的大小不受 unsigned 数的位数限制。通常来说,bitset 优于整型数据的低级直接位操作。
5.3.2. 将移位操作符用于IO
输入输出标准库(IO library)分别重载了位操作符 >> 和 << 用于输入和输出。即使很多程序员从未直接使用过位操作符,但是相当多的程序都大量用到这些操作符在IO 标准库中的重载版本。重载的操作符与该操作符的内置类型版本有相同的优先级和结合性。因此,即使程序员从不使用这些操作符的内置含义来实现移位操作,但是还是应该先了解这些操作符的优先级和结合性。
IO 操作符为左结合
像其他二元操作符一样,移位操作符也是左结合的。这类操作符从左向右地结合,正好说明了程序员为什么可以把多个输入或输出操作连接为单个语句:
cout << "hi" <<" there" << endl;
执行为:
( (cout << "hi") <<" there" ) << endl;
在这个语句中,操作数"hi"与第一个 << 符号结合,其计算结果与第二个 <<符号结合,第二个 << 符号操作后,其结果再与第三个 << 符号结合。移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。若 IO 表达式的操作数包含了比IO 操作符优先级低的操作符,相关的优先级别将影响书写该表达式的方式。通常需使用圆括号强制先实现右结合:
cout << 42 + 10; // ok, + has higherprecedence, so the sum is
printed
cout << (10 < 42); // ok:parentheses force intended grouping; prints
1
cout << 10 < 42; // error: attemptto compare cout to 42!
The second cout is interpreted as
第二个cout 语句解释为:
(cout << 10) < 42;
该表达式说“将 10 写到 cout,然后用此操作(也就是 cout)的结果与 42
做比较”。
5.4. 赋值操作符
赋值操作符的左操作数必须是非 const 的左值。下面的赋值语句是不合法的:
int i, j, ival;
const int ci = i; // ok: initialization notassignment
1024 = ival; // error: literals are rvalues
i + j = ival; // error: arithmeticexpressions are rvalues
ci = ival; // error: can't write to ci
数组名是不可修改的左值:因此数组不可用作赋值操作的目标。而下标和解引用操作符都返回左值,因此当将这两种操作用于非 const 数组时,其结果可作为赋值操作的左操作数:
int ia[10];
ia[0] = 0; // ok: subscript is an lvalue
*ia = 0; // ok: dereference also is anlvalue
赋值表达式的值是其左操作数的值,其结果的类型为左操作数的类型。通常,赋值操作将其右操作数的值赋给左操作数。然而,当左、右操作数的类型不同时,该操作实现的类型转换可能会修改被赋的值。此时,存放在左、右操作数里的值并不相同:
ival = 0; // result: type int value 0
ival = 3.14159; // result: type int value 3
上述两个赋值语句都产生int 类型的值,第一个语句中 ival 的值与右操作数的值相同;但是在第二个语句中,ival 的值则与右操作数的值不相同。
5.4.1. 赋值操作的右结合性
与下标和解引用操作符一样,赋值操作也返回左值。同理,只要被赋值的每个操作数都具有相同的通用类型,C++语言允许将这多个赋值操作写在一个表达式中:
int ival, jval;
ival = jval = 0; // ok: each assigned 0
与其他二元操作符不同,赋值操作具有右结合特性。当表达式含有多个赋值操作符时,从右向左结合。上述表达式,将右边赋值操作的结果(也就是 jval)赋给 ival。多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换(第 5.12 节)为同一类型的数据类型:
int ival; int *pval;
ival = pval = 0; // error: cannot assignthe value of a pointer to
an int
string s1, s2;
s1 = s2 = "OK"; // ok:"OK" converted to string
第一个赋值语句是不合法的,因为 ival 和 pval 是不同类型的对象。虽然0 值恰好都可以赋给这两个对象,但该语句仍然错误。因为问题在于给 pval 赋值的结果是一个 int* 类型的值,不能将此值赋给 int 类型的对象。另一方面,第二个赋值语句则是正确的。字符串字面值可以转换为string 类型,string 类型的值可赋给 s2 变量。右边赋值操作的结果为 s2,再将此结果值赋给 s1。
5.4.2. 赋值操作具有低优先级
另一种通常的用法,是将赋值操作写在条件表达式中,把赋值操作用作长表达式的一部分。这种做法可缩短程序代码并阐明程序员的意图。例如,下面的循环调用函数 get_value,假设该函数返回 int 数值,通过循环检查这些返回值,直到获得需要的值为止——这里是 42:
int i = get_value(); // get_value returnsan int
while (i != 42) {
// do something ...
i = get_value(); }
首先,程序将所获得的第一个值存储在 i 中,然后建立循环检查i 的值是否为 42,如果不是,则做某些处理。循环中的最后一条语句调用 get_value() 返回一个值,然后继续循环。该循环可更简洁地写为:
int i;
while ((i = get_value()) != 42) {
// do something ...
}
现在,循环条件更清晰地表达了程序员的意图:持续循环直到 get_value 返回 42 为止。在循环条件中,将 get_value 返回的值赋给 i,然后判断赋值的结果是否为 42。
在赋值操作上加圆括号是必需的,因为赋值操作符的优先级低于不等操作符。如果没有圆括号,操作符 != 的操作数则是调用 get_value 返回的值和42,然后将该操作的结果 true 或false 赋给 i—— 显然这并不是我们想要的。
谨防混淆相等操作符和赋值操作符
可在条件表达式中使用赋值操作,这个事实往往会带来意外的效果:
if (i = 42)
此代码是合法的:将 42 赋给 i,然后检验赋值的结果。此时,42 为非零值,因此解释为 true。其实,程序员的目的显然是想判断i 的值是否为 42:
if (i == 42)
这种类型的程序错误很难发现。有些(并非全部)编译器会为类似于上述例子的代码提出警告。
5.4.3. 复合赋值操作符
我们常常在对某个对象做某种操作后,再将操作结果重新赋给该对象。例如,考虑第 1.4.2节的求和程序:
int sum = 0;
// sum values from 1 up to 10 inclusive
for (int val = 1; val <= 10; ++val)
sum += val; // equivalent to sum = sum +val
C++ 语言不仅对加法,而且还对其他算术操作符和位操作符提供了这种用法,称为复合赋值操作。复合赋值操作符的一般语法格式为:
a op= b;
其中,op= 可以是下列十个操作符之一:
+= -= *= /= %= // arithmetic operators
<<= >>= &= ^= |= // bitwiseoperators
这两种语法形式存在一个显著的差别:使用复合赋值操作时,左操作数只计算了一次;而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操
作数,而第二次则用做左操作数。除非考虑可能的性能价值,在很多(可能是大
部分的)上下文环境里这个差别不是本质性的。
a = a op b;
这两种语法形式存在一个显著的差别:使用复合赋值操作时,左操作数只计算了一次;而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。除非考虑可能的性能价值,在很多(可能是大部分的)上下文环境里这个差别不是本质性的。
5.5. 自增和自减操作符
自增(++)和自减(--)操作符为对象加1 或减1 操作提供了方便简短的实现方式。它们有前置和后置两种使用形式。到目前为止,我们已经使用过前自增操作,该操作使其操作数加1,操作结果是修改后的值。同理,前自减操作使其操作数减 1。这两种操作符的后置形式同样对其操作数加 1(或减 1),但操作后产生操作数原来的、未修改的值作为表达式的结果:
int i = 0, j;
j = ++i; // j = 1, i = 1: prefix yieldsincremented value
j = i++; // j = 1, i = 2: postfix yieldsunincremented value
因为前置操作返回加1 后的值,所以返回对象本身,这是左值。而后置操作
返回的则是右值。
后置操作符返回未加 1 的值
当我们希望在单个复合表达式中使用变量的当前值,然后再加1 时,通常会
使用后置的 ++ 和 -- 操作:
vector
ivec; // empty vector int cnt = 10;
// add elements 10...1 to ivec
while (cnt > 0)
ivec.push_back(cnt--); // int postfixdecrement
这段程序使用了后置的 -- 操作实现cnt 减 1。我们希望把 cnt 的值赋给vector 对象的下一个元素,然后在下次迭代前 cnt 的值减 1。如果在循环中使用前置操作,则是用 cnt 减 1 后的值创建 ivec 的新元素,结果是将 9 至 0十个元素依次添加到 ivec中。
在单个表达式中组合使用解引用和自增操作
下面的程序使用了一种非常通用的 C++ 编程模式输出 ivec 的内容:
vector
::iterator iter =ivec.begin(); // prints 10 9 8 ... 1
while (iter != ivec.end())
cout << *iter++ << endl; //iterator postfix increment
如果程序员对 C++ 和 C 语言都不太熟悉,则常常会弄不清楚表达式 *iter++ 的含义。
由于后自增操作的优先级高于解引用操作,因此 *iter++ 等效于*(iter++)。子表达式 iter++ 使 iter 加 1,然后返回 iter 原值的副本作为该表达式的结果。因此,解引用操作 * 的操作数是 iter 未加 1 前的副本。
这种用法的根据在于后自增操作返回其操作数原值(没有加 1)的副本。如果返回的是加 1 后的值,则解引用该值将导致错误的结果:ivec 的第一个元素没有输出,并企图对一个多余的元素进行解引用。
建议:简洁即是美
没有 C 语言基础的 C++ 新手,时常会因精简的表达式而苦恼,特别是像 *iter++ 这类令人困惑的表达式。有经验的 C++程序员非常重视简练,他们更喜欢这么写:
cout << *iter++ << endl;
而不采用下面这种冗长的等效代码:
cout << *iter << endl;
++iter;
对于初学 C++ 的程序员来说,第二种形式更清晰,因为给迭代器加 1 和获取输出值这两个操作是分开来实现的。但是更多的 C++ 程序员更习惯使用第一种形式。
要不断地研究类似的代码,最后达到一目了然的地步。大部分的 C++ 程序员更喜欢使用简洁的表达式而非冗长的等效表达式。因此,C++ 程序员必须熟悉这种用法。而且,一旦熟悉了这类表达式,我们会发现使用起来更不容易出错。