1. 本章总览

这一章回答一个最基本的问题:计算机到底是什么?
作者从两个著名的理论模型出发来定义计算机:

  1. 图灵模型(Turing Model) — 计算机的哲学/数学定义
  2. 冯·诺伊曼模型(Von Neumann Model) — 现代计算机的工程实现基础
    然后介绍计算机的三大组成部分、发展历史以及社会伦理问题。

2. 图灵模型(Turing Model)

2.1 什么是数据处理器?

最简单的理解:计算机就像一个黑盒子

ASCII 示意图:
 输入数据          计算机           输出数据
[3, 12, 8, 22] → [  黑盒子  ] → [3, 8, 12, 22]

用公式表达:
输出 = f ( 输入数据 ) 输出 = f(输入数据) 输出=f(输入数据)
问题:这个定义太宽泛了,连口袋计算器也算"计算机",而且没说明它能做几种事情。

2.2 可编程数据处理器(Turing 模型的核心)

Alan Turing 在 1937 年提出:在黑盒子里加一个程序,这就是图灵模型的关键。

程序 = 一组告诉计算机"该对数据做什么"的指令集合
用公式表达:
输出 = f ( 输入数据 ,  程序 ) 输出 = f(输入数据,\ 程序) 输出=f(输入数据, 程序)
这意味着输出取决于两个因素:输入数据 + 程序。

2.3 三种情况演示

情况一:同一程序,不同输入数据

程序:排序
输入 [3, 12, 8, 22]  →  输出 [3, 8, 12, 22]
输入 [14, 6, 8, 12]  →  输出 [6, 8, 12, 14]

情况二:同一输入数据,不同程序

输入数据:[3, 12, 8, 22]
程序:排序   →  输出 [3, 8, 12, 22]
程序:求和   →  输出 45
程序:找最小 →  输出 3

情况三:同一程序 + 同一数据 → 结果永远相同
f ( 程 序 A ,  数 据 X ) = 结 果 Y (每次都一样) f(程序_A,\ 数据_X) = 结果_Y \quad \text{(每次都一样)} f(A, X)=Y(每次都一样)

2.4 通用图灵机(Universal Turing Machine)

只要提供合适的程序,就能做任何可计算的事情。
这就是现代计算机的哲学基础——一台机器通过换程序,可以完成无数种任务。

3. 冯·诺伊曼模型(Von Neumann Model)

John von Neumann 在 1944–1945 年提出:程序和数据都应该存放在内存里
这是对图灵模型的工程化落地。

3.1 四大子系统

输入设备
Input

内存
Memory

算术逻辑单元 ALU

控制单元
Control Unit

输出设备
Output


子系统 中文名 功能说明
Memory 内存 存放程序和数据
ALU 算术逻辑单元 做加减乘除、逻辑运算
Control Unit 控制单元 指挥其他三个部件协调工作
Input/Output 输入输出 接收外部数据,输出处理结果

3.2 存储程序概念(Stored Program Concept)

早期计算机:程序 = 拨开关 + 重新接线(每次换任务都要人工改硬件)
冯·诺伊曼的革命性想法:

程序和数据在逻辑上是一样的东西,都用 0 0 0 1 1 1 表示,都存在内存里!

内存里的样子:
┌────────────────────┐
│  程序(指令序列)   │  ← 也是二进制
│  1. 读入第一个数   │
│  2. 读入第二个数   │
│  3. 两数相加       │
│  4. 输出结果       │
├────────────────────┤
│  数据              │  ← 也是二进制
│  3, 12, 8, 22 ... │
└────────────────────┘

3.3 顺序执行指令

控制单元按顺序执行指令:
取指令 → 解码 → 执行 → 取下一条指令 → ⋯ 取指令 \rightarrow 解码 \rightarrow 执行 \rightarrow 取下一条指令 \rightarrow \cdots 取指令解码执行取下一条指令
(某条指令可以让程序跳转,但整体仍是有序执行的)

4. 计算机的三大组成部分

计算机系统

硬件 Hardware

数据 Data

软件 Software

内存

算术逻辑单元

控制单元

输入输出

存储:二进制 0/1

组织:从小单元到大单元

程序

算法

编程语言

软件工程

操作系统

4.1 数据

计算机内部只认识两种状态(电信号的有或无):
计算机内部数据 = 二进制序列 = 0  和  1  的组合 \text{计算机内部数据} = \text{二进制序列} = 0\ \text{和}\ 1\ \text{的组合} 计算机内部数据=二进制序列=0  1 的组合
现实世界的各种数据,都要先转换成二进制才能存入计算机:

数据类型 现实形式 计算机存储形式
数字 十进制 0–9 二进制 0/1
文字 字母、汉字 二进制编码
图像 像素颜色 二进制编码
音频 声波 二进制采样
视频 连续图像 二进制压缩

4.2 软件

程序为什么要由指令组成?
答案是:可复用性(Reusability)
不同程序可以组合同一套指令,就像用同一套积木搭不同的房子。
算法是编程的基础:先用步骤描述解法,再用指令实现:

例:求两数之和的算法
步骤 1:读入第一个数 a
步骤 2:读入第二个数 b
步骤 3:计算 sum = a + b
步骤 4:输出 sum

对应的 C++ 代码(完整可运行):

#include <iostream>  // 标准输入输出头文件
int main() {
    // 声明两个整数变量,用于存储输入
    int a, b;
    // 步骤1、2:读入两个数
    std::cout << "请输入第一个数: ";
    std::cin >> a;
    std::cout << "请输入第二个数: ";
    std::cin >> b;
    // 步骤3:计算两数之和
    int sum = a + b;
    // 步骤4:输出结果
    std::cout << "两数之和为: " << sum << std::endl;
    return 0;  // 程序正常结束
}

编程语言的演进

机器语言(纯二进制)
     ↓  难以阅读和书写
汇编语言(符号代替二进制)
     ↓  仍然繁琐
高级语言(FORTRAN, COBOL, C++, Python...)
     ↓  接近人类语言
现代编程语言(更简洁、更安全)

5. 计算机发展历史

5.1 三大时期概览

机械时代(1930年前) 17世纪 Pascal 发明帕斯卡计算器 只能加减 17世纪末 Leibnitz 发明莱布尼茨轮 可乘除 1801 Jacquard 织布机 用打孔卡存储程序 1823 Babbage 差分机 可解多项式方程 1890 Hollerith 制表机 自动处理打孔卡 电子计算机诞生(1930–1950) 1939 ABC计算机 第一台电子专用计算机 1941 Z1 德国通用计算机 1944 Mark I 电气+机械混合 1944 Colossus 破解德国密码 1946 ENIAC 第一台通用全电子计算机 1950 EDVAC 第一台冯诺伊曼模型计算机 计算机世代(1950–至今) 1950-1959 第一代:真空管 1959-1965 第二代:晶体管 1965-1975 第三代:集成电路 1975-1985 第四代:微处理器 1985至今 第五代:笔记本/多媒体/互联网 计算机发展历史

5.2 五代计算机对比


世代 时间 核心技术 特点
第一代 1950–1959 真空管 体积巨大、价格昂贵、只有大机构用得起
第二代 1959–1965 晶体管 体积缩小、成本降低、出现 FORTRAN/COBOL
第三代 1965–1975 集成电路 更小更便宜、出现软件包产业
第四代 1975–1985 微处理器 出现台式电脑(Altair 8800)、计算机网络
第五代 1985–今 综合 笔记本、多媒体、虚拟现实、互联网

5.3 ENIAC 的规模(感受一下)

18000 根真空管,长度 ≈ 30  米,高度 ≈ 3  米,重量 ≈ 30  吨 \text{18000 根真空管},\text{长度} \approx 30\ \text{米},\text{高度} \approx 3\ \text{米},\text{重量} \approx 30\ \text{吨} 18000 根真空管长度30 高度3 重量30 
相比之下,今天手机里的芯片有数十亿个晶体管,重量不到 1 克。

6. 社会与伦理问题

6.1 社会问题

计算机普及

依赖性 Dependency
生活离不开计算机

社会公正 Social Justice
穷人负担不起

数字鸿沟
Digital Divide

电子连接群体
用邮件、网购、流媒体

非电子连接群体
用纸质信件、传统娱乐

数字鸿沟简单来说:

社会分成两类人:能上网的 vs 不能上网的。在发达国家这条鸿沟在缩小,但在发展中国家可能长期存在。

6.2 伦理问题


问题 说明
隐私 Privacy 电子通信难以保证不被窃听,需要网络安全技术保护
版权 Copyright 互联网让复制和分享变得极为容易,数字版权成为难题
计算机犯罪 黑客入侵、病毒传播等新型犯罪在计算机时代涌现

7. 计算机科学作为一门学科

计算机科学分为两大类:

计算机科学
├── 系统领域(Systems)
│   ├── 计算机体系结构
│   ├── 计算机网络
│   ├── 安全
│   ├── 操作系统
│   ├── 算法
│   ├── 编程语言
│   └── 软件工程
└── 应用领域(Applications)
    ├── 数据库
    └── 人工智能

8. 关键概念速查表


术语(英文) 中文 一句话解释
Turing Machine 图灵机 理论上能做任何计算的抽象机器
Von Neumann Model 冯·诺伊曼模型 现代计算机的四部件架构
ALU 算术逻辑单元 负责数学运算和逻辑判断
Control Unit 控制单元 指挥其他部件工作的"大脑"
Stored Program 存储程序 程序和数据一起存在内存里
Algorithm 算法 解决问题的一步步操作流程
Digital Divide 数字鸿沟 能上网与不能上网的社会分层
Operating System 操作系统 管理硬件资源的基础软件

9. 本章核心逻辑梳理(总结)

Alan Turing 1937年
提出图灵模型

计算机 = 数据 + 程序

Von Neumann 1945年
程序也存入内存

现代计算机的四部件
内存 ALU 控制单元 输入输出

五代计算机演进
从真空管到微处理器

今天的计算机科学
系统领域 + 应用领域

带来社会问题
数字鸿沟 隐私 版权 犯罪

第二章:数字系统 — 详细中文解析

1. 本章总览

这一章解决一个根本问题:计算机只认识 0 和 1,那它怎么表示各种数字?
我们日常用十进制(0~9),但计算机内部只有"有电"和"没电"两种状态,所以必须用二进制。为了书写方便,程序员还常用十六进制八进制作为二进制的简写。
本章学习四种进制,以及它们之间如何互相转换。

2. 什么是数字系统?

数字系统就是"用一套符号来表示数量的规则"。
同一个数量可以用不同的系统表示:
42 10 = ( 2 A ) 16 = ( 52 ) 8 = ( 101010 ) 2 42_{10} = (2A)_{16} = (52)_8 = (101010)_2 4210=(2A)16=(52)8=(101010)2
这就好比"马"这个动物,中文叫"马",法语叫"cheval",拉丁语叫"equus"——名字不同,指的是同一个东西。

3. 两大类数字系统

数字系统

位置型
Positional
符号的位置决定其值

非位置型
Non-positional
符号的值固定不变

十进制 base 10

二进制 base 2

八进制 base 8

十六进制 base 16

罗马数字

4. 位置型数字系统的核心公式

在位置型系统中,每个数字符号的位置决定它代表多大的值。
一个数的通用表示:
N = ± S k − 1 × b k − 1 + ⋯ + S 1 × b 1 + S 0 × b 0 + S − 1 × b − 1 + ⋯ + S − l × b − l N = \pm S_{k-1} \times b^{k-1} + \cdots + S_1 \times b^1 + S_0 \times b^0 + S_{-1} \times b^{-1} + \cdots + S_{-l} \times b^{-l} N=±Sk1×bk1++S1×b1+S0×b0+S1×b1++Sl×bl
其中:

  • b b b = 基数(base),即这个系统用几个不同符号
  • S i S_i Si = 第 i i i 位上的符号(数字)
  • 小数点左边的位:次方为 0 , 1 , 2 , … 0, 1, 2, \ldots 0,1,2,(整数部分)
  • 小数点右边的位:次方为 − 1 , − 2 , … -1, -2, \ldots 1,2,(小数部分)
    理解要点:越靠左边的数字,乘以的次方越大,所以权重越大。

5. 十进制(Decimal,base 10)

这是我们最熟悉的系统,基数 b = 10 b = 10 b=10,符号集合 S = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } S = \{0,1,2,3,4,5,6,7,8,9\} S={0,1,2,3,4,5,6,7,8,9}

5.1 整数的位置值

1224 1224 1224 为例:

位置值:  10²   10¹   10⁰
数字:      1     2     4
计算:
N = 1 × 100 + 2 × 10 + 4 × 1
  = 100 + 20 + 4
  = 124

等等,书中的例子是 1224 1224 1224,让我们完整来一遍:
N = 1 × 10 3 + 2 × 10 2 + 2 × 10 1 + 4 × 10 0 = 1000 + 200 + 20 + 4 = 1224 N = 1 \times 10^3 + 2 \times 10^2 + 2 \times 10^1 + 4 \times 10^0 = 1000 + 200 + 20 + 4 = 1224 N=1×103+2×102+2×101+4×100=1000+200+20+4=1224

5.2 最大值公式

k k k 位十进制整数能表示的最大值:
N m a x = 10 k − 1 N_{max} = 10^k - 1 Nmax=10k1
例如 k = 5 k=5 k=5 时: N m a x = 10 5 − 1 = 99999 N_{max} = 10^5 - 1 = 99999 Nmax=1051=99999

5.3 实数(带小数的数)

124.13 124.13 124.13 为例:
R = 1 × 10 2 + 2 × 10 1 + 4 × 10 0 + 1 × 10 − 1 + 3 × 10 − 2 R = 1 \times 10^2 + 2 \times 10^1 + 4 \times 10^0 + 1 \times 10^{-1} + 3 \times 10^{-2} R=1×102+2×101+4×100+1×101+3×102
= 100 + 20 + 4 + 0.1 + 0.03 = 124.13 = 100 + 20 + 4 + 0.1 + 0.03 = 124.13 =100+20+4+0.1+0.03=124.13

6. 二进制(Binary,base 2)

计算机的"母语"!基数 b = 2 b = 2 b=2,符号集合只有 S = { 0 , 1 } S = \{0, 1\} S={0,1}
原因:计算机由电子开关组成,只有"开(1)"和"关(0)"两种状态。

6.1 整数转换:二进制 → 十进制

公式:
N = S k − 1 × 2 k − 1 + ⋯ + S 1 × 2 1 + S 0 × 2 0 N = S_{k-1} \times 2^{k-1} + \cdots + S_1 \times 2^1 + S_0 \times 2^0 N=Sk1×2k1++S1×21+S0×20
例: ( 110 ) 2 (110)_2 (110)2 转换为十进制

位置值:  2²   2¹   2⁰
数字:     1    1    0
N = 1×4 + 1×2 + 0×1 = 4 + 2 + 0 = 6

所以 ( 110 ) 2 = 6 10 (110)_2 = 6_{10} (110)2=610
例: ( 11001 ) 2 (11001)_2 (11001)2 转换为十进制

位置值:  2⁴   2³   2²   2¹   2⁰
数字:     1    1    0    0    1
N = 1×16 + 1×8 + 0×4 + 0×2 + 1×1
  = 16 + 8 + 0 + 0 + 1 = 25

所以 ( 11001 ) 2 = 25 10 (11001)_2 = 25_{10} (11001)2=2510

6.2 二进制常用次方表

2的次方快速参考表:
2⁰  = 1
2¹  = 2
2²  = 4
2³  = 8
2⁴  = 16
2⁵  = 32
2⁶  = 64
2⁷  = 128
2⁸  = 256

6.3 最大值公式

N m a x = 2 k − 1 N_{max} = 2^k - 1 Nmax=2k1
例如 k = 5 k=5 k=5 N m a x = 2 5 − 1 = 31 N_{max} = 2^5 - 1 = 31 Nmax=251=31(即 ( 11111 ) 2 (11111)_2 (11111)2

6.4 二进制小数

例: ( 101.11 ) 2 (101.11)_2 (101.11)2 转换为十进制

位置值:  2²   2¹   2⁰  ·  2⁻¹  2⁻²
数字:     1    0    1       1    1

R = 1 × 4 + 0 × 2 + 1 × 1 + 1 × 0.5 + 1 × 0.25 = 4 + 0 + 1 + 0.5 + 0.25 = 5.75 R = 1\times4 + 0\times2 + 1\times1 + 1\times0.5 + 1\times0.25 = 4+0+1+0.5+0.25 = 5.75 R=1×4+0×2+1×1+1×0.5+1×0.25=4+0+1+0.5+0.25=5.75
所以 ( 101.11 ) 2 = 5.75 10 (101.11)_2 = 5.75_{10} (101.11)2=5.7510

7. 十六进制(Hexadecimal,base 16)

为什么需要十六进制?

  • 二进制太长, 42 10 42_{10} 4210 要写成 ( 101010 ) 2 (101010)_2 (101010)2,6位
  • 直接用十进制看不出二进制的结构
  • 十六进制是二进制的"压缩版":4个二进制位 = 1个十六进制位
    基数 b = 16 b = 16 b=16,符号集合:
    S = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , A , B , C , D , E , F } S = \{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F\} S={0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F}

十六进制 十进制值
0–9 0–9(同十进制)
A 10
B 11
C 12
D 13
E 14
F 15

例: ( 2 A E ) 16 (2AE)_{16} (2AE)16 转换为十进制

位置值:  16²   16¹   16⁰
数字:     2     A     E
          (2)  (10)  (14)
N = 2×256 + 10×16 + 14×1
  = 512 + 160 + 14 = 686

7.1 最大值公式

N m a x = 16 k − 1 N_{max} = 16^k - 1 Nmax=16k1
例如 k = 5 k=5 k=5 N m a x = 16 5 − 1 = 1,048,575 N_{max} = 16^5 - 1 = 1{,}048{,}575 Nmax=1651=1,048,575

8. 八进制(Octal,base 8)

基数 b = 8 b = 8 b=8,符号集合 S = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 } S = \{0,1,2,3,4,5,6,7\} S={0,1,2,3,4,5,6,7}
关系:3个二进制位 = 1个八进制位
例: ( 1256 ) 8 (1256)_8 (1256)8 转换为十进制
N = 1 × 8 3 + 2 × 8 2 + 5 × 8 1 + 6 × 8 0 = 512 + 128 + 40 + 6 = 686 N = 1\times8^3 + 2\times8^2 + 5\times8^1 + 6\times8^0 = 512+128+40+6 = 686 N=1×83+2×82+5×81+6×80=512+128+40+6=686

8.1 最大值公式

N m a x = 8 k − 1 N_{max} = 8^k - 1 Nmax=8k1

9. 四种进制对比总表


系统 基数 符号 示例
十进制 10 0–9 2345.56
二进制 2 0, 1 ( 1001.11 ) 2 (1001.11)_2 (1001.11)2
八进制 8 0–7 ( 156.23 ) 8 (156.23)_8 (156.23)8
十六进制 16 0–9, A–F ( A 2 C . A 1 ) 16 (A2C.A1)_{16} (A2C.A1)16

数字 0 到 15 在四种进制中的对应关系:

十进制 二进制 八进制 十六进制
0 0000 0 0
1 0001 1 1
2 0010 2 2
3 0011 3 3
4 0100 4 4
5 0101 5 5
6 0110 6 6
7 0111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 A
11 1011 13 B
12 1100 14 C
13 1101 15 D
14 1110 16 E
15 1111 17 F

10. 进制转换方法大全

10.1 其他进制 → 十进制(最简单)

方法:每一位数字乘以对应的位置值,然后全部加起来。
例: ( 110.11 ) 2 (110.11)_2 (110.11)2 → 十进制
= 1 × 2 2 + 1 × 2 1 + 0 × 2 0 + 1 × 2 − 1 + 1 × 2 − 2 = 1\times2^2 + 1\times2^1 + 0\times2^0 + 1\times2^{-1} + 1\times2^{-2} =1×22+1×21+0×20+1×21+1×22
= 4 + 2 + 0 + 0.5 + 0.25 = 6.75 = 4 + 2 + 0 + 0.5 + 0.25 = 6.75 =4+2+0+0.5+0.25=6.75
例: ( 1 A .23 ) 16 (1A.23)_{16} (1A.23)16 → 十进制
= 1 × 16 1 + 10 × 16 0 + 2 × 16 − 1 + 3 × 16 − 2 = 1\times16^1 + 10\times16^0 + 2\times16^{-1} + 3\times16^{-2} =1×161+10×160+2×161+3×162
= 16 + 10 + 0.125 + 0.012 ≈ 26.137 = 16 + 10 + 0.125 + 0.012 \approx 26.137 =16+10+0.125+0.01226.137

10.2 十进制 → 其他进制(整数部分):反复除法

核心思想:不断用目标进制的基数去除,每次取余数,余数从右向左排列就是结果。

流程图:
开始
  ↓
用基数除当前数,得到商和余数
  ↓
余数插入结果的最左边
  ↓
商变成新的"当前数"
  ↓
商 = 0? ──是──→ 结束,输出结果
          ↓
         否
          ↓
        继续

例: 35 10 35_{10} 3510 → 二进制

35 ÷ 2 = 17 余 1    ← 最右位
17 ÷ 2 =  8 余 1
 8 ÷ 2 =  4 余 0
 4 ÷ 2 =  2 余 0
 2 ÷ 2 =  1 余 0
 1 ÷ 2 =  0 余 1    ← 最左位
从下往上读余数:1 0 0 0 1 1
结果:35 = (100011)₂

例: 126 10 126_{10} 12610 → 八进制

126 ÷ 8 = 15 余 6
 15 ÷ 8 =  1 余 7
  1 ÷ 8 =  0 余 1
从下往上读:1 7 6
结果:126 = (176)₈

例: 126 10 126_{10} 12610 → 十六进制

126 ÷ 16 = 7 余 14(即 E)
  7 ÷ 16 = 0 余 7
从下往上读:7 E
结果:126 = (7E)₁₆

10.3 十进制 → 其他进制(小数部分):反复乘法

核心思想:不断用目标进制的基数去乘小数部分,每次取整数部分,整数部分从左向右排列就是结果。

流程图:
开始
  ↓
用基数乘当前小数,得到一个结果
  ↓
取结果的整数部分,放到目标数的最右边
  ↓
结果的小数部分变成新的"当前小数"
  ↓
小数部分 = 0?或位数够了? ──是──→ 结束
          ↓
         否
          ↓
        继续

例: 0.625 10 0.625_{10} 0.62510 → 二进制

0.625 × 2 = 1.25   → 整数部分 1,剩余 0.25   ← 最左位
0.25  × 2 = 0.50   → 整数部分 0,剩余 0.50
0.50  × 2 = 1.00   → 整数部分 1,剩余 0.00   ← 最右位(结束)
从上往下读:1 0 1
结果:0.625 = (0.101)₂

验证 1 × 2 − 1 + 0 × 2 − 2 + 1 × 2 − 3 = 0.5 + 0 + 0.125 = 0.625 1\times2^{-1} + 0\times2^{-2} + 1\times2^{-3} = 0.5 + 0 + 0.125 = 0.625 1×21+0×22+1×23=0.5+0+0.125=0.625
例: 178.6 10 178.6_{10} 178.610 → 十六进制(整数和小数分开算)
整数部分 178 178 178

178 ÷ 16 = 11 余 2    (11 = B)
 11 ÷ 16 =  0 余 11
结果:B2

小数部分 0.6 0.6 0.6

0.6 × 16 = 9.6  → 整数部分 9,剩余 0.6
(循环,取一位即可)
结果:.9

最终: 178.6 10 ≈ ( B 2.9 ) 16 178.6_{10} \approx (B2.9)_{16} 178.610(B2.9)16

10.4 二进制 ↔ 十六进制(最快方法)

规律:4个二进制位 = 1个十六进制位(因为 2 4 = 16 2^4 = 16 24=16
二进制 → 十六进制:从右向左每4位一组,查表换成十六进制

例:(10011100010)₂
先从右到左每4位分组:
100  1110  0010
(不够4位的左边补0)
 100 → 0100 → 4
1110 → E (14)
0010 → 2
结果:(4E2)₁₆

十六进制 → 二进制:每个十六进制位换成4位二进制

例:(24C)₁₆
2 → 0010
4 → 0100
C → 1100
结果:(001001001100)₂

10.5 二进制 ↔ 八进制(快速方法)

规律:3个二进制位 = 1个八进制位(因为 2 3 = 8 2^3 = 8 23=8
二进制 → 八进制:从右向左每3位一组,查表换成八进制

例:(101110010)₂
从右到左每3位分组:
101  110  010
101 → 5
110 → 6
010 → 2
结果:(562)₈

八进制 → 二进制:每个八进制位换成3位二进制

例:(24)₈
2 → 010
4 → 100
结果:(010100)₂

10.6 八进制 ↔ 十六进制(借道二进制)

没有直接的快捷规律,需要以二进制为中转:

八进制 → 二进制 → 十六进制
十六进制 → 二进制 → 八进制

例图:

八进制:   4    1    1    6
          ↓    ↓    ↓    ↓
二进制: 100  001  001  110
        重新4位分组:
        1000  0100  1110
          ↓     ↓     ↓
十六进制:  8     4     E

10.7 需要多少位数?

公式一:已知十进制数 N N N,在基数 b b b 的系统中需要多少位?
k = ⌈ log ⁡ b N ⌉ k = \lceil \log_b N \rceil k=logbN
其中 ⌈ x ⌉ \lceil x \rceil x 表示向上取整(天花板函数)。
例:十进制 234 234 234 在各进制中需要多少位?

  • 十进制: k = ⌈ log ⁡ 10 234 ⌉ = ⌈ 2.37 ⌉ = 3 k = \lceil \log_{10} 234 \rceil = \lceil 2.37 \rceil = 3 k=log10234=2.37=3
  • 二进制: k = ⌈ log ⁡ 2 234 ⌉ = ⌈ 7.8 ⌉ = 8 k = \lceil \log_2 234 \rceil = \lceil 7.8 \rceil = 8 k=log2234=7.8=8 位 → ( 11101010 ) 2 (11101010)_2 (11101010)2
  • 八进制: k = ⌈ log ⁡ 8 234 ⌉ = ⌈ 2.62 ⌉ = 3 k = \lceil \log_8 234 \rceil = \lceil 2.62 \rceil = 3 k=log8234=2.62=3 位 → ( 352 ) 8 (352)_8 (352)8
  • 十六进制: k = ⌈ log ⁡ 16 234 ⌉ = ⌈ 1.96 ⌉ = 2 k = \lceil \log_{16} 234 \rceil = \lceil 1.96 \rceil = 2 k=log16234=1.96=2 位 → ( E A ) 16 (EA)_{16} (EA)16
    公式二:已知源系统用 k k k 位(基数 b 1 b_1 b1),目标系统(基数 b 2 b_2 b2)最少需要几位?
    x = ⌈ k × log ⁡ b 1 log ⁡ b 2 ⌉ x = \left\lceil k \times \frac{\log b_1}{\log b_2} \right\rceil x=k×logb2logb1
    例:6位十进制数,转成二进制最少需要几位?
    x = ⌈ 6 × log ⁡ 10 log ⁡ 2 ⌉ = ⌈ 6 × 1 0.301 ⌉ = ⌈ 19.93 ⌉ = 20  位 x = \left\lceil 6 \times \frac{\log 10}{\log 2} \right\rceil = \left\lceil 6 \times \frac{1}{0.301} \right\rceil = \lceil 19.93 \rceil = 20 \text{ 位} x=6×log2log10=6×0.3011=19.93=20 

11. 另一种思路:拆分法(小整数快速转二进制)

把十进制数拆成二的幂次之和:

位置值 2 7 2^7 27 2 6 2^6 26 2 5 2^5 25 2 4 2^4 24 2 3 2^3 23 2 2 2^2 22 2 1 2^1 21 2 0 2^0 20
十进制 128 64 32 16 8 4 2 1

例: 165 10 165_{10} 16510 → 二进制
165 = 128 + 32 + 4 + 1 = 2 7 + 2 5 + 2 2 + 2 0 165 = 128 + 32 + 4 + 1 = 2^7 + 2^5 + 2^2 + 2^0 165=128+32+4+1=27+25+22+20

位:  1  0  1  0  0  1  0  1
对应:128 64 32 16  8  4  2  1
结果:(10100101)₂

小数拆分法(分母为2的幂时):

位置值 2 − 1 2^{-1} 21 2 − 2 2^{-2} 22 2 − 3 2^{-3} 23 2 − 4 2^{-4} 24 2 − 5 2^{-5} 25 2 − 6 2^{-6} 26
十进制 1 / 2 1/2 1/2 1 / 4 1/4 1/4 1 / 8 1/8 1/8 1 / 16 1/16 1/16 1 / 32 1/32 1/32 1 / 64 1/64 1/64

例: 27 / 64 27/64 27/64 → 二进制
27 64 = 16 64 + 8 64 + 2 64 + 1 64 = 1 4 + 1 8 + 1 32 + 1 64 \frac{27}{64} = \frac{16}{64} + \frac{8}{64} + \frac{2}{64} + \frac{1}{64} = \frac{1}{4} + \frac{1}{8} + \frac{1}{32} + \frac{1}{64} 6427=6416+648+642+641=41+81+321+641
缺少 1 2 \frac{1}{2} 21 1 16 \frac{1}{16} 161,补 0:

位:  0   1   1   0   1   1
对应:1/2 1/4 1/8 1/16 1/32 1/64
结果:(0.011011)₂

12. 非位置型数字系统:罗马数字

罗马数字中,符号的位置不决定它的绝对值,而是通过相对位置进行加减。

符号 I V X L C D M
1 5 10 50 100 500 1000

规则

  1. 小值在大值后面相加(如 V I I I = 5 + 1 + 1 + 1 = 8 VIII = 5+1+1+1 = 8 VIII=5+1+1+1=8
  2. 小值在大值前面相减(如 I V = 5 − 1 = 4 IV = 5-1 = 4 IV=51=4
  3. I I I V V V 不能放在 C C C 前面(不能相差100倍以上)
  4. 在符号上方加横线 → 乘以1000(如 V ˉ = 5000 \bar{V} = 5000 Vˉ=5000
  5. 罗马数字没有零
    转换示例
III   = 1+1+1 = 3
IV    = 5-1   = 4
XIX   = 10+(10-1) = 19
LXXII = 50+10+10+1+1 = 72
MMVII = 1000+1000+5+1+1 = 2007
MDC   = 1000+500+100 = 1600

13. 完整的 C++ 代码:进制转换工具

#include <iostream>   // 标准输入输出
#include <string>     // 字符串操作
#include <cmath>      // log, ceil 数学函数
#include <algorithm>  // reverse 字符串翻转
// =========================================
// 函数:将十进制整数转换为任意进制的字符串
// 参数:n     = 要转换的十进制正整数
//       base  = 目标进制(2, 8, 或 16)
// 返回:目标进制的字符串表示
// =========================================
std::string decimalToBase(int n, int base) {
    if (n == 0) return "0";
    // 十六进制需要字母 A-F
    const std::string digits = "0123456789ABCDEF";
    std::string result = "";
    while (n > 0) {
        int remainder = n % base;       // 取余数
        result += digits[remainder];    // 把余数对应的符号加到结果末尾
        n = n / base;                   // 商变成新的被除数
    }
    // 因为余数是从低位到高位产生的,所以要翻转字符串
    std::reverse(result.begin(), result.end());
    return result;
}
// =========================================
// 函数:将任意进制的字符串转换为十进制整数
// 参数:s     = 源进制的字符串(如 "1A3")
//       base  = 源进制(2, 8, 或 16)
// 返回:十进制整数
// =========================================
int baseToDecimal(const std::string& s, int base) {
    int result = 0;
    int power = 1;  // 从 base^0 开始
    // 从字符串右边(最低位)往左处理
    for (int i = s.size() - 1; i >= 0; i--) {
        char c = s[i];
        // 把字符转成对应的数值
        int digitValue;
        if (c >= '0' && c <= '9') {
            digitValue = c - '0';       // '3' → 3
        } else if (c >= 'A' && c <= 'F') {
            digitValue = c - 'A' + 10; // 'A' → 10, 'B' → 11 ...
        } else if (c >= 'a' && c <= 'f') {
            digitValue = c - 'a' + 10; // 小写也支持
        } else {
            digitValue = 0;
        }
        result += digitValue * power;   // 当前位的值 = 数字 × 位置权重
        power *= base;                  // 下一位的权重是当前的 base 倍
    }
    return result;
}
// =========================================
// 函数:计算在基数 b 的系统中表示十进制数 N 需要几位
// 公式:k = ceil(log_b(N))
// =========================================
int digitsNeeded(int N, int base) {
    if (N == 0) return 1;
    // log_b(N) = log10(N) / log10(base)
    return (int)std::ceil(std::log10(N) / std::log10(base));
}
int main() {
    // ---- 演示 1:十进制 → 二进制 ----
    int decimal = 35;
    std::string binary = decimalToBase(decimal, 2);
    std::cout << decimal << " (十进制) = (" << binary << ")₂ (二进制)" << std::endl;
    // 输出:35 (十进制) = (100011)₂ (二进制)
    // ---- 演示 2:十进制 → 八进制 ----
    decimal = 126;
    std::string octal = decimalToBase(decimal, 8);
    std::cout << decimal << " (十进制) = (" << octal << ")₈ (八进制)" << std::endl;
    // 输出:126 (十进制) = (176)₈ (八进制)
    // ---- 演示 3:十进制 → 十六进制 ----
    decimal = 126;
    std::string hex = decimalToBase(decimal, 16);
    std::cout << decimal << " (十进制) = (" << hex << ")₁₆ (十六进制)" << std::endl;
    // 输出:126 (十进制) = (7E)₁₆ (十六进制)
    // ---- 演示 4:二进制 → 十进制 ----
    std::string binStr = "11001";
    int dec = baseToDecimal(binStr, 2);
    std::cout << "(" << binStr << ")₂ = " << dec << " (十进制)" << std::endl;
    // 输出:(11001)₂ = 25 (十进制)
    // ---- 演示 5:十六进制 → 十进制 ----
    std::string hexStr = "2AE";
    dec = baseToDecimal(hexStr, 16);
    std::cout << "(" << hexStr << ")₁₆ = " << dec << " (十进制)" << std::endl;
    // 输出:(2AE)₁₆ = 686 (十进制)
    // ---- 演示 6:需要多少位 ----
    int N = 234;
    std::cout << "\n十进制 " << N << " 在各进制中需要的位数:" << std::endl;
    std::cout << "  十进制:" << digitsNeeded(N, 10) << " 位" << std::endl;
    std::cout << "  二进制:" << digitsNeeded(N, 2)  << " 位" << std::endl;
    std::cout << "  八进制:" << digitsNeeded(N, 8)  << " 位" << std::endl;
    std::cout << "  十六进制:" << digitsNeeded(N, 16) << " 位" << std::endl;
    return 0;
}

预期输出:

35 (十进制) = (100011)₂ (二进制)
126 (十进制) = (176)₈ (八进制)
126 (十进制) = (7E)₁₆ (十六进制)
(11001)₂ = 25 (十进制)
(2AE)₁₆ = 686 (十进制)
十进制 234 在各进制中需要的位数:
  十进制:3 位
  二进制:8 位
  八进制:3 位
  十六进制:2 位

14. 转换方法总结

反复除法整数部分
反复乘法小数部分

反复除法

反复除法

每位乘以2的幂次求和

每位乘以8的幂次求和

每位乘以16的幂次求和

每3位一组

每位拆成3位

每4位一组

每位拆成4位

先转二进制再4位分组

先转二进制再3位分组

十进制

二进制

八进制

十六进制

15. 本章关键公式汇总


公式 含义
N m a x = b k − 1 N_{max} = b^k - 1 Nmax=bk1 k k k 位、基数为 b b b 的系统能表示的最大整数
k = ⌈ log ⁡ b N ⌉ k = \lceil \log_b N \rceil k=logbN 表示十进制数 N N N 在基数 b b b 中需要的位数
x = ⌈ k × log ⁡ b 1 log ⁡ b 2 ⌉ x = \lceil k \times \frac{\log b_1}{\log b_2} \rceil x=k×logb2logb1 从基数 b 1 b_1 b1 k k k 位数转换到基数 b 2 b_2 b2 至少需要多少位
4 bits = 1 hex digit 二进制与十六进制的关系
3 bits = 1 octal digit 二进制与八进制的关系

16. 快速记忆技巧

记住"248"三个数:
二进制(Binary)   → base 2  → 2个符号:0,1
八进制(Octal)    → base 8  → 8个符号:0-7
十六进制(Hex)    → base 16 → 16个符号:0-9,A-F
快捷转换:
  二进制 ←→ 十六进制:4位一组
  二进制 ←→ 八进制:  3位一组
  其余:    先转十进制或先转二进制作中转

第三章:数据存储 — 详细中文解析

1. 本章总览

计算机内部只有 0 和 1,但我们要存储数字、文字、音乐、图片、视频……这章回答一个问题:

这些五花八门的数据,到底是怎么变成 0 和 1 存进去的?

现实世界的数据

数字 Numbers

文本 Text

音频 Audio

图像 Images

视频 Video

统一转换为位模式
Bit Pattern: 0和1的序列

存入计算机内存

2. 基本概念:位和位模式

2.1 位(Bit)

是计算机能存储的最小数据单位,只有两个值: 0 0 0 1 1 1
对应物理现实:

  • 开关:开 = 1,关 = 0
  • 电信号:有 = 1,无 = 0

2.2 位模式(Bit Pattern)

把多个位排在一起,就是位模式(也叫位串)。

一个16位的位模式示例:
1 0 0 0 0 0 1 1 1 1 1 1 1 1 0 1
↑                             ↑
最左位(最高位)           最右位(最低位)

重要约定

  • 字节(Byte):8个位组成一个字节,是最常用的存储单位
  • 位数越多,能表示的不同状态越多: k k k 位能表示 2 k 2^k 2k 种不同的模式

2.3 同一位模式可以代表不同含义

关键理解:内存只存 0 和 1,不知道它代表什么。是数字还是字母,由程序决定!

位模式 01000001 可以是:
  - 数字:  65(数学程序解读)
  - 字母:  'A'(文字处理程序解读)
  - 图像:  某个像素的颜色分量
  - 音频:  某个声音采样值
  - 视频:  某帧画面的一部分

3. 存储整数

整数分两类:

  • 无符号整数(Unsigned):只有非负数(0 和正整数)
  • 有符号整数(Signed):有正有负
    小数点的处理:整数使用定点表示,默认小数点在最右边,不需要实际存储。

3.1 无符号整数(Unsigned Integer)

规则:直接把十进制转成二进制,左边不足补 0。
n n n 位无符号整数的范围:
0 ≤ N ≤ 2 n − 1 0 \leq N \leq 2^n - 1 0N2n1
存储步骤

  1. 十进制 → 二进制
  2. 左边补 0,凑够 n n n
    例:把 7 存入 8 位内存
7 → (111)₂ → 补5个0 → 00000111

例:把 258 存入 16 位内存

258 → (100000010)₂ → 补7个0 → 0000000100000010

取出: 直接把二进制转回十进制即可。
溢出(Overflow)
若要存的数超过 2 n − 1 2^n - 1 2n1,就会溢出。计算机会丢弃多余的高位,保留低 n n n 位,得到错误的结果。

4位存储,最大 15:
想存 20 = (10100)₂,有5位
丢掉最左位 → 保留 (0100)₂ = 4
结果:存了20,读出来是4!(错误)

溢出: 11 + 9 = 20 ,但4位存储显示  4 \text{溢出:} 11 + 9 = 20,\text{但4位存储显示}\ 4 溢出:11+9=204位存储显示 4

3.2 符号—数值表示(Sign-and-Magnitude)

这种格式现在主要用于浮点数的尾数部分,不用于存整数,但需要理解。
规则:最左位是符号位,剩余位存数值大小。

  • 最左位 = 0 0 0 → 正数
  • 最左位 = 1 1 1 → 负数
    n n n 位符号—数值格式的范围:
    − ( 2 n − 1 − 1 ) ≤ N ≤ + ( 2 n − 1 − 1 ) -(2^{n-1}-1) \leq N \leq +(2^{n-1}-1) (2n11)N+(2n11)
    8位范围: − 127 -127 127 + 127 +127 +127
    缺点:有两个零!
+0 = 00000000
-0 = 10000000   ← 两个零,浪费一个位模式

例:存 +28(8位)

28 → 7位二进制 → 0011100
加符号位 0 → 00011100

例:存 -28(8位)

28 → 7位二进制 → 0011100
加符号位 1 → 10011100

3.3 二进制补码(Two’s Complement)— 现代计算机的标准

几乎所有现代计算机都用这种格式存储有符号整数。
n n n 位二进制补码的范围:
− 2 n − 1 ≤ N ≤ 2 n − 1 − 1 -2^{n-1} \leq N \leq 2^{n-1}-1 2n1N2n11
8位范围: − 128 -128 128 + 127 +127 +127(负数比正数多一个!)
最左位规则

  • 0 0 0 → 非负数(零或正数)
  • 1 1 1 → 负数
3.3.1 两个关键操作

操作一:取反码(One’s Complement)
把每一位翻转(0变1,1变0)。

原始:   0 0 1 1 0 1 1 0
取反码: 1 1 0 0 1 0 0 1

操作二:取补码(Two’s Complement)
从右往左,遇到第一个 1 之前的位(包括这个1)原样保留,之后的位全部翻转。

原始:   0 0 1 1 0 1 0 0
         ↑从右往左找第一个1
         最右边的1在第3位(从右数)
         第3位及右边原样保留:100
         左边的位全部翻转:  11001
结果:   1 1 0 0 1 1 0 0

也可以先取反码,再加 1,结果相同。
重要性质:对任何数做两次补码操作,得到原数。
补码 ( 补码 ( X ) ) = X \text{补码}(\text{补码}(X)) = X 补码(补码(X))=X

3.3.2 存储步骤
  • 正数或零:直接存二进制(和无符号一样)
  • 负数:先转换绝对值为二进制,再取补码,然后存储
    例:存 +28(8位补码)
28 → (00011100)₂ → 直接存 00011100

例:存 -28(8位补码)

28的绝对值 → (00011100)₂
取补码:
  从右找第一个1(第3位)→ 保留低3位 100
  其余位翻转:00011 → 11100
结果:     11100100
存储:     11100100
3.3.3 取出步骤
  • 最左位是 0 0 0:直接转十进制,是正数
  • 最左位是 1 1 1:先取补码,再转十进制,加负号
    例:取出 00001101
最左位 = 0,正数
00001101 → 13
结果:+13

例:取出 11100110

最左位 = 1,负数
先取补码:11100110 → 00011010
转十进制:26
加负号:-26
3.3.4 补码只有一个零

0 = 00000000 (不像符号—数值有两个零) 0 = 00000000 \quad \text{(不像符号—数值有两个零)} 0=00000000(不像符号数值有两个零)

3.4 三种整数表示对比

4位存储的三种格式对照:

位模式 无符号 符号—数值 二进制补码
0000 0 +0 0
0001 1 +1 +1
0010 2 +2 +2
0011 3 +3 +3
0100 4 +4 +4
0101 5 +5 +5
0110 6 +6 +6
0111 7 +7 +7
1000 8 -0 -8
1001 9 -1 -7
1010 10 -2 -6
1011 11 -3 -5
1100 12 -4 -4
1101 13 -5 -3
1110 14 -6 -2
1111 15 -7 -1

4. 存储实数(浮点数)

实数有整数部分和小数部分,如 23.7 23.7 23.7 − 0.00032 -0.00032 0.00032

4.1 为什么不用定点表示?

问题一:精度丢失
例如:定点格式只允许小数点后2位,存 1.00234 1.00234 1.00234 → 只能存 1.00 1.00 1.00,精度损失。
问题二:范围不够
例如:定点格式整数部分只有10位,存 236154302345.00 236154302345.00 236154302345.00 → 数太大,整数部分被截断。
结论:实数用浮点表示(Floating-Point),让小数点"浮动"。

4.2 科学记数法(十进制中的浮点思想)

7425000000000000000000.00 = + 7.425 × 10 21 7425000000000000000000.00 = +7.425 \times 10^{21} 7425000000000000000000.00=+7.425×1021
− 0.0000000000000232 = − 2.32 × 10 − 14 -0.0000000000000232 = -2.32 \times 10^{-14} 0.0000000000000232=2.32×1014
三个组成部分:
± ⏟ 符号 d . x x x x ⏟ 尾数(定点数) × 10 n ⏟ 移位量(指数) \underbrace{\pm}_{\text{符号}} \underbrace{d.xxxx}_{\text{尾数(定点数)}} \times \underbrace{10^{n}}_{\text{移位量(指数)}} 符号 ±尾数(定点数) d.xxxx×移位量(指数) 10n

4.3 二进制浮点表示

二进制中,同样思路:
± 1. y y y y y y × 2 指数 \pm 1.yyyyyy \times 2^{\text{指数}} ±1.yyyyyy×2指数
规范化(Normalization):统一要求小数点左边只有一个非零数字。

  • 十进制:小数点左边是 1 1 1~ 9 9 9 中的某一个
  • 二进制:小数点左边只能是 1(二进制里非零只有1)
    例: ( 101001 000 … 0 ⏟ 26 个 0 .00 ) 2 = 1.01001 × 2 32 (101001\underbrace{000\ldots0}_{26个0}.00)_2 = 1.01001 \times 2^{32} (101001260 0000.00)2=1.01001×232
    规范化后只存三样东西
原数:+11000111.0101
规范化:+1.10001110101 × 2⁶
存储:
  符号位 S = 0(正)
  指数   E = 6(要加偏置后存)
  尾数   M = 10001110101(小数点右边的部分)
注意:小数点和左边的1不存,是隐含的!

4.4 指数的存储:超量表示(Excess System)

指数可以是负数(如 2 − 14 2^{-14} 214),不能直接存无符号数。
解决方法:给所有指数加一个固定偏置值(bias),使其变为非负数,然后当作无符号整数存储。
偏置值公式 m m m 为存储指数的位数):
bias = 2 m − 1 − 1 \text{bias} = 2^{m-1} - 1 bias=2m11
例:4位存储,偏置 = 2 4 − 1 − 1 = 7 2^{4-1}-1 = 7 2411=7(Excess-7)

实际指数范围:-7 到 +8
加偏置7后:   0 到 15
存储为4位无符号整数:0000 到 1111
转换:存储值 = 实际指数 + 7
读取:实际指数 = 存储值 - 7

4.5 IEEE 754 标准(现代计算机的标准)

IEEE(电气电子工程师协会)制定了两种标准:

单精度(32位):
┌──┬──────────┬───────────────────────────┐
│S │    E     │            M              │
│1位│   8位    │           23位            │
└──┴──────────┴───────────────────────────┘
  偏置 = 127(Excess_127)
双精度(64位):
┌──┬───────────┬──────────────────────────────────────────────────────┐
│S │     E     │                         M                            │
│1位│   11位    │                        52位                          │
└──┴───────────┴──────────────────────────────────────────────────────┘
  偏置 = 1023(Excess_1023)

参数 单精度 双精度
总位数 32 64
符号位 1 1
指数位 8 11
尾数位 23 52
偏置值 127 1023

4.6 存储步骤(单精度为例)

  1. 确定符号 S(正 = 0,负 = 1)
  2. 十进制 → 二进制
  3. 规范化:变成 1. x x x × 2 n 1.xxx \times 2^n 1.xxx×2n 的形式
  4. 计算 E = 实际指数 + 127,转成8位二进制
  5. M = 小数点右边的位(不够23位右边补0)
  6. 拼接 S + E + M,得到32位
    例:存 +5.75(单精度)
a. S = 0(正数)
b. 5.75 = (101.11)₂
c. 规范化:(1.0111)₂ × 2²
d. E = 2 + 127 = 129 = (10000001)₂
   M = 0111(后面补19个0,共23位)
     = 01110000000000000000000
e. 拼接:
   S         E              M
   0   10000001   01110000000000000000000
最终32位:01000000101110000000000000000000

例:存 -161.875(单精度)

a. S = 1(负数)
b. 161.875 = (10100001.111)₂
c. 规范化:(1.0100001111)₂ × 2⁷
d. E = 7 + 127 = 134 = (10000110)₂
   M = 0100001111(补13个0)
     = 01000011110000000000000
e. 最终:11000011010000111100000000000000

例:存 -0.0234375(单精度)

a. S = 1
b. 0.0234375 = (0.0000011)₂
c. 规范化:(1.1)₂ × 2⁻⁶
d. E = -6 + 127 = 121 = (01111001)₂
   M = 1(补22个0)
     = 10000000000000000000000
e. 最终:10111100110000000000000000000000

4.7 取出步骤(单精度)

  1. 分离 S(1位)、E(8位)、M(23位)
  2. S=0正数,S=1负数
  3. 实际指数 = E - 127
  4. 还原:在 M 左边加 “1.”,得到规范化的二进制
  5. 按指数移动小数点,得到实际二进制数
  6. 加符号,转十进制

4.8 截断误差(Truncation Error)

尾数只有23位,超出部分被截断,导致存储值与原值有微小误差。
截断误差 = 原始值 − 实际存储值 \text{截断误差} = \text{原始值} - \text{实际存储值} 截断误差=原始值实际存储值
这在需要极高精度的场合(如航天计算)是个严重问题,需要用双精度甚至更高精度。

4.9 溢出与下溢

数轴示意:
← 负溢出 | 可表示负数 | 负下溢 | 0 | 正下溢 | 可表示正数 | 正溢出 →
                      ↑                    ↑
              绝对值太小,                绝对值太小,
              接近0但存不了              接近0但存不了
  • 溢出(Overflow):数的绝对值太大,超出最大范围
  • 下溢(Underflow):数的绝对值太小,接近0但小于最小可表示值

5. 存储文本

文字 = 一系列符号。用位模式对每个符号编码。
需要的位数由符号总数决定(对数关系):
位数 = ⌈ log ⁡ 2 符号总数 ⌉ \text{位数} = \lceil \log_2 \text{符号总数} \rceil 位数=log2符号总数

符号总数 所需位数
2 1
4 2
8 3
128 7
256 8
65536 16
4294967296 32

5.1 ASCII 码

  • 美国国家标准协会(ANSI)制定
  • 使用 7 位,可表示 2 7 = 128 2^7 = 128 27=128 个符号
  • 包括:大小写英文字母、数字、标点、控制字符

5.2 Unicode

  • 使用 32 位,可表示 2 32 = 4,294,967,296 2^{32} = 4{,}294{,}967{,}296 232=4,294,967,296 个符号
  • 覆盖世界上几乎所有语言的文字,包括汉字、阿拉伯文、表情符号等
  • ASCII 是 Unicode 的子集(前128个编码相同)
    示例:字符串 “CATS” 的存储
C → 位模式1
A → 位模式2
T → 位模式3
S → 位模式4
四个连续的位模式存入内存

6. 存储音频

6.1 音频的本质

音频是模拟信号(连续变化的),不像文字那样是离散的可数符号。

音频信号波形:
幅值
  ↑    ∿∿∿
  |  ∿      ∿    ∿∿
  |∿           ∿    ∿
--+-------------------→ 时间
  |
  ↓                  (每秒内有无限多个值,无法全部存储)

6.2 三步转换过程

模拟音频信号
连续,无限个值

采样 Sampling
每秒取有限个点

量化 Quantization
把实数值四舍五入为整数

编码 Encoding
整数转成位模式存储

数字音频文件

第一步:采样(Sampling)
每隔固定时间取一次音频值。采样率 = 每秒采多少次。

  • 信号变化越快 → 需要采样率越高
  • 实践标准:每秒 40,000 次采样已足够还原人耳可听的音频
    第二步:量化(Quantization)
    每次采样得到实数值,把它四舍五入成最近的整数。
实数值 17.2 → 量化为 17
实数值 17.7 → 量化为 18

第三步:编码(Encoding)
每个量化后的整数用固定位数的位模式存储。

  • 位深度(Bit Depth):每个采样用多少位存
    • 过去:8位
    • 现在:16位、24位、32位
      比特率(Bit Rate)
      R = S × B R = S \times B R=S×B
      其中 S S S = 采样率(次/秒), B B B = 位深度(位/次)
      例:MP3 标准
      R = 44100 × 16 = 705,600  位/秒 ≈ 706  kbps R = 44100 \times 16 = 705{,}600 \text{ 位/秒} \approx 706 \text{ kbps} R=44100×16=705,600 /706 kbps
      经过有损压缩(去掉人耳听不到的信息)后,实际文件更小。

7. 存储图像

7.1 位图(光栅图形,Raster Graphics)

适合存储照片等连续色调图像。
原理:把图像划分成网格,每个格子叫像素(Pixel),存储每个像素的颜色值。

图像网格示意:
┌───┬───┬───┬───┬───┐
│ 像│ 像│ 像│ 像│ 像│
│ 素│ 素│ 素│ 素│ 素│
├───┼───┼───┼───┼───┤
│ 像│ 像│ 像│ 像│ 像│
│ 素│ 素│ 素│ 素│ 素│
└───┴───┴───┴───┴───┘
每个像素存储一个颜色值

分辨率(Resolution):每英寸存多少像素(dpi),越高越清晰。
颜色深度(Color Depth):每个像素用多少位存储颜色。
True-Color(真彩色)

  • 每个像素用 24位
  • 分成 RGB 三个通道,每通道 8 位(0~255)

颜色 R G B
黑色 0 0 0
红色 255 0 0
绿色 0 255 0
蓝色 0 0 255
黄色 255 255 0
青色 0 255 255
品红 255 0 255
白色 255 255 255

True-Color 可表示 2 24 = 16,776,216 2^{24} = 16{,}776{,}216 224=16,776,216 种颜色(约1677万色)。
索引色(Indexed Color)
很多应用不需要1677万色,从中选256种常用色,每像素只需 8 位存索引号。
存储量对比(300万像素照片):
True-Color : 3,000,000 × 24 = 72,000,000  位 \text{True-Color}: 3{,}000{,}000 \times 24 = 72{,}000{,}000 \text{ 位} True-Color:3,000,000×24=72,000,000 
索引色 : 3,000,000 × 8 = 24,000,000  位 \text{索引色}: 3{,}000{,}000 \times 8 = 24{,}000{,}000 \text{ 位} 索引色:3,000,000×8=24,000,000 
常见格式:JPEG(True-Color + 压缩)、GIF(索引色)

7.2 矢量图形(Vector Graphics)

不存像素,存图形的数学描述

位图存的是:                 矢量图存的是:
每个像素的颜色               "画一个圆,圆心(100,100),半径50,红色"
放大后模糊                   放大后仍然清晰(重新计算)

优点:放大不失真,文件通常更小
缺点:不适合存照片细节(色彩过渡复杂的图像)
用途:字体(TrueType/PostScript)、工程制图(CAD)、Flash动画

8. 存储视频

视频 = 一系列图像(帧)按时间顺序播放。
视频 = 图像 1 + 图像 2 + 图像 3 + ⋯ \text{视频} = \text{图像}_1 + \text{图像}_2 + \text{图像}_3 + \cdots 视频=图像1+图像2+图像3+
每一帧按图像的方式存储,所有帧连起来就是视频。通常需要压缩(MPEG格式)。

9. 完整 C++ 代码:整数的三种格式转换

#include <iostream>   // 标准输入输出
#include <string>     // 字符串
#include <bitset>     // 方便输出二进制
#include <cmath>      // pow 函数
// ================================================
// 函数:十进制 → 无符号8位二进制字符串
// 参数:n = 非负整数(0 到 255)
// 返回:8位二进制字符串,如 "00000111"
// ================================================
std::string toUnsigned8(int n) {
    if (n < 0 || n > 255) {
        return "OVERFLOW";  // 超出8位无符号范围
    }
    // bitset<8> 直接把整数转为8位二进制
    return std::bitset<8>(n).to_string();
}
// ================================================
// 函数:8位二进制字符串 → 无符号十进制整数
// 参数:bits = 8位二进制字符串
// 返回:0 到 255 的整数
// ================================================
int fromUnsigned8(const std::string& bits) {
    int result = 0;
    int power = 1;  // 从 2^0 开始
    // 从右往左(从低位到高位)累加
    for (int i = 7; i >= 0; i--) {
        if (bits[i] == '1') {
            result += power;
        }
        power *= 2;  // 下一位权重翻倍
    }
    return result;
}
// ================================================
// 函数:十进制 → 二进制补码8位字符串
// 参数:n = -128 到 127 的整数
// 返回:8位补码二进制字符串
// ================================================
std::string toTwosComplement8(int n) {
    if (n < -128 || n > 127) {
        return "OVERFLOW";
    }
    // C++ 的 int 本身就是补码存储
    // bitset<8> 会正确处理负数的补码位模式
    unsigned char uc = (unsigned char)n;  // 转为无符号,保留位模式
    return std::bitset<8>(uc).to_string();
}
// ================================================
// 函数:8位补码二进制字符串 → 十进制有符号整数
// 参数:bits = 8位二进制字符串
// 返回:-128 到 127 的整数
// ================================================
int fromTwosComplement8(const std::string& bits) {
    // 最高位是符号位
    if (bits[0] == '0') {
        // 正数:直接按无符号处理
        return fromUnsigned8(bits);
    } else {
        // 负数:把位模式当作无符号整数,减去 256(即 2^8)
        return fromUnsigned8(bits) - 256;
    }
}
// ================================================
// 函数:对8位二进制字符串取补码
// 参数:bits = 8位二进制字符串
// 返回:取补码后的8位二进制字符串
// ================================================
std::string twosComplement(const std::string& bits) {
    std::string result = bits;
    // 方法:从右往左,找到第一个1,其右边(含自身)不动,左边全部翻转
    int i = 7;
    // 找第一个 '1' 并停在那里
    while (i >= 0 && result[i] == '0') {
        i--;
    }
    // i 现在指向第一个 '1',跳过它
    i--;
    // 翻转 i 左边(含 i)的所有位
    while (i >= 0) {
        result[i] = (result[i] == '0') ? '1' : '0';
        i--;
    }
    return result;
}
int main() {
    std::cout << "=== 无符号整数(8位)===" << std::endl;
    std::cout << "存入 7:   " << toUnsigned8(7)   << std::endl;  // 00000111
    std::cout << "存入 258: " << toUnsigned8(258)  << std::endl;  // OVERFLOW
    std::cout << "存入 255: " << toUnsigned8(255)  << std::endl;  // 11111111
    std::cout << "取出 00101011: " << fromUnsigned8("00101011") << std::endl;  // 43
    std::cout << "\n=== 二进制补码(8位)===" << std::endl;
    std::cout << "存入 +28:  " << toTwosComplement8(28)   << std::endl;  // 00011100
    std::cout << "存入 -28:  " << toTwosComplement8(-28)  << std::endl;  // 11100100
    std::cout << "存入 -128: " << toTwosComplement8(-128) << std::endl;  // 10000000
    std::cout << "取出 00001101: " << fromTwosComplement8("00001101") << std::endl;  // 13
    std::cout << "取出 11100110: " << fromTwosComplement8("11100110") << std::endl;  // -26
    std::cout << "\n=== 取补码操作演示 ===" << std::endl;
    std::string original = "00110100";
    std::string comp     = twosComplement(original);
    std::string backAgain = twosComplement(comp);
    std::cout << "原始:       " << original  << std::endl;
    std::cout << "取补码:     " << comp      << std::endl;
    std::cout << "再取补码:   " << backAgain << std::endl;  // 应该等于原始
    std::cout << "\n=== 音频比特率计算 ===" << std::endl;
    int samplingRate = 44100;   // MP3: 每秒44100次采样
    int bitDepth     = 16;      // 每次采样16位
    long long bitRate = (long long)samplingRate * bitDepth;
    std::cout << "采样率: " << samplingRate << " 次/秒" << std::endl;
    std::cout << "位深度: " << bitDepth     << " 位/次" << std::endl;
    std::cout << "比特率: " << bitRate      << " 位/秒 = "
              << bitRate / 1000 << " kbps" << std::endl;
    return 0;
}

预期输出:

=== 无符号整数(8位)===
存入 7:   00000111
存入 258: OVERFLOW
存入 255: 11111111
取出 00101011: 43
=== 二进制补码(8位)===
存入 +28:  00011100
存入 -28:  11100100
存入 -128: 10000000
取出 00001101: 13
取出 11100110: -26
=== 取补码操作演示 ===
原始:       00110100
取补码:     11001100
再取补码:   00110100
=== 音频比特率计算 ===
采样率: 44100 次/秒
位深度: 16 位/次
比特率: 705600 位/秒 = 705 kbps

10. 本章知识结构总览

数据存储

数字

文本

音频

图像

视频

整数
定点表示

实数
浮点表示

无符号
0到2ⁿ-1

符号-数值
最左位=符号

二进制补码
现代标准

规范化

符号S + 指数E + 尾数M

IEEE单精度32位

IEEE双精度64位

ASCII 7位
128个符号

Unicode 32位
42亿个符号

采样
40000次/秒

量化
实数取整

编码
转为位模式

位图
像素存储

矢量图
数学公式描述

True-Color 24位

索引色 8位

帧序列
每帧=一幅图像

11. 关键公式总结


概念 公式 说明
k k k 位能表示的种类数 2 k 2^k 2k 位数越多,可表示种类越多
无符号 n n n 位最大值 2 n − 1 2^n - 1 2n1 全1时最大
补码 n n n 位范围 − 2 n − 1 -2^{n-1} 2n1 + 2 n − 1 − 1 +2^{n-1}-1 +2n11 负数比正数多一个
文字所需位数 ⌈ log ⁡ 2 N ⌉ \lceil\log_2 N\rceil log2N N N N=符号总数
音频比特率 R = S × B R = S \times B R=S×B S S S=采样率, B B B=位深度
IEEE单精度偏置 127 127 127 指数存储时加127
IEEE双精度偏置 1023 1023 1023 指数存储时加1023
零的存储(浮点) S = E = M = 0 S=E=M=0 S=E=M=0 特殊规定

第四章:数据运算 — 详细中文解析

1. 本章总览

计算机对数据的操作分三大类:

数据运算

逻辑运算
Logic Operations
对位进行布尔运算

移位运算
Shift Operations
把位向左或右移动

算术运算
Arithmetic Operations
加减乘除

NOT 取反

AND 与

OR 或

XOR 异或

逻辑移位
用于无符号数

循环移位
位不丢失

算术移位
用于有符号补码数

补码整数加减

符号-数值整数加减

浮点数加减

2. 逻辑运算(Logic Operations)

2.1 基本概念

把位 0 0 0 理解为"假(false)“,位 1 1 1 理解为"真(true)”,就可以用布尔代数对位进行运算。
逻辑运算分两个层次:

  • 位级(Bit Level):对单个位操作
  • 模式级(Pattern Level):对整个位串,等价于对每对对应位做相同的位级操作

2.2 四种逻辑运算

NOT(取反,一元运算)

输入一个位,输出它的反。
N O T   0 = 1 N O T   1 = 0 NOT\ 0 = 1 \qquad NOT\ 1 = 0 NOT 0=1NOT 1=0

输入 x x x 输出 NOT x x x
0 1
1 0

口诀:翻转,0变1,1变0。

AND(与,二元运算)

两个输入都是 1 1 1 时,输出才是 1 1 1;其他情况输出 0 0 0
x   A N D   0 = 0 (只要有一个0,结果就是0) x\ AND\ 0 = 0 \qquad \text{(只要有一个0,结果就是0)} x AND 0=0(只要有一个0,结果就是0

x x x y y y x x x AND y y y
0 0 0
0 1 0
1 0 0
1 1 1

口诀:全1才1,有0则0。

OR(或,二元运算)

两个输入都是 0 0 0 时,输出才是 0 0 0;其他情况输出 1 1 1
x   O R   1 = 1 (只要有一个1,结果就是1) x\ OR\ 1 = 1 \qquad \text{(只要有一个1,结果就是1)} x OR 1=1(只要有一个1,结果就是1

x x x y y y x x x OR y y y
0 0 0
0 1 1
1 0 1
1 1 1

口诀:有1则1,全0才0。

XOR(异或,二元运算)

两个输入不同时输出 1 1 1相同时输出 0 0 0
1   X O R   x = N O T   x (与1异或 = 取反) 1\ XOR\ x = NOT\ x \qquad \text{(与1异或 = 取反)} 1 XOR x=NOT x(与1异或 = 取反)
0   X O R   x = x (与0异或 = 不变) 0\ XOR\ x = x \qquad \text{(与0异或 = 不变)} 0 XOR x=x(与0异或 = 不变)

x x x y y y x x x XOR y y y
0 0 0
0 1 1
1 0 1
1 1 0

口诀:相同为0,不同为1。
XOR 可以用其他操作来等价实现:
x   X O R   y ⇔ [ x   A N D   ( N O T   y ) ]   O R   [ ( N O T   x )   A N D   y ] x\ XOR\ y \Leftrightarrow [x\ AND\ (NOT\ y)]\ OR\ [(NOT\ x)\ AND\ y] x XOR y[x AND (NOT y)] OR [(NOT x) AND y]

2.3 位模式级的逻辑运算示例

NOT 示例

输入:  1 0 0 1 1 0 0 0
NOT
输出:  0 1 1 0 0 1 1 1

AND 示例

输入1: 1 0 0 1 1 0 0 0
AND
输入2: 0 0 1 0 1 0 1 0
           ↓
输出:  0 0 0 0 1 0 0 0   (只有两者都是1的位,结果才是1)

OR 示例

输入1: 1 0 0 1 1 0 0 1
OR
输入2: 0 0 1 0 1 1 1 0
           ↓
输出:  1 0 1 1 1 1 1 1   (两者有任一是1,结果就是1)

XOR 示例

输入1: 1 0 0 1 1 0 0 1
XOR
输入2: 0 0 1 0 1 1 1 0
           ↓
输出:  1 0 1 1 0 1 1 1   (不同为1,相同为0)

2.4 逻辑运算的实际应用(掩码技术)

掩码(Mask):专门构造的位模式,配合逻辑运算来精确修改目标位。

目标操作 使用运算 掩码规则
把特定位清零 AND 目标位置放 0 0 0,其余放 1 1 1
把特定位置一 OR 目标位置放 1 1 1,其余放 0 0 0
把特定位翻转 XOR 目标位置放 1 1 1,其余放 0 0 0
所有位取反 NOT 无需掩码

例1:清零(AND)— 把左边5位清零

输入:  1 0 1 0 0 1 1 0
AND
掩码:  0 0 0 0 0 1 1 1   ← 左5位是0(要清零的位),右3位是1(保持不变)
           ↓
输出:  0 0 0 0 0 1 1 0   ← 左5位变0,右3位不变

例2:置一(OR)— 把左边5位置一

输入:  1 0 1 0 0 1 1 0
OR
掩码:  1 1 1 1 1 0 0 0   ← 左5位是1(要置一),右3位是0(保持不变)
           ↓
输出:  1 1 1 1 1 1 1 0   ← 左5位变1,右3位不变

例3:翻转(XOR)— 把左边5位翻转

输入:  1 0 1 0 0 1 1 0
XOR
掩码:  1 1 1 1 1 0 0 0   ← 左5位是1(要翻转),右3位是0(保持不变)
           ↓
输出:  0 1 0 1 1 1 1 0   ← 左5位各自翻转,右3位不变

3. 移位运算(Shift Operations)

移位就是把所有位向左或向右移动若干位。

3.1 逻辑移位(Logical Shift)

用于无符号数(不代表有符号整数的位模式)。
逻辑右移:所有位向右移一位,最左边补 0 0 0,最右边的位丢失。

原始:  1 0 0 1 1 0 0 0
右移→
结果:  0 1 0 0 1 1 0 0   ← 最左补0,最右位(0)丢失

逻辑左移:所有位向左移一位,最右边补 0 0 0,最左边的位丢失。

原始:  1 0 0 1 1 0 0 0
←左移
结果:  0 0 1 1 0 0 0 0   ← 最右补0,最左位(1)丢失

3.2 循环移位(Circular Shift / Rotate)

位不会丢失,从一端移出的位,从另一端补回来。
循环左移

原始:  1 0 0 1 1 0 0 0
循环左移(最左位1绕到最右边)
结果:  0 0 1 1 0 0 0 1
        ↑从左移走的1      ↑ 补到最右边

循环右移

原始:  1 0 0 1 1 0 0 0
循环右移(最右位0绕到最左边)
结果:  0 1 0 0 1 1 0 0
       ↑从右移走的0补到最左边

3.3 算术移位(Arithmetic Shift)

用于有符号补码整数,移位代表乘以或除以 2 2 2,但必须保持符号位不变。
算术右移(等价于除以 2 2 2,向下取整):

  • 符号位保持,并且复制到它的右边邻位
  • 最右边的位丢失
原始:  1 0 0 1 1 0 0 1   (补码表示 -103)
算术右移
结果:  1 1 0 0 1 1 0 0   (补码表示 -52,即 -103÷2 向下取整)
       ↑ 符号位1保留并复制到右边

算术左移(等价于乘以 2 2 2):

  • 最左位(符号位)丢失,最右边补 0 0 0
  • 如果新的符号位与原来不同 → 溢出,结果无效
正常情况(-39 × 2 = -78):
原始:  1 1 0 1 1 0 0 1   (补码 -39)
算术左移
结果:  1 0 1 1 0 0 1 0   (补码 -78)✓ 新符号位仍是1,正确
溢出情况(127 × 2 期望254,但溢出):
原始:  0 1 1 1 1 1 1 1   (补码 +127)
算术左移
结果:  1 1 1 1 1 1 1 0   (补码 -2)✗ 新符号位从0变成1,溢出!

3.4 移位与乘除的关系

算术左移 1 位 ⇔ × 2 \text{算术左移 1 位} \Leftrightarrow \times 2 算术左移 1 ×2
算术右移 1 位 ⇔ ÷ 2  (向下取整) \text{算术右移 1 位} \Leftrightarrow \div 2\ \text{(向下取整)} 算术右移 1 ÷2 (向下取整)
算术左移 k 位 ⇔ × 2 k \text{算术左移 k 位} \Leftrightarrow \times 2^k 算术左移 k ×2k
算术右移 k 位 ⇔ ÷ 2 k  (向下取整) \text{算术右移 k 位} \Leftrightarrow \div 2^k\ \text{(向下取整)} 算术右移 k ÷2k (向下取整)

3.5 综合应用:提取特定位

:要知道一个8位模式中,从右数第3位(位置2)是0还是1?

原始位模式:  a b c d e f g h
                              ↑第1位(最右)
                          ↑第3位(目标)
步骤1:右移2位(逻辑右移),让目标位移到最右边
       结果:0 0 a b c d e f
步骤2:AND 掩码 00000001,只保留最右位
       掩码:0 0 0 0 0 0 0 1
       结果:0 0 0 0 0 0 0 f   ← f就是目标位的值
判断:结果 = 1 → 目标位是1
      结果 = 0 → 目标位是0

4. 算术运算(Arithmetic Operations)

4.1 二进制加法基本规则

列竖式相加,逐位计算,每列最多3个位(加上来自上一列的进位):

该列有几个1 该列结果(Sum) 进位(Carry)
0个1 0 0
1个1 1 0
2个1 0 1
3个1 1 1

4.2 补码整数的加法和减法

最重要的特性:补码格式下,减法可以转换为加法!
A − B = A + 补码 ( B ) A - B = A + \text{补码}(B) AB=A+补码(B)
为什么最后的进位要丢弃:因为内存只有 n n n 位,超出部分自然丢失,而补码的数学保证了这不影响结果的正确性。
加法步骤:直接逐位相加,最高位产生的进位丢弃。
减法步骤:先对第二个数取补码,然后做加法。
例: + 17 + ( + 22 ) +17 + (+22) +17+(+22)(8位补码)

两数的补码表示:
A = +17 → 00010001
B = +22 → 00010110
列竖式:
进位:       1
  A:  0 0 0 1 0 0 0 1
+ B:  0 0 0 1 0 1 1 0
--------------------------
  R:  0 0 1 0 0 1 1 1  →  +39 ✓

例: + 24 + ( − 17 ) +24 + (-17) +24+(17)(8位补码)

A = +24 → 00011000
B = -17 → 11101111  (-17的补码)
列竖式:
进位:  1 1 1 1 1
  A:  0 0 0 1 1 0 0 0
+ B:  1 1 1 0 1 1 1 1
--------------------------
     (1)0 0 0 0 0 1 1 1  → 最高位进位丢弃
  R:   0 0 0 0 0 1 1 1  →  +7 ✓  (+24 + (-17) = +7)

例: + 24 − ( − 17 ) = + 41 +24 - (-17) = +41 +24(17)=+41(减法转加法)

-17 的补码是 11101111
对 11101111 再取补码 = 00010001 (即 +17)
A:  0 0 0 1 1 0 0 0
+补码(B):  0 0 0 1 0 0 0 1
--------------------------
进位:       1
  R:  0 0 1 0 1 0 0 1  →  +41 ✓

例:溢出情况 + 127 + ( + 3 ) +127 + (+3) +127+(+3)(期望 + 130 +130 +130,但8位补码最大只有 + 127 +127 +127

A = 127 → 01111111
B =   3 → 00000011
进位:1 1 1 1 1 1 1
  A:  0 1 1 1 1 1 1 1
+ B:  0 0 0 0 0 0 1 1
--------------------------
  R:  1 0 0 0 0 0 1 0  →  -126(错误!期望 +130)

溢出判断:正数 + 正数,结果符号位变成了 1(负数),说明溢出。

4.3 符号—数值整数的加法和减法

符号—数值格式比补码复杂得多,需要分情况讨论。

减法

加法

0 同号

1 异号

开始

操作类型

翻转B的符号位
转化为加法

符号位 A XOR B = ?

同号:直接相加两数的数值部分
结果符号 = A的符号

异号:对B的数值取补码后相加

有溢出?

报告溢出

存入结果

有进位溢出?

结果符号 = A的符号
数值部分直接用

结果符号 = B的符号
对数值取补码

关键规则(异号相加时)

  • 若相加时产生溢出(进位出最高位):说明 ∣ A ∣ > ∣ B ∣ |A| > |B| A>B,结果符号同 A,数值即为运算结果
  • 若相加时没有溢出:说明 ∣ A ∣ < ∣ B ∣ |A| < |B| A<B,结果符号同 B,数值要取补码
    例: ( + 17 ) + ( + 22 ) (+17) + (+22) (+17)+(+22),同号相加
A = (0 0010001)  符号0,数值0010001 = 17
B = (0 0010110)  符号0,数值0010110 = 22
符号位 A XOR B = 0 XOR 0 = 0,同号
直接相加数值部分:
  0010001
+ 0010110
-----------
  0100111  = 39,无溢出
结果符号 = A的符号 = 0(正)
最终:(0 0100111) = +39 ✓

例: ( + 17 ) + ( − 22 ) (+17) + (-22) (+17)+(22),异号相加

A = (0 0010001)  符号0,|A| = 17
B = (1 0010110)  符号1,|B| = 22
符号位 0 XOR 1 = 1,异号
对B的数值部分取补码:
  0010110 的补码 = 1101010
然后相加:
  0010001
+ 1101010
-----------
  1111011   没有溢出(最高位没产生进位)
没有溢出 → 结果符号 = B的符号 = 1(负)
对结果数值再取补码:1111011 的补码 = 0000101 = 5
最终:(1 0000101) = -5 ✓  (+17 + (-22) = -5)

4.4 浮点数的加法和减法

浮点数加减需要先对齐小数点(统一指数),再做整数加减,最后重新规范化。
五步流程

减法

加法

开始

任一数为0?

结果 = 另一个数,结束

操作类型

翻转第二个数的符号位
转化为加法

去规范化:
在尾数前加上隐含的1
指数各加1

对齐指数:
较小指数递增
对应尾数右移
直到两指数相等

把 符号+尾数 当作
符号-数值整数做加减

有溢出?

尾数右移1位
指数加1

规范化:
移动小数点使左边只有1

截断/舍入尾数到规定位数

结束

关键理解:对齐指数是什么意思
比如要算 1.11101 × 2 4 + 1.01 × 2 2 1.11101 \times 2^4 + 1.01 \times 2^2 1.11101×24+1.01×22
两个数的指数不同(4 和 2),不能直接加尾数,必须先让它们指数相同:
1.01 × 2 2 = 0.00101 × 2 4 1.01 \times 2^2 = 0.00101 \times 2^4 1.01×22=0.00101×24
现在两个数都是 × 2 4 \times 2^4 ×24,尾数可以直接相加。
完整例子: 5.75 + 161.875 = 167.625 5.75 + 161.875 = 167.625 5.75+161.875=167.625(IEEE 单精度)
第一步:两数的存储格式

A = 5.75:
  S=0, E=10000001(129), M=01110000000000000000000
  实际:+1.0111 × 2²
B = 161.875:
  S=0, E=10000110(134), M=01000011110000000000000
  实际:+1.0100001111 × 2⁷

第二步:去规范化(加上隐含的1,指数各加1)

A:S=0, E=10000010(130), M=101110000000000000000000(24位)
B:S=0, E=10000111(135), M=101000011110000000000000(24位)

第三步:对齐指数(让A的指数从130变成135,尾数右移5位)

A:S=0, E=10000111(135), M=000001011100000000000000
B:S=0, E=10000111(135), M=101000011110000000000000

第四步:同号相加(符号相同,直接加尾数)

  000001011100000000000000
+ 101000011110000000000000
= 101001111010000000000000  没有溢出
R:S=0, E=10000111, M=101001111010000000000000

第五步:规范化(已经是 1. x x x 1.xxx 1.xxx 形式,只需去掉隐含1,指数减1)

R:S=0, E=10000110(134), M=01001111010000000000000(23位)

验证: 1.0100111101 × 2 134 − 127 = 1.0100111101 × 2 7 = 10100111.101 = 167.625 1.0100111101 \times 2^{134-127} = 1.0100111101 \times 2^7 = 10100111.101 = 167.625 1.0100111101×2134127=1.0100111101×27=10100111.101=167.625

5. 完整 C++ 代码

#include <iostream>   // 标准输入输出
#include <string>     // 字符串
#include <bitset>     // 二进制位操作
#include <cassert>    // 断言(验证)
// ================================================
// 第一部分:逻辑运算演示
// ================================================
// 打印8位二进制字符串
void printBits(const std::string& label, unsigned char val) {
    std::cout << label << ": " << std::bitset<8>(val) << std::endl;
}
void demoLogicOps() {
    std::cout << "=== 逻辑运算演示 ===" << std::endl;
    unsigned char A = 0b10011000;  // 10011000
    unsigned char B = 0b00101010;  // 00101010
    printBits("A           ", A);
    printBits("B           ", B);
    printBits("NOT A       ", (unsigned char)(~A));       // 取反
    printBits("A AND B     ", (unsigned char)(A & B));    // 与
    printBits("A OR  B     ", (unsigned char)(A | B));    // 或
    printBits("A XOR B     ", (unsigned char)(A ^ B));    // 异或
    std::cout << "\n--- 掩码应用 ---" << std::endl;
    unsigned char pattern = 0b10100110;  // 测试位模式
    printBits("原始模式     ", pattern);
    // 用 AND 清零左边5位(掩码:00000111)
    unsigned char clearMask = 0b00000111;
    printBits("AND掩码(清零左5位)", clearMask);
    printBits("AND结果      ", (unsigned char)(pattern & clearMask));
    // 用 OR 置一左边5位(掩码:11111000)
    unsigned char setMask = 0b11111000;
    printBits("OR掩码(置1左5位) ", setMask);
    printBits("OR结果       ", (unsigned char)(pattern | setMask));
    // 用 XOR 翻转左边5位(掩码:11111000)
    printBits("XOR掩码(翻转左5位)", setMask);
    printBits("XOR结果      ", (unsigned char)(pattern ^ setMask));
}
// ================================================
// 第二部分:移位运算演示
// ================================================
void demoShiftOps() {
    std::cout << "\n=== 移位运算演示 ===" << std::endl;
    unsigned char val = 0b10011000;
    printBits("原始值        ", val);
    // 逻辑右移(用无符号右移保证左边补0)
    printBits("逻辑右移1位   ", (unsigned char)(val >> 1));
    // 逻辑左移
    printBits("逻辑左移1位   ", (unsigned char)(val << 1));
    // 循环右移(手动实现:最右位移到最左边)
    unsigned char rotateRight = (val >> 1) | (val << 7);
    printBits("循环右移1位   ", rotateRight);
    // 循环左移(手动实现:最左位移到最右边)
    unsigned char rotateLeft = (val << 1) | (val >> 7);
    printBits("循环左移1位   ", rotateLeft);
    // 算术右移(有符号数,C++中对signed char右移保留符号位)
    signed char signedVal = (signed char)0b10011001;  // 补码 -103
    std::cout << "\n算术移位(有符号数 -103):" << std::endl;
    printBits("原始值        ", (unsigned char)signedVal);
    signed char arithRight = signedVal >> 1;           // 算术右移1位 = 除以2
    printBits("算术右移1位   ", (unsigned char)arithRight);
    std::cout << "  十进制: " << (int)signedVal
              << " >> 1 = " << (int)arithRight << std::endl;
    // 提取第3位(从右数,0-indexed)
    std::cout << "\n--- 提取特定位 ---" << std::endl;
    unsigned char target = 0b10110100;
    printBits("目标模式      ", target);
    // 右移2位,再AND 00000001
    unsigned char bit2 = (target >> 2) & 0x01;
    std::cout << "第3位(从右数)的值: " << (int)bit2 << std::endl;
}
// ================================================
// 第三部分:补码整数加减法演示
// ================================================
// 把有符号8位整数转成8位补码字符串(用于显示)
std::string toTwoCompStr(signed char n) {
    return std::bitset<8>((unsigned char)n).to_string();
}
void demoTwoCompArith() {
    std::cout << "\n=== 补码整数加减法演示 ===" << std::endl;
    // 例1:+17 + (+22) = +39
    signed char a1 = 17, b1 = 22;
    signed char r1 = a1 + b1;  // C++ 的 signed char 自动用补码运算
    std::cout << "A = " << toTwoCompStr(a1) << " (" << (int)a1 << ")" << std::endl;
    std::cout << "B = " << toTwoCompStr(b1) << " (" << (int)b1 << ")" << std::endl;
    std::cout << "A+B = " << toTwoCompStr(r1) << " (" << (int)r1 << ")" << std::endl;
    std::cout << std::endl;
    // 例2:+24 + (-17) = +7
    signed char a2 = 24, b2 = -17;
    signed char r2 = a2 + b2;
    std::cout << "A = " << toTwoCompStr(a2) << " (" << (int)a2 << ")" << std::endl;
    std::cout << "B = " << toTwoCompStr(b2) << " (" << (int)b2 << ")" << std::endl;
    std::cout << "A+B = " << toTwoCompStr(r2) << " (" << (int)r2 << ")" << std::endl;
    std::cout << std::endl;
    // 例3:减法转加法 +24 - (-17) = +41
    signed char a3 = 24, b3 = -17;
    // 减法 = 加上被减数的补码(C++ 自动处理)
    signed char r3 = a3 - b3;
    std::cout << "A - B = " << (int)a3 << " - (" << (int)b3 << ") = " << (int)r3 << std::endl;
    // 手动展示:补码(b3) = -b3
    signed char negB3 = -b3;
    std::cout << "等价于 A + 补码(B): " << toTwoCompStr(a3)
              << " + " << toTwoCompStr(negB3) << " = "
              << toTwoCompStr(r3) << std::endl;
    std::cout << std::endl;
    // 例4:溢出 +127 + (+3) = 期望130但溢出
    signed char a4 = 127, b4 = 3;
    signed char r4 = a4 + b4;  // 会溢出
    std::cout << "溢出示例:127 + 3" << std::endl;
    std::cout << "A = " << toTwoCompStr(a4) << std::endl;
    std::cout << "B = " << toTwoCompStr(b4) << std::endl;
    std::cout << "结果 = " << toTwoCompStr(r4)
              << " (" << (int)r4 << ") ← 溢出!期望130" << std::endl;
}
// ================================================
// 主函数
// ================================================
int main() {
    demoLogicOps();
    demoShiftOps();
    demoTwoCompArith();
    return 0;
}

预期输出(节选)

=== 逻辑运算演示 ===
A           : 10011000
B           : 00101010
NOT A       : 01100111
A AND B     : 00001000
A OR  B     : 10111010
A XOR B     : 10110010
--- 掩码应用 ---
原始模式     : 10100110
AND掩码(清零左5位): 00000111
AND结果      : 00000110
OR掩码(置1左5位) : 11111000
OR结果       : 11111110
XOR掩码(翻转左5位): 11111000
XOR结果      : 01011110
=== 移位运算演示 ===
原始值        : 10011000
逻辑右移1位   : 01001100
逻辑左移1位   : 00110000
循环右移1位   : 01001100
循环左移1位   : 00110001
算术移位(有符号数 -103):
原始值        : 10011001
算术右移1位   : 11001100
  十进制: -103 >> 1 = -52
=== 补码整数加减法演示 ===
A = 00010001 (17)
B = 00010110 (22)
A+B = 00100111 (39)
A = 00011000 (24)
B = 11101111 (-17)
A+B = 00000111 (7)
A - B = 24 - (-17) = 41
等价于 A + 补码(B): 00011000 + 00010001 = 00101001
溢出示例:127 + 3
A = 01111111
B = 00000011
结果 = 10000010 (-126) ← 溢出!期望130

6. 本章核心规律总结


运算 特点 关键规则
NOT 一元,翻转全部位 0 → 1 ,   1 → 0 0 \to 1,\ 1 \to 0 01, 10
AND 二元,全1才1 有0则0;配合掩码清零
OR 二元,有1则1 全0才0;配合掩码置一
XOR 二元,不同才1 相同为0;配合掩码翻转
逻辑右移 左补0 用于无符号数
逻辑左移 右补0 用于无符号数
算术右移 符号位复制到右邻 ÷ 2 \div 2 ÷2(下取整)
算术左移 右补0 × 2 \times 2 ×2,可能溢出
补码加法 最高位进位丢弃 最简洁
补码减法 转为加补码 A − B = A + 补码 ( B ) A-B = A+\text{补码}(B) AB=A+补码(B)

最重要的设计哲学

补码之所以成为现代计算机的标准,是因为减法可以直接转换为加法,硬件电路设计大幅简化——ALU 只需要一个加法器,就能完成所有整数的加减运算。

浮点数加减法:是二进制加减吗?

结论先行

不是简单的二进制整数加减法。 浮点数加减法遵循 IEEE 754 标准,需要先对齐指数,再进行尾数加减,最后规格化结果。

IEEE 754 浮点数结构(以 float 为例)

符号位(1) | 指数位(8) | 尾数位(23)
  [31]    | [30..23]  | [22..0]
  • 符号位:0 = 正,1 = 负
  • 指数:存储值 = 真实指数 + 127(偏移量 bias)
  • 尾数:隐含最高位 1,即实际尾数 = 1.{23位}
    例如 0.1f 的二进制表示:
0 01111011 10011001100110011001101
符号=正  指数=123-127=-4  尾数≈1.6

浮点加法的真实步骤

1.5f + 0.125f 为例:

步骤 说明
① 取出指数和尾数 分别解析两个操作数
对齐指数 将较小指数的尾数右移,使两者指数相同
尾数相加 对齐后的尾数做整数加法
规格化 结果若溢出则右移并调整指数
舍入 按 round-to-nearest-even 舍入
⑥ 打包结果 重新组合符号、指数、尾数

⚠️ 步骤②"对齐指数"是关键:这意味着直接对两个 float 的二进制位做整数加法,结果是错误的!

C++ 验证代码

#include <iostream>
#include <cstdint>
#include <cstring>
#include <bitset>
#include <cmath>
// 将 float 的二进制位读取为 uint32_t(无类型转换)
uint32_t float_to_bits(float f) {
    uint32_t bits;
    std::memcpy(&bits, &f, sizeof(f));
    return bits;
}
// 将 uint32_t 位模式还原为 float
float bits_to_float(uint32_t bits) {
    float f;
    std::memcpy(&f, &bits, sizeof(f));
    return f;
}
// 打印 float 的详细二进制结构
void print_float_bits(const char* name, float f) {
    uint32_t bits = float_to_bits(f);
    uint32_t sign     = (bits >> 31) & 0x1;
    uint32_t exponent = (bits >> 23) & 0xFF;
    uint32_t mantissa =  bits        & 0x7FFFFF;
    std::cout << name << " = " << f << "\n";
    std::cout << "  二进制: " << std::bitset<32>(bits) << "\n";
    std::cout << "  符号=" << sign
              << "  指数=" << exponent << "(真实指数=" << (int)exponent - 127 << ")"
              << "  尾数=" << std::bitset<23>(mantissa) << "\n\n";
}
int main() {
    float a = 1.5f;
    float b = 0.125f;
    std::cout << "========== 浮点数结构 ==========\n\n";
    print_float_bits("a (1.5f)   ", a);
    print_float_bits("b (0.125f) ", b);
    // ---- 实验1:正确的浮点加法 ----
    float correct_sum = a + b;
    std::cout << "========== 实验1:正确的浮点加法 ==========\n";
    std::cout << "a + b = " << correct_sum << "\n";
    print_float_bits("a + b", correct_sum);
    // ---- 实验2:直接对二进制位做整数加法(错误做法)----
    uint32_t bits_a = float_to_bits(a);
    uint32_t bits_b = float_to_bits(b);
    uint32_t wrong_bits = bits_a + bits_b;          // 直接加位模式!
    float wrong_result = bits_to_float(wrong_bits);
    std::cout << "========== 实验2:直接对位模式做整数加法(错误!)==========\n";
    std::cout << "bits_a + bits_b(整数相加) = " << wrong_bits
              << "  还原为float = " << wrong_result << "\n";
    print_float_bits("错误结果", wrong_result);
    std::cout << "正确答案:" << correct_sum << "\n";
    std::cout << "错误答案:" << wrong_result << "\n";
    std::cout << "差值:" << std::abs(correct_sum - wrong_result) << "\n\n";
    // ---- 实验3:精度损失(经典的 0.1 + 0.2 问题)----
    std::cout << "========== 实验3:0.1 + 0.2 精度问题 ==========\n";
    float x = 0.1f, y = 0.2f;
    float z = x + y;
    std::cout << "0.1f + 0.2f = " << z << "\n";
    printf("高精度输出:%.20f\n", (double)z);
    std::cout << "等于 0.3f 吗?" << (z == 0.3f ? "是" : "否(精度损失!)") << "\n\n";
    print_float_bits("0.1f", x);
    print_float_bits("0.2f", y);
    print_float_bits("0.1f + 0.2f", z);
    // ---- 实验4:大数吃小数(指数差异过大导致精度完全丢失)----
    std::cout << "========== 实验4:大数吃小数 ==========\n";
    float big   = 1e8f;
    float small = 1.0f;
    float result = big + small;
    std::cout << "1e8f + 1.0f = " << result << "\n";
    std::cout << "结果等于 1e8f 吗?" << (result == big ? "是(小数被完全吞噬!)" : "否") << "\n";
    return 0;
}

预期输出与解读

========== 实验1:正确的浮点加法 ==========
a + b = 1.625
  二进制: 00111111110100000000000000000000
  ...
========== 实验2:直接对位模式做整数加法(错误!)==========
错误结果: 约 2.26...(完全不正确)
========== 实验3:0.1 + 0.2 精度问题 ==========
高精度输出:0.30000001192092895508
等于 0.3f 吗?否(精度损失!)
========== 实验4:大数吃小数 ==========
1e8f + 1.0f = 1e+08
结果等于 1e8f 吗?是(小数被完全吞噬!)

关键结论

问题 答案
浮点加减是整数二进制加减吗? 不是,需要对齐指数
直接对位模式加减会怎样? 结果完全错误
为什么 0.1 + 0.2 ≠ 0.3? 0.1 和 0.2 在二进制下都是无限循环小数,存储时已有截断误差
什么是"大数吃小数"? 两数指数差 > 23 时,小数的尾数被右移出去,贡献为零

编译运行

g++ -o float_demo float_demo.cpp -std=c++17
./float_demo

IEEE 754 浮点数加减法详解

从零开始,彻底搞懂浮点数为什么不是简单的二进制加减

一、浮点数长什么样?

先看清楚一个 32 位 float 的内部结构,后面所有步骤都围绕它展开。

 31  30      23  22                  0
  |   |       |   |                  |
  S   EEEEEEEE   MMMMMMMMMMMMMMMMMMMMMMM
  |   |       |   |                  |
  符号  指数(8位)       尾数(23位)

字段 位数 含义
符号位 S 1位 0=正数,1=负数
指数 E 8位 存储值 = 真实指数 + 127(偏置值 bias=127)
尾数 M 23位 小数点后的部分,最高位的 1 被隐藏(隐含位)

一个浮点数的真实值公式:
v a l u e = ( − 1 ) S × 1. M × 2 E − 127 value = (-1)^S \times 1.M \times 2^{E - 127} value=(1)S×1.M×2E127
关键点:尾数前面隐藏了一个 “1.”,不占用存储空间,但实际计算时必须加上。

二、为什么不能直接对二进制位做加法?

举个简单例子:
1.5 + 0.5 1.5 + 0.5 1.5+0.5

  • 1.5 = 1.1 2 × 2 0 1.5 = 1.1_2 \times 2^0 1.5=1.12×20,指数是 0
  • 0.5 = 1.0 2 × 2 − 1 0.5 = 1.0_2 \times 2^{-1} 0.5=1.02×21,指数是 -1
    它们的指数不同,就像十进制里 150 + 5 150 + 5 150+5 不能把 1、5、0 和 5 直接一位一位相加,必须先对齐小数点:
  150.0
+   5.0
-------
  155.0

浮点数也是一样,必须先对齐指数(对齐"小数点"),才能相加尾数。直接对二进制位做整数加法会得到完全错误的结果。

三、同符号浮点数加法(8步详解)

示例输入

数字1: 01000011011101100100011100000000
数字2: 01000001010100110011100011011101

第 1 步:把指数从二进制转成十进制

从第 [30…23] 位取出 8 位指数字段:
数字1 的指数字段: 10000110
128 + 4 + 2 = 134 128 + 4 + 2 = 134 128+4+2=134
数字2 的指数字段: 10000010
128 + 2 = 130 128 + 2 = 130 128+2=130

注意:这里不减偏置值 127,只是先算出存储的指数,方便后面比较大小。

第 2 步:在尾数前面补上隐含的 “1.”

取出第 [22…0] 位的尾数字段,在前面加上 1.

数字1: 1.11101100100011100000000
数字2: 1.10100110011100011011101

此时两个数分别表示:
数字 1 = 1.11101100100011100000000 2 × 2 134 − 127 数字1 = 1.11101100100011100000000_2 \times 2^{134 - 127} 数字1=1.111011001000111000000002×2134127
数字 2 = 1.10100110011100011011101 2 × 2 130 − 127 数字2 = 1.10100110011100011011101_2 \times 2^{130 - 127} 数字2=1.101001100111000110111012×2130127

第 3 步:对齐指数(移动小数点)

两个数的指数不同(134 vs 130),差值为:
134 − 130 = 4 134 - 130 = 4 134130=4
指数小的那个(数字2)需要把小数点向左移 4 位,同时指数加 4,变成和数字1 一样的指数:
移位前:

1.10100110011100011011101  × 2^(130)

向左移 4 位后:

0.0001101001100111000110111 01  × 2^(134)

小数点左移 = 数值变小 = 指数变大,两者抵消,数值不变。
移出去的位(末尾的 01)要保留,后面舍入时会用到。

第 4 步:尾数相加

对齐后,两个数的尾数直接相加,就像普通的二进制加法:

  1.11101100100011100000000
+ 0.00011010011001110001101110 1
---------------------------------

二进制加法规则:

加数A 加数B 进位in 结果 进位out
0 0 0 0 0
0 1 0 1 0
1 1 0 0 1
1 1 1 1 1

加法过程(从右向左,带进位):

进位:  11  1111  111
  1.11101100100011100000000
+ 0.00011010011001110001101 110 1
---------------------------------
 10.00000110111101010001101 110 1

结果:
10.0000001101111010100011011101 2 × 2 134 10.000000110111101010001101110 1_2 \times 2^{134} 10.00000011011110101000110111012×2134

第 5 步:规格化

加法结果的整数部分变成了 10,说明小数点位置不对。需要把小数点向左移 1 位,同时指数加 1:
移位前:
10.0000011011110101000110111 2 × 2 134 10.0000011011110101000110111_2 \times 2^{134} 10.00000110111101010001101112×2134
移位后:
1.00000011011110101000110111 2 × 2 135 1.00000011011110101000110111_2 \times 2^{135} 1.000000110111101010001101112×2135

第 6 步:舍入

尾数只有 23 位,但现在有 28 位小数,需要截断并舍入。

1.00000011011110101000 110 1110 1
                      ^^^
                      第23位(最低有效位,用红色标记位置)
                         ^^^^^^^
                         超出的部分:1101110 1

舍入规则(IEEE 754 默认:最近偶数舍入):

情况1: 超出部分首位是 0  → 直接截断(向下舍入)
情况2: 超出部分首位是 1,后面还有 1  → 向上舍入(+1)
情况3: 超出部分是 10000... (正好一半)→ 让最低有效位变成 0(偶数舍入)

本例超出部分是 1101110 1,首位是 1 且后面有更多 1 → 向上舍入

尾数(23位): 00000011011110101000110
                                     + 1
                                  -------
结果:         00000011011110101000111

规格化后的完整数:
1.00000011011110101000111 2 × 2 135 1.00000011011110101000111_2 \times 2^{135} 1.000000110111101010001112×2135

第 7 步:把指数转回二进制

指数 135 转二进制:
135 = 128 + 4 + 2 + 1 = 10000111 2 135 = 128 + 4 + 2 + 1 = 10000111_2 135=128+4+2+1=100001112

第 8 步:拼装结果


部分
符号位 0(正数)
指数(8位) 10000111
尾数(23位) 00000011011110101000111

最终结果:

0 10000111 00000011011110101000111

即:01000011100000011011110101000111

四、同符号浮点数减法(8步详解)

减法和加法的流程几乎一样,只有第 4 步(尾数运算)变成了减法,以及需要处理"谁大谁小"的问题。

示例输入

被减数(minuend):   00111100011010110111000000100000
减数(subtrahend):  00111101100010110001101110000110

第 1 步:指数转十进制

被减数指数字段: 01111000
64 + 32 + 16 + 8 = 120 64 + 32 + 16 + 8 = 120 64+32+16+8=120
减数指数字段: 01111011
64 + 32 + 16 + 8 + 2 + 1 = 123 64 + 32 + 16 + 8 + 2 + 1 = 123 64+32+16+8+2+1=123

第 2 步:补上隐含的 “1.”

被减数: 1.11010110111000000100000  × 2^(120)
减数:   1.00010110001101110000110  × 2^(123)

第 3 步:对齐指数

被减数指数更小(120 vs 123),差值为 3,被减数小数点向左移 3 位:

移位前: 1.11010110111000000100000  × 2^(120)
移位后: 0.00111010110111000000100  × 2^(123)

第 4 步:尾数相减

对齐后进行减法。先比较大小:

减数:    1.00010110001101110000110
被减数:  0.00111010110111000000100

减数(1.000...)大于被减数(0.001...),所以用减数减被减数,结果加负号。
二进制减法(借位法):

  1.00010110001101110000110
- 0.00111010110111000000100
--------------------------
  0.11011011010110110000010

由于我们把顺序交换了(大的在上),结果需要加负号:
− 0.11011011010110110000010 2 × 2 123 -0.11011011010110110000010_2 \times 2^{123} 0.110110110101101100000102×2123

第 5 步:规格化

结果是 0.11011...,小数点后第一位才是 1,需要把小数点向右移 1 位,指数减 1:
− 1.1011011010110110000010 2 × 2 122 -1.1011011010110110000010_2 \times 2^{122} 1.10110110101101100000102×2122

第 6 步:舍入

小数部分只有 21 位,不足 23 位,不需要舍入,末尾补 0:
− 1.10110110101101100000100 2 × 2 122 -1.10110110101101100000100_2 \times 2^{122} 1.101101101011011000001002×2122

第 7 步:指数转二进制

122 = 64 + 32 + 16 + 8 + 2 = 1111010 2 122 = 64 + 32 + 16 + 8 + 2 = 1111010_2 122=64+32+16+8+2=11110102
(补齐 8 位):01111010

第 8 步:拼装结果


部分
符号位 1(负数,因为被减数小于减数)
指数(8位) 01111010
尾数(23位) 10110110101101100000100

最终结果:

1 01111010 10110110101101100000100

即:10111101010110110101101100000100

五、流程图总览

加法

减法

输入两个浮点数

第1步: 提取指数转十进制

第2步: 尾数补上隐含的1.

第3步: 对齐指数
小指数的数右移尾数

加法还是减法?

第4步: 尾数相加

第4步: 大数减小数
记录符号

第5步: 规格化
调整小数点位置

第6步: 舍入到23位

第7步: 指数转回二进制

第8步: 拼装符号+指数+尾数

输出结果

六、不同符号时怎么办?

如果两个数符号不同:

正数 + 负数  →  转成  正数 - 正数(减法)
负数 + 正数  →  转成  正数 - 正数(减法)
正数 - 负数  →  转成  正数 + 正数(加法)
负数 - 正数  →  转成  负数 + 负数(加法)

即:先统一符号,再决定做加法还是减法。

七、完整 C++ 验证代码

// 文件:float_ieee754.cpp
// 功能:逐步演示 IEEE 754 浮点数加减法的每个步骤
// 编译:g++ -o float_ieee754 float_ieee754.cpp -std=c++17
// 运行:./float_ieee754
#include <iostream>
#include <cstdint>
#include <cstring>
#include <bitset>
#include <string>
#include <cmath>
#include <cassert>
// =========================================================
// 工具函数:类型双关(type punning),安全地读取 float 的位
// =========================================================
// 把 float 的二进制位原封不动地读取为 uint32_t
uint32_t float_to_bits(float f) {
    uint32_t bits;
    std::memcpy(&bits, &f, sizeof(f)); // 不做任何类型转换
    return bits;
}
// 把 uint32_t 的位模式原封不动地还原为 float
float bits_to_float(uint32_t bits) {
    float f;
    std::memcpy(&f, &bits, sizeof(f));
    return f;
}
// =========================================================
// 解析 IEEE 754 float 的三个字段
// =========================================================
struct FloatParts {
    uint32_t sign;      // 符号位:0 或 1
    uint32_t exponent;  // 指数字段(8位,未减偏置)
    uint32_t mantissa;  // 尾数字段(23位,不含隐含位)
    int      true_exp;  // 真实指数 = exponent - 127
    float    value;     // 原始浮点值
};
FloatParts parse_float(float f) {
    uint32_t bits = float_to_bits(f);
    FloatParts p;
    p.value    = f;
    p.sign     = (bits >> 31) & 0x1;           // 最高位
    p.exponent = (bits >> 23) & 0xFF;           // 第[30..23]位
    p.mantissa =  bits        & 0x7FFFFF;       // 第[22..0]位
    p.true_exp = (int)p.exponent - 127;         // 减去偏置值
    return p;
}
// 打印一个浮点数的完整结构
void print_float_structure(const char* name, float f) {
    FloatParts p = parse_float(f);
    uint32_t bits = float_to_bits(f);
    std::cout << "  [" << name << "] = " << f << "\n";
    std::cout << "    完整位串: " << std::bitset<32>(bits) << "\n";
    std::cout << "    符号位:   " << p.sign << "\n";
    std::cout << "    指数字段: " << std::bitset<8>(p.exponent)
              << " (存储值=" << p.exponent
              << ", 真实指数=" << p.true_exp << ")\n";
    std::cout << "    尾数字段: " << std::bitset<23>(p.mantissa)
              << " (加隐含位: 1." << std::bitset<23>(p.mantissa) << ")\n\n";
}
// =========================================================
// 核心演示:加法的各步骤
// =========================================================
void demo_addition(float a, float b) {
    std::cout << "=========================================\n";
    std::cout << "  浮点加法演示: " << a << " + " << b << "\n";
    std::cout << "=========================================\n\n";
    // --- 步骤 1: 解析两个数的字段 ---
    std::cout << "【第1步】提取各字段\n";
    print_float_structure("a", a);
    print_float_structure("b", b);
    FloatParts pa = parse_float(a);
    FloatParts pb = parse_float(b);
    // --- 步骤 2: 补上隐含位,扩展为 64 位定点数方便计算 ---
    // 把尾数扩展到 64 位,并补上隐含的最高位 1(第23位)
    // 为了保留移位精度,再向左移 8 位作为"保护位"
    std::cout << "【第2步】补上隐含的1.\n";
    // 使用 uint64_t 来保存扩展后的尾数
    // 格式:[隐含1][23位尾数][8位保护位] = 共32个有效位
    uint64_t mant_a = ((uint64_t)1 << 23 | pa.mantissa) << 8;
    uint64_t mant_b = ((uint64_t)1 << 23 | pb.mantissa) << 8;
    std::cout << "  a 的尾数(含隐含位): 1." << std::bitset<23>(pa.mantissa) << "\n";
    std::cout << "  b 的尾数(含隐含位): 1." << std::bitset<23>(pb.mantissa) << "\n\n";
    // --- 步骤 3: 对齐指数(移动小数点)---
    std::cout << "【第3步】对齐指数\n";
    int exp_diff = (int)pa.exponent - (int)pb.exponent;
    int common_exp;  // 对齐后的公共指数(存储值)
    uint64_t ma, mb; // 对齐后的两个尾数
    if (exp_diff >= 0) {
        // a 的指数更大或相等,b 的尾数向右移
        common_exp = pa.exponent;
        ma = mant_a;
        mb = mant_b >> exp_diff; // b 右移 exp_diff 位
        std::cout << "  a的指数(" << pa.exponent << ") >= b的指数(" << pb.exponent << ")\n";
        std::cout << "  b的尾数向右移 " << exp_diff << " 位\n\n";
    } else {
        // b 的指数更大,a 的尾数向右移
        common_exp = pb.exponent;
        ma = mant_a >> (-exp_diff);
        mb = mant_b;
        std::cout << "  b的指数(" << pb.exponent << ") > a的指数(" << pa.exponent << ")\n";
        std::cout << "  a的尾数向右移 " << (-exp_diff) << " 位\n\n";
    }
    // --- 步骤 4: 尾数相加 ---
    std::cout << "【第4步】尾数相加\n";
    uint64_t sum = ma + mb;
    std::cout << "  相加结果(64位): " << std::bitset<32>((uint32_t)(sum >> 8)) << "\n\n";
    // --- 步骤 5: 规格化 ---
    std::cout << "【第5步】规格化\n";
    // 正常情况:隐含位在第 (23+8)=31 位
    // 如果加法产生进位,隐含位会跑到第 32 位
    // 检测最高有效位的位置
    int shift = 0;
    if (sum & ((uint64_t)1 << 32)) {
        // 有进位,整体右移 1 位,指数加 1
        sum >>= 1;
        common_exp += 1;
        shift = -1;
        std::cout << "  产生进位!小数点左移1位,指数+1 -> 指数=" << common_exp << "\n\n";
    } else if (!(sum & ((uint64_t)1 << 31))) {
        // 没有进位但最高位不在第31位,需要左移(通常发生在减法后)
        while (!(sum & ((uint64_t)1 << 31)) && sum != 0) {
            sum <<= 1;
            common_exp -= 1;
            shift++;
        }
        std::cout << "  需要左移 " << shift << " 位,指数-" << shift
                  << " -> 指数=" << common_exp << "\n\n";
    } else {
        std::cout << "  不需要调整\n\n";
    }
    // --- 步骤 6: 舍入 ---
    std::cout << "【第6步】舍入(取低8位作为保护位)\n";
    // 保护位在低 8 位,检查是否需要进位
    uint32_t guard = (uint32_t)(sum & 0xFF); // 低8位(超出23位的部分)
    uint32_t rounded_mantissa = (uint32_t)(sum >> 8) & 0x7FFFFF;
    if (guard > 0x80) {
        // 超出部分 > 0.5,向上舍入
        rounded_mantissa += 1;
        std::cout << "  保护位=" << std::bitset<8>(guard)
                  << " > 0x80,向上舍入\n\n";
    } else if (guard == 0x80) {
        // 正好是 0.5,最低有效位是1则向上(偶数舍入)
        if (rounded_mantissa & 1) {
            rounded_mantissa += 1;
            std::cout << "  保护位=0x80(正好一半),最低位=1,向上舍入(偶数舍入)\n\n";
        } else {
            std::cout << "  保护位=0x80(正好一半),最低位=0,保持不变(偶数舍入)\n\n";
        }
    } else {
        std::cout << "  保护位=" << std::bitset<8>(guard)
                  << " < 0x80,截断(向下舍入)\n\n";
    }
    // 检查舍入后是否溢出(尾数超过23位)
    if (rounded_mantissa >> 23) {
        rounded_mantissa >>= 1;
        common_exp += 1;
        std::cout << "  舍入后尾数溢出,再次右移,指数+1 -> 指数=" << common_exp << "\n\n";
    }
    // --- 步骤 7 & 8: 拼装结果 ---
    std::cout << "【第7-8步】拼装结果\n";
    // 确定符号位(同号加法,符号不变)
    uint32_t sign = pa.sign;
    // 组合三个字段
    uint32_t result_bits = (sign << 31) | ((uint32_t)common_exp << 23) | rounded_mantissa;
    float result = bits_to_float(result_bits);
    std::cout << "  符号位: " << sign << "\n";
    std::cout << "  指数:   " << std::bitset<8>(common_exp)
              << " (值=" << common_exp << ")\n";
    std::cout << "  尾数:   " << std::bitset<23>(rounded_mantissa) << "\n";
    std::cout << "  完整位串: " << std::bitset<32>(result_bits) << "\n\n";
    // --- 验证 ---
    float expected = a + b;
    std::cout << "  手动计算结果: " << result << "\n";
    std::cout << "  CPU 直接计算: " << expected << "\n";
    std::cout << "  结果一致:     " << (result == expected ? "是" : "否(有细微差异)") << "\n\n";
}
// =========================================================
// 演示1:直接对位模式做加法(错误做法)
// =========================================================
void demo_wrong_addition(float a, float b) {
    std::cout << "=========================================\n";
    std::cout << "  [错误演示] 直接对位模式做整数加法\n";
    std::cout << "=========================================\n\n";
    uint32_t bits_a = float_to_bits(a);
    uint32_t bits_b = float_to_bits(b);
    // 直接把两个 float 的位模式当作整数相加(完全错误的做法)
    uint32_t wrong_bits = bits_a + bits_b;
    float wrong_result = bits_to_float(wrong_bits);
    float correct_result = a + b;
    std::cout << "  a = " << a << "  位模式: " << std::bitset<32>(bits_a) << "\n";
    std::cout << "  b = " << b << "  位模式: " << std::bitset<32>(bits_b) << "\n";
    std::cout << "  位模式直接相加:  " << std::bitset<32>(wrong_bits) << "\n";
    std::cout << "  还原为 float:    " << wrong_result << "  (错误!)\n";
    std::cout << "  正确答案:        " << correct_result << "\n";
    std::cout << "  误差:            " << std::abs(wrong_result - correct_result) << "\n\n";
}
// =========================================================
// 演示2:精度损失(0.1 + 0.2 问题)
// =========================================================
void demo_precision() {
    std::cout << "=========================================\n";
    std::cout << "  精度损失演示: 0.1f + 0.2f\n";
    std::cout << "=========================================\n\n";
    float x = 0.1f, y = 0.2f, z = x + y;
    std::cout << "  0.1f 的实际存储值(高精度): ";
    printf("%.20f\n", (double)x);
    std::cout << "  0.2f 的实际存储值(高精度): ";
    printf("%.20f\n", (double)y);
    std::cout << "  0.1f + 0.2f 的实际结果:       ";
    printf("%.20f\n", (double)z);
    std::cout << "  等于 0.3f 吗? "
              << (z == 0.3f ? "是" : "否(精度损失!)") << "\n";
    std::cout << "  原因:0.1 和 0.2 在二进制下都是无限循环小数,\n"
              << "        存储时已经发生截断,误差在加法后累积。\n\n";
}
// =========================================================
// 演示3:大数吃小数(指数差异过大)
// =========================================================
void demo_absorption() {
    std::cout << "=========================================\n";
    std::cout << "  大数吃小数演示: 1e8f + 1.0f\n";
    std::cout << "=========================================\n\n";
    float big = 1e8f, small = 1.0f;
    float result = big + small;
    FloatParts pb = parse_float(big);
    FloatParts ps = parse_float(small);
    int diff = pb.true_exp - ps.true_exp;
    std::cout << "  1e8f 的真实指数: " << pb.true_exp << "\n";
    std::cout << "  1.0f 的真实指数: " << ps.true_exp << "\n";
    std::cout << "  指数差: " << diff << "(超过了尾数23位!)\n";
    std::cout << "  对齐时 1.0f 的尾数需要右移 " << diff << " 位\n";
    std::cout << "  所有尾数位都被移出,贡献为零\n\n";
    std::cout << "  1e8f + 1.0f = " << result << "\n";
    std::cout << "  结果等于 1e8f 吗? "
              << (result == big ? "是(1.0 被完全吞噬!)" : "否") << "\n\n";
}
// =========================================================
// 主函数
// =========================================================
int main() {
    std::cout << "\n";
    // 演示 1:加法步骤(文档中的示例数)
    // 对应位串: 01000011011101100100011100000000 + 01000001010100110011100011011101
    float a1 = bits_to_float(0x436C4700u); // 233.277...
    float b1 = bits_to_float(0x41533871u); // 13.19...
    demo_addition(a1, b1);
    // 演示 2:直接对位做加法(错误)
    demo_wrong_addition(1.5f, 0.125f);
    // 演示 3:精度损失
    demo_precision();
    // 演示 4:大数吃小数
    demo_absorption();
    return 0;
}

https://godbolt.org/z/Ps8oE1hfd

=========================================
  浮点加法演示: 236.277 + 13.2013
=========================================
【第1步】提取各字段
  [a] = 236.277
    完整位串: 01000011011011000100011100000000
    符号位:   0
    指数字段: 10000110 (存储值=134, 真实指数=7)
    尾数字段: 11011000100011100000000 (加隐含位: 1.11011000100011100000000)
  [b] = 13.2013
    完整位串: 01000001010100110011100001110001
    符号位:   0
    指数字段: 10000010 (存储值=130, 真实指数=3)
    尾数字段: 10100110011100001110001 (加隐含位: 1.10100110011100001110001)
【第2步】补上隐含的1.
  a 的尾数(含隐含位): 1.11011000100011100000000
  b 的尾数(含隐含位): 1.10100110011100001110001
【第3步】对齐指数
  a的指数(134) >= b的指数(130)
  b的尾数向右移 4 位
【第4步】尾数相加
  相加结果(64位): 00000000111110010111101010000111
【第5步】规格化
  不需要调整
【第6步】舍入(取低8位作为保护位)
  保护位=00010000 < 0x80,截断(向下舍入)
【第7-8步】拼装结果
  符号位: 0
  指数:   10000110 (值=134)
  尾数:   11110010111101010000111
  完整位串: 01000011011110010111101010000111
  手动计算结果: 249.479
  CPU 直接计算: 249.479
  结果一致:     是
=========================================
  [错误演示] 直接对位模式做整数加法
=========================================
  a = 1.5  位模式: 00111111110000000000000000000000
  b = 0.125  位模式: 00111110000000000000000000000000
  位模式直接相加:  01111101110000000000000000000000
  还原为 float:    3.19015e+37  (错误!)
  正确答案:        1.625
  误差:            3.19015e+37
=========================================
  精度损失演示: 0.1f + 0.2f
=========================================
  0.1f 的实际存储值(高精度): 0.10000000149011611938
  0.2f 的实际存储值(高精度): 0.20000000298023223877
  0.1f + 0.2f 的实际结果:       0.30000001192092895508
  等于 0.3f 吗? 是
  原因:0.1 和 0.2 在二进制下都是无限循环小数,
        存储时已经发生截断,误差在加法后累积。
=========================================
  大数吃小数演示: 1e8f + 1.0f
=========================================
  1e8f 的真实指数: 26
  1.0f 的真实指数: 0
  指数差: 26(超过了尾数23位!)
  对齐时 1.0f 的尾数需要右移 26 位
  所有尾数位都被移出,贡献为零
  1e8f + 1.0f = 1e+08
  结果等于 1e8f 吗? 是(1.0 被完全吞噬!)

八、舍入模式速查

IEEE 754 定义了 5 种舍入模式,默认使用第一种:

模式 规则 例子(保留1位小数)
最近偶数舍入(默认) 正好在中间时,让最低位变0 0.15 → 0.2,0.25 → 0.2
向正无穷舍入 始终向更大的方向取整 0.11 → 0.2,-0.11 → -0.1
向负无穷舍入 始终向更小的方向取整 0.19 → 0.1,-0.11 → -0.2
向零截断 去掉多余位,不进位 0.19 → 0.1,-0.19 → -0.1
向远离零舍入 总是远离零方向进位 0.11 → 0.2,-0.11 → -0.2

九、常见"坑"汇总


现象 原因 示例
0.1 + 0.2 ≠ 0.3 0.1 + 0.2 \neq 0.3 0.1+0.2=0.3 0.1、0.2 都是二进制无限小数,存储时已截断 0.1f + 0.2f == 0.30000001f
大数 + 小数 = 大数 指数差太大,小数尾数全部移出 1e8f + 1.0f == 1e8f
加法不满足结合律 每次加法都有舍入误差 (a+b)+c != a+(b+c)
直接比较浮点数不可靠 精度误差导致"相等"变"不等" 应用 fabs(a-b) < epsilon

十、编译与运行

# 编译
g++ -o float_ieee754 float_ieee754.cpp -std=c++17 -Wall
# 运行
./float_ieee754
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐