Char转为int时高位符号扩展的问题

来源:互联网 发布:海螺水泥怎么样 知乎 编辑:程序博客网 时间:2024/05/06 00:49
代码示例: 
static get_utili(constchar*p){int util;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型到unsigned char型的转换,以避免按照char的最高位进行符号扩展。
上述出错代码的符号扩展过程如下:
因为要扩展的短数据类型为有符号数的-- char x=10011100b(即0x9A)
因而在int y=(int)x时--进行符号扩展,即短数据类型的符号位填充到长数据类型的高字节位(比短数据类型多出的那一部分),则y的值为11111111 10011100b(变成了十进制的-102);
但是,将要扩展的短数据类型变成无符号数后--unsigned char x=10011100b(即0x9A)
在 int y=(int)x时--进行扩展的时候是以零扩展,即用零来填充长数据类型的高字节位,则y的值应为00000000 10011100b(十进制的154)。

正确代码: 
util=(int)*p++;改成util=(int)(unsigned char)*p++
Bug定位: 

该bug是在code review的过程中发现的。
char符号扩展的问题,如果在测试时没有构造相应的case,就会很难被发现。面对这类问题,细致的code review是必不可少的,不管是通过code review直接发现问题还是通过review来丰富相应case的构造,code review都应该是一个不可缺少的环节。


关于符号扩展

一、短数据类型扩展为长数据类型

1、要扩展的短数据类型为有符号数的

      进行符号扩展,即短数据类型的符号位填充到长数据类型的高字节位(即比短数据类型多出的那一部分),保证扩展后的数值大小不变

如1:char x=10001001b;   short y=x;  则y的值应为11111111 10001001b;

    2:char x=00001001b;   short y=x;  则y的值应为00000000 00001001b;

2、要扩展的短数据类型为无符号数的

     进行零扩展,即用零来填充长数据类型的高字节位

如1:unsigned char x=10001001b;   short y=x;  则y的值应为00000000 10001001b;

    2:unsigned char x=00001001b;   short y=x;  则y的值应为00000000 00001001b;

二、长数据类型缩减为短数据类型

    如果长数据类型的高字节全为1或全为0,则会直接截取低字节赋给短数据类型;如果长数据类型的高字节不全为1或不全为0,则转会就会发生错误。

三、同一长度的数据类型中有符号数与无符号数的相互转化

     直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另短类型扩展为长类型时,但短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。

附:有符号数的转换

 从到方法charshort符号位扩展charlong符号位扩展charunsigned char最高位失去符号位意义,变为数据位charunsigned short符号位扩展到short;然后从short转到 unsigned shortcharunsigned long符号位扩展到long; 然后从long 转到unsigned longcharfloat符号位扩展到long; 然后从long 转到floatchardouble符号位扩展到long; 然后从long 转到doublecharlong double符号位扩展到long; 然后从long 转到long doubleshortchar保留低位字节shortlong符号位扩展shortunsigned char保留低位字节shortunsigned short最高位失去符号位意义,变为数据位shortunsigned long符号位扩展到long; 然后从long转到unsigned doubleshortfloat符号位扩展到long; 然后从long 转到floatshortdouble符号位扩展到long; 然后从long 转到doubleshortlong double符号位扩展到long; 然后从long 转到doublelongchar保留低位字节longshort保留低位字节longunsigned char保留低位字节longunsigned short保留低位字节longunsigned long最高位失去符号位意义,变为数据位longFloat使用单精度浮点数表示。可能丢失精度。longdouble使用双精度浮点数表示。可能丢失精度。longlong double使用双精度浮点数表示。可能丢失精度。图片点击可在新窗口打开查看

无符号数的转换

 方法unsigned charchar最高位作为符号位unsigned charshort0扩展unsigned charlong0扩展unsigned charunsigned short0扩展unsigned charunsigned long0扩展unsigned charfloat转换到long; 再从 long 转换到floatunsigned chardouble转换到long; 再从 long 转换到doubleunsigned charlong double转换到long; 再从 long 转换到doubleunsigned shortchar保留低位字节unsigned shortshort最高位作为符号位unsigned shortlong0扩展unsigned shortunsigned char保留低位字节unsigned shortunsigned long0扩展unsigned shortfloat转换到long; 再从 long 转换到floatunsigned shortdouble转换到long; 再从 long 转换到doubleunsigned shortlong double转换到long; 再从 long 转换到doubleunsigned longchar保留低位字节unsigned longshort保留低位字节unsigned longlong最高位作为符号位unsigned longunsigned char保留低位字节unsigned longunsigned short保留低位字节unsigned longfloat转换到long; 再从 long 转换到floatunsigned longdoubleConvert directly to doubleunsigned longlong 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

%1111_1111_1111_1111_1111_1111_1000_0000

$28

$0028

$0000_0028

%0000_0000_0000_0000_0000_0000_0010_1000

$9A

$FF9A

$FFFF_FF9A

%1111_1111_1111_1111_1111_1111_1001_1010

$7F

$007F

$0000_007F

%0000_0000_0000_0000_0000_0000_0111_1111

n/a

$1020

$0000_1020

%0000_0000_0000_0000_0001_0000_0010_0000

n/a

$8086

$FFFF_8086

%1111_1111_1111_1111_1000_0000_1000_0110

       处理无符号二进制数的时候,可以使用零扩展(zero extension)来将小位数的无符号数扩展到大位数的无符号数。零扩展非常简单——只需要用零来填充大位数操作数的高端各个字节即可。例如,为了将8位数$82零扩展到16位,只需要在高端字节中插入零,即得到$0082。

表2-6  零扩展举例

8位

16位

32位

二进制补码表示

$80

$0080

$0000_0080

%0000_0000_0000_0000_0000_0000_1000_0000

$28

$0028

$0000_0028

%0000_0000_0000_0000_0000_0000_0010_1000

$9A

$009A

$0000_009A

%0000_0000_0000_0000_0000_0000_1001_1010

$7F

$007F

$0000_007F

%0000_0000_0000_0000_0000_0000_0111_1111

n/a

$1020

$0000_1020

%0000_0000_0000_0000_0001_0000_0010_0000

n/a

$8086

$0000_8086

%0000_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 (%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语言中符号缩减的典型代码:

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语句序列。

引用:
http://testing.etao.com/experience_list/66
http://apps.hi.baidu.com/share/detail/40431986

http://www.51testing.com/?uid-377613-action-viewspace-itemid-806772

原创粉丝点击