C++中的vector使用详解及重要部分底层实现

news2024/12/23 13:36:23

  

  本篇文章会对vector的语法使用进行详解。同时,还会对重要难点部分的底层实现进行讲解。其中有vector的迭代器失效深拷贝问题。希望本篇文章的内容会对你有所帮助。

目录

一、vector 简单概述

1、1 C语言中数组的不便

1、2 C++中的动态数组容器vector 

二、vector的常用语法举例

2、1 vector的声明和定义

2、1 尾插 push_back

2、2 尾删 pop_back

2、3 设置容量大小 reserve

2、4 赋值 =

2、5 在pos位置插入

2、6 任意位置删除

2、7 访问vector中的元素

2、8 数组中的头和尾元素front()、back()

 三、部分重要底层实现及常见问题

3、1 拷贝构造的底层实现

3、2 insert的底层实现及迭代器失效

3、3  erase的底层实现及迭代器失效

3、4 vector中深拷贝的问题


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++  👀

💥 标题:vector讲解💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️  

一、vector 简单概述

1、1 C语言中数组的不便

  在C语言中,我们所要存放一组类型相同的数据,我们可以选择数组。C语言中的数组是静态的,一旦声明后,其大小就是固定的,无法动态调整。这就导致使用起来并不方便。当然,我们也用malloc、calloc来动态申请空间。当我们不再使用此数组时,我们也要时刻注意是否已经释放我们所动态开辟的空间

1、2 C++中的动态数组容器vector 

   针对C语言中静态数组的不便,C++中引出了动态数组容器vector,vector有以下几个不同和优点:

  1. 可以根据需要自动调整大小,可以动态地添加或删除元素。
  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  3. C++中的vector自动处理内存管理,无需手动指定大小或释放内存。
  4. C++中的vector提供了边界检查功能,能够确保在访问元素时不会发生越界错误。
  5. C++中的vector提供了丰富的函数和操作,如添加元素、删除元素、排序、查找等。这些功能能够更方便地操作和处理数组元素。

  本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是 一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。

   只有概念并不能很好的使用vector,我们接下来看一下vector语法的讲解。 

二、vector的常用语法举例

#include <vector>
#include <iostream>
using namespace std;
int main()
{
	vector<int> v;
	vector<int> v1(10);
	vector<int> v2(10, 1);
	vector<int> v3(v2);


	v.push_back(10);
	v.push_back(20);

	v.pop_back();

	v.reserve(10);

	v = v1;

	v.insert(v.begin() + 2, 3);

	v.erase(v.begin() + 2);

	int sz = v.size();
	for (int i = 0; i < sz; i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;

	v.clear();
    
    v.front();
    v.back();
	return 0;
}

   我们就上面的例子展开对vector的用法进行详解。

2、1 vector的声明和定义

  当我们想用vector时,我们首先要引入头文件:#include<vector>。当我们可展开命名空间std,也可选择不展开命名空间std。具体例子如下:

#include <vector>
#include <iostream>
using namespace std;

int main() 
{
    //std::vector<int> v;
	vector<int> v;
	vector<int> v1(10);
	vector<int> v2(10, 1);
	vector<int> v3(v2);
    return 0;
}   

  注意,上述中的变量v为空数组,空间大小为0。v1是开辟了一个大小为10个int的数组,这10个元素默认为0。v2是开辟了一个大小为10个int的数组,这10个元素初始化为1。v3是用v2中的元素来进行初始化的,也就是v3中的元素与v2中的元素相同。我们可看如下结果:

2、1 尾插 push_back

  有了vector,我们想要往里添加元素,我们可使用push_back进行尾部插入元素。具体例子如下:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	vector<int> v1(10);
	vector<int> v2(10, 1);


	v.push_back(10);
	v.push_back(20);
	v.push_back(30);
    return 0;
}

  运行结果如下:

   我们发现,v数组不是空间大小为0吗,怎么还能插入元素呢?这个是因为vector是一个动态数组,底层就是一个动态的顺序表当空间容量不够时,会自动进行扩容

2、2 尾删 pop_back

   pop_back可对尾部元素进行删除。当然,尾删的前提是数组不为空。否则会程序会被终止。具体例子如下:‘

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	//v.pop_back();
	vector<int> v1(10);
	vector<int> v2(10, 1);


	v.push_back(10);
	v.push_back(20);
	v.push_back(30);

	v.pop_back();
    return 0;
}

  我们先看看在空vector进行删除的结果:

  我们再看看实际的尾删效果:

2、3 设置容量大小 reserve

  当我们知道要存储的元素个数是多少时,我们可直接通过reserve来设置vector的容量大小。有人就说了,vector空间不够了可以自己进行扩容,为啥还要用reserve来设定容量大小呢?注意,扩容是有损耗的。频繁的扩容会大大的降低程序的运行效率。我们来看reserve的实际效果。

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;

	v.push_back(10);
	v.push_back(20);
	v.push_back(30);

	v.pop_back();

	
	v.reserve(10);
    return 0;
}

  运行结果如下:

  我们之前看到尾删后的容量(capacity)大小为3,当我们reserve()后,容量大小变为了10。size为当前vector中的实际有多少个元素。capacity为当前vector实际能够存储多少个元素。两者是不同的。

2、4 赋值 =

  当然,我们也可直接对不同的vector进行赋值操作。具体例子如下:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	vector<int> v1(10);

	v.push_back(10);
	v.push_back(20);
	v.push_back(30);

	v.pop_back();

	
	v.reserve(10);

	
	v = v1;
    return 0;
}

  运行结果如下:

  赋值完后,v与v1完全相同。 

2、5 在pos位置插入

  我们发现尾插并不能很好的满足我们对插入的实现。insert就可以选择位置进行插入。当然,我们插入的位置必须是合法的位置。否则程序就会终止。我们看如下例子:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	vector<int> v1(10);

	v.push_back(10);
	v.push_back(20);
	v.push_back(30);

	v.pop_back();

	
	v.reserve(10);

	
	v = v1;

	
	v.insert(v.begin() + 2, 3);
    return 0;
}

  运行结果如下:

  我们是在开始位置往后偏移两个元素的位置进行插入结果也正是如此。细心的同学也会发现,在插入的同时实现的扩容。 具体的扩容大小

2、6 任意位置删除

  尾删也并不能满足我们所需的删除功能。erase可进行任意位置删除。具体例子如下:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v;
	vector<int> v1(10);

	v.push_back(10);
	v.push_back(20);
	v.push_back(30);

	v.pop_back();

	
	v.reserve(10);

	
	v = v1;

	
	v.insert(v.begin() + 2, 3);

	v.erase(v.begin() + 2);
    return 0;
}

   运行结果如下:

   我们就是在之前所插入的位置进行了删除。

2、7 访问vector中的元素

   访问vector中的元素有两种:通过 []  + 下标索引 、迭代器。具体例子如下:

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> v(10,2);
    int sz = v.size();
	for (int i = 0; i < sz; i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	for (auto it : v)
	{
		cout << it << " ";
	}
	cout << endl;
    return 0;
}

  size()可返回vector中的元素个数。运行结果如下:

  都能够很好的打印出vector中的元素。

2、8 数组中的头和尾元素front()、back()

   front()、back()函数可返回数组中的第一个元素和最后一个元素。当然,我们也可通过下标直接访问。所以这两个函数也并不常用。

 三、部分重要底层实现及常见问题

  vector的底层是由三个指针是私有成员变量。来记录数组的不同位置、大小、容量。实际如下:

	template<class T>

	class vector
	{
	public:
		typedef T* iterator;private:
    private:
		iterator start;
		iterator finish;
		iterator end_of_storage;
	};

  注意,底层使用类模板来试实现的。很好的解决了vector可以存储不同类型的数据问题。

3、1 拷贝构造的底层实现

  拷贝构造底层实现的方式有很多,但思路是大同小异的,具体如下:

		vector(const vector<T>& v)
			:start(nullptr)
			,finish(nullptr)
			,end_of_storage(nullptr)
		{
			iterator tmp = new T[v.size()];
			//memcpy(tmp, v.start, sizeof(T) * v.size());
			//T为自定义类型时,自定义类型自动调用赋值重载,完成深拷贝
			for (size_t i = 0; i < v.size(); i++)
			{
				tmp[i] = v.start[i];
			}

			start = tmp;
			finish = start + v.size();
			end_of_storage = start + v.size();
		}

		//vector(const vector<T>& v)
		//	:start(nullptr)
		//	, finish(nullptr)
		//	, end_of_storage(nullptr)
		//{
		//	reserve(v.size());
		//	for (auto it : v)
		//	{
		//		push_back(it);
		//	}
		//}

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

		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(const vector<T>& v)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}

3、2 insert的底层实现及迭代器失效

  在实现insert时,我们需要注意的是扩容后,释放掉原来的空间,原来的pos就变成了野指针!需要记录相对位置,更新pos。具体代码如下:

		iterator insert(iterator pos,const T& x)
		{
			assert(pos >= start);
			assert(pos <= finish);
			if (start == end_of_storage)
			{
				//注意,扩容后原来的pos就变为野指针,需要记录相对位置更新pos
				size_t len = pos - start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = start + len;
			}

			iterator end = finish - 1;
			while (end >= pos)
			{
				*(end+1) = *end;
				end--;
			}
			*pos = x;
			finish++;

			return pos;
		}
	void test()
	{
		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 e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		vector<int>::iterator p = find(v.begin(), v.end(), 3);
		if (p != v.end())
		{
			// 在p位置插入数据以后,不要访问p,因为p可能失效了。
			v.insert(p, 30);

			//cout << *p << endl;
			//v.insert(p, 40);
		}

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

		v.insert(v.begin(), 1);
	}

  我们在用迭代器时,可能会出现很多意想不到的错误。上述代码中的变量 p 为例子,我们能一直在变量 p 位置插入数据吗?答案是不能!!!为什么呢?因为当我们扩容后,原指针 p 所指向的空间就会被释放,p 就变成了野指针!所以,在p位置插入数据以后,不要访问p,因为p可能失效了。这就是所谓的迭代器失效

3、3  erase的底层实现及迭代器失效

   erase的底层实现较为简单,直接更改finish指针即可。我们直接看代码:

		iterator erase(iterator pos)
		{
			assert(pos >= start);
			assert(pos < finish);

			iterator cur = pos + 1;
			while (cur < finish)
			{
				*(cur - 1) = *cur;
				cur++;
			}

			finish--;
			return pos;
		}

  erase并没有扩容,为啥会出现迭代器失效呢?我们先看个例子:

void test_vector4()
{
	// 正常运行
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	// 要求删除所有的偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}

		++it;
	}

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

void test_vector4()
{
	// 崩溃
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 要求删除所有的偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		++it;
	}

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

void test_vector4()
{
	// 结果不对
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(4);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	// 要求删除所有的偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
			
		++it;
	}

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

   上述准确来说,第一个运行结果正确的为侥幸。为什么呢?注意,erase删除后,后面的元素就会向前移动。并且会返回当前所指向的位置,所以不用再 it++ 了。正确的代码如下:

	 //正确的方式
	void test_vector4()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(4);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);

		// 要求删除所有的偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;

			}
		}

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

  总的来说,我们进行插入和删除后,就尽可能的不要再次去访问原指针了。很有可能已经失效了,会出现意想不到的效果。

3、4 vector中深拷贝的问题

  vector中,不管是赋值重载还是拷贝构造,都是需要进行深拷贝的。但是vector中元素也必须进行深拷贝。为什么呢?当vector中的元素为int、char等内置类型无所谓,如果是自定义类型,那问题就出来了。

  当vector中的元素为自定义类型,对元素进行先拷贝,元素还是指向的同一块空间。

  vector中存储的为string:

   _str中存储的是string的地址,如果使用浅拷贝,在插入扩容时,就把原来的地址拷贝过来。

  再释放掉原来的地址,就会导致新拷贝的地址变成野指针。具体如下:

 

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

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

相关文章

vue中实现div可编辑,并插入指定元素,样式

前言&#xff1a; vue中实现一个既可以编辑内容&#xff0c;有可以动态编辑内容插入一个带有样式的内容&#xff0c;改变默认内容后&#xff0c;这个样式消失的效果&#xff0c;这里来整理下调研与解决实现问题之路。 实现最终效果&#xff1a;图2为默认内容 1、可以光标点击任…

自定义MVC框架优化

目录 一、前言 二、优化问题 1.子控制器的初始化配置问题 2.页面跳转优化代码冗余问题 3.优化参数封装问题 三、进行优化 1.解决子控制器初始化配置 2.解决页面跳转的代码冗余问题 3.解决优化参数封装问题 4.中央控制器 一、前言 在自定义MVC框架原理中讲述了什么是…

Redis - Redis GEO实现经纬度测算距离,附近搜索范围

Redis GEO 主要用于存储地理位置信息&#xff0c;并对存储的信息进行操作&#xff0c;该功能在 Redis 3.2 版本新增 一、Redis GEO 操作方法 geoadd&#xff1a;添加地理位置的坐标 geopos&#xff1a;获取地理位置的坐标 geodist&#xff1a;计算两个位置之间的距离 geor…

client-go初级篇,从操作kubernetes到编写单元测试

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 尽管长篇系列《client-go实战》的内容足够丰富&#xff0c;然而内容太多每个知识点也有一定深度&#xff0c;对于打算快速学习并开始kubernetes开发…

分层架构简介

MVC是架构模式&#xff08;设计模式中的结构性模式&#xff09;&#xff0c;不是系统架构&#xff0c;更不是我们常说的三层架构 MVC的缺陷如下&#xff1a; 1.导致控制器冗余&#xff08;有大量的业务逻辑&#xff0c;可能开始没有&#xff0c;但是后来越来越多&#xff09;…

QT学习笔记5--槽函数重载解决办法

connect函数 connect(sender, signal, receiver, slot); 槽函数示例 void student:: treat(QString foodname) void student:: treat(int index) 由上可见&#xff0c;有两个名字相同&#xff0c;但形参不同的槽函数。 可以通过函数指针的方式 &#xff0c;用指针指向具体…

linux环境安装mysql8.0.32

linux环境安装mysql8.0.32 一、下载安装包二、安装前准备2.1 卸载旧版本mysql2.2 检查是否安装了 mariadb 数据库2.3 安装依赖包创建 mysql 用户 三、安装3.1 上传并解压安装包&#xff08;上传路径没有要求&#xff0c;一般在/usr/local&#xff09;3.2 初始化数据库3.3 注册数…

Java面试题6月

redis有哪些缓存淘汰策略 https://blog.51cto.com/u_11720620/5198874 生产环境内存溢出&#xff08;OOM&#xff09;问题处理方案 https://note.youdao.com/ynoteshare/index.html?id5cc182642eb02bc64197788c7722baae&typenote&_time1688287588653 jstack找出占用…

C++之GNU C的__attribute__((constructor))和((destructor))静态构造函数实现(一百四十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

Elasticsearch实战(二十三)---ES数据建模与Mysql对比 一对多模型

Elasticsearch实战—ES数据建模与Mysql对比实现 一对多模型 文章目录 Elasticsearch实战---ES数据建模与Mysql对比实现 一对多模型1.一对多 模型1.1 Mysql建模 2.一对多 Index ES 数据模型2.1 类似Mysql, 依旧创建两个Index索引库2.2 采用ES架构 嵌套数组模型2.3采用ES架构 冗余…

【JUC-2】Synchronized关键字相关知识

Synchronized synchronized是Java中的关键字&#xff0c;是一种同步锁。它修饰的对象有以下几种&#xff1a; 修饰一个代码块&#xff0c;被修饰的代码块称为同步语句块&#xff0c;其作用的范围是大括号{}括起来的代码&#xff0c;作用的对象是调用这个代码块的对象&#xf…

【C++2】进程 信号 dbus

文章目录 1.进程&#xff1a;fork()&#xff0c;ps -ef (同-aux) | more2.信号&#xff1a;signal&#xff08;, EXIT&#xff09;&#xff0c;jps2.1 捕捉信号&#xff1a;ctrlc&#xff1a;22.2 捕捉信号&#xff1a;kill -9&#xff1a;92.3 捕捉信号&#xff1a;kill&#…

欧几里得算法

0x00 前言 改补的内容是一点都不会少。本章来看欧几里得算法 0x01 概述 欧几里得算法又称为辗转相除法&#xff0c;指用于计算两个非负整数a和b的最大公约数。 两个整数的最大公约数是能够同时整除他们的最大的正整数。 基本原理&#xff1a;两个整数的最大公约数等于其中…

【动态规划】子数组系列(上)

子数组问题 文章目录 【动态规划】子数组系列&#xff08;上&#xff09;1. 最大子数组和1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 代码实现 2. 环形子数组的最大和2.1 题目解析2.2 算法原理2.2.1 状态表示2.2.2 …

C++2(表达式和关系运算)

目录 1.表达式基础 1.表达式基础 运算符重载&#xff0c;就是自己定义 - * / 之类的运算符怎么运算 C中的左值和右值 C语言左值在左侧&#xff0c;右值在右侧 在cpp中要复杂的多 能取到地址的表达式是左值 不能取到地址的表达式是右值 常量对象为代表的左值不能作为赋值语句的左…

【Linux】网络相关概念概述以及原理简单分析介绍

文章目录 [toc] Linux 网络概述网络发展独立模式网络互联局域网LAN 和 广域网WAN 认识 "协议"协议的分层网络协议栈OSI七层模型TCP/IP五层(四层)模型TCP/IP网络协议栈 与 操作系统 的关系 **重新以计算机的视角看待 网络协议栈 局域网内部通信原理简单介绍不同局域网…

mybatis web使用02

处理 transfer 请求的 servlet package com.wsd.web;import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRe…

GAMES101 笔记 Lecture08 Shading 2(Shading, Pipeline and Texture Mapping)

目录 Specular Term(高光项)Ambient Term(环境光照项)Blinn-Phong Reflection ModelShading Frequencies(着色频率)Shade each triangle(flat shading)在每个三角形上进行着色Shade each vertex (Gouraud shading)(顶点着色)Shade each pixel (Phong shading)Defining Per-Vert…

【C++详解】——哈希

目录 unordered系列关联式容器 unordered_map unordered_map的接口说明 1.unordered_map的构造 2.unordered_map的容量 3.迭代器相关 4.unordered_map的元素访问 5. unordered_map的查询 6.unordered_map的修改操作 unordered_set 性能测试 底层结构——Hash 哈希…

copula简介

二元正态copula最为重要