Skip to content

5.7 - 逻辑运算符

Key Takeaway
  • 逻辑非的优先级非常高,如果使用它的目的是对整个表达式的结果取反,则要对整个表达式加括号
  • 区分逻辑与(&&)按位与(&)逻辑非(||)按位非(|)
  • 短路求值可能会导致逻辑与逻辑非不对第二个表达式求值。所以请避免将具有副作用的运算符和逻辑与或逻辑非一起使用。
  • 逻辑或和逻辑与运算符的两个操作数,求值顺序是固定的,因为标准里对其进行了说明,左边的操作数总是先求值。
  • 只有 C++ 自带的这些逻辑运算符会执行短路求值,如果你自己重载了这些操作符以便将其应用于你自己定义的类型,这些重载后的运算符是不具备短路求值功能的。
  • 逻辑与的优先级高于逻辑或的优先级,如果在一个表达式里同时使用逻辑与和逻辑或,最好使用括号为其明确指定优先级。
  • 德摩根定律
    • !(x && y) 等价于 !x || !y
    • !(x || y) 等价于 !x && !y
  • C++ 没有异或运算符。可以使用不等号(!=)来模拟逻辑异或(相同为真)
  • C++ 中的很多操作符 (例如 operator ||) 都可以使用名字来代替符号吗,因为并不是所有的键盘都支持输入符号。

尽管条件(比较)运算符可以被用来测试一个特定的表达式是否为真,但它们一次只能对一个条件进行测试。很多时候,我们需要知道多个条件是否同时为真。例如,为了确定彩票是否中奖,我们必须将购买的每一个数字和中奖号码的各个数字逐一比较。对于有6个数的彩票,这就需要6次比较,只有当它们的结果全部都是真的时候,才能看做中奖。还有一些情况,我们需要知道多个条件中是否有一个为真。例如,如果我今天生病了,或者太累了,又或者中了彩票,这三个条件只要有一条为真我就会考虑翘班。

逻辑运算符就为我们提供了这样的能力。

C++ 提供了三种逻辑运算符:

运算符 符号 形式 操作
逻辑非 NOT ! !x 如果 x 为真则为 false,如果 x 为假则为 true
逻辑与 AND && x && y 如果 x 和 y 都是真,则为 true,否则为 false
逻辑或 OR || x || y 如果 x 或 y 其中一个位真,则为真,否则为假

逻辑非(NOT)

在 4.9 - 布尔值 一节课中你已经遇到逻辑非这个一元运算符:

操作符 结果
true false
false true

如果逻辑非的操作数求值为 true,则逻辑非求值为 false。如果逻辑非的操作数求值为 false,逻辑非的求值为 true。换句话说,逻辑非可以将布尔值反转,真变成假,假变成真。

逻辑非经常被用在条件表达式中:

1
2
3
4
5
bool tooLarge { x > 100 }; // tooLarge is true if x > 100
if (!tooLarge)
    // do something with x
else
    // print an error

有件事情需要注意,那就逻辑非的优先级非常高。新手程序员经常会犯这样的错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#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;
}

打印结果如下:

1
5 is greater than 7

但是 x 并不比 7 大,这结果是怎么得到的?这是因为逻辑非的优先级比大于号的优先级高,因此 ! x > y 实际上相当于(!x) > y。因为 x 是 5,!x 求值结果为 0,而  0 > y 是假,所以执行的是 else 的语句。

正确的做法应该像下面这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#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) 并不需要括号,因为这时不会受到优先级的影响。

逻辑或(OR)

逻辑或运算符被用来测试两个条件中是否有为真的。如果左操作数或右操作数为 true,或者两个都是 true,则逻辑或表达式会返回 true,否则返回 false

左操作数 右操作数 结果
false false false
false true true
true false true
true true true

例如,考虑下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#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 语句就会执行。

你可以将多个逻辑或表达式连接起来:

1
2
if (value == 0 || value == 1 || value == 2 || value == 3)
     std::cout << "You picked 0, 1, 2, or 3\n";

新手程序员经常会把逻辑或运算符 (||) 和按位或运算符搞混 (|) (稍后会介绍)。 即使它们的名字中都有一个或,其功能确是不同的。将它们混为一谈会带来错误的结果。

逻辑与(AND)

逻辑与运算符被用作测试两个操作数是否均为真。如果均为真,则逻辑与会返回 true,否则返回值为false

左操作数 右操作数 结果
false false false
false true false
true false false
true true true

例如,我们想要判断 x 是否在 10 到 20 之间:我们必须知道 x 是否大于 10并且是否小于 20:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#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) 右侧的条件 (value < 20) 是否都是 true。如果都是真,则逻辑与运算符的求值结果为 true,执行 if 语句。如果左右两个条件都是假,则逻辑与运算符的求值结果为 false,执行 else 语句。

和逻辑或运算符一样,你可以把多个逻辑与运算符连接起来:

1
2
3
4
if (value > 10 && value < 20 && value != 16)
    // do something
else
    // do something else

如果上述所有条件都是真,则执行 if 语句。如果任何一个条件为假,则执行 else 语句。

新手程序员不仅容易把逻辑或按位或搞混,也会把逻辑与 (&&) 和按位与 (&)搞混。

短路求值

为了让逻辑与返回 true,两个操作数都需要求值为真。如果第一个操作数求值为假,不论第二个操作数求值结果如何,逻辑与运算符一定求值为false。这种情况下,逻辑与运算符会立即返回false,而不会再对第二个操作数求值。这个方法称为短路求值,这么做可以优化程序性能。

类似地,如果逻辑与运算符的第一个操作数求值结果为真,则它会直接返回true,而不必对第二个操作数进行求值。

短路求值还向我们展示了,为什么有副作用的运算符,不应该放到复合表达式中。考虑下面的代码:

1
2
if (x == 1 && ++y == 2)
    // do something

如果 x 不等于 1,那么整个条件一定会返回 false。所以 ++y 根本不会被求值。因此,只有当 x 等于 1 时,y 才会递增,也许这并不是你需要的!

注意

短路求值可能会导致逻辑与逻辑非不对第二个表达式求值。所以请避免将具有副作用的运算符和逻辑与或逻辑非一起使用。

关键信息

逻辑或和逻辑与运算符的两个操作数,求值顺序是固定的,因为标准里对其进行了说明,左边的操作数总是先求值。(参考:5-1-Operator-precedence-and-associativity#表达式求值顺序和函数参数处理顺序大多是未指明的

扩展阅读

只有 C++ 自带的这些逻辑运算符会执行短路求值,如果你自己重载了这些操作符以便将其应用于你自己定义的类型,这些重载后的运算符是不具备短路求值功能的。

混合 AND 和 OR

在一个表达式里面混合使用逻辑与和逻辑或运算符通常是无法避免的,但是这样的用法也存在一定的危险。

很多程序员会假定逻辑与和逻辑或具有相同的优先级(或忘记了它们的优先级是不同的),就像加/减或者乘/除那样。但是,逻辑与的优先级其实比逻辑或要高,因此逻辑与会先于逻辑或求值(除非有括号修改其优先级)。

新手程序员通常会写出这样的表达式:value1 || value2 && value3。因为逻辑与的优先级更高,所以上述表达式等价于 value1 || (value2 && value3) 而不是 (value1 || value2) && value3。如果这正是你想要的,那就好了。但是如果你假设上面的表达式是按照从左向右的顺序求值,那得到的结果肯定出你所料!

如果在一个表达式里同时使用逻辑与和逻辑或,最好使用括号为其明确指定优先级。这样不仅可以避免由优先级引起的顺序,还能够增强可读性并清晰地表明你的目的。例如,value1 && value2 || value3 && value4 最好改写为 (value1 && value2) || (value3 && value4).

最佳实践

如果在一个表达式里同时使用逻辑与和逻辑或,最好使用括号为其明确指定优先级。

德摩根定律(De Morgan‘s law)

很多程序员会认为 !(x && y) 等价于 !x && !y。不幸的是,逻辑非运算符并不能像这样进行分配。

根据德摩根定律 ,逻辑非的分配律应该是下面这种方式:

  • !(x && y) 等价于 !x || !y
  • !(x || y) 等价于 !x && !y

换言之,当你分配逻辑非运算符时,必须将逻辑与替换为逻辑或,反之亦然。

有时候,我们可以通过德摩根定律对复杂的表达式进行简化。

扩展阅读

我们可以证明德摩根定律的第一部分是正确的,即对于任何可能的xy!(x && y) 等于 !x || !y。为此,我们需要使用一个称为真值表的数学概念。

x y !x !y !(x && y) !x || !y
false false true true true true
false true true false true true
true false false true true true
true true false false false false

在表中,第一列和第二列分别表示 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
false false true true true true
false true true false false false
true false false true false false
true true false false false false

同样的,对于任意 x 和 y,我们可以看到 !(x || y) 等于 !x && !y

逻辑异或运算符在哪里?

有些语言提供了逻辑异或(XOR)运算符,用来测试是否有奇数个条件为真

左操作数 右操作数 结果
false false false
false true true
true false true
true true false

C++ 并没有提供逻辑异或运算符。和逻辑或或者逻辑与不同,逻辑异或并没有短路求值特性。因此,使用逻辑或或者逻辑与来构建一个逻辑异或是很困难的。但是,你可以使用不等号(!=)来模拟逻辑异或:

1
if (a != b) ... // a XOR b, 假设 a 和 b 都是布尔值

也可以对多个操作数来使用:

1
if (a != b != c != d) ... // a XOR b XOR c XOR d, 假设 a, b, c, 和 d 都是布尔值

注意,上面的方法只有当操作数都是布尔类型时才有效 (不能是整型)。如果你想将逻辑异或应用于非布尔类型的操作数,你可以使用 static_cast 将它转换为布尔类型。

1
if (static_cast<bool>(a) != static_cast<bool>(b) != static_cast<bool>(c) != static_cast<bool>(d)) ... // a XOR b XOR c XOR d, for any type that can be converted to bool

运算符的其他表示形式

C++ 中的很多操作符 (例如 operator ||) 都有可以代替符号表示法的关键字。出于历史原因,并不是所有的键盘或者标准支持输入这些运算符的符号。因此C++提供了一组关键字用于代替符号。例如,|| 也可以用or来替代。

完整的列表可以参考这里,尤其可以注意一下下面三种:

运算符 关键字替代名
&& and
|| or
! not

因此下面两行代码是等价的:

1
2
std::cout << !a && (b || c);
std::cout << not a and (b or c);

尽管这些关键字看起来可读性更好,但是大多数有经验的 C++ 程序员还是喜欢使用符号而不是关键字。因此,我们还是建议你学习这些运算符的符号表示法,这也是代码中最常见的形式。