循环语句:把重复的活儿丢给机器
计算机最擅长的事情就是不知疲倦地重复做同一件事。倒不如说,计算机就是由无穷无尽的数据存,取,不倦的判断0和1,然后循环的做他们,组成了我们的互联网世界!
人会累,我现在让你手动打印 100 行 "Hello",你只会说CharlieChen114514显然是脑子进水了,但计算机只需要一条循环指令就能搞定。循环语句让我们告诉程序"把这个动作重复 N 遍"或者"一直做,直到某个条件满足为止"——这是几乎所有有意义程序的核心结构。
我们最熟悉的 while 循环:不知道次数就一直做
while 循环是最直白的循环结构:先检查条件,条件为真就执行循环体,执行完再回来检查,直到条件为假才停下来。
while (condition) {
// 循环体
}每次进入循环体之前先算一次 condition,结果是 true 就执行大括号里的代码,执行完毕后回到条件再判断。如果一开始条件就是 false,循环体一次都不会执行。
什么时候用 while?最典型的场景就是"我们事先不知道要循环多少次,但是肯定知道退出的条件是什么的场景"。比如让用户不断输入数字累加,直到输入 0 为止:
展开代码收起代码共 22 行
#include <iostream>
int main()
{
int sum = 0;
int value = 0;
std::cout << "请输入数字(输入 0 结束): ";
std::cin >> value;
// 嗯,发生啥咱们不知道,但是咱们知道
// 要是咱们的 value 不是 0了,循环就要退役了。
while (value != 0) {
sum += value;
std::cout << "当前累加和: " << sum << std::endl;
std::cout << "请继续输入(0 结束): ";
std::cin >> value;
}
std::cout << "最终结果: " << sum << std::endl;
return 0;
}咱们编译一下上面的小程序,运行效果看起来就是这样的:
请输入数字(输入 0 结束): 10
当前累加和: 10
请继续输入(0 结束): 25
当前累加和: 35
请继续输入(0 结束): 0
最终结果: 35循环体里面必须有能改变条件的操作(这里是我们每次重新读入 value),否则就会变成死循环。
不是意图内的死循环是 while 最常见的坑。如果循环体里没有任何操作能让条件变成 false,程序就会一直跑下去永远不退出。比如忘了写 std::cin >> value; 这一行,value 永远不变,条件永远为真。写 while 循环的时候,养成习惯检查"循环体里有没有改变条件的代码"。
do-while 循环:先做一次再说的 while 循环变体
do-while 和 while 很像,只有一个关键区别:循环体至少执行一次。条件检查放在循环体之后:
do {
// 循环体
} while (condition); // 注意这里有个分号!因为"先做后判"的特性,do-while 特别适合菜单系统这种场景——菜单至少得显示一次,然后根据用户选择决定是否继续:
int choice = 0;
do {
std::cout << "\n=== 菜单 ===" << std::endl;
std::cout << "1. 打印问候 0. 退出" << std::endl;
std::cout << "请选择: ";
std::cin >> choice;
if (choice == 1) {
std::cout << "你好!欢迎学习 C++!" << std::endl;
}
} while (choice != 0);do-while 末尾的分号千万别忘。漏掉的话,编译器会把下一行代码当成 while 的循环体来解析,报错信息可能非常诡异。这是 C++ 里少数几个必须在 } 后面加分号的地方之一,和 if、while、for 都不一样,很容易搞混。
PS,可能有朋友见过
do { /* 一些代码 */ } while(0);这种代码宏,在C里非常常见,但是笔者要说明的是,在今天的编译器的环境下,这个写法纯粹多余。函数完全可以替代。
另外一位旗鼓相当的选择:for 循环
当循环次数已知的时候,for 循环是最清晰的选择。它把初始化、条件判断和递增操作都集中在一行里,一眼就能看出循环的范围:
for (init; condition; increment) {
// 循环体
}执行顺序是:先执行一次 init,然后检查 condition,为真就执行循环体,执行完做 increment,再回去检查 condition,如此往复。
for (int i = 1; i <= 10; ++i) {
// 这个输出这样读:从 i = 1 开始,一直执行到i不再满足 i <= 10 这个关系,逼近的方式是i自增1.
std::cout << i << " ";
}
// 注意,出了这个for循环,i 就没办法使用了!
// 输出: 1 2 3 4 5 6 7 8 9 10for 还支持同时操作多个变量,用一个经典的双指针翻转来演示:
int data[] = {1, 2, 3, 4, 5};
int n = 5;
// 双指针从两端向中间走,交换元素
for (int i = 0, j = n - 1; i < j; ++i, --j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
// data 现在是 {5, 4, 3, 2, 1}初始化部分声明了 i 和 j 两个变量,递增部分同时做 ++i 和 --j,从两端向中间逼近,相遇时停止。
差一错误(off-by-one error)是 for 循环最经典的坑。本意是循环 10 次,写成 for (int i = 1; i < 10; ++i) 就只跑了 9 次。一个实用建议:养成固定习惯。您有两个范式选。
- 要么总是从 0 开始用
<(for (int i = 0; i < n; ++i)) - 要么从 1 开始用
<=(for (int i = 1; i <= n; ++i))
不要混用,混用就是差一错误的温床。
循环中间的确需要打断了:那就用"紧急出口" break 和 continue
break 立即跳出当前循环,不再判断条件,就跟break的含义一样——打破我们的循环!;continue 跳过当前轮次剩余代码,直接进入下一轮迭代。
int data[] = {4, 7, 2, 9, 5, 1};
int target = 9;
for (int i = 0; i < 6; ++i) {
if (data[i] == target) {
std::cout << "找到 " << target << ",下标为 " << i << std::endl;
break; // 找到了,不用继续搜了
}
}
// 输出: 找到 9,下标为 3continue 的例子——打印 1 到 20 之间的奇数:
for (int i = 1; i <= 20; ++i) {
if (i % 2 == 0) {
continue; // 偶数跳过
}
std::cout << i << " ";
}
// 输出: 1 3 5 7 9 11 13 15 17 19注意 break 只能跳出最内层的循环。嵌套两层时,内层的 break 只会跳出内层,外层该怎么转还怎么转。想一次性跳出多层循环,通常用一个标志变量配合外层条件判断,或者把逻辑封装成函数用 return 退出。
break 和 continue 用多了会让代码逻辑变得支离破碎,读代码的人得在脑子里跳来跳去地追踪执行流程。如果一个循环体里有超过两三个 break 或 continue,就该考虑是不是该把循环条件写得更清晰,或者把部分逻辑抽成单独的函数。简单直接的循环条件永远比到处 break 更容易维护。
第五步——嵌套循环:循环里的循环
循环体里面可以再放一个循环,它解决的是"对每一行做 X,每一行里的每一列做 Y"这类二维问题。来看经典的九九乘法表:
#include <iostream>
#include <iomanip> // std::setw
int main()
{
for (int i = 1; i <= 9; ++i) {
for (int j = 1; j <= i; ++j) {
std::cout << j << "x" << i << "=" << std::setw(2) << i * j << " ";
}
std::cout << std::endl;
}
return 0;
}运行结果:
1x1= 1
1x2= 2 2x2= 4
1x3= 3 2x3= 6 3x3= 9
1x4= 4 2x4= 8 3x4=12 4x4=16
...
1x9= 9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81外层循环控制行号 i,内层循环控制列号 j,j 从 1 遍历到 i,打印出来是三角形。std::setw(2) 让输出项占 2 个字符宽度,一位数和两位数能对齐。
嵌套循环的执行次数是各层循环次数的乘积。外层 N 次、内层 M 次,总共 N * M 次内层循环体。对于 N=1000 的双层嵌套,内层要执行一百万次——所以心里要有这个概念:数据量大的时候,嵌套层数越少越好。
完整实战——loops.cpp
我们把前面学的几种循环综合到一个程序里:九九乘法表、猜数字小游戏(while + break)、金字塔图案打印(嵌套 for)。
展开代码收起代码共 71 行
// loops.cpp -- 综合循环练习
// 编译: g++ -Wall -Wextra -o loops loops.cpp
#include <iostream>
#include <iomanip>
/// @brief 打印九九乘法表
void print_multiplication_table()
{
std::cout << "=== 九九乘法表 ===" << std::endl;
for (int i = 1; i <= 9; ++i) {
for (int j = 1; j <= i; ++j) {
std::cout << j << "x" << i << "=" << std::setw(2) << i * j << " ";
}
std::cout << std::endl;
}
}
/// @brief 猜数字游戏,演示 while + break 的配合
void guess_number_game()
{
const int kSecret = 42;
int guess = 0;
int attempts = 0;
std::cout << "\n=== 猜数字游戏 ===" << std::endl;
std::cout << "我想了一个 1-100 之间的数字,你来猜!" << std::endl;
while (true) {
std::cout << "你的猜测: ";
std::cin >> guess;
++attempts;
if (guess == kSecret) {
std::cout << "恭喜!你用了 " << attempts << " 次猜中了!" << std::endl;
break;
} else if (guess < kSecret) {
std::cout << "太小了,再试试。" << std::endl;
} else {
std::cout << "太大了,再试试。" << std::endl;
}
}
}
/// @brief 打印由星号组成的金字塔
void print_pyramid()
{
const int kHeight = 5;
std::cout << "\n=== 金字塔图案 ===" << std::endl;
for (int row = 1; row <= kHeight; ++row) {
// 打印前导空格
for (int space = 0; space < kHeight - row; ++space) {
std::cout << " ";
}
// 打印星号(第 row 行有 2*row - 1 个星号)
for (int star = 0; star < 2 * row - 1; ++star) {
std::cout << "*";
}
std::cout << std::endl;
}
}
int main()
{
print_multiplication_table();
guess_number_game();
print_pyramid();
return 0;
}编译运行:
g++ -Wall -Wextra -o loops loops.cpp
./loops=== 九九乘法表 ===
1x1= 1
1x2= 2 2x2= 4
...(中间省略)
1x9= 9 2x9=18 ... 9x9=81
=== 猜数字游戏 ===
你的猜测: 50
太大了,再试试。
你的猜测: 25
太小了,再试试。
你的猜测: 42
恭喜!你用了 3 次猜中了!
=== 金字塔图案 ===
*
***
*****
*******
*********来拆一下金字塔的逻辑。第 row 行需要 kHeight - row 个前导空格让星号居中,然后打印 2 * row - 1 个星号。这个 2n-1 的规律在图案打印中非常常见。猜数字游戏里的 while (true) + break 也是一种经典模式——当退出条件不容易浓缩成一个布尔表达式时,在循环体内部判断然后 break 是一种清晰的做法。
在线运行
在线运行综合循环示例,观察九九乘法表、金字塔图案和素数筛选的输出:
Compiler Explorer
循环语句综合演示:乘法表、金字塔、素数
在线运行并观察 for 循环、嵌套循环和 break 的综合运用。试着修改 kHeight 或素数范围。
动手试试
光看懂不够,得自己写一遍才算真的会。以下是四个练习,建议每个都动手完成。
练习一:打印空心正方形
输入一个正整数 N,打印一个 N x N 的空心正方形。比如 N=5 时:
* * * * *
* *
* *
* *
* * * * *只有第一行、最后一行、第一列和最后一列打印星号,中间全是空格。提示:用嵌套 for 循环,内层判断当前是不是边界位置。
参考答案
展开代码收起代码共 29 行
#include <iostream>
int main()
{
int n = 0;
std::cout << "输入一个正整数 N: ";
if (!(std::cin >> n) || n < 1)
{
std::cout << "输入无效,请输入一个正整数!" << std::endl;
return 1;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if ((i != 1) && (i != n))
{
if ((j != 1) && (j != n))
{
std::cout << " ";
continue;
}
}
std::cout << " * ";
}
std::cout << std::endl;
}
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
输入一个正整数 N: 5
* * * * *
* *
* *
* *
* * * * *练习二:计算阶乘
用 for 循环计算 N 的阶乘(N!)。比如 5! = 120。注意阶乘增长极快,用 int 的话 13! 就会溢出,可以试试 long long 能撑到多大。
参考答案
展开代码收起代码共 21 行
#include <iostream>
int main()
{
int n = 0;
long long factorial = 1;
std::cout << "输入一个正整数 N: ";
if (!(std::cin >> n) || n < 1)
{
std::cout << "输入无效,请输入一个正确的正整数!" << std::endl;
return 1;
}
for (int i = n; i >= 1; i--)
{
factorial *= i;
}
std::cout << n << "的阶乘" << "(" << n << "!): " << factorial << std::endl;
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
输入一个正整数 N: 5
5的阶乘(5!): 120练习三:找素数
输入一个正整数 N,打印从 2 到 N 之间所有的素数。判断素数的方法:对于数 m,检查 2 到 m-1 之间有没有能整除 m 的数,如果没有就是素数。提示:外层循环遍历候选数,内层循环做整除检查,找到因子后用 break 提前退出内层。
参考答案
展开代码收起代码共 33 行
#include <iostream>
int main()
{
int n = 0;
bool flag=0;
std::cout << "输入一个正整数 N: ";
if (!(std::cin >> n) || n < 2)
{
std::cout << "输入无效,请输入一个正确的正整数!" << std::endl;
return 1;
}
std::cout << "2" << "到" << n << "之间所有的素数: " << std::endl;
for (int i = 2; i <= n; i++)
{
flag = 0;
for (int j = 2; j <= i - 1; j++)
{
if (i % j == 0)
{
flag = 1;
break;
}
}
if (flag != 1)
{
std::cout << i << std::endl;
}
}
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
输入一个正整数 N: 5
2到5之间所有的素数:
2
3
5练习四:打印菱形
输入一个奇数 N,打印一个 N 行的菱形图案。比如 N=5 时:
*
***
*****
***
*提示:上半部分和金字塔一样,下半部分是金字塔的镜像——行号从大到小。
参考答案
展开代码收起代码共 36 行
#include <iostream>
int main()
{
int n = 0;
std::cout << "输入一个正奇数 N: ";
if (!(std::cin >> n) || n <= 0 || n % 2 == 0)
{
std::cout << "输入无效,请输入一个正确的正奇数!" << std::endl;
return 1;
}
const int middleRow = (n + 1) / 2;
for (int row = 1; row <= n; ++row)
{
const int stars = row <= middleRow
? 2 * row - 1
: 2 * (n - row) + 1;
const int spaces = (n - stars) / 2;
for (int column = 0; column < spaces; ++column)
{
std::cout << ' ';
}
for (int column = 0; column < stars; ++column)
{
std::cout << '*';
}
std::cout << std::endl;
}
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
输入一个正奇数 N: 5
*
***
*****
***
*