引言:
上次我們學習了容器vector
的使用及其底層實現,今天我們再來學習一個容器list
,
這里的list
可以參考我們之前實現的單鏈表,但是這里的list
是雙向循環帶頭鏈表,下面我們就開始list
的學習了。
一:list
的介紹
list的介紹文檔:
二:list
的使用
1. 約定:
關于list
的接口眾多,下面我們只介紹一些比較常用的接口,其他接口同學們可以自行去list
的介紹文檔中學習。
2. list的構造:
- list (size_type n, const value_type& val =value_type()):構造的
list
中包含n
個值為val
的元素。 - list():構造空的
list
。 - list (const list& x):拷貝構造函數。
- list (InputIterator first, InputIterator last):用
[first, last)
區間中的元素構造
list
。
代碼演示:
3.list iterator
的使用
注:這里大家先將list的迭代器理解為指向節點的一個指針。
- begin:返回第一個元素的迭代器。
- end:返回最后一個元素下一個位置的迭代器。
- rbegin:返回第一個元素的
reverse_iterator
,即end
位置。 - rend:返回最后一個元素下一個位置的
reverse_iterator
,即begin
位置。
代碼演示:
注:
begin
與end
為正向迭代器,對迭代器執行++
操作,迭代器向后移動rbegin(end)
與rend(begin)
為反向迭代器,對迭代器執行++
操作,迭代器向前移動
4.list capacity
的使用
- empty:檢測
list
是否為空,為空返回true
,反之則返回false
。 - size:計算
list
中有效數據的個數。
代碼演示:
5. list element access
- front:返回
list
的第一個節點中值的引用。 - back:返回
list
的最后一個節點中值的引用。
代碼演示:
6. list modifiers
- push_front:在
list
首元素前插入值為val
的元素。 - pop_front:刪除
list
中第一個元素。 - push_back:在
list
尾部插入值為val
的元素。 - pop_back:刪除
list
中最后一個元素。 - insert:在
list position
位置中插入值為val
的元素。 - erase:刪除
list position
位置的元素。 - swap:交換兩個
list
中的元素。 - clear:清空
list
中的有效元素。
代碼演示:
(1)push_back(front)
pop_back(front)
(2)erase
和insert
swap
和clear
7. list
迭代器失效問題
前面說過,此處大家可將迭代器暫時理解成類似于指針,迭代器失效即迭代器所指向的節點的無效,即該節點被刪除了。因為list
的底層結構為帶頭結點的雙向循環鏈表,因此在list
中進行插入時是不會導致list
的迭代器失效的,只有在刪除時才會失效,并且失效的只是指向被刪除節點的迭代器,其他迭代器不會受到影響。
這里想要解決迭代器失效的話,處理方式和之前是一樣的,需要對其重新進行賦值:
三:list
的模擬實現
1. 思考:
這里在模擬實現list
時就和vector
的實現有所區別了,之前實現的vector
的本質上一個數組,其迭代器和原生指針沒什么區別,對其迭代器解引用就可以拿到對應的數據,但是這里的list
里面存儲的是一個個節點,因此這里的迭代器并不是單純的原生指針,你對節點指針解引用的話拿到的是一個節點,并不能直接拿到節點里的數據,但是這不是我們期望的,按照我們的邏輯是解引用節點指針是要拿到這個節點指針對應節點里面的數據,因此這里在模擬實現的時候就比之前要復雜一點,牽扯到迭代器的構造以及有關迭代器的一些重載。
2.基本框架:
(1)節點結構:
(2)迭代器結構:
(3)鏈表結構:
3. 迭代器結構完善
(1)*
重載:
(2) 前置++和后置++重載:
(3) 前置- - 和后置 - - 重載:
(4) 比較運算符重載:
4. 構造函數
注:這里在構造的過程中由于會對e進行解引用,因此這里要用引用。
5. begin
和 end
6. insert
函數
注:由于這里除了數據的申請,其他的邏輯和之前實現的雙向鏈表沒啥區別,就不細講了。
7. push_back
函數
注:這里就直接復用insert
即可。
8. push_front
函數
9. erase
函數
注:
- 這里需要注意頭結點我們是不能動的。(大哥動不得)
- 為解決
erase
引起的迭代器失效問題,這里我們的erase
給了返回值。
10. pop_back
函數
注:這里就是直接復用erase
即可。
11. pop_front
函數
12. clear
函數
13. 析構函數
14. size
函數
注:為了避免重復遍歷鏈表來計算個數,因此這里我們就來維護一個成員變量_size
,因此之前的插入和刪除都需要維護_size
15. swap
函數
四: 拷貝構造函數引起的問題及解決方案
1. 問題的引發
我們這里在實現拷貝構造函數的時候出現了以下問題:
那么為什么會出現這個問題呢?
2. 思考:
因為拷貝構造函數這里給的參數是const
類類型,但是這里在構造的過程中用到了范圍for
,我們知道,范圍for
本質上就是迭代器的替換,并且因為需要進行解引用,所以這里的范圍for我們用的是引用,但是由于傳入參數是const
類型,這里需要const
類型的迭代器來支持,所以這里才會出現問題,因此我們就需要實現const
類型的迭代器。
3. 解決方案:
這里由于const
迭代器和普通類型的迭代器就一點不同,如果再封裝一個類的話就太冗余了,因此這里就可以從模版下手:
這個時候就解決了上面出現的問題,而且這樣使得我們在使用不同的迭代器時編譯器能夠自動推導類型。
4. 賦值運算符重載:
五:補充內容— -> 重載
1. 引入:
首先我們來看vector
存儲自定義類型時的訪問:
下面再來看用list
來存儲自定義類型時的訪問:
可以看到用list
來存儲自定義類型的時候就不能用->
來訪問,這是為什么呢?
還是回到前面提到的迭代器的區別,vector
的迭代器就可以理解為原生指針,用->
就可以拿到其里面的數據,而list
這里的迭代器->
的話拿到的是節點,而不是數據,因此,這里這樣寫就有問題,那么我們看一下標準庫里面的list的->
:
可以看到標準庫里面的list
就可以通過->
來訪問數據,這是因為標準庫里的list
對->
進行了特殊處理,因此我們這里也需要對->
進行重載才可以。
2. ->
重載:
注:這里->
的重載看著就有點奇怪了,注意這里返回的是指向數據的指針(數據的地址)
注意這里的兩種寫法都可以,第一種方式只是編譯器對其進行了特殊處理,其實第二種寫法更符合我們理解的邏輯。