C++ vector 深度剖析:从入门到模拟实现,避开所有坑

一、为什么 vector 是"最强容器"?

在 C++ 标准模板库(STL)中,vector 是一个动态数组。它与普通数组的区别在于:

  • 大小可以自动增长(不需要手动 realloc)。

  • 支持随机访问([] 运算符,O(1) 时间)。

  • 在尾部增删元素效率高(均摊 O(1))。

  • 提供丰富的成员函数,如 push_backpop_backinserterase 等。

学习 STL 有三个境界:能用 → 明理 → 能扩展。本文会帮你至少达到第二个境界,并向第三个境界迈进。

二、vector 的基础使用

使用 vector 需要包含 <vector> 头文件,并引入 std 命名空间。

2.1 构造方式

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

int main() {
    vector<int> v1;               // 空 vector
    vector<int> v2(5, 10);        // 5 个元素,每个都是 10
    vector<int> v3(v2);           // 拷贝构造
    vector<int> v4(v2.begin(), v2.end()); // 迭代器区间构造

    int arr[] = {1,2,3,4};
    vector<int> v5(arr, arr+4);   // 使用数组构造
    return 0;
}

2.2 迭代器与遍历

vector 支持随机访问迭代器,常用方式有三种:

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

// 1. 下标 operator[]
for (size_t i = 0; i < v.size(); ++i)
    cout << v[i] << " ";

// 2. 迭代器
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
    cout << *it << " ";

// 3. C++11 范围 for
for (auto e : v)
    cout << e << " ";

2.3 容量相关:size、capacity、resize、reserve

这是 vector 新手最容易混淆的地方。

  • size():当前实际存储的元素个数。

  • capacity() :当前分配的内存能容纳的元素个数(capacity >= size)。

  • resize(n, val) :改变 sizen。若 n > size,则用 val 填充(默认用 0 或默认构造);若 n < size,则截断。它会影响 size,也可能改变 capacity

  • reserve(n) :预留至少 n 个元素的空间(改变 capacity 但不改变 size)。常用于提前知道元素数量,避免多次扩容。

    vector v;
    v.reserve(100); // 容量至少 100,size 仍为 0
    for (int i = 0; i < 100; ++i)
    v.push_back(i); // 不会触发扩容
    cout << v.size() << " " << v.capacity() << endl; // 100 至少100

2.4 扩容机制:1.5 倍还是 2 倍?

不同 STL 实现的扩容策略不同,这是一个常考的点。

  • VS (微软) :按 1.5 倍 扩容。

  • g++ (Linux / SGI STL) :按 2 倍 扩容。

测试代码:

复制代码
vector<int> v;
size_t sz = v.capacity();
for (int i = 0; i < 100; ++i) {
    v.push_back(i);
    if (sz != v.capacity()) {
        sz = v.capacity();
        cout << "capacity changed: " << sz << endl;
    }
}

VS 输出示例:1, 2, 3, 4, 6, 9, 13, 19, 28, 42, 63, 94, 141 ...

g++ 输出:1, 2, 4, 8, 16, 32, 64, 128 ...

结论 :不要迷信"2 倍"这个说法。编写可移植代码时,不要假设扩容因子。需要高效时,主动使用 reserve

三、增删查改操作

函数 作用
push_back(val) 尾插
pop_back() 尾删
insert(pos, val) 在迭代器 pos 前插入 val
erase(pos) 删除迭代器 pos 处的元素
find(begin, end, val) 算法中的查找,不是 vector 成员
swap(vec) 交换两个 vector 的数据
operator[] 随机访问

示例:

复制代码
vector<int> v = {1, 2, 3};
v.push_back(4);           // 1 2 3 4
v.pop_back();             // 1 2 3
v.insert(v.begin(), 0);   // 0 1 2 3
v.erase(v.begin() + 1);   // 0 2 3

四、迭代器失效 ------ 最容易踩的坑

迭代器本质是一个指针(或封装后的指针),指向容器中的某个元素。当容器的内存布局发生变化时,旧的迭代器可能指向无效内存,称为迭代器失效

4.1 会导致扩容的操作(insert、push_back、reserve、resize、assign)

这些操作可能重新分配内存,使得原有的迭代器全部失效。

复制代码
vector<int> v{1,2,3};
auto it = v.begin();
v.reserve(100);      // 扩容,it 失效
// 此时 it 已经不安全,不能再使用
while (it != v.end()) { // 错误!可能崩溃
    cout << *it;
}

正确做法:在可能导致扩容的操作后,重新获取迭代器。

复制代码
it = v.begin();      // 重新赋值

4.2 erase 导致的失效

erase 删除元素后,被删除元素及其之后的所有迭代器都会失效(因为元素发生了移动)。典型的错误写法:

复制代码
vector<int> v{1,2,3,4};
auto it = v.begin();
while (it != v.end()) {
    if (*it % 2 == 0)
        v.erase(it);   // 错误:erase 后 it 失效,再 ++it 就是野指针
    ++it;
}

正确写法 :利用 erase 返回下一个有效迭代器。

复制代码
while (it != v.end()) 
{ if (*it % 2 == 0) it = v.erase(it); 
// erase 返回被删除元素的下一个位置 else ++it;
 }

4.3 Linux (g++) 与 VS 的差异

  • VS 对迭代器失效非常敏感,一旦使用失效迭代器,大概率立即崩溃(调试模式下会断言)。

  • g++ 则相对宽容,扩容后旧迭代器可能仍指向原内存(但已被释放),程序可能"看起来正常",实则存在隐患,例如输出乱码或段错误。

建议:统一按照"任何修改容量的操作都会导致迭代器失效"来编程,不要依赖编译器行为。

五、OJ 实战:巩固 vector 使用

5.1 只出现一次的数字(异或法)

复制代码
int singleNumber(vector<int>& nums) {
    int ret = 0;
    for (auto e : nums) ret ^= e;
    return ret;
}

5.2 杨辉三角(vector<vector<int>>)

复制代码
vector<vector<int>> generate(int numRows) {
    vector<vector<int>> vv(numRows);
    for (int i = 0; i < numRows; ++i) {
        vv[i].resize(i + 1, 1);
    }
    for (int i = 2; i < numRows; ++i) {
        for (int j = 1; j < i; ++j) {
            vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
        }
    }
    return vv;
}

练习推荐:

  • 删除排序数组中的重复项

  • 数组中出现次数超过一半的数字

  • 电话号码的字母组合

六、模拟实现 vector:核心框架

为了深入理解 vector,我们尝试自己实现一个简化版,命名为 bit::vector

6.1 成员变量与基本接口

复制代码
namespace bit {
    template<class T>
    class vector {
    public:
        // 迭代器就是原生指针
        typedef T* iterator;
        typedef const T* const_iterator;

        // 构造、析构
        vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
        vector(int n, const T& val = T()) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {
            reserve(n);
            for (int i = 0; i < n; ++i)
                push_back(val);
        }
        ~vector() {
            delete[] _start;
            _start = _finish = _end_of_storage = nullptr;
        }

        // 迭代器
        iterator begin() { return _start; }
        iterator end() { return _finish; }
        const_iterator begin() const { return _start; }
        const_iterator end() const { return _finish; }

        // 容量
        size_t size() const { return _finish - _start; }
        size_t capacity() const { return _end_of_storage - _start; }
        bool empty() const { return _start == _finish; }

        // 元素访问
        T& operator[](size_t pos) { return _start[pos]; }
        const T& operator[](size_t pos) const { return _start[pos]; }

        // 修改
        void push_back(const T& val);
        void pop_back();
        void reserve(size_t n);
        void resize(size_t n, const T& val = T());
        iterator insert(iterator pos, const T& val);
        iterator erase(iterator pos);

    private:
        iterator _start;          // 指向数据起始
        iterator _finish;         // 指向最后一个有效数据的下一个位置
        iterator _end_of_storage; // 指向已分配内存的末尾
    };
}

6.2 核心实现:reserve 与 push_back

复制代码
template<class T>
void vector<T>::reserve(size_t n) {
    if (n > capacity()) {
        size_t old_size = size();
        T* new_data = new T[n];
        if (_start) {
            // 拷贝旧数据
            for (size_t i = 0; i < old_size; ++i)
                new_data[i] = _start[i];
            delete[] _start;
        }
        _start = new_data;
        _finish = _start + old_size;
        _end_of_storage = _start + n;
    }
}

template<class T>
void vector<T>::push_back(const T& val) {
    if (_finish == _end_of_storage) {
        size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
        reserve(new_cap);
    }
    *_finish = val;
    ++_finish;
}

6.3 深拷贝与 insert/erase

复制代码
template<class T>
typename vector<T>::iterator vector<T>::insert(iterator pos, const T& val) {
    // 判断扩容
    if (_finish == _end_of_storage) {
        size_t offset = pos - _start;
        reserve(capacity() == 0 ? 1 : capacity() * 2);
        pos = _start + offset;   // 更新 pos,因为扩容后原迭代器失效
    }
    // 后移元素
    for (iterator it = _finish; it > pos; --it)
        *it = *(it - 1);
    *pos = val;
    ++_finish;
    return pos;
}

template<class T>
typename vector<T>::iterator vector<T>::erase(iterator pos) {
    for (iterator it = pos; it < _finish - 1; ++it)
        *it = *(it + 1);
    --_finish;
    return pos;   // 返回被删除元素的下一个位置
}

七、致命陷阱:memcpy 浅拷贝问题

在模拟实现 reserve 时,如果用 memcpy 进行内存复制会怎样?

复制代码
// 错误示范
void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];
        if (_start) {
            memcpy(tmp, _start, size() * sizeof(T));  // 危险!
            delete[] _start;
        }
        _start = tmp;
        // ...
    }
}

问题

如果 Tstring 或其他管理资源的类型(如 vector<int>),memcpy 只是按字节复制指针(浅拷贝),导致两个对象指向同一块堆内存。当旧对象被 delete[] 时,会析构每个元素,释放资源;而新对象中的元素仍持有已释放的指针,最终导致双重释放内存泄漏

正确做法:使用赋值操作(深拷贝)。

复制代码
for (size_t i = 0; i < old_size; ++i)
    tmp[i] = _start[i];   // 调用 T 的拷贝赋值,实现深拷贝

因此,在编写通用容器时,绝不能使用 memcpy 处理非 POD 类型

八、动态二维数组:vector<vector<T>>

杨辉三角的代码展示了 vector 的嵌套使用。物理上,外层 vector 的每个元素又是一个内层 vector,它们的内存不一定是连续的,但每个内层 vector 内部连续。

复制代码
vector<vector<int>> vv(5);   // 5 行
for (int i = 0; i < 5; ++i)
    vv[i].resize(i+1, 1);    // 每行长度 i+1,初始化 1

这种结构比 C 语言的"指针数组"更安全、更易用。

九、总结与建议

  1. 优先使用 vector:动态数组是绝大多数场景的最佳选择。

  2. 善用 reserve:提前分配空间,避免频繁扩容。

  3. 警惕迭代器失效:任何可能改变容量的操作后,原来持有的迭代器都可能失效,务必重新获取。

  4. 模拟实现是提升内功的最佳途径 :亲手实现 reservepush_backinserterase,你会对深拷贝、浅拷贝、异常安全有更深理解。

  5. 不要用 memcpy 拷贝非 POD 元素:始终使用赋值或拷贝构造。

vector 的用法看似简单,但其中的陷阱和原理值得每个 C++ 开发者反复琢磨。希望这篇文章能帮你彻底掌握 vector,并在面试和工程中游刃有余。

练习题推荐

  • LeetCode 26. 删除有序数组中的重复项

  • LeetCode 118. 杨辉三角

  • LeetCode 17. 电话号码的字母组合

下一篇预告 :我们将深入 list 容器,对比 vectorlist 的优劣,并探讨迭代器失效在不同容器中的表现。敬请期待!

相关推荐
凯瑟琳.奥古斯特1 小时前
力扣1235完整解法详解
java·开发语言·leetcode
z落落1 小时前
C# 继承基础详解(代码实战+权限规则)
java·开发语言
techdashen1 小时前
你想在 Rust 中实现动态库热重载?
开发语言·chrome·rust
不会C语言的男孩1 小时前
C++ Primer 第5章:语句
开发语言·c++
酉鬼女又兒1 小时前
零基础入门计算机网络:从基本概念到核心交换技术
开发语言·计算机网络·考研·职场和发展·php
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇)第三篇:SAP ALV 报表样式定制:字段布局与交互功能配置
服务器·开发语言·学习·交互·sap·abap
chao1898441 小时前
基于SIFT和SURF特征的图像配准(MATLAB)
开发语言·matlab
摇滚侠1 小时前
JDBC 基础到高级一套通关!基础篇 00-15
java·开发语言·数据库
Swift社区2 小时前
OpenHarmony鸿蒙PC平台移植 gifsicle:CC++ 三方库适配实践(Lycium tpc_c_cplusplus)
c语言·c++·harmonyos