Lesson 06: 九九乘法表 CNB
练习任务
编写一个 C 程序,打印九九乘法表(下三角形式):外层循环控制行(i 从 1 到 9),内层循环控制列(j 从 1 到 i),每项格式为 j*i=结果,用制表符 \t 分隔,每行末尾换行。
期望输出:
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81提示:这是 Lesson 05 单层
for循环的升级版——你需要两层嵌套循环,一层控制「行」,一层控制「列」。仔细观察期望输出中j和i的顺序——是j*i不是i*j。
核心知识点
- 嵌套循环
for-for— 外层控制行,内层控制列,二层逻辑嵌套 - 循环执行顺序追踪 — 外层先进入 → 内层完整执行一轮 → 外层推进 → 内层重新执行,完整步进追踪表
printf格式化输出 —%d*%d=%d的三个参数对应关系,宽度修饰符%2d- 制表符
\t与列对齐 —\t跳到下一个 8 字符边界(tab stop),多位数列偏移问题与%2d改进方案 - 循环边界控制 —
j <= i(三角形)vsj < 10(方形),数学对称性与交换律 break在嵌套循环中的行为 — 只跳出最近一层循环,跳出外层的三种替代方案(标志变量、goto、函数封装)continuevsbreak的区别 — 跳过本次迭代剩余部分 vs 终止整个循环- 花括号
{ }的必要性 — 嵌套结构中省略花括号的常见陷阱,编译器理解 vs 缩进欺骗 - 循环变量命名与作用域 —
i/j的 Fortran 历史与数学矩阵惯例,C89 vs C99 声明差异,循环结束后变量值 - C99 循环内变量声明 —
for初始化部分声明,作用域最小化,避免命名冲突 - 乘法表的数学结构与矩阵对称性 — 下三角本质、交换律与重复项消除、从数学到代码的思维映射
- 时间复杂度 O(n²) 分析 — 总执行次数推导(45 vs 81),n² 规模的直观含义
printf缓冲与 stdout 刷新机制 — 行缓冲 vs 全缓冲,\n触发刷新,fflush(stdout)- 多行表格格式化选项 — 字段宽度、左对齐 vs 右对齐、制表符替代方案
代码框架
#include <stdio.h>
int main(void)
{
int i, j;
// 外层循环: i 从 1 到 9,每行一个被乘数
for (i = 1; i <= 9; i++)
{
// 内层循环: j 从 1 到 ???,每列一个乘数
for (j = 1; j <= ???; j++)
{
// 打印每一项,格式: j*i=结果,用 \t 分隔
printf(/* 格式化字符串 */, j, i, i * j);
}
// 每行结束后换行
printf(/* 换行 */);
}
return 0;
}填充以上框架的关键思考:内层循环 j 的上界应该是多少——为什么不是 9 而是 i?printf 的三个 %d 分别对应什么参数?制表符 \t 在字符串中怎么表示?
TIP
先不要往下翻看参考解答。尝试自己填完框架后编译运行,观察输出是否与期望格式一致。
深度讲解
1. 嵌套循环 for-for:二层逻辑嵌套
1.1 从单层循环到双层循环
回顾 Lesson 05 中我们用单层 for 循环计算 1 到 100 的累加——一个循环变量 i,一条循环路径。九九乘法表需要两个维度:行和列。一个循环变量只能走一条线,两个循环变量才能编织一张网格。
// 单层循环:一条线
for (i = 1; i <= 9; i++)
printf("%d\n", i); // 1, 2, 3, ..., 9 —— 一列数字
// 双层循环:一张网格
for (i = 1; i <= 9; i++) // 外层:行
for (j = 1; j <= i; j++) // 内层:列
printf("%d*%d=%d\t", j, i, i * j); // 每个格子核心思想:外层循环每推进一步,内层循环完整执行一轮。就像织布——经线(外层)每走一根,纬线(内层)从头织到尾。
把嵌套循环想象成钟表机械:
外层循环 → 时针,每走一格(i++),内层完成一整圈
内层循环 → 分针,在时针每格内走完自己的全部范围(j 从 1 到 i)这个类比帮助我们理解:外慢内快,外进内满——外层每推进一次,内层从头到尾完整运行一轮。
1.2 执行顺序追踪(完整步进表)
理解嵌套循环的关键是追踪执行顺序。以下追踪表展示 i = 1 到 i = 3 时内外层循环的完整执行过程:
外层 i=1 开始(i <= 9 为真,进入外层循环体)
│
├── 内层 j=1 开始(j <= 1 为真)
│ ├── printf("1*1=1\t") 输出 "1*1=1\t"
│ └── j++ → j=2(j <= 1 为假,退出内层)
│
├── printf("\n") 输出换行
│
├── i++ → i=2(i <= 9 为真,外层继续)
外层 i=2 开始
│
├── 内层 j=1 开始(j <= 2 为真)
│ ├── printf("1*2=2\t") 输出 "1*2=2\t"
│ └── j++ → j=2(j <= 2 为真,继续内层)
│ ├── printf("2*2=4\t") 输出 "2*2=4\t"
│ └── j++ → j=3(j <= 2 为假,退出内层)
│
├── printf("\n") 输出换行
│
├── i++ → i=3(i <= 9 为真,外层继续)
外层 i=3 开始
│
├── 内层 j=1 开始(j <= 3 为真)
│ ├── printf("1*3=3\t") 输出 "1*3=3\t"
│ └── j++ → j=2(j <= 3 为真)
│ ├── printf("2*3=6\t") 输出 "2*3=6\t"
│ └── j++ → j=3(j <= 3 为真)
│ ├── printf("3*3=9\t") 输出 "3*3=9\t"
│ └── j++ → j=4(j <= 3 为假,退出内层)
│
├── printf("\n") 输出换行
│
├── i++ → i=4(i <= 9 为真,外层继续)
... 以此类推,直到 i=9 内层执行完毕,i++ → i=10,i <= 9 为假,整个程序结束逐行分析追踪表的含义:
- 外层
i=1:内层j只取1(因为j <= 1),内层循环体执行 1 次,输出1*1=1,然后换行。 - 外层
i=2:内层j取1、2(因为j <= 2),内层循环体执行 2 次,输出1*2=2、2*2=4,然后换行。 - 外层
i=3:内层j取1、2、3,内层循环体执行 3 次,输出 3 项,然后换行。
以此类推,第 i 行的内层循环体恰好执行 i 次。
总执行次数:内层循环体执行了 ( 1 + 2 + 3 + \cdots + 9 = 45 ) 次——恰好是九九乘法表的项数。公式验证:等差数列求和 ( \frac{9 \times (1 + 9)}{2} = 45 )。
IMPORTANT
嵌套循环的执行顺序不是「外层走一步,内层走一步」——而是「外层走一步,内层走完一轮」。初学者最常见的误解是把两层循环想象成交替执行,实际上内层总是在外层的一次迭代中完整完成。
1.3 嵌套层次与时间复杂度
嵌套循环的层数直接影响时间复杂度。用 n 表示输入规模(这里 n = 9):
| 嵌套层数 | 内层体执行次数 | 时间复杂度 | 示例 |
|---|---|---|---|
单层 for | n | O(n) | 累加 1 到 n |
双层 for-for | n(n+1)/2 ~ n² | O(n²) | 九九乘法表(下三角) |
| 双层(方形) | n² | O(n²) | 完整 9×9 乘法表(81 项) |
三层 for-for-for | n³ | O(n³) | 三维遍历(如三层密码) |
对于九九乘法表(n = 9),下三角模式执行 45 次。但如果打印 n × n 的完整方形乘法表,内层上界改为 n,总操作次数就是 n²——当 n = 10000 时,就是 1 亿次操作。
O(n²) 的直观感受:输入翻倍,工作量翻四倍。例如:
n = 10→ 100 次(方形表)n = 100→ 10,000 次n = 1000→ 1,000,000 次
这就是为什么程序员需要对嵌套循环保持警惕——如果内外层都遍历完整范围,增长速度非常快。下三角(j <= i)将 O(n²) 的常数因子从 1 降低到约 1/2,虽然复杂度级别不变,但在实际编程中,能省一半操作总是值得的。
2. printf 格式化输出与制表符 \t
2.1 格式字符串与参数对应
printf("%d*%d=%d\t", j, i, i * j) 包含三个 %d 和三个对应参数:
printf("%d*%d=%d\t", j, i, i * j);
// ↑1 ↑2 ↑3 ↑1 ↑2 ↑3
// j i i*j j i i*j对应规则:格式字符串中的 %d 按从左到右的顺序,依次对应后续参数。第一个 %d → j,第二个 %d → i,第三个 %d → i * j。
格式字符串中的普通字符(*、=)原样输出。完整的字符串形成过程:
参数值: j=2, i=3, i*j=6
格式串: "%d*%d=%d\t"
↓ ↓ ↓
输出: "2*3=6\t"WARNING
注意参数顺序是 j, i, i*j 而非 i, j, i*j——这是因为期望输出格式是 j*i=结果(如 2*3=6),而不是 i*j=结果。如果参数顺序弄错,输出会变成 3*2=6——虽然结果值一样,但格式与期望不符,评测系统会判定为错误。
2.2 制表符 \t 的机制
\t 不是「插入几个空格」——它是跳到下一个制表位(tab stop)。制表位是每 8 个字符位置的固定锚点:
位置: 0 8 16 24 32 40
| | | | | |
制表位 制表位 制表位 制表位 制表位 制表位\t 的行为取决于当前光标位置:
"1*1=1\t" → 光标在位置 5,\t 跳到位置 8(插入 3 个空格)
"1*2=2\t" → 光标在位置 5,\t 跳到位置 8(插入 3 个空格)
"2*2=4\t" → 光标在位置 13,\t 跳到位置 16(插入 3 个空格)
"3*4=12\t" → 光标在位置 22,\t 跳到位置 24(插入 2 个空格)
"4*4=16\t" → 光标在位置 30,\t 跳到位置 32(插入 2 个空格)
"9*9=81\t" → 光标在位置 38,\t 跳到位置 40(插入 2 个空格)这解释了为什么 \t 后面的内容总是从固定的制表位开始——不同长度的字符串会自动「对齐到下一个锚点」。
历史背景:制表符起源于打字机时代。打字机上有「Tab」键,按下后纸张滑动到预设的固定位置(tab stop),用于快速对齐表格的列。C 语言继承了这一传统,\t 的行为精确模拟了打字机的制表机制。
2.3 多位数列的偏移问题
观察实际输出:
1*4=4 2*4=8 3*4=12 4*4=163*4=12 占 6 字符而非 5,\t 只跳 2 个位置就到了下一个制表位(位置 16)。而 4*4=16 也是 6 字符,\t 从位置 24 跳到位置 32。视觉效果上,包含两位数的列和一位数的列对齐得不够整齐:
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16 ← "12" 和 "16" 打破了整齐感问题根源:\t 只关心光标当前位置到下一个制表位的距离,不关心列内容的实际宽度。当列内容宽度不一致时(如 9 和 16),就会出现视觉不对齐。
2.4 printf 宽度修饰符 %2d
解决方案:用 printf 的宽度修饰符 %2d 保证每位数列占固定宽度:
printf("%d*%d=%2d\t", j, i, i * j); // %2d:至少占 2 字符位,不足补空格%2d 的含义:输出的整数至少占 2 个字符宽度,不足则在左侧补空格(默认右对齐)。具体行为:
| 值 | %d 输出 | 宽度 | %2d 输出 | 宽度 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 2 |
| 9 | 9 | 1 | 9 | 2 |
| 10 | 10 | 2 | 10 | 2 |
| 81 | 81 | 2 | 81 | 2 |
输出效果对比:
# 仅用 \t(列不对齐)
1*4=4 2*4=8 3*4=12 4*4=16
# %2d + \t(列对齐)
1*4= 4 2*4= 8 3*4=12 4*4=16printf 还支持更多宽度修饰符变体:
| 修饰符 | 含义 | 示例(值=9) |
|---|---|---|
%2d | 至少 2 位,右对齐(默认) | 9 |
%-2d | 至少 2 位,左对齐 | 9 |
%02d | 至少 2 位,不足补 0(零填充) | 09 |
%+d | 正数前显示 + 号 | +9 |
TIP
本课练习题的期望输出使用 \t 而非 %2d,因此你的提交代码只需 \t 即可。但理解 \t 的对齐局限和 %2d 的改进方案,是格式化输出的重要知识——在实际项目中,用宽度修饰符做表格对齐比依赖 \t 更可靠。
3. 循环边界控制:三角形 vs 方形乘法表
3.1 下三角模式:j <= i
九九乘法表使用 j <= i 作为内层上界,意味着第 i 行只打印 1*i 到 i*i,形成下三角:
i=1: 1*1=1 ← 1 项
i=2: 1*2=2 2*2=4 ← 2 项
i=3: 1*3=3 2*3=6 3*3=9 ← 3 项
...
i=9: 1*9=9 2*9=18 ... 9*9=81 ← 9 项关键设计决策:j <= i 而非 j <= 9。内层上界不是固定常数,而是依赖外层变量 i 的动态值——这正是嵌套循环的核心技巧:让内层循环的边界由外层循环的当前状态决定。
3.2 方形模式:j < 10
如果内层上界改为 j < 10(或 j <= 9),每行打印 9 项,形成方形:
for (j = 1; j <= 9; j++)
printf("%d*%d=%d\t", j, i, i * j);输出:
1*1=1 2*1=2 3*1=3 ... 9*1=9
1*2=2 2*2=4 3*2=6 ... 9*2=18
...
1*9=9 2*9=18 3*9=27 ... 9*9=81方形表包含 81 项——每一项都在,但有很多重复:2*3=6 和 3*2=6 是同一个乘法事实,只是乘数和被乘数交换了位置。
3.3 数学对称性:为什么选择下三角
乘法满足交换律:( a \times b = b \times a )。方形表中的 ( 9 \times 9 = 81 ) 项,去掉重复后只剩 ( \frac{9 \times 10}{2} = 45 ) 项——正好是下三角表的项数。
下三角表的每一项 j*i(其中 ( j \le i ))都是唯一的乘法事实。没有重复,没有遗漏——这是数学上最紧凑的表示。
方形表: 9 × 9 = 81 项(含重复)
下三角表: 9 × 10 / 2 = 45 项(无重复)
节省: 81 - 45 = 36 项重复为什么是 36 项重复? 因为对于每一对 (a, b) 其中 a ≠ b,方形表会同时包含 a*b 和 b*a,形成 36 个重复对。对角线上的 1*1, 2*2, ..., 9*9(共 9 项)没有对称配对,是唯一的。
用矩阵视角来看:
方形 9×9 矩阵(81 个元素):
1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
2 2 4 6 8 10 12 14 16 18
3 3 6 9 12 15 18 21 24 27
...
主对角线下方 = 上三角上方(交换律)
去掉上三角(含对角线)= 下三角 45 项NOTE
中国传统九九乘法表采用下三角形式,从「一一得一」开始到「九九八十一」——这种形式既无重复又方便朗读记忆,体现了数学的简洁之美。代码中的 j <= i 精确实现了这一数学结构。
3.4 边界设计的编程思维
j <= i 的设计不是随意的——它来自对问题数学结构的理解。编程不只是写语法正确的代码,更是用代码表达数学和逻辑结构。
数学结构 代码实现
─────────────────────────────────────────
第 i 行有 i 个乘法事实 → for (j = 1; j <= i; j++)
j 始终 ≤ i(消除重复) → 循环条件 j <= i
第 i 行第 j 列 = j × i → printf("%d*%d=%d", j, i, i*j)
行结束换行 → printf("\n")这体现了编程的核心思维训练:将数学结构精确映射为代码结构。理解「为什么是 j <= i 而不是 j <= 9」需要从数学角度思考——这不是语法问题,是设计问题。
4. break 在嵌套循环中的行为
4.1 break 只跳出最近一层
README 中展示了一种用 break 实现下三角的替代写法:
for (i = 1; i < 10; i++)
{
for (j = 1; j < 10; j++)
{
if (j > i)
break; // 跳出内层循环,而非外层
printf("%d*%d=%d\t", j, i, i * j);
}
printf("\n");
}break 的规则:只跳出最近的一层循环——这里 break 跳出的是内层 for (j = ...),外层 for (i = ...) 继续推进。
执行流程追踪(以 i = 3 为例):
外层 i=3 开始
│
├── 内层 j=1: j > 3? 否 → printf("1*3=3\t")
├── 内层 j=2: j > 3? 否 → printf("2*3=6\t")
├── 内层 j=3: j > 3? 否 → printf("3*3=9\t")
├── 内层 j=4: j > 3? 是 → break → 跳出内层!
│
├── printf("\n")
├── i++ → i=4,外层继续这与直觉可能不同——有人期望 break 能跳出两层循环,但 C 语言没有 break 2 这样的语法(某些语言如 Java 有标签化 break,但 C 没有)。
4.2 跳出多层循环的三种方案
如果确实需要从内层跳出外层循环,有以下三种方法:
方法 1:标志变量(flag variable)
int done = 0;
for (i = 1; i <= 9 && !done; i++)
{
for (j = 1; j <= 9; j++)
{
if (some_condition) {
done = 1; // 设置标志
break; // 跳出内层
}
}
// 外层下次检查 !done,条件为假,自动退出
}优点:逻辑清晰,易于理解。缺点:需要额外变量,外层条件需要检查标志。
方法 2:goto(C 语言中跳出多层循环的经典正当用法)
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= 9; j++)
{
if (some_condition)
goto outer_end; // 直接跳到外层循环之后
}
}
outer_end:
// 继续执行方法 3:函数封装(最推荐)
int find_value(void)
{
int i, j;
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= 9; j++)
{
if (some_condition)
return i * j; // 直接退出整个函数
}
}
return -1; // 未找到
}三种方案的比较:
| 方案 | 清晰度 | 额外开销 | 适用场景 |
|---|---|---|---|
| 标志变量 | 中等 | 一个 int | 简单条件,两层以内 |
| goto | 中等 | 无 | 深层嵌套,内核代码,性能敏感场景 |
| 函数封装 | 最高 | 函数调用栈 | 几乎所有场景(推荐首选) |
CAUTION
goto 在一般情况下应避免使用,但跳出多层嵌套循环是 C 语言中 goto 的经典正当用途——Linux 内核代码中大量使用这种模式。其他场景(如替代 if-else、替代循环控制流)不应使用 goto。
4.3 continue vs break 的区别
这是一个初学者常混淆的概念:
| 语句 | 行为 | 循环是否继续 | 类比 |
|---|---|---|---|
break | 立即终止整个循环 | 不再执行 | 「这堂课到此结束」 |
continue | 跳过本次迭代的剩余部分 | 继续下一次 | 「这道题跳过,看下一道」 |
// break:遇到第一个偶数就停止
for (i = 1; i <= 10; i++) {
if (i % 2 == 0)
break; // i=2 时跳出,后续都不执行
printf("%d ", i); // 只输出 1
}
// 输出: 1
// continue:跳过偶数,继续奇数
for (i = 1; i <= 10; i++) {
if (i % 2 == 0)
continue; // i=2 时跳过 printf,但继续 i=3,4,...
printf("%d ", i); // 输出所有奇数
}
// 输出: 1 3 5 7 9在嵌套循环中,continue 和 break 一样,只作用于最近的一层循环:
for (i = 1; i <= 3; i++) {
for (j = 1; j <= 3; j++) {
if (j == 2)
continue; // 只跳过内层 j=2 的 printf,外层 i 不受影响
printf("(%d,%d) ", i, j);
}
printf("\n");
}
// 输出:
// (1,1) (1,3)
// (2,1) (2,3)
// (3,1) (3,3)5. 花括号 { } 的必要性
5.1 内层循环的花括号
九九乘法表的参考代码中,内层循环体只有一条 printf——理论上可以省略花括号:
// 简洁写法(内层只有一条语句)
for (i = 1; i <= 9; i++) {
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j);
printf("\n");
}但如果你之后想在内层循环中添加更多操作(比如调试输出、计数等),省略花括号的写法会导致只有第一条语句属于循环:
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j); // 属于循环体
count++; // 不属于循环体!每次外层推进只执行一次缩进让人误以为 count++ 在循环内,但编译器看到的结构是:
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j); // 循环体
count++; // 循环外的语句关键原则:C 语言的缩进对人眼有意义,对编译器无意义。编译器只看花括号和分号,不看缩进。缩进是写给程序员看的,花括号才是写给编译器看的。
5.2 外层循环的花括号
外层循环体包含两条语句(内层循环 + 换行),必须用花括号:
for (i = 1; i <= 9; i++)
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j);
printf("\n"); // 这条不属于外层循环!编译器的理解:
for (i = 1; i <= 9; i++)
for (j = 1; j <= i; j++) // 外层循环体(只有一个内层循环)
printf("%d*%d=%d\t", j, i, i * j); // 内层循环体
printf("\n"); // 循环结束后只输出一个换行结果:所有 45 项挤在同一行,最后只有一个换行——完全不符合期望。
5.3 常见陷阱总结
| 陷阱 | 代码特征 | 后果 |
|---|---|---|
| 内层花括号省略后添加语句 | for (j=...)\n stmt1;\n stmt2; | stmt2 不在循环内,缩进造成错觉 |
| 外层花括号省略(多语句外层体) | for (i=...)\n for(...);\n printf(...) | 外层体只含内层循环,printf 在循环外 |
| 嵌套中花括号不对称 | { { ... }(少一个 }) | 编译错误或逻辑完全错误 |
| 在单语句循环体后添加分号(空语句陷阱) | for (j=...);\n printf(...); | 循环体为空,printf 只执行一次 |
WARNING
嵌套循环中花括号的遗漏是极常见的错误源。核心规则:外层循环体包含多条语句时必须加 { };内层只有一条时可以省略,但建议始终使用 { } 以避免后续修改时的遗漏。记住一句话:花括号是编译器的眼睛,缩进是你的眼睛——两者必须一致。
6. 循环变量命名与作用域
6.1 i/j 的惯例与历史
i 和 j 是循环变量的最常见命名——从 Fortran 时代延续至今。
Fortran 的隐式类型规则:Fortran(1957 年发布,是最早的高级编程语言之一)规定,以 I、J、K、L、M、N 开头的未声明变量默认为 INTEGER(整数)类型,而以其他字母开头的变量默认为 REAL(浮点类型)。程序员习惯性地使用 I、J、K 作为循环变量——不需要额外声明,编译器自动识别为整数。
这一惯例传到了 C 语言,尽管 C 需要显式声明类型,但 i、j、k 作为循环变量的命名习惯已经深入人心。
与数学矩阵的关联:在数学中,矩阵元素记为 ( A_{ij} ),其中 i 表示行号(row),j 表示列号(column)。嵌套循环中 i 通常表示外层(行),j 表示内层(列)——这与矩阵下标惯例完全一致:
// 遍历 3×3 矩阵的每个元素 A[i][j]
for (int i = 0; i < 3; i++) // i: 行 (row)
for (int j = 0; j < 3; j++) // j: 列 (column)
printf("%d ", matrix[i][j]);6.2 C89 vs C99 循环变量声明
C 语言的不同标准版本对变量声明位置有不同要求。这在嵌套循环中尤为明显:
C89/C90 模式(传统风格):所有变量必须在块({ })的开头声明,不能在 for 中声明:
int main(void)
{
int i, j; // 必须在块开头声明所有变量
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j);
printf("\n");
}
// i 和 j 在 main 的整个作用域内都可见
return 0;
}C99/C11 模式(现代风格):允许在 for 的初始化部分声明变量,变量作用域仅限循环体内:
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 9; i++) // i 在 for 中声明
{
for (int j = 1; j <= i; j++) // j 在 for 中声明
printf("%d*%d=%d\t", j, i, i * j);
printf("\n");
}
// 此处 i 和 j 已不可访问——编译器报错
// printf("%d\n", i); // 错误:'i' undeclared
// 可以重新声明同名变量,互不冲突
for (int i = 0; i < 5; i++)
printf("%d ", i);
return 0;
}C99 风格的优势:
- 作用域最小化——变量只存在于需要它的范围内,减少误用
- 避免命名冲突——多个独立循环可以各自声明
int i,互不干扰 - 代码更紧凑——声明和使用在同一行,减少上下滚动查找声明
- 安全——循环外意外使用循环变量会在编译期被捕获
IMPORTANT
C89/C90 模式下(如某些嵌入式编译器或旧项目),变量声明必须放在块开头,不能在 for 中声明。编译时加 -std=c99 或 -std=c11 可启用此特性。如果遇到 'for' loop initial declarations are only allowed in C99 mode 错误,就是编译器在使用 C89 模式。
6.3 循环结束后的变量值
循环结束后,循环变量的值是什么?这是一个常见的考察点:
int i;
for (i = 1; i <= 9; i++)
printf("%d ", i);
// 循环结束后: i == 10
// 原因: i++ 将 i 从 9 增到 10,然后 i <= 9 为假,退出循环
int j;
for (j = 1; j <= i; j++) // 这里 i 是 10,所以 j 从 1 到 10
printf("%d ", j);
// 循环结束后: j == 11通用规律:对于 for (init; condition; increment),循环结束时循环变量的值是第一个使 condition 为假的值。
这和 Lesson 05 中的分析一致——for 循环的终止条件是「条件为假时退出」,退出时的循环变量值是让条件变假的那个值。
C99 模式下的区别:如果在 for 中声明变量,循环结束后变量不可访问,因此不会有「循环结束后变量值是多少」的问题——这是 C99 风格的另一个安全优势。
7. 乘法表的数学结构与编程思维
7.1 下三角的数学本质
九九乘法表的下三角结构来自一个数学事实:对于 ( j \times i )(其中 ( 1 \le j \le i \le 9 )),每一项对应一个唯一的乘法事实。如果 ( j > i ),则 ( j \times i ) 已经在 ( i \times j ) 中出现过(交换律)。
集合论视角:定义集合 ( S = { (a, b) \mid 1 \le a \le 9, 1 \le b \le 9 } ),包含 81 个有序对。定义等价关系 ( (a, b) \sim (b, a) )(乘法交换律)。选择代表元:取每个等价类中 ( a \le b ) 的那个(即下三角)。则代表元个数为 ( \frac{9 \times 10}{2} = 45 )。
内层上界 j <= i 的设计不是随意的——它精确地过滤掉了所有重复项,只保留 ( j \le i ) 的那些。
7.2 从数学到代码的思维映射
| 数学概念 | 代码实现 |
|---|---|
| 行号 ( i )(被乘数) | 外层循环变量 i |
| 列号 ( j )(乘数) | 内层循环变量 j |
| ( j \le i )(去掉重复) | 内层循环条件 j <= i |
| 表项 ( j \times i ) | printf("%d*%d=%d\t", j, i, i * j) |
| 行结束 | printf("\n") |
| 表的总行数 ( n = 9 ) | 外层循环条件 i <= 9 |
| 第 i 行有 i 项 | 内层循环条件 j <= i |
编程思维的训练:将一个数学结构(乘法表)映射为代码结构(嵌套循环),是编程中最基础也最重要的能力。理解「为什么 j <= i 而不是 j <= 9」需要从数学角度思考——这不是语法问题,是设计问题。
7.3 矩阵视角:对称性与存储
用矩阵的视角看乘法表:
列 j
1 2 3 4 5 6 7 8 9
┌────────────────────────────────────
行 1│ 1 2 3 4 5 6 7 8 9
i 2│ 2 4 6 8 10 12 14 16 18
3│ 3 6 9 12 15 18 21 24 27
4│ 4 8 12 16 20 24 28 32 36
5│ 5 10 15 20 25 30 35 40 45
6│ 6 12 18 24 30 36 42 48 54
7│ 7 14 21 28 35 42 49 56 63
8│ 8 16 24 32 40 48 56 64 72
9│ 9 18 27 36 45 54 63 72 81这是一个对称矩阵:元素 (i, j) = 元素 (j, i)。对角线上的元素 (1, 1), (2, 2), ..., (9, 9) 是平方数(1, 4, 9, 16, 25, 36, 49, 64, 81)。
下三角表(j <= i)提取了主对角线及以下的部分,正好覆盖了所有不重复的乘法事实。这为后续学习二维数组和矩阵操作埋下伏笔——Lesson 07 及后续课程将涉及更多二维数据结构。
8. printf 缓冲与 stdout 刷新机制
8.1 标准输出的缓冲策略
你可能好奇:为什么 printf 的输出会「准时」出现在屏幕上?这背后是 C 标准库的缓冲机制。
#include <stdio.h>
int main(void)
{
// 以下输出可能不会立即显示在屏幕上
printf("Hello"); // 没有 \n,可能留在缓冲区
printf(" World"); // 仍在缓冲区
// 以下会触发刷新(flush)
printf("\n"); // \n 触发行缓冲刷新
// 显式刷新
printf("Processing...");
fflush(stdout); // 强制刷新,立即输出
return 0;
}三种缓冲模式:
| 缓冲模式 | 触发刷新的条件 | 典型场景 |
|---|---|---|
| 全缓冲 | 缓冲区满(通常 4096/8192 字节) | 文件写入(磁盘) |
| 行缓冲 | 遇到 \n 或缓冲区满 | 标准输出(终端) |
| 无缓冲 | 每次写入立即输出 | 标准错误 stderr |
标准输出 stdout 连接到终端时是行缓冲——这意味着每次遇到 \n,缓冲区的内容会被刷新到屏幕。这就是为什么九九乘法表的每行末尾 printf("\n") 不仅换行,还确保该行内容被实际输出。
8.2 为什么需要关心缓冲
在以下场景中,理解缓冲很重要:
场景 1:进度提示不显示
for (int i = 0; i < 1000000; i++) {
// 复杂计算...
printf("."); // 没有 \n,缓冲未刷新
// 用户可能长时间看不到任何输出
}
// 修正:每 1000 次手动刷新
for (int i = 0; i < 1000000; i++) {
// 复杂计算...
printf(".");
if (i % 1000 == 0)
fflush(stdout); // 强制刷新
}场景 2:九九乘法表中的隐式刷新
for (i = 1; i <= 9; i++) {
for (j = 1; j <= i; j++)
printf("%d*%d=%d\t", j, i, i * j); // \t 不触发刷新
printf("\n"); // \n 触发刷新!
}每行末尾的 printf("\n") 不仅产生换行,还触发行缓冲刷新——确保该行内容立即显示在屏幕上。如果去掉 \n,整个输出可能在程序结束时才一次性出现。
TIP
九九乘法表中,printf("\n") 的双重作用:一是产生换行使表格分行,二是触发行缓冲刷新使输出逐行显示。这是一个优雅的设计——一行代码同时满足格式化需求和 I/O 需求。
9. 多行表格格式化选项
9.1 制表符 vs 字段宽度
我们已经看到了两种列对齐方法:
方法 A:制表符 \t
printf("%d*%d=%d\t", j, i, i * j);- 优点:代码简洁,无需计算宽度
- 缺点:依赖制表位(每 8 字符),内容宽度不一致时不对齐
方法 B:宽度修饰符 %Nd
printf("%d*%d=%-2d ", j, i, i * j); // 左对齐,固定 2 字符宽- 优点:精确控制列宽,对齐可靠
- 缺点:需要预先知道数据最大宽度
9.2 多行表格的完整格式化方案
对于一个更复杂的表格输出(如包含表头、分隔线的完整乘法表),可以使用组合格式化:
#include <stdio.h>
int main(void)
{
int i, j;
// 表头
printf(" ");
for (j = 1; j <= 9; j++)
printf("%4d", j);
printf("\n");
// 分隔线
printf(" +");
for (j = 1; j <= 9; j++)
printf("----");
printf("\n");
// 表体
for (i = 1; i <= 9; i++) {
printf("%2d |", i); // 行号
for (j = 1; j <= 9; j++)
printf("%4d", i * j); // 右对齐 4 字符宽
printf("\n");
}
return 0;
}输出效果:
1 2 3 4 5 6 7 8 9
+------------------------------------
1 | 1 2 3 4 5 6 7 8 9
2 | 2 4 6 8 10 12 14 16 18
3 | 3 6 9 12 15 18 21 24 27
...9.3 对齐方式对比
| 修饰符 | 对齐方式 | 示例(值=9,宽度=4) | 适用场景 |
|---|---|---|---|
%4d | 右对齐 | 9 | 数字列(常规) |
%-4d | 左对齐 | 9 | 文本列、标签 |
%04d | 零填充 | 0009 | 固定位数(如编号) |
%+4d | 右对齐 | +9 | 需要显式正负号 |
九九乘法表的练习中使用 \t 是为了简洁,但在实际项目中,用宽度修饰符做表格对齐是更可靠和专业的做法。
参考解答
九九乘法表(for-for 嵌套循环)
#include <stdio.h>
int main(void)
{
int i, j;
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= i; j++)
{
printf("%d*%d=%d\t", j, i, i * j);
}
printf("\n");
}
return 0;
}核心逻辑:外层 i 从 1 到 9 控制行,内层 j 从 1 到 i 控制每行的项数。j <= i 保证下三角形状,printf 的三个 %d 分别对应 j、i、i*j——注意顺序是 j*i=结果。\t 分隔各项,\n 结束每行。
使用 break 的替代写法
#include <stdio.h>
int main(void)
{
int i, j;
for (i = 1; i < 10; i++)
{
for (j = 1; j < 10; j++)
{
if (j > i)
break;
printf("%d*%d=%d\t", j, i, i * j);
}
printf("\n");
}
return 0;
}这种写法让内层循环上界固定为 9,但在 j > i 时用 break 提前跳出内层——效果与 j <= i 相同。break 只跳出内层循环,外层继续推进。
C99 风格(for 中声明变量)
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= i; j++)
{
printf("%d*%d=%d\t", j, i, i * j);
}
printf("\n");
}
return 0;
}C99 风格在 for 初始化部分声明变量,作用域限于循环体内。编译时可能需要 -std=c99 或 -std=c11 选项。
while-while 实现
#include <stdio.h>
int main(void)
{
int i = 1, j;
while (i <= 9)
{
j = 1;
while (j <= i)
{
printf("%d*%d=%d\t", j, i, i * j);
j++;
}
printf("\n");
i++;
}
return 0;
}while-while 实现与 for-for 在功能上等价,但初始化、条件、递增分散在多处。注意内层 while 前需要 j = 1 重新初始化——这是 while 与 for 的一个关键区别:for 每次进入循环自动执行初始化,while 需要手动重置。
对照检查:内层上界是否用了
i而非9?printf参数顺序是否是j, i, i*j?\t是否在格式字符串内?每行是否都输出了\n?
课堂讨论
- 列举出 3 种可以用
for-for两重循环来解决的问题场景,并说明为什么单层循环不够用。 - 示例中的
{ }是必须的吗?如果去掉外层或内层的{ },会有什么问题?给出具体代码示例。 - 如果用
while-while两重循环来编写这个程序,如何实现?与for-for写法相比有什么不同? break在嵌套循环中能跳出两层吗?如果需要从内层直接跳出外层循环,有哪三种方法?各自的适用场景是什么?- 如果要打印一个完整的 9×9 方形乘法表(包含所有 81 项),只需要修改哪一处代码?为什么这一处修改就能产生截然不同的输出?
continue和break在嵌套循环中的行为有什么区别?如果在内层循环中使用continue,外层循环会受影响吗?- 为什么
printf("%d*%d=%d\t", j, i, i * j)中参数顺序是j, i, i*j而不是i, j, i*j?两种写法输出的结果有什么不同? - 如果要求每列宽度固定为 4 个字符,且数字右对齐,应该如何修改
printf的格式字符串?
讨论答案
Q1: 列举 3 种可以用 for-for 两重循环解决的问题
- 矩阵/二维数组遍历:遍历棋盘、图像像素、电子表格。外层遍历行、内层遍历列。单层循环只能处理一维序列,无法同时定位行和列。
- 排序算法(如冒泡排序):外层控制轮次,内层逐对比较交换。每轮将当前最大值「冒泡」到末尾。
- 组合生成(如密码穷举):生成所有可能的两位密码组合(00~99),外层十位、内层个位。三层循环可扩展到三位密码。
共同特征:这些问题的数据天然具有二维结构,或需要两维度的穷举。单层循环只能处理一维序列,双层循环才能处理二维网格。
Q2: 示例中的 { } 是必须的吗?
外层 { } 是必须的,因为外层循环体包含两条语句(内层循环 + printf("\n"))。省略花括号后,只有第一条语句(内层循环)属于外层循环体,printf("\n") 会变成循环外的语句——最终所有输出挤在一行,只有一个换行:
for (i = 1; i <= 9; i++) // 外层循环
for (j = 1; j <= i; j++) // 外层循环体(唯一一条语句)
printf("%d*%d=%d\t", j, i, i * j);
printf("\n"); // 循环外!只执行一次
// 输出: 1*1=1 1*2=2 2*2=4 ... 9*9=81 (全部在一行,最后才换行)内层 { } 可以省略,因为内层循环体只有一条 printf。但建议始终使用花括号——后续添加语句(如调试输出)时不容易遗漏。这是一个防御性编程的好习惯。
Q3: 用 while-while 两重循环如何实现?
#include <stdio.h>
int main(void)
{
int i = 1, j;
while (i <= 9)
{
j = 1; // 关键:每次进入内层前重置 j
while (j <= i)
{
printf("%d*%d=%d\t", j, i, i * j);
j++;
}
printf("\n");
i++;
}
return 0;
}与 for-for 的区别:
| 特性 | for | while |
|---|---|---|
| 初始化 | 在循环头部自动执行 | 在循环前手动初始化(j = 1) |
| 条件 | 在循环头部,每次迭代前检查 | 在 while 括号中,每次迭代前检查 |
| 递增 | 在循环头部,每次迭代后自动执行 | 在循环体内手动递增(j++) |
| 可读性 | 三要素集中,一目了然 | 分散在多处,需要仔细查找 |
| 适用 | 计数型循环(次数已知) | 条件型循环(次数不确定) |
for 循环将「初始化、条件、递增」三要素集中在头部,更适合计数型循环。while 更适合条件不确定的循环(如「读到文件末尾为止」)。九九乘法表的循环次数确定(i 从 1 到 9),因此 for 是更自然的选择。
Q4: break 能跳出两层循环吗?跳出多层循环的三种方法
不能。 break 只跳出最近的一层循环。
要跳出多层循环,有三种方法:
方法 1:标志变量
int done = 0;
for (i = 1; i <= 9 && !done; i++) {
for (j = 1; j <= 9; j++) {
if (condition) {
done = 1;
break;
}
}
}方法 2:goto
for (i = 1; i <= 9; i++) {
for (j = 1; j <= 9; j++) {
if (condition)
goto outer_end;
}
}
outer_end:方法 3:函数封装(最推荐)
int search(void) {
for (int i = 1; i <= 9; i++)
for (int j = 1; j <= 9; j++)
if (condition)
return i * j;
return -1;
}| 方法 | 适用场景 | 推荐度 |
|---|---|---|
| 标志变量 | 两层以内,条件简单 | ★★★ |
| goto | 深层嵌套,内核代码,性能敏感 | ★★ |
| 函数封装 | 几乎所有场景 | ★★★★★ |
Q5: 打印方形乘法表只需修改哪一处?
只需修改内层循环上界:j <= i 改为 j <= 9(或 j < 10)。
for (j = 1; j <= 9; j++) // 原来是 j <= i,现在固定为 9
printf("%d*%d=%d\t", j, i, i * j);这一处修改让每行打印 9 项而非 i 项,从下三角变为方形。修改量极小(改一个字符),但效果截然不同——从 45 项变为 81 项,从无重复变为有 36 个重复项。
这体现了良好代码结构的标志:修改一处,效果明确。内层循环上界从 i(动态值)改为 9(固定值),改变了输出形状——边界设计是循环结构中最有表达力的部分。
Q6: continue 和 break 在嵌套循环中的区别
| 语句 | 行为 | 外层是否受影响 | 类比 |
|---|---|---|---|
break | 立即终止内层循环 | 否,外层继续 | 「下课,离开教室」 |
continue | 跳过内层本次迭代剩余部分 | 否,外层继续 | 「这题跳过,下一题」 |
在内层循环中使用 continue:
for (i = 1; i <= 3; i++) {
for (j = 1; j <= 3; j++) {
if (j == 2)
continue; // 只跳过 j=2 的 printf,外层 i 不受影响
printf("(%d,%d) ", i, j);
}
printf("\n");
}
// 输出:
// (1,1) (1,3) ← j=2 被跳过
// (2,1) (2,3) ← j=2 被跳过
// (3,1) (3,3) ← j=2 被跳过continue 和 break 都只作用于最近的一层循环,外层循环完全不受影响。
Q7: 为什么参数顺序是 j, i, ij 而不是 i, j, ij?
期望输出格式是 j*i=结果(如 2*3=6),而不是 i*j=结果。printf 的参数与格式字符串中的 %d 从左到右一一对应:
// 正确:输出 "2*3=6"
printf("%d*%d=%d\t", j, i, i * j);
// ↑1 ↑2 ↑3 ↑1 ↑2 ↑3
// 错误:输出 "3*2=6"(虽然结果值相同,但乘数和被乘数顺序反了)
printf("%d*%d=%d\t", i, j, i * j);
// ↑1 ↑2 ↑3 ↑1 ↑2 ↑3两种写法数学结果相同(交换律),但输出格式不同。在自动评测系统中,格式必须精确匹配期望输出——顺序错误会导致判定失败。这也提醒我们:代码不仅要结果正确,格式也要正确。
Q8: 固定列宽 4 字符、数字右对齐的格式字符串
printf("%d*%d=%4d", j, i, i * j);
// ↑
// %4d:至少 4 字符宽,右对齐,不足左侧补空格如果需要左对齐:
printf("%d*%d=%-4d", j, i, i * j);
// ↑
// %-4d:至少 4 字符宽,左对齐,不足右侧补空格输出效果对比:
%4d (右对齐): "1*1= 1" "2*2= 4" "9*9= 81"
%-4d (左对齐): "1*1=1 " "2*2=4 " "9*9=81 "右对齐是数字列的常规做法(个位对齐便于比较大小),左对齐适用于文本列。
课后练习
矩形星号图案:编写程序打印一个
m × n的星号矩形(m行n列的*),m和n由用户输入。知识点提示:双层
for循环,外层控制行数,内层控制列数。与乘法表结构相同,只是循环体改为打印*。下三角星号图案:打印一个
n行的下三角星号图案(第 1 行 1 个星号,第 2 行 2 个,...),n由用户输入。知识点提示:内层上界
j <= i——与乘法表相同的边界逻辑。这是九九乘法表的「图案化」应用。完全数查找:一个数如果等于其所有真约数(不包括自身)之和,则称为完全数。例如 6 = 1 + 2 + 3。找出 1~1000 以内的所有完全数。
知识点提示:外层遍历每个数,内层遍历其可能的约数(1 到 n-1),用
n % i == 0判断是否为约数。双层循环加条件判断的经典组合。水仙花数:一个 n 位数(n ≥ 3)每个位上的数字的 n 次幂之和等于它本身。例如 1³ + 5³ + 3³ = 153。找出 n = 3 的所有水仙花数(即 100~999)。
知识点提示:拆解三位数的百位
n/100、十位(n/10)%10、个位n%10,计算每位立方之和。单层循环即可,但拆解数字是后续课程的重要基础。棋盘打印:打印一个 5×5 的棋盘(0 表示空位,1 表示有子),用户输入一个坐标(如 2 3),在该位置放置棋子后重新打印棋盘。
知识点提示:二维数组(
int board[5][5])、双层循环遍历、scanf读坐标。注意数组下标从 0 开始,需要转换。九九加法表:仿照九九乘法表,打印一个九九加法表(下三角形式),格式为
j+i=结果,用\t分隔。知识点提示:只需将乘法
i*j改为加法i+j,格式字符串中*改为+。理解「模式复用」——同一结构应用于不同运算。
练习1: 矩形星号图案
#include <stdio.h>
int main(void)
{
int m, n, i, j;
printf("请输入行数和列数: ");
scanf("%d %d", &m, &n);
for (i = 1; i <= m; i++)
{
for (j = 1; j <= n; j++)
printf("*");
printf("\n");
}
return 0;
}逻辑:外层 i 控制行数,内层 j 控制每行的星号数。与乘法表结构相同,只是循环体改为打印 * 而非 j*i=结果。这个练习展示了嵌套循环的「模板化」特性——改变循环体的内容,同一结构可以生成完全不同的输出。
练习2: 下三角星号图案
#include <stdio.h>
int main(void)
{
int n, i, j;
printf("请输入行数: ");
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
for (j = 1; j <= i; j++)
printf("*");
printf("\n");
}
return 0;
}内层上界 j <= i 与九九乘法表完全一致——第 i 行打印 i 个星号。掌握了一种模式(j <= i 下三角),就能应用到多种场景:星号图案、数字三角、乘法表等。
练习3: 完全数查找
#include <stdio.h>
int main(void)
{
int num, i, sum;
for (num = 2; num <= 1000; num++)
{
sum = 0;
for (i = 1; i < num; i++)
{
if (num % i == 0)
sum += i;
}
if (sum == num)
printf("%d is a perfect number\n", num);
}
return 0;
}双层循环嵌套:外层遍历 2~1000 的每个候选数,内层遍历其可能的约数。num % i == 0 判断 i 是否为约数,累加所有真约数之和,最后检查是否等于自身。
输出:6、28、496(第四个完全数是 8128,超出 1000 范围)。
练习4: 水仙花数
#include <stdio.h>
int main(void)
{
int n, hundreds, tens, units;
for (n = 100; n <= 999; n++)
{
hundreds = n / 100; // 百位
tens = (n / 10) % 10; // 十位
units = n % 10; // 个位
if (hundreds * hundreds * hundreds +
tens * tens * tens +
units * units * units == n)
printf("%d\n", n);
}
return 0;
}输出:153、370、371、407。
核心技巧是拆解三位数的各位数字:百位 n/100、十位 (n/10)%10、个位 n%10。虽然本题只用单层循环,但拆解数字的方法在后续多位数处理中非常实用。
练习5: 棋盘打印
#include <stdio.h>
int main(void)
{
int board[5][5] = {0}; // 全部初始化为 0
int row, col, i, j;
printf("请输入坐标 (行 列): ");
scanf("%d %d", &row, &col);
if (row >= 1 && row <= 5 && col >= 1 && col <= 5)
board[row - 1][col - 1] = 1; // 数组下标从 0 开始
for (i = 0; i < 5; i++)
{
for (j = 0; j < 5; j++)
printf("%d ", board[i][j]);
printf("\n");
}
return 0;
}二维数组 board[5][5] 存储棋盘状态,双层循环遍历打印。注意数组下标从 0 开始,所以用户输入的坐标需要减 1 转换。{0} 初始化将整个数组置零。这引入了数组和二维遍历的概念——后续课程会详细讲解。
练习6: 九九加法表
#include <stdio.h>
int main(void)
{
int i, j;
for (i = 1; i <= 9; i++)
{
for (j = 1; j <= i; j++)
{
printf("%d+%d=%d\t", j, i, i + j);
}
printf("\n");
}
return 0;
}与乘法表相比,只改了两处:* 改为 +,i * j 改为 i + j。其余结构完全不变——这就是「模式复用」的力量。理解了一种嵌套循环模式后,稍作修改就能适配多种场景。
参考资料
- ISO C99 Standard, Section 6.8.5 —
for语句的语法与语义定义,包括嵌套循环的执行规则 - The C Programming Language (K&R), Section 1.4 — Kernighan & Ritchie 对符号常量与循环的经典讲解
- Linux Kernel Coding Style, Section 7 — Linux 内核关于
goto在嵌套循环中的使用指南 - printf Man Page —
printf格式化输出的完整文档(含宽度修饰符、对齐方式等) - C99 Rationale, Section 6.8.5 — C99 允许
for循环内声明变量的设计理由
"First, solve the problem. Then, write the code." — John Johnson