计算机的运算方法
C语言中的基本数据类型有:
- 整型(short、int、long、char)
- 浮点型(float、double)
- 指针
考虑到指针的本质是无符号整型,于是归根结底来说就两个类型:整型和浮点型
-
整型
存储:
这个环节以4bit的有符号整型为例
现今计算机中的int几乎全部以补码形式存储,是因为补码自有他的精妙所在,为了阐述这种精妙,需要引入原码,反码的概念,但这两个东西是已经被淘汰掉的存储形式,因此没必要记忆它们,目前来看,原码补码主要两个用处:一是做卷子用,二是辅助理解补码。
上面是4bit的有符号整型【-8~7】的原码反码和补码。
首先简单粗暴地给出三者的变换规则(考试用):
由于众所周知的原因,这里只有负数的变换
- 【原码】->【反码】:符号位不动,数值部分按位取反
- 【反码】->【补码】:符号位不动,数值部分末位加一
- 【原码】->【补码】:符号位不动,数值部分取反再加一,但是这里还有一个超近道的方法,就是原码符号位依旧不动,数值部分从右往左遇到第一个1,这个1及其右边的位保持不动,左边全部取反。
发展沿革:
这个环节以8bit的有符号整型为例
关于具体的三种编码的出处没有考究到,但应该是按原码、反码、补码的顺序发展的。
背后的逻辑(并没按这个逻辑讲)
所有编码系统的设计,都在追求连续性和唯一性。
原码,反码和补码的演化,就在不断提高整数编码的这两方面性能。
原码:
那么随着计算机的发展,人们发现要去处理一些负数的计算,怎么办,就拿着计算机的第一位做符号位呗!在原码的规则下编码映射是这样的:
原码的优点:
- 计算机可以表示负数了!
- 从人的视角看编码代表的真值很方便,但是这样的一种映射符号位和数值部分是隔离的,
原码的缺点:
-
计算机运算很不方便!这样的编码下符号位和数值部分是隔离的,想象一下5+(-3),计算机先要看二者是异号,然后看二者绝对值,谁大结果采取谁的符号,最后做运算。这一个加法的硬件设计将会很麻烦。
能不能让符号位也像普通数值位一样一起参与运算?
-
有如图所示的有两个编码都映射到0
-
一些临界点运算会出错(后面将展示)
反码:
为了给底层的加法运算简化硬件设计,有了补码,其映射关系是这样的:
反码怎么来的,就是原码的数值部分按位取反,
反码的优点:
- 解决了最重要的加法硬件设计复杂(或者依赖减法器)的问题,现在的加法器只要无脑相加就能得到正确的结果,不用再单独考虑符号位了。
反码的缺点:
- 但是!没有解决真值0的重复映射问题
- 在一些临界点运算还会出错(后面将展示)
补码
结局是补码解决了后两个问题:
从此加法器只要开足马力求和就好了,编码的精妙使得硬件很容易实现。但是即便是补码也还是会有溢出问题,但是!溢出是程序员的错,不是CPU的错。
前面三种方法在处理临界点数据的表现:
红色代表错了,蓝色正确
补码的精妙:
仍以8bit有符号整型为例
历史为什么会选择补码?当然是它解决了以上的种种问题,现在问题是为何它就能解决呢?
-
第一:0的重复映射问题
在补码的编码下,原来的0x80被映射给了-128,这样2^8个编码有各自的映射对象。
-
第二:为什么编码加减结果正好可以代表对应数的计算结果
补码编码精髓在于对负数的处理,负数的补码被定义成
2^n+x
这样对于任意的
a,c,(a>0,c>0)
考虑a-c- a>c结果肯定是正数 a-c=a+(2^n-c) mod 2^n=a-c 没问题
- a<c结果肯定是负数 a-c=a+(2^n-c) mod 2n=2n+(a-c) 没问题,结果正好就是a-c的补码
-
第三:为什么反码不能解决这两个问题
负数的反码是对x<0,其补码为2^n-1+x
同样对于任意的
a,c,(a>0,c>0)
考虑a-c- a>c结果肯定是正数 a-c=a+(2^n-1-c) mod 2^n=a-c -1 这显然不行
- a<c结果肯定是负数 a-c=a+(2^n-1-c) mod 2n=2n-1+(a-c) 没问题,这种情况下是正确的
- 当然容易知道a=c时也是正确的
补充:
余数定理:
如果a ≡ b (mod m),c ≡ d (mod m) 那么:
(1)a ± c ≡ b ± d (mod m)
(2)a * c ≡ b * d (mod m)
补码的映射关系如图:
小结:int的情况只不过时数量上的扩充,至于无符号数就是跟钟表完全一样的循环转圈
IEEE754格式
日常开发中不可能只用整型,肯定有使用小数的场景,给你一个32bit的位置,可以用差不多40亿个不同的数编码,那么如何映射这些编码映射的范围更广呢?答案是采用浮点数格式,也即
这就是现行IEEE754标准进行对单精度浮点数的定义:
- 一个符号位是刚需
- 8是指数的存储空间,这个决定了浮点数上下所能达到的范围
- 23是尾数(有效数)的空间,这个决定了浮点数的精度
- 8和23是精度和范围权衡的结果
这样的一个32位编码映射到的小数是这样的:
补充概念:
移码:对 X 加上一个常数 2^(n-1),也就是上面的Bias,把 X 本身转换为一个正数,再以正数编码。
练习:
-
用IEEE754单精度表示-0.75(10进制)
-
化为二进制小数:-0.11
-
用规格化科学记数:-1.1X2^(-1)
-
对比上面的式子写出三个部分的值
- S=1
- Fraction=0.10000000000000000000000(小数点后23位)
- Exponent=-1+bias=-1+127=126=01111110(二进制)
-
一个萝卜一个坑
-
-
二进制转十进制浮点数
0xc0a00000
- 写成二进制并按格式拆分:1 10000001 0100000000000000000000
- 写出三部分的值
- S=1
- Fraction=0.25
- Exponent=129
- 计算十进制浮点数
- -1.25*2^2=-5
为什么偏移是127?为什么要把指数偏移一下?
浮点数运算
练习:
0.5和-0.4375相加(使用4位精度)
先将二者化为规格化科学记数法:0.5=1.000x2^-1
-0.4375=-1.110x2^-2
- 将最小指数的数的有效位进行右移,直到其指数和较大数匹配:
-1.110x2^-2=-0.111x2^-1
- 将有效数相加
-0.111x2^-1 + 1.000x2^-1 = 0.001x2^-1
- 将和规格化并检查上溢和下溢
0.001x2^-1 = 1.000x2^-4
- 舍入和 这里不需要舍入
1.000x2^-4 =0.0625(10进制)
0.5和-0.4375相乘(使用4位精度)
- 将不带偏阶的指数相加 (-1)+(-2)= (-3)
- 有效数相乘:1.000 *1.110=1.110x2^-3
- 检查有效数的积是否规格化,这里是的;检查指数是否上溢和下溢,这里127>-3>-126,没问题
- 对积舍入
- 定符号 结果-0.21875
总结:
- int的范围是
-2^31~2^31-1
,两个特殊点是0xffffffff=-1,0x80000000=-2^31
- float的可表示范围是
参考资料: