什么是vector
vector是一個封裝了動態大小數組的順序容器跟任意其它類型容器一樣,它能夠存放各種類型的對象。
模擬實現
實現前的準備
在實現vector之前,為了和庫里的區分開需要將實現的vector放在一個自定義的命名空間里。而且vector需要實現成模版的方式,所以需要我們傳模版參數,模版參數也就是順序容器所存的數據類型。而且根據庫里的vector,有三個關鍵的成員變量,分別指向順序容器的起始位置,數據容量位置,空間容量位置。
namespace cr
{template<class T>class vector{public:private:iterator _start;iterator _finish;iterator _endofstorage;};
}
迭代器的相關實現?
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{return _start;
}
iterator end()
{return _finish;
}
const iterator begin() const
{return _start;
}
const iterator end() const
{return _finish;
}
這里要提醒的就是,_finish指向的是最后一個有效數據的下一個位置,_endofstorage指向的是對象的最大容量位置處的下一個位置。
下標引用operator[]
T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}
這里需要注意的是:不要忘記斷言
容器大小
size_t capacity() const
{return _endofstorage - _start;
}
size_t size() const
{return _finish - _start;
}
為了保證const對象調用該函數時也可以正常通過,所以對該函數進行const修飾。
析構函數
~vector()
{delete[] _start;
}
擴容reserve
void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (sz != 0){memcpy(tmp, _start, sizeof(T) * sz);delete[] _start;//勿忘}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}
}
這里進行擴容時一定要注意判斷空間大小關系,尤其是要注意三個指針的指向問題,在此之前記錄好原空間中有效數據的個數,因為執行new操作都是異地擴容,所以_start存的地址肯定發生改變,如果此時通過_start求得的_finish就一定會出錯,所以需要提前記錄好位置關系。但是該函數不一定正確
尾插數據push_back?
void push_back(const T& x) {if (_finish == _endofstorage)reserve(size() == 0 ? 4 : capacity() * 2);*_finish = x;_finish++; }
push_back需要注意的地方就是當空間大小為0時需要擴的大小,以及_finish++。這里來驗證一下自內置類型的數據結果:
?再看看string類的數據結果:引發了異常: 讀取訪問權限沖突。
?其實這里的問題哦就是reserve的實現出了問題:
?當空間不夠時會發生擴容,就會調用memcpy函數,memcpy是庫里的,而且我們知道memcpy內部就是通過值拷貝將_start解引用進行拷貝給tmp(_start是一個string類的指針),所以此時拷貝好的數據相當于是淺拷貝,指向如下:
?然后調用析構函數,將_start空間內存釋放,但是_start內部數據是string類,所以會先調用string的析構函數,釋放string得空間,所以此時就是報錯所在:tmp內的string同樣也銷毀了。
修改reverse函數
void reserve(size_t n) {if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (sz != 0){//memcpy(tmp, _start, sizeof(T) * sz);for (int i = 0; i < sz; i++){tmp[i] = _start[i];//如果T是string,調用std::string的賦值重載}delete[] _start;//勿忘}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;} }
這種拷貝的方式就有效避免了淺拷貝的問題,如果模版參數T是string類的話,tmp[i]的類型就是string,所以此時的賦值就會調用string的賦值重載operator=()進行深拷貝,所以就不會出問題。
改變有效數據個數resize
void resize(size_t n, const T& x = T())//匿名對象
{if (n > size()){reserve(n);while (_finish != _endofstorage){*_finish = x;_finish++;}}else{_finish = _start + n;}}
這里要注意的就是T x給的缺省值T(),T()其實就是調用匿名對象的構造函數,然后在調用拷貝構造給x對象,而編譯器會優化成直接的構造。匿名對象具有常性(不能&(同一塊空間)不加const),出了該行就自動析構,const引用可以延長匿名對象的生命周期。但是如果是內置類型,這樣寫也是沒問題的,其實內置類型也是可以看做有構造函數的。就如下測試:
?構造函數
vector(size_t n=0,const T& val=T()):_start(nullptr), _finish(nullptr), _endofstorage(nullptr) {reserve(n);while (n--){push_back(val);} }
拷貝構造
vector(const vector<T>& v):_start(new T[v.capacity()]),_finish(_start),_endofstorage(_start+v.capacity()) {for (auto ch : v){*_finish = ch;//如果T是string,調用std::string的賦值重載_finish++;} }
通過迭代器區間初始化的構造函數
//在一個類模版里面還可以繼續寫模版函數 template<class InputIterator> vector(InputIterator first, InputIterator last):_start(nullptr), _finish(nullptr), _endofstorage(nullptr) {while (first != last){push_back(*first);first++;} }
這里迭代器區間的構造函數是需要實現成模版類型的,因為庫里面的STL說明不同的容器可以通過迭代器區間進行初始化,只不過有點不兼容的可以進行強制類型轉換:
但是當執行到構造函數時:
cr::vector<int> vv(3, 1);
會報錯:非法的間接尋址??
原因:當我們調用構造函數時,會優先找最匹配的構造函數進行調用,所以此時以上的三種構造函數就會調用迭代器區間構造函數,而不是第一種
vector(size_t n=0,const T& val=T())//一般構造 vector(InputIterator first, InputIterator last)迭代器區間構造
調用第一種時會發生int類型到size_t類型的轉換,而第三種構造函數會直接調用,因為第三種構造有模版參數,而對于兩個參數(3,1)都是int類型,所以此時模版形參InputIterator就是替換成int型,所以此時第三種更匹配。
解決方法:
- 將第一個構造函數的形參改變成int類型
- 再生成一個構造函數形成重載
vector(int n, const T& val = T()):_start(nullptr), _finish(nullptr), _endofstorage(nullptr) {reserve(n);while (n--){push_back(val);} }
賦值重載operator=?
vector<T>& operator=(vector<T> v)
{swap(_start, v._start);swap(_finish, v._finish);swap(_endofstorage, v._endofstorage);return *this;
}
這里是要一個返回值的,主要是避免連等的情況:a=b=c;
在某位置插入數據insert
void insert(iterator pos, T x) {assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage);{reserve(_start==nullptr?4:capacity() * 2);}iterator end = _finish-1;while (end >= pos){*(end + 1) = *end;end--;}*pos = x;_finish++; }
以上代碼其實是有問題的,如下測試:
調試時你會發現是在insert內部出了問題?而且會在下面代碼不斷循環
while (end >= pos) {*(end + 1) = *end;end--; }
所以問題極可能出現在上面的代碼段:?
此時pos依舊是nullptr,就相當于pos指向的依舊是原空間的地址,插入數據擴容時,空間都發生了變化,所以再拿原空間的指針和新空間的指針比較就不妥。
修改代碼:其實就是記錄pos位置和_start之間的差距,好在擴容到新空間時,將差距補上
void insert(iterator pos, T x)//insert使用之后迭代器失效最好不要使用 {assert(pos >= _start && pos <= _finish);//不要忘記斷言if (_finish == _endofstorage);{size_t len = pos - _start;reserve(_start==nullptr?4:capacity() * 2);//異地擴容改變了_start,可能導致pos成為野指針pos = _start + len;}iterator end = _finish-1;while (end >= pos){*(end + 1) = *end;end--;}*pos = x;_finish++; }
?雖然修改了代碼,編譯起來也沒問題,但是如果你執行下列操作還是跑不通:
?此時是斷言處出問題了,其實這種情況屬于迭代器失效因為你插入數據時發生了擴容了,所以同上面一樣,it指針指向的是原空間的首個位置,所以就會錯誤
解決方法:每次插入數據是重新調用迭代器v.begin()或v.end()來獲得迭代器的位置。
在某位置刪除數據erase
void erase(iterator pos) {assert(pos < _finish&& pos >= _start);iterator end = pos + 1;while (end < _finish){*(end - 1) = *end;end++;}_finish--; }
這里和inssert是不同的,erase不會發生擴容,所以也不存在迭代器失效,但是這erase要注意的就是,每次刪除一個數據時位置會發生挪動,所以一般在刷題時一定要注意這個小細節。
而VS下的erase是并不是這樣實現的,VS下的erase也只能使用一次,用完了該指針也就是失效了。盡管進行it++還是--來改變it再使用也是不可以的。
?所以可以借鑒insert的用法:
?但是VS下的erase其實是有返回值的,
其實返回的就是你刪除的那個數據的下一個位置,但是位置會發生挪動,所以,返回的也就是pos位置?
如有解釋不當,歡迎大家留言!
?