第 2 章 信息的表示和处理

第 2 章 信息的表示和处理

   现代计算机存储和处理的信息以 二值信号表示。这系微不足道的二进制数字,或者称为 位(bit),形成了数字革命的基础。大家熟悉了 1000 多年前的十进制(以 10 为基础)起源于印度,在 12 世纪被阿拉伯数学家改进,并在 13 世纪被意大利科学家 Leonardo Pisano(大约公元 1170 - 1250,更为大家熟知的名字是 Fibonacci)带到西方。对于有是个手指的人类来说,使用十进制表示法是很自然的事情,但是当构造存储和处理信息的机器时,二进制工作的更好。二值信号,能够更容易的被表示、存储和传输。例如,可以表示为穿孔卡片上有洞或无动、导线上的高电压或低电压、或者 顺时针或逆时针的磁场。对二值新号进行存储和执行计算的电子电路非常简答和可靠,制造商能够在一个单独的硅片上集成数百万甚至数亿个这样的电路。
   孤立的讲,单个的位不是非常有用。然而,当把这些位组合在一起,再加上某种解释,即 赋予不同的可能位模式以含意,我们就能够表示 任何有限集合的元素。比如,使用一个二进制数字系统,我们能够用位组来编码非负数。通过使用标准的字符码,我们能够对文档中的字母和符号进行编码。在本章中,我们将讨论这两种编码,以及负数表示和实数近似值的编码。
   我们研究三种最重要的数字表示。无符号(unsigned)编码基于传统的二进制表示法,表示大于或者等于 0 的数字。补码(two’s-completment)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。浮点数(floating-point)编码是表示实数的科学计数法的以 2 为基数的版本。计算机用这些不同的表示方法实现算术运算,例如 加法和乘法,类似于对应的整数和实数运算。
   计算机的表示法是用有限数量的位来表示一个数字编码。因此,当结果太大以至不能表示时,某些运算就会溢出(overflow)。溢出会导致某些令人吃惊的后果。例如,在今天的大多数计算机上(使用 32 位来表示数据类型的 int),计算表达式 200 * 300 * 400 * 500 会得出结果 -884 901 888。这违背了整数运算的特性,计算一组正数的乘积不应产生一个复的结果。
   另一方面,整数的计算机运算满足人们所熟知的真正整数运算的许多性质。例如,利用乘法的结合律和交换律,计算下面任何一个 C 表达式,都会得出结果 -884 901 888:
   (500 * 400) * (300 * 200)
   ((500 * 400) * 300) * 200
   ((200 * 500) * 300) * 400
   400 * (200 * (300 * 500))
   计算机可能没有产生期望的结果,但是至少它是一致的!
   浮点运算有完全不同的数学属性。 虽然溢出会产生特殊的值 +∞,但是一组正数的乘积总是正的。由于表示的精度有限,浮点运算是不可结合的。例如,在大多数机器上,C 表达式(3.14 + le20) - le20 求得的值会是 0.0,而 3.14 + (le20 - le20) 求得的值会是 3.14。整数运算和浮点数运算会有不同的数学属性是因为 它们处理数字表示有限性 的方式不同,整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精确的;而浮点数虽然可以编码一个较大的数值范围, 但是这种表示只是近似的。
   通过研究数字的实际表示,我们能够了解可以表示的值的范围和不同算术运算的属性。为了使编写的程序能够在全部数值范围内正确工作,而且可以具有跨越不同机器、操作系统和编译器组合的可移植性,了解这种属性是非常重要的。后面我们会讲到,大量计算机的安全漏洞都是由于计算机算术运算的微妙细节引发的。在早期,当人们碰巧触发了程序漏洞,只会给人们带来一些不便,但是现在,有众多的黑客企图利用他们能找到的任何漏洞,不经过授权就进入他人的系统。这就要求程序员有众多的责任和义务,去了解他们的程序如何工作,以及如何被迫产生不良的行为。
   计算机有几种不同的二进制表示形式来编码数值。随着第 3 章进入机器级编程,你需要熟悉这些表示方式。在本章中,我们描述这些编码,并且教你如何退出数字的表示。
   通过直接操作数字的位级表示,我们得到了几种进行算数运算的方式。理解这些技术对于理解编译器产生的机器级代码是很重要的,编译器会试图优化算术表达式值的性能。
   我们对这部分内容的处理是基于一组核心的数学原理的。从编码的基本定义开始,然后得出一些属性,例如 可表示的数字的范围、它们的位级表示以及算术运算的属性。我们相信从这样一个抽象的观点来分析这些内容,对你来说是很重要的,因为程序员需要对计算机运算与更为人熟悉的整数和实数运算之间的关系有清晰的理解。在这里插入图片描述
   C++ 编程语言建立在 C 语言基础之上,它们使用完全相同的数字表示和运算。本章中关于 C 的所有内容对 C++ 都有效。另一方面,Java 语言创造出了一套新的数字表示和运算标准。 C 标准的设计允许多种实现方式,而Java标准的数据的格式和编码上是非常精确具体的。本章中多处着重介绍了 Java 支持的表示和运算。在这里插入图片描述

2.1 信息存储

   大多数计算机使用 8 位 的块, 或者 字节(byte),作为最小的可寻址的内存单位,而 不是访问内存中单独的位。机器级程序将 内存 视作一个 特别大的字节数组,称为 虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识,称为它的 地址(address),所有可能地址的集合就称为 虚拟地址空间(virtual address space)。顾名思义,这个虚拟地址空间只是一个展示给机器级程序的概念性映像。实际的实现(见第 9 章),是将 动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,为系统提供一个看上去 统一的字节数组。
   在接下来的几章中,我们将讲述编译器和运行时系统是如何将存储器空间划分为更可管理的单元,来存放不同的 程序对象 (project object),即 程序数据、指令和控制信息。可以用各种机制来分配和管理程序不同部分的存储。这种管理完全是在 虚拟地址空间 里完成的。例如,C 语言中一个指针的值(无论它指向一个整数、一个结构或某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。C 编译器还把每个指针和类型信息联系起来,这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。尽管 C 编译器还维护着这个类型信息,但是它生成的实际机器级程序并不包含数据类型的信息。每个程序对象可以简单的视为一个字节块,而程序本身就是一个字节序列。在这里插入图片描述

2.1.1 十六进制表示法

   一个字节由 8 位 组成。在二进制表示法中,它的值域是 0000 00002 ~ 1111 11112 。如果看成十进制整数,它的值域就是 010 ~ 25510。两种符号表示法对于描述 位模式 来说,都不是非常方便。二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。替代的方法是,以 16 位基数,或者叫做 十六进制数 (hexadecimal),来表示 位模式。十六进制(简写位为 'hex‘)使用数字 ‘0’ ~ ‘9’ 以及字符 ‘A’ ~ ‘F’ 来表示 16 个可能的值。图 2-2 展示了 16 个十六进制数字对应的十进制值和二进制值。用十六进制书写,一个字节的值域为 0016 ~ FF16
在这里插入图片描述
   在 C 语言中,以 0x 或 0X 开头的数字常量被认为是十六进制的值。字符 ‘A’ ~ ‘F’ 既可以是大写,也可是小写。例如,我们可以将数字 FA1D37B16 写作 0xFA1D37B,或者 0xfa1d37b,甚至是大小写混合,比如,0xFa1D37b。在本书中,我们将使用 C 表示法来表示十六进制值。
   编写机器级程序 的一个 常见任务 就是 在位模式的十进制、二进制和十六进制表示之间人工转换。二进制和十六进制之间的转换比较简单直接,因为可以一次执行一个十六进制数字的转换。数字的转换可以参考如图 2-2 所示的表。一个简单的窍门是,记住十六进制数字 A、C 和 F 相对应的十进制。而对于把十六进制 B、D、E转换成十进制,则可以通过计算它们与前三个值的相对关系来完成。
   比如,假设给你一个数字 0x173A4C。可以通过展开每个十六进制数字,将它转换为二进制格式,如下所示:
在这里插入图片描述
   这样就得到了二进制表示 000101110011101001001100。
   反过来,如果给定一个二进制数字,可以通过首先把它分为每 4 个一组来转换成十六机制。不过要注意,如果位总数不是 4 的倍数,最左边的一组可以少于 4 位,前面用 0 补足。然后将每个 4 位组转换为对应的十六进制。
在这里插入图片描述
练习题 2.1 完成下面的数字转换
   A. 将 0x39A78F 转换成 二进制
   B. 将 二进制 11001001 转换成 十六进制
   C. 将 0xD5E4C 转换成 二进制
   将 二进制 1001101110011110110101 转换成 十六进制

2.1.2 字数据大小

   每台计算机都有一个 字长(word size),指明 指针数据的标称大小(nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的 最重要的系统参数就是 虚拟地址空间的 最大大小。也就是说,对于一个字长为 w 位的机器而言,虚拟地址的范围为 0 ~ 2w-1,程序最多访问 2w 个字节。
   最近这些年,出现了大规模的从 32 位字长机器到 64 位机器字长机器的迁移。这种情况首先出现在为大型科学和数据库应用设计的高端机器上,之后是台式机和笔记本电脑,最近则出现在智能手机的处理器上。32 位字长限制虚拟地址为 4 千兆字节(写作 4 GB),也就是说,刚刚超过 4 X 109 字节。扩展到 64 位字长使得虚拟地址空间为 16 EB,大约是 1.84 X 1019 字节。
   大多数 64 位机器也可以运行 32 位机器编译的程序,这是一种向后兼容。因此,举例来说,当程序 prog.c 用如下伪命令编译后,

linux> gcc -m32 prog.c

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

linux> gcc -m64 prog.c

   那就只能在 64 位机器上运行。因此,我们将程序称为 “32 位程序” 或者 “64 位程序” 时,区别在于该程序是如何编译的,而不是其运行的机器类型。
   计算机和编译器支持多种不同格式编码的数字格式,如不同长度的整数和浮点数。比如,许多机器都有处理单个字节的指令,也有处理表示为 2 字节、4 字节或者 8 字节整数的指令,还有些指令支持表示为 4 字节 和 8 字节的浮点数。
   C 语言支持整数和浮点数的多种数据格式。图 2-3 展示了为 C 语言各种数据类型分配的字节数。(我们在 2.2 节讨论 C 标准保证的字节数和典型的字节数之间的关系。)有些数据类型的确切字节数依赖于程序是如何被编译的。我们给出的是 32 位 和 64 位 程序的典型值。整数或者为有符号的,即 可以表示 负数、零 和 正数;或者为无符号的,即 只能表示 非负数。C 的数据类型 char 表示一个单独的字节。尽管 “char” 是由于它被用来存储文本串中的单个字符这一事实而得名,但它也能被用来存储整数值。数据类型 short、int 和 long 可以提供各种数据大小。即使是为 64 位系统编译,数据类型 int 通常也只有 4 个字节。数据类型 long 一般在 32 位程序中为 4 字节,在 64 位程序中则为 8 字节。
在这里插入图片描述
   为了避免由于依赖 “典型” 大小和不同编译器设置带来的奇怪行为, ISO C99 引入了一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化。其中就有数据类型 int32_t 和 int64_t,他们分别为 4 个字节和 8 个字节。使用确定大小的整数类型是程序员精准控制数据表示的最佳途径。
   大部分数据类型都编码为 有符号数值,除非有前缀关键字 unsigned 或对确定大小的数据类型使用了特定的无符号声明。数据类型 char 是一个例外。尽管大多数编译器和机器将它们视作有符号数,但 C 标准不保证这一点。相反,正如方括号指示的那样,程序员应该用有符号字符的声明来保证其为一个字节的有符号数值。不过,在很多情况下,程序行为对数据类型 char 是有符号的还是无符号的并不敏感。
   对关键字的顺序以及包括还是省略可选关键字来说,C 语言允许存在很多形式。比如,下面所有的声明都是一个意思:

unsigned long
unsigned long int
long unsigned
long unsigned int

   我们将始终使用图 2-3 给出的形式。
   图 2-3 还展示了指针(例如 一个被声明为类型为 “char * ”的变量)使用程序的全字长。大多数机器还支持两种不同的浮点数格式:单精度(在 C 中声明为 float)和双精度(在 C 中声明为 double)。这些格式分别使用 4 字节 和 8 字节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值