1. 程式人生 > >C++ primer學習筆記——第九章 順序容器

C++ primer學習筆記——第九章 順序容器

一個容器就是一些特定型別物件的集合。順序容器為程式設計師提供了控制元素儲存和訪問順序的能力。

一、順序容器概述

順序容器型別
vector 可變大小陣列。支援快速隨機訪問。在尾部之外的位置插入或刪除元素可能很慢
deque 雙端佇列。支援快速隨機訪問。在頭尾位置插入/刪除速度很快
list 雙向連結串列。支援雙向順序訪問
forward_list 單向連結串列。只支援單向順序訪問
array 固定大小陣列。支援快速隨機訪問,不能新增或者刪除元素
string 專門用於儲存字元。隨機訪問快。類似於vector

string和vector將元素儲存在連續的記憶體空間內,因此由元素的下標來計算其地址是非常快速的。但是,在這兩種容器的中間位置新增或刪除元素就會非常耗時。

list和forward_list在任何位置新增/刪除元素都非常快速。作為代價,這兩個容器不支援元素的隨機訪問。而且,與vector、array、deque相比,這兩個容器的額外記憶體開銷也很大。

forward_list沒有size操作。

確定使用哪種順序容器

  • 通常使用vector是最好的選擇,除非你有更好的理由選擇其他容器
  • 注重空間開銷的,不要使用list或forward_list
  • 只在頭尾,不在中間插入/刪除元素的,使用deque
  • 在中間插入/刪除元素的,使用list或forward_list

二、容器庫概覽

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

容器均定義為模板類,我們需要額外提供元素型別資訊:

list<Sales_data>
deque<double>

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

順序容器幾乎可以儲存任意型別的元素。特別是,我們可以定義一個容器,其元素的型別是另一個容器:

vector<vector<string>> lines;    //vector的vector

某些類沒有預設建構函式,我們可以定義一個儲存這種型別物件的容器,但我們在構造這種容器時不能只傳遞給它一個元素數目引數:

//假定noDefault是一個沒有預設建構函式的型別
vector<noDefault> v1(10,init);   //正確:提供了元素初始化器
vector<noDefault> v2(10);        //錯誤:必須提供一個元素初始化器
容器操作(通用操作)
類型別名
iterator 此容器型別的迭代器型別
const_iterator 只能讀不能修改的迭代器型別
size_type 無符號整數型別,足夠儲存此類容器型別最大可能容器的大小
difference_type 帶符號整數型別,足夠儲存兩個迭代器之間的距離
value_type 元素型別
reference 元素的左值型別與value_type&含義相同
const_reference const value_type&
建構函式
C c;
C c1(c2);
C c(b,e);
C c{a,b,c,d.....};
賦值與swap
c1=c2
c1={a,b,c,d,e....}
a.swap(b)
swap(a,b);
大小
c.size()
c.max_size()
c.empty()
新增/刪除元素(不適用於array)
c.insert(args)
c.emplace(inits)
c.erase(args)
關係運算符
==,!=
<,<=,>,>=
獲取迭代器
c.begin(),c.end()
c.cbegin(),c.end()
反向容器的額外成員
reverse_iterator
const_reverse_iterator
c.rbegin(),c.rend()
c.crbegin(),c.crend()

1、迭代器

迭代器範圍(begin和end)

  • 它們指向同一個容器中的元素,或者是容器最後一個元素之後的位置
  • end不在begin之前

這種範圍被稱為左閉合區間:

[begin,end)

2、容器成員型別

為了使用類型別名,我們必須顯示使用其類名:

list<string>::iterator iter;
vector<int>::difference_type count;

3、begin和end成員

對一個非常量物件呼叫begin、end、rbegin、rend,得到的是返回iterator的版本;對一個const物件呼叫這些函式時,才會得到一個const版本。但以c開頭的版本還是可以獲得const_iterator的,而不管容器的型別是什麼。

當不需要寫訪問時,應使用cbegin和cend.

4、容器定義和初始化

容器定義和初始化
C c;
C c1(c2); C c1=c2;
C c(b,e); array不適用

C c{a,b,c,d.....}

C c={a,b,c,d.....}

只有順序容器(不包括array)的建構函式才能接受大小引數
C seq(n) string不適用
C seq(n,t)

當將一個容器初始化為另一個容器的拷貝時,兩個容器的容器型別和元素型別都必須相同。不過,當傳遞迭代器引數來拷貝一個範圍時,就不要求容器型別是相同的了,元素型別也可以不同,只要能將元素型別轉換即可:

list<string> authors={"Milton","Shakesperae","Austern"};
vector<const char*> articles={"a","an","the"};

list<string> list2(authors); //正確:型別都匹配
deque<string> authList(authors);  //錯誤:容器型別不匹配
vector<string> words(articles);   //錯誤:容器型別不匹配

//正確:可以將const char*元素轉換成string
forward_list<string> words(articles.begin(),articles.end());

與順序容器大小相關的建構函式

只有順序容器的建構函式才接受大小引數,關聯容器並不支援

標準庫array具有固定大小

大小也是array型別的一部分。當定義一個array時,除了指定元素型別,還要指定容器大小:

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

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

array<int,10>::size_type i;  //陣列型別包括元素型別和大小
array<int>::size_type j;     //錯誤:array<int>不是一個型別

雖然我們不能對內建陣列型別進行拷貝或物件賦值操作,但array並無此限制。此時,要求容器型別、元素型別、大小都必須一致:

int digs[10]={0,1,2,3,4,5,6,7,8,9};
int cpy[10]=digs;                //錯誤:內建陣列不支援拷貝或賦值
array<int,10> digits={0,1,2,3,4,5,6,7,8,9};
array<int,10> copy=digits;       //正確:只要陣列型別匹配即合法

5、賦值和swap

賦值運算
c1=c2
c1={a,b,c,d,e....}
a.swap(b)
swap(a,b);
assign操作不適用與關聯容器和array
seq.assign(b,e)
seq.assign(l1)
seq.assign(n,t)

賦值相關運算會導致指向左邊容器內部的迭代器、引用和指標失效。而swap操作將容器內容交換不會導致指向容器的迭代器、引用和指標失效(array和string的情況除外)

使用assign(僅順序容器)

賦值運算子(=)要求左邊和右邊的運算物件具有相同的型別。assign允許我們從一個不同但相容的型別賦值,或者從容器的一個子序列賦值:

list<string> names;
vector<const char*> oldstyle;
names=oldstytle;   //錯誤:容器型別不匹配
names.assign(oldstyle.cbegin(),oldstyle.cend());    //正確,可以將const char*轉換為string

由於其舊元素被替代,因此傳遞給assign的迭代器不能指向呼叫assign的容器

assign的第二個版本:

//等價於slist.clear()
//後跟slist.insert(slist,10,"Hiya");
list<string> slist(1);     //1個元素,為空string
slist.assign(10,"Hiya");   //10個元素,每個都是“Hiya”

使用swap

swap操作交換兩個相同型別容器的內容,呼叫swap之後,兩個容器中的元素將會交換。

除array外,交換兩個容器內容的操作保證會很快——元素本身未交換,swap只是交換了兩個容器的內部資料結構。

對於array,swap會真正交換它們的元素。

7、關係運算符

關係運算符兩邊的運算物件必須是相同型別的容器,且必須儲存相同型別的元素

比較兩個容器實際上是進行元素的逐對比較,比較方式與string比較類似。

容器的關係運算符使用元素的關係運算符完成比較

只有當其元素型別也定義了相應的比較運算子時,我們才可以使用關係運算符來比較兩個容器。

三、順序容器操作

向順序容器新增元素的操作

這些操作會改變容器的大小;array不支援這些操作

forward_list有自己專有版本的insert和emplace

forward_list不支援push_back和emplace_back

vector和string不支援push_front和emplace_front

c.push_back(t) 在c的尾部建立一個值t或由args建立的元素。返回void
c.emplace_back(args)
c.push_front(c) 在c的頭部建立一個值t或由args建立的元素。返回void
c.emplace_front(args)
c.insert(p,t) 在p指向的元素之前建立一個值為t或者由args建立的元素。返回指向新新增的元素的指標
c.emplace(p,args)
c.insert(p,n,t) 在p指向的元素之前插入元素。返回指向新新增的第一個元素的迭代器。若新元素數量為0,則返回p
c.insert(p,b,e)
c.insert(p,il)
向一個vector、string或deque插入元素會使所有指向容器的迭代器、引用和指標失效

使用push_back

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

當我們用一個物件初始化容器時,或將一個物件插入到容器中時,實際上放入到容器中的是物件值的一個拷貝,而不是物件本身。

使用push_front

//此迴圈使ilist在頭部儲存3/2/1/0
list<int> list;
for(size_t ix=0;ix!=4;++ix)
    ilist.push_front(ix);

在容器中的特定位置新增元素

slist.insert(iter,"hello!");  

將元素插入到vector、string和deque中的任何位置都是合法的。然而,這樣做可能很耗時。

插入範圍內元素

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

vector<string> v={"quasi","simba","forllo","scar"};
slist.insert(slist.begin(),v.end()-2,v.end());
slist.insert(slist.end,{"these","words","will","go","at","the","end"});

//錯誤:迭代器表示要拷貝的範圍,不能指向與目的位置相同的容器
slist.insert(slist.begin(),slist.begin(),slist.end());

使用insert的返回值

insert返回指向新新增的元素的迭代器

//等價於呼叫push_front
list<string> lst;
auto iter=lst.begin();
while(cin>>word)
    iter=lst.insert(iter,word);

使用emplace操作

當我們呼叫一個emplace成員函式時,是將引數傳遞給元素型別的建構函式。emplace成員使用這些引數在容器管理的記憶體空間中直接構造元素:

//使用三個引數的Sales_data建構函式,在c的末尾構造一個Sales_data物件
c.emplace_back("978-2458930153",25,15.99);
//錯誤:沒有接受三個引數的push_back版本
c.push_back("978-2458930153",25,15.99);
//正確:建立一個臨時的Sales_data物件傳遞給push_back
c.push_back(Sales_data("978-2458930153",25,15.99));

傳遞給emplace函式的引數必須與元素類的建構函式相匹配:

c.emplace_back(); //使用Sales_data的預設建構函式
c.emplace(iter,"999-999999999");   //隱形轉換,使用Sales_data(string)
c.emplace_front("978-2458930153",25,15.99);

2、訪問元素

在順序容器中訪問元素的操作

at和下標操作符只適用於string、vector、deque和array

back不適用於forward_list

c.back() 返回c中尾元素的引用。若c為空,函式行為未定義
c.front() 返回c中首元素的引用。若c為空,函式行為未定義
c[n] 返回c中下標為n的元素的引用。n是一個無符號整數。n<c.size()
c.at(n) 返回c中下標為n的元素的引用。下標不可越界,否則丟擲out_of_range異常
對一個空容器呼叫front和back,就像使用一個越界的下標一樣,是一種嚴重的程式設計錯誤

3、刪除元素

順序容器的刪除操作

這些操作會改變容器的大小,所以不適用於array

forward_list有特殊版本的erase

forward_list不支援pop_back;

vector和string不支援popfront

c.pop_back() 刪除c中尾元素。若c為空,則函式行為未定義。函式返回void
c.pop_front() 刪除c中首元素。若c為空,則函式行為未定義。函式返回void
c.erase(p) 刪除迭代器p指向的元素,返回被刪除元素之後元素的迭代器;若p指向尾元素,則返回尾後迭代器;若p是尾後迭代器,則函式行為未定義
c.erase(b,e) 刪除迭代器b和e所指範圍內的元素。返回一個指向最後一個被刪除元素之後元素的迭代器;若e本身就是尾後迭代器,則函式也返回尾後迭代器
c.clear() 刪除c中的所有元素。返回void

刪除deque中除首尾位置之外的任何元素都會使所有迭代器、引用和指標失效

指向vector或string中刪除點之後位置的迭代器、引用和指標都會失效

刪除list中的所有奇數:

list<int> lst={0,1,2,3,4,5,6,7,8,9};
auto it=lst.begin();
while(it!=lst.end())
    if(*it%2)
        it=lst.erase(it);  //刪除奇數並移動it
    else
        ++it;
     

4、特殊的forward_list操作

在一個單向連結串列中,沒有簡單的辦法來獲取一個元素的前驅。所以,forward_list中新增或刪除元素的操作是通過改變給定元素之後的元素來完成的。

在forward_list中插入或刪除元素的操作
lst.before_begin() 返回指向連結串列首元素之前不存在的元素的迭代器。此迭代器不可引用
lst.cbefore_begin() 返回一個const_iterator
lst.insert_after(p,t) 在迭代器p之後的位置插入元素。返回指向最後一個插入元素的迭代器。
lst.insert_after(p,n,t)
lst.insert_after(p,b,e)
lst.insert_after(p,il)
emplace_after(p,args) 在p指定的元素之後建立一個元素。返回一個指向這個新元素的迭代器。
lst.erase_after(p) 返回p指向位置之後的元素,返回指向被刪除元素之後元素的迭代器
lst.erase_after(b,e) 刪除從b之後直到(但並不包括)e之間的元素,返回指向被刪除元素之後元素的迭代器

從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_after(prev);    //刪除並移動curr
    else{
        prev=curr;
        ++curr;
    }
}

5、改變容器大小

我們可以使用resize來增大或縮小容器

順序容器大小操作
resize不適用於array
c.resize(n) 調整c的大小為n個元素。若n<c.size(),則多出的元素被丟棄。若必須新增新元素,對新元素進行值初始化
c.reseize(n,t) 調整c的大小為n個元素。任何新新增的元素都初始化為值t
如果resize縮小容器,則指向被刪除元素的迭代器、引用和指標都會失效;對vector、string或deque進行resize可能導致迭代器、指標和引用失效
list<int> ilist(10,42);  //10個int,每個都是42
ilist.resize(15);        //將5個值為0的元素新增到ilist的末尾
ilist.resize(25,-1);     //將10個值為-1的元素新增到ilist的末尾
ilist.resize(5);         //從ilist末尾刪除20個元素

6、容器操作可能使迭代器無效

編寫改變容器的迴圈程式

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);//刪除偶數元素
        //不應該向前移動迭代器,iter指向我們刪除的元素之後的元素
}

不要儲存end返回的迭代器

如果在一個迴圈中插入/刪除deque、string或vector中的元素,不要快取end返回的迭代器。

必須在操作前重新呼叫end()

while(begin!=end())
{
    //做一些處理
    begin=v.insert(iter,42);
    begin+=2;
}

四、vector物件是如何增長的

當不得不獲取新的記憶體空間時,vector和string的實現通常會分配比新的空間需求更大的記憶體空間。容器預留這些空間作為備用,可以來儲存更多的新元素。這樣,就不需要每次新增新元素都重新分配容器的記憶體空間了。

管理容量的成員函式

容器大小操作管理

shrink_to_fit只適用於vector、string和deque

capacity和reserve只適用於vector和string

c.shrink_to_fit 請求將capacity()減少為與size()相同大小,但不保證一定退回記憶體空間
c.capacity() 不重新分配記憶體空間的話,c可以儲存多少元素
c.reserve(n) 分配至少能容納n個元素的記憶體空間

reverse並不改變容器中元素的數量,它僅影響vector預先分配多大的記憶體空間。

如果需求大小小於或等於當前容量,reverse什麼也不做。當需求大小小於當前容量,容器不會退回記憶體空間。

resize成員函式只改變容器中元素的數目,而不是容器的容量。

capacity和size

容器的size是指它已經儲存的元素數目;而capacity則是在不分配新的記憶體空間的前提下它最多可以儲存多少元素

每個vector實現都可以選擇自己的記憶體分配策略。但是必須遵守的一條原則是:只有當迫不得已時才可以分配新的記憶體空間

五、額外的string操作

1、構造string的其他方法

構造string的其他方法
n、len2和pos2都是無符號值
string s(cp,n) s是cp指向的陣列前n個元素的拷貝。此陣列至少應該包含n個字元
string s(s2,pos2) s是string s2從下標pos2開始的字元的拷貝。如果pos2>s2.size(),建構函式的行為未定義
string s(s2,pos2,len2) s是string s2從下標pos2開始的len2個字元的拷貝。pos2>s2.size(),建構函式的行為未定義。不管len2的值是多少,建構函式至多拷貝s2.size()-pos2個字元

通常我們從一個const char*建立string時,指標指向的陣列必須以空字元結尾,拷貝操作遇到空字元停止。如果我們還傳遞給建構函式一個計數值,陣列就不必以空字元結尾。如果我們未傳遞計數值且陣列未以空字元結尾,或者給定計數值大於陣列大小,則建構函式的行為是未定義的。

子字串操作
s.substr(pos,n) 返回一個string,包含s中從pos開始的n個字元的拷貝。pos的預設值為0,如果pos>s.size(),丟擲一個out_of_range異常。n的預設值為s.size()-pos,即拷貝從pos開始的所有字元

2、改變string的其他方法

修改string的操作
s.insert(pos,args) 在pos之前插入args指定的字元,pos可以是一個下標,下標版本返回一個指向s的引用
s.erase(pos,len) 刪除從位置pos開始的len個字元,返回一個指向s的引用
s.assign(args) 將s中的字元替換為args指定的字元。返回一個指向s的引用
s.append(args) 將args追加到s。返回一個指向s的引用
s.replace(range,args) 刪除s中範圍rang內的字元,替換為args指定的字元。返回一個指向s的引用

assign和append函式無須指定要替換string中的哪個部分:assign總是替換string中的所有內容,append總是將新字元追加到string末尾

replace有兩個版本:

replace(pos,len,args)

replace(b,e,args)

insert有兩個版本:

insert(pos,args)

insert(iter,args)

3、string搜尋操作

string搜尋操作
搜尋操作返回指定字元出現的下標,如果未找到則返回npos
s.find(args)
s.rfind(args)
s.find_first_of(args)
s.find_last_of(args)
s.find_first_not_of
s.find_last_not_of
args必須是以下形式之一
c,pos
s2,pos
cp,pos
cp,pos,n

每個搜尋操作都返回一個string::size_type值,表示匹配發生位置的下標。如果搜尋失敗,則返回一個名為string::npos的static成員。npos的型別為const string::size_type

搜尋是對大小寫敏感的

指定在哪裡開始搜尋

迴圈地搜尋子字串出現的所有位置:

string::size_type pos=0;
//每步迴圈查詢name中下一個數
while((pos=name.find_first_of(numbers,pos))
       !=string::npos){
    cout<<"found number at index: "<<pos
        <<" element is "<<name[pos]<<endl;
        ++pos;   //移動到下一個字元,否則會無限迴圈
}

4、compare函式

類似於C語言中的strcmp,根據s是等於、大於還是小於引數指定的字串,s.compare返回0、整數或負數

compare的幾種引數模式
S2
pos1,n1,s2
pos1,n1,s2,pos2,n2
cp
pos1,n1,cp
pos1,n1,cp,n2

5、數值轉換

string和數值之間的轉換
to_string(val) 返回數值val的string表示,val可以是任何算術型別
stoi(s,p,b) 返回s的起始子串(表示整數內容)的數值,返回型別分別對應;b表示轉換所用的基數,預設值是10;p是size_t指標,用來儲存s中第一個非數值字元的下標,預設為0,即,函式不儲存下標
stol(s,p,b)
stoul(s,p,b)
stoll(s,p,b)
stoull(s,p,b)
stof(s,p) 返回s的起始子串(表示浮點數內容)的數值
stod(s,p)
stold(s,p)

如果string不能轉換為一個數值,這些函式丟擲一個invalid_argument異常。如果轉換得到的數值無法用任何型別來表示,則丟擲一個out_of_range異常。

六、容器介面卡

除了順序容器,還定義了三個順序容器介面卡:stack(棧)、queue(佇列)和priority_queue。

本質上,介面卡是一種機制,能使某種事物的行為看起來像另外一種事物一樣。一個容器介面卡接受一種已有的容器型別,使其行為看起來像一種不同的型別。

所有容器介面卡都支援的操作和型別
size_type
value_type
container_type
A a;
A a(c);
關係運算符
a.empty()
a.size()
a.swap(b)
swap(a,b)

定義一個介面卡

//deq是一個deque<int>
stack<int> stk(deq);  //從deq拷貝元素到str

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

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

介面卡的容器限制:

stack——(不能)array、forward_list

queue——list、deque——(不能)vector

priority_queue——vector、deque——(不能)list

棧介面卡

stack型別定義在stack標頭檔案中。每個容器介面卡都基於底層容器型別的操作定義了自己的特殊操作,但是我們只能使用介面卡操作,而不能使用底層容器型別的操作。

棧獨有的操作
棧預設基於deque實現,也可以在list或vector上實現
s.pop()
s.push(item)
s.emplace(args)
s.top()

佇列介面卡

queue和priority_queue介面卡定義在queue標頭檔案中。

priority_queue允許我們為佇列中的元素簡歷優先順序。新加入的元素會排在所有優先順序比它低的已有元素之前。

queue和priority_queue獨有的操作

queue預設基於deque實現,priority_queue預設基於vector實現

queue也可以用list或者vector實現,priority_queue也可以用deque實現

q.pop()
q.front()
q.back()
q.top() (只適用於priority_queue)
q.push(item)
q.emplace(args)