Cpp学习11 第 8 章 控制流
8.1 — 控制流简介
| Category | Meaning | C++ 实现 |
|---|---|---|
| Conditional statements | 仅当满足某些条件时,才执行一段代码。 | if, else, switch |
| Jumps | 告诉 CPU 从其他位置开始执行语句。 | goto, break, continue |
| Function calls | 跳转到其他位置,然后再返回。 | function calls, return |
| Loops | 重复执行某些代码序列零次或多次,直到满足某个条件为止。 | while, do-while, for, ranged-for |
| Halts 停止 | 终止程序。 | std::exit(), std::abort() |
| Exceptions 例外情况 | 一种专为错误处理而设计的特殊流程控制结构。 | try, throw, catch |
8.2 — If 语句和代码块
如果程序员没有在 if statement 或 else statement 的语句部分声明代码块,编译器会隐式地声明一个。
一些新手程序员有时会尝试在隐式代码块中定义变量,代码无法编译,编译器会报错,提示标识符 x 未定义。变量 x会在代码块结束时被销毁
考虑将与 if 或 else 相关的单个语句放在代码块中(尤其是在学习阶段)。
8.3 — 常见的 if 语句问题
为了避免嵌套 if 语句时出现歧义,最好将内部 if 语句显式地放在一个代码块中。
空语句是指仅包含分号的表达式语句:
1 | if (x > 10) |
8.4 — constexpr if 语句
C++17 引入了 constexpr if 语句 ,该语句要求条件表达式必须是常量表达式。constexpr if 语句的条件表达式将在编译时求值。
如果 constexpr 条件判断结果为 true ,则整个 if-else 语句将被替换为真语句。如果 constexpr 条件判断结果为 false ,则整个 if-else 语句将被替换为假语句(如果存在),或者不替换任何内容(如果没有 else 语句)。
1 |
|
8.5 — switch 语句基础知识
switch 语句中的条件必须求值为整型(如果您需要回顾哪些基本类型属于整型,请参阅 4.1 课——基本数据类型简介 )或枚举类型(将在后续课程 13.2 课——非作用域枚举和 13.6 课——作用域枚举(枚举类)中介绍 ),或者可以转换为其中一种类型。求值为 浮点型、字符串和大多数其他非整型 的表达式不能在此处使用。
Case labels, The default label
如果条件表达式的值与任何 case 标签都不匹配,且未提供默认 case,则 switch 语句内的任何 case 都不会执行。执行会在 switch 代码块结束后继续进行。
标签下的每一组语句都应该以 break 语句或 return 语句结尾。这包括 switch 语句中最后一个标签下的语句。
尽量不要缩进标签。这样可以使标签从周围的代码中脱颖而出,而不会让人觉得它们定义了一个嵌套的作用域。
1 | void printDigitName(int x) |
当测试单个表达式(非布尔整数类型或枚举类型)与少量值是否相等时,应优先使用 switch 语句而不是 if-else 语句。
8.6 — Switch fallthrough和作用域
当 switch 表达式匹配到 case 标签或可选的 default 标签时,执行将从匹配标签之后的第一条语句开始。然后,执行将按顺序继续,直到发生终止条件。
当执行从一个标签下的语句流向下一个标签下的语句时,这被称为语句向下传递(fallthrough) 。
由于很少有人希望或有意使用 fallthrough,因此许多编译器和代码分析工具会将 fallthrough 标记为警告。
但万一有人就是要用fallthrough呢?为了解决这个问题,C++17 添加了一个名为 [[fallthrough]] 的新属性。
属性 Attributes 是现代 C++ 的一项特性,它允许程序员向编译器提供一些关于代码的额外数据。要指定一个属性,属性名称需要放在双括号内。属性不是语句——相反,它们几乎可以在任何上下文相关的地方使用。
1 | switch (2) |
我们可以“堆叠” case 标签,使所有这些 case 标签之后共享同一组语句。这不属于 fallthrough 行为,因此这里不需要使用注释或 [[fallthrough]] :
1 | bool isVowel(char c) |
对于 switch 语句,标签之后的所有语句的作用域都限于 switch 代码块。不会创建任何隐式代码块。case 1 和 default 标签之间的 2 个语句的作用域是 switch 块的一部分,而不是 case 1 隐含的块。
如果某个 case 语句需要定义和/或初始化一个新变量,最佳实践是在 case 语句下方的显式代码块中进行操作。
8.7 — goto 语句
在 C++ 中,无条件跳转通过 goto 语句实现,跳转到的位置由语句标签标识。与 switch case 标签一样,语句标签通常不缩进。
1 |
|
语句标签具有函数作用域。
避免使用 goto 语句(除非其他方法会显著降低代码可读性)。
8.8 — 循环和 while 语句简介
积分循环变量通常应为有符号积分类型。
略。
8.9 — Do while
do-while 循环并不常用。将条件放在循环底部会掩盖循环条件,从而导致错误。
当面临相同选择时,优先使用 while 循环而不是 do-while 循环。
8.10 — For 语句
在 C++ 中,使用最广泛的循环语句无疑是 for 语句。当循环变量的含义非常明确时, for 语句 (也称为 for 循环 )是首选,因为它能够让我们轻松简洁地定义、初始化、测试和更改循环变量的值。
for (init-statement; condition; end-expression)
statement;
在 for 循环条件中进行数值比较时,应避免使用 operator!= 尽可能优先使用 operator< 或 operator<= 。
带有多个计数器的 for 循环
1 |
|
在 C++ 中,只有在这里,在同一语句中定义多个变量并使用逗号运算符才被认为是可接受的做法。
仅在循环内部使用的变量应在循环内部定义。
8.11 — Break & continue
在 switch 语句中,通常在每个 case 的末尾使用 break 语句,以表示该 case 已结束(可以防止程序继续执行后续的 case)
在循环中,可以使用 break 语句提前结束循环。循环结束后,程序将继续执行下一条语句。break 也是摆脱故意陷入的无限循环的常用方法。
continue 语句提供了一种便捷的方式来结束循环的当前迭代,而无需终止整个循环。
当 early return 能够简化函数逻辑时,请使用早期回报。
8.12 — Halts(提前退出程序)
halt 语句用于终止程序。在 C++ 中,halt 语句是用函数(而不是关键字)实现的,因此我们的 halt 语句实际上是函数调用。
std::exit() 是一个使程序正常终止的函数。 正常终止意味着程序以预期的方式退出。请注意, normal termination 并不意味着程序是否成功执行( status code 用于指示成功执行)。例如,假设您正在编写一个程序,期望用户输入要处理的文件名。如果用户输入了无效的文件名,您的程序可能会返回一个非零 status code 来指示失败状态,但它仍然会 normal termination 。
虽然 std::exit() 会在 main() 函数返回后隐式调用,但也可以显式调用 std::exit(),需要包含 cstdlib 头文件。
关于显式调用 std::exit() 需要特别注意一点: std::exit() 不会清理任何局部变量(无论是在当前函数中,还是在调用栈中更早的函数中)。这意味着,如果你的程序依赖于局部变量的自我清理机制,那么调用 std::exit() 可能会很危险。
C++ 提供了 std::atexit() 函数,允许您指定一个函数,该函数将在程序通过 std::exit() 终止时自动调用。
被注册的清理函数必须不带参数且没有返回值。最后,你可以使用 std::atexit() 注册多个清理函数,它们会按照注册顺序的逆序被调用(最后注册的清理函数会被最先调用)。
在多线程程序中,调用 std::exit() 可能会导致程序崩溃(因为调用 std::exit() 的线程会清理一些静态对象,而这些对象可能仍被其他线程访问)。
std::abort 和 std::terminate
std::abort() 函数会导致程序异常终止。 异常终止意味着程序在运行时遇到了某种异常错误,无法继续运行。std::terminate() 函数通常与 exceptions 一起使用(我们将在后面的章节中介绍异常)。虽然可以显式调用 std::terminate ,但更常见的情况是,当异常未被处理时(以及其他一些与异常相关的情况下),它会被隐式调用。默认情况下, std::terminate() 会调用 std::abort() 。
何时应该使用halt?
“几乎从不”。销毁局部对象是 C++ 的重要组成部分(尤其是在使用类时),而上述函数均不会清理局部变量。异常处理是处理错误情况的更好、更安全的机制。
8.13 — 随机数生成简介
以下是一个简单的算法,用于生成一个数字序列,其中每个后续数字都加 1:
1 |
|
如果一个算法在多次调用之间保留某些信息,则称该算法是有状态的 。相反, 无状态算法不存储任何信息(每次调用时都必须提供它所需的所有信息)。我们的 plusOne() 函数是有状态的,因为它使用静态变量 s_state 来存储最后生成的数字。当应用于算法时,“ 状态 ”一词指的是有状态变量中保存的当前值(即在多次调用之间保留的值)。
伪随机数生成器(PRNG)
Pseudo-random
为了生成不同的输出序列,伪随机数生成器(PRNG)的初始状态需要改变。用于设置 PRNG 初始状态的值(或值集)称为随机种子 (简称种子 )。当使用种子设置了 PRNG 的初始状态后,我们称之为 “播种 seeded” 。
所有伪随机数生成器(PRNG)都是周期性的,这意味着生成的数字序列在某个时刻会开始重复。PRNG 开始重复之前的序列长度称为周期 。
从 C++20 开始,梅森旋转 Mersenne twister 算法是唯一一个在 C++ 中自带的、兼具良好性能和质量的伪随机数生成器。然而,值得注意的是,按照现代伪随机数生成器(PRNG)的标准来看,梅森旋转算法已经有些过时了 。梅森旋转算法最大的问题在于,在生成 624 个数字后,其结果就可以预测,因此它不适用于任何需要不可预测性的应用。
8.14 — 使用梅森旋转算法生成随机数
1 |
|
mt() 是调用函数 mt.operator() 的简洁语法。
使用梅森旋转算法掷骰子
随机数库中包含许多随机数分布,其中大部分除非用于统计分析,否则你永远不会用到它们。但有一种随机数分布极其有用:均匀分布。 均匀分布是指以相等的概率产生介于两个数字 X 和 Y(包含 X 和 Y)之间的输出的随机数分布。
1 |
|
使用系统时钟进行种子输入
C++ 有一个包含各种时钟的 chrono 库,我们可以用它来生成一个初始时间值。为了最大限度地降低程序快速连续运行时出现两个时间值相同的概率,我们需要使用一种变化速度尽可能快的计时单位。为此,我们将向时钟询问自其能够测量的最早时间点以来已经过去了多少时间。这个时间单位是“滴答”(ticks),这是一个非常小的时间单位(通常是纳秒,但也可能是毫秒)。
1 |
|
使用随机设备设置种子
使用 std::random_device 来初始化你的伪随机数生成器(除非它没有针对你的目标编译器/架构正确实现)。
许多伪随机数生成器 (PRNG) 在初始播种后可以重新播种。这实际上会重新初始化随机数生成器的状态,使其从新的种子状态开始生成结果。除非有特殊原因,否则通常应避免重新播种,因为它可能会导致结果随机性降低,甚至完全失去随机性。
种子数量不足的 PRNG 可能会产生次优结果,无法满足那些需要最高质量结果的应用的需求。略….
调试使用随机数的程序
使用随机数的程序很难调试,因为每次运行程序都可能出现不同的行为。在这种情况下,错误行为可能出现,也可能不会出现。这会浪费大量时间。调试时,确保程序每次都以相同的(错误)方式执行会很有帮助。这样,你就可以根据需要多次运行程序,从而找出错误所在。
因此,在调试时,一个有用的技巧是使用一个特定的固定值(例如 5 )作为伪随机数生成器 (PRNG) 的种子,该种子值会导致错误行为的发生。如果给定的种子值不会导致程序出现错误,则继续递增种子值,直到找到一个能够导致错误的种子值为止。这将确保程序每次生成相同的结果,从而简化调试过程。一旦找到错误,就可以使用正常的种子值设置方法重新开始生成随机结果。
8.15 — 全局随机数(Random.h)
如果我们想在多个函数或文件中使用随机数生成器(PRNG)该怎么办?
最佳方案是创建一个全局随机数生成器对象(放在命名空间内!)。还记得我们说过要避免使用非常量全局变量吗?这里是个例外。
这里提供一个简单的、仅包含头文件的解决方案,您可以将其 #include 到任何需要访问随机化、自种子 std::mt19937 代码文件中:
Random.h:
1 |
|
使用 Random.h
- 将以上代码复制粘贴到项目目录下的名为
Random.h的文件中并保存。您也可以选择将 Random.h 添加到您的项目中。 - 在项目中任何需要生成随机数的 .cpp 文件中
#include "Random.h"。 - 调用
Random::get(min, max)生成一个介于min和max(含 min 和 max)之间的随机数。无需初始化或设置。
8.x — 第 8 章总结和测验
1 |
|
Random.h的使用,算了…
- 标题: Cpp学习11 第 8 章 控制流
- 作者: 铁名_IronName
- 创建于 : 2026-08-09 09:26:42
- 更新于 : 2026-08-09 11:17:22
- 链接: https://blog.ironname.top/2026/Cpp/Cpp学习11/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。