1. 先把C语言操作符的“家谱”捋一遍
操作符这玩意儿,说白了就是你对数据动手的“工具”。很多人在初学阶段把它理解成简单的加减乘除,这其实亏大了。C语言的操作符是这门语言极其核心的一块,它决定了你能否写出简洁、高效、可读性强的代码。你可以把操作符想象成乐高积木里的不同连接件,普通用户只理解“搭起来”,高手知道每种连接件的受力方向和承重上限。接触的第三个阶段,就是在已经认识几个操作符的基础上,系统地把它们归位、弄懂区别、搞清楚优先级和结合性,这一步跨过去了,后面看别人写的代码会顺畅很多。
我在学习过程中,明显感觉到翁恺老师的教学思路就非常重视这一段。他反复强调,操作符不是孤立存在的,它跟变量、表达式、控制流程强绑定。你写int sum = a + b;是操作符,写if (a == b)也是操作符,写while (i++ < 10)更是操作符。衡量一个C语言学习者是否入门,最简单的标准就是问他:a++和++a到底有什么区别?如果答得吞吞吐吐,说明基础还欠火候。
C语言的操作符数量差不多有四十多个。如果按功能扎堆,常见的分类是这样的:
- 算术操作符:
+、-、*、/、% - 关系操作符:
>、<、>=、<=、==、!= - 逻辑操作符:
&&、||、! - 位操作符:
&、|、^、~、<<、>> - 赋值操作符:
=、+=、-=、*=、/=等 - 自增自减:
++、-- - 条件操作符:
?:,也就是三目运算 - 逗号操作符:
, - 其他:
sizeof、强制类型转换()、取地址&、解引用*等
乍一看会觉得“怎么这么多”,但实际写代码的时候,90%的代码用到的只是算术、关系、逻辑、赋值和自增自减这五类。位运算和逗号操作符属于特定场景下的利器,不会天天用,但一定要知道它们存在的意义。
1.1 算术、赋值与关系操作符的常见误用
算术操作符里,最容易出问题的就是除法/和取模%,尤其是涉及到负数的时候。整型除法是“截断”的,不是“四舍五入”。7 / 2结果是 3,不是 3.5。这个大家都能理解,但-7 / 2是多少?在C语言标准里,C99 之后规定结果是 -3,也就是向零截断。不同编译器和语言在这个点上可能会不一样,所以跨语言编程时很容易埋坑。而取模操作符%要求两个操作数必须是整数,同样,负数取模的结果在不同标准下也有历史差异,C99 之后规定a % b的符号与a相同。举个例子:-7 % 2在C语言里是 -1,不是 1。我在给初学者讲的时候,他们就经常在这个地方卡住,因为数学直觉告诉他们 -7 除以 2,余数应该是 1。这里必须先接受 C 语言的规则:余数符号跟随被除数。
关系操作符的误用集中在把==写成=。这大概是C语言初学者最常见的翻车现场。if (a = 3)在C语言里是完全合法的,它做的事是:把 3 赋值给 a,然后判断 a 的值(也就是 3)是否为真,结果当然是真。这会导致程序逻辑完全乱掉。我自己早期甚至因为这个 bug 排查了好几个小时,后来才学着把“常量写在左边”来止损,也就是写成if (3 == a)。如果笔误写成if (3 = a),编译器直接报错,不会给你运行时留隐患。
关系操作符的另一个坑是浮点数比较。永远不要直接用==比较两个浮点数是否相等。因为浮点数在计算机里是近似存储的,比如 0.1 + 0.2 并不精确等于 0.3。这种误差累积起来会让你的判断失效。正确做法是比较两个数的差的绝对值是否小于一个很小的阈值,比如fabs(a - b) < 1e-6。这属于典型的需要靠经验才能总结出来的技巧。
1.2 位运算、逗号、条件操作符:看起来冷门,实战很香
位运算在初学者阶段容易被忽略,因为平时做数学题根本用不上。但往深处走,尤其是后面接触单片机、嵌入式、底层的寄存器操作时,位运算是吃饭的家伙。a & 0xFF是取出 a 的低8位;a | 0x80是把第7位置1;a ^ b是异或,它可以用来交换两个变量的值,虽然日常工程里一般不这么干(可读性差且未必更快)。位运算是直接对二进制位动手,处理效率极高,而且天然适合表示标志位。比如你要用一个unsigned char存8个开关状态,每个bit对应一个开关,置位、清零、判断,全都通过位运算实现,这就是典型的高级用法。
条件操作符?:是C语言里唯一一个三目操作符。别看它少,它在表达式里非常好用。比如int max = a > b ? a : b;一句话就把求最大值写完了,比if-else简洁不少。但要注意不要滥用,嵌套多了可读性会很差。我的经验是:一个?:能看懂,嵌套两个就慌,嵌套三个就该重构了。
逗号操作符可能是最不受重视的一个。它的作用是按顺序执行多个表达式,然后把最后一个表达式的值作为整个表达式的值。比如for (i = 0, j = 10; i < j; i++, j--)这种写法就是逗号操作符的经典应用。在for循环的初始化部分和调整部分都可以用逗号操作符塞多个语句,非常方便。
2. 优先级和结合性:程序跑歪的根源基本都在这
操作符光认识没用,你还得知道它们之间谁说了算。C语言的“谁说了算”就是优先级和结合性。优先级解决的是“不同操作符谁先执行”,结合性解决的是“相同级别的操作符按什么方向执行”。好多初学者代码逻辑没问题,运算结果就是不对,多半是优先级踩了雷。
老实说,优先级表那几十行的规则,没有谁能百分之百背下来,我自己看代码时也会经常翻表。但有几个常见组合必须烂熟于胸:赋值操作符优先级很低,逗号操作符优先级最低,位运算比关系运算低、比逻辑运算高(这个排列很恶心,所以位运算的表达式强烈建议加括号)。
举个经典例子:
if (a & b == c)这行代码想表达什么?逻辑上你可能以为是(a & b) == c,但根据C语言优先级,==的优先级高于&,所以实际上它等价于a & (b == c)。这种坑你哪怕写一辈子C语言都可能踩到,因为它的结果不是编译错误,而是逻辑错误。预防方案只有一个:位运算左右都加括号。别相信自己的记忆力,加括号永远是对的。
2.1 一张表吃透优先级:不需要死记,但必须会查
为了让你有个整体印象,我画了一张精简过的优先级表(按从高到低排列),把常用的操作符都列出来。你自己写代码的时候可以对照着看,不要求背,但一定要知道去哪查。
| 优先级 | 操作符 | 作用 | 结合性 |
|---|---|---|---|
| 1 | ()[].->++--(后缀) | 函数调用、下标、成员访问、自增自减后缀 | 左到右 |
| 2 | !~++--(前缀)-(类型)*&sizeof | 逻辑非、按位取反、自增自减前缀、负号、强制转换、解引用、取地址 | 右到左 |
| 3 | */% | 乘法、除法、取模 | 左到右 |
| 4 | +- | 加法、减法 | 左到右 |
| 5 | <<>> | 左移、右移 | 左到右 |
| 6 | <<=>>= | 关系比较 | 左到右 |
| 7 | ==!= | 相等比较 | 左到右 |
| 8 | & | 按位与 | 左到右 |
| 9 | ^ | 按位异或 | 左到右 |
| 10 | ` | ` | 按位或 |
| 11 | && | 逻辑与 | 左到右 |
| 12 | ` | ` | |
| 13 | ?: | 三目条件 | 右到左 |
| 14 | =+=-=*=/=%=&=^=` | = <<= >>=` | 赋值运算 |
| 15 | , | 逗号 | 左到右 |
你发现规律没有?优先级最高的是括号和成员访问,最低的是逗号。赋值操作符的优先级低得出奇,所以才会有while ((ch = getchar()) != EOF)这种把=整个用括号包起来的经典写法。
2.2 结合性到底怎么理解:从右到左的那些坑
结合性是优先级相等时的裁决规则。大部分操作符是从左到右(左结合),比如100 / 10 * 5就是(100 / 10) * 5,结果是50,不是2。但有几个要特别留心是从右到左的:赋值类、条件操作符、前缀自增自减、取地址、解引用、强制转换。
a = b = c是怎么执行的?从右往左,先b = c,再把 c 的值赋给 a。这就是链式赋值。*p++呢?要拆开理解:后缀++优先级高于*,所以*p++先执行p++,再解引用指针,但因为是后缀自增,拿到的还是自增前那个位置的元素。对指针操作感兴趣的话,这个很值得玩味。
还有一个容易踩的是a ? b : c ? d : e这种嵌套三目。因为?:是右结合的,它等价于a ? b : (c ? d : e)。如果你不清楚这一点,代码就会做出与预期不同的选择。
3. 操作符在真实场景里怎么用:从练习到实战一步到位
光讲概念太抽象,我结合常见的C语言练习题和一些实际工程需求,把操作符怎么彻底用明白做个拆解。别小看这些例子,很多看起来“简单”的题目,背后全是操作符的基本功。
3.1 自增自减:九九乘法表、字符串逆序里的经典坑
九九乘法表是很多人大学上机课的必做题。外层循环控制行数,内层循环控制列数。写法很多,但很少有人注意到里面有个操作符选择问题:是i++还是++i?在这类for循环里,两者差别不明显,因为都是单语句里的步进。但在表达式里,差别就大了。
课本上经常考的一道题:
int i = 5; int j = i++ + ++i;请问 j 是几?这种题目其实是不严谨的,因为C语言标准里,这种同一个变量在一个表达式里被多次修改的行为是未定义行为。不同编译器、不同优化级别下结果都可能不同。我在GCC和MSVC上都试过,结果不一样,甚至有次我把优化开高之后结果也变了。所以遇到这种题,直接跟老师说这题有问题就够了,别真的去纠结答案。真实写代码时,永远不要在同一个表达式里对同一个变量做两次自增自减。
字符串逆序的经典写法:
char str[] = "hello"; int left = 0, right = strlen(str) - 1; while (left < right) { char temp = str[left]; str[left++] = str[right]; str[right--] = temp; }这里的left++和right--就是后缀自增自减的典型应用。先取下标,再移动下标,动作紧凑又不容易看漏。如果想用前缀版本,就得先++right再赋值,逻辑绕一圈,没这个优雅。
在翁恺老师的作业题里,有一类是跟scanf配合使用自增自减的。比如把输入读到数组里,用scanf("%d", &a[i++]),先填入当前位置再移动下标。我自己不太喜欢这种写法,因为它在一行里做了两件事,调试的时候要停住看半天,但不可否认它很简洁。等到你代码量上去了,就会明白“可读性”往往比“简洁”更重要。能用两行说清楚的事,别硬塞一行。
3.2 位运算实战:ADC滤波、寄存器操作、二进制标志位
位运算在热词里出现得很多,尤其是“ADC值滤波函数”这个点。处理模拟量采样的常见滤波手段,比如滑动平均滤波或者去极值平均滤波,单纯用算术操作符也能写,但位运算能极大提升效率。比如你要对采样的 8 个值求平均,如果样本个数是 2 的幂次,比如 8 或 16,就可以用右移代替除法:sum >> 3(除以8)。这对于没有浮点单元、除法开销极大的单片机来说,是实打实的性能优化。
再比如寄存器操作,这是我们控制硬件时的日常。一个32位的寄存器,每一bit都有特定含义。你要把 bit5 置1,就写REG |= (1U << 5);你要把 bit5 清零,就写REG &= ~(1U << 5);你要翻转 bit5,就写REG ^= (1U << 5);你要判断 bit5 是否为1,就写if (REG & (1U << 5))。这三招基本上覆盖了95%的寄存器操作需求,也是位运算最标准的玩法。
还有个很经典的应用场景:用无符号整数当布尔数组用。假设系统里有32个运行标志,你开一个unsigned int flags = 0;然后把每个bit当作一个开关。置位用flags |= (1U << id);清除用flags &= ~(1U << id);判断用if (flags & (1U << id))。比起开一个bool flags[32],这种方式的栈开销小、缓存友好、拷贝方便,尤其在嵌入式领域极其常用。但要注意,代码可读性会下降,所以业内通常配一套宏定义或者函数封装来提升可读性。
3.3 短路求值与逻辑判断:闰年、鞍点、日期计算
针对热词里反复出现的“日期计算”、“鞍点问题”,它们真正吃操作符的地方不是算术,而是逻辑组合。
判断闰年的标准规则是:能被4整除但不能被100整除,或者能被400整除。用C语言写就是:
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))这里就有&&和||的短路求值问题。&&左侧为假时,右侧根本不会执行;||左侧为真时,右侧也直接跳过。这个特性在正常业务里影响不大,但在涉及到函数调用或者指针判断时,就是救命的技巧。
比如写:
if (p != NULL && p->value > 10)当 p 是 NULL 时,左侧为假,右侧直接不执行,也就不会发生对空指针解引用而崩溃的问题。如果你不小心写成if (p->value > 10 && p != NULL),那左侧先执行时 p 是空指针,程序直接就挂了。这就是为什么我们习惯把容易失败的判断放在短路求值的左边。
鞍点问题是二维数组里的经典题:找一个点,它在该行最大、在该列最小。逻辑判断的核心就是逐行扫描加逐列比对。我在做这个题时,经常看到同学犯一个错误:判断完行最大之后,直接认为它就是鞍点,忘了去检查列最小。这其实是操作符没组合好导致的逻辑漏洞。正确思路是两层循环,先找出第 i 行的最大值位置,再遍历该列检查是否是最小值。整个过程用到的是关系操作符的反复组合。
再拿日期计算举个例子。输入年、月、日,要求输出这是这一年的第几天。这里面最关键的运算就是在3月之前分情况,以及闰年的判断(因为2月的天数会变)。核心就是组合逻辑判断。之前提到的“输出该年第几天”在PTA和浙大OJ上是高频题,操作符的熟练度直接决定你写这个题的速度和准确率。
4. 操作符的边界地带:短路、除法、溢出、sizeof,这些细节决定成败
到了这个阶段,你可能已经会写不少代码了。但操作符这块的高阶细节,往往才是拉开人与人差距的地方。光知道“怎么用”还不够,你得知道“什么时候会出错”。
4.1 短路求值带来的隐藏副作用
我在前面简单提过短路求值。这里必须展开,因为它经常产生“看起来正常其实漏执行了”的bug。
看这段代码:
int i = 0; if (i && ++i) { // do something }因为 i 是 0,所以&&左侧为假,右侧的++i根本不会执行。如果你指望if之后 i 变成1,那你的预期就落空了。同理:
int a = 1; if (a || (a = 2)) { // ... }a 已经为真,右侧a = 2不执行,a 还是 1。这类写法我在代码评审里见过太多,都是被“我明明写了赋值啊”的抱怨包围着的。不要在逻辑操作符右侧写带副作用的表达式,这是铁律。副作用就是变量被改变这种事。你永远不知道将来重构代码的人会不会调整左右顺序,也不确定编译器优化会怎么处理。老老实实分开写,清晰而且靠谱。
还有一类跟短路效果类似的是三目运算符?:的“只算一处”特性:cond ? a : b中,cond 为真时只执行 a,为假时只执行 b,另一边绝不会执行。这个特性常被用来做运行时选择,但同样要小心,别在?:里放带副作用的表达式后去依赖另一边“没执行”带来的状态。
4.2 负数除法与取模:不仅仅是一个数学问题
前面提过 C99 之后整数除法向零截断。但从实战角度看,负数除法常常配合取模一起出问题。
举个经典例子:你要把一个带符号的整数拆成十进制位,比如把 -123 反转成 -321。很多人第一步写digit = n % 10;结果得到 -3,再 n /= 10 得到 -12,再取模得到 -2,再得到 -1,整个过程余数都是负的,导致后面拼接字符串时还要额外处理负号。这个解法不是不行,但很多新手第一次跑出来一堆负数会一脸懵。
更常见的场景是循环数组下标。假设一个环形队列用(index - 1 + n) % n来算前一个位置,如果 index 是负数,C语言的结果可能是负数,这会导致数组越界。业界标准做法是先加一个足够大的数再取模,或者用(index + n - 1) % n这种把负数先扳正。写这类代码时,一定要心里清楚“负数和取模混在一起,C语言会给一个跟随被除数符号的结果”。
4.3 sizeof的“假执行”陷阱
sizeof是一个编译期操作符,它计算的是类型或变量所占的字节数。有一个很经典的坑:sizeof的表达式在运行期不会真的求值。什么意思?看这段代码:
int i = 5; int size = sizeof(i++); printf("%d %d\n", i, size);结果 i 还是5,size 是4。因为i++这个表达式实际不会执行,sizeof 只是从类型上推算它的大小,不会引起副作用。很多初学者第一次遇到时都难以置信。知道这个特性后,你会发现它其实是优点:你可以用sizeof做类型判断而不用担心触发一次无谓的计算。
另一个sizeof的经典坑是数组名在表达式里的退化和在sizeof里的不退化的区别。sizeof(arr)返回整个数组占用的字节数,但sizeof(arr + 0)就只是指针的大小。这一点在写代码时一旦搞混,容易出现非常隐蔽的内存错误。比如你要把数组传进一个函数,在函数里用sizeof(arr)想拿到数组长度,结果是拿不到的,因为函数形参里的数组其实是被“退化”成指针了。所以永远不要在函数内部对形参数组用sizeof求长度。这就是C语言的很多细节与经验组成的地方。
4.4 赋值操作符与类型转换的“隐形手”
赋值的过程里常常伴随隐式类型转换。比如char c = 300;会发生截断,实际存进去的是 300 对 256 取模后的值,不同编译器对这种有符号整型转有符号窄类型的处理可能不同,但多数都会直接截断。这种“截断”往往会带来意想不到的符号位问题。
再比如:
int a = 5; float b = a / 2;这段代码的 b 是 2.0,不是 2.5。因为a / 2是整数除法,结果是2,然后才转换为浮点数赋值给 b。如果你想要 2.5,必须写成a / 2.0或(float)a / 2。这种隐式转换的坑,几乎每天都能在讨论帖里看到,尤其是大家在“除法精度”上翻车的时候。同理,double c = (a + b) / 2;看起来能整除,其实要小心(a + b)是否溢出。
5. 常见操作符错误与排查技巧实录
这节我把自己这些年在答疑、审代码、改bug里碰到的操作符相关问题整理成了一份速查手册,很多都是课本里不写但实际项目里反复踩的。
5.1 你好我好大家好的“赋值与相等”陷阱
=与==写错,是新手第一高频错误。除了养成“常量写左边”的习惯,还可以善用编译器警告。GCC 里用-Wparentheses可以额外检测这类问题;很多 IDE 也会把if (a = b)标黄。我的建议是,开了警告之后,把警告当错误处理,也就是常用的-Werror,这样有类似问题直接编译失败,就不至于把隐患带到运行时。
还有一点值得说:把返回值赋值语句写到判断条件里,有时候其实是故意的。比如前面说的while ((ch = getchar()) != EOF),这是C语言里精炼代码的经典手法。但前提是:你已经确认,并且读者也能看出来,你是有意为之,而且加了额外的括号来明确优先级。如果你不小心写成了while (ch = getchar() != EOF),那实际意思是先比较getchar()的结果和 EOF,再把比较结果赋值给 ch,这就完全是另一个程序了。这类写法上的细微差距,正是操作符优先级知识的用武之地。
5.2 自增自减混用的危害
前面提过同一表达式内多次修改同一变量是未定义行为。实际项目里我再补一个场景:函数参数里写i++。
printf("%d %d\n", i++, ++i);这行代码试图优雅地在printf里完成两步操作,但参数的求值顺序在C语言标准里没有明确规定,GCC 和 Clang 的结果都可能不同。后果就是你printf出来的两个数,在不同的编译优化级别下可能完全不一样。遇到这种问题,我在线上环境调试时,第一反应就是把表达式拆开,先i++再单独++i,或者先存临时变量。看起来是丢了点“酷炫”,但程序的可预测性上来了,排查成本也就下去了。
5.3 位运算和算术运算混用:加括号才是好习惯
前面提到a & b == c的优先级坑。实际里面还有更隐蔽的:a + b << 2其实是(a + b) << 2还是a + (b << 2)?根据优先级表,加法的优先级高于移位,所以在C语言里它是(a + b) << 2。这个结果有时恰好是你想要的,有时不是你想要的,但这种“幸好碰巧对了”的感觉是最危险的。我的建议很粗暴:所有位运算、移位运算和算术运算混用的表达式,一律括号包死,哪怕你觉得优先级已经背熟了。编程不是测试自己记忆力的游戏,代码是写给包括三个月后的自己在内的人看的。
5.4 浮点数比较与求模的特殊情况
有个冷门坑是浮点数取模运算fmod。标准库的fmod可以计算浮点数余数,但如果你一不小心直接用%对浮点数操作,编译器直接报错“invalid operands to binary %”。这个还不算难,难的是你想判断一个浮点数是不是整数时,绝对不能写f == (int)f,因为浮点误差可能让你判断错。更靠谱的是fabs(f - round(f)) < 1e-6,或者用fmod(f, 1.0)的值来判断。这种方法在实际项目中很常用。
5.5 复合赋值操作符的求值陷阱
a += b;等于a = a + b;这是大家都知道的。但有人会忽略:复合赋值操作符里,左边的表达式只求值一次。听起来像废话,实则不然。
arr[i++] += 5;问题来了:这个i++到底执行一次还是两次?C语言标准规定,复合赋值E1 op= E2等价于E1 = E1 op E2,但 E1 只求值一次。所以上面的代码等价于arr[i] = arr[i] + 5; i++;,而不是arr[i] = arr[i++] + 5; i++;。如果你没意识到这点,很容易在写了类似代码后把所有下标都搞错。这类坑在数据结构和算法题里出现的频率非常高,我甚至见到过有人因为这里的问题,整个排序算法跑出来的结果都是乱的。
6. 学习操作符的路线与方法建议
学操作符这件事,真的不是看一遍概念就完事,大量靠练习和查表。我个人的建议是分三步走。
第一步,跟着教材或视频把每个操作符的作用和优先级看一遍,动手写,写那种只看输出结果的测试小程序。比如写一个程序,分别打印5 / 2、-5 / 2、5 % 2、-5 % 2的结果,然后和预期对比。这看起来幼稚,但非常有用,能帮你把“数学直觉”和“计算机规则”区分开。
第二步,做几道经典的OJ题。比如PTA和浙大OJ上的日期计算、字符串逆序、九九乘法表、鞍点问题、冒泡排序。这些题覆盖面广,基本能把算术、赋值、关系、逻辑、自增自减、数组下标等操作符都练到。做完之后看看题目讨论区,通常会发现别人有更简洁的解法,这时候就去拆解对方的表达式,搞清楚每一步的优先级和结合性,学习效率极高。
第三步,如果能接触到嵌入式或者底层的项目,一定不要放过。位运算在寄存器配置、协议解析、内存池管理这些场景里的高频应用,会让你对操作符的理解上一个台阶。比如你写一个LED灯的状态切换程序,用异或^来翻转标志位,之后你会真的感受到“原来操作符还可以这么用”。
7. 环境与工具的选择建议
学C语言,环境这块确实纠结了不少人。我在热词里看到不少关于“编译器”、“虚拟机配置C语言环境”的讨论,这里想说点实在的。
初学者最不需要的就是折腾环境。现在随手可得的在线编译器,比如一些网页版IDE,直接就能跑C代码,省去了安装配置的麻烦。对于逻辑层面的学习,这就够了。但如果你要学的是文件操作、指针、位运算以及和硬件底层交互的内容,那还是得有本地环境。
Windows上,我推荐的组合是 Visual Studio 或者 VS Code + MinGW-w64。VS 集成度高,调试体验友好,但工程文件有时候会让初学者觉得“被框架绑架了”。VS Code 加扩展的方式更轻巧,但对新手不友好,一堆配置文件要折腾。Linux 上,GCC 就是亲儿子,又在包管理器里,一条命令装完就能用。如果实在要用 Windows 又不想装太多东西,那 Code::Blocks 或者 Dev-C++ 也行,虽然老,但在教学场景里胜在简单。
我个人更建议直接用虚拟机装个桌面版 Ubuntu,然后在里面装 VS Code 和 GCC。这样做的优点是,你学到的是真正贴近服务器和嵌入式开发的工具链,将来换环境也不会懵。缺点是需要一点 Linux 基础。好在网上教程非常丰富,照着走一遍下来,整个环境也就十几分钟的事。配置完成后,写第一行hello world的成就感,是值得的。
8. 针对初学者的一些最终心得
回顾我学C语言操作符的整个过程,最深的体会是:操作符不是“记住就行了”的知识,它是你写代码时思维的一部分。你最初写a = b + c可能需要停顿一下想想优先级,等到熟练之后,这些东西就像肌肉记忆一样,完全不会占用大脑的额外带宽。而达到这个状态最有效的路径,就是多做练习,多读别人代码,多分析“为什么这里要加括号”。
我在实际带人过程中发现,很多学习者卡住的点并不是某个操作符本身,而是“操作符优先级和流程控制、函数调用混在一起”的组合场景。比如一个while循环里有scanf返回值判断,有条件赋值,还有指针偏移,这三样加一起,新手基本就懵了。这个时候我通常的建议是:拆。一行代码拆成三行,每一步打印一次中间结果,看清楚之后再决定要不要合并回去。
另外,调试工具的价值怎么强调都不过分。GDB 是排查C语言问题最靠谱的帮手,你不会用单一的调试输出语句一条条看值,也能解决,但效率天差地别。热词里提到的“利用GDB调试C语言程序”就是很好的学习方向。设置断点、打印变量、单步执行,你很快就能直观看到操作符执行的顺序和中间值的变化。我至今遇到优先级拿不准的表达式,第一反应还是在GDB里跑一下看看结果,而不是空想。
最后,保持“较真”的态度。遇到a++和++a这种问题,不要放过,用代码去如实测试验证。遇到奇怪现象,先怀疑操作符,再怀疑环境。这个排查思路,能帮你少走很多弯路。