整数在计算机中的表示

由于人类有10根手指,为了方便平时记数,所以我们生活中所使用的数字都是十进制数。但是在电路中只有高低电平两种信号,用高电平表示1,低电平表示0,这样就刚好可以用来表示二进制数。所以计算机中也就使用二进制信号来表示和存储信息。二进制数是由0和1组成的数字串,例如00011010,我们也可以把这样的数字串称为位模式,每一位称为一个二进制位,8个二进制位组成1个字节

如果一个二进制数它有位,从右至左为这个数的每一位从0开始编号,这个二进制数就可以写成,它的最高位是,最低位是,每一位上的取值都是0或1。用二进制来表示整数的时候要区分两种情况,分别是无符号数有符号数。本文讨论的无符号数和有符号数均为整数。

无符号数

无符号数的位模式每一位都用来表示数,不需要表示符号,所以只能表示非负整数也就是大于等于0的整数。

我们先看下十进制整数,十进制数是以10基数的整数,每逢10向前进1。从右至左每一位分别代表的是个,十,百,千,万,……,由此可以把各个位都用一个10的整数次幂来表示即。一个位的十进制整数的值就是把位每一位上的数乘以所在位的10的整数次幂形式,然后把所得到的这个结果相加。例如十进制数可以表示为:

整个过程可以用数学公式表示成:

用一个例子对公式进行展开:

二进制数是以2基数的整数,每逢2向前进1。它与十进制类似,只是各个位用2的整数次幂来表示,因此从右至左的每一位变成了

下面公式描述了将一个无符号二进制数转换成十进制的过程:

通过两个例子对公式进行展开:

如果一个位的二进制数的每一位都是1,那么这个数就是位二进制数所能表示的最大数,代入公式:

其实求该式子的值其实是在求一个等比数列的和,这个等比数列的首项是1公比是2,代入等比数列求和公式得到:

可以得出一个位的二进制数能表示的最大数是。例如一个4位的二进制数可以表示的最大值是1111对应的十进制就是

再比如C语言中的unsigned int(无符号整数)类型一般用占4个字节的内存空间也就是32位,可以算出unsigned int类型可以表示的最大值是

把这个公式可以推广至任意进制数转换成十进制数,将原来公式中的替换,指是进制数,如果要转换成10进制则就是。得到新的公式:

例如十六进制数(十六进制用字母A-F来代替10-15)转换成十进制数的式子:

有符号数

如果要用二进制来表示负数,我们很自然的想到可以直接在二进制的前面加上一个符号,例如-1001。但是计算机不能直接表示这样的符号,它只知道0和1,因此有符号数需要用最高位来表示符号符号位为1时表示值为负,为0时表示值为非负。所以它既可以像有符号数一样表示非负整数以外还可以表示负整数。有三种方式可以对有符号整数进行编码,分别是补码,反码,原码。目前大多数计算机都是用补码对有符号数进行编码,所以本文中的无符号数都是以补码表示的。

补码和无符号整数不同之处在与它最高位所对应的2的整数次幂是负的,补码的最高位也被称为符号位。如果符号位是1整个数就是负数,例如:,如果符号位是0整个数就是非负数。例如:

可以得到转换公式:

仔细观察该式子可以发现最高位的贡献,如果是0那么的值为0,表示这个数是一个非负数。如果是1那么的值始终小于,所以表示这个数是一个负数

下面分别通过两个例子对公式进行展开:

一个位的补码,它能表示的最小值是当符号位为1其余位全是0,此时值为。它能表示的最大值是当符号位为0,其余位全是1,此时值为。也可以发现用补码表示的有符号数,它们的最小值和最小值是不对称的,因为它们的绝对值差1。

当一个位的补码的每一位都是1时,符号位在补码转十进制公式中对应的值为: ,后半部分通过等比数列求和可以得出:,两个部分相加刚好得到。所以可以得出当一个位的补码的每一位都是1时,它所对应的十进制位

有符号数与无符号数的转换

有符号数与无符号数之间的转换并不会影响存储在内存中的位模式,只是会采用新类型的解释方式对内存中的位模式进行重新解释

在C语言中用unsigned int表示无符号整数,用int表示有符号整数,可以在两个类型之间进行相互转换。unsigned intint都占用32位的内存大小,所以它们之间的转换并不会改变位模式的长度。

例如下面代码可以对变量进行隐式转换

1
2
3
int x = -2147483518;	// x: 10000000 00000000 00000000 10000010
unsigned int u = x; // u: 10000000 00000000 00000000 10000010
printf("u=%u\n", u); // output: u=2147483778

第1行代码中的变量x是有符号类型,这里是使用补码对有符号数进行编码。它在内存中的位模式是10000000 00000000 00000000 10000010
用补码转十进制的公式可以得出对应的十进制是

第2行代码将有符号整数变量x赋值给了无符号整数变量u,该操作只将变量x在内存中的值拷贝给变量u,并不会改变位模式,所以变量u在内存中的位模式还是10000000 00000000 00000000 10000010,只是后面我们使用变量u的时候,它将会用无符号的解析方式对这个位模式进行解析。用无符号的公式可以得出对应的十进制是,所以最后打印出u=2147483778

也可以尝试反过来赋值:

1
2
3
unsigned int u = 2147483778;	// u: 10000000 00000000 00000000 10000010
int x = u; // x: 10000000 00000000 00000000 10000010
printf("x=%d\n", x); // output: x=-2147483518

上面代码同样不会改变存储在内存中的值,将会把变量x用有符号数的方式进行解释。

C/C++中会把一个同时包含有符号数无符号数的表达式中的有符号数强制转换成无符号数,这会导致很多奇怪的问题。

比如在做比较运算时:

1
2
3
unsigned int u = 0;		// u: 00000000 00000000 00000000 00000000
short x = -1; // x: 11111111 11111111 11111111 11111111
printf("x<u=%d\n", x < u); // output: x<u=0

上面代码中无符号变量u的位模式是00000000 00000000 00000000 00000000有符号变量x的位模式是11111111 11111111 11111111 11111111。表达式x < u中就同时包含了有符号数无符号数,按照C/C++的规则会把表达式中的有符号变量x强制转换成无符号数。变量x以无符号的方式解释出来的值就是。这个值远大于u,因此表达式x < u的值就出乎意料的成了0(False)。

同样的问题也会出现在当使用作差法来比较两个无符号变量的大小,可能会得到令你失望的结果:

1
2
3
unsigned int a = 2;
unsigned int b = 10;
printf("(a-b) < 0=%d\n", ((a-b) < 0)); // output: (a-b) < 0=0

这里和上面的例子的问题一样,表达式a-b会得到负数,按照C/C++的规则会把a-b转换成无符号数,此时它就变成了一个很大的正数,所以表达式(a-b) < 0的结果就成了0(False)。

扩展位模式

把一个位的位模式扩展成更大的位时(),为了使值保持不变,需要在原来位模式的左侧通过扩展位来补齐长度,使新的位模式长度为。对于无符号数和有符号数它们扩展位的方式不同。

扩展无符号数

扩展一个无符号数时,会在左侧用0补齐长度,同时不会改变数值。因为对于无符号数来说左侧新增0时,并不会改变数值大小。

例如在C语言中把一个uint8_t(8位无符号类型)型变量赋值给一个的uint16_t(16位无符号类型)型变量:

1
2
3
uint8_t u8 = 130;		// u8: 10000010
uint16_t u16 = u8; // u16: 00000000 10000010
printf("u16=%d\n", u16); // output: u16=130

上面代码中8位无符号变量u8的位模式是10000010,把它赋值给16位无符号变量u16。由于u16可以容纳16位二进制,多出了8个位,所以在赋值过程中就会用0来补齐。最后得到u16的位模式为00000000 10000010,对应的十进制数值保持不变还是130。

扩展有符号数

扩展一个有符号数时,有符号类型不能像无符号类型一样用0来补齐长度,因为如果一个数是负数,当左边用0补齐的话就会改变这个数的符号。所以它的规则是用原来数的符号位来补齐长度。

例如在C语言中把一个16位short型变量赋值给一个的32位int型变量:

1
2
3
short s = -32758;		// s: 10000000 00001010
int x = s; // x: 11111111 11111111 10000000 00001010
printf("x=%d\n", x); // output: x=-32758

上面代码中16位有符号变量s的位模式是10000000 00001010。把s赋值给32位有符号的变量x时,多出了16个位,这16个位会用原来变量s的符号位来填充。由于s是一个负数它的符号位是1 ,所以x的位模式就变成了11111111 11111111 10000000 00001010。即使位模式改变了,但是所表示的数值不会变,x的值仍旧是-32758。

数学推导

对于有符号数我们只需要证明在补码的左边扩展一位符号位可以使表示的数值不变,那么对于扩展任意个符号位依然保持这种特性。

把一个位的补码表示为,最高位是它的符号位。对这个补码扩展1位符号位之后长度变成了,可以得到,由于是扩展的符号位,所以符号位还是原来的。我们要推导出扩展前和扩展后都表示同一个值。

扩展前的补码代入公式:

扩展后的补码代入公式:

我们把上面扩展后的式子经过变形可以得出与扩展前的式子相等:

从而证明出对一个位的补码进行符号位扩展,并不会改变所表示的具体数值。上面变化过程中最关键的一点是证明式子

我们还可以更直观的理解上面证明过程。在扩展后的补码中第位和位对整个补码的数值产生了影响,因此我们可以把这两位所表示的数值加起来看是否等于扩展前符号位上的数值即可。也就是,简化得到。其实这也就跟上面证明两个式子相等原理差不多。

推导过程对于无符号数的扩展同样适用,因为无符号数的扩展是把多余的位用0填充,这刚好与有符号数在符号位是0的情况下扩展方式是一样的。

截断位模式

把一个位的位模式变成比它小的位时(),由于新的位模式长度容纳不下原先的数,就会把左边多余的位会被截断,因此值会发生改变。无论是有符号数还是无符号数它们的截断方式一样的。

在C语言中如果给某个变量赋予一个超出该变量可以容纳的值时会产生溢出,此时就会被截断处理。有时候我们给一个无符号变量赋予一个很大的值时,会得到一个负数,这是因为发生了溢出,位模式被截断了,刚好被截断之后的符号位是1,就产生了负数。

例如把一个32位的int赋值给16位的short

1
2
3
int x = 2147516546;		// x: 10000000 00000000 10000000 10000010
short s = x; // s: 10000000 10000010
printf("s=%d\n", s); // output: x=-32638

上面代码中变量x的位模式是10000000 00000000 10000000 10000010,把它赋值给s后,由于sshort类型只有16位,所以在赋值过程中就会把左边的16个位全部截断,然后把剩下的16位拷贝给s,所以最后s的位模式为10000000 10000010,截断后的数最高位是1,表示它还是一个有符号数,对应的十进制就是