负值的二进制补码形式因位数不同而不同。8位的有符号数必须经过转换才能在涉及16位数的表达式中使用。这种转换叫作符号扩展(SignExtension)操作,其逆过程(16位数转换为8位数)称为收缩(Contraction)操作。
以-64为例,其8位二进制补码值为$C0。而相等的16位补码值是$FFC0。这显然是两种不同的位模式。而+64的8位版本和16位版本分别是$40和$0040。负值的扩展方式和非负值不同。
一个值的符号扩展就是将符号位复制到新格式的其他高位中。例如,将8位数扩展为16位数,就是将8位数中的第7位复制到16位数的第8~15位。要将16位数扩展成双字,就是将16位数的第15位复制到双字的第16~31位。
当把一个字节加到一个字上时,需要在求和之前将字节符号扩展到16位。其他运算可能需要将符号扩展到32位。
表2-5展示了一些符号扩展的示例。
表2-5 符号扩展示例
零扩展 (ZeroExtension)是将位数较少的无符号数转换为位数较多的无符号数。零扩展非常容易,只需用0填充位数较多的操作数的高位字节即可。例如,将8位数$82零扩展到16位,就是用0填充高位字节,得到$0082。
表2-6中列出了一些示例。
表2-6 零扩展示例
续表
许多高级语言编译器可以自动处理符号扩展和零扩展。下面的示例展示了C语言中扩展的工作原理:
一些编程语言(例如Ada或Swift)要求显式说明从较少位数到较多位数的转换。查看语言参考手册,了解是否需要显式转换。要求显式转换的语言有一个优点,那就是编译器做的事情都是可见的。如果显式转换出错了,编译器会发出诊断消息。
需要注意,符号扩展和零扩展并不总是没有成本。将位数较少的整数赋值给位数较多的整数,需要的机器指令要比在两个位数相等的整数变量之间移动数据更多(执行时间也更长)。因此,在同一条算术表达式或赋值语句中混用不同位数的变量时要特别小心。
符号收缩(将数值转换为位数更少的相等值)更麻烦一些。以-448为例,其16位十六进制数的表示形式为$FE40。这个数8位空间无法容纳,因此不能将其符号收缩为8位。
要将一个值正确地符号收缩为另一个值,必须检查要丢弃的高位字节。首先,高位字节必须全部是0或$FF。其次,结果的高位的每一位必须与丢弃的每一位都相同。下面是一些将16位值转换为8位值的例子(有成功的,也有失败的):
●$FF80(%1111_1111_1000_0000)可以符号收缩为$80(%1000_0000)。
●$0040(%0000_0000_0100_0000)可以符号收缩为$40(%0100_0000)。
●$FE40(%1111_1110_0100_0000)不能符号收缩为8位。
●$0100(%0000_0001_0000_0000)不能符号收缩为8位。
有些高级语言直接将值的低位部分存储到位数较少的变量中,丢弃剩余的高位部分,C语言就是这样做的。C语言编译器最多也就是给出潜在的精度损失警告。可以不让C语言编译器报警,但是编译器不会检查无效值。在C语言中进行符号收缩通常会使用下面这些代码:
在把值存储到位数更少的变量之前,先将该表示形式的值与其上下限比较,这是C语言唯一的安全转换方法。前面的代码加上检查之后如下:
这段代码非常难看。在C/C++中可以把这段代码定义成宏(#define)或函数,以提高可读性。
有些高级语言(例如Free Pascal和Delphi)会自动对数值进行符号收缩,并且确保结果不会超出目标操作数的范围。 如果超出范围,会抛出异常(或停止程序)。修正这些错误需要编写异常处理代码或者使用类似前面给出的if语句。