STL 容器:vector 动态数组的全面解析

STL 容器:vector 动态数组的全面解析

在C++ STL的序列式容器中,vector无疑是最常用、最灵活、最贴合日常开发需求的容器之一。无论是笔试面试中的算法题,还是实际项目中的数据存储,vector都能凭借其"动态扩容""随机访问"的核心优势,成为开发者的首选。

很多开发者对vector的认知停留在"可以自动变长的数组",但其实它的底层实现、扩容机制、接口用法以及性能优化,都有很多值得深究的细节。今天这篇博客,我们就来全面拆解vector------从基础用法到底层原理,从实操示例到避坑指南,让你真正吃透这个STL"高频容器",做到灵活运用、规避隐患。

一、vector 是什么?核心定位与优势

vector 是 STL 标准模板库中的序列式容器,底层基于动态数组实现,本质是封装了动态分配内存的模板类。它可以存储任意数据类型(内置类型int、float,自定义结构体、类等),支持动态扩容(无需手动管理内存大小),同时提供了便捷的接口,实现数据的增、删、改、查等操作。

vector 的核心优势(为什么首选它?)

  • 随机访问高效:支持像普通数组一样通过下标 [ ] 直接访问元素,时间复杂度为 O(1),这是vector区别于list(双向链表)的核心优势,适合频繁读取元素的场景。

  • 动态扩容省心:无需手动计算和分配内存,当存储的数据超过当前容量时,vector会自动扩容,开发者只需专注于数据操作,无需关心底层内存管理。

  • 接口丰富易用:STL为vector提供了完善的成员函数(如插入、删除、排序、清空等),接口命名规范、用法简洁,上手成本低。

  • 内存连续:底层数组的内存是连续的,缓存命中率高,相较于非连续内存的容器(如list),在遍历、访问时性能更优。

vector 的适用场景

结合其优势,vector 适合以下场景,也是日常开发中最常遇到的场景:

  • 需要频繁通过下标访问元素(如存储用户列表、成绩排名、日志数据);

  • 数据量不确定,需要动态增减(如接收用户输入、读取文件内容);

  • 频繁在尾部插入/删除元素(尾部操作效率极高,时间复杂度 O(1));

  • 需要与算法协同使用(如sort排序、find查找,vector支持随机访问迭代器,适配所有STL算法)。

二、vector 基础用法:从初始化到常用操作

在使用vector之前,需包含对应的头文件#include <vector>,并建议使用 std 命名空间(或显式调用 std::vector)。下面从初始化开始,逐一讲解vector的核心基础操作,搭配示例代码,方便直接复制测试。

1. vector 的初始化(5种常用方式)

vector 的初始化方式灵活,可根据实际需求选择,不同方式的效率和适用场景略有差异,重点掌握前3种即可。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    // 方式1:默认初始化,空vector,容量为0,后续添加元素时自动扩容
    vector<int> v1;
    
    // 方式2:初始化n个元素,每个元素默认值为0(内置类型),自定义类型需有默认构造
    vector<int> v2(5); // v2: [0, 0, 0, 0, 0]
    
    // 方式3:初始化n个元素,每个元素赋值为指定值(推荐,高效)
    vector<int> v3(5, 3); // v3: [3, 3, 3, 3, 3]
    
    // 方式4:通过已有数组/vector初始化(拷贝构造)
    int arr[] = {1, 2, 3, 4, 5};
    vector<int> v4(arr, arr + 5); // v4: [1, 2, 3, 4, 5](左闭右开区间)
    vector<int> v5(v4); // 拷贝v4,v5与v4元素完全一致
    
    // 方式5:通过初始化列表初始化(C++11及以上支持,最简洁)
    vector<int> v6 = {1, 2, 3, 4, 5};
    vector<int> v7{10, 20, 30}; // 省略等号,同样有效
    
    // 打印测试(后续会讲遍历方式,此处暂用简单循环)
    for (int i = 0; i < v7.size(); i++) {
        cout << v7[i] << " ";
    }
    return 0;
}

2. vector 核心成员函数(必学)

vector 的成员函数众多,但常用的只有10个左右,我们按"访问、插入、删除、容量、其他"分类讲解,结合示例说明用法,避免记混。

(1)访问元素(3种方式)
  • operator[]:下标访问,与普通数组一致,无越界检查(高效,但不安全);

  • at():下标访问,有越界检查,越界会抛出 out_of_range 异常(安全,效率略低);

  • front()/back():访问第一个/最后一个元素,时间复杂度 O(1)(高频使用)。

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
cout << v[0] << endl;      // 输出1(无越界检查)
cout << v.at(2) << endl;   // 输出3(有越界检查)
cout << v.front() << endl; // 输出1(第一个元素)
cout << v.back() << endl;  // 输出5(最后一个元素)

// 注意:v[10] 不会报错,但会访问非法内存(未定义行为);v.at(10) 会直接抛出异常
(2)插入元素(2种高频方式)
  • push_back():在vector尾部插入一个元素,时间复杂度 O(1)(无扩容时),最常用

  • insert():在指定位置插入元素/元素组,时间复杂度 O(n)(需移动后续元素,效率较低)。

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

// 1. push_back:尾部插入
v.push_back(4); // v: [1, 2, 3, 4]
v.push_back(5); // v: [1, 2, 3, 4, 5]

// 2. insert:指定位置插入(需配合迭代器)
// 插入单个元素:在第2个位置(下标1)插入10
v.insert(v.begin() + 1, 10); // v: [1, 10, 2, 3, 4, 5]
// 插入多个相同元素:在第3个位置(下标2)插入2个20
v.insert(v.begin() + 2, 2, 20); // v: [1, 10, 20, 20, 2, 3, 4, 5]
// 插入另一个vector的元素:在末尾插入v2的所有元素
vector<int> v2 = {6, 7, 8};
v.insert(v.end(), v2.begin(), v2.end()); // v: [1,10,20,20,2,3,4,5,6,7,8]
(3)删除元素(3种高频方式)
  • pop_back():删除尾部最后一个元素,时间复杂度 O(1),最常用

  • erase():删除指定位置/指定区间的元素,时间复杂度 O(n)(需移动后续元素);

  • clear():清空vector中所有元素,但不释放内存(容量不变,size变为0)。

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

// 1. pop_back:删除尾部元素
v.pop_back(); // v: [1, 10, 20, 20, 2, 3, 4]

// 2. erase:删除指定位置元素(下标3的元素20)
v.erase(v.begin() + 3); // v: [1, 10, 20, 2, 3, 4]
// erase:删除指定区间元素(从下标1到下标2,左闭右开)
v.erase(v.begin() + 1, v.begin() + 3); // v: [1, 2, 3, 4]

// 3. clear:清空所有元素
v.clear(); 
cout << v.size() << endl;  // 输出0(元素个数为0)
cout << v.capacity() << endl; // 输出4(容量不变,仍为之前的大小)
(4)容量相关(4个核心函数)

vector 的"容量(capacity)"和"大小(size)"是极易混淆的两个概念,必须重点区分:

  • size() :返回vector中当前元素的个数(实际存储的数据量);

  • capacity() :返回vector底层数组能容纳的最大元素个数(已分配的内存大小);

  • empty():判断vector是否为空(size() == 0),返回bool值(高效,推荐使用);

  • resize():调整vector的size(元素个数),若size增大,新增元素为默认值/指定值;若size减小,删除末尾多余元素(不改变capacity);

  • reserve() :提前分配内存,调整vector的capacity(容量),不改变size(用于优化扩容效率)。

cpp 复制代码
vector<int> v;
cout << v.size() << " " << v.capacity() << endl; // 0 0(空vector)

v.push_back(1);
cout << v.size() << " " << v.capacity() << endl; // 1 1(容量自动扩容到1)

v.push_back(2);
cout << v.size() << " " << v.capacity() << endl; // 2 2(容量扩容到2)

v.push_back(3);
cout << v.size() << " " << v.capacity() << endl; // 3 4(容量扩容到4,通常是2倍扩容)

// resize:调整size为5,新增元素默认值0
v.resize(5);
cout << v.size() << " " << v.capacity() << endl; // 5 4?不,容量会自动扩容到不小于size,此处输出5 8
// resize:调整size为3,删除末尾2个元素
v.resize(3);
cout << v.size() << " " << v.capacity() << endl; // 3 8(容量不变)

// reserve:提前分配容量为10,不改变size
v.reserve(10);
cout << v.size() << " " << v.capacity() << endl; // 3 10(容量变为10,size仍为3)
(5)其他常用操作
  • swap():交换两个vector的元素、size和capacity(高效,时间复杂度 O(1));

  • data():返回vector底层数组的首地址(可用于与C语言数组交互)。

3. vector 的遍历方式(4种常用方式)

vector 支持多种遍历方式,可根据场景选择,其中"范围for循环"和"迭代器遍历"最常用,适配不同需求。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    
    // 方式1:普通for循环(下标访问,最直观,适合需要下标场景)
    for (int i = 0; i < v.size(); i++) {
        cout << v[i] << " ";
    }
    cout << endl;
    
    // 方式2:范围for循环(C++11及以上,简洁,无需关心下标,仅遍历)
    for (int val : v) {
        cout << val << " "; // val是v中元素的拷贝,修改val不影响原vector
    }
    cout << endl;
    // 范围for循环(修改原元素,需用引用)
    for (int& val : v) {
        val *= 2; // 修改原vector中的元素
    }
    
    // 方式3:迭代器遍历(最通用,适配所有STL算法,推荐)
    // 普通迭代器(可读可写)
    vector<int>::iterator it;
    for (it = v.begin(); it != v.end(); ++it) {
        cout << *it << " "; // *it 访问迭代器指向的元素
    }
    cout << endl;
    // 常量迭代器(只读,不允许修改元素)
    vector<int>::const_iterator cit;
    for (cit = v.cbegin(); cit != v.cend(); ++cit) {
        cout << *cit << " ";
    }
    cout << endl;
    
    // 方式4:反向迭代器(从尾部到头部遍历)
    vector<int>::reverse_iterator rit;
    for (rit = v.rbegin(); rit != v.rend(); ++rit) {
        cout << *rit << " "; // 输出:10 8 6 4 2
    }
    cout << endl;
    
    return 0;
}

三、vector 底层原理:扩容机制(核心重点)

很多开发者使用vector时,只知道它能自动扩容,但不清楚扩容的底层逻辑,导致在频繁插入元素时,出现性能瓶颈或迭代器失效问题。这一部分,我们就来拆解vector的扩容机制,搞懂"为什么扩容""怎么扩容""扩容有什么隐患"。

1. 扩容的本质:重新分配内存 + 拷贝元素

vector 底层是连续的动态数组,一旦数组被分配,内存地址就固定了。当我们通过 push_back() 插入元素,且当前 size == capacity 时,vector 就会触发"扩容",扩容的核心步骤的是:

  1. 分配一块更大的新内存(通常是原容量的2倍,不同编译器略有差异,如VS是1.5倍,GCC是2倍);

  2. 将原内存中的所有元素,拷贝到新内存中;

  3. 释放原内存空间;

  4. 将vector的指针指向新内存,并更新capacity(容量)。

2. 扩容的性能损耗

从扩容的步骤可以看出,扩容是一个"耗时操作"------涉及内存分配、元素拷贝、原内存释放,这些操作的时间复杂度是 O(n)(n为当前元素个数)。如果频繁插入元素,会触发多次扩容,导致性能下降。

举个例子:如果我们要插入1000个元素,vector初始容量为0,每次扩容2倍,那么会触发的扩容次数为:0→1→2→4→8→16→32→64→128→256→512→1024,共11次扩容,每次扩容都要拷贝当前所有元素,累计拷贝次数较多。

3. 扩容优化:提前 reserve 分配内存

针对扩容的性能损耗,STL 提供了 reserve() 函数,允许我们提前分配足够的容量,避免频繁扩容。比如上面的例子,我们提前 reserve(1000),就可以只分配一次内存,插入1000个元素时无需触发任何扩容,大幅提升性能。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v1;
    vector<int> v2;
    
    // v2提前分配容量1000,避免扩容
    v2.reserve(1000);
    
    // 插入1000个元素,对比两者性能(肉眼可见v2更快)
    for (int i = 0; i < 1000; i++) {
        v1.push_back(i);
        v2.push_back(i);
    }
    
    cout << v1.capacity() << endl; // 1024(多次扩容后的容量)
    cout << v2.capacity() << endl; // 1000(提前分配的容量)
    return 0;
}

注意:reserve() 只能增大容量,不能减小容量;如果传入的参数小于当前容量,reserve() 不会做任何操作。

4. 扩容导致的迭代器失效问题

这是vector使用中最常见的"坑"------当vector触发扩容后,原内存被释放,指向原内存的迭代器、指针、引用都会变成"野指针",继续使用会导致程序崩溃或未定义行为。

cpp 复制代码
vector<int> v = {1, 2, 3};
vector<int>::iterator it = v.begin(); // it指向原内存的第一个元素(地址假设为0x123)

// 插入元素,触发扩容(原容量3,插入第4个元素,扩容到6)
v.push_back(4);
v.push_back(5);
v.push_back(6);
v.push_back(7); // 触发扩容,原内存释放,it变成野指针

// 继续使用it,未定义行为(可能崩溃、输出乱码)
cout << *it << endl; 

// 解决方法:扩容后,重新获取迭代器
it = v.begin();
cout << *it << endl; // 正常输出1

四、vector 进阶用法:自定义类型、排序与去重

vector 不仅支持内置类型,还支持自定义结构体、类,同时可以与STL算法协同使用,实现排序、去重等高级操作,这也是vector高频使用的场景之一。

1. vector 存储自定义结构体

存储自定义结构体时,需确保结构体有默认构造函数(如果手动定义了带参构造,需显式定义默认构造),否则初始化时会报错。

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 自定义结构体:学生信息
struct Student {
    string name;
    int age;
    int score;
    
    // 默认构造函数(必须有,否则vector<Student> v(5)会报错)
    Student() : name(""), age(0), score(0) {}
    // 带参构造函数(方便初始化)
    Student(string n, int a, int s) : name(n), age(a), score(s) {}
};

int main() {
    // 初始化自定义结构体vector
    vector<Student> students;
    students.push_back(Student("张三", 18, 90));
    students.push_back(Student("李四", 19, 85));
    students.push_back(Student("王五", 18, 95));
    
    // 遍历输出
    for (const auto& stu : students) { // 用const引用,避免拷贝,提高效率
        cout << "姓名:" << stu.name << ",年龄:" << stu.age << ",成绩:" << stu.score << endl;
    }
    
    return 0;
}

2. vector 排序(配合 sort 算法)

sort 是STL最常用的排序算法,vector支持随机访问迭代器,因此可以直接使用sort对vector进行排序。默认是升序排序,也可以自定义排序规则(降序、按结构体字段排序等)。

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // sort算法头文件
using namespace std;

// 自定义排序规则:降序排序(函数形式)
bool cmpDesc(int a, int b) {
    return a > b; // a > b 时,a排在b前面,即降序
}

// 自定义结构体
struct Student {
    string name;
    int score;
    // 按成绩降序排序(成员函数形式,可选)
    bool operator<(const Student& other) const {
        return score > other.score;
    }
};

int main() {
    // 1. 内置类型排序
    vector<int> v1 = {3, 1, 4, 1, 5, 9};
    sort(v1.begin(), v1.end()); // 默认升序:1 1 3 4 5 9
    sort(v1.begin(), v1.end(), cmpDesc); // 自定义降序:9 5 4 3 1 1
    
    // 2. 自定义结构体排序(两种方式)
    vector<Student> v2;
    v2.push_back({"张三", 90});
    v2.push_back({"李四", 85});
    v2.push_back({"王五", 95});
    
    // 方式1:使用成员函数 operator<(已定义按成绩降序)
    sort(v2.begin(), v2.end());
    
    // 方式2:使用lambda表达式(更灵活,无需定义额外函数)
    sort(v2.begin(), v2.end(), [](const Student& a, const Student& b) {
        return a.score > b.score; // 按成绩降序
    });
    
    // 遍历输出
    for (const auto& stu : v2) {
        cout << stu.name << ":" << stu.score << endl;
    }
    
    return 0;
}

3. vector 去重(配合 unique + erase 算法)

vector 本身没有去重接口,但可以通过 STL 的 unique 算法(去重)+ erase 函数(删除重复元素)实现去重。注意:unique 算法只能去除"相邻的重复元素",因此去重前必须先排序。

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v = {3, 1, 4, 1, 5, 9, 5, 3};
    
    // 步骤1:先排序,让重复元素相邻
    sort(v.begin(), v.end()); // 排序后:1 1 3 3 4 5 5 9
    
    // 步骤2:unique去重,返回指向第一个重复元素的迭代器
    auto it = unique(v.begin(), v.end()); // 去重后:1 3 4 5 9 ? ? ?(后面的元素未删除)
    
    // 步骤3:erase删除重复元素(从it到末尾)
    v.erase(it, v.end());
    
    // 输出去重后的结果:1 3 4 5 9
    for (int val : v) {
        cout << val << " ";
    }
    
    return 0;
}

五、vector 使用避坑指南(高频错误总结)

结合前面的讲解,总结vector使用中最容易出现的5个错误,以及对应的解决方案,帮你规避隐患,写出更安全、高效的代码。

1. 越界访问(最常见错误)

错误:使用 v[i] 访问下标超出 size()-1 的元素(如 v.size() 为5,访问 v[5]),无越界检查,导致程序崩溃或未定义行为。

解决方案:

  • 访问前判断下标是否合法(i < v.size());

  • 使用 at() 访问(有越界检查,抛出异常,便于调试);

  • 避免在循环中使用固定下标(如 for (int i = 0; i < 10; i++),忽略 v.size() 可能小于10)。

2. 迭代器失效(最隐蔽错误)

错误:扩容后继续使用原迭代器、指针、引用;或在 erase() 后继续使用被删除位置的迭代器。

解决方案:

  • 扩容后,重新获取迭代器(如 it = v.begin());

  • erase() 会返回指向被删除元素下一个位置的迭代器,可利用返回值更新迭代器(如 it = v.erase(it));

  • 提前 reserve() 分配足够容量,避免频繁扩容。

3. 混淆 size() 和 capacity()

错误:认为 v.capacity() == v.size(),或用 capacity() 判断元素个数(如 for (int i = 0; i < v.capacity(); i++))。

解决方案:

  • size():当前元素个数,用于遍历、判断元素是否存在;

  • capacity():底层容量,仅用于扩容优化(reserve()),不用于元素访问。

4. 频繁在中间插入/删除元素

错误:在vector中间频繁插入/删除元素(如 v.insert(v.begin() + 1, val)),导致大量元素移动,性能低下。

解决方案:

  • 如果需要频繁在中间插入/删除,优先使用 list(双向链表,任意位置插入删除效率 O(1));

  • 若必须使用vector,可尽量将插入/删除操作集中在尾部(push_back/pop_back)。

5. 未释放内存(内存泄漏隐患)

错误:vector 存储指针类型(如 vector<int*>)时,只 clear() 清空元素,未释放指针指向的内存,导致内存泄漏。

解决方案:

  • 清空vector前,遍历所有元素,手动释放指针指向的内存;

  • 优先使用智能指针(如 shared_ptr、unique_ptr),避免手动管理内存。

cpp 复制代码
// 错误示例:内存泄漏
vector<int*> v;
v.push_back(new int(1));
v.push_back(new int(2));
v.clear(); // 仅清空元素,未释放new分配的内存,导致内存泄漏

// 正确示例:手动释放内存
vector<int*> v;
v.push_back(new int(1));
v.push_back(new int(2));
// 遍历释放内存
for (int* p : v) {
    delete p; // 释放指针指向的内存
    p = nullptr; // 避免野指针
}
v.clear(); // 再清空元素

六、总结:vector 的核心价值与使用建议

vector 作为 STL 中最常用的容器,其核心价值在于"平衡了效率与便捷性"------既有普通数组的随机访问高效性,又有动态数组的灵活性,同时封装了内存管理,让开发者无需关注底层细节。

最后,给大家几点使用建议,帮助你更好地运用vector:

  1. 优先使用 vector,除非有明确的场景需求(如频繁中间插入删除用 list,高频查找用 unordered_map);

  2. 插入大量元素时,提前用 reserve() 分配容量,优化性能;

  3. 访问元素优先用 [ ](高效),调试时可用 at()(安全);

  4. 遍历元素优先用范围for循环(简洁),需要迭代器时用普通迭代器(通用);

  5. 规避迭代器失效、越界访问、内存泄漏三大坑,写出安全、高效的代码。

vector 的用法看似简单,但想要真正用好、用活,需要深入理解其底层扩容机制和注意事项。希望本文的全面解析,能帮助你彻底吃透vector,在笔试面试和实际开发中,都能灵活运用这个STL"利器"。

最后,附上本文所有核心示例代码汇总,方便大家复制测试、快速上手:

cpp 复制代码
// 本文核心示例代码汇总
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

// 1. 初始化与基础操作
void testInitAndBase() {
    vector<int> v1;
    vector<int> v2(5, 3);
    vector<int> v3 = {1, 2, 3, 4, 5};
    
    v3.push_back(6);
    v3.insert(v3.begin() + 2, 10);
    v3.pop_back();
    v3.erase(v3.begin() + 1);
    
    for (int val : v3) {
        cout << val << " ";
    }
    cout << endl;
}

// 2. 容量操作与扩容优化
void testCapacity() {
    vector<int> v;
    v.reserve(10);
    for (int i = 0; i < 10; i++) {
        v.push_back(i);
    }
    cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
}

// 3. 自定义结构体与排序
struct Student {
    string name;
    int score;
    Student(string n, int s) : name(n), score(s) {}
};

void testSort() {
    vector<Student> v = {{"张三", 90}, {"李四", 85}, {"王五", 95}};
    sort(v.begin(), v.end(), [](const Student& a, const Student& b) {
        return a.score > b.score;
    });
    for (const auto& stu : v) {
        cout << stu.name << ":" << stu.score << endl;
    }
}

// 4. 去重操作
void testUnique() {
    vector<int> v = {3, 1, 4, 1, 5, 9, 5, 3};
    sort(v.begin(), v.end());
    auto it = unique(v.begin(), v.end());
    v.erase(it, v.end());
    for (int val : v) {
        cout << val << " ";
    }
    cout << endl;
}

// 5. 指针类型vector,避免内存泄漏
void testPointer() {
    vector<int*> v;
    v.push_back(new int(1));
    v.push_back(new int(2));
    for (int* p : v) {
        delete p;
        p = nullptr;
    }
    v.clear();
    cout << "内存释放完成" << endl;
}

int main() {
    testInitAndBase();
    testCapacity();
    testSort();
    testUnique();
    testPointer();
    return 0;
}
相关推荐
小妖6661 小时前
js 实现插入排序算法(希尔排序算法)
java·算法·排序算法
星火开发设计1 小时前
标准模板库 STL:C++ 的利器 —— 容器、算法、迭代器
java·开发语言·数据结构·c++·算法·html
MX_93592 小时前
Spring注解方式整合Mybatis
java·后端·spring·mybatis
无巧不成书02182 小时前
Kotlin Multiplatform(KMP)核心解析
android·开发语言·kotlin·交互·harmonyos
天開神秀2 小时前
解决 n8n 在 Windows 上安装社区节点时 `spawn npm ENOENT/EINVAL` 错误
前端·windows·npm
wuqingshun3141592 小时前
谈谈你对泛型的理解
java·开发语言·jvm
工业HMI实战笔记2 小时前
图标标准化:一套可复用的工业图标库设计指南
前端·ui·性能优化·自动化·汽车·交互
重生之后端学习2 小时前
105. 从前序与中序遍历序列构造二叉树
java·数据结构·后端·算法·深度优先
前路不黑暗@2 小时前
Java项目:Java脚手架项目的地图的POJO
android·java·开发语言·spring boot·学习·spring cloud·maven