聊C语言的循环语句。for、while、do-while这仨兄弟,几乎每个C程序里都有它们的身影,不管你是刚接触编程的学生,还是准备计算机等级考试的考生,又或者是回头复习基础的开发者,循环这块儿都绕不过去。很多人学循环的时候,语法背得滚瓜烂熟,一到写代码就翻车,死循环、漏循环、边界错位,各种问题层出不穷。这篇文章不打算讲教科书上那套,而是从实际写代码的角度,把循环语句的底层逻辑、选型思路、避坑经验一次说透。
1. 循环语句的整体设计与选择逻辑
1.1 为什么需要循环:重复做事是程序的本能
先想一个问题:让你打印1到100,你会怎么写?老老实实写100行printf?那如果打印1到10000呢?这时候你就明白循环的意义了——把重复性的操作交给计算机,是编程最基本的能力。
但循环不只是“重复”这么简单,它背后藏着两个核心要素。第一是重复执行某段代码,第二是每次执行时状态在变化。这两个要素缺一不可。看这个例子:
int i = 1; while (i <= 100) { printf("%d\n", i); i++; }这段代码里,printf是重复的部分,i++是状态变化的部分。如果只有重复没有状态变化,那循环就成了永不停歇的复读机,这就是死循环的根源。理解了这一点,你在写任何循环之前,都应该先问自己两个问题:我要重复做什么?每做一次,什么东西在变?
很多初学者上来就写代码,想到哪儿写到哪儿,结果循环条件、变量更新、循环体三者各写各的,互相矛盾。正确做法是先想清楚这两个问题,再动手。我把这个过程叫做“循环三要素”:初始化、条件、更新。任何循环,不管用哪种语法写,本质上都是这三个要素的排列组合。
1.2 三种循环结构的一次性对照
C语言提供了三种循环结构,很多人纠结“到底用哪个”,其实你只要搞清楚它们的执行次序,选择就变成了一件很自然的事。
| 循环类型 | 语法骨架 | 执行次序 | 典型场景 |
|---|---|---|---|
for | for (初始化; 条件; 更新) | 先初始化,再判断条件,执行循环体,执行更新,再判断条件 | 循环次数已知或可通过变量确定 |
while | while (条件) { 循环体 } | 先判断条件,为真才执行循环体 | 次数不确定,依靠条件终止 |
do-while | do { 循环体 } while (条件); | 先执行循环体,再判断条件,条件为真则继续 | 至少需要执行一次的场景 |
光看表可能还不够直观。for和while在本质上是等价的,for只是把初始化、条件、更新这三大件集中写在了同一行,结构更紧凑。do-while则是三个里面最特殊的一个——它先斩后奏,不管条件是否成立,循环体至少跑一次。
我用一个特别生活化的例子来帮大家记。你把循环想象成“检查手机有没有新消息”:while是先看一眼有没有消息,有就看,没有就睡觉;do-while是闭着眼睛先刷一下朋友圈,刷完了再告诉自己“哦今天没有新内容”或者“哎还真有”,然后继续刷。前者是“看了看再决定做不做”,后者是“先做了再说”。绝大部分日常循环属于前者,只有少数场景(比如菜单显示、输入校验)必须用后者。
1.3 选型原则:不纠结,按场景走
根据我写代码的习惯,选型其实可以压缩成三条非常简单的规则:
- 知道要循环多少次,或者次数能用一个表达式算出来,用
for。比如遍历数组的N个元素,for (i = 0; i < N; i++),一眼就能看出循环范围。 - 不知道次数,但知道什么时候该停,用
while。典型就是读文件或读用户输入,你没法预估对方会给多少数据,只能靠“读到末尾”或“收到退出指令”来判断。 - 不管条件成不成立,至少得执行一次的业务逻辑,用
do-while。比如用户菜单:先显示菜单,再根据用户选择判断是否继续,你总得先把菜单显示出来,才谈得上“用户可能选退出”。
这个选型规则帮我省掉了大量纠结的时间。很多人学循环的时候有个误区,总觉得一定要把三种全用上才算学会,其实不是。三种循环是工具,不是竞赛项目,哪种顺手用哪种。真正的高手写的代码,往往大量使用for,因为它的结构最规范、意图最清晰。而while和do-while则用在那些确实更贴合的特定场景里。
2. 三种循环结构的核心细节与边界
2.1 for循环:三大件的执行顺序是命门
for循环是所有循环里最容易“看懂”的,但也是出错后最难查的,原因就在于它的三大件写在同一行,执行顺序容易被忽略。我用一个例子把它彻底拆开:
for (int i = 0; i < 10; i++) { printf("i = %d\n", i); }这段程序的执行顺序是:先执行int i = 0(初始化,只执行一次),然后判断i < 10,成立则进入循环体打印,打印完成后执行i++,再回到条件判断。注意,i++不是在循环体内执行的,而是在循环体的末尾执行的。这看起来是小事,但影响很大。比如你写:
for (int i = 0; i < 10; i++) { if (i == 5) { continue; } printf("i = %d\n", i); }continue会跳过printf,但不会跳过i++,因为i++在语法结构上是for的更新部分,不受continue影响。这一点很多人会弄混——以为continue跳过了本轮所有代码,结果i没有更新,死循环了。实际上,在for循环中,continue只是跳到更新表达式那里,该更新的还是会更新。
再说一个高频问题:for里三个部分其实都可以省略,但分号不能省略。for (;;)是个死循环,等价于while (1)。我见过有人把for (;;)写成for (),编译直接报错。这种写法偶尔在嵌入式编程或驱动代码里见到,但在应用层写业务逻辑时,我建议还是老老实实把三大件写全,代码的可读性比那点“偷懒”重要得多。
还有一个细节是for循环的初始化表达式可以用逗号表达式同时做多件事,比如for (i = 0, j = n - 1; i < j; i++, j--),这个写法在双指针算法里特别常见。但刚入门的时候我不推荐你用这种花活,等熟悉了基础流程再去用,否则容易把自己绕晕。
2.2 while与do-while:判断时机决定一切
while和do-while的区别我在前面提到过,核心就是判断时机。但真实的坑往往藏在“什么时候该用do-while”上。
举个例子,你要写一个“密码输入校验”的程序:用户输入密码,不对就继续输,直到对为止。这里有一个隐含需求——用户至少得输入一次密码,因为程序不可能在一开始就知道用户密码对不对,必须先拿到输入才能判断。这种情况下用do-while是最自然的:
char pwd[20]; do { printf("请输入密码:"); scanf("%s", pwd); } while (strcmp(pwd, "123456") != 0);如果用while写,你得先把密码变量初始化成一个“必然不等于正确密码”的初始值,才能进入循环,这样做既不优雅又容易出问题。do-while把“至少执行一次”这个语义直接写进了语法里,恰好对应了这类真实需求。
但要注意,do-while有个容易忽略的点,就是结尾的分号。很多初学者写完do { ... } while (条件)之后忘了加分号,编译直接报错。其实这个分号在语法上是必须的,它不是普通语句的分号,而是do-while语句的一部分。编译器报错的时候给的提示往往不太明显,这时候你就得有这个意识:去检查do-while尾部。
再说while。while有个特点,它没有初始化部分,所以循环变量的初始化必须在进入while之前单独写好。这意味着如果循环体内的某些条件会改变循环变量,你就得格外小心——如果初始化出了问题,循环体一次都不会执行。这个特性在某些情境下反而是优点,比如“当还有数据就继续读”的模型就很适合while:
int ch; while ((ch = getchar()) != EOF) { putchar(ch); }这里的技巧是:把读取操作直接放在条件判断里,先让getchar()执行,把返回值赋给ch,再拿ch和EOF比较。这既完成了数据读取,又完成了条件判断,一举两得。这种写法很常见,但我建议初学者先拆开写:
int ch = getchar(); while (ch != EOF) { putchar(ch); ch = getchar(); }逻辑更直白,虽然代码长一点,但不容易出错。
2.3 break和continue:控制流的两个转向开关
break和continue是循环体内部的两个“转向开关”。break是立即跳出整个循环,continue是跳过本轮剩余代码,直接进入下一轮。这个区别嘴上说说容易,写起来就有人犯糊涂。
特别要注意的是,break只能跳出一层循环。如果你在一个多层嵌套循环里写了个break,它只会跳出最内层的那一层,外层循环该怎么走还是怎么走。这个问题我在后面讲嵌套循环时会再展开,但在这里先给个预告——很多人写二维数组遍历时想用break提前结束全部搜索,结果只跳出了内层循环,外层还在跑,导致逻辑错误。
continue则要注意使用位置。在while循环里,如果你不小心把continue放在循环变量的更新语句前面,那continue一执行,更新语句就被跳过了,循环变量不再变化,死循环就来了。看这段代码:
int i = 0; while (i < 10) { if (i == 5) { continue; // 死循环! } printf("%d\n", i); i++; }当i等于5时,continue直接跳到条件判断,i++永远没有机会执行,于是i永远定格在5,循环永远不退出。这种bug特别隐蔽,因为程序不会崩溃,也不会立刻暴露问题,但程序就像“卡住”了一样。排查方法我会在第4章详细讲,这里先提醒一句:但凡在while循环里用了continue,先检查更新语句有没有可能被跳过。
2.4 循环变量:类型、边界与符号的隐形陷阱
循环变量不是随便挑个int就完事了,它牵扯的问题相当多。一个最经典的大坑是unsigned和>= 0组合。看这段代码:
unsigned int i; for (i = 10; i >= 0; i--) { printf("%u\n", i); }你以为它会打印10到0,实际上它是无限循环。因为i是无符号整数,当i从0减1时,它不会变成-1,而是绕回4294967295(32位无符号最大值),条件i >= 0永远成立。这个坑踩进去的时候,很多人对着屏幕怀疑人生。
解决办法很简单:要么循环变量用signed int,要么把条件写成i > 0然后内部打印时再处理。我在实际写代码时,循环变量一律优先用signed int,只有在明确需要非负语义时(比如数组下标)才考虑size_t,而且使用size_t时绝对不用反向递减的写法。
还有一个常见问题是边界究竟是< n还是<= n - 1。我记得刚学C语言时特别纠结这个问题,后来发现业界惯例是一律用半开区间[0, n),即for (i = 0; i < n; i++)。这个习惯的出发点很多:数组下标是从0开始的,最后一个合法下标是n - 1,循环条件写i < n正好覆盖了0到n - 1的全部合法下标,逻辑上无歧义。而如果你用i <= n - 1,不但要额外思考n = 0时的边界情况,还多了一次计算,没有任何好处。
3. 实操过程:从零开始搭一个完整循环
3.1 完整拆解一个经典案例:从1加到100
理论讲再多,不如写一个典型例子。我们来解决一道最经典的问题:计算从1加到100的和。
第一步,明确循环三要素。重复做的事是“把当前的数加到累加器里”,状态变化是“当前的数从1递增到100”。所以初始化是int sum = 0和int i = 1,条件是i <= 100,更新是i++。
第二步,选择循环结构。循环次数是明确的100次,直接用for:
#include <stdio.h> int main(void) { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } printf("sum = %d\n", sum); return 0; }运行结果当然是5050。这个例子虽然简单,但它把循环的骨架完整地展示了一遍。我建议初学者拿到任何循环需求,都先在纸上或者注释里把三个要素写出来,再动手敲代码。这个习惯一旦养成,能少写一半的bug。
这里我要额外强调一个细节:循环体内的代码要尽量少做无关的事。上面的循环体只有一句sum += i,干净利落。有些人喜欢把printf放进循环体里打印每个中间值,方便调试,这可以理解,但正式写逻辑时不要这么干——循环体每执行一次,printf就跑一次,100次循环就是100次输出,既拖慢程序又把日志刷得乱七八糟。调试用临时打印没问题,记得最后删掉。
3.2 循环次数的口算公式
写循环最怕边界搞错,比如想循环10次却实际跑了9次或者11次。这里分享一个我常用的口算方法。
假设循环变量初始值是start,每次增加step,条件是变量< end或者<= end,循环次数可以这么算:
- 条件为
i < end时,循环次数 = ceil((end - start) / step),其中ceil是向上取整。 - 条件为
i <= end时,循环次数 = floor((end - start) / step) + 1。
举个例子,for (i = 0; i < 10; i++),次数 = ceil((10 - 0) / 1) = 10次。for (i = 0; i <= 10; i++),次数 = floor((10 - 0) / 1) + 1 = 11次。两者差了1次,而这点差别恰恰是很多bug的来源。
你可能会想,这么简单的循环还需要算吗?确实,步长为1且从0开始的情况不复杂,但一旦步长不是1,或者起始值不是0,口算公式就很有用了。比如for (i = 3; i <= 15; i += 2),次数 = floor((15 - 3) / 2) + 1 = 7次。你要是靠数的话,很容易数晕。
还有一个容易被忽略的点:循环变量的更新步长和边界条件的单位必须一致。如果步长是2但条件还是按步长1的心智来写,就会多算或少算。我写循环前会先用笔头把次数算一遍,花不了10秒钟,但能省下调试时的一小时。
3.3 循环体内的代码组织:把不变的东西挪出去
实操中你会发现,同样一个循环,不同的人写出来的效率天差地别。一个常见的优化点是循环不变量的提取。看下面的代码:
for (i = 0; i < n; i++) { double factor = computeFactor(); // 每次循环都重新计算 result[i] = data[i] * factor; }如果computeFactor()的结果在循环过程中不会变,那把它放进循环里就是浪费。正确做法是提到循环外面:
double factor = computeFactor(); for (i = 0; i < n; i++) { result[i] = data[i] * factor; }这一改动对可读性的帮助比性能还大——它明确告诉读代码的人:“factor在循环期间是常量”,减少了理解的成本。我第一次在实践中这样改代码时,发现运行时间从肉眼可见的卡顿变成了瞬间完成,那时候我才真正理解“循环体内的每一行代码都会被放大N倍”这句话的分量。
另一个组织原则是不要在循环体内改变循环变量的值(除非你有非常明确的原因)。循环变量是循环的控制中枢,在循环体内随意修改它,会让循环次数变得不可预测。有些人写查找代码时喜欢在找到目标后把i改成n来制造循环结束,这种写法虽然能跑,但可读性很差,也让循环的“契约”变得模糊。更好的做法是用break,语义清晰。
4. 常见问题与排查技巧实录
4.1 死循环:最常见的C语言翻车现场
死循环大概是C语言初学者遇到的第一个“看似睡觉实则崩溃”的问题。代码没有蹦出任何错误,控制台也不输出东西,程序像被定格了一样,其实它正在疯狂地做无用功。
排查死循环,我的套路是三步走:
- 先看条件是否可能永远为真。比如
while (i >= 0)配合无符号整型就会永远为真。把条件两边列出来,逐个代入几个关键值,判断有没有“出口”。 - 再看循环体内是否更新了关键变量。
continue跳过更新、更新语句写在条件分支里、或者干脆忘了写更新,都是常见原因。 - 最后看循环体的花括号是否匹配。有时候你本以为某行代码在循环内,实际上它在循环外,导致条件里的变量根本没被更新。
我调过一个印象很深的bug,同事写的代码长这样:
int i = 0; while (i < 100); { printf("%d\n", i); i++; }发现了吗?while (i < 100);后面多了一个分号。这个分号让while的空循环体被执行了,而i++所在的代码块在while之外,根本不会执行。于是程序直接卡在while的空转里。这种一个分号引发的血案,在C语言里实在太常见了。排查时看到while后面紧跟分号,就要格外警惕。
4.2 循环变量溢出与类型转换
循环变量的溢出不像死循环那么明显,但它会造成非常诡异的运行结果。典型场景出现在循环次数特别多,或者循环变量被用于累加的时候。
看这个例子:
int i; int sum = 0; for (i = 0; i < 50000; i++) { sum += i; }int在多数平台上是32位有符号整数,最大值为2147483647。如果循环次数和累加值的乘积接近或超过这个上限,sum就会溢出,出现“负的总和”这种违背直觉的结果。这个问题的根源是int的范围有限,而你们计算的目标超过了它。
解决方案很简单:预估你的数值范围,选择long、long long甚至unsigned long long。对于需要精确控制范围的场景,可以在循环里加一个保护判断:
long long sum = 0; for (int i = 0; i < 50000; i++) { sum += i; }另一个和类型相关的坑是隐式类型转换。比如循环条件里混合了int和unsigned int,编译器会隐式地把int转换成unsigned int再比较,这可能导致比较结果和你以为的不一样。最简单的规避办法就是:循环里不要混用有符号和无符号类型,需要比大小就统一用同一种类型。
4.3 边界判断错误:少一次、多一次的经典失误
循环少跑一次或多跑一次,是最容易让程序“看起来对但实际错”的问题。典型例子是数组遍历漏掉了最后一个元素。看这个代码:
int arr[5] = {1, 2, 3, 4, 5}; for (int i = 0; i <= 5; i++) { // 越界!最后一个合法下标是4 printf("%d\n", arr[i]); }数组长度是5,合法下标是0到4,但你用i <= 5,当i = 5时访问arr[5],这已经越界了。C语言不像某些高级语言会在数组越界时抛出异常,它只会去读取内存中紧挨着数组的未知数据,可能打印出一个莫名其妙的值,也可能直接导致程序崩溃。这类bug调试起来非常头疼,因为它不像死循环那样“一眼可见”。
我的经验是:写数组遍历时,条件一律用i < 数组长度,绝不用i <= 数组长度。这是C语言社区里非常基础但极其重要的习惯,具备这个意识的人写出的代码会少很多隐藏问题。
4.4 条件中的赋值与比较混淆
=和==的混淆,是C语言界的“传世经典”。一个等号是赋值,两个等号是比较。条件里如果是if (i = 5),这不是在判断i是否为5,而是把5赋值给i。在C语言中,赋值表达式的值是赋进去的值,所以if (i = 5)永远是“真”。这在循环里会导致死循环或者退出条件永不满足。
我还见过更隐蔽的写法:
while (c = getchar() != EOF) { // ... }这里因为运算符优先级的关系,实际执行的是c = (getchar() != EOF),先把比较结果赋给c,c变成了0或1,而不是你想要的字符。要正确实现“先赋值再比较”,必须加括号:while ((c = getchar()) != EOF)。括号一丢,逻辑彻底变了。类似的问题在写条件表达式时一定要警惕。
为了防这类问题,有些编译器在开启警告(-Wall)时会提示“建议在赋值表达式外加括号”,建议大家编译时把警告选项打开,让编译器帮你排查这类肉眼难查的问题。
5. 嵌套循环与真实场景综合应用
5.1 嵌套循环:二维数据遍历的基本功
嵌套循环就是循环套循环,外层循环每执行一轮,内层循环就要完整地执行一遍。二维数组的遍历、乘法表的打印、矩阵运算,这些都离不开嵌套循环。
看一个打印乘法表的经典例子:
for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { printf("%d × %d = %2d ", j, i, i * j); } printf("\n"); }这个例子特别适合用来理解嵌套循环的执行流程:外层i取1时,内层j从1到1,打印1行;外层i取2时,内层j从1到2,打印2行……所以最终打印出来的是一个梯形结构的乘法表。内层的条件j <= i是关键,它利用外层循环变量的当前值来动态限制内层的循环范围,这种“外层控制行、内层控制列”的模式是嵌套循环最常见的设计思路。
写嵌套循环时,我强烈建议给循环变量起有意义的名字,比如用row、col或者i、j。最好别都用i,虽然C语言允许内层和外层使用同名变量(因为它们在不同的作用域,内层会遮蔽外层),但这会让代码读起来一脸懵。坚持“外层i、内层j、再内层k”的传统约定,在简单的二维场景里没问题,再深层的循环就要考虑用更语义化的命名了。
5.2 经典算法里的循环应用:素数判断与冒泡排序
循环在实际算法中的应用花样繁多,但万变不离其宗。拿判断一个数是否是素数来说,核心逻辑就是从2开始试着除到它的平方根,一旦发现能整除,就说明不是素数。这里有一个关键的优化点:只需要除到sqrt(n)而不是n,因为如果n有一个大于sqrt(n)的因子,那必然有一个小于sqrt(n)的因子与之对应。
int is_prime(int n) { if (n <= 1) { return 0; } for (int i = 2; i * i <= n; i++) { if (n % i == 0) { return 0; } } return 1; }这个循环里有一个很值得学习的写法:用i * i <= n代替i <= sqrt(n)。一方面避免了调用数学库函数,另一方面也避免了浮点数比较的潜在精度问题。在循环条件里直接做乘法,每次都要算一次i * i,很多人会担心损耗性能。实际上现在的编译器会在开启优化时识别这类模式,而且即使不优化,i * i的整数乘法也远比sqrt函数调用快。所以在循环里能用整数运算解决的问题,就不要引入浮点运算。
再看冒泡排序。它的核心思想是反复比较相邻元素,如果顺序不对就交换,每一轮都能把当前未排序部分的最大值“冒泡”到末尾。代码里必然是嵌套循环:
for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } }内层的j < n - 1 - i就是那个“每一轮少比较一次”的细节来源——因为经过i轮冒泡,数组末尾的i个元素已经排好序了,不需要再参与比较。这个写法是冒泡排序的核心优化点。很多教材直接写j < n - 1,功能上也对,但既然理解了原理,就应该把边界写准,既减少无效比较,也体现了对算法本身的理解深度。
5.3 循环的复杂度意识:从O(n²)到O(n)的思考
嵌套循环最容易带来性能问题。两个10000次的循环嵌套在一起,就是一亿次操作,在普通PC上也要跑一小会儿。所以写循环的时候要有基本的复杂度意识。
随着循环层数增加,复杂度会指数级增长。单层循环通常是O(n),双层循环通常是O(n²),三层循环是O(n³)。当你在一个函数里看到三层以上的嵌套循环,几乎总要想办法优化,比如用哈希表把内层查找从O(n)降为O(1),或者用双指针把O(n²)降为O(n)。
但注意,复杂度意识不等于过早优化。初学者写代码,正确性和可读性永远排在第一位。先把逻辑对了,如果后续发现性能确实有瓶颈,再回来优化对应的循环。我接手过很多“过度优化”的代码,为了省几次循环写出了一堆难懂的位运算和循环展开,结果维护成本极高。所以我建议:循环要写得清晰,同时知道它为什么够用或者不够用。
6. 循环的性能优化与长期编码习惯
6.1 循环内重复计算:把不变的东西提出去
第3章里提到了循环不变量的提取,这里再展开讲一些具体操作。循环体内的计算开销会被执行次数放大,所以循环内的每一行代码,都值得你用“这笔计算是不是每次都需要”的眼光重新审视。
看这个例子:
for (int i = 0; i < strlen(str); i++) { // 处理字符 }这段代码有个隐蔽的性能问题:strlen(str)在每次循环条件判断时都会被重新调用。如果字符串长度是10000,那strlen就要跑10000次,而字符串长度在循环过程中是不变的。正确的写法是:
size_t len = strlen(str); for (size_t i = 0; i < len; i++) { // 处理字符 }类似的还有math.h的pow、sqrt等函数,如果参数在循环里不变,就提到外面。我实践下来,这类优化的效果立竿见影,尤其当循环次数大时,可能把耗时的操作从10秒缩短到0.1秒。
不过我要提醒一点:提取的前提是确定这些值在循环过程中不会变化。如果你提取了一个可变的量,循环里逻辑就错了。所以提取之前先想清楚“这个值和循环变量有没有关系”,有关系就留在循环内,没关系就提出来。
6.2 编译器优化与代码可读性的平衡
现代编译器在开优化选项后,会帮你做很多循环层面的优化,比如循环展开、向量化、强度削减等。这就产生了一个有趣的问题:既然编译器会优化,是不是写循环时可以放飞自我?
恰恰相反。编译器优化的前提是它能分析出代码的语义。如果你的循环写得逻辑混乱、有隐藏的副作用,编译器很难做激进的优化。而且生产环境并不总是开着最高优化级别运行的,尤其是调试时经常需要关闭优化,这时候代码本身的循环效率就直接影响程序表现。
所以在写循环时,我的原则是:先写人看得懂的,再考虑机器跑得快的。以下几条算是我个人比较看重的长期习惯:
- 循环变量尽量在
for里定义,把生命周期限制在循环内,避免代码其他地方误用。 - 条件判断避免复杂的函数调用,优先用简单比较。
- 循环体内不要写无关的IO操作,打印输出放在循环外。
- 边界条件用常量或具名变量,不要用魔法数字。
比如遍历数组时,不要默认写死100,而是用一个明确的常量ARRAY_SIZE:
#define ARRAY_SIZE 100 for (int i = 0; i < ARRAY_SIZE; i++) { // ... }这样后续修改数组大小时,只需要改一处定义,循环边界会自动适配。
6.3 编码中的循环自检清单
最后分享一个我每次写完循环都会默默过一遍的自检清单,排查常见隐患:
| 检查项 | 提问方式 | 隐患示例 |
|---|---|---|
| 边界条件 | 循环次数是否符合预期?最后一个值能不能访问? | i <= n导致越界访问 |
| 类型选择 | 循环变量有无符号?会不会溢出? | unsigned配合>= 0死循环 |
| 循环变量更新 | 每轮都能更新吗?continue会跳过更新吗? | 更新语句在continue之后被跳过 |
| 花括号匹配 | 循环体的范围是否如你所想? | while后多分号导致空循环体 |
| 运算符 | 用了==还是=? | 条件里误用赋值 |
| 循环内重计算 | 有没有可以提到循环外的常量? | 每次循环都调用strlen |
| break层级 | 只想跳出内层还是跳出全部? | 嵌套循环里break只跳一层 |
这个清单花不了30秒,但能帮我拦截掉相当一部分循环类bug。特别是面试或者比赛写代码时,循环写完之后快速过一遍清单,能避免很多低级的提交失败。
写了这么多,我自己最深的感受是:循环语句的知识点并不难,难的是写完循环之后那份“边界感”。所谓边界感,就是清楚循环从哪里开始、到哪里结束、中间如何处理临界情况。我见过太多代码,语法完全正确,程序也能跑,但就是循环的边界判断得不清不楚,导致线上问题不断。写循环时多问自己一句“最后一次循环发生在什么时候”,很多问题都能在刚动手的那刻被你挡在门外。循环是程序最基础的节拍器,把节拍打准了,后面的路就稳了。