对于一个数,计算机要使用一定的编码方式进行存储:原码,反码,补码是机器存储一个具体数字的编码方式;既然有三种存储形式,那么计算机会选取哪种通用形式呢?答案是:选取补码,即数值在计算机中是以补码的形式来存储的,下面分步来说明为什么要用补码来存?
原码:
(1)原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小!比如如果是8位二进制 1 :
[+1]原 = 0000 0001
[-1]原 = 1000 0001
(2)因为第一位是符号位,所以8位二进制数的取值范围就是:
[1111 1111 , 0111 1111]
#换成 数字为:
[-127 , 127]
(3)原码的优缺点:优点是简单直观,大脑最容易理解,例如,我们用8位二进制表示一个数,+11的原码为00001011,-11的原码就是10001011;缺点就是:原码不能直接参加运算,如果运算可能会出错。例如:数学上,1+(-1)=0,而在二进制中:
00000001+10000001=10000010 # 换算成十进制为-2 ;这显然出错了
所以原码的符号位不能直接参与运算,必须和其他位分开,这就增加了硬件的开销和复杂性
正数的反码还是其本身;负数的反码是在其原码的基础上,符号位不变,其余各个位取反
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
'''
如果一个反码表示的是负数, 除了直观的看到它的最高位是1,它表示是个负数外,
我们无法直观的得出来它的具体数值,通常要将其转换成原码再计算
这里假如直接将负数的二进制反码,按:最高位为符号位“-” 剩下的位数按照二进制来转换的话
'''
11111110 --> -126 显然也不是原来的值 -1
正数的补码还是其本身;负数的补码是其反码+1(也即是:在其原码的基础上, 符号位不变, 其余各位取反, 最后+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
# 如果一个补码表示的是负数, 除了直观的看到它的最高位是1,它表示是个负数外,
# 我们无法直观的得出来它的具体数值,通常要将其转换成原码再计算
# 这里假如直接将负数的二进制补码,按:最高位为符号位“-” 剩下的位数按照二进制来转换的话
11111111 --> -127 显然也不是原来的值 -1
(1)计算机只有加法运算,没有设置减法运算!原因是:对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单;计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂!于是人们想出了将符号位也参与运算的方法;我们知道,根据运算法则:减去一个正数等于加上一个负数,即: 1-1 = 1 + (-1) = 0;所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单!
那么计算机要选取哪种码值来做加法运算呢?我们先来一一尝试,并看结果为什么要选择补码!
# 先补充:计算机是如何计算 减法 的,看下面的例子(这里我们假设已经知道是选取补码来运算)
# 求:1-2 的值
# 首先:将1-2 变成 1+(-2),然后分别将1的补码和-2的补码拿来运算
0000 ... 0001 (正数 1补码还是本身)
+
1111 ... 1110 (-2 的补码)
=
1111 ... 1111 (补码)
# 上面结果一看,不对啊,怎么不是-1啊?不急,有规定,我们先看最高位符号位为1,说明它是个负数
# 如果得到的是负数的话,我们需要将它转换成原码的表示形式,如果是正数(最高位符号位为0)则不需要
# 所以我们接着做:
1111 ... 1111 (补码)
1111 ... 1110 (反码)
1000 ... 0001 (原码)
即结果为-1
(2)如果选择 原码来 存储:
# 假设要计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
'''
所以如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的,
这也就是为何计算机内部不使用原码表示一个数
'''
(3)如果选择 用 反码 来存储:
# 同样假设要计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反
# 再将 反码 转成 原码:
[1111 1111]反 = [1000 0000]原 = -0
'''
看上去可行?因为按照我们通常认为的 0 和 -0 是一样;如果 0 和 -0 是一样的话,那么就有 [0000 0000]原和 [1000 0000]原 两个编码表示0,这样显然会造成混乱,而且[1000 0000]也用来表示0造成浪费!
'''
(4)看了上面两种码值的缺点后,我们选取补码的话,就能很好的解决0的符号以及两个编码的问题:
# 同样假设要计算十进制的表达式: 1-1=0 这次我们选取补码
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补
(这里或许会有一个疑问:[0000 0001]补 + [1111 1111]补 相加 [0000 0000]补 进位后,多出的一位 去哪了?后面会详细说明下)
# 再将补码 转成 原码:
[0000 0000]补=[0000 0000]原 # 可以把它看成一个正数,或者是单独的0
(补充:0的反码、补码 都是0)
接着上面,这样0用[0000 0000]表示,而且可以用[1000 0000]表示 -128,也即是用 -128 代替了 -0 (看下面详细推导):
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
即是,相比较于反码和原码要将最高位当做一个符号位的做法,采取补码可以多表示一位(之前我们认为1111 1111是 -127 它最小也只能表示这么多了,因为最高位是符号位,剩余位数都为1了)!
同时:使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数;这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127];而使用补码表示的范围为[-128, 127];
因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位,而使用补码表示时又可以多保存一个最小值.
(5)总结:
原码+反码:8位原码和反码能够表示数的范围是 [-127–127];
补码:8位补码能够表示数的范围是 [ -128 – 127]
(在补码中用 -128代替了-0,所以补码的表示范围为:(-128-0-127)共256个)
(以上内容参考:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html 进行简化和修改补充)
>>>bin(10)
'0b1010'
>>> bin(20)
'0b10100'
(2)内置函数 int() 用于将一个字符串或数字转换为整型。
语法格式:class int(x, base=10) ;x – 字符串或数字,base – 进制数,默认十进制。
>>>int() # 不传入参数时,得到结果0
0
>>> int(3)
3
>>> int(3.6)
3
>>> int('12',16) # 如果是带参数base的话,12要以字符串的形式进行输入,12 为 16进制
18
>>> int('0xa',16)
10
>>> int('10',8)
8
注意事项:
若 x–为纯数字时,不能设置base进制,否则报错
>>> int(3.1415926)
3
>>> int(-11.123)
-11
>>> int(2.5,10)
#报错
>>> int(2.5)
2
若 x 为 str,则 base 可略可有。
base 存在时,视 x 为 base 类型数字,并将其转换为 10 进制数字。
若 x 不符合 base 规则,则报错。
>>>int("9",2) #报错,因为2进制无9
>>> int("9")
9 #默认10进制
>>> int("3.14",8)
>>> int("1.2")
#均报错,str须为整数
>>>int("1001",2)
9
# "1001"才是2进制格式,并转化为十进制数字9
>>> int("0xa",16)
10
# ≥16进制才会允许入参为a,b,c...
>>> int("b",8) #报错
>>> int("123",8)
83
# 视123为8进制数字,对应的10进制为83
a1 = bin(-3)
print(a2)
a2 = bin(3)
print(a2)
b = bin(-3 & 0xffffffff)
print(b)
c = bin(0xfffffffd)
print(c)
'''输出结果分别为:
-0b11 # 特别注意这个
0b11
0b11111111111111111111111111111101
0b11111111111111111111111111111101
'''
(1)注意,此时Python的坑出现了,首先我们知道Python 中的整型也是用 补码形式存储的,Python 中 bin 一个正数(十进制表示)结果就是它的二进制补码表示形式(看上面的a2结果);但是Python 中 bin 一个负数(十进制表示),输出的是它的原码的二进制表示加上个负号,这显然不是我们要求的负数的补码,因为我们可以求出 -3的补码应该为:ob11111111111111111111111111111101,而不应该是-0b11 ,下面将详细将Python中遇到负数要如何处理!
(2)Python 中 bin 一个负数(十六进制表示),输出的是对应的二进制表示,把上面的例子拿下来↓↓↓
c = bin(0xfffffffd) # 看上图
print(c)
# 结果为:0b11111111111111111111111111111101 (补码)
#(上图中:4294967293不是真值,它是不考虑最高符号位,直接得出的二进制值,这个可以不看)
# 将上面结果的补码,转换成原码为:0b10000000000000000000000000000011 刚好就是-3
# 再来用下 内置方法 int()
c = bin(0xfffffffd)
print(c)
# c='0b11111111111111111111111111111101' (补码)
c1 = '0b10000000000000000000000000000011' (c的原码)
print(int(c, base=2)) # 将c的原码直接用int算,又会出现一个坑,Python的int()也不认符号位,如下结果
'''输出结果
0b11111111111111111111111111111101
2147483651 # 这不是真值
'''
# 所以我们也不要用 int()直接去将负数的二进制数还原成10进制,下面会将快捷方法
11111111 11111111 11111111 11111101 (-3 的 补码)
&
11111111 11111111 11111111 11111111 (0XFFFFFFFF)
=
11111111 11111111 11111111 11111101 (二进制 补码)
# 刚好可以换成 十六进制的
0xFFFFFFFD
# 所以按照上面的推导
b = bin(-3 & 0xffffffff) # 这一步相当于bin(0xfffffffd)
print(b)
# ob11111111 11111111 11111111 11111101 (-3 补码)
# 虽然绕了一圈,但是总算是解决了Python负数的正确表示形式,请看下面的总结↓↓↓
def get_num(n):
count = 0
if n < 0:
n = n & 0xffffffff # 得到负数的补码表示形式
while n:
count += 1
n = n & (n - 1)
return count
print(get_num(2)) # 1
print(get_num(-1)) # 32
(3)但是此时不可以直接将补码扔给 int()函数来转成十进制,上面结果已经得出了,int()也会忽略符号位!那么负数的补码如何转成十进制呢?看下面详解
(1)当然我们可以直接将补码还原成原码,然后求出十进制值,这样做需要三步,而且不适用于代码运算中,**数值在[-256,-1]**之间的负数,我们可以用下面的快捷方式来解答↓↓↓
(2)算出补码的最后8位,直接在二进制中的数 减去 256 即可!嗯?一脸懵逼,为什么这样可以?我们来举例说明:
还是以上面的 -3 上面的补码为例
ob11111111 11111111 11111111 11111101 (-3 补码)
# 我们先取它的最后8位即是 11111101
# 将最后8位 直接忽略符号位,算出它的值为:253
# 让后用 253-256=-3 神奇啊,Python居然可以这样
(3)所以我们总结下数值在[-256,-1]之间的负数,求负数补码转换成十进制的方式:
c = bin(-3 & 0xffffffff) # 模拟一个负数
# c ='0b11111111111111111111111111111101' 模拟一个负数
print(int(c[-8:], base=2) - 256) # 取最后8位,然后忽略符号位求出值再减去256
# -3
(4)原理:结合我们上面到现在所有的知识点,我们选择了以补码的形式来存储(运算),如果直接无视符号位,8位二进制数结果就是所以位数的相加的和,它的最大值就是11111111=255,如果带上符号‘1’1111111,它的补码是‘1’0000001结果是 -1,为了能真正得到这个-1,我们必须让 255-256,所以在这个在[-256,-1]之间的负数,可以使用这个原理!
a = 60 # 60 = 0011 1100
b = 13 # 13 = 0000 1101
c = 0
c = a & b; # 12 = 0000 1100
print ("1 - c 的值为:", c)
c = a | b; # 61 = 0011 1101
print ("2 - c 的值为:", c)
c = a ^ b; # 49 = 0011 0001
print ("3 - c 的值为:", c)
c = ~a; # -61 = 1100 0011
print ("4 - c 的值为:", c)
c = a << 2; # 240 = 1111 0000
print ("5 - c 的值为:", c)
c = a >> 2; # 15 = 0000 1111
print ("6 - c 的值为:", c)
'''输出结果
1 - c 的值为: 12
2 - c 的值为: 61
3 - c 的值为: 49
4 - c 的值为: -61
5 - c 的值为: 240
6 - c 的值为: 15
'''
特别注意:所有的操作都是在补码的基础上来操作的,正数当然原码、反码、补码一样不用加以考虑,负数就要注意了,一定要先求出它的补码,再来进行位运算和移位运算的操作,如下例子:
# 求 ~ -5 (给 -5 取反)
# 步骤:先求出 -5的补码
1000 .... 0101 (原码) # 总共有32位,为了方便写中间就省略了
1111 .... 1010 (反码)
1111 .... 1011 (补码)
# 然后进行 取反操作(取反操作不分符号位,即对所有位都取反)
1111 .... 1011 (补码)
~
0000 .... 0100 (结果符号位为正数,所以可以直接表示为:4)
# 求 -3 ^ 3 (求 -3 异或 3)
# 步骤:先求出 -3 补码
1000 .... 0011 (原码)
1111 .... 1100 (反码)
1111 .... 1101 (补码)
# 再来完成和 3 的异或
1111 .... 1101 (-3 的补码)
^
0000 .... 0011 (3 的补码)
=
1111 .... 1110 (两者异或的结果)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
1111 .... 1110 (补码)
1111 .... 1101 (反码)
1000 .... 0010 (原码)
# 所以最后-3 ^ 3 异或的结果为 -2
(1)<< 左移运算符
将运算对象的 各二进制位 全部左移若干位:符号位不变,低位(右边)补 0
11 << 2 = 44
# 详细过程
00000000 00000000 00000000 00001011
<< 2
00000000 00000000 00000000 00101100 (因为最高位是0,它表示一个正数)
=
44
————————————
-14 <<2 =-56
# 详细过程
11111111 11111111 11111111 11110010 (-14的补码)
<< 2
11111111 11111111 11111111 11001000 (补码)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
11111111 11111111 11111111 11001000 (补码)
11111111 11111111 11111111 11000111 (反码)
10000000 00000000 00000000 00111000 (原码)
# 所以最后结果为:
-56
左移,对于正数来说,左移多少位等于 乘以 2 ^ (左移位数);左移1 相当于乘以2(但效率比乘法高)
(2)>> 右移运算符
将运算对象的 各二进制位 全部 右移若干位:低位溢出,符号位不变,并用符号位补溢出的高位
4 >> 2 = 1
# 详细过程
00000000 00000000 00000000 00000100
>> 2
00000000 00000000 00000000 00000001(因为最高位是0,它表示一个正数)
# 所以最后结果为:
1
————————————
-14 >> 2 = -4
# 详细过程
11111111 11111111 11111111 11110010 (-14的补码)
>> 2
11111111 11111111 11111111 11111100 (补码)
# 看上面的结果,符号位为1是负数,我们还要将它转换成 原码 才能计算出它的值
11111111 11111111 11111111 11111100 (补码)
11111111 11111111 11111111 11111011 (反码)
10000000 00000000 00000000 00000100 (原码)
# 所以最后结果为:
-4
右移,对于正数来说,右移多少位等于 除以 2 ^ (右移位数);右移 1 相当于除以2(效率比除法高哦)
# 给定两个整形变量a、b要求在不使用其他变量的情况下,完成两个变量值得交换
# 当然 Python可以直接 a,b=b,a 本质是因为Python中的‘=’ 不是直接赋值,而是内存地址的引用;
# 但是其他语言 像java 要完成的话,需要借助一个中间变量 来完成交换,这里学了位运算可以妙用一下
a = 1
b = 2
a = a ^ b
b = a ^ b
a = a ^ b
print(a, b) # 2 1
# 连续 3 次 异或 操作便可以互换两变量的值
# x&(x-1) 该操作可以把 x 二进制形式中最低位的1转化成0 例如:
x=0b1010110
x-1=0b1010101
x&(x-1)
1010110
&
1010101
=
1010100
x = 0b01010110
x1 = x & ~(x - 1)
if x1 == 0b00000010:
print('true')
print(bin(x1))
print(x1)
'''
true
0b10
6
'''
def swapBit(x, i, j):
# 如果第i位和第j位上的数值相同那就没必要进行操作
if ((x>>i) & 1) != ((x>>j) & 1):
x ^= ((1<<i) | (1<<j))
return x