数据在计算机中的存储形式和运算( 原码,反码,补码)&=, |=, >>=,

2019-04-14 18:26发布

一、数据概述
以C语言为例,里面所有的基本数据类型,都是以符合人类世界和自然世界的逻辑而出现的。比如说int,bool,float等等。这些数据类型出现的目的,是更于让人容易理解,可以说,这些数据类型是架通人类思维 与 计算机的桥梁。 我们知道。依照冯诺依曼体系,计算机中并没有这些int  float等等,而全部都是0和1表示的二进制数据,并且计算器只能理解这些0和1的数据。所以说,所有的数据在计算机里面都是以0和1存储和运算的,这是冯诺依曼体系的基础。因此,符合我们人类思维的数据都要通过一定的转换才能被正确的存储到计算机中。
二、进制
要想理解数据的存储,首先要明白最基本的二进制问题,因为,这是计算机中数据最基本的形式,首先看下面的问题: 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”表示。   (2)简化运算规则:两个二进制数和、积运算组合各有三种,运算规则简单,有利于简化计算机内部结构,提高运算速度。   (3)适合逻辑运算:逻辑代数是逻辑运算的理论依据,二进制只有两个数码,正好与逻辑代数中的相吻合。   (4)易于进行转换,二进制与十进制数易于互相转换。   (5)用二进制表示数据具有抗干扰能力强,可靠性高等优点。因为每位数据只有高低两个状态,当受到一定程度的干扰时,仍能可靠地分辨出它是高还是低。
(3)、八进制和十六进制出现是为什么
人类一般思维方式是以十进制来表示的,而计算机则是二进制,但是对于编程人员来说,都是需要直接与计算器打交道的,如果给我们一大串的二进制数。比如说一个4个字节的int型的数据:0000 1010 1111 0101 1000 1111 11111 1111,我想任何程序员看到这样一大串的0、1都会很蛋疼。所以必须要有一种更加简洁灵活的方式来呈现这对数据了。 你也许会说,直接用十进制吧,如果是那样,就不能准确表达计算机思维方式了(二进制),所以,出现了八进制、十六进制,其实十六进制应用的更加广泛,就比如说上面的int型的数据,直接转换为八进制的话,32./3 余2 也就是说  ,我们还要在前面加0,但是转换为十六进制就不同了。32/4=8,直接写成十六进制的8个数值拼接的字符串,简单明了。 所以说用十六进制表达二进制字符串无疑是最佳的方式,这就是八进制和十六进制出现的原因。

(4)、进制间的相互转换问题
常用的进制有二进制、十进制、八进制和十六进制
①、八进制、十六进制、二进制-------------->十进制
都是按权展开的多项式相加得到十进制的结果。 比如 二进制1010.1到十进制:1×2^3  +  0×2^2  +  1×2^1  +  0×2^0  +  1×2^(-1)=10.5
八进制13.1到十进制:1×8^1  +  3×8^0  +  1×8^(-1)=11.125 十六进制13.1到十进制:1×16^1  +  3×16^0  +  1×16^(-1)=19.0625

②、十进制-------------->八进制、十六进制、二进制
都是按照整数部分除以基数(r)取余,小数部分乘以基数(r)取整 十进制10.25 到二进制:整数部分除2,一步步取余。小数部分乘2,一步步取整
八进制到十进制,十六进制到十进制都是和上面的一样,只不过不在是除2乘2,而是8或者16了,这是根据自己的基数来决定的。

③、二进制<------------->八进制、十六进制 二进制转换成八进制的方法是:从小数点起,把二进制数每三位分成一组,小数点前面的不够三位的前面加0,小数点后面的不够三位的后面加0,然后写出每一组的对应的十进制数,顺序排列起来就得到所要求的八进制数了。 依照同样的思想,将一个八进制数的每一位,按照十进制转换二进制的方法,变成用三位二进制表示的序列,然后按照顺序排列,就转换为二进制数了。 二进制数10101111.10111转换为八进制的数:(010   101   111.101  110)=  2   5   7.5  6=257.56 八进制数257.56转换为二进制的数:2   5  7.5  6  =(010   101   111.101   110)=10101111.101

二进制转换到十六进制差不多:从小数点起,把二进制数每四位分成一组,小数点前面的不够四位的前面加0,小数点后面的不够四位的后面加0,然后写出每一组的对应的十进制数,然后将大于9的数写成如下的形式,10---->A,11-->B,12---->C,13----->D,14----->E,15---->F,在顺序排列起来就得到所要求的十六进制了。
同样,将一个十六进制数的每一位,按照十进制转换二进制的方法,变成用四位二进制表示的序列,然后按照顺序排列,就转换为二进制数了。 二进制数  10101111.10111转换为十六进制的数:(1010   1111.1011 1000)=A  F.B  8=AF.B8 十六进制AF.B8转换为二进制:   A    F.B  8=(1010   1111.1011  1000)=10101111.10111

三、数据的分类
学过编程知识的同学肯定知道,特别是面向对象的,数据类型一般分类基本数据类型  和 复合数据类型。其实从本质上将,复合数据类型也是由基本数据类型构成的。所以,这里先只讨论基本数据类型的存储情况。 以C语言为例,基本数据类型包括,无符号整形,带符号整形,实型,char型,有朋友说还有bool,其实在C语言中bool类型也还是整形数据,只不过是用宏声明的而已,不明白的可以看这篇文章:http://blog.csdn.net/lonelyroamer/article/details/7671242

1、先看无符号整形 无符号整形在数据中的存储无疑是最方便的,因为没有符号位,只表示正数,所以在存储计算方面都很简单。无符号整形在就是以纯粹的二进制串存储在计算机中的。 比如说看下面的例子:
从输出的十六进制数中可以看出,它就是以直接的二进制
数表示的。
2、在看带符号整形
对于带符号数,机器数的最高位是表示正、负号的符号位,其余位则表示数值。 先不谈其他的问题,只谈二进制表达数据的问题(我也不知道怎么说),看下面的例子: 假设机器字长为8的话: 一个十进制的带符号整形 1,表达为二进制就是 (0000 0001) 一个十进制的带符号整形 -1,表达为二进制就是 (1000 0001)
那么,两者相加 ,用十进制运算 1+(-1)=0 在看二进制运算  (0000 0010)+(1000 0001)=(1000 0010)    这个数转换为十进制结果等于-2。 可以发现出问题了,如上所表示的方式,就是今天所要讲的原码。 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
①、原码 数值X的原码记为[x]原,如果机器字长为n(即采用n个二进制位表示数据)。则最高位是符号位。0表示正号,1表示负号,其余的n-1位表示数值的绝对值。数值零的原码表示有两种形式:[+0]原=0000 0000   ,-[0]原=1000 0000. 例子:若机器字长n等于8,则 [+1]原=0000 00001           [-1]原=1000 00001  
[+127]原=0111 1111          [-127]原=1111 1111 [+45]原=0010 1101           [-45]原=1010 1101     
可见,原码,在计算数值上出问题了,当然,你也可以实验下,原码在计算正数和正数的时候,它是一点问题都没有的,但是出现负数的时候就出现问题了。所以才会有我下面将的问题:反码
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
②、反码
数值X的反码记作[x]反,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的反码与原码相同,负数的反码则是其绝对值按位求反。数值0的反码表示有两种形式:[+0]反=0000 0000   ,-[0]反=1111 1111. 例子:若机器字长n等于8,则 [+1]反=0000 00001           [-1]反=1111 1110 
[+127]反=0111 1111          [-127]反=1000 0000
[+45]反=0010 1101           [-45]反=1101 0010  

在看反码计算的问题:
1+(-1)=0               |            (0000 0001)+(1111 1110)=(1111 1111)=(1000 0000)原=【-0】  可以看到,虽然是-0,但是问题还不是很大
1+(-2)=-1              |            (0000 0001)+(1111 1101)=(1111 1110)=(1000 0001)原=【-1】  可以看到,没有问题 -1+(2)=1              |            (1111 1110)+(0000 0010)=(0000 0000)=(0000 0000)原=【0】  可以看到,问题发生了,因为溢出,导致结果变为0了。 所以,看以看到,用反码表示,问题依然没有解决,所以,出现了下面的补码

③、补码
数值X的补码记作[x]补,如果机器字长为n,则最高位是符号位,0表示正号,1表示负号,正数的补码与原码反码都相同,负数的补码则等于其反码的末尾加1。数值0的补码表示有唯一的编码:[+0]补=0000 0000   ,-[0]补=0000 0000. 例子:若机器字长n等于8,则 [+1]补=0000 00001           [-1]补=1111 1111  
[+127]补=0111 1111          [-127]补=1000 0001
[+45]补=0010 1101           [-45]补=1101 0011   

在看补码计算的问题: 1+(-1)=0               |            (0000 0001)+(1111 1111)=(0000 0000)=(0000 0000)原=【0】  可以看到。没有问题
1+(-2)=-1              |            (0000 0001)+(1111 1110)=(1111 1111)=(1000 0001)原=【-1】  可以看到,没有问题 -1+(2)=1              |            (1111 1111)+(0000 0010)=(0000 0001) =(0000 0001)原=【1】  可以看到,没有问题
通过上面的计算,我们发现,用补码的方式,就不存在在原码和反码中存在的计算问题了。其实,这也是计算机表达带符号整数用补码的原因。如果,你觉得我举得例子太少,缺少代表行,你可以自己试试。不过,放心补码一定是不会存在原码和反码的问题的。
讨论下原码反码补码的原理,没兴趣的同学可以跳过 。不过我觉得从本质上了解补码的机制还是很有好处的。
1、为什么原码不行? ( 1 ) 10-  ( 1 )10 =  ( 1 )10 + ( -1 )10 =  ( 0 )10 (00000001)原 + (10000001)原 = (10000010)原 = ( -2 ) 显然不正确.
   通过上面原码计算式可以看出,当正数加上负数时,结果本应是正值,得到的却是负值(当然也有可能得到的是正数,因为被减数与减数相加数值超过0111 1111,即127,就会进位,从而进位使符号位加1变为0了,这时结果就是正的了)。而且数值部分还是被减数与减数的和。
并且,当负数加上负数时(这里就拿两个数值部分加起来不超过0111 1111的来说),我们可以明显看出符号位相加变为0,进位1被溢出。结果就是正数了。 因此原码的错误显而易见,是不能用在计算机中的。
2、补码的原理 既然原码并不能表示负数的运算问题,那么当然要另想他法了。这个方法就是补码,关于补码是如何提出的,我并不知道,但不得不说,这是一个最简洁的方法,当然,也可以用别的更复杂的方法,那就不是我们想要的了。 我自己研究补码的时候,也在网上找了些资料,都是到处copy,反正我是看的迷糊了,本人数学功底不怎么样,看不懂那些大神写的,只好,自己理解了下。
要谈补码,先看看补数的问题。什么是补数,举个简单的例子,100=25+75。100用数学来说就是模M,那么就可以这样概括。在M=100的情况下,25是75的补数。这就是补数。 25是75的补数,这是在常规世界中,在计算机上就不是这样了,因为,在计算机中,数据存在这溢出的问题。
假设机器字长是8的话,那么能表达的最大无符号数就是1111 11111,在加1的话,就变成1  0000  0000 ,此时因为溢出,所以1去掉,就变成0了,这个很简单,相信学计算机的人都会明白。 也就是说,在计算机中,补数的概念稍微不同于数学之中,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   这个二进制换做无符号的整数大小就是254,而8位二进制数的M=2^8=256。(很多文章中把M写成2^7,这根本就是不对的,根本没有解决符号位的问题) 你发现什么了没,当换成补码后,-2和254就是补数的关系。 也就是1+(-2)  等价于了 1+254了。 这样做,好处在什么地方,你自己都可以看得到: ①、利用补数和溢出的原理,减法变成了加法 ②、符号位不在是约束计算的问题,不会存在原码中的问题了,因为变成补码后,虽然最高位依然是1,但是这个1就不在是最为符号位了,而是作为一个普通的二进制位,参与运算了。

所以,这就是补码的原理所在,通过补数和溢出,解决了减法和负数问题。不知道各位理解了没有,额,反正我是通过这种方法安慰自己的,不知道是不是有失偏颇。
十进制数求补码,补码求十进制数                                                          十进制求补码: 如果是正数,直接求它的原码,符号位为0 如果是负数,比较好的方法是先求十六进制,在由十六进制求二进制,符号位为1,在除了符号位都取反,在加1,即可得到补码。 补码就十进制 : 根据符号位判断,如果符号位是0,表示是正数,就是原码,直接转换就十进制即可。 如果符号为是1,表示是负数。那么,连符号位在内都取反,在加1,将该二进制转换为十进制,该十进制数即使该负数的绝对值,加个负号-,就得到该负数。
3、在看小数存储的问题

四、位运算符
语言位运算符:与、或、异或、取反、左移和右移 位运算是指按二进制进行的运算。在系统软件中,常常需要处理二进制位的问题。C语言提供了6个位操作运算符。这些运算符只能用于整型操作数,即只能用于带符号或无符号的char,short,int与long类型。 C语言提供的位运算符列表
运算符 含义 描述
& 按位与 如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
| 按位或 两个相应的二进制位中只要有一个为1,该位的结果值为1
^ 按位异或 若参加运算的两个二进制位值相同则为0,否则为1
~ 取反 ~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1变0
<< 左移 用来将一个数的各二进制位全部左移N位,右补0
>> 右移 将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数,高位补0
1、“按位与”运算符(&)
    按位与是指:参加运算的两个数据,按二进制位进行“与”运算。如果两个相应的二进制位都为1,则该位的结果值为1;否则为0。这里的1可以理解为逻辑中的true,0可以理解为逻辑中的false。按位与其实与逻辑上“与”的运算规则一致。逻辑上的“与”,要求运算数全真,结果才为真。若,A=true,B=true,则A∩B=true 例如:3&5 3的二进制编码是11(2)。(为了区分十进制和其他进制,本文规定,凡是非十进制的数据均在数据后面加上括号,括号中注明其进制,二进制则标记为2)内存储存数据的基本单位是字节(Byte),一个字节由8个位(bit)所组成。位是用以描述电脑数据量的最小单位。二进制系统中,每个0或1就是一个位。将11(2)补足成一个字节,则是00000011(2)。5的二进制编码是101(2),将其补足成一个字节,则是00000101(2)
按位与运算:
00000011(2)
&00000101(2)
00000001(2)
由此可知3&5=1
c语言代码:
#include
main()
{
int a=3;
int b = 5;
printf("%d",a&b);
}
按位与的用途:
(1)清零
若想对一个存储单元清零,即使其全部二进制位为0,只要找一个二进制数,其中各个位符合一下条件: 原来的数中为1的位,新数中相应位为0。然后使二者进行&运算,即可达到清零目的。
例:原数为43,即00101011(2),另找一个数,设它为148,即10010100(2),将两者按位与运算:
00101011(2)
&10010100(2)
00000000(2)
c语言源代码:
#include
main()
{
int a=43;
int b = 148;
printf("%d",a&b);
}
(2)取一个数中某些指定位
若有一个整数a(2byte),想要取其中的低字节,只需要将a与8个1按位与即可。
a 00101100 10101100
b 00000000 11111111
c 00000000 10101100
(3)保留指定位:
与一个数进行“按位与”运算,此数在该位取1.
例如:有一数84,即01010100(2),想把其中从左边算起的第3,4,5,7,8位保留下来,运算如下:
01010100(2)
&00111011(2)
00010000(2)
即:a=84,b=59
    c=a&b=16
c语言源代码:
#include
main()
{
int a=84;
int b = 59;
printf("%d",a&b);
}
2、“按位或”运算符(|)
两个相应的二进制位中只要有一个为1,该位的结果值为1。借用逻辑学中或运算的话来说就是,一真为真 。
例如:60(8)|17(8),将八进制60与八进制17进行按位或运算。
00110000
|00001111
00111111 
c语言源代码:
#include
main()
{
int a=060;
int b = 017;
printf("%d",a|b);
}
应用:按位或运算常用来对一个数据的某些位定值为1。例如:如果想使一个数a的低4位改为1,则只需要将a与17(8)进行按位或运算即可。
3、交换两个值,不用临时变量
例如:a=3,即11(2);b=4,即100(2)。
想将a和b的值互换,可以用以下赋值语句实现:
    a=a∧b;
    b=b∧a;
    a=a∧b;
a=011(2)
    (∧)b=100(2)
a=111(2)(a∧b的结果,a已变成7)
    (∧)b=100(2)
b=011(2)(b∧a的结果,b已变成3)
    (∧)a=111(2)
a=100(2)(a∧b的结果,a已变成4)
等效于以下两步:
    ① 执行前两个赋值语句:“a=a∧b;”和“b=b∧a;”相当于b=b∧(a∧b)。
    ② 再执行第三个赋值语句: a=a∧b。由于a的值等于(a∧b),b的值等于(b∧a∧b), 因此,相当于a=a∧b∧b∧a∧b,即a的值等于a∧a∧b∧b∧b,等于b。
很神奇吧!
c语言源代码:
#include
main()
{
int a=3;
int b = 4;
a=a^b;
b=b^a;
a=a^b;
printf("a=%d b=%d",a,b);
}
4、“取反”运算符(~)
他是一元运算符,用于求整数的二进制反码,即分别将操作数各二进制位上的1变为0,0变为1。
例如:~77(8)
源代码:
#include
main()
{
int a=077;
printf("%d",~a);
}
5、左移运算符(<<) 左移运算符是用来将一个数的各二进制位左移若干位,移动的位数由右操作数指定(右操作数必须是非负 值),其右边空出的位用0填补,高位左移溢出则舍弃该高位。
例如:将a的二进制数左移2位,右边空出的位补0,左边溢出的位舍弃。若a=15,即00001111(2),左移2 位得00111100(2)。
源代码:
#include
main()
{
int a=15;
printf("%d",a<<2);
}
左移1位相当于该数乘以2,左移2位相当于该数乘以2*2=4,15<<2=60,即乘了4。但此结论只适用于该 数左移时被溢出舍弃的高位中不包含1的情况。
    假设以一个字节(8位)存一个整数,若a为无符号整型变量,则a=64时,左移一位时溢出的是0 ,而左移2位时,溢出的高位中包含1。
6、右移运算符(>>)
右移运算符是用来将一个数的各二进制位右移若干位,移动的位数由右操作数指定(右操作数必须是非负 值),移到右端的低位被舍弃,对于无符号数,高位补0。对于有符号数,某些机器将对左边空出的部分 用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。注 意:对无符号数,右移时左边高位移入0;对于有符号的值,如果原来符号位为0(该数为正),则左边也是移 入0。如果符号位原来为1(即负数),则左边移入0还是1,要取决于所用的计算机系统。有的系统移入0,有的 系统移入1。移入0的称为“逻辑移位”,即简单移位;移入1的称为“算术移位”。 
例: a的值是八进制数113755: 
   a:1001011111101101 (用二进制形式表示)
   a>>1: 0100101111110110 (逻辑右移时)
   a>>1: 1100101111110110 (算术右移时)
   在有些系统中,a>>1得八进制数045766,而在另一些系统上可能得到的是145766。Turbo C和其他一些C 编译采用的是算术右移,即对有符号数右移时,如果符号位原来为1,左面移入高位的是1。
源代码:
#include
main()
{
int a=0113755;
printf("%d",a>>1);
}
7、位运算赋值运算符 位运算符与赋值运算符可以组成复合赋值运算符。
   例如: &=, |=, >>=, <<=, ∧=
   例: a & = b相当于 a = a & b
         a << =2相当于a = a << 2