整数在内存中的存储
我们来回顾一下,整数在计算机是以补码的形式进行存储的,整数分为正整数和负整数,正整数的原码、反码和补码是一样的,负整数的原码、反码和补码略有不同(反码是原码除符号位,其他位按位取反(即0变成1,1变成0),补码就是反码+1),负整数的补码变为反码也就是一样的,先+1,保留符号位,其他位按位取反就可以得到原码。
在计算机储存数据的时候,会有一个符号位(最开头的数位用1来表示负数,用0来表示正数)来表示正负,剩下的是数值位来表示数值。正数的原码、反码和补码是完全一样的,这里注意符号位是不会变的,负数的反码就是除了原码的符号位不变之外,其他位按位取反(就是0变成1,1变成0),补码就是在反码的基础上加1
这里我们以32位机器来举例:
大小端字节序
我们来引入一个例子
大家是不是看到存储的数据是倒过来的,当数据超过一个字节的时候就会出现字节存储的顺序问题,这就是大小端字节序存储的问题。
解释
大端字节序就是数据的高位存到低地址处,低位存到高地址处。
小端字节序就是数据的低位存到低地址处,高位存到高地址处。
我们以从左到右是从低地址到高地址进行增加为例:
由此可见,开始我们引入的例子是按小端存储的,我是用VS2022来演示的。
练习
问题一
设计代码来判断当前机器的字节序
我们可以使用一个数字来进行判断,这里我使用数字1,数字1的十六进制表示为0x00000001,我们只要拿出第一个字节就可以判断大小端存储了,如果拿出的是0,就是大端字节序存储,如果拿出的是1,那就是小端字节序存储。
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char*)&i);
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("⼩端\n");
}
else
{
printf("⼤端\n");
}
return 0;
}
代码分析题
代码一
#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;
}
我们来分析一下这段代码,首先char有没有符号取决于编译器的,我这里使用的是VS2022,char是有符号的,所以a和b 就会打印-1,但是c 就不一样,首先-1在内存中的原码是10000001,反码是11111110,补码是11111111,由于 c 是无符号的,所以在内存中的11111111的第一个1就不会解读为符号位,而是正常的数位,所以打印255。
来看一下运行结果:
代码二
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
首先 -128 写出二进制为1000 0000 0000 0000 0000 0000 1000 0000,反码为1111 1111 1111 1111 1111 1111 0111 1111,补码为1111 1111 1111 1111 1111 1111 1000 0000,由于是char 类型,所以发生截断,以八个比特位来进行保存,就是1000 0000,以%u的形式打印,%u是无符号的整型(就是unsigned int 类型),所以发生整型提升,就变成1111 1111 1111 1111 1111 1111 1000 0000,这个数字我们拿计算机算一下,就会得出
来看一下运行结果:
代码三
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
128 的原码是 0000 0000 0000 0000 0000 0000 1000 0000,由于是整数,所以原码反码补码是一样的。由于是char 类型发生截断,保留八个比特位,就是1000 0000,用%u来进行打印,发生整型提升,由于char 是有符号的,所以提升为1111 1111 1111 1111 1111 1111 1000 0000 ,然后打印,由于是无符号的整数打印,其实结果和上一道题目的结果是一样的。
代码四
#include <stdio.h>
#include <string.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
这个我们就要思考 char 是有取值范围的,-128~127,一共有256个数字,strlen遇到\0,就会停止计算,这里的a从-1开始存储,当a变为-128时,再减一就会变成127,一直减到0,之后减一变为-1,依次类推形成一个循环结构,就如下图所示,所以,strlen(a)的计算结果应该是0之前的255个数。
来验证一下:
代码五
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
这个代码还是一样,拿出我们的循环图:
当 i 从 1 开始,加到255时,再加1 就不是256,而是255,所以不会跳出循环,而是一个死循环。
代码六
#include <stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
这个代码其实不用画循环图也能做,因为unsigned int 是无符号的整数,i 永远大于等于0,所以这还是一个死循环。
代码七
#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;
}
先看ptr1:
&a取出的是整个数组的地址,&a+1是跳过整个数组,ptr1[-1] = (ptr+(-1)),由于prt1是int类型的,所以ptr1-1就会往后移动一个int,解引用就是指向 4
ptr2:
由于是小端字节序存储,我们来画一下内存图:
由于a 是数组首元素的地址,所以 a 其实指向 1 ,由于(int) a 强制类型将a转换为int类型,+1后就是a的地址+1,指向01后面的00,由于ptr2是int*类型,搜易解引用就会取出四个字节的数据,也就是0x02 00 00 00
浮点数在内存中的存储
首先我们要知道浮点数的存储和整数的存储是不一样的,不信的可以看一下下面的运行截图:
现在我们来探讨一下浮点数在内存中是如何存储的:
由于浮点数的存储比较复杂,为了方便大家快速了解,我对此进行了一下总结:
首先,一个公式:(-1)^S * M * 2^E, 浮点数也是有符号的,所以我们用S来控制(-1)的几次方进而也就控制了浮点数的正负性,然后就是我们将浮点数化成类似科学计数法的形式(在二进制里,我们化成1.xxxx*2^E), 之后我们去掉1留下的来的xxxxx就是M,而E的处理就有点复杂,因为E可能为负数,所以为了避免负数的情况我们将其加上一个中间数(127或者1023)
注意了:浮点数的二进制表示,如0.5,怎么表示小数部分,要注意小数点后面浮点数的二进制权重是从2(-1)、2(-2)、2(-3)…依次叠加的,所以我们要凑出0.5,也就是1,因为1*2(-1)就是0.5
我们来看一下32位的float和64位的double存储情形:
E加中间数也是根据浮点数的类型进行加的,32的float加127,64位的double 加1023
正常情况下:
E不为全0或全1,那就是正常处理,减去中间数,取出S得到正负,取出M再加1得到一个1.xxxx,然后通过公式(-1)^S * M * 2^E
特殊情况:
当E位全0:
当E为全0的时候,在float下,E就是-127;在 double 下,E就是-1023,那么2^E就是一个很小的数,接近0
当E为全1:
当E为全1时,E就是一个很大的数,2^E就会使这个浮点数接近无穷大。
现在我们来分析一下开头的例子:
#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("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
9 的原码是0000 0000 0000 0000 0000 0000 0000 1001
32位的float,S是0,E是取八位 0000 0000 ,M是000 0000 0000 0000 0000 1001,由于E为全0,所以浮点数无限接近0。
9.0 用浮点数的公式表示:先算出M来,9.0 转化为二进制 1001.0,变为科学计数法就是1.0010 * 2^3 ,则M 为001,后面凑0,知道凑满23位,E等于3+127=130,化成二进制就是
1000 0010 ,由于是整数,所以符号位取0。
所以在内存中9.0表示为:
0100 0001 0001 0000 0000 0000 0000 0000
由于要进行整数打印,将上面一串二进制直接翻译成整数,拿计算机一算:
结果就是我们打印的结果。