当前位置 飘雪电影 资讯 正文

为什么在 C 语言中,i=1;i=(++i)+(++i)+(++i)+(++i); 得到 i 的结果是 15 而不是 14 ?

1、这种写法,是C标准严格禁止的。和伸手摸电门一样,写这种代码属于做死。

2、关于这种写法的结果的一切讨论,都是无意义的。

这在c标准里,叫做未定义行为

当然,我得说,这个术语实在糟糕,可能翻译成“不定义”反而更能反映真实态度。否则搞的好像只是现在没有定义、将来哪天搞c/c++标准的那帮官僚习气严重的老头子们一拍脑袋,给个定义就能扶正一样;但实际上呢,这个术语真正的意思是:相关情况早已被摸透且争论清楚,最终的决定就是“不管这种纯2行为,随便遇到什么结果纯属自己作死”:即“未定义”不是字面意义上看来好像是一个临时的、暂时未能确定的状态,而是对相关行为置之不理、任其自作自受这样一个非常恶劣的态度。

一切未定义行为的执行后果都是不可知的;就着某个结果随意发挥,相当于就着某次掷硬币的结果胡说八道,和“未定义行为”的定义八杆子打不着。

简单说,c/c++是一种工程师给自己设计的语言

这种语言有个特点,它不像如pascal之类学术界设计的语言那样严谨,丁是丁卯是卯;而是视实际需要允许一些变通。

比如说i++,在pascal中是不允许的,因为它并不符合表达式的定义;而在c/c++中,为了少写一行代码、或者为了和某条汇编语句对应,这颗语法糖就被加进去了。

但是呢,这种非标准的东西一旦加入,就打开了潘多拉的魔盒。因为它和普通表达式不同啊,还有个赋值的副作用。这下问题就大了。

因为和正规意义上的表达式不同,这种东西的副作用发生时机,会深刻改变程序的执行结果。

另一个基础知识:当编译器为c表达式生成机器码时,会偷偷做很多优化动作,以尽量加快程序执行速度。

怎么保证这些优化动作是对的、没有优化出错误呢?这就要靠C标准来规范了。

而对i=i++之类的表达式,C标准委员会说了,这种写法纯属蛋疼,谁写谁2,所以不规定它的正确结果是什么样子(这就是“未定义行为”的含义),编译器折腾成啥都没关系——哪怕你去格他的硬盘,也是符合标准的。

——比如说,如果我写个c编译器,发现这种代码就在屏幕上打印出血红的两个大字“F××K YOU”,然后格掉硬盘往BIOS填充垃圾数据……没关系,这个编译器是完全符合C标准的。
——而且,说不定这种编译器反而会很受软件公司欢迎。因为老板很容易就可以知道哪个小子喜欢写这种代码,可以及时解雇他。

既然这种写法是C标准明文禁止的,那么我们就可以说,任何(严肃的)写出有“未定义行为”代码的家伙,都是装X不成、反而一头撞上了“未定义行为”枪口的小丑。

至于那个一脸严肃的教人写这种小丑代码的家伙……

3、学术点说,这个写法的错误和“序列点”这个概念相关。

序列点就是类似c/c++里面, ; 这样的符号(但要注意,函数调用时,参数之间的逗号并不是逗号运算符)。它的作用就好像打拍子一样,每个拍节结束,就必须有一个确定的状态。

显然,只有大家步调一致,才能把事情做好:现在是1点整,两点整我要出门,所以必须在两点前把文件整理好,否则就会误了行程,进而巴拉巴拉……

具体每一拍内部做了什么,C标准委员会没兴趣知道。他们只关心结果。

但,对i+++i++来说,如果不规定每一拍内部的执行细节,那么就可以有很多很多种不同结果。

可如果限定了执行细节,那么在某些机器上可能就会变得很慢、或者导致一些优化算法失效,加大编译器实现的难度。

更重要的是,i++本身是一个有着特定内涵(改变i存储的值)的指令,并不是单纯的数学表达式。

显然,把它当基本数学表达式滥用,得到的复合表达式是没有数学意义(因而也没有现实意义)的。

因此,做出任何规定,都无法为这种怪异的东西赋予合理意义,都不过是要求使用者死记硬背某种毫无意义的咒语罢了——搞C标准的那帮子人还不至于这么没格调。

最终,C/C++委员会的决定是:任何类似的写法都不予支持。如果有人这么做了,那么编译器输出任何结果都是合法的。

”类似情况“指的就是“在两个序列点之间,不允许一个变量两次改变其值(这是不严谨的简化说法,不要轻信)”——这里就显出格调了:要禁,就禁这种错误的本质。人家才没闲功夫去慢慢罗列各种错误可能呢。

对照这个判定标准,很显然,i=i++就不是合法的代码。因为i++本身就改变了一次i的取值,然后赋值操作再次修改了i值。

按照规定,这种非法代码,执行结果是无法确定的。编译器为其生成任何代码都合法——当然,目前为止,还没见哪个编译器真的去格式化肇事者的硬盘——所以,现实是,不同编译器、同一个编译器的不同版本、相同版本编译器使用不同编译参数,结果都可能不同。

和编译器不同的是,对写出这种代码的人,软件公司的态度倒是完全一致、且非常坚决的:解雇。

————————————————————————————

2014.5.6 补充:

刚看到的一个花絮: 还真有编译器在遇到未定义代码时玩彩蛋恶搞肇事者的。这个编译器就是著名的gcc,有彩蛋的版本是1.17:

C语言的整型溢出问题

花絮:编译器的彩蛋

上面说了所谓的undefined行为就全权交给编译器实现,gcc在1.17版本下对于undefined的行为还玩了个彩蛋(参看Wikipedia)。

下面gcc 1.17版本下的遭遇undefined行为时,gcc在unix发行版下玩的彩蛋的源代码。我们可以看到,它会去尝试去执行一些游戏NetHack, Rogue 或是Emacs的 Towers of Hanoi,如果找不到,就输出一条NB的报错。

execl(“/usr/games/hack”, “#pragma”, 0); // try to run the game NetHack

execl(“/usr/games/rogue”, “#pragma”, 0); // try to run the game Rogue

// try to run the Tower’s of Hanoi simulation in Emacs.

execl(“/usr/new/emacs”, “-f”,”hanoi”,”9″,”-kill”,0);

execl(“/usr/local/emacs”,”-f”,”hanoi”,”9″,”-kill”,0); // same as above

fatal(“You are in a maze of twisty compiler features, all different”);

不知道谭派弟子或者谭浩强本人见到这个彩蛋,又会发表什么神论。将来的计算机二级考试,又会出什么奇葩怪题……

_________________________

应一些朋友的要求,补充关于i++确切含义的一些解释:

i=i+i++之类问题,根本不是优先级的问题。

简单说吧,a = b ,和数学课本上的 等式 完全是两个意思。

在计算机领域,它的意思是:先计算出表达式b的值,然后把这个值赋给a。

表达式的定义为: 一个单独的字面值,或者一个单独的变量,或者通过算术/逻辑运算甚至函数调用连接起来的表达式——注意,赋值操作可不是什么算术/逻辑运算,也不是函数调用。

显然,对于表达式b来说,它的运算符优先级有多复杂都不是问题。

但,因为太重要所以需要再说一遍:请注意,表达式里面不允许出现赋值操作,因为这个操作并不是算术/逻辑运算。

————————————————————
显然,i++的问题在于,虽然i++看起来只有操作符和操作数组合、而且通常作为表达式使用,但其实它的含义是i=i+1 ——这根本不是一个表达式,而是“计算表达式i+1的值,并将其赋予变量i”:换句话说,这里面额外有一个赋值操作。

事实上,i++本身作为一个c/c++语句,是不可删除的;而 2+3、a&&b、!a之类真正的表达式构成的单独语句则可以在编译时直接删除。原因就是i++另外还隐含了一个赋值操作,从而多了个会影响程序状态的“副作用”。

——c/c++里面,类似这个赋值操作的、执行后会影响程序状态的行为,被称为“副作用(side effect)”。

进一步的,c/c++标准里面对这类有表达式外表、但却另有额外语义的“假”表达式叫做“有side effect的表达式”(关于何谓side effect,c/c++标准有专门定义,请尽量参考这个定义,因为我的转述很可能会有某些瑕疵之处,不可轻信),实质上也是强调了它和原始意义上的表达式的不同之处。

但是呢,为了写代码的便利,c/c++系语言提供了一个语法糖,允许程序员将i++用到表达式里面,同时规定其含义为:首先取i的值,用这个值代入表达式,供以后求值用;之后,执行i=i+1(执行i=i+1的确切时机不限,在表达式求值之前还是之后都行,只要执行了就对)。

如此一来,忽略副作用不提的话,i++看起来就像是一个真正的表达式。

但,必须注意,i++毕竟不是一个表达式,它毕竟还有个副作用藏在里面。粗暴的用某种规定允许它掺乎进去,就必然带来很多棘手的问题。

比如说,i=i++,这个语句如何解释?

首先,这显然是一个赋值语句,所以最终i应该存的是等号右侧表达式的值;虽然i++不是表达式,但按照规定,它可以解释为“语句执行前i的取值”;所以,这其实是把语句执行前,i的取值赋给i的一个赋值语句——也就是说,执行后,i的值应该不变。

但,注意i++还有一个赋值动作。即:把语句执行前的i值加一,然后赋值给变量i——所以,执行后,i的值应该增加了1。

显然,两个赋值动作的执行结果出现了矛盾。究竟哪个对呢?

进一步的,i=(i++)+(i++)呢?这里面可有三个针对i的赋值操作啊。

不仅如此,对于函数调用,如max(i++, i++),这又是什么意义呢?

很显然,不是表达式的i++,绝对不能和表达式混淆。

虽然,为了表达简洁,c/c++系列语言允许它在特定场合代替表达式,但这并不等于说,c/c++就认为它和表达式没有差别。

相反,c/c++自始至终都认为它是一个赋值操作,只是可以在严格限定的场景替代表达式而已——这个“严格限定”,就是“不允许一个变量在一对序列点之间两次改变其值”(不太严谨的说法)。

只有满足了这个“严格限定”,程序才不会出现“二义”。

换句话说,i++本身是一个有着特定内涵(对i赋值)的指令,并不是单纯的数学表达式。
把它当基本数学表达式滥用,得到的复合表达式是没有数学意义(因而也没有现实意义)的。

把它用对,是程序员的责任。

联系我们

联系我们

0898-88881688

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部