STL原始碼剖析——vector容器

只愛宅zmy發表於2020-09-22
寫在前面
vector是我們在STL中最常用的容器,我們對它的各種操作也都瞭然於胸。然而我們在使用vector的時候總會有一種很虛的感覺,因為我們不清楚介面內部是如何實現的。在我們眼裡宛如一個黑箱,既危險又迷人。
為了打破這種顧慮,接下來我就帶大家深入vector底層,徹底弄懂vector介面內部實現細節,開啟這個黑箱。這樣在使用vector的時候我們也就不會慌了,做到真正的瞭然於胸。
vector 底層原理概述
vector是動態空間,隨著元素的增加,其內部機制會自行擴充空間來容納新元素。
vector動態增加大小時,並不是在原空間之後持續新空間(因為根本無法保證原空間之後尚有可供配置的空間),而是以原大小的兩倍另外配置一塊較大的空間,然後將內容複製過來,然後才開始在原內容之後構造新元素,並釋放原空間,
重點原始碼理解
1.迭代器內部型別
下面我們來看看STL原始碼裡面是如何來定義迭代器的吧。
template <class T, class Alloc = alloc>
class vector {
public:
// vector 的巢狀型別定義
typedef T value_type;
typedef value_type* iterator; // 迭代器本身是一個模板類的物件
typedef value_type& reference;
...
};
如上面程式碼所示,迭代器iterator本身是一個類型別,運算子*被過載。迭代器iterator指向vector的內部元素,可以理解為iterator與vector的內部元素捆綁在一起,其行為類似指標,但是又不能把它當作指標。
靈魂拷問一:迭代器與指標有什麼區別?
我們可以這樣理解,迭代器本質上就是模板類產生的一個物件,而其運算子*和->都是經過運算子過載實現的。這個物件指向vector的內部元素(元素又是迭代器的物件),所以當迭代器指向的元素被刪除或者移動,迭代器與元素就斷開連結,迭代器也就沒有用了,也就是我們通常說的迭代器失效。迭代器的行為類似指標,但是又有所區別。
反觀指標,指標與記憶體是聯絡在一起的。如果指標指向的記憶體地址儲存的元素被刪除或者移動,指標並不會因此失效,它依然指向了該地址。
根據上述定義,迭代器可以這樣宣告:
vector::iterator ivite;
vector::iterator svite;
看完上面的原始碼,我們也就清楚為什麼迭代器要這樣宣告瞭。
2.vector 的資料結構
vector使用兩個迭代器start和finish來表示已使用空間的範圍,並以迭代器end_of_storage指向分配空間的尾端。程式碼如下:
template <class T, class Alloc = alloc>
class vector {
...
protected:
iterator start; // 表示目前使用空間的頭
iterator finish; // 表示目前使用空間的尾,即最後一個元素的下一個元素
iterator end_of_storage; // 表示目前分配的整個空間的尾
...
};
利用以上三個迭代器,我們能夠封裝vector的各種成員函式。
template <class T, class Alloc = alloc>
class vector {
...
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return size_type(end() - begin()); }
bool empty() const { return begin() == end(); }
reference front() { return *begin(); }
reference back() { return *(end() - 1); }
reference operator[](size_type n) { return *(begin() + n); }// 運算子[]過載,能夠使用迭代器來訪問元素
};
上面一些基礎操作已經一目瞭然了,這裡就不一一述說了。這裡只提兩點,第一,從上面程式碼可以看出operator對運算子[]進行了過載,這樣能夠使迭代器像陣列索引一樣遍歷vector。
第二,迭代器finish指向的是vector最後一個元素的下一個元素,封裝的end()函式也如此。這也就是我們常常說的vector的前閉後開特性。
靈魂拷問二:為什麼容器要設計成前閉後開的特性?
這樣做是為了在遍歷容器元素時減少判斷條件。因為STL的核心是泛型程式設計,使得設計的介面是通用的。由於只有部分容器支援>和<運算子過載,而!=則是全部容器都支援,所以遍歷元素的時候優先使用!=過載運算子。
如果將end()指向容器最後一個元素的下一個,則遍歷操作只需要寫成:
vector vec;
auto it = vec.begin();
while (it != vec.end()) {
...
++it;
}
但是如果end()指向的是最後一個元素,上述程式碼會少遍歷一個元素,這就需要在while迴圈裡增加額外的判斷條件,並且這個判斷條件可能因容器的不同要進行修改,而上述程式碼在任何順序容器都能這樣呼叫,減少了很多多餘工作。
3.vector 的元素操作
vector 的建構函式
vector的建構函式有多種形式,下面摘取原始碼中的部分程式碼:
// 建構函式,允許指定 vector 大小和初值
vector() : start(0), finish(0), end_of_storage(0) {}
vector(size_type n, const T& value) { fill_initialize(n, value); }
explicit vector(size_type n) { fill_initialize(n, T()); }
分別對應如下初始化:
vector vec;
vector vec(2,3);
vector vec(2);
push_back() 與 pop_back()
當我們以push_back()將新元素插入vector尾端時,該函式首先檢查是否還有備用空間,如果有就直接在備用空間上構造元素,並調整迭代器finish。如果沒有備用空間了,就擴充空間(重新配置、移動資料、釋放原空間)。
void push_back(const T& x) {
if (finish != end_of_storage) { // 還有備用空間
construct(finish, x);
++finish;
}
else // 已無備用空間
insert_aux(end(), x); // 插入函式
}
插入函式原型為:
void insert_aux(iterator position, const T& x);
這個函式比較長,具體思路:在有備用空間情況下,在備用空間起始處構造一個元素,迭代器finish自增一;在無備用空間情況下,重新配置兩倍的原記憶體空間,將原vector的內容複製到新vector中,再釋放掉原空間。
注:插入函式是將元素插入到對應位置,原先該位置以及後面的元素都向後移動一位。
刪除vector尾部元素操作pop_back()更加簡單。
void pop_back() {
--finish;
destroy(finish);
}
直接將尾部迭代器finish向前移動一位,然後釋放掉。由於尾部迭代器finish指向的是最後一個元素的下一位,所以減一後正好是原來的最後一個元素。
erase() 與 clear()
erase()表示刪除vector的某一個元素或者某一區間內的所有元素。
// 刪除 vector 的某一個位置的元素
iterator erase(iterator position) {
if (position + 1 != end())
copy(position + 1, finish, position);
--finish;
destroy(finish);
return position;
}
// 刪除 vector 的某一個區間的元素
iterator erase(iterator first, iterator last) {
iterator i = copy(last, finish, first);
destroy(i, finish);
finish = finish - (last - first);
return first;
}
如果不對erase()函式謹慎使用,可能會出現迭代器失效的問題。
靈魂拷問三:在什麼情況下使用erase()函式迭代器會失效?
通常我們寫出這樣的程式碼迭代器會失效。
for(auto it = vec.begin();it != vec.end();++it) {
if(/* 刪除某元素的判斷條件 */) {
vec.erase(it);
}
}
由靈魂拷問一可知,刪除元素後由於被刪除元素後面的資料都會發生移動,所以後面的迭代器都會失效。故上述程式碼在刪除了某個迭代器後,後面的++it遍歷已經失去意義,不會得到正確的結果。
那應該如何更改呢?由前面刪除vector的某一個位置的元素的原始碼可知,erase()返回的是一個迭代器,這個迭代器實際上是被刪除元素的下一個元素繫結的迭代器,這個迭代器是資料移動後新的有效的迭代器。也可以說是更新了迭代器。
正確的寫法為:
for(auto it = vec.begin();it != vec.end();) {
if(/* 刪除某元素的判斷條件 */) {
it = vec.erase(it); // 更新了迭代器
}
else {
++it;
}
}
clear()表示清空vector上的所有元素。
void clear() { erase(begin(), end()); }
作者:程式設計異思坊
連結:
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983372/viewspace-2723116/,如需轉載,請註明出處,否則將追究法律責任。

相關文章