0. 概要
老规矩,先回顾一下后面三篇文章咱们都讲了什么。
首先,第一篇【谈谈二进制(一)】咱们从进制自身的意义开始,意识了二进制和其余进制,而后实现了十进制和其余各种进制之间的转换。
接着,第二篇【谈谈二进制(二)——四则运算】中,咱们则通过十进制的四则运算原理,推导出二进制的四则运算。
上一篇【谈谈二进制(三)——位运算及其利用】,咱们将二进制从纯数学的世界中带到了计算机的世界里,并通过一个实在的算法题,理解了二进制位运算在理论编程中的应用。
明天这篇,咱们来看看二进制在计算机中的非凡示意。
1. 有符号数和无符号数
在讲各种码之前,先来相熟一下两个概念:有符号数 和无符号数。这两个概念很好了解,就是字面的意思,一个数值是否有正负符号。
在上一篇文章中,按位取反的局部,咱们在 C++
代码中用 unsigned
关键字定义了一个无符号数。无符号数的意思就是,这个数字存储在计算机中的时候是没有符号的,也就是负数;而有符号数则会把正负号也存入计算机中。但咱们晓得,计算机所有的数值都是由二进制组成的,那么正负号这种货色该怎么存储呢?
其实正和负这两个货色,就像布尔型的真和假一样,是两种截然相同的状态,因而正和负也能够应用 0
和1
来示意,计算机里理论也是这么做的:0
示意正,1
示意负。并且正负号在原数无效数的最后面(右边),所占的这一位被称为 符号位 。譬如+1001
,它的理论存储值就是0,1001
,-1001
就是1,1001
,符号位在逗号的右边。这里的逗号,实际上是一种为了书写不便以及区别整数小数的约定俗成的写法,即整数的数值与符号位之间用逗号隔开,而小数的符号位和数值位之间则以小数点隔开,譬如-0.1001
,会写成1.1001
。
这种把正负号给 数字化 后的数值,被称为 机器数 ,带正负号的原数被叫做 真值。
讲各种码之前,这里还要提一句,原码、补码、反码、移码这些码,都只是机器数的不同示意模式,就像后面咱们讲过的进制,无论二进制还是十进制,对于同一个数值来讲,也只是不同的示意办法而已。
2. 原码
先来看原码,原码是泛滥码中最简略的一种机器数表示法,其实咱们下面在说有符号位的符号的时候,就曾经把原码的概念给讲完了,原码就是符号位和真值的绝对值组成的 。它和真值之间仅仅是正负号被换成了0
和1
,而后依据整数或小数加上,
,也就是上文中写到的那几个例子,这里简略列举一下:
$$[+1001]_{原}=0, 1001$$
$$[-1001]_{原}=1, 1001$$
$$[+0.1001]_{原}=0. 1001$$
$$[-0.1001]_{原}=1. 1001$$
不过这里有几点咱们要提一下,十分有意思。
第一个是整数正数的原码,上文中咱们晓得,就是把负号给换成 1
,而后写的时候加一个逗号,举个例子,-1001(-9)
,原码示意为1, 1001
。这里咱们做一个大胆的试验:把逗号去掉,而后把残余的11001
看做一个残缺的二进制正整数,也就是 25
,看看它跟-1001(-9)
有什么分割。
从十进制上的数来看,乍看之下 25
和(-9)
如同没什么分割,而后咱们来看二进制 11001
和-1001
,咦?仿佛有点像,除了最高位的 1
和负号之外,余下的 4
位截然不同,咱们把它俩相加:11001 + (-1001) = 10000
。嘿,这就更有意思了,相熟二进制运算的敌人们必定一眼就看进去,10000
实际上就是 $2^4=16$。
咱们是不是能够从这个试验中猜想一个论断:一个负整数的原码,等于 2
的n
次方减去本人 ?没错,这是一个正确的论断。这里的要害是,n
是多少?下面的试验中咱们的 n
为4
,这里的 4
, 其实就是原数的位数。用数学表达式表白就是:当 $0≥x>-2^n$(x 为原数真值)时,则 $[x]_{原}=2^n-x$。
正整数就没啥好说的,间接在真值绝对值后面加个0,
。
再来看看小数,因为小数的符号位和数值位用小数点隔开,因而当 0 ≤ x < 1
时,也就是正小数时,它的原码就等于它本人。负小数呢?譬如-0.1001
,原码写成1. 1001
,也很简略,就是1. 1001 = 1 - (-0.1001)
。
原码就是这样,前面的这些数学运算法则其实不论也无所谓,因为原码自身切实是太简略易懂了。
3. 补码
补码就略微简单一点,它的根底原理波及到了 补数
和模
的概念,这里我参考了 唐朔飞老师的《计算机组成原理》中的一些概念和例子来解说。
3.1 补数和模
补数的概念首次接触会感觉生疏,但它实际上可能就存在于咱们的日常生活中。譬如,某一时刻时钟的某个指针指向 6
,如果咱们想让它指向3
,咱们能够让指针沿逆时针方向走3
格,也能够让它沿顺时针方向能走 9
格,最终都会让这个指针指向 3
。假如顺时针方向为正,逆时针方向为负,则方才的两个行为别离为:6 + (-3) = 3
和6 + 9 = 15
。
咱们晓得,尽管咱们日常采纳 24
小时制,但钟表上的刻度通常只有 12
个刻度,因而指针超过 12
时,就会回归 1
、2
等等。因而下面咱们失去的 15
,实际上是15 - 12 = 3
,也就是咱们一开始所要达到的指向3
的目的地。所以这里 15
和3
在时钟上指向的都是 3
,那么,在方才的操作过程中,-3
和+9
对时钟而言,作用是一样的,都是让本来在 6
的指针,指向了3
。
下面的例子里,在数学上,称时钟的 12
格刻度为 模
,写作 mod 12
,而 称+9
是 -3
以12
为模的补数,记作
$$-3 ≡ +9 \qquad(\text{mod}\ 12)$$
或者说,对于 模 12
而言,-3
和 +9
是互为补数的。同理有
$$-4 ≡ +8 \qquad(\text{mod}\ 12)$$
$$-5 ≡ +7 \qquad(\text{mod}\ 12)$$
即对于 模 12
而言,+8
和 +7
别离是 -4
和-5
的补数。可见,只有确定一个 模
,就能够找到一个与正数等价的负数来代替该正数,而这个失去的负数,即为正数的 补数
,同时,咱们察看到, 该正数的绝对值,和它的补数相加,就是模(论断一)。
那么当咱们有一个正数,且已知模时,如何求它对应的负数补数呢?很简略,只须要 让正数和模相加 (论断二),譬如-4 + 12 = 8(mod 12)
,这个+8
就是 -4
的补数了。
这就是模和补数的概念。这里大家可能会有疑难了:下面的几个式子和最初的论断,讲的都是正数的补数,那么负数有补数吗?
有的,但先别着急,咱们先来看下,下面咱们求得的正数的模到底有什么用。其实参考咱们一开始讲的时钟的例子,咱们模摸糊糊能够感觉到,补数这个货色,能够让减法变成加法。仍然来看一个例子,咱们设A = 9, B = 5
,求A - B(mod 12)
。最通常的解法是减法:
$$A – B = 9 – 5 = 4$$
这么解必定没问题,但咱们题目中给出了 mod 12
,那么对模12
而言,-5
能够用它的补数 +7
代替,他们是恒等的,于是这题的另一个解法如下:
$$A – B = 9 + 7 = 16$$
这样咱们失去了 16
,回忆一下时钟,16
这个数字是不存在的,过后钟拨到 16
时,其实是 4
,所以对于mod 12
,16
又等价于 4
,即4 ≡ 16(mod 12)
。那么如果此时时钟再顺时针转一圈(12
格),到了 28
呢?它仍然是4
,即
$$4 ≡ 16 ≡ 28 \qquad(\text{mod}\ 12)$$
$$4 ≡ 4 + 12 ≡ 4 + 2\times12 ≡ 4 \qquad(\text{mod}\ 12)$$
这也就是说,负数的补数,就是负数自身(论断三)。
后面讲进制时咱们重复讲过一点,不同的进制之间仅仅是进位形式不同,所以下面咱们尽管用的是十进制来介绍模,实际上二进制仍然能够用模,联合模和补数的概念,以及下面的三个论断,咱们能够失去如下二进制数的补数:
$$-1011 ≡ +0101 \qquad(\text{mod}\ 2^4)$$
$$+0101 ≡ +0101 \qquad(\text{mod}\ 2^4)$$
$$-0.1001 ≡ +1.0111 \qquad(\text{mod}\ 2)$$
$$+0.1001 ≡ +0.1001 \qquad(\text{mod}\ 2)$$
用下面粗体的三个论断,很容易就能求得各种数字的补数,这里几个二进制我就不再计算验证了,大家能够本人试一下。
有了补数的概念后,人们看到了它能够将减法变成加法,便将其概念用到了计算机中,于是呈现了补码这种机器数。
3.2 补码的定义
补码和原码一样,须要用一位数字在高位作为符号位表正负,而低位的数值局部则采纳了补数的概念,因而,联合下面补数的特色,咱们能够设想到,因为负数的补数是它本人,因而在补码中,负数的数值位是它本人,符号位则同原码一样,负数为0
,正数为1
。譬如上面两个例子:
$$[+1001]_{补}=0, 1001$$
$$[+0.1001]_{补}=0. 1001$$
也就是说,负数的补码和原码一样。
正数就要麻烦一些。首先,通过上一大节咱们晓得,二进制的补数计算形式和十进制一样,譬如 -1101
,它的最高位为第3
位,因而咱们取比它高一位的最小值 $2^4$ 作为模,求得它的补数为 10000 + (-1101) = 0011
。按理说,咱们失去的正数的补数是负数,这里只须要再加一个符号位0
即可,但留神了,补码中的理论状况并不是这样,只管正数的补数为负数,但正数的补码前的符号位仍然是负符号位1
,即符号位与原数保持一致。
为什么会这样?明明求得的补数是负数,符号位却要用负符号1
?
实际上,在后面讲补数的时候咱们提到过,补数的呈现,能够让本来作减法的式子变成加法 ,补码这种机器数也正是看准了这一点才被计算机引入的。对于计算机来说, 让减法变成加法,相当于让计算机只须要在硬件层面上实现一个加法器,就能够解决加减法两种问题。那么这里补码的符号位的问题,天然也是为了计算而设定如此的,咱们来看一个理论的例子就晓得了。
设 A = 1110,B = 1101
,咱们来求A - B
的值。惯例用减法,一眼能够看进去,后果是 1110 - 1101 = 0001
。咱们将A
和-B(-1101)
两个数都换成补码,用加法计算,此时 $A = 0,1110\quad(mod\ 2^4)$,$-B = 1,0011\quad(mod\ 2^4)$。留神了,在应用补码计算时,补码的符号位也参加计算,那么此时这两个补码相加后的后果为:
$$0,1110 + 1,0011 = 10,0001 \qquad(\text{mod}\ 2^4)$$
咱们看到,后果的符号位产生了进位,变成了 10
。这里其实波及到了另一个知识点: 补码运算的符号位进位(溢出)。因为篇幅关系,本文不对该知识点开展解说,大家有趣味的能够去查找材料理解一下。总之,这里符号位尽管进位了,但并没有产生溢出,咱们把符号位的最高位 1
拿掉,失去了最终的后果0,0001
,也就是+0001
,与之前的减法后果统一。
这就是正数求补码后让符号位也放弃负值 1
的起因了。下面讲的都是负负数,实际上负小数同理,这里不再赘述,咱们间接上两个数学公式来对正数求补码进行一个总结。首先是整数,正整数就不说了,和原码一样,负整数呢?除了下面用规范的补数算法外,咱们能够间接用上面这个公式来计算:
$$[x]_{补}=2^{n+1}+x\qquad0>x≥-2^n\quad(\text{mod}\ 2^{n+1})$$
这里的 n
就是整数 x
的位数。拿后面咱们算过的 -1101
举例:
$$[-1101]_{补}=2^{4+1}+(-1101)=100000-1101=1,0011$$
最高位 1
就是符号位,与前面的数值位局部用逗号隔开,就失去了最终的后果。其实这里咱们能够察看到一个法则:负整数的补码,就是原数字除符号位外,数值局部的每一位按位求反后再+1
。
接着是负小数:
$$[x]_{补}=2+x\qquad0>x≥-1\quad(\text{mod}\ 2)$$
看上去更简略了,举个例子,咱们求 -0.1001
的补码:
$$[-0.1001]=2+(-0.1001)=10.0000 – 0.1001=1.1010$$
其实这里求负小数的补码的形式,和上一节中咱们求小数的补数的形式是一样的。还记得上一节的 论断二 吗?求一个正数的补数,就是 让正数和模相加 。而后咱们再仔细观察一下,会发现下面负整数的求补法则在负小数也见效,即 正数的补码,就是原数字除符号位外,数值局部的每一位按位求反后再+1
,而后符号位该怎么写怎么写。记住这个论断,它能让咱们在求补码的时候防止加减法运算,更加容易。
补码的概念根本就讲完了,想要把握的话,其实还是须要本人拿笔算一算,写一写。
4. 反码
反码,这种机器数的次要场景用于原码和补码的互相转换,反码作为两头数适度应用。首先,负数的反码,老样子,仍然和原码一样,符号位 0
和1
示意正负,而后是数值位局部一成不变,这里间接拿后面原码里的两个负数例子:
$$[+1001]_{反}=0, 1001$$
$$[+0.1001]_{反}=0. 1001$$
有区别的仍然是正数,但反码的正数就简略多了:除了符号位,数值位局部每一位都按位取反。因而:
$$[-1001]_{反}=1, 0110$$
$$[-0.1001]_{反}=1. 0110$$
非常简单对不对?那么为什么说反码是原码和补码之间互相转换的两头过渡呢?在上一节补码的最初,咱们失去了一个求补码的简略法则:正数的补码,就是原数字除符号位外,数值局部的每一位按位求反后再 +1
,而后符号位该怎么写怎么写。其实这个过程,就是对 原码 先求 反码 再给末位 +1
。同理, 如果咱们晓得了一个补码,想要求它的原码,只须要先将补码的末位 -1
后,再将数值位局部按位求反即可。
好了,明确了这一点后,咱们来看一个上一篇文章中遗留的问题。
4.1 按位取反运算
在上一篇文章【谈谈二进制(三)——位运算及其利用】中的取反运算局部,咱们对有符号数 5
进行取反,却失去了 -6
这个匪夷所思的答案,过后我在文章中说,这是因为计算机中存储数字的形式都是用补码存储,之所以会失去负值,则是因为补码的最高位示意符号位,按位取反时连符号位一起取反了。明天咱们深刻理解了补码和反码,也推出了补码和原码之间的转换形式,那么就用明天的常识,来彻底解决这个按位取反的问题吧。
首先,5
的二进制为 101
,其补码为0,101
。在进行按位取反操作,即~5
时,实际上把符号位也取反了,于是咱们失去了 1,010
这个后果,请留神,这个 1,010
仍然是补码 。而从符号位的1
咱们看到,这是一个正数补码,咱们通过补码不能直观地看出它的真值,因而须要先转换成原码。依照前文咱们提到的补码转原码的形式,先将 1,010
末位 -1
,失去1,001
,接着,咱们将数值位按位取反,符号位不动,失去1,110
。此时,1,110
曾经是 原码 了,能够间接将其求出:
$$1,110=-110_{原}=-6_{(10)}$$
于是,咱们失去了这个-6
。
这个让咱们之前摸不着头脑的答案,在咱们学完了补码之后,变得清晰明了。
除了这个 -6
之外,按位取反的局部还有一个小问题,咱们起初又应用了一个无符号数 5
来进行按位取反,发现失去了一个微小的数字4294967290
,并且这个数实际上是 $2^{32}-6$:
0000,0000,0000,0000,0000,0000,0000,0101 // 5
1111,1111,1111,1111,1111,1111,1111,1010 // 4294967290
这个问题其实能够有两种角度去了解,一种是最直观的:因为无符号数不波及符号位的问题,而 C
语言中(这个数字是用 C++
求得的)unsigned int
的取值范畴在 32
位机器上为 0 ~ 4294967295
,没错,最大值就是下面的两个数之和 $2^{32}-1$,也就是32
个1
。所以将 5(101)
左侧不存在的 0
都取反为 1
后,就失去了这么大的一个数字。
第二种角度就是来解释 -6
这个偶合了。咱们只须要把 $2^{32}$ 看做一个 模,依照后面计算补数的运算形式,来求 -6
的补数,即让模和 -6
相加,就失去了 4294967290
这个数字,同时,4294967290
是无符号数 5
按位求反的后果,它们相加的和,则是 unsigned int
的最大值(32
位,$2^{32}-1$),自身就比 $2^{32}$ 小1
,1 + 5 = 6
,于是就很凑巧的变成了咱们看到的法则。
5. 移码
补码对计算机来说是个好货色,毕竟它让计算机对于数字的计算变得更加不便了。但对人类来说,补码可就不那么敌对了,不说和真值,哪怕和原码相比,补码也存在着一个微小的毛病:数字自身的被补码暗藏,使得数字的大小不直观。举个例子:
$$+15_{补}=[+1111]_{补}=0,1111$$
$$-15_{补}=[-1111]_{补}=1,0001$$
如果咱们把符号位的 1
也看作是整个二进制数的一部分,那么显然10001 > 01111
。尽管在咱们懂得了补码的原理和运算过程后,咱们并不会间接这么去比拟,但因为正数的补码将数值局部都给改写了,总体来说,补码仍然给人以十分不直观的感触,一旦逗号遗记写,或者其余起因造成了误读,那么补码之间的比拟将成为劫难。就下面的两个数来说,咱们很难一眼就看进去这两个二进制数竟然互为相反数,但如果是用原码示意,那么起码他们的数值位是雷同的,咱们能第一工夫反馈过去。
此时,如果咱们将两个数字的 真值 都加上 $2^n$,这里的 n
同后面咱们讲过的那些 n
一样,是真值的位数,那么新失去的两个数字的大小就变得很直观了:
$$15 + 2^4 = 1111 + 10000 = 11111$$
$$-15 + 2^4 = 10000 – 1111 = 00001$$
只管还是无奈间接看出它俩的真值互为相反数,但咱们能够很直观地比拟出这两个数字的理论大小了,不会因为符号位的问题造成误读。而这两个计算后失去的新数字,就是移码。
所以移码的数学定义非常简单:
$$[x]_{移}=2^n+x\qquad(2^n>x≥-2^n)$$
这里的 n
就是真值的位数,x
则是真值。
咱们仔细观察下面两个数字的补码和移码,会发现一个乏味的景象:移码实际上就是扭转了补码的符号位,从 0
变1
,从 1
变0
。这样一来,当咱们晓得了一个数的补码后,只须要替换它的符号位,就能够失去移码,从而与其余数值进行间接比拟了。
6. 总结
明天咱们理解了二进制数的四种机器数示意形式,并且解决了上一篇文章中遗留的小问题。在下一篇文章里,咱们将意识 定点数 和浮点数,对,就是咱们编程中十分相熟的那个浮点数float
。