1. 程式人生 > >C++:順序容器與迭代器

C++:順序容器與迭代器

0.順序容器與迭代器的概念

容器:一個容器實際上就是一組相同資料型別元素的集合。相當於是對C語言中的內建陣列的一種泛化。順序容器為程式設計師提供了控制元素儲存和訪問順序的能力。

迭代器:迭代器是C++為了更好的切合容器的使用而引入的一種特殊的資料型別。迭代器在功能上和C語言的指標十分相像,可以快速方便的訪問、查詢、修改容器裡邊的元素。

1.常用順序容器及其迭代器

容器型別 資料結構 支援的操作
vector 可變長陣列 快速隨機訪問 在尾部插入刪除元素很快
deque 雙端佇列 快速隨機訪問 在頭、尾插入刪除元素很快
list 雙向連結串列 雙向順序訪問 任意位置插入刪除元素都很快
forward_list 單向連結串列 單向順序訪問 任意位置插入刪除元素都快
array 固定大小陣列 快速隨機訪問 不能新增或者刪除元素
string 可變長字元陣列 快速隨機訪問 尾部插入刪除元素很快

根據上表可以得出一些結論:

  1. 除固定大小的array外,其他容器都可以新增刪除元素,也就是說提供了高效、靈活的記憶體管理。

  2. string和vector將元素儲存在連續的記憶體空間,可以快速訪問資料,但是刪除與插入時會移動很多元素所以會很慢。

  3. list和forward_list這兩個容器的設計目的是令容器在任意位置插入和刪除元素都很快,作為代價,在這兩種容器中查詢元素比較慢。

  4. deque更為複雜。它支援如同string和vector的快速隨機訪問,在中間位置刪除新增元素花費很高,但是在deque的頭尾插入元素是很快的,幾乎和list和forward_list花費相當。

一般來說,我們都選用vector作為常用容器,除非有更好的理由更換。
下面簡單的介紹一下如何定義一個容器。
既然是容器,那麼肯定能容納不同的資料元素。我們希望vector既可以容納int也能容納double,而且這應當是兩種不同的容器型別才可以。所以C++將所有容器都設定為模板類,通過下列方式定義容器:

vector<typename> vec;

這條語句的意思是:vec是一種vector的容器,其中容納的元素型別是typename的
舉個例子:

vector<int> intVec;
vector<double> doubleVec;

實際上,typename不必須是系統內建的型別,可以是使用者自定義的類、結構體、範型

vector<myClass> vec;
vector< vector<int> > intVec;

該例子中的第二個,表示intVec是一個vector容器,容器中容納的元素是一個int的vector(這裡相當於是二維陣列)

下面簡單說一下迭代器。

因為迭代器可以對容器的元素訪問,所以迭代器的型別必須與容器型別相匹配。
所以,迭代器的定義應該按照下述方式:

vector<int>::iterator it1;
vector<double>::iterator it2;

it1只能指向int型vector,it2只能指向double型vector。

通常,我們都是用一對迭代器來指示某一個範圍。
兩個迭代器分別指向同一個容器當前元素或者是尾元素的下一個位置
可以形象的表示為:
[begin,end)(值得一提的是,begin和end可以指向同一個元素)
表示範圍從begin開始到end之前結束。
於是,給出定義,當且僅當兩個迭代器begin和end滿足下列所有條件時,我們稱begin和end為一個迭代器對。

  1. 它們指向同一個容器的元素,或者是容器最後一個元素之後的位置

  2. 我們可以通過反覆遞增begin到達end,即end不在begin之前。

    在這兩個條件的約束下,我們可以利用迭代器迴圈處理一個範圍內的元素

while(begin != end){//注意這裡不可以寫"<"或者">",指標迭代器不可以比較大小 
    *begin = val;//正確,範圍非空,begin指向一個元素
    //迭代器是一種特殊型別的指標,因此可以通過解引用符*來訪問迭代器指向記憶體中的資料
    ++begin;
}

對於標準容器,可以直接使用成員函式begin和end獲得容器的迭代器。
例如:

vector<int>::iterator it1 = intVec.begin();
vector<double>::iterator it2 = doubleVec.end();

2.容器的定義與初始化

每個容器型別都定義了一個預設建構函式。除array外,其他容器的預設建構函式都會建立一個指定型別的空容器

構造方式 解釋
Container c 呼叫預設建構函式。如果c是array,c中元素按照值初始化方式,否則c為空
Container c1(c2) c1初始化為c2的拷貝。c1和c2必須同種容器、容器元素型別相同,對於array型別,c1和c2必須等長
Container c{a,b,..} 列表初始化。要求元素型別相容(即可以存在隱式轉換)。對array型別,列表中元素數目必須小於等於c的長度,遺漏元素全部預設初始化(int型別就會賦值為0,因為是列表初始化)
Container c1(c2.beg,c2.end) c初始化為[beg,end)範圍內的拷貝,元素型別必須相容,c1和c2的容器型別不必相同,array不適用
Container seq(n) seq是順序容器,包含n個元素,對元素進行值初始化,如果元素是某類的物件,呼叫預設建構函式,string不適用
Container seq(n,t) 將n個元素初始化為值t

舉幾個例子:

vector<int> intVec1;//預設建構函式,空的vector;

vector<int> intVec2 = {1,2,3,4,5};//列表初始化

vector<int> intVec3(intVec2);//拷貝初始化

vector<int> intVec4(10);//seq(n)

vector<string> strVec1(10,"i am ok");//seq(n,t)

list<string> strLis1 = { "Milton", "Shakespare", "Austen" };

vector<string> strVec2(strLis1.begin(),strLis1.end());//strVec2與strLis1雖然容器型別不同,但是容器內元素型別相同

vector<double> doubleVec1 = {1.0,2.0};

list<int> intLis1(doubleVec1.begin(),doubleVec1.end());//這裡doubleVec1和intLis1容器型別不同,元素型別不同,但是元素型別是相容(double可以隱式轉換為int)的因此編譯成功

3.容器常用函式

  • assign 賦值函式
    常規的賦值是通過賦值符號“=”完成的。但是“=”兩邊容器型別不匹配時無法使用“=”符號,為此有了更為便利的內建函式assign,通過迭代器操作。
    舉個例子:
list<string> lis = { "Milton", "Shakespare", "Austen" };
vector<string> vec;
vec.assign(lis.begin(),lis.end());

這樣就完成了對vec的賦值。
需要注意的是,賦值運算會使得原本容器(vec)的迭代器、引用、指標失效
也就是說:

list<string> lis = { "Milton", "Shakespare", "Austen" };
vector<string> vec;
vector<string>::iterator iter3 = vec.begin();
vec.assign(lis.begin(),lis.end());//這裡是用了assign賦值,賦值運算會使迭代器、引用、指標失效
//cout << *iter3 << endl;//這裡如果不註釋掉,執行會崩壞,原因是assgin後使得iter3失效,不在指向vec.begin()
//經測試iter3的地址沒有發生改變的,但是對iter3解引用不是我們想看到的Milton而是系統崩潰,實際上vec.begin()在assign後發生了改變
//以前的iter3所指向的記憶體不可以被訪問,也就是我們這裡所說的迭代器失效
  • swap交換函式
    swap函式交換兩個型別相同的容器的內容,呼叫該swap兩個容器的元素會發生互換。swap保證交換很快,因為實際上並不是元素髮生了交換,而是交換了兩個容器的內部資料結構(指標)。所以swap操作後容器的迭代器並不會發生改變。
    看一個例子:
vector<string>::iterator it1 = vec.begin();
vector<string>::iterator it2 = vec2.begin();
cout << "before swap  vec[0] at : " << &vec[0] << " and  vec.begin() is " << *vec.begin() << endl;
cout << "before swap vec2[0] at : " << &vec2[0] << " and vec2.begin() is " << *vec2.begin() << endl;
swap(vec, vec2);
//vec.swap(vec2); 
cout << "after  swap vec2[0] at : " << &vec2[0] << " and  vec2.begin() is : " << *vec2.begin() << endl;
cout << "after  swap  vec[0] at : " << &vec[0] << " and vec.begin() is : " << *vec.begin() << endl;

輸出結果如下:

before swap vec[0] at : 002F8190 and  vec.begin() is Milton
before swap vec2[0] at : 002FBC60 and vec2.begin() is i
after  swap vec2[0] at : 002F8190 and  vec2.begin() is : Milton
after  swap  vec[0] at : 002FBC60 and vec.begin() is : i

由此我們可以看出,swap交換了兩個容器的內部指標。
這樣指向原來指標的迭代器自然不會失效。
詳細說來:

假定iter在swap前指向vec[3],swap後iter指向vec2[3]
(因為vec[3]和vec2[3]的地址發生了交換)
before swap
swap之後:
這裡寫圖片描述
可以看出來iter並沒有失效

  • insert插入函式

    insert函式可以對容器插入一個或者幾個元素。
    基本的語法格式如下:

vec.insert(vec.begin(), "hello");//在迭代器之前的位置插入hello

除了第一個迭代器引數以外,insert函式還可以接受更多的引數

vec.insert(vec.end(), 10, "hello");
//vec的末尾插入10個“hello”元素
vec.insert(vec.end(), { "xx", "yy", "zz", "kk" })
//在vec後邊插入四個string,分別為"xx","yy","zz","kk"
vec.insert(it1,it2,it3)//將[it2,it3)範圍內的元素插入到it1之前

看最後一個例子:
c++標準要求要拷貝的範圍不能指向與目的位置相同的容器

vec.insert(vec.begin(),vec.begin(),vec.end());

也就是說這種方式是不允許的。親測有些編譯器可以通過,那是編譯器的實現問題,不要採用這種方式。

insert()的返回值將是新插入元素的迭代器,我們可以利用這個方法,不斷的在某一個位置(一般來說是begin位置)之前插入元素:

auto it = vec.begin();
while (cin >> word){
    it = vec.insert(it, word);//相當於push_front
}

這裡需要強調的是:
insert()操作不一定會引起迭代器失效!
如果插入新的元素後需要重新分配記憶體,那麼所有迭代器失效。
如果插入新的元素後不會重新分配記憶體,那麼插入位置之前的迭代器不會失效,插入位置之後的迭代器失效。

因此,下述程式碼是嚴重錯誤:

void insertDoubleValue(vector<int> &iv, int some_val)
{
    vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2 ;
    while (iter != mid){
        if (*iter== some_val)
            iter = iv.insert(iter, 2 * some_val);
        else ++iter;
    }
}

插入後,iter之後的迭代器將發生改變,意味著mid的值會發生改變,iter永遠不等於mid,因此,應當改成:

void insertDoubleValue(vector<int> &iv, int some_val)
{
    vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2 ;
    while (iter != mid){
        if (*mid == some_val)
            mid = iv.insert(mid, 2 * some_val);
        else --mid;
    }
}
  • emplace 構造操作

emplace函式直接構造而非拷貝元素。
呼叫insert或者push函式時,將物件當做引數傳遞,這些物件被拷貝到容器中。
呼叫emplace函式時,將物件傳遞給對應元素型別的建構函式,直接在記憶體中構造物件。
舉個例子:
假如說有如下類:

class Sales_item {
public:
    Sales_item(): units_sold(0), revenue(0.0) { }
    Sales_item(const std::string &book): 
                  bookNo(book), units_sold(0), revenue(0.0) { }
    Sales_item(std::istream &is) { is >> *this; }
private:
    std::string bookNo;      // implicitly initialized to the empty string
    unsigned units_sold;
    double revenue;
};
vector<Sales_item> vec3;

vec3.emplace_back(Sales_item());//呼叫預設建構函式
auto it = vec3.end();
vec3.emplace(it, Sales_item("999-99999"));//呼叫Sales_item(const & string)

empalce會在容器管理的記憶體空間中直接建立物件,呼叫push則會建立一個臨時物件,然後將其壓入容器。
顯然這種方式更適合將類插入到某容器中,而且會呼叫建構函式,這就需要那個類有合適的建構函式,引數必須能匹配建構函式。

vec3.emplace(it, Sales_item("999-99999",0,0));
//error Sales_item不存在接受三個引數的建構函式

顯然emplace會使迭代器失效

  • erase刪除函式
    erase函式和insert函式很像,可以在容器中刪除一個或者多個元素。
    例如,下面一個迴圈刪除vec中的所有奇數:
vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto it = v.begin();
while (it != v.end()){
    if (*it % 2 == 0)
        it++;
    else
        it = v.erase(it);//返回刪除元素 之後 的迭代器
}

刪除多個元素:

v.erase(it1,it2)//刪除[it1,it2)範圍內元素,並且返回被刪除的最後一個元素的後一個位置
it1 = v.erase(it1,it2);//實際上刪除後,it1 == it2
v.erase(v.begin(),v.end() - 3);//保留最後三個元素刪除其餘元素

erase操作使得被刪除元素之前的迭代器、引用、指標有效,之後的元素迭代器等會失效。
並且,erase操作總會使尾後迭代器失效,因此刪除操作時儲存尾後迭代器將引起災難性錯誤。

auto it = v.end();
auto x = v.begin();
while (x != it){
    if (*x == 5)
        x = v.erase(x);
    else
        x++;
}

這段程式碼將會引起災難性錯誤,大多數都會進入死迴圈。因為一旦完成一次刪除操作,尾後迭代器it失效了,x永遠不會 == it,將會死迴圈,正確的方法應當是:

auto x = v.begin();
while (x != v.end()){
    if (*x == 5)
        x = v.erase(x);
    else
        x++;
}