整数在计算机中的表示
由于人类有10根手指,为了方便平时记数,所以我们生活中所使用的数字都是
十进制数
。但是在电路中只有高低电平两种信号,用高电平表示1,低电平表示0,这样就刚好可以用来表示二进制数
。所以计算机中也就使用二进制信号
来表示和存储信息。二进制数是由0和1组成的数字串
,例如00011010
,我们也可以把这样的数字串
称为位模式
,每一位称为一个二进制位
,8个二进制位组成1个字节
。
如果一个二进制数从右至左
为这个数的每一位从0开始编号,这个二进制数就可以写成无符号数
和有符号数
。本文讨论的无符号数和有符号数均为整数。
无符号数
无符号数的位模式每一位都用来表示数,不需要表示符号,所以只能表示
非负整数
也就是大于等于0的整数。
我们先看下十进制整数
,十进制数是以10
为基数
的整数,每逢10向前进1。从右至左
每一位分别代表的是个,十,百,千,万,……
,由此可以把各个位都用一个10的整数次幂
来表示即十进制整数
的值就是把10的整数次幂
形式,然后把所得到的这
整个过程可以用数学公式表示成:
用一个例子对公式进行展开:
二进制数是以
2
为基数
的整数,每逢2向前进1。它与十进制类似,只是各个位用2的整数次幂
来表示,因此从右至左
的每一位变成了。
下面公式描述了将一个无符号二进制数
转换成十进制
的过程:
通过两个例子对公式进行展开:
如果一个
其实求该式子的值其实是在求一个等比数列的和,这个等比数列的首项是1
,公比是2
,代入等比数列求和公式得到:
可以得出一个
位的二进制数能表示的最大数是 。例如一个4位的二进制数可以表示的最大值是 1111
对应的十进制就是。
再比如C语言中的unsigned int
(无符号整数)类型一般用占4个字节
的内存空间也就是32位,可以算出unsigned int
类型可以表示的最大值是
把这个公式可以推广至任意进制数
转换成十进制数
,将原来公式中的进制数
,如果要转换成10进制则
例如十六进制数(十六进制用字母A-F
来代替10-15
)转换成十进制数的式子:
有符号数
如果要用二进制来表示负数,我们很自然的想到可以直接在二进制的前面加上一个符号,例如
-1001
。但是计算机不能直接表示这样的符号,它只知道0和1,因此有符号数需要用最高位来表示 符号
,符号位
为1时表示值为负,为0时表示值为非负。所以它既可以像有符号数一样表示非负整数
以外还可以表示负整数
。有三种方式可以对有符号整数进行编码,分别是补码,反码,原码。目前大多数计算机都是用补码
对有符号数进行编码,所以本文中的无符号数都是以补码
表示的。
补码和无符号整数不同之处在与它最高位所对应的2的整数次幂
是负的,补码的最高位也被称为符号位
。如果符号位是1整个数就是负数
,例如:非负数
。例如:
可以得到转换公式:
仔细观察该式子可以发现最高位非负数
。如果负数
。
下面分别通过两个例子对公式进行展开:
一个
当一个
位的补码的每一位都是1时, 符号位
在补码转十进制公式中对应的值为:,后半部分通过等比数列求和可以得出: ,两个部分相加刚好得到 。所以可以得出当一个 位的补码的每一位都是1时,它所对应的十进制位 。
有符号数与无符号数的转换
有符号数与无符号数之间的转换并不会影响存储在内存中的
位模式
,只是会采用新类型的解释方式
对内存中的位模式进行重新解释
。
在C语言中用unsigned int
表示无符号整数,用int
表示有符号整数,可以在两个类型之间进行相互转换。unsigned int
和int
都占用32位的内存大小,所以它们之间的转换并不会改变位模式
的长度。
例如下面代码可以对变量进行隐式转换
:
1 | int x = -2147483518; // x: 10000000 00000000 00000000 10000010 |
第1行代码中的变量x
是有符号类型,这里是使用补码
对有符号数进行编码。它在内存中的位模式是10000000 00000000 00000000 10000010
。
用补码转十进制的公式可以得出对应的十进制是
第2行代码将有符号整数变量x
赋值给了无符号整数变量u
,该操作只将变量x在内存中的值拷贝给变量u,并不会改变位模式,所以变量u
在内存中的位模式还是10000000 00000000 00000000 10000010
,只是后面我们使用变量u
的时候,它将会用无符号
的解析方式对这个位模式进行解析。用无符号的公式可以得出对应的十进制是u=2147483778
。
也可以尝试反过来赋值:
1 | unsigned int u = 2147483778; // u: 10000000 00000000 00000000 10000010 |
上面代码同样不会改变存储在内存中的值,将会把变量x
用有符号数的方式进行解释。
C/C++中会把一个同时包含
有符号数
和无符号数
的表达式中的有符号数强制转换
成无符号数,这会导致很多奇怪的问题。
比如在做比较运算时:
1 | unsigned int u = 0; // u: 00000000 00000000 00000000 00000000 |
上面代码中无符号
变量u
的位模式是00000000 00000000 00000000 00000000
,有符号
变量x
的位模式是11111111 11111111 11111111 11111111
。表达式x < u
中就同时包含了有符号数
和无符号数
,按照C/C++的规则会把表达式中的有符号变量x
强制转换成无符号数。变量x
以无符号的方式解释出来的值就是u
,因此表达式x < u
的值就出乎意料的成了0(False)。
同样的问题也会出现在当使用作差法
来比较两个无符号
变量的大小,可能会得到令你失望的结果:
1 | unsigned int a = 2; |
这里和上面的例子的问题一样,表达式a-b
会得到负数,按照C/C++的规则会把a-b
转换成无符号
数,此时它就变成了一个很大的正数,所以表达式(a-b) < 0
的结果就成了0(False)。
扩展位模式
把一个
位的位模式扩展成更大的 位时( ),为了使值保持不变,需要在原来位模式的左侧通过 扩展位
来补齐长度,使新的位模式长度为。对于无符号数和有符号数它们 扩展位
的方式不同。
扩展无符号数
扩展一个无符号数时,会在左侧用0补齐长度,同时不会改变数值。因为对于无符号数来说左侧新增0时,并不会改变数值大小。
例如在C语言中把一个uint8_t
(8位无符号类型)型变量赋值给一个的uint16_t
(16位无符号类型)型变量:
1 | uint8_t u8 = 130; // u8: 10000010 |
上面代码中8位无符号
变量u8
的位模式是10000010
,把它赋值给16位无符号
变量u16
。由于u16
可以容纳16位二进制,多出了8个位,所以在赋值过程中就会用0来补齐。最后得到u16
的位模式为00000000 10000010
,对应的十进制数值保持不变还是130。
扩展有符号数
扩展一个有符号数时,有符号类型不能像无符号类型一样用0来补齐长度,因为如果一个数是负数,当左边用0补齐的话就会改变这个数的符号。所以它的规则是用原来数的符号位
来补齐长度。
例如在C语言中把一个16位short
型变量赋值给一个的32位int
型变量:
1 | short s = -32758; // s: 10000000 00001010 |
上面代码中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 | int x = 2147516546; // x: 10000000 00000000 10000000 10000010 |
上面代码中变量x
的位模式是10000000 00000000 10000000 10000010
,把它赋值给s
后,由于s
是short
类型只有16位,所以在赋值过程中就会把左边的16个位全部截断,然后把剩下的16位拷贝给s
,所以最后s
的位模式为10000000 10000010
,截断后的数最高位是1,表示它还是一个有符号数,对应的十进制就是