数值系统
学习的时候不但要知其然,而且要知其所以然。
比如为什么要用这种表示方式?有什么好处?
二进制数表示方式
原、反、补码及其缺点和补码好处
原码
最初,人们直接使用二进制原码的第一位作为符号位,剩下的位为大小,例如:
2 ---> 0010
-2 ---> 1010
这样问题在于计算机做加法的时候需要额外判断两个数的正负,而且有+0和-0。
运算规则是: 若同号则直接相加,异号则用绝对值相减,最终根据大小决定正负。(实际和这个可能不一样,此处主要说明他算起来不方便)
反码
人们使用反码解决了需要额外判断正负号的问题。反码在原码的基础上,对负数的非符号位取反(注意:正数三种码都和原码一样),做加法的时候只需要直接开加即可,如果有负数最终要额外再加上一个1就是正确答案,依然有点麻烦,例如
-2 ---> 1010 //原码
-2 ---> 1101 //反码
-2 + 5 = 3
1101
+0101
+0001 //额外加一
=0011 ---> 3
-1 + -1 = -2
1110
1110
0001
1101 --->-2
缺点是仍然存在+0和-0,而且需要判断有没有负数
补码
反码基础上加一就是补码,补码码运算的时候不必加上最后的1,不用判断正负数。
补码还有好处是:运算结果连续,没有+0 -0。
至于他为什么可以直接相加得到最后的答案,用数学知识可以证明,此处略。
-2 + 5 = 3
1110
0101
0011 ---> 3
范围和溢出
讨论范围要分有符号和无符号
无符号
这个比较简单 $[0,2^k-1]$ ,如果发生溢出,会从头开始
//unsigned 4_bits
0000-1111
1111 + 0001 = 0000
有符号
正数,符号位占一位,所以范围只有: $[0, 2^{k - 1} - 1]$
负数,计算方式为 $-2^k + 剩下部分直接当正数计算$ 所以范围是 $[-1, -2^k]$
//负数举例
1111 = -2^4 + 7 = -9
1000 = -2^4 + 0 = -16
有符号的溢出,会变成异号,很好理解,最高位发生改变。
0111 + 1 = 1000 ---> 7 + 1 = -16
1000 - 1 = 1000 + 1111 = 0111 ---> 7
他们的范围可以看作一个圆,从0到正最大,再迈出一步到负最小,再继续增加到-1,0.
相当于把数轴两端粘到一起。
注意
C++中无符号和有符号运算的时候会发生一些自动转换,容易出错。例如
vector<int> nums;
//size()返回的size_t是无符号的,所以若size() == 0,会变得很大
for (int i = 0; i < nums.size() - 1)
浮点数
格式
第一位表示正负(s),一些位数表示小数值(f位),一些位数(e位)用于表示指数大小,off是用于偏移指数的值
$$x = (-1)^s * (1 + f) * 2 ^ {(e - off)}$$
注意上述公式中f
是小数部分的二进制位
e
是指数的真实大小,off
的作用是将指数范围都转到正数,使得e - off
指数这个整体是正数,不必另外处理指数的正负,最终存储的是e - off
的二进制位,举例:
假设我们有一个单精度浮点数:
0 10000010 11010000000000000000000
其中:
符号位:0,表示正数
指数位:10000010,偏移量为 127,所以真实指数值为 10000010 - 127 = -25
尾数位:11010000000000000000000
注意:尾数位计算应该倒着看,比如上面的二进制算十进制:
$$(-1)^0 * (1.1011)_2 * 2 ^ {(-25)} $$
为什么二进制浮点数不能精确表示某些十进制数字
从二进制转十进制的时候的算法就可以看出来
$$2^{-1} + 2^{-2} + ....$$
这样算必然不能表示所有的小数
为什么说浮点数的精度和范围不可兼得
可以从计算公式看出来:总位数固定,小数位越多,指数越少,那么范围就越小,反之亦然。
为什么不要用==比较
精度问题,一些小数不能精确表示,可能看起来相同,比较低的一些二进制位却有差别
if (0.1 + 0.2 == 0.3)
左边本身的表示误差加起来之后误差更大,就可能会不相等。
存储
大小端
指的是二进制如何存储。假设一个存储单位是8bits,直接举例
0x789abc
//假设左边低地址,右边高
//大端,高位存在地址低端
78 9a bc
//小端,低位存低端
bc 9a 78
写代码验证:
typedef unsigned char* pointer;
void show_bytes(pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf("%p\t0x%.2x\n", start + i, start[i]); //%.2x 宽度两位
printf("\n");
}