C++ 补码详解

C++ 补码详解【原码定义】 符号位为 0 代表正数,符号位为 1 代表负数,数值位为真值的绝对值。 计算机的四则运算希望设计的尽量简单。但是引入符号位的概念,对于计算机来说还要考虑正负数相加,等于引入了减法,所以希望是计算机底层只设计一个加法器,就能把加法和减法都做了。

文章目录


在这里插入图片描述


一、引例

	printf("%d\n", abs(INT_MIN));
  • 这段的代码正确输出应该是什么呢?
  • 凭直觉肯定是个正数,因为 abs 这个函数是求一个数的绝对值,数学上任何数的绝对值都是大于等于0的;然而…

-2147483648

  • 那么我们来研究一下,计算机背后到底做了什么手脚;

二、机器数和真值

1、机器数

  • 我们知道计算机是内部由 0 和 1 组成的编码,无论是整数还是浮点数,都会涉及到负数,对于机器来说是不知道正负的,而 “正” 和 “负” 正好是两种对立的状态,所以规定用 “0” 表示 “正”,“1” 表示 “负”,这样符号就被数字化了,并且将它放在有效数字的前面,就成了有符号数;
  • 把符号 “数字化” 的数称为 机器数

2、真值

  • 而带有 “+” 或者 “-” 的数称为 真值
  • 然而,当符号位和数值部分放在一起后,如何让它一起参与运算呢?那就要涉及到接下来要讲的计算机的各种编码了;

三、计算机编码

1、原码

  • 这里的原码并不是源码(源代码)的意思,而是机器数中最简单的一种表示形式;为了快速理解,这里只介绍整数,不介绍小数的情况;

【原码定义】 符号位为 0 代表正数,符号位为 1 代表负数,数值位为真值的绝对值。

[ x ] 原 = { x ( 0 < = x < 2 n − 1 ) 2 n − 1 − x ( − 2 n − 1 < x < = 0 ) [x]_原 = \begin{cases} x & (0 <= x < 2^{n-1})\\ 2^{n-1} – x & (-2^{n-1} < x <= 0)\\ \end{cases} [x]原​={x2n−1−x​(0<=x<2n−1)(−2n−1<x<=0)​
(这里 n n n 的取值是 8 、 16 、 32 、 64 8、16、32、64 8、16、32、64,目前计算机的整型 int 都是 32 位的,但是为了便于阅读,本文介绍的整数都按照 8 位来举例)

【原码举例】

  • 1)当真值 x = +100101 时,原码为:00100101;
  • 2)当真值 x = -100101 时,原码为:10100101;
  • 原码是最贴近人类的编码方式,并且很容易和真值进行转换,但是让计算机用原码进行加减运算过于繁琐,如果两个数符号位不同,需要先判断绝对值大小,然后用绝对值大的减去绝对值小的,并且符号以绝对值大的数为准,本来是加法却需要用减法来实现;为了让计算机做的事情尽量简单,于是设计出来了补码;

2、补码

【补码定义】 正数的补码是它本身,符号位为0;负数的补码为原码数值位取反后+1,符号位为1;
[ x ] 补 = { x ( 0 < = x < 2 n − 1 ) 2 n + x ( − 2 n − 1 < = x < 0 ) [x]_补 = \begin{cases} x & (0 <= x < 2^{n-1})\\ 2^{n} + x & (-2^{n-1} <= x < 0)\\ \end{cases} [x]补​={x2n+x​(0<=x<2n−1)(−2n−1<=x<0)​
【补码举例】

  • 1)当真值 x = +100101 时,补码为:00100101;
  • 2)当真值 x = -100101 时, 补码为:11011011;

(尝试把负数的数值部分和它的补码进行相加运算,可以得到 2 n 2^n 2n)

3、反码

  • 反码一般用来作为 补码 求 原码 或者 原码 求 补码 的中间过渡;

【反码定义】 整数的反码是它本身,符号位为0;负数的反码为原码数值取反,符号位为1;
[ x ] 反 = { x ( 0 < = x < 2 n − 1 ) 2 n − 1 + x ( − 2 n − 1 < x < = 0 ) [x]_反 = \begin{cases} x & (0 <= x < 2^{n-1})\\ 2^{n}-1 + x & (-2^{n-1} < x <= 0)\\ \end{cases} [x]反​={x2n−1+x​(0<=x<2n−1)(−2n−1<x<=0)​
【反码举例】

  • 1)当真值 x = +100101 时,补码为:00100101;
  • 2)当真值 x = -100101 时, 补码为:11011010;

(尝试把负数的数值部分和它的补码进行相加运算,可以得到 2 n − 1 2^n-1 2n−1)

4、编码总结

  • 1)这三种机器数的最高位均为符号位;
  • 2)当真值为正数时,原码、补码、反码的表示形式相同,符号位用 “0” 表示,数值部分真值相同;

[ + 1 ] = [ 00000001 ] 原 = [ 00000001 ] 反 = [ 00000001 ] 补 [+1] = [00000001]_原 = [00000001]_反 = [00000001]_补 [+1]=[00000001]原​=[00000001]反​=[00000001]补​

  • 3)当真值为负数时,原码、补码、反码的表示形式不同,但是符号位都用 “1” 表示,数值部分:反码是原码的 “每位求反”,补码是原码的 “取反加1”;

[ − 1 ] = [ 10000001 ] 原 = [ 11111110 ] 反 = [ 11111111 ] 补 [-1] = [10000001]原 = [11111110]反 = [11111111]补 [−1]=[10000001]原=[11111110]反=[11111111]补

四、为什么要用补码

1、主要目的

  • 计算机的四则运算希望设计的尽量简单。但是引入符号位的概念,对于计算机来说还要考虑正负数相加,等于引入了减法,所以希望是计算机底层只设计一个加法器,就能把加法和减法都做了。

2、原码运算

  • 对于原码的加法,两个正数相加的情况如下:

1 + 1 = [ 00000001 ] 原 + [ 00000001 ] 原 = [ 00000010 ] 原 1 + 1 = [00000001]_原 + [00000001]_原 = [00000010]_原 1+1=[00000001]原​+[00000001]原​=[00000010]原​ = 2

  • 好像没有什么问题?于是人们开始探索减法,但是起初设计的人的初衷是希望不用减法,只用加法运算就能够将加法和减法都包含进来,于是,我们尝试用原码的负数表示来做运算;

1 − 2 = 1 + ( − 2 ) = [ 00000001 ] 原 + [ 10000010 ] 原 = [ 10000011 ] 原 1 – 2 = 1 + (-2) = [00000001]_原 + [10000010]_原 = [10000011]_原 1−2=1+(−2)=[00000001]原​+[10000010]原​=[10000011]原​ = -3

  • 这个结果是错的,于是为了解决减法问题,引入了反码运算;

3、反码运算

  • 对于反码的加法,一正一负两数相加的情况如下:

1 − 2 = 1 + ( − 2 ) = [ 00000001 ] 反 + [ 11111101 ] 反 = [ 11111110 ] 反 1 – 2 = 1 + (-2) = [00000001]_反 + [11111101]_反 = [11111110]_反 1−2=1+(−2)=[00000001]反​+[11111101]反​=[11111110]反​ = -1

  • 没有什么问题?但是某种情况下,反码会有歧义,当两个相同的数相减时,如下:

1 − 1 = 1 + ( − 1 ) = [ 00000001 ] 反 + [ 11111110 ] 反 = [ 11111111 ] 反 1 – 1 = 1 + (-1) = [00000001]_反 + [11111110]_反 = [11111111]_反 1−1=1+(−1)=[00000001]反​+[11111110]反​=[11111111]反​ = -0

  • 这里出现了一个奇怪的概念,就是 “负零”,反码运算过程中会出现有两个编码表示零这个数值;
  • 为了解决正负零的问题引入了补码的概念;

4、补码运算

  • 1)一正一负两个数相加,且非零的情况:

1 − 2 = 1 + ( − 2 ) = [ 00000001 ] 补 + [ 11111110 ] 补 = [ 11111111 ] 补 1 – 2 = 1 + (-2) = [00000001]_补 + [11111110]_补 = [11111111]_补 1−2=1+(−2)=[00000001]补​+[11111110]补​=[11111111]补​ = -1

  • 2)一正一负两个数相加,且答案为零的情况:

1 − 1 = 1 + ( − 1 ) = [ 00000001 ] 补 + [ 11111111 ] 补 = [ 00000000 ] 补 1 – 1 = 1 + (-1) = [00000001]_补 + [11111111]_补 = [00000000]_补 1−1=1+(−1)=[00000001]补​+[11111111]补​=[00000000]补​ = 0

两个互为相反数的数相加后,得到的数的补码为 2 n 2^n 2n(你可以认为是是溢出了),和我们之前提到的定义吻合;

  • 综上所述,计算机内部都是用补码表示;

五、回到原点

  • 最后我来回答下,文章一开始提到的那个问题,即为什么一个负数的绝对值还是一个负数;

  • 这个负数不是一个一般的负数,它是32位有符号整型的最小值,即:
    I N T _ M I N = − 2 31 INT\_MIN = -2^{31} INT_MIN=−231

  • 它的补码表示为:
    [ − 2 31 ] 补 = [ 10000000    00000000    00000000    00000000 ] 补 [-2^{31}]_补 = [10000000\ \ 00000000\ \ 00000000\ \ 00000000]_补 [−231]补​=[10000000  00000000  00000000  00000000]补​

  • 从补码的定义出发:
    [ x ] 补 = { x ( 0 < = x < 2 31 ) 2 32 + x ( − 2 31 < = x < 0 ) [x]_补 = \begin{cases} x & (0 <= x < 2^{31})\\ 2^{32} + x & (-2^{31} <= x < 0)\\ \end{cases} [x]补​={

    x232+x​(0<=x<231)(−231<=x<0)​

2 31 − 2 31 = 2 31 + ( − 2 31 ) = X + [ − 2 31 ] 补 = 0 2^{31} – 2^{31} = 2^{31} + (- 2^{31}) = X + [-2^{31}]_补 = 0 231−231=231+(−231)=X+[−231]补​=0

解这个简单方程得到 X X X 唯一的值为:
[ 10000000    00000000    00000000    00000000 ] 补 [10000000\ \ 00000000\ \ 00000000\ \ 00000000]_补 [10000000  00000000  00000000  00000000]补​

  • 所以在 32位有符号整数体系内, I N T _ M I N = − I N T _ M I N INT\_MIN = -INT\_MIN INT_MIN=−INT_MIN;

今天的文章C++ 补码详解分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23085.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注