1. 程式人生 > >C++Primer第五版【筆記】——第九章——順序容器

C++Primer第五版【筆記】——第九章——順序容器

1. 順序容器概述

表一中的順序容器,提供了對元素快速的順序訪問。但是其他操作的開銷則不同:
  • 新增或刪除元素的開銷
  • 進行非順序訪問的開銷
表一:順序容器型別
 vector    可變長度陣列。支援快速隨機訪問。在非尾部插入或刪除元素速度很慢
 deque   雙端佇列。支援快速隨機訪問。快速在頭或尾的插入和刪除
 list   雙向連結串列。只支援雙向的順序訪問。可以在list的任何位置快速插入和刪除
 forward_list    單向連結串列。只支援單向順序訪問。可在任何位置快速插入和刪除
 array   定長陣列。支援快速隨機訪問。不能新增或刪除元素。
 string   專門用來儲存字元的容器,類似vector。支援快速隨機訪問。快速在尾部的
  插入和刪除。
除了array是定長容器之外,容器提供了高效的,靈活的記憶體管理。我們可以新增和刪除元素,動態的增長或縮減容器的大小。 容器中元素的儲存方法決定了它所支援的操作,以及操作的效率。比如,string和vector中的元素儲存在連續的記憶體中。因此它們的元素是連續的,支援快速隨機訪問。但是,在元素序列中的插入或刪除操作開銷很大。list和forward_list則可以快速的在容器中的任意位置插入或刪除元素。但是,不支援隨機訪問。deque是一個比較複雜的資料結構。它可以像string和vector那樣,支援快速隨機訪問。同時可以在容器的頭部和尾部快速的插入和刪除元素。 【C++11】 forward_list和array是新標準中加入的。array相比內建陣列,更加安全、易用。它具有固定長度,不支援插入和刪除元素。forward_list可以與最高效的手寫單鏈表相媲美。forward_list不支援size操作,因為該操作開銷很大。 【注】 新版本的庫容器要比之前版本的效率高的多。所以,儘可能的使用它們吧。

如何選擇使用哪個順序容器?

可以按照下面的規則:
  • 使用vector,除非你有理由使用其他容器
  • 如果你的程式有許多小元素,且空間開銷很重要時,不要使用list或forward_list
  • 如果程式需要隨機訪問元素,使用vector或deque
  • 如果程式需要在容器中間插入或刪除元素,使用list或forward_list
  • 如果程式需要在頭部或尾部插入或刪除元素,而不是中間,使用deque
  • 如果程式只在讀入資料時需要在容器中間插入元素,隨後需要隨機訪問元素:
    -- 首先,確定是否真的需要在容器中間插入元素。一般先為vector追加元素,然後呼叫sort函式排序會更容易。
    -- 如果真的需要在中間插入元素,考慮在輸入階段使用list。然後當輸入完成後,將list複製到vector。
如果程式既需要隨機訪問,又需要在中間插入或刪除元素。那麼就需要比較list或forarad_list訪問元素的開銷,和vector,deque插入或刪除元素的開銷。同時還需要考慮程式本身是隨機訪問次數更多還是插入、刪除操作更多。或者還可以考慮使用更復雜的資料結構來支援。

2. 容器庫概述

容器的操作種類可以劃分為一個層次:
  • 一些操作是通用的。
    Type Aliases
     iterator  容器類的迭代器型別
     const_iterator  該迭代器只能讀,不能改變元素
     size_type  無符號整型,足夠儲存最大可能的容器類的大小
     difference_type  有符號整型,足夠儲存兩個迭代器的距離
     value_type  元素型別
     reference  元素左值型別;與value_type&等價
     const_reference  元素的const左值型別(const value_type &)
    Construction
     C c;  預設建構函式,空容器
     C c1(c2);  建立c1為c2的一個副本
     C c(b, e);  複製迭代器b和e範圍內的元素(不適用於array)
     C c{a, b, c...};  列表初始化
    Assignment and swap
     c1 = c2  用c2中的元素值代替c1中的元素
     c1 = {a,b,c...}  用列表中的元素代替c1中的元素 (不適用於array)
     a.swap(b)  交換a和b中的元素
     swap(a, b)  等價於a.swap(b)
    Size
     c.size()  c中元素的個數(不適用於forward_list)
     c.max_size()  c最多可以儲存的元素個數
     c.empty()   如果c為空,true
    Add/Remove Elements
    (not valid for array)
    Note: the interface to these operations varies by container type
     c.insert(args)  將args指代的元素插入c中
     c.emplace(inits)  使用inits構造一個c中的元素
     c.erase(args)  刪除args指代的元素
     c.clear()  刪除c中所有元素,返回void
    Equality and Relational Operators
     ==, !=  所有容器都支援
     <, <=, >, >=  關係(非有序關係容器不支援)
    Obtain Iterators
     c.begin(), c.end()  Return iterator to the first, one past the last element in c
     c.cbegin(), c.cend()  Return const_iterator
    Additional Members of Reversible Containers  (not valid for forward_list)
     reverse_iterator  Iterator that addresses elements in reverse  order
     const_reverse_iterator  Reverse iterator that cannot write the elements
     c.rbegin(), c.rend()  Return iterator to the last, one past the first element in c
     c.crbegin(), c.crend()  Return const_reverse_iterator

  • 順序容器,關係容器,非順序容器的其他操作
    Defining and Initializing Containers
     C c;  預設建構函式。如果C是array,則c中的元素是預設初始化,否則c為空
     C c1(c2)  c1 is a copy of c2. c1 and c2 must have the same type
    (必須是相同型別的容器,且其中的元素型別也相同;對於array,長度也需要相同)
     C c{a,b,c...}  c is a copy of the elements in the initializer list.列表中元素的型別必須與C中元素的型別是相容的。對於array,列表中元素的個數必須小於或等於array的長度,缺少的部分用值初始化
     C c={a,b,c...}
     C c(b,e) c is a copy of the elements in the range denoted by iterators b and e. 元素型別必須和C的元素型別相容。(不適用於array)
    Constructors that take a size are valid for
    sequential containers only
    (not including array)
     C seq(n)  seq has n value-initialized elements; the constructor is explicit.(Not valid for string).
     C seq(n, t)  seq has n elements with value t.
容器幾乎可以儲存任何的型別,包括容器本身。但是還是有一些限制的。比如一些容器需要其元素支援某個特定的操作,如果某型別不支援這種操作,那麼就不能作為容器的元素。
vector<vector<int>> vvi;
vector<vector<int> > vv_i; // 舊版本需要加一個空格

2.1 迭代器

迭代器支援的操作和指標類似,包括自增、自減、解引用等。對於不同的容器,其迭代器支援的操作也會不同。比如forward_list就不支援--操作。 需要特別指出的是,迭代器的範圍是一個前開後閉區間:[begin, end)

2.2 容器的型別成員

容器中定義了一些型別成員,比如size_type,iterator, const_iterator等。這些類型別名可以讓我們在使用的時候不需要知道具體型別,在泛型程式設計中很有用。

2.3 begin和end

【C++11】 c版本的begin和end是新標準加入的,使用auto可以很方便的定義。
vector<string> vs;
const vector<string> c_vs;
auto beg = vs.begin(); // iterator
auto cbeg = c_vs.begin(); // const_iterator
auto cbeg1 = vs.cbegin(); // const_iterator 
不帶c的版本在內部是過載函式。一個const版本的和一個非const版本的。如果是const物件,則呼叫const版本,如果是非const物件則呼叫非const版本。

2.4 定義和初始化容器

除了array之外,每個容器都定義了預設建構函式,以及一個指定容器大小和初始化元素值的建構函式。 根據一個容器來初始化另一個容器有兩種方法:第一種是直接複製,容器型別和元素型別必須匹配;第二種是用迭代器指定一個範圍,容器型別和元素型別都不需要一致,元素型別要保證是可以轉換的。
vector<string> vs = {"hello", "world"};
list<const char*> lcc = {"ok", "I", "want"};
deque<string> ds(vs); // error
forward_list<string> fs(lcc.begin(), lcc.end()); // ok
順序容器的初始化:
vector<int> vi(10, 1024); // 初始化為10個1024
list<string> ls(10, "ok"); // 初始化為10個"ok"
forward_list<int> fi(10); // 初始化為10個0
deque<string> ds(10); // 初始化為10個空string
需要指出的是,只提供一個元素個數的建構函式,需要元素本身(如果是類型別)具有預設建構函式。

array

array和內建陣列型別類似,固定長度,陣列大小是型別的一部分。
array<int, 50> a = {0, 1, 2 ,3}; // 其餘元素為0

2.5 賦值和交換

assign operation
assign operations not valid for associative containers or array
 seq.assign(b,e)   Replaces elements in seq with those in the range denoted by iterators
 b  and e. The iterators b and e must not refer to elements in seq.
 seq.assign(il)  Replaces the elements in seq with those in the initializer list il.
 seq.assign(n,t)  Replaces the elements in seq with n elements with value t.
【WARNING!】Assignment related operations invalidate iterators, references, and pointers into the left-hand container. Aside from string they remain valid after a swap, and (excepting arrays) the containers to which they refer are swapped.
與內建陣列不同的是,array支援賦值。
array<int,5> arr1 = {1,2,3,4,5};
array<int,5> arr2 = {0};
arr1 = arr2;
【注】賦值操作的等號左右兩邊的型別必須一致

使用assign

順序容器還支援assign操作,提供了更加靈活的賦值,只要型別是相容的(即可以相互轉換)。
list<string> ls;
vector<const char*> vcc;
ls = vcc; // error
ls.assign(vcc.cbegin(), vcc.cend()); // ok

使用swap

swap可以用來交換兩個相同型別的容器,該操作可以在常數時間內完成。資料本身沒有交換,交換的是內部資料結構,比如迭代器,引用,指標

2.6 size操作

size函式返回當前容器中包含的元素個數;如果容器為空,empty返回ture,否則返回false;max_size返回容器最大可以容納的元素個數。

2.7 關係操作符

所以容器都支援相等操作(==和!=);所以容器除了無序關聯容器,都支援關係操作(>, >=, <, <=)。 【注】只有容器的元素支援相應的關係操作時,才能比較該容器。

3. 順序容器操作

3.1 新增元素

除了array,所以的庫容器都支援容器大小在執行時的動態改變。
c.push_back(t)
c.emplace_back(args)
在容器c的尾部,建立一個值為t的元素,或根據args構建一個元素。返回void
c.push_front(t)
c.emplace_front(args)
在容器c的頭部,建立一個值為t的元素,或根據args構建一個元素。返回void
c.insert(p,t)
c.emplace(p,args)
在迭代器p所指元素的前面,插入一個值為t的元素,或根據args構建一個元素;
返回指向插入元素的迭代器
c.inesrt(p, n, t) 在迭代器p所指元素的前面,插入n個值為t的元素;返回指向插入的第一個元素的迭代器,
如果n為0,返回p
c.insert(p, b, e) 在迭代器p所指元素的前面,插入迭代器指向範圍為[b,e)的元素,b,e不能指向c本身。
返回指向插入的第一個元素的迭代器,如果範圍為空,返回p
c.insert(p, il) il是包含元素值的括號序列。在迭代器p指向的元素前面插入該序列。返回指向插入的
第一個元素的迭代器,如果序列為空,返回p
【注意】在vector, string, deque中插入元素,會使現有的迭代器,引用和指標無效。forward_list不支援push_back和emplace_back。vector和string不支援push_front和emplace_front. 不同容器的插入操作效率是不同的。比如在vector中插入(非尾部)元素,需要元素的移動操作,這會導致額外開銷。
list<string> ls;
vector<string> vs = {"this", "is", "for", "test"};
ls.insert(ls.begin(), vs.end()-2, v.end());
ls.insert(ls.end(), {"love","coding"});
【c++11】
新標準中,接受一個計數引數或迭代器範圍的insert版本會返回指向插入的第一個元素的迭代器(之前的版本返回void)。 【c++11】
emplace_front, emplace, emplace_back是新標準加入的三個操作,與push_front, insert, push_back不同的是,它們用args引數呼叫元素型別的建構函式在插入的位置建立新的元素,而不是根據傳遞的元素複製一個副本。 相當於根據引數型別,呼叫相應的建構函式,在容器所在的記憶體空間(插入的位置)建立新的元素。

3.2 訪問元素

順序容器的元素訪問操作
 c.back()   返回容器c的最後一個元素的引用。如果c為空,則結果未定義
 c.front()   返回容器c的第一個元素的引用。如果c為空,則結果未定義
 c[n]  返回下標n所在位置的元素。如果n>=c.size(),則結果為定義
 c.at(n)  返回下標n所在位置的元素。如果n超出範圍,則丟擲out_of_range 異常
每個順序容器包括array,都支援front操作,除了forward_list,都支援back操作。

3.3 刪除元素

順序容器的刪除操作
c.pop_back() 刪除容器c的最後一個元素。如果c為空,則未定義。返回void
c.pop_front() 刪除容器c的第一個元素。如果c為空,則未定義。返回void
c.erase(p) 刪除迭代器p指向的元素,返回指向被刪除元素後一位的迭代器。
c.erase(b,e) 刪除迭代器範圍為[b,e)的元素,返回指向被刪除元素後一位的迭代器。
c.clear() 刪除c的所有元素,返回void
【注】在deque中刪除非開頭或結尾的元素會導致其所有的迭代器、引用和指標失效。在vector或string中刪除元素,指向被刪除元素後面位置的迭代器、引用和指標會失效。

3.4 forward_list的專用操作

對於單向連結串列來說,插入或刪除元素,會改變其前面一個元素中的指標。所以它的插入和刪除操作不同於其他容器。
forward_list的插入和刪除操作
 lst.before_begin()
 lst.cbefore_begin()
 指向連結串列開頭前一個位置的迭代器,該迭代器指向的不是連結串列中的某個元素,
 不能解引用。cbefore_begin()返回const_iterator
 lst.insert_after(p,t)
 lst.insert_after(p,n,t)
 lst.insert_after(p,b,e)
 lst.insert_after(p,il)
 在迭代器p所指元素的後面插入相應的元素。
 emplace_after(p, args)  在迭代器p所指元素的後面,根據args構造一個新的元素
 lst.erase_after(p)
 lst.erase_after(b,e)
 刪除在迭代器p所指的,或範圍為[b,e)的元素。返回被刪除元素的後繼的迭代器。

3.5 改變容器大小

容器(除了array)可以使用resize來改變大小。
順序容器size操作
 c.resize(n)  重新定義c的大小為n。如果n < c.size(),刪除多餘的元素。 
 否則新增新的元素(值初始化)
 c.resize(n,t)   重新定義c的大小為n。增加的新元素值為t
【注】當resize使容器縮小時,指向被刪除元素的迭代器、引用和指標會失效;resize會使vector、string、deque的所以迭代器、引用和指標失效。 如果容器儲存的是類型別元素,當resize使得增加元素時,會呼叫該類的預設建構函式,如果沒有預設建構函式,則必須提供與建構函式相應的初始化值。

4. vector如何增長

vector的記憶體管理是,根據一定的策略事先分配一段記憶體空間,當元素個數增長到空間不夠用時,再分配新的記憶體空間,然後將元素搬到新的空間,再釋放舊的空間。
容器記憶體管理
 c.shrink_to_fit()  將capacity()縮小到與size()相等
 c.capacity()  容器c的容量
 c.reserve(n)  使c的容量至少可以儲存n個元素
需要注意的是,resize不會改變容器的容量,其改變的是容器中元素的個數。reserve不會使容器的容量減少。 【c++11】 新標準加入的shrink_to_fit使得可以釋放容器中多餘的容量。適用於deque, vector和string。 增長測試:
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> vi;
	cout << "size: " << vi.size() << "; capacity: " << vi.capacity() << endl;
	for(vector<int>::size_type idx = 0; idx <= 50; ++idx) {		
		vi.push_back(idx);
		cout << "size: " << vi.size() << "; capacity: " << vi.capacity() << endl;
	}
	return 0;
}

5. string操作

略過。

6. 容器介面卡

介面卡是一個通用概念。包括容器介面卡,迭代器介面卡和函式介面卡。介面卡是一種使一種物件按另一種物件的規則操作的機制。 除了前面幾節講到的順序容器,C++庫還定義了三種順序容器介面卡:stack, queue和priority_queue。

定義一個介面卡

預設情況下,stack和queue在deque上實現,priority_queue在vector上實現。
stack<int> stk;
也可以指定用來實現的容器:
stack<int, vector<int>> stk_vec
實現介面卡的容器是有限制的。所有的介面卡都要求有新增和刪除元素的能力。所以array不能使用。所以的介面卡都要求在容器的末尾新增和刪除元素,forward_list也不能使用。stack要求push_back, pop_back和back操作,可以用vector, deque, list實現。queue要求back, push_back, front和push_front操作,可以用list, deque實現。priority_queue除了要求front, push_back和pop_back 操作,還要求隨機訪問,所以可以用vector 或 deque實現。