初学C++时,书上或者网上的博客中会讲“整数在内存中存储的是补码”,一直也没有理解为什么要以补码存储,以及补码到底是什么。最近看了计算机组成原理,有所感悟,遂于此记录,如有错误欢迎大家指出。
先打一段代码,三行打印分别是什么?
unsigned int a = -1;
printf("a = %x\n", a); // a = ffffffff
printf("a = %u\n", a); // a = 4294967295
printf("a = %d\n", a); // a = -1
以前的理解可能是这样:a
是unsigned int
,a
的范围是0~2^32-1,将 -1 赋值给a
会出现溢出,所以第二行打印是4294967295。
现在觉得这样理解有一些僵硬,不易于理解,接下来就记录下我对整数存储的理解。
在开始之前,首先要了解下二进制加减法:对于一个32位寄存器来说,它最大可以记录的值为0xffffffff
,也就是所有位都为1
11111111 11111111 11111111 11111111
在最大值基础之上再加一会变成多少呢?加一之后每一位都会向前进1,由于寄存器并没有第33位,所以留下低32位,变成0x0
11111111 11111111 11111111 11111111
+ 00000000 00000000 00000000 00000001
=100000000 00000000 00000000 00000000
在0的基础之上减一会变成多少呢?由于0比1小,所以最低位会向前一位借位,前一位再向它的前一位借位,一直到第32位,它会向第33位借位(其实是不存在的),最后得到结果0xffffffff
100000000 00000000 00000000 00000000
- 00000000 00000000 00000000 00000001
= 11111111 11111111 11111111 11111111
符号指的是-
负号和+
正号,无符号整数指的是大于等于0的正整数。
无符号整数的存储很简单,寄存器中/内存中的值等于无符号整数的二进制值,例如无符号整数10,它的二进制数为0b1010
,所以它在寄存器中为
00000000 00000000 00000000 00001010
当寄存器所有位都为1时就是无符号整数的最大值,十六进制值为0xffffffff
,十进制值为2^32-1
。所以无符号整数的范围为0 ~ 2^32-1
。
我们在第一节了解到0xffffffff
加1会变成0x0
,所以无符号整数最大值加一会变成零,这种情况称为加法溢出;
unsigned int a = 0xffffffff;
printf("a = %x\n", a); // ffffffff
printf("a = %u\n", a); // 4294967295
unsigned int b = a + 1;
printf("b = %x\n", b); // 0
printf("b = %u\n", b); // 0
同样的,0x0
减1会变成0xffffffff
,即零减一会变成最大值,这种情况称为减法溢出。
int b = 0;
unsigned int c = b - 1;
printf("b = %x\n", c); // ffffffff
printf("b = %u\n", c); // 4294967295
无符号整数表示0 ~ 2^32-1
之间的正数,如果想要一个负数应该怎么办呢?这时候就发明了有符号数,规定最高位为符号位,符号位为1则该数为负数,符号位为0则为正数。
有符号正整数的存储方式和无符号整数的存储方式相同,唯一的不一样是,由于最高位为符号位,所以有符号整数的最大值为0x7fffffff
,对应十进制值为2^31-1
01111111 11111111 11111111 11111111
接下来看内存中负数应该如何存储呢?
我们先从十进制的角度理解负数,十进制中的 -1 = 0 - 1
, -2 = 0 - 2
,以此类推 -n 等于 0 减 n。
二进制中也是同样道理,-10
就是 0b0
减 0b1010
,得到0xfffffff6
。因此-10
在内存中的值是0xfffffff6
00000000 00000000 00000000 00000000
- 00000000 00000000 00000000 00001010
= 11111111 11111111 11111111 11110110
有符号整数可以表示的负数的最小值是多少呢?上面我们讲到最高位为符号位,负数的符号位为1,所以最小值对应的内存中的值应该是0x80000000
,我们来计算一下:
00000000 00000000 00000000 00000000
- ?
= 10000000 00000000 00000000 00000000
计算得到?
处的值是0x80000000
,对应的十进制值为-2^31
,所以有符号整数的范围为-2^31 ~ 2^31-1
。
同样的,有符号整数也会有溢出的情况出现:
最大值2^31-1
(0x7fffffff
)加1
会变成0x80000000
(-2^31
),一下子从最大变成了最小值,
01111111 11111111 11111111 11111111
+ 00000000 00000000 00000000 00000001
= 10000000 00000000 00000000 00000000
int a = 0x7fffffff;
printf("a = %x\n", a); // a = 7fffffff
printf("a = %d\n", a); // a = 2147483647
int b = a + 1;
printf("b = %x\n", b); // b = 80000000
printf("b = %d\n", b); // b = -2147483648
最小值-2^31
(0x80000000
)减1
会变成0x7fffffff
(`2^31-1``),一下子从最小变成了最大值,这里就不贴代码了。
10000000 00000000 00000000 00000000
- 00000000 00000000 00000000 00000001
= 01111111 11111111 11111111 11111111
前面两节了解了有符号数,无符号数,以及正数和负数在内存中的存储方式。再回顾一下:正数在内存中存储的值就是其二进制数,负数在内存中存储的值需要用0减去其绝对值的二进制数。
这时候就发现将二进制值转换为负数,或者将负数转化为二进制值要比正数麻烦很多。或者说是负数在内存中的存储方式很不符合我们的阅读习惯。
举个例子,我们平时更习惯将-10
表示为0b10000000 00000000 00000000 1010
,符号位为1说明是一个负数,这样可以很快知道这个二进制数表示的是-10
。
我的理解是:将这种符合阅读习惯的二进制数称为原码,将计算机内存中实际存储的内容称为补码。
我们如何从这个原码计算得到它在内存中真正存储内容(补码)呢?
先看正数10,我们以原码的形式表示它,得到0b1010
,等于内存实际存储内容,所以说正数的原码等于补码。
00000000 00000000 00000000 1010
再看负数-10,它在内存中的存储值(补码)就是将0b1010
(10)取反再加1,将取反得到的值称为反码。
11111111 11111111 11111111 0101
+ 00000000 00000000 00000000 0001
= 11111111 11111111 11111111 0110
回到一开始的例子
unsigned int a = -1;
printf("a = %x\n", a); // a = ffffffff
printf("a = %u\n", a); // a = 4294967295
printf("a = %d\n", a); // a = -1
无符号数a
的值为-1,我们不需要了解是否发生溢出了,我们只需要知道内存中存储的值是多少,这里的值是-1,所以内存中的值为0xffffffff
。
我们读取到的十进制数是多少取决于我们以什么格式去读取,如果以%u
无符号数读取,它就是4294967295;如果以%d
有符号数读取,它就是-1。