有符号数与无符号数比较-详解

2023-11-14

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

一、迷惑人的有符号下无符号数的比较操作

废话不多说,马上来看一下例子,让你先来体验一下这个奇妙的旅程,源代码文件名为unsigned.c,源代码如下:

[cpp] view plain copy  print?

  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,代码如下:

[cpp] view plain copy  print?

  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给出,代码如下:

[cpp] view plain copy  print?

  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)  
  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循环改为:

[cpp] view plain copy  print?

  1. for(i = 0; i < length; ++i)  

因为去除了length-1,所以当length为0时也能正常比较。

接下来是代码2,它是一个判断第一个字符串是否长于第二个字符串,若是,返回1,若否返回0,代码如下:

[cpp] view plain copy  print?

  1. int strlonger(char *s1, char *s2)  
  2. {  
  3.     return strlen(s1) - strlen(s2) > 0;  
  4. }  

如果我又跟你说这段代码是有bug,你现在找不找得出来呢,还是认为这段代码是没有任何问题的呢?说真的就这么看这个函数好像的确是没有什么问题,但是如果你知道了strlen函数的原型,可能你就会有点明白了,在Linux下可用man 3 strlen命令查看,strlen函数的原型为:

[cpp] view plain copy  print?

  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)时返回假,其他情况都返回真。

下面是我的测试代码:

[cpp] view plain copy  print?

  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都是不等价的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

有符号数与无符号数比较-详解 的相关文章

  • 从 Invoke 方法获取 RETURN

    我正在尝试从另一个线程上的列表框项目中读取值 我尝试创建一种新方法来运行调用命令 我可以设法将命令发送到列表框 例如通过调用方法添加 但我似乎无法得到响应 我似乎无法获取该项目的值 我尝试了几种方法 一旦我将它从空变为字符串 事情就开始变得
  • 通过另一个列表更新列表(linq)

    我有类 Data 的对象列表 如下所示 class Data int code string name DateTime date update 我还有另一个课程列表 例如 class RefCodes int old code int n
  • 为什么在 C++ 中声明枚举时使用 typedef?

    我已经很多年没有写过任何 C 了 现在我正试图重新开始 然后我遇到了这个并考虑放弃 typedef enum TokenType blah1 0x00000000 blah2 0X01000000 blah3 0X02000000 Toke
  • 如何调整 Windows 窗体以适应任何屏幕分辨率?

    我知道这是重复的问题 但我检查了所有其他相关问题 他们的答案没有帮助 结果仍然与屏幕截图 2 中所示相同 我是 C Windows 窗体新手 如截图1所示 我有Form1有一些控件 每组控件都放在一个面板中 我在 PC1 中设计了应用程序
  • 如何查明 .exe 是否正在 C++ 中运行?

    给定进程名称 例如 程序 exe C 标准库没有这样的支持 您需要一个操作系统 API 来执行此操作 如果这是 Windows 那么您将使用 CreateToolhelp32Snapshot 然后使用 Process32First 和 Pr
  • make_shared<>() 中的 WKWYL 优化是否会给某些多线程应用程序带来惩罚?

    前几天我偶然看到这个非常有趣的演示 http channel9 msdn com Events GoingNative GoingNative 2012 STL11 Magic Secrets作者 Stephan T Lavavej 其中提
  • PrivateObject 找不到属性

    我的结构基本上如下所示 abstract class A protected string Identificator get set private void DoSomething DoSomethingSpecific protect
  • 关闭整数的最右边设置位

    我只需要关闭最右边的设置位即可 我的方法是找到最右边位的位置 然后离开该位 我编写这段代码是为了这样做 int POS int n int p 0 while n if n 2 0 p else break n n 2 return p i
  • C 类型命名约定,_t 或 ALLCAPS

    我一直想知道是否有任何命名约定 例如何时对类型使用全部大写以及何时追加 t 什么时候不使用任何东西 我知道当时 K R 发布了各种有关如何使用 C 的文档 但我找不到任何相关内容 在 C 标准库类型中 t看起来漂亮占主导地位 time t
  • 提升mapped_file_source、对齐方式和页面大小

    我正在尝试在性能很重要的上下文中解析一些大小高达几百兆字节的文本文件 因此我使用 boostmapped file source 解析器期望源以空字节终止 因此我想检查文件大小是否是页面大小的精确倍数 如果是 则使用较慢的非内存映射方法 我
  • 如果在代码中添加元素,“FindName”将不起作用

    在 WPF 应用程序中 如果在 XAML 中声明 ContentControl
  • 如何在 EF Core 2.1 中定义外键关系

    我的 DAL 使用 EF Core 2 1 这就是我的模型的样子 一名用户只能拥有一种角色 Role entity kind of master public class Role public int RoleId get set pub
  • 测验;这个编译了吗?如果是的话它会返回什么(我知道答案)

    我最近发现这个错字 if name find string npos 显然开发者的意思是输入 if name find string npos 但令我惊讶的是发现错误甚至编译 Wall Werror 没有尝试过 pedantic 那么 咖啡
  • WPF DataGrid - 在每行末尾添加按钮

    我想在数据网格的每一行的末尾添加一个按钮 我找到了以下 xaml 但它将按钮添加到开头 有人知道如何在所有数据绑定列之后添加它吗 这会将按钮添加到开头而不是末尾
  • 在 Qt 中播放通知(频率 x)声音 - 最简单的方法?

    Qt 5 1 或更高版本 我需要播放频率为 x 的通知声音 n 毫秒 如果我能像这样组合音调那就太好了 1000Hz 持续 2 秒 然后 3000Hz 持续 1 秒 最简单的方法是使用文件 WAV MP3 例如如此处所述 如何用Qt播放声音
  • 如何测试某些代码在 C++ 中无法编译? [复制]

    这个问题在这里已经有答案了 可能的重复 单元测试编译时错误 https stackoverflow com questions 605915 unit test compile time error 我想知道是否可以编写一种单元测试来验证给
  • 用数组或向量实现多维数组

    我想使用单个数组或向量实现多维数组 可以像通常的多维数组一样访问它 例如 a 1 2 3 我陷入困境的是如何实施 操作员 如果数组的维数为 1 则 a 1 应该返回位于索引 1 处的元素 但是如果维数大于一怎么办 对于嵌套向量 例如 3 维
  • 与 Entity Framework Core 2.0 的一对零关系

    我正在使用 C 和 NET Framework 4 7 将 Entity Framework 6 1 3 Code First 库迁移到 Entity Framework Core 我一直在用 Google 搜索 Entity Framew
  • 对多个对象使用事件处理程序

    我有 20 件物品List
  • Emacs C++,打开相应的头文件

    我是 emacs 新手 我想知道 是否有在头文件 源文件和相应的源文件 头文件之间切换的快捷方式 是否有像通用 emacs 参考卡那样的参考卡 Thanks There s ff find other file 您可以使用以下方法将其绑定到

随机推荐

  • Android Studio 红米3 一直运行或者debug不成功,提示 Failed to establish session 解决方案

    换了一个测试机 红米note3开发 一直run OR debug 失败 下面是提示图 找了半天原因 后面发现原因所在了 一般手机默认用开发工具跑起来 会弹出提示 确认是否安装XXX应用 而红米note3就是个奇葩 在它的开发者选项中 有个
  • MATLAB 多目标规划

    作者简介 人工智能专业本科在读 喜欢计算机与编程 写博客记录自己的学习历程 个人主页 小嗷犬的个人主页 个人网站 小嗷犬的技术小站 个人信条 为天地立心 为生民立命 为往圣继绝学 为万世开太平 本文目录 多目标规划 数学模型 正负偏差变量
  • c/c++不定参数函数

    http plutoblog iteye com blog 1150671 不定参数函数 stdarg h是C语言中C标准函数库的头文件 stdarg是由stdandard 标准 arguments 参数 简化而来 主要目的为让函数能够接收
  • WdatePicker日期控件与UEditor富文本编辑器

    WdatePicker日期控件 My97日期控件 下载 更新日志 My97Datepicker Download Changelog 代码中的生日使用插件
  • libevent服务端,多线程应用

    下面的方式是创建多个event base来处理多线程的 主event base用来处理连接请求 各个子event base用来处理读写和关闭请求 另一种方式是 所有的连接 读写 断开操作 都在一个event base里面 然后当读到数据时
  • cesium加webgl的构思

    1 传递gl var gl viewer scene context gl
  • C++类模板

    1 定义类模板 程序清单类模板 1列出了类模板和成员函数模板 明确这些模板不是类和成员函数定义很重要 因为它们是C 编译指令 说明了如何生成类和成员函数定义 不能将模板成员函数放在独立的实现文件中 由于模板不是函数 它们不能单独编译 模板必
  • #、##、__VA_ARGS__的使用,自由扩展printf 可变参数输出到终端和追加到文件等

    include
  • JAVA后端使用MultipartFile类接收处理上传图片【超级简单】

    本例子再SpringBoot项目上 使用Spring MVC的MultipartFile类再JAVA后端 接收前端上传文件请求 1 MultipartFile 单文件图片上传 例子中接收对象与文件 先保存文件 再把文件保存到对象 再保存对象
  • 前端系列之jQuery(jQuery插件)

    jQuery的插件机制 jQuery主要有两种使用方式 1 在jQuery集合对象上调用方法 2 直接调用jQuery方法 扩展jQuery对象上的方法 jQuery fn extend 扩展jQuery工具方法 jQuery extend
  • docker 安装

    docker ce社区版安装 1 首先卸载以前的docker相关内容 yum remove docker docker client docker client latest docker common docker latest dock
  • 102个java计算机本科毕业设计项目大全(附源码)

    今天给计算机专业大四的同学分享102个毕业设计项目 希望对正在为毕业设计发愁的小伙伴有帮助 一 成品列表 以下所有springboot框架项目的源码博主已经打包好上传到百du云了 在文末处 大家自行获取即可 1 Springboot高校专业
  • Vue项目打包成移动端APP

    Vue项目打包成移动端APP 需要准备的工具 Hbuilder 目录 Vue项目打包成移动端APP 首先打包vue到dist目录 然后再Hbuilder中打开dist目录 然后将dist包含的 web项目 转换为 移动 APP项目 前几步配
  • python解最小二乘(least square)

    给定 A R d n A in R d times n
  • 常用的前端4种请求方式

    一 GET请求 前端页面 第一种情况下 第二种情况下 后端代码 对应第一种传输对象 接参方式 若我们强行给对象添加 RequestBody注解 会发生如下错误 第二种情形下 我们取消用 PathVariable来接收前端发来的ID 情况如下
  • Vue学习

    Vue环境的搭建以及Vue项目的创建与启动 时光独白 AWY的博客 CSDN博客 vue 环境启动
  • Git命令上传项目到远程仓库

    1 为当前目录添加Git本地仓库 git init 实例化仓库 为当前目录添加Git本地仓库 添加成功会看到 git的隐藏目录 2 添加到暂存区 git add 文件名或目录名或 其中 表示当前目录下的全部文件 将指定文件 目录 当前目录全
  • 使用power shell连接远程linux服务器

    打开powershell 输入ssh 用户名 ip地址 比如 ssh root 111 111 111 111 输入yes 提示要输入密码 此时输入服务器密码即可
  • adb 调试命令

    ADB Android Debug Bridge 这里性能调试如下 性能测试需要进行如下设置 如果要让user模式能够进行root操作 需要更改 system core adb adb c 将无用的log信息去掉 define LOG NI
  • 有符号数与无符号数比较-详解

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