我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

C语言条件判断与循环详解:从if/else到嵌套实战

C语言条件判断与循环详解:从if/else到嵌套实战 C语言学了一段时间之后很多人都会有一个共同的感受语法规则背得滚瓜烂熟但一写代码就卡壳尤其是遇到“先判断再做事”“反复做同一件事”这种场景脑子里明明有思路落到代码上却总觉得别扭。其实原因很简单——你还没有真正把C语言里的条件判断和循环当成自己的思维工具。C语言的逻辑骨架就两条条件判断决定程序“走哪条路”循环决定程序“重复做多少遍”。这篇文章我会从条件语句的本质讲起一直拆到循环嵌套和实战中容易踩的坑内容覆盖 if / else、switch、while、do-while、for、break、continue以及经典的九九乘法表、冒泡排序、字符串逆序这些练习题思路。不管你是刚入门的新手还是学了一阵但代码写不顺手的自学者这篇文章都值得从头到尾看一遍。1. 条件判断的本质C语言如何处理“真假”说到条件判断很多人第一反应就是 if 和 else觉得这东西太简单了。但真正写代码的时候你会发现“条件”本身才是最容易出错的地方。C语言的条件判断里藏着一个关键观念任何表达式的值都是“真”或“假”而在C语言中“真”就是用非0表示“假”就是用0表示。这个“非0即真”的原则渗透在所有判断语句里。1.1 “非0即真”原则所有表达式都可以当条件用比如你写if (a) { printf(a不为0); }这里 a 可以是整数、浮点数、甚至是指针。只要 a 的值不是0就进入 if 分支。这一点和 Python、Java 这些语言有明显差异——C语言里没有专门的布尔类型判断结果本质就是整数。这个特性最典型的应用就是判断指针是否为空int *p malloc(100); if (!p) { printf(内存分配失败); return -1; }这里的 !p 等价于 p NULL因为 NULL 在C语言里就是 0。很多老手写代码喜欢用这种简洁写法但新手往往看着懵。理解了“非0即真”这种代码就能一眼看懂。1.2 关系运算符与逻辑运算符的细节条件判断必然涉及比较。C语言的关系运算符有大于()、小于()、大于等于()、小于等于()、等于()、不等于(!)。这些运算符产生的结果是 int 类型的 1 或 0。逻辑运算符有三个与、||或、!非。这里面有一个特别重要的行为——短路求值。if (a ! 0 b / a 2) { // ... }当 a 0 时第一个条件为假整个 表达式的结果已经确定是假了C语言不会再执行 b / a 这一句从而避免了除零错误。同理|| 如果左边为真右边就直接跳过。这个特性在实战中非常实用可以用来写“先判断再操作”的安全代码。1.3 悬空 else 问题if 的配对规则if 和 else 的配对有一个经典坑叫做“悬空 else”dangling else。看这段代码if (a 0) if (b 0) printf(a和b都大于0); else printf(a不大于0);这段代码的本意是第一个 if 对应 else但C语言的规则是else 总是和离它最近的尚未匹配的 if 配对。所以上面的 else 实际上匹配的是内层的 if (b 0)而不是外层。结果是当 a 0 且 b 0 时会输出“a不大于0”完全反了。解决办法很简单任何时候都加大括号。if (a 0) { if (b 0) { printf(a和b都大于0); } } else { printf(a不大于0); }这个原则在C语言开发里怎么强调都不过分。1.4 switch-case 的穿透行为与使用场景当判断的目标是一个整数变量的多个离散取值时switch 往往比一串 if-else 更清晰。switch 的基本结构是switch (grade) { case A: printf(优秀); break; case B: printf(良好); break; default: printf(未知等级); break; }新手最容易犯的错误是漏写 break导致“穿透”执行完 case A 的语句后会继续执行 case B 的语句一直往下走。这在某些场景下反而是有意为之的比如多个 case 要处理相同逻辑时可以合并switch (month) { case 4: case 6: case 9: case 11: printf(30天); break; default: printf(其他月份); break; }另外要注意switch 里的条件只能是整数类型char 也算不能用来判断浮点数区间也不能判断字符串。这些场景还是老老实实用 if-else。2. 三种循环语句的选型逻辑while、do-while 与 for循环是C语言里最体现“机械重复”魅力的部分。循环的本质是先判断一个条件条件为真就执行一段代码执行完再回到判断点直到条件为假才退出。这个“判断-执行-再判断”的过程很多人第一次学的时候觉得绕其实用一个生活类比就懂了你烧开水水没开就一直等水开了就关火——这就是一个循环。C语言提供三种循环语句while、do-while、for。三者的核心区别就两个维度先判断还是先执行以及循环变量在哪个位置更新。2.1 while 循环先判断后执行while 的格式是while (表达式) { 循环体; }执行流程先判断表达式为真就执行循环体执行完再次判断表达式为假就跳出循环。这意味着如果表达式一开始就是假循环体一次都不会执行。int i 1; int sum 0; while (i 100) { sum i; i; } printf(1加到100的结果是%d, sum);这里 i 的作用极其关键。如果忘了写 ii 永远等于1循环条件永远为真程序就陷入死循环CPU占用飙升程序卡死。这是新手最常犯的错误。2.2 do-while 循环先执行后判断do-while 的格式是do { 循环体; } while (表达式);注意do-while 的正确使用场景是循环体至少需要执行一次。比如你要从用户那里读取输入直到读到合法值为止int num; do { printf(请输入一个正数); scanf(%d, num); } while (num 0);这里如果用 while就要求 num 在进入循环前必须初始化逻辑上会更啰嗦。do-while 直接保证先询问一次再根据输入判断是否继续天然契合“至少要执行一次”的场景。2.3 for 循环三个表达式各司其职for 的格式是for (表达式1; 表达式2; 表达式3) { 循环体; }三个表达式的分工是表达式1初始化表达式进入循环前执行一次表达式2循环条件每次进入循环体前进行判断表达式3更新表达式每次循环体执行完之后执行经典写法for (int i 1; i 100; i) { sum i; }你会发现for 把循环变量的“初始化”“条件判断”“变量更新”集中到了第一行阅读起来一目了然。所以当循环次数事先可以确定时for 是最优选择当循环次数依赖运行时的某个状态时用 while 更自然。一个常见的误区是三个表达式都可以省略但分号不能省。如果省略表达式2就相当于条件永远为真构成死循环。有些人故意写成 for(;;)这和 while(1) 一样都是死循环。2.4 三种循环的对比表格特性whiledo-whilefor判断时机先判断后执行先执行后判断先判断后执行最少执行次数0次1次0次适用场景条件不确定循环次数未知至少执行一次循环次数可确定循环变量更新位置循环体内循环体内表达式3典型出错点忘记更新循环变量导致死循环忘记末尾分号分号写成逗号选用哪种循环核心就看一件事循环的次数在进入循环前是否能够确定。能确定用 for不能确定但要先执行一次再判断用 do-while不能确定且可能一次都不执行用 while。3. 循环控制语句与常见误区break、continue、goto 的使用边界循环写好了但有时候需要在中途“提前退出”或者“跳过某一次”。C语言为此提供了 break 和 continue 两个控制语句还有一个争议很大的 goto。很多新手搞不清 break 和 continue 的区别代码写出来逻辑一塌糊涂。这一节把这些问题彻底讲透。3.1 break结束整个循环break 的作用是立即跳出当前所在的整个循环层不再执行后续的循环体也不再进行条件判断直接到循环体外的下一条语句。经典场景是在数组中查找某个元素找到了就不需要继续遍历了。int arr[5] {3, 7, 1, 9, 4}; int target 9; int i; for (i 0; i 5; i) { if (arr[i] target) { printf(找到了下标是%d, i); break; } }3.2 continue跳过本次循环的剩余部分continue 的作用是跳过本次循环体剩余的语句直接进入下一次循环。条件判断会重新执行循环变量照常更新。经典场景是计算1到100之间所有偶数的和。int sum 0; for (int i 1; i 100; i) { if (i % 2 ! 0) { continue; } sum i; }这里奇数的处理直接被跳过只有偶数才累加。注意continue 在 while 里有一个隐蔽的坑int i 1; while (i 10) { if (i 5) { continue; // 糟糕 } printf(%d , i); i; }当 i 等于5时continue 跳到条件判断但 i 被跳过了i 永远是5程序陷入死循环。而在 for 循环中不存在这个问题因为表达式3i是在循环体结束后执行的不受到 continue 影响。这是 while 和 for 在 continue 行为上的一个本质差异务必牢记。3.3 goto能用但尽量别用特殊场景除外goto 可以无条件跳转到函数内部任意带标签的位置。长期以来 goto 被视为代码混乱的根源因为滥用 goto 会让程序的执行流变成一团乱麻调试难度成倍上升。但这不意味着 goto 完全不能用。某些场景下 goto 反而是最优解典型就是多层循环的内层错误处理for (int i 0; i 10; i) { for (int j 0; j 10; j) { if (某个严重错误) { goto error_exit; } } } error_exit: printf(发生严重错误退出); return -1;多层循环中break 只能退出当前层要想一口气跳出所有层要么用层层标志变量要么用 goto。用标志变量的写法往往更啰嗦goto 反而干净利落。我的建议是在单层以内能用 break/continue 解决的问题就不要碰 goto。只在多层嵌套的错误处理场景中使用。3.4 浮点数比较的循环陷阱循环条件中比较浮点数是一个非常隐蔽的坑。浮点数在计算机中不是精确存储的0.1 实际上是一个近似值。下面的循环可能比你预期的多执行或少执行一次for (float x 0.0f; x ! 1.0f; x 0.1f) { // 危险循环次数不确定 }解决方案有两个一是改成区间判断二是使用整数循环加上换算。for (int i 0; i 10; i) { float x i / 10.0f; // 处理x }这个思路在数值计算中非常实用循环变量尽量用整数需要浮点数时再换算精确且完全没有性能损失。4. 条件判断与循环的混合嵌套实战从九九乘法表到冒泡排序条件判断和循环单独用都很简单真正的挑战在于嵌套。程序的世界里几乎不可能只有单一的条件或单一的循环实际代码中永远是你中有我、我中有你。这一节我用几个经典题目来拆解嵌套逻辑的分析方法每个题目都附上完整思路而不仅仅是扔出一段代码。4.1 九九乘法表嵌套循环的入门训练九九乘法表的核心逻辑可以拆成两句话外层循环控制行数从1到9内层循环控制列数第 i 行有 i 列for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-4d, j, i, i * j); } printf(\n); }很多新手想不通为什么内层条件是 j i而不是 j 9。因为乘法表是三角形的第1行只有1列第2行有2列第 i 行有 i 列。内层循环的边界依赖外层循环的变量这就是“嵌套”的精髓。%-4d 的作用是左对齐并占4个字符宽度保证输出对齐好看。4.2 冒泡排序双重循环加条件交换的经典组合冒泡排序的逻辑是从左到右两两比较相邻元素如果前一个比后一个大就交换位置。每一轮比较后最大的元素会像气泡一样“浮”到最右端。外层循环共进行 n-1 轮每轮把当前未排序部分的最大值移到末尾内层循环比较相邻元素并交换。void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }这里有两个关键点内层 j n - 1 - i 是因为每完成一轮末尾已经有一个元素归位无需再比较交换数组元素必须借助中间变量 temp否则会丢失原值初学者可以把整个排序过程拆成“比较-交换”两个动作来理解。建议用纸笔画一遍数组元素的移动过程画完就彻底懂了。冒泡排序还可以加一个优化如果某一轮没有任何交换说明数组已经有序直接提前退出。for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) break; }4.3 字符串逆序循环边界条件的分析与验证字符串逆序的需求很常见将 “hello” 变成 “olleh”。经典做法是双指针一头一尾交换字符直到两个指针相遇。void reverse_string(char str[]) { int len 0; while (str[len] ! \0) { len; } for (int i 0, j len - 1; i j; i, j--) { char temp str[i]; str[i] str[j]; str[j] temp; } }这个代码的循环条件是 i j为什么不是 i ! j因为在字符串长度为偶数时两个指针刚好“擦肩而过”i 会越过 ji ! j 的判断在某一轮之后可能永远不会成立导致越界访问。而 i j 保证了当两个指针相遇或交错时就停下逻辑安全。记住一个原则双指针移动的循环边界条件用比用!安全得多。4.4 买房子问题与字符串分割循环累加与条件控制的综合应用经典题目“买房子”是循环和条件结合的典型某人年薪固定为20万每年存下全部年薪房价第一年是200万之后每年上涨10%。问多少年能买得起房。这道题的关键在于用循环模拟每年的收入积累和房价上涨在循环内同时判断两个量的大小关系double salary 20.0; double price 200.0; double saving 0.0; int year 0; while (saving price) { saving salary; price * 1.1; year; if (year 20) { printf(买不起\n); return; } } printf(%d年后买得起\n, year);每年固定流程攒钱、房价上涨、判断是否买得起。这个“按年份依次模拟”的思路是很多编程练习题的核心解法。类似地“将一个字符串按空格分割”也属于典型的循环条件嵌套问题。核心思路是用一个循环遍历字符串的每个字符遇到空格就完成一个单词的处理其他字符则拼接到当前单词中。这种“状态机”式的循环思维是后续字符串处理的基础。5. 条件与循环的工程化思考从写对到写好语法学会了题目也刷了接下来要面对的是真实的工程代码。真实代码里的条件判断和循环往往比练习题复杂得多。这一节我讲讲从“能跑”到“好维护”之间关于条件与循环的一些工程化建议。5.1 循环边界条件用“”还是“”很多新手在写循环时纠结于边界条件的写法。比如遍历输出数组元素for (int i 0; i n; i) { // 正确标准写法 } for (int i 0; i n - 1; i) { // 也能运行但可读性差 }行业惯例是使用半开区间 [0, n)即i 0; i n; i。这样循环次数一目了然和数组下标从0开始的规律一致还能避免 n - 1 的额外计算。凡是遍历数组、遍历字符串用不要用。5.2 循环嵌套的性能意识两层甚至三层循环在数据量大的时候性能差异会非常明显。以时间复杂度 O(n^2) 的双重循环为例n 从 1000 涨到 10000运算量从100万涨到1个亿差了100倍。实战中要注意把不依赖内层循环的计算提到外层尽量减少内层循环中的函数调用能用单层循环解决的问题不要用双重循环比如计算每个学生的各科平均分时如果先内层循环累加每科分数再外层循环遍历学生就比反过来快得多。这个优化习惯在初学时就要养成。5.3 统一代码风格大括号不仅仅是美观关于大括号的争论在C语言社区一直存在——KR风格左大括号跟在控制语句后面和Allman风格左大括号另起一行。我不打算在这个问题上站队但有一条铁律无论哪种风格都必须统一。大括号的真正价值在于消除悬空 else 的歧义避免缩进误导。看这段代码if (a 0) printf(正数); if (b 0) printf(b也是正数);如果没有大括号第二个 if 完全不受第一个 if 的控制。而缩进相同眼睛看着就像同一个条件块的结果很容易误读。加了大括号之后逻辑归属一目了然。5.4 先用注释把逻辑写清楚再写代码这一条是我自己写代码多年养成的习惯特别建议新手试试写循环之前先用中文注释把“什么时候开始、什么时候继续、什么时候结束”三件事描述清楚。// 从数组第一个元素开始 // 遇到负数就跳过 // 直到遍历完所有元素 for (int i 0; i n; i) { if (arr[i] 0) continue; // 处理正数 }先想清楚再写代码可以避免大量写一半发现逻辑不对再推翻重来的情况。这个习惯在编写复杂循环嵌套时尤其管用因为脑子里的逻辑一旦混乱代码就会越改越糟。5.5 从练习题思维过渡到工程思维练习题里循环和条件判断往往是为了某个明确的结果而存在比如求和、排序、查找。但真实项目中循环和条件判断更多是在处理边界情况和异常路径。举个简单的例子读取用户输入的数字直到输入合法为止这个需求如果用练习题思维可能就是简单的一个 while(1) 加 break。但在工程中你还要考虑输入超长、输入类型错误、EOF 结束等多种情况。处理这些情况靠的还是条件判断和循环的组合。从这个角度看扎实掌握条件判断与循环其实是在为后续的编程生涯打地基。我个人在实际教学和写代码时最深的体会是当你把“先判断再做事没做完继续做做完了停下来”这个模型内化成自己的思维习惯之后C语言就算真正入门了。条件判断和循环就像是编程世界里的两个基础动作——走路和呼吸任何复杂的程序都离不开这两个动作的编排组合。把基础打牢后面学到指针、结构体、链表、文件操作时你会发现核心逻辑依然离不开这两个老朋友。希望这篇文章能帮你把 C语言 的循环与条件判断这块内容彻底吃透。
返回列表