计算机使用二值信号存储和表示信息。
当把位组合在一起,再加上某种解释(interpretation),即赋予不同的可能位模式以含意,我们就能够表示任何有限集合的元素。
无符号(unsigned)编码基于传统的二进制表示法,表示大于或者等于零的数字。
补码(two's-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
浮点数(floating-point)编码是表示实数的科学记数法的以2为基数的版本。
计算机的表示法是用有限数量的位来对一个数字编码,因此,当计算结果太大以至于不能表示时,就会产生溢出。
浮点数表示的精度有限,因而浮点运算是不可结合的(大数吃小数)。
整数的表示范围小但是精确,浮点数表示的范围大但是是近似的。
许多安全漏洞是由算术运算的微妙细节导致的。
GNU编译器套装(GNU Compiler Collection,GCC)可以基于不同的命令行选项,依照多个不同版本的C语言规则来编译程序,如图2-1所示。比如,根据ISO C11来编译程序prog.c,我们就使用命令行:
linux>gcc -std=c11prog.c
2.1信息存储
计算机一般使用字节作为最小的可寻址的内存单位,而不是访问内存中单独的位。
机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。
内存的每个字节都由一个唯一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space)。
顾名思义,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现(见第9章)是将动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。
在机器级程序中不包含关于数据类型的信息。
指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)是某个存储块的第一个字节的虚拟地址。
程序对象(program object):包括程序数据、指令和控制信息。
可以用各种机制来分配和管理程序不同部分的存储。这种管理完全是在虚拟地址空间里完成的。
C编译器把每个指针和类型信息联系起来,这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。
尽管C编译器维护着这个类型信息,但是它生成的实际机器级程序并不包含关于数据类型的信息。
每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。
2.1.1十六进制表示法
二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。因此使用十六进制(hexadecimal)数,来表示位模式。十六进制(简写为"hex")。
十六进制以0x开头。
A:10,1010;C:12,1100;F:15,1111.
字符'A'~'F'既可以是大写,也可以是小写,一个数字里甚至可以大小写混合。
进制转换:
2->16四位一组,不足的最前面补0
16->2每个十六进制位写成4个二进制位
10->16除16取余
16->10按权展开式
2.1.2字数据大小
每个计算机有对应的字长(word size),指明指针数据的标称大小(nominal size)。虚拟地址用一个字来编码,所以字长决定了虚拟地址空间的大小。64位机器的指针类型长度为8字节。
32位机器的虚拟地址空间为4GB,64位字长的虚拟地址空间位16EB。
大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。
当程序prog.c用如下伪指令编译后就可以在32位或64位机器上正确运行。
linux> gcc -m32prog.c
若程序用下述伪指令编译就只能在64位机器上运行。
linux> gcc -m64 prog.c
因此,我们将程序称为"32位程序”或"64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。
尽管"char"是由于它被用来存储文本串中的单个字符这一事实而得名,但char也能被用来存储整数值。
为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其中包括int32_t和int64_t类型,分别为4字节和8字节,不受机器影响。使用确定大小的整数类型很有用。
对32位和64位机器而言,char、short、int、longlong长度都是一样的,为1,2,4,8。long的长度不一样。
float和double的长度一样,分别为4,8
程序对char有无符号一般不敏感。
C中的字符编码是ASCII编码,只有0-127有对应字符。而0-127只需要7个bit位就可以表示了,但是计算机存储一般用字节作为最小单位会比较高效,所以char类型是用一个字节存储的,那么多出来的那一个比特位就可以用于表示正负号。
当使用有符号的char类型时,char对应的二进制数字范围是[-128,127],虽然负数部分没有对应的字符,但是仍然能转换成int类型打印出来。
无符号char类型对应的二进制数字范围是[0,255],其中128-255都是没有对应字符的,但是可以转为int输出。
2.1.3寻址和字节顺序
对于跨越多字节的对象,它的地址是它所用字节中的最小地址。
两种字节存储法:
小端法(little endian):数字的低位在前(前就是最小地址)
大端法(big endian):数字的高位在前
大多数Intel都是小端法,不是所有。
许多比较新的微处理器是双端法(bi-endian),也就是说可以把它们配置成作为大端或者小端的机器运行。然而,实际情况是:一旦选择了特定操作系统,那么字节顺序也就固定下来。
Android(来自Google)和iOS(来自Apple)只能运行于小端模式。
给C语言初学者 使用typedef来命名数据类型
Typedef可以极大地改善代码的可读性,因为深度嵌套的类型声明很难读懂。
typedef int *int_pointer;
int_pointer ip;
将类型"int_pointer"定义为一个指向int的指针,并且声明了一个这种类型的变量ip。
给C语言初学者 指针的创建和间接引用
C的”取地址”运算符&创建一个指针。
表达式&x创建了一个指向保存变量x的位置的指针。这个指针的类型取决于x的类型,因此这个指针的类型为int*、float*和void**。(数据类型void*是一种特殊类型的指针,没有相关联的类型信息。)
强制类型转换运算符可以将一种数据类型转换为另一种。因此,强制类型转换(byte_pointer)&x表明无论指针&x以前是什么类型,它现在就是一个指向unsigned char的指针。这里给出的这些强制类型转换不会改变真实的指针,它们只是告诉编译器以新的数据类型来看待被指向的数据。
旁注 生成一张ASCII表
可以通过执行命令manascii来得到一张ASCII字符码的表。
2.1.4表示字符串
C语言字符串是以null字符结尾的字符数组,即'\0'
ASCII字符适合编码英文文档。
在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
Unicode(UTF-8)使用4字节表示字符,一些常用的字符只需要1或2个字节,而不太常用的字符需要多一些的字节数。所有ASCII字符在UTF-8中是一样的。
JAVA使用UTF-8来编码字符串。
2.1.5表示代码
不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,所以二进制代码是不兼容的,一般无法在不同机器间移植。
从机器的角度看,程序就是一个字节序列。
2.1.6布尔代数
布尔代数是在0和1基础上的定义
可以把字节看作是一个长为8的位向量。
位向量的一个应用是表示有限集合。如位向量[0110 1001]表示集合A={0,3,5,6}。
网络旁注DATA:BOOL
关于布尔代数和布尔环的更多内容
乘法对加法的分配律,写为a•(b+c)=(a•b)+(a•c),
而布尔运算 & 对 | 的分配律,写为a&(b|c)=(a&b)|(a&c)。此外,布尔运算 | 对 & 也有分配律,写为a|(b&c)=(a|b)&(a|c)。
当考虑长度为w的位向量上的^、&和~运算时,会得到一种不同的数学形式,称为布尔环(Boolean ring)。
布尔环的“加法”运算是^,每个元素的加法逆元是它自己本身。也就是说,对于任何值a来说,a^a=0,这里我们用0来表示全0的位向量。可以看到对单个位来说这是成立的,即0^0=1^1=0,将这个扩展到位向量也是成立的。当我们重新排列组合顺序,这个属性也仍然成立,因此有(a^b)^a=b。
2.1.7C语言中的位级运算
C语言支持按位布尔运算。
C语言所使用的布尔运算符号:|就是OR(或),&就是AND(与),~就是NOT(取反),而^就是EXCLUSIVE-OR(异或)。这些运算能运用到任何“整型”的数据类型上。
以下是一些对char数据类型表达式求值的例子:
确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转换回十六进制。
位运算的常见应用是实现掩码。
掩码是一个位模式,表示从一个字中选出的位的集合,如掩码0xFF(最低的8位为1)表示一个字的低8位。
位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x=Ox89ABCDEF,x&0xFF将得到0x000000EF。
表达式~0可以生成一个全1的掩码,不管机器的字大小是多少。对于32位机器来说,同样的掩码可以写成OxFFFFFFFF,但是这样的代码不是可移植的。
2.1.8C语言中的逻辑运算
C语言还提供了一组逻辑运算符||、&&和!,分别对应于命题逻辑中的OR、AND和NOT运算。逻辑运算和位级运算的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。它们返回1或者0,分别表示结果为TRUE或者为FALSE。
逻辑运算符&&和||如果第一个参数就能确定结果,就不再计算第二个参数。因此,例如,表达式a&&5/a将不会造成被零除,而表达式p&&*p++也不会导致间接引用空指针。
2.1.9C语言中的移位运算
左移k位丢掉最高的k位,并在右端补k个0。
移位运算是从左至右可结合的,所以x<<j<<k等价于(x«j)«k。
右移分为逻辑右移和算术右移。逻辑右移左端补0,算术右移左端补最高有效位的值。
一般都对有符号数使用算术右移,即补符号位的值。无符号数,只能是逻辑右移,即补0。
C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。然而,实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移。
旁注 移动k位,这里k很大
当移动一个w位的值时,移位指令只考虑位移量的低log2w位,因此实际上位移量就是通过计算kmodw得到的。
例如,当w=32时,移动32、36、40位运算分别是移动0、4和8位。32=100000,只考虑log2w位,即低5位,即00000,即移动0位,不移动。
2.2整数表示
有符号数到无符号数的转换会产生漏洞,避免错误的方法之一是绝不使用无符号数。
除了C以外很少有语言支持无符号整数,Java就只支持有符号数。
2.2.1整数数据类型
整型数据类型表示有限范围的整数。
在64位系统上
int:4字节,可表示十进制数字位数:10位(-20~20亿以内)(注:2^32=4294967296)
long long:8字节,可表示十进制数字位数:19位(千亿亿级)
long:8字节
double:8字节,精度15位,可表示十进制数字位数308位
float:4字节,精度6位,可表示十进制数字38位
char:-128~127
Java只支持有符号数。
旁注 float和double
某些语言将float分为float32和float64,C语言不这样区分,而是分为float和double。(这两种叫法是一模一样的东西,只是名字不同)
float,也即我们常说的单精度,存储占用4个字节,也即4*8=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数
double,也即我们熟悉的双精度,存储占用8个字节,也即8*8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数
精度主要取决于尾数部分的位数。
对于float(单精度)来说,表示尾数的为23位,除去全部为0的情况以外,最小为2^-23,约等于1.19*10^-7,所以float小数部分只能精确到后面6位,加上小数点前的一位,即有效数字为7位。
同理double(双精度)的尾数部分为52位,最小为2^-52,约为2.22*10^-16,所以精确到小数点后15位,加上小数点前的一位,有效位数为16位。
浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在math包中找到:
常量math.MaxFloat32表示float能取到的最大数值,大约是3.4e38;
常量math.MaxFloat64表示double能取到的最大数值,大约是1.8e308;
float和double能表示的最小值分别为1.4e-45和4.9e-324。
浮点数虽然能表示的数值很大,但精度位却没有那么大。
float的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
double的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度
举例说明:
10000018这个数,用float的类型来表示的话,由于其有效位是7位,将10000018表示成科学计数法,就是1.0000018*10^7,能精确到小数点后面6位(刚好没超过2^-23约等于1.19*10^-7,所以还可以准确表示)。此时用科学计数法表示后,小数点后有7位,刚刚满足我们的精度要求,意思是此时你对这个数进行+1或者-1等数学运算,都能保证计算结果是精确的。
100000187,同样使用float类型,表示成科学计数法,由于精度有限,表示的时候小数点后面7位是准确的,但若是对其进行数学运算,由于第八位无法表示,所以运算后第七位的值,就会变得不精确。由于精度的问题,就会出现这种很怪异的现象:myfloat == myfloat+1会返回true。
2.2.2无符号数的编码
无符号表示、补码表示与数据的映射都是双射,即一一对应。
函数B2Uw(Binary to Unsigned 的缩写,长度为w):将一个01串直接看成一个数,那么将会得到一个无符号数。
2.2.3补码编码(two's-complement)
补码的定义实际就是将符号位解释为负权。
将字的最高有效位解释为负权(negativeweight)。我们用函数B2Tw(Binary to Two's-complement的缩写,长度为w)来表示。
它能表示的最小值是位向量[10...0](也就是设置这个位为负权,但是清除其他所有的位),而最大值是位向量[01...1](清除具有负权的位,而设置其他所有的位)。
我们定义函数T2Bw(即“补码到二进制")作为B2Tw的反函数。
注意:
补码的范围是不对称的:|TMin|=|TMax|+1,也就是说,TMin没有与之对应的正数
最大的无符号数值刚好比补码的最大值的两倍大一点:|UMax|=2|TMax」+1。补码表示中所有表示负数的位模式在无符号表示中都变成了正数。
-1和UMax有同样的位表示:一个全1的串。数值0在两种表示方式中都是全0的串。
C库头文件<limits.h>定义了一组常量来限定不同整数数据类型的取值范围。INT_MAX、INT_MIN、UINT_MAX,对于一个补码的机器,数据类型int有w位,这些常量就对应于TMaxw、TMinw和UMaxw的值。
旁注 关于确定大小的整数类型的更多内容
C库头文件<stdint.h>中定义了形如intN_t和uintN_t的一组数据类型,一般N可以是8,16,32,64。uint16_t,int32_t等类型,用于声明确定宽度类型的整数。
这些数据类型对应着一组宏,定义了每个N的值对应的最小和最大值。
这些宏名字形如INTN_MIN、INTN_MAX和UINTN_MAX。
确定宽度类型的带格式打印需要使用宏,以与系统相关的方式扩展为格式串。因此,举个例子来说,变量x和y的类型是int32_t和uint64_t,可以通过调用printf来打印它们的值,如下所示:
printf("x=%"PRId32",y=%"PRIu64"\n",x,y);
编译为64位程序时,宏PRId32展开成字符串“l””u”宏PRIu64则展开成两个宇符串"l""u"。当C预处理器遇到仅用空格(或其他空白字符)分隔的一个字符串常量序列时,就把它们串联起来。因此,上面的printf调用就变成了:
printf("x=%d,y=%lu\n",x,y);
使用宏能保证:不论代码是如何被编译的,都能生成正确的格式字符串。
旁注 有符号数的其他表示方法
反码和原码对于数字0都有两种不同的编码方式。
[00…0]都解释为+0。而值-0在原码中表示为[10…0],在反码中表示为[11…1]。几乎所有的现代机器都使用补码。
补码:对于非负数x,我们用2^w - x来计算-x的w位表示。
反码:我们用[111…1] - x来计算-x的反码表示。
计算-1的反码表示(w=8):
2.2.4有符号数和无符号数之间的转换
在有符号数与无符号数之间进行强制类型转换的结果是保持位值不变,只改变解释位的方式。
T2U = B2U(T2B)
U2T = B2T(U2B)
给定位模式的两个数值(补码和无符号数)之和等于2^w。
无符号表示中的UMax有着和补码表示的-1相同的位模式。因此1 + UMaxw = 2^w。
图2-17说明了函数T2U的一般行为。如图所示,当将一个有符号数映射为它相应的无符号数时,负数就被转换成了大的正数,而非负数会保持不变。
最靠近0的负数映射为最大的无符号数。在另一个极端,最小的负数映射为一个刚好在补码的正数范围之外的无符号数。
2.2.5C语言中的有符号数和无符号数
通常,大多数数字都默认为是有符号的。例如,当声明一个像12345或者0x1A2B这样的常量时,这个值就被认为是有符号的。
要创建一个无符号常量,必须加上后缀字符'U'或者'u',例如,12345U或者0x1A2Bu。
int tx, ty;
unsigned ux, uy;
//case1显式类型转换
tx = (int)ux;
uy = (unsigned)ty;
//case2隐式类型转换
tx = ux;
uy = ty;
C语言中有符号数和无符号数相加减,有符号被转换成无符号,都成为非负数再运算。
隐式强制转换对于标准的算术运算来说并无多大差异,但对于像<和>(小于,大于)这样的关系运算符来说,它会导致非直观的结果。如图所示:
网络旁注DATA:C语言中TMin的写法
在C头文件limits.h,使用了跟上表写TMin32和TMax32类似的方法:
/*Minimum and maximumvalues a'signed int'can hold.*/
#define INT_MAX2147483647
#define INT_MIN(-INT_MAX-1)
原因和补码表示的不对称性和C语言的转换规则之间奇怪的交互有关,本书此处不做深究。
2.2.6扩展一个数字的位表示
扩展无符号数使用零扩展,即在最高位前加0
扩展有符号数使用符号扩展,即在最高位前加最高有效位的值
C语言标准要求:在一次类型转换中,既包含长度转换(扩展),又包含有无符号的转换,那么先要改变大小,之后再完成从有符号到无符号的转换。
举例:
short sx = -12345; /*-12345*/
unsigned uy = sx; /*4 294 954 951*/
当把short转换成unsigned时,
(unsigned)sx等价于(unsigned)(int)sx,求值得到4294 954 951,而不等价于(unsigned)(unsignedshort)sx,后者求值得到53 191。
2.2.7截断数字
对一个w位的数字截断为一个k位数字,将丢弃高w-k位。
对于无符号数而言,截断后的数字实际上等于w mod 2^k,即取余。
2.2.8关于有符号数与无符号数的建议
许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。
当我们想要把字仅仅看做是位的集合而没有任何数字意义时,无符号数值是非常有用的。例如,往一个字中放入描述各种布尔条件的标记(flag)时,就是这样。地址自然地就是无符号的,所以系统程序员发现无符号类型是很有帮助的。当实现模运算和多精度运算的数学包时,数字是由字的数组来表示的,无符号值也会非常有用。
2.3整数运算
两个正数相加会得出一个负数,而比较表达式x<y和比较表达式x-y<0会产生不同的结果。这些属性是由于计算机运算的有限性造成的。
2.3.1无符号加法
两个w位数字相加可能需要w+1位表示,这种持续的”字长膨胀”意味着,要想完整地表示算术运算的结果,我们不能对字长做任何限制。
C语言支持固定精度的运算,因此像“加法”和"乘法”这样的运算不同于它们在整数上的相应运算。
考虑溢出,C语言不会将溢出作为错误发出信号
当x+y>=2^w,实际结果为s=x+y-2^w
对任意的x+y,s=(x+y)%2^w
溢出的结果:和小于两个加数
检验溢出的方式:如果s<x,说明溢出。
模数加法形成了一种数学结构,称为阿贝尔群(Abeliangroup),
它是可交换的、可结合的。它有一个单位元0,并且每个元素有一个加法逆元。
该加法的逆操作可以表述如下:
无符号数的非:~x = 2^w - x(x>0)
2.3.2补码加法
当x+y >= 2^(w-1),s = x+y-2^w
当x+y < -2^(w-1),s = x+y+2^w
正溢出的结果是负数,负溢出的结果是正数。
检验溢出的方式:当x,y>0而s<=0是正溢出;当x,y<0而s>=0是负溢出
两个数的w位补码之和与无符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器
指令来执行无符号或者有符号加法。
2.3.3补码的非
当x = TMin,-x = TMin;当x ≠ TMin,-x = -x
网络旁注DATA
执行位级补码非的第一种方法:对每一位求补,结果再加1
计算补码非的第二种方法:假设k是最右边的1的位置,对k左边的所有位取反
2.3.4无符号乘法
无符号乘法的积 m = (x*y) % 2^w
C语言中的无符号乘法被定义为产生w位的值,就是2w位的整数乘积的低w位表示的值。
将一个无符号数截断为w位等价于计算该值模2^w
2.3.5补码乘法
可以认为补码乘法和无符号乘法的位级表示是一样的
C语言在运算时将x,y视为无符号数进行乘法运算,结果取余后将其按补码方式解释
补码乘法的积 m = (x*y) % 2^w
2.3.6乘以常数
大多数机器上,整数乘法需要10个或更多的时钟周期,而加法、减法、位级运算和移位只需要1个时钟周期。因此,编译器使用了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。
左移k位等于乘以2^k,如
x * 14 = (x<<3)+(x<<2)+(x<<1) = (x<<4)-(x<<2)
判断如何移动的方式很简单:14的位级表示为1110,所以分别左移3,2,1
大多数编译器只在需要少量移位、加法和减法就足够的时候才使用这种优化。
2.3.7除以2的幂
大多数机器上,整数除法更慢,需要30个或更多的始终周期。
整数除法向0舍入。即向下舍入一个正值,而向上舍入一个负值。
(只有)除以2的幂可以用移位运算来代替,无符号采用逻辑右移,补码采用算术右移
对于有符号数而言,算术右移的结果相当于进行除法运算后向下舍入
使用(x+(1<<k)-1)>>k的结果相当于进行除法运算然后向零舍入
//代码实现求x/2^k
(x<0? x+(1<<k)-1 : x) >> k;
2.3.8关于整数运算的最后思考
计算机执行的“整数”运算实际上是一种模运算形式。
补码使用了与无符号算术运算相同的位级实现,包括加法、减法、乘法甚至除法。都有完全一样或非常类似的位级行为。
2.4浮点数
浮点数对于非常大,非常接近零,近似值计算都很有用。
目前,实际上所有的计算机都支持这个后来被称为IEEE浮点的标准
旁注 电气和电子工程师协会
IEEE,读做"eye-triple-ee"。
2.4.1二进制小数
小数的二进制表示法只能表示那些能够写为x * 2^w 的数,其他的数都是近似表示。x 必须可以由形如 2^i + 2^j + ... + 2^n 的多项式表示
形如0.11…12的数表示的是刚好小于1的数。用简单的表达法1-ℇ来表示这样的数值。
二进制小数点向左移动一位相当于这个数被2除,二进制小数点向右移动一位相当于将该数乘2。
浮点运算的不精确性可能产生严重后果。
二进制转十进制:
2.4.2IEEE浮点表示
IEEE浮点标准的表示形式为:V= (-1)^S * M * 2^E,它分为三部分:
符号(sign):S决定这数是负数(s=l)还是正数(s=O),而对于数值0的符号位解释作为特殊情况处理。
阶码(exponent):E的作用是对浮点数加权,这个权重是2的E次幕(可能是负数)。
尾数(significand):M是一个二进制小数,范围是1~2-ε或0~1-ε
在对浮点数的位编码时:
一个单独的符号位编码直接编码S
k位的阶码字段exp编码E;float中k=8,double中k=11
n位的小数字段frac编码M;float中n=23,double中n=52,编码出来的值也依赖于阶码字段的值是否等于0。
偏置值bias=2k-1-1:单精度是127,双精度是1023。
小数字段frac被解释为描述小数值f,其中O≤f<1,其二进制表示为0.fn-1…f1f0,也就是二进制小数点在最高有效位的左边。
有时,这种方式也叫做隐含的以1开头的(implied leading1)表示,因为我们可以把M看成一个二进制表达式为1.fn-1fn-2…f0的数字。
既然我们总是能够调整阶码E,使得尾数M在范围1≤M<2之中(假设没有溢出),那么这种表示方法是一种轻松获得一个额外精度位的技巧。既然第一位总是等于1,那么我们就不需要显式地表示它。
E和M的编码方式分为三种情况:
规格化的值:阶码字段即不全为0也不全为1时属于规格化值(0001~1110)
a)阶码字段解释方式:E = e - Bias;e是无符号数,如k=4时,E的范围是-6~7;(由此得到的指数对于单精度是-126~+127,而对于双精度是-1022~+1023。
b)小数字段解释方式:M = 1 + f。
非规格化的值:阶码字段全为0时属于非规格化形式
阶码字段解释方式:E = 1 - Bias;与规格化值中e = 1时的E相同
小数字段解释方式:M = f,也就是小数字段的值,不包含隐含的开头的1。
旁注 非规格化值
非规格化值这样设置偏置值看似违反直觉,但是实际上这种方式提供了一种从非规格化值平滑转换到规格化值的方法。
特殊值:阶码字段全为1时,分两种情况:
小数字段全为0:表示无穷
小数字段非零:表示NaN。即“不是一个数(Not aNumber)”的缩写。出现在一些运算的结果不能是实数或无穷、表示未初始化的数据,比如∞-∞的结果就返回NaN。
2.4.3数字示例(略)
0有+0.0和-0.0两种表示方式
最大非规格化数到最小规格化数的过渡是平滑的。
浮点数能够使用正数排序函数来排序,即浮点数的位级表示当用整数方式来解释时是顺序的(正数升序负数降序)。
浮点数可表示的数的分布是不均匀的,越接近零时越稠密
几个特殊的值的位级表示:
+0.0全为0
最小的正非规格化值:最低有效位为1,其他为0
最大的非规格化值:小数字段全为1,其他为0
最小的正规格化值:阶码字段最低位为1,其他为0
最大的规格化值:阶码字段最低位为0,符号位为0,其他为1
2.4.4舍入(略)
因为范围和精度有限,浮点运算只能近似表示实数运算。
在浮点数的近似匹配上,IEEE浮点格式定义了四种舍入方式(默认第一种):
向偶数舍入(向最接近的值舍入):非中间值(0.5)四舍五入,中间值向偶数舍入。
向零舍入
向下舍入
向上舍入
向偶数舍入可以计算一组数的平均数时避免统计偏差。
实际上这种舍入是发生在二进制小数上的。
2.4.5浮点运算(略)
IEEE标准定义1/-0 = -∞,1/+0 = +∞
浮点运算是可交换不可结合也不可分配的。
浮点加法满足加法和乘法上的单调性。如果a>=b,则x+a>= x+b
缺乏结合性和分配性会使一些简单问题变得很复杂
2.4.6C语言中的浮点数
在支持IEEE浮点格式的机器上,float和double类型对应于单精度和双精度浮点。另外,这类机器使用向偶数舍入的舍入方式。
C语言标准不要求机器使用IEEE浮点,所以没有标准的方法来改变舍入方式或者得到诸如-0、+∞、-∞或者NaN之类的特殊值。大多数系统提供include('.h')文件和读取这些特征的过程库,但是细节随系统不同而不同。
例如,当程序文件中出现下列句子时,GNU编译器GCC会定义程序常数INFINITY(表示+∞)和NAN(表示NaN):
#define_GNU_SOURCE 1
#include<math.h>
在int、float、double间进行强制类型转换时的几种情况:
int到float:不会溢出,可能舍入
int或float到double:精确,不会溢出也不会舍入
double到float:可能溢出和舍入
float或double到int:向零舍入,很大时可能溢出,很接近零时也可能溢出。当从浮点转换到整数时如果溢出,转变结果都为[10…00],因此一个正浮点可能得到一个负整数。
把大的浮点数转换为整数是一种常见的错误。
C语言标准本身没有对这种情况指定固定的结果。与Intel兼容的微处理器指定位模式[10…00](字长为w时的TMinw)为整数不确定(integer indefinite)值。
要小心地使用浮点运算。