一、vector的介绍及使用:
1.1、vector的介绍:
1.vector是表示可变大小数组的序列容器。
2.就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3.本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4.vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。5.因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6.与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。
1.2、vector的使用:
vector学习时一定要学会查看文档:vector的文档介绍,vector在实际中非常的重要,在实际中我们熟悉常见的接口就可以,下面列出了哪些接口是要重点掌握的。
此外对于vector内部的成员函数,基本上和string相同,因此本文章在模拟实现之前只介绍大多使用函数的函数名以及功能,具体细节会在模拟实现中解释。
1.2.1、vector的构造:
构造函数 | 接口说明 |
vector() | 无参的默认构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x) | 拷贝构造 |
vector (InputIterator first, InputIterator last) | 迭代器区间构造(各种类型的迭代器) |
1.2.2、vector迭代器iterator:
iterator类型 | 接口说明 |
begin + end | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
1.2.3、vector的空间类接口:
函数接口 | 接口说明 |
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
reserve | 改变vector的capacity |
resize | 改变vector的size |
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。(即可以直接将扩容一次性到位)
resize在开空间的同时还会进行初始化,影响size。
1.2.4、vector的增删查改接口:
函数接口 | 接口说明 |
push_back | 尾插 |
pop_back | 尾删 |
find | 查找(注意这个是算法模块实现,不是vector的成员接口) |
insert | 在pos之前插入val |
erase | 删除pos位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] | 像数组一样访问 |
二、vector的模拟实现:
接下来我们更加彻底的了解一下vector的底层:
2.1、vector的构造:
由上面对vector的介绍可知,vector有四种常用的构造:
构造函数 | 接口说明 |
vector() | 无参的默认构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x) | 拷贝构造 |
vector (InputIterator first, InputIterator last) | 迭代器区间构造(各种类型的迭代器) |
2.1.1、无参的默认构造:
//默认构造
vector()
:_start(nullptr) //也可以在声明时用缺省值
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
2.1.2、构造并初始化n个val对象:
对于该构造方式,我们需要先使用reserve接口提前开辟好vector所用的空间,再将n个val尾插进去。
值得注意的是,在给val缺省值时,我们用的是匿名对象的方式,匿名对象的生命周期只在它所在那那行代码,但是经过const修饰后的匿名对象的生命周期与val一致。
vector(size_t n, const T& val = T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
2.1.3、迭代器区间构造:
根据一个迭代器区间进行构造,这个迭代器可以是任意的类型。
//迭代器区间构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last) //迭代器遍历不要使用<,链表的迭代器不是连续的
{
push_back(*first);
++first;
}
对于以上两种初始化方法,我们进行一下测试,构造对象vector<int> v(10,5),我们看一下会有怎样的结果:
可以看出运行失败,报错为非法简介寻址,因为当构造v(10,5)时,编译器会将10默认当作为int类型,而第一种构造的参数为size_t,调用该函数就会发生隐式类型转换,所以编译器会优先以迭代器初始化的方式来进行构造,但其本身时int类型不是地址,不能被解引用,若是强制解引用,就会出现野指针问题,就会运行报错。
而解决此问题的方法也非常简单,便是令写一份int类型的构造:
//为vector<int>类型构造,防止其被识别为迭代器区间构造,造成非法指针访问
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
2.1.4、拷贝构造:
对于拷贝构造来说,我们有传统写法和现代写法之分,传统写法就是所谓的普通操作步骤,即:开空间、赋值。
//拷贝构造
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
//memcpy(_start, v._start, sizeof(T) * v.size()); //浅拷贝
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
当然,既然有传统写法,也就有现代写法,我们一般用的也都是现代写法实现拷贝构造:(代码如下)
template <class InputIterator>
vector(InputIterator first, InputIterator last)//迭代器初始化
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
//现代写法
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);//this和tmp进行swap
}
2.2、模拟迭代器iterator:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
2.3、空间类接口模拟实现:
容量空间 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector的capacity |
size、capacity、empty:
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
bool empty() const
{
return _start == _finish;
}
reserve:
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size(); //记录扩容前对象的大小
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * size()); //浅拷贝
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
这里我们需要记录一下扩容前对象的大小,为确定扩容后的_finish准备,那为什么不直接+size()?看一下函数size(),其是由迭代器相减得出的结果,即_finish-_start,如果按照_finish = _start + size()求解,此时得size()是刚销毁后重置后得,其值为0,所以导致_finish得地址错误。
resize:
void resize(size_t n, T& val = T())
{
if (n < size())
{
//删除数据
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
2.4、增删查改:
对于这些接口,上述提到过:迭代器失效与扩容机制息息相关,而增删查改会频繁的调用扩容机制,因此在这里也就详细的介绍关于迭代器失效的场景以及解决的方法。
push_back、pop_back:
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
void pop_back()
{
assert(!empty());
--_finish;
}
insert:
iterator insert(iterator pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
if (_finish == _end_of_storage)
{
size_t len = pos - _start; //保留扩容前的pos位置
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len; //确定扩容后的pos位置,解决迭代器pos失效
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//插入数据
*pos = val;
++_finish;
return pos;
}
对于insert,当一个对象容量满了时,我们需要对其进行扩容,而在扩容后,就会出现一个经典的问题-迭代器失效,当我们将想要插入数据的位置pos和数据val传入函数时,pos会指向对象中的一个数据,而扩容后,整个对象的数据空间改变,但是pos还是指向原先那个已经被销毁的空间,导致迭代器失效,而解决办法便是在扩容后,更新一下pos的位置即可。
erase:
iterator erase(iterator pos)
{
assert(pos < _finish);
assert(pos >= _start);
//挪动数据
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
对于erase模拟实现,我们所需要避免的,并不是异地扩容的问题,因为erase是不需要扩容的,但是有可能会发生一些找不到的问题,即如果在最后一个位置删除,亦或者连续的偶数需要删除,这都是我们需要在代码中处理的细节问题,因此对于这些问题,我们在模拟实现时只需要记住一件事,一定要留意pos的指向,即我们必须在函数用过后及时更新pos的值,这也与库中的处理方法相同,通过返回值的方法。
其他函数接口:
T& operator[](size_t pos)
{
return _start[pos];
}
const T& operator[](size_t pos) const
{
return _start[pos];
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
三、深拷贝问题:
3.1、 vector<vector< int >>:
void test_vector9()
{
vector<vector<int>> vv;
vector<int> v(5, 1);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
for (size_t i = 0; i < vv.size(); i++)
{
for (size_t j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
可以看出vector支持T为vector类型。
此时,我们再尾插一个数据,观察一下扩容后整个对象:
void test_vector9()
{
vector<vector<int>> vv;
vector<int> v(5, 1);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
for (size_t i = 0; i < vv.size(); i++)
{
for (size_t j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
}
可以明显的看出,扩容后整个对象出现问题。
为什会出现这样的问题,仔细观察一下,对象中的大多数据都为随机值, 不难猜出,是异地扩容后,发生了浅拷贝,对象中的_start还是指向原来的地址,而且异地扩容后,会对原空间地址进行销毁,也就是_start变成了野指针。
当我们到了第五个push_back,也就是需要扩容的时候,我们发现:tmp与原本_start的位置指向的是同一个位置(注意外部的_start与内部的第一个_start指向的位置是一样的),这是由于memcpy引起的,而我们知道,memcpy所引起的异地扩容会释放旧空间,即释放旧位置所指向的位置,但这一释放,就导致了新开辟的tmp内部的指针变量指向的空间也被释放了。即:
至此,我们知道这是由于reserve中的memcpy所造成的的浅拷贝导致的,那么如何进行处理呢?
既然浅拷贝的memcpy不行,那我们就可以通过赋值的方式在拷贝中开辟新空间,进行深拷贝:
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size(); //记录扩容前对象的大小
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * size()); //浅拷贝
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
因此我们同样也需要注意: 在C++中要避免使用C语言中的函数:memcpy、realloc、malloc等(realloc原地扩还好,若是异地扩容,就会发生我们所提到的错误)
四、模拟vector代码:
vector.h:
namespace my_vector
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
//默认构造
vector()
:_start(nullptr) //也可以在声明时用缺省值
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
//vector<int> v(10,5)
vector(size_t n, const T& val = T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
//为vector<int>类型构造,防止其被识别为迭代器区间构造,造成非法指针访问
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
//迭代器区间构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last) //迭代器遍历不要使用<,链表的迭代器不是连续的
{
push_back(*first);
++first;
}
}
//拷贝构造
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
//memcpy(_start, v._start, sizeof(T) * v.size()); //浅拷贝
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
void resize(size_t n, T& val = T())
{
if (n < size())
{
//删除数据
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size(); //记录扩容前对象的大小
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * size()); //浅拷贝
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
void pop_back()
{
assert(!empty());
--_finish;
}
iterator insert(iterator pos, const T& val)
{
assert(pos <= _finish);
assert(pos >= _start);
if (_finish == _end_of_storage)
{
size_t len = pos - _start; //保留扩容前的pos位置
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len; //确定扩容后的pos位置,解决迭代器pos失效
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//插入数据
*pos = val;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos < _finish);
assert(pos >= _start);
//挪动数据
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
bool empty() const
{
return _start == _finish;
}
T& operator[](size_t pos)
{
return _start[pos];
}
const T& operator[](size_t pos) const
{
return _start[pos];
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
void test_vector1()
{
vector<int> v(10, 5);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); ++i)
{
cout << v1[i] << " ";
}
cout << endl;
v1.pop_back();
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
void test_vector2()
{
vector<int> v1(10, 5);
vector<int> v2(v1);
for (size_t i = 0; i < v2.size(); ++i)
{
cout << v2[i] << " ";
}
cout << endl;
vector<int> v3;
v3.push_back(1);
v3.push_back(2);
v3.push_back(3);
v3.push_back(4);
v3.push_back(5);
auto pos = find(v3.begin(), v3.end(), 2);
v3.insert(pos, 20);
for (size_t i = 0; i < v3.size(); ++i)
{
cout << v3[i] << " ";
}
cout << endl;
v3.erase(pos);
for (size_t i = 0; i < v3.size(); ++i)
{
cout << v3[i] << " ";
}
cout << endl;
}
void test_vector3()
{
vector<int> v(10, 5);
}
}