信息的表示和处理

2014/05/02 Computer

信息的表示和处理

三种最重要的数字表示:

  • 无符号(unsigned)编码基于传统的二进制表示法,表示大于或等于零的数字。
  • 补码(two’ s-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
  • 浮点数(floating-point)编码是表示实数的科学记数法的以2为基数的版本。

计算机的表示法是用有限数量的位来对一个数字编码,因此,当结果太大以至不能表示时,某些运算就会溢出(overflow)。

信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。

内存的每个字节都由一个唯一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space )。

C语言中指针的作用

指针是C语言的一个重要特性。
它提供了引用数据结构(包括数组)的元素的机制。

指针的两个方面: 值和类型。
它的值表示某个对象的位置,而它的类型表示那个位置上所存储对象的类型(比如整数或者浮点数)。

十六进制表示法

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

在 C 语言中,以 0x 或 0X 开头的数字常量被认为是十六进制的值。字符 ‘A’ ~ ‘F’ 既可以是大写,也可是小写。例如,我们可以将数字 FA1D37B16 写作 0xFA1D37B,或者 0xfa1d37b,甚至是大小写混合,比如,0xFa1D37b。在本书中,我们将使用 C 表示法来表示十六进制值。

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

字数据大小

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

每台计算机都有一个 字长(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 语言支持整数和浮点数的多种数据格式。整数或者为有符号的,即 可以表示 负数、零 和 正数;或者为无符号的,即 只能表示 非负数。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

寻址和字节顺序

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

排列一个对象的字节有两个通用的规则。考虑一个 w 位的整数,其位表示为 [xw-1,xw-2,…,x1,x0],其中 xw-1 是最高有效位,而 x0 是最低有效位。假设 w 是 8 的倍数,这些位就能被分组成字节,其中最高有效字节包含位 [xw-1,xw-2,…,xw-8],而最低有效位包含 [x7,x6,…,x0],其他字节包含中间的位。某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则,最低有效字节在最前面的方式,称为 小端法(little endian)。后一种规则,最高有效字节在最前面的方式,称为 大端法(big endianl)。

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

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

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

表示字符串

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

布尔代数简介

二进制值 是计算机编码、存储 和 操作信息 的 核心,所以围绕数值 0 和 1 的研究已经演化出了丰富的数学知识体系。这起源于 1850 年前后 乔治 · 布尔 (George Boole, 1815-1864)的工作,因此也称为 布尔代数(Boolean algebra)。布尔注意到 通过将逻辑值 TRUE (真)和 FALSE (假)编码为二进制 1 和 0,能够设计出一种代数,以研究逻辑推理的基本原则。

后来创立信息论领域的 Claude Shannon(1916~2001)首先建立了 布尔代数和数字逻辑之间的关系。他在 1937年 的硕士论文中 表明了 布尔代数可以用来设计和分析机电继电器网络。尽管那时计算机技术已经取得了相当的发展,但是布尔代数仍然在数字系统的设计和分析中扮演着重要的角色。

我们可以将上述 4 个布尔运算扩展到 位向量 的运算。位向量 就是 固定长度为 w,由 0 和 1 组成的串。位向量的运算可以定义成参数的每个对应元素之间的运算。假设 a 和 b 分别表示位向量 [aw-1>,aw-2,…,a0] 和 [bw-1>,bw-2,…,b0]。我们将 a&b 也定义为一个长度为 w 的位向量,其中第 i 个元素等于ai&-bi,0≤i<w。可以用类似的方式将运算 、^ 和 ~ 扩展到位向量上。举个例子,假设 w = 4,参数 a = [0110],b = [1100]。

C 语言中的位级运算

C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:|就是OR(或),&就是 AND(与),~就是NOT(取反),而^就是EXCLUSIVE-OR(异或)。

位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。让我们来看一个例子,掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x= 0x89ABCDEF,其表达式将得到0x000000EF。表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器来说,同样的掩码可以写成OxFFFFFFFF,但是这样的代码不是可移植的。

C 语言中的逻辑运算

C语言还提供了一组逻辑运算符Ⅱ、&&和!,分别对应于命题逻辑中的OR、AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。它们返回1或者0,分别表示结果为TRUE或者为FALSE。

逻辑运算符&&和与它们对应的位级运算&和 之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此,例如,表达式a&85/a将不会造成被零除,而表达式p&&*p++也不会导致间接引用空指针。

C 语言中的移位运算

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

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

整数表示

整数数据类型

C语言支持多种整型数据类型——表示有限范围的整数。这些类型如图2-9和图2-10所示,其中还给出了“典型”32位和64位机器的取值范围。每种类型都能用关键字来指定大小,这些关键字包括char、short、long,同时还可以指示被表示的数字是非负数(声明为unsigned),或者可能是负数(默认)。如图2-3所示,为这些不同的大小分配的字节数根据程序编译为32位还是64位而有所不同。根据字节分配,不同的大小所能表示的值的范围是不同的。这里给出来的唯一一个与机器相关的取值范围是大小指示符long的。大多数64位机器使用8个字节的表示,比32位机器上使用的4个字节的表示的取值范围大很多。

无符号数的编码

编码补码

对于许多应用,我们还希望 表示负数值。最常见的有符号数的计算机表示方式就是 补码 (two’s-complement)形式。在这个定义中,将字的最高有效位解释为 负权 (negativeweight)。

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

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

C 语言中的有符号数和无符号数

如图2-9和图2-10所示,C语言支持所有整型数据类型的有符号和无符号运算。尽管C语言标准没有指定有符号数要采用某种表示,但是几乎所有的机器都使用补码。通常,大多数数字都默认为是有符号的。例如,当声明一个像12345或者0x1A2B这样的常量时,这个值就被认为是有符号的。要创建一个无符号常量,必须加上后缀字符‘U’或者‘u’,例如,12345U或者0x1A2Bu。 C语言允许无符号数和有符号数之间的转换。虽然C标准没有精确规定应如何进行这种转换,但大多数系统遵循的原则是底层的位表示保持不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数U2T。,而从有符号数转换为无符号数时,就是应用函数T2U,其中w表示数据类型的位数。

截断数字

假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。

关于有符号数和无符号数的建议

整数运算

Search

    微信好友

    博士的沙漏

    Table of Contents