用了这么久的 vector
,今天终于有时间来看下STL的实现源码了,开心?~
最近几个月在刷 leetcode
,用的较多的数据结构就是STL里面的 vector
了,相比较于直接的 array
数组,它具备了灵活地根据需求去分配管理内存,用户只管往里面扔东西,拿东西,而不用费心费力去解决C++里面的动态内存问题。
那么大致猜想一下,要实现一个这样的容器,不难想出在 vector
中至少存在这样三个私有成员:head(指向列表第一个元素位置), tail(指向列表最后一个元素位置), size(容器大小)
,
打开源码看看,得到验证:
template<typename _Tp, typename _Alloc>
struct _Vector_base {
......
struct _Vector_impl_data {
pointer _M_start; // 指向容器中的第一个元素,是一个指针,指针类型为 Tp 所示类型
pointer _M_finish; // 指向容器最后一个元素,也是一个指针
pointer _M_end_of_storage; // 指向容器最后的位置
......
}
}
template<typename _Tp, typename _Alloc = std::allocator<_tp> >
class vector : protected _Vector_base<_tp> {typedef _Vector_base<_tp> _Base; // 如上 _Vector_base 所示,是一个基础实现
......public:typedef _Tp value_type; // 数据类型typedef typename _Base::pointer pointer; // 全局数据指针
......
}
总之就是一层套一层,封装了一个又一个,来完成对数据的抽象。但是最后的接口是放在 vector
上放开的。
vector
支持动态内存分配,支持随机存取访问,因此在数据访问上具备了指针有的特性。那么它在源码上是怎么实现的呢?首先来看它的接口(一部分常用接口)都有哪些:
**注:**虽然包含 头文件就可以用
vector
,但是它的实现源码其实是在 bits/stl_vector.h
下的,而且下面的程序删掉了一些注释类、系统判断类函数。
template<typename _Tp, typename _Alloc = std::allocator<_tp> >
class vector : protected _Vector_base<_tp> {
...// 获取指向第一个元素的指针
iterator begin() { return iterator(this->_M_impl._M_start); }// 获取指向容器最后一个可存放位置的下一个位置iterator end(){ return iterator(this->_M_impl._M_finish); }// 获取容器当前存放了多少元素size_type size() const { return size_type(this->_M_impl._M_finish - this->_M_impl._M_start);
}// 对容器进行重新分配大小void resize(size_type __new_size) {if (__new_size > size())
_M_default_append(__new_size - size());else if (__new_size _M_erase_at_end(this->_M_impl._M_start + __new_size);
}// 获取容器自身大小size_type capacity() const {return size_type(this->_M_impl._M_end_of_storage
- this->_M_impl._M_start);
}// 看看容器当前是否为空bool empty() const { return begin() == end(); }// 获取一个能够对返回对象读写操作的引用类型,其实就是指针
reference operator[](size_type __n) {return *(this->_M_impl._M_start + __n);
}protected:// 检查访问是否合法void _M_range_check(size_type __n) const {if (__n >= this->size())
__throw_out_of_range_fmt(__N("vector::_M_range_check: __n ""(which is %zu) >= this->size() ""(which is %zu)"),
__n, this->size());
}public:// 跟 [] 操作符差不多,只不过多了一层范围检查reference at(size_type __n) {
_M_range_check(__n);return (*this)[__n];
}// 获取能够对头部元素读写的指针reference front() { return *begin(); }// 获取最后一个元素reference back() { return *(end() - 1); }// 向容器末尾追加一个元素进来void push_back(value_type &&__x) { emplace_back(std::move(__x)); }template<typename... _Args>#if __cplusplus > 201402L
reference#elsevoid#endif
emplace_back(_Args &&... __args);// 将最后一个元素从容器中删掉void pop_back() {
--this->_M_impl._M_finish;
_Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);
}// 在指定位置插入 n 个 __xiterator insert(const_iterator __position, size_type __n, const value_type &__x) {
difference_type __offset = __position - cbegin();
_M_fill_insert(begin() + __offset, __n, __x);return begin() + __offset;
}// 擦除掉某一个位置上的元素iterator erase(const_iterator __position) { return _M_erase(begin() + (__position - cbegin()));
}// 交换两个容器中的东西void swap(vector &__x) {this->_M_impl._M_swap_data(__x._M_impl);
_Alloc_traits::_S_on_swap(_M_get_Tp_allocator(),
__x._M_get_Tp_allocator());
}// 将容器内所有元素清空void clear() { _M_erase_at_end(this->_M_impl._M_start); }
};
程序中大量出现的 allocator
,是来自SGI STL的空间分配器,说白了就是用来分配空间内存的。在拿到这部分源码后,vector
的实现原理也就可以大致有所掌握了(毕竟每个接口也不过几行?,这可能就是优秀设计的结果??)。而在调用过程中无非就是 vector
调用 vector_base
再调用 allocator
再对 _Vector_impl_data
进行操作。
还有一个问题:在添加元素过程中,如果容器满了,那么容器的容量是按照怎样的规则递增呢?参考了《STL源码刨析》之后,得到这样的结论:
如果超过当前容器容量,那么容量会扩增至两倍,如果两倍容量仍不够,那么就扩张至足够大的容量。
在容量的扩张过程中,必须经历“重新分配内存,元素移动,释放原有空间”三个操作,这是因为原有的空间之后不一定能够满足需求,所以统一进行这三个操作来完成。
如何知道是扩增两倍的呢?最直观直接的方法就是执行一下这个过程看看,例如:
int main() {
vector<int> v;
v.push_back(1);
cout <" " <endl; // >: 1 1
v.push_back(2);
cout <" " <endl; // >: 2 2
v.push_back(3);
cout <" " <endl; // >: 3 4
v.push_back(4);
cout <" " <endl; // >: 4 4
v.push_back(5);
cout <" " <endl; // >: 5 8
}
这样的容量扩张过程,也带来另一个问题:当容量为 2
时获取到的 iterator
,那么在容量为 8
时,还可以用嘛?答案是不一定行,例如:
int main() {
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
auto iter = v.begin();
cout <endl; // >: 1
v.push_back(4);
cout <endl; // >: 1
v.push_back(5);
cout <endl; // >: 310076128[具体运行结果视内存情况而定]
iter = v.begin();
cout <endl; // >: 1
}
由此可知,如果当时的内存环境允许,会直接拼到原有容器后面去,如果不允许,那么就需要把当前容器的内容移动到其他地方去了,这时候原来的 iterator
就不能用了。务必小心!
从这一方面也可以体会到,其实 iterator
就是一个类型为传入 vector
中 T
类型的指针。
在所有的接口中,觉得最有意思的就是 insert
接口了?,它的实现过程比较好玩。
首先,假设调用函数为:insert(position, n, x)
,而且剩余空间够用,那么它需要分成两种情况:
- 插入元素个数 n < 插入点之后的元素个数 。
- 插入元素个数 n > 插入点之后的元素个数 。
上面两种情况分别对应下面图中的左右两边:
在有限的过程和空间里实现最高效的操作,不愧是STL???。