
在计算机系统中,数据是如何被存储和表示的?了解数据在内存中的存储方式,特别是整数的补码、大小端字节序以及浮点数的 IEEE 754 标准,是深入理解计算机底层工作原理的关键。 本文将详细解析这些核心概念。
整数的二进制表示方法有三种:原码、反码和补码。
有符号整数的三种表示方法都包含符号位和数值位两部分。最高位的一位被当作符号位,其中:
对于整型数据,数据在内存中存放的其实是补码。原因在于:使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU 只有加法器)。
大小端字节序是指在计算机系统中,超过一个字节的数据在内存中存储时(如int、float等),存在存储顺序的问题,从而分为大端字节序存储和小端字节序存储。

可以看到将a变量存入内存中,并不是像我们赋值那样的顺序存入,而是反着存,这也就是我们后面将要了解到的小端字节序存储。
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8 bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数⼤于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度⼤于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了⼤端存储模式和⼩端存储模式。
没有绝对的对错,只是历史发展形成的不同标准,有的侧重人类可读性,有的侧重计算效率,各自在特定场景下都有其优势。这种多样性反映了计算机架构发展中的实用主义取向——不同的设计团队根据各自的目标和约束做出了最优选择。
既然是因为超过了一个字节,所以有了字节序,那我们创建一个多字节整型变量(例如设置为1,其十六进制为0x00000001),然后通过单字节char类型指针访问该变量的首字节内存;由于整型占多个字节,在小端模式下最低有效字节存储在最低地址,因此首字节值为1,而在大端模式下最高有效字节存储在最低地址,因此首字节值为0,通过检测这一差异即可确定系统的字节序存储方式。
以下是当前思路判断当前机器字节序的实现:
void chek_sys()
{
int i = 1;
if (*(char*)&i == 1)
printf("小端字节序");
else if(*(char*)&i == 0)
printf("大端字节序");
}
还有一种方法是使用联合体进行判断,因为联合体所有成员共享同一块内存空间。当联合体中同时包含整型(如int,占4字节)和字符型(char,占1字节)成员时,对整型成员赋值后,字符型成员访问的正是该共享内存空间的首字节,从而直接暴露了字节的存储顺序特征。
判断思路是:创建一个包含整型成员和字符数组成员的联合体,将整型成员赋值为1后,通过检查字符数组首元素的值来确定字节序——若首元素为1则是小端存储(低位字节在低地址),若为0则是大端存储(高位字节在低地址),这利用了联合体各成员共享同一块内存空间的特性。
void chek_sys()
{
union
{
int i;
char c;
}un;
un.i = 1;
if (un.c == 1)
printf("小端字节序");
else
printf("大端字节序");
}
同样也可以进行判断。
这里刚开始想的时候,不明白取第一位地址究竟是取的谁

实际上压根没有那么绕,这里我们可以看见从左往右四个字节的数据,拿出第一个字节实际就是拿到最左边这个数据而已,也就是01,在十六进制地址处,最低位就是01,故最低位存储在低地址处,所以是小端字节序。
所有的数据类型,无论 signed char 、 unsigned char 、 int 还是其他,在计算机底层都只是一串二进制位。同一个二进制串,如果被解释为有符号数(如 signed char )或无符号数(如 unsigned char),它们所代表的实际数值是截然不同的。而我们之前学习过整型提升,它的关键所在:提升过程只涉及位模式(bit pattern)的扩展,而数值的解释方式则取决于原始类型和目标类型。
核心图示:字节的双重身份,请看下面这张图,它清晰地展示了一个 8 位(一个字节)的二进制串是如何在有符号字符(signed char)和无符号字符(unsigned char)这两种身份之间进行转换和解释的。
这张图是理解后续所有整型提升问题的基石,请仔细观察:
记住: 当 char 类型进行整型提升时,它会携带原始的符号属性(有符号/无符号),通过符号位扩展或零扩展来转换成 32 位的 int ,但其核心的位模式与数值对应关系,就蕴含在这两个圆环之中。

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),不存在负数。当将一个负值赋给无符号变量时,会发生截断和模运算。
1111 1111 赋给 unsigned char1111 1111 对应的十进制值是 因此,变量 c 在内存存储的 8 位补码是 1111 1111,其语义值是 255。
步骤二:整型提升(Integer Promotion)
1. 变量 a 和 b 的提升(有符号)
1111 1111,符号位是 12. 变量 c 的提升(无符号)
1111 1111步骤三:结果输出
变量 | 原始类型 | 原始值 | 提升规则 | 提升后的 32 位 int 值 | 最终输出(%d) |
|---|---|---|---|---|---|
a | char | -1 | 符号扩展 | -1 | -1 |
b | signed char | -1 | 符号扩展 | -1 | -1 |
c | unsigned char | 255 | 零扩展 | 255 | 255 |

#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}按照上一题思路,求补码,求内存中的8个补码位,然后整型提升,假设这里是128呢,结果会有差异吗?给出过程图:

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

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
...1 0000 00000000 0000而strlen(s) 函数用于计算字符串 s 的长度。它的工作方式是:从字符串的起始地址开始,遍历字符直到遇到第一个空字符 ,即数值为 0 的字符。
在循环中:
a[0] = -1a[1] = -2a[254] = -255 (截断后为 1)a[255] = -256 (截断后为 0)因此,字符串的长度是 255(从索引 0 到 254 的字符数量)。


先说结论,这两个题都会无限循环打印:
第一题: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 的条件,使得程序继续执行,因此造成了无限循环。
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.14159、1E10 等,浮点数家族包括 float、double、long double 类型。
考虑以下代码的输出结果:
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;
}先给出结果:

要理解这个结果,必须搞懂浮点数在计算机内部的表示方法 。
根据国际标准 IEEE 754,任意一个二进制浮点数
可以表示成下面的形式:
:表示符号位 3。当
,V 为正数;当
,V 为负数 4。
:表示有效数字,
是大于等于 1,小于 2 的 5。
:表示指数位 6。
举例:
,写成二进制是
,相当于
。可得出
,
,
。
,写成二进制是
,相当于
。可得出
,
,
。
IEEE 754 存储结构规定:
类型 | 符号位 S | 指数 E | 有效数字 M |
|---|---|---|---|
32位 (float) | 1位 (最高位) | 8位 | 23位 |
64位 (double) | 1位 (最高位) | 11位 | 52位 |

这个图就可以抽象出float类型数据在内存中的存储,而double类型需要将指数位和有效数字位分别扩大到11位、52位。
IEEE 754 对
和
有特别规定: 有效数字 M: 由于
,
可以写成
的形式。标准规定,计算机内部保存
时,默认第一位总是
,因此可以被舍去,只保存后面的
小数部分。这样做是为了节省 1 位有效数字(例如 32 位浮点数可以保存 24 位有效数字)。 指数 E:
是一个无符号整数 (unsigned int)。
的真实值必须加上一个 中间数(偏移量)再存入内存。
float),中间数是 127。double),中间数是 1023。从内存中取出指数
的情况分为三种:
的计算值减去 127(或 1023)得到真实值。
前面要加上第一位的 1。
等于
(或
)即为真实值。
不再加上第一位的
,而是还原为
的小数。这样做是为了表示
以及接近于
的很小的数字。
全为
,表示
(正负取决于符号位
)。
环节 1:int n=9,按 float 读取
以整型形式存储在内存中的二进制序列是(假设 32 位):
)拆分:
(全为
)
全为
,符合 E 全为
的情况。
写成:
。
的正数 44,所以输出为 0.000000。
环节 2:*pFloat = 9.0,按 int 读取
转换为
形式存储:
换算成科学计数法是
。
47。
,即
。
后面加 20 个
。
)是: