数据在内存中的存储
一、整数在内存中的存储
- 整数在内存中是以二进制的方式进行存储的。
- 整数的二进制表示方式有三种:原码、反码和补码。在计算机中存储的是整数的补码。其中有符号整数由符号位+数值位组成,最高位一位为符号位,剩余位是数值位。正数的符号位为0,负数的符号位为1。
- 正数的原反补都相同,负数:原码—>(取反)反码—>(+1)补码。
二、大小端字节序和字节序判断
整数在内存中的存储,存储的内容是整数的补码,存储方式有两种,一种是大端字节序存储,另一种是小端字节序存储。
2.1 什么是大小端
大端字节序
高位字节内容存储到高地址,地位字节内容存储到低地址。
小端字节序
高位字节内容存储到低地址,低位字节内容存储到高地址。
例如:
vs2022中使用的是msvc的编译器,支持的是小端字节序存储方式,因此我们想将一个十六进制的数字存储到计算机内存中时,计算机会将0x112233按照“高位字节内容存储到低地址,低位字节内容存储到高地址。”的存储规则进行存储。结果如下:
通过内存监视窗口,得到a的地址存放的内容,我们可以看到112233在内存中是反向存储,符合小端字节序存储。
2.2 判断大小端
写一个程序,判断编译器是大端存储还是小端存储。
char judge()
{
int i = 1;
return *((char*)&i);
}
int main()
{
char a = judge();
if (a == 1)
printf("小端");
else
printf("大端");
return 0;
}
注意,1在16进制内存下表示为0x00000001,如果是小端存储:01 00 00 00;如果是大端存储:00 00 00 01.因此我们只需要将第一个字节通过强制类型转换取出即可,如果是1则为小端,反之为大端。同理,可以将i的值换为其他符合条件的值。
2.3 为什么有大小端
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在C语⾔中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数大于8位的处理,例如16位或者32位的处理器,由于寄存器宽度大于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
2.4 练习
- 打印结果是什么
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;
}
答案:
int main()
{
char a = -1;
//-1:原码:10000000000000000000000000000001
// 补码:11111111111111111111111111111111
//a时char类型,只能存放8个字节,即11111111
signed char b = -1;
//同理,b中存放的也是11111111
unsigned char c = -1;
//同理,c中存放的也是11111111
printf("a=%d,b=%d,c=%d", a, b, c);
//因为打印占位符用的%d,即要求打印十进制的有符号整型,因此需要对char类型进行整型提升
//a是有符号char,最高位为符号位,因此补符号位:11111111111111111111111111111111,这是补码且为负数,因此取反加一得到的结果是-1
//b同理也是有符号char,打印结果为-1
//c是无符号char,没有符号位,因此整型提升时补0:00000000000000000000000011111111,当用%d打印时,将第一位看成符号位,因此是正数,补码=原码,即打印255
return 0;
}
- 打印的结果是什么
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
答案:
int main()
{
char a = -128;
//-128的原码是:10000000000000000000000010000000
// 补码是:11111111111111111111111110000000
//a是char类型,只能存8个bit位,即10000000
printf("%u\n", a);
//%u打印十进制无符号整数,需要对a进行整形提升
//因为a是有符号char,所以提升时补充符号位:11111111111111111111111110000000
//因为打印是无符号整型,因此直接打印整型提升后的数据,结果为4,294,967,168
return 0;
}
变式:
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
答案:
int main()
{
char a = 128;
//128的原码:00000000000000000000000010000000,补码也是这个
//a中存8个bit位,10000000
printf("%u\n", a);
//%u打印十进制无符号整数,需要对a进行整形提升,因为char是有符号char,所以补符号位:11111111111111111111111110000000
//打印结果为:4294967168(不变)
return 0;
}
- 打印结果是什么
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
答案:
上述代码实现向数组a中每个元素进行赋值。从下标为0的元素开始赋值-1,然后下标每+1,值每-1。看下图:
可以将char的取值范围想象成一个圆环,a数组从-1开始取值,沿圆环逆时针赋值,当赋值到0是一共255个元素,即整个char的取值范围(-128–127),所以strlen求出的a数组的长度为255。
- 打印结果是什么
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
答案:
无符号char的取值范围是0-255,所以i永远<=255,所以会一直打印hello world,陷入死循环。
变式:
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
答案:
无符号整型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;
}
答案:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
//ptr1指向数组末尾
int* ptr2 = (int*)((int)a + 1);
//ptr2指向数组第二个字节位置,整数+1就是单纯+1
printf("%x,%x", ptr1[-1], *ptr2);
//ptr1访问的数组倒数第一个元素,即4,%x打印4
//ptr2从第二个字节开始,向后访问4个字节,%x打印得到2000000
return 0;
}
三、浮点数在内存中的存储
常见的浮点数有小数(3.14)、科学计数法(1E10)等,浮点数类型包括:float,double,long double类型。浮点数类型所在的头文件:float.h
3.1 示例
下列代码会输出什么?
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;
}
答案:
这是为什么呢?——这是因为浮点数和整数在内存中的存储方式不同。
3.2 浮点数在内存中的存储
浮点数在内存中的表示方法:
- S表示该数字是否为正数,S=0表示正数,S=1表示负数。
- M表示实际存储的二级制位,1<= M <2。
- E表示该数据的指数位。
举例:
十进制的5.0表示为二进制:101.0,写成浮点数存储的方式:(-1)0 * 1.01 * 22
对于32位的浮点数来说,最高位表示符号位S,紧接着8位表示指数E,剩余23位表示实际存储的M。
对于64位的浮点数来说,最高位表示符号为E,紧接着11位表示指数E,剩余52位表示实际存储的M。
此外还有一些特别的规定:
- 对于M来说,M的值是大于等于1小于2的,因此小数点之前肯定是1,那么我们在存储时可以将小数点之前的1省略掉,然后多存储一位小数位,则我们可以存储小数点后24位。
- 对于E来说,既可能为正也可能为负,所以规定E的取值要加上一个中间数再进行存储,在32位下中间数是127,在64位下中间数是1023。
指数E在提取中会出现三种情况:
- E为全0:E为全0意味着在32(64)位下,E+127(1023)后大小为0,则规定E的真实值为1-127(1-1023)。
- E为全1:E为全1意味着在32(64)位下,表示±无穷大,符号位取决于S。
- E不为全0也不为全1:将E-127(1023)再正常计算即可。
3.3 回顾练习
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
//ptr1指向数组末尾
int* ptr2 = (int*)((int)a + 1);
//ptr2指向数组第二个字节位置,整数+1就是单纯+1
printf("%x,%x", ptr1[-1], *ptr2);
//ptr1访问的数组倒数第一个元素,即4,%x打印4
//ptr2从第二个字节开始,向后访问4个字节,%x打印得到2000000
return 0;
}
解析:
int main()
{
int n = 9;
float* pFloat = (float*)&n;
//9在32位下二进制表示:00000000000000000000000000001001
printf("n的值为:%d\n", n);
//n为整形,以%d打印直接打印有符号整形,得到9
printf("*pFloat的值为:%f\n", *pFloat);
//pFloat是一个浮点型指针,存储时按照浮点数进行存储
//0 00000000 00000000000000000001001
//S E M
//(-1)^0 * 00000000000000000001001 * 2^-126
//由于浮点数默认打印小数点后6位,因此得到值为0.000000
*pFloat = 9.0;
printf("n的值为:%d\n", n);
//9.0先写成二进制:1001.0
//写成存储标准形式为:(-1)^0 * 1.001 * 2^130
//所以内存中存储为01000001000100000000000000000000
//最高位0表示正数,补码=原码,直接读取得到1091567616
printf("*pFloat的值为:%f\n", *pFloat);
//浮点数的形式直接打印9.0,默认打印六位小数,9.000000
return 0;
}