关于后端:谈谈二进制四原码补码反码移码

55次阅读

共计 7332 个字符,预计需要花费 19 分钟才能阅读完成。

0. 概要

老规矩,先回顾一下后面三篇文章咱们都讲了什么。

首先,第一篇【谈谈二进制(一)】咱们从进制自身的意义开始,意识了二进制和其余进制,而后实现了十进制和其余各种进制之间的转换。

接着,第二篇【谈谈二进制(二)——四则运算】中,咱们则通过十进制的四则运算原理,推导出二进制的四则运算。

上一篇【谈谈二进制(三)——位运算及其利用】,咱们将二进制从纯数学的世界中带到了计算机的世界里,并通过一个实在的算法题,理解了二进制位运算在理论编程中的应用。

明天这篇,咱们来看看二进制在计算机中的非凡示意。

1. 有符号数和无符号数

在讲各种码之前,先来相熟一下两个概念:有符号数 无符号数。这两个概念很好了解,就是字面的意思,一个数值是否有正负符号。

在上一篇文章中,按位取反的局部,咱们在 C++ 代码中用 unsigned 关键字定义了一个无符号数。无符号数的意思就是,这个数字存储在计算机中的时候是没有符号的,也就是负数;而有符号数则会把正负号也存入计算机中。但咱们晓得,计算机所有的数值都是由二进制组成的,那么正负号这种货色该怎么存储呢?

其实正和负这两个货色,就像布尔型的真和假一样,是两种截然相同的状态,因而正和负也能够应用 01来示意,计算机里理论也是这么做的:0示意正,1示意负。并且正负号在原数无效数的最后面(右边),所占的这一位被称为 符号位 。譬如+1001,它的理论存储值就是0,1001-1001 就是1,1001,符号位在逗号的右边。这里的逗号,实际上是一种为了书写不便以及区别整数小数的约定俗成的写法,即整数的数值与符号位之间用逗号隔开,而小数的符号位和数值位之间则以小数点隔开,譬如-0.1001,会写成1.1001

这种把正负号给 数字化 后的数值,被称为 机器数 ,带正负号的原数被叫做 真值

讲各种码之前,这里还要提一句,原码、补码、反码、移码这些码,都只是机器数的不同示意模式,就像后面咱们讲过的进制,无论二进制还是十进制,对于同一个数值来讲,也只是不同的示意办法而已。

2. 原码

先来看原码,原码是泛滥码中最简略的一种机器数表示法,其实咱们下面在说有符号位的符号的时候,就曾经把原码的概念给讲完了,原码就是符号位和真值的绝对值组成的 。它和真值之间仅仅是正负号被换成了01,而后依据整数或小数加上,,也就是上文中写到的那几个例子,这里简略列举一下:

$$[+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$。

咱们是不是能够从这个试验中猜想一个论断:一个负整数的原码,等于 2n次方减去本人 ?没错,这是一个正确的论断。这里的要害是,n 是多少?下面的试验中咱们的 n4,这里的 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) = 36 + 9 = 15

咱们晓得,尽管咱们日常采纳 24 小时制,但钟表上的刻度通常只有 12 个刻度,因而指针超过 12 时,就会回归 12 等等。因而下面咱们失去的 15,实际上是15 - 12 = 3,也就是咱们一开始所要达到的指向3 的目的地。所以这里 153在时钟上指向的都是 3,那么,在方才的操作过程中,-3+9对时钟而言,作用是一样的,都是让本来在 6 的指针,指向了3

下面的例子里,在数学上,称时钟的 12 格刻度为 ,写作 mod 12,而+9-312为模的补数,记作

$$-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 1216 又等价于 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. 反码

反码,这种机器数的次要场景用于原码和补码的互相转换,反码作为两头数适度应用。首先,负数的反码,老样子,仍然和原码一样,符号位 01示意正负,而后是数值位局部一成不变,这里间接拿后面原码里的两个负数例子:

$$[+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$,也就是321。所以将 5(101) 左侧不存在的 0 都取反为 1 后,就失去了这么大的一个数字。

第二种角度就是来解释 -6 这个偶合了。咱们只须要把 $2^{32}$ 看做一个 ,依照后面计算补数的运算形式,来求 -6 的补数,即让模和 -6 相加,就失去了 4294967290 这个数字,同时,4294967290是无符号数 5 按位求反的后果,它们相加的和,则是 unsigned int 的最大值(32位,$2^{32}-1$),自身就比 $2^{32}$ 小11 + 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则是真值。

咱们仔细观察下面两个数字的补码和移码,会发现一个乏味的景象:移码实际上就是扭转了补码的符号位,从 01,从 10。这样一来,当咱们晓得了一个数的补码后,只须要替换它的符号位,就能够失去移码,从而与其余数值进行间接比拟了。

6. 总结

明天咱们理解了二进制数的四种机器数示意形式,并且解决了上一篇文章中遗留的小问题。在下一篇文章里,咱们将意识 定点数 浮点数,对,就是咱们编程中十分相熟的那个浮点数float

正文完
 0