C/C++ 语言中的表达式求值

 

在此,首先向裘老师致敬!
程序员

裘宗燕:C/C++ 语言中的表达式求值
数组

常常能够在一些讨论组里看到下面的提问:“谁知道下面C语句给n赋什么值?”
m = 1; n = m+++m++;
最近有位不相识的朋友发email给我,问为何在某个C++系统里,下面表达式打印出两个4,而不是4和5:
a = 4; cout << a++ << a;
C++ 不是规定 << 操做左结合吗?是C++ 书上写错了,仍是这个系统的实现有问题?
函数

注:运行a = 4; cout << a++ << a; 如在Visual c++ 6.0中,获得的是4和4;在Visual Studio中,获得的是4和5. 到底哪一个是对的呢?请详看后面的分析!

要弄清这些,须要理解的一个问题是:若是程序里某处修改了一个变量(经过赋值、增量/减量操做等),何时从该变量可以取到新值?有人可能说,“这算什么问题!我修改了变量,再从这个变量取值,取到的固然是修改后的值!”其实事情并不这么简单。
C/C++ 语言是“基于表达式的语言”,全部计算(包括赋值)都在表达式里完成。“x = 1;”就是表达式 “x = 1”后加表示语句结束的分号。要弄清程序的意义,首先要理解表达式的意义,也就是:1)表达式所肯定的计算过程;2)它对环境(能够把环境看做 当时可用的全部变量)的影响。若是一个表达式(或子表达式)只计算出值而不改变环境,咱们就说它是引用透明的,这种表达式早算晚算对其余计算没有影响(不 改变计算的环境。固然,它的值可能受到其余计算的影响)。若是一个表达式不只算出一个值,还修改了环境,就说这个表达式有反作用(由于它多作了额外的事)。a++ 就是有反作用的表达式。这些说法也适用于其余语言里的相似问题。
如今问题变成:若是C/C++ 程序里的某个表达式(部分)有反作用,这种反作用什么时候才能实际体现到使用中?为使问题更清楚,咱们假定程序里有代码片断 “...a[i]++ ... a[j] ...”,假定当时i与j的值刚好相等(a[i] 和a[j] 正好引用同一数组元素);假定a[i]++ 确 实在a[j] 以前计算;再假定其间没有其余修改a[i] 的动做。在这些假定下,a[i]++ 对 a[i] 的修改能反映到 a[j] 的求值中吗? 注意:因为 i 与 j 相等的问题没法静态断定,在目标代码里,这两个数组元素访问(对内存的访问)必然经过两段独立代码完成。现代计算机的计算都在寄 存器里作,问题如今变成:在取 a[j] 值的代码执行以前,a[i] 更新的值是否已经被(从寄存器)保存到内存?若是了解语言在这方面的规定,这个问 题的答案就清楚了。
程序语言一般都规定了执行中变量修改的最晚实现时刻(称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时 刻),语言保证一旦执行到达一个顺序点,在此以前发生的全部修改(反作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此以后的全部修改都还 没有发生。在顺序点之间则没有任何保证。对C/C++ 语言这类容许表达式有反作用的语言,顺序点的概念特别重要。
如今上面问题的回答已经很清楚了:若是在a[i]++ 和a[j] 之间存在一个顺序点,那么就能保证a[j] 将取得修改以后的值;不然就不能保证。
C/C++语言定义(语言的参考手册)明肯定义了顺序点的概念。顺序点位于
1. 每一个完整表达式结束时。完整表达式包括变量初始化表达式,表达式语句,return语句的表达式,以及条件、循环和switch语句的控制表达式(for头部有三个控制表达式);
2. 运算符 &&、||、?: 和逗号运算符的第一个运算对象计算以后;
3. 函数调用中对全部实际参数和函数名表达式(须要调用的函数也可能经过表达式描述)的求值完成以后(进入函数体以前)。
假设时刻ti和ti+1是先后相继的两个顺序点,到了ti+1,任何C/C++ 系统(VC、BC等都是C/C++系统)都必须实现ti以后发生的全部副 做用。固然它们也能够不等到时刻ti+1,彻底能够选择在时段 [t, ti+1] 之间的任什么时候刻实如今此期间出现的反作用,由于C/C++ 语言容许 这些选择。
前面讨论中假定了a[i]++ 在a[i] 以前作。在一个程序片断里a[i]++ 到底是否先作,还与它所在的表达式肯定的计算过程有关。咱们都熟悉C/C++ 语言有关优先级、结合性和括号的规定,而出现多个运算对象时的计算顺序却经常被人们忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
这里“*”的两个运算对象中哪一个先算?fun及其三个参数按什么顺序计算?对第一个表达式,采用任何计算顺序都不要紧,由于其中的子表达式都是引用透明的。 第二个例子里的实参表达式出现了反作用,计算顺序就很是重要了。少数语言明确规定了运算对象的计算顺序(Java规定从左到右),C/C++ 则有意不予 规定,既没有规定大多数二元运算的两个对象的计算顺序(除了&&、|| 和 ,),也没有规定函数参数和被调函数的计算顺序。在计算第二 个表达式时,首先按照某种顺序算fun、a++、b和a+5,以后是顺序点,然后进入函数执行。
很多书籍在这些问题上有错(包括一些很流行的书)。例如说C/C++ 先算左边(或右边),或者说某个C/C++ 系统先计算某一边。这些说法都是错误 的!一个C/C++ 系统能够永远先算左边或永远先算右边,也能够有时先算左边有时先算右边,或在同一表达式里有时先算左边有时先算右边。不一样系统可能采 用不一样的顺序(由于都符合语言标准);同一系统的不一样版本彻底能够采用不一样方式;同一版本在不一样优化方式下,在不一样位置均可能采用不一样顺序。由于这些作法 都符合语言规范。在这里还要注意顺序点的问题:即便某一边的表达式先算了,其反作用也可能没有反映到内存,所以对另外一边的计算没有影响。
回到前面的例子:“谁知道下面C语句给n赋什么值?”
m = 1; n = m++ +m++;
正确回答是:不知道!语言没有规定它应该算出什么,结果彻底依赖具体系统在具体上下文中的具体处理。其中牵涉到运算对象的求值顺序和变量修改的实现时刻问题。对于:
cout << a++ << a;
咱们知道它是
(cout.operator <<(a++)).operator << (a);
的简写。先看外层函数调用,这里须要算出所用函数,还须要计算a的值。语言没有规定哪一个先算。若是真的先算函数,这一计算中出现了另外一次函数调用,在被调 函数体执行前有一个顺序点,那时a++的反作用就会实现。若是是先算参数,求出a的值4,然后计算函数时的反作用固然不会改变它(这种状况下输出两个 4)。固然,这些只是假设,实际应该说的是:这种东西根本不应写,讨论其效果没有意义。
有人可能说,为何人们设计 C/C++时不把顺序规定清楚,免去这些麻烦?C/C++ 语言的作法彻底是有意而为,其目的就是容许编译器采用任何求值顺序,使编译器在优化中能够根据须要调整实现表达式求值的指令序列,以获得效率更高的代码。像 Java那样严格规定表达式的求值顺序和效果,不只限制了语言的实现方式,还要求更频繁的内存访问(以实现反作用),这些可能带来可观的效率损失。应该 说,在这个问题上,C/C++和Java的选择都贯彻了它们各自的设计原则,各有所获(C/C++ 潜在的效率,Java更清晰的程序行为),固然也都有 所失。还应该指出,大部分程序设计语言实际上都采用了相似C/C++的规定。
讨论了这么多,应该获得什么结论呢?C/C++ 语言的规定告诉咱们,任何依赖于特定计算顺序、依赖于在顺序点之间实现修改效果的表达式,其结果都没有保证。程序设计中应该贯彻的规则是:若是在任何“完整表达式”(造成一段由顺序点结束的计算)里存在对同一“变量”的多个引用,那么表达式里就不该该出现对这一“变量”的反作用。不然就不能保证获得预期结果。注意:这里的问题不是在某个系统里试一试的问题,由于咱们不可能试验全部可能的表达式组合形式以及全部可能的上下文。这里讨论的是语言,而不是某个实现。总而言之,毫不要写这种表达式,不然咱们或早或晚会某种环境中遇到麻烦。
后记:去年参加一个学术会议,看到有同行写文章讨论某个C系统里表达式究竟按什么顺序求值,并总结出一些“规律”。从讨论中了解到某“程序员水平考试”出 了这类题目。这使我感到很不安。今年给一个教师学习班讲课,发现许多专业课教师也对这一基本问题也不甚明了,更以为问题确实严重。所以整理出这篇短文供大 家参考。
后后记:4年多过去了,许多新的和老的教科书仍然在不厌其烦地讨论在C语言里本来并没有意义的问题(如本文所指出的)。但愿学习和使用C语言的人不要陷入其中。
学习


参考:http://bbs.csdn.net/topics/370153775优化

相关文章
相关标签/搜索