在基础数据类型这篇文章中,已经介绍过各种不同的数据类型,同时C语言也规定了这些数据类型的大小范围,那么这个范围又是怎么得到的,总不可能是随便定义的吧。理解了数据在内存中的存储,这个问题便不再是问题。
目录
整型存储
原码、反码和补码
隐式类型转换
浮点型存储
大小端存储
数据在内存中是以二进制的形式进行存储的,以整型为例,整型大小为4个byte也就是32个bit位,那么一个整型就可以用一个32位的2进制序列来表示,这个序列也被称为原码。
例如:5 的原码 - 00000000000000000000000000000101
在有符号数数据类型下,这个序列的最高位被称为符号位(正数为0,负数为1),而其余位均为数值位。那么-5的原码就可以表示为:10000000000000000000000000000101
原码虽然可以直接表示具体值,但数据在存储时并不是按照原码的形式而是按照补码的形式。
对于正数,原码、反码、补码相同,而对于负数则需单独计算。
反码:原码的符号位保持不变,其余位按位取反(1和0对调)
补码:反码+1。例如:
//-1: 10000000000000000000000000000001(原码)
// 11111111111111111111111111111110(反码)
// 11111111111111111111111111111111(补码)
为什么按照补码的形式来存储?
首先计算机是没有减法运算的,例如2 - 1 其实是转换为 2 + (-1)在进行加法运算的。如果用原码的方式来计算,很明显是没有办法得到正确答案的。为了能够在加法运算中引入负数,因而引入了补码,通过用补码的方式来计算,就可以解决这个问题:
//-1 11111111111111111111111111111111(补码)
//2: 00000000000000000000000000000010
// 100000000000000000000000000000001
虽然这种方法最后结果是33位,不过整型只能存放32个bit位,多出的部分发生截断,即得到的结果实际上是:00000000000000000000000000000001,也就是数字1。
而正数的原反补码相同的原因就是正数不需要转换形式,就能自然地参与加法运算。
因此补码可以看作是为了引入负数的运算。使用补码作为引入负数运算的工具还有一个原因是:
原码取反后+1可以得到补码,而补码取反后+1也可以的到原码(可能发生截断)。
当然截断也不只发生在整型溢出的情况下。在不同数据类型转换时截断发生的更频繁。
其实也就是之前的文章中已经提到了整型提升的,即:为了尽可能地保证精度,对于char型和short型,C语言会先将其转换成普通int型再使用。对于有符号数的整型提升,如果是负数则在前补1,如果是正数则在前补零。而对于无符号数,则统一在前面补零。例如:
char a = -1;
//变量a的二进制位(补码)中只有8个比特位:11111111
//而char在VS环境下为有符号型,如果整型提升则高位补符号位
//则整型提升后的结果为:11111111111111111111111111111111
char b = 1;
//变量b的二进制位(补码)中只有8个比特位:00000001
//则整型提升后的结果为:00000000000000000000000000000001
unsigned char c = -1;
//变量a的二进制位(补码)中只有8个比特位:11111111
//无符号数高位补零
//则整型提升后的结果为:00000000000000000000000011111111
整型提升的例子:
char a = -129;
printf("%d\n", a);
我们知道char型可以存放的最小值为-128,那么如果强行将-129放到char类型会有什么结果呢?有如何计算呢?
首先将-129写成二进制(补码):10000001,这是个有符号数,因此高位补符号位,整型提升后的结果为:11111111111111111111111110000001,发生截断->10000001,转换为原码->11111111,即127。当然,对于特殊情况10000000,C语言定义为-128。
浮点型的存储方法与整型大不相同,浮点型没有原码反码补码的概念,而另有一套方法存储:
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式: (-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
当然,在转换前仍要将其转换为二进制,例如:
十进制的3.5转换为二进制——> 11.1(1 * 2^1 + 1 * 2^0 + 1 * 2^(-1) = 2 + 1 + 0.5 = 3.5)
11.1 ——> 1.11 * 2^1
当然,某些小数是无法被二进制精确表示的,它只能无限的逼近,例如0.3,像这样的值会丢失一定的精度,这也是部分浮点数不能精确表示的原因。
这里3.5为正数因此S = 0,M = 1.11,E = 1
则最终转换结果为:(-1)^0 * 1.11 * 2^1
转换后的结果所包含的信息即为要存储的信息。
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
单精度浮点数float的存储方式如下:
在存储前我们发现,M的值始终在1~2之间,如果前面的整数1在存储时忽略,等到用时再加上,
也没有什么问题,因此M存储的实际上只有小数点后面的部分。对于1.11只存11,而后面多出的位置补零。
S的位置存0还是1只和符号有关,负数存1,正数存0。
E的部分比较复杂:
首先E是一个无符号整型,这意味着,如果E为8位,它表示的范围为0~255;如果E为11位,它表示的范围为0~2047。但是,我们 知道,科学计数法中的E是可以有负数出现的。
例如0.5->0.1->1.0 * 2^(-1)-> (-1)^0 * 1.0 * 2^(-1),这时E的值为-1
考虑到这些值,IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如说2^5,如果要保存为32位浮点数类型,这里的5要加上127以后即132才能存储到E的位置。
从内存中取出E时也能分出三种情况:
E中全为零:浮点数的指数E等于1-127(或者1-1023),有效数字M的前面不用+1,这样可以表示0或者无限逼近0的+-值。
E中全为1:如果有效数字M全为0,则表示无穷大。
其他情况:E的真实值为当前值-127(或-1023),在将有效数字M的前面+1。
那么3.5转换成二进制序列即为:0 10000000 11000000000000000000000
即 0100 0000 0110 0000 0000 0000 0000 0000转换成十六进制为:40 60 00 00
如果运行程序并观察:
结果的确如我们推算那样。
再看这段代码:
int main()
{
int n = 5;
float* pFloat = (float*)&n;
*pFloat = 5.0;
printf("%d\n", n);
return 0;
}
利用上面的方法可以求得5.0的二进制序列:01000000101000000000000000000000
说明我们的算法没有问题。不过我们在观察内存时发现内存中存放的值的顺序好像与我们求得的结果反过来了,这就涉及到计算机的存储方式了。
在C语言中,字节为单位,而除了char型以外,还有更大的short型、int型等,这些数据类型都不止一个字节,因此在存储时一定会存在顺序的问题。对于位数大于8位的处理器,也必然会产生顺序的问题。因此产生了两种存储模式:大端字节序存储模式和小端字节序存储模式。
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址 中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地 址中。
在我们常用的x86结构下是小端模式,而VS中的地址是从上往下变大,因此产生了“逆序”存放现象。