long int能表示的数的范围_深入理解计算机系统之信息的表示和处理(三)

f355c9bdc26e2948c82282b800d93def.png

整数表示

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

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位。我们可以将位向量写成

,表示整个向量,或者写成[
,...,
],表示向量中的每一位。把
看成一个二进制表示的数就获取了
的无符号表示。我们用一个函数
的缩写长度为w来表示。

(
)=

在这个等式中,符号“=”表示左边被定义为等于右边。函数

将一个长度为w的0,1串映射到非负数。举一个例子,图2-11展示的是下面几种情况B2U给出位向量到整数的映射。
 ([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=

=
-1。w=4位为例,
。因此函数
能够被定义为一个映射
:{0,1}
{0,...,
}。

无符号数的二进制表示有一个重要的属性,就是每个介于0-

之间的整数都有唯一一个w位的值的范围。例如,十进制值11作为无符号数,只有一个4位的表示即[1011]。这个属性用数学语言描述就是函数
是个双射------对于每一个长度为w的位向量,都有一个唯一的值与之对应,反过来在0-
之间的每个整数都有一个唯一的长度为w的位向量二进制表示与之对应。

2.2.3 补码表示

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

来表示。

(
)=
+

最高有效位

也称为符号位,它的“权重”为
,是无符号表示中权重的负数。符号位被设置为1时,表示值为负,而当设置为0时,值为非负。这里看一下示例,图2-12展示的是下面几种情况下B2T给出的从位向量到整数的映射。
 ([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](也就是设置这个位为位权,但是清除其他所有的位),其整数值为

。而最大值是位向量[01...1](清除具有负权的位,而设置其他所有的位)。其整数值为
。以长度为4为例,我们有

我们可以看出

是一个从长度为w的位模式到
之间数字的映射,写做
:{0,1}
{
,...,
}。同无符号表示一样,可以表示的取值范围内的每个数字都有唯一的w为的补码编码。用数学语言来说就是
是一个双射------每个长度为w的位向量都对应一个唯一的值;反过来,每个介于
之间的整数都有一个唯一的长度为w的位向量二进制表示。

图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

前三个给出的是可表示的整数的范围,用

来表示。在后面的讨论中,我们还会经常引用到这三个特殊值。如果可以从上下文中推断出w,或者w不是讨论的主要内容时,我们会省略下标w,直接引用UMAX,TMIN和TMAX。

关于这些数字,有几点值得注意。第一,从图2-8和图2-9可以看出,补码范围是不对称的:|TMIN|=|TMAX|+1。也就是说,TMIN没有与之对应的正数。正如我们将会看到的,这导致补码运算的某些特殊属性,并且容易造成程序中细微的错误。之所以会有这样的不对称性,是因为一半的位模式表示负数,而一半的数表示非负数。因为0是非负数,也就意味着能表示的正数比负数少一个。第二,最大的无符号数值刚好比补码的最大值的两倍大一点

=
。补码表示中所有表示负数的位模式在无符号表示中都变成了正数。图2-13页给出了常量数-1和0的表示。注意-1和UMAX有同样的位表示------一个全1的串。数值0在两种表示方式中都是全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(

)和补码形式的-1的位模式是完全一样的。将unsigned int 强制转换成int,底层的位表示保持不变。

对于大多数C语言的实现而言,处理同样字长的有符号和无符号数之间相互转换的一般规则是:数值可能会改变,但位模式不变。下面我们用更数学化的形式来描述这个规则。既然

都是双射,它们就有定义明确的逆映射将
定义为
,而将
定义为
。这些函数给出了一个数值的无符号或者补码的位模式。也就是说,给定 0<=x<
范围内的一个整数x,函数
会给出X的唯一的w位无符号表示。相似的,当满足
<=x<
,函数
会给出x的唯一的w位补码表示。可以观察到,对于在范围 0<=x<
内的值,这两个函数将生成同样的位模式------最高位是0,因此这个位是正权还是负权就没有关系了。

现在将函数

定义为
。这个函数的输入是一个0~
之间的的数,结果得到一个
~
之间的值。这里两个数有相同的位模式,除了参数是无符号的,而结果是补码表示的,类似的,对于
~
之间的值x,函数
,生成一个数的无符号表示和X的补码表示相同。

继续前面的例子,从图2-14中,我们可以看到

,并且
。也就是说,十六进制表示写作0xcfc7,的16位模式即使-12345的补码表示,又是53191的无符号表示。类似地,从图2-13我们看到
=4294967295。并且
。也就是说,无符号表示中的UMax有着和补码表示的-1同样的位模式。

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

为了更好的理解一个有符号数字x和与之对应的无符号数

之间的关系,我们可以利用它们有相同位模式 表示这一事实,推导出一个数学关系。比较等式(2-1)和等式(2-3)可以发现对于位模式
,如果我们计算
之差的位的加权和相互抵消掉,剩下一个值
。这就得到一个关系:
。如果令
,我们就得到以下公式

这个关系对于证明无符号和补码运算之间的关系是很有用的。在X的补码表示中,位

决定了x是否为负,得到

反过来我们希望推导出无符号u和与之对应的有符号数

之间的关系。如果用
=
,我们得到以下公式

在u的无符号表示中,位

决定了U是否大于或者等于
,得到

总结一下,我们考虑无符号数与补码表示之间相互转换的结果,对于0<=x<

范围之内的值x而言,我们得到
。也就是说,在这个范围内的数字有相同的无符号和补码表示。对于这个范围以外的数值,转换需要加上或者减去
。例如,我们有
-----最靠近0的负数映射为最大的无符号数。在另一个极端,我们可以看到
------最小的负数映射为一个刚好在补码的正数范围之外的无符号数。使用如图2-14的示例,我们能得到

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

如图2-8和2-9所示,c语言支持所有整型数据类型的有符号和无符号运算。尽管c语言标准没有指定有符号数采用某种表示,但几乎所有的机器都使用补码。通常大多数数字都默认是有符号的。例如,当声明一个像12345或者0x1a2b这样的常量时,这个值就被认为是有符号的。要创建一个无符号常量,必须加上后缀字符'U'或者'u'。例如12345u和0x1a2bu。

C语言允许无符号数和有符号数之间的转换。转换的原则是底层的位表示不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数

而从有符号数转为无符号数时,就是应用函数
其中w表示数据类型的位数。

显示的强制类型装换就会导致转换发生。就像下面的代码。

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位

的副本。下面的证明是对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位的数

=[
,
,...,
]截断为一个K位数字时,我们会丢弃高w-k得到一个位向量
=[
,
,...,
]。截断一个数字可能会改变他的值-----溢出的一种形式。我们现在来研究什么样的数值会产生这种情况。对于一个无符号数字x截断它到k位的结果就相当于计算X mod
。通过对等式(2-1)应用取模运算就可以得到。

对一个补码数字来说,补码数字截断的结果是

2.2.8 关于有符号数与无符号数的建议

就像我们看到的那样,有符号数到无符号数的隐式强制类型装换导致某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换细微差别的错误很难被发现。

我们已看到了由于许多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞方式。避免这类错误的一种方法就是绝对不使用无符号数。实际上,除了C之外,很少有语言支持无符号整数。很明显,这些语言的设计者认为他们带来的麻烦比益处多的多。

当我们想要把字仅仅看成是位的集合,并没有任何数字意义时无符号数值是非常有用的,地址自然得就是无符号的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值