首先,你必須要明白的是,容器容納著許多個物件,但不是你傳給它的那些原始對象,而是對象的拷貝。
此外,當你從容器中擷取一個對象時,得到的不是容器裡的那個對象,而是對象的拷貝。同樣的,當你向容器中添加一個對象時(通過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容器使用了拷貝,但是別忘了一個事實:相比數組來說它們仍然是一個進步。