计算机是怎么存储整数的,原码、反码、补码又是个啥?

简介: 计算机是怎么存储整数的,原码、反码、补码又是个啥?

昨天在群里面看到耳濡目染同志发了这么一张图。

问的是 -1155459666 怎么变成 3139507630,这个问题比较有意思,虽然简单,但涉及到了原码、反码、补码相关的知识,本篇文章就来详细解释一下。

抛出一个问题,有两个整数 94、78,如果让你计算它们的和,你会怎么做?不用想,我们都会像下面这样。

c6cee4ede5aed1b58e9bee201f27b36d.png

十进制是逢十进一,当产生溢出时会往前进一位,因此结果就是 110 + 062 等于 172。但计算机在运算时会使用二进制,本质是一样的,只不过二进制是逢二进一。

fc0cf5892d3fe55ce62ae13a8342ed1a.png

我们验证一下。

print(94 + 78)  # 172
print(0b10101100)  # 172

另外我们知道,与运算时,如果两个二进制位均是 1,那么结果是 1否则结果是 0;异或运算时,如果个二进制位相同,结果是 0,否则结果是 1。

# 与运算,两个二进制位均是 1,结果才是 1
print(1 & 1)  # 1
print(1 & 0)  # 0
print(0 & 1)  # 0
print(0 & 0)  # 0
# 异或运算,两个二进制位相同,结果为 0,相异结果为 1
print(1 ^ 1)  # 0
print(1 ^ 0)  # 1
print(0 ^ 1)  # 1
print(0 ^ 0)  # 0

然后再观察一下之前的式子。

001ee67aac794e8c0fa3287fc4e086c2.png

所以当两个整数相加时,我们还可以这么做。

a = 94
b = 78
# 无进位加法
print(a ^ b)  # 16
# a + b 的进位
# 既然是进位,那么 (a & b) 要左移一位
print((a & b) << 1)  # 156
# 两者相加
print(
    ((a & b) << 1) + (a ^ b)
)  # 172

当然两个整数直接相加的话,这么做没啥意义,它一般会应用于二分查找。在二分查找中,为了避免两个整数相加溢出,求平均值的时候一般会这么做。

l = 123
r = 789
# 对于 C、Java 等语言来说,当 l、r 非常大时,相加可能会产生溢出
print((l + r) // 2)  # 456
# 因此一般都会这么做
print(l + (r - l) // 2)  # 456
# 但显然还有更高效的做法
print((l & r) + ((l ^ r) >> 1))  # 456

然后来说一说原码、反码和补码,不过首先我们要知道负数是如何表示的,对于一个有符号整数来说,它的最高位表示符号位。比如一个 8 位整数:

4d8e55ab8702aeb153739bad8cb4d454.png

开头的符号位如果为 0,则表示正数,为 1 则表示负数,然后剩余的有效位则表示具体的数值。比如 15 的二进制是 1111,那么:

  • 八位整数 15 的二进制就是 00001111;
  • 八位整数 -15 的二进制就是 10001111;

由于 7 个位能表示的最大整数是 2 的 7 次方减 1,所以八位整数的最大值就是 127,而最小值是 -128。那么问题来了,为什么最小值是 -128 呢?这就涉及到原码、反码和补码了。

计算机为了人类阅读方便,会以原码的形式展示,但在计算和存储的时候则是以补码的形式。

  • 对于正数来说,它的原码、反码、补码是相同的。
  • 对于负数来说,它的反码等于原码的符号位不变、其它位取反(1 变 0,0 变 1),而补码则等于反码加 1。当然这是基于原码求补码,反过来也是一样。反码也等于补码的符号位不变、其它位取反,然后再加 1 得到原码,过程是一样的。

我们举个例子,假设两个八位整数 17 和 -5 相加。

88e40dec79d62e50d0c7b3c4fdb91e25.png

因此 00001100 就是两个整数的补码相加之后的结果,也就是 12,由于是正数,它的原码和反码也是 12。

再举个例子,假设两个八位整数 -17 和 5 相加。

1408ac855703f21b7650668e2adceae9.png

相信你应该明白整个逻辑了,计算机在存储整数时会以补码存储,运算也是以补码的形式,但展示则是以原码的形式。我们以 C 语言为例,来直观感受一下这个过程。

#include <stdio.h>
int main(int argc, char const *argv[]) {   
    // 对于有符号整数来说,最高位表示符号位
    // 但对于无符号整数来说,所有的位都是有效位
    // 所以无符号八位整数的范围是 0 ~ 255
    // 147 的二进制为 0b10010011
    unsigned char num = 147;
    // 由于是正数,所以原码、反码、补码都是 0b10010011
    printf("%hhu\n", num);  // 147
    // 但 C 语言不看你怎么存,就看你怎么读
    // 如果以 %hhd 来读取的话,那么会以有符号的格式打印八位整数
    // 此时 10010011 开头的 1 就变成了符号位
    // 而计算机存储的 10010011 是补码,展示的时候要转成原码
    // 反码:补码符号位不变,其它位取反,等于 11101100
    // 原码:反码加 1,等于 11101101,转成十进制打印就是 -109
    printf("%hhd\n", num);  // -109
    return 0;
}

我们再举个例子:

#include <stdio.h>
int main(int argc, char const *argv[]) {   
    // 八位有符号整数 -1 的原码是 1000_0001
    // 反码是 1111_1110,补码是 1111_1111
    // 所以计算机在存储 -1 的时候,存的就是二进制的 11111111
    char num = -1;
    // 但我们以无符号形式打印,那么 11111111 就全部都是有效位
    printf("%hhu\n", num);  // 255
    printf("%hhu\n", 0b11111111);  // 255
    // 再比如我们使用 16 位整数,原码是 10000000_00000001
    // 那么补码就是 11111111_11111111
    // 如果以无符号格式打印,结果显然是 65535
    short num2 = -1;
    printf("%hu\n", num2);  // 65535
    printf("%hu\n", 0b1111111111111111);  // 65535
    // 32 位整数也是如此
    int num3 = -1;
    // 2 的 32 次方减 1,结果是 4294967295
    printf("%u\n", num3);  // 4294967295
    return 0;
}

总结,对于一个有符号整数来说:

  • 如果是正数(符号位是 0),它的原码、反码、补码是一致的。
  • 如果是负数(符号位是 1),反码等于原码的符号位不变、其它位取反,补码等于反码加 1;或者反码等于补码的符号位不变、其它位取反,原码等于反码加 1。


计算机存储和运算使用的都是补码,但展示的是原码。

现在回到开头的问题了,-1155459666 是怎么变成 3139507630 的。

ef429092df2d8500e394f88234959e0d.png

还是那句话,不看你怎么存,就看你怎么读,反正存储的就是这一坨二进制。如果以有符号格式读取,最高位是符号位,结果就是 -1155459666;以无符号格式读取,最高位也是有效位,结果就是 3139507630。

from ctypes import c_int, c_uint
print(c_int(-1155459666).value)
"""
-1155459666
"""
print(c_uint(-1155459666).value)
"""
3139507630
"""
print(0b10111011_00100001_00010101_10101110)
"""
3139507630
"""

那么问题来了,为什么要整出补码这个东西出来呢?很好理解,有了补码,加法和减法可以共用一套逻辑,无论是 a + b 还是 a - b,底层的运算逻辑是相同的。

最后再来解释一下,为什么有符号八位整数的最大值是 127,最小值是 -128。

fde4a323927f5277ed4594cef4f3b59a.png

两个整数 a 和 b,首先 a 的值肯定是 0,但问题是 b 的值是多少?难道是 -0 吗?如果不考虑 0,那么有符号八位整数能表达的正数范围是 1 ~ 127,负数范围是 -1 ~ -127,然后还剩下两个 0。于是让 00000000 表示 0,让 10000000 表示 -128。

ce154f775e60c46b82d953fd138e0faf.png

不管是多少位的有符号整数,它的最小值的绝对值一定比最大值多 1,因此八位整数的最小值是 -128,最大值是 127。

事实上 10000000 表示 -128 有些人为规定的意思,但它确保了数字范围的对称性,并允许有效地利用二进制位来表达整数。

以上就是整在底层的存储方式,以及原码、反码、补码之间的关系。

相关文章
|
JSON 前端开发 Java
SpringBoot中Date格式化处理
日期格式化处理:从混乱到清晰,轻松转换日期格式
1120 1
|
编解码
一文详解 URLEncode
使用浏览器进行Http网络请求时,若请求query中包含中文,中文会被编码为 `%+16进制+16进制`形式,但你真的深入了解过,为什么要进行这种转义编码吗?编码的原理又是什么?
2078 0
一文详解 URLEncode
|
2月前
|
安全 关系型数据库 API
深入源码:Hermes Agent 如何实现 "Self-Improving"
Hermes Agent 是首个实现“自我进化”的AI智能体,上线半年GitHub星标破10万。它通过Memory(记人)、Skill(记事)、Nudge Engine(提醒学习)三大系统闭环,让Agent越用越懂你、越用越强——非手写配置,而是自动从实践中提炼可复用技能并持续优化。
深入源码:Hermes Agent 如何实现 "Self-Improving"
|
缓存 JavaScript Java
SpringBoot集成onlyoffice实现word文档编辑保存
SpringBoot集成onlyoffice实现word文档编辑保存
3041 0
|
6月前
|
Java 应用服务中间件 Maven
Spring Boot 开发环境搭建和项目启动
本文介绍了Spring Boot开发环境的完整搭建流程,涵盖JDK、Maven配置,IDEA与Eclipse中JDK的设置,项目创建、编码统一为UTF-8,以及通过Spring Initializr快速构建工程。详细解析了项目结构、核心注解@SpringBootApplication,编写首个Controller并测试访问,支持端口修改。助力开发者快速入门Spring Boot,实现“开箱即用”的高效开发体验。
|
网络协议 数据安全/隐私保护 网络架构
OSI七层模型和TCPIP五层模型
OSI七层模型和TCPIP五层模型
|
存储 缓存 应用服务中间件
|
人工智能 IDE 程序员
GitHub Copilot 免费了!程序员们的福音来了!
《GitHub Copilot 免费了!程序员们的福音来了!》 近日,GitHub 宣布其 AI 编程助手 GitHub Copilot 现在可以免费使用。曾经每月需支付 10 美元订阅费的 Copilot,现在向所有人开放免费版本,这对个人开发者、初学者和小型团队来说是个大好消息。免费版支持 GPT 和 Claude 模型,并提供每月 2000 次代码补全和 50 条聊天消息等核心功能。用户只需注册或登录 GitHub 账户,在 VS Code 中安装扩展并激活免费版即可使用。此外,Visual Studio Code 也完全免费,进一步降低了开发门槛。 除了
13380 7
GitHub Copilot 免费了!程序员们的福音来了!
|
IDE Java API
IDEA 2022 之 Lombok 使用 教程
IDEA 2022 之 Lombok 使用 教程
1759 0
|
网络协议 网络安全 程序员
socket,tcp,http三者之间的原理和区别
socket,tcp,http三者之间的原理和区别
socket,tcp,http三者之间的原理和区别