2的补码
计算机的底层是一堆的bit,具体怎么理解bit,取决于我们怎么解释,对于同样的8个bit,例如 11111111
,我们可以把它看成是一个无符号数,也可以看成是一个有符号数。
如果我们把它看成是一个无符号数,那么它的值就是255,如果我们把它看成是一个有符号数,那么它的值就是-1, 如果理解为字符,那么它就是一个字符,具体是什么字符,取决于编码方式,如果是ASCII编码,那么它就是DEL字符。
对于无符号数,我们可以直接用二进制表示,例如 11111111
就是255,计算机很轻易就能理解二进制,但是人类比较熟悉十进制。我们看看换算过程:
1*2^7 + 1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 1*2^2 + 1*2^1 + 1*2^0 = 255
但是现实情况中,数字不仅有正数,还有负数,那么我们怎么表示负数呢?我们可以用补码的方式来表示负数。
原码
原码是最简单的一种表示方法,就是用最高位来表示符号,0表示正数,1表示负数,其余位表示数值。例如,00000001
表示1,10000001
表示-1。
原码的优点是,非常直观,可以直接看出一个数是正数还是负数,但是原码的缺点也很明显,就是加减法非常麻烦,因为要考虑符号位。
比如我们要计算1+1,首先我们要找到1的原码,然后再相加,最后再判断符号位。
1的原码:00000001
1的原码:00000001
相加:00000010
换算成十进制:2
但是,如果我们要计算1-1,首先我们要找到1的原码,然后找到-1的原码,然后再相加,最后再判断符号位。
1的原码:00000001
-1的原码:10000001
相加:10000010
换算成十进制:-2
可以看到,计算结果是错误的,这是因为原码的计算中,符号位也参与了计算,导致结果错误。
因此有了反码的出现。
反码
反码是在原码的基础上,对负数的数值部分取反。例如,00000001
表示1,11111110
表示-1。
反码的优点是,加减法非常简单,只需要把两个数的反码相加即可,不需要考虑符号位。
比如我们要计算1+1,首先我们要找到1的反码,然后再相加,最后再判断符号位。
1的反码:00000001
1的反码:00000001
相加:00000010
换算成十进制:2
如果我们要计算1-1,首先我们要找到1的反码,然后找到-1的反码,再相加,最后再判断符号位。
1的反码:00000001
-1的反码:11111110
相加:11111111
换算成十进制:-0
但是反码的缺点也很明显,就是有两种0,正0和负0,这样会导致一些问题,因此有了补码的出现。
什么是补码
补码是一种用来表示负数的方法,它的特点是,正数的补码就是它本身,负数的补码是它的绝对值的二进制取反加1。
例如,我们要表示-1,首先我们要找到1的二进制表示,然后取反,最后加1,就是-1的补码。
1的二进制表示:00000001
取反:11111110
加1:11111111
所以-1的补码是11111111。
如果我们要计算1+1,首先我们要找到1的补码,然后再相加,最后再判断符号位。
1的补码:00000001
1的补码:00000001
相加:00000010
换算成十进制:2
如果我们要计算1-1,首先我们要找到1的补码,然后找到-1的补码,再相加,最后再判断符号位。
1的补码:00000001
-1的补码:11111111
相加:00000000
换算成十进制:0
我们可以看出,2的补码有以下优点:
- 0的补码是00000000,所以0只有一种表示方法,不会出现正0和负0的问题。
- 两个数相加,只需要把它们的补码相加,然后舍弃最高位的进位,就是最终的结果。
- 两个数相减,只需要把减数的补码取反加1,然后和被减数的补码相加,然后舍弃最高位的进位,就是最终的结果。
- 补码的表示范围是-2^(n-1)到2^(n-1)-1,其中n是bit的位数,而原码和反码的表示范围是-2^(n-1)+1到2^(n-1)-1。
可以看到,补码的形式中,只有一个0,所以不会出现正0和负0的问题,这样就避免了一种情况,同时,两个数相加和相减的操作也变得非常简单,只需要把补码相加即可,简化了硬件的实现。因此, 补码才会被广泛选择作为表示负数的方法。
总结
记得初学计算机的时候,觉得补码难以理解,工作以后,才慢慢理解,现实情况中,我们往往会因为实际情况拖鞋,补码就是如此,舍弃了易理解性,但是提高了计算的效率,这就是现实情况中的折中。
软件工程中,到处都是取舍,我们要根据实际情况,选择最合适的方法,往往我们都找不到完美的方案,但是却有一个最合适的方案。
更多文章
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- frp 源码阅读与分析(一):流程和概念
- 用peewee代替SQLAlchemy
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(一):runc和containerd
- Docker组件介绍(二):shim, docker-init和docker-proxy
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程