新书推荐:4.3 算术运算符

本节必须掌握的知识点:

运算符

表达式

优先级

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"ij);//i = 3,j = 4

j = ++i;//相当于++i;j = i;

printf("i = %d,j = %d\n"ij);//i = 4,j = 4

j = --i;//相当于--i;j = i;

printf("i = %d,j = %d\n"ij);//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", ++ij);//i = 4,j = 3

j = i;      

printf("i = %d,j = %d\n", --ij);//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语言》。

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值