本节必须掌握的知识点:
运算符
表达式
优先级
4.3.1 算术运算符
算术运算符
一元运算符:++、--。
二元运算符:+、-、*、/、%。
三元运算符:expr1 ? expr2 :expr3 。
■加减运算
a + b = a和b的和。
a - b = a和b的差。
■乘除运算符和取模运算符
a * b = a和b的乘积。
a / b = a除以b的商,商为整数,整数运算小数部分丢弃,只取整数部分。
a % b = a除以b得到的余数(取模),a和b必须都是整数 。
■除法运算的商和余数
整数运算,商取整数,余数为模
■除法运算的结果
进行除法运算的/运算符和%运算符的运算结果是依赖于编译器的。
●两个操作数都是正数时,不管哪种编译器,商和余数都是正数。
正 % 正 -> 正
●两个操作数中至少有一个为负数时,商和余数的结果取决于编译器。
负 % 负 -> 结果取决于编译器
正 % 负 -> 结果取决于编译器
负 % 正 -> 结果取决于编译器
下表列出了不同编译器除法运算的不同计算方法:
例: x = -22 ,y = -5 | x / y | 商为4,余数为-2 | |||
商为5,余数为3 | |||||
例: x = -22 ,y = +5 | x / y | 商为-4,余数为-2 | |||
商为-5,余数为3 | |||||
例: x = +22 ,y = -5 | x / y | 商为-4,余数为+2 | |||
商为-5,余数为-3 |
表4-2 不同编译器除法运算和取模运算结果
4.3.2 一元算术运算符-示例十三
一元算术运算符又被称为单目运算符。算术运算符中的前置 “++、--”和后置“++、--”算术运算符就是一元算术运算符。
示例十三
/*
一元运算符++、--
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int i = 3;//将3赋值给int型变量i
int j = 0;//将0赋值给int型变量j
j = i++;//相当于j = i;i++;
printf("i = %d,j = %d\n", i,j);//i = 4,j = 3
j = i--;//相当于j = i;i--;
printf("i = %d,j = %d\n", i, j);//i = 3,j = 4
j = ++i;//相当于++i;j = i;
printf("i = %d,j = %d\n", i, j);//i = 4,j = 4
j = --i;//相当于--i;j = i;
printf("i = %d,j = %d\n", i, j);//i = 3,j = 3
system("pause");
return 0;
}
●输出结果
i = 4,j = 3
i = 3,j = 4
i = 4,j = 4
i = 3,j = 3
■代码分析
C语言提供了增量和减量运算符++、--。又分为前置后后置两种方式。
前置:变量先加1或减1,再参与运算;
后置:先参与运算,再加1或减1;
举例
++a:a加1,然后在a所在的表达式中使用它的新值。
--b:a减1,然后在b所在的表达式中使用它的新值。
a++:在a所在的表达式中使用它的当前值,然后再加1。
b--:在b所在的表达式中使用它的当前值,然后再减1。
接下来剖析示例十三代码:
int i = 3; //将3赋值给int型变量i。
int j = 0; //将0赋值给int型变量j。
j = i++; //相当于j = i;i++; //这一行代码是:先将i的值赋给j,i再自加1。
详细解说:j = i;此时i = 3; j = 3;赋值给j之后,i++相当于,i = i + 1;此时i = 4;所以这行代码执行完 i = 4; j = 3。
j = i--; //相当于j = i;i--; //这一行代码是:先将i的值赋给j,i再自减1。
详细解说:j = i;此时i = 4; j = 4;赋值给j之后,i--相当于,i = i - 1;此时i = 3;所以这行代码执行完 i = 3; j = 4。
j = ++i; //相当于++i;j = i; //这一行代码是:i先自加,再赋值给j;
详细解说:首先++i;相当于i = 1+i;此时i = 4;赋值给j,此时j =4;所以这行代码执行完 i = 4; j = 4。
j = --i; //相当于--i;j = i; //这一行代码是:i先自减,再赋值给j;
详细解说:首先--i;相当于i = i - 1;此时i = 3;赋值给j,此时j =3;所以这行代码执行完 i = 3; j = 3。
接下来我们换一种实现方式。
实验三十五:前置和后置++、--
VS中新建项目4-3-2.c。代码如下:
/*
一元运算符++、--
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int i = 3;//将3赋值给int型变量i
int j = 0;//将0赋值给int型变量j
j = i;
printf("i = %d,j = %d\n", i++, j);//i = 3,j = 3
j = i;
printf("i = %d,j = %d\n", i--, j);//i = 4,j = 4
j = i;
printf("i = %d,j = %d\n", ++i, j);//i = 4,j = 3
j = i;
printf("i = %d,j = %d\n", --i, j);//i = 3,j = 4
system("pause");
return 0;
}
输出结果:
i = 3,j = 3
i = 4,j = 4
i = 4,j = 3
i = 3,j = 4
第一个printf语句,先输出变量i和j的值,然后将变量i加一;
第二个printf语句,先输出变量i和j的值,然后将变量i减一;
第三个printf语句,先将变量i的值加一,然后再输出变量i和j的值;
第四个printf语句,先将变量i的值减一,然后再输出变量i和j的值;
总结
1.在一个语句中增量和减量一个变量时,前置形式和后置形式具有相同的效果。只要当变量出现在较大的表达式环境中时,前置增量和后置增量才会具有不同的效果。
i++;等价于++i;
j--;等价于--j;
2.试图在非简单变量名的表达式中使用增量运算符或减量运算符,例如++(x + 1)是错误的。
3.ANSI标准通常不会指出对运算符的操作数进行求值的顺序。因此在被增加或减少的变量不止一次地出现在语句中,程序员应该避免使用带有增量运算符或减量运算符的语句。
4.在条件表达式中,除非你确定是想要的结果,否则不建议使用。例如:
While(i++ > 10){
…
}
等价于:
While(i > 10){
…
i++
}
建议初学者使用第二种写法,减少不必要的错误。
【注】循环语句我们将在第七章详细讲述。
■汇编解析
●汇编代码
;C标准库头文件和导入库
include vcIO.inc
.data
i sdword ?
j sdword ?
.const
szMsg db "i = %d,j = %d",0dh,0ah,0
.code ;代码区
start:
mov sdword ptr i,3;将3存入变量i地址处
mov sdword ptr j,0;将0存入变量j地址处
mov eax,i;将变量i地址处的值存入eax寄存器
mov j,eax;将eax寄存器的值存入变量j地址处
inc eax;eax寄存器值加1
mov i,eax;将加1后eax寄存器的值存入变量i地址处
invoke printf,offset szMsg,i,j ;输出变量i和j地址处的值 ;
mov eax,i
mov j,eax
dec eax
mov i,eax
invoke printf,offset szMsg,i,j
;
mov eax,i
inc eax
mov i,eax
mov j,eax
invoke printf,offset szMsg,i,j
;
mov eax,i
dec eax
mov i,eax
mov j,eax
invoke printf,offset szMsg,i,j
;
invoke _getch
ret
end start
输出结果:
i = 4,j = 3
i = 3,j = 4
i = 4,j = 4
i = 3,j = 3
上述汇编代码非常清晰的表述了指令的执行顺序,每一条语句均对应一条机器指令(invoke高级汇编伪指令除外),不存在任何疑义。这是C语言语句无法做到的。因此,当我们每当对C语言的执行有疑惑的时候,不要忘了看一下翻译为汇编语句后的执行结果。
●反汇编代码
int i = 3;//将3赋值给int型变量i
013B1838 mov dword ptr [i],3
int j = 0; //将0赋值给int型变量j
013B183F mov dword ptr [j],0
j = i++; //相当于j = i;i++;
013B1846 mov eax,dword ptr [i]
013B1849 mov dword ptr [j],eax
013B184C mov ecx,dword ptr [i]
013B184F add ecx,1
j = i++; //相当于j = i;i++;
013B1852 mov dword ptr [i],ecx
printf("i = %d,j = %d\n", i,j);//i = 4,j = 3
013B1855 mov eax,dword ptr [j]
013B1858 push eax
013B1859 mov ecx,dword ptr [i]
013B185C push ecx
013B185D push offset string "i = %d,j = %d\n" (013B7B30h)
013B1862 call _printf (013B104Bh)
013B1867 add esp,0Ch
j = i--; //相当于j = i;i--;
013B186A mov eax,dword ptr [i]
013B186D mov dword ptr [j],eax
013B1870 mov ecx,dword ptr [i]
013B1873 sub ecx,1
013B1876 mov dword ptr [i],ecx
printf("i = %d,j = %d\n", i, j);//i = 3,j = 4
013B1879 mov eax,dword ptr [j]
013B187C push eax
013B187D mov ecx,dword ptr [i]
013B1880 push ecx
013B1881 push offset string "i = %d,j = %d\n" (013B7B30h)
013B1886 call _printf (013B104Bh)
013B188B add esp,0Ch
j = ++i; //相当于++i;j = i;
013B188E mov eax,dword ptr [i]
013B1891 add eax,1
013B1894 mov dword ptr [i],eax
013B1897 mov ecx,dword ptr [i]
013B189A mov dword ptr [j],ecx
printf("i = %d,j = %d\n", i, j);//i = 4,j = 4
013B189D mov eax,dword ptr [j]
013B18A0 push eax
013B18A1 mov ecx,dword ptr [i]
013B18A4 push ecx
013B18A5 push offset string "i = %d,j = %d\n" (013B7B30h)
013B18AA call _printf (013B104Bh)
013B18AF add esp,0Ch
j = --i; //相当于--i;j = i;
013B18B2 mov eax,dword ptr [i]
013B18B5 sub eax,1
013B18B8 mov dword ptr [i],eax
013B18BB mov ecx,dword ptr [i]
013B18BE mov dword ptr [j],ecx
printf("i = %d,j = %d\n", i, j);//i = 3,j = 3
013B18C1 mov eax,dword ptr [j]
013B18C4 push eax
013B18C5 mov ecx,dword ptr [i]
013B18C8 push ecx
013B18C9 push offset string "i = %d,j = %d\n" (013B7B30h)
013B18CE call _printf (013B104Bh)
013B18D3 add esp,0Ch
上述代码为4-3-1.c程序的反汇编代码。注意与我们手写的汇编代码相比,有以下几点不同:
1.mov dword ptr [i],3与mov sdword ptr i,3语句不同
源代码中使用sdword ptr指定变量i为有符号32位正数更准确,反汇编代码中使用dword ptr表示变量i为32位整数,如果变量i是正整数肯定不会影响结果的正确性。如果变量i为负整数,编译器编译后将负整数转换为补码形式存储,补码使用dword ptr也是没有错误的。只是在输出变量i时,依据输出的格式化说明符输出。如果格式化说明符为’%d’,最高位为1的数输出为负整数,最高位为0的数输出为正整数。如果格式化说明符为’%u’,则一律按正整数格式输出。
借用16位汇编语言中的描述,有符号整数和无符号整数由程序员自己决定。
2.add eax,1与inc eax语句不同。二者等价,只是写法不同而已,inc指令不影响CF位。
3.call指令调用printf与invoke伪指令调用printf不同。
invoke伪指令是高级汇编指令,是call指令的简化形式,编译编译invoke语句后,仍然会还原成下述反汇编语句。
证明:
将汇编代码编译后的程序4-3-1.exe拖入DtDebug调试器中,按Ctrl+F9进入程序入口地址,查看反汇编窗口,反汇编代码如下:
013B18C1mov eax,dword ptr [j]
013B18C4push eax
013B18C5mov ecx,dword ptr [i]
013B18C8push ecx
013B18C9push offset string "i = %d,j = %d\n" (013B7B30h)
013B18CEcall _printf (013B104Bh)
00AE1000 >/$C705 0030AE00>MOV DWORD PTR DS:[AE3000],3
00AE100A |. C705 0430AE00>MOV DWORD PTR DS:[AE3004],0
00AE1014 |. A1 0030AE00 MOV EAX,DWORD PTR DS:[AE3000]
00AE1019 |. A3 0430AE00 MOV DWORD PTR DS:[AE3004],EAX
00AE101E |. 40 INC EAX
00AE101F |. A3 0030AE00 MOV DWORD PTR DS:[AE3000],EAX
00AE1024 |. FF35 0430AE00 PUSH DWORD PTR DS:[AE3004] ; /<%d> = 0
00AE102A |. FF35 0030AE00 PUSH DWORD PTR DS:[AE3000] ; |<%d> = 0
00AE1030 |. 68 1020AE00 PUSH 4-3-1.00AE2010 ; |format = "i = %d,j= %d",CR,LF,""
00AE1035 |. E8 84000000 CALL 4-3-1.00AE10BE ; \printf
invoke语句被还原为push和call语句。
4.3.3 二元算术运算符-示例十四
二元算术运算符:+、-、*、/、%。
示例代码十四
/*
二元运算符+、-、*、/、%
*/
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int i = 5;//将整数常量3赋值给int型变量i
int j = 2;//将整数常量2赋值给int型变量j
printf("%d\n", i + j);
printf("%d\n", i - j);
printf("%d\n", i * j);
printf("%d\n", i / j);//5/2的商为2
printf("%d\n", i % j);//5/2的余数为1
system("pause");
return 0;
}
●输出结果:
7
3
10
2
1
■代码分析
上述代码分别对变量i和j进行了加、减、乘、除、取模算术运行,然后输出计算的结果。因为是整数算术运算,因此结果都是整数,小数部分丢弃。
■汇编解析
●汇编代码
;C标准库头文件和导入库
include vcIO.inc
.data
i sdword ?
j sdword ?
.const
szMsg db "%d",0dh,0ah,0
.code ;代码区
start:
mov sdword ptr i,5;将3存入变量i地址处
mov sdword ptr j,2;将0存入变量j地址处
;
mov eax,i ;将变量i地址处的值存入eax寄存器
add eax,j ;i+j
invoke printf,offset szMsg,eax;输出变量i和j的和
;
mov eax,i ;将变量i地址处的值存入eax寄存器
sub eax,j ;i-j
invoke printf,offset szMsg,eax;输出变量i和j的差
;
mov eax,i ;将变量i地址处的值存入eax寄存器
imul eax,j ;eax=i*j
invoke printf,offset szMsg,eax;输出变量i和j的积
;
mov eax,i ;将变量i地址处的值存入eax寄存器
cdq
mov ebx,j
idiv ebx ;i/j,eax=商,edx=余数
push edx ;保护edx寄存器的值
invoke printf,offset szMsg,eax;输出变量i和j的商
pop edx ;恢复edx寄存器的值
invoke printf,offset szMsg,edx;输出变量i和j的模
;
invoke _getch
ret
end start
●输出结果:
7
3
10
2
1
汇编代码中分别使用add、sub、imul和idiv指令实现加减乘除和取模运算。
imul指令是有符号数乘法指令,两个操作数的乘积保存在第一个操作数eax中。
idiv指令是有符号数除法指令,进行除法运算之前,使用cdq指令先将32位被除数eax扩展为64位edx:eax(eax的符号位扩展到edx),然后除以保存在ebx寄存器中的除数,商保存在eax中,余数(模)保存在寄存器edx中。
【注意】接下来的push edx语句将余数edx寄存器入栈保护,因为下面的printf函数调用会修改edx寄存器原有的值。待第二个printf函数执行前,从栈中恢复edx寄存器的余数。
●反汇编代码
int i = 5;//将整数常量3赋值给int型变量i
01251838 mov dword ptr [i],5
int j = 2;//将整数常量2赋值给int型变量j
0125183F mov dword ptr [j],2
printf("%d\n", i + j);
01251846 mov eax,dword ptr [i]
01251849 add eax,dword ptr [j]
0125184C push eax
0125184D push offset string "%d\n" (01257B30h)
01251852 call _printf (0125104Bh)
01251857 add esp,8
printf("%d\n", i - j);
0125185A mov eax,dword ptr [i]
0125185D sub eax,dword ptr [j]
01251860 push eax
01251861 push offset string "%d\n" (01257B30h)
01251866 call _printf (0125104Bh)
0125186B add esp,8
printf("%d\n", i * j);
0125186E mov eax,dword ptr [i]
01251871 imul eax,dword ptr [j]
01251875 push eax
01251876 push offset string "%d\n" (01257B30h)
0125187B call _printf (0125104Bh)
01251880 add esp,8
printf("%d\n", i / j);//5/2的商为2
01251883 mov eax,dword ptr [i]
01251886 cdq
01251887 idiv eax,dword ptr [j]
0125188A push eax
0125188B push offset string "%d\n" (01257B30h)
01251890 call _printf (0125104Bh)
01251895 add esp,8
printf("%d\n", i % j);//5/2的余数为1
01251898 mov eax,dword ptr [i]
0125189B cdq
0125189C idiv eax,dword ptr [j]
printf("%d\n", i % j);//5/2的余数为1
0125189F push edx
012518A0 push offset string "%d\n" (01257B30h)
012518A5 call _printf (0125104Bh)
012518AA add esp,8
对比分析反汇代码和汇编代码:反汇编代码中,连续两次进行了除法运算,这是编译器自动翻译的结果,显然是不必要的。C语言编译器处于安全性和稳定性的考虑,将C语言翻译成汇编语句时存在冗余代码,降低了程序的性能。
在进行除法运算之前,需要先扩展被除数,所得商和余数的对应关系如下表4-3所示:
除数位数 | 隐含的被除数 | 商 | 余数 |
8位 | AX | AL | AL |
16位 | DX:AX | AX | DX |
32位 | EDX:EAX | EAX | EDX |
表4-3
●扩展规则:
无符号数除法运算div:使用XOR指令将AH、DX或EDX扩展为0。例如,XOR AH,AH;XOR DX,DX;XOR EDX,EDX。
有符号数除法运算idiv:将被除数的最高符号位扩展到AH、DX或EDX。例如,CBW,CWD,CDQ。
4.3.4 三元运算符
C语言中只有唯一一个三元运算符:条件运算符。
expr1 ? expr2 : expr3;
当expr1为真时,表达式的值为expr2;
当expr1为假时,表达式的值为expr3;
举例
(1 + 2)? 4:0; //先计算括号内的表达式,1+2=3,结果为真(非0),输出4。
(1 - 1)? 4:3; //先计算括号内的表达式,1-1=0,结果为假(0),输出3。
注意
在计算三元运算符时,有括号的先计算括号里的表达式。
在C语言中,0为假,非0为真。
条件运算符可以简化简单的条件语句,但是对于复杂的条件语句,不建议使用条件运算符。
【注】我们将在第六章分支语句中详细讲解条件语句。
本文摘自编程达人系列教材《汇编的角度——C语言》。