1. 循环在生活上的作用
循环就像在操场上绕圈跑步,需要有以下几个逻辑:
- 开始:循环的开始,第一次循环
- 计数:每循环一次时候要有一次计数
- 循环的内容:每个循环要干的事情,要执行的语句
- 结束:循环的结束,跳出这个循环
循环的作用:
- 避免重复,提高效率
- 处理大量内容,简化代码
- 灵活控制程序的行为(有检查机制,根据条件控制循环次数)
2. 初探while
循环
while
循环的基础语法为:
1 | while (expression) { |
与if
类似,expression
是一个结果为真(true)或假(false)的表达式,当它为真的时候,循环执行括号里的statement
,一直到它是假的时候直接跳出循环,继续执行下面的程序。
例如,一个人需要跑步十圈:
1 | const uint8_t TOTAL_LAPS = 10; // 总圈数(常量) |
const
关键字代表此变量为常量,程序中不可改变,其变量名一般全为大写。
一个while
循环,需要有循环要达到的目标与目前的初始化值。没有达到目标时,循环一直进行,当达到想要的目标时,循环结束。
使用continue;
语句可以立即结束当前循环,跳过本次循环的剩余部分,并进入下一次循环,可以在需要提前结束此次循环时使用。
使用break;
语句可以立即结束循环流程,继续执行循环外的代码,可以在需要跳出循环时使用。
正常来说,每次循环都需要有一个迭代点,使初始化值不断向目标靠近,否则将会进入死循环。
一般的while
循环的整个流程可以用下面的流程图来表示:
graph TB A[初始化变量] --> B([循环开始]) B --> C{循环条件判断} C -- 条件为真 --> D[执行循环体] D --> E[更新循环变量] E --> C C -- 条件为假 --> F([循环结束])
由此,一个循环需要包括以下五个部分:
- 初始化
- 循环条件判断
- 循环体
- 迭代点
- 终止条件
3. 自动贩卖机案例
案例要求:
- 模拟自动贩卖机买饮料的过程
- 假设机器只支持硬币,每次投入一枚硬币
- 只卖一种饮料,每瓶价格是5元
- 用户需要一直投币,直到金额足够,机器才能提供饮料
- 附加条件1:机器只支持1元、2元、5元的硬币,其他的金额不支持
- 附加条件2:用户投入的金额大于饮料金额时,需要为用户找零
代码实现:
1 |
|
当使用
scanf_s()
获取输入时,当数据类型为例如uint32_t
的标准类型时,虽然也可以使用PRIu32
标准类型,但是对于输入,有专门的SCNu32
类型,也包含在头文件inttypes.h
中。
4. 遇到循环问题的解决方案与经验
在循环部分:
- 确保所有用到的变量都被正确初始化;
- 确保循环开始条件、结束条件正确,不会导致无法进入循环或进入意料之外的死循环;
- 确保迭代点逻辑正确,经过迭代,可以靠近最终要实现的目标。
在企业中,循环体内尽量不要做太多的计算,减少对函数的调用,也应该避免使用一些容易破坏循环的数据结构。
程序中,变量命名要尽量直观合理,常量一定要全部大写,且一定要添加变量的注释,这样会提高代码的可读性,Visual Studio中鼠标悬停时候也可以看到注释内容,这在排查问题时候会有很大的帮助。
排查时在关键的地方也可以多用printf()
来打印关键变量的值,来帮助排查问题。
5. break
与利用死循环-求和案例
案例要求:
- 写一个计算求整数和的案例
- 用户从终端输入一系列数字,数字间用回车隔开,直到用户输入一个单一的“0”作为结束
- 程序把所有输入的数字相加,输出最终结果
第一种程序实现:
1 | // 初始化变量 |
第一种写法就是按照一般的思路,while
循环中的条件与循环结束时需要满足的条件相反,即number != 0
,但此时变量number
的值如果被初始化为0
,进入while
循环之前的第一次判断就无法通过,导致程序无法进入循环。
因此,这种写法应当将number
的初始值初始化为0
以外的其他值,这样就可以正常进入循环,程序正常运行。
值得一提的是,
number
的值不管初始化为多少,只要不是0
,就都不会影响程序运行,因为它的值会在scanf_s()
中被重新赋值(从键盘输入重新给number
赋值)。如果不对
number
做初始化,程序会直接报错。
这种写法看似可行,但逻辑比较绕,不是一个好的写法,一个比较好的写法如下:
1 | // 初始化变量 |
while
的条件中写true
,可以不做任何条件判断,直接进入死循环,当输入的number
不是0时,将其加和给sum
,并进行下一次循环,一直到输入的number
为0的时候,使用break;
语句打断并跳出循环。
此时
number
只需要声明,不做初始化时程序也不会报错。
6. 处理字符和字符串的退出检测问题(选修)
上一节的程序中,循环的结束是用0来判定的,但是0也是一个数字,这样判定并不好,最好用一个字母,例如输入一个单独的“q”时,程序结束。
如果对上一节的程序不做修改,直接输入字符“q”,程序就会陷入死循环,这是因为
scanf_s()
不会对非预期的类型输入做处理,也就是目前程序中的预期输入类型为%u
,如果输入为其他类型,输入的数据就一直卡在内存中无法被处理,每次循环时,scanf_s()
都会对内存中的这个数据做处理且处理失败,从而不会请求新的输入,使得程序陷入死循环。
下面来做这个拓展,对于一个简单的扩展来说,使用一个char
类型的字符代替数字输入,但是此时此求和器就只能计算10以内的加减法,输入的数字超过10,程序就会出问题。
1 | // 初始化变量 |
其中的scanf_s()
需要注意,因为输入数字并输入回车时,实际的输入为数字+\n
,因此如果要让程序正常运行,需要在%c
前加空格,且输入字符时,需要加上scanf_s()
的第三个参数。
这个程序虽然实现了输入q退出,但是有个很明显的问题,就是只能计算10以内的求和,接下来将使用比较复杂的知识来解决这个问题。
1 |
|
这个程序比较复杂,但也比较完善,用到了数组、指针、库函数调用、错误码的使用等知识,几乎对输入数据所有可能出现的情况做了处理,可用于参考学习。
7. do-while
与while
的区别
do-while
循环的基础语法为:
1 | do { |
与while
类似,expression
是一个结果为真(true)或假(false)的表达式,当它为真的时候,循环执行括号里的statement
,一直到它是假的时候直接跳出循环,继续执行下面的程序。
与while
不同的是,while
是在执行statement;
前先判断一次expression
,当它是真的时候进入循环,为假的时候不进入循环,但是do-while
是先执行一次statement;
,然后判断expression
来决定是否要继续循环。
由此可见,do-while
循环的流程图如下:
graph TB A[初始化变量] --> B([循环开始]) B --> C[执行循环体] C --> D[更新循环变量] D --> E{循环条件判断} E -- 条件为真 --> C E -- 条件为假 --> F([循环结束])
do-while
用于需要在进入循环前至少执行一次循环体的情况,比如第5节中的求和案例,除利用死循环和break;
配合之外,可以使用do-while
实现:
1 | // 初始化变量 |
此时即使不对number
做初始化,程序也不会报错。
8. 随机数猜数游戏案例-do-while
练习
猜数游戏案例要求:
- 程序自己生成一个1-100的随机数
- 循环读取用户输入的数字,直到用户输入的数字与目标数字相同
- 当大于或小于目标数时,给出对应的提示
代码实现:
1 |
|
这个程序与上上一节出现的问题一样,当输入非法数据,例如输入一个字符“q”的时候,程序会进入死循环,因此需要检查判断输入是否符合要求,下面两种处理方式用于参考学习。
第一种处理方式:
1 | uint32_t secretNumber; // 用于存储生成的随机数 |
使用status
储存scanf_s()
的返回值,当返回值不为1时,输入无效,此时清除输入缓冲区并进入下一次循环。
第二种处理方法比较复杂:
1 | uint32_t secretNumber; // 用于存储生成的随机数 |
9. continue
和break
联用条件判断和实际用途
案例要求:
- 用户输入一系列数字,需要忽略所有负数和大于100的数,0-100之间的数才是有效数字
- 对于每一个有效数字,如果它是偶数,告知用户这个数是偶数
- 如果它是奇数,告知用户这个数是奇数
- 当用户输入-1时,退出程序
代码实现:
1 | int32_t number; |
continue
和break
与前面提到的卫语句类似,一般与if
联用,用于对一些特殊情况做一些特殊处理,因此一般需要在循环体的靠前部分调用。
10. 初探for
循环
for
循环的基础语法为:
1 | for (init-expression; cond-expression; loop-expression) { |
for
语句的括号中可以包含三个表达式:
init-expression
:为循环指定初始化,在循环中只在开始时执行一次,如int i = 0
,如果在这里声明并初始化,这个变量就是局部变量,只能在这个for
循环中使用;cond-expression
:是循环条件,和while
循环类似,是一个结果为真(true)或假(false)的表达式,当它为真的时候,循环执行括号里的statement;
,一直到它是假的时候直接跳出循环,继续执行下面的程序。一般与init-expression
中定义的初始化循环变量有关;loop-expression
:是迭代点,一般是对init-expression
中定义的初始化循环变量做操作,使其每次循环都接近cond-expression
。
可以用流程图表示为:
graph TB A([循环开始]) --> B[初始化表达式
(init-expression)] B --> C{条件表达式
(cond-expression)} C -- 条件为真 --> D[执行循环体
(statement;)] D --> E[更新表达式
(loop-expression)] E --> C C -- 条件为假 --> F([循环结束])
对于while
循环中的跑步案例,用for
循环可以这样实现:
1 | const uint8_t TOTAL_LAPS = 10; // 总圈数(常量) |
11. 训练:求和平方
要求:
- 用户输入一个正整数N,程序计算从1到N的所有整数的平方和
代码实现:
1 | uint32_t number; // 用户输入的正整数 |
12. 训练:倒数五个数
要求:
- 用户输入一个正整数N,程序从N开始,一直倒数到1
代码实现:
1 | uint32_t start_nmber; // 倒数的起始数字 |
扩展:目前程序会直接倒数到结束,现扩展程序功能,实现按实际的时间来倒数。
不同平台下有不同的库函数来实现延时倒数功能,例如Windows平台下可以引用头文件windows.h
,使用函数Sleep()
来实现延时,此函数传入参数的单位是毫秒。
Linux平台下可以引用头文件
unistd.h
,使用sleep()
来实现延时,此函数传入参数的单位是秒。
具体代码实现为:
1 |
|
13. 训练:阶乘
要求:
- 用户输入一个正整数N,计算N的阶乘(
N!
)
代码实现:
1 | uint32_t number; // 用户输入的正整数 |
14. 训练:判断素数
开平方根:引入头文件
math.h
后,使用函数sqrt(number)
即可对传入这个函数的number
计算开平方根。素数:又被成为质数,一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数(规定1既不是质数也不是合数)。
要求:
- 用户输入一个除0和1正整数,程序将判断这个数是否为素数,并输出结果
分析:
当一个数字N不是素数时,会有两个数字的乘积等于N,而又得知,N的开方与它自身相乘,结果也是N,则前面得到的两个数,不可能都大于,也不可能都小于,因此他们只能分布在两侧,也就是这两个数中的较小值,必定小于。
又因为一个数的所有因数必定是对应的,因此只要一一检查N能否被从1到之间的所有整数整除即可。
代码实现:
1 | uint32_t number; // 用户输入的正整数 |
程序也可以有优化的方法,for
循环的循环条件中的i <= sqrt(number)
包含开根操作,在程序中比较耗时,可以用更简单的i * i <= number
来替代。
15. 训练:简单的乘法表
要求:
- 用户输入一个正整数N,程序生成一个简单的当前数的乘法表并输出
程序实现:
1 | uint32_t number; // 用户输入的正整数 |
16. 训练:简单绘画正方形
要求:
- 用户输入一个正整数N,程序绘制出一个用“*”组成的正方形
- 正方形的长和宽都是N
代码实现:
1 | uint32_t size; // 正方形的边长 |
17. 训练:简单绘画三角形
要求:
- 用户输入一个正整数N,程序绘制一个用“*”组成的三角形
- 三角形的行数等于N,每一行的”*“的个数从1开始,逐行增加
代码实现:
1 | uint32_t size; // 正方形的边长 |
18. 训练:金字塔数字
要求:
- 用户输入一个正整数N,程序绘制一个行数为N的正金字塔
- 每一行的数字个数逐行增加,最中间的数字是从1到N,其他数字由最中间的数字向两侧递减到1
- 每一行数字居中显示
例如N为5时,输出应该为:
1 | 1 |
代码实现:
1 | uint32_t levels; // 定义金字塔的层数变量 |
此程序还能做一些优化,打印空格时,每一次循环都要计算一次levels - i
,然而进入每一行时,这个值都是固定的,可以提前计算好,使得每一次循环只需要做判断即可。
打印空格部分变为:
1 | // 打印空格 |
19. 案例:进度条
要求:使用循环模拟一个进度条
代码实现:
1 | uint32_t total_steps = 100; // 总步骤数 |
这个代码实现中总步骤数字只能是100,导致进度条过长,超出一行的范围时,进度条刷新就不太正确,可以增加进度条长度和当前步骤,在每一次绘制进度条之前计算当前位置在进度条中的占比,以此来实现在进度条较短时均匀刷新。
1 | uint32_t total_steps = 100; // 总步骤数 |
20. 案例:检查组件故障
要求:
- 模拟若干组件的健康检查流程
- 每个组件会有20%的概率随机出现故障
- 此案例用于参考学习
代码实现:
1 |
|
21. 使用VS进行debug调试
在循环中遇到问题时,可以使用printf()
在合适的地方打印输出,可以了解程序执行到当前这一步时候各个循环变量的值,方便调试。
同时,Visual Studio还有一个非常强大的功能,就是debug调试,可以在想要监测的某一行代码的行号左边点击断点设置栏,为这一行代码打上断点,出现一个红色的圆点即为设置成功。
此时使用调试模式运行程序,程序执行到这一行代码时候就会停下来,同时在界面上显示目前所有相关变量的值,也可以点击Continue(继续)
来使程序继续运行,直到运行到下一个断点,或是点击单步调试按钮,让程序从当前代码的位置按每一小步继续向下运行,相关变量的值也会实时显示在界面上并随着程序的运行而变化。
断点再次点击即可消失。
这种调试手段非常强大,不需要写很多的printf()
输出,简单又高效,应该熟练掌握。
可以自行搜索其他相关资料或自己摸索,来学习这种debug调试方式。
22. 第五章结束语
循环这一章非常注重训练,一定要多思考,多训练!
每个人都有自己的优缺点,不要自负,也不必自卑。
保持谦虚,坚持学习!
本文链接: https://hanqingjiang.com/2025/07/15/20250715_C_loop/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
