C语言笔记归纳17:数据的存储

数据的存储

目录

数据的存储

1. 🔢 整数存储:原码、反码、补码的 “加法逻辑”

1.1 三大编码的核心逻辑

1.2 实战转换:以 int a = -5 为例

1.3 补码的核心优势:减法变加法

2. 🔀 大小端字节序:字节的 “排列密码”

2.1 大小端的核心定义

2.2 实战:判断机器字节序

2.3 补充:char 类型的取值范围

3. 📐 浮点数存储:IEEE754 标准与精度丢失的真相

3.1 IEEE754 标准的核心公式

3.2 存储结构(32 位 float/64 位 double)

3.3 关键优化:M 和 E 的存储技巧

3.4 精度丢失的本质

4. 🚨 6 个经典实战练习

练习 1:char/unsigned char 的取值范围陷阱

超详拆解:

结论:符号位的存在和整型提升,导致相同二进制值解读出不同结果。

练习 2:char a=-128 的无符号打印

超详拆解:

结论:% u 会将有符号整数的补码,按无符号数解读,导致结果远超预期。

练习 3:char 数组的 strlen 陷阱

超详拆解:

结论:signed char 的溢出循环,导致数组中第一个 \0 出现在索引 255,strlen 结果为 255。

练习 4:无符号整数的循环陷阱

超详拆解:

结论:无符号整数溢出后会循环,避免用 “<= 最大值” 作为循环条件。

练习 5:大小端 + 指针类型转换

超详拆解:

结论:指针类型决定步长,强制类型转换会改变地址的解读方式,结合大小端会产生意想不到的结果。

练习 6:整数与浮点数的内存视角转换

超详拆解:

结论:相同的二进制数据,按不同类型解读(int/float),结果完全不同,因为存储规则不同。

5. 📝 核心知识点总结(避坑指南 + 快速回顾)

🎯 最后总结


✨引言:

作为 C 语言学习者,你是否曾被这些 “诡异现象” 困扰?

  • 同样是 a = -1char 类型打印是 -1unsigned char 却打印 255
  • 明明写的是 int n = 0x11223344,调试时内存却显示 44 33 22 11
  • 浮点数 0.1 + 0.2 结果不是 0.3,而是 0.30000000447

这些问题的答案,都藏在内存存储的底层规则里。今天这篇博客,会用 “原理 + 实战” 的模式,把整数的原反补码、大小端字节序、浮点数 IEEE754 标准讲透,每个练习的讲解都很详细,让你不仅知其然,更知其所以然!

1. 🔢 整数存储:原码、反码、补码的 “加法逻辑”

整数在内存中存储的不是 “直接二进制”,而是补码—— 这是 CPU 硬件设计的必然选择:CPU 只有加法器,无法直接处理减法,补码能让 “减法” 转化为 “加法”,同时统一符号位和数值位的运算规则。

1.1 三大编码的核心逻辑

所有有符号整数的二进制都包含两部分:

  • 符号位:最高位,0 表示正数,1 表示负数;
  • 数值位:剩余位,存储数值的二进制信息。

三种编码的转换规则(以 32 位 int 为例):

编码类型正数转换逻辑负数转换逻辑
原码符号位设为 0,数值位直接翻译十进制符号位设为 1,数值位直接翻译十进制
反码与原码完全相同符号位不变,数值位按位取反(0→1,1→0)
补码与原码、反码完全相同反码基础上 + 1(逢二进一)

1.2 实战转换:以 int a = -5 为例

  1. 求原码:符号位 = 1(负),数值位 = 5 的二进制 00000101 →原码:10000000 00000000 00000000 00000101
  2. 求反码:符号位保持 1,数值位取反 →反码:11111111 11111111 11111111 11111010
  3. 求补码:反码 + 1,注意逢二进一 →补码:11111111 11111111 11111111 11111011(内存中实际存储的最终值)。

1.3 补码的核心优势:减法变加法

为什么必须用补码?看 1 + (-1) = 0 的计算过程:

  • 用原码计算:00000001(1的原码) + 10000001(-1的原码) = 10000010 → 结果为 -2(错误);
  • 用补码计算:00000001(1的补码) + 11111111(-1的补码) = 100000000 → 舍弃溢出的最高位(32 位系统只保留 32 位),结果为 0(正确)。

这就是补码的核心价值:让 CPU 无需额外硬件,就能统一处理正负数的加减运算。

2. 🔀 大小端字节序:字节的 “排列密码”

当数据占超过 1 字节(如 intlong)时,字节在内存中的 “排列顺序” 会影响数据读取 —— 这就是大小端字节序,是跨平台开发、数据传输的高频考点。

2.1 大小端的核心定义

把数据想象成 “一串数字钥匙”,内存地址是 “钥匙孔”,每个字节是 “一节钥匙”:

  • 大端字节序:“高位字节”(钥匙的前半部分)插在 “低地址钥匙孔”,“低位字节”(钥匙的后半部分)插在 “高地址钥匙孔”;例:int n = 0x11223344 → 内存排列:11 22 33 44(低地址→高地址);
  • 小端字节序:“低位字节”(钥匙的后半部分)插在 “低地址钥匙孔”,“高位字节”(钥匙的前半部分)插在 “高地址钥匙孔”;例:int n = 0x11223344 → 内存排列:44 33 22 11(低地址→高地址);
  • 常用场景:x86 架构(PC、多数嵌入式开发板)为小端,C51 单片机、网络传输(TCP/IP)为大端。

2.2 实战:判断机器字节序

核心思路:利用 int(4 字节)和 char(1 字节)的类型转换,仅读取低地址的 1 字节数据,就能判断大小端。

#include <stdio.h>

int check_sys() {
    int n = 1; // 32位补码:00000000 00000000 00000000 00000001
    // 关键逻辑:char* 指针只能读取1字节,即低地址的那个字节
    // 小端环境:低地址存01 → 返回1
    // 大端环境:低地址存00 → 返回0
    return *(char*)&n; 
}

int main() {
    int ret = check_sys();
    if (ret == 1) {
        printf("小端字节序\n");
    } else {
        printf("大端字节序\n");
    }
    return 0;
}

2.3 补充:char 类型的取值范围

char 占 1 字节(8 位),分为 signed char 和 unsigned char

  • signed char(默认):符号位 + 7 位数值位;最大值:01111111(符号位 0,数值位全 1)→ 127;最小值:10000000(特殊规定,无对应原 / 反码)→ -128;取值范围:-128 ~ 127
  • unsigned char:无符号位,8 位全为数值位;最大值:11111111 → 255;最小值:00000000 → 0;取值范围:0 ~ 255

3. 📐 浮点数存储:IEEE754 标准与精度丢失的真相

浮点数(floatdouble)的存储规则和整数完全不同,遵循 IEEE754 国际标准—— 这也是浮点数 “精度丢失” 的根源。

3.1 IEEE754 标准的核心公式

任意二进制浮点数 V 可表示为:

V = (-1)^S × M × 2^E
  • (-1)^S:符号位,S=0 为正,S=1 为负;
  • M:有效数字,范围 1 ≤ M < 2(格式为 1.xxxxxxxxxxxx 是小数部分);
  • 2^E:指数位,控制数值的大小。

3.2 存储结构(32 位 float/64 位 double)

类型符号位 S(1 位)指数位 E(8 位 / 11 位)有效数字 M(23 位 / 52 位)
float第 31 位第 30~23 位第 22~0 位
double第 63 位第 62~52 位第 51~0 位

3.3 关键优化:M 和 E 的存储技巧

  1. 有效数字 M 的优化:因为 1 ≤ M < 2,M 的整数部分永远是 1,存储时可省略,仅存小数部分(xxxxxx)—— 这样能节省 1 位空间,提升精度。例:M=1.011 → 存储时仅存 011,读取时自动补回 1.
  2. 指数位 E 的优化:E 是无符号整数,但实际指数可正可负(如 0.5=1.0×2^(-1))。IEEE754 规定:                                                                                                                                E 的存储值 = 真实值 + 中间值(float 中间值 = 127,double 中间值 = 1023)                        例:E 真实值 = 2(float)→ 存储值 = 2+127=129(二进制 10000001)。

3.4 精度丢失的本质

为什么 0.1 无法精准存储?

  1. 十进制 0.1 转二进制:0.0001100110011...(无限循环);
  2. float 的有效数字 M 只有 23 位,无法存下完整的循环部分,只能截断;
  3. 截断后的 M 值对应的十进制,无限接近 0.1 但不等于 0.1 → 精度丢失。

✅ 避坑指南:浮点数比较不能用 ==,需判断两者差值是否小于允许的精度(如 fabs(f1-f2) < 1e-6)。

4. 🚨 6 个经典实战练习

练习 1:char/unsigned char 的取值范围陷阱

#include <stdio.h>

int main() {
    char a = -1;
    signed char b = -1;
    unsigned char c = -1;
    
    printf("a=%d, b=%d, c=%d\n", a, b, c); // 输出:-1 -1 255
    return 0;
}
超详拆解:
  1. 变量 a(char = signed char)

    • -1 的补码:11111111(8 位);
    • 用 %d 打印时,发生整型提升(char→int):signed char 提升时,高位补符号位(1)→ 32 位补码:11111111 11111111 11111111 11111111
    • 该补码对应的原码:10000000 00000000 00000000 00000001 → 十进制 -1
  2. 变量 b(signed char)

    • 逻辑和 a 完全相同,最终打印 -1
  3. 变量 c(unsigned char)

    • -1 的补码:11111111(8 位);
    • unsigned char 无符号位,11111111 直接解读为数值位;
    • 整型提升时,高位补 0 → 32 位值:00000000 00000000 00000000 11111111 → 十进制 255
结论:符号位的存在和整型提升,导致相同二进制值解读出不同结果。

练习 2:char a=-128 的无符号打印

#include <stdio.h>

int main() {
    char a = -128;
    printf("%u\n", a); // 输出:4294967168
    return 0;
}
超详拆解:
  1. 求 - 128 的补码(signed char):

    • 原码:10000000(符号位 1,数值位 0000000,特殊规定);
    • 反码:11111111(符号位 1,数值位取反);
    • 补码:10000000(反码 + 1 溢出,最终为 8 位 10000000);
  2. 整型提升

    • signed char 提升为 int,高位补符号位(1)→ 32 位补码:11111111 11111111 11111111 10000000
  3. % u 打印(无符号整数)

    • 无符号整数不识别符号位,直接将 32 位二进制解读为数值;
    • 32 位二进制 11111111 11111111 11111111 10000000 是 4294967295
结论:% u 会将有符号整数的补码,按无符号数解读,导致结果远超预期。

练习 3:char 数组的 strlen 陷阱

#include <stdio.h>
#include <string.h>

int main() {
    char a[1000];
    int i;
    for (i = 0; i < 1000; i++) {
        a[i] = -1 - i;
    }
    printf("%zd\n", strlen(a)); // 输出:255
    return 0;
}
超详拆解:
  1. 循环赋值逻辑

    • i=0 → a[0] = -1;i=1 → a[1] = -2;...;i=127 → a[127] = -128;
    • i=128 → a [128] = -129 → signed char 溢出:-128 -1 = 127(因为 signed char 取值范围 - 128~127,溢出后循环);
    • i=129 → a[129] = 126;...;i=255 → a[255] = 0;
    • i>255 → a [i] 继续循环赋值,但 strlen 只关心第一个 \0(ASCII=0);
  2. strlen 的核心逻辑

    • strlen 统计 \0 之前的字符数,遇到 \0 停止;
    • 从 a [0] 到 a [254],共 255 个非 0 字符,a [255] = 0 → 统计停止;
结论:signed char 的溢出循环,导致数组中第一个 \0 出现在索引 255,strlen 结果为 255。

练习 4:无符号整数的循环陷阱

#include <stdio.h>

unsigned char i = 0;
int main() {
    for (i = 0; i <= 255; i++) {
        printf("hello world\n"); // 死循环
    }
    return 0;
}
超详拆解:
  1. unsigned char 的取值范围0 ~ 255
  2. 循环条件分析
    • i=255 时,满足 i <= 255,执行循环体;
    • 执行 i++ → 255+1=256,unsigned char 溢出,循环回 0
    • 此时 i=0,再次满足 i <= 255,循环永不停止;
结论:无符号整数溢出后会循环,避免用 “<= 最大值” 作为循环条件。

练习 5:大小端 + 指针类型转换

#include <stdio.h>

int main() {
    int a[4] = {1,2,3,4};
    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);
    
    printf("%#x,%#x\n", ptr1[-1], *ptr2); // 输出:0x4, 0x2000000
    return 0;
}
超详拆解:
  1. 数组 a 的内存布局(小端环境)

    • a [0] = 1 → 补码 00000001 00000000 00000000 00000000 → 内存:01 00 00 00
    • a [1] = 2 → 补码 00000010 00000000 00000000 00000000 → 内存:02 00 00 00
    • a [2] = 3 → 内存:03 00 00 00
    • a [3] = 4 → 内存:04 00 00 00
    • 完整内存:01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00(低地址→高地址);
  2. ptr1 的逻辑

    • &a 是数组指针,类型为 int(*)[4]&a + 1 跳过整个数组(4×4=16 字节);
    • ptr1 = (int*)(&a + 1) → 指向数组后面的地址;
    • ptr1 [-1] = *(ptr1 - 1) → 指针后退 4 字节,指向 a [3](值为 4)→ 输出 0x4
  3. ptr2 的逻辑

    • a 是数组首地址(低地址),(int) a 把地址转为整数,+1 后还是整数;
    • (int*)((int) a + 1) → 指向数组首地址 + 1 的位置(即 00 这个字节);
    • *ptr2 读取 4 字节:00 00 00 02(低地址→高地址);
    • 小端环境下,4 字节 00 00 00 02 解读为 0x02000000 → 输出 0x2000000
结论:指针类型决定步长,强制类型转换会改变地址的解读方式,结合大小端会产生意想不到的结果。

练习 6:整数与浮点数的内存视角转换

#include <stdio.h>

int main() {
    int n = 9;
    float* pFloat = (float*)&n;
    
    printf("n=%d, *pFloat=%f\n", n, *pFloat); // 输出:9, 0.000000
    *pFloat = 9.0;
    printf("n=%d, *pFloat=%f\n", n, *pFloat); // 输出:1091567616, 9.000000
    return 0;
}
超详拆解:
  1. 第一次打印:int n=9 按 float 解读

    • n=9 的补码:00000000 00000000 00000000 00001001
    • pFloat 是 float*,按 IEEE754 标准解读该二进制:
      • S=0(符号位);
      • E=00000000(8 位指数位)→ 真实 E=0-127+1= -126(E 全 0 时的特殊规则);
      • M=00000000000000000001001(23 位有效数字)→ 真实 M=0.00000000000000000001001;
      • V=(-1)^0 × 0.00000000000000000001001 × 2^(-126) → 数值极小,打印为 0.000000
  2. 第二次打印:float 9.0 按 int 解读

    • float 9.0 的 IEEE754 存储:
      • 9.0 → 二进制 1001.0 → 标准化:1.001 × 2^3
      • S=0,M=001(省略整数 1),E=3+127=130(二进制 10000010);
      • 完整二进制:01000001000100000000000000000000
    • 按 int 解读该二进制:直接转十进制 → 1091567616
结论:相同的二进制数据,按不同类型解读(int/float),结果完全不同,因为存储规则不同。

5. 📝 核心知识点总结(避坑指南 + 快速回顾)

  1. 整数存储:内存存补码,正整数三码合一,负整数补码 = 反码 + 1;
  2. 大小端:小端是主流,大端用于 C51 / 网络传输,多字节数据需注意顺序;
  3. 浮点数:遵循 IEEE754 标准,M 省略整数 1,E 加偏移,精度丢失不可避免;
  4. 字符类型:signed char(-128~127),unsigned char(0~255),溢出会循环;
  5. 无符号陷阱:无符号整数永远≥0,溢出循环,循环条件需谨慎;
  6. 指针转换:指针类型决定步长,强制转换可能改变地址解读方式,结合大小端需格外注意;
  7. 浮点数比较:不用==,用fabs(f1-f2) < 精度(如 1e-6)。

🎯 最后总结

内存存储是 C 语言的 “底层地基”,这些规则看似抽象,但只要结合 “逐行拆解 + 实战推导”,就能彻底掌握。理解这些知识点,不仅能解决很多 “诡异 bug”,还能在面试中脱颖而出 —— 毕竟,面试官最看重的就是底层逻辑能力。如果这篇博客帮你打通了内存存储的 “任督二脉”,欢迎点赞收藏🌟~ 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值