C语言学习:数据在内存中的存储

1.整数在内存中的存储

一、核心概念速记

概念 定义 特点
原码 数值直接翻译成二进制,最高位是符号位(0 正 1 负) 直观,但加减运算复杂
反码 正数:和原码相同负数:符号位不变,其余位取反 是从原码到补码的过渡
补码 正数:和原码相同负数:反码 + 1 计算机实际存储的形式,可统一处理加减

二、完整例子(以 8 位二进制为例)

1. 正数:+5

  • 原码:0000 0101
  • 反码:0000 0101(和原码相同)
  • 补码:0000 0101(和原码相同)

2. 负数:-5

  • 原码:1000 0101(最高位 1 表示负,数值位是 5 的二进制)
  • 反码:1111 1010(符号位不变,其余位取反)
  • 补码:1111 1011(反码 + 1)

3. 用补码计算 5 - 3(即 5 + (-3)

  • 5 的补码:0000 0101
  • -3 的补码:1111 1101
  • 相加:0000 0101 + 1111 1101 = 1 0000 0010
  • 最高位的进位丢弃,结果是 0000 0010,也就是 +2,结果正确!

三、为什么计算机要用补码?

  1. 统一处理符号位和数值位 用补码时,符号位和数值位可以一起参与运算,不用单独处理。
  2. 加法和减法可以用同一套电路实现CPU 只有加法器,用补码可以把减法变成加法。
  3. 避免出现 "+0" 和 "-0" 的歧义 原码 / 反码中,+0-0 有两种表示;补码中只有一种

四、关键结论

  • 计算机中所有整数都以补码形式存储,我们写代码时看到的十进制数,底层都是补码。
  • 正数的原 / 反 / 补码完全相同,不用额外转换。
  • 负数的补码是 "反码 + 1",反码是 "原码除符号位外取反"。

2. 大小端核心概念

一、 什么是大小端?

以 4 字节的 int a = 0x11223344 为例,它的4 个字节从高到低是0x11 0x22 0x33 0x44

  • 大端模式高字节存低地址,低字节存高地址→ 内存中顺序是 11 22 33 44
  • 小端模式低字节存低地址,高字节存高地址 → 内存中顺序是 44 33 22 11

二、为什么会有大小端?

因为计算机以字节为单位 寻址,而 **short/int/long**是多字节数据,不同硬件架构对多字节数据的存储顺序定义不同:

  • x86/x86-64、ARM(多数):小端模式
  • Keil C51、部分网络协议:大端模式
  • 部分 ARM 架构支持软件 / 硬件切换模式

三、练习题解析(从易到难)

练习 1:判断大小端的两种写法

这是最经典的笔试题,核心思路都是「用 char*int 的第一个字节」

复制代码
// 写法1:指针强制转换
int check_sys() {
    int i = 1;
    return *(char *)&i;
}
// 小端:内存是 01 00 00 00 → 取第一个字节得到 1 → 输出小端
// 大端:内存是 00 00 00 01 → 取第一个字节得到 0 → 输出大端

// 写法2:共用体(union)实现
int check_sys() {
    union {
        int i;
        char c;
    } un;
    un.i = 1;
    return un.c;
}
// union 的成员共享同一块内存,char c 会直接取 int i 的第一个字节,和写法1原理一致

练习 2:char/signed 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",a,b,c);
    return 0;
}

运行结果: a = -1, b = -1, c = 255

解析:(输出的原码)(存的是补码)(%d是以十进制数打印一个有符号整数)

  • char a=-1 char 默认是有符号,存 11111111(补码),整型提升符号位扩展 → 输出 -1(原码)

  • signed char b=-1 有符号 char,存 11111111,整型提升符号位扩展一直补1 → 输出 -1

  • unsigned char c=-1 无符号 char,存 11111111(补码)整型提升高位补 0(符号位变成0) → 输出 255(原码就和补码相同)


练习 3:char 赋值越界题

代码 1:char a = -128; printf("%u\n",a);
  • char 是有符号类型 ,范围是**-128 ~ 127** ,-128 的补码是 10000000
  • %u 输出时(%u以十进制形式,打印无符号整数(就没有原反补概念了)) ,会符号扩展为 0xFFFFFF80,十进制为 4294967168
代码 2:char a = 128; printf("%u\n",a);
  • char 最大正数是 127128 超出范围,实际存储的是 128 - 256 = -128 (因为取的10000000与-128的补码前八位一样)
  • 补码同样是 10000000,按 %u 输出和代码 1 结果一样,为 4294967168

练习 4:strlenchar 数组题

复制代码
int main() {
    char a[1000];
    int i;
    for(i = 0; i < 1000; i++) {
        a[i] = -1 - i;
    }
    printf("%d", strlen(a));
    return 0;
}

运行结果:255

解析:

  • char 是有符号类型,赋值范围是**-128 ~ 127** ,循环赋值会溢出:
    • i=0a[0] = -1
    • i=1a[1] = -2
    • ...
    • i=127a[127] = -128
    • i=128a[128] = -129 → 溢出为 127
    • i=255a[255] = -256 → 溢出为 0
  • strlen'\0'(即 0)作为结束标志,第一个 0 出现在 a[255]所以长度是 255

练习 5:死循环题

代码 1:unsigned char i = 0; for(i=0; i<=255; i++) printf("hello world\n");
  • unsigned char 的范围是 0 ~ 255 ,当 i=255i++ 会溢出回到 0
  • 条件 i<=255 永远成立,这是一个无限循环
代码 2:unsigned int i; for(i=9; i>=0; i--) printf("%u\n",i);
  • unsigned int 没有负数,i=0 时,i-- 会溢出变成 4294967295无符号最大值
  • 条件 i>=0 永远成立,也是一个无限循环

练习 6:指针与大小端题(x86 小端环境)(小端字节序)

复制代码
#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;
}

1. 先算 ptr1[-1] 的值
  • &a整个数组的地址 ,类型是 int(*)[4](指向 4 个 int 的数组指针)。
  • &a + 1 会跳过整个数组的大小 ,也就是 4*sizeof(int) = 16 字节,指向数组最后一个元素的下一个地址。
  • ptr1 = (int *)(&a + 1); 把这个地址转成了**int* 类型。**
  • ptr1[-1] 等价于 *(ptr1 - 1),也就是从 ptr1 往前移动 1 个 int(4 字节),正好指向数组的最后一个元素 a[3]
  • a[3] 的值是 4,用 %x 输出就是 4

2. 再算 *ptr2 的值
  • a 是数组名,作为地址时等价于**&a[0]** ,类型是 int*

  • (int)a 把这个地址强制转换成了整数 ,假设 a 的地址是 0x0000FF00(仅举例),那么 (int)a + 1 就是 0x0000FF01

  • ptr2 = (int *)((int)a + 1); 把这个地址又转回了 int* 类型,指向 0x0000FF01 这个地址。

  • 我们来看内存布局(x86 是小端序,低地址存低位):

    复制代码
    地址:  FF00 FF01 FF02 FF03 FF04 FF05 FF06 FF07 ...
    数据:  01   00   00   00   02   00   00   00 ...
           (a[0]=1)            (a[1]=2)
  • ptr2 指向 FF01*ptr2 会从**FF01 开始读取 4 个字节** :00 00 00 02。(因为是int*)

  • 因为是小端序,这 4 个字节组成的**int0x02000000。**

最终运行结果
复制代码
4, 2000000

3. 浮点数在内存中的存储(IEEE 754 标准)

一、基础概念

  • 浮点数家族:floatdoublelong double
  • 表示范围:定义在头文件 float.h
  • 常见例子:3.141591E10

二、经典例题

复制代码
#include <stdio.h>
int main()
{
    int n = 9;
    float *pFloat = (float *)&n;
    printf("n的值为:%d\n", n);
    printf("*pFloat的值为:%f\n", *pFloat);

    *pFloat = 9.0;
    printf("n的值为:%d\n", n);
    printf("*pFloat的值为:%f\n", *pFloat);
    return 0;
}

运行结果:

复制代码
n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000

这个运行结果说明 整数在内存的存储方式和浮点数是不一样的


三、核心原理:IEEE 754 浮点数表示法

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

例 1:正整数5 的表示

  1. 5 的二进制是 101,写成科学计数法:5 = 1.01 × 2²
  2. 套公式:
    • S=0正数
    • M=1.01 (满足 1 ≤ M < 2
    • E=2所以:V = (-1)^0 × 1.01 × 2² = 1 × 1.01 × 4 = 5

例 2:负小数 -3.5 的表示

  1. 转成二进制科学计数法:
    • 3.5 的二进制是**11.1**
    • 写成**1.11 × 2¹**
  2. 套公式:
    • S=1 → 负数
    • M=1.11
    • E=1 所以:V = (-1)^1 × 1.11 × 2¹ = -1 × 1.11 × 2 = -3.5

存储结构

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

四、存储与读取规则

1. 存的过程

  • 符号位 S:直接存正负号
  • 尾数位 M
    • 科学计数法中 M总是 1.xxxx 的形式
    • 计算机中只存小数部分 xxxx,前面的 1 不存(节省 1 位空间)
    • 读取时再把 1 补上
  • 指数位 E
    • 存的是偏移值:真实指数 + 中间数
    • float 的中间数是 127,double 是 1023(进行修正 避免E为负数)
    • 例:2^3 的指数是 3,float 中存为 3+127=130

但是有时候会遇到特殊情况 比如有的浮点数无法精确保存

1.2 这种十进制小数,转成二进制时是无限循环小数1.2₁₀ = 1.001100110011...₂(0011 无限循环)

2. 取的过程(三种情况)

  1. E 不全为 0 / 不全为 1(常规情况)
    • 真实指数 = 存储的 E - 中间数
    • 有效数字 M 前面补上省略的 1
  2. E 全为 0
    • 指数真实值 = 1 - 中间数(float 为 - 126)
    • 有效数字 M 不再补 1,直接按 0.xxxx 解析
    • 用于表示接近 0 的极小值
  3. E 全为 1
    • 若 M 全为 0 :表示**±∞(正负由 S 决定)**
    • 若 M 不全为 0:表示 NaN(非数字)

经过上面的知识学习后我们再回过头来解释一下这个代码

五、例题回顾

复制代码
int n = 9;
float *pFloat = (float *)&n;  // 把int的地址强转成float指针
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);

*pFloat = 9.0;  // 用float指针修改同一块内存
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);

核心是:n*pFloat 指向的是同一块 4 字节内存,只是解释方式不同

  • int n:按整数规则解释这 4 个字节
  • float *pFloat:按IEEE 754 浮点数规则解释这 4 个字节

第一步:int n = 9; 时的内存和输出

1. n 的二进制表示

int n = 9 是一个 32 位整数,二进制为:

复制代码
00000000 00000000 00000000 00001001

int 解释,它就是十进制的 9 ,所以第一句输出 n的值为:9

2. 用 float 解释这串二进制

把上面这串二进制,按 IEEE 754 浮点数规则拆分:

  • 符号位 S:最高位 0 → 正数
  • 指数位 E:接下来 8 位 00000000E 全 0(非规格化数)
  • 尾数位 M:剩下 23 位**00000000000000000001001**

套用非规格化数的公式代入

  • S=0,所以 (-1)^0 = 1
  • 0.M = 0.00000000000000000001001₂,这个数极小**(非规范数不用加1)**
  • 乘以 (2^{-126}) (1-127得126)后,结果是一个无限接近 0 的超级小正数

printf("%f") 默认保留 6 位小数 ,这么小的数会被直接显示为 0.000000 ,所以第二句输出 *pFloat的值为:0.000000


第二步:*pFloat = 9.0; 时的内存和输出

1. 9.0 的 IEEE 754 表示

先把 9.0 转成二进制科学计数法:9.0 1001.0 (-1)^0 * (1.001) * 2^3 按 IEEE 754 规则编码:

  • 符号位 S0(正数)
  • 指数位 E真实指数是 3 ,加上偏移量127 → (3 + 127 = 130 ) → 二进制 10000010
  • 尾数位 M:省略 1.001 前面的 1存小数部分 001后面补 0 凑够 23 位:00100000000000000000000

最终 32 位二进制:

复制代码
0 10000010 00100000000000000000000

2. 把这串二进制按 int 解释

把上面这串二进制,当成 32 位有符号整数解析:(符号位为0 原码就是补码直接输出)

  • 转成十进制1091567616 所以此时 n 的值变成了 1091567616 ,第三句输出 n的值为:1091567616

3. 用 float 解释

串二进制本来就是 9.0 的 IEEE 754 编码 ,所以按 float 解释就是 9.0 ,第四句输出 *pFloat的值为:9.000000**。**

总结

我们认识到浮点数的存储与整数是完全不一样的 我们要清楚正确认识浮点数的存储中存和取的过程(非常特殊)

相关推荐
我想我不够好。1 小时前
2026.5.14 消防监控学习 35min
学习
钱多多_qdd1 小时前
基于mac环境,升级python环境问题解决
开发语言·python·macos
AOwhisky1 小时前
Docker 学习笔记:Docker Compose 多容器编排
linux·运维·笔记·学习·docker·容器
boonya1 小时前
Python 量化金融框架及技术落地方案
开发语言·python·金融
qeen871 小时前
【算法笔记】各种常见排序算法详细解析(上)
c语言·数据结构·c++·学习·算法·排序算法
金色光环1 小时前
【DSP学习】 EPWM 原理-基于普中DSP开发攻略
学习·dsp开发
Ulyanov1 小时前
《从质点到位姿:基于Python与PyVista的导弹制导控制全栈仿真》: 基石——3-DOF质点弹道的高保真建模与数值稳定性分析
开发语言·python·算法·ui·系统仿真
学习中.........1 小时前
Java 并发容器深度解析:从早期遗留类到现代高并发架构
java·开发语言·架构
加号31 小时前
【C#】 实现程序最小化后重新拉起并强制置顶显示的技术指南
开发语言·c#