一、概述
我们平时使用数组(array)时是不是经常因为扩容问题而头疼,其实vector与array之间多的就是空间配置的部分。本章序列容器的vector对array进行了空间管理的封装以及几种迭代器的封装,从而实现了我们所认识的第一个序列容器——vector。
二、定义式
代码示例:
// alloc 是 SGI STL 的空間配置器,見第二章。 template <class T, class Alloc = alloc> class vector {
public:
// vector 的巢狀型別定義
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:
// 以下,simple_alloc 是 SGI STL 的空間配置器,見 2.2.4 節。
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() {
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 capacity() const { return size_type(end_of_storage - begin()); }
bool empty() const { return begin() == end(); }
reference operator[](size_type n) { return *(begin() + n); }
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()
destroy(start, finish); // 全域函式,見 2.2.3 節。
deallocate(); // 這是 vector 的一個 member function
}
reference front() { return *begin(); } // 第一個元素
reference back() { return *(end() - 1); } // 最後一個元素
void push_back(const T& x) { // 將元素安插至最尾端
if (finish != end_of_storage) {
construct(finish, x); // 全域函式,見 2.2.3 節。
++finish; }
else
insert_aux(end(), x); // 這是 vector 的一個 member function
}
void pop_back() { // 將最尾端元素取出
--finish;
destroy(finish); // 全域函式,見 2.2.3 節。
}
iterator erase(iterator position) { // 清除某位置上的元素
if (position + 1 != end())
copy(position + 1, finish, position); // 後續元素往前搬移
--finish;
destroy(finish); // 全域函式,見 2.2.3 節。
return position;
}
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); // 全域函式,見 2.3 節
return result;
}
从上述代码中可以看出,vector是提前申请地址空间的,因此需要记录当前使用的头尾以及实际真正的尾结点。同时还提供一个对空间的刷新功能。
三、迭代器以及地址结构
3.1 迭代器
我们知道array是一个连续性地址空间,而vector也是。因此array拥有的++、-- 操作在vector中依然有效。
vector提供的是Random Access Iterators。
template <class T, class Alloc = alloc> class vector {
public:
typedef T value_type;
typedef value_type* iterator;
...
};
因此其迭代器返回的类型其实就是传入的类型指针。
3.2 地址结构
STL源码书中对vector地址空间的描述非常简单,就是一块连续性地址空间。
为了提高空间扩容是的效率,它在初始化时会提前多申请部分空间,当空间满了之后,下一个数据插入时将会申请新的空间。
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对容量相关的迭代器返回值,例如:目前使用的头、目前使用的尾、容量等。
具体的扩容动作如下:
3.3 元素操作的地址变化
下面我们只拿较复杂的插入操作来举例:
注:uninitialized_copy:从前往后复制同时调用元素的构造、copy_backward:从后往前复制。
上图的意思是,当插入的元素小于备用空间同时插入元素个数小于插入位置后的元素个数时,则会分两次拷贝后续元素。首先是从前往后拷贝待插入元素个数的元素并且每个元素的构造都会被调用;其次是剩余的元素会草丛后往前复制刚刚的元素前面,最后再插入新元素。
当我们待插入的元素多于我们插入位置的后续元素而且不需要扩容时,我们会把多的元素个数先插入到备用空间,然后再把前面的元素拷贝都后面的未初始化的空间中并初始化。 最后执行插入操作。
最后,如果空间不足时,我们将会申请一片新的空间,然后把插入位置前的元素拷贝过去。然后插入,再拷贝插入位置后的元素。
四、总结
vector是一个连续的地址空间,同时提供了种初始化方式。我们可以直接指定其容量,也可以不指定。当vector扩容时,会申请一个是原来地址2倍的空间,同时将旧的元素拷贝到新的地址空间中,并释放原空间。而操作上,vector的插入会消耗较多的时间,因为后续的元素需要向后偏移后再将新元素放入到插入位置,因此使用的时候应该尽量避免中间插入。