1. 程式人生 > 其它 >STL 之 vector

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_backemplace_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;
    }

參考連結

[1] https://www.zhihu.com/question/21677869