欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于不同编译器下C语言中自加(++)运算符的解释

程序员文章站 2022-06-01 19:45:35
这篇备忘是由同学发的一个疑问,确实我也忘了我在学的时候遇到这个问题么有,主要是很少这么用过,而且纯数学计算也没有怎么写过。因为相对来说,用matlab会更好。 其实c语言是门精美...

这篇备忘是由同学发的一个疑问,确实我也忘了我在学的时候遇到这个问题么有,主要是很少这么用过,而且纯数学计算也没有怎么写过。因为相对来说,用matlab会更好。

其实c语言是门精美的语言,也是我认为最为舒服的语言,只是没有面向对象,扩展后的c++语法复杂性爆炸增长,而且各种库也比较蛋疼,mfc也成了昨日黄花,不知道object-c如何,想必苹果用的东西应该还可以。要是哪天牛逼到自己写个c的面向对象扩展超集多好,按照自己理解来,语言名字想好了(这是最简单的工作),可惜没那本事,谁叫我编译原理学的很差呢。

闲言少叙,开始。

里面东西很浅显,汇编之类的很多年都没用过了,生疏的很,希望大牛们不要笑话,只是自己做个备忘。

不过这个疑问确实很好,我研究了一下。

程序如下,非常简单:

#include<stdio.h>

#include<stdlib.h>

 


intmain()

{

int a=1,b=3,c=0;

a=(++b)+(++b)+(++b);

printf("a=%d\nb=%d\n",a,b);

return0;

}


准确说这是故意为了明白自加运算符而做的程序,实际上这是很糟糕的一段代码,尽管它有一点的效率提升,为什么糟糕,原因在于不同的编译器的解释是不一样的。

我开始看到同学在vc下的运行结果我吃了一斤(也没胖),应该说我在学tc时候也应该接触类似的程序,但是并没有发现什么特殊的结果,但是确实没在vc下运行过。

于是我在gcc下运行了一次:发现跟vc结果一样的,当然,这两个编译器是不同的。

老大用c#运行了一次,结果是正常人理解的15。

gcc是多少呢?答案是16;自加后b的值都是一样的。

 关于不同编译器下C语言中自加(++)运算符的解释

 

如果我们按照平常的理解,似乎是4+5+6=15;但是为什么gcc下是16呢?而且vc下也是16;而我要告诉你的是tc下是18;

刚才也试了刚学的python,发现这玩意没有自增运算。

我试了半天,也没理解这是怎么回事,算了,看看汇编代码把。

看一下汇编代码,说实话,linux没有用过汇编,学的8086汇编是基于intel的,我们知道汇编是与硬件紧密联系的语言,不同平台上语法存在不同,伪代码也有所区别。

 关于不同编译器下C语言中自加(++)运算符的解释


 

汇编代码有点多,在vc下也可以看,相对来说,代码要简洁多了,主要是屏蔽了一些底层的东西。

我们知道一段c代码,经过语法分析,预处理,编译,链接,最后成为可执行文件。在内存中,除了你编写的代码,还有堆栈段等一系列数据结构。作用不一而足。

我们看到关键的部分:a=(++b)+(++b)+(++b);

首先先解释下汇编,经过查阅,在linux下用的是at&t汇编(我说一开始看这玩意怎么有点奇怪),与intel几个不同点,大部分的伪命令是一致的;

加法,移动等操作,右边是目标操作数,左边是源操作数,与intel正相反;

addl----刚开始有点发蒙,难道是加到左边?其实就是add,“l”表示操作数是32bit的long类型,我擦;

$0x3----0x么,16精制数好解释,前面美元符啥意思?取这个数的地址?后来查了一下,是立即数的表示,尼玛,就是intel下面的mov esp 0x3

%esp-----esp么,寄存器,前面%,哎,不解释,还是一种表示记号,at&t下面寄存器就是以%开头,esp等共有8个32bit寄存器,还有edx之类的。

我的能力也就能解释一下a=(++b)+(++b)+(++b)这段了:

1,首先是addl$0x1,0x1c(%esp),就是加1到右边的寄存器,0x1c似乎是地址标示

2,一样的语句;

3,mov语句,将自加后的esp值放到eax寄存器中;

4,add,将eax中数自加到本身;

5,addl,将esp再自加1,看到没有

6,现在再将esp加到eax寄存器中;

7,最后把eax中的值放入变量a中;

我们看到了这个表达式的执行过程,首先是将变量b自加了两次!!!然后相加,最后在自加一次b,再和前面的和相加得出最后结果。

怎么会自加两次呢?我们知道++b是先自加后使用,关键是我们怎么去理解“使用”这个词语?

a=(++b)+(++b)+(++b);

c语言中,语法分析是采用最大识别原则,就是从左向右,不断读进字符,直到无法解释为止。

那么对(++b)+(++b),显然括号的等级最高,把左边(++b)读到栈里面,先加了1,然后读进中间的”+”号,发现右边出现左括号,故继续读入字符,注意这时候“+运算”并没有执行,那么接着运算第二个(++b),这里面就有问题,到底是5呢,还是4呢?编译器直接在变量上自加,所以,是5,而且当+右方的()运算完成后才开始计算加法,也就是“使用”,但不是4+5,而是5+5,因为b已经是5了,也就是,编译器把b变量统一为最后自加结果。所以编译器的解释是5+5+6=16!!!

是不是可以这样理解,(++b)+(++b)认为是“使用”,毕竟相加了么,

即:(++b)+(++b)为一次运算,算出为5+5,然后b变量在5基础上自加一次,故有5+5+6=16;

很不幸,这样理解不对,我们看下这个例子:a=(++b)+(b++)+(b++),如果我们按照上述逻辑思考的话,应该是4+4+5=13,意即在(++b)+(b++)完成后,可以算是使用了,b++执行,所以b为4+1=5;可惜啊,答案是12;也就是编辑器是以表达式为单位来理解“使用”这个词语。但是这样理解似乎对a=(++b)+(++b)+(++b)又无法解释,如果以表达式为单位算使用,那么似乎应该是先做完自加,然后在相加,(这是从人的角度解释的)所以结果是6+6+6=18,但是gcc下不是,但是我要说的是,tc下编译器是这么理解的!!!

我们看下a=(++b)+(b++)+(b++)的情况:

 关于不同编译器下C语言中自加(++)运算符的解释

 

从汇编上我们可以清晰看出执行流程。

似乎已经有点眉目:编译器!!


如果我们把程序修改如下:

#include<stdio.h>

#include<stdlib.h>

int main()

{

int a=1,b=3,c=++b;

a=c+(++b)+(++b);

printf("a=%d\nb=%d\n",a,b);

return0;

}

其实大多数人理解的是这个意思,这个避免了自增的一个b=4丢失的问题,仅对三个有用,多了还是上面的解释。

 关于不同编译器下C语言中自加(++)运算符的解释

 

似乎我们有了点答案,再玩玩把,我们看看a=(b++)+(b++)+(b++)会有什么结果。

 关于不同编译器下C语言中自加(++)运算符的解释

 

有没有觉得非常犀利!!

看一下汇编语句:

三个自加操作,是在最后完成的!!!

 关于不同编译器下C语言中自加(++)运算符的解释

也就是等于a=1+1+1,然后做三次自加运算。

 


那么试一下:a=(++b)+(b++)+(++b)+(++b)结果是多少呢?

前面两个似乎容易啊:

4+4=8,对呢,后面怎么玩呢?是先都自加还是一个个来呢?前面说过了,c语言是“最大口径”读入,从做到右一次完成运算(针对gcc编译器规则)。

所以,算出8以后,读入“+”,再读入右边(++b),运算出结果8+5=13,然后b+1=6;故而最后结果是13+6=19!

那么请问b=???

 关于不同编译器下C语言中自加(++)运算符的解释


 

呵呵,一开始会说6吧,其实b=7,为什么,忘了还有个b++了吧,这是放在最后运算的部分。

如果是a=(++b)+(++b)+(++b)+(b++)+(++b)+(++b);如此变态的表达式!我擦,也能写的出来。

结果是(gcc):a=37;b=9!!!其实主要是前两个++的理解:(++b)+(++b),要注意,++b并不是4,人们往往以为第一个是4,然后4+5,计算机并没有额外存储4这个数字,那么在都到下一个(++b)后,b=5,然后运算b+b=10,懂了吧?人类往往把4额外存储起来,就像这个式子表达的一样c=++b;a=c+(++b)+(++b);上面我已经做了演示。


下面我们看下tc的编译器理解:

 


tc下面执行b=3;a=(++b)+(++b)+(++b)是多少呢?答案是18;


可以看出tc编译器对此的解释是先全部做完自加运算得出最后的b值,然后再做加法运算,

本人尝试将tc反汇编一下,但是代码的可读性非常差。找了半天找到了关键部分:

[html] * referenced by a call at address: 
|:0001.011a 

:0001.01fa 55                     push bp *把基址压倒堆栈 
:0001.01fb 8bec                   mov bp, sp *把堆栈偏移地址放入bp 
:0001.01fd 56                     push si 
:0001.01fe 57                     push di   
:0001.01ff bf0100                 mov di, 0001  a 
:0001.0202 be0300                 mov si, 0003   b 
:0001.0205 46                     inc si   ++b 
:0001.0206 46                     inc si   ++b 
:0001.0207 46                     inc si    ++b 
:0001.0208 8bfe                   mov di, si    
:0001.020a 03fe                   add di, si 
:0001.020c 03fe                   add di, si 
:0001.020e 56                     push si 
:0001.020f 57                     push di 
:0001.0210 b89401                 mov ax, 0194 
:0001.0213 50                     push ax 
:0001.0214 e8b206                 call 08c9 
:0001.0217 83c406                 add sp, 0006 
:0001.021a e85410                 call 1271 
:0001.021d 33c0                   xor ax, ax 
:0001.021f eb00                   jmp 0221 
* referenced by a call at address:
|:0001.011a
|
:0001.01fa 55                     push bp *把基址压倒堆栈
:0001.01fb 8bec                   mov bp, sp *把堆栈偏移地址放入bp
:0001.01fd 56                     push si
:0001.01fe 57                     push di 
:0001.01ff bf0100                 mov di, 0001  a
:0001.0202 be0300                 mov si, 0003   b
:0001.0205 46                     inc si   ++b
:0001.0206 46                     inc si   ++b
:0001.0207 46                     inc si    ++b
:0001.0208 8bfe                   mov di, si  
:0001.020a 03fe                   add di, si
:0001.020c 03fe                   add di, si
:0001.020e 56                     push si
:0001.020f 57                     push di
:0001.0210 b89401                 mov ax, 0194
:0001.0213 50                     push ax
:0001.0214 e8b206                 call 08c9
:0001.0217 83c406                 add sp, 0006
:0001.021a e85410                 call 1271
:0001.021d 33c0                   xor ax, ax
:0001.021f eb00                   jmp 0221

 

看到没有,si寄存器保存了b=3变量值,并且先自增了三次,变为6了,然后做了两次加法,和存在di中。这个与gcc编译器解释不同吧,哎,大约6年没用汇编了,看了很生疏,很多都忘了,抽空看看把。

 


总结:

编写代码,效率要考虑,但是要避免有歧义,费解的表达方式,程序还有个可读性要求,毕竟你写的代码以后要维护。

对于自加这种运算,要注意使用条件,有时你确实少写了那么一点代码,提高了那么一丁点的效率;但是往往会带来意想不到的错误。而且问题是不同编译器会做优化,所以实际执行顺序与你理解的可能并不一样。不过想必也没有人会在生产环境中写这样的代码。这篇文章也只是从汇编的角度来阐释了处理流程,我看到有些文章是从运算符结合和优先顺序来解释的,其实本质上是编译器的选择过程。

我试图能讲的很深入,发现很多东西都还给老师了,惭愧,哎,抽空复习复习。

拙文一篇,仅做抛砖引玉。

 

摘自 designlab