当前位置: 首页 > 编程日记 > 正文

【C++干货铺】解密vector底层逻辑

=========================================================================

个人主页点击直达:小白不是程序媛

C++系列专栏:C++干货铺

代码仓库:Gitee

=========================================================================

目录

vector介绍

vector的使用

vector的定义和使用

vector的空间增长问题

vector的增删查改

解密vector及模拟实现

成员变量

成员函数

构造函数

拷贝构造函数

operator = 

析构函数

reserve

push_back

erase

 resize

insert

operator [ ] 

迭代器

size

capacity


vector介绍

1. vector是表示可变大小数组的序列容器
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。


vector的使用

vector的使用和string差不多,有很多的接口供我们函数调用。这些函数的功能包括增、删、查、改等。

vector的定义和使用

接口函数名称

接口函数功能

vector()无参构造
vector(size_type n,const value_type&val=value_type())构造并初始化n个val
vector(const vector& x)拷贝构造
vector (InputIterator first, InputIterator last)使用迭代器进行初始化构造
begin+end使用迭代器对实例化的对象进行读取
operator[ ]使用数组的方式对实例化的对象读取
范围for使用范围for的方式对实例化的对象读取
	vector <int> v1;
	vector <int> v2(10, 1);
	for (size_t i = 0; i < v2.size(); i++)
	{
		cout << v2[i] << " ";
	}
	cout << endl;
	vector<int>::iterator it = v2.begin();
	while (it != v2.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	string s1("hello world");
	vector<char> v3(s1.begin(), s1.end());
	for (auto ch : v3)
	{
		cout << ch << " ";
	}
	cout << endl;
	vector<char> v4(v3);
	for (auto ch : v4)
	{
		cout << ch << " ";
	}
	cout << endl;

vector的空间增长问题

接口函数接口函数的作用
size获取有效数据的个数
capacity获取容量大小
resize改变vector的size
reserve改变vector的capacity
	vector<int> v1;
	
	size_t sz = v1.capacity();
	cout << "初始容量:" << sz << endl;
	for (size_t i = 0; i < 100; i++)
	{
		v1.push_back(i);
		if (sz != v1.capacity())
		{
			sz = v1.capacity();
			cout << "容量: " << sz << endl;
		}
	}

  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
  • resize在开空间的同时还会进行初始化,影响size。

vector的增删查改

接口函数接口函数的作用
push_back尾插
pop_back尾删
insert在pos位置插入val
erase删除pos位置的数据
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (auto i : v)
	{
		cout << i << " ";
	}
	cout << endl;
	v.pop_back();
	for (auto i : v)
	{
		cout << i << " ";
	}
	cout << endl;
	vector<int>::iterator pos = v.begin();
	v.insert(pos, 1);
	for (auto i : v)
	{
		cout << i << " ";
	}
	cout << endl;
	v.erase(v.begin());
	for (auto i : v)
	{
		cout << i << " ";
	}
	cout << endl;


解密vector及模拟实现

成员变量

	//指向动态开辟空间的开始
	iterator _start;
	//指向最后一个有效数据的结尾
	iterator _finish;
	//指向动态开辟空间的结尾
	iterator _end;

vector是通过三个指针实现的,并不像我们在数据结构初阶那样使用一个指向动态开辟的指针和两个变量来实现。三个指针都指向的是动态开辟的空间,只不过各司其职;第一个指针直接指向动态开辟空间,第二个指针指向动态开辟空间中有效数据的个数的下一位,第三个指针指向的是动态开辟空间的结尾。

成员函数

构造函数

	//传入数据类型的指针
	typedef T* iterator;
	//静态
	typedef const T* const_iterator;
	//构造函数
	Vector()
		:_start(nullptr)
		, _finish(nullptr)
		, _end(nullptr)
	{}

使用初始化列表对成员变量进行初始化为空指针

拷贝构造函数

	Vector(const Vector<T>& x)
		:_start(nullptr)
		, _finish(nullptr)
		, _end(nullptr)
	{
		reserve(x.capacity());
		for (auto e : x)
		{
			push_back(e);
		}
	}

operator =

	void swap(Vector<T>& v)
	{
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_end, v._end);
	}
	Vector<T>& operator=( Vector<T>& tmp)
	{
		swap(tmp);
		return *this;
	}

析构函数

	~Vector()
	{
		delete[] _start;
		_start = _finish = _end = nullptr;
	}

reserve

	void reserve(size_t n)
	{
		
		if (n > capacity())
		{
			T* tmp = new T[n];
			//记录开始和有效数据结尾中间的有效数据个数防止,迭代器失效
			size_t sz = size();
			//判断原指针是否为空指针,第一次开辟空间是空指针
			if (_start)
			{
				//将有效数据的字节数拷贝到新空间中
                //memcpy函数只会进行的是浅拷贝,对于自定以类型无法完成深拷贝
				//memcpy(tmp, _start, sizeof(T) * sz);
				for (size_t i = 0; i < sz; i++)
				{
                    //赋值重载会完成深拷贝
					tmp[i] = _start[i];
				}
				//释放旧空间
				delete[] _start;
			}

			_start = tmp;
			//重置指针,解决迭代器失效问题
			_finish = _start + sz;
			_end= _start + n;
		}
	}

这里我们要进行空间的扩容,先将size记录下来;开辟好新的空间后根据size和第一个指针重置第二个指针;根据第一个指针和开辟好空间的容量重置第三个指针。

push_back

	void push_back(const T& x)
	{
		//需不需要扩容
		if (_finish == _end)
		{
			//第一次容量为空
			reserve(capacity() == 0 ? 4 : capacity() * 2);
		}
		//尾插数据,移动指针
		*_finish = x;
		_finish++;
	}

尾插根据第二个和第三个指针判断容量的大小,是否需要扩容;注意:第一次扩容时使用三目操作符进行判断;

erase

	void /*iterator*/ erase(iterator pos)
	{
		assert(pos < _finish);
		iterator begin = pos + 1;
		//移动数据
		while (begin < _finish)
		{
			*(begin - 1) = *(begin);
			begin++;
		}
		//修改指针;
		_finish--;
		//return pos;
	}

resize

	void resize(size_t n , const T& val=T())
	{
		if (n < size())
		{
			//重置的大小小于当前大小
			//修改指针即可
			_finish = _start + n;
		}
		else
		{
			//两种情况
			//重置的大小小于大于当前容量  需要扩容
			//重置的大小小于小于当前容量  不用扩容直 接放数据
			if (n > capacity())
			{
				reserve(n);
			}
			while (_finish < _start + n)
			{
				*_finish = val;
				_finish++;
			}
		}
	}

insert

	void insert(iterator pos, const T& x)
	{
		//判断容量
		if (_finish == _end)
		{
			//记录旧空间的pos位置
			size_t sz = pos - _start;
			reserve(capacity() == 0 ? 4 : capacity() * 2);
			//在新空间中找到pos位置
			pos = _start + sz;
		}
		auto end = _finish - 1;
		while (end >= pos)
		{
			*(end + 1) == *end;
			end--;
		}
		*pos = x;
		_finish++;
	}

这里扩容后改指向旧空间pos是野指针,程序会崩溃;要在扩容前配合第一个指针记录此时的位置,扩容后进行重置。

operator [ ]

	//可读可写
    T& operator[](size_t pos)
	{
		return _start[pos];
	}
    //只读
	const T& operator[](size_t pos) const
	{
		return _start[pos];
	}

迭代器

    //可读可写
    iterator begin()
	{
		return _start;
	}
	iterator end()
	{
		return _finish;
	}
	//只读
    const_iterator begin()const
	{
		return _start;
	}
	const_iterator end()const
	{
		return _finish;
	}

size

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

capacity

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

今天对vector的介绍和底层模拟实现的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法。您三连的支持就是我前进的动力,感谢大家的支持!!!

相关文章:

【Linux系统化学习】进程的父子关系 | fork 进程

本篇文章介绍了进程的父子关系和使用fork函数创建一个进程!

【Linux系统化学习】探索进程的奥秘 | 第一个系统调用

本片文章主要介绍了Linux下的进程和第一个系统调用。

人工智能时代:AIGC的横空出世

AIGC是一种新的人工智能技术,即人工智能生成内容。它是一种基于机器学习和自然语言处理的技术,能够自动产生文本、图像、音频等多种类型的内容。

零基础搭建本地Nextcloud私有云结合内网穿透实现远程访问

本文主要讲解如何搭建本地Nextcloud私有云结合内网穿透实现远程访问

如何在外远程访问本地NAS威联通QNAP?

本文主要讲解在外远程访问本地NAS威联通QNAP。

使用Linux JumpServer堡垒机本地部署与远程访问

本文主要讲解如何使用Linux JumpServer堡垒机本地部署与远程访问。

Java中判断两个Long类型是否相等

也就是说这个值在-128到127之间会使用缓存,超过就会创建一个对象,所以上述的两个值分别创建了两个对象,那么使用。可以使用 .longValue() 或 .equals() 进行比较。推荐使用equals方法进行比较。现象1和现象2结果不一样,现象2使用==判断两个Long类型的值,结果竟然是false!

Nginx反向代理跳过国内备案(以宝塔面板为例)

Nginx代理跳过大陆备案验证,需要两台服务器,一台已备案或者免备案,一台国内主力服务器放你的项目。B服务器在配置文件里设置listen监听端口号。先把域名解析到A服务器。然后在A服务器里配置。

大华摄像头windows、linuxJavaSDK开发使用

本文档主要介绍 SDK 接口参考信息,包括主要功能、接口函数和回调函数。主要功能包括:SDK 初始化、设备登录、实时预览、云台控制、语音对讲、报警监听、智能订阅、录像回放和录像下载等。根据环境不同,开发包包含的文件会不同,具体如下所示。Windows 开发包所包含的文件如下:Linux 开发包所包含的文件如下:SDK 的功能库和配置库是必备库。功能库是设备网络 SDK 的主体,主要用于网络客户端与各类产品之间的通讯交互,负责远程控制、查询、配置及码流数据的获取和处理等。

什么是缓存穿透、缓存击穿、缓存雪崩,以及各自的解决方案

当缓存数据大面积失效,导致请求无法从缓存中拿到数据而是直接访问数据库。

并发编程的基本概念

单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的。总结为一句话就是:微观串行,宏观并行,多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent。

try catch 应该在 for 循环里面还是外面?

有个老哥昨天被面试官欺负了,但是是被这个问题(标题)欺负的?其实是个比较基础的问题,只要有了解过,叙述是非常简单OK的。

储存容量单位:Bit, Byte, KB, MB, GB, TB , PB, EB, ZB, YB等的关系

有趣的是从 Wikipedia 看到的单位英文在「十进位」与「二进位」不同进制之间所使用的英文单字是不太一样的,例如我们常讲 30GB 会唸成 30 Gigabytes,不过正确的唸法应该是 Gibibytes 才对,不过大家都随便念、随便写,反正差不多、听的懂就好,我想唸过计算机概论的人自己都会知道 1GB = 1024 MB 吧,如果唸成 Gibibytes 搞不好还会被笑没知识!这样大的数据单位估计在未来的五年内是无法达到的,不过我相信假以时日人类的需求一定能够达到或者超越CB级。

Jmeter执行接口自动化测试-如何初始化清空旧数据

生命不息,奋斗不止。每一份努力都不会被辜负,只要坚持不懈,终究会有回报。珍惜时间,追求梦想。不忘初心,砥砺前行。你的未来,由你掌握!生命短暂,时间宝贵,我们无法预知未来会发生什么,但我们可以掌握当下。珍惜每一天,努力奋斗,让自己变得更加强大和优秀。坚定信念,执着追求,成功终将属于你!只有不断地挑战自己,才能不断地超越自己。坚持追求梦想,勇敢前行,你就会发现奋斗的过程是如此美好而值得。相信自己,你一定可以做到!

带有 RaspiCam 的 Raspberry Pi 监控和延时摄影摄像机

​一段时间以来,我一直想构建一个运动激活且具有延时功能的树莓派相机,但从未真正找到我喜欢的案例。我在thingiverse上找到了这个适合树莓派和相机的好案例。它是为特定的鱼眼相机设计的,但从模型来看,我拥有的廉价中国鱼眼手机镜头之一似乎非常适合孔中。​

Nginx基础篇:Nginx搭建、Nginx反向代理、文件服务器部署配置。

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Рамблер)开发的,公开版本1.19.6发布于2020年12月15日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、简单的配置文件和低系统资源的消耗而闻名。2022年01月25日,nginx 1.21.6发布。

如何在Android平板上远程连接Ubuntu服务器使用code-server代码开发

如何在Android平板上远程连接Ubuntu服务器使用code-server代码开发

使用Pytorch实现Grad-CAM并绘制热力图

就直接从官方的torch vision这个库当中导入一些我们常用的model。比如说我这里的例子是采用的mobile net v3 large这个模型。它就会自动的去下载torch官方在imagenet上预训练好的模型权重。我们这里采用的模型呢是在image net 1k上预训练好的模型。接着这里的target layers也要根据你自己的模型去设置。首先呢我们这里需要指定一下我们的target layers。紧接着呢我们还需要去指定一下我们所感兴趣的这个类别。

CentOS最小化安装后怎么转图形界面/可视化桌面?

2.如果在图形界面下,按:Ctrl+Alt+F2/F3,可以进入命令行模式。如果安装的是最小化,那么init 5 (进入图像化桌面)命令是无效的。1.如果在命令行模式,按Ctrl+Alt+F1,可以进入图形界面;设置完后,用上一条命令:systemctl get-default。注意:进入图形界面要创建一个新用户,同样要记好账号和密码。查看是否返回:Graphical.target。图形界面:终端输入init 3进入命令行。命令行:输入init 5进入图形。设置默认启动桌面(看个人喜欢)

Python中的深拷贝和浅拷贝的区别

深拷贝和浅拷贝是Python中非常重要的概念,它们在处理对象和数据结构时有着截然不同的行为。理解深拷贝和浅拷贝的区别以及适用场景对于面试和实际编程工作都非常有帮助。在选择深拷贝和浅拷贝时,需要根据具体的需求和数据结构来决定。

VMware安装Ubuntu20.04并使用Xshell连接虚拟机

注意,还原默认设置你的网络地址可能发生改变,而且之前如果手动配置过VMware8的IP地址和DNS服务器地址,也会还原为默认的自动获取IP地址和DNS服务器地址。如果你是新安装的VMware,你应该会直接看到下面还原了网络设置后的界面。根据下载链接,下载安装完成VMware,在VMware里创建虚拟机,镜像选择刚才下载的Ubuntu Server 20.04。注意,还原默认设置后,子网IP发生了变化,从。记住你配置的子网,后面配置的VMnet8、网关、虚拟机的IP地址都跟它有关。至于为什么选择这个版本?

Nginx按指定格式记录访问日志

其实我们在用常用的web服务器上都有这项功能,我们这里用Nginx举例,我们的访问日志一般正常都是什么设备在什么地址访问了我们的什么资源,后端服务器的响应时间是多少,客户端请求处理的总时间是多少;一般我们作为开发人员关注的日志只是在应用程序层面的,我们称它为应用程序日志,访问日志和错误日志可以被认为是应用程序日志的一部分,因为它们都与应用程序的运行状态和用户访问行为有关。:代表User-Agent HTTP头部,指示发起请求的客户端的用户代理(例如,浏览器)。:代表发起请求的客户端的IP地址。

Redis保证高可用的三种方式

Redis保证高可用主要有三种方式:主从、哨兵、集群。

库卡LBR_iisy_3_R760协作机器人导入到coppeliasim

一般载都是这个step文件格式,其他的好像不太好用。coppeliasim导入格式用的是stl,需要用freeCAD打开重新转换一下。下载下来后,很多都是一个整体,在freeCAD导入中,导入选择要不勾选合并。下载完用CAD Assisitant打开后是这个样子的。

nacos 2.0 版本在spring cloud 2022.0.0.0-RC2读取配置文件失败

报错信息如下Spring 官方给出的解决方案如下。

gRPC三种流和消息格式

服务端实现protocol buffer定义的方法,客户端保留一个存根,提供服务端方法的抽象,客户端只需要调用存根中的方法,就可以远程调用服务端方法。客户端多个请求发给服务端,服务端发送一个响应给客户端,比如更新业务,客户端的读个请求发过来,服务端更新完返回一个成功的结果。通信时可以是一个请求,服务端多次响应,比如查询业务,服务端模糊匹配找到一次就返回客户端一次响应这样的多次响应。客户端发送,包含3个部分:请求头信息、长度前缀的消息、流结束标记。在写入消息前,先写入长度消息表明每条消息的大小。

如何本地搭建Linux DataEase数据可视化分析工具并实现公网访问

如何本地搭建Linux DataEase数据可视化分析工具并实现公网访问

SpringBoot进行自然语言处理,利用Hanlp进行文本情感分析

自然语言处理,或简称NLP,是处理和转换文本的计算机科学学科。它由几个任务组成,这些任务从标记化开始,将文本分成单独的意义单位,应用句法和语义分析来生成抽象的知识表示,然后再次将该表示转换为文本,用于翻译、问答或对话等目的。

深入理解JVM内存空间的担保策略

本文将介绍 jvm中垃圾回收(GC)时空间担保策略是如何执行的

C#winform根据选择的Excel文件在数据库中创建数据表

C#winform根据选择的Excel文件在数据库中创建数据表