资讯动态

C语言关系运算与逻辑运算全解析:从短路求值到优先级避坑

发布时间:2026/10/9 18:55:50 来源:尧图企业网站定制
上一篇我们讲完了算术运算符和赋值运算符评论区就有读者在催什么时候讲判断我想写if可光会算加减乘除不顶用。这一篇正好补上通往分支和循环的最后一公里——关系运算与逻辑运算。别看这两个词听起来平淡它们几乎构成了C语言里所有决策逻辑的骨架。我见过不少初学者在指针上没翻车反而栽在 a b c 这种看似无害的比较上。这篇我会把六种关系运算符、三种逻辑运算符、短路求值机制和优先级顺序一次讲透顺便带三个能直接抄进作业和项目里的实战例子。1. 关系运算六个比较符号最终只回答是或否1.1 六种关系运算符一次看清C语言提供了六种关系运算符本质上就是你在数学课上学过的六种比较。先看这张表我建议直接存下来运算符含义示例结果为真的条件大于a ba 严格大于 b小于a ba 严格小于 b大于等于a ba 大于或等于 b小于等于a ba 小于或等于 b等于a ba 与 b 相等!不等于a ! ba 与 b 不相等这些都是双目运算符也就是说它们需要左右各有一个操作数比如x 0里x和0就是左右两个操作数。这里有个特别容易引起初学者困惑的点和的区别。前者是是否相等的比较后者是赋值我在第6部分会专门展开讲但你现在就要在脑子里划清界线——C语言里比较是否相等必须用两个等号。另外注意关系运算符没有类似约等于这种模糊比较和已经是唯一的带等于形式。想判断两个浮点数是否近似相等你得自己处理精度问题这一点在第 1.3 节详述。1.2 关系表达式的真值是整数0 和 1这是全篇第一个关键认知在C语言中关系表达式的结果不是true或false而是一个 int 类型的整数。条件成立得到 1不成立得到 0。你可以直接把它打印出来验证#include stdio.h int main(void) { printf(%d\n, 5 3); // 输出 1 printf(%d\n, 3 5); // 输出 0 printf(%d\n, 7 7); // 输出 1 printf(%d\n, 5 5); // 输出 1 return 0; }这里其实引出了C语言的一整套真假观0 表示假任何非 0 值都表示真。所以很多人写if (x)而不写if (x ! 0)两者效果完全一样写if (!ptr)和if (ptr NULL)也等价。理解了这一点再看各种判断条件就不会觉得别扭了。更有意思的是既然真值是整数那它就能参与算术运算。下面这种代码在语法上是合法的int result (5 3) (2 1); // result 的值是 2能看懂这种代码是一回事但我强烈不建议你在实际项目里这么写。可读性太差代码评审的时候多半会被同事喷。不过你要知道C语言的灵活性就体现在这些地方它不会像某些高级语言那样把类型给你焊死。1.3 浮点数比较0.1 0.2 到底等不等于 0.3直接说结论在C语言里0.1 0.2不等于0.3。不信你跑一下#include stdio.h int main(void) { double a 0.1 0.2; double b 0.3; printf(%.20f\n, a); // 输出 0.30000000000000004441 if (a b) { printf(相等\n); } else { printf(不相等\n); } return 0; }原因是浮点数在计算机内部采用 IEEE 754 标准用二进制无法精确表示 0.1 和 0.2 这样的十进制小数误差就这样累积出来了。这不是C语言的 bug所有主流编程语言都这样只是 C 不会帮你掩盖而已。正确的比较方式是判断两个数的差的绝对值是否小于某个足够小的精度值#include math.h #include stdio.h int main(void) { double a 0.1 0.2; double b 0.3; double epsilon 1e-6; // 精度阈值按业务需要调整 if (fabs(a - b) epsilon) { printf(近似相等\n); } else { printf(不相等\n); } return 0; }浮点比较这个坑很多工作三五年的老手都会踩。我在项目里见过因为直接比较浮点相等导致地图坐标判断失灵的问题定位半天才发现是精度误差。记住浮点数之间别用用差值范围判断。2. 逻辑运算、||、! 三兄弟2.1 三种逻辑运算符与现实条件组合的对应关系运算符让你能比较一个条件是否成立但真实世界的问题往往需要同时处理多个条件。比如你想出门跑步条件是不下雨并且气温不低于5度食堂打饭刷卡成功与否是卡的余额充足或者处于免费用餐时段。这类并且或者取反的组合在C语言里就是三种逻辑运算符!逻辑非取反。真的变假假的变真。逻辑与两侧都为真时结果才是真。||逻辑或两侧只要有一个为真结果就是真。注意它们的写法和||都是双字符中间不能有空格也别和单字符的、|搞混。后者是按位运算我后面会单独辨析。2.2 真值表零和一之间只有四种可能把真值表完整列出来这是理解逻辑运算最快的方式。假设 a 和 b 的取值只有 0假或 1真那所有可能组合无非四种ab!aa ba || b00100011011000111011记忆口诀就两句话逻辑与一假即假逻辑或一真即真。比||更严格要求全员通过||更宽松有一个人点头就行。!最干脆反过来就完事。真值表在实践中的价值在于当你在一个复合条件里写了三个以上的逻辑运算符时可以一步步用真值表化简排查到底是哪个条件把孩子带偏了。2.3 逻辑运算与位运算的辨析 是 是 这是个高频混淆点我干脆一次性说清楚。、||、!是逻辑运算参与运算的操作数按真假看待结果只能是 0 或 1而、|、~是按位运算参与运算的操作数按二进制每一位看待结果是按位计算出来的整数。举个例子int a 5; // 二进制 0101 int b 3; // 二进制 0011 printf(%d\n, a b); // 5非0为真3非0为真真真 1 printf(%d\n, a b); // 0101 0011 0001 1 printf(%d\n, a || b); // 真||真 1 printf(%d\n, a | b); // 0101 | 0011 0111 7再看!和~的差别printf(%d\n, !1); // 0逻辑非把真变假 printf(%d\n, ~1); // -2按位取反二进制补码的结果~1为什么是 -2因为整数 1 在内存里是 ... 0000 0001按位取反变成 ... 1111 1110在补码表示下这就是 -2。这种细节新手不需要死记但你要明白!和~根本不是一回事。写条件判断时用!写位掩码操作时用~。3. 短路求值C语言里最实用也最容易坑人的特性3.1 短路求值是怎么工作的和||在C语言里有一个非常实用的行为叫做短路求值计算A B时先算 A如果 A 为假整个表达式必定为假B 就不再计算了。计算A || B时先算 A如果 A 为真整个表达式必定为真B 就不再计算了。用人话说就是C语言很懒一旦结果已经确定就绝不多干一步活。这个设计最初是为了效率但后来在工程上成了重要的防御手段。看这个经典例子判断b / a 1是否成立但前提是a不能为 0。如果不做保护直接除零程序就崩了。int a 0; int b 10; if (a ! 0 b / a 1) { printf(成立\n); } else { printf(a为0或者结果不大于1\n); }因为a ! 0为假短路求值让b / a 1这一步压根不会执行程序安全。如果你把换成那两边都会老老实实算完除零错误就直接发生了。这就是逻辑运算和位运算在行为上的又一重大差异。3.2 用短路求值写出防崩溃的代码短路求值的最大工程价值在于先判断是否安全再访问数据。最典型的场景是遍历链表或使用指针时判空while (p ! NULL p-data ! target) { p p-next; }如果写成while (p-data ! target p ! NULL)那你顺序就反了——先访问了p-data当p是 NULL 时程序直接段错误。顺序一换安全系数天壤之别。再比如从标准输入读取数据后判断是否读取成功再继续处理int num; if (scanf(%d, num) 1 num 0) { printf(输入了正数%d\n, num); }这里scanf返回成功读取的变量个数先判断读取是否成功再判断值是否为正。如果读入失败num可能是旧的或未初始化的值根本不该去比较。这就是短路求值帮你挡掉的逻辑错误。3.3 别把自增、赋值表达式塞进短路区短路求值有个副作用右侧表达式可能根本不执行。如果右侧带有自增、自减、赋值这类操作程序行为会变得极其诡异。int x 0; if (x 0 || x) { printf(x %d\n, x); // 猜猜输出什么 }答案是x 0。因为x 0为真||已经能确定整个条件为真所以x根本没机会执行。你要是本想着顺便让 x 加 1那代码结果就和预期差了十万八千里。同理也别在条件里写if (count 0 count 10)这种代码。我见过的一些线上 bug排查到最后就是这种短路区里的副作用导致的。我给自己定了一条铁律复合条件表达式里保持零副作用所有修改变量的操作放到判断之前或之后单独写。你觉得多写一行累debug 的时候才知道那行代码有多值钱。4. 优先级与结合性复杂表达式到底按什么顺序算4.1 从高到低理清这条运算食物链在同一个表达式里出现算术运算、关系运算、逻辑运算、赋值运算混排时C语言有一套严格的优先级规则。跟我到目前为止讲的内容相关的优先级从高到低是优先级运算符类别举例高圆括号( )较高逻辑非!中高算术运算* / % -中等关系运算 中低相等比较 !较低逻辑与更低逻辑或||最低赋值运算 -等记忆方法我给你总结成一句话先算数学再比大小再看相等然后逻辑与、逻辑或最后才赋值。!是另类它优先级很高仅次于括号所以!x y等价于(!x) y千万别理解成!(x y)。4.2 几个实战中常见的优先级决策点优先级规则光背不行得会做题。下面这几个组合我在刷题网站和笔试题目里见过无数次。第一个算术运算高于关系运算。int a 1, b 2; printf(%d\n, a b 2); // 先算 ab3再比较 32结果是 1这里的a b 2天然就是(a b) 2因为加法的优先级高于大于号。多数情况下这符合你的直觉危险的反而是那些你以为优先级不一样、实际上一样的情况。第二个相等比较低于关系比较。int a 1, b 2, c 1; printf(%d\n, a b c);等价写法是(a b) c。先把a b算成 1再拿 1 和 c值也是1比较结果为 1。如果你没意识到关系比较发生在相等比较之前这个式子就会让人一头雾水。第三个逻辑与和逻辑或的经典混排。if (a 0 || b 0 c 0)由于优先级高于||这行代码实际是a 0 || (b 0 c 0)。如果你想表达a大于0或b大于0且c大于0那就必须写成(a 0 || b 0) c 0。这种歧义是代码评审里最容易引发争论的地方我的态度很明确直接用括号消除歧义。4.3 我的原则用括号表达意图我不打算劝你把整张优先级表背得滚瓜烂熟。说句实在话我自己在工作中写复合条件时也常常会给那些边界组合手动加括号。原因很简单写代码是写给下一个维护者看的优先级你是按规则推断的括号则是直接陈述的。括号不是给编译器看的是给人看的。比如闰年判断if ((year % 4 0 year % 100 ! 0) || year % 400 0)即使天然高于||、不加括号逻辑也正确我还是会把第一段用括号包起来。因为读者不用去推算优先级一眼就能看出这句话的意图是两种情况满足其一。顺便提两个在条件化简里很实用的定律——德摩根定律!(A B)等价于!A || !B!(A || B)等价于!A !B写复杂条件时把取反的与或式转换成非的或与式往往能让代码读起来顺很多。5. 三个实战场景闰年、成绩等级与登录校验5.1 闰年判断一个复合条件的经典入门闰年的规则是能被4整除但不能被100整除或者能被400整除。这种规则简直是练逻辑运算的完美题目。#include stdio.h int main(void) { int year; printf(请输入年份); scanf(%d, year); if ((year % 4 0 year % 100 ! 0) || year % 400 0) { printf(%d年是闰年\n, year); } else { printf(%d年是平年\n, year); } return 0; }拆解这个条件year % 4 0 year % 100 ! 0能被4整除且不能被100整除。|| year % 400 0或者能被400整除。注意像1900年可以被4整除也能被100整除但不满足不能被100整除所以1900年不是闰年2000年满足第二个分支所以是闰年。运行验证输入2024输出2024年是闰年输入1900输出1900年是平年。这一步验证很重要因为逻辑运算写不对这种题特别容易某几个年份对、某几个年份错。5.2 成绩等级用逻辑运算守好输入边界很多初学者写成绩等级判断喜欢把每个区间都写成独立条件if (score 90) grade A; if (score 80 score 90) grade B; if (score 70 score 80) grade C;这样写没错但非常啰嗦。更常见也更好的写法是配合else的天然排斥if (score 90) { grade A; } else if (score 80) { grade B; } else if (score 60) { grade C; } else { grade D; }但注意一旦进入else if分支就说明前面条件已经不成立了。所以score 80其实意味着score在 80~89 之间不需要再写 score 90。这种写法既简洁又避免了区间重叠的隐患。不过在此之前通常还要先做输入合法性校验这就轮到||上场了if (score 0 || score 100) { printf(成绩非法请检查输入\n); return 1; }一个非法即拦截的条件比好几个合法才放行的条件好写得多。这是因为逻辑或专门用来表达只要踩中任何一条红线就触发处理。5.3 登录校验字符串比较中的 0 陷阱字符串比较是初学者重灾区。先看一个模拟的登录判断#include stdio.h #include string.h int main(void) { char username[32], password[32]; printf(用户名); scanf(%31s, username); printf(密码); scanf(%31s, password); if (strcmp(username, admin) 0 strcmp(password, 123456) 0) { printf(登录成功\n); } else { printf(用户名或密码错误\n); } return 0; }这里的核心点在于strcmp的返回值两个字符串相等时strcmp返回 0。所以判断相等必须写 0而不是! 0或者干脆漏掉。为什么这么设计因为strcmp返回的是两个字符串逐字符比较的差值大于0表示第一个大小于0表示第一个小。这个设计在排序场景很有用但也成了无数新手第一次写登录功能时的拦路虎。我看到过不少代码把条件写成strcmp(username, admin)随手点一下编译器就通过了因为C语言允许把任意整数值当条件用——非零为真、零为假。而strcmp相等时返回 0恰好是假登录逻辑就整个反过来了。所以记住这个搭配字符串相等 →strcmp(...) 0。6. 关系与逻辑运算的常见坑位清单6.1 if (x 5)赋值当比较的老牌陷阱这大概是C语言界流传最广的翻车案例。新手写比较相等时手一抖漏写一个等号if (x 5)变成了if (x 5)int x 0; if (x 5) { printf(x 等于 5\n); }让我说清楚它会发生什么x 5先把 5 赋给 x然后这个赋值表达式的值就是 5。5 是非零值在C语言的真假观里等于真所以if一定会进入代码块。你以为在做判断实际上在做赋值而且条件永远成立。这个问题的经典防御写法是把常量写到左边if (5 x)如果你再次手抖写成if (5 x)编译器会直接报错——因为 5 是字面量不能作为赋值的目标。这属于C语言里所谓左值的概念简单理解就是能被赋值的必须是变量不能是常量。现代编译器对if (x 5)也会给出 warning但很多人在刷题网站上忽略警告然后对着错误结果百思不得其解。6.2 a b c链式比较为什么永远别用数学老师教过你a b c表示a小于b且b小于c。C语言并不认这套它只会机械地从左往右算int a 3, b 2, c 1; if (a b c) { printf(你猜我会不会执行\n); }这段代码居然会执行。过程是这样的先算a b也就是3 2结果是 0再算0 c也就是0 1结果是 1。于是整个条件为真程序输出那句话。可 3 2 1 明明是假的C语言却告诉你成立。想表达数学上的链式比较必须用逻辑与把它们拆开if (a b b c)我在代码评审里真抓出过这种写法。对方还很自信地说这有什么问题数学里就这么写的。问题大了去了运行结果和你脑子里的数学概念完全是两回事。这个坑几乎每个C语言学习者都会踩一次踩完就记住了。6.3 一段话总结我的避坑经验关系运算和逻辑运算本身不难难点全在细节的累积上。我根据自己的踩坑和带学生的经验把最重要的事情浓缩成几条比较相等用条件里写单个九成是bug。任何非零值是真零是假所以strcmp相等返回0这种反直觉返回值要格外留意。不要写a b cC语言不提供链式比较。浮点数比较用fabs(a - b) epsilon不要直接。、||有短路求值别在右边的表达式里塞自增、自减或赋值操作。复合条件不确定优先级时直接加括号。括号不花钱能省很多debug时间。把这几条刻进脑子里后面学if、while、for的时候会顺畅非常多。关系运算和逻辑运算不是孤立的语法点它们是你写任何一份带判断逻辑代码的地基。地基打牢了写出来的程序才经得起推敲。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑