说说C语言运算符的“优先级”与“结合性”

论坛和博客上经常看到关于C语言中运算符的迷惑,甚至是错误的解读。这样的迷惑或解读大都发生在表达式中存在着较为复杂的反作用时。但从本质上看,仍然是概念理解上的误差。本文试图经过对三个典型表达式的分析,集中说说运算符的优先级、结合性方面的问题,同时说明它们跟求值过程之间存在的区别与联系。c++

 

优先级决定表达式中各类不一样的运算符起做用的优先次序,而结合性则在相邻的运算符的具备同等优先级时,决定表达式的结合方向。ide

 

(一)a = b = c;
关于优先级与结合性的经典示例之一就是上面这个“连续赋值”表达式。
b的两边都是赋值运算,优先级天然相同。而赋值表达式具备“向右结合”的特性,这就决定了这个表达式的语义结构是“a = (b = c)”,而非“(a = b) = c”。即首先完成c向b的赋值(类型不一样时可能发生提高、截断或强制转换之类的事情),而后将表达式“b = c”的值再赋向a。咱们知道,赋值表达式的值就是赋值完成以后左侧操做数拥有的值,在最简单的状况下,即a、b、c的类型彻底相同时,它跟“b = c; a = b;”这样分开来写效果彻底相同。
通常来说,对于二元运算符▽来讲,若是它是“向左结合”的,那么“x ▽ y ▽ z”将被解读为“(x ▽ y) ▽ z”,反之则被解读为“x ▽ (y ▽ z)”。注意,相邻的两个运算符能够不一样,但只要有同等优先级,上面的结论就适用。再好比“a * b / c”将被解读为“(a * b) / c”,而不是“a * (b / c)”——要知道这可能致使彻底不一样的结果。
而一元运算符的结合性问题通常会简单一些,好比“*++p”只可能被解读为“*(++p)”。三元运算符后面会提到。函数

 

(二)*p++;
像下面这样实现strcpy函数的示例代码随处都能见到:post

[cpp] view plaincopy优化

  1. char* strcpy( char* dest, const char* src ){  lua

  2.     char*p = dest;  spa

  3.     while(*p++ = *src++);  .net

  4.   

  5.     return dest;  指针

  6. }  代码规范


理解这一实现的关键在于理解“*p++”的含义。
首先,解引用运算符“*”的优先级低于后自增运算符“++”,因此,这个表达式在语义上等价于“*(p++)”,而不是“(*p)++”。
论坛上常常有朋友不明白,为何“p++”加不加括号效果都同样,这就是答案:由于后自增的优先级原本就比解引用高,加上括号也是多余。(这里仅指语义上多余,有人以为从程序可读性上考虑并很少余,那是另外一回事。)
但这里还有一个问题容易让人糊涂,那就是后自增运算符的语义。许多书上都讲“后自增是先取值,后加1。”这么讲固然没错,但在上面这样的while语句中,人们仍是容易糊涂。当一个表达式中同时包含自增、解引用和赋值,并最终作为控制循环的条件,所谓的“先取值”又是“先”到什么地步呢?咱们仍是看看C语言标准上的说法吧。如下摘自C99标准:ISO/IEC 9899:1999:
6.5.2.4-2:The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. …… The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point.
也就是说,后自增表达式的结果值就是被自增以前的那个值,而后这个结果值被肯定以后,操做数的值会被自增。而这种“自增”的反作用会在上一个“序列点”跟下一个“序列点”之间完成。
本文不打算详细讨论序列点。有兴趣的读者能够阅读一下标准。须要指出的是:赋值运算在C语言中并非一个序列点,因此,上面的while语句中,src的自增效果无需是在赋值以前完成。但while的整个控制表达式的结束倒是一个序列点。
咱们能够这样解析“while(*p++ = *src++) ;”:首先,while当中的条件变量是个赋值表达式,左侧操做数是“*p++”,右侧操做数是“*src++”,整个表达式的值将是赋值完成以后左侧项的值。而左右两侧是对两个后自增表达式解引用。既然解引用做用于整个后自增表达式而不是仅做用于p或src,那么根据上面引用的标准,它们“取用”的分别是指针p和src的当前值。而自增的反作用只需在下一个序列点以前完成便可。
综上所述:编译器要分别取得指针p和src的当前值,基于这个值完成“*src”向“*p”的赋值;同时这个赋值结果成为整个赋值表达式的值,用以决定是否退出while循环。而后,在整个表达式结束时的某一时刻(在不影响以前叙述的前提下),p和src分别被加1。
简言之,整个表达式彻底结束之时,咱们既完成了基于p和src的旧值所进行的赋值和循环条件判断,也完成了p和src的自增。
显然,这样的描述仍是让人头晕。我曾见过关于后自增(后自减)运算的另外两种“说法”,虽然跟C语言标准上的说法并不彻底一致,但在最终的语义效果上却一模一样。这两种说法是:
(1)后自增“x++”至关于一个逗号表达式:“tmp = x, ++x, tmp”;
(2)后自增就是把操做数加1,而后返回加1以前的值做为整个表达式的值。
相对来说,仍是标准中的说法为编译器的实现(特别是优化)留下了更多空间,但上面的这两种“说法”却更便于人的理解,并且跟正确的用法在最终效果上是一致的。在C++语言中,当须要重载后自增运算符时,惯常采用的机制就是基于上面两种说法。

有了这些理解,再来理解相似下面的strlen实现也就没什么问题了:

[cpp] view plaincopy

  1. size_t strlen(const char* str){  

  2.     const char* p = str;  

  3.     while(*p++);  

  4.     return p - str - 1;  

  5. }  


注意上面函数中最后的减1。虽然是否退出while循环是由p的当前值解引用决定的,但即便while要退出,在“正式”退出以前,后自增(“++”)加1的反作用仍是要体现。也能够这么理解:所谓“退出循环”,是指“再也不执行循环体”,但控制表达式并不是循环体的一部分,它的全部反作用在整个表达式结束以前都会生效。因此,咱们最后要减掉循环退出时多走的这一步。
还想重复一遍:*p++就是*(p++),它们除了可读性以外没有任何区别,因此那种认为加上括号就能够实现先加1再解引用的想法是错误的。要达到那样的效果,能够用“*++p”。

 

(三)x > y ? 100 : ++y > 2 ? 20 : 30
这个表达式看起来有点吓人。让咱们先给出更多的上下文吧:

[cpp] view plaincopy

  1. int x = 3;  

  2. int y = 2;  

  3. int z = x > y ? 100 : ++y > 2 ? 20 : 30;  


此时,z的值该是多少呢?
这里面是两个条件运算符(?:,也叫“三目运算符”)嵌套,许多人会去查条件运算符的特性,得知它是“向右结合”的,因而认为右侧的内层条件运算“++y > 2 ? 20 : 30”先求值,这样y首先被加1,大于2的条件成立,从而使第二个条件运算取得结果“20”;而后再来求值整个条件表达式。这时,因为y已经变成3,“x > y”再也不成立。整个结果天然就是刚刚求得的20了。
这种思路是错误的。
错误的缘由在于:它把优先级、结合性跟求值次序彻底混为一谈了。
首先,在多数状况下,C语言对表达式中各子表达式的求值次序并无严格规定;其次,即便是求值次序肯定的场合,也是要先肯定了表达式的语义结构,在得到肯定的语义以后才谈得上“求值次序”。
对于上面的例子,条件运算符“向右结合”这一特性,并无决定内层的条件表达式先被求值,而是决定了上面表达式的语义结构等价于“x > y ? 100 : (++y > 2 ? 20 : 30)”,而不是等价于“(x > y ? 100 : ++y) > 2 ? 20 : 30”。——这才是“向右结合”的真正含义。
编译器肯定了表达式的结构以后,就能够准确地为它产生运行时的行为了。条件运算符是C语言中为数很少的对求值次序有明确规定的运算符之一(另位还有三位,分别是逻辑与“&&”、逻辑或“||”和逗号运算符“,”)。
C语言规定:条件表达式首先对条件部分求值,若条件部分为真,则对问号以后冒号以前的部分求值,并将求得的结果做为整个表达式的结果值,不然对冒号以后的部分求值并做为结果值。
所以,对于表达式“x > y ? 100 : (++y > 2 ? 20 : 30)”,首先看x大于y是否成立,在本例中它是成立的,所以整个表达式的值即为100。也所以冒号以后的部分得不到求值机会,它的全部反作用也就没机会生效。

 

总结一下,本文主要阐述了如下几点:
(1)优先级决定表达式中各类不一样的运算符起做用的优先次序,而结合性则在相邻的两个运算符的具备同等优先级时,决定表达式的结合方向;
(2)后自增(后自减)从语义效果上能够理解为在作完自增(自减)以后,返回自增(自减)以前的值做为整个表达式的结果值;
(3)准确来说,优先级和结合性肯定了表达式的语义结构,不能跟求值次序混为一谈。



[PS-1] 维基百科上有C/C++语言运算符表:http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B
[PS-2] 曾在新浪微博上见benbearchen提到有的公司在代码规范中要求:若是while的循环体为空语句,那么必需以continue语句代替,不许只写一个分号。我本人很同意这个。上面strcpy和strlen的两个例子之因此没那么用,只是为了“随大流”,由于这两个函数的示例实现,许多人、许多书上都这么写。


一.运算符的优先级

    在C++ Primer一书中,对于运算符的优先级是这样描述的:

    Precedence specifies how the operands are grouped. It says nothing about the order in which the operands are evaluated.

    意识是说优先级规定操做数的结合方式,但并未说明操做数的计算顺序。举个例子:

    6+3*4+2

    若是直接按照从左到右的计算次序获得的结果是:38,可是在C/C++中它的值为20。

    由于乘法运算符的优先级高于加法的优先级,所以3是和4分组到一块儿的,并非6与3进行分组。这就是运算符优先级的含义。

二.运算符的结合性

    Associativity specifies how to group operators at the same precedence level.

    结合性规定了具备相同优先级的运算符如何进行分组。

    举个例子:

    a=b=c=d;

    因为该表达式中有多个赋值运算符,究竟是如何进行分组的,此时就要看赋值运算符的结合性了。由于赋值运算符是右结合性,所以该表达式等同于(a=(b=(c=d))),而不是(a=(b=c)=d)这样进行分组的。

    同理如m=a+b+c;

   等同于m=(a+b)+c;而不是m=a+(b+c);

三.操做数的求值顺序

   在C/C++中规定了全部运算符的优先级以及结合性,可是并非全部的运算符都被规定了操做数的计算次序。在C/C++中只有4个运算符被规定了操做数的计算次序,它们是&&,||,逗号运算符(,),条件运算符(?:)。

   如m=f1()+f2();

   在这里是先调用f1(),仍是先调用f2()?不清楚,不一样的编译器有不一样的调用顺序,甚至相同的编译器不一样的版本会有不一样的调用顺序。只要最终结果与调用次序无关,这个语句就是正确的。这里要分清楚操做数的求值顺序和运算符的结合性这两个概念,可能有时候会这样去理解,由于加法操做符的结合性是左结合性,所以先调用f1(),再调用f2(),这种理解是不正确的。结合性是肯定操做符的对象,并非操做数的求值顺序。

    同理2+3*4+5;

    它的结合性是(2+(3*4))+5,可是不表明3*4是最早计算的,它的计算次序是未知的,未定义的。

    好比3*4->2+3*4->2+3*4+5

    以及2->3*4->2+3*4->2+3*4+5和5->3*4->2+3*4->2+3*4+5这些次序都是有可能的。虽然它们的计算次序不一样,可是对最终结果是没有影响的。

相关文章
相关标签/搜索