zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    vue组件详解(四)——使用slot分发内容
    vue组件详解(三)——组件通信
    vue组件详解(二)——使用props传递数据
    vue组件详解(一)——组件与复用
    vue表单详解——小白速会
    vue class与style 绑定详解——小白速会
    vue内置指令详解——小白速会
    vue计算属性详解——小白速会
    SQL查询当天、本周、本月记录详解
    SQL Server中使用convert进行日期转换
  • 原文地址:https://www.cnblogs.com/zzk0/p/15550783.html
Copyright © 2011-2022 走看看