一、溢出的本质
溢出的本质是计算机无法存放过大或者过小的数据。
假设一个计算机CPU是4位的,那么每一位或者为0,或者为1,根据排列组合,这四位最多共有2*2*2*2=16种可能的组合方式,也就是说这台计算机只能最多表示16个数字。
以计算机中的无符号整数为例,那么4位CPU的计算机表示出来的就只有0~15这16个数字。如果你拿两个数,一个为11,另一个为5,做加法的话,计算结果会显示为0而不是16。
因为11加4已经等于15了,再加1它已经无法表示,所以又回到了0处,这种情况就属于上溢。反之,2-3的话,得到的结果为15,因为2-2已经为0,再减的话就转回到了15,这属于下溢。
总之一句话,溢出反应了计算机处理能力的上限和下限,太大的数和太小的数均无法直接呈现出来。
二、探讨有符号数与无符号数上溢出下溢出的问题
1,有符号数的溢出
#includeVoid main(){ int i= 2147483647; printf(“%d,%d”,i.i+1);}
输出结果为:2147483647,-2147483648
这是因为加减运算过后,它们的值超出了它们对应的那种整数类型的表示范围,我们把这种现象称为溢出。
注意:看清楚数字总是在循环的变化。如从最大2147483647,再加一后就变成了最小-2147483648。即循环的顺序是:
0— 2147483647— -2147483648— 0。
规律:
SHRT_MAX+1 == SHRT_MIN
SHRT_MIN-1 == SHRT_MAX例如:
#includeint main () { short int a=32767,b=32767,c; a=a+b; //实现两数不借助第三变量交换的功能 printf("a=%d,b=%d\n",a,b); b=a-b; printf("a=%d,b=%d\n",a,b); a=a-b; printf("a=%d,b=%d\n",a,b); c=sizeof(short int); printf("sizeof=%d",c); return 0;}
结果:
short int n=1, sum=0;while(sum<=32767) {sum+=n; n++;}printf(“n=%d\n”,n-1);
另外google的一道笔试题中也需要意识到溢出的存在:
short cal(short x){ if(x==0) return 0; else return x+cal(x-1);}
答案
x=0时,0
x>0时,x+(x-1)+(x-2)+…+1+0
x<0时,x+(x-1)+…+(-32768)+【溢出】+32767+32766……+1+0,中途栈溢出,计算此表达式最后的结果时,也会多次溢出,因此最后的结果人力很难算出。
假如是short类型的负数来说,-32768减去1之后,变成32767,就是说对于有符号整数来说:最小的负数-1=最大的整数,最大的整数+1=最小的负数。
假如栈不溢出,那么将递归32768-|x|+32767次,最后的值按照上面是可以计算出来的。但是栈的空间有限,当栈溢出的时候,错误,强制退出。
在gcc下,测试,假如上述数据类型是char,最后是能计算出值的大小,栈的大小还够用。
2,下面为无符号数的溢出
在c语言的程序开发调试中,经常碰到非法操作导致程序强行终止。这种情况的发生多是因为程序指针的指向错误,数据溢出就是其中的一种,下面我将介绍一下常见的几种溢出情况。
1)无符号整数上溢
示例代码:
bool funcB(char *s1,unsigned short int len1,char *s2,unsigned short int len2){ if (1 + len1 + len2 > 64) return false; char *buf = (char*)malloc(len1+len2+1); if (buf) { memcpy(buf,s1,len1); /*函数解释:void *memcpy(void *to , const void *from, unsigned int count) :从from指向的内存区向to指向的内存区复制count个字节;如果两内存区重叠,不定义该内存区的定义*/ memcpy(buf+len1,s2,len2); } if (buf) free(buf); return true;}
这段代码存在整数上溢问题,当len1等于64,len2是0xFF(255),这段代码就会发生溢出。
因为在定义为unsigned short char 类型下1+0xFF=0,这样就绕过了1 + len1 + len2 > 64的条件判断,因为刚好等于64。
直接导致后面的代码中错误地分配64字节内存,在内存拷贝时将产生溢出错误。无符号整数上溢出的意思就是:无符号整数a已达最大数,+1之后又从小开始计算:1+0xFF=0。
2)无符号整数下溢
示例代码:
bool funcA(unsigned int cbSize){ if (cbSize < 1024) { char *buf = new char[cbSize-1]; memset(buf,0,cbSize-1); /*函数解释:void memset(void *buf, int ch, unsigned int count): 把ch的低字节复制到buf指向的内存区的前count个字节处,常用于把某个内存区域初始化已知值。*/ delete buf; return true; } else return false;}
这是一个整数下溢的例子。
当函数调用时,如果参数cbSize赋值为0,由于参数cbSize被定义为unsigned int 型,则(cbSize-1)= (0-1) = 0XFFFFFFFF。
分配如此大的内存,后果可想而知(4G内存,1个char是一个字节)!
无符号整数下溢就是:无符号整数a为最小值0,再-1后变成最大值,例如:(0-1) = 0XFFFFFFFF。
三、例子
1,
#includeshort int fac( short int x){ static short int y=1; //用static修饰,即使出了fac函数,内存也不会被回收,可以一直累计。 静态区的变量不会被回收,直至进程结束才能回收,有别于堆或栈中的变量。 y*=x; return y;}int main(void){ int s=0; short i; for(i=1;i<=8;i++) s+=fac(i); printf("S=%d\n",s); return 1; } 运行结果:S=-19303
运行SETP:
Setp1:i=1 y=1 S=0+1=1
Setp2:i=2 y=2 S=1+2=3
Setp3:i=3 y=6 S=3+6=9
Setp4:i=4 y=24 S=9+24=33
Setp5:i=5 y=120 S=33+120=153
Setp6:i=6 y=720 S=153+720=873
Setp7:i=7 y=5040 S=873+5040=5913
Setp8:i=8 y=40320溢出-->y=-25216 S=5913+(-25216)=-19303
16位内存空间存储情况:1001,1101,1000,0000(即-25216的二进制补码表示,若表示无符号数原码则为40320)
故:y=-25216(与40320同余,模65536,因为40320-(-25216)=65536,或-25216-40320=-65536,类似于时钟,16点-4点=12点(模),又类似:-2与10互补,2与-10互补,以数值12为模)
即:将负数的补码,当成无符号数的原码,即是此负数的同余数,或叫补数。此是补码的真谛,补码即是反码,并不是相反数,而是同余,正是同余,才可以将减变成加。
比如25217-25216 = 25217+40320 = 65537,对于16位的数值来说,表示范围[-32768,0,32767],模为65536,因此上溢出,65537 = 1,因此25217-25216 = 1。
有人说看表示范围,不是到32767就溢出了吗,怎么这里到65537才溢出?没关系,表示范围是表示范围,而模是溢出的实现方法,不相关。
因此在补码计算中溢出是正确的也是必须的,这样才能得出正确的结果,这是补码的真谛。
因此,不同机器字长的计算机,内部必然设定有一个与机器字长相关的模。比如8位计算机模256,16位计算机模65536,以此实现运算。
2,
#includevoid main(){ char c1 = 128;//溢出 unsigned char c2 = 257;//溢出 short s1 = 65535;//溢出 unsigned int s2 = 65537;//不溢出 printf("%d,%d,%d,%d",c1,c2,s1,s2);}
输出结果:-128,1,-1,65537