1. 为什么运算符优先级值得单独拎出来讲很多人学C语言的时候运算符优先级这张表通常是扫一眼就翻过去了。原因也很简单——写代码的时候多加几个括号不就完了这个想法本身没错但问题在于你不可能给每一行代码都套上括号而且当你读别人代码的时候尤其是读一些底层库、宏定义、或者嵌入式驱动代码的时候运算符优先级和结合性直接决定了你能不能看懂那行代码到底在干什么。我见过太多这样的情况一段代码逻辑看起来没问题编译也不报错但运行结果就是不对。排查半天最后发现是*p和(*p)搞混了或者是a b c这种表达式被编译器理解成了a (b c)。这类问题不是靠调试器能快速定位的因为编译器完全按照优先级规则来解析它觉得你的代码没问题只是你的意图和它的理解不一致。C语言一共有34种运算符不同教材和标准的统计口径略有差异有的算上sizeof、_Alignof等有的把复合赋值拆开算这里按常见的34种来梳理它们的优先级从高到低分为15个层级。这个层级关系不是随便定的背后有一套设计逻辑越靠近“原子操作”的运算符优先级越高越偏向“整体运算”的优先级越低。理解了这个逻辑你就不需要死记硬背那张表了。这篇文章适合所有写C语言的人看——不管你是刚学完基础语法的新手还是写了几年代码但一直靠加括号规避问题的老手。我会把34种运算符的优先级和结合性拆开讲清楚更重要的是告诉你哪些地方最容易踩坑、怎么快速判断一个复杂表达式的求值顺序、以及在实际编码中怎么用最少的括号写出最清晰的代码。2. 从“原子”到“整体”优先级层级的底层逻辑2.1 最高优先级那一档后缀运算符优先级最高的四个运算符是()函数调用、[]数组下标、-结构体指针成员访问、.结构体成员访问。这四个加上后缀和--构成了优先级的第一梯队。为什么它们最高因为它们都是“取值”操作——从一个已有的对象中取出某个部分或者调用某个函数。你可以把它们理解成“原子操作”就像化学里的原子一样是构成更大表达式的基本单元。比如arr[i].name先通过[]取出数组元素再通过.取出成员这两个操作都是“取值”所以优先级最高从左到右依次执行。这里有一个容易忽略的点后缀和--的优先级和()、[]、-、.是同一档的结合性也是从左到右。这意味着p-next的意思是(p-next)而不是p-(next)。同理arr[i]是(arr[i])。这个规则在实际写链表操作的时候特别重要。2.2 单目运算符从右向左的“改造”操作第二梯队是单目运算符前缀、前缀--、正号、-负号、!逻辑非、~按位取反、*解引用、取地址、sizeof、(type)强制类型转换。这些运算符的特点是“单目”——只作用于一个操作数而且结合性是从右向左。什么叫从右向左就是!!x先算右边的!x再算左边的!。*x先算x取地址再算*解引用结果就是x本身。这里有一个非常经典的坑*p。很多人第一反应是“先取p指向的值然后p自增”。但实际上后缀的优先级高于单目*所以这个表达式等价于*(p)——先执行p返回p的旧值同时p指向下一个位置然后对旧值解引用。也就是说你取到的是p原来指向的那个值但p本身已经移动了。如果你想取当前值然后移动指针*p是对的如果你想取当前值但不动指针应该写(*p)或者直接*p。再比如sizeof和强制类型转换。sizeof(int)*p的意思是(sizeof(int)) * p因为sizeof的优先级高于*。而(int*)p是把p强制转换成int*类型这里的(int*)是一个整体优先级和单目运算符同级。2.3 算术运算符先乘除后加减的“数学直觉”第三梯队是算术运算符分两层*、/、%在上一层、-在下一层。这个和数学里的“先乘除后加减”完全一致所以大部分人不会在这里犯错。但有一个细节值得注意%取模的优先级和*、/是一样的结合性也是从左到右。所以a % b * c等价于(a % b) * c而不是a % (b * c)。这个在写循环控制或者哈希计算的时候容易搞混。另外算术运算符的结合性都是从左到右。a - b - c等价于(a - b) - c这个大家都知道。但a / b * c等价于(a / b) * c有些人会误以为是a / (b * c)结果算出来的值差很多。2.4 移位运算符比算术低比关系高第四梯队是移位运算符和。它们的优先级低于算术运算符但高于关系运算符。这意味着a b 2等价于(a b) 2而不是a (b 2)。这个规则在设计位操作的时候很有用——你可以先做加法再做移位不需要额外加括号。但反过来a 2 b等价于a (2 b)因为的优先级高于。这个就很容易出问题了。我见过有人在写嵌入式代码的时候想表达“a左移2位然后加b”结果写成了a 2 b实际执行的是“a左移(2b)位”结果完全不对。2.5 关系运算符比移位低比相等高第五梯队是关系运算符、、、。第六梯队是相等运算符、!。为什么要把关系运算符和相等运算符分开因为a b c d这种表达式按照优先级会解析成(a b) (c d)也就是先比较大小再比较两个比较结果是否相等。这个规则在写条件判断的时候偶尔会用到但大多数情况下这种表达式可读性很差建议加括号。这里有一个非常经典的坑a b c。很多人想表达“a和b按位与的结果等于c”但的优先级高于所以实际解析成a (b c)。如果b和c都是布尔值或者0/1这个表达式可能还能跑但逻辑完全错了。正确的写法是(a b) c。2.6 位运算符容易被忽略的优先级陷阱第七梯队是按位与第八梯队是^按位异或第九梯队是|按位或。这三个运算符的优先级都低于相等运算符这意味着a b c会被解析成a (b c)a | b c会被解析成a | (b c)。这个规则几乎每年都会让一批人踩坑。为什么C语言要这样设计因为位运算符的优先级设计参考了逻辑运算符的优先级——高于||对应的高于^高于|。但问题在于的优先级被放在了上面这就导致了“先比较再按位与”的解析顺序和大多数人的直觉相反。实际编码中只要表达式中同时出现了位运算符和比较运算符一律加括号。这不是可读性的问题是正确性的问题。2.7 逻辑运算符短路求值的优先级基础第十梯队是逻辑与第十一梯队是||逻辑或。这两个运算符的优先级关系是高于||这个和数学里的“与高于或”一致。结合性都是从左到右。逻辑运算符的优先级低于位运算符这意味着a b c | d会被解析成(a b) (c | d)。这个规则在写条件判断的时候需要注意——如果你想表达“a和b按位与的结果非零并且c和d按位或的结果非零”这个表达式是对的但如果你想表达别的意思就需要加括号。另外逻辑运算符的短路求值特性是C语言的一个重要机制。a b中如果a为假b不会被执行a || b中如果a为真b不会被执行。这个特性依赖于优先级——先于||求值所以a || b c等价于a || (b c)如果a为真整个表达式直接为真b c根本不会执行。2.8 条件运算符三目运算的右结合性第十二梯队是?:条件运算符。它的优先级低于逻辑运算符高于赋值运算符。结合性是从右向左。从右向左的结合性意味着a ? b : c ? d : e等价于a ? b : (c ? d : e)。这个规则在写嵌套条件表达式的时候很有用但嵌套太多层可读性会很差建议拆成if-else。条件运算符的优先级低于逻辑运算符这意味着a b ? c : d等价于(a b) ? c : d这个符合直觉。但a ? b : c d会出问题——因为赋值运算符的优先级低于条件运算符所以这个表达式等价于(a ? b : c) d而不是a ? b : (c d)。如果c是一个左值这个表达式可能能编译通过但逻辑完全错了。2.9 赋值运算符从右向左的“链式赋值”第十三梯队是赋值运算符、、-、*、/、%、、、、^、|。它们的优先级只比逗号运算符高结合性是从右向左。从右向左的结合性使得链式赋值成为可能a b c 0等价于a (b (c 0))。这个规则大家都很熟悉。赋值运算符的优先级低于条件运算符这意味着a b c ? d : e等价于a ((b c) ? d : e)这个符合直觉。但a b ? c : d e会出问题——因为的优先级低于?:所以这个表达式等价于a ((b ? c : d) e)而不是a (b ? c : (d e))。2.10 逗号运算符最低优先级的“顺序执行”第十四梯队是,逗号运算符。它的优先级最低结合性从左到右。逗号运算符的作用是“顺序执行”——先算左边的表达式再算右边的表达式整个表达式的值是右边表达式的值。比如a (b 1, c 2)先执行b 1再执行c 2最后把c 2的值也就是2赋给a。逗号运算符的优先级最低这意味着a 1, 2等价于(a 1), 2a的值是1整个表达式的值是2。如果你想表达a (1, 2)需要加括号。在实际编码中逗号运算符很少单独使用大多数情况下出现在for循环的初始化和更新部分。比如for (i 0, j n; i j; i, j--)这里的逗号就是逗号运算符。3. 结合性同优先级下的求值方向3.1 左结合与右结合的分界线结合性解决的是“同优先级运算符连续出现时先算哪边”的问题。C语言中大多数运算符是左结合的只有单目运算符、条件运算符、赋值运算符是右结合的。左结合的意思是“从左往右算”。比如a - b - c先算a - b再算(a - b) - c。右结合的意思是“从右往左算”。比如a b c先算b c再算a (b c)。这个分界线其实很好记所有“单目”和“赋值”类的运算符都是右结合其他都是左结合。单目运算符包括前缀、前缀--、、-、!、~、*、、sizeof、(type)。赋值运算符包括和所有复合赋值。条件运算符?:也是右结合。3.2 右结合带来的实际影响右结合在实际编码中最常见的体现就是链式赋值和嵌套条件表达式。链式赋值a b c 0因为是右结合所以先算c 0再算b (c 0)最后算a (b (c 0))。这个规则大家都很熟悉但有一个细节a b c中b c的值是赋值后b的值所以a和b最终都等于c的值。嵌套条件表达式a ? b : c ? d : e因为?:是右结合所以等价于a ? b : (c ? d : e)。这个规则在写一些紧凑的条件判断时很有用但嵌套超过两层就会严重影响可读性。单目运算符的右结合性体现在!!x、*x、- -x注意中间有空格否则会被解析成--x这些表达式中。!!x先算右边的!x再算左边的!结果是把x转换成布尔值。*x先算x取地址再算*解引用结果就是x本身。3.3 左结合中的“隐形陷阱”左结合看起来更符合直觉但也有一些容易忽略的陷阱。a / b * c是左结合等价于(a / b) * c。如果a10b3c2结果是(10/3)*2 3*2 6。如果你误以为是10/(3*2) 10/6 1那就错了。这个在写浮点运算或者整数除法的时候特别容易出问题。a b c是左结合等价于(a b) c。如果a1b2c3结果是(12)3 43 32。如果你误以为是1(23) 116 65536那就差远了。a b c是左结合等价于(a b) c。按位与满足结合律所以这个其实无所谓。但a - b - c不满足结合律左结合和右结合的结果完全不同。4. 那些年我们踩过的优先级坑4.1*p与(*p)指针操作的经典陷阱这是C语言优先级问题中出现频率最高的一个。*p等价于*(p)先取p的旧值然后p自增最后对旧值解引用。(*p)等价于先解引用p然后对p指向的值自增p本身不动。我见过一个真实的案例某开发者在写字符串遍历的时候想用*p来逐个字符处理但他在循环条件里写成了while (*p)结果每次循环p都会多移动一次导致跳过了某些字符。正确的写法应该是while (*p)然后手动p或者用for循环明确控制p的移动。还有一个类似的坑*p *q。这个表达式等价于*(p) *(q)先取p和q的旧值然后两个指针都自增最后把q旧值指向的内容赋给p旧值指向的位置。这个写法在字符串复制或者内存拷贝中很常见但如果你不理解优先级就会觉得这行代码“莫名其妙”。4.2a b c位运算与比较的优先级错位这个坑我在前面已经提过了但值得再强调一次。的优先级高于所以a b c等价于a (b c)。如果b和c都是0或1这个表达式可能还能跑但逻辑完全错了。正确的写法是(a b) c。这个括号不是可选的是必须的。类似的还有a | b c等价于a | (b c)a ^ b c等价于a ^ (b c)。只要表达式中同时出现了位运算符和比较运算符一律加括号。4.3a b c ? d : e赋值与条件的优先级关系这个表达式等价于a ((b c) ? d : e)因为?:的优先级高于。这个符合大多数人的直觉所以一般不会出错。但反过来a ? b : c d等价于(a ? b : c) d因为的优先级低于?:。这个就很容易出问题了。如果你想表达“如果a为真把d赋给b否则把d赋给c”应该写成a ? (b d) : (c d)或者直接用if-else。4.4sizeof(int) * psizeof的优先级sizeof的优先级和单目运算符同级高于算术运算符。所以sizeof(int) * p等价于(sizeof(int)) * p也就是“int类型的大小乘以p”。如果你想表达“p指向的类型的大小”应该写sizeof(*p)。还有一个常见的坑sizeof(arr) / sizeof(arr[0])。这个表达式用来计算数组长度因为sizeof的优先级高于/所以等价于(sizeof(arr)) / (sizeof(arr[0]))结果是数组元素个数。这个写法在C语言中非常常见但如果你把sizeof误认为是函数调用就会觉得sizeof(arr)和sizeof(arr[0])的优先级有问题。4.5a 2 b移位与加法的优先级的优先级高于所以a 2 b等价于a (2 b)。如果你想表达“a左移2位然后加b”应该写成(a 2) b。这个坑在嵌入式开发中特别常见因为位操作和算术运算经常混在一起。我的建议是只要表达式中同时出现了移位运算符和算术运算符一律加括号。不要试图靠记忆优先级来省括号省下来的时间不够你排查一个bug的。5. 快速判断复杂表达式求值顺序的实用方法5.1 括号法从最内层开始逐层剥离面对一个复杂的表达式最可靠的方法就是“括号法”——按照优先级从高到低逐步给子表达式加括号直到整个表达式被完全括号化。比如a b c * d 2 0xFF按照优先级从高到低*最高c * d→a b (c * d) 2 0xFF次之b (c * d)→a (b (c * d)) 2 0xFF再次(b (c * d)) 2→a ((b (c * d)) 2) 0xFF再次((b (c * d)) 2) 0xFF→a (((b (c * d)) 2) 0xFF)最后整个表达式这个方法虽然看起来笨但非常可靠。你不需要记住所有优先级只需要每次找出当前表达式中优先级最高的运算符给它加括号然后重复这个过程。5.2 查表法34种运算符的完整优先级表下面这张表把34种运算符按优先级从高到低排列同一行的运算符优先级相同结合性也相同。优先级运算符名称结合性1()[]-.函数调用、数组下标、结构体指针成员、结构体成员左结合2--后缀后缀自增、后缀自减左结合3--前缀-!~*sizeof(type)前缀自增/自减、正负号、逻辑非、按位取反、解引用、取地址、sizeof、强制类型转换右结合4*/%乘、除、取模左结合5-加、减左结合6左移、右移左结合7关系运算符左结合8!相等、不等左结合9按位与左结合10^按位异或左结合11|按位或左结合12逻辑与左结合13||逻辑或左结合14?:条件运算符右结合15-*/%^|赋值和复合赋值右结合16,逗号运算符左结合注意这张表把优先级分成了16档但有些教材把后缀/--和第一档合并有些把复合赋值拆开算所以“34种运算符”的统计口径会略有差异。关键是理解层级关系而不是纠结于具体数字。5.3 实际编码中的括号策略虽然括号法可以解决所有优先级问题但在实际编码中你不可能给每个表达式都加满括号。我的策略是只要表达式中同时出现了位运算符和比较运算符一律加括号。只要表达式中同时出现了移位运算符和算术运算符一律加括号。只要表达式中同时出现了*解引用和/--一律加括号。只要表达式中同时出现了?:和赋值运算符一律加括号。其他情况如果优先级关系符合直觉比如先乘除后加减可以不加括号。这个策略的核心思想是只在“优先级关系与直觉不符”的地方加括号。这样既能保证正确性又不会让代码被括号淹没。6. 从编译器视角看优先级语法树是怎么生成的6.1 递归下降解析中的优先级处理编译器在解析表达式的时候通常会使用“递归下降”或者“运算符优先”算法。递归下降解析器的每一层对应一个优先级高优先级的运算符在更深的递归层中处理。比如一个简化的表达式解析器可能长这样// 解析加减表达式 Expr *parse_additive() { Expr *left parse_multiplicative(); while (current_token || current_token -) { Token op current_token; advance(); Expr *right parse_multiplicative(); left make_binary_expr(op, left, right); } return left; } // 解析乘除表达式 Expr *parse_multiplicative() { Expr *left parse_unary(); while (current_token * || current_token / || current_token %) { Token op current_token; advance(); Expr *right parse_unary(); left make_binary_expr(op, left, right); } return left; }这个结构本身就体现了优先级parse_additive调用parse_multiplicative所以乘除会先被解析形成更深的语法树节点。当你写a b * c的时候解析器会先解析a然后看到调用parse_multiplicative解析b * c最后把a和b * c组合成加法节点。理解了这个机制你就能明白为什么优先级是“语法层面”的规则而不是“运行层面”的规则。编译器在生成代码之前就已经按照优先级把表达式组织成了语法树运行时的求值顺序完全由语法树决定。6.2 短路求值与优先级的关系逻辑运算符的短路求值特性也是通过语法树实现的。a b在语法树中是一个逻辑与节点左子节点是a右子节点是b。编译器在生成代码的时候会先计算a如果a为假直接跳过b的计算整个节点的值为假。这个机制依赖于优先级——的优先级高于||所以a || b c的语法树是a || (b c)如果a为真整个表达式直接为真b c根本不会被求值。如果你不理解优先级就可能写出a || b c这样的表达式然后误以为它是(a || b) c。虽然后者可以通过加括号来实现但前者的短路行为完全不同——前者在a为真时不会计算b和c后者在a为真时会计算b然后根据b的值决定是否计算c。6.3 未定义行为与优先级的关系有些优先级问题会导致未定义行为而不仅仅是逻辑错误。比如i i这个表达式在C语言中是未定义行为因为i在同一个表达式中被修改了两次而且没有序列点分隔。i i的优先级解析是i (i)先取i的旧值然后i自增最后把旧值赋给i。但问题在于赋值和自增的先后顺序在标准中没有明确规定不同的编译器可能生成不同的代码。有的编译器先自增再赋值结果i不变有的编译器先赋值再自增结果i加1。类似的还有a[i] i这个也是未定义行为。i在[]和中都被使用了而且修改了i的值导致a[i]中的i到底是旧值还是新值不确定。这类问题不是靠加括号能解决的因为问题出在“同一个变量在同一个表达式中被修改多次”这个层面。唯一的解决办法是拆成多条语句int temp i; i; a[temp] temp;7. 实战中的优先级经验从代码审查说起7.1 代码审查中最常见的优先级问题我在代码审查中见过最多的优先级问题排名第一的是a b c排名第二的是*p和(*p)的混淆排名第三的是a 2 b。这些问题有一个共同特点代码看起来“很自然”但实际行为与直觉不符。a b c看起来像是“a和b按位与的结果等于c”但实际是“a和(bc)按位与”。*p看起来像是“取p指向的值然后p自增”但实际是“p自增然后取旧值指向的值”。a 2 b看起来像是“a左移2位然后加b”但实际是“a左移(2b)位”。解决这些问题的办法不是记住所有优先级规则而是养成一个习惯只要表达式中同时出现了“容易混淆”的运算符组合就加括号。这些组合包括位运算比较、移位算术、解引用自增/自减、条件赋值。7.2 宏定义中的优先级陷阱宏定义是优先级问题的重灾区。因为宏是文本替换如果不加括号替换后的表达式可能会因为优先级问题而改变含义。比如#define SQUARE(x) x * x如果你写SQUARE(a b)替换后变成a b * a b等价于a (b * a) b而不是(a b) * (a b)。正确的写法是#define SQUARE(x) ((x) * (x))再比如#define MAX(a, b) a b ? a : b如果你写MAX(x, y) 1替换后变成x y ? x : y 1等价于x y ? x : (y 1)而不是(x y ? x : y) 1。正确的写法是#define MAX(a, b) ((a) (b) ? (a) : (b))宏定义中的括号规则是每个参数都要加括号整个表达式也要加括号。这个规则看起来简单但在实际写宏的时候很容易漏掉。7.3 位操作中的优先级实践位操作是优先级问题的高发区因为位运算符的优先级普遍低于比较运算符而大多数人直觉上认为位运算应该先算。比如你想判断一个数的第3位是否为1可能会写if (flags 0x08 0x08) { ... }这个表达式等价于flags (0x08 0x08)也就是flags 1完全错了。正确的写法是if ((flags 0x08) 0x08) { ... }再比如你想把两个标志位合并可能会写int combined flags1 | flags2 0x0F;这个表达式等价于flags1 | (flags2 0x0F)因为的优先级高于|。这个可能符合你的意图但如果你想让flags1 | flags2先算就需要加括号(flags1 | flags2) 0x0F。我的建议是在位操作中只要表达式中出现了多个运算符一律加括号。位操作的代码通常对正确性要求很高加括号的成本远低于排查bug的成本。7.4 条件表达式中的优先级实践条件运算符?:的优先级低于逻辑运算符高于赋值运算符。这个规则在实际编码中需要注意的地方是a ? b : c d等价于(a ? b : c) d而不是a ? b : (c d)。如果你想在条件表达式中做赋值应该写成a ? (b d) : (c d);或者更清晰地写成if-elseif (a) { b d; } else { c d; }条件运算符的另一个常见问题是嵌套。a ? b : c ? d : e等价于a ? b : (c ? d : e)这个规则可能符合你的意图但嵌套超过两层就会严重影响可读性。我的建议是条件运算符最多嵌套一层超过一层就用if-else。8. 把优先级规则内化成编码习惯8.1 不要试图记住所有规则34种运算符、15个优先级层级、左结合和右结合——这些规则加起来有几十条试图全部记住是不现实的。即使你记住了在写代码的时候也不一定能想起来。更实际的做法是记住那些“与直觉不符”的规则其他规则靠直觉就行。与直觉不符的规则包括位运算符的优先级低于比较运算符、^、|都低于和!移位运算符的优先级低于算术运算符、低于、-后缀/--的优先级高于单目**p等价于*(p)条件运算符的优先级高于赋值运算符a ? b : c d等价于(a ? b : c) d记住这四条就能避免90%以上的优先级问题。8.2 用括号表达意图而不是用括号规避规则加括号的目的不是“绕过优先级规则”而是“表达你的意图”。当你写(a b) c的时候你是在告诉读代码的人“我先做按位与再比较”。这个括号让代码的意图变得明确而不是让读者去查优先级表。同样当你写*p的时候如果你确实想表达“取当前值然后移动指针”这个写法是没问题的因为这是C语言的惯用法。但如果你不确定加个括号写成*(p)会更清晰。我的原则是如果一行代码需要读者查优先级表才能看懂那就加括号。代码是写给人看的不是写给编译器看的。8.3 代码审查中的优先级检查清单如果你负责代码审查可以用下面这个清单快速检查优先级问题检查项常见问题正确写法位运算比较a b c(a b) c移位算术a 2 b(a 2) b解引用自增*p确认意图必要时写(*p)条件赋值a ? b : c da ? (b d) : (c d)宏定义#define SQUARE(x) x * x#define SQUARE(x) ((x) * (x))逻辑位运算a b c确认意图必要时加括号这个清单不能覆盖所有情况但能覆盖大多数常见问题。在实际审查中只要看到这些组合就多问一句“这里的优先级是你想要的吗”8.4 从“记住规则”到“形成直觉”优先级规则最终要内化成编码直觉而不是每次写代码都去查表。形成直觉的方法只有一个多写、多读、多踩坑。我自己的经验是每次遇到优先级相关的bug不要只是改代码而是把出错的表达式和正确的表达式都记下来分析为什么出错。积累多了你就会对哪些组合容易出问题形成条件反射。比如我现在看到a b c这种表达式第一反应就是“这里少了一对括号”。看到*p第一反应就是“这是取当前值然后移动指针”。看到a 2 b第一反应就是“这里可能少了一对括号”。这种直觉不是靠背规则背出来的是靠一次次踩坑踩出来的。所以不要怕犯错但要确保同样的错误不犯第二次。9. 几个值得反复琢磨的边界案例9.1ab到底怎么解析ab这个表达式看起来很奇怪但它是合法的C语言。词法分析器会按照“最长匹配”原则来切分token所以ab被切分成a、、b等价于(a) b。如果你想表达a (b)应该写成a b中间加空格。这个例子说明词法分析阶段就已经决定了token的切分方式优先级是在token切分之后才起作用的。9.2a b, c d的求值顺序a b, c d等价于(a b), (c d)因为逗号运算符的优先级最低。先执行a b再执行c d整个表达式的值是c d的值。如果你想表达a (b, c) d这个表达式是非法的因为(b, c)不是一个左值不能出现在赋值运算符的左边。9.3sizeof和数组的优先级问题sizeof(arr) / sizeof(arr[0])这个表达式用来计算数组长度因为sizeof的优先级高于/所以等价于(sizeof(arr)) / (sizeof(arr[0]))。但如果你写sizeof(arr[0]) / sizeof(arr)结果就是1 / 数组长度通常等于0整数除法。这个错误在初学者中很常见因为sizeof看起来像函数调用容易让人误以为它的优先级和函数调用一样。9.4-和的优先级组合p-next等价于(p-next)因为-和的优先级相同结合性都是从左到右。先通过-取出next成员然后对next自增。如果你想表达p-(next)这个表达式是非法的因为next不是一个指针不能自增。如果你想表达(p-next)这个写法是正确的但通常不需要加括号因为-和的优先级关系已经决定了这个解析顺序。9.5和|的优先级关系a b | c等价于(a b) | c因为的优先级高于|。这个规则和逻辑运算符的高于||是一致的。但a | b c等价于a | (b c)因为的优先级高于|。这个规则可能和你的直觉不符——你可能觉得应该从左到右依次计算。但实际上的优先级高于|所以先算b c再算a | (b c)。这个规则在写位掩码操作的时候需要注意。如果你想表达(a | b) c必须加括号。10. 优先级规则背后的设计哲学C语言的优先级规则不是随意制定的它背后有一套设计哲学越靠近“取值”的操作优先级越高越靠近“组合”的操作优先级越低。函数调用、数组下标、成员访问——这些都是“取值”操作优先级最高。单目运算符——这些是“改造”操作优先级次之。算术运算符——这些是“计算”操作优先级再次。比较运算符——这些是“判断”操作优先级更低。逻辑运算符——这些是“组合判断”操作优先级更低。赋值运算符——这些是“存储”操作优先级更低。逗号运算符——这是“顺序执行”操作优先级最低。这个设计哲学解释了为什么位运算符的优先级低于比较运算符——因为位运算符是“计算”操作比较运算符是“判断”操作判断应该在计算之后。但这个设计也导致了a b c这样的问题因为大多数人直觉上认为位运算应该先算。理解了这个设计哲学你就不需要死记硬背优先级表了。当你遇到一个不确定的表达式时问自己这个运算符是“取值”、“改造”、“计算”、“判断”、“组合判断”、“存储”还是“顺序执行”然后按照这个顺序判断优先级。这个方法的准确率不是100%因为有些运算符的优先级设计确实有点反直觉。但它能帮你快速判断大多数情况剩下的少数情况靠加括号解决就行。11. 我个人的几条实战建议写了这么多年C语言关于运算符优先级我总结了几条实战建议都是踩坑踩出来的。第一条位运算和比较运算混用的时候永远加括号。(a b) c比a b c多打两个字符但能省你半小时的调试时间。第二条移位运算和算术运算混用的时候永远加括号。(a 2) b比a 2 b清晰得多而且不会出错。第三条宏定义中每个参数和整个表达式都要加括号。这个规则没有例外漏掉一个括号就可能出问题。第四条*p和(*p)要分清楚。如果你不确定就拆成两条语句int temp *p; p;或者(*p);。拆开写虽然多一行但可读性更好。第五条不要在一行代码里塞太多运算符。如果一行代码需要你查优先级表才能看懂那就拆成多行。代码是写给人看的不是写给编译器看的。第六条遇到不确定的表达式用括号法逐层分析。找出当前表达式中优先级最高的运算符给它加括号然后重复这个过程。这个方法虽然笨但100%可靠。第七条多读底层代码。Linux内核、嵌入式驱动、标准库源码中有大量位操作和指针操作读这些代码能帮你快速建立优先级直觉。第八条每次遇到优先级bug记下来。不要只是改代码要分析为什么出错正确的写法是什么。积累多了你就会对哪些组合容易出问题形成条件反射。这些建议看起来简单但真正做到需要时间和练习。优先级规则不是靠背出来的是靠写出来的。写得多了自然就记住了。
阅读完成 · 觉得有帮助?