程序设计基础 · 第6次课 · 第1课时
从硬件位权展开、原反补码到 IEEE 754 浮点体系,理解“计算机如何存储与解释数据”
同样是内存里的 8 个 bit,为什么能解释出完全不同的结果?
为什么 11111111 既能代表 -1,又能代表 255?为什么
0.1 + 0.2 在计算机里不等于 0.3?
剥开 C 语言类型的语法外衣,看透二进制、符号位、补码以及浮点数 IEEE 754 科学编码机制。
二进制按权展开、补码加法硬件归一化、指数移码偏置,以及浮点有限尾数带来的舍入误差。
-128 ~ 127)fabs(a - b) < eps 编写规范的浮点数安全近似比较逻辑1 bit(位)= 1 个二进制开关(0 或 1) 8 bits = 1 Byte(字节,内存最小寻址单位)
1 个 bit 只能记录 2 种状态(如高低电平、对与错)。面对现实世界纷繁复杂的数值,必须将多个 bit 捆绑使用。
8 个 bit 排成一排,可以构成 \(2^8 = 256\) 种截然不同的 0/1 状态组合。内存中的每一个字节都有唯一的物理内存地址。
💡 操作提示:从右往左位权依次为 2⁰=1, 2¹=2, 2²=4, 2³=8, 2⁴=16, 2⁵=32, 2⁶=64, 2⁷=128。尝试将第 7、5、3、1 位设为 1,观察十六进制与十进制结果。
| 进制类型 | 基数与数码 | C 语言前缀表示 | 十进制 10 | 十进制 16 | 应用场景 |
|---|---|---|---|---|---|
| 十进制 | 基数 10,逢十进一(0~9) | 无前缀(默认),如 16 |
10 |
16 |
人类日常思维与常规输出 |
| 二进制 | 基数 2,逢二进一(0, 1) | 0b / 0B(C99扩展) |
0b1010 |
0b10000 |
硬件底层位串直接对应 |
| 八进制 | 基数 8,逢八进一(0~7) | 以数字 0 开头 |
012 |
020 |
传统文件权限、历史系统 |
| 十六进制 | 基数 16,逢十六进一(0~9, A~F) | 以 0x 或 0X 开头 |
0xA |
0x10 |
内存地址表示、位掩码排版 |
0xFB),紧凑且绝不丢失位结构。
-5
人类使用专门的符号 - 标识负数,使用数字 5 标识绝对值大小。
[ ? ] 0 0 0 0 1 0 1
内存电路上只有 0 和 1,无法存入第三种“减号”形状的符号!
0
代表正数,1 代表负数。
最高位作符号位,其余各位为数值的绝对值:
+5 → 00000101 -5 → 10000101
致命缺陷:零有两种编码:+0 (00000000) 与
-0 (10000000);且无法直接用普通加法器做减法!
正数反码与原码相同;负数符号位为 1,数值位按位取反:
+5 → 00000101 -5 → 11111010
局限:依然存在 +0 (00000000) 与
-0 (11111111) 的二义性,跨零计算需要循环进位。
-5100001011111101011111011在补码体系下,\(A - B = A + (-B)_{\text{补}}\)。CPU 内部只需要设计结构极为简单高效的加法器,无需专门的减法器硬件!
+0 的补码是 00000000;-0 取反加 1 后为
[1]00000000(溢出进位丢弃),同样归一为 00000000!
| 编码方案 | +5 的 8 位表示 |
-5 的 8 位表示 |
零的编码情况 | 运算硬件要求 |
|---|---|---|---|---|
| 原码 | 00000101 |
10000101 |
存在 +0 与 -0 两种表示 |
符号与绝对值需分开处理,减法器复杂 |
| 反码 | 00000101 |
11111010 |
存在 +0 与 -0 两种表示 |
加减法需设计循环进位端 |
| 补码(现代标准) | 00000101 |
11111011 |
零只有唯一的 00000000 |
统一使用加法器,运算效率极高 |
-127 ~ +127
由于 +0 与 -0 重复占用了 2 个编码位置,因此 256 个状态只能表示 255
个独立数值。
-128 ~ +127
因为零只有唯一编码,多出来的一个位串 10000000 在数学上恰好被定义为最小负数
-128!
试一试:5 + (-5) 产生进位溢出,低 8 位自然保留 0;而
127 + 1 会发生什么?符号位从 0 翻转为 1,产生严重的整型溢出(变成 -128)!
11111111₂ (0xFF):signed char 解释,它是补码 -1;unsigned char 解释,它是纯无符号正数 255。
char: -128 ~ 127unsigned char: 0 ~ 255| C 数据类型 | 典型字节数 | 总位数 | 有符号取值范围 | 无符号取值范围 |
|---|---|---|---|---|
char |
1 Byte | 8 bits | -128 ~ 127 | 0 ~ 255 |
short [int] |
2 Bytes | 16 bits | -32,768 ~ 32,767 | 0 ~ 65,535 |
int |
4 Bytes | 32 bits | 约 -21.47 亿 ~ +21.47 亿 | 0 ~ 约 42.94 亿 |
long long |
8 Bytes | 64 bits | 约 \(-9.22 \times 10^{18} \sim 9.22 \times 10^{18}\) | 0 ~ 约 \(1.84 \times 10^{19}\) |
注:各类型具体大小由编译器与系统 ABI 决定,C 标准仅规定
sizeof(short) <= sizeof(int) <= sizeof(long)。
💡 课堂互动:同桌两人对照推演表,直观体会 127 变成 -128 的反转灾难,理解为什么循环控制变量不能用范围过小的整型。
从低位到高位逆序排列余数:
13 ÷ 2 = 6 ... 16 ÷ 2 = 3 ... 03 ÷ 2 = 1 ... 11 ÷ 2 = 0 ... 1
得
13₁₀ = 1101₂
从高位到低位顺序列出整数部分:
0.25 × 2 = 0.5 ... 取整 00.50 × 2 = 1.0 ... 取整 1
小数部分清零,结束!
得
0.25₁₀ = 0.01₂
13.25₁₀ = 1101.01₂
思考:先试 13.25 与 0.625,它们很快乘 2 归零;再试
0.1,为什么它一直乘 2 却永远无法清零?
1 bit:0 代表正数,1 代表负数。决定整体数值的正负方向。
保存经“移码(Bias)”偏移后的幂次,决定数值的覆盖范围与数量级。
保存规格化后小数点后的小数位(最高位 1 隐含不存),决定数值的精度与分辨率。
指数既有正(如 \(2^3\)),也有负(如 \(2^{-5}\))。如果用补码存储,负指数最高位为 1,直接比较大小极其麻烦,硬件需要复杂的符号判断。
单精度 float 偏置 Bias = 127。真实指数是 3,存储时加上 127 变成
130 (10000010);真实指数是 -2,存为 125 (01111101)。
| 规格类型 | 总位数 | 符号位 S | 阶码指数 E (Bias) | 尾数字段 M | 有效十进制精度 | 数值绝对值范围 |
|---|---|---|---|---|---|---|
float(单精度 binary32) |
32 bits (4B) | 1 bit | 8 bits (Bias = 127) | 23 bits (实含24位有效) | 约 6 ~ 7 位 | \(\approx 1.18 \times 10^{-38} \sim 3.40 \times 10^{38}\) |
double(双精度 binary64) |
64 bits (8B) | 1 bit | 11 bits (Bias = 1023) | 52 bits (实含53位有效) | 约 15 ~ 17 位 | \(\approx 2.23 \times 10^{-308} \sim 1.79 \times 10^{308}\) |
C 语言默认浮点字面量(如 3.14)均为 double 类型;若要指定为 float
需加后缀 3.14f。
输入任意数值,观察 32 位与 64 位下符号位、指数移码、尾数有效位的分布切片。
1101.01₂1.10101₂ × 2³3 + 127 = 130S | E | M| 分段 | 字段含义 | 推导计算 | 最终存储二进制 |
|---|---|---|---|
| S 符号 | 正数符号为 0 | 正数 | 0 |
| E 指数 | 8 位移码 (Bias=127) | \(3 + 127 = 130\) | 10000010 |
| M 尾数 | 23 位(去掉隐含前导1) | 后跟 10101,右补零至23位 |
10101000000000000000000 |
最终拼成:0 10000010 10101000000000000000000(十六进制 0x41540000)
阶码指数决定了该数值在数轴上的宏观跨度数量级(能表示多大如 \(10^{38}\),或多小如 \(10^{-38}\))。
尾数的有效位数决定了邻近两个可分辨数值之间的微观刻度分辨率。float 超过 7 位十进制数就会发生舍入变形!
在十进制中,\(0.1 = \frac{1}{10}\),因为分母 10 是基数 10 的质因数,所以它是一位有限小数。
在二进制中,分母 10 包含质因数 5,无法表示为 \(2^k\) 的有限和:
0.1₁₀ = 0.0001100110011...₂(无限循环)
double a = 0.1 + 0.2;
if (a == 0.3) {
printf("相等\n");
} else {
printf("不相等!实际 a = %.17f\n", a);
}
不相等!实际 a = 0.30000000000000004
因为 0.1 和 0.2 底层都被截断微量舍入,二者相加后比 0.3 多了 \(4 \times 10^{-17}\)!
在任何严谨的工业级软件(如医疗仪器、航天飞行控制)中:
绝对禁止对计算产生的浮点数直接使用 ==
或 != 进行判定!
#include <stdio.h>
#include <math.h>
const double EPS = 1e-6; /* 定义允许的工程误差容限 0.000001 */
int main(void)
{
double a = 0.1 + 0.2;
double target = 0.3;
if (fabs(a - target) < EPS) {
printf("工程判定:数值在容差内相等!\n");
}
return 0;
}
fabs(a - b) < 1e-6 是处理浮点比较的通用解法。'0' 是
48,'A' 是 65,'a' 是
97。
大小写字母间距为 32('a' - 'A' = 32)。
在 C 语言中
char 就是 1 字节整数。'A' + 1 精确得到整数
66,以 %c 输出即为 'B'!
'A' 是字符常量(占1字节);"A" 是字符串(末尾含隐含的
'\0' 结束符,占2字节)。二者绝不能混淆!
'\n':换行(ASCII
10)'\t':横向制表跳格(Tab)'\\':反斜杠本身'\'':单引号字符
100:int 型常量100L:long
型常量3.14:double 型常量3.14f:float 型常量
int sbp = 128; /* 收缩压:整型 */
unsigned int id = 10025; /* 居民档案号:无符号整型 */
float temp = 36.6f; /* 体温:单精度浮点 */
double bmi = 22.86; /* BMI指数:双精度浮点 */
char grade = 'A'; /* 慢病风险等级:字符型 */
printf("档案号:%u | 血压:%d mmHg | 体温:%.1f ℃ | BMI:%.2f | 评级:%c\n",
id, sbp, temp, bmi, grade);
%d 对应 int,%u 对应
unsigned,%f / %lf 对应浮点,%c 对应 char。
-128 ~ +127==,需用 fabs < eps'A' 与双引号 "A" 的区别%d, %u, %f, %c)必须严格匹配11111011 表示的真值是多少?double a = 0.1 + 0.2; if (a == 0.3) 的判断结果常常为假?float 和 double 在内存结构与有效精度上有何本质差别?
① 解析:最高位为 1 是负数。补码取反加 1 还原绝对值:11111011 符号不变数值取反为
10000100,再加 1 得 10000101(原码 -5)。因此代表整数 -5。
② 解析:因为 0.1 和 0.2 在二进制中均为无限循环小数,截断存储产生微小舍入误差,相加后结果为
0.30000000000000004,与严格的 0.3 不完全相等。工程上必须用
fabs(a - 0.3) < 1e-6。
③ 解析:float 占 32 位(1符号+8阶码+23尾数),有效数字约 6~7 位;double 占 64 位(1符号+11阶码+52尾数),有效数字约 15~17 位,精度和覆盖范围均大幅超越 float。
sizeof 打印本机各类型字节数printf("%.20f", 0.1) 观察 0.1 在内存中的真实展开近似值
我们已经清楚了解了数据的存储结构:
但如果在算术计算中把 int 和 double 混合在一起,会发生什么?
为什么 5 / 2 算出来等于 2 而不是 2.5?
下节见——第2课时:类型转换与表达式求值!