总览
还有3个适配器 stack queue priority_queue
vector
维护的是一个连续线性空间,对于gnu2.9来说,有3个数据成员 start,finish,end_of_storage,都是普通指针,分别指向开头、尾后、和实际可容纳最后一个元素+1 的位置, push_back函数就是 首先检查是否还有备用空间,如果有就直接在备用空间上构造函数,并调整finish,没有备用空间,就新申请一个double size的空间,把原来的数据 拷贝过去,(可以用 traits进行优化,bitwise copy还是逐个调用拷贝构造函数这样),然后释放原来的空间。 (唯一可能需要注意的点就是,对vector的任何操作,引起空间重新配置,那么指向原vector的迭代器就会失效),感觉没什么东西。
哦,还有vector的最大大小:
vector的下标是size_t, (这里有个坑!!!unsinged int 无符号数 看关于size_t 这篇文章)它的类型跟具体平台有关系,32位应该是 unsigned int 32位比特 也就是 2的32次方, 64位应该是unsigned long, 64位比特,2的64次方。 这个也是限制vector最多能放多大元素的其中一个因素,另一个因素是机器的内存和外存也是有限的,对于容器是保存在栈上还是堆上又有点区别。
加点别人的总结,随便看看吧。
在STL中,最常用的就是容器,最常用的容器就是vector了。vector类似内置数组。但是数组是静态的,一旦配置就不能再变大小,而容器的大小事容器本身自己调整的。在实现容器的代码中可以看到,容器可以动态增大,但是不能动态减小。
容器有已用空间和可用空间,已用空间就是容器已经使用了的空间,可用空间就是指vector的大小capacity。
容器是占用一段连续线性空间,所以容器的迭代器就等价于原生态的指针(这是造成我一直以为迭代器就是指针的原因),vector迭代器类型是RandomAccessIterator类型。vector的实现依赖于前面内存的配置和内存的初始化,以及迭代器,看学习vector代码可以帮助我们更加深入理解stl_alloc.h、stl_uninitialized.h、stl_iterator.h。
gnu 2.9 vector中的iterator 就是一个内置指针
gnu 4.9 vector中的iterator 变成了一个类,这个类里面有一个内置指针型的数据成员 T *
关于vector迭代器失效:
对于vector而言,它的迭代器其实就是一个原生指针或者一个封装了原生指针的类,所以假如因为insert or push_back
这些操作引起了扩容,所有元素都搬到一个新的地址空间去,那全部迭代器都会失效,而平常的插入or删除操作,没有引起扩容,那只会让被操作的那个元素之后的迭代器失效(因为vector是放在一段连续的地址空间,你删除了一个元素,后面的所有元素都会往前面挪一下),而之前的迭代器没什么影响。
更详细的:
看下reserve resize 和 swap吧
这里是一系列比较常用的工具函数,包括begin、end、size、capacity等,都是很简单的函数。
从size和capacity函数,就可以理解finish和end_of_storage指针的区别。然后,构造函数,都是通过uninitialized_fill_n
来实现的,也是说,如果是拷贝PODtype的vector,实际上和memset没有区别,因为不会逐个调用构造函数。而且,析构函数也没有释放内存,只是destory所有元素。如果该类型有nontrivial的destructor,才会依次调用析构函数。
vector<_Tp, _Alloc>& operator=(const vector<_Tp, _Alloc>& __x);
void reserve(size_type __n) {
if (capacity() < __n) {
const size_type __old_size = size();
iterator __tmp = _M_allocate_and_copy(__n, _M_start, _M_finish);
destroy(_M_start, _M_finish);
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __tmp;
_M_finish = __tmp + __old_size;
_M_end_of_storage = _M_start + __n;
}
}
void resize(size_type __new_size, const _Tp& __x) {
if (__new_size < size())
erase(begin() + __new_size, end());
else
insert(end(), __new_size - size(), __x);
}
reserve(n)的逻辑: 当 n小于等于现有的 capacity 无事发生, 大于的话, 新申请一个n的空间,把原来的拷贝过来,原来的析构、释放。
这个reserve
函数用来保证vector有可以存放n个value元素的空间。如果空间不够了,就会执行allocate new area -> copy -> destroy and deallocate
。而resize
函数与之类似,只不过reserve是对capacity而言,而resize是对size操作。(并且假如新size比老size小,那把之前老size多余部分给erase掉了,而如果新size比老size大,会把在后面插入那多出来的那部分,并且执行默认构造函数),所以说reserve比resize效率高?
void swap(vector<_Tp, _Alloc>& __x) {
__STD::swap(_M_start, __x._M_start);
__STD::swap(_M_finish, __x._M_finish);
__STD::swap(_M_end_of_storage, __x._M_end_of_storage);
}
另外一个函数swap也是用的比较多,这个函数是用来交换两个vector,但是实际上只是交换了那三个重要指针,也就是交换了两个vector的底层内存。
所以,这样就可以理解为什么会有利用swap来释放某个vector的空间。一般是先创建一个栈上的vector,并且是空的,然后swap另一个vector,这样另一个vector的capacity就是0了,然后本来是空的vector拥有了capacity,会在生命周期结束时调用析构函数,释放空间了。
void clear() { erase(begin(), end()); }
还有这个clear
函数,并不会清空内存,只是改变了size,而不会改变capacity,毕竟内存都是在vector base的析构函数中释放的。
insert
vector中最重要的,大概就是push_back、insert和erase函数了,而这些函数都实际上会调用insert_aux
来做底层实现。
void push_back(const _Tp& __x) {
if (_M_finish != _M_end_of_storage) {
construct(_M_finish, __x);
++_M_finish;
}
else
_M_insert_aux(end(), __x);
}
iterator insert(iterator __position, const _Tp& __x) {
size_type __n = __position - begin();
if (_M_finish != _M_end_of_storage && __position == end()) {
construct(_M_finish, __x);
++_M_finish;
}
else
_M_insert_aux(__position, __x);
return begin() + __n;
}
对于单个元素的插入,都是先判断容量是否充足,如果不是,就会调用insert_aux
这个函数,去具体实现。然后,对于范围insert就会有点其他操作,范围insert分为通过iterator指定的范围数据和插入n个相同的数据,后者实现相对简单,会让fill_insert
函数来执行具体操作。
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;
//拷贝[__position, _M_finish-2)的数据到以_M_finish-1结尾(右开)的内卒中
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;
__STL_TRY {
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
construct(__new_finish, __x);
++__new_finish;
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
}
__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;
}
}
这里的insert_aux
函数看起来很复杂,但是大部分都是调整那三个指针,以及搬运数据。先是判断capacity是否足够,如果充足,那么就将position后面的数据整体后移,然后插入新数据。如果capacity不够了,那就按照两倍增长的速度来申请新的空间,然后把东西搬过去,再释放旧空间,这就是vector比较核心的动态增长策略的实现了。
还有一个函数fill_insert
,与insert_aux
的区别只是插入的是n个元素,所以具体实现上很类似,就是capacity增长有点不同:
const size_type __len = __old_size + max(__old_size, __n);
iterator __new_start = _M_allocate(__len);
这里不是两倍增长,而是判断插入的元素有多少个,如果两倍增长都不够,那就增长n个。比如说,有一个capacity为8的vector,如果插入100个元素,那么增长到16是肯定不行的,所以需要增长到108。
erase
void pop_back() {
--_M_finish;
destroy(_M_finish);
}
iterator erase(iterator __position) {
if (__position + 1 != end())
copy(__position + 1, _M_finish, __position);
--_M_finish;
destroy(_M_finish);
return __position;
}
iterator erase(iterator __first, iterator __last) {
iterator __i = copy(__last, _M_finish, __first);
destroy(__i, _M_finish);
_M_finish = _M_finish - (__last - __first);
return __first;
}
删除很简单,毕竟没有涉及到capacity的问题,只是简单的destroy和调整那三个指针。
list
双向链表,不向vector一样是连续线性空间,相较于vector,空间额外开销比较大,因为一个node 存了3个元素,有两个额外的指针,所以对于小的data元素来说,尽量别选list
只支持双向顺序访问 对于任何位置插入删除,都是常数时间
list的迭代器是 一个 has a 指向list节点的普通指针 的类,所以不是random iterator 而是双向迭代器,另外有一个重要的性质,插入,删除啊,这些操作基本都不会让原有迭代器失效
SGI 的list 是一个环状双向链表,也就是循环双向链表,在实现上在环状链表的尾部加上一个空白节点,而list类它有一个data 成员 就是指向这个节点的指针
stl 有几个点需要注意,对于迭代器 永远是前闭后开 [ ) 的形式 (所以对于 迭代器范围构造函数啊这些,不要迷惑)
还有一个是insert,永远的 插入节点之前,这是stl的策略
还有就是注意几个成员函数:
splice 将1个list的一段连续数据接在另一个list的某个位置之前
merge、sort(使用的是归并)
deque
1 . deque是由一段一段的定量连续空间构成的,它自身维护一个指针数组(map),数组里面的元素分别指向一段相同长度的连续空间,除了头和尾元素所在的那缓冲区(就是某段连续空间)之外,中间的缓冲区都是已经被占满了的,并且假如要在 deque 的前端开一个新的缓冲区,那么新元素是先占据那个缓冲区的尾部,而要在尾端增加新空间,新元素是先占据新开的缓冲区头部。
2. 并且deque初始化的时候,是先占据那个指针数组(map)中间的区段,目的是为了头尾两端的扩充一样多(p154)
3. deque的 insert和erase 具体实现是看移动前半部分的元素比较方便(也就是元素数目少),还是移动后半部分的元素少,来决定移动那个半边的(肯定要移动,为了中间的缓冲区一定要被占满,这样迭代器才好移动)
4. 假如map数组满了的话,deque的实现跟vector是一样的,新申请一个新的map数组(新的size至少是原来的两倍),把原来的那个map数组里面的值给它拷贝过来(同样先占据中间的位置),注意,原来map数组的元素的值是没变的,也就是已经开辟了的缓冲区是不释放的,新的map数组里面的元素还是指向那些缓冲区
5. deque的迭代器类型是 random access,所以可以+n -n
- deque 是一种双向开口的连续线性空间。可以在头尾两端分别做元素的插入和删除操作。
- deque 和 vector 的差异:第一,deque 允许于常数时间内对起头端进行元素的插入或移除操作;第二,deque 没有容量,它是动态地以分段连续空间组合而成。
- deque 由一段段的定量连续空间构成。一旦有必要在 deque 的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个 deque 的头端或尾端。
- deque 实现复杂,其复杂的迭代器架构。
- 在 deque 的以前版本中,还有一个额外的模板参数,以便用户可以控制节点的大小。 这种扩展证明是违反 C++ 标准(可以使用模板模板参数检测到),并且已被删除。
deque 的中控器
map
deque 采用一块所谓的 map 作为主控。这个 map 是一小块连续空间,其中每个节点都是指针,指向另一段连续线性空间,称为缓冲区。
deque 的迭代器
_Deque_iterator
deque 是分段连续空间,维持其“整体连续”假象的任务。
迭代器包含 cur 指向缓冲区的当前元素,first 指向缓冲区的头,last 指向缓冲区的尾,node 指向中控器某一个 node 节点。
push_back()
:尾端插入
push_front()
:前端插入
pop_back()
:尾端取出
pop_front()
:前端取出