1. 程式人生 > 實用技巧 >c++primer筆記九、順序容器

c++primer筆記九、順序容器

一個容器就是一些特定型別物件的集合。
順序容器為程式設計師提供了控制元素儲存和訪問順序的能力。
順序和元素加入時的位置有關,和本身的值無關。

9.1順序容器概述

主要型別有

vector              //向量,可變大小的陣列,訪問快速,插入刪除慢
deque               //雙端佇列,支援快速隨機訪問,頭尾插入刪除很快
list                //雙向連結串列,只支援雙向順序訪問,插入刪除很快
forward_list        //單向連結串列,只支援單向順序訪問。插入刪除很快
array               //固定大小陣列,支援快速隨機訪問,不能
string              //字串,類似vector
選擇使用哪種容器

一些基本原則

1:如果有很好的理由選擇其他容器,否則就用vector
2:有很多小的元素,且空間的額外開銷很重要,則不用list
3:要求隨機訪問元素用vector和deque
4:要求中間插入用list
5:頭尾插入用deque
6:只有讀取時要在中間位置插入,讀取完要隨機訪問,可以先list然後拷貝到vector。
如果程式既要隨機訪問,又要在中間插入,就應該比較效能

9.2容器庫概覽

容器的操作有一些層次:

1、有些操作所有容器都提供
2、有一些操作僅僅針對順序容器,關聯容器或無序容器
3、有些操作適用於小部分容器

每個容器都定義在一個頭檔案中,與型別名相同。

對容器可以儲存的元素型別的限制

可以儲存幾乎任何型別,除了某些容器對元素型別有自己的特殊要求。
例如:如果類沒有預設建構函式,則必須初始化才行。

//noDefault時一個沒有預設建構函式的型別
vector<noDefault> v1(10, init);
vector<noDefault> v2(10);       //錯誤,必須提供初始化器,因為noDefault沒有建構函式
迭代器

標準容器型別上的所有迭代器都允許我們訪問容器中的元素。
迭代器都通過解引用來實現。
操作見表3.6
一個例外forward_list不支援遞減(--)

迭代器範圍

迭代器由一對迭代器表示,分別指向容器中的元素或者是尾元素之後的位置。
通常稱為begin和end,或者first和last。
第二個end指向尾元素之後的位置,是個空的
成為左閉合區間

[begin, end)

使用左閉合範圍蘊含的程式設計假定

假設begin和end構成一個迭代器範圍,則:
1:如果begin和end相等,範圍為空
2:如果不能,則至少包含一個,且begin指向範圍內的第一個元素
3:可以對begin遞增若干次,直到begin == end

while (begin != end)
{
    *begin = val;
    ++begin;
}
容器型別成員

每個容器都定義了多個型別,使用這些型別必須顯式地使用其類名

list<string> :: iterator iter;
vector<int> :: difference_type;
begin和end成員

有4種版本,r開頭的是反向迭代器,c開頭的是const迭代器

list<string> a = {"Milton", "Shakespeare", "Austen"};
auto it1 = a.begin();
auto it2 = a.rbegin();
auto it3 = a.cbegin();
auto it4 = a.crbegin();

不過以c開頭的函式都是過載過的,實際上有兩個begin的成員,一個是const成員,返回const_iterator,另一個返回iterator。當對非常量呼叫是,得到的是iterator版本,只有對一個const物件呼叫時,才會得到一個const版本。
可以將普通的iterator轉換為對應的const_iterator,反之不行。
c開頭時新加的,用以支援auto使用。過去只能顯式宣告使用哪種迭代器。

list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
//等同於
auto it7 = a.begin();
auto it9 = a.cbegin();

當不需要寫訪問時,就應該用cbegin和cend

容器定義和初始化

每個容器都有預設的建構函式,除了array,其他的預設建構函式都會建立一個指定的空容器,且都可以接受大小和初始值的引數。
更多見表9.3

將一個容器初始化為另一個容器的拷貝

將一個容器建立為另一個容器的拷貝有兩種方法:可以之間拷貝,或者拷貝由一個迭代器對指定的元素範圍。
第一種要求兩種容器型別及元素必須匹配,第二種不要求容器型別相同,且元素型別也可以不同,只要能夠進行元素轉換即可。

list<string> authors = {"Milton", "Shakespeare", "Austen"}
vector<const char*> articles = {"a", "an", "the"}

list<string> list2(authors);    //正確
deque<string> authList(authors);    //錯誤
vector<string> word(articles);      //錯誤
forward_list<string> words(articles.begin(), articles.end());   //正確
列表初始化

顯式地指定了容器每個元素地值,並且隱含了指定容器地大小

vector<const char*> articles = {"a", "an", "the"}
與順序容器大小相關地建構函式
vector<int> iver(10, -1);       //10個int,每個都初始化-1
deque<string> sver(10);         //10個string,每個都是空

只要順序容器才支援接受大小引數,關聯容器不支援。

標準庫array具有固定大小

定義array時必須指定大小

array<int, 10>
array<string, 10>

為了使用array,必須同時指定元素型別和大小

array<int, 10> :: size_tpye i;

列表初始化時值地數目必須小於或等於array大小,剩下的都會進行值初始化。

array<int, 10> ia = {42}        //ia[0]為42,剩下為0

內建陣列不能拷貝,array可以拷貝,只需要陣列型別和大小匹配

賦值和swap

賦值運算子將左邊容器的全部元素替換為右邊元素的拷貝

c1 = c2;
c1 = {a, b, c};

與內建陣列不同,array允許賦值,且等號兩側必須具有相同型別

array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {0};    //初始全為0
a1 = a2;        //替換a1的元素
a2 = {0};       //錯誤

容器賦值運算有:

c1 = c2;
c1 = {a, b, c};
swap(c1, c2);
c1.swap(c2);        //更快
seq.assign(b,e);    seq元素替換為迭代器b和e表示的元素範圍
seq.assign(il);     seq替換為初始化列表il中的元素
seq.assign(n,t)     seq中的元素替換為n個值為t的元素
使用assign(僅順序容器)

用來元素的替換

list<string> names;
vector<const char*> oldstyle;
names = oldstyle;       //錯誤,型別不匹配
names.assign(oldstyle.cbegin(), oldstyle.cend());   正確

assign中的迭代器不能是呼叫assign的容器
assign第二個版本接受整型和一個元素值,全部替換

list<string> slist1(1);     //1個元素,為空
slist1.assign(10, "Hiya~"); //10個元素,都為"Hiya~"

//一種等價
slist1.clear();
slist1.insert(slist1.begin(), 10, "Hiya~");
使用swap

交換兩個相同型別的容器
元素本身沒有交換,交換的是兩個容器的內部資料結構(沒有進行拷貝刪除插入操作,複雜度為常數時間)
元素不會移動意味著除了string外,指向容器的迭代器,引用和指標都不會失效,仍然指向swap操作之前的那些元素。
但是swap後這些元素屬於不同容器了。
對string呼叫swap會使迭代器,引用和指標失效。
對array進行swap,會真正交換元素。
非成員版本的swap在泛型程式設計中很重要,所以最好使用非成員版本的swap。

容器大小操作
size:返回大小
empty:如果為空則返回true
max_size:返回一個大於或等於該型別容器所能容納的最大元素的值

有一個例外forward_list不支援size

關係運算符

每種容器都支援相等運算子(==和!=);除了無需關聯容器外都支援關係運算符(>、>=、<、<=)
關係運算符左右必須是相同型別的容器,且必須儲存相同型別的元素。
比較實際是逐對比較,和string類似:

1:如果兩個容器大小相等且所以元素兩兩相等,則相等
2:如果大小不同,但小容器的元素都等於大容器的元素,則小容器小於大容器
3:如果兩個容器都不是另一個的字首子序列,則取決於第一個不同元素的比較結果
容器的關係運算符使用元素的關係運算符比較

如果元素無法使用關係運算符,就不能比較兩個容器

9.3順序容器操作

向順序容器新增元素

array大小不能改變,因此不支援任何操作。
forward_list有自己專業版本的insert和emplace
forward_list不支援push_back和emplace_back
vector和string不支援push_front和emplace_fornt

c.push_back(t);     //尾部新增一個值為t或者由args建立的元素
c.emplace_back(args);

c.push_front(t);    //頭部新增
c.emplace_front(args);

c.insert(p, t);      //在迭代器p指向的元素前建立t或args建立的元素,返回新新增元素的迭代器
c.emplace(p, args);

c.insert(p, n, t);  //在迭代器p前插入n個t,返回新增的第一個元素迭代器。如果n為0,返回p
c.insert(p, b, e);  //將迭代器b和e指定範圍內的元素插入到p指向的元素前。
//b和e不能是c中的元素,返回新新增的第一個元素迭代器
c.insert(p, il);    在p之前插入一個列表

向vector、string和deque插入元素會使迭代器、引用和指標失效

使用push_back

除了array和forward_list,都支援push_back。

string word;
while (cin >> word)
    container.push_back(word);

在container尾部建立了一個新元素,size也增大了1,該元素是word的拷貝。
string也可以push_back向末尾新增字元

word.push_bacl('s')     //等價word += 's'

容器的元素是拷貝

push_front

list、forward_list、deque支援頭部插入

list<int> ilist;
ilist.push_front(1);

和vector一樣,deque在首尾以外插入元素很耗時。

在容器特定位置新增元素

使用insert進行一般的新增,每個insert都接受一個迭代器作為第一個引數。
在迭代器指向的前一個位置插入

svec.insert(svce.begin(), "Hello!");
插入範圍內元素

可以將十個元素插入到末尾

svec.insert(svec.end(), 10, "Anna");

也可以接受一個迭代器或初始化列表的insert版本將給的範圍的元素插入到指定位置前。

vector<string> v = {"qyasu", "simba", "frollo", "scar"};
//將V的最後2個元素新增到slist的開始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(), {"these", "words", "end"});

新版本的insert操作會返回指向第一個新加入元素的迭代器。

使用insert的返回值
list<string> lst;
auto iter = lst.begin();
//每次iter都指向第一個,等價於push_front
while (cin >> word)
    iter = lst.insert(iter, word);
使用emplace

emplace_操作構造元素而不是拷貝,而push和insert是把物件拷貝到容器中。
當呼叫emplace成員時,是把引數傳遞給元素型別的建構函式。
例如:

//假定c儲存Sales_data元素:
c.emplace_back("987-085945", 25, 15.99) //臨時構造一個Sales_data,直接傳給c
c.push_back(Sales_data("987-085945", 25, 15.99))//臨時構造一個Sales_data,拷貝給c

emplace_back構造完直接傳遞,而push_back則構造完再拷貝

訪問元素

有幾種方法:

1:at和下標操作適用於string, vector, deque和array
2:back不適合forward_list
c.back()        //尾元素引用
c.front()       //首元素引用
c[n]            //下標n的引用,若n>size,則函式行為未定義
c.at(n)         //返回下標n的引用,若越界,則丟擲out_of_range異常
訪問成員函式返回的是引用

如果容器是const物件,則返回值是const引用,否則就是普通引用

if (!c.empty()){
    c.front() = 42;
    auto &v = c.back();
    v = 1024;               //改變c中元素
    auto v2 = c.back();
    v2 = 0;                 //未改變
}
下標操作和安全的隨機訪問

給的下標必須在範圍內,下標運算子不會檢查是否合法。
如果希望下標是合法的,可以用at函式

vector<string> svec;
cout << svec[0];        //執行錯誤
cout << svec.at(0);     //丟擲異常
刪除元素

array不支援刪除
forward_list有特殊的erase;
forward_list不支援pop_back;vector和string不支援pop_front

c.pop_back()        //刪除尾元素,返回void
c.pop_front()       //刪除首元素
c.earse(p)          //刪除迭代器p所指元素
c.erase(b, e)       //刪除迭代器b和e範圍內的元素
c.clear()           //刪除全部元素
pop_front和pop_back

分別刪除首元素和尾元素。
vector和 string不支援pop_front。
forward_list不支援pop_back

//操作返回void,如果需要值需要先取出
process(ilist.front());
ilist.pop_front();
容器內部刪除一個元素

erase從容器中指定位置刪除元素。
可以刪除迭代器指定的一個或一個範圍的元素,返回為刪除後的第一個元素的位置迭代器
例:刪除list中的奇數

list<int> lst = {0,1,2,3,4,5,6,7,8,9};
auto it = lst.begin();
while (it != lst.begin())
    if (*it % 2)
        it = lst.erase(it);
    else
        ++it;
刪除多個元素
elem1 = slist.erase(elem1, elem2);  //刪除elem1到elem2之前的元素
slist.clear();      //刪除所有元素
slist.erase(slist.begin(), slist.end()) //等價
特殊的forward_list

連結串列的操作不太一樣,刪除當前值會改變上一個值的指向,因此沒有insert,emplace和erase,而是用其他操作

lst.before_begin()      //返回首元素之前不存在的元素的迭代器,這個不能解引用。相當於哨兵節點
lst.cbefore_begin()
lst.insert_after(p,t)       //在p之後的位置插入元素,n是數量
lst.insert_after(p,n,t)
lst.insert_after(p,b,e)         //b和e是表示範圍的得帶起
lst.insert_after(p,il)      //il是花括號
emplace_after(p,args)       //構造版本
lst.erase_after(p)          //刪除p所指元素
lst.erase_after(b,e)        //刪除b之後直到e的元素。

可以forward_list寫刪除奇數

forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_begin();
auto curr = flst.begin();
while (curr != flst.end()){
    if (*curr % 2)
        curr = flst.erase_fater(prev);
    else{
        prev = curr;
        ++curr;
    }
}
改變容器大小
resize不適用於array
c.resize(n)     //c的大小改為n
c.resize(n,t)   //大小改為n,新增的元素都初始化為t
容器操作可能使迭代器失效

向容器新增元素後:
1:如果是vector和string,且儲存空間被重新分配,則指向容器的迭代器,指標和引用都失效。如果儲存空間未重新分配,指向插入位置之前的元素的迭代器指標和引用仍遊戲,但插入位置之後的都無效。
2:對於deque,插入到首尾位置之外的任何位置都會導致迭代器指標和引用失效。如果在首尾新增元素,迭代器失效,但引用和指標不失效。
3:list和forward_list,全都仍有效

當刪除元素後:
1:當前元素迭代器指標引用都失效
2:對於list和forward_list,其他位置迭代器引用指標仍有效
3:deque,首尾以外都失效。如果是刪除尾元素,則尾後迭代器失效,其它不影響。如果是首元素,其他不影響。
4:對於vector和string,指向被刪除元素之前的都有效,後面的迭代器失效。

編寫改變容器的迴圈程式

如果迴圈呼叫的是insert或erase,可以容易地更新迭代器。

//刪除偶數,複製奇數
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin();
while (iter != vi.end()) {
    if (*iter % 2){
        iter = vi.insert(iter, *iter);
        iter +=2;
    }else
    {
        iter = vi.erase(iter);
    }
}
不要儲存end返回的迭代器

當我們新增或刪除vector或string元素,或者在deque首元素之外的地方新增刪除元素後,end總會失效。
必須在每次操作後重新呼叫end(),而不能在迴圈開始前儲存它返回的迭代器

auto end = v.end(); //儲存尾迭代器是一個壞想法
//安全的方法:每次迴圈步新增/刪除元素後都重新計算end
while(begin != v.end()){
    ++begin;    //向前移動begin,因為想在此元素之後插入元素
    begin = v.insert(begin, 42)
    ++begin;    //向前移動begin, 跳過剛剛加入的元素
}

9.4 vector物件是如何增長的

為了避免每次新增元素vector就執行記憶體分配和釋放,採用了可以減少容器空間重新分配次數的策略。

管理容量的成員函式

vector提供了一些成員函式允許我們與它的實現中記憶體分配部分互動。

shrink_to_fit只適用於vector、string和deque
capacity和reserve只適用於vector和string

c.shrink_to_fit()   //將capacity()減少為與size()相同大小
c.capacity()        //不重新分配記憶體,c可以儲存多少元素
c.reserve(n)        //分配至少能容納n個元素的記憶體空間

只有當需要的記憶體空間超過當前容量,reserve才會改變。如果需求大於當前容量,reserve至少分配與需求一樣大的記憶體空間(或者更大)。否則什麼也不做,且不會退回多餘空間。

具體實現時,呼叫shrink_to_fit也並不保證一定退回記憶體空間

capacity和size

size是指已經儲存的元素的數目;
capacity是不分配新的記憶體空間的前提下它能最多儲存的元素個數

vector<int> ivec;
cout << "ivec: size:" << ivec.size()
     << "ivec: capacity:" << ivec.capacity() << endl;
//add 24 elements
for(vector<int> :: size_type ix = 0; ix != 24; ++ix)
    ivec.push_back(ix);

cout << "ivec: size:" << ivec.size()
     << " capacity :" << ivec.capacity() << endl;

結果為:

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

可以再預分配一些額外空間

ivec.reserve(50);

接下來用光預留空間

while (ivec.size() != ivec.caoacity())
    ivec.push_back(0);

只要操作沒大於預留空間,vector就不會重新分配空間

ivec.push_back(42);

此時capacity = 100;
vector實現採用的策略似乎是在每次需要分配空間時將當前容量翻倍
可以呼叫shrink_to_fit()來要求退回多餘記憶體

ivec.shrink_to_fit();

shrink_to_fit()只是一個請求,標準庫不保證能退還記憶體
所有實現要遵循一個原則:確保用push_back向vector新增元素的操作有高效率

9.5 額外的string操作

構造string的其他方法
string s(cp,n)      //s是cp指向的陣列中前n個字元的拷貝,此陣列至少包含n個字元
string s(s2,pos2)       //s是string s2從下標pos2開始的拷貝
string s(s2,pos2,lens)  //從s2下標pos2開始拷貝lens長度

建構函式接受一個string或const char*的引數,還可以接受指定數量的字元以及開始的下標

使用前必須考慮字串長度沒有溢位,且如果一直拷貝到最後必須以空字元結尾。

substr
s.substr(pos,n)     //返回一個string,從pos開始的n個字元的拷貝
改變string的其他方法

擁有額外的insert和erase版本(接受下標的版本)

s.insert(s.size(), 5, '!');     //在s末尾插入5個感嘆號
s.erase(s.size() - 5, 5);       //刪除最後5個字元

標準庫string型別還提供了接受c風格字串陣列的insert和assign

const char *cp = "Stately, plump, Buck";
s.assign(cp, 7);        //s = "Stately"
s.insert(s.size(), cp + 7);     //s = "Stately, plump, Buck"

也可以指定將來自其他string或子字串的字元插入到string

string s = "some string", s2 = "some other string";
s.insert(0, s2);        //在位置0前插入說s2
s.insert(0, s2, 0, s2.size());      //在0前插入s2中s2[0]開始的s2.size()個字元
append 和 replace

append是在末尾插入的簡寫形式
replace是呼叫erase和insert的簡寫形式

s.append("aaa");    //末尾新增aaa
s.replace(11, 3, "5th") //從11開始,刪除3個字元,插入5th

更多的過載函式見表9.13

string的搜尋操作

提供6種搜尋函式,每個函式有4個過載版本。見表9.14
每個返回一個string::size_type的值,表示匹配發生位置的下標。
如果搜尋失敗返回string::npos的static成員,npos定義為一個const string::size_type型別,初始值為-1。
size_type是一個unsigned型別,因此用int或者無符號來儲存這個值不好。

find完成簡單搜尋,查詢引數指定的字串,找到返回下標,找不到返回npos

string name("AnnaBelle")
auto pos1 = name.find("Anna");  //0

查詢與給定字串中任何一個字元匹配的位置

string numbers("0123456789"), name("r2d2");
auto pos = name.find_first_of(numbers);     //1
string dept("03714p3");
auto pos = dept.find_first_not_of(numbers); //5
指定從哪裡開始搜尋

可以給find傳遞一個可選的開始位置,預設置0

string::size_type pos = 0;
while ((pos = name.find_first_of(numbers, pos)) != string::npos)
{
    cout << "found number at index:" << pos 
         << " element is " << name[pos] << endl;
    ++pos;
}
逆向搜尋

rfind函式搜尋最後一個匹配

string river("Mississippi");
auto first_pos = river.find("is");      //返回1
auto last_pos = river.rfind("is");      //返回4

類似還有find_last_of和find_last_not_of,搜尋匹配的最後一個字元和最有一個不出現在給定string中的字元

compare函式

用於比較,有6個版本見表9.15

數值轉換
int i = 42;
string s = to_string(i);        //i轉成字元
double d = stod(s);             //s轉成浮點

string s2 = "pi = 3.14";
d = stod(s2.substr(s2.find_first_of("+-.0123456789"))//轉換以數字開始的第一個字串

9.6容器介面卡

三個順序容器介面卡:stack、queue和priority_queue
介面卡是一種機制,使某種事物的行為看起來像另外一種事物。
一個容器介面卡能接受一種已有容器型別,使其行為看起來像一種不同的型別。
見表9.17

定義一個介面卡

每個介面卡都定義兩個建構函式:預設建構函式建立一個空物件,接受一個容器的建構函式拷貝該容器來初始化介面卡。
假定deq是一個deque

stack<int> stk(deq);    //從deq拷貝元素到stk

預設情況下,stack和queue是基於deque實現的,priority_queue實在vector之上實現的。我們可以再建立一個介面卡時將一個命名的順序容器作為第二個型別引數,來過載預設容器型別。

// 在vector上實現的空棧
stack<string, vector<string>> str_stk;
//str_stk2在vector上實現,初始化時儲存svec的拷貝
stack<string, vector<string>> str_stk2(svec);

所有介面卡都要求新增刪除元素,因此不能構建在array之上。
stack要求push_back、pop_back、back操作,可以使用除array和forward_list以為的任何容器
queue要求back,push_back,front和push_front,因此可以構造於list或deque之上,但不能基於vector。
priority_queue除了front、push_back和pop_back以外要求隨機訪問能力,因此可以用vector和deque構造,不能用list

棧介面卡

定義在stack標頭檔案

stack<int> intStack;
for (size_t ix = 0; ix != 10; ++ix)
    intStack.push(ix);
while(!intStack.empty())
{
    int value = intStack.top(); //使用棧頂
    intStack.pop(); //彈出棧頂
}

操作有

//預設基於deque,也可以在list或vector上實現
s.pop()     //彈出
s.push(item)    //加入
s.emplace(args)
s.top()   //返回棧頂元素

雖然是基於deque的,但不能直接使用deque的操作,不能用push_back必須用push

佇列介面卡

queue預設基於deque
priority_queue預設基於vector

q.pop()     //刪除
q.front()   //返回首元素或尾元素,但不刪除
q.back()    //只適用於queue
q.top()     //只適用priority_queue,返回優先順序最高元素
q.push(item)    //末尾新增
q.emplace(args)

queue使用先進先出的儲存和訪問策略。
priority_queue允許為佇列元素建立優先順序,按優先順序排位置