本文主要详细讲述了无符号的各种负面特性。很多中文书籍或文章没有专门详细地解析清楚这方面的内容,所以我这里专门开篇写一文,在写本文的时候,也从老外的相关资料做了不少的借鉴,并且利用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的转换的有效数据区间
足以应付日常开发的方方面面。