条款3:使容器里对象的拷贝操作轻量而正确
容器可以存储对象。当向容器添加对象(insert或push_back),添加到容器的对象是指定对象的拷贝;同理,取出对象时也是通过拷贝。
存储在容器里的对象,还可能会对它拷贝。例如,从序列容器插入或删除元素,会引起元素的移动,移动是通过拷贝进行的(条款 14、15)。如果使用了排序算法(条款 31)next_permutation、previous_permutation、remove、unique等或同类算法(条款 32),rotate或reverse等,对象也会拷贝(移动)。
对象的拷贝是通过拷贝构造函数和赋值操作符完成。关于构造函数,可以参考这里。
因为会发生拷贝,如果这个拷贝过程比较“昂贵”,那么这可能会是性能的瓶颈。容器中的对象越多,那么就很可能在拷贝上消耗更大的代价。此外,还有一个非传统意义上的“拷贝”对象,把这样的对象放进容器会导致不幸(例子参考条款 8)。
因为继承的存在,拷贝时可能会发生分割。即,如果用基类对象建立容器,而插入派生类对象,这时通过基类的拷贝构造函数插入,派生类对象会被切割为基类对象。
vector<Widget> vw;
class SpecialWidget:public Widget {...}; // SpecialWidget从上面的Widget派生
SpecialWidget sw;
vw.push_back(sw); // sw被当作基类对象拷入vw
// 当拷贝时它的特殊部分丢失了
避免上面的问题的一个解决方法是建立指针容器,这样拷贝更快,且没有分割问题。但是指针容器本身也有问题(条款 7、33)。要避免这个问题的办法是建立智能指针的容器(条款 7)。
STL虽然进行了大量拷贝,但它设计为避免不必要的拷贝。和数组做个比较:
Widget w[maxNumWidgets]; // 建立一个大小为maxNumWidgets的Widgets数组
// 默认构造每个元素
上面数组中,每个数组对象都使用构造函数构造了;但有时我们不会使用全部数组对象,或使用前重新给它赋值,最开始的构造是没必要的。这时可以通过STL来代替:
vector<Widget> vw; // 建立一个0个Widget对象的vector
// 需要的时候可以扩展
STL中的vector是动态开辟内存,如果确定元素个数,可以给它预先分配内存
vector<Widget> vw;
vw.reserve(maxNumWidgets); // reserve的详细信息请参见条款14
条款4:用empty来代替检查size()是否为0
对于任意容器c
if(c.size()==0)
本质上等价于
if(c.empty())
empty的典型实现是一个返回size是否为0的内联函数。但首选应该是empty,因为对于所有标准容器,empty是一个常数时间操作,但对于list,size的花费为线性时间。
list之所以不能提供常数时间的size实现,是因为list特有的splice有很多要处理的东西。例如:
list<int> list1;
list<int> list2;
...
list1.splice( // 把list2中
list1.end(), list2, // 从第一次出现5到
find(list2.begin(), list2.end(), 5), // 最后一次出现10
find(list2.rbegin(), list2.rend(), 10).base() // 的所有节点移到list1的结尾。
); // 关于调用的
// "base()"的信息,请参见条款28
上面这段代码假设了list2在5后面有个10。执行完上面代码后,list1中有多少元素,在遍历find(list2.begin(), list2.end(), 5)和find(list2.rbegin(), list2.rend(), 10).base()之间有多少元素之前,无法得知list1中元素个数。
list中如果把size设计成常数时间操作,那么list成员函数在更新list时也要更新size的大小,包括splice。这时splice就是线性时间操作了。size和splice不能都是常数时间操作,必须有一个让步。
不同list实现用不同方式解决这个矛盾,依赖于它的作者是让size或splice的区间形势达到最高效率。即使在一个平台上,size是常数时间,但是代码可能还会移植到另一个平台,不同平台STL实现可能不同。
因此,用empty代替size()==0,是一个明智的选择。