摘要:類模擬實現類的基本結構結點類成員變量成員函數迭代器類成員函數解引用運算符重載箭頭運算符重載迭代器前置迭代器后置迭代器的比較鏈表類成員變量成員函數構造函數拷貝構造賦值重載析構函數其他小型接口類的基本結構中是一個雙向帶頭循環鏈表。
xxxxSTL中list是一個雙向帶頭循環鏈表。除了頭結點不存儲有效信息外,其余node結點存儲有效信息。同時,為了防止代碼冗余,對于存儲信息類型不同的問題,將采用模板的方式解決。
xxxxlist需要創建3個類:結點類、迭代器類和鏈表類。
xxxx就像C語言中鏈表的創建,C++的鏈表也需要構建一個類(類似于C的結構體)包含節點中數據信息、下一節點地址和上一節點的地址三部分信息。
結點類成員變量如下:
template<class T>struct _list_node_{ T _val; //存儲數據 _list_node_<T>* _next; _list_node_<T>* _prev;};
xxxx分析我們需要寫的成員函數:發現只有構造函數需要寫,因為在new新的結點時,會自動調用它的構造函數,如果不寫就會產生隨機值。但是結點不會拷貝構造,不會賦值。并且節點的釋放是list控制。所以不需要寫拷貝構造、賦值運算法重載和析構函數。
結點類完整代碼如下:
解釋:由于結點類的成員變量需要被其他類直接訪問,所以要將成員變量設置成公有,所以直接用struct(默認內容為公有public)。下面迭代器類同理
xxxx不同于string、vector兩種容器,list的迭代器并不是原生的指針,而是封裝結點指針成了一個類。
迭代器類代碼如下:
template<class T, class Ref, class Ptr> struct _list_iterator_ //迭代器就是需要淺拷貝,不需要重新寫拷貝構造或者復制 { //析構函數:迭代器只是存一下指針,指針指向的結點由鏈表管理,不需要迭代器去釋放空間 typedef _list_node_<T> node; typedef _list_iterator_<T, Ref, Ptr> self; _list_iterator_(node* pnode = nullptr) :_pnode(pnode) {} Ref operator*() //不加引用就是返回一個臨時變量,不可更改具有常屬性 { return _pnode->_val; } Ptr operator->() { return &_pnode->_val; //it->->_year it->獲得T*,T*->獲得T類中的內容 } bool operator==(const self& s) const { return _pnode == s._pnode; } bool operator!=(const self& s) const { return _pnode != s._pnode; } self& operator++() { _pnode = _pnode->_next; return *this; } self operator++(int) //返回的是臨時對象,所以不返回引用 { self tmp(*this); _pnode = _pnode->_next; return tmp; } self& operator--() { _pnode = _pnode->_prev; return *this; } self operator--(int) //返回的是臨時對象,所以不返回引用 { self tmp(*this); _pnode = _pnode->_prev; return tmp; } //成員變量 node* _pnode; };
xxxx問題一:為什么要這樣做呢?因為由于list并不是序列型容器,而是一個一個結點通過地址連接起來的。而為了保證所有容器迭代器使用方法的統一性,都要有對迭代器++/–/*等操作。++/–會直接訪問上一個結點或者下一個結點的數據;解引用操作則會直接訪問當前結點存儲的數據。如果我們使用原生指針(結點的指針),則無法達到迭代器所要求的結果。因此,我們需要將結點的指針包裝成類,通過運算符重載改變它的行為。達到這樣的目的。
xxxx問題二:為什么迭代器類不寫拷貝構造、賦值重載和析構函數呢?因為迭代器在拷貝構造和賦值的時候,就是希望做到淺拷貝,就是將值進行拷貝,讓他們指向同一塊空間,而不是將指向的內容進行深拷貝。因此編譯器自動生成的淺拷貝就夠了。其次,由于迭代器并沒有自己開辟空間,因此無空間的釋放,所以不需要析構函數。結點的釋放是鏈表的任務
xxxx問題三:迭代器的模板中,為何需要三個模板參數?三個模板參數分別代表什么意思?。我們都知道,list的迭代器不同于string和vector的原生指針類型的迭代器,他是一個類。對于原生指針來說。被const修飾和不被const修飾的兩種指針就是兩種類型。因此對于正常的思維來說,迭代器就應該實現成兩種類,一個是iterator,一個是const_iterator,兩種分開寫。因為如果還是想string、vector一樣單純把const和非const兩種迭代器看作一種,在list類里typedef成const和非const類型的迭代器。這樣就會導致const無效,還是可以更改內容。因為,迭代器仍然是非const,你只是把迭代器的類型名改成了const_iterator,并沒有改變迭代器的實質。所以當調用解引用的操作符重載時,還是會返回T&,還是可以更改。所以第一種解決方法就是將iterator和const_iterator兩種迭代器類型分成兩個類來寫,這樣const修飾的鏈表就會去調用const_iterator的類,非const鏈表就會調用iterator的類。但是這樣就會造成大量代碼的冗余。
xxxx于是,就出現了利用模板的方法,來區分兩種迭代器。
xxxx第一個參數模板class T:代表的是數據的類型。
xxxx第二個參數模板class Ref:代表的是結點中數據的引用。
xxxx第三個參數模板class Ptr:代表的是結點中數據的指針。
具體如下圖:
如果還是有不理解可以通過下面成員函數的解析再次加深理解。
Ref operator*() //不加引用就是返回一個臨時變量,不可更改具有常屬性{ return _pnode->_val;}
xxxx對于string、vector迭代器是原生指針的容器來說。他們的迭代器就是數據的地址,因此解引用可以直接拿到數據。而list并不是,所以需要運算符重載來改變它的行為。可以通過對迭代器解引用直接獲取數據。所以return _pnode->val
xxxx同時,對于返回值,我們需要返回的是一個引用,否則,返回的就是一個臨時變量,具有常屬性,不可改變。并且,如果迭代器是const_iterator所以它的Ref就是const T&就不能對數據進行寫,只能讀。const類型迭代器返回的是T&,就可讀可寫了。
Ptr operator->(){ return &_pnode->val;}
xxxx若迭代器為原生指針,則it->就可以直接獲取it的內容,但是list的迭代器不可以,所以就需要運算符重載改變行為。
xxxx值得注意的是
typedef _list_iterator_<class T, class Ref, class Ptr> selfself& operator++(){ _pnode = _pnode->_next; return *this;}
xxxxself是一個類型,返回的就是迭代器自己。self是typedef出來的。前置–類似就是將_next換成_prev。
self operator++(int){ self tmp(*this); _pnode = _pnode->_next; return tmp;}
xxxx因為返回的是tmp,是臨時變量,所以不能返回引用。因為返回的值是改變之前的值,所以要報錯原來的值,返回再將*this改變。后置–類似,就是將_next換成_prev。
xxxx迭代器的比較比較簡單,所以就不細說了。
template<class T>class list{ typedef _list_node_<T> node;public: typedef _list_iterator_<T, T&, T*> iterator; typedef _list_iterator_<T, const T&, const T*> const_iterator; list() { _head = (node*)malloc(sizeof(node)); _head->_next = _head; _head->_prev = _head; } list(const list<T>& lt) { _head = new node; _head->_next = _head; _head->_prev = _head; for (const T& e:lt) { push_back(e); } } list<T>& operator=(list<T> lt) //現代寫法 { swap(_head, lt._head); return *this; } ~list() { clear(); delete _head; _head = nullptr; } void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } iterator begin() { return _head->_next; } const_iterator begin() const { return _head->_next; } iterator end() { return _head; } const_iterator end() const { return _head; } void push_back(const T& x) { node* tail = _head->_prev; node* newnode = new node(x); //調用node的構造函數 tail->_next = newnode; newnode->_next = _head; _head->_prev = newnode; newnode->_prev = tail; } void insert(iterator pos, const T& x) { assert(pos._pnode); node* cur = pos._pnode; node* prev = cur->_prev; node* newnode = new node(x); prev->_next = newnode; newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev; } iterator erase(iterator pos) { assert(pos._pnode); assert(pos != end()); node* prev = pos._pnode->_prev; node* next = pos._pnode->_next; delete pos._pnode; prev->_next = next; next->_prev = prev; return iterator(next); } size_t size() { size_t sz = 0; iterator it = begin(); while (it != end()) { it++; sz++; } return sz; } bool empty() { return begin() == end(); }private: node* _head;};
xxxx成員變量就是一個頭結點。同時還需要typedef出結點、const_iterator和iterator
list(){ _head = (node*)malloc(sizeof(node)); _head->_next = _head; _head->_prev = _head;}
xxxx注意_head需要使用malloc來開辟空間,因為頭結點不需要存儲數據,所以不需要初始化。如果用new的話存在一個問題,就是如果出現數據類型T也是list,那么用new開辟頭結點空間時,就會自動迪調用構造函數初始化,初始化就要調用構造函數,調用構造函數就要new并且初始化,就會出現一個無限循環。所以就需要malloc來開辟空間。
xxxx對于空鏈表來說,應該是頭結點的next和prev都指向自己。
list(const list<T>& lt){ _head = (node*)malloc(sizeof(node)); _head->_next = _head; _head->_prev = _head; for(const auto& e : lt) { push_back(e); }}
xxxx可以復用代碼,使用push_back。但是push_back之前,必須是規范的空鏈表,必須初始化。
list<T>& operator=(const list<T> lt){ swap(_head, lt._head); return *this;}
xxxx這是一種現代寫法,很簡單,先利用拷貝構造,構造出一個完全相同的,這樣把*this的_head與拷貝構造出來的_head一換,這樣就直接把拷貝構造出來的內容的給了this
~list(){ clear(); delete _head; _head = nullptr;}
xxxx同樣的道理,也是復用,先用clear,將鏈表置空(所有的結點都被釋放掉),然后再釋放掉頭結點。
void clear(){ iterator it = begin(); while(it != end()) { it = erase(it); }}
xxxx挨個釋放所有結點就是復用erase,同時,因為釋放結點后,就無法找到該結點后的結點,所以it要接受erase的返回值(即:被釋放結點后結點的迭代器)
iterator begin(){ return _head->_next;}const_iterator begin() const{ return _head->_next;}iterator end(){ return _head->_prev;}const_iterator end() const{ return _head->_prev;}
void insert(iterator pos, const T&x){ assert(pos._pnode); node* cur = pos._pnode; node* prev = cur->_prev; node* newnode = new node(x); prev->_next = newnode; newnode->_next = cur; cur->_prev = newnode; newnode->_prev = prev;}
xxxxprev、cur、next三個結點的鏈接關系搞清楚即可,由于是帶頭雙向循環,所以,不需要考慮結點之間連接的順序,也不需要考慮多種情況。需要考慮的就是pos除是否為空,空的位置不能操作,斷言一下。提高安全性。
iterator erase(iterator pos){ assert(pos._pnode); assert(pos != end()); node* prev = pos._pnode->_prev; node* next = pos._pnode->_next; delete pos._pnode; prev->_next = next; next->_prev = prev; return iterator(next);}
xxxx刪除后,為了防止迭代器失效,所以要把下一位置的的迭代器當做返回值返回。這是STL中容器的普遍做法。
xxxx其余的接口都比較比較簡單,直接看代碼就能明白,代碼在上面的list類完整版部分。
xxxx
xxxx
xxxx
xxxx
xxxx如果讀者還有任何疑問,可以評論留言,或者私信我,我們一起探討,一起進步
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/118802.html
摘要:對類采用三個模板來實現迭代器。楷體類中和模板定義分別對應迭代器中模板定義的楷體采用向上傳值的方式,傳入不同值來采用不同迭代器。首先是迭代器的,分為前置和后置。迭代器的和都是獲取迭代器所對應的值。唯一的差別就是就是用到了迭代器。 ...
摘要:中的集合稱為單列集合,中的集合稱為雙列集合。洗牌通過數字完成洗牌發牌發牌將每個人以及底牌設計為將最后張牌直接存放于底牌,剩余牌通過對取模依次發牌。存放的過程中要求數字大小與斗地主規則的大小對應。 01Map集合概述 A:Map集合概述: 我們通過查看Map接口描述,發現Map接口下的集合與Collection接口下的集合,它們存儲數據的形式不同 ? a:Collection中的集...
1_(去除ArrayList中重復字符串元素方式)* A:案例演示 需求:ArrayList去除集合中字符串的重復值(字符串的內容相同) 思路:創建新集合方式 import java.util.ArrayList; import java.util.Iterator; public class ArrayList_1_demo { /* 創建新集合將重復元素去掉 * 1.明...
摘要:并且線程可被中斷,那么在訂單處理過程中接收的取消請求會結束剩余的處理流程。演示可取消任務的股票交易處理程序交易訂單創建數量為的線程池來執行訂單。邪惡線程邪惡線程,隨機的取消某些訂單。判斷是否取消成功,在每兩個請求之間讓邪惡線程睡一會。 FutureTask類 重點是那個股票交易處理程序的例子,認真看三遍。本文花了三個小時。 GitHub代碼歡迎star。 小白認為學習語言最好的方式就是...
摘要:本文介紹了的常用接口的使用,并對其進行了模擬實現,包括迭代器的實現。與為反向迭代器,對迭代器執行操作,迭代器向前移動。 本文介紹了list的常用接口的使用,并對其進行了模擬實現,包括list迭代器的實現。 目錄 一、list的介紹 二、list的常用接口的使用 1. list的構造 2. l...
閱讀 1025·2022-07-19 10:19
閱讀 1800·2021-09-02 15:15
閱讀 1013·2019-08-30 15:53
閱讀 2659·2019-08-30 13:45
閱讀 2658·2019-08-26 13:57
閱讀 1987·2019-08-26 12:13
閱讀 1010·2019-08-26 10:55
閱讀 551·2019-08-26 10:46