深度解析数据内存存储与排布规则

1. 整数在内存中的存储

整数的2进制表⽰⽅法有三种,即原码、反码和补码

有符号的整数,三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰"正",⽤1表

⽰"负",最⾼位的⼀位是被当做符号位,剩余的都是数值位。

正整数的原、反、补码都相同。

负整数的三种表⽰⽅法各不相同。

原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。

为了更直观地对比原码、反码和补码的区别,下面以 -5 为例,列出三种编码的详细对比:

编码类型 定义 计算方法 示例(-5,假设8位有符号整数)
原码 直接将数值按正负形式翻译成二进制,最高位为符号位(0正1负),其余为数值位 符号位写1,数值部分写5的二进制 0000101 1000 0101
反码 原码的符号位不变,其他位按位取反 原码 1000 0101 → 符号位1不变,数值位 0000101 取反得 1111010 1111 1010
补码 反码 + 1 反码 1111 1010 + 1 = 1111 1011 1111 1011

说明:

  • 正整数的原码、反码、补码完全相同,例如 +5 的三种编码都是 0000 0101
  • 负整数的三种编码各不相同,如上表所示,-5 的原码、反码、补码依次为 1000 01011111 10101111 1011
  • 计算机中实际存储的是补码,因为补码可以将减法统一为加法运算,简化硬件设计。

补码:反码+1就得到补码。


2. ⼤⼩端字节序和字节序判断

其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储,下⾯是具体的概念:

⼤端(存储)模式:

是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。

⼩端(存储)模式:

是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。


下面是一个C语言程序,用于判断当前机器的字节序是大端还是小端:

c 复制代码
#include <stdio.h>

// 方法1:通过联合体(union)判断字节序
// 联合体的特点是所有成员共享同一块内存空间
int check_endian_union() {
    union {
        int i;          // 4字节整数
        char c[4];      // 4字节字符数组
    } u;
    
    u.i = 0x12345678;  // 将一个已知值写入整数成员
    
    // 如果小端存储:低地址存放低位字节,即 c[0] = 0x78
    // 如果大端存储:低地址存放高位字节,即 c[0] = 0x12
    if (u.c[0] == 0x78) {
        return 0;  // 小端
    } else if (u.c[0] == 0x12) {
        return 1;  // 大端
    } else {
        return -1; // 未知
    }
}

// 方法2:通过指针强制类型转换判断字节序
int check_endian_pointer() {
    int i = 0x12345678;          // 定义一个整数
    unsigned char* p = (unsigned char*)&i;  // 将int指针转为char指针,指向最低地址
    
    // 读取最低地址处的第一个字节
    // 小端:最低地址存放的是低位字节 0x78
    // 大端:最低地址存放的是高位字节 0x12
    if (*p == 0x78) {
        return 0;  // 小端
    } else if (*p == 0x12) {
        return 1;  // 大端
    } else {
        return -1; // 未知
    }
}

int main() {
    printf("===== 字节序判断程序 =====\n\n");
    
    // 使用联合体方法判断
    int result1 = check_endian_union();
    printf("【方法1:联合体判断】\n");
    printf("测试值:0x12345678\n");
    printf("内存布局(从低地址到高地址):\n");
    
    union {
        int i;
        unsigned char c[4];
    } u;
    u.i = 0x12345678;
    for (int j = 0; j < 4; j++) {
        printf("  地址 +%d:0x%02X\n", j, u.c[j]);
    }
    
    if (result1 == 0) {
        printf("结论:当前机器为 **小端字节序**\n");
        printf("解释:低地址(地址+0)存放的是低位字节 0x78\n");
    } else if (result1 == 1) {
        printf("结论:当前机器为 **大端字节序**\n");
        printf("解释:低地址(地址+0)存放的是高位字节 0x12\n");
    }
    
    printf("\n");
    
    // 使用指针方法判断
    int result2 = check_endian_pointer();
    printf("【方法2:指针强制转换判断】\n");
    int test_val = 0x12345678;
    unsigned char* p = (unsigned char*)&test_val;
    printf("测试值:0x12345678\n");
    printf("最低地址处的字节内容:0x%02X\n", *p);
    
    if (result2 == 0) {
        printf("结论:当前机器为 **小端字节序**\n");
        printf("解释:将int指针转为char指针后,读取最低地址得到 0x%02X,这是低位字节\n", *p);
    } else if (result2 == 1) {
        printf("结论:当前机器为 **大端字节序**\n");
        printf("解释:将int指针转为char指针后,读取最低地址得到 0x%02X,这是高位字节\n", *p);
    }
    
    printf("\n===== 原理说明 =====\n");
    printf("1. 我们定义一个整数 0x12345678,它在内存中占用4个字节\n");
    printf("2. 这4个字节分别是:0x12(最高位)、0x34、0x56、0x78(最低位)\n");
    printf("3. 通过联合体或指针,我们可以访问整数在内存中的第一个字节(最低地址)\n");
    printf("4. 如果第一个字节是 0x78(低位字节),说明是小端模式\n");
    printf("5. 如果第一个字节是 0x12(高位字节),说明是大端模式\n");
    printf("6. 常见的x86/x64架构(如Intel、AMD)都是小端模式\n");
    printf("7. 网络协议通常使用大端模式(网络字节序)\n");
    
    return 0;
}

代码逐步解释:

  1. 联合体方法(check_endian_union

    • 联合体 union 中的所有成员共享同一块内存空间
    • 我们定义了一个包含 intchar[4] 的联合体,两者都占用4个字节
    • u.i 赋值为 0x12345678 后,通过 u.c[0] 读取第一个字节
    • 小端模式下,u.c[0]0x78(低位字节在低地址)
    • 大端模式下,u.c[0]0x12(高位字节在低地址)
  2. 指针方法(check_endian_pointer

    • int* 强制转换为 unsigned char*,这样指针就指向整数的第一个字节
    • 解引用 *p 得到最低地址处的字节内容
    • 根据该字节的值判断字节序
  3. 内存布局输出

    • 程序会打印出整数 0x12345678 在内存中从低地址到高地址的4个字节
    • 小端:78 56 34 12(低地址→高地址)
    • 大端:12 34 56 78(低地址→高地址)
  4. 实际应用

    • 在网络编程中,需要将主机字节序转换为网络字节序(大端),使用 htonl()htons() 等函数
    • 在读写二进制文件或跨平台数据传输时,必须考虑字节序问题

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

常⻅的浮点数:3.14159、1E10等,浮点数家族包括:float、double、long double 类型。

浮点数表⽰的范围:float.h 中定义。

3.1 练习

c 复制代码
int main()
{
	int n = 9;
	//00000000000000000000000000001001
	//
	float* pFloat = (float*)&n;
	printf("n的值为:%d\n", n);
	printf("*pFloat的值为:%f\n", *pFloat);//0.000000
	//0 00000000 00000000000000000001001
	//0.00000000000000000001001 * 2^-126
	//
	*pFloat = 9.0;
	//1001.0
	//(-1)^0 * 1.001 * 2^3
	//S = 0
	//E = 3 +127 = 130
	//M = 1.001
	//0 10000010 00100000000000000000000
	//
	//
	printf("n的值为:%d\n", n);
	//01000001000100000000000000000000
	printf("*pFloat的值为:%f\n", *pFloat);

	//n的值为:9
	//*pFloat的值为:0.000000
	//n的值为:1091567616
	//*pFloat的值为:9.000000
	return 0;
}

3.2 浮点数的存储

例如,数值 9.0 的二进制表示为 0 10000010 00100000000000000000000,其中 S=0(正数),E=130(真实指数 = 130 - 127 = 3),M=001...(有效数字为 1.001,即十进制 1.125),最终值为 (-1)^0 × 1.125 × 2^3 = 9.0

3.2.1 浮点数取的过程

E不全为0或不全为1(常规情况)

这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。

c 复制代码
0 01111110 00000000000000000000000

E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。

c 复制代码
0 00000000 00100000000000000000000

E全为1

这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s);如果M不全为0,表⽰NaN(Not a Number,⾮数值)。

详细解释:

当指数E的所有位都为1时(即E=255,对于float类型),这是一个特殊的标记值,用于表示无穷大或非数值。

  • M全为0 → 无穷大 :当有效数字M的所有位都是0时,这个浮点数表示的是无穷大。符号位s决定正负:s=0表示+∞,s=1表示-∞。例如,0 11111111 00000000000000000000000 表示 +∞,而 1 11111111 00000000000000000000000 表示 -∞。
  • M不全为0 → NaN:当M中至少有一位是1时,这个浮点数表示NaN(Not a Number),用于表示无效的运算结果,比如0/0、∞-∞等。

为什么这样设计?

IEEE 754标准之所以用E全为1来编码无穷大和NaN,是因为在科学计算和工程应用中,经常需要处理溢出、除零等异常情况。通过保留专门的位模式来表示这些特殊值,程序可以在不崩溃的情况下继续执行,并检测到异常。

C语言代码示例与逐步解释:

c 复制代码
#include <stdio.h>
#include <math.h>  // 用于 isinf() 和 isnan() 函数

int main() {
    // 示例1:通过除法得到正无穷大
    double pos_inf = 1.0 / 0.0;
    printf("1.0 / 0.0 = %f\n", pos_inf);           // 输出:inf
    printf("isinf(pos_inf) = %d\n", isinf(pos_inf)); // 输出:1(是无穷大)
    
    // 示例2:通过除法得到负无穷大
    double neg_inf = -1.0 / 0.0;
    printf("-1.0 / 0.0 = %f\n", neg_inf);          // 输出:-inf
    printf("isinf(neg_inf) = %d\n", isinf(neg_inf)); // 输出:1
    
    // 示例3:通过溢出得到无穷大
    double overflow = 1e308 * 10.0;  // double最大值约1.8e308,乘以10会溢出
    printf("1e308 * 10.0 = %f\n", overflow);        // 输出:inf
    printf("isinf(overflow) = %d\n", isinf(overflow)); // 输出:1
    
    // 示例4:0/0 得到 NaN
    double nan_val = 0.0 / 0.0;
    printf("0.0 / 0.0 = %f\n", nan_val);            // 输出:nan
    printf("isnan(nan_val) = %d\n", isnan(nan_val)); // 输出:1(是NaN)
    
    // 示例5:无穷大参与运算
    printf("pos_inf + 100 = %f\n", pos_inf + 100);  // 输出:inf
    printf("pos_inf + neg_inf = %f\n", pos_inf + neg_inf); // 输出:nan(∞-∞无意义)
    
    // 示例6:手动构造一个float类型的正无穷大
    // 二进制:0 11111111 00000000000000000000000
    // 即:符号位0,指数全1,有效数字全0
    unsigned int inf_bits = 0x7F800000;  // 0 11111111 000...0
    float* p_inf = (float*)&inf_bits;
    printf("手动构造的float正无穷大 = %f\n", *p_inf); // 输出:inf
    printf("isinf(*p_inf) = %d\n", isinf(*p_inf));   // 输出:1
    
    return 0;
}

代码逐步解释:

  1. double pos_inf = 1.0 / 0.0; :在C语言中,浮点数除以0不会导致程序崩溃,而是返回无穷大。1.0/0.0的结果是正无穷大,其内部二进制表示为 0 11111111 000...0(double类型指数11位全1,有效数字全0)。

  2. double neg_inf = -1.0 / 0.0;:负数的除法得到负无穷大,符号位为1。

  3. double overflow = 1e308 * 10.0;:当计算结果超出double能表示的最大值(约1.8×10³⁰⁸)时,发生上溢,结果被表示为无穷大。

  4. double nan_val = 0.0 / 0.0;:0/0是一个未定义的数学运算,结果被表示为NaN。NaN的二进制特征是:指数全1,有效数字不全为0。

  5. 无穷大参与运算:无穷大加上任何有限数仍然是无穷大;但正无穷大加负无穷大没有意义,结果为NaN。

  6. 手动构造无穷大 :通过将内存中的位模式 0x7F800000 解释为float,我们直接构造了一个正无穷大。这个32位二进制数正好对应 0 11111111 00000000000000000000000

关键点总结:

  • E全为1且M全为0 → ±∞(无穷大),用于表示溢出或除零的结果
  • E全为1且M不全为0 → NaN(非数值),用于表示无效运算结果
  • 使用 isinf()isnan() 函数可以检测这些特殊值
  • 无穷大和NaN可以参与运算,遵循IEEE 754的特殊规则
相关推荐
weixin_421725267 小时前
C语言、C++与C#深度研究报告:从底层控制到现代企业级开发的演进
c语言·c++·c·内存管理·编译模型
晓梦林7 小时前
kakeru靶场学习笔记
笔记·学习
aloha_7897 小时前
信息系统项目管理师选择题考前真题错题笔记汇总
java·笔记·学习·tomcat
aloha_7897 小时前
信息系统项目管理师真题做题笔记
java·笔记·学习·软件工程·学习方法
小+不通文墨8 小时前
利用树莓派部署的emqx向mqttx发送信息(python)
经验分享·笔记·学习·树莓派·emqx
Hua-Jay8 小时前
OpenCV联合C++/Qt 学习笔记(二十五)----加载深度神经网络模型及深度神经网络模型的使用
c++·笔记·qt·opencv·学习·计算机视觉·dnn
xuhaoyu_cpp_java8 小时前
Git学习(三)
经验分享·笔记·git·学习
爱吃龙利鱼8 小时前
ubuntu2026.04部署k8s1.36版本的傻瓜式教程(注:运行时为docker,网络插件为calico)
运维·网络·笔记·docker·云原生·kubernetes