【C++】STL | vector 详解及重要函数的实现

news2024/11/26 20:45:56

目录

前言

总代码

vector类框架建立(模板与成员变量)

构造、析构、swap 与 赋值重载

构造

析构

swap

赋值重载

reserve 扩容(重要!!)、size、capacity

operator[ ]重载

insert 插入

逻辑讲解

insert中的迭代器失效

erase 删除

删除主逻辑

迭代器失效

push_back、pop_back

begin 与 end

拷贝构造、n个值构造

一段迭代器区间构造

initializer_list

结语


前言

vector在STL中属于较为简单的知识点,学习难度相对较低,但是相比于string,本篇博客的底层实现会添加一些很有意思且很实用的东西,比如:initializer_list、一段迭代器区间初始化等等

如果有对库中的vector有了解需求的话,可以看一下C++中较为官方的网站,这里面有详细的讲解和用法介绍,网址如下:

https://legacy.cplusplus.com/reference/vector/vector/?kw=vector

总代码

如果有友友只是复习需要,只想看完整代码的话,可以直接点下面的gitee链接

当然对于看完了整篇文章的友友也可以照着这里的代码敲一篇进一步理解喔...(* ̄0 ̄)ノ

gitee - vector - blog - 2024-08-06

vector类框架建立(模板与成员变量)

首先我们可以将我们的类包在一个命名空间里面这样就不会在写完之后因为名字与库中的相同而报错

接着,我们可以写一个类模板,我们vector的本质就是数组,而我们的数组里面可以存任何类型,可以存一个int,可以存char,甚至可以存一个vector<int>类型实现二维数组

所以我们需要写一个模板T,表示类型

接着就是成员变量,这个地方有多种选择,我们可以和上一篇的string一样使用一个指针,一个size一个capacity的策略

但由于源码中的成员是三个迭代器,一个指向开始位置,一个指向有效数据的尾(有效数据个数),一个指向整个空间的尾(总的空间大小),虽然都可以,但我们还是尽量与源码保持一致

由于条件的需要,我们先来谈谈vector中的迭代器是什么:

由于我们的vector是一块连续开辟的空间,所以我们的迭代器和string的一样,都是原生指针,并不需要单独写一个类来实现迭代器的功能,如下:

//迭代器
typedef T* iterator;
typedef const T* const_iterator;

成员如下:

namespace hjx
{
	template<class T>
	class vector
	{
    // 此处的迭代器就是T*

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}
		

构造、析构、swap 与 赋值重载

构造

由于我们的三个成员本质上都是iterator,也就是原生指针,所以我们并不需要专门写一个构造函数来初始化这三个指针

至于一段迭代器区间初始化,initializer_list初始化,n个值的初始化,拷贝构造

这些需要我们后面讲完了插入之后才能进行讲解,因为其主逻辑都是一个一个插入

这里既然不需要显示写,但又需要生成,不然就没法写拷贝构造

所以我们可以使用下面的写法:

//编译器强制生成构造函数
vector() = default;

析构

这里的析构主要就干两件事:

  1. 将空间销毁
  2. 将指针全部置为空指针

当然这一切都得在空间存在的情况下才需要执行,所以我们还可以添加一个判断条件

代码如下:

//析构
~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

swap

由于vector的交换函数只需要交换三个指针,库中的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);
}

赋值重载

这里我们可以使用”现代写法“:

我们在接收参数的时候,如果不加引用,那么默认接收的是这个参数的拷贝

既然如此,我们vector的拷贝也有空间,我们就只需要交换一下两边的指针,将参数的空间交换过来,销毁的时候调用不到这块空间,就不会销毁

有个词很符合这中做法:”剥削“(bushi

代码如下:

//赋值重载
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

reserve 扩容(重要!!)、size、capacity

在该函数中,我们的参数是一个 size_t 的类型,意味要将空间扩容为多大,也就是将扩容后的总空间大小传过来了

而我们实现扩容时,有一个需要注意的点,就是迭代器失效

因为我们C++的扩容需要自己开空间,拷贝数据,销毁久空间,所以有可能出现原来的指针还指向已销毁空间但我们还需要用的情况,这是迭代器失效的其中一种情况

而我们后面要重新另指针指向新空间时,需要根据每个指针之间的距离作为考量来初始化,如下:

_start = tmp;
_finish = _start + len1;
_end_of_storage = _start + len2;

但如果 len1   len2 就是通过指针本身计算的呢?

假设 len1 = _finish - _start,但是此时_finish 还指向旧空间,这么计算就错了

所以我们需要提前存储一份oldsize

而扩容的主逻辑无非就是

  1. 开新空间
  2. 拷贝数据
  3. 销毁旧空间

但是第二第三步是要在旧空间存在的情况下才执行的,如果旧空间本身就为空,那么我们自然不需要拷贝数据与销毁

代码如下:

void reserve(size_t n)
{
    如果原空间不够了才需要扩容
	if (n > capacity())
	{
		//提前记录,后面会出现迭代器失效的情况
		size_t oldsize = size();
		T* tmp = new T[n];

        //如果旧空间本身就为空,则不需要拷贝数据与销毁
		if (_start)
		{
			for (size_t i = 0; i < oldsize; i++)
			{
				tmp[i] = _start[i];
			}
			delete[]_start;
		}

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

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

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

如上我们还添加了size和capacity函数的实现,两个的主逻辑就是指针的相减

operator[ ]重载

我们执行operator[ ]时想要的效果就是返回那个位置的值,所以我们返回就是了

代码如下:

//opertaor[]
T& operator[](size_t i)
{
	return _start[i];
}

const T& operator[](size_t i)const
{
	return _start[i];
}

insert 插入

逻辑讲解

我们能看到,库中的第一个参数都是迭代器类型的,我们自己实现的版本也需要为位置为迭代器的类型(假设该参数名为pos

至于第二个参数自然是传入的要插入的参数了

插入的大逻辑是:

  1. 先判断是否需要扩容,如果需要就将扩容后的总大小作为参数传给reserve
  2. 将pos位置后的所有数据全部往后移一位,将数据插入
  3. 处理成员指针

前两点和我们上一篇讲解string的处理方法极为相似,代码如下:

//判断扩容
if (_finish == _end_of_storage)
{
	size_t len = pos - _start;

	size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
	reserve(newcapacity);

	pos = _start + len;
}
//插入数据
iterator end = _finish;
while (end > pos)
{
	*end = *(end - 1);
	--end;
}

扩容时需要注意的点就是:扩完容之后我们的空间就是新空间了,但是我们的pos还指向原来的空间,所以我们需要提前将数据存储一下,扩容完成后再更新pos

而我们的所有数据向后移,直接使用一个while循环即可,就像我们使用迭代器遍历的那样

insert中的迭代器失效

由于我们的插入需要扩容,如果我们在外面使用迭代器访问的话,就有可能出现迭代器还指向原空间的情况,进而导致随机值的出现,如下:

auto it = v1.begin();
v1.insert(v1.begin()+2, 1000);

如果我们的insert此时插入之后刚好扩容了,那么我们的迭代器 it 还指向原空间,如果再去使用的话,就是越界访问了,也就是迭代器失效的一种情况

为了解决这种情况,C++源码中的解决方案是:将insert后的地址返回,然后我们在外面使用的时候再进一步的赋值即可,如下:

auto it = v1.begin();
it = v1.insert(v1.begin()+2, 1000);

综上,我们的insert需要一个iterator类型的返回值

这才是参数需要iterator类型的意义

总代码如下:

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

	//判断扩容
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;

		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newcapacity);

		pos = _start + len;
	}
	//插入数据
	iterator end = _finish;
	while (end > pos)
	{
		*end = *(end - 1);
		--end;
	}

    //插入数据
	*pos = x;
	_finish++;

	return pos;
}

erase 删除

删除主逻辑

删除其实就只干两件事:

  1. 移动数据向前覆盖一格
  2. --_finish(_finish指向的是有效数据的尾部,减减之后就不会访问到最后一个位置,后面要再插入也会将其覆盖)

到这里,大体上和insert也差不多,接下来我们就来详细讲讲erase的迭代器失效

迭代器失效

我们来看,如果我们要删除的是最后一个位置,那么删除了之后,指针(也就是迭代器)没有更新,就会造成越界访问

另外在有一些平台下面,当数据减少到一个程度之后,会执行缩容操作,此时我们的迭代器也会指向错误的位置造成越界访问

所以这么看,erase也是会造成迭代器失效的,而处理的方法和insert一样,将删除后位置的迭代器返回即可

综上,代码如下:

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

	iterator it = pos + 1;
	while (it != _finish)
	{
		*(it - 1) = *it;
		it++;
	}
	--_finish;

	return pos;
}

push_back、pop_back

上文中我们已经实现了insert了,所以push_back也可以复用insert,因为其本质就是在尾部插入一个数据

代码如下:

void push_back(const T& x)
{
	insert(end(), x);
}

而我们的pop_back相对简单,倒也不需要复用erase,只需要将Z_finish--即可,还是相当简单的

代码如下:

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

begin 与 end

//迭代器
typedef T* iterator;
typedef const T* const_iterator;

iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin()const
{
	return _start;
}

const_iterator end()const
{
	return _finish;
}

拷贝构造、n个值构造

我们的拷贝构造本质上也是构造一个类,只不过是拷贝了另一个vector而已

而无论是什么写法,即使是现代写法在参数那里编译器也会复制一遍,其他写法也是一个一个放数据,本质上效率都一样

所以我们这里的写法就是:直接使用范围for遍历另一个vector,再将值一个一个push_back即可

但是在此之前,我们还需要先reserve出一块空间,虽然说push_back内部也有扩容逻辑,但是我们提前开好倒也算是减去了一部分消耗

代码如下:

//拷贝构造
vector(vector<T>& v)
{
	reserve(v.capacity());
	for (auto e : v)
	{
		push_back(e);
	}
}

而n个值初始化也是同理,我们只需要写一个for循环循环n次,再执行push_back的逻辑即可

代码如下:

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

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

这里之所以写两个版本(一个size_t, 一个int),是因为后面我们在实现一段迭代器区间构造的时候会用到函数模板,如果我们n个值构造传的是一个int,一个其他类型的倒还好

但如果我们传两个int的话会匹配到迭代器区间初始化那里去

所以我们在写size_t版本的同时再写一个int的版本,为的就是应对这种情况

一段迭代器区间构造

//一段迭代器区间初始化
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

我们之所以要搞一个模板,为的就是无论什么结构的数据,都能初始化到vector这来,如果我们单纯给T的话,就只有vector的迭代器区间可以放进来初始化

但如果我们写了模板,那么无论是链表的,栈的,红黑树的等等,都可以放数据进来给vector初始化

而内部逻辑也与上述的拷贝构造、n个值构造一样,用的主要都是push_back的逻辑,这里就不过多讲解了(因为就是将迭代器指向的值一个一个尾插进来,再将迭代器往下移动一个而已)

initializer_list

我们通过查看文档可以发现,initializer_list本质上就是一个类,类里面有几个函数,仅此而已

所以我们可以直接将initializer_list中的值拿来push_back,由于里面有begin和end函数,这也就意味着我们可以使用范围for对initializer_list进行遍历(〃 ̄︶ ̄)人( ̄︶ ̄〃)

代码如下:

vector(initializer_list<T> il)
{
	reserve(il.size());
	for (auto e : il)
	{
		push_back(e);
	}
}

结语

本篇文章关于vector的讲解到这里就结束啦(~ ̄▽ ̄)~

如果觉得对你有帮助的话,希望能多多关注喔(○` 3′○)

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

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

相关文章

手撸高性能日志系统(一):百万日志,秒秒落盘(小试牛刀篇)

一、需求一丢&#xff0c;谁累成狗 最近由于某些需要&#xff0c;计划手撸一个高性能的日志系统。需求很简单&#xff1a; 1、 不允许丢一条日志信息&#xff08;很重要很重要&#xff09; 2、支持多线程&#xff0c;必须线程安全 3、性能要越优越好&#xff0c;尽量百万可秒级…

【逗老师的无线电】QRZ快速得到Incoming请求的准确QSO时间

各位友台&#xff0c;有没有遇到过别人从QRZ发过来了Incoming的QSO请求&#xff0c;但是我完全不记得QSO的时间和波段&#xff0c;盲猜要猜好久。尤其是下面这种&#xff0c;8月份发来的6月份的通联记录&#xff0c;这我天天FT8&#xff0c;上哪翻当天的记录啊&#xff08;大概…

第6章>>实验6:PS(ARM)端Linux RT与PL端FPGA之间(通过Reg寄存器进行通信和交互)-《LabVIEW ZYNQ FPGA宝典》

1、实验内容 前面第五章入门实验和上一个实验5里面我们向大家展示通过了布尔类型的Reg寄存器通道实现了ZYNQ PS端ARM和PL端FPGA二者之间的开关量交互&#xff0c;抛砖引玉。 从本节实验开始&#xff0c;接下来4个实验我们将着重向大家讲解更为通用和更为全面的4种交互方式&…

研0 冲刺算法竞赛 day27 P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G

P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 考点&#xff1a;哈夫曼树 思路&#xff1a;建优先队列&#xff0c;自动排序&#xff0c;然后每次取出最小两个即可。本来思路是数组的&#xff0c;但是一直写…

这才是你需要的C语言、C++学习路线!

大家好&#xff0c;我已经整理好了关于学习 C 语言和 C 的路径图。 接下来&#xff0c;让我们先聊一些有趣且常见的话题。 这些问题是我经常在私信中收到的&#xff0c;同时也是我在学习过程中曾经感到困惑的地方。 粉丝福利&#xff0c; 免费领取C/C 开发学习资料包、技术视…

Jenkins保姆笔记(2)——基于Java8的Jenkins插件安装

前面我们介绍过&#xff1a; Jenkins保姆笔记&#xff08;1&#xff09;——基于Java8的Jenkins安装部署 本篇主要介绍下基于Java8的Jenkins插件安装。为什么要单独讲一个插件安装&#xff1f;因为一些原因&#xff0c;Jenkins自带的插件源下载几乎都会失败&#xff0c;如图…

小怡分享之Java的String类

前言&#xff1a; &#x1f308;✨之前小怡给大家分享了图书管理系统这个项目&#xff0c;今天小怡给大家分享Java的String类。 1.String类的重要性 String是字符串类型&#xff0c;C语言中没有字符串类型。 Java当中没有说字符串的结尾是 \0这样的说法。C语言中要表示字符串只…

【爬虫实战】利用代理爬取Temu电商数据

引言 在行业竞争激烈、市场变化快速的跨境电商领域&#xff0c;数据采集可以帮助企业深入了解客户需求和行为&#xff0c;分析市场趋势和竞争情况&#xff0c;从而优化产品和服务&#xff0c;提高客户满意度和忠诚度。同时&#xff0c;数据采集可以实时跟踪库存水平和销售情况&…

Windows10上安装SQL Server 2022 Express

Microsoft SQL Server 2022 Express是一个功能强大且可靠的免费数据管理系统&#xff0c;可为轻量级网站和桌面应用程序提供丰富可靠的数据存储&#xff0c;为关系数据库&#xff1a; (1).LocalDB(SqlLocalDB)&#xff1a;是Express的一种轻型版本&#xff0c;该版本具备所有可…

常见中间件漏洞复现之【Tomcat】!

Tomcat介绍 tomcat是⼀个开源⽽且免费的jsp服务器&#xff0c;默认端⼝ : 8080&#xff0c;属于轻量级应⽤服务器。它可以实现 JavaWeb程序的装载&#xff0c;是配置JSP&#xff08;Java Server Page&#xff09;和JAVA系统必备的⼀款环境。 在历史上也披露出来了很多的漏洞 …

Python爬虫实战:利用代理IP爬取百度翻译

文章目录 一、爬取目标二、环境准备三、代理IP获取3.1 爬虫和代理IP的关系3.2 巨量IP介绍3.3 超值企业极速池推荐3.4 IP领取3.5 代码获取IP 四、爬虫代码实战4.1分析网页4.2 寻找接口4.3 参数构建4.4 完整代码 一、爬取目标 本次目标网站&#xff1a;百度翻译&#xff08;http…

Java I/O (Input/Output)——文件字节流

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;Java SE 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Java I/O 简介 Java I/O&#xff08;输入/输出&#xff09;是 Java 程序中…

抖音ip地址怎么换到别的地方

在数字化时代&#xff0c;抖音作为一款风靡全球的短视频社交平台&#xff0c;让我们的生活充满了无限乐趣与创意。然而&#xff0c;有时我们可能希望自己的抖音能够显示一个不同于当前所在地的IP地址&#xff0c;无论是出于隐私保护、还是其他个性化需求。那么&#xff0c;如何…

Linux学习记录(三)-----文件io和标准io的区别

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言文件IO和标准IO的区别1.\r和\n的区别2.缓冲2.1缓冲区的概念2.2.缓冲区的分类 3.文件IO和标准IO的区别 前言 文件IO和标准IO的区别 1.\r和\n的区别 \r 回车操作…

无人机之植保机篇

一、什么是植保无人机 植保无人机是用于农林植物保护作业的无人驾驶飞机&#xff0c;该型无人飞机由飞行平台、导航飞控、喷洒机构三部分组成&#xff0c;通过地面遥控器或导航飞控&#xff0c;来实现喷洒作业&#xff0c;可以喷洒药剂、种子、粉剂等。目前国内销售的植保无人机…

【已解决】VSCode连接Linux云服务器,代码写着写着服务器突然挂了是怎么回事?

文章目录 1. 问题描述2. 问题原因3. 解决方法 1. 问题描述 在使用 VSCode 连接远程 Ubuntu 云服务器写代码的时候&#xff0c;感觉越写越卡&#xff0c;代码提示半天出不来&#xff0c;最后更是直接断开连接了&#xff1a; 即使把 VSCode 关了&#xff0c;再重启也没用&#x…

五种IO模型与阻塞IO

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 IO本质 我们常说IO就是input&#xff0c;output&#xff0c;也就是输入和输出&#xff0c;但是&#xff0c;他的本质是什么&#xff1f;站在OS角度&#xff0c;站在进程的角度&#xff0c;IO是什么&#xff1f;我们想&#…

申请专利需要准备哪些材料?

申请专利需要准备哪些材料&#xff1f;

代码之外的生存指南——自我营销

你是否有去过酒吧、夜店看过驻场乐队的演出&#xff1f; 你到了那里面听过之后你会发现那些乐队的演唱水平丝毫不亚于原唱的艺术家们&#xff0c;都很有才华&#xff1b; 你有没有想过【为什么这些驻场乐队就只能在那小小的夜店里做驻唱演出&#xff0c;每天疲于奔命&#xff0…

图综述-GGNN详解

A Survey of Geometric Graph Neural Networks:Data Structures, Models and Applications 本文主要介绍了在化学领域的分子设计和预测任务中&#xff0c;如何利用几何图神经网络&#xff08;Geometric Graph Neural Networks&#xff0c;简称GGNN&#xff09;来处理具有几何信…