【C++】簡單學——vector類(模擬實現)

模擬實現的準備工作

看源碼,了解這個類的大概組成

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是什么東西

因為這個函數是函數模板,所以里面的一切模板都是沒有實例化的(實例化是在后面調用的時候)

編譯器是不敢去沒有實例化的模板里面去取東西的

編譯器會根據這種(通過指定類域的方式來取數據)情況有兩種猜測:

  1. 這是一個內嵌類型(typedef定義的類型,內部類等等在類里面定義的類型)
  2. 這是一個類里面的靜態變量,如果說是靜態變量,那后面肯定就是要去跟分號( ;)的了

為了解決這個問題,編譯器規定這種情況要在前面加上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一直往后越界,直到訪問到沒有開辟的空間就會崩了

問:那如果我選擇刪除的時候不動不就好了?

也不可以

  1. STL不強制要求具體實現,其他的vector具體的實現有可能縮容,縮容就意味著delete原空間之后開新空間,然后迭代器就失效了
  2. 因為vs規定,只要你前面進行了erase的話,后面就沒辦法用之前的迭代器了(因為vs用的不是原生指針,內部會有特殊處理和檢查)

在erase之后,it就不能再用了,除非你新搞一個出來

解決辦法:

既然有了這個問題,那么庫里面自然會給出解決辦法

會返回刪除位置的下一個位置的迭代器

修改方案:

我們自己的erase也要修改

總結:只要進行了插入刪除就要默認前面的迭代器已經失效了,因為STL沒有規定具體實現,也就不知道什么時候會擴容然后換新空間

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/90629.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/90629.shtml
英文地址,請注明出處:http://en.pswp.cn/web/90629.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【WRF】根據自動安裝腳本安裝 WRF / WRF-CHEM等

目錄 GitHub 上 WRF 自動安裝腳本 ?? 腳本的作用 ??? 支持的系統 ?? 可安裝的 WRF 版本及其選項 ? 如何使用(以 WRF 4.6.1 為例) ? 依賴庫的安裝位置 完整安裝腳本分析 參考 GitHub 上 WRF 自動安裝腳本 GitHub 上的 WRF-Install-Script 項目的 Releases(發布版本…

M2IV:面向大型視覺-語言模型中高效且細粒度的多模態上下文學習

MIV&#xff1a; Towards Efficient and Fine-grained Multimodal In Context Learning in Large Vision-Language Models COLM 2025 why 新興的研究方向&#xff1a;上下文學習&#xff08;ICL&#xff09;的效果“向量化”&#xff0c;其核心思想是用transformer內部的向量來…

龍迅#LT8711UXD適用于Type-C/DP1.4 /EDP轉 HDMI2.0 功能,分辨率高達4K60HZ,可支持HDCP!

1. 描述LT8711UXD 是一款高性能雙通道 Type-C/DP1.4 轉 HDMI2.0 轉換器&#xff0c;旨在將 USB Type-C 源或 DP1.4 源連接到 HDMI2.0 接收器。該LT8711UXD集成了一個符合 DP1.4 標準的接收器和一個符合 HDMI2.0 標準的發射器。此外&#xff0c;還嵌入了兩個用于CC通信的CC控制器…

《計算機組成原理與匯編語言程序設計》實驗報告一 基本數字邏輯及漢字顯示

目 錄 一、實驗學時 二、實驗目的 三、實驗要求 四、實驗內容 五、實驗步驟 1、打開Logisim軟件&#xff0c;列出異或邏輯電路真值表&#xff0c;并使用與、或、非基本原件實現異或邏輯電路。 2、打開Logisim軟件&#xff0c;列出同或邏輯電路真值表&#xff0c;并使用…

聚焦牛牛道:綠色積分模式如何實現快速發展?

?綠色消費積分政策再次進入大眾視野&#xff0c;這種能為企業減輕庫存負擔、讓咨金周轉更靈活的促銷方式&#xff0c;很快就成了焦點。牛牛道作為積極踐行這一政策的平臺&#xff0c;憑借其獨樹一幟的商業模式和運營思路&#xff0c;在短時間內就取得了顯著發展。一、牛牛道平…

高頻 RFID 賦能工業教學設備教學應用

高頻 RFID 賦能工業教學設備教學應用應用背景傳統工業教學設備側重機械原理、電氣控制等基礎功能演示&#xff0c;缺乏對 RFID 等工業識別技術的具象教學載體。學生在理論學習中難以直觀理解 RFID 技術的工業適配邏輯&#xff0c;實訓中缺乏設備識別系統的部署、調試經驗&#…

Transformer:顛覆NLP的自注意力革命

Transformer:顛覆NLP的自注意力革命 Transformer是自然語言處理領域中極具影響力的深度學習模型架構,以下是對其的詳細介紹: 提出背景與應用:2017年,Vaswani等人在《Attention Is All You Need》論文中首次提出Transformer架構,它主要用于處理序列到序列的任務,如機器翻…

基于 KeepAlived + HAProxy 搭建 RabbitMQ 高可用負載均衡集群

基于 KeepAlived HAProxy 搭建 RabbitMQ 高可用負載均衡集群 基于 KeepAlived HAProxy 搭建 RabbitMQ 高可用負載均衡集群實戰指南 一、前言 在企業級應用中&#xff0c;消息隊列的高可用性是系統穩定性的重要保障。RabbitMQ 作為主流的消息中間件&#xff0c;雖然自身支持…

京東獲得JD商品詳情 API 返回值說明||京東API接入文檔

京東商品詳情API返回值核心字段說明一、商品基礎信息商品ID&#xff08;skuId/productId&#xff09;唯一標識符&#xff0c;用于定位具體商品或SKU&#xff08;如不同顏色、尺寸的變體&#xff09;。示例&#xff1a;"skuId": "123456789"商品標題&#x…

其他世界的自來水

西歐&#xff0c;北美&#xff0c;亞洲日韓等地區&#xff0c;他們的自來水可以直接飲用以英國為例&#xff1a;自來水的質量可能等同或者有可能超過純凈水&#xff0c;不需要消毒和過濾直接可以飲用。直接從水龍接的水和瓶裝純凈水沒有什么差別&#xff0c;甚至比瓶裝純凈水更…

IO密集型、CPU密集型、負載、負載均衡

0、引入 從宏觀上來講&#xff0c;計算機可以抽象為【輸入 > 計算 > 輸出】這三部分 輸入輸出自然就是io&#xff0c;而計算部分自然歸cpu管 不同的任務&#xff0c;對io和cpu的依賴程度不同&#xff0c;由此有了cpu密集型任務和io密集型任務 1、IO密集型 更依賴輸入…

從甲方的角度看MOM項目成敗的原因

關鍵詞&#xff1a;MOM、數字化轉型、成敗數字化轉型中流行這么一句話&#xff1a;SAP項目加班到晚上8點&#xff0c;MOM項目最少到晚上10點。由此可見&#xff0c;MOM項目實施的難度、復雜度。但&#xff0c;為什么MOM難度大&#xff1f;先引入1個故事&#xff1a;1個價值300萬…

MySQL操作進階

系列文章目錄 MySQL的基礎操作-CSDN博客 目錄 系列文章目錄 前言 一、數據庫的約束 1. 約束類型&#xff1a;not null 2. 約束類型&#xff1a;unique 3. 約束類型&#xff1a;default 4. 約束類型&#xff1a;primary key 5. 約束條件&#xff1a;foreign key 二、表…

表征工程 中怎么 調整參數或比例

表征工程 中怎么 調整參數或比例 在表征工程(Representation Engineering)中,調整參數或比例的核心目標是平衡干預效果與模型基礎能力,避免過度干預導致語義失真或能力退化。以下是幾種常用的方法論及具體案例: 1. 系數縮放法(Scaling Coefficients):通過權重參數控制…

如何使用Anaconda(miniconda)和Pycharm

文章目錄前言具體操作Pycharm連接配置 Anaconda&#xff08;miniconda&#xff09;創建的虛擬環境PipAnacondaPyCharm三者關系一圖勝千言總結前言 本文介紹如何利用Anaconda和Pycharm這兩個強大的工具&#xff0c;實現Python項目的高效開發。通過構建虛擬環境、安裝依賴包及利…

【07】C#入門到精通——C# 生成dll庫 C#添加現有DLL C#調用自己生成的dll庫

文章目錄0 多個.cs文件源碼01 Hero.cs02 ShowInfo.cs03 Program.cs &#xff08;相當于Main文件&#xff09;04 運行效果1 生成dll庫1.1 創建類庫1.2 添加要生成 dll庫 的代碼文件1.2.1 添加 Hero類1.2.2 添加 ShowInfo類1.3 生成dll庫 及 查看3 添加自己生成的dll庫4 調用運行…

進程控制->進程替換(Linux)

在之前的博客中&#xff0c;我們已經探討了進程創建、終止和等待的相關知識。今天&#xff0c;我們將繼續深入學習進程控制中的另一個重要概念——進程替換。回顧之前的代碼示例&#xff0c;我們使用fork()創建子進程時&#xff0c;子進程會復制父進程的代碼和數據&#xff08;…

認識泛型、泛型類和泛型接口

目錄泛型泛型類泛型接口泛型 定義類、接口、方法時&#xff0c;同時聲明了一個或者多個類型變量&#xff08;如&#xff1a;<E>&#xff09;&#xff0c;稱為泛型類、泛型接口、泛型方法、它們統稱為泛型 作用&#xff1a;泛型提供了在編譯階段約束所能操作的數據類型&…

如何排查并解決項目啟動時報錯Error encountered while processing: java.io.IOException: closed 的問題

如何排查并解決項目啟動時報錯Error encountered while processing: java.io.IOException: closed 的問題 摘要 本文針對Java項目啟動時出現的java.io.IOException: closed錯誤&#xff0c;提供系統性解決方案。該異常通常由流資源異常關閉或損壞引發&#xff0c;常見于Maven依…

Kafka——多線程開發消費者實例

引言在分布式系統領域&#xff0c;Kafka憑借高吞吐量、低延遲的特性成為消息隊列的事實標準。隨著硬件技術的飛速發展&#xff0c;服務器多核CPU已成常態——一臺普通的云服務器動輒配備16核、32核甚至更多核心。然而&#xff0c;Kafka Java Consumer的設計卻長期保持著"單…