C语言分支与循环:从if判断到for遍历的编程基础实战
写C语言最爽也最容易翻车的地方不是指针不是结构体而是分支和循环。很多新手学C语言语法背了一堆一到写程序就卡在“为什么我的if没反应”“为什么这个循环一直跑不停”。我自己带过几轮入门项目也踩过不少坑今天就把《C语言》分支和循环上这部分从头到脚拆一遍先讲清楚分支和循环各自怎么用再给出能直接上手敲的练习和排错经验。这篇适合刚开始学if、while、for的新手也适合刷了好几天题但是一遇到“判断循环”就发怵的同学。看完之后你可以照着代码敲遇到问题也有地方查。1. 核心设计思路程序为什么需要分支和循环1.1 程序默认是直线跑的但现实世界全是岔路C语言的main函数默认是从上往下一条路走到黑的。第一条语句执行完接着执行第二条直到main函数返回。这种顺序结构能解决“按流程走”的问题但现实里的需求往往不是线性的。比如“如果成绩大于等于60输出及格否则输出不及格”这种逻辑如果不用分支结构就只能写两份完全不同的代码还要靠程序员自己想办法绕过。分支结构的意义就是让程序在执行时能根据某个条件的结果动态决定接下来走哪一段代码。在C语言里判断的依据是条件表达式的真假。很多教材会说“非零即真零为假”这一点你一定要刻在脑子里。比如if(3 2)这个条件3 2的结果是1代表真if(2 3)的结果是0代表假。关系表达式、逻辑表达式最后都会落在一个整数上真就是1假就是0。这也解释了为什么你经常看到别人写if(x)x是整数时只要x不等于0就会进入分支。之前有同学问我为什么if里不能直接写“大概是5”这种模糊判断。因为在计算机里没有“大概”只有精确的相等、大小、与或非。分支结构的本质就是用这些精确规则把现实中的岔路翻译成程序能理解的条件。理解了这一点后面看if、switch甚至循环里的退出条件都会轻松很多。1.2 重复代码是万恶之源循环用来消灭它新手写代码最容易犯的毛病是为了应付重复而复制粘贴。比如打印100个“Hello World”最笨的办法是写100行printf。这种写法不仅占地方最要命的是将来想改内容你得改100行还容易漏改。循环结构就是为了解决这类“重复劳动”而生的。看懂循环的钥匙是抓住三样东西初始状态、结束条件、状态更新。举个跑步的例子你打算跑10圈不能只喊一句“跑10圈”就完事你得知道自己现在跑了几圈每跑完一圈要计数加一还要判断是不是已经跑到第10圈。在循环里这三件事分别对应变量的初始化、循环条件的判断、循环体里的更新语句。任何一个缺失轻则少跑一圈重则跑到死都停不下来。很多人把分支和循环分开学其实是误区。实际代码里两者几乎总是混在一起循环负责“重复做”分支负责“每做一次时根据情况判断怎么做”。比如后面要写的猜数字游戏、冒泡排序都是循环和分支互相配合的典型例子。所以这篇虽然分成分支、循环两块讲但你心里要清楚它们是一套组合拳。2. 分支结构给程序装上一个“如果”2.1 if和else先把最常见的形态吃透if语句是C语言分支结构的绝对主力。基本形态很简单if后面括号里放条件成立就执行后续语句块。如果再加上else就是“条件成立执行一段不成立执行另一段”。我先给一个最经典的奇偶判断#include stdio.h int main(void) { int n; printf(请输入一个整数); scanf(%d, n); if (n % 2 0) { printf(%d 是偶数\n, n); } else { printf(%d 是奇数\n, n); } return 0; }这段代码里有几个细节值得新手注意。第一scanf里必须写n不是写n因为scanf需要把读到的整数放到变量n的内存地址里忘了取地址符程序大概率崩溃或者得到奇怪的值。第二n % 2 0里%取模运算的优先级高于关系运算符所以这里其实是(n % 2) 0是对的。第三if后面的圆括号不能少而语句块的大括号我建议永远都写哪怕只有一句话也写。这不是强迫症是为了防止以后往里面加代码时忘加括号导致逻辑悄悄变味。再来看多分支。现实中判断往往不止两路比如成绩等级90分以上是A80到89是B70到79是C60到69是D60以下是F。用else if连成一串是常见写法if (score 90) { grade A; } else if (score 80) { grade B; } else if (score 70) { grade C; } else if (score 60) { grade D; } else { grade F; }这段代码的精髓在于顺序。假设score是85第一个条件score 90不成立接着检查第二个score 80成立于是grade变成B。后面的分支不会再执行。这就意味着条件的先后顺序会直接影响结果。如果我把score 60放在第一个那么75分就会直接得到D后面全部被跳过。新手写多分支时要么想清楚从高到低或从低到高的顺序要么干脆把范围写完整比如score 80 score 90。我个人的建议是初学阶段优先保证逻辑清晰哪怕多写一点条件也比日后读不懂要强。2.2 switch-case多路分支的“平铺”玩法当判断的对象是整数或者字符并且可能取值是有限几个离散值的时候switch-case往往比一堆else if看着更清爽。它的基本结构是switch(表达式)后跟一串case常量程序匹配到哪个case就进入哪里。注意每个case最后都要加break否则会发生“穿透”——也就是执行完当前case后继续执行下一个case的代码。很多人嫌弃switch不如if灵活但它在“菜单选择”“命令解析”这类场景下非常好用。我写一个简单的菜单判断char op; printf(请输入操作符(、-、*、/)); scanf( %c, op); switch (op) { case : printf(你选择了加法\n); break; case -: printf(你选择了减法\n); break; case *: printf(你选择了乘法\n); break; case /: printf(你选择了除法\n); break; default: printf(未知操作\n); break; }先说一个很多新手都会踩的坑scanf( %c, op);这里%c前面有没有空格差别很大。如果你写成scanf(%c, op);在用户输入完上一个内容后缓冲区里往往还留着一个换行符%c会优先读到那个换行导致switch直接进default。这个细节后面还会再讲但你现在最好养成习惯读单个字符时格式串前面加个空格。switch里的case后面的值必须是整型常量表达式不能是变量也不能是浮点数。字符类型char本质上是整数所以能用来做case。如果你有意让多个case执行同一段逻辑可以故意省略break比如case a: case A: 共用一段代码但正常业务里千万别随手漏break漏了就是bug。2.3 逻辑表达式和三个高频翻车点if里的条件经常不止一个简单关系这时要用逻辑运算符来组合表示“并且”||表示“或者”!表示“取反”。它们的优先级要特别小心!最高然后是算术运算符、关系运算符、、||。很多人会写if(a 0 a 100)来限定范围这没问题但你要理解编译器是先算a 0得到0或1再和a 100的结果做运算。更实用的一点是短路求值左边一旦为假右边根本不会执行||左边一旦为真右边也不会执行。利用这个特性你可以放心写if(i n a[i] 0)只有i没越界时才会去访问a[i]不会因为短路把数组下标搞炸。关于分支有三个高频翻车点我一个个说。第一赋值当判断。if(a 1)是C语言新手最容易犯的错。单个等号是赋值会把1赋给a然后整个表达式的值就是1恒为真。哪怕你原意是判断a是否等于1结果也变成了“不管是啥先塞成1再说”。这个bug隐蔽在于编译器只会警告不会直接不让过。我自己的防御习惯是写成if(1 a)把常量放左边。如果不小心少打一个等号变成if(1 a)编译器会直接报错而不是默默留下一个后门。第二悬空else。C语言有一条配对规则else总是和最近的、没有配对的if结合。缩进只是给人看的编译器不看缩进。比如下面这段if (a 0) if (b 0) printf(两个都正\n); else printf(a不为正\n);你从缩进看似乎else是想配对外层if表示“a不大于0”时执行。但实际上编译器会把else配给内层if。结果是a 0且b 0时也会走进那个else输出“a不为正”完全不是你的本意。解决方法是给内层if加上大括号最好从头到尾所有if都加大括号。第三if行尾多加分号。if(x 0);后面那个分号表示一个空语句属于if控制的循环体或语句块。于是不管x是多少后面的printf照样执行看起来就像if失效了一样。这种错误排查起来很气人但解决方法很简单写完if后先看行尾有分号就删掉。3. 循环结构让程序学会重复3.1 while循环先检查再进厂while循环的语法是while(条件) { 循环体 }。它的执行逻辑是先判断条件条件为真就执行一次循环体执行完再回到条件的判断如此往复直到条件为假才退出。所以while是“先看门再进门”条件一开始就不满足的话循环体一次都不会执行。我来写一个经典问题从1加到100。#include stdio.h int main(void) { int i 1; int sum 0; while (i 100) { sum i; i; } printf(1加到100的结果是%d\n, sum); return 0; }这里的三要素很清晰i初始化为1是初始状态i 100是结束条件i是状态更新。三个要素少一个都会出事。如果忘了写ii永远等于1i 100永远成立循环就变成死循环。sum初始化为0也很关键因为累加要从0起步如果初始化为1最终结果会多1。这种循环变量初始值的问题是练习分支和循环时最常被忽略的细节。while还有一种配套用法while(1)故意写出死循环然后在循环体里用break退出。它适合处理“不确定循环多少次但满足某个条件就停”的场景。比如读取用户输入直到输入0为止while (1) { scanf(%d, n); if (n 0) { break; } sum n; }这段代码其实同时用了循环和分支循环负责“不停地读”分支负责“看到0就下车”。很多新手不习惯把break放在if里觉得循环出口应该只有一个但实际工程里这种写法非常常见。放心用break不会拖累性能反而让逻辑更清楚。3.2 do-while至少执行一次的循环do-while和while的区别只有一个do-while先执行一次循环体再去判断条件。所以它至少会执行一次。语法是do { 循环体 } while(条件);注意结尾有一个分号这是while和do-while最容易被看漏的区别。什么时候需要“至少执行一次”典型场景是输入验证和菜单展示。比如用户要设置密码密码必须满足长度或者做猜数字游戏时你要先让用户输入一次再判断猜得对不对。如果用while循环你得在循环开始前单独写一次输入代码会重复用do-while就可以把输入动作统一放在循环体里。举个例子模拟一个密码校验int password; do { printf(请输入密码); scanf(%d, password); } while (password ! 123456); printf(密码正确\n);不管用户第一次输入什么循环体会先执行一遍然后判断条件。如果不对就再执行一遍直到正确为止。这比while版少写一次input语句逻辑也自然得多。新手容易忘掉do-while后面的分号编译会报错所以看到“缺少分号”的报错信息先往do-while那边想一想。3.3 for循环把三要素牢牢捆在一起for循环是所有循环里“套路感”最强的。它把初始化、条件判断、更新这三个动作都写在for后面的一对括号里用分号隔开for(初始化; 条件; 更新)。执行顺序是先执行一次初始化然后判断条件条件为真就执行循环体再执行更新再判断条件一直到条件为假。用for重写1到100累加int sum 0; for (int i 1; i 100; i) { sum i; }一眼看上去i的初始值、终止条件、每次加几全都集中在for那一行比while版清晰多了。这就是为什么“知道要循环几次”的场合首选for循环。数组遍历是for最经典的舞台int a[10]; for (int i 0; i 10; i)下标从0开始到9结束千万不能写i 10那样就多访问了一个不存在的元素。有一点要注意int i声明在for的内部是C99标准才支持的。如果你的教材和考试环境还在用老标准需要把int i;提前声明。现在主流编译器都默认支持C11但有些在线题库版本比较老遇到for(int i...)报错就先改成int i;再写for(i 0; ...)。在循环里还有一个重要操作break是直接跳出整个循环continue是跳过本次循环剩下的代码直接进入下一次循环。这两个控制词和分支结合很紧密但连着展开容易一下子灌太多。这篇先掌握基本循环下一篇再专门讲break、continue的坑和技巧。4. 实操过程把分支和循环真的跑起来4.1 猜数字游戏分支和循环的完整组合理论讲再多不如亲手敲一个能跑的游戏。猜数字游戏的逻辑是程序随机生成一个1到100的数字用户反复猜程序每次提示“大了”或“小了”直到猜中为止。这个题目几乎覆盖了本章所有知识点随机数、do-while、if/else if/else、计数器。#include stdio.h #include stdlib.h #include time.h int main(void) { int answer, guess, count 0; srand((unsigned)time(NULL)); answer rand() % 100 1; printf(我心里想了一个1到100之间的数字。\n); do { printf(请猜一个数字); scanf(%d, guess); count; if (guess answer) { printf(大了\n); } else if (guess answer) { printf(小了\n); } else { printf(猜中了一共用了%d次\n, count); } } while (guess ! answer); return 0; }这里有几个点要讲清楚。rand()生成一个0到RAND_MAX之间的随机整数rand() % 100得到0到99再加1就得到1到100。但你直接连续运行两次程序会发现每次的随机数序列是一样的因为rand默认种子固定。所以要先调用srand((unsigned)time(NULL))用当前时间作为随机种子每次运行的答案才会不一样。time(NULL)需要包含time.h头文件。为什么用do-while而不是while因为我们必须先让用户猜一次程序才能判断猜得对不对。如果用户第一次就猜中do-while也能正确处理执行一次循环体后判断guess ! answer为假退出循环。这个场景用do-while就是“刚刚好”的感觉。整段代码里if/else if负责提示方向while条件负责决定是否结束计数器count放在循环体里每次累加最后输出猜测次数分支和循环配合得明明白白。4.2 打印九九乘法表嵌套循环练手感九九乘法表是每一个学C语言循环的人都要过一遍的坎。它考的是嵌套循环外层循环控制行内层循环控制列两层的循环变量还会相互影响。代码长这样#include stdio.h int main(void) { for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-2d , j, i, i * j); } printf(\n); } return 0; }先把运行结果在脑子里过一遍第一行i1内层j从1到1输出111第二行i2j从1到2输出122和2*24第三行i3输出三组依次类推。内层循环的条件是j i不是j 9。如果写成j 9每一行都会输出9列乘法表就变成完整的9x9方块了。这个j i就是嵌套循环里最值得品的地方外层变量会参与内层条件的判断让内层循环的规模跟着外层变。printf里的% -2d是左对齐并占2位配合后面的空格能让输出对齐得好看一点。如果你直接用%d两位数乘法会出现错位观感差很多。这个格式控制以后做题也会经常用建议顺手记住。如果想要完整版的9x9把j i改成j 9就行想要倒三角改外层循环的方向和范围即可。自己动手改一改比你干看十遍要有效果。4.3 冒泡排序循环与分支的经典算法组合九九乘法表只用了循环还没用到分支。冒泡排序就很典型地同时用上了双层循环加if判断。冒泡排序的思路很简单每一轮比较相邻两个数如果前一个比后一个大就交换一轮下来最大的数会像气泡一样浮到最后面。然后下一轮就不管它继续排前面剩下的数。#include stdio.h int main(void) { int a[5] {5, 1, 4, 2, 8}; int i, j, tmp; for (i 0; i 4; i) { for (j 0; j 4 - i; j) { if (a[j] a[j 1]) { tmp a[j]; a[j] a[j 1]; a[j 1] tmp; } } } for (i 0; i 5; i) { printf(%d , a[i]); } printf(\n); return 0; }这段代码值得逐行拆解。数组长度是5外层循环需要跑4轮因为每轮确定一个数的位置剩下最后一个数不用再排所以写成i 4。内层循环每轮要比较相邻元素而每轮结束后都有一个数已经归位所以内层的上界是4 - i。如果不减i你会多比较已经排好的元素甚至访问a[5]越界。这就是我在常见问题里反复强调的边界问题虽然程序不一定崩但结果可能是错的。if条件做的是“比较后决定是否交换”。交换要用第三块空间tmp因为a[j] a[j 1]之后a[j]原来的值就被覆盖了没有临时变量就丢了。这个三步交换是C语言里的肌肉记忆写多了自然就会。冒泡排序本身性能不算好但作为理解“循环里套分支”的入门算法非常合适。5. 常见问题与排查技巧5.1 判断条件明明“相等”if却就是不进入这类问题遇到太多了。第一个可能是把写成了。我在前面反复提过if(a 1)会把1强行赋给a条件恒为真而不是判断相等。反过来也一样if(a 0)条件恒为假整个分支永远不执行这种更隐蔽。第二个可能是浮点数比较。两个浮点数直接通常不靠谱因为浮点有精度误差0.1 0.2很可能不等于0.3。你要比较浮点数是否近似相等要用fabs(a - b) 1e-6这种写法这需要math.h头文件。第三个可能是scanf没读进去。比如scanf(%d, n)返回值为0说明输入格式不匹配n还是旧值if判断的当然不是你新输入的内容。排查这类问题最土也最有效的方法是在if前面打印printf(n%d, n1的结果是%d\n, n, n 1);打印出来一看问题原因就清楚了。5.2 循环不退出多半是“三要素”少了哪一个死循环是所有循环新手最容易撞的墙。程序卡住不动CPU风扇越转越快其实就是某个循环条件永远为真。排查思路很简单先把循环变量、条件、更新这三件事列出来。常见代码int i 1; while (i 10) { printf(%d\n, i); }很明显循环体里没有ii一直是1条件永远成立。还有一种更隐蔽的是continue跳过更新int i 0; while (i 10) { if (i % 2 0) { continue; } printf(%d\n, i); i; }当i为偶数时continue直接跳到下一次循环i根本没机会执行i永远停在0循环卡死。解决这种问题要么把i放在continue之前要么用for循环因为for的更新语句写在条件后面即使遇到continuefor也会在进入下一次前执行更新表达式这就是我推荐新手多用for的原因之一。另外for(i 0; i 10; i);这种分号问题也会导致循环体为空看起来像没执行一样排查时同样要看行尾。5.3 scanf缓冲区里的换行符让你的程序“跳过输入”这个坑非常经典。假设你先用scanf(%d, n);读一个整数然后再用scanf(%c, ch);读一个字符。运行时你输入5按下回车程序并不会等你输入字符而是直接结束或输出奇怪结果。原因在于按回车留在缓冲区里的换行符\n会被%c当作一个字符读走。解决办法是格式串里加空格scanf( %c, ch);这个空格会跳过空白字符包括换行、空格、制表符。还有一个常见的连带问题输入的不是数字比如用户输入abcscanf(%d, n)返回0n没有被赋值但abc还留在缓冲区。如果程序处于循环里它会对同一个abc反复读取形成“输入卡死”。处理方法是检查scanf的返回值如果返回0就用while(getchar() ! \n);把这一行残渣清掉再继续下一次输入。5.4 嵌套循环的边界多一次少一次都不行嵌套循环出问题基本都出在边界判断上。九九乘法表写成j i没问题你改成j i第一行就什么都不输出而且整个表会少一条对角线。冒泡排序的内层写成j 4 - i没问题你改成j 4 - i访问a[j 1]时就可能越界。数组遍历更是重灾区长度为n的数组下标范围是0到n-1循环条件必须写i n或者i n-1写i n就是访问越界。我不希望你靠死记“到底写不写等于”而是养成一个习惯每次写完循环挑几个特殊值在脑子里跑一遍比如i0、i1、in-1、in。拿冒泡排序举例最外层循环i0到3最后一轮i3时内层j 1只比较一次没问题如果外层写成i 4最后一轮i4内层j 0循环体不执行看似没影响但循环轮数不对排序可能会漏掉比较。这种边界问题手推几行就知道哪里不对。5.5 编译报警告别无视尤其是if和循环相关很多新手觉得编译器警告无所谓能运行就行。但分支和循环里的很多问题编译器其实已经用警告提醒过你了。比如if(a 1)GCC会提示“suggest parentheses around assignment used as truth value”。这句话翻译过来就是你的赋值被当成判断条件了建议加括号。你只要用gcc -Wall编译这类问题能提前暴露一半。平时练习养成编译带上-Wall的习惯不要只看到没有error就交作业。Warning和Error的区别只是“程序能不能跑”不代表“程序是对的”。你跑出来的结果可能是错的而且错得莫名其妙追查半天才发现原来编译器早就警告过。6. 写在最后的经验我当年学分支和循环时最大的体会是别急着写代码先拿纸笔把流程画出来。哪怕只是画几个框、几条箭头把“当前变量是什么”“条件成立走哪边”“循环怎么回来”画清楚写代码就只是把图翻译成C语言而已。很多同学写程序卡壳不是不会语法而是脑子里根本没有流程。你让他讲一下“用户输入0就退出”要怎么走他讲不清楚那代码怎么可能写对。还有一个我强烈推荐的笨办法改参数。把九九乘法表的9改成5看它会输出什么把猜数字的随机范围改成1到1000把冒泡排序的数组长度改成10数组内容自己编一组。每次只改一个地方然后运行观察变化。这种看着无聊的折腾反而是理解嵌套循环边界、变量更新时机的最好方式。C语言的分支和循环是整个程序世界的骨架后面链表反转、二叉树遍历、各种算法题全都从这上面长出来。这篇先说到这下一篇我们继续把break、continue、goto和更复杂的嵌套逻辑盘清楚。如果你照着代码敲的时候遇到报错或者运行结果不符合预期欢迎随时留言一起排查。