代码示例:
static get_utili(constchar*p){intutil;…while(isspace((int)*p))//跳过空格++p;util=(int)*p++;…}
现象&后果:
当传入的参数p指向的内容为0x9A、0XAB等内容(最高位为1)时,得到的int型变量util的值将会出错,因为char会进行符号扩展,使得0x9A(十进制的154)变成了-102。会造成程序运行时的数据处理错误。
Bug分析:
char符号扩展是与编译器相关的,但在x86平台上,对于任何主流的编译平台,char总是进行符号扩展的。上述代码在将char型的*p赋给int型变量util的时候,需要先进行char型到unsignedchar型的转换,以避免按照char的最高位进行符号扩展。
上述出错代码的符号扩展过程如下:
因为要扩展的短数据类型为有符号数的-- char x=10011100b(即0x9A)
因而在inty=(int)x时--进行符号扩展,即短数据类型的符号位填充到长数据类型的高字节位(比短数据类型多出的那一部分),则y的值为1111111110011100b(变成了十进制的-102);
但是,将要扩展的短数据类型变成无符号数后--unsigned char x=10011100b(即0x9A)
在 int y=(int)x时--进行扩展的时候是以零扩展,即用零来填充长数据类型的高字节位,则y的值应为0000000010011100b(十进制的154)。
正确代码:
util=(int)*p++;改成util=(int)(unsigned char)*p++
Bug定位:
该bug是在code review的过程中发现的。
char符号扩展的问题,如果在测试时没有构造相应的case,就会很难被发现。面对这类问题,细致的codereview是必不可少的,不管是通过code review直接发现问题还是通过review来丰富相应case的构造,codereview都应该是一个不可缺少的环节。
关于符号扩展
一、短数据类型扩展为长数据类型
1、要扩展的短数据类型为有符号数的
如1:charx=10001001b;
2、要扩展的短数据类型为无符号数的
如1:unsigned charx=10001001b;
二、长数据类型缩减为短数据类型
三、同一长度的数据类型中有符号数与无符号数的相互转化
附:有符号数的转换
从 | 到 | 方法 |
char | short | 符号位扩展 |
char | long | 符号位扩展 |
char | unsignedchar | 最高位失去符号位意义,变为数据位 |
char | unsignedshort | 符号位扩展到short;然后从short转到 unsigned short |
char | unsignedlong | 符号位扩展到long; 然后从long 转到unsigned long |
char | float | 符号位扩展到long; 然后从long 转到float |
char | double | 符号位扩展到long; 然后从long 转到double |
char | longdouble | 符号位扩展到long; 然后从long 转到longdouble |
short | char | 保留低位字节 |
short | long | 符号位扩展 |
short | unsignedchar | 保留低位字节 |
short | unsignedshort | 最高位失去符号位意义,变为数据位 |
short | unsignedlong | 符号位扩展到long; 然后从long转到unsigned double |
short | float | 符号位扩展到long; 然后从long 转到float |
short | double | 符号位扩展到long; 然后从long 转到double |
short | longdouble | 符号位扩展到long; 然后从long 转到double |
long | char | 保留低位字节 |
long | short | 保留低位字节 |
long | unsignedchar | 保留低位字节 |
long | unsignedshort | 保留低位字节 |
long | unsignedlong | 最高位失去符号位意义,变为数据位 |
long | Float | 使用单精度浮点数表示。可能丢失精度。 |
long | double | 使用双精度浮点数表示。可能丢失精度。 |
long | longdouble | 使用双精度浮点数表示。可能丢失精度。 |
无符号数的转换
从 | 到 | 方法 |
unsignedchar | char | 最高位作为符号位 |
unsignedchar | short | 0扩展 |
unsignedchar | long | 0扩展 |
unsignedchar | unsignedshort | 0扩展 |
unsignedchar | unsignedlong | 0扩展 |
unsignedchar | float | 转换到long;再从 long 转换到float |
unsignedchar | double | 转换到long;再从 long 转换到double |
unsignedchar | longdouble | 转换到long;再从 long 转换到double |
unsignedshort | char | 保留低位字节 |
unsignedshort | short | 最高位作为符号位 |
unsignedshort | long | 0扩展 |
unsignedshort | unsignedchar | 保留低位字节 |
unsignedshort | unsignedlong | 0扩展 |
unsignedshort | float | 转换到long;再从 long 转换到float |
unsignedshort | double | 转换到long;再从 long 转换到double |
unsignedshort | longdouble | 转换到long;再从 long 转换到double |
unsignedlong | char | 保留低位字节 |
unsignedlong | short | 保留低位字节 |
unsignedlong | long | 最高位作为符号位 |
unsignedlong | unsignedchar | 保留低位字节 |
unsignedlong | unsignedshort | 保留低位字节 |
unsignedlong | float | 转换到long;再从 long 转换到float |
unsignedlong | double | Convertdirectly to double |
unsignedlong | longdouble | 转换到long;再从 long 转换到double |
---------------------------------------------------------
表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 |
表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;
short intsword;
long intsdword;
. . .
sword = sbyte;
sdword =sbyte;
sdword =sword;
符号缩减,即将一个某位数转换为值相同但位数变小的数,比较麻烦。符号扩展永远不会失败,使用符号扩展,一个m位有符号数永远可以转换为一个n位数(这里n>m)。不幸的是,在m的情况下,一个n位数不是总能转换为m位数。例如,-448的16位十六进制表示是$FE40,而这个数的大小对于8位来说太大了,我们无法将其符号缩减到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 位
signed char sbyte;
short intsword;
long intsdword;
. . .
sbyte = (signed char) sword;
sbyte = (signed char) sdword;
sword = (short int) sdword;
if( sword >= 128 && sword <= 127 )
{
}
else
{
}
// 另一种方案,使用断言:
assert( sword >= 128 && sword <= 127 )
sbyte = (signed char) sword;
assert( sdword >= 32768 && sdword <= 32767 )
sword = (short int) sdword;
有些高级语言(例如Pascal和Delphi/Kylix)会自动进行符号缩减,还会检查结果来确保它适用于目标操作4。这些语言在越界违例发生的时候会产生某种类型的异常(或者停止程序的运行)。当然了,如果你想加入纠错代码,要么就需要写点异常处理代码,要么就使用前面C语言例子中使用的if语句序列。