整数在机器中的表示可以分为无符号数(非负数)和有符号数两种。虽然最终的编码结果都是二进制数据,但是因为编码形式的不同,它们表示的意义(数据大小、数据范围)是完全不同的。有符号数除了能表示零和正数之外,还能表示负数。进一步来说,即使是同一个二进制数,分别从非负数的角度和有符号数的角度去解释,也会有不同的意义。
C语言支持多种整型数据类型,如表5-1(32位机)和表5-2(64位机)所示。在C语言变量的声明语句中,可以用关键字来定义数据类型,相应地,也就确定了数据的大小。这些关键字包括char、short、long、long long等。同时,C语言使用关键字unsigned来表示非负数,没有这样特别指示的数默认为有符号数(可能是负数)。根据机器的字长和编译器的不同,这些数据类型被分配不同大小的字节数。例如,long类型在32位机器上用4字节表示,但是在64位机器上就可以用8字节表示,比4字节的表示范围大了很多。需要引起注意的是,表5-1和表5-2中数据表示的取值范围是不对称的,这与有符号数的表示方法有关。在学习有符号数的表示方法时,我们会看到导致这种情况的根本原因。同时Java学习者可能也会发现,虽然C和C++都同时支持有符号数(默认)和无符号数(额外声明),但是Java只支持有符号数。
有符号数与无符号数
扫描上方二维码可观看知识点讲解视频
表5-1 32位机上C语言整型数据类型的典型取值范围
(续)
表5-2 64位机上C语言整型数据类型的典型取值范围
可以将无符号数的编码简单地看作非负十进制数的二进制转换,不过要根据实际机器的字长以及数据的类型来决定二进制编码的位数。假设一个整数数据类型有 w 位(数据类型的字节数×8),我们将位向量写成 x =[ x w -1 , x w -2 ,…, x 0 ], x i ( i =0,…, w -1)表示向量中的每一位。将向量 x 看作一个二进制数就获得了它的无符号表示。通常使用函数B2U w (Binary to Unsigned,长度为 w )来表示。
式(5-1)完成了从一个长度为 w 的0、1串到一个非负十进制整数的映射,例如当 w =4时:
(1110) 2 =1×2 3 +1×2 2 +1×2 1 +0×2 0 =(14) 10
现在考虑 w 位能表示的数值范围。从二进制的角度考虑, w 位二进制数的取值范围是从00…0( w 个0)到11…1( w 个1),则最大值11…1( w 个1)的十进制表达为:
4位二进制数能表示的范围为0000~1111,最小值为0,最大值为2 4 -1=15。因此对于非负数来说, w 位二进制数可以表示2 w 个十进制数,取值范围为0~( 2 w -1)。
无符号数的二进制表示有一个非常重要的属性,即每一个介于0~(2 w -1)之间的数都有唯一一个 w 位的编码。这个属性用数学中的集合术语来说就是,函数B2U w 是一个双射——对于每一个长度为 w 的位向量,都有一个唯一的值与之对应;反过来,在0~(2 w -1)之间的每一个整数都有一个唯一的长度为 w 的二进制位向量与之对应。
仅有非负数是无法完成所有的计算任务的,因此必须采取另外一种编码方式来提供同时包括正数、零和负数(统称有符号数)的二进制表示方法。最常见的有符号数的计算机表示方法就是补码(Two's Complement)形式。在最本质的定义中,补码规定:将二进制数的最高位解释为负权(Negative Weight)。使用函数B2T w (Binary to Two's-Complement的缩写,长度为 w )来表示补码。
对于式(5-3)最直观的理解可以用图5-1来描述。
图5-1 从无符号数到有符号数的取值范围变化
从图5-1可以看出,在 w 固定的情况下,二进制表达数的个数确定都是2 w 个。而最高位为1的,用来表示为负数的数就是其中一半,即2 w -1 个。将这些最高位为1的B2T w ( x )与其原来表示的无符号数B2U w ( x )比较,它们之间相差:
也就是说,将原本无符号数中最高位为1的2 w -1 个用来表示负数,等于将它们原本的取值范围向负轴滑动2 w (减掉2 w ),从而使补码表示范围呈现为高位为1的负数与形式不变的非负数范围的集合。
如图5-1所示,该滑动不改变取值范围原本2 w 的宽度,但是最小值落在了-2 w -1 ,最大值相应地滑动到2 w -1 -1。以 w =4为例,对无符号数,可以表示0~15共16个数;同样以这4位二进制做补码编码,依据式(5-3)计算,此时补码表示的是-8~7这16个数。由此可见,补码的二进制最高位作为负权(-2 w -1 ),起到的作用就是将原来0~(2 w -1)的无符号数表示范围向数轴负方向拖动2 w -1 个单位,变成从-2 w -1 到2 w -1 -1的补码表示范围。
最高有效位 x w -1 在补码中也被称为符号位,权重为-2 w -1 ,是无符号数最高权重的负数。符号位为1时,依据补码定义式(5-3),表示值为负;符号位为0时,表示值为非负。同样,B2T w 也是一个双射。补码取值范围的最小值对应的位向量是[10…0],记为TMin w ≡-2 w -1 ;最大值位向量为[01…1],记为TMax w ≡2 w -1 -1。同样以 w =4为例,我们有
TMin 4 =B2T 4 ([1000])=-2 4-1 =-2 3 =-8
TMax 4 =B2T 4 ([0111])=2 4-1 -1=2 3 -1=7
需要注意的是,补码的表示范围不是对称的。这是因为最高位为0的有符号数中包含0,因此正数只有2 w -1 -1个,但是最高位为1的负数有2 w -1 个。
不同 w 值(字长)的补码的极值如表5-3所示。
表5-3 不同字长补码的极值
这里介绍另外一种对于补码比较有趣的理解。以生活中的钟表表盘为例(如图5-2所示),从上午9点到下午4点,时针前行了7个小时,而时针逆向走5个小时同样指到4。就表盘这个范围为12的区域来说,+7和-5这两个绝对值之和恰好为12的数之间体现了一种互补关系。同样,以 w =4的二进制为例,-6在2 4 =16的范围内与+10的绝对值和为16,构成互补关系。实际上-6的补码表示就是+10的二进制表示1010。这一点可以通过式(5-3)验证。
图5-2 用表盘表示的互补关系
从补码的定义可以看出,补码就是用无符号数中最高位为1的非负数二进制来表达与其构成互补关系的负数,最高位为0的数的表示不变。由此得到四种求负数 x 补码的方法(均以 w =4为例)。
· 2 w - | x |= a ,则 a 的二进制表达即为 x 的 w 位补码。这种方法利用的就是上述所谓互补关系的含义,用 w 位二进制的表示范围2 w 来求取负数对应的互补数作为补码,例如 w =4、 x =-6,则 x 的补码为16-6=10→(1010) 2 。
· 2 w -1 -| x |= a ,则在符号位1之后接上 a 的二进制表达。该方法与第一种方法原理相同,只不过先行确定了符号位为1,然后对剩下的二进制位求互补关系。依然以 w =4、 x =-6为例,2 4-1 -6=8-6=2→(010) 2 ,因此 x 的补码为(1010) 2 。
· 写出 x 的二进制原码,然后符号位不变,其余各位取反加1。例如-6的原码为(1110) 2 ,最高位1为符号位,表示负数,余下3位110表示6。符号位不变,其余各位取反加1得到(1010) 2 。
· 写出 x 的绝对值的二进制,所有位取反后加1。例如-6的绝对值为6,二进制表达为(0110) 2 ,所有位取反加1后得到(1010) 2 。该方法与第三种方法的原理相同,即二进制的各位取反后加1,得到的就是与原数有互补关系的二进制表达。
上述四种方法中,第一种和第二种方法便于理解补码的概念,适用于 w 较小的情况;第三种和第四种方法操作较为便捷,适用于 w 较大的情况,尤其便于使用硬件电路实现。
C语言允许不同数据类型之间进行强制的转换。强制转换通过使用数据类型关键字重新强制定义之前已经声明过的数据类型变量来实现。例如,现有已经声明为int的变量 x 和已经声明为unsigned的变量 u ,表达式(unsigned) x 会将 x 的值转换成一个无符号数值,而(int) u 会将 u 的值转换成一个有符号整数。要注意的是,对于大多数C语言的实现来说,这种转换都是从位级的角度而不是数的角度来看的。
有符号数与无符号数的数值都具有一定的特性,这是由它们本身二进制的构成及补码的定义决定的。首先是等价性,即非负数的补码就是其本身;其次是B2T w 作为双射带来的唯一性,即每一个“位模式”表示唯一的一个整数值,而每一个整数值都有唯一的一个“位模式”与之对应;最后是可逆性,即无论有符号数还是无符号数,它们的位模式与相应的整数值之间均可逆,这其实也是双射性质的一种体现。
有符号数与无符号数之间的映射,本质就是保持位模式不变,而采用目标数的规则来解释。例如(1010) 2 作为表示无符号数10的位模式,映射到有符号数中就需要用补码的定义来解释,得到结果-6。这种映射关系清晰地体现在图5-3中。
图5-3 有符号数与无符号数之间的映射( w =4)
从图5-3中可以看到,非负数在有符号数和无符号数之间的等价性、两种类型数之间的映射方法以及补码表示的负数与其对应的无符号数之间的基于16( w =4)的互补关系。图5-4进一步展示了两者之间的转换关系。
图5-4 有符号数与无符号数之间的转换关系
如果从二进制位模式的角度来审视有符号数和无符号数之间的关系,我们发现这种映射其实就是最高位权重的变化问题。同一个二进制位模式,作为无符号数时,其最高位充当的是最大正权重的角色;作为有符号数时,最高位依据补码定义变成了最大负权重。由此还可以得出一个便捷计算两种数据类型转换的公式:对于同一个位模式来说,解释为有符号数时,依据补码定义最高位为负权重(-2 w -1 );而解释为无符号数时,最高位为正权重(2 w -1 )。其余各位的解释相同。那么在最高位为1的情况下,此二进制位模式对应的有符号数与无符号数之间的差就是两倍的2 w -1 ,即2 w (这正是我们之前在直观描述补码定义时获得的结果);最高位为0则两数相等(非负数补码为其本身)。所以有:
在C语言中,常数被默认为有符号数,除非使用后缀U来表明该常数为无符号数。两者之间的转换除了上述的重新强制声明之外,还可以通过赋值和调用语句来实现变量数据类型的改变,例如对于已经定义的int x和unsigned u,语句u=x把x的值作为无符号数赋值给u。同时在输出语句中也可以通过定义输出结果的类型将变量强制转换成指定的类型输出。如果一个表达式中既有有符号数又有无符号数,有符号数将会被转换成无符号数再代入表达式中进行计算。看如下的C语言代码:
在32位机器上运行上述代码时,它的输出结果是:
这是因为-1的补码表示为[11…1] 2 ,但是在第一条输出语句中做了一个输出类型的强制转换,将其作为32位无符号数解释,此时[11…1] 2 应该解释为整型无符号数的最大值2 32 -1,所以打印出来的结果为2 32 -1=4 294 967 296-1=4 294 967 295。
u的值为2 31 ,其无符号数表示为[100…0] 2 ,在第二条输出语句中对其做了输出类型的强制转换,将其解释为32位带符号整数,此时[100…0] 2 代表补码中的最小负数,因此打印结果会显示为-2 32-1 =-2 31 =-2 147 483 648。
在计算任务中,有时需要将数据在不同字长的整数之间转换,同时保持数值不变。这就需要对数据进行数值不变的扩展与截断。
整数的扩展与截断
扫描上方二维码可观看知识点讲解视频
1.数据扩展
一般来说,从一个无符号数转换为一个较大的数据类型,只需要简单地在数据的开头添加0直到所需的字长即可,这种运算称为零扩展(Zero Extension)。将一个补码数字转换为一个更大的数据类型则需要执行符号扩展(Sign Extension),规则是在数据的开头添加符号位的副本直到所需的字长。例如,一个补码数据[ x w -1 , x w -2 ,…, x 0 ]做符号扩展的结果就是[ x w -1 , x w -1 ,…, x w -1, x w -2 ,…, x 0 ]。更具体地来说,如果给定一个 w 位的数 x ,需要将它扩展到( w + k )位而保持数据不变,规则就是将 x 的符号位复制 k 个放在 x 的前面,形成所需的( w + k )位数据(如图5-5所示)。
图5-5 数据的扩展
零扩展能够保持数值不变是很好理解的,毕竟任何数前面加上无穷多个0并不改变其数值。但是为什么当有符号数的二进制表达从 w 位扩展到( w + k )位时,只需要进行符号扩展(在该数前面加上 k 个该数的符号位)就能实现扩展后数值不变呢?首先,对于有符号数中的非负数来说,由于它们的符号位为0,因此所谓符号扩展等同于零扩展。而对于有符号数中的负数而言,需要讨论的就是为什么其 w 位补码在经过符号扩展(添加 k 个1)之后,可以得到其( w + k )位补码。
我们可以依然用“表盘”的概念来解释有符号数补码的符号扩展。 w 位二进制补码扩展成( w + k )位,补码的“表盘”范围就变成了2 w + k ,此时对负数 x 求补码变为:
而之前的 w 位补码为:
二者相差:
2 k -1从二进制来看就是 k 个1;再乘上2 w 就是左移 w 位,所以 w 位补码与( w + k )位补码之间相差:
也就是说,原来的 w 位补码加上式(5-8)所示的值之后就得到( w + k )位补码。从式(5-8)的特征来看,这个加法过程(原来的 w 位补码填充入 w 个0中,前面 k 个1不变)就是一个符号扩展过程。因此,符号扩展能保证扩展后的新补码与扩展后的原补码表示同一个负数。
仍然以前述的 w =4时,-6的补码(1010) 2 为例。当 w =8时,需要对其进行从4位到8位的扩展。我们先来求 w =8时-6的补码,依据5.1.3节中的第一种方法很容易求得结果为(11111010) 2 。简单对比 w =4时的补码,我们发现这只是做了符号位扩展而已。由此证明简单的符号位扩展可以在扩展有符号数位数的同时保证数值不变。
2.数据截断
对于数据的截断来说,我们需要减少表示数值的二进制的位数。这会导致结果与被截断之前不同,需要重新解释。在C语言中,与有符号数和无符号数的强制转换类似,可以用数据声明的关键字对不同类型的数据进行强制类型转换。例如,对于已经声明为int x的数据,语句(short)x就能将整型的x截断为短整型的数据赋值给相应的变量。下面来看一段示例代码:
运行的结果如下:
这一段代码将原本整型的数据x截断为短整型sx,然后又重新扩展回整型y,我们发现数值发生了变化(x ≠ y)。首先,在第二条语句short sx=(short)x中,整型x(位模式为0x0000cfc7)从32位的整型截断为16位的短整型,得到的位模式变成0xcfc7(这也是-12345的16位补码形式)。而sx作为短整型数据默认是有符号数,因此在输出sx时,是将0xcfc7作为有符号数来解释的,输出为-12345。之后的第三条语句int y=sx又将16位的短整型数据扩展到了整型。依据有符号数的符号扩展规则,复制了16个符号位1添加到sx前方,形成位模式为0xffffcfc7的二进制表达形式,这同时也是-12345的32位补码形式。作为int类型的y,默认为有符号数,因此输出时结果也为-12345。
一般来说,将一个 w 位的数 x =[ x w -1 , x w -2 ,…, x 0 ,]截断为一个 k 位数字时,最高的 w-k 位会被丢弃,生成的新位向量为 x ’=[ x k -1 , x k -2 ,…, x 0 ,]。对于一个无符号数字 x ,截断它到 k 位的操作就相当于计算 x mod 2 k ,因此通过对B2U w 的计算公式进行取模运算可以实现这一过程:
对于补码数据,截断运算的推导与上面类似,结果为
从这一推导来看,似乎 x mod 2 k 可以用一个([ x k -1 , x k -2 ,…, x 0 ])模式的无符号数表示。但其实对于被截断的有符号数,我们依然认为它是有符号数,其数值应该是U2T k ( x mod 2 k )。因此有符号数的截断结果最终为U2T k (B2U w ([ x w -1 , x w -2 ,…, x 0 ])mod 2 k )。图5-6给出了一些数据截断的示例(4位截断成3位)。
图5-6 数据的截断示例