标签: CSAPP
本章研究三种最重要的数字表示:无符号、补码和浮点数。
1. 信息存储
- 大多数计算机使用 8 位的块,或者 字节 作为最小的可寻址的内存单位,而不是访问内存中单独的位。
- 机器级程序将内存视为一个非常大的字节数组,称为 虚拟地址。
- 内存的每一个字节都由一个数字来标识,称为它的 地址。
- 所有可能地址的集合就称为 虚拟地址空间。
指针有两个方面:指和类型。指表示某个对象的位置,而类型表示那个位置上所存储对象的类型。
1.1 十六进制
- 用 ‘0’ ~ ‘9’ 和 ‘A’ ~ ‘F’ 表示 0 ~ 15,其中 ‘A’ ~ ‘F’ 可以是大写也可以是小写。
- 在 C 语言中,用 0x 或者 0X 开头表示十六进制的值。
1.2 字数据大小
- 每台计算机都有一个 字长,指明指针数据的标称大小。
- 虚拟地址是以一个字来编址的,所以字长决定 虚拟地址空间的最大大小。比如对一个字长为
w
位的机器来说,虚拟地址范围为
0 ~ 2w−1 ,程序最多访问 2w 个字节。
我们将程序称为“32位程序”或“64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。
为避免由于编译器的不同带来的数据类型大小的改变,强烈建议用
int32_t
和int64_t
代替int
,这样可以准确控制数据表示。
1.3 寻址和字节顺序
- 大端法(Big endian):最高有效字节在最前面。
- 小端法(Little endian):最低有效字节在最前面。
- 假设一个
int
类型的数,地址为0x100
,值为0x1234567
,则
1.4 C语言中的几种运算
1.4.1 位级运算
按位进行布尔运算。
|
: 或 运算&
: 与 运算~
: 非 运算(按位取反)^
: 异或 运算
1.4.2 逻辑运算
- 逻辑运算中,所有非零的参数表示
TRUE
,参数 0 表示FALSE
||
: 或(OR)&&
: 与(AND)!
: 非(NOT)
逻辑运算从左向右求值,如果已经能确定表达式的值了,就不会继续往下求值。
1.4.3 移位运算
- 左移: 左移
k
位就是丢弃最高的
k 位,并在右端补 k 个 0。 - 右移:右移
k 位就是丢弃最低的 k 位,并
- 逻辑右移:在左端补
k 个 0 - 算术右移:在左端补 k 个 最高有效位的值。主要针对有符号整数的运算。
- 逻辑右移:在左端补
C 语言没有明确规定有符号数应该使用哪种类型的右移。所有会遇到可移植性的问题。
对于一个
w 位组成的数据类型,移位 k 位,当k>w 时,在很多机器上,最终的位移量是 kmod w 位,Java 中按照这个标准进行位移,但是,C 语言对此不做任何保证,所以应该保持位移量小于待位移值的位数。
2. 整数表示
整数分为 无符号数 和 有符号数。无符号数一般采用原码表示,有符号数一般采用补码表示。
- 采用补码表示导致了取值范围的 不对称————负数的范围比正数的范围大 1.
- C 语言并没有要求用补码形式表示有符号整数,但是几乎所有的机器都是这么做的。
强制类型转换: 结果保持位值不变,只是改变了这些位的解释方式。
当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么 C 语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。
数字的位扩展:整数从较小的类型转换为较大的类型,同时又保持数值大小不变。
- 扩展 无符号数
零扩展:简单的在表示的开头补零 - 扩展 补码数
符号扩展:在表示中添加最高有效位的值。
- 扩展 无符号数
截断数字:与位扩展相反,截断是将大类型的数转换为小类型的,但是不能保证数的大小不变。
将一个w 位的数字 x 截断为k 位的 y 时:x 为 无符号数
y=x mod 2k-
x
为 补码数
将x 看做无符号数截断以后,将截断的数转化为补码(即将最高位转换为符号位)。
由于有符号数到无符号数的隐式转换会导致一些难以察觉的错误,所以新手程序员尽量少用无符号数。
3. 整数运算
3.1 无符号加法
- 由于运算精度的问题,两个数相加后可能会超出最大可表示的范围,因此实际上,无符号数的加法是一种取模运算。
对于 w 位的x 和 y 来说 :x+y=(x+y) mod 2w - 判断溢出:只需判断 和 是否比任意一个 加数 要小即可。
3.2 补码加法
- 补码的加法在位级表示上与无符号的加法一样,所以补码的加法就是将补码看成无符号数进行加法运算,然后将结果转换为补码表示。
- 判断溢出:补码的溢出有两种。设
s=x+y
- 正溢出:当且仅当 x>0,y>0,但s<=0 时。
- 负溢出:当且仅当 x<0,y<0,但s>=0 时
3.3 补码的非
- 非 即加法逆元,在这里是相加为零的两个数互为 逆元。
- 一般来说,非 就是求其负数,但是由于补码表示的不对称性,负数的最小值没有对应的正数是其逆元,故负数的最小值的逆元就是其本身。
3.4 无符号乘法
- 与无符号加法类似,结果需要截断,即取模。
- 对于
w
位的
x 和 y 来说 :x∗y=(x∗y) mod 2w
3.5 补码乘法
- 与补码加法一样,补码乘法在位级表示上与无符号乘法法一致,但是仅限与截断后的结果。也就是说,补码乘法只要将补码看成无符号数进行运算,然后将截断后的值转换为补码表示即可。
4. 浮点数(IEEE 标准)
IEEE 浮点标准用 V=(−1)s∗M∗2E 的形式来表示一个数:
- 符号 s:决定这个数是正数(s = 0)还是负数(s = 1),对于数值 0 的符号位作为特殊情况处理。用 1 位二进制数表示。
- 尾数 M:M是一个二进制小数。用
n
位二进制数表示。位模式是
f=fn−1...f1f0 。 - 阶码 E:E 的作用是对浮点数加权这个权重是 2 的 E 次幂(可能是负数)。用
k
位二进制数表示。位模式是
e=ek−1...e1e0 。 - 在单精度浮点数(32位表示)中, k=8,n=23 。在双精度浮点数(64位表示)中, k=11,n=52 。
IEEE 表示的三种情况:
- 规格化的值
当 e 的位模式不全为 0,也不全为 1 时,就属于规格化表示。此时,
E=e−Bias 。其中, Bias=2k−1−1- M=1+f
- 规格化的值
- 非规格化的值
当阶码域全为 0 时,就属于这种情况。此时,
- E=1−Bias
- M=f
- 特殊值
当阶码域全为 1 时,属于这种情况。此时:
- 当小数域全为 0 时,表示无穷, s=0 表示正无穷, s=1 时表示负无穷。
- 当小数域非零时,表示 NaN,即“Not a Number”。
5. 小结
计算机将信息编码为 位(比特),通常组织成 字节 序列。有不同的编码方式来表示整数、实数和字符串。不同的计算机模型在编码数字和多字节数据中的字节顺序时使用不同的约定。
C 语言的设计可以包容多种不同字长和数字编码的实现。64 位字长的机器逐渐普及,并正在取代统治市场长达 30 多年的 32 位机器。由于 64 位机器也可以运行 32 位机器编译的程序,我们的重点就放在区分 32 位和 64 位程序,而不是机器本身。64 位程序的优势是可以突破 32 位程序具有的 4GB 地址限制。
大多数机器对整数使用补码编码,而对浮点数使用 IEEE 标准 754 编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于想使编写的程序能在全部数值范围上正确运行的程序员来说,是很重要的。
在相同长度的无符号和有符号整数之间进行强制类型转换时,大多数 C 语言实现遵循的原则是底层的位模式不变。C 语言隐式的强制类型转换会出现许多程序员无法预计的结果,常常导致程序错误。
由于编码的长度有限 与传统整数和实数运算相比,计算机运算具有非常不同的属性。当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近于 0.0,从而转换成 0 时,也会下溢。
和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比,有一些特殊的属性。例如,由于溢出,表达式 x∗x 能够得出负数。但是,无符号数和补码运算都满足整数运算的许多其他属性,包括结合律、交换律和分配律。这就允许编译器做很多优化。
浮点表示通过将数字编码为 x∗2y 的形式来近似地表示实数。最常见的浮点表示方法是由 IEEE 标准 754 定义的。它提供了几种不同的精度,最常见的是单精度(32位)和双精度(64 位)。 IEEE 浮点数也能表示特殊值。
必须非常小心的使用浮点运算,因为浮点运算只有有限的范围和精度,而且并不遵守普遍的算术属性,比如结合性。