C++【STL容器系列(二)】vector的模拟实现

news2024/12/24 0:34:47

文章目录

  • 1. vector的结构
  • 2. vector的默认成员函数
    • 2.1构造函数
      • 2.1.1 默认构造
      • 2.1.2 迭代器构造
      • 2.1.3 用n个val初始化构造
    • 2.2 拷贝构造
    • 2.3 析构函数
    • 2.4 operator=
  • 3. vector iterator函数
    • 3.1 begin 和 cbegin函数
    • 3.2 end() 和 cend()函数
  • 4. vector的小函数
    • 4.1 size函数
    • 4.2 capacity函数
    • 4.3 swap函数
  • 5. vector的访问函数
    • 5.1 operator[]
    • 5.2 front
    • 5.2 back
  • 6 vector的增删改
    • 6.1 reserve
    • 6.2 push_back(尾插)
    • 6.3 pop_back(尾删)
    • 6.4 insert(在任意位置插入数据)
    • 6.5 erase(删除数据)
    • 6.6 clear(清除数据)
    • 6.7 empty(判空)
  • 结语

1. vector的结构

我们通过查看vector的底层,能发现结构并不是我们所想的数组,_size,_capacity
在这里插入图片描述
而是由三个指针所组成的,这三个指针分别为;start:开始元素finish:末尾元素的后一个位置end_of_storage:容量(iterator是在前面typedef过的,原型为 T*)

我们前面讲结构,vector是通过模板来实现的,但模板并不能分离成两个文件,所以本次模拟只会有"vector.h"这个头文件。

由于我们是模拟实现,那么底层结构也要是三个指针

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

2. vector的默认成员函数

2.1构造函数

		vector(); //默认构造
		template <class InputIterator>
		vector(InputIterator first, InputIterator last) //迭代器构造
		vector(size_t n, const T& x = T()) //用n个val初始化构造

2.1.1 默认构造

其实默认构造我们并不需要写,因为我们在声明成员变量的时候就已经给了缺省值nullptr,所以写不写都无所谓,用编译器生成的默认构造就可以了,但是如果你写了一个构造函数,那么编译器就不会生成默认构造,这时候就可以使用C++11引入的default,来让编译器生成默认构造。

vector() = default; //让编译器生成默认构造

2.1.2 迭代器构造

这里其实并不复杂,push_back迭代器所代表的内容就好了。

但问题是:为什么要在多写一个模板出来呢?

答案也很简单,因为外面的模板和这个模板推导的类型不一样,外面是推导vector<T>中的T,这个是推导整个vector<T>

拿vector<int>举例,外面模板类型是 int,里面模板类型是vector<int>

		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

2.1.3 用n个val初始化构造

这个也很简单,用一个循环来push_back(val)就好了

		vector(size_t n, const T& x = T())
		{
			reserve(n); 
			for (size_t i = 0; i < n; ++i)
			{
				push_back(x);
			}
		}
  • reserve: 用于提前开好空间,这样就不用反复扩容了

但是这样写会有问题,我使用其他类型不会报错,但是使用vector<int>(n,val)的时候就会报错!!!
在这里插入图片描述
竟然调用了迭代器初始化的构造函数,这是为什么呢,原因就在模板这里。

调用函数的机制是只要类型匹配,怎么简单怎么来,由于用n个val初始化是模板参数(const T& x = T()),迭代器构造也是模板参数,但是用n个val初始化还有一个 size_t 的参数,那么模板推导后有需要类型转换,势必会麻烦一点,而迭代器是两个模板参数,推导完可以直接使用,所以编译器在调用函数的时候,自然而然就会调用迭代器版本的构造。

解决方法也很简单,就是自己再写一个 int 版本的构造(其实库里也是这样实现的)
在这里插入图片描述

所以我们实现的时候,要多实现一个整形版本的构造(这里只实现了int版本的)

		vector(size_t n, const T& x = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(x);
			}
		}

		vector(int n, const T& x = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(x);
			}
		}

2.2 拷贝构造

拷贝构造其实也很简单,不需要局限于传统写法和现代写法,使用范围for就能完美解决拷贝构造。

vector(const vector& v)
{
	reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

2.3 析构函数

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

2.4 operator=

在完成operator=的时候,现代写法是目前最好的写法;传参的时候不传引用,而是传值传参(这样会进行拷贝构造,而拷贝构造就完成了形参的深拷贝),然后再交换形参。

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

3. vector iterator函数

还是和string一样,只实现iteratorconst_iterator这两个版本

		typedef T* iterator;
		typedef const T* const_iterator;

3.1 begin 和 cbegin函数

		iterator begin()
		{
			return _start;
		}

		const_iterator begin() const
		{
			return _start;
		}

3.2 end() 和 cend()函数

		iterator end()
		{
			return _finish;
		}

		const_iterator end() const
		{
			return _finish;
		}

4. vector的小函数

4.1 size函数

左闭右开,一减就是元素个数

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

4.2 capacity函数

只是被减数的不同,返回容量数

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

4.3 swap函数

调用库里面的swap函数,避免进行深拷贝。

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

5. vector的访问函数

5.1 operator[]

直接返回该位置元素的引用

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

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

5.2 front

返回第一个元素的引用

		T& front()
		{
			return *_start;
		}

		const T& front() const
		{
			return *_start;
		}

5.2 back

返回最后一个元素的引用

		T& back() 
		{
			return *(_finish - 1);
		}

		const T& back() const
		{
			return *(_finish - 1);
		}

6 vector的增删改

6.1 reserve

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		
		memcpy(tmp, _start, size() * sizeof(T));
		delete[] _start;

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

注意:我们需要在delete[] _start之前存储原本的数据个数,不然会出问题。
如果我们delete在刷新_finish,那么就会用到size()这个函数,但size这个函数是_start - _finish,由于_start这整个空间已经被释放了(_star、_finish、_end_of_storage是一块空间的不同位置),所以我们需要在释放_start之前先存储原来的数据个数size_t old_size = size();,再用old_size来刷新_finish。

但是这会有一个问题,如果我的vector是自定义类型且内部自己有申请空间,这时候扩容就会出问题,因为memcpy是浅拷贝,也就是一个字节一个字节的拷贝,这样虽然tmp是new出来的新空间,但是经过memcpy,_start 和 tmp就会指向同一个空间,delete[] _start,就相当于把tmp也delete了,这时候在尾插,析构的时候程序就会崩溃掉。
在这里插入图片描述
解决方法就是调用operator=,这样自定义类型就会调用自己的operator=。

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				//浅拷贝 -> 遇到有申请空间的对象出问题
				//memcpy(tmp, _start, size() * sizeof(T));

				//深拷贝 调用他们自己的 operator=
				for (size_t i = 0; i < size(); i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;

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

6.2 push_back(尾插)

		void push_back(const T& val)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			} 
			*_finish = val;
			++_finish;
		}

6.3 pop_back(尾删)

void pop_back()
{
	assert(!empty());
	_finish -= 1;
}

6.4 insert(在任意位置插入数据)

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

	if (_finish == _end_of_storage)
	{
		
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	
	iterator end = _finish - 1;
	while (pos <= end)
	{
		*(end + 1) = *end;
		--end;
	}
	*pos = val;
	++_finish;

	return pos;
}

这里和扩容后的size同理,括完容后要更新pos,要提前记录pos与_start之间的距离。
使用完insert会造成迭代器失效,解决方法:使用完这个函数后,认为这个迭代器已经失效,不要使用

6.5 erase(删除数据)

iterator erase(iterator pos)
{
	assert(pos <= end());
	if (pos == end())
	{
		pop_back();
	}

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

用后面的数据覆盖前面就好了。
这个函数同样会造成迭代器失效,解决方法和 insert 一样。

6.6 clear(清除数据)

void clear()
{
	_finish = _start;
}

6.7 empty(判空)

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

结语

那么这次的分享就到这里结束了~
最后感谢您能阅读完此片文章~
如果您认为这篇文章对你有帮助的话,可以用你们的手点一个免费的赞并收藏起来哟~
如果有任何建议或纠正欢迎在评论区留言~
也可以前往我的主页看更多好文哦(点击此处跳转到主页)。

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

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

相关文章

【linux】网络基础 ---- 应用层

1. 再谈 "协议" 协议是一种 "约定"&#xff0c;在读写数据时, 都是按 "字符串" 的方式来发送接收的. 但是这里我们会遇到一些问题&#xff1a; 如何确保从网上读取的数据是否是完整的&#xff0c;区分缓冲区中的由不同客户端发来的数据 2. 网…

C语言PythonBash:空白(空格、水平制表符、换行符)与转义字符

C语言 空白 C语言中的空白&#xff08;空格、水平制表符、换行符&#xff09;被用于分隔Token&#xff0c;因此Token间可以有任意多个空白。 // 例1 printf("Hello, World!"); 例1中存在5个Token&#xff0c;分别是&#xff1a; printf("Hello, World! \n&qu…

Linux基础(十四)——BASH

BASH 1.BASH定义2.shell的种类3.bash的功能3.1 命令记录功能3.2 命令补全功能3.3 命令别名设置3.4 工作控制、 前景背景控制3.5 程序化脚本&#xff1a; &#xff08; shell scripts&#xff09;3.6 万用字符 4.bash的内置命令5.shell的变量功能5.1 变量的取用5.2 新建变量5.3 …

【重学 MySQL】八十二、深入探索 CASE 语句的应用

【重学 MySQL】八十二、深入探索 CASE 语句的应用 CASE语句的两种形式CASE语句的应用场景数据分类动态排序条件计算在 SELECT 子句中使用在 WHERE子句中使用在 ORDER BY 子句中使用 注意事项 在MySQL中&#xff0c;CASE 语句提供了一种强大的方式来实现条件分支逻辑&#xff0c…

由播客转向个人定制的音频频道(1)平台搭建

项目的背景 最近开始听喜马拉雅播客的内容&#xff0c;但是发现许多不方便的地方。 休息的时候收听喜马拉雅&#xff0c;但是还需要不断地选择喜马拉雅的内容&#xff0c;比较麻烦&#xff0c;而且黑灯操作反而伤眼睛。 喜马拉雅为代表的播客平台都是VOD 形式的&#xff0…

7+纯生信,单细胞识别细胞marker+100种机器学习组合建模,机器学习组合建模取代单独lasso回归势在必行!

影响因子&#xff1a;7.3 研究概述&#xff1a; 皮肤黑色素瘤&#xff08;SKCM&#xff09;是所有皮肤恶性肿瘤中最具侵袭性的类型。本研究从GEO数据库下载单细胞RNA测序&#xff08;scRNA-seq&#xff09;数据集&#xff0c;根据原始研究中定义的细胞标记重新注释各种免疫细胞…

uniapp解析蓝牙设备响应数据bug

本文章为了解决《uniapp 与蓝牙设备收发指令详细步骤(完整项目版)》中第十步的Array 解析成 number函数bug 1、原代码说明 function array16_to_number(arrayValue) {const newArray arrayValue.filter(item > String(item) ! 00 || String(item) ! 0)const _number16 ne…

【递归回溯与搜索算法篇】算法的镜花水月:在无尽的自我倒影中,递归步步生花

文章目录 递归回溯搜索专题&#xff08;一&#xff09;&#xff1a;递归前言第一章&#xff1a;递归基础及应用1.1 汉诺塔问题&#xff08;easy&#xff09;解法&#xff08;递归&#xff09;C 代码实现时间复杂度和空间复杂度易错点提示 1.2 合并两个有序链表&#xff08;easy…

大数据开发面试宝典

312个问题&#xff0c;问题涵盖广、从自我介绍到大厂实战、19大主题&#xff0c;一网打尽、真正提高面试成功率 一、Linux 1. 说⼀下linux的常⽤命令&#xff1f; 说一些高级命令即可 systemctl 设置系统参数 如&#xff1a;systemctl stop firewalld关闭防火墙 tail / hea…

链表归并与并集相关算法题|两递增归并为递减到原位|b表归并到a表|两递减归并到新链表(C)

两递增归并为递减到原位 假设有两个按元素递增次序排列的线性表&#xff0c;均以单链表形式存储。将这两个单链表归并为一个按元素递减次序排列的单链表&#xff0c;并要求利用原来两个单链表的节点存放归并后的单链表 算法思想 因为两链表已按元素值递增次序排列&#xff0…

【RabbitMQ】06-消费者的可靠性

1. 消费者确认机制 没有ack&#xff0c;mq就会一直保留消息。 spring:rabbitmq:listener:simple:acknowledge-mode: auto # 自动ack2. 失败重试机制 当消费者出现异常后&#xff0c;消息会不断requeue&#xff08;重入队&#xff09;到队列&#xff0c;再重新发送给消费者。…

【陕西】《陕西省省级政务信息化项目投资编制指南(建设类)(试行)》-省市费用标准解读系列07

《陕西省省级政务信息化项目投资编制指南&#xff08;建设类&#xff09;&#xff08;试行&#xff09;》规定了建设类项目的费用投资测算方法与计价标准&#xff0c;明确指出建设类项目费用包括项目建设费和项目建设其他费&#xff08;了解更多可直接关注咨询我们&#xff09;…

VB6.0桌面小程序(桌面音乐播放器)

干货源码 Imports System.IO Imports System.Security.Cryptography Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load Button1.Text “上一曲” Button4.Text “播放” Button3.Text “下一曲” Button2.Text “顺序播…

docker安装jdk8

1、拉取镜像 docker pull openjdk:82、运行镜像 docker run -d --restartalways --network portainer_network -it --name jdk8 openjdk:8命令 作用 docker run 创建并启动一个容器 –name jdk8 将容器取名为jdk8 -d 设置后台运行 –restartalways 随容器启动 –network port…

【人工智能】Transformers之Pipeline(二十三):文档视觉问答(document-question-answering)

​​​​​​​ 目录 一、引言 二、文档问答&#xff08;document-question-answering&#xff09; 2.1 概述 2.2 impira/layoutlm-document-qa 2.2.1 LayoutLM v1 2.2.2 LayoutLM v2 2.2.3 LayoutXLM 2.2.4 LayoutLM v3 2.3 pipeline参数 2.3.1 pipeline对象实例化…

微服务day06

MQ入门 同步处理业务&#xff1a; 异步处理&#xff1a; 将任务处理后交给MQ来进行分发处理。 MQ的相关知识 同步调用 同步调用的小结 异步调用 MQ技术选型 RabbitMQ 安装部署 其中包含几个概念&#xff1a; publisher&#xff1a;生产者&#xff0c;也就是发送消息的一方 …

[CKS] K8S RuntimeClass SetUp

最近准备花一周的时间准备CKS考试&#xff0c;在准备考试中发现有一个题目关于RuntimeClass创建和挂载的题目。 ​ 专栏其他文章: [CKS] Create/Read/Mount a Secret in K8S-CSDN博客[CKS] Audit Log Policy-CSDN博客 -[CKS] 利用falco进行容器日志捕捉和安全监控-CSDN博客[CKS…

Halcon基于laws纹理特征的SVM分类

与基于区域特征的 SVM 分类不同&#xff0c;针对图像特征的 SVM 分类的算子不需要直接提取 特征&#xff0c;下面介绍基于 Laws 纹理特征的 SVM 分类。 纹理在计算机视觉领域的图像分割、模式识别等方面都有着重要的意义和广泛的应 用。纹理是指由于物体表面的物理属性不同所…

初始Python篇(6)—— 字符串

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 字符串的常见操作 格式化字符串 占位符 f-string 字符串的 format 方法 字符串的编码与解码 与数据验证相关的方法 …

基于Spring Boot+Vue的养老院管理系统【原创】

一.系统开发工具与环境搭建 1.系统设计开发工具 后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk17 前端&#xff1a; 技术&#xff1a;框架Vue.js&#xff1b;UI库&#xff1a;ElementUI&#xff1b; 开发工具&…