C++初阶:适合新手的手撕vector(模拟实现vector)

news2024/10/4 16:35:27

上次讲了常用的接口:C++初阶:容器(Containers)vector常用接口详解
今天就来进行模拟实现啦


文章目录

  • 1.基本结构与文件规划
  • 2.空参构造函数(constructor)
  • 4.基本函数(size(),capacity(),resize(),reserve())
  • 4.增删改查(push_back,pop_back,insert,erase)
  • 5.在实现Insert和erase时迭代器失效问题
  • 6.重载[]
  • 7. 完善构造函数
    • 7.1vector (size_type n, const value_type& val = value_type());
    • 7.2利用迭代器进行构造
    • 7.3拷贝构造
  • 8.重载=
  • 9.析构函数


1.基本结构与文件规划

请添加图片描述

  • vector.h头文件:包含类的全部(函数的声明与定义)
  • test.cpp源文件:进行调用test函数,测试和完善功能

基本结构,先看一下源码:

请添加图片描述

namespace MyVector
{
	template <class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;//先定义好迭代器

		//各种函数

	private:
		iterator _start;
		iterator _finish;
		iterator _endOfStorage;
	};
}
  • _start:指向动态数组的起始位置的指针,即第一个元素的位置。
  • _finish:指向动态数组中最后一个元素之后的位置的指针。在这个实现中,_finish 指针始终指向当前元素范围的末尾,也就是下一个要插入元素的位置。
  • _endOfStorage:指向动态数组分配的内存空间的末尾之后的位置的指针。在这个实现中,_endOfStorage 指针指向当前分配的内存空间的末尾,当需要扩充容量时,会通过比较 _finish_endOfStorage 的位置来判断是否需要重新分配更大的内存空间

2.空参构造函数(constructor)

		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endOfStorage(nullptr)//直接使用初始化列表
		{}

都初始化为空指针


#3.迭代器(iterator)(begin(),end())

		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}

		const_iterator begin()const
		{
			return _start;
		}
		const_iterator end()const
		{
			return _finish;
		}

进行const的重载

4.基本函数(size(),capacity(),resize(),reserve())

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				int old_size = size();//保存一下长度,方便后续给_finish移到新的位置
				T* tmp = new T[n];
				if (_start != nullptr)//vector里存东西了
				{
					for (size_t i = 0; i < size(); ++i)
					{
						tmp[i] = _start[i];//_start本质是指针
					}
				}
				delete[] _start;
				_start = tmp;

				_finish = _start + old_size;
				_endOfStorage = _start + n;
			}
		}

		void resize(size_t n, const T& x = T())
		{
			if (n > size())
			{
				reserve(n);//<capacity 的话,也没有进行处理
				while (_finish != _start + n)
				{
					*_finish = x;
					++_finish; 
				}
			}
			else
			{
				_finish = _start + n;//小于长度时,直接移动finish
			}
		}

		size_t size()
		{
			return _finish - _start;
		}

		size_t capacity()
		{
			return _endOfStorage - _start;
		}

  1. reserve 函数:
  • reserve 函数用于保留至少能容纳 n 个元素的内存空间。如果当前的容量小于 n,则会分配新的内存空间,并将原来的元素复制到新的内存空间中。
  • 首先,它会创建一个新的大小为 n 的临时数组 tmp,然后将原始数组中的元素复制到临时数组中。
  • 接着,释放原始数组的内存空间,将 _start 指针指向新分配的内存空间,同时更新 _finish_endOfStorage 的位置。
  1. resize 函数:
  • resize 函数用于改变数组的大小,使其包含 n 个元素,并使用值 x 进行初始化。
  • 如果 n 大于当前的大小,它会调用 reserve 函数以确保数组有足够的容量,然后将数组的大小增加到 n,并使用值 x 进行初始化。
  • 如果 n 小于当前的大小,它会直接将 _finish 指针移动到新的位置,从而改变数组的大小。
  1. size 函数:
  • size 函数用于返回数组中元素的个数,即 _finish_start 之间的距离。
  1. capacity 函数:
  • capacity 函数用于返回数组的容量,即 _endOfStorage_start 之间的距离

怎么来理解:const T& x = T()

实现给出各种类型的默认值,在这里为了妥协,其实内置类型也有构造函数在 C++ 中。内置类型(如 intfloatdouble 等)也有默认构造函数。默认构造函数对于内置类型来说,其实就是不带参数的构造函数,它会将变量初始化为默认值

  1. T() 表示创建一个类型 T 的临时对象,并进行值初始化。这里假设 T 是一个类或者结构体,那么这个语句会调用 T 的默认构造函数来创建一个临时对象。
  2. const T& x 表示创建一个类型为 T 的常量引用 x。这里的引用是 T 类型的引用,而且是常量引用,意味着 x 引用的对象是不可修改的。
  3. const T& x = T() 将这个临时对象绑定到常量引用 x 上。这样做的好处是可以避免不必要的拷贝,同时也可以确保 x 引用的对象是不可修改的。

使用如下来测试

	void test1()
	{
		vector<int> v;
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.resize(10);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.resize(5);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

请添加图片描述


4.增删改查(push_back,pop_back,insert,erase)

		void push_back(const T& x)
		{
			if (_finish == _endOfStorage)
			{
				int newcapacity = capacity() == 0 ? 2 : 2 * capacity();
				reserve(newcapacity);
			}
			*_finish = x;
			_finish++;
		}

		void pop_back()
		{
			assert(size() > 0);
			--_finish;
		}

		iterator insert(iterator pos, const T& x)//在pos前插入
		{
			assert(pos < _finish&& pos >= _start);

			if (_finish == _endOfStorage)
			{
				size_t site = pos - _start;
				int newcapacity = capacity() == 0 ? 2 : 2 * (capacity());
				reserve(newcapacity);

				pos = _start + site;//pos到新空间的位置上
			}
			iterator end = _finish - 1;
			while (end >= pos)//开始整体向后退
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = x;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)//删pos处
		{
			assert(pos < _finish&& pos >= _start);
			assert(size() > 0);
			//开始向前移动
			iterator start = pos + 1;
			while (start < _finish)
			{
				*(start - 1) = *start;
				start++;
			}
			_finish--;
			return pos;//返回删除的位置
		}

使用test2函数看功能是否正常

void test2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);//尾插3个
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.pop_back();//尾删一个
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.insert(v.begin(), 0);//头插一个0
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		v.erase(v.begin());//头删
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

请添加图片描述


5.在实现Insert和erase时迭代器失效问题

当使用迭代器遍历容器时,如果在遍历的过程中对容器进行了结构性的修改(例如插入、删除元素,重新分配内存等操作),可能会导致迭代器失效。迭代器失效意味着该迭代器不再指向有效的元素或容器的结尾,因此继续使用失效的迭代器可能会导致未定义行为。

迭代器失效的原因主要有以下几种:

  1. 插入操作:当在容器中插入元素时,可能会导致容器内部的元素发生移动或重新分配内存,这会导致原先的迭代器失效。因为插入元素后,原先的迭代器可能不再指向正确的位置。
  2. 删除操作:当在容器中删除元素时,可能会导致容器内部的元素发生移动,也会导致原先的迭代器失效。因为删除元素后,原先的迭代器可能指向了一个已经被删除的元素,或者指向了不正确的位置。
  3. 重新分配内存(扩容时):某些容器在元素数量达到一定阈值时会进行内存的重新分配,这会导致原先的迭代器失效。因为重新分配内存后,原先的迭代器可能指向了无效的内存地址。
  4. 容器的清空:当对容器进行清空操作时,所有的元素都被移除,迭代器也会失效。

迭代器失效可以大致分为两类:

  1. 结构性变化导致的失效:这类失效包括扩容时申请了新空间、插入或删除元素导致元素位置改变等情况。在这种情况下,原先的迭代器可能会指向已经被移动或者删除的元素,或者指向了新分配的内存空间,导致迭代器失效。
  2. 数据变化导致的失效:这类失效包括使用了 memmovestd::copy 等函数对容器内部元素进行移动或复制的情况。这些函数可能会导致容器内部的元素发生移动,导致原先的迭代器指向的位置发生变化,从而导致迭代器失效。
	void test3()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
		//删除偶数
		vector<int>::iterator it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it=v.erase(it);//这里不能只是v.erase(it); 删除后
			}
			else
			{
				it++;
			}
		}
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

在使用 erase 函数删除元素后,erase 函数会返回指向被删除元素之后的元素的迭代器,而不是原先被删除元素的迭代器。如果使用 v.erase(it);,则会导致 it 迭代器失效,因为它指向的元素已经被删除,而 it 没有更新。因此,为了确保迭代器的有效性,需要将返回的迭代器赋值给 it,以便在下一次循环中继续使用正确的迭代器。


6.重载[]

		T& operator[](size_t i)
		{
			assert(i < size());

			return _start[i];
		}


		const T& operator[](size_t i) const
		{
			assert(i < size());

			return _start[i];
		}

7. 完善构造函数

7.1vector (size_type n, const value_type& val = value_type());

		vector(size_t n, const T& val= T())
		{
			resize(n, val);
		}

		vector(int n, const T& val = T())//适用于  vector<int> v(5,1)
		{
			resize(n, val);
		}

7.2利用迭代器进行构造

		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

为什么使用模版:

因为可能使用其他类型的迭代器来进行初始化

7.3拷贝构造

		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endOfStorage(nullptr)//先利用初始化列表进行初始化
		{
			reserve(v.capacity());
			for (const auto& e : v)
			{
				push_back(e);
			}
		}

8.重载=

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

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

注意这里的参数不是常量引用,而是按值传递的。这是因为在赋值操作符中我们会调用 swap 函数,按值传递可以保证传入的参数会被复制一份,避免对原对象的修改。在函数体内,我们调用了 swap 函数,将当前对象和传入的对象进行内容交换,然后返回 *this,即当前对象的引用。


9.析构函数

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

好啦,今天就到这里啦,感谢大家支持!!!

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

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

相关文章

Android---Jetpack Compose学习002

Compose 布局。Compose 布局的目标&#xff1a;1&#xff09;实现高性能&#xff1b;2&#xff09;让开发者能够轻松编写自定义布局&#xff1b;3&#xff09;在 Compose 中&#xff0c;通过避免多次测量布局子级可实现高性能。如果需要进行多次测量&#xff0c;Compose 具有一…

【MySQL】数据库的基础——数据库的介绍、MySQL的介绍和架构、SQL分类、MySQL的基本使用、MySQL的存储引擎

文章目录 MySQL1. 数据库的介绍1.2 主流数据库 2. MySQL的介绍2.1 MySQL架构2.2 SQL分类2.3 MySQL的基本使用2.4 MySQL存储引擎 MySQL 1. 数据库的介绍 数据库&#xff08;Database&#xff0c;简称DB&#xff09;是按照数据结构来组织、存储和管理数据的仓库。它是长期存储在计…

安装了多个Java版本如何指定特定版本

Java版本问题的实战场景 机器安装了一个JDK 8的安装版本&#xff0c;默认的安装路径是 C:\Program Files\Java&#xff0c;JDK的安装版本同时安装了JDK 和JRE, 安装的路径分别是&#xff1a; JDK 路径&#xff1a; C:\Program Files\Java\jdk1.8.0_361JRE 路径&#xff1a; C…

Java图形化界面编程——菜单组件 笔记

2.7 菜单组件 ​ 前面讲解了如果构建GUI界面&#xff0c;其实就是把一些GUI的组件&#xff0c;按照一定的布局放入到容器中展示就可以了。在实际开发中&#xff0c;除了主界面&#xff0c;还有一类比较重要的内容就是菜单相关组件&#xff0c;可以通过菜单相关组件很方便的使用…

在 Windows上恢复删除照片的 4 种有效方法

您是否曾在 Windows 7/8/10/11 中不小心删除过照片&#xff1f;如何轻松快速地恢复已删除的照片&#xff1f;在这里这篇文章列出了几种在Windows 11/10/8/7中恢复已删除照片的可行方法&#xff0c;而MiniTool数据恢复软件 是丢失照片恢复的最佳选择。 意外删除的照片 根据一项…

【深度学习每日小知识】卷积神经网络(CNN)

在深度学习领域&#xff0c;卷积神经网络&#xff08;CNN&#xff09;彻底改变了视觉分析领域。凭借从图像中提取复杂模式和特征的能力&#xff0c;CNN 已成为图像分类、目标检测和面部识别等任务不可或缺的一部分。本文全面概述了 CNN&#xff0c;探讨了其架构、训练过程、应用…

《CSS 简易速速上手小册》第5章:CSS 动画与过渡(2024 最新版)

文章目录 5.1 CSS 过渡基础&#xff1a;网页的微妙舞步5.1.1 基础知识5.1.2 重点案例&#xff1a;按钮悬停效果5.1.3 拓展案例 1&#xff1a;渐变显示导航菜单5.1.4 拓展案例 2&#xff1a;动态调整元素大小 5.2 关键帧动画&#xff1a;编排你的网页芭蕾5.2.1 基础知识5.2.2 重…

宠物空气净化器哪个牌子好?养猫家庭如何挑选宠物空气净化器?

养猫的朋友都知道&#xff0c;猫咪掉毛是一个令人头痛的问题。猫毛和皮屑会漂浮在空气中&#xff0c;不仅遍布全屋的各个角落&#xff0c;而且清理起来也非常麻烦&#xff0c;特别是那些难以清除的猫毛。更糟糕的是&#xff0c;这些猫毛还可能引发人们的过敏反应&#xff0c;如…

Netty源码系列 之 HashedWheelTimer源码

Netty优化方案 之前总结NioEventLoop以及其他内容时&#xff0c;已经总结了Netty许多优化的设计方案。 1.Selector的优化 (1) 为epoll空转问题提供了解决思路&#xff0c;虽然并没有从根本上解决epoll空转问题&#xff0c;但是使用一个计数器的方式可以减少空转所带来的性能…

[word] word如何打印背景和图片? #微信#其他#经验分享

word如何打印背景和图片&#xff1f; 日常办公中会经常要打印文件的&#xff0c;其实在文档的打印中也是有很多技巧的&#xff0c;可以按照自己的需求设定&#xff0c;下面给大家分享word如何打印背景和图片&#xff0c;一起来看看吧&#xff01; 1、打印背景和图片 在默认的…

【数据结构与算法】【小白也能学的数据结构与算法】迭代算法专题

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《数据结构与算法&#xff1a;初学者入门指南》&#x1f4d8;&am…

面向数据报编程-UDP协议

目录 前言&#xff1a; 1.UDP协议API 1.1UDP编程原理 1.2DatagramSocket类 &#xff08;1&#xff09;DatagramSocket构造方法 &#xff08;2&#xff09;DatagramSocket普通方法 1.3DatagramPacket类 &#xff08;1&#xff09;DatagramPacket构造方法 &#xff08;2…

电商小程序03登录页面开发

目录 1 创建应用2 创建页面3 首页功能搭建4 登录页搭建5 设置叠加效果总结 小程序开发在经过需求分析和数据源设计之后&#xff0c;就可以进入到页面开发的阶段了。首先我们需要开发登录的功能。 登录功能要求用户输入用户名和密码&#xff0c;勾选同意用户协议和隐私协议&…

SAP-PS-02-004利润中心移除分配公司代码

问题描述 在用KE51创建利润中心时&#xff0c;如果不采用“参考创建”的方式&#xff0c;SAP默认将所有公司代码分配给此利润中心。 上图中&#xff0c;L100003利润中心分配了1000、1001、1002公司&#xff0c;但是实际需求是&#xff1a;L100003只需要分配1000公司。 解决方法…

elasticsearch增删改查

一、数据类型 1、字符串类型 &#xff08;1&#xff09;text &#xff08;2&#xff09;keyword 2、数值类型 &#xff08;1&#xff09;long、integer、short、byte、float、double 3、日期类型 &#xff08;1&#xff09;date 4、布尔类型 &#xff08;1&#xff0…

ChatGPT高效提问—prompt常见用法(续篇九)

ChatGPT高效提问—prompt常见用法(续篇九) ​ 如何准确地向大型语言模型提出问题,使其更好地理解我们的意图,从而得到期望的答案呢?编写有效的prompt的技巧,精心设计的prompt,获得期望的的答案。 1.1 增加条件 ​ 在各种prompt技巧中,增加条件是最常用的。在prompt中…

基于JavaWeb的网上订餐项目

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88825723?spm1001.2014.3001.5503 Java项目-16 浏览商品&#xff0c;会员登录&#xff0c;添加购物车&#xff0c;进行配送等功能 文件代码功能介绍 1.Src下的java文件存放的我们后端的…

基于PHP网上图书销售商城系统qo85w

软件体系结构方案&#xff1a;由于本系统需要在不同设备上都能运行&#xff0c;而且电脑配置要求也要越低越好&#xff0c;为了实现这一要求&#xff0c;经过考虑B/S结构成为最佳之选。使用B/S结构的系统可以几乎在任何电脑上运行&#xff0c;只要浏览器可以正常工作就可以正常…

C++ //练习 5.12 修改统计元音字母的程序,使其也能统计空格、制表符和换行符的数量。

C Primer&#xff08;第5版&#xff09; 练习 5.12 练习 5.12 修改统计元音字母的程序&#xff0c;使其也能统计空格、制表符和换行符的数量。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /******************************…

【JavaScript 】finally() 方法和Filter() 方法

JavaScript 中的finally() 方法 finally是 JavaScript 构造中使用的方法try-catch。try它在and阻塞之后执行catch&#xff0c;无论 Promise 是已履行还是已拒绝。该函数的主要作用是执行必要的清理任务并向用户传达消息。一个常见的用例可能是通知用户“您的请求已被处理”&am…