前言
调试观察内存的时候,总发现数据是倒着存的?
浮点数怎么和整形大不相同的感觉?
今天来揭下数据存储神秘的面纱
1.数据类型
回顾一下数据类型
- 整形:都有 signed & unsigned type
char - 字符类型
short - 短整形
int - 整形
long - 长整型
long long - 更长的整形
- 浮点数
float - 单精度浮点数
double - 双精度浮点数
- 构造类型
数组类型 - type array[ ]
结构体类型 - struct
枚举类型 - enum
联合类型 - union
- 指针类型:指针的类型决定了所指向的对象的类型,也决定了能访问多大内存空间
int* pi
char* pc
float* pf
…
- 空类型
void (无类型)
类型决定了:
- 为此数据开辟的内存空间有多大
- 编译器是如何看待这块内存空间
2. 整形在内存中的存储
*关于整形类型的数值范围:可以在“limits.h”中看到
奇怪,-10 怎么是个 " f6 ff ff ff " ?
2.1 原码、反码、补码
其实,对于整形的存储,有原码、反码、补码的概念
原码
直接将数值按照正负数的形式写成二进制
反码
符号位不变,对原码按位取反
补码
反码+1
- 正整数的原反补相同
没事儿为什么要搞出个原反补?便于计算:
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统
一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程
是相同的,不需要额外的硬件电路。
由上,可以知道内存中存储的是数据的补码
再倒回来看看上面的" f6 ff ff ff"
简单计算一下,就可以对应…
诶? 怎么 f6 的位置不对劲?
这里又牵扯到“大小端字节序”…
2.2 大小端字节序
我们已经知道:内存中,一个内存单元的大小为一个字节。而许多类型的数据所占空间都不止一个字节,因此产生一个问题:如何安排多个字节?
大端字节序(大端存储模式):地址由低到高,先存高位,后存低位
小端字节序(小端存储模式):地址由低到高,先存低位,后存高位
我用的 vs2019 编译器采用的就是 小端字节序 ,所以
地址由低到高,先存了 f6 ,再存 ff ff ff
来看一道百度面试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
int check_sys()
{
int i = 1;
//1:
//00 00 00 01
//01 00 00 00
char* pi = (char*)&i;
if (*pi)
return 1;
else
return 0;
}
int main()
{
int ret = check_sys();
if (ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
通过 char* 来达到只访问一个字节的目的,通过首个字节判断大小端
3. 浮点数在内存中的存储
*关于浮点数类型的数值范围,可以在“float.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 , 9.0 , 9 , 9.0
实际结果是什么呢?
怎么是这么个结果?
num 和 *pfloat 完全是一个数,怎么打印出来相差这么多?
对了,类型决定了编译器看待内存空间的视角!那原因肯定出在 整形和浮点型的解读规则不一样!
必须了解浮点数存储规则才行…
3.1 浮点数存储规则
根据国际标准 IEEE(电气和电子工程协会)754,任意的二进制浮点数 V 都可以表示成同一种形式:
(-1)^S * M * 2^E
(-1)^S 代表符号位 ; M 代表有效数字 ; E 代表指数位
S 只占用1个比特位,当 S = 0,V为正数;反之为负数
M 占用23个比特位, 1 <= M < 2(有时也是0.xxxx)
E 占用8个比特位,E是 size_t 类型的
- float存储模型
- double存储模型
也没什么区别,不过是
S(1bit) -> E(11bit) -> M(52bit)
举个例子:十进制的 5.0 ,二进制 101.0 是正的, 也是 (-1)^0 * 1.01 * 2^2
S=0 , M=1.01, E=2
3.2 M & E
3.2.1 有效数字 M
上面提到, 1 <= m < 2 ,也就是 M 都是写成 1.xxxxx 的形式,其中xxxxx表示小数部分
但在 IEEE 754 中规定,存储M的时候,总是舍去第一位的1(因为1.xxx中,1总是不变的),等到读取的时候再补上。
这样做可以节省一位有效数字
3.2.2 指数 E
E作为 size_t(unsigned int)类型,实际存储要加上中间值127/1023来避免负数,当E为全0/全1时 也要单独讨论
- 中间值
小数的指数,可能为负,如0.5 = 0.1 = 1.0 * 2^-1,而且E又是unsigned int >=0 ,为了放得进去,我们通常加上一个中间值 127/1023 (8位的E/11位的E),读取的时候又减掉中间值
- 当E不为全0或不为全1
指数E的计算值(原E+中间值),减去中间值(127/1023) , 得到真实值 , 有效数字M前补上第一位的1
- 当E为全0
1-中间值 即为真实值 , 有效数字M 前 ,也不再补1 , 而是补 0
为什么?想想:原E + 中间值 = 0 ,即是说 原E = -127 ,1.xxxxxx * 2^-127 ,可以视作无穷小了 , 前面补上0 ,可以更好地表示无限接近0的数
- 当E为全1
如果 有效数字M 为全0 ,则表示±∞
了解了浮点数的存储规则,再回头分析前面的题目,才有下手的地方
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);//(1)
printf("*pFloat的值为:%f\n",*pFloat);//(2)
//0 00000000 0000000 00000000 00001001
//S=0 M=0000000 00000000 00001001 E = -126
//+ 0000000 00000000 00001001 * 2^-126
//E为全0,非常接近于0 ,用十进制表示就是 0.000000
*pFloat = 9.0;
printf("num的值为:%d\n",n);//(3)
//浮点数n的二进制序列
//0 10000010 0010000 00000000 00000000
//S=0 E=3+127=130 M=001 (1暂时丢掉)
//这个二进制序列以 "%d" 的格式打印 就是1091567616
printf("*pFloat的值为:%f\n",*pFloat);//(4)
return 0;
}
分析:
(1):以 “%d” 的形式打印一个十进制整形,类型和格式匹配,没毛病
(2):这里是把 9(int) 看成 float , 也就是说,编译器会把“整形9”的二进制序列,当作浮点数的二进制序列分析
(3):把将 9.0 以浮点数规则存进去,再用"%d"格式打印
(4):类型和格式匹配,没毛病
可能误解的地方:整形9的 1001 怎么前面一堆0,而浮点9的(1)001 却在后面补0
- 整形9 1001 前面都是0 ,是因为它作为整形9的二进制序列本来就是这样,此时的序列不是我们通过浮点数存储规则分析出来的
- 浮点9的二进制序列,是我们自己分析出来的,而浮点9在后面补0,才能不影响数值,
1.001 * 2^3 才对,1.000000…0001*2^3 当然就不对了
补充:printf 打印的是原码哦,如果这里是 -9 ,打印时要转成原码打印
今天的分享就到这里,培根的blog,与你共同进步!