3.5 逻辑运算符
可以暂时跳过
在学习语句之前,逻辑运算符没什么用处。
逻辑运算符作用于标量类型的操作数,对其求值的结果是 int 类型的 1 或 0。其结果通常用在语句(如 if, while)的条件部分。
为了符合对于逻辑的普遍认识,下文(包括下面的数个章节)用真指代某标量对象不等于 0,用假指代某标量对象等于 0。
1. 逻辑与 &&
逻辑与 (Logical AND) 是一个二元运算符。当且仅当两个操作数都为真(即都为非零值)时,其运算结果才为真 (1)。只要有一个操作数为假(即为 0),结果就为假 (0)。
| 操作数 1 (逻辑) | 操作数 2 (逻辑) | 结果 |
|---|---|---|
1(真) | 1(真) | 1 |
1(真) | 0(假) | 0 |
0(假) | 1(真) | 0 |
0(假) | 0(假) | 0 |
示例:
int a = 5;
int b = 0;
int result = (a > 1) && (b == 0); // (5 > 1) 为真,(0 == 0) 为真
// 真 && 真 -> 结果为 12
3
4
5
短路求值 (Short-circuit Evaluation)
逻辑与 && 具有短路特性。如果第一个操作数求值为假(即 0),那么整个表达式的结果就已经确定为假,此时第二个操作数将不会被求值。这对于避免不必要或危险的计算非常重要。
在 && 运算符的左操作数的求值后有一个序列点。
2. 逻辑或 ||
逻辑或 (Logical OR) 是一个二元运算符。只要两个操作数中至少有一个为真(即为非零值),其运算结果就为真 (1)。只有当两个操作数都为假(都为 0)时,结果才为假 (0)。
| 操作数 1 (逻辑) | 操作数 2 (逻辑) | 结果 |
|---|---|---|
| 真 | 真 | 1 |
| 真 | 假 | 1 |
| 假 | 真 | 1 |
| 假 | 假 | 0 |
示例:
int score = 85;
// 检查分数是否无效 (小于 0 或 大于 100)
int is_invalid = (score < 0) || (score > 100);
// (85 < 0) 为假,(85 > 100) 为假
// 假 || 假 -> 结果为 02
3
4
5
短路求值
逻辑或 || 同样具有短路特性。如果第一个操作数求值为真(即非零),那么整个表达式的结果就已经确定为真,此时第二个操作数将不会被求值。
在 || 运算符的左操作数的求值后有一个序列点。
3. 逻辑非 !
逻辑非 (Logical NOT) 是一个一元运算符,它会反转其操作数的逻辑状态。
- 如果操作数为假(即
0),结果为真 (1)。 - 如果操作数为真(即非零值),结果为假 (
0)。
所以,逻辑非 !a 与 (a == 0) 等价。
| 操作数 (逻辑) | 结果 |
|---|---|
| 真 | 0 |
| 假 | 1 |
示例:
int has_error = 0;
if (!has_error) { // ! 假 -> 结果为 1 (真)
printf("操作成功,没有错误。\n");
}2
3
4
5
这段代码执行到该分支时会输出:
操作成功,没有错误。
4. 习题
4.1 巩固
写出下面程序的输出,并说明每个结果为什么只能是 0 或 1。
#include <stdio.h>
int main(void) {
int a = 5, b = 0, c = -3;
printf("%d %d %d\n", a && b, a || b, !a);
printf("%d %d %d\n", !!a, !c, !(a - 5));
printf("%d %d\n", 2 && 1, 2 & 1);
printf("%d %d\n", 1 || 0 && 0, (1 || 0) && 0);
return 0;
}2
3
4
5
6
7
8
9
10
11
编译时加上 -Wall,抄下最后一行触发的警告。然后回答:
a && b的两个操作数是5和0,结果为什么不是5或0之一?结果的类型由什么决定?2 && 1与2 & 1结果不同,说明逻辑运算与位运算在哪一点上根本不同?1 || 0 && 0与(1 || 0) && 0的结果说明&&与||的优先级关系如何?!c中c为-3,为什么结果是0而不是3?
下面的程序用计数器观察短路求值:
#include <stdio.h>
static int calls;
int check(int value) {
calls++;
return value;
}
int main(void) {
calls = 0;
int r1 = check(0) && check(1);
printf("r1 = %d, calls = %d\n", r1, calls);
calls = 0;
int r2 = check(1) || check(0);
printf("r2 = %d, calls = %d\n", r2, calls);
calls = 0;
int r3 = check(1) && check(0);
printf("r3 = %d, calls = %d\n", r3, calls);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 先写出三行输出,再编译运行核对。
- 把三处
&&、||分别换成&、|,其余不动,重新运行。三行的calls各变成多少?说明两种运算符在求值上的区别。 - 把
check换成会打印一行的版本(在函数体里加printf("调用 check(%d)\n", value);),用输出顺序说明&&与||的左操作数求值之后确实存在一个序列点。 - 把
r1那一行改成int r1 = check(0) & check(1);,check不变,说明为什么这时调用次数变了,而结果值不变。
4.2 编程
写四个判断函数,要求每个函数体只有一条 return 语句,且用逻辑运算符把关系表达式组合起来:
int mdr_in_range(int value, int lo, int hi);
int mdr_ratio_gt(int a, int b, int limit);
int mdr_any_negative(int a, int b, int c);
int mdr_all_in_range(int a, int b, int c, int lo, int hi);2
3
4
mdr_in_range判断闭区间[lo, hi],两端都算在内。mdr_ratio_gt判断a / b > limit,但b为 0 时直接返回0,不能执行除法。注意两个条件的先后顺序。mdr_any_negative判断三个数中是否至少有一个为负。mdr_all_in_range判断三个数是否都落在[lo, hi]内,内部调用mdr_in_range。
验收:用下面的 main 运行,输出必须逐行相同。
int main(void) {
printf("%d %d %d\n", mdr_in_range(50, 0, 100), mdr_in_range(-5, 0, 100),
mdr_in_range(100, 0, 100));
printf("%d %d %d\n", mdr_ratio_gt(100, 4, 30), mdr_ratio_gt(100, 0, 5),
mdr_ratio_gt(-100, 3, -40));
printf("%d %d\n", mdr_any_negative(1, -2, 3), mdr_any_negative(1, 2, 3));
printf("%d %d\n", mdr_all_in_range(1, 5, 10, 0, 10),
mdr_all_in_range(1, 11, 10, 0, 10));
return 0;
}2
3
4
5
6
7
8
9
10
1 0 1
0 0 1
1 0
1 02
3
4
第二行第二个调用验证了保护条件有效。把 mdr_ratio_gt 里的 && 换成 &,重新运行同一个 main:程序会怎样结束?编译时有没有警告?据此说明为什么需要短路求值,而不只是「少算一次」。
4.3 综合与挑战
(1) [3.3] 下面的四个函数都写错了,运行输出为 1 1 1、1 1 1、0 0、0 1 1:
#include <stdio.h>
int is_teen(int age) {
return age >= 13 || age <= 19;
}
int is_weekend(int weekday) {
return weekday == 6 || 7;
}
int is_adult(int age) {
return !age >= 18;
}
int has_flag(unsigned flags, unsigned mask) {
return flags & mask == mask;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
逐个说明每个函数实际在算什么,改成正确写法,使四个函数的输出分别变为 0 1 0、0 1 1、1 0、1 1 0(测试值见下)。对每个函数抄下 gcc -std=c17 -Wall -Wextra 的诊断:哪些错误编译器能提醒,哪个(is_weekend)一条警告也没有?
printf("%d %d %d\n", is_teen(5), is_teen(15), is_teen(40));
printf("%d %d %d\n", is_weekend(1), is_weekend(6), is_weekend(7));
printf("%d %d\n", is_adult(20), is_adult(10));
printf("%d %d %d\n", has_flag(0x10u, 0x10u), has_flag(0x11u, 0x10u),
has_flag(0x01u, 0x10u));2
3
4
5
(2) [4.2] 用德摩根律改写下面两个判断,把否定号推到最内层,并保证改写前后对所有输入结果相同:
if (!(age >= 18 && age <= 60)) {
return 1;
}
return !(score >= 60 || years >= 2);2
3
4
5
先列出改写前后的真值表(age、score、years 各取两组代表性取值),再编译运行验证。最后回答:为什么改写后可以去掉 !,而原来的 !age >= 18 那种写法得不到想要的结果?
(3) [5.3] 下面两行的结果不同:
int i = 0;
int a = i++ && i++;
printf("%d %d\n", a, i);
int j = 0;
int b = j++ + j++;
printf("%d %d\n", b, j);2
3
4
5
6
7
写出两行输出。第二行为什么在 -Wall 下有警告、第一行没有?结合本节对 && 左操作数之后那个序列点的说明解释。再用下面两行验证左操作数的副作用:
int k = -2;
int r = (k += 2) || (k += 3);2
k 的初值换成正数、0,r 和 k 各变成多少?据此说明短路发生在什么条件下,以及左操作数自身的副作用为什么一定会完成。
(4) [5.3] 写 int mdr_count_true(int a, int b, int c),返回三个参数中非零值的个数(0 到 3)。要求不用 if、while、? :,也不使用 && 和 ||,整个函数只有一条 return 语句。验收:
mdr_count_true(0, 0, 0) -> 0
mdr_count_true(1, 0, 0) -> 1
mdr_count_true(1, -2, 0) -> 2
mdr_count_true(1, -2, 3) -> 32
3
4
再回答两个问题。把三个参数写成 a != 0 而不是 !!a,结果是否相同?如果把整个表达式写成 a + b + c(不加 !!),四个验收用例和 (-2, 0, 0) 各得到什么,为什么需要先把每个操作数归一化到 0 或 1?