Index

第 2 章

C H A P T E R 2

信息的表示和处理

现代计算机存储和处理的信息以二值信号表示。这些微不足道的二进制数字,或者称 为位( bit ) , 形成了数字革命的基础。大 家熟悉并使用了 10 00 多年的十进制(以10 为基数) 起源于印度, 在 12 世纪被阿拉伯数学家改进,并 在 13 世纪被意大利数学家 Leona rdo P isano ( 大约公元 11 70- 1250 , 更为大家所熟 知的名字是 Fibo nacci ) 带到西方。对 千有 10 个手指的人类来说,使用十进制表示法是很自然的事情,但是当构造存储和处理信息的机 器时,二进制值工作得更好。二值信号能够很容易地被表示、存储和传输,例如,可以表 示为穿孔卡片上有洞或无洞、导线上的高电压或低电压、或者顺时针或逆时针的磁场。对 二值信号进行存储和执行计算的电子电路非常简单和可靠,制造商能够在一个单独的硅片 上集成数百万甚至数十亿个这样的电路。

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

我们研究三种最重 要的数字表示。无符号 ( unsig ned ) 编码基千传统的 二进制表示法,

表示大千或者等 千零的 数字。补码( t wo ’ s- com plemen t ) 编码是 表示有符号整数的最常见的方式,有 符号整数就是可以 为正或者为负的 数字。浮点数 ( float ing- poin t ) 编码是表示实数的科学 记数法的以 2 为基数的 版本。计算机用这些不同的 表示方法实现算术运算 ,例如加法和乘法,类似于对应的整数和实数运算。

计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表 示时,某些运算 就会溢出 ( overfl o w) 。溢出会导致某些令人吃惊的后果。例如,在 今天的

大多数计算机上(使用32 位来表示数据类型 i nt ) , 计算表达式 200*300*400*500 会得出结果

—88 4 901 888。这违背了整数运算的特性,计 算一组正数的乘积不应产生一个负的结果。

另一方面,整数的计算机运算满足人们所熟知的真正整数运算的许多性质。例如,利 用乘法的结合律和交换律,计 算下面任何一个 C 表达式, 都会得出结果一88 4 901 888:

(500* 400)* (300 *200)
((500* 400)* 300) *200
((200* 500)* 300) *400
400* (200* (300 *500))

计算机可能没有产生期望的结果,但是至少它是一致的!

浮点运算有完全不同的数学属性。虽 然溢出会产生特殊的 值十(X) ’ 但是一组正数 的乘积总是正的。由千表示的精度有限 , 浮点 运算是 不可结合的。例如, 在大多数机器上 , c 表达式 (3 . 1 4+1e 20 ) - l e 20 求得的值会是0. 0 , 而 3 . 1 4+ (1 e 20 - l e 20 ) 求得的值会是 3. 14。

整数运算和浮点数运算会有不同的数学属性是因为它们处理数字表示有限性的方式不 同 整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精确的;而浮

点数虽然可以编码一个较大的数值范围,但是这种表示只是近似的。

通过研究数字的实际表示,我们能够了解可以表示的值的范围和不同算术运算的属性。为了使编写的程序能在全部数值范围内正确工作,而且具有可以跨越不同机器、操作 系统和编译器组合的可移植性,了解这种属性是非常重要的。后面我们会讲到,大量计算 机的安全漏洞都是由于计算机算术运算的微妙细节引发的。在早期,当人们碰巧触发了程 序漏洞,只会给人们带来一些不便,但是现在,有众多的黑客企图利用他们能找到的任何 漏洞,不经过授权就进入他人的系统。这就要求程序员有更多的责任和义务,去了解他们 的程序如何工作,以及如何被迫产生不良的行为。

计算机用几种不同的二进制表示形式来编码数值。随着第 3 章进入机器级编程,你 需要熟悉这些 表示方式。在本章中, 我们描述这些编码,并 且 教 你 如 何 推出数字的表示。

通过直接操作数字的位级表示,我们得到了几种进行算术运算的方式。理解这些技术对于理解编译器产生的机器级代码是很重要的,编译器会试图优化算术表达式求值的性能。

我们对这部分内容的处理是基千一组核心的数学原理的。从编码的基本定义开始,然 后得出一些属性,例如可表示的数字的范围、它们的位级表示以及算术运算的属性。我们 相信从这样一个抽象的观点来分析这些内容,对你来说是很重要的,因为程序员需要对计 算机运算 与更为人熟悉的整数和实数运算之间的关系有清晰的理解。

m 怎样阅读本章

本章我们研究在计算机上如何表示数宇和其他形式数据的基本属性,以及计算机对 这些数 据执行操作的属性。这就要求我们深入研究数 学语 言, 编写公 式和方程式,以 及展 示重要 属性的推导。

为了帮助你阅读,这部分内容安排如下:首先给出以数学形式表示的属性,作为原 理。然后 ,用 例子和非形式化的讨论来解释这个原 理。我们建议你反复阅读原理描述和它的 示例 与讨 论, 直到你对该属性的说明内容及其重要 性 有了 牢固的 直觉。 对于更 加复杂的属性, 还会提供推导, 其结构看上去将会像 一个数 学证 明。虽 然最终你应该尝试理解这些推 导,但 在第一次阅读时你 可以跳过它们 。

我们也鼓 励你在阅读正文的过程中完成练习题 , 这会促使你主动学习, 帮助你理 论联系实际 。有了这些例 题和练习题作 为背景知 识, 再返回推导, 你将发现理解起来会容易许多。 同时,请放 心, 掌握好高中代数知识的 人都具备理解这些内容 所需要的数学技 能。

C++ 编程语言建立在 C 语言基础之上, 它 们使用完全相同的数字表示和运算。本章中关于 C 的所有内容对 C++ 都有效。另一方面, Java 语 言 创 造了一套新的数字表示和运算标准。C 标准的设计允许多种实现方式, 而 Java 标准在数据的格式和编码上是非常精确具体的。本章中多处着重介绍了 Java 支持的表示和运算。

豆日C 编程语言 的演变

前面提 到过, C 编程 语言是 贝 尔 实验 室的 Dennis Ritch ie 最早开发 出 来的, 目的是 和 U nix 操作系统 一起使用 ( U nix 也是 贝 尔实 验室开 发的)。在那个时候 , 大多数 系统程序, 例如操作 系统 , 为 了访问不同数 据类型的低级表示, 都必须 大量地使 用 汇编代码。比如说,像 malloc 库函数提供的内存 分配功能, 用当 时的其他 高级 语 言是无法编 写的 。

Brian Kern ighan 和 Dennis Ritchie 的著作的第 1 版 [ 60] 记录了 最初贝 尔 实验 室的 C

语言版本。随着时间的推移, 经过 多个标准化 组织的努力 , C 语 言也在不断地演变。 1989

年, 美国 国 家标 准学会 下的一个工作组推出 了 A NSI C 标准, 对最初的贝 尔 实验室的 C 语言做 了重 大修 改。ANSI C 与贝 尔 实验室的 C 有了很 大的不同, 尤其是 函数声 明的方式。Brian Kern ig han 和 Dennis Rit chie 在著作的第 2 版[ 61] 中描述了 A NSI C, 这本书至今仍被公认为关于 C 语言最好的参考手册之一。

国际标 准化 组织接 替 了对 C 语言进行标准化的任务, 在 1990 年推出 了一个几乎和ANSI C 一样的版本,称 为 " ISO C90"。该组织在 1999 年又对 C 语言做 了 更新, 推出" ISO C99" 。在这一版本中, 引入了 一些新的数据类型, 对使用不符合英语语言宇符 的文本字符 串提 供 了 支持。更新的 版本 2011 年得到批准, 称为 " ISO Cll", 其中再次添加了更多的数据类型和特性。最近增加的大多数内容都可以向后兼容,这意味着根据早 期标准(至少可以回 溯到 ISO C90 ) 编写的 程序按新标准编译时会有同样的行为。

GNU 编译 器 套 装 ( G NU Comp iler Collec­ tion, GCC) 可以基 于 不 同 的命令行 选项,依 照多 个不 同版本的 C 语言规则来编译程序,如 图 2-

1 所示。比如,根 据 ISO Cl l 来编译程序 pr og .

c, 我们就使用命令行:

linux> gee -s t d=e11 rp

og . e

图 2-1 向 GCC指定不同的 C 语言版本

编译选项- a ns i 和- s t d=c89 的 用 法是 一样的一一会根据 A NSI 或者 ISO C90 标准来编译程序。( C90 有时也称为 " C89" , 这是因为 它的 标准化 工作 是从 1989 年开始的。)编译选项- s t d =c 99 会让编译器按 照 ISO C99 的规则进行 编译。

本书 中,没 有指定任何编译选项时,程 序会按照基于 IS O C90 的 C 语言版本进行编 译,但 是 也 包括 一些 C99、Cll 的特性, 一些 C+ + 的特性, 还 有 一些是与 GCC 相关的 特性。GNU 项目正在开发一个结合 了 ISO Cl l 和其他一些特 性的版本, 可以通过命令行选项- s t d=gnull 来指定。(目前,这 个实现 还未完成。今)后, 这个版本会成为默 认的 版本。

2. 1 信息存储

大多数计算机使用 8 位的块, 或者宇节 ( byte ) , 作为最小的可寻址的内存单位, 而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称 为虚拟内存( virt ua l memo ry) 。内存 的 每个字节都由 一个唯一的数 字来标识, 称为它的 地址 C ad­ dr ess ) , 所有可能地址的集合就称为虚拟地址空 间 ( vir t ual add ress space) 。顾名思义, 这个 虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现(见第 9 章)是将动态随机访问存储器( DRAM ) 、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来, 为程序提供一个看上去统一的字节数组。

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

区 日 C 语言中指针的作用

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

真正理解指针需要查看它们在机器级 上的表示以 及 实现。这将是 第 3 章 的 重点之一, 3. 10. 1 节将 对其进行深入介绍。

  1. 1. 1 十六进制表示法

    一个字节由 8 位组成。在二进制表示法中,它 的 值 域是 00000000 2 ~ 111111112 。 如果看成十进制整数 ,它的 值域就是 010 ~ 25510。 两种符号表示法对于描述位模式来说都不是非常方便。二进制表示 法太冗长, 而十进制表示法与位模式的互相转化很麻烦。替代的方法是, 以 16 为基数 ,或 者叫做十六进制 ( hexadecimal) 数 , 来表示位模式。十六进制(简写为 " hex" ) 使用数字 ’ O’ ~ ’ 9 ’ 以及字符 ’ A ’ ~ ’ F ’ 来表示 16 个可能的值。图 2- 2 展示 了 1 6 个十六进制数字对应的 十进制值和二进制值。用十六进制书写,一 个字节的值域为 0016 ~FF16 o

图 2- 2 十六进制表示法。每个十六进制数字都对 16 个值中的一个进行了编码

在 C 语言中,以 Ox 或 OX 开 头 的 数 字常量被认为是十六进制的值。字符 ’ A ’ ~ ’ F '

既可以是大写,也 可以是小写。例如, 我们可以将数字 F A1 D37B16 写 作 Ox F A1 D37B, 或者

Oxfald37b, 甚至是大小写混合,比 如 , Ox Fa lD 3 7b 。 在本书中, 我们将使用 C 表示法来表示十六进制值。

编写机器级程序的一个常见任务就是在位模式的十进制、二进制和十六进制表示之间 人工转换。二进制和十六进制之间的转换比较简单直接,因 为可以一次执行一个十六 进制数字的转换。数字的转换可以参考如图 2-2 所示的表。一个简单的窍门是, 记 住十六进制数字 A、C 和 F 相应的十进制值。而对千把十六进制值 B、D 和 E 转换成十进制值,则 可以通过计算它们与前三个值的相对关系来完成。

比如, 假设给你一个数字 Ox l 7 3 A4C。 可以通过展开每个十六进制数字, 将它转换为二进制格式,如下所示:

十六进制A
二进制000101110011101001001100

这样就得到了二进制表示 0001 0111 0011101001 0011 00 。

反过来,如果 给定一个二进制数字 111100101011 011 0110011 , 可以通过首先把它分为每 4 位一组来转换为十六进制。不过要注意, 如果位总数不是 4 的倍 数 , 最左边的一组可以少于 4 位,前 面用 0 补足。然后将每个 4 位组转换为相应的十六进制数字:

二进制1111001010110110110011
十六进制3ADB

练习题 2. 1 完成下面的数字转换:

  1. 将 Ox 3 9A7F8 转换 为 二进 制。

    B. 将二进 制 11 00100101 111011 转换 为 十 六进 制。

    C. 将 Ox D5E4C 转换 为 二进 制。

    D. 将二进制 1001101110011110110101 转换 为 十 六进 制。

    当值 x 是 2 的非负整数 n 次幕时,也 就 是 x = 2飞 我们可以很容易 地将 x 写成十六进制形式,只 要记住 x 的二进制表示就是 1 后面跟 n 个 0。十六进制数字 0 代表 4 个二进制

    0。所以,当 n 表示成曰一句 的形式,其 中 O i 3 , 我们可以把 x 写成开头的十六进制数字为 l( i = O) 、 2( i = l ) 、4 ( i = 2 ) 或者 8 ( i = 3) , 后面跟随着]个十六进制的 0。比如, x = 2048 = 211’ 我们有 n = ll = 3 + 4 • 2, 从而得到十六进制表示 Ox8 00 。

    练习题 2. 2 填写 下表中 的 空白 项, 给出 2 的不 同次 幕的二进制和十 六进 制表 示:

    十进制和十六进制表示之间的转换需要使用乘法或者除法来处理一般情况。将一个十 进制数字 x 转换为十六进制, 可以反复地用 16 除 x , 得到一个商 q 和一个余数 r , 也就是x=q• l6+ r。然后 , 我们用十六进制数字表示的r 作为最低位数字,并 且 通过对 q 反复进行这个过程得到剩下的数字。例如, 考虑十进制 314 156 的转换:

    314 156=19 634• 16+12 (C)

    19 634= 1227• 16+2 (2)

    1227= 76• 16+11 (B)

    76= 4• 16+12 CC)

    4= 0• 16 + 4 (4)

    从这里, 我们能读出十六进制表示为 Ox 4CB2C。

    反过来, 将一个十六进制数字转换为十进制数字, 我们可以用相应的 16 的 幕乘以每个十六进制数字。比如, 给定数字 Ox 7AF , 我们计算它对应的十进制值为 7 • 1 62 + 10 • 16+15=7• 256+10• 16+ 15 = 1792 + 160 + 1 5 = 1 967 。

    练习题 2. 3 一个 字 节 可以用 两个十 六进制 数 字来 表 示。 填写 下表 中缺 失的项, 给 出不同 字 节模 式的 十进 制、 二进制和 十 六进制值 :

十。进制二进制 0000 0000十六进制 OxOO
167
62
188
00110111
1000 1000
1111 0011
Ox52
OxAC
OxE7

m 十进 制和十六进制间的转换

较大数值的 十进 制和 十六进 制之 间的 转换, 最好是让计算机或者计算器来完 成。有大量的工具可以 完成这 个工作 。一个简单 的方法就是 利用任 何标准的搜 索引 擎, 比如查询:

把 Ox a b c d 转换为十进 制数

把 1 23 用十 六进 制表 示。

练习题 2. 4 不 将数 字 转换 为 十进制或 者 二进制 , 试 着解答下 面 的 算 术题, 答 案 要用十六 进制表示。 提 示: 只要将执行 十进制加法和 减 法所使 用的方 法 改成以 1 6 为基数。

  1. Ox 5 03c +Ox 8 =

  2. Ox 5 03c - Ox 40 =

    C. Ox 5 03c +6 4=

    D. Ox 5 0e a - Ox 5 03c =

  3. 1. 2 字数据大小

    每台计算 机都有一个宇长 ( w o rd size) , 指明指针数据的标称大小( no minal s ize ) 。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空

间的最大大小。也就是说, 对于一个字长为 w 位的机器而言 , 虚拟地址的范围为 0 ~ w2

程序最多 访问 沪 个字节。

- l ,

最近这些年,出现了 大规模的从32 位字长机器到 64 位字长机器的迁移。这种情况首先出现在为大型科学和数据库应用设计的高端机器上,之后是台式机和笔记本电脑,最近则出现在 智能手机的处理器 上。32 位字长限制虚拟地址空间为 4 千兆字节(写作 4GB) , 也就是说,刚刚超过 4 X l 铲字节。扩展到 64 位字长使得虚拟地址空间为 16EB, 大约是 1. 84 X l 沪字节。

大多数 64 位机器也可以运行为 32 位机器编译的程序, 这是一种向后兼容。因此,举例来说, 当程序 pr o g . c 用如下伪指令编译后

li nux> g e e - m3 2 pr og . e

该程序就可以在 32 位或 64 位机器上正确运行。另一方面, 若 程序用下述伪指令编译

码的数字格式,如不同长度的整数和浮点

示为 4 字节和 8 字节的浮点数。

C 语言支持整数和浮点数的多种数据格式。图 2-3 展示了为 C 语言各种数据类

图 2-3 基本 C 数据类型的典型大小(以字节为单位)。分配的字节数受程序是如何 编译的影响而变化。本图给出的是 32 位和 64 位程序的典型值

型分配的字节数。(我们在 2. 2 节讨论 C 标准保证的字节数 和典型的字节数 之间的关系。) 有些数据类型的 确切字节数依赖于程序是 如何被编译 的。我们给出的是 32 位和 64 位程序的典型值。整数或者为有符号的,即可以表示负数、零和正数;或者为无符号的,即只能 表示非负数 。C 的数据类型 c ha r 表示一个单独的字节。尽管 " cha r" 是由于它被用来存储文本串中的单 个字符这一事 实而得名, 但它也能被用来存储整数值。数据类型 s hor t 、i n t 和 l o ng 可以提供各种数据大小 。即使是为 64 位系统编译, 数据类型 l 阰 通常也只有

4 个字节。数 据类型 l o ng 一般在 32 位程序中为 4 字节, 在 64 位程序中则 为 8 字节。

为了避免由于依赖 "典型” 大小和不同 编译器设置带来的奇怪行为, IS O C99 引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其中就有数据类型 int32 t 和 i n t 64 t, 它们分别为 4 个字节和 8 个字节。使用确定大小的整数类型是 程序员准确控制数据表示的最佳途径。

大部分数据类型都编码为有符号数值 , 除非有前 缀关键字 uns i g ne d 或对确定大小的数据类型使 用了特定的无符号声明 。数据类型 c h a r 是一个例外。尽管大多数编译器和机器将它们视为有符号数 , 但 C 标准不保证这一点 。相反, 正如方括号指示的那 样,程序 员应该用有符号字符的声明来保证其为一个字节的有符号数值。不过,在很多情况下,程序 行为对数据类型 c har 是有符号的还是无符号的并不 敏感。

对关键字的顺序以 及包括还是省略可选关键字来说, C 语 言 允 许 存在多种形式。比如,下面所有的声明都是一个意思:

unsigned long unsigned long int long unsigned long unsigned int

我们将始终使 用图 2-3 给出的格式。

图 2-3 还展示了指针(例如 一个被声明为类 型为 " c h ar * “的变量)使用程序的全字长。大多数机器还支持两 种不同的浮点 数格式: 单精度(在C 中声明为 fl o a t ) 和双精度

(在 C 中声明为 d o ub l e ) 。这些格式分别使用 4 字节和 8 字节。

声明指针

对于任何 数据类型 T , 声明

T *p;

表明 p 是一个指针 变量,指 向一个类型 为 T 的对象。例如 ,

char *p;

就将一个指针 声明 为指 向一个 c h ar 类型的 对象。

程序员应该力图使他们的程序在不同的机器和编译器上可移植。可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。C 语言标准对不同数据类型的数字范围设置了下界(这点在后面还将讲到), 但是却没有上界。因为从 1980 年左右到 2010 年左右, 3 2 位机

器和 32 位程序是主流的组合, 许多程序的编写都假设为图 2- 3 中 32 位程序的字节分 配。随着 64 位机器的日益普及, 在将这些 程序移植到新机 器上时 , 许多隐藏的对字长的 依赖性就会显现出来, 成为错误。比如,许多程序员假设一个声明为i nt 类型的程序对象能被用来存储一个指针。这在大多数32 位的机鞋上能正常工作, 但是在一台64 位的机器上却会导致问题。

  1. 1. 3 寻址和字节顺序

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

    排列表示一个对象的字节有两个通用的规则。考虑一个 w 位的整数,其 位表示为[ x..,,- 1’ X ,.,- 2’ … , X1, X。J’ 其 中 X w- 1 是最高有效位, 而 x。是最低有效位。假设 w 是 8 的倍数,这些位就能 被分组成为字 节,其 中最 高 有效字节包含位[ x心 一 I • X..,,- 2 • … , x正 s J’ 而最低有效

    字节包含位 [ x1’ Xs’ … , x 。], 其他字节包含中间的位。某些机器选择在内存中按照从最低 有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效 字节的顺 序存储。前一种规则- 最低有效字节在最前面的方式 ,称 为小端 法( little en dian)。后一种规则 —— 最高有效字节在最前面的方式,称 为大端 法( big endian) 。

    假设变量 x 的类型为 i n t , 位于地址 Ox l OO 处 ,它 的 十六进制值为 Ox 01 2 3 45 67。地址范围 Ox l OO~ Ox1 0 3 的 字 节顺序依赖于机器的类型:

–壺 #

小端法

OxlOO OxlOl Ox1 0 2 Ox103

注意,在字 Ox 01 23 45 67 中, 高位字节的十六进制值 为 Ox Ol , 而低位字节值为 Ox 67 。

大多数 Intel 兼容机都只用小端模式。另一方面, IB M 和 Oracle ( 从 其 201 0 年收购Sun Microsys tems 开始)的大多数机器则是按大端模式操作。注意我们说的是“大多数”。这些规则并 没有严格按照企业界限来划分。比如, IBM 和 Oracle 制造的个人计算机使用的是 Intel 兼容的处理器,因 此 使 用 小 端法。许多比较新的微处理器是双端 法 ( bi-endian) , 也就是说可以把它们配置成作为大端或者小端的机器运行。然而, 实际情况是: 一 旦 选择了特定操作 系统,那 么 字节顺序也就固定下来。比如,用 于 许 多 移动电话的 AR M 微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上最常见的两种操作系统——

And roid( 来自 Google) 和 IOS (来自 Apple) 却只能运行于小端模式。

令人吃惊的是,在哪种字节顺序是合适的这个问题上,人们表现得非常情绪化。实际上,术语 " lit tle endian(小端)” 和 " big endian( 大端)” 出 自 Jo nat han Swift 的《格 利 佛 游 记》(Gulliver ’ s T ravels)一书, 其 中 交战的两个派别无法就应该从哪一端(小端还是大端) 打开一个半熟的鸡蛋达成一致。就像鸡蛋的问题一样,选择何种字节顺序没有技术上的理 由,因此争论沦为关千社会政治论题的争论。只要选择了一种规则并且始终如一地坚持, 对于哪种字节排序的选择都是任意的。

田日 ”端”的起源

以下是 J on at han Swift 在 1 72 6 年关于大小端之 争历史的 描述:

"……我下 面要 告诉你的是 , L ill ip ut 和 Blefu sc u 这两 大强国 在过去 36 个 月里 一直在苦战。战争开始是 由于以下的原 因: 我们大家都认为 , 吃鸡蛋前, 原始的 方 法是 打破鸡蛋较大的一端,可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个 手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命令全体臣民吃鸡蛋时打 破鸡蛋较小的 一端, 违令者重罚。老百姓们 对这项命 令极为反感。 历 史告 诉我 们, 由此曾发 生过 六次叛 乱, 其中一 个皇帝送 了命, 另一 个丢了王位。这些叛 乱大多都是由 Ble­ fu s cu 的国王大臣们 煽动起 来的。叛乱平息后 , 流亡的人 总是逃到 那个 帝国 去寻救避难。据估计, 先后几次有 11 000 人情愿受死也 不肯去打破 鸡蛋 较小的 一端。 关 于这一 争端, 曾出版过几百本大部著作,不过大端派的书一直是受禁的,法律也规定该派的任何人不 得做官。”( 此段译文摘自网上 蒋剑锋 译的《格利佛 游记》第一 卷第 4 章。)

在他那个时代 , S w ift 是在讽刺 英 国 C Lill ip ut ) 和法国 ( Blefu s cu ) 之间持续的 冲 突。

Danny Cohen, 一位网络协议的早期开创者,笫一次使用这两个术语来指代字节顺序

[24], 后来这 个术语被 广泛 接纳了 。

对于大多数应用程序员来说,其机器所使用的字节顺序是完全不可见的。无论为哪种 类型的机器所编译的程序都会得到同样的结果。不过有时候,字节顺序会成为问题。首先 是在不同类型的机器之间通过网络传送二 进制数 据时, 一个常见 的问题是当小端法机器产生的数据被发送到大端法机器或者反过来时, 接收程序 会发现, 字里的字节成了反序的。为了避免这类问题,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以 确保发送方机器将它的内部表示转换成网络标准,而接收方机器则将网络标准转换为它的 内部表示。我们将在第 11 章中看到这种转换的 例子。

第二种情况是,当阅读表示整数数据的字节序列时字节顺序也很重要。这通常发生在检查机器级程序时。作为一个示例,从某个文件中摘出了下面这行代码,该文件给出了一个针对 In t el x8 6-64 处理器的机器级代码的文本表示:

4004d3: 01 05 43 Ob 20 00 add %eax,Ox200b43(%rip)

这一行是由反汇编 器( d isas s em bler ) 生成 的, 反汇编器是一种确定 可执行程序文件所表示的指 令序列 的工具。我们将 在第 3 章中学习有关 这些工具的更多知识,以 及怎样解释像这样的行。而现在,我们只 是注意这行表述的 意思是: 十六进制字 节串 01 05 43 Ob 20 00 是一条指令的字节级表示,这条指令是把一个字长的数据加到一个值上,该值的存储地址由 Ox 2 00b 43 加上当前 程序计数 器的 值得到, 当前程序计数器的值即为下 一条将要执行指令的地址。如果取出这个序列的最后 4 个字节: 43 Ob 20 00, 并且按照相反的顺序写出,我

们得到 0 0 20 Ob 43。去掉开头的 o, 得到值 Ox 2 00b 43 , 这就是右边的数值。当阅读像此

类小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,这正好和通常书写数字时最高有效位在左边,最低有效位在右边的方式相反。

字节顺序变得重要的第三种情况是当编写规避正常的类型系统的程序时。在 C 语言中, 可以通过使用强制 类型 转换 ( ca s t ) 或联合( unio n ) 来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推 荐这种编码技巧,但是它们对系统级编程来说是非常有用,甚至是必需的。

图 2-4 展示了一段 C 代码,它 使用强制类型转换 来访问和打印不同程序对象的 字节表示。我们用 t y pe d e f 将数据类型 b yt e _ p o i n t er 定义为一个指向类型为 " u n s i g ne d

cha r” 的对象的指针。这样一个字节指针引用一个字节序列 , 其中每个字节都被认为是一个非负整数 。第一个例程 s h o w_ b y t e s 的 输入是一个字节序列的地址 ,它 用 一个字节指针以及一个字节数 来指示。该字节数指定为数据类型 s i ze —七, 表示数据结构大小的首选数据类型。s how _ b yt e s 打印出每个以 十六 进制表示的字节。C 格式化指令 " %. 2x" 表明整数必须用至 少两个数字的十六进 制格式输出。

1 #include <s t di o . h>

2

3 typedef unsigned char *byte_pointer;

4

5 void show_bytes(byte_pointer start, size_t len) {

6 s. 1ze_t 1;

7 for (i = O; i < len; i++)

s printf (" %. 2x", start [i]) ;

9 printf("\n");

10 }

11

  1. void show_int(int x) {

  2. show_bytes((byte_pointer) &x, sizeof(int));

    14 }

    15

  3. void show_float(float x) {

  4. show_bytes((byte_pointer) &x, sizeof(float));

    18 }

    19

  5. void show_pointer(void *x) {

  6. show_bytes ((byte_pointer) &x, sizeof (void *));

    22 }

图2- 4 打印程序对象的字节表示。这段代码使用强制类型转换来规避类型系统。很容易定义针对其他数 据类型的类似函数

过程 s h o w_ i n t 、 s ho w_ f l o 扛 和 s ho w_p o i n t er 展示了如何使用程序 s ho w_b yt e s 来分别输出类型为 i n t 、f l o 红 和 v o i d * 的 C 程序对象的字节表示。可以 观察到它们仅仅传递给 s ho w—b yt e s 一个指向它们参数 x 的指针 &x , 且这个指针被强制类型转换为 " u n ­ signed char * “。这种强制类型转换 告诉编译器, 程序应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后,这个指针会被看成是对象使用的最 低字节地址。

这些过程使用 C 语言的运算符 s i ze o f 来确定对象使用的字节数。一般来说 , 表达式sizeof (T ) 返回存储一个类型为 T 的对象所需要的字节数。使用 s i ze o f 而不是一个固定的值,是向编写在不同机器类型上可移植的代码迈进了一步。

在几种 不同的机器上运行如图 2-5 所示的代码, 得到如图 2- 6 所示的 结果。我们使用了以下几种机器:

Linux 32: 运行 Lin ux 的 In tel IA 32 处理器。

Windows: 运行 Window s 的 I nt el IA 32 处理器。

Sun: 运行 Solaris 的 Sun Microsystems SPARC 处理器。(这些机器现在由Oracle 生产。)

Linux 64: 运行 Lin ux 的 In tel x86 - 64 处理器。

void test_show_bytes(int val) {

  1. int ival = val;
    1. float fval = (float) ival;
    2. int *pval = &ival;
    3. show_int(ival);
    4. show_float(fval);
    5. show_pointer(pval);

8 }

code/data/show-bytes.c

code/data/show-bytes.c

图 2-5 字节表示的示例。这段代码打印示例数据对象的 字节表示

图2-6 不同数据值的字节表示。除了字节顺序以外 , i nt 和 fl oa t 的结果是一样的。指针值与机器相关参 数 1 2 3 45 的 十六进制表示为 Ox 0 0 00 3 0 3 9 。 对 千 i n t 类型的数据,除 了 字 节 顺 序以

外, 我们在所有机器上都得到相同的结果。特别地, 我们可以看到在 L in u x 3 2、W in dow s 和 L in u x 64 上,最 低 有效 字 节值 Ox 3 9 最 先 输 出 , 这说明它们是小端法机器; 而 在 S u n 上最后输出,这 说明 S u n 是 大 端 法 机 器 。 同 样 地 , f l o a t 数 据 的 字 节 ,除 了 字 节 顺 序 以 外 , 也 都 是 相 同 的 。 另 一 方 面,指 针值却是完全不同的。不同的机器/操作系统 配置使用不同的存储分配规则。一个值得注意的特性是 L in u x 3 2、W i nd o w s 和 S u n 的机器使用 4 字节地址,而 L i n u x 64 使 用 8 字节地址。

使用t ype de f 来命名数据类型

C 语言中的 t y p e d e f 声明 提供了一种给数据类型命名的方式。这能够极 大地 改善代码的可读性,因为深度嵌套的类型声明很难读懂。

t yp e d e f 的语法与 声明 变量的 语法十分相像 ,除 了它使 用的 是类型名, 而不是 变量名。因此, 图 2- 4 中 b y 七e _ p o i n t er 的声 明和将一个变量声 明 为 类型 " u n s i g n e d char

* “有相同的形式。

例如,声明:

typedef int•int_pointer; int_pointer ip;

将类型 " i n t _ p o i n t er " 定义为 一个指向 i n t 的指针, 并且声明 了一 个这种类型的变量 i p 。我们还可以将这个变量 直接 声明 为:

int *ip;

一 使用 pr i n t f 格式化输出

p r i n t f 函数(还有它的 同 类 f p r i n t f 和 s pr i n t f ) 提供 了一 种 打 印 信 息 的 方式, 这 种方式对格式化细节有相 当 大 的 控 制能力。 第 一 个 参 数 是 格 式 串 ( fo r m a t string), 而其余的参数都是要打印 的值 。在 格 式 串 里 , 每 个 以 " %” 开始的 宇符序 列 都 表 示如何格 式化下一个参数。典型的示例 包括 : ’ %ct’ 是 输 出一 个十进制整数, ’ %f ’ 是 输 出一 个 浮点数, 而 ’ %c ’ 是 轮 出一个宇符 , 其编码由参数给出。

指定确定大小数据 类型的格式, 如 i n 七3 2 _ t , 要 更 复 杂 一 些, 相 关内容参 见 2. 2. 3

节的 旁注。

可以观察到, 尽 管 浮点 型 和整 型数 据都 是对 数 值 1 2 345 编 码 , 但 是 它 们 有 截 然 不 同 的字节模 式 : 整 型 为 Ox 0 0 0 03 03 9 , 而 浮 点 数 为 Ox 4 64 0E 4 0 0。 一 般 而言 , 这 两 种 格 式 使 用 不同的 编 码方法。如果我们将这些 十六 进制模式扩展为二进制形式, 并 且 适 当 地 将 它 们 移位, 就会发 现一 个 有 1 3 个 相 匹 配 的 位 的 序 列 , 用一 串 星号 标识 出来 :

0 0 0 0 3 0 3 9

0000000000000000001100000011 1 001

**** * ********

4 6 4 0 E 4 0 0

01000110010000001110010000000000

这并不是巧合。当我们研究浮点数格式时, 还 将 再 回 到 这 个 例 子 。

凶 ii1 指 针 和 数 组

在 函数 s ho w b y t e s ( 图 2-4) 中, 我们看到指针和数组之间 紧密的 联 系, 这 将 在 3. 8 节中详 细描述。这个函数有一个类型 为 b y t e _p o i n t er ( 被 定 义 为一 个指 向 u n s i g ne d c ha r 的指针)的参数 s t ar t , 但是我们在 第 8 行 上 看到数 组引用 s t ar t [ i ] 。在 C 语 言 中, 我 们能够用数组表示法来引用指针,同时我们也能用指针表示法来引用数组元素。在这个例子 中·, 引用 s t a r t [ i ] 表 示我们想要读取以 s t ar t 指向的位置为起 始的 第 i 个位置处的 字节。

JI 指 针 的 创 建 产 间接引 用

在图 2-4 的第 1 3、1 7 和 21 行 , 我 们看到对 C 和 C++ 中两种 独 有 操 作 的 使 用。 C 的

”取 地 址” 运 算 符 & 创建一个指针。在这三行中,表 达 式 &x 创建了 一 个指向保存 变量 x 的位置的 指针。这 个指 针 的 类型取 决 于 x 的 类型, 因 此 这 三 个指 针 的 类 型 分 别 为 i n t *、fl oa t *和 v o i d ** 。(数据类型 vo i d *是一种特殊类型的指针, 没有相 关联的 类型信息。)

强制类型 转换 运 算 符 可以 将 一 种数 据 类 型 转换 为 另 一 种 。 因 此 , 强 制 类 型 转 换( b y t e主 o i n t e r ) &x 表 明 无 论 指 针 &x 以 前 是 什 么类型, 它现 在 就是 一 个指 向 数 据 类型为 u n s i g n e d c h ar 的 指 针 。 这 里 给 出的 这 些强 制类型转换不会 改 变 真 实的 指 针 , 它们只是告诉编译器以新的数据类型来看待被指向的数据。

m 生成一张 ASCII 表

可以 通过执行命令 ma n a s c 江 来得 到一张 ASCII 宇符码的表。

练习题 2. 5 思考下面对 s h o w_ b y 七e s 的 三次调用:

int v a l = Ox87654321;

byte_pointer valp = (byt e _po i nt er ) &v al ;

show_bytes(valp, 1); I* A. *I show_bytes(valp, 2); I* B. *I show_bytes(valp, 3); I* C. *I

指出在小端法机器和大端法机器上,每次调用的输出值。

  1. 小端法: 大端法:
  2. 小端法:
    1. 小端法:

      大端法: 大端法:

练习题 2. 6 使用 s h o w_ i n t 和 s h o w_ f l o a t , 我们确定整数 3510593 的十 六进 制表 示为 Ox 0035 9141 , 而浮 点数 351 05 93 . 0 的 十 六进制表 示为 Ox 4A5645 04。

  1. 写出这两个十六进制值的二进制表示。
  2. 移动这两个二进制串的相对位置,使得它们相匹配的位数最多。有多少位相匹配呢?
  3. 串中的什么部分不相匹配?

2. 1. 4 表示字符串

C 语言中字符串被编码为一个以 null ( 其值为 0 ) 字符结尾的字符数组。每个字符都由某个标准编码来表示,最 常 见 的 是 ASCII 字符码。因此, 如果我们以参数 " 12345” 和 6

(包括终止符)来运行例程 s h ow_bytes, 我们得到结果 31 32 33 34 35 00。请注意, 十进制数字 x 的 ASCII 码正好是 Ox3x , 而 终 止 字节的十六进制表示为 Ox OO。 在 使 用 ASCII 码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。

_.练习题 2. 7 下 面对 s ho w_b yt e s 的调用将输出什 么结 果?

const char *B = “abcdef”;

, show_bytes ((byte_pointer) s, strlen (s)) ;

注意字母 ’ a ’ ’ z ’ 的 ASCII 码为 Ox 6l Ox 7A 。

因 日 文字编码的 Un icode 标 准

ASCII 字符 集适合 于编码 英语 文档,但 是 在表达一些特殊宇符 方 面并 没有太多 办法, 例如法语的 “C”。 它完全不适合 编码希腊语、俄语和中文等语言的文档。这些年,提 出了很 多方 法来对不同语 言的文字进行 编码。Unicod e 联 合 会 ( U ni code Co nsorti um) 修订了 最全面且 广泛接 受的文字编码 标准。当前的 Unicode 标准( 7. 0 版)的宇库 包括 将近 100 000 个字符, 支持广泛的 语言种类, 包括古埃及和巴比伦的语言。为 了保 持 信 用, U nicode 技 术委员会 否决了为 K ling on( 即电视 连续剧《星际迷航 》中的虚构文明)编写语 言标准的提 议。

基本编码, 称为 U nicode 的“统一字符集“,使 用 32 位 来表示字符 。这好像要求文本串中每 个宇符要占用 4 个宇节。 不过 , 可以有一些替代编码, 常见的宇符只需要 1 个或 2 个字节, 而不太常用的 字符 需要多一些的 字节数 。特别地, U T F-8 表 示将每个字符 编码为一 个字节序 列, 这样标准 ASCII 字符还是使 用和它们在 ASCII 中一样的单宇 节编码,这 也 就 意味 着所 有的 ASCII 宇节序 列用 ASCII 码表示和 用 U T F-8 表 示是 一样的。

Java 编程语言使用 U nicod e 来表示字符 串。 对于 C 语言也有支持 U nicode 的程序库。

2. 1. 5 表示代码

考虑下面的 C 函数:

1 int swn(int x, int y) {

return x + y;

当我们在示例机器上编译时,生成如下字节表示的机器代码:

Linux 32 55 89 e5 8b 45 Oc 03 45 08 c9 c3

Windows 55 89 e5 8b 45 Oc 03 45 08 5d c3

Sun 81 c3 eO 08 90 02 00 09

Linux 64 55 48 89 e5 89 7d fc 89 75 f8 03 45 fc c9 c3

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

计算机系统的 一个基本概念就是, 从机器的角度来看, 程序仅仅只是 字节序列。机器没有关千原 始源程序的 任何信息, 除了可能有些用 来帮助调试的辅助表以外。在第 3 章学习机器级编程时 ,我 们将更清楚地看到这一点 。

  1. 1. 6 布尔代数简介

    二进制值是计算 机编码、存储 和操作信息的核心,所以围绕数值 0 和 1 的研究已经演化出了丰富的数学知识 体系。这起源于 18 50 年前后乔治·布尔 ( George Boole, 1815—18 64 ) 的

    工作, 因此也称为布 尔代数 ( Boolean algebra ) 。布尔注意到通过将逻辑值 TRUE ( 真)和

    FALS E ( 假)编码为二进制值 1 和 o, 能够设计出一种代数,以研究逻辑推理的基本原则。最简单的布尔代数是在二元集合{0, 1 }

    基础上的定 义。图 2- 7 定义了这种布尔代数 0 I

    中的几种运算 。我们用 来表示这些运算的符

号与 C 语言位级运算使用的符号是相匹配的, 这些将在后 面讨论到。布尔运算 ~ 对应于逻辑运 算 NOT , 在命题逻辑中用符号–,

表示。也 就是说, 当 P 不是真的时候, 我

图 2-7 布尔代数的运算 。二进制值 1 和 0 表示逻辑值 T RUE 或者 FALSE , 而运 算符

~ 、&、I 和^分别表示逻辑运算 NOT 、

AND、OR 和 EXCLUSIVE-OR

们就说–ip 是真的,反 之亦然。相应地 ,当 P 等于0 时, - P 等于 1, 反之亦然。布尔运算

& 对应于逻辑运算 AND , 在命题逻辑中 用符号I\ 表示。当 P 和Q 都为真时,我 们说 p I\

Q 为真。相 应地,只 有当 p = l 且 q = l 时, p &.q 才等于 1。布尔运算 1 对应于逻辑运算

OR, 在命题 逻辑中用符 号 V 表示。当 P 或者 Q 为真时, 我们说 P V Q 成立。相应地, 当p= l 或者 q= l 时, p lq 等于 1。布尔运算^对应于逻辑 运算 异或, 在命题逻辑中用符号令表示。 当 P 或者Q 为真但不同 时为真时 ,我们说 P 令Q 成立。相应地 , 当 p = l 且 q = O, 或者 p = O 且 q = l 时, p Aq 等千 1。

后来创立信息论领域的 C la ud e S ha nno n0 916- 2001 ) 首先建立了布尔代数和数字逻辑之间的联 系。他在 1 937 年的硕士论文中表明了布尔代数可以 用来设计和分 析机电继电器网络。尽管那时计算机技术已经取得了相当的发展, 但是布尔代数仍然在数字系统的设计和分析中扮演着重要的角色。

我们可以将上述 4 个布尔运算 扩展到位向 量的运算,位 向量就是固定长度为 w 、由 0

和 1 组成的串。位向量的 运算可以定 义成参数的每个对应元素之间的运算。假设 a 和b 分

别表示位向量[ a w- 1 • a w- 2 • …, a。] 和[ b.,.- 1 , bw- 2 , …, b。]。我们将 a & b 也定义为一个 长度为 w 的位向量, 其中第 1 个元素等于a ;& b; , O i < w 。可以用类似的方式将运算 I 、^

和~扩展到位向量上。

举个例子, 假设 w = 4 , 参数 a = [0110], b= [1100]。那么 4 种运算 a & b、a l b 、a A b

和- b 分别得到以下结果: ,

0110

& 1100

0100

0110

I 1100

1110

0110

- 1100

1010

一 11 00

0011

饬 练习题 2. 8 填写下表,给出位向量的布尔运算的求值结果。

运算 结果
a b -a -b a&b a l b a l\ b[01101001] [0101 0101]

“ 关千布尔代数和布尔环的更多内容

对于任 意整数 w > O, 长度 为 w 的位向量上的 布 尔运算 I 、& 和~ 形成了一 个布 尔

代数。最简单 的情况是 w = l 时,只有 2 个元素;但 是对于更普 遍的情况,有 沪 个长度为 w 的位向量。布尔代数和整数算术运算有很 多相似之处。例如, 乘法对加 法的 分配律,写 为 a • (b+c)=(a• b)+(a• c), 而布 尔运算 & 对1 的分配律 ,写 为 a & ( b Jc ) = (a&b) I ( a & c) 。 此外,布 尔运 算1 对 & 也有分配律 ,写 为 a l (b&c)=(aJb)&(alc), 但是对于整数我 们不能说 a + ( b • c)=(a+b)• (a+ c)。

当考 虑长度 为 w 的位向 量上 的^、&和~ 运算时, 会得到一种不同的 数学形 式, 我们称 为布 尔环( Boolea n r ing ) 。布 尔环与整数运算有很 多相同的 属性。例如,整 数运算的一个属性 是每个值 x 都有一个加 法逆元 ( addit ive inverse)-x, 使得 x + ( - x ) = O。 布

尔环也有类似的属性,这里的“加法”运算是^,不过这时每个元素的加法逆元是它自

己本 身。也 就是说, 对于任何值 a 来说 , a Aa = O, 这里我们用 0 来表 示全 0 的位向量。可以 看到 对单个位 来说这是成立的 , 即 O AO= l Al = O, 将这个扩展到位向量也是成立的。当我们重新排列组 合顺序,这 个属性也 仍然成 立,因此有 ( a Ab ) Aa = b。这个属性 会引起一些很有趣的结果和聪 明的技 巧,在 练习题 2. 10 中我们 会有所探 讨。

位向量一个很有用的应用就是表示有 限集合。我们可以用位向量[ a 心 一 I ’ … , a1, a。] 编码任何子集 Ai;;;;;;;{o , 1, …, w - 1 } , 其中 a , = 1 当且仅当 i E A。例如(记住我们是把 a…,- 1 写 在左边,而 将 a。写在右边), 位向量 a == [ 011 01001 ] 表示集合 A = { O, 3, 5, 6},而 b兰 [ 01010101] 表示集合 B = {O, 2, 4, 6 }。使用 这种编码集合的方法, 布尔运算 1 和

& 分别对应千集合的并和交,而 ~ 对应于于集合的补。还是用前面那个例子, 运算 a & b

得到位向量[ 01000001] , 而 A n B = {O, 6} 。

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

练习题 2. 9 通过混合三种不同颜色的光(红色、绿色和蓝色),计算机可以在视频屏幕或者 液晶 显示器上 产 生 彩 色 的 画 面。 设想 一 种简 单的 方 法 , 使 用 三 种 不 同 颜色 的光,每种光都能打开或关闭,投射到玻璃屏幕上,如图所示:

光源 玻璃屏幕

那么基于光 源 R ( 红)、G ( 绿)、B ( 蓝)的关 闭 ( 0 ) 或打开 (1 )’ 我们 就 能 够 创 建 8

种不 同的颜色 :

。 。 。 。 。 #

这些颜色中的每 一种都能用一个长度为 3 的位向量来表 示,我们可以对它们进行布尔运算。

  1. 一种颜色的补是通过关掉打开的光源,且打开关闭的光源而形成的。那么上面列 出的 8 种颜色每一种的补是什 么?
    1. 描述下列颜色应用布尔运算的结果:
蓝色l绿色
黄色 红色蓝绿色 红紫色
  1. 1. 7 C 语 言 中 的 位 级 运 算

    C 语言的 一个 很 有 用 的 特性 就 是 它 支待按位布尔运算。事实上, 我们在布尔运算中使用的那些符号就是 C 语言所使用的: I 就 是 O R ( 或),& 就 是 AND ( 与),~就 是 NOT ( 取

    反), 而^就是 EX CLUSIVE-OR ( 异或)。这 些 运算能运用到任何 “ 整型” 的 数 据 类型上, 包括图 2- 3 所示内容。以下是一些对 c h ar 数据类型表达式求值的例子:

C 的 表达式二进制表达式二进制结果十六进制结果
~Ox41- [0100 0001][! Oll ll!O]OxBE
~OxOO- [0000 0000][1111 1111]OxFF
Ox69&0x55[OllO 1001]&[0101 0101][0100 0001]Ox41
Ox69l0x55(0110 10011 I (0101 01011[01111101)Ox7D

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

练习题 2. 10 对于任 一位向量 a , 有 a Aa = O。 应用这 一属性, 考虑下 面的程序:

void inplace_s-wap(int *X, int *Y) {

*Y = *X- *Yi I* Step 1 *I

*X = *X- *Yi I* Step 2 *I

*Y = *X- *Yi I* Step 3 *I

正如程 序名 字所暗 示的 那样, 我们认为 这个过程的效果是交换指针变 量 x 和 y 所指向的存储位置处存放的值。注意,与通常的交换两个数值的技术不一样,当移动一个值 时,我们不需要第三个位置来临时存储另一个值。这种交换方式并没有性能上的优 势,它仅仅是一个智力游戏。

以 指针 x 和 y 指 向的位置存储的值分别是 a 和 b 作为 开始, 填 写 下表, 给出在 程序的

每 一步 之后, 存储在这 两个位 置 中 的 值。 利 用^的属 性证 明 达到 了 所希 望 的 效果。回想一下, 每个元 素 就是它 自 身的加法逆元 ( a Aa = O) 。

步骤 初始*x a*y b
第1步
第2步 第3步

练习题 2. 11 在练 习 题 2. 10 中的 i n p l a c e—s wa p 函数的基础 上, 你决定写 一段代码 , 实现将一个数组中的元素头尾两端依次对调。你写出下面这个函数:

void reverse_array(int a[], int cnt) { int first, last;

for (first = 0, last = cnt-1; first<= last; first++,last–)

inplace_swap(&a[first], &a[last]);

当你 对一个 包含元 素 l 、 2 、 3 和 4 的数 组使 用这个函 数 时, 正 如 预 期 的 那样, 现在 数组 的元 素 变 成 了 4 、 3 、 2 和 1 。不过, 当你对一个 包含元素 1 、2 、3 、4 和 5 的数组使用 这 个 函数 时, 你 会很惊奇地看到得到 数 字的元 素为 5、4、0、2 和 1。 实际上, 你会发现这段代码对所有偶数长度的数组都能正确地工作,但是当数组的长度为奇数时, 它就 会把 中间的元 素设 置成 0 。

  1. 对于一个 长 度 为 奇数的 数 组 , 长 度 c n t = 2k+ 1,

    循环 中 , 变 量 f i r s t 和 l a s t 的值分别是什 么?

    函 数r e v er s e _ a r r a y 最后 一 次

  2. 为 什 么这 时调用 函 数 i n p l a c e _ s wa p 会 将数组 元素设 置为 0 ?

    1. 对 r e v e r s e _ar r a y 的代码做 哪 些简单改 动就能消除这个问题?

      位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字 中选出的位的集合。让我们来看一个例子, 掩 码 Ox FF( 最 低 的 8 位 为 1) 表 示 一 个 字 的 低 位字 节 。 位 级 运 算 x &Ox FF 生 成 一 个 由 x 的 最 低 有 效 字 节 组 成 的 值 , 而 其 他 的 字 节 就 被 置 为

  3. 。 比 如 , 对 千 x = Ox89ABCDEF, 其 表 达式 将得 到 Ox OOOOOO EF 。 表 达式 - 0 将生 成 一 个 全

  4. 的掩码 , 不 管 机 器 的 字 大 小 是 多 少 。 尽 管 对 于 一 个 32 位 机 器来 说 , 同 样 的 掩 码 可 以 写 成

    OxFFFFFFFF, 但是这样的代码不是可移植的。

练习题 2. 12 对于下面的 值, 写 出 变 量 x 的 C 语 言表达 式。 你 的 代 码 应 该 对任何 字长 w 8 都 能 工作。我 们 给出 了 当 x = Ox 8 765 4321 以及 w = 32 时表达 式 求值的结果, 仅供参考。

  1. X 的最低有效 字 节 , 其他位均置为 0 。 [ Ox 0 00000 21 ] 。

  2. 除了 x 的最低有效字节 外, 其他的位都取 补, 最低有效字节保 持不变。 [Ox789 ABC21]。

  3. X 的最低 有效 字 节设 置成全 1, 其他 字 节都保持不 变 。 [Ox 8 7 65 43FF] 。

    练习题 2. 13 从 20 世纪 70 年代末到 80 年代末, Dig it a l Eq uipm e nt 的 V A X 计 算机是一种非 常流行 的 机 型。 它 没 有 布 尔运 算 A ND 和 OR 指令, 只 有 b i s ( 位 设 置)和b i c ( 位 清除)这两种指令。 两种指令的输入都是 一个 数据 字 x 和 一个 掩码 字 m。 它 们生成 一个 结果 z , z 是由根据掩码 m 的位 来修 改 x 的位得到 的。使用 b i s 指令 , 这 种修改就是在 m 为 1 的每个位置上, 将 z 对应 的位设置为 1 。 使 用 b 江:指 令 , 这 种修 改就是在 m 为 1 的 每 个 位 置, 将 z 对应 的位设 置为 0。

    为 了看清楚这些运 算与 C 语言位级运算的关系,假 设我们 有两个 函数 bi s 和 bi c 来实

    现位设置和位清除操作。只想用这两个 函数, 而 不使 用任何其他 C 语言运算, 来 实现按位1 和^运算。填写下列代码中缺失的代 码。提示: 写出 b i s 和 b i c 运算的 C 语言表达式。

    I* Declarations of functions implementing operations bis and bic *I int bis(int x, int m);

    int bic(int x, int m);

I* Compute xly using only calls to functions bis and bic *I int bool_or(int x, int y) {

int r esul t = · return result;

. I* Compute x-y using only calls to functions bis and bic *I int bool_xor(int x, int y) {

int result=· return result;

2. 1. 8 C 语 言 中 的 逻 辑 运 算

C 语 言 还 提 供 了 一 组 逻 辑 运 算 符 II 、&.&. 和!, 分 别 对 应 于 命 题 逻 辑 中 的 O R 、A N D 和 NOT 运算。逻辑运算很容易 和位级运算 相 混淆, 但 是 它 们 的 功 能 是 完 全 不 同 的 。 逻 辑运算认 为所有非零的参数都 表示 T R U E , 而参数 0 表示 F ALS E。它们返回 1 或者 o, 分别

表示结果为 T RU E 或者为 F ALSE。以下是一些表达式求值的示例。

可 以 观 察 到 , 按 位 运算 只 有 在特 殊 情 况 下 , 也 就 是 参 数 被 限 制 为 0 或者 1 时 , 才 和 与

其对应的逻辑运算 有相同的行为。

逻辑运算符 && 和 II 与它们对应的位级运算 & 和1 之间第二个重要的区别是,如果 对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此, 例如, 表达式 a &&S/ a 将不会造成被零除, 而表达式 p &&*p +叫 1 不会导致间接引用空指针。沁因 练习题 2. 14 假设 x 和 y 的 字节值 分别为 Ox 66 和 Ox 3 9 。 填写下表 , 指明各 个 C 表达

式的字节值。

表达式表达式 值
X & y X I yX && y X I y
~x I ~y!x 11 !y
X & ! yX && y

练习题 2. 15 只使 用位 级和逻 辑运 算, 编写 一个 C 表达式, 它 等价 于 x = = y 。换句话

说 , 当 x 和 y 相 等 时 它 将返 回 1, 否则 就 返 回 0。

2. 1. 9 C 语言中的移位运算

C 语言还提供了一组移位运算 , 向左或者向右移动位模式。对于一个 位表示为[ x w- 1 • Xw-Z• …, x。]的操作数 x , C 表达式 x<<k 会生成一个值, 其位表示为[ Xw- k- 1 , Xw- k- 2 , …,

X o , 0, … , O] 。也 就是说, x 向左移动 k 位, 丢弃最高的 K 位,并 在右端补 k 个 0。移位量应该是 一个 o w—1 之间的值。移位运算是从左至右可结合的, 所以 x << j << k 等价于

(x « j ) « k 。

有一个相应的右移运算 x >>k , 但是它的行为有点 微妙。一般而言, 机器支持两种形式的右移: 逻辑右移 和算术右移。逻辑右移在左端补 K 个 o, 得到的结果是[ O, …, o,

Xw- 1 • Xw- 2 • …, Xk] 。 算术右移是在左端补 K 个最高有效位的值, 得到的结果是[ x w- 1 • …, Xw- 1 , Xw- 1 , Xw-2, …, Xk ] 。 这种做法看 上去可能有点奇特, 但是我们会发现它对有符号整数数 据的运算非常有用。

让我们来看一个例子 ,下 面的 表给出 了对一个 8 位参数 x 的两个不同的值做不同的移位操作得到的结果:

操作
参数 x[01100011][10010101]
X << 4[00110000][OIO10000]
X >> 4 (逻辑右移)[0000011O][00001001]
X >> 4 ( 算术右移)[00000110][11111001]

斜体的数字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一个条目 之外, 其他的都包含填充 0。唯一的例外是算术右移[ 10010101] 的情况。因为操作数的最高位是 1, 填充的值就是 1。

C 语言标准并没有明确定义对千有符号数应该使用哪种类型的右-移

算术右移或者逻辑

右移都可以。不幸地, 这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。然而, 实际上, 几乎所有的编译器/机器组合都对有符号数使用算术 右移,且许多

程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。

与 C 相比, J ava 对 于 如何进行右移有明确的定义。表达是 x >>k 会将 x 算术右移 k 个位置,而 x >>>k 会对 x 做逻辑右移。

m 移动 k 位, 这里 k 很大

对于一 个由 w 位组成的数据类型,如 果 要 移动 k w 位会得 到什 么结果呢? 例如, 计算下 面的 表达式会得到什 么结 果,假 设 数 据 类型 i n t 为 w = 3 2 :

int lval = OxFEDCBA98«32; int aval = OxFEDCBA98»36; unsigned uval = OxFEDCBA98u»40;

C 语言标准很 小心地规避了说 明 在 这种情况下该如何做。在许多机 器上 , 当移动一个

w 位的值 时,移位 指令只考虑位 移量的低 log2w 位,因 此实际上位移量就是通过计 算 k mod

w 得到的。例如, 当 w = 32 时, 上面三个 移位运算分别是移动 0、4 和 8 位,得 到结果:

1 val OxFEDCBA98

a val OxFFEDCBA9

uval OxOOFEDCBA

不过这种行 为对于 C 程序来说是没有保证的, 所以应该保持位移量小于待移位值的位数。另一方面, J a va 特别要 求位移数量应 该按照我们前面所 讲的求模的方法来计 算。

m 与移位运算有关的操作符优先级问题

常常有人会写这样的表达式 1 <<2+3<<4 , 本意是 (1 « 2 ) + (3« 4 ) 。 但 是 在 C 语言中, 前面的 表达式等价于 1 << (2 +3 ) << 4, 这是由于加法(和减法)的优先级比移位运算要高。然后 ,按 照从 左至右 结合性规则 ,括 号应该是这样打的 ( l << (2+3)) <<4, 得到的结果是

512, 而不是 期望的 52 。

在 C 表达式中搞错优先级是一种常见的程序错误原因, 而且常常很难检查出 来。 所以.当你拿不准的时候,请加上括号!

沁§ 练习题 2. 16 填写下表,展示不同移位运算对单字节数的影响。思考移位运算的最好方式是使用二进制表示。将最初的值转换为二进制,执行移位运算,然后再转换回 十 六进 制。每个答案都 应该是 8 个二进制数 字或 者 2 个十 六进 制 数 字。

2. 2 整数表示

在本节中, 我们描述用位来编码整数的两种不同的方式: 一 种 只 能 表示非负数, 而另一 种能 够表示负数、零和正数。后面我们将会看到它们在数学属性和机器级实现方面密切相关。我们还会研究扩展或者收缩一个已编码整数以适应不同长度表示的效果。

图 2-8 列出了我们引入的数学术语,用 于 精确定义和描述计算机如何编码和操作整数。这些术语将在描述的过程中介绍,图在此处列出作为参考。

符号类型含义
B2wT函数二进制转补码
B2U,,,函数二进制转无符号数
U2B,,,函数无符号数转二进制
u2r:切函数无符号转补码
T2Bw函数补码转二进制
T2Uw TMin “’ TMawx UMawx t +W u +“ *t切 *wu I w u “'函数常数常数常数操作操作操作操作操作操作补码转无符号数最小补码值 最大补码值最大无符号数补码加法 无符号数加法补码乘法 无符号数乘法补码取反 无符号数取反

图 2-8 整数的数据与算术操作术语。下标 w 表示数据表示中的位数

2. 2. 1 整型数据类型

C 语言支持多种整型数 据类型 表示有限范围的整数。这些类型如图 2-9 和图 2- 10

所示, 其中还给出了 "典型” 32 位和 64 位机器的取值范围。每种类型都能用关键字来指定大小,这些关 键字包括 c h ar 、s h or t 、l o n g , 同时还可以指示被表示的数字是非负数

(声明为 u n s i g n e d ) , 或者可能是 负数(默认)。如图 2-3 所示,为 这些不同的大小分配的字节数根据程序编译为 32 位还是 64 位而有所不同。根据字节分配, 不同的大小所能表示的值的范圉是不同的。这里给出 来的唯一一个与机器相关的取值范围是大小指示符 l o n g 的。大多数 64 位机器使用 8 个字节的表示, 比 32 位机器上使用的 4 个字节的表示的取值范围大很多。

图 2-9 32 位 程 序上 C 语言整型数据类型的典型取值范围

C数据类型最小值最大值
[signed]char-12。8 -3276。8 -2 147 483 64。8 -9 223 372 036 854 775 80。8 -2 147 483 64。8 -9 223 372 036 854 775 80。8127
unsigned char255
short32 767
unsigned short65 535
int2 147 483 647
unsigned4 294 967 295
long9 223 372 036 854 775 807
unsigned long18 446 744 073 709 551 615
int32_t2 147 483 647
u i n 七32_ t4 294 967 295
i n七64_ t9 223 372 036 854 775 807
u i n 七64_ 七18 446 744 073 709 551 615

图 2- 10 64 位程序上 C 语言整型数据类型的典 型取值范围

图 2- 9 和图 2- 1 0 中一个很值得注意的特点是取值范围不是对称的一— 负数的范围比整数的范围大 1。当我们考虑如何表示负数的时候, 会看到为什 么会这样。

C 语言标准定义了每种数 据类型必须能够表示的最小的取值范闱。如图 2- 1 1 所示 ,它们的取值范围与图 2- 9 和图 2- 1 0 所示的典型实现一样 或者小一些。特别地,除 了 固定大小的数据类型是例外,我们看到它们只要求正数和负数的取值范围是对称的。此外,数据类 型 i nt 可以用 2 个字节的数字来实现, 而这几 乎回退到了 1 6 位机器的时代。还可以 看到, l ong 的大小可以用 4 个字节的数字来实 现, 对 32 位程序来说 这是很典型的。固定 大小的数据类型保证数值的范围与图 2- 9 给出的典型数值一致 , 包括负数与正数的不对称性。

C数据类型最小值最大值
[signed]char unsigned char- 12。7127 255
short unsigned short-3276。732 767 65 535
int unsigned一32 76。732 767 65 535
long unsigned long-2 147 483 64。72 147 483 647 4 294 967 295
i n 七3 2_ t uint32_t-2 147 483 64。82 147 483 647 4 294 967 295
i n七64_ t uint64_t-9 223 372 036 854 775 80。89 223 372 036 854 775 807 18 446 744 073 709 551 615

图 2- 11 C 语言的整型数据类型的保证的取值范围 。C 语言标准要求这些数据类型必须至少具有这样的取值范围

区 C、C++ 和 Java 中的有符号和无符号数

C 和 C++ 都支持有符号(默认)和无符号数。 J a v a 只支持有符号数 。

2.2. 2 无符号数的编码

假设有一个整数数据类型有 w 位。我们 可以将位向量写成 王, 表示整个向量, 或者写成[ X w- 1 • X w- 2 • …, x。J’ 表示向量中的每一位。把 I 看做一个二进制表示的数,就 获得

了;的无符号表示。在这个编码中, 每个位 X , 都取值为 0 或 1 , 后一种取值意味着数值2’ 应为数字值的一部分。我们用一个 函数 B2队 ( Binary to U nsigned 的缩写, 长度为 w ) 来表示:

原理:无符号数编码的定义

对向量 士=[立 - I’ 五 - 2’ …, Xo ] :

,,- 1

B 2U”’ G ) 辛 x , 2'

,-o

C2. 1)

在这个等式中, 符号“兰” 表示左边被定 义为等千右边。函数 BZU “’ 将一个长度为 w 的

0、1 串映射到非负整数。举一个示例,图 2-11 展示的是下面几种情况下 BZU 给出的从位向掀到整数的映射:

(2. 2)

在图中, 我们用长度为 2’ 的指向右侧箭头的 条表示每个位的位置1。每个位向量对应的数值 就等于所有值为 1 的位对应的条 23 8

的长度之和。

让我们来考虑一下 w 位所能表示的值的范围。最小值是用位向量[ 00…

O] 表示,也 就是整数值 o, 而最大值是

用位向量[ 11 …l ] 表示, 也就是整数 值

UMa x w = 江 =沪 - 1 。以 4 位情况

,- o

22 = 4 -

0 I 2 3 4 5 6 7 8 9 10 II 12 13 14 15 16

(0001)

(0101)

为例, UM a x4 = B 2U 八 [ 1111 ] ) = 24 - (1011]

1 = 1 5。因此, 函数 B2U心 能够被定义 (1111)

为一个映射 B 2Uw : { O, 1}w - { o, … 图 2-12 w= 4 的无符号数示例。当二进制表示

沪 — 1 } 。 中位 t 为 1, 数值就会相应加上 2'

无符号数的二进制表示有一个很重要的属性,也 就是每个介千 o ~ wz - 1 之间的数都

有唯一一个 w 位的值编码。例如, 十进制 值 11 作为无符号数,只 有一个 4 位的表示, 即

[ 1011] 。我们用数 学原理来重点 讲述它 ,先 表述原理再 解释。原理:无符号数编码的唯一性

函数 B2U u. 是一个双射 。

数学术语双射是指一个函数 J 有两面: 它将数值 x 映射为数值 y , 即 y = f(x), 但它也可以反向操作, 因为对每一个 y 而言, 都有唯一 一个数值 x 使得f 位 )= y 。这可以用反函数 1- 1 来表示, 在本例中, 即 x = 1- 1 ( y ) 。 函 数 B2U u. 将每一个长度为 w 的位向量都映

射为 0~ w2

—1 之间的一个唯一值; 反过来 , 我们称其为 U2B w( 即“无符号数到二进制”) '

在 0 ~ 沪 — 1 之间的每一个整数都 可以映射为一个唯一的长度为 w 的位模式。

2. 2. 3 补码编码

对于许多应用, 我们还希望表示负数值。最常见的有符号数的计算机表示方式就是补码( t wo ’ s-com plemen t ) 形式。在这个定义中, 将字的最高有效位解 释为负权 ( nega tive weight ) 。我们用函数 B2兀 ( Bina ry to T wo ’ s-com plement 的缩写,长度 为 w ) 来表示:

原理:补码编码的定义

对向量 :i:= [ 江 - 1 ’ 五 - 2’ … ,工。]:

正 2

B 2T wG ) 主 — 乓 -1 2匹 1 + x , 2'

;=o

(2. 3)

最高有效位 Xw-1 也 称 为 符号位, 它 的 "权 重 ” 为— zw-1 , 是无符号表示中权重的负

数。符号 位被设置为 1 时, 表 示值为负, 而 当 设 置为 0 时, 值为非负。这里来看一个示例, 图 2-13 展示的是下面几种情况下 B2T 给出的从位向量到整数的映射。

B2T4 ([0001]) = — 0 • 23 + 0 • 22 + 0 • 21 + 1 • 2° = 0 + 0 + 0 + 1 = 1

B2兀 c[ o1 01] ) =-o. 23 +1. 22 +o. 21 +1. 2° = 0+4+0+1 = 5

B2T4 ([1011]) = — 1 • 沪 + 0 • 22 + 1 • 21 + 1 • 2° = - 8 + 0 + 2 + 1 =- 5

B 2T 八[ 1111 ] ) = — 1 • 沪+ 1 • 22 + 1 • 21 + 1 • 2° = — 8 + 4 + 2 + 1 = — 1

(2. 4)

在这个图中,我们用向左指的条表示符号位具有负权重。于是,与一个位向量相关联的数值是由可能的向左指的条和向右指的条加起来决定的。

我们可以看 到, 图 2-12 和图 2-13 中的位模式都是一样的, 对 等 式 ( 2. 2) 和等式 ( 2. 4) 来说也 是一 样 , 但 是 当 最高有效 位是 1 时,数 值 是 不 同 的 , 这是因为在一种情况中,最高有效位的权重 是十8 , 而在另一种情况中,它的权重是一8 。

[0001]

[0101]

[1011]

[1111]

. 寄宅器嚣- 23 = -8

22 = 4 -

-8 -7 -6 -5 -4 -3 -2 -1 0 l 2 3 4 5 6 7 8

让 我们来 考 虑一下 w 位补码所能 图 2-13 w = 4 的补码示例。把位3 作为符号位, 因此当它

表示的值的范围。它能表示的最小值是 位向量[ 10…O] ( 也 就 是 设 置 这个位为负

为 1 时, 对数值的影响是一 沪=—8。 这 个 权 重

在图中用带向左箭头的条表示

权,但 是 清除 其 他 所 有 的 位 ),其 整数值为 TMin产三— zw-1 。 而最大值是位向量[ 01… 1]

ur-2

( 清除具有负权的位, 而设 置其他所有的位),其 整数值为 TMa x 心 == 2· = zur-1 - 1 。 以

,- o

长度为 4 为例,我们 有 TMin4 = B 2兀 ( [ 1000] ) = —穸= - 8 , 而 T Ma x4 =B2T4 ([0111]) =

沪 + 21+ 2°= 4+ 2+ 1 = 7 。

我们可以看出 B2兀 是一个从长度为 w 的位模式到 TMin 心 和 TMa x 切之 间 数 字 的 映射,写 作 B 2T w : {O, l}w —- { T M i nw, … , T Ma x w }。 同 无 符 号 表 示 一样, 在 可 表 示的取值范围内的每个数字都有一个唯一的 w 位的补码编码。这就导出了与无符号数相似的补码数原理:

原理:补码编码的唯一性

函数 B2兀 是 一个双射。

我们定义函数 T2B心(即 “补 码 到 二 进 制" )作为 B2兀 的反函数。也就是说,对 千每个数 x , 满足 TMinw女 T M a x 心,则 T 2B w( x ) 是 x 的(唯 一的)w 位模式。

练习题 2. 17 假设 w = 4 , 我们能给每个可能的十六进制数字赋予一个数值,假设用一个 无符 号或者补码表 示。 请根据这些表 示, 通过写 出 等式( 2. 1 ) 和等 式 ( 2. 3 ) 所 示的求和公 式 中的 2 的非零次幕, 填写下表:

X

十六进制 二进制

B2 U.( 又) B2T 4仅)

OxE [1110] 23+22+21=14 - 23+2’ + 2’=- 2

OxO OxS Ox8 OxD OxF

图 2-14 展示了针 对不同字长, 几个重要数字的位模式和数 值。前三个给出的是可表示的整数的范围,用 UMax w、TMin w 和 TMa x 心 来表示。在后面的讨论中, 我们还会经常引用到这三个特殊的值。如果 可以从上下文中推断出 w , 或者 w 不是讨论的主要内 容时,我们 会省略下 标 w , 直接引用 UMax 、TMin 和 TMa x 。

图 2-1 4 重要的数字。图中给出了数值和十六进制表示

关于这些数字, 有几点值得注意。第一, 从图 2-9 和图 2- 10 可以看到, 补码的范围是不对称的: I TMinl = I TM曰 + 1 , 也就是说, TMi n 没有与之对应的正数。正如我们将

会看到的,这导致了补码运算的某些特殊的属性,并且容易造成程序中细微的错误。之所 以会有这样的不对称性, 是因为一半的位模式(符号位设置为 1 的数)表示负数,而另 一半

(符号位设置为 0 的数)表示非负数。因为 0 是非负数, 也就意味着能表示的整数比负数少一个。第二, 最大的无符号数值刚好比补码的最大值的 两倍大一点: UMa工w = 2TM a工w +

l 。补 码表示中所有表示负数的位模式 在无符号表示中都变成了 正数。图 2-14 也 给出了 常

数— l 和 0 的表示。注意一1 和 UMa工 有同样的位表示一 - 个全 1 的串。数值 0 在两种表示方式中都是 全 0 的串。

C 语言标准并没有要求 要用补码形式来表示有符号整数, 但是几 乎所有的机器都是这

么做的。程序员如果希望代码具有最大可移植性,能够在所有可能的机器上运行,那么除 了图 2-11 所示的那些范围之外,我 们不应该假设任何可表示的数值范围,也 不应该假设有符号数会使用何种特殊的表示方式。另一方面,许多程序的书写都假设用补码来表示有 符号数, 并且具有图 2-9 和图 2- 10 所示的 "典型的” 取值范围, 这些程序也 能够在大量的机器和编译器上移植。C 库中的文件< l i mi t s . h >定义了一组常量, 来限定编译器运行的这台机器的 不同整型数据类型的取值范围。比如,它 定义了常量 IN T_ MAX、 I NT_ MIN 和

UINT_MAX, 它们描述了有符号和无符号整数的范围。对于一个补码的机器, 数据类型 i n t

有 w 位, 这些常量就对应 于 TMa工U 、 TMi nw 和 UMa工 的值。

m 关千确定大小的整数类型的更多内容

对于某些程序来说,用某个确定 大小的表示来编码数据类型非常重要 。例如,当编 写程序 , 使得机器能够按照一个标准协议在因特网上通信时,让数据类型与协议指定的数据类型兼容是 非常重要 的。我们前面看到了,某些 C 数据类型, 特别是 l ong 型,在不同的机器上有不同的取值范围,而实际上 C 语言标准只指定了每种数据 类型的 最小范围,而不是 确定的 范围。虽然 我们可以选择与大多数机器上的标准表示兼容的数据类型,但是这也不能保证可移植性。

我们已经见 过了 3 2 位和 64 位版本的确定 大小的整数类型(图2-3 ) , 它们是一个更大数据类型 类的 一部 分。ISO C99 标准在 文件 s t d i nt . h 中引入了这个整数类型类。 这个文件定 义了一组数据类型, 它们的声明 形如 i n t N_t 和 u i nt N_t , 对 不 同的 N 值 指 定

N 位有符号和无符号整数。N 的具体值与 实现 相 关 ,但 是 大 多 数 编译 器 允 许的值 为 8、

16、32 和 64。因此, 通过将它的 类型 声明 为 u i n t l 6_ t , 我们可以无歧 义地声明一个 16

位无符号 变量 , 而如 果 声明为 i n t 32 _ t , 就是一个 32 位有符号变量 。

这些数据类型对应着一组宏,定 义了每 个 N 的值对应的最小和最大值 。这些宏名字

形如 IN TN_MIN 、IN TN_ MAX 和 UI NTN_MAX 。

确定宽度类型的带格式打印需要使 用 宏,以 与 系统 相关的方式扩展 为 格 式 串。 因此, 举个例子来说 , 变量 x 和 y 的类型是 i nt3 2_t 和 ui nt 64_七, 可以通过调用 pr i n t f 来打印它们的值,如下所示:

printf(“x = %” PRid32 “, y = %” PRiu64 “\n”, x, y);

编译为 64 位程序时, 宏 PRi d 32 展 开成字符 串 " d “’ 宏 PRi u 64 则展 开成 两 个 字符 串"1” " u" 。 当 C 预处理器遇 到仅 用空格(或其他空白 宇符)分隔的一个字符 串常量序列 时, 就把 它们 串联 起 来。 因此,上 面的 pr i nt f 调用就变成 了 :

printf(“x = %d, y = %lu\n”, x, y);

使用宏能保证: 不论代码是如何被 编译的 ,都 能生成 正确的格式字符 串。

. 关于整数数据类型的取 值范围和表示,Java 标准是非常明确的。它要求采用补码表示,取值范围与图 2-10 中 64 位的情况一 样。在Java 中,单 字节数据类型称为 byt e , 而不是 char 。这些非常具体的要求都是为了保证无论在什么机器上运行, Ja va 程序都能表现地完全一样。

田 日 有符号数的 其他表示方法

有符号数还有两种标准的表示方法:

反码( Ones ’ Com plement ) : 除了最高有 效位的权是一 ( 2心- I —1 ) 而不是 — 2心- I ’ 它

和补码是一样的:

B 20 切(印 土— X ur–1 ( 2urI-

-1) +

ur-2

X 心

,-o

原 码(Sign-Magnitude) : 最高有效位是符号位,用来确定剩下的位应该取负权还是正权:

匹 2

B 2S卫 )辛( - l Yw–1 • ( x;t)

i - 0

这两种表 示方法都有一个奇怪的属性, 那就是 对于数 字 0 有两种 不 同的编码 方式。这两种表 示方法,把 [ 00…O] 都解释为 十0。 而值 —0 在原码中表 示为 [ 10 …O] , 在反码

中表示为[ 11…1] 。 虽 然过去生产过 基于反 码表示的机 器, 但 是 几乎所有的现代机 器都

使用补码 。 我们将看到在浮点数中有使 用原码编码。

请注意补码 ( T wo ’ s co m plem en t ) 和反码 ( O ne s ’ co m plem e nt ) 中撇 号的 位 置是 不 同的 。 术语补 码 来 源 于这样一 个情况, 对 于非 负数 X , 我 们 用 2"’ - x ( 这 里 只 有一 个 2 ) 未计算—x 的 w 位表示。术语反码 来源 于这样一 个属性 , 我 们用[ 11 1 …1 ] - x ( 这里有很 多个 1 ) 来 计 算 - x 的 反 码 表 示。

为了更好地理解补码表示,考虑下面的代码:

short x = 12345; short mx = -x;

show_bytes((byte_pointer) &:x, sizeof(short)); show_bytes ((byte_pointer) &:mx, sizeof (short));

当在大端法机器上运行时, 这 段 代 码 的 输 出 为 3 0 3 9 和 c f c7, 指 明 x 的 十六进制表示为 Ox 3 03 9 , 而 mx 的 十 六 进 制 表 示 为 Ox CFC7 。 将 它 们 展 开 为 二 进 制 , 我 们 得 到 x 的 位模 式 为[ 0011 000 00011 1 001 ] , 而 mx 的 位 模 式 为 [ 11 0011 11 11 0 001 11 ] 。 如 图 2-15 所 示 , 等式 ( 2. 3 ) 对 这两 个 位 模 式 生成 的 值 为 1 2 345 和 一1 2 345 。

1 116 384 -32 7681 116 384 32 768

图 2-1 5 12 345 和一1 2 345 的补码表示,以及 53 191 的无符号表示。注意后面两个数有相同的位表示

练 习题 2. 18 在 第 3 章 中, 我 们将看到由反汇编 器 生成的列表, 反 汇编 器 是 一种将可执 行 程序 文件 转换回可读 性更好的 ASCII 码形 式的程序。这些 文件包含 许 多 十 六进制数 字 , 都是用典型的补码形 式 来 表 示 这 些 值。 能 够 认 识这 些 数 字 并 理 解 它 们 的 意 义

(例如它们是正数还是负数),是一项重要的技巧。

在下面的 列 表 中, 对千标 号为 A ~ I ( 标记在右边)的那些行 , 将指令名( s ub 、mov

和 a d d ) 右边 显示的 ( 3 2 位补码形 式 表 示的)十六进制值 转换 为 等价的十进制值。

4004d0:4881eceO020000sub$0x2e0,%rspA .
4004d7:488b4424a8mov-Ox58(%rsp),%raxB.
4004dc:48034728addOx28(%rdi),%raxC.
4004e0:48894424dOmov%rax,-Ox30(%rsp)D .
4004e5:48 8b442478movOx78(%rsp),%raxE.
4004ea:48 898788000000mov%rax,Ox88 (ir 儿 di )F .
4004f1: 4004f8:48 8b 008424f80100movOx1f8 (%rsp) , %raxG .
4004f9:48 03442408addOx8(%rsp),%rax
4004fe:48 898424cO0000mov%rax,Oxc0(%rsp)H.
400505:00

400506: 48 8b 44 d4 b8 mov -Ox48(%rsp, ir 人

dx , 8 ) , %r ax I .

  1. 2. 4 有符号数和无符号数之间的转换

    C 语言允许在各种不同的数字数据类型之间做强 制类型转换。例如, 假设变量 x 声明为i nt , u 声明为 u n s i g n e d 。表达式 (u n s i g n e d ) x 会将 x 的值转换成一个无 符号数值,而(int) u 将 u 的值转换成一个有符号整数。将有符号数强制类型转换成无符号数, 或者反过来,会得到什么结果呢?从数学的角度来说,可以想象到几种不同的规则。很明显,对 于在两 种形式中都能 表示的值, 我们是想要保 持不变的 。另 一方面,将负 数转换成无符号数可能 会得到 0。如果 转换的无符号数太大以至于超出了补码能够表示的范围, 可能会得到 T Ma 工。 不过, 对千大多数 C 语言的实现来说, 对这个问 题的回答都是从位级角度来看的, 而不是数的 角度。

    比如说, 考虑下面的代码:

    1. short int v = - 12345 ·

    2. unsigned short uv = (unsigned short) v;

      3 printf(“v = %d, uv = %u\n”, v, uv);

在一台采用补码的机器上,上述代码会产生如下输出:

V = -12345, UV= 53191

我们看到 , 强制类型转换的结果保持位值不变,只是 改变了解释这些位的方式。在图 2-1 5 中我们看 到过 ,一 1 2 3 45 的 16 位补码表示与 53 1 91 的 1 6 位无符号表示是完全一样的。将s ho 江 强制类型转换为 u n s i g ne d s h or t 改变数值, 但是不改变位表示。

类似地,考虑下面的代码:

unsigned u = 4294967295u; I* UMax *I

  1. int tu= (int) u;

  2. printf(“u = %u, tu= %d\n”, u, tu);

    在一台采用补码的机器上,上述代码会产生如下输出:

    u = 4294967295, tu= -1

从图 2-1 4 我们可以看到 , 对于 3 2 位字长来 说, 无符号形式的 4 294 967 295 ( UM a 工32) 和补码形 式的- 1 的位模式是完全一样的。将 u n s i g n e d 强制类型转换 成 i n t , 底层的位表示保持不变。

对于大多数C 语言的实现,处理同样字长的有符号数 和无符号数之间相互转换的一般规则是: 数值可能会改 变,但是位模式不 变。让我们用 更数学化的形式来描述这个规则。我们定义函数 U 2Bw 和 T2 B,,_,, 它们将数值映射为无符号数和补码形式的位表示。也就是说,给

定 O¾ x ¾ UM a 工心范围内的一个整数 工, 函数 U 2B心( x ) 会给出 工的唯一的 w 位无符号表示。相似地,当 工满足 T M in心< 年;; TM釭心, 函数 尥凡 m 会给出工的 唯_的 w 位补码表示。

现在, 将函数 T 2U 切 定义为 T 2U心丘 )土B 2U w( T 2 B 心 ( x ) ) 。 这个函数的输入是一个

TMinw T M a x 立的数,结 果得到一个 o UMa x 心 的 值, 这里两个数有相同的位模式, 除了参数是无符号的, 而结果是以补码表示的。类似地, 对于 o UMax切 之间的值 x , 定义函数 U2兀 为 UZT u. 位) 主B2兀 (U2B w位))。生一成个数的无符号表示和 工的补 码表示相同。

继续我们前 面的例子,从 图 2-15 中, 我 们看到 T 2U,6 ( — 1 2 345) =53 191, 并且

U2T 16 (53 1 91) = — 1 2 345 。也就是说, 十六进制表示写作 Ox CFC 7 的 16 位位模式既是

—1 2 345的补码表示, 又是 53 191 的无 符号表示。同时请注意 12 345 + 53 191 = 65 536 = 沪 。这个属性可以推广到 给定位模式的两个数值(补码和无符号数)之间的关系。类似地, 从图 2-1 4 我们看到 T 2U 32( —1 ) =4 294 967 295, 并且 U2T 32 (4 294 967 295) = —1。也就是

说,无 符号表示中的 UMa工 有着和补码表示的—1 相同的位模式。我们在这两个数之间也能看到这种关系: l + UMax w = 2三

接下来,我们 看到函数 U2T 描述了从无符号数到补码的转换, 而 T ZU 描述的是 补码到无符号的转换。这两个 函数描述了在大多 数 C 语言实现中这两种数据类型之间的强制类型转换效果。

练 习 题 2. 19 利用你解答练 习题 2. 17 时填写的表格, 填 写下列描述函数 T2队 的表格。

#

通过上述这些例子, 我们可以看到给定位 模式的补码与无符号数之间的关系可以表示为函数 T2U 的一个属性:

原理: 补码转换为无 符号数

对 满足 TMi n,,_. x T M a x心 的 x 有:

T 2仄 ( x ) = {

x + 2"’ , X < 0

x , x O

(2. 5)

比如, 我们看到 T 2U1 s C—12 34 5 ) = —12 345 + 216 =53 1 91 , 同时 T 2U心( - 1) = - l +

沪 = U M a x wo

该 属性可以通过比较公 式( 2. 1 ) 和公式( 2. 3 ) 推导出来。推导: 补码转换为无符号数

比较等式( 2. 1) 和等式( 2. 3) , 我们可以发现对于位模式 x, 如果我们计 算 B2U w Cx) -

B2T 切( 王)之差,从 0 到 w—2 的位的加权和将互相抵消掉,剩下一个值: B 2从(王)- B 2兀(动=

平 一,I c z-w

I - ( - z-w

I ) ) = x ,,_.-1 沪 。 这就得到一个关系: B 2U心(x ) = 立 -I 沪 + B 2Tw Cx) 。我

们因此就有

B2U w ( T 2B w C x ) ) = T 2Uw ( x ) = x + x w-1- 沪 ( 2. 6)

根据公式( 2. 5) 的两种情 况,在 x 的补码表示中 , 位 X u,- 1 决定了 x 是否为负。

比如说, 图 2-1 6 比较了当 w = 4 时函数 B2U 和 B 2T 是如何将数值变成位模式的。对补码来说 , 最高有效位是符号位, 我们用带向左箭头的条来表示。对千无符号数来说, 最高有效位是正权重,我 们用带向右的箭头的条来表示。从补码变为无符号数, 最高有效位

的权重从- 8 变为十8 。因 此 ,补 码表示的负数如果看成无符号数,值 会 增 加 24 = 1 6。因而, - 5 变成了十11 , 而—1 变成了十15 。

绣嚣震璧翌黑殴醮-l = -s

2 =8

22 = 4 -

- 8 - 7 -6 -5 -4 - 3 - 2 -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

[! Oll ]

[ l lll ]

图 2-1 6 比较当 w = 4 时无符号 表示和补码表示(对补码和无符号数来说,

最高有效位的权重分别是 一8 和 十8’ 因而产生一个差为 16 )

图 2-1 7 说明 了函数 T 2U 的一般行为。如图所示,当 将 一 个 有 符 号 数 映 射 为它相应的无符号数时,负数就被转换成了大的正数,而非负数会保待不变。

练习题 2. 20 请说明等式(2. 5) 是如何应用 到解答练 习题 2. 19 时生成的表格 中的各项 的。反过来看, 我们希望推导出一个无符号数 u 和与之对应的有符号数 U2Tw( u) 之间的关 系: 原 理: 无符 号数转换为补 码

对 满足 0冬u UMa x ,., 的 u 有:

.该原 理证明如下:

uz 兀 ( u ) = { u,

u — 沪 ,

三 TMa x w u > TMaxw

( 2. 7)

推导:无 符 号 数 转换为补码

设 u = U2BwCu) , 这个位向量也是U2兀 ( u) 的补码表示 。公式(2. 1) 和公式(2. 3) 结合起来有

U2Tw(u) = — U ur- 1 沪 十 u (2. 8)

在 u 的无符 号 表示中, 对公 式 ( 2. 7 ) 的两 种情况来说, 位 u印一 ]决定 了 u 是 否 大 于

TMa工切 = 2-w

l -10 ■

图 2-18 说明了函数 U 2T 的行 为。对于小的数( T M a 工w)’ 从 无 符 号 到 有 符 号 的 转 换将保留数字的原 值。对于大的数( > TMa工切) ,数 字 将被转换为一个负数值。

2w 2 w

无符号数, - · 斗尸►

+2 w–1 T

. t 2 wI- 无符号数

- 2 w-1 .l ’l _2 w-l

图 2-1 7 从补码到无符号数的转换。函数 图 2-18 从无符号数到补码的转换。函数 U2T T 2U 将负数转换 为大的正数 把大千 zw一l —1 的 数 字 转 换 为 负 值

总结一下, 我们考 虑无符号与补码表示之间互 相转换的结果。对于在范圉 O x 冬T M a x w 之 内 的 值 x 而 言 , 我们得到 T 2U 心 ( x ) = x 和 U 2兀 ( x ) = x 。也就是说,在 这个范即内的数字有相同的无符号和补码表示。对于这个范围以外的数值,转换需要加上或者减 去 zw。例如, 我们有 T 2U w ( - l ) =-1 + 2w = UM a x 心 最 靠 近 0 的负数映射为最 大的无符号数。在另 一个 极 端, 我们可以看到 T 2队 ( T M in w ) = — 2正 1 + 2 三 = 2 正 1 = T M a xw +

1 — 最 小 的 负 数 映 射为一个刚好 在补码的正数范围之外的无符号数。使用图 2-1 5 的示例 , 我们能看到 T 2U 16 ( — 1 2 345) = 65 563+ —12 345=53 191。

2. 2. 5 C 语言 中的 有符号 数与无符号数

如图 2-9 和图 2- 10 所示, C 语 言 支 持 所 有 整型数据类型的有符号和无符号运算。尽管

C 语言标准没有指定 有符号数要 采用某种表示, 但 是 几 乎 所 有 的 机 器 都 使 用 补 码。通常, 大 多 数 数 字 都 默 认 为是有符号的。例如,当 声 明 一 个 像 1 2 3 45 或者 Ox 1 A2B 这样的常星时, 这 个 值 就 被 认 为是有符号的。要创建一个无符号常量,必 须 加 上 后 缀 字 符 ’ u’ 或者 ’ u ’’ 例 如 , 1 2 3 45 U 或 者 Ox 1 A2Bu 。

C 语言允 许无符号数 和有符号数之间 的转换。虽 然 C 标 准没有精确规定应如何进行这种转换, 但 大多数系统遵循的原则是底层的位表示保持不变。因此,在 一 台 采用补码的机器上, 当从 无 符 号 数转换为有符号数时,效 果 就 是 应 用 函 数 U ZT w , 而从有符号数转换为无符号数时,就 是 应 用 函 数 T ZU w , 其 中 w 表示数据类型的位数。

显式的强制类型转换就会导致转换发生,就像下面的代码:

int tx, ty; unsigned ux, uy;

tx "" (int) ux; uy"" (unsigned) t y;

另外, 当一 种类 型 的表 达式被赋值给另外一种类型的变量时,转 换 是 隐 式 发 生 的 ,就像下面的代码:

int tx, ty; unsigned ux, uy;

tx = ux; I* Cast to signed *I uy = ty; I* Cast to unsigned *I

当用 pr i nt f 输出数值时,分 别 用 指示符%d 、%u 和 %x 以 有 符 号 十进制、无符号十进制和十六进制格式输出一个数字。注意 pr i n 七f 没 有 使 用 任 何 类 型 信 息 ,所 以 它 可 以 用 指 示符%u 来 输 出 类 型 为 1 平 的数值,也 可 以 用 指 示符%d 输 出 类 型 为 un s i g ne d 的数值。例如, 考虑下面的代码:

int X = -1;

unsigned u = 2147483648; I* 2 to the 31st *I

printf(“x = %u = %d\n”, x, x); printf(“u = %u = %d \ n " , u, u);

当 在 一 个 3 2 位机器上运行时,它 的 输 出 如 下 :

X = 4294967295 = -1

u = 2147483648 = -2147483648

笫 2 章 信息的表示和处理 53

在这两种情 况下, pr i n t f 首先 将这个字当作一个无符号数输出,然 后 把 它 当 作 一 个 有符号数输出。以下是实际运行中的转换函数: T2U32 (-1) =UMax3 z = 23 2 - 1 和 U 2 T32 (231) = 沪 —232 = - 231 = TMin32o

由千 C 语言对同时包含有符号和无符号数表达式的这种处理方式,出 现 了 一 些 奇特的行为。当 执行一个运算时, 如果它的一个运算数是有符号的而另一个是无符号的,那 么 C 语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执 行这个运算。就像我们将要看到的,这种方法对于标准的算术运算来说并无多大差异,但 是对于像< 和> 这样的关系运算符来说, 它 会 导 致非直观的结果。图 2- 1 9 展示了一些关系表达式 的示例以及它们得到的求值结果,这 里假设数据类型 i n t 表示为 3 2 位补码。考

虑比较式- l < OU 。 因 为第二个运算数是无符号的,第 一 个运算数就会被隐式地转换为无符号数,因 此表达式就等价于 4 2 9 4 96 72 95 0 <0 0 ( 回想 T 2U ,,_,( — l ) = U M a x 心),这 个 答 案显然是错的。其他那些示例也可以通过相似的分析来理解。

图 2-19 C 语言的升级规则的效果

注: 非直观的情况标注了` *' 。 当一个运算 数是无符号的时候 ,另 一个运算数也被隐式投制转换为无符号 。

将 TMin32 写 为 - 21 474 83647- 1 的 原 因请 参 见 网 络 旁 注 DATA:T MIN。

练习题 2 . 21 假设在采用 补码运算的 3 2 位机器上对这些表达式求值, 按照 图 2- 1 9 的格式填写下表,描述强制类型转换和关系运算的结果。

&JllEmlill C 语言中 TMin 的 写法

在图 2- 1 9 和练习题 2 . 21 中, 我们很 小心地将 T M i n 32 写成- 2 1 7 4 8 3 6 4 7 - 1 。 为什 么

不 简单地 写成 - 2 1 47 4 8 3 6 48 或者 Ox 8 0 0 0 0 0 0 0 ? 看一下 C 头 文 件 l i mi t s . h , 注意到它们使用 了跟 我们写 T M i n 32 和 T M a x 32 类似的方法:

I* Minimum and maximum values a’signed int’can hold. *I

#define INT_MAX 2147483647

#define INT_MIN (-INT_MAX - 1)

不幸的是, 补码表示的 不对称性和 C 语言的转换规则之间奇怪的交互 , 迫使 我们用

这种不寻常的 方式 来写 T M i n 32 。 虽然 理解这 个问 题需要 我们钻研 C 语言标准的 一些比较隐晦的 角落, 但是它能 够帮助我 们充分领会 整数数据类型和表 示的 一些细微之处。

2. 2. 6 扩展—个数字的位表示

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

要将一个无符号数转换为一个更大 的数据类型, 我们只 要简单地在表示的开头添加 0。这种运算 被称为零扩展 ( zero e x t e ns io n ) , 表示原理如下:

原理:无符号数的零扩展

定义宽 度为 w 的位向量 U= [ u w- 1 , Uw- Z, … , u。] 和宽度为 w ’ 的位向 量 矿 = [ O , 0 , Uw- 1 , Uw- 2 , … , u。J’ 其中 w ’ > w 。则 B ZU w ( u ) = B ZU w’ ( 矿)。

按照公式( 2. 1), 该原理 可以看作是 直接遵循了无符号数编码的定 义。

要将一个补码数字转换为一个更大的数据类型, 可以执行一个符号扩展 ( s ig n exten­

sion), 在表示中添加最高有效 位的值, 表示为如 下原理。我们用蓝色标出符号位 五- i’来突出它在符号扩展中的角色。

原理:补码数的符号扩展

定义宽度为 w 的位向量 王=[五 - I • Xw- Z • … , X。]和宽度为 w 的位向量 x ’ = [ x ,,.- 1 ,

江 -· I ’ 工三 , 江 - 2 ’ … , x 。J’ 其 中 w’ > w 。 则 B Z兀 (x ) = B ZT w,(了 )。例如,考虑下面的代码:

short sx = -12345; I* -12345 *I

2 unsigned short usx = sx; I* 53191 *I

3 int x = sx; I* -12345 *I

4 unsigned ux = usx; I* 53191 *I

6 printf(“sx = %d:\t”, sx);

7 show_bytes((byte_pointer) &sx, sizeof(short));

8 printf(“usx = %u:\t”, usx);

9 show_bytes((byte_pointer) &usx, sizeof(unsigned short));

10 printf(“x = %d:\t”, x);

11 show_bytes((byte_pointer) &x, sizeof(int));

12 printf(“ux = %u:\t”, ux);

13 show_bytes((byte_pointer) &ux, sizeof(unsigned));

在采用补码表示的 3 2 位大端法机器上运 行这段代码时, 打印出如下输出:

sx = -12345: cf c7 usx = 53191: cf c7

X = -12345: ff ff cf c7

ux = 53191: 00 00 cf c7

我们看到, 尽管—12 345 的补码表示和 53 191 的无符号表示在 16 位字长时是相同的, 但是

在 32 位字长时却是不同的。特别地, - 12 345 的 十六进制表示为 Ox FFFF CFC7 , 而 53 191 的十六进制表示为Ox 0000 CFC7。前者使用的是符号扩展- 最开头加了 16 位, 都是最高有效位1 , 表示为十六进制就是Ox FFFF 。 后者开头使用16 个 0 来扩展, 表示为十六进制就是 Ox OOOO。

图 2- 20 给出了从字长 w = 3 到 w = 4 的符号扩展的结果。位向量[ 1 01 ] 表示值- 4 + 1 =

- 3。对它应用符 号扩展, 得到位向量[ 1101] , 表示的值—8 + 4+ 1 = —3。我们可以看到, 对于 w= 4, 最高两位的组合值是 - 8+ 4 = - 4 , 与 w = 3 时符号位的值相同。类似地, 位向量[ 111] 和[ 1111] 都表示值- 1。

[101)

[I IOI]

-23 =-8

-2 =-4

22=4-

21=2 lllt

2°=.1

-8 -7 -6 -5 -4 -3 -2 -I O I 2 3 4 5 6 7 8

图 2-20 从 w= 3 到 w= 4 的 符 号 扩展示例。对于 w= 4, 最高两位组合权重为- 8+ 4= - 4, 与 w= 3 时 的 符号 位的权重一样

有了这个直觉,我们现在可以展示保持补码值的符号扩展。推导:补码数值的符号扩展

令 w’ = w + k , 我们想要证明的是

B 2T 叶 k ( [ 工u气 ,…,工匹-1’ 工匹-I , Xw-z , … ,Xo ] ) = B 2T w ( [ 工u- 1 , Xw-z , … ,工。])

k次

下面的证明是对 K 进行归 纳。也就是说,如果 我们能够证明符号扩展一位保持了数值不变,那么符号扩展任意位都能保待这种属性。因此,证明的任务就变为了:

B2T w+I([ 工正一 I , Xu- I , X匹-2’ … , X。] ) = B 2T .,.( [ .r …气 ,工一U

用等式( 2. 3) 展开左边的表达式, 得到:

2’ … ,工。])

B 2T叶 l ([ 乓 - I , X,一,_

w—1

I’ 乓 - 2’ … ,工。])—=立-1 沪 + x i 2'

, = O

w—2

= - x .,,_- 1 沪 十 五 4 尸 + xi ;2

, = O

w-2

= - Xu - ] ( 2W — z- u J ) + x.2'

,=O

" - 乓 _1 2-”

w-2

1 + x , 2'

,= O

= B 2兀 ( [ :r 一u I , Xw–2 • … ,工。]) .

我们使用的关键属性是 zw- zw-1 = zw- 1 。 因此,加上 一个权值为—沪 的位, 和将一个权值为

- z-w 1的 位转换为一个 权值为 zw-1 的 位, 这两项运算的综合效果就会保持原始的数值 。练习题 2. 22 通过 应用 等式( 2. 3), 表明下 面每个位 向量都是 —5 的补 码表 示。A. [1011]

B. [11011]

C. [111011]

可以看到第二个和第三个位向量可以通过对第一个位向量做符号扩展得到。

值得一提的是,从一个数据大小到另一个数据大小的转换,以及无符号和有符号数字之间的转换的相对顺序能够影响一个程序的行为。考虑下面的代码:

short sx = -12345; unsigned uy = sx;

I* -12345 *I

I* Mystery! *I

printf (“uy = i 儿 u : \ t " , uy); show_bytes((byte_pointer) &uy, sizeof(unsigned));

在一台大端法机器上,这部分代码产生如下输出:

uy = 4294954951: ff ff cf c7

这表明当把 s hor t 转换成 u ns i g ne d 时, 我们先要改变大小,之 后 再完 成 从 有符号到无符 号 的 转 换。也 就 是 说 ( u n s i g n e d ) s x 等 价 于 ( u n s i g n e d ) (int) sx, 求值得到4 294 954 951, 而不等价于 (u n s i g n e d ) (unsigned short) sx, 后者求值得到 53 1 91 。事实上, 这个 规则 是 C 语 言标 准要 求 的 。

练习题 2. 23 考虑下面的 C 函数:

int fun1(unsigned word) {

return (int) ((word«24)»24);

int fun2(unsigned word) {

return ((int) word«24)»24;

假设在 一个 采用 补码运算的 机器上以 3 2 位程 序来执行这些 函 数。还假设有符号数值的右移是算术右 移 , 而 无符 号数值的右移是逻辑右移。

  1. 填写 下表, 说明这些 函数对几个示 例参数的 结果。你会发现用 十 六进制 表 示来 做会更方便, 只要记住十 六进 制数 字 8 到 F 的最高有效位等于 1。
  2. 用语言来描述这些函数执行的有用的计算。

2. 2. 7 截断数字

假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。例如下面代码中这种情况:

int X = 53191;

short sx = (short) x; int y = sx;

I* -12345 *I

/* 一1 2345 *I

当 我们把 x 强制类型转换为 s hor t 时, 我们就将 32 位的 i nt 截断为了 16 位的 s hor t i nt 。

就像前面所看到的 ,这 个 16 位的位模式就是—12 345 的补码表示。当我们把它强制类型

转换回 扛江时, 符号扩展把高 16 位设置为 1, 从而生成—1 2 345 的 32 位补码表示 。

当将一个 w 位的数 x = [ x w- 1 • Xw-2• … , X。J截断为一个 K 位数字时, 我们会丢弃高w- k 位,得 到一个位向量 X1 = [Xk-1 , Xk-2, …, x。]。截断一个数字可能会改变它的值 溢出的一种形式。对千一个无符号数,我们可以很容易得出其数值结果。

原理:截断无符号数

令 I 等于位向 量[ x ..,- 1’ 立 - 2’ … , x 。J’ 而 了是将其截断为 K 位的结果:了 = [ Xk- 1 •

石 -2 ’ … , Xo ] 。 令 x = BZU 心(印 , x ’ = B ZU* G ’ ) 。 则 x ’ = x mod Z* o

该原理背后的直觉 就是所有被截去的位其权重形式 都为 2’’ 其中 i k , 因此,每一个权在取模操作下结果都为零。可用如下推导表示:

推导:截断无符号数

通过对等式 ( 2. 1) 应用取模 运算就 可以看到:

w–1

B 2队 ( [ x 正 I’ 工 w–2’ …心 )mod 2k = [ x ;2; ] mod 2k

, =O

=[江,;2 ]mod 2k

; = o

k 一 ]

= x ; 2'

,=O

= B 2队 ( [ x1,1-

, X1,2-

, … , X。])

在这段推导 中,我 们利用了属性: 对于任何 彦 k , 2’mod 2k = 0。 补码截断也具有相似的属性,只不过要将最高位转换为符号位: 原理:截断补码数值

令; 等于位向 量[ x w-1 • 立 -2 ’ …,工。], 而 了是将其截 断为 K 位的结果: X1 =[xk-1,

Xk-‘2, …, x。]。令 x = B2仄 (x ) , x ’ = B 2兀(了)。则x ’ = U 2兀 ( x mod 2k) 。

在这个公式中, x mod 2k 将是 0 到 2k —1 之间的一个数。对其应用函数 U2兀 产生的

效果是把最高有效 位 Xk -1 的 权重从 zk- 1 转变为— z- k 1 。 举例来看, 将数值 x = 53 191 从

i nt 转换为 s hor t 。由 千 216 = 65 536 :r’ 我们有 x mod 216 = x 。 但是, 当我们把这个数转换为 16 位的补码时, 我们得到 x ’ = 53 191 - 65 536=-12 345。

推导:截断补码数值

使用与无符号数截断相同的参数,则有

B2U w( [ x 匹 I ,Xw-2• … ,X。] ) mod 2k = B 2队 [ x1,-1 , X1,-2 , … , X。]

也就是, x mo d 沪能 够被一个位级表示为[ x k- 1 , Xk-2• …, x。]的 无符号数表示。将 其转换为补码 数则有 x ’ = U 2兀 ( x mod 2k ) 。

总而言之,无符号数的截断结果是:

B2Uk[x1,-1 ,x1,-2, … ,X。] = B 2U w( [ x U-, I , x 正 2 ’ … , X。]) mod 2k (2. 9)

而补码数字的截断结果是:

B2Tk [ x k-1 心 -2 ’ … ,X。]= U 2兀 ( B 2U w( [ x 一u ,,xu一. 2’ … ,X。]) mod 2勹 ( 2. 10)

练习题 2. 24 假设 将一个 4 位数值(用十六进 制数 字 O ~ F 表 示)截断到 一个 3 位数值

(用十六进制 数 字 0 ~ 7 表 示)。填写 下表 , 根据那些位 模 式的 无符 号和补码 解释, 说明这种截断对某些情况的结果。

原始值 截断值 原始值 原始值

解释如何将等 式 ( 2. 9) 和等 式 ( 2. 1 0 ) 应用 到这些 示例 上。

  1. 2. 8 关千有符号数与无符号数的建议

    就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的, 程序员经常忽视了它的影响。

    下面两个练习题说明了某些由于隐式强制类型转换和无符号数据类型造成的细微的错误。

    练习题 2. 25 考虑下 列代 码 , 这段代码试图计算数组 a 中所有元素的和, 其中 元 素的数量由参数 l e n g t h 给出。

    I* WARNING: This is buggy code *I

    float surn_elements (float a[] , unsigned length) { inti;

    float result= O;

for (i = O; i <= length-1; i++) result+= a[i];

return result;

当参数 l e n g t h 等于 0 时, 运行这段代码 应 该返回 0. 0 。但 实际 上, 运行时会遇到一个内存错误。请解释为什么会发生这样的情况,并且说明如何修改代码。

练习题 2. 26 现在给你一个任务, 写 一个函数用 来判定一个字 符 串 是否 比 另 一个更长。 前提是你要用 字符 串 库函数 s t r l e n , 它的 声明 如下:

f* Prototype for library function strlen *I size_t strlen(const char *s);

最开始你写的函数是这样的:

I* Determine whether strings is longer than string t *I I* WARNING: This function is buggy *I

int strlonger(char *s, char *t) { return strlen(s) - strlen(t) > O;

当你在一些示例数据上测试这个函数时,一切似乎都是正确的。进一步研究发现 在头文件 s t d i o . h 中 数据类型 s i z e _ t 是定义成 u n s i g n e d i n t 的。

  1. 在什么情况下,这个函数会产生不正确的结果?
    1. 解释为什么会出现这样不正确的结果。
    2. 说明如何修改这段代码好让它能可靠地工作。

m 函数 g e t p e er n ame 的 安全漏 洞

2002 年 , 从 事 F re eBSD 开源操 作 系统 项 目 的 程 序 员 意 识 到, 他 们对 ge t pe er na me

函数的实现 存 在 安 全 漏洞。代码的 简 化 版 本 如 下:

/*

  1. * Illustration of code vulnerability similar to that found in

  2. * FreeBSD’s implementation of getpeername 0

  3. *I

    5

  4. I* Declaration of library function memcpy *I

  5. void *memcpy(void *dest, void *src, size_t n);

    8

  6. I* Kernel memory region holding user-accessible data *I

  7. #define KSIZE 1024

  8. char kbuf [KSI ZE] ; 12

    13 / * Copy at most maxlen bytes from kernel region to user buffer *I

    14 int copy_from_kernel(void *user_dest, int maxlen) {

    1s I* Byte count len is minimum of buffer size and maxlen *I

  9. int len = KSIZE < maxlen? KSIZE: maxlen;

  10. memcpy(user_dest, kbuf, len);

  11. return len;

    19 }

    在这段代码里, 第 7 行给 出的 是 库 函 数 me mc p y 的 原 型, 这 个函数是要将一段 指 定长度 为 n 的 宇 节从 内 存 的 一 个 区域复制到 另 一 个 区域 。

    从 笫 14 行 开始的函数 c op y_ fr om_ ker ne l 是 要 将 一些操作 系统 内核 维护的数据复制到指定的 用 户可以访问的内存区域。 对用 户来说 , 大多数 内核 维护的数据结构应该是不可读的,因为这些数据结构可能包含其他用户和系统上运行的其他作业的敏感信息, 但是显示为 kb u f 的 区域 是 用 户可 以 读 的 。 参 数 ma x l e n 给 出的 是 分 配 给 用 户的 缓 冲 区的长度 , 这 个缓冲区是 用参数 u s er _d e s t 指 示的。 然后 , 第 1 6 行 的 计算确保 复制的 字节数 据 不会超 出 源或 者 目标缓 冲区可用的 范围。

    不过 ,假 设 有 些怀有恶意的 程 序 员 在 调 用 c o p y_ fr om_ ker n e l 的 代 码 中 对 ma x l e n 使 用 了 负数 值 , 那么, 第 1 6 行 的 最 小值 计 算会把 这 个值赋给 l e n , 然后 l e n 会 作 为 参数 n 被 传 递给 me mc p y 。 不过, 请 注意参数 n 是被 声明 为数 据 类型 s i ze _ t 的。这个数据类型是在库文件 s t d i o . h 中(通过 t yp e d e f ) 被 声 明 的 。 典 型地, 对 3 2 位 程序 它被 定 义

    为 u ns i g n e d int, 对64 位程序定义为 u n s i g ne d l o n g。 既 然参数 n 是 无符号的 , 那 么

    me mc p y 会 把 它当作 一 个非常大的 正整数 , 并 且 试 图将这样 多 字 节的 数 据 从 内核 区域 复制到用 户的缓 冲区。 虽 然复制这 么 多 字节( 至 少 沪 个)实 际 上 不 会 完成 , 因 为 程 序 会 遇到进程中非法地址的错误,但是程序还是能读到它没有被授权的内核内存区域。

    我们可以看到,这个问题是由于数据类型的不匹配造成的:在一个地方,长度参数 是有符号数; 而另一 个地 方, 它又是无符号数。正如这个例子表明的 那样 , 这 样 的 不 匹配会成为缺 陷的原 因, 甚 至 会 导 致 安 全 漏洞。 幸运 的 是 , 还 没 有 案 例 报 告 有 程 序 员 在F reeBSD 上利 用 了 这 个漏洞。他 们发布 了 一 个安全 建议, " F reeBS D-S A- 0 2 : 38. sig ned ­

    error”, 建议系统 管理 员如 何 应 用补 丁 消除 这 个 漏洞。要 修 正这 个缺陷, 只 要 将 c o p y_ fr o m_ ker n e l 的 参 数 ma x l e n 声 明 为 类型 s i ze _ t , 也就是与 me mc p y 的 参 数 n 一 致 。 同时, 我们也应该将本地 变量 l e n 和返 回值 声 明 为 s i z e _ t 。

我们已经看到了许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转 换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。实际 上,除 了 C 以外 很 少 有语 言 支持无符号整数。很明显, 这些语言的设计者认为它们带来的麻烦要比益处多得多。比如, J a va 只支待有符号整数,并 且 要 求 以 补 码 运 算 来 实 现。正常的右移运算符>>被定义为执行算术右移。特殊的运算符>>>被指定为执行逻辑右移。

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

2. 3 整数运算

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

2. 3. 1 无符号加法

考虑两个非负整数 x 和 y , 满足 0冬X , y< Zw。每个数都能 表示为 w 位无符号数字。然而, 如果计算 它们的 和 , 我们就有一个可能的范围 O x + y Z叶 t_ z。表 示 这 个 和可能需要 w + l

位。例如,图 2-21 展示了 当 x 和 y 有 4 位表示时,函数 x + y 的 坐标图。参数(显示在水平轴上)取值范围为 0~ 15, 但是和的取值范围为 0~ 30。函数的形状是一个有坡度的平面(在两个维度上,函数都是线 性的)。如果保持和为一个 w+ l 位的数字,并 且把它加上另外一个数值,我 们可能需要 w+ 2 个位 ,以 此类推。这种持续的 ”字 长膨胀” 意 味 着 , 要想完整地表示算术运算的结果, 我们不能对字长做任何限制。一些编程语言,例 如 L isp, 实际上就支持无限精度的运算,允许任意的(当然,要在机器的内存限制之内)整数运算。更常见的是,编程语言支持固 定精度的运算,因此像“加法”和"乘法”这样的运算不同千它们在整数上的相应运算。

14

图 2- 21 整数加法。对 于一个 4 位的字长 , 其和可能需要 5 位

让我们为参数 x 和 y 定义运算 十心, 其中 O x , y<Z"’, 该操作是把整数 和 x + y 截断为 w 位得到的结果 ,再 把这个结果看做是一个无 符号数。这可以 被视为一 种形式的模运算, 对 x + y 的位级表示,简 单丢弃任何权重大千 zw-1 的位就可以计算出和模 2心。 比如, 考虑一个 4 位数字表示, x = 9 和 y = l Z 的 位表示分别为[ 1001] 和[ 1100] 。它们的和是 21, 5 位的表示 为[ 10101] 。但是如果丢弃最高位, 我们就得到[ 0101] , 也就是说,十进制值 的 5。这就和值 21 mod 16 = 5 一致。

我们可以将操作十;描述为: 原理:无符号数加法

对满足 O x , y < wZ 的 x 和 y 有:

x+y, x+ y< 沪 正常

X + ,y = {

X + y- 2"’, 2切 x + y < zu+l 溢出

(2. 11)

图 2-22 说明了公式( 2. 11 ) 的这两种情况,左 边的和x + y 映射到右边的无符号 w 位的和 x + 切。正常情况下 x + y 的值保持不变,而溢出情况则是该和数减去沪的结果。

推导:无符号数加法

一般而言,我 们可以看到 ,如果 x + y < 2"’ , 和的 w +

1 位表示中 的最高位会等于 o , 因此丢弃它不会改变这个数

值。另一 方面,如果 2气乓x + y < 2w+1 ’ 和的 w + l 位表示 图 2-22 整数加法和无符号加法中的最高位会等 于 1, 因此丢弃它就相当于从和中减去 间的关系。当x + y 大 于

了 2"0’

  • 沪 - 1 时, 其和溢出

说一个算术运算溢出,是指完整的整数结果不能放到数据类型的字长限制中去。如等 式( 2. 11 ) 所示,当两 个运算数的和为 沪 或者更大时, 就发生了溢出。图 2-23 展示了字长w= 4 的无符号加法函数的 坐标图 。这个和是按模 24 = 1 6 计算的。当 x + y < l 6 时, 没有溢出 , 并且 x + y 就是 x + y。这对应于图中标记为“正常" 的斜面。当 x + y l 6 时, 加法溢出 , 结果相当于从 和中减去 16。这 对应于图中标记为 "溢出" 的斜面。

图 2-23 无符号加法( 4 位字长, 加法是模 16 的)

当执行 C 程序时, 不会将溢出作 为错误 而发信号。不过有的时候, 我们可能希望判定是否发生了溢出。

原理:检测无符号数加法中的溢出

对在 范围 O::( x , y::( UM a x" 中的 x 和 Y • 令 s 兰 x + 切 。 则 对计算 s’ 当 且 仅 当 s < x

(或者等价地 s< y ) 时, 发生了溢 出。

作为说明 ,在 前面的示 例中, 我们看到 9 + n 2 = s 。由于 5 < 9 , 我们可以看出发生了溢出。

推导:检测无符号数加法中的溢出

通过观察发现 x + y x , 因此如果 s 没有溢出, 我们能够肯定 s 工。另一方面,如果

s 确实溢出了, 我们就有 s = x + y — 2中。 假设 y < 2切, 我们就有 y — 沪 < O, 因此 s = x + ( y - 2勹<工。

练习题 2. 27 写出一个具有如下原型的函数:

I* Determine whether arguments can be added without overflow *I

int uadd_ok(unsigned x, unsigned y);

如果 参数 x 和 y 相加 不会产 生溢 出,这 个函数就 返回 l 。

模数加法形成了一种数学结构, 称为 阿贝 尔群 ( A be lia n group), 这是以丹麦数学家Niels Henrik Abel( 180 2 18 29 ) 的名字命名。也就说,它 是可交换的(这就是为什么叫" a belia n" 的地方)和可结合的。它有一个单位元 o , 并且每个元素有一个加法逆元。让我们考虑 w 位的无符号数的集合, 执行加法运算 + ::,。 对千每个值 工,必 然有某个值—釭 满足一巨 +扛 = O。该 加法的逆操作可以 表述如下:

原理:无符号数求反

对满足 0 工< 沪 的任意 工,其 w 位的无符 号逆元 —扛 由下式给 出:

玉=厂沪'- x ,

该结果可以很容易地通过案例分析推导出来: 推导:无符号数求反

x=O x>O

( 2. 12)

+::, 下的逆元。

练习题 2. 28 我们 能用 一个 十六进 制数 字来表 示长度 w = 4 的位模式。 对于这 些数 字的无符 号解释,使 用等 式( 2. 1 2 ) 填写下表 , 给出所 示数 字的 无符 号加 法逆元的 位表 示

(用十六进制形式)。

2. 3. 2 补码加法

对于补码加法,我们必须确定当结果太大(为正)或者太小(为负)时,应该做些什么。

给定在范围 - z-w I < x , Y< wz - 1 —1 之内的整数值 x 和 y , 它们的和就在范围— zw< x +

y w2 - 2 之内, 要想准确表示, 可能需 要 w + l 位。就像以前一样, 我们通过将表示截断

到 w 位, 来避免数据大小的不断扩张。然而, 结果却不像模数加法那样在数学上感觉很熟悉。定义 x + 切 为整数和 x + y 被截断为 w 位的结果, 并将这个结果看做是补码数。

原理:补码加法

对满足 — zw- I < x , Y < zw- 1 — 1 的 整 数 x 和 y , 有:

x+ y-2"’, 2正 l x + y

气 y { x + y , — 2正’,;;;X + y < z-• X + y + zw , X + y < — 2 正 l

图 2-24 说明了这个原理, 其中, 左边的和 x + y

的取值范围 为— 2-w 三x + y 冬 wz - 2 , 右边显示的是该

正溢出正常负溢出

x+y

+zw

(2. 13)

和数截断为 w 位补码的结果。(图中的标号“情况 l " 情况4

到“情况 4" 用于该原理形式化推导的案例分析中。)

当和 x + y 超过 TMax 心时(情况 4) , 我们说发生了正溢 情况3

出。在这种情况下, 截断的结果是从和数中减去 2"0'

当和 x + y 小千 TMin 亿,时( 情况 1 )’ 我们说发 生了 负 溢

+ 2W一)

x+‘y

+zw-1

出。 在这种情况下 ,截断的结果是把和数加上 2"’ 。

两个数的 w 位补码之和与无 符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器指令来执行无符号或者有符号加法。

情况2

情况I

-2 W 一l

-2 w

-2 w-1

推导:补码加法

既然补码加法与无符号数加法有相同的位级表示, 我们就可以按如下步骤表示运算+ :,,: 将其参数转换为无符号数,执行无符号数加法,再将结果转换为补码:

图 2- 24 整数和补码加法之间的关系。当 x + y 小 于一 2-u· l 时, 产生负溢出。当它大千 2w- l 时, 产生正溢出

x + :,,y 丰 U 2Tw ( T 2Uw ( x ) + ::,T ZUw ( y ) ) (2. 14)

根据等式 ( 2. 6), 我们可以把 TZU w ( x ) 写 成 Xw-1 沪 + x , 把 T ZU w ( y ) 写成 Yw-1 沪 + y 。使用属性,即十;是模沪的加法,以及模数加法的属性,我们就能得到:

x + 心 y = u z 兀 ( T ZUw Cx ) + 汇 T ZUw( y ) )

= u z兀 [ ( x 正 1 沪 十 x + Yw1- 沪 + y) mod 2勹

= UZT w[ ( x + y) mod 2 勹

消除了 x心一 1 沪 和Yw- 1 沪 这两项, 因为它们模 沪 等于 0。

为了更 好地理解这个数最,定 义 z 为整数和 z 土 x + y , z ’ 为 z ’ 辛 z mod 2气 而 z" 为z"辛 u z兀 ( z ’ ) 。数值 z"等于 x + 切 。我们分成 4 种情况分析, 如图 2-24 所示。

  1. —2w:::今 < — 2-w

l 。 然后, 我们会有 z ’ = z + 2切。 这就得出 o < z ’ < — 2-w

l +2w =

w2 - l 。 检查等式( 2. 7), 我们看到 z ’ 在满足 z"= z ’ 的范围之内。这种情况称为 负 溢出( nega­ ti ve overflow ) 。我们将两个负 数 x 和 y 相加(这是我们能得到 z < - 2w-l 的 唯一方式), 得

到一个非负的结果 z"= x + y + w2 0

  1. - 2w气 < z < O。 那么, 我们又将有 z ’ = z + 2心, 得到— 2-w l + 2心 = 2切一1< z ’ < 2 二

    检查等式 ( 2. 7), 我们看到 z ’ 在满足 z" = z ’ —沪 的范围之内, 因此 z" = z’ — 沪 = z 十沪 — 沪= z。也就 是说,我们 的补码和 z"等千整数 和 x + y 。

  1. 0冬z < 2-w

l 。那 么, 我们将有 z ’ = z , 得到 o< z’ < 2-w

l , 因此 z" = z’ = z。补码和

z"又等于整数和 x + y。

  1. zw- l z < 2切。 我们 又将有 z ’ = z , 得到 z-w

1 冬 z ’ < zw 。 但是在这个范围内 , 我们有

z11 = z1 —沪 , 得到 z" = x + y - 2也。 这种情况称为正溢出( positive overflow ) 。我们将正数 x和 y 相加(这是我们能得到 z多 2心一]的唯一方式), 得到一个负数结果 z" = x + y - 2亿’ 0 ■ 图 2- 25 展示了一些 4 位补码加法的示例作为说 明。每个示例的情况都被标号为对 应

于等式( 2. 13 ) 的推导过程中的情况 。注意 24 = 1 6’ 因此负溢出得到的结果比整数和大 16 , 而正溢出得到的结果比之小 16。我 们包括了运算数和结果的位级表示。可以观察到, 能够通过对运算数执行二 进制加法并将结果截断到 4 位,从 而得到结果。

图 2- 25 补码加法示例。通 过执行运算数的二进制加法并将结果截断到 4 位, 可以获得 4 位补码和的位级表示

图 2-26 阐述了字长 w = 4 的补码加法。运算数的范围为—8 7 之间。当 x + y < — 8 时, 补码加法就会负溢出 ,导 致和增加了 16。当一8 x + y < 8 时, 加法就产生 x + y。当x + y娑8 , 加法就会正溢出 ,使 得和减少了 16。这三种情况中的每一种都形成了图中的一个斜面。

图 2- 26 补 码 加 法(字长为 4 位的情况下,当 x + y < - 8 时 ,

产生负溢出;工 + y 多 8 时, 产生正溢出)

等式 ( 2. 1 3 ) 也让我们认出了哪些情况下会发生溢出: 原理:梒测补码加法中的溢出

对满足 T M i nw< x , y < T M a x 心的 x 和 y , 令 烂 = x + 切 。 当 且 仅 当 x > O, y>O, 但

冬0 时, 计算 s 发 生了 正 溢出。 当 且 仅 当 x < O, y<O, 但s O 时 , 计算 s 发生了 负 溢出。图 2-25 显示了 当 w = 4 时 , 这 个 原 理 的 例 子 。 第 一 个 条 目 是 负 溢 出 的 情 况 , 两 个 负 数

相加得到一个正数。最后一个条目是正溢出的情况,两个正数相加得到一个负数。 推导:检测补码加法中的溢出

让我们先来分析正溢出。如果 x > O, y > O, 而 s < O, 那么显然发生了正溢出。反过来,正溢出的条件为: l)x>O, y> O( 或者 x + y < T M a x w ) , 2 ) s < O( 见 公 式 ( 2. 1 3 ) ) 。 同样的讨论也适用于负溢出情况。

练习题 2. 29 按照 图 2- 25 的形 式填 写 下表。 分别 列 出 5 位参数的整数值、整数和 与补码 和的数值、 补码 和的位级表示 , 以及属于等 式 C2. 1 3 ) 推导中的哪种情况。

Xy
[10100][10001]
[11000][11000]
[10111][01000]
(00010][00101]
[01100][00100]

练习题 2. 30 写出一个具有如下原型的函数:

I* Determine whether arguments can be added without overflow *I

int tadd_ok(int x, int y);

如果参数 x 和 y 相加不会产 生溢出 , 这 个函数就返回 1 。

练 习题 2. 31 你的同事对你补码加法溢出条件的分析有些不耐烦了,他给出了一个函数 t a d d _o k 的实现, 如下所 示 :

I* Determine whether arguments can be added without overflow *I I* WARNING: This code is buggy. *f

int tadd_ok(int x, int y) { int sum= x+y;

return (sum-x == y) && (sum-y == x);

你看了代码以后笑了。解释一下为什么。

练习题 2. 32 你现在有个任务, 编 写 函 数 t s u b_ o k 的代码 , 函数的参数是 x 和 y , 如果计算 x - y 不产 生溢出, 函数就返回 1 。假设你写 的练 习题 2. 30 的代码 如下所 示:

I* Determine whether arguments can be subtracted without overflow *I I* WARNING: This code is buggy. *I

int tsub_ok(int x, int y) {

return tadd_ok(x, -y);

x 和 y 取什 么值时, 这 个 函 数 会 产 生 错误的 结 果? 写 一个 该 函 数 的正确 版 本(家

庭作业 2. 74) 。

2. 3. 3 补码的非

可以看到范围在 TMinw x T M a x 心 中 的 每个数字 x 都有十;下的加法逆元, 我们将

- :.,x 表示如下。

原理: 补 码 的非

对满足 T M i n u女 T M a x w 的 X , 其补码的非 一扛 由 下式给出

飞={ T M i n w’ X = T M i n w

— x , X > TM i n w

(2. 15)

也 就 是 说 ,对 w 位的补码加法来说, T M i n切是 自己的加法的逆, 而 对 其 他 任何数值

x 都有- x 作为其加法的逆。推导:补码的非

观察发现 T M i un . + T M 匹 =_ z-u· 1 + ( — z-u· 1 ) = — 2心。 这 将导致 负 溢 出, 因 此

T Mi nw已 T M i n w = —w2 十沪 = O。 对满足 x > T Minw 的 x , 数 值 - x 可以表示为一个 w 位

的补码, 它 们的 和—x + x = O 。

练习题 2. 33 我们 可以用 一个 十 六进制数 字 来表 示长 度 w = 4 的位模式。 根据这些 数字的 补码 的解释, 填写 下表, 确定 所示数 字的 加法 逆元。

对于补码和无符号(练习题 2. 28 ) 非 ( ne ga t ion ) 产 生的 位模式 , 你观察到什么?

一 补 码非的位级表示

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

执行位级补码非的笫一种方法是 对每一位求补, 再 对结果加 1。在 C 语言中, 我们可以说, 对于任 意整数值 x , 计算表达式- x 和 ~x +l 得到的结果 完全 一样。

下面是 一些示例, 字长为 4 :

-4 [0011]

[11 11]

从 前 面的例子我们知道 Ox f 的补是 Ox O, 而 Ox a 的补是 OxS, 因 而 Ox f ff ff ff a 是

—6 的补码表示。

计算一个数 x 的补码非的笫二种方法是建立在将位向 量分为两部分的基础之上的。假设

k 是最右边的 1 的位置, 因 而 x 的位级表示形如[ x ,.,-1 , Xw-2• … , XHJ , 1, Q, … , O] 。

( 只要 x #- 0 就 能够找到 这样的 K。)这个值的非写成二进制格 式就是[~乓- 1 , ~ x 心- 2 ,

~ xHI , 1, Q, …, O] 。 也 就 是 , 我们对位 K 左边的所有位取反。

我们用一些 4 位数字来说明这个方法,这里我们用斜体 来突出最右边的模式 1, o,…, 0:

X一X
[1100]-4[0100]4
(1000]-8[1000]-8
[0101]5[1011]-5
[0111]7[1001]- 7
2. 3. 4无符号乘法

范围在 O x , y 沪 —l 内的整数 x 和 y 可以被表示为 w 位的无符号数, 但 是 它 们 的乘积 x • y 的取值范围为 0 到( 2w- 1 ) 2= z2w —zw+ l + 1 之 间 。 这可能需要 2w 位来表示 。不过,C 语言中的无符号乘法被定义为产生 w 位的值,就 是 2w 位的整数乘积的低 w 位表示的值。我们将这个值表示为 X * 切 。

将一个 无符号数截断为 w 位等价于计算该值模 2切, 得到:

原理:无符号数乘法

对满足 O x , y UMa x 切 的 x 和 y 有:

X * 沁y = (x• y)mod 2切 (2. 16)

2. 3. 5 补码乘法

范围在—z-w l X, y zw- 1 —1 内的整数 x 和 y 可以被表示为 w 位的补码数字,但 是它们的乘 积 x • y 的取 值范 围 为 - zw- 1 • ( zw- l_ l) = - 2红 - 2 + zw- 1 到 — zw- 1 0 — zw- 1 =

_ zw-2 2 之 间 。 要 想 用 补 码 来 表 示这个乘积,可 能 需 要 2w 位。然而, C 语 言 中 的 有符号乘

法是通 过将 2w 位的乘积截断为 w 位来实现的。我们将这个数值表示为 X * 切 。 将 一 个 补码数截 断为 w 位相当于先计算该值模 wz ’ 再 把 无 符 号 数 转换为补码 ,得 到 :

原理:补码乘法

对 满足 TM i n,,冬 x , y T M a x w 的 x 和 y 有 :

x * :.,y = U 2T w ( ( x • y)mod 2勹 ( 2. 17)

我们认为对于无符号和补码乘法来说,乘法运算的位级表示都是一样的,并用如下原 理说明:

原理:无符号和补码乘法的位级等价性

给定长度 为 w 的位向 量 王 和 y, 用 补 码形 式的位向量表 示 来定 义整数 x 和 y : x= B2TwG), y= B2兀 (y ) 。 用 无符号形 式的 位向 量表 示 来定义非 负 整 数 x ’ 和 y ’ : x ’ =

B2UwG), y’ = BZU 切(如 。 则

T 2 凡 ( x 亡 y ) = U ZB 心 ( x ’ 已 y ’ )

作 为说明,图 2-27 给出了不同 3 位数字的乘法结果。对于每一对位级运算数, 我们执行无符号和补码乘法,得 到 6 位 的 乘 积 ,然 后 再 把 这些乘积截断到 3 位。 无 符号 的截 断后的乘积总是等于 x • y mod 8。虽然无符号和补码两种乘法乘积的 6 位表示不同,但 是 截断后的乘积的位级表示都相同。

推导:无 符 号 和 补码 乘 法 的 位 级 等价性

根据等式( 2. 6), 我们有 x ’ = x + x w- 1 沪 和 y ’ = y + y心 一 1 沪 。 计 算 这 些 值 的 乘 积 模 2心

得到以下结果:

(x’• y’)mod w2 = [ ( x + x w1- 沪 ) • (y+ y 正 1 沪 ) ] mod 2心

= [ x • y + (x匹 1 Y + Yur1- X) 沪 十 Xur1- Yur1- 22 勹mod 2"’

= ( x • y) mod 2 切

( 2. 18)

由 千模运算符,所 有带有权重 沪 和 2红的项都丢掉了。根据等式( 2. 1 7 ) , 我们有 x * :Vy = U 2兀 ( ( x • y) mod 2"’) 。对等式两边应用操作 T 2Uw 有:

T 2U心位 * 沁y ) = T 2U,, ( U 2兀 ( ( x • y) mod 2心)) = ( x • y) mod 2 心

将 上述结果与式 ( 2. 1 6 ) 和式 ( 2. 18 ) 结 合 起 来 得 到 T 2U心 ( x * :Vy ) = ( x ’ • y’) mod zw=

x ’ 亡 y ’ 。 然 后 对 这个等式的两边应用 U2B..,. , 得到 .

模式Xyx·y截断的x·y
无符号5[101)3[Oil]15[001III]7 [111]
补码-3[IOI]3[011)- 9[110111)-1 [I l l]
无符号4[100]7[Ill]28[011100]4 [JOO]
补码-4[100]-1[111]4(000100]—4 (100]
无符号3(011]3[Oil]9[001001]l [001]
补码3[Oil]3[Oil]9[001001]I [001]

图 2-27 3 位无符号和补码乘法示例。虽然完整的乘积的位级表示可能会不同 , 但是截断后乘积的位级表示是相同的

练习题 2. 34 按照 图 2-27 的风格填写 下 表, 说明 不同的 3 位数 字乘 法的 结果 。

模式Xyx ·y截断的x ·y
无符号(100)(101]
补码[100][ IO I]
无符号[010][III]
补码[010][111]
无符号[I IO][I IO]
补码(110][110]

练习题 2. 35 给你一个 任务, 开发 函数 t mu l t _ o k 的代码 , 该函 数会判断 两个 参数相乘是否会产生溢出。下面是你的解决方案:

I• Determine whether arguments can be multiplied without overflow•I int tmult_ok(int x, int y) {

int p = x•y;

/• Either xis zero, or dividing p by x gives y•I return !x 11 p/x == y;

你用 x 和 y 的很多值 来测试这段代码 , 似 乎都 工作正 常。 你的同 事挑战 你, 说:

“ 如果我不能用减法来 检验加法是 否溢 出(参见 练 习题 2. 31), 那么你怎么能用除法来检验乘法是否溢出呢?”

按照 下面的思路, 用 数 学推 导来证 明 你的 方 法是对的。 首先, 证 明 x = O 的 情 况是正确 的。 另 外, 考虑 w 位数 字 X ( x -=/=-0) 、 y 、 p 和 q’ 这里 p 是 x 和 y 补码 乘 法的结果, 而 q 是 p 除以 x 的结果。

1 ) 说明 x 和 y 的整数 乘 积 X • y, 可 以写 成这样的形 式 : X• y= p + tzw , 其中,

t -=l=-0 当且 仅当 p 的计算溢出。

2 ) 说 明 p 可以写 成这样的形式 : p = x • q + r , 其 中 1 门< l x l 。

3 ) 说 明 q = y 当 且 仅 当 r = t = O 。

练习题 2. 36 对于数据 类型 i n t 为 3 2 位的情况, 设 计一个 版 本的 t mu l 七_ o k 函 数(练

习题 2. 35), 使用 64 位精度的数据 类 型 i n t 64—七, 而 不使 用除法。

国日XOR 库中的 安 全 漏 洞

2002 年 ,人 们发现 S un M icro s ys t ems 公 司提 供 的 实现 XDR 库的代 码 有安 全 漏洞 ,

XDR 库是一个广 泛使 用的 、 程序 间 共 享数 据 结 构 的 工具 , 造 成 这 个 安 全 漏洞的 原 因是程序会在毫 无察觉的情 况下产生乘法溢出。

包含安全漏洞的代码与下面所示类似:

  1. I* Illustration of code vulnerability similar to that found in

    1. * Sun’s XOR library.

    2. *I

      4 void* copy_elements (void *ele_src [] , int ele_cnt, size_t ele_size) {

      s I*

  2. * Allocate buffer for ele_cnt objects, each of ele_size bytes

  3. * and copy from locations designated by ele_src

    s *I

  4. void *result= malloc(ele_cnt * ele_size);

  5. if (result == NULL)

  6. I* malloc failed *I

  7. return NULL;

  8. void *next= result·

  9. int i;

  10. for (i ·= O; i < ele_cnt; i++) {

  11. I* Copy object i to destination *I

  12. memcpy(next, ele_src[i], ele_size);

  13. I* Move pointer to next memory region *I

  14. next+= ele_size;

    . 20 }

    21 return result;

    22 }

    函数 c op y_e l e me n t s 设 计 用 来将 e l e _c nt 个数据结构复制到笫 9 行 的 函 数 分 配的缓冲区中, 每 个数据结构 包含 e l e _ s i z e 个宇节。 需要的 字节数 是 通过 计算 e l e _c nt * e l e_s i ze 得到的。

    想象一下, 一个怀有恶意的程序 员在 被 编 译 为 32 位的 程序 中 用参 数 e l e _ c n t 等 于1048 577(220 + 1) 、 e l e _ s i z e 等 于 4096 ( 212) 来 调 用这 个函数。 然后 笫 9 行 上 的 乘 法会 溢出, 导致只 会 分 配 4096 个 字节 , 而不是 装 下这些数据所需要的 4 294 971 392 个宇节 。从第 15 行 开始的循环会试图复 制所有的 字节 , 超 越 已分 配的缓 冲 区的 界 限 , 因 而 破 坏了其他的数据结构。这会导致程序崩溃或者行为异 常。

    几乎每 个操作 系统 都 使 用了这 段 Sun 的代码,像 Intern et Explorer 和 Ker beros 验 证 系统 这 样 使 用 广 泛的 程 序 都 用到 了 它。 计 算机 紧急 响 应 组 ( Computer Emergency Response Team , CERT) , 由卡内基-梅隆软件工程协会 ( Carnegie Mellon Software Engineering Insti­ tute) 运作的一个追踪安全漏洞或失效的组织, 发 布了 建议 " CA-2002- 25" , 于是许多公司急忙对它们的代码打补丁。幸运的是, 还 没 有 由 于这个漏洞引起的安全失效的报告。

    库函数 c a l l o c 的 实现 中存在 着类似的 漏洞。 这 些已 经被修补过 了 。 遗 憾 的 是 , 许

多程序员调用分 配函数(如 ma l l o c ) 时,使 用算 术表达式 作为参数, 并且 不 对这些表 达式进行 溢出检查。编写 c a l l o c 的可靠版本 留作 一道 练习题(家庭作业 2. 76 ) 。

练习题 2. 37 现在你 有一个任务, 当数 据类型 i n t 和 s i z e —t 都是 32 位的 , 修补上述旁注给出的 XOR 代码中的 漏 洞。 你决 定将待分配 字节 数设置为数 据类型 u i n t 64_ t , 来消除 乘法溢 出的 可能性。 你把原来 对 ma l l o c 函数的调用(第 9 行)替换如 下: uint64_t asize =

ele_cnt * (uint64_t) ele_size;

void *result= malloc(asize);

提磋一下, ma l l o c 的参数类型是 s i ze _ 七。

  1. 这段代码对原始的代码有了哪些改进?
  2. 你该如何修改代码来消除这个漏洞?

2. 3. 6 乘以常数

以往,在 大多数机器上, 整数乘法指令相当慢, 需 要 10 个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运 算和移位)只需要 1 个时钟周期。即使在我们的参考机器 In t el Core i7 H as well 上, 其整数乘法也需要 3 个时钟周期。因此, 编译器使用了一项重要的优化, 试着用移位和加法运算的组 合来代替乘以常数因子的乘法。首先, 我们会考虑乘以 2 的幕的情况, 然后再 概括成乘以任意常数 。

原理: 乘以 2 的幕

设 x 为位模 式[ x w- 1 , X..,_.- 2 • … , x 。] 表示的 无符 号整数 。那么, 对于任何 k O, 我们 都认为[ x w- 1 • Xw-2• …, Xo , 0 , … , O ] 给出 了 x 2k 的 w + k 位的 无符 号表示,这 里右边增 加 了 K 个 0。

因此, 比如, 当 w = 4 时, 11 可以 被表示为[ 1011] 。k = 2 时将其左移得到 6 位向 量

[101100], 即可编码 为无符号 数 11 • 4 = 44。

推导:乘以 2 的幕

这个属性可以通过等式( 2. 1) 推导出来:

B 2U吐八[ Xw–1-

, Xw-2-

认 尸 一1

, …,工O ,o ,…,O ] ) = x ; 2’十k

,- o #

=[笘x 心]• 2k

. = X 2k .

当对固定字长左移 k 位时, 其高 k 位被丢弃, 得到

[ x亡 k- 1 , x正 仁 2 ’ … ,Xo , Q , … ,O ]

而执行固定字长的乘法也 是这种情况。因此,我 们可以看出左 移一个数值等价千执行 一个与 2 的幕相乘的无符号乘法。

原理: 与 2 的幕相 乘的无符号 乘法

C 变量 x 和 K 有无符 号数值 x 和 k’ 且 O k< w , 则 C 表达式 x <<k 产 生数 值 X * ::,2k o

由于固定大小的补码算术运算的 位级操作与其无符号运算 等价, 我们就可以对补码运算的 2 的幕的乘法与左移之间的关系进行类 似的表述:

原理: 与 2 的幕相乘的补码乘法

C 变量 x 和 K 有补码值x 和无符号数值k , 且 O k< w , 则 C 表达式x<<k 产生数值 x 只笠。

注意,无 论 是 无 符 号 运算还是补码运算,乘 以 2 的 幕都 可能会导致溢出。结果表明, 即使溢出的时候, 我们通过移位得到的结果也是一样的。回到前面的例子, 我们将 4 位模式[10 11] ( 数值 为 11 ) 左移两位得 到[ 101100]( 数值为 44 ) 。将这个值截断 为 4 位得到[ 1100] ( 数值为 12 = 44 mod 16 ) 。

由于整数乘法比移位和加法的代价要大得多,许 多 C 语言编译器试图以移位、加法和减法的组 合来消除很多整数乘以常数的情况。例如, 假设一个程序包含表达式 X * 1 4。 利用14 = 23 十沪 + 21 , 编 译 器会将乘法重写为 (x <<3 ) + (x <<2 ) + (x <<l ) , 将一个乘法替换为三 个移位和两个 加法。无论 x 是无符号的还是补码, 甚至当乘法会导致溢出时, 两 个 计 算 都会得到一样的结果。(根据整数运算的属性可以证明这一点。)更好的是, 编 译 器 还可以利用属性 14 = 24 - 21 ’ 将 乘 法重写为 (x « 4 ) 一 (x <<l ) , 这时只需要两个移位和一个减法。

练习题 2. 38 就像我们 将在 第 3 章中看到 的 那样, L E A 指令能 够执行形如 (a <<k ) +b 的计 算, 这里 k 等于 0 、1 、2 或 3’ 而 b 等于 0 或 者某个程序值。编译器 常 常用 这条指令 来执行常数因子乘法。 例 如, 我们 可 以 用 (a <<l ) +a 来计算 3*a 。

考虑 b 等于 0 或者等于 a 、K 为 任意可 能的值的 情况 , 用 一条 L E A 指令 可以 计算

a 的哪 些倍 数?

归纳一下我们的例子,考 虑 一 个 任 务 , 对于某个常数 K 的表达式 x * K 生成代码。编译器会将 K 的二进制表示表达为一组 0 和 1 交替的序列:

[ (O···O) ( 1· •• l) ( O·· · O) · ··0 ·· · 1 ) ]

例如, 1 4 可以写成[ ( O… 0 ) (1 11 ) ( 0 ) ] 。 考 虑 一组从位位置 n 到位位置 m 的连续的 l ( n

m ) 。(对于 14 来说, 我们有 n = 3 和 m = l 。)我们可以用下面两种不同形式中的一种来计算这些位对乘积的影响:

形 式 A: ( x« n ) + ( x « ( n—1 ) ) + …+ ( x <<m)

形式 B: (x«(n+l))-(x<<m)

把每个 这样连续的 1 的结果加起来, 不 用 做 任何乘法, 我们就能计算出 x * K。当然, 选择使用移位、加法和减法的组合, 还是使用一条乘法指令, 取 决 千这些指令的相对速度, 而这些 是与机器高度相关的。大多数编译器只在需要少量移位、加法和减法就足够的时候才使用这种优化。

练习题 2. 39 对于位位置 n 为 最高有效位的 情况, 我们 要怎样修 改形 式 B 的表达式? 练习题 2. 40 对于下 面每个 K 的 值, 找 出 只 用 指定数 量的运 算表达 X * K 的 方 法, 这里我们认为 加法和 减法的开 销 相 当。 除 了 我们 已 经 考 虑 过的 简 单的 形 式 A 和 B 原则, 你可 能会需要使 用 一些技巧。

K 6移位 2加法/减法 I表达式
31II
-62I
5522

练习题 2. 41 对于一组 从位位置 n 开始到 位位置 m 的连 续的 l< n m ) , 我们看到可以 产生 两种 形式的代码, A 和 B。编译器该如何 决定使用哪一种 呢?

  1. 3. 7 除以 2 的幕

    在大多数机器上, 整 数 除 法要比整数乘法更慢一— 需要 30 个或者更多的时钟周期。

除以 2 的幕也可以用移位运算来实现,只 不 过我们用的是右移, 而不是左移。无符号和补码数分别使用逻辑移位和算术移位来达到目的。

整数除法总是舍入到零。为了准确进行定义,我 们要引入一些符号。对于任何实数 a ,

定义La 」为唯一的整数 a ’’ 使 得 a ’ a < a ’ + l 。例如 , L 3. 1 4」= 3 , L- 3. 1 4」= - 4 而L 3 」=

3 。同 样 ,定 义「a l 为唯一的整数 a ’’ 使得 a ’ —l < a a ’ 。例如 ,「3. 14 7= 4 , 「—3. 14 l= -3,

而「3 1= 3。对于 x 多0 和 y> O, 结 果 会 是 L x / y 」, 而 对 于 x < O 和 y > O, 结 果 会是「x / y l。也就是说, 它将 向下 舍入一个正值, 而向上舍入一个负值。

对无符号运算使用移位是非常简单的,部分原因是由千无符号数的右移一定是逻辑 右移。

原 理: 除 以 2 的幕的 无符号除法

C 变量 x 和 K 有无符号数值 x 和 k’ 且 O k < w , 则 C 表达式 x >>k 产 生数 值L x / 2勹。例如 ,图 2-28 给出了在 12 340 的 16 位表示上执行逻辑右移的结果,以 及 对 它执行除

以 1、2、16 和 256 的结果。从左端移入的 0 以斜体表示。我们还给出了用真正 的运算做除法得到的结果。这些示例说明,移位总是舍入到零的结果,这一点与整数除法的规则 一样。

图 2- 28 无符号数除以 2 的幕(这个例子说明了执行一个逻辑右移k 位与

除以 2k 再舍人到零有一样的效果 )

推导:除 以 2 的幕的无符号除法

设 x 为位模式[ x w- 1 • Xw- 2 , …, x 。] 表 示 的 无 符 号 整数, 而 K 的 取 值 范 围 为 O::( k <

W 。 设 x ’ 为 w —K 位 位 表示[ Xw- 1 , Xw- 2 • … , Xk ] 的 无 符 号 数 , 而 x" 为 K 位 位表示[ Xk- 1 •

…, x。]的 无 符号数。由此, 我们可以看到 x = 2坛'+工", 而 O::( x’’ < 2k 。 因 此, 可得L x i

沪」= x ’ 。

对位向量[ x w- 1 , Xw- 2 , … , x。J逻辑右移 k 位会得到位向量

[ O’ … ,O , x 亿 一 1 , X已 ,…,k工] .

这 个 位向量有数值 x ’’ 我们看到,该 值可以通过计算 x>>k 得到。

对于除以 2 的幕的补码运算来说,情 况要稍微复杂一些。首先, 为了保证负数仍然为负,移 位 要 执行 的 是 算术右移。现在让我们来看看这种右移会产生什么结果。

原理: 除 以 2 的幕的 补码除法,向 下舍 入

C 变量 x 和 K 分别有补码值 x 和无符号数值 k’ 且 O k < w , 则当执行算术移位时,

C 表达式 x >>k 产生数 值L x / 2k 」。

对 于 x O, 变扯 x 的最高有效位为 o, 所以效果与逻辑右移是一样的。因此,对千非负 数来说,算 术 右移 K 位 与除 以 沪 是 一样的。作为一个负数的例子,图 2-29 给出了对—12 340 的 16 位表示进行算术右移不同位数的结果。对于不需要舍入的情况( k = 1), 结果 是 x / 2k 0 但是 当需 要 进行舍入时,移 位导 致结 果 向 下 舍 入。例如, 右移 4 位将会把一771. 25 向下舍入为—772。我们需要调整策略来处理负数 x 的除法。

k>>k C二进制)十进制- J234Q / 2k
。 I 4 8ll OOll l l llOOll 00-12340-12340.0
JI 10011111100110—6170-6170.0
II JJI IOOI I I II IOO-772-771.25
11JJJJJJ11001111-49—48.203 125

图 2- 29 进行算术 右移(这个例子说明了算术右移类似于除以 2 的幕, 除 了是向下舍入, 而不是向 零舍入)

推导:除 以 2 的幕的补码除法,向 下 舍 入

设 x 为位模式[ x w- 1 • 五 - 2’ … , 工。]表 示 的 补 码 整数, 而 K 的 取 值 范 围 为 O k < w 。设 x ’ 为 w —k 位[ x w- 1’ 江 - 2’ … , Xk ] 表 示的补码数, 而 x"为低 k 位[ Xk- 1 , …, x o J 表 示的无符号数。通过与对无符号情况类似的分析, 我们有 x = 2坛’ + x" , 而 O x “< Zk , 得到

x’ = 巨 / 2勹。 进 一 步 , 可以观察到, 算术右移位向量[ xw- 1 , Xw-2 , …, X。] k 位 ,得 到 位 向量

[ x u气 ,…,Xu- 1 , Xu一 l , X已 ,…,Xk]

它刚好就 是将[五- ] ’ 立 - 2 ’ … , Xk ] 从 w —k 位符号扩展到 w 位。因此, 这 个 移 位 后 的 位向量就是L x / 2勹的 补 码 表 示。

我们 可以通过在移位之前 "偏置( biasin g) " 这个值, 来修正这种不合适的舍入。原理:除 以 2 的幕的补码除法,向 上舍入

C 变量 x 和 K 分别有补 码值 x 和无符号数值 k’ 且 O k < w , 则当执行算术移位时,

C 表达式 (x + (l « k) — l ) » k 产生数 值L x / 2k 」。

图 2-30 说明在执行算术右移之前加上一个适当的偏置量是如何导致结果正确舍入的。在第 3 列, 我们给出了—12 340 加上偏最值之后的结果,低 k 位(那些 会向 右移 出的 位)以 斜体表示 。我们可以看到,低 K 位 左 边 的 位可能会加 1 , 也可能不会加 1。对于不需要舍入的情 况( k = 1)’ 加 上 偏 量 只 影响那些被移掉的位。对于需要舍入的情况,加 上 偏 量导致较高的 位加 1, 所以结果会向零舍入。

k偏量-12 340 + 偏量>> k (二进制)十进制- l 2340 /2k
。 I 4 8。 I 15 2551100111111001100 110011111100 I101 1100111111011011 110100001100101111001 l llllOOllOO JI 10011111100110 1111110011111101 11111lllllOIOOOO-12340 —6170 -771 -48-12340.0 - 6170 . 0 —771.25 -48.203125

图 2-30 补码除以 2 的幕(右移之前加上一个偏蜇,结 果就向零舍入了)

偏置技术利用如下属性:对 于 整 数 x 和 y ( y > O) , 「 x / y l = L<x + y —1) / y 」。例如, 当x = —30 和 y = 4 , 我们有 x + y —1 = — 27 , 而「- 30/ 4 7= - 7 = L— 27 / 4 」。当 x = — 32 和y = 4 时 , 我们有 x + y - l = - 29 , 而「—32/ 4 7= —8 = L—29/ 4」。

推导: 除 以 2 的 幕 的 补 码除法,向 上 舍 入

查看「x / y l = l ( x + y - l ) / y 」,假 设 x = qy + r , 其中 o<r < y , 得 到 ( x + y —1 ) / y =

q+ ( r + y - l ) / y , 因此LC x + y - D / y 」= q+ LrC + y - 1 ) / y 」。 当r = O 时 ,后 面一项等千 o ,

而当 r > O 时 ,等 于 1 。也 就 是 说 , 通过给 x 增加一个偏量 y - 1, 然后再将除法向下舍入, 当 y 整除 x 时, 我们得到 q , 否则, 就得 到 q+ l 。

回到 y = 护 的情况, C 表达式 x + ( l <<k ) - 1 得到数值 x + 2k — l 。 将这个值算术右移 K

位即产生巨 / 2k 」0 ■

这个分析表明对于使用算术右 移的补码机器 , C 表达式

(x<O? x+(1<<k)-1 : x)»k

将会计算数值 x / 2* o

练习题 2. 42 写 一个 函 数 d i v 1 6 , 对 于整数 参 数 x 返 回 x / 1 6 的 值。 你的 函 数 不 能使用 除法、 模运算、 乘 法、 任 何 条 件 语 句(江 或 者?:)、 任 何 比 较 运 算 符( 例 如 <、

>或==)或 任何 循 环。 你可以 假设数 据 类 型 i n t 是 3 2 位 长 ,使 用 补码 表 示 , 而右 移

是算术右 移。

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

练习题 2. 43 在下 面的 代码 中,我们 省略 了常数 M 和 N 的定 义:

#define M /* Mystery number 1 *I

#define N I* Mystery number 2 *I int arith(int x, int y) {

int result= O;

result= x*M + y/N; I* Mand N are mystery numbers. *I return result;

我们以 某个 M 和 N 的值编 译这段代码。 编译器用 我们 讨论 过的方 法优 化乘 法和除

法。 下面是将 产 生出的 机器代 码翻译 回 C 语言的 结果 :

/• Translation of assembly code for arith•/ int optarith(int x, int y) {

int t = x;

X <<= 5;

X -= t;

if (y < 0) y += 7;

y»= 3; I• Arithmetic shift•/ return x+y;

M 和 N 的值为 多少?

2. 3. 8 关千整数运算的最后思考

正如我们看到的 , 计算机执行的 “整数” 运算实际 上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围 ,结 果运算可能溢出。我们还看到, 补码表示提供了一种既能表示负数也能表示正数的灵活方法 ,同 时使用了与执行无符号算术相同的位级实现,这些运算包括像加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补 码形式表示的,都有完全一样或者非常类似的位级行为。

我们看 到了 C 语言中的某些规定可能会产生令人意想不 到的结果 , 而这些结果 可能是难以察觉或理解的缺陷的源头。 我们特别 看到了 u n s i g ne d 数据类型,虽 然它概念上很简单, 但可能导致即使是资深程序员都 意想不到的行为。我们还看到这种数据类型会以出乎意料的方式出现, 比如,当 书写整数常数 和当调用库函数时。

区U 练习题 2. 44 假设我们在对有符号值使用补码运算的 32 位机器上运行代码。对于有符号值使用的是算术右移,而对于无符号值使用的是逻辑右移。变量的声明和初始化如下:

int x “‘foo(); I* Arbitrary value *I

int y “‘bar(); I* Arbitrary value *I

unsigned ux = x; unsigned uy = y;

对于下面每个 C 表达 式, 1) 证明对 于所有的 x 和 y 值, 它都 为 真(等于 1) ; 或者

2 ) 给出使得 它为假(等于 0 ) 的 x 和 y 的值 :

A. (x > 0) 11 (x-1 < 0)

B. (x & 7) ! = 7 11 (x«29 < o)

C. (x * x) >= 0

  1. x < 0 I I -x <= 0

  2. x > 0 I I -x >= 0

  3. x+y == uy+ux

  4. X*-y + UY*UX == -x

    1. 4 浮点数

      浮点表示对 形如 V= x X 护 的有理 数进行编码。它对执行涉及非常大的数字( I VI >>

  5. 、非常接近于 O( IV l << D 的数 字, 以及更普遍地作为实数运算的近似 值的计算, 是很有用的。

    直到 20 世纪 80 年代,每 个计算机制造商都设计 了自己的 表示浮点数的规则, 以及对浮点数执行运算的细节。另外,它们常常不会太多地关注运算的精确性,而把实现的速度 和简便性看得比数字精确性更重要。

    • 大约在 1 985 年, 这些情况随着 IE EE 标准 75 4 的推出而改变了, 这是一个仔细制订的表示浮点 数及其运算的标 准。这项工作是从 1976 年开始由 I nt el 赞助的, 与 808 7 的设计同时进 行, 808 7 是一种为 8086 处理器提供浮点支持的芯片。他们请 Will ia m Kahan(加州大学伯克利分校的一位教授)作为顾问 , 帮助设计未来处理器浮点标准。他们支持 Kaha n 加入一个 IE E E 资助的制订工业标准的委员会。这个委员会最终采纳的标准非常接近于

      Kahan 为 Int el 设计的标准。目 前,实 际上所有的计算机都支持这个后来被称为 IE E E 浮点的标准。这大大提高了科学应用程序在不同机器上的可移植性。

      m IEEE ( 电气和电子工程师协会)

      电气和电 子工程 师协会( IE E E , 读做 " eye- t rip le-ee” ) 是一个包括所有电子和计算机技术的专业团体。它出版刊物,举办会议,并且建立委员会来定义标准,内容涉及从电 力传 输到软件 工程 。另一 个 IE E E 标准的 例子是 无线 网络的 802. 11 标准。

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

2. 4. 1 二进制小数

理解浮点数的第一步是考虑含有小数值的二进制数字。首先,让我们来看看更熟悉的十进制表示法。十进制表示法使用如下形式的表示:

d ‘“d 1 ···d i d 。. d - 1d-2···d-n

其中每个十进制数 d, 的取值范围是 0~ 9。这个表达描述的数 值 d 定义如下 :

d = ;t"lO; X d,

数字权的定 义与十进制小数点符号( ‘.’)相关, 这意味着小数点左边的数字的权是 10

的正幕, 得到 整 数值, 而小数点右边的数字的权是 10 的负幕, 得到小 数值。例如,

12. 3 知 表示数字 1 X 101 + 2 X 10° + 3 X 10-1 + 4 X 1-0

2 =12 34

100°

类似, 考虑一个形如

丛bm- 1 •;• bl b。. b一I b- 2 •••b- n- 1b- n

的表示法,其中每个二进制数字,或者 称为 位, b; 的取值范围是 0 和 1, 如图 2-31 所示。这种表示方法表示的数 b 定义如下:

2m

z m-1

二:

b = 2i X b; (2. 19)

, = - n

符号’.‘现在变为了二进制的点,点 左边的位的权是 2 的 正幕,点 右边的 位的权是 2 的负幕。例如, 1 01. 112 表示

数字 1 X 22 + 0 X 21 + 1 X 2° + 1 X -2 1 +

1/2n-1

1/2”

1 x -2

2= 4+ 0 + 1 + 丿 + 1_ = 5 立

2 4 4° 图 2-31 小数的二进制表示。二进制点左边的数字的

从 等式 ( 2. 19 ) 中可以 很容易 地 看 权形如 ;2 ’ 而右边的数字的权形如 1 / 2’

出, 二进制小数点向左移动一位相当于这个数被 2 除。例如, 101. 11 2 表示数 5 一, 而

10. 1112表示数 2+ 0 +

1 1 1 7

— + — + - = 2 - 。类 似, 二进制小数点向右移动一位相当千将该

2 4 8 8

数乘 2。例如 1011. 12 表示数 s + o + 2+ 1 + - = 11 -

2 2°

注意, 形如 0. 11··士 的数表示的是刚好小于 1 的数。例如, 0. 1111112

63

表示祈, 我们

将用简单的表达法 1. 0 - € 来表示这样的数值。

假定我们仅考虑有限长度的编码,那么十进制表示法不能准确地表达像一和—这样的 数。类似,小 数的二进制表示法只能表示那些能够被写成 x X 护的 数。其他的值只能够被近似地表示。例如, 数字— 可以用十进制小数 0. 20 精确表示。不过, 我们并不能把它准

确地表示为一个二进制小数,我们只能近似地表示它,增加二进制表示的长度可以提高表示的精度:

练习题 2. 45 填写下 表中的缺 失的信息 :

练习题 2 . 46 浮点运算的 不精 确性能够产 生灾 难性 的后果 。1 9 91 年 2 月 2 5 日 , 在第一次海湾战 争期间 , 沙特 阿拉伯 的达摩地 区设 置的 美国爱国 者导 弹, 拦截伊拉克 的飞毛腿导 弹失败。 飞毛腿 导弹 击中 了 美国的 一个 兵 营, 造成 2 8 名 士 兵 死亡。 美国 总审计局 ( G AO ) 对失败原 因做 了 详细的 分析[ 76] , 并且确定底层的原因在于一个数字计算不精确。 在这 个练 习中,你将 重现 总审计局分 析的 一部 分。

爱国者 导弹 系统 中含 有 一个内置的 时钟 , 其实现 类似 一个 计数器, 每 0. 1 秒就 加

1 。为 了以 秒为 单位 来确定 时间 , 程 序将用 一个 2 4 位的近似 于 1 / 1 0 的 二进 制小 数值来乘以 这个计数 器的值。 特别地 , 1 / 1 0 的二进 制表 达式是 一个无 穷序 列 0 . 0 0 0 11 0 0 1 1 [ 0 0 11 ] …2’ 其中, 方括号里 的部 分是无限重复的 。 程序用值 x 来近似 地表 示 0 . 1, X

只考虑这个 序 列 的 二 进制 小 数 点 右 边 的 前 2 3 位: :r = 0 . 0 0 0 11 0 0 11 0 0 11 0 0 11 0 0 11 0 0 。

(参考练 习题 2. 51, 里 面有 关于如何 能够 更精确地 近似表 示 0. 1 的讨论。)

  1. 0 . 1- x 的二进 制表 示是什 么?

  2. 0. 1 - x 的近似的十进 制值是 多少?

    c. 当系 统初始启 动 时, 时钟 从 0 开始, 并且一直保持计 数。 在这个例 子中, 系统 巳经运 行了大 约 1 0 0 个小 时。 程序计 算出的 时间和 实际 的时间之差 为 多少?

    D. 系统 根据一枚来袭导 弹的 速 率和 它最 后被 雷达侦 测 到的 时间, 来预 测 它 将在 哪里出现 。假定飞毛腿 的速率 大约是 2 0 0 0 米每 秒, 对它的预测 偏差 了多 少?

    通过一次读取 时钟得到的绝对 时间 中的轻微错误 , 通常不会 影响 跟踪的 计算。相反, 它应该 依赖于两次连续的读取之间的相对时间。问题是爱国者导弹的软件 巳经升级 , 可以使用更精确的函数来读取时间 , 但不 是所有的 函数调 用都用 新的代码替 换了。 结果 就是 , 跟踪软件 一次读取用的是精确的时间 ,而另 一次读取用的是不精确的时间 [ 10 3] 。

  3. 4. 2 IEEE 浮点 表示

    前一节中谈到的定点表示法不能很有效地表示非常大的数字。例如, 表达式 5 Xz 1 0 0 是用 101 后面跟随 1 00 个零的位模式来表示。相反 , 我们希望通过给定 x 和 y 的值, 来表示形如 x X 护 的数 。

    IEEE 浮点标准用 V = C— l ) ’ X M X 沪的形式来表示一个数:

    • 符号( sig n ) s 决定这数是负数 Cs = 1 ) 还是正 数( s = O) , 而对于数值 0 的符号位解释作为特殊情况处理。
    • 尾数( s ig ni fi cand ) M 是一个二进制小数, 它的范围是 1 ~ 2—€’ 或者是 O~ l —C。
    • 阶码( ex po nen t ) E 的作用是对浮点数加权 ,这 个权重是2 的 E 次幕(可能是负数)。将浮点数的位表示 划分为三个字段,分别 对这些值进行编码:
    • 一个单独的符号位 s 直接编码符号 s 。
  • k 位的阶码字段 e xp = ek- 1…e1e 。编码阶码 E。

  • n 位小数字段 fr a c = f n- 1 …八儿 编码尾数 M, 但是编码出来的值也依赖于阶码字段的值是否等千 0。

    图 2- 32 给出了将这三个字段装进字中两种最常见的格式。在单精度浮点 格式C C 语言中的 fl oa t ) 中, s 、 e x p 和 fr a c 字段分别 为 1 位、k = 8 位和 n = 23 位, 得到一个 32 位的

    表示。在双精度 浮点格式cc 语言中的 do ub l e ) 中, s 、 e xp 和 fr a c 字段分别为 1 位、k =

    11 位和 n = 52 位, 得到一个 64 位的表示。

    单精度

    31 30 23 22

双精度

63 62 52 51 32

frac(31:0)

图 2-32 标 准 浮点格式(浮点数由 3 个字段表示。两种最常见的格式是它们被封装到 32 位(单精度)和64 位(双精度)的字中)

给定位 表示, 根据 e x p 的值, 被编码的值可以分成三种不同的情况(最后一种情况有两个变种)。图2-33 说明了对单精度格式的情 况。

  1. 规格化的
  2. 非规格化的

3a. 无穷大

3b.NaN

日粗巨冈荆咽 ¥-0

图 2-33 单精度浮点数值的分类(阶码的值决定了这个数是规格化的、非规格化的或特殊值)

情况 1 : 规格化的值

这是最普遍的 情况。 当 e xp 的位模式既不全为 0 ( 数值 0 ) , 也不全为 1(单精度数值为

255, 双精度数值为 2047) 时, 都属于这类情况。在这 种情况中,阶码 字段被解释为以偏置

(biased) 形式表示的 有符号整数。也 就是说, 阶码的值是 E = e- Bias , 其中 e 是无符号数,

其位表示为 ek -1 …e心 , 而 Bias 是一个等于 2- k l -1 ( 单精度是 127 , 双精度是 1023) 的偏置

值。由此 产生指数的取值范围, 对千单 精度是—126 ~ + 127, 而对于双精度是—1022 ~

+ 1023。

小数字段 fr a c 被解释为描述小数值 f , 其中 o::;;;;J < I. 其二进制表示为 0. f n- 1 …

!1!0• 也就是二进制小数点在最高 有效位的左边。尾数定义为 M = l + J 。有时 , 这种方式 也叫做隐 含的以 1 开头的 ( implied leading 1 ) 表示 , 因为我们可以 把 M 看成一个二进制表 达式为 l. f n - Jf n- 2 … 儿的数字。既 然我们总是能够调整阶码 E , 使得尾数 M 在范围 1:::;;; M< 2 之中(假设没有溢出), 那么这种表示方法是一种轻松获得一个额外精度位的技巧。既然第一位总是 等千 1, 那么我们就不需要显式地表示它。

情况 2 : 非规格化的值

当阶码域 为全 0 时, 所表示的数 是非规 格化形 式。在这种情况下, 阶码值是 E = l ­

Bias, 而尾数的 值是 M= f , 也就是小数字段的 值, 不包含隐含的开头的 1。

m 对千非规 格化值 为什 么要这样设 置偏置值

使阶码值为 l - Bia s 而不 是简单的—Bia s 似乎是 违反直觉的 。我们将很快看到 , 这种方式 提供了 一种从 非规 格化值平滑转换到规 格化值的方法 。

非规格化数有 两个用途。首先, 它们提供了一种表示数值 0 的方法, 因为使用规格化

数,我们必须总 是使M诊1, 因此我们就不能表示 0。实际上,十 0. 0 的浮点表示的位模式为全 0: 符号位是 o, 阶码字段全为 0( 表明是一个非规格化值), 而小数域也全为 o, 这就得到

M= f = O。 令人奇怪的是, 当符号位为 1 , 而其他域全为 0 时, 我们得到值— 0. 0。根据

I庄 E 的浮点格式, 值+ o. o 和—0. 0 在某些方 面被认为是不同的 , 而在其他方面是相同的。非规格化数的 另外一个功能是表示那些非常接近于 0. 0 的数。它们提供了一种属性,

称为逐渐 溢出 ( grad ual underflow) , 其中, 可能的数值分布均匀地接近于 0. 0。情况 3 : 特殊值

最后一类数值是当指阶码全为 1 的时候出现的。当小数域全为 0 时, 得到的值表示无穷,当 s = O 时是十= , 或者当 严 1 时是一~ 。当 我们把两 个非常大的数相乘, 或者除以零时,无穷能 够表示溢出的结果 。当小数域为非 零时, 结果值被称为 " NaN” , 即“不是一个数( Not a Number)” 的缩写。一些运算的结果不能是实数或无穷, 就会返回这样的 NaN 值, 比如当计算F 了或~ —~时。在某些应用中, 表示未初始化的数据时 , 它们也很有用处。

2. 4. 3 数字示例

图 2-34 展示了一组数值,它 们可以用假定的 6 位格式来表示, 有 k = 3 的阶码位和n =

2 的尾数位。偏置量是 23- J —1 = 3。图中的 a 部分显示了所有可表示的 值(除了 Na N ) 。两个无穷值在两个末 端。最大数量值的规 格化数是土14 。非规 格化数聚集在 0 的附近。图 的

b 部分中 ,我 们只展 示了介于— 1. 0 和十1. 0 之间的数值,这 样就能够看得更加清楚 了。

两个零是特殊的非 规格化数。可以观察到, 那些可表示的 数并不是均匀分布的一 越靠近原点处它们越稠密。

n 众

-oc

分 志 * 金

-10

-5 0 +5

七 众

+10

.. …. 口

+ oo

I • 非规格化的

… 规 格化. 的..

o 无-

穷. J

^ , 山

-1 -0.8

血 , 血 血 , 血 ^

—0 .6 -0.4

a ) 完整范围

血..

- 0 . 2

. .,.

+ 0.2

血 血 I • .t,. I 心

+0.4 +0.6

么 , 心

+0.8

F 格化的 … 规格化的 a 无 穷 l

b) 范围在- 1.0 ~ +1.0的数值

图 2-34 6 位浮点格式可表示的值( k = 3 的阶码位和 11= 2 的 尾数位。偏 置量 是 3 )

图 2-35 展示了假定的 8 位浮点格式的示例,其 中 有 k = 4 的阶码位和 n = 3 的 小数 位。偏置量是 2-1 1 - 1 = 7。图被分成了三个区域,来 描 述三类数字。不同的列给出了阶码字段是如何编码阶码 E 的,小 数 字段是如何编码尾数 M 的, 以 及 它 们 一 起 是 如何形成要表示的值 V = 2E X M 的。从 0 自身开始,最 靠 近 0 的是非规格化数。这种格式的非规格化数的

E = l —7 = —6 ,

1

得到权沪=6-4 °

小数J 的值的范围是o,

7

— ,从 而得到数 V 的

8

范围是 o -1 x -7 = 7

64 8 512°

描述 位表示

指数 小数

e E 2£ f M

。 0 0000 000 。-6 I i 8

最小的非规格化数 0 0000 001 -6 I

0 0000 010 。-6

召 k k

忐 i i

0 0000 011 -6 I

召 i i

最大的非规格化数 00000111 。-6 I 7 7

i l

#

I 9

8 8

14

0 0110 Ill 6 - 1 ½ 。 孕

I 00111 000 7

l 8 I

。 l k 9

2

8

i #

7

8

图 2-35 8 位浮点 格式的非负值示例( k = 4 的 阶码位的和 n = 3 的小数位。偏置员是 7)

这种形式的最小规格化数同样有 E = l - 7 = -6, 并且小数取值范围也为 o,

... ,

7

一。然而,

8

尾数在范围 1 + 0 = 1 和 1 +

7 15

—8 =一8 之间

得出数 V

8 1 15

在范围一 =— 和一-之间。

512 64 512

8

可以观察到最大非规格化数百歹和最小规格化数可石之间的平滑转变。这种平滑性归功

于我们 对非规格化数的 E 的定义。通过将 E 定义为 1— Bia s , 而不是—Bia s , 我们可以补偿非规格 化数的尾数没有隐含的开头的 1。

当增大 阶码时, 我们成功地得到更大的规格化值, 通过 1. 0 后得到最大的规格化数。

这个数具有 阶码 E = 7 , 得到一个 权 2E = 1 28。小 数等 千—8 得到尾数

15

M = -8 。因此, 数 值

是 V= 240 。超出这个值就会溢出到十~ 。

这种表示 具有一个有趣的属性, 假 如 我们将图 2-35 中 的值的 位 表达式解释为无符号整数, 它们就是按升序排列的, 就 像 它 们 表示的浮点数一样。这不是偶然的 IEEE 格式如此设计 就是 为了浮点数能够使用整数排序函数来进行排序。当处理负数时,有 一 个 小的难点, 因为它们有开头的 1 , 并且它们是按照降序出现的,但是不需要浮点运算来进行比较也能解决 这个问 题(参见家庭作业 2. 84 ) 。

区i 练习题 2. 47 假设一个基于 IE EE 浮点格式的 5 位浮点表 示, 有 1 个符 号位、2 个阶

码位 ( k = 2) 和两个 小数位( n = 2 ) 。 阶码偏 置 量是 2- 2 1 —l = l 。

下表 中列举 了这个 5 位浮点表示的 全部非负 取值范围。 使用 下面的条件, 填写 表格中的空白项:

e: 假定阶码 字段 是一个 无符号整数所表 示的 值。

E: 偏置之后的阶码值。

2气 阶码的权重。

J: 小数值。

M: 尾数的值。

沪 X M : 该数(未归 约的)小数值。

V: 该数归约 后的 小数值。

.十进制 : 该数的 十进制表 示。

写 出 2气 J、M、2E X M 和 V 的值, 要 么是 整数(如果可能的话),要 么是 形如王的小数 , 这里 y 是 2 的幕。标注为 “一 ” 的条目不用填。

eEfM2ExMV十进制
0 00 00
0 00 01
0 00 10
0 00 11
0 01 00
0 01 01II¾¾¾¾1.25
0 01 10
0 01 11
0 1 0 00
0 10 01
0 1 0 1 0
0 1 0 11
0 11 00
0 11 01
0 1110
0 1111

图 2-36 展示了一些重要的单 精度和双精度浮点 数的表示和数字值。根据图 2-35 中展示的 8 位格式, 我们能够看出有 K 位阶码 和 n 位小数的浮点表示的一般属性。

描述expfr a c单精度双精度
十进制十进制
。 最小非规格 化 数00· · ·00 00· · ·000···00 0 - · · 01。值 2 - 23 X 2-126 (J-E:)xz-126 Jx 2-126 I X 2° (2 - E:) X 21270.0 \.4 X 10- 45 J.2 X J0- 3S J.2 X 10- 38 1.0 3.4 X 1038。值 z-52 X z-1022 (I - c ) X z-1022 I x 2-1022 I X 2° (2-1,) x 210230.0 4.9 X 10-324 2.2 X 10- 308 2.2 X JO- JOS 1.0 J.8 X JQJOS
最 大非规格化数00· · ·00I· ··11
最小规格化数00· ·· 010 .. · 00
101· ·· II-0 ·· 00
最大规格化数I I .. ·IOI .. ·II

图 2-36 非 负 浮 点 数 的 示 例

  • 值+ o. o 总 有一个全为 0 的位表示。
  • 最小的正非规格化值 的位表示 , 是由最低有效位为 1 而其他 所有位为 0 构成的。它具有小数(和尾数)值M = f= 2- • 和阶码值 E = - 2k- ) + 2。因此它的数字值是

V= z - n- -2’

’ +2 。

  • 最大的非规格化值的位模式是由全为 0 的阶码字段和全为 1 的小数字段组成 的。它

有小数(和尾数)值M = f = l -

-2 " ( 我们写成 1 - c) 和阶码值 E = — zk- 1 + z。因此,

数 值 V = o - z- • ) x -z 2 +2 , 这仅比最小的规格化值小一点。

  • 最小的正规格化值的位模式 的阶码字段的最低有效位为 1, 其他位全为 0 。它的尾

数值 M = l , 而阶码值 E = — z- k

1 + z 。 因此, 数值 V = -z l- 1 +2 .

  • 值 1. 0 的位表示的阶码字段除 了最高有效位等千 l 以外, 其他位都 等于 0。它的尾数值是 M = l , 而它的阶码值是 E = O。

  • 最大的规格化值的位表示的符号 位为 o, 阶码的最低有效位等千 o, 其他位等于 1。

    它的小数值 f = l - -z • , 尾数 M = z - -z • ( 我们写作 2—c) 。它 的 阶码值 E = zk-_ 1

    1’ 得到数值 V = ( Z—2一") X 22 -I = (1 — z - •- l ) X2 ;-2 i 0

    练习把一些整数值转 换成浮点 形式对理解浮点 表示很有用。例如, 在图 2-1 5 中我们看到 1 2 345 具有二进 制表示[ 11 000000111 001 ] 。通过将二进制小数点左移 1 3 位, 我们 创

    建这个数的一个规格化表示 ,得到 1 2345 = 1. 100000011 10012 X 21 3 。 为了 用 IEEE 单精度 形式来编码, 我们丢弃开头的 1’ 并且在末尾增 加 10 个 o, 来构造小数字段,得到二进制表示[ 10000001110010000000000] 。为了构造阶码字段, 我们用 13 加上偏置扯 127 , 得到

    140, 其二进 制 表示 为[ 100011 00 ] 。加 上 符 号 位 0 , 我们就得到二进制的浮点表示

    [ 0100011001000000111010000000000] 。回想一下 2. 1. 3 节, 我们观察到整数值 1 23 45

    ( Ox 303 9) 和单精度浮点值 1 23 45 . 0( 0x 4640E400 ) 在位级表示上有下列关系:

    0 0 0 0 3 0 3 9

    00000000000000000011000000111001

*************

4 6 4 0 E 4 0 0

01000110010000001110010000000000

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

练习题 2. 48 正 如 在 练 习 题 2. 6 中 提 到 的, 整 数 3 510 593 的 十 六 进 制 表 示 为

Ox00359141, 而单 精度 浮点数 351 05 93 . 0 的十 六进 制表 示为 Ox 4A5 645 0 4。 推导 出这个浮点表示,并解释整数和浮点数表示的位之间的关系。

练习题 2. 49

  1. 对于一种具 有 n 位小 数的 浮点格 式, 给出不能准 确描 述的 最小正 整数的公 式(因为要想 准确 表 示它需要 n + l 位小 数)。假设 阶码 字 段长度 K 足够大, 可以 表 示的 阶码范围不会限制这个问题。
  2. 对于 单精 度格 式( n = 23) , 这个整数的数字值是多少?
2. 4. 4 舍入 #

因为表示方法限 制了浮点数的范围和精度, 所以浮点运算只能近似地表示实数运算。因此, 对于值 x , 我们一般想用一种系统的方法, 能够找到“ 最接近的" 匹配值 x ’’ 它可 以用期 望的浮点形式 表示出来。这就是舍入( ro unding ) 运算 的任务。一个关键问题是在两个可能值的中间确定舍入方向。例如, 如果我有 1. 50 美元, 想把它舍入到最接近的美元

数,应 该是 1 美元还是 2 美元呢?一种可选择的方法是维持实际数字的下界和上界。例如,我 们可以 确定可表示的值 x一和 工+ ,使 得 工的 值位于它们之间: x一冬 工::;;x勹 IEEE

浮点格式定义了四种不同的舍入方式。默认的方法是找到最接近的匹配,而其他三种可用 于计算上界和下界。

图 2-37 举例说明了四种舍入方式, 将一个金额数舍入到最接近的 整数美元数。向偶数舍入 ( ro und- to-e ven ) , 也被称为向最接近的值舍入( ro und - to- nea res t ) , 是默认的方式, 试图找到一个 最接近的 匹配值。因此, 它将 1. 40 美元舍入成 1 美元 , 而将 1. 60 美元舍入成 2 美元, 因为它们是 最接近的整数美元 值。唯一的设计决策是确定两个可能结果中间数值的舍入效果。向偶数舍入方式采用的方法是:它将数字向上或者向下舍入,使得结果的 最低有效数字是偶数 。因此,这 种方法将 1. 5 美元和 2. 5 美元都舍入成 2 美元。

方式1.401.601.502.50-1.50
向偶数舍入1222-2
向零舍入III2-1
向下舍入III2-2
向上舍入2223一1

图 2-37 以美元舍入为例说明舍入方式(第一种方法是舍入到一个最接近的值, 而其他三种方法向上或向下限定结果,单位为美元)

其他三种方式产生实际值的确界 ( g ua ra n teed bo und ) 。这些方法在一些数字应用中是很有用的 。向零舍入方式把正数向下舍入, 把负数向上舍入, 得到值x, 使 得1 分 l < lx l 。向下舍入方式 把正数和负数都向下舍入, 得到值 x一 ,使 得 x- < x。向 上舍入方式把正数和负数都向上舍 入, 得到值 x十 , 满足 x< x勹

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

在 50%的时间里,它 将 向 上舍入,而 在 50 %的时间里,它 将 向 下 舍 入 。

在我们不想舍入到整数时,也可以使用向偶数舍入。我们只是简单地考虑最低有效数字是奇数还是偶数。例如,假设我们想将十进制数舍人到最接近的百分位。不管用那种舍入方式, 我们都将把 1. 2349999 舍 入到 1. 23, 而 将 1. 2350001 舍入到 1. 24, 因为它们不

是在 1. 23 和 1. 24 的正中间。另一方面我们将把两个数 1. 2350000 和 1. 2450000 都舍入到

  1. 24, 因为 4 是偶数。

    相似地,向 偶 数 舍 入法能够运用在二进制小数上。我们将最低有效位的值 0 认为是偶数,值 1 认 为是奇数。一般来说,只有对形如 XX… X. YY… Yl OO… 的 二 进 制 位 模 式 的 数 , 这 种 舍 入 方 式 才 有 效 ,其 中 X 和 Y 表示任意位值,最 右 边的 Y 是要 被舍入的位置。只有这种位模式表示在两个可能的结果正中间的值。例如,考虑舍入值到最近的四分之一的问

    题(也就是二进制小数点右边 2 位)。我们 将 10. 000112 ( 2 点 )向下 舍 入 到 10. 002 (2)’

  2. 01102(气3)向 上舍 入 到 1 0. 012 ( 2 丁 ),因为这些值不是两个可能值的正中间值。我们将

    10. 111002 ( 2 百 )向上舍 入成 11. 002 ( 3) , 而 10. 101002 ( 2 旬 向 下舍 入成 10. 102 (22— )’ 因为

    这些值是两个可能值的中间值,并且我们倾向于使最低有效位为零。

    ; 练习题 2. 50 根据舍入到偶数规则,说明如何将下列二进制小数值舍入到最接近的 二分之一(二进 制小数点右边 1 位)。对每种情况, 给出舍入前后的数 字值 。

    A. 10. 0102

    B. 10. 0112

    C. 10. 1102

    D. 11. 0012

    ; 练习题 2. 51 在 练 习 题 2. 46 中 我 们 看 到 , 爱 国 者 导 弹 软件 将 0. 1 近似 表 示 为 x = 0. 00011 00110 01100110011002 。 假设使用 IEE E 舍入到 偶 数 方 式 来确定 o. 1 的 二进制小数点右边 23 位的近似表 示 x ’ 。

    1. x ’ 的二进制表 示是 什么?

    2. x’-o. 1 的十进制表 示的 近似值是什么?

    3. 运行 100 小时后 , 计算 时钟值会有 多少 偏 差?

    4. 该程序对飞毛腿导弹位置的预测会有多少偏差?

      笠 练习题 2. 52 考虑下列 基于 IEE E 浮 点 格式 的 7 位 浮 点 表 示。 两 个格 式 都 没 有 符 号位——它们只能表示非负的数字。

      1. 格式 A
        • 有 k = 3 个阶码位。 阶码 的偏 置值是 3。
        • 有 n = 4 个小数位。
      2. 格式 B
        • 有 k = 4 个阶码位。 阶码 的偏置值是 7。

        • 有 n = 3 个小数位。

          下面给出 了一 些格 式 A 表 示的位模 式, 你的 任 务是将它 们 转 换成格式 B 中最接近的值。如果 需要, 请使 用 舍入到 偶 数 的舍入原 则。 另 外, 给出 由格式 A 和格 式 B 表 示的 位模式对应 的数 字的 值。 给出整数(例如 17 ) 或 者小数(例如 17 / 64 ) 。

格式A格式B
位 011 0000 101 1110 010 l 001 110 1111 000 0001值 1位 0111 000值 I
  1. 4. 5 浮点运算

    IEEE 标准指定了一个简单的规则, 来确定诸如加法和乘法这样的算术运算的结果。把浮点 值 x 和y 看成实数, 而某个运算O 定义在实数上, 计算将产生 R ound ( x0 y ) , 这是对实际运算的精确结果进行舍入后的结果。在实际中,浮点单元的设计者使用一些聪明的 小技巧来避免执行这种精确的计算,因为计算只要精确到能够保证得到一个正确的舍入结 果就可以了 。当参数中有一个是特殊值(如—0 、—~或 N a N ) 时, IE E E 标准定义了一些使之更合理 的规则。例如,定 义 1 / - 0 将产生一= , 而定义 1; + 0 会产生十~ 。

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

    前面我们看到了整数(包括无符号和补码)加法形成了阿贝尔群。实数上的加法也形成了 阿贝尔群, 但是我们必须考虑舍入对这些属性的影响。我们 将 x + 勺定义为 Round ( x + y ) 。这个运算的定 义针对 x 和y 的所有取值, 但是虽然 x 和 y 都是实数, 由于溢出 ,该 运算可能得到无穷 值。对于所有 x 和y 的值, 这个运算是可交换的 ,也 就是说 x +r y = y +r x 。 另

    一方面 , 这个运算是 不可结合的。例如, 使用单精度浮点 , 表达式 (3. 14+lel0) - l el O 求值得到 o. o iz寸为舍入, 值 3. 14 会丢失。另一方面, 表达式 3. 1 4 + (l e l 0- l e l 0 ) 得出值

  2. 14。作为阿贝尔 群, 大多数值在浮点加法下都有逆元,也 就是说 x +r — x = O。 无穷(因 为十 = - = = N a N ) 和 N a N 是例外情况, 因为对于任何 X , 都有 N a N + 1 x = N a N 。

    浮点加法不具有结合性,这是缺少的最重要的群属性。对于科学计算程序员和编译器编写者来说,这具有重要的含义。例如,假设一个编译器给定了如下代码片段:

编译器可能试图通过产生下列代码来省去一个浮点加法:

t “‘b + c; xaaa+t; y”’t + d;

然而, 对千 x 来说,这个 计算可能会 产生与原始值不同的值, 因为它使用了加法运算的不同的结合方式。在大多数 应用中, 这种差异小得无关紧要。不幸的是,编译 器无法知道在效率和忠实于原始程序的确切行为之间,使用者愿意做出什么样的选择。结果是,编 译器倾向千保守,避免任何对功能产生影响的优化,即使是很轻微的影响。

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

浮点乘法也遵循通常乘法所具有的许多属性。我们定义 X * f y 为R ou nd ( x X y ) 。这个

运算在乘法中是 封闭的(虽然可能产生无穷大或 Na N ) , 它是可交换的,而且它的乘法单位元

为 1. 0。另一方面, 由于可能发生溢出, 或者由于舍入而失去精度,它 不具有可结 合性。例如,单 精度浮点情况下, 表达式 (l e 2 0*l e 2 0 ) * l e - 20 求值为+ = , 而 l e 20* (l e 20 * l e - 20 ) 将得出 l e 20。另外, 浮点乘法在加法上不具备分配性。例如, 单精度浮点 悄况下, 表达式

l e 2 0 * (l e 2 0 - l e 2 0 ) 求值为 o. o, 而 l e 20* l e 2 0- l e 2 0* l e 20 会得出 N a N 。

另一方面,对于任何a 、b 和 c , 并且a 、b 和 c 都不等千 N a N , 浮点乘法满足下列单调性:

a b 且 C 0 a* 1 c b * 1c

a b 且 c 冬 0 a * 气< b* re

此外, 我们还可以保证, 只要 a# N a N , 就有 a *r a O。 像我们先前所看到的, 无符号或补码的乘法没有这些单调性属性。

对于科学计算程序员和编译器编写者来说,缺乏结合性和分配性是很严重的问题。即使为了在三维空间中确定两条线是否交叉而写代码这样看上去很简单的任务,也可能成为一个很大的挑战。

  1. 4. 6 C 语言中的浮点数

    所有的 C 语言版本提供了两种不同的浮点数据类型: fl oa t 和 doub l e。在支持 IEEE 浮点格式的机器上.这些数据类型就对应千单精度和双精度浮点。另外,这类机器使用向偶数舍入 的舍入方式。不幸的是, 因为C 语言标准不要求机器使用 IEEE 浮点, 所以没有标准的方法来改变舍 入方式或者得到诸如一0 、 十OO 、 - oo 或者 N a N 之类的特殊值。大多数系统提供i nc l ud e ( ’ . h ’ ) 文件和读取这些特征的过程库, 但是细节随系统不同而不同。例,如当程序文件中出现下列句子时, G NU 编译器GCC 会定义程序常数INF INI TY( 表示十oo ) 和 NAN( 表示 Na N ) :

    #define _GNU_SOURCE 1

    #include <math.h>

    霆 练习题 2. 53 完成 下列 宏定 义, 生成双 精度值 + = 、—CX) 和 0 :

    #define POS_INFINITY

    #define NEG_INFINITY

    #define NEG_ZERO

不 能使 用 任何 i n c l u d e 文件(例如 ma 七h . h ) , 但你能利用这样一个事实:双精度能够表 示的最 大的 有限 数, 大 约是 1. 8 X 10308o

当在 i n 七、 fl o a t 和 d o u b l e 格式之间进行强制类型转换时, 程序改 变数值和位 模式的原则如下(假设i n t 是 32 位的):

  • 从 i n t 转换成 f l o a t , 数字不会溢出,但是可能被舍入。
    • 从 i n t 或 fl o a t 转换成 do u b l e , 因为 d o u b l e 有更大的范围(也就是可表示值的范围),也有更高的精度(也就是有效位数),所以能够保留精确的数值。
      • 从 d o u b l e 转换成 f l o a t , 因为范围要小一些,所以值可能溢出成十~或—~。另外,由于精确度较小,它还可能被舍入。

      • 从 fl o a t 或者 d o u b l e 转换成 i n t , 值将会向零舍入。例如, 1. 999 将被转换成 1’ 而—1. 99 9 将被转换成—1 。进一步来说, 值可能会溢出。C 语言标准没有对这种情况指定固定的结果。与 I nt el 兼容的微处理器指定位模式[ 10 …00 ] ( 字长为 w 时的 T M i n ..,,) 为整数不确定 ( in t eg e r in d ef init e ) 值。一个从浮点数到整数的转换 , 如果不能为该浮点数找到一个合理的整数近似值,就会产生这样一个值。因此,表达式

        (int) +lel O 会得到- 21 4 83 6 48 , 即从一个正值变成了一个负值。

fm Ariane 5— 浮点溢出的高昂代价

将大的 浮点数转换成整数是 一种常见的程序错误 来源。1 996 年 6 月 4 日 , A r ia ne 5

,火箭初 次航 行 , 一 个错误 便 产 生 了 灾难 性 的后 果。发 射后 仅 仅 37 秒钟 , 火 箭 偏 离 了 它的飞行 路 径 , 解 体 并 且 爆 炸 。 火箭上 栽有价值 5 亿 美元的 通信 卫 星。

后 来的调 查[ 73 , 33] 显 示 , 控 制 惯性 导航 系统 的 计 算 机 向 控 制 引 擎 喷 嘴 的 计 算 机 发送了一个无效数据。它没有发送飞行控制信息,而是送出了一个诊断位模式,表明在将 一个 64 位 浮点数 转换成 1 6 位 有 符 号 整数 时 , 产 生 了 溢 出。

溢 出的 值 测 量的 是 火箭的 水 平速 率 , 这 比 早 先 的 A ria ne 4 火 箭 所 能 达到 的 速 度 高 出

r 了 5 倍 。 在 设 计 A r ia ne 4 火 箭 软 件 时 , 他 们 小 心 地 分 析 了这 些数 宇值 , 并且 确 定 水 平 速率决 不会 超 出一 个 1 6 位 数 的 表 示 范 围。 不 幸 的 是 , 他 们 在 A riane 5 火箭的 系 统 中 简 单地重用了这一部分,而没有检查它所基于的假设。

练习题 2. 54 假 定 变 量 x 、 f 和 d 的 类型分别是 i nt 、fl oa t 和 do ub l e。 除 了 f 和 d 都不能 等于十~ 、 一~ 或 者 Na N , 它们 的值是 任意 的。 对于 下 面每 个 C 表 达 式, 证 明 它总是 为真(也就 是 求 值 为 1) ’ 或 者 给 出一个使 表达 式不 为 真 的 值(也就 是 求 值 为 0 ) 。

  1. x == (int) (double) x

  2. x == (int) (fl oa 七 ) x

  3. d == (double) (fl o a 七 )d

  4. f == (fl o a 七)(double) f

    E. f == - (-f) F.1.0/2==1/2.0 G. d*d >= 0. 0

    H. (f+d)-f ==d

  5. 5. 小结

    计算机将信息编码 为位(比特), 通常组织成字 节序列 。有不同的编码方式用来表示整数、实数和字符串。不同的计算机模 型在编码数字 和多字节数据中的字节顺 序时使用不同的约定。

    C 语言的设计可以包容多种不同 字长和数字编码的实现。64 位字长的机器逐渐普及 ,并 正在取代统治市场长达 30 多年的 32 位机器。由千 64 位机器也可以运行 为 32 位机器编译的 程序, 我们的 重点就放在区分 32 位和 64 位程序, 而不是机器本身。 64 位程序的优势是可以突破 32 位程序具有的 4GB地址限制。

    大多数机器对整数使用补码 编码,而对 浮点数使用 IEEE 标准 754 编码。在位级上理解这些编码, 并且理解算术运算的数学特性,对于想使编写的程序能在全部数值范围上正确运算的程序员来说,是很重 要的。在相同 长度的无符 号和有符号整数之间进行强制类型转换时,大多 数 C 语言实现遵循的 原则是底层

    的位模式不变。在补码 机器上 , 对于一个 w 位的值, 这种行为是由 函数 T 2U 立 和 U 2T w 来描述的。C 语言隐式的强制类型转换会出现许多程序员无法预计的结果,常常导致程序错误。

    由于编码的 长度有限 , 与传统整数和实数运算 相比,计 算机运算具有非常不同的属性。当超出表示范围时 ,有限长度能够引起数值溢出。当浮点数非常接近于 0. o, 从而转换成零 时, 也会下溢。

    和大多数 其他程序语言一样,C 语言实现的有限整数 运算和真实的整数运算相比 , 有一些特殊的属性。例如,由 于溢出,表达式 x *x 能够得出负数。但是,无 符号数和补码的运算都满足整数运算的许多其他属性 , 包括结合律、交换律和分 配律。这就允许编译器做很多的 优化。例 如,用 (x <<3 ) - x 取代表达式 7*x 时,我们 就利用了结合律、交换律和分配律的属性, 还利用了移位 和乘以 2 的幕之间的关 系。

    我们已经 看到了几 种使用位级运算和算术运算组合的聪明 方法。例如,使 用 补码运算, ~x +l 等价千议。另外一个例子,假设我们想要 一个形如[ O, …, O, l, ··· , 1] 的 位模式,由 w —k 个 0 后面紧 跟着 K

个1 组成。这些位模式有助于掩码运算。这种模式能够通过 C 表达式 (l « k) 一1 生 成 ,利 用的是这样一个属性,即 我们想要的位模式的数值为2• - 1。例如,表 达式 (1« 8 ) - 1 将 产 生 位 模 式 OxFF 。

浮 点 表 示通过将数字编码为 x X 护 的 形 式 来 近似地表示实数。最常见的浮点表示方式是由 IEEE 标准 754 定义的。它提供了几种不同的精度,最 常 见 的 是 单 精 度 ( 32 位)和双精度 ( 64 位)。IE EE 浮点 也能够表示特 殊值 + = 、—CX) 和 Na N。

必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且并不遵守普遍的算术属 性,比如结合性。

参考文献说明

关于 C 语言的参考书[ 45 , 61] 讨论了不同的数据类型和运算的 属性。(这两本书中 ,只 有 Stee le 和Harbison 的书[ 45] 涵盖了 ISO C99 中的新特性。目前还没有看到任何涉及 ISO Cl l 新特性的书籍。)对于精确的字长或者数字编码 C 语言标准没有详细的定义。这些细节是故意省去的,这 样 可以在更 大范围的不同机器上实现 C 语言。已经有几本书[ 59 , 74] 给了 C 语言程序员一些建议,菩 告 他 们关于溢出、隐式强制类型转换到无符号数,以及其他一些已经在这一章中谈及的陷阱。这些书还提供了对变昼命名、编 码风格和代码测试的有益建议。Seacord的书[ 97] 是关 千 C 和 C + + 程 序中的安 全问 题 的 , 本书结合了 C 程序的有关信息,介 绍 了如何 编译 和执 行 程 序,以 及 漏洞是如何造成的。关于 Java 的 书(我们推荐 Java 语言的创 始人 James Gosling 参与编写的一本书[ 5] ) 描述了 Java 支持的数据格式和算术运算。

关于逻辑设计的书[ 58 , 116] 都 有关 于编码和算术运算的 章节 , 描 述 了 实 现 算 术 电 路 的 不 同 方式。

Overton 的关千 IEEE 浮点数的书[ 82] , 从 数 字应用程序员的角度,详 细描述了格式和属性。

家庭作业

拿 2. 55 在你能够访问的不同机器上,使 用 s how_b yt e s ( 文件 s how- byt e s . c ) 编 译 并 运行示例代码。确定这些机楛使用的字节顺序。

  • 2. 56 试着用不同的示例值来运行 s ho w_byt e s 的 代 码 。
    • 2 . 57 编 写 程 序 s how_s ho r t 、 s how_l ong 和 s how_do ub l e , 它们分别打印类型为 s hor t 、l ong 和 doub­ l e 的 C 语言对象的字节表示。请试着在几种机器上运行。

      拿 拿 2. 58 编写过程 i s _l i 七七l e _e nd i a n , 当在小端法机器上编译和运行时返回 1, 在大端法机器上编译运行时则返回 0。这个程序应该可以运行在任何机器上,无 论 机 器 的 字 长是多少。

      申 2. 59 编写一个 C 表达式,它 生 成 一 个 字 , 由 x 的 最 低 有 效 字节和 y 中剩下的字节组成。对于运算数 x

      =Ox8 9ABCDEF 和 y=Ox 76543210, 就得到 Ox 765432EF。

      申 拿 2. 60 假设我们将一个 w 位的字中的字节从 0 ( 最低位)到w/ 8 - 1 ( 最高位)编号。写出下面 C 函数的代码,它 会返回一个无符号值,其 中 参 数 x 的 字 节 i 被 替 换 成 字 节 b :

      unsigned replace_byte (unsigned x, inti, unsigned char b);

以下示例,说明了这个函数该如何工作:

replace_byte(Ox12345678, 2, OxAB) –> Ox12AB5678 replace_byte(Ox12345678, 0, OxAB) –> Ox123456AB

位级整数编码规则

在接下来的作业中, 我们特意限制了你能使用的编程结构,来 帮你更好地理解 C 语言的位级、逻辑和算术运算。在回答这些问题时,你的代码必须遵守以下规则:

  • 假设
    • 整数用补码形式表示。
      • 有符号数的右移是算术右移。
        • 数据类型 i nt 是 w 位长的。对于某些题目,会 给定 w 的值,但 是在其 他情况下 ,只 要 w 是 8 的整 数倍 ,你的 代 码 就 应该 能 工 作 。你 可以用表达式 s iz e of (i n七)<<3 来计算 w。
  • 禁止使用
    • 条件语句( if 或者?:)、循环、分支语句、函数调用 和宏调用。

    • 除法、模运算和乘法。

    • 相对比较运算(<、>、<=和>=)。

      ·允许的运算

    • 所有的位级和逻辑运算。

  • 左移和右移 , 但是位移量只能 在 0 和,互- 1 之间。
    • 加法和减法。

    • 相等(==)和不相等(! =)测试。(在有些题目中,也不允许这些运算。)

    • 整型常数 IN T_MIN 和 IN T_MAX。

    • 对 i n t 和 un s i g ne d 进行强制类型转换 ,无论是显式的 还是隐 式的。

      即使有这些条件的限制,你仍然可以选择带有描述性的变扯名,并且使用注释来描述你的解决方案 的逻辑,尽量提高代码的 可读性。例如 , 下面这段代码从整数参数 x 中抽取出最高有效 字节:

      I• Get most significant byte from x•I int get_msb(int x) {

      /• Shift by 切 一 8 •I

      int shift_val = (sizeof(int)-1)«3;

      I• Arithmetic shift•/

      int xright = x >> shift_val; I• Zero all but LSB•/ return xright & OxFF;

•• 2. 61

** 2. fr2

** 2. 63

写一个 C 表达式 , 在下列描述的条件下 产生 1, 而在其他情况下得到 0。假设 x 是 i n t 类型。

A. X 的任何位都等千 l 。

8. X 的任何位都等 于 0。

  1. X 的最低有效字节中的 位都等于 1。

  2. X 的最高有效字节中的 位都等千 0。

    代码应该遵循位级整数编码规则,另外还有一个限制,你不能使用相等(==)和不相等(! =)

    测试。

    编写一个 函数 i n t _s h i f t s _ar e _ar i t hme t i c (), 在对 i nt 类型的 数使用算术右移的 机器上运行 时这个函数生成 1, 而其他情 况下 生成 0。你的代码应该可以 运行 在任何字长的机器上。在几 种机器上测试你的代码。

    将下面的 C 函数代码补充完整。函数 sr l 用算术右移(由值xs r a 给出)来完成逻辑 右移,后 面的其他操作不 包括右移或者除 法。函数 sr a 用逻辑右移(由值 xsr l 给出)来完成算术右移,后 面的其他操作不包 括右移或者除法。可以 通过计算 8*s i ze o f (i n t ) 来确定 数据类型 i n t 中的位数 w。位移量 k 的取值 范围为 O~ w - 1 。

    unsigned srl(unsigned x, int k) {

    I• Perform shift arithmetically•/ unsigned xsra = (int) x»k;

intsra(int x, int k) {

/• Perform shift logically•/ int xsrl = (unsigned) x >> k;

  • 2. 64 写出代码实现如下函数:

    /• Return 1 when any odd bit of x equals 1; 0 otherwise.

    Assume w=32•/

    int any_odd_one(unsigned x);

函数应该遵循位级整数编码规则 ,不 过 你 可以假设数据类型 i n t 有 w = 3 2 位。

:: 2. 6 5 写出代码实现如下函数:

/• Return 1 when x contains an odd number of 1s; 0 ot her 廿 i s e .

Assume w=32•/

int odd_ones(unsigned x);

函数应该遵循位级整数编码规则 ,不 过 你 可以假设数据类型 i n t 有 w = 3 2 位。你的代码最多只能包含 1 2 个算术运算、位运算和逻辑运算。

·: 2. 66 写出代码实现如下函数:

/*

  • Generate mask indicating leftmost 1 in x. Assume w=32.

  • For example, OxFFOO -> Ox8000, and Ox6600 –> Ox4000.

  • If x = 0, then return 0.

    */

    int leftmost_one(unsigned x);

函数应该遵循位级整数编码规则 ,不 过你可以假设数据类型 i n t 有 w = 3 2 位。你的代码最多只能包含 1 5 个算术运算、位运算 和逻辑运算。

提示:先 将 x 转换成形如[ O…011…l ] 的 位向量。

•• 2. 67 给你一个任务,编 写 一 个 过程 i n t _ s i ze _ i s _ 3 2 (), 当在一个 i n t 是 32 位的机器上运行时, 该 程序产生 1, 而其他情况则产生 0。不允许使用 s i ze o f 运算符。下面是开始时的尝试:

/• The following code does not run properly on some machines•/

2 int bad_i nt _s i z e _i s _32 0 {

3 /• Set most significant bit (msb) of 32-bit machine•/

4 int set msb = 1«31;

5 I• Shift past msb of 32-bit word•/

6 int beyond_msb = 1«32;

7

8 I•set_msb is nonzero when word size >= 32

9 be yond _ms b is zero when word s i ze <= 32•/

10 return set_msb && ! b e yond _ms b ;

11 }

当在 SUN SP ARC 这样的 32 位机器上编译并运行时, 这个过程返回的却是 0。下面的编译器信息给了我们一个问题的指示:

warning: left shift count >= width of type

  1. 我们的代码在哪个方面没有遵守 C 语言标准?

  2. 修改代码,使 得它在 i n t 至少为 32 位的任何机器上都能正确地运行。

  3. 修改代码,使得 它在 i nt 至少为 1 6 位的任何机楛上都能正确地运行。

    •• 2. 68 写出具有如下原型的函数的代码:

    /*

  • Mask with l east signficant n bits set to 1

  • Exampl e s : n = 6 –> Ox3F, n = 17 –> Ox1FFFF

  • Assume 1 <= n <= w

    •I

    int l o 仅 er _one _mas k ( i nt n);

函 数 应该 遵 循 位级整数编码规则。要注意 n = w 的情况。

•: 2. 69 写出具有如下原型的函数的代码:

/*

  • Do rotating left shift. Assume O <= n < w

  • Examples when x = Ox12345678 and w = 32:

    * n=4 -> Ox23456781, n=20 -> Ox67812345

    */

    u.ns 屯 ned rotate_left(u.nsigned x, int n);

函数应该 遵循位级整数 编码规则 。要注意 n = O 的情况。

.. 2. 70 写出具有如下原型的函数的代码:

/*

  • Return 1 when x can be represented as an n-bit, 2’s-complement

  • number; 0 otherwise

  • Assume 1 <= n <= w

    */

    int fits_bits(int x, int n);

函数应该遵循位级整数编码规则。

  • 2. 71 你刚刚开始在 一家公司工作, 他们要实现一组过程来操 作一个数据结构 , 要将 4 个有符号字节封装成一个 3 2 位 u n s i g ne d。一个字中的字 节从 0 ( 最低有效字节)编号到3 ( 最高有效 字节)。分配给你的任务是:为一个使用补码运算和算术右移的机器编写一个具有如下原型的函数:

    I* Declaration of data t ype 口 here 4 bytes are packed into an unsigned *I

    typedef unsigned packed_t;

I* Extract byte from word. Return as signed integer *I int xbyte(packed_t word, int bytenum);

也就是说, 函数会抽取 出指定的字节, 再把它符号扩展为一个 3 2 位 i n t 。你的前任(因为水平不够高而被解雇了)编写了下面的代码 :

I* Failed attempt at xbyte *I

int xbyte(packed_t “Word, int bytenum)

return (‘Word»(bytenum << 3)) & OxFF;

  1. 这段代码错在哪里?

  2. 给出函数的正确实现,只能使用左右移位和一个减法。

    •• 2. 72 给你一个任务 , 写一个函数, 将整数 v a l 复制到缓冲区 b u f 中, 但是只有当缓冲区中 有足够可用的空间时,才执行 复制。

    你写的代码如下:

    I* Copy integer into buffer if space is available *I I* WARNING: The following code is buggy *I

    void copy_int(int val, void *buf, int maxbytes) { if (maxbytes-sizeof(val) >= 0)

    memcpy(buf, (void*) &val, sizeof(val));

这段代码使用了库函数 me mc p y 。虽 然在这里用这个函数有点 刻意,因 为我们只 是想复制一个江比, 但是它说明了一种复制较大数 据结构的常见方法。

你仔细地测试了这段代码后发现 , 哪怕 ma x b y t es 很小的时候, 它也能把值复制到缓 冲区中 。

  1. 解释为什么代码中的条件测试总是成功。提示: s i ze o f 运算符返回 类型为 s i z e _ t 的值 。

  2. 你该如何重写这个 条件测试 ,使 之工作正确。

    .. 2. 73 写出具有如下原型的函数的 代码:

    I* Addition that saturates to TMin or TMax *I int saturating_add(int x, int y);

同正常的补码加法溢出的方式不同, 当正溢出时, 饱和加法返回 TMax , 负溢出时,返回

TMin。饱 和运算常常用在执行 数字信号 处理的程序中。你的函数应该遵循位级整数编码规则。

•• 2. 74 写出具有如下原型的函数的代码:

/• Determine whether arguments can be subtracted without overf l 叩 */

int tsub_ok(int x, int y);

如果计算 x- y 不溢出, 这个函数就返回 1。

·: 2. 75 假设我们想要计算 x • y 的完整的 2亿,位表示, 其中, x 和 y 都是无符号数,并 且运行在数据类型u n s i g ne d 是 w 位的机器上。乘 积的低 w 位能够用表达式 x * y 计算,所以 ,我 们只需 要一个具有下列原型的函数:

unsigned unsigned_high_prod(unsigned x, unsigned y);

这个函数计算 无符号变量 x • y 的高 w 位。我们使用一个具有下面原型的库函数:

int s i gn ed 上 i gh_pr od ( i nt x, int y);

它计算在 x 和y 采用补码 形式的情 况下, x • y 的 高 w 位。编写代码 调用这个过程, 以实现用无符号数为参数的函数。验证你的解答的正确性。

提示: 看看等式 ( 2. 18 ) 的推导中 ,有符 号乘积 x • y 和无符号乘积 x’ • y’ 之间的关系。

  • 2. 76 库函数 c a l l o c 有如下声明 :

    void•calloc(size_t nmemb, size_t size);

根据库文档:" 函数 c a l l o c 为一个数组分配内存, 该数组有 nme mb 个元素, 每个元素为 s i ze 字节。内存设置为 0 。如果 nmemb 或 s i ze 为 o, 则 c a l l o c 返回 NULL。”

编写 c a l l o c 的实现, 通过调用 ma l l oc 执行分配 , 调用 me ms e t 将内存设 置为 0。你的代码应该没有任何由算 术溢出引起的漏洞, 且无论数据类型 s i ze _ t 用多少位表示, 代码都应该正常工作。

作为参考 , 函数 ma l l o c 和 me ms e t 声明 如下 :

void•malloc(size_t size); void•memset(void•s, int c, size_t n);

•• 2. 77 假设我们有一 个任务: 生成一段代码 , 将整数 变量 x 乘以不同的常数因子 K 。为了 提高效率,我们想只使用十、一和<< 运算。对于下 列 K 的值,写 出执行乘法运算 的 C 表达式 ,每个 表达式中最多使用 3 个运算。

  1. K=l7
    1. K=-7

    2. K=60

      D. K= -112

      •• 2. 78 写出具有如下原型的函数的代码:

      /• Divide by power of 2. Assume O <= k < w 一 1 • I int divide_power2(int x, int k);

该函数要用正确的舍入方式计算 x / 2’ , 并且应该遵循位级整数编码规则。

•• 2. 79 写出函数 mu l3 d i v 4 的代码,对于整数参 数 x , 计算 3*x / 4 , 但是要遵循位级整数编码规则。你的代码计算 3*x 也会产生溢出。

•: 2. 80 写出函数 t hr e e f o ur t h s 的代码, 对于整数参数 x , 计算 3 / 4x 的值,向 零舍人。它 不会溢出。函

数应该遵循位级整数编码规则。

•• 2. 81 编写 C 表达式产生如下位模式,其中 a• 表示符号a 重复 K 次。假设一个 w 位的数 据类型。代码可以包含对参数 J 和 K 的引用,它 们分别表示 )和K 的 值, 但是不能使用表示 w 的参数。

  1. 1w- k ok

  2. ow- k- j1ko1

    , 2. 82 我们在一 个 i n t 类型值为 32 位的机器上运行程序。这些 值以补码形式表示, 而且它们都是算术右移的。u ns i g ne d 类型的值也是 32 位的。

    我们产生随机数 x 和 y, 并且把它们转换成无符号数,显示如下:

    /• Create some arbitrary values•/ int x “‘random() ;

    int y random() ;

    I• Convert to unsigned•/ unsigned ux = (unsigned) x; unsigned uy = (unsigned) y;

对千下列每个 C 表达式, 你要指出表达式是否 总是为 1。如果它 总是为 1, 那么请描述其中的数学原理 。否则,列 举出一个使它为 0 的参数示例。

A. (x <y ) = = (- x>- y )

B. ((x+y)«4) +y-x==l 7*y+l5*x

C. · x +· y+l = =’ (x+y )

D. (ux- u y ) = = - (unsigned) (y-x)

E. ( (x » 2 ) « 2 ) <=x

.. 2. 83 一些数字的二进制表示是由形如 o. y y y y y y …的无穷串组成的 , 其中 y 是一个 K 位的序列。例如,— 的二 进制 表示是 0. 01010101 … ( y = O l ) , 而— 的二进 制表示是 o. 00 11 00 11 0011 … ( y =

0011 ) 。

  1. 设 Y= B2队 ( y ) ’ 也就是说,这个数具 有二进制表示 y 。给出 一个由 Y 和 K 组成的公式表示这个无穷串的值。

    提示: 请考虑将二进制小数点右移 K 位的结果。

  2. 对于下列的 y 值, 串的数值是多少?

    ( a) l Ol

    ( b ) Oll O ( c) Ol OOll

    , 2. 84 填写下列程序的返 回值, 这个程 序测试它的 第一个参数是否小千或者等千第二个参数。假定函数f 2u 返回一个 无符号 32 位数字,其 位表示与它的浮点 参数相同。你可以假设两个参数都不是Na N。两种 o, + o 和一0 被认为是相等的。

    int float_le(float x, floaty) { unsigned ux = f2u(x); unsigned uy = f2u(y);

    I* Get the sign bits *I unsigned sx = ux»31; unsigned sy = uy»31;

/• Give an expression using only we, uy, sx, and sy•I return

拿 2. 85 给定一个浮点格式 , 有 K 位指数和 n 位小数, 对千下 列数, 写出阶码 E 、尾数 M 、小数 f 和值 V

的公式。另外,请 描述其位表示。

  1. 数 7. 0。

  2. 能够被准确描述的最 大奇整数。

  3. 最小的规格化数的倒数。

    拿 2. 86 与 Intel 兼容的处理器也支待 "扩展精度“ 浮点形式, 这种格式具 有 80 位字长, 被分成 1 个符号

位、k = l 5 个阶码位、1 个单独的整数位 和 n = 63 个小数位。整数位是 IEEE 浮点表示中隐 含位的显式副本。也就是说 ,对 千规格化的值 它等于 1, 对于非规格 化的值它等于 0。填写下表,给 出用这种格式表示的一些“有趣的“数字的近似值。

最大的规格化数

将数据类型声明为 l o ng double, 就可以 把这种格式用于为与 Intel 兼容的机器编译 C 程序。但是, 它会强制编译器以传 统的 8087 浮点指令为基础生成代码 。由此产生的程序很可能会比数据类型为 fl oa t 或 d oub l e 的 情况慢上许多。

  • 2. 87 2008 版 IEE E 浮点标准, 即 IE EE 754-2008, 包含了一种 16 位的“半精度“ 浮点格式。它最初是由计算机图形公 司设计的 , 其存储的数据所需的动态范围要高 于 16 位整数可获得的 范围。这种格式具 有 1 个符号位、5 个阶码位 Ck = 5 ) 和 10 个小数位( n = 10 ) 。阶码偏置量是 sz- I - 1 = 15 。

    对于每个给定的数,填写下表,其中,每一列具有如下指示说明:

    Hex: 描述编码形式的 4 个十六进制数字。

M: 尾数的值 。这应该是一个形如 x 或王 的数, 其中 x 是一个整数, 而 y 是 2 的整数幕。例

如: 0 、67 和

64 256°

E, 阶码的整数值。

v, 所表示的数字值。使用 x 或者 x X 2” 表示,其 中 x 和 z 都是整数。

D: ( 可能近似的)数值,用 pr i n t f 的格式 规范%f 打印。

举一个例子, 为了 表示数— , 我们有 s = O, M = — 和 E = - 1。因此这个数的阶码字段为

8

011102( 十进制值 15—1 = 1 4) , 尾数字段为 11000000002, 得到一个十六进制的表示 3B00。其数值

为 0. 875 。

标记为 “— " 的条目不用填写。

描述 -o 最小的> 2 的值HexMEV -oD - o. o
512512512. 0
最大的非规格化数
-oo-co- ex,
十六进制表示为 3BBO的数3B80

•• 2 . 88 考虑下 面两个 基于 IEEE 浮点格式的 9 位浮点表示。

  1. 格式 A
    • 有一个符号位。
      • 有 k= 5 个阶码位 。阶码偏置量是 15 。
      • 有 n = 3 个小数位 。
    1. 格式 B
      • 有一个符号位。
      • 有 k = 4 个阶码位。阶码偏置量是 7。
  • 有 n = 4 个小数位。

    下面给出了一些 格式 A 表示的位模式,你 的任务是把它们转换成最接近的 格式 B 表示的值。如果需要舍入,你要 向+ oo舍入。另外 ,给出用 格式 A 和格式 B 表示的 位模式 对应的值。要么是整数(例如1 7) , 要么是小数(例如 17/ 64 或 17 / 26 ) 。

格式A格式B
1 01 11 0 001一9 161 Oll O 0010-9 百
0 1 011 0 1 01
1 00111 110
0 00000 101
1 11011 000
0 11000 100
  • 2. 89 我们在一 个 i n t 类型为 32 位补码表示的 机器上运行 程序。f l oa t 类型的值使用 32 位 IEEE 格式,

    而 doub l e 类型的值使用 64 位 IEEE 格式。

    我们产 生随机整数 x、y 和 z , 并且把它们转换成 doub l e 类型的 值:

    /• Create some arbitrary values•/ int x = random();

    int y = random() ;

    int z * random O ;

    /• Convert to double•/ double dx• (double) x; double dy = (double) y; double dz”’(double) z;

    对于下列的每个 C 表达式, 你要指出表达式是否总是为 1。如果它总是为 1, 描述其中的数学原理。否则, 列举出使它为 0 的参数的 例子。请注意 , 不能使用 IA32 机器运行 GCC 来测试你的答案 ,因为对 千 f l oa t 和 do ub l e , 它使用的都是 80 位的扩展精 度表示。

  1. (float)x==(float)dx
  2. dx-dy== (double) (x-y)
  3. (dx+dy) +dz==dx+ (dy+dz)
  4. (dx*dy) *dz==dx* (dy*dz)
  5. dx/dx==dz/dz
  • 2. 90 分配给你一个任务 , 编写一个 C 函数来计算 护的浮点表示。你意 识到完成 这个任务的最好方法是直接创建结果的 IEEE单精度表示。当 x 太小 时,你的 程序将 返回 o. o。 当 x 太大时, 它会返回

    + = 。填写下列代码的空白部分,以 计 算出正确的结果。假设函数 u2f 返回的浮点值与它的无符号参数有相同的位表示。

    float fp 江 2(int x)

/• Result exponent and fraction•/ unsigned exp, frac;

unsigned u;

if (x <) {

I• Too small. Return 0. 0 • I exp= ,

frac = ,

} else if (x <) {

/• Denormalized result•I exp= ,

frac = ,

} else if (x <) { I* Normalized result. *I exp= ,

frac = ,

} else {

I* Too big. Return +oo *I exp= ,

frac = ,

/• Pack exp and frac into 32 bits•/ u =exp«23 I frac;

/• Return as float•/ return u2f(u);

223 22

  • 2. 91 大约公元前 250 年, 希腊数学家阿 基米德 证明了— - < re< — 。 如果当时有一台计算机和标 准库

    71

    < math.h>, 他就能够确定 T( 的单精度浮点近似值的十六进制表示为 Ox 40 490 FDB。当然 , 所有的这些都只是近似值,因为穴不是有理数。

    1. 这个浮点值表示的二进制小数是多少?

      22

    2. 一的二进制小数表示是什么? 提示: 参见家庭作业 2. 83。

  1. 这两个 T 的近似值从 哪一位(相对于二进制小数点)开始不同 的? 位级浮点编码规则

    在接下来的题目中,你所写的代码要实现浮点函数在浮点数的位级表示上直接运算。你的代码应该 完全遵循 IEEE 浮点运算的规则 , 包括当需 要舍入时 , 要使用向偶数舍人的方式。

    为此 ,我们 把数据类型 fl o a t - b i t s 等价于 un s i g ne d :

    I• Access bit-level representation floating-point number•I typedef unsigned float_bits;

你的代码中不使用数 据类型 f l o a t , 而要使用 fl o a t _b it s 。你可以使用数据类型 i n t 和 u n s i g ne d , 包括无符号和整数常数 和运算。你不可以使用任何 联合、结构和数组。更 重要的 是, 你不能使用任何 浮点数据类型、运算或者常数。取而代之 , 你的代码应该执行实 现这些指定的浮点运算的位操作 。

下面的函数说明了对这些规则的使用。对千参数 f , 如果 J 是非规格化的, 该函数返回士 0 ( 保持 f

的符号),否 则, 返回 f 。

/• If f is denorm, return 0. Otherwise, return f•/ float_bits float_denorm_zero(float_bits f) {

I• Decompose bit representation into parts•/ unsigned sign= f»31;

unsigned exp= f»23 & OxFF ; unsigned frac = f & Ox7FFFFF; if (exp== 0) {

/• Denormalized. Set fraction to O•/ frac = O;

I• Reassemble bits•/

return (sign«31) I (exp«23) I frac;

•• 2. 92 遵循位级浮点编码规则,实现具有如下原型的函数:

/• Compute -f. If f is NaN, then return f. •/ float_bits float_negate(float_bits f);

对于浮点数 J, 这个函数计箕 - J。如果 J 是 Na N , 你的函数应该简单地返回 J。

测试你的函数, 对参数 f 可以取的所有 232 个值求值 , 将结果与你使 用机器的浮点运算得到的结果

相比较。

•• 2. 93 遵循位级 浮点编码规则 ,实 现具有如下原型的函数:

I• Compute lfl. If f is NaN, then return f. •/ float_bits float_absval(float_bits f);

对于浮点数 f , 这个函数计算 Ii i 。如果 J 是 N a N , 你的函数应该简单地返回 f 。

测试你的 函数, 对参数 f 可以取的所有 23 2 个值求值 ,将结果 与你使用机器的浮点运算得到的结果相比较。

·: 2. 94 遵循位级浮点 编码规则 , 实现具有如下原 型的函数:

I• Compute 2•f. If f is NaN, then return f. •/ float_bits float_twice(float_bits f);

对于浮点 数 f , 这个函数计算 2. 0• f 。如果 J 是 N a N , 你的函数应该 简单地返回 f 。

测试你的函数, 对参数 f 可以取的所有 23 2 个值求值, 将结果与你使用机器的 浮点运算得到的结果相比较。

,:2. 95 遵循位级浮点编码规则,实现具有如下原型的函数:

I• Compute 0.5•f. If f is NaN, then return f. •/ float_bits float_half(float_bits f);

对于浮点数 f , 这个函数计算 0. 5• f 。如果 J 是 N a N , 你的函数应该简单地返回 f 。

测试你的 函数, 对参数 f 可以取的所有 22’ 个值求值 ,将结果 与你使用 机器的 浮点运算得到的结果相比较。

::2. 96 遵循位级浮点编码规则,实 现具有如下原 型的函数:

/*

* Compute (int) f.

* If conversion causes overflow or f is NaN, return Ox80000000

*/

int float_f2i(float_bits f);

对于浮点数 f , 这个函数计算 ( i n t ) / 。如果 f 是 N a N , 你的函数应该向零舍入。如果 f 不能用整数表示(例如, 超出表示 范围,或者 它是一个 N a N ) , 那么函数应该返回 Ox 8 00 00000 。

测试你的函数, 对参数 f 可以取的所有 232个值求值,将结果 与你使用机 器的浮点运算得到的

结果相比较。

::2. 97 遵循位级浮点编码规则,实现具有如下原型的函数,

I• Compute (float) i•/ float_bits float_i2f(int i);

对于函数 i , 这个函数计算 (fl o a t ) i 的位级表示。

测试你的函数, 对参数 f 可以取的所有 223 个值求值 , 将结果与你使用机器的 浮点运算得到的结果相比较。

练习题答案 #

2. 1 在我们开始查看机器级程序的时候,理解十六进制和二进制格式之间的关系将是很重要的。虽然本书中介绍了完成这些转换的方法,但是做点练习能够让你更加熟练。

A.

B.

C. 将 Ox DSE4 C 转换成二进制:

D.

2. 2 这个问题给你一个机会思考 2 的幕和它们的十六进制表示。

n‘X’ ( 十进制)2” (十六进制)
9512Ox200
19524 288Ox 8 00 0 0
1416 384Ox4000
1665 536OxlOOOO
17131 072Ox 2 0000
532Ox20
7128Ox80

2. 3 这个问题给你一个机会试着对一些小 的数在十六 进制和十进制 表示 之间进行转换。对于较大的数, 使用计算器或者转换程序会更加方便和可靠。

二进制十六进制
0000 0000OxOO
167 = 10 · 16 + 71010 0111OxA7
62 = 3 · 16 + 14OOll 1 11 0Ox3E
188 = 11 · 16 + 121011 1100OxBC
3·16 +7= 550011 0111Ox37
8 · 16 + 8 = 1361000 1000Ox88
15 · 16 + 3 = 2431111 0011OxF3
5 · 16 + 2 = 820101 0010Ox52
10 · 16 + 12 = 1721010 llOOOxAC
14 · 16 + 7 = 23 11110 0111OxE7
  1. 4 当开始调试机器级程序时 , 你将发现在许多情况中 , 一些简单的十六进 制运算是很有用的 。可以总是把数转换 成十进制 ,完成运算 ,再把它 们转换 回来,但是能 够直接用十六进制工作 更加有效 , 而且能够提供更多的信息。
    1. Ox503c +Ox 8 =0x50 44 。8 加上十六进 制 c 得到 4 并且进位 1。

    2. Ox 503c - Ox 40 =0x 4ff c 。在第二个数位, 3 减去 4 要从第 三位借 l 。因为第 三位是 o, 所以我们必须从第四位借位。

    3. Ox503c +64=0x507c 。十进制 64( 26 ) 等于十六 进制 Ox 40 。

    4. Ox50 e a - Ox 503c =Oxa e 。 十六进制数 a ( 十进制 10 ) 减去十六 进制数 c ( 十进制 1 2 ) , 我们从第二位借 16 , 得到十六进制数 e ( 十进制数 14 ) 。在第二个数位,我们 现在用 十六进制 d ( 十进制 13 ) 减去 3 , 得到十六进 制 a ( 十进制 10 ) 。

      2. 5 这个练习测试你对数据的字节表示 和两种不同 字节顺序的理 解。

小端法: 2 1大端法: 87
小端法: 2143大端法: 8 765
小端法: 2143 65大端法: 8765 43

回想一下, show_b y t e s 列举了一系列字节,从低位 地址的 字节 开始,然 后逐一列出高位地址的 字

节。在小端法机器上,它将按照从最低有效字节到最高有效字节的顺序列出字节。在大端法机器 上,它将按照从最高有效字节到最低有效字节的顺序列出字节。

  1. 6 这又是一个练习从十六进制到二进制转换的机会。同时也让你思考整数和浮点表示。我们将在本章后面更加详细地研究这些表示。
    1. 利用书中示例的符号,我们将两个串写成:

      00359141

      00000000001101011001000101000001

      ********************* 4A564504

      01001010010101100100010100000100

    2. 将第二个字相对于第一 个字向右移 动 2 位, 我们发 现一个有 21 个匹配位的序列。

    3. 我们发现除 了最高有效 位 1. 整数的所有位都嵌在浮点数中。这正好也是书中示例的情况。另外,浮点数有一些非零的高位不与整数中的高位相匹配。

      2. 7 它打印 61 62 63 64 65 66。回想一下, 库函数 s tr l e n 不计算终止的 空字符, 所以 s ho w byt e s 只 打印到字符 ’ f ’ 。

      2. 8 这是一个帮助你更加熟悉布尔运算的练习。

运算结果运算结果
a[01101001]a&b[01000001]
b[01010101]alb(01111101]
-a[10010110]a “b(OOllllOO]
-b[10101010]
  1. 9 这个问题说明了怎样用布尔代数来描述和解释现实世界的系统。我们能够看到这个颜色代数和长度为 3 的位向 量上的 布尔代数是一 样的。
    1. 颜色的取补是通过对 R、G 和 B 的值取 补得到的 。由此,我们可以看出,白 色是黑色的 补, 黄色是蓝色的补,红紫色是绿色的补,蓝绿色是红色的补。
    2. 我们基于颜色的位向量表示来进行布尔运算。据此,我们得到以下结果:
蓝色 <ooDI绿色( 010 ) =蓝绿色( 011 )
黄色(1 10 )&.蓝绿色( 011 ) =绿色( 010 )
红色(1 00 )A紫红色(1 01) =蓝色( 001 )

2. 10 这个程序依赖于两个 事实, EXCLUSVI E-OR是可交换的和可结合,以及对于任意的 a , 有 a Aa = O。

步骤 •x *y
初始 a b
步骤l a a"b
步骤2a”(a"b)=(a"a)" b =ba"b
步骤3bb"(a “b)= (b “b)" a= a

某种情况下 这个函数会 失败, 参见练习题 2. 11 。

  1. 11 这个题目说明了我们的原地交换例程微妙而有趣的特性。
    1. fir s t 和 l a s t 的值都为 k , 所以我们试图交换正中间的元素和它自己。
    2. 在这种情况 中, i np l a c e _s wa p 的 参数 x 和 y 都指向同一个位置。当 计算* x " * y 的时候,我 们得到 0 。然后将 0 作为数组正中 间的元素, 而后面的 步骤一直都把这个元素设 置为 0 。我们可以看到,练习题 2. 10 的推理隐 含地假设 x 和 y 代表不同的位置。
    3. 将r e ver s e _arr a y 的第 4 行的测试简单地替换成 f ir s t <l a s t , 因为没有必要交换正中间的元素和它自己。
  2. 12 这些表达式如下:
  3. x & OxFF
    1. xA~OxFF

    2. x I OxFF

      这些表达式是 在执行低级 位运算中经常发现的典型类型。表达式 ~ Ox F F 创建一个掩码, 该掩码 8

      个最低位等于 o, 而其余的 位为 1。可以观察到,这些 掩码的产生和字长无关 。而相比之下, 表达式 Ox FFFFFFOO 只 能工作在 32 位的机 器上。

      2. 13 这个问题帮助你思考布尔运算和程序员应用掩码运算的典型方式之间的关系。代码如下:

      /• Decla 工 ·at i ons of functions implementing operations bis and bic•/ int bis(int x, int m);

      int bic(int x, int m);

/• Compute xly using only calls to functions bis and bic•/ int bool_or(int x, int y) {

int result= bis(x,y); return result;

/• Compute x~y using only calls to functions bis and bic•/ int bool_xor(int x, int y) {

int result= bis(bic(x,y), bic(y,x)); return result;

bi s 运算等价于 布尔 OR一 如果 x 中或者 m 中 的 这一位置位了, 那么 z 中的这一位就 置位。另一方面, b i c (x, m) 等价于 x &~m; 我们想实现只有 当 x 对应的位为 1 且 m 对应 的位为 0 时, 该位等于 1。

由此,可以通过对 b i s 的一次调用来实现 1 。 为了实 现^, 我们利用以 下属性

工 Ay = ( x &.~ y ) I (~x&.y)

2. 14 这个问 题突出了位级 布尔运算和 C 语言中的逻辑运算之间的关系。常见的 编程错误是在想用 逻辑运算的时候用了位级运算,或者反过来。

表达式 值 表达式 值
X&yOx20X&&yOxOl
XIyOx7FX11yOxO l
~xI~yOxDF!x11! yOxOO
X&!yOxOOX&&~yOxO l

2. 15 这个表达式是 ! ( x A y ) 。

也就是,当且仅当 x 的每一位和 y 相应的每一位匹配时 , X A y 等于零。然后,我们 利用! 来判定一个字是否包含任何非零位。

没有任何实际的理由 要去使用这个 表达式 ,因 为可以简单 地写成 x= =y , 但是它说明了位级运算和逻辑运算之间的一些细微差别。

2. 16 这个练习可以帮助你理解各种移位运算 。

X X<<3 (逻辑) ( 算术) X>>2 X> >2
十六进制二进制二进制十六进制二进制十六进制二进制十六进制
OxC3[11000011)[00011000]OxlB[00110000]Ox30[l 1110000]OxFO
Ox75(01110101)[10101000]OxAB(00011101]Ox l D[00011101]OxlD
Ox87[10000111](00111000]Ox38[00100001)Ox21[11100001]OxEl
Ox66[01100110](00110000]Ox30[00011001)Oxl9[00011001]Ox19

第 2 章 信息的表示和处理 101

2. 17 一般而言,研究字长非常小的例子是理解计算机运算的非常好的方法。

无符号值对应于图 2- 2 中的值。对于补码 值, 十六进制数字 0 ~ 7 的最高有效位为 o , 得到非负值,然 而十六 进制数字 8 ~ F 的最高有效 位为 1 , 得到一个为负的 值。

2 18 对于 32 位的机器,由 8 个十六进制数 字组 成的, 且开始的那个数字在 8 ~ f 之间的任何值,都 是一个负数。数 字以串 f 开头是很普遍的 事情 , 因为负数的 起始位全为 1 。不过, 你必须看仔细了。例如, 数 Ox80 48337 仅仅有 7 个数字。把起始位 填入 0’ 从而得到 Ox080 48337 , 这是一个正数。

4004d0:48 81 ec eO 02 00 00sub$0x2e0,%rspA . 736
4004d7:48 8b 44 24 a8mov-Ox58(%rsp),%raxB. -BB
4004dc:48 03 47 28addOx28(%rdi),%raxC. 40
4004e0:48 89 44 24 dOmov%rax,-Ox30(%rsp)D. -48
4004e5:48 8b 44 24 78movOx78(%rsp),%raxE. 120
4004ea:48 89 87 88 00 00 00mov如r ax , Ox88 ( r 讼 di )F. 136
4004f1:48 8b 84 24 f8 01 00movOx1f8 (%rsp) , %raxG . 504
4004f8:00
4004f9:48 03 44 24 08addOx8(%rsp),%rax
4004fe:48 89 84 24 co 00 00mov%rax,Oxc0(%rsp)H 192
400505:00
400506:48 Bb 44 d4 bBmov-Ox48 (%rsp, %rdx, 8), %raxI . -72

2. 19 从数学的 视角来 看,函 数 T2 U 和 U2 T 是非常奇特的 。理解它们的行为非常重 要。

我们根据补码的 值解答这个间 题, 重新排列练习题 2. 17 的 解答中的行 , 然后列出无符号值作为函数应用的结果。我们展示十六进制值,以使这个过程更加具体。

2. 20 这个练习题测试你对等式 ( 2. 5 )的理解。

对千前 4 个条目,x 的值是负的,并 且 T 2队 ( x ) = x + 2’ 。对于剩下的两个条目, x 的 值是非负的, 并且 T2亿 ( x ) = x 。

2. 21 这个问题加强你对补码和无符号表示之间关 系的理解,以 及对 C 语言升级规则( pro mo tion ru le ) 的影响的理解。回 想一下 , T M in ,2是一 2 147 483 648 , 并且将它强制类型转换为无符号数后,变成了 2 147 483 648。另外,如果 有任何 一个运算数是无符号的,那 么在比较之前, 另一个运算数会被强制类型转换为无符号数。

2. 22 这个练习很具 体地说明了符号扩展如何保 持一个补码表示的数值 。

A. [ 1011 ] :

B. [ 11 011 ] :

一 沪 + 21 + 2 0

—2 4 + 2 3 + 2 1 + 20 =

- 8 + 2 + 1

- 16 + 8 + 2 + 1

c. [ 111 011 ] : 一沪 + 24+ 23 + 2’ + 2° = - 32 + 16 + 8 + 2 + 1 = - 5

2 . 23 这些函数 的表达式是 常见的程序 ”习惯用语“, 可以从多个位字段打包成的一个字中提取 值。它们利用不同移位运算的零填充和符 号扩展属性。请注意强制类 型转换和移位运算的顺序。在 f un l 中,移位是 在无符号 wo r d 上进行的,因 此是逻辑移位。在 f un 2 中, 移位是在把 wo r d 强制类 型转换为 i nt 之后进行的 ,因此 是算术移位。

A.

B. 函数 f unl 从参数的低 8 位中提取一 个值, 得到范围 0 ~ 255 的一个整数。函数 f un2 也从这个参数的 低 8 位中提取一个值,但是 它还要执行符 号扩展。结果将是介于—128 ~ 127 的一个数。

2. 24 对于无符号数来说,截断的 影响是相当直观的, 但是对于补码数却不是。这个练习让你使用非常小的字长来研究 它的属性。

原始数 。十六进制 截断后的数 。原始数 。无符号 截断后的数 。补码 原始数 截断后的数 。 。
222222
919I- 71
B3113- 53
F7157- 1-1

正如等式( 2. 9 ) 所描述的 , 这种截断无 符号数值的结果就 是发现它们模 8 的余数。截断有符号数的结果要更复杂一些。根据等式 ( 2. 1 0) , 我们首先计算这个参数模 8 后的余数。对千参数 0 ~ 7 , 将得出值 0 ~ 7 , 对于参数 一8 ~ - 1 也是一样。然后我们 对这些余数应用函数 UZT3 , 得出两个O~

3 和- 4 ~ 1 序列的反复。

2. 25 设计这个问题是要说明从有符号数到无符号数的隐式强制类型转换很容易引起错误。将参数l e ng t h 作为一个无符号数来传递看上去是件相当自然的 事情,因 为没有人会想 到使用一个长度为负数的值。停止条件 i < =l e ng t h- 1 看上去也很自然。但是把这两点组合 到一起 , 将产生意想不到的结果!

因 为参数 l e ngt h 是无符号的,计 算 0 - 1 将使用无符号 运算 ,这 等价 于模 数加法。结果得到

UM釭 0 比较同 样使用无符号 数比较, 而因为任何 数都是小于或者等千 UMa x 的, 所以这个比较总是为真! 因此,代码将试图 访问数组 a 的非法元素。

有两种方法可以改正这段代码,其 一是将 l e ng吐 声明为 i n t 类型, 其二是将 f o r 循环的测试条件改为 江l e ng t h。

2 26 这个例子说明了无符号运算的一个 细微的 特性, 同 时 也是我们 执行无符号运算时不会意识 到的属性。这会导致一些非 常棘手的错误。

  1. 在什么情况下,这个函数会产生不正确的结果? 当 s 比 t 短的时候,该 函数会不正确地返回1。

  2. 解释为什么会出现这样不 正确的结果 。由于 s tr l e n 被定义为产生一个无符号的结果, 差和比较都采用无符号运算来计算 。当 s 比 t 短的时候, s tr l e n (s ) - s 七rl e n (t ) 的差 会为负 , 但是变成了一个很大的无符号数,且大 千 0 。

  3. 说明如何修改这段代码好让它能可靠地工作。将测试语句改成:

    return strlen(s) > strlen(t);

2. 27 这个函数是对确定无符号加法是否溢出的规则的直接实现。

I* Determine whether arguments can be added without overflow *I int uadd_ok(unsigned x, unsigned y) {

unsigned sum= x+y; return sum>= x;

2. 28 本题是对算术模 16 的简单示范。最容易的解决方法 是将十六 进制模式 转换成它的无符号十进 制值。对 于非零的 x 值,我们必 须有(一4x ) + x = l 6。然后,我 们就可以 将取补后的值转换回十六进制。

2. 29 本题的目的是确保你理解了补码加法 。

Xyx+yx+ y情况
-12-15-275I
[10100][10001][100101][00101]
-8-8-16-162
(11000][11000][110000][10000]
-98-1-12
[10111][01000][111111J[11111]
25773
[00010][00101)[000111][00111]
12416-164
(01100][00100][010000][10000]

2. 30 这个函数是对确定补码加法是否溢出的规则的直接实现。

/• Determine whether arguments can be added without overflow•I int tadd_ok(int x, int y) {

int sum= x+y;

int neg_over = x < 0 && y < O && sum>= O; int pos_over = x >= 0 && y >= 0 && sum< O; return !neg_over && !pos_over;

2. 31 通过学习 2. 3. 2 节,你的同 事可能已经学 到补码加会形成一个阿贝尔群, 因此表达式 (x +y ) - x 求值得到 Y• 无论加法是否溢出 , 而 (x +y ) - y 总是会求值得到 x。

2. 32 这个函数会 给出正确的 值,除 了当 y 等于 T M in 时。在这 个情况下 ,我 们有- y 也等于 TM in , 因此函数 t a dd_o k 会认为只要 x 是负数时 , 就会溢出, 而 x 为非负 数时, 不会溢出。实际上, 情况恰恰相反: 当 x 为负数时, t s ub_o k( x , TM in ) 为 1 ; 而当 x 为非负时 , 它为 0。

这个练习说明 ,在 函数的任何 测试过程中 , T M i n 都应该作为一种测试 情况。

2. 33 本题使用非常小的字长来帮助你理解补码的非。

对于 w = 4 , 我们 有 T M in, = - 8 。因此一8 是它自己的加法逆元 , 而其他数值是 通过整数非来取非的。

十六进制 。X十进制 。一4I 十进制 。X 十六进制 。
55-5B
8-8-88
D-333
F- II1

2. 34

2. 35

对于无符号数的非,位的模式是相同的。本题目是确保你理解了补码乘法 。

模式 X y X · Y 截断了的X ·Y
无符号数4(100)5[IOI]20[010100]4[100]
补码-4[100]-3[JOI]12[001100]-4[100]
无符号数2[010]7[lll]14(001110]6[110]
补码2[010]-1[Ill]-2(111110]-2(110]
无符号数6[110]6[110]36(100100]4[100)
补码-2[110]-2[110]4[000100)-4(JOO]

对所有可能 的 x 和 y 测试一遍这个 函数是不现实的。当数据类型 i nt 为 32 位时 , 即使你每秒运行一百亿个测试 , 也需要 58 年才能 测试完所有的组合 。另一方面,把 函数中的 数据类型改成 s hor t或者 c har , 然后再穷尽 测试, 倒是测试代码的一 种可行的方法。

我们 提出以下论 据, 这是一个更理论的方法 :

  1. 我们知 道 工. y 可以写成一个 2w 位的补码数字。用 u 来表示低 w 位表示的无 符号数 , v 表示高

w 位的补码数字。那么,根据公式 ( 2. 3), 我们可以得到 工 y = v沪 十u。

我们还知道 u = T 2U,,. ( p ) , 因为它们是从同一个位模式得出来的无符号和补码数字,因此根据等式( 2. 6), 我们有 u= p + p,,,_ , 沪 , 这里 Pw- 1 是 p 的 最高有效位 。设 t = v + Pw- 1 , 我们有

工 y= p + tw2 0

2. 36

当 t = O 时 , 有 工. y=p; 乘法不会 溢出。当 t =l=- 0 时 , 有 工. y=/=- p; 乘法不会溢出 。

2 ) 根据整数除 法的定义 ,用 非零数 工除 以 p 会得到商 q 和余数r ’ 即 p = 工 q+r, 且 I 叶 < I 工 I 。

( 这里用的是绝对值, 因为 工和 r 的符号可能不一致。例如,一 7 除以 2 得到商- 3 和余数- 1。)

3 ) 假设 q = y 。那么有 工 y = 工 y + r + t2中。 在此, 我们 可以得到 r + t2w = o。但是 l r l < I 工 I

沪 , 所以只有当 t = O 时,这个等式 才会成立 ,此时 r = O。假设r = t = O。 那么我们有 工 y = 工. q, 隐 含有 y = q。

当 工= O 时 ,乘法不溢出 , 所以我们的代码提供了一种可靠的方法来测试补码乘法是否会导致溢。出如果用 64 位表示, 乘法就不 会有溢出 。然后我们 来验证将乘积强制类型转换为 32 位是否会改 变它的值:

I* Determine whether the arguments can be multiplied without overflow *I

int tmult_ok(int x, int y) {

I* Compute product without overflow *I int64_t pll = (int64_t) x*y;

I* See if casting to int preserves value *I return pll == (int) pll;

注意 ,第 5 行右边的强制类型转换 至关重要 。如果我们将 这一行写成

int64_t pll = 江 y;

就会用 32 位值来计算 乘积(可能会溢出),然后再符号扩展到 64 位。

2. 37 A. 这个改 动完全没有帮助。虽 然 a s i ze 的计算会更 准确,但是 调用 ma l l oc 会导致这个值被转换成一个 32 位无符号数字,因 而还是会出现同样的溢出条件。

  1. ma l l o c 使用一个 32 位无符号数作为参数 ,它 不可能分配一 个大于 沪 个字节的块, 因此, 没有必要试图 去分配或者复 制这样大的一块内存。取而代 之, 函数应该放弃,返 回 NULL, 用下面的代码取代 对 ma l l o c 原 始的调用(第9 行):

    uint64_t required_size’"’ele_cnt * (uint64_t) ele_size; size_t request_size = (size_t) required_size;

    if (required_size !• request_size)

    /• Overflow must have occurred. Abort operation•/ return NULL;

    void•result= malloc(request_size); if (result== NULL)

    /• malloc failed•/ return NULL;

    2. 38 在第 3 章中, 我们将看到很多实际的 LEA 指令的例子。用这个指令来支持指针运算 , 但是 C 语言编译器经常用它来执行小常数乘法 。

    对于每个 k 的值, 我们可以计算出 2 的倍数: 2 女( 当 b 为 0 时)和2’ + 1( 当 b 为 a 时)。因此我

    们能够计算出倍数 为 1, 2, 3, 4, 5, 8 和 9 的值。

    2. 39 这个表达式就变成了- (x< <m)。要看清这一点,设字长为w , n = w - l 。形式 B 说我们要计算 (x< <w) ­ (x < <m)’ 但是将 x 向左移 动 w 位会得到值 0。

    2. 40 本题要求你使用讲过的优化技术 ,同时 也需要自己的一点 儿创造力。

K移位加法碱法表达式
621(X< <2 ) + (X << l )
31lI(X<<5) - X
-62I(x <<l ) - (x <<3 )
5522(X< < 6 ) - (X <<3 ) - X

可以观察到, 第四种情 况使用了形 式 B 的改进版本。我们可以 将位模式 [ 110111] 看作 6 个连续的 1 中间有一个 o, 因而我们对形式 B 应用这个原则,但 是需要在后来把中间 0 位对应的项减掉。

2. 41 假设加法和减法有同样的性能, 那么原则就是当 n = m 时, 选择 形式 A, 当 n = m + l 时,随 便选哪种,而当 n> m + l 时,选 择形式 B。

这个原则的证明 如下。首先假设 m> O。 当 n = m 时, 形式 A 只需要 1 个移位, 而形式 B 需要

2 个移位和 1 个减法。当 n = m + l 时, 这两种形式都需要 2 个移位和 1 个加法或者 1 个减法。当n> m + l 时, 形式 B 只需要 2 个移位和 1 个减法,而形 式 A 需要 n - m + 1 > 2 个移位和 n - m > l 个加法。对 于 m = O 的 情况, 对千形式 A 和 B 都要少 1 个移位, 所以在两者中选择时, 还是适用同样的原则。

2. 42 这里唯一的 挑战是不使用任何 测试或条件运算来计算偏 置量。我们利用了一 个诀窍,表 达式 x > >

31 产生一个字,如果 x 是负数,这个 字为全 1, 否则为全 0。通过掩码屏蔽掉适当的位,我们 就得到期望的偏置值。

int div16(int x) {

I• Compute bias to be either O (x >= 0) or 15 (x < 0)•I int bias= (x»31) & OxF;

return (x + bias)»4;

2. 43 我们发现当人们直接与汇 编代码打交道时是有困难的 。但当把它放入 op t ar i 七h 所示的形式中时,问题就变得更加清晰明了。

我们可以看到 M 是 31 ; 是用 (x < <S) - x 来计算 x* M。

106 笫一部分 程序结构和执行

我们可以 看到 N 是 8 ; 当 y 是负数时, 加上偏置量 7’ 并且右移 3 位。

2. 44 这些 " C 的谜题“ 清楚地告诉 程序员 必须理 解计算 机运算的属性。

A. (x > 0) I I ( (x 一 1 ) < 0)

假。设 x 等于- 2 147 483 648 ( TM in 32) 。 那么, 我们有 x- 1 等千 2147483647( TMa 工32) 。

B. (x & 7) != 7 11 (x << 29 < 0)

真。如果 (x & 7) != 7 这个表达式的值为 o, 那么我们必须有位 工2 等于 1。当左移 29 位时 , 这个位将变成符号位。

C. (x * x) > = 0

假。当 x 为 65 535( Ox FFFF) 时 , X * X 为- 131 071( Ox FFF EOOOl ) 。

  1. x < 0 I I -x < = 0

    真。如果 x 是非负数, 则- x 是非正的。

    1. x > 0 I I -x > = 0

      假。设 x 为- 2 147 48 3 648 ( TM in32) 。 那么 x 和- x 都为负数。

    2. x+y = = uy+ux

      真。补码和无符号乘法有相同的位级行为,而且它们是可交换的。

    3. x*~y + uy*ux == - x

      真。~y 等千- y- 1。uy*ux 等千 x *y 。因此 , 等式左边等价 千 x *- y- x +x *y 。

      2. 45 理解二进制小数表示是理解浮点编码的一个重要步骤。这个练习让你试验一些简单的例子。

考虑二进制小数 表示的 一个简单 方法是将一个数表示为形如责 的小数。我们将这个形式表示

为二进制的过程是:使用 :r: 的二 进制表示 , 并把二进制小 数点插入从右边算起的第 k 个位置。举一个例子,对 25 ,我们有 2510 11001 2 。 然后我们 把二进制小数点放在从右算起的第 4 位, 得

16

到 1. 10012 0

  1. 46 在大多数情况中, 浮点数的有限精度不是 主要的问 题,因为计算 的相对误差仍然是相当低的。然而在这个例 子中, 系统对于绝对误 差是很敏感的。
    1. 我们 可以看到 o. 1 - 工的二进制表示 为:

      0. 000000000000000000000001100[ 1100] ··· 2

    2. 把这个表示与— 的二进 制表示进行比较 ,我 们可以 看到这就是 2一 2o x - , 也就是大约 9. 54 X 10 10

      10 - s .

      C. 9. 54 X 1-0 s X 100 X 60 X 60 X 10 <::::0 . 3 43 秒 。

      D. O. 343 X 2000 <::::687 米 。

      2. 47 研究字长非常小的 浮点表示能够帮助澄清 IEEE 浮点是怎样工作的。要特别注意非 规格化数和规格化数之间的过渡。

2. 48 十 六 进 制 Ox 3591 41 等 价 于 二 进 制 [ 1101011 001000 101000001 ] 。将 之 右 移 21 位 得 到

1. 101011 001 000101000001 2 X 22 1 。除 去起 始位 的 1 并 增 加 2 个 0 形 成小 数字 段,从 而得 到 [ 10101100100010100000100 ] 。阶 码 是 通 过 21 加 上 偏 置 狱 1 27 形 成的, 得 到 148 ( 二 进 制[ 10010100] ) 。我们把它 和符号字段 0 联合起来 , 得到二进制表示

[01001010010101100100010100000100]

我们看到两种表示中 匹配的位对应于整数的 低位到最高 有效位等千 1, 匹配小数的高 21 位:

0 0 3 5 9 1 4 1

00000000001101011001000101000001

*********************

4 A 5 6 4 5 O 4

01001010010101100100010100000100

  1. 49 这个练习帮助你思考什么数不能用浮点准确表示。
    1. 这个数的二进制表示是: 1 后面跟着 n 个 o, 其后再跟 1 , 得到值是 2+• 1+ 1 。

      B. 当 n = 23 时,值是 224+ 1 = 1 6 777 217。

      2. 50 人工舍人帮助你加强二进制数舍人到偶数的概念。

原始值 舍入后的值

l 10 .0

2. 51 A. 从 1/ 10 的无穷序列中我们可以看到 , 舍人位置右边 2 位都是 1 , 所以对 1 / 10 更好一点儿的近似值应该是对 x 加 1, 得到 x ’ = O. 000110011001100110011012, 它比 0. 1 大一点儿。

  1. 我们可以 看到 x ’ - 0. l 的二进制表示为:

    0.0000000000000000000[1100]

    将这个值与 1 / 10 的二进制表示做比较,我 们可以 看到它等于 2一22 X 1 / 10 , 大约等于 2. 38 X

    10- s .

    1. 2. 38X 10-s X l OO X 60 X 60X 10 :::::::::。. 郊6 秒,爱国者导弹系统中的误差是它的 4 倍。

      D. O. 086 X 2000::::::::: 订 1 米 。

      2. 52 这个题目考查了很多关于浮点表示的概念,包括规格化和非规格化的值的编码,以及舍入。

108 笫一部分 程序结构和执行

格式A 位格式B 位
011 000010111 000I
10111101521001111152
010 1001OllO 100¾ 向下舍入
110 11113121011 00016向上舍人
0 0 0 00010001 000非规格化一规格化

2. 53 一般来说,使用库宏 ( librar y macro) 会比你自己写的代码更好 一些。不过, 这段代码似乎可以 在多种机器上工作。

假设值 l e 400 溢出为无穷 。

#define POS_INFINITY 1e400

#define NEG_INFINITY (-POS_INFINITY)

#define NEG_ZERO (-1.0/POS_INFINITY)

  1. 5 4 这个练习可以 帮助你从 程序员的角度来提高研究 浮点运算的能力 。确信自己理解下 面每一个答案 。

    1. x == (int) (double) x

      真, 因为 d o ub l e 类型比 i n t 类型具有更 大的精度和范图。

    2. x == (i n 七 )(double) x

      假,例如当 x 为 TMa x 时。

    3. d == (double) (float) d

      假,例 如当 d 为 l e 40 时,我 们在右边得 到十C0 。

    4. f == (float) (double) f

      真,因为 d o ub l e 类型比 f l o a t 类型具有更大的精度 和范围。

    5. f == -(-fl

      真,因 为浮点 数取非就是 简单地对它的符 号位取反 。

      F. 1.0/2 == 1/2.0

      真,在执行除法之前,分子和分母都会被转换成浮点表示。

  2. d *d > =O. O

    真 ,虽 然它可能 会溢出到十C0 。

  3. (f +d ) 一f == d

    假, 例如当 f 是 1 . 0e 20 而 d 是 1. 0 时, 表达式 f +d 会舍入到 1 . Oe20, 因此左边的表达式求值得到 o. o, 而右边是 1. 0。