初識C++ · 模擬實現vector

目錄

前言:

1 部分簡單函數的實現

2 push_back和pop_back

3 reserve和resize

4 Print_vector

5 insert和erase

6 拷貝構造

7 構造

8 賦值

9 memcpy的問題

10 迭代器失效


前言:

繼上文模擬實現了string之后,接著就模擬實現vector,因為有了使用string的基礎之后,vector的使用就易如反掌了,所以這里直接就模擬實現了,那么實現之前,我們先看一下源代碼和文檔:

源碼不多看,因為很容易繞暈進去,我們從這兩個部分得到的信息就夠了,首先,vector是一個類模板,即我們可以往里存放許多不同的數據,比如實現一個二維數組,我們可以用vector里面套一個vector,就實現了一個二維數組,為什么說二維數組是一維數組的集合,因為vector的每個空間的start指針指向的空間又是一塊連續的空間,即這就是二維數組的構造。

在public的下面有一堆的typedef,這也是不要多看源碼的一個原因,很容易繞進去的,比如vector的成員變量的類型是iterator,我們看到value_type*是重命名為iterator了,那么value_type又是T的重命名,即start的類型就是T*,現在我們搞懂了一個問題即模擬實現vector的時候成員變量的類型是模板指針。

那么現在就需要搞懂三個成員變量是什么?

這里就明說了,start是空間的起始位置,finish是空間的最后一個數據的下一個位置,end_of_storage是空間的最后一個位置,所以我們實現size(),capacity()的時候就兩兩相減就可以了。

基本類型我們了解了,現在就開始模擬實現吧。


1 部分簡單函數的實現

這里實現以下函數,size(),capacity(),begin(),end()以及const版本,operator[]以及const版本,empty(),析構函數,注意的就是返回值返回類型即可:

iterator begin()
{return _start;
}
iterator end()
{return _finish;
}
const_iterator begin()const
{return _start;
}
const_iterator end()const
{return _finish;
}
size_t size()
{return _finish - _start;
}
size_t capacity()
{return _end_of_storage - _start;
}
T& operator[](size_t pos)
{assert(pos <= _finish);return *(_start + pos);
}
const T& operator[](size_t pos)const
{assert(pos <= _finish);return *(_start + pos);
}
bool empty()
{return _start == _finish;
}
~vector()
{delete[] _start;_start = _finish = _end_of_storage = nullptr;
}

因為是[]重載,返回的是可以修改的類型所以用引用,這里模擬實現的時候一般都返回size_t類型,這里文檔里面的原型,最好保持一致。


2 push_back和pop_back

尾插的時候要注意空間的擴容,擴容的判斷條件即是_finish = _end_of_storage的時候,擴容方式和前面實現順序表鏈表的時候沒有什么區別,使用2倍擴容,但是實際上vs下的vector擴容的時候是使用的1.5倍擴容,我們模擬實現的是linux下的vector,在string模擬實現的時候,我們拷貝數據使用的是strcpy,在這里因為是模板,所以不能使用字符串函數,使用內存拷貝,即使用memcpy,當然memset也可以,但是memcpy就可以了,因為不存在空間重疊的問題。

這里有個隱藏的坑,到后面插入string類的時候才會顯式出來,這里先不說,目前插入內置類型是沒有問題的:

	void push_back(const T& val){//判斷擴容if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = _start + size();_end_of_storage = tmp + newcapacity;}*_finish = val;_finish++;}

好了,實現好了嗎?當然沒有,這里也有個坑,即size()的問題,因為實現size()的時候,實現方式是_finish - _start,但是擴容之后,_start的位置已經改變,所以此時size()的實現變成了原來的_finish - 新的_start了,所以會報錯,解決方式就是把size()先存起來再使用:

	void push_back(const T& val){//判斷擴容if (_finish == _end_of_storage){size_t old_size = size();size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();T* tmp = new T[newcapacity];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = tmp + old_size;//容易出錯_end_of_storage = tmp + newcapacity;}*_finish = val;_finish++;}

這里如果忘了memcpy的使用可以看看文檔。

那么push_back就還有一個坑沒有解決了,欲知后事如何,請看下去咯。

pop_back的實現相對來說就簡單多了,尾刪不代表真正的刪除數據,指針指向的位置改變就是尾刪了,當然,刪除操作想要執行就vector就不能為空:

	void pop_back(){assert(!empty());_finish--;}

3 reserve和resize

reserve的實現我們剛才其實已經實現過了,reserve即是擴容,push_back的時候我們已經考慮了擴容,所以代碼差不了多少:

void reserve(size_t n)
{if (capacity() < n){size_t old_size = size();T* tmp = new T[n];memcpy(tmp, _start, sizeof(T) * size());delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

當然,這里的坑和push_back是一樣的,只有在插入自定義類型的時候才會出現,先不急,那么這里的擴容就是把newcapacity換成了n而已,其余的是沒有差別的。

resize模擬實現的時候就要注意了,如果resize實現的是縮容,就直接移動_finish指針就好了,如果實現的是擴容,那么默認參數我們怎么給呢?

文檔里面記錄的缺省值是value_type(),而value_type是typedef之后的T,所以我們可以看成T val = T(),看著是有點奇怪?其實一點都不奇怪,比如:

int main()
{//Test7_vector();int a = int();cout << a;char ch = char();cout << ch;return 0;
}

這種代碼運行是完全沒有問題的,這里的a是0,這里的ch是'\0',這是一種初始化方式,所以我們傳參的時候就不能寫成0,因為不是所有的vector都是vevtor<int>類型的,所以這里我們采用文檔的方式,使用這種缺省值,那么擴容的時候多余的空間就給上這個缺省值就好了:

void resize(size_t n, const T& val = T())
{if (size() < n){//擴容reserve(n);while (_finish < _start + n){*_finish = val;_finish++;}}else{//刪減_finish = _start + n;}
}

實際上我們也可以一直尾插T(),但是我們既然知道了我們要多少空間,我們不如直接把空間開完,然后進行賦值就好了,賦值的循環條件是_finish指針到最后擴容的指針的位置,也是比較好理解的。


4 Print_vector

當插入和擴容操作都完成了,下一步就是打印了,當然會有人說初始化怎么辦,不急,我們先構造一個空的vector就可以了:

vector()
{}iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;

先使用上缺省值,方便測試即可。

Print_vector是用來打印vector的,這里其實變相的考了遍歷方式,遍歷就是三件套:

void Print_vector(const T& t)
{for (auto e : t){cout << e << " ";}cout << endl;//typename 告訴編譯器這是個類型typename vector<T>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}for (int i = 0;i < size();i++){cout << v[i] << " ";}
}

這是三種遍歷方式,下標訪問和迭代器使用都是沒有問題的,唯獨需要注意的是typename那里,如果那里我們使用auto it = v.begin()是沒有問題的,但是如果我們使用vector<T>::iterator it = v.begin()就會有問題了,這里是因為編譯器不知道這里的vector<T>::iterator是類型還是變量了,編譯器的原則是不會去類模板里面找東西,而這里使用了模板,那么從編譯器的角度出發,它不知道這個究竟是個類型還是變量,所以使用typename。


5 insert和erase

push_back和pop_back是尾插尾刪,想要任意位置插入就需要用到insert和erase,文檔里面erase有刪除一段區間的,這里我們就實現任意位置刪除一個數據即可,因為刪除一段區間的原理是一樣的,因為是任意位置刪除和插入,所以實現了之后在push_back和pop_back上也可以復用

insert實現的原理很簡單,挪動數據添加數據即可,當然要注意是否要擴容:

void insert(iterator pos, const T& val)
{assert(pos <= _finish);assert(pos >= _start);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());pos = len + _start;//易錯點}iterator it1 = _finish - 1;while (it1 >= pos){*(it1 + 1) = *it1;it1--;}*pos = val;_finish++;
}

當然,為了位置的合法性,這里使用assert斷言一下,然后就是判斷擴容,但是這里和push_back的size是一個道理,一旦發生擴容,那么pos位置就會變化,while循環的判斷就會出問題,所以我們擴容后要更新一下pos的位置,這樣才能保證循環的進行。

這里相對string還有一個很好的地方就是我們不用擔心end >= pos的那種隱式類型轉換比較的發生。

erase的實現原理是一樣的,把刪除位置的數據往后移動即可。

iterator erase(iterator pos)
{assert(pos <= _finish);assert(pos >= _start);iterator it1 = pos + 1;while (it1 < _finish){*(it1 - 1) = *it1;it1++;}_finish--;return pos;
}

那么這里的復用就是:

void push_back(const T& val)
{insert(end(), val);
}
void pop_back()
{assert(!empty());_finish--;
}

6 拷貝構造

這里的拷貝構造沒有string拷貝那么復雜了,不用想深拷貝的問題,開空間一直插入就可以了:

vector(const vector<T>& v)
{reserve(capacity());for (auto& e : v){push_back(e);}
}

拷貝構造的最基本要求不能忘記,參數必須是const 的引用類型,不然會發生無限遞歸的問題。


7 構造

構造是最終boss。

第一個構造,即構造一個空的順序表,我們可以專門寫個函數:

vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{}

當然,沒有必要,我們給上三個缺省值就可以了,這是一個構造重載。

第二個重載是給n個element,也很好操作,一直尾插就好了:

vector(size_t n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}

第二個也沒多大問題,再來看第三個區間構造,實際上第三個重載的最后一個參數可以完全不用管的,沒有多大意義,區間構造會再用到一個模板,即類模板的成員函數也可以是個模板:

	template<class InputIterator>vector(InputIterator first,InputIterator last){while (first != last){push_back(*first);first++;}}

以為結束了?還沒有,看這段代碼:

vector<int> v3 = { 1,2,3,4,5,6 };

這個構造是不是覺得有點奇怪,好像對不上上面三個構造中的任何一個,因為花括號里面的類型是initializer_list

這是一個類,它的成員變量也就幾個迭代器和size,但是這是c++11引進的,即c++11準許如下的初始化方式:

void Test_vector()
{//有關initializer_listint a = 1;int b = { 1 };int c{ 1 };//上面寫法均正確 但是不推薦后面兩種vector<int> v1 = { 1,2,3,4,5,6 };//隱式類型轉化vector<int> v2({ 1,2,3,4,5,6,7,8 });//直接構造
}

但是不推薦這種方式,代碼可讀性沒那么高,但是我們看到這種初始化方式應該知道這是使用的initializer_list類型來初始化的,這里的初始化也是涉及到了隱式類型轉換,先構造一個臨時對象,再拷貝構造這個臨時對象,那么編譯器會優化為直接構造。

所以構造就是這樣構造的:

vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}

但是,這里會出問題了:

vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}vector(size_t n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}void Test()
{vector<int> v(10,1);vector<int> v(10u,1);vector<int> v(10,'a');
}

下面的兩個構造就不會出問題,第二個的u是無符號的標志,即size_t,第一個會出問題,因為10和1默認的類型是int,那你說,調用上面的沒問題吧?調用下面的,雖然第一個的參數類型是size_t,但是將就將就吧也可以,那你說,編譯器調用哪個?

所以這里會報錯,那么源代碼里面解決的就很簡單粗暴:

vector(int n, const vector<T>& val = T())
{reserve(n);for (int i = 0;i < n;i++){push_back(val);}
}

類型轉換一下~?


8 賦值

這里的賦值我們可以直接采用現代寫法,不用傳統的先開空間,再進行數據拷貝,我們利用swap函數,可以達到很好的效果:

	void swap(const vector<T>& v){std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_end_of_storage,v._end_of_storage);}

這里需要注意到的是swap的調用需要用到域名訪問限定符,不然編譯器會調用離函數最近的函數,即它本身,而我們使用的是std庫里面的交換函數,所以需要使用::。

有了交換,我們構造一個臨時對象,再進行交換即可:

vector<int>& operator=(vector<T> v)
{swap(v);return *this;
}

我們提及到過連續賦值的條件是返回值應該是賦值過后的值,所以我們需要返回賦值之后的對象。


9 memcpy的問題

先看這樣一段代碼:

void Test5_vector()
{vector<string> v;v.push_back("11111");	v.push_back("22222");v.push_back("33333");v.push_back("44444");v.push_back("55555");for (auto& e : v){cout << e << " ";}cout << endl;
}

如果我們只存入4個或者及以下的數據是沒有問題的,但是存入5個數據之后,就會存在訪問出錯的問題,這一切都是因為memcpy,也就是在擴容,尾插的時候會涉及到的問題。

我們了解memcpy的底層之后,就知道memcpy拷貝的方式是逐字節的拷貝,所以當_start指向的空間是自定義類型的時候,經過擴容之后,就會導致_start指向的空間被銷毀,但是拷貝過來的指針依舊是指向那塊被銷毀的空間的,我們再次訪問自然就會報錯。

所以這里我們最好的方法就是進行賦值:

void reserve(size_t n)
{if (capacity() < n){size_t old_size = size();T* tmp = new T[n];//memcpy(tmp, _start, sizeof(T) * size());for (int i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}
}

本質就是拷貝構造一個臨時對象,就不會導致越界訪問的問題。


10 迭代器失效

void Test6_vector()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);v1.push_back(6);v1.push_back(7);v1.push_back(8);vector<int>::iterator it = v1.begin() + 3;v1.insert(it, 40);Print_vector(v1);//it = v1.begin() + 3;cout << *it << endl;
}

當我們多次插入之后,伴隨著擴容的發生,所以it的位置會發生改變,it指向的是原來的空間位置,所以此時我們打印it的值的時候就會打印出來錯誤的值,所以如果發生了空間的改變導致迭代器失效,如果我們還要使用迭代器,我們就需要重置迭代器-》即那段注釋。

void Test7_vector()
{vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(4);v1.push_back(5);v1.push_back(4);vector<int>::iterator it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){v1.erase(it);it++;}}for (auto e : v1){cout << e << " ";}cout << endl;
}

且看這段刪除偶數的代碼,好像沒有問題?有以下幾種情況就會出錯了,11252,1223。

因為刪除之后會有移動數據的發生,比如1223,刪除了第一個2,第二個2往前面移動,然后再pos位置的指針往后移動,就相當于it指針移動了兩次,所以會錯過,那么如果最后一個是偶數,即11252,就會完美錯過end,那么循環條件就無法結束,就會死循環了。

解決方法也很簡單,讓it刪除之后原地踏步就好了:

while (it != v1.end())
{if (*it % 2 == 0){it = v1.erase(it);//解決方法}else{++it;}
}

這也是為什么erase的返回值是iterator的原因,整個代碼都是前后呼應的。


感謝閱讀!

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

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

相關文章

DataFrame—數據匯總9

s3.sort_index() 文章最前&#xff1a; 我是Octopus&#xff0c;這個名字來源于我的中文名--章魚&#xff1b;我熱愛編程、熱愛算法、熱愛開源。所有源碼在我的個人github &#xff1b;這博客是記錄我學習的點點滴滴&#xff0c;如果您對 Python、Java、AI、算法有興趣&#xf…

MyBatis復習筆記

3.Mybatis復習 3.1 xml配置 properties&#xff1a;加載配置文件 settings&#xff1a;設置駝峰映射 <settings><setting name"mapUnderscoreToCamelCase" value"true"/> </settings>typeAliases&#xff1a;類型別名設置 #這樣在映射…

如何去除視頻上的文字?免費無痕去水印分享!視頻制作良器!

對于需要進行二次創作的視頻素材&#xff0c;去除原有的文字可以提供一個更加干凈的畫布&#xff0c;方便創作者在其基礎上進行新的創作和編輯。同時&#xff0c;去除文字后的視頻也更方便分享到各種平臺&#xff0c;避免因為平臺對文字的限制而導致視頻無法發布或傳播。 要去除…

Kotlin 標準函數 with、run、apply 的定義和使用

Kotlin 標準函數 with、run、apply 的定義和使用 1. with 函數 定義&#xff1a; with 函數允許你在一個對象的上下文中執行一個 lambda 表達式&#xff0c;而不需要在 lambda 表達式中重復引用該對象。 kotlin.internal.InlineOnly public inline fun <T, R> with(r…

云計算期末復習(1)

云計算基礎 作業&#xff08;問答題&#xff09; &#xff08;1&#xff09;總結云計算的特點。 透明的云端計算服務 “無限”多的計算資源&#xff0c;提供強大的計算能力 按需分配&#xff0c;彈性伸縮&#xff0c;取用方便&#xff0c;成本低廉資源共享&#xff0c;降低企…

python 3.10 install on centos

CentOS 7 安裝 Python 3.10_yum python3.10-CSDN博客

Homebrew安裝mysql之后,啟動和使用MySQL服務:

啟動MySQL服務&#xff1a; brew services start mysql 手動啟動服務&#xff1a; mysql.server start 例如&#xff1a; mysql.server start Starting MySQL .. SUCCESS! 停止 MySQL服務&#xff1a; brew services stop mysql 或者 mysql.server stop 重啟MySQL服務&a…

IDEA使用Maven打包項目的所有的依賴

要使用 Maven 命令將 Spring Boot 項目的依賴打包到 lib 文件夾中&#xff0c;你可以在終端中運行以下命令&#xff1a; mvn dependency:copy-dependencies -DoutputDirectory./lib這個命令會將項目的所有依賴&#xff08;包括運行時依賴&#xff09;復制到當前目錄的 lib 文件…

Windows操作系統基本知識整理

目錄 引言 一、Windows操作系統的發展歷史 1.1 Windows 1.0到Windows 3.0 1.2 Windows 95到Windows Me 1.3 Windows NT到Windows 2000 1.4 Windows XP到Windows 7 1.5 Windows 8到Windows 10 二、Windows操作系統的核心組件 2.1 內核 2.2 文件系統 2.3 圖形用戶界面&…

內網橫向移動小補充 --->PTK

大家別急&#xff0c;我的基于資源的約束性委派攻擊還在寫&#xff0c;這個東西一時半會講不清楚&#xff0c;所以我在這里先來補充一點橫向移動以前沒說好的東西&#xff01;&#xff01;&#xff01; 在更啦&#xff0c;別催啦~~~~ 還記得我之前在內網滲透里面講過這個PTK&a…

亞馬遜云主管馬特·加爾曼面臨壓力,致力于在人工智能領域趕超競爭對手

每周跟蹤AI熱點新聞動向和震撼發展 想要探索生成式人工智能的前沿進展嗎&#xff1f;訂閱我們的簡報&#xff0c;深入解析最新的技術突破、實際應用案例和未來的趨勢。與全球數同行一同&#xff0c;從行業內部的深度分析和實用指南中受益。不要錯過這個機會&#xff0c;成為AI領…

mysql中連接查詢的成本

大家好。上篇文章我們講了mysql中成本的含義以及單表查詢如何計算成本。現在我們接著講講mysql中連接查詢的成本。 在講之前&#xff0c;我們先創建兩張一樣的表single_table和single_table2&#xff0c;并在表中插入10000條數據。在下面的講解中&#xff0c;我們稱single_tab…

java并發工具類都有哪些

Java中的并發工具類包括&#xff1a; CountDownLatch CountDownLatch允許一個或多個線程等待其他線程完成某些操作。它通常用于線程間的同步&#xff0c;例如在一個線程完成其工作后通知其他線程繼續執行。 CyclicBarrier CyclicBarrier是一個同步輔助類&#xff0c;它允許一…

使用@Transactional 注解下,事務失效的場景

前言 Transactional是一種基于注解管理事務的方式&#xff0c;spring通過動態代理的方式為目標方法實現事務管理的增強。 Transactional使用起來方便&#xff0c;但也需要注意引起Transactional失效的場景&#xff0c;本文總結了七種情況&#xff0c;下面進行逐一分析。 一、…

【面試必看】Java并發

并發 1. 線程 1. 線程vs進程 進程是程序的一次執行過程&#xff0c;是系統運行程序的基本單位&#xff0c;因此進程是動態的。 系統運行一個程序即是一個進程從創建&#xff0c;運行到消亡的過程。在 Java 中&#xff0c;當我們啟動 main 函數時其實就是啟動了一個 JVM 的進…

ChaosMeta V0.7.0 版本發布 進入CNCF混沌工程全景圖

混沌工程 ChaosMeta 的全新版本 V0.7.0 現已正式發布&#xff01;該版本包含了許多新特性和增強功能&#xff0c;在編排界面提供了多集群管理&#xff0c;在代碼層面支持多命令下發通道的選擇。另外由螞蟻集團發起的ChaosMeta于北京時間2024年1月10日正式進入CNCF混沌工程全景圖…

20232906 2023-2024-2 《網絡與系統攻防技術》第十一次作業

20232906 2023-2024-2 《網絡與系統攻防技術》第十一次作業 1.實驗內容 一、web瀏覽器滲透攻擊 任務&#xff1a;使用攻擊機和Windows靶機進行瀏覽器滲透攻擊實驗&#xff0c;體驗網頁木馬構造及實施瀏覽器攻擊的實際過程。 二、取證分析實踐—網頁木馬攻擊場景分析 ①首先你…

07_Servlet

Servlet 一 Servlet簡介 1.1 動態資源和靜態資源 靜態資源 無需在程序運行時通過代碼運行生成的資源,在程序運行之前就寫好的資源. 例如:html css js img ,音頻文件和視頻文件 動態資源 需要在程序運行時通過代碼運行生成的資源,在程序運行之前無法確定的數據,運行時動態生成…

轉行一年了

關注、星標公眾號&#xff0c;直達精彩內容 ID&#xff1a;技術讓夢想更偉大 整理&#xff1a;李肖遙 來公司一年了。 說是轉行其實還是在半導體行業&#xff0c;熟悉我的朋友知道 &#xff0c;我在18年開始進入半導體行業&#xff0c;那個時候想著行業很重要&#xff0c;站對了…

【前端三劍客之JS】詳解JS

1. JS的引入方式 (1). 內部腳本方式引入 在頁面上&#xff0c;通過一對script標簽引入js代碼.script代碼放置位置有一定隨意性&#xff0c;一般放在head標簽中. (2).外部腳本方式引入. 內部腳本只能在當前頁面中使用&#xff0c;代碼復用度不高.可以將腳本放在單獨的js文件…