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

数据在内存中的存储深度解析

在C语言编程中,理解数据在内存中的存储方式是掌握底层原理的关键。无论是整数、浮点数的存储规则,还是字节序的差异,都会直接影响程序的运行结果。本文将从整数存储、大小端字节序、浮点数存储三个维度,结合代码示例深入剖析数据在内存中的存储机制。

1. 整数在内存中的存储

在计算机中,整数的存储并非直接使用二进制原码,而是通过原码、反码、补码三种形式处理,其中补码是实际存储的形式。

1.1 原码、反码与补码的概念

整数的二进制表示有三种形式,均包含符号位数值位两部分:

  • 符号位:最高位为符号位,0表示正数,1表示负数。
  • 数值位:剩余位表示数值的绝对值。

三者的转换规则如下表所示:

整数类型原码反码补码
正整数直接将数值转换为二进制(符号位为0)与原码相同与原码相同
负整数直接将数值转换为二进制(符号位为1)符号位不变,数值位按位取反反码 + 1

示例:以8位整数 -5 为例

  • 原码:10000101(符号位1,数值位0000101)
  • 反码:11111010(符号位不变,数值位取反)
  • 补码:11111011(反码 + 1)

1.2 补码存储的优势

核心结论:整数在内存中实际存储的是补码。原因如下:

  1. 符号位与数值域统一处理:补码中符号位可直接参与运算,无需额外逻辑区分正负。
  2. 加减法统一处理:CPU仅具备加法器,补码可将减法转换为加法(如 a - b = a + (-b) 的补码相加)。
  3. 转换逻辑简单:补码与原码的转换过程一致(补码的补码即为原码),无需额外硬件电路。

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

当数据长度超过1字节时(如intshort),其字节在内存中的排列顺序存在差异,这就是字节序问题。

2.1 大小端的定义与差异

  • 大端(Big-Endian)模式:数据的高位字节存于内存的低地址,低位字节存于高地址。
  • 小端(Little-Endian)模式:数据的低位字节存于内存的低地址,高位字节存于高地址。

示例:对于十六进制数 0x11223344(4字节int)

  • 大端存储:低地址 → 11 22 33 44(高位字节11在低地址)
  • 小端存储:低地址 → 44 33 22 11(低位字节44在低地址)

2.2 大小端存在的原因

计算机以字节为最小存储单位,但处理器寄存器宽度常大于1字节(如16位、32位)。当存储多字节数据时,需规定字节排列顺序,因此产生大小端:

  • 常见架构:X86为小端模式,KEIL C51为大端模式,部分ARM可硬件切换模式。

2.3 字节序判断方法与代码实现

经典问题:判断当前机器的字节序(百度笔试题)

方法1:指针法

#include <stdio.h>int check_sys() 
{int i = 1;  // 二进制:0x00000001(32位)// 将int指针转为char*,仅访问第一个字节return *(char*)&i;  
}int main() 
{if (check_sys() == 1) {printf("小端模式\n");  // 小端:第一个字节为0x01} else {printf("大端模式\n");  // 大端:第一个字节为0x00}return 0;
}

原理i=1的补码为0x00000001,小端模式下低地址存0x01,大端存0x00

方法2:联合体法

#include <stdio.h>int check_sys() 
{union {  // 联合体成员共享内存int i;char c;} un;un.i = 1;return un.c;  // 访问i的第一个字节
}int main() 
{printf("%s\n", check_sys() ? "小端模式" : "大端模式");return 0;
}

原理:联合体unic共享内存,un.i=1后,un.c的值即为i第一个字节的内容。

2.4 经典练习题解析

练习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);  // 输出:-1,-1,255return 0;
}

解析

  • charsigned char为有符号类型,-1的补码为0xff,打印时符号扩展为32位0xffffffff(对应-1)。
  • unsigned char无符号,0xff直接解释为255。
练习2:字符串长度计算
#include <stdio.h>
#include <string.h>
int main() 
{char a[1000];int i;for(i=0; i<1000; i++) {a[i] = -1 - i;  // 依次为-1,-2,...,0,-1...}printf("%d", strlen(a));  // 输出:255return 0;
}

解析
strlen'\0'(值为0)为结束标志。当i=255时,-1-255=-256char类型溢出后补码为0x00(即'\0'),因此长度为255。

练习3:无符号循环陷阱
#include <stdio.h>
unsigned char i = 0;
int main() 
{for(i=0; i<=255; i++) {  // 无限循环printf("hello world\n");}return 0;
}

解析
unsigned char取值范围为0~255,i=255i++溢出为0,永远满足i<=255

3. 浮点数在内存中的存储

浮点数(floatdouble)的存储方式与整数完全不同,遵循IEEE 754标准,通过科学计数法表示。

3.1 浮点数的IEEE 754标准

任意二进制浮点数V可表示为:
(V=(-1)^{S} * M * 2^{E})

  • S:符号位(0为正,1为负)。
  • M:有效数字(1 ≤ M < 2,形如1.xxxxxx)。
  • E:指数位(整数,可正可负)。

示例:十进制5.0 → 二进制101.0 → 科学计数法1.01×2²,因此S=0,M=1.01,E=2。

3.2 浮点数的存储结构

IEEE 754规定了floatdouble的存储分配:

类型符号位(S)指数位(E)有效数字(M)
float(32位)1位8位23位
double(64位)1位11位52位

3.3 浮点数的存储与读取过程

存储过程:
  1. 有效数字M:省略首位1,仅存小数部分(节省空间)。例如M=1.01存为01
  2. 指数E:E为无符号整数,存储时需加中间值(float加127,double加1023)。例如E=3(float)存为3+127=130(二进制10000010)。
读取过程:
  • E不全为0且不全为1:真实指数 = E存储值 - 中间值,M补回首位1
  • E全为0:真实指数 = 1 - 中间值,M为0.xxxxxx(表示接近0的数)。
  • E全为1:M全0表示±无穷大,M非0表示NaN(非数值)。

3.4 浮点数存储经典练习题解析

#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;
}

解析

  1. int 9 被解读为float

    • n=9的补码:00000000 00000000 00000000 00001001
    • 按float拆分:S=0,E=00000000(全0),M=000…0001001。
    • 计算:( V=1.001×2^{-146} )(接近0,故打印0.000000)。
  2. float 9.0 被解读为int

    • 9.0 → 二进制1001.0 → 科学计数法1.001×2³
    • 存储:S=0,E=3+127=130(10000010),M=001…000(23位)。
    • 二进制序列:0 10000010 00100000000000000000000 → 对应整数1091567616。

总结

数据在内存中的存储是C语言底层原理的核心,本文通过整数补码、大小端字节序、浮点数IEEE 754标准三大模块,结合代码示例深入解析了存储规则。掌握这些知识能帮助你规避符号位溢出、字节序错误等陷阱,为底层开发打下基础。

关键要点

  • 整数存补码,浮点数存IEEE 754格式,二者解读方式完全不同。
  • 大小端需通过代码判断,避免跨平台数据传输错误。
  • 无符号类型的溢出和符号扩展是常见易错点。
http://www.lryc.cn/news/609783.html

相关文章:

  • 【基础完全搜索】USACO Bronze 2019 January - 猜动物Guess the Animal
  • [找出字符串中第一个匹配项的下标]
  • OCR 精准识别验讫章:让登记与校验更智能
  • 嵌入式 - 数据结构:查找至双向链表
  • 用户管理——配置文件和命令
  • 【数据库】使用Sql Server创建索引优化查询速度,一般2万多数据后,通过非索引时间字段排序查询出现超时情况
  • Linux-Shell脚本基础用法
  • 【VSCode】 使用 SFTP 插件实现多服务器同步
  • 随机森林知识点整理:从原理到实战
  • 区块链基础之Merkle树
  • 数据结构——单向链表
  • CMakeLists.txt学习
  • 《JavaScript高级程序设计》读书笔记 35 - 代理捕获器、反射方法以及代理模式
  • React 19 + Next.js 15 中实现混合布局
  • React配置proxy跨域
  • ref和reactive的区别
  • 通过 Flink 和 CDC 从 Oracle 数据库获取增量数据,并将这些增量数据同步到 MySQL 数据库中
  • [GESP202306 四级] 2023年6月GESP C++四级上机题超详细题解,附带讲解视频!
  • Spring Boot + ShardingSphere 实现分库分表 + 读写分离实战
  • AWS VPC Transit Gateway 可观测最佳实践
  • 【物联网】基于树莓派的物联网开发【23】——树莓派安装SQLite嵌入式数据库
  • 16_OpenCV_漫水填充(floodFill)
  • Nginx vs Spring Cloud Gateway:限流功能深度对比与实践指南
  • Spring Cloud Gateway 实现登录校验:构建统一认证入口
  • 图片的放大缩小选择全屏
  • XSS的原型链污染1--原型链解释
  • 笔记本电脑联想T14重启后无法识别外置红米屏幕
  • Django + Vue 项目部署(1panel + openresty)
  • AI“炼金术”:破解绿色水泥的配方密码
  • 三防平板电脑是什么?这款三防平板支持红外测温!