Skip to content

Latest commit

 

History

History
219 lines (151 loc) · 7.79 KB

4-3.md

File metadata and controls

219 lines (151 loc) · 7.79 KB

对重复操作的化简 · 迭代语句

计算机本质上是个高级机械,所以它很擅长做不断重复的工作。而将枯燥乏味的各种计算工作交给计算机来办,是再合适不过的事了。

我们接下来将继续深入,探索 C++ 是如何做到循环操作的。


先来尝试一道简单的数学题:

求 1 + 2 + 3 + ... + n 的结果,其中 n 是一个正整数。

如果知道 n 是几,我们也许可以这么写,简单粗暴:

// 设 n 为 10
cout << 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 << endl;

但如果我们事先不知道 n 的值呢?

此时,就该 for 上场了:

int n, sum = 0;		// n 为输入的数字,sum 为要输出的累加数值
cin >> n;
for (int i = 1; i <= n; ++i)
{
    // 这对花括号内的内容将执行 n 遍
    sum = sum + i;	// 在第 i 遍时,将 i 累加到 sum 上
}
cout << sum << endl;

for 与先前的 if 有些相似,其后也跟着一对小括号,且小括号后也可跟单独一条语句或一对花括号组成的语句块。

它的格式如下:

// 第一种
for (<初始化语句>; <循环条件>; <迭代操作>) <满足条件时执行的语句>;

// 第二种
for (<初始化语句>; <循环条件>; <迭代操作>)
{
    <语句1>
    <语句2>
    ...
}

小括号内的东西有点多,我们逐个分析。

首先是初始化语句。你可以在这里定义一个或多个同类型的变量。例如,定义新变量int i = 0 与赋值 ch1 = 'a', ch2 = 'b' 均是可以的。

再之后是循环条件。这里写的内容与 if 后跟的条件是一样的,是一个最终运算结果为布尔值的表达式。这个条件的运算结果将决定循环是否(再次)执行——为 true 时将执行一遍循环体,并进入下一轮判断;为 false 时则结束循环,执行循环体之后的内容。

最后的迭代操作可以是任何表达式,它在每一轮循环的结尾都会执行一次。很多时候,我们都会在此写诸如 i++j -= 2 这类自增或自减的表达式。

我们再拿刚刚的例子具体剖析:

for (int i = 1; i <= n; ++i)

第一部分很好理解,声明了个变量 i 并初始化为 1。值得注意的是,在 for 语句的小括号内创建的变量仅在 for 语句的循环体内有效,出了这个范围便无法使用这类变量:

for (int i = 1; i <= n; ++i)
{
    cout << i << endl;		// 正确,可以使用
}
cout << i << endl;			// 错误,无法使用由 for 声明的变量 i

第二部分与第三部分一起看,可以发现 i 由此可以从 1 数到 n

稍微变一下刚刚的例子:

for (int i = 1; i < n; ++i)

注意第二部分的小于等于改成了小于。这么做会导致其改为从 1 数到 n - 1,而不是数到 n

为什么呢?我们不妨设想一下第 n - 1 次循环执行结束时的场景。在循环的末尾,先执行了 ++i,使 i 从原本的 n - 1 变为 n,然后进入下一轮循环前的判断。由于 i 现在的值是 n,已不满足 i < n 这个条件,便不再执行第 n 次循环,而继续执行循环体外的部分。而修改前的条件 i <= n 可以接受 i 等于 n 时的情况,所以可以执行第 n 次循环。

另外,for 的小括号中三个部分均可省略,例如:

for (; tag == 's';) ...;	// 只有条件
for (;; count += 3) ...;	// 只有迭代动作,无限循环
for (;;) ...;				// 什么都不写,无限循环

哪怕三个部分一个都不写,也必须写两个分号占位。

有关无限循环的内容,我们会在后文的跳转语句部分具体了解。


除了 for 以外,我们还可以使用 whiledo...while 做到循环。相比较于 for 来讲,这两者更纯粹、更简单,因为它们只需要提供判断条件即可,不需要初始化与迭代操作。

我们先来看一下 while

// 第一种
while (<条件>) <满足条件时执行的语句>;

// 第二种
while (<条件>)
{
    <语句1>
    <语句2>
    ...
}

当程序执行到此处时,会先检查条件。条件满足时,才会进行第一次循环,否则会连第一次循环都不执行,转而执行循环体外的下一条语句。之后便是检查条件与执行循环体的交替过程,条件失效时便不再执行循环体。

它其实也可以看作是 for 的基础版。如果我们将 for 改为以 while 实现,将会是这样的:

// for 形式
for (int i = 0; i < n; ++i) cout << i << endl;
// while 形式
int i = 0;
while (i < n)
{
    cout << i << endl;
    ++i;
}

很明显,用 while 实现与 for 类似的功能更是费劲,而且也将原本仅在循环体内有效的变量 i 拿了出来,导致循环体外也能访问到变量 i 了。是否需要这么做,还需要看具体需求。


我们再来看一下 do...while

// 第一种
do <满足条件时执行的语句> while (<条件>);

// 第二种
do
{
    <语句1>
    <语句2>
    ...
} while (<条件>);

while 不同的是,不论条件怎样,do...while 总是会先执行一次循环体,然后再作判断。当条件不满足时仍会结束循环。

如果不好理解,你可以想象它是先无条件执行了一遍循环体,然后再执行同样条件的 while 循环:

<循环体...>
while (<条件>) <循环体...>;

尤其需要注意的是,do...while 的末尾一定要加分号,切记!


循环与什么最搭配呢?数组这类东西当然是最适合的了!

数组的下标是从 0 开始数起的一系列自然数,而 for 很是擅长数下标:

int a[5] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; ++i) cout << a[i] << endl;

我们可以看到,这段程序将 i 从 0 数到 4,正好用在了选取数组 a 的共 5 个元素上。

是否有更简单的办法呢?有,方法是使用范围 for

for (auto n : a) cout << n << endl;

看起来是否特别简洁?

这便是范围 for,它的格式如下:

for (<变量类型> <变量名> : <可被迭代的对象>) ...

变量名可以写成值传递的形式,也可以写成引用传递的形式。我们来看两个例子:

// 值传递
for (auto n : a) n *= 2;
// 执行结束后,a = { 1, 2, 3, 4, 5 },没有任何变化

// 引用传递
for (auto &n : a) n *= 2;
// 执行结束后,a = { 2, 4, 6, 8, 10 },每一个元素均被修改

回忆一下先前对引用的知识,引用本质上就是对象的别名。如此一来,引用传递的意思,其实就相当于将所取得的那个元素本身传递过来,于是对此变量的各种修改也同样作用在了对应的各个元素。

值传递很好理解。在值传递模式下,每个元素都会被拷贝一次,我们对此变量的各种修改仅仅是相当于修改了对应元素的副本。因此,我们不论作出如何的修改,都不会影响到对应的元素本身。

最后再来看一下可被迭代的对象。除了数组之外,它还支持对列表初始化得到的常量列表进行遍历:

for (auto ch: { 'm', 'i', 'k', 'u' }) cout << ch;
cout << endl;

在后续章节中,我们还将了解由标准库提供的许多容器,它们也可以用在范围 for 上。


循环同样是 C++ 程序中不可或缺的一部分。forwhiledo...while 三者为我们提供了条件,使得程序进行大规模运算成为了可能。借助于现代计算机强劲的性能,我们将能由此摆脱各种繁冗纷杂的计算。