STL第三章迭代器收貨

一、爲什麼需要traits

首先看一下如下例子:

template<class T>
class test
{
public:
	static T a;
};

template<class M>
void show()
{
	cout << typeid(M::a).name() << endl;
}

int main()
{
	show<test<int>>();
	show<test<string>>();
	show<test<short>>();
	system("pause");
}

結果如下:
在這裏插入圖片描述
在show函數中想要得到得到模板M的模板參數類型,可以使用這樣的方法,但是這種方法並不能夠定義一個同樣的變量。那如何才能實現定義同樣的變量呢?那就是使用traits機制。利用traits機制的改版如下:

template<class T>
class test
{
public:
	typedef T value_type;//在所有show函數可能用到的模板中,都加上這個定義
	static T a;
};

template<class M>
void show()
{
	M::value_type a;//這樣,show就能根據模板的不同,甚至同一個模板的不同定義,而定義不同的變量。
	cout << typeid(a).name() << endl;
}

int main()
{
	//同上
	...
}

結果如下
在這裏插入圖片描述
其實traits就是爲了解決模板函數中如何利用模板參數本身的一些信息,比如上面的例子中,就想利用模板參數M的模板參數T來定義變量。
而這種情境在泛化編程中經常遇到。

二、迭代器的作用

在書中,有這樣一句話:STL的中心思想在於——將數據容器和算法分開,彼此獨立設計,最後再以一帖膠着劑將他們撮合在一起。
其中數據容器,個人理解是數據結構,也就是比如線性的數組,鏈表等,關聯式的樹,堆等。可以使用模板類來設計。
算法則可以使用模板函數來設計。
==問題是=:如何讓不止一個數據結構都適用同一個算法的模板函數呢。
舉例來說:我設計了一個查找算法模板函數,如下:

template<class T>
? find(?, ?){...}

輸入參數和返回參數我使用?來表示,因爲按邏輯輸入參數應該是兩個,就是查找的範圍,而範圍用一頭一尾可以表示。但是這個頭和尾我該如何去表達呢?比如數組和雙向鏈表的頭和尾可以用指針來表示。但是哈希表也可以用指針來表示嘛?明顯不行的,如圖所示:
在這裏插入圖片描述
那既然無法通過頭尾指針找到範圍內的所有元素,又如何去執行find函數呢。
所以似乎不同的數據結構,針對同一個算法的模板函數,入口參數無法統一,如果不能統一,那麼數據容器和算法就無法分開,因爲需要針對每一個數據結構,都要特製一種算法。
而在STL中find函數的入口參數就是迭代器,迭代器就是數據容器和算法的粘合劑,能夠統一算法的入口參數。
所以只要每一個數據容器都根據標準自定義迭代器,那麼STL提供的算法的模板函數就可以使用,因爲模板函數都是使用迭代器作爲輸入參數以及返回參數的。

三、迭代器與traits

迭代器和traits結合是非常緊密的,或者我源碼看到現在爲止,所有的traits都是在迭代器中呈現的(不保證traits只在迭代器中使用到
爲何迭代器中使用如此頻繁,因爲首先迭代器自身就是一個模板類,而迭代器作爲模板函數入口參數以及模板參數,完美符合了我在第一點中提到的使用情境,自然使用較多。模板函數中可以通過traits機制,萃取出許多迭代器的特性,並加以利用。

四、迭代器中traits的具體體現

迭代器的相應型別:迭代器的相應型別就是模板函數通過traits機制,可以獲得的信息類型。比如在第一點中,通過萃取機制,可以獲得函數模板的模板入口參數T。
而在stl迭代器中提供五種相應型別,源碼如下:

template <class _Category, class _Tp, class _Distance = ptrdiff_t,
          class _Pointer = _Tp*, class _Reference = _Tp&>
struct iterator {
  typedef _Category  iterator_category;//模板函數可以萃取iterator_category知道這個迭代器是什麼類型的,迭代器類型見第五點
  typedef _Tp        value_type;//迭代器或者說迭代器的模板參數,也就是迭代器所指對象的類型
  typedef _Distance  difference_type;//表示兩個迭代器之間距離的類型,
  //比如說有的相鄰兩個類型之間相差(unsigned int)1,而有的相鄰兩個類型之間相差(int)1,difference_type就是存儲距離類型是unsigned int還是int。
  //不過一般都是使用的ptrdiff_t類型,就是__int64類型
  typedef _Pointer   pointer;//一般都是取value_type*,就是迭代器指向對象的類型的指針
  typedef _Reference reference;//一般都是取value_type&,就是迭代器指向對象的類型的引用
};

這五種迭代器型別是最常用的,如果你想要自定義數據結構,那麼在自定義迭代器時,一定要加入這五種型別,纔可以被模板函數使用。
注意:以上五種型別都可以用來直接定義變量的

專門用來萃取的類

template <class _Iterator>
struct iterator_traits {
  typedef typename _Iterator::iterator_category iterator_category;//迭代器種類
  typedef typename _Iterator::value_type        value_type;
  typedef typename _Iterator::difference_type   difference_type;
  typedef typename _Iterator::pointer           pointer;
  typedef typename _Iterator::reference         reference;
};

在模板函數中,stl又加入了一層萃取封裝,使用方法以count源碼舉例說明:

template <class _InputIter, class _Tp>
typename iterator_traits<_InputIter>::difference_type
count(_InputIter __first, _InputIter __last, const _Tp& __value) {
  typename iterator_traits<_InputIter>::difference_type __n = 0;//特別注意!!!
  for ( ; __first != __last; ++__first)
    if (*__first == __value)
      ++__n;
  return __n;
}

模板函數count想要調用迭代器模板_InputIter中的距離類型來定義一個迭代器模板_InputIter適配的距離變量。
其實這裏使用

typename iterator_traits<_InputIter>::difference_type __n = 0;
//等價於
typename _InputIter::difference_type __n = 0;

問題一:爲什麼需要進行一層額外封裝?
爲了處理這樣的情況,如果對於count函數,我輸入的模板參數不是迭代器,而是一個普通類型的指針,那該怎麼辦?比如說我有一個int類型的數組,頭尾節點都是int*類型的指針,如下的語句:

typename iterator_traits<int *>::difference_type __n = 0;

如果iterator_traits只有如上的一種定義的話,這顯然是不對的,因爲int *是沒有自己定義difference_type類型的。所以這種情況下,需要對iterator_traits進行偏特化,爲了讓普通類型指針也能夠適用這個函數。源碼如下:

template <class _Tp>
struct iterator_traits<_Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef _Tp*                        pointer;
  typedef _Tp&                        reference;
};

template <class _Tp>
struct iterator_traits<const _Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef const _Tp*                  pointer;
  typedef const _Tp&                  reference;
};

如果有指針作爲模板參數,那麼五個相應型別就不在是從迭代器中去得到,而是直接根據指針來得到。
問題2:爲什麼只有指針的偏特化,如果是普通類型變量怎麼辦?
普通類型變量就不可以使用find函數,所有的模板函數中使用迭代器的地方,就不可能使用普通類型變量,最多隻能使用普通類型指針。因爲迭代器就是起到了指針的作用。相當於一個函數需要指針作爲入口參數,你輸入一個變量,那顯然是不行的。
有了上面兩種偏特化定義,下面的程序就對了

typename iterator_traits<int *>::difference_type __n = 0;

五、迭代器型別iterator_category

iterator_category是代表的迭代器指針的類型,迭代器指針類型有五種,如下所示:

  • input_iterator:只讀迭代器,可以讀取迭代器指向的對象
  • output_iterator:只寫迭代器,向迭代器指向對象寫入數值,但是不可以讀取
  • forward_iterator:前向迭代器,可雙向移動
  • bidirectional_iterator:雙向迭代器,並對迭代器指向的對象進行讀取
  • random_access_iterator:隨機訪問迭代器,可以直接跳到容器的任何一個元素處,對其進行讀寫操作。

後三種使用較多,前三種沒有看到stl中有容器使用
問題一:那如何來實現這些區別的呢,其實就是不同類型的迭代器,重載了不同的運算符。
如下圖所示:
在這裏插入圖片描述
注意:->運算符並不是那種迭代器的特例,如果需要可以自定義重載,如果不需要,就不用重載,上圖所示的是屬於這種迭代器必須重載的運算符。

問題二:那程序如何自動區分這些類別呢,或者說如果萃取iterator_category,得到的是什麼呢?
其實得到的是如下五種空類:

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};

五種類型分別對應了一種空類,至於爲什麼使用繼承關係,詳見書98頁。
其中forward_iterator_tag沒有繼承output_iterator_tag,網上有人解答說output_iterator不完全匹配forward_iterator,具體原因不詳。
不過這其實用處不大,很少會用到繼承關係的,因爲使用符合自己類別的效率最高。
在這裏插入圖片描述
而這五種空類需要自己根據自己迭代器的特性來判斷屬於那種,並定義那種空類。

六、迭代器最重要的兩點

上面所講的迭代器和traits方面,都是爲了迭代器和算法函數能夠無縫對接,但是作爲數據容器和算法的粘合劑,如何與數據容器進行無縫對接呢?
這一點很簡單,就是在迭代器中,加入數據容器相關的成員變量。

所以看一個迭代器最重要的就是兩點

  • 迭代器中的相應型別,其中最主要關注的就是這個迭代器是什麼類型的,重載了哪些運算符
  • 迭代器中的成員變量,這些成員變量都是和數據容器相關的。

七、以一個例子來講解一下迭代器使用的全過程

advance函數就是將__i++循環n次

template <class _InputIterator, class _Distance>
inline void advance(_InputIterator& __i, _Distance __n) {
  __advance(__i, __n, iterator_category(__i));
}
//__advance有三種重載形式
//針對只讀迭代器的
template <class _InputIter, class _Distance>
inline void __advance(_InputIter& __i, _Distance __n, input_iterator_tag) {
  while (__n--) ++__i;//由於input_iterator只有++這種運算符,所以只能接收n爲正值
}

//針對雙向迭代器的
template <class _BidirectionalIterator, class _Distance>
inline void __advance(_BidirectionalIterator& __i, _Distance __n, 
                      bidirectional_iterator_tag) {
  if (__n >= 0)//bidirectional_iterator重載了++和--,所以可以接收負值
    while (__n--) ++__i;
  else
    while (__n++) --__i;
}

//針對隨機存儲迭代器的
template <class _RandomAccessIterator, class _Distance>
inline void __advance(_RandomAccessIterator& __i, _Distance __n, 
                      random_access_iterator_tag) {
  __i += __n;//由於random_access_iterator可以任意跳轉,所以更快
}

可以看到,如果將random_access_iterator迭代器使用input_iterator迭代器的函數,那麼效率會非常低
另外,判斷類別的函數iterator_category()源碼如下:

iterator_category(const _Iter& __i) { return __iterator_category(__i); }

__iterator_category(const _Iter&)//返回模板參數_Iter對應的迭代器種類
{
  typedef typename iterator_traits<_Iter>::iterator_category _Category;
  return _Category();//定義了一個iterator_traits<_Iter>::iterator_category空變量,但是足夠讓重載函數識別了。
}

八、STL迭代器的遍歷

容器 迭代器類型 成員變量 備註
vector Random Access Iterator 指向該數組某一位置的普通指針 在linux版本的STL中不僅僅是普通指針
list Bidirectional Iterator 指向鏈表中某一節點的指針
deque Random Access Iterator 共有四個參數:①指向map中控數組的指針②該迭代器指向的元素③該迭代器指向元素所在緩衝區的首地址④該迭代器指向元素所在緩衝區的尾地址
slist Forward Iterator 指向鏈表中的某一節點的指針
RB-tree Bidirectional Iterator 指向紅黑樹中某一節點的指針 這個迭代器也是map,set,multimap,multiset的迭代器
hashtable Forward Iterator 共兩個參數:①迭代器所指向的元素②迭代器指向的元素所屬桶的指針 這個迭代器也是hash_map,hash_set,hash_multimap,hash_multiset的迭代器

注意:

  • 這上面的迭代器都只是SGI版本的迭代器,但是在其他版本中並不一定是一樣的。
  • 但是可以看到的是,基本所有的迭代器中都有一個元素,是指向該容器中某一個元素的指針,所以迭代器有和指針非常類似的行爲,也來源於此。

九、總結

其實迭代器從使用的角度來看,行爲類似於指針,並且有和->操作符,但是從源碼角度看,裏面蘊含的技巧真的很多。首先迭代器是一個類,這個類中重載了和->操作符,所以行爲類似指針,並且類中的成員變量也基本都是指針,也可以理解爲對指針作了一些封裝。收貨很大!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章