计算机本质上是个高级机械,所以它很擅长做不断重复的工作。而将枯燥乏味的各种计算工作交给计算机来办,是再合适不过的事了。
我们接下来将继续深入,探索 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
以外,我们还可以使用 while
与 do...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++ 程序中不可或缺的一部分。for
、while
与 do...while
三者为我们提供了条件,使得程序进行大规模运算成为了可能。借助于现代计算机强劲的性能,我们将能由此摆脱各种繁冗纷杂的计算。