browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

自增运算符的副作用

Posted by on 2008 年 03 月 12 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

好了,就从最为臭名昭著的“(++i) + (++i) + (++i)”开始吧。

C++代码
  1. int i = 4;   
  2. int a = (++i) + (++i) + (++i);  

题目要求是求a的值,多见于各种等级考试、期末考试的选择题。
显然,这道题的考点是前缀自增运算符。与之相似的还有后缀自增(减)或前后缀增减混合的情况。
一墙之隔,围城内外。在象牙塔外的世界,这个题目是最早遭到诟病者之一。因为,出题者所默认的程序运行环境是Turbo C,所以标准答案自然也就是TC的运行结果(有TC的朋友们不妨试一试,看看TC的结果是不是你那卷子上的标准答案)。而事实上,对于这个题目的结果,a的值是无法预期的——C/C++标准规定,三个++i的子表达式是没有求值顺序点的,同时它们又是有副作用的,因此语言本身并不能保证副作用的顺序。
眼见为实,让我们来看看三款不同编译器产生的代码吧。我为每个必要的细节加了注释,以便理解。

汇编代码
  1. ; Visual C++ 6.0 sp6:   
  2. mov [ebp+i], 4   ; i = 4   
  3. mov eax, [ebp+i]   
  4. add eax, 1       ; ++i, i == 5   
  5. mov [ebp+i], eax   
  6. mov ecx, [ebp+i]   
  7. add ecx, 1       ; ++i, i == 6   
  8. mov [ebp+i], ecx   
  9. mov edx, [ebp+i]   
  10. add edx, [ebp+i] ; 6 + 6 == 12   
  11. mov eax, [ebp+i]   
  12. add eax, 1       ; ++i, i == 7   
  13. mov [ebp+i], eax   
  14. add edx, [ebp+i] ; 12 + 7 == 19   
  15. mov [ebp+a], edx ; a = 19   
  16.   
  17. ; Visual Studio 2005:   
  18. mov [ebp+i], 4   ; i = 4   
  19. mov eax, [ebp+i]   
  20. add eax, 1       ; ++i, i == 5   
  21. mov [ebp+i], eax   
  22. mov ecx, [ebp+i]   
  23. add ecx, 1       ; ++i, i == 6   
  24. mov [ebp+i], ecx   
  25. mov edx, [ebp+i]   
  26. add edx, 1       ; ++i, i == 7   
  27. mov [ebp+i], edx   
  28. mov eax, [ebp+i]   
  29. add eax, [ebp+i] ; 7 + 7 == 14   
  30. add eax, [ebp+i] ; 14 + 7 == 21   
  31. mov [ebp+a], eax ; a = 21   
  32.   
  33. ; gcc.exe (GCC) 3.4.5 (mingw-vista special):   
  34. mov [ebp+i], 4      ; i = 4   
  35. lea eax, [ebp+i]   
  36. inc dword ptr [eax] ; ++i, i == 5   
  37. lea eax, [ebp+i]   
  38. inc dword ptr [eax] ; ++i, i == 6   
  39. mov eax, [ebp+i]   
  40. mov edx, [ebp+i]   
  41. add edx, eax        ; 6 + 6 == 12   
  42. lea eax, [ebp+i]   
  43. inc dword ptr [eax] ; ++i, i == 7   
  44. mov eax, edx   
  45. add eax, [ebp+i]    ; 12 + 7 == 19   
  46. mov [ebp+a], eax    ; a = 19  

——其实我大可不必列出如是这般冗长的汇编代码,而只需要一个a值结果的总结表格就可以说明问题。不过我还是选择了汇编语言,原因有二:第一,任何的砖家、叫兽告诉你的东西都远远不及最终生成的目标代码可靠;第二,使用汇编代码可以把自己伪装成高手,用来装B的效果肯定比简单的表格来得有效,何乐而不为哉。
装都装了,自然不怕遭雷劈。再来一个嵌入式设备上的代码,环境是eMbedded Visual C++ 4.0 sp4的ARMV4编译器:

汇编代码
  1. MOV R0, #4        ; i = 4   
  2. STR R0, [SP,#8+i]   
  3. LDR R1, [SP,#8+i]   
  4. ADD R0, R1, #1    ; ++i, i == 5   
  5. STR R0, [SP,#8+i]   
  6. LDR R1, [SP,#8+i]   
  7. ADD R0, R1, #1    ; ++i, i == 6   
  8. STR R0, [SP,#8+i]   
  9. LDR R1, [SP,#8+i]   
  10. ADD R0, R1, #1    ; ++i, i == 7   
  11. STR R0, [SP,#8+i]   
  12. LDR R1, [SP,#8+i]   
  13. LDR R0, [SP,#8+i]   
  14. ADD R2, R1, R0    ; 7 + 7 == 14   
  15. LDR R3, [SP,#8+i]   
  16. ADD R0, R2, R3    ; 14 + 7 == 21   
  17. STR R0, [SP,#8+a] ; a = 21  

相信到这里诸位都看到了,一个表达式在不同的编译器上会出现不同的结果——特别是微软的VC6和VS2005,一家产的编译器的结果也是不一样的。亦即是说,倘使你写下了诸如“(++i) + (++i) + (++i)”这样的代码,你得到的结果将是一个无法预期的结果,必须的。
末了,说点八卦的。很多程序员将这种病态、晦涩的编码方式归咎于谭浩强版的《C程序设计》,认为谭老爷子是这种学究代码的始作俑者。李马饶有兴致地考证了一番,发现谭老爷子在《C程序设计》(第二版)的第58~59页中对这种情况进行了讨论,并指出以下几点:

  1. 应该避免++/–的副作用可能产生的歧义性,建议将这样的表达式拆开写。
  2. 对于i+++j的情况,应使用括号来使代码明晰以避免误解,如(i++)+j或i+(++j)。
  3. “总之,不要写出别人看不懂的、也不知道系统会怎样执行的程序。”

窃为谭老爷子鸣不平啊。

掌掴学究欢迎提供各类素材,请致信titilima@163.com

订阅本站

4 Comments

  • At 2008.03.12 12:13, z said:

    反正使用中仅用到i++或者++i
    其他的就不用了,自己写着变态,看着变态,机器跑起来也变态。。。
    回头试试看symbian下运行是啥结果。。应该和gcc的是一样的。

    • At 2008.03.17 16:21, a said:

      李马大哥请问怎么用gcc逆成intel语法的汇编语法啊?难道是自己转的..
      感觉gas的语法要比intel微软的汇编语法好理解,把内存当作一个很大的数组, 8(%ebp),12(%ebp)便是第一第二个参数,mov指令也比较具体,movb,movl,movw 给出了参数的具体大小,貌似我也装了一下b,装就装了..
      昨天碰巧看了叫兽的cs解说,很好很叫兽

      • At 2008.03.17 16:31, 李马 said:

        To a:
        gcc应该是有生成汇编代码的选项,我采用的方法是你楼下的z编译好的exe,然后我使用IDA逆之。

        • At 2010.05.17 11:14, zjkl19 said:

          呵呵,半年前我看这篇文章一窍不通,现在学了汇编也开始学着装B了。
          tc2.0下源程序:
          int main()
          {
          int i = 4;
          int a = (++i) + (++i) + (++i);
          return 0;
          }
          debug 反汇编之关键代码:
          -u
          1589:01FA 55 PUSH BP
          1589:01FB 8BEC MOV BP,SP
          1589:01FD 83EC02 SUB SP,+02
          1589:0200 56 PUSH SI
          1589:0201 BE0400 MOV SI,0004 ;i=4
          1589:0204 46 INC SI ;++i,i==5
          1589:0205 46 INC SI ;++i,i==6
          1589:0206 46 INC SI ;++i,i==7
          1589:0207 8BC6 MOV AX,SI
          1589:0209 03C6 ADD AX,SI ;7+7=14
          1589:020B 03C6 ADD AX,SI ;14+7=21
          1589:020D 8946FE MOV [BP-02],AX ;i=21
          1589:0210 33C0 XOR AX,AX

          (Required)
          (Required, will not be published)