2.2 整数表示
在本节中,我们描绘用位来编码整数的两种不同方式:一种只能表示非负数,而另一种能够表示负数、零和正数。
2.2.1 整数数据类型
C语言支持多种整型数据类型--表示有限范围的整数。每种类型都能用关键字来指定大小,这些关键字包括char,short,long,同时还可以指示被表示的数字是非负数(声明为unsigned),或者可能是负数(默认)。为这些不同的大小分配的字节数根据程序编译为32位还是64位而有所不同。根据字节分配,不同的大小所能表示的值的范围是不同的。
| C数据类型 | 32位最小值 | 32位最大值 | 64位最小值 | 64位最大值 |
| [signed] char | -128 | 127 | -128 | 127 |
| unsigned char | 0 | 255 | 0 | 255 |
| short | -32 768 | 32 767 | -32 768 | 32 767 |
| unsigned short | 0 | 65 535 | 0 | 65 535 |
| int | -2 147 483 648 | 2 147 483 647 | -2 147 483 648 | 2 147 483 647 |
| unsigned | 0 | 4 294 967 295 | 0 | 4 294 967 295 |
| long | -2 147 483 648 | 2 147 483 647 | - 9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
| unsigned long | 0 | 4 294 967 295 | 0 | 18 446 744 073 709 551 615 |
从上图中一个很值得注意的特点是取值范围不是对称的--负数的范围比整数的范围大1,当我们考虑如何表示负数时,会看到为什么会这样。C语言标准定义了每种数据类型必须能够表示的最小取值范围。
2.2.2 无符号数的编码
假设有一个整数数据类型有w位。我们可以将位向量写成(向量)x ,表示整个向量,或者写成[xw-1,xw-2,...,x0],表示向量中的每一位。把(向量)x看做一个二进制表示的数,就获得了(向量)x的无符号表示。
无符号数的二进制表示有一个很重要的属性,也就是每个介于0~2w-1之间的数都有唯一一个w位的值编码。例如,十进制值11作为无符号数,只有一个4位的表示,即[1011]。原理:无符号数编码的唯一性。
2.2.3 补码编码
对于很多应用,我们还希望表示负数值。最常见的有符号数的计算机表示方式就行补码形式。在这个定义中,将字的最高有效位解释为负权。
同无符号表示一样,在可表示的取值范围内每个数字都有一个唯一的w位的补码编码。这就导出了与无符号数相似的补码数原理,补码编码的唯一性。
下图展示了针对不同字长,几个重要数字的位模式和数值。前三个给出的是可表示的整数的范围,用UMaxw、TMinw和TMaxw来表示。关于这些数字,有几点需要注意。第一,补码的范围是不对称的:|TMin| = |TMax| + 1,也就是说,TMin没有与之对应的正数。之所以会有这样的不对称性,是因为一半的位模式(符号数设置为1的数)表示负数,而另一半(符号位设置为0的数)表示非负数。因为0是非负数,也就意味着能表示的正数比负数少一个。第二,最大的无符号数值刚好比补码的最大值的两倍大一点:UMaxw = 2TMaxw + 1。补码表示中所有表示负数的位模式在无符号中都变成了正数。图中也给出了常数-1和0的表示。注意-1和UMax有同样的位表示---一个全1的串。数值0在两种表示方式中都是全0的串。
| 数 | 字长w | |||
| 8 | 16 | 32 | 64 | |
| UMaxw | 0xFF | 0xFFFF | 0xFFFFFFFF | 0xFFFFFFFFFFFFFFFF |
| 255 | 65 535 | 4 294 967 295 | 18 446 744 073 709 551 615 | |
| TMinw | 0x80 | 0x8000 | 0x80000000 | 0x8000000000000000 |
| -128 | -32 768 | -2 147 483 648 | -9 223 372 036 854 775 808 | |
| TMaxw | 0x7F | 0x7FFF | 0xFFFFFFF | 0x7FFFFFFFFFFFFFFF |
| 127 | 32 767 | 2 147 483 647 | 9 223 372 036 854 775 807 | |
| -1 | 0xFF | 0xFFFF | 0xFFFFFFFF | 0xFFFFFFFFFFFFFFFF |
| 0 | 0x00 | 0x0000 | 0x00000000 | 0x0000000000000000 |
C语言标准并没有要求要用补码来表示有符号整数,但几乎所有的机器都是这么做的。
有符号数的其他表示方法:反码:除了最高有效位的权是-(2w-1 - 1)而不是-2w-1,它和补码是一样的。原码:最高有效位是符号位,用来确定剩下的位应该取负权还是正权。
2.2.4 有符号和无符号数之间的切换
C语言允许在各种不同的数字数据类型之间做强制类型转换。例如,假设变量x声明为int,u声明为unsigned。表达式(unsigned)x会将x的值转换为一个无符号数值。而(int)u会将u的值转换成一个有符号整数。将有符号数强制类型转换为无符号数,或者反过来,会得到什么结果呢?对应大多数C语言的实现来说,对这个问题的回答都是从位级角度来看的,而不是数的角度。
比如考虑下面的代码:
short int v = -12345; unsigned short uv = (ungigned short) v ; printf("v = %d, uv = %u\n",v, uv)
在一台采用补码的机器上,上面代码会产生如下输出
v=-12345, uv = 53191
我们看到,强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。-12 345的16位补码表示与53 191的16位无符号表示是完全一样的。将short强制类型转换为unsigned short 改变数值,但是不改变位表示。类似的,考虑下面的代码:
unsigned u =4294967295u; int tu = (int) u; printf("u=%u,tu=%d",u,tu);
在一台采用补码的机器上,上述代码会产生如下输出:
u = 4294967295, tu=-1
对于32位字长来说,无符号形式 的4 294 967 295(UMax32)和补码形式的-1的位模式是完全一样的。将unsigned强制类型转换为int,底层的位保持不变。
对于大多数C语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。让我们用更数学化的形式来描述这个规则。我们定义函数U2Bw和T2Bw,它们将数值映射为无符号数和补码形式的表示。也就是说,给定0 <= x <= UMaxw范围内的一个整数x,函数U2Bw(x)会给出x的唯一的w位无符号表示。相似地,当x满足TMinw<=x<=TMaxw,函数T2Bw(x)会给出x的唯一的w位补码表示。
继续前面的例子,我们看到T2U16(-12 345) = 53 191,并且U2T16(53 191) = -12 345。也就是说,十六进制表示写作0xCFC7的16位位模式即是-12 345的表示又是53 191的无符号表示。同时请注意12 345 + 53 191=65 536 = 216。这个属性可以推广到给定位模式的两个数值(补码和无符号数)之间的关系。类似的,T2U32(-1) = 4 294 967 295,并且U2T32( 294 967 295)= -1。也就是说,无符号表示中的UMax有着和补码表示的-1相同的位模式。我们在这两个数之间也能看到这种关系:1+UMaxw = 2w。
练习题 2.19 填写下列描述函数T2U4的表格
| x | T2U4(x) |
| -8 | 8 |
| -3 | 13 |
| -2 | 14 |
| -1 | 15 |
| 0 | 0 |
| 5 | 5 |
通过上述的例子,我们可以看到给定位模式的补码与无符号数之间的关系可以表示为函数的一个属性:
原理:补码转换为无符号数

比如,我们看到T2U16(-12 345) = -12 345 +216 = 53 191,同时T2Uw(-1) = -1+2w = UMaxw。
总结一下,我们考虑无符号数与补码表示之间互相转换的结果。对于在范围 0 <= x <=TMaxw之内的值x而言,我们得到T2Uw(x)=x和U2Tw(x)=x。也就是说,在这个范围内的数字有相同的无符号和补码表示。对于这个范围以外的数值,转换需要加上或者减去2w。例如,我们有T2Uw(-1)=-1+2w=UMaxw——最靠近0的负数映射为最大的无符号数。在另一个极端,我们可以看到T2Uw(TMinw)=-2w-1+2w=2w-1=TMaxw+1——最小的负数映射为一个刚好在补码的正数范围之外的无符号数。我们能看到T2U16(-12 345) = 65 536 + -12 345 = 53 191。
2.2.5 C语言中的有符号数与无符号数
C语言支持所有整数数据类型的有符号和无符号运算。尽量C语言标准没有指定有符号数要采用某种表示,但是几乎所有的机器都使用补码。通常,大多数数字都默认为是有符号的。例如,当声明一个像 12345或者0x1A2B这样的常量时,这个值就被认为是有符号的。要创建一个无符号常量,必须加上后缀字符'U'或者'u',例如,12345U或者0x1A2Bu。
C语音允许无符号和有符号数之间的转换。虽然C标准没有精确规定如何进行这种转换,但大多数系统遵循的是底层的位保持不变。因此,在一台采用补码的机器上,当从无符号数转换为有符号数时,效果就是应用函数B2Tw,而从有符号数转换为无符号数时,就是应用函数T2Uw,其中w表示数据类型的位数。
当用printf输出数值时,分别用指示符%d、%u和%x以有符号十进制、无符号十进制和十六进制格式输出一个数字。例如,考虑下面的代码:
int x = -1; unsigned u = 2147483648; printf("x = %u = %d \n", x, x); printf("x = %u = %d \n", u, u);
当在一个32位的机器上运行时,它的输出如下:
x = 4294967295 = -1 x = 2147483648 = -2147483648
在这两种情况下,prinft首先将这个字当做一个无符号数输出,然后把它当做一个有符号数输出。以下是实际运行中的转换函数:T2U32(-1)=UMax32=232-1和U2T32(232)=231-232=-231=TMin32。
由于C语言对同时包含有符号和无符号表达式的这种处理方式,出现了一些奇特的行为。当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设两个数都是非负的,来执行这个运算。考虑比较式-1 < 0U。因为第二个运算数是无符号的,第一个运算数就会被隐式的转换为无符号数,因此表达式就等价于 4294967295<0U,这个答案显然是错的。其他示例也可以通过相似的分析来理解
| 表 达 式 | 类 型 | 求 值 |
| 0 == 0U | 无符号 | 1 |
| -1 < 0 | 有符号 | 1 |
| -1 < 0U | 无符号 | 0 |
| 2147483647 > -2147483647-1 | 有符号 | 1 |
| 2147483647U > -2147483647-1 | 无符号 | 0 |
| 2147483647 > (int)2147483648U | 有符号 | 1 |
| -1 > -2 | 有符号 | 0 |
| (unsigned)-1 >-2 | 无符号 | 1 |
2.2.6 扩展一个数字的位表示
一个常见的运算是在不同字长的整数之间的转换,同时又保持数值不变。当前,当目标数据类型太小以至于不能表示想要的值时,这根本就是不可能的。然而,从一个较小的数据类型转换到一个较大的类型,应该总是可能的。
要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示的开头添加0。这种运算被称为零扩展。 原理:无符号数的零扩展
要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加最高有效位的值。 原理:补码数的符号扩展
考虑下面代码:
short sx = -12345; unsigned short usx = sx; int x = sx; unsigned ux = usx; printf("sx = %d \n", sx); printf("usx = %u \n", usx); printf("x = %d \n", x); printf("ux = %u \n", ux);
在采用补码表示的32位大端机器上运行这段代码,打印如下输出:
sx = -12345 cf c7 usx = 53191 cf c7 x = -12345 ff ff cf c7 ux = 53191 00 00 cf c7
我们看到,尽管-12 345的补码表示和53 191的无符号表示在16位字长时是相同的,但是在32位字长时却是不相同的。特别地,-12 345的十六进制表示为0xFFFFCFC7,而53 191的十六进制表示为0x0000CFC7。前者使用的是符号扩展--最开头加了16位,都是最高有效位1,表示为十六进制就是0xFFFF。后者开头使用16个0来扩展,表示为十六进制就是0x0000。
从字长w=3到w=4的符号扩展,位向量 [101]表示值-4+1=-3。对它应用符号扩展,得到位向量[1101],表示的值-8+4+1=-3。我们可以看到,对于w=4,最高两位的组合值时-8+4=-4,与w=3时符号位的值相同。类似地,位向量[111]和[1111]都表示值-1。
2.2.7 截断数字
假设我们不用额外的位来扩展一个数值,而是减少表示一个数字的位数。例如下面代码:
int x = 53191; short sx = (short)x; //-12345 int y = sx; //-12345
当我们把x强制类型转换为short时,我们就将32位的int截断为16位的short int。就像前面所看到的,这个16位的位模式就是-12 345的补码表示。当我们把它强制类型转回int时,符号扩展把高16位设置为1,从而生成-12 345的32位补码表示。
当将一个w位的数向量x=[xw-1,xw-2,...,x0]截断为一个k位数字时,我们会丢弃高w-k位,得到一个位向量x'=[xk-1,xk-2,...,x0]。截断一个数字可能会改变它的值--溢出的一种形式。对于一个无符号数,我们可以很容易得到其数值结果。
练习题 2.24 假设将一个4位数值(0-F)截断到一个3位数值(0-7)
| 十六进制 | 无符号 | 补码 | |||
| 原始值 | 截断值 | 原始值 | 截断值 | 原始值 | 截断值 |
| 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2 | 2 | 2 | 2 | 2 |
| 9 | 1 | 9 | 1 | -7 | 1 |
| B | 3 | 11 | 3 | -5 | 3 |
| F | 7 | 15 | 7 | -1 | -1 |
2.2.8 关于有符号数与无符号数的建议
就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。因为这种强制类型转换时在代码中没有明确指示的情况下发生的,程序员经常忽视了它的影响。
我们已经看到了很多无符号运算的细微特性,尤其是有符号数到无符号数的隐式转换,会导致错误或者漏洞的方式。避免这类错误的一种方法就是绝不使用无符号数。实际上,除了C语言以外很少有语言支持无符号整数。很明显,这些语言的设计者认为他们带来的麻烦要比益处多得多。比如,Java只支持有符号整数,并且要求以补码运算来实现。

浙公网安备 33010602011771号