前言
本文主要介绍自己在使用C++编程时曾遇见的一些bug以及解决方案。
记得刚接触编程时,老师说过:学一门编程语言,要多写代码,多看报错,多dug。的确,对于初学者最忌纸上谈兵,光说不做,看看就行;结果一到亲自写代码,一个 hello world 程序,IDE全是红线 (不知多少人是怎么过来的) 。那时候,最反感的就是莫名巧妙的报错 + 一堆看不懂的英文。当学完基础语法后,刚被语法错误折磨完,又来了逻辑错误。
曾经找个bug,一个钟头这么就过去了 (我那宝贵的游戏时间就这么没了-_-)… …
下面我将介绍一些曾让我受罪的bug:
1. 无符号整型回绕
vector<int> nums{0, 1, 2};
for (auto i = nums.size() - 1; i >= 0; i--)
cout << nums[i] << " ";
乍一看好像就只是倒序输出 vector 中的内容,但一运行,会输出不止3个数,进入死循环,这是怎么回事呢?相信知道的小伙伴一眼就能看到问题所在——“auto”。
auto 关键词是C++11引入的,用于自动推导类型
vector的 size() 函数的返回类型为 size_t,查看其定义可知 size_t 为 unsigned long long (不同平台可能不一样,但是都是无符号整型)。我们知道无符号整型有一个回绕问题(简单说就是对于一个无符号整数 i = 0,那么 i - 1 不为 -1,而应该是此类型的最大值。不清楚原因的请自行百度),那么现在再看代码,当 i 减为 0 时,再减 1,就会变为 size_t 的最大值(反正是个正数),而判断条件为 i >= 0,显然会永远满足,从而进入死循环,同时 vector 访问越界。
所以解决方案很简单,只要将 i 设为有符号整型即可。从这个案例也可以看出,当无符号整型参与运算时,我们不得不注意其回绕问题。
2. 整除
int x = 5, y = 2;
double d = x / y;
cout << d << endl;
看此代码,如果你不清楚C++的 /
的特性,可能你会觉得 d = 2.5,然而事实上 d = 2。C++ 的 /
并不是传统数学中的除法,其的行为会根据操作数的类型有所不同:
- 当操作数都为整型时,
/
表示整除,向下取整,因此返回结果也是 整型。- 当操作数存在浮点数时,
/
就与数学中的除法一样,返回的是 浮点型。
因此,只需要将其中一个操作数转为浮点数即可。
插一句,比如以下代码:
double d = 5 / 2;
结果也为 2,原因同上。记得以前上课老师让我们将其改正确,结果诞生出了一堆复杂化代码,显式转换类型,还记得我写的是:
double d = static_cast<double>(5) / 2;
结果最简单的代码是
double d = 5.0 / 2;
当时的内心:我是XX
因此上面给的演示代码可以写为
double d = x * 1.0 / y;
3. 数据溢出
int x = INT_MAX, y = INT_MAX;
int res = (x + y) / 2;
cout << res;
INT_MAX在 <climits> 头文件中 ,表示 int 类型所能表示的最大值 2147483647
如果你不假思索,可能直接认为 res = 2147483647 (INT_MAX) ,但是事实往往事与愿违——res = -1。不讨论结果为什么为 -1,讨论为什么结果不正确:
- 我将上述的文件编译为汇编,值得注意的内容如下:
可以看到使用的是 x + y 被送入 32 位的寄存器 edx
因为 x,y 为 int 类型,int 类型占 4 字节,也就是 32 位
- 现在我将上述代码中的 x, y 的类型改为 long long,然后编译为汇编,内容如下:
现在 x + y 被送入 64 位的寄存器 rdx
long long 为 8 字节,也就是 64 位
如果你懂汇编,你应该能分析出问题所在
如果你不懂汇编,你可以看下面的不严谨的分析
因此不难分析出问题所在,当 x 与 y 都是 int 时,那么 int res = (x + y) / 2; 可以看做两步指令:
- int temp = x + y;
- int res = temp / 2;
temp 为 int 的原因不是 res 是 int,而是因为 x, y 是 int。因此如果你将代码改为 long long res = (x + y) / 2; 结果仍然错误。 (long long res = (x + y) / 2; 可以看做 int temp = x + y; long long res = temp / 2; )
由于第一步指令 x 与 y 的值为 INT_MAX,显然会造成 int 数据溢出,故最后结果不正确。
因此避免此类情况的方法就是采用更大的类型来存储 x, y,或者更好的方法:
将括号展开:int res = x / 2 + y / 2;
又或者: int res = (x - y) / 2 + y;
此 bug 的案例可能比较容易看出问题所在,因为直接告诉你 x,y 很大,但是日常写代码中,很少出现此情况,更多地可能会是一个数在经过多次计算变得较大但是没有溢出,然后经过类似上述的案例,从而发生数据溢出。