一、整数在内存中的存储

整数二进制的表示方法有三种:原码、反码、补码三种表示方法均由符号位和数

值位组成符号位用 0 表示正数,用 1 表示负数最高位当作符号位,其余均为数值位

正整数的原码、反码、补码都相同

负整数的原码就是二进制表示形式,最高位是 1

负整数的反码是符号位不变,数值位全部取反(0变1,1变0)

负整数的补码是反码 + 1

对于整形来说:数据存在内存中存放的是补码

计算机硬件只有加法器,没有专门的减法器,补码可以把减法完全转换成加法运

算;同时补码解决了「数字 0 有两种编码」的歧义、最大化利用二进制编码空

间、让符号位直接参与运算,是兼顾硬件简单、运算高效、编码规范的最优方

案。

二、大小端字节序和字节序判断

大端字节序:数据的低位字节内容放在高地址处,高位字节内容保存在内存的低地址处

小端字节序:数据的低位字节内容放在低地址处,高位字节内容保存在内存的高地址处

在 visual studio 2026 中采用的是小端字节序存放数据。

2.1 设计程序判断当前环境的大小端字节序

对 1 取出来一个字节的地址,解应用是 1 就是小端字节序,是 0 就是大端字节序

1
2
3
4
5
6
7
8
9
int n = 1;
if (*(char*)&n)
{
printf("小端\n");
}
else
{
printf("大端\n");
}


1
2
3
4
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d", a, b, c);

-1 是一个负整数,他在内存中存储的是 11111111
11111111 11111111

11111111

存放在变量 a b c 中时,存放的是 11111111

对于变量 a b, 他们都是有符号的,在处理时可处理为-1

对于变量 c , 是无符号的,按正数的存储来,就是原码也是 11111111,即 255


1
2
char a = -128;
printf("%u\n", a);

-128 在内存中存储的补码是 11111111 11111111 11111111 10000000

放在 a 中的就是 10000000

而 %u 是输出无符号十进制数,输出时发生整型提升,根据整型提升规则,提升为

11111111 11111111 11111111 10000000

因为是无符号,所以原码、反码、补码都相同,所以输出的值是 4294967168


1
2
char a = 128;
printf("%u\n", a);

128 在内存中存放的是 00000000 00000000 00000000 10000000

放到变量 c 里面的就是 10000000,这里可以发现,和上面的代码中,存放

的是一样的补码,变量 c 的类型仍然是 char , 故这里实际上 c 中存放的是 -128

而 %u 是输出无符号十进制数,输出时发生整型提升,根据整型提升规则,提升为

11111111 11111111 11111111 10000000

所以输出结果为 4294967168


1
2
3
4
5
6
7
char a[1000];
int i = 0;
for (i = 0;i < 1000;i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));

char 类型因为是一个字节的有符号数据类型,其中存放的数值的范围是 -128 ~

127

当 -1 - i 的结果是 -129 时,就会出现一个现象

-129 的二进制原码是 11111111 11111111 11111111 01111111,存放到 char
类型

中的是 01111111 ,而这个值是 127,

类似的 -130 的二进制原码是 11111111 11111111 11111111 01111110,存放到 char 类型

中的是 01111110 , 而这个值是 126

通过一个图来理解

可以看到对于一个字节来说,一直不停的 +1 ,这个字节的值会在 -128 ~ 127 之间循环,构成了一个圆


1
2
3
4
5
unsigned char i = 0;
for (i = 0;i <= 255;i++)
{
printf("hello world\n");
}

这里的 i 是无符号 char 型,存储数据的范围是 0 ~ 255, 即 11111111

当 i 的值为 255 时, 再加 1 ,由于 char 型只能存放一个字节的数据, 而 256 是 1 00000000,存放到 i 中的是 00000000 , i 的值是 0, 会一直这么循环下去,所以成了死循环


1
2
3
4
5
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}

首先这个循环一定是一个死循环,因为 i 的类型是 unsigned int , i >= 0 是恒成立的

再来看看 i 后续的值,那么当 i 的值为 0, 再 -1 时,往 i 中存放 -1 的补码 11111111

11111111 11111111 11111111 时,unsigned int 会认为这是一个非常大的正整数

所以就是死循环


1
2
3
4
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%#x,%#x", ptr1[-1], *ptr2);

先来看 ptr1,这是一个 int* 类型的指针,&a 取出的是整个数组的地址,是一个 int(*)[]

(数组指针),而 a + 1 就是跳过的整个数组,指向的就是数组后的那一块内存空间,

赋值给了 ptr1

假如现在有一个数组 arr , arr[1] 其实就是 *(arr + 1)

所以 ptr1[-1] 其实就是 *(ptr1 - 1)

再看 ptr2 , 数组名 a 被强转成 int 再 + 1,就是一个整数数值 + 1

这个操作可以理解成让这个地址向前移动了一个字节,再强转(int*)赋值给ptr2,而数

组 a 是一个整型数组,一个整型占四个字节。int* 类型解引用就是从地址起始位置

向后拿四个字节

而 %x 占位符是十六进制,1的十六进制是0x000001,2的十六进制是0x000002,由于

在 visual studio 2026 中采用的是小端字节序,内存中存放如图

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

浮点数表示的范围在 float.h 中定义

3.1 浮点数的存储与整型的存储不同

1
2
3
4
5
6
7
8
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);


通过运行结果可以看出浮点数在内存中的存储与整型不同

3.2 浮点数的存储

根据国际标准IEEE(电子和电气工程协会)754,任意一个二进制浮点数 V 可以表示成

下面的形式

比如5.5可以表示成二进制 101.1

按照 IEEE754,就是表示成1.01 * 2 ^ 1
其中 S = 0,M = 1.01, E = 1

相应的 -5.5 的表示就是-1 ^ 1 * 1.01 * 2 ^ 1
其中 S = 1,M = 1.01, E = 1

IEEE754规定:对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数

E,剩下的23位存储有效数字M

IEEE754规定:对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数

E,剩下的52位存储有效数字M

3.3 浮点数存储的过程

IEEE754规定 在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍

去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时

候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数

为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

而 E 是一个无符号整数,如果E为8位,它的取值范围为0~255;如果E为11位,

它的取值范围为0~2047。但是,科学计数法中的E是可以出现负数的,所以

IEEE754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个

中间数是127;对于11位的E,这个中间数是1023。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 5.5
float f = 5.5f;
// 二进制表示形式 101.1
// (-1) ^ 0 * 1.011 * 2 ^ 2
// S = 0 M = 1.011 E = 2
// 根据 IEEE754 规定
// 往内存中存的是 E 要加 127 ,M 只存放小数点后面的位置
// 2 + 127 = 129 二进制表示为 10000010
// 所以 5.5 在内存中存放的是
// 0 10000001 01100000000000000000000
// 转成 16 进制
// 0100 0000 1011 0000 0000 0000 0000 0000
// 40 b0 00 00 (16进制)

调试查看

由于浮点数在内存中的保存方式,有部分浮点数在内存中无法精确保存时避免不了的

1
2
float f = 3.14f;
printf("%.20f\n",f);

3.4 浮点数取的过程

E 从内存中取出的时候还可以再分成三种情况:
以 float 型举例

E 不全为 0 或 不全为 1

1
0 10000001 01100000000000000000000

这属于普遍情况,在取出 E 的时候 -127 就拿到真实的E

E 全为 0

1
0 00000000 01100000000000000000000

这属于特殊情况, E 是 +127 还能是 0, E的真实值被指定为 1 - 127 = -126,出现这种情况时,有效数字 M 不再加上1,而是直接还

原为 0.xxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

E 全为 1

1
0 11111111 01100000000000000000000

这属于特殊情况, E 是 +127 得到255, E 是 128 ,表示存入的数字是 1.xxx

* 2 ^ 128 次方, 这是一个极大数,出现这种情况时,如果 M 全为 0, 表示±无穷大

3.5 回看代码

1
2
3
4
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:% d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);

n 在内存中存放的是

1
00000000 00000000 00000000 00001001

对 pFloat 解引用时拿到的也是这串补码

但是 pFloat 是 float* 类型,解引用时应用的是浮点数存储的方式算数值,应该这么

1
0 00000000 00000000000000000001001

E 对应的全是 0 ,特殊情况, E在取出来时被指定为 -126, M 取出来时不再 +1 ,这里取出来的值是

1
0.1001 * 2 ^ -126

这是一个无限接近于 0 的数


1
2
3
*pFloat = 9.0;
printf("num的值为:% d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);

而这段代码, 是对 pFloat 解引用进行赋值,这是以浮点数在内存中的存储规则去进行的

9.0 的二进制表示形式是

1
2
3
4
1001.0

-1 ^ 0 * 1.001 * 2 ^ 3
S = 0, M = 1.001 , E = 3

在存放时 E + 127 = 130 存放, 存的是 10000010

故存放的是

1
0 10000010 00100000000000000000000

而从 n 的角度去看,适用的是整数在内存中的存储的规则,看到的是

1
01000001 00010000 00000000 00000000

1
2 ^ 30 + 2 ^ 24 + 2 ^  20