深入理解计算机系统_第一部分_第二章_信息的表示和处理

深入,并且广泛
				-沉默犀牛

文章导读

这一章介绍了计算机中信息(即二值信号)的表示和处理。

  1. 信息存储
    1.1 十六进制表示法:介绍了十六进制的产生和十六进制、十进制、二进制之间的相互转换规律
    1.2 字数据大小:介绍了在不同机器和编译器中,数据类型数据在内存中所占的大小
    1.3 寻址和字节顺序 : 介绍了寻址方法和字节顺序(小端法、大端法),float与int的二进制表示有联系
    1.4 表示字符串:介绍了字符串的编码方式,以ASCII为例,说明了文本数据比二进制数据移植性好
    1.5 表示代码:介绍了在不同机器上产生的机器指令是不同的
    1.6 布尔代数简介:介绍了布尔代数及四种运算(~、&、|、^)
    1.7 C语言中的位级运算:介绍了布尔运算在C语言中的运用
    1.8 C语言中的逻辑运算:介绍了C语言中的逻辑运算及与位级运算的不同
    1.9 C语言中的移位运算:介绍了左移和右移(逻辑右移和算术右移)
  2. 整数表示
    2.1 整数数据类型:介绍了常见的整数数据类型
    2.2 无符号数的编码
    2.3 补码编码
    2.4 有符号数和无符号数之间的转换:同样的位模式应用不同的解释方法
    2.5 C语言中的有符号数与无符号数:着重提及了有符号数与无符号数混杂时的潜在危险
    2.6 扩展一个数字的位表示:介绍了无符号数的零扩展和补码数的符号扩展
    2.7 截断数字:介绍了无符号数的截断和补码数的截断
  3. 整数运算
    3.1 无符号加法:介绍了无符号数加法和溢出
    3.2 补码加法:介绍了补码数的加法和溢出
    3.3 补码的非 : 非是指加法逆元
    3.4 无符号乘法
    3.5 补码乘法:简单方法是转换为10禁止计算后转换为二进制后截断
    3.6 乘以常数:把常数转为 2的任意次幂的相互加减
    3.7 除以2的幂:分为无符号和有符号(补码)除法
    3.8 关于整数运算的最后思考:思考有限字长对于结果的影响
  4. 浮点数
    4.1 二进制小数
    4.2 IEEE浮点表示:分为规格化、非规格化和特殊值
    4.3 数字示例
    4.4 舍入:分为向偶数舍入、向零舍入、向下舍入、向上舍入
    4.5 浮点运算:加法可交换不可结合,乘法不具有结合性,不具有加法分配性

信息的表示和处理

现在计算机存储和处理的信息以二值信号表示。对于有10个手指的人类来说,使用十进制是很自然的事情,但是当构造存储和处理信息的机器是,二进制工作得更好。二值信号能够很容易得被表示、存储和传输,例如,可以表示为穿孔卡片上有洞或无洞、导线上的高电压或低电压,或者顺时针或者逆时针的磁场。对二值信号进行存储和执行计算的电子电路非常简单和可靠,制造商能够在一个单独的硅片上集成数百万甚至数十亿个这样的电路。

[说不定这个宇宙中有一个星球,在那上面的生物长了3个手指,那他们一定就是3进制的吧哈哈哈哈哈]

孤立地讲,单个的位不是非常有用。然而,当把位组合在一起,再加上某种解释,即赋予不同的可能位模式以含义,我们就能够表示任何有限集合的元素。比如,使用一个二进制数字系统,我们能够用位组来编码非负数。通过使用标准的字符码,我们能够对文档中的字母和符号进行编码。

我们研究三种最重要的数字表示。无符号(unsigned)编码基于传统的二进制表示法,表示大于或者等于零的数字。补码(two‘s-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。浮点数(floating-point)编码是表示实数的科学计数法的以2为基数的版本。计算机用这些不同的表示方法实现算术运算,例如加法和乘法,类似于对应的整数和实数运算。

计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至于不能表示时,某些运算就会溢出(overflow)。溢出会导致某些令人吃惊的后果。例如,在今天的大多数计算机上(使用32位来表示数据类型int),计算表达式200300400*500会得出-884901888。这违背了整数运算的特性,计算一组正数的乘积不应产生一个负的结果。

另一方面,整数的计算机运算满足人们所熟知的真正整数运算的许多特性。例如,利用乘法的结合律和交换律。虽然计算机可能没有产生期望的结果(人类期望),但是至少它是一致的!

浮点运算有完全不同的数学属性。虽然溢出会产生特殊的值正无穷,但是一组正数的乘积总是挣得。由于表示的精度有限,浮点运算是不可结合的。例如,在大多数机器上,C表达式(3.14 + 1e20)- 1e20求得的值会是0.0,而3.14 +(1e20 - 1e20)求得的值会是3.14。整数运算和浮点运算会有不同的数学属性是因为它们处理数字表示有限的方式不同——整数的表示虽然只能编码一个相对较小的数字范围,但是这种表示是精确的;而浮点数虽然可以编码较大的数值范围,但是这种表示只是近似的。

通过研究数字的实际表示,我们能够了解可以表示的值的范围和不同算术运算的属性。大量的计算机的安全漏洞都是由于计算机算术运算的微妙细节引发的。这导致了众多的黑客企图利用他们能找到的任何漏洞,不经过授权就进入他人的系统。

[学习微机的时候就觉得数字的存储和运算很麻烦,终于还是逃不脱啊。。。]

2.1 信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位[把8个位想成一个班级,校领导能找的最小单位是班级]。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合就称为虚拟地址空间。顾名思义,这个虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现是将动态随机访问存储器、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为程序提供一个看上去统一的字节数组。

在接下来的几章中,我们将讲述编译器和运行时系统是如何将存储空间划分为更可管理的单元,来存放不同的程序对象(program object),即程序数据、指令和控制信息。可以用各种机制来分配和管理程序不同部分的存储。这种管理完全是在虚拟地址空间里完成的。例如,C语言中一个指针的值(无论它指向一个整数、一个结构或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。C编译器还把每个指针和类型信息联系起来,这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。尽管C编译器维护着这个类型信息,但是它生成的实际机器级程序并不包含关于数据类型的信息。每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

指针是C语言的一个重要特性。它提供了引用数据结构(包括数组)的元素的机制。与变量类似,指针也有两个方面:值和类型。它的值表示某个对象的位置,而它的类型表示那个位置上所存储对象的类型(比如整数或者浮点数)。

2.1.1 十六进制表示法

一个字节由8位组成。在二进制表示法中,它的值域是00000000 ~ 11111111。如果看成十进制数,它的值域就是0 ~ 255。两种符号表示法对于描述位模式来说都不是非常方便。二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。替代的方法是,以16为基数,或者叫做十六进制(hexadecimal)数,来表示位模式。十六进制(简写hex)使用数字“0” ~ “9” 以及字符“A”~“F”来表示16个可能的值[马路上的红灯超时间超过100秒后就会用A ~ F来表示]。用十六进制书写,一个字节的值域为00~FF。

在C语言中,以0x或0X开头的数字常量被认为是十六进制的值。字符A~F既可以是大写,也可以是小写,也可以是大小写混合。

记住十六进制与十进制转化的窍门就是,记住十六进制A、C、F相应的十进制数,这样对于B、D、E的十进制数只要加1即可。

比如,给你一个数字0x173A4C。可以通过展开每个十六进制数字,将它转换为二进制格式,如下图所示:
在这里插入图片描述

反过来,如果给你一个二进制数,可以通过首先把它分为每4位一组来转化为十六进制。不过要注意,如果位总数不是4的倍数,最左边的一组要前面补0,然后再将每个4位组转换为相应的十六进制数字:
在这里插入图片描述

[这个内容是比较简单的,进制转换是程序员的基本功吧,不过才知道原来十六进制的提出是为了简便的。]

十进制和十六进制表示之间的转换需要使用乘法或者除法来处理一般情况,例如,十进制为314156,则如下图:在这里插入图片描述

所以十六进制表示为 0x4CB2C。
如果十六进制为0x7AF,则十进制数为7×16的平方 + 10×16 + 15 = 1967

2.1.2 字数据大小

每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为0~(2的w地方 )- 1,程序最多访问(2的w次方)个字节。

[看来我们的系统分为32位64位的差别就在于此,所以同样的机器既可以装32位的系统,也可以装64位的系统,因为这个位数决定了虚拟地址的大小,跟实际你的机器的物理内存大小没关系。

但是大多数软件就分为了32位版本64位版本,因为软件就需要用到虚拟地址,32位64位的虚拟地址大小不同,所以如果装错了会出现蓝屏等现象,我想一定是64位的软件装在了32位的系统上,请求了超过4GB(2的32次方)-1)这个大小的地址,就蓝屏了。]

最近这些年,出现了大规模的从32位字长机器到64位字长机器的迁移。32位字长限制虚拟地址空间为4GB,扩展到64位字长使得虚拟地址空间为16EB。

大多数64位机器也可以运行(为32位机器编译)的程序,这是一种后向兼容。我们将程序称为“32位程序”或者“64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。

计算机和编译器支持多种不同方式编码的数字格式,如不同长度的整数和浮点数。比如,许多机器都有处理单个字节的指令,也有处理表示为2字节、4字节或者8字节整数的指令,还有些指令支持表示为4字节和8字节的浮点数。

C语言支持整数和浮点数的多种数据格式。有些数据类型的确切字节数依赖于程序是如何被编译。

在这里插入图片描述
为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其中就有数据类型int32_tint64_t,它们分别是4个字节和8个字节。使用确定大小的整数类型是程序员准确控制数据表示的最佳途径。

[怪不得我在很多程序中看到int32_tint64_t这两种定义方法,原来是为了固定数据大小。]

程序员应该力图使它们的程序在不同的机器和编译器上可移植。可移植性的一个方面就是使程序对不同数据类型确切大小不敏感。如果不关注这个问题,就有可能出现错误,比如,许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针。这在大多数32位的机器上能正常工作,但是在一台64位的机器上却会导致问题。

[这一点我很疑惑,int类型在32位机器为4个字节,作为指针的话最大地址为4GB,但是64位机器的寻址地址最大为16EB,是大于4GB的,为啥会导致问题呢??????]

2.1.3 寻址和字节顺序

对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。例如,假设一个类型为int的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么(假设数据类型int为32位表示)x的4个字节将被存储在内存的0x100、0x101、0x102、0x103位置。

排列表示一个对象的字节有两个通用的规则。即小端法(little endian)大端法(big endian)小端法是指机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象;大端法是指机器选择在内存中按照从最高有效字节到最低有效字节的顺去存储对象。

假设变量x的类型为int,位于地址0x100处,它的十六进制值为0x01234567。
在这里插入图片描述

注意,在字0x01234567中,高位字节的十六进制值是0x01,而地位字节是0x67。在哪种字节顺序是合适的这个问题上,人们表现得非常情绪化。但其实只要选择了一种规则并且始终如一地坚持,对哪种字节排序的选择都是任意的。

对于大多数应用程序员来说,其机器所使用的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到同样的结果。不过有的时候,字节顺序会成为问题。

  1. 在不同类型的机器之间通过网络传送二进制数据时,小端法机器产生的数据发送到大端法机器时(或者反过来),字里的字节成了反序的。为了避免这类问题,网络应用程序的代码必须遵守已经建立的关于字节顺序的规则,以确保发送方机器将它内部表示转换成网络标准,而接收方机器则将网络标准转换为它的内部表示。
  2. 阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检查机器级程序时。就是在小端法机器生成的机器级程序表示中,书写字节序列的方式是最低位字节在左边,最高位在右边,这正好与通常人类书写数字时最高有效位在左边,最低有效位在右边的方式相反。
  3. 编写规避正常的类型系统的程序时字节顺序也会很重要,在C语言中,可以通过使用强制转换类型(cast)联合(union)来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。

这里展示一段C代码,及其测试程序和结果:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

参数12345的十六进制表示为0x00003039。

  1. 对于int类型的数据,除了字节顺序以外,我们在所有机器上得到了相同的结果。(我们可以看到在Linux 32、windows和Linux 64上,最低有效字节0x39最先输出,这说明它们是小端法机器;而在Sun上最后输出,这说明Sun是大端法机器。)
  2. float数据的字节,除了字节顺序以外,也都是相同的。
  3. 指针值却是完全不同的。不同的机器/OS配置使用不同的存储分配规则,Linux 32、Windows和Sun的机器使用4字节地址,而Linux 64使用8字节地址。

可以观察到,尽管float和int数据都是对数值12345编码,但是它们有截然不同的字节模型:int为0x00003039,而float为0x4640E400。如果我们将这些十六进制模式扩展为二进制形式,并且适当地将它们移位,就会发现一个有13个相匹配的位的序列:
在这里插入图片描述

这不是巧合,以后研究浮点数格式的时候,还将再回到这个例子。

2.1.4 表示字符串

C语言中字符串被编码为一个以null字符结尾的字符数据。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。因此,如果我们以参数“12345”和6来运行上面的show_bytes,我们得到结果31 32 33 34 35 00。注意,十进制数字x的ASCII码正好是0x3x,而终止字节的十六进制表示为0x00。在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。

2.1.5 表示代码

考虑下面的C函数:
在这里插入图片描述

当我们在示例机器上编译时,生成如下字节表示的机器代码:
在这里插入图片描述

我们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。

计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能用来帮助调试的辅助表以外。

[怪不得在转移代码的时候,是转移.c文件到对方电脑上去编译,而不是直接转移.o文件,原来是我自己电脑上的.o文件到对方电脑不能用的。]

2.1.6 布尔代数简介

二进制值是计算机编码、存储和操作信息的核心,所以围绕数值0和1的研究已经演化出了丰富的数学知识体系。
在这里插入图片描述
最后图表中的^代表了异或。

将上述4个布尔运算扩展到位向量的运算,位向量就是固定长度为w、由0和1组成的串。假设a = [0110] ,b = [1100],则四中运算:
在这里插入图片描述

布尔运算的运算法则有符合分配律:
&对|的分配律 :a & ( b | c ) = ( a & b ) | ( a & c )
|对&的分配律 :a | ( b & c ) = ( a | b ) & ( a | c )

此外布尔运算还有一个有趣的属性:
a ^ a = 0
0 ^ 0 = 1 ^ 1 = 0
( a ^ b ) ^ a = b

即a是a的加法逆元

位向量一个很有用的应用就是表示有限集合。比如,a = [ 011010001 ]表示集合A = { 0 , 3 , 5 , 6 },而 b = [ 01010101 ]表示集合B = { 0 , 2 , 4 , 6 }。

在大量实际应用中,我们都能看到用位向量来对集合编码。例如,我们会看到很多不同的信号会中断程序执行。我们能够通过指定一个位向量掩码,有选择地使能或屏蔽一些信号,其中某一位位置上为1时,表示信号i是有效的,而0表明该信号是被屏蔽的。因此,这个掩码表示的就是设置为有效信号的集合。

2.1.7 C语言中的位级运算

C语言的一个很有用的特性就是它支持按位布尔运算。以上的四种布尔运算(| 、 & 、 ~、 ^ )可以运用到任何“整形”的数据类型上。下图是对char数据类型表达式求值的例子:
在这里插入图片描述

确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转换回十六进制。

上面说过位级运算的一个常见用法就是实现掩码运算,这里的掩码是一个位模式,表示从一个字中选出的位的集合。举个例子,掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算( x & 0xFF )生成一个由 x 的最低有效字节组成的值,而其他的字节就被置为0。比如, x = 0x89ABCDEF,其表达式将得到0x000000EF。

2.1.8 C语言中的逻辑运算

C语言还提供了一组逻辑运算符|| 、 &&、 !,分别对应于命题逻辑中的OR、AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。
在这里插入图片描述

看得出来,按位运算只有在特殊情况下,也就是参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。

2.1.9 C语言中的移位运算

C语言还提供了移位运算,向左或者向右移动位模式。

左移运算:x << k ,向左移动 k 位,丢弃最高的 k 位,并在右端补 k 个0。
右移运算:x >> k ,向右移动 k 位,但是它的行为有点微妙。一般而言,机器支持两种形式的右移逻辑右移算术右移逻辑右移在左边补 k 个0,算术右移是在左端补 k 个最高有效位的值(虽然看上起很奇特,但是它对有符号整数数据的运算非常有用)。
以下是例子:
在这里插入图片描述

C语言标准并没有明确定义对于有符号数应该使用哪类右移——算术右移逻辑右移都可以。不幸地,这就意味着任何假设一种或者另一种右移形式的代码都可能遇到可移植性的问题。然而实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序员也都假设机器会使用算术右移。对于无符号数,右移必须是逻辑右移

还需要注意的就是,必须要保证位移量 k 小于待移位值的位数。

2.2 整数表示

在本节中,我们描述用位来编码整数的两种不同的方式:一种只能表示非负数,而另一种能够表示负数、零和整数。
下表展示引入一些数学术语,这些术语会在描述的过程中介绍。

在这里插入图片描述

2.2.1 整型数据类型

C语言支持多种整型数据类型——表示有限范围的整数。这里给出来的唯一一个与机器相关的取值范围是大小指示符long的。大多数64位机器使用8个字节的表示,比32位机器使用的4个字节的表示的取值范围大很多。
在这里插入图片描述
在这里插入图片描述

以上两个图标有一个值得注意的特点:取值范围不对称——负数的范围比整数的范围大 1 。

C语言标准定义了每种数据类型必须能够表示的最小的取值范围。如下图,它们的取值范围与上面所示的典型实现一样或者小一些。特别的,除了固定大小的数据类型是例外,我们看到它们只要求正数和负数的取值范围是对称的。此外,数据类型int可以用两个字节的数字来实现,这几乎回退到了16位机器的时代。long的大小可以用4个字节的数字来实现。
在这里插入图片描述

2.2.2 无符号的编码

无符号数即位向量的每一个位都成为数字值的一部分,我们用一个函数B2U(Binary to Unsigned)来表示:
在这里插入图片描述

下面示例几种情况下的B2U给出的从位向量到整数的映射:
在这里插入图片描述

无符号数的二进制表示有一个很重要的属性,就是每个介于0 ~ (2的w次方 )- 1 的数都有唯一一个w位的值编码,比如十进制数11作为无符号数,只有一个4为表示,即[ 1011 ]。
在这里插入图片描述

数学术语双设是指一个函数 f 有两面:它将数值 x 映射为数值 y ,即 y = f ( x ) ,但是它也可以反向操作,即 x = f-1 ( y )。

2.2.3 补码编码

许多情况下,我们还希望表示负数值。最常见的有符号数的计算机表示方式就是补码(two's-complement)形式。这个定义中,将字的最高有效位解释为负权(negative Weight)。我们用函数B2T(Binary to Two’s - complement)来表示。
在这里插入图片描述

最高有效位也成为符号位,它的权重-(2的w-1次方),是无符号表示中权重的负数。符号位被设置为 1 时,表示值为负,反之为非负。在这里插入图片描述

B2T也是一个双射函数
在这里插入图片描述

在这里插入图片描述

关于这些数字,有几个点值得注意:

  1. 补码范围是不对称的,Tmin绝对值 = Tmax绝对值 + 1,也就是说 Tmin没有与之对应的正数。之所以有这样的不对称性,是因为一半的位模式(符号位 = 1)表示负数,而另一半(符号位 = 0)表示非负数。因为0是非负数,也就以为着能表示的正数比负数少一个。
  2. 最大的无符号数值刚好比补码的最大值的两倍大一点,Umax = 2 * Tmax + 1。补码表示中所有表示负数的位模式在无符号表示中都变成了正数。
  3. 注意 -1 和 Umax 有同样的位表示——一个全1的串
  4. 数值 0 在两种表示方式中都是全0的串

C语言标准并没有要求用补码形式来表示有符号整数,但是几乎所有的机器都是这么做的。所以如果我们希望代码有最大可移植性,除了C语言标准定义的数据类型的数值范围之外,不能再假设任何可表示的数值范围,也不能假设有符号数会使用何种特殊的表示方式。

为了更好地理解补码,考虑下面的代码:
在这里插入图片描述

当在大端法机器上运行时,这段代码的输出为 30 39 cf c7,指明 x 的十六进制表示为0x3039,mx 的十六进制表示为0xcfc7,分别展开为位模式,x = [ 0011 0000 0011 1001] ,mx = [ 1100 1111 1100 0111 ],对这两个位模式生成的值为 12345 和 -12345。
在这里插入图片描述

2.2.4 有符号数和无符号数之间的转换

C语言允许在不同的数字数据类型之间做强制类型转换。例如,假设变量 x 声明为 int,u 声明为 unsigned。表达式(unsigned)x 会将 x 的值转化为一个无符号数值,而(int)u 将 u 的值转换为一个有符号整数。
那么强制类型转换会得到什么结果呢?对于大多数C语言的实现来说,对于这个问题的回答是从位级角度来看的,而不是数的角度,即转换前后的二进制位模式不会变,只是解释方式改变了。

比如如下的代码:
在这里插入图片描述
在一台采用补码的机器上,上述代码会产生如下输出:
v = -12345, uv = 53191
在上面的表格中看到了,-12345 的16为补码 和 53191的无符号表示的位模式是一样的。

第二个例子:
在这里插入图片描述
在一台采用补码的机器上,上述代码会产生如下输出:
u = 4294967295, tu = -1

对于32位字长来说,无符号形式的4294967295(Umax)和补码形式的 -1 的位模式是完全一样的。将unsigned强制类型转换为 int ,底层的位表示保持不变。

从这个例子来看,T2U(- 12345 ) = 53191,U2T( 53191 ) = -12345。也就是说,十六进制表示写作 0xCFC7 的16位位模式既是 -12345 的补码表示,优势 53191 的无符号表示。同时,12345 + 53191 = 65535 = 2的16次方。这个属性可以推广到给定位模式的两个数值(补码和无符号数)之间的关系。类似的,T2U(-1) = 4294967295,冰鞋U2T(4294967295) = -1。同时,1 + 4294967295 = 4294967296 = 2的32次方。

也就是说,无符号表示中的Umax有着和补码表示的 -1 相同的位模式。这两个数的关系: 1 + Umax = 2的w次方。

在这里插入图片描述
[这些答案都是我亲自算过的,有不懂的可以直接留言问我。]

2.2.5 C语言中的有符号数与无符号数

C语言匀速无符号数和有符号数之间的转换,大多数系统遵循的原则是底层的位表示保持不变。

显示的强制类型转换就会导致转换发生:
在这里插入图片描述

另外,当一种类型的表达式被赋值给另外一种类型的变量时,转换是隐式发生的:
在这里插入图片描述

当用printf输出数值时,分别用 %d、 %u、 %x以有符号十进制、无符号十进制和十六进制格式输出一个数字。考虑下面的代码:

在这里插入图片描述

当在一个32位机器上运行时,它的输出如下:
x = 4294967295 = -1
u = 2147483648 = -2147483648

当执行一个运算时,如果它的一个运算数是有符号的,另一个是无符号的,那么C语言会隐式地将有符号数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。

这对于像 < 和 > 这样的关系运算符来说,它会导致非直观的结果。
在这里插入图片描述

这里假设数据类型 int 表示为32位补码。考虑比较式 -1 < 0u。因为第二个运算数是无符号的,第一个运算数就会被隐式的转换为无符号数,因此表达式就变为了 4294967295u < 0u,这个答案显然为 0 。

在这里插入图片描述
[这些答案都是我亲自算过的,有不懂的可以直接留言问我。]

2.2.6 扩展一个数字的位表示

一个常见的运算实在不同字长的整数之间转换,同时又保持数值不变。当然,当目标数据类型太小以至于不能表示想要的值时,这根本就是不可能的。然而,从一个较小的数据类型转换到一个较大的类型,应该总是可能的。

要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示的开头添加0。这种运算被称为零扩展(zero extension)

要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展(sign extension),在表示中添加最高有效位的值。

考虑下面的代码:
在这里插入图片描述

在采用补码表示的32位大端法机器上运行这段代码时,打印如下:
在这里插入图片描述

可以看到,尽管-12345 的补码表示和 53191 的无符号表示在16位字长时是相同的,但是在32位字长时却是不同的。特别地,-12345 的十六进制表示为 0xFFFFCFC7,而 53191 的十六进制表示为 0x0000CFC7。前者使用的是符号扩展——最开头加了16位,都是最高有效位 1,表示为十六进制就是0xFFFF。后者开头使用16个0来扩展,表示为十六进制就是0x0000。

练习题:
在这里插入图片描述
[可以看到这个练习中,从 [ 1011 ] 扩展到 [ 11011 ] 再扩展到 [ 111011 ] ,它们都是 -5 的补码表示。可见有符号数位扩展时候,左边扩展符号位,不会对它的数值有影响。这些答案都是我亲自算过的,有不懂的可以直接留言问我。]

在这里插入图片描述
在这里插入图片描述
[这些答案都是我亲自算过的,有不懂的可以直接留言问我。]

2.2.7 截断数字

假设我们不用额外的位来扩展一个数值,而是减少一个数字的位数。例如下面的情况:
在这里插入图片描述
当我们把 x 强制类型转换为 short 时,我们就将32位的int截断为了16位的short int。这个16位的位模式就是 -12345 的补码表示。当我们把它强制类型转换回int时,符号扩展把高16位设置为 1,从而生成 -12345 的32位补码表示。

截断一个数字可能会改变它的值——溢出的一种形式。对于一个无符号数,我们可以很容易得出其数值结果。

1.截断无符号数
在这里插入图片描述
mod 是求模数的运算(求余数),举个例子:
B2U ( [ 10101 ] ) = 21
B2U ( [ 101 ] ) = 5
21 mod 8(2的3次方) = 5

B2U ( [ 11100 ] ) = 28
B2U ( [ 00 ] ) = 0
28 mod 4(2的2次方) = 0

  1. 截断补码数值(将最高位转换为符号位):
    在这里插入图片描述

[这个我没弄懂。。。之后弄明白了来更新]

在这里插入图片描述

[自己编程一定不会在变量赋值的时候造成截断,给自己找麻烦。]

2.2.8 关于有符号数与无符号数的建议

像上一节看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。

用以下两个例子来展现这种细微的错误:
在这里插入图片描述
在这里插入图片描述

[这里可能有的疑问是 无符号0 - 1 = Umax,[0000] - [0001] = [1111] = Umax,所以这个条件就成了必然满足的条件了。]

在这里插入图片描述

A: 当 s 比 t 短的时候,该函数会返回1
B:由于 strlen 被定义为产生一个无符号的结果,差和比较都采用无符号运算来计算。当 s 比 t 短的时候,strlen(s) - strlen(t)的差会为负,但是变成了一个很大的无符号数,大于0。
C:改为 return strlen(s) > strlen(t);

[ 举个负数变成了很大的无符号数的例子: 3 - 4 ===> [0011] - [0100] = [1111] ,这个[1111] 视为有符号数(补码),则为-1,结果正确了;视为无符号数,则为15,结果就错了。]

我看到了很多无符号数运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。

当我们想要把字仅仅看作是位的集合而没有任何数字意义时,无符号数值是非常有用的。例如,往一个字中放入描述各种布尔条件的标记(flag)时,就是这样。地址自然地就是无符号的,所以系统程序员发现无符号类型是很有帮助的。当实现模运算和多精度运算的数学包时,数字是由字的数组来表示的,无符号值也会非常有用。

2.3 整数运算

刚入门的程序员非常惊奇地发现,两个正数相加会得出一个负数,而比较表达式 x < y 和比较表达式 x - y < 0会产生不同的结果。这些属性是由于计算机运行的有限性造成的。理解计算机运算的细微之处能够帮助程序员编写更可靠的代码。

2.3.1 无符号加法

如果两个加数取值范围是0-15(四位),但是和的取值范围为0-30(五位),这样的情况成为字节膨胀,这意味着,想要完整的表示算术运算的结果,我们不能对字长做任何限制。但是常见的编程语言支持固定精度的运算,因此像“加法”和“乘法”这样的运算不同于它们在整数上的相应运算。

x = 9 和 y = 12 的位表示分别是 [1001] 和 [1100] ,它们的和是21, 5位表示为 [10101] ,但是如果丢弃了最高位,我们得到了 [0101],是十进制的5。这就和值 21 mod 16 = 5一致。

说一个算术运算溢出,是指完整的整数结果不能放到数据类型的字节限制中去。下图中,当 x + y < 16 时,没有溢出,对应与图中标记为“正常”的斜面。当 x + y >= 16 时,加法溢出,对应与图中标记为“溢出”的斜面。

在这里插入图片描述

当执行C程序时,不会将溢出作为错误而发信号,不过有时候,我们可能希望判定是否发生了溢出。
在这里插入图片描述
9 + 12 = 5 。 由于 5 < 9 ,我们可以看出发生了溢出。

在这里插入图片描述

练习题:
在这里插入图片描述

[ 十六进制D的位模式为 [1101] 所以十进制就是13,是无符号数,所以求反就是用(2的w次方 - x),16 - 13 = 3 ]

2.3.2 补码加法

在这里插入图片描述

在这里插入图片描述

下图描述了字长为4的补码加法。运算数的范围为-8 ~ 7之间。当 x + y < -8时,补码加法就会负溢出,导致和增加了16。当 -8 <= x + y <= 8时,补码加法就是x + y。当 x + y >= 8时,补码加法就会正溢出,导致和减少了16。
在这里插入图片描述

溢出检查:

在这里插入图片描述

练习题:
在这里插入图片描述

在这里插入图片描述
补码相加会形成阿贝尔群,因此表达式(x + y)- x 总是得到y,无论加法是否溢出,而(x + y)- y 总是会得到x。

[模数相加会形成阿贝尔群 ,它是可交换的和可结合的。
模数加法即对任何两个数据相加求模的运算,如任何对10的模数加法,如9 + 9 = 8。
补码相加就是对2的w次方的模数相加,如 9 + 12 = 5,所以补码相加模数相加,即一定形成阿贝尔群,即一定可交换可结合。 ]

2.3.3 补码的非

在这里插入图片描述

这里最好把无符号的非 和补码的非 对比起来看:
无符号的非:
在这里插入图片描述

[无符号数的取反在上面说过了,这里不再赘述]

补码的非:
在这里插入图片描述

[因为是补码,所以从十六进制转换为十进制时要把位模式理解为补码,D的位模式是 [1101] 要注意这是补码,所以转为十进制的时候,-8 * 1 + 4 * 1 + 2 * 0 + 1 * 1 = -3 ,补码数取非遵循上面的公式,-3 取非为 3,-5 取非为 5 ,-8(Tmin)取非为 -8 ]

补码非的位级表示:
计算一个位级表示的值的补码非有几种聪明的方法。这些计数很有用(例如当你在调试程序的时候遇到值0xfffffffa),同时它们也能够让你更多了解补码表示的本质。

执行位级补码非的第一种方法是对每一位取反,再对结果加1。在C语言中,对于任意整数值x,计算表达式-x 和 ~x+1得到的结果完全相同。

示例如下:
在这里插入图片描述

[要注意这里说的结果完全相同,是说转化为十进制的结果相同,不是位模式相同。-4的位模式是[1100] 取反后为 [0011] 加1后为 [0100] 转换为十进制为 4 ]

[之前总不明白原码反码补码,以为有的编码就是原码,有的编码就是补码,现在才知道,编码(即位模式)就是编码,如果你把它当做了原码,那读出来的十进制数就是无符号数,如果把它当做补码,那读出来的十进制数就是有符号数。

比如位模式[1101],作为原码,则读出8 + 4 + 1 = 13,作为补码,则读出 -8 + 4 + 1 = -3。
]

2.3.4 无符号乘法

在这里插入图片描述

x 和 y 都是w位的无符号数,不管x * y 的结果需要用多少位来表示,C语言都将结果截断为w位。而将一个无符号数截断为w位等价于计算该值对(2的w次方)的模。

2.3.5 补码乘法

在这里插入图片描述

补码数(即有符号数)的乘积最后还是补码数,对这个补码数截断为w位相当于计算该值对2的w次方求模,再把结果转换为补码。
[比如下图中的 -3 * 3 = -9 ,-9 mod 8 = -1 ,-1 的补码表示就是[111]。]
在这里插入图片描述

小练习:
在这里插入图片描述

[这里有几道题有些难,我弄明白了来更新]
在之前XDR库的代码的安全漏洞,就是因为乘法溢出导致的,如果在32位的机器上,两个乘数结果的位模式超过了32位,截断为32位以后就出问题了。

2.3.6 乘以常数

以往,在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其他整数运算(加法、减法、位级计算额移位)只需要1个时钟周期。即使在Intel Core i7上,其整数乘法也需要3个时钟周期。因此,编译器使用了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。

首先考虑乘以2的幂的情况:

在这里插入图片描述

比如,4位。11的位模式为[1011]。k = 2时将其左移得到6位位模式[101100],即可编码为无符号数11 * 4 = 44。

[左移一个数值等价于执行一个与2的幂相乘的无符号乘法。而且无论是无符号运算还是补码运算,乘以2的幂都可能导致溢出。即使溢出的时候,我们通过移位得到的结果也是一样的。]

由于整数乘法比移位和加法的代价大得多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。比如,x * 14 = x * (8 + 4 + 2) = ( x << 3 ) + ( x << 2 ) + ( x << 1 ),将乘法替换为三个位移和两个加法

[到这里我有个疑问,x * 14 是可以这样,那x * -14呢?其实,x * -14 = ( x << 1 ) - ( x << 4 )]

归纳一下:对于某个常数K的表达式 x * K 生成代码。编译器会将K的二进制表示为一组0和1交替的序列,比如14可以写为[(0…0)(111)(0)]。考虑一组从位置n到位置m的连续的1(n >= m),(对于14来说 n = 3, m = 1。)我们可以用下面两种方法来计算这些位对乘积的影响:
在这里插入图片描述

那么怎么选择A还是B呢?原则是 n = m,选择A;否则选择B。其实就是为了尽可能的减少移位和加减的次数。

小练习:
在这里插入图片描述

2.3.7 除以2的幂

在大多数机器上,整数除法比整数乘法更慢——需要30个或者更多的时钟周期。所以除以2的幂也是用移位来实现,只不过用的是右移。无符号和补码分别使用逻辑右移和算术右移来实现

[还记得之前说过的吗?左移只有一种,右移就分为了逻辑右移和算术右移,在这里就用到了,逻辑右移是左边补0,算术右移左边补符号位]

在这里插入图片描述

直接以下面的例子来看,很直观:
在这里插入图片描述

左端移入的0以斜体表示。并且移位总是舍入到零(例如771.25 舍入到 771)

在这里插入图片描述

对于x >= 0 ,变量x的符号位为0,所以效果和逻辑右移是一样的。对于负数,会按照向下舍入(例如-771.25 会舍入到 -772)

看这个例子很直观:
在这里插入图片描述

我们可以通过在移位之前偏置(biasing)这个值,来修正这个不合适的舍入
[为啥这是不合适的。。。我没觉得不合适啊。。。]

在这里插入图片描述

看下面的例子:
在这里插入图片描述

现在我们可以看到,除以2的幂可以通过逻辑或者算术右移来实现。这也正是为什么大多数机器上提供这两种类型的右移。不过,这种方法不能想乘法那样推广到任意常数。

[是啊,乘以3 可以用(乘以4 - 本身)来实现,但是除以3 显然没办法做出类似的操作。]

2.3.8 关于整数运算的最后思考

计算机执行的“整数”运算实际上是一种模运算形式。
表示数字的有限字长限制了可能的值的取值范围,运算结果可能溢出。

补码表示提供了一种既能表示负数也能表示正数的灵活方法,同时使用了执行无符号算数相同的位级实现,包括加法、减法、乘法和除法,无论运算数是以无符号形式还是补码形式表示的,都有完全一样或者非常类似的位级行为。

我们看到了C语言中的某些规定可能会产生意想不到的结果,而这些结果可能是难以察觉或理解的缺陷的源头。特别看到了unsigned数据类型,虽然它概念上很简单,但可能导致意想不到的行为。这种数据类型还会以出乎意料的方式出现,比如书写整数常数和调用库函数时。
下面这道题很好,一起来做一下:
在这里插入图片描述

在这里插入图片描述

2.4 浮点数

浮点数对执行涉及非常大的数字、非常接近于0的数字,以及更普遍地作为实数运算的近似值的计算,是很有用的。

直到20世纪80年代,每个计算机制造商都设计了自己的表示浮点数的规则,以及对浮点数执行运算的细节。另外,它们常常不会太多地关注运算的精确性,而把实现的速度和简便性看得比数字精确性更重要。
现在所有的计算机都支持IEEE浮点的标准,这大大提高了科学应用程序在不同机器上的可移植性。

本节中,我们将看到IEEE浮点格式中数字是如何表示的。我们还会探讨舍入(rounding)的问题,即当一个数字不能被准确地表示为这种格式时,就必须向上或者向下调招。然后,我们将探讨加法、乘法和关系运算符的数学属性。许多程序员认为浮点数没意思,往坏了说,深奥难懂。我们将看到,因为IEEE格式是定义在一组小而一致的原则上的,所以它实际上是相当优雅和容易理解的。

2.4.1 二进制小数

理解浮点数的第一步是考虑含有小数值的二进制数字。首先,让我们来看看更熟悉的十进制表示法。
在这里插入图片描述

数字权的定义与十进制小数点符号(‘.’)相关,这意味着小数点左边的数字的权是10的正幂,得到整数值,而小数点右边的数字的权是10的负幂,得到小数值。例如
在这里插入图片描述

类似的,这种表示方法表示的二进制数定义如下:
在这里插入图片描述

图示Wie:
在这里插入图片描述
例如:
在这里插入图片描述
在这里插入图片描述

[根据这个定义,很容易知道二进制小数是无法表示所有的十进制小数的,比如0.25 = 1/4 是1/2的2次幂,所以二进制小数就是0.01,但是如果是0.2 = 1/5 ,不是1/2的任何次幂,就无法表示了。]

注意,形如0.11…1的数表示的是刚好小于1的数。

假定我们仅考虑有限长度的编码,那么十进制表示法不能准确的表达像1/3 和 5/7 这样的数。类似,小数的二进制表示法只能表示哪些能够被写成(x * 2的y次幂)的数,其他值只能被近似地表示。例如,数字1/5可以用十进制小数0.20精确表示。不过我们并不能把它准确的表示为一个二进制小数,我们只能近似的表示它,增加二进制表示的长度可以提高表示精度:
在这里插入图片描述

小练习:
在这里插入图片描述

浮点运算的不精确性能够产生灾难性的后果。1991.2.25,在第一次海湾战争期间,沙特阿拉伯的达摩地区设置的美国爱国者导弹,拦截伊拉克的飞毛腿导弹失败。飞毛腿导弹击中了美国一个兵营,造成28名士兵死亡。美国总审计局对失败原因做了详细的分析,并且确定底层的原因在于一个数字计算不精确。
在这里插入图片描述

在这里插入图片描述

2.4.2 IEEE浮点表示

前一节中谈到的定点表示法不能很有效地表示非常大的数字,例如,表达式5 * 2的100次幂是用101后面跟随100个零的位模式来表示。
在这里插入图片描述

下图给出将这三个字段装进字中两种最常见的格式。在单精度浮点格式(C语言中的float)中,s、exp和frac字段分别为1位,k = 8位和n = 23位,得到一个32位的表示。在双进度浮点格式(C语言中的double)中,s、exp和frac字段分别为1位、k = 11位和n = 52位,得到一个64位的表示。

在这里插入图片描述

给定位表示,根据exp的值,被编码的值可以分为三种不同的情况(最后一种情况有两个变种)。
在这里插入图片描述

[在看以下的3个case的描述时,你可能会觉得很懵,我建议你像我一样,看两遍,第一遍只需要知道这三类值的表示范围,只要知道这就是“三种值”跟“浮点数”的关系类似“正数,零,负数”三个定义跟“数字”的关系一样就好。然后去看8位浮点数格式的表格,再反过来看这三种情况的描述就会觉得豁然开朗了。]

case 1:规格化的值
这是最普遍的情况。当阶码的位模式既不全为0,也不全为1,都属于这种情况。这种情况中,阶码字段被解释为以偏置(biased)形式表示的有符号整数。也就是说,阶码的值是 E = e - Bias,其中e是无符号数,而Bias是一个等于(2的k-1次方 - 1)的偏置值。
小数字段frac被解释为描述小数值f,其中0 <= f < 1(二进制小数点在最高有效位的左边)。尾数定义为M = 1 + f。有时,这种方式也叫做 隐含的以1开头的(implied leading 1)表示。
既然我们总是能够调整阶码E,使得尾数M在范围 1 <= M < 2,之中,那么这种表示方法是一种轻松获得一个额外精度位的技巧。既然第一位总是1,那么就不需要显式地表示它。

这种情况表示的范围,对于单精度是-126 ~ +127,而对于双精度是 -1022 ~ +1023。

case 2:非规格化的值
当阶码域全为0时,所表示的数是非规格化数。这种情况下,阶码值是E = 1 - Bias,而尾数的值是M = f,也就是小数字段的值,不包含隐含的开头的1。

非规格化数有两个用途。首先,它们提供了一种表示数值0的方法,+0.0的浮点表示的位模式为全0:符号位是0,阶码字段全为0,小数域也全为0,这就得到M = f = 0。当符号位为1,而其他域全为0时,我们得到值-0.0。根据IEEE浮点格式,值+0.0和-0.0在某些方面被认为是不同的,而在其他方面是相同的。
非格式化数的另外一个功能是表示那些非常接近于0.0的数。它们提供了一种属性,称为逐渐溢出(gradual underflow),其中,可能的数值分布均匀地接近于0.0。

case 3:特殊值
阶码全为1的时候出现的。
当小数域全为0时,得到的值表示无穷,s = 0时是+无穷,s = 1时是- 无穷。当我们把两个非常大的数相乘,或者除以零时,无穷能够表示溢出的结果。当小数域非零时,结果值被称为“NaN”,即“不是一个数(Not a Number)”。一些运算结果不能是实数或无穷,就会返回这样的NaN值。在某些应用中,表示未初始化的数据时,它们也很有用处。

2.4.3 数字示例

下图展示了一组数值,它们可以用假定的6位格式来表示,有k = 3的阶码位和n = 2的尾数位。偏置量是[2的(3-1)次方 - 1 = 3]。a部分显示了所有可能的值(除了NaN)。两个无穷值在两个末端。最大数量值的规格化数是±14。非规格化数聚集在0的附近。图中b部分中,我们只展示了介于-1.0到+1.0之间的数值。两个零是特殊的非规格化数。可以看到,那些可表示的数并不是均匀分布的——越靠近原点处它们越稠密。
在这里插入图片描述

[这段描述中我不明白为啥做最大数量值是 +14 / -14]

在这里插入图片描述

这个图展示了8位浮点数格式的示例,其中有k = 4的阶码位和 n = 3的小数位。偏置量是7。
图被分为三个区域,来描述三类数字。不同的列给出了阶码字段是如何编码阶码E的,小数字段是如何编码尾数M的,以及它们一起是如何形成要表示的值
V = 2的e次方 * M的。
从0自身开始,最靠近0的是非规格化数。这种格式的非规格化数的E = 1 - 7 = -6,得到权是 1/64。小数f的值的范围是0,1/8,2/8,…,7/8,从而得到数V的范围是0 ~ 1/64 * 7/8 = 7/512。
接下来的是规格化数, E = 1 - 7 = -6,并且小数取值范围也为0,1/8,2/8,…,7/8。然而,尾数在范围 1 + 0 = 1 和 1 + 7/8 = 15/8之间,得出数V在范围8/512 和 15/512之间。
这种表示具有一个有趣的属性,假如我们把上图中的值的位模式解释为无符号数,它们就是按升序排列的,就像它们表示的浮点数一样。这不是偶然的——IEEE格式如此设计就是为了浮点数能够使用整数排序函数来进行排序。当处理负数时,有一个小的难点,因为它们有开头的1,而且它们是按照降序出现的,但是不需要浮点运算来进行比较也能解决这个问题。

小练习:
这里我们来联系一下浮点数的表示方式
在这里插入图片描述

在这里插入图片描述

联系把一些整数值转换成浮点形式对理解浮点表示很有用。
例如,12345的无符号位模式是[ 11 0000 0011 1001]。通过将二进制小数点左移13位,得到这个数的一个规格化表示,得到 12345 = 1.1000000111001 * 2的13次方。
为了用IEEE单精度形式来编码,我们丢弃开头的1,并且在末尾加10个0,来构造小数字段。得到 [1000000111001 0000000000]。为了构造阶码字段,我们用13加上偏置量127,得到140,其二进制表示为 [1000 1100]。加上符号位0,得到二进制浮点表示 : [0 10001100 10000001110010000000000]。

[这里为什么要加10个0呢?因为IEEE单精度形式编码,要求浮点数一共是32位,1位符号,8为阶码,23位小数位,12345的无符号位模式14位,去掉前面的1需要在家10位才能到23位。]

整数值13245(0x3039)和单精度浮点值12345.0(0x4640E400)在位级表示上有下列关系:
在这里插入图片描述

可以看到,相关的区域对应于整数的低位,刚好在等于1的最高有效位之前停止(这个位就是隐含的开头的位1),和浮点表示的小数部分的高位是相匹配的。

再做一个小练习:
3510593的十六进制表示为 0x00359141,推导出单精度浮点数3510593.0的十六进制表示:0x4A564504

2.4.4 舍入

因为表示方法限制了浮点数的范围和精度,所以浮点运算只能近似地表示实数运算。因此,对于值x,我们一般想用这一种系统的方法,能够找到“最接近的”匹配值x’,它可以用期望的浮点形式表示出来,这就是舍入(rounding)运算的任务。

一个关键问题是在两个可能值的中间确定舍入方向。例如,如果我有1.50美元,想把它舍入到最接近的美元数,应该是1美元还是2美元呢?一种可选择的方法是维持实际数字的下界和上界。例如,我们可以确定可表示的值-x和+x,使得-x <= x <= +x。IEEE浮点格式定义了四种不同的舍入方式。默认的方法是找到最接近的匹配,而其他三种可用于计算上界和下界。

下图表示了四种舍入方式,默认使用的是向偶数舍入向偶数舍入试图找到一个最接近的匹配值。它采用的方法是:它将数字向上或者向下舍入,使得结果的最低有效数字是偶数。

在这里插入图片描述

[1.5 ,2.5, -1.5 这种数字可以用浮点数表示啊,为啥还要舍入呢? 我觉得这里举例的数字只是为了说明各个舍入的效果而已吧]

向偶数舍入看上去是一个很随意的目标——有什么理由偏向取偶数呢?为什么不始终把位于两个可表示的值中间的值都向上舍入呢?使用这种方法的一个问题就是很容易假想到这样的情景:这种方法舍入一组数值,会在计算这些值的平均数中引入统计偏差。我们采用这种方式舍入得到的一组数的平均值将比这些数本身的平均值略高一些。相反,如果我们总是把两个可表示值中间的数字向下舍入,那么舍入后的一组数的平均值将比这些数本身的平均值略低一些。向偶数舍入在大多数实现情况中避免了这种统计偏差。在50%的时间里,它将向上舍入,而在50%的时间里,它将向下舍入

在我们不想舍入到整数时可以使用向偶数舍入。我们只考虑最低有效数字是奇数还是偶数。例如,假设我们想将十进制数舍入到最接近的百分位。不管用哪种舍入方式,我们都将把1.2349999舍入到1.23,而将1.2450000都舍入到1.24,因为4是偶数。

相似地,向偶数舍入法能够运用在二进制小数上。我们将最低有效位的值0认为是偶数,值1认为是奇数。一般来说,只有对形如XX…X.YY…Y100…的二进制位模式的数,这种舍入方式才有效,其中X和Y表示任意位置,最右边的Y是要被舍入的位置。只有这种位模式表示在两个可能的结果正中间的值。例如,考虑摄入到最近的四分之一的问题(也就是二进制小数点右边2位)。我们将10.00011(23/32)向下舍入到10.00(2),10.00110(23/16)向上舍入到10.01(2*1/4),因为这些值不是两个可能值的正中间值。我们将10.11100(2 * 7/8)向上舍入到11.00(3),而10.10100(2 * 5/8)向下舍入成10.10(2 * 1/2),因为这些值是两个可能值的中间值,并且我们倾向于使最低有效位为0。

以下舍入到最接近的二分之一(二进制小数点右边1位):
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

[看过A答案后,发现舍入位置右边如果有两个1,那肯定是在舍入位置进1,如果舍入位置右边有一个1,那么就是刚才说的处于两个可能值的中间值,以让最低有效位为0的原则进行舍入。]

在这里插入图片描述

在这里插入图片描述

2.4.5 浮点运算

IEEE标准指定了一个简单的规则,来确定诸如加法和乘法这样的算术运算的结果。在实际中,浮点单元的设计者使用了一些聪明的小技巧,使得计算只要精确到能够保证得到一个正确的舍入结果。当参数中有一个是特殊值(如-0,负无穷,NaN)时,IEEE标准定义了一些使之更合理的规则。例如,定义 1 / -0 将产生负无穷,而定义1 / +0 会产生正无穷。

IEEE标准中指定浮点运算行为方法的一个优势在于,它可以独立于任何具体的硬件或者软件实现。因此,我们可以检查它的抽象数学属性,而不必考虑它实际上是如何实现的。

前面我们看到了整数(包括无符号和补码)加法形成了阿贝尔群。实数上的加法也形成了阿贝尔群,是可以交换的,但是不可以结合。例如,(3.14 + 1e10 ) - 1e10 求值得到 0.0 ——因为舍入,值3.14 会丢失。另一方面,表达式 3.14 + (1e10 - 1e10)得出值为 3.14 。作为阿贝尔群,大多数值在浮点加法下都有逆元,也就是说 x + -x = 0 。无穷(因为 正无穷 - 无穷 = NaN)和NaN 是例外情况,因为对于任何x,都有Nan + x = NaN。

浮点加法不具有结合性,这是缺少的最重要的群属性。对于科学计算程序员和编译器编写者来说,这具有重要的含义。例如,假设一个编译器给定了如下代码片段:
x = a + b + c;
y = b + c + d;
编译器可能试图通过产生下列代码来省去一个浮点加法:
t = b + c;
x = a + t;
y = t + d;
然而,对于x来说,这个计算可能会产生于原始值不同的值,因为它使用了加法运算的不同的结合方式。在大多数应用中,这种差异小得无关紧要。不幸的是,编译器无法知道在效率和忠实于原始程序的确切行为之间,使用者愿意做出什么样的选择。结果是,编译器倾向于保守,避免任何对功能产生影响的优化,即使是很轻微的影响。

另一方面,浮点加法满足了单调性属性:如果 a >= b ,那么对于任何 a、b以及 x的值,除了NaN,都有 x + a >= x + b 。无符号或补码加法不具有这个实数(和整数)加法的属性。

浮点乘法也遵循通常乘法所具有的许多属性。我们定义 x * y 为Round(x * y)。这个运算在乘法中是封闭的(虽然可能产生无穷大或NaN),它是可交换的,而且它的乘法单元为1.0。另一方面,由于可能发生溢出,或者由于舍入而失去精度,它不具有可结核性。例如,单精度浮点情况下,表达式(1e20 * 1e20)1e-20求值为正无穷,而1e20 * (1e201e-20)将得出1e20。另外,浮点乘法在加法上不具备分配性。例如,单精度浮点情况下,表达式 1e20 * (1e20 - 1e-20)求值为 0 ,而1e20 * 1e20 - 1e20 * 1e20 会得出NaN。

另一方面,对于任何 a、b 和 c,并且a、b 和 c 都不等于NaN,浮点乘法满足下列单调性:
a >= b 且 c >= 0 得出 a * c >= b * c
a >= b 且 c <= 0 得出 a * c <= b * c
此外,我们还可以保证,只要 a 不等于 NaN ,就有 a * a >= 0 。像我们先前所看到的,无符号或补码的乘法没有这些单调性属性。
对于科学计算程序员和编译器编写者来说,缺乏结合性和分配性是很严重的问题。即使为了在三维空间中确定两条线是否交叉而写代码这样看上去很简单的任务,也可能成为一个很大的挑战。

2.4.6 C语言中的浮点数

所有的C语言版本提供了两种不同的浮点数据类型:floatdouble 。在支持IEEE浮点格式的机器上,这些数据类型就对应于单精度和双精度浮点。另外,这类机器使用 向偶数舍入 的舍入方式。不幸的是,因为C语言标准不要求机器使用IEEE浮点,所以没有标准的方法来改变舍入方式或者得到诸如-0正无穷负无穷或者NaN之类的特殊值。大多数系统提供include(‘.h’)文件和读取这些特征的过程库,但是细节随系统不同而不同。例如,当程序文件中出现下列句子时,GUN编译器GCC会定义程序常数INFINTY(表示正无穷NAN(表示NaN)
#define_GUN_SOURCE 1
#include <math.h>

当在intfloatdouble格式之间进行强制类型转换时,程序改变数值和位模式的原则如下(假设 int 是 32 位的):

  • int 转换成 float ,数字不会溢出,但是有可能被舍入。
  • intfloat 转换成 double ,因为 double 有更大的范围(也就是可表示值的范围),也有更高的精度(也就是有效位数),所以能够保留精确的数值。
  • double 转换成 float ,因为范围要小一些,所以值可能溢出成 正无穷 或者 负无穷。另外,由于精确度较小,它还可能被舍入。
  • float 或者 double 转换成 int,值将会向零舍入。例如,1.999将被转换成1,而 -1.999 将被转换成 -1。进一步来说,值可能会溢出。C语言标准没有对这种情况指定固定的结果。与 Intel 兼容的微处理器指定位模式[10 … 00](字长为w时的TMinw)整数不确定(integer indefinite)值。一个浮点数到整数的转换,如果不能为该浮点数找到一个合理的整数近似值,就会产生这样一个值。因此,表达式(int) + 1e10 会得到 -21483648,即从一个正值变成了一个负值。

2.5 小结

计算机将信息编码为位(比特),通常组织成字节序列。有不同的编码方式表示整数、实数和字符串。不同的计算机模型在编码数字和多字节数据中的字节顺序时使用不同的约定。
C语言的设计可以包容多种不同字长和数字编码的实现。64位字长的机器逐渐普及。由于64位机器也可运行那些为32位机器编译的程序,我们的重点就放在区分32位和64位程序,而不是机器本身。64位程序的优势是可以突破32位程序具有的4GB地址限制。
大多数机器对整数使用补码编码,而对浮点数使用IEEE标准754编码,在位级上理解这些编码,并且理解算数运算的数学特性,对于想使编写的程序能在全部数值范围上正确运算的程序员来说,是很重要的。
在相同长度的无符号和有符号整数之间进行强制类型转换时,大多数C语言实现遵循的原则是底层的位模式不变。在补码机器上,对于一个w位的值,这种行为是由函数T2U 和 U2T 来描述的。C语言隐式的强制类型转换会出现许多程序员无法预计的结果,常常导致程序错误。
由于编码的长度有限,与传统整数和实数运算相比,计算机运算具有非常不同的属性。当超出表示范围时,有限长度能够引起数值溢出。当浮点数非常接近于0.0,从而转换成零时,也会下溢。
和大多数其他程序语言一样,C语言实现的有限整数运算和真实的整数运算相比,有一些特殊的属性。例如,由于溢出,表达式 x * x 能够得出负数。但是,无符号数和补码的运算都满足整数运算的许多其他属性,包括结合律、交换律和分配律。这就允许编译器做很多的优化。例如,用(x << 3) - x 取代表达式 7 * x时,我们就利用了 结合律、交换律和分配律的属性,还利用了位移 和 乘以2的幂之间的关系。
我们已经看到了几种使用位级运算和算术运算组合的聪明方法。例如,使用补码运算,~x + 1等价于 -x。另外一个例子,假设我们想要一个形如[0,…,0,1,…,1]的位模式,由 w - k 个0后面紧跟着 k 个1组成。这些位模式有助于掩码运算。这种模式能够通过C表达式(1 << k) - 1生成,利用的是这样一个属性,即我们想要的位模式的数值为 2的k次方 - 1。例如,表达式(1 << 8) - 1 将产生位模式 0xFF。
浮点表示通过将数字编码为 x * 2的y次方 的形式来近似地表示实数。最常见的浮点表示方式是由IEEE标准754定义的。它提供了几种不同的精度,最常见的是单精度(32位)和双精度(64位)。IEEE浮点也能够表示特殊值 正无穷、负无穷 和 NaN。
必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且并不遵守普遍的算术属性,比如结合性。

猜你喜欢

转载自blog.csdn.net/qq_35065875/article/details/85055219