[C++]vector模拟实现

news2024/11/16 7:33:28

目录

前言:

1. vector结构

2. 默认成员函数

2.1 构造函数

无参构造:

有参构造:

有参构造重载:

2.2 赋值运算符重载、拷贝构造(难点)

2.3 析构函数:

3. 扩容

3.1 reserve

3.2 resize

4. 插入删除

5. 迭代器操作


前言:

        本篇文章模仿的vector与STL源码并不完全一致,例如本文直接通过new来开辟空间,但是源码中通过内存池分配,但是这并不影响彼此之间的关系,所以本篇文章还是有一定的学习意义的。

1. vector结构

模板:

template<class T>

class vector

{

public:

        typedef T* iterator;

        typedef const T* const_iterator;

private:

        iterator _start;

        iterator _finish;

        iterator _end_of_storage;

        我相信如果大家用过vector的话,一定是知道每一次使用vector都需要标注清楚这个类是用来存储什么样的数据的,例如:

vector<int> vv1;                存整型数据

vector<double> vv2;         存浮点型数据

vector<vector<int>> vv3;  存存整型数据的vector数据

        所以,我们模拟的vector类就不能单独面向某一种数据,而是应该考虑到所有的类型,就算是一直自定义类型嵌套也能实例化出来,如下:

vector<vector<vector<vector<vector<int>>>>>  vv4;

         虽然上述的类型对于我们来说估计这辈子都不会遇到,但是我们总不能防止某些好奇心重的小伙伴搞事,所以是必须要能够支持这种写法的,就像是为了满足到酒吧点炒饭的帅小伙。

        所以首先得出结论:我们的类需要被构建成模板类。

成员变量:

        上面代码中可以看到我们的成员变量的类型是自定义类型重定义iterator,也就是平时我们熟知的迭代器,我们的迭代器实现又是指针的方式,所以可以将这三个变量理解为我们存储数据空间的三个位置。_start对应开头,_finish对应数据结尾,_end_of_storage对应空间容量的最后位置。也即是对应我们顺序表的size和capacity。

2. 默认成员函数

        每当我们实现一个类,默认成员函数是必不可少的,特别是像是我们vector这样需要向堆申请空间的类。

2.1 构造函数

无参构造:

        无参构造可以说是非常简单了,我们甚至连空间都不需要开辟,只需要通过初始化列表为我们的三个迭代器变量初始化即可,如下:

//默认构造方式,全指针都置空值,后续无论怎么插入都有扩容为指针赋值
vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{}

        可能有朋友要问,那用这种方式,不就表示了自己之后不能插入数据之类的吗?事实不是,因为我们还有其它的函数接口为我们服务,大家看下去就明白了。

有参构造:

        有参构造咱们就与库保持一次,既然它不支持通过一个值直接去初始化,而支持通过n个T类型数据去构造,那么我们也这样学习它这样:

         看不懂上面的库实现方式没事,看我的也是一样:

//有参构造n个T类型的数据进入vector<int>
vector(size_t n, const T& va = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	T* temp = new T[n];
	for (size_t i = 0; i < n; ++i)
	{
		//这里赋值操作是<T>和<T>之间的操作
		temp[i] = va;
	}
	_start = temp;
	_finish = _start + n;
	_end_of_storage = _finish;
}

        这里的构造也是一样,需要通过初始化列表先将三个迭代器变量初始化空指针,因为我们不能保证有没有小聪明蛋给n赋值为0去初始化,其次就是我这里使用了T()这样的匿名对象作为缺省参数。

        有同学在这里就要问了:T()这样的匿名对象生命周期不是只有一行吗?你这代码都多少行了,还在用不是错的吗?

        其实不是,在C++当中,当我们用const加引用类型的变量去接收匿名对象,会改变匿名对象的生命周期为这个变量的生命周期长度,如下:

const int& ret = T();        改变T()的生命周期为ret

T();                                生命周期只有一行

         值得注意的是上面的const是必须加的,因为我们的T()属于本身是属于临时对象的,而临时对象又有常量属性,也就是不可更改属性。如果不用const接收,那么就会导致错误。

        有点意思的是,不加const在vs2013及其之前的版本都是不报错的,后续版本我不清楚,但是到了vs2019这个BUG就被修复了。

函数设计:

        这里的代码我用了最全面的写法,后面可以通过函数复用的方式直接究极简化。

        首先通过new向堆申请n个T类型的空间,这里不能使用malloc这种C语言的申请方式,原因我相信大家也明白,就是因为我们要实现模板类,那必然有可能使用自定义类型,有自定义类型要就要去调用它的构造函数,只有new才能实现,malloc不行。

T* temp = new T[n];

        然后通过循环不断的赋值,最后为三个迭代器变量定位即可,这里循环体中的赋值操作涉及了很复杂的嵌套操作,绝不是简单的一行代码,等到下一小节重载赋值运算符我再来讲。

有参构造重载:

vector(int n, const T& va = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	while (n--)
	{
		push_back(va);
	}
}

        该重载的函数体就是我所说的究极优化的方式,通过尾插函数,不断的复用,就实现了函数功能,但是重点不在这里,我重载这个函数的意义在于为我们的迭代器区间构造函数服务。 

        为什么这么说?请先看迭代器区间构造函数。

template<class InputIterator>
vector(InputIterator start,InputIterator end)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	while (start != end)
	{
		push_back(*start);
		++start;
	}
}

         首先我们得知道一个点,那就是C++为我们提供了函数重载,那么编辑器就会通过传入的参数类型为我们匹配最适合的函数

        举例:当我们通过下列方式传参:

vector<int> vv1(5,5);

        编辑器会将我们的传参类型认为是int,int,而不是size_t,int,也就是说,编辑器就会找到迭代器区间构造函数中去了,这并不是我们想要的结果,而编辑器却认为这是最合适的函数,为了避免这种情况的出现,所以需要重载int型的构造函数。

        当然,有的小伙伴可能不太理解为什么我们需要再写一个模板出来,这理解起来真难受。这确实让人很难受,但是他带来的好处也是很大的,因为这样实现的功能,让我们的构造方式做种多样的起来,无论是何种形式的迭代器去构造,我们都有对应的方式去构造出来,这就是牛逼之处。

2.2 赋值运算符重载、拷贝构造(难点)

代码:

vector<T>& operator=(const vector<T>& va)
{
	if (_start != va._start)
	{
		//只能通过new的方式实现,因为要调用自定义类型的构造函数
		T* temp = new T[va.capacity()];
		//保留数据
		//memcpy(temp, va._start, sizeof(T)* va.size());
		int len = va.size();
		for (int i = 0; i < len;++i)
		{
			//嵌套
			temp[i] = va[i];
		}
		//释放掉原来空间,如果有的话
		delete[] _start;
		_start = temp;
		_finish = _start + va.size();
		_end_of_storage = _start + va.capacity();
	}
	return *this;
}

        赋值需要相同类型才能成功,这里用const vector<T>&相信大家也能理解,我就直接切入重点了。

        为什么我代码中要用循环一个一个的赋值,为什么不直接调用库函数memcpy()呢?简单又方便,而是通过循环一个一个的赋值,感觉写得好搓哦。其实呢,也不是博主不想用memcpy(),而是现实给了博主一巴掌,让我认清,写模板类的深拷贝有多令人头疼。

        举一个例子:如果我们构建的vector<vector<int>>类型的类,那么我们开辟了一片空间用于存n个vector<int>,然后这些vector<int>被我们直接用memcpy直接拷贝过来了,但是我们呢,又不能直接为单独写一个函数,因为这是模板,要是单独写了,又不满足vector<int>类型了。下图:

         原本我们是希望赋值出来的对象能有一个新的空间去承载这些数据,如上图,但是如果我们通过memcpy来实现能够成功吗?请看下图:

         很明显,如果通过memcpy实现了一个什么功能?我们确实实现了一层深拷贝,将vector<int>用新的空间承载起来了,但是还有更里面的空间呢?这些空间也是需要new出来的哇。用memcpy还能行吗?能行,但不完全行,除非你保证你以后不会用自定义类型模板。

        所以决定采用循环的方式,一次一次的赋值,其中有一句话非常重要,那就是:

temp[i] = va[i];

         这一句话没有你想象的那么简单,你想,如果是内置类型我们关不关心他申请空间?不关心,那么对于自定义类型它会怎么做?嵌套!!!,直到遇到内置类型嵌套就结束了,如下图:

        上图中,我们可以看到,每次我们赋值拷贝,如果遇到了一个赋值运算符,编辑器就回去检查是否是自定义类型,需要去调用赋值运算符重载函数,如此反复,直到没有赋值运算符重载函数,也就是没有需要申请空间的必要了

         下图的编译,运行,使用也不会报任何的错误了。

拷贝构造:

//拷贝构造
vector(const vector<T>& va)
{
	//赋值重载
	*this = va;
}

        拷贝构造直接复用赋值运算符重载函数就行,写法和逻辑思维差不多,博主也想偷懒。

2.3 析构函数:

//析构函数
~vector()
{
	//释放空指针不会出错,无论是什么方式
	delete[] _start;
	_start = nullptr;
	_finish = nullptr;
	_end_of_storage = nullptr;
}

        析构函数也没有什么好讲的,看代码就行。

3. 扩容

3.1 reserve

代码:

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t len = size();

		T* temp = new T[n];

		if (_start != nullptr)
		{
			//memcpy(temp, _start, sizeof(T) * size());
			for (size_t i = 0; i < len; ++i)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = temp;
		_finish = _start + len;
		_end_of_storage = _start + n;
	}
}

        vector和其它string、顺序表或者说任何数据结构一样,能不缩容就不缩容,所以,当我们的n小于容量大小的时候,不做任何处理直接返回即可。

        同样,这里也涉及到了多次深拷贝的问题,也就不能用memcpy来实现,需要用循环依次赋值。还有,咱们需要考虑到当空间异地扩容之后是需要将原空间释放的,以防内存泄漏。

3.2 resize

代码:

void resize(size_t n,const T val& = T())
{
	//小于操作不需要缩容,只需要将_finish重定位即可
	if (n < size())
	{
		_finish = _start + n;
	}
	else if (n == size()){}		//无动作
	else
	{
		int len = n - size();
		//_finish和_end_of_storage的绝对位置之差与n的大小之比
		if (_finish + n > _end_of_storage)
		{
			reserve(capacity() == 0 ? n : capacity()+n);
		}

		//补齐T类型数据
		while (len-- > 0)
		{
			*_finish = val;
			_finish++;
		}
				
	}
}

        resize功能比reserve多一个,那就是能够实现重定size大小,大于size的部分通过数据val占位。然后复用reserve,如果是第一次扩容,那就需要用三目运算来判断给定大小。

4. 插入删除

        内容比较简单,相信大家配合注解能够看懂,直接上代码:

//尾插只需要考虑如果空间不够就应该扩容
//并且,还有空容量的情况也需要考虑
void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	//如果不需要reserve表示不用扩容,那么就是原地扩,不需要动_start和_end_of_storage
	//如果扩了容,reserve会为我们将这几个变量重定向
	*_finish = val;
	++_finish;
}

//插入
iterator insert(iterator pos, const T& val)
{
	//检查插入位置的有效性
	assert(pos >= _start);
	assert(pos <= _finish);

	//提前保留绝对距离
	size_t len = pos - _start;
	//判断扩容
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	//通过绝对值偏移重定向
	pos = _start + len;

	//指向最后一个位置的下一个地址
	iterator end = _finish;

	//移动完pos位置的数据,结束
	while (end > pos)
	{
		*(end) = *(end - 1);
		--end;
	}
	//因为有赋值运算符重载,那么不管是否是内置类型,都能满足
	*pos = val;

	//向后移动一位
	++_finish;
	return pos;
}

//删除
void erase(iterator pos)
{
	//判断pos可行性
	assert(pos >= _start);
	assert(pos < _finish);

	//从pos下一个位置地址开始依次向前移动
	iterator start = pos + 1;

	//中途是没有任何扩容缩容的地方,所以迭代器不会更换
	while (start != _finish)
	{
		*(start - 1) = *start;
		++start;
	}

	//删除会有机会产生结果不确定的意外情况
	//所以,任何一次删除,该迭代器都应该被销毁
	//该函数才不会设置返回值
	--_finish;
}

//尾删,检查是否还有数据能删除
void pop_back()
{
	assert(_start != _finish);
	--_finish;
}

5. 迭代器操作

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

//求个数、容量
size_t size() const
{
	return _finish - _start;
}
size_t capacity() const
{
	return _end_of_storage - _start;
}

//重载[],需要区分const有无的区别和pos位置的准确性
T& operator[](size_t pos)
{
	assert(pos < size());

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

	return _start[pos];
}

        迭代器是有失效的情况的,这部分根据不同的编辑器会有不同的实现方式,博主这里不太想多讲,不过我建议大家通过迭代器插入删除数据之后就别再使用这个迭代器了,或者是重新给他定位,防止非法越界。


        以上就是博主对vector模拟实现的全部理解了,希望能够帮到大家。

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

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

相关文章

Matlab小波去噪——基于wden函数的去噪分析

文章目录一、问题描述二、代码问题1&#xff1a;原始信号加6分贝高斯白噪声问题2&#xff1a;确定合适的小波基函数问题3&#xff1a;确定最合适的阈值计算估计方法问题4&#xff1a;确定合适的分解层数问题5&#xff1a;实际信号去噪问题6&#xff1a;对比三、演示视频最后一、…

团队死气沉沉?10种玩法激活你的项目团队拥有超强凝聚力

作为项目经理和PMO&#xff0c;以及管理者最头疼的是团队的氛围和凝聚力&#xff0c;经常会发现团队死气沉沉&#xff0c;默不作声&#xff0c;你想尽办法也不能激活团队&#xff0c;也很难凝聚团队。这样的项目团队你很难带领大家去打胜仗&#xff0c;攻克堡垒。但是如何才能避…

Python|贪心|数组|二分查找|贪心|数学|树|二叉搜索树|在排序数组中查找元素的第一个和最后一个位置|计数质数 |将有序数组转换为二叉搜索树

1、在排序数组中查找元素的第一个和最后一个位置&#xff08;数组&#xff0c;二分查找&#xff09; 给定一个按照升序排列的整数数组 nums&#xff0c;和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target&#xff0c;返回 […

第十四届蓝桥杯三月真题刷题训练——第 2 天

目录 题目1&#xff1a;奇数倍数 代码: 题目2&#xff1a;求值 代码: 题目3&#xff1a;求和 代码: 题目4&#xff1a;数位排序 代码: 题目1&#xff1a;奇数倍数 题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即…

收银系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;随着销售行业竞争的日益激烈&#xff0c;收银系统的引入显得极其重要。收银系统不但可以提高商品存储管理的工作效率&#xff0c;而且可以有效减少盲目采购、降低采购成本、合理控制库存、减少资金占用并提高市场灵敏度&#xff0c…

Java虚拟机的运行时数据区-go语言实现

Java虚拟机的运行时数据区 Java虚拟机把存放各式各样数据的内存区域叫作运行时数据区。运行时数据区分成两类&#xff1a; 一类时多线程共享的&#xff0c;一类时线程私有的。多线程共享的数据在Java虚拟机启动时创建好&#xff0c;在Java虚拟机退出时销毁。线程私有的运行时…

序列号和反序列化--java--Serializable接口--json序列化普通使用

序列化和反序列化序列化和反序列化作用为什么需要用途Serializable使用serialVersionUID不设置的后果什么时候修改Externalizable序列化的顺序json序列化序列化和反序列化 序列化&#xff1a;把对象转换为字节序列的过程称为对象的序列化。 反序列化:把字节序列恢复为对象的过…

【Go语言学习】安装与配置

文章目录前言一、Go语言学习站二、安装与配置1.安装2.环境变量配置3.Gland编辑器安装与配置Hello, World!总结前言 Go语言特性 Go&#xff0c;又称为 Golang&#xff0c;是一门开源的编程语言&#xff0c;由 Google 开发。Go 语言的设计目标是提供一种简单、快速、高效、安全…

在MySQL中使用不等于符号还能走索引吗?

一般情况下&#xff0c;我们会在一个索引上较多的使用等值查询或者范围查询&#xff0c;此时索引大多可以帮助我们极快的查询出我们需要的数据。 那当我们在where条件中对索引列使用!查询&#xff0c;索引还能发挥他的作用吗&#xff1f; 以此SQL为例&#xff1a; select * …

农产品销售系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;这篇文章主要描述的是农产品蔬菜在线销售系统的设计与实现。主要应用关于JSP网站开发技术&#xff0c;并联系到网站所处理的数据的结构特点和所学到的知识&#xff0c;应用的主要是Mysql数据库系统。系统实现了网站的基本功能&…

计算机组成原理|第一章(笔记)

目录第一章 计算机系统概论1.1 计算机系统简介1.1.1 计算机的软硬件概念1.1.2 计算机系统的层次结构1.1.3 计算机组成和计算机体系结构1.2 计算机的基本组成1.2.1 冯 诺伊曼计算机的特点1.2.2 计算机的硬件框图1.2.3 计算机的工作过程1.3 计算机硬件的主要技术指标1.3.1 机器字…

kaggle数据集下载当中所遇到的问题

kaggle数据集下载当中所遇到的问题报错分析pip install kagglethe SSL module is not available解决方法pip的版本升级解决办法下载kaggle包kaggle数据集下载问题解决参考内容报错分析 今天在尝试使用pip install kaggle的方法去下载我需要的数据集的时候遇到了一些报错的问题…

二分查找与判定树

二分查找的算法思想二分查找也称“折半查找”&#xff0c;要求查找表为采用顺序存储结构的有序表。本例一律采用升序排列。二分查找每一次都会比较给定值与序列[low,high]的中间元素&#xff0c;该元素的下标为mid (lowhigh)/2,若两者相等&#xff0c;则返回元素的下标为mid;如…

Django的DRF从入门到精通

第一讲:建立纯净版Django项目 ① 创建Django项目 ② 创建app一个 python manage.py startapp APP名字 ③ 在settings里配置rest_framework,把不需要的全部注释掉 INSTALLED_APPS = [# django.contrib.admin,# django.contrib.auth,# django.contrib.contenttypes,# djang

centos7 安装 MySQL5.7

1.下载MySQL官方的 Yum Repository wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm2.安装 Yum Repository yum -y install mysql57-community-release-el7-10.noarch.rpm3 使用 yum 安装 MySQL yum -y install mysql-community-server若…

推荐系统1--Deepfm学习笔记

目录 1 keras实现Deepfm demo 2 deepctr模版 3 其他实现方式 ctr_Kera 模型 数据集 预处理 执行步骤 4何为focal loss 参考 1 keras实现Deepfm 假设我们有两种 field 的特征&#xff0c;连续型和离散型&#xff0c;连续型 field 一般不做处理沿用原值&#xff0c;离散型一…

Promise学习基础学习 promise封装fs模块、AJAX请求

Promise 是什么&#xff1f; 抽象表达&#xff1a; 1、Promise 是一门新的技术&#xff08;ES6规范&#xff09; 2、Promise 是JS中进行异步编程的新解决方案 备注&#xff1a;旧方案是单纯使用回调函数 具体表达&#xff1a; 1、从语法上来说&#xff1a;Promise 是一个构造…

QML Loader(加载程序)

Loader加载器用于动态加载 QML 组件。加载程序可以加载 QML 文件&#xff08;使用 source 属性&#xff09;或组件对象&#xff08;使用 sourceComponent 属性&#xff09; 常用属性&#xff1a; active 活动asynchronous异步&#xff0c;默认为falseitem项目progress 进度so…

package.json中 版本号详解

1. 版本号简介 软件版本号有四部分组成&#xff1a; 第一部分&#xff1a;主版本号&#xff0c;当进行不兼容的 API 更改时&#xff0c;则升级主版本&#xff1b;第二部分&#xff1a;次版本号&#xff0c;当以向后兼容的方式添加功能时&#xff0c;则升级次版本&#xff1b;…

FPGA实现SDI视频编解码 SDI接收发送,提供2套工程源码和技术支持

目录1、前言2、设计思路和框架SDI接收SDI缓存写方式处理SDI缓存读方式处理SDI缓存的目的SDI发送3、工程1详解4、工程2详解5、上板调试验证并演示6、福利&#xff1a;工程代码的获取1、前言 FPGA实现SDI视频编解码目前有两种方案&#xff1a; 一是使用专用编解码芯片&#xff0…