前置++与后置++之一道简单的题目引发的思考

引言

昨晚一时兴起,我脑子就问自己下面的代码会输出什么,也不知道我脑子为什么有这个代码模型,只是模糊的有些印象:

#include <stdio.h>
#include <stdlib.h>int main(int argc,char** argv)
{int i=3,j;j=(i++)+(i++)+(++i);printf("i = %d, j = %d\n",i,j);exit(0);
}

您会怎样考虑这个问题呢?您不运行这个程序能准确地说出答案吗?我猜想肯定有大部分人不能肯定且准确地说出答案!如果您不能,这篇文章就是为你准备的,保证您看完之后豁然开朗!请细看下文,outline如下:

1、诸君的回答
1.1、A君的回答
1.2、B君的回答
1.3、C君的回答
1.4、D君的回答
2、编译器的输出
2.1、Visual Studio的输出
2.2、GCC的输出
2.3、Visual C++ 2010的输出
3、分析
3.1、gcc编译器上的分析
3.2、分析gcc编译之后的汇编代码
3.3、vs编译器上的分析
3.4、分析VS编译之后的汇编代码
4、扩散思维
4.1、思维放射
4.2、VS的输出
4.3、GCC的输出
5、感慨

1、诸君的回答
我拿这道题目问了几个人,他们的答案不尽相同。

1.1、A君的回答

因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是由于前面两个++是后置++,最后一个++是前置++,故j = 3+4+6 = 13。

1.2、B君的回答

因为i = 3,故第一个i++后为4,第二个i++后为5,接着做i+i操作 = 5+5=10,最后与(++i)相加 = 10+6=16。

1.3、C君的回答

因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是第一i、第二个i的++是后置++,先进行i+i操作,然后进行两次i++后置操作,故等价于(i)+(i) = 3+3=6,i++,i++,最后与++i=6相加等于12。

1.4、D君的回答

因为i = 3,故依次i++=4,i++=5,++i=6,i最后输出为i = 6;但是前面两个++都是后置++,故先做i+i+(++i)操作,然后才在i++,i++操作,第三个++是前置++,故等价于 i+i+(++i)=3+3+4=10,i++,i++。

到底哪个人说得对呢?

2、编译器的输出

首先让我们先来看看编译器会输出什么?

2.1、Visual Studio的输出

运行环境:Win7+VS2005 or VS2010,输出如下图所示:

这里写图片描述

2.2、GCC的输出

运行环境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,运行结果如下:

image

2.3、Visual C++的输出

运行环境:Win7+VC2010,输出和VS一样,及i = 6 & j = 12

看到这里你肯定想问why? why?? why???

3、分析

重编译器的输出结果来看貌似C君、D君的分析都是对的,这种差异跟编译器有直接的关系,因为对于这个表达式怎么编译还没有形成标准,编译器的结合方向不同,答案因此会有所不同。而且当然还包括运算符的优先级等。其实顶多算C君答对了一部分,其他几个人的回答都是错的,详情见下面的分析。

3.1、gcc编译器上的分析

(i++)+(i++)+(++i) <=> i+i+(++i); i++; i++;即如果表达式中含有i++,一律替换成i,然后在表达式之后进行i++操作。

这样的话上面的代码就可以很好的理解了,即3+3+4=10。

3.2、分析gcc编译之后的汇编代码

可以对gcc编译之后的执行文件进行反编译分析验证正确性。在Linux下面可以用objdump –d xxx(执行文件)命令反汇编执行文件。反编译之后可以看到如下图所示的代码:
这里写图片描述

说明:Linux下采用的是AT&T的汇编语法格式,Windows下面采用的是Intel汇编语法格式。二者的主要区别在于:

  1. 指令操作数的赋值方向是不同的
    Intel:第一个是目的操作数,第二个是源操作数
    AT&T:第一个是源操作数,第二个是目的操作数
  2. 指令前缀
    AT&T:寄存器前边要加上%,立即数前要加上$
    Intel:没有这方面的要求
  3. 内存单元操作数
    Intel:基地址使用[]
    AT&T: 基地址使用()
    比如:intel中 mov ax,[bx]
    AT&T中 movl (%eax),%ebx
  4. 操作码的后缀
    AT&T中操作码后面有一个后缀字母:“l” 32位,“w” 16位,“b” 8位
    Intel却使用了在操作数前面加dword ptr, word ptr, byte ptr的格式
    例如:mov al,bl (Intel)
    movb %bl %al (AT&T)
  5. AT&T中跳转指令标号后的后缀 表示跳转方向,“f”表示向前,“b”表示向后

下面我们重点分析红框中的代码:

movl $0x3 ,0x1c(%esp):将3赋给i,即i=3
mov 0x1c(%esp) ,%eax:将esp中的i放到eax中
add %eax ,%eax:进行i+i操作,即3+3
addl 0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(++i)
add 0x1c(%esp),%eax:将eax中i+i的结果6,加上++i之后的i,即6+4=10
addl 0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(i++)
addl 0x1 ,0x1c(%esp):对i进行加1操作,即表达式中的(i++)

至此关键代码已经分析完成,由此可见我们之前对gcc编译器上的分析是正确的。

3.3、vs编译器上的分析

(i++)+(i++)+(++i) <=>(++i)+i+i; i++; i++;即如果表达式中含有前置++i,首先执行++i操作;表达式中的i++,一律换成i,然后执行加法操作;最后在进行i++操作。

这样的话上面的代码就可以很好的理解而来,即首先执行++i,i变为4了;然后进行i+i+i=4+4+4;i++,i++。

其实对于VS/VC2010编译器中的可以总结为:当用于四则运算时,前置++/–的运算优先级最高,后置++/–的运算优先级最小,其它的居中。(跟你书上看到是不是不同!)

3.4、分析VS编译之后的汇编代码

用W32Dasm反汇编vs编译生成的exe文件,追踪代码。我们可以看到如下图所示的代码:
这里写图片描述

反汇编后的代码

下面重点分析一下框中代码:

mov [ebp-08],3:将3赋给i,即i=3
mov eax,dword ptr [ebp-08]:将ebp中的i的值放到eax中,是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。dword ptr表示这是一个双字指针,即所要寻址的数据是一个双字(4字节)
add eax,1:对eax中的i进行加1操作
mov dword ptr [ebp-08] ,eax:将eax中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果
mov ecx,dword ptr [ebp-08]:将ebp中的i放到ecx中
add ecx,dword ptr [ebp-08]:将ebp中的值加上i,即4+4
add ecx,dword ptr [ebp-08]:将ebp中的值加上i,即4+4+4
mov dword ptr [ebp-14],ecx:将ecx中的值赋给j
mov edx,dword ptr [ebp-08]:将i放到edx中
add edx,1:对edx中的i进行加1操作
mov dword ptr [ebp-08] ,edx:将edx中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果
mov eax,dword ptr [ebp-08]:将i放到eax中
add eax,1:对eax中的i进行加1操作
mov dword ptr [ebp-08] ,eax:将eax中的i赋给ebp中i,即将i加1之后的值赋给i,也即达到i=i+1的效果

至此,上面表达式的关键运算部分已经分析完成。从这里可以知道,上面我们地VS编译器的分析是正确的。

4、发散思维

可以说通过上面那么篇幅的介绍,我们对涉及前置++和后置++的加法运算表达式的计算过程有了一个清楚的认识,下面就我们发散一下我们的思维,释放我们的能量。

4.1、思维放射

您看下面的代码会输出什么,现在知道了吧!

#include <stdio.h>
#include <stdlib.h>int main(int argc,char** argv)
{int i=3,j=3,k=3,l=3,m=3,n=3,result1,result2,result3,result4,result5,result6;result1=(++i)+(++i);printf("i = 3\n");printf("result1= (++i)+(++i) = %d\n\n",result1);result2=(j++)+(j++);    printf("j = 3\n");printf("result2= (j++)+(j++) = %d\n\n",result2);result3=(++k)+(++k)+(++k);printf("k = 3\n");printf("result3= (++k)+(++k)+(++k) = %d\n\n",result3);result4=(++l)+(++l)+(l++);printf("l = 3\n");printf("result4= (++l)+(++l)+(l++) = %d\n\n",result4);result5=(m++)+(m++)+(m++);printf("m = 3\n");printf("result5=(m++)+(m++)+(m++) = %d\n\n",result5);result6=(n++)+(++n)+(n++);printf("n = 3\n");printf("result6=(n++)+(++n)+(n++) = %d\n\n",result6);exit(0);
}

请不看结果先自己分析一下,然后和结果对比!

4.2、VS的输出

运行环境:Win7+VS2005 or VS2010,输出如下图所示:

image

4.3、GCC的输出

运行环境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,运行结果如下:

image

根据前面我们挖掘到的规则,我们可以得到result3之外所有其它答案。最后,还有一点要说明的是:gcc中的加法运算表达死中,是按照从左到右按顺序,如果运算符两边有++i操作数,就先进行++i操作,然后进行加法运算;vs中的加法运算表达式中,则不一样,只要表达式中有++i操作数,就要先计算,最后才是进行加法运算。这也是为什么result3不同的原因!加法运算可以扩展到减法、乘法、除法运算和前置–、后置–。但是如果是四则混合运算还要考虑加、减、乘、除的优先级问题。

5、感慨

通过这么多分析,我们可以算得上是对涉及++、–的运算表达式计算过程有了透彻理解!我在挖掘这个计算过程的路上,可是化了不少功夫也在刚开始分析汇编代码时遇到了一些困难,但这颗求知的心,推动着我坚持要去弄清楚它!最后我想说:请不要写这种语句!理由很简单,它既不好理解又不好维护,最重要的是它的结果会因编译器的不同而不同。

作者:吴秦
出处:http://www.cnblogs.com/skynet/
本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/526184.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

有趣的for循环

#include <stdio.h> int main() {int i 0;for(i 0, printf(" First %d", i);i<10, printf(" Second %d", i);i, printf(" Third %d", i)){printf(" Fourth %d", i);}system("pause");return 0; } 这段代码会…

重载函数编译后的新名字

我们都知道很多语言都支持函数重载&#xff0c;那么编译器是怎么处理编译后它们的命名冲突的呢&#xff1f; 1、先给出几个正确的重载函数&#xff1a; #include <iostream> using namespace std;int Add(int a, int b) {return a b; }double Add(double a, double b)…

初识C++之函数重载

最近开始学习C&#xff0c;了解到它在C语言没有的一个特性 – 函数重载&#xff0c;这一特性使得c的函数数量得以减少&#xff0c;减小了对名字空间的污染&#xff0c;另外对程序的可读性也有很大帮助。 那么c的函数重载这一特性是怎么实现的&#xff0c;为什么不会发生命名冲…

const在C与C++中的区别

1、const用于C –> 运行时常量 a. 修饰变量 修饰变量为常变量&#xff08;只读变量&#xff09; const int i 10; 此时i还是一个变量&#xff0c;不信你可以把它用来当作定义一个数组时的数组长度&#xff0c;一定会报错。不过它具有一种常属性&#xff0c;它的值一直都…

乱入的'\0'

看这个题之前&#xff0c;先来回忆一下strlen函数的工作机制&#xff1a; strlen所作的仅仅是一个计数器的工作&#xff0c;它从内存的某个位置&#xff08;可以是字符串开头&#xff0c;中间某个位置&#xff0c;甚至是某个不确定的内存区域&#xff09;开始扫描&#xff0c;…

强迫症的自我恢复

怎样克服强迫症&#xff1f;这是很多的人比较关注的一个问题。强迫症给人们的工作和生活带来很严重的影响&#xff0c;如果不及时进行治疗&#xff0c;对人的健康损害是很大的。那么怎样克服强迫症呢&#xff1f;下面就介绍一些怎样克服强迫症的调试方法&#xff0c;希望能帮助…

如何克服拖延症

拖延几乎成为现代人的通病&#xff0c;“先放一下&#xff0c;待会再行动”成为拖延者最大的思想毒瘤&#xff0c;如何积极地克服拖延症&#xff0c;已经成为了一个大课题。 1、正视自己的拖延症。 要改掉拖延症坏毛病&#xff0c;首先就是要正确认识到拖延症的危害&#xff…

初识C++之指针与引用

1、What a. 指针&#xff1a; 指针可以看做是一个特殊的变量&#xff0c;它是用来存放变量的地址值的。 b. 引用 引用的话&#xff0c;可以看做是给变量起的一个别名&#xff0c;而不是定义一个新变量&#xff0c;它与那个变量的本质是相同的&#xff0c;内容与地址都是一样…

初识C++之剖析const与#define

1、 编译器处理方式不同   #define是一个宏定义命令&#xff0c;它是在预处理阶段就进行了替换&#xff1b;   const修饰的是一个编译时常量&#xff0c;它是在编译阶段处理的。 2、 类型和安全检查不同   #define定义的标识符仅仅是产生文本替换&#xff0c;不管内容…

初识C++之运算符重载

C里面有一个叫作运算符重载的特性&#xff0c;它其实是基于函数实现的&#xff0c;下面就来介绍一下运算符重载。 1、What  C中预定义的运算符的操作对象只能是基本数据类型。但实际上&#xff0c;对于许多用户自定义类型&#xff08;例如类&#xff09;&#xff0c;也需要类…

对堆栈的认识

什么是堆和栈&#xff0c;它们在哪儿&#xff1f; 问题描述 编程语言书籍中经常解释值类型被创建在栈上&#xff0c;引用类型被创建在堆上&#xff0c;但是并没有本质上解释这堆和栈是什么。我仅有高级语言编程经验&#xff0c;没有看过对此更清晰的解释。我的意思是我理解什…

Waiting For Debugger

最近楼主在使用手机上的邮政银行时&#xff0c;总是打不开&#xff0c;要强制关闭&#xff0c;给我一个“Waiting For Debugger”的提示&#xff0c;相信朋友们应该遇到过类似的问题&#xff0c;当然这里不一定就是邮政银行出问题&#xff0c;可能是你手机里的任意一款软件&…

防止头文件重复包含之pragma once与#ifndef

在我们自己编写 C/C的头文件时&#xff0c;可能会忽略一点&#xff1a;用一些处理机制来避免头文件的重复包含&#xff0c;因为头文件的内容在预编译时是把头文件的内容完全拷贝到引入的地方替换头文件的包含命令&#xff0c;而包含的头文件可能有包含很多内容&#xff0c;所以…

初识C++之继承

1、何为继承 C中所谓继承&#xff0c;就是在一个已存在类的基础上创建一个新的类&#xff0c;新类获得已存在类的部分特性&#xff08;为什么是部分特性&#xff0c;后面会讲到&#xff09;。已存在类被称为基类&#xff08;Base Class&#xff09;或父类&#xff08;Father Cl…

初识C++之函数重载、重写、重定义的区别

在C的学习中&#xff0c;慢慢接触了一些很容易混淆的名词&#xff0c;今天就来剖析几个容易混淆的名词。 1、函数重载   重载函数是函数的一种特殊情况&#xff0c;为方便使用&#xff0c;C允许在同一范围中声明几个功能类似的同名函数&#xff0c;但是这些同名函数的形式参…

初识C++之封装

学习C不得不说到C的三大特性&#xff1a;封装、继承、多态&#xff0c;今天就先来剖析一下他的封装性。 1、什么是封装   封装就是将抽象得到的数据和行为&#xff08;或功能&#xff09;相结合&#xff0c;形成一个有机的整体&#xff0c;也就是将数据与操作数据的源代码进…

初识C++之虚函数

1、什么是虚函数   在基类中用virtual关键字修饰&#xff0c;并在一个或多个派生类中被重新定义的成员函数&#xff0c;用法格式为&#xff1a;   virtual 函数返回类型 函数名&#xff08;参数表&#xff09;    {     函数体    }    虚函数是实现多态性…

初识C++之多态

多态性是将接口与实现进行分离&#xff1b;用形象的语言来解释就是实现以共同的方法&#xff0c;但因个体差异&#xff0c;而采用不同的策略。 1、什么是多态   多态&#xff08;Polymorphism&#xff09;按字面的意思就是“多种状态”。在面向对象语言中&#xff0c;接口的…

从尾到头打印单链表(C语言)

void PrintListTailToHead(PSListNode pHead) {if (NULL ! pHead){//递归实现PrintListTailToHead(pHead->pNextNode);printf("%d ", pHead->data);} } 递归方式很容易就能实现。

删除一个无头单链表的非尾节点(C语言)

void DelNotTailNode(PSListNode pos) {PSListNode pNode NULL;assert(pos);if (NULL pos->pNextNode){return;}else{DataType temp 0;//交换pos和pos->pNextNode的数据&#xff08;相当于交换了两个结点的位置&#xff09;&#xff0c;使问题转换为删除pos指向的结点…