learn C++ NO.12——vector

news2025/7/11 10:54:14

前言

不知何时开始产生了不更新博客的习惯,开始编程学习也过了两年多了。恍惚了一个阶段,我觉得是时候恢复博客产出了,我认为写博客是一种好的学习的方式,不仅可以让你对已经学习过的知识又一遍回顾,还记录了你编程学习的每一步,同时也是正向激励自己的方式。话不多说,本篇文章主要介绍STL中的一个高频使用的容器vector。我会从vector的使用入手,然后简单模拟实现一下vector,以更好的了解这种容器。下面进入正文

使用vector

什么是vector?

首先,vector 这个单词有向量的意思。它是STL中的一个容器,它的本质是一个动态数组。然后,由于它是一个模板类,所以它是支持存储任意类型数据的,它也提供了许多操作它的接口,如push_back,insert等等。vector提供了类似于数组类似的访问方式,它可以支持对容器任意位置数据进行随机访问的。

为什么需要使用vector

  1. vector提供了一种方便且高效的动态数组的实现,开发者在使用这种容器时,它会根据实际需求开辟好堆区的空间来存储数据。
  2. 它适用于频繁尾插尾删的场景,在尾插尾删操作的时间复杂度为o(1)。但是,头插头删的效率比较差,时间复杂度为O(N),因为数据挪动是有代价的。所以在实际使用vector的过程中需要注意,尽量避免频繁地头插头删。
  3. vector支持像数组一样的随机访问方式。所以,在一些需要频繁随机访问的场景下,它的效率是极好的。时间复杂度为O(1)。
  4. 由于c++支持运算符重载,使得vector的使用语法可以像原生数组一样,所以它的学习成本其实不是很高。它的能力又要比原生数组强,vector还是非常好用的。

如何使用vecotr

首先,在正式使用vector前都是需要包含头文件的。下面我们就通过在线文档加上一些代码演示的方式,来进行vector使用的学习。

简单定义一个vector对象

在简答定义一个vector对象前,我们需要知道vector其实是有两个模板参数的,一个是类型模板参数,另一个是空间配置器模板参数(这里不做介绍)。
在这里插入图片描述
在这里插入图片描述

使用库提供的方法操作vector对象

在这里插入图片描述
这里我就以常用的接口来进行演示。

void test_vector1()
{
    vector<int> v;//默认构造
    vector<int> v1;
    v1.reserve(10); //直接扩容至可以存储10个元素的大小
    //reserve可以避免频繁扩容带来的性能损耗
    v.push_back(1);//在vector的尾部插入数据
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);

    for(int i = 0; i < v.size(); i++)//size()获取有效长度
    {
        cout << v[i] << " ";//支持[下标]的方式随机访问
    }
    cout << endl;

    v.pop_back();//尾删数据


    v[0] += 10; //支持类似于数组的操作方式
    v[1]++;



}


由于STL的容器都是支持迭代器进行访问数据的,下面就简单给大家介绍一下vector迭代器的使用。以及常用相关操作的接口

void test_vector2()
{
    int arr[] = {1,2,3,4,5,6};
    //vector也支持迭代器区间初始化
    vector<int> v(arr,arr+(sizeof(arr) / sizeof(int)));

    //迭代器的简单使用
    vector<int>::iterator vit = v.begin();//将vector其实位置
    while(vit != v.end())
    {
        cout << (*vit) << " ";
        ++vit;
    }
    cout << endl;

    //支持迭代器就支持范围for
    for(auto& e : v)
    {
        cout << e << " ";
    }
    cout << endl;

    int tmp[] = {3,1,6,8,9,10};
    //用inset插入一段迭代器区间
    v.insert(v.end(),tmp,tmp+((sizeof(tmp)/ sizeof(int))));

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

    //使用erase删除一段区间
    v.erase(v.begin(), v.begin() + 3);


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

下面简单介绍一下反向迭代器的使用,以及resize接口。

void test_vector3()
{
    vector<int> v1(10);//初始化10个元素,默认为0
    vector<int> v2(10,2);//初始化10个2,
    vector<int> v3;
    v3.resize(10,4);//使用resize接口,调整vector的有效长度,resize会先调用reserve扩容,第二个参数不给默认值  为0。
    
    auto rit = v1.rbegin();
    while(rit != v1.rend())
    {
        cout << (*rit) << " ";
        ++rit;
    }
    cout << endl;

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

    for(auto& e : v3)
    {
        cout << e << " ";
    }
    cout << endl;
}
迭代器失效的问题

迭代器失效是指在对容器进行插入或删除等操作后,原本指向容器元素的迭代器可能变得无效,不能再用于安全访问容器元素。这是因为这些操作可能导致容器的内存重新分配,元素的位置发生变化,从而使先前获得的迭代器指向的位置不再有效。
下面请看样例

#include <iostream>
#include <vector>

int main() {
    vector<int> myVector = {1, 2, 3, 4, 5};

    // 迭代器指向第三个元素
    vector<int>::iterator it = myVector.begin() + 2;

    // 在第三个元素之后插入一个新元素
    myVector.insert(it + 1, 99);

    // 此时,原来的迭代器 it 可能已经失效

    // 尝试使用失效的迭代器
    cout << "Value at original iterator position: " << *it << endl; 
    // 这可能导致未定义的行为

    return 0;
}

下面看看在不同环境下这段代码的结果是什么?
在这里插入图片描述
在这个例子中,我们插入一个新元素后,原来指向第三个元素的迭代器 it 可能已经失效,因为插入元素可能导致容器重新分配内存,原来的元素位置发生变化。因此,尝试使用失效的迭代器可能导致程序行为不可预测,甚至崩溃。

避免迭代器失效问题的常见方法

避免迭代器失效问题的关键在于在可能导致迭代器失效的操作后,重新获取迭代器。以下是一些常见的方法:

  1. 使用返回值: 在执行可能导致迭代器失效的操作(如插入或删除元素)后,重新获取迭代器,而不是继续使用原来的迭代器。

    std::vector<int>::iterator it = myVector.begin() + 2;
    it = myVector.insert(it + 1, 99); // 重新获取迭代器
    
  2. 使用下标访问而非迭代器: 使用下标访问而不是迭代器进行元素的访问和操作。下标是相对稳定的,而迭代器可能在插入或删除操作后失效。

    std::vector<int> myVector = {1, 2, 3, 4, 5};
    size_t index = 2;
    
    myVector.insert(myVector.begin() + index + 1, 99);
    
    // 使用下标而非迭代器
    std::cout << "Value at position " << index << ": " << myVector[index] << std::endl;
    
  3. 避免在循环中修改容器: 如果在循环中对容器进行修改,确保重新获取迭代器或使用下标,以防止迭代器失效。

    for (std::vector<int>::iterator it = myVector.begin(); it != myVector.end(); ++it) {
        // 避免在此处插入或删除元素,或者确保重新获取迭代器
    }
    
  4. 使用反向迭代器: 如果你知道要在容器尾部进行插入或删除操作,可以考虑使用反向迭代器。这样,在插入或删除元素后,迭代器仍然指向相同的位置。

    std::vector<int>::reverse_iterator rit = myVector.rbegin();
    myVector.insert(rit.base(), 99); // 使用 base() 获取插入位置
    

通过谨慎使用迭代器和在可能的地方重新获取迭代器,可以有效地避免迭代器失效问题。

模拟实现vector

简单描述vector的结构

在这里插入图片描述

这里通过模拟实现一份简易版本的vector来学习它,以加深对它的了解。我们以SGI版本为原型进行模拟实现。通过上图可以了解到vector的成员有三个,分别是start,它用于标识vector容器的起始地址空间。以及finish用于标识最后一个数据的下一个地址空间。而end_of_storage用于标识当前容器申请到有效空间的最后一个地址空间,用于标识当前容器的容量。根据对这三个成员变量的了解,我们可以推断出size(),capacity()等成员函数的实现。size()其实就是finish - start。而capacity()等于end_of_storage - start。

下面就搭一个简单的vector

在这里插入图片描述

push_back()的实现

为了验证上面写的功能没有问题,需要再实现一个插入接口,插入数据验证一下。就实现一个push_back()接口来验证。push_back()实现思路如下,首先我们需要判断一下当前容器的空间是否还足够,不够的话我们就要扩容。然后再finish的位置上插入数据,让finish往后移一步即可。
在这里插入图片描述

如何扩容呢?我们需要调用函数reserve()进行扩容。

reserve(size_t n)函数实现思路如下,我们首先需要判断一下参数是否是有效参数。因为n如果小于当前的容量,处于对性能的考虑,reserve()不提供缩小容量。所以我们只考虑扩容的情况。
接着需要开辟空间,用于这里实现的是一份简单的vector,我们就先不考虑使用空间配置器,而是使用new开辟新空间。若start指向非空地址(默认构造出来的指向空地址)说明需要将旧数据挪到新空间上。最后更新一下成员变量即可。
在这里插入图片描述

下面分析一份经典的错误代码
在这里插入图片描述
这里的问题在于最后修改成员变量finish的时候。因为size()接口的实现是通过finish - start。而在这里修改成员变量finish的时候,恰巧是第一次调用,由于finish一开始值是nullptr。这里调用size()就会造成nullptr - tmp的情况导致程序崩溃。解决方案可以是提前保存size到局部变量中。另一个解决方案是先修改finish这个成员变量再修改start,并配上相应的注释会比较好。

关于memcpy导致深拷贝类型出错的问题。由于memcpy是浅拷贝,下一步的delete会去依次调用深拷贝对象函数的析构函数,在释放整体空间,进而导致出错。所以为了避免T为深拷贝类型而导致的问题,应该去调用深拷贝类型的复制重载。这样才能避免memcpy带来的问题。

在这里插入图片描述

operator[]实现

[]操作符使得对vector的操作就像操作原生数组那样。这里需要对下标做一个处理,避免越界情况。当然,还需要实现一个const版本的[]以供const对象使用

在这里插入图片描述

insert接口的实现

先看一下insert接口
在这里插入图片描述

整体的实现思路如下,首先,判断pos位置合法性。然后,对容量进行判断,需要注意的是扩容可能引发迭代器失效问题。为了解决迭代器失效问题,我们需要先保存pos位置相较于start的偏移量,扩容后,修改新的pos即可。最后,就是从最后一个位置开始挪动数据,完成插入数据。

下面就以插入单个数据为例实现一份insert接口

在这里插入图片描述

在insert()内部迭代器失效的问题解决了,并不代表用户在使用的时候会规范。下面演示一个经典的迭代器失效的场景。
在这里插入图片描述

避免insert后迭代器可能因为扩容而失效,我们可以在调用insert插入数据时,接受它的返回值已更新迭代器。这样可以有效避免迭代器失效问题。

erase接口的实现

以删除某个位置的值为例,实现erase接口的思路如下,先判断pos位置的合法性。然后将pos位置之后的数据从后向前覆盖。最后返回pos位置的迭代器。

在这里插入图片描述

下面聊一聊关于erase()使用后需要注意的细节。当迭代器传入erase函数后,无论如何都不应该再去使用它。因为这样的行为是高危险的。在vs平台下,这样的行为一律都会被强制报错。而在g++平台下,特定场景也会导致程序崩溃。所以,只要是erase失效的迭代器都不应该再去使用它。

由于vs平台只要访问失效迭代器一律报错,这里我就以g++平台为例,带大家看一个在g++平台下,访问失效迭代器导致程序段错误的场景。

在这里插入图片描述

上图是一个删除容器中所有偶数的算法。为什么会导致崩溃呢?因为在调用erase后,it已经失效,如果两个偶数连着势必导致漏删其中一个。最致命的问题是当最后一个数为偶数时,由于迭代器失效,会导致finish和it永远不会相遇,进而导致程序崩溃错误。
在这里插入图片描述

规范使用insert和erase

在写纯c++代码时,我们需要考虑代码的可移植性和健壮性。但是,不可避免的是不同平台的差异,如vs平台对于迭代器失效问题是我认为比价好的,它并不会让你去访问已经使用过的迭代器。这样能避免你访问失效迭代器而导致未知的错误(标准为定义)。而使用g++这种比较“自由”的平台时,更应该规范使用迭代器,因为时错时不错的情况有时候反而更折磨人。

resize的实现

在这里插入图片描述
resize用于调整当前vector的有效元素个数。先谈一谈第二个参数为什么是这样设计的。value_type是一个模板类型,为了使用的时候便捷,第二个参数是提供了默认参数,即当调用resize函数编译器会调用这个参数提供的默认构造。如我们初始化的是string类,就会去调用string类的默认构造。如果value_type类型是内置类型呢?由于模板的出现,编译器其实也为内置类型提供了默认构造函数。所以像int、char等内置类型也完全不用担心兼容性问题。

在这里插入图片描述

拷贝构造的实现

先介绍一下现代写法的实现思路。用reserve()函数开辟被拷贝的对象的容量大小的空间,然后依次插入被拷贝对象的数据即可。
在这里插入图片描述
传统写法实现思路如下,先开辟新空间,然后用memcpy拷贝一下被拷贝对象的数据,最后修改一下成员变量即可。
在这里插入图片描述

现代写法需要依赖reserve和push_back()接口,相较于传统写法比较清爽。传统写法则不需要依赖自己实现reserve()和push_back(),。实现逻辑其实都是开辟空间,然后拷贝数据,只是实现方式不同。个人认为两者掌握其一即可。

operator=重载的实现

我们采用现代写法实现
在这里插入图片描述
用于调用operator=时,形参会拷贝一份实参。我们实现一份swap调用库里面的swap交换成员变量后,返回*this即可。这是一种比较通用的写法,不仅vector可以这么写,像list、string等容器都可以这样实现。

迭代器区间初始化构造函数

实现思路就是使用循环不停push_back()元素即可。唯一需要注意的是,我们需要提供一个vector(int n, const T& val = T())的重载,这样可以避免调用构造函数时,指向不够明确导致程序编译错误的问题。
在这里插入图片描述

总结

vector是一个能够自动增长容量的线性表,它比原生数组的功能强大,但是又可以像原生数组一样去对它进行操作。这也意味着像二维数组等场景下,原生数组操作比较繁琐的问题。相应的vector也有迭代器失效的问题需要我们注意。操作迭代器去insert和erase后,如果还需要用该迭代器就需要获取返回值更新迭代器,以避免迭代器失效问题。

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

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

相关文章

2024年全国大学生数学建模竞赛(A题) 建模解析|“板凳龙” 闹元宵|小鹿学长带队指引全代码文章与思路

我是鹿鹿学长&#xff0c;就读于上海交通大学&#xff0c;截至目前已经帮200人完成了建模与思路的构建的处理了&#xff5e; 本篇文章是鹿鹿学长经过深度思考&#xff0c;独辟蹊径&#xff0c;实现综合建模。独创复杂系统视角&#xff0c;帮助你解决国赛的难关呀。 完整内容可以…

React学习-hooks

官方文档&#xff1a;https://zh-hans.react.dev/reference/react/useActionState 1.useEffect useEffect(setup, dependencies?) 1.1 基础使用 //hooks import { useEffect } from "react"; import "./App.css";function App(){useEffect(()>{con…

kaggle注册收不到验证码、插件如何下载安装

综合这三个来看&#xff0c; 1.插件下载用的大佬给的分享链接 2.下载好压缩包以后需要解压缩 Header Editor插件网盘下载安装教程 - 哔哩哔哩 (bilibili.com) 3.安装插件时没找到crx文件&#xff0c;在浏览器插件界面点击“加载解压缩的扩展” 4.复制网址到插件里&#xff…

基于IDEA快速重构代码的几种方式

文章目录 1. 方法重构1.1 操作方式1.2 优化方式 2. 类重构2.1 操作方式2.2 优化方式 3. 基于代码分析重构3.1 操作方式 4 基于重复代码重构4.1 操作方式 本篇主要基于AI工具 TONGYI Lingma, 静态代码分析工具 Qodana 进行讨论 1. 方法重构 当你对某个封装的方法不满意时, 借助…

在js渲染的dom中的事件中传递对象

在某些情况下&#xff0c;可能需要将整个对象或部分对象嵌入到 HTML 元素的属性中&#xff0c;可以将对象数据序列化为 JSON 字符串&#xff0c;存储在 data-* 自定义属性中。这样可以在事件中取出并解析对象数据&#xff1a; <!DOCTYPE html> <html lang"en&qu…

Ubuntu Python与GitHub API 交互,获取仓库更新信息

1. 获取 GitHub 个人访问令牌 登录 GitHub &#xff0c;首先使用帐户登录 GitHub 在 GitHub 页面右上角点击头像&#xff0c;然后选择 “Settings” 在左侧菜单栏滚动到最下方&#xff0c;找到并点击 “Developer settings” 在 “Developer settings” 页面中&#xff0c…

【笔记】数据结构08

文章目录 最小堆的构建 另一个树的子树找树左下角的值 折半查找的平均查找长度[顺序表] 画出折半查找树将关键字按完全二叉树形式画出查找树 l o g 2 n 1 log_2n1 log2​n112个结点画出高度为4的查找树 1层1个节点比较1次&#xff0c;2层2个节点比较2次&#xff0c;3层4个节点…

2024年高教杯国赛(A题)碰撞检测模型+最优螺距|数学建模竞赛解题思路|完整代码论文集合

我是Tina表姐&#xff0c;毕业于中国人民大学&#xff0c;对数学建模的热爱让我在这一领域深耕多年。我的建模思路已经帮助了百余位学习者和参赛者在数学建模的道路上取得了显著的进步和成就。现在&#xff0c;我将这份宝贵的经验和知识凝练成一份全面的解题思路与代码论文集合…

计算机,数学,AI在社会模拟中的应用

国家智囊团会使用社会模拟器来预测社会动向和一些问题的涌现&#xff0c;亚洲社会仿真学会&#xff08;ASSA&#xff09;最近在武汉成立&#xff0c;旨在推动大型社会模拟器的研发和应用。 未来随着计算机算力的提升以及人工智能的进化&#xff0c;我们每个人都可能在计算机中被…

如何通过学习英语,打出人生的‘王炸’?

如何通过学习英语&#xff0c;打出人生的‘王炸’&#xff1f; 每个人都渴望一张“王炸”牌&#xff0c;能瞬间改变人生的走向。有的人通过天赋&#xff0c;有的人依靠机遇&#xff0c;而今天我们要探讨的&#xff0c;是通过学习英语打出人生“王炸”的真实故事。英语&#xf…

DB2创建数据库-创建用户-赋值权限-导入数据

这篇文章主要内容以下几点&#xff1a; 1&#xff09;创建数据库 2&#xff09;创建用户 3&#xff09;给用户赋权限 4&#xff09;导入准备好的建表语句和数据 一、创建数据库 安装DB2数据库软件&#xff0c;在我另一篇文章写有&#xff0c;自己查看。这里不讲解安装。假设你…

react 使用Ant Design中DatePicker设置mode=“year“无法获取value

一、问题描述 <DatePicker placeholder"请选择年份" mode"year" onChange{this.onChange}/>// 使用上边代码&#xff0c;界面呈现出只有年份的选择器。但是&#xff0c; onChange 事件根本不会触发&#xff0c;获取不了值&#xff01;二、解决办…

【系统架构设计师-2011年】综合知识-答案及详解

更多内容请见&#xff1a; 备考系统架构设计师-核心总结索引 文章目录 【第1题】【第2~4题】【第5~7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17题】【第18~19题】【第20~21题】【第22题】【第23题】【第24题】【第2…

【PyQt6 应用程序】解说+原声视频混剪无显卡精简版,无显卡可用

在当今视频内容创作日益繁荣的时代,利用自动化工具进行视频编辑和二次创作已成为提高生产效率和创作水平的重要手段。本文将介绍如何使用PyQt6创建一个应用程序,该程序能够自动提取视频中的解说和原声部分,并使用人工智能对解说进行二次创作,从而生成具有独特风格的新视频内…

[网络原理]关于网络的基本概念 及 协议

文章目录 一. 关于网络的概念介绍1. 局域⽹LAN2. ⼴域⽹WAN3. 主机4. 路由器5. 交换机IP地址端口号 二. 协议协议分层TCP/IP五层模型(或四层)OSI七层模型封装分用 一. 关于网络的概念介绍 1. 局域⽹LAN 局域⽹&#xff0c;即 Local Area Network&#xff0c;简称LAN。 Local …

会声会影2024是一款功能强大的专业视频制作软件及会声会影字体怎么安装

字幕在视频中扮演着多重角色&#xff0c;对于不同类型的观众都有重要意义。通过提供文字信息&#xff0c;字幕帮助观众更好地理解视频内容&#xff0c;特别是对于那些听力受损或不懂视频语言的观众来说&#xff0c;字幕是他们获取信息的重要途径。这篇文章就一起来看看会声会影…

面试高频:Mysql事务

在MySQL中&#xff0c;事务是用于管理数据库操作的一组语句&#xff0c;它们作为一个整体执行&#xff0c;以确保数据的一致性和完整性。事务提供了一种机制&#xff0c;可以将多个操作合并为一个原子单元&#xff0c;这样要么所有操作都成功执行&#xff0c;要么在出现错误时&…

HarmonyOS开发实战( Beta5版)合理使用动画丢帧规范实践

本文列举了部分用于优化动画时延的正反案例&#xff0c;帮助开发者在遇到相似场景时进行优化&#xff0c;解决构建页面动画时遇到动画时延较长的问题。 减少动画丢帧 在播放动画或者生成动画时&#xff0c;画面产生停滞而导致帧率过低的现象&#xff0c;称为动画丢帧。 播放…

《Learning To Count Everything》CVPR2021

摘要 论文提出了一种新的方法来解决视觉计数问题&#xff0c;即在给定类别中仅有少量标注实例的情况下&#xff0c;对任何类别的对象进行计数。将计数问题视为一个少样本回归任务&#xff0c;并提出了一种新颖的方法&#xff0c;该方法通过查询图像和查询图像中的少量示例对象…

【Linux高级命令】2_查看日志命令(重点)

文章目录 一、查看日志文件1.1 日志文件1.2 查看日志信息&#xff1a;head1.3 查看日志信息&#xff1a; tail【重点】1.4 工作中的使用场景 二、查看操作系统信息 一、查看日志文件 1.1 日志文件 就是一个文本文件 里面记录了程序运行的信息 作用 通过最后几行信息&#xff…