ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解运算符优先级与结合性:分类、陷阱与跨语言差异

深入理解运算符优先级与结合性:分类、陷阱与跨语言差异 1. 别急着背优先级表先搞清楚运算符是怎么被分成三类的1.1 操作数是什么为什么它是分类标准先从一个最基础的问题说起什么叫运算符简单说运算符就是告诉编译器或解释器对数据做某种操作的符号。加号、减号、乘号、比较大小、判断相等、取反、赋值这些都算运算符。那为什么要用操作数个数来给运算符分类因为这是理解运算符之间关系的第一把钥匙。操作数就是运算符作用的对象。比如说a b这里的a和b就是操作数是运算符。一个运算符需要几个操作数直接决定了它长什么样、怎么写、优先级怎么排。你去看任何一门编程语言的语法文档里面都会把运算符按目数arity分类一元unary运算符操作一个数二元binary运算符操作两个数三元ternary运算符操作三个数。这个分类不是教科书为了凑知识点硬造的而是编译器在解析表达式时真实遵循的规则——它决定了表达式怎么被读进语法树。很多人学运算符喜欢先背优先级表结果背了三天发现还是不会写复杂的表达式。我的建议是反过来先搞清楚运算符的目数再去理解优先级和结合性。因为优先级表再长本质上就是在回答一个问题当一个表达式中同时出现多个运算符时它们到底按什么顺序被组织起来而这个问题的前提是你得先知道每个运算符到底要吃掉几个操作数。1.2 一元运算符操作数只有一个但作用不容小觑一元运算符是编程语言里最专一的一类它只作用于一个操作数。常见的一元运算符包括负号-x、逻辑非!x、按位取反~x、自增i、自减--i、取地址x、解引用*p以及一些语言里特有的类型转换(int)x、求长度sizeof(x)、取类型typeof x等。为什么我要强调一元运算符作用不小因为它在表达式中的位置和绑定关系非常容易引起误解。举个例子-2 * 3的结果是多少直觉上很多人会说是-6答案没错但关键的问题是这个表达式是先算-2再乘3还是先算2 * 3再取负绝大多数语言中一元负号的优先级高于乘法所以它等价于(-2) * 3。但在某些语言里你会遇到完全相反的坑。Python 中-2 ** 2的结果是-4因为**幂运算符的优先级高于取负而数学上-2²也确实是-4这个是一致的但如果你在代码里写2 ** -1想表达2的-1次方Python 会直接报语法错误因为幂运算符的优先级高于一元负号它根本没法把-1当成操作数——这种细节就是运算符关系最常出问题的地方。还有个特别容易忽略的点自增和自减--也属于一元运算符但它们还分前缀和后缀两种形式。前缀i是先加后用后缀i是先用了再加这在单独一行使用的时候没什么区别一旦嵌进复杂表达式里就成了重灾区。我见过不少新人调试到崩溃就是因为把arr[i] i这类表达式当成了理所当然的顺序。后面我会专门展开讲这部分坑。1.3 二元运算符编程里的绝对主力二元运算符是使用频率最高的一类几乎所有语言的运算符表里二元运算符都占绝大多数。它们可以按功能分成几大族算术运算符、-、*、/、%关系运算符、、、、、!逻辑运算符、||位运算符、|、^、、赋值运算符、、-、*、/等移位运算符、空值合并运算符??、成员访问运算符等视语言而定为什么会存在这么多种二元运算符根本原因是程序里大量操作本身就是两个对象之间的关系两个数相加、两个值比较、两个条件做逻辑运算、把一个值赋给变量……二元结构是最自然的表达方式。理解了这一点你就知道为什么大多数运算符都是二元的了——因为它们要表达的语义天然就需要两个参与方。这里有个容易掉进去的误区很多初学者以为运算符优先级只看它是什么运算比如觉得乘法就是比加法高。这话对常见四则运算成立但换成位运算、逻辑运算、赋值运算时死记硬背很容易记串。比如x y 0这个表达式在 C 语言里它的求值顺序是先算y 0再算x (y 0)因为在 C 语言中的优先级高于按位与。这跟很多人的直觉完全相反——你可能会想位运算难道不应该比比较运算更紧密吗答案是不一定。所以我才说理解运算符之间的关系不能靠直觉得从每种运算符的优先级级别入手而优先级值的设定其实跟语言设计者的历史包袱和语义考虑都有关系。1.4 三元运算符为什么它独一无二三元运算符在几乎所有主流语言里都只有一个条件运算符? :写法是condition ? value1 : value2。它接收三个操作数一个条件表达式、一个为真时的取值、一个为假时的取值。这个运算符之所以特殊不仅因为它三目更因为它本身是表达式可以嵌套在别的表达式里也可以作为函数参数传递。很多初学者第一次看到result a b ? a : b时会觉得它不过是个简化的 if-else。但实际上条件运算符和 if-else 有个本质区别if-else是语句statement不产生值三元运算符是表达式expression它会产生值。这意味着你可以把三元运算符塞进赋值语句、函数调用、甚至另一个三元运算符里。这个区别在 Rust、Kotlin 这类现代语言里被进一步放大——Rust 直接用 if-else 表达式取代了传统三元运算符但本质思路一样表达式产生值。不过三元运算符虽然强大我却建议你在实际项目里慎用它的嵌套形式。a b ? (c d ? e : f) : (g h ? i : j)这种写法嵌套到第二三层以后人的眼睛基本就分不清谁跟谁配对了。这其实引出了一个重要的原则运算符本身越简洁组合起来越要谨慎。优先级规则帮你解决了编译器怎么读的问题但解决不了人怎么读的问题。一个表达式如果连你自己都要凑近屏幕数括号那它该拆成多行了。分类操作数个数典型运算符判断要点一元运算符1!x、-x、i、~x操作数在符号的一侧前缀或两侧后缀二元运算符2a b、x y、k v操作数分别在符号两侧三元运算符3c ? a : b三个操作数由?和:分隔2. 优先级和结合性表达式求值顺序绕不开的两个核心2.1 优先级管的到底是什么很多人对优先级的理解是优先级高的先执行这话不完全对。更准确的说法是优先级决定的是运算符之间怎么结合也就是先形成哪个子表达式。它并不直接决定哪个运算先算完而是决定表达式在语法结构上如何被切分。拿经典表达式a b * c来说因为*的优先级高于所以语法树会先把b * c绑在一起变成a (b * c)然后才做加法。你说这句先执行乘法再执行加法也没毛病但一旦引入函数调用、赋值、副作用这些因素先形成哪个子表达式和先执行哪一步就会出现本质区别。比如f() g() * h()优先级决定了语法结构是f() (g() * h())但f()、g()、h()这三个函数在 C 里谁先被调用优先级管不了那是求值顺序evaluation order的问题。很多人就是在这里翻车的——把优先级当成唯一的求值顺序依据结果在莫名其妙的地方看到不确定行为。还有一点需要明确括号永远是最强的优先级。(a b) * c中括号硬生生把优先级较低的加法提到了前面。但括号并不是运算符它只是改变结合方式的分组工具。理解了括号的作用你就知道为什么很多大牛的代码里宁可多写括号也不依赖冷门优先级——严格遵守优先级确实是编译器的责任但让人一眼看懂优先级是写代码的人的责任。2.2 结合性同一优先级运算符连续出现时从哪里开始算优先级解决的是不同运算符之间的尊卑问题那同一个优先级里呢就要靠结合性associativity了。结合性分两种左结合left-to-right和右结合right-to-left。左结合的典型例子是加减法a - b - c等价于(a - b) - c也就是从左往右算。如果换成右结合那a - b - c就变成a - (b - c)了数学意义完全不同。右结合的典型例子是赋值运算符a b c等价于a (b c)也就是说先把c赋给b再把b的值赋给a。这个设计是合理的因为赋值本身就是把右侧的值往左侧变量里塞如果从左往右(a b) c在大多数语言里根本说不通。结合性里最容易出问题的是混在一起的场景。C、C、Java、C# 的条件运算符?:就是右结合的所以a ? b : c ? d : e等价于a ? b : (c ? d : e)。这个本来没什么但一旦你去读别人写的这种嵌套三元表达式第一眼基本反应不过来。还有赋值运算复合形式a b和普通赋值一样是右结合a b c等价于a (b c)这个顺着解释也能通。真正让我觉得需要特别记住的是和移位运算在大多数 C 系语言中是左结合但在 Python 里也是左结合所以1 2 3会先算2 3因为的优先级高于跟结合性反而没关系——你看优先级、结合性、操作数个数这三者经常纠缠在一起缺一个就理解不了完整图景。2.3 一张不求全但求用的优先级速查思路我不建议你把各种运算符优先级表背得滚瓜烂熟因为每个语言都略有差异背错一个反而更糟。但有一个通用层级是绝大多数语言都几乎遵守的从上到下大概是括号、下标、函数调用、成员访问最高一元运算符正负号、逻辑非、自增自减乘除取模加减移位关系比较、、、注意、!在这层之后相等比较、!按位与按位异或^按位或|逻辑与逻辑或||条件三元运算符赋值最低除了逗号运算符为什么这个顺序几乎成了行业公约因为它跟自然语言的直觉一致先算算术数字之间的事再比较关系判断再做逻辑运算多个条件组合最后赋值。你把这个链条记住再看任何具体语言的优先级表会发现它只是在这个大框架里做微调比如位运算和逻辑运算之间偶尔有变动赋值运算符的复合形式细节不同。这样比逐条背省力得多也不容易被具体语言的差异绕晕。2.4 几个经典的优先级陷阱案例光讲概念不容易有体感我列几个实际写代码时遇到的优先级陷阱全部是真实项目里出现过的问题。陷阱一!的优先级比高。假设你想判断变量flag不等于0看到别人写了if (!flag 0)你觉得对吗因为!的优先级高于这个表达式实际是(!flag) 0也就是先取反再和 0 比较。而flag为 0 时!flag为 1不等于 0条件为假flag非 0 时!flag为 0等于 0条件为真。结果整个逻辑完全反了。正确的写法应该是if (!(flag 0))或者直接if (flag ! 0)。陷阱二和的优先级纠缠。前面提过的x y 0在 C 语言里等价于x (y 0)因为优先级高于。如果你是想判断x 和 y 按位与以后的结果是否为 0必须写成(x y) 0。这个问题在网络编程里出现过很多次比如写 socket 相关代码时判断标志位if (flags O_NONBLOCK 0)会被解析成flags (O_NONBLOCK 0)结果跟预期差了十万八千里。陷阱三a b c在 C 系语言里不是链式比较。Python 里a b c是连比表示a b b c但在 C、Java 里a b c会先算a b得到 0 或 1然后拿这个 0 或 1 再去和c比较。比如1 2 1在 C 里是(1 2) 1等于1 1结果为假但如果写成1 2 2(1 2) 2等于1 2结果为真看起来没问题实际上根本不是你想表达的语义。这种 bug 特别隐蔽因为某些数据下结果碰巧是对的换一批数据就翻车。这三个陷阱的共同特点是代码读起来像是按直觉走的编译器的语法树却是按优先级走的。所以遇到这个表达式结果和我预期不一致时第一反应不要猜直接找语言规范查优先级或者干脆加括号。3. 踩坑实录短路求值、自增自减和赋值陷阱的完整排查链路3.1 短路求值逻辑运算符的隐藏规则和||有个在几乎所有主流语言里都成立的特殊规则短路求值short-circuit evaluation。什么意思a b中如果a是假整个表达式必假那么b根本不会被求值a || b中如果a是真整个表达式必真b也不会被求值。这个规则的初衷是效率优化但它带来了非常实用的副作用可以用它来保护后续操作不崩溃。最典型的场景是判空if (ptr ! NULL *ptr 42)当ptr是空指针时ptr ! NULL为假后面的*ptr压根不会执行自然也不会有空指针崩溃。如果没有短路规则这个表达式会先算两个操作数直接炸掉。不过短路求值也是 bug 高发区。我排查过一个很有意思的问题有人写了一个逻辑判断if (is_valid || process_data() 0)他以为不管is_valid是什么process_data()都会执行但实际上is_valid为真时后面的process_data()被短路数据根本没处理。这种错误在旧代码里特别难找因为你单看结果真真假假可能发现不了什么问题但程序行为就是跟预期不一致。查了半小时最后我用调试器在process_data()里加断点发现断点根本没被命中才意识到是短路在捣鬼。排查这类问题的思路我总结为三步第一步在可疑的表达式周围加日志分别打印每个子操作数的值第二步确认哪些子表达式被短路了——最直接的方法是在子表达式里加断点或者打印语句如果没输出说明被短路了第三步根据语义决定是调整逻辑还是显式拆开写。很多时候为了可读性和可维护性直接拆成两行或者加中间变量比依赖短路规则更靠谱。3.2与赋值表达式被当成条件判断的经典事故这是编程界元老级的坑任何语言都逃不过。C 系语言里赋值表达式本身是有值的值就是被赋进去的那个值。所以if (x 0)是合法的它先把 0 赋给 x然后整个表达式的值是 0假于是条件为假。那if (x 5)呢赋值表达式的值是 5真条件为真——如果你本意是if (x 5)那程序会彻底偏离你的预期而且编译器还不一定报警告。这种 bug 为什么难排查因为单看代码很难看到问题if (x 5)一眼望去太像if (x 5)了。我在 code review 里抓到过不止三次这种问题每次对方的第一反应都是不可能我明明写的是判断。排查这类问题有个笨但有效的办法在条件判断所在的语句前后打印 x 的值如果发现 x 被莫名其妙改掉了那八成就是赋值被当成判断了。现代的编译器和 IDE 大多会对这种写法给出警告比如 GCC 的-Wall会提醒 suggest parentheses around assignment used as truth value。所以我建议你把编译器警告级别调高这属于用工具对抗人性弱点。3.3 自增自减的前后缀以及 C/C 里的未定义行为i和i的差别新手都知道一个是后加一个是先加但真正在表达式里拼起来就很容易翻车。先说前后缀的单独使用for (i 0; i 10; i)你用i还是i对这个循环的结果没有任何区别最多是微小的性能差异——现代编译器会在语义允许时自动优化掉这个差异所以你不需要为此纠结。一旦放进表达式里事情就复杂了。int y x 2;这意味着 y 等于 x 原来的值加 2之后 x 再自增。这个行为在 Java、C#、JavaScript 里都有明确定义。但 C 和 C 里有个著名的未定义行为区在一个表达式中多次修改同一个变量或者在一个表达式里既读取又修改同一个变量而且修改和读取之间没有顺序点比如i i i;这种代码到底输出什么完全取决于编译器实现同一个写法在不同编译器上可能结果不同甚至同一个编译器不同优化级别下结果都不同。这种未定义行为怎么排查说实话这类代码唯一正确的处理方式就是重写别想着去猜结果。我在实际工作中处理过一段遗留 C 代码里面有一句*dst *src单看这个表达式其实是合法的因为dst和src是两个不同指针每个指针的自增和赋值操作互不干扰。但后来有人对它做了小优化改成*dst *src结果因为两个不同的指针不再构成完全不同的访问在某些编译器上行为变得诡异排查了半天最后发现就是这一行惹的祸。所以我的原则很明确自增自减放在独立语句里用放进复杂表达式之前先问问自己这行代码三个月后我还能一眼看懂吗。3.4 三元运算符嵌套可读性灾难现场三元运算符本身是个好设计它让简单的条件赋值变得很清爽int max a b ? a : b;。但它最大的问题就是容易叠起来。我有一次接手一个老项目看到一行这样代码int status (a b) ? ((c d) ? 1 : 2) : ((e f) ? 3 : 4);当时我就沉默了。逻辑上它没错?:是右结合的这样嵌套完全合法但要让后来的人理解这行代码得先在草稿纸上画一棵树。后来我改成多个 if-else虽然代码行数多了 5 行但可读性上升了几个数量级。这里的教训是运算符可以用但要留意人在阅读时的心智负担。优先级表解决的是计算机怎么读的问题可读性解决的是人怎么读的问题。如果一个表达式超过 15 到 20 个字符或者包含两层及以上的三元嵌套我强烈建议拆开。有人觉得这是代码啰嗦但我见过太多因为简洁过头导致后来者改错逻辑的案例。代码写出来是给人看的顺便给机器执行——这句话在选型运算符时一样适用。3.5 排查表达式问题的方法论从现象到根因的完整链路前面讲的都是具体坑这一节我总结一下通用的排查思路避免你碰到一个没见过的问题时无从下手。第一步复现并最小化。把出问题的表达式抽出来尽量用最简单的数据复现比如传入固定的几个值确认结果确实和预期不符。这一步的目的是把问题从庞大的业务代码里隔离出来。第二步对齐语法树。把表达式按你理解的优先级和结合性手动加上括号转化成标准形式然后用编译器或解释器验证。比如你怀疑x y z有问题就分别跑一下(x y) z和x (y z)看哪个结果与业务预期一致。第三步查找语言规范。大部分语言官方文档都有运算符优先级表出问题时常看表比猜靠谱得多。这一步不是丢人的事从业十年的人写代码时也经常翻规范。第四步事后加自测。确认根因后把出错的表达式写成一个小小的回归测试用例保证以后不会被无意改回去。这个习惯能帮你省掉很多重复踩坑的时间。4. 不同语言里优先级并不完全一样对照几个常见语言的差异4.1 C 语言家族大框架一致细节各有脾气如果把 C、C、Java、C#、JavaScript 放在一起看大部分运算符的优先级和结合性是高度相似的毕竟有共同的语法血缘。但细节上的差异足以让你在跨语言开发时栽跟头。先说instanceof、in这类运算符不同语言的优先级各有归属都不奇怪。Java 里a instanceof B的优先级和关系运算符一样所以!x instanceof B会被解析成!(x instanceof B)很多人以为能像!x一样直接取反其实需要加括号。再说 JavaScript 里经典的加号和字符串拼接问题1 2 3结果是123因为从左往右结合先得到字符串12再拼上3而1 2 3结果是33因为先算数值的1 2得 3再拼字符串。同样是但结合性和类型转换让结果差异很大。C 语言和 C 还有一个不同C 的new运算符、作用域解析::的优先级在 C 里根本不存在这种语言级别的差异没法靠通用优先级推算只能看规范。我的经验是跨语言写代码时先默认跟我熟悉的语言一样但一旦遇到怪异语法或行为立刻去查目标语言的优先级表不要想当然。4.2 Python比较运算符可以链式逻辑运算符优先级低于比较Python 的优先级整体也符合那个大框架但有几个地方很特别。最著名的是比较运算符的链式写法1 x 10在 Python 里是合法的等价于1 x and x 10。这在 C 系语言里是语法错误或者诡异行为在 Python 里却是日常写法。但注意这个链式也带来了一个隐藏问题如果中间夹了一个会被短路求值影响的表达式比如a f() bf()只被求值一次这在某些场景下是优点但如果你期望它被调用多次写成链式就会踩坑。Python 的幂运算符**优先级高于一元负号前面已经提过-2 ** 2 -4。另外 Python 里and、or的优先级低于所有比较运算符所以a b and b c不需要加括号这跟 C 系里的和的关系也类似。但要小心Python 的、|按位运算优先级比比较运算符低吗答案是在 Python 中、|、^的优先级都高于比较运算符这一点和 C 系正好相反。举个例子a b c在 Python 里等价于(a b) c而在 C 语言里是a (b c)。这种反直觉的跨语言差异如果不加括号迟早被坑。4.3 SQL 和脚本类语言三值逻辑与各自约定SQL 的运算符优先级又是一个独特的存在。SQL 里逻辑比较的结果不是简单的 true/false还有 UNKNOWN空值参与比较时。AND的优先级高于OR所以WHERE a 1 OR b 2 AND c 3会被解析成WHERE a 1 OR (b 2 AND c 3)。在实际查数据时这个优先级经常导致查出的结果和业务预期不符。我看过不少慢查询优化案例分析到最后发现问题出在 WHERE 条件组合错了而不是索引问题。Bash 脚本里的和||又有自己的逻辑它们既是逻辑运算符又常用于命令链的短路控制。cmd1 cmd2表示 cmd1 成功才执行 cmd2cmd1 || cmd2表示 cmd1 失败才执行 cmd2。这里的、||优先级其实不高比管道符|低这导致很多人写cmd1 | grep something cmd2时容易弄错解析范围。PowerShell 的和||是较新版本才支持的命令链运算符行为类似 Bash但在 PowerShell 的表达式上下文里不像 C 语言那样自由。脚本语言的运算符优先级往往与职责高度相关——它服务于命令调度而不仅是数学表达式。4.4 触发器为什么需要一张可见的对照表场景C/JavaPythonSQL与谁更高更高先比后与更高先与后比看数据库实现比较运算符能否链式不支持abc会先算(ab)c支持abc是连比支持如BETWEEN是特殊语法三元运算符? :右结合嵌套灾难没有三元运算符用a if c else b没有通用三元运算符有CASE WHEN幂运算与一元负号关系一元负号高**高-2**2为 -4无直接对应这张表没必要背但它提醒了一个事实优先级表不是数学公理是语言设计者定的规则。不同语言考虑问题时有的为了直白有的为了历史兼容有的为了接近数学习惯最终呈现的优先级顺序五花八门。这也是为什么我一直强调用之前先查文档——对于一个要长期维护的项目这个步骤值得花 30 秒。5. 不背表也能写对表达式几条可靠准则和自测练习题5.1 四条实用准则就算不背优先级表我认为也可以靠下面四条准则把绝大多数表达式写对第一遇到不确定的直接加括号。括号不丢人反而能让读代码的人快速理解。编译器不会因为括号多而变慢现代编译器优化之后生成的机器码几乎没有区别。第二赋值表达式不要嵌进别的表达式。while ((ch getchar()) ! EOF)这种写法虽然经典但如果你不确定工作机制还是拆开写先赋值再判断。拆开的行数多了但心智负担小了很多尤其是对团队里其他成员。第三逻辑运算符的短路特性要有意识使用。利用先判空再访问成员利用||提供默认值这是既安全又简洁的模式。但要注意带短路的逻辑表达式里不要写有副作用的函数调用否则很容易被短路掉而没有执行。第四复杂条件用中间变量命名。比如bool is_admin user ! null user.role ADMIN;比在 if 里直接写一长串表达式清晰得多。中间变量的成本极低收益却很大。5.2 加括号的边界问题什么时候加什么时候不要画蛇添足虽然我说不确定就加括号但也有个度。a b * c你硬要写成(a * b) c不对我是说a (b * c)这种完全符合直觉的优先级加不加括号其实都行。但如果你在团队里要考虑到其他人读到这行代码时的心理预期a b * c所有人一眼就能看出先乘后加写成a (b * c)反而多了个解释成本。所以我的准则是优先级越符合数学和自然直觉的越不用加括号优先级越冷门、越容易和相邻运算符纠缠的越要显式括号。具体来说算术运算符的四则混合运算乘除先于加减属于全民共识不用加括号逻辑表达式里和||混用按优先级先于||但很多人记不住最好加括号表达你的意图位运算和比较运算混用这是重灾区务必加括号赋值运算嵌在其他运算里必须加括号。这样既能保证正确性又不会让代码变成括号海洋。5.3 自测练习题判断这些表达式的结果理论说多了容易飘下面这几道题建议你先不看解析自己算一遍再对照。第一题C 语言中int x 0; if (x 2) { ... }这个条件判断为真还是假第二题C 语言中int a 5, b 3; int r a b a 3 ? a : b;求 r 的值并说明 a 最终的值。第三题JavaScript 中let s 0; let arr [1, 2, 3]; arr[s] s;最终 arr 数组是什么s 是什么第四题Python 中result 2 ** 3 ** 2结果是多少提示**是右结合的第五题C 语言中int mask 0xF0; int flags 0x10; int r flags mask 0x10;r 的值是多少下面给解析第一题条件是x 2这是赋值表达式值为 2非零即真。所以条件为真。但 x 的值被改成了 2。如果你本来想判断x 2那就写错了。第二题优先级高于?:但整体结构比较复杂。先看a b为真a 3中 a 原本是 5大于 3 为真所以a b a 3为真。注意a这一项被执行了a 变成 6。因为是?:三目运算符真分支是a而a的最新值是 6所以 r 为 6。第三题JavaScript 的数组下标里混了自增。arr[s] s这行会先执行右边的ss 变成 2再赋值给arr[s]的下标这个下标是 s 原来的值 0因为 s 返回自增前的值最终 arr[0] 2s 变成 2。这跟直觉可能不太一样所以要警惕后缀自增和前缀自增混在同一行极其容易出错。第四题Python 幂运算是右结合所以2 ** 3 ** 2 2 ** (3 ** 2) 2 ** 9 512不是(2 ** 3) ** 2 64。这个和数学里从右往上的幂运算习惯一致但跟大多数二元算术运算符的左结合不同。第五题flags mask 0x10在 C 语言中优先级高于先算mask 0x100xF0 不等于 0x10结果为假0然后flags 0为 0所以 r 的值是 0。如果你本意是判断 flags 和 mask 按位与后是否等于 0x10需要写成(flags mask) 0x10那才是0x10 0x10 0x10结果为真。5.4 我个人在实操中总结的一个习惯最后分享一个我用了很多年的习惯写完表达式后用人脑括号法再读一遍。所谓人脑括号法就是你盯着表达式按你自己理解的优先级在脑子里把每个子表达式用括号框起来比如看到a b * c / d你会自动框成a ((b * c) / d)。如果某个地方你发现自己要停下来犹豫才知道该框谁那就说明这里对读代码的人不友好最好用显式括号或者拆行。这个方法听起来没什么技术含量但它逼着你去审视自己和读者对运算符关系的共识是否一致。很多 bug 不是你不会优先级而是你写的时候脑子里的括号和编译器实际生成的括号不一样。养成这个习惯以后你在 code review 里抓别人优先级错误的准确率也会明显提升。我见过太多人把熟悉运算符优先级等同于把表背下来其实完全不是一回事。真正重要的是你脑中有一个关于操作数个数、优先级、结合性、求值顺序的清晰模型并且知道在什么时候应该相信直觉、什么时候应该加括号、什么时候应该立刻查文档。带着这个模型去写代码你会发现那些一度让你头疼的复杂表达式慢慢都变成了可以拆解、可以读懂的普通代码。
返回列表