运算符优先级
鉴于算术运算符比较简单,所以我们放在最开始讲。然后下面我们要讲C语言运算符的两个非常重要的性质:
左结合性:意味着具有相同优先级的运算符将从左到右进行计算。
右结合性:意味着具有相同优先级的运算符将从右到左进行计算。
搞清楚它们,对于分析清楚一个复杂的C语言的表达式至关重要。
下面这张表格就罗列出了常见的运算符的优先级,以及它的结合性,其中:
中文版
版本一
版本2
由于 *
的优先级高于 +
,因此 i + j * k
等价于 i + (j * k)
。
当表达式中包含两个或者更多个具有相同优先级的运算符时,仅有运算符优先级规则是不够的。在这种情况下,运算符的结合性开始发挥作用。如果运算符是从左向右结合的,那么这种运算符是左结合的。二元运算符大多是左结合的:
i - j + k 等价于 (i - j) + k
i * j / k 等价于 (i * j) / k
如果运算符是从右向左结合的,那么称这种运算符是右结合的。一元运算符大多是右结合的:
-+i 等价于 -(+i)
关于运算符优先级和结合性的建议
运算符的优先级和结合性显然是运算符非常核心的重要概念,但强行把这张表格记下来显然是不现实的,所以我们给出以下总结和建议:
- 一元运算符的优先级总是高于二元运算符。
- 赋值运算符(包括复合赋值运算符)的优先级几乎是最低的。这很好理解,如果运算还没结束,赋值就完成了,这不是想看到的。
- 在实践中不要写出非常长,非常复杂、可读性非常差的的表达式。优秀的程序员不应以"写出别人不理解的代码"为荣:
- 如果表达式有过长的趋势,不妨改成两个式子
- 表达式中即便优先级没问题,也最好用合适的小括号括起来关键位置,明确代码意图,增强代码可读性。
- C语言经历了漫长的发展,有很多关于复杂表达式的惯用法,除此接触它们你可能会很头疼,但基于习惯,我们还是建议程序员记住它们。
以上。
举例说明
现在根据这张表格,我们尝试来分析几个复杂表达式的运算过程:
p是一个指向int变量的指针类型,对于表达式int num = *p++
,该表达式是如何进行计算的呢?分析如下:
- 后缀自增自减符号在表达式中拥有最高的计算优先级,所以整个表达式中
p++
最先进行计算。 - 但后缀形式的
p++
,它的主要作用是直接返回当前p的值,副作用是将p的值加1。 - 所以
*(p++)
就是*p
,也就是对指针p执行解引用运算。 - 最后执行赋值运算符
=
,将*p
的结果赋值给变量num。整个表达式执行完毕。 - 当然,整个表达式执行完毕后,p会自增1。这是表达式
p++
带来的副作用。 - 整个表达式的运算过程是:将
*p
的结果赋值给变量num,并且p指针自增1。
对于表达式int num = ++*p
,该表达式是如何进行计算的呢?分析如下:
- 在表达式
++*p
中,前缀自增运算符++
和解引用运算符*
都作用于指针p
。 - 虽然,前缀自增运算符优先级要比解引用运算符高,但它必须结合一个操作数才能计算,根据书写的位置,前缀运算符只能结合
*p
。所以整个表达式最先计算的是*p
- 前缀形式意味着主要作用是返回自增自减后的结果,副作用是自增自减1。
- 所以
=
会将*p
的结果自增1后再赋值给变量num。 - 整个表达式的运算过程是:将
*p
的结果自增1后赋值给变量num。
首先看一个简单的例子:
a + b - c
或者a * b / c
如何计算呢?
任何人都会觉得很简单——从左往右运算呗,那么为什么呢?
这就是结合性的作用:两个表达式中的运算符分别都具有相同的优先级,那么考虑运算顺序就需要考虑它们的结合性。而它们的结合性是左结合的,于是整个表达式的运算顺序就是从左往右计算。
再举一个例子:
假如s
是一个Student结构体对象,math
成员表示Student对象的数学成绩。那么表达式s.math++
是如何运算的呢?
再假如p
是一个指向s对象的指针,那么表达式p->math++
是如何运算的呢?
也许你还不懂结构体的语法,但没有关系。->
、++
、.
这三个运算符显然具有同样的优先级,于是它们之间的运算就需要考虑结合性。
而这些运算符的结合性都是左结合性
,于是整体的运算就是从左往右的——先获取s对象的math数学成绩,然后将这个成绩自增1。
对于数组的一个区间[a, b]
,表达式(b - a >> 1) + a
的含义是什么?
表达式 (b - a >> 1) + a
是用来计算区间 [a, b]
的中间值的。
b - a
计算出区间的长度。(算术运算符在所有二元运算符中优先级最高,先计算)>> 1
是右移一位操作,相当于除以 2,用于将区间长度除以 2,得到中间值的偏移量。- 最后再加上
a
,得到的就是区间[a, b]
的中间值。
相信结合这些例子,你对运算符的优先级和结合性都有一定的理解了。