正如我们所知道的,编程语句都有很多的基本数据类型,如char,inf,float等等,而在C和C++中还有一个特殊的类型就是无符号数,它由unsigned修饰,如unsigned int等。大家有没想过,就是因为这些不同的类型,而使大家编写的看似非常正确的程序出现了预想不到的错误呢?
一、迷惑人的有符号下无符号数的比较操作
废话不多说,马上来看一下例子,让你先来体验一下这个奇妙的旅程,源代码文件名为unsigned.c,源代码如下:
 
#include <stdio.h>
#include <stdlib.h> int main()
{
int a = -1;
unsigned int b = 1; if(a > b)
printf("a > b, a = %d, b = %u\n", a, b);
else
printf("a <= b, a = %d, b = %u\n", a, b);
exit(0);
}
输出结果为:



看到输出结果之后,你可能会大吃一惊,-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,代码如下:
 
#include <stdio.h>
#include <stdlib.h> void show_bytes(unsigned char *start, int len)
{
int i = 0;
for(; i < len; ++i)
printf(" %.2x", start[i]);
printf("\n");
} int main()
{
int a = -1;
unsigned int b = 4294967295; printf("a = %d, a = %u\n", a, a);
printf("b = %d, b = %u\n", b, b); show_bytes((unsigned char*)&a, sizeof(int));
show_bytes((unsigned char*)&b, sizeof(unsigned int));
exit(0);
}
输出为:



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

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

你可能会说,你不会用一个无符号数与一个有符号数作比较,所以你觉得你可以放心了,但是来看看下面的两段代码。
代码1是一个求数组中前length个数据的和的函数,数组中元素的个数由参数length给出,代码如下:
 
float sum_elements(float a[], unsigned length)
{
int i = 0;
float sum = 0;
for(i = 0; i <= length -1; ++i)
sum += a[i];
return sum;
}
如果我告诉你这是一段有错的代码,可能你也不太相信,因为这个函数的一切看起来是这么的自然,因为数据的长度(或个数)肯定是一个非负数,所以把length声明为一个unsigned很合理,计算的数据个数和返回类型也正确。的确如此,但是这都是在length不为0的情况,试想,当调用函数时,把0作为参数传递给length会发生什么事情?回想一下前面我们所说的知识,因为length是unsigned类型,所以所有的运算都被隐式地被强制转换为unsigned类型,所以length-1(即0-1 = -1),-1对应的无符号类型的值为UMax,所以for循环将会循环UMax次,数组也会越界,发生错误。那么如何优化上面的代码呢?其实答案非常简单,你也可以自己想一想,这里就给出答案吧,就是把for循环改为:
for(i = 0; i < length; ++i)
因为去除了length-1,所以当length为0时也能正常比较。
接下来是代码2,它是一个判断第一个字符串是否长于第二个字符串,若是,返回1,若否返回0,代码如下:
 
int strlonger(char *s1, char *s2)
{
return strlen(s1) - strlen(s2) > 0;
}
如果我又跟你说这段代码是有bug,你现在找不找得出来呢,还是认为这段代码是没有任何问题的呢?说真的就这么看这个函数好像的确是没有什么问题,但是如果你知道了strlen函数的原型,可能你就会有点明白了,在Linux下可用man 3 strlen命令查看,strlen函数的原型为:
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)时返回假,其他情况都返回真。
下面是我的测试代码:
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h> int strlonger(char *s1, char *s2)
{
return strlen(s1) - strlen(s2) > 0;
} int main()
{
char s1[] = "abc";
char s2[] = "cd"; if(strlonger(s1, s2))
printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2);
else
printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2); if(strlonger(s2, s1))
printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1);
else
printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1);
}
运行结果如下:



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

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

都是类型惹的祸——小心unsigned的更多相关文章

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

    正如我们所知道的,编程语句都有很多的基本数据类型,如char,inf,float等等,而在C和C++中还有一个特殊的类型就是无符号数,它由unsigned修饰,如unsigned int等.大家有没想 ...

  2. ie8 background css没有显示?——都是空格惹的祸

    ie8 background css没有显示?——都是空格惹的祸

  3. 都是SCI惹的祸?

    都是SCI惹的祸? 过去只知道地质学家需要跋山涉水寻找宝藏,最近同一位海外归来的学者谈起,方知少数其它领域的科研人员,也"跋山涉水",在内地研究机构寻找可以写好文章的研究成果,不管 ...

  4. 都是iconv惹的祸

    今天在做采集的时候发现只取到了网页的部分内容,当时我就郁闷了,之前都用的采集都可以采集到网页的所有内容,但这次死活就取到部分内容.寻找原因才知道原来是iconv惹的祸. 发现问题时,网上搜了搜,才发现 ...

  5. Dubbo 高危漏洞!原来都是反序列化惹得祸

    前言 这周收到外部合作同事推送的一篇文章,[漏洞通告]Apache Dubbo Provider默认反序列化远程代码执行漏洞(CVE-2020-1948)通告. 按照文章披露的漏洞影响范围,可以说是当 ...

  6. Session丢失,都是CDN惹的祸

    周六下午,正在外面吃饭,运营的同事火急火燎地给我打电话,说是网站出问题了,用户登录不了,而且这种情况也不是全部,只有部分地区有问题.没办法,只能回到家里找问题,打开代码,翻来覆去地找问题,搞了整整一下 ...

  7. 【实习记】2014-08-26都是回车惹的祸——shell脚本必须是unix行尾

        事情由起:svn的url在excel里,我复制到txt文本下,vi做些文本处理,只提取了url,保存为url.txt.再用vi处理url.txt,加上svn checkout等词,变成可以运行 ...

  8. 都是分号惹的祸 ORA-00911

    使用JMeter连接oracle数据库,访问JDBC 请求,执行结果提示:ORA-00911: ??Ч??? 意思为无效的字符错误 说明了在执行的的SQL语句中出现了无效字符,所以在AQL语句无法通过 ...

  9. 实战c++中的vector系列--再谈vector的insert()方法(都是make_move_iterator惹的祸)

    之前说过了关于vector的insert()方法,把vector B的元素插入到vector A中.vector A中的结果我们可想而知,可是vector B中的元素还会怎样? 看看之前写过的程序: ...

随机推荐

  1. A.xml

    pre{ line-height:1; color:#1e1e1e; background-color:#f0f0f0; font-size:16px;}.sysFunc{color:#627cf6; ...

  2. ArcGIS API Reference & Flex API samples学习进度备忘

    书签:跳过:另外跳过的内容有待跟进 __________________学习资源: 1.http://help.arcgis.com/en/webapi/flex/apiref/index.html ...

  3. 主机找不到vmnet1和vmnet8

    今天跑程序时,突然发现虚拟机ping不通主机了,返过来可行,防火墙什么的都设置好了,仍然不行,后来发现,在网络和共享中心已经看不到vmnet1和vmnet8了,更改适配器设置也只有本地连接和宽带连接, ...

  4. pku3659 Cell Phone Network

    http://poj.org/problem?id=3659 树状DP,树的最小点覆盖 #include <stdio.h> #include <vector> #define ...

  5. 第三百二十二天 how can I 坚持

    昨晚好像一直在做梦,模模糊糊的,其实很难受. 真不知道该怎么办了,是没有勇气,还是怕什么,总之, 不知道该咋办了. 搞不懂. 回来第二天了,节奏又一点的开始了,一年又会很快过去. 刘松说要来北京,整天 ...

  6. 我的第一个CUDA程序

    最近在学习CUDA框架,折腾了一个多月终于把CUDA安装完毕,现在终于跑通了自己的一个CUDA的Hello world程序,值得欣喜~ 首先,关于CUDA的初始化,代码和解释如下,这部分主要参考GXW ...

  7. C++ 16进制转10进制

    #include <stdio.h>#include <string.h>unsigned long f(char* str){ unsigned long var=0; un ...

  8. datagridview自动填充列头

    //填充datagridview dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;

  9. POJ 2828Buy Tickets

    POJ 2828 题目大意是说有n个插入操作,每次把B插入到位置A,原来A以后的全部往后移动1,球最后的序列 tree里保存的应该是这整个区间还有多扫个位置可以插入数据,那么线段树里从后往前扫描依次插 ...

  10. ELF学习--可执行文件

    承接上一篇.我们来看看可执行文件和重定位文件的不同点. readelf -h test 相比较重定位文件,可执行文件的ELF header中入口地址是0x8048320.而且除了section hea ...