深入理解C++ STL中的 vector

文章目录

  • 1. vector 的概述
    • 1.1 vector 是什么?
    • 1.2 vector 的优点
    • 1.3 vector 的缺点
  • 2. vector 的基本使用
    • 2.1 vector 的定义
    • 2.2 基本操作
    • 2.3 示例
    • 2.4 迭代器的使用
  • 3. vector 的内部实现原理
    • 3.1 动态数组的实现
    • 3.2 内存管理
    • 3.3 内存扩展策略
    • 3.4 元素的插入与删除
      • 3.4.1 尾部插入与删除
      • 3.4.2 中间或头部插入与删除
  • 4. vector 高级功能
    • 4.1 复制与赋值
    • 4.2 移动语义与 std::move
    • 4.3 vector 的初始化列表(C++11)
    • 4.4 emplace_back() 与 push_back() 的区别
    • 4.5 shrink_to_fit() 减少容量浪费
  • 5. vector 的常见应用场景
    • 5.1 动态数组
    • 5.2 堆栈
    • 5.3 动态队列
    • 5.4 2D 矩阵的实现
  • 6. vector 的复杂度分析
    • 6.1 时间复杂度
    • 6.2 空间复杂度
  • 7. vector 的常见问题和陷阱
  • 7.1 容量浪费问题
  • 7.2 迭代器失效(最需要注意的地方)
  • 7.3 元素的析构

前言: C++ Standard Template Library (STL) 是一个强大且灵活的库,提供了许多有用的数据结构和算法,其中vector 是最常用的容器之一。
vector是动态数组的封装,可以在运行时自动调整大小,提供了数组的效率以及更多的功能和灵活性。
在本文中,我们将深入讨论 vector的特性、使用方法、底层实现及其复杂性分析。

1. vector 的概述

vector文档的链接:http://www.cplusplus.com/reference/vector/vector/

1.1 vector 是什么?

vector 是 C++ STL 中一种顺序容器(sequence container),其底层实现基于动态数组。与普通数组不同的是,vector 可以根据需要动态扩展其大小,即它能够存储任意数量的元素,而不需要在创建时指定一个固定的大小。此外,vector 提供了丰富的成员函数,可以方便地对元素进行插入、删除、遍历、查找等操作。

1.2 vector 的优点

动态扩展:vector 能够自动调整大小,避免了固定大小数组带来的内存不足问题。
高效的随机访问:由于 vector 底层是连续的内存块,因此它可以像数组一样通过索引进行快速的随机访问。
灵活的插入和删除:vector 支持高效的尾部插入和删除操作,且提供了多种插入、删除方式。
STL 兼容性:vector 是 STL 容器,支持 STL 的算法和迭代器,可以与其他 STL 容器和算法无缝结合。

1.3 vector 的缺点

头部和中间的插入、删除效率低:由于 vector 使用连续的内存块,因此在中间或头部插入或删除元素时,需要移动大量元素,时间复杂度为 O(n)。 内存浪费:为了提高扩展效率,vector 通常会预留比实际需要更多的内存,这可能导致内存浪费。

2. vector 的基本使用

在使用 vector 时,我们需要包含头文件 。下面是一些 vector 的基本使用方法和常见操作:

2.1 vector 的定义

#include <vector>std::vector<int> vec;          // 定义一个存储整数的空vector
std::vector<int> vec(10);      // 定义一个包含10个元素的vector,元素值默认初始化为0
std::vector<int> vec(10, 5);   // 定义一个包含10个元素的vector,元素值为5
std::vector<int> vec2 = vec;   // 定义一个新vector,并通过拷贝构造函数初始化

2.2 基本操作

push_back():在 vector 的末尾插入元素。
pop_back():删除 vector 末尾的元素。
size():返回 vector 中元素的数量。
capacity():返回 vector 当前的容量,即它在不重新分配内存的情况下最多可以容纳的元素数。
clear():清空 vector 中的所有元素。
empty():判断 vector 是否为空。
resize():调整 vector 的大小。
reserve():为 vector 预留一定的容量,避免频繁的重新分配内存。

2.3 示例

#include <iostream>
#include <vector>int main() {std::vector<int> vec;// 向vector中添加元素vec.push_back(1);vec.push_back(2);vec.push_back(3);// 输出vector的元素for (int i = 0; i < vec.size(); ++i) {std::cout << vec[i] << " ";}std::cout << std::endl;// 删除最后一个元素vec.pop_back();// 检查是否为空if (!vec.empty()) {std::cout << "The vector is not empty!" << std::endl;}return 0;
}

2.4 迭代器的使用

迭代器是一种用于遍历 vector 元素的工具。vector 提供了多种迭代器,包括 begin() 和 end()。

std::vector<int> vec = {1, 2, 3, 4, 5};// 使用迭代器遍历
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {std::cout << *it << " ";
}
std::cout << std::endl;

也可以使用 C++11 的范围 for 循环来遍历:

for (int val : vec) {std::cout << val << " ";
}

3. vector 的内部实现原理

3.1 动态数组的实现

vector 的底层实现基于动态数组。当需要插入新元素时,如果当前容量不足,vector 会自动分配更大的内存块,并将原来的元素拷贝到新的内存块中。这种动态扩展策略的时间复杂度较低,因为 vector 的容量在每次扩展时通常是当前容量的两倍。

std::vector<int> vec;
vec.push_back(1);  // 第一次插入,分配内存
vec.push_back(2);  // 插入,内存足够,不需要重新分配
vec.push_back(3);  // 插入,内存足够,不需要重新分配
// 当容量不足时,vector 会重新分配内存,通常是原来容量的两倍。

3.2 内存管理

vector 通过 capacity 来控制当前分配的内存大小,而 size 表示实际存储的元素数量。capacity 总是大于或等于 size。当 size 超过 capacity 时,vector 会重新分配内存,并将所有现有元素拷贝到新的内存地址。

std::vector<int> vec;
vec.reserve(10);  // 预先分配10个元素的内存
vec.push_back(1); // 不会触发内存重新分配
vec.push_back(2); // 不会触发内存重新分配

通过使用 reserve(),可以避免多次内存重新分配,从而提高效率。

3.3 内存扩展策略

当 vector 的容量不足时,它通常会以指数倍(通常是 2 倍)的方式扩展。每次扩展都会重新分配一块新的内存,并将旧的元素拷贝到新的位置。这种方式虽然在内存重新分配时会有较大的开销,但由于扩展的频率较低,总体上这种策略是高效的。

3.4 元素的插入与删除

3.4.1 尾部插入与删除

vector 尾部的插入和删除操作是最为高效的,时间复杂度为 O(1),因为它们不需要移动其他元素。使用 push_back() 和 pop_back() 可以高效地操作 vector 末尾的元素。

3.4.2 中间或头部插入与删除

在 vector 中间或头部插入和删除元素时,需要将插入位置之后的所有元素向后移动,这样才能为新元素腾出空间。这使得这些操作的时间复杂度为 O(n)。

vec.insert(vec.begin() + 1, 10);  // 在第二个位置插入10,后面的元素都需要向后移动
vec.erase(vec.begin() + 2);       // 删除第三个元素,后面的元素都需要向前移动

4. vector 高级功能

4.1 复制与赋值

当一个 vector 被赋值或复制时,会创建一个新对象,并将所有元素进行深拷贝。这意味着修改新对象不会影响原来的 vector。

std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = vec1;   // 深拷贝
vec2[0] = 10;
std::cout << vec1[0];  // 输出1,vec1不受影响

4.2 移动语义与 std::move

C++11 引入了移动语义,允许将 vector 的资源直接转移到另一个对象,而不需要进行深拷贝。这可以极大地提高性能,尤其是在处理大型对象时。

std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);  // 使用std::move,vec1的资源转移给vec2

在上面的例子中,std::move 将 vec1 的所有资源(如内存和元素)直接转移到 vec2 中,而不需要进行深拷贝。这种操作后,vec1 将处于“空”状态,不再拥有原来的数据,size() 将返回0。

4.3 vector 的初始化列表(C++11)

C++11 引入了初始化列表,可以直接通过大括号初始化 vector:

std::vector<int> vec = {1, 2, 3, 4, 5};  // 初始化列表

这种方式非常方便,不需要手动调用 push_back() 来插入每个元素。

4.4 emplace_back() 与 push_back() 的区别

emplace_back() 是 C++11 新增的功能,它允许直接在容器的末尾构造对象,而无需先构造对象再拷贝到 vector。相比之下,push_back() 会进行额外的拷贝操作,因此在某些情况下,emplace_back() 会比 push_back() 更高效。

class Person {
public:Person(const std::string& name, int age) : name(name), age(age) {}
private:std::string name;int age;
};// 使用push_back
std::vector<Person> people;
people.push_back(Person("John", 30));  // 先构造Person对象,然后拷贝到vector中// 使用emplace_back
people.emplace_back("Jane", 25);  // 直接在vector内部构造Person对象,避免拷贝

使用 emplace_back() 时,参数直接传递给对象的构造函数,从而减少了不必要的拷贝或移动操作。

4.5 shrink_to_fit() 减少容量浪费

由于 vector 通常会预留比实际所需更多的内存空间(capacity()),可能会造成内存浪费。shrink_to_fit() 函数用于释放未使用的内存,使得 vector 的容量等于其大小。

std::vector<int> vec = {1, 2, 3};
vec.reserve(10);   // 预留10个元素的空间
std::cout << vec.capacity();  // 输出10vec.shrink_to_fit();
std::cout << vec.capacity();  // 输出3,容量被调整为实际使用的大小

需要注意的是,shrink_to_fit() 并不保证一定会减少容量,但在支持该操作的系统上可以显著减少内存占用。

5. vector 的常见应用场景

5.1 动态数组

vector 最常见的应用场景就是作为动态数组使用。当程序中需要动态调整数组大小时,vector 提供了极大的方便。

std::vector<int> vec;
int n;
std::cin >> n;
for (int i = 0; i < n; ++i) {int x;std::cin >> x;vec.push_back(x);  // 动态添加元素
}

5.2 堆栈

vector 可以模拟堆栈的功能,因为它提供了高效的尾部插入(push_back())和删除(pop_back())操作。虽然 C++ STL 中已经有 stack 容器,但使用 vector 实现堆栈也是完全可行的。

std::vector<int> stack;
stack.push_back(1);  // 入栈
stack.push_back(2);
stack.pop_back();    // 出栈

5.3 动态队列

虽然 C++ STL 提供了 queue 容器,但 vector 同样可以用来实现队列。通过在 vector 的末尾插入元素并从头部移除元素,我们可以模拟队列的行为。

std::vector<int> queue;
queue.push_back(1);  // 入队
queue.push_back(2);
queue.erase(queue.begin());  // 出队,删除第一个元素

5.4 2D 矩阵的实现

vector 可以轻松实现二维或多维数组。通过将 vector 嵌套使用,可以构建动态调整大小的二维数组或矩阵。

std::vector<std::vector<int>> matrix(3, std::vector<int>(3));  // 创建3x3的矩阵
matrix[0][0] = 1;
matrix[1][1] = 2;
matrix[2][2] = 3;

这个方法可以处理动态大小的矩阵,使得二维数组的尺寸不需要在编译时确定。

6. vector 的复杂度分析

6.1 时间复杂度

随机访问:由于 vector 是连续的内存块,随机访问某个元素的时间复杂度为 O(1)。
尾部插入和删除:尾部插入(push_back())和尾部删除(pop_back())的平均时间复杂度为 O(1),但在某些情况下(如扩容时)插入操作的复杂度可能会暂时达到 O(n)。
中间或头部插入和删除:由于插入或删除会导致大量元素的移动,因此这些操作的时间复杂度为 O(n)。

6.2 空间复杂度

vector 使用连续的内存块来存储元素,其空间复杂度与存储的元素个数成正比,即 O(n)。由于 vector 会在扩展时预留更多的内存,因此有时它的实际内存使用量会超过其存储的元素量。

为了减少内存浪费,可以使用 shrink_to_fit() 来回收未使用的空间。

7. vector 的常见问题和陷阱

7.1 容量浪费问题

如前所述,vector 的容量通常大于其实际存储的元素数量。如果程序中频繁进行插入操作且对内存使用敏感,可以使用 shrink_to_fit() 来减少浪费。

7.2 迭代器失效(最需要注意的地方)

在 vector 中进行插入或删除操作时,所有指向 vector 元素的迭代器、指针或引用可能会失效。这是因为插入或删除操作可能导致 vector 重新分配内存,从而改变所有元素的地址。

std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4);  // 可能导致内存重新分配
std::cout << *it;  // 迭代器可能失效,行为未定义

解决方法是,在插入或删除操作后,尽量避免使用之前的迭代器或指针。

7.3 元素的析构

当 vector 中的对象被删除时,会调用对象的析构函数。因此,如果 vector 存储的是指针类型,在删除 vector 或清空元素时需要特别小心,确保不会引发内存泄漏。

std::vector<int*> vec;
vec.push_back(new int(10));
vec.clear();  // 仅删除了指针,但没有释放内存,可能导致内存泄漏

解决方案是在删除元素之前手动释放内存,或者使用智能指针。

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

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

相关文章

VSCode 查看 Git 的历史记录的三种技巧

前言 在我们日常开发工作过程中&#xff0c;可能经常会看到一些离谱的历史代码&#xff0c;或者当项目发生线上事故时&#xff0c;如何快速定位是谁提交的代码导致的&#xff1f; 作为前端开发者&#xff0c;VSCode 是目前最为流行的代码编辑工具&#xff0c;也是日常最常打开…

服务器CUDA版本升级

https://blog.csdn.net/m0_52583356/article/details/138150039 上面这篇文章是按照显卡驱动所支持的最高cuda版本来更新cuda Toolkit的&#xff0c;但是如果你想要更新显卡驱动最高支持的CUDA版本&#xff0c;就需要更新显卡驱动了。 更新显卡驱动需要先卸载原有显卡驱动&am…

Vue中计算属性computed—(详解计算属性vs方法Methods,包括案例+代码)

文章目录 计算属性computed3.1 概述3.2 使用3.3 计算属性vs方法Methods3.4 计算属性的完整写法 计算属性computed 3.1 概述 基于现有的数据&#xff0c;计算出来的新属性。 依赖的数据变化&#xff0c;自动重新计算 语法&#xff1a; 声明在 computed 配置项中&#xff0c;…

OPC UA与PostgreSQL如何实现无缝连接?

随着工业4.0的推进&#xff0c;数据交换和集成在智能制造中扮演着越来越重要的角色。OPC UA能够实现设备与设备、设备与系统之间的高效数据交换。而PostgreSQL则是一种强大的开源关系型数据库管理系统&#xff0c;广泛应用于数据存储和管理。如何将OPC UA与PostgreSQL结合起来&…

python pip安装requirements.txt依赖与国内镜像

python pip安装requirements.txt依赖与国内镜像 如果网络通畅&#xff0c;直接pip安装依赖&#xff1a; pip install -r requirements.txt 如果需要国内的镜像&#xff0c;可以考虑使用阿里的&#xff0c;在后面加上&#xff1a; -i http://mirrors.aliyun.com/pypi/simple --…

基于System.js的微前端实现(插件化)

目录​​​​​​​ 写在前面 一、微前端相关知识 &#xff08;一&#xff09;概念 &#xff08;二&#xff09; 优势 &#xff08;三&#xff09; 缺点 &#xff08;四&#xff09;应用场景 &#xff08;五&#xff09;现有框架 1. qiankun 2. single-spa 3. SystemJ…

GO之流程控制

一、流程控制简述 一&#xff09;流程控制的作用 流程控制语句是用来控制程序中语句执行顺序的语句&#xff0c;可以把语句组合成能完成一定功能的小逻辑块 二&#xff09;流程控制的分类 控制语句分为三类&#xff1a;顺序、选择和循环 顺序结构&#xff1a;依次执行&#xf…

通过Express + Vue3从零构建一个用户认证与授权系统(二)数据库与后端项目搭建与实现

前言 上一篇完成了系统的相关设计文档的编写&#xff0c;本文将详细介绍如何一步步使用 TypeScript 和 Express 搭建一个模块化、类型安全的用户认证与授权系统&#xff0c;包括数据库设计、后端项目搭建、用户认证、角色与权限管理、错误处理以及 Swagger 文档集成。 项目准…

Label Studio 半自动化标注

引言 Label Studio ML 后端是一个 SDK,用于包装您的机器学习代码并将其转换为 Web 服务器。Web 服务器可以连接到正在运行的 Label Studio 实例,以自动执行标记任务。我们提供了一个示例模型库,您可以在自己的工作流程中使用这些模型,也可以根据需要进行扩展和自定义。 1…

Ubuntu22.04环境下源码安装OpenCV 4.8.1

因为项目需要用OpenCV对yolov8模型进行推理&#xff0c;通过DNN模块&#xff0c;之前本地的OpenCV版本是4.5.4&#xff08;好像安装完ROS2 humble之后系统就自带了opencv&#xff09;&#xff0c;加载onnx模型一直报错&#xff0c;网上查询到需要4.7以上&#xff0c;干脆直接升…

开发教程 | 插件使用常见问题与调用攻略

Q1&#xff1a;插件是什么&#xff1f; 插件可以理解为是在一些专业领域上的单独的专精模型&#xff0c;比如专门生成PPT的模型、专门生成简历的模型。 大模型本身其实只是一个文字生成工具&#xff0c;只能根据自己在预训练过程中投入的语料以及用户的指令来回答问题。这给大…

内核定时器API实现点灯

1.内核定时器 定时器是一个很常用的功能&#xff0c;需要周期性处理的工作都要用到定时器。 Linux 内核定时器 采用系统时钟来实现&#xff0c;并不是6ull里面的硬件定时器。 Linux 内核定时器使用很简单&#xff0c;只需要提供超时时间(相当于定时值)和定时处理函数即…

500万人报名的软考到底是什么?有什么用?考什么?怎么报名?

软考是目前中国计算机领域最权威的认证考试之一&#xff0c;被广大IT从业者视为职业生涯发展的重要里程碑。通过参加软考&#xff0c;考生可以获得国家级资格认证&#xff0c;证明其具备一定的计算机专业知识和技能。本文将详细介绍软考的相关信息&#xff0c;帮助读者了解软考…

docker compose入门2—docker-compose.yaml中的version表示什么意思

在 Docker Compose 中&#xff0c;version 字段用于指定 docker-compose.yml 文件的版本格式。不同版本定义了不同的功能和语法&#xff0c;因此 version 决定了你能够使用哪些特性和指令。 常见的 Compose 文件版本 Version 1: 不需要明确指定 version 字段。只支持最基础的功…

大数据-159 Apache Kylin 构建Cube 准备和测试数据

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

QT TCP服务器/客户端

服务器 首先要在.pro文件中添加network&#xff0c;否则将不能使用QTcpserver QT core gui network#ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer> #include <QTcpSocket> #define PORT 8000QT_BEGIN_NAMESPACE namesp…

【网络安全】-web安全-基础知识梳理

1.渗透测试流程&#xff1a; 信息收集 1、获取域名的whois信息,获取注册者邮箱姓名电话等&#xff0c;丢社工库里看看有没有泄露密码&#xff0c;然后尝试用泄露的密码进行登录后台&#xff0c;用邮箱做关键词进行丢进搜索引擎。利用搜索到的关联信息找出其他邮箱进而得到常用…

使用Rollup.js快速开始构建一个前端项目

Rollup 是一个用于 JavaScript 项目的模块打包器&#xff0c;它将小块代码编译成更大、更复杂的代码&#xff0c;例如库或应用程序。Rollup 对代码模块使用 ES6 模块标准&#xff0c;它支持 Tree-shaking&#xff08;摇树优化&#xff09;&#xff0c;可以剔除那些实际上没有被…

第7章 网络请求和状态管理

一、Axios 1 Axios概述 Axios是一个基于Promise的HTTP库&#xff0c;可以发送get、post等请求&#xff0c;它作用于浏览器和Node.js中。当运行在浏览器时&#xff0c;使用XMLHttpRequest接口发送请求&#xff1b;当运行在Node.js时&#xff0c;使用HTTP对象发送请求。 Axios的…

docker-compose与docker

“docker-compose” 是一个用于定义和运行多容器 Docker 应用程序的工具。它使用一个名为 docker-compose.yml 的配置文件来描述应用程序的服务、网络和卷&#xff0c;然后通过简单的命令就可以管理整个应用。 以下是一些常用的 docker-compose 命令及其用法&#xff1a; 启动…