本章将讲解主要的控制、循环语句。并且对 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 进行判断,构建 while 和 for 循环,用 break、continue 来干预循环的流程,让我们成为自己代码世界的“诗人”,写出优雅而高效的篇章。
7.2 若我不曾见过“条件”(if-else/switch-case) #
所谓条件控制,就是 特定条件下对程序执行流程或生成模型输出进行控制的技术,人话就是分类讨论:如果……那么……(否则……)——计算机喜欢这样清晰明了的语句。因此,我们要养成将大问题拆解成简单的小问题的习惯。
7.2.1 If 语句 #
if-else 的语法如下:
if(条件) {
你要干的事
} else if(条件) {
你要干的事
} else if(条件) {
可以一直套……
} else {
你要干的事
}
格式上没有硬性要求,你要是觉得我的风格不好看可以尽管调整。if 是必须的 总得有个头啊!(康熙音);else、else if 是可选的(也就是没有需要就可以不要),但是顺序必须是 if -> else if -> else。
其实如果 if(else 同理)后面只有一个语句(分号结尾),你也可以这样写:
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:
-
==与=:这是最最经典错误!if (a = 5)是赋值,表达式结果为5(非零,所以永远是真),条件永远成立!if (a == 5)才是比较。时刻警惕! -
短路求值:
&&和||有短路特性。对于a && b,如果a为假,就不会再去计算b。对于a || b,如果a为真,也不会再去计算b。这在b是一个函数调用或带有副作用(如i++)时尤为重要。
举例子(其实第三章课后题有)int x = 0; bool y = ((x++ == 3) and (x++ == 4));x是1,y是false,原因很简单,从左往右看,(x++ == 3)为false,已经可以确定整个表达式的值,所以(x++ == 4)不会执行,x只增加一次。很搞吧?所以实际使用千万别自找苦吃。
-
结合性:它决定了优先级相同时的运算方向。
a = b = c从右向左,等价于a = (b = c)。a + b + c从左向右,等价于(a + b) + c。这个基本上和数学的一样,不必在意。 -
万能括号:记不住优先级没关系! 当你无法确定运算顺序时,多用括号
()来明确你的意图,这是最高效、最安全的方法,也能让代码更清晰。比较一下: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 的代码,直到遇到 break 或 switch 语句结束。这就是所谓的 " 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 继续沉醉 自我迂回(循环) #
循环主要有两类(姑且这么算吧),while 和 for。这两种循环基本上可以相互转换,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
-
范围
for循环 #这是 C++11 引入的现代循环语法,专门用于遍历容器、数组等序列中的 所有元素,无需手动处理索引或迭代器。
for (元素类型 变量名 : 序列) { // 循环体 }示例:
int numbers[] = {10, 20, 30, 40, 50}; // auto在C++里面是自动推断类型的意思 // 你也可以写int for (auto num : numbers) { cout << num << " "; } cout << endl; -
迭代器
迭代器是 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 循环控制 #
循环一旦开始,一般就会一直执行到条件表达式不成立。如果我们需要在循环 过程中 提前开启新一轮循环,或者打断循环,就要使用到 continue 和 break 了。你应该在之前的很多代码里面见过它们了。
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 吧!
习题:
-
使用 C++编写一个程序,实现以下菜单并供用户选择。安全起见,不要再使用 C 风格的输入了。
分别尝试使用
if-else和switch-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 没有这个商品。 -
修改上面的程序,显示新的菜单:
[1] 西瓜 ¥2/斤 [2] 大象 ¥1919/斤 [3] 生瓜蛋子 ¥0.5/斤 [4] 吸铁石 ¥42/斤 [0] 退出实现以下交互(循环输入,0 退出):
[1] 西瓜 ¥2/斤 [2] 大象 ¥1919/斤 [3] 生瓜蛋子 ¥0.5/斤 [4] 吸铁石 ¥42/斤 [0] 退出 输入您要购买的商品:1 您购买了“西瓜”。 输入您要购买的商品:99 没有这个商品。 输入您要购买的商品:0 -
修改上面的程序,实现以下交互(一直显示菜单):
[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思考使用什么循环体可以少写一次菜单显示。
-
经典永流传编写程序,输出 1000 以内所有“幸运数字”,它满足:-
能被 7 整除(提示:
%运算) -
不包含数字 7 (提示:动动脑子!
真理医生音)
-