信息的表示和处理之整数表示
整数表示
图2-3列出了我们引入的数学术语,用于精确定义和描述计算机如何编码和操作整数:
图2-3 整数的数据与算数操作术语。下表w表示数据的位数
整型数据类型
C语言支持多种整型数据类型——表示有限范围整数,如图2-4和2-5所示:
图2-4 32位程序上C语言整型数据类型的典型取值范围
图2-5 64位程序上C语言整型数据类型的典型取值范围
图2-4和2-5中一个值得注意的点是取值范围不是对称的,负数的范围比整数多1。C语言标准定义了每种数据类型必须能够表示的最小的取值范围,如图2-6所示:
图2-6 C语言的整型数据类型保证的取值范围
图2-6的取值范围相比图2-4和2-5的典型实现一样,或者更小些。除了固定大小的数据类型是例外,我们看到他们只要求正数和负数的取值范围是对称的。此外,数据类型int可以用2个字节的数字来实现,这几乎回退到16位机器的时代。还可以看到,long的大小可以用4个字节的数字来实现,对32位程序来说这是很典型的。固定大小的数据类型保证数值的范围与图2-4给出的典型数值一致,包括负数与正数的不对称性。
无符号数的编码
假设一个整数数据类型有w位,我们可以将位向量写成,表示整个向量,或者写成[xw-1,xw-2,……,x0],表示向量中的每一位。把看做一个二进制表示的数,就获得了的无符号表示。在这个编码中,每个位xi都取值为0或1,后一种取值意味着数值2i应为数字值的一部分。我们用一个函数B2Uw(Binary to Unsigned的缩写,长度为w)来表示:
符号“”表示左边被定义为等于右边。函数B2Uw将一个长度为w的0、1串映射到非负整数。举个栗子,图2-6展示的是下面几种情况下B2U给出的从位向量到整数的映射:
在图2-7中,我们使用长度为2i的指向右侧箭头的条来表示每个位的位置i。每个位向量对应的数值就等于所有值为1的位对应的条的长度之和。
图2-7 w=4的无符号数示例。当二进制表示中位i为1,数值就会相应加上2i
让我们来考虑下w位所能表示的值的范围。最小值是用位向量[00……0]表示,也就是整数值0,而最大值是用位向量[11……1]表示,也就是整数值。以4位情况为例,UMax4=B2U4([1111])=24-1=15。因此,函数B2Uw能够被定义为一个映射B2Uw:{0,1}w→{0,……,2w-1}。
无符号数的二进制表示有一个很重要的属性,每个介于0~2w-1之间的数都有唯一一个w位的值编码。例如,十进制11作为无符号数,只有一个4位的表示,即[1011]。
原理:无符号数编码的唯一性。
函数B2Uw是一个双射。
数学术语双射是指一个函数f有两面:它将数值x映射为数值y,即y=f(x),但它也可以反向操作,因为对每个y而言,都有唯一一个数值x使得f(x)=y。这可以用反函数f-1表示,即x=f-1(y)。函数B2Uw将每一个长度为w的位向量都映射为0~2w-1之间的一个唯一值;反过来,我们称其为U2Bw(即“无符号数到二进制”),在0~2w-1之间的每一个整数都可以映射为一个唯一的长度为w的位模式。
补码编码
对于许多应用,我们还希望表示负值。最常见的有符号数的计算机表示方式为补码形式。我们用函数B2Tw(Binary to Two's-complement的缩写,长度为w)来表示:
原理:补码编码的定义:
最高有效位xw-1也称为符号位,它的“权重”为-2w-1,是无符号表示中权重的负数。当符号位被置位1时,表示值为负。这里来看一个示例,图2-8展示的是下面几种情况下B2T给出的从位向量到整数的映射。
我们可以看到,图2-7和2-8中位模式一样,对等式(2.2)和等式(2.4)来说也是一样,但是当最高有效位是1时,数值是不同的,这是因为在一种情况中,最高有效位的权重是+8,而另一种情况是-8。
图2-8 w=4的补码示例。把位3作为符号位,因此当它为1时,对数值的影响是-23=-8。这个权重在图中用带向左箭头的条来表示。
让我们来考虑下w位补码所能表示的范围,最小值为[10……0],也就是设置这个值为负权,但是清除其他所有位,其整数值为。而最大值是位向量[01……1],清除具有负权的位,而设置其他所有的位,其整数值为。以长度为4为例,我们有TMin4=B2T4([1000])=-23=-8,而TMax4=B2T4([0111])=22+21+20=4+2+1=7。
我们可以看出B2Tw是一个从长度为w的位模式到TMinw和TMaxw之间数字的映射:写作B2Tw:{0,1}w→{TMinw,……,TMaxw}。同无符号表示一样,在可表示的取值范围内的每个数字都有一个唯一的w位的补码编码。这就导出了无符号数相似的补码数原理:
原理:补码编码的唯一性
函数B2Tw是一个双射。
我们定义函数T2Bw(即“补码到二进制”)作为B2Tw的反函数。也就是说,对于每个数x,满足TMinw<=x<=TMaxw,则T2Bw(x)是x的(唯一的)w位模式。
为了更好的理解补码的表示,我们考虑下面的代码:
#include <stdio.h> typedef unsigned char *byte_pointer; void show_bytes(byte_pointer start, size_t len) { size_t i; for (i = 0; i < len; i++) printf(" %.2x", start[i]); printf("\n"); } int main(int argc, char *argv[]) { short x = 12345; short mx = -x; show_bytes((byte_pointer)&x, sizeof(short)); show_bytes((byte_pointer)&mx, sizeof(short)); return 0; }
当在大端法机器上运行时,这段代码的输出为30 39和cf c7,x的十六进制表示为0x3039,而mx的十六进制表示为0xCFC7。将它们展开为二进制,我们得到x的位模式为[0011 0000 0011 1001],而mx的位模式为[1100 1111 1100 0111]。如图2-9所示,等式(2.3)对这两个位模式生成的值为12345和-12345。
图2-9 12345和-12345的补码表示,以及53191的无符号表示。
有符号数和无符号数之间的转换
C语言允许在各种不同的数字数据类型之间做强制类型转换。假设变量x声明为int,u声明为unsigned。表达式(unsigned)x会将x的值转换成一个无符号数值,而(int)u将u的值转换成一个有符号整数。将有符号整数强制类型转换成无符号数,或者反过来,会得到什么样的结果?从数学的角度来说,可以想象到几种不同的规则。很明显,对于在两种形式都能表示的值,我们希望能保持不变。另一方面,将负数转成无符号数可能得到0。如果转换的无符号数太大以至于超过了补码能够表示的范围,可能会得到TMax。不过,对于大多数C语言的实现来说,对于这个问题的回答都是从位级角度来看,而不是数的角度。
比如说,考虑下面的代码:
#include <stdio.h> int main(int argc, char *argv[]) { short int v = -12345; unsigned short uv = (unsigned short) v; printf("v = %d, uv = %u\n", v, uv); return 0; }
在一台采用补码的机器上,上述代码会产生如下输出:v = -12345, uv = 53191。我们看到,强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。在图2-9中我们看到过,-12 345的16位补码表示与53 191的16位无符号表示完全一样。将short强制类型转换为unsigned short改变数值,但是不改变位表示。
类似地,考虑下面的代码:
#include <stdio.h> int main(int argc, char *argv[]) { unsigned u = 4294967295u; int tu = (int) u; printf("u = %u, tu = %d\n", u, tu); return 0; }
在一台采用补码的机器上,上述代码会产生如下输出:
u = 4294967295, tu = -1
对于32位字长来说,无符号形式的4294967295(UMax32)和补码形式的-1的位模式完全一样。将unsigned强制类型转换为int,底层的位表示保持不变。
对于大多数C语言实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,位模式不变。我们定义U2Bw和T2Bw,它们将数值映射为无符号数和补码形式的位表示。也就是说,给出0<=x<=UMaxw范围内的一个整数x,函数U2Bw(x)会给出x的唯一的w位无符号表示。相似地,当x满足TMinw<=x<=TMaxw,函数T2Bw(x)会给出x的唯一的w位补码表示。
现在,将函数T2Uw定义为。这个函数的输入是一个TMinw~TMaxw的数,结果得到一个0~UMaxw的值,这里两个数有相同的位模式,除了参数是无符号的,而结果是以补码表示的。类似的,对于0~UMaxw之间的值x,定义函数U2Tw为。生成一个数的无符号数表示和x的补码表示相同。
从图2-9中,我们看到T2U16(-12 345)=53 191,并且U2T16(53 191)=-12 345。也就是说,十六进制表示写作0xCFC7的16位位模式即是-12 345的补码表示,又是53 191的无符号表示。同时请注意,12 345+53 191=65 536=216。这个属性可以推广到给定位模式的两个数值(补码和无符号数)之间的关系。类似地,从图2-10我们看到T2U32(-1)=4 294 967 295,并且U2T32(4 294 967 295)=-1。也就是说,无符号表示中的UMax有着和补码表示的-1相同的位模式。我们将在这两个数之间也能看到这种关系1+UMaxw=2w。
图2-10 重要的数字。图中给出了数值和十六进制表示
原理:补码转换为无符号数
对于满足TMinw<=x<=TMaxw的x有:
比如,我们看到的T2U16(-12 345)=-12 345+216=53 191,同时T2Uw(-1)=-1+2w=UMaxw。该属性可通过公式(2.1)和(2.3)推导出来。
推导:补码转换为无符号数
比较等式(2.1)和等式(2.3),我们可以发现对于位模式,如果我们计算之差,从0到w-2的位的加权和将互相抵消掉,剩下一个值:,这就得到一个关系:。我们因此有:
根据公式(2.5)的两种情况,在x的补码表示中,位xw-1决定了x是否为负。
比如说,图2-11比较了当w=4时函数B2U和B2T是如何将数值变成位模式的。对补码来说,最高有效位是符号位,我们用向左箭头的条来表示。对于无符号数来说,最高有效位是正权重,我们用向右箭头表示。从补码变为无符号数,最高有效位的权重从+8变为-8。因此,补码表示的负数如果看成无符号数,值会增加24=16。因而-5变成11,而-1变成+15。
图2-11 比较当w=4时无符号表示和补码表示
图2-12说明了函数T2U的一般行为。如图所示,当将一个有符号数映射为它相应的无符号数时,负数就会被转成大的正数,而非负数则保持不变。
图2-12 从补码到无符号数的转换。函数T2U将负数转为较大的正数
原理:无符号数转换为补码
对于满足0<=u<=UMaxw的u有:
该原理证明如下:
推导:无符号数转换为补码
设,这个位向量也是U2Tw(u)的补码表示。公式(2.1)和(2.3)结合起来有
在u的无符号表示中,对公式(2.7)的两种情况来说,位uw-1决定了u是否大于TMaxw=2w-1-1。
图2-13说明了函数U2T的行为。对于小的数(<=TMaxw),从无符号到有符号的转换将保留数字的原值。对于大的数(TMaxw),数字将转为一个负值。
图2-13 从无符号数到补码的转换。函数U2T把大于2w-1-1的数字转换为负值
总结一下,我们考虑无符号与补码表示之间互相转换的结果。对于范围在0<=x<=TMaxw之内的值,我们得到T2Uw(x)=x和U2Tw(x)=x。也就是说,这个范围内的数字有相同的无符号和补码表示。对于这个范围以外的数值,转换需要加上或减去2w。例如,我们有T2Uw(-1)=-1+2w=UMaxw,——最靠近0的负数映射为最大的无符号数。在另一个极端,我们可以看到T2Uw(TMinw)=-2w-1+2w=2w-1=TMaxw+1——最小的负数映射为一个刚好在补码的正数范围之外的无符号数。使用图2-9为例,我们可以看到T2U16(-12 345)=65 536-12 345=53 191。
C语言的有符号数和无符号数
C语言支持所有整型数据类型的有符号和无符号运算,尽管C语言标准没有指定有符号数要采用某种表示,但几乎所有机器都使用补码。通常,大多数数字都默认是有符号的。要创建一个无符号常量,必须加上后缀字符'U'或'u',例如,12345U或0x1A2Bu。C语言允许无符号数和有符号数之间的转换,虽然C标准没有精确规定应如何进行转换,但大多数系统遵循的原则是底层的位保持不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数U2Tw, 而从有符号数转换为无符号数时,就是应用函数T2Uw,其中w表示数据类型的位数。
显示的强制类型转换就会导致转换发生,如下面的代码:
int tx, ty; unsigned ux, uy; tx = (int) ux; uy = (unsigned) ty;
另外,当一种类型的表达式被赋值给另外一种类型的变量时,转换是隐式发生的,就像下面的代码:
int tx, ty; unsigned ux, uy; tx = ux; /* Cast to signed */ uy = ty; /* Cast to unsigned */
当用printf输出数值时,分别用指示符%d、%u和%x以有符号十进制、无符号十进制和十六进制格式输出一个数字。注意,printf没有使用任何类型信息,所以它可以用指示符%u来输出类型为int的数值,也可以用指示符%d输出类型为signed的数值。考虑下面的代码:
#include <stdio.h> int main(int argc, char *argv[]) { int x = -1; unsigned u = 2147483648; printf("x = %u = %d\n", x, x); printf("u = %u = %d\n", u, u); return 0; }
运行结果:
x = 4294967295 = -1 u = 2147483648 = -2147483648
在这两种情况下,printf首先将变量当做一个无符号输出,然后当做一个有符号输出。以下是实际运行中的转换函数:T2U32(-1)=UMax32=232-1和U2T32(231)=231-232=-231=TMin32。
由于C语言对同时包含有符号数和无符号数表达式的这种处理方式,出现了一些奇特的行为。当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。图2-14展示了一些关系表达式的示例以及它们得到的求值结果,这里假设数据类型int表示为32位补码。考虑比较式-1<0U,因为第二个运算数是无符号的,第一个运算数会被隐式地转换为无符号数。因此表达式等价于4294967295U<0U,这个答案显然是错的。其他示例也可以通过相似的分析来理解。
图2-14 C语言的升级规则的效果。
注:非直观的情况标注了‘*’,当一个运算数是无符号的时候,另一个运算数也会被强制隐式地转换为无符号。
扩展一个数字的位表示
一个常见的运算是在不同字节长度的整数之间转换,同时又保持数值不变。要将一个无符号数转换成一个更大的数据类型,我们只要简单地在表示的开头添加0。这种运算被称为零扩展,表示原理如下:
原理:无符号数的零扩展
定义宽度为w的位向量和宽度为w'的位向量,其中w'>w,则。
按照公式(2.1),该原理可以看做是直接遵循了无符号数编码的定义。
要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加更高有效位的值,表示为如下原理:
原理:补码数的符号位扩展
定义宽度为w的位向量和宽度为w'的位向量,其中w'>w。则。
图2-15给出了从字长w=3到w=4的符号扩展的结果。位向量[101]=-4+1=-3。对它应用符号位扩展,得到位向量[1101],表示的值-8+4+1=-3。我们可以看到,对于w=4,最高两位的组合值是-8+4=-4,与w=3时符号位的值相同。类似地,位向量[111]和[1111]都表示-1。
图2-15 从w=3到w=4的符号扩展示例。对于w=4,最高两位组合权重为-8+4=-4,与w=3时的符号位权重一样
有了这个直觉,我们现在可以展示保持补码值的符号扩展。
推导:补码数值的符号扩展
令w'=w+k,我们想要证明的是
下面的证明是对k进行归纳。也就是说,如果我们能证明符号扩展一位保持的数值不变,那么符号扩展任意位都能保持这种属性。因此,证明任务变成了:
用等式(2.3)展开左边的表达式,得到:
我们使用关键属性是2w-2w-1=2w-1。因此,加上一个权值为-2w的位,和将一个权值为-2w-1的位转换为一个权值为-2w-1的位,这两种运算的综合效果就会保持原始的数值。
截断数字
假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。例如下面代码中的情况:
int x = 53191; short sx = (short) x; /* -12345 */ int y = sx; /* -12345 */
当我们把x强制类型转换为short时,我们就将32位的int截断为16位的short int。当我们把它强制类型转换为int时,符号扩展把高位设置为1,从而生成-12345的32位补码表示。
当将一个w位的数截断为一个k位数字时,我们会丢弃高w-k位,得到一个位向量。截断一个数字可能会改变它的值——溢出的一种形式。对于一个无符号数,我们可以很容易地得出其数值的结果。
原理:截断无符号数
令等于向量,而是将其截断为k位的结果:。令。则。
该原理背后的直觉就是所有被截取的位其权重形式都为2i,其中i>=k,因此,每一个权在取模操作下结构都为0,可用如下推导表示:
推导:截断无符号数
通过对等式(2.1)应用取模运算就可以看到:
在这段推导中,我们利用了属性:对于任何i>=k,2i mod 2k=0。
补码截断也具有相似的属性,只不过要将高位转换为符号位:
原理:截断补码数值
令等于向量,而是将其截断为k位的结果:。令。则。
在这个公式中,x mod 2k 将是0到2k-1之间的一个数。对其应用函数U2Tk产生的效果是把最高有效位xk-1的权重从2k-1转变为-2k-1。举例来看,将数值x=53 191从int转换为short。由于216=65 536>=x,我们有x mod 216=x。但是,当我们把这个数转换为16位的补码时,我们得到x'=53 191- 65 536=-12 345。
推导:截断补码数值
使用与无符号数截断相同的参数,则有
也就是,x mod 216能够被一个位级表示为的无符号数表示。将其转换为补码则有。
总而言之,无符号数的截断结果是:
而补码数字的截断结果是: