目录
一. 初步了解
框架
如果说string实质上是一个字符串的话,那么vector实质上就是一个数组,而与string不同的是,vector的成员变量其实是三个迭代器
iterator _start; // 指向数据块的开始
iterator _finish; // 指向有效数据的尾
iterator _endOfStorage; // 指向存储容量的尾
因此,vector的大多数接口都是围绕迭代器展开的
而string是basic_string类模板的一个实例化,但vector本身就是一个类模板,这一点也需要注意
因此在使用时,我们应该在使用单书名号包上所要实例化的类型
还有一点不同的是,字符串的末尾是有一个'\0'的,而字符类型的vector也没有'\0'
构造、析构、赋值
构造
首先呢,除了最后一个拷贝构造以外,我们都能看到一个东西:alloc,这其实是一个空间配置器,我们在这里可以忽略它。因此,第一种方法其实就是一个无参的构造,构造一个空的vector类。 第二种,则是使用n个val进行构造,第三种,使用的是前后两个迭代器,至于迭代器前面的Input之类的前缀,我们在模拟实现的时候再做介绍,我们只需要知道它是迭代器就行了
而在传迭代器时,传的不仅仅可以使vector的迭代器,还可以是其他的,例如string
void test1()
{
vector<int> v1;
vector<int> v2(5, 8);
vector<int> v3(v2.begin() + 1, v2.end());
vector<int> v4(v3);
}
而上面代码中所使用的begin和end的效果和string中的一样,都是取头部的迭代器和尾部后面的迭代器。
析构
析构嘛,没什么东西
赋值
void test1()
{
vector<int> v1;
vector<int> v2(5, 8);
v1=v2;
}
迭代器
熟悉的接口,熟悉的功能
void test2()
{
vector<int> v1(5,8);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
容量
size、capacity、max_size
size大小、capacity容量,max_size有些许的不同,string中是字符串,字符大小固定为1字节,所以max_size也是固定的,而在vector中,本质是一个数组,而数组的数据类型有多种,max_size需要根据数据类型的大小来计算
resize 、reserve
void test4()
{
vector<int> v1;
v1.resize(5, 8);
v1.reserve(10);
}
访问
下标访问
void test5()
{
vector<int> v1(5, 8);
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
}
迭代器遍历
void test5()
{
vector<int> v1(5, 8);
for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++)
{
cout << *it << " ";
}
cout << endl;
}
增删
分配
我们这里就只是来看一下迭代器方式,第二种就不看了,string 里讲过了
void test6()
{
vector<int> v1(5, 8);
vector<int> v2(3, 6);
v2.assign(v1.begin() + 1, v1.end() - 1);
for (int i = 0; i < v2.size(); i++)
{
cout << v2[i] << " ";
}
cout << endl;
}
尾插、尾删、插入、删除、交换、清除
就。。。很容易理解,重点还是模拟实现上,而插入时位置参数的类型是迭代器,稍微说说
void test7()
{
vector<int> v1(4, 8);
vector<int> v2(3, 6);
v1.insert(v1.begin() + 2, 3);
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
v1.insert(v1.begin() + 3, 2, 4);
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
v1.insert(v1.begin() + 3,v2.begin(),v2.end());
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
}
查找
我们在vector中并没有发现find这个接口,这是为什么呢?这是因为,在大多数容器中,都需要find这个接口,因此就把find函数封装到stl库中了
void test8()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
cout << *find(v1.begin(), v1.end(), 2) << endl;
}
而为什么string有find这个接口呢?这是因为,string比较特殊,不仅需要查找字符,也需要查找字符串,而该find函数只能做到查找字符。
模拟实现
框架
依旧是需要一个命名空间放置冲突,不同的是由于是一个类模板,需要在头部加上tmplate...
而类的内部,我们说过,成员变量是三个迭代器,而在vector和string中,迭代器实质上就是指针,所以我们可以将指针重命名为迭代器的类型
namespace bit
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
构造、析构、赋值
构造
最简单的肯定就是无参的构造
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
之后,还有拷贝构造的传统写法,我们会使用到size()和capacity()的接口,先用着,后面实现。而在拷贝时,由于vector迭代器本质上是个指针,所以可以使用memcpy
vector(vector<T>& v)
{
_start = new T[v.capacity()];
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
memcpy(_start, v._start, v.size()*sizeof(T));
}
然后,就是传迭代器,在前面,我们说了,不只是需要传vector的迭代器,同时,由于一个类模板的成员函数又可以是一个函数模板,因此,我们可以将传迭代器的构造函数写作一个函数模板
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
而我们为什么会采用这样的名字来定义模板参数关键字呢?
这是因为,其实迭代器分为5种,input_iterator、output_iterator、forward_iterator、bidirectional_iterator以及randomaccess_iterator,根据,名字我们就可以猜出它们所能实现的功能。而从继承来看(后面学),后面的都是前面的子类(input...、output...是并列的),因此,在传参时,例如我们需要一个forward_iterator,那么我们只能传它或它的子类,也就是forward_iterator、bidirectional_iterator以及randomaccess_iterator。在讲清楚迭代器的分类后,我们上边的命名也就很好理解了,是作为一个暗示来提醒这里需要一个input_iterator。
实现了迭代器的构造之后,我们就可以实现一下拷贝构造的现代写法,依旧是构造+交换,而这里我们还需要begin()和end()的接口,由于类成员变量本身就是迭代器,所以很容易实现,我们就在这里顺便实现了
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
}
析构
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
赋值
赋值的话我们·这里就直接写现代写法了
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
容量
size、capacity
由于vector中的迭代器本质上就是指针,因此,我们可以用指针减指针来得到size和capacity
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
reserve
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
memcpy(tmp, _start, sizeof(T) * size());
_endofstorage += n - capacity();
delete[] _start;
_start = tmp;
_finish = _start + size();
_endofstorage = _start + n;
}
}
这样写看上去是没什么问题,但是,其实在给_finish赋值的时候,size()的计算已经出现了问题,因为_start已经被赋值了,而_finish还是原来的位置,因此我们可以将_finish的赋值放在前面,当然,我们也可以使用一个变量来提前计算size()
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
memcpy(tmp, _start, sizeof(T) * size());
_endofstorage += n - capacity();
delete[] _start;
_finish = _start + size();
_start = tmp;
_endofstorage = _start + n;
}
}
除此之外,还有一个问题,memcpy是浅拷贝,例如当我们的数组类型中的空间为动态开辟时,例如string,在进行浅拷贝时就会出现例如野指针的问题,因此我们要把memcpy改为深拷贝
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
resize
依旧是注意好n的大小判断就好
void resize(size_t n, const T& val = T())
{
if (n < size())
_finish = _start + n;
else
{
if (n > capacity())
reserve(n);
while (_finish != _endofstorage)
{
*_finish = val;
_finish++;
}
}
}
而模拟实现好函数内部后,我们回过头来看一个问题,在开头的传引用中,我们使用了一个匿名对象作为val的缺省值,但我们知道,匿名对象的周期只有它所在的这一行,这又是为什么呢?
这是由于,若是匿名对象赋值给一个const引用的变量时,会延迟匿名对象的生命周期,使其与该变量保持一致,上面的生命周期就是在整个函数
下标访问
也挺简单的
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
增删
insert
所注意的还是那几点,只是在一开始判断pos位置是否合法时要注意pos的类型时迭代器
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)
reserve(capacity() == 0 ? 4, 2 * capacity);
for (iterator end = _finish; end > pos; end--)
{
*end = *(end - 1);
}
*pos = x;
_finish++;
return pos;
}
这段代码看起来可能也没有什么问题,但是真的没有问题吗?
真的没有问题我就不会说出来了,那么问题出在哪里呢?
可以看到,我们若是进行扩增的话,创建的是一个新的空间,而且将成员变量赋给新的位置,然而,成员变量被重新赋值了,那么pos呢?pos还是指向的原来的位置啊,没有人在意pos的感受吗?这也就导致了迭代器的失效的问题,因此,我们也就需要重新赋值一下pos
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4, 2 * capacity);
pos = _start + len;
}
for (iterator end = _finish; end > pos; end--)
{
*end = *(end - 1);
}
*pos = x;
_finish++;
return pos;
}
小小的改进一下就好了,而返回pos这个迭代器也是为了避免原pos被继续使用而引发问题
erase
删除就不用考虑什么迭代器失效了,没什么可以失效的
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
for (iterator begin=pos; begin < _finish-1; begin++)
{
*begin = *(begin + 1);
}
_finish--;
return pos;
}
但是,真的就不会引发迭代器的失效了吗?
例如我们想实现删除数组中的偶数
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
++it;
}
要是这么写就出问题了,在删除了一个偶数之后,it迭代器指向的位置是不变的,而原位置已经被原本的下一个位置所覆盖,因此,在删除过后,其实it就算是挪到了下一个位置,不需要再进行++,如果进行++,若是刚好在最后一个位置,还是会引发迭代器的失效,即使不在最后一个位置,也会引发一些其他的错误
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
else
{
++it;
}
}
当然,string类中也有迭代器失效,情况和vector差不多
push_back、pop_back
这俩就真没什么好说的了
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = x;
++_finish;
}
void pop_back()
{
assert(_finish > _start);
--_finish;
}