二进制溢出

一、溢出的本质

溢出的本质是计算机无法存放过大或者过小的数据。

假设一个计算机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,有符号数的溢出

#include<void.h>

Void 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

例如:

#include <stdio.h>

int 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;
}

结果:

a=-2,b=32767  因为:32767+1=-32768     -32768+32766=-2
a=-2,b=32767       因为:-2-32766 =-32768    -32768-1=32767,反正都是在[-32768,32767]之间循环
a=32767,b=32767
sizeof=2
因此,下面的程序就是错误的,while是个无限循环:
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,

#include <stdio.h>

short 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,

#include <stdio.h>

void 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

posted @ 2017-12-21 15:19  重头来过  阅读(2353)  评论(0编辑  收藏  举报