大家可能在学习的时候会经常疑惑数据在内存中是怎样存储的,今天用一篇博客给你讲清楚!!!从此不再疑惑!!!
文章目录
1. 整数在内存中的存储
整数的2进制表示方法有三种,即 原码、反码和补码。
有符号的整数,三种表示方法均有符号位和数值位两部分,符号位都是用0表⽰“正”,用1表示“负”,最⾼位的⼀位是被当做符号位,剩余的都是数值位。
正整数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
对于整型来说:数据存放内存中其实存放的是补码。
为什么呢?
2. 大小端字节序和字节序判断
我们来看一段代码:
int main()
{
int n = 0x11223344;//十六进制数字
return 0;
}
我们调试一下,在内存中观察一下:
我们可以看到,它在内存中是反着存的(以字节为单位存储,但是字节的存储是倒着的)。
这里进行几点说明:
那么为什么是倒着存储的呢?这个就和我们下面要讲到的大小端的问题有关了。
2.1 什么是大小端
- 那么我们是怎么理解高位节和低位节呢?
当前VS上使用的就是小端存储。
2.2 为什么有大小端
为什么会有大小端模式之分呢?
2.3 练习
2.3.1 练习1
-
题目:
请简述⼤端字节序和⼩端字节序的概念,设计⼀个⼩程序来判断当前机器的字节序。(10分)-百度笔试题。 -
思路:
- 代码:
#include<assert.h>
#include<stdio.h>
int judge(int* p)
{
assert(p);
if (*(char*)p == 1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int n = 1;
int ret = judge(&n);
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
- 代码优化:
#include<assert.h>
#include<stdio.h>
int judge(int* p)
{
assert(p);
return *(char*)p;
}
int main()
{
int n = 1;
int ret = judge(&n);
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
2.3.2 练习2
- 题目:
#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;
}
请问上面代码的输出结果是什么?
在解决这个题之前,我们大家要明确signed char(有符号的) 和 unsigned char(无符号的),这两个东西究竟是什么玩意儿?只有搞明白,我们才能正确做出这道题。
题目解析:
#include <stdio.h>
int main()
{
//这里说明一下,char 是有符号的char还是无符号的char是取决于编译器的
//在vs上char==signed char
char a = -1;
//10000000 00000000 00000000 00000001 - 原码
//11111111 11111111 11111111 11111110 - 反码
//11111111 11111111 11111111 11111111 - 补码
//但是由于我们定义的是 char 类型的,所以只存进去1个bit位,这个时候就会发生截断
//只存 11111111 - a
signed char b = -1;
//11111111 - b
unsigned char c = -1;
//11111111 - c
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}
注意: 上面写的补码都是在内存中存的补码,但是站在不同类型的数据往外面取的时候就不一定是这样了,我们接着往下看:
2.3.3 练习3
- 题目:
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);//以%u打印是无符号的打印
return 0;
}
我们来看这道题目的输出结果是什么?
- 题目解析:
#include <stdio.h>
int main()
{
char a = -128;
//100000000 - -128
//111111111 11111111 11111111 10000000 - 整型提升
//而%u又认为内存中存的是一个无符号数,
//正数的原码反码补码都相同,所以就会直接打印。
printf("%u\n", a);//以%u打印是无符号的打印
return 0;
}
- 代码结果:
题目:
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
我们再来看这道题目的输出结果是什么?
- 题目解析:
2.3.4 练习4
- 题目:
#include <stdio.h>
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
那么这个代码的结果又是什么呢?
- 题目解析:
2.3.5 练习5
- 题目:
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
- 题目解析:
- 题目:
#include <stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
- 题目解析:
2.3.6 练习6
- 题目:
#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;
}
同样的,这段代码的运行结果是什么?
- 题目解析:
3. 浮点数在内存中的存储
3.1 练习
接下来,我们先来看一段代码:
#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;
}
大家先来自己看一下这个代码运行的结果是多少,然后再往下看答案,感受一下自己的思路和正确的思路偏差在哪.
我们从上往下一行一行地先大概来分析一下这段代码。
#include <stdio.h>
int main()
{
int n = 9; //定义了一个int类型的常量n, 并赋值为9.
float* pFloat = (float*)&n;//取出了n的地址,强制类型转换成( float* )类型,紧接着将其赋给float*类型的指针变量pFloat。
printf("n的值为:%d\n", n);//以%d的形式打印n
printf("*pFloat的值为:%f\n", *pFloat);//以%f的形式打印*pFloat
*pFloat = 9.0;//通过指针间接访问n,并将其值改为9.0
printf("num的值为:%d\n", n);//以%d的形式打印n
printf("*pFloat的值为:%f\n", *pFloat);//以%f的形式打印*pFloat
return 0;
}
看到这里你是不是还以为答案是9,9.0,9,9.0.
其实并不是,我们来看一下运行结果:
看完答案,你是不是有很多疑惑,没关系,我们带着疑问往下走,会得到答案的,我们一点一点地分析到底为什么是这样的一个答案。
我们知道,&n是int类型的,占4个字节,强制类型转换成float类型的之后,也是占4个字节啊,我们在解引用的时候应该是恰好能访问它本来存储的4个字节的空间的呀!
那么按理说,答案就应该是我们上面所说的答案啊,可是为什么那不是正确答案呢?
那就说明我们的浮点数在内存中的存储并不是像我们想的那样,不是和整数一样的。
3.2 浮点数的存储
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
举例来说:
那么我们按照国际标准表示的5.5就是5.5 = (-1)^0 * 1.011 * 2 ^ 2(这样写不太好看,请看如下图)
在这里,S=0, M=1.011, E=2,因为所有的浮点数都能按照这个形式表示出来,只不过是不同的浮点数的S, M, E的值是不同的,而这个形式中的其他数据是不变的,那么就意味着,我们在存储浮点数的时候只需要将S, M, E存储好就行了,事实上,浮点数在内存中的存储,九四存储的S, M, E相关的值。
那么浮点数在内存中究竟是怎么存储的呢?
3.2.1 浮点数存的过程
IEEE 754 对有效数字M和指数E,还有⼀些特别的规定。
⾄于指数E,情况就比较复杂了
这里大家可能会有一个疑惑,如果这个负数加上中间数之后,还是一个负数该怎么办呢?
接下来,我们就还以5.5为例来看一下浮点数在内存中的存储究竟是怎样的。
int mian()
{
float f = 5.5;
//二进制表示:(-1)^0 * 1.011 * 2^2
//其中,S = 0, M = 1.011, E = 2
//S(1位)在内存中的存储:0
//E(8位)在内存中的存储:2+127(float类型) = 129, 10000001
//M(23位)在内存中的存储:01100000000000000000000(位数不够时,补0)
//所以浮点数5.5在内存中的存储是01000000101100000000000000000000
return 0;
}
那这个时候是不是感觉这个二进制太长了,看着不得劲,那么我们可以将其写成16进制的数字,会比较好看些。
//我们知道,一位16进制的数字对应的是4个2进制的数字。
//0100 0000 1011 0000 0000 0000 0000 0000
//4 0 B 0 0 0 0 0
//40 B0 00 00
我们来调试一下,看看内存中的存储方式是不是和我们说的一样。
我们在这里看到果真和我们写的一样(这里提示一下,大家看到内存中是反过来存储的,这是小端存储,不太了解的同学可以往这篇博客的上面翻翻,翻到最上面,看一下目录,就可以直接找到大小端的讲解)。
大家在举例子的时候可不敢乱举哈,你看完之后,特别兴奋,感觉自己又掌握了一个新知识,你就开始随便举例子,比如,你举了一个3.14这个浮点数,你会发现你用二进制表示的时候,无法精确的表示3.14,无论你怎么用小数点后的01来凑0.14,你可能都无法精确的凑出来。也就是说,如果小数点后的位太多,就可能导致浮点数在内存中无法精确的保存。所以,浮点数在内存中可能会出现这种情况,无法在内存中精确保存。
3.2.2 浮点数取的过程
前面介绍了浮点数存的过程,那么浮点数在取的时候又是怎样的呢?
常规情况下,就是按照存的相反过程往回返就可以了。
但是也有不常规的情况。在浮点数取的过程分为3种情况。
3.3 题目解析
下⾯,让我们回到上面的练习,来看解析吧,同志们!
int main()
{
int n = 9;
//00000000 00000000 00000000 00001001----9在内存中的二进制补码
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
//n是int类型的,又以%d的形式输出,这里输出9是没有任何问题的
printf("*pFloat的值为:%f\n", *pFloat);
//这里是以%f的形式打印的,那么站在pFloat的角度来看,
//它会认为自己指向的是一个float类型的数据,那么9在内存中的存储就是下面这样的了
// 0 00000000 00000000000000000001001
// S E M
//那么,这个时候我们要取出来这个数据,就是我们说的E全为0的情况,
//那么,它就是一个非常非常小的数字,是一个无限接近于0的数字
*pFloat = 9.0;
//将9.0这个浮点数通过指针变量pFloat间接访问n,pFloatde 的类型是float*类型的
//此时,pFloat是以浮点数的视角存储9.0的
//我们按照标准规定写出9.0
//(-1)^0 * 1.001 * 2^3
//此时,S=0, M=1.001, E=3
//0 10000010 00100000000000000000000
printf("num的值为:%d\n", n);
//此时,n已经被pFloat按照浮点数的视角存进去了,此时打印要以整数的的视角往回拿
//那么此时整数的视角的二进制序列就是下面这样的:
//01000001 00010000 00000000 00000000
//那么此时以%d的形式打印,%d表示的是有符号的,那么最高位就是符号位,最高位是0,是正数
//那么补码就是其原码,直接 打印出来就好了。
printf("*pFloat的值为:%f\n", *pFloat);
//前面我们说过存的时候是按照float的视角存进去的,那取的时候也是按照float取的话,
//那么,和存进去的时候是没差的,所以打印的就是9.0。
return 0;
}