整数表示
在本节中,我们描述用位来编码整数的两种不同的方式:一种只能表示非负数,而另一种能够表示负数,零和正数。后面我们将会看到他们的数学属性和机器级实现方面密切关联。我们会研究扩展或者收缩一个已编码整数以适应不同长度表示的效果。
2.2.1 整数数据类型
C语言支持多种整型数据类型------表示有限范围内的整数。这些类型如图2-8和2-9所示,其中还给出了“典型的”32位和64位机器的取值范围。
C语言类型 最小值 最大值
char -128 127
unsigned char 0 255
short -32768 32767
unsigned short 0 65535
int -2147483648 2147483647
unsigned 0 4294967295
long -2147483648 2147483647
unsigned long 0 4294967295
long long -9223372036854775808 9223372036854775807
unsigned long long 0 18446744073709551615
2-8 32位机器上C语言的整数数据类型的典型取值范围
C语言类型 最小值 最大值
char -128 127
unsigned char 0 255
short -32768 32767
unsigned short 0 65535
int -2147483648 2147483647
unsigned 0 4294967295
long -9223372036854775808 9223372036854775807
unsigned long 0 18446744073709551615
long long -9223372036854775808 9223372036854775807
unsigned long long 0 18446744073709551615
2-9 64位机器上C语言的整数数据类型的典型取值范围
每种类型都能用关键字来指定大小,这些关键字包括char,short,long,long long。同时可以指示被表示的数字是非负数,或者可能是负数(默认)。如图2-3所示,这些不同大小的分配的字节数会根据机器的字长和编译器有所不同。根据字节分配,不同大小所能表示的值的范围是不同的。这里给出唯一一个与机器相关的取值范围是大小指示符long的。大多数64位机器使用8字节表示,比如32位机器上使用4字节表示的范围大的多。
2.2.2 无符号数的编码
假设一个整数数据类型有w位。我们可以将位向量写成
在这个等式中,符号“=”表示左边被定义为等于右边。函数
([0001])=0* +0* +0* +1* =1
([0101])=0* +1* +0* +1* =4+1=5
([1011])=1* +0* +1* +1* =8+2+1=11
([1111])=1* +1* +1* +1* =8+4+2+1=15
让我们来考虑一下w位所能表示的值的范围。最小值是用位向量[00...0]表示也就是整数值0,而最大值是用位向量[11...1]表示,也就是整数值UMAX=
无符号数的二进制表示有一个重要的属性,就是每个介于0-
2.2.3 补码表示
对于许多应用,我们还希望表示负数值。最常见的有符号数的计算机表示方式就是补码形式。在这个定义中,将字的最高有效位解释为负权。我们用函数
最高有效位
([0001])=-0* +0* +0* +1* =1
([0101])=-0* +1* +0* +1* =4+1=5
([1011])=-1* +0* +1* +1* =-8+2+1=-5
([1111])=-1* +1* +1* +1* =-8+4+2+1=-1
我们可以看到,图2-11和图2-12中位模式都一样的。对等式(2-2)和等式(2-4)来说也一样,但是当最高有效位是1时,数值是不同的,这是因为在一种情况中,最高有效位的权重是+8,而另一种情况中,它的权重是-8。
让我们考虑一下w位补码所能表示的值的范围。它能表示的最小值是位向量[10...0](也就是设置这个位为位权,但是清除其他所有的位),其整数值为
我们可以看出
图2-13展示了针对不同字长,几个重要数字的位模式和数值。
字长 8 16 32 64
UMAX 0xff 0xffff 0xffffffff 0xffffffffffffffff
255 65535 4294967295 18446744073709551615
TMIN 0x80 0x8000 0x80000000 0x8000000000000000
-128 -32768 -2147483648 -9223372036854775808
TMAX 0x7f 0x7fff 0x7fffffff 0x7fffffffffffffff
127 32767 2147483647 9223372036854775807
-1 0xff 0xffff 0xffffffff 0xffffffffffffffff
0 0x00 0x0000 0x00000000 0x0000000000000000
前三个给出的是可表示的整数的范围,用
关于这些数字,有几点值得注意。第一,从图2-8和图2-9可以看出,补码范围是不对称的:|TMIN|=|TMAX|+1。也就是说,TMIN没有与之对应的正数。正如我们将会看到的,这导致补码运算的某些特殊属性,并且容易造成程序中细微的错误。之所以会有这样的不对称性,是因为一半的位模式表示负数,而一半的数表示非负数。因为0是非负数,也就意味着能表示的正数比负数少一个。第二,最大的无符号数值刚好比补码的最大值的两倍大一点
C库中的文件<limits.h>定义了常量INT_MAX,INT_MIN和UINT_MAX,它们描述了有符号和无符号整数的范围。对于一个补码机器,数据类型int有w位,这些常量就对应于
确定大小的整数类型
对于某些程序来说,用某个确定大小的表示来编码数据类型非常重要。
ISO C99标准在文件stdint.h中引入了另一类整数类型。这个文件定义了一组数据类型,它们的声明型如intN_t和uintN_t,指定的是N位有符号和无符号整数。N的具体值与实现相关,但是大多数编译器允许值为8,16,32和64.因此,通过将它的类型声明为uint16_t,我们可以无歧义的声明一个16位无符号变量,如果声明为int32_t,也就是一个32位有符号变量。
这些数据类型对应一组宏,定义每个N的值对应最小值和最大值。这些宏名字型如INTN_MAX,INTN_MAX和UINTN_MAX。
关于整数数据类型的取值范围和表示,java标准是非常明确的。它要求采用补码表示取值范围与图2-9中64位的情况一样。在java中单字节数据类型称为byte,而不是char,而且没有long long数据类型。这些非常具体的要求都是为了保证无论在什么机器上java程序运行的表现都能完全一样。
2.2.4 有符号和无符号之间的转化
C语言允许在各种不同的数字数据类型之间做强制类型转换。例如,假设变量x声明为int,u声明为unsigned。表达式(unsigned)x会将x的值转换为一个无符号数值。而(int)u将u的值转换为一个有符号数。将有符号数强制类型转换为无符号数,或者,反过来,会得到什么结果呢?对于大多数C语言实现来说,这个问题的回答都是从位级角度来看的,而不是数的角度。
short v=-12345;
unsigned short uv=(unsigned short)v;
printf("v=%d,uv=%un",v,uv);
在一台采用补码的机器上,上述代码会产生如下输出:
v=-12345,uv=53191
我们看到,强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。在图2-14中我们看到过,-12345的16位补码表示与53191的16位无符号表示完全一样的。将short int 强制类型转换为unsigned short 改变数值,但不改变位表示。
unsigned u =4294967205u;
int tu=(int)u;
printf("v=%u,tu=%dn",u,tu);
在一台采用补码的机器上,上述代码会产生如下输出:
u=4294967295,tu=-1
从图2-13我们可以看到,对于32位字长来说,无符号形式的4294967295(
对于大多数C语言的实现而言,处理同样字长的有符号和无符号数之间相互转换的一般规则是:数值可能会改变,但位模式不变。下面我们用更数学化的形式来描述这个规则。既然
现在将函数
继续前面的例子,从图2-14中,我们可以看到
接下来,我们看到函数U2T描述从无符号数到补码的转换。而T2U描述的是补码到无符号数的转换。这两个函数描述了大多数C语言实现中两种数据类型之间强制类型转换的效果。
为了更好的理解一个有符号数字x和与之对应的无符号数
这个关系对于证明无符号和补码运算之间的关系是很有用的。在X的补码表示中,位
反过来我们希望推导出无符号u和与之对应的有符号数
在u的无符号表示中,位
总结一下,我们考虑无符号数与补码表示之间相互转换的结果,对于0<=x<
2.2.5 C语言中有符号和无符号数
如图2-8和2-9所示,c语言支持所有整型数据类型的有符号和无符号运算。尽管c语言标准没有指定有符号数采用某种表示,但几乎所有的机器都使用补码。通常大多数数字都默认是有符号的。例如,当声明一个像12345或者0x1a2b这样的常量时,这个值就被认为是有符号的。要创建一个无符号常量,必须加上后缀字符'U'或者'u'。例如12345u和0x1a2bu。
C语言允许无符号数和有符号数之间的转换。转换的原则是底层的位表示不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数
显示的强制类型装换就会导致转换发生。就像下面的代码。
int tx,ty;
unsigned ux,uy;
tx=(int)ux;
uy=(unsigned)ty;
另外,当一种类型的表达式被赋值给另外一种类型的变量时,转换是隐式发生的,就像下面代码:
int tx,ty;
unsigned ux,uy;
tx=ux;
uy=ty;
当用printf输出数值时,分别用指示符%d,%u和%x,以有符号十进制,无符号十进制和十六进制格式输出一个数字。注意printf没有使用任何类型信息,所以他可以用指示符%u来输出类型为int的值,也可以用指示符%d输出类型为unsigned的数值。例如,考虑下面代码:
int x=-1;
unsigned u=2147483648;
printf("x=%u=%dn",x,x);
printf("u=%u=%dn",u,u);
当在一个32位机器上运行时,它的输出如下:
x=4294967295=-1
u=2147483648=-2147483648
两种情况下,printf首先将这两个字作为一个无符号数输出,然后把它作为一个有符号数输出。以下是实际运行的转换函数
由于C语言对同时包含有符号数和无符号数表达式的这种处理方式。出现了一些奇特的行为。当执行一个运算时,如果它的一个运算是有符号而另一个是无符号,那么C语言会隐式将有符号数强制类型转换为无符号数。并假设这两个数都是非负数,来执行这个运算。就像我们看到的,这种方法对于标准的算术运算来说并无多大的差异,但是想<和>这样的关系运算符来说,他会导致非直观的结果。如图,展示了一些关系表达式的示例以及它们得到的求值结果。这里假设使用一台采用补码的32位机器。考虑比较式-1<0u。因为第二个运算数是无符号的。第一个运算符就会被隐式的转换为无符号数。因此表达式就等价于4294967295u<0u,这个答案显然是错的。其他那些示例也可以通过相似的分析来理解。
表达式 类型 求值
0==0u 无符号 1
-1<0 有符号 1
-1<0u 无符号 0
2147483647>-214748367-1 有符号 1
2147483647u>-214748367-1 无符号 0
2147483647>-(int)214748368u 有符号 1
-1>-2 有符号 1
(unsigned)-1>-2 无符号 1
2.2.6 扩展一个数字的位表示
一种常见的运算是在不同字长的整数之间转换,同时又保持数值不变。当然,当目标数据类型太小以至于不能表示想要的值时,这根本就是不可能的,然而从一个较小的数据类型转换到一个较大的类型,这应该总是可能的。将一个无符号数转换为一个更大的数据类型。我们只要简单的在表示的开头添加0,这种运算称为零扩展。将一个补码数字转换为一个更大的数据类型可以执行符号扩展。规则是在表示中添加最高有效位的值的副本。由此可知,如果我们原始值的位表示为[
考虑字长w=3到w=4的符号扩展。位向量[101]表示值-4+1=-3。对他应用符号扩展,得到位向量[1101],表示值-8+4+1=-3。我们可以看到对于w=4,最高两位的组合值是-8+4=-4。与w=3时的符号位的值相同。类似的[111]和[1111]都表示-1。
如何证明符号扩展工作是否正确呢?我们想要证明的是
这里,在表达式的左边。我们增加了k位
([ , , ,... ])= ([ , ,..., ])
([ , , ,... ]=
=
=
=
= ([ , ,..., ])
我们使用的关键属性是
值得一提的是,从一个数据大小到另一个数据大小的转换,以及无符号和有符号数字之间的转换的相对顺序能够影响一个程序的行为。当把short转换为unsigned时,我们先改变大小,之后再完成从有符号到无符号的转换。也就是说(unsigned)sx等价于(unsigned)(int)sx。事实上,这个规则是C语言标准要求的。
2.2.7 截断数字
假设我们不用额外的位来扩展一个数值。而是减少表示一个数字的倍数。例如下面代码的情况:
int x=53191;
short sx=(short)x;
int y=sx;
在一台典型的32位机器上,当把x强制类型转换为short时,我们将32位的int截断为16位的short int。就像前面所看到的,这个16位的位模式就是-12345的补码表示。当我们把它强制类型转换回int时,符号扩展把高16位设置为1,从而生成-12345的32位补码表示。
将一个w位的数
对一个补码数字来说,补码数字截断的结果是
2.2.8 关于有符号数与无符号数的建议
就像我们看到的那样,有符号数到无符号数的隐式强制类型装换导致某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换细微差别的错误很难被发现。
我们已看到了由于许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞方式。避免这类错误的一种方法就是绝对不使用无符号数。实际上,除了C之外,很少有语言支持无符号整数。很明显,这些语言的设计者认为他们带来的麻烦比益处多的多。
当我们想要把字仅仅看成是位的集合,并没有任何数字意义时无符号数值是非常有用的,地址自然得就是无符号的。