平时你是否遇见过下面这种情况?
此图源自比特就业
同样是 “9”,为什么 int 和 float 互相转换会得到完全离谱的结果?答案藏在计算机的内存存储规则里 —— 今天就从底层拆解整数、浮点数的存储逻辑,再用代码实战验证大小端,让你彻底搞懂这些底层原理!
一、前言
位(bit):计算机最小的存储单位。
字节(Byte):1 字节 = 8 位,是数据处理的基本单元(文件大小、内存占用都按字节算);
二进制高低位:最左侧是高位,最右侧是低位(比如 1100 0010,左边的 1 是高位,右边的 0 是低位)。
十进制转二进制:
- 整数部分:将十进制整数反复除以 2,记录每次的余数,直到商为 0,最后将余数倒序排列就是二进制结果。
- 小数部分:将十进制小数反复乘以 2,记录每次的整数部分(0 或 1),直到小数部分为 0(或达到精度要求),最后将整数部分正序排列就是二进制小数。
- eg:十进制:5.5 对应的二进制:101.1
二、整数在内存中的存储
整数分有符号(int/char)和无符号(unsigned int),核心差异是 “是否有符号位”,而负数的存储是关键 —— 内存中存的是补码。
(一)有符号的整数:
三种表示方式均有符号位和数值位两部分。最高位为符号位,符号位上0表示正,1表示负。其余为数值位。
正整数:原码、补码、反码都相同。
负整数:
- 原码:将数值按照正负数形式转换为二进制得到的就是原码。
- 反码:原码符号位不变,剩余位依次按位取反即可得到。
- 补码:反码加一即可得到补码
- eg:-5 在 8 位 char 类型中的存储形式
(二)无符号整数:
与有符号整数的区别:没有符号位,所有位数都用来表示数值,原码 = 反码 = 补码。
(三)范围说明
char:
无符号:位数为8,所有位均表示数值
最大取值:1111 1111 = 255
最小取值:0000 0000 = 0
有符号:位数为8,最高位表示符号,其余位表示数值
最大取值:0111 1111 = 127
最小取值:1000 0000 = -128
思考:为什么8位有符号是-128~127?
补码规则下,0 只有一种表示(0000 0000),而原码 1000 0000 本应表示 “-0”(无意义),因此被重新定义为 -128,相当于多挤出一个数值位,范围从 -127 扩展到 -128。
int:
无符号:位数为32,所有位均表示数值
最大取值:2^32 −1=4294967295
最小取值:0000 0000 …… 0000 = 0
有符号:位数为32,最高位表示符号,其余位表示数值
最大取值:2^31−1=2147483647
最小取值:−2^31 =−2147483648
三、浮点数在内存中的存储
(一)浮点数存储的核心标准:IEEE 754
核心公式
所有符合IEEE 754 标准的浮点数,最终值都按以下公式进行计算:
- S:符号位(0/1)
- M:尾数的二进制小数
- E:指数位的十进制值
(二)浮点数存的过程
此图源自比特就业
- 符号位:
S:0表示正数,1表示负
2.指数位:
偏移量
指数部分存储的是指数真实值+偏移量。
- 单精度偏移量:127
- 双精度偏移量:1023
3.尾数:
规格化数:
当指数不全为0且不全为1时,尾数前隐含一个1,即实际尾数=1.……(二进制)。这样表述的有效数字比存储位多一位,提高精度。
- 规格化数的尾数在存储时去掉前导1,仅存储小数部分。
4.特殊值
- 0:指数和尾数全为0(符号位可为0或+0)
- 无穷大:指数全为1,尾数全为0(符号位区分正负无穷
5.示例
将十进制数-5.75转换为单精度浮点数
1.转换为二进制:-5.75=-101.11
2.规格化:-1.0111*2^2
3.符号位:S=1(负数)
4.指数:E 2+127=129=10000001(二进制)
5.尾数:去掉 1.……,得到01110000000000000000000(23位)
6.拼接:1 10000001 01110000000000000000000
将十进制数0.5转换为单精度浮点数
1.转换为二进制:0.1
2.规格化:1.0*2^(-1)
3.符号位:S=0(正数)
4.指数:E -1+127=126=01111110(二进制)
5.尾数:00000000000000000000000
6.拼接:0 01111110 00000000000000000000000
(三)浮点取的过程
指数E从内存中取出还可以再分为三种情况:
E不全为0或不全为1
• 指数计算:真实指数 = 存储的 E 值 - 偏移量(单精度为 127,双精度为 1023)
• 尾数处理:有效数字 M 前补隐含的 1,即数值形式为 1.M × 2^(真实指数)
• 示例:0.5
- 二进制:0.1 → 规格化为 1.0 × 2^(-1)
- 指数:-1 + 127 = 126 → 二进制 01111110
- 尾数:去掉整数部分 1 后补 0 至 23 位 → 00000000000000000000000
- 完整存储:0 01111110 00000000000000000000000(符号位 0 表示正数)
E 全为 0
- 指数计算:真实指数 = 1 - 偏移量(单精度为 1-127=-126,双精度为 1-1023=-1022)
- 尾数处理:有效数字 M 前不再补 1,即数值形式为 0.M × 2^(真实指数)
E 全为 1
- 若尾数 M 全为 0:表示 “±∞”(符号位决定正负)
- 若尾数 M 不全为 0:表示NaN(Not a Number),用于无效运算结果(如 0/0、∞-∞)
回顾最开始导入的那段代码
int n=9的存储:00000000 00000000 00000000 00001001(十六进制:0x00000009)
int取出:直接按 “符号位 + 数值位” 转十进制,所以解析结果就是 9。
float取出:S=0 E=00000000(十进制 0)M=0000000 00000000 00001001(后面补 0 到 23 位) 最终值 V = (-1)^S*(0.M)*2^(1-127)(极接近 0),所以最终输出0.000000。
float 9.0 的存储:0 10000010 00100000000000000000000(十六进制:0x41100000)
int取出:0x41100000 转十进制:1091567616
float取出:S=0 E=10000010 M=00100000000000000000000 指数=130-127=3 所以会输出9.000000
四、大小端字节序和字节序判断
多字节类型(int/short/long)在内存中怎么排列?比如 0x12345678(4 字节),是先存 0x12 还是 0x78?
这就是**「大小端」**的核心问题(单字节 char 无此问题)。
大端模式(Big-Endian)
- 核心规则:高位字节 → 低地址,低位字节 → 高地址
- 类比:和我们书写数字的顺序一致(如0x12345678,先写高位0x12,再写低位0x78
小端模式(Little-Endian)
- 核心规则:低位字节 → 低地址,高位字节 → 高地址
- 类比:和我们书写数字的顺序相反(如0x12345678,先存低位0x78,再存高位0x12
- 假设0x12345678存储在地址0x100:
判断字节序
#include <stdio.h>
int main() {
int num = 0x12345678; // 4字节整数
char* p = (char*)# // 指向低地址的第一个字节
// 小端:低地址存 0x78;大端:低地址存 0x12
printf("当前字节序:%s\n", *p == 0x78 ? "小端模式" : "大端模式");
return 0;
}
运行结果:绝大多数电脑会输出「小端模式」(比如 Windows、Linux 主机)。