计算机有时候是无法精确表示现实世界中的数字的,一个是因为计算机表示数字所要用到的bit是有限的,另外一个原因则是有些数字本身无法用计算机的(二进制)表示法来存储,例如0.1、0.2等等。
在面向对象的编程语言中,C++本身是建立在C的基础之上的,它们使用完全相同的数字表示和运算,且C标准的设计允许多种实现方式,而Java标准在数据的格式和编码上是非常精确具体的。
2.1 信息存储
字节是最小的可寻址的存储器单位,而不是在存储器中访问单独的位。机器级程序将存储器视为一个非常大的字节数组,即虚拟存储器(第一章中有讲过,虚拟存储器包括内存和I/O设备,而I/O设备在抽象层次上称为文件)。存储器每个字节都有个地址,统称虚拟地址空间。
2.1.1 十六进制表示法
在C语言中,以0x和0X开头的数字为十六进制数。字符‘A’~‘F’可以大写或小写,甚至是大小写混合(同C语言一样,Java也用0x开头来表示十六进制数值,而八进制数用0开头(不过比较容易弄混淆),二进制数用0b开头。 ——资料来源:《Java核心技术卷1(中文第9版)》第33~34页)。
二进制数与十六进制数之间的转换技巧:若 x 是2的非负整数n次幂,则x的二进制表示就是1后面有 n 个0。一个十六进制数后面有多少个0,那转换后的二进制数后面的0的个数就相当于前者的4倍。综上所述,当n可以表示成i + 4j时(其中0 ≤ i ≤ 3),我们可以把x写成开头的十六进制数字为1(i = 0)、2(i = 1)、4(i = 2)、8(i = 3),后面跟着j个十六进制的0。
该技巧的解析:
i.我们都知道4个二进制位可以表示一个十六进制数位,即从(0000)2对应(0)16到(1111)2对应(F)16,如果(1111)2再加个1,即(10000)2,则该二进制数就无法用一个十六进制数位来表示了。
ii.根据我们的经验,一个二进制数中若最左边为1,而1的右边为n个0,那么这个数转换成十进制数就是2n,反之亦然。
iii.二进制转换成十六进制是将整个二进制数从右往左分组,4个二进制位为一组,从前面的叙述可知每组对应一个十六进制数位,故一个以1为开头、后面是n个0的二进制数,转换成十六进制数时,它的末尾就有j = n/4个0。至于最后的(0001)2(有i = 0个0)、(0010)2(有i = 1个0)、(0100)2(有i = 2个0)、(1000)2(有i = 3个0),将其转换成十六进制即可。
在Java上二进制、八进制、十进制和十六进制之间的转换通过java.lang.Integer下的valueOf、parseInt和toString方法来完成。 ——《Java核心技术卷1(原书第9版)》第189页
2.1.3 数据大小
字长是整数和指针数据的标称大小。
C语言中的数据类型所分配字节数依赖于机器和编译器。因此,用C语言编写的程序在可移植性上会受到限制。
2.1.4 寻址和字节顺序
当一个程序对象跨越多个字节时,我们需要考虑两个问题:这个对象的地址是什么,以及在存储器中如何排列这些字节。通常情况下,多字节对象在存储器中以连续的字节来排列,对象的地址为所使用字节中的最小地址。
注意理解本书第26页有关小端法和大端法的概念。
因为无论为哪种类型的机器编译的程序都能得到相同的结果,所以对大多数程序员来说,他们的机器所使用的字节顺序是完全不可见的。但字节顺序在某些情况下也会成为问题。首先是在不同类型的机器之间通过网络传送二进制数据时,接收方会得到与发送方截然相反的字节顺序。对此,在网络应用程序的编写中应遵守关于字节顺序的网络标准,接收方则可以将符合网络标准的数据转换为它的内部表示,以此作为折中方案。
与字节顺序有关的第二种情况是,我们在检查机器级程序中的字节序列时所用到的字节顺序也很重要。当阅读小端法机器生成的机器级程序时,经常要将字节按照相反的顺序来读取,这样得到的数字才与我们书写数字时最高有效位在左边,最低有效位在右边的方式相符。
注意本书第33~35页有关强制类型转换的描述及本人在上面所作的笔记。
C语言中字符串被编码为一个以null字符‘/0’作为字符串终止标志,而java则没有这个说法。
在使用ASCII码作为字符码的任何系统上都能得到相同的结果,与字节顺序和字大小无关,故文本数据比二进制数据有更强的平台独立性。
以C语言为例,将一个C函数在不同类型的机器上编译时,会产生不同的二进制机器代码。因为不同的机器类型使用不同的且不兼容的指令和编码方式,即使是同一进程在不同的操作系统上也会有不同的编码规则,故二进制代码是不兼容的。
关于整数运算中的截断问题:
无论是无符号数还是有符号数,它们之间的加法乘除基本运算都是建立在位级的表示基础上的,最终结果无论溢不溢出,在位模式上都会保持原有的位个数。
整数乘法:
整数乘法指令消耗的时钟周期比较多,而其他整数运算(例如加法、减法、位级运算和移位)只需要1个时钟周期。编译器为了对其优化,以移位、加法和减法的组合来代替常数因子的乘法。
当然,选择使用移位、加法和减法的组合,还是使用一条乘法指令,取决于这些指令的相对速度,而这些是与机器高度相关的。
整数除法:
整数除法要比整数乘法更慢一些。除以2的幂也可以用移位——右移来实现。无符号和补码数分别使用逻辑移位和算术移位来达到目的。
两个正整数(无论是无符号数还是有符号数)相除,结果都是正整数(将小数部分舍去)。这与位级的表示和移位有关。
当有负整数(有符号数)参与时,移位会导致向下舍入而不是向零舍入(这与我们的主观意识相反)。此时,可以在正常右移之前将小于0的被除数偏置(加上除数再减1),那么就可以得到正确的结果。
整数除法中正整数(无论是无符号数还是有符号数)的右移能产生与我们主观意为相符的结果,而负整数(有符号数)的右移却不能产生正确的结果的原因(请结合书上的2.3.7节来理解):
- 一个正整数,它在右移后有可能把原来在位级上的非零位移到小数点后边去了,即把某些非零位通过右移(即除以2k)给移走了,而在计算机的整数运算中只可能显示出小数点左边的位模式。但在自然界中,非零数(即上面说的非零位)无论怎么除,结果都不会为零。比如1除以2、1除以22、1除以24、1除以216……,结果都不为零,只是精度能保留到多少而已。也就是说,正整数被除后,它的结果在小数点后面还可能会有非零的数的,但因为在计算机中整数运算的性质,这些非零数被抹去了,只留下小数点左边的整数,结果只能比原来的我们在小学学的除法运算的结果小,但符合我们主机意识中对计算机整数除法的理解,即向零舍入。
- 负整数的右移和上述相同。右移把结果中小数点右边的非零位给抹去了。这些非零位其实是正的,但因为抹去了,结果就为原来的负数结果减去这些非零位所代表的值,那么,最终结果也就形成了向下舍入而不是向零舍入的这个局面。
浮点数:
表示浮点数及其运算的标准:IEEE标准754。
浮点数有规格化的、非规格化的、无穷大和NaN四种情况。