《深入理解计算机系统》第二章学习笔记
Representing and Manipulating Information
1 、信息存储
计算机字长,指明指针数据的标称大小。32位机器虚拟地址空间为4GB。64位为16EB。64位机器可以运行32位机器编译的程序,反过来不行。
操作系统会给每个进程提供私有的虚拟内存地址空间,一个进程可以访问自己的数据,但是不能访问别人的数据。在虚拟内存中地址是连续的,对应物理内存则不一定,根据字长的不同,有不同的间隔,即根据字长的不同,指针大小也不同。如下图所示
C语言各种数据类型字节长度如下:
寻址和字节顺序:多字节对象在内存中的存放方式分为小端法和大端法。例如一个int有4个字节[x4,x3,x2,x1],x4位最高位,若x4在最前面(低地址)则为大端法,若x1在最前面则为小端法。大多数Intel兼容机都只用小端模式(也就是低地址放低位,高地址放高位)。大多数IBM的使用大端。
大小端产生的问题:1)在不同类型的机器之间通过网络传输二进制数据(用协议解决)2)在阅读表示整数数据的字节序列时。3)编写规避正常的类型系统的程序时。
字符串表示:字符串被编码为一个以null(‘\0’)字符结尾的字符数组。每个字符用某个标准编码来表示,常见的为ASCII字符码。扩展有Unicode,UTF-8,其中UTF-8兼容了ASCII码。
代码表示:不同机器的指令编码是不同的,一般二进制代码是不兼容的。
布尔运算:~非,&与,|或,^异或,位运算。
逻辑运算:逻辑运算认为所有非0参数表示TRUE,0表示FALSE。对应OR(||)、AND(&&)和NOT(!)。如果第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。
移位运算:
左移,x<<k,丢弃最高的k位,右端补k个零。
右移分为逻辑右移和算术右移。
逻辑右移在左端补0,算术右移在左端补最高位。
对于有符号数(补码),右移一定是算术右移;对于无符号数,右移一定是逻辑右移
2、 整数表示
无符号整数直接用其二进制数值表示。
有符号整数用补码编码,最高位为符号位,1表示负数,0为非负。
补码的作用:用来表示有符号数。
补码的计算方式:非最高位的值减去最高位(如果最高位是1,则减去最高位的权重;如果最高位是0,则直接是该二进制的值)。
负数的补码为原码的反码加1,正数的补码为本身。
补码表示的范围,以4位为例,1000表示最小(-8),最大为0111(7),1111表示-1,0000表示0。
可以看出补码的范围是不对称的,|Tmin|=|Tmax|+1,C语言中文件<limits.h>定义了一组常量,来限定编译器运行时整数的范围,有INT_MAX,INT_MIN,UINT_MAX等。
为了保证程序的兼容性,ISO C99标准在文件stdint.h中引入了整数类型类,定义了如int32_t和int64_t这样恒定长度的整数类型。
在C语言中,允许强制类型转换,有符号数和无符号数之间的转换规则是位模型不变,但是解释这些位的方式改变了。例如下面代码:
当需要在有符号数和无符号数之间转换时,我们只需要将十进制数以一种方式转换成二进制数,再将二进制数以另一种方式转换成十进制数。
int i = -1;
unsigned int v = (unsigned int)i;
cout<<v;
输出是4294967295(unsigned int的最大值)。因为-1的补码表示为0xFFFFFFFF,看做无符号数则为4294967295。
在执行一个运算时,如果一个运算数为有符号的而另一个为无符号的,C语言会隐式的将有符号的转为无符号的,并假设这两个数非负。这在比较大小时会产生问题。
在-1<0U中,-1转为无符号数为4294967295,其大于0。
将一个较小的数转换成较大的数(不会改变数值):
- 将一个无符号数转换为一个更大的数据类型,直接补零就可以,这叫零扩展。
- 将一个有符号数转换为一个更大的数据类型,补符号位,叫符号扩展。(当最高位是0时,补0;当最高位是1时,补1)。经过扩展,可以保持数值不变。
将一个较大的数转换成较小的数(可能会改变数值):
-
对于无符号数,将一个w位的数,截断成k位时,丢弃最高的w-k位。截断操作可以对应于取模运算(即求余运算),要截断成k位,相当于对10的k次方取模。
-
对于有符号数,我们先将十进制数按照有符号的方式转换成二进制数,用无符号数的函数映射来解释底层的二进制位,将二进制数以无符号的方式截断,截断后按照有符号的方式转换成十进制数。
3 、整数运算
无符号加法:溢出直接去除高位。
由于在c语言中溢出不会报错,所以我们可以用以下程序来判断是否溢出
有符号加法:当做无符号数进行加法,溢出直接截断,截断后按照有符号的方式转换成十进制。
-
正溢出:
-
负溢出:
-
判断正溢出和负溢出的方法:
减法:
我们通过加法来实现减法,由此引入加法逆元(加上某个数的逆元等于减去某个数):
-
无符号数的逆元:
x和x`都是无符号数,符合无符号数的要求
-
有符号数的逆元
当x>有符号数的最小值时,直接对x取相反数即可(加上x`等于减去x)
但是由于有符号数的表示范围是不对称的的,当x=最小值时,没有与之对应的相反数,所以只能利用负溢出,我们发现最小值加上它自身负溢出后等于0,所以此时x的逆元是它自身。
有符号的非:-x=~x+1,即等于按位取反再加1.
乘法:
不管是无符号数还是有符号数,都是将两个数相乘后进行截断,只不过补码乘法比无符号数乘法多一步,需要将截断后的无符号数转换成补码。
对于有符号数和无符号数,乘积的完整结果可能不同,但是截断后却是相同的
由于乘法需要花费多个时钟周期,所以编译器常使用移位,加法,减法来代替整数乘法的操作。
有符号数的乘法:xy=(x的补码y的补码)的补码
除法:
除法中可能会出现小数的情况,此时会向0舍入。
编译器也可以通过移位来代替除法运算,但由于运算法则的原因,移位只能代替除以2的k次方的除法运算,无法代替任意数的的除法运算。
-
无符号数:除以2的k次方,就进行k位的逻辑右移。
-
有符号数:对有符号数进行的是算术右移。
对于非负数,过程与无符号数相同,直接进行算术右移即可得到结果。
-
对于负数,在高位补1。但是当出现小数时的结果与期望向0舍入的结果不同。
因此,需要在移位之前加入一个偏置,来修正这种不合适的舍入
偏置的值等于(1<<k)-1,通过加入偏置后,再进行算术右移,即可得到向0舍入的结果。
然而这种方法只适用于2的k次方,无法推广到除以任意数的除法运算。
总而言之,对整数运算的核心法则就是:不管是什么数,统一按照二进制位进行运算,运算完后该溢出溢出,该截断截断,将截断后的结果按照不同的函数映射方式转换成无符号数或有符号数。
4 、浮点数表示(IEEE标准)
符号位s:负数s=1,正数s=0
尾数M:是一个二进制小数
阶码E:对浮点数加权,权重为2,可以为负数。
将这三个数划分为三个字段:以float(32位)为例,s为最高位,30-23(8位)表示阶码E,剩下的22-0表示尾数M。double的E为11位,M为52位。
有些时候,阶码并不等于二进制位的值,为了方便描述,我们用e来表示阶码的二进制位的值。
浮点数分为三类,由阶码字段的二进制位(e)决定浮点数属于哪一类
三类浮点数对应到数轴中的情况:
-
规格化值:e不全为0,也不全为1。
此时阶码的值并不等于e所表示的值,而是等于e的值减去一个偏置量,偏置量与阶码的位数相关。
之所以需要采用一个偏移量,是为了保证 e 编码只需要以无符号数来处理。
阶码的值为E=e-(2^(k-1)-1),e为阶码的二进制位表示的无符号数值,k为阶码E的位数,float为8位。
小数字段被解释为小数值f,尾数定义为M=1+f,例如001表示二进制数0.001,十进制为1/8,则M为9/8。
对于 M,一定是以 1 开头的:也就是 M=1.xxx…x2。其中 xxx 的部分就是 frac 的编码部分,也就是说开头的 1 是『免费附送的』,并不需要实际的编码位。
举个例子,
float F = 15213.0;
,那么
于是 frac 部分的值就是小数点后面的数值,而 Exp = E + Bias = 13 + 127 = 140 = 100011002100011002,于是编码出来的浮点数是这样的:0 10001100 11011011011010000000000 s exp frac
-
非规格化值:e全为0,用来表示0和非常接近0的数。
阶码值等于1减去偏置量,E=1-(2^(k-1)-1),尾数为M=f,阶码值的规则和规格化数不同是因为要使得非规格化数过渡到规格化数时平滑。
-
特殊值:阶码全为1。
尾数为0时表示无穷大,其他表示NaN(不是一个数)。
整型到浮点型的转换:
浮点数的舍入:从上述浮点数的表示可以发现只能近似地表示实数运算,因此需要舍入。默认为向偶数舍入法。
在C语言中,有两种浮点数float和double,舍入方法为向偶数舍入,不能修改舍入方法,在标准方法中不能得到诸如-0,无穷或者NaN的特殊值,在math.h中有这些值。
整数和浮点数之间转换规则:
int转float不会溢出,可能被舍入。
double转float有可能溢出,可能被舍入。
float或double转int会向0舍入,例如1.9转为1,-1.9转为-1.可能溢出。