Fork me on GitHub

C++之那些年踩过的坑(三)

这一篇会讲一个很小的点,但又经常容易犯错 ¯_(ツ)_/¯ ———— unsigned type

一、unsigned type 的坑

看到这篇的开头,你可能就会想,unsigned type 能有什么坑的呀!那我们就直接了当一些吧!

1. 小心可能陷入的死循环

其实单独的 unsigned type 你还是比较容易想明白的,怕的就是它跟一些其它东西搭配(比如 auto),而你又忽略了那就是 unsigned type 的时候,例如:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <cstring>
int main()
{
char sz[] = "Hello world!";
// 倒序输出字符串
for (auto i = std::strlen(sz) - 1; i >= 0; --i)
{
std::cout << sz[i];
}
}

对于上述的代码,会有什么问题吗?当然啦,有些经验的人还是一眼就能看出来的,还有一些人需要多看两眼才能看出来。

问题就在于 std::strlen 的返回值类型是 std::size_t ,这是一个 unsigned type,而一个 unsigned type 的数值永远大于等于 0,是的没错,你很快就反应过来,死循环了。当然这个例子比较简单,而且如果你用 VS 这种有 intelliSense 的 IDE,你敲出函数,或者鼠标移到函数名字上,就能看到函数的声明。那万一不是用这种 IDE 的呢?又或者,一些你不认识的函数,而且类型也被 typedef 到你认不出来了,你又如何分辨呢?虽然这是简单的例子,但确实存在这种风险。

2. 小心可能的访问越界

其实上面的例子也是属于访问越界,但 C++ 的数组兼容 C,而 C 的数组是不作边界检查的,所以实际上,上述代码运行起来不会崩溃,甚至还不一定会出错。为什么说不一定呢,因为 sz[-1] 的内容是不知道的…所以输出也有可能让你撞对了,但是!这个代码一定是错误的。所以我会推荐使用 C++ 的容器,对于日常使用,已经绰绰有余了。C++ 的容器,在 Debug 模式下,是会做边界检查的。比如用 VS,在 Debug 下,容器访问越界了,它会有弹出一个 MessageBox 显示 xxx subscript out of range 之类的信息。

好了回归正题,使用容器时,要是一不小心,写出了类似代码:

1
2
3
4
5
std::vector<int> v{ 1,2,3,4,5 };
for (auto i = v.size() - 1; i >= 0; --i)
{
std::cout << v[i] << " ";
}

就会导致了访问越界了,原因也是 size() 的返回类型是 std::size_t

之前我写一个 BigInteger 类的时候,就有很多用到了这样的倒序输出的地方。一开始没注意,也是出现了访问越界的错误,后来看到 vector subscript out of range 才反应过来。

解决方案

unsigned type 的坑其实也不算很深,主要在于 0 这个点,要注意以下。解决的方案(至少)有以下几种:

1. 显示指定可容纳范围内的 signed type

如果你知道你数据的确切范围,比如不会超过 100w,那么就可以用一个能够放得下 100w 的一个 signed type 去指定它:

1
for (int32_t i = v.size() - 1; i >= 0; --i)

2. 手动做边界检查

如果你不能确定大小,而且不确定 signed type 能否容纳下那个 unsigned type 的全部范围,那就自己在循环的周围做一个边界检查:

1
2
3
4
5
6
7
8
9
if (v.size() != 0)
{
for (auto i = v.size() - 1; i >= 0; --i)
{
std::cout << v[i] << " ";
if (i == 0)
break;
}
}

3. 把边界检查移到循环外部

其实是第2种方法的变形,如果你唯恐在循环内检查损失了很多性能,那么就可以这样做:

1
2
3
4
5
6
7
8
if (v.size() != 0)
{
for (auto i = v.size() - 1; i > 0; --i)
{
std::cout << v[i] << " ";
}
std::cout << v[0];
}

4. 使用 C++ 迭代器

既然用了 C++ 的容器,那么更好的写法当然是这样啦:

1
2
3
4
for (auto i = v.rbegin(); i != v.rend(); ++i)
{
std::cout << *i << " ";
}

这样就不需要去考虑下标范围啦,什么检查啦,多方便!如果想写出更泛型,更有 C++ Style 的代码,还可以这样:

1
std::for_each(std::rbegin(v), std::rend(v), [&](int i) { std::cout << i << " "; });

最终选择什么样的方式还是看实际情况跟个人爱好咯!

三、其它杂谈

我在网上看到不少关于什么“代码优化技巧”等等文章,即使是最近出的,还是这样写,也不知道是不是到处抄的。比如我看到其中一篇就说到:

有些处理器处理无符号unsigned 整形数的效率远远高于有符号signed整形数

正确与否我就先不说了,错别字我也不去说了。留给读者先自己实验一下,我会在下一篇中用一篇来讨论一下这类问题。

四、总结

小标题即总结。

-------------------------------- 全文完 感谢您的阅读 --------------------------------
「写的那么辛苦,连一块钱都不打赏吗/(ㄒoㄒ)/~~」