第1篇:C/C++ 有符号和无符号数字的迷途

本文主要详细讲述了无符号的各种负面特性。很多中文书籍或文章没有专门详细地解析清楚这方面的内容,所以我这里专门开篇写一文,在写本文的时候,也从老外的相关资料做了不少的借鉴,并且利用bitset这个工具函数,从内存二进制数据的角度来解析无符号和带符号整数的相关特性。下面的各个示例已经运行过一片,做了自己充分的分析。本文可能还不太完善,我也会在以后不定时地更新。

常用整数的区间

有符号整数溢出

下面的示例代码,是用来展示什么叫数字溢出(Integer Overflow)

int main(int argc, char const *argv[])
{
    short c = 33333;
    std::cout << c << std::endl;
    std::cout << std::bitset<sizeof(short) * 8>(c) << std::endl;
    std::cout << std::bitset<sizeof(short) * 8>(-33333) << std::endl;
    std::cout << std::bitset<17>(33333) << std::endl;
    return 0;
}

我们值整数33333(如果用auto关键字声明,编译器会为该值需要32位的int存储该值,实际该值真正用到就17位,除了最高位0作为MSB判断正/负,其余15位是为了内存对齐填充之用),但是我们故意强制存储在16字节就会发生溢出,而溢出的部分是前16位的高位地址,因为我们声明的变量是short类型为2个字节,CPU只会截取指定的16位中的二进制数据,如下图


同时对于unsigned short的声明类型,CPU截取的16位字节码会被认为是二进制补码形式,这里其实就是-32203的绝对值的补码的后16位的字节码,因此这种溢出的后果就输出了-32203。

上面的示例代码,如果我们声明为 unsigned short的话,就不存二进制的补码形式,在无符号整数中也不存在所谓的MSB位。因此可以正确解读为33333。

副作用2:无符号整数的环绕

int main()
{
    unsigned short x = 65535; 
    std::cout << "x是: " << x << '\n';
    std::cout << "65535 的二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = 65536; 
    std::cout << "x是: " << x << '\n';
    std::cout << "65536 的二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = 65537; 
    std::cout << "x 是: " << x << '\n';
    std::cout << "65537 的二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    return 0;
}

其实无符号整数没有溢出的说法,如果被赋值的整数超出该无符号整数可以表示的范围, 任何大于该类型可表示的最大数字的数字都只是“环绕(wraps around)

65535在2字节整数范围内,因此65535可以。
但是
65536超出了范围,绕回了值0,二进制形式:0000000000000000
65537超出了范围,绕回了值1,二进制形式:0000000000000001
65538超出了范围,绕回了值2,二进制形式:0000000000000010

如此类推....
当我们向无符号类型的整数赋一个负数,无符号整数的环绕会从该无符号整数的最大整数方向开始环绕

int main()
{
    unsigned short x = 0;

    std::cout << "x=0 时,二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';
    
    x = -1;
    std::cout << "x=-1时,二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = -2;
    std::cout << "x=-2 时,二进制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    return 0;
}

x=0 时,二进制形式: 0000000000000000,这个在取值区间内,是正常的
x=-1时,二进制形式: 1111111111111111,会被环绕成65535
x=-2 时,二进制形式: 1111111111111110,会被环绕成65534

副作用3:无符号整数的减法问题

许多老资格的C/C++程序员都推荐在日常项目尽量避免使用无符号整数,上文已经罗列了无符号整数的副作用了,这里再看看此处的代码

int main()
{
    unsigned int x = 2;
    unsigned int y = 5;
    auto r = x - y;
    std::cout << "x-y=" << r << std::endl;
    return 0;
}

我们得知2-5是等于-3,但是-3是无法在无符号整数类型中正确表示的,那么为什么会输出424967293,我们按照上面的例子提供思路,你推理得到,由于-3无法正常表示的,环绕到int类型最大整数范围的顶部。

-1环绕到4,294,967,295
-2环绕到4,294,967,294
-3环绕到4,294,967,293

错误的输出

我们再进一步用
std::bitset<32>(-3)
std::bitset<32>(4294967293)
可以得知一个下图的32位的二进制码,该字节码能对于不同的数据类型能做不同解读

  • 对于signed int能够解读为-3的二进制补码
  • 对于unsigned int 能够解读为4294967293


副作用4:无符号整数和带符号整数混用

第二,混合有符号和无符号整数时,可能会导致意外行为。例如我们将无符号的整数作为函数的参数,C编译器是无法检测传入的整数是否符合特定的程序逻辑的,尤其是无符号和带符号的比较问题,可以参见如下例子,这个例子引用自stackflow网站,而且提问率也非常高。

例如执行如下函数,我们希望给函数传递一个无符号整数,当传入参数大于10才执行特定操作,否则就不执行该操作。

void foo(size_t n){
      if(n>10){
            cout<<n<"我正在处理某项任务...."<<endl;
       }else{
            cout<<n<"条件没达成,我没有执行任务...."<<endl;
      }
}

int main(void){
    foo(-10);
}

我们向foo中传递任意一个负数,都能该程序不按我们的逻辑完全跑偏了,编译器在编译程序的时候也没有警告。 因此,混淆带符号和无符号对于程序设计来说通常是一个逻辑错误。那么问题的根源是什么呢?stackflow的答案我仍然不太满意,同学们可以自行查找该问题,而问题的本质仍然提到无符号整数的环绕行为。至于-10被环绕成size_t的某个无符号整数的最大值,请读者自行思考,我就不重复熬述了。

程序没有按我们的意愿去执行

而解决该问题即非常简单,只要我们将size_t类型的参数变换成带符号的int/long等可以令程序正常运行了。

直到写本文用的是C++17,这个问题C ++程序员仍然无法回避,当你翻开C++标准库的头文件,大量地使用了类似size_t作为参数的类型和函数返回值。我们必须勇敢面对无符号整数的各种副作用。

从那么多的副作用示例中,我们得到结论就是在我们的应用代码中尽量避免使用无符号类型的数字类型,但有时候我们必须从将标准符的API中返回size_t类型转换为带符号的数字类型版本。

正确面对无符号类型的整数

毫不奇怪,在任何类型的转换期间都可能发生奇怪的事情,并且使用无符号到有符号整数也不例外。但是,这些类型的转换特别棘手,因为它们可能导致并非总是引人注意的错误,并最终会影响您的C ++代码,当您具有可与STL库互操作的C ++代码时,无符号整数很常见。实际上,例如您的程序是一个使用在std :: vector容器中获取商品计数,则vector的size方法将返回就是size_t的类型的值,这是一个无符号整数。

那么,从无符号到带符号转换的过程中,我们如何获得singed类型变量的最大值?

转换前检查整数限制

从无符号整数到有符号整数的转换。我们需要检查输入的无符号的字面量值是否超过当前有符号整数类型的最大上限。 若超出在这种情况下,我们将继续抛出C ++异常来,提醒用户目前处理的整数超出了当前带符号整数的最大整数。

嗯~~,C ++标准库中有一个名为std :: numeric_limits的标准组件。 这是一个类模板,可用于查询算术类型(包括int)的各种属性。 您将类型的名称作“ numeric_limits <T> :: max()”将返回类型T的最大值。这里的例子以将最大值存储在int类型的变量中,因此我们可以简单地调用numeric_limits <int>::max() ,如下示例

template <typename T>
T convert_numberic(size_t n)
{
    if (n > static_cast<size_t>(std::numeric_limits<T>::max()))
    {
        throw std::overflow_error("参数n转换错误");
    }
    return static_cast<T>(n);
};

除非你从事的是天文学类似的计算,我敢肯定从无符号的int /long到带符号的int/long的转换的有效数据区间
足以应付日常开发的方方面面。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容