目录
I.实现基本框架
0x00 结构的定义
因为众所周知的缘故,我们只实现阉割版本的vector(太菜了)
成员变量的定义:
cpp
#include<iostream>
#include<assert.h>
using namespace std;
namespace zzz
{
template<T>
class vector {
public:
typedef T* iterator;
private:
iterator _start;//开始位置
iterator _finish;//结束位置
iterator _eos;// end of storage
};
}
跟实现string不一样啊!!!
其实,尽管表面上大相径庭,但是其实是差不多滴
cpp
vector()
: _start(nullptr)
, _finish(nullptr)
, _eos(nullptr) {}
我们用指针记录_start,_finish,_eos的位置,只需要指针减指针就可以知道大小或者容量
我们想求 size,只需要_finish - _start 即可,
同样的,求 capacity 我们可以_eos - _start。甚至可以求可用空间,_eos - _finish 就行。
0x01 构造函数的实现
这里要完成的是初始化工作,我们利用初始化列表将它们值成空指针即可
cpp
vector()
: _start(nullptr)
, _finish(nullptr)
, _eos(nullptr) {}
0x02析构函数的实现
析构函数也没什么说的,要做的就是释放空间,并将定义的指针置空。
cpp
/* 析构函数 */
~vector() {
if (_start) {
delete[] _start;
_start = _finish = _eos = nullptr;
}
}
0x03 0x03 实现 size() 和 capacity()
通过刚才的讲解,我们已经知道 _start 、_finish 与*_eos*的用法了,
通过指针减指针,我们就可以轻松实现 size() 和 capacity() 了,实现方式如下:
_size
cpp
size_t size() const {
return _finish - _start; // 返回数据个数
}

_capacity
cpp
size_t capacity() const {
return _eos - _start; // 返回容量
}

0x04 实现 push_back 尾插
既然我们要实现插入,我们首先需要检查是否需要增容,需要增容,就先增容后再插入数据;不需要增容,就直接插入数据。
我们之前的判断方式是 size == capacity 的时候需要增容(数据结构专栏、string 的模拟实现)
问题是,这次我们没有定义_size 和 _capacity,取而代之的是_start 、 _finish 和 _eos 的形式。

当 _finish 触及到 _eos (end of storage) 时,不就说明容量不够了吗?如下所示:

如果需要增容,我们再来思考一下增容的实现 
大概分为四步
1 开一块带有新容量的空间存到tmp中
2 再把原空间的数据拷贝到新空间
3 释放就空间
4 最后将三个指针指向新空间
注意!!!
值得注意的是,最后一步如果先将 _start 指向 tmp 后,再计算 _finish 时,此时不能现场算 size() ,现场算会出问题,因为 _start 已经被更新成 tmp 了,
如果不想改变顺序,还是想按 _start、_finish 和 _eos 的顺序赋值,我们可以提前把 size() 算好,存到一个变量中
至于新容量给多少,我们还是按照自己的习惯,首次给 4 默认扩 2 倍的方式去增容。

检查增容和增容完毕后,就剩下插入数据了,不过这个是最简单的

cpp
void push_back(const T& x)
{
if (_finish == _eos)
{
size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
size_t sz = size(); //提前算好size
T* tmp = new T[new_capacity];// 开一块新容量
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());// 再把原空间的数据拷贝到新空间,并释放原有的旧空间。
delete[] _start;// 并释放原有的旧空间
}
_start = tmp;// 指向新空间
_finish = tmp + sz;//现场计算size(),会有问题,因为start已经别改为tmp了
_eos = _start + new_capacity;
}
*_finish++ = x;
}
0x05 实现 operator[ ]

**T:**由于我们不知道返回值类型,所以给 T。
**T&:**引用返回减少拷贝。
**const:**这里 cosnt 修饰 T 和 this,是为了限制写。
cpp
const T& operator[](size_t idx) const
{
assert (idx<size());
return _start[idx];
}
II 迭代器的实现
0x00 实现迭代器的begin和end
vector 的迭代器是一个原生指针
cpp
template<class T>
class vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
我先实现一下 begin() 和 end() ,直接分别返回 _start 和 _finish即可:

cpp
iterator begin() {
return _start;
}
iterator end() {
return _finish;
}
0x01 实现const迭代器的begin和end
const 类型的迭代器,即可读不可写。在实现的时候用 const 修饰即可
cpp
const_iterator begin() const {
return _start;
}
const_iterator end() const {
return _finish;
}
啊哈,既然我们实现了迭代器,当然范围for也就可以使用了,我们来测试一下三种遍历方式
cpp
void test_vector1() {
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
// 下标 + []
for (size_t i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
// 迭代器
vector<int>::iterator it = v.begin();
while (it != v.end()) {
cout << *it << " ";
it++;
}
cout << endl;
// 范围for
for (auto e : v) {
cout << e << " ";
}
cout << endl;
}
结果如下

0x03 实现迭代器区间

我们在构造时需要注意,使用迭代器区间必须是左闭右开 ------

cpp
template <class InputIterator>
vector (InputIterator first,InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_eos(nullptr)
{
while(first!=last)
{
push_back(*first++);
}
}
关于这个InputIterator,是这样的
0x04 浅谈迭代器分类

就功能来说,下面的比上面的强,它们本质是一个继承关系,下面是子类,上面是父类,子类都是一个父类,它满足父类的所有特征,也就是说,虽然在语法上他是一个模板,允许你穿任意类型的迭代器,但是更深层次上存在着更进一步的限制

① 它要求你传随机迭代器,你就不能用双向迭代器。因为只有随机迭代器才能满足随机迭代器的所有操作。换言之,你不能用功能比它指定的迭代器少的迭代器。(可以理解为权限的放大)

② 它要求你用双向迭代器,你就不能用单向迭代器,因为单项迭代器不能满足所有双向迭代器的操作。但是你可以用比它功能多的迭代器,比如随机迭代器,因为随机迭代器也能满足双向迭代器的操作。因为随机迭代器是双向迭代器的子类,它满足父类(双向迭代器)的所有功能。(可以理解为权限的缩小)
我们弄明白了这些,我们再回到刚才提的问题 ------
❓ 为什么这里要叫 InputIterator ?不用它行不行?
首先,InputIterator 是输入迭代器,这么写是为了满足命名规范。
可以不用,我们可以传单向迭代器、双向迭代器,也可以传随机迭代器。
因为这些迭代器都满足输入迭代器的所有功能。
III vector的扩容
0x00 reverse
我们要实现 vector 的 insert,肯定需要用到增容,我们这里当然不会傻傻地重写一遍。
我们可以把刚才写 push_back 实现的增容部分拎出来,实现一个 CheckCapacity 函数。
但是我们这里可以直接实现出 reserve,到时候实现 resize 也可以复用得上,岂不美哉?

所以,我们先实现 reserve,顺便把 resize 再实现一下,再去实现 insert 。
cpp
/* reserve */
void reserve(size_t new_capacity) {
if (new_capacity > capacity()) { // 检查是否真的需要扩容
if (_finish == _eos) {
size_t sz = size(); // 提前把size算好
T* tmp = new T[new_capacity];
if (_start) {
memcpy(tmp, _start, sizeof(T) * size()); // 再把原空间的数据拷贝到新空间,并释放原有的旧空间。
delete[] _start; // 并释放原有的旧空间
}
_start = tmp; // 指向新空间
_finish = tmp + sz; // 现场算size() 会有问题,因为start已经被更新成tmp了
_eos = _start + new_capacity;
}
}
}
0x01 push_back 复用 reserve
实现完 reserve 之后,我们可以把刚才的 push_back 简化一下:
有了 reserve,我们的 push_back 直接去调它就可以了,还是按首次给4,默认扩2倍的形式走。
三目运算符得到的结果传入 reserve,结果变成 reserve 中的 new_capacity 参数,
然后 reserve 执行 new 的时候,会按照传入的new_capactiy 的大小去开空间。
cpp
/* 尾插:push_back */
void push_back(const T& x) {
// 检查是否需要增容
if (_finish == _eos) {
// 扩容
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
// 插入数据
*_finish = x;
_finish++;
}
0x02 memcpy拷贝的潜在问题
我们一开始实现的 push_back 就用了 memcpy 进行拷贝的,
然后我们刚才实现了 reserve,因而又让 push_back 复用了 reserve,
reserve 搬元素的时候也是 memcpy 去进行拷贝的,其实这里存在一个非常严重的问题!
cpp
void test_vector10() {
vector<string> v; // 在vector里放string
v.push_back("233333333333333333");
v.push_back("233333333333333333");
v.push_back("233333333333333333");
v.push_back("233333333333333333");
v.push_back("233333333333333333");
v.push_back("233333333333333333");
v.push_back("233333333333333333");
for (auto& e : v) {
cout << e << " ";
}
cout << endl;
}
这是结果:

为什么会这样?原因在于我们在扩容和深拷贝时,用了一个 memcpy!
push_back 调用 reserve 扩容时就会出问题,根本原因是 memcpy 是浅拷贝。


问题分析:memcpy 是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。如果拷贝的是自定义类型的元素,memcpy 既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为 memcpy 的拷贝实际是浅拷贝。
**结论:**如果对象中涉及到资源管理时,千万不能使用 memcpy 进行对象之间的拷贝
因为 memcpy 是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃
解决方案:不要使用 memcpy,我们手动去拷!我们修改一下 reserve:
cpp
/* reserve */
void reserve(size_t new_capacity) {
if (new_capacity > capacity()) { // 检查是否真的需要扩容
if (_finish == _eos) {
size_t sz = size(); // 提前把size算好
T* tmp = new T[new_capacity];
if (_start) {
// memcpy(tmp, _start, sizeof(T) * size()); 有问题!
//自己把原空间的数据拷贝到新空间
for (size_t i = 0; i < sz; i++) {
// 如果T是int,一个一个拷贝没问题
// 如果T是string等自定义问题,一个一个拷贝调用的是T的深拷贝,也不会出问题。
tmp[i] = _start[i];
}
delete[] _start; // 并释放原有的旧空间
}
_start = tmp; // 指向新空间
_finish = tmp + sz; // 现场算size() 会有问题,因为start已经被更新成tmp了
_eos = _start + new_capacity;
}
}
}
如果 T 是 int,一个一个拷贝没问题,
如果 T 是 string 等自定义问题,一个一个拷贝调用的是 T 的深拷贝,也不会出问题。
0x03 实现resize()
cpp
/* resize */
void resize(size_t new_capacity, const T& val = T()) {
// 如果容量足够
if (new_capacity < size()) {
_finish = _start + new_capacity; // 直接修改 _finish
}
else { // 容量不够
if (new_capacity > capacity()) { // 检查是否需要扩容
reserve(new_capacity);
}
while (_finish != _start + new_capacity) { // 初始化
*_finish = val; // 按val初始化,默认缺省为 T()
_finish++;
}
}
}
vector 的 resize 如果不给第二个参数,默认给的是其对应类型的缺省值作为 "填充值"。
由于这里我们不知道具体类型是什么,这里缺省值我们使用匿名对象 T()
此外因为匿名对象的生命周期仅在当前一行,这里必须要用 const 引用匿名对象,
可以理解为延长其生命周期
0x04 实现pop_back
pop_back 很简单,只需要 _finish-- 就可以了。
但是需要考虑删完的情况,我们这里采用暴力的处理方式 ------ 断言。
cpp
/* 尾删:pop_back */
void pop_back() {
assert(_finish > _start);
_finish--;
}
IV 迭代器失效问题
0x00 Insert/erase迭代器失效
我们通过实现vector的insert和erase,来讲解一下迭代器失效的问题
❓ 什么是迭代器失效?
"迭代器失效是一种现象,由特定操作引发,这些特定操作对容器进行操作,使得迭代器不指向容器内的任何元素,或者使得迭代器指向的容器元素发生了改变。"
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,
或者是对指针进行了封装,比如:vector 的迭代器就是原生态指针 T* 。
因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,
而使用一块已经被释放的空间,造成的后果是程序崩溃,
即,如果继续使用已经失效的迭代器,程序可能会出现崩溃。
0x01 Insert
插入可分为四个步骤:① 检查 pos 是否越界 ② 检查是否需要扩容 ③ 移动数据 ④ 插入数据
cpp
/* 插入 */
void insert(iterator pos, const T& x) {
assert(pos >= _start);
assert(pos <= _finish);
// 检查是否需要增容
if (_finish == _eos) {
// 扩容
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
// 移动数据
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end;
end--;
}
// 插入数据
*pos = x;
_finish++;
}
测试一下
cpp
void test_vector7() {
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
vector<int>::iterator pos = find(v1.begin(), v1.end(), 2);
if (pos != v1.end()) {
v1.insert(pos, 20);
}
for (auto e : v1) cout << e << " "; cout << endl;
}
结果:

我们的 insert 似乎没什么问题?我们再 push_back 一个数据看看,让它出现扩容的情况:
cpp
void test_vector7() {
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
vector<int>::iterator pos = find(v1.begin(), v1.end(), 2);
if (pos != v1.end()) {
v1.insert(pos, 20);
}
for (auto e : v1) cout << e << " "; cout << endl;
}
结果:

迭代器失效问题。扩容导致的 pos 失效,我们的 insert 没有去处理这个问题。
如果发生扩容,我们的 pos 是不是应该去更新一下?
cpp
/* 插入 */
void insert(iterator pos, const T& x) {
assert(pos >= _start);
assert(pos <= _finish);
// 检查是否需要增容
if (_finish == _eos) {
// 扩容会导致迭代器失效,扩容需要更新一下 pos
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
// 移动数据
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end;
end--;
}
// 插入数据
*pos = x;
_finish++;
}
结果如下:

但是外面的 pos(实参) 还是失效的,这里是传值,pos(形参) 是 pos(实参) 的临时拷贝。
如果 insert 中发生了扩容,那么会导致 pos(实参)指向空间被释放。
pos(实参) 本身就是一个野指针,这种问题我们称之为 ------ 迭代器失效
❓ 如何解决这里的迭代器失效问题?传引用?
传引用当然时不好的,有的 vector 还会缩容呢,传引用不能彻底解决所有问题。
🔍 我们来看看大佬是如何解决这一问题的:

然而它们是通过返回值去拿的,返回新插入的迭代器。
如果迭代器失效了,你想拿另一个迭代器去代替,就可以通过返回值去拿一下。
cpp
/* 插入 */
iterator insert(iterator pos, const T& x) {
assert(pos >= _start);
assert(pos <= _finish);
// 检查是否需要增容
if (_finish == _eos) {
// 扩容会导致迭代器失效,扩容需要更新一下 pos
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
// 移动数据
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end;
end--;
}
// 插入数据
*pos = x;
_finish++;
return pos;
}
0x02 实现 erase
cpp
void erase(iterator pos) {
assert(pos >= _start);
assert(pos <= _finish);
iterator begin = pos + 1;
while (begin < _finish) {
*(begin - 1)* begin;
begin++;
}
_finish--;
}
测试:
cpp
void test_vector8() {
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
vector<int>::iterator pos = find(v1.begin(), v1.end(), 2);
if (pos != v1.end()) {
v1.erase(pos);
}
for (auto e : v1) cout << e << " "; cout << endl;
}
结果:

迭代器失效情况
cpp
比如我们要求删除 v1 所有的偶数:
void test_vector8() {
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
// 要求删除v1所有的偶数
vector<int>::iterator pos = find(v1.begin(), v1.end(), 2);
while (pos != v1.end()) {
if (*pos % 2 == 0) {
v1.erase(pos);
}
pos++;
}
for (auto e : v1) cout << e << " "; cout << endl;
}
我们用三个场景来测试
cpp
1 2 3 4 5
1 2 4 5
1 2 3 4



erase(pos) 以后,pos 指向的意义已经变了,直接 pos++ 可能会导致一些意料之外的结果。
对于情况 ③:比如连续的偶数,导致后一个偶数没有判断,导致没有删掉。
再其次,erase 的删除有些 vector 版本的实现,不排除它会缩容。
如果是这样,erase(pos) 以后,pos 也可能会是野指针,跟 insert 类似。
(SGI 和 PJ 版本 vector 都不会缩容)
对于情况 ②:如果最后一个数据是偶数,会导致 erase 以后,pos 意义变了。
再 ++ 一下,导致 pos 和 end 错过结束判断,出现越界问题。
而情况 ①: 之所以没有翻车,是因为被删除的偶数后面恰巧跟的是奇数,运气好逃过了一劫。
导致上述三种问题的本质:erase(pos) 以后,pos 的意义变了,再去 pos++ 是不对的。
为了解决这个问题,erase 是这么说明的:

cpp
/* 删除 */
iterator erase(iterator pos) {
assert(pos >= _start);
assert(pos <= _finish);
iterator begin = pos + 1;
while (begin < _finish) {
*(begin - 1) = *begin;
begin++;
}
_finish--;
return pos;
}
void test_vector9() {
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
// 要求删除v1所有的偶数
vector<int>::iterator pos = find(v1.begin(), v1.end(), 2);
while (pos != v1.end()) {
if (*pos % 2 == 0) {
pos = v1.erase(pos); // erase以后pos失效,会返回下一个位置的迭代器
}
else {
pos++;
}
}
for (auto e : v1) cout << e << " "; cout << endl;
}
一样是返回一个迭代器,这样就规避了迭代器失效的情况
V 深拷贝
0x00 拷贝构造
可以使用传统写法,也可以使用现代写法。
cpp
传统写法
/* v2(v1) */
vector(const vector<T>& v) {
//_start = new T[v.capacity()];
//_finish = _start + v.size();
//_eos = _start + v.capacity();
reserve(v.capacity()); // 我们可以直接调用写好的reserve去开空间
// memcpy(_start, v._start, v.size() * sizeof(T)); // 会翻车
for (const auto& e : v) {
push_back(e);
}
}
现代写法
/* 现代写法:v2(v1) */
vector(const vector<T>& v)
: _start(nullptr)
, _finish(nullptr)
, _eos(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(_start, tmp._start);
swap(_finish, tmp._finish);
swap(_eos, tmp._eos);
}
根据经验,我们下面肯定还会用到 swap 的,我们不如把它封装成一个 Swap 函数
cpp
void Swap(vector<T>& tmp) {
swap(_start, tmp._start);
swap(_finish, tmp._finish);
swap(_eos, tmp._eos);
}
更新一下
cpp
/* v2(v1) */
vector(const vector<T>& v)
: _start(nullptr)
, _finish(nullptr)
, _eos(nullptr)
{
vector<T> tmp(v.begin(), v.end());
Swap(tmp);
}
0x01 赋值构造operator=

传统写法就是把 v2 赋值给 v1,自己把 v1 释放了,再去深拷贝出 v2 一样大的空间......
太麻烦了,所以我选择现代写法
cpp
/* v1 = v3 */
vector<T>& operator=(vector<T> v) {
Swap(v); // 让形参v充当tmp工具人
return *this;
}
啊 终于写完了!!!