第二章 信息的表示和处理
2.1 信息存储
- 大多数计算机使用8位的块(称为字节(byte)),作为最小的可寻址的内存单位,而不是去访问单独的一个位。
- 机器级程序将计算机的内存看做是一个很大的字节数组,称作虚拟内存。虚拟内存的每个字节都由唯一的数字来标识,称它为地址
2.1.1 十六进制表示法
- 一个字节有8位,即用二进制表示会有八位数,这样表示会比较冗长,所以使用最多的还是十六进制数(如0xA9),一个位十六进制数就可以表示一个8位的二进制数。同理,一个字节可以由两个十六进制数表示。
- 十进制,二进制,十六进制转换图
2.1.2 字数据大小
- 每台计算机都有字长(32位或64位,这里的概念注意跟字节区分),指明指针数据的标称大小。
- 因为虚拟地址是一个字长来表示的,所以字长的大小决定了虚拟地址的寻址大小。
- 在编译的时候就会决定一个程序是32位程序还是64位程序
- C语言中的char型就表示一个单独的字节,尽管它是用来保存单个字符而得名。
2.1.3 寻址和字节顺序
- 在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址就是字节序列中最小的地址。
- 最低有效字节在最前面的方式称为 小端法(little endian)
- 最高有效字节在最前面的方式成为 大端法(big endian)
- Android 和 IOS都是只能运行小端法
- 尤其注意的是不同类型的机器通过网络传输二进制数据的时候,会发现字节序列是反序的
- 阅读整数数据的字节序列时,字节的顺序也很重要。
- 在编程中,取对象的字节表示时,字节顺序也很重要。
2.1.4 表示字符串
- C语言中字符串被编码以一个以null(其值为0)字符结尾的字符数组
- 字符串中的每个字符都是以某个标准编码来表示,最常见的就是ASCII码。(可以使用命令man ascii 来生成一张ASCII字符编码表)
2.1.5 代码表示
- 代码在不同机器上编译成机器指令时,字节码的表示是完全不同的。
- 从计算机的角度来看,一个程序仅仅是一个字节序列。除了可能用来帮助调试的表以外,机器没有关于原始源程序的任何信息。
2.1.6 布尔代数简介
- 逻辑值(真)和逻辑值(假)分别对应二进制编码1和0
- 布尔代数运算:
2.1.7 C语言中的位级运算
- C语言的一个很有用的特性就是它支持按位布尔运算,这种运算可以运用于任何“整型”的数据类型上。
例如:
- 位级运算最常见的用法就是实现掩码运算
2.1.8 C语言中的逻辑运算
- C语言还提供逻辑运算或(||)、与(&&)、非(!),逻辑运算容易跟位运算混淆,它们是完全不同的两个概念。
- 逻辑运算中,将所有非零的参数都表示TRUE,参数0表示为FASLE,它们返回1或者0,分别表示结果为TRUE或FALSE,例如:
2.1.9 C语言中的位移运算
- 位移运算是一种按位操作的模式,比如左移(即 x<<k, x变量向左移动k位),就是将x的最高k位舍弃,并在低位补k个0。
- 右移比较特殊(x>>k),分为两种情形:算术右移和逻辑右移。
- 逻辑右移:在左端补k个0。
- 算术右移:在左端补k个最高有效位。
- C语言中并没有明确表示对有符号的右移使用哪种右移,但是对于几乎所有的编译器来说,对有符号使用算术右移,对于无符号来说右移都是逻辑右移。
- 与C语言相比,JAVA语言就明确指定了算术右移(x>>k)或逻辑右移(x>>>k)。
2.2 整数表示
- 整数表示分为两种:一种是非负数表示,即0和正数的表示。
- 另一种是能表示负数、零和正数
2.2.1 整型数据类型
- C语言支持多种类型的整型数据类型 – 表示有限范围的整数
-
- 32位和64位程序上取值范围不同的就是long整型。
- 值得注意的是有符号的数据类型,取值不是对称的,负数范围取值比正数取值范围大1。(与负数的编码有关)
- C语言和C++语言都支持有符号(默认的)和无符号, JAVA只支持有符号数
2.2.2 无符号数的编码
- 无符号数编码定义:
其中 B 2 U w B2U_w B2Uw 表示将一个 w w w位的二进制向量 x ⃗ \vec{x} x(其中的元素都是二进制数)为无符号数的函数(Binary To Unsigned 的缩写 ) - 从上面的定义可以看出,这种编码的最大值,就是 w w w位二进制全为1的情况,最小值就是全为0的情况。
- 每一个0 ~ 2 w − 1 2^w-1 2w−1范围内的整数都可以映射为一个唯一的 w w w位的二进制数,反之 每个 w w w位的二进制数都可以映射为0 ~ 2 w − 1 2^w-1 2w−1范围内的整数(函数的这种属性称之为双射)
2.2.3 补码编码
- 计算机表示负数的形式是通过补码(two’s-complement)形式来表示。
- 补码就是将最高有效位解释为负权(negative weight)。
- 补码编码的定义:
- 最高位为负权,从公式可以看出,当最高位二进制数为1时,此时表示的是负数,最高位也称为符号位。
- 补码表示的最小值是最高位为1,其他位为零的情况。
- 补码表示的最大值是最高位为0,其他位都为1的情况(从这里就可以看到有符号类型的表示范围不是对称的原因,一半的范围用来表示非负数,另一半用来表示了负数)。
- 补码的表示也是一一对应的关系,即一个二进制数可以唯一的转换为一个整数,反之也是唯一对应的。
- 无符号的最大值刚好比有符号的最大值的两倍大一: U m a x w = 2 T m a x w + 1 Umax_w = 2Tmax_w + 1 Umaxw=2Tmaxw+1
- 在java中单字节数据类型称为byte,而不是char。
2.2.4 有符号数和无符号数之间的转换
- 对于机器来说,有符号数和无符号数之间的转换是不会改变一个数的位级表示,只是改变了解释这些位的方式。
- 对于同样字长的有符号数和无符号数之间相互转换的规则就是:位模式不会变,但是它表示的数值可能会改变(比如有符号的最高位是符号位,转换为无符号时,它就是一个有权值的位。)
- 补码转换为无符号数:
- 所以直观的从公式看,一个有符号数负数转换为无符号时,会成为一个很大的正数。(也可以解释为,因为负数的最高位为1,转换为无符号数时,位模式不会变,最高位还为1,这时解释成无符号数时就会变成很大的正数)。
- 非负数的有符号数转换为无符号数时,会保持不变。
- 无符号数转换为补码
- 所以可以总结一下,当一个数的位模式最高位为0时,无符号数和有符号数(补码)之间转换,数值不会改变,如果最高位为1时,那么在表示无符号数时,最高位就是实实在在的权值,而表示有符号时,最高位就是符号位,表示负数。
2.2.5 C语言中的有符号数与无符号数
- C语言支持无符号与有符号,默认状态下C语言声明的整数都是有符号数,如果要声明无符号数需要在数字后面加U或u 或者是用关键字unsigned声明。
- C语言中的有符号数都是用补码表示
- 无符号与有符号之间的转换,主要遵守的原则就是底层的位表示不会变。
- 由于C语言中是同时包含无符号与有符号数,当执行一个运算就需要尤其注意,**当一个运算数是无符号,另外一个是有符号数时,C语言会隐式的将有符号数转换为无符号数,并且都将他们看作非负数处理。**特别是在做大小判断的时候尤其需要注意。
2.2.6 扩展一个数字的位表示
- 将一个无符号数扩展成为一个更大的无符号数,我们就在最高位添0,这种称为零扩展(zero extension),原理如下,该原理遵守了无符号数的定义。
- 将一个有符号扩展成为一个更大的有符号数,可以执行符号位扩展(sign extension),即在扩展时添加最高有效位的值:
2.2.7 截断数字
-
无符号数截断
即 截断时直接去掉高位多余的位,剩下的位表示继续使用 -
补码截断
从公式中的原理来看,与无符号类似,截去高位多余的位表示,不同的是,剩下的位模式的最高位将变成符号位
2.2.8 关于有符号与无符号的建议
- C语言中有很多地方都碰到运算符,尤其是从有符号到无符号的隐式转化,会导致错误或漏洞的方式。避免这一类的问题就是绝不使用无符号数。事实上,除了C语言很少有语言支持无符号整数
- 当我们把一个变量仅仅看作是位的集合而不表示任何数字意义的时候,这个时候使用无符号数就是非常有用的。
2.3 整数运算
2.3.1 无符号加法
-
无符号加法只用关注溢出的情况,当两个无符号相加溢出时,有且只会溢出一位。所以两个无符号加法定义如下:
-
说一个运算结果溢出,是指运算之后的结果,不能用指定的数据类型的字长来表示。
-
检测一个加法溢出:
-
无符号数求反
每一个无符号数,都可以找到一个与之相加最后结果等于零的数(溢出之后,刚好结果为零)
2.3.2 补码加法
- 对于补码加法,即是有符号数加法,我们就必须要确定几种情况,结果太大?或者结果太小?
- 两个无符号数之和和两个有符号数之和有相同的位级表示,大多数机器使用同样的指令来执行无符号或有符号加法。
- 检测补码加法中的溢出
2.3.3 补码的非
- 补码的非
- 当一个补码是最小值的时候,它的非就是它自己的加法的逆(加法的逆是指这个数加上这个逆等于零)。
- 根据定义,补码的最小值就时符号位(即最高位)为1,其它位为0的情况,它加上它本身刚好产生溢出,成为0,所以 T M i n w TMin_w TMinw的逆就他本身。
2.3.4 无符号乘法
- 将一个无符号数截断为w位等价于计算该值模 2 w 2^w 2w
2.3.5 补码乘法
- 对于无符号和有符号来说,乘法的位级表示都是一样的。
2.3.6 乘以常数
-
在大多数情况下, 乘法的运算需要十个或者更多的机器周期,然而其他整数运算只需要一个时钟周期。
-
原理:乘以2的幂
-
原理:与2的幂相乘的无符号乘法
-
原理: 与2的幂相乘的补码乘法
-
无论是补码或者无符号运算,与2相乘的幂都可能溢出,即使是溢出,通过移位获得的结果也是一样的
-
由于整数乘法比位移和加法的运算代价都要大的多,很多C编译器试图以位移、加法和减法的组合来优化乘法运算带来的运算代价
2.3.7 除以2的幂
-
整数除法比乘法更慢 – 需要30个或更多时钟周期,所以除以2的幂也可以通过位移运算来实现,与乘法不同的是除法是右移,无符号和补码数分别使用逻辑位移和算术位移来达到目的
-
整数的除法总是舍入到零(可以理解为做舍入操作时,总是向零的方向做舍入)
-
除以2的幂的无符号除法:就是逻辑右移:
-
因为我们想要达到的效果是舍入到零,那么做2的幂的补码除法如下:
2.3.8 关于整数运算的最后思考
- 整数运算在计算机中实际上是一种模运算形式,表示整数的位有限导致运算结果可能溢出。
- 在底层的运算中,无论运算数是无符号或是补码,它们都有非常类似或完全一样的位级行为。
2.4 浮点数
- 早在浮点数诞生之前,有很多编码浮点数的标准,随着时间的推移,目前计算机中的浮点数都支持IEEE的浮点标准。这大大提高了程序在不同机器上的可移植性。
2.4.1 二进制小数
- 表示方法:
- 浮点数只能很精确的表示可以写成形如 x ∗ 2 y x*2^y x∗2y的数,其它的数只能用很近似的值表示,浮点数二进制表示位数越多,表示的小数精度越高。
2.4.2 IEEE浮点表示
- IEEE浮点数表示法:
- IEEE浮点数的二进制表示:
- 浮点数表示根据阶码的值可以把编码分成以下三种情况:
- 规格化的: 阶码不全为零且不全为1,在这种情况下,阶码字段解释为以偏置形式表示的有符号整数,其值为E=e-bias, 其中e的位表示为 e k − 1 e k − 2 . . . . e 0 e_{k-1}e_{k-2}....e_{0} ek−1ek−2....e0,bias是一个等于 2 k − 1 − 1 2^{k-1}-1 2k−1−1的偏置值。小数字段二进制表示 0. f n − 1 . f n − 2 . f n − 3 . . . . . . . f 0 0.f_{n-1}.f_{n-2}.f_{n-3}...... .f_{0} 0.fn−1.fn−2.fn−3.......f0,尾数M定义为 M = 1 + f M=1+f M=1+f,这种方式叫做隐含的以1开头的表示。
- 非规格化的: 阶码全为零:当阶域为全零时,表示的非规格化形式,阶码值是 E = 1 − B i a s E=1-Bias E=1−Bias,而尾数的值是 M = f M=f M=f,也就是小数字段的值,不包含隐含的开头的1。非规格化一般有两种用途,一种是表示数值零,另一种是表示非常接近0.0的数值,它提供了一种属性称为逐渐溢出属性,可以使能表示的数字均匀分布接近0.0。
- 无穷大:阶码全为1,且尾码全为零。 当指阶全为1的时候出现,当小数域全为零,表示无穷大,当符号位s为0时,表示无穷大,当符号位s为1时,表示无穷小。
- NaN:阶码全为1,且尾码不等于零。当小数位不全为零时,表示NaN(即not a number的缩写。),在一些程序中,表示一些没有初始化的数据是很有帮助的。
2.4.3 数字示例
- 下图展示了一组数值,假定可以用6位来表示浮点数,其中k=3的阶码位和n=2的尾数位:
可以看到,浮点数可表示的数并不是均匀分布的,越靠近原点处它们越稠密 - 再看一个示例:假定一个8位浮点格式示例,其中k=4的阶码,和n=3的小数位(还有一个留给是符号位),偏置量为
2
4
−
1
−
1
=
7
2^{4-1}-1=7
24−1−1=7:
可以观察到最大非规格化数和最小规格化数之前的平滑转变。这种平滑转变就要归功于对非规格化数的 E E E的定义(我们定义为 1 − E 1-E 1−E)。
2.4.4 舍入
- 因为表示浮点的方法限制了浮点表示的范围和精度,所以浮点数只能近似的表示实数运算。
- IEEE浮点格式定义了四种不同的舍入方式:向偶数舍入,向零舍入,向下舍入,向上舍入:
- 向偶数舍入:也称为向最接近的值舍入,但是当值处于中间的请求,那么他会向舍入结果最低有效位是偶数的方向舍入,所以举例来说1.5或者2.5,通过向偶数舍入的结果都是2。而1.4的舍入结果就是1。
- 向偶数舍入:也称为向最接近的值舍入,但是当值处于中间的请求,那么他会向舍入结果最低有效位是偶数的方向舍入,所以举例来说1.5或者2.5,通过向偶数舍入的结果都是2。而1.4的舍入结果就是1。
- 向偶数舍入一般会是用在避免统计偏差的情况,如果总是使用向上舍入或者向下舍入,在统计数据时数据的误差就会因为舍入的原因,不断加大(数据越多,误差就会越大),向偶数舍入在大多数情况下可以避免这种情况,50%的时间它是向上舍入,50%的时间是向下舍入。
2.4.5 浮点运算
- 把浮点数看作实数,实数做运算后,对结果进行舍入,得到的最后的值就时浮点数做运算的结果。
- 浮点数运算不具有结合性,因为浮点数的运算可能有舍入操作,每一步运算的舍入都可能不一样
2.4.6 C语言中的浮点数
- C语言版本提供了两种不同类型的浮点数:float 和 double , 分别是单精度和双精度。
- C语言不要求使用IEEE浮点,所以就没有标准的方法来改变舍入方式或者得到诸如-0,负无穷大,或正无穷大。
- C 语言中,int 、float 、double之间的转换,需要注意以下几点:
2.5 小结
- C语言将信息编码为bits(比特),通常组织成为字节序列,一个字节8位,有不同的编码方式用来表示整数,实数或字符串。
- C语言对有符号使用补码表示,对小数使用浮点数表示。深刻的理解他们的位级编码方式,对正确编程特别是正确的数值运算有很大帮助。
- 浮点数由于他很特别的编码方式,运算的结合律对于浮点数来说都是不适用的,这一点尤为需要注意。