[学习信息的存储(编码)和处理有什么用?]
研究数字在计算机中是如何存储的,以及值的范围和算术属性,有助于我们跨越不同的机器、系统以及编译器获得更好的可移植性。了解这些细节非常重要,程序员有责任和义务编写健壮的程序,了解其内部如何工作,其不良行为背后的原因,对于安全领域也有非常高的价值。
[本章是如何展开的?]
本章首先对计算机是如何存储信息(编码)进行了讨论,中间涉及了二进制、十六进制数据的表示、大小以及如何顺序存储、布尔运算等,然后研究了三种重要的编码方式:无符号、补码(有符号)以及浮点数的编码。
[主要笔记]
一、信息的存储:编码
单个的位没啥用处,当把位组合在一起(字节8个位),再进行某种解释,赋予不同的含义,我们就能表示世间万物了。每个程序都可以简单的视为一个字节块,程序本身就是一个字节序列。
1.十六进制
由于二进制信息太过冗长,于是在描述位模式的时候不是很方便,就发明了16进制。这里没什么好说的了,如下表:
❤:学习过计算机的同学对这个内容都不陌生,关于各个进制之间的转换作者让我们记住:A C F对应的十进制,然后推出BDE的值:
还有一个简单的计算诸如:
可以使用公式:n = i + 4j 其中n = 11 ;i的取值范围是[0-3]对应的值为:0对应1,1对应2,2对应4,3对应8相当于2的i次幂,j就代表多少个0。回到上面的例子中,11 = 3 + 4 X 2 就可以写成0X800 (j=2两个0,i=3对应8)。算是奇技淫巧吧,了解一下就可以了。
对于16进制和10进制的相互转化就无非是反复乘以或者除以16,也没啥好说的了。
2.字
我们前面说过虚拟地址空间可以使得我们很方便的范围到每个字节,但是虚拟地址是以一个字来进行编码的,所以字的长度就决定了我们能范围的最大范围。对于我们使用的32位的计算机而言,程序最多范围2的32次方个数据,也就是我们经常所说的4GB
3.数据大小
关注数据大小的原因是使得程序对于不同数据类型的大小不敏感,如果我们用一个int类型(4字节)来存一个指针(64位下可能是8字节)就会带来不小的麻烦。
4.寻址和字节顺序
对于跨越多个字节的数据、指令和控制信息,我们必须要知道他的地址是什么,以及是按照什么顺序在计算机中存储的。对于同样的一个数字:1234567;有两种存储方法:
就数据1234567来说,它跨越了4个字节,从0x100开始到0x103结束,我们除了必须要知道开始的地址0x100外,还有一个重要的就是必须要了解是何种顺序在计算机中存储的。就具体的应用来说,至少有下面三个方面:
①通过网络在不同机器以及系统中传递数据时,必须要遵守建立的字节顺序;
②强制类型转换:不会改变真实指针,只是告诉编译器以新的类型来解释数据;
③阅读表示整数的数据类型时(不是很理解)
5.字符串:文本数据比二进制数据有更强的平台独立性
字符串是以null(0)字符结尾的字符数组,在任何系统上面都能看到相识的结果。但二进制机器码就不一样了:
编译成不同的机器码的结果如下:
因此:二进制代码是不兼容的,从机器的角度来看,程序仅仅只是字节序列。
6.布尔运算
关于布尔运算的基本的方法与、或、非、异或就不在叙述了。除了判断逻辑以外讲讲有什么用:
掩码运算:举一个例子,我们要用守蒙住脸蛋防止别人看到我们的脸,但是我们又很想看看对方长啥样子,这时候就会留一个缝隙,让眼睛可以往外边看。这基本上就是掩码的功能了。我们有选择的屏蔽了一些信号,如长相。又如:0xFF(1111 1111)任何一个数与上0xFF,就能将最低的8位保留下来。
位运算与逻辑运算的区别:逻辑运算认为非0就是true,而0表示false;逻辑运算如果第一个表达式能确定结果就不会对第二个求值。
移位运算:
❤ 对于无符号的数,右移动必须是逻辑上的。对于有符号的数,可以是任何一种,但常用的是算术右移动
二、整数的表示(存储)
本节首先介绍了两种编码方式,一种只能表示非负数(无符号编码),另外一种可以表示负数、0和正数(补码编码),然后讨论了这两组编码的数学属性和机器实现,最后对于一个已知编码的扩展和收缩的方法进行了介绍
C语言支持的数据取值范围:
我们将介绍这些具体的取值范围是如何得来的,以及之间转换所遵循的规则,不知道大家有没有注意到一点:有符号数的范围并不对称,负数的范围比正数大1?我们接下来的内容会告诉大家原因
1. 无符号数的编码
我们来探究一个公式,完成二进制到无符号数的编码(Binary to Unsigned)我们编号为(2.1):
用几个例子和一幅图帮助大家理解:
这是对w=4位的几个数字的无符号数的编码,很好理解,就是各个位具体的值和每个位的权值相加,用如下的图来表示就更更清楚了:
在无符号的表示中,统一都用的是向右向量来表示,各个位的权长度不一样,最高的是8最低的是1。我们来看看表示的范围是多少:从最小的无符号数[0000]到最大的无符号数[1111]范围就是0-15(最大值为2的w次方-1)。
2.有符号数补码的编码(Binary to Two's-complement):
用实际的例子表示为:
最高位为符号位,当为0的时候没什么影响,为1的时候加的就是负权了,如下图:
我们来看看不对称性的根源?
这种对称性是由于一半的位模式(符号位设置为1)表示负数,而另外一半的位模式(符号位设置为0)表示非负数,因为0是非负数,也就意味着能表示的正数比负数要少一个。
同样来讨论一下取值范围,最小的负数TMin为[1000]结果为-8,最大的正数TMax[0111]结果为7,我们看得出来|TMin| = |TMax| + 1,最小的负数TMin没有与之对应的+8,这种不对称的特殊性,正是由于0也是非负数,所以-8就没有与之对应了正数值了。
另外,有符号数还有其他两种表示方法:反码和原码,由于运用的非常少,大多数机器都采用的是补码的方式,我们就不再研究了。
3.有符号与无符号数的转换:
类型转换的结果是保存位值不变,只是改变了解释这些位的方式
从存数学的角度考虑,我们能想到的规则是:首先对于两者之间的交集,我们保持不变;其次,对于超出范围的值,比如将最大的负数(前文中说的-8无对应的情况)转换成无符号可能会得到0,将无符号的数(太大的部分)转换为有符号的数可能会得到TMax,举个例子:
以w=4位为例:有符号数能表示的范围是:[-8--7] 包含两端的-8和7而无符号的表示范围是 :[0--15] 这样,当我们在[0-7]之间的数的进行转换的时候将保持不变;而在进行诸如:无符号数[8-15]转有符号数的时候就只能用TMax表示;有符号数[1111]=-1转到无符号数就变成了[1111]=15。注意看下面这个图:
-12345同53191有同样的位表示,到这里我们就明白了,在计算机内部的实现方式是:
规则:数值可能会改变,但位的模式不变。
从数学角度来讨论这个规则:
我们定义B2U()的逆运算为U2B();定义B2T()的逆运算为T2B();
那么完成U2T()的转换就相当于:
等式一:U2T() = B2T(U2B())
解释一下就是,先完成无符号到二进制的转换U2B,然后在将二进制转换层有符号数B2T
同样的道理完成T2U()的转换就相当于:
等式二:T2U() = B2U(T2B())
我们先来看看U2T(无符号转有符号)之间的转换公式的推导过程:
假设w=4位
①无符号数B2U计算公式:[1111] = 1 * 8 + 1 * 4 + 1* 2 +1 = 15
②有符号数B2T计算公式:[1111] = -1 * 8 + 1 * 4 + 1 * 2 + 1 = -1
用①-② 也就是B2U()-B2T() 后面的内容1 * 4 + 1 * 2 + 1可以抵消掉,其实就是头两个数之间的差值:
B2U()- B2T() = 1 * 8 + 1 * 8 = 1 * (8 + 8)= 1*16
❤ 也就是1 乘以2的4次方,写成公式为:
有符号数转无符号数:
令x = T2B(x),带入上述公式转换的结果为:
由于B2U(T2B) = T2U;B2T(T2B)=x。所以上面的等式可以写成如下形式:
这个公式由于x的最高位w-1位的属性,又决定了以下两种形式,得到:
进一步解释一下就是,以w=4位为例,[0001]-[0111]之间的有符号数转无符号数其值是不变的。如果无符号数[1001]-[1111]之间的数由于最高位有值,那么结果就会加上2的w次方。比如:将-5 = [1011]转换为无符号数就是:11 (-5+16),而-1就变成了15.如下图看到的
而T2U的一般行为就是,非负数保持不变,而负数就变成了大的正数
至此我们探究了T2U的转换内幕,以及其行为,下面我们来看看U2T是如何工作的
无符号数转有符号数:
将x = U2B(x)带入标❤公式:我们得到
我们将上面的等式整合一下,得到了U2T的公式为:
根据无符号位w-1时候大于或者等于2的w-1次方,我们将上个公式可以写成下面这种形式:
同样的我们以w=4位为例,完成对[0-7]转有符号数的时候,保持本身值不变。而如果是对于≥8的数转换为有符号数,[1001]-[1111]之间的数,由于最高位要解释成符号位,所以结果会减去2的w次方。也就是上述公式所显示的内容,将无符号数[1001]=9转换为有符号数为[1001] = -7 = 9 - 16.
4.C语言中的有符号数与无符号数
C当然没有指定有符号数用什么编码方式,但是各个机器基本上都是使用补码的形式表示。所以,如果要创建一个无符号数常量,那么就必须要申明加入后缀‘U’或者'u'的形式。
特别是类型转换中的隐形转换,笔录表达式赋值给另外的一个变量,就容易被忽略,很难发现错误之处。其实一个为了避免出错,最好的一个做法就是尽量不使用无符号数。
(另外提一下,无符号数有什么用处:当我们想把字仅仅当做位的集合来看,没有任何数字意义的时候,无符号数还是非常有用的。例如:往字中放入描述各种布尔条件的标记时,就形成了地址,而地址当然是能访问的越多越好)
C语言中的转换规则,如果执行一个运算,它的一个运算符是有符号另外一个无符号,那么C就会隐式的件有符号的参数强制类型转换为无符号数,并假设这两个都是非负数来进行计算。这里特别需要注意的就是像>或者<结果可能出错。
如比较:-1 < 0U的值时,先将-1转无符号数:4294967295 < 0U 就有问题了。
我们来说说为什么-1转有符号的数会是一个这么大的数,
同样的以w=4位为例:-1 = [1111] 转换为无符号数,由于保持位值不变,只是改变解释那么无符号数[1111] = 15 也就是能表示的最大值,4294957595是采用补码的32位机器所表示的最大无符号数。
5.对数字的位进行扩展与截断的技术
①扩展一个数字的位:
这里在一个程序中应用的特别多,比如将一个short类型的数据转换为int,或者将unsigned short 转为unsigned int 这种数字长度的增加,到底是依据如何的规则进行的转换,大致说来有两种扩展方式:零扩展和符号扩展:
1>(零扩展)无符号转更大数据:在数据的开头位添加0即可
2>(符号扩展)有符号转更大数据:在数据的开头添加最高位的副本
零扩展不用说会保持数值不变,我们来研究一下符号扩展:假设字长从3为变成了4,有补码表示的数[101] = -3 将变成[1101] = -3 即使增加了1位结果任然是一样的,是什么原因保证了这种变化保存了数值的不变呢?我们来探究一下:
这里有一个关键属性就是:就是上图所示的,当我们做最开始的两位的加法的时候,其结果与上一等式的第一个数的值相同。
用更普遍的观点来看,关键的属性就是:
另外要提一下,将一个数据大小改变,和无符号有符号之间这两组转换的相对顺序,会影响一个程序的行为。如果将short转为unsigned时,就会先改变大小,然后再完成有符号到无符号的转变。如:short sx = -12345 转为 unsigend的uy时,先将0X CF C7扩展成0X FF FF CF C7然后再进行到无符号数的解释,结果就得到了4294954951这样的一个数字。
②截断一个数字的位:
对于无符号的数:[1111] = 15 截断1位,其实是将15 mod 8(2的3次方) = 7 [111]
对于有符号数:[1101] = -3 的截断,是先将[1101]转为无符号的13然后来mod 8 = 5无符号表示就是[101],然后再把[101]解释成有符号数[101] = -3
6.注意事项:
1> 尽量避免使用无符号数
2> 特别留意隐藏的强制类型转换行为
三、整型的运算(相当于mod运算)
我们首先要来理解一下“字节膨胀”的概念:
比如我们以w=4位为例,进行无符号数[1111]=15和无1.符号数[1010]=10的加法运算,结果为25=[11001]需要5位来表示结果,依次类推我们如果要完整的表示运算结果,就不能对字长做任何限制。大部分编程语言都选择了固定精度的加减乘除运算,会对结果进行一定的处理。也就与我们数学上的运算有所不同,这一节我们就来学习这些处理方法。
首先来看看加法运算:我们会接触到[无符号的加法]和[补码的加法]。这两组加法运算使用的是相同的机器指令
1.无符号数的加法:
溢出的真正含义就是:完整的结果不能放入到固定精度的字长中去,于是最高位就被丢弃掉了。减去2的w次方,相当于结果mod(2的w次方)。
⚠️ 如何判断是否溢出?
运行c程序的时候,溢出并不认为是一种错误。那么我们自己如何判断是否发生了溢出呢?我们可以设s=x+y,当结果s < x或者s < y的时候发生了溢出。(证明如下:前面我们2.11的第二种情况下有溢出,我们确定的是y<2的w次方,那么y-2dw就<0。两边加上x,结果就是x+y-2dw < x发生了溢出)
2.补码的加法:
为什么会有“正溢出产生负数,负溢出产生正数”?
举例说明:
正溢出[0101] +[0101] = 5 + 5 = 10 = [01010]截断最高位0结果为[1010]=-6;
负溢出 [1000]+ [1011]= -8 + -5 = -13 = [10011]截断高位1结果为[0011]=3.
主要的原因还是我们使用的是固定精度的运算,由于结果不能被完整的保存,我们就需要使用截断高位保存低位的方法。这样做由于正溢出是两个大正数相加,完整的结果仍然数正数,截断最高位相当于减少了2的w次方;而负溢出数两个大负数相加,完整的结果仍然数负数,截断高位1以后相当于加上了2的w次方。
3.补码的非
以w=4位为例,补码的表示范围在[-8,7]之间,也就是说[-7,7]内的数可以表示为-x,但是对于最小的TMin=-8的情况怎么办呢?C语音中求解补码的方法是:每位求反,结果加1.
就如[0101] = 5 每位求反为 [1010] 再加上1为:[1011]补码表示为-5。那么同样的方法计算[1000] = -8的求反[0111]再加上1的结果还是[1000] = -8我们就认为的定义了:-8的非就是-8,也就是上个算式中显示的内容了。
4.无符号乘法和补码的乘法(使用相同的机器指令)
无符号的乘法:我们知道如果是w位的两个数相乘,结果最大可能是2w位,C语音中仍然使用的是固定精度的运算,这就导致了结果只能截断到w位,只保留真实值的低w位。计算公式为:
补码的乘法:使用的是同无符号数相同的机器指令,并且在低位是相同的
公式表示为:
关于低w位相同的证明其实很简单:
由于结果中带有2的w次方的同mod2的w次方会丢弃掉。因此我们看到了,mod2的w次方留下的就是低w位,结果同无符号数是一样的。
5.乘以常数(左移)
乘法运算太慢了,需要10个时钟周期,于是编译器就使用移位和加法指令来替代乘以常数。
如:x * 14 :其中 14被分解为:2d3 + 2d2 + 2d1 (其中d代表次方)编译器的写为:(x<<3)+(x<<2)+(x<<1)一些聪明的编译器甚至改写成:(2<<4) - (2 << 1)这时候只需要两个移位指令和一个减法指令了。
6.除以2的幂(右移)
除法指令比乘法更慢,相当于30多个时钟周期,所以当除以2的幂的时候经常用右移来代替。
无符号数:逻辑右移动(左边空出来的位补0)
补码:算数右移(左边补出来的数加最高位的值)
舍入的方法:整数的除法是舍入到0的,其中对于负数的除法结果是向下舍入,如-7/2不是-3而是-4,这样做其实是使用的一种偏置值的方法。[X/Y] = [(X+Y-1)/Y]也就是原本是-30/4]=7.5却变成了[-27/4]=6.75向下舍入到-7
7.整数运算总结
整数运算不论是加减乘除,其本身来说就是一种mod运算。由于结果的固定精度,大的就可能会溢出。补码和无符号数使用的是相同的机器运算指令,有相同的位级表示。特别是无符号数的一些意向不到的行为,程序员特别需要注意。
四、浮点数
1.IEEE浮点表示标准:
说明:
符号(S):当s=1为负数,当s=0为正数;
尾数(M):表示从(1~2)或者(0~1)之间的数;
阶码(E):可以是负数
单精度和双精度的表示:
① 规格化值 E ≠ 0 或者 E ≠ 255 (E不全为0或者1)
阶码段被解释成有偏置值形式的有符号整数:E = e - Bias (其中Bias = 2的k-1次方 - 1 ;以8位为例就是127,11位为例为2047)。
M尾数定义为1+f 也就是隐含了已1开头,多表示了1位。
② 非规格化值:(E全为0时)
上一个方法中如何来表示0呢,非规格化就是在表示非常接近于0和0的数
M = f
E = 1 - Bias
③ 特殊值(E全为1时)
a.当M=0时得到无穷大s=0是正无穷大,s=1时是负无穷大;
b.当M≠0时得到了Not a Number(NAN)
应用举例:以w=6位为例
我们尝试来表示最大的规格化数字:(E不全为0或者1)
合成6位的表示就是[0] [110] [11]也就是:011011;
解释一下:
S = 0 结果为 + 偏置值为 3
那么E不全为1,能表示的最大值就是[110]表示为:E = 6-3 = 3
M最大为[11]也就是3/4由于M = 1 + f 所以结果为 7 / 4
那么最后的计算就是:+1 * 8 * (7/4)= 14
如果以8位为例,我们看图就很好理解了】
主要解释一下过渡阶段:
非规格化偏置被设置成1-Bias,而不是-Bias。我们其实是补偿非规格化的尾数没有隐含的1开头这一事实,这样7/512和8/512就实现了平滑的过渡。
我们来练习一下将整数12345二进制表示为[11000000111001]转化为单精度浮点表示,将二进制左移13位表示为1.1000000111001 X 2的13次方。为了适应IEEE规格化表示,我们表示尾数M时丢弃掉最高位1,并在末尾增加10个0.M = [1000000111001 0000000000]。为了构造阶码字段我们用13+127 = 140 二进制表示为[10001100]再加上一个符号位0我们的最后结果就是[01000110010000001110010000000000]我们观察12345(0X3039)同浮点数12345.0(0X4640e400)的位级表示:
这样的存储结果,加*的上下两段其实是一样的。看起来完全不同的位级表示,其实有其内在的相同点。
2. 舍入
计算机使用的是向偶数舍入的方法,为了避免统计上的误差,在一半的时间向下舍入,另一半的时间向上舍入。
3.浮点数的数学属性
由于舍入而产生的丢失精度,浮点数的运算中不具有结合性
C语言中的浮点数使用注意事项:
【词汇】
程序对象:学习过汇编的同学应该不难理解,有点儿像程序的数据段、代码段,即是:程序数据、指令和控制信息;