i++和++i作为参数时的编译器处理方式分析~

简介: 【补充声明】此文完成于几年前回答 BCCN 论坛的网友提问,就问题本身而言,对于这个问题似乎是没必要深究的,因为这种代码在读取一个变量的值的过程中反复尝试修改它的值,其结果依赖编辑器的实现。这种代码当然也是不可能在现实应用中出现的。

【补充声明】此文完成于几年前回答 BCCN 论坛的网友提问,就问题本身而言,对于这个问题似乎是没必要深究的,因为这种代码在读取一个变量的值的过程中反复尝试修改它的值,其结果依赖编辑器的实现。这种代码当然也是不可能在现实应用中出现的。不过作为一个问题,如果他一定要问,某编译器为什么会给出这样的结果,那就必须了解编译器对这个代码的编译结果细节,这就是本文所论述的东西。本文只涉及到了 TC2, VC6, VC2005 几种编译器。而且后两者应该使用的 WIN32 DEBUG,通常 Release 版本和 Debug 版本是一种在运行结果表现上的等效关系,对这个具体问题在当时我并未有精力再去细分。此次更新编辑顺便修改了原文中的个别错别字。原文中的术语“堆栈”修改为“栈”。原文中的一些中间结论不够准确,但暂未删除,已经用删除线做了处理。

    ---- hoodlum1980 ,2012年9月10日。


 

首先我们来看一下这个问题的提出,来自于一个网友的提问:
http://bbs.bccn.net/thread-200774-1-1.html
----------------------------------------------------------------------------------------------------------

求教大家,简单问题,但为什么是这样的结果?(vc6.0)

很简单的程序
void main()
{
     int i=8;
     printf("%d,%d,%d,%d\n", ++i, --i, i++, i--);
}
但是结果为(8 7 8 8)无论是从左到右顺序求值还是从右到左顺序求值都不应该是这个结果吧?
我觉得从左到右应该是(9 8 8 9 )从右到左是(8 7 7 8),
是我的错还是编译器的原因?如果是从右到左顺序求值,为什么结果不是(8 7 7 8)而是(8 7 8 8)
请大家指点一下!
[ 本帖最后由 默默无纹 于 2008-2-24 21:04 编辑 ]
-----------------------------------------------------------------------------------------------------------
    在这里我使用了VS.NET2005编译的结果是: 8,8,7,8。用TC2.0编译的结果是:8,7,7,8。VC6.0我没有安装,所以没有试过,也没办法分析。
    这里我们可以看到,由不同的编译器产生了不同结果,可见这个问题是依赖编译器的理解和实现的。换句话说,对于 i++ 和 ++i 的处理在这里是有歧义的,当然在自己应用中我相信也不会有任何人写出这样的代码。但是作为一个问题,我们有必要分析一下不同编译器究竟如何理解i++和++i操作符的。

        我们在学习C的时候,应该已经大概知道了 i++ 和 ++i 两者的区别,即“++”符号在 i 之前还是之后,决定了 i 自增操作和他的语句的执行顺序的关系。即i++,理解为i在其语句中取原始值,++i在其语句中取自增后的新值。这一点是毫无疑义的。但是问题在于,网友的问题中又涉及到了 i++,++i 在作为参数时候的处理,所以这时候我们就会感到困惑,i++ 和 ++i 在作为参数的时候,和进入栈的顺序之间有何关系呢?根据前面的实验,可见TC2.0和VS.net2005的处理不同,可见两者对其处理不同,那么造成这种不同的结果的原因是什么呢?我们从代码上无法看到差异,因此我们必须看汇编语言才能知晓,编译器到底把我们的代码翻译成了什么样子。下面我采用IDA反汇编编译器把生成的.exe文件,结果如下:
 
VS.NET 2005的代码
                                     mov     [ebp+var_8], 8      //i=8
.
text:004113E5                 mov     eax, [ebp+var_8]    //  
.
text:004113E8                 mov     [ebp+var_D0], eax   //i--之前把i的值保存到,temp[0]=8   
.
text:004113EE                 mov     ecx, [ebp+var_8]     
.
text:004113F1                 sub     ecx, 1               //i--,(从右向左数第一个参数)
.
text:004113F4                 mov     [ebp+var_8], ecx     //i=7
.
text:004113F7                 mov     edx, [ebp+var_8]     
.
text:004113FA                 mov     [ebp+var_D4], edx   //i++之前把i保存到:temp[1]=7 (这时候已经执行过i--了)
.
text:00411400                 mov     eax, [ebp+var_8]    //i++,(从右向左数第二个参数) 
.
text:00411403                 add     eax, 1
.
text:00411406                 mov     [ebp+var_8], eax    //i=8
.
text:00411409                 mov     ecx, [ebp+var_8]    
.
text:0041140C                 sub     ecx, 1               //--i, 无需保存修改前的值,直接改变实参i
.
text:0041140F                 mov     [ebp+var_8], ecx    //i=7
.
text:00411412                 mov     edx, [ebp+var_8]
.
text:00411415                 add     edx, 1              //++i,
.
text:00411418                 mov     [ebp+var_8], edx    //i=8
.
text:0041141B                 mov     esi, esp
.
text:0041141D                 mov     eax, [ebp+var_D0]   //压栈temp[0]=8
.
text:00411423                 push    eax
.
text:00411424                 mov     ecx, [ebp+var_D4]   //压栈temp[1]=7
.
text:0041142A                 push    ecx
.
text:0041142B                 mov     edx, [ebp+var_8]    //压栈i=8
.
text:0041142E                 push    edx
.
text:0041142F                 mov     eax, [ebp+var_8]    //压栈i=8
.
text:00411432                 push    eax
.
text:00411433                 push    offset aDDDD    ; "%d,%d,%d,%d\n"  //压栈字符串"%d,%d,%d,%d\n"的地址   
.text:00411438                 call    ds:printf            //调用打印函数,输出8,878
.
text:0041143E                 add     esp, 14h
.
text:00411441                 cmp     esi, esp
.
text:00411443                 call    sub_41113B
.
text:00411448                 mov     esi, esp

    可见,++i和--i执行的时候直接改变了i的值,而i++和i--必须在所在的这个语句执行后才能改变i的值,所以i++作为参数时,实际上是这样的过程,
         printf("%d",i++);


    相当于下面的语句:
         int temp=i;
           i = (i+1);
         printf("%d",temp);

 

    因此上面的代码可以翻译为:
        int i=8;
          printf("%d,%d,%d,%d",++i,--i,i++,i--);

 

    因此可以翻译为下面的等效代码:
    i=8;
    temp0=i; //temp0=8;
    i--;     //7
    temp1=i; //temp1=7
    i++;     //8
    --i;     //7
    ++i;     //i=8
    printf("%d,%d,%d,%d",i,i,temp1,temp0);   


    所以打印结果是8,8,7,8

---------------------------------------------------------------------------------------------------------
    我们再看在TC2.0下的反汇编代码:
TC2.0下面的反汇编代码
----------------------TC2.0反汇编结果--------------------
0:01FA sub_1FA         proc near               ; CODE XREF: start+11A
目录
相关文章
|
2月前
|
存储 编译器 C语言
【C/C++ 函数返回的奥秘】深入探究C/C++函数返回:编译器如何处理返回值
【C/C++ 函数返回的奥秘】深入探究C/C++函数返回:编译器如何处理返回值
130 3
|
2月前
|
存储 算法 编译器
C/C++编译器局部优化技术:局部优化是针对单个函数或基本块进行的优化
C/C++编译器局部优化技术:局部优化是针对单个函数或基本块进行的优化
37 0
|
10月前
|
存储 C语言 C++
c++类型转换与RTTI运行阶段类型识别
我们都知道C++完全兼容C语言,C语言的转换方式很简单,可以在任意类型之间转换,但这也恰恰是缺点,因为极其不安全,可能不经意间将指向const对象的指针转换成非const对象的指针,可能将基类对象指针转成了派生类对象的指针,这种转换很容易出bug,需要严格审查代码才能消除这种隐患,但是C这种转换方式不利于我们审查代码,且程序运行时也可能会出bug。
79 2
|
监控 Cloud Native Java
字节码编程,Byte-buddy篇二《监控方法执行耗时动态获取出入参类型和值》
在前面的ASM、Javassist 章节中也有陆续实现过获取方法的出入参信息,但实现的方式还是偏向于字节码控制,尤其ASM,更是需要使用到字节码指令将入参信息压栈操作保存到局部变量用于输出,在这个过程中需要深入了解Java虚拟机规范,否则很不好完成这一项的开发。但!ASM也是性能最牛的。其他的字节码编程框架都是基于它所开发的。
623 0
字节码编程,Byte-buddy篇二《监控方法执行耗时动态获取出入参类型和值》
|
缓存 自然语言处理 前端开发
【Java原理探索】「编译器专题」重塑认识Java编译器的执行过程(消除数组边界检查+公共子表达式)!
【Java原理探索】「编译器专题」重塑认识Java编译器的执行过程(消除数组边界检查+公共子表达式)!
133 0
【Java原理探索】「编译器专题」重塑认识Java编译器的执行过程(消除数组边界检查+公共子表达式)!
|
编译器
由编译器特别支持的包装
由编译器特别支持的包装
44 0
|
Java
JVM参数调优基础-参数的类型详解(上)
JVM参数调优基础-参数的类型详解(上)
131 0
JVM参数调优基础-参数的类型详解(上)
|
XML 前端开发 安全
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
127 0
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
C++RTTI(运行时类型识别)与异常处理解析
C++RTTI(运行时类型识别)与异常处理解析
104 0
|
编译器 Linux C语言
关于 QtCreartor编写纯C++程序调用不到C++某些标准库和枚举以及运行错误 的解决方法
关于 QtCreartor编写纯C++程序调用不到C++某些标准库和枚举以及运行错误 的解决方法
关于 QtCreartor编写纯C++程序调用不到C++某些标准库和枚举以及运行错误 的解决方法