文章目录
引用侯捷所著《STL源码剖析》(2002,华中科技大学出版社)前言内容:
人们常说,不要从轮子重新造起,要站在巨人的肩膀上,面对扮演轮子角色的这些STL组件,我们是否有必要深究其设计原理或实现细节呢?答案固人而异。从应用的角度思考,你不需要探索实现细节一一然而相当程度地认识底层实现,对实际运用绝对有帮助,从技术研究与本质提升的角度看,深究细节可以让你得族掌握一切一不论是为了重温数据结构和算法,或是为了扮演轮子角色,或是想要进一步扩张别人的轮子,都帮可因此获得深厚扎实的基础:天下大事,必作于细!参观飞机工厂不能让你学得流体力学、也不能让你学会开飞机。但是如果你既会开飞机又操流体力学,参观飞机工厂可以带给你最大的乐趣和价值。
本文并不适合STL初学者。对于那些熟练掌握 C++ 模板和 STL 的日常使用,理解内存分配与对象生命周期,并且有扎实的数据结构基础,希望深刻了解STL实现细节,从而得以提升对STL的扩充能力,或是希望藉由观察STL源代码,学习世界一流程序员身手,并藉此彻底了解各种被广泛运用之数据结构和算法的人,本文可能更适合你。
前言
为什么C++中要设计vector这个容器,相信使用过vector容器的都会发现,使用起来好像和数组一摸一样,只不过在操作的时候直接调用接口而不是自己对数组进行操作。vector底层实际上就是在维护一块动态开辟的空间,它把如何维护,维护的细节都封装起来的,用户通过一些对外开放的接口对这块空间进行操作;这就使得使用者只需要调用想做的操作,而不再需要控制维护一些管理时的繁琐细节,比如空间够不够?中间插入时要先挪动数据...
本篇博客将从5个部分来剖析解释vector的源码:
- vector 的数据结构;
- vector 的迭代器;
- vector 的构造和内存管理;
- vector 的操作接口;
本文的源码主要来自 SGI STL(Silicon Graphics, Inc. 实现的 STL 版本);
vector的数据结构
为了降低频繁扩容的成本,vector实际配置的空间大小可能比客户端要求的更大一些,以备将来可能的扩容。这也就是说vector要维护一个比正在使用的空间更大的空间。换句话说,vector不仅有大小,还用容量,并且容量永远大于等于vector的大小。
vector管理的是线性的地址空间,采用的数据结构非常简单,使用三个指针/迭代器来对该线性空间进行管理,分别是:
- start指向使用空间的起始位置;
- finish指向使用空间的末尾;
- end_of_storage指向可用空间的末尾。
如图是vector管理的示意图:
vector的迭代器
基础迭代器
vector维护的是连续线性的空间 ,不论内部存储的是什么类型,都是对线性空间进行操作和管理,这也就意味着vector中元素存储的地址是线性递增的,所以使用普通指针就可以满足对迭代器的需求,如operator* ,operator-> ,operator-- ...,没有必要再对迭代器进行封装。
所以上面的start,finish,end_of_storage都是一个个的迭代器,本质就是一个个指向临界位置的指针,这三个迭代器也是vector类的默认成员。
cpp
template <class T, class Alloc = alloc>
class vector {
public:
typedef T value_type;
typedef value_type* iterator; // 本质就是T*
typedef const value_type* const_iterator; // 本质就是const T*
typedef value_type& reference;
typedef const value_type& const_reference;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef size_t size_type;
typedef ptrdiff_t difference_type; // 一个__int64常用来对地址加减是的类型转换
protected:
iterator start;
iterator finish;
iterator end_of_storage;
借迭代器实现的函数
有了上面三个迭代器来划分空间的各个边界,判断容器是否满,容器已经使用空间的数量,以及容器的begin迭代器和end迭代器,解引用操作等,都可以直接实现。
cpp
template <class T, class Alloc = alloc>
class vector {
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return size_type(end() - begin()); }
size_type capacity() const { return size_type(end_of_storage - begin()); }
bool empty() const { return begin() == end(); }
reference operator[](size_type n) { return *(begin() + n); }
reference front() { return *begin(); }
reference back() { return *(end() - 1); }
vector的构造和内存管理
vector构造
看一下在项目中最常使用的vector构造,指定空间大小和初始值。
cpp
template <class T, class Alloc = alloc>
class vector {
public:
vector(size_type n, const T& value) { fill_initialize(n, value); }
protected:
void fill_initialize(size_type n, const T& value) {
start = allocate_and_fill(n, value); // 开辟n个T类型的空间,并初始化为value
finish = start + n; // 设置已使用空间的结束位置
end_of_storage = finish; // 设置容积边界
}
iterator allocate_and_fill(size_type n, const T& x) {
iterator result = data_allocator::allocate(n); // 开辟n个T类型的空间,底层就是malloc
__STL_TRY { // 宏就是try
uninitialized_fill_n(result, n, x); // 将刚开辟好的空间进行初始化
return result;
}
__STL_UNWIND(data_allocator::deallocate(result, n)); // 宏可以理解为catch
}
typedef simple_alloc<value_type, Alloc> data_allocator;
uninitialized_fill_n
会根据第一个参数的类型,看内置类型还是自定义类型,如果内置类型就调用fill_n()
来进行空间的初始化,如果时自定义类型,就调用construct()
来进行放入构造对象实现初始化。
vector的元素操作
vector的push_back
在插入元素之前,vector会先判断容量够不够,是否需要扩容。此处扩容并不是直接在容器后面开辟新的空间,因为无法保证后面的空间是否被使用了,此处采用的方式是开辟一整块新的空间,将原数据拷贝过来,释放原来数据,最后将要插入的数据进行插入。
为了防止扩容时对原数据的拷贝和删除,vector在扩容的时候不是用一个扩一个,而是一次扩二倍。
cpp
template <class T, class Alloc = alloc>
class vector {
public:
void push_back(const T& x) {
if (finish != end_of_storage) { // 空间够
construct(finish, x); // 直接在finish位置调用T类型的构造
++finish; // 让finish指向下一个位置
}
else // 空间不够
insert_aux(end(), x); // 该函数实际上是用来实现指定位置插入的,此处是指定在end()结尾位置插入
}
};
template <class T, class Alloc> // 指定位置position插入x
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
if (finish != end_of_storage) { // 空间够
construct(finish, *(finish - 1)); // 将finish-1位置的数据拷一份到finish
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1); // 将[positon , finish - 2]向后移动一位 ; copy_backward拷贝函数,从后往前拷贝, 将[position , finish - 2] 的数据拷贝到 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 = data_allocator::allocate(len); // 开空间
iterator new_finish = new_start;
__STL_TRY {
new_finish = uninitialized_copy(start, position, new_start); // 拷前半部分
construct(new_finish, x); // 调构造,插入元素
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish); // 拷后半部分
}
#ifdef __STL_USE_EXCEPTIONS
catch(...) {
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
#endif /* __STL_USE_EXCEPTIONS */
destroy(begin(), end()); // 释放原空间
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
提醒:在对vector进行任何操作的时候,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了,所以在对vector进行增删后,原迭代器的使用要谨慎。
vector的pop_back
在pop_back中直接让finish向前移动一位,将删除位置视为可被覆盖,再调用删除位置对应元素的析构即可。
cpp
template <class T, class Alloc = alloc>
class vector {
public:
void pop_back() {
--finish; // finish向前移动
destroy(finish); // 调用析构
}
vector的erase
先看一下erase删除区间[first , last)
的实现:
cpp
iterator erase(iterator first, iterator last) {
iterator i = copy(last, finish, first); // 用[last,finish]的数据覆盖[first, last]
destroy(i, finish); // 调用[i , finish) 析构
finish = finish - (last - first); // 调整指针的位置
return first;
}

删除指定位置的元素:
cpp
iterator erase(iterator position) {
if (position + 1 != end())
copy(position + 1, finish, position); // 将[position + 1 , finish)数据整体向前移动一位
--finish; // finish向前移一位
destroy(finish); // 将finish位置元素析构
return position;
}
vector的insert
此处以 void insert (iterator pos, size_type n, const T& x);
在指定位置插入n个x数据:
分为两种情况:空间够,空间不够。
先看第一种:空间足够:
在空间足够的情况下,就只需要考虑对原数据的移动问题了。
在库中,将数据的移动分为了两种:
- 插入的个数
n < finish - position
即position
后面的有效数据比插入数据多,此时是:- 将
[position , finish)
分成了两组:[position , finish - n)
和[finish - n , finish)
; - 先将
[finish - n , finish)
数据拷贝到finish后面; - 再将
[position , finish - n)
的数据拷贝到finish前面,最后插入数据;!
- 将
- 插入的个数
n >= finish - position
即position
后面的有效数据比插入数据少或等于,此时是:- 因为新增的数据一定会放到finish后面,所以先在finish后面插入
n - (finsh - position)
个元素; - 再将
[position,finish)
的数据放到finish + n - (finsh - position)
的后面; - 最后再将
[position , finish)
赋值为x。
- 因为新增的数据一定会放到finish后面,所以先在finish后面插入
cpp
template <class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x) {
if (n != 0) {
if (size_type(end_of_storage - finish) >= n) { // 空间足够
T x_copy = x;
const size_type elems_after = finish - position;
iterator old_finish = finish;
if (elems_after > n) {
uninitialized_copy(finish - n, finish, finish); // 拷贝[finish - n ,finish)到finish后面
finish += n;
copy_backward(position, old_finish - n, old_finish); // 拷贝[position , old_finish - n]到old_finish前面
fill(position, position + n, x_copy); // 插入目标元素
}
else {
uninitialized_fill_n(finish, n - elems_after, x_copy); // 在finish后面插入缺少的目标元素
finish += n - elems_after;
uninitialized_copy(position, old_finish, finish); // 将[position , old_finish)移动到finish的后面
finish += elems_after;
fill(position, old_finish, x_copy); // 插入剩余的x元素
}
}
else
{
// ......
}
第二种情况,空间不够,整体分五步:
- 先扩容;
- 将原来
[start , position)
的数据拷贝到新地址; - 再新地址
new_start + position
的后面插入新增的元素; - 将
[position , finish)
整体移动到new_satrt + position + n
的后面; - 最后进行收尾,将原空间释放,更新start和finish.
cpp
template <class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x) {
if (n != 0) {
if (size_type(end_of_storage - finish) >= n) { // 空间足够
}
else {
const size_type old_size = size();
const size_type len = old_size + max(old_size, n);
iterator new_start = data_allocator::allocate(len); // 开辟新空间
iterator new_finish = new_start;
__STL_TRY {
new_finish = uninitialized_copy(start, position, new_start); // 拷贝[start , positoin) 的数据到新空间
new_finish = uninitialized_fill_n(new_finish, n, x); // 插入目标元素
new_finish = uninitialized_copy(position, finish, new_finish); // 将[start + position , finish)拷贝到new_satrt + position + n后面
}
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
destroy(start, finish); // 释放原空间
deallocate(); // 析构
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}