数值的表示

任何存储于计算机中的数据,其本质都是以二进制码存储的。计算机中所有的数都以补码形式存在,加减运算都是补码间的加法运算。为何及其要用补码存储,我们需要了解原码、反码和补码的概念。

原码

原码是符号位加真值的绝对值。有符号的二进制数将最高位作为符号位用来区分正负数。 正数的符号为 0,负数的符号为 1。

对于 8 位二进制数:

1
2
+1 的原码为 0000 0001
-1 的原码为 1000 0001

使用原码表示数值会出现两个 0

1
2
+0 的原码为 0000 0000
-0 的原码为 1000 0000

因此使用原码表示 8 位二进制数的范围就是 [1111 1111, 0111 1111] 也就是 [-127, 127],里面包括了 +0 和 -0 两个数,总共有 256 个数

反码

  • 正数的反码与其原码相同
  • 负数的反码是在其原码的基础上,符号位不变,其余逐位取反。

那么对于 8 位二进制数:

1
2
+1 = 0000 0001(原码) = 0000 0001(反码)
-1 = 1000 0001(原码) = 1111 1110(反码)

这样的话也会有两个 0, 0000 0000(+0) 和 1111 1111(-0)

补码

  • 正数的补码是其本身
  • 负数的补码是在反码的基础上 +1

对于 8 位二进制数

1
2
+1 = 0000 0001(原码) = 0000 0001(反码) = 0000 0001(补码)
-1 = 1000 0001(原码) = 1111 1110(反码) = 1111 1111(补码)

在计算机的运算中,只有加法没有减法,减法就相当于加上这个数的相反数,如果我们将符号位参与运算,并且只保留加法运算,对于原码来说,无法得到正确的结果,比如 1-1=0 这个运算。

  1. 用原码进行计算减法,结果是不正确的:
1
1 - 1 = 1 + (-1) = 0000 0001(原) + 1000 0001(原) = 1000 0010(原) = -2
  1. 用反码进行计算减法,得到结果的真值是正确的,但是对于 0 这个结果具有特殊性。对于人类理解来说 +0 和 -0 是相同的,但对于机器而言,带有符号的 0 没有意义,且 0 会存在 0000 0000 和 1000 0000 两个编码。
1
1 - 1 = 1 + (-1) = 0000 0001(反) + 1111 1110(反) = 1111 1111(反) = 1000 0000(原) = -0
  1. 用补码进行计算减法,能够修复原码中 0 的符号正负之分,及存在两个编码的问题。 0 只有一个编码 0000 0000
1
1 - 1 = 1 + (-1) = 0000 0001(补) + 1111 1111(补) = 0000 0000(补) = 0000 0000(原) = 0

而且可以用 1000 0000(补) 来表示整数 -128

1
-1 - 127 = -1 + (-127) = 1000 0000(原) + 1111 1111(原) = 1111 1111(补) + 1000 0001(补) = 1000 0000(补)

实际上就是以前的 -0 用来表示 -128, 因此 -128 并没有原码和反码的表示(对 -128 的补码 1000 0001 算出来的原码是 0000 0000 是不正确的)

在 8 位二进制中:

  • 使用原码/反码表示的范围为 [-127, +127], 包含 +0 和 -0 共 256 个数
  • 使用补码表示的范围为 [-128, +127], 0 没有符号

由于机器使用补码,能够多表示一个最低数,那么 8 位的二进制数用原码或反码表示的范围是 [-127, 127], 而用补码表示的范围是 [-128, 127]

编程中常用到的 32 为 int 类型,能够表示的范围是 [-2^31, 2^31-1]

INT32

32 位的二进制数可以用 16 进制来表示:也就是将二进制的每相邻 4 位作为一个二进制数,计算出其值,再用 16 进制来表示。
比如 32 位的最大值: 0111 1111 1111 1111 1111 1111 1111 1111 (第一位是符号位)
用 16 进制表示就是: 0x7fff ffff (0x 开头表示 16 进制,16 进制的 15 是 f)

对于有符号的 32 位整数,0x8000 0000 可以表示其最小值
其二进制的原码是: 1000 0000 0000 0000 0000 0000 0000 0000 (最左边的 1 是符号位)
反码是: 1111 1111 1111 1111 1111 1111 1111 1111 (符号位不变,其他取反)
补码是: 1 1000 0000 0000 0000 0000 0000 0000 0000 (反码 +1, 有进位)
补码的值为 -2^31

有符号的 32 位的最大值为 0x7fff ffff
有符号的 32 位的最小值为 0x8000 0000

不用加减乘除做加法

编程题: 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、* 、/四则运算符号

1
2
3
4
5
6
7
8
9
# -*- coding:utf-8 -*-
class Solution:
def Add(self, num1, num2):
while num2:
sum = (num1 ^ num2) & 0xffffffff
carry = 0xffffffff & (num1 & num2) << 1
num1 = sum
num2 = carry
return num1 if num1 <= 0x7fffffff else ~(num1^0xffffffff) # ~(num1^0xffffffff) 相当于 num1- 2 ** 32

在 python 中,要解决这道题比较麻烦的一点在于 python 中的位运算需要进行越界检查,要理解这道题的解决方法需要学习一下关键点:

  1. 原码、反码、补码

  2. 越界检查
    由于 python 的 long 类型可以表示无限位,不存在数值溢出情况,因此要人工设置边界,避免死循环。在编程中常用到的是 32 位整形,因此我们把数值和 0xffffffff 进行按位与操作, 0xffffffff 代表 16 进制下的边界,也就是 32 位整数的边界。

  3. 正负数判断
    正数 & 0xffffffff 得到的数仍是其本身
    负数 & 0xffffffff 得到对应二进制数的真值,得到的值会超过最大值

比如 -15 & 0xff = 1000 1111(原) & 1111 1111 = 1111 0001(补) & 1111 1111 = 1111 0001 = 241
而 8 为二进制的最大值为 127,因此 -15 & 0xff 超过了 127,因此我们可以根据结果与最大值比较判断是正数还是负数。

32 位整数的最大值为 0x7fffffff,如果 num1 <= 0xfffffff,就返回本身;否则,返回原来负数的值,也就是 ~(num1^0xffffffff)

参考 https://blog.csdn.net/lrs1353281004/article/details/87192205