STL學習筆記之容器篇

容器


條款1:仔細選擇你的容器

C++提供了很多可供程序員使用的容器:
(1) 標準STL序列容器:vector,string,deque和list
(2) 標準STL關聯容器:set,multiset,map和multimap
(5) vector<char>可以作為string的替代品
(6) vector作為標準關聯容器的替代品
(7) 幾種標準非STL容器,包括數組、bitset、valarray、stack、queue和priority_queue
又新增了一些容器,包括:array,unordered容器,還有tuple容器。

不同容器有不同的優缺點,用戶需要根據實際應用的特點綜合決定使用哪種容器,如:vector是一種可以默認使用的序列類型,當很頻繁地對序列中部進行插入和刪除時應該用list,當大部分插入和刪除發生在序列的頭或尾時可以選擇deque這種數據結構


條款2:小心對“容器無關代碼”的幻想

本條款要告誡程序員:編寫與容器無關的代碼是沒有必要的
有人想編寫這樣的程序,剛開始時使用vector存儲,之后由于需求的變化,將vector改為deque或者list,其他代碼不變。實際上,這基本上是做不到的。這是因為:不同的序列容器所對應了不同的迭代器、指針和引用的失效規則,此外,不同的容器支持的操作也不相同,如:vector支持reserve()和capacity(),而deque和list不支持;即使是相同的操作,復雜度也不一樣(如:insert),這會讓你的系統產生意想不到的瓶頸

此外,鼓勵程序員在聲明容器和迭代器的時候使用typedef進行重命名,這能夠對你的程序進行封轉,從而使用起來更簡單,如有下面一個map容器:

map<string,vectorWidget>::iterator,CIStringCompare>;

如要用const_iterator遍歷這個map,你需不止一次地寫下:

map<string, vectorWidget>::iterator, CIStringCompare>::const_iterator

如果使用typedef,會快速方便很多。


條款3:使容器里對象的拷貝操作輕量而正確

容器容納了對象,但不是你給它們的那個對象。當你向容器中插入一個對象時,你插入的是該對象的拷貝而不是它本身;當你從容器中獲取一個對象時,你獲取的是容器中對象的拷貝
拷貝是STL的基本工作方式。當你刪除或者插入某個對象時,現有容器中的元素會移動(拷s貝);當你使用了排序算法,remove、uniquer或者他們的同類,rotate或者reverse,對象會移動(拷貝)。
一個使拷貝更高效、正確的方式是建立指針的容器而不是對象的容器,即保存對象的指針而不是對象,然而,指針的容器有它們自己STL相關的頭疼問題,改進的方法是采用智能指針


條款4:用empty來代替檢查size()是否為0

對于任意容器c,寫下

if (c.size() == 0)

本質上等價于寫下

if (c.empty())

但是為什么第一種方式比第二種優呢?理由很簡單:對于所有的標準容器,empty是一個常數時間的操作,但對于一些list實現,size花費線性時間
這什么造成list這么麻煩?為什么不能也提供一個常數時間的size?如果size是一個常數時間操作,當進行增加/刪除操作時每個list成員函數必須更新list的大小,也包括了splice,這會造成splice的效率降低(現在的splice是常量級的),反之,如果splice不必修改list大小,那么它就是常量級地,而size則變為線性復雜度,因此,設計者需要權衡這兩個操作的算法:一個或者另一個可以是常數時間操作。


條款5:盡量使用區間成員函數代替單元素操作

給定兩個vector,v1和v2,怎樣使v1的內容和v2的后半部分一樣?
可行的解決方案有:
(1)使用區間函數assign:

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

(2)使用單元素操作:

vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2;
ci != v2.end();
++ci) 
v1.push_back(*ci);

(3)使用copy區間函數

v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));

(4)使用insert區間函數

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

最優的方案是assign方案,理由如下:
首先,使用區間函數的好處是:
● 一般來說使用區間成員函數可以輸入更少的代碼。
● 區間成員函數會導致代碼更清晰更直接了當。
使用copy區間函數存在的問題是:
【1】 需要編寫更多的代碼,比如:v1.clear(),這個與insert區間函數類似
【2】 copy沒有表現出循環,但是在copy中的確存在一個循環,這會降低性能
使用insert單元素版本的代碼對你征收了三種不同的性能稅,分別為:
【1】 沒有必要的函數調用;
【2】 無效率地把v中的現有元素移動到它們最終插入后的位置的開銷
【3】 重復使用單元素插入而不是一個區間插入必須處理內存分配

下面進行總結:
說明:參數類型iterator表示容器的迭代器類型,也就是container::iterator,參數類型InputIterator表示可以接受任何輸入迭代器。
【1】 區間構造
所有標準容器都提供這種形式的構造函數:

container::container(InputIterator begin,
// 區間的起點
InputIterator end);
// 區間的終點

【2】區間插入
所有標準序列容器都提供這種形式的insert:

void container::insert(iterator position,
// 區間插入的位置
InputIterator begin,
// 插入區間的起點 
InputIterator end);
// 插入區間的終點

關聯容器使用它們的比較函數來決定元素要放在哪里,所以它們了省略position參數。

void container::insert(lnputIterator begin, InputIterator end);

【3】區間刪除
每個標準容器都提供了一個區間形式的erase,但是序列和關聯容器的返回類型不同。序列容器提供了這個:

iterator container::erase(iterator begin, iterator end);

而關聯容器提供這個:

void
container::erase(iterator begin, iterator end);

為什么不同?解釋是如果erase的關聯容器版本返回一個迭代器(被刪除的那個元素的下一個)會招致一個無法接受的性能下降.
注意這個特點在新版本的STL中已經解決了

【4】 區間賦值
所有標準列容器都提供了區間形式的assign:

void
container::assign(InputIterator begin, InputIterator end);

條款7:當使用new得指針的容器時,記得在銷毀容器前delete那些指針

條款8:永不建立auto_ptr的容器**//注意,在新版本中auto_ptr已經不再被使用

條款9:在刪除選項中仔細選擇

(1)假定你有一個標準STL容器,c,容納int,

Container<int> c;

而你想把c中所有值為1963的對象都去,則不同的容器類型采用的方法不同:沒有一種是通用的.

  • 如果采用連續內存容器(vector、queue和string),最好的方法是erase-remove慣用法:
c.erase(remove(c.begin(), c.end(), 1963),c.end());
// 當c是vector、string
// 或deque時,erase-remove慣用法是去除特定值的元素的最佳方法
  • 對于list,最有效的方法是直接使用remove函數:
c.remove(1963);
  • 對于關聯容器,解決問題的適當方法是調用erase:
c.erase(1963);
// 當c是標準關聯容器時,erase成員函數是去除特定值的元素的最佳方法

(2)讓我們換一下問題:不是從c中除去每個有特定值的元素,而是消除下面判斷式返回真的每個對象:

bool badValue( int x);
// 返回x是否是“bad”
  • 對于序列容器(vector、list、deque和string),只需要將remove換成remove_if即可:
// 當c是vector、string
// 或deque時這是去掉badValue返回真的對象的最佳方法
c.erase(remove_if(c.begin(), c.end(), badValue),c.end());

// 當c是list時這是去掉badValue返回真的對象的最佳方法
c.remove_if(badValue);
  • 對于關聯容器,有兩種方法處理該問題,一個更容易編碼,另一個更高效。“更容
    易但效率較低”的解決方案用remove_copy_if把我們需要的值拷貝到一個新容器中,然后把原容器的內容和新的交換:
    “更高效”的解決方案是直接從原容器刪除元素。不過,因為關聯容器沒有提供類似remove_if的成員函數,所以我們必須寫一個循環來迭代c中的元素,和原來一樣刪除元素:
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin();i != c.end();){
    if(badValue(*i))
         c.erase(i++);
    else
         ++i;
}
  • 對于vector、string、list和deque,必須利用erase的返回值。那個返回值正是我們需要的:一旦刪除完成,它就是指向緊接在被刪元素之后的元素的有效迭代器。換句話說,我們這么寫:
for(SeqContainer<int>::iterator i = c.begin(); i != c.end();){
    if(badValue(*i)){
        i = c.erase(i);
      // 通過把erase的返回值賦給i來保持i有效
    } 
   else
       ++i;
}
條款12:對STL容器線程安全性的期待現實一些

在工程中多線程操作STL的場景應該還是比較常見的,一個典型的例子就是用其來做生產者——消費者模型的隊列或者其他共享隊列,這樣為了應對線程安全問題我們必須自己對容器操作進行封裝。這是我自己實現的的封裝類:

template< typename Container>    
// 獲取和釋放容器的互斥量的類的模板核心;
class Lock {                    
    public :                         
    // 忽略了很多細節
       Lock( const Containers container) : c(container){  
          // 在構造函數獲取互斥量       
          getMutexFor(c); 
       }
      ~Lock(){
           // 在析構函數里釋放它
           releaseMutexFor(c); 
       }
  private:
      const Container& c;
};

原創文章,轉載請注明: 轉載自董的博客
本文鏈接地址: http://dongxicheng.org/cpp/effective-stl-part1/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容