目录
目录
前言
废话不多说,我们直接来模拟实现vector的底层实现,有了上篇文章string的底层封装和模拟实现,相信大家已经对stl的容器的底层有了一定的了解
基本框架
cpp
namespace bit {
template<class T>
class vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
// 主要接口函数
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
- 这里我们的三个成员变量参考源码中的实现方式,用指针来模拟实现。
- 并且采用缺省参数的形式将它们都初始化为nullptr,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了
迭代器
- 这里就很简单的返回我们指向开头的指针和指向最后一个元素的指针就行了
cpp
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
容量
- 如何获取元素个数和内存容量,在C语言中我们讲过指针减去指针就是他们之间的元素个数,如此即可
cpp
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
- 这里我们来重点说一下这个reserve扩容函数,他牵扯到一个很大的问题。

- 我们这里再写一个push_back的接口(后面讲),让代码先跑起来
cpp
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_finish = x;
_finish++;
}
第一个测试,野指针异常
- 测试代码
cpp
bit::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
结果:
- 可以看到,程序崩溃了,那是什么原因了,不妨调试看一下
- 插入的时候对空指针解引用,我们再一步步看看_finish指针为啥是空指针
cpp
_finish = _start + size();
- 这个时候就知道了,我们扩容的时候_start已经更新到tmp新空间的位置,但是更新_finish的时候,size()函数的结构是这样的
- 但是现在我们的_start已经是更新到tmp新空间的位置上去了,这时候_finish是旧空间,两个指针不是同一块空间相减就是一个随机值
- 那么又如何来解决这个问题呢?
- 根据上面的描述,我们发现其实就是因为更新_finish我们调用了size函数造成的问题,那么这个时候我们其实就是想要知道元素个数,但是必须要在_start更新之前,所以我们就在_start更新之前获取元素个数即可
cpp
if (n > capacity())
{
// 先保存一下原先的size()
size_t sz = size();
T* tmp = new T[n]; // 开一块新空间
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
第二轮测试,浅拷贝的问题
- 下面是我们要进行第二轮测试的代码,内部类型使用的是 string类
cpp
void test_vector2()
{
bit::vector<string> v;
v.push_back("11111");
v.push_back("22222");
v.push_back("33333");
v.push_back("44444");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
- 运行起来看并没有什么问题
- 但是呢当我再去push_back("55555")的时候程序却出现了问题,这个时候发生了扩容

- 就是对于自定义类型,虽然我们的确是tmp独立开了一块新空间,但是memcpy函数就是把指针_str的值拷贝过来,导致原来的_str和tmp的_str指向同一块空间。就会发生两个指针指向同一块空间。
- 那么在执行下面这句代码的时候,先会去把this指针的_str指向的那块空间析构掉,这个时候因为他们指向的是同一块空间,改变_str也同时更改了tmp中的_str
cpp
delete[] _start;

- 那么知道了是什么问题,我们又该如何解决这个问题呢?
- 我们知道导致这个原因的就是因为memcpy是浅拷贝,那么我们实现一个深拷贝就行了
这里我们就直接执行下面的代码:
cpp
for (size_t i = 0; i < OldSize; i++)
{
tmp[i] = _start[i];
}
- 这里的逻辑就是把_start的每个值赋值给tmp用来更新新的_start,这个时候就要重载一下赋值重载完成深拷贝,这会在后面介绍,请往下看。
接下来再看一下resize这个函数的实现
cpp
void resize(size_t n, const T& val = T())
{
if (n <= size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
- 如果重置的长度小于原来的长度,那么直接把_finish更新到_start + n的位置,就是n个长度就行
- 如果大于原来的长度,直接扩容到n个,如果扩容到n个后_finish还是小于n个的时候,再把_finish更新到n个。
元素访问
- 对于元素访问的话我们最常用的就是下标 + []的形式,这里给出两种,一个是const版本和非const版本
cpp
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
修改操作
push_back
cpp
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
- 这一块的逻辑非常简单,当最后一个元素的指针和容量的指针位置相同的时候,就需要扩容,否则直接插入,更新_finish指针就行了
insert
cpp
void insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
// 1.首先考虑扩容逻辑
if (_finish == _end_of_storage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
// 2.挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
}
- 这一块同理,插入数据先考虑是否需要扩容
- 然后把pos位置后面的数据往后面挪,知道pos位置插入x即可。
迭代器失效问题
1.这里指定位置插入函数,如果空间不管扩容的时候就出现了迭代器失效的问题
2.如果不更新pos的位置,那我们还在释放的旧空间里面插入,就会发生访问野指针!

- 那么如何去解决这个问题呢?
首先我们要明确导致这个问题发生的就是pos没有正确的更新,pos需要更新到新空间和原空间的相应位置,那么我们就算出旧空间中pos的相对位置,在新空间开辟后,把pos更新到新空间的相对位置就行了
- 但是呢就上面这样还不够,我们只解决了内部迭代器失效的问题,而外部迭代器失效的问题并没有很好地解决。
- 外部迭代器又是个什么东西,接下来看看这段代码
cpp
bit::vector<int>::iterator it = v.begin();
v.insert(it, 33);
bit::print(v);
cout << *it << endl;
bit::print(v);
- 可以看到,在使用完这个这个迭代器之后再去访问就出现了问题
- 这是因为,形参迭代器的传值并不能改变外面的实参迭代器。
- 有的同学就会说,那简单,传个引用就行了,但是你再来看看这个情况
cpp
v.insert(v.begin() + 3, 6);


erase
- 对于【erase】来说,我们也是需要先去挪动数据的,但是在这里呢我们需要从前往后挪,也是防止造成覆盖的情况
cpp
void erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator end = pos + 1;
// 移动覆盖
while (end != _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
}
- 立马来测试一下

- 看起来并没啥问题,那接下来看看下面的2个场景
- 场景1:我们在中间插入两个偶数,现在我们要对容器中的偶数进行删除,可以看看下方结果
- 可以看到发生了逻辑错误,我们的目的是把所有的偶数都删除了,可是现在容器中还有偶数!
Why?
如下图,我们删除一个数后,会把他前面的一个数挪到被删除数字的位置,那么这个时候it++,如果被挪到到删除位置的数是一个偶数就会漏掉这个数的判断。
- 场景2:如果最后一个元素是偶数,那么删除会咋样呢?如图看看我们的代码,那这样把最后一个元素删除后,不就越界访问了吗,it 会永远不等于 _finish
- 这个时候有人说,我们就要修改一下,当删除偶数的时候,不要让it++,这是对的,但还缺点东西
- 可是,万一别人底层实现erase很多次,容器的数据变少了,别人要缩容呢? 缩容是异地缩容,就是重新开一段比之前小的空间,把原来的空间数据拷贝过去,最后释放原来的空间,异地缩容是因为C++开辟内存只能是连续的内存,所以释放的时候不能只释放一部分。这时候it不就又变成,inset中的那样野指针的问题导致迭代器失效了吗?
- 这个时候,就返回被删除元素的下一个元素的位置值,这里返回pos,是因为我们的算法是被删除元素的下一个位置元素移到这个被删除元素的位置,所以被删除元素的下一个元素的位置就是pos本身.当然这里我们没有实现缩容,但是如果这里缩容,就会更新pos的位置,把新pos返回的
默认成员函数
构造函数
- 首先的话一定是构造函数,有参构造是一定要实现的,因为这里的逻辑和resize()是类似的,因此我们直接去做一个复用即可
cpp
// 有参构造
vector(size_t n, const T& val = T())
{
resize(n, val);
}
- 那有同学可能会问,三个私有成员变量不需要去做初始化吗?
- 这个时候我们的缺省值就发挥作用了,很好的避免了忘记初始化三个成员变量的问题
cpp
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
- 除了上面这种初始化,我再介绍一种方法:那就是使用 迭代器区间
cpp
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
- 复用push_back接口就行了
- 我们再补充一个构造
cpp
vector(initializer_list<T> lt)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器
for (auto e : lt)
{
push_back(e);
}
}
- 这个考前使用迭代器就行,他底层是类似数组
双重构造引发调用歧义

- 那么如何解决呢?我们模板匹配还有个原则就是有现成吃现成的,如果有int类型的构造,模板就不用去实例化,所以重载一个int类型的,让他匹配

拷贝构造
cpp
vector(const vector<T>& v)
{
reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率
for (auto& e : v)
{
push_back(e);
}
}
- push_back扩容的时候完成深拷贝
赋值重载
cpp
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
- 这里的逻辑类似string里面的,this想要得到v的资源并且把原来的资源抛弃,得到v的资源直接交换他们指向的指针就行了。由于v是局部变量,调用完会析构,就把this的资源析构了。

析构函数
cpp
~vector()
{
if (_start)
{
delete[] _start; //同一块空间
_start = _finish = _endofstorage = nullptr;
}
}
源码
cpp
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
namespace bit
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
}
vector(initializer_list<T> lt)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器
for (auto e : lt)
{
push_back(e);
}
}
vector(const vector<T>& v)
{
reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率
for (auto& e : v)
{
push_back(e);
}
}
template <class InputIterator> //类模板的成员函数也可以是一个函数模板, 这里的迭代器可以是任何类型如:string, list的迭代器构造
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
void swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
if (_start)
{
delete[] _start; //同一块空间
_start = _finish = _endofstorage = nullptr;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstorage - _start;
}
void reserve(size_t n)
{
size_t OldSize = size();
if (n > capacity())
{
T* tmp = new T[n];
for (size_t i = 0; i < OldSize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + OldSize;
_endofstorage = _start + n;
}
}
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
bool empty() const
{
return size() == 0;
}
void pop_back()
{
assert(!empty());
--_finish;
}
void resize(size_t n, const T& val = T())
{
if (n <= size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
iterator insert(iterator pos, const T& x) //这里改成引用我希望形参可以改变实参
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator end = _finish;
while (end != pos)
{
*end = *(end - 1);
end--;
}
*pos = x;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator i = pos + 1;
while (i < _finish)
{
*(i - 1) = *i;
i++;
}
_finish--;
return pos;
}
private:
iterator _start = nullptr; //写一个类一定要把缺省参数写上,防止初始化列表没初始化导致的一系列越权访问问题
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
end
感谢阅读,希望对大家有所帮助。快去实现一下吧