你真的理解了C语言++和——运算符么?

choupiaoyi 2018-10-20

这个主题对于刚开始学习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等于几,还是说编译出错

这些你都可以通过汇编去分析

你真的理解了C语言++和——运算符么?

相关推荐