C++程序员中,如果用到过STL,那么一定肯定用过vector,这个是最常见,最初步的一个数据类型。上一篇提到的array远远比不上它。毕竟那玩意儿相对vector是很久远后才提出来的。在这之前,std::vector承担了多少小菜鸟处理
数组各种问题的最优选方法。不用处理内存,可以删除,任意增加不考虑越界。那简直是一种最单纯质朴的快乐。
如果单纯的只考虑开发目标,而不考虑对内存占用和再分配导致的性能下降,不用考虑删除时迭代器的问题,那么几乎这玩意儿就可以能抵挡住大多数情况下的对数组的使用了,可惜,没有单纯的想法,还是得面对现实。std::vector重载了[],所以支持类似数组一样的随机访问,你可以简单理解成它就是一个大数组,只不过他更强大,支持迭代器访问和动态处理(删除、增加等),且不需要你担心对内存的处理。
std:vector的源码很容易找到,其实你看它代码也不复杂,之所以看上去眼花缭乱的原因不是因为他复杂,而是为了兼容和安全性搞了好多新功能和方法,再加上一些模板本身的技巧。这些都可以暂时忽略过去,重点看重点的相关的函数方法的实现。
向量的构造函数其实就是两部分模板类型名称和分配器,而分配器一般使用默认的分析器类型,这个回头再分配器中再详细分析。
template >
class vector { // varying size array of values
private:
template
friend class _Vb_val;
friend _Tidy_guard;
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"));
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, _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;
using const_reverse_iterator = _STD reverse_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
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
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(_Whereptr - _Myfirst);
const auto _Oldsize = static_cast(_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
#include
#include
void TestAdd()
{
std::vector 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 vec = {0,1,2,3,4,5,6,7,8,9};
std::vector::iterator it = vec.begin();
std::cout << "first is:" << vec.front() << "end is:" << vec.back() << std::endl;
std::cout << "second is:" << vec[1] <<"at three:"<().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++标准的东西揉合进来,不过限于个人的能力,可能会有不少问题,希望看到后,能及时提出来,共同进步。