模擬實現的準備工作
看源碼,了解這個類的大概組成
1.先看成員變量
成員變量的組成是三個迭代器
問:這個iterator內嵌類型究竟是什么?即這個迭代器是什么?
迭代器實際就是T*
問:這三個迭代器代表什么意思?
連蒙帶猜:元素的開始,元素的結束,容量的結束
不過因為只是猜測,所以還是需要去自己試驗一下的
2.看構造函數
要看對象要怎么初始化
無參的時候是全部迭代器初始化成空?
3.看pushback(增加數據)
如果經驗豐富的話,就可以知道這個是尾插
問:這個construct是什么意思?
內部其實就是一個定位new
定位new是對已有的空間進行初始化
construct是在finish的位置那里進行尾插,然后finish++
STL的空間都是從空間配置器(內存池)來的,內存池只負責開辟空間,沒有初始化,如果說要對已有的空間進行初始化,就要用定位new
問:insert_aux(end(),x)又是什么意思?
如果說finish==end_of_storage,就擴容,可以從這里看出來,vs的擴容是2倍擴容
總結
由上面的信息可以得出下面的圖和信息
size就是finish-start
capacity就是end_of_storage-start
開始嘗試能不能實現
因為我們是模擬實現,所以會有很多沒有考慮到的地方,所以如果我們要寫一些方法,強烈推薦寫一個畫一個圖,這樣還可以來查漏補缺,看看那些隱藏的bug
模擬實現的意義
1、讓我們更加理解底層,能夠更好地使用
2、可以跟那些大佬學習他們是怎么實現的(盡管因為大佬在搞的時候會因為考慮全局方面而被迫搞得很復雜,但是仍然有參考意義)
嘗試把剛剛前面了解的東西寫一下
問:既然我們都已經寫了缺省參數,并且我們也沒有必要搞有參構造函數,那能不能把那個無參拷貝構造給刪掉
答:如果說之后寫了拷貝構造,編譯器就不會生成默認構造函數,就會導致當你不傳參構造對象的時候沒有默認構造函數可以調用
析構函數
為了方便測試,所以先寫一個pushback嘗試跑通一下
對于類模板,函數如果要用到模板,就要特別小心了
除了考慮各種值,還要考慮各種值是否合適
例如:假如這個地方push _back的是一個string或者是vector等等,就會導致消耗很大;如果加了引用,還要小心這個傳入的值在里面被修改了
(解決辦法:加引用,加const)
push_back的邏輯:
1.先判斷夠不夠容量
如果要擴容,就記錄一下新開的空間要多大,至于size和capacity怎么算,我們就順便把size和capacity給實現了吧
擴容邏輯:根據原本的容量進行二倍擴容,然后把原有的數據拷貝下來,然后原本的start指向的空間就銷毀,然后讓start指向新的空間,把那些新的成員變量都更新一遍
插入邏輯:在finish的位置的值設置為val,然后finish++往后移一格
問:如果說vector實現了析構函數,tmp開辟了空間,tmp的生命周期結束的時候難道不會把開辟的那個空間給delete掉嗎?
答:我們必須得區分好,只有對象才會在銷毀的時候自動調用析構函數,對象是包括了_start、_finish、_endofstorage的對象,而我們現在的開辟空間只不過是利用一個T的指針去在堆里面開辟空間,并不是對象,不會自動銷毀堆的空間
為了方便測試,我們就做一個遍歷吧,為了遍歷方便,順便做一個operator[ ]
(記得包assert的頭文件)
測試用例:
但是結果出問題了
調試之后,發現finish在進行更新的時候出了問題
問:為什么這個地方的_finish變成了空指針
答:直到_start更新都是沒問題的,但是這個地方的finish = tmp + size();而size是等于finish - start;
也就是說,此時的start已經更新了,而finish還沒有更新,所以size = (舊的)finish - (新的)start
所以公式就變成了這樣:(新的)finish = (新的)start + (舊的)finish - (新的)start = (舊的)finish
因為舊空間在前面就已經被銷毀了,所以新的finish仍然指向這個被銷毀的空間,所以_finish就變成了空指針
解決辦法:提前記錄size(記錄start和finish的相對位置)
?size和capacity
迭代器
測試用例:對比三種
const的也要實現
晚點把size和capacity也實現const
為了方便我們進行測試,我們直接把剛剛遍歷的內容搞成一個print函數來調用吧
當然,也可以實現成模板
但是這里有一個潛藏的問題:因為這個print用了auto所以才避免了
報錯的原因:
語法上有沖突,編譯器不認識這個const_iterator是什么東西
因為這個函數是函數模板,所以里面的一切模板都是沒有實例化的(實例化是在后面調用的時候)
編譯器是不敢去沒有實例化的模板里面去取東西的
編譯器會根據這種(通過指定類域的方式來取數據)情況有兩種猜測:
- 這是一個內嵌類型(typedef定義的類型,內部類等等在類里面定義的類型)
- 這是一個類里面的靜態變量,如果說是靜態變量,那后面肯定就是要去跟分號( ;)的了
為了解決這個問題,編譯器規定這種情況要在前面加上typename
pop_back和empty
尾刪直接讓finish往前移動一格就好,為了防止一直往后刪,所以要寫一個判斷為空的方法
insert
我們就實現第一個就好
插入之前先檢查空間夠不夠(順便把reserve給實現了)
然后把數據插入到pos位置
把前一個數據挪到后一個數據處,直到把pos位置的數據給挪走
問:string的時候,pos是size_t,導致頭插的時候,判斷條件讓size_t?it?永遠不會比pos小而死循環,為什么現在不會有這個問題了?
答:我們現在的pos是迭代器,而地址永遠都不會為0(地址本質上是每個內存單位的編號,最小的指針就是空指針),所以不存在string的那種問題
出問題了
這個地方我們選擇不斷頭插11.11
當超過4個值,要進行擴容的時候就出錯了
調試后發現,pos的位置不對勁,我們原本是打算pos指向頭元素的,但是擴容之后,原來的那個空間被銷毀了,但是pos還是在舊空間上(迭代器失效)
問:為什么會出現-6.2777等的數字?
答:因為pos沒有更新,所以當進行it>=pos的時候,此時的pos是0x0163f068,it是0x01635250,此時的pos要比it大,所以就不會進到循環判斷中,也就是啥都沒做,但是finish卻憑空++了,然后就會看到尾部多了一個非法的數據;(這種情況是因為迭代器失效導致的,所以正常情況下不擴容是看不到這個錯誤的)
解決辦法:提前記錄pos和start的相對位置
reserve
直接把前面push_ back時臨時搞的擴容直接拷貝過來就好
最終實現
push別忘了改回去
erase
依次把pos+1的位置往前覆蓋,直到pos+1到了結尾
由此可以復用pop_back
resize
根據庫里面實現的resize,我們可以得到以下的函數
const T& val = T()是一個調用了默認構造的無參的匿名對象
好處:可以根據不同的類型來給初始值(因為string和其他更多類型不能簡單的用0來初始化,就可以用匿名對象來進行初始化)
問:如果說是vector<int>,int是內置類型,沒有默認構造怎么辦?
因為模板出來之后,為了解決這個問題,祖師爺讓內置類型也有了默認構造(被迫升級),就是為了應對這個地方
整形就是0(char也是0,只不過在ascll中就是'\0'),浮點數就是0.0,指針就是空指針;
resize的邏輯:無非就是插入和刪除的問題
因為我們reserve有做元素個數和容量大小的檢查,所以這個地方我們可以直接分成兩種情況(而不是三種情況)
插入:n比size大(無所謂capacity),我不管三七二十一,反正我就先擴容,反正實際擴不擴容還是得看reserve
刪除:n比size小
總結就是以下
拷貝構造
因為編譯器自動生成的拷貝構造走的是淺拷貝,所以當他們指向同一塊空間時,在銷毀時就會析構兩次,就會報錯,所以我們需要自己實現拷貝構造
(銷毀的時候直接崩掉)
另一種拷貝構造的方法
push_back后會自己開空間和初始化,利用這個原理
(此處是this.push_back,不要疑惑)
可以利用把v1的值一次頭插給v2來實現拷貝構造
但是這個拷貝構造還是不夠好,還可以優化
(如果是string的話,范圍for的時候會有很大消耗,所以說用引用;如果說要拷貝1000個數據,可以提前開空間)
賦值拷貝
現代寫法
為了實現我們的現代寫法賦值拷貝,我們提前搞一個swap來實現
但是還是報錯了
我們這個swap期望是去調用庫里面的,但是因為編譯器在查找的時候會就近原則,他就會以為是遞歸那種自己調用自己,調自己的話就是一個參數,所以才報錯說不接受兩個參數
解決辦法:
加上類域指定
構造(補充)
用迭代器構造
科普:如果說類的模板參數不夠用,我還可以自己給自己加模板參數
可以利用其它對象的迭代器來初始化
調用的例子:
那我這樣調用呢?想要從指向第二個迭代器和倒數第二個迭代器來進行初始化
會出問題:
begin和end是傳值返回,所以是臨時拷貝;不能加引用,防止會修改自己那個底層的成員
++和--的特點就是會去修改自身,所以編譯器因為沒辦法修改臨時拷貝而報錯
解決辦法:不改變就好了
也可以這樣調用
迭代器構造寫成模板的原因就是為了支持前面的寫法
n個值的構造
但是會出錯
原本希望調用n個值的構造,結果卻調用到了迭代器構造;
劃紅圈的位置是報錯的位置
找bug技巧:編譯錯誤如果實在不好看,我們可以利用排除法(屏蔽掉部分代碼進行確認)
當屏蔽迭代器構造函數之后,可以發現,我們的n個值的構造函數是沒有問題的
但是一旦兩個同時出現的時候就會報錯
以下的問題跟模板有關:
如果說10不用某些標識符來區分的話,默認就是int類型而不知sizt_t
此時第一個調用,10跟size_t? n不是特別匹配,但也能用;但是調用那個迭代器構造就非常合適了,所以這個構造就會調用迭代器的構造
在這個函數里面,*first的時候相當于(*10),想把地址編號為10的值進行尾插,所以就會報錯說非法的間接尋址
第二個調用和第三個調用都是n個值構造比較適配,所以就不會出問題
解決辦法:
庫里面的解決辦法是寫一個重載,讓特殊情況方便調用
{}構造(initializer_list)(C++11)
這部分內容只是提前看一下
原理:靈感來源于初始化數組
花括號類型:initializer_list<class>類
只有這幾個成員方法
本質:這個類里面有兩個指針,而{1, 2, 3, 4, 5, 6, 7, 8, 9}就是一個常量數組,在常量區開了一個空間,一個指針指向這個常量數組的開始lbegin,一個指向結束end?
通過sizeof計算這個花括號常量數組大小,得出來的大小是8byte(兩個指針)也可以來證明這一點
有點像是vector,但是數據是在常量區,不能動態擴容 ? ? ??
所以下面的代碼其實就是在用initializer_list對象來初始化vector(隱式類型轉換)
所以說如果要實現用大括號來進行構造的話,就要有一個單參數構造函數,且?參數為initializer_list
initializer_list單參數構造函數
有迭代器就可以范圍for
具體使用:
特殊bug
復雜vector情況
這樣子會掛,關鍵在于開空間
通過檢查,發現是析構的時候崩了
memcpy導致string淺拷貝了(所有的嵌套開辟空間的類型都會有這個問題)
當需要擴容的時候,會把原本的空間給delete[],同時也會調用每個string的析構函數,盡管mencpy對于vector是深拷貝,但是對于string仍然是淺拷貝,還是析構兩次的問題
所以說必須要實現這樣
問:解決方案可以是用swap嗎?
不可以,因為不知道T是什么,所以也不知道T有沒有swap,萬一沒有,調用了庫里的淺拷貝一樣完蛋
解決方案:不用mencpy了,直接for循環把原本空間的元素一個個賦值到新空間里
迭代器失效
情況1
這里的insert插入之后,導致擴容,然后原本的空間就失效了,it還是指向原本的空間,所以這個就是迭代器失效
問:之前我在解決這個東西的時候,不是遇到了這個問題,然后更新了pos嗎?為什么還會失效?
因為形參是實參的拷貝,形參的修改不影響實參,it在外面不變
問:那為了解決這個問題,我傳引用不就好了?
也不行,因為會導致以下的傳參方式不行
begin是傳值返回,具有常性,普通引用不能引用具有常性的值
解決辦法:失效之后就不要再使用了,如果還想要找剛剛那個位置,那就自己找,自己去重新更新這個迭代器
情況2
erase的時候,會把后面的值覆蓋前面的值,相當于把整體往后移了,相對的,自己就++了,所以會多移動一次
第一個對了是巧合
當最后一個數據是偶數就會出大問題(錯過了判斷條件)
就會讓it一直往后越界,直到訪問到沒有開辟的空間就會崩了
問:那如果我選擇刪除的時候不動不就好了?
也不可以
- STL不強制要求具體實現,其他的vector具體的實現有可能縮容,縮容就意味著delete原空間之后開新空間,然后迭代器就失效了
- 因為vs規定,只要你前面進行了erase的話,后面就沒辦法用之前的迭代器了(因為vs用的不是原生指針,內部會有特殊處理和檢查)
在erase之后,it就不能再用了,除非你新搞一個出來
解決辦法:
既然有了這個問題,那么庫里面自然會給出解決辦法
會返回刪除位置的下一個位置的迭代器
修改方案:
我們自己的erase也要修改
總結:只要進行了插入刪除就要默認前面的迭代器已經失效了,因為STL沒有規定具體實現,也就不知道什么時候會擴容然后換新空間