C++类——Vector容器的模拟实现

news2024/11/18 2:55:39

目录

一.vector类的成员变量:

二.Vector类的初始化方式:

三.vector的基本成员函数 

四.vector类的增删查改:

指针失效问题:

insert():

代码解析:

erase():

代码解析:

所以erase()函数的正确写法:

 五迭代器:

六:构造函数新写法:

        6.2非法寻址报错 

解决方法:

七.拷贝构造和赋值重载

        7.1拷贝构造:

        7.2赋值重载函数


       在上篇博客中,我们主要学习了STL的容器之一——vector(顺序表),理解了它的底层原理,在多个例子的测试和运行结果中,我们学会了众多成员函数的特性,感兴趣的小伙伴们可以点击下方的链接看一看:

zz​​​​​​​C++STL——vector类_橙予清的zzz~的博客-CSDN博客icon-default.png?t=N6B9https://blog.csdn.net/weixin_69283129/article/details/131899708?spm=1001.2014.3001.5502

        为了更加深刻的理解vector容器,今天我们就来剖析剖析vector类的底层实现代码!

注:此次实现只是能够大致模拟出vector的底层原理和成员函数实现!

一.vector类的成员变量:

template <class T>
	class vector {
	public:
		typedef T* iterator;

    private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};

        由上可知:vector作为顺序表可以存储任意类型的数据,那么就需要用到泛型模板,而成员变量的类型全都是泛型(T*)指针: 

        成员1:_start指向的是容器vector里指向数组的首地址;

        成员2:_finish指向的是容器vector数组中存放的最后一个数据位置的下一个位置;

        成员3:_endof_storage指向的是容器vector数组中总容量的下一个位置。

 

二.Vector类的初始化方式:

template <class T>
	class vector {
	public:
		typedef T* iterator;
         vector()	//构造函数——无参构造
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr)
		{
		}
		//扩容函数
		void reserve(size_t n) {
			if (n > capacity()) {
				size_t len = size();    //标记原来的存储长度
				T* tmp = new T[n];      //默认为异地扩容
				//memcpy(_start, tmp, sizeof(T) * len);		
				for (size_t i = 0; i < len; ++i) {	
						tmp[i] = _start[i];    //将原来空间的数据传到新空间中
					}
				}
				delete[] _start;    //释放旧空间
				_start = tmp;      
				_endof_enocrage = _start + n; //重新赋值
				_finish = _start + len;
			}
		}
		~vector() {    //析构
			delete[] _start;
			_start = _finish = _endof_enocrage = nullptr;
		}

     private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};   

        因为成员变量都是指针的缘故,构造函数对其成员变量做初始化只需要先构造为空指针即可——利用初始化列表进行。 

        若是对vector类对象增添数据时,需要提前判断,并且使用reserve扩容函数进行即可。

        而析构函数中只需要删除_start指向的堆区空间即可,因为_start永远指向空间的首元素地址,_start一释放,_finish和_endof_enocrage就全变为了野指针,到时候只需要置空即可。

 

三.vector的基本成员函数 

 

根据这些成员变量我们可以轻松的写出这些函数的底层实现:

    bool empty() const {        //判空
			return _finish == _start;
		}

    void clear() {
    		_finish = _start;	//只清数据,不释放空间,也不清容量
		}

	size_t size()const {        //获取数据存储长度
			return _finish - _start;
		}

	size_t capacity() const {    //获取数组现有容量
			return _endof_enocrage - _start;
		}

    size_t max_size() const {    //获取数组能存储的最大数据数量
		    size_t max = -1;
		    return (max / 4);
	    }

    void resize(size_t n, T val = T()) {    //调整数组已存数据大小,分三种情况
		if (n > capacity()) {
			reserve(n);
		}
		if (n > size()) {
			while (_finish < _endof_enocrage) {
				*_finish = val;
				++_finish;
			}
		}
		else {
			_finish = _start + n;
		    }
		}

对于size()和capacity()函数来说,使用的都是指针-指针的方式求出整型数据量。

对resize()函数底层看不懂的,可以去上边我发的链接中了解了解resize()函数的功能特性!

 

四.vector类的增删查改:

template <class T>
class vector {
    public:	 
    void push_back(const T& val) {    //尾插
		//若插入的时候空间不够,先扩容
		if (_finish == _endof_enocrage) {
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);
		}
		*_finish = val;    //插入数据
		++_finish;
	}

	void pop_back() {    //尾删
		assert(!empty());
		--_finish;
	}
    //[]重载运算符函数——用于查看或者修改vector类对象的数据
    T& operator[](size_t pos) {
	    assert(pos < size());
		return _start[pos];
	}
};

指针失效问题:

        这个问题关系到insert()和erase()两个函数的使用:

insert():

iterator insert(iterator pos, const T& val) {
			assert(pos >= _start);	  
			assert(pos < _finish);	 
			size_t len = pos - _start;
			if (_finish == _endof_enocrage) {
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);    //扩容

				//更新pos位置
				pos = _start + len;    
			}
			//挪动位置
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//插入数据
			*pos = val;
			++_finish;
			return pos;		//返回pos即可
		}

  代码解析:


           语句1: assert(pos >= _start);      //允许头插,中间插
           语句2: assert(pos < _finish);       //不允许尾插,因为有push_back()

           语句3: size_t len = pos - _start;  //记录扩容前的pos位置,这是为下文埋下的伏笔!
           语句4: pos = _start + len;            //因为扩容后pos的位置会失效,所以需要重新更新pos的指向:pos的类型是指针,且pos也是指向vector数组空间的某个位置的,若是当前vector数组可能已经满容量了,需要进行扩容,意味着会发生异地扩容的情况,异地扩容后,系统会为其重新分配一块空间,那么原来的地址空间就失效了, pos原本指向旧地址空间的特定,旧空间的地址也就不属于vector了,pos成为野指针!,所以需要重新变更pos位置的值才行!!!——这就是指针失效的解决方法。
         

        剩下的就是挪动pos位置的数据,为pos位置留下一个数据空位,供该位置增添数据。

举个例子:

        如上图:就是在该数组的元素3位置(pos)处增添一个数据9,pos的值应该是:_start+8处(_start指向的数组首元素地址,pos位置的值就需是指向_start的两个元素(int是4字节)之后的地址)

        现在由于该数组容量已满,需要扩容,重新划分一块堆区空间:

        那么pos的值就不能再指向旧空间的0x1122334c了,需要重新根据_start的地址进行+8处理。,否则就是产生insert指针失效问题! 

 


erase():

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

			iterator end = pos + 1;
            //挪动数据
			while (end < _finish) {
				*(end - 1) = *end;
				++end;
			}
			--_finish;
		}

代码解析:

        erase函数的指针失效问题在于案例测试上,代码没有什么需要注意的地方,因为删除元素不会影响扩容缩容等问题,_start和pos位置也就不需要变更。

 

 

 

         erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上指针不应该会失效。
         但是,若是出现了下面这种情况:即pos刚好是指向最后一个元素位置,那么删完之后,成员变量_finish指针就会往前挪一个元素位置,pos也就与该指针指向同一块位置,而endof_enocrage的位置是没有元素存储的,那么pos现在就处于越界状态。

        这种特殊情况会导致erase后指针失效,那么之后想要再使用之前pos指针就会发生报错等问题!为了统一普通情况和特殊情况pos位置的安全性,我们就认为erase后,pos位置指针失效了!

        所以解决方法为:加上erase的函数返回值类型为pos指针类型,每次使用完erase函数后都及时在外部更新pos位置。这样就不会导致其失效了。

 

所以erase()函数的正确写法:

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

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

加上返回值和函数返回值类型。 

 

试验代码:
void Test15() {
	Cheng::vector<int> v1;
	for (int i = 1; i < 5; ++i) {
		v1.push_back(i);
	}

	for (auto& e : v1) {
		cout << e << " ";
	}
	cout << endl;	

	//删除所有偶数
	auto pos = v1.begin();
	while (pos != v1.end()) {
		if (*pos % 2 == 0) {
			pos=v1.erase(pos);	//更新it,否则it会失效,出现错误
		}
		//若*it%2!=0,则++it(跳过)
		else {
			++pos;
		}
	}

 


 

 五迭代器:

template <class T>
	class vector {
	public:
		typedef T* iterator;
    //迭代器
		iterator begin() {
			return _start;
		}

		iterator end() {
			return _finish;
		}

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

		const_iterator end() const {
			return _finish;
		}

      private:
		iterator _start;
		iterator _finish;
		iterator _endof_enocrage;
	};

        由于迭代器的begin、end等都是指针,那么类型自然与成员变量的类型相同都是泛型指针。 

六:构造函数新写法:

如上图,这是C++官方库中给出的关于vector类的构造函数,有很多种构造函数,有了这些构造函数,可以让我们构建不同的vector对象,如下图:

 

         所以我们也可以构建几个常用的vector构造函数进行实现:

        vector()	//无参构造法
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr)
		{
		}

        //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

        //迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

        6.2非法寻址报错 

在测试使用这些构造函数时,我发现了一个问题,当我执行下面这两句代码时,代码产生了编译报错!:


vector<int> v1(5);

vector<int> v2(5,'a');

 

class vector{
public:
 //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

//迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
        }

     void push_back(const T& val) {    //尾插
		//若插入的时候空间不够,先扩容
		if (_finish == _endof_enocrage) {
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);
		}
		*_finish = val;    //插入数据
		++_finish;
	        }
 private:
	iterator _start;
	iterator _finish;
	iterator _endof_enocrage;
    };

int main(){
    vector<int> v1;
    for (size_t i = 0; i <4; ++i) {
	    	v1.push_back(i);
    }

    vector<int> v2(5,1);    //本意:创建5个元素,且全赋值为1

}

传两个参数报错:非法的间接寻址

        原因在于将vector类对象实例化时,编译器会根据类对象后面给出的参数进而寻找最匹配的构造函数,5,1都是int类型,和半缺省的size_t和T虽不是最匹配,但能用;而和迭代器区间构造函数InputIterator最匹配,但这俩参数并不能代入迭代器区间中,编译器不能理解,产生了编译报错!

解决方法:

  //vector带参构造函数法
        vector(size_t n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {
			reserve(n);
			for (int i = 0; i < n; ++i) {
				push_back(val);
			}
		}

	//vector带参构造函数
		vector(int n,const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _endof_enocrage(nullptr) {

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

        //迭代器区间构造法
        template <class InputIterator>
		vector(InputIterator first, InputIterator last) 
		:_start(nullptr)
		,_finish(nullptr)
		,_endof_enocrage(nullptr) {
			while (first != last) {
				push_back(*first);
				++first;
			}
		}

        再写一个int类型的vector带参构造函数,这样创建类对象时vector<int> v2(5,1); 编译器寻找最匹配的构造函数时会先匹配int类型的构造函数,这样就不会报寻址错误了! 

 


七.拷贝构造和赋值重载

        7.1拷贝构造:

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

        拷贝构造的本质上就是拷贝形参v的所有数据,而默认构造函数的拷贝方式是浅拷贝,对于内置类型来说,浅拷贝是正常的做法;但对于有指针开辟堆区空间的成员来说,浅拷贝无疑会造成内存泄漏,析构同一块空间多次的情况,所以浅拷贝得自己写才行! 

        所以针对深拷贝的情况,那么就得让拷贝的对象拥有一块自己的空间,剩下的就是复制被拷贝对象的数据了。

上面的代码为传统写法的代码,而拷贝构造还有一种更间接方便的形式:

    //交换函数
	void Swap(vector<T>& v) {
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_endof_enocrage, v._endof_enocrage);
	}

	//拷贝构造
	vector(const vector<T>& v)
		:_start(nullptr)
		, _finish(nullptr)
		, _endof_enocrage(nullptr)
	{
		//引用传参就得新创建对象tmp,否则直接传swap(v)就是浅拷贝
		vector<T> tmp(v.begin(), v.end());
		Swap(tmp);
	}

        首先,指派一个打工人帮你把被拷贝对象的数据做构造,你什么都不需要干,等打工人帮你把事情做好后,你就可以获取打工人所做的一切成果,然后把你自己的一无所有给了打工人,实现两者的交换,这样就完成了拷贝构造。

        注:而对于两者的交换函数,我采用的是std库中的swap函数,利用this指针与形参的各个成员变量值做交换。

        创建临时对象tmp时,编译器调用的是迭代器区间构造函数,意味着,tmp是自己开辟的一块堆区空间,并不是指向的v对象的_start空间,tmp也只是拷贝了对象v除_start以外的数据罢了,不会发生一块空间析构两次的情况!


 

        7.2赋值重载函数

 

        赋值重载函数与拷贝构造有着异曲同工之妙,所以赋值重载函数的传统写法我也就不写了。

直接展示新写法:

vector<T>& operator=(const vector<T>& v) {    
            //(引用传参不会在函数中开空间)
			//所以需要创建临时对象(实例化空间)去swap拷贝
			vector<T> tmp(v.begin(), v.end());	
			this->Swap(tmp);
			return *this;
		}

        赋值重载的新写法也是如出一辙,都是利用打工人去帮你做事情,然后使用交换函数,将各个成员变量进行交换,依次得到拷贝好的数据! 

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

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

相关文章

【面试题】Linux关于操作日志的命令行

学到老&#xff0c;活到老~ 文章目录 前言1. 查看日志文件内容1.1 cat1.2 less1.3 tail1.4 grap 2. 删除日志文件3. 分析日志文件3.1 awk3.2 sed 前言 在面试过程中&#xff0c;命令行针对日志的操作是高频的问题&#xff0c;今天我们就来彻底解决这个问题。 1. 查看日志文件内…

DAY7,C++(封装标准模板库(STL)---Vectors容器模板)(已优化)

1.封装标准模板库(STL)---Vectors容器模板&#xff08;已优化&#xff09;; #include <iostream>using namespace std;template <typename T> class Myvector { private:T *first; //指向堆区空间容器的首地址T *last; //指向容器最后一个元素的下一个位置T *en…

编译/反编译

1.Android APK 1.软件 1.apktool 1.作用&#xff1a;反编译apk或重新打包apk 2.dex2jar 1.作用&#xff1a;将Android的可执行文件.dex转换为.jar 3.jd-gui 1.作用&#xff1a;方便阅读jar文件的代码工具 2.步骤 1.通过apktool将apk软件反编译2.使用dex2jar将classes.dex文件转…

算法的时间复杂度、空间复杂度如何比较?

目录 一、时间复杂度BigO 大O的渐进表示法&#xff1a; 例题一&#xff1a; 例题2&#xff1a; 例题3&#xff1a;冒泡排序的时间复杂度 例题4&#xff1a;二分查找的时间复杂度 书写对数的讲究&#xff1a; 例题5&#xff1a; 实例6&#xff1a; 利用时间复杂度解决编…

filter(过滤器)

目录 一、过滤器应用场景 二、Filter&#xff1a;过滤器 1. 快速入门 2. 过滤器执行流程 3. 过滤器生命周期方法 4. 过滤器配置详解 5. 过滤器链(配置多个过滤器) 三、监听器 一、过滤器应用场景 过滤器是处于客户端与服务器资源文件之间的一道过滤网&#xff0c;在访问…

基于Python机器学习、深度学习在气象、海洋、水文等技能提升教程

详情点击链接&#xff1a;基于Python机器学习、深度学习技术提升气象、海洋、水文领域实践应用 前言 Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;能够在不同操作系统和平台使用&#xff0c;简洁的语法和解释性语言使其成为理想的脚本语言。…

经营简报及考核360表格

文章目录 经营简报效果图代码tableObjectSpanMethod.js 考核360委员会效果图 经营简报效果图不需要合并单元格且有汇总表头的 懒得封装了&#xff0c;所以整体没有封装 经营简报 效果图 代码 <template><el-tableref"tableRef":data"tableData.lengt…

基础实验篇 | CopterSim中回传提示消息实验

基础实验篇|CopterSim中回传提示消息实验 01实验名称及目的 回传提示消息实验&#xff1a;在飞控中&#xff0c;我们时常需要向外发布一些文字消息&#xff0c;来反映系统当前的运行状态&#xff0c;这个功能可以通过发送“mavlink_log”的uORB消息来实现。 02实验效果 在Cop…

如何让 ChatGPT 更懂你?新功能 Custom Instructions 尝试

对比 我们先来做一个对比实验。这里咱们让 ChatGPT 执行一个很简单的任务 —— 介绍一下 AI 生成内容&#xff08;AIGC&#xff09;。为了能够让 ChatGPT 查询资料&#xff0c;咱们给它提供了 Web Pilot 插件。但是 ChatGPT 并没有主动调用插件&#xff0c;而是直接给出了解释。…

Cisco IOS操作(红茶三杯CCNA)

Cisco IOS模式 &#xff1a;用户模式 #&#xff1a;特权模式(config)#&#xff1a;全局配置模式 接口模式&#xff1a;(config-if)#线路模式&#xff1a;(config-line)#路由配置模式&#xff1a;(config-router)# 使用CLI的帮助 - 命令提示及补全:?、Tab、命令关键字&…

【微信支付】Java微信支付流程

微信支付 微信支付流程 当我们需要支付一件商品时&#xff0c;首先从前端像后端传来商品ID&#xff0c;后端根据商品ID查询商品信息&#xff0c;然后封装订单信息&#xff0c;保存订单。下一步就是向微信远程调用支付接口&#xff0c;微信返回code_url&#xff0c;后端封装co…

vscode恢复被误删的文件(巧用本地历史记录)

背景&#xff1a;&#xff08;希望永远不要有这个背景&#xff09;使用vscode开发项目时&#xff0c;新建了文件&#xff0c;且文件没有git add、没有git stash、没有git commit。但是不小心点中了撤销更改&#xff08;新文件的撤销更改&#xff0c;其实就是删除该新文件&#…

【教学类-34-06】20230726拼图(“三角”凹凸拼图)3*4格子(中班主题《个别化拼图》偏美术)

图片展示&#xff1a; 圆形凹凸角变成三角形凹凸角&#xff0c;便于幼儿剪直线。 背景需求&#xff1a; 5月底&#xff0c;我自制凹凸角拼图&#xff08;动画片图片转拼图&#xff09;给幼儿裁剪&#xff0c;拼贴 教学实际操作时&#xff0c;发现圆形的凸角不适合幼儿裁剪&…

ROS 2 — 托管(生命周期)节点简介

一、说明 这篇文章是关于理解ROS 2中托管&#xff08;生命周期&#xff09;节点的概念。我们描述了概念性的想法以及我们为什么需要它。所以让我们开始吧&#xff01; 二、托管式节点 — 什么和为什么&#xff1f; 为了理解托管式节点&#xff0c;让我们从一个简单的问题陈述开…

微服务——服务异步通讯RabbitMQ

前置文章 消息队列——RabbitMQ基本概念容器化部署和简单工作模式程序_北岭山脚鼠鼠的博客-CSDN博客 消息队列——rabbitmq的不同工作模式_北岭山脚鼠鼠的博客-CSDN博客 消息队列——spring和springboot整合rabbitmq_北岭山脚鼠鼠的博客-CSDN博客 目录 Work queues 工作队列…

数据结构(c++实现)

数据结构 目录 数据结构1.链表实现单链表双链表 2.栈(先进后出&#xff0c;后进先出)3.单调栈4.队列&#xff08;先进先出&#xff09;5.单调队列6.小根堆操作 7.KMP8.Trie树(字典树) 1.链表实现 单链表 #include <iostream>using namespace std;const int N 100010;/…

AcWing 3708. 求矩阵的鞍点

输入样例&#xff1a; 3 4 1 2 3 4 1 2 3 4 1 2 3 4输出样例&#xff1a; 1 4 4 2 4 4 3 4 4 #include<bits/stdc.h> using namespace std; const int N1010; int n,m,a[N][N],x[N],y[N],flag1; int main(){scanf("%d%d",&n,&m);for(int i1;i<n;i…

抖音西瓜实时作品监控,一秒更新提醒

抖音西瓜实时作品监控&#xff0c;一秒更新提醒 安装必要的依赖库&#xff1a;使用pip安装aweme库。 pip install aweme 导入所需的库。 import datetime import time import schedule from aweme import API 创建一个函数&#xff0c;用于检查抖音作品是否更新。 def check_u…

端到端的视频编码方法及码率控制算法

文章目录 基于卷积神经网络的的端到端的视频编码方法自编码器 基于端到端学习的图像编码研究及进展变换量化熵编码 面向视频会议场景的 H.266/VVC 码率控制算法研究基于强化学习的视频码率自适应决策研究自适应流媒体传输技术码率自适应算法研究现状强化学习深度强化学习算法介…

mp4视频太大怎么压缩?教你轻松减小视频大小

MP4视频太大怎么办&#xff1f;很多人都会遇到这样的问题&#xff0c;MP4视频往因为画面清晰度高&#xff0c;画面流畅&#xff0c;所以视频文件会比较大&#xff0c;如果你想向朋友或者家人分享这个视频&#xff0c;但是又因为文件太大无法发送&#xff0c;那么怎么办呢&#…