虽然关系(比较)运算符可用于测试特定条件是真还是假,但它们一次只能测试一个条件。通常我们需要知道多个条件是否同时为真。例如,要检查我们是否中奖,我们必须比较我们选择的所有多个数字是否与中奖号码匹配。在一个有 6 个号码的彩票中,这将涉及 6 次比较,所有这些比较都必须为真。在其他情况下,我们需要知道多个条件中是否有任何一个为真。例如,如果我们生病了,或者我们太累了,或者我们在上一个例子中中奖了,我们可能会决定今天不上班。这将涉及检查 3 个比较中是否有任何一个为真。
逻辑运算符为我们提供了测试多个条件的能力。
C++ 有 3 个逻辑运算符
运算符 | 符号 | 用法示例 | 操作 |
---|---|---|---|
逻辑非 | ! | !x | 如果 x 为假则为真,如果 x 为真则为假 |
逻辑与 | && | x && y | 如果 x 和 y 都为真则为真,否则为假 |
逻辑或 | || | x || y | 如果 x 或 y(或两者)为真则为真,否则为假 |
逻辑非
你已经在课程4.9 -- 布尔值中遇到过逻辑非一元运算符。我们可以这样总结逻辑非的效果
逻辑非(运算符 !) | |
---|---|
操作数 | 结果 |
真 | 假 |
假 | 真 |
如果逻辑非的操作数求值为真,则逻辑非求值为假。如果逻辑非的操作数求值为假,则逻辑非求值为真。换句话说,逻辑非将布尔值从真翻转为假,反之亦然。
逻辑非通常用于条件表达式
bool tooLarge { x > 100 }; // tooLarge is true if x > 100
if (!tooLarge)
// do something with x
else
// print an error
需要注意的是,逻辑非具有非常高的优先级。新程序员经常犯以下错误
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!x > y)
std::cout << x << " is not greater than " << y << '\n';
else
std::cout << x << " is greater than " << y << '\n';
return 0;
}
这个程序打印
5 is greater than 7
但是x不大于y,这怎么可能呢?答案是,因为逻辑非运算符的优先级高于大于运算符,表达式!x > y
实际上求值为(!x) > y
。由于x是 5,!x 求值为0,0 > y
为假,所以else语句执行!
编写上述代码片段的正确方法是
#include <iostream>
int main()
{
int x{ 5 };
int y{ 7 };
if (!(x > y))
std::cout << x << " is not greater than " << y << '\n';
else
std::cout << x << " is greater than " << y << '\n';
return 0;
}
这样,x > y
将首先被评估,然后逻辑非将翻转布尔结果。
最佳实践
如果逻辑非旨在对其他运算符的结果进行操作,则其他运算符及其操作数需要用括号括起来。
逻辑非的简单用法,例如if (!value)
不需要括号,因为优先级不起作用。
逻辑或
逻辑或运算符用于测试两个条件中的任何一个是否为真。如果左操作数求值为真,或右操作数求值为真,或两者都为真,则逻辑或运算符返回真。否则它将返回假。
逻辑或(运算符 ||) | ||
---|---|---|
左操作数 | 右操作数 | 结果 |
假 | 假 | 假 |
假 | 真 | 真 |
真 | 假 | 真 |
真 | 真 | 真 |
例如,考虑以下程序
#include <iostream>
int main()
{
std::cout << "Enter a number: ";
int value {};
std::cin >> value;
if (value == 0 || value == 1)
std::cout << "You picked 0 or 1\n";
else
std::cout << "You did not pick 0 or 1\n";
return 0;
}
在这种情况下,我们使用逻辑或运算符来测试左条件 (value == 0) 或右条件 (value == 1) 是否为真。如果其中任何一个(或两者)为真,则逻辑或运算符评估为真,这意味着 if 语句执行。如果两者都不为真,则逻辑或运算符评估为假,这意味着 else 语句执行。
警告
新程序员有时会尝试这个
if (value == 0 || 1) // incorrect: if value is 0, or if 1
当1
被求值时,它将隐式转换为bool
true
。因此,这个条件将始终求值为true
。
如果要将变量与多个值进行比较,则需要多次比较变量
if (value == 0 || value == 1) // correct: if value is 0, or if value is 1
你可以将许多逻辑或语句串联起来
if (value == 0 || value == 1 || value == 2 || value == 3)
std::cout << "You picked 0, 1, 2, or 3\n";
新程序员有时会将逻辑或运算符 (||) 与按位或运算符 (|)(在课程O.2 -- 按位运算符中介绍)混淆。尽管它们名称中都包含“或”,但它们执行不同的功能。混淆它们可能会导致不正确的结果。
逻辑与
逻辑与运算符用于测试两个操作数是否都为真。如果两个操作数都为真,则逻辑与返回真。否则,它返回假。
逻辑与(运算符 &&) | ||
---|---|---|
左操作数 | 右操作数 | 结果 |
假 | 假 | 假 |
假 | 真 | 假 |
真 | 假 | 假 |
真 | 真 | 真 |
例如,我们可能想知道变量x的值是否在10和20之间。这实际上是两个条件:我们需要知道x是否大于10,以及x是否小于20。
#include <iostream>
int main()
{
std::cout << "Enter a number: ";
int value {};
std::cin >> value;
if (value > 10 && value < 20)
std::cout << "Your value is between 10 and 20\n";
else
std::cout << "Your value is not between 10 and 20\n";
return 0;
}
在这种情况下,我们使用逻辑与运算符来测试左条件 (value > 10) AND 右条件 (value < 20) 是否都为真。如果两者都为真,则逻辑与运算符求值为真,并且if 语句执行。如果两者都不为真,或者只有一个为真,则逻辑与运算符求值为假,并且else 语句执行。
与逻辑或一样,你可以串联许多逻辑与语句
if (value > 10 && value < 20 && value != 16)
// do something
else
// do something else
如果所有这些条件都为真,则if 语句将执行。如果这些条件中的任何一个为假,则else 语句将执行。
与逻辑与按位或一样,新程序员有时会将逻辑与运算符 (&&) 与按位与运算符 (&) 混淆。
短路求值
为了让逻辑与返回真,两个操作数都必须求值为真。如果左操作数求值为假,逻辑与知道它必须返回假,无论右操作数求值为真还是假。在这种情况下,逻辑与运算符会立即返回假,甚至不评估右操作数!这被称为短路求值,它主要是为了优化目的而完成的。
类似地,如果逻辑或的左操作数为真,则整个 OR 条件必须评估为真,并且右操作数将不会被评估。
短路求值提供了另一个机会,可以说明为什么不应在复合表达式中使用会引起副作用的运算符。考虑以下代码片段
if (x == 1 && ++y == 2)
// do something
如果x不等于1,则整个条件必须为假,所以 ++y 永远不会被评估!因此,只有当x求值为 1 时,y才会被递增,这可能不是程序员的意图!
警告
短路求值可能导致逻辑或和逻辑与不评估右操作数。避免将带有副作用的表达式与这些运算符结合使用。
关键见解
逻辑或和逻辑与运算符是操作数可以以任何顺序求值的规则的一个例外,因为标准明确规定左操作数必须首先求值。
致进阶读者
只有这些运算符的内置版本执行短路求值。如果你重载这些运算符以使其与你自己的类型一起使用,那些重载的运算符将不会执行短路求值。
混合使用 AND 和 OR
在同一个表达式中混合使用逻辑与和逻辑或运算符通常是不可避免的,但这是一个充满潜在危险的领域。
由于逻辑与和逻辑或看起来像一对,许多程序员认为它们具有相同的优先级(就像加法/减法和乘法/除法一样)。然而,逻辑与的优先级高于逻辑或,因此逻辑与运算符将先于逻辑或运算符进行评估(除非它们已被括号括起来)。
新程序员通常会写出诸如value1 || value2 && value3
这样的表达式。由于逻辑与具有更高的优先级,这会求值为value1 || (value2 && value3)
,而不是(value1 || value2) && value3
。希望这就是程序员想要的!如果程序员假设从左到右关联(如加法/减法或乘法/除法那样),程序员将得到一个他或她不期望的结果!
当在同一个表达式中混合使用逻辑与和逻辑或时,最好明确地将每个运算符及其操作数用括号括起来。这有助于防止优先级错误,使你的代码更易于阅读,并清楚地定义了你希望表达式如何求值。例如,与其写value1 && value2 || value3 && value4
,不如写(value1 && value2) || (value3 && value4)
。
最佳实践
在单个表达式中混合使用逻辑与和逻辑或时,请明确地将每个操作用括号括起来,以确保它们按照你期望的方式进行求值。
德摩根定律
许多程序员也错误地认为!(x && y)
与!x && !y
是相同的。不幸的是,你不能以这种方式“分配”逻辑非。
德摩根定律告诉我们逻辑非在这些情况下应该如何分配
!(x && y)
等价于 !x || !y
!(x || y)
等价于 !x && !y
换句话说,当你分配逻辑非时,你还需要将逻辑与翻转为逻辑或,反之亦然!
这有时在试图使复杂表达式更易于阅读时很有用。
致进阶读者
我们可以通过证明!(x && y)
对于x
和y
的每个可能值都等于!x || !y
来证明德摩根定律的第一部分是正确的。为此,我们将使用一个称为真值表的数学概念
x | y | !x | !y | !(x && y) | !x || !y |
---|---|---|---|---|---|
假 | 假 | 真 | 真 | 真 | 真 |
假 | 真 | 真 | 假 | 真 | 真 |
真 | 假 | 假 | 真 | 真 | 真 |
真 | 真 | 假 | 假 | 假 | 假 |
在此表中,第一列和第二列代表我们的x
和y
变量。表中的每一行显示了x
和y
可能值的一种排列。因为x
和y
是布尔值,我们只需要 4 行即可涵盖x
和y
可以保存的每种可能值的组合。
表中其余的列表示我们希望根据x
和y
的初始值进行评估的表达式。第三列和第四列分别计算!x
和!y
的值。第五列计算!(x && y)
的值。最后,第六列计算!x || !y
的值。
你会注意到每一行中,第五列的值与第六列的值匹配。这意味着对于x
和y
的每个可能值,!(x && y)
的值等于!x || !y
,这正是我们试图证明的!
我们可以对德摩根定律的第二部分做同样的事情
x | y | !x | !y | !(x || y) | !x && !y |
---|---|---|---|---|---|
假 | 假 | 真 | 真 | 真 | 真 |
假 | 真 | 真 | 假 | 假 | 假 |
真 | 假 | 假 | 真 | 假 | 假 |
真 | 真 | 假 | 假 | 假 | 假 |
同样,对于x
和y
的每个可能值,我们可以看到!(x || y)
的值等于!x && !y
的值。因此,它们是等价的。
逻辑异或(XOR)运算符在哪里?
逻辑异或是某些语言中提供的一种逻辑运算符,用于测试奇数个条件是否为真
逻辑异或 | ||
---|---|---|
左操作数 | 右操作数 | 结果 |
假 | 假 | 假 |
假 | 真 | 真 |
真 | 假 | 真 |
真 | 真 | 假 |
C++ 不提供显式逻辑异或运算符(operator^
是按位异或,而不是逻辑异或)。与逻辑或或逻辑与不同,逻辑异或不能进行短路求值。因此,用逻辑或和逻辑与运算符来创建一个逻辑异或运算符是具有挑战性的。
然而,当给定bool
操作数时,operator!=
会产生与逻辑异或相同的结果
左操作数 | 右操作数 | 逻辑异或 | operator!= |
---|---|---|---|
假 | 假 | 假 | 假 |
假 | 真 | 真 | 真 |
真 | 假 | 真 | 真 |
真 | 真 | 假 | 假 |
因此,逻辑异或可以按如下方式实现
if (a != b) ... // a XOR b, assuming a and b are bool
这可以扩展到多个操作数,如下所示
if (a != b != c) ... // a XOR b XOR c, assuming a, b, and c are bool
如果操作数(a
、b
和c
)中奇数个求值为true
,则此表达式求值为true
。
如果操作数不是bool
类型,则使用operator!=
实现逻辑异或将无法按预期工作。
致进阶读者
如果你需要一种能够处理非布尔操作数的逻辑异或形式,你可以将操作数静态转换为布尔值
if (static_cast<bool>(a) != static_cast<bool>(b) != static_cast<bool>(c)) ... // a XOR b XOR c, for any type that can be converted to bool
然而,这有点冗长。以下技巧也有效,并且更简洁
if (!!a != !!b != !!c) // a XOR b XOR c, for any type that can be converted to bool
这利用了operator!
(逻辑非运算符)隐式将其操作数转换为bool
的事实。然而,operator!
还会将bool
从true
反转为false
,反之亦然。因此,我们需要应用operator!
两次。第一次进行隐式转换为bool
并反转布尔值。第二次将布尔值反转回其原始值。这种双重反转在多操作数异或具有奇数个操作数的情况下是必要的,否则异或将产生反转的结果。
这两者都不是非常直观,所以如果你使用它们,请好好记录。
替代运算符表示
C++ 中的许多运算符(例如运算符 ||)的名称都只是符号。历史上,并非所有键盘和语言标准都支持输入这些运算符所需的所有符号。因此,C++ 支持一组替代关键字,用于使用单词而不是符号的运算符。例如,你可以使用关键字or
代替||
。
完整列表可以在这里找到。特别值得注意的是以下三个
运算符名称 | 关键字替代名称 |
---|---|
&& | and |
|| | 或 |
! | not |
这意味着以下内容是相同的
std::cout << !a && (b || c);
std::cout << not a and (b or c);
虽然这些替代名称现在看起来更容易理解,但大多数经验丰富的 C++ 开发人员更喜欢使用符号名称而不是关键字名称。因此,我们建议学习和使用符号名称,因为这是你在现有代码中通常会发现的。
小测验时间
问题 #1
计算以下表达式。
注意:在以下答案中,我们通过向您展示获得最终答案的步骤来“解释我们的工作”。这些步骤用=>符号分隔。由于短路规则而被忽略的表达式放在方括号中。例如
(1 < 2 || 3 != 3) =>
(true || [3 != 3]) =>
(true) =>
真
表示我们计算 (1 < 2 || 3 != 3) 得到 (true || [3 != 3]),然后计算它得到 "true"。由于短路,3 != 3 从未执行。
a) (true && true) || false
b) (false && true) || true
c) (false && true) || false || true
d) (5 > 6 || 4 > 3) && (7 > 8)
e) !(7 > 6 || 3 > 4)
问题 #2
在课程6.3 -- 求余和指数中,我们编写了一个判断数字是否为偶数的函数,如下所示
#include <iostream>
bool isEven(int x)
{
// if x % 2 == 0, 2 divides evenly into our number, which means it must be an even number
return (x % 2) == 0;
}
int main()
{
std::cout << "Enter an integer: ";
int x{};
std::cin >> x;
if (isEven(x))
std::cout << x << " is even\n";
else
std::cout << x << " is odd\n";
return 0;
}
使用operator!
而不是operator==
重写此函数。