你真的理解了C语言++和——运算符么?
这个主题对于刚开始学习C语言时可能会觉得很简单啊,那好你告诉我下面几个题目的输出是什么,你要是能说对,并且说出为什么,那你就可以不用往下看了
int i = 0,j = 0;
1、j = (i++)+(i++)+(i++); //而不是j = i++i++i++;
2、j = (++i)+(++i)+(++i); //而不是j = ++i++i++i;
3、j = ++i+++i+++i;
4、j = i+++j;
下面我们一题一题来进行分析
首先:我们来分析1和2两个题,这里需要稍微懂点汇编知识,因为C语言是分析不出来的,所以只能从汇编的角度去分析
但是不懂汇编语言也不用怕,因为我也不懂汇编语言,用到我都是百度查询,有的也不是很懂,下面我在VS2010里面编写上面代码
#include <stdio.h>
#include <string.h>
int main()
{
int i = 0,j = 0;
//第一题
j = (i++)+(i++)+(i++);
//下面我们自己分析下认为应该是
//j= 0 + 1 + 2; i = 3
printf("i = %d,j = %d",i,j); //实际输出i = 3 j = 0
//第二题
j = (++i)+(++i)+(++i);
//下面我们自己分析下认为应该是
//j = 4 + 5 + 6; i = 6
printf("i = %d,j = %d",i,j); //实际输出i = 6 j = 18
//第三题
//j = ++i+++i+++i; //编译出错
//printf("i = %d,j = %d",i,j);
//第四题
j = i+++j;
printf("i = %d,j = %d",i,j);
system("pause");
return 0;
}
再看下对应反汇编代码
#include <stdio.h>
#include <string.h>
int main()
{
003A34A0 push ebp
003A34A1 mov ebp,esp
003A34A3 sub esp,0D8h
003A34A9 push ebx
003A34AA push esi
003A34AB push edi
003A34AC lea edi,[ebp-0D8h]
003A34B2 mov ecx,36h
003A34B7 mov eax,0CCCCCCCCh
003A34BC rep stos dword ptr es:[edi]
int i = 0,j = 0;
003A34BE mov dword ptr [i],0
003A34C5 mov dword ptr [j],0
//第一题
j = (i++)+(i++)+(i++);
003A34CC mov eax,dword ptr [i]
003A34CF add eax,dword ptr [i]
003A34D2 add eax,dword ptr [i]
003A34D5 mov dword ptr [j],eax
003A34D8 mov ecx,dword ptr [i]
003A34DB add ecx,1
003A34DE mov dword ptr [i],ecx
003A34E1 mov edx,dword ptr [i]
003A34E4 add edx,1
003A34E7 mov dword ptr [i],edx
003A34EA mov eax,dword ptr [i]
003A34ED add eax,1
003A34F0 mov dword ptr [i],eax
//下面我们自己分析下认为应该是
//j= 0 + 1 + 2; i = 3
printf("i = %d,j = %d",i,j); //实际输出i = 3 j = 0
003A34F3 mov esi,esp
003A34F5 mov eax,dword ptr [j]
003A34F8 push eax
003A34F9 mov ecx,dword ptr [i]
003A34FC push ecx
003A34FD push offset string "i = %d,j = %d" (3A5A00h)
003A3502 call dword ptr [__imp__printf (3A82B0h)]
003A3508 add esp,0Ch
003A350B cmp esi,esp
003A350D call @ILT+295(__RTC_CheckEsp) (3A112Ch)
//第二题
j = (++i)+(++i)+(++i);
003A3512 mov eax,dword ptr [i]
003A3515 add eax,1
003A3518 mov dword ptr [i],eax
003A351B mov ecx,dword ptr [i]
003A351E add ecx,1
003A3521 mov dword ptr [i],ecx
003A3524 mov edx,dword ptr [i]
003A3527 add edx,1
003A352A mov dword ptr [i],edx
003A352D mov eax,dword ptr [i]
003A3530 add eax,dword ptr [i]
003A3533 add eax,dword ptr [i]
003A3536 mov dword ptr [j],eax
//下面我们自己分析下认为应该是
//j = 4 + 5 + 6; i = 6
printf("i = %d,j = %d",i,j); //实际输出i = 6 j = 18
003A3539 mov esi,esp
003A353B mov eax,dword ptr [j]
003A353E push eax
003A353F mov ecx,dword ptr [i]
003A3542 push ecx
003A3543 push offset string "i = %d,j = %d" (3A5A00h)
003A3548 call dword ptr [__imp__printf (3A82B0h)]
003A354E add esp,0Ch
003A3551 cmp esi,esp
003A3553 call @ILT+295(__RTC_CheckEsp) (3A112Ch)
//第三题
//j = ++i+++i+++i; //编译出错
//printf("i = %d,j = %d",i,j);
//第四题
j = i+++j;
003A3558 mov eax,dword ptr [i]
003A355B add eax,dword ptr [j]
003A355E mov dword ptr [j],eax
003A3561 mov ecx,dword ptr [i]
003A3564 add ecx,1
003A3567 mov dword ptr [i],ecx
printf("i = %d,j = %d",i,j);
003A356A mov esi,esp
003A356C mov eax,dword ptr [j]
003A356F push eax
003A3570 mov ecx,dword ptr [i]
003A3573 push ecx
003A3574 push offset string "i = %d,j = %d" (3A5A00h)
003A3579 call dword ptr [__imp__printf (3A82B0h)]
003A357F add esp,0Ch
003A3582 cmp esi,esp
003A3584 call @ILT+295(__RTC_CheckEsp) (3A112Ch)
system("pause");
003A3589 push offset string "pause" (3A57B0h)
003A358E call @ILT+445(_system) (3A11C2h)
003A3593 add esp,4
return 0;
003A3596 xor eax,eax
}
首先我们来分析第1个题:j = (i++)+(i++)+(i++);
前面一些初始化我就不讲了,我们直接对这句汇编进行分析
//第一题
j = (i++)+(i++)+(i++);
003A34CC mov eax,dword ptr [i]
003A34CF add eax,dword ptr [i]
003A34D2 add eax,dword ptr [i]
003A34D5 mov dword ptr [j],eax
003A34D8 mov ecx,dword ptr [i]
003A34DB add ecx,1
003A34DE mov dword ptr [i],ecx
003A34E1 mov edx,dword ptr [i]
003A34E4 add edx,1
003A34E7 mov dword ptr [i],edx
003A34EA mov eax,dword ptr [i]
003A34ED add eax,1
003A34F0 mov dword ptr [i],eax
这里面实际就用了两条汇编指令mov和add:
mov指令:数据传输指令,用C语言的话讲就是赋值指令‘=’比如:mov AL,20H 相当于C语言就是 AL = 20H AL是寄存器
add指令:加法指令,用C语言的话讲就是一个复合赋值运算符指令‘+=’比如:add AX,8H 相当于C语言就是 AX += 8,再简单点就是AX = AX + 8
eax,ebx,ecx,edx,esi,edi,ebp,esp:这些都是通用寄存器,用C语言的话讲就是全局变量(但是这些寄存器又有特殊用处,这里不详细讲,感兴趣可以百度)
dword:双字 就是四个字节
ptr:pointer 即指针
[]:里的数据是一个地址值,这个地址值指向一个双字型数据
比如:mov eax,dword ptr [12345678];把内存地址12345678中的双字型(32位)数据赋给eax,相当于C语言就是 exa = *12345678;
mov eax,dword ptr [i] ;把内存地址&i中的双字型(32)数据赋给exa,相当于C语言就是eax = i;
好了知道这些汇编指令就可以分析了
003A34CC mov eax,dword ptr [i] ; 相当于C语言中 eax = i;因为i = 0,所以eax = 0
003A34CF add eax,dword ptr [i] ; 相当于C语言中 eax = eax + i; 因为i = 0,eax = 0,所以 eax = eax + i = 0
003A34D2 add eax,dword ptr [i] ; 相当于C语言中 eax = eax + i; 同上,eax = 0
003A34D5 mov dword ptr [j],eax ; 相当于C语言中 j = eax; 因为eax = 0,所以 j = 0
所以前四条汇编指令执行完,j = 0,再往下面分析
003A34D8 mov ecx,dword ptr [i] ; 相当于C语言中 ecx = i;因为i = 0,所以ecx = 0
003A34DB add ecx,1 ; 相当于C语言中 ecx = ecx + 1 ; 所有ecx = 1
003A34DE mov dword ptr [i],ecx ; 相当于C语言中 i = ecx;所以i = 1
003A34E1 mov edx,dword ptr [i] ; 相当于C语言中 edx = i;因为i = 1,所以edx = 1
003A34E4 add edx,1 ; 相当于C语言中 edx = edx + 1 ; 所有edx = 2
003A34E7 mov dword ptr [i],edx ; 相当于C语言中 i = edx;所以i = 2
003A34EA mov eax,dword ptr [i] ; 相当于C语言中 eax = i;因为i = 2,所以eax = 2
003A34ED add eax,1 ; 相当于C语言中 eax = eax + 1 ; 所有eax = 3
003A34F0 mov dword ptr [i],eax ; 相当于C语言中 i = eax;所以i = 3
所以通过上面分析,j = 0,i = 3;
这个分析完全和我们注释的分析是不一样的
好了我们在分析第2题(别忘了j = 0,i = 3)
j = (++i)+(++i)+(++i);
003A3512 mov eax,dword ptr [i] ; 相当于C语言中 eax = i;因为i = 3,所以eax = 3
003A3515 add eax,1 ; 相当于C语言中 eax = eax + 1 ; 所有eax = 4
003A3518 mov dword ptr [i],eax ; 相当于C语言中 i = eax;所以i = 4
003A351B mov ecx,dword ptr [i] ; 相当于C语言中 ecx = i;因为i = 4,所以ecx = 4
003A351E add ecx,1 ; 相当于C语言中 ecx = ecx + 1 ; 所有ecx = 5
003A3521 mov dword ptr [i],ecx ; 相当于C语言中 i = ecx;所以i = 5
003A3524 mov edx,dword ptr [i] ; 相当于C语言中 edx = i;因为i = 5,所以edx = 5
003A3527 add edx,1 ; 相当于C语言中 edx = edx + 1 ; 所有edx = 6
003A352A mov dword ptr [i],edx ; 相当于C语言中 i = edx;所以i = 6
003A352D mov eax,dword ptr [i] ; 相当于C语言中 eax = i;因为i = 6,所以eax = 6
003A3530 add eax,dword ptr [i] ; 相当于C语言中 eax = eax + i ; 所有eax = 12
003A3533 add eax,dword ptr [i] ; 相当于C语言中 eax = eax + i ; 所有eax = 18
003A3536 mov dword ptr [j],eax ; 相当于C语言中 j = eax;因为eax = 18,所以j = 18
通过上面分析,i = 6,j = 18
这个分析完全和我们注释的分析也是不一样的
好了我们接着分析第3题 (别忘了,i = 6)
j = ++i+++i+++i;
看看这个表达式是不是就是第2题的表达式去掉大括号啊,还真是啊
因为编译不通过,就没有办法通过反汇编分析了,所以只能从C语言角度分析了
分析过程:首先编译器读取第一个字符‘+,这时编译器可能认为这是一个加法,也可能认为是一个自增运算符,所以编译器还会往后面读取,再读取一个字符‘+’,这时编译器就可以判断出来了,这是一个自增运算符,而且后面肯定有一个变量在后面跟着,否则编译出错,所以再读取一个字符‘i’,总结前面就是执行了一个“++i”,然后往下分析,这时编译器往后面读取字符‘+’,这时编译器可能认为是加法,也可能认为是自增运算符,所以编译器还得往后面读取才能知道到底是什么字符,这时编译器再读取一个字符‘+’,这时编译器就能判断出来了,这是一个自增运算符,同时编译器也会报错,为什么会报错呢,因为前面++i,执行完是一个常数7,7后面又跟了自增运算符,相当于7++,这里肯定是错误的,因为自增或者自减运算符只能对变量执行,不能对常数,所以编译肯定报错的
上面编译器处理的方法叫做“贪心法”,编译器通过贪心法处理表达式中的子表达式
有人可能认为你这些都是你瞎猜的,谁知道你分析的对不对,又没有对应反汇编代码,所以我们在VS里面再加上一条语句
j = 7++;
看它是否和第3题的错误提示信息是否是一样,如果是一样的,就说明我们的分析是对的
看见没,是一样的错误信息,所以我们的分析完全正确,我同时也在ubuntu 10里面试了下,
gcc 提示信息:test.c:17: error: lvalue required as increment operand,中文意思:左值必须是一个变量操作数
讲的有点累了,说的也比较啰嗦,好了我们再分析下第4题(别忘了i = 6,j = 18)
j = i+++j;
这个表达式就会有两种结果:
第1种:j = (i++) + j;
第2种:j = i + (++j);
我们这次采用两种方法讲解:
第一种:直接从C语言用“贪心法”分析
第二种:从反汇编角度去分析
第一种:首先编译器读取i++,执行i++,然后在往后面读取字符‘+’这时编译认为可能是加法,也可能是自增运算符,所以还得往后面读取字符才能知道,再次读取一个字符‘j’,这时编译器就判断出来了,这是一个加法,所以编译器先执行,i++,然后在加上j,执行结果就是i = 7,j = 24,也就是它是按第1种情况执行的
第二种:从反汇编角度去分析
003A3558 mov eax,dword ptr [i] ; 相当于C语言中 eax = i;因为i = 6,所以eax = 6
003A355B add eax,dword ptr [j] ; 相当于C语言中 eax = eax + j ; 因为j = 18,所有eax = 24
003A355E mov dword ptr [j],eax ; 相当于C语言中 j = eax;所以j = 24
003A3561 mov ecx,dword ptr [i] ; 相当于C语言中 ecx = i;因为i = 6,所以ecx = 6
003A3564 add ecx,1 ; 相当于C语言中 ecx = ecx + 1 ; 所有ecx = 7
003A3567 mov dword ptr [i],ecx ; 相当于C语言中 i = ecx;所以i = 7
通过上面两种方法分析,i = 7,j = 24
两种分析方法里面具体执行细节还是不一样的,反汇编肯定是最详细的,最权威的,C语言还是不够详细的,比如这道题里面,执行i++时,表面感觉,结果肯定是6,执行完时i本身已经变成7了么?显然是看不出来的,只有通过反汇编我们才知道,实际i本身值没有变成7,而是最后才变成7的
这里好了,我们再硬插一道第5题(不然还得写一篇博客)
看见没,第五题就是b = b/*p;p是指向整型a的变量,但是你如果不注意,输入完毕,直接编译,编译出错
这时你在仔细回去看下代码,发现b = b/*p;后面语句都是绿色了,都注释掉了,这是为什么,其实这是因为编译器把/*p当成‘/*’注释符了,所以后面全都注释掉了,那难道就没有办法解决么?实际是可以解决的,你把除号后面加一个空格就可以了,b = b/ *p;
开始对上面进行总结:
1、++和--操作符在混合运算中的行为可能不同
2、++和--对应汇编指令不一定连续执行
3、在混合运算中,++和--的汇编指令可能被打断执行
4、编译器通过“贪心法”处理表达式中的子表达式
5、空格可以作为C语言中的一个完整符号的休止符
6、编译器读入空格后立即对之前读入的符号进行处理
这里面还有很多关于++,--的一些坑
比如:printf("%d,%d",i++,i++);
printf("%d,%d",i++,i);
你认为上面输出会是一样的么?
i = 1;
j = ++i+i+++i;
printf("j = %d",j); //j等于几,还是说编译出错
这些你都可以通过汇编去分析