1. 程式人生 > >《STL原始碼剖析》學習之迭代器

《STL原始碼剖析》學習之迭代器

一、迭代器作用

       在設計模式中有一種模式叫迭代器模式,簡單來說就是提供一種方法,在不需要暴露某個容器的內部表現形式情況下,使之能依次訪問該容器中的各個元素,這種設計思維在STL中得到了廣泛的應用,是STL的關鍵所在,通過迭代器,容器和演算法可以有機的粘合在一起,只要對演算法給予不同的迭代器,就可以對不同容器進行相同的操作。在這裡提到了一個叫迭代器的東西,說得簡單一點,就是一種指標,學習C和C++的同學肯定不會對指標感到陌生,這確實是個讓我們又愛又恨的東西。不曾忘記,因為指標操作引起的記憶體洩露、段錯誤而徹夜難眠;也不曾忘記,因為指標的靈活和強大,讓我們自由地遊刃在記憶體之中。以下以演算法find為例,展示了容器、演算法和迭代器如何合作:

template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, const T &value)
{
    while (first != last && *frist != value)
        ++first;
    return first;
}

        從以上程式碼可以看到,演算法通過傳入的迭代器,順序訪問容器中的元素,尋找並返回符合條件的元素。通過向演算法傳入指向不同容器的迭代器,實現了演算法以相同的邏輯對不同容器的訪問。

二、 迭代器的重要特性

2.1 迭代器是一種智慧指標

        與其說迭代器是一種指標,不如說迭代器是一種智慧指標,它將指標進行了一層封裝,既包含了原生指標的靈活和強大,也加上很多重要的特性,使其能發揮更大的作用以及能更好的使用。迭代器對指標的一些基本操作如*、->、++、==、!=、=進行了過載,使其具有了遍歷複雜資料結構的能力,其遍歷機制取決於所遍歷的資料結構。下面上一段程式碼,瞭解一下迭代器的“智慧”:

template<typename T>
class Iterator
{
public:
    Iterator& operator++();

    //...

private: 
    T *m_ptr;
};

       對於不同的資料容器,以上Iterator類中的成員函式operator++的實現會各不相同,例如,對於陣列的可能實現如下:

//對於陣列的實現
template<typename T>
Iterator& operator++()
{ 
    ++m_ptr; 
    retrun *this;
}

       對於連結串列,它會有一個類似於next的成員函式用於獲取下一個結點,其可能實現如下:

//對於連結串列的實現
template<typename T>
Iterator& operator++()
{
    m_ptr = m_ptr->next();//next()用於獲取連結串列的下一個節點 
    return *this;
}

        從上面三段程式碼可以看到,迭代器的operator++操作對於不同資料結構,就會有不同的實現,這是C++原生所做不到的,也是迭代器為什麼是一種智慧指標的原因之一。看到這裡,可能會有疑問,前面說迭代器針對不同的資料結構會有不同的實現,那意思是不是指迭代器一定要在資料結構內實現呢?答案是否定的,迭代器只要瞭解資料結構的實現,是可以在資料結構外定義,但是卻有一定的代價。

2.2 不同的容器都有專屬的迭代器

       下面嘗試實現一個自己的迭代器,由於迭代器的作用物件是容器,因此需要首先實現一個容器,下面程式碼展示了一個單向連結串列的實現

template<typename T>
class ListItem
{
public:
    ListItem() { m_pNext = 0;}
    ListItem(T v, ListItem *p = 0) { m_value = v; m_pNext = p;}
    T Value() const { return m_value;}
    ListItem* Next() const { return m_pNext;}
	
private:
    T m_value;	//儲存的資料
    ListItem* m_pNext;	//指向下一個ListItem的指標
};

template<typename T>
class List
{
public:
    //從連結串列尾部插入元素
    void Push(T value)
    {
       m_pTail = new ListItem<T>(value);
       m_pTail = m_pTail->Next();
    }
	
    //列印連結串列元素
    void Print(std::ostream &os = std::cout) const
    {	
        for (ListItem<T> *ptr = m_pHead; ptr; ptr = ptr->Next())
        os<<ptr->Value<<" ";
        os<<endl;
    }
	
    //返回連結串列頭部指標
    ListItem<T>* Begin() const { return m_pHead;}

    //返回連結串列尾部指標
    ListItem<T>* End() const { return 0;}
	
    //其它成員函式

private:
    ListItem<T> *m_pHead;    //指向連結串列頭部的指標
    ListItem<T> *m_pTail;    //指向連結串列尾部的指標
    long m_nSize;    //連結串列長度
};

       下面程式碼展示了操作以上List容器的一個迭代器的簡單實現:

template<typename T>
class ListIter
{
public:
    ListIter(T *p = 0) : m_ptr(p){}
	
    //解引用,即dereference
    T& operator*() const { return *m_ptr;}
	
    //成員訪問,即member access
    T* operator->() const { return m_ptr;}
	
    //前置++操作
    ListIter& operator++() 
    { 
        m_ptr = m_ptr->Next(); //暴露了ListItem的東西
        return *this;
    }
	
    //後置++操作
    ListIter operator++(int)
    {
        ListIter temp = *this;
        ++*this;
        return temp;
    }
	
    //判斷兩個ListIter是否指向相同的地址
    bool opeartor==(const ListIter &arg) const { return arg.m_ptr == m_ptr;}
	
    //判斷兩個ListIter是否指向不同的地址
    bool operator!=(const ListIter &arg) const { return arg.m_ptr != m_ptr;}
	
private:
    T *m_ptr;
};

       以下為相應的測試程式碼:

int main(int argc, const char *argv[])
{
    List<int> mylist;
	
    for (int i = 0; i < 5; ++i)
    {
        mylist.push(i);
    }
    mylist.Print();	//0 1 2 3 4
	
    //暴露了ListItem
    ListIter<ListItem<int> > begin(mylist.Begin());
    ListIter<ListItem<int> > end(mylist.End());
    ListIter<ListItem<int> > iter;
	
    iter = find(begin, end, 3);//從連結串列中查詢3
    if (iter != end)
        cout<<"found"<<endl;
    else
        cout<<"not found"<<endl;
}

       上面使用迭代器的測試程式碼給人的第一感覺就是好麻煩,首先需要宣告和定義了begin和end兩個ListIter<ListItem<int> >型別的迭代器,分別用來標識所操作容器List的頭部和尾部,這時候暴露了ListItem;在ListIter的實現中,為了實現operator++的功能,我們又暴露了ListItem的函式Next()。另外,細心的你可能發現,演算法find是通過*first != value用來判斷元素是否符合要求,而上面測試程式碼中,first的型別為ListItem<int>,而value的型別為int,兩者之間並沒有可用的operator!=函式,因此,需要另外宣告一個全域性的operator!=過載函式,程式碼如下:

template<typename T>
bool operator!=(const ListItem<T> &item, T n)
{
    return item.Value() != n;
}

       為了實現迭代器ListIter,我們在很多地方暴露了容器List的內部實現ListItem,這違背一開始說的迭代器模式中不暴露某個容器的內部表現形式情況下,使之能依次訪問該容器中的各個元素的定義。為了解決這種問題,STL將迭代器的實現交給了容器,每種容器都會以巢狀的方式在內部定義專屬的迭代器。各種迭代器的介面相同,內部實現卻不相同,這也直接體現了泛型程式設計的概念。

三、迭代器的分類

     在STL中,原生指標也是一種迭代器,除了原生指標以外,迭代器被分為五類:

  • Input Iterator
此迭代器不允許修改所指的物件,即是隻讀的。支援==、!=、++、*、->等操作。
  • Output Iterator
允許演算法在這種迭代器所形成的區間上進行只寫操作。支援++、*等操作。
  • Forward Iterator
允許演算法在這種迭代器所形成的區間上進行讀寫操作,但只能單向移動,每次只能移動一步。支援Input Iterator和Output Iterator的所有操作。
  • Bidirectional Iterator
允許演算法在這種迭代器所形成的區間上進行讀寫操作,可雙向移動,每次只能移動一步。支援Forward Iterator的所有操作,並另外支援--操作。
  • Random Access Iterator
包含指標的所有操作,可進行隨機訪問,隨意移動指定的步數。支援前面四種Iterator的所有操作,並另外支援it + n、it - n、it += n、 it -= n、it1 - it2和it[n]等操作。

     迭代器的分類和繼承體系可用下面的圖表示:

       為什麼要將迭代器分為這五類,而且為什麼要將它們設計為這種繼承體系呢?在學習C++繼承的時候,我們知道,位置繼承體系越後的類,功能越強大,但是考慮的東西也會越多,體型也會越臃腫。為了提供最大化的執行效率,STL在設計演算法時,會盡量提供一個最明確最合適的迭代器,在完成任務的同時,也儘量提高演算法的效率。假設有個演算法可接受Forward Iterator,此時,你可以傳入一個Random Access Iterator,因為Random Access Iterator也是一種Forward Iterator,但是可用並不代表最合適,我們只需要Forward Iterator的功能,卻傳入了更多屬於Random Access Iterator的在這裡沒有用到的功能,一定程度上會降低了演算法的效率。
       以函式advance為例,說明這種迭代器分類和繼承體系的好處,此函式有兩個引數,分別是迭代器i和數值n,主要作用是將i前進n距離,下面會有advance函式的三份定義,一份是針對Input Iterator,一份針對Bidirectional Iterator,另一份針對Random Access Iterator,而針對Forward Iterator的實現和針對Input Iterator的實現是一樣的,因此沒有單獨列出,程式碼如下:

template<typename InputIterator, typename Distance>
void advance_II(InputIterator &i, Distance n)
{
    //單向逐一前進
    while (n--) ++i;
}

template<typename BidirectionalIterator, typename Distance>
void advance_BI(BidirectionalIterator &i, Distance n)
{
    //雙向逐一前進
    if (n >= 0)
      while (n--) ++i;
    else
      while (n++) --i;
}

template<typename RandomAccessIterator, typename Distance>
void advance_RAI(RandomAccessIterator &i, Distance n)
{
    //雙向跳躍前進
    i += n;
}

       對於Random Access Iterator來說,當程式呼叫advance_RAI函式時,只需O(1)的時間複雜度;當程式呼叫advance_II()函式時,操作非常缺乏效率,原本只需O(1)時間複雜度的操作竟然變成為O(N)。因此,為了最大限度提高效率,STL將迭代器進行了明確的分類,同時將其設計為一種繼承關係,以提高演算法的可用性,如果某個迭代器沒有相應版本的演算法,通過型別轉換,可以使用父類版本的演算法,儘管效率不一定最優,但至少可用。

四、迭代器的使用例項

       前面說了這麼多,下面簡單展示一下迭代器的使用:

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;

int main(int argc, const char *argv[])
{
    int arr[5] = { 1, 2, 3, 4, 5};
 
    vector<int> iVec(arr, arr + 5);//定義容器vector
    list<int iList(arr, arr + 5);//定義容器list

    //在容器iVec的頭部和尾部之間尋找整形數3
    vector<int>::iterator iter1 = find(iVec.begin(), iVec.end(), 3);
    if (iter1 == iVec.end())
        cout<<"3 not found"<<endl;
    else
        cout<<"3 found"<<endl;

    //在容器iList的頭部和尾部之間尋找整形數4
    list<int>::iterator iter2 = find(iList.begin(), iList.end(), 4);
    if (iter2 == iList.end())
        cout<<"4 not found"<<endl;
    else
        cout<<"4 found"<<endl;

    return 0;
}

       從上面迭代器的使用中可以看到,迭代器依附於具體的容器,即不同的容器有不同的迭代器實現,同時,我們也看到,對於演算法find來說,只要給它傳入不同的迭代器,即可對不同的容器進行查詢操作。通過迭代器的穿針引線,有效地實現了演算法對不同容器的訪問,這也是迭代器的設計目的。