c语⾔之⽆符号有符号数加减法运算
c语⾔之⽆符号/有符号数加减法运算
1.加减运算指令是不区分有符号和⽆符号的,编程⼈员⾃⼰要⼼理有数;
2.在PC系统中,有符号数⽤补码表⽰. 正数的补码是其本⾝,负数的补码是其对应正数取反加⼀,
补码表⽰,使得加减法统⼀了起来。;
3.CPU只会根据输⼊信号进⾏逻辑运算,在硬件级别是没有有符号⽆符号的概念,运算结束会根据运算前的信号和输出信号来设置⼀些标志位,是不是有符号由写程序的⼈决定,标志位要看你把操作数当有符号还是⽆符号来选择,就像内存中的数据,你可以按照需要来解析,原始数据在那⾥,你要按什么数据格式来解析在于⾃⼰的选择;
4.在汇编语⾔层⾯,声明变量的时候,没有 signed 和 unsignde 之分,汇编器统统,将你输⼊的整数字⾯量当作有符号数处理成补码存⼊到计算机中,只有这⼀个标准!;
5.加减法只有⼀套指令,因为这⼀套指令同时适⽤于有符号和⽆符号。乘法运算就不⾏了,必须⽤两套指令;
6.计算机对有符号整数的表⽰只采取⼀套编码⽅式,不存在正数⽤原码,负数⽤补码这⽤两套编码之说,⼤多数计算机内部的有符号整数都是⽤补码,就是说⽆论正负,这个计算机内部只⽤补码来编码!!!只不过正数和0的补码跟他原码在形式上相同,负数的补码在形式上与其绝对值的原码取反加⼀相同。
7.系统对有符号数和⽆符号数的加减法都采⽤⼀样的策略。
楼主所分析的是完全正确,不⽤怀疑。
两个负数的补码相加⽆符号加减法不存在溢出问题,只是将进位或借位存储在CF中。
机器不知道你进⾏的运算是否有符号,如果你进⾏的是有符号运算,你需要查看OF,否则不需要。
所以,溢出不溢出,是由程序员判断的,机器不知道。
1、加减运算指令是不区分有符号和⽆符号的,编程⼈员⾃⼰要⼼理有数。
说清这个问题,需要理解⼏个概念:
1. CF 进位标志位 Carry Flag
反映运算是否产⽣进位或者借位的操作,如果运算结果的最⾼位产⽣了⼀个进位或错位,那么,其值为1,否则为0。
2. OF 溢出标志 Overflow Flag
溢出标志OF⽤于反映有符号数加减运算所得结果是否溢出,如果运算结果超过当前运算位数所能表⽰的范围,则成为溢出,OF的值被设置为1,否则OF的值被清为0。溢出和进位是两个不同的概念。
3.在PC系统中,有符号数⽤补码表⽰. 正数的补码是其本⾝,负数的补码是其对应正数取反加⼀,
补码表⽰,使得加减法统⼀了起来。
4. pc 中正负数是靠最⾼为来判断的,最⾼为就等于SF, (sf=0 为正, sf=1为负)
5. 给定⼀个数,如果你认为它是正数,要⽤CF 判断溢出。判断⼤⼩也⽤CF
如果你认为它是负数,要⽤OF 判断溢出。判断⼤⼩也⽤要⽤OF,SF配合使⽤。具体怎么配合这就不说了
CPU只会根据输⼊信号进⾏逻辑运算,在硬件级别是没有有符号⽆符号的概念,运算结束会根据运算前
的信号和输出信号来设置⼀些标志位,是不是有符号由写程序的⼈决定,标志位要看你把操作数当有符号还是⽆符号来选择,就像内存中的数据,你可以按照需要来解析,原始数据在那⾥,你要按什么数据格式来解析在于⾃⼰的选择,所以玩汇编的要做到⼼⾥有数
继续
关于有符号数和⽆符号数,LZ看看这个吧:
有符号数和⽆符号数探讨
这个问题,要是简单的理解,是很容易的,不过要是考虑的深了,还真有些东西呢。
下⾯我就把这个东西尽量的扩展⼀点,深⼊⼀点和⼤家说说。
⼀、只有⼀个标准!
⼀、只有⼀个标准!
在汇编语⾔层⾯,声明变量的时候,没有 signed 和 unsignde 之分,汇编器统统,将你输⼊的整数字⾯量当作有符号数处理成补码存⼊到计算机中,只有这⼀个标准!汇编器不会区分有符号还是⽆符号然后
⽤两个标准来处理,它统统当作有符号的!并且统统汇编成补码!也就是说,db -20 汇编后为:EC ,⽽ db 236 汇编后也为 EC 。这⾥有⼀个⼩问题,思考深⼊的朋友会发现,db 是分配⼀个字节,那么⼀个字节能表⽰的有符号整数范围是:-128 ~ +127 ,那么 db 236 超过了这⼀范围,怎么可以?是的,+236 的补码的确超出了⼀个字节的表⽰范围,那么拿两个字节(当然更多的字节更好了)是可以装下的,应为:00 EC,也就是说 +236的补码应该是00 EC,⼀个字节装不下,但是,别忘了“截断”这个概念,就是说最后汇编的结果被截断了,00 EC 是两个字节,被截断成 EC ,所以,这是个“美丽的错误”,为什么这么说?因为,当你把 236 当作⽆符号数时,它汇编后的结果正好也是 EC ,这下皆⼤欢喜了,虽然汇编器只⽤⼀个标准来处理,但是借⽤了“截断”这个美丽的错误后,得到的结果是符合两个标准的!也就是说,给你⼀个字节,你想输⼊有符号的数,⽐如 -20 那么汇编后的结果是符合有符号数的;如果你输⼊ 236 那么你肯定当作⽆符号数来处理了(因为236不在⼀个字节能表⽰的有符号数的范围内啊),得到的结果是符合⽆符号数的。于是给⼤家⼀个错觉:汇编器有两套标准,会区分有符号和⽆符号,然后分别汇编。其实,你们被骗了。:-)
⼆、存在两套指令!
第⼀点说明汇编器只⽤⼀个⽅法把整数字⾯量汇编成真正的机器数。但并不是说计算机不区分有符号数和⽆符号数,相反,计算机对有符号和⽆符号数区分的⼗分清晰,因为计算机进⾏某些同样功能的处理时有两套指令作为后备,这就是分别为有符号和⽆符号数准备的。但是,这⾥要强调⼀点,⼀个数到底
是有符号数还是⽆符号数,计算机并不知道,这是由你来决定的,当你认为你要处理的数是有符号的,那么你就⽤那⼀套处理有符号数的指令,当你认为你要处理的数是⽆符号的,那就⽤处理⽆符号数的那⼀套指令。加减法只有⼀套指令,因为这⼀套指令同时适⽤于有符号和⽆符号。下⾯这些指令:mul div movzx … 是处理⽆符号数的,⽽这些:imul idiv movsx … 是处理有符号的。
举例来说:
内存⾥有⼀个字节x 为:0x EC ,⼀个字节 y 为:0x 02 。当把x,y当作有符号数来看时,x = -20 ,y = +2 。当作⽆符号数看时,x = 236 ,y = 2 。下⾯进⾏加运算,⽤ add 指令,得到的结果为:0x EE ,那么这个 0x EE 当作有符号数就是:-18 ,⽆符号数就是 238 。所以,add ⼀个指令可以适⽤有符号和⽆符号两种情况。(呵呵,其实为什么要补码啊,就是为了这个呗,:-))
乘法运算就不⾏了,必须⽤两套指令,有符号的情况下⽤imul 得到的结果是:0x FF D8 就是 -40 。⽆符号的情况下⽤ mul ,得到:0x 01 D8 就是 472 。(参看⽂后附录2例程)
三、可爱⼜可怕的c语⾔。
为什么⼜扯到 c 了?因为⼤多数遇到有符号还是⽆符号问题的朋友,都是c⾥⾯的 signed 和 unsigned 声明引起的,那为什么开头是从汇编讲起呢?因为我们现在⽤的c编译器,⽆论gcc 也好,vc6 的cl 也好,
都是将c语⾔代码编译成汇编语⾔代码,然后再⽤汇编器汇编成机器码的。搞清楚了汇编,就相当于从根本上明⽩了c,⽽且,⽤机器的思维去考虑问题,必须⽤汇编。(我⼀般遇到什么奇怪的c语⾔的问题都是把它编译成汇编来看。)
C 是可爱的,因为c符合kiss 原则,对机器的抽象程度刚刚好,让我们即提⾼了思维层⾯(⽐汇编的机器层⾯⼈性化多了),⼜不⾄于离机器太远(像c# ,java之类就太远了)。当初
K&R 版的c就是⾼级⼀点的汇编……:-)
C⼜是可怕的,因为它把机器层⾯的所有的东西都反应了出来,像这个有没有符号的问题就是⼀例(java就不存在这个问题,因为它被设计成所有的整数都是有符号的)。为了说明它的可怕特举⼀例:
#include <stdio.h>
#include <string.h>
int main()
{
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
printf("%d\n",y);
}
结果应该是 -1 但是却得到:2147483647 。为什么?因为strlen的返回值,类型是size_t,也就是unsigned int ,与 int 混合计算时有符号类型被⾃动转换成了⽆符号类型,结果⾃然出乎意料。。。
观察编译后的代码,除法指令为 div ,意味⽆符号除法。
解决办法就是强制转换,变成 int y = (int)(x - strlen(str) ) / 2; 强制向有符号⽅向转换(编译器默认正好相反),这样⼀来,除法指令编译成 idiv 了。
我们知道,就是同样状态的两个内存单位,⽤有符号处理指令 imul ,idiv 等得到的结果,与⽤⽆符号处理指令mul,div等得到的结果,是截然不同的!所以牵扯到有符号⽆符号计算的问题,特别是存在讨厌的⾃动转换时,要倍加⼩⼼!(这⾥⾃动转换时,⽆论gcc还是cl都不提⽰)
为了避免这些错误,建议,凡是在运算的时候,确保你的变量都是 signed 的。
四、c的做法。
对于有符号和⽆符号的处理上,c语⾔层⾯做的更“⼈性化”⼀些。⽐如在声明变量的时候,c 有signed 和 unsigned 前缀来区别,⽽汇编呢,没有任何区别,把握全在你⾃⼰,⽐如:你想在⼀个字节中输⼊⼀个有符号数,那么这个数就别超过 -128 ~ +127 ,想输⼊⽆符号数,要保证数值在 0~255 之间。如果你输⼊了 236 ,你还要说你输⼊的是有符号数,那么你肯定错了,因为有符号数236⾄少要两个字节来存放(为00 EC),不要⼩看了那⼀个字节的00,在有符号乘法下,两个字节的00 EC 与⼀个字节的EC,在与同样⼀个数相乘时,得到的结果是截然不同的
我们来看下具体的列⼦(⽤vc6的cl编译器⽣成):
C语⾔编译后⽣产的汇编语⾔
……
char x;
char x;
unsigned char y;
int z;
x = 3;
y = 236;
z = x*y;
…… ……
_x$ = -4
_y$ = -8
_z$ = -12
……
mov BYTE PTR _x$[ebp], 3
mov BYTE PTR _y$[ebp], 236
movsx eax, BYTE PTR _x$[ebp]
mov ecx, DWORD PTR _y$[ebp]
and ecx, 255
imul eax, ecx
mov DWORD PTR _z$[ebp], eax
……
我们看到,在赋值的时候(绿⾊部分),汇编后与本⽂第⼀条论述相同,是否有符号把握全在⾃⼰,c⽐汇编做的更好这⼀点没有得到体现,这也可以理解,因为c最终要被编译成汇编,汇编没有在变量声明时区分有⽆符号这⼀功能,⾃然,c也没有办法。但既然c提供了signed和unsigned声明,汇编后,肯定有代码体现这⼀点,表格⾥的红⾊部分就是。对有符号数x他进⾏了符号扩展,对⽆符号y进⾏了零扩展。这⾥为了举例的⽅便,进⾏了有符号数和⽆符号数的混合运算,实际编程中要避免这种情况。
(完)
附录:
1.计算机对有符号整数的表⽰只采取⼀套编码⽅式,不存在正数⽤原码,负数⽤补码这⽤两套编码之说,⼤多数计算机内部的有符号整数都是⽤补码,就是说⽆论正负,这个计算机内部只⽤补码来编码!!!只不过正数和0的补码跟他原码在形式上相同,负数的补码在形式上与其绝对值的原码取反加⼀相同。
2. 两套乘法指令结果例程:
;; 程序存储为 x.s
extern printf
global main
section .data
str1: db "%x",0x0d,0x0a,0
n: db 0x02
section .text
main:
xor eax,eax
mov al, 0xec
mul byte [n] ;有符号乘法指令为: imul
push eax
push str1
call printf
add esp,byte 4
ret
编译步骤:
1. nasm -felf x.s
2. gcc x.o
ubuntu7.04 下⽤nasm和gcc编译通过。结果符合⽂章所述。
当n=8时,可表⽰的有符号数的范围为-128~+127;当n=16时,可表⽰的有符号数的范围为 -32768~+32767。两个有符号数进⾏加减运算时,如果运算结果超过可表⽰的有符号数的范围时,就会发⽣溢出,使计算机结果出错。很显然,溢出只能出现在两个同号数相加或两个异号数相减的情况。具体的讲,对于加运算,如果次⾼位(数值部分最⾼位)形成进位加⼊最⾼位,⽽最⾼位(符号位)相加(包括次⾼位的进位)却没有进位输出时;或者反过来,次⾼位没有进位加⼊最⾼位,但最⾼位却有进位输出时,都将发⽣溢出。因为这两种情况分别是:两正数相加,结果超出了范围,形式上变成了负数;两负数相加,结果超出了范围,形式上变成了正数。
例例
对于减运算,当次⾼位不需从最⾼位借位,但最⾼位却需借位(整数减负数,差超出范围);或者反过来,次⾼位需从最⾼位借位,但最⾼位不需借位(负数减整数,差超出范围),也会出现溢出。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论