C++ 容器(一):順序容器簡介
C++提供了使用抽象進行高效程式設計的方式,標準庫中定義了許多容器類以及一系列泛型函式,使程式設計師可以更加簡潔、抽象和有效地編寫程式,其中包括:順序容器,關聯容器和泛型演算法。本文將簡介順序容器(vector
,list
和deque
)的相關內容。
1.順序容器的概念
標準庫vector
型別,就是一種最常見的順序容器,它將單一型別元素聚集起來成為容器,然後根據位置來儲存和訪問這些元素,這就是順序容器。順序容器的元素排列順序與其值無關,而僅僅由元素新增到容器裡的次序決定。
標準庫定義了三種順序容器:vector
,list
和deque
。它們的區別在於訪問元素的方式,以及新增或刪除元素相關操作的執行代價。如下表:
順序容器 | 功能 |
---|---|
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 是容器型別名,如vector ,T 是元素型別,如int ,string 。適用於所有容器 |
C c(c2) | 建立容器c2 的副本c ;c2 和c 必須具有相同的容器型別,並存放相同型別的元素。適用於所有容器 |
C c(n) | 建立有n 個初始化元素的容器c 。只適用順序容器 |
C c(n, t) | 使用n 個為t 的元素建立容器c t 必須是容器型別C 的元素型別的值,或者是可以轉換為該型別的值。只適用順序容器 |
C c(b, e) | 建立容器c ,其中元素是迭代器b 和e 標示的範圍內元素的副本。適用於所有容器 |
注意: 所有的容器型別都定了預設建構函式,用於創建制定型別的空容器物件。預設建構函式不帶引數。為了使程式更加清晰、簡短,容器型別最常用的建構函式時預設建構函式。在大多數的程式中,使用預設建構函式能達到最佳執行效能,並且使容器更容易使用。
- 將一個容器初始化為另一個容器的副本
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)容器內元素操作
begin
和end
成員
操作 | 功能 |
---|---|
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
操作返回的迭代器。新增或者刪除vector
或deque
容器內的元素都會導致迭代器失效。
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 型別(只適用於list 和deque 容器型別) |
c.insert(p, t) | 在迭代器p 所指向的元素前面插入值為t 的新元素,返回指向新新增元素的迭代器 |
c.insert(p, n, t) | 在迭代器p 所指向的元素前面新增插入n 個值為t 的新元素,返回void 型別 |
c.insert(p, b, e) | 在迭代器p 所指向元素前面插入由迭代器b 和c 標記範圍的元素,返回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
操作可能會使迭代器失效。在vector
或deque
容器上做resize
操作可能使其所有迭代器都失效。對於所有容器型別,如果resize
操作壓縮了容器,則指向已刪除的元素的迭代器失效。
- 訪問元素
操作 | 功能 |
---|---|
c.back() | 返回容器c 的最後一個元素的引用,如果c 為空,則該操作未定義 |
c.front() | 返回容器c 的第一個元素的引用,如果c 為空,則該操作未定義 |
c[n] | 返回下標為n 的元素的引用,如果n<0 或n>c.size() ,則該操作未定義(只適用於vector 和deque 容器) |
c.at(n) | 返回下標為n 的元素的引用。如果下標越界,則該操作未定義(只適用於vector 和deque 容器) |
注意:使用越界的下標,或呼叫空容器的front
或back
函式,都會導致程式出現 嚴重的錯誤。
- 刪除元素:與插入元素對應容器型別提供了刪除容器內元素的操作。
操作 | 功能 |
---|---|
c.erase(p) | 刪除迭代器p 所指向的元素,返回一個迭代器,它指向被刪除元素後面的元素。如果p 指向容器內的最後一個元素,則返回的迭代器指向容器的超出末端的下一位置,如果p 本身就是指向超出末端的下一位置的迭代器,則該函式未定義 |
c.erase(b, e) | 刪除迭代器b 和e 標記的範圍內的所有元素。返回一個迭代器,它指向被刪除元素段後面的元素。如果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
或將begin
和end
迭代器傳遞給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 的元素複製給c1 。c1 和c2 的型別(包括容器型別和元素型別)必須相同 |
c1.swap(c2) | 交換內容:呼叫完該函式後,c1 中存放的是c2 原來的元素,c2 中存放的是原來c1 的元素。c1 和c2 的型別必須相同。該函式的執行速度通常要比將c2 複製到c1 的操作快 |
c.assign(b, e) | 重新設定c 的元素,將迭代器b 和c 標記範圍內的所有元素複製到c 中。b 和e 必須不是指向c 中元素的迭代器 |
c.assign(n, t) | 將c 重新設定為儲存n 個值為t 的元素 |
注意:
(a) swap
操作不會刪除或插入任何元素,而且保證在常量的時間內重新整理交換。由於容器內沒有移動任何元素,因此迭代器不會失效。
(b) 在這裡補充一點,vector
容器大小有兩個描述引數size
和capacity
。size
前面已經講述過,指容器中當前已儲存元素的數目,而capacity
儲存的是容器所分配的儲存空間可以儲存的元素總數。一般來說capacity >= size
。在clear
, 賦值(c1 = c2
),assign
(不超過原容器大小)等操作中,並未改變容器的capacity
,也就是說,只是把已經分配好的記憶體上寫入的元素資料清掉或者重新賦值,但並未對儲存空間進行變動;但是swap
操作時,size
和capacity
都會改變。
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
的增長效率比起list
和deque
通常會更高。