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

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

作者:刘俊延(Alinshans)

本系列文章针对我在写C++代码的过程中,尤其是做自己的项目时,踩过的各种坑。以此作为给自己的警惕。

 


 

【版权声明】转载请注明原文来自:http://www.cnblogs.com/GodA/p/6569254.html

 

前言:

  如果你看了我的上一篇博客:《C++之那些年踩过的坑(二)》 我推荐你再看一遍,因为我对内容和排版做了一些修改,尤其是对说的不当的地方进行了修正。

 

  最近挺忙,这篇存了好久,还没发出去,讲多了一下写不完。所以今天就随便聊点简单的东西——unsigned type

 

一、unsigned type 会有什么坑?

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

 

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

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

#include <iostream>
#include <cstdlib>

int main()
{
  char sz[] = "Hello world!";
  for (auto i = strlen(sz) - 1; i >= 0; --i)
  {
    std::cout << sz[i];
  }
}

  上述代码逆序输出字符串,有什么问题?一部分人一眼看出了问题,相当一部分人一眼没看出问题,多看两遍终于看出了问题,还有一小部分人到现在还没看出问题。

  问题就在于 strlen 返回的类型是 size_t ,它是一个无符号类型,而一个无符号类型永远大于等于 0!是的没错,死循环了。如果你用的是 vs 这种有 intelliSense 的IDE,那还好,鼠标移过去,你能够看到函数的声明。那万一没有这东西呢?又或者,有一些你不认识的函数,那些类型已经被 typedef 到你认不出了,你又如何辨别出来呢?上述的例子非常非常简单,但难免在实际中有可能会出现更复杂的例子,然后你头晕眼花就写出了这样的代码。所以,当你用一个数值作为循环条件,你就要警惕一下,数值表示的范围是什么,循环终止的条件,边界等等。当然,unsigned type 是更容易被忽视的,所以出错的概率更大。

 

2、小心可能的访问越界

  其实上面的例子也是属于访问越界,但C++的数组兼容C,C的数组是不作边界检查的,所以你写出 sz[-1] 这样的代码也不一定会错。所以还是推荐使用C++标准库的容器,对于日常使用,绰绰有余。C++的容器,如果越界了,他就会有提示(Debug模式),比如用 vs 的话,在Debug模式下,越界了他就会有弹出一个对话框显示“xxx subscript out of range”之类的信息。

这个的错法跟上面的其实也差不多,也是写出了类似这样的代码:

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

  之前我写一个 BigInteger 类的时候,也有很多用到这样的倒序输出,一开始我也是这样写,然后一直错。然后我就想肯定是循环下标出了问题的,然后就按 Ctrl+F 查找 for,还好也不是很多,一个一个看,最终才醒悟过来。真是屡踩坑不改~~所以才要记下来让自己印象深刻,下次即使出错也能很快反应过来是错在了什么地方。

 

二、解决方案

  这个 unsigned type 的坑比较简单,主要在 0 这个点容易出错,有这个意识就好了。解决方案(至少)有以下几种:

1、显式指定可以容纳范围的 signed 类型

  如果你知道数值的确切范围了,如果可以有一个 signed 类型能容纳它,那么就显式指定出来,比如我知道 vector 最大大小不会超过一百万个,那么我就可以用:

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

这样来指定类型。

 

2、在循环内部做检查

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

 

  if (v.size() != 0)
  {
    for (auto i = v.size() - 1; i >= 0; --i)
    {
      std::cout << v[i] << " ";
      if (i == 0)
        break;
    }
  }

 

 

3、把边界情况移到循环外部

  其实是第2种方法的变形,如果你觉得检查下标浪费了很多性能,那么就可以这样做:

 

  if (v.size() != 0)
  {
    for (auto i = v.size() - 1; i > 0; --i)
    {
      std::cout << v[i] << " ";
    }
    std::cout << v[0];
  }

 

 

4、使用迭代器

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

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

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

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

最终选择哪种就看实际情况和个人爱好咯!

 

三、其它杂谈

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

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

 

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

 

四、总结

标题即总结。

 

posted on 2017-03-27 15:26  Alinshans  阅读(1008)  评论(2编辑  收藏  举报