C++ STL: 容器vector源码分析

文章目录

      • 前言
      • vector的核心接口
      • vector push_back实现
        • vector 的 Allocator
        • vector 的 push_back
      • 总结

前言


vector 是我们C++STL中经常使用的一个容器,提供容量可以动态增长,并且随机访问内部元素的数据存储功能。

这个容器我们最常使用,所以也是最为熟悉的一种容器。通过它来看STL的源码设计,以及STL六大组件之间的搭配使用,可以让我们对C++这种语言,对优秀的编码设计,对有趣的数据结构和算法都能有深刻的理解和认识。

STL六大组件关系如下:
C++ STL: 容器vector源码分析_第1张图片
其中容器主要是 提供数据存储的功能。分配器用来给容器的数据存储分配空间,算法通过迭代器可以访问容器中的数据。仿函数提供类似于函数的功能,这里算法就是用仿函数来实现的。各个适配器给各个组件做一些功能上的补充。

今天主要对容器vector的实现进行深入的分析。

本节涉及的源代码是基于:gcc 2.95 版本的libstdc++ 标准库进行分析的。不同版本的代码实现方式可能有一些差异,但是核心思想应该是一样的。
gcc-2.95 源码下载

vector的核心接口

我们在使用vector的过程中从声明到使用的基本接口如下:

vecotor<int,std::alloctor<int>> arr(10,0); 
arr.push_back(3);
arr.size();
arr.max_szie();
arr.back();
arr.pop_back();
arr.capacity();
arr.erase(3);
...

详细的就不一一列举了,这里我们主要关注的是push_back()插入的操作,因为只有元素的持续插入,vector的底层数据存储结构才会跟着进行变化,这也是它的核心接口了。

vector push_back实现

首先用一张图来表示vector的基本存储情况以及扩容的过程(其中的字段是源码部中的字段
C++ STL: 容器vector源码分析_第2张图片
左侧的存储代表扩容之前的存储结构,右侧是扩容之后的存储结构。
扩容的过程是两倍的容量扩充,且不是在原来的基础上进行增长的,而是重新分配一块两倍于原来大小的内存空间,将原来的存储空间的元素一个一个拷贝到新的两倍大小的存储空间之中

  • _M_start 代表起始位置的指针
  • _M_finish 代表已存储的元素的末尾位置
  • _M_end_of_storage 代表整个vector空间的结束位置
  • size ,即 我们使用的arr.size() ,表示vector实际存储的元素的个数
  • capacity, 即我们使用的arr.capacity(),表示当前vector可以存储的元素个数(包括已分配空间,但未存储元素的空间区域)

具体实现的类图关系如下:
C++ STL: 容器vector源码分析_第3张图片
从上面的类图关系中我们能够看到最终vector对象的大小sizeof(vector())为24B(x86_64),三个指针类型的大小(_M_start, _M_finish,_M_end_of_storage)

接下来我们来看一下具体的成员函数实现

vector 的 Allocator

vector类 通过函数模版,制定了基本数据类型,以及实例化了分配器的类型

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> 
{
private:
  typedef _Vector_base<_Tp, _Alloc> _Base;
public:
  typedef _Tp value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type* iterator;
  typedef const value_type* const_iterator;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
  ...

这里我们再追踪一下空间分配器 Allocator是如何参与到vector容器之中的。

可以很明显得看到这里分配器使用的是stl默认的分配器 allocator,即

define __STL_DEFAULT_ALLOCATOR(T) allocator<T>

allocator的声明如下,这里主要看一下stl分配器中的allocatedeallocate函数,他们如何进行空间分配

template <class _Tp>
class allocator {
  typedef alloc _Alloc;          // The underlying allocator.
public:
  typedef size_t     size_type;
  typedef ptrdiff_t  difference_type;
  typedef _Tp*       pointer;
  typedef const _Tp* const_pointer;
  typedef _Tp&       reference;
  typedef const _Tp& const_reference;
  typedef _Tp        value_type;
  ......
    // __n is permitted to be 0.  The C++ standard says nothing about what
  // the return value is when __n == 0.
  // 传入要分配的个数n,并调用_Alloc的allocate函数进行空间的分配
  // 分配的总大小是n *  sizeof(_Tp)
  _Tp* allocate(size_type __n, const void* = 0) {
    return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp)))
                    : 0;
  }
  ......

_Alloc的allocate最终会调用malloc_allc::allocate函数,再通过malloc分配空间。

static void* allocate(size_t __n)
{
  _Obj* __VOLATILE* __my_free_list;
  _Obj* __RESTRICT __result;

  //如果一次分配的空间大于128B,则会直调用malloc进行空间的申请
  if (__n > (size_t) _MAX_BYTES) {
      return(malloc_alloc::allocate(__n));
  }

  // 如果不足128B,从空闲链表中取出空间
  __my_free_list = _S_free_list + _S_freelist_index(__n);
  // Acquire the lock here with a constructor call.
  // This ensures that it is released in exit or during stack
  // unwinding.
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif
  __result = *__my_free_list;
  if (__result == 0) {
      void* __r = _S_refill(_S_round_up(__n));
      return __r;
  }
  *__my_free_list = __result -> _M_free_list_link;
  return (__result);
};


static void* allocate(size_t __n)
{
  void* __result = malloc(__n);
  if (0 == __result) __result = _S_oom_malloc(__n);
  return __result;
}

同样allocator的deallocate函数实现如下,最终也会调用到free进行空间的释放

//allocator 的deallocate函数
void deallocate(pointer __p, size_type __n)
  { _Alloc::deallocate(__p, __n * sizeof(_Tp)); }

//_Alloc的deallocate函数
static void deallocate(void* __p, size_t __n)
{
  _Obj* __q = (_Obj*)__p;
  _Obj* __VOLATILE* __my_free_list;

  if (__n > (size_t) _MAX_BYTES) {
      malloc_alloc::deallocate(__p, __n);
      return;
  }
  __my_free_list = _S_free_list + _S_freelist_index(__n);
  // acquire lock
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif /* _NOTHREADS */
  __q -> _M_free_list_link = *__my_free_list;
  *__my_free_list = __q;
  // lock is released here
}

// malloc_alloc的deallocate函数
static void deallocate(void* __p, size_t /* __n */)
{
  free(__p);
}

到此我们就基本清楚了当前版本的vector容器是如何得到具体的存储空间的。

接下来我们继续回到vector的实现之中。

vector 的 push_back

vector的push_back 逻辑有两种

  1. 当vector的内部的finish指针并没有达到end_of_storage指针,即没有达到我们理解的容量的上限的时候,会构造一个_Tp的对象加入到finish指针区域,同时finish指针增加。
  2. 当finish指针到达end_of_storage指针的位置的时候需要重新进行分配。
    分配的过程是 重新分配一块原来存储空间两倍大小的空间,将原来的元素拷贝到新的存储空间。

接下来看一下具体的过程,push_back过程如下

void push_back(const _Tp& __x) {
	//finish指针 没有到达end的位置,表示还有空间可用
   if (_M_finish != _M_end_of_storage) {
     construct(_M_finish, __x);
     ++_M_finish;
   }
   else
   //否则,开始重新分配
     _M_insert_aux(end(), __x);
}

调用了_M_insert_aux函数,在空间不足时进行重新分配。
实现过程如下。

这里我们能看到 函数开头又重新写了是否达到容量上限的判断逻辑。

因为这个函数除了被push_back调用,还会被迭代器的insert调用,导致当前线程想要分配的时候空间已经扩容好了,所以还需要再增加一次空间是否足够的判断。

同时扩容的过程中调用了两次拷贝的过程,也是因为insert的函数,在指定的位置插入元素,可能也会造成vector的扩容,所以插入的位置前后都需要拷贝到新的存储空间之中。

template <class _Tp, class _Alloc>
void 
vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
  if (_M_finish != _M_end_of_storage) {
    construct(_M_finish, *(_M_finish - 1));
    ++_M_finish;
    _Tp __x_copy = __x;
    copy_backward(__position, _M_finish - 2, _M_finish - 1);
    *__position = __x_copy;
  }
  else {
  	/* 获取旧的容量 */
    const size_type __old_size = size();
    const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
    /* 重新分配 */
    iterator __new_start = _M_allocate(__len);
    iterator __new_finish = __new_start;
    /*try...catch子句,进行异常捕获,如果这个过程失败了,会回滚已经分配好了的空间*/
    __STL_TRY {
      /* 
      将原来的vector内容拷贝到当前vector:
	  */
      __new_finish = uninitialized_copy(_M_start, __position, __new_start);
      construct(__new_finish, __x);  // 为新的元素设置初始值,添加到vector末尾
      ++__new_finish; // 调整水位
      // 这里需要再次进行一次拷贝,是因为当前函数可能还会被insert(p,x)调用
      // insert表示在第p个位置插入x元素,这个操作可能也会造成数组的扩容
      // 所以需要将安插点之后的数据拷贝到当前位置
      __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); 
    }

	/*catch*/
    __STL_UNWIND((destroy(__new_start,__new_finish), 
                  _M_deallocate(__new_start,__len)));

	/*销毁旧的存储空间,并更换三个指针为新的地址空间的指针*/
    destroy(begin(), end());
    _M_deallocate(_M_start, _M_end_of_storage - _M_start);
    _M_start = __new_start;
    _M_finish = __new_finish;
    _M_end_of_storage = __new_start + __len;
  }
}

总结

通过 vector的扩容过程的实现,我们能看到vector的扩容会有一些隐患:

  1. 当扩容产生,将旧的空间的元素拷贝到新的空间的元素会触发 临时对象的构造和 拷贝构造函数,此时如果元素体量较为庞大,那大量的新对象的构造和拷贝构造函数调用 会产生一点的性能开销。
  2. 新的空间和元素都拷贝过去之后,旧的空闲也需要释放,此时又会有大量的析构调用来进行空间的释放。

验证代码如下:

#include 
#include 
#include 

using namespace std;

class Test {
private:
  int num;

public:
  Test(int x):num(x){std::cout << "constructed\n";}

  /*拷贝构造函数*/
  Test(const Test &obj) {
      num = obj.num;
      std::cout << "copy constructed\n";
  }
	
  /*C++11 支持的 转移构造函数*/
  Test (Test &&obj) {
      num = std::move(obj.num);
      std::cout << "moved constructed\n";
  }

};

int main() {
  vector<Test> arr;

  std::cout << "push_back : Test(1)\n";
  arr.push_back(1);

  return 0;
}
push_back : Test(1)
constructed
moved constructed

怎么解决呢?
这就是我们C++11在这一方面推出的性能方面的改进逻辑:

  1. push_back 将拷贝构造函数变更为 转移构造(std::move)函数,减少来空间释放的开销
  2. 推出emplace_back函数,使用变长模版参数forward进行对象在的原地构造,只需要一次对象本身的构造即可。

emplace_back函数,详细的实现可以参考empalce_back和push_back的差异

vectoremplace_back实现如下:

vector<_Tp, _Allocator>::emplace_back(_Args&&... __args)
{
    if (this->__end_ < this->__end_cap())
    {
        __RAII_IncreaseAnnotator __annotator(*this);
        __alloc_traits::construct(this->__alloc(),
                                  _VSTD::__to_raw_pointer(this->__end_),
                                  _VSTD::forward<_Args>(__args)...);
        __annotator.__done();
        ++this->__end_;
    }
    else
        __emplace_back_slow_path(_VSTD::forward<_Args>(__args)...);
#if _LIBCPP_STD_VER > 14
    return this->back();
#endif
}

可以看到STL的开发者们 不断得完善逻辑,改进性能。并且不断得推出新的特性来供给我们这一些语言的使用者来使用,如果我们还用不好,那是真的愧对大佬们的付出了。

精进自己,追求极致,向大佬们致敬。

你可能感兴趣的:(#,编程语言:C++,编程语言)