模拟实现vector

目录

I.实现基本框架


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;
}

啊 终于写完了!!!

相关推荐
少许极端2 小时前
算法奇妙屋(四十二)-贪心算法学习之路 9
学习·算法·贪心算法
CoderCodingNo2 小时前
【NOIP】1998真题解析 luogu-P1010 幂次方 | GESP四、五级以上可练习
算法
瞭望清晨2 小时前
Python多进程使用场景
开发语言·python
py有趣2 小时前
力扣热门100题之最小覆盖子串
算法·leetcode
汀、人工智能2 小时前
[特殊字符] 第102课:添加与搜索单词
数据结构·算法·均值算法·前缀树·trie·添加与搜索单词
汀、人工智能2 小时前
07 - 字典dict:哈希表的Python实现
数据结构·算法·数据库架构·哈希表的python实现
oG99bh7CK2 小时前
高光谱成像基础(六)滤波匹配 MF
人工智能·算法·目标跟踪
汀、人工智能2 小时前
04 - 控制流:if/for/while
数据结构·算法·链表·数据库架构··if/for/while
海参崴-2 小时前
C++代码格式规范
java·前端·c++