《Effective STL》读书笔记之容器1-5

参考链接:http://blog.csdn.net/haotiangg/article/details/76266579


第一章 容器

第1条:慎重选择容器类型:选择合适的容器很关键!

C++中各种标准或非标容器:
标准STL序列容器:    vector、string、deque和list(双向列表)。
标准STL关联容器:    set、multiset、map和multimap。
非标准STL序列容器: slist(单向列表)和rope(重型字符串?)。
非标准STL关联容器: hash_set、hash_multiset、hash_map和hash_multimap。(c++11引入了unordered_set、unordered_multiset、unordered_map和unordered_multimap,其亦基于hash表,但属于最新的标准关联容器,所以相对hash_*拥有更高的效率和更好的安全性)
标准非STL容器:    数组(c++11中有新的array标准)、bitset、valarray(用于数值计算,但是一般很少使用)、stack、queue和priority_queue(常用于模拟最大堆和最小堆)
其他:    vector在某些情况下可以替换string, vector在某些情况下可以替换标准关联容器。

不要使用 vectorvector不是容器,至少,不是标准意义上的容器。C++标准对于vector值有其特殊的实现方法。目的是为了减小空间的耗用。特殊版本内部只使用一个bit来存储一个元素,所以通常要比一般的bool值小8倍之多。建议使用deque替代之。

选择参考:

  1. 如果需要在容器的任意位置插入新元素,就选择序列容器;关联容器是不行的。
  2. 是否关心容器中的元素是如何排序的?如果不关心,则hash容器是一个可以的选择;否则,你要避免hash容器。
  3. 选择容器必须是标准C++的一部分吗,如果必须是,就排除了hash容器(疑惑:我怎么记得hash容器已经成为标准了呢?)、slist和rope。
  4. 当发生元素的插入或删除操作时,若需要避免移动容器中原来的元素,就要避免连续内存的容器。vector
  5. 容器中数据的布局需要和C兼容,则只能选择vector。
  6. 元素的查找速度是否很关键,如果是,则考虑hash容器、排序的vector和标准关联容器。
  7. 如果容器内部使用了引用计数技术,你是否介意?如果是,就要避免使用string和rope。可以用vector 替代string。

面对不同的情形,选择合适的容器!


第二条:小心对“容器无关代码”(container-independent code)的幻想。

本条款要告诫程序员:编写与容器无关的代码是没有必要的。

STL是建立在泛型的基础上,但由于不同容器的特性不同(尤其是迭代器、指针和引用的类型与失效规则不同),支持所有容器的相同接口是不存在的。比如:
只有序列容器支持: push_front和push_back,
只有关联容器支持: logN时间复杂度的lower_bound、upper_bound和equal_range;
如果在工程中需要跟换容器类型,可以通过用typedef来减少跟换所带来的代码跟新量。

class Widget { ... };  
typedef vector WidgetContainer;  
typedef WidgetContainer::iterator WCIterator;  
WidgetContainer cw;  
Widget bestWidget;  
...  
WCIterator i = find(cw.begin(), cw.end(), bestWidget); 
同样,typedef可以用来简化一个经常被运用到的容器的定义,并减少维护的成本。但是typedef只是其他类型的同义字,
如果不想暴露所使用的容器类型,可以将所用的容器封装到一个class中。可以在这个class实现额外的功能,并对原始容器的操作进行封装。


第三条:确保容器中的对象拷贝正确而高效:

容器容纳了对象,但不是你给它们的那个对象。当你向容器中插入一个对象时,你插入的是该对象的拷贝而不是它本身;当你从容器中获取一个对象时,你获取的是容器中对象的拷贝。

拷贝对象是STL的基本工作方式。当你删除或者插入某个对象时,现有容器中的元素会移动(拷贝);当你使用了排序算法,remove、uniquer或者他们的同类,rotate或者reverse,对象会移动(拷贝)。

一个使拷贝更高效、正确的方式是建立指针的容器而不是对象的容器,即保存对象的指针而不是对象,然而,指针的容器有它们自己STL相关的头疼问题,改进的方法是采用智能指针


第四条:调用empty而不是检查size()是否为0

对于任意容器c,写下

if (c.size() == 0)…

本质上等价于写下
if (c.empty())…
但是为什么第一种方式比第二种优呢?理由很简单:对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。

这什么造成list这么麻烦?为什么不能也提供一个常数时间的size?如果size是一个常数时间操作,当进行增加/删除操作时每个list成员函数必须更新list的大小,也包括了splice,这会造成splice的效率降低(现在的splice是常量级的),反之,如果splice不必修改list大小,那么它就是常量级地,而size则变为线性复杂度,因此,设计者需要权衡这两个操作的算法:一个或者另一个可以是常数时间操作。


第五条:尽量使用区间成员函数代替单元素操作

给定两个vector,v1和v2,怎样使v1的内容和v2的后半部分一样?
可行的解决方案有:

(1)使用区间函数assign:

v1.assign(v2.begin() + v2.size() / 2, v2.end());

(2)使用单元素操作:
vector::const_iterator ci = v2.begin() + v2.size() / 2; 
ci != v2.end(); 
++ci) 
v1.push_back(*ci);

(3)使用copy区间函数:
v1.clear(); 
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));

(4)使用insert区间函数:
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());

最优的方案是assign方案,理由如下:

首先,使用区间函数的好处是:
● 一般来说使用区间成员函数可以输入更少的代码。
● 区间成员函数会导致代码更清晰更直接了当。

使用copy区间函数存在的问题是:
【1】 需要编写更多的代码,比如:v1.clear(),这个与insert区间函数类似
【2】 copy没有表现出循环,但是在copy中的确存在一个循环,这会降低性能

单元素变量操作指运用循环,将单个元素一一进行的操作,这类操作主要是插入(insert,push_front,push_back)和删除操作(erase)。值得注意的是如下copy操作虽然看起来是区间操作,其本质上还是单元素操作。

使用insert单元素版本的代码对你征收了三种不同的性能税,分别为:
【1】 没有必要的函数调用;
【2】 无效率地把v中的现有元素移动到它们最终插入后的位置的开销;
【3】 重复使用单元素插入而不是一个区间插入必须处理内存分配。

使用区间操作的好处是什么呢?以insert函数为例:一、减少函数调用,区间操作只需调用一次insert(),而单元素操作需要调用N次insert();二、对于内存连续型容器,每次插入时,需要移动后面的所有元素。所以区间操作只需移动一次,而单元素操作需要移动N次(list型不需要移动元素,但需要设置prev和next指针,也会造成类似的效率影响);三、对于vector和string,当所分配内存已满,需要重新分配内存并移动容器内已有元素。区间操作会实现计算所需内存,只需最多一次的内存重新分配和元素移动,而但单元素操作需要logN次内存重新分配和元素移动。

下面进行总结:
说明:参数类型iterator表示容器的迭代器类型,也就是container::iterator,参数类型InputIterator表示可以接受任何输入迭代器。

【1】区间构造
所有标准容器都提供这种形式的构造函数:

container::container(InputIterator begin, // 区间的起点 
InputIterator end); // 区间的终点

【2】区间插入
所有标准序列容器都提供这种形式的insert:
void container::insert(iterator position, // 区间插入的位置 
InputIterator begin, // 插入区间的起点 
InputIterator end); // 插入区间的终点

关联容器使用它们的比较函数来决定元素要放在哪里,所以它们了省略position参数。
void container::insert(lnputIterator begin, InputIterator end);

【3】区间删除
每个标准容器都提供了一个区间形式的erase,但是序列和关联容器的返回类型不同。序列容器提供了这个:
iterator container::erase(iterator begin, iterator end);

而关联容器提供这个:
void container::erase(iterator begin, iterator end);

为什么不同?解释是如果erase的关联容器版本返回一个迭代器(被删除的那个元素的下一个)会招致一个无法接受的性能下降.

【4】区间赋值
所有标准列容器都提供了区间形式的assign:

void container::assign(InputIterator begin, InputIterator end);



你可能感兴趣的:(Effective,STL读书笔记,STL,C,容器,vector)