鱼C论坛

 找回密码
 立即注册
查看: 2020|回复: 1

[技术交流] 关于C语言表达式求值问题的解释

[复制链接]
发表于 2014-7-10 00:54:11 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
很多新手对于C的表达式求值(如i++ + ++i)这一问题感到十分的疑惑,不能很好地理解,今日小轩献丑,准备为新手们解释一下,如有不通之处,请各位同道指出。

首先,贴上裘老的解释:http://bbs.csdn.net/topics/370153775

其次,是小轩认为要掌握这个问题需要的一些基础知识:


1.变量的访问种类与副作用的产生
在程序设计中经常用到“数”这一概念,在程序设计语言中,“数”被抽象为“量”,在C语言中,“量”分为“变量”和“常量”两种。其中与变量相关的内容较多,主要为访问类型和属性两大类(本文解释表达式求值问题,此问题与变量的属性无过多牵连,所以会在另外一篇文章中向大家介绍变量的属性)。C语言中访问类型有两种:透明引用和真引用;透明引用指不产生副作用的访问,而真引用与之恰好相反,指产生副作用的访问。
那么,什么是副作用呢?副作用指编译器系统对一个变量实行的数值改变的过程,简单来说,就是改变一个变量的数值。

第一个基础知识介绍完了,现在举个例子说明一下:
printf("%d",a+1);这里编译器对变量a的访问属于透明引用,没有修改变量a的值(即没有产生副作用)
if(++a) a=0;7 这里的对变量a的两个访问都属于真引用,都修改了变量a的值(即都产生了副作用)


2.序列点的定义,作用及其存在位置
序列点,也称顺序点,对于编译器系统而言,它是一个瞬间,一个在此序列点之前的全部的副作用都必须要实现的瞬间,也就是说,编译器系统处理到存在序列点的地方时,前面的所有副作用都已产生(即被真引用的变量都已改变它的数值),序列点存在的意义是为了保证编译工作的有序性和有效性。
有序性:多个不同的序列点的存在使源程序中处于不同序列点之间并被多次真引用的变量的副作用的产生有序化。
有效性:序列点的存在可以使编译器各模块之间相互通信,序列点就是他们的通信方式,可以保证编译器各模块工作的有效性。

那么,都什么地方存在序列点呢?C FAQs中明确写道:
1.完整表达式(表达式语句或不为任何其他表达式的子表达式的表达式)的尾部,即“;”处
2.“||”、“&&”、“?:”或逗号操作符处
3.函数调用处(参数求值完毕,函数被实际调用之前)

关于序列点的基础知识就这么多,现在让我们看几个例子:
a=1;本例中有一个序列点,位于结束符处,有一个副作用,在唯一的序列点之前有效。
a=1,b=2,c=3;本例中有三个序列点,分别位于两个逗号操作符处和结束符处,变量a的副作用在第一个序列点之前有效,变量b的副作用在第二个序列点之前,第一个序列点之后有效,变量c的副作用在第二个序列点之后,第三个序列点之前有效。
if(a==1 && ++b>0)本例中有两个序列点,分别位于"&&"处和语句结束符处(由于if语句的特殊性,没有结束符,但if内的表达式是一个完整表达式,所以也存在序列点),变量a被透明引用,没有产生副作用,即第一个序列点之前没有副作用产生,变量b被真引用,在第一个序列点之后,第二个序列点之前实现其副作用。
printf("%d",a);本例中有两个序列点,分别位于函数调用处和结束符处(函数中的“,”是分隔符,用来分割参数,不是逗号操作符),两个序列点之前及其相对应的序列点之后均没有副作用
a=0xffffffff,a++,c=malloc(a),*c=1;                本例中有五个序列点,分别位于三个逗号操作符处、函数调用处和结束符处,变量a的第一个副作用在第一个序列点之前有效,第二个副作用在第一个序列点之后,第二个序列点之前有效,变量c的第一个副作用在第二个序列点之后,第三个序列点(第三个序列点是函数表达式内的那个)之前有效,第三个序列点与第四个序列点之间没有副作用,变量c的第二个副作用在地四个序列点之后,第五个序列点之前有效。


3.对于前缀运算和后缀运算的解释
前缀运算,主要包括前缀自增运算(++i)和前缀自减运算(--i),相对应的,后缀运算主要包括后缀自增运算(i++)和后缀自减运算(i--)。
前缀运算与后缀运算的差别主要在于其副作用实现依据的序列点的不同。
前缀运算的副作用在本句结束符处的序列点之前有效,而后缀运算的副作用在本句结束符处的序列点之后有效。

举例:
a++;本例中有一个副作用和一个序列点,其中副作用由后缀运算产生,所以在本句结束之前,变量a的值都不改变,在本句结束之后,变量a的值改变。
--a;本例中有一个副作用和一个序列点,其中副作用由前缀运算产生,所以在本句结束之前,变量a的值就已经改变。


4.C语言一个重要的规则
ANSI/ISO C标准中有这样的描述:在上一个和下一个序列点之间,一个对象所保存的值至多只能被表达式的求值修改一次,而且只有在确定将要保存的值的时候才能访问前一个值。

我做具体解释如下:
在上一个和下一个序列点之间指的就是相邻的两个序列点,一个对象所保存的值至多只能被表达式求值修改一次指的是一个变量的值只能修改一次,只有在确定将要保存的值的时候才能访问前一个值指的是将要用来计算表达式计算的每个变量的值都应该是确定的;整合化简后,可以改述为:在两个相邻的序列点之间,对于同一个变量只能进行一次真引用。

现举例如下:
a=1,b=2,a=b;本例中有三个序列点和副作用,且相邻序列点之间对同一变量进行真引用的次数没有超过一次,所以本表达式合法。
a=a+1;本例中有一个序列点和一个副作用,变量a在寻列点之间出现两次,但对变量a的真引用的次数只有一次,没有违反我们刚才的规则,所以本表达式同样合法。
a[b=2]=++b;本例中有一个序列点和两个副作用,变量b的第一个副作用对变量b就行了一次真引用,就变量b的值修改为2,变量b的第二个副作用源自于前缀自增表达式,两个副作用都在同两个序列点之间,所以编译器不知道是先进行第一个副作用还是先进行第二个副作用了,所以就有了四种结果(a[2]=2、a[2]=3、a[3]=2、a[3]=3)了,这是我们不希望看到的,所以本表达式不合法。
a[b=2]=b--;本例中有两个序列点和两个副作用,但是变量b的第二个副租用源自于后缀运算,这个副作用在本句结束符处的序列点之后有效,所以本句还可以解释为a[2]=2,b=1;这样的表达式符合我们的规则,所以本表达式合法。

注:我这里指的表达式不合法不是说表达式不能通过编译器的编译,指的是可以通过编译但结果未知(就像a[b=2]=++b这个例子一样有四种解释方案,结果自然未知),我们习惯上统称此类问题为UB问题(未定义问题)。未定义问题主要是因为C语言标准没有明确规定遇到这种问题时编译器应如何处理,所以,对于未定义问题不同编译器给出的结果可能不同,可能相同,例如char c="asdf";在VC6.0和LCC-WIN32的结果就是不同的。


下面回到主题,我们终于可以看看i++ + ++i了!在本例中,只有一个序列点(未写出全句),但在这个序列点之前访问了两次变量i,其中有一次访问为真引用,这样就使表达式产生了二义性(后面的i自加后,前面的i是自加前的i还是自加后的i?),所以这个表达式就成为了未定义问题。







ok,写到这里,这篇文章就差不多该结稿了,我希望新手们将本文多读几遍,就算不能全部理解,也要能记下多少就记多少,以后多练练,就回了;至于老手们,看后如果找到了什么不恰当的地方,还请指出,我一定会努力修改,使本文日臻完美的。
最后在此声明,任何转载本文者均需注明来源(www.fishc.com)。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2014-7-10 10:22:42 | 显示全部楼层
本人觉得不应该把++与其他表达式合并在一个表达式中,因为不容易让别人理解
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-11-24 14:30

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表