《C++Primer》第九章-順序容器-學習筆記(2)-自增長&容器選用

《C++Primer》第九章-順序容器-學習筆記(2)

日誌:
1,2020-03-12 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)

vector 容器的自增長

在容器對象中 insert 或壓入一個元素時,該對象的大小增加 1。類似地,如果 resize 容器以擴充其容量,則必須在容器中添加額外的元素。標準庫處理存儲這些新元素的內存分配問題。
一般來說,我們不應該關心標準庫類型是如何實現的:我們只需要關心如何使用這些標準庫類型就可以了。然而,對於 vector 容器,有一些實現也與其接口相關。爲了支持快速的隨機訪問,vector 容器的元素以連續的方式存放——每一個元素都緊挨着前一個元素存儲
已知元素是連續存儲的,當我們在容器內添加一個元素時,想想會發生什麼事情:如果容器中已經沒有空間容納新的元素,此時,由於元素必須連續存儲以便索引訪問,所以不能在內存中隨便找個地方存儲這個新元素。於是,vector 必須重新分配存儲空間,用來存放原來的元素以及新添加的元素:存放在舊存儲空間中的元素被複制到新存儲空間裏,接着插入新元素,最後撤銷舊的存儲空間。
如果 vector 容器在在每次添加新元素時,都要這麼分配和撤銷內存空間,其性能將會非常慢,簡直無法接受。
對於不連續存儲元素的容器,不存在這樣的內存分配問題。例如,在 list 容器中添加一個元素,標準庫只需創建一個新元素,然後將該新元素連接在已存在的鏈表中,不需要重新分配存儲空間,也不必複製任何已存在的元素。由此可以推論:

一般而言,使用 list 容器優於 vector 容器。

但是,通常出現的反而是以下情況:

對於大部分應用,使用 vector 容器是最好的。原因在於,標準庫的實現者使用這樣內存分配策略:以最小的代價連續存儲元素。由此而帶來的訪問元素的便利彌補了其存儲代價

爲了使 vector 容器實現快速的內存分配,其實際分配的容量要比當前所需的空間多一些。vector 容器預留了這些額外的存儲區,用於存放新添加的元素。
於是,不必爲每個新元素重新分配容器。所分配的額外內存容量的確切數目因庫的實現不同而不同。比起每添加一個新元素就必須重新分配一次容器,這個分配策略帶來顯著的效率。事實上,其性能非常好,因此在實際應用中,比起 list 和deque 容器,vector 的增長效率通常會更高。

capacity 和 reserve 成員

vector 容器處理內存分配的細節是其實現的一部分。然而,該實現部分是由 vector 的接口支持的。vector 類提供了兩個成員函數:capacityreserve使程序員可與 vector 容器內存分配的實現部分交互工作。capacity操作獲取在容器需要分配更多的存儲空間之前能夠存儲的元素總數,而 reserve
操作則告訴 vector 容器應該預留多少個元素的存儲空間。
弄清楚容器的capacity(容量)size(長度)的區別非常重要。size 指容器當前擁有的元素個數;而 capacity 則指容器在必須分配新存儲空間之前可以存儲的元素總數
爲了說明 size 和 capacity 的交互作用,考慮下面的程序:

vector<int> ivec;
// size should be zero; capacity is implementation defined
cout << "ivec: size: " << ivec.size()<< " capacity: " << ivec.capacity() << endl;
// give ivec 24 elements
for (vector<int>::size_type ix = 0; ix != 24; ++ix)
	ivec.push_back(ix);
// size should be 24; capacity will be >= 24 and is implementationdefined
cout << "ivec: size: " << ivec.size()<< " capacity: " << ivec.capacity() << endl;

在我們的系統上運行該程序時,得到以下輸出結果:

ivec: size: 0 capacity: 0
ivec: size: 24 capacity: 32

由此可見,空 vector 容器的 size 是 0,而標準庫顯然將其 capacity 也設置爲 0。當程序員在 vector 中插入元素時,容器的 size 就是所添加的元素個數,而其 capacity 則必須至少等於 size,但通常比 size 值更大。在上述程序中,一次添加一個元素,共添加了 24 個元素,結果其 capacity 爲 32。 容
器的當前狀態如下圖所示:
在這裏插入圖片描述
現在,可如下預留額外的存儲空間:

ivec.reserve(50); // sets capacity to at least 50; might be more
// size should be 24; capacity will be >= 50 and is implementation defined
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

正如下面的輸出結果所示,該操作保改變了容器的 capacity,而其 size 不變:

ivec: size: 24 capacity: 50

下面的程序將預留的容量用完:

// add elements to use up the excess capacity
while (ivec.size() != ivec.capacity())
	ivec.push_back(0);
// size should be 50; capacity should be unchanged
cout << "ivec: size: " << ivec.size() << " capacity: " << ivec.capacity() << endl;

由於在該程序中,只使用了預留的容量,因此 vector 不必做任何的內存分配工作。事實上,只要有剩餘的容量,vector 就不必爲其元素重新分配存儲空間。
其輸出結果表明:此時我們已經耗盡了預留的容量,該容器的 size 和capacity 值相等:

ivec: size: 50 capacity: 50

此時,如果要添加新的元素,vector 必須爲自己重新分配存儲空間:

ivec.push_back(42); // add one more element
// size should be 51; capacity will be >= 51 and is implementationdefined
cout << "ivec: size: " << ivec.size()
<< " capacity: " << ivec.capacity() << endl;

這段程序的輸出:

ivec: size: 51 capacity: 100

表明:每當 vector 容器不得不分配新的存儲空間時,以加倍當前容量的分配策略實現重新分配。
vector 的每種實現都可自由地選擇自己的內存分配策略。然而,它們都必須提供 vector 和 capacity 函數,而且必須是到必要時才分配新的內存空間。分配多少內存取決於其實現方式。不同的庫採用不同的策略實現。
此外,每種實現都要求遵循以下原則:確保 push_back 操作高效地在vector 中添加元素。從技術上來說,在原來爲空的 vector 容器上 n 次調用push_back 函數,從而創建擁有 n 個元素的 vector 容器,其執行時間永遠不能超過 n 的常量倍。

容器的選用

在前面的章節中可見,分配連續存儲元素的內存空間會影響內存分配策略和容器對象的開銷。通過巧妙的實現技巧,標準庫的實現者已經最小化了內存分配的開銷。元素是否連續存儲還會顯著地影響

  • 在容器的中間位置添加或刪除元素的代價。
  • 執行容器元素的隨機訪問的代價。

程序使用這些操作的程序將決定應該選擇哪種類型的容器。vector 和 deque容器提供了對元素的快速隨機訪問,但付出的代價是,在容器的任意位置插入或刪除元素,比在容器尾部插入和刪除的開銷更大。list 類型在任何位置都能快速插入和刪除,但付出的代價是元素的隨機訪問開銷較大。

插入操作如何影響容器的選擇

list 容器表示不連續的內存區域,允許向前和向後逐個遍歷元素。在任何位置都可高效地inserterase 一個元素。插入或刪除 list 容器中的一個元素不需要移動任何其他元素。另一方面,list 容器不支持隨機訪問,訪問某個元素要求遍歷涉及的其他元素。(list 容器將新元素連接在已存在的鏈表中)

對於 vector 容器除了vector 容器尾部外,其他任何位置上的插入(或刪除)操作都要求移動被插入(或刪除)元素右邊所有的元素。例如,假設有一個擁有 50個元素的 vector 容器,我們希望刪除其中的第 23 號元素,則 23 號元素後面的所有元素都必須向前移動一個位置。否則, vector 容器上將會留下一個空位(hole),而 vector 容器的元素就不再是連續存放的了。

deque 容器擁有更加複雜的數據結構。從 deque 隊列的兩端插入和刪除元素都非常快。在容器中間插入或刪除付出的代價將更高。 deque 容器同時提供了list 和 vector 的一些性質:

  • 與 vector 容器一樣,在 deque 容器的中間 insert 或 erase 元素效率比較低。
  • 不同於 vector 容器,deque 容器提供高效地在其首部實現 insert 和erase 的操作,就像在容器尾部的一樣。
  • 與 vector 容器一樣而不同於 list 容器的是, deque 容器支持對所有元素的隨機訪問
  • 在 deque 容器首部或尾部插入元素不會使任何迭代器失效,而首部或尾部刪除元素則只會使指向被刪除元素的迭代器失效。在 deque 容器的任何其他位置的插入和刪除操作將使指向該容器元素的所有迭代器都失效

元素的訪問如何影響容器的選擇

vector 和 deque 容器都支持對其元素實現高效的隨機訪問。也就是說,我們可以高效地先訪問 5 號元素,然後訪問 15 號元素,接着訪問 7 號元素,等等。 由於 vector 容器的每次訪問都是距離其起點的固定偏移,因此其隨機訪問非常有效率。在 list 容器中,上述跳躍訪問會變得慢很多。在 list 容器的元素之間移動的唯一方法是順序跟隨指針。從 5 號元素移動到 15 號元素必須遍歷它們之間所有的元素。通常來說,除非找到選擇使用其他容器的更好理由,否則vector 容器都是最佳選擇。

選擇容器的提示

下面列舉了一些選擇容器類型的法則:

  1. 如果程序要求隨機訪問元素,則應使用vector 或 deque 容器

  2. 如果程序必須在容器的中間位置插入或刪除元素,則應採用list 容器

  3. 如果程序不是在容器的中間位置,而是在容器首部或尾部插入或刪除元素,則應採用 deque 容器

  4. 如果只需在讀取輸入時在容器的中間位置插入元素,然後需要隨機訪問元素,則可考慮在輸入時將元素讀入到一個 list 容器,接着對此容器重新排序,使其適合順序訪問,然後將排序後的 list 容器複製到一個 vector容器。

  5. 如果程序既需要隨機訪問又必須在容器的中間位置插入或刪除元素,那應該怎麼辦呢?
    此時,選擇何種容器取決於下面兩種操作付出的相對代價:隨機訪問 list 容器元素的代價,以及在 vector 或 deque 容器中插入/刪除元素時複製元素的代價。通常來說,應用中佔優勢的操作(程序中更多使用的是訪問操作還是插入/刪除操作)將決定應該什麼類型的容器

決定使用哪種容器可能要求剖析各種容器類型完成應用所要求的各類操作的 性能。

如果無法確定某種應用應該採用哪種容器,則編寫代碼時嘗試 只使用 vector 和 lists 容器都提供的操作:使用迭代器,而不是下標,並且避免隨機訪問元素。這樣編寫,在必要時,可很方便地將程序從使用 vector 容器修改爲使用 list 的容 器。

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章