第19讲:数据在内存中的存储
📚 第19讲:数据在内存中的存储 💾
从“0”和“1”开始,揭开计算机记忆的秘密!
📂 目录
- 整数在内存中的存储 —— 补码的暗号系统
- 大小端字节序 —— 字节的排队方式
- 浮点数在内存中的存储 —— 科学计数法的特工编码
📖 正文开始
我们每天写 int a = 5;
,但你有没有想过:
“5” 这个数字,真的在内存里写了个“5”吗?
为什么-1
变成unsigned
后,突然变成了255
?
为什么9
当成float
读,会变成0.000000
?
别急,我们一个一个“破案”,还会运行真实代码,亲眼见证“内存魔术”!
整数在内存中的存储 —— 补码的暗号系统 🔐
🌟 生活比喻:银行的“负数账户”
想象你去银行开户:
- 存了 5 元 → 记账:
+5
- 欠了 5 元 → 银行不会写
-5
,而是用一种“暗号”来记账。
这个“暗号系统”就是 补码(Two’s Complement)。
计算机世界里,所有整数都用补码存储!
🔹 三种“暗号”形式
类型 | 规则 |
---|---|
原码 | 直接翻译成二进制,符号位为1表示负数 |
反码 | 符号位不变,其余位取反 |
补码 | 反码 + 1 |
✅ 示例:-5
的表示(8位)
原码: 1000 0101 ← 符号位1,数值101
反码: 1111 1010 ← 符号位不变,其余取反
补码: 1111 1011 ← 反码 + 1
✅ 正整数的原、反、补码都相同!
🔧 为什么用补码?—— 因为CPU只有“加法器”!
想象你家孩子想算:5 - 3 = ?
但你家的计算器只有“+”按钮,没有“-”按钮。怎么办?
聪明的办法:
把 -3
编码成一个“特殊数字”,让 5 + (这个数字) = 2
这个“特殊数字”就是 -3 的补码!
✅ 示例代码:验证补码加法
#include <stdio.h>int main() {int a = 5;int b = -3;printf("5 + (-3) = %d\n", a + b); // 输出:2return 0;
}
✅ 虽然你写了减法,但CPU底层是用“加法器”完成的!
大小端字节序 —— 字节的“排队方式” 🚶♂️🚶♀️
🌟 生活比喻:快递包裹的打包方式
假设你要寄一个大箱子,里面有4个小包裹:11
、22
、33
、44
但快递公司规定:必须按字节存放。
问题来了:
你是把“大号包裹”放前面,还是“小号包裹”放前面?
这就引出了两种“打包规则”:
打包方式 | 描述 |
---|---|
大端模式 | 高位字节 → 低地址 低位字节 → 高地址 |
小端模式 | 低位字节 → 低地址 高位字节 → 高地址 |
🔍 真实代码观察:内存中的“倒序”
#include <stdio.h>int main() {int a = 0x11223344; // 十六进制数return 0;
}
🔍 调试时你会发现:内存中存储的是
44 33 22 11
—— 倒着存的!
这就是 小端模式 的典型特征!
我们常用的 x86 电脑就是小端模式。
✅ 如何判断你的电脑是“左撇子”还是“右撇子”?
方法一:指针法(经典)
#include <stdio.h>int check_sys() {int i = 1;return (*(char *)&i); // 取第一个字节
}int main() {if (check_sys() == 1) {printf("小端\n"); // 小端:低字节在低地址} else {printf("大端\n");}return 0;
}
✅ 输出:
小端
方法二:联合体法(更直观)
int check_sys() {union {int i;char c;} un;un.i = 1;return un.c; // 共享内存,直接读char部分
}
✅ 两种方法殊途同归!
🧩 综合练习与代码解析
🔹 练习2:char
的“身份危机”
#include <stdio.h>
int main() {char a = -1;signed char b = -1;unsigned char c = -1;printf("a=%d, b=%d, c=%d\n", a, b, c);return 0;
}
📌 输出:
a=-1, b=-1, c=255
🔍 解析:
a
,b
:有符号,-1
的补码11111111
解释为-1
c
:无符号,11111111
解释为255
🔹 练习3:负数转无符号
#include <stdio.h>
int main() {char a = -128;printf("%u\n", a); // 输出:4294967168return 0;
}
🔍 过程:
char a = -128
→ 补码10000000
- 整型提升 →
11111111 11111111 11111111 10000000
- 以
%u
打印 → 无符号整数:4294967168
🔹 练习4:strlen
与字符范围
#include <stdio.h>
#include <string.h>
int main() {char a[1000];int i;for(i = 0; i < 1000; i++) {a[i] = -1 - i;}printf("%d\n", strlen(a)); // 输出:255return 0;
}
🔍 解析:
char
范围:-128 ~ 127
a[255] = -1 - 255 = -256 ≡ 0 mod 256
→\0
- 前255个都不是
\0
,所以strlen
返回255
🔹 练习5:无符号循环陷阱
#include <stdio.h>
unsigned char i = 0;
int main() {for(i = 0; i <= 255; i++) {printf("hello world\n"); // 死循环!}return 0;
}
⚠️ 警告:
i
是unsigned char
,最大255
,i++
后会回绕到0
,永远满足i <= 255
→ 无限循环!
🔹 练习6:指针运算(X86 小端)
#include <stdio.h>
int main() {int a[4] = {1, 2, 3, 4};int *ptr1 = (int *)(&a + 1); // 指向数组末尾后int *ptr2 = (int *)((char *)a + 1); // 从首地址偏移1字节printf("%x,%x\n", ptr1[-1], *ptr2); // 输出:4,2000000return 0;
}
🔍 解析:
ptr1[-1]
→a[3]
→4
a[0] = 1 = 0x00000001
(小端:01 00 00 00
)- 从第2字节开始读
int
→00 00 00 02
?不对- 实际:
00 00 00
+ 下一个int
的第1字节02
→02 00 00 00
→0x2000000
✅ 答案:4,2000000
浮点数在内存中的存储 —— 科学计数法的“特工编码” 🕵️♂️
🌟 生活比喻:科学家的“极简记账法”
科学家写数字从不写 9.0
,而是写:1.001 × 2³
IEEE 754 就是把这种写法“编码”成二进制。
✅ IEEE 754 公式:
V=(−1)S×M×2EV=(−1)S×M×2E
字段 | 含义 |
---|---|
S | 符号位:0 正,1 负 |
M | 有效数字,1 ≤ M < 2 |
E | 指数(阶码) |
🔧 存储时的“压缩技巧”
技巧1:省掉“1.”
M = 1.001
→ 存的时候只存 .001
,读的时候再加回来。
就像你写“1米5”省略“1”,别人知道是“1.5米”。
技巧2:指数加“偏移量”
E = 3` → 存 `3 + 127 = 130
因为内存里不能存负数!
就像温度计:-5°C
实际记录为268
(假设偏移273)
🔍 经典谜题破解:int
和 float
的“身份互换”
#include <stdio.h>
int main() {int n = 9;float *pFloat = (float *)&n;printf("n的值为:%d\n", n); // 9printf("*pFloat的值为:%f\n", *pFloat); // 0.000000*pFloat = 9.0;printf("n的值为:%d\n", n); // 1091567616printf("*pFloat的值为:%f\n", *pFloat); // 9.000000return 0;
}
🔍 第一环节:
9
当作float
读
E
全为0 → 特殊情况,M
前面是0.
→ 数值极小 →0.000000
🔍 第二环节:
9.0
存入,int
读出
9.0 = 1.001 × 2³
→E=130
,M=001...
- 二进制:
0 10000010 00100000000000000000000
- 当成整数读:
1091567616
🎯 总结:三张“记忆卡片” 🃏
卡片 | 核心要点 |
---|---|
整数 | 所有整数都用补码存储,负数的“暗号”让CPU只用加法器 |
大小端 | 小端:低字节在低地址(x86常见) 大端:高字节在低地址(网络协议常见) |
浮点数 | IEEE 754:S (正负)、E (指数+127)、M (省掉1.) |
🎯 恭喜你!
你现在不仅能写代码,还能看懂内存中的“0”和“1”在说什么!