跳至内容
Topos Uranos

摘要

一个只是把语句一条接一条排列起来的程序,无论数据如何,总是做同样的事;要对数据作出响应,它就需要判断。本课研究 C++ 的判断:if 和 else,它们根据一个条件在两条路径之间选择;else if 链,它们在多条路径之间选择,而且其中条件的顺序决定每个分支处理哪个情形;嵌套判断和悬空 else 的陷阱;以及配合 enum class 的 switch,它们在有名字的离散值之间选择,而编译器会在缺少某个情形或某个情形贯穿到后面的情形时发出警告。统一这一切的概念是分情形分析:正确的判断把输入的集合划分为互斥的情形,并且这些情形完全覆盖它;每个情形都用其内部的一个测试和其每条边界上的一个测试来检查。

学习目标

  1. 编写简单的、串联的和嵌套的判断,使其处理规格说明的所有情形。
  2. 验证 if 与 else if 链的各情形互斥且穷尽,并为每个情形设计一个测试输入。
  3. 对离散值使用配合 enum class 的 switch,并解释缺少某个情形时编译器会警告什么。
  4. 发现悬空 else 以及 switch 中情形之间的贯穿,并加以改正。

if 与 else

判断的形式

从问题到算法一课介绍了构成一切算法的三种形式:顺序、判断和重复。到目前为止,本课程的程序都是顺序结构,只有第 1 单元中提前用过的个别 if 除外;本课用 C++ 写出第二种形式。判断对一个条件求值,也就是对一个 bool 类型的表达式求值,就像关系与逻辑一课中的那些表达式;条件为真时执行一个块,为假时执行另一个块:

C++ 代码
if (条件) {    // 条件为真时执行} else {    // 条件为假时执行}

条件总是写在括号里。else 部分是可选的:没有它时,若条件为假,就什么也不做,程序接着执行判断之后的语句。在两种情况下,两条路径最后都会汇合,因此从外部看,判断是一条只有一个入口和一个出口的语句,正如第 1.1 课所要求的。

例题欧几里得余数

算术运算一课说明,a % b 的符号与被除数相同,因此当 a=−7a = -7、b=2b = 2 时,它等于 −1-1,而永不为负的欧几里得余数等于 1;该课还预告,安全的修正方法是只在余数为负时加上 bb。程序假定 b>0b > 0。

C++ 程序
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    int r{a % b};    if (r < 0) {        r = r + b;    }    std::println("{} 除以 {} 的欧几里得余数:{}", a, b, r);}
程序输入
-7 2
输出
-7 除以 2 的欧几里得余数:1

变量 r 不再是常量,因为判断可能改变它。下图跟踪两条可能的路径。

证明

  1. 执行到达判断时,r 已经算好,它首先做的是对条件 r < 0 求值:菱形是程序中路径分开的唯一位置。

  2. 如果条件为真,例如输入 −7-7 和 2 时,就执行 if 的块:r 从 −1-1 变为 1。

  3. 如果条件为假,例如输入 7 和 2 时,整个块被跳过,r 保持原值。

  4. 两条路径在同一点汇合,从那里起程序在两种情况下以同样的方式继续:写出结果的语句总会执行。

有了 else,判断就在两个互相排斥的动作之间选择。关系与逻辑一课中的闰年条件产生一个 bool;判断把它变成程序的行为。

C++ 程序
#include <iostream>#include <print>​int main(){    int year{};    std::cin >> year;    const bool isLeap{(year % 4 == 0 && year % 100 != 0) || year % 400 == 0};    if (isLeap) {        std::println("{} 年是闰年:二月有 29 天。", year);    } else {        std::println("{} 年不是闰年:二月有 28 天。", year);    }}
程序输入
1900
输出
1900 年不是闰年:二月有 28 天。

给条件起个名字,例如 isLeap,就把判断什么与判断之后做什么分开了。

始终使用花括号

C++ 的语法允许在块只有一条语句时省略花括号:if (r < 0) r = r + b; 是合法的。然而,没有花括号时,判断只管辖紧随其后的那一条语句,而读者以为被管辖的部分取决于缩进,编译器却忽略缩进。若是如此,以后的任何修改(添加第二条语句,把第一条注释掉)都可能改变含义,而文本上看不出来。由此得出本课程的规则:if 或 else 的块始终加花括号,即使其中只有一条语句。

所有错误中最短的一个证实了这条规则。括号后面的一个分号是一条空语句,判断管辖的就是这条空语句;其后的块不再属于 if,总会执行。

C++ 程序
#include <print>​int main(){    const int stock{12};    if (stock == 0);    {        std::println("商品已售罄");    }    std::println("可用数量:{}", stock);}
编译器警告
main.cpp: In function ‘int main()’:main.cpp:6:20: warning: suggest braces around empty body in an ‘if’ statement [-Wempty-body]    6 |     if (stock == 0);      |                    ^main.cpp:6:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]    6 |     if (stock == 0);      |     ^~main.cpp:7:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’    7 |     {      |     ^
输出
商品已售罄可用数量:12

程序宣布商品已售罄,尽管还剩 12 件。GCC 给出两条警告:-Wempty-body 建议在空的语句体周围加上花括号,-Wmisleading-indentation 指出这个块的缩进看起来好像受 if 管辖。另一个经典错误,用 if (x = 0) 代替 if (x == 0),在编译、运行与配置一课中已经见过:赋值的值为零,零被转换为 false,而 -Wparentheses 会发出警告。在这两种情况下程序都能编译;没有这些警告,错误就会被忽视。

花括号还有一个后果:在 if 的块内声明的变量只在那里存在。如果在某个分支中算出的值要在判断之后使用,变量就在判断之前声明,就像余数例子中的 r;作用域将在参数与作用域一课中研究。

条件运算符

当判断的两个分支只是选择一个值时,C++ 提供了一种更简短的形式:条件 ? a : b 在条件为真时等于 a,为假时等于 b,并且只对被选中的那个元素求值。与作为语句的 if 不同,条件运算符是一个表达式:它有值和类型,可以出现在任何表达式能出现的地方,例如常量的初始化中,而 if 若不放弃 const 就做不到这一点。

C++ 程序
#include <iostream>#include <print>​int main(){    int a{};    int b{};    std::cin >> a >> b;    const int larger{a > b ? a : b};    std::println("较大者:{}", larger);    std::println("{} {}", b, b == 1 ? "个单位(仅此一个)" : "个单位");    const auto mixed{a > b ? a : 0.5};    std::println("公共类型:{:.1f}", mixed);    std::println("不加括号:{}", 10 + a > b ? 1 : 0);    std::println("加括号:{}", 10 + (a > b ? 1 : 0));}
程序输入
7 3
输出
较大者:73 个单位公共类型:7.0不加括号:1加括号:11

前两行展示了使代码更清晰的用法:选出较大者,以及按数目选择措辞,而不必在两个分支中重复整条语句。第三行表明,不论选中哪个分支,表达式都只有一个类型:如果两个结果的类型不同,就用算术运算一课的常用算术转换把它们转换为一个公共类型;这里 int 和 double 得到 double,选中的 7 被存为 7.0。最后两行展示优先级:条件运算符属于优先级最低的运算符之一,因此不加括号时条件是 10 + a > b,值为 1,而不是 11。当条件运算符是更大表达式的一部分时,要加括号。

一旦要求这个运算符做的不只是选择一个值,它就会使代码变得晦涩。嵌套时,例如 a > b ? (a > c ? a : c) : (b > c ? b : c),它迫使读者在脑中重建一个嵌套判断,却没有使之易读的花括号;为了副作用而使用时,例如 ok ? std::println("是") : std::println("否"),它就是一个伪装的 if。本课程的规则是只用它在两个简短的值之间选择,不嵌套;其余一切都用 if 来写。