首先,你必须要明白的是,容器容纳着许多对象,但不是你传给它的那些原始对象,而是对象的拷贝。
此外,当你从容器中获取一个对象时,得到的不是容器里的那个对象,而是对象的拷贝。同样的,当你向容器中添加一个对象时(通过insert或push_back),添加到容器的是你给的对象的拷贝。copy进去,copy出来,这就是STL的方式。所以,STL要求对象必须是可拷贝的。对象被存到容器里之后,对它的拷贝并不少见。如果你从vector、string或deque中插入或删除了元素,现有元素会移动(拷贝)。如果你使用了任何排序算法,一般的排序算法都要求交换,交换是需要拷贝的,这种例子很多。
你可能会对所有这些拷贝是怎么完成的感兴趣。这很简单,一个对象通过使用它的拷贝成员函数来拷贝,特别是它的拷贝构造函数和它的拷贝赋值构造函数。这就是传说中的BIG 3. 对于用户自定义类,比如Widget,这些函数传统上是这么声明的:
class Widget { public: ...
Widget();
Widget(const Widget&); // 拷贝构造函数
Widget& operator=(const Widget&); // 拷贝赋值操作符 ... };
如果你自己没有声明这些函数,你的编译器会在需要的时候为你生成它们。拷贝内建类型(比如int、指针等)也始终是通过简单地拷贝他们的二进制bit值来完成的。(有关拷贝构造函数和赋值操作符的详细情况,请参考任何C++的介绍性书籍。我想你推荐c++ programming和inside c++ project model.这两本书讲的很透彻)。
如果你用一个拷贝操作很昂贵的对象填充一个容器,那么一个简单的操作——把对象放进容器也会被证明为是一个性能瓶颈。容器中移动越多的东西,你就会在拷贝上浪费越多的内存和CPU时钟周期。此外,如果你定义了的有问题拷贝构造函数,这也会直接影响到容器。
当然由于继承的存在,拷贝会导致切割片(slicing)。那就是说,如果你以基类对象建立一个容器,而你试图插入派生类对象,那么当对象(通过基类的拷贝构造函数)拷入容器的时候对象的派生部分会被删除。
vector<Widget> vw; class SpecialWidget: // SpecialWidget从上面的Widget派生 public Widget {...}; SpecialWidget sw; vw.push_back(sw); // sw被当作基类对象拷入vw, 当拷贝时它的子类部分丢失了
切片问题暗示了把一个派生类对象插入基类对象的容器几乎总是错的。如果你希望结果对象表现为派生类对象,比如,调用派生类的虚函数等,总是错的。(关于slicing问题更多的背景知识,请参考《Effective C++》条款22。)
一个使拷贝更高效、正确而且避免分割问题的简单的方式是建立指针的容器而不是对象的容器。也就是说,不是建立一个Widget的容器,建立一个Widget*的容器。拷贝指针很快,而且不会有额外的开销(仅仅是简单的的二进制值的拷贝),而且当指针拷贝时不会产生slicing的问题。美中不足的是,指针的容器有带来了另外一个令人头疼的问题,你需要自己手动来删除这些指针。如果你不想手动来删除指针,在权衡效率、正确性和slicing这些因素时,智能指针会是一个不错的解决方案。
如果所有这些使STL的拷贝机制听起来很疯狂,就请重新想想。是,STL进行了大量拷贝,但它一般设计时,会尽量避免不必要的对象拷贝,实际上,它的实现也尽量避免不必要的对象拷贝。和C和C++内建容器的行为做个对比,下面的数组:
Widget w[maxNumWidgets]; // 建立一个大小为maxNumWidgets的Widgets数组, 默认构造每个元素
即使我们一般只使用其中的一些或者我们立刻使用从某个地方获取(比如,一个文件)的值覆盖每个默认构造的值,这也得构造maxNumWidgets个Widget对象。使用STL来代替数组,你可以使用一个可以在需要的时候增长的vector:
vector<Widget> vw; // 建立一个0个Widget对象的vector, 需要的时候可以扩展
我们也可以建立一个可以足够包含maxNumWidgets个Widget的空vector,但没有构造Widget:
vector<Widget> vw;
vw.reserve(maxNumWidgets);
和数组对比,STL容器更灵活。它们只建立(通过拷贝)你需要的个数的对象,而且它们只在你指定的时候做。是的,我们需要知道STL容器使用了拷贝,但是别忘了一个事实:相比数组来说它们仍然是一个进步。