vector 是我们C++STL中经常使用的一个容器,提供容量可以动态增长,并且随机访问内部元素的数据存储功能。
这个容器我们最常使用,所以也是最为熟悉的一种容器。通过它来看STL的源码设计,以及STL六大组件之间的搭配使用,可以让我们对C++这种语言,对优秀的编码设计,对有趣的数据结构和算法都能有深刻的理解和认识。
STL六大组件关系如下:
其中容器主要是 提供数据存储的功能。分配器用来给容器的数据存储分配空间,算法通过迭代器可以访问容器中的数据。仿函数提供类似于函数的功能,这里算法就是用仿函数来实现的。各个适配器给各个组件做一些功能上的补充。
今天主要对容器的vector
的实现进行深入的分析。
本节涉及的源代码是基于:gcc 2.95 版本的libstdc++ 标准库进行分析的。不同版本的代码实现方式可能有一些差异,但是核心思想应该是一样的。
gcc-2.95 源码下载
我们在使用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的基本存储情况以及扩容的过程(其中的字段是源码部中的字段)
左侧的存储代表扩容之前的存储结构,右侧是扩容之后的存储结构。
扩容的过程是两倍的容量扩充,且不是在原来的基础上进行增长的,而是重新分配一块两倍于原来大小的内存空间,将原来的存储空间的元素一个一个拷贝到新的两倍大小的存储空间之中。
具体实现的类图关系如下:
从上面的类图关系中我们能够看到最终vector对象的大小sizeof(vector
为24B(x86_64),三个指针类型的大小(_M_start, _M_finish,_M_end_of_storage)
接下来我们来看一下具体的成员函数实现
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分配器中的allocate
和deallocate
函数,他们如何进行空间分配
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 逻辑有两种
接下来看一下具体的过程,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的扩容会有一些隐患:
验证代码如下:
#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在这一方面推出的性能方面的改进逻辑:
emplace_back
函数,详细的实现可以参考empalce_back和push_back的差异
vector
的emplace_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的开发者们 不断得完善逻辑,改进性能。并且不断得推出新的特性来供给我们这一些语言的使用者来使用,如果我们还用不好,那是真的愧对大佬们的付出了。
精进自己,追求极致,向大佬们致敬。