上一篇我们讲完了算术运算符和赋值运算符,评论区就有读者在催:什么时候讲判断?我想写if,可光会算加减乘除不顶用。这一篇正好补上通往分支和循环的"最后一公里"——关系运算与逻辑运算。别看这两个词听起来平淡,它们几乎构成了C语言里所有决策逻辑的骨架。我见过不少初学者在指针上没翻车,反而栽在 a < b < c 这种看似无害的比较上。这篇我会把六种关系运算符、三种逻辑运算符、短路求值机制和优先级顺序一次讲透,顺便带三个能直接抄进作业和项目里的实战例子。
1. 关系运算:六个比较符号,最终只回答"是"或"否"
1.1 六种关系运算符一次看清
C语言提供了六种关系运算符,本质上就是你在数学课上学过的六种比较。先看这张表,我建议直接存下来:
| 运算符 | 含义 | 示例 | 结果为真的条件 |
|---|---|---|---|
> | 大于 | a > b | a 严格大于 b |
< | 小于 | a < b | a 严格小于 b |
>= | 大于等于 | a >= b | a 大于或等于 b |
<= | 小于等于 | a <= b | a 小于或等于 b |
== | 等于 | a == b | a 与 b 相等 |
!= | 不等于 | a != b | a 与 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(真),那所有可能组合无非四种:
| a | b | !a | a && b | a || b |
|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 0 | 1 |
| 1 | 1 | 0 | 1 | 1 |
记忆口诀就两句话:逻辑与"一假即假",逻辑或"一真即真"。&&比||更严格,要求全员通过;||更宽松,有一个人点头就行。!最干脆,反过来就完事。
真值表在实践中的价值在于:当你在一个复合条件里写了三个以上的逻辑运算符时,可以一步步用真值表化简,排查到底是哪个条件把孩子带偏了。
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); // 先算 a+b=3,再比较 3>2,结果是 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 < c,C语言不提供链式比较。 - 浮点数比较用
fabs(a - b) < epsilon,不要直接==。 &&、||有短路求值,别在右边的表达式里塞自增、自减或赋值操作。- 复合条件不确定优先级时,直接加括号。括号不花钱,能省很多debug时间。
把这几条刻进脑子里,后面学if、while、for的时候会顺畅非常多。关系运算和逻辑运算不是孤立的语法点,它们是你写任何一份带判断逻辑代码的地基。地基打牢了,写出来的程序才经得起推敲。