C++ STL--> vector的模拟实现!

从vetor的模拟实现开始,我们就会开始涉及到泛型编程,模板类。作为模板类你要知道,vectro的元素可以有基本的数据类型: 字符,整形,空类型,浮点。 和自定义类型: 库里面的就有string,甚至可以vector<vector<int>>这样二维的。

所谓万事开头难? 我们该如何实现它呢? 从确定成员变量开始!

1. 成员变量和迭代器

这个思路其实很像顺序表升级为了模板类,原来只能放int 但是现在能放基本数据类型和自定义类型了,一一对应原来的( int*_arr(数组的首地址), size_t _size() // 个数,_capacity // 顺序表容量)

但是现在是模板类,不能很多时候可能不能用这些下标来操控,要时候指针,所以就定义三个指针: T* 类型的,分别指向 vector的首地址,和元素的末尾下一个, 以及容器的容量的位置。 但是为了统一容器操作,所以封装T*为 iterator 类型的。

其次为了更加方便的实现begin, end 这些迭代器,可以提取把 size() 和capacity实现

成员变量
cpp 复制代码
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
迭代器声明 和析构函数实现:

这里也可以写一个const的迭代器,会好一些符合库里面写的标准:

cpp 复制代码
class vector {

public:
	typedef T* iterator;
	typedef const T* const_iterator;

	iterator begin() {
		return _start;
	}

	iterator end() {
		return _finish;
	}
	// const 迭代器

	const_iterator begin() const
	{
		return _start;
	}

	const_iterator end() const {
		return _finish;
	}

	~vector() {
		delete[]_start;
		_start = nullptr;
		_finish = nullptr;
		_end_of_storage = nullptr;
	}
size和capacity:

因为我们的成员变量都是指针嘛,所以我的建议是最好把这个提前写出来 后面用起来就很方便。

cpp 复制代码
size_t size()  const
{
	return _finish - _start;
}
size_t capacity() const
{
	return _end_of_storage - _start;
}

然后写完这些,我觉得开始写一个reserve,然后再写一个push_back. 会更好哦

2.扩容的reserve(size_t n)和push_back(T val)

reserve的实现:

reserve的实现思路,最好提前存一下size,标记sz ,然后判断是否需要扩容。

扩容的操作是,1. 提前开好n个空间, 2.如果原来空间不是空的(所以需要用_start来判空)注意这里最好不要用memcpy, 你会发现我这样是循环遍历然后一个一个的赋值的操作。 下面会讲解原因 ) 不是空的,那就赋值,最好是销毁delete原来空间 。

最后是更新成员变量。

为什么把原来的值拷贝的时候不用memcpy:

这需要你对memcpy有一定的了解,memcpy是按照字节,一个一个的拷贝到新的数组中,有浅拷贝的意味,它并不会调用自定义函数的构造函数,这就是为什么要赋值操作一个一个的原因。 c中的内存管理都没有对自定义类型调用构造函数的功能,所以它的拷贝几乎都是浅拷贝。

所以我们要用赋值,举个例子,如果这里是 vector<string>, 成员变量是string,如果是通过memcpy过去的它拷贝的时候按照string的成员变量拷贝的是它的指针数组的首地址,这是浅拷贝

当我们析构原来数组的时候,这时候这块string指向的内存已经被销毁了,所以出现二次析构的现象,会造成程序崩溃。 这就是浅拷贝的危害。

cpp 复制代码
void reserve(size_t n)
{
	size_t sz = size();
	if (n > capacity())
	{
		T* temp = new T[n];
		if (_start) {
      //  不要使用 memcpy(temp,_start)
			for (int i = 0;i < size();i++)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = temp;
		_finish = _start + sz;
		_end_of_storage = _start + n;

	}

}
void push_back(T val)的实现

这个实现思路很简单的,就是 让finish指向的内存为插入的值,值得注意的地方就是,需要我们对他进行扩容判断。

不同编译器传递扩容的形式是不一样的,简单来看课vs编译器下对库里面的扩容是什么样的 和g++编译器 (linux x86内核)

g++

和vs下的:

显然linux中g++编译器使用的策略是每次都是2被扩容,vs中比较灵活,几乎都是1.5倍扩容,这是为了减少内存的浪费。

下面我的思路是效仿g++的2被扩容

cpp 复制代码
void push_back(T val) {
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : 2 * capacity());
	}
	_start[size()] = val;
	_finish++;

}

前面的 基本的迭代器啊,扩容搭建好了,后面写其他的就很顺手了,我这里是先写resize,然后进一步拷贝构造和赋值重载写了,再顺手把 insert 和eraser写了。

顺便把resize写了:

思路就是 判断n,然后小于等于size()自然就是之间覆盖就行了。

另一种情况则需要扩容然后就是赋值。 这里的建议就是赋值的时候之间利用_finishi这个指针就好了不用创建其他变量 处理结束赋值 _finish也到了指定位置了。

还有就是这里的为什么是小于等于 ,等于这种情况也算进去就因为方便,不浪费资源没必要去扩容了。

cpp 复制代码
void resize(size_t n, T val = T())
{
	if (n <= size())
	{
		_finish = _start + n;
	 }
	else {
		// 扩容 和赋值
		reserve(n);
		while(_finish <_start+n)
		{
			*_finish = val;
			_finish++;
		}

	}

}

	

3.构造函数:

主要写了3种。 空参构造 :我们这里就没有初始化列表了,因为在声明的时候就已经写好了,其次就是迭代器构造,这个是最典型的

提前写一个迭代器类型哦,因为你并不知道这个迭代器会是什么样的,它可能是一个数组传过来的,还可能是另一个vector等等 vector(InputIterator begin,InputIterator last)

实现起来就是遍历这个迭代器然后push_back一下

还有两种就是 vector (size_t n,T val=T()) ,含义就是 构造一个具有n个的元素都是Val的vector但是有一个前提就是,这里对val的缺省值是用了T()这是一个比较新的东西对刚接受实现vector来说,这是因为你传递的值还有可能是自定义类型的值,对应内置类型也是有基类的所以这样调用的构造函数是没有问题的。 这里的本质就是调用一个匿名函数然后拷贝构造给val。 这是很常见用于缺省值的。

存在一个经常报经过的问题传递参数是vector(5,1) 这里的数字5默认是int的,然后被强转为size_t 所以同时还可以重载一个vector (int n,T val=T())

cpp 复制代码
// 1. 空参构造  初始化在成员变量声明就已经初始化了
vector() {
  }

template<class InputIterator>
//  迭代器插入
vector(InputIterator begin, InputIterator last) {

	while (begin < last)
	{
		push_back(*begin);
		begin++;
	}
}
// n  val   为了兼容同时可以写一个int n 
 
vector(size_t n, T val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
		push_back(val);

}
vector(int  n, T val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
		push_back(val);

}

4.现代写法的拷贝构造和赋值重载 以及swap

其实写法跟string的实现很类似,先把swap实现了 ,然后就是拷贝构造中利用构造函数创建了一个temp中间变量。 然后和现在对象swap。

赋值重载则是在传参的时候不穿引用会利用拷贝构造创建的temp 然后在swap一下

swap的实现

swap的实现 就是把当前对象的所有成员变量的值一一交换一下就行。

cpp 复制代码
void swap(vector<T>& v)
{
	std::swap(_start,v._start);
	std::swap(_end_of_storage,v._end_of_storage);
	std::swap(_finish,v._finish);
}
拷贝构造和赋值重载

你看吧挺巧妙的,层层利用: 拷贝构造利用构造

赋值重载 利用拷贝构造

cpp 复制代码
//拷贝构造和赋值重载
 vector(const vector<T>&v)
 { 
	 vector temp(v.begin(), v.end());
	 swap(temp);
  	 }

 vector& operator=(vector<T>temp)
 {
	 swap(temp);
	 return *this;

 }

5.erase和insert

首先你心里面有这张图很重要。

erase 实现:

eraser, iterator eraser(iterator pos)

删除指定位置的参数。

首先最后是断言一下pos在_start 和_finish之间,其次只需要进行一个覆盖就行从_finishi-1到pos覆盖就行了。

可是从后往前覆盖会丢失数据使用可以从前往后覆盖 写一个临时迭代器it=pos+1

然后 *(it-1 )=*it 一直到it==finish

最后记得让_finish--

cpp 复制代码
// resize(n,T val=T());
iterator eraser(iterator pos)
{
	assert(pos>=_start&&pos<_finish);
	iterator it = pos+1;
	while (it<_finish)
	{
		*(it - 1) = *it;
		it++;
	  }
	_finish--;
return pos;

 }
insert的实现

iterator insert(iterator pos,const T&val)

在pos的位置插入val的值,那就从后往前覆盖把位置空处理。然后进行插入即可

迭代器失效问题

迭代器失效问题还有好几种,下面我会说明的。这里的修改后的结果:

cpp 复制代码
// insert的实现 
iterator insert(iterator pos, const T& val)
{
	
	
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
	
		reserve(capacity() == 0 ? 4 : 2 * capacity());
		pos = _start + len;
	}
	
	iterator it = _finish;
	while (it > pos)
	{
		*it = *(it - 1);
		it--;
	}
	*pos = val;
	_finish++;
	return pos;
相关推荐
weixin_307779135 分钟前
Redis Windows迁移方案与测试
c++·windows·redis·算法·系统架构
歪歪1002 小时前
HTML 如何转 Markdown
开发语言·chrome·python·程序人生·html
zm2 小时前
bool 类型转换运算符重载
c++
小指纹2 小时前
cf--思维训练
c++·算法·macos·ios·objective-c·cocoa
小指纹2 小时前
河南萌新联赛2025第(四)场【补题】
数据结构·c++·算法·macos·objective-c·cocoa·图论
菜鸟555552 小时前
河南萌新联赛2025第四场-河南大学
c++·算法·思维·河南萌新联赛
源远流长jerry2 小时前
C++、STL面试题总结(二)
jvm·c++
小坏坏的大世界2 小时前
C++中多线程和互斥锁的基本使用
开发语言·c++
路由侠内网穿透2 小时前
本地部署 SQLite 数据库管理工具 SQLite Browser ( Web ) 并实现外部访问
运维·服务器·开发语言·前端·数据库·sqlite
王者鳜錸2 小时前
PYTHON从入门到实践-18Django模版渲染
开发语言·python·django