前言C 语言的运算符有四十多个优先级和结合性决定了表达式的解析方式。记不住优先级并不丢人专业代码的通行做法是「拿不准就加括号」但读别人的代码、排查一个诡异的判断条件时能快速判断a b c到底怎么结合仍然很有用。常见的误解有三个。第一把优先级当成求值顺序——优先级只决定「谁和谁结合」不决定「谁先算」。f() g()中两个函数谁先调用标准没有规定。第二以为逻辑运算符和位运算符的优先级相邻——的优先级远低于||又低于中间隔着好几层。第三以为赋值运算符优先级很高——它几乎垫底只比逗号高所以if (a b)是合法的但含义是赋值而不是比较。本文先给出完整的优先级表再给一段口诀然后逐个讲解最容易出错的几组运算符最后区分「优先级」「结合性」「求值顺序」三个概念。一、优先级汇总表下表按优先级从高到低排列同一行的运算符优先级相同。C11 为准。级别运算符含义结合性1()[].---后缀函数调用、下标、成员、后缀自增自减左2-!~--前缀*sizeof(类型)一元运算、取址、解引用、强制转换右3*/%乘、除、取余左4-加、减左5左移、右移左6关系比较左7!相等、不等左8按位与左9^按位异或左10\按位或11逻辑与左12\\13?:条件运算符右14-*/%^\赋值与复合赋值15,逗号左表里的\|是为了在 Markdown 表格中显示竖线而做的转义实际代码里写|。几个整体规律后缀高于前缀前缀高于算术算术高于移位移位高于关系关系高于相等相等高于按位与/异或/或位运算高于逻辑与/或逻辑高于条件条件高于赋值逗号最低。一元运算符和赋值运算符是右结合其余大多数是左结合。位运算符的优先级低于相等比较这是最容易踩的坑a b c解析成a (b c)不是(a b) c。二、口诀把上面的层级压成八句按顺序背括号后缀先单目紧相连乘除再加减移位跟在后关系与相等位与异或或逻辑与或分条件接赋值逗号最后沉。逐句对应「括号后缀先」()、[]、.、-、后缀/--。「单目紧相连」前缀/--、!、~、一元/-、*、、sizeof、强制类型转换。「乘除再加减」* / %高于 -。「移位跟在后」 低于加减。「关系与相等」 高于 !。「位与异或或」、^、|依次降低。「逻辑与或分」高于||。「条件接赋值」?:高于赋值。「逗号最后沉」,最低。口诀的作用是快速回忆顺序不是替代括号。实际写代码时只要一个表达式里混用了不同层级的运算符就应当加括号把意图写死既避免错误也让读代码的人不必查表。三、最容易出错的几组位运算与相等比较/* 适用于 C11 */int a 5, b 3, c 1;int x a b c; /* 解析为 a (b c)即 5 0结果是 0 */int y (a b) c; /* 先按位与再比较结果是 1 */的优先级高于所以不加括号时先比较、后按位与。这类错误在判断「某一位是否等于某个值」时特别常见。正确写法是把位运算整体括起来/* 适用于 C11 */if ((flags MASK) MASK) {/* 所有掩码位都置位 */}移位与加减/* 适用于 C11 */int a 1 2 3; /* 解析为 1 (2 3)结果是 32 */int b (1 2) 3; /* 结果是 7 */加法优先级高于移位。想把「1 左移 2 位再加 3」必须加括号。反过来x 1 1也不是「先移位再减」而是x 2。赋值与比较/* 适用于 C11 */int a 1, b 2;if (a b) {/* 把 b 赋给 a条件值为 2恒为真 */}if (a b) {/* 比较 */}赋值优先级低于关系与相等所以if (a b)能编译只是语义不是比较。现代编译器通常会对「条件里出现赋值」给出警告但不要依赖警告。把常量写在左边if (2 a)是一种防御写法写漏等号时会变成对常量赋值编译报错不过这种写法牺牲了可读性更推荐开启编译器的-Wparentheses一类警告。解引用与自增/* 适用于 C11 */int arr[3] {10, 20, 30};int* p arr;int v *p; /* 解析为 *(p)取当前指向的值然后 p 后移 */后缀的优先级高于一元*所以*p是「先取指针指向的值再移动指针」不是「对值自增」。想对值自增要写(*p)。这两种写法在遍历数组时语义完全不同务必分清。条件运算符与赋值/* 适用于 C11 */int a 1, b 2, c 3;int x a b ? a : b; /* 正确取较大值 *//* a b ? a : b c; */ /* 错误解析为 (a b ? a : b) c左值不合法 */?:的优先级高于所以条件表达式整体会先结合再尝试赋值。真要在分支里赋值应当写成a b ? (a c) : (b c);或者直接用if。逗号运算符逗号是优先级最低的运算符。a 1, b 2;先执行a 1再执行b 2整个表达式的值是 2。但在函数实参里逗号是分隔符不是逗号运算符/* 适用于 C11 */int m (a 1, b 2); /* 括号里是逗号表达式m 为 2 *//* f(a 1, b 2); */ /* 这里是两个实参不是逗号表达式 */函数调用括号里的逗号永远分隔参数除非再套一层括号。这一点在宏展开时尤其容易出问题。四、优先级不等于求值顺序这是必须单独强调的一点。优先级和结合性只回答「表达式如何分组」不回答「哪一部分先求值」。/* 适用于 C11 */int f(void);int g(void);int r f() g(); /* f 和 g 谁先调用标准未规定 */的操作数求值顺序是未指定的unspecified编译器可以先调用f也可以先调用g。如果两个函数都有副作用程序的结果可能随编译器或优化级别变化。这不是未定义行为但也不是可移植的确定行为。真正由标准规定顺序的只有少数几个位置、||、?:的条件与分支、逗号运算符以及函数调用前的序列点。和||还保证短路左边确定结果后右边不求值。最危险的是「同一对象在未排序的多个位置被修改」/* 适用于 C11行为未定义 */int i 0;i i 1;i在同一个表达式中被修改多次且这些修改之间没有顺序属于未定义行为undefined behavior, UB。不要用「先算右边」「后算左边」去解释它标准没有给出任何保证。五、实践建议混合使用位运算与比较时把位运算括起来(x mask) value。移位表达式里有加减时加括号1 (n 1)。条件里不要写赋值如果确实要赋值再判断拆成两条语句。指针解引用与自增混用时加括号(*p)或*(p)按意图选择。条件运算符嵌套超过一层就加括号或改if。不要依赖实参或操作数的求值顺序有副作用的表达式拆开写。常见坑点1. 位与和相等比较❌if (flags MASK MASK)—— 解析为flags (MASK MASK)。 ✅if ((flags MASK) MASK)。2. 移位和加法❌1 2 3以为等于(1 2) 3。 ✅ 实际是1 5要前者就写(1 2) 3。3. 条件里写赋值❌if (a b)—— 赋值表达式的值作为条件。 ✅if (a b)需要赋值时拆成a b; if (a) ...。4. 解引用与自增混淆❌*p以为是对指向的值加一。 ✅ 它是*(p)对值加一写(*p)。5. 逻辑与和位与混用❌if (a b c)——优先级高于先算b c。 ✅ 明确意图if (a ((b c) ! 0))。6. 条件运算符当左值❌cond ? a : b 1;—— 解析为(cond ? a : b) 1左值不合法。 ✅cond ? (a 1) : (b 1);或改用if。7. 以为优先级决定求值顺序❌ 认为f() g() * h()中g()一定先于f()调用。 ✅ 优先级只决定分组操作数求值顺序未指定有副作用就拆开。8. 同一表达式多次修改同一变量❌i i 1;—— 未定义行为。 ✅ 拆成多条语句保证每条只做一次修改。总结层级运算符记忆要点高后缀、单目括号、下标、成员、自增自减、取址解引用中高乘除、加减、移位算术先于移位中关系、相等比较先于位运算中低位与、异或、或三者依次降低低逻辑与、或高于 很低条件、赋值赋值右结合最低逗号只在需要时使用优先级表值得背个大概口诀可以帮助回忆顺序但真正可靠的做法是加括号。位运算与比较、移位与加减、解引用与自增这三组是错误高发区而「优先级不决定求值顺序」这一点比记错某一级更容易导致难以复现的 bug。把表达式写简单、把副作用拆开比背熟整张表更有价值。
阅读完成 · 觉得有帮助?