[读书笔记]信息的表示和处理
信息的存储
32位机器就是指他的地址可以用32位二进制数来表示,这里的32就是所谓的word size,同时也是一个指针的大小,64位机器同理。因为2的32次方等于4G,所以32位的进程最多只能有4G的内存。32位的程序由于向后兼容,可以在大多数64位机器上运行,反之则不行。32位程序或64位程序是在编译时决定的,而且在某些数据类型的长度上可能有所不同,比如用c语言编译器编译时指定-m32的参数,一个long就是4字节,如果指定m-64参数,一个long就是8字节。我们应该极力避免在代码中对某种特定长度的假设或依赖,增加可移植性。
内存可以看作一个很大的字节数组,其中每一个字节元素都有一个唯一的地址。我们约定:一个需要多个字节表示的程序对象的地址为它的所有字节中其中最小的地址。另一方面,关于这些组成这个对象的字节的顺序有两种约定:Big endian(大端表示法)和Little endian(小端表示法)。比如有个32位C程序的int变量的值是0x01234567,那么两种约定的字节的顺序分别如下图所示:
注意,用最小的地址(在这里就是0x100)来表示这个int变量的地址。用Big endian还是Little endian可能和机器型号、操作系统或编程语言有关。
C语言中,一个字符串用一个以null(值为0)结尾的字符数组表示,由于各种标准的字符编码的存在,字符串的byte表示和大小端没什么关系,并且在不同系统上都是一样的。
右移分逻辑右移和算术右移(左移不分,原因是因为带符号和无符号数的2倍或者2^k倍都可以用左移k位来表示,顺便一提:左移比乘法效率高)。一般来说带符号位的数据类型都是用算术右移(如果是负数的话高位补1),无符号数据类型则用逻辑右移(Java中用>>>表示)。
整数的表示
一串bit如果代表的是一个整数,那么想得到这个整数具体的值,通常有两种编码。two‘s complement(补码)编码得到的值的取值范围是-2^(w-1)
到2^(w-1)-1
,unsigned(无符号数)编码得到的值的取值范围是0到2^(w-1)
,这两种编码都是从一串bit representation(bit表示)转换为一个具体的整数的数值,其中的w是指bit representation中有多少个bit。从一个bit representation转到two‘s complement值其实和转到unsigned值的唯一区别就是,最高位的“权重”是负的,比如将1011转为two‘s complement就是:-2^3+2^1+2^0= -8+2+1= -5
,转为unsigned就是2^3+2^1+2^0= 8+2+1= 11
。所以同一个bit representation,它对应的two‘s complement和unsigned其实就相差2^w
这么多(因为最高位的权重是2^(w-1)
,2^(w-1) - (-2^(w-1)) = 2*2^(w-1) = 2^w
)。
某种整型的expand(扩展,比如int类型转换为long类型)或者truncate(截断,比如long类型强制转换为int类型)或其他整数类型转换的原则都是bit representation保持不变。truncate时,高位都不要了。expand时,如果是unsigned转unsigned,高位填充零即可,如果是two’s complement转two’s complement,只要用符号位填充高位就行了(这样就能保持和原来代表的数值一样)。
整数的运算
整型数据的强制转换和四则运算,和数学中整数的四则运算有很多相似的地方,但是主要的区别就在于:计算机中的整型数据的位数是有限的,所以在这个前提下,会有一些不同的性质和额外的概念(如溢出)。另外,也有一些规定,比如x除以2^k,可以用右移来完成,但是结果必须也是整型,所以规定:如果x是正数,就向下取整,如果x是负数,就向上取整(即“向零取整”),有了这个规定,我们就可以从数学上推导出如何才能满足这个规定,但在实际应用中,这个规定并不是统一的标准。另外值得注意的是,加法或乘法的溢出,以及参数的隐式转换(尤其是有符号数到无符号数的隐式转换(在代码中往往不明显),可能会从一个负数变成一个很大的正数),都可能会导致安全漏洞。
浮点(Floating Point)数
相比整数类型,浮点数的特点是能表示的数的范围更大,但是无法保证能精确表示。最常见的单精度和双精度格式如下图:
其中有三个字段:
- s代表符号位。
- exp即exponent field,代表exponent。
- frac即fraction field,代表significand,用大写字母M表示。
浮点数的格式可以理解为“为了在精确度和表示范围之间做出的权衡”而定下的一些规则设定(比如:exponent field和fraction field在单精度和双精度下分别占多少位),它所代表的的数值可以用下面的公式得到:
V=(-1)^s × M × 2^E
这里的E和exponent field之间的转化关系需要一个叫Bias的偏移量,Bias为2^(k-1)-1
,k为exponent field的位数,所以在单精度和双精度下Bias分别为127和1023,这个Bias也只是个规定而已(主要是为了能“平滑的过度”,也许能让底层硬件更容易处理);s只不过是个符号位,1负0正。有几点以后可能会忘:
- exponent field不全为0也不全为1的时候,叫Normalized的情况,E=e-Bias,e为exponent field的unsigned值,所以以单精度为例,E的范围为-126到+127,fraction部分是“1点几几几”(这里默认为“1.几几几”,所以可以省下一位,因为总能调整exponent field,使数值可以写成“1.几几几”×2^n,类似科学计数法。至于为什么不是默认“0.几几几”,因为在第二种情况下已经可以包含精度更高的0到1范围的数)。
- exponent field全为0的时候叫Denormalized的情况,这种情况下,E=1-Bias(这个规定是为了“平滑的过度”(从Normalized平滑地来到Denormalized),也就是以bit representation的角度看的时候,每个不同的bit组合代表一个独一无二的值,并且随着bit representation的“递增”,对应的浮点数的值也递增),而fraction部分是“0点几几几”(这里不能是“1.几几几”是因为:一.要有可以表示0的bit组合,二.可以表示更接近零的小数)。
- exponent field全为1的时候,分两种特殊情况,分别是infinity和NaN,NaN是Not A Number。
四种情况总结如下图:
最最需要注意和记住的还是:浮点数不能精确的表示十进制下的某些数,包括小数和整数。若忽视了这点,可能会写出难以察觉的bug。
IEEE(Institute of Electrical and Electronics Engineers)定义了四种rounding mode(舍入模式),其中默认的mode叫做round-to-even,规则是:当遇到正好在中间的数值时(比如十进制中的0.5),就让舍入后的least significant digit(最后一位)是个偶数,比如如果要舍入到整数的话,2.5就变成了2,1.5也变成了2。这种模式的好处在于,一组数的平均值会更精确,如果用四舍五入的话,平均值则会更高(因为当遇到正好在中间的数值时,都“入”上去了,而在round-to-even规则下,则有50%的概率向上,50%概率向下)。同理,在二进制下的round-to-even,碰到正好在中间的数值时,则是让舍入后的least significant digit是0就行了。
而浮点数的加减乘除运算就是,两个数值实际的运算结果再舍入一下,就是最终结果。需要注意的是,正由于以上定义,导致浮点数的加法(和乘法)并不满足associativity(结合律),比如(3.14+1e10)-1e10
的计算结果为0(3.14被舍掉了),但是3.14+(1e10-1e10)
的计算结果为3.14。还有一些关于infinity和NaN的特殊定义,比如+∞-∞=NaN。
关于int和float和double之间的转换,主要就是int转到double能完全保留precision(精度)和range;而int转到float却不能,所以可能会有舍入,但不会有溢出。