本文链接:https://blog.csdn.net/steedhorse/article/details/5903974

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

优先级决定表达式中各种不同的运算符起作用的优先次序,而结合性则在相邻的运算符的具有同等优先级时,决定表达式的结合方向。

(一)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函数的示例代码随处都能见到:

char* strcpy( char* dest, const char* src ){ char*p = dest; while(*p++ = *src++); return dest; }
理解这一实现的关键在于理解“*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实现也就没什么问题了:

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

(三)x > y ? 100 : ++y > 2 ? 20 : 30
这个表达式看起来有点吓人。让我们先给出更多的上下文吧:
int x = 3; int y = 2; 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语言运算符的“优先级”与“结合性”相关推荐

  1. [c语言]运算符的优先级与结合性

    c语言中运算符的优先级和结合性常常被人混淆一谈,本文目的在于简单谈谈两者的区别.本文举几个简单的例子说明,这些运算符也特别常用. 首先要明白的是:优先级决定表达式中各种不同的运算符起作用的优先次序:而 ...

  2. C语言运算符的优先级及结合性

    C语言运算符的优先级我们结合实例了解会更加简单易懂. 示例程序 #include <stdio.h> int main(){int a = 16, b = 4, c = 2;int d = ...

  3. C语言 运算符的优先级和结合性是怎样的?

    1.问题阐述 各种运算符的优先级是怎样的?结合顺序又是怎样的? 2.专家解答 C语言中的运算符种类丰富,有40多种,分为15种优先级,而且还具有结合性的特点.在表达式中,各运算对象参与运算的先后顺序不 ...

  4. 巧解C语言运算符的优先级和结合性

    本篇文章我们从一个例子入手讲解,请看下面的代码: #include int main(){ int a = 16, b = 4, c = 2; int d = a b * c; int e = a / ...

  5. C语言运算符的优先级和结合性

    刚学习C语言的同学看到优先级和结合性往往是一脸懵懂. 其实,所谓优先级,就是表达式中有不同的运算符,我们该就先计算哪一个?例如,a+b*c,先计算乘法,再计算加法. 所谓结合性,就是表达式中的相同类型 ...

  6. C语言 运算符的优先级与结合性

    说明 根据运算符所带的操作数的数量进行划分,C语言的运算符有三种类别: 单目运算符:只带一个操作数的运算符.如:++运算符. 双目运算符:带两个操作数的运算符.如:+.-.*./.%运算符. 三目运算 ...

  7. C语言运算符的优先级和结合性方面的试题,C语言运算符优先级与结合性一览表.docx...

    C语言运算符优先级和结合性一览表 所谓优先级就是当一个表达式中有多个运算符时,先计算谁,后计算谁.事实上在编程的时候也不需要考虑优先级的问题.因为如果不知道优先级高低的话,加一个括号就可以了,因为括号 ...

  8. c语言左值中有运算符,C语言左值,运算符的优先级以及结合性探讨

    刚刚开始看一本书.<C陷阱与缺陷>,相信学习C语言的大家都对这本书有耳闻.今天看到了里面的贪心法则.也即在读到一个字符后,尽可能多的读入更多的字符,直到读入的字符组成的字符串已经不可能再组 ...

  9. C语言运算符的优先级和结合

    今天去翻了下C语言运算符的优先级和结合性,发现当初学习的时候就没认真记住,惭愧.发现一篇讲得不错的文章,编辑了下转来供以后翻阅. C语言运算符优先级表(由上至下,优先级依次递减) 运算符 结合性 () ...

最新文章

  1. 关于reviewboard汉化
  2. 按一行一行的方法将一个文本文件复制到另一个文件中_大文件上的结构化数据计算示例...
  3. spring原始注解
  4. TypeError系列之:TypeError: conv2d(): argument 'input' (position 1) must be Tensor, not NoneType
  5. 使用PowerDesigner做数据库设计(一)
  6. 如何用Java代码解析json
  7. 百面机器学习—6.PCA与LDA要点总结
  8. 找出数组中的最大值和最小值
  9. Ryu控制器正则表达式字符串 r'[0-9]{1,4}|all' 分析
  10. linux grep -rn如何只过滤第二层的某个文件夹
  11. 百度地图Android开发清除上一次驾车导航路线
  12. 什么叫状态服务器 博客,pending是什么意思?HTTP Status pending (进程信号的未决状态)详解...
  13. Android 音乐播放器实例 嘟嘟音乐Music
  14. 【统计建模与R软件】第二章 R软件的使用 上机练习4 猴子吃桃 鸡兔同笼
  15. 新起点,新希望,为梦想而战!!!
  16. Ubuntu系统下硬盘安装windows
  17. 快速理解数据库函数依赖(平凡函数依赖、非平凡函数依赖、完全函数依赖、部分函数依赖、传递依赖)
  18. 由FlexBox算法强力驱动的Weex布局引擎
  19. nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 的解决办法
  20. 视频转接芯片方案IT6251FN:LVDS转EDP

热门文章

  1. python中常用的函数
  2. C03-Java同步实践加强班第5周上机任务
  3. android学汇资料总整理
  4. PowerDesigner 反向工程生成ER图
  5. 内网渗透-域内信息收集
  6. abap 生成流水号每天从1开始_条码软件如何制作循环流水号
  7. 获取3的倍数_获取和设置pdf目录
  8. RxSwift之UI控件UISwitch与UISegmentedControl扩展的使用
  9. Metal之基本简介及常用组件说明
  10. iOS开发两个距离较近的按钮同时触发事件的解决方法