上一期的链接:
《深入理解计算机系统》CSAPP,全书笔记分章节分享(一)-CSDN博客
目录
第2章信息的表示和处理
现代计算机存储和处理的信息以二值信号(0、1)表示。
三种常见数字表示:
1、无符号(unsigned)编码:在二进制数中表示非负整数的一种方式。在无符号编码中,所有的位(bits)都被用来表示数值的大小,没有位被用作符号位来区分正负。
2、补码(two's-complement)编码:表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。在补码系统中,正数的表示与其二进制表示相同,而负数的表示则是其正数形式的二进制反码(每一位取反)加1。
* 例如,8位二进制数的补码表示:
正数5的二进制是0000 0101
。
负数-5的补码是1111 1011
(正数5的反码1111 1010
加1)
3、浮点数(floating-point)编码:是用于表示实数的一种方法,它允许表示非常大或非常小的数值。浮点数通常由三部分组成:符号位(最高位,0表示正数,1表示负数)、指数部分(表示数值的范围,接下来的几位用于表示指数,它是一个偏移的二进制数,用于调整尾数的位置。)和尾数(或称为小数部分,表示数值的精度)。
溢出(overflow):计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表 示时,某些运算就会溢出。
2.1 信息存储
字节(Byte):最小的可寻址的内存单位,而非访问内存中单独的位。一个字节由8位组成(8-bit块,值域:00000000~11111111)。用于表示计算机cpu中的寄存器(e.g:5GB)。
字:用于表示计算机cpu中的寄存器。CPU中ALU的数据位数=CPU中通用寄存器的位数,通常计算机是XX位的,是指这台计算机CPU字的长度。汇编语言/机器语言编程中,一个字指的是16位。数据存放时高字节在高地址、低字节在低地址。
位(Bit,比特):最底层的二进制数字(数码),是信息的基本单位,代表一个二进制值,即0或1。是计算机存储和处理数据的最小单元。
位组合:把位组合到一起,采用某种规则进行解读。每个位组合都有含义:LSB(最低有效位(Least Significant Bit))、MSB(最高有效位(Most Significant Bit))。
虚拟内存(virtual memory):机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。
地址(address):内存的每个字节都由一个唯一的数字来标识,称为它的地址。
虚拟地址空间(virtualaddress space):所有可能地址的集合就称为虚拟地址空间。这只是一个展现给机器级程序的概念性映像,是将动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。
程序对象(program object):程序数据、指令、控制信息。
编译器和运行时系统会帮助程序员更有效地管理程序中的数据和指令。它们把内存分成小块,每块用来存储不同类型的数据或指令。这个过程是在虚拟地址空间中完成的,也就是说,内存被逻辑上划分,而不是物理上分割。
在C语言中,指针用来指向程序中的不同对象,比如整数、结构体等。每个指针都关联着一个虚拟地址,这个地址是它指向的数据块的起始位置。C编译器会根据指针的数据类型生成相应的机器代码,以正确地访问和操作这些数据。
尽管编译器知道每个指针的数据类型,但编译后生成的机器代码本身并不包含这些类型信息。在机器代码层面,所有的程序对象都被视为字节序列,没有类型区分。这就意味着,虽然编译器在编译时会考虑数据类型,但最终的执行是由机器根据内存地址来完成的,而不考虑数据的类型。
C语言中指针的作用:
指针是C语言的一个重要特性。它提供了引用数据结构(包括数组)的元素的机制。 与变量类似,指针也有两个方面:值和类型。它的值表示某个对象的位置,而它的类型表示那个位置上所存储对象的类型(比如整数或者浮点数)。
2.1.1 十六进制表示法
C语言中的16进制的值:数字常量以0x或0X开头。字符‘A’~’F‘ 既可以是大写,也可以是小写,还可以混写。
2进制:逢二进一,由0和1两个数码组成,基数为2,各个位权以表示
16进制:基数16,逢16进位,位权为,16个数码: 0, 1,2,3,4,5,6,7,8,9,A,B,C,D,E,F
2进制和16进制之间的转换:每4个二进制位对应1个十六进制位,若位总数不是4的倍数,最左边的一组可以少于4位,前面用 0 补足。然后将每个4位组转换为相应的十六进制数字。(e.g: 00111010B=3AH,F2H=11110010B)
10进制和16进制之间的转换:将一个10进制数字x转换为16进制,可以反复地用 16 除 x 得到一个商 q 和一个余数 r 也就是 x=g*16+r 然后,我们用十六进制数字表示的r作为最低位数字,并且通过对q反复进行这个过程得到剩下的数字。例如:十进制 314 156 的转换:
反过来,将一个十六进制数字转换为十进制数字,我们可以用相应的16的幂乘以每个十六进制数字。 比如,给定数字 0x7AF 我们计算它对应的十进制值为7*16²+10* 16+15=7•256+10 •16+15=1792+160+15=1967。
2、8、16进制间的转换:4个2进制位对应1个16进制位、3个2进制位对应1个8进制位。
2.1.2 字数据大小
字长(word size):每台计算机都有一个(32位字长 or 64位字长),用来指明指针数据的标称大小(nominalsize)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为ω位的机器而言,虚拟地址的范围为0~-1, 程序最多访问个字节。
32位字长:限制虚拟地址空间为4千兆字节(4GB),也就是说,刚刚超过4*字节。
64位字长:虚拟地址空间为16EB,大约是1.84*字节。
向后兼容:大多数64位机器可以运行32位机器编译的程序。
当程序prog.c用如下伪指令编译后,该程序就可以在32或64位机器上正确运行:
linux> gcc -m32 prog.c
但若是用如下伪指令编译后,则只能在64位机器上运行:
linux> gcc -m64 prog.c
对关键字的顺序以及包括还是省略可选关键字来说,C语言允许存在多种形式。例如以下的所有声明均是一个意思:
unsigned long
unsigned long int
long unsigned
long unsigned int
2.1.3 寻址和字节顺序
在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
而针对跨越多字节的程序对象,需要建立的两个规则:
1、这个对象的地址是什么
2、在内存中如何排列这些字节
e.g:若一个类型为int的变量x的地址为0x100,则其地址表达式&x的值为0x100。那么,(假设数据类型int为32位表示)x 的4个字节将被存储在内存的0x100、0x101、0x102和 0x103位置
排列表示一个对象的字节的两个通用规则:
1、小端法(littleendian):最低有效字节在最前面的方式
2、大端法(bigendian):最高有效字节在最前面的方式
大多数Intel兼容机都只用小端模式,而许多比较新的微处理器用的是双端法。
双端法(bi-endian):配置为大端或者小端的机器运行。
实际情况中::一旦选择了特定操作系统,那么字节顺序也就固定下来了。比如,用于许多移动电话的ARM微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统 Android(Google)和 IO( Apple)—— 却只能运行于小端模式。
无论为哪种类型的机器所编译的程序都会得到同样的结果。不过有时候,字节顺序会成为问题。首先是在不同类型的机器之间通过网络传送二进制数据时,一个常见的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时,接收程序会发现,字里的字节成了反序的。为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。
反汇编器(disassembler):一种确定可执行程序文件所表示的指令序列的工具。
PS:可以通过执行如下指令来得到一张ASCII表:
man ascii
2.1.4 表示字符串
C语言中字符串被编码为一个以 null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。
在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。所以,文本数据比二进制数据具有更强的平台独立性。
2.1.5 表示代码
示例机器配置:
Linux 32: 运行 Linux的 Intel IA32 处理器。
Windows: 运行 Windows 的 Intel IA32 处理器。
Sun: 运行Solaris的 Sun Microsystems SPARC处理器。(这些机器现在由 Oracle生产。)
Linux 64: 运行 Linux的 Intel x86-64 处理器
在示例机器上编译如下代码时,会生成如下字节表示的机器代码:
int sum(int x, int y)
{
return x + y;
}
可以发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方式。
即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。
二进制代码很少能在不同机器和操作系统组合之间移植。
计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调试的辅助表以外。
2.1.6 布尔代数简介
二进制值是计算机编码、存储和操作的核心。
布尔代数(Boolean algebra):将逻辑值 TRUE(真)和 FALSE(假)编码为二进制值1和0,以研究逻辑推理的基本原则。
四个常见的布尔运算:
可以将上述4个布尔运算推广到位向量的运算:
2.1.7 C语言中的位级运算(&, |, ~, ^)
2.1.8 C语言中的逻辑运算(||、&&、!)
C语言还提供了一组逻辑运算符||、&&和!,分别对应于命题逻辑中的OR、AND 和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。
逻辑运算和位级运算的区别:
1、逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。它们返回的计算结果总是1或者0,分别表示结果为TRUE或者为FALSE。以下是一些表达式求值的示例:
可以观察到,按位运算只有在特殊情况下,即:参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。
2、逻辑运算符&&和||与它们对应的位级运算 &和|之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。
因此, 例如,表达式a&&5/a将不会造成被零除,而表达式p&&*p++也不会导致间接引用空指针。
2.1.9 C语言中的移位运算(>>、<<)
C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。
但实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。
与C相比,Java对于如何进行右移有明确的定义。表达是x>>k会将x算术右移k个位置,而x>>>k会对x做逻辑右移。
2.2 整数编码(Encoding Integers)
用位来编码整数的两种不同的方式:1、只能表示非负数 2、能够表示负数、零和正数。
PS:用于精确定义和描述计算机如何编码和操作整数的相关数学术语:
2.2.1 整型数据类型
C语言支持多种整型数据类型——表示有限范围的整数。
根据字节分配,不同的大小所能表示的值的范围是不同的。其中,唯一一个与机器相关的取值范围是大小指示符long的。大多数64位机器使用8个字节的表示,比32位机器上使用的4个字节的表示的取值范围大很多。
ps:以指示被表示的数字是非负数 (声明为 unsigned) 或者可能是负数(默认)。
上述体现的特点:取值范围不是对称的——复数的范围比整数的范围大1。
特别地,除了固定大小的数据类型之外,我们看到它们只要求正数和负数的取值范围是对称的。
固定大小的数据类型保证数值的范围与图2-9给出的典型数值一致,包括负数与正数的不对称性。
2.2.2 无符号数的编码
ω表示整数数据类型的位数(长度)。位向量,表示整个向量,并将看成一个二进制表示的数,即可得到的无符号表示。在这个编码中每个位都取值为0或1,后一种取值意味着数值应为数字值的一部分:
无符号数(Unsigned number):是一种在计算机科学中使用的数值表示方法,它不包含任何符号位,只用来表示非负整数。因此,无符号数的编码确实都是正数,包括零。
在计算机中,无符号数的表示范围从 0 到 -1,其中 n 用于表示该数值的位数。例如,一个 8 位的无符号整数可以表示的数值范围是从 0 到 255。
2.2.3 补码编码
常见的有符号数编码方式包括:
- 原码(Sign-magnitude representation):最高位作为符号位,其余位表示数值的大小。
- 补码(Two's complement representation):这是计算机中最常用的有符号数表示方法,它允许直接进行加法和减法运算,同时也可以表示负数。
在补码表示中,正数的表示与无符号数相同,但负数通过取反(除符号位外的所有位取反)然后加一来表示。
补码(two’s-complement)形式:最常见的有符号数的计算机表示方式。通常使用一个单独的位(通常是最高位)作为符号位,用来表示数值是正数还是负数。在有符号数的表示中,数值可以是正数、零或负数。
PS:关于整数数据类型的取值范围和表示,Java标准是非常明确的。它要求采用补码表示,取值范围与图2-10中64位的情况一样。在Java中,单字节数据类型称为byteÿ 而不是char。 这些非常具体的要求都是为了保证无论在什么机器上运行,Java程序都能表现地完全一样。
2.2.4 有符号数和无符号数之间的转换
C语言允许在各种不同的数字数据类型之间做强制类型转换。例如,假设变量x声明为int, u声明为unsigned。 表达式(unsigned)x会将 x 的值转换成一个无符号数值,而 (int)u 将 u 的值转换成一个有符号整数。
对于大多数C语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。
无符号数与补码之间的转换:
取反加一:(~x)+1 = -x 【无符号数取反=补码加一】
给定位模式的补码与无符号数之间的关系可以表示为函数T2U的一个属性:
推导:补码转换为无符号数。
比如说,图2-16比较了当w=4时函数B2U和B2T是如何将数值变成位模式的。对补码来说,最高有效位是符号位,我们用带向左箭头的条来表示。对于无符号数来说,最高有效位是正权重,我们用带向右的箭头的条来表示。从补码变为无符号数,最高有效位的权重从-8变为+8。因此,补码表示的负数如果看成无符号数,值会增加=16。因而,-5变成了+11,而-1变成了+15。
图2-17说明了函数T2U的一般行为。如图所示,当将一个有符号数映射为它相应的无符号数时,负数就被转换成了大的正数,而非负数会保持不变。
整理:
2.2.5 C语言中的有符号数与无符号数
如图2-9 和图2-10所示,C语言支持所有整型数据类型的有符号和无符号运算。尽管C语言标准没有指定有符号数要采用某种表示,但是几乎所有的机器都使用补码。通常,大多数数字都默认为是有符号的。例如,当声明一个像12345或者0x1A2B这样的常量时, 这个值就被认为是有符号的。
要创建一个无符号常量,必须加上后缀字符‘U’或者‘u' 。e.g:12345U或者 0x1A2Bu。
C语言允许无符号数和有符号数之间的转换。虽然C标准没有精确规定应如何进行这种转换,但大多数系统遵循的原则是底层的位表示保持不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数U2,而从有符号数转换为无符号数时,就是应用函数T2 。其中ω切表示数据类型的位数。
由于C语言对同时包含有符号和无符号数表达式的这种处理方式,出现了一些奇特的行为。当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C 语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。这种方法对于标准的算术运算来说并无多大差异,但是对于像这样的关系运算符来说,它会导致非直观的结果。
这里假设数据类型 int 表示为32位补码。考虑比较式 -1<0U。因为第二个运算数是无符号的,第一个运算数就会被隐式地转换为无符号数,因此表达式就等价于4294967295U<0U(回想T2 (-1)=UMa),这个答案显然是错的。其他示例也可以通过相似的分析来理解。
有符号数和无符号数之间的基本转换规则:
2.2.6 扩展一个数字的位表示
一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。当然,当目标数据类型太小以至于不能表示想要的值时,这根本就是不可能的。然而,从一个较小的数据类型转换到一个较大的类型,应该总是可能的。
零扩展(zero extension):只要简单地在表示的开头添加0,即可将一个无符号数转换为一个更大的数据类型,其原理如下:
例子1:
例子2:
拓展的推导:
从短整数类型向长整数类型转换时,C自动进行符号扩展
总结:扩展的方法:
2.2.7 截断数字
假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。例如下面代码中这种情况:
int x = 53191;
short sx = (short) x; /* -12345 */
int y = sx; /* -12345 */
当我们把x强制类型转换为short时,我们就将32位的int截断为了16位的 short int。就像前面所看到的,这个16位的位模式就是一12345的补码表示。当我们把它强制类型转换回 int 时,符号扩展把高16位设置为1,从而生成一12345的32位补码表示。
补码截断也具有相似的属性,只不过要将最高位转换为符号位:
注:
1、无论有/无符号数:多出的位(高位)均被截断结果重新解读。
2、无符号数:相当于求模运算;有符号数:与求模运算相似。
对比总结扩展与截断:
2.2.8 关于有符号数与无符号数的建议
就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的, 程序员经常忽视了它的影响。
我们已经看到了许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。
实际上,除了C以外很少有语言支持无符号整数。
很明显,这些语言的设计者认为它们带来的麻烦要比益处多得多。比如,Java只支持有符号整数,并且要求以补码运算来实现。正常的右移运算符>>被定义为执行算术右移。特殊的运算符 >>>指定为执行逻辑右移。
当我们想要把字仅仅看做是位的集合而没有任何数字意义时,无符号数值是非常有用的。例如,往一个字中放入描述各种布尔条件的标记(flag)时,就是这样。地址自然地就是无符号的,所以系统程序员发现无符号类型是很有帮助的。当实现模运算和多精度运算的数学包时,数字是由字的数组来表示的,无符号值也会非常有用。
2.3 整数运算
我们经常发现,两个正数相加会得到一个负数,而比较表达式 x<y 和比较表达式 x-y<0 会产生不同的结果。这些属性是由于计算机运算的有限性造成的。
理解计算机运算的细微之处能够帮助程序员编写更可靠的代码。
2.3.1 无符号加法
考虑两个非负整数 x 和 y,满足 0≤x,y<。每个数都能表示为 ω 位无符号数字。然而, 如果计算它们的和,我们就有一个可能的范围 0≤x+y≤-2 。表示这个和可能需要 ω+1 位。
例如,下图就展示了当 x 和 y 有 4 位表示时,函数 x+y 的坐标图。参数(显示在水平轴上)取值范围位0~15,但是和的取值范围为0~30。函数的形状是一个有坡度的平面(在两个维度上,函数都是线性的)。
如果保持和为一个 ω+1 位的数字,并且把它加上另外一个数值,我们可能需要 ω+2 个位,以此类推。这种持续的“字长膨胀”意味着,要想完整地表示算术运算的结果,我们不能对字长做任何限制。
一些编程语言,例如 Lisp,实际上就支持无限精度的运算,允许任意的(当然,要在机器的内存限制之内)整数运算。更常见的是,编程语言支持固定精度的运算,因此像“加法”和“乘法’’这样的运算不同于它们在整数上的相应运算。
整数加法可视化与无符号数加法可视化的示意图对比:
推导——无符号数加法:
当执行C程序时,不会将溢出作为错误而发信号。不过有的时候,我们可能希望判定是否发生了溢出。
阿贝尔群(Abelian group) :模数加法形成的一种数学结构,是以丹麦数荸家 Niels Henrik Abel(1802~1829)的名字命名。也就说,它是可交换的(这就是为什么叫 “abelian”的地方)和 可结合的。它有一个单位元 0,并且每个元素有一个加法逆元。让我们考虑 ω 位的无符号数的集合,执行加法运算 。对于每个值x,必然有某个值 x 满足 x x=0。该加法的逆操作可以表述如下:
阿贝尔群的数学性质:
2.3.2 补码加法
对于补码加法,我们必须确定当结果太大(为正)或者太小(为负)时,应该做些什么。
∴ 整数和补码加法之间的关系:当x+y小于时,产生负溢出;当它大于时,产生正溢出
下图阐述了字长 ω=4 的补码加法。运算数的范围为 -8~7 之间。当x+y<8,时,补码加法就会负溢出,导致和增加了16。当 -8≤x+y<8 时,加法就产生 x+y 。当x+y≥8,加法就会正溢出,使得和减少了16。这三种情况中的每一种都形成了图中的一 个斜面。
2.3.3 补码的非
求补码的非方法:对每一位求补,再对结果加1
对ω位的补码加法来说, TMax是自已的非
2.3.4 无符号乘法
2.3.5 补码乘法
我们认为对于无符号和补码乘法来说,乘法运算的位级表示都是一样的, 并用如下原理说明:
推导——无符号和补码乘法的位级等价法:
C语言的无符号数乘法和有符号数乘法:
PS:
扩展乘积的字长:在需要时用软件方法完成,例如: 算术程序包 “arbitrary precision”
2.3.6 乘以常数
以往,在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运算和移位)只需要1个时钟周期。即使在我们的 参考机器Intel Core i7HaSwell 上,其整数乘法也需要 3 个时钟周期。
因此,编译器使用 了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。首先,我们会考虑乘以2的幂的情况,然后再概括成乘以任意常数。
注意:
2.3.7 除以2的幂
在大多数机器上,整数除法要比整数乘法更慢—— 需要30个或者更多的时钟周期。
除以2的幂也可以用移位运算来实现,只不过我们用的是右移,而不是左移。无符号和补码数分别使用逻辑移位和算术移位来达到目的。
对无符号运算使用移位是非常简单的,部分原因是由于无符号数的右移一定是逻辑右移。
而对于除以2的幂的补码运算来说,情况要稍微复杂一些。首先,为了保证负数仍然为 负,移位要执行的是算术右移。现在让我们来看看这种右移会产生什么结果。
我们可以通过在移位之前“偏置(biasing)”这个值,来修正这种不合适的舍入:
现在我们看到,除以2的幂可以通过逻辑或者算术右移来实现。这也正是为什么大多数机器上提供这两种类型的右移。
但是,这种方法不能推广到除以任意常数。同乘法不同,我们不能用除以2的幂的除法来表示除以任意常数K的除法。
2.3.8 关于整数运算的最后思考
正如我们看到的,计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。
我们还看到,补码表示提供了一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括像加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补 码形式表示的,都有完全一样或者非常类似的位级行为。
我们看到了C语言中的某些规定可能会产生令人意想不到的结果,而这些结果可能是难以察觉或理解的缺陷的源头。
我们看到了unsigned数据类型,虽然它概念上很简单,但可能导致即使是资深程序员都意想不到的行为。我们还看到这种数据类型会以出乎意料的方式出现,e.g:当书写整数常数和当调用库函数时。
2.4 浮点数
2.4.1 二进制小数
理解浮点数的第一步是考虑含有小数值的二进制数字。
“小数点” 右边的位代表小数部分,
二进制小数点向左移动一位相当于这个数被2除。
二进制小数点向右移动一位相当于将该数乘2
注意:形如0.11...1的数表示的是刚好小于1的数:
⭐二进制数中的问题:
1、局限性——近似表示
2、在计算机内的实现问题:
1.长度有限的 ω 位
2.只能在 ω 位内设置一个二进制小数点3.限制了数的范围( 非常小?or 非常大?)3、定点数:小数点 隐含在 ω 位编码的某一个固定位置上e.g:1、 MSB 做符号位,隐含后面是小数点,表示小于 1.0 的纯小数2、 123.456 该怎么处理等
2.4.2 IEEE 浮点表示
前一节中谈到的定点表示法不能很有效地表示非常大的数字。例如,表达式5*是用101后面跟随100个零的位模式来表示。相反,我们希望通过给定 x 和 y 的值,来表示形如:x* 的数。
⭐浮点数的表示:
⭐三种情况:
给定位表示,根据exp的值,被编码的值可以分成三种不同的情况(最后一种情况有 两个变种)。图2-33说明了对单精度格式的情况。
注:
1、规格化的值【exp的位模式既不全为0,也不全为1】
2、非规格化的值【exp的位模式全为0】
3、特殊值【exp的位模式全为1】
⭐ 浮点编码总结:
1、非规格化数据:
2、IEEE754 规格化浮点数表示范围:
2.4.3 数字示例
可表示的数并不是均匀分布的——“越靠近原点处它们越稠密”:
这种表示具有一个有趣的属性,假如我们将图2-35中的值的位表达式解释为无符号整数,它们就是按升序排列的,就像它们表示的浮点数一样。这不是偶然的—— IEEE格式如此设计就是为了浮点数能够使用整数排序函数来进行排序。当处理负数时,有一个小的难点,因为它们有开头的1,并且它们是按照降序出现的,但是不需要浮点运算来进行比较也能解决这个问题
2.4.4 舍入
因为表示方法限制了浮点数的范围和精度,所以浮点运算只能近似地表示实数运算。
其他三种方式产生实际值的确界(guaranteed bound)这些方法在一些数字应用中是很有用的。
向偶数舍入初看上去好像是个相当随意的目标—— 有什么理由偏向取偶数呢?为什么不始终把位于两个可表示的值中间的值都向上舍入呢?
使用这种方法的一个问题就是很容易假想到这样的情景:这种方法舍入一组数值,会在计算这些值的平均数中引人统计偏差。我们采用这种方式舍入得到的一组数的平均值将比这些数本身的平均值略高一些。
相反,如果我们总是把两个可表示值中间的数字向下舍入,那么舍入后的一组数的平均值将比这些数本身的平均值略低一些。
向偶数舍入在大多数现实情况中避免了这种统计偏差。在50%的时间里,它将向上舍入,而在50%的时间里,它将向下舍入。
在我们不想舍入到整数时,也可以使用向偶数舍入。我们只是简单地考虑最低有效数字是奇数还是偶数。例如,假设我们想将十进制数舍入到最接近的百分位。不管用那种舍入方式,我们都将把1.2349999 舍入到 1.23,,而将1.2350001 舍入到 1.24,因为它们不是在1.23 和1.24 的正中间。另一方面我们将把两个数1.2350000 和1.2450000 都舍人到 1.24, 因为 4 是偶数。
2.4.5 浮点运算
IEEE标准中指定浮点运算行为方法的一个优势在于,它可以独立于任何具体的硬件或者软件实现。因此,我们可以检查它的抽象数学属性,而不必考虑它实际上是如何实现的。
2.4.6 C语言中的浮点数
所有的C语言版本提供了两种不同的浮点数据类型:float 和 double。
在支持IEEE浮点格式的机器上,这些数据类型就对应于单精度和双精度浮点。
另外,这类机器使用向偶数舍入的舍入方式。不幸的是,因为C语言标准不要求机器使用IEEE浮点,所以没有标准的方法来改变舍人方式或者得到诸如 -0、+∞、-∞ 或者 NaN之类的特殊值。
大多数系统提供include('.h')文件和读取这些特征的过程库,但是细节随系统不同而不同。例如,当程序文件中出现下列句子时,GNU 编译器 GCC 会定义程序常数INFINITY(表示+∞)和NAN(表示 NaN):
#define _GNU_SOURCE 1
#include <math.h>
PS:
双精度能够表示的最大的有限数,大约是1.8*。
2.5 小结
计算机将信息编码为位(比特),通常组织成字节序列。
有不同的编码方式用来表示整数、实数和字符串。
不同的计算机模型在编码数字和多字节数据中的字节顺序时使用不同的约定。
C语言的设计可以包容多种不同字长和数字编码的实现。
64位字长的机器逐渐普及,并正在取代统治市场长达30多年的32位机器。且由于64位机器也可以运行为32位机器编译的程序,我们的重点就放在区分32位和64位程序,而不是机器本身。
64位程序的优势是可以突破32位程序具有的4GB地址限制。
大多数机器对整数使用补码编码,而对浮点数使用IEEE标准754编码。在位级上理解这些编码,并且理解算术运算的数学特性,对于想使编写的程序能在全部数值范围上正确运算的程序员来说,至关重要。
在相同长度的无符号和有符号整数之间进行强制类型转换时,大多数C语言实现遵循的原则是底层的位模式不变。
在补码机器上,对于一个ω位的值,这种行为是由函数 T2 和U2,来描述的。
C语言隐式的强制类型转换会出现许多程序员无法预计的结果,常常导致程序错误。
由于编码的长度有限,与传统整数和实数运算相比,计算机运算具有非常不同的属性。
当超出表示范围时,有限长度能够引起数值溢出。
当浮点数非常接近于0.0,从而转换成零时,也会下溢。
和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比,有一些特殊的属性。
例如,由于溢出,表达式 x*x 能够得出负数。但是,无符号数和补码的运算都满足整数运算的许多其他属性,包括结合律、交换律和分配律。这就允许编译器做很多的优化。例如,用(x<<3)-x取代表达式 7*x 时,我们就利用了结合律、交换律和分配律的属性,还利用了移位和乘以2的幂之间的关系。
我们已经看到了几种使用位级运算和算术运算组合的聪明方法。
例如,使用补码运算,~ x+1等价于-x。
另外一个例子,假设我们想要一个形如[0,... ,0,1,... ,1]的位模式,由ω-k个0后面紧跟着k个1组成。这些位模式有助于掩码运算。这种模式能够通过C表达式(1<<k)-1 生成,利用的是这样一个属性,即我们想要的位模式的数值为 -1。
例如,表达式(1<<8)-1 将产生位模式 0xFF。
浮点表示通过将数字编码为 x* 的形式来近似地表示实数。
最常见的浮点表示方式是由IEEE标准754 定义的。它提供了几种不同的精度,最常见的是单精度(32位)和双精度(64位)。
IEEE浮点也能够表示特殊值+∞、-∞和NaN。
必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且并不遵守普遍的算术属性,比如结合性。