[置顶] 都是类型惹的祸——小心unsigned

正如我们所知道的,编程语句都有很多的基本数据类型,如char,inf,float等等,而在C和C++中还有一个特殊的类型就是无符号数,它由unsigned修饰,如unsigned int等。大家有没想过,就是因为这些不同的类型,而使大家编写的看似非常正确的程序出现了预想不到的错误呢?

一、迷惑人的有符号下无符号数的比较操作
废话不多说,马上来看一下例子,让你先来体验一下这个奇妙的旅程,源代码文件名为unsigned.c,源代码如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. int main()  
  5. {  
  6.     int a = -1;  
  7.     unsigned int b = 1;  
  8.   
  9.     if(a > b)  
  10.         printf("a > b, a = %d, b = %u\n", a, b);  
  11.     else  
  12.         printf("a <= b, a = %d, b = %u\n", a, b);  
  13.     exit(0);  
  14. }  
输出结果为:



看到输出结果之后,你可能会大吃一惊,-1竟然大于1,你没有看错,从输出结果上来看的确是这样。为什么会产生这样的结果呢?这还得从C语言对同时包含有符号数和无符号数表达式的处理方式讲起。

二、有符号数与无符号运算时数强制类型转换方式及底层表示

当执行一个运算时(如这里的a>b),如果它的一个运算数是有符号的而另一个数是无符号的,那么C语言会隐式地将有符号 参数强制类型为无符号数,并假设这两个数都是非负的,来执行这个运算。这种方法对于标准的算术运算来说并无多大差异,但是对于像<和>这样的运算就可能产生非直观的结果。

所以对应回上面的例子,就是它先把-1(变量a的值)这个有符号数强制转换成无符号数,然后再与1(变量b)的值,来进行比较,并假设这两个数原本都是非负的,然后进行比较。那么-1转换为无符号数后,其值为多少呢?你可以写一个小小的程序来验证一下,在32和64位的机子上,-1对应的无符号数应该是4 294 967 295,即32位的无符号数的最大值(UMax),所以if中的条件总是为真。

要想这段代码正常执行,我们需要怎么办呢?很简单,把if语句改为if(a > (int)b)即可。这样程序就会认为是两个有符号数在进行比较,-1就不会隐式地转换为无符号数而变成UMax。

可能你已经有一个问题,为什么使用强制类型,把变量b的类型变成int程序就能正常,而-1转换成无符号数为什么会是4 294 967 295呢?这就得从整型数据在计算机中的表示和C语言对待强制类型转换的方式说起。

我们知道,整数在计算机中通常是以补码的形式存在的,而-1的补码(用4个字节储存)为1111,1111,1111,1111。而C语言对于强制类型转换是怎么处理的呢?对大多数C语言的实现,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会改变,但是位模式不变。也就是说,将unsigned int强制类型转换成int,或将int转换成unsigned int底层的位表示保持不变。

也就是说,即使是-1转换成unsigned int之后,它在内存中的表示还是没有改变,即1111,1111,1111,1111。我们知道在计算机的底层,数据是没有类型可言的,所有的数据非0即1。数据类型只有在高层的应用程序才有意义,也就是说,同样的储存表示对于应用程序而言可能对应着不同的数据,例如1111,1111,1111,1111对于有符号数而言它表示-1,但对于无符号数而言,它表示UMax,但是它们的底层存储都是一样的。现在你应该明白为什么-1转换成无符号数之后,就成了UMax了吧。

三、查看数据的底层表示

为了证明上面所说的内容,请再看下面的代码,里面有个函数show_byte,它可以把从指针start开始的len个字节的值以16进制数的形式打印出来。源文件为showbyte.c,代码如下:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3.   
  4. void show_bytes(unsigned char *start, int len)  
  5. {  
  6.     int i = 0;  
  7.     for(; i < len; ++i)  
  8.         printf(" %.2x", start[i]);  
  9.     printf("\n");  
  10. }  
  11.   
  12. int main()  
  13. {  
  14.     int a = -1;  
  15.     unsigned int b = 4294967295;  
  16.   
  17.     printf("a = %d, a = %u\n", a, a);  
  18.     printf("b = %d, b = %u\n", b, b);  
  19.   
  20.     show_bytes((unsigned char*)&a, sizeof(int));  
  21.     show_bytes((unsigned char*)&b, sizeof(unsigned int));  
  22.     exit(0);  
  23. }  
输出为:



分析:printf函数中,%u表示以无符号数十进制的形式输出,%d表示以有符号十进制的形式输出。通过show_bytes函数,我们可以看到,-1与4 294 967 295的底层表示是一样的,它们的位全部都是全1,即每个字节表示为ff。

四、由于无符号数减法引起的错误

你可能会说,你不会用一个无符号数与一个有符号数作比较,所以你觉得你可以放心了,但是来看看下面的两段代码。
代码1是一个求数组中前length个数据的和的函数,数组中元素的个数由参数length给出,代码如下:
  1. float sum_elements(float a[], unsigned length)  
  2. {  
  3.     int i = 0;  
  4.     float sum = 0;  
  5.     for(i = 0; i <= length -1; ++i)  //有bug
  6.         sum += a[i];  
  7.     return sum;  
  8. }  
如果我告诉你这是一段有错的代码,可能你也不太相信,因为这个函数的一切看起来是这么的自然,因为数据的长度(或个数)肯定是一个非负数,所以把length声明为一个unsigned很合理,计算的数据个数和返回类型也正确。的确如此,但是这都是在length不为0的情况,试想,当调用函数时,把0作为参数传递给length会发生什么事情?回想一下前面我们所说的知识,因为length是unsigned类型,所以所有的运算都被隐式地被强制转换为unsigned类型,所以length-1(即0-1 = -1),-1对应的无符号类型的值为UMax,所以for循环将会循环UMax次,数组也会越界,发生错误。那么如何优化上面的代码呢?其实答案非常简单,你也可以自己想一想,这里就给出答案吧,就是把for循环改为:
  1. for(i = 0; i < length; ++i)  
因为去除了length-1,所以当length为0时也能正常比较。

接下来是代码2,它是一个判断第一个字符串是否长于第二个字符串,若是,返回1,若否返回0,代码如下:
  1. int strlonger(char *s1, char *s2)  
  2. {  
  3.     return strlen(s1) - strlen(s2) > 0;  //有bug
  4. }  
如果我又跟你说这段代码是有bug,你现在找不找得出来呢,还是认为这段代码是没有任何问题的呢?说真的就这么看这个函数好像的确是没有什么问题,但是如果你知道了strlen函数的原型,可能你就会有点明白了,在Linux下可用man 3 strlen命令查看,strlen函数的原型为:
  1. size_t strlen(const char *s);  
注意这里有一个数据类型size_t,它被定义在stdio.h文件中,其实它就是unsigned int,一个字符串的长度当然不可能为负,这样的定义显然是合理的,但是有时却因为这样,而存在不少的问题,如函数strlonger的实现。当s1的长度大于等于s2时,这个函数并没有什么问题,但是你可以想像,当s1的长度小于s2的长度时,这个函数会返回什么吗?没错,因为此时strlen(s1) - strlen(s2)为负(从数学的角度来解释的话),而又由于程序把它作为unsigned为处理,则此时的值肯定是一个比0大的值。换句话来说,这个函数只有在strlen(s1) == strlen(s2)时返回假,其他情况都返回真。

下面是我的测试代码:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4.   
  5. int strlonger(char *s1, char *s2)  
  6. {  
  7.     return strlen(s1) - strlen(s2) > 0;  
  8. }  
  9.   
  10. int main()  
  11. {  
  12.     char s1[] = "abc";  
  13.     char s2[] = "cd";  
  14.   
  15.     if(strlonger(s1, s2))  
  16.         printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2);  
  17.     else  
  18.         printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2);  
  19.   
  20.     if(strlonger(s2, s1))  
  21.         printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1);  
  22.     else  
  23.         printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1);  
  24. }  
运行结果如下:



从运行结果来看,确实如此,只要s1与s2长度不等,就返回真。那么我们在怎么样改善这段代码呢?其实答案也是很简单的,所函数改为如下即可:
[html] view plain copy
 print?
  1. int strlonger(char *s1, char *s2)  
  2. {  
  3.     return strlen(s1) > strlen(s2);  
  4. }  
这样就可以利用两个无符号数进行直接的比较而不会因为减法而出现负数(数学上来说)而影响比较结果

五、建议
这么看来,unsigned还真是一个危险的东西,大家还是要谨慎使用啊。其实个人建议,没有什么必要的原因,就不要使用unsigned,即使有时它看起来是那么的合理,因为有它在的运算,很多时候会产生非直观的错误,而且这种错误还非常难发现。如果你要使用的话,则尽量避免有符号数与无符号数的比较运算和避免减法运算,在很多时候,在unsigned的世界里,x-y>0与x>y都是不等价的。

转自:http://blog.csdn.net/ljianhui/article/details/10367703
posted @ 2017-04-11 21:06  隔壁王叔叔a  阅读(346)  评论(0编辑  收藏  举报