跳过正文

丙加·第7章·If I Can Stop One Loop By Breaking(条件、循环与控制)

·1211 字·6 分钟·
目录

本章将讲解主要的控制、循环语句。并且对 C++独有的特性进行稍深入的探讨

7.1 使一循环止于断处
#

If I can stop one loop by breaking
I shall not live in repeats;
If I can mark one cycle the edging,
Or show one boundary,
Or fix one bug that’s lurking
In endless code again,
I shall not live in repeats.

— Emily Dickinson (Adapted by 命令提示符)

诗人 Emily Dickinson 写下这首诗,是希望用微小的善行让世界变得更好。而我们则用 break 语句来阻止循环陷入无尽的虚空,这何尝不是一种“微小的善行”(至少对你的 CPU 而言)呢?

本章,我们将学习如何用 if 进行判断,构建 whilefor 循环,用 breakcontinue 来干预循环的流程,让我们成为自己代码世界的“诗人”,写出优雅而高效的篇章。

7.2 若我不曾见过“条件”(if-else/switch-case)
#

所谓条件控制,就是 特定条件下对程序执行流程或生成模型输出进行控制的技术,人话就是分类讨论:如果……那么……(否则……)——计算机喜欢这样清晰明了的语句。因此,我们要养成将大问题拆解成简单的小问题的习惯。

7.2.1 If 语句
#

if-else 的语法如下:

if(条件) {
    你要干的事
} else if(条件) {
    你要干的事
} else if(条件) {
    可以一直套……
} else {
    你要干的事
}

格式上没有硬性要求,你要是觉得我的风格不好看可以尽管调整。if 是必须的 总得有个头啊!(康熙音)elseelse if 是可选的(也就是没有需要就可以不要),但是顺序必须是 if -> else if -> else

其实如果 ifelse 同理)后面只有一个语句(分号结尾),你也可以这样写:

if(条件)
    语句;
else
     ...

因为 if 语句默认控制紧随其后的第一条语句(或者一个用花括号括起来的代码块),所以其实所谓的 else if 其实可以理解为 else { if(...) { ... } }。但是除了 else if,这种写法弊端很多,考虑以下代码:

if(1+1 == 3) 
    a();
    b();
    c()
会让人误以为 `a`、`b`、`c` 都不会执行,但是其实只有 `a` 在 `if` 的支配下(或者叫“作用域”)。所以为了你少因为调试这种低级错误掉头发,即使是一句话,也最好用花括号括起来。

至于其他的应该不必过多解释,有英文基础的读者一眼便能明白。我们的核心放在如何表达好“条件”上。

7.2.1.1 If I Can-If I Can…(条件表达式)
#

认识以下几个关键的运算符,你就能构建出复杂的条件判断。下表按 优先级从高到低 排列(同一行优先级相同):

优先级 类别 运算符 名称与含义 示例 结合性
括号 () 括号:提升内部表达式优先级 (a + b) * c 从左到右
单目运算符 ! 逻辑非:取反 !is_ready 从右到左
+, - 正负号:一元加、减 -number 从右到左
乘除模 *, /, % 乘、除、取模 a * b, a % 3 从左到右
加减 +, - 加、减 a + b, a - 5 从左到右
关系运算符 <, <=, >, >= 小于、小于等于、大于、大于等于 age >= 18 从左到右
相等运算符 ==, != 等于、不等于 password == input_value 从左到右
逻辑与 && 逻辑与:两边都为真,结果才为真 age >= 18 && money > 0 从左到右
逻辑或 ` ` 逻辑或:两边有一边为真,结果就为真
条件运算符 ? : 三目运算符:条件 ? 表达式 1 : 表达式 2
相当于 if(条件) 表达式1 else 表达式2,善用会缩减你的代码
fee = (age < 18) ? 50 : 100; 从右到左
最低 赋值运算符 =, +=, -=, *=, /=, %=
(以及 &&=, |= 等)
赋值、复合赋值
比如 x += 1; 就是 x = x + 1; 的意思
绝对不要尝试 x += x++; 这种语义模糊的句子
甚至是 x += x++ + ++x; 这类未定义行为!

温馨提示 Warm Tips

  1. ===:这是最最经典错误!if (a = 5) 是赋值,表达式结果为 5(非零,所以永远是真),条件永远成立!if (a == 5) 才是比较。时刻警惕!

  2. 短路求值&&|| 有短路特性。对于 a && b,如果 a 为假,就不会再去计算 b。对于 a || b,如果 a 为真,也不会再去计算 b。这在 b 是一个函数调用或带有副作用(如 i++)时尤为重要。
    举例子(其实第三章课后题有)

    int x = 0; 
    bool y = ((x++ == 3) and (x++ == 4));
    

    x1yfalse,原因很简单,从左往右看,(x++ == 3)false,已经可以确定整个表达式的值,所以 (x++ == 4) 不会执行,x 只增加一次。

    很搞吧?所以实际使用千万别自找苦吃。

  3. 结合性:它决定了优先级相同时的运算方向。a = b = c 从右向左,等价于 a = (b = c)a + b + c 从左向右,等价于 (a + b) + c。这个基本上和数学的一样,不必在意。

  4. 万能括号记不住优先级没关系! 当你无法确定运算顺序时,多用括号 () 来明确你的意图,这是最高效、最安全的方法,也能让代码更清晰。比较一下:

    if ((a > 5) && ((b < 10) || (c == 15))) {
        // ...
    }
    
    if (a > 5 && (b < 10 || c == 15)) {
        // ...
    }
    

    明显前者更好懂。

    碎碎念:C ? X : Y 运算符

    这是三元运算符的独苗苗。是 if - else 的缩写形式。比如:

    abs_x = (x>=0) ? x : -x;
    

    相等于:

    if (x > 0) {
        abs_x = x;
    } else {
        abs_x = -x;
    }
    

7.2.2 switch 语句
#

当需要大量相等的判断的时候,这样写会很长:

if (choice == 1) {
    ...
} else if (choice == 2){
    ...
} else if (choice == 3) {
    ...
}
......
else {
    ...
}

天空一声巨响,switch-case 闪亮登场,这正是它大展拳脚的时机:

switch(choice) {
    case 1:
        ... // 一
        break;
    case 2:
        ... // 二
        break;
    case 3:
        ... // 三
        break;
    ......
    default:
        ... // 最后
        break;
}

case,顾名思义,就是“情况”。上面的代码意思是,如果 choice 是 1,就进入一语句然后退出;choice 是 2,就进入二语句然后退出;……;如果都不是,就进入最后的语句然后退出。

注意这里“退出”的操作是 break 完成的。如果忘记写 break 会发生什么呢?看看下面的代码:

switch(choice) {
    case 1:
        printf("一\n");
    case 2:
        printf("二\n");
        break;
    case 3:
        printf("三\n");
        break;
    default:
        printf("其他\n");
        break;
}

choice 为 1 时,程序会输出:

这是因为没有 break 语句,程序在执行完 case 1 后会继续执行 case 2 的代码,直到遇到 breakswitch 语句结束。这就是所谓的 " case 穿透”。

有时候,我们可以故意利用这个特性,比如多个 case 情况需要执行相同代码时:

switch(choice) {
    case 1:
    case 2:
    case 3:
        printf("选项是1、2或3\n");
        break;
    case 4:
        printf("选项是4\n");
        break;
    default:
        printf("其他选项\n");
        break;
}

这样,当 choice 为 1、2 或 3 时,都会执行相同的代码块,提高了代码的复用性和可读性。

7.3 继续沉醉 自我迂回(循环)
#

循环主要有两类(姑且这么算吧),whilefor。这两种循环基本上可以相互转换,while 结构简单些,而 for 更规整些。我们先介绍 while

7.3.1 (do-)while 循环
#

while 循环可以视为一直 if,如果成立就继续:

int something[10] = {1, 1, 4, 5, 1, 4, 1, 9, 1, 9};

int index = 0;
// 其实理论上可以直接while(index++ < 10),但是这样费解些。
// 我个人还是喜欢分开或者使用for循环
while(index < 10) {
    std::cout << something[index];
    index++;  
}

所以之前的例子中你经常看到在“健壮的”输入中出现的 while(1) 就是一个无限循环,除非 break 打破它(后面会说),它会一直进行下去。

上面的 while 的例子中,如果一开始条件就不成立,那么 while 就不会执行,但我们有些时候 需要 至少执行一次,比如用户输入、菜单显示等等。为了避免复制代码,有一种 do-while 的循环体可以使用。以下是菜单显示的例子(伪代码),因为要先显示一次菜单再获取输入,所以可以选择 do-while 结构

do {
    displayMenu();
    choice = getUserChoice();
    processChoice(choice);
} while(choice != EXIT_OPTION);

do-while 需要在结尾打分号,别忘了。

7.3.2 for 循环
#

for 循环是十分工整的循环结构,它如此工整,以至于(笔者看到的)不少开源项目的代码规范都要求甚至强制使用这种循环结构(而不使用 while)。工整在哪?在于它将循环初始化、循环继续的条件、每轮循环后的操作都放在一起集中展示。以下是框架:

// 表达式可以不填,但是分号得留上
for (初始化表达式; 条件表达式; 更新表达式) {
    // 循环体
    //也就是要重复执行的代码
}

上面 7.3.1 中 while 的例子,可以重写为:

int something[10] = {1, 1, 4, 5, 1, 4, 1, 9, 1, 9};

// 原来的index作用域在for之外,而这里i在for以内
// 所以命名就稍随意些。
// 不过仍然有项目要求写清楚
for(int i = 0 ; i < 10 ; i++) {
    std::cout << something[i];
}

注意:如果条件表达式留空,默认为真。所以 for(;;) { ... } 是个无限循环。

7.3.2.1 C++的 for
#

在 C++中,引入了许多类型和模板,因此也加入更新的方式用于遍历这些相应的对象。主要是迭代 for 和范围 for

  1. 范围 for 循环
    #

    这是 C++11 引入的现代循环语法,专门用于遍历容器、数组等序列中的 所有元素,无需手动处理索引或迭代器。

    for (元素类型 变量名 : 序列) {
     // 循环体
    }
    

    示例:

    int numbers[] = {10, 20, 30, 40, 50};
    
    // auto在C++里面是自动推断类型的意思
    // 你也可以写int
    for (auto num : numbers) {
        cout << num << " ";
    }
    cout << endl;
    
  2. 迭代器

    迭代器是 C++中用于遍历 STL 容器元素的通用机制,它提供了一种统一的方式来访问各种容器中的元素。

    for (容器类型::iterator it = 容器.begin(); it != 容器.end(); ++it) {
        // 使用 *it 访问元素
    }
    

    示例:

    // 创建一个int类型的可扩展的数组
    vector<int> vec = {1, 2, 3, 4, 5};
    
    // 这里vector<int>::iterator要与声明的vector<int>一致
    // 当然,使用auto是个明智的选择
    // 因为当你修改成vector<double>的时候就不用修改循环了
    for (vector<int>::iterator it = vec.begin(); it != vec.end(); it++) {
        // it的用法和指针差不多。暂时理解为高级一点的指针
        cout << *it << " ";
    }
    cout << endl;
    

7.3.4 循环控制
#

循环一旦开始,一般就会一直执行到条件表达式不成立。如果我们需要在循环 过程中 提前开启新一轮循环,或者打断循环,就要使用到 continuebreak 了。你应该在之前的很多代码里面见过它们了。

continue 语句将跳过其后面的语句,直接进行下一次循环,当然,for 的更新语句(i++ 之类)会正常执行。这样说可能有点抽象,我们直接看例子:

int arr[] = {1, 2, 3, 4, 5, 6};

// 其实有更聪明的大小计算方式,不需要硬编码6
// int len = sizeof(arr) / sizeof(arr[0]);
// 然后把6改成len
for(int i = 0; i < 6; i++) {
    if(i == 3) continue;
    
    std::cout << arr[i] << " ";
}

输出:

1 2 3 5 6 

因为 if(i == 3) continue; 的作用,在 i == 3 的时候(arr[3] 是 4 哦!)直接跳过。所以输出没有 4。

碎碎念

如果你不习惯 arr[3] 是 4,可以在 arr[0] 的地方垫一个 0。当然,循环条件可以更自然地写成 int i = 1

break 语句直接跳出循环(一层),我们还是直接看例子:

int arr[] = {00, 1, 2, 3, 4, 5, 6};

// 这样就要多-1了
//不过反人类的部分就可以缩减到1处了
int len_of_arr = sizeof(arr) / sizeof(arr[0]);

for(int i = 1; i < len_of_arr; i++) {
    if(i == 3) break;
    
    printf("%d ", arr[i]);
}

输出:

1 2 

​ 因为 if(i == 3) break; 的作用,在 i == 3 的时候直接跳过。所以输出只有 1 2

碎碎念:没什么地位的 goto

goto 是 C 语言中的一个“远程”跳转语句,它允许程序无条件地跳转到同一个函数内被标签(label)标记的语句处执行。

#include <stdio.h>

int main() {
    printf("Hello\n");
    goto skip;      // 跳转到 skip 标签处
    printf("This will be skipped\n");
    
skip:              // 标签定义
    printf("World\n");
    return 0;
}

输出:

Hello
World

因为 goto 中间的 printf 被跳过了。

虽然 goto 看起来直接有效,但是它破坏了正常程序的流程,goto 出现在代码里面,特别是很多的时候,就会让程序像蜂窝一样复杂难懂。而且几乎所有 goto 的使用场景都可以用更结构化的方式更好地实现。就算是要一步到位跳出多个循环这种看起来不方便的实践,也可以通过:1) 设置一个 bool exitAll,为真的时候全部退出(也就是在 for 里面加一个 && exitAll 的事);2) 更简单地,将循环体放到函数里面(或者是 lambda 函数,我们后面讲)然后直接 return 退出整个函数体。因此,goto 在经历了短暂的辉煌后(早期 BASIC 程序员迁移到 C),逐渐衰落了下去

至此,我们已掌握了控制流程的基本语法。希望读者都能少些死循环,能够 If I Can Stop One Loop By Breaking


你已经学会循环和控制了,快来优化 R 星的 18 亿行`scanf 吧!

习题:

  1. 使用 C++编写一个程序,实现以下菜单并供用户选择。安全起见,不要再使用 C 风格的输入了。

    分别尝试使用 if-elseswitch-case 实现

    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    

    实现以下交互(一次就好):

    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    输入您要购买的商品:1
    您购买了“西瓜”。
    
    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    输入您要购买的商品:99
    没有这个商品。
    
  2. 修改上面的程序,显示新的菜单:

    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    [0] 退出
    

    实现以下交互(循环输入,0 退出):

    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    [0] 退出
    输入您要购买的商品:1
    您购买了“西瓜”。
    输入您要购买的商品:99
    没有这个商品。
    输入您要购买的商品:0
    
  3. 修改上面的程序,实现以下交互(一直显示菜单):

    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    [0] 退出
    输入您要购买的商品:1
    您购买了"西瓜"。
    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    [0] 退出
    输入您要购买的商品:99
    没有这个商品。
    [1] 西瓜 ¥2/斤
    [2] 大象 ¥1919/斤
    [3] 生瓜蛋子 ¥0.5/斤
    [4] 吸铁石 ¥42/斤
    [0] 退出
    输入您要购买的商品:0
    

    思考使用什么循环体可以少写一次菜单显示。

  4. 经典永流传 编写程序,输出 1000 以内所有“幸运数字”,它满足:

    • 能被 7 整除(提示:% 运算)

    • 不包含数字 7 (提示:动动脑子!真理医生音

命令提示符@CommandPrompt-Wang
作者
命令提示符@CommandPrompt-Wang