1. 为什么先学流程控制再学别的——你写的程序从这讲开始才叫有脑子如果你是从第一讲跟着走过来的到这一步应该已经能写出类似输入两个数求平均值把摄氏度转成华氏度这类程序了。这些程序有一个共同特点从上到下按顺序执行每条语句都跑一遍不多也不少。这在C语言里叫顺序结构。但现实问题往往不是这么线性的。举个例子你想写一个程序判断用户输入的成绩是及格还是不及格。程序得先读到成绩这个数然后做出选择——60分以上输出及格否则输出不及格。这就是分支结构。再比如你想打印1到100之间的所有偶数难道要写50条printf当然不行。你得让某一段代码反复执行每次执行时变量加2这就是循环结构。C语言里就这三种基本结构顺序、分支、循环。任何复杂的程序——小到一个计算器大到操作系统内核——归根结底都是由这三者组合出来的。所以这一讲的内容不是某种语法技巧而是你从只会写直线到能写真正逻辑的分水岭。本讲先覆盖分支结构if/else、switch和三种循环while、do-while、for的基础用法下一讲再深入嵌套循环、break/continue的进阶场景和综合案例。有个很重要的学习心态先说清楚分支和循环的语法你半小时就能看完但真正难的是会做题。因为实际写程序时你面对的不是这题用if还是switch而是这个问题怎么拆成几个步骤每一步用哪种结构实现。这不是看教程能看会的必须上手写、上机调。所以我建议你读完每一节哪怕觉得懂了也把示例代码亲手敲一遍然后改一两个条件看看输出变没变、为什么变。2. 分支结构if / else 的顺序、匹配与常见误用2.1 最简单的if一个条件两种去向先看最基础的样子#include stdio.h int main() { int score; printf(请输入成绩); scanf(%d, score); if (score 60) { printf(及格\n); } return 0; }if (条件)后面括号里的东西在C语言里叫条件表达式。它不一定非得是score 60这种比较运算任何能得出真或假的表达式都可以。C语言里没有专门的布尔类型直到C99才有了_Bool规则是表达式结果不为0就是真为0就是假。哪怕你写if (3)也是真因为3不等于0。这个细节在以后读别人代码时会非常有用。大括号不只是把代码圈起来的容器它代表的是一个整体。如果条件成立执行整个大括号里的所有语句如果只有一条语句大括号可以省略if (score 60) printf(及格\n);注意缩进只是给人看的编译器不关心。但如果你写成if (score 60) printf(及格\n); printf(你有资格参加补考\n);第二行printf根本不归if管它无论什么情况都会执行。这就是初学者最常犯的悬挂if问题——缩进会骗人大括号才是亲爹。我的建议是不管几条语句一律加大括号。多打两个字符换来的是代码清晰和省去无数脑细胞。2.2 if-else两条路必须二选一升级成两种输出if (score 60) { printf(及格\n); } else { printf(不及格\n); }if-else保证了两个分支必走一个不可能两个都走也不可能两个都不走。这和连续几个独立的if是有本质区别的// 错误示范效率低而且逻辑不互斥 if (score 60) { printf(及格\n); } if (score 60) { printf(不及格\n); }这段代码虽然输出结果在某些情况下和if-else一样但两个条件都判断了。如果条件复杂、判断成本高这就是浪费如果条件互斥关系没把握好还可能两个都执行。所以只要语义是二选一就用if-else不要写两个独立的if。2.3 else if多分支阶梯判断顺序很重要成绩要分优秀、良好、及格、不及格四档就用到else ifif (score 90) { printf(优秀\n); } else if (score 80) { printf(良好\n); } else if (score 60) { printf(及格\n); } else { printf(不及格\n); }这个结构的精髓在于一旦某个条件成立后面所有else if和else都不会再执行。上面的写法如果score是95第一个条件成立输出优秀后面的良好、及格、不及格全被跳过。我见过很多零基础的同学栽在顺序上比如写成if (score 60) { printf(及格\n); } else if (score 80) { printf(良好\n); }这就出大问题了——一个95分的人会先被score 60拦住输出及格根本轮不到良好。优秀和良好必须放在及格前面因为 60这个条件范围太宽把高分都兜走了。判断条件要从范围窄的、苛刻的到范围宽的、宽松的严格者优先。如果实在拿不准该谁先谁后画一条数轴把各个分段往上标一眼就看明白了。2.4 switch多路等值判断的另一种写法如果判断的不是区间而是一个变量的具体取值比如菜单选择、数字转星期switch往往比一长串if-else更清爽#include stdio.h int main() { int choice; printf(请选择1-3); scanf(%d, choice); switch (choice) { case 1: printf(您选择了开始游戏\n); break; case 2: printf(您选择了读取存档\n); break; case 3: printf(您选择了退出\n); break; default: printf(无效选项\n); break; } return 0; }switch后面的表达式会计算出整数值字符其实也算整数所以case A这种写法也常见然后从上到下逐个匹配case标签后面的常量值。匹配成功就从这个case开始往下执行直到遇到break跳出整个switch。这里有个非常经典的坑break忘了写。比如把第一个case 1下面的break删掉你输入1程序会先输出您选择了开始游戏紧接着把您选择了读取存档也输出了。这叫case穿透fall-through是C语言的设计不是bug。它其实是有意为之的——有些场景你需要穿透。比如代码想统一处理几个取值switch (grade) { case A: case B: printf(成绩良好\n); break; case C: printf(成绩及格\n); break; default: printf(成绩不及格\n); }这里case A和case B都执行同一句输出。但在入门阶段我强烈建议你每个case都写break除非你明确知道自己要穿透。这就像开车时系安全带虽然也有不系没事的时候但代价太高了。switch和if-else怎么选我的经验是如果是区间范围的判断用if-else如果是几个具体值的对等判断用switch。表格里面我列一下对比你以后写代码时可以直接翻出来参考场景推荐写法原因判断成绩区间、年龄范围等if-else条件不是等于几个具体值而是连续区间switch表达不了菜单选项、状态码、枚举值switch结构清晰一眼看到所有可取值执行效率也略高分支超过4~5个且都是等值switch一长串else if可读性太差分支个数少、逻辑简单if-else不需要为此引入switch2.5 嵌套分支不要超过三层if里面还可以套if这叫嵌套。比如既要判断成绩是否及格又要判断及格的人里面是不是优秀if (score 60) { if (score 90) { printf(优秀\n); } else { printf(及格\n); } } else { printf(不及格\n); }逻辑上完全没问题但我要提醒你嵌套层数一多代码就变得很难读而且极易出错。每层嵌套都要在大脑里维护一个当前条件成立吗的栈三层以上基本就要崩溃了。我见过一些学长写的五层嵌套调试时恨不得烫手。对于能避免的嵌套尽量用、||把条件合并。比如上面那两层的写法用if (score 60 score 90)其实就退化成了score 90那还不如直接拆成并列条件先判断优秀。真正的实战技巧是先处理异常和特殊情况早退出少嵌套。这个思路等你写到复杂业务代码时体会会更深。3. 三种循环的底层差异与选型逻辑如果说分支是岔路口那循环就是不断地绕圈跑。C语言提供了三个跑步姿势while、do-while、for。三者在底层性能上没有本质差别差别在什么时候判断条件什么时候初始化循环变量的操作放在哪。3.1 while先判断再执行int i 1; while (i 100) { printf(%d , i); i; }执行流程是先看i 100成不成立成立就进循环体执行完出来之后再回头判断一旦条件不成立循环体一次都不进去。所以while循环有可能一次都不执行的特点。这里有个很多人第一次被绕晕的地方循环体里到底要不要写i如果忘了写i永远是1条件永远成立程序就陷入了死循环控制台不停滚动。你要是遇到了别慌在绝大多数环境里按CtrlC就能强制终止程序。死循环并不可怕有时候还是有意为之的——比如你后面学服务器编程while (1)就是最常见的写法靠break来退出。3.2 do-while先执行再判断int i 1; do { printf(%d , i); i; } while (i 100);唯一的语法差别是do块里先执行一次循环体然后才判断while后面的条件。也就是说无论条件成立与否循环体至少执行一次。这个特性在什么时候用得上典型场景是先做一次再问要不要继续。比如菜单程序先显示一遍菜单让用户选选完问一句还要继续吗答是就再来一遍。还有输入校验先让用户输入一个数如果不在合法范围内就循环提示重新输入。这种情况下do-while是天然的选择。初学者最容易在do-while上犯的错是忘了末尾的分号。while (...);这个分号是必须的少了它编译就报错。我当年自己就漏过编译器报错报得还很隐晦找了半天才发现在这里。3.3 for循环把初始化、条件、增量写在最顶上for (int i 1; i 100; i) { printf(%d , i); }for括号里用分号隔开三个部分第一部分是初始化只执行一次第二部分是循环条件每次进入循环体前判断第三部分是每轮循环体结束后执行的更新操作。它的等价写法是int i 1; while (i 100) { printf(%d , i); i; }那为什么还要用for因为它把循环相关的三个关键信息集中到一行不会散落在代码各处。两个人合作写代码看for (int i 0; i n; i)就知道这是个从头遍历n次的循环看while就得往函数体里找i是在哪初始化的、在哪更新的费劲。所以在循环次数明确、有固定步长的场景下for是绝对首选。还有个热搜词叫c语言 for循环顺序 1243说的是for三个部分和循环体的执行顺序。这里我明确讲一下假设循环体编号为1、初始化编号为2、条件判断编号为3、增量编号为4那一轮完整的流程是2初始化→ 3条件成立→ 1循环体→ 4增量→ 3条件成立→ 1 → 4 → 3……直到条件不成立结束。所以最多只执行一次初始化条件和循环体的执行次数是一样的增量比循环体少执行一次。这点搞清楚特别重要尤其当你日后在第三部分写复杂表达式时。3.4 三种循环怎么选一张表搞定场景推荐原因知道要循环多少次比如遍历数组for初始化、条件、更新都在一行最清晰不知道多少次只知道什么时候结束while比如读文件直到末尾条件判断在入口处至少得先执行一次do-while比如菜单、输入校验天然保证最少跑一次记住一个规律能用for就用for必须用while才用whiledo-while只在至少执行一次这种特定场景用。这不是绝对的但作为入门标准够用了。3.5 循环变量的坑边界差一、类型溢出、改变循环变量写循环最常见的错误是差一off-by-one。比如想打印1到100写成for (i 1; i 100; i)就只打了99个数。这个错误不容易看出来尤其在数字偏大时更是防不胜防。我的纠错手段是用极端值手推一遍。把i设为1、2、99、100分别代进去看条件成不成立是不是你期望的。养成这个习惯后差一出现的概率会大幅下降。另一个坑是循环体内意外改变循环变量的值。比如你在循环体里给i重新赋值或者让i参与了运算后忘了还原。不是说不能改而是改了之后循环次数就变得不可预测代码极难排查。如果你确实需要在循环里做逻辑不要轻易动循环变量本身另设一个临时变量。还有一个在数字很大的情况下会遇到的问题整数溢出。int类型最大到21亿多如果你循环累积一个变量加到了20亿又继续加它会回绕变成负数。这在入门阶段不常见但做累加、阶乘、斐波那契数列这类练习时会有同学算到一半发现数字变负了这就是溢出。标准做法是把累加变量换成long甚至long long。4. 分支循环里的隐藏规则关系表达式、逻辑短路与嵌套循环4.1 赋值号和相等比较一个字母级别却致命的错误这个坑我可以说是C语言入门第一大坑。在if的条件判断里很多人会把判断相等写成赋值if (x 5) { // 本意是 x 5 printf(x是5\n); }x 5是赋值表达式它的值是5。5不等于0所以这个条件永远为真printf每次都会执行。更可怕的是x还被悄悄改成了5程序原有的值被覆盖了。这个错误编译器一般不会报错因为它符合语法。新手排查的时候特别痛苦因为逻辑看着都对。怎么防两个实用技巧。第一把常量写在左侧写成if (5 x)。这样如果你不小心漏了一个等号变成if (5 x)编译器立刻报错——你不能给常量赋值。这个方法很多老手也一直在用。第二开启编译器警告。在GCC里加-Wall参数很多编译器对这类嫌疑会给出warning虽然程序能跑但提示你这里有风险。在VS Code的tasks.json里配置编译参数时可以顺手加上-Wall -Wextra让编译器替你盯梢。4.2 逻辑运算符的短路机制多分支条件往往涉及逻辑与和||逻辑或。比如if (score 60 score 90) { printf(良好\n); }这里表示并且两个条件都成立才为真。C语言的有个关键特性叫短路求值如果左边条件已经是假了右边干脆不会被执行。同理||是如果左边已经为真右边也不执行。这个特性在某些场景下非常好用——比如你要判断指针不为空且指针指向的值大于0写成if (p ! NULL p-value 0)如果p是空指针右边根本不会执行你就不会触碰到空指针导致程序崩溃。反过来||的短路特性也意味着如果你在右边写了有副作用的代码比如函数调用、变量自增而这个副作用只在左边条件不满足时才发生你的程序行为会变得难以预料。所以不要在逻辑表达式的右侧写影响程序状态的语句这是个好习惯。4.3 嵌套循环循环里再套循环入门阶段分支和循环讲完紧接着就是循环套循环比如打印九九乘法表、输出一个星号三角形。这属于本讲的进阶部分我先给个概念for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%2d , i, j, i * j); } printf(\n); }外层循环负责行内层循环负责列。执行流程是外层i1时内层j从1到1执行一次换行外层i2时内层j从1到2执行两次换行……每行内容取决于外层当前值。理解嵌套循环的关键是内层循环要完整地跑完所有次数外层才走下一步。如果这里有点转不过弯可以自己手动模拟几行多跑几次就能领会。这一讲关于嵌套循环先点到为止下一讲循环下我会专门带着大家做出几个典型的图案输出题还会讲到break和continue在嵌套循环中到底控制哪一层。5. 上机实战三个必须亲手敲到能背出来的入门程序光看语法像在岸上看游泳教学视频下了水才见真章。下面这三个程序是我认为覆盖本讲知识点的最小有效训练集每个都有明确的目的代码不长但值得你反复敲、反复改。5.1 猜数字游戏if-else、while、随机数的第一次组合#include stdio.h #include stdlib.h #include time.h int main() { int target, guess, attempts 0; srand(time(NULL)); target rand() % 100 1; // 生成1~100之间的随机数 printf(我心里想了一个1~100的数你猜猜看\n); do { printf(输入你的猜测); scanf(%d, guess); attempts; if (guess target) { printf(大了再试一次\n); } else if (guess target) { printf(小了再试一次\n); } else { printf(恭喜你猜了%d次就猜中了。\n, attempts); } } while (guess ! target); return 0; }重点解释几个细节srand(time(NULL))是设置随机数种子。如果不写这一行每次运行程序生成的随机数序列是一样的就不好玩了。time(NULL)返回当前时间作为种子让每次运行都不一样。rand() % 100 1表示产生1到100之间的数。%取余的结果是0到99再加1就是1到100。用do-while的典型理由玩家至少要猜一次最多不限次数。如果你用while写还得先把guess初始化为一个怪值很别扭。自己改动的方向把随机数范围改成1到1000限定最多猜10次超过就结束并公布答案增加一个如果输入非法字符就重来的处理。最后这个升级对初学者有点难但值得挑战。5.2 计算1到n的和与乘积for循环的入门标准题#include stdio.h int main() { int n; long sum 0; long product 1; printf(请输入n值); scanf(%d, n); for (int i 1; i n; i) { sum i; product * i; } printf(1到%d的和是%ld\n, n, sum); printf(1到%d的乘积是%ld\n, n, product); return 0; }这里我特意把sum和product声明成long原因就是上一节提到的溢出问题。你试着把n输入20乘积已经是21亿多了超过int范围用int显示出来会是负数。long在多数平台上能到92亿亿覆盖面大得多。在这道题上你能真实体会到类型选错带来的诡异后果。一个值得思考的细节为什么product要初始化为1而不能初始化为0因为任何数乘以0都得0。同理求和的变量初始化为0是合理的求乘积的变量初始化为1才是正确的起点。这种初始值选错导致整个结果错的问题靠编译器和调试都很难查出来只能靠理解业务逻辑来避免。5.3 判断素数分支、循环、break的综合应用预告#include stdio.h int main() { int n, isPrime 1; printf(请输入一个正整数); scanf(%d, n); if (n 2) { isPrime 0; } else { for (int i 2; i n; i) { if (n % i 0) { isPrime 0; break; } } } if (isPrime) { printf(%d是素数\n, n); } else { printf(%d不是素数\n, n); } return 0; }素数是大于1的自然数中除了1和它本身以外不再有其他因数的数。判断思路从2开始一直试到n-1只要有一个数能整除nn就不是素数。这里用到了break——一旦确认不是素数就不需要继续循环了立刻跳出这种提前终止能省下大量计算。你能看出来这个程序的效率不高。比如判断101试到10就可以停了因为如果n有因数那么必然有一个因数不大于sqrt(n)即n的平方根。但入门阶段先用最直观的写法跑通逻辑下一讲再继续优化到i * i n的形式。一步一步来别一口吃成胖子。建议你改改代码感受一下删掉break看输出有没有变化把i n改成i n看看结果会怎样。6. 调试入门程序不听话时怎么问它零基础学到这里你们遇到的报错基本上分成两类编译错误编译器直接告诉你哪一行、什么错和运行结果不符合预期程序能跑但结果不对。后者比前者麻烦得多因为编译器帮不了你只能自己找。我的调试三板斧如下。6.1 printf大法在关键位置打印中间变量这是最朴素也最有效的调试方式。比如你怀疑for循环的边界有问题就在循环体开头打印ifor (int i 1; i 100; i) { printf(当前i%d\n, i); // 临时调试语句 // 其他逻辑 }跑一遍看i是从几到几、有没有跳变。定位到问题后再把调试语句删掉。不要觉得low这个土办法在专业开发中一样常用只是很多人会用日志框架替代printf但原理完全相同。6.2 分段注释法确认负责的程序段如果一段程序输出完全不对你想知道是哪一段把问题搞出来了就把代码一段一段注释掉看哪一段注释掉之后现象变了那问题大概率就在那一段。这个过程叫二分排查效率很高。比如猜数字程序一直提示大了你就先确认target生成的值是多少——在scanf之前打印一下target。很多同学会惊讶地发现原来每次生成的随机数根本不在1~100之间或者一直是同一个数。6.3 在VS Code里设置断点如果你用的是VS Code配置C语言环境它其实带完整的调试功能。在代码行号左侧点击一下打上红点断点然后按F5启动调试程序会停在断点那行你可以在左侧变量窗口看到所有变量的当前值然后按F10一步步往下走观察每步之后变量怎么变化。这比你printf要精细得多。配置方法其实不复杂需要安装C/C扩展然后创建一个.vscode/launch.json文件指向gcc编译出来的可执行文件。等你配置好了看到了单步调试的魔力你会刷新对排错的认知。7. 本讲的练习清单与自测标准学完一个知识点不能只看我感觉懂了。下面这份练习清单是按从易到难排的每一题都尽量覆盖一个具体的语法点。建议你别急着看答案先自己上机试卡住20分钟以上再求助。初级组输入一个年份判断它是否为闰年。闰年规则能被4整除但不能被100整除或者能被400整除。这题综合了条件运算和逻辑运算是分支结构的必练题。输出从1到100之间所有能被3整除的数。用for循环加if判断很简单但能帮你巩固循环加分支的组合写法。输入一个正整数倒序输出它。比如输入12345输出54321。这题涉及取模%和整除/的配合对理解数字拆分非常有帮助。热搜词里有字符串逆序c语言pta那是用字符串的方法你先把数字方法练熟再看字符串的。中级组打印九九乘法表参考上面给的嵌套循环代码。用循环输出斐波那契数列前20项。每一项是前两项之和1、1、2、3、5、8……这题练的是用临时变量交换数据的能力也是很多面试笔试的常客。输入一个数n计算n的阶乘并输出。注意溢出问题记得用long。自测标准是什么呢我给自己定的规矩是不看笔记能独立写出代码并运行通过才算真正掌握。如果你需要翻着笔记才能写出来说明还没内化需要多写几遍。写代码有点像学骑自行车看别人骑一百遍不如自己摔两跤。我身边很多学C语言的同学都是在这个阶段开始分化的。有些人卡在感觉自己懂了一写就报错然后放弃了有些人咬牙把每道题都练完突然就顿悟了——原来代码就是把脑子里想的逻辑用编译器听得懂的方式说清楚。后者往往在后面的指针、结构体、链表这些更难的内容上反而学得轻松因为他们已经在入门阶段练出了把逻辑拆解成步骤的肌肉记忆。练完这些就可以进入下一讲嵌套循环的经典图形题、break和continue更深层的用法、以及用循环解决更多实际问题的综合案例。你会在那讲发现其实更多的不是新语法而是你已经掌握的这些东西的组合。