談談 C++ STL 中的迭代器

ZhiboZhao發表於2021-08-02

C++中的迭代器和指標

在前面的內容中我們簡單講過,STL主要是由三部分組成

  1. 容器(container),包括vector,list,set,map等
  2. 泛型演算法(generic algorithm),用來操作這些容器,包括find(),sort(),replace()等
  3. 迭代器(iterator),泛型演算法操作容器的工具,是連線容器和演算法的粘合劑

一、迭代器(iterator)

在介紹STL之前,首先了解一下什麼是迭代器。STL中的泛型演算法提供了很多可作用於容器類以及陣列類上的操作,這些演算法與他們想要操作的元素型別無關(int,double,string等)且與容器類獨立(vector,list,array等)。很容易想到,泛型演算法通過函式模板(function template)技術來達到 “與操作物件的元素型別無關” 的目的,而實現與 “容器無關” 則不直接在容器本身進行操作,而是藉助一對 iterator 來標示我們要進行迭代的元素範圍。我們通過一個具體的問題來引入 iterator 的設計動機。

問題描述:
給定一個儲存整數的vector,如果vector記憶體在目標值value,就返回指向該值的指標;否則返回0。

首先很容易想到的一種做法是:

int* find(vector<int>& nums, const int& value){
    for(int i=0; i<nums.size(); i++){
        if(nums[i]==value)	return &nums[i];
    }
    return 0;
}

接下來我們使用函式模板技術來擴充這個函式的功能,使其能夠處理不同型別的資料型別:

template <typename T>
T* find(vector<T>& nums, const T& value){
    for(int i=0; i<nums.size(); i++){
        if(nums[i]==value)	return &nums[i];
    }
    return 0;
}

緊接著我們會想,函式能不能同時實現對vector和array型別的輸入進行查詢,一種解決辦法是通過函式過載的技術來實現,但是如果要實現很多種型別的容器,那麼便需要寫很多個過載函式。另一種更好的解決辦法是:我們便不將容器本身作為引數傳入,而是傳入需要處理的資料的開始和結束位置,這樣便對任意的輸入有了普遍性。

對於 array 陣列型別的資料 int array[10]而言,array=&arran[0] 即陣列名就代表陣列的開始地址,也代表陣列第一個元素的地址。由於在傳遞時,無論是 array[10],array[24],都不會傳遞 array 的結束地址,因此需要額外傳遞一個引數 size,或者一個結尾地址,那麼程式便可以寫成:

方法一:傳遞陣列的大小作為引數來標示結束為止
template <typename T>
T* find(const T* array, int size, const T& value){...}
方法二:傳遞陣列的結尾指標來標示結束為止
template <typename T>
T* find(const T* begin, const T* end, const T& value){...}

上面我們已經完成了 array 型別輸入的find 函式的編寫,下面我們就來簡單看一下呼叫方式:

int 	in_array[5] = {1, 4, 5, 7, 2};
double 	do_array[7] = {1.5, 2.7, 3.2, 4, 7, 2, 1.7};
int* f1 = find(array, 5, 4);	//採用第一種呼叫方式,傳入開始位置和陣列大小
int* f2 = find(do_array, do_array+7, 2); //採用第二種呼叫方式,傳入開始位置和結束位置即[開始位置,結束位置)

那麼針對 vector 型別的容器,它的儲存方式跟 array 相同,都是以一塊連續的記憶體儲存所有元素,因此可以採用跟 array 相同的方式來實現 find 函式。但是二者不同的是:vector 容器可以為空而 array 不能為空,因此:

vector<int> nums;	find(&nums[0], &nums[0+n], value);	//正確
int array[];	//無法對 array[0] 取地址

所以為了避免每次在計算 array 的首地址時,array 為空的情況,抽象出一個新的函式 begin(),具體定義如下:

template <typename T>
inline T* begin(const vector<T>& vec){
    return vec.empty() ? 0:&vec[0];
}

我們以同樣的方式封裝成 end() 函式,返回 vector 的結束地址。因此我們便有了放之四海而皆準的呼叫方式:

find( begin(vec), end(vec), value ); // 開始地址,結束地址,查詢值

再進一步,我們可以嘗試將 find 函式應用在所有的容器型別,但是由於大部分容器(比如:list,map,set)並不是順序儲存,因此 vectorarray 的這種指標定址的方式並不適合其他非連續記憶體空間儲存的容器型別。解決這個問題的方法是,在底層指標的行為之上提供一層抽象,取代程式原本的 “指標直接操作” 方式。我們把底層指標的處理全部放在此抽象層中,將原本的指標操作根據具體的容器型別進行過載,這樣我們便可以處理標準庫所提供的的所有容器類,這便是 iterator 的建立原因。iterator 的操作方式跟指標一樣,但是 iterator 的 ++, !=, * 等運算子是根據具體的容器型別過載過得。對 list 而言,++ 會按照連結串列的方式前進到下一個元素,對 vector 而言,++ 會直接指向下一個記憶體位置。

既然知道了迭代器的實現原理,那麼下面我們來簡單實現一下 `list` 的迭代器:
/*************定義單連結串列的類************/
template<typename T>
class node {
public:
    T value;
    node *next;
    node() : next(nullptr) {}
    node(T val, node *p = nullptr) : value(val), next(p) {}
};
/*************封裝單連結串列***************/
template<typename T>
class my_list {
private:
    node<T> *head;
    node<T> *tail;
    int size;
private:
    //單連結串列迭代器的實現
    class list_iterator {
    private:
        node<T> *ptr; //指向list容器中的某個元素的指標
    public:
        list_iterator(node<T> *p = nullptr) : ptr(p) {}     
        //過載++、--、*、->等基本操作
        //返回引用,方便通過*it來修改物件
        T &operator*() const {
            return ptr->value;
        }
        node<T> *operator->() const {
            return ptr;
        }
        list_iterator &operator++() {
            ptr = ptr->next;
            return *this;
        }
        list_iterator operator++(int) {
            node<T> *tmp = ptr;
            // this 是指向list_iterator的常量指標,因此*this就是list_iterator物件,前置++已經被過載過
            ++(*this);
            return list_iterator(tmp);
        }
        bool operator==(const list_iterator &t) const {
            return t.ptr == this->ptr;
        }
        bool operator!=(const list_iterator &t) const {
            return t.ptr != this->ptr;
        }
    };

public:
    typedef list_iterator iterator; //型別別名
    my_list() {
        head = nullptr;
        tail = nullptr;
        size = 0;
    }
    //從連結串列尾部插入元素
    void push_back(const T &value) {
        if (head == nullptr) {
            head = new node<T>(value);
            tail = head;
        } else {
            tail->next = new node<T>(value);
            tail = tail->next;
        }
        size++;
    }
    //列印連結串列元素
    void print(std::ostream &os = std::cout) const {
        for (node<T> *ptr = head; ptr != tail->next; ptr = ptr->next)
            os << ptr->value << std::endl;
    }
public:
    //操作迭代器的方法
    //返回連結串列頭部指標
    iterator begin() const {
        return list_iterator(head);
    }
    //返回連結串列尾部指標
    iterator end() const {
        return list_iterator(tail->next);
    }
    //其它成員函式 insert/erase/emplace
};

二、容器(container):物之所置也

2.1 順序性容器

  1. vector 以一塊連續的記憶體來存放元素,對 vector 進行隨機訪問很有效率,但是由於 vector 的每個元素都被儲存在距離起始點的固定偏移位置,如果將元素插在任意位置,那麼效率很低。同理,刪除任意位置的元素也缺乏效率;
  2. list 以雙向連結而非連續記憶體來儲存內容,因此實現 list 內部任意位置的插入和刪除操作效率很高,但是如果要對 list 進行隨機訪問,則效率很低;
  3. deque 與 vector 一樣都是使用連續記憶體來存放元素,deque 在最前端插入元素,最後端刪除元素。

2.2 關聯容器

map:被定義為一對(key-value)數值,其中的 key 通常是個字串,扮演索引的角色,另一個數值是 valuevaluekey 通過對映函式 f 得到的值,可以記錄 key 出現的次數等。map 物件中 keyfirst 物件來表示,valuesecond 物件來表示,即:

map<string, int>::iterator it = words.begin();
while(it != words.end()){
	cout << "key:" << it->first << "\nvalue:" << it->second << endl;
}

查詢map是否存在 key 有三種方法:

/**********************方法一**********************/
string target="a";
int count = words[target]; // 查詢words中是否存在 "a"
/**********************方法二**********************/
string target="a";
map<string, int>::iterator it = words.find(target);
/**********************方法三**********************/
string target="a";
int count = words.count(target);

其中:

  1. 方法一:如果 words 中存在 "a",count 中就記錄了 "a" 的個數。**但是,當 words 中本來就不含有 "a" 時,該方法會通過 words[target] 自動新增進 words,此時 words[target]=0 **,因此該方法不建議用在查詢中;
  2. 方法二:類似於上一節中寫的 find 函式,當找到該元素時,返回指向該元素的迭代器,否則返回指向最有一個元素的後一個位置的迭代器 words.end()。所以通過判斷函式返回值是否為 words.end() 便可以知道結果;
  3. 方法三:count 會返回某個特定項在 map 內的個數。

set:set的操作方式跟map差不多,set中相當於只記錄了 key 值。

無論是 map 還是 set,在進行插入元素後會對其中的元素進行排序,因此當不需要排序時,需要定義:

unordered_map<pair<type1, type2>>my_map;
unordered_set<pair<type1, type2>>my_set;

2.3 所有容器的共通操作

  1. ==, != :返回 true 或者 false, 判斷是否相等;
  2. empty():在容器為空時返回 true,否則返回 false
  3. size():返回容器內的元素個數;
  4. clear():清空容器內的元素,但是保留容器的長度;
  5. begin():返回容器第一個元素的 iterator
  6. end():返回容器最後一個元素的後一個位置的 iterator
  7. insert():在容器的指定位置插入元素;
  8. erase():在容器的指定位置刪除元素;
  9. push_back():在容器的末端插入元素;
  10. pop_back():在容器的首端取出元素.......

上面列舉的都是比較常見的一部分,由於精力有限難免有錯誤和疏漏,歡迎大家在閱讀的同時對文中的不當之處進行指正、補充,不勝感激 !

三、參考內容

  1. 《Essential C++》中文版,侯捷譯
  2. https://www.cnblogs.com/wengle520/p/12492708.html

相關文章