跟我学C++中级篇——STL的容器vector

223 篇文章 92 订阅

 

一、顺序容器vector

C++程序员中,如果用到过STL,那么一定肯定用过vector,这个是最常见,最初步的一个数据类型。上一篇提到的array远远比不上它。毕竟那玩意儿相对vector是很久远后才提出来的。在这之前,std::vector承担了多少小菜鸟处理
数组各种问题的最优选方法。不用处理内存,可以删除,任意增加不考虑越界。那简直是一种最单纯质朴的快乐。
如果单纯的只考虑开发目标,而不考虑对内存占用和再分配导致的性能下降,不用考虑删除时迭代器的问题,那么几乎这玩意儿就可以能抵挡住大多数情况下的对数组的使用了,可惜,没有单纯的想法,还是得面对现实。std::vector重载了[],所以支持类似数组一样的随机访问,你可以简单理解成它就是一个大数组,只不过他更强大,支持迭代器访问和动态处理(删除、增加等),且不需要你担心对内存的处理。

二、源码

std:vector的源码很容易找到,其实你看它代码也不复杂,之所以看上去眼花缭乱的原因不是因为他复杂,而是为了兼容和安全性搞了好多新功能和方法,再加上一些模板本身的技巧。这些都可以暂时忽略过去,重点看重点的相关的函数方法的实现。
向量的构造函数其实就是两部分模板类型名称和分配器,而分配器一般使用默认的分析器类型,这个回头再分配器中再详细分析。

 template <class _Ty, class _Alloc = allocator<_Ty>>
 class vector { // varying size array of values
 private:
     template <class>
     friend class _Vb_val;
     friend _Tidy_guard<vector>;

     using _Alty        = _Rebind_alloc_t<_Alloc, _Ty>;
     using _Alty_traits = allocator_traits<_Alty>;

 public:
     static_assert(!_ENFORCE_MATCHING_ALLOCATORS || is_same_v<_Ty, typename _Alloc::value_type>,
         _MISMATCHED_ALLOCATOR_MESSAGE("vector<T, Allocator>", "T"));

     using value_type      = _Ty;
     using allocator_type  = _Alloc;
     using pointer         = typename _Alty_traits::pointer;
     using const_pointer   = typename _Alty_traits::const_pointer;
     using reference       = _Ty&;
     using const_reference = const _Ty&;
     using size_type       = typename _Alty_traits::size_type;
     using difference_type = typename _Alty_traits::difference_type;

 private:
     using _Scary_val = _Vector_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Simple_types<_Ty>,
         _Vec_iter_types<_Ty, size_type, difference_type, pointer, const_pointer, _Ty&, const _Ty&>>>;

 public:
     using iterator               = _Vector_iterator<_Scary_val>;
     using const_iterator         = _Vector_const_iterator<_Scary_val>;
     using reverse_iterator       = _STD reverse_iterator<iterator>;
     using const_reverse_iterator = _STD reverse_iterator<const_iterator>;

     vector() noexcept(is_nothrow_default_constructible_v<_Alty>) : _Mypair(_Zero_then_variadic_args_t{}) {
         _Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
     }

     explicit vector(const _Alloc& _Al) noexcept : _Mypair(_One_then_variadic_args_t{}, _Al) {
         _Mypair._Myval2._Alloc_proxy(_GET_PROXY_ALLOCATOR(_Alty, _Getal()));
     }
......
public:
    vector& operator=(vector&& _Right) noexcept(noexcept(_Move_assign(_Right, _Choose_pocma<_Alty>{}))) {
        if (this != _STD addressof(_Right)) {
            _Move_assign(_Right, _Choose_pocma<_Alty>{});
        }

        return *this;
    }

    ~vector() noexcept {
        _Tidy();
#if _ITERATOR_DEBUG_LEVEL != 0
        auto&& _Alproxy = _GET_PROXY_ALLOCATOR(_Alty, _Getal());
        _Delete_plain_internal(_Alproxy, _STD exchange(_Mypair._Myval2._Myproxy, nullptr));
#endif // _ITERATOR_DEBUG_LEVEL != 0
    }
    ......
    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));
}
template <class... _Valty>
iterator emplace(const_iterator _Where, _Valty&&... _Val) { // insert by perfectly forwarding _Val at _Where
    const pointer _Whereptr = _Where._Ptr;
    auto& _My_data          = _Mypair._Myval2;
    const pointer _Oldlast  = _My_data._Mylast;
#if _ITERATOR_DEBUG_LEVEL == 2
    _STL_VERIFY(
        _Where._Getcont() == _STD addressof(_My_data) && _Whereptr >= _My_data._Myfirst && _Oldlast >= _Whereptr,
        "vector emplace iterator outside range");
#endif // _ITERATOR_DEBUG_LEVEL == 2

    if (_Oldlast != _My_data._Myend) {
        if (_Whereptr == _Oldlast) { // at back, provide strong guarantee
            _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
        } else {
            auto& _Al = _Getal();
            _Alloc_temporary<_Alty> _Obj(_Al, _STD forward<_Valty>(_Val)...); // handle aliasing
            // after constructing _Obj, provide basic guarantee
            _Orphan_range(_Whereptr, _Oldlast);
            _Alty_traits::construct(_Al, _Unfancy(_Oldlast), _STD move(_Oldlast[-1]));
            ++_My_data._Mylast;
            _Move_backward_unchecked(_Whereptr, _Oldlast - 1, _Oldlast);
            *_Whereptr = _STD move(_Obj._Storage._Value);
        }

        return _Make_iterator(_Whereptr);
    }

    return _Make_iterator(_Emplace_reallocate(_Whereptr, _STD forward<_Valty>(_Val)...));
}

iterator insert(const_iterator _Where, const _Ty& _Val) { // insert _Val at _Where
    return emplace(_Where, _Val);
}

iterator insert(const_iterator _Where, _Ty&& _Val) { // insert by moving _Val at _Where
    return emplace(_Where, _STD move(_Val));
}
......
void clear() noexcept { // erase all
    auto& _My_data    = _Mypair._Myval2;
    pointer& _Myfirst = _My_data._Myfirst;
    pointer& _Mylast  = _My_data._Mylast;

    _My_data._Orphan_all();
    _Destroy(_Myfirst, _Mylast);
    _Mylast = _Myfirst;
}

public:
void swap(vector& _Right) noexcept /* strengthened */ {
    if (this != _STD addressof(_Right)) {
        _Pocs(_Getal(), _Right._Getal());
        _Mypair._Myval2._Swap_val(_Right._Mypair._Myval2);
    }
}

_NODISCARD _Ty* data() noexcept {
    return _Unfancy_maybe_null(_Mypair._Myval2._Myfirst);
}

_NODISCARD const _Ty* data() const noexcept {
    return _Unfancy_maybe_null(_Mypair._Myval2._Myfirst);
}

_NODISCARD iterator begin() noexcept {
    auto& _My_data = _Mypair._Myval2;
    return iterator(_My_data._Myfirst, _STD addressof(_My_data));
}

_NODISCARD const_iterator begin() const noexcept {
    auto& _My_data = _Mypair._Myval2;
    return const_iterator(_My_data._Myfirst, _STD addressof(_My_data));
}

_NODISCARD iterator end() noexcept {
    auto& _My_data = _Mypair._Myval2;
    return iterator(_My_data._Mylast, _STD addressof(_My_data));
}

_NODISCARD const_iterator end() const noexcept {
    auto& _My_data = _Mypair._Myval2;
    return const_iterator(_My_data._Mylast, _STD addressof(_My_data));
}

_NODISCARD reverse_iterator rbegin() noexcept {
    return reverse_iterator(end());
}

_NODISCARD const_reverse_iterator rbegin() const noexcept {
    return const_reverse_iterator(end());
}

_NODISCARD reverse_iterator rend() noexcept {
    return reverse_iterator(begin());
}

_NODISCARD const_reverse_iterator rend() const noexcept {
    return const_reverse_iterator(begin());
}

_NODISCARD const_iterator cbegin() const noexcept {
    return begin();
}

_NODISCARD const_iterator cend() const noexcept {
    return end();
}

_NODISCARD const_reverse_iterator crbegin() const noexcept {
    return rbegin();
}

_NODISCARD const_reverse_iterator crend() const noexcept {
    return rend();
}
......
}

其中有大量的简化别名,这个也导致了初学者的看着不“顺眼”。慢慢来,习惯就好。
std::vector是线性的或者说平坦的空间,所以访问它的元素,从理论上讲更容易,也就是前面提到的随机访问。迭代器在线性空间上的应用也只要控制好指针的位置即可,而不强需要指针的迭代性。许多操作看上去挺简单,其实都反复对性能和应用的平衡的结果,这也是为什么有很多人一直说STL的不好的主要原因。一定要搞清楚到底是为什么这么做,明白一个原因,其它也就全通了。学习STL的过程,就是学习C++大牛们的思想的过程,这才是最重要的部分,一定不能念歪了经。
vector的内存管理使用两级策略:
第一级 __malloc_alloc_template内存分配器
此分配器是对malloc、realloc以及free的调用封装;

第二级 __default_alloc_template
其实就是维护一个内存池,一个链表s_free_list可以把这些空闲的内存连接起来,写过内存池或者接触过的一说应该就明白,其大小为16。这16个链表中每个链表中的空闲空间的大小都是固定的8,16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128字节。
那么实际分配就好说了,大于128的使用第一种,否则使用当前内存池中的链表遍历,查找可用空闲找到后直接使用,否则*8,调用refill再次分配。
释放相对就简单了,同样分成两部分,大于128字节转第一个调用free,否则加入回收空闲列表。

template <class... _Valty>
pointer _Emplace_reallocate(const pointer _Whereptr, _Valty&&... _Val) {
    // reallocate and insert by perfectly forwarding _Val at _Whereptr
    _Alty& _Al        = _Getal();
    auto& _My_data    = _Mypair._Myval2;
    pointer& _Myfirst = _My_data._Myfirst;
    pointer& _Mylast  = _My_data._Mylast;

    _STL_INTERNAL_CHECK(_Mylast == _My_data._Myend); // check that we have no unused capacity

    const auto _Whereoff = static_cast<size_type>(_Whereptr - _Myfirst);
    const auto _Oldsize  = static_cast<size_type>(_Mylast - _Myfirst);

    if (_Oldsize == max_size()) {
        _Xlength();
    }

    const size_type _Newsize     = _Oldsize + 1;
    const size_type _Newcapacity = _Calculate_growth(_Newsize);

    const pointer _Newvec           = _Al.allocate(_Newcapacity);
    const pointer _Constructed_last = _Newvec + _Whereoff + 1;
    pointer _Constructed_first      = _Constructed_last;

    _TRY_BEGIN
    _Alty_traits::construct(_Al, _Unfancy(_Newvec + _Whereoff), _STD forward<_Valty>(_Val)...);
    _Constructed_first = _Newvec + _Whereoff;

    if (_Whereptr == _Mylast) { // at back, provide strong guarantee
        _Umove_if_noexcept(_Myfirst, _Mylast, _Newvec);
    } else { // provide basic guarantee
        _Umove(_Myfirst, _Whereptr, _Newvec);
        _Constructed_first = _Newvec;
        _Umove(_Whereptr, _Mylast, _Newvec + _Whereoff + 1);
    }
    _CATCH_ALL
    _Destroy(_Constructed_first, _Constructed_last);
    _Al.deallocate(_Newvec, _Newcapacity);
    _RERAISE;
    _CATCH_END

    _Change_array(_Newvec, _Newsize, _Newcapacity);
    return _Newvec + _Whereoff;
}

在c++11中提供了shrink_to_fit()函数,可以减少相关的内存使用,不过需要注意的,一定要看使用后是否引起了迭代器的失效,如果已经失效一定要重新处理迭代器的引用,否则会引起崩溃。

三、例程

std::vector的成员有很多,这里不一一介绍,看一下下面的例程,有什么不明白的直接查相关帮助即可:

#include <vector>
#include <iostream>
#include <iterator>

void TestAdd()
{
    std::vector<int> vec;
    vec.push_back(0);
    std::cout << "vec size:" << vec.size() << " vec capacity:" << vec.capacity() << std::endl;
    vec.emplace(vec.begin(),1);
    vec.emplace_back(2);
    vec.insert(vec.end(),3);

    for (auto v : vec)
    {
        std::cout << "vec value:" << v << std::endl;
    }

    vec.resize(100);
    std::cout << "vec size:" << vec.size() << " vec capacity:" << vec.capacity() << std::endl;
    vec.shrink_to_fit();
    std::cout << "vec size:" << vec.size() << " vec capacity:" << vec.capacity() << std::endl;
}
void TestDel()
{
    std::vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
    std::vector<int>::iterator it = vec.begin();

    std::cout << "first is:" << vec.front() << "end is:" << vec.back() << std::endl;
    std::cout << "second is:" << vec[1] <<"at three:"<<vec.at(3)<< std::endl;

    for (; it != vec.end(); /*it++*/)
    {
        if (*it == 3)
        {
            it = vec.erase(it);
        }
        else
        {
            ++it;
        }

    //此处会出现异常,留给大家思考
    std::cout << "vec content:" << *it << std::endl;

    }

    vec.pop_back();
    vec.empty();
    vec.clear();
  std::vector<int>().swap(vec);

    for (auto it = vec.begin(); it != vec.end(); it++)
    {
        std::cout << "vec content:" << *it << std::endl;
    }

}
int main()
{
    TestAdd();
    TestDel();
    return 0;
}

运行结果是:

vec size:1 vec capacity:1
vec value:1
vec value:0
vec value:2
vec value:3
vec size:100 vec capacity:100
vec size:100 vec capacity:100
first is:0end is:9
second is:1at three:3
vec content:1
vec content:2
vec content:3
vec content:4
vec content:5
vec content:6
vec content:7
vec content:8
vec content:9
vec content:

四、注意点

使用std::vector需要注意的有以下几点:
1、空间的分配问题
不建议在小内存环境下申请使用较大的vector对象,容易引起内存的不足。前面提到过,如果使用默认的分配器,内存的处理会比实际需要的大好多,特别是做一些插入操作时,遇到临界点会引起重新分配(大约是1.5到2倍)和拷贝,也很耗费时间。

2、自定义对象的操作符重载问题
如果使用自定义对象,因为迭代器的需要,一定在结构体或类中实现对操作符==,=及相关操作符的重载。

3、删除元素时一定要及时获取下一个对象的迭代器
如果想当然的删除,可能会引起崩溃,这个崩溃几回才好,你才能记得更清楚,正所谓“事非经过不知难”,说得再好听,大多数人都得经过几次才长心。

4、添加元素尽量使用emplace_back
原因是可以减少拷贝对象的次数。c++11提供的move语义也提供了更高效的方式。

五、总结

其实写这个STL,就很犹豫,毕竟前面有大神侯捷的STL源码剖析,而应用网上有官方的网站,各种例程都很多并且相当清晰。但是为啥还是写了下来呢,除了是让中级c++篇更完善,另外一个原因是想把更新的一些c++标准的东西揉合进来,不过限于个人的能力,可能会有不少问题,希望看到后,能及时提出来,共同进步。

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值