首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >彻底搞懂计算机底层:补码、大小端、IEEE 754,一文吃透数据在内存中到底怎么存!

彻底搞懂计算机底层:补码、大小端、IEEE 754,一文吃透数据在内存中到底怎么存!

作者头像
Extreme35
发布2025-12-23 18:19:58
发布2025-12-23 18:19:58
6580
举报
文章被收录于专栏:DLDL

在计算机系统中,数据是如何被存储和表示的?了解数据在内存中的存储方式,特别是整数的补码、大小端字节序以及浮点数的 IEEE 754 标准,是深入理解计算机底层工作原理的关键。 本文将详细解析这些核心概念。

一、整数在内存中的存储

整数的二进制表示方法有三种:原码反码补码

1.1 符号位与数值位

有符号整数的三种表示方法都包含符号位和数值位两部分。最高位的一位被当作符号位,其中:

  • 0 表示“正”,1 表示“负”。
  • 正整数:原码、反码、补码都相同
  • 负整数:三种表示方法各不相同。

1.2 原码、反码、补码的转换

  1. 原码:直接将数值按照正负数的形式翻译成二进制得到。
  2. 反码:将原码的符号位不变,其他位依次按位取反得到。
  3. 补码:反码 + 1 得到。

对于整型数据,数据在内存中存放的其实是补码原因在于:使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU 只有加法器)。

二、大小端字节序

大小端字节序是指在计算机系统中,超过一个字节的数据内存中存储时(如intfloat等),存在存储顺序的问题,从而分为大端字节序存储和小端字节序存储。

在这里插入图片描述
在这里插入图片描述

可以看到将a变量存入内存中,并不是像我们赋值那样的顺序存入,而是反着存,这也就是我们后面将要了解到的小端字节序存储。

2.1 什么是大小端?

  • 大端字节序:数据的高位字节存储在内存的低地址处,低位字节存储在高地址处,字节的存储顺序与人类的阅读习惯一致,类似于我们书写数字时先写高位再写低位。
  • 小端字节序:数据的低位字节存储在内存的低地址处,高位字节存储在高地址处,这种存储方式与人类的阅读习惯相反,但在计算机处理时更加高效,x86和ARM架构均采用此种方式。

2.2 为什么存在大小端模式?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8 bit 位,但是在C语⾔中除了8 bitchar 之外,还有16 bitshort 型,32 bitlong 型(要看具体的编译器),另外,对于位数⼤于8位的处理器例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存储模式。 没有绝对的对错,只是历史发展形成的不同标准,有的侧重人类可读性,有的侧重计算效率,各自在特定场景下都有其优势。这种多样性反映了计算机架构发展中的实用主义取向——不同的设计团队根据各自的目标和约束做出了最优选择。

2.3 如何判断机器字节序

既然是因为超过了一个字节,所以有了字节序,那我们创建一个多字节整型变量(例如设置为1,其十六进制为0x00000001),然后通过单字节char类型指针访问该变量的首字节内存;由于整型占多个字节,在小端模式最低有效字节存储在最低地址,因此首字节值为1,而在大端模式最高有效字节存储在最低地址,因此首字节值为0,通过检测这一差异即可确定系统的字节序存储方式。

以下是当前思路判断当前机器字节序的实现:

代码语言:javascript
复制
void chek_sys()
{
	int i = 1;
	if (*(char*)&i == 1)
		printf("小端字节序");
	else if(*(char*)&i == 0)
		printf("大端字节序");
}
在这里插入图片描述
在这里插入图片描述

还有一种方法是使用联合体进行判断,因为联合体所有成员共享同一块内存空间。当联合体中同时包含整型(如int,占4字节)和字符型(char,占1字节)成员时,对整型成员赋值后字符型成员访问的正是该共享内存空间的首字节,从而直接暴露了字节的存储顺序特征。 判断思路是:创建一个包含整型成员和字符数组成员的联合体,将整型成员赋值为1后,通过检查字符数组首元素的值来确定字节序——若首元素为1则是小端存储(低位字节在低地址),若为0则是大端存储(高位字节在低地址),这利用了联合体各成员共享同一块内存空间的特性。

代码语言:javascript
复制
void chek_sys()
{
	union
	{
		int i;
		char c;
	}un;
	un.i = 1;
	if (un.c == 1)
		printf("小端字节序");
	else
		printf("大端字节序");
}
在这里插入图片描述
在这里插入图片描述

同样也可以进行判断。

这里刚开始想的时候,不明白取第一位地址究竟是取的谁

在这里插入图片描述
在这里插入图片描述

实际上压根没有那么绕,这里我们可以看见从左往右四个字节的数据,拿出第一个字节实际就是拿到最左边这个数据而已,也就是01,在十六进制地址处,最低位就是01,故最低位存储在低地址处,所以是小端字节序。

2.4 判断练习

所有的数据类型,无论 signed charunsigned charint 还是其他,在计算机底层都只是一串二进制位。同一个二进制串,如果被解释为有符号数(如 signed char )或无符号数(如 unsigned char),它们所代表的实际数值是截然不同的。而我们之前学习过整型提升,它的关键所在:提升过程只涉及位模式(bit pattern)的扩展,而数值的解释方式则取决于原始类型和目标类型。

核心图示:字节的双重身份,请看下面这张图,它清晰地展示了一个 8 位(一个字节)的二进制串是如何在有符号字符(signed char)和无符号字符(unsigned char)这两种身份之间进行转换和解释的。

这张图是理解后续所有整型提升问题的基石,请仔细观察:

  • 红色区域: 表示非负数(0 到 127),在这两种类型中,数值和位模式是完全一致的。
  • 蓝色区域: 表示最高位为 1 的位模式。
    • 在左侧的 signed char 环中,这些位模式被解释为负数(-1 到 -128)。
    • 在右侧的 unsigned char 环中,同样的位模式却被解释为正数(128 到 255)。
  • 虚线: 分隔了最高位为 0 和最高位为 1 的区域。

记住: 当 char 类型进行整型提升时,它会携带原始的符号属性(有符号/无符号),通过符号位扩展或零扩展来转换成 32 位的 int ,但其核心的位模式与数值对应关系,就蕴含在这两个圆环之中。

在这里插入图片描述
在这里插入图片描述
2.4.1 练习一
代码语言:javascript
复制
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;
}

步骤一:变量的初始化与内存存储(8位 char) char 类型通常占用 8 个比特位(字节)。在计算机中,所有数据(包括负数)都以补码形式存储。 1. 有符号数 -1 的存储(a 和 b) 对于 8 位有符号类型(char / signed char),表示 -1 的过程如下:

阶段

8 位二进制

描述

原码

1000 0001

符号位 1,数值 1

反码

1111 1110

符号位不变,数值位取反

补码

1111 1111

反码 +1

因此,变量 a 和 b 在内存存储的 8 位补码都是 1111 1111

2. 无符号数 -1 的存储(c)

对于 8 位无符号类型(unsigned char),不存在负数。当将一个负值赋给无符号变量时,会发生截断和模运算。

  • 将 -1 的 8 位补码 1111 1111 赋给 unsigned char
  • 无符号数将其所有位视为数值位,因此 1111 1111 对应的十进制值是
2^8 - 1 = 255

因此,变量 c 在内存存储的 8 位补码是 1111 1111,其语义值是 255。

步骤二:整型提升(Integer Promotion)

1. 变量 a 和 b 的提升(有符号)

  • 类型: char / signed char
  • 规则: 符号扩展(Sign Extension)。提升时,高位用原数的符号位来填充。由于 8 位补码是 1111 1111,符号位是 1
  • 提升结果(32 位补码):
32\text{位补码}=\underbrace{1111\ldots1111}_{24\text{个}1}11111111
  • 数值解析: 这是一个符号位为 1 的负数,其 32 位补码表示的值正是 -1

2. 变量 c 的提升(无符号)

  • 类型: unsigned char
  • 规则: 零扩展(Zero Extension)。提升时,高位用 0 来填充。由于 8 位补码是 1111 1111
  • 提升结果(32 位补码):
32\text{位补码}=\underbrace{0000\ldots0000}_{24\text{个}0}11111111
  • 数值解析: 这是一个符号位为 0 的正数,其 32 位值是 255

步骤三:结果输出

变量

原始类型

原始值

提升规则

提升后的 32 位 int 值

最终输出(%d)

a

char

-1

符号扩展

-1

-1

b

signed char

-1

符号扩展

-1

-1

c

unsigned char

255

零扩展

255

255

在这里插入图片描述
在这里插入图片描述
2.4.2 练习二
代码语言:javascript
复制
#include <stdio.h>
int main()
{
 char a = -128;
 printf("%u\n",a);
 return 0;
}

按照上一题思路,求补码,求内存中的8个补码位,然后整型提升,假设这里是128呢,结果会有差异吗?给出过程图:

在这里插入图片描述
在这里插入图片描述

完全无差别,-128与128的内存中8个补码位都是一致的,最后都当作无符号进行零扩展填充,故结果也一致。

在这里插入图片描述
在这里插入图片描述
2.4.3 练习三
代码语言:javascript
复制
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}

char 类型特性

  • char 类型通常占用 1 字节 (8 位)
  • 在本例中,char 默认是有符号类型(signed char),其数值范围是 [-128, 127]

溢出分析 由于 a[i] 是 8 位的有符号类型,当计算结果 -1 - i 超出其范围时,就会发生溢出(或者更准确地说是截断)

迭代变量 i

计算值 (-1 - i)

8 位 char (补码)

8 位 char (十进制值)

0

-1

1111 1111

-1

1

-2

1111 1110

-2

127

-128

1000 0000

-128

128

-129

0111 1111

127 (发生溢出/截断,值回绕)

129

-130

0111 1110

126

255

-256

0000 0000

0

256

-257

1111 1111

-1

关键点:i = 255 时,计算值是 -1 - 255 = -256

  • -256 的 32 位补码是 ...1 0000 0000
  • 截断为 8 位后,结果是 0000 0000
  • 在 char 类型中,这代表数值 0

strlen(s) 函数用于计算字符串 s 的长度。它的工作方式是:从字符串的起始地址开始,遍历字符直到遇到第一个空字符 ,即数值为 0 的字符。

在循环中:

  • a[0] = -1
  • a[1] = -2
  • a[254] = -255 (截断后为 1)
  • a[255] = -256 (截断后为 0)

因此,字符串的长度是 255(从索引 0 到 254 的字符数量)。

在这里插入图片描述
在这里插入图片描述
2.4.4 练习四
在这里插入图片描述
在这里插入图片描述

先说结论,这两个题都会无限循环打印: 第一题:unsigned char i = 0 的循环 错误原因在于对 unsigned char 类型溢出规则的误解。该类型是 8 位无符号整数,其最大值是 255。循环条件是 i <= 255。当 i 的值达到 255 后,执行 i++ 时,无符号数会发生回绕,即 255 + 1 溢出后结果是 0。由于 0 仍然满足循环条件 i <= 255,变量 i 会不断在 0 到 255 之间循环,导致程序陷入无限循环。 第二题:unsigned int i 的倒序循环 错误原因在于将无符号类型用于递减到负数的循环条件。unsigned int 永远大于或等于 0,所以循环条件 i >= 0 对于任何 unsigned int 的值(包括 0 和最大值)都恒为真。当 i 递减到 0 时,执行 i-- 会导致无符号数回绕到其类型的最大值。这个最大值也满足 i >= 0 的条件,使得程序继续执行,因此造成了无限循环。

2.4.5 练习五
代码语言:javascript
复制
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;
}
在这里插入图片描述
在这里插入图片描述

对于 ptr1 = (int*)(&a + 1)&a 是数组指针,加 1 跳过整个数组 16 字节,指向数组末尾的下一个地址;因此 ptr1[-1] 正好访问到数组的最后一个元素 a[3],其值为 4。对于 ptr2 = (int*)((int)a + 1)a 被视为地址,加 1 是字节级别的位移,因此 ptr2 指向 a[0] 的第二个字节(地址 0x1001);由于 X86 是小端字节序,a[0]=1 存储为 01 00 00 00,而 a[1]=2 存储为 02 00 00 00,从 0x1001 处以 int 类型读取 4 字节数据得到 00 00 00 02(来自 a[0] 的后三字节和 a[1] 的首字节),按小端序组合后结果是 0x02000000,所以最终输出为 4,20000000

在这里插入图片描述
在这里插入图片描述

三、浮点数在内存中的存储

常见的浮点数包括 3.141591E10 等,浮点数家族包括 floatdoublelong double 类型。 考虑以下代码的输出结果:

代码语言:javascript
复制
int main()
{
 int n = 9;
 float *pFloat = (float *)&n;
 printf("n的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 *pFloat = 9.0;
 printf("num的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 return 0;
}

先给出结果:

在这里插入图片描述
在这里插入图片描述

要理解这个结果,必须搞懂浮点数在计算机内部的表示方法 。

3.1 浮点数的存储:IEEE 754 标准

根据国际标准 IEEE 754,任意一个二进制浮点数

V

可以表示成下面的形式:

V=(-1)^{S} \times M \times 2^{E} \text{ }
(-1)^{S}

:表示符号位 3。当

S=0

,V 为正数;当

S=1

,V 为负数 4。

M

:表示有效数字,

M

是大于等于 1,小于 2 的 5。

2^{E}

:表示指数位 6。

举例:

  • 十进制的
5.0

,写成二进制是

101.0

,相当于

1.01 \times 2^{2}

。可得出

S=0

,

M=1.01

,

E=2

  • 十进制的
-5.0

,写成二进制是

-101.0

,相当于

-1.01 \times 2^{2}

。可得出

S=1

,

M=1.01

,

E=2

IEEE 754 存储结构规定:

类型

符号位 S

指数 E

有效数字 M

32位 (float)

1位 (最高位)

8位

23位

64位 (double)

1位 (最高位)

11位

52位

在这里插入图片描述
在这里插入图片描述

这个图就可以抽象出float类型数据在内存中的存储,而double类型需要将指数位和有效数字位分别扩大到11位、52位。

3.1.1 浮点数存的过程

IEEE 754

M

E

有特别规定: 有效数字 M: 由于

1 \le M < 2

M

可以写成

1.xxxxxx

的形式。标准规定,计算机内部保存

M

时,默认第一位总是

1

,因此可以被舍去,只保存后面的

xxxxxx

小数部分。这样做是为了节省 1 位有效数字(例如 32 位浮点数可以保存 24 位有效数字)。 指数 E:

E

是一个无符号整数 (unsigned int)。

  • 为了表示科学计数法中可能出现的负指数,
E

的真实值必须加上一个 中间数(偏移量)再存入内存。

  • 对于 8 位的 E(float),中间数是 127
  • 对于 11 位的 E(double),中间数是 1023
3.1.2 浮点数取的过程

从内存中取出指数

E

的情况分为三种:

  1. E 不全为 0 或不全为 1:
    • 指数
    E

    的计算值减去 127(或 1023)得到真实值。

    • 有效数字
    M

    前面要加上第一位的 1

  2. E 全为 0:
    • 指数
    E

    等于

    1 - 127

    (或

    1 - 1023

    )即为真实值。

    • 有效数字
    M

    不再加上第一位的

    1

    ,而是还原为

    0.xxxxxx

    的小数。这样做是为了表示

    \pm 0

    以及接近于

    0

    的很小的数字。

  3. E 全为 1:
    • 如果有效数字
    M

    全为

    0

    ,表示

    \pm \infty

    (正负取决于符号位

    S

    )。

3.2 题目解析

环节 1:int n=9,按 float 读取

9

以整型形式存储在内存中的二进制序列是(假设 32 位):

0\ 00000000\ 000000000000000000001001 \text{ }
  • 将此二进制序列按浮点数(
S + E + M

)拆分:

S=0
E=00000000

(全为

0

)

M=000\ 0000\ 0000\ 0000\ 0000\ 1001

  • 由于
E

全为

0

,符合 E 全为

0

的情况。

V

写成:

(-1)^{0} \times 0.00\ldots 1001 \times 2^{-126}

  • 这是一个很小且接近于
0

的正数 44,所以输出为 0.000000

环节 2:*pFloat = 9.0,按 int 读取

  • 首先将浮点数
9.0

转换为

S+E+M

形式存储:

9.0

换算成科学计数法是

1.001 \times 2^{3}

S=0

47。

E = 3 + 127 = 130

,即

10000010

M = 001

后面加 20 个

0

  • 存储的 32 位二进制序列(
S+E+M

)是:

0\ 10000010\ 00100000000000000000000 \text{ }

  • 这个 32 位二进制数被当做整数的补码来解析。
  • 其对应的十进制整数原码正是 1091567616
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、整数在内存中的存储
    • 1.1 符号位与数值位
    • 1.2 原码、反码、补码的转换
  • 二、大小端字节序
    • 2.1 什么是大小端?
    • 2.2 为什么存在大小端模式?
    • 2.3 如何判断机器字节序
    • 2.4 判断练习
      • 2.4.1 练习一
      • 2.4.2 练习二
      • 2.4.3 练习三
      • 2.4.4 练习四
      • 2.4.5 练习五
  • 三、浮点数在内存中的存储
    • 3.1 浮点数的存储:IEEE 754 标准
      • 3.1.1 浮点数存的过程
      • 3.1.2 浮点数取的过程
    • 3.2 题目解析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档