目录
一. vector 介绍
二. vector 模拟实现
1. vector的参数
2. 默认构造函数
3. size
4. capacity
5. reserve
6. resize
8. 构造函数
9. iterator
10. push_back
11. insert
12. erase
13. operator[]
STL(Standard Template Library)中的vector是一种常用的动态数组容器。它在内存中按顺序存储元素,并可根据需要自动调整容量。以下是一些关于vector容器的介绍:
总体上,vector是一个灵活、高效、易于使用的容器,用于存储和操作动态大小的元素序列。它是C++标准库(STL)中最常用的容器之一。
这里说明一下,我们模拟实现的 vector 不会实现库里面的函数,只是实现一些常用的函数,帮助我们理解 vector 的底层
vector 是一个模板,所以我们的里面存储的内热当然也是需要变动的,我们需要用到我们的模板来进行声明变量,而我们的vector 里面也是有三个参数
但是我们库里面的 vector 的三个参数都是用指针来表示的
template
class vector
{
public:
private:
iterator _start; // 起始位置
iterator _finish; // size
iterator _endofstorage; // capacity
};
}
这里说明一下为什么可以用指针代表上面的size 和 capacity
首先,_start 是不用说明的,他就是指向存储数据的一个指针,而_finish 表示存的数据的最后一个位置的后一个位置,_endofstorage表示最大存储容量的后一个位置,而我们知道指针-指针就是代表中间的数据个数,所以我们的 _finish - _start 就表示 size 而我们的 _endofstorage- _start 就表示 capacity
实现思路:默认的构造函数就是一个无参的构造函数,但是库里面的容器都有一个缺省的空间适配器,我们这里没有,所以我们这里的无参构造就是一个无参的函数,而我们的默认构造函数里面我们也不需要干什么,我们也不需要开空间,我们只需要把里面的参数初始化就好了,我们的成员变量是三个指针,所以我们可以初始化为 nullptr
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
size 该函数就是返回我们的数据个数,我们前面说了,我们只需要返回 _finish - _start 就可以了,因为我们的指针 - 指针就是中间的数据个数
size_t size() const
{
return _finish - _start;
}
capacity 返回当前最大的存储容量,我们还是可以使用 _endofstorage - _start 来计算出最大存储容量
size_t capacity() const
{
return _endofstorage - _start;
}
reserve 就是我们要开空间,那么我们怎么开空间呢? new 我们可以 new T (模板)类型的对象,然后我们将原来的值拷贝进去,如果原来没有值当然不需要拷贝,然后设置它的 _finish 和 _endofstorage。
但是着还并没有完,我们还是有很多细节的,我们在扩容的时候,如果我们(_start)是一个空呢? 我们如果直接扩容,然后修改 _start 的指向,那么我们的_finish 和 _endofstorage 呢? 所以我们是需要在扩容后修改 _finish 和 _endofstorag,但是光这样还是不够的,我们假设我们现在又数据,然后我们扩容后修改_finish 那么我们要怎么修改?我们可不可以调用 size 来确认需要将 _finish 修改到那里? 这里是不可以的,因为如果我们先修改 _start 的话,我们的 size 调用是调用 _finish - _start 来计算的,如果我们先修改了 _start 在修改 _finish 的话我们就会出错,所以我们这里又两种解决方法
上面为什么没有说 _endofstorage 呢? 因为我们的 _endofstorage 不会被影响
还没有结束,这里还有一条,既然我们的 vectoe 里面可以存储任何类型,那么我们当然是不可能只存储 内置类型,我们还会存储一些自定义类型,所以这时候问题就出来了,如果我们的自定义类型是需要深拷贝的呢?
如果我们的自定义类型需要深拷贝,但是我们又只是对 vector 进行了开空间,然后我们对 vector 里面的数据进行了浅拷贝,那么我们的 vector 里面的对象就出问题了,在虚构的时候我们会析构两次,然后出错,所以我们不能对 vector 的拷贝构造函数只进行浅拷贝,我们需要调用自定义类型对象的赋值重载。
下面详细看代码
void reserve(size_t n)
{
// 检查是否需要扩容
if (n > capacity())
{
// record old_size
size_t old_size = _finish - _start;
iterator tmp = new T[n];
// 拷贝数据
//memcpy(tmp, _start, sizeof(T) * size());
for (int i = 0; i < old_size; ++i)
{
tmp[i] = _start[i]; // 调用内置/自定义类型对象的赋值重载
}
_start = tmp;
// update _finish and _endofstorage
_finish = _start + old_size;
_endofstorage = _start + n;
}
}
我们先可以看一下 resize 的参数
void resize(size_t n, const T& val = T());
我们的 resize 又两个参数其中一个是 想要设置的大小,另外一个是设置的值,如果原本有值,那么不会修改原本的值,如果原本没有值,会设置为传入的值,如果 n 小于 size 那么就会缩小 size 为 n,如果 n 大于capacity 那么也会起到扩容的作用,然后还回填值,所以扩容的话我们可以直接复用 reserve 函数,然后扩容后在填值
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
7. 拷贝构造
我们先看一下参数
vector(const vector& v);
我们的拷贝构造就是拷贝一个和参数一样的值,那么我们的拷贝构造如何实现?
我们的拷贝构造我们需要开一块和 v 一样的值,所以但是这里和我们的 reserve 是一样的,如果我们仅仅只是对于 v 里面对象进行浅拷贝,那么我们肯定是行不通的,所以我们还是需要进行深拷贝,也就是调用v中对象的赋值重载函数
vector(const vector& v)
{
_start = new T[v.capacity()];
for (int i = 0; i < v.capacity(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
这里我们要说两个构造函数,还是先看一下这两个函数
vector(size_t n, const T& val = T());
vector(int n, const T& val = T());
首先是这个构造函数,我们创建了两个 一个是 size_t 的一个是 int 的,等我们讲完另一个构造函数后说这个问题,但是这两个函数的内部是没有一点差别的,只是参数有一点差别
我们的这两个函数的参数的意思就是定义的时候开多少个空间,用什么值来初始化,如果不传值的话,那么就是使用 T 类型的默认构造了(这里说明一下,在C++里面加入了模板之后 C++就把内置类型升级了,让内置类型也有构造函数)
vector(size_t n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
_start = new T[n];
for (int i = 0; i < n; ++i)
{
_start[i] = val;
}
_finish = _start + n;
_endofstorage = _start + n;
}
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
_start = new T[n];
for (int i = 0; i < n; ++i)
{
_start[i] = val;
}
_finish = _start + n;
_endofstorage = _start + n;
}
下面在说一下另外一个构造函数
template
vector(InputIterator first, InputIterator last);
我们的这个构造函数时可以通过迭代器来初始化该对象的,但是如果我们只是用 iterator (也就是vector 里面 typedef 的 iterator)那么我们就只能用 vector 的迭代器来初始化 vector 对象,所以我们不能只式使用一种类型的迭代器,所以我们可以在该函数前面加一个模板,让我们传参去自动推导,剩下的就是不是什么问题了
template
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
好了,这里我们说一下为什么需要了两个前面的构造函数,我们先看一下下面他们两个的的参数
vector(size_t n, const T& val = T());
template
vector(InputIterator first, InputIterator last)
我们的这两个函数的参数是这样的,那么我们看起来是并没有冲突的,但是我们可以想一下,如果我们现在传入两个值(10,1),那么这个传值会调用哪个函数?
这里我直接说了,他会调用下面的那个,但是为什么呢?我们知道在C++中的我们函数传值的话会自动挑选最匹配的那个,我们如果传值是(10,1),那么我们的模板会推导为int类型,但是我们上面的那个函数一个是 size_t 一个是 模板,所以我们的 编译器会认为下面的函数会更匹配,所以就会调用下面的函数,所以我们的解决方法就是我自己手动写一个更匹配的,所以就有了下面的 int 的函数
我这里就把 iterator 的函数一起说了,因为我们的 vector 的底层是连续的数组空间,所以我们的指针就是原生的迭代器,所以我们不需要特别写一个迭代器
下面的迭代器也是比较简单的,所以这里就不过多介绍了
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator end() const
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
其实前面的函数都介绍完后介绍这个函数就简单多了,我们的 push_back 就是尾插,当然在插入之前我们是需要检查容量的,所以我们还是先检查容量,如果我们的 capacity 是0 的话,那么我们就先少给一点空间,给4个空间(其实这里给多少都可以),然后扩容结束后我们就插入数据就可以了
void push_back(const T& value)
{
//判断是否需要扩容
if (_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = value;
_finish++;
}
insert 其实和我们前面介绍 string 时候的 insert 的逻辑差不多,也是一样的检查容量,然后挪动数据,然后插入,但是insert 是有一个问题的,我们下面会把他说清楚
iterator insert(iterator pos, const T& value)
{
// check pos 是否合法
assert(pos >= _start && pos <= _finish);
// 判断是否需要扩容
if (_finish == _endofstorage)
{
// 如果扩容,那么可能会导致 pos 位置失效(迭代器失效),所以扩容我们就要跟新 pos 的位置
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
// update pos
pos = _start + len;
}
// 挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
// 插入数据
*pos = value;
++_finish;
return pos;
}
我们想一下,如果我们在 pos 位置插入,然后我们在插入的时候刚好引起了扩容,那么我们会怎么样?
我们引起了扩容,首先我们当然是进行扩容,这里是没有任何问题的,但是我们在扩容后还是需要插入数据的在 pos 位置,那么我们的还可以在 pos 位置插入吗?
显然是不可以的,因为我们扩容后我们的 pos 位置就失效了,所以我们如果发生了扩容后还是需要对传入的 pos 位置需要进行修改了,但是我们只能修改里面的值,不能修改外面的值,所以我们的pos 位置就是在插入后很有可能会失效的,所以我们这款i就不建议在词使用 pos 位置,但是我们的库里面是有一个解决方案的,那就是返回值,我们会返回插入后的位置,然后我们可以通过使用 pos 来接受返回值,但是我们这里还是并不建议使用 插入后的 pos 位置
erase检查删除的 pos 位置是否合理,如果合理的话那么就删除,否则就报错,如果合理的话那么就挪动数据就好了,但是这里也有和 insert 同样的问题
iterator erase(iterator pos)
{
// 检查 pos 位置合法性
assert(pos >= _start && pos < _finish);
// 挪动数据
iterator current = pos + 1;
while (current != end())
{
*(current - 1) = *current;
++current;
}
--_finish;
return pos;
}
我们的现在假设我们要删除 pos 位置,如果我们删除后那么后面的元素当然会向前挪动覆盖被删除的元素,实际上如果我们不做处理,我们也是可以继续访问的,但是这里在 windows 下的 vs 下面是会报错的,这个在 linux 的 g++ 下面据没有事,这个也和平台有关,但是我们的这个 erase 的实现斌没有规定说不可以继续使用之前被使用过的迭代器,但是我们的 vs 做了特殊的处理,如果使用的话就会报错,这里我们还是建议不要使用删除后的pos 位置,因为我们不免会有一些平台事删除后会缩容,那么我们这时使用 pos 位置就会产生灾难性的后果了
我们的vector可以重载方括号([ ])而且我们重载方括号也并不会麻烦,我们下面直接看代码
T& operator[](size_t pos)
{
assert(_start + pos >= _start && _start + pos < _finish);
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos >= _start && pos < _finish);
return _start[pos];
}
今天我们要讲的就是这些,这里不免会有一些缺漏,但是我还是尽量向把他说明白,如果有什么发现缺漏或者错误的地方可以联系我。
最后这个 vector 的模拟实现的代码在我的 码云 里面,有需要的可以自取