C++7:STL-模拟实现vector

news2024/9/20 20:26:49

目录

vector的成员变量

构造函数

reserve

size()

capacity()

push_back

 一些小BUG

赋值操作符重载

析构函数

 【】操作符重载

resize

pop_back

Insert

 迭代器失效

erase

二维数组问题

总结一下


vector,翻译软件会告诉你它的意思是向量,但其实它就是个顺序表容器,与我们刚开始实现的顺序表大差不差,但是模板的应用让它变得更加万能,接下来我们就尝试去了解学习源码是如何实现vector的。

vector的使用:

int main()
{
	vector<int> v;
	
	v = { 1,2,3,4,5 };

	for (auto& e : v)
	{
		cout << e;
	}
}	

由于模板的使用,vector不仅能存放内置类型,也可以存放自定义类型,比如说用vector套vector

 当然vector还有很多的应用型接口,在这里不记述,有需要的话请移步至这个网站:https://cplusplus.com/reference/

看上去vector的使用相当方便,也加入了我们的老熟人模板参数,那么我们就来模拟实现一下。

vector的成员变量

 借助顺序表的前车之鉴,我们推断vector的成员变量的实现应该是如下这样的

namespace myvector
{
	template<class T>
	class vector
	{

	public:
	private:
		T* _v;
		size_t _size;
		size_t _capacity;
	};
}

但是事与愿违,跑去查询STL的源码给了我们当头棒喝

 坏了,怎么是3个迭代器?我们看到,迭代器的定义是两个typedef的套娃,它的本质依旧是T*,Start我们可以理解,顺序表结构的头指针,但是为什么size和capacity变成了finish和end of storage?

在《SLT源码剖析》中我们得到了答案

 那么其实它们的本质没什么太大的分别,这样也有助于范式使用。但我这边还是比较喜欢size和capacity就不更改了。

构造函数

我们先实现一个最基本的无参版本

//无参构造函数
vector()
	:_start(nullptr), _size(nullptr), _capacity(nullptr)
{}

 然后是传参版本


vector(int n, const T& val = T())
	:_start(nullptr), _finish(nullptr), _endofstorge(nullptr)
	{
		reverse(n);
		for (int i = 0; i < n; ++i)
		{
			push_back(val);
		}
	}

reserve

 为了能实现一个有基本功能的vector,我们先解决一下扩容的问题,然后就可以愉快的写push_back了。

由于扩容的前提是不要缩容,那么我们就需要得到当前vector的size,但是不同于string可以直接得到当前的size,我们需要额外写一个函数来获取,不过也不算难事,毕竟也是比较常用的函数。

两个同类型的指针相减,得到的就是它们之间的数据类型个数。

size()

size_t size()
{
	return _size - _start;
}

capacity()

size_t capacity()
{
	return _capacity - _start;
}

那么就可以拿来实现reserve了,重置空间的逻辑跟string差不太多,首先我们检查是否发生了缩容,然后借助传入的需要开辟的空间个数来new出来一个新空间,接着把旧空间的数据用memcpy拷贝过去,然后新空间的size和capacity还需要重置一下,因为它们的本体是指针,指向的空间已经被销毁了,我们就以当前的新头指针+=上原先的数据个数让其重归正轨。

如下的代码有小BUG,我们后面讲

void reserve(const size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
        

        //如果旧空间就是需要被开辟的,也就是_start是个空指针,不需要拷贝直接赋值就行
        //重置空间的话就往下走。
		if (_start != nullptr)
		{
			memcpy(tmp, _start, _size);
			delete[] _start;
		}

		_start = tmp;
		_size = _start + size();
		_capacity = _start + n;
	}
}

push_back

 有了空间开辟的函数,push_back就没什么难度了。

		void push_back(const T& val)
		{
			if (_size == _capacity)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			*_size = val;
			++_size;
		}

 一些小BUG

 到这里时,一个基本的vector就可以使用了,但是还是有一个小问题,假如我们直接运行如上代码,就会报如下的错误。

怎么回事?为什么_size 还是空?调试时已经走过了前面的过程,那么问题就应该出现在扩容的时候,_size 赋值的方法出现了问题。

我们回到reseve函数,发现我们想利用size()函数来获得当前的数据个数时,忽略了此时的_size本身还是指向旧空间的,而_start早已更新,两个指针相减根本得不出正确答案

那么我们还是需要保存一下旧空间的个数

void reserve(const size_t n)
 {
    if (n > capacity())
	{
		T* tmp = new T[n];

		size_t oldsize = size();

		if (_start != nullptr)
		{
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}

		_start = tmp;
		_size = _start + oldsize;
		_capacity = _start + n;
	}
}

再插入5个数据试试看

 没有问题。

赋值操作符重载

 为了应对赋值的问题,编译器默认生成的赋值操作符重载函数是浅拷贝会导致析构两次而崩溃的问题,所以我们还是需要实现一下

赋值的情况发生在同类型的情况下,所以返回值和参数都应该是vector

然后借用我们之前string的现代写法,也就是把一份拷贝与当前的this进行交换,我们再实现一个简单的swap函数

void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_size, v._size);
	std::swap(_capacity, v._capacity);
}

 那么为了不影响到赋值操作符左边的值,我们不传引用,直接传值,产生一个拷贝构造然后交换。

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

析构函数

		//析构函数
		~vector()
		{
			delete[] _start;
			_start = _size = _capacity = nullptr;
		}
		 

 【】操作符重载

T& operator[](size_t n)
{
	assert(pos < size());
	return *(_start+n);
}		

resize

 resize函数同string的逻辑没什么太多的区别,唯一需要注意的是关于填充缺省值的问题。

当n>_capacity的时候,需要扩容,当n>size的时候需要向多出来的空间填充缺省值,当n<size的时候需要重置当前size的位置到n

void resize(const size_t n,T val = T())
{
	if (n > capacity())
	{
		reserve(n);
	}

    if (n > size())
    {
		while (_size < _start + n)
	    {
			*_size = val;
			++_size;
		}
	}
	else
	{
		_size = _start + n;
	}
}

在这里val为了适应自定义类型,采用了匿名构造的方式来为val赋值,对于内置类型也是生效的

T val = T()

pop_back

		void pop_back()
		{
			assert(_size  > _start);

			--_size;
		}

Insert

 我们先看看描述

 传入一个迭代器,然后在迭代器位置插入数据。那么我们简单实现一个迭代器

//迭代器
iterator begin() 
{
	return _start;
}

iterator end()
{
	return _size - 1;
}

由于auto的使用原理是自动推导,所以当我们实现了某个容器的迭代器的时候,就可以正常的使用范围for了,auto可以在范围for中成功的推导出来容器的迭代器从而实现遍历。

接下来是insert的主体逻辑

		void insert(iterator n, const T& val)
		{
			assert(n >= _start);
			//若满扩容
			if (_size == _capacity)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}

			if (n < _size)
			{
				iterator end = _size;
				while (end > n)
				{
					*end = *(end - 1);
					--end;
				}
				*n = val;
				++_size;
			}

		}

 坏了,怎么崩溃了?发生什么事了?我们调试看看

通过调试,我们发现n的值完全不在当前_start 到 _size 的位置之间,这个while循环也走了非常多遍,而且这个问题是在扩容时才发生的,那么我们就得出了一个最初的结论,n所指向的位置在扩容后无法正确生效,也就是迭代器失效问题。

 迭代器失效

迭代器失效原因如图所示,为了更好的理解,传入的迭代器n更名为pos

 那么为了能成功的修正pos的新位置,跟我们处理pushback的方法一样,我们保存之前的数据长度,然后在新的空间更新pos

		void insert(iterator n, const T& val)
		{
			assert(n >= _start);

			//扩容会引发迭代器失效的问题,需要更新迭代器
			if (_size == _capacity)
			{
				size_t length = n - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				n = _start + length;
			}

			if (n <  _size)
			{
				iterator end = _size;
				while (end > n)
				{
					*(end) = *(end - 1);
					end--;
				}

				*n = val;
				//整体大小+1,更新_size
				_size++;
			}
		}

那么通过上面的分析,我们知晓了迭代器失效的具体原因,而迭代器失效指向的真正失效其实是inset之后的迭代器不能再使用了。因为我们是传值调用,inset外部的迭代器依然没有更新,假如我们想要继续使用很有可能发生越界访问的问题。

erase

void erase(iterator n)
{
	assert(n >= _start);
	assert(n < _size);
			
	if (n == _size - 1)
	{
		pop_back();
		return;
	}

	iterator cur = n + 1;
	while (cur < _size)
	{
		*(cur - 1) = *cur;
		++cur;
	}
	--_size;
}

二维数组问题

 vector容器本身服务的对象是各种各样的类型,所以当我们希望在vector内部存放其他容器类型的时候,它也应该是支持的,我们就拿我们自己实现的vector来尝试实现一个二维数组。

 没啥问题,但是一旦发生扩容会直接崩溃

 通过调试我们发现问题发生在析构的时候

 但追根溯源,析构能发生错误的时候肯定是空间的开辟发生了问题,那么罪魁祸首应该就是我们的reserve了

 那么为什么创建二维数组的时候才报错?还是需要画图来缕一缕。

 

那么为了避免这种情况,我们应该抛弃malloc换成更加深层次的拷贝,也就是新开辟一个空间之后把原本的值原封不动的拷贝一份新的,也就是内容相同但是地址不同的新拷贝再放入扩容之后的空间。

		void reserve(const size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];

				size_t oldsize = size();

				if (_start)
				{
					for (size_t i = 0; i < oldsize; ++i)
					{
						tmp[i] = _start[i];
					}
                    //释放旧空间防止内存泄漏
					delete[]_start;
				}



				_start =  tmp;
				_size  =  tmp + oldsize;
				_capacity = _start + n;
			}
		}

总结一下

 根据以上的模拟实现,我们基本上了解了vector的基本结构以及接口的使用,其本质不同于顺序表,为了服务于自定义类型以及泛型变成,成员变量是迭代器,而迭代器的本身则是类模板参数,实现并不算困难但是细节还是需要额外处理。

 到此,模拟vector的概述就结束了,感谢阅读!希望对你有点帮助!

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

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

相关文章

面试腾讯测开岗,结束后被面试官吐槽“什么阿猫阿狗都敢来面试大厂了吗?”.....

前一阵子有个小徒弟向我诉苦&#xff0c;说自己在参加某大厂测试面试的时候被面试官怼得哑口无言&#xff0c;场面让他一度十分尴尬 印象最深的就是下面几个问题&#xff1a; 根据你以前的工作经验和学习到的测试技术&#xff0c;说说你对质量保证的理解&#xff1f; 非关系型…

不连接显示器或者HDMI欺骗器来 使用Moonlight串流游戏

环境&#xff1a;WIN11NVIDIA显卡Moonlight串流 问题&#xff1a;当主机不连接显示器时&#xff0c;Moonlight客户端黑屏 解决办法&#xff1a;使用虚拟显示器来使显卡工资 背景&#xff1a;当SteamDeck 大卖的时候&#xff0c;我开始思考是否也需要购买一台Steam Deck来躺在床…

G1垃圾回收器详解

文章目录前言一、思考问题二、官方文档三、基本介绍四、G1的内存模型五、G1的标记过程六、G1的垃圾回收1、G1过程梳理2、Young GC3、Mixed GC4、Full GC七、参数介绍八、典型问题1、疏散失败&#xff08;Evacuation Failure&#xff09;2、大对象分配&#xff08;Humongous All…

【Linux】动静态库以及动静态链接

环境&#xff1a;centos7.6&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅&#x1f339;链接扩展我们在使用Linux的时候&#xff0c;不禁会有这么一个疑问&#xff1a;为什么我们能够在Linux下进行c/c代码的编写以及编译呢&#xff1f;这是因…

_Linux(网络基础)

文章目录1. 相关基础概念2. 认识 "协议"3. 网络协议初识协议分层OSI七层模型TCP/IP五层(或四层)模型4. 网络传输基本流程网络传输流程图数据包封装和分用小结5. 网络中的地址管理认识IP地址认识MAC地址认识端口号1. 相关基础概念 独立模式: 计算机之间相互独立网络互…

MySQL数据库13——插入数据(INSERT)

下面的语句用于向student表插入数据。 插入语句&#xff1a; INSERT INTO student(ID,name,sex,birthday,origin,contact1,contact2,institute) VALUES (0013,塔赛努,男,1997/9/15,内蒙古自治区,NULL,NULL,计算机学院);INSERT INTO student VALUES (0014,呼和嘎拉,男,1995-02…

字节青训营——秒杀系统设计学习笔记(一)

如何做系统设计 1. 场景分析(Scenario) 什么系统&#xff0c;需要哪些功能&#xff0c;多大的并发量 2. 存储设计(Storage) 数据如何组织&#xff0c;Sq|存储&#xff0c; NoSq|存储 3. 服务设计(Service) 业务功能实现和逻辑整合 4. 可扩展性(Scale) 解决设计缺陷&…

Go的web开发Gin框架1(八)——Gin

一、重点内容&#xff1a; 知识要点有哪些&#xff1f; 1、了解Gin框架 2、导入使用Gin框架 3、尝试配合GORM开发 4、整合html&#xff0c;css&#xff0c;js 二、详细知识点介绍&#xff1a; 1、Gin框架介绍 ​ Gin是一个golang的微框架&#xff0c;封装比较优雅&…

MyBatis无法通过getGenerateKeys获得自增主键的问题

我遇到这个问题的法伤原因比较蠢&#xff0c;查阅了网上相关经验都没有能够解决。看看这个经验能否帮助到你。问题描述&#xff1a;设置了属性的自增后想通过getGenerateKeys获得MySQL对应表单中自增主键id&#xff0c;检查了类、映射器、xml都没有发现问题&#xff0c;但是进行…

如何使用 Python 编程进行多线程

多线程&#xff1a;理论上能在同一个时间段执行多个程序片段&#xff0c;每个程序片段就看作是一个线程。为什么要说理论上&#xff0c;因为实际在操作系统中真正的在同一时间段基本是不存在的&#xff0c;但是在软件编程中我们可以理解为它是在同一时间段执行的。 同步&#…

c/c++开发,无可避免的模板编程实践(篇一)

一、c模板 c开发中&#xff0c;在声明变量、函数、类时&#xff0c;c都会要求使用指定的类型。在实际项目过程中&#xff0c;会发现很多代码除了类型不同之外&#xff0c;其他代码看起来都是相同的&#xff0c;为了实现这些相同功能&#xff0c;我们可能会进行如下设计&#xf…

SpringBoot自定义JsonSerializer和JsonDeserializer,兼容LocalDateTime和LocalDate

1.前言 JDK1.8中添加新的时间日期API&#xff0c;LocalDate、LocalDateTime、LocalTime&#xff0c;但是我们在开发中使用时间戳作为参数值来传递是比较常用的&#xff0c;然而在SpringBoot中并没有为我们提供合适的JsonSerializer和JsonDeserializer。 我们先看看使用默认的Js…

Linux根文件系统移植

目录 一、根文件系统 1.1根文件系统 1.2根文件系统内容 二、根文件系统移植 2.1BusyBox 2.2BusyBox的获取 2.3BusyBox的使用 2.4make menuconfig 2.5编译和安装 2.6修改根文件系统 一、根文件系统 1.1根文件系统 根文件系统是内核启动后挂载的第一个文件系统系统引…

Homekit智能家居创意DIY一智能灯

一、什么是智能灯 传统的灯泡是通过手动打开和关闭开关来工作。有时&#xff0c;它们可以通过声控、触控、红外等方式进行控制&#xff0c;或者带有调光开关&#xff0c;让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块&#xff0c;可与手机、家庭智能助手、或其他智能…

链表题目总结 -- 递归

目录一. 递归反转整个链表1. 思路简述2. 代码3. 总结二. 反转链表前 N 个节点1. 思路简述2. 代码3. 总结三、反转链表的一部分1. 思路简述2. 代码3.总结四、反转链表后N个节点1. 思路简述2. 代码3.总结一. 递归反转整个链表 题目链接&#xff1a;https://leetcode.cn/problems…

部署智能合约到公链

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

【面试】生命周期详情解释及案例

目录 1.什么是生命周期 2.生命周期函数 3.vue2中生命周期的函数 4.生命周期的阶段 5.生命周期图示 第一个阶段&#xff1a;组件创建阶段 第二个阶段&#xff1a;编译HTML模板并渲染到浏览器中 第三阶段&#xff1a;组件更新阶段 第四阶段&#xff1a;组件销毁阶段 1.什…

【数据结构】基础:图的基本概念与实现(附C++源代码)

【数据结构】基础&#xff1a;图的基本概念与实现&#xff08;附C源代码&#xff09; 摘要&#xff1a;将会在数据结构专题中开展关于图论的内容介绍&#xff0c;其中包括四部分&#xff0c;分别为图的概念与实现、图的遍历、图的最小生成树以及图的最短路径问题。本文将介绍图…

遗传算法(Genetic Algorithm,GA)实现数据排序,python

遗传算法(Genetic Algorithm,GA)实现数据排序&#xff0c;python 遗传算法是一种比较广泛、通用的算法体系&#xff0c;为了说明遗传算法的原理和实现&#xff0c;现在用GA解决一个计算机科学最基本、最古老的问题&#xff1a;排序问题。 需要特别说明的是&#xff0c;遗传算…

【GPLT 三阶题目集】L3-016 二叉搜索树的结构

二叉搜索树或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的左子树不空&#xff0c;则左子树上所有结点的值均小于它的根结点的值&#xff1b;若它的右子树不空&#xff0c;则右子树上所有结点的值均大于它的根结点的值&#xff1b;它的左、右子树也分…