vector 是动态空间的 array, 随着其元素的加入,内部机制会自行扩充以容纳新元素,因此,vector 的运用对内存的合理运用和运用的灵活性有很大的帮助。
vector 的实现技术, 关键在于其对大小的控制 以及 重新配置时的数据移动效率。
接下来我们来了解以下 vector 内部我们可访问的成员和成员函数做一下讲述:
using value_type = T;
using pointer = value_type *;
using iterator = value_type *;
using reference = value_type &;
using size_type = size_t;
using difference_type = ptrdiff_t;
vector 维护的是连续线性空间,且 vector 迭代器应需要的操作行为 : operator *, operator ->, operator ++,operator --,operator +, operator -, operator +=, operatpr -=,vector支持随机存取 ,这些普通指针天生都具备,因此 vector 的迭代器是 Random Access iterator:
template<class T, class Alloc = std::allocator<T>>
class MyVector {
public:
using value_type = T;
using pointer = value_type *;
using iterator = value_type *; //vector 的迭代器是普通指针
。。。
}
vector 所采用的数据结构是非常简单的: 线性连续空间。
使用三个迭代器管理器线性的连续空间: start 和 finish 分别指向配置得来的连续空间中目前已经使用的范围,以迭代器 end_of_storage 指向整块连续空间(含备用空间)的尾端。
iterator start; //表示目前使用空间的头部
iterator finish; //表示目前使用空间的尾部
iterator end_of_storage; //表示目前可用空间的尾部
容量(capacity)的观念:为了降低空间配置的成本, vector 的实际配置的大小比客户端需求量更大一点, 以备将来可能的扩充。
vector 的性质:容量永远大于等于其 size, 一旦容量等于 size,便是满载,下次再有新增元素,整个vector 就必须另寻其他空间。
运用 start , finish, end_of_storage 三个迭代器,便可轻易地提供首尾标示,大小,容量,空容器判断,索引( [ size_type n] 和 at( size_type n) ),最前端元素,最后端元素值等等操作。
iterator begin() const { return start; }
iterator end() const { 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 at(size_type n) {
if (begin() + n >= end())
throw std::out_of_range("n is out_of_range!\n");
return *(begin() + n);
}
reference front() { return *begin(); } //返回首元素
reference back() { return *(end() - 1); } //返回尾巴元素
首先我们知道,为了完成 容量 这一机制,其内存分配 和 构造元素 的操作一定是分开的,且内存分配大小一定比构造的元素数量要大,仅当内存分配的空间不足以构造元素时,会开辟新空间,转移原元素,释放原空间。
接下来就让我们深入理解这一过程在源码中的体现。
vector 缺省使用 alloc (SGI STL特殊的空间配置器) 作为其内存分配的空间配置器.
这里为了我们的演示,博主使用 标准库 的 std::allocator 作为我们的空间配置器(以元素大小为配置单位)
设置其为静态成员变量 data_allocator
template<class T, class Alloc = std::allocator<T>>
templat<class T, class Alloc> Alloc vector<T,Alloc>::data_allocator;
void fill_initialize(size_type n, const T &value) {
start = allocate_and_fill(n, value);
finish = start + n;
end_of_storage = finish;
}
// 配置空间并填满内容
iterator allocate_and_fill(size_type n, const T &x) {
iterator result = data_allocator.allocate(n);
std::uninitialized_fill_n(result, n, x);
return result;
}
void push_back(const T &x) {
if (finish != end_of_storage) {
data_allocator.construct(finish, x);
++finish;
} else {
insert_aux(end(), x);
}
}
解析: insert_aux(iterator position, const T &x)
如果当前 size < 容量,说明不需要开辟新空间,可用直接插入,具体做法为:
如果当前 size == 容量,说明必须额外开辟新空间, 新空间为原空间的 2 倍。
接下来的操作需要 使用 try catch,对异常发生有安全的处理。
template<class T, class Alloc>
void MyVector<T, Alloc>::insert_aux(iterator position, const T &x) {
if (finish != end_of_storage) { //还有备用空间
data_allocator.construct(finish, *(finish - 1));
++finish;
T x_copy = x;
std::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;
iterator new_start = data_allocator.allocate(len);
iterator new_finish = new_start;
try {
new_finish = std::uninitialized_copy(start, position, new_start);
data_allocator.construct(new_finish, x);
++new_finish;
new_finish = std::uninitialized_copy(position, finish, new_finish);
} catch (...) {
destory(new_start, new_finish);
data_allocator.deallocate(new_start, len);
throw;
}
this->destory(begin(), end());
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
注意: 所谓动态增加大小,不是指在原空间之后接续新空间(无法保证原空间之后有足够的空间),而是以两倍的原空间大小另外配置一片新的空间,然后将原内容拷贝,并开始在原内容之后构造新元素,并释放原空间。
空间重新配置后,指向原vector 的所有迭代器就都失效了!!!
vector 支持的元素操作细节我们还是需要讲一讲的,在 standly lippman 的口中我们知道 vector 的很多操作性能上并不友好,反而会伴随重新分配空间或者大量移动操作,接下来我们来看一下其常用操作的源码。
void pop_back() {
--finish;
data_allocator.destory(finish);
}
操作的算法思想是将删除位置后的元素拷贝到 从删除位置开始的位置,拷贝完成后,再将拷贝位置后到末尾端的元素全部析构。
template<class T, class Alloc>
typename MyVector<T, Alloc>::iterator MyVector<T, Alloc>::erase(iterator position) {
if (position + 1 != end())
std::copy(position + 1, finish, position);
--finish;
data_allocator.destroy(finish);
return position;
}
template<class T, class Alloc>
//typename 要写,因为MyVector::iterator 在T没有被推断之前,不清楚iterator到底是什么
typename MyVector<T, Alloc>::iterator MyVector<T, Alloc>::erase(iterator first, iterator last) {
iterator i = std::copy(last, finish, first);
destory(i, finish);
finish = finish - (last - first);
return first;
}
void clear() { erase(begin(), end()); }
template<class T, class Alloc>
void MyVector<T, Alloc>::insert(iterator position, size_type n, const T &x) {
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) {
std::uninitialized_copy(finish - n, finish, finish);
finish += n;
std::copy_backward(position, old_finish);
std::fill(position, position + n, x_copy);
} else {
std::uninitialized_fill_n(finish, n - elems_after, x_copy);
finish += n - elems_after;
std::uninitialized_copy(position, old_finish, finish);
finish += elems_after;
std::fill(position, old_finish, x_copy);
}
} else {
//备用空间小于”新增元素个数” (额外配置内存)
const size_type old_size = size();
const size_type len = old_size + std::max(old_size, n);
// 配置新的vector空间
iterator new_start = data_allocator.allocate(len);
iterator new_finish = new_start;
try {
//首先将旧 vector 插入点之前的元素全部赋值到新空间
new_finish = std::uninitialized_copy(start, position, new_start);
// 以下再将新增元素(初值皆为n)填入新空间
new_finish = std::uninitialized_fill_n(new_finish, n, x);
//以下再将旧 vector 的插入点之后的元素复制到新空间
new_finish = std::uninitialized_copy(position, finish, new_finish);
} catch (...) {
destory(new_start, new_finish);
data_allocator.deallocate(new_start, len);
throw;
}
this->destory(start, finish);
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
}
STL中为我们提供了额外的对容器大小管理操作:
c.shrink_to_fit() //将容量缩小至 size 大小
c.capacity() //获取当前容量
c.reserve(n) //分配至少能容纳n个元素的内存空间
如果我们预知我们使用vector 存放多少元素时,我们使用 reserve 预先分配,可以避免因为多次扩容带来的效率问题,
如果我们不用再次存放元素(插入或者添加)我们使用 shrink_to_fit 将额外容量释放掉,保证资源不会倍占用太多。
vector 的中间插入操作是非常慢的,(很大几率导致内存重新分配及 内存分配异常)强烈建议避免这些操作。