第一章容器
条款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<char>在某些情况下可以替换string, vector在某些情况下可以替换标准关联容器。
如何选择容器?需要考虑一下几点:是否要求内存连续(随机访问)?是否有频繁的插入/删除?是否要求容器内元素有序?是否有查找速度的要求?本书作者认为没有一个默认容器。(而《c++ primer》认为如果没有非常正当的理由,就应该选vector)
条款2:小心对“容器无关代码”(container-independent code)的幻想
STL是建立在泛型的基础上,但由于不同容器的特性不同(尤其是迭代器、指针和引用的类型与失效规则不同),支持所有容器的相同接口是不存在的。比如:
- 只有序列容器支持: push_front和push_back,
- 只有关联容器支持: logN时间复杂度的lower_bound、upper_bound和equal_range;
class Widget { ... };
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
...
WCIterator i = find(cw.begin(), cw.end(), bestWidget);
同样,typedef可以用来简化一个经常被运用到的容器的定义,并减少维护的成本。但是typedef只是其他类型的同义字,
如果不想暴露所使用的容器类型,可以将所用的容器封装到一个class中。可以在这个class实现额外的功能,并对原始容器的操作进行封装。
条款3:使容器里对象的拷贝操作轻量而正确
容器内元素的改变、扩充或删除总是伴随着拷贝。如果将一个对象插入容器中,实际上是将这个对象拷贝进这个容器。拷贝是基于此对象对应class的拷贝构造函数和拷贝赋值操作符。
那么问题来了,如果对于某个class来说,他的拷贝非常昂贵,那么拷贝和可能成为容器的瓶颈。此外,拷贝还有可能带来分割的问题,若以基类建立容器,而插入派生类,那么派生部分会被切割。
解决这一问题的一个办法是建立指针的容器,尤其是智能指针的容器。
当然,STL容器在设计时,已经避免了绝大多数无谓的拷贝,相比内置数组,STL vector显然效率更高。
条款4:用empty来代替检查size()是否为0
对于一般的容器,例如vector,v.size()==0 与c.empty()等价。但是对于list来说,l.size()的时间复杂度为o(N)。
理论上,list每次插入元素时,可以跟新其size,这样能够保证l.size()的时间复杂度为o(1)。但list的特殊之处在于splice()成员函数,splice()可以将一个list插入到另一个list中,并且在o(1)内完成。如果需要在调用splice()时跟新size的值,唯一的方法是遍历待插入的list,并计算其长度。换而言之,l.size()和l.splice()之中,只能有一个为o(1)。而在大多数STL的实现中,前者时间复杂度为o(N),而后者时间复杂度为o(1)。
而对于list的empty()的实现,其源代码为