1 容器

第 1 条:慎重选择容器类型。

STL 容器不是简单的好,而是确实很好。

容器类型如下:

  • 标准 STL 序列容器:vector、string、deque、list。
  • 标准 STL 关联容器:set、multiset、map、multimap。
  • 非标准序列容器 slist 和 rope。slist 是一个单向链表,rope 本质上是一个 “重型” string。
  • 非标准的关联容器:hash_set、hash_multiset、hash_map、hash_multimap。
  • vector 作为 string 的替代
  • vector 作为标准关联容器的替代:有时 vector 在运行时间和空间上都要优于标准关联容器。
  • 几种标准的非 STL 容器:数组、bitset、valarray、stackqueuepriority_queue

选择标准:

  1. vector 应该是默认使用的序列类型;需要频繁地在序列中间做插入和删除操作时,使用 list;大多数插入和删除的操作发生在序列的头部和尾部时,使用 deque。

  2. 连续内存容器:也称为基于数组的容器(array-based container),特点是会把它的元素存放在一块或多块动态分配的内存中,每块内存中有多个元素

    插入或删除元素时,同一内存块中的所有元素要向前或向后移动,这样会影响效率和异常安全性。

    连续内存容器有 vector、string、deque、rope。

  3. 基于节点的容器:每一个动态分配的内存块中只存放一个元素。元素的插入和删除只影响到指向节点的指针,而不影响节点本身的内容。所以插入或删除操作时元素的值不需要移动。

    基于节点的容器有 list 和 slist。

  4. 如果容器内部使用了引用计数技术(reference counting),你是否介意?如果是,就要避免使用 string 和 rope(都使用了引用计数技术)。可以考虑 vector

  5. 你需要使迭代器、指针、引用变为无效的次数最少吗?基于节点的容器没有这个问题,针对连续内存容器的插入和删除操作一般会导致指向该容器的迭代器、指针和引用变为无效。

  6. 如果在容器上使用 swap,使得迭代器、指针或引用变为无效了,你会在意吗?如果在意,你要避免使用 string,因为 string 是 STL 中在 swap 过程中会导致迭代器、指针和引用变为无效的唯一容器

第 2 条:不要试图编写独立于容器类型的代码。

容器类型被泛化为序列容器和关联容器,类似的容器被赋予相似的功能:

  1. 标准的连续内存容器提供随机访问迭代器,而标准的基于节点的容器提供了双向迭代器。
  2. 序列容器支持 push_front 和/或 push_back 操作,而关联容器则不然。
  3. 关联容器提供了对数时间的 lower_bound、upper_bound、equal_range 成员函数,但序列容器却没有提供。

所以,试图编写对序列容器和关联容器都适用的代码是毫无意义的,因为很多成员函数仅当其容器为某一类型时才存在。这种限制的根源在于,对不同类型的序列容器,使迭代器、指针和引用无效(invalidate)的规则是不同的。不同的容器是不同的,它们有非常明显的优缺点。它们并不是被设计来交换使用的。

一种容器类型转换为另一种容器类型:typedef

class Widget{...};
typedef vector<Widget> WidgetContainer;
WidgetContainer cw;
Widget bestWidget;
...
WidgetContainer::iterator i = find(cw.begin(), cw.end(), bestWidget);

这样就使得改变容器类型要容易得多,尤其当这种改变仅仅是增加一个自定义得分配子时,就显得更为方便(这一改变不影响使迭代器/指针/引用无效的规则)。

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

  • 存在继承关系的情况下,拷贝动作会导致剥离(slicing):如果创建了一个存放基类对象的容器,却向其中插入派生类对象,那么在派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它所特有的部分(即派生类中的信息)将会丢失。

    vector<Widget> vw;
    class SpecialWidget:			// SpecialWidget 继承于上面的 Widget
    	public Widget{...};
    SpecialWidget sw;
    vw.push_back();					// sw 作为基类对象被拷贝进 vw 中
    								// 它的派生类特有部分在拷贝时被丢掉了
    
    • 剥离意味着向基类对象中的容器中插入派生类对象几乎总是错误的
    • 解决剥离问题的简单方法:使容器包含指针而不是对象。

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

  • empty 通常被实现为内联函数(inline function),并且它做的仅仅是返回 size 是否为 0.

  • empty 对所有标准容器都是常数时间操作,而对于一些 list 实现,size 耗费线性时间。

第 5 条:区间成员函数优先于与之对应的单元素成员函数。

小测验:给定 v1 和 v2 两个向量,使 v1 的内容和 v2 后半部分相同的最简单操作是什么?

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

  • TIP:assign 存在于所有的标准序列容器(vector,string,deque,list)中,当你需要完全替换一个容器的内容时,应该想到赋值(assignment)
  • 使用区间成员函数的原因:少写一些代码;得到意图清晰和更加直接的代码;更高的效率。

把一个 int 数组拷贝到一个 vector 的前端(P22):

int data[numValues];
vector<int> v;
...
v.insert(v.begin(), data, data + numValues);	// 使用区间形式的 insert

C++标准要求区间 insert 函数把现有容器中的元素直接移动到它们最终的位置上,即只需付出每个元素移动一次的代价。

支持区间的成员函数:

  • 区间创建:所有的标准容器都提供以下形式的构造函数:

    container::container(InputIterator begin,		// 区间开始
                        InputIterator end)			// 区间结束
    
  • 区间插入:所有的标准序列容器都提供以下形式的 insert:

    void container::insert(iterator position,		// 在何处插入区间
                          InputIterator begin,		// 区间开始
                          InputIterator end)		// 区间结束
    

    关联容器:void container::insert(InputIterator begin, InputIterator end);

  • 区间删除:所有的标准容器都提供了区间形式的删除(erase)操作。

    序列容器:iterator container::erase(iterator begin, iterator end);

    关联容器:void container::erase(iterator begin, iterator end);

第 6 条:当心 C++ 编译器最烦人的分析机制。

围绕参数名的括号会被忽略,而独立的括号则表明参数列表的存在(存在一个函数指针参数)。

C++中一条普遍规律:尽可能地解释为函数声明。

// 将一个含有整数的文件复制到一个list中。
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile),		// dataFile两边的括号会被忽略
              istream_iterator<int>());				// 函数指针
													// 上式声明了一个函数,而不是初始化对象!

第 7 条:如果容器中包含了通过 new 操作创建的指针,切记在容器对象析构前将指针 delete 掉。

void doSomething()
{
    vector<Widget*> vwp;
    for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
        vwp.push_back(new Widget);
    ...												// 使用 vwp  	
}													// 这里发生 Widget 泄露

当你使用指针的容器,而其中的指针应该被删除时,为了避免资源泄露,你必须或者用引用计数形式的智能指针对象(比如 Boost 的 shared_ptr)代替指针,或者当容器被析构时手工删除其中的每个指针。

使用 for_each 和 delete 函数对象来解决这个问题。

struct DeleteObject{							// 这里去掉了模板化和基类
    template<typename T>						// 在这里加入模板化
    void operator()(const T* ptr) const
    {
        delete ptr;
    }
}

void doSomething()
{
    deque<SpecialString*> dssp;
    for_each(dssp.begin(), dssp.end(),
            DeletObject());						// 确定的行为,但不是异常安全的
}

使用智能指针来解决这个问题。

void doSomething(){    typedef boost::shared_ptr<Widget> SPW;	// SPW = 指向 Widget 的 shared_ptr        vector<SPW> vwp;    for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)        vwp.push_back(SPW(new Widget));    ...												// 使用vwp 	}													// 这里不会有Widget泄露,即使上面的代码														有异常被抛出。

第 8 条:请勿创建包含 auto_ptr 的容器对象

auto_ptr 的容器(COAP)是被禁止的,试图使用它们的代码不会被编译通过。

复制一个 auto_ptr 时,它所指向的对象的所有权被移交到复制的 auto_ptr 上,而它自身被置为 NULL。复制一个 auto_ptr 意味着改变它的值

vector<auto_ptr<Widget> > widgets;sort(widgets.begin(), widgets.end(),    widgetAPcompare());			//	对vector所做的排序操作可能会改变它的内容!

第 9 条:慎重选择删除元素的方法

删除容器中有特定值的所有对象

对标准容器 Container<int>c; 删除其中所有值为 1963 的元素的方法。

erase-remove 习惯用法(连续内存容器 vector,deque,string):

c.erase(remove(c.begin(), c.end(), 1963), c.end());

list:c.remove(1963);

关联容器:c.erase(1963); 对数时间开销,基于等价而不是相等。注意关联容器没有名为 remove 的成员函数,使用任何名为 remove 的操作都是完全错误的。

删除容器中满足特定判别式(条件)的所有对象

删除使下面的判别式返回 true 的每一个对象

bool badValue(int );// 序列容器(vector,string,deque,list)c.erase(remove_if(c.begin(), c.end(), badValue), c.end());// listc.remove_if(badValue);

对于标准关联容器,则没有这么直截了当。

简单但效率稍低的办法:利用 remove_copy_if 把我们需要的值复制到一个新容器中,然后把原来容器的内和新容器的内容相互交换:

AssocContainer<int> c;...AssocContainer<int> goodValues;				// 保持不被删除的值的临时容器remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValues.end()), badValue);c.swap(goodValues);

高效方法:写一个循环遍历容器中的元素,并在遍历过程中删除元素。注意,对于关联容器(map,set,multimap,multiset),删除当前的 iterator,只会使当前的 iterator 失效

原因:关联容器的底层使用红黑树实现,插入、删除一个结点不会对其他结点造成影响。erase 只会使被删除元素的迭代器失效。关联容器的 erase 返回值为 void,可以使用 erase(iter++) 的方式删除迭代器。

AssocContainer<int> c;ofstream logFile;...for (AssocContainer<int>::iterator i = c.begin(); i != c.end(); /*什么也不做*/){    if (badValue(*i))     {        logFile << "Erasing " << *i << '\n';	// 写日志文件        c.erase(i++);							// 使用后缀递增删除元素,避免迭代器无效。    }    else ++i;}

对于序列式容器(vector,string,deque),删除当前的 iterator 会使后面所有元素的 iterator 都失效

原因: vector、string、deque 使用了连续分配的内存,删除一个元素会导致后面的所有元素都向前移动一个位置。所以不能使用 erase(iter++) 的方式,但可以使用 erase 方法,序列容器的 erase 可以返回下一个有效的 iterator

for (SeqContainer<int>::iterator i = c.begin(); i != c.end(); ){    if (badValue(*i))    {		logFile << "Erasing " << *i << '\n';        i = c.erase(i);						// 把erase的返回值赋给i,使i的值保持有效。    }    else ++i;}

第 10 条:了解分配子(allocator)的约定和限制

分配子最初是作为内存模型的抽象而产生的。

编写自定义的分配子,需要记住哪些内容:

  • 分配子是一个模板,模板参数 T 代表你为它分配内存的对象类型。
  • 提供类型定义 pointer 和 reference,但是始终让 pointer 为 T*,reference 为 T&。
  • 千万别让你的分配子拥有随对象而不同的状态(per-object state)。通常,分配子不应该有非静态的数据成员。
  • 传给分配子的 allocate 成员函数的是那些要求内存的对象的个数,而不是所需的字节数。同时要记住,这些函数返回 T* 指针(通过 pointer 类型定义),即使尚未有 T 对象被构造出来。
  • 一定要提供嵌套的 rebind 模板,因为标准容器依赖该模板。(P42)

第 11 条:理解自定义分配子的合理用法。

第 12 条:切勿对 STL 容器的线程安全性有不切实际的依赖。

STL 只支持以下多线程标准:

  • 多个线程读是安全的。
  • 多个线程对不同的容器做写入操作是安全的。

你不能指望 STL 库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。