数据在计算机中的存储形式

分类: Java |
一、数据概述
以C语言为例,里面所有的基本数据类型,都是以符合人类世界和自然世界的逻辑而出现的。比如说int,bool,float等等。这些数据类型出现的目的,是更于让人容易理解,可以说,这些数据类型是架通人类思维 与 计算机的桥梁。
我们知道。依照冯诺依曼体系,计算机中并没有这些int
二、进制
要想理解数据的存储,首先要明白最基本的二进制问题,因为,这是计算机中数据最基本的形式,首先看下面的问题:
1、什么是二进制?进制的概念?
2、计算机中为什么要用二进制?
3、二进制和符合人类思维的十进制之间的关系?
4、为什么又会出现八进制、十六进制?
5、所有进制之间的转换?
(1)、进制的概念
进制也就是进位制,是人们规定的一种进位方法。
对于任何一种进制---X进制,就表示某一位置上的数运算时是逢X进一位。
十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一
在采用进位计数的数字系统中,如果只用r个基本符号表示数值,则称为r进制(Radix-r Number System),r称为该数制的基数(Radix)。不同的数制的共同特点如下:
(1)、每一种数制都有笃定的符号集。例如,十进制数制的基本符号有十个:0,1,2。。。,9。二进制数制的基本符号有两个:0和1.
(2)、每一种数制都使用位置表示法。即处于不同位置的数符所代表的值不同,与它所在位的权值有关。
例如:十进制1234.55可表示为
1234.55=1×10^3+2×10^2+3×10^1+4×10^0+5×10^(-1)+5×10^(-2)
可以看出,各种进位计数制中权的值恰好是基础的某次幂。因此,对任何一种进位计数制表示的数都可以写成按权展开的多项式。
(2)、计算机中为什么要用二进制
电脑使用二进制是由它的实现机理决定的。我们可以这么理解:电脑的基层部件是由集成电路组成的,这些集成电路可以看成是一个个门电路组成,(当然事实上没有这么简单的)。
当计算机工作的时候,电路通电工作,于是每个输出端就有了电压。电压的高低通过模数转换即转换成了二进制:高电平是由1表示,低电平由0表示。也就是说将模拟电路转换成为数字电路。这里的高电平与低电平可以人为确定,一般地,2.5伏以下即为低电平,3.2伏以上为高电平
电子计算机能以极高速度进行信息处理和加工,包括数据处理和加工,而且有极大的信息存储能力。数据在计算机中以器件的物理状态表示,采用二进制数字系统,计算机处理所有的字符或符号也要用二进制编码来表示。用二进制的优点是容易表示,运算规则简单,节省设备。人们知道,具有两种稳定状态的元件(如晶体管的导通和截止,继电器的接通和断开,电脉冲电平的高低等)容易找到,而要找到具有10种稳定状态的元件来对应十进制的10个数就困难了
1)技术实现简单,计算机是由逻辑电路组成,逻辑电路通常只有两个状态,开关的接通与断开,这两种状态正好可以用“1”和“0”表示。
(3)、八进制和十六进制出现是为什么
人类一般思维方式是以十进制来表示的,而计算机则是二进制,但是对于编程人员来说,都是需要直接与计算器打交道的,如果给我们一大串的二进制数。比如说一个4个字节的int型的数据:0000 1010 1111 0101 1000 1111 11111 1111,我想任何程序员看到这样一大串的0、1都会很蛋疼。所以必须要有一种更加简洁灵活的方式来呈现这对数据了。
你也许会说,直接用十进制吧,如果是那样,就不能准确表达计算机思维方式了(二进制),所以,出现了八进制、十六进制,其实十六进制应用的更加广泛,就比如说上面的int型的数据,直接转换为八进制的话,32./3
余2 也就是说
所以说用十六进制表达二进制字符串无疑是最佳的方式,这就是八进制和十六进制出现的原因。
(4)、进制间的相互转换
三、数据的分类
学过编程知识的同学肯定知道,特别是面向对象的,数据类型一般分类基本数据类型
以C语言为例,基本数据类型包括,无符号整形,带符号整形,实型,char型,有朋友说还有bool,其实在C语言中bool类型也还是整形数据,只不过是用宏声明的而已,不明白的可以看这篇文章:http://blog.csdn.net/lonelyroamer/article/details/7671242
1、先看无符号整形
无符号整形在数据中的存储无疑是最方便的,因为没有符号位,只表示正数,所以在存储计算方面都很简单。无符号整形在就是以纯粹的二进制串存储在计算机中的。
比如说看下面的例子:
http://my.csdn.net/uploads/201206/17/1339925261_2091.png
从输出的十六进制数中可以看出,它就是以直接的二进制
数表示的。
2、在看带符号整形
对于带符号数,机器数的最高位是表示正、负号的符号位,其余位则表示数值。
先不谈其他的问题,只谈二进制表达数据的问题(我也不知道怎么说),看下面的例子:
假设机器字长为8的话:
一个十进制的带符号整形 1,表达为二进制就是 (0000 0001)
一个十进制的带符号整形 -1,表达为二进制就是 (1000 0001)
那么,两者相加 ,用十进制运算 1+(-1)=0
在看二进制运算
可以发现出问题了,如上所表示的方式,就是今天所要讲的原码。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
①、原码
数值X的原码记为[x]原,如果机器字长为n(即采用n个二进制位表示数据)。则最高位是符号位。0表示正号,1表示负号,其余的n-1位表示数值的绝对值。数值零的原码表示有两种形式:[+0]原=0000
0000
例子:若机器字长n等于8,则
[+1]原=0000
00001
[+127]原=0111 1111
[+45]原=0010
1101
可见,原码,在计算数值上出问题了,当然,你也可以实验下,原码在计算正数和正数的时候,它是一点问题都没有的,但是出现负数的时候就出现问题了。所以才会有我下面将的问题:反码
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
②、反码
数值X的反码记作[x]反,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的反码与原码相同,负数的反码则是其绝对值按位求反。数值0的反码表示有两种形式:[+0]反=0000
0000
例子:若机器字长n等于8,则
[+1]反=0000
00001
[+127]反=0111 1111
[+45]反=0010
1101
在看反码计算的问题:
1+(-1)=0
1+(-2)=-1
-1+(2)=1
所以,看以看到,用反码表示,问题依然没有解决,所以,出现了下面的补码
③、补码
数值X的补码记作[x]补,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的补码与原码反码都相同,负数的补码则等于其反码的末尾加1。数值0的补码表示有唯一的编码:[+0]补=0000
0000
例子:若机器字长n等于8,则
[+1]补=0000
00001
[+127]补=0111 1111
[+45]补=0010
1101
在看补码计算的问题:
1+(-1)=0
1+(-2)=-1
-1+(2)=1
通过上面的计算,我们发现,用补码的方式,就不存在在原码和反码中存在的计算问题了。其实,这也是计算机表达带符号整数用补码的原因。如果,你觉得我举得例子太少,缺少代表行,你可以自己试试。不过,放心补码一定是不会存在原码和反码的问题的。
讨论下原码反码补码的原理,没兴趣的同学可以跳过
。不过我觉得从本质上了解补码的机制还是很有好处的。
1、为什么原码不行?
( 1
)
(00000001)原
并且,当负数加上负数时(这里就拿两个数值部分加起来不超过0111 1111的来说),我们可以明显看出符号位相加变为0,进位1被溢出。结果就是正数了。
因此原码的错误显而易见,是不能用在计算机中的。
2、补码的原理
既然原码并不能表示负数的运算问题,那么当然要另想他法了。这个方法就是补码,关于补码是如何提出的,我并不知道,但不得不说,这是一个最简洁的方法,当然,也可以用别的更复杂的方法,那就不是我们想要的了。
我自己研究补码的时候,也在网上找了些资料,都是到处copy,反正我是看的迷糊了,本人数学功底不怎么样,看不懂那些大神写的,只好,自己理解了下。
要谈补码,先看看补数的问题。什么是补数,举个简单的例子,100=25+75。100用数学来说就是模M,那么就可以这样概括。在M=100的情况下,25是75的补数。这就是补数。
25是75的补数,这是在常规世界中,在计算机上就不是这样了,因为,在计算机中,数据存在这溢出的问题。
假设机器字长是8的话,那么能表达的最大无符号数就是1111 11111,在加1的话,就变成1
也就是说,在计算机中,补数的概念稍微不同于数学之中,25+75=100,考略计算机中的溢出问题,那么25+75就等于0了。也就是说,25和75不是互为补数了。
我觉得用闹钟来比喻这个问题在形象不过了,因为闹钟也存在着溢出的问题,当时间到达11:59 ,在加1分钟的话就变成0:0了,这和计算机的溢出是同一个道理。
那么,有一个时钟,现在是0点,我想调到5点,有两种方法,一个是正着拨5,到5点。第二种方法是倒着拨7,也可以到5点。正着拨5记作+5,倒着拨7,记作-7,而闹钟的M是12,也就是说,在考略溢出的情况下,M=12,5是-7的补数。用个数学等式可以这样表达0+5=0+-7,即0+5=0-7
这就是计算机中的数值计算和数学中的计算不同的地方。
明白了计算机中补数的道理,那么就明白补码的问题了。还是用例子说明:
在计算机中计算十进制 1+(-2)。
1的原码是:0000 0001
-2的原码是:1000 0010
-2的补码是:1111 1110
你发现什么了没,当换成补码后,-2和254就是补数的关系。
也就是1+(-2)
这样做,好处在什么地方,你自己都可以看得到:
①、利用补数和溢出的原理,减法变成了加法
②、符号位不在是约束计算的问题,不会存在原码中的问题了,因为变成补码后,虽然最高位依然是1,但是这个1就不在是最为符号位了,而是作为一个普通的二进制位,参与运算了。
所以,这就是补码的原理所在,通过补数和溢出,解决了减法和负数问题。不知道各位理解了没有,额,反正我是通过这种方法安慰自己的,不知道是不是有失偏颇。
十进制数求补码,补码求十进制数
十进制求补码:
如果是正数,直接求它的原码,符号位为0
如果是负数,比较好的方法是先求十六进制,在由十六进制求二进制,符号位为1,在除了符号位都取反,在加1,即可得到补码。
补码就十进制 :
根据符号位判断,如果符号位是0,表示是正数,就是原码,直接转换就十进制即可。
如果符号为是1,表示是负数。那么,连符号位在内都取反,在加1,将该二进制转换为十进制,该十进制数即使该负数的绝对值,加个负号-,就得到该负数。
四、小数的存储形式
把浮点数刻意留在了后面来介绍。我的理解是在我们理解了内存,指针,位运算等后,再来介绍浮点这个特殊而又普通的数据类型比较好理解。浮点数和基本类型数据的存储差别比较大,这里不是说存储形式的差别,而是浮点数存放的时候是要经过运算后再转换成整数的4字节或8字节的形式,然后再存放到内存里。因此,只通过16进制数是看不出来和整数有什么差别。同样,浮点数具体是怎么存储的,在大学的课程上一般不会细细讲解,一般是我们自己有兴趣再查阅资料。包括本篇的内容,如果你不是一个自学者或者充满好奇心,你也不会看下去,也不会找到本篇的URL。因此,包括很多已经工作很多年的程序员都不知道浮点数具体是怎么运算然后存储的。就我来讲,认为还是非常有必要了解这个常用的数据类型的换算过程,虽然我们个人来讲很难去打破当前浮点数的计算规则以至于将他的精度提高,但是了解下底层工作者们的辛苦,我们应该向他们真诚的致敬。因为有他们,我们便有了大树可以乘凉。
好了,废话不多说。本篇的目的就是为了让更多的人了解浮点数存储的基本原理,还是那句话,学习的同时带着思考。同样这里不讨论浮点数的精度损失和数值的计算理论。直接讲实质的表现。
在计算机发展过程中,我们使用的小数和实数曾经提出过很多种的表示方法。典型的比如相对于浮点数的定点数(Fixed Point Number)。在这种表达方式中,小数点固定的位于实数所有数字中间的某个位置。货币的表达就可以使用这种方式,比如 88.22 或者 22.88 可以用于表达具有四位精度(Precision),小数点后有两位的货币值。由于小数点位置固定,所以可以直接用四位数值来表达相应的数值。SQL 中的 NUMBER 数据类型就是利用定点数来定义的。还有一种提议的表达方式为有理数表达方式,即用两个整数的比值来表达实数。
很显然,上面的定点数表示法有缺陷,不能表示很小的数或者很大的数。于是,为了解决这种问题,我们的前辈们自然想到了科学技术法的形式来表示,即用一个尾数(Mantissa
),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数。比如 123.456
用十进制科学计数法可以表达为 1.23456 × 102
大约就在1985年,IEEE标准754的推出,它是一个仔细制定的表示浮点数及其运算的标准。这项工作是从1976年Intel发起8087的设计开始的,8087是一种为8086处理器提供浮点支持的芯片,他们雇佣了William Kahan,加州大学伯克利分校的一位教授,作为帮助设计未来处理器浮点标准的顾问。他们支持Kahan加入一个IEEE资助的制订工业标准的委员会。这个委员会最终采纳了一个非常接近于Kahan为Intel设计的标准。目前,实际上所有的计算机够支持这个后来被称为IEEE浮点(IEEE floating point)的标准。这大大改善了科学应用程序在不同机器上的可移植性。所谓IEEE就是电器和电子工程师协会。
介绍完了历史,先来看看浮点数最直接的表示。在数学上:
12.341010
在比如二进制:
101.112
上面简单的描述了在数学意义上的浮点数表示,但是在计算机中,我们存放在内存中的直观上看16进制数,那么这些16进制数是怎么表示我们浮点数的二进制形式呢?
在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式:
float
double
我们都知道浮点数在32位机子上有两种精度,float占32位,double占64位。很多朋友喜欢把double用于8字节的数据存储。从这点我们应该不要特殊看到浮点数的内存存储形式,他跟整数没有什么区别,只是在这4字节或者8字节里有3个区域,整数有符号只有符号位及后面的数值,之所以最高位表示有符号数的符号位。原因之一在于0x7fffffff位最大整数,为整个32位所能表示的最大无符号整数0xffffffff的一半减一,也就是:比如1字节:无符号是:0xff,有符号正数为:(0, 127],负数为[-128, 0)。在8位有符号时,肯定内存值大于等于: 0x80。二进制就是1000 0000,比他大,只会在低7位上变化,最高位已经是1了,变了就变小了。所以这里也是一个比较巧用的地方,一举两得。
那么,我们先来看32位浮点数
1. 从浮点数到16进制数
float
就这个浮点数,我们一步一步将它转换为16进制数。
首先,整数部分5,4位二进制表示为:0101。
其次,小数部分0.2,我们应该学了小数转换为二进制的计算方法,那么就是依次乘以2,取整数部分作为二进制数,取小数部分继续乘以2,一直算到小数结果为0为止。那么对0.2进行计算:
0.2*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2(0.2)*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2 ... ...
因此,这里把0.2的二进制就计算出来了,结果就为:0.00110011... ... 这里的省略号是你没有办法计算完。二进制序列无限循环,没有到达结果为0的那一天。那么此时我们该怎么办?这里就得取到一定的二进制位数后停止计算,然后舍入。我们知道,float是32位,后面尾数的长度只能最大23位。因此,计算结束的时候,整数部分加上小数部分的二进制一共23位二进制。因此5.2的二进制表示就为:
101.00110011001100110011
一共23位。
此时,使用科学计数法表示,结果为:
1.0100110011001100110011 * 22
由于我们规定,使用二进制科学计数法后,小数点左边必须为1(肯定为1嘛,为0的话那不就是0.xxxx*sxxx
.01001100110011001100110
这里后面蓝色的0就是补上的,这里不是随便补的一个0,而是0.2的二进制在这一位上本来就应该为0,如果该为1,我们就得补上一个1.是不是这样多了一位后,实际上我们用23位表示了24位的数据量。有一个位是隐藏了,固定为1的。我们不必记录它。
但是,在对阶或向右规格化时,尾数要向右移位,这样被右移的尾数的低位部分会被丢掉,从而造成一定的误差,因此要进行舍入处理。
举个例子:
123.456的二进制表示:
123.456的二进制到23位时:111 1011.0111 0100 1011
1100
后面还有依次为01...等低位,由于最高位的1会被隐藏,向后扩展一位如果不做舍入操作则结果为:
1.11
1011
但是经过舍入操作后,由于被舍掉的位的最高位是1,或者“恒置1”法,最后面的0都应该是1。因此最终就应该是:
1.11
1011
在这里需要说明,不管是恒置1,还是0舍1入法,其根本都是为了减小误差。
好了,尾数在这里就计算好了,他就是
再来看阶数,这里我们知道是2^2次方,那么指数就是2。同样IEEE标准又规定了,因为中间的
float
中的
这里的阶码就是12910
因此,拼接起来后:
1000
0001
|
一共就是31位了,这里还差一位,那就是符号位,我们定义的是5.2,正数。因此这里最高位是0,1表示负数。
而后结果就是:
1位
|
到这里,我们内存里面的十六进制数产生了,分开来看:
0
因此,我们看到的就是0x40A66666, 此就是5.2最终的整数形式。
2.从十六进制数到浮点数
我们还是可以用上面5.2的例子,再将0x40A66666换算回去,用同样一个例子,结果更直观,逆运算更好理解。那我们就开始吧。
首先,要还原回去,必须将这个16进制用我们的计算器换算成二进制:
0
我是COPY上面的。这里颜色已经很明显了,我划分成了3个区域
首先确定符号,这里是0,因此是正数。
其次看绿色的8位,换成10进制就是:12910
我们逆运算,知道这里需要129 - 127 = 2得到指数,得到了指数,我们便知道我们小数点是向哪个方向移动了好多位。脑子里已经有了一个科学计数法的锥形。
再次把红色的23位提取出来,这里不把它换成10进制,因为我们指数是表示的二进制上移动了多少位,底数是2,而不是10。
这里因为之前我们都知道有个固定的1给省略了,因此这里要给加上去。加上去之后:
1
这里是24位,我们先不管,小数点添进去:
1
然后将科学计数法变换成普通的二进制小数:
1
到这里,就真正可以把整数部分换成十进制了:
1
我们知道了,整数部分是5,后面的小数部分再进行逆运算:
这里我们就应该想想小数到二进制数是乘法,这里逆运算就应该除以2,因此就可以表示为:
0
0 +
0*2-1
5.1999998。
因此我们可以看到精度已经有损失了。
问题一:写写-5.2的16进制数?
再来看一个例子:
float var = 0.5, 算16进制数。
首先,0.5整数部分为0,这里就不处理了。
其次,0.5小数部分,二进制表示为:0.1
这里是0.1,将尾数补满23位则是:
0.10 0000 0000 0000 0000 0002
由于小数点左边是0,因此需要向右移动一位
1.0 0000 0000 0000
0000 00002
这里1又被省略掉,所以23位全部变成了0
.00 0000 0000 0000
0000 00002
然后,因为这里指数是-1,因此阶码就是:-1 + 127 = 126 = 0111 11102
这样一来,阶码就有了,由于又是正数,那么组合起来:
0
这样一来,最终的16进制数则为:0x3f000000.
是不是很简单啊。
64位浮点数
这里就不再具体说明怎么换算的了,只需要提到2个地方:
一是,中间的阶码在double中占有11位,因此就不是+127了,而是加上1023,因为11位能表示的最大无符号数是2047,因此有符号范围[-1024, 1023]。
二是,尾数是52位,因此精度更高,能表示的数也就越大。我们在换算5.2的时候,后面的小数二进制+前面的5的二进制再省略一位后的总位数要填满52位。
好了,浮点数也没有太多要说的,就到这里吧,在用的时候注意精度和范围就可以了。
最后在提一个问题:
问题二:
float var0 =
5.2;
float var1 = 500.2;
float var2 = 50000.2;
float var3 = 5000000.2;
观察这几个数,加深一下那三个域的计算方式,并说出这些数据有什么规律?
本文参考:http://blog.csdn.net/jjj19891128/article/details/22945441