从 |
到 |
方法 |
unsigned char |
char |
最高位作为符号位 |
unsigned char |
short |
0扩展 |
unsigned char |
long |
0扩展 |
unsigned char |
unsigned short |
0扩展 |
unsigned char |
unsigned long |
0扩展 |
unsigned char |
float |
转换到long; 再从 long 转换到float |
unsigned char |
double |
转换到long; 再从 long 转换到double |
unsigned char |
long double |
转换到long; 再从 long 转换到double |
unsigned short |
char |
保留低位字节 |
unsigned short |
short |
最高位作为符号位 |
unsigned short |
long |
0扩展 |
unsigned short |
unsigned char |
保留低位字节 |
unsigned short |
unsigned long |
0扩展 |
unsigned short |
float |
转换到long; 再从 long 转换到float |
unsigned short |
double |
转换到long; 再从 long 转换到double |
unsigned short |
long double |
转换到long; 再从 long 转换到double |
unsigned long |
char |
保留低位字节 |
unsigned long |
short |
保留低位字节 |
unsigned long |
long |
最高位作为符号位 |
unsigned long |
unsigned char |
保留低位字节 |
unsigned long |
unsigned short |
保留低位字节 |
unsigned long |
float |
转换到long; 再从 long 转换到float |
unsigned long |
double |
Convert directly to double |
unsigned long |
long double |
转换到long; 再从 long 转换到double |
---------------------------------------------------------
符号扩展,零扩展,以及缩减
数现代高级程序设计语言允许程序员使用包含不同大小的整数对象的表达式。那么,当一个表达式的两个操作数大小不同的时候,会发生什么呢?有些语言会报错,而其他的语言则会自动将操作数转换成一个统一的格式。这种转换是有代价的,因此,如果你不希望编译器在你不知情的情况下自动加入各种转换到你原本非常完美的代码中,就需要掌握编译器是如何处理这些表达式的。
进制补码系统中,同一个负数在不同大小的表示法中的表示是不同的。你不能在一个包含16位数的表达式中随意地使用8位有符号数,转换是必需的。这种转换,以及其逆操作(将16位数转换为8位)就是符号扩展(sign extension)与缩减(contraction)操作。
-64为例,其8位的二进制补码表示是$C0,而等效的16位二进制补码表示则是$FFC0。很显然,其位模式不一样。再看看数+64,其8位和16位表示分别是$40与$0040。一个很显然的事实就是,扩展负数的大小与扩展非负数的大小是完全不同的。
个数从某个位数符号扩展到一个更大的位数很简单,只需要将符号位复制到新格式新增的高端各位即可,例如,为了将一个8位的数符号扩展到16位,只需将8位数的第7位复制到16位数的第8 .. 15位即可。而将一个16位数符号扩展到一个双字,只需要将第15位复制到双字的第16 .. 31位即可。
理不同长度有符号数的时候,必须使用符号扩展。例如,在将一个字节量与一个字量相加的时候,在相加之前必须将字节量符号扩展到16位。其他运算可能又会需要符号扩展到32位。
表2-5 符号扩展举例
8位
|
16位
|
32位
|
二进制补码表示
|
$80
|
$FF80
|
$FFFF_FF80
|
11_1111_1111_1111_1111_1111_1000_0000
|
$28
|
$0028
|
$0000_0028
|
00_0000_0000_0000_0000_0000_0010_1000
|
$9A
|
$FF9A
|
$FFFF_FF9A
|
11_1111_1111_1111_1111_1111_1001_1010
|
$7F
|
$007F
|
$0000_007F
|
00_0000_0000_0000_0000_0000_0111_1111
|
n/a
|
$1020
|
$0000_1020
|
00_0000_0000_0000_0001_0000_0010_0000
|
n/a
|
$8086
|
$FFFF_8086
|
11_1111_1111_1111_1000_0000_1000_0110
|
处理无符号二进制数的时候,可以使用零扩展(zero extension)来将小位数的无符号数扩展到大位数的无符号数。零扩展非常简单——只需要用零来填充大位数操作数的高端各个字节即可。例如,为了将8位数$82零扩展到16位,只需要在高端字节中插入零,即得到$0082。
表2-6 零扩展举例
8位
|
16位
|
32位
|
二进制补码表示
|
$80
|
$0080
|
$0000_0080
|
00_0000_0000_0000_0000_0000_1000_0000
|
$28
|
$0028
|
$0000_0028
|
00_0000_0000_0000_0000_0000_0010_1000
|
$9A
|
$009A
|
$0000_009A
|
00_0000_0000_0000_0000_0000_1001_1010
|
$7F
|
$007F
|
$0000_007F
|
00_0000_0000_0000_0000_0000_0111_1111
|
n/a
|
$1020
|
$0000_1020
|
00_0000_0000_0000_0001_0000_0010_0000
|
n/a
|
$8086
|
$0000_8086
|
00_0000_0000_0000_1000_0000_1000_0110
|
大多数高级语言编译器会自动处理符号扩展与零扩展,以下C语言的例子说明了它们是如何工作的:
signed char sbyte; // C语言中的字符类型是一个字节
short int sword; // C语言中的短整型一般是16位
long int sdword; // C语言中的长整型一般是32位
. . .
sword = sbyte; //自动将8位值符号扩展到16位
sdword = sbyte; //自动将8位值符号扩展到32位
sdword = sword; //自动将16位值符号扩展到32位
语言(例如Ada)在从小数据类型转换到大数据类型时需要显式转换(explicit cast)。查一下所用语言的参考手册就知道这种显式转换是不是必需的了。要求提供显式转换的语言的优点在于编译器永远不会在程序员不知情的情况下做任何事情。如果你没有提供必要的转换,编译器会给出一个诊断消息,让你知道程序还需要改进。
符号扩展和零扩展,有一点需要明确的是,它们是需要付出代价的。将一个小整型赋值给一个大整型可能会比在同样大小的整型变量间传输数据需要更多的机器指令(执行时间更长)。因此,在一个数学表达式或者一条赋值语句中混合使用不同大小的变量要小心。
符号缩减,即将一个某位数转换为值相同但位数变小的数,比较麻烦。符号扩展永远不会失败,使用符号扩展,一个m位有符号数永远可以转换为一个n位数(这里n>m)。不幸的是,在m<n的情况下,一个n位数不是总能转换为m位数。例如,-448的16位十六进制表示是$FE40,而这个数的大小对于8位来说太大了,我们无法将其符号缩减到8位。
将一个数值正确地符号缩减,必须要检查需要丢弃的高端字节。首先,这些高端字节必须是全零或者$FF,如果它们包含其他值,我们就无法对这个数进行符号缩减。其次,最终结果的最高位必须与被丢弃的所有位一致。以下就是一些从16位数转换到8位数的例子:
$FF80 (11_1111_1000_0000) 可以被符号缩减为 $80 (00_0000).
$0040 (00_0000_0100_0000) 可以被符号缩减为 $40 (00_0000).
$FE40 (11_1110_0100_0000) 不能被符号缩减为8 位
$0100 (00_0001_0000_0000) 不能被符号缩减为8 位
级语言里使用缩减有点困难,有些语言,譬如说C语言,会直接将表达式的低端部分存储到比较小的变量中,并将高端部分丢弃(在最好的情况下,C编译器可能会在编译过程中给出一个警告,提示可能会出现的精度损失)。你可以采取措施来让编译器停止抱怨,但是它仍然不会检查数值的有效性。以下是C语言中符号缩减的典型代码:
signed char sbyte; // C语言中的字符类型是一个字节
short int sword; // C语言中的短整型一般是16位
long int sdword; // C语言中的长整型一般是32位
. . .
sbyte = (signed char) sword;
sbyte = (signed char) sdword;
sword = (short int) sdword;
语言中,唯一安全的解决方案就是在将表达式的结果值存储到一个小变量中之前,将该结果值与某个上下边界值进行比较。不幸的是,如果需要经常做这种操作,代码会变得比较笨拙。以下就是加上这些检查之后的转换代码:
if( sword >= 128 && sword <= 127 )
{
sbyte = (signed char) sword;
}
else
{
// 报告错误
}
// 另一种方案,使用断言:
assert( sword >= 128 && sword <= 127 )
sbyte = (signed char) sword;
assert( sdword >= 32768 && sdword <= 32767 )
sword = (short int) sdword;
易见,这让代码变得丑陋。在C/C++中,你可能会倾向于将它们编写为宏(#define)或者函数,以提高代码的可读性。
有些高级语言(例如Pascal和Delphi/Kylix)会自动进行符号缩减,还会检查结果来确保它适用于目标操作4。这些语言在越界违例发生的时候会产生某种类型的异常(或者停止程序的运行)。当然了,如果你想加入纠错代码,要么就需要写点异常处理代码,要么就使用前面C语言例子中使用的if语句序列。
|