一、问题描述

今天再次遇到一个在 C++ 中有趣的代码,虽然没有上一次奇怪的循环(C++)分析难,但是确实很有意义,值得研究总结一番。

首先,让我们看一下这个奇怪的循环代码到底长什么样,下面就是他的全貌:

奇怪的循环(C++)II
1
2
3
4
vector<int> arr(1, 10);
for (size_t i = arr.size() - 1; i >= 0; --i) {
cout << i << "\n";
}

乍一看,好像这段代码并没有什么问题。其实,如果你还记得 size_t 被定义为 unsigned int/long 的话,你可能就会发现这个盲点。

为了讲述方便,我们呢直接默认在 64 位机器上,size_t 被定义位 unsigned long 类型,同时将代码简化成下述样子:

简化后代码
1
2
3
for (size_t i = 1; i >= 0; --i) {
cout << i << "\n";
}

很容易知道,当循环进行到 i = 0 这一层结束的时候会执行 --i 并判断 i >= 0,也就是 0 - 1(但是这个 0 是 unsigned long 类型),于是你会得到 18446744073709551615 这个值(2^64 - 1)。

如果你反应足够快,你会发现这个数就是 size_t 类型变量的最大值,而这个数是永远不会小于 0 的。于是乎这个循环以一种奇怪的方式又重新开始了… …

二、如何避免?

这个东西看起来很简单,但确实程序中很容易出现的致命错误。那么,如何避免就显得非常的重要。

2.1 非根源性避免

在第一个例子中,主要是面向一些常见的代码场景。对于这类情况,可以引出一个 C++17 及以上版本的 CPPCoder 的好习惯:使用 auto 关键字

auto 关键字会强制你初始化一个变量,同时会强制推导类型与初始化返回值类型一致。那么,你就会在这样类似的场景下避免该问题。

1
2
3
4
vector<int> arr(1, 10);
for (auto i = arr.size() - 1; i >= 0; --i) {
cout << i << "\n";
}

之所以说这是非根源性的解决办法是因为这个解决办法不涉及该问题的本质,但是该问题确实可以引出 auto 关键字的好处,所以才拿出来 show show😛。

2.2 根源性避免

其实这个问题的本质是对变量类型的临界点不敏感造成的,如果你能在编写代码时对 unsigned int/long 的最小值 0 足够敏感,那么你几乎 99% 会注意到 i >= 0 这个判断条件有问题。

因为在平常的编写代码过程中,0 往往不会涉及到 int 类型的边界问题,所以类似于 i >= 0 这样的判断条件自然也不会多疑。

所以,我们应该随时保持对边界问题的敏感,一旦涉及边界问题就应该多想一步!

对于该问题,根源性避免就是保持对 unsigned * 类型的边界 0 的敏感,处理时多想一想。