FOUNDATIONS OF COMPUTER SCIENCE 5TH EDITION计算机科学导论学习:第一章:计算机导论
1. 本章总览
这一章回答一个最基本的问题:计算机到底是什么?
作者从两个著名的理论模型出发来定义计算机:
- 图灵模型(Turing Model) — 计算机的哲学/数学定义
- 冯·诺伊曼模型(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 四大子系统
| 子系统 | 中文名 | 功能说明 |
|---|---|---|
| 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. 计算机的三大组成部分
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 三大时期概览
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 社会问题
数字鸿沟简单来说:
社会分成两类人:能上网的 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. 本章核心逻辑梳理(总结)
第二章:数字系统 — 详细中文解析
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. 两大类数字系统
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=±Sk−1×bk−1+⋯+S1×b1+S0×b0+S−1×b−1+⋯+S−l×b−l
其中:
- 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=10k−1
例如 k = 5 k=5 k=5 时: N m a x = 10 5 − 1 = 99999 N_{max} = 10^5 - 1 = 99999 Nmax=105−1=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×10−1+3×10−2
= 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=Sk−1×2k−1+⋯+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=2k−1
例如 k = 5 k=5 k=5: N m a x = 2 5 − 1 = 31 N_{max} = 2^5 - 1 = 31 Nmax=25−1=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=16k−1
例如 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=165−1=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=8k−1
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×2−1+1×2−2
= 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×16−1+3×16−2
= 16 + 10 + 0.125 + 0.012 ≈ 26.137 = 16 + 10 + 0.125 + 0.012 \approx 26.137 =16+10+0.125+0.012≈26.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×2−1+0×2−2+1×2−3=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} 2−1 | 2 − 2 2^{-2} 2−2 | 2 − 3 2^{-3} 2−3 | 2 − 4 2^{-4} 2−4 | 2 − 5 2^{-5} 2−5 | 2 − 6 2^{-6} 2−6 |
|---|---|---|---|---|---|---|
| 十进制 | 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 |
规则:
- 小值在大值后面 → 相加(如 V I I I = 5 + 1 + 1 + 1 = 8 VIII = 5+1+1+1 = 8 VIII=5+1+1+1=8)
- 小值在大值前面 → 相减(如 I V = 5 − 1 = 4 IV = 5-1 = 4 IV=5−1=4)
- I I I 或 V V V 不能放在 C C C 前面(不能相差100倍以上)
- 在符号上方加横线 → 乘以1000(如 V ˉ = 5000 \bar{V} = 5000 Vˉ=5000)
- 罗马数字没有零
转换示例:
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. 转换方法总结
15. 本章关键公式汇总
| 公式 | 含义 |
|---|---|
| N m a x = b k − 1 N_{max} = b^k - 1 Nmax=bk−1 | 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 存进去的?
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 0≤N≤2n−1
存储步骤:
- 十进制 → 二进制
- 左边补 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 2n−1,就会溢出。计算机会丢弃多余的高位,保留低 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=20,但4位存储显示 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) −(2n−1−1)≤N≤+(2n−1−1)
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 −2n−1≤N≤2n−1−1
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×10−14
三个组成部分:
± ⏟ 符号 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} (10100126个0 000…0.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} 2−14),不能直接存无符号数。
解决方法:给所有指数加一个固定偏置值(bias),使其变为非负数,然后当作无符号整数存储。
偏置值公式( m m m 为存储指数的位数):
bias = 2 m − 1 − 1 \text{bias} = 2^{m-1} - 1 bias=2m−1−1
例:4位存储,偏置 = 2 4 − 1 − 1 = 7 2^{4-1}-1 = 7 24−1−1=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 存储步骤(单精度为例)
- 确定符号 S(正 = 0,负 = 1)
- 十进制 → 二进制
- 规范化:变成 1. x x x × 2 n 1.xxx \times 2^n 1.xxx×2n 的形式
- 计算 E = 实际指数 + 127,转成8位二进制
- M = 小数点右边的位(不够23位右边补0)
- 拼接 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 取出步骤(单精度)
- 分离 S(1位)、E(8位)、M(23位)
- S=0正数,S=1负数
- 实际指数 = E - 127
- 还原:在 M 左边加 “1.”,得到规范化的二进制
- 按指数移动小数点,得到实际二进制数
- 加符号,转十进制
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)
每隔固定时间取一次音频值。采样率 = 每秒采多少次。
- 信号变化越快 → 需要采样率越高
- 实践标准:每秒 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. 本章知识结构总览
11. 关键公式总结
| 概念 | 公式 | 说明 |
|---|---|---|
| k k k 位能表示的种类数 | 2 k 2^k 2k | 位数越多,可表示种类越多 |
| 无符号 n n n 位最大值 | 2 n − 1 2^n - 1 2n−1 | 全1时最大 |
| 补码 n n n 位范围 | − 2 n − 1 -2^{n-1} −2n−1 到 + 2 n − 1 − 1 +2^{n-1}-1 +2n−1−1 | 负数比正数多一个 |
| 文字所需位数 | ⌈ 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. 本章总览
计算机对数据的操作分三大类:
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) A−B=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 符号—数值整数的加法和减法
符号—数值格式比补码复杂得多,需要分情况讨论。
关键规则(异号相加时):
- 若相加时产生溢出(进位出最高位):说明 ∣ 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 浮点数的加法和减法
浮点数加减需要先对齐小数点(统一指数),再做整数加减,最后重新规范化。
五步流程:
关键理解:对齐指数是什么意思
比如要算 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×2134−127=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 0→1, 1→0 |
| 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) A−B=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×2E−127
关键点:尾数前面隐藏了一个 “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×2−1,指数是 -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×2134−127
数字 2 = 1.10100110011100011011101 2 × 2 130 − 127 数字2 = 1.10100110011100011011101_2 \times 2^{130 - 127} 数字2=1.101001100111000110111012×2130−127
第 3 步:对齐指数(移动小数点)
两个数的指数不同(134 vs 130),差值为:
134 − 130 = 4 134 - 130 = 4 134−130=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
五、流程图总览
六、不同符号时怎么办?
如果两个数符号不同:
正数 + 负数 → 转成 正数 - 正数(减法)
负数 + 正数 → 转成 正数 - 正数(减法)
正数 - 负数 → 转成 正数 + 正数(加法)
负数 - 正数 → 转成 负数 + 负数(加法)
即:先统一符号,再决定做加法还是减法。
七、完整 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
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)