下面小编就带大家简单谈谈C/C语言中的表达式求值。我觉得边肖挺好的。我现在就分享给你,给你一个参考。来和边肖一起看看吧。
在一些讨论组里经常可以看到下面这个问题:“谁知道下面的C语句给N什么值?”
m=1;n=m m
最近有个不认识的朋友给我发邮件,问为什么在某些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].一个[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.在每个完整表达式的末尾。完整表达式包括变量初始化表达式、表达式语句、返回语句以及条件、循环、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)资金(a,b,a 5)
这里“*”的两个操作数哪个先来?乐趣和它的三个参数是按什么顺序计算的?对于第一个表达式,采用什么计算顺序都无所谓,因为里面所有的子表达式都是引用透明。第二个例子的自变量表达式有副作用,所以计算顺序很重要。少数语言明确规定了操作对象的计算顺序(Java规定从左到右),而C/C有意没有。它没有指定大多数二元运算中两个对象的计算顺序(除,||和,),也没有指定函数参数和调整函数的计算顺序。计算第二个表达式时,先按一定顺序计算fun,A,B,a 5,然后是序列点,再进入函数执行。
很多书在这些问题上是错误的(包括一些非常流行的书)。比如C/C先算左(或右),或者某个C/C系统先算某个边。这些说法都是错的!C/C系统总是可以先计算左边或右边,或者有时先计算左边或右边,或者有时在同一个表达式中先计算左边或右边。不同的系统可能采用不同的序列(因为都符合语言标准);同一系统的不同版本可以采用不同的方式;同一版本在不同的优化方法下,不同的位置可能采用不同的顺序。因为这些做法符合语言规范。这里还要注意顺序点的问题:即使先计算一边的表达式,它的副作用也不一定在内存中体现出来,所以对另一边的计算没有影响。
回到前面的例子:“谁知道下面的C语句给n赋什么值?”
m=1;n=m m
正确答案是:不知道!语言并没有规定要计算什么,结果完全取决于具体系统在具体语境下的具体处理。涉及到运算对象的求值顺序和变量修改的实现时间。用于:
couta a;
我们知道它是。
(cout.operator(a))。操作员(a);
的缩写。先看外层函数调用。这里,我们需要计算使用的函数和a的值。语言没有指定哪个先出现。如果真的是先计算函数,在这个计算中又发生了一个函数调用,在被调用的函数执行之前有一个序列点,那么就会实现A的副作用。如果先计算参数,求a的值。
4,然后计算函数的副作用当然不会改变它(这种情况下输出两个4)。当然,这些只是假设。其实应该说的是:这种东西根本就不应该写,讨论它的效果没有意义。
有人可能会说,为什么人们在设计C/C的时候不指定顺序,这样就可以避免这些麻烦了?C/C语言的练习完全是有意的,其目的就是允
许编译器采用任何求值顺序,使编译器在优化中可以根据需要调整实现表达式求值的指令序列,以得到效率更高的代码。
和Java一样,严格指定表达式的求值顺序和效果,不仅限制了语言的实现,还需要更频繁的内存访问(以达到副作用),可能带来相当大的效率损失。应该说,在这个问题上,C/C和Java的选择都贯彻了各自的设计原则,各有所获(C/C的潜在效率,Java更清晰的程序行为),当然也有所失。还需要指出的是,大多数编程语言实际上都采用了类似C/C的规则。
经过这么多的讨论,我们应该得到什么结论呢?C/C语言的规定告诉我们,在任何依赖于特定计算顺序、依赖于在顺序点之间实现修改效果的表达式,其结果都没有保证。的编程中应该实现的规则是:如果在任何一个“完整表达式”中有对同一个“变量”的多次引用(形成一个以序列点结束的计算),那么表达式中就不应该有对这个“变量”的副作用。否则,无法保证预期的结果。注意:这里的问题不是在某个系统中尝试,因为我们不可能测试所有可能的表达式组合和所有可能的上下文。我们谈论的是语言,而不是实现。总之,千万不要写这种表情,不然我们迟早会在某种环境下陷入困境。
后记:去年,我参加了一个学术会议。看到有同事写文章讨论一个C系统中表达式求值的顺序,总结了一些“规则”。从讨论中,我了解到一些“程序员水平测试”有这样的问题。这让我感到很不安。今年给一个老师的班级讲课,发现很多专业课的老师都不清楚这个基本问题,觉得问题确实比较严重。所以编这篇短文,供大家参考。
以上关于C/C语言表达式求值的讨论就是边肖分享的全部内容。希望能给你一个参考,多多支持我们。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。