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
}

问:扩容的时候,元素是如何动的?

首先在 C++11 之后,“移动语义” 肯定要安排上吧,毕竟原来的数组是要丢弃了的,所以直接移动就好了。接下来,vector 的内存管理归根结底还是由 allocator 来负责,所以最终调用的还是要走到 allocator.construct 这个方法去。此外有个细节:如果 vector 的模板参数有 move constructor,那么就会调用;如果没有,那么调用 copy constructor。如果 copy / move 都被 delete 了,那么编译时会报错。

问: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。比较常犯的错误是,一边遍历,一边删除,删除之后迭代器是会失效的。什么是失效呢?在 MSVC 里面,iterator 变成了空指针,在 GCC 里面,iterator 里面存储的指针仍然指向原地址(比如 insert 了之后,iterator 仍然指向 insert 之前的位置)。所以失效的意思是,不满足用户想要的语义。假如用户知道 gcc 那样的实现,就是要那么用,那就算不上失效了hhh

以 MSVC 的代码为例子看看吧。我们可以找到 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

原文地址:https://www.cnblogs.com/zzk0/p/15550783.html