Hello_Motty

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

条款21:永远让比较函数对相等的值返回false

  stl中set比较两个键值使用operator <, 如果将其换为<= 那么会造成一些未定义错误,比如你可以插入两个相同的值但是查找的时候只会返回给你一个;这当然会令人联想到multiset,但是实际情况是如果你把multiset的默认比较函数换成了重载的<=操作符,仍然不会在插入同一个值时插入在它的equal_range范围内,而是单独产生另一个。除非你的比较函数总是为相等的值返回false,你将会打破所有的标准关联型容器,不管它们是否允许存储复本,这也叫作严格弱序化。

条款22:避免原地修改set和multiset的键

  stl中set和multiset的行为或者成员函数依赖于它们默认有序,如果修改了键值有可能会破坏容器。当然对map而言并没有这个问题,因为map的键值在模板定义时即定义成了const类型,除非使用映射(强制类型转换)的方式否则无法修改。阻止修改的方法,可以通过将set<T>::iterator重载operator*的方式修改为const T&。虽然默认的set并没有const特性, 如果要修改set中定义的对象非键值,如果不在乎可移植性,并且编译器支持,那可以在原地修改,否则在没映射情况下是不能改的。

  当然映射也不能简单的映射到对象的类,因为那会创建并修改一个临时对象而不是你想要修改的内容,这时如果你要修改,你需要把它映射到一个对象的引用上去除它的const特性。如果想要安全的修改set中的非键值,那你需要按照以下步骤:

 

  • 定位你想要改变的容器元素。
  • 拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。毕竟,你想要改变它!
  • 修改副本,使它有你想要在容器里的值。
  • 从容器里删除元素,通常通过调用erase(参见条款9)。
  • 把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步获得的迭代器作为提示。

条款23:考虑用有序vector代替关联容器

  标准关联容器的典型实现是平衡二叉查找树,它们对数据结构的使用可以总结为这样的三个截然不同的阶段:

  1. 建立。通过插入很多元素建立一个新的数据结构。在这个阶段,几乎所有的操作都是插入和删除。几乎没有或根本没有查找。
  2. 查找。在数据结构中查找指定的信息片。在这个阶段,几乎所有的操作都是查找。几乎没有或根本没有插入和删除。
  3. 重组。修改数据结构的内容,也许通过删除所有现有数据和在原地插入新数据。从动作上说,这个阶段等价于阶段1。一旦这个阶段完成,应用程序返回阶段2。

  对于这么使用它们的数据结构的应用来说,一个vector(有序的)可能比一个关联容器能提供更高的性能(时间和空间上都是)。

 

   第一个原因:大小问题。假设我们需要一个容器来容纳对象,关联容器,我们几乎确定了要使用平衡二叉树。这样的树是由树节点组成,每个都不仅容纳了一个Widget,而且保存了一个该节点到左孩子的指针,一个到它右孩子的指针,和(典型的)一个到它父节点的指针,当在vector中存储并没有开销:我们简单地存储一个对象。假设我们的数据结构足够大,它们可以分成多个内存页面,但是vector比关联容器需要的页面要少,关联容器和vector比起来,你将会使用大几倍的内存。如果你使用的环境可以用虚拟内存,就很可以容易地看出那会造成大量的页面错误,因此一个系统会因为大数据量而明显慢下来。

  第二个原因:如果你的STL实现没有改进树节点中的引用局部性,这些节点会分散在所有你的内存空间,而基于节点的容器更难保证在容器的遍历顺序中一个挨着一个的元素在物理内存上也是一个挨着一个正好是页面错误最少的。

  如果你的程序不是按照阶段的方式操作数据结构,那么使用有序vector代替标准关联容器几乎可以确定是在浪费时间。

条款24:当关乎效率时应该在map::operator[]和map-insert之间仔细选择

  在没有对象时,operator[]进行了四次构造:创建一个临时对象,向红黑树中拷贝先建立pair中对象,再建立红黑树中节点对象,返回对象引用类型创建对象进行赋值;insert进行了三次构造:创建pair对象,对红黑树进行插入调用insert_unique进行了两次构造。所以没对象时insert更高效。

  在有对象时:operator直接返回对象并赋值,没有构造;insert创建pair对象和红黑树节点对象,进行两次构造。所以operator[]更高效。

条款25:熟悉非标准散列容器(c++11中已经是标准散列容器了)

  unordered_set、unordered_multiset、unordered_map和unordered_multimap。当然这些有两种实现,一种是单链表指针组成的开放散列且compare中包含相等判断;一种是迭代器数组组成的元素,使用桶+散列表,支持双向迭代器。

条款26:尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator

  从iterator到const_iterator、从iterator到reverse_iterator和从reverse_iterator到const_reverse_iterator可以进行隐式转换。并且,reverse_iterator可以通过调用其base成员函数转换为iterator。const_reverse_iterator也可以类似地通过base转换成为const_iterator。不过通过base得到的也许并非你所期待的iterator。

  insert和erase的一些版本要求iterator。如果你需要调用这些函数,你就必须产生iterator,而不能用const或reverse iterators。不可能把const_iterator隐式转换成iterator,我们将会在条款27中讨论从一个const_iterator产生一个iterator的技术并不普遍适用,而且不保证高效。从reverse_iterator转换而来的iterator在转换之后可能需要相应的调整,在条款28中我们会讨论何时需要调整以及调整的原因。

条款27:用distance和advance把const_iterator转化成iterator

  如果你只有一个const_iterator,而你要在它所指向的容器位置上插入新元素呢?每当无路可走的时候,就举起大锤!在C++的世界里,你的意思只能是:映射。这种想法很可耻。真不知道你是哪儿学来的。对于这些容器而言,iterator和const_iterator是完全不同的类。它们之间并不比string和complex<float>具有更多的血缘关系。

  如果你得到一个const_iterator并且可以访问它所指向的容器,那么有一种安全的、可移植的方法获取它所对应的iterator:

1   typedef deque<int> IntDeque; // 和以前一样
2   typedef IntDeque::iterator Iter;
3   typedef IntDeque::const_iterator ConstIter;
4   IntDeque d;
5   ConstIter ci;
6   ... // 让ci指向d
7   Iter i(d.begin()); // 初始化i为d.begin()
8   advance(i, distance<ConstIter>(i, ci)); // 把i移到指向ci位置

  为什么要对distance指明类型,请看distance的模板:

1   template<typename InputIterator>
2   typename iterator_traits<InputIterator>::difference_type
3   distance(InputIterator first, InputIterator last);

  i使用的迭代器和ci不一样而模板中参数的类型需要保持一致,所以这里要将类型指明,并将i隐式转换为ConstIter类型,否则编译出错。

条款28:了解如何通过reverse_iterator的base得到iterator

  要实现在一个reverse_iterator ri指出的位置上插入新元素,在ri.base()指向的位置插入就行了。对于insert操作而言,ri和ri.base()是等价的,而且ri.base()真的是ri对应的iterator。要实现在一个reverse_iterator ri指出的位置上删除元素,就应该删除ri.base()的前一个元素。对于删除操作而言,ri和ri.base()并不等价,而且ri.base()不是ri对应的iterator,如果这里你不能减少调用base的返回值,只需要先增加reverse_iterator的值,然后再调用base。

条款29:需要一个一个字符输入时考虑使用istreambuf_iterator

  条款6中举例了一个看似很有道理的字符串输入,虽然经过改装后我们可以大致进行使用了,但是其实仍然有问题。因为istream_iterators使用operator>>函数来进行真的读取,而且operator>>函数在默认情况下忽略空格。当然清除输入流的skipws标志就可以读入空格,比如按照下面这么写:

1     ifstream inputFile("interestingData.txt");
2     inputFile.unset(ios::skipws); // 关闭inputFile的忽略空格标志
3     string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());

  但是格式化输入必须每次都去判断输入流标志,所以影响了速度。但是下面这种方法就不会有这个问题。

1     ifstream inputFile("interestingData.txt");
2     string fileData((istreambuf_iterator<char>(inputFile)),istreambuf_iterator<char>());

  因为istreambuf_iterator<char> 对象从一个istream s中读取会调用s.rdbuf()->sgetc()来读s的下一个字符,不会忽略空格也不需要输入流标志。当然ostream_iterator也有这种操作。

条款30:确保目标区间足够大

  STL容器在被添加时自动扩展它们自己来容纳新对象,但是并不是万能的。像下面这段代码:

1 int transmogrify(int x); // 这个函数从x
2 // 产生一些新值
3 vector<int> values;
4 ... // 把数据放入values
5 vector<int> results; // 把transmogrify应用于
6 transform(values.begin(), values.end(), // values中的每个对象
7     results.end(), // 把这个返回的values
8     transmogrify); // 附加到results
9                             // 这段代码有bug!

  transform通过对目标区间的元素赋值的方法写入结果,transform会把transmogrify应用于values[0]并把结果赋给*results.end()。然后它会把transmogrify应用于value[1]并把结果赋给*(results.end()+1)。因为在*results.end()没有对象,*(results.end()+1)也没有。transform愉快地试图对results尾部的原始的、未初始化的内存赋值。通常,这会造成运行期错误,因为赋值只在两个对象之间操作时有意义,而不是在一个对象和一块原始的比特之间。即使这段代码碰巧作了你想要它做的事情,results也不会知道transform在它的未使用容量上“创造”的新“对象”。直到results知道之前,它的大小在调用transform后仍然和原来一样。同样的,它的end迭代器仍和调用transform前指向同样的位置。正确的方式:

1 vector<int> values; // 同上
2 vector<int> results;
3 results.reserve(results.size() + values.size()); // 同上
4 transform(values.begin(), values.end(), // 把transmogrify的结果
5     back_inserter(results), // 写入results的结尾,
6     transmogrify); // 处理时避免了重新分配

  如果你选择增加大小,就使用插入迭代器,比如ostream_iterators或从back_inserter、front_inserter或inserter返回的迭代器。

TBC

 

posted on 2017-08-25 23:40  Hello_Motty  阅读(263)  评论(0编辑  收藏  举报