当前位置: 首页 > news >正文

深入理解数据在内存中的存储:整数与浮点数的二进制表示

在这里插入图片描述

✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松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

核心规则:

  • 正整数的原反补码都相同。
  • 负整数的原码、反码、补码需要按照上述规则转换。
  • 所有整型数据在内存中存放的都是补码。
二、为什么内存中存储的是补码?

使用补码进行存储并非偶然,而是一项非常厉害的设计,主要基于以下三大原因(了解即可):

  1. 符号统一
    使用补码,可以将符号位和数值位同等对待,一同参与运算,无需额外区分正负。

  2. 运算统一
    计算机的CPU中只有加法器,没有减法器。通过补码,减法可以转换为加法运算。

    例如:计算 5 - 3,相当于 5 + (-3)。用补码计算:00000101 + 11111101 = 00000010(进位溢出的部分舍弃),结果正是 2

  3. 零值统一
    在原码和反码中,+0-0 的表示不同(原码:0000000010000000),这会导致两个不同的编码对应同一个数值零。而在补码中,0 的表示是唯一的(00000000),用 10000000 来表示 -128(对于8位有符号char),从而扩展了可表示的负数范围。

2. 大小端字节序和字节序判断

在理解了整数以补码形式存储后,我们来看一个细节:

当一个数据超过一个字节时,它的各个字节在内存中是如何排列的?这就是字节序问题,它决定了数据在内存中的顺序。

一、什么是大小端字节序?

字节序分为两种主要模式:大端字节序小端字节序

  • 大端字节序:数据的高位字节存储在低地址处,低位字节存储在高地址处。
  • 小端字节序:数据的低位字节存储在低地址处,高位字节存储在高地址处。

假设一个32位整数 0x11223344(其中 0x11 是最高位字节,0x44 是最低位字节)。

内存地址大端模式存储内容小端模式存储内容
低地址0x110x44
0x220x33
0x330x22
高地址0x440x11

从调试器的内存窗口看,小端模式下的 0x11223344 会显示为 44 33 22 11

在这里插入图片描述

二、为什么会有大小端之分?

字节序的存在源于计算机系统设计的两大现实:

  1. 以字节为寻址单位:内存每个地址对应一个字节(8 bit)。
  2. 多字节数据的处理:对于 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;
}

解析:

  1. 变量 ab(有符号char)
    • -1 的8位补码是 11111111
    • 当用 %d 打印时,发生整型提升:由于是有符号类型,高位用符号位填充
    • 11111111 → 提升为 11111111 11111111 11111111 11111111(32位,仍是-1的补码)
    • 所以输出 -1
  2. 变量 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:

  1. 赋值阶段:对于8位有符号char,-128 的补码是 10000000
  2. 整型提升阶段:用 %u 打印时,char 先被提升为 int
    • 由于是有符号类型且符号位为1,高位全部补1
    • 1000000011111111 11111111 11111111 10000000
  3. 解释阶段:这个32位二进制序列被 %u 当作无符号整数解释
    • 11111111 11111111 11111111 10000000 = 2³² - 128 = 4294967168

解析代码片段2:

  1. 赋值阶段:8位有符号char的范围是 -128 到 127
    • 128 超过了最大值127,发生溢出
    • 128 的二进制是 10000000,这正好是 -128 的补码
    • 所以实际上 a 存储的是 -128
  2. 后续阶段:与代码片段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;
}

解析:

  1. 理解赋值过程 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
  2. 关键:char类型的溢出行为
    • 有符号char的范围是 -128 到 127
    • i=128 时:-1 - 128 = -129,超过了char的最小值
    • 发生溢出:-129 会"回绕"到 127
      • -129127 (因为 -129 = -128 - 1 → 127)
    • 继续:
      • i=129a[129] = -130126
      • i=130a[130] = -131125
      • 直到 i=255a[255] = -2560
  3. 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;
}

解析:

第一部分:理解 ptr1ptr1[-1]

  1. &a 的类型&a 是整个数组的地址,类型是 int (*)[4](指向长度为4的int数组的指针)
  2. &a + 1:指针算术运算,加上的是整个数组的大小
    • 数组 a 有4个int元素,每个int占4字节,总大小为 16 字节
    • &a + 1 指向数组末尾之后的位置
  3. ptr1[-1]:等价于 *(ptr1 - 1)
    • ptr1 - 1 向后移动一个int(4字节),指向 a[3]
    • ptr1[-1] 的值就是 a[3],即 4

第二部分:理解 ptr2*ptr2

  1. (int)a:将数组首地址转换为整数(失去指针类型信息)
  2. (int)a + 1:地址值加1(不是加一个int,而是加1个字节)
  3. 转换为 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等,浮点数家族包括: floatdoublelong 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全为01 - 偏移量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标准,我们一同走过了这段探索底层存储机制的旅程。

限于篇幅和作者水平,文中难免有疏漏或阐述不清之处。如果各位在阅读过程中发现任何问题,或是存在疑惑不解的地方,非常欢迎大家提出宝贵意见与指正。你们的每一次反馈,都是我们共同进步的契机。

感谢您的阅读时间,期待在评论区与您继续交流探讨!

http://www.dtcms.com/a/470496.html

相关文章:

  • 广东网站营销seo费用品牌推广活动有哪些
  • 特效音网站建设公司官网制作平台
  • MySQL数据库安装后,如何设置自动化备份策略?
  • 【开题答辩全过程】以 保险业务信息管理系统为例,包含答辩的问题和答案
  • 进口食品销售销售在那个网站做seo托管
  • 公司模板网站建设成绩查询系统网站开发
  • 建设大淘客网站雅布设计师
  • Oracle ADRCI工具全面使用指南:从基础到故障诊断实战
  • 美食网站设计欣赏上海著名网站建设
  • 【智能系统项目开发与学习记录】bringup功能包详解
  • 外贸网建站建公司网站的详细步骤
  • 美食网站建设书成都seo技术
  • 江河建设集团有限公司网站梧州网站建设流程
  • 在Qt中使用VTK
  • 正安北郊湖吉他文化广场自动化监测
  • 【论文阅读】DSPy-based neural-symbolic pipeline to enhance spatial reasoning in LLMs
  • cn域名后缀网站163企业邮箱格式
  • psql常用命令
  • 高速公路自动车道保持系统原理与实现
  • 番禺做网站最便宜的哪家公司wordpress注册界面
  • 【推荐100个unity插件】将您的场景渲染为美丽的冬季风景——Global Snow 2
  • Windows安装Elasticsearch保姆级教程
  • 温州网站链接怎么做在山东省建设监理协会网站
  • C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数
  • 32.渗透-.Kali Linux-工具-netcat的说明
  • Large Kernel Modulation Network for Efficient Image Super-Resolution 学习笔记
  • 城乡住房建设部网站杭州市萧山区哪家做网站的公司好
  • 高精度逆向工程:XTOM蓝光扫描仪赋能自由曲面微尺寸共性电路的增材制造
  • 多区域主动-主动(PostgreSQL 逻辑复制 + 冲突解决)在 ABP 的落地
  • 东莞精密机械制造工厂如何10个SolidWorks共用一台服务器资源