第22篇 数据的存储

一、底层基础理论:数据在内存中的存储形式

1.1 核心概念标准定义
1.1.1 基础概念梳理

在计算机系统中,数据最终都以二进制形式存储。对于有符号整数,其表示方法主要有三种:原码、反码和补码。

  • 原码:直接将数值的正负符号和绝对值翻译成二进制。最高位为符号位(0表示正,1表示负),其余位为数值位。
  • 反码:在原码的基础上,符号位保持不变,其余数值位按位取反(0变1,1变0)。
  • 补码:在反码的基础上加1得到。

对于正整数及无符号整数,其原码、反码、补码的表示形式完全相同。对于负整数,三种表示方法各不相同。

关键结论 :在计算机系统中,有符号整数一律使用补码来表示和存储。

1.2 底层运行约束规则
1.2.1 补码存储的硬件逻辑推导

采用补码存储的根本原因在于简化硬件电路设计,提升运算效率。

  1. 统一符号位与数值位的处理:使用补码,CPU可以将符号位和数值位一同参与运算,无需为符号位设计单独的处理逻辑。
  2. 统一加法与减法运算 :计算机的算术逻辑单元(ALU)中通常只设计了加法器。通过补码,减法运算 A - B 可以被转换为加法运算 A + (-B) 的补码形式。这样,无论是加法还是减法,都可以通过同一个加法器来完成,极大地简化了硬件电路的复杂度。
  3. 转换过程一致:补码与原码之间的相互转换,其运算过程是相同的(即"按位取反,末位加一"),无需额外的硬件电路支持。
1.3 硬件配套通识科普

从电子信息专业的角度看,内存是由无数个存储单元(如触发器)构成的。补码的设计巧妙地利用了二进制溢出的特性。例如,在一个8位系统中,计算 1 + (-1)

  • 1 的补码是 0000 0001
  • -1 的补码是 1111 1111
  • 两者相加得到 1 0000 0000。由于只有8位存储空间,最高位的 1 会自然溢出丢失,结果变为 0000 0000,恰好是 0 的补码。

这个过程完美地用加法实现了减法,体现了硬件设计中"用简单规则实现复杂功能"的哲学。

二、基础语法规范:大小端字节序

2.1 核心运算符语法定义
2.1.1 语法规则推导

当一个数据占用多个字节时(如 int 类型通常占4字节),这些字节在内存中的存储顺序就成为一个问题。这引出了大端和小端两种字节序。

  • 大端字节序 (Big-Endian) :数据的高位字节 内容,保存在内存的低地址 处;数据的低位字节 内容,保存在内存的高地址处。这类似于我们日常书写数字的习惯(高位在左)。
  • 小端字节序 (Little-Endian) :数据的低位字节 内容,保存在内存的低地址 处;数据的高位字节 内容,保存在内存的高地址处。

核心规则 :大小端描述的是多字节数据在内存中的字节排列顺序

2.2 变量定义与存储逻辑
2.2.1 内存存储逻辑

为什么会有大小端之分?根本原因在于计算机以字节(8 bit)为最小寻址单位,但CPU的寄存器宽度通常大于一个字节(如16位、32位、64位)。当一个多字节数据需要存入内存时,就必须规定其字节的排列顺序。

  • 常见架构 :我们常用的 x86/x64 架构的PC机通常采用小端模式。而一些网络协议和特定的处理器(如早期的PowerPC)则采用大端模式。
2.3 验证代码

以下代码演示了如何判断当前系统的字节序。其核心思想是定义一个多字节整数,然后通过字符指针(每次访问1字节)来检查其最低有效字节在内存中的位置。

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

// 判断当前机器字节序的函数
int check_system_endian(void)
{
    int test_num = 1; // 0x00000001
    // 取test_num的地址,并强制转换为char*类型
    // char* 每次只访问一个字节
    char *byte_ptr = (char *)&test_num;
    
    // 如果最低地址处的字节值为1,说明低位字节存在低地址,为小端
    if (*byte_ptr == 1)
    {
        return 1; // 小端
    }
    else
    {
        return 0; // 大端
    }
}

int main(void)
{
    int ret = check_system_endian();
    if (ret == 1)
    {
        printf("当前系统为小端字节序\n");
    }
    else
    {
        printf("当前系统为大端字节序\n");
    }
    return 0;
}

代码分析

  1. 定义 int test_num = 1,其十六进制表示为 0x00000001。其中 01 是低位字节,00 是高位字节。
  2. 通过 (char *)&test_num 获取其首字节的地址。
  3. 在小端系统中,低位字节 01 存放在低地址,因此 *byte_ptr 的值为 1
  4. 在大端系统中,高位字节 00 存放在低地址,因此 *byte_ptr 的值为 0

三、易混淆概念底层区分:整数与浮点数的存储差异

3.1 两类语法行为差异推导
3.1.1 内存行为对比分析

整数和浮点数在内存中的解读方式完全不同,这导致了即使内存中的二进制位完全相同,用不同类型的指针去访问,也会得到截然不同的结果。

  • 整数存储:直接存储其补码形式。
  • 浮点数存储 :遵循 IEEE 754 标准,将一个二进制浮点数 V 表示为 (-1)^S * M * 2^E 的形式。
    • S (Sign):符号位,占1位。0为正,1为负。
    • M (Mantissa) :有效数字,占23位(float)或52位(double)。规定 1 ≤ M < 2,因此存储时省略整数部分的 1,只存小数部分,以节省一位精度。
    • E (Exponent) :指数位,占8位(float)或11位(double)。E是一个无符号整数,存储时需要加上一个中间数(float为127,double为1023)。

核心差异:整数的每一位都代表一个权重(2的幂),而浮点数的32位被分割成S、E、M三个部分,各自有独立的解读规则。

3.2 对照验证代码

下面的代码清晰地展示了同一块内存,用 intfloat 两种方式解读的巨大差异。

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

int main(void)
{
    int int_data = 9;
    // 将int数据的地址强制转换为float指针
    float *float_ptr = (float *)&int_data;

    printf("以整数形式解读: %d\n", int_data);
    printf("以浮点数形式解读: %f\n", *float_ptr);

    // 反向操作
    *float_ptr = 9.0;
    printf("写入浮点数9.0后,以整数形式解读: %d\n", int_data);
    printf("写入浮点数9.0后,以浮点数形式解读: %f\n", *float_ptr);

    return 0;
}

代码分析

  1. int_data = 9 的补码为 00000000 00000000 00000000 00001001
  2. 当用 float 指针解读时,CPU会按照 IEEE 754 规则解析这32位:
    • S = 0
    • E = 00000000 (全为0,表示一个非常接近0的数)
    • M = 00000000000000000001001
    • 根据规则,这表示一个极小的正数,打印出来就是 0.000000
  3. *float_ptr = 9.0 时,9.0 被转换为 IEEE 754 格式存入内存:
    • 9.0 的二进制为 1001.0,即 1.001 * 2^3
    • S = 0
    • E = 3 + 127 = 130,二进制为 10000010
    • M = 001 (后面补0至23位)
    • 最终内存中的32位为 0 10000010 00100000000000000000000
  4. 当再用 int 指针解读这块内存时,它被当作一个普通的补码整数,其值为 1091567616

四、全章节逻辑闭环总结

  1. 数据存储基础 :有符号整数在内存中以补码形式存储,此举统一了加减法运算,简化了CPU硬件设计。
  2. 多字节数据存储 :超过一个字节的数据在内存中存在大端小端两种字节序,区别在于高位字节和低位字节在内存地址中的排列顺序。x86架构通常为小端。
  3. 数据解读差异 :同一块内存,用整型指针和浮点型指针访问会得到不同结果,因为整数 按补码解读,而浮点数遵循 IEEE 754 标准(S-E-M结构)解读。