【STL】Vector剖析及模拟实现

news2024/11/25 22:24:41

作者阿润菜菜
📖专栏C++


vector的常用接口

首先贴上:vector的文档介绍,以备查阅使用。
vector的基本框架
在这里插入图片描述
在这里插入图片描述

vector的成员变量分别是空间首部分的_start指针和最后一个元素的下一个位置的_finish指针,以及指向可用空间末尾的下一个位置的_end_of_storage指针,对于vector来说,它的底层就是由顺序表实现的,所以它的迭代器就是原生指针T*,我们定义const和非const的迭代器,便于const和非const对象的迭代器的调用。

vector的构造函数

vector提供四种构造函数来进行初始化:在这里插入图片描述
1.对于无参构造,我们利用初始化列表来进行初始化,用nullptr初始化

vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{}

2.构造并初始化是用n个value值来初始化,因为value既可能是内置类型,也可能是自定义类型,所以如果用引用作参数的话,需要用const引用,也就是常引用来作参数,否则无法接收内置类型的实参。

//vector<int> v(10,5)初始化
		vector(size_t n, const T& val = T()) //这里调用匿名对象的默认构造初始化
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);  //进行深拷贝,不要用memcpy,浅拷贝会带来二次析构等问题
			}
		}

注意匿名对象的生命周期只有其所在那一行,const引用会延长匿名对象的生命周期到引用对象域结束,因为后续调用就是匿名对象,并自动销毁

在实现完n个value构造的构造函数之后,如果我们此时用10个int类型的数字1来构造对象v1,实际会报错,报错的原因其实是由于函数的匹配优先级所导致的实参无法正确匹配相应的构造函数。而使用10个char类型的字符A却不会报错,这是由于函数的匹配优先级决定的
对于这种问题STL源码的解决方式是利用了函数重载来解决了这个问题,多重载了一个类型为int的构造函数

	//int 不能向size_t 类型转换
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

3.对于拷贝构造函数的实现有个一定需要注意的问题: 深浅拷贝问题 — 对象嵌套深浅拷贝问题
我们传统写法会是memcpy申请资源并按字节拷贝,这种写法用在string类实现可以,但在vector类实现就肯定不行了,因为vector的模板实例化有int数组对象,string对象、及vector的vector嵌套等等。所以对于自定义类型如果时浅拷贝,会使两个对象指向同一块空间,对象调用析构函数时会造成二次析构!值拷贝很多时候是一种危险的行为。

我们采用现代写法,也就是复用写法,利用形参对象v的迭代器来构造临时对象tmp,然后将对象tmp和* this进行交换,这里交换函数需要我们自己提供,复用库里的swap即可。同时为了防止对象tmp离开函数栈帧销毁时造成野指针的访问问题,所以在调用构造函数时采用了初始化列表的方式将* this的三个成员都初始化为nullptr。
在实现拷贝构造后,我们还需要实现赋值重载,利用传值拷贝构造的临时对象即可,然后调用swap类成员函数即可完成自定义类型的赋值工作。为了符合连续赋值含义,我们利用引用来作为返回值。

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

	return *this;
}

vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	//复用区间构造
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);//交换的时候就可以体现出拷贝构造函数初始化列表的好处了
}

// v1 = v2;
// v1 = v1;
vector<T>& operator=(vector<T> v)//返回引用,符合连续赋值的含义
{
	swap(v);
	return *this;
}

不复用swap函数,通过复用reserve函数实现,要注意更新_finish和_endofstorge

      vector(const vector<T>& v)

            : _start(nullptr), _finish(nullptr), _endOfStorage(nullptr)

        {

            reserve(v.capacity());

            iterator it = begin();

            const_iterator vit = v.cbegin();

            while (vit != v.cend())

            {

                *it++ = *vit++;

            }

            _finish = _start + v.size();

            _endOfStorage = _start + v.capacity();

        }

小tips:在拷贝构造函数,可以不加模板参数 为了可读性我们推荐加上

4.迭代器区间构造
vector的迭代器区间初始化是个模板 可以传入任意类型,并可以部分初始化,但实际上vector很少用迭代器。

  template<class InputIterator>

        vector(InputIterator first, InputIterator last)

        {

            reserve(last - first);

            while (first != last)

            {

                push_back(*first);

                ++first;

            }

        }

vector iterator — 迭代器

接口说明
begin + end获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator

在这里插入图片描述

vector 空间函数

在这里插入图片描述
1.capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。
不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL

测试代码

// 测试vector的默认扩容机制
void TestVectorExpand()
{
 size_t sz;
 vector<int> v;
 sz = v.capacity();
 cout << "making v grow:\n";
 for (int i = 0; i < 100; ++i) 
 {
 v.push_back(i);
 if (sz != v.capacity()) 
 {
 sz = v.capacity();
 cout << "capacity changed: " << sz << '\n';
 }
 }
}

2.reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题,提高效率。
resize在开空间的同时还会进行初始化,影响size。

reserve函数
1.对于reserve以及resize函数来说,官方并没有将其设定为能够实际缩容的功能,明确规定这个函数在其他情况下,例如预留空间要比当前小的情况下,这个函数的调用是不会引起空间的重新分配的,也就是说容器vector的实际开辟的capacity是不会被影响的。值得注意的是缩容表面看起来是降低了空间的使用率,想要提高程序的效率,但实际上并未提高效率,缩容是需要异地缩容的,需要重新开空间和拷贝数据,代价不小,所以不建议对空间进行缩容
2.shrink_to_fit就是缩容函数,强制性的将capacity的大小降低到适配size大小的值,它的设计理念就是以空间来换时间,但日常人们所使用的手机或者PC空间实际上是足够的,不够的是时间,所以这种函数还是不要使用的为好,除非说你后面肯定不会插入数据了,不再进行任何modify操作,那你可以试着将空间还给操作系统,减少空间的使用率

//开辟空间
		void reserve(size_t n)
		{
			if (n > capacity()) //先检查,避免缩容
			//如果大于原有空间,则重新开辟并挪动数据,否则就不做任何行为
			{
				size_t sz = size();
				T* tmp = new T[n]; //开辟失败会抛异常
				if (_start)
				{
					memcpy(tmp, _start, , sizeof(T) * size());//拷贝旧数据
					delete[] _start;
				}
				//更新
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;

			}
		}

resize函数

1.注意C++是泛型编程的,所以resize提供了参数 T val = T () 用匿名对象默认构造来初始化

	//调整空间和初始化
		void resize(size_t n; T val = T()) //泛型编程 --- T val = T() 即用匿名对象的默认构造来初始化
		{
			if (n < size()) //若小于原空间数据量
			{
				//删除数据,但并不实际上缩小空间
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

vector 增删查改

在这里插入图片描述
1.这里push_back和pop_back都是对insert和erase的复用,所以我们主要介绍三个函数的模拟实现。find函数我们无需提供,直接调用算法模块库里的,对于swap我们需要直接实现,因为我们是一个自定义类型,浅拷贝无法实现swap。同时在实现insert和erase函数是我们会遇到第一个经典的迭代器失效问题,就是因为扩容操作,造成了迭代器野指针问题。

insert函数

1.在insert函数模拟实现中,因为插入数据,很多时候我们要对空间扩容,而且基本上是异地扩容,在扩容后迭代器pos指针就会指向已释放的空间,造成了野指针访问的问题,解决就是:更新pos迭代器指针
我们提供返回值 返回pos(使用时需要pos 接受返回值更新pos迭代器指针)

2.而且vector的插入操作如果导致底层空间重新开辟,则迭代器就会失效。如果空间足够,那么迭代也算失效了,因为数据相对位置已经发生改变,他已经不是指向之前的位置了。
3.那我们为什么不能用引用传参, 注意所有的传值返回都是拷贝临时对象,具有常性(不能改变),不能传给左值引用

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

			if (_finish == _end_of_storage) //检查扩容
			{
				size_t len = pos - _start; //记录len 后续pos更新
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				//扩容后更新pos,解决迭代器pos失效问题
				pos = _start + len;
			}

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

			*pos = val;
			++_finish;

			return pos;
		}

erase函数
1.erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
2. 在实现函数erase时,erase 之后,认为pos是否失效? 我们认为失效,不要访问,因为行为结果不能确定的,根据具体的编译器实现有关系 ---- 本质是不让pos跳过_finish(本该访问数据) 发生段错误或者结果不对
3. 在使用时,vector 的erase 要配合find实现
在这里插入图片描述
vs编译器会进行强制检查,erase以后的迭代器,都不能继续访问

// 返回删除数据的下一个数据

    // 方便解决:一边遍历一边删除的迭代器失效问题

        iterator erase(Iterator pos)

        {

            // 挪动数据进行删除

            iterator begin = pos + 1;

            while (begin != _finish)

            {

                *(begin - 1) = *begin;

                ++begin;

            }

            --_finish;

            return pos;

        }

operator[ ]
1.其实vector一般不常用迭代器。常用的stl只有 string和vector 会使用方括号,其他的都会使用迭代器
2.我们要注意越界访问的检查机制,直接断言assert(pos<size)

	//[]
		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模拟实现源码

vector深度剖析

使用memcpy拷贝问题

  1. memcpy是内存的二进制格式按字节拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。这是一种妥妥浅拷贝
  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且
    自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝,会造成对象和拷贝对象指向同一块空间,在调用析构时,会二次析构。
    在这里插入图片描述
    在这里插入图片描述
    结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是
    浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

迭代器失效问题

1.vector的迭代器是一个原生指针的typedef,所以迭代器失效的本质就是指针失效,换句话说就是野指针访问,对指针指向的无效空间进行访问所导致的问题。
2.在使用insert时,我们需要传某个位置的迭代器,如果在insert中不发生扩容,则这个迭代器在insert之后还是有效的,但只要在insert中发生扩容,则迭代器就会失效,因为reserve进行的是异地扩容,所以原有的迭代器就不能使用,因为原有迭代器指向的是已经释放的旧空间的某个位置,所以如果继续使用,则势必会发生野指针的访问,这正是我们所说的迭代器失效。
3.比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指向的
空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,
程序可能会崩溃)
4.对于vector可能会导致其迭代器失效的操作有:会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。

迭代器失效解决办法:在使用前,对迭代器重新赋值即可

vector二维数组

构造一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:
在这里插入图片描述
vv中元素填充完成之后,如下图所示:
在这里插入图片描述
使用标准库中vector构建动态二维数组时与上图实际是一致的.

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

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

相关文章

Tomcat安装及启动

日升时奋斗&#xff0c;日落时自省 目录 1、Tomcat下载 2、JDK安装及配置环境 3、Tomcat配置环境 4、启动Tomcat 5、部署演示 1、Tomcat下载 直接入主题&#xff0c;下载Tomcat 首先就是别下错了&#xff0c;直接找官方如何看是不是广告&#xff0c;或者造假 搜索Tomc…

【强度混合和波段自适应细节融合:PAN-Sharpening】

Intensity mixture and band-adaptive detail fusion for pansharpening &#xff08;用于全色锐化的强度混合和波段自适应细节融合&#xff09; 全色锐化的目的是通过高分辨率单通道全色&#xff08;PAN&#xff09;图像锐化低分辨率多光谱&#xff08;MS&#xff09;图像&a…

ChatGPT、人工智能、人类和一些酒桌闲聊

© 2023 Conmajia Initiated 10th March, 2023 昨天跟某化学家喝酒&#xff0c;期间提到了 ChatGPT。他的评价是&#xff1a;这鬼东西大量输出毫无意义、错漏百出甚至是虚假的信息&#xff0c;“in a confident accent”。例如某次 GPT 针对“描述某某记者”这一问题&#…

C++的入门

C的关键字 C总计63个关键字&#xff0c;C语言32个关键字 命名空间 我们C的就是建立在C语言之上&#xff0c;但是是高于C语言的&#xff0c;将C语言的不足都弥补上了&#xff0c;而命名空间就是为了弥补C语言的不足。 看一下这个例子。在C语言中会报错 #include<stdio.h>…

【C++】C++11——左右值|右值引用|移动语义|完美转发

文章目录一、左值与右值1.概念2.引用3.注意二、右值引用的意义1.左值引用意义2.右值引用和移动语义3.容器新增三、万能引用四、完美转发一、左值与右值 1.概念 左值是什么&#xff1f;右值是什么&#xff1f; 左值是一个表示数据的表达式&#xff08;如变量名或解引用的指针&…

学校学生心理测评系统

青少年在线心理测评系统 这款系统&#xff0c;是和北大合作开发&#xff0c;并真实用于线上测评场景&#xff0c;该项目有完整后台&#xff0c;以及学生管理等模块。 我们欢迎以下形式合作&#xff1a; 单纯研究项目。合作对该测评平台进行升级。单纯使用。 请联系我们 silv…

MyBatis里面用了多少种设计模式?

在MyBatis的两万多行的框架源码中&#xff0c;使用了大量的设计模式对工程架构中的复杂场景进行解耦&#xff0c;这些设计模式的巧妙使用是整个框架的精华。经过整理&#xff0c;大概有以下设计模式&#xff0c;如图1所示。图101类型&#xff1a;创建型模式▊ 工厂模式SqlSessi…

英飞凌Tricore原理及应用介绍04_中断请求及仲裁过程

目录1.概述2. 中断请求及过程仲裁3. 中断传到CPU会被即时响应吗&#xff1f;1.概述 在Tricore架构中允许有多个中断源包括片上外设及外部中断世间产生的中断请求&#xff0c;以打断中断服务的提供者如CPU或DMA通道&#xff0c;那你知道在Tricore里中断请求在内核中的仲裁及处理…

【java基础】ArrayList源码解析

文章目录基本介绍构造器指定初始容量默认创建通过集合创建添加add扩容机制批量添加addAll添加指定位置add添加多个元素到指定位置addAll删除删除指定元素remove删除指定索引元素remove条件删除removeIf批量删除removeAll修改修改指定位置set替换所有满足要求元素replaceAll一些…

vscode环境配置(支持跳转,阅读linux kernel)

目录 1.卸载clangd插件 2.安装C插件 3. 搜索框内输入 “intell”&#xff0c;将 C_Cpp&#xff1a;Intelli Sense Engine 开关设置为 Default。 4.ubuntu安装global工具 5.vscode安装插件 6.验证是否生效 7.建立索引 1.卸载clangd插件 在插件管理中卸载clangd插件 2.安…

课设-机器学习课设-实现新闻分类

✅作者简介&#xff1a;CSDN内容合伙人、信息安全专业在校大学生&#x1f3c6; &#x1f525;系列专栏 &#xff1a;课设-机器学习 &#x1f4c3;新人博主 &#xff1a;欢迎点赞收藏关注&#xff0c;会回访&#xff01; &#x1f4ac;舞台再大&#xff0c;你不上台&#xff0c;…

linux下安装SonarQube

目录1. 准备安装环境2. 安装postgres数据库3. 安装SonarQube4. 使用SonarQube1. 准备安装环境 这里安装SonarQube的系统环境是Red Hat Enterprise Linux release 8.7 &#xff0c;然后将jdk的压缩包&#xff08;jdk-17.0.2_linux-x64_bin.tar.gz&#xff09;和sonarQube的压缩…

Web Components学习(2)-语法

一、Web Components 对 Vue 的影响 尤雨溪在创建 Vue 的时候大量参考了 Web Components 的语法&#xff0c;下面写个简单示例。 首先写个 Vue 组件 my-span.vue&#xff1a; <!-- my-span.vue --> <template><span>my-span</span> </template>…

Spring——spring整合JUnit

JUnit定义: Junit测试是程序员测试&#xff0c;即所谓 白盒测试 &#xff0c;因为程序员知道被测试的软件如何&#xff08;How&#xff09;完成功能和完成什么样&#xff08;What&#xff09;的功能。 Junit是一套框架&#xff0c;继承TestCase类&#xff0c;就可以用Junit进行…

基于Selenium+Python的web自动化测试框架(附框架源码+项目实战)

目录 一、什么是Selenium&#xff1f; 二、自动化测试框架 三、自动化框架的设计和实现 四、需要改进的模块 五、总结 总结感谢每一个认真阅读我文章的人&#xff01;&#xff01;&#xff01; 重点&#xff1a;配套学习资料和视频教学 一、什么是Selenium&#xff1f; …

SpringBoot bean 加载顺序如何查看(源码解读)

背景 SpringBoot bean 加载顺序如何查看&#xff0c;想看加载了哪些bean&#xff0c; 这些bean的加载顺序是什么&#xff1f; 实际加载顺序不受控制&#xff0c;但会有一些大的原则&#xff1a; 1、按照字母顺序加载&#xff08;同一文件夹下按照字母数序&#xff1b;不同文件…

界面开发(4)--- PyQt5实现打开图像及视频播放功能

PyQt5创建打开图像及播放视频页面 上篇文章主要介绍了如何实现登录界面的账号密码注册及登录功能&#xff0c;还简单介绍了有关数据库的连接方法。这篇文章我们介绍一下如何在设计的页面中打开本地的图像&#xff0c;以及实现视频播放功能。 实现打开图像功能 为了便于记录实…

CobaltStrike攻击payload(有效载荷)介绍

HTA文档Office宏payload生成器有效载荷生成器windows可执行程序windows可执行程序windows stageless生成所有有效载荷HTA文档该模块为HTML Application attack&#xff08;HTML应用攻击&#xff09;。简单来说&#xff0c;就是这个包生成一个运行有效负载的HTML应用程序该模块下…

TCP UPD详解

文章目录TCP UDP协议1. 概述2. 端口号 复用 分用3. TCP3.1 TCP首部格式3.2 建立连接-三次握手3.3 释放连接-四次挥手3.4 TCP流量控制3.5 TCP拥塞控制3.6 TCP可靠传输的实现3.7 TCP超时重传4. UDP5.TCP与UDP的区别TCP UDP协议 1. 概述 TCP、UDP协议是TCP/IP体系结构传输层中的…

Flink 定时加载数据源

一、简介 flink 自定义实时数据源使用流处理比较简单&#xff0c;比如 Kafka、MQ 等&#xff0c;如果使用 MySQL、redis 批处理也比较简单 如果需要定时加载数据作为 flink 数据源使用流处理&#xff0c;比如定时从 mysql 或者 redis 获取一批数据&#xff0c;传入 flink 做处…