这篇备忘是由同学发的一个疑问,确实我也忘了我在学的时候遇到这个问题么有,主要是很少这么用过,而且纯数学计算也没有怎么写过。因为相对来说,用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的值都是一样的。
如果我们按照平常的理解,似乎是4+5+6=15;但是为什么GCC下是16呢?而且VC下也是16;而我要告诉你的是TC下是18;
刚才也试了刚学的python,发现这玩意没有自增运算。
我试了半天,也没理解这是怎么回事,算了,看看汇编代码把。
看一下汇编代码,说实话,LINUX没有用过汇编,学的8086汇编是基于Intel的,我们知道汇编是与硬件紧密联系的语言,不同平台上语法存在不同,伪代码也有所区别。
汇编代码有点多,在VC下也可以看,相对来说,代码要简洁多了,主要是屏蔽了一些底层的东西。
我们知道一段C代码,经过语法分析,预处理,编译,链接,最后成为可执行文件。在内存中,除了你编写的代码,还有堆栈段等一系列数据结构。作用不一而足。
我们看到关键的部分:a=(++b)+(++b)+(++b);
首先先解释下汇编,经过查阅,在LINUX下用的是AT&T汇编(我说一开始看这玩意怎么有点奇怪),与Intel几个不同点,大部分的伪命令是一致的;
加法,移动等操作,右边是目标操作数,左边是源操作数,与Intel正相反;
ADDL----刚开始有点发蒙,难道是加到左边?其实就是ADD,“L”表示操作数是32bit的LONG类型,我擦;
$0x3----0x么,16精制数好解释,前面美元符啥意思?取这个数的地址?后来查了一下,是立即数的表示,尼玛,就是Intel下面的mov
esp0x3
%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++)的情况:
从汇编上我们可以清晰看出执行流程。
似乎已经有点眉目:编译器!!
如果我们把程序修改如下:
#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丢失的问题,仅对三个有用,多了还是上面的解释。
似乎我们有了点答案,再玩玩把,我们看看a=(b++)+(b++)+(b++)会有什么结果。
有没有觉得非常犀利!!
看一下汇编语句:
三个自加操作,是在最后完成的!!!
也就是等于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=???
呵呵,一开始会说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反汇编一下,
尼玛,这代码看的人都飞了,三行代码,反汇编后是110多页汇编。这可读性差到无法言语。差点放弃了写这部分。
搜了半天,找到了关键部分:
*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值到di
:0001.0202 BE0300 mov si, 0003 *b值放入si
:0001.0205 46 inc si *++b
:0001.0206 46 inc si *++b
:0001.0207 46 inc si *++b
:0001.0208 8BFE mov di, si *b值放入di
:0001.020A 03FE add di, si *di=di+si;即:6+6
:0001.020C 03FE add di, si *同样;即:12+6
:0001.020E 56 push si *把si ,di压入堆栈保存
:0001.020F 57 push di
:0001.0210 B89401 mov ax, 0194 *ax累加寄存器,后面这个数字没看懂,程序中也没查找到,按理似乎应该把和的结果放入ax中
:0001.0213 50 push ax
:0001.0214 E8B206 call 08C9 *调用08C9
:0001.0217 83C406 add sp, 0006 *偏移地址加6
:0001.021A E85410 call 1271
:0001.021D 33C0 xor ax, ax *累加寄存器清零
:0001.021F EB00 jmp 0221 *跳转地址
*号是我添加的注释。
Si,di是两个可以自增操作的寄存器。所以汇编把b仍进了si
5,6年没用过汇编了,知识都还给老师了,我干,当年汇编我可是考了满分,两题附加题都做出来了的啊,虽说大学考不了什么太难的东西,但是基本的都忘了,有点过分,干,看这段代码还要查点资料,丢人啊。好在关键语句是弄懂了,至于堆栈指针,跳转之类还是不要管它了。。。知识么,不求甚解就好。
通过上面汇编段,可以看出TC编译器对语句a=(++b)+(++b)+(++b)的解释是:
先把变量b自增三次,然后再做两次加法运算,算出结果,所以答案当然是6+6+6=18了。
总结:
编写代码,效率要考虑,但是要避免有歧义,费解的表达方式,程序还有个可读性要求,毕竟你写的代码以后要维护。
对于自加这种运算,要注意使用条件,有时你确实少写了那么一点代码,提高了那么一丁点的效率;但是往往会带来意想不到的错误。而且问题是不同编译器会做优化,所以实际执行顺序与你理解的可能并不一样。不过想必也没有人会在生产环境中写这样的代码。这篇文章也只是从汇编的角度来阐释了处理流程,我看到有些文章是从运算符结合和优先顺序来解释的,其实本质上是编译器的选择过程。
我试图能讲的很深入,发现很多东西都还给老师了,惭愧,哎,抽空复习复习。
拙文一篇,仅做抛砖引玉。
分享到:
相关推荐
根据C语言的运算规则,`++i`在`+`运算符之前发生,但是括号中的运算顺序由编译器决定,这导致了不同编译器可能产生不同的结果。 在TC 2.0下,编译器按照从左到右的顺序处理`++`运算,每次遇到`++`都会立即增加`i`,...
在C语言中,自加(++)和自减(--)运算符是非常重要的操作,尤其在使用Dev C++环境进行程序开发时,理解这些运算符的使用方法和它们在复杂表达式中的求值情况至关重要。自加和自减运算符属于C语言的单目运算符,...
最后,编译器支持了自增`++`、自减`--`以及复合赋值运算符`+=`和`-=`。这些运算符在实际编程中非常常见,它们简化了代码,提高了效率。在编译器内部,这些运算需要转化为相应的赋值和算术操作,这在编译原理中是一个...
C语言编译器是将C源代码转换成机器可执行代码的关键工具,它涉及了编译原理中的多个重要概念和技术。这些概念包括词法分析、语法分析、语义分析、中间代码生成、优化以及目标代码生成。在提供的压缩包文件中,我们...
在编译器的构造过程中,词法分析器(也称为扫描器或 tokenizer)是一个至关重要的组件,它负责将源代码分解为一系列有意义的单元,即“词法单元”或“记号”(tokens)。对于C语言,这个过程尤为重要,因为C语言的...
- 不同的编译器可能对自加/自减运算符的处理方式略有不同。 - 在编写代码时,应考虑跨平台的兼容性问题。 通过上述讨论可以看出,自加(`++`)和自减(`--`)运算符在C语言中是非常重要的概念。正确理解和运用这些...
C语言中的自增运算符(++)和自减运算符(--)是该语言中的特有元素,它们使得变量的值增加或减少1。这两个运算符的使用规则看似简单,但在实际编程中却很容易导致错误。本文将详细讲解自增和自减运算符的用法、运算...
由于C语言中关于自增自减运算符的特定特性的深入分析较少,因此对于初学者来说,理解其复杂性可能会带来困扰。本篇文章通过一系列的实验,编写程序并分析了在不同编译环境中的运算结果,总结了在不同编译环境下的...
CO编译器是一种用于将CO语言源代码转换为目标机器代码的程序,它是计算机科学中编译器设计领域的一个实例。本文将深入探讨CO编译器的C语言实现,包括其基本概念、工作原理以及实现过程。 一、编译器概述 编译器是...
在编译器设计中,我们需要理解C语言的所有语法规则,包括变量声明、数据类型、运算符、流程控制语句(如if-else、switch-case、for、while)、函数定义与调用、指针操作等,这些都是编译器进行源代码解析的基础。...
C语言中运算符的优先级和结合律 C语言中运算符的优先级和结合律是决定复合表达式中操作数的结合方式的两个重要概念。运算符的优先级决定了操作数的结合方式,当复合表达式中的运算符的优先级不同时,操作数的结合...
虽然自增和自减运算符在大多数现代编译器中会被优化,但在某些特定情况下,它们的使用可能影响程序的性能。例如,如果一个变量频繁自增,将它预先加上所需数值可能更快。 7. 实际应用 在实际编程中,自增和自减...
在本项目中,"基于Java实现的C语言编译器【100012136】"是一个课程设计任务,目标是构建一个能够解析并处理C语言源代码的编译器。这个编译器的实现采用了Java编程语言,这使得它具有跨平台的能力,能够在多种操作...
在C语言中,大多数情况下表达式从左到右求值,但是自加和自减运算符是例外,因为它们可能涉及变量值的变更。 以自加运算符为例,当它参与的表达式包含函数调用时,函数参数的求值顺序可能对结果产生重大影响。特别...
因此,在编写涉及自增自减运算符的代码时,应当格外注意代码的可移植性问题,以及不同编译器可能对这些运算符的不同处理方式。 综上所述,C语言中的自增自减运算符虽然能够有效地简化代码,但在使用时必须遵循它们...
C语言编译器是将C语言源代码转换成机器可执行代码的关键工具,它的源代码对于深入理解编译原理和程序语言的底层运作至关重要。在编程领域,掌握编译器的工作原理有助于提升软件开发的效率和质量。C语言编译器源代码...
根据提供的【部分内容】来看,文档可能是关于深入分析C语言中自增、自减运算符在不同上下文中使用的专业指导文献,其中包含了代码示例和调试标记(如 /***found***/),显示了代码的分析过程和某些需要特别注意或...
C语言中的自加自减运算,是编程过程中经常会遇到的操作符,它们对于初学者来说可能会带来一定的困惑。本文主要探讨了++和--这两种运算符在C语言中的运用及其特性。 首先,++运算符(自加运算)是单目运算符,只能...
### 单片机C语言运算符表解析 #### 概述 在计算机编程尤其是嵌入式系统开发领域,掌握一种编程语言的运算符是至关重要的基础能力之一。本文将详细介绍单片机C语言中的运算符及其特性,帮助读者更好地理解和应用这些...