首页 / 资讯中心 / 文章详情

C++中的运算符和运算符优先级总结

C++中的运算符和运算符优先级总结 ★ FEATURED ARTICLE
前言运算符优先级是 C 里最容易被背过就忘的知识点。多数人记住的是一张从高到低的表却忽略了两个更重要的区分优先级只决定表达式如何结合不决定子表达式何时求值结合性只决定同级运算符从哪边开始不决定操作数的计算顺序。把这三件事混为一谈就会写出看起来加了括号就对了却仍然有未定义行为的代码。一个典型误解是优先级高的先算。a b * c确实先形成b * c这个子表达式但这说的是语法结构在运行期a、b、c的求值顺序可能被编译器任意安排只要不违反顺序规则。再比如f() g()两个函数谁先调用是未指定的优先级表对此一个字都没说。本文先给出常用的优先级分层与结合性速查再集中讲最容易写错的组合最后说明 C17 对求值顺序做了哪些明确规定、哪些仍然不能依赖。文中所有结论以标准语义为准代码不声称经过编译或运行。一、优先级与结合性到底规定什么表达式a b * c的语法树是a (b * c)因为*的优先级高于。表达式a - b - c的语法树是(a - b) - c因为-是左结合。优先级回答不同级运算符谁先结合结合性回答同级运算符从哪边结合两者合起来唯一确定语法结构。C 的运算符可以粗分为十八层。下面这张表按从高到低排列只列常见项层级运算符结合性示例含义1::左std::vector限定名字2aa--a()a[].-左p-x、v[i]3a--aa-a!~、类型转换、*aa、sizeof、new、delete右*p、-x、new int4.*-*左成员指针访问5*/%左乘除取余6-左加减7左移位、流插入提取8左三路比较C209左关系比较10!左相等比较11左按位与12^左按位异或13\左14左逻辑与短路15\\16?:右条件运算符17-*/%^\右18,左逗号运算符这张表不需要逐行背。真正需要条件反射的是几个反直觉的位置移位低于加减、按位与低于相等比较、赋值低于条件运算符、逗号最低。写表达式时只要对这三四处没把握直接加括号即可括号永远比记忆可靠。还有一条与表无关的规则重载运算符不改变优先级、结合性或操作数个数。你可以为自定义类型重载和*但a b * c仍然先结合b * c你也可以重载做流输出但它的优先级仍然低于、高于关系比较。运算符重载改变的是这个符号对某类型做什么不是这个符号在语法里排第几。二、最容易写错的组合2.1 一元运算符与后缀自增*p结合为*(p)即先取指针旧值解引用再让指针后移。若想自增所指对象应写(*p)若想先移动指针再解引用应写*p。三者语义完全不同但视觉上只差一个括号。// 适用于 C17int a[3] {10, 20, 30};int* p a;int x *p; // x 10p 指向 a[1]int y (*p); // y 20a[1] 变成 21int z *p; // p 先指向 a[2]z 30按标准语义这段结束后x、y、z分别为 10、20、30数组第二个元素为 21。这类代码可读性差生产代码更推荐拆成多行。2.2 位运算与比较x 1 0结合为x (1 0)也就是x false即x 0结果恒为 0条件恒为假。正确写法是(x 1) 0。同样的陷阱出现在x | y z、x ^ y ! 0上因为按位运算符的优先级都低于相等比较。C 的这条设计与 C 一致属于历史遗留早期为了少写括号把位运算排在了比较之后。现代代码不应利用它凡是位运算参与条件判断都显式加括号。2.3 赋值、条件与逗号赋值是右结合所以a b c等价于a (b c)先把c赋给b结果再赋给a。这依赖赋值表达式返回左操作数的引用对内置类型与遵循惯例的类类型都成立。条件运算符也是右结合a ? b : c ? d : e等价于a ? b : (c ? d : e)。但条件运算符的优先级高于赋值所以flag ? x : y 1会被解析为(flag ? x : y) 1只有当两个分支都是可修改左值时才能通过编译语义也往往不是作者想要的。想表达条件成立才赋值应写flag ? (x 1) : (y 1)或者直接用if。逗号运算符优先级最低a 1, b 2是(a 1), (b 2)整个表达式的值是b 2的结果。函数调用里的逗号是参数分隔符不是逗号运算符两者不要混淆。2.4 移位与流std::cout a b是左结合等价于(std::cout a) b这正是链式输出的基础。但std::cout a ? b : c会先算(std::cout a)得到一个流对象再把它按布尔值转换作为条件——结果几乎总不是作者想要的某些分支类型组合还会直接编译失败。把条件表达式整体括起来或改用if才是清晰写法。同理1 2 3是1 (2 3)因为加法优先级高于移位结果是 32 而不是 12。涉及移位时宁可写成1 (2 3)。2.5new与数组、函数调用new int[10]与new int(10)的区别不是优先级而是语法形式前者分配十个int后者分配一个初始化为 10 的int。但new与后缀表达式的组合容易出错例如new int[10][20]是二维数组而new (int[10])不是合法写法。涉及new时优先用容器或std::make_unique从源头绕开这类语法细节。三、优先级不等于求值顺序这是全文最需要记住的一节。优先级和结合性在编译期确定语法树求值顺序描述的是运行期各子表达式按什么次序执行。标准对后者有单独一套规则而且很多情况是未指定甚至未定义。3.1 未定义行为同一对象的修改之间没有顺序如果两个副作用修改同一个内存位置且二者之间没有顺序关系行为未定义。经典例子是i i 2在 C17 之前右侧的自增与赋值对i的修改缺乏顺序属于未定义行为C17 规定赋值的右操作数先于左操作数求值这个具体写法才变成有定义。但i i i仍然未定义因为自增与另一处读取之间没有顺序。实践结论很简单不要在同一个表达式里对同一变量既读又改。拆成多条语句任何标准版本下都不会错。3.2 未指定顺序操作数之间大多数二元运算符的两个操作数之间没有先后保证。f() g()谁先调用未指定a[i] i中读取i与修改i没有顺序属于未定义行为。函数实参的求值在 C17 起是indeterminately sequenced——不会交错执行但先后仍不确定且不同调用可能不同。3.3 C17 明确规定的几处顺序C17 细化了求值顺序常用的有表达式形式C17 起的顺序保证E1[E2]E1先于E2E1 E2、E1 E2E1先于E2E1 E2、E1 E2E2先于E1E1.*E2、E1-*E2E1先于E2E1 E2、E1 \\cond ? a : bcond先于被选中的分支函数调用函数名先于各实参实参之间不确定重载运算符运算符写法遵循它所重载的内置运算符规则注意最后一行std::cout a b之所以能按顺序输出是因为 C17 规定移位的左操作数先于右操作数而重载的operator在用运算符写法调用时沿用这条规则。若写成函数调用形式operator(std::cout, a)实参之间就不再有这条保证。四、完整示例重载不改变优先级下面的例子让自定义类型重载、*、与用来观察语法结构如何决定结果。按标准语义a b * c应先算b * c而(a b) * c先算括号1 1 2应先算加法。// 适用于 C17#include iostreamclass Score {public:explicit Score(int value) : value_(value) {}Score operator(const Score rhs) {value_ rhs.value_;return *this;}Score operator*(const Score rhs) {value_ * rhs.value_;return *this;}int value() const { return value_; }private:int value_;};Score operator(Score lhs, const Score rhs) {lhs rhs;return lhs;}Score operator*(Score lhs, const Score rhs) {lhs * rhs;return lhs;}std::ostream operator(std::ostream os, const Score s) {return os s.value();}int main() {Score a(2);Score b(3);Score c(4);Score r1 a b * c; // 等价于 a (b * c)Score r2 (a b) * c; // 括号改变结合结构std::cout r1 r2 \n;int x 1;int y 2;std::cout (x 1 y) \n; // 1 y 先算再左移return 0;}按标准语义r1的值为 14r2的值为 20最后一行输出 8因为1 3。这段代码同时说明了两件事重载的operator*仍然服从*的优先级operator的链式调用依赖左结合与左操作数先求值。常见坑点❌ 认为f() g()会先调用f()再调用g()。✅ 两个操作数的求值顺序未指定有先后依赖就拆成两条语句。❌ 写if (x 1 0)判断奇偶。✅ 写成if ((x 1) 0)因为的优先级高于。❌ 把*p当成自增p指向的值。✅ 它是*(p)自增所指对象要写(*p)。❌ 在条件里写if (a b)把赋值当比较。✅ 写if (a b)打开编译器警告让赋值出现在条件中被发现。❌ 写std::cout a ? b : c以为会按条件输出。✅ 加括号写成std::cout (a ? b : c)或改用if。❌ 在同一表达式里写i i 2并认为所有编译器结果一致。✅ C17 前是未定义行为即使 C17 起该写法有定义也应拆成多条语句。❌ 认为重载operator后自定义类型会先算加号。✅ 重载不改变优先级与结合性仍由运算符本身决定语法结构。❌ 把new int[10]与new int(10)当成同一件事。✅ 前者是数组后者是单个值初始化优先用容器与智能指针避免混淆。总结概念回答的问题何时确定能否依赖优先级不同级运算符谁先结合编译期能但建议加括号结合性同级运算符从左还是从右编译期能但建议加括号求值顺序子表达式按什么次序执行运行期语义仅标准明确规定的部分能依赖未定义行为修改同一对象缺乏顺序不保证任何情况下都不能写运算符优先级是一张查表就能解决的事真正容易出事的是把它当成求值顺序。写表达式时坚持两条结构有疑问就加括号同一表达式里不重复修改同一变量。做到这两点绝大多数优先级问题根本不会出现。
阅读完成 · 觉得有帮助?
咨询建站