43、短路求值:&&与||的短路特性及其依赖

短路求值,这名字听着挺唬人。说白了就是:C语言在计算逻辑表达式时,一旦能确定整个表达式的真假,就立马停手,后面的代码不看了

我刚开始学C的时候,觉得这特性没啥大不了的。直到有一次,我在一个嵌入式项目里写了个判断语句,程序跑起来时好时坏,查了两天才发现——嗯,就是短路求值在“捣鬼”。从那以后,我对这个特性就格外上心了。

什么是短路求值?

先看两个运算符:

  • &&(逻辑与):左边为假,右边就不算了。因为假与任何值都是假。
  • ||(逻辑或):左边为真,右边就不算了。因为真或任何值都是真。

你想想看,这其实很符合人的直觉。比如你判断“门没锁并且屋里有人”,如果门锁着,你还会去看屋里有没有人吗?不会。C语言也是这么干的。

核心规则:

  • expr1 && expr2:如果 expr1 为假,expr2 不执行
  • expr1 || expr2:如果 expr1 为真,expr2 不执行

一个经典的例子

int a = 0;
int b = 5;

if (a != 0 && b / a > 2) {
    // 这段代码永远不会执行
    printf("条件成立\n");
}

为什么会这样?因为 a != 0 是假,所以 b / a 根本不会计算。你想想看,如果C语言不短路,这里就要除零了——程序直接崩溃。短路求值在这里救了你一命。

我个人习惯,在写这种可能除零的代码时,一定会把判零条件写在左边。这不是什么高深技巧,纯粹是保命用的。

短路求值的常见陷阱

我在项目中遇到过最典型的坑,就是有人把带副作用的函数调用放在了逻辑表达式里。

int count = 0;

int increment() {
    return ++count;
}

if (increment() && increment()) {
    // 这里 count 是多少?
    printf("count = %d\n", count);
}

猜猜看?count 是 2。因为第一次调用返回 1(真),所以第二次调用也会执行。但如果改成这样:

if (increment() || increment()) {
    // 这里 count 是多少?
    printf("count = %d\n", count);
}

答案是 1。第一次调用返回 1(真),短路了,第二次 increment() 根本没执行。

警告:永远不要在逻辑表达式中使用带副作用的函数调用。除非你非常清楚自己在做什么,并且这个副作用是你刻意设计的。否则,代码的可读性和可维护性都会大打折扣。

短路求值的实际应用

短路求值不光是陷阱,它也是很有用的工具。我经常用它来写“安全访问”的代码。

// 安全地访问指针
if (ptr != NULL && ptr->value > 0) {
    // 只有 ptr 非空时,才会访问 ptr->value
    printf("value = %d\n", ptr->value);
}

// 或者用 || 实现默认值
int result = (get_value() || 0);  // 如果 get_value 返回 0,就用 0 兜底

这种写法在嵌入式开发里特别常见。比如你要读一个传感器的值,但传感器可能还没初始化好:

if (sensor_ready() && sensor_read() > THRESHOLD) {
    // 传感器就绪了,才去读数据
    trigger_alarm();
}

你看,一行代码就完成了“先检查,再操作”的逻辑。简洁又安全。

短路求值与运算符优先级

这里有个容易混淆的点。短路求值是求值顺序的规则,而运算符优先级是结合规则。两者不是一回事。

int a = 0, b = 1, c = 2;

// 优先级:&& 高于 ||
if (a || b && c) {
    // 等价于 a || (b && c)
    // 因为 a 为假,所以计算 b && c
    // b 为真,c 为真,整体为真
}

优先级决定了表达式怎么“分组”,但短路求值决定了每个子表达式“算不算”。

表达式 求值顺序 结果
0 && func() 先算 0,短路,func() 不执行 0
1 || func() 先算 1,短路,func() 不执行 1
0 || func() 先算 0,不短路,继续算 func() 取决于 func()
1 && func() 先算 1,不短路,继续算 func() 取决于 func()

我曾经踩过的坑

有一次,我在一个通信协议栈里写了一段代码:

if (packet_received() && parse_packet() && validate_checksum()) {
    process_packet();
}

看起来没问题对吧?但问题是,packet_received() 返回真之后,parse_packet() 会修改全局状态。而 validate_checksum() 依赖这个状态。如果 parse_packet() 返回假,validate_checksum() 就不会执行,但全局状态已经被改了——这就导致后续的逻辑全乱了。

从那以后,我给自己定了个规矩:如果逻辑表达式里的函数有副作用,就拆开来写

// 拆开写,清晰可控
bool received = packet_received();
if (received) {
    bool parsed = parse_packet();
    if (parsed) {
        if (validate_checksum()) {
            process_packet();
        }
    }
}

代码长了点,但每个步骤的副作用都清清楚楚,不会因为短路而“漏掉”某些操作。

知识体系总览

下面这张图,把短路求值的核心逻辑串起来了:

短路求值知识体系 短路求值 && 逻辑与 || 逻辑或 左假 → 右不执行 左真 → 右不执行 安全指针访问 (ptr && ptr->val) 默认值兜底 (func() || 0) ⚠ 陷阱:带副作用的函数调用

总结几条实用建议

  • 判零、判空写在左边:这是最安全的写法,能避免除零、空指针解引用等问题。
  • 别在逻辑表达式里放带副作用的函数:除非你明确知道短路不会影响你的逻辑。
  • 复杂条件拆开写:如果条件里涉及多个函数调用,拆成 if-else 嵌套,可读性更好,也更容易调试。
  • 记住:短路求值是C标准规定的,不是编译器优化——所有C编译器都必须遵守这个规则。

小技巧:如果你不确定某个表达式会不会短路,可以在关键位置加个打印语句测试一下。比如 if (a && (printf("here\n"), b)),看看 "here" 有没有打印出来。当然,这只是调试手段,正式代码里别这么写。

短路求值这个特性,用好了是利器,用不好就是坑。我个人觉得,理解它的关键在于记住一句话:C语言很懒,能少算就少算。你顺着这个思路去想,很多问题就迎刃而解了。

专注资料整理