1.什么是序列式容器?什么是关联式容器?
书上给出的解释是,序列式容器中的元素是可序的(可理解为可以按序索引,不管这个索引是像数组一样的随机索引,还是像链表一样的顺序索引),但是元素值在索引顺序的方向不一定是有序的。下面这幅图是SGI STL中的各种容器,图中内缩表示内含关系,类似于UML类图里面的组合关系。比如说heap内含一个vector,表示heap是借由vector实现的。
2.vector
- vector概述
- vector的空间是动态分配的,对vector进行size()操作得到的结果是含有的实际元素的个数,capacity()操作得到的结果是当前分配空间能容纳的元素个数,这个在源代码里面一目了然,即如果有vector v,一定有v.capacity()>=v.size()
- vector具有自定义栈和数组的特性,使用时把它当做不用操心底层的栈或数组即可
- 对于vector来说,所需要的空间是连续的,重新配置空间,特别是找到一个连续的空间比之前空间还大的新空间比较耗费时间成本,因此一开始给vector的空间都要比实际需要的空间大,这样可以保证vector扩容的时候不需要每次都重新配置空间(一般扩容以2倍方式扩容,当然,别的版本stl可能实现不一样)。当然,具体实现比这里说的可能要更加精细一些,毕竟,扩充空间(包括配置新空间、数据移动和释放原有空间)都是非常耗时的操作
- vector的源码
#include<iostream>
using namespace std;
#include<memory.h>
// alloc是SGI STL的空间配置器
template <class T, class Alloc = alloc>
class vector
{
public:
// vector的嵌套类型定义,typedefs用于提供iterator_traits<I>支持
typedef T value_type;
typedef value_type* pointer;
typedef value_type* iterator;
typedef value_type& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
protected:
// 这个提供STL标准的allocator接口
typedef simple_alloc <value_type, Alloc> data_allocator;
iterator start; // 表示目前使用空间的头
iterator finish; // 表示目前使用空间的尾
iterator end_of_storage; // 表示实际分配内存空间的尾
void insert_aux(iterator position, const T& x);
// 释放分配的内存空间
void deallocate()
{
// 由于使用的是data_allocator进行内存空间的分配,
// 所以需要同样使用data_allocator::deallocate()进行释放
// 如果直接释放, 对于data_allocator内部使用内存池的版本
// 就会发生错误
if (start)
data_allocator::deallocate(start, end_of_storage - start);
}
void fill_initialize(size_type n, const T& value)
{
start = allocate_and_fill(n, value);
finish = start + n; // 设置当前使用内存空间的结束点
// 构造阶段, 此实作不多分配内存,
// 所以要设置内存空间结束点和, 已经使用的内存空间结束点相同
end_of_storage = finish;
}
public:
// 获取几种迭代器
iterator begin() { return start; }
iterator end() { return finish; }
// 返回当前对象个数
size_type size() const { return size_type(end() - begin()); }
size_type max_size() const { return size_type(-1) / sizeof(T); }
// 返回重新分配内存前最多能存储的对象个数
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); }
// 本实作中默认构造出的vector不分配内存空间
vector() : start(0), finish(0), end_of_storage(0) {}
vector(size_type n, const T& value) { fill_initialize(n, value); }
vector(int n, const T& value) { fill_initialize(n, value); }
vector(long n, const T& value) { fill_initialize(n, value); }
// 需要对象提供默认构造函数
explicit vector(size_type n) { fill_initialize(n, T()); }
vector(const vector<T, Alloc>& x)
{
start = allocate_and_copy(x.end() - x.begin(), x.begin(), x.end());
finish = start + (x.end() - x.begin());
end_of_storage = finish;
}
~vector()
{
// 析构对象
destroy(start, finish);
// 释放内存
deallocate();
}
vector<T, Alloc>& operator=(const vector<T, Alloc>& x);
// 提供访问函数
reference front() { return *begin(); }
reference back() { return *(end() - 1); }
// 向容器尾追加一个元素, 可能导致内存重新分配
// push_back(const T& x)
// |
// |---------------- 容量已满?
// |
// ----------------------------
// No | | Yes
// | |
// ↓ ↓
// construct(finish, x); insert_aux(end(), x);
// ++finish; |
// |------ 内存不足, 重新分配
// | 大小为原来的2倍
// new_finish = data_allocator::allocate(len); <stl_alloc.h>
// uninitialized_copy(start, position, new_start); <stl_uninitialized.h>
// construct(new_finish, x); <stl_construct.h>
// ++new_finish;
// uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
void push_back(const T& x)
{
// 内存满足条件则直接追加元素, 否则需要重新分配内存空间
if (finish != end_of_storage)
{
construct(finish, x);
++finish;
}
else
insert_aux(end(), x);
}
// 在指定位置插入元素
// insert(iterator position, const T& x)
// |
// |------------ 容量是否足够 && 是否是end()?
// |
// -------------------------------------------
// No | | Yes
// | |
// ↓ ↓
// insert_aux(position, x); construct(finish, x);
// | ++finish;
// |-------- 容量是否够用?
// |
// --------------------------------------------------
// Yes | | No
// | |
// ↓ |
// construct(finish, *(finish - 1)); |
// ++finish; |
// T x_copy = x; |
// copy_backward(position, finish - 2, finish - 1); |
// *position = x_copy; |
// ↓
// data_allocator::allocate(len); <stl_alloc.h>
// uninitialized_copy(start, position, new_start); <stl_uninitialized.h>
// construct(new_finish, x); <stl_construct.h>
// ++new_finish;
// uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
// destroy(begin(), end()); <stl_construct.h>
// deallocate();
iterator insert(iterator position, const T& x)
{
size_type n = position - begin();
if (finish != end_of_storage && position == end())
{
construct(finish, x);
++finish;
}
else
insert_aux(position, x);
return begin() + n;
}
iterator insert(iterator position) { return insert(position, T()); }
void pop_back()
{
--finish;
destroy(finish);
}
iterator erase(iterator position)
{
if (position + 1 != end())
copy(position + 1, finish, position);
--finish;
destroy(finish);
return position;
}
iterator erase(iterator first, iterator last)
{
iterator i = copy(last, finish, first);
// 析构掉需要析构的元素
destroy(i, finish);
finish = finish - (last - first);
return first;
}
// 调整size, 但是并不会重新分配内存空间
void resize(size_type new_size, const T& x)
{
if (new_size < size())
erase(begin() + new_size, end());
else
insert(end(), new_size - size(), x);
}
void resize(size_type new_size) { resize(new_size, T()); }
void clear() { erase(begin(), end()); }
protected:
// 分配空间, 并且复制对象到分配的空间处
iterator allocate_and_fill(size_type n, const T& x)
{
iterator result = data_allocator::allocate(n);
uninitialized_fill_n(result, n, x);
return result;
}
// 提供插入操作
// insert_aux(iterator position, const T& x)
// |
// |---------------- 容量是否足够?
// ↓
// -----------------------------------------
// Yes | | No
// | |
// ↓ |
// 从opsition开始, 整体向后移动一个位置 |
// construct(finish, *(finish - 1)); |
// ++finish; |
// T x_copy = x; |
// copy_backward(position, finish - 2, finish - 1); |
// *position = x_copy; |
// ↓
// data_allocator::allocate(len);
// uninitialized_copy(start, position, new_start);
// construct(new_finish, x);
// ++new_finish;
// uninitialized_copy(position, finish, new_finish);
// destroy(begin(), end());
// deallocate();
template <class T, class Alloc>
void insert_aux(iterator position, const T& x)
{
if (finish != end_of_storage) // 还有备用空间
{
// 在备用空间起始处构造一个元素,并以vector最后一个元素值为其初值
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
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;
// 以上配置元素:如果大小为0,则配置1(个元素大小)
// 如果大小不为0,则配置原来大小的两倍
// 前半段用来放置原数据,后半段准备用来放置新数据
iterator new_start = data_allocator::allocate(len); // 实际配置
iterator new_finish = new_start;
// 将内存重新配置
try
{
// 将原vector的安插点以前的内容拷贝到新vector
new_finish = uninitialized_copy(start, position, new_start);
// 为新元素设定初值 x
construct(new_finish, x);
// 调整水位
++new_finish;
// 将安插点以后的原内容也拷贝过来
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch(...)
{
// 回滚操作
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
// 析构并释放原vector
destroy(begin(), end());
deallocate();
// 调整迭代器,指向新vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
// 在指定位置插入n个元素
// insert(iterator position, size_type n, const T& x)
// |
// |---------------- 插入元素个数是否为0?
// ↓
// -----------------------------------------
// No | | Yes
// | |
// | ↓
// | return;
// |----------- 内存是否足够?
// |
// -------------------------------------------------
// Yes | | No
// | |
// |------ (finish - position) > n? |
// | 分别调整指针 |
// ↓ |
// ---------------------------- |
// No | | Yes |
// | | |
// ↓ ↓ |
// 插入操作, 调整指针 插入操作, 调整指针 |
// ↓
// data_allocator::allocate(len);
// new_finish = uninitialized_copy(start, position, new_start);
// new_finish = uninitialized_fill_n(new_finish, n, x);
// new_finish = uninitialized_copy(position, finish, new_finish);
// destroy(start, finish);
// deallocate();
template <class T, class Alloc>
void insert(iterator position, size_type n, const T& x)
{
// 如果n为0则不进行任何操作
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; // 将vector 尾端标记后移
copy_backward(position, old_finish - n, old_finish);
fill(position, position + n, x_copy); // 从插入点开始填入新值
}
else
{
// 插入点之后的现有元素个数 小于等于 新增元素个数
uninitialized_fill_n(finish, n - elems_after, x_copy);
finish += n - elems_after;
uninitialized_copy(position, old_finish, finish);
finish += elems_after;
fill(position, old_finish, x_copy);
}
}
else
{ // 剩下的备用空间小于“新增元素个数”(那就必须配置额外的内存)
// 首先决定新长度:就长度的两倍 , 或旧长度+新增元素个数
const size_type old_size = size();
const size_type len = old_size + max(old_size, n);
// 以下配置新的vector空间
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
__STL_TRY
{
// 以下首先将旧的vector的插入点之前的元素复制到新空间
new_finish = uninitialized_copy(start, position, new_start);
// 以下再将新增元素(初值皆为n)填入新空间
new_finish = uninitialized_fill_n(new_finish, n, x);
// 以下再将旧vector的插入点之后的元素复制到新空间
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(start, finish);
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
};
- vector的数据结构
线性连续空间,一旦空间分配完成,空间类似于数组
- vector的迭代器
- 普通指针即可满足迭代器所有要求,因此vector提供的迭代器就是普通指针。
- vector构造与内存管理
- vector缺省适合用std::alloc为空间配置器,另外定义了一个data_allocator,方便以元素大小配置内存
- vector能动态增加大小,并不是在原空间的后面按续接新的空间,而是以原来大小的2倍配置另一块连续空间,将原来的内容拷贝过来,释放原空间,因此如果vector的操作一旦引起了空间重新配置,之前的迭代器就失效了。
- 在使用的过程中,vector的capacity一般只增不减,若是以类实例的方式声明的vector,等该实例的生命周期结束自然会被析构掉,但是如果是指针形式可能不会被析构掉,这是需要使用swap技法来清楚内存。
- vector相关面试题解析
- vector使用中需要注意的问题?
- 一是要注意扩容的时候迭代器,引用,指针都会失效
- 二是为了效率考虑最好在能遇见到空间需求的情况下提前分配好空间
- 三是对于元素为指针的情况注意防止内存泄露,这个可以手工,当然也可以用智能指针
- vector的迭代器不一定就是原生指针,只是说有的版本可以用指针来实现而已。所以不要将它的迭代器当做元素地址传递给严格需要元素地址的地方。
- clear操作符是不会释放vector所指内存空间的,只是让size变为了0,对capacity没影响
- vector怎么释放内存空间?
- swap技法
swap技法的原理很简单,但是不一定能完全释放,原理是将需要释放的vector(比如说v)在一个临时作用域里面与一个空的vector(比如说tmp)交换,这之后tmp出了临时作用域自动析构,就完成了v的内存释放。代码如下:
//加一对大括号是可以让tmp退出{}的时候自动析构
{
std::vector<int> tmp;
v.swap(tmp);
}
当然上述也可以写成函数形式,且由上面的方法也可以看出,缩减内存空间也可以用类似的方法.
- swap技法
- vector使用中需要注意的问题?
3.list(很简单,不需要多说)
list的数据结构就是链表,所以空间配置起来不像vector那么麻烦,迭代器也不会出现失效的问题,但是迭代器的设计就相对比较麻烦,需要实现迭代器的接口。唯一需要注意的就是它的迭代器是bidirectional类型的,不是random的。当然,这也说明了链表是双向链表。
4.deque
- deque概述
- deque是双向开口的连续线性空间,准确地说是分段连续空间,vector在头部效率极差,所以看做是一个尾部开口的单向开口空间。
- deque的迭代器是random_accesss_iterator,要维持这种假象,要求付出更多的代码
- deque迭代器
- 既然是分段连续,要实现随机存储,制造这个假象,肯定要重载[]操作符,+,-,++,--等操作符,以让跨连续空间的迭代器移动看起来平滑,这就需要一个中控的数据结构,在deque的SGI实现中这称作map(不是STL的那种map容器),这个map是一个小段的连续空间,存有各个实际连续区块的指针,由于多了这一层间接性,每当迭代器到达实际连续区块边缘的时需要特别小心。一旦map满载,则需要扩容(同vector的重新配置,拷贝,释放原有空间三部曲)。
- 其他的都是实现问题而已,要满足迭代器的接口。
- 由原理也可以知道,deque也会存在迭代器失效问题。
- deque的操作
- 常见操作都有,当然只是实现细节问题。
5.stack
- stack概述
- stack的特点是只能存取栈顶,因此省了好多操作
- stack默认以deque实现,这种实现方法是adapter模式的典型应用
- stack实现
- SGI STL默认用deque实现stack,相当于封掉一段并禁止遍历的deque。当然亦可以在stack声明时指出第二个模板参数说明底层实现的类别。比如,list也是可以作为stack的底层实现
- stack迭代器
- 不允许遍历,迭代器无效,因此stack不提供迭代器。
6.queue
- queue概述
- queue是一个双向开口的数据结构,只有在两端提供数据访问,元素进出满足先进先出原则
- queue迭代器
- 无迭代器
- queue实现
- 底层默认用deque实现
7.heap
heap是用来实现priority_queue的底层数据结构,封装了heap的构造与内存管理操作,封装了heap算法,因此priority算法实现起来就比较容易了,heap的底部实现一般是vector,因此是有迭代器的。
8.priority_queue
简单地配接heap就好了,是一个有权值的queue,能够以较高效率(o(lgN))实现取权值最高的元素,只在两端操作,因此无迭代器。
9.slist
list是一个双向链表,有时候有点杀鸡用牛刀的感觉,当只需要用到单向链表的功能时,slist就够了,slist的迭代器是一个forward_iterator,当然,这导致它的使用有诸多限制,比如,不提供push_back(),只提供push_front()。
总结:整篇读书笔记中只有vector讲得比较仔细,因为一旦理解了vector之后,其它的理解起来就相对容易了。