1. 程式人生 > >C++ 容器(一):順序容器簡介

C++ 容器(一):順序容器簡介

C++提供了使用抽象進行高效程式設計的方式,標準庫中定義了許多容器類以及一系列泛型函式,使程式設計師可以更加簡潔、抽象和有效地編寫程式,其中包括:順序容器,關聯容器和泛型演算法。本文將簡介順序容器(vectorlistdeque)的相關內容。

1.順序容器的概念

標準庫vector型別,就是一種最常見的順序容器,它將單一型別元素聚集起來成為容器,然後根據位置來儲存和訪問這些元素,這就是順序容器。順序容器的元素排列順序與其值無關,而僅僅由元素新增到容器裡的次序決定。

標準庫定義了三種順序容器:vectorlistdeque。它們的區別在於訪問元素的方式,以及新增或刪除元素相關操作的執行代價。如下表:

順序容器功能
vector支援快速隨機訪問
list支援快速插入/刪除
deque雙端佇列

(1) 標頭檔案 
為了定義一個容器型別的物件,必須先包含相關的標頭檔案:

#include <vector>  // vector
#include <list>    // list
#include <deque>   // deque
  • 1
  • 2
  • 3

(2) 定義 
所有容器都是類模版,要定義某種特殊的容器,必須在容器名後加一對尖括號,裡面提供存放元素的型別:

vector<string> sVec; // empty vector that can hold strings
list<int
>
iList; // empty list that can hold ints deque<float> fDeque; // empty deque that can holds floats
  • 1
  • 2
  • 3

(3)初始化 
容器的建構函式:

建構函式含義
C<T> c建立一個名為c的空容器,C是容器型別名,如vectorT是元素型別,如intstring。適用於所有容器
C c(c2)建立容器c2的副本cc2c必須具有相同的容器型別,並存放相同型別的元素。適用於所有容器
C c(n)建立有n個初始化元素的容器c。只適用順序容器
C c(n, t)使用n個為t的元素建立容器c
,其中值t必須是容器型別C的元素型別的值,或者是可以轉換為該型別的值。只適用順序容器
C c(b, e)建立容器c,其中元素是迭代器be標示的範圍內元素的副本。適用於所有容器

注意: 所有的容器型別都定了預設建構函式,用於創建制定型別的空容器物件。預設建構函式不帶引數。為了使程式更加清晰、簡短,容器型別最常用的建構函式時預設建構函式。在大多數的程式中,使用預設建構函式能達到最佳執行效能,並且使容器更容易使用。

  • 將一個容器初始化為另一個容器的副本
vector<int> iVec;
vector<int> iVec2(iVec);   // ok
vector<double> dVec(iVec); // error, iVec holds int not double
list<int> iList(iVec);     // error, iVec is not list<int>
  • 1
  • 2
  • 3
  • 4

注意:講一個容器複製給另一個容器時,必須型別匹配(容器的型別和元素的型別都必須相同)。

  • 初始化為一段元素的副本 
    通過使用迭代器,間接實現將一種容器內的元素複製給另一種容器。使用迭代器時,不要求容器型別相同,容器內的元素型別也可以不相同,只要它們相互相容,能夠將要複製的元素轉換為所構建的新容器的元素型別,即可實現複製。
vector<string> sVec;
// initialize sList with copy of each element of sVec
list<string> sList(sVec.begin(), sVec.end());
// calculate the midpoint in the vector
vector<string>::iterator mid = sVec.begin() + sVec.size() / 2;
// initialize front with first half of sVec: the elements up to but not including *mid
vector<string> front(sVec.begin(), mid);

// also can initialize with a pointer
char* words[] = {"first", "second", "third", "forth"};
int sizeOfWords = sizeof(words) / (sizeof(char*));
vector<string> word2(words, words + sizeOfWords);

// cout
for ( int idx=0; idx<sizeOfWords; idx ++ )
    cout << word2[idx] << endl;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 分配和初始化指定數目的元素 
    建立順序容器時,可顯式地指定容器大小和一個(可選的)元素初始化式。容器的大小可以是常量或者非常量表達式,元素初始化式必須是可用於初始化其元素型別物件的值:
const list<int>::size_type listSize = 64; // also can be: int listSize = 64
list<std::string> lstr(listSize, "str");  // 64 strings, each is str

vector<int> iVec(listSize); // 64 ints, each initialized to 0 
  • 1
  • 2
  • 3
  • 4

(4)容器內元素的型別約束 
C++語言中,大多數型別都可用作容器的元素型別。容器元素型別必須滿足最基本的兩個約束:

  • 元素型別必須支援賦值運算;
  • 元素型別的物件必須可以複製。

除此外,一些容器操作對元素型別還有特殊要求。如果元素型別不支援這些要求,則相關的容器操作就不能執行:我們可以定義該型別的容器,但不能使用某些特定的操作。

另外,舊版C++標準中,指定容器作為容器型別時,必須使用如下空格:

vector<vector<int> > myVec;  // the space required between close >
  • 1

而在新版標準中,並無要求:

vector<vector<int> > myVec; // ok
vector<vector<int>> myVec;  // ok
  • 1
  • 2

2.順序容器的操作

每種順序容器都提供了一組有用的型別定義以及以下操作:

  • 在容器中新增元素;
  • 在容器中刪除元素;
  • 設定容器大小;
  • (如果有的話)獲取容器內的第一個和最後一個元素

(1)容器定義的類型別名

類型別名含義
size_type無符號整型,足以儲存此容器型別的最大可能容器長度
iterator此容器型別的迭代器型別
const_iterator元素只讀迭代器型別
reverse_iterator按逆序定址元素的迭代器型別
const_reverse_iterator元素只讀逆序迭代器型別
difference_type足夠儲存兩個迭代器差值的有符號整型,可為負數
value_type元素型別
reference元素的左值型別,是value_type&的同義詞
const_value_type元素的常量左值型別,等效於const value_type&

例如:

// iter is the iterator type defined by vector<string>
vector<string>::iterator iter;

// cnt is the difference_type type defined by vector<int> 
vector<int>::difference_type cnt;
  • 1
  • 2
  • 3
  • 4
  • 5

(2)容器內元素操作

  • beginend成員
操作功能
c.begin()返回一個迭代器,指向容器c的第一個元素
c.end()返回一個迭代去,指向容器c的最後一個元素的下一個位置
c.rbegin()返回一個逆序迭代器,指向容器c的最後一個元素**
c.rend()返回一個逆序迭代器,指向容器c的第一個元素前面的位置

注意: 
(a) 迭代器範圍是左閉右開區間,標準表達方式為:

// includes the first and each element up to but not including last
[first, lase)
  • 1
  • 2

(b) 容器元素都是副本。在容器中新增元素時,系統是將元素值複製到容器裡,被複制的原始值與新容器中的元素互不相關,此後,容器內元素值發生變化時,被複制的原值不會收到影響,反之亦然。

(c) 不要儲存end操作返回的迭代器。新增或者刪除vectordeque容器內的元素都會導致迭代器失效。

vector<int> v(42);
// cache begin and end iterator
vector<int>::iterator first = v.begin(), last = v.end();

while( first!= last ) // disaster: this loop is undefined
{
    // insert new value and reassign first, which otherwise would be invalid
    first = v.insert(++first, 2);
    ++ first; // advance first just past the element we added
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 新增元素
操作功能
c.push_back(t)在容器c的尾部新增值為t的元素,返回void型別
c.push_front(t)在容器c的前端新增值為t的元素,返回void型別(只適用於listdeque容器型別)
c.insert(p, t)在迭代器p所指向的元素前面插入值為t的新元素,返回指向新新增元素的迭代器
c.insert(p, n, t)在迭代器p所指向的元素前面新增插入n個值為t的新元素,返回void型別
c.insert(p, b, e)在迭代器p所指向元素前面插入由迭代器bc標記範圍的元素,返回void型別
// add elements at the end of vector
vector<int> iVec;
for ( int idx=0; idx<4; ++ idx )
{
    iVec.push_back( idx );
}

// insert an element
vector<string> sVec;
string str("Insert");
// warning: inserting anywhere but at the end of a vector is an expensive operation
sVec.insert(sVec.begin(), str);

// insert some elements
sVec.insert(sVec.begin(), 10, "Anna");
string array[4] =  {"first", "second", "third", "forth"};
sVec.insert(sVec.end(), array, array+4);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 容器大小的操作
操作功能
c.size()返回容器c中元素個數,返回型別為c::size_type
c.max_size()返回容器c可容納的最多元素個數,返回型別為c::size_type
c.empty()返回標記容器大小是否為0的布林值
c.resize(n)調整容器c的長度大小,使其能容納n個元素。如果n<c.size(),則刪除多餘的元素,否則,新增採用值初始化的新元素
c.resize(n, t)調整容器c的大小,使其能包納n個元素,所有元素的值都為t
vector<int> iVec(10, 1); // 10 ints, each has value 1
iVec.resize(15);     // adds 5 elements of value 0 to back of iVec
iVec.resize(25, -1); // adds 10 elements of value -1 to back of iVec
iVec.resize(5);      // erases 20 elements from the back of iVec
  • 1
  • 2
  • 3
  • 4

注意:resize操作可能會使迭代器失效。在vectordeque容器上做resize操作可能使其所有迭代器都失效。對於所有容器型別,如果resize操作壓縮了容器,則指向已刪除的元素的迭代器失效。

  • 訪問元素
操作功能
c.back()返回容器c的最後一個元素的引用,如果c為空,則該操作未定義
c.front()返回容器c的第一個元素的引用,如果c為空,則該操作未定義
c[n]返回下標為n的元素的引用,如果n<0n>c.size(),則該操作未定義(只適用於vectordeque容器)
c.at(n)返回下標為n的元素的引用。如果下標越界,則該操作未定義(只適用於vectordeque容器)

注意:使用越界的下標,或呼叫空容器的frontback函式,都會導致程式出現 嚴重的錯誤。

  • 刪除元素:與插入元素對應容器型別提供了刪除容器內元素的操作。
操作功能
c.erase(p)刪除迭代器p所指向的元素,返回一個迭代器,它指向被刪除元素後面的元素。如果p指向容器內的最後一個元素,則返回的迭代器指向容器的超出末端的下一位置,如果p本身就是指向超出末端的下一位置的迭代器,則該函式未定義
c.erase(b, e)刪除迭代器be標記的範圍內的所有元素。返回一個迭代器,它指向被刪除元素段後面的元素。如果e本身就是指向超出末端的下一位置的迭代器,則返回的迭代器也指向容器末端的下一位置
c.clear()刪除容器c內的所有元素,返回void
c.pop_back()刪除容器c的最後一個元素,返回void。如果c為空容器,則該操作未定義
c.pop_font()刪除容器c的第一個元素,返回void。如果c為空容器,則該操作未定義

注意: 
(a) pop_front操作通常與front操作配套使用,實現棧(先進先出)的方式處理:

while ( !vec.empty() )
{
    // do something with the current top of vec
    process(vec.front()); 
    // remove first element
    vec.pop_front();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(b)刪除一個或一段元素更通用的方法是erase操作。erase操作不會檢查它的引數,使用時必須確保用作引數的迭代器或迭代器範圍是有效的。

(c) 尋找一個指定元素的最簡單的方法是使用標準庫的find演算法(程式設計時需要新增標頭檔案#include <algorithm>)。find函式需要一對標記查詢範圍的迭代器以及一個在該範圍內查詢的值作為引數。查詢完成後,返回一個迭代器,它指向具有指定值的第一個元素或超出末端的下一位置。

string searchValue("find");
vector<std::string> vec(1, "find");
vector<string>::iterator iter = std::find(vec.begin(), vec.end(), searchValue);
if ( iter!= vec.end() )
    cout << *iter << endl;
  • 1
  • 2
  • 3
  • 4
  • 5

(d) 刪除所有元素,可以用clear或將beginend迭代器傳遞給erase函式。

vec.clear(); // delete all the elements within the container
vec.erase(vec.begin(), vec.end()); // equivalent
  • 1
  • 2
  • 賦值與swap

賦值操作中,首先刪除其左運算元容器內的所有元素,然後將右運算元容器中的所有容器插入到左邊容器中:

vec1 = vec2; // replace contents of vec1 with a copy of elements in vec2
// equivalent operation using erase and insert
vec1.erase(vec1.begin(), vec1.end()); // delete all elements in vec1
vec1.insert(vec2.begin(), vec2.end()); // insert vec2
  • 1
  • 2
  • 3
  • 4
操作功能
c1=c2刪除容器c1中所有的元素,然後將c2的元素複製給c1c1c2的型別(包括容器型別和元素型別)必須相同
c1.swap(c2)交換內容:呼叫完該函式後,c1中存放的是c2原來的元素,c2中存放的是原來c1的元素。c1c2的型別必須相同。該函式的執行速度通常要比將c2複製到c1的操作快
c.assign(b, e)重新設定c的元素,將迭代器bc標記範圍內的所有元素複製到c中。be必須不是指向c中元素的迭代器
c.assign(n, t)c重新設定為儲存n個值為t的元素

注意: 
(a) swap操作不會刪除或插入任何元素,而且保證在常量的時間內重新整理交換。由於容器內沒有移動任何元素,因此迭代器不會失效。

(b) 在這裡補充一點,vector容器大小有兩個描述引數sizecapacitysize前面已經講述過,指容器中當前已儲存元素的數目,而capacity儲存的是容器所分配的儲存空間可以儲存的元素總數。一般來說capacity >= size。在clear, 賦值(c1 = c2),assign(不超過原容器大小)等操作中,並未改變容器的capacity,也就是說,只是把已經分配好的記憶體上寫入的元素資料清掉或者重新賦值,但並未對儲存空間進行變動;但是swap操作時,sizecapacity都會改變。

vector<int> vec(100); // size: 100, capacity: 100;
vec.clear();          // size: 0, capacity: 100

vector<int> vec2(50);
vec = vec2;           // size: 50, capacity: 100

vector<int> vec3(30);
vec.assign(vec3.begin(), vec.end()); // size: 30, capacity: 100

vec.swap(vec3); // error!

vector<int> v1(30), v2(50);
v1.swap(v2); // v1: size 50, capacity 50; v2: size 30, capacity 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

基於此原因,有些時候,當我們想刪除一個容器的所有元素的同時,又想把容器佔用的記憶體釋放掉時,clear並不能完全實現這一目的,但是可以通過swap:

vector<int> vec(100);      // size 100, capacity 100
vector<int>().swap( vec ); // size 0, capacity 0
  • 1
  • 2

(c) vector容器中有reserve操作,可以設定儲存空間大小:

vector<int> vec(24);  // size 24, capacity 24
vec.reserve(50);      // size 24, capacity 50
cout<< "size" << vec.size() << endl << 
    "capacity" << vec.capacity() << endl;
  • 1
  • 2
  • 3
  • 4

3.結束語

我們很喜歡使用容器,因為確實很便捷,相比於陣列,它可以很隨意的實現元素的新增、刪除等。我們也無需擔心記憶體分配的問題,因為標準庫會幫我們都搞定。但是我們最好還是瞭解一下。

vector為例,為了支援快速的隨機訪問,vector容器的元素以連續的方式存放,即每一個元素都挨著前一個元素儲存。當我們向容器中新增元素時,想想會發生什麼:如果容器中已經沒有空間容納新元素,由於容器必須連續儲存以便索引訪問,所以不能在記憶體中隨便找個地方來儲存新元素,而是必須重新分配儲存空間,存放在舊儲存空間的元素被複制到新儲存空間裡,接著插入新元素,最後撤銷舊的儲存空間。如果vector容器在每次新增新元素時,都要這麼分配和撤銷記憶體空間,那麼其效能將會非常慢!所以,標準庫不會這麼做,為了使vector容器實現快速的記憶體分配,其實際分配的容量要比當前所需的空間大一些,例如分配舊儲存空間n倍(例如2倍)大小的新儲存空間,這樣的策略顯著提高了其效率。

vector容器的記憶體分配策略是以最小的代價連續儲存元素,通過訪問上的便利彌補其儲存代價,雖然list容器優於vector容器,但是大部分情況下人們還是覺得vector更好用。實際中vector的增長效率比起listdeque通常會更高。