ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C语言运算符与表达式:优先级、结合性与易错细节全解析

2026/10/1 18:43:30 拓冰建站 浏览量
C语言运算符与表达式:优先级、结合性与易错细节全解析 我最初学C语言的时候最没当回事的一章就是“运算符与表达式”。语法嘛不过就是加减乘除、加减乘除、比较大小、逻辑判断这有什么好学的结果后来才发现不管是考试、刷题还是项目里跑偏出来的bug十有八九都跟这章没学透有关。比如*p到底是先取指针指的值还是先让指针移动a b里b到底执行没有1.0f 1.0为什么不成立这些问题看着小真踩上去个个扎脚。这篇就把C语言里运算符和表达式这章掰开揉碎讲一遍把很容易想当然但实际会翻车的细节都圈出来。内容不会太长——我尽量说人话、用案例把这些知识点一次过透。不管是刚入门想打好地基还是考试前想临阵磨枪这篇文章都适合。1. 为什么这章卡住了很多人运算符与表达式的真实体感1.1 先搞清楚“表达式”到底是个什么概念书面上说“表达式是由运算符和操作数组成的式子”这话没错但太干巴了。我的理解更直白表达式就是“能求出一个值的东西”。1 2是表达式结果是3a * b是表达式结果是某个数连x 3本身也是表达式它的值是3。关键是理解“求值”这件事。C语言里几乎所有东西都能看作表达式包括赋值、函数调用、逗号运算。比如int m (a 3, b 4, a b);这个表达式最后的值是7a被赋成3b被赋成4。逗号表达式每一项都会执行整个表达式的值是最后一项的值。这种写法吓人但确实存在而且有些“老夫老衲”的代码就靠这个压缩行数。再补一个概念左值lvalue和右值rvalue。左值指的是在内存中有明确位置的东西比如变量、数组元素右值就是单纯的“值”。a b 1里a是左值b 1是右值。为什么我强调这个因为很多人不理解为什么3 a会报错——3没有内存位置不能当左值。也不理解为什么(a b)编译不过——a b的结果是个临时值不是左值不能自增。记住这条线后面很多编译报错你一眼就能看懂。1.2 运算符分类先建立一张完整的“地图”C语言的运算符数量其实比大多数人印象里多得多别只盯着算术那几只。我按功能把常用的分了几类初学者把这地图记熟了后面看代码才不会懵。类别运算符典型用途算术运算符 - * / %小学级别的加减乘除加上取余关系运算符 !比较大小、判断相等逻辑运算符 || !与、或、非位运算符 | ^ ~ 按位与、或、异或、取反、移位赋值运算符 - * / % | ^ 各种复合赋值条件运算符? :三元条件逗号运算符,依次求值指针运算符* 取内容、取地址求字节数sizeof算类型占多少字节下标运算符[]访问数组元素成员访问. -访问结构体/指针结构体成员其中我觉得最容易被忽略的是取余运算符%。它只适用于整数浮点数不行。还有它的结果正负号跟随被除数比如-7 % 3的结果是-1不是1。很多人在笔试里栽在这。位运算符这块很多初学者一开始用不上但等学到位图、状态压缩、低层协议解析的时候就会回来补课。先把 \| ^ 这五个认下来逐个配合进制转换练几遍后面不慌。2. 表达式怎么搭才不容易错我的一套实用构造思路2.1 从最简单的表达式起步逐层叠加复杂因子很多新人上手写表达式喜欢一口气憋出一长串比如result a * b c / d % e f - g * h[i] (j k ? l : m) * n;这种写法如果逻辑在心里过了三遍还能完全说清那也算本事。但现实里我见过太多同学写这种长表达式一编译出优先级警告人直接懵掉。我的建议是从最简表达式逐层往上搭每一步都确认求值顺序和结果类型不要一口气写完。比如要实现一个函数判断“三个整数是否构成三角形”。不要直接写if (a b c a c b b c a)而是心里走四步先拿到三条边长a, b, c。第一次拆解“构成三角形”的条件任意两边之和大于第三边。分别写成三个简单关系表达式a b c、a c b、b c a。三个条件都要成立所以用逻辑与连接a b c a c b b c a。这个思路的核心是关系运算符优先级高于逻辑与先算大小比较再组合“并且”。你先写三个独立关系式再串起来甚至不需要背优先级表也能大概率写对。再来个更常见的题目判断闰年。规则是“能被4整除但不能被100整除或者能被400整除”。直接奔着一行写if (year % 4 0 year % 100 ! 0 || year % 400 0)这句语法没问题但读起来费劲而且隐含了高于||的优先级。我的建议是加一对括号把意图锁死if ((year % 4 0 year % 100 ! 0) || (year % 400 0))别嫌括号多括号是优先级问题的最廉价解药没有之一。2.2 经典题目拆解用“5×5鞍点”例子理解表达式怎么组合成逻辑热词里有一个“计算5×5鞍点问题”这题特别适合练“表达式 分支/循环”的组合能力。题目是说在5×5的矩阵里某元素既是它所在行的最大值又是它所在列的最小值这个元素就叫鞍点。这题当年我老师布置下来全班不少人卡住原因不是循环没学会而是“比较表达式逻辑没理清”。我的做法是先不要太早纠结实现把条件翻译成“伪表达式”“是第i行的最大值”等价于max_row[i] a[i][j]“是第j列的最小值”等价于min_col[j] a[i][j]这两个都要成立所以串联max_row[i] a[i][j] min_col[j] a[i][j]。然后才动手写代码#include stdio.h #include limits.h int main(void) { int a[5][5]; int max_row[5], min_col[5]; int i, j; for (i 0; i 5; i) for (j 0; j 5; j) scanf(%d, a[i][j]); // 初始化行最大值用整型最小值列最小值用整型最大值 for (i 0; i 5; i) { max_row[i] INT_MIN; min_col[i] INT_MAX; } // 先算每行最大值 for (i 0; i 5; i) for (j 0; j 5; j) if (a[i][j] max_row[i]) max_row[i] a[i][j]; // 再算每列最小值 for (j 0; j 5; j) for (i 0; i 5; i) if (a[i][j] min_col[j]) min_col[j] a[i][j]; // 最后逐个检查 for (i 0; i 5; i) for (j 0; j 5; j) if (max_row[i] a[i][j] min_col[j] a[i][j]) printf(鞍点: a[%d][%d] %d\n, i, j, a[i][j]); return 0; }注意里面两个细节这也是初学者容易漏掉的一是初始化。算最大值不能把变量初始化为0否则矩阵里全是负数时就错了得用INT_MIN算最小值反过来用INT_MAX。这就是为什么要#include limits.h。二是比较条件。max_row[i] a[i][j] min_col[j] a[i][j]关系运算符的返回值是0或1逻辑与再把这俩结果组合。你看表达式这张地图就是这么被用起来的每一个运算符都各司其职。3. 优先级这东西背表不如理解表3.1 一张分区记忆的优先级表别再死磕顺序我知道很多人一看到C语言运算符优先级表就头皮发麻几十个运算符按优先级排下来看着像天书。我的建议是不需要背完整表格只需要掌握六大分区。我从最优先到最不优先给你分好括号、下标、成员访问() [] . -——最高优先级神挡杀神。单目运算符! ~ -- - * sizeof——紧跟其后。算术运算符先乘除取余* / %后加减 -。移位运算符 低于加减但高于关系。关系运算符 先来 !后来等于不等于优先级更低。位与、异或、或^|这个顺序经常有人记反。逻辑与、或||。条件运算符?:这个优先级很低低于逻辑。赋值运算符 ...几乎最低先把别的事干完再赋值。逗号运算符最低没有之一。为什么这样分组就够了因为C语言的优先级设计其实有内在规律单目高于算术算术高于移位移位高于关系关系高于位逻辑位逻辑高于逻辑逻辑高于条件条件高于赋值赋值高于逗号。这个从高到低的链条跟“操作的粒度从小到大”是对应的先处理单个变量/指针再处理四则运算再处理比较最后处理判断和执行。可以类比成日常说话括号是把句子切开的停顿单目是词级修饰算术是短语内部运算关系是短语间的比较逻辑是句子间的连接赋值是整句话落定。顺序顺下来比背表格强得多。3.2 几个特别容易被“直觉”带偏的优先级细节细节一 和加减的优先级关系。1 2 3你会先算哪个很多人以为是(1 2) 3其实C语言规定算术优先于移位所以实际是1 (2 3)结果是32。这个坑一旦踩了排查半天才发现是优先级问题非常憋屈。细节二位逻辑和关系运算的顺序。a 0xff 0你以为是先按位与再比较实际上的优先级比高这句先判断0xff 0再把a和这个假值按位与。建议判等最好是显式加括号(a 0xff) 0。细节三条件运算符的低优先级。a b c ? 1 : 2;其实是a (b c ? 1 : 2)因为条件运算符比赋值高不会跑出(a b c) ? 1 : 2的歧义。但如果写a b c ? 1 : b c ? 2 : 3;很多人就晕了。这个表达式实际等价于a (b c ? 1 : (b c ? 2 : 3))条件运算符是右结合的从右往左处理。我建议嵌套三元表达式尽量放在括号里工程代码里尤其如此不然代码评审时别人看半天还是一脸问号。这里给个最实际的建议记不清就加括号。加括号不会让代码变慢编译结果也几乎没差别但能让你和看代码的人少掉一大把头发。4. 结合性存在感很低但坑起来要命4.1 从左往右 vs 从右往左赋值和自增全在这优先级决定谁先算结合性决定同优先级运算符按什么方向算。C语言里大多数运算符都是从左往右结合的看着很正常但有两个例外全是麻烦制造者赋值运算符和条件运算符是右结合的。为什么赋值右结合因为链式赋值需要它a b c 0;这句能成立的根本原因就是赋值从右往左结合先c 0结果是0再b 0最后a 0。如果赋值是左结合那a b先来整个式子就崩了。还有一个不得不提的经典坑*p到底算什么很多教材说“*p等价于*(p)”这句话对了一半容易误导。拆一下后置的优先级高于*所以*p被解析成*(p)。但这不代表“先取指后自增”这么简单。准确的结果是表达式整体返回p原本指向的那个值同时p本身向后移动。对新手来说这句话的意思是*p拿走了原始指向的值下次再访问p就已经跑了一位。写链式遍历的时候这个用法很顺手比如把数组元素一个个读出来while (count-- 0) sum *ptr;这里*ptr让指针每次循环后自己前进代码简洁但也容易让初读的人一愣。我的经验是这种“副作用藏在表达式里”的写法适合自己写、自己看工程协作里最好拆开写我还见过有人在这类代码上反复纠缠半天。4.2 副作用和“未定义行为”别在同一个表达式中重复修改同一变量这是C语言里最让人头大的一块。像a a (a 1);这种在C标准里其实是undefined behavior未定义行为。不是“结果碰巧是多少”的问题而是编译器怎么处理都行——结果可能每次编译都不一样。为什么这样设计因为C语言标准想让编译器有充分的优化空间。如果表达式里有“对一个变量的多个副作用”且顺序不明确编译器可以按自己方便的顺序安排这恰恰意味着你写出的代码结果不可预期。典型例子int a 1; int b a a; // 不推荐行为未定义有的编译器结果是2有的结果是3有的结果对着不同版本又变了。你千万别抱着“我的编译器一直出2所以没事”的心态换个编译平台或者开个O2优化直接翻车。我的建议就一条一个表达式里对同一个变量最多只能有一个副作用。比如sum arr[i];问题不大但arr[i] i;必死无误。如果你发现需要在一个表达式里干太多事及时拆两行。还有一个常见的误区a get_val() get_val2()这种两个函数的调用顺序在C标准里也未必确定。虽然现在大多数编译器是从左到右调用但标准从来没保证过。如果这两个函数共享某个全局状态先后顺序影响结果那这代码就不稳了。别依赖函数调用的顺序宁可一步步写清楚。5. 工程实战里最容易翻车的几个运算符细节5.1 短路求值看起来理所应当用错了很难查逻辑与和逻辑或||都有短路特性也就是如果左边的结果已经能决定整体结果右边就不执行了。这个特性平时用着很爽但写不好就是隐藏的雷。比如这个经典表达式if (ptr ! NULL ptr-value 10)ptr为空时ptr-value根本不会执行所以不会崩。这是短路的一大好处几乎每个C程序员都会用到。但反过来这种就会出问题if (i 5 arr[i] 0) { ... }如果i 5arr[i]根本不会执行那i就不会自增。你原本期待不管条件成不成立都自增结果逻辑错了还没发现。我见过函数里因为这种问题少走了几步循环最后浪费了大半个下午排查。所以记住一个原则短路求值不是“运算符特性”而是“执行路径的分叉点”。你写表达式的时候必须意识到右边代码可能压根没跑。5.2 浮点数比较为什么“1.0 / 10.0 1.0 / 10.0”可能为假浮点数比较是新手几乎必踩的坑。我先说结论不要直接用比较两个浮点数。原因在于浮点数是二进制表示的很多十进制小数比如0.1在二进制里都是无限循环小数只能近似存储。0.1 0.2在计算机里实际上等于0.30000000000000004左右直接和0.3比较当然是假。正确的做法是判断“差的绝对值是否足够小”#include math.h double a 0.1 0.2; double b 0.3; if (fabs(a - b) 1e-9) { // 视为相等 }那个1e-9就是误差容忍度具体取多少看业务需求通常是1e-6到1e-12之间。这不算什么高深技巧但它能让你的浮点判断从“时灵时不灵”变成“稳如老狗”。5.3 自增自减的三种典型用法把副作用控制在你能看见的位置和--有三种常见位置效果差别很大。很多人只记了i是先用后加i是先加后用真用起来还是会乱。我给一个更稳定的理解方式i就是“改值并返回新值”。i就是“改值但返回旧值”。单独写i;和i;时两者没区别都只是让变量加一。所以真正有分别的场合是“返回值被使用”的场景。比如int a 1; printf(%d\n, a); // 输出1 printf(%d\n, a); // 输出3上一步a已为2此步先自增变3再输出还有一个二分查找里常见的用法把中点计算包裹在low (high - low) / 2里这不算自增但很多人在里面误写(low high) / 2导致整数溢出也要小心。这类细节属于“代码跑起来看着对测试用例一撑爆就现原形”的类型。我的经验就是一句话如果或--的返回值没被用到就单独写一行别和其他运算符挤在一起。这样代码可读性和安全性都大幅提升。把副作用限制在最长一眼看到的位置是写C的老手都会遵守的纪律。最后一个新手容易困惑的printf(%d, 5 % 2 * 2 1)之类混合运算。你就按优先级表一层层拆先乘除取余后加减同级的从左往右就能拿到结果。出题者很喜欢用这种式子考你优先级和结合性的功底练几道手感就出来了。