c语言真题博士

c语言规范标准C99明确规定这种行为是未定义的,不同的编译器有不同的实现,所以你计算出来的结果是不一样的。不要纠结这个问题,没有意义,编程时也不要写行为未定义的代码。

翻到邱宗炎博士关于这个问题的一篇描述性文章,非常好理解。

不能贴网址,只贴内容:

在一些讨论组里经常可以看到下面的问题:“谁知道下面的C语句给n什么值?”m = 1;n = m+++m++;

最近,一个不认识的朋友发邮件给我,问为什么在某个C++系统中,下面的表达式打印了两个4,而不是4和5:

a = 4;cout & lt& lta++ & lt;& lta;

C++不是一个规则

要理解这一点,需要理解的一个问题是:如果在程序的某个地方修改了一个变量(通过赋值、递增/递减运算等。),什么时候可以从变量中获取新值?有人可能会说:“什么问题!我修改了变量,然后我从这个变量中取值,当然得到了修改后的值!”其实没那么简单。

C/C++语言是基于表达式的语言,所有的计算(包括赋值)都是在表达式中完成的。" x = 1;"也就是说,表达式“x = 1”后跟一个分号,表示语句结束。要理解程序的含义,首先要理解表达式的含义,即由表达式决定的计算过程;2)其对环境的影响(环境可视为当时所有可用的变量)。如果一个表达式(或子表达式)只计算值而不改变环境,我们说它引用透明,这个表达式对其他计算没有影响(不改变计算的环境)。当然,它的值可能会受到其他计算的影响)。如果一个表达式不仅计算一个值,而且修改环境,就说这个表达式有副作用(因为它做了额外的事情)。A++是一个有副作用的表达式。这些陈述也适用于其他语言中的类似问题。

现在问题变成了:如果C/C++程序中的一个表达式(部分)有副作用,那么这个副作用什么时候才能在使用中真正体现出来?为了使问题更清楚,我们假设有一个代码片段”...a[i]++...一个[j]...”在程序中,并假设当时I和J的值正好相等(A [I]和A [j]只是指同一个数组元素);假设a[i]++确实是在a[j]之前计算的;假设没有其他动作来修改a[i]。在这些假设下,a[i]++的修改能否体现在对a[j]的评价中?注意:因为I和J不能静态确定,所以在目标代码中,这两个数组元素的访问(对内存的访问)必须由两个独立的代码来完成。现代计算机的计算都是在寄存器中完成的。现在的问题是:在执行取值为a[j]的代码之前,a[i]的更新值是否已经保存到内存中(从寄存器中)?如果我们知道这方面的语言规则,这个问题的答案就清楚了。

编程语言通常会指定变量修改在执行中的最晚实现时间(称为序列点、序列点或执行点)。程序执行中有一系列顺序点(时刻),语言保证一旦执行到一个顺序点,在这个点之前发生的所有修改(副作用)都必须实现(必须体现在后续对同一存储位置的访问中),在这个点之后的所有修改都还没有发生。连续点之间没有保证。对于像C/C++这样允许表达式有副作用的语言来说,顺序点的概念尤其重要。

现在上面这个问题的答案已经很清楚了:如果a[i]++和a[j]之间有一个序贯点,那么可以保证a[j]会得到修改后的值;否则无法保证。

C/C++语言定义(语言参考手册)明确定义了序列点的概念。序列点位于:

1.在每个完整表达式的末尾。完整表达式包括变量初始化表达式、表达式语句、返回语句的表达式以及条件、循环、开关语句的控制表达式(for开头有三个控制表达式);

2.运算符&;& amp、||、?:在计算和逗号运算符的第一个操作数之后;

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)

这里“*”的两个操作数先算哪个?乐趣和它的三个参数是按什么顺序计算的?对于第一个表达式,任何计算顺序都无所谓,因为里面的子表达式都是引用透明的。第二个例子,实参数表达式有副作用,所以计算顺序很重要。少数语言明确规定了操作数的计算顺序(Java规定从左到右),而C/C++在大多数二元运算中刻意不规定两个对象的计算顺序(除了& & amp,|||和,),并且没有指定函数参数和调谐函数的计算顺序。计算第二个表达式时,先按一定顺序计算fun,a++,B,a+5,然后是序列点,再进入函数执行。

很多书在这些问题上是错误的(包括一些非常流行的书)。比如C/C++先算左(或右),或者某个C/C++系统先算某个边。这些说法都是错的!C/C++系统总是可以先计算左边或右边,或者在同一个表达式中,有时先计算左边,有时先计算右边,或者有时先计算左边,有时先计算右边。不同的系统可能采用不同的顺序(因为都符合语言标准);同一系统的不同版本完全可以采用不同的方法;同一版本在不同的优化方法下,不同的位置可能采用不同的顺序。因为这些做法符合语言规范。这里还要注意顺序点的问题:即使先计算一边的表达式,它的副作用也不一定会在内存中体现出来,所以对另一边的计算没有影响。

回到前面的例子:“谁知道下面的C语句给n什么值?”

m = 1;n = m+++m++;

正确答案是:不知道!语言并没有规定要计算什么,结果完全取决于具体系统在具体语境下的具体处理。涉及到操作数的求值顺序和变量修改的实现时间。用于:

cout & lt& lta++ & lt;& lta;

我们知道它是

(cout.operator & lt& lt(a++)。操作员& lt& lt(一);

简称。我们先来看看外层的函数调用。这里我们需要计算所使用的函数(从带下划线的段落中获得)和a的值。Language没有指定首先计算哪一个。如果真的是先计算函数,在这个计算中出现了另一个函数调用,在被调用的函数体执行之前有一个序列点,那么就会实现a++的副作用。如果先计算参数,a的值是4,然后计算函数的副作用当然不会改变它(这种情况下输出两个4)。

当然,这些只是假设。其实应该说的是,这种东西根本就不应该写,讨论它的效果是没有意义的。有人可能会说,为什么人们在设计C/C++的时候不把顺序指定清楚,这样就可以避免这些麻烦了?C/C++语言的做法完全是有意为之,其目的是允许编译器采用任意求值顺序,以便编译器在优化中根据需要调整表达式求值的指令顺序,从而得到更高效的代码。像Java那样严格规定表达式的求值顺序和效果,不仅限制了语言实现,还需要更频繁的内存访问(以达到副作用),可能带来相当大的效率损失。应该说,在这个问题上,C/C++和Java的选择贯彻了各自的设计原则,各有所获(C/C++的潜在效率和Java更清晰的程序行为),当然,两者都有所失。还需要指出的是,大多数编程语言实际上都采用了类似于C/C++的规定。

讨论了这么多,我们应该得出什么结论?C/C++语言的规定性告诉我们,任何依赖于特定计算顺序的表达式的结果和顺序点之间的修改效果都是不保证的。编程中应该实现的规则是,如果在任何一个“完整表达式”中有对同一个“变量”的多次引用(形成一个以序列点结束的计算),那么表达式中的这个“变量”就不应该有副作用。否则,无法保证预期的结果。注意:这里的问题不是在某个系统中尝试,因为我们不可能尝试所有可能的表达式组合和所有可能的上下文。这里讨论的是语言,不是实现。总之,千万不要写这个表情,不然我们迟早会在某个环境中陷入困境。

后记:去年参加一个学术会议,看到一个同事写文章讨论一个C系统中的表达式应该按什么顺序求值,总结了一些“规则”。从讨论中,我了解到一个“程序员级别考试”有这样的问题。这让我感到很不安。今年给一个老师的班级讲课,发现很多专业老师对这个基本问题都不是很清楚,感觉问题真的很严重。因此,特编此文,供大家参考。

后记:四年多过去了,很多新老教材还在不厌其烦地讨论在C语言中毫无意义的问题(如本文所指出的)。想学习和使用C语言的人不要沉迷其中。