C++ STL学习之【vector的使用】

news2024/11/24 1:16:50

✨个人主页: Yohifo
🎉所属专栏: C++修行之路
🎊每篇一句: 图片来源

  • The power of imagination makes us infinite.
    • 想象力的力量使我们无限。

遇见彩虹,吃定彩虹


文章目录

  • 📘前言
  • 📘正文
    • 1、默认成员函数
      • 1.1、默认构造
      • 1.2、拷贝构造
      • 1.3、析构函数
      • 1.4、赋值重载
    • 2、迭代器
      • 2.1、正向迭代器
      • 2.2、反向迭代器
    • 3、容量相关
      • 3.1、大小、容量、判空
      • 3.2、空间扩容
      • 3.3、大小调整
      • 3.4、缩容
    • 4、数据访问相关
      • 4.1、下标随机访问
      • 4.2、首尾元素
    • 5、数据修改相关
      • 5.1、尾插尾删
      • 5.2、任意位置插入删除
      • 5.3、交换、清理
    • 6、相关试题
  • 📘总结


📘前言

vector 是表示可变大小数组的序列 容器,其使用的是一块 连续 的空间,因为是动态增长的数组,所以 vector 在空间不够时会扩容;vector 优点之一是支持 下标的随机访问,缺点也很明显,头插或中部插入效率很低,这和我们之前学过的 顺序表 性质很像,不过在结构设计上,两者是截然不同的

两者区别


📘正文

本文介绍的是 vector 部分常用接口

1、默认成员函数

vector 的成员变量如上图所示,就是三个指针,分别指向:

  • _start 指向空间起始位置,即 begin()
  • _finish 指向最后一个有效元素的下一个位置,相当于 end()
  • _end_of_storage 指向已开辟空间的终止位置

结构

1.1、默认构造

vector 支持三种默认构造方式

  1. 默认构造大小为 0 的对象
  2. 构造 n 个元素值为 val 的对象
  3. 通过迭代器区间构造,此时元素为自定义类型,如 stringvectorDate

默认构造

int main()
{
	vector<int> v1;	//构造元素值为 int 的对象

	vector<char> v2(10, 'x');	//构造10个值为'x'的对象

	string s = "abcedfg";
	vector<char> v3(s.begin(), s.end());	//构造 s 区间内的元素对象
	return 0;
}

结果
注:也可以直接通过 vector<int> v4 = {1, 2, 3} 的方式构造对象,不过此时调用了 拷贝构造 函数

vector<int> v4 = {1, 2, 3};	//这种构造方式比较常用,有点像数组赋初始值

1.2、拷贝构造

拷贝构造将对象 x 拷贝、构造出新对象 v拷贝构造 函数的使用方法很简单,利用一个已经存在的 vector 对象,创建出一个值相同的对象

拷贝构造

vector<int> x = { 1,2,3,4,5 };

vector<int> v(x);	//利用对象 x 构造出 v

结果
可以看到,对象 v 和对象 x 的值是一样的(copy)

注意: 调用拷贝构造时,两个对象类型需匹配,且被复制对象需存在

拷贝构造赋值重载深度拷贝 的讲究,在模拟实现 vector 时演示

1.3、析构函数

析构函数,释放动态开辟的空间,因为 vector 使用的空间是连续的,所以释放时直接通过 delete[] _start 释放即可

析构函数 会在对象生命周期 结束时自动调用,平常在使用时无需关心

析构函数

// ~vector 函数内部
delete[] _start;
_start = _finish = _end_of_storage = nullptr;

1.4、赋值重载

拷贝构造 的目的是创建一个新对象,赋值重载 则是对一个老对象的值进行 改写

赋值重载

int arr[] = { 6,6,8 };
vector<int> v1(arr, arr + (sizeof(arr) / sizeof(arr[0])));	//迭代器区间构造

vector<int> v2;	//创建一个空对象
v2 = v1;	//将 v1 的值赋给老对象 v2

赋值重载
注意: v1 对象赋值给 v2 对象后,v1 本身并不受任何影响,改变的只是 v2

赋值重载 函数有返回值,适用于多次赋值的情况

vector<int> v3 = { 9,9,9 };
v2 = v1 = v3;	//这样也是合法的,最终 v1、v2 都会受到影响

2、迭代器

迭代器 是一个天才设计,它的出现使得各种各样的容器都能以同一种方式进行 访问遍历 数据

vector 支持下标随机访问, 所以大多数情况下访问数据都是使用下标,但 迭代器 相关接口它还是有的
迭代器
vectorstring迭代器 本质上就是原生指针,比较简单,但后续容器的 迭代器 就比较复杂了

复杂归复杂,但每种 容器 的迭代器使用方法都差不多,这就是 迭代器 设计的绝妙之处

注:stringvector 的迭代器都是 随机迭代器(RandomAccessIterator),可以随意走动,支持全局排序函数 sort

迭代器分类

2.1、正向迭代器

正向迭代器即 从前往后 遍历的 迭代器
正向迭代器
利用迭代器正向遍历 vector 对象

const char* ps = "Hello Iterator!";
vector<char> v(ps, ps + strlen(ps));	//迭代器构造

vector<char>::iterator it = v.begin();	//创建该类型的迭代器

while (it != v.end())
{
	cout << *it;
	it++;
}
cout << endl;

结果
注意:

  • 迭代器在创建时,一定要先写出对应的类型,如 vector<int>,嫌麻烦可以直接用 auto 推导
  • 在使用迭代器遍历时,结束条件为 it != v.end() 不能写成 <,因为对于后续容器来说,它们的空间不是连续的,判断小于无意义
  • begin() 为第一个有效元素地址,end() 为最后一个有效元素的下一个地址

vector随机迭代器,也支持这样玩

//auto 根据后面的类型,自动推导迭代器类型
auto it = v.begin() + 3;	//这是随机的含义

2.2、反向迭代器

反向迭代器常用来 反向遍历(从后往前)容器

反向迭代器
反向遍历 vector 对象

const char* ps = "Hello ReverseIterator!";
vector<char> v(ps, ps + strlen(ps));

vector<char>::reverse_iterator it = v.rbegin();	//创建该类型的迭代器

while (it != v.rend())
{
	cout << *it;
	it++;
}
cout << endl;

结果
反向迭代器的注意点与正向迭代器一致,值得注意的是 rbegin()rend()
反向迭代
begin()end() 适用于 正向迭代器

  • begin() 为对象中的首个有效元素地址
  • end() 为对象中最后一个有效元素的下一个地址

rbegin()rend() 适用于 反向迭代器

  • rbegin() 为对象中最后一个有效元素地址
  • rend() 为对象中首个有效元素的上一个地址

注意: begin() 不能和 rend() 混用

上述 迭代器 都是用于正常 可修改 的对象,对于 const 对象,还有 cbegin()cend()crbegin()crend(),当然这些都是 C++11 中新增的语法

其他迭代器

注:对于 const 对象,存在重载版本,如 begin() const,也就是说,const 修饰的对象也能正常使用 begin()end()rbegin()rend()C++11 中的这个新语法完全没必要,可以不用,但不能看不懂


3、容量相关

下面来看看 vector 容量相关函数和扩容机制

3.1、大小、容量、判空

大小 size()
容量 capacity()
判空 empty()

这些函数对于我们太熟悉了,和 顺序表 的一模一样

接口
直接拿来用一用

vector<int> v = { 1,2,3,4,5 };
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl;
cout << "empty:" << v.empty() << endl;

结果
这几个函数都是直接拿来用的,没什么值得注意的地方

3.2、空间扩容

连续空间可扩容,像 string 一样,vector 也有一个提前扩容的函数:reserve()
输入指定容量即可扩容,常用来 提前扩容避免因频繁扩容而导致的内存碎片

下面来通过一个小程序先来简单看看 PJ 版 和 SGI 版的 默认扩容机制

vector<int> v;
size_t capacity = v.capacity();
cout << "Default capacity:" << capacity << endl;

int i = 0;
while (i < 100)
{
	v.push_back(i);	//尾插元素 i

	//如果不相等,证明出现扩容
	if (capacity != v.capacity())
	{
		capacity = v.capacity();
		cout << "New capacity:" << capacity << endl;
	}

	i++;
}

扩容机制
可以看出,PJ 版采用的是 1.5 倍扩容法,而 SGI 版直接采用 2 倍扩容法,待扩容量较小时,PJ 版会扩容更多次,浪费更多空间;但待扩容量越大时,变成 SGI 版浪费更多空间,总的来说,两种扩容方式各有各的优点

如果我们提前知道待扩容空间大小 n,可以直接使用 reserve(n) 的方式进行 提前扩容,这样一来,无论是哪种版本,最终容量大小都是一致的,且不会造成空间浪费

v.reserve(100);	//提前开辟空间

提前扩容
此时是非常节约空间的,而且不会造成很多的内存碎片

注意: n 小于等于 capacity() 时,reserve 函数不会进行操作

3.3、大小调整

与提前扩容相似的大小调整,主要调整的是 _finish

在扩容的同时对新空间进行初始化,参数2 val 为缺省值,缺省为对应对象的默认构造值

  • 自定义类型也有默认构造函数,如 int(),构造后为 0
  • 这种构造方法称为 匿名构造,后续会经常简单(很方便)

resize

vector<int> v1;
v1.resize(10);	//使用缺省值

vector<int> v2;
v2.resize(10, 6);	//使用指定值

resize
区别在于:是否指定初始化值

resizereserve

  • 两者的共同点是都能起到扩容的效果
  • resize 扩容的同时还能进行初始化,reserve 则不能
  • resize 会改变 _finish,而 reserve 不会
  • 对于两者来说,当 n 小于等于 capacity() 时,都不进行扩容操作
    • resize 此时会初始化 size()capacity() 这段空间

3.4、缩容

vector 中还提供一个了缩容函数,将原有容量缩小,但这完全没必要,以下是缩容步骤:

  • 开辟新空间(比原空间更小的空间)
  • 用原空间中的数据将新空间填满,超出部分丢弃
  • 释放原空间,完成缩容

为了一个缩容而导致的是代价是很大的,因此 不推荐缩容想要改变 size() 时,可以使用 resize 函数

缩容函数
这里就不演示这个函数了,就连官方文档上都有一个 警告标志


4、数据访问相关

连续空间数据访问时,可以通过 迭代器,也可以通过 下标,这里还是更推荐使用 下标,因为很方便;作为 “顺序表”,当然也支持访问首尾元素

4.1、下标随机访问

下标访问是通过 operator[] 运算符重载实现的

下标访问
库中提供了两个重载版本,用以匹配普通对象和 const 对象

const char* ps = "Hello";
vector<char> v(ps, ps + strlen(ps));	//迭代器区间构造
const vector<char> cv(ps, ps + strlen(ps));	//迭代器区间构造

size_t pos = 0;	//下标
while (pos < v.size())
{
	cout << v[pos];	//普通对象
	cout << cv[pos];	//const 对象
	pos++;
}
cout << endl;

结果
除了 operator[] 以外,库中还提供了一个 at 函数,实际就是对 operator[] 的封装

v.at(0);
v[0]	//两者是完全等价的

注意: 因为是下标随机访问,所以要小心,不要出现 越界 行为

4.2、首尾元素

front() 获取首元素,back() 获取尾元素

vector<int> v = { 1,1,1,0,0,0 };
cout << "Front:" << v.front() << endl;
cout << "Back:" << v.back() << endl;

结果
实际上,front() 就是返回 *_startback() 则是返回 *_finish


5、数据修改相关

vector 也可以随意修改其中的数据,比如尾部操作,也支持任意位置操作,除此之外,还能交换两个对象,亦或是清除对象中的有效元素

5.1、尾插尾删

push_back()pop_back() 算是老相识了,两个都是直接在 _finish 上进行操作

文档
这两个函数操作都很简单,不再演示

注意: 如果对象为空,是不能尾删数据的

对于已有对象数据的修改,除了赋值重载外,还有一个函数 assign(),可以重写指定对象中的内容,使用方法很像默认构造函数,但其本质又和赋值重载一样
重写
第一种方式是通过迭代器区间赋值,第二种是指定元素数和元素值赋值

5.2、任意位置插入删除

任意位置插入删除是使用 vector 的重点,因为这里会涉及一个问题:迭代器失效,这个问题很经典,具体什么原因和如何解决,将在模拟实现 vector 中解答

文档
简单演示一下用法:

int arr[] = { 6,6,6 };
vector<int> v = { 1,0 };

//在指定位置插入一个值
v.insert(find(v.begin(), v.end(), 1), 10);	//10,1,0

//在指定位置插入 n 个值
v.insert(find(v.begin(), v.end(), 0), 2, 8);	//10,1,8,8,0

//在指定位置插入一段迭代器区间
v.insert(find(v.begin(), v.end(), 8), arr, arr + (sizeof(arr) / sizeof(arr[0])));	//10,1,6,6,6,8,8,0

//删除指定位置的元素
v.erase(find(v.begin(), v.end(), 10));	//1,6,6,6,8,8,0

//删除一段区间
v.erase(v.begin() + 1, v.end());	//1

先浅浅演示一下 迭代器失效的场景

vector<int> v = { 1,2,3 };
auto it = v.end();	//利用迭代器模拟尾插

for (int i = 0; i < 5; i++)
{
	v.insert(it, 10);
	it++;	//再次使用迭代器
	i++;
}

运行(调试模式下)结果是这样的:

报错

不止 insert 的迭代器会失效,erase 的迭代器也会失效

简单来说:插入或删除后,可能导致迭代器指向位置失效,此时没有及时更新,再次使用视为非法行为

因此我们认为 vector 在插入或删除后,迭代器失效,不能再使用,尤其是 PJ 版本,对迭代器失效的检查十分严格

至于其具体原因和方法,留在下篇文章中揭晓

5.3、交换、清理

还剩下两个简单函数,简单介绍下就行了

函数

vector<int> v1 = { 1,2,3 };
vector<int> v2 = { 4,5,6 };

v1.swap(v2);	//交换两个对象

v1.clear();
v2.clear();	//清理

std 中已经提供了全局的 swap 函数,为何还要再提供一个呢?

  • 这个函数实现原理不同 std::swapstd::swap 实际在交换时,需要调用多次拷贝构造和赋值重载函数,对于深拷贝来说,效率是很低的
  • vector::swap 在交换时,交换是三个成员变量,因为都是指针,所以只需要三次浅拷贝交换,就能完美完成任务
  • 实际在 vector::swap 内部,还是调用了 std::swap,不过此时是高效的浅拷贝

至于 clear 函数,实现很简单:

  • _finish 等于 _start,就完成了清理,不需要进行缩容,这样做是低效的

关于 vector 更多、更详细的内容,欢迎移步 《C++ STL学习之【vector的模拟实现】》


6、相关试题

光知道怎么使用是不够的,还需要将知识付诸于实践,切记纸上谈兵

下面是一些比较适合练习使用 vector 的试题,可以做做

vector 值得做的题目


📘总结

以上就是本次关于 STLvector 的全部讲解了,vector 相对来说函数比较少,也比较好理解,不过在实际使用中,会存在不少问题,需要对 vector 的不断使用以提高认知,如果对 vector 剩余函数感兴趣,可以阅读官方文档 vector

如果你觉得本文写的还不错的话,可以留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


星辰大海

相关文章推荐

STL 之 string 类

C++ STL学习之【string类的模拟实现】

C++ STL 学习之【string】

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

内存、模板

C++【模板初阶】

C/C++【内存管理】

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

类和对象实操

类和对象实操之【日期类】

感谢支持

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

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

相关文章

STM32之SPI

SPISPI介绍SPI是串行外设接口(Serial Peripherallnterface)的缩写&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线&#xff0c;并且在芯片的管脚上只占用四根线&#xff0c;节约了芯片的管脚&#xff0c;同时为PCB的布局上节省空间&#xff0c;提供方便…

蓝桥杯嵌入式(G4系列):定时器捕获

前言&#xff1a; 定时器的三大功能还剩下最后一个捕获&#xff0c;而这在蓝桥杯嵌入式开发板上也有555定时器可以作为信号发生器供定时器来测量。 原理图部分&#xff1a; 开发板上集成了两个555定时器&#xff0c;一个通过跳线帽跟PA15相连&#xff0c;最终接到了旋钮R40上&…

STM32F103CubeMX定时器

前言定时器作为最重要的内容之一&#xff0c;是每一位嵌入式软件工程师必备的能力。STM32F103的定时器是非常强大的。1&#xff0c;他可以用于精准定时&#xff0c;当成延时函数来使用。不过个人不建议这么使用&#xff0c;因为定时器很强大&#xff0c;这么搞太浪费了。如果想…

Zookeeper的Java API操作

Zookeeper的Java API操作一、先启动Zookeeper集群二、IDEA 环境搭建三、创建子节点四、获取子节点并监听节点变化五、判断 Znode 是否存在六、Watcher工作流程一、先启动Zookeeper集群 二、IDEA 环境搭建 1.创建一个Maven工程&#xff1a;ZookeeperProject 2.在pom.xml文件添…

ARM uboot 的移植4 -从 uboot 官方标准uboot开始移植

一、添加DDR初始化1 1、分析下一步的移植路线 (1) cpu_init_crit 函数成功初始化串口、时钟后&#xff0c;转入 _main 函数&#xff0c;函数在 arch/arm/lib/crt0.S 文件中。 (2) 在 crt0.S 中首先设置栈&#xff0c;将 sp 指向 DDR 中的栈地址&#xff1b; #if defined(CONF…

CNCF x Alibaba云原生技术公开课 【重要】第九章 应用存储和持久化数据卷:核心知识

1、Pod Volumes 场景 同一个pod中的某个容器异常退出&#xff0c;kubelet重新拉起来&#xff0c;保证容器之前产生数据没丢同一个pod的多个容器共享数据 常见类型 本地存储&#xff0c;常用的有 emptydir/hostpath&#xff1b;网络存储&#xff1a;网络存储当前的实现方式有两…

2021年我国半导体分立器件市场规模已达3037亿元,国内功率半导体需求持续快速增长

半导体分立器件是由单个半导体晶体管构成的具有独立、完整功能的器件。例如&#xff1a;二极管、三极管、双极型功率晶体管(GTR)、晶闸管(可控硅)、场效应晶体管(结型场效应晶体管、MOSFET)、IGBT、IGCT、发光二极管、敏感器件等。半导体分立器件制造&#xff0c;指单个的半导体…

proteus I2C Debugger 查看 AT24C02写入读取

I2C Debugger仪器&#xff0c;在仿真调试期中&#xff0c;该仪器可以显示I2C数据传送时间、S&#xff08;START状态&#xff09;、Sr(ReStart状态&#xff09;、A&#xff08;Ask响应&#xff09;、N &#xff08;No ask状态&#xff09;、P&#xff08;Stop状态&#xff09;、…

中值滤波+Matlab仿真+频域响应分析

中值滤波 文章目录中值滤波理解中值滤波的过程Matlab 实现实际应用频域分析中值滤波是一种滤波算法&#xff0c;其目的是去除信号中的噪声&#xff0c;而不会对信号本身造成太大的影响。它的原理非常简单&#xff1a;对于一个给定的窗口大小&#xff0c;将窗口内的数值排序&…

【C++进阶】四、红黑树(三)

目录 一、红黑树的概念 二、红黑树的性质 三、红黑树节点的定义 四、红黑树的插入 五、红黑树的验证 六、红黑树与AVL树的比较 七、完整代码 一、红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可…

嵌入式安防监控项目——html框架分析和环境信息刷新到网页

目录 一、html控制LED 二、模拟数据上传到html 一、html控制LED 简单来说就是html给boa服务器发了一个控制指令信息&#xff0c;然后boa转发给cgi进程&#xff0c;cgi通过消息队列和主进程通信。主进程再去启动LED子线程。 这是老师给的工程。 以前学32都有这工具那工具来管…

导航技术调研(CSDN_0023_20221217)

文章编号&#xff1a;CSDN_0023_20221217 目录 1. 惯性导航 2. 组合导航技术 3. 卡尔曼滤波 1. 惯性导航 惯性导航系统(INS-Inertial Navigation System)是上个世纪初发展起来的。惯性导航是一种先进的导航方法&#xff0c;但实现导航定位的原理却非常简单&#xff0c;它是…

RHCSA-用户和组管理和文件系统权限(3.11)

目录 用户&#xff08;UID&#xff09; 用户类别&#xff08;UID&#xff09;&#xff1a; 用户的增删改查&#xff1a; 修改用户密码&#xff1a; 查看用户是否存在&#xff1a; 组&#xff08;GID&#xff09; 组的增删改查&#xff1a; 设置组密码&#xff1a; 用户…

idea集成GitHub

设置 GitHub 账号绑定账号有两种方式&#xff1a;1. 通过授权登录2.如果上述登录不成功&#xff0c;用Token口令的方式登录&#xff0c;口令在github账号哪里生成&#xff0c;点击settings --->Developer settings --->pwrsonal access tokens ----> 复制口令到idea 口…

设置cpp-httplib 服务器模式模式下的线程池大小 以及如何增加默认处理函数 以便能够实现http请求转发

先说说默认的创建的线程池数量 原因是某天调试在gdb调试下 一启动程序发现 开启了好多线程 如下图 因为我们程序 没几个线程 数了下 居然有60多个线程 不需要那么多 所以看下 httplib的源码 构造函数的时候 设置了最大线程池数量 看下这个宏 然后打印了下 发现 居然那么大 …

FusionCompute安装和配置步骤

1. 先去华为官网下载FusionCompute的镜像 下载地址&#xff1a;https://support.huawei.com/enterprise/zh/distributed-storage/fusioncompute-pid-8576912/software/251713663?idAbsPathfixnode01%7C22658044%7C7919788%7C9856606%7C21462752%7C8576912 下载后放在D盘中&am…

【rabbitmq 实现延迟消息-插件版本安装(docker环境)】

一&#xff1a;插件简介 在rabbitmq 3.5.7及以上的版本提供了一个插件&#xff08;rabbitmq-delayed-message-exchange&#xff09;来实现延迟队列功能。同时插件依赖Erlang/OPT 18.0及以上。 二&#xff1a;插件安装 1&#xff1a;选择适合自己安装mq 版本的插件&#xff1…

设计模式---抽象工厂模式

目录 1 介绍 2 优缺点 3 实现 1 介绍 抽象工厂模式(Abstract Factory Pattern) 是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 在抽象工厂模式中&#xff0c;接口是负…

ROC和AUC

目录 ROC AUC ROC ROC曲线是Receiver Operating Characteristic Curve的简称&#xff0c;中文名为"受试者工作特征曲线"。ROC曲线的横坐标为假阳性率(False Postive Rate, FPR)&#xff1b;纵坐标为真阳性率(True Positive Rate, TPR).FPR和TPR的计算方法分别为 F…

Spring——案例-业务层接口执行效率和AOP通知获取数据+AOP总结

执行时间获取:记录开始时间和结束时间&#xff0c;取差值。 这里使用环绕通知来实现。 环境准备: 项目文件结构: 业务层接口和实现类: 数据层: 采用mybatis注解开发&#xff0c;这里没有实现类&#xff0c;直接在接口方法里面实现映射。 domain层: 实现了数据库里面每一个…