C++STL的vector模拟实现

news2024/12/29 10:57:38

文章目录

  • 前言
  • 成员变量
  • 成员函数
    • 构造函数
    • push_back
    • pop_back
    • insert
    • erase
    • 析构函数
    • 拷贝构造

前言

成员变量

namespace but
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

我们之前实现顺序表是用指向数组的的指针和数组个数和容量来维护顺序表的,这里用三个指针来实现其实大差不差。
在这里插入图片描述

成员函数

构造函数

vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{}

push_back

void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);//避免容量为0扩容还是0的情况
	}
	*_finish = x;
	++_finish;
}

首先放数据怎么放呢?
直接赋值。

为什么可以直接赋值?
因为空间是new出来的,如果是内置类型,有没有初始化都可以直接赋值。
但是自定义类型没有初始化不能直接赋值。

扩容

void reserve(size_t n)
{
	//避免缩容
	//1.缩容的代价太大
	//2.反复缩容与扩容,降低了性能。
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		//如果旧空间没有数据就不用拷贝了
		if (_start)
		{
			//因为是类型不一定是字符串,所以得使用memcpy
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}
		_start = tmp;
		//这里有个小坑
		//_finish=_start + size();
		
		//提前把size()记录下来,防止这里出错
		//改成tmp也可以,但是有点影响我们原本的理解,不利于维护。
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

我们把其他一些需要用到的代码补上,然后测试一下。

	size_t capacity() const
	{
		return _end_of_storage - _start;
	}

	size_t size() const
	{
		return _finish - _start;
	}

迭代器

//这个普通迭代器实现起来也相当简单
iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

pop_back

删除数据好像也很简单,直接finish- -就可以,但是也有一些问题,那具体有什么问题呢?我们来看一下。
在这里插入图片描述
我们简单测试一下。
我们一直pop,就出问题了。
在这里插入图片描述
_finish一直减就走到前面去了,这样我们使用迭代器就出问题了。

我们简单改一下。

void pop_back()
{
	assert(!empty());
	--_finish;
}
bool empty()
{
	return _start == _finish;
}

针对const对象的访问
在这里插入图片描述
本质还是涉及到权限的放大,我们把成员函数都改为const就可以了。

resize()

void resize(size_t n, T val = T())//T()默认构造,是匿名对象,具体解释看下面
{
	if (n < size())
	{
		// 删除数据
		_finish = _start + n;
	}
	else
	{
		if (n > capacity())
			reserve(n);
			
		while (_finish != _start+n)
		{
			*_finish = val;
			++_finish;
		}
	}
}

上面resize的缺省值能不能给0?
其实答案很明显, 显然不行,因为T是泛型编程,类型不一定是int,如果是double或者指针,对象都不行。

那问题又来了,int有默认构造函数吗?
我们在之前学习类和对象的时候知道,内置类型是没有构造函数的,但是有了模板之后它需要有。

在这里插入图片描述

insert

在这里插入图片描述
下面这段代码有什么问题?
在这里插入图片描述
如果容量不够,挪动数据就越界了。
在这里插入图片描述
除了容量不够还有没有其他什么问题?
提示一下pos==0; 好吧,其实不会发生之前模拟实现string的时候发生的无符号整型最大值的问题。

在这里插入图片描述
大家看这样子去测试就出问题了
在这里插入图片描述
注意,func(v1)是两次读取
程序运行时崩了。
在这里插入图片描述

先简单分析一下,这可能时内存问题,可能时数组越界。
为什么push_back 5次的时候没问题,push_back 4次就出问题了?
5个和4个的区别是什么?

insert的时候扩容出问题了。

注意看
在这里插入图片描述
在这里插入图片描述
发生了什么,扩容之后,start和finish变了,start 和finish为什么会变?
这是我们遇见的最经典的迭代器失效的问题。

pos变成野指针了。
这也就导致一直在循环的问题。
在这里插入图片描述
那这个怎么解决呢?
更新一下pos.

//void insert(iterator pos, const T& val)
iterator insert(iterator pos, const T& val)
{
	assert(pos >= _start);
	assert(pos <= _finish);

	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);

		// 扩容后更新pos,解决pos失效的问题
		pos = _start + len;
	}

	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}

	*pos = val;
	++_finish;

	return pos;
}

接下来,继续问一个问题,看前面的测试图片,insert之后能不能修改迭代器的位置。

(*pos)++;//我想修改这个3的位置。
func(v1);

在这里插入图片描述
程序没有报错,但是很明显不符合我们的预期。为什么?
不是已经把pos更新了吗,为什么外面还是不行呢?怎么不起作用。
因为自己写的insert是传值形参,形参的改变 不会影响实参的改变 。

怎么解决?

能不能用引用传参解决,看起来不错,实际不好。用引用传参会报错,为什么?
在这里插入图片描述

insert 引起迭代器失效的两种情况:
1.野指针问题
2.意义已经变了

通过返回值去解决
但是最好别用,你也不知道是什么时候失效。
insert以后我们认为pos失效了,不能再使用。

erase

在这里插入图片描述
现在又有一个问题,erase以后pos会不会失效?
不会,但是库里面的失效了,并且vs报错非常强烈,看下面。
在这里插入图片描述
报了一个断言错误。

再看一下g++下的运行。
在这里插入图片描述

那到底erase以后pos失效还是不失效?vs比较合理还是g++合理?
如果pos位置是4呢,那这个位置就很不合理了。

所以我们认为失效,不要访问,行为结果未定义,跟具体的编译器有关。
一定要注意,不然会被坑的很惨。

要想解决一下这种情况,我们都是用返回值来处理一下,其实本质就是不要pos跳过。

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);

	iterator start = pos + 1;
	while (start != _finish)
	{
		*(start - 1) = *start;
		++start;
	}

	--_finish;

	return pos;
}

下面这个测试,连续的偶数和最后一个是偶数都能解决,就没什么大问题了。
在这里插入图片描述

1. vs进行强制检查,erase以后,迭代器不能访问。
2.g++没有强制检查,具体问题具体分析,结果未定义。

string有没有迭代器失效?
有,但是string不容易发生迭代器失效问题。
insert和erase不喜欢用迭代器,用的都是下标。

析构函数

~vector()
{
	delete[] _start;
	_start = _finish = _end_of_storage = nullptr;
}

下面的构造函数。
在这里插入图片描述

给大家看一个大坑。
先看下面的代码,这样写有没有什么问题?
在这里插入图片描述
程序崩了

不初始化会出各种问题。
一调试很容易看到会出现哪些问题。

加上初始化列表

vector(size_t n, const T& val = T())//T()是什么前面已经讲得很清楚了
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(n);
	for (size_t i = 0; i < n; ++i)
	{
		push_back(val);
	}
}

看一下这个构造函数。
在这里插入图片描述
在这里插入图片描述
如果是用iterator就写死了,你必须用vector的迭代器来进行初始化。

迭代器区间初始化,是必须用vectro的迭代器初始化吗?
一个容器用迭代器区间初始化,需要时任意类型的迭代器。

这里引出了另一个语法,允许一个类的成员函数再是函数模板。

// [first, last)
template <class InputIterator>
vector(InputIterator first, InputIterator last)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	while (first != last) //不能用 <=,如果是链表肯定就不行
	{
		push_back(*first);
		++first;
	}
}

如果我们不写初始化列表,可用c++11的语法,成员变量声明的时候给个缺省值
缺省值是给构造函数的初始化列表用的。
在这里插入图片描述

在这里插入图片描述

奇怪的事情发生了,编译的时候报了一个这样的错误
在这里插入图片描述

为什么匹配错了,匹配到上面写的那个迭代区间初始化那个?
在这里插入图片描述

我们知道编译器会调用最匹配的那个,仔细分析一下其实可以发现,如果推演一下的话,如果要匹配 vector(size_ t n, const T& val =T()); 的话,就会发生类型转换,而调用迭代器区间初始化的话,进行推演,如果类型是int 直接就匹配上了。

然后看迭代器区间初始化的代码,int不能解引用,就直接报错了。

怎么解决?
1.加个u, 代表我这个变量是无符号。
在这里插入图片描述

2…看STL的源码,看源码是怎么解决这个问题的。
我们这里可以用一个很简单的方式去就解决,提供一个重载的版本。

引用返回时有风险的,要谨慎使用,除非你想想operator【】那样,可以去修改。

给大家看一下神奇的东西。
在这里插入图片描述
只要类型能匹配,char可以发生转换。

最神奇的还是可以这么玩。
在这里插入图片描述

甚至可以是数组,为什么可以是数组?
原生指针可以是天然的迭代器,有个前提,这个原生指针是指向数组。
其实vector的迭代器,string的迭代器也可以是原生指针。

接着扩展一下。
sort

默认是升序
在这里插入图片描述
这是一个函数模板,它的名字叫随机访问迭代器,那随机访问是什么呢?一般底层是数组。
在这里插入图片描述
它可以帮我们排序,并且用起来很爽。

如果是降序,我们就这么用。
1.
在这里插入图片描述
2.
在这里插入图片描述
这两个其实是等价了。

拷贝构造

我们还涉及深浅拷贝的问题。
在这里插入图片描述
首先这样写,是我们经典的浅拷贝的问题。

我们先写一个传统写法的深拷贝。
在这里插入图片描述

vector(const vector<T>& v)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	_start = new T[v.capacity()];
	memcpy(_start, v._start, sizeof(T) * v.size());
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

看下面的代码,崩了,为什么?
在这里插入图片描述

在这里插入图片描述
也就是说当我们数据是int的时候程序能正常运行,当我们的数据是string的时候就崩的。

因为memcpy也是一种浅拷贝。调用拷贝构造的时候,memcpy干了什么事情。
从起始位置开始依次把所有值拷贝下来。

在这里插入图片描述
这里面还有一层是我们没有考虑的,这是深拷贝里面又套了一层深拷贝。memcpy就是深层次的浅拷贝。
调用析构的时候会崩。

怎么解决呢?
我们肯定是要解决三个问题,数据是int类型,或者string类型,或者vector的vector类型。

怎样完成深拷贝呢?难道是我们自己写吗?
我们不能自己解决,因为T是模板,我们也不知道它具体是什么类型。
不能自己给它写一个深拷贝,因为它们是私有的,动不了里面的。

所以我们这里面调一个深拷贝的函数来完成。
赋值就是深拷贝。
在这里插入图片描述

其实我们还没有完全解决所有的问题 ,除了拷贝构造用了memcpy,扩容也用了memcpy。

修改扩容的代码。

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			//memcpy(tmp, _start, sizeof(T)*size());
			for (size_t i = 0; i < sz; ++i)
			{
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

那接着,哪里还有问题呢?vector里面的数据比如说是vector对象呢,比如vectro<vector >,给大家看一下,一个杨辉三角的例子。
在这里插入图片描述
测试
在这里插入图片描述

在这里插入图片描述
出错了,为什么?还有什么问题没有解决。
外面的vector是深拷贝,里面的vector是浅拷贝。
问题还是出现在拷贝构造,我们并没有自己写赋值。所以编译器还是用的默认生成的,是浅拷贝。

我们自己写个赋值就解决了。
在这里插入图片描述

现代写法
直接复用。

void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}
vector(const vector<T>& v)
{
	vector<T> tmp(v.begin, v.end());
	swap(tmp);
}

最后一个小问题,不加模板参数语法上也允许,但是不推荐这样写。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1304886.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《opencv实用探索·十八》Camshift进行目标追踪流程

CamShift&#xff08;Continuously Adaptive Mean Shift&#xff09;是一种用于目标跟踪的方法&#xff0c;它是均值漂移&#xff08;Mean Shift&#xff09;的扩展&#xff0c;支持对目标的旋转跟踪&#xff0c;能够对目标的大小和形状进行自适应调整。 cv::CamShift和cv::me…

【CANopen进阶日记】③ CANopen对象字典工具

【CANopen进阶日记】专栏目录 第一章 CAN协议栈详解 第二章 CANopen协议栈详解 第三章 CANopen对象字典工具 文章目录 【CANopen进阶日记】专栏目录前言一、对象字典简介二、CANopen协议栈框架三、NMT主从四、SDO客户端服务器五、PDO生产者消费者5.1 RPDO5.2 TPDO同步周期5.3 …

python代码200行左右,python100行代码案例

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python代码200行左右&#xff0c;python100行代码案例&#xff0c;今天让我们一起来看看吧&#xff01; 有用的 Python 单行代码片段&#xff0c;只需一行代码即可解决特定编码问题&#xff01; 在本文中&#xff0c;我…

CTF V8 pwn入门(一)

仍然是因为某些原因&#xff0c;需要学学浏览器pwn 环境 depot_tools建议直接去gitlab里下&#xff0c;github上这个我用魔法都没下下来 下完之后执行 echo export PATH$PATH:"/root/depot_tools" >> ~/.bashrc路径换成自己的就ok了 然后是ninja git clo…

DSP28335之CAN通信易错点总结

一、CAN初始化流程 ①接收发送引脚初始化&#xff1b; ②接收和发送邮箱设置&#xff1b; ③邮箱使能&#xff1b; ④波特率配置&#xff1b; ⑤接收掩码设置&#xff1b; ⑥中断配置。 二、几个重要的知识点 ①影子寄存器 因为ECanbRegs是不可以单独对位操作&#xf…

SD-WAN架构:优化连接以提升性能

SD-WAN架构主要分为三种类型&#xff0c;分别为本地架构、支持云的架构、支持云的骨干架构。每一种架构都基于它们利用广域网&#xff08;WAN&#xff09;的方式而有其独特的优势。本文将对三种SD-WAN架构进行简要介绍。 SD-WAN本地架构 SD-WAN本地架构是在现场使用SD-WAN盒或…

焦炭冶金工艺3D可视化仿真展示更直观、形象

冶金行业作为重要的工业领域&#xff0c;其岗位实践培训一直面临着诸多挑战&#xff0c;随着web3d开发和VR虚拟仿真技术的不断创新和应用&#xff0c;冶金3D虚拟仿真实践教学平台应运而生&#xff0c;为钢铁生产培训带来了崭新的变革。 冶金3D虚拟仿真实践教学平台采用了先进的…

AIGC专题报告:ChatGPT纪要分享

今天分享的AIGC系列深度研究报告&#xff1a;《AIGC专题报告&#xff1a;ChatGPT纪要分享》。 &#xff08;报告出品方&#xff1a;久谦中台&#xff09; 报告共计&#xff1a;135页 OpenAI 高管解密 ChatGPT GPT-3 是一种大型语言模型&#xff0c;被训练用来在给定上下文中…

百度文库下载要用券?Kotlin爬虫几步解决

百度作为国内知名的网站&#xff0c;尤其是文库里面有各种丰富的内容&#xff0c;对我们学习生活都有很大的帮助&#xff0c;就因为其内容丰富&#xff0c;如果看见好用有意思的文章还用复制粘贴等方式就显得有点落后了&#xff0c;今天我将用我所学的爬虫知识给你们好好上一课…

Mac 下 Python+Selenium 自动上传西瓜视频

背景 研究下 PythonSelenium 自动化测试框架&#xff0c;简单实现 Mac 下自动化批量上传视频西瓜视频并发布&#xff0c;分享给需要的同学&#xff08;未做过多的异常处理&#xff09;。 脚本实现 首先通过手工手机号登录&#xff0c;保存西瓜视频网站的 cookie 文件 之后加载…

有什么项目可以大量使用c++实现?

有什么项目可以大量使用c实现&#xff1f; 在开始前我有一些资料&#xff0c;是我根据自己从业十年经验&#xff0c;熬夜搞了几个通宵&#xff0c;精心整理了一份「c的资料从专业入门到高级教程工具包」&#xff0c;点个关注&#xff0c;全部无偿共享给大家&#xff01;&#x…

《opencv实用探索·十七》calcBackProject直方图反向投影

在了解反向投影前需要先了解下直方图的概念&#xff0c;可以看我上一章内容&#xff1a;opencv直方图计算calcHist函数解析 直方图反向投影是一种图像处理技术&#xff0c;通常用于目标检测和跟踪。通过计算反向投影&#xff0c;可以将图像中与给定模式&#xff08;目标对象&a…

工作流程flowable(二)流程节点跟踪

flowable 节点跟踪 一、发起1-1 流程模型1-1-1 流程模型定义1-1-2 部署流程模型 {modelId} 1-2 部署管理1-2-1 流程定义列表查询 新的改变 二、我的申请新的改变 三、我的待办新的改变 三、我的已办新的改变 一、发起 1-1 流程模型 1-1-1 流程模型定义 1-1-2 部署流程模型 {…

我的隐私计算学习——隐私集合求交(2)

笔记内容来自多本书籍、学术资料、白皮书及ChatGPT等工具&#xff0c;经由自己阅读后整理而成。 前篇可见&#xff1a;我的隐私计算学习——隐私集合求交&#xff08;1&#xff09; &#xff08;三&#xff09;PSI应用场景问题 ​在目前的实际应用中&#xff0c;衍生出一些新…

美食大赛的题解

目录 原题描述&#xff1a; 题目描述&#xff1a; 输入格式&#xff1a; 输出格式&#xff1a; 样例输入&#xff1a; 样例输出&#xff1a; 数据规模&#xff1a; 题目大意&#xff1a; 主要思路&#xff1a; 注&#xff1a; 代码&#xff1a; 原题描述&#xff1a…

updateBatch批量更新

java代码 Dao层 int updateBatch(Param("userList") List<User> userList);sql配置文件 <update id"updateBatch" parameterType"java.util.List" ><foreach collection"userList" item"user" separator&quo…

Next.js中的App Router与Page Router,各自的作用和使用方式,如何理解和配置使用?

App Router介绍 Next.js中的App Router是全局的路由器&#xff0c;它用于在应用程序的所有页面之间进行导航。它可以用于在页面之间传递状态和数据&#xff0c;类似于React中的Context。 App Router是通过_app.js文件中的getInitialProps方法来配置的。 在 Next.js 中&#xf…

算法通关村第十三关—数学与数学基础问题(青铜)

数学与数学基础问题 一、统计专题 1.1 符号统计 LeetCode1822给定一个数组&#xff0c;求所有元素的乘积的符号&#xff0c;如果最终答案是负的返回-1&#xff0c;如果最终答案是正的返回1&#xff0c;如果答案是0返回0。  题目比较简单&#xff0c;正数对结果完全没影响&…

RK3568驱动指南|第八篇 设备树插件-第75章ConfigFS的核心数据结构

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…