第八章总结
本节阅读量:
章节回顾
CPU在程序中执行的特定语句序列称为程序的执行路径。顺序执行的程序每次运行时都会采用相同的路径。
控制流语句允许更改正常的执行路径。当控制流语句导致程序开始执行某个非顺序的指令序列时,这称为分支。
条件语句用于指定是否应执行某些关联语句。
if语句允许根据某个条件是否为真来执行关联语句。如果关联条件为false,则执行else语句。可以将多个if和else语句链接在一起。
当else语句应该连接到哪个if语句不明确时,就会出现悬空else。悬空else语句会与同一代码块中最后一个尚未匹配else的if语句匹配。可以通过将嵌套的if语句放入代码块中来避免悬空else。
空语句是仅由分号组成的语句。它什么也不做,通常用于语言标准要求此处必须存在语句,但程序员不需要执行任何操作的场景。
switch语句为在多个匹配项之间进行选择提供了一种更清晰、更快速的方法。switch语句仅适用于整型等可支持的类型。case标签用于标识要匹配的条件值。如果找不到匹配的case标签,则执行default标签下的语句。
当从标签下的语句延续到后续标签下的语句时,这称为fallthrough。break语句(或return语句)可用于防止fallthrough。[[fallthrough]]属性可用于标记有意的fallthrough。
goto语句允许程序向前或向后跳转到代码中的其他位置。通常应该避免使用它,因为这会产生意大利面条代码,意味着程序的执行路径类似于一碗纠缠在一起的意大利面条。
只要给定条件计算为true,循环就允许程序重复执行某些语句。在循环执行之前,会判断是否满足循环条件。
无限循环的循环条件始终为true。除非使用另一个控制流语句来停止它,否则它会永远循环。
循环变量(也称为计数器)是一个整数变量,用于统计循环已执行的次数。循环的每次执行都称为一次迭代。
do-while循环类似于while循环,但条件是在循环执行之后判断的。
for循环是最常用的循环。当需要循环特定次数时,它是理想选择。要注意,不要多迭代或少迭代一次。
break语句允许中断switch、while、do-while或for循环。continue语句允许立即进入下一次循环迭代。
退出允许我们终止程序。正常终止是指程序以预期方式退出(状态码会指示它是否成功)。在main的末尾会自动调用std::exit()。也可以显式调用它来终止程序。它会执行一些清理,但不会清理任何局部变量,也不会清理调用堆栈。
当程序遇到某种意外错误并必须关闭时,会发生异常终止。异常终止会调用std::abort。
算法是一个有限的指令序列,可以按照它来解决某些问题或产生一些有用的结果。如果算法在调用之间保留了一些信息,则认为该算法是有状态的。相反,无状态算法不存储任何信息(调用它时必须提供所需的所有信息)。用于算法时,术语“状态”指的是保存在有状态变量中的当前值。
如果对于给定输入(为启动提供的值),算法始终产生相同的输出序列,则该算法被认为是确定性的。
伪随机数生成器(PRNG)是一种模拟随机数序列生成的算法。当实例化PRNG时,可以提供称为随机种子(或简称种子)的初始值(或一组值)来初始化PRNG的状态。当用种子初始化PRNG时,我们说它已经被播种。种子值的大小可以小于PRNG状态的大小。当这种情况发生时,我们说PRNG的种子不足。PRNG输出结果开始自我重复之前的序列长度称为其周期。
随机数分布会将PRNG的输出转换为其他数字分布。均匀分布是一种在两个数字X和Y(包括X和Y)之间以相等概率产生输出的随机数分布。
习题
注意:从这里开始习题会变得更难,但你能做到的。让我们攻克这些习题!
问题 1
在「4.12 第4章总结」中,我们编写了一个模拟小球从塔顶落下的程序。由于当时还没有学过循环,小球只能下落5秒。
请对下面这个程序进行修改,让小球下落足够长的时间,直到它落到地面为止。并按已介绍的最佳实践(命名空间、constexpr等)更新程序。
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
#include <iostream>
// 从用户处获取塔的高度并返回
double getTowerHeight()
{
std::cout << "Enter the height of the tower in meters: ";
double towerHeight{};
std::cin >> towerHeight;
return towerHeight;
}
// 返回小球在"seconds"秒后的高度
double calculateBallHeight(double towerHeight, int seconds)
{
const double gravity { 9.8 };
// 使用公式:s = (u * t) + (a * t^2) / 2
// 这里 u(初速度)= 0,因此 (u * t) = 0
const double fallDistance { gravity * (seconds * seconds) / 2.0 };
const double ballHeight { towerHeight - fallDistance };
// 如果小球会落到地面以下,就把它放在地面上
if (ballHeight < 0.0)
return 0.0;
return ballHeight;
}
// 打印小球离地面的高度
void printBallHeight(double ballHeight, int seconds)
{
if (ballHeight > 0.0)
std::cout << "At " << seconds << " seconds, the ball is at height: " << ballHeight << " meters\n";
else
std::cout << "At " << seconds << " seconds, the ball is on the ground.\n";
}
// 计算小球当前的高度并打印出来
// 这是一个辅助函数,让调用更方便
void calculateAndPrintBallHeight(double towerHeight, int seconds)
{
const double ballHeight{ calculateBallHeight(towerHeight, seconds) };
printBallHeight(ballHeight, seconds);
}
int main()
{
const double towerHeight{ getTowerHeight() };
calculateAndPrintBallHeight(towerHeight, 0);
calculateAndPrintBallHeight(towerHeight, 1);
calculateAndPrintBallHeight(towerHeight, 2);
calculateAndPrintBallHeight(towerHeight, 3);
calculateAndPrintBallHeight(towerHeight, 4);
calculateAndPrintBallHeight(towerHeight, 5);
return 0;
}
|
查看答案
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
#include <iostream>
namespace Constants
{
constexpr double gravity { 9.8 };
}
// 从用户处获取塔的高度并返回
double getTowerHeight()
{
std::cout << "Enter the height of the tower in meters: ";
double towerHeight{};
std::cin >> towerHeight;
return towerHeight;
}
// 返回小球在"seconds"秒后的高度
// 注意:这个函数可以写成 constexpr,但我们还没有介绍 constexpr 函数
double calculateBallHeight(double towerHeight, int seconds)
{
// 使用公式:s = (u * t) + (a * t^2) / 2
// 这里 u(初速度)= 0,因此 (u * t) = 0
const double fallDistance { Constants::gravity * (seconds * seconds) / 2.0 };
const double ballHeight { towerHeight - fallDistance };
if (ballHeight < 0.0)
return 0.0;
return ballHeight;
}
// 打印小球离地面的高度
void printBallHeight(double ballHeight, int seconds)
{
if (ballHeight > 0.0)
std::cout << "At " << seconds << " seconds, the ball is at height: " << ballHeight << " meters\n";
else
std::cout << "At " << seconds << " seconds, the ball is on the ground.\n";
}
// 计算小球当前的高度并打印出来
// 这是一个辅助函数,让调用更方便
double calculateAndPrintBallHeight(double towerHeight, int seconds)
{
const double ballHeight{ calculateBallHeight(towerHeight, seconds) };
printBallHeight(ballHeight, seconds);
return ballHeight;
}
int main()
{
const double towerHeight{ getTowerHeight() };
int seconds { 0 };
while (calculateAndPrintBallHeight(towerHeight, seconds) > 0.0)
{
++seconds;
}
return 0;
}
|
问题 2
质数是大于1的自然数,它只能被1和自身整除(没有余数)。
请使用for循环编写isPrime()函数,补全下面的程序。如果正确,程序会打印「Success!」。
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
32
33
34
35
36
|
// 确保即使在 release 模式下编译,assert 也会触发
#undef NDEBUG
#include <cassert> // assert
#include <iostream>
bool isPrime(int x)
{
return false;
// 用 for 循环实现这个函数
}
int main()
{
assert(!isPrime(0)); // 如果 isPrime(0) 为真,则终止程序
assert(!isPrime(1));
assert(isPrime(2)); // 如果 isPrime(2) 为假,则终止程序
assert(isPrime(3));
assert(!isPrime(4));
assert(isPrime(5));
assert(isPrime(7));
assert(!isPrime(9));
assert(isPrime(11));
assert(isPrime(13));
assert(!isPrime(15));
assert(!isPrime(16));
assert(isPrime(17));
assert(isPrime(19));
assert(isPrime(97));
assert(!isPrime(99));
assert(isPrime(13417));
std::cout << "Success!\n";
return 0;
}
|
相关内容:
assert是一个预处理宏,当关联的实参求值为假时,它会终止程序。因此当我们写assert(!isPrime(0))时,意思就是「如果isPrime(0)为真,则终止程序」。我们会在「9.5 断言」中更详细地介绍assert。
查看答案
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// 确保即使在 release 模式下编译,assert 也会触发
#undef NDEBUG
#include <cassert> // assert
#include <iostream>
// 未优化的版本
bool isPrime(int x)
{
if (x <= 1) // 如果 x 为负数、0 或 1,则该数字不是质数
return false;
for (int test{ 2 }; test < x; ++test)
{
if (x % test == 0) // 如果 x 能被整除
return false; // 那么这个数字不是质数
}
return true; // 如果没有找到任何约数,那么 x 一定是质数
}
int main()
{
assert(!isPrime(0));
assert(!isPrime(1));
assert(isPrime(2));
assert(isPrime(3));
assert(!isPrime(4));
assert(isPrime(5));
assert(isPrime(7));
assert(!isPrime(9));
assert(isPrime(11));
assert(isPrime(13));
assert(!isPrime(15));
assert(!isPrime(16));
assert(isPrime(17));
assert(isPrime(19));
assert(isPrime(97));
assert(!isPrime(99));
assert(isPrime(13417));
std::cout << "Success!\n";
return 0;
}
|
附加题
上面解法中的for循环在两个方面不够理想:
- 它检查了偶数约数。除了2以外,我们不需要检查这些。
- 它对直到
x的每个数字都检查是否为约数。非质数(合数)必然有至少一个小于等于其平方根的约数,因此检查超过x平方根的约数是没有必要的。std::sqrt(x)(位于头文件中)返回x的平方根。
对于后一点,我们有两种选择:在循环之前先算出std::sqrt(x),然后用循环变量与该值比较;或者把比较式两边同时平方,从而把std::sqrt(x)从比较中彻底优化掉(感谢读者JJag的建议)。后面的解法采用后一种做法。
显示提示
给定两个变量a >= 0和b >= 0,可以把比较式a <= sqrt(b)的两边同时平方,比较关系依然成立。这样就得到a * a <= sqrt(b) * sqrt(b),它等价于a * a <= b。
请更新上面的解法,实现这两项优化。
查看答案
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// 确保即使在 release 模式下编译,assert 也会触发
#undef NDEBUG
#include <cassert> // assert
#include <iostream>
// 优化版本
bool isPrime(int x)
{
if (x <= 1) // 如果 x 为负数、0 或 1,则该数字不是质数
return false;
if (x == 2) // 2 是唯一的偶质数
return true;
if (x % 2 == 0) // 其他任何偶数都不是质数
return false;
// 对于大于等于 3 的数字,检查 3 到 sqrt(x) 之间的奇数是否为约数
// (这就是每次加 2 的原因)
// 参见 https://stackoverflow.com/questions/5811151/why-do-we-check-up-to-the-square-root-of-a-number-to-determine-if-the-number-is
// 由于 test >= 0 且 x >= 0,可以把 test < std::sqrt(x) 优化为 test * test < x
for (int test{ 3 }; test * test <= x; test += 2)
{
if (x % test == 0) // 如果 x 能被整除
return false; // 那么这个数字不是质数
}
return true; // 如果没有找到任何约数,那么 x 一定是质数
}
int main()
{
assert(!isPrime(0));
assert(!isPrime(1));
assert(isPrime(2));
assert(isPrime(3));
assert(!isPrime(4));
assert(isPrime(5));
assert(isPrime(7));
assert(!isPrime(9));
assert(isPrime(11));
assert(isPrime(13));
assert(!isPrime(15));
assert(!isPrime(16));
assert(isPrime(17));
assert(isPrime(19));
assert(isPrime(97));
assert(!isPrime(99));
assert(isPrime(13417));
std::cout << "Success!\n";
return 0;
}
|
问题 3
实现一个猜数字游戏(Hi-Lo)。首先,程序应当在1到100之间随机选择一个整数。用户有7次机会来猜这个数字。
如果用户没有猜对,程序应当告诉他们猜得过大还是过小。如果用户猜对了,程序应当告诉他们赢了。如果次数用完了,程序应当告诉他们输了,并告诉他们正确的数字是多少。游戏结束时,应当询问用户是否想再玩一次。如果用户输入的不是「y」或「n」,就再问一次。
本题假设用户输入的都是合法数字。
使用「8.14 全局随机数」中的 Random.h 头文件。
程序的输出应当像下面这样:
Let's play a game. I'm thinking of a number between 1 and 100. You have 7 tries to guess what it is.
Guess #1: 64
Your guess is too high.
Guess #2: 32
Your guess is too low.
Guess #3: 54
Your guess is too high.
Guess #4: 51
Correct! You win!
Would you like to play again (y/n)? y
Let's play a game. I'm thinking of a number between 1 and 100. You have 7 tries to guess what it is.
Guess #1: 64
Your guess is too high.
Guess #2: 32
Your guess is too low.
Guess #3: 54
Your guess is too high.
Guess #4: 51
Your guess is too high.
Guess #5: 36
Your guess is too low.
Guess #6: 45
Your guess is too low.
Guess #7: 48
Your guess is too low.
Sorry, you lose. The correct number was 49.
Would you like to play again (y/n)? q
Would you like to play again (y/n)? n
Thank you for playing.
附加题:把最小值、最大值和猜测次数做成可配置的形参。
查看答案
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
#include <iostream>
#include "Random.h" // https://www.learncpp.com/cpp-tutorial/global-random-numbers-random-h/
// 用户获胜返回 true,失败返回 false
// 本程序没有使用这个返回值,但保留它并不会增加复杂度,而且在将来(例如想把输赢信息移到函数外部,
// 或者根据输赢做其他处理时)可能会用得上
bool playHiLo(int guesses, int min, int max)
{
std::cout << "Let's play a game. I'm thinking of a number between " << min << " and " << max << ". You have " << guesses << " tries to guess what it is.\n";
const int number { Random::get(min, max) }; // 这是用户需要猜的数字
// 遍历所有猜测次数
for (int count{ 1 }; count <= guesses; ++count)
{
std::cout << "Guess #" << count << ": ";
int guess{};
std::cin >> guess;
if (guess > number)
std::cout << "Your guess is too high.\n";
else if (guess < number)
std::cout << "Your guess is too low.\n";
else // guess == number,用户猜对了
{
std::cout << "Correct! You win!\n";
return true;
}
}
// 用户失败
std::cout << "Sorry, you lose. The correct number was " << number << '\n';
return false;
}
bool playAgain()
{
// 不断询问用户是否想再玩一次,直到用户选择 y 或 n
while (true)
{
char ch{};
std::cout << "Would you like to play again (y/n)? ";
std::cin >> ch;
switch (ch)
{
case 'y': return true;
case 'n': return false;
}
}
}
int main()
{
constexpr int guesses { 7 }; // 用户拥有这么多次猜测机会
constexpr int min { 1 };
constexpr int max { 100 };
do
{
playHiLo(guesses, min, max);
} while (playAgain());
std::cout << "Thank you for playing.\n";
return 0;
}
|
我们会在「9.6 第9章总结」中为这个解法加上错误处理。