当前位置: 首页 > news >正文

【C++】Vector完全指南:动态数组高效使用

0. 官方文档

vector

1. vector介绍

Vector 简单来说就是顺序表,是一个可以动态增长的数组。

  1. vector是表示可变大小数组的序列容器。

  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

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

  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。

  5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。

  6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。

优点:

  1. 支持下标随机访问,间接的就很好的支持了排序、二分查找、堆算法等等

缺点:

  1. 头部和中部的插入删除效率低。O(N),因为需要挪动数据。

  2. 插入数据空间不够需要增容,增容需要开新空间,拷贝数据,释放旧空间,会付出很大代价。

2. vector库函数的使用

2.1 vector 的基本构造、拷贝与赋值操作

vector 的默认构造与元素添加

vector<int> v1;  // 创建一个空的整型vector
v1.push_back(1); // 在vector末尾添加元素1
v1.push_back(2); // 在vector末尾添加元素2
v1.push_back(3); // 在vector末尾添加元素3
v1.push_back(4); // 在vector末尾添加元素4
  • vector<int> v1 使用默认构造函数创建一个空的vector容器

  • push_back() 成员函数用于在vector的末尾添加新元素

vector 的拷贝构造

vector<int> v2(v1); // 使用v1作为源创建v2(拷贝构造)
  • 拷贝构造函数 vector<int> v2(v1) 创建一个新vector,其元素是v1元素的完整拷贝

  • 两个vector对象完全独立,修改一个不会影响另一个

vector 的遍历与下标访问

for (size_t i = 0; i < v1.size(); ++i)
{cout << v1[i] << " "; // 使用下标运算符[]访问元素
}
  • size() 成员函数返回vector中元素的数量

  • 可以使用下标运算符 [] 像数组一样访问vector中的元素

vector 的赋值操作

vector<int> v3;
v3.push_back(60);
v3.push_back(61);
v3.push_back(62);
v3.push_back(63);v1 = v3; // 将v3的内容赋值给v1
  • 赋值操作 v1 = v3 会将v3的所有元素复制到v1中

  • 赋值后v1的原始内容会被替换,大小会调整为与v3相同

2.2 vector 的遍历与元素修改

使用下标遍历和修改元素

for (size_t i = 0; i < v.size(); ++i)
{v[i] *= 2;        // 修改元素值cout << v[i] << " "; // 访问元素值
}
  • 使用下标遍历是最直观的方式,类似于操作普通数组

  • 可以通过下标直接修改vector中的元素

使用迭代器遍历和修改元素

vector<int>::iterator it = v.begin(); // 获取指向第一个元素的迭代器
while (it != v.end())                 // 循环直到迭代器指向末尾
{*it *= 2;          // 解引用迭代器并修改元素值cout << *it << " "; // 访问元素值++it;              // 移动迭代器到下一个元素
}
  • begin() 返回指向第一个元素的迭代器

  • end() 返回指向最后一个元素之后位置的迭代器

  • 迭代器提供了类似指针的语法来访问和修改元素

使用范围for循环遍历元素

for (auto e : v)
{cout << e << " "; // 访问每个元素
}
  • 范围for循环是C++11引入的语法糖,代码更简洁

  • 编译器会自动将其转换为迭代器操作

  • 注意:这种方式默认是值拷贝,如果需要修改元素,应使用引用 for (auto& e : v)

2.3 vector 的迭代器类型

        一般来说分为三大种:正向、反向、const

普通正向迭代器

vector<int>::iterator it = v.begin();
while (it != v.end())
{*it *= 2;        // 可读写操作cout << *it << " ";++it;
}
  • 普通迭代器允许读取和修改指向的元素

  • 使用 iterator 类型定义,适用于需要修改vector内容的场景

const 正向迭代器

vector<int>::const_iterator it = vt.begin();
while (it != vt.end())
{// *it *= 2;     // 错误:不能修改const迭代器指向的值cout << *it << " "; // 只能读取++it;
}
  • const迭代器指向的元素不能被修改

  • 使用 const_iterator 类型定义,适用于只读访问场景

反向迭代器

vector<int>::reverse_iterator rit = v.rbegin();
while (rit != v.rend())
{cout << *rit << " "; // 从后向前遍历++rit;               // 注意:++操作是向前移动
}
  • 反向迭代器从容器的末尾向开头遍历

  • rbegin() 返回指向最后一个元素的迭代器

  • rend() 返回指向第一个元素之前位置的迭代器

  • ++ 操作使迭代器向前移动(向容器开头方向)

const 反向迭代器

vector<int>::const_reverse_iterator crit = v.rbegin();
  • 结合了反向迭代和const特性的迭代器

  • 可以反向遍历容器,但不能修改元素值

函数参数中的const引用

void print_vector(const vector<int>& vt)
{vector<int>::const_iterator it = vt.begin();while (it != vt.end()){cout << *it << " ";++it;}cout << endl;
}
  • 使用const引用传参 const vector<int>& vt 避免不必要的拷贝

  • 函数内部使用const迭代器确保不会意外修改原始数据

  • 这是C++中常见的编程实践,既能提高效率又能保证数据安全

const对象与const迭代器

  • const对象只能使用const迭代器进行遍历

  • 在实际工程中,const对象常用于函数参数,防止函数内部修改调用者的数据

  • 使用const是正确的编程习惯,可以提高代码的可读性和安全性

2.4 容量相关

下面是一段扩容测试代码,插入1000个数据,观测容量空间变化:

void test_vector4()
{vector<int> v;cout << "making v grow:\n";size_t sz = v.size();for (int i = 0; i < 1000; ++i){v.push_back(i);if (sz != v.capacity()){sz = v.capacity();cout << "capacity changed: " << sz << '\n';}}
}

同样的代码,如下图,左边是linux环境下编译运行出的结果,右边是window的vs编译的结果:

  • 增容次数越少,效率越高,但可能导致的空间浪费越多。

  • 增容次数越多,效率越低,但可能导致的空间浪费越少。

v.reserve(1000);        // 扩容
v.resize(1000);         // 扩大size,额外扩出来的部分会自动补'\0'或自定义字符

reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。

resize在开空间的同时还会进行初始化,影响size。

2.5 修改与查找

尾部插入元素

vector<int> v;
// 尾插 - 使用 push_back() 在容器末尾添加元素
v.push_back(5);
v.push_back(4);
v.push_back(3);
v.push_back(2);
v.push_back(1);
  • push_back() 是 vector 最常用的添加元素方法

  • 每次调用会在 vector 的末尾添加一个新元素

  • 如果当前容量不足,vector 会自动重新分配更大的内存空间

指定位置插入元素

// 头插 - 使用 insert() 在指定位置插入元素
v.insert(v.begin(), 0);     // 在开头插入元素0
v.insert(v.end(), -1);    // 在结尾插入元素-1
  • insert() 方法可以在任意位置插入元素

  • 第一个参数是迭代器,指定插入位置

  • 第二个参数是要插入的值

  • 在开头插入元素会导致后续所有元素向后移动,效率较低

删除指定位置元素

// 头删 - 使用 erase() 删除指定位置的元素
v.erase(v.begin());  // 删除第一个元素
  • erase() 方法删除指定位置的元素

  • 参数是一个迭代器,指向要删除的元素

  • 删除元素后,后面的所有元素会向前移动

  • 删除开头元素效率较低,因为需要移动所有后续元素

查找操作:使用标准库的 find 算法

// vector 没有提供内置的查找方法// 使用标准库中的 find 算法
vector<int>::iterator pos = find(v.begin(), v.end(), 5);
  • vector 本身不提供查找方法,需要使用标准库的 find 算法

  • find 需要包含 <algorithm> 头文件

  • find 接受三个参数:起始迭代器、结束迭代器和要查找的值

  • 查找范围是左闭右开区间 [first, last),即包含 first 但不包含 last

处理查找结果

if (pos != v.end())  // 检查是否找到元素
{v.erase(pos);    // 如果找到,删除该元素
}
  • find 返回一个迭代器,指向找到的元素

  • 如果没找到,返回结束迭代器 v.end()

  • 在删除前必须检查是否找到元素,否则可能引发未定义行为

vector 的排序操作:使用标准库的 sort 算法

// 使用标准库的 sort 算法对 vector 排序
sort(v.begin(), v.end());  // 默认升序排序
  • sort 需要包含 <algorithm> 头文件

  • 默认情况下,sort 按升序排列元素

  • sort 使用快速排序算法实现,平均时间复杂度为 O(N log N)

  • 排序范围也是左闭右开区间 [first, last)

2.6 标准库算法的说明

find 算法

// std::find 函数模板声明(简化)
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value);
  • find 是一个函数模板,可用于任何支持迭代器的容器

  • 它在 [first, last) 范围内查找第一个等于 value 的元素

  • 如果找到,返回指向该元素的迭代器;否则返回 last

  • 使用 == 运算符进行比较,因此元素类型必须支持此操作

sort 算法

// std::sort 函数模板声明(简化)
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
  • sort 要求迭代器是随机访问迭代器,vector 的迭代器满足此要求

  • 默认使用 < 运算符进行比较,因此元素类型必须支持此操作

  • 可以自定义比较函数来实现不同的排序规则

2.7 迭代器失效问题

1.增容导致的迭代器失效

看下面这段代码:

void test_vector8()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);vector<int>::iterator it = v.begin();v.push_back(6);v.push_back(7);while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}

如果我们把上面的代码修改一下,把push_back(7)注释掉,就可以运行:

        这是为什么呢?这个问题和动态增容有关。

        假设我们插入 7 之前的 v 有6个空间,即 v.capacity() 为6,当我们插入 7 后发生了增容,开辟了一块新的空间,v 的内容全都被拷贝去了新的空间,然而此时 it 迭代器还指向旧空间的 v.begin() ,it 迭代器就失效了。

        因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

        所以 push_back 、insert 、resize 、reserve 等可能扩容的接口都会导致迭代器失效。

解决办法(正确写法):

        很简单,不要在初始化迭代器后在调用可能导致扩容的接口就行了。

2.删除导致的迭代器失效

void test_vector9()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);// 要求删除容器中的所有偶数vector<int>::iterator it = v.begin();while (it != v.end()){if (*it % 2 == 0){v.erase(it);}++it;}for (auto e : v){cout << e << " ";}cout << endl;}

程序又崩溃了:

        当 it 到 2 的位置时,会删掉 2 ,然后后面的数据会往前挪,it++,此时 it 会直接略过 3 ,到 4 的位置。

        也就是说,删除 it 位置的元素后,it 就失效了,这里的失效是 it 迭代器已经失效了,再 ++it 就不行。这个问题在 vs 下会报错,是编译器强制检查的,但是在 gcc 下就不报错,但是结果不对,无法删除掉连续的偶数(比如把3改成30就无法删除30)。

        编译器检查迭代器失效原因:erase删除 it 位置元素后,it 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果 it 刚好是最后一个元素,删完之后 it 刚好是end的位置,而end位置是没有元素的,那么 it 就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。

        总结:不管哪个平台下,erase(it) 后,it 就失效了,只是导致的结果不一样,总之都有各种各样的问题。

        作为编译器检查迭代器失效的印证,哪怕把代码改成下面这样也会报错(vs 会报错,linux 下正常运行)

解决办法(正确写法):

        通过官网对 erase 的返回值的介绍,我们可以知道,erase 的返回值是被删除元素的下一个元素的位置。我们只需要改一下代码就可以正常运行了:

void test_vector9()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);// 要求删除容器中的所有偶数vector<int>::iterator 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;}


文章转载自:

http://5JdboDRl.fsLrx.cn
http://zfdejSaB.fsLrx.cn
http://53f5VtMm.fsLrx.cn
http://QY7Jdnpn.fsLrx.cn
http://Nvzw0kuF.fsLrx.cn
http://zGzHK9mR.fsLrx.cn
http://vaHB8a3q.fsLrx.cn
http://MaxFU8kO.fsLrx.cn
http://bj6p8NmY.fsLrx.cn
http://VjrRwUDx.fsLrx.cn
http://dHKHB1o8.fsLrx.cn
http://6RmxIG6H.fsLrx.cn
http://0qyA1dPd.fsLrx.cn
http://vwelSfoX.fsLrx.cn
http://1lUycFPd.fsLrx.cn
http://0PUiq58V.fsLrx.cn
http://E1fzidGq.fsLrx.cn
http://W8BxMG56.fsLrx.cn
http://tdVuC0FX.fsLrx.cn
http://pOb2VmL8.fsLrx.cn
http://Y4ZJ3Cjg.fsLrx.cn
http://P8WBAtTG.fsLrx.cn
http://O7RcZj5n.fsLrx.cn
http://9ZUsrZDo.fsLrx.cn
http://7iTH8IXq.fsLrx.cn
http://cHdNL8Xy.fsLrx.cn
http://CmtaTws1.fsLrx.cn
http://MLDHmoM8.fsLrx.cn
http://UxDH2qWq.fsLrx.cn
http://s34hUR6X.fsLrx.cn
http://www.dtcms.com/a/369684.html

相关文章:

  • 状压 dp --- TSP 问题
  • 【数字孪生核心技术】什么是倾斜摄影?
  • 公共卫浴感应开关选红外还是雷达
  • 解决 Apache/WAF SSL 证书链不完整导致的 PKIX path building failed 问题
  • 计算机二级C语言操作题(填空、修改、设计题)——真题库(17)附解析答案
  • 上位机通信基础知识
  • Acrobat-2025.001.20643_Win中文_PDF编辑器_便携版安装教程
  • Java基础 9.5
  • javafx笔记
  • 大基座模型与 Scaling Law:AI 时代的逻辑与困境
  • 扩展与改进的密钥协商协议
  • Spring整合MQTT使用
  • AI应用开发-技术架构 PAFR介绍
  • 9月5日星期五今日早报简报微语报早读
  • Zynq-7000 上 RT-Thread 的 MMU 与 SMP 优势分析
  • 【完整源码+数据集+部署教程】西兰花实例分割系统源码和数据集:改进yolo11-AggregatedAtt
  • 数据库查询优化
  • PiscCode基于 Mediapipe 实现轨迹跟踪
  • 硬件(三) 通信方式、串口通信
  • 在 CentOS 上完整安装 Docker 指南
  • 详解人造卫星遭遇的地球反射光与月球反射光
  • NAF、INRAS、NACF论文解读
  • 【Linux】系统部分——进程间通信1(管道)
  • 从策略到实效|Adobe Target 实战应用与成功案例
  • 连锁门店可用性监测和进程监测最佳实践
  • 残差网络ResNet
  • 人工智能之数学基础:逻辑回归算法的概率密度函数与分布函数
  • Pinia 两种写法全解析:Options Store vs Setup Store(含实践与场景对比)
  • MySQL抛出的Public Key Retrieval is not allowed
  • 贵州移动创维E900V22F-S905L3SB-全分区备份