深度剖析数据在内存中的存储
一、数据类型详细介绍
这里我们仅介绍基本数据类型在内存中的存储方式、规则。
1.数据类型介绍
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
注意:C语言中没有字符串类型,我们常用数组来储存字符串。
2.类型的基本归类
二、整形在内存中的存储
1.借用调试观察
我们知道一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型决定的。
接下来我们以32位操作系统为设置,来探讨数据在内存中的存储。
我们定义一个int 型数字:
#include<stdio.h>
#include<stdlib.h>
int main(){
int num = 10;
system("pause");
return 0;
}
我们定义了一个变量,接下来我们用调试窗口来访问这个变量在内存中的存在形式:
启动调试->调试->窗口->内存
接下来我们会看到这样一个表格:
我们可以在内存中直接取地址来找变量:
输入&num
我来仔细梳理一下这个表格的意思:
因为num是int型,4个字节,在内存中存储方式就是:
int型在内存中是以4个字节为单位存储的,内存中用16进制表示。可以看到a的值为0a(16进制),换算成十进制就是10。
2.大小端字节序
大小端字节序是什么?
字节序就是研究数字的高位在内存的高地址还是低地址的问题。
在我们的日常规律中,高位放在低地址是主流,比如123,百位1在左边低地址,3作为个位在高地址。这个叫做大端字节序。
计算机存储中大多数是小端字节序,就是高位放在高地址。我们还是通过调试起来理解这个问题:
#include<stdio.h>
#include<stdlib.h>
int main(){
int num = 0x11223344;//直接写一个16进制,方便观察
system("pause");
return 0;
}
我们发现这里的表示方法并不是我定义的,而是反过来的,这就是因为大小端字节序的问题。
我们可以写一个简单的函数来判断我们机器的字节序是大端还是小端:
#include <stdio.h>
int isBigEnd(int a) {
int* p = &a;
char* p2 = (char*)p;
//此处是强制类型转换,仅输出一个字节的内容
if (*p2 == 0x11) {
printf("大端字节序");
return 1;
}
printf("小端字节序");
return 0;
}
int main()
{
int a = 0x11223344;
printf("%d\n", isBigEnd(a));
system("pause");
return 0;
}
3.整形在内存中的存储:原码、反码、补码
3.1原码、反码、补码
这三个形式都是由符号位+数字位构成(第一位是符号位,0为正,1为负)
原码:就是将数字直接转化为二进制的形式
反码:就是将原码符号位不变,其他的按位取反
补码=反码+1
是计算机的实际保存方式
我们依旧举一个例子来验证这些:
#include<stdio.h>
#include<stdlib.h>
int main(){
int num = -10;//负数
system("pause");
return 0;
}
我们的得到是:
f6 ff ff ff
已知我们的计算机是小端字节序所以真正的存储是:
ff ff ff f6
ff ff ff f6用二进制的方式表示:
1111 1111 1111 1111 1111 1111 1111 0110
-10的原码:
1000 0000 0000 0000 0000 0000 0000 1010
-10的反码:
1111 1111 1111 1111 1111 1111 1111 0101
-10的补码(反码+1):
1111 1111 1111 1111 1111 1111 1111 0110
ff ff ff f6的二进制表示和-10的补码一样
3.2补码的意义
我们可能会想问为什么是补码计算机的储存格式呢?
数据存放内存中其实存放的是补码在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
如果是正数,还是按照原码的方式存储
如果是负数,就按照补码的方式存储
补码存在的意义就是为了让硬件实现更简单。
把负数按照补码的格式来存储就会让-和+统一。
我们可以借助一个例子来理解:
三、浮点型在内存中的存储解析
1.浮点型数据的存储规则
我们先来看看专业的规定:
根据国际标准IEEE(电气和电子工程协会) ,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
说了很多的规定,总的就是以科学计数法表示。(只不过改成以2为底数)
S是符号位,0正 1负;
E指数部分;
M有效数字;
也就是± M*2^E
举个例子:
十进制的5.0,写成二进制是101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
我们可以分析到:
M占用的比特位越多,表示的数据精度就越高;
E占用的比特位越高,表示的数据范围就越大。
所以double无论是精度还是范围都优于float。
如下是float和double的存储模式:
IEEE 754规定: 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
2.浮点数的精度损失
使用浮点型经常会出现一个问题,就是得到的小数字往往不是准确值,而是近似值。因为很多无限循环的数字会精度损失。
我们依旧举个例子来看看这个精度损失的实际问题:
#include <stdio.h>
int main()
{
float a = 11.0;
float b = a / 3.0;
if (b * 3.0 == a) {
printf("相等!\n");
} else {
printf("不相等\n");
}
system("pause");
return 0;
}
我们知道11.0/3后在*3还是11,但是看运行的结果:
原因就是在做除法的时候数据太多导致溢出,从而导致数据只是近似值,而不是绝对值。
如果在工程项目中出现这种问题,一般是确定误差在一定范围内认为他们相等。