STL 之 vector
vector
目前用的最多的容器,沒有之一。非常有必要更多地瞭解它。vector 是動態陣列,陣列的容量不是固定的。它的原理很簡單,當陣列的元素數量達到了容量時,插入新的元素會發生擴容。擴容會開一塊新的記憶體出來,然後將元素複製過去,擴容的大小為 1.5 倍。
介面
vector 提供了哪些介面,看文件即可。
文件:https://www.cplusplus.com/reference/vector/vector/
注意事項:
- begin/end 是前開後閉區間,即 begin 指向首元素,end 指向尾元素的後一個位置。
- 注意區分 size capacity resize reverse
- resize 是否擴容取決於是否大於 capacity,大於則擴容
- insert 是插入到 “迭代器” 之前,用的是迭代器
- 效率上微小的區別:emplace vs. insert, push_back vs. emplace_back
問與答
問:擴容的演算法是怎麼樣子的?
答:初始情況下大小為 0,傳入的 _Newsize 為 1,於是第一次擴容大小變為 1。後續就按照這個公式計算下一次擴容時候的大小,每次擴容為 1.5 倍。
size_type _Calculate_growth(const size_type _Newsize) const { // given _Oldcapacity and _Newsize, calculate geometric growth const size_type _Oldcapacity = capacity(); const auto _Max = max_size(); if (_Oldcapacity > _Max - _Oldcapacity / 2) { return _Max; // geometric growth would overflow } const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2; if (_Geometric < _Newsize) { return _Newsize; // geometric growth would be insufficient } return _Geometric; // geometric growth is sufficient }
問:push_back
和 emplace_back
之間的區別?
答:可能會多一次移動建構函式的呼叫,你想想看這兩個函式的引數有什麼區別?
empalce_back
使用可變引數模型,可以接收引數,用這些引數直接在 vector 的尾部構造元素,從而減少了一次移動構造的開銷。push_back
先構造出一個臨時物件,後會進行移動構造。push_back
內部的實現其實只是呼叫了emplace_back
,所以絕大多數情況是沒有區別的。除了emplace_back
可以直接在對應的位置建立構造器。這啟發我們,如果要構造臨時物件,那麼用 emplace_back。其他則沒有區別。
// push_back 其實就是呼叫 emplace_back void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee emplace_back(_Val); } void push_back(_Ty&& _Val) { // insert by moving into element at end, provide strong guarantee emplace_back(_STD move(_Val)); } // emplace_back 內部實現,可以直接在陣列上構建元素 template <class... _Valty> decltype(auto) _Emplace_back_with_unused_capacity(_Valty&&... _Val) { // insert by perfectly forwarding into element at end, provide strong guarantee auto& _My_data = _Mypair._Myval2; pointer& _Mylast = _My_data._Mylast; _STL_INTERNAL_CHECK(_Mylast != _My_data._Myend); // check that we have unused capacity // 在元素尾部構造元素 _Alty_traits::construct(_Getal(), _Unfancy(_Mylast), _STD forward<_Valty>(_Val)...); _Orphan_range(_Mylast, _Mylast); _Ty& _Result = *_Mylast; ++_Mylast; return _Result; }
問:vector 為什麼不給用引用型別?
答:因為不允許使用 “指向引用的指標”。說到底 vector 總是需要分配記憶體的,用到了 allocator。如果 vector 的泛型引數是引用型別,那麼 allocator 內部的就有一個 “指向引用的指標”。下面報的錯誤大部分來自於 allocator,引用型別在模板例項化的時候出錯了。更進一步地說,其實只要用了 allocator 的容器,就是不能用引用型別。注意區分,“指向指標的引用” 這個又是可以的。至於為什麼不能有 “指向引用的指標”,我很贊同 [1] 的回答,因為引用本身是不能修改 “指向” 的指標,那麼指向它的指標就沒有意義,標準委員會說不能,那就不能吧。核心就是要理解到引用和指標的區別:引用的指向是不可變的了。
問:迭代器失效的情況?
答:擴容,erase。比較常犯的錯誤是,一邊遍歷,一邊刪除,刪除之後迭代器是會失效的。什麼是失效呢?iterator 變成了空指標。我們可以看到 earse 的程式碼,其中有一段會呼叫下面的 _Orphan_range 來使到 first 和 last 之間全部置空。
// 使某個範圍內的 iterator 失效
void _Orphan_range(pointer _First, pointer _Last) const { // orphan iterators within specified (inclusive) range
#if _ITERATOR_DEBUG_LEVEL == 2
_Lockit _Lock(_LOCK_DEBUG);
_Iterator_base12** _Pnext = &_Mypair._Myval2._Myproxy->_Myfirstiter;
while (*_Pnext) {
const auto _Pnextptr = static_cast<const_iterator&>(**_Pnext)._Ptr;
if (_Pnextptr < _First || _Last < _Pnextptr) { // skip the iterator
_Pnext = &(*_Pnext)->_Mynextiter;
} else { // orphan the iterator
// _Myproxy 是 iterator 內部儲存的資料結構, erase 會將當前到最後置空
(*_Pnext)->_Myproxy = nullptr;
*_Pnext = (*_Pnext)->_Mynextiter;
}
}
#else // ^^^ _ITERATOR_DEBUG_LEVEL == 2 ^^^ // vvv _ITERATOR_DEBUG_LEVEL != 2 vvv
(void) _First;
(void) _Last;
#endif // _ITERATOR_DEBUG_LEVEL == 2
}
// iterator 過載了 * 操作符, 通過 _Myproxy -> _Mycont -> _Myfirst 來判斷是否在範圍,是否已經失效
_NODISCARD reference operator*() const noexcept {
#if _ITERATOR_DEBUG_LEVEL != 0
const auto _Mycont = static_cast<const _Myvec*>(this->_Getcont());
_STL_VERIFY(_Ptr, "can't dereference value-initialized vector iterator");
_STL_VERIFY(
_Mycont->_Myfirst <= _Ptr && _Ptr < _Mycont->_Mylast, "can't dereference out of range vector iterator");
#endif // _ITERATOR_DEBUG_LEVEL != 0
return *_Ptr;
}