深入理解数据在内存中的存储:整数与浮点数的二进制表示
✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松get !
🚀 个人主页 :0xCode小新 · CSDN
🌱 代码仓库 :0xCode小新· Gitee
📌 专栏系列
- 📖 《c语言》
📖 《鸿蒙应用开发项目教程》💬 座右铭 : “ 积跬步,以致千里。”
你是否有过这样的疑惑,为什么同样的二进制序列,整数和浮点数解读时结果天差地别?数据在内存中究竟是如何存储的呢?
我将带你深入探索数据在内存中的存储方式,从整数的原码、反码、补码,到大小端字节序的判断,再到浮点数遵循的IEEE(电气和电子工程协会) 754标准,一步步揭开内存存储的底层秘密。
目录
- 1. 整数在内存中的存储
- 一、整数的三种二进制表示
- 二、为什么内存中存储的是补码?
- 2. 大小端字节序和字节序判断
- 一、什么是大小端字节序?
- 二、为什么会有大小端之分?
- 三、如何判断当前机器的字节序?
- 四、练习
- 3. 浮点数在内存中的存储
- 一、浮点数存储的困惑
- 二、IEEE 754标准:浮点数的表示方法
- 三、浮点数在内存中的存储格式
- 四、存储过程的特殊规则
- 五、读取过程的三种情况
- 六、实例解析:回到开头的例子
1. 整数在内存中的存储
一、整数的三种二进制表示
在之前我们讲解操作符时我们就讲到:计算机中的整数有三种二进制表示方法:原码、反码和补码。这三种方式都是为了表示有符号整数,其中最高位为符号位(0代表正,1代表负),其余位为数值位。
- 原码:最直观的表示法,直接将数值转换为二进制。
- 例如:
+5
的原码是00000101
,-5
的原码是10000101
。
- 例如:
- 反码:在原码基础上,符号位不变,数值位按位取反。
- 例如:
-5
的原码是10000101
,其反码为11111010
。
- 例如:
- 补码:反码 + 1。这是整数在内存中最终存储的形式。
- 例如:
-5
的反码是11111010
,其补码为11111011
。
- 例如:
核心规则:
- 正整数的原反补码都相同。
- 负整数的原码、反码、补码需要按照上述规则转换。
- 所有整型数据在内存中存放的都是补码。
二、为什么内存中存储的是补码?
使用补码进行存储并非偶然,而是一项非常厉害的设计,主要基于以下三大原因(了解即可):
-
符号统一
使用补码,可以将符号位和数值位同等对待,一同参与运算,无需额外区分正负。 -
运算统一
计算机的CPU中只有加法器,没有减法器。通过补码,减法可以转换为加法运算。例如:计算
5 - 3
,相当于5 + (-3)
。用补码计算:00000101 + 11111101 = 00000010
(进位溢出的部分舍弃),结果正是2
。 -
零值统一
在原码和反码中,+0
和-0
的表示不同(原码:00000000
和10000000
),这会导致两个不同的编码对应同一个数值零。而在补码中,0
的表示是唯一的(00000000
),用10000000
来表示 -128(对于8位有符号char),从而扩展了可表示的负数范围。
2. 大小端字节序和字节序判断
在理解了整数以补码形式存储后,我们来看一个细节:
当一个数据超过一个字节时,它的各个字节在内存中是如何排列的?这就是字节序问题,它决定了数据在内存中的顺序。
一、什么是大小端字节序?
字节序分为两种主要模式:大端字节序 和 小端字节序。
- 大端字节序:数据的高位字节存储在低地址处,低位字节存储在高地址处。
- 小端字节序:数据的低位字节存储在低地址处,高位字节存储在高地址处。
假设一个32位整数 0x11223344
(其中 0x11
是最高位字节,0x44
是最低位字节)。
内存地址 | 大端模式存储内容 | 小端模式存储内容 |
---|---|---|
低地址 | 0x11 | 0x44 |
↑ | 0x22 | 0x33 |
↑ | 0x33 | 0x22 |
高地址 | 0x44 | 0x11 |
从调试器的内存窗口看,小端模式下的 0x11223344
会显示为 44 33 22 11
。
二、为什么会有大小端之分?
字节序的存在源于计算机系统设计的两大现实:
- 以字节为寻址单位:内存每个地址对应一个字节(8 bit)。
- 多字节数据的处理:对于
short
(16 bit)、int
(32 bit) 等多字节数据类型,当处理器寄存器宽度大于一个字节时,就必须约定好多个字节的存放顺序。
不同的硬件厂商做出了不同的选择:
- Intel x86/x64架构、以及大多数的ARM处理器采用小端模式。
- KEIL C51、以及某些网络设备和早期的PowerPC、Motorola处理器采用大端模式。
此外,网络传输中使用的网络字节序是大端模式。这是为了确保不同字节序的主机在网络上通信时,数据能被正确解析。
三、如何判断当前机器的字节序?
这是一个经典的面试题(百度面试题)。核心思路是:创建一个多字节的整数,然后检查其第一个字节(最低地址处的字节)的内容。:
使用指针类型转换(强制类型转换)
#include <stdio.h>int check_sys() {int i = 1; // 0x00000001// 将int*强制转换为char*,然后解引用,即可访问到i的低地址字节return (*(char *)&i);
}int main() {int ret = check_sys();if (ret == 1) {printf("小端\n");} else {printf("大端\n");}return 0;
}
原理分析:
- 变量
i
的值为1。在小端模式下,其内存布局为01 00 00 00
;在大端模式下,为00 00 00 01
。 (char *)&i
获取i
的地址,并将其转换为指向char
(单字节)的指针。- 解引用这个指针
*(char *)&i
,就得到了i
在最低地址的那个字节。 - 如果这个字节是
1
,说明低位字节在低地址,是小端机器;如果是0
,说明高位字节在低地址,是大端机器。
四、练习
接下来我们通过几个练习来加深对知识点的掌握,建议先独立思考程序的运行结果,再对照后续解析验证,这样能更高效地巩固所学内容哦~
练习1
#include <stdio.h>
int main()
{char a = -1;signed char b = -1;unsigned char c = -1;printf("a=%d,b=%d,c=%d", a, b, c);return 0;
}
解析:
- 变量
a
和b
(有符号char)
-1
的8位补码是11111111
- 当用
%d
打印时,发生整型提升:由于是有符号类型,高位用符号位填充11111111
→ 提升为11111111 11111111 11111111 11111111
(32位,仍是-1的补码)- 所以输出
-1
- 变量
c
(无符号char)
-1
赋值给无符号char时,-1
的补码11111111
被直接解释为无符号数11111111
作为无符号数就是255
- 当用
%d
打印时,同样发生整型提升:但由于是无符号类型,高位用0填充11111111
→ 提升为00000000 00000000 00000000 11111111
(值为255)- 所以输出
255
练习2
#include <stdio.h>
int main()
{char a = -128;printf("%u\n", a);return 0;
}int main()
{char a = 128;printf("%u\n", a);return 0;
}
解析代码片段1:
- 赋值阶段:对于8位有符号char,
-128
的补码是10000000
- 整型提升阶段:用
%u
打印时,char
先被提升为int
- 由于是有符号类型且符号位为1,高位全部补1
10000000
→11111111 11111111 11111111 10000000
- 解释阶段:这个32位二进制序列被
%u
当作无符号整数解释
11111111 11111111 11111111 10000000
= 2³² - 128 = 4294967168
解析代码片段2:
- 赋值阶段:8位有符号char的范围是 -128 到 127
128
超过了最大值127,发生溢出128
的二进制是10000000
,这正好是-128
的补码- 所以实际上
a
存储的是-128
- 后续阶段:与代码片段1完全相同
- 整型提升后得到
11111111 11111111 11111111 10000000
- 用
%u
输出为 4294967168
练习3
#include <stdio.h>
int main()
{char a[1000];int i;for (i = 0; i < 1000; i++){a[i] = -1 - i;}printf("%d", strlen(a));return 0;
}
解析:
- 理解赋值过程
a[i] = -1 - i
- 当
i=0
时,a[0] = -1
- 当
i=1
时,a[1] = -2
- …
- 当
i=127
时,a[127] = -128
(char的最小值)- 当
i=128
时,a[128] = -129
- 关键:char类型的溢出行为
- 有符号char的范围是 -128 到 127
- 当
i=128
时:-1 - 128 = -129
,超过了char的最小值- 发生溢出:
-129
会"回绕"到127
-129
→127
(因为 -129 = -128 - 1 → 127)- 继续:
i=129
→a[129] = -130
→126
i=130
→a[130] = -131
→125
- …
- 直到
i=255
→a[255] = -256
→0
- strlen的工作原理
strlen
函数从数组开头开始计数,直到遇到第一个'\0'
(即数值0)- 在我们的数组中,第一个0出现在
a[255]
- 因此
strlen(a)
计算的是a[0]
到a[254]
的字符个数,共255个
练习4
#include <stdio.h>
unsigned char i = 0;
int main()
{for (i = 0; i <= 255; i++){printf("hello world\n");}return 0;
}int main()
{unsigned int i;for (i = 9; i >= 0; i--){printf("%u\n", i);}return 0;
}
解析代码片段1:
unsigned char
的范围是 0 到 255- 当
i = 255
时,执行i++
会溢出变为0
- 循环条件
i <= 255
永远为真 - 结果:无限循环
解析代码片段2:
unsigned int
的范围是 0 到 4294967295 (32位系统)- 当
i = 0
时,执行i--
会下溢变为最大值 4294967295 - 循环条件
i >= 0
对于无符号数永远为真 - 结果:无限循环
练习5
#include <stdio.h>
//X86环境 ⼩端字节序
int main()
{int a[4] = { 1, 2, 3, 4 };int* ptr1 = (int*)(&a + 1);int* ptr2 = (int*)((int)a + 1);printf("%x,%x", ptr1[-1], *ptr2);return 0;
}
解析:
第一部分:理解 ptr1
和 ptr1[-1]
&a
的类型:&a
是整个数组的地址,类型是int (*)[4]
(指向长度为4的int数组的指针)&a + 1
:指针算术运算,加上的是整个数组的大小
- 数组
a
有4个int元素,每个int占4字节,总大小为 16 字节&a + 1
指向数组末尾之后的位置ptr1[-1]
:等价于*(ptr1 - 1)
ptr1 - 1
向后移动一个int(4字节),指向a[3]
ptr1[-1]
的值就是a[3]
,即4
第二部分:理解 ptr2
和 *ptr2
(int)a
:将数组首地址转换为整数(失去指针类型信息)(int)a + 1
:地址值加1(不是加一个int,而是加1个字节)- 转换为
int\*
:将这个不对齐的地址强制转换为int*
ptr2
指向a + 1
字节处:
- 从
a[0]
的第2个字节开始读取4个字节- 读取的字节序列为:
00 00 00 02
小端序解释:
- 在小端序中,低位字节在前
- 字节序列
00 00 00 02
被解释为0x02000000
- 所以
*ptr2 = 0x02000000 = 33554432
(十进制)
3. 浮点数在内存中的存储
现在我们就来揭开这个困惑现象背后的秘密——这一切的关键,就藏在浮点数独特的存储规则里,也就是IEEE 754 标准。
常见的浮点数:3.14159、1E10等,浮点数家族包括: float
、double
、long double
类型。
浮点数表示的范围: float.h
中定义
一、浮点数存储的困惑
先来看一个例子:
#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;
}
上面的代码中, num
和 *pFloat
在内存中明明是同⼀个数,为什么浮点数和整数的解读结果会差别这么大呢?
答案很简单:整数和浮点数在内存中使用完全不同的编码规则。理解这种差异,就需要深入IEEE 754标准。
二、IEEE 754标准:浮点数的表示方法
IEEE 754标准将任意一个二进制浮点数 V 表示为以下形式:
V=(−1)S×M×2EV=(−1)^S×M×2^E V=(−1)S×M×2E
- S(符号位):0表示正数,1表示负数
- M(有效数字):范围在 [1, 2) 之间的小数
- E(指数位):2的幂次
举例说明:
- 十进制
5.0
→ 二进制101.0
→ 科学计数法1.01 × 2^2
- S = 0, M = 1.01, E = 2
- 十进制
-5.0
→ 二进制-101.0
→ 科学计数法-1.01 × 2^2
- S = 1, M = 1.01, E = 2
三、浮点数在内存中的存储格式
IEEE 754为不同精度的浮点数定义了具体的存储布局:
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
四、存储过程的特殊规则
1. 有效数字M的存储优化
由于M的范围总是 [1, 2),其整数部分必定是1。IEEE 754利用这一特性进行优化:
- 存储时:省略整数部分的1,只保存小数部分
- 读取时:自动加上整数部分的1
这样,23位的M实际上可以表示24位的精度。
2. 指数E的偏移表示法
指数E可能是负数,但内存中存储的是无符号整数。为此,IEEE 754引入了偏移量:
- 32位float:偏移量为127,存储值 = 真实值 + 127
- 64位double:偏移量为1023,存储值 = 真实值 + 1023
示例:
- 真实指数 2 → 存储值 2 + 127 = 129 (二进制:10000001)
- 真实指数 -1 → 存储值 -1 + 127 = 126 (二进制:01111110)
五、读取过程的三种情况
情况1:E不全为0且不全为1(正常情况)
- 真实指数 = 存储值 - 偏移量
- 有效数字M = 1.[小数部分]
情况2:E全为0(接近0的非常小数值)
- 真实指数 = 1 - 偏移量(float为-126)
- 有效数字M = 0.[小数部分](不再加1)
- 用于表示±0和接近0的微小数值
情况3:E全为1
- 如果M全为0:表示±无穷大(取决于符号位S)
- 如果M不全为0:表示NaN(非数字)
情况 | 指数E特征 | 真实指数计算 | 有效数字M处理 | 表示的数值范围 |
---|---|---|---|---|
情况1 | 不全为0且不全为1 | 存储值 - 偏移量 | M = 1.[小数部分] | 正常浮点数 |
情况2 | 全为0 | 1 - 偏移量 | M = 0.[小数部分] | ±0和微小数值 |
情况3 | 全为1 | 无意义 | M决定类型 | ±∞和NaN |
六、实例解析:回到开头的例子
1. 为什么整数9变成浮点数就是0.000000?
整数9的二进制:00000000 00000000 00000000 00001001
按浮点数格式拆分:
- S = 0
- E =
00000000
(全0) - M =
000 0000 0000 0000 0000 1001
由于E全为0,属于情况2:
- 真实指数 = 1 - 127 = -126
- 有效数字M = 0.00000000000000000001001
- 最终值 = ±0.00000000000000000001001 × 2^(-126)
这是一个极其接近0的数,用%f打印就是0.000000。
2. 为什么浮点数9.0变成整数就是1091567616?
浮点数9.0的表示:
- 9.0 = 1001.0 = 1.001 × 2^3
- S = 0, M = 1.001, E = 3
内存存储:
- 指数E存储值 = 3 + 127 = 130 (二进制:10000010)
- 有效数字M存储 = 001后面补20个0
- 完整二进制:
0 10000010 00100000000000000000000
这个32位二进制序列被当作整数解读时,就是:
01000001000100000000000000000000
= 1091567616
那么以上就是关于数据在内存中存储方式的全面探讨了。从整数的补码表示,到大小端字节序的奥秘,再到浮点数的IEEE 754标准,我们一同走过了这段探索底层存储机制的旅程。
限于篇幅和作者水平,文中难免有疏漏或阐述不清之处。如果各位在阅读过程中发现任何问题,或是存在疑惑不解的地方,非常欢迎大家提出宝贵意见与指正。你们的每一次反馈,都是我们共同进步的契机。
感谢您的阅读时间,期待在评论区与您继续交流探讨!