2015/11/18

C语言整形溢出


C语言的整形溢出,分为有符号整形溢出和无符号整形溢出。signed and unsigned。对于unsigned整形溢出,C的规范是有定义的:溢出后的数会以2^(8*sizeof(type))作模运算,也就 是说,如果一个 unsigned char(1字符,8bits)溢出了,会把溢出的值与 256 求模。例如:

unsigned char x = 0xff;
printf("%d\n", ++x);

上面的代码会输出:0 (因为0xff + 1是256,与2^8求模后就是0)。


对于 signed 整型的溢出,C的规范定义是 "undefined behavior",也就是说,编译器爱怎么处理溢出就怎么处理。比如:

signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);

上面的代码会输出:-128,因为0x7f + 0x01得到0x80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。

另外,千万别以为 signed 整型溢出就是负数,这个是不定的。比如:

signed char x = 0x7f;
signed char y = 0x05;
signed char r = x * y;
printf("%d\n", r);

上面的代码会输出:123

相信对于这些大家不会陌生了。


unsigned 与 signed 区别

  1. 计算机最小的存储单位是 "位" 也就是 bit,用来存放一个二进制数,即 0 或 1 。 8个二进制位为一个字节 Byte

  2. 对于 16-bit(16位) 的计算机,int 是以2个字节来储存的,而 32-bit 的计算机,则是以4个字节,即32个bit来储存的。

如果想要明白 singed 与 unsigned 的区别,除了这两个基本知识,还需要了解整数在计算机中的存储方式,以16-bit 计算机为例,定义 int a = 1; 那么a的存储方式用表格来表示:

0000 0000 0000 0001

首先需要提到的一点是,在 C语言 中十进制的整数都会转化为二进制存储在计算机。上面所声明的 int a = 1,也就是 signed int a = 1,C语言默认整形是一个 signed 类型。上面 表格中最左端的为最高位,最右端的为最低位。

signed类型的整数,只用了去除最高位,剩下的15位来进行编码的,而最高位只是用来做标记(sign),标记整数的正负,0表示正,1表示负。所以对于 signed 整数的存储范围是 (-2^15 to 2^15-1) ,也就是 -32768 到 +32767 的整数。

而对于 unsigned 的整数,其16位全部用来编码,存储范围便是 (0 to 2^16-1) ,即 0 到 65535 的非负整数。但是需要提到的一点是,不管整数的类型是signed 还是 unsigned,都用了16位来存储,也就是说16位全部用来存储数据。

上面所看到 a=1 的存储方式,就是将十进制的 a 在程序员计算器上转化为2字节的2进制,然后将这个结果放到上面的表格里。(原码存储) 可是对于 int a = -1 是怎样存储的?也就是说负数的存储方式是怎样的?

负数是以(补码存储),即是以原码的补码形式存储:

原码
0000 0000 0000 0001

反码
1111 1111 1111 1110

补码
1111 1111 1111 1111

原码、反码与补码

我们知道,在计算机内部存储的带符号数都是以补码形式存储,用补码形式进行运算的。什么是一个数的补码?为什么要用补码?这要从数的原码、反码开始讲。我们以整型数为例,且假定字长为8位。


1.原码

整数X的原码是指:其符号位为0表示正,为1表示负;其数值部分就是X的绝对值的二进制数。如:

[+100]原=01100100 [+0]原=00000000
[-100]原=11100100 [-0]原=10000000 注意:在原码中,零有两种表示形式。

原码表示法简单易懂,与真值(带符号数本身)转换方便,只要符号还原即可,但当两个正数相减或不同符号数相加时,必须比较两个数哪个绝对值大,才能决定谁减谁,才能确定结果是正还是负,所以原码不便于加减运算。


2.反码

X的反码是指:对于正数,反码与原码相同;对于负数,符号位不变,其数值位X的绝对值取反(1变0,0变1)。如

[+100]反=01100100 [+0]反=00000000
[-100]反=10011011 [-0]反=11111111

注意:在反码中,零也有两种表示形式。

反码运算也不方便,通常用来作为求补码的中间过渡。


3.补码

X的补码是指:对于正数,补码与原码相同;对于负数,符号位不变,其数值位X的绝对值取反后在最低位加1。如:

[+100]补=01100100 [+0]补=00000000
[-100]补=10011100 [-0]补=00000000

注意:在补码中,零有唯一的编码,[+0]补=[-0]补=00000000。

补码运算简单方便,符号位可以作为数据的一位参与运算,不必单独处理;二进制的减法可用其补码的加法来实现,简化了硬件电路。


补码意义


[例子1]用8位二进制数分别表示 +0 和 -0。

解:我们知道,对于有符号数,我们规定最高位为符号位,0表示正数,1表示负数。剩余位为数值位,用来表示数的大小。

所以 +0 就表示为 0000 0000 ,而 -0 表示为 1000 0000


[例子2]计算9-6的结果。

解:我们知道:9-6=9+(-6)=3

  0000 1001
+ 1000 0110
-----------
  1000 1111

结果为-15,明显不对。而如果我们采用补码来进行计算呢?

我们知道,9的补码是0000 1001,-6的补码是1111 1010,重新进行运算

    0000 1001
+   1111 1010
-------------
  1 0000 0011

最高位的1溢出,剩余8位二进制表示的是3的补码。结果为3,正确。


[例子3]分析程序运行结果。

int
main(int argc, const char *argv[]) {
    int a=100,b=-1;
    printf("a=%d,%x,%o,%u\n", a,a,a,a);
    printf("b=%d,%x,%o,%u\n", b,b,b,b);

    return 0;
}

运行结果:

a=100,64,144,100
b=-1,ffff,177777,65535

[例子1]中,为什么同样一个0有两种不同的表示方法呢?

[例子2]中,为什么第一种计算方法会错,而用补码计算结果才对呢?

[例子3]中,为什么-1以十六进制、八进制以及无符号整型输出的结果分别变成了 ffff , 177777 , 65535 ?

这是因为在计算机系统中,数值一律用补码来存储。

主要原因:

1、统一了零的编码;

2、将符号位和其它数值位统一处理;

3、将减法运算转变为加法运算;

4、两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。