深入了解数据在内存中的存储
1、数据类型介绍
已了解的基本内置类型:
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
类型的意义:
1)使用这个类型开辟内存空间的大小(大小决定了适用范围)。
2)如何看待内存空间的视角(存储的数据代表什么)。
演示如下:
int main()
{
int a = 49;
char b = '1';
//调试>窗口>内存>内存1,输入&a,设置列显示4,内存会以4个字节为一行显示
//可以看到a在内存中储存如下
//0x00CFFEC8 31 00 00 00
//即内存中int类型数据代表十进制数49
//输入&b,可以看到b在内存中储存如下
//0x00CFFEBF 31 cc cc cc
//即内存中char类型数据的31代表字符‘1’
//穿插讲解:
//以上内存 0x00CFFEC8 和0x00DFFEBF 各自代表四个字节的起始地址
//十六进制数 C8 - BF = 8 ,即两个数字相差8个字节。
//在VS2019 中,两行定义变量中间会差8个字节。
//在前面调试技巧中,Debug版本和Release版本中提到数组循环进入死循环
//Debug版本进入死循环就是因为两个变量间差8个字节,而数组越界2个int类型
//2个int类型正好是8字节,所以其中的i被置0,进入死循环。
//而Release版本将这个隐患优化消除了,所以没有死循环。
return 0;
}
穿插讲解部分,为调试技巧中例子,具体代码如下:
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("haha\n");
}
//Debug 模式运行,可以运行,但死循环。
//Release 模式运行,可以运行,不死循环。
return 0;
}
//进入调试发现,arr[12] 地址和i 地址相同
//arr[12] = 0 即 i = 0,循环重新开始,致死循环
1.1、类型的基本归类
整形家族:
char(内存中存储的是ASCII码,ASCII码是整型)
unsigned char
signed char
short
unsigned short [ int ]
signed short [ int ]
int
unsigned int
signed int
long
unsigned long [ int ]
signed long [ int ]
注: unsigned 指无符号位,即只有正数,没有负数。
浮点数家族:
float
double
long double
构造类型(自定义类型):
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型:
int *pi
char *pc
float *pf
void *pv
空类型(无类型):
函数返回类型:void test()
函数参数:test(void)
指针类型:void* P
2、整形在内存中的存储
变量创建需要在内存中开辟空间,空间的大小根据不同的类型而定。
整型数表示的范围在 limits.h 中定义。
2.1、原码、反码、补码
计算机中的整数有三种表示方法,即原码、反码、补码。
三种表示方法均有符号位和数值位两部分,符号位都是用 0 表示“正”,用 1 表示“负”,正整数的原、反、补码都相同,而负整数的数值位三种表示方法各不相同。
1)原码:符号位为1,其余位按照正整数的形式翻译成二进制。
2)反码:符号位为1,其余位按位取反。
3)补码:符号位为1,其余位在反码基础上+1。
对于整型来说:数据存放内存中其实存放的是补码。
早期计算机计算负数值、减法等比较麻烦,后来人们发现,通过补码的形式可以将符号位和数值域统一,可以方便硬件设计。
演示如下:
int main()
{
int a = 10;
int b = -10;
//a 是十进制数10,转换成2进制
//00000000 00000000 00000000 00001010-原码、反码、补码
//b 是十进制数-10,转换成2进制
//10000000 00000000 00000000 00001010-原码
//11111111 11111111 11111111 11110101-反码
//11111111 11111111 11111111 11110110-补码
//好处1、可以表示负数
//计算机本身只能表示0 1 这两个正数,没法表示 负号 -
//通过补码的形式可以表示负数。
//好处2、方便计算减法。
// 计算机本身是没有减法运算的,但通过补码可以将减法转换成加法。
// 如10-10 = 10+(-10)
//00000000 00000000 00000000 00001010-补码,a = 10
//11111111 11111111 11111111 11110110-补码,b = -10
//相加时从地位往高位计算,即从右往左计算,可得
//00000000 00000000 00000000 00000000-补码,结果为0
//用其他数值测试,同样正确。
//好处3、方便硬件电路。
//原码转换成补码存储,提取的时候可以用同样的过程提取
//11111111 11111111 11111111 11110110-补码,b = -10
//10000000 00000000 00000000 00001001-补码的反码
//10000000 00000000 00000000 00001010-补码的反码+1,b=-10
//用其他数测试,同样正确。
//可见从原码到补码的计算过程,跟从补码到原码的计算过程一致。
//计算机计算时,通过硬件电路,则两者的硬件电路可以一致。
return 0;
}
其他数值测试步骤相同。
2.2、大小端介绍
计算机存储数据时分为大小端存储模式。
大端存储模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
小端存储模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
大小端存储模式的成因:
在计算机系统中,是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char类型外,还有16 bit的short类型,32 bit的int类型(要看具体的编译器)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在如何排列多个字节的问题。
因此,出现了大端存储模式和小端存储模式。
例如:
一个16 bit的short型数据X,在内存中的地址为 0x0010,X的值为0x1122,那么 0x11为高字节,0x22为低字节。
对于大端存储模式,就会将高字节的0x11放在低地址中,即地址0x0010中,将低字节的0x22放在高地址中,即地址0x0011中。
对于小端存储模式,就会将高字节的0x11放在高地址中,即地址0x0011中,将低字节的0x22放在低地址中,即地址0x0010中。
常用的X86结构式小端存储模式,而keil C51则为大端存储模式。很多ARM,DSP都是小端存储模式。有些ARM处理器可以由硬件选择是大端模式还是小端模式。
演示如下:
//VS2019 C语言Debug环境下
int main()
{
int a = 2;
int b = -2;
//数据在内存中以二进制存储,二进制表示
//00000000 00000000 00000000 00000010——补码 a= 2
//10000000 00000000 00000000 00000010——原码 b=-2
//11111111 11111111 11111111 11111101——反码 b=-2
//11111111 11111111 11111111 11111110——补码 b=-2
//为方便显示,内存窗口以16进制表示地址,十六进制表示
//0 0 0 0 0 0 0 2 ——补码 a= 2
//F F F F F F F E ——补码 b=-2
//调试>窗口>内存>内存1,输入&a,设置列显示4
//可以看到a在内存中储存如下
//0x0028FEC0 02 00 00 00
//可以看到b在内存中储存如下
//0x0028FEB4 fe ff ff ff
//可以看到,a代表的实际数字是 00 00 00 02
//而内存中a的存储是按 02 00 00 00顺序
//即内存中数据以字节单位按从低位到高位排序,这就是小端存储
//以b= -2 验证
//b代表的实际数字补码是FF FF FF FE
//而内存中b的存储按 FE FF FF FF顺序
//同样以字节为单位,从地位到高位排序
return 0;
}
验证大小端存储模式:
//判断是大端存储模式还是小端存储模式
int main()
{
int a = 1;
//a在内存中的数据应该是
//00000000 00000000 00000000 00000001
//二进制看起来比较麻烦,转化为十六进制表示为
//00 00 00 01
char* pa = (char*)&a; //取出整型数值a首地址pa。
//如果是大端存储,a的首地址上应该是 00
//如果是小端存储,a的首地址上应该是 01
if (*pa == 1)
{
printf("小端存储模式\n"); //打印结果 小端存储模式 。
}
else
{
printf("大端存储模式\n");
}
return 0;
}
3、浮点型在内存中的存储
浮点数包括:float、double、long double类型。
浮点数表示的范围在 float.h 中定义。
3.1、浮点数存储规则
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
V = (-1)^S * M * 2^E
(-1)^S 表示符号位,V为正数时,S = 0;V为负数时,S = 1.
M 表示有效数字,M ≥1,M<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。
按照上面V的格式,可得出 S = 1;M = 1.01;E = 2。
3.2、IEEE 754规定
3.2.1、对32位的浮点数(float类型),最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
3.2.2、对于64位的浮点数(double类型),最高位的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
3.2.3、有效数字M和指数E特别规定
1)有效数字M
由于M取值范围是 1≤M<2,即1.xxxxxxxx,其中xxxxxxxx表示小数部分。为了节省一位有效数字,规定小数点前面的 1 默认省略,读取的时候自动加1。
比如1.01保存的时候,有效数字 M = .01。(.是为了示意,实际不保存)
以32为浮点数为例,M只有23位,但是第一位的1默认省略后,可以等效为24有效数字。
2)指数E
指数E是一个无符号整数,即它的取值是全正的。但由于科学计数法中的指数会出现负数,为方便表示,IEEE754规定,所有数值的指数存入内存时,都加上中间数,对8位的E,加上127(对11位的E,加上1023)。
这样即使出现负指数,也能用正数表示。如 2^(-3),保存成32位浮点数时,指数E须保存成 -3+127=124,即0111 1100。同理,2^10,保存时指数E为10+127=137,即1000 1001。
3)关于有效数字M和指数E的例外情况1
当E全为0时,规定浮点数的指数E为-126,且有效数字M的小数点 . 前不加1,而是加0。这样是为了表示正±0,以及接近于0的很小的数字。
例如内存中二进制数 0000 0000 0000 0000 0000 0000 0000 1001,还原时。由于指数E所在的八位(第二位到第九位)全为0,所以还原时,数值V=(-1)^0 × 0.000 0000 0000 0000 0000 1001 × 2^(-126) = 1.001 ×2^(-146)。
显然,V是一个接近于0的很小的数字。
4)关于有效数字M和指数E的例外情况2
当E全为1,且有效数字M全为0时,表示±无穷大(正负取决于符号位S)。
演示如下:
//浮点型数在内存中存储验证
int main()
{
int a = 5; //定义整型变量 a = 5
//a在内存中存储为二进制,二进制表示为
//00000000 00000000 00000000 00000101
float* pa = (float*)&a; //定义浮点型指针*pa指向变量a所在的地址。
printf("%d\n", a);
//以有符号整型数的方式读取变量a的数值,打印结果 5 。
printf("%f\n", *pa);
//以浮点型数的方式读取地址pa中的数值,打印结果0.000000 。
//
//分析,a的二进制如下
//00000000 00000000 00000000 00000101
//以浮点型数的方式读取,先看符号位,即第一位 S = 0,正数
//再看指数位,即第二位到第九位,全为0,表示 E= -126。
//最后看有效数字位 M 。由于E全为0,则M读取时不在前面默认+1.
//即M = 0.0000000 00000000 00000101 ,是一个非常小的数字
//浮点数 v = (-1)^S * M * 2^E
//则*pa = (-1)^0 * 0.0000000 00000000 00000101 * 2^(-126) =1.01*2^(-147)
//因此,浮点型数*pa 的数值打印是0.000000 。
float b = 5.0; //定义浮点型变量 b = 5.0,二进制表示为 101 =(-1)^0 * 1.01 * 2^2
//b在内存中存储为二进制,二进制表示为
//0 10000001 0100000 00000000 00000000
//其中第一位0代表符号位0,第二位至第九位10000001 代表指数位 2+127
//第十位往后代表有效数字 (1).0100000 00000000 00000000 其中首位的1默认省略。
int* pb = (int*)&b; //定义整型指针*pb指向变量b所在的地址。
printf("%d\n", *pb);
//以整型数的方式读取地址pb中的数值,打印结果是1084227584 。
//分析,b的二进制如下
//01000000 10100000 00000000 00000000
//整型直接读取为 2^(30)+2^(23)+2^(21) = 1084227584 。
//因此,整型数*pb 的数值打印是 1084227584 。
printf("%f\n", b);
//以浮点型数的方式读取地址pb中的数值,打印结果是5.000000 。
return 0;
}