第 08 章

循环:for、while、do-while 的五个组成部分

这一章把循环拆成初始化、起点、测试、推进与循环体五部分,讲清 for 的执行顺序与迭代器作用域,再比较 while 与 do-while 在「先判断」和「先执行」上的差别。

Loops: the Five Parts of for, while, and do-while

This chapter splits every loop into initializer, start, test, advance, and body; it traces the order in which a for loop runs, explains iterator scope, and contrasts while with do-while on "test first" versus "run first".

首发于 2022.02.17中 / EN
系统课程cppcontrol-flowloopsfor-loopwhile-loopsize-t
语言
字号
目录
篇内目录

这一章在讲什么

这是 31 小时长视频的第 8 章,对应 07:53:35–08:48:27。

上一章解决「走哪条路」,这一章解决「重复多少次」。开场的问题很直白:把一句话打印 10 遍,复制十行也能做到;打印一万遍呢?循环就是把「重复」从手工劳动变成一条语句。

本章讲三种循环:for、while、do-while。讲者反复强调一个重要观点:所有循环都由五个部分组成——迭代器、起点、测试、推进方式、循环体。三种语法的区别只是这五部分写在哪里。

时间轴

时间内容
07:53:35动机:打印 10 次、10000 次、100000 次的区别
07:55:19for 的语法与五个组成部分
07:58:08逐步跟踪:循环为什么正好执行 10 次
08:01:04迭代器类型:unsigned int 与 size_t
08:03:27迭代器的作用域;省略花括号的条件
08:04:35把迭代器声明在循环外,使其在循环后仍可用
08:05:27不要硬编码次数:把上限放进 count 变量
08:06:28VS Code 实操:for 循环与 sizeof(size_t)
08:16:51作用域实验:在循环外使用 i 的编译错误
08:25:18while 循环的语法与执行顺序
08:29:21VS Code 实操:while 版本
08:36:42do-while:先执行、后测试
08:45:32do-while 的陷阱:次数为 0 时仍执行一次

for:把五个部分写在一行

for (size_t i {0}; i < 10; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}

括号里用分号分成三段:

  1. 初始化:size_t i {0} 声明迭代器并给起点。只在进入循环前执行一次。
  2. 测试:i < 10 控制何时结束。每次进入循环体之前检查。
  3. 推进:++i 在每次循环体结束后执行,让迭代器向终点移动。

循环体是 {} 之间的代码。这就是讲者所说的五个部分:迭代器、起点、测试、推进、循环体。

执行顺序

这是初学者最该动手推演一遍的地方:

  1. 执行初始化 i = 0(只做一次)。
  2. 检查测试 0 < 10,成立,进入循环体。
  3. 循环体执行完,做推进 ++i,i 变成 1。
  4. 回到第 2 步,检查 1 < 10……

当 i 变成 10 时,测试 10 < 10 不成立,循环结束,控制流落到循环之后。因此循环体执行了 10 次,i 依次取 0 到 9——不是 1 到 10,也不是 11 次。

输出:

0 : I love C++
1 : I love C++
...
9 : I love C++

把测试改成 i < 100,输出立刻变成 0 到 99;循环的代码长度不变,只有边界变了。这正是循环的价值。

迭代器用哪种整数类型

三种常见选择:

  • unsigned int:能表示非负整数,够用但不保证足够大。
  • size_t:标准库用来表示「大小」的无符号类型,sizeof(size_t) 在本机是 8 字节。
  • int:有符号,写循环时容易和 size() 之类返回无符号值的函数产生警告。

讲者的建议是用 size_t,因为标准库的下标就是它,混用时不会触发符号不匹配的警告。需要注意的是它是无符号的:给它赋负数会回绕成一个极大的正数,循环条件将长期成立,形成事实上的死循环。

std::cout << "sizeof(size_t) : " << sizeof(size_t) << std::endl;

本机输出 8,说明它可以表示 0 到约 1.8×10191.8\times10^{19}。

迭代器的作用域

在 for 的初始化部分声明的迭代器,作用域只限这条循环:

for (size_t i {0}; i < 10; ++i) {
    // i 可用
}
// 这里不能再用 i

循环结束后使用 i 会报错:

error: 'i' was not declared in this scope

如果确实需要在循环结束后保留最终值,有两种写法。把迭代器声明在外面:

size_t i {0};
for (i = 0; i < 10; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}
std::cout << "Loop done, i = " << i << std::endl;   // i 为 10

或者干脆省略初始化部分,只保留分号:

size_t j {0};
for (; j < 10; ++j) {
    // ...
}

两种写法等价。第二种在阅读时稍微隐蔽,讲者只作为「你可能在别人的代码里见到」来介绍。

不要硬编码循环次数

for (size_t i {0}; i < 10; ++i)   // 10 是硬编码

如果这个 10 在程序里出现很多次,改需求时就要到处找。更好的做法是把次数放进常量:

const size_t COUNT {10};
for (size_t i {0}; i < COUNT; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}

这样只有一个控制点。常量名用全大写是 C++ 的常见约定,不是语法要求。

while:先测试,再执行

while 把同样的五个部分拆开写:

const size_t COUNT {10};
size_t i {0};              // 迭代器与起点

while (i < COUNT) {        // 测试
    std::cout << i << " : I love C++" << std::endl;
    ++i;                   // 推进,放在循环体末尾
}

执行顺序与 for 完全一致:先测试,成立才进入循环体,循环体结束后回到测试。区别只是初始化、测试、推进分处三行。

一个必须警惕的差别:for 的推进会由语法强制你写在括号里,while 的推进藏在循环体内部。忘记写 ++i 时:

size_t i {0};
while (i < 10) {
    std::cout << i << " : I love C++" << std::endl;
    // 忘了 ++i
}

i 永远是 0,条件永远成立,程序无限打印——这是 while 最常见的死循环来源。

选择建议:循环次数已知、有明确的计数器时用 for;循环依赖某个状态、次数不确定时(例如读到文件末尾、等待某个条件)用 while。

do-while:先执行,再测试

const size_t COUNT {10};
size_t i {0};

do {
    std::cout << i << " : I love C++" << std::endl;
    ++i;
} while (i < COUNT);

语法上注意两点:while (...) 在花括号之后,并且整句以分号结尾。

语义上,do-while 与另外两种循环有一个本质差别:循环体至少执行一次,测试放在执行之后。正常次数为 10 时,三种循环的输出完全一样;差别只在「次数为 0」的边界上暴露。

把 COUNT 改成 0:

const size_t COUNT {0};
size_t i {0};

do {
    std::cout << i << " : I love C++" << std::endl;
    ++i;
} while (i < COUNT);

std::cout << "Loop done" << std::endl;

输出:

0 : I love C++
Loop done

条件 0 < 0 一开始就是假,但循环体已经先跑了一遍。如果本意是「次数为 0 时什么都不做」,do-while 就是错的,应改用 while 或 for。

do-while 真正合适的场景是「至少要做一次,然后根据结果决定要不要继续」,例如反复询问用户输入直到得到合法值。

一句话总结: 循环永远是「起点—测试—推进—循环体」四件事加一个迭代器;for 把它们挤在一行,while 先测后做,do-while 先做后测。

本章结论

  • 循环由五部分组成:迭代器、起点、测试、推进、循环体。
  • for (init; test; increment) 中初始化只执行一次;测试在每次循环体之前;推进在每次循环体之后。
  • i < 10 且从 0 开始,循环体执行 10 次,i 取 0 到 9。
  • 推荐用 size_t 作为循环下标;它是无符号类型,赋负值会回绕。
  • for 中声明的迭代器只在该循环内可见;需要循环后取值就声明在外部。
  • 循环次数应放进常量,避免硬编码。
  • while 先测试后执行;do-while 先执行后测试,因此至少执行一次。

理解检验

1. 写出 for (size_t i {0}; i < 10; ++i) 的五部分,并说明为什么循环体执行 10 次而不是 9 次或 11 次。

参考答案:迭代器是 i,起点是 0,测试是 i < 10,推进是 ++i,循环体是花括号里的语句。初始化只执行一次,之后按「测试—循环体—推进」循环。i 依次取 0、1、…、9:每次测试都成立,循环体执行一次;当 i 变为 10 时测试 10 < 10 为假,循环结束。因此循环体执行了 10 次,i 的取值是 0 到 9,共 10 个不同的值。

2. size_t 与 int 做循环下标有什么区别?把 -1 赋给 size_t 会发生什么?

参考答案:size_t 是无符号类型(本机 8 字节),只能表示非负值,标准库用它表示大小与下标;int 是有符号类型。把 -1 赋给 size_t 时会发生无符号回绕,变成该类型的最大值(64 位上约为 1.8×10191.8\times10^{19})。如果循环条件是 i < COUNT,这样的值会让条件长期成立,循环几乎不会结束。所以无符号下标要么保证非负,要么在递减循环里格外小心,避免走到 0 以下。

3. 在 for 的初始化部分声明的迭代器,作用域到哪里结束?如果循环结束后还需要它的值,应该怎么写?

参考答案:作用域从声明处到循环结束(包括测试与推进部分),循环之后的代码看不到它,使用会报 'i' was not declared in this scope。若循环后仍需要该值,可以在循环外声明迭代器,循环的初始化部分改成赋值甚至留空:size_t i {0}; for (; i < COUNT; ++i) { ... },循环结束后 i 保留最终值(正常退出时等于 COUNT)。

4. while 循环为什么比 for 更容易写出死循环?给出一个具体例子。

参考答案:for 的推进部分固定在括号里,语法结构强制你面对它;while 的推进通常写在循环体末尾,很容易漏掉。例如

size_t i {0};
while (i < 10) {
    std::cout << i << " : I love C++" << std::endl;
    // 忘了 ++i
}

i 始终为 0,条件 0 < 10 恒成立,程序会无限输出。补上 ++i 即可。此外,如果测试依赖的状态在循环体内没有被修改,也会出现同样的问题。

5. 当循环次数设为 0 时,while 与 do-while 的行为有什么不同?各给出一个适合的场景。

参考答案:while 先测试,条件一开始为假就一次也不执行;do-while 先执行循环体、后测试,即使条件一开始为假也已经执行了一次。例如 COUNT = 0 时,while 版本只打印 Loop done,do-while 版本会先打印 0 : I love C++ 再打印 Loop done。适合 do-while 的场景是「至少做一次,再根据结果决定是否继续」,例如反复读取用户输入直到合法;适合 while 的场景是「次数可能为 0」,例如遍历一个可能为空的容器。

6. 为什么把循环上限写成 const size_t COUNT {10} 比直接写 10 更好?

参考答案:硬编码的 10 会把「循环几次」这个决策散落在代码各处,需求一变就要逐个查找替换,容易漏改。提取成常量后只有一处定义,改名或改值都只改一行;常量名本身也说明了这个数字的含义,比裸露的字面量更易读。编译器还能对 const 做优化,且 const 防止在循环里被意外修改。

与前后章的关系

第 7 章的条件分支决定「走哪条路」,这一章的循环决定「走多少遍」,两者合起来构成所有命令式程序的骨架。下一章(第 9 章)讲数组,会立刻用到循环来遍历和初始化元素;再往后,第 12 章的引用、第 13 章的字符串、第 17 章的模板,都会反复出现 for 与 size_t 的组合。

What this chapter is about

This is chapter 8 of a single 31-hour video, covering 07:53:35–08:48:27.

The previous chapter decided which path to take; this one decides how many times to repeat. The opening question is blunt: printing a message ten times is easy to copy by hand, but ten thousand times? A loop turns repetition from manual labour into one statement.

Three loops are covered: for, while, and do-while. The lecturer keeps returning to one idea: every loop has five parts — iterator, starting point, test, advance, and body. The three syntaxes differ only in where those parts are written.

Timeline

TimeTopic
07:53:35Motivation: printing 10 times, 10,000 times, 100,000 times
07:55:19for syntax and its five parts
07:58:08Step-by-step trace: why the loop runs exactly ten times
08:01:04Iterator types: unsigned int versus size_t
08:03:27Iterator scope; when the braces may be omitted
08:04:35Declaring the iterator outside so it survives the loop
08:05:27Do not hard-code the bound: store it in count
08:06:28Live coding: the for loop and sizeof(size_t)
08:16:51Scope experiment: the compile error from using i after the loop
08:25:18while syntax and execution order
08:29:21Live coding: the while version
08:36:42do-while: run first, test afterwards
08:45:32The do-while trap: it runs once even when the count is zero

for: all five parts on one line

for (size_t i {0}; i < 10; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}

The parentheses hold three sections separated by semicolons:

  1. Initializer: size_t i {0} declares the iterator and sets the start. Runs once, before the loop.
  2. Test: i < 10 decides when to stop. Checked before each iteration.
  3. Advance: ++i runs after each pass through the body, moving the iterator toward the end.

The body is the code between the braces. Those are the five parts: iterator, start, test, advance, body.

Execution order

This is the part every beginner should trace by hand:

  1. Run the initializer, i = 0 (only once).
  2. Evaluate the test 0 < 10; true, so enter the body.
  3. Finish the body, run the advance ++i, so i becomes 1.
  4. Go back to step 2 and test 1 < 10.

When i reaches 10, the test 10 < 10 is false, the loop ends, and control continues after it. So the body runs ten times and i takes the values 0 through 9 — not 1 through 10, and not eleven times.

Output:

0 : I love C++
1 : I love C++
...
9 : I love C++

Change the test to i < 100 and the output becomes 0 through 99; the loop code stays the same length and only the bound moves. That is the value of a loop.

Which integer type for the iterator

Three common choices:

  • unsigned int: represents non-negative values, convenient but not guaranteed to be large enough.
  • size_t: the standard library's unsigned type for sizes; sizeof(size_t) is 8 bytes on this machine.
  • int: signed, and prone to sign-mismatch warnings when combined with functions returning unsigned sizes.

The lecturer recommends size_t, since standard-library indices use it and mixing types then raises no sign warnings. Remember it is unsigned: assigning a negative value wraps around to a huge positive one, so a i < COUNT test stays true for a very long time, which is effectively an infinite loop.

std::cout << "sizeof(size_t) : " << sizeof(size_t) << std::endl;

On this machine the output is 8, so it can represent 0 up to roughly 1.8×10191.8\times10^{19}.

Iterator scope

An iterator declared in the for initializer is visible only inside that loop:

for (size_t i {0}; i < 10; ++i) {
    // i is visible here
}
// i is not visible here

Using i after the loop fails to compile:

error: 'i' was not declared in this scope

If the final value is genuinely needed afterwards, there are two forms. Declare the iterator outside:

size_t i {0};
for (i = 0; i < 10; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}
std::cout << "Loop done, i = " << i << std::endl;   // i is 10

Or drop the initializer entirely and keep the semicolon:

size_t j {0};
for (; j < 10; ++j) {
    // ...
}

The two are equivalent. The second is a little less obvious to a reader, so the lecture presents it only as something you may meet in other people's code.

Do not hard-code the bound

for (size_t i {0}; i < 10; ++i)   // 10 is hard-coded

If that 10 appears in many places, changing the requirement means hunting every occurrence. Store it in a constant instead:

const size_t COUNT {10};
for (size_t i {0}; i < COUNT; ++i) {
    std::cout << i << " : I love C++" << std::endl;
}

Now there is a single point of control. Upper-case constant names are a C++ convention, not a language rule.

while: test first, then run

while spreads the same five parts out:

const size_t COUNT {10};
size_t i {0};              // iterator and start

while (i < COUNT) {        // test
    std::cout << i << " : I love C++" << std::endl;
    ++i;                   // advance, at the end of the body
}

The execution order matches for exactly: test, enter the body only on success, and return to the test after the body. Only the placement differs.

One dangerous difference: in for the advance is forced into the parentheses, whereas in while it hides inside the body. Forget it:

size_t i {0};
while (i < 10) {
    std::cout << i << " : I love C++" << std::endl;
    // forgot ++i
}

i stays 0, the condition stays true, and the program prints forever — the most common source of infinite loops with while.

Rule of thumb: use for when the number of iterations is known and driven by a counter; use while when the loop depends on a state whose duration is unknown, such as reading to the end of a file or waiting for a condition.

do-while: run first, test afterwards

const size_t COUNT {10};
size_t i {0};

do {
    std::cout << i << " : I love C++" << std::endl;
    ++i;
} while (i < COUNT);

Two syntactic details: the while (...) comes after the braces, and the whole statement ends with a semicolon.

Semantically, do-while differs in one essential way: the body runs at least once, because the test happens afterwards. With a count of 10 all three loops print the same thing; the difference only appears at the boundary where the count is zero.

Set COUNT to 0:

const size_t COUNT {0};
size_t i {0};

do {
    std::cout << i << " : I love C++" << std::endl;
    ++i;
} while (i < COUNT);

std::cout << "Loop done" << std::endl;

Output:

0 : I love C++
Loop done

The condition 0 < 0 is false from the start, but the body has already run once. If the intent was "do nothing when the count is zero", do-while is the wrong tool and while or for should be used.

The natural use of do-while is "do it at least once, then decide whether to repeat", such as asking for input until a valid value arrives.

One-line summary: every loop is a start, a test, an advance and a body around one iterator; for packs them into one line, while tests first, and do-while runs first.

Chapter takeaways

  • A loop has five parts: iterator, start, test, advance, body.
  • In for (init; test; increment) the initializer runs once, the test runs before every iteration, and the increment runs after every body.
  • Starting at 0 with i < 10 runs the body ten times, with i taking 0 through 9.
  • Prefer size_t for indices; it is unsigned, so negative values wrap around.
  • An iterator declared in the for header lives only inside that loop; declare it outside if you need its final value.
  • Store the iteration count in a constant instead of hard-coding it.
  • while tests before running; do-while runs before testing, so it always executes at least once.

Comprehension check

1. Name the five parts of for (size_t i {0}; i < 10; ++i) and explain why the body runs ten times rather than nine or eleven.

Reference answer: The iterator is i, the start is 0, the test is i < 10, the advance is ++i, and the body is the braced statement. The initializer runs once; afterwards the cycle is test, body, advance. i takes 0, 1, …, 9, and each test succeeds so the body runs once per value; when i becomes 10 the test 10 < 10 fails and the loop ends. So the body runs exactly ten times and i visits ten values, 0 through 9.

2. How does size_t differ from int as a loop index, and what happens if you assign -1 to a size_t?

Reference answer: size_t is unsigned (8 bytes here) and represents non-negative values only; the standard library uses it for sizes and indices. int is signed. Assigning -1 to a size_t wraps around to the largest value of the type (about 1.8×10191.8\times10^{19} on 64-bit). With a test like i < COUNT, such a value keeps the condition true for an extremely long time. Unsigned indices are fine as long as values stay non-negative; a decreasing loop that could go below zero needs extra care.

3. Where does the scope of an iterator declared in the for header end, and how do you keep its value after the loop?

Reference answer: Its scope ends with the loop, including the test and advance parts; code after the loop cannot see it and fails with 'i' was not declared in this scope. To keep the value, declare the iterator before the loop and use the initializer slot for an assignment or leave it empty: size_t i {0}; for (; i < COUNT; ++i) { ... }. After a normal exit, i equals COUNT.

4. Why is while more prone to infinite loops than for? Give a concrete example.

Reference answer: In for the advance is fixed in the header and cannot be overlooked; in while it usually sits at the end of the body and is easy to drop. For example

size_t i {0};
while (i < 10) {
    std::cout << i << " : I love C++" << std::endl;
    // forgot ++i
}

i never changes, 0 < 10 stays true, and the program prints forever. Adding ++i fixes it. The same problem occurs whenever the state the test depends on is never modified inside the body.

5. With an iteration count of zero, how do while and do-while differ? Give one suitable use for each.

Reference answer: while tests first, so a false condition at the start means the body never runs; do-while runs the body and tests afterwards, so it executes once even when the condition is false from the beginning. With COUNT = 0, the while version prints only Loop done, while the do-while version prints 0 : I love C++ and then Loop done. do-while fits "do it at least once, then decide", such as reading input until it is valid; while fits "possibly zero iterations", such as walking a container that may be empty.

6. Why is const size_t COUNT {10} better than writing 10 directly in the loop?

Reference answer: A hard-coded 10 scatters the decision "how many times" across the program, so a change of requirement means finding and editing every occurrence, with a real risk of missing one. A named constant has a single definition, can be changed in one place, and its name documents what the number means. The compiler can also optimise a const, and the qualifier prevents accidental modification inside the loop.

How this connects to the neighbouring chapters

Chapter 7 decided which path to take; this chapter decides how many times to take it, and together they form the skeleton of every imperative program. Chapter 9 introduces arrays, which immediately need loops to initialise and walk their elements. Later, references in chapter 12, strings in chapter 13 and templates in chapter 17 all keep pairing for with size_t.

来源