0. 概要
1. 有符号数和无符号数
C++代码中用
unsigned关键字定义了一个无符号数。无符号数的意思就是,这个数字存储在计算机中的时候是没有符号的,也就是正数;而有符号数则会把正负号也存入计算机中。但我们知道,计算机所有的数值都是由二进制组成的,那么正负号这种东西该怎么存储呢?
0和
1来表示,计算机里实际也是这么做的:
0表示正,
1表示负。并且正负号在原数有效数的最前面(左边),所占的这一位被称为符号位。譬如
+1001,它的实际存储值就是
0,1001,
-1001就是
1,1001,符号位在逗号的左边。这里的逗号,实际上是一种为了书写方便以及区别整数小数的约定俗成的写法,即整数的数值与符号位之间用逗号隔开,而小数的符号位和数值位之间则以小数点隔开,譬如
-0.1001,会写成
1.1001。
2. 原码
0和
1,然后根据整数或小数加上
,,也就是上文中写到的那几个例子,这里简单罗列一下:
第一个是整数负数的原码,上文中我们知道,就是把负号给换成1
,然后写的时候加一个逗号,举个例子,-1001(-9)
,原码表示为1, 1001
。这里我们做一个大胆的试验:把逗号去掉,然后把剩余的11001
看做一个完整的二进制正整数,也就是25
,看看它跟-1001(-9)
有什么联系。
25和
(-9)好像没什么联系,然后我们来看二进制
11001和
-1001,咦?似乎有点像,除了最高位的
1和负号之外,余下的
4位一模一样,我们把它俩相加:
11001 + (-1001) = 10000。嘿,这就更有意思了,熟悉二进制运算的朋友们肯定一眼就看出来,
10000实际上就是
2的
n次方减去自己?没错,这是一个正确的结论。这里的关键是,
n是多少?上面的试验中我们的
n为
4,这里的
4,其实就是原数的位数。用数学表达式表达就是:当
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为模的补数,记作
模12而言,
-3和
+9是互为补数的。同理有
模12而言,
+8和
+7分别是
-4和
-5的补数。可见,只要确定一个
模,就可以找到一个与负数等价的正数来代替该负数,而这个得到的正数,即为负数的
补数,同时,我们观察到,该负数的绝对值,和它的补数相加,就是模(结论一)。
-4 + 12 = 8 (mod 12),这个
+8就是
-4的补数了。
A = 9, B = 5,求
A - B(mod 12)。最通常的解法是减法:
mod 12,那么对模
12而言,
-5可以用它的补数
+7代替,他们是恒等的,于是这题的另一个解法如下:
16,回想一下时钟,
16这个数字是不存在的,当时钟拨到
16时,其实是
4,所以对于
mod 12,
16又等价于
4,即
4 ≡ 16(mod 12)。那么如果此时时钟再顺时针转一圈(
12格),到了
28呢?它依然是
4,即
3.2 补码的定义
0,负数为
1。譬如下面两个例子:
-1101,它的最高位为第
3位,因此我们取比它高一位的最小值24作为模,求得它的补数为
10000 + (-1101) = 0011。按理说,我们得到的负数的补数是正数,这里只需要再加一个符号位
0即可,但注意了,补码中的实际情况并不是这样,尽管负数的补数为正数,但负数的补码前的符号位依然是负符号位
1,即符号位与原数保持一致。
A = 1110,B = 1101,我们来求
A - B的值。常规用减法,一眼可以看出来,结果是
1110 - 1101 = 0001。我们将
A和
-B(-1101)两个数都换成补码,用加法计算,此时
10。这里其实涉及到了另一个知识点:补码运算的符号位进位(溢出)。由于篇幅关系,本文不对该知识点展开讲解,大家有兴趣的可以去查找资料了解一下。总之,这里符号位虽然进位了,但并没有发生溢出,我们把符号位的最高位
1拿掉,得到了最终的结果
0,0001,也就是
+0001,与之前的减法结果一致。
1的原因了。上面讲的都是负正数,实际上负小数同理,这里不再赘述,我们直接上两个数学公式来对负数求补码进行一个总结。首先是整数,正整数就不说了,和原码一样,负整数呢?除了上面用标准的补数算法外,我们可以直接用下面这个公式来计算:
n就是整数
x的位数。拿前面我们算过的
-1101举例:
1就是符号位,与后面的数值位部分用逗号隔开,就得到了最终的结果。其实这里我们可以观察到一个规律:负整数的补码,就是原数字除符号位外,数值部分的每一位按位求反后再
+1。
-0.1001的补码:
+1,然后符号位该怎么写怎么写。记住这个结论,它能让我们在求补码的时候避免加减法运算,更加容易。
4. 反码
0和
1表示正负,然后是数值位部分原封不动,这里直接拿前面原码里的两个正数例子:
+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
已经是原码了,可以直接将其求出:
-6
。
这个让我们之前摸不着头脑的答案,在我们学完了补码之后,变得清晰明了。
除了这个-6
之外,按位取反的部分还有一个小问题,我们后来又使用了一个无符号数5
来进行按位取反,发现得到了一个巨大的数字4294967290
,并且这个数实际上是
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
,没错,最大值就是上面的两个数之和 32
个1
。所以将5(101)
左侧不存在的0
都取反为1
后,就得到了这么大的一个数字。
第二种角度就是来解释-6
这个巧合了。我们只需要把 -6
的补数,即让模和-6
相加,就得到了4294967290
这个数字,同时,4294967290
是无符号数5
按位求反的结果,它们相加的和,则是unsigned int
的最大值(32
位, 1
,1 + 5 = 6
,于是就很凑巧的变成了我们看到的规律。
5. 移码
1也看作是整个二进制数的一部分,那么显然
10001 > 01111。虽然在我们懂得了补码的原理和运算过程后,我们并不会直接这么去比较,但因为负数的补码将数值部分都给改写了,总体来说,补码依然给人以非常不直观的感受,一旦逗号忘记写,或者其他原因造成了误读,那么补码之间的比较将成为灾难。就上面的两个数来说,我们很难一眼就看出来这两个二进制数居然互为相反数,但如果是用原码表示,那么起码他们的数值位是相同的,我们能第一时间反应过来。
n同前面我们讲过的那些
n一样,是真值的位数,那么新得到的两个数字的大小就变得很直观了:
n就是真值的位数,
x则是真值。
0变
1,从
1变
0。这样一来,当我们知道了一个数的补码后,只需要替换它的符号位,就可以得到移码,从而与其他数值进行直接比较了。
6. 总结
float。




