1. 控制流简介

当程序运行时,CPU 从 main() 的顶部开始执行,执行一定数量的语句(默认情况下按顺序),然后程序在 main() 的末尾终止。CPU 执行的特定语句序列称为程序的执行路径(execution path)(或简称 path)。C++ 提供了许多不同的控制流语句(也称为流控制语句),这些语句允许程序员通过程序更改正常的执行路径。

当控制流语句导致执行点更改为非顺序语句时,这称为分支

类别 含义 在C++中的实现方式
条件语句 如果满足某个条件,则执行一段代码序列。 if, else, switch
跳转 告诉CPU从其他位置开始执行语句。 goto, break, continue
函数调用 跳转到其他位置并返回。 函数调用, return
循环 根据条件重复执行一段代码序列,次数可以为零或多次。 while, do-while, for, 范围for
终止程序 终止程序运行。 std::exit(), std::abort()
异常 一种用于错误处理的特殊流程控制结构。 try, throw, catch

2. If 语句和块

C++ 支持两种基本类型的条件语句:if 语句和 switch 语句。

if 语句采用以下格式:

1
2
3
4
if (condition)
true_statement;
else
false_statement;

else是可选的,if-else 语句包含多个语句时应该用块,如果是单个语句,最好也放在块中

如果程序员没有在 if 语句或 else 语句的 statement 部分声明块,编译器将隐式声明一个块。因此上面的格式等价于:

1
2
3
4
5
6
7
8
if (condition)
{
true_statement;
}
else
{
false_statement;
}

在if语句中定义的变量无法被外部访问

什么时候应该使用 if-else(if 后跟一个或多个 else 语句)或 if-if(if 后跟一个或多个额外的 if 语句)。

  • 当您只想在第一个 true 条件之后执行代码时,请使用 if-else。

  • 如果要在所有 true 条件之后执行代码,请使用 if-if。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

void ifelse(bool a, bool b, bool c)
{
if (a) // always evaluates
std::cout << "a";
else if (b) // only evaluates when prior if-statement condition is false
std::cout << "b";
else if (c) // only evaluates when prior if-statement condition is false
std::cout << "c";
std::cout << '\n';
}

void ifif(bool a, bool b, bool c)
{
if (a) // always evaluates
std::cout << "a";
if (b) // always evaluates
std::cout << "b";
if (c) // always evaluates
std::cout << "c";
std::cout << '\n';
}

int main()
{
ifelse(false, true, true);
ifif(false, true, true);

return 0;
}

3. 常见的 if 语句问题

可以将 if 语句嵌套在其他 if 语句中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;

if (x >= 0) // outer if statement
// it is bad coding style to nest if statements this way
if (x <= 20) // inner if statement
std::cout << x << " is between 0 and 20\n";

return 0;
}

现在考虑以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;

if (x >= 0) // outer if-statement
// it is bad coding style to nest if statements this way
if (x <= 20) // inner if-statement
std::cout << x << " is between 0 and 20\n";

// which if statement does this else belong to?
else
std::cout << x << " is negative\n";

return 0;
}

上述程序引入了一个潜在的歧义来源,称为悬挂else问题。在上述程序中,else语句是与外层if语句还是内层if语句匹配的?答案是 else 语句与同一块中最后一个不匹配的 if 语句配对。因此,在上面的程序中,else 语句与内部 if 语句匹配,就好像程序是这样编写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;

if (x >= 0) // outer if statement
{
if (x <= 20) // inner if statement
std::cout << x << " is between 0 and 20\n";
else // attached to inner if statement
std::cout << x << " is negative\n";
}

return 0;
}

为了在嵌套 if 语句时避免这种歧义,最好将内部 if 语句显式地括在一个块中。

嵌套的 if 语句通常可以通过重构逻辑或使用逻辑运算符来展平,嵌套较少的代码不太容易出错。上面的示例可以按如下方式展平:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
std::cout << "Enter a number: ";
int x{};
std::cin >> x;

if (x < 0)
std::cout << x << " is negative\n";
else if (x <= 20) // only executes if x >= 0
std::cout << x << " is between 0 and 20\n";
else // only executes if x > 20
std::cout << x << " is greater than 20\n";

return 0;
}

null 语句是仅包含分号的语句:

1
2
if (x > 10)
; // this is a null statement

Null 语句不执行任何作。当语言需要存在一个语句,但程序员不需要时,通常会使用它们。为了提高可读性,null 语句通常放在它们自己的行上。Null 语句很少有意与 if 语句一起使用。但是,它们可能会无意中给我们带来问题。请考虑以下代码段:

1
2
if (nuclearCodesActivated()); // note the semicolon at the end of this line
blowUpTheWorld();

在上面的代码段中,程序员不小心在 if 语句的末尾放了一个分号(这是一个常见的错误,因为分号结束了许多语句)。这个不起眼的错误编译正常,并导致代码段执行,就像它是这样编写的一样:

1
2
3
if (nuclearCodesActivated())
; // the semicolon acts as a null statement
blowUpTheWorld(); // and this line always gets executed!

小心不要用分号“终止”你的 if 语句,否则你想要有条件执行的语句将无条件执行(即使它们在一个块内)

在 Python 中,pass 关键字用作 null 语句。它通常用作稍后将实现的代码的占位符。因为它是一个单词而不是一个符号,所以 pass 不容易被无意中误用,并且更易于搜索(允许您稍后轻松找到这些占位符)。

1
2
for x in [0, 1, 2]:
pass # To be completed in the future

在 C++ 中,我们可以使用预处理器来模拟 pass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define PASS

void foo(int x, int y)
{
if (x > y)
PASS;
else
PASS;
}

int main()
{
foo(4, 7);

return 0;
}

在条件语句中,在测试相等性时,你应该使用 operator==,而不是 operator=(即赋值)

4. constexpr if 语句

通常,if 语句的条件是在运行时计算的。但是,请考虑条件是常量表达式的情况,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main()
{
constexpr double gravity{ 9.8 };

// reminder: low-precision floating point literals of the same type can be tested for equality
if (gravity == 9.8) // constant expression, always true
std::cout << "Gravity is normal.\n"; // will always be executed
else
std::cout << "We are not on Earth.\n"; // will never be executed

return 0;
}

因为 gravity 是 constexpr 并使用值 9.8 初始化,所以条件 gravity == 9.8 的计算结果必须为 true。因此,将永远不会执行 else 语句。在运行时评估 constexpr 条件是浪费的(因为结果永远不会改变)。将代码编译成永远无法执行的可执行文件也是浪费的。

C++17 引入了 constexpr if 语句,该语句要求条件为常量表达式。constexpr-if 语句的条件将在编译时进行求值。如果常量条件的计算结果为 true,则整个 if-else 将被 true 语句替换。如果常量条件的计算结果为 false,则整个 if-else 将被 false 语句(如果存在)或什么都没有(如果没有 else)替换。

要使用 constexpr-if 语句,我们在 if 后添加 constexpr 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int main()
{
constexpr double gravity{ 9.8 };

if constexpr (gravity == 9.8) // now using constexpr if
std::cout << "Gravity is normal.\n";
else
std::cout << "We are not on Earth.\n";

return 0;
}

它将编译以下内容:

1
2
3
4
5
6
7
8
int main()
{
constexpr double gravity{ 9.8 };

std::cout << "Gravity is normal.\n";

return 0;
}

出于优化目的,现代编译器通常会将具有 constexpr 条件的非 constexpr if 语句视为 constexpr-if 语句。但是,他们不需要这样做。编译器遇到带有 constexpr 条件的非 constexpr if 语句时,可能会发出警告,建议你改用 if constexpr。这将确保进行编译时评估(即使禁用了优化)。

5. switch 语句基础知识

由于针对一组不同的值测试变量或表达式是否相等是很常见的,因此 C++ 提供了一个专门用于此目的的替代条件语句,称为 switch 语句。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

void printDigitName(int x)
{
switch (x)
{
case 1:
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}

int main()
{
printDigitName(2);
std::cout << '\n';

return 0;
}

switch 语句背后的想法很简单:对表达式(有时称为条件)进行求值以生成值。然后发生以下情况之一:

  • 如果表达式的值等于任何 case-labels 之后的值,则执行匹配的 case-label 之后的语句。

  • 如果找不到匹配的值,并且存在 default 标签,则执行 default 标签后面的语句。

  • 如果找不到匹配的值,并且没有默认标签,则跳过整个 switch 语句。

switch 中的条件必须计算为整型。为什么 switch 类型只允许整型 (或枚举) 类型?答案是因为 switch 语句被设计为高度优化。从历史上看,编译器实现 switch 语句的最常见方法是通过 Jump tables – 而 Jump Table 仅适用于整数值。

switch 语句使用了两种标签。第一种标签是 case 标签,它使用 case 关键字声明,后跟一个常量表达式。常量表达式必须与条件的类型匹配,或者必须可转换为该类型。如果条件表达式的值等于 case 标签后面的表达式,则执行从该 case 标签后面的第一个语句开始,然后按顺序继续执行。第二种标签是 default 标签(通常称为 default case),它是使用 default 关键字声明的。如果条件表达式与任何 case 标签都不匹配,并且存在 default 标签,则从 default 标签后的第一个语句开始执行。

将 default case 放在 switch 块的最后。

break 语句(使用 break 关键字声明)告诉编译器我们已经完成了 switch 中的语句执行,并且应该在 switch 块结束后继续执行该语句。这允许我们退出 switch 语句而不退出整个函数。下面是一个略微修改的示例,使用 break 而不是 return 重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

void printDigitName(int x)
{
switch (x) // x evaluates to 3
{
case 1:
std::cout << "One";
break;
case 2:
std::cout << "Two";
break;
case 3:
std::cout << "Three"; // execution starts here
break; // jump to the end of the switch block
default:
std::cout << "Unknown";
break;
}

// execution continues here
std::cout << " Ah-Ah-Ah!";
}

int main()
{
printDigitName(3);
std::cout << '\n';

return 0;
}

标签下的每组语句都应以 break-statement 或 return-statement 结尾。这包括 switch 中最后一个标签下的语句。

传统上,标签通常不缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Preferred version
void printDigitName(int x)
{
switch (x)
{
case 1: // not indented from switch statement
std::cout << "One";
return;
case 2:
std::cout << "Two";
return;
case 3:
std::cout << "Three";
return;
default:
std::cout << "Unknown";
return;
}
}

这样可以轻松识别每个标签。而且,由于语句仅从 switch 块缩进一级,因此它正确地暗示了这些语句都是 switch 块范围的一部分。

6. switch 穿透和范围界定

6.1 switch 穿透

当 switch 表达式与 case 标签或可选的 default 标签匹配时,执行从匹配标签后面的第一个语句开始。然后,执行将按顺序继续,直到发生以下终止条件之一:

  • 到达 switch 块的末尾。

  • 另一个控制流语句(通常是 break 或 return)会导致 switch 块或函数退出。

  • 其他事情打断了程序的正常流程(例如,作系统关闭了程序,宇宙内爆,等等…)

请注意,存在另一个 case 标签不是这些终止条件之一。因此,如果没有 break 或 return,执行将溢出到后续 case 中。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main()
{
switch (2)
{
case 1: // Does not match
std::cout << 1 << '\n'; // Skipped
case 2: // Match!
std::cout << 2 << '\n'; // Execution begins here
case 3:
std::cout << 3 << '\n'; // This is also executed
case 4:
std::cout << 4 << '\n'; // This is also executed
default:
std::cout << 5 << '\n'; // This is also executed
}

return 0;
}

输出:

1
2
3
4
2
3
4
5

这可能不是我们想要的!当执行从标签下的语句流向后续标签下的语句时,这称为 穿透(fallthrough)

一旦 case 或 default 标签下的语句开始执行,它们就会溢出 (穿透) 到后续的 case 中。break 或 return 语句通常用于防止这种情况。

注释有意的穿透是一种常见的约定,用于告诉其他开发人员穿透是有意的。虽然这适用于其他开发人员,但编译器和代码分析工具不知道如何解释注释,因此它不会消除警告。为了帮助解决这个问题,C++17 添加了一个名为 [[fallthrough]] 的新属性。

属性是一种现代 C++ 功能,它允许程序员向编译器提供有关代码的一些附加数据。要指定属性,请将属性名称放在双括号之间。属性不是语句——相反,它们几乎可以在与上下文相关的任何位置使用。

[[fallthrough]] 属性修改 null 语句,以指示穿透是有意为之(不应触发警告):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main()
{
switch (2)
{
case 1:
std::cout << 1 << '\n';
break;
case 2:
std::cout << 2 << '\n'; // Execution begins here
[[fallthrough]]; // intentional fallthrough -- note the semicolon to indicate the null statement
case 3:
std::cout << 3 << '\n'; // This is also executed
break;
}

return 0;
}

输出:

1
2
2
3

你可以使用 switch 语句通过按顺序放置多个 case 标签将多个测试合并到一个语句中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool isVowel(char c)
{
switch (c)
{
case 'a': // if c is 'a'
case 'e': // or if c is 'e'
case 'i': // or if c is 'i'
case 'o': // or if c is 'o'
case 'u': // or if c is 'u'
case 'A': // or if c is 'A'
case 'E': // or if c is 'E'
case 'I': // or if c is 'I'
case 'O': // or if c is 'O'
case 'U': // or if c is 'U'
return true;
default:
return false;
}
}

这不被视为穿透行为,因此此处不需要使用注释或 [[fallthrough]]。

6.2 范围界定

使用 if 语句,在 if 条件之后只能有一个语句,并且该语句被视为隐式地位于块内:

1
2
if (x > 10)
std::cout << x << " is greater than 10\n"; // this line implicitly considered to be inside a block

但是,对于 switch 语句,labels 后面的语句的作用域都限定为 switch 块。不会创建隐式块。

1
2
3
4
5
6
7
8
9
switch (1)
{
case 1: // does not create an implicit block
foo(); // this is part of the switch scope, not an implicit block to case 1
break; // this is part of the switch scope, not an implicit block to case 1
default:
std::cout << "default case\n";
break;
}

你可以在 switch 中声明或定义(但不能初始化)变量,包括 case 标签之前和之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch (1)
{
int a; // okay: definition is allowed before the case labels
int b{ 5 }; // illegal: initialization is not allowed before the case labels

case 1:
int y; // okay but bad practice: definition is allowed within a case
y = 4; // okay: assignment is allowed
break;

case 2:
int z{ 4 }; // illegal: initialization is not allowed if subsequent cases exist
y = 5; // okay: y was declared above, so we can use it here too
break;

case 3:
break;
}

尽管在case 1 中定义了变量 y,但它也在case 2 中使用。switch 中的所有语句都被视为同一范围的一部分。因此,在一个 case 中声明或定义的变量可以在以后的 case 中使用,即使定义变量的 case 从未执行过(因为 switch 跳过了它)!

如果 case 需要定义和初始化新变量,最佳实践是在 case 语句下的显式块内执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (1)
{
case 1:
{ // note addition of explicit block here
int x{ 4 }; // okay, variables can be initialized inside a block inside a case
std::cout << x;
break;
}

default:
std::cout << "default case\n";
break;
}

7. goto 语句

在 C++ 中,无条件跳转是通过 goto 语句实现的,要跳转到的位置通过使用语句标签来标识。就像 switch 大小写标签一样,语句标签通常不缩进。以下是 goto 语句和语句标签的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <cmath> // for sqrt() function

int main()
{
double x{};
tryAgain: // this is a statement label
std::cout << "Enter a non-negative number: ";
std::cin >> x;

if (x < 0.0)
goto tryAgain; // this is the goto statement

std::cout << "The square root of " << x << " is " << std::sqrt(x) << '\n';
return 0;
}

在此程序中,要求用户输入非负数。但是,如果输入负数,则程序将使用 goto 语句跳回到 tryAgain 标签。然后,系统会再次要求用户输入新号码。通过这种方式,我们可以不断要求用户输入,直到输入有效内容。

示例运行:

1
2
3
Enter a non-negative number: -4
Enter a non-negative number: 4
The square root of 4 is 2

我们前面介绍了两种范围:本地 (块) 范围和文件 (全局) 范围。语句标签使用第三种类型的范围:函数范围,这意味着标签甚至在其声明点之前就在整个函数中可见。goto 语句及其相应的语句标签必须出现在同一个函数中。

虽然上面的示例显示了向后跳转(到函数中的前一点)的 goto 语句,但 goto 语句也可以向前跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

void printCats(bool skip)
{
if (skip)
goto end; // jump forward; statement label 'end' is visible here due to it having function scope

std::cout << "cats\n";
end:
; // statement labels must be associated with a statement
}

int main()
{
printCats(true); // jumps over the print statement and doesn't print anything
printCats(false); // prints "cats"

return 0;
}

跳转有两个主要限制:只能在单个函数的边界内跳转(不能从一个函数跳转到另一个函数)。如果向前跳转,则无法在跳转位置仍在范围内的任何变量的初始化上向前跳转。例如:

1
2
3
4
5
6
7
8
int main()
{
goto skip; // error: this jump is illegal because...
int x { 5 }; // this initialized variable is still in scope at statement label 'skip'
skip:
x += 3; // what would this even evaluate to if x wasn't initialized?
return 0;
}

请注意,你可以向后跳转变量初始化,并且在执行初始化时,该变量将重新初始化。

在 C++(以及其他现代高级语言)中避免使用 goto。著名计算机科学家 Edsger W. Dijkstra 在一篇著名但难以阅读的论文中阐述了避免 goto 的理由,名为 Go To Statement Considered Harmful。goto 的主要问题是它允许程序员任意地跳转代码。这就产生了不太亲切地称为意大利面条代码的代码。意大利面条代码是执行路径类似于一碗意大利面条(全部缠结和扭曲)的代码,这使得遵循此类代码的逻辑变得极其困难。

一个值得注意的例外是,当你需要退出嵌套循环而不是整个函数时 —— 在这种情况下,转到循环之外可能是最干净的解决方案。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

int main()
{
for (int i = 1; i < 5; ++i)
{
for (int j = 1; j < 5; ++j)
{
std::cout << i << " * " << j << " is " << i*j << '\n';

// If the product is divisible by 9, jump to the "end" label
if (i*j % 9 == 0)
{
std::cout << "Found product divisible by 9. Ending early.\n";
goto end;
}
}

std::cout << "Incrementing the first factor.\n";
}

end:
std::cout << "And we're done." << '\n';

return 0;
}

8. 循环和 while 语句简介

while 语句(也称为 while 循环)是 C++ 提供的三种循环类型中最简单的一种,它的定义与 if 语句的定义非常相似:

1
2
while (condition)
statement;

while 语句是使用 while 关键字声明的。执行 while 语句时,将计算表达式条件。如果条件的计算结果为 true,则执行关联的语句。但是,与 if 语句不同的是,一旦语句完成执行,控制权就会返回到 while 语句的顶部,并重复该过程。这意味着只要条件继续计算为 true,while 语句就会一直循环。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main()
{
int count{ 1 };
while (count <= 10)
{
std::cout << count << ' ';
++count;
}

std::cout << "done!\n";

return 0;
}

输出:

1
1 2 3 4 5 6 7 8 9 10 done!

如果条件最初计算结果为 false,则关联的语句将根本不执行。请考虑以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main()
{
int count{ 15 };
while (count <= 10)
{
std::cout << count << ' ';
++count;
}

std::cout << "done!\n";

return 0;
}

另一方面,如果表达式的计算结果始终为 true,则 while 循环将永远执行。这称为无限循环。下面是一个无限循环的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main()
{
int count{ 1 };
while (count <= 10) // this condition will never be false
{
std::cout << count << ' '; // so this line will repeatedly execute
}

std::cout << '\n'; // this line will never execute

return 0; // this line will never execute
}

我们可以像这样声明一个有意的无限循环:

1
2
3
4
while (true)
{
// this loop will execute forever
}

退出无限循环的唯一方法是通过 return 语句、break语句、exit语句、goto 语句、引发的异常或用户终止程序。这里有一个愚蠢的例子来证明这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{

while (true) // infinite loop
{
std::cout << "Loop again (y/n)? ";
char c{};
std::cin >> c;

if (c == 'n')
return 0;
}

return 0;
}

该程序将持续循环,直到用户输入 n 作为输入,此时 if 语句的计算结果为 true,关联的返回 0;将导致函数 main() 退出,从而终止程序。

循环变量是用于控制循环执行次数的变量。例如,给定 while (count <= 10),``count 是一个循环变量。虽然大多数循环变量的类型为 int,但你偶尔会看到其他类型(例如 char)。循环变量通常被赋予简单的名称,其中 ij 和 k 是最常见的。

但是,如果你想知道程序中哪里使用了循环变量,并且您在 ij 或 k 上使用了搜索函数,则搜索函数将返回程序中一半的行!因此,一些开发人员更喜欢循环变量名称,如 iiijjj 或 kkk。因为这些名称更唯一,所以这使得搜索循环变量变得更加容易,并有助于它们在循环变量中脱颖而出。更好的主意是使用 “真实” 变量名称,例如 countindex 或提供有关你正在计算的内容的更多详细信息的名称(例如 userCount)。最常见的循环变量类型称为计数器,它是一个循环变量,用于计算循环已执行的次数。

整型循环变量几乎总是应该有符号的,因为无符号整数可能会导致意外问题。请考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int main()
{
unsigned int count{ 10 }; // note: unsigned

// count from 10 down to 0
while (count >= 0)
{
if (count == 0)
{
std::cout << "blastoff!";
}
else
{
std::cout << count << ' ';
}
--count;
}

std::cout << '\n';

return 0;
}

事实证明,这个程序是一个无限循环。它首先根据需要打印 10 9 8 7 6 5 4 3 2 1 blastoff! ,但随后循环变量 count 溢出,并从 4294967295 开始倒计时(假设为 32 位整数)。为什么?因为循环条件 count >= 0 永远不会为 false!当 count 为 0 时,0 >= 0 为 true。然后执行 --count,并将 count 回绕回 4294967295。由于 4294967295 >= 0 为 true,因此程序继续进行。因为 count 是无符号的,所以它永远不能是负数,而且因为它永远不能是负数,所以循环不会终止。

每次执行循环时,它称为迭代。通常,我们希望每 2 次、第 3 次或第 4 次迭代做一些事情,例如打印换行符。这可以通过在我们的计数器上使用取余运算符轻松完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// Iterate through every number between 1 and 50
int main()
{
int count{ 1 };
while (count <= 50)
{
// print the number (pad numbers under 10 with a leading 0 for formatting purposes)
if (count < 10)
{
std::cout << '0';
}

std::cout << count << ' ';

// if the loop variable is divisible by 10, print a newline
if (count % 10 == 0)
{
std::cout << '\n';
}

// increment the loop counter
++count;
}

return 0;
}

输出:

1
2
3
4
5
01 02 03 04 05 06 07 08 09 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
41 42 43 44 45 46 47 48 49 50

也可以将循环嵌套在其他循环中。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

int main()
{
// outer loops between 1 and 5
int outer{ 1 };
while (outer <= 5)
{
// For each iteration of the outer loop, the code in the body of the loop executes once

// inner loops between 1 and outer
// note: inner will be created and destroyed at the end of the block
int inner{ 1 };
while (inner <= outer)
{
std::cout << inner << ' ';
++inner;
}

// print a newline at the end of each row
std::cout << '\n';
++outer;
} // inner destroyed here

return 0;
}

输出:

1
2
3
4
5
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5

9. do while 语句

考虑一下这样一种情况:我们想向用户显示一个菜单并要求他们进行选择——如果用户选择了无效的选择,则再次询问他们。显然,菜单和选择应该进入某种循环中(这样我们就可以一直询问用户,直到他们输入有效的输入),但是我们应该选择什么样的循环呢?

由于 while 循环会预先评估条件,因此这是一个尴尬的选择。我们可以像这样解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

int main()
{
// selection must be declared outside while-loop, so we can use it later
int selection{ 0 };

while (selection != 1 && selection != 2 &&
selection != 3 && selection != 4)
{
std::cout << "Please make a selection: \n";
std::cout << "1) Addition\n";
std::cout << "2) Subtraction\n";
std::cout << "3) Multiplication\n";
std::cout << "4) Division\n";
std::cin >> selection;
}

// do something with selection here
// such as a switch statement

std::cout << "You selected option #" << selection << '\n';

return 0;
}

但这之所以有效,是因为selection的初始值 0 不在有效值集(1、2、3 或 4)中。如果 0 是有效选择,该怎么办?我们必须选择一个不同的初始化器来表示 “invalid” —— 现在我们在代码中引入了魔术数字。我们可以添加一个新变量来跟踪有效性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

int main()
{
int selection { 0 };
bool invalid { true }; // new variable just to gate the loop

while (invalid)
{
std::cout << "Please make a selection: \n";
std::cout << "1) Addition\n";
std::cout << "2) Subtraction\n";
std::cout << "3) Multiplication\n";
std::cout << "4) Division\n";

std::cin >> selection;
invalid = (selection < 1 || selection > 4);
}

// do something with selection here
// such as a switch statement

std::cout << "You selected option #" << selection << '\n';

return 0;
}

它引入了一个新变量,只是为了确保循环运行一次,这增加了复杂性和出现其他错误的可能性。为了帮助解决上述问题,C++ 提供了 do-while 语句:

1
2
3
do
statement; // can be a single statement or a compound statement
while (condition);

do while 语句是一种循环结构,其工作方式与 while 循环类似,不同之处在于该语句始终至少执行一次。执行语句后,do-while 循环会检查条件。如果条件的计算结果为 true,则执行路径将跳回到 do-while 循环的顶部并再次执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

int main()
{
// selection must be declared outside of the do-while-loop, so we can use it later
int selection{};

do
{
std::cout << "Please make a selection: \n";
std::cout << "1) Addition\n";
std::cout << "2) Subtraction\n";
std::cout << "3) Multiplication\n";
std::cout << "4) Division\n";
std::cin >> selection;
}
while (selection < 1 || selection > 4);

// do something with selection here
// such as a switch statement

std::cout << "You selected option #" << selection << '\n';

return 0;
}

在实践中,do-while 循环并不常用。将条件放在循环的底部会掩盖循环条件,这可能会导致错误。

10. for 语句

到目前为止,C++ 中使用最多的循环语句是 for 语句。当我们有一个明显的循环变量时,for 语句(也称为 for 循环)是首选,因为它让我们可以轻松简洁地定义、初始化、测试和更改循环变量的值。从 C++11 开始,有两种不同类型的 for 循环。

for 语句抽象地看起来非常简单:

1
2
for (init-statement; condition; end-expression)
statement;

初步了解 for 语句工作原理的最简单方法是将其转换为等效的 while 语句:

1
2
3
4
5
6
7
8
{ // note the block here
init-statement; // used to define variables used in the loop
while (condition)
{
statement;
end-expression; // used to modify the loop variable prior to reassessment of the condition
}
} // variables defined inside the loop go out of scope here

for 语句分为 3 个部分进行计算:

  • 首先,执行 init-statement。这仅在启动循环时发生一次。init-statement 通常用于变量定义和初始化。这些变量具有“循环范围”,这实际上只是一种块范围的形式,其中这些变量从定义点一直存在于循环语句的末尾。

  • 其次,在每次循环迭代中,都会计算条件。如果此结果为 true,则执行该语句。如果此结果为 false,则循环终止,并继续执行循环之外的下一个语句。

  • 最后,在执行语句后,将计算 end-expression 。通常,此表达式用于递增或递减 init-statement 中定义的循环变量。计算完 end-expression 后,执行返回到第二步(并再次计算条件)。

当编写涉及值的循环条件时,我们通常可以用许多不同的方式编写条件。以下两个循环的执行方式相同:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
for (int i { 0 }; i < 10; ++i) // uses <
std::cout << i;

for (int i { 0 }; i != 10; ++i) // uses !=
std::cout << i;

return 0;
}

前者是更好的选择,因为即使 i 跳转超过值 10 它也会终止,而后者则不会。以下示例演示了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main()
{
for (int i { 0 }; i < 10; ++i) // uses <, still terminates
{
std::cout << i;
if (i == 9) ++i; // jump over value 10
}

for (int i { 0 }; i != 10; ++i) // uses !=, infinite loop
{
std::cout << i;
if (i == 9) ++i; // jump over value 10
}

return 0;
}

在 for 循环条件中进行数值比较时,避免使用 operator!=。尽可能首选 operator< 或 operator<=

新程序员在使用 for 循环(和其他利用计数器的循环)时遇到的最大问题之一是 差1错误(Off-by-one errors)。当循环迭代太多或 1 次以产生所需的结果时,会发生 差1错误。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main()
{
// oops, we used operator< instead of operator<=
for (int i{ 1 }; i < 5; ++i)
{
std::cout << i << ' ';
}

std::cout << '\n';

return 0;
}

这个程序应该打印 1 2 3 4 5,但它只打印 1 2 3 4,因为我们使用了错误的关系运算符。

可以编写省略任何或所有语句或表达式的 for 循环。例如,在以下示例中,我们将省略 init-statementend-expression,只保留 condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main()
{
int i{ 0 };
for ( ; i < 10; ) // no init-statement or end-expression
{
std::cout << i << ' ';
++i;
}

std::cout << '\n';

return 0;
}

输出:

1
0 1 2 3 4 5 6 7 8 9

以下示例生成了一个无限循环:

1
2
for (;;)
statement;

这可能有点出乎意料,因为您可能希望省略的 condition-expression 被视为 false。但是,C++ 标准明确(且不一致)定义 for 循环中省略的条件表达式应被视为 true。我们建议完全避免这种形式的 for 循环,而是使用 while (true)

尽管 for 循环通常只迭代一个变量,但有时 for 循环需要处理多个变量。为了帮助实现这一点,程序员可以在 init-statement 中定义多个变量,并可以使用逗号运算符来更改 end-expression 中多个变量的值:

1
2
3
4
5
6
7
8
9
#include <iostream>

int main()
{
for (int x{ 0 }, y{ 9 }; x < 10; ++x, --y)
std::cout << x << ' ' << y << '\n';

return 0;
}

这大约是 C++ 中唯一一个在同一语句中定义多个变量并且使用逗号运算符被认为是可接受的做法的地方。

与其他类型的循环一样,for 循环可以嵌套在其他循环中。在以下示例中,我们将一个 for 循环嵌套在另一个 for 循环中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
for (char c{ 'a' }; c <= 'e'; ++c) // outer loop on letters
{
std::cout << c; // print our letter first

for (int i{ 0 }; i < 3; ++i) // inner loop on all numbers
std::cout << i;

std::cout << '\n';
}

return 0;
}

输出:

1
2
3
4
5
a012
b012
c012
d012
e012

新程序员通常认为创建变量的成本很高,因此最好创建一次变量(然后为其赋值)而不是多次创建变量(并使用初始化)。这会导致循环看起来像下面的一些变体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
int i {}; // i defined outside loop
for (i = 0; i < 10; ++i) // i assigned value
{
std::cout << i << ' ';
}

// i can still be accessed here

std::cout << '\n';

return 0;
}

上面的示例使 i 在循环之外可用。除非需要在循环之外使用变量,否则在循环外定义变量可能会产生两个后果:

  • 它使我们的程序更加复杂,因为我们必须阅读更多代码才能看到变量的使用位置。

  • 它实际上可能会更慢,因为编译器可能无法有效地优化具有更大范围的变量。

与我们在尽可能小的合理范围内定义变量的最佳实践一致,仅在循环中使用的变量应在循环内部定义,而不是在循环外部定义。

11. break 和 continue

11.1 break 语句

break 语句会导致 while 循环、do-while 循环、for 循环或 switch 语句结束,并在循环或 switch 中断后继续执行下一条语句。

在循环的上下文中,可以使用 break 语句提前结束循环。在循环结束后继续执行 next 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

int main()
{
int sum{ 0 };

// Allow the user to enter up to 10 numbers
for (int count{ 0 }; count < 10; ++count)
{
std::cout << "Enter a number to add, or 0 to exit: ";
int num{};
std::cin >> num;

// exit loop if user enters 0
if (num == 0)
break; // exit the loop now

// otherwise add number to our sum
sum += num;
}

// execution will continue here after the break
std::cout << "The sum of all the numbers you entered is: " << sum << '\n';

return 0;
}

输出示例:

1
2
3
4
5
Enter a number to add, or 0 to exit: 5
Enter a number to add, or 0 to exit: 2
Enter a number to add, or 0 to exit: 1
Enter a number to add, or 0 to exit: 0
The sum of all the numbers you entered is: 8

break也是摆脱有意无限循环的常见方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main()
{
while (true) // infinite loop
{
std::cout << "Enter 0 to exit or any other integer to continue: ";
int num{};
std::cin >> num;

// exit loop if user enters 0
if (num == 0)
break;
}

std::cout << "We're out!\n";

return 0;
}

break 语句终止 switch 或 loop,并在 switch 或 loop 之后的第一个语句继续执行。return 语句终止循环所在的整个函数,并在调用函数的位置继续执行。

11.2 continue 语句

continue 语句提供了一种在不终止整个循环的情况下结束循环的当前迭代的便捷方法。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main()
{
for (int count{ 0 }; count < 10; ++count)
{
// if the number is divisible by 4, skip this iteration
if ((count % 4) == 0)
continue; // go to next iteration

// If the number is not divisible by 4, keep going
std::cout << count << '\n';

// The continue statement jumps to here
}

return 0;
}

此程序打印从 0 到 9 的所有不能被 4 整除的数字:

1
2
3
4
5
6
7
1
2
3
5
6
7
9

continue 语句的工作原理是使当前执行点跳转到当前循环的底部。

在 for 循环的情况下,for 循环的结束语句(在上面的示例中为 ++count)仍然在 continue 之后执行(因为这发生在循环体结束后)。

将 continue 语句与 while 或 do-while 循环一起使用时要小心。这些循环通常会更改循环体内条件中使用的变量的值。如果使用 continue 语句导致跳过这些行,则循环可能会变得无限!

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

int main()
{
int count{ 0 };
while (count < 10)
{
if (count == 5)
continue; // jump to end of loop body

std::cout << count << '\n';

++count; // this statement is never executed after count reaches 5

// The continue statement jumps to here
}

return 0;
}

该程序旨在打印 0 到 9 之间的每个数字,但 5 除外。但它实际上打印了:

1
2
3
4
5
0
1
2
3
4

当然,你已经知道,如果你有一个明显的 counter 变量,你应该使用 for 循环,而不是 while 或 do while 循环。

许多教科书告诫读者不要在循环中使用 break 和 continue,因为它会导致执行流跳来跳去,并且因为它会使逻辑流更难遵循。然而,如果使用得当,break 和 continue 可以通过减少嵌套块的数量并减少对复杂循环逻辑的需求来帮助提高循环的可读性。

请考虑以下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

int main()
{
int count{ 0 }; // count how many times the loop iterates
bool keepLooping { true }; // controls whether the loop ends or not
while (keepLooping)
{
std::cout << "Enter 'e' to exit this loop or any other character to continue: ";
char ch{};
std::cin >> ch;

if (ch == 'e')
keepLooping = false;
else
{
++count;
std::cout << "We've iterated " << count << " times\n";
}
}

return 0;
}

该程序使用 Boolean 变量来控制循环是否继续,以及仅在用户不退出时运行的嵌套块。下面是一个更容易理解的版本,使用 break 语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main()
{
int count{ 0 }; // count how many times the loop iterates
while (true) // loop until user terminates
{
std::cout << "Enter 'e' to exit this loop or any other character to continue: ";
char ch{};
std::cin >> ch;

if (ch == 'e')
break;

++count;
std::cout << "We've iterated " << count << " times\n";
}

return 0;
}

continue 语句最有效地用于 for 循环的顶部,以便在满足某些条件时跳过循环迭代。这可以让我们避免嵌套块。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
for (int count{ 0 }; count < 10; ++count)
{
// if the number is not divisible by 4...
if ((count % 4) != 0) // nested block
{
// Print the number
std::cout << count << '\n';
}
}

return 0;
}

我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
for (int count{ 0 }; count < 10; ++count)
{
// if the number is divisible by 4, skip this iteration
if ((count % 4) == 0)
continue;

// no nested block needed

std::cout << count << '\n';
}

return 0;
}

对于 return 语句,也有类似的参数。不是函数中最后一个语句的 return 语句称为 early return。当 early returns 简化函数的逻辑时,可以使用 early return。

12. halts(提前退出程序)

halt 是终止程序的流控制语句。在 C++ 中,halt 被实现为函数(而不是关键字),因此我们的 halt 语句将是函数调用。

std::exit() 是使程序正常终止的函数。正常终止意味着程序已按预期方式退出。请注意,术语正常并不意味着程序是否成功(这就是status code的用途)。例如,假设您正在编写一个程序,您希望用户键入要处理的文件名。如果用户键入了无效的文件名,则程序可能会返回非零status code以指示失败状态,但它仍将正常终止。

std::exit() 执行许多清理功能。首先,销毁具有静态存储持续时间的对象。然后,如果使用了任何文件,则执行一些其他杂项文件清理。最后,控制权返回给作系统,并将传递给 std::exit() 的参数用作status code

虽然 std::exit() 是在函数 main() 返回后隐式调用的,但 std::exit() 也可以显式调用,以便在程序正常终止之前停止程序。以这种方式调用 std::exit() 时,您需要包含 cstdlib 标头。

下面是显式使用 std::exit() 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cstdlib> // for std::exit()
#include <iostream>

void cleanup()
{
// code here to do any kind of cleanup required
std::cout << "cleanup!\n";
}

int main()
{
std::cout << 1 << '\n';
cleanup();

std::exit(0); // terminate and return status code 0 to operating system

// The following statements never execute
std::cout << 2 << '\n';

return 0;
}

输出:

1
2
1
cleanup!

请注意,调用 std::exit() 后的语句永远不会执行,因为程序已经终止。可以从任何函数调用 std::exit() 以在此时终止程序。

关于显式调用 std::exit() 的一个重要说明:std::exit() 不会清理任何局部变量(无论是在当前函数中,还是在调用堆栈中的函数中)。这意味着如果你的程序依赖于任何自我清理的局部变量,调用 std::exit() 可能会很危险。

C++ 提供了 std::atexit() 函数,该函数允许您指定一个函数,该函数将在程序终止时通过 std::exit() 自动调用。下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <cstdlib> // for std::exit()
#include <iostream>

void cleanup()
{
// code here to do any kind of cleanup required
std::cout << "cleanup!\n";
}

int main()
{
// register cleanup() to be called automatically when std::exit() is called
std::atexit(cleanup); // note: we use cleanup rather than cleanup() since we're not making a function call to cleanup() right now

std::cout << 1 << '\n';

std::exit(0); // terminate and return status code 0 to operating system

// The following statements never execute
std::cout << 2 << '\n';

return 0;
}

输出和之前的相同。

关于 std::atexit() 和清理函数的一些说明:首先,因为 std::exit() 在 main() 终止时被隐式调用,所以如果程序以这种方式退出,这将调用 std::atexit() 注册的任何函数。其次,被注册的函数必须不带参数,也不能有返回值。最后,如果需要,你可以使用 std::atexit() 注册多个清理函数,它们将按注册的相反顺序调用(最后一个注册的函数将首先调用)。

在多线程程序中,调用 std::exit() 可能会导致程序崩溃(因为调用 std::exit() 的线程将清理其他线程可能仍可访问的静态对象)。出于这个原因,C++引入了另一对函数,它们的工作方式类似于 std::exit() 和 std::atexit(),称为 std::quick_exit() 和 std::at_quick_exit()std::quick_exit() 正常终止程序,但不清理静态对象,并且可能会也可能不会进行其他类型的清理。std::at_quick_exit() 对于以 std::quick_exit() 结尾的程序,执行与 std::atexit() 相同的角色。

C++ 包含另外两个与 halt 相关的函数。std::abort() 函数会导致程序异常终止。异常终止意味着程序存在某种异常的运行时错误,并且程序无法继续运行。例如,尝试除以 0 将导致异常终止。std::abort() 不做任何清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cstdlib> // for std::abort()
#include <iostream>

int main()
{
std::cout << 1 << '\n';
std::abort();

// The following statements never execute
std::cout << 2 << '\n';

return 0;
}

仅当没有安全或合理的方法从 main 函数正常返回时,才使用 halt。如果您尚未禁用异常,则首选使用异常来安全地处理错误。

参考资料

Learn C++ – Skill up with our free tutorials