进制
虽然在日常生活中,我们已经习惯了使用10进制数字,但在由数字电路构成的数字世界中,2进制才是效率更高的选择。
10进制与2进制
10进制(decimal)计数法(一般也叫阿拉伯计数法)是在日常生活中使用得最多的一种计数法,它是一种位值记数法(positional notation)。位值计数法的意思是不同位置的数字有着不同的权重(weight),即不同的值。比如数字 ”333“ ,第一个数字3表示的是三百(3×100),第二个数字3表示的是三十(3×10),第三个数字3则是表示三(3×1)。
罗马计数法也是10进制的,但不同的是,它不是一种位值记数法,而是加减制的。它的一般规则是:
罗马数字只有七个符号:Ⅰ,Ⅴ,Ⅹ,L,C,D,M 。它们依次代表1,5,10,50,100,500,1000。相同数字并列时就相加,不同数字并列时,小数放在大数的右边就作为加数;放在大数的左边(限于基本符号),就作为减数。
罗马记数法记较大的数十分冗长,例如 “3888” 就要记作 MMMDCCCLXXXVIII,书面计算更麻烦,故一般不通用。
10进制有 0~9 共十个数字,所以它 “逢十进一”,当然也可以说它的基数是十。2进制(binary)则只有0和1这两个数字,所以它 “逢二进一”,或者说它的基数是二。二进制计数法也是一种位值记数法,它的不同位置的 “ 0 或 1” 有不同的含义。例如数字 “1101” 的第一个数字1表示的是8(1×23),第二个数字1表示的是4(1×22),第二个数字0表示的是0(0×21),第四个数字1则是表示1(1×20)。
据说人类之所以使用10进制,很大可能是因为人类有10根手指。这一说法我不知道是否正确,但我知道数字电路之所以使用2进制是因为电路有两个很明显的状态:”关“ 和 ”开“。
8进制和16进制
除了2进制外,8进制(Octal)和16进制(hexadecimal)在数字电路中也很普遍。它们可以很方便地分别将2进制数字分为3个一组和4个一组,从而方便了多位2进制数的书写和阅读。
8进制以8为基数,共使用 0~7 这8个数字。因为2的3次方是8,所以3个2进制数字可以很方便地转化成一个8进制数字,例如2进制数字 “110_010” 可以转化成8进制数字 “62”。
类似的,16进制以16为基数,共使用09,加AF这16个数字。因为2的4次方是16,所以4个2进制数字可以很方便地转化成一个16进制数字,例如2进制数字”1101_0001“ 可以转化成16进制数字 “D1” 。
不同进制之间的转换如下(部分数字):
原码、反码和补码
原码、反码和补码是2进制数的3种不同表示形式,关于这个概念,我在这篇文章中有一个详尽且通俗的说明,如何简单理解原码、反码和补码?
我们先来了解一个概念:模数系统。所谓模,是指一个计数器的容量,或者称模数。例如,常见的圆盘式钟表就是一个以12为模的模数系统。因为它的模是12,所以它只能表示1~12这12个数,超出12的数就无法表示了。
假设现在你在一个见不到外面光线的房间,发现钟表正指向11点整的位置。仅根据这个信息,你是无法直接判断当下是上午11点还是下午11点(即23点)的。下午11点也是23点,所以在模12系统中,23就等于11,这可以表示为:23 = 11(mod12),也称23和11对模12是同余的。
原码
原码就是某个数的2进制形式,比如10进制数字 “3”,如果用4位二进制数表示,则是 “0011”,4个位可以表示从 “0000~1111” 这16个数字(即10进制的0~15)。
这种方法有个问题就是只能表示正数,不能表示负数。在10进制系统中,我们表示负数的方法是在前面添加负号“-”,比如“-5”就是负5,而“5”和“+5”则是正5,因为正数的使用更常见,所以“+“ 号大多数时候都会被约定俗成地给省略掉。
2进制的使用场景主要是数字电路,电路只能识别 “0” 和 “1”(高电平和低电平),识别不了 “正负符号 +/- ” ,所以使用 符号“+/-” 来区分正负的方法行不通。既然一个2进制单位可以表示0/1,即可以表示两种状态,那么我们就可以单独把其中一个数拿出来表示这个数字是正数还是负数。通常的做法是把最高位拿出来表示正负,并约定:若该数为1则为负数,若为0则为正数。
这样的话,就有:
+5 = 0_101,-5 = 1_101
”+5“ 和 “-5” 除了最高位的符号位不同外,剩余3位都是相同的。
这种方法也有问题,那就是没法直接做计算。按理说(+5)+(-5)= 0,但是 0101+1101 = 1_0010,如果不截断第5位,那它的结果是 “-2”,如果截断,那它的结果是 “2”。不管怎么处理,它的结果都和正确结果 “0” 对不上。
补码
我们希望设计的是这样一种系统:它不光可以表示正负,同时还可以直接做运算。例如(+5)+(-5)= 0 我们希望在这一表示系统中也可以直接实现。
(+5)+(-5)因为是加法,所以它的结果怎么都不可能是 “0000”,但是,它有没有可能是 “1_0000” ? 完全有可能!因为 “1_0000” 就是10进制的 “16”,从上面说的模数系统的概念中可知,16和0对模16同余,也就是说在一个模16的系统中,16就是0,0就是169!
刚好4位2进制数就是一个模16的系统!再来看这个式子: 0101 + 1011 = 10000,而我们希望得到的是 “0000” ,这意味着最高位的1可以被舍弃掉。这个过程是不是可以看做是结果减了 “16”(从 “10000” 到 “0000” 相当于减16)? 如果将原本表示10进制数 “-5” 的 “1011” 若视为10进制正整数的话,那就是则是 “11” ,也就是说 0101 + 1011 = 5 + 11 = 16 ,然后舍去最高位相当于-16,所以最后结果为0!补码的出现把减法变成了加法!
“-5” 的表示就很清晰了,1_0000 - 0101 = 1011,这种表示方法就叫做补码。补码是通过将负数转换成同余数并利用溢出,从而实现减法和负数表示的一种方法。
例如 “-10” 的8位补码是这么求的:
8位数的模为256,“-10” 与 “246” 对模256同余,所以 “-10” 的补码就是 “246” 的原码,即 “1111_0110” ;也可以用 “256 - 10” 的方法来求,即 1_0000_0000 - 0000_1010 = 1111_0110,二者结果是一样的。
反码
你可能听过:正数的原码等于它自身,而负数的原码则等于符号位不变的其他位取反加1,而符号位不变的其他位取反又被称为反码,所以负数的补码等于其反码加1。
在我看来,反码仅仅只是拿来求补码的中间产物,并没有太多的其他作用。
任何一对绝对值相同的正数和负数,其正数原码与负数的反码相加,其值都是全1。例如 “+42” 原码是 “0010_1010”,“-42” 的反码是 “1101_0101” ,加起来就是“1111_1111” 。显然,之所以会出现这种情况,是因为这两个数的每位都是相反的。我想这可能也是反码(ones’ complement)这个中文译名的由来。
反码的英文名叫Ones’ complement,粗暴点翻译就是 “1们的补集” 或者 “多个1的补集”。反码本质上是在求正数的算术负数,也就是说,将数字的所有位取反产生的结果与从 0 中减去该值的结果相同。
式①:负数的补码 =容量(模) - 负数的绝对值
8位字长下,任何一个负数与其反码相加结果均为全1,即 正数原码 + 负数反码 = 1111_1111。8位字长下容量是1_0000_0000,即2^8 = 256,而1_0000_0000 = 0_1111_1111 + 0_0000_0001。也即
式②:容量(模) = 1111_1111 + 1 = 正数原码 + 负数反码 + 1
结合①②式,有
负数的补码 = 正数原码 + 负数反码 + 1 - 负数的绝对值 = 正数原码 + 负数反码 + 1 - 正数原码 = 负数反码 + 1
这也就是常说的:负数的补码等于取反加1 。这样就把对负数求补码的运算在电路上给转换成了 按位取反 和 加法(+1) 运算了,这是数字电路很容易实现的形式。
所以说,根据取反加1来求负数的补码只是一种简便方法,而并不是一般定义。一般定义仍是:
负数补码 = 模(容量) - 负数对应的绝对值。
无符号数和有符号数
了解了原码和补码后,就很容易理解无符号数(unsigned number)和有符号数(signed number)了。
无符号数是一串最高位不表示符号,只表示数值的二进制数序列。相反,有符号数则是最高位用来表示符号。例如同样的4位补码表示 “1001” ,如果看做是无符号数的话,那它等于10进制的 “9” ;如果看做是有符号数的话则是 “-7”。
无符号数和有符号数在相同位数下可以表示的数字个数是一样多的,但是表示范围却不一样。例如,8位2进制无符号数的表示范围是 “0~255”,而8位2进制有符号数的表示范围是 “-128~127”。
求有符号数的10进制值
无符号数的转换比较简单,只要把每个位上的数按权重加起来就可以,例如4位的 “1101”,结果就是 8+4+0+1 = 13。有符号数的转换按照传统的取反加1方法则麻烦一点,比如4位的 “1101”,先取反后是 “1010” ,再加1是 “1011” ,最高位表示是负数,剩余的 “011” 为是数值即 “3”,最终结果为 “-3” 。
其实,有一种更简便的求有符号数的10进制值的办法,那就是把最高位同样纳入带权重的加法,但是权重为 “ -1 ” 。例如4位的 ”1101“ ,就是 -8+4+0+1 = -3,这样和上面一种方法求得的结果是相同的。
原理是什么呢?因为4位二进制数的模是8(除去符号位只有3个有效位),从上面的推断可知,负数的补码除去符号位外剩余的数值是其在该模下的同余数,例如 “1101” 除去符号位是 “101” 即 10进制数“5”,最高位的权重为 “-1 ”,实际就相当于减去模值8,即 “1101” 相当于 5 - 8 = -3。
有符号数的高位扩展
10进制数 “-3” 用4bits来表示是 “1101” ,用5bits表示呢?答案是 “11101”。6bits到8bits的表示则分别是 “111101”,“1111101”,“11111101”。
4bits | 1_101 |
---|---|
5bits | 11_101 |
6bits | 111_101 |
7bits | 1111_101 |
8bits | 1111_101 |
可以看到,每次将位宽拓展一位,都是在补码的最高位补1。同时,对正数的扩展,毋庸置疑肯定是在高位补0。二者结合起来就是补码的高位扩展是补符号位。
正数的高位扩展补0没什么好讨论的,和10进制的 “1” 和 “01" 是同样的数值一样,2进制的 “0111” 和 “001111” 显然也是同一个数值。但是负数的高位扩展为什么也只要补符号位就行?理解这一点有两个方法。
方法1:往高位扩展1后,原本的最高位变成了次高位,次高位的权重从-1变成了+1,而最高位的权重又相当于次高位的2倍,二者相加后,相当于这两位在加法中的和根本没变。例如 ”-3“ 的4位补码 ”1_101“ ,原码可以看做是 -8 + 5 = -3,变成5位补码后(11_101)则是(-16+8) + 5 = -8 + 5 = -3。
方法2:根据负数的补码是除符号外取反加1来求的方法可知,往高位补1后,次高位的1取反加1后只有一种情况不是0,而这个0是不会对结果有影响的。例如 ”1_101“ 扩展到 ”11_101“ ,取反是 ”10_010“ ,加1后是 ”10_011“ ,只要低3位不是 ”000“ ,那么取反加1后就不会溢出,也就不会对次高位产生影响,从而保证了次高位一直是 ”0“ 。溢出的情况则比较特殊,仍以4bits数为例,只有 “1000” 这么算会产生溢出,但是 “1000” 即10进制的 “-8” 在4bits情况下是不存在原码和反码的。但从方法1中可知,“1000” 和 “11000” 都是 "-8"的补码,所以高位补符号位的规律仍然适用。
无符号数的加法与溢出
2进制数的加法同10进制类似,都是逢基数进一。以两个4bits无符号数 6 + 4 = 10为例,它的计算过程是这样的:
需要注意的是,两个4bits数相加是有可能产生5bits的和的,所以我们一般会把结果扩大到5bits,以防止溢出。例如14 + 8 = 22 的计算过程(有溢出)是这样的:
有符号数的加法与溢出
因为减法可以转换成加法,所以两个有符号数的加法只有3种情况:
- 正数 + 正数
- 正数 + 负数
- 负数 + 负数
接下来分别进行讨论。
1、正数 + 正数
情况类似上面讨论的无符号的加法,仍以 0110 + 0100 = 1010 为例,由于此时的结果为有符号数,故结果 “1010” 会被看做是 “-6” ,这显然不是 (6 + 4 = 10)的预期结果。
产生这一现象的原因是4bits的2进制有符号数最多只能表示 ”7“,而不能表示 ”10“,超出了范围产生了溢出。解决方法是将结果扩展一位,这样最后的结果就是01010,等于10进制数“10”,结果正确。
2、负数 + 负数
和 “正数 + 正数” 的情况类似,结果可能会溢出,所以也建议把结果扩展一位。
有一种情况类似(-1)+(-3)=(-4),尽管结果 “11100” 产生了高位溢出,但是这个溢出是可以被省略掉的,因为 “11100” 和 “1100” 都是 “-4” 的补码,只是位数不同罢了。和正数往高位补0不会改变数值一样,负数补码往高位补1同样不会改变数值。
还有一种情况则比如(-3)+(-6)=(-9),结果 “10111” 如果舍去最高位,则变成了 “0111”(10进制数 “7” ),这样明显和预期结果不符。但如果把结果扩展一位,则是 “10111” ,即10进制的 “-9” ,与预期结果相符。
3、正数 + 负数
正数 + 负数 等同于减法,减法的结果肯定比被减数小,所以理论上不会有溢出。但是2进制负数是使用补码来表示的,从而将减法转换成了加法,而加法则可能产生溢出,但这个溢出并不会影响运算结果。相反,减法的实现反而还依赖这个溢出。
例如 5 +(-2)= 3,如果结果只取低4位,那就是对的;如果结果也扩展一位,那反而出错了。
对上面三种情况的分析可知,其中有2种运算的结果可能会产生溢出(正数 + 正数、负数 + 负数),为了防止运算错误,需要将结果扩展一位。而正数 + 负数这种情况,若也将结果扩展1位则会运算错误。
问题是很多时候,我们做计算是无法保证输入的数据只是正数或负数,上面这3种情况可能在同一个模块中都会出现,为了保证设计的通用性,我们希望能有一种方法可以同时满足上面三种情况。
为此可以这样尝试:两个 N 位二进制补码相加,为了防止结果溢出产生错误,可以将两个加数进行符号位扩展,变为 N+1 位数,然后相加,结果也拓展到N+1位数。
正数 + 正数 的情况很显然,往正数的高位补符号位(0)后,相当于结果的最高位也多了一个0,所以不会对结果产生影响。
负数 + 负数 的情况类似,相当于结果的最高位多了一个1,同样不会对结果产生影响。下面产生了6位结果,而我们定义的位宽为5位,所以最终结果仍是符合预期的。
正数 + 负数 ,需要分别对其高位补0和补1,最终的运算结果因为存在负数补码的关系肯定也会溢出一位,但是最高位会舍去,所以结果也是就是正确的,比如:
- 📣您有任何问题,都可以在评论区和我交流📃!
- 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net
- 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!