【C++ vector】

在 C++ STL 容器体系中,vector 是使用频率最高的序列式容器,没有之一。它本质上是一个支持动态扩容的数组,相比原生数组,它封装了自动化的内存管理能力,提供了丰富的成员接口,同时完美保留了数组 O (1) 时间复杂度的随机访问特性。

一、vector 基础用法:核心接口全掌握

vector 的接口繁多,但日常开发与面试中,我们只需重点掌握核心接口即可。下面我们按功能模块,拆解 vector 的核心用法。

1.1 构造函数:4 种核心构造方式

构造函数接口 功能说明
vector() 无参构造,创建空的 vector 对象(最常用)
vector(size_type n, const value_type& val = value_type()) 构造并初始化 n 个值为 val 的元素
vector(const vector& x) 拷贝构造,用已有 vector 对象创建新对象
vector(InputIterator first, InputIterator last) 迭代器区间构造,用其他容器的区间初始化 vector
代码示例:
cpp 复制代码
#include <vector>
#include <iostream>
using namespace std;

int main() {
    // 1. 无参构造
    vector<int> v1;
    
    // 2. 构造5个值为10的元素
    vector<int> v2(5, 10);
    
    // 3. 拷贝构造
    vector<int> v3(v2);
    
    // 4. 迭代器区间构造
    int arr[] = {1, 2, 3, 4, 5};
    vector<int> v4(arr, arr + sizeof(arr)/sizeof(int));
    
    // C++11 拓展:列表初始化
    vector<int> v5 = {1, 2, 3, 4, 5};
    return 0;
}

1.2 迭代器:vector 遍历的核心工具

迭代器是 STL 的核心设计,它让算法无需关心容器的底层数据结构,而 vector 的迭代器本质就是原生指针T*,支持随机访问。

核心迭代器接口分为两组:

1.正向迭代器:begin() + end()

begin():返回第一个元素的迭代器

end():返回最后一个元素的下一个位置的迭代器(左闭右开区间)

2.反向迭代器:rbegin() + rend()

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

rend():返回第一个元素前一个位置的反向迭代器
三种遍历方式代码示例:

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 1. 迭代器遍历
vector<int>::iterator it = v.begin();
while (it != v.end()) {
    cout << *it << " ";
    ++it;
}
cout << endl;

// 2. 下标[]遍历(vector最常用的遍历方式)
for (size_t i = 0; i < v.size(); ++i) {
    cout << v[i] << " ";
}
cout << endl;

// 3. C++11 范围for遍历(底层封装了迭代器)
for (auto e : v) {
    cout << e << " ";
}
cout << endl;

1.3 空间管理:吃透容量与大小的核心接口

vector 的空间管理是其核心特性,也是面试高频考点,核心接口如下:

接口 功能说明
size() 获取 vector 中有效元素的个数
capacity() 获取 vector 当前的容量(可容纳元素的最大个数)
empty() 判断 vector 是否为空
resize(size_t n, val = value_type()) 改变 vector 的有效元素个数 size
reserve(size_t n) 改变 vector 的容量 capacity

核心考点 1:vector 的扩容机制

vector 作为动态数组,当插入元素时如果size() == capacity(),就会触发自动扩容。不同编译器的 STL 实现,扩容策略不同:
VS 编译器(PJ 版 STL):按1.5 倍扩容
g++ 编译器(SGI 版 STL):按2 倍扩容
扩容的完整流程:开辟新的更大内存空间 → 将旧空间的元素拷贝到新空间 → 释放旧内存空间 → 更新迭代器指针。这也是为什么扩容会导致迭代器失效的根本原因。

cpp 复制代码
void TestVectorExpand() {
    size_t sz;
    vector<int> v;
    sz = v.capacity();
    cout << "making vector grow:\n";
    for (int i = 0; i < 100; ++i) {
        v.push_back(i);
        if (sz != v.capacity()) {
            sz = v.capacity();
            cout << "capacity changed: " << sz << '\n';
        }
    }
}

核心考点 2:resize 与 reserve 的核心区别

这是面试必问的知识点,二者的核心差异如下:
1.reserve :只操作容量 capacity,不改变有效元素个数 size,不会创建 / 初始化对象,仅负责提前开辟空间。如果 n 小于当前 capacity,该函数在多数编译器下不会做任何操作。
2.resize :直接改变有效元素个数 size,同时会初始化对象。如果 n 大于当前 capacity,会先扩容再初始化;如果 n 小于当前 size,会析构掉超出的元素。
性能优化技巧:如果提前知道 vector 需要存储的元素个数,使用reserve()提前开辟足够空间,可以避免频繁扩容带来的元素拷贝、内存释放的性能损耗。

cpp 复制代码
// 优化版:提前预分配空间,避免100次插入过程中的多次扩容
void TestVectorExpandOP() {
    vector<int> v;
    v.reserve(100); // 提前将容量设置为100
    size_t sz = v.capacity();
    cout << "after reserve, capacity: " << sz << '\n';
    for (int i = 0; i < 100; ++i) {
        v.push_back(i);
        if (sz != v.capacity()) {
            sz = v.capacity();
            cout << "capacity changed: " << sz << '\n';
        }
    }
}

1.4 增删查改:高频操作接口

vector 的增删查改接口中,尾插尾删是最高效的(O (1)),而随机位置的插入删除需要移动元素,时间复杂度为 O (n),核心接口如下:

1.4 增删查改:高频操作接口

接口 功能说明
push_back(const T& val) 尾插元素(最常用)
pop_back() 尾删元素
insert(iterator pos, const T& val) 在 pos 迭代器位置前插入元素 val
erase(iterator pos) 删除 pos 迭代器位置的元素,返回下一个元素的迭代器
swap(vector& x) 交换两个 vector 的底层数据空间(O (1) 时间复杂度)
operator[] 像数组一样通过下标随机访问元素
注意:vector 没有提供成员函数find,查找元素需要使用 STL 算法库的find函数,需要包含头文件。
cpp 复制代码
#include <algorithm>

int main() {
    vector<int> v = {1, 2, 3, 4};
    
    // 尾插、尾删
    v.push_back(5); // v: 1 2 3 4 5
    v.pop_back();   // v: 1 2 3 4
    
    // 插入:在开头插入0
    v.insert(v.begin(), 0); // v: 0 1 2 3 4
    
    // 查找:找到3的位置
    vector<int>::iterator pos = find(v.begin(), v.end(), 3);
    
    // 删除:删除3这个元素
    if (pos != v.end()) {
        v.erase(pos);
    } // v: 0 1 2 4
    
    // 下标访问修改
    v[0] = 10; // v: 10 1 2 4
    
    // 交换两个vector
    vector<int> v2(5, 100);
    v.swap(v2);
    
    return 0;
}

二、vector 核心坑点:迭代器失效全解

迭代器失效是 vector 使用中最容易踩的坑,也是面试的绝对高频考点。我们需要彻底搞懂:什么是迭代器失效、哪些场景会导致失效、如何解决失效问题。

2.1 什么是迭代器失效?

vector 的迭代器底层本质就是原生指针T*,迭代器失效,本质就是迭代器对应的底层指针指向的内存空间被销毁,或者指针指向的位置已经不符合容器的逻辑状态。继续使用已经失效的迭代器,会导致未定义行为:轻则程序运行结果错误,重则直接程序崩溃。

2.2 迭代器失效的两大核心场景

场景 1:底层空间重新分配,导致原迭代器全部失效

所有可能触发 vector 扩容的操作,都会导致原迭代器全部失效,包括: resize、reserve、insert、push_back、assign等。
失效原因 :扩容会释放旧的内存空间,开辟新的内存空间,而原来的迭代器依然指向已经被释放的旧空间,变成了野指针,继续访问就会导致程序崩溃。
错误代码示例

cpp 复制代码
int main() {
    vector<int> v = {1,2,3,4,5};
    auto it = v.begin();
    
    // 触发扩容,旧空间被释放,it失效
    v.reserve(100);
    
    // 错误:使用已经失效的迭代器it,vs下直接崩溃,g++下结果错误
    while(it != v.end()) {
        cout << *it << " ";
        ++it;
    }
    return 0;
}

解决方案:扩容操作完成后,重新给迭代器赋值,重新获取begin()和end()。

场景 2:erase 删除元素,导致指定位置迭代器失效

erase 删除元素后,pos 位置之后的元素会整体向前移动,底层空间并没有发生改变,理论上迭代器不会完全失效,但标准规定:erase 会导致指向被删除元素及其之后的所有迭代器失效。

这里有两个典型的坑:

1.删除元素后,直接访问原迭代器,导致非法访问;

2.循环删除元素时,直接对迭代器 ++,导致元素跳过甚至程序崩溃。

经典错误案例:删除 vector 中所有偶数
cpp 复制代码
// 错误版本
int main() {
    vector<int> v = { 1, 2, 3, 4 };
    auto it = v.begin();
    while (it != v.end()) {
        if (*it % 2 == 0) {
            v.erase(it); // 这里it已经失效
        }
        ++it; // 失效的迭代器++,导致未定义行为
    }
    return 0;
}

错误原因

当 erase 删除 it 指向的元素后,it 已经失效,后续对 it 的 ++ 操作是非法的;

即使编译器不崩溃,也会导致元素跳过:比如删除 2 之后,3 前移到 2 的位置,it++ 直接指向 4,跳过了 3 的检查。

cpp 复制代码
// 正确版本
int main() {
    vector<int> v = { 1, 2, 3, 4 };
    auto it = v.begin();
    while (it != v.end()) {
        if (*it % 2 == 0) {
            // erase返回被删除元素的下一个位置的迭代器,更新it
            it = v.erase(it);
        } else {
            // 没有删除元素,才对迭代器++
            ++it;
        }
    }
    return 0;
}

2.3 不同编译器的行为差异

VS(PJ 版 STL) :对迭代器失效的检查非常严格,只要迭代器触发了失效场景,继续使用就会直接断言崩溃;
g++(SGI 版 STL) :对迭代器失效的检测比较宽松,非扩容的 erase 操作后,迭代器可能还能正常运行,但尾元素被删除后,迭代器指向 end (),再 ++ 必然会段错误。
重要提醒:绝对不能依赖编译器的宽松检查,必须严格按照 C++ 标准编写代码,主动规避迭代器失效问题,否则代码跨平台运行时会出现难以排查的 bug。

2.4 迭代器失效的通用解决方案

1.任何可能触发扩容的操作后,重新给迭代器赋值,重新获取begin()和end();

2.erase 删除元素时,用 erase 的返回值更新迭代器,禁止使用已删除位置的迭代器;

3.循环中修改 vector 结构时,优先使用下标访问,规避迭代器失效问题。

补充:与 vector 类似,string 在插入、扩容、erase 操作后,同样会发生迭代器失效,解决方案完全一致。

相关推荐
机器视觉的发动机1 小时前
图像处理-机器视觉算法中的数学基础
开发语言·人工智能·算法·决策树·机器学习·视觉检测·机器视觉
Darkwanderor1 小时前
离散化思维的应用
数据结构·c++·算法·哈希算法
guohahaya1 小时前
attention-2026
开发语言·c#
PAK向日葵4 小时前
【C++】整数类型(Integer Types)避雷指南与正确使用姿势
c++·安全·面试
lntu_ling4 小时前
Python-基于Haversine公式计算两点距离
开发语言·python·gis算法
ShineWinsu10 小时前
对于C++:继承的解析—上
开发语言·数据结构·c++·算法·面试·笔试·继承
小付同学呀10 小时前
C语言学习(五)——输入/输出
c语言·开发语言·学习
梦幻精灵_cq10 小时前
学C之路:不可或缺的main()主函数框架(Learn-C 1st)
c语言·开发语言
消失的旧时光-194310 小时前
C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)
java·开发语言·c++·并发