6 运算符
6.1 运算符优先级和结合性
6.1.1 复合表达式求值
为了评估表达式,编译器必须做两件事:
- 在编译时,编译器必须解析表达式并确定操作数如何与运算符分组。这是通过优先级和结合性规则完成的。
- 在编译时或运行时,对操作数进行评估并执行运算以产生结果。
6.1.2 运算符优先级和结合性
优先级较高的运算符会优先与操作数分组。
若两个运算符具有相同的优先级,则编译器不能仅使用优先级来确定应如何分组。
如果两个具有相同优先级的运算符在表达式中相邻,则运算符的结合性会告诉编译器是从左到右还是从右到左计算运算符(而不是操作数)。例如减法的优先级为 6,优先级为 6 的运算符的结合性为从左到右。
6.1.3 括号
在 C++ 中,我们可以显式地使用括号来根据需要设置操作数的分组。这是因为括号具有最高优先级之一,所以括号通常会先于其内部内容进行求值。
为了减少错误并使您的代码更容易理解而无需参考优先表,最好将任何非平凡复合表达式括起来,这样您的意图就很清楚了。一个好的经验法则是:除加、减、乘、除之外,所有运算都用括号括起来。
6.1.4 函数参数的求值顺序不确定
考虑以下程序:
#include <iostream>
int getValue()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
return x;
}
void printCalculation(int x, int y, int z)
{
std::cout << x + (y * z);
}
int main()
{
printCalculation(getValue(), getValue(), getValue()); // this line is ambiguous
return 0;
}
在该程序中,由于不确定表达式x+(y*z)
中x、y、z的取值顺序,初始输入的3个值可能被编译器任意地分配给三个参数,该顺序取决于编译器实现。因此,在不同的机器上运行会得到不同的结果。
操作数、函数参数和子表达式可以按任意顺序进行求值。
一个常见的错误是认为运算符的优先级和结合性会影响计算顺序。优先级和结合性仅用于确定操作数如何与运算符分组,以及值的计算顺序。
6.2 修改运算符和非修改运算符
可以修改其操作数值的运算符通常被称为修改运算符。在 C++ 中,大多数运算符都是非修改运算符——它们仅使用操作数进行计算并返回值。但是,有两类内置运算符会修改其左操作数(并返回值):
- 赋值运算符包括标准赋值运算符(
=
),算术赋值运算符(+=
,-=
,*=
,/=
和%=
),以及按位赋值运算符(<<=
,>>=
,&=
,|=
和^=
)。 - 增量和减量运算符(分别为
++
和--
)。
6.3 取余和幂指运算
6.3.1 取余运算符
取余运算符%
(通常也称为模运算符或取模运算符)是执行整数除法后返回余数的运算符。
然而在C++中,将%
称为模运算有欠妥当,因为%
和数学定义的模运算不同,在操作数有且仅有一个为负数的情况下,数学中的模运算通常与 %
产生的结果不同。
例如:
-21 mod 4 = 3
-21 % 4 = -1
6.3.2 幂指运算
C++ 不包含指数运算符。要在 C++ 中计算指数,请#include <cmath>
标头,并使用 pow()
函数。
请注意,函数 pow() 的参数(和返回值)均为双精度类型。由于浮点数的舍入误差,pow() 的结果可能不精确(即使你传递整数)。如果要进行整数幂运算,最好使用自己的函数(平方幂)。
6.4 增量/减量运算符
6.4.1 前缀递增和递减
前缀递增/递减运算符非常简单。首先,操作数递增或递减,然后表达式计算操作数的值。例如:
#include <iostream>
int main()
{
int x { 5 };
int y { ++x }; // x is incremented to 6, x is evaluated to the value 6, and 6 is assigned to y
std::cout << x << ' ' << y << '\n';
return 0;
}
6.4.2 后缀递增和递减
后缀自增/自减操作符更复杂。首先,复制操作数。然后自增或自减操作数(而不是副本)。最后,返回副本。例如:
#include <iostream>
int main()
{
int x { 5 };
int y { x++ }; // x is incremented to 6, copy of original x is evaluated to the value 5, and 5 is assigned to y
std::cout << x << ' ' << y << '\n';
return 0;
}
请注意,后缀版本需要更多步骤,因此性能可能不如前缀版本。
在编写可以同时使用前缀或后缀版本的情况下,最好使用前缀版本,因为它们通常更具性能,并且不太可能引起意外。
6.4.3 副作用
如果函数或表达式除了产生返回值之外还有一些可观察的效果,则称其具有副作用。副作用的常见示例包括更改对象的值、执行输入或输出或更新图形用户界面(例如,启用或禁用按钮)。
因为求值顺序问题,副作用可能产生未定义的行为。在上面的学习中我们得知表达式求值的计算顺序是编译器实现的,因此在同一个表达式中同时存在x
和++x
会导致未定义的行为:
#include <iostream>
int add(int x, int y)
{
return x + y;
}
int main()
{
int x { 5 };
int value{ add(x, ++x) }; // undefined behavior: is this 5 + 6, or 6 + 6?
// It depends on what order your compiler evaluates the function arguments in
std::cout << value << '\n'; // value could be 11 or 12, depending on how the above line evaluates!
return 0;
}
因此,请勿在给定语句中多次使用具有副作用的变量。如果这样做,结果可能未定义。
6.5 逗号运算符
逗号运算符(,)
让我们在允许单个表达式的地方可以编写多个表达式。逗号运算符计算左操作数,然后计算右操作数,最后返回右操作数的结果。请注意,逗号在所有运算符中的优先级最低,甚至低于赋值。
避免使用逗号运算符,但在for循环中除外。
6.6 关系运算符和浮点比较
6.6.1 关系运算符
关系运算符是用于比较两个值的运算符。有6个关系运算符:
运算符 | 符号 | 使用形式 | 结果 |
---|---|---|---|
大于 | > | x > y | x大于y,返回true,否则false |
小于 | < | x < y | x小于y,返回true,否则false |
大于等于 | >= | x >= y | x大于等于y,返回true,否则false |
小于等于 | <= | x <= y | x小于等于y,返回true,否则false |
等于 | == | x == y | x等于y,返回true,否则false |
不等于 | != | x != y | x不等于y,返回true,否则false |
它们非常直观。每个操作符的计算结果都是布尔值true(1)或false(0)。
6.6.2 浮点比较
比较浮点值可能会有问题:
#include <iostream>
int main()
{
constexpr double d1{ 100.0 - 99.99 }; // should equal 0.01 mathematically
constexpr double d2{ 10.0 - 9.99 }; // should equal 0.01 mathematically
if (d1 == d2)
std::cout << "d1 == d2" << '\n';
else if (d1 > d2)
std::cout << "d1 > d2" << '\n';
else if (d1 < d2)
std::cout << "d1 < d2" << '\n';
return 0;
}
如果在调试器中检查d1和d2的值,您可能会看到d1=0.010000000000005116和d2=0.99999999997868。这两个数字都接近0.01,但d1大于,d2小于。
使用任何关系运算符比较浮点值都可能是危险的。这是因为浮点值不精确,浮点操作数中的舍入错误可能会导致它们比预期的稍小或稍大。
当小于(<)、大于(>)、小于等于(<=)和大于等于(>=)运算符与浮点值一起使用时,它们在大多数情况下都会产生可靠的答案(当操作数的值不相似时)。然而,如果操作数几乎相同,则应认为这些运算符不可靠。例如,在上面的示例中,d1>d2碰巧产生true,但如果数值舍入方向相反,则也可能结果是false。
如果操作数相似时得到错误答案的结果是可以接受的,那么使用这些操作符也是可以的。这是一个特定于应用程序的判定。例如,考虑一个游戏(如太空入侵者),您希望确定两个移动对象(如导弹和外星人)是否相交。如果对象仍然相距很远,则这些运算符将返回正确的答案。如果这两个对象非常接近,您可能会得到错误答案。在这种情况下,错误的答案可能根本不会被注意到(它只是看起来像是差点儿打中或差点儿击中),游戏将继续。
等式运算符(== 和 !=)要麻烦得多。考虑运算符==,它仅在其操作数完全相等时返回true。因为即使最小的舍入误差也会导致两个浮点数不相等,所以当预期结果为true时,运算符==返回false的风险很高。运算符 != 也有同样的问题。
因此,通常应避免将这两个运算符与浮点操作数一起使用。
6.7 逻辑运算符
虽然关系(比较)运算符可以用于测试特定条件是真还是假,但它们一次只能测试一个条件。逻辑运算符提供了测试多个条件的能力。C++有3个逻辑运算符:
运算符 | 符号 | 使用形式 | 结果 |
---|---|---|---|
逻辑NOT | ! | !x | x为true,返回false,x为false,返回true |
逻辑AND | && | x && y | x与y均为true,返回true,否则返回false |
逻辑OR | || | x || y | x或y为true,返回true,否则返回false |
混合使用AND和OR
在同一表达式中混合使用逻辑AND和逻辑OR运算符通常是不可避免的,但这是一个充满潜在危险的领域。
因为逻辑AND和逻辑OR看起来像一对,所以许多程序员假设它们具有相同的优先级(就像加法/减法和乘法/除法)。然而,逻辑AND的优先级高于逻辑OR,因此逻辑AND运算符将在逻辑OR运算符之前计算(除非它们已被括号括起来)。
新手程序员通常会编写 value1 || value2 && value3 这样的表达式。由于逻辑“AND”具有更高的优先级,因此它的计算结果为 value1 ||(value2 && value3),而不是(value1 || value2)&& value3。如果假设从左到右执行(就像加法/减法或乘法/除法那样),将得到一个没有预料到的结果!
在同一表达式中混合逻辑AND和逻辑OR时,最好显式地用括号括起每个运算符及其操作数。这有助于防止优先级错误,使代码更易于阅读,并清楚地定义表达式的计算方式。