ARTICLE DETAIL

建站实战干货

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

C/C++运算符优先级详解:从结合性到易错场景的实战指南

2026/9/12 2:55:31 拓冰建站 浏览量
C/C++运算符优先级详解:从结合性到易错场景的实战指南 C/C的运算符优先级问题几乎是每个初学者都会撞上的墙甚至是工作多年的老手偶尔也会被它绊一跤。我之前在调试一段图像处理代码时遇到过一个大坑一个看似简单的表达式计算出来的结果完全不符合预期排查了一下午最后发现是移位运算符和加减法的优先级搞混了。类似的经历恐怕每个C/C开发者都有过。所以这篇内容就想用大白话把运算符优先级这件事彻底说清楚不求你死记硬背整个优先级表但求看完之后你能建立起一套自己的判断逻辑知道哪里容易出问题怎么写才能让代码既正确又清晰。本文适合正在学习C或C语言的学生、刚入门想夯实基础的开发者以及写了很多年代码但偶尔还会被优先级坑一把的“老手”。不管你是刚配好VSCode的C/C开发环境正在为第一个程序运行结果困惑还是在刷算法题时老是在表达式上出错这篇文章都能给你一个相对完整的视角。1. 运算符优先级到底解决什么问题先想一个问题下面这行代码它的结果是多少int result 2 3 * 4;如果从左往右算结果是20如果先算乘法再算加法结果是14。数学老师告诉我们乘法的优先级高所以应该等于14。C语言在设计时就遵循了这套数学上的约定运算符优先级这个规则就是为了规定当一个表达式里出现多个运算符时到底先算谁。但C/C的运算符远不止加减乘除这么简单它有几十种运算符从最简单的赋值“”到诡异的指针解引用“*”、取成员“-”、逗号“,”、三目运算符“?:”、位运算“”、“|”、“^”还有各种复合赋值“”、“”。这些运算符混合在一起时执行顺序的规则就变得很复杂而这个规则就是“运算符优先级表”给出的。优先级本质上是一个分组规则它决定表达式中的各个部分怎么结合成一个整体。结合完分组之后还得靠“结合性”决定在同一个优先级层级内从左算还是从右算。这两个概念是一体两面的缺一不可。我见过很多人努力背那个二三十行的优先级表背的时候挺熟写代码的时候还是会犯错。因为真正的问题不在于你记不记得住表而在于面对一个复杂表达式时你能不能快速判断出它的实际计算顺序。这就需要一个比“死记硬背”更聪明的思路。2. 优先级的分层记忆法一张简化脑图完整的C/C优先级表非常庞大从最高到最低大概有15到18个级别。你要是去翻CppReference会看到一张巨大的表格几十种运算符密密麻麻列在那。别说初学者了我自认写了十多年C那张表也很少完整扫过。但事情其实有捷径。绝大多数真正需要你注意的优先级冲突集中在几个高频区间。按“从高到低”的顺序我把这些运算符分为几大梯队第一梯队最高后缀运算符包括函数调用()、数组下标[]、成员访问.和-、后置自增自减i、i--。第二梯队单目运算符包括前置自增自减i、--i、逻辑非!、按位取反~、正负号、-、指针解引用*、取地址、强制类型转换(type)、求字节数sizeof。第三梯队乘除取模运算*、/、%以及取余相关的运算。第四梯队加减运算、-。第五梯队移位运算、。第六梯队关系运算、、、。第七梯队相等性比较、!。第八梯队按位与。第九梯队按位异或^。第十梯队按位或|。第十一梯队逻辑与。第十二梯队逻辑或||。第十三梯队三目条件运算符?:。第十四梯队最低赋值运算、、-、*等还有逗号运算符,。这个梯队顺序比原始表粗糙但好记得多。你可以发现几个关键规律后缀运算永远最优先单目运算次之然后才是双目运算最后是赋值和逗号。双目运算内部又是算术运算符优先于移位移位优先于关系关系优先于位运算位运算优先于逻辑运算逻辑运算优先于三目和赋值。这套分层记忆法的好处是大多数人写代码时不会刻意构造几百种运算符混用的极端表达式你只需要把握住算术、移位、关系、位运算、逻辑、赋值之间的先后顺序就已经能解决90%的优先级问题。3. 结合性同一层级运算符的粘合方向优先级只能解决“不同运算符谁先算”的问题。如果是同一个优先级比如a - b - c到底是(a - b) - c还是a - (b - c)这就是结合性要管的。大多数双目运算符是左结合的也就是从左往右算。加减乘除、移位、关系、位运算、逻辑运算全部都是左结合。这跟数学里的习惯一致8 / 4 / 2在程序里等于(8 / 4) / 2 1而不是8 / (4 / 2) 4。需要特别注意的是一小撮右结合的运算符赋值运算符、、-、*、/等都是右结合。所以a b c实际执行的是a (b c)先把c赋给b再把结果赋给a。这正好能解释为什么赋值表达式可以连等。三目条件运算符?:也是右结合。a ? b : c ? d : e等价于a ? b : (c ? d : e)也就是从右往左看。前置自增自减i、--i是右结合。后置的i、i--是左结合。单目运算符!、~、*、、、-整体上是右结合的这意味着*p等价于*(p)先取地址再解引用。强制类型转换以及sizeof也可以看作右结合的单目运算符。结合性的实际意义在于当你把多个同一优先级的运算符连续写在一起时必须搞清楚粘合的方向。一个最典型的案例是文件流读取的写法while (std::cin x y) { // ... }是左结合的所以这行代码等价于(std::cin x) y先读x返回cin再读y。如果它是右结合的逻辑就会变成先读y再读x那语义就完全变了。之所以这么设计正是因为左结合能让连续读取按照从左到右的自然顺序执行。4. 最容易翻车的几类优先级组合分层记忆法可以让你对整体顺序有概念但真正容易导致bug的其实是几组特定的组合冲突。下面挑几个高频翻车点都是实战中极其常见的。4.1 移位运算符与加减法经典中的经典int x 1; int y x 2 1;很多人误以为比优先所以y应该是(x 2) 1 5。但实际上算术加减法的优先级高于移位运算符所以实际计算是x (2 1)也就是1 3 8。这个坑在嵌入式开发里特别常见因为寄存器赋值的代码经常会写成REG 1 4 | 0x0F;这里|的优先级更低所以实际上先算1 4再和0x0F进行按位或。如果打算先按位或再移位就必须加括号。我见过不止一次有人写寄存器配置时因为没加括号导致整个寄存器的值完全不对调试了半天最后发现是优先级问题。4.2 赋值运算符与逻辑运算符int a 0; int b 1; if (a b) { // 本意是 a b // 这里总会进入 }a b是赋值表达式赋值运算符的优先级极低只高于逗号运算符。这里if (a b)先把b的值赋给a然后a作为判断条件。由于b是1条件为真所以括号里的代码总是会执行。这通常是程序员笔误想要写a b但编译器可能只是给出一个警告不会报错。相比之下if ((a b) ! 0)才是真正先赋值再比较的写法这里必须显式加括号因为!的优先级高于。4.3 逻辑与短路求值的误解逻辑与和逻辑或||的优先级虽然低于位运算符但它们的执行机制中有一个非常容易被忽略的特性——短路求值。int *p NULL; if (p ! NULL *p 42) { // ... }这段代码是安全的。因为规定了先计算左边如果左边为假右边根本不会执行。所以即使p是NULL也不会真的去解引用它。许多人一开始听说优先级高就以为整个表达式会被一次性算完但这里恰恰是优先级和求值顺序的另一个维度问题了。另一个常见例子int x 0; if (1 || x) { // 短路x不会执行x还是0 }这里需要注意的是虽然逻辑或的优先级比较低但短路规则仍然生效。换句话说优先级决定的是“哪些部分是操作数”而短路规则决定的是“程序会不会进入那一部分去计算”。这是不少考生面试时会踩的坑。4.4 三目运算符的粘连问题三目运算符是少数几个“低优先级”但使用频率不低的运算符。它比赋值高但比其他几乎所有运算符都低。int x 3; int y 2; printf(%d\n, x y ? x : y 2);这里y 2会先被算出来因为优先级高于?:。但如果写成x y ? x : y 1 ? y : x那就得靠“右结合”来理解了等价于x y ? x : (y 1 ? y : x)。在代码里滥用三目运算符嵌套可读性是非常差的。我曾经查过一段同事留下的代码五六个三目运算符连在一起加上优先级规则肉眼根本看不出对应关系。后来我的原则很简单三目运算符只用于单层简单判断一旦要嵌套就直接改if-else。4.5 逗号运算符被遗忘的极低优先级逗号运算符的优先级是所有运算符中最低的比赋值还低。这意味着int a 1, b 2; int result (a b, a * b); // 结果为2吗不整体先算逗号右边结果是2解释一下(a b, a * b)是先算a b丢弃结果然后算a * b把右边的结果作为整个表达式的值。所以result等于2。但更常见的是在循环里for (int i 0, j 10; i j; i, --j) { // ... }这里的逗号不是逗号运算符而是逗号分隔符它把变量声明和表达式列表分隔开来。很多初学者在这点上是懵的同样是逗号在声明里和在表达式里语义截然不同。理解了这一点再看代码就不会犯迷糊。5. 指针操作相关的优先级细节指针是C/C的重头戏而指针操作符*、、-、[]与自增自减混在一起时的优先级问题是无数人的噩梦。这部分的优先级规则非常反直觉值得单独拉出来讲。5.1 后缀运算符高于单目运算符int arr[5] {10, 20, 30, 40, 50}; int *p arr; int x *p; // 等同于 *(p)而不是 (*p)p是后缀自增它的优先级高于单目解引用*。所以*p先取出*p的值也就是10然后将p指针向后移动一位。结果是x的值是10p指向arr[1]。这就可以用来顺序遍历数组。如果写成(*p)那意思是取出p指向的值并让这个值自增。区别非常明显。前者移动指针后者修改数据。只差一个括号语义天差地别。5.2 数组下标与指针运算int arr[5]; int *p arr; p[2] 42;[]运算符的优先级和函数调用一样高所以p[2]是先进行下标运算。C/C里下标运算本质上就是*(p 2)。这里的优先级没有坑但有一点需要提醒2[p]在C/C里也是合法的它和p[2]等价。因为下标运算的定义就是*( (p) (2) )加法的交换性保证了两种写法一样。但这种写法可读性太差了看到别人这么写大概率是在炫技或者写混淆代码你自己千万别学。5.3 解引用与成员访问的经典组合struct Node { int value; Node *next; }; Node *node ...; int v *node.value; // 错误无法编译这里的问题在于.成员访问运算符的优先级高于单目*。所以*node.value被解析为*(node.value)而node是一个指针指针没有.value成员严格来说需要先用(*node).value因此编译器报错。正确写法是int v (*node).value; // 或者更简洁 int v node-value;-运算符的优先级同样很高。所以如果你写*node-value它解析为*(node-value)也就是先取value成员再解引用当value是指针时。这在链表、树等结构中非常常见。5.4 后置自增与多重解引用int a 100; int *p a; int **pp p; int x *pp; // 实际上是 *(pp)先取pp指向的p的地址里的值然后pp指针后移 int y **pp; // 第二个取值这里最容易迷糊的是*pp中到底哪个部分先执行。由于优先级高于*所以pp先作为整体得到pp旧值然后进行解引用。也就是先获取*pp即p的值也就是a的地址然后pp指针向后移动。至于a的值需要再解引用一次**pp。很多人背了优先级表到这里还是会懵原因在于“先算pp”和“先取值”之间的时序关系容易搞混。实际上对于后缀自增表达式的值是自增前的值副作用是之后才发生。所以*pp在读取*pp的时候pp还没有真正后移但表达式结果已经确定后pp会立即后移。这种语义上的微妙差别是理解指针遍历代码的关键。6. 编写不易出错的表达式几条硬性纪律讲了这么多优先级规则其实我在实际写代码时并不会依赖自己去精确记忆整张表而是靠一套“加括号”的纪律。这里分享几条我自己遵守多年的原则6.1 涉及混合运算时不要吝啬括号// 不推荐的写法 if (a b c) { ... } // 推荐的写法 if ((a b) c) { ... }括号不会影响性能编译器优化之后生成的指令是完全一样的。但括号极大提高了可读性也杜绝了优先级误判。每当你自己都不确定优先级时加括号当你在review代码时看到别人没加括号的多运算符混合表达式先假定可能是bug再仔细对照优先级表。6.2 判断条件里赋值必须显式加括号if ((fd open(path, O_RDONLY)) 0) { perror(open); return -1; }这里fd open(...)必须括起来否则的优先级高于表达式会被解析为fd (open(...) 0)把比较结果赋给fd逻辑上完全错误。这成了一种约定俗成的写法看到if里有赋值并伴随比较就必须确保赋值被括号包裹。6.3 位运算和逻辑运算混合时保持清晰// 不推荐运算符混用且不分组 if (x 0xFF 0x80) { ... } // 推荐 if ((x 0xFF) 0x80) { ... }为什么大家老是写错因为优先级高于。如果不加括号编译器把表达式看作x (0xFF 0x80)先计算0xFF 0x80得到0或1再用x与它做按位与这极大概率不是你想要的。这类错误极难排查因为它能编译通过逻辑上看不出来只有跑到边界条件时才暴露。6.4 移位运算参与复合表达式时至少加一层括号// 不推荐 uint32_t val 1 8 1; // 推荐 uint32_t val (1 8) 1;移位运算的优先级比算术加减法低这有点反直觉因为很多人觉得“移位”是一种算术行为自然而然地覺得它跟乘除同级。正是这种直觉误区导致移位和加减混用在真实项目里高频踩坑。我在代码审查时看到移位运算旁边有加减法第一反应就是去确认有没有括号。6.5 不建议在表达式中使用逗号运算符除非是for循环的迭代语句否则不要用逗号运算符。它太不直观了读者必须回忆“逗号优先级最低”这条规则才能理解代码。而且它的作用基本都能用分开的语句替代。写出来的代码是为了被人读懂不是为了在混淆大赛里拿名次。7. 前置与后置自增自减的优先级陷阱自增自减运算符因为能和指针、数组、函数调用混在一起是另一个高频翻车区。很多人以为理解了i和i的区别就万事大吉结果实际代码里还是栽在这上面。究其原因还是没把“后缀运算高于单目”和“副作用发生时机”这两条吃透。来看一个最常见的错误int arr[10]; int i 0; while (i 10) { arr[i] i; }这段代码的本意是给数组赋arr[i] i然后i自增。但由于i的副作用arr[i] i实际计算时右侧的i返回的是自增前的值同时i已经变成i1。这里真正的问题在于这行代码到底先取左侧的i还是先执行右侧的i在C/C标准里属于未指定顺序unspecified behavior不同编译器可能给出不同结果。你无法确定是arr[0] 0; arr[1] 1; ...还是arr[1] 0; arr[2] 1; ...。这种“某个变量在同一表达式里既被读取又被修改且没有序列点分隔”的写法在C和C中都是未定义行为必须避免。类似的未定义行为还有int i 0; f(i, i); // 参数求值顺序未定义 int j (i) (i); // 未定义行为 int k i i; // 未定义行为这里要特别强调一句这不是“优先级能解决”的问题而是标准里故意留白让编译器自行决定。所以即使你背熟了优先级表也无法推断出正确结果因为压根没有正确结果。正确的做法就是不要在同一条表达式里对同一个变量做多次带副作用的修改实在需要就先分开写。8. sizeof、强制类型转换与优先级sizeof和强制类型转换都属于“单目运算符”优先级很高但仍然有需要注意的地方。先看sizeofint x 10; size_t size sizeof x 1; // 是 (sizeof x) 1而不是 sizeof (x 1)这行代码如果理解为sizeof (x 1)结果会是sizeof(int)也就是4实际运行时由于优先级低于sizeof表达式的值其实是sizeof x 1 4 1 5。只有当操作数本身是类型时必须加括号size_t size sizeof(int); // 合法 size_t size sizeof int; // 非法编译错误可见对于类型操作数括号是必须的。对于表达式操作数sizeof会先对表达式求值准确地说是确定类型不求值再返回类型大小。再看强制类型转换的优先级int *p (int*)malloc(sizeof(int) * 10);这里(int*)作用于紧随其后的malloc(...)因为函数调用的优先级高于强制类型转换整个malloc(...)结果被转换。这没问题。但如果是char c (char)some_int 1;这里强制类型转换优先于所以先把some_int转为char然后再加1。如果初衷是先加1再转char应该写成(char)(some_int 1)。这类优先级问题在类型转换中很容易被忽视因为人眼扫过去觉得(char)在开头应该作用于整个表达式。但C/C的规则是强制类型转换只作用于紧随其后的单个表达式单元而普通算术运算符的优先级低于它。9. 编译器警告与静态检查工具你最好的优先级老师前面讲的都是“怎么理解规则”但人总有疏忽的时候需要靠工具兜底。现代编译器其实对很多优先级相关的疑似错误都有警告只是默认没开或没设成错误。这里分享我的几个常用配置。9.1 GCC/Clang的-WparenthesesGCC和Clang都有-Wparentheses选项它会针对一些常见的、几乎肯定是错误的优先级用法给出警告。最经典的就是if (a b c) // 警告建议加括号 if (a b 1) // 警告开启方式gcc -Wall -Wextra -Wparentheses source.c-Wall里已经包含了-Wparentheses所以你只要保证编译时带-Wall这类问题通常就会被捕捉到。而在Clang里额外推荐开启-Wdocumentation和-Wgnu等但起码-Wall别省。在CMake工程里可以这样设置if(CMAKE_C_COMPILER_ID MATCHES GNU|Clang) add_compile_options(-Wall -Wextra -Wpedantic) endif()9.2 Visual Studio的C4519警告MSVC也有对应的警告机制。对于某些优先级歧义写法编译时会输出例如“warning C4550: expression has no effect”或者更常见的C4519“default arguments are only allowed on function declarations”。VS对括号相关的建议在启用“Microsoft Code Analysis”后会更明显一般用默认的/W3或/W4也能覆盖大多数情况。9.3 静态分析工具的强提醒编译警告之外Clang-Tidy和Cppcheck这类静态分析工具对优先级的检查更加激进。Clang-Tidy有一个检查项叫bugprone-parent-virtual针对析构函数和readability-misleading-indentation但它最适合的是解决可疑表达式。Cppcheck对可疑的位运算优先级也会报warning。把这些工具集成到CI或者编辑器保存时自动运行等于让代码在合并之前多了一轮自动review。我自己在VSCode里配置C/C开发环境时除了安装C/C插件还会启用Clang-Tidy的实时检查。这样在写代码的过程中如果有可疑优先级写法编辑器里直接就有黄色波浪线提示根本不用等到编译阶段才发现。9.4 编译器的优化对未定义行为的影响优先级判错和未定义行为混在一起时是最难排查的。现代编译器在开启O2/O3优化后对未定义行为代码的处理可能非常“激进”它会假设你的代码没有未定义行为然后基于这个假设做优化导致结果出乎意料。例如前面提到的arr[i] i在Debug模式下可能输出一种结果在Release模式下又变成另一种结果甚至直接崩溃。原因就是编译器在对未定义行为做不同处理。所以如果你遇到“同一个代码开优化就跑错不开优化就对”除了检查数组越界、指针空引用之外一定要多想一步是不是表达式里有未定义行为10. 面试题和考试里的优先级考点因为优先级问题太容易考笔试面试里几乎成了保留节目。整理几个高频考点看看你能不能一眼说出答案。int a 5, b 3, c 2; int r1 a b ? a : b 1; // 先算 b14再判断 ab 为真取 a5 int r2 a 1 b; // 优先于 所以是 a (13) 5 4 0 int r3 a 0xFF 0x05; // 优先于 所以是 a (0xFF 0x05) 5 0 0 int r4 a || b c; // 优先于 ||所以是 a || (b c) 1 int r5 a b * c; // 先算 b*c6再 a6得11 int r6 a, b, a; // 逗号表达式从左到右最终a7b4这些题看起来绕其实用上前面说的分层记忆法几秒钟就能判断出来。前提是别跟“结合性”混淆因为有些时候两个运算符优先级不同但结合性起的作用也不一样。再给一个C里常见的面试题std::cout (1 2 3) std::endl; // 输出32因为先算235再算1532 std::cout (1 2 | 3) std::endl; // 先移位得到4再和3按位或得到7如果对优先级掌握不牢这两题很容易答反。面试官问这类问题的目的往往是想看你是否能识别“混合表达式中的次序问题”同时也看你会不会主动提出“实际工程中我会加括号来避免歧义”。后者其实比标准答案更重要因为这体现了一个程序员的工程素养。11. 不同场景下的优先级实践建议优先级规则是死的但应用场景是活的。在不同的开发场景里对优先级的关注重点完全不同。这里按场景做一个总结方便你对照自己的情况来查缺补漏。11.1 算法竞赛与刷题场景在竞赛代码里追求的是短、快、准。很多人习惯写出很长的表达式比如int ans (a % mod b % mod c % mod) % mod;这类代码括号很多看起来冗余但不会出错。竞赛选手一般会把容易出错的位运算、取模运算尽量用括号隔离。如果看到有人写x 1 1这多半是没分清优先级正确参与判断应该是(x 1) 1。在这种高速编码场景下我的经验是永远把“取模”和“位运算”的结果用括号包起来。这不是胆小而是当你处理大量数据时一个优先级错误会导致整道题Wrong Answer而调试这类WA往往比写代码本身还费时间。11.2 嵌入式与驱动开发场景嵌入式代码高度依赖位操作和寄存器读写优先级错误直接跟硬件行为挂钩了。配置寄存器时:REG_CTRL | (0x3 4) MASK;这种写法能让意图非常清晰先把0x3移到bit4位置再与MASK按位与最后跟原有寄存器值做或操作。如果少了一层括号极可能产生错误寄存器值硬件行为随之异常而这种异常往往非常难复现和定位。另外嵌入式里还经常用#define定义寄存器位的宏#define BIT_MASK(n) (1 (n)) #define SET_BITS(reg, mask) ((reg) | (mask))宏定义里大量使用括号不仅是为了防优先级还为了防宏参数展开后的优先级问题。你在写这类宏时任何参数出现的地方都应该加一层括号这是嵌入式C开发的基本素养。11.3 大型项目代码审查场景在团队协作中代码可读性和可维护性比个人炫技重要得多。我自己做代码审查时看到多运算符混合的表达式第一件事就是问作者这一层有没有加括号如果没加我会建议补上即便它的优先级其实是正确的。理由很简单优先级可能不会写错但阅读代码的人不一定每次都记得整张表。代码不仅要给编译器看更重要的是给人看。一个多小时后你可能忘了当时为什么这么写一个括号能省掉太多猜疑链。11.4 泛型模板和现代C场景C模板代码里运算符优先级更是一个“看不见的杀手”。刚学模板的人写SFINAE或类型萃取时常常被::和*的优先级搞晕template typename T struct IsPointer { static const bool value false; }; template typename T struct IsPointerT* { static const bool value true; };这里T*的星号是类型声明的一部分不涉及运行时优先级但如果你把is_pointer的判断写在表达式中std::is_pointerT::value true // 没问题因为::优先级极高::是作用域解析运算符它在优先级表里跟后缀运算符同级非常高。所以std::is_pointerT::value能作为一个整体被解析然后再与true比较。如果你不了解这一点看到这行代码可能会疑惑为什么::不需要括号。现代C里我们更推荐直接使用if constexpr和auto来规避复杂的模板表达式if constexpr (std::is_pointer_vT) { // ... }这类写法简洁也不存在优先级打架的问题。12. 到底应不应该死记优先级表到了这里一个很自然的问题浮现了既然优先级这么容易坑人那是不是应该把整张表背下来我的看法是不需要也不建议。完整优先级表有几十条你就算背下来了写代码时也不可能每次都逐条回忆。更重要的是优先级表里的很多组合在你日常开发中根本不会遇到记住它们纯粹是浪费脑容量。你需要的是建立前面说的“分层直觉”加上“重要组合的特殊记忆”以及“不确定就加括号”的习惯。这三条合起来已经足以保证你不会写出歧义代码。分层直觉怎么培养我的建议是找几张典型的优先级陷阱题自己先做一遍再对照编译器实际输出和汇编结果感受一下优先级对生成代码的影响。比如int f(void) { int x 1; int y x 2 1; return y; }编译后看汇编你会发现它先做了加法再移位。这个“看汇编”的过程能帮你把抽象的优先级规则具象化比单纯读十遍优先级表都有效。另外还可以利用隐式转换和编译警告来反向验证自己的判断。当编译器给出警告时不要简单改掉报警的地方就完事要停下来想一想为什么这里会有警告是不是我的优先级直觉出了偏差这种反思式的学习一次比十次死记硬背都有价值。13. 一个综合实战案例的拆解最后用一个综合性例子把前面讲到的知识点串起来。假设你要写一个函数判断一个IPv4地址字符串是否是本机回环地址127.0.0.1。当然实际工程里会用正则或现成库这里主要是为了演示混合表达式#include stdio.h #include stdint.h #include string.h int is_loopback(const char *ip) { int a, b, c, d; if (sscanf(ip, %d.%d.%d.%d, a, b, c, d) ! 4) { return 0; } // 这里涉及多个运算符、、移位、按位或 uint32_t addr ((uint32_t)a 24) | ((uint32_t)b 16) | ((uint32_t)c 8) | ((uint32_t)d); return (addr 0xFF000000) 0x7F000000; }这个函数里优先于|但我不依赖这个规则而是直接把移位结果用括号包住。最后一行(addr 0xFF000000) 0x7F000000也加了括号包住因为优先级高于。如果忘了括号addr 0xFF000000 0x7F000000会变成addr (0xFF000000 0x7F000000)后面的比较结果恒为0表达式恒为0函数永远返回0。这种bug无论是静态审查还是动态调试都非常难第一时间发现。再看一个更容易被忽视的点return (addr 0xFF000000) 0x7F000000 ? 1 : 0;这里优先级高于?:所以整个比较结果会成为三目运算的判断条件。这个写法其实没问题但有些人会想当然地把?:的优先级看高误以为比较结果会被扩进去。为了防止误读更稳妥的写法是return ((addr 0xFF000000) 0x7F000000) ? 1 : 0;加上一层外层括号意图一目了然。这就是运算符优先级在实践中最真实的体现它不总能让你写出错了的代码有时优先级恰好符合直觉但它总是能让你写出可读性差的代码。而我们的目标是让代码在“正确”和“可读”两个维度上都站得住脚。如果你看完了这篇我建议你做一件事回到你最近写过的代码里全局搜索一下有没有混合使用了、、、、|、这些运算符的长表达式。如果有逐个加上括号。做完这一步你对运算符优先级的理解就已经超过大多数“背了表还是忘”的程序员了。