第五章 表达式(part1) 算术、关系、逻辑、位、赋值

来源:互联网 发布:pc机装mac 编辑:程序博客网 时间:2024/05/16 02:42

5.1. 算术操作符

表 5.1 按优先级来对操作符进行分组——一元操作符优先级最高,其次是乘、除操作,接着是二元的加、减法操作。
高优先级的操作符要比低优先级的结合得更紧密。
这些算术操作符都是左结合,这就意味着当操作符的优先级相同时,这些操作符从左向右依次与操作数结合。

表 5.1. 算术操作符

Operator

操作符

Function

功能

Use

用法

+

unary plus(一元正号)

+ expr

-

unary minus(一元负号)

- expr

*

multiplication(乘法)

expr * expr

/

division(除法)

expr / expr

%

remainder(求余)

expr % expr

+

addition(加法)

expr + expr

-

subtraction(减法)

expr - expr


警告:溢出和其他算术异常 

某些算术表达式的求解结果未定义,其中一部分由数学特性引起,例如除零操作;其他则归咎于计算机特性,如溢出:计算出的数值超出了其类型的表示范围。

考虑某台机器,其 short 类型为 16 位,能表示的最大值是 32767。假设 short 类型只有 16 位,下面的复合赋值操作将会溢出:

     // max value if shorts are 8 bits     short short_value = 32767;     short ival = 1;     // this calculation overflows     short_value += ival;     cout << "short_value: " << short_value << endl;

表示 32768 这个有符号数需 17 位的存储空间,但是这里仅有 16 位,于是导致溢出现象的发生,此时,许多系统都不会给出编译时或运行时的警告。对于不同的机器,上述例子的 short_value 变量真正获得的值不尽相同。在我们的系统上执行该程序后将得到:

     short_value: -32768

其值“截断(wrapped around)”,将符号位的值由 0 设为 1,于是结果变为负数。

因为算术类型具有有限的长度,因此计算后溢出的现象常常发生。


算术操作符 +-* 和 / 具有直观的含义:加法、减法、乘法和除法。对两个整数做除法,结果仍为整数,如果它的商包含小数部分,则小数部分会被截除:

     int ival1 = 21/6;  //  integral result obtained by truncating the remainder     int ival2 = 21/7;  //  no remainder, result is an integral value

ival1 和 ival2 均被初始化为 3


操作符 % 称为“求余(remainder)”或“求模(modulus)”操作符,用于计算左操作数除以右操作数的余数。该操作符的操作数只能为整型,包括boolcharshort 、int 和 long 类型,以及对应的 unsigned 类型:

     int ival = 42;     double dval = 3.14;     ival % 12;   //  ok: returns 6     ival % dval; //  error: floating point operand

如果两个操作数为正,除法(/)和求模(%)操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数为负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零):

     21 % 6;   //  ok: result is 3     21 % 7;   //  ok: result is 0     -21 % -8; //  ok: result is -5     21 % -5;  //  machine-dependent: result is 1 or -4     21 / 6;   //  ok: result is 3     21 / 7;   //  ok: result is 3     -21 / -8; //  ok: result is 2     21 / -5;  //  machine-dependent: result -4 or -5
当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。
如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。


5.2. 关系操作符和逻辑操作符

表 5.2. 关系操作符和逻辑操作符

Each of these operators yields bool

下列操作符都产生 bool 值

Operator

操作符

Function

功能

Use

用法

!

logical NOT(逻辑非)

!expr

<

less than(小于)

expr < expr

<=

less than or equal(小于等于)

expr <= expr

>

greater than(大于)

expr > expr

>=

greater than or equal(大于等于)

expr >= expr

==

equality(相等)

expr == expr

!=

inequality(不等)

expr != expr

&&

logical AND(逻辑与)

expr && expr

||

logical OR(逻辑或)

expr || expr


<Note>:

逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。

只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。

我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。


对于逻辑与操作符,一个很有价值的用法是:如果某边界条件使 expr2 的计算变得危险,则应在该条件出现之前,先让 expr1 的计算结果为 false

例如,编写程序使用一个 string 类型的对象存储一个句子,然后将该句子的第一个单词的各字符全部变成大写,可如下实现:

     string s("Expressions in C++ are composed...");     string::iterator it = s.begin();     // convert first word in s to uppercase     while (it != s.end() && !isspace(*it)) {         *it = toupper(*it); // toupper covered in section 3.2.4 (p. 88)         ++it;     }
在这个例子中,while 循环判断了两个条件。首先检查 it 是否已经到达 string 类型对象的结尾,
如果不是,则 it 指向 s 中的一个字符。
只有当该检验条件成立时,系统才会计算逻辑与操作符的右操作数,即在保证it确实指向一个真正的字符之后,才检查该字符是否为空格。
如果遇到空格,或者 s 中没有空格而已经到达 s 的结尾时,循环结束。


逻辑非操作符

逻辑非操作符(!)将其操作数视为条件表达式,产生与其操作数值相反的条件值。如果其操作数为非零值,则做 ! 操作后的结果为 false

例如,可如下在 vector 类型对象的 empty 成员函数上使用逻辑非操作符,根据函数返回值判断该对象是否为空:

     // assign value of first element in vec to x if there is one     int x = 0;     if (!vec.empty())         x = *vec.begin();

不应该串接使用关系操作符

关系操作符(<、<=、>、<=)具有左结合特性。事实上,由于关系操作符返回bool类型的结果,因此很少使用其左结合特性。

如果把多个关系操作符串接起来使用,结果往往出乎预料:

     // oops! this condition does not determine if the 3 values are unequal     if (i < j < k) { /* ... */ }

这种写法只要 k 大于 1,上述表达式的值就为 true

这是因为第二个小于操作符的左操作数是第一个小于操作符的结果:true 或 false

也就是,该条件将 k 与整数 0 或 1 做比较。为了实现我们想要的条件检验,应重写上述表达式如下:

     if (i < j && j < k) { /* ... */ }

相等测试与 bool 字面值


由于 true 转换为 1,因此要检测某值是否与 bool 字面值 true 相等,其等效判断条件通常很难正确编写:

if (val == true) { /* ... */ }

val 本身是 bool 类型,或者 val 具有可转换为 bool 类型的数据类型。如果 val 是 bool 类型,则该判断条件等效于:

     if (val) { /* ... */ }
这样的代码更短而且更直接(尽管对初学者来说,这样的缩写可能会令人费解)。

更重要的是,如果 val 不是 bool 值,val 和 true 的比较等效于:

     if (val == 1) { /* ... */ }

这与下面的条件判断完全不同:

     // condition succeeds if val is any nonzero value     if (val) { /* ... */ }

此时,只要 val 为任意非零值,条件判断都得 true。如果显式地书写条件比较,则只有当 val 等于指定的 1 值时,条件才成立。


5.3. 位操作符

表 5.3. 位操作符

Operator

操作符

Function

功能

Use

用法

~

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 位。位求反操作符(~)的功能类似于bitsetflip 操作(第 3.5.2 节):将操作数的每一个二进制位取反:将 1 设置为 0、0 设置为 1,生成一个新值:

unsigned char bits = 0227;

10010111

bits = ~bits;

01101000


<<>> 操作符提供移位操作,其右操作数标志要移动的位数。这两种操作符将其左操作数的各个位向左(<<)或向右(>>)移动若干个位(移动的位数由其右操作数指定),从而产生新的值,并丢弃移出去的位。

unsigned char bits = 1;

10011011

bits << 1; // left shift

00110110

bits << 2; // left shift

01101100

bits >> 3; // right shift

00010011


左移操作符(<<)在右边插入 0 以补充空位。对于右移操作符(>>),如果其操作数是无符号数,则从左边开始插入 0;如果操作数是有符号数,则插入符号位的副本或者 0 值,如何选择需依据具体的实现而定。移位操作的右操作数不可以是负数,而且必须是严格小于左操作数位数的值。否则,操作的效果未定义。

位与操作(&)需要两个整型操作数,在每个位的位置,如果两个操作数对应的位都为 1,则操作结果中该位为 1,否则为 0。


下面我们用图解的方法说明两个 unsigned char类型值的位与操作,这两个操作数均用八进制字面常量初始化:

unsigned char b1 = 0145;

01100101

unsigned char b2 = 0257;

10101111

unsigned char result = b1 & b2;

00100101

位异或(互斥或,exclusive or)操作符(^)也需要两个整型操作数。在每个位的位置,如果两个操作数对应的位只有一个(不是两个)为 1,则操作结果中该位为 1,否则为 0。

result = b1 ^ b2;

11001010


位或(包含或,inclusive or)操作符(|)需要两个整型操作数。在每个位的位置,如果两个操作数对应的位有一个或者两个都为 1,则操作结果中该位为 1,否则为 0。

result = b1 | b2;

11101111


5.3.1. bitset 对象或整型值的使用

bitset 类比整型值上的低级位操作更容易使用。观察下面简单的例子,了解如何使用 bitset 类型或者位操作来解决问题。

假设某老师带了一个班,班中有 30 个学生,每个星期在班上做一次测验,只有及格和不及格两种测验成绩,对每个学生用一个二进制位来记录一次测试及格或不及格,以方便我们跟踪每次测验的结果,这样就可以用一个bitset对象或整数值来代表一次测验:

     bitset<30> bitset_quiz1;     //  bitset solution     unsigned long int_quiz1 = 0; // simulated collection of bits

使用 bitset 类型时,可根据所需要的大小明确地定义 bitset_quiz1,它的每一个位都默认设置为 0 值。

如果使用内置类型来存放测验成绩,则应将变量 int_quiz1 定义为 unsigned long 类型,这种数据类型在所有机器上都至少拥有32位的长度。

最后,显式地初始化 int_quiz1 以保证该变量在使用前具有明确定义的值。

老师可以设置和检查每个位。例如,假设第27位所表示的学生及格了,则可以使用下面的语句适当地设置对应的位:

     bitset_quiz1.set(27);   //  indicate student number 27 passed     int_quiz1 |= 1UL<<27;   //  indicate student number 27 passed

如果使用 bitset 实现,可直接传递要置位的位给 set 函数。而用 unsigned long 实现时,实现的方法则比较复杂。

设置指定位的方法是:将测验数据与一个整数做位或操作,该整数只有一个指定的位为 1。

也就是说,我们需要一个只有第 27 位为 1 其他位都为0的无符号长整数(unsigned long),这样的整数可用左移操作符和整型常量 1 生成:

     1UL << 27;  //  generate a value with only bit number 27 set

然后让这个整数与 int_quiz1 做位或操作,操作后,除了第 27 位外其他所有位的值都保持不变,而第 27 位则被设置为 1。

这里,使用复合赋值操作(第 1.4.1 节)将位或操作的结果赋给int_quiz1,该操作符|= 操作的方法与 += 相同。于是,上述功能等效于下面更详细的形式:

     //  following assignment is equivalent to int_quiz1 |= 1UL << 27;     int_quiz1 = int_quiz1 | 1UL << 27;

如果老师重新复核测验成绩,发现第 27 个学生实际上在该次测验中不及格,这时老师应把第 27 位设置为 0:

     bitset_quiz1.reset(27);   // student number 27 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 did student number 27 do?     status = int_quiz1 & (1UL<<27);  // how did student number 27 do?

使用 bitset 的版本中,可直接读取其值判断他是否及格。使用 unsigned long 时,首先要把一个整数的第 27 位设置为 1,然后用该整数和int_quiz1 做位与操作,如果int_quiz1 的第 27 位为 1,则结果为非零值,否则,结果为零。

<Note>:

一般而言,标准库提供的 bitset 操作更直接、更容易阅读和书写、正确使用的可能性更高。而且,bitset 对象的大小不受unsigned 数的位数限制。通常来说,bitset 优于整型数据的低级直接位操作。

将移位操作符用于IO

输入输出标准库(IO library)分别重载了位操作符 >><< 用于输入和输出。即使很多程序员从未直接使用过位操作符,但是相当多的程序都大量用到这些操作符在IO标准库中的重载版本。重载的操作符与该操作符的内置类型版本有相同的优先级和结合性。因此,即使程序员从不使用这些操作符的内置含义来实现移位操作,但是还是应该先了解这些操作符的优先级和结合性。

IO 操作符为左结合

像其他二元操作符一样,移位操作符也是左结合的。这类操作符从左向右地结合,正好说明了程序员为什么可以把多个输入或输出操作连接为单个语句:

     cout << "hi" << " there" << endl;

执行为:

     ( (cout << "hi") << " there" ) << endl;

在这个语句中,操作数"hi"与第一个 << 符号结合,其计算结果与第二个 << 符号结合,第二个 << 符号操作后,其结果再与第三个<< 符号结合。

移位操作符具有中等优先级:其优先级比算术操作符低,但比关系操作符、赋值操作符和条件操作符优先级高。若 IO 表达式的操作数包含了比IO操作符优先级低的操作符,相关的优先级别将影响书写该表达式的方式。通常需使用圆括号强制先实现右结合:

     cout << 42 + 10;   // ok, + has higher precedence, so the sum is printed     cout << (10 < 42); // ok: parentheses force intended grouping; prints 1     cout << 10 < 42;   // error: attempt to compare cout to 42!

第二个cout语句解释为:

     (cout << 10) < 42;

该表达式说“将 10 写到 cout,然后用此操作(也就是 cout)的结果与 42 做比较”。


5.4. 赋值操作符

赋值操作符的左操作数必须是非 const 的左值。下面的赋值语句是不合法的:

     int i, j, ival;     const int ci = i;  // ok: initialization not assignment     1024 = ival;       // error: literals are rvalues     i + j = ival;      // error: arithmetic expressions 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 an lvalue
赋值表达式的值是其左操作数的值,其结果的类型为左操作数的类型。

通常,赋值操作将其右操作数的值赋给左操作数。然而,当左、右操作数的类型不同时,该操作实现的类型转换可能会修改被赋的值。此时,存放在左、右操作数里的值并不相同:

     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

多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换为同一类型的数据类型:

     int ival; int *pval;     ival = pval = 0; // error: cannot assign the value of a pointer to an int     string s1, s2;     s1 = s2 = "OK";  // ok: "OK" converted to string

第一个赋值语句是不合法的,因为 ivalpval 是不同类型的对象。虽然0值恰好都可以赋给这两个对象,但该语句仍然错误。

因为问题在于给 pval 赋值的结果是一个 int* 类型的值,不能将此值赋给 int 类型的对象。

另一方面,第二个赋值语句则是正确的。字符串字面值可以转换为 string 类型,string 类型的值可赋给s2 变量。

右边赋值操作的结果为 s2,再将此结果值赋给 s1


5.4.2. 赋值操作具有低优先级

另一种通常的用法,是将赋值操作写在条件表达式中,把赋值操作用作长表达式的一部分。

这种做法可缩短程序代码并阐明程序员的意图。例如,下面的循环调用函数 get_value,假设该函数返回 int 数值,通过循环检查这些返回值,直到获得需要的值为止——这里是 42:

     int i = get_value();  // get_value returns an 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。

<Note>:

在赋值操作上加圆括号是必需的,因为赋值操作符的优先级低于不等操作符。

如果没有圆括号,操作符 != 的操作数则是调用 get_value 返回的值和 42,然后将该操作的结果 truefalse 赋给 i—— 显然这并不是我们想要的。

谨防混淆相等操作符和赋值操作符

可在条件表达式中使用赋值操作,这个事实往往会带来意外的效果:

     if (i = 42)

此代码是合法的:将 42 赋给 i,然后检验赋值的结果。此时,42 为非零值,因此解释为 true。其实,程序员的目的显然是想判断i的值是否为 42:

     if (i == 42)

这种类型的程序错误很难发现。有些(并非全部)编译器会为类似于上述例子的代码提出警告。

5.4.3. 复合赋值操作符

我们常常在对某个对象做某种操作后,再将操作结果重新赋给该对象。例如,考虑求和程序:

     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     <<= >>=   &=   ^=   |=   // bitwise operators

这两种语法形式存在一个显著的差别:使用复合赋值操作时,左操作数只计算了一次;

而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。

除非考虑可能的性能价值,在很多(可能是大部分的)上下文环境里这个差别不是本质性的。

     a = a op b;

这两种语法形式存在一个显著的差别:使用复合赋值操作时,左操作数只计算了一次;

而使用相似的长表达式时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。

除非考虑可能的性能价值,在很多(可能是大部分的)上下文环境里这个差别不是本质性的。


















原创粉丝点击