数据类型与表达式(一)
数据的表示与存储:内存底层的位与码

从硬件位权展开、原反补码到 IEEE 754 浮点体系,理解“计算机如何存储与解释数据”

本课核心问题

同样是内存里的 8 个 bit,为什么能解释出完全不同的结果?

现实认知冲突

为什么 11111111 既能代表 -1,又能代表 255?为什么 0.1 + 0.2 在计算机里不等于 0.3?

今天核心推进

剥开 C 语言类型的语法外衣,看透二进制、符号位、补码以及浮点数 IEEE 754 科学编码机制。

底层核心机制

二进制按权展开、补码加法硬件归一化、指数移码偏置,以及浮点有限尾数带来的舍入误差。

第1课时学习目标

知识目标

  • 理解二进制位权原理与十进制、十六进制的双向转换
  • 掌握有符号整数的原码、反码、补码定义与补码加法溢出
  • 理解 IEEE 754 浮点数三要素(符号位、移码指数、尾数)与精度限制
  • 掌握字符与整数的 ASCII 映射关系及常见转义字符

能力目标

  • 能手算 8 位有符号数的原反补码并准确预测边界溢出范围(-128 ~ 127)
  • 能用 fabs(a - b) < eps 编写规范的浮点数安全近似比较逻辑
  • 能借助 AI 协作推演整型溢出边界,避免由于类型选用不当导致的隐蔽 Bug
6.1 / 进位计数制与位权展开
从物理开关到信息表示:
比特、字节与十六进制
计算机一切信息的最小物理单位只有 0 和 1,按权展开是理解一切进位制的通用钥匙。
比特 Bit 字节 Byte 按权展开 十六进制 0x

先看最小单位:0 和 1(比特与字节)

1 bit(位)= 1 个二进制开关(0 或 1)  8 bits = 1 Byte(字节,内存最小寻址单位)

单个比特的局限

1 个 bit 只能记录 2 种状态(如高低电平、对与错)。面对现实世界纷繁复杂的数值,必须将多个 bit 捆绑使用。

8位字节的丰富表达

8 个 bit 排成一排,可以构成 \(2^8 = 256\) 种截然不同的 0/1 状态组合。内存中的每一个字节都有唯一的物理内存地址。

在内存中,一切数据(无论是居民的血压数值、还是字符姓名)在物理芯片上都只是一串由高低电平构成的 0 和 1。

动手拨动 8 个二进制位:体验位权展开

💡 操作提示:从右往左位权依次为 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 内存地址表示、位掩码排版
为什么程序员钟爱十六进制? 因为 1 位十六进制数正好对应 4 位二进制(半字节,Nibble),8 位刚好写成 2 位十六进制(如 0xFB),紧凑且绝不丢失位结构。
6.1 / 整数编码与硬件运算
负号藏在哪里?
补码如何让减法变成加法
为了让计算机只用加法器就能完成减法,并消除 +0 与 -0 的冗余,补码应运而生。
符号位 原码 反码 补码 补码加法溢出

一个问题:负号存在哪里?

纸面上的数学写法

-5

人类使用专门的符号 - 标识负数,使用数字 5 标识绝对值大小。

机器的物理现实

[ ? ] 0 0 0 0 1 0 1

内存电路上只有 0 和 1,无法存入第三种“减号”形状的符号!

工程妥协:牺牲最高位(最左侧的一位)作为符号位(Sign Bit):约定 0 代表正数,1 代表负数。

原码与反码:负数的最初尝试与局限

原码(Sign-Magnitude)

最高位作符号位,其余各位为数值的绝对值:

+5 → 00000101  -5 → 10000101

致命缺陷:零有两种编码:+0 (00000000) 与 -0 (10000000);且无法直接用普通加法器做减法!

反码(Ones' Complement)

正数反码与原码相同;负数符号位为 1,数值位按位取反:

+5 → 00000101  -5 → 11111010

局限:依然存在 +0 (00000000) 与 -0 (11111111) 的二义性,跨零计算需要循环进位。

补码的诞生:加1让减法彻底转化为加法

原码表示 -5
10000101
→
数值位取反(反码)
11111010
→
末位加 1(补码)
11111011

硬件革命:减法变成加法

在补码体系下,\(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 统一使用加法器,运算效率极高

补码为什么能表示 -128?不对称区间之谜

8位原码/反码的取值范围

-127 ~ +127

由于 +0 与 -0 重复占用了 2 个编码位置,因此 256 个状态只能表示 255 个独立数值。

8位补码的取值范围

-128 ~ +127

因为零只有唯一编码,多出来的一个位串 10000000 在数学上恰好被定义为最小负数 -128!

重要推论:任何 \(n\) 位补码整数表示的范围都是不对称的:\([-2^{n-1}, \; 2^{n-1}-1]\)。负数总是比正数多一个!

补码加法:让机器只做自然溢出的位加法

试一试:5 + (-5) 产生进位溢出,低 8 位自然保留 0;而 127 + 1 会发生什么?符号位从 0 翻转为 1,产生严重的整型溢出(变成 -128)!

有符号与无符号:解释规则变了

同样是内存中的 11111111₂ (0xFF):
若作为 signed char 解释,它是补码 -1;
若作为 unsigned char 解释,它是纯无符号正数 255。

有符号类型(默认)

  • 最高位解释为符号位(0正1负)
  • 采用补码运算,支持负数表达
  • char: -128 ~ 127

无符号类型(unsigned)

  • 所有位均作为绝对数值位参与位权展开
  • 只能表示非负数,正数范围翻倍
  • unsigned char: 0 ~ 255

实际 C 语言整数类型的存储大小(sizeof)

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)。

🤖 AI 协作互动:整型溢出与符号位翻转推演

🤖 课堂实战:让 AI 推演整型溢出的微观时序

我正在学习 C 语言整数补码。有一段循环代码:for (signed char c = 120; c <= 130; c++) printf("%d ", c);。请不要直接修改代码,帮我逐步推演:当 c 达到 127 后执行 c++,底层的 8 位补码发生了什么变化?为什么循环条件 c <= 130 永远无法打破并导致死循环?请按【内存位变化—补码解释—逻辑后果】提供分析。

💡 课堂互动:同桌两人对照推演表,直观体会 127 变成 -128 的反转灾难,理解为什么循环控制变量不能用范围过小的整型。

6.1 / 实数的存储:IEEE 754
有限的比特,
如何表示无限连续的实数?
定点数走向浮点数:科学记数法的二进制映射与移码指数机制。
科学记数法 IEEE 754 标准 移码 Bias float / double

从整数走向小数:十进制小数转二进制

整数部分:除 2 取余法

从低位到高位逆序排列余数:

13 ÷ 2 = 6 ... 1
6 ÷ 2 = 3 ... 0
3 ÷ 2 = 1 ... 1
1 ÷ 2 = 0 ... 1
得 13₁₀ = 1101₂

小数部分:乘 2 取整法

从高位到低位顺序列出整数部分:

0.25 × 2 = 0.5 ... 取整 0
0.50 × 2 = 1.0 ... 取整 1
小数部分清零,结束!
得 0.25₁₀ = 0.01₂

组合得到定点实数:13.25₁₀ = 1101.01₂

动手把十进制小数改写成二进制(交互实验)

思考:先试 13.25 与 0.625,它们很快乘 2 归零;再试 0.1,为什么它一直乘 2 却永远无法清零?

浮点数的三部分架构:符号、指数与尾数

任何一个二进制实数都可以规格化为类似科学记数法的形式:
\[V = (-1)^S \times (1.M) \times 2^{E - \text{Bias}}\]

1. 符号位 \(S\)

1 bit:0 代表正数,1 代表负数。决定整体数值的正负方向。

2. 阶码/指数 \(E\)

保存经“移码(Bias)”偏移后的幂次,决定数值的覆盖范围与数量级。

3. 尾数 \(M\)

保存规格化后小数点后的小数位(最高位 1 隐含不存),决定数值的精度与分辨率。

为什么浮点指数需要“移码(Bias)”?

如果指数也用补码

指数既有正(如 \(2^3\)),也有负(如 \(2^{-5}\))。如果用补码存储,负指数最高位为 1,直接比较大小极其麻烦,硬件需要复杂的符号判断。

移码:加上一个固定偏置值

单精度 float 偏置 Bias = 127。真实指数是 3,存储时加上 127 变成 130 (10000010);真实指数是 -2,存为 125 (01111101)。

移码的精妙之处:将所有可能的指数全部平移为非负整数!比较两个浮点数大小时,硬件直接像整数一样逐位比对,极大简化电路设计。

单精度 float 与双精度 double:字段横向对比

规格类型 总位数 符号位 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。

IEEE 754 位级观察器(动态拆解实验)

输入任意数值,观察 32 位与 64 位下符号位、指数移码、尾数有效位的分布切片。

实战拆解:将 13.25 编码为单精度 IEEE 754

写出二进制
1101.01₂
→
科学规格化
1.10101₂ × 2³
→
求阶码移码
3 + 127 = 130
→
组合 32 位位串
S | 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 位十进制数就会发生舍入变形!

常见误区:“float 范围比 int 大,所以精度一定比 int 高”——错误! float 虽然范围远超 32 位整型,但其有效数字只有 24 位,表示大整数时会丢失个位甚至低多位精度!

为什么 0.1 无法在二进制浮点中精确存储?

十进制视角的 0.1

在十进制中,\(0.1 = \frac{1}{10}\),因为分母 10 是基数 10 的质因数,所以它是一位有限小数。

二进制视角的 0.1

在二进制中,分母 10 包含质因数 5,无法表示为 \(2^k\) 的有限和:

0.1₁₀ = 0.0001100110011...₂(无限循环)

因为 float 只有 23 位尾数,double 只有 52 位尾数,无限循环小数必须被截断舍入。因此,计算机中存入的并不是严格的 0.1,而是一个极其逼近 0.1 的近似值!
6.1 / 浮点判断与字符编码
绝对相等是幻觉,
字符本质也是整数
浮点数判断必须引入误差容限 epsilon;字符在内存中本质是 ASCII 整数编码。
== 比较陷阱 fabs 容限 ASCII 编码 转义字符

认知冲突:为什么计算后的浮点数不宜直接 ==?

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}\)!

工程规范法则

在任何严谨的工业级软件(如医疗仪器、航天飞行控制)中:
绝对禁止对计算产生的浮点数直接使用 == 或 != 进行判定!

浮点比较容差实验(调整 Epsilon 验证)

引入容差门限:只要二者差值的绝对值小于一个极小的阈值(如 \(10^{-6}\)),我们就认为在工程上它们“相等”。

正确比较两个浮点数:引入精度门限

#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 是处理浮点比较的通用解法。

字符也是编码:从数字回到其他类型

ASCII 码的核心映射

'0' 是 48,'A' 是 65,'a' 是 97。
大小写字母间距为 32('a' - 'A' = 32)。

字符变量的本质

在 C 语言中 char 就是 1 字节整数。
'A' + 1 精确得到整数 66,以 %c 输出即为 'B'!

字符常量、转义字符与常见常量类型

字符 vs 字符串

'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。
第1课时总结 / 课堂检验
从物理位到逻辑类型:
构筑安全高效的程序基础
一切类型皆为解释规则。理解底层存储,才能避开溢出与精度黑洞。
知识图谱 课堂检验 课后延伸

第1课时全景小结:一张图串起所有概念

1. 整数与补码

  • 最小单位是 0/1 位与 8 位字节
  • 现代计算机整数统一采用补码存储
  • 补码消除了双零,把减法统一为加法
  • 8 位补码范围:-128 ~ +127

2. 实数与 IEEE 754

  • 浮点三要素:符号位、阶码、尾数
  • 阶码采用移码,便于快速硬件比对
  • 0.1 无法在二进制有限位中精确表示
  • 浮点比较禁止用 ==,需用 fabs < eps

3. 字符与规范

  • 字符本质也是小整数(ASCII 码)
  • 注意单引号 'A' 与双引号 "A" 的区别
  • 输出时类型与格式说明符(%d, %u, %f, %c)必须严格匹配

Exit Ticket · 课堂检验

【诊断题一】 8 位二进制补码 11111011 表示的真值是多少?
【诊断题二】 为什么 double a = 0.1 + 0.2; if (a == 0.3) 的判断结果常常为假?
【诊断题三】 float 和 double 在内存结构与有效精度上有何本质差别?

课后巩固与第2课时先导

课后实战任务

  • 在本地 IDE 中编写程序,用 sizeof 打印本机各类型字节数
  • 尝试用 printf("%.20f", 0.1) 观察 0.1 在内存中的真实展开近似值
  • 写一段程序验证 8 位补码 127 + 1 发生溢出变为 -128 的全过程

第2课时悬念:类型转换与除法截断

我们已经清楚了解了数据的存储结构:
但如果在算术计算中把 int 和 double 混合在一起,会发生什么?
为什么 5 / 2 算出来等于 2 而不是 2.5?
下节见——第2课时:类型转换与表达式求值!