C++ vector从入门到模拟实现过程

 更新时间:2026年06月02日 10:22:13   作者:晚风吹红霞  
这段文章详细讲解了C++ STL中的vector容器,从其优势、基础删查改操作到迭代器失效等,并提供了模拟实现和常见陷阱的解析,帮助开发者全面掌握vector的用法,重点强调了reserve的用法、迭代器失效的处理和深拷贝、浅拷贝的区别,感兴趣的朋友一起看看吧

一、为什么 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):改变 size 为 n。若 n > size,则用 val 填充(默认用 0 或默认构造);若 n < size,则截断。它会影响 size,也可能改变 capacity
  • reserve(n):预留至少 n 个元素的空间(改变 capacity 但不改变 size)。常用于提前知道元素数量,避免多次扩容。
vector<int> 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;
        // ...
    }
}

问题
如果 T 是 string 或其他管理资源的类型(如 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 语言的“指针数组”更安全、更易用。

九、总结与建议

  • 优先使用 vector:动态数组是绝大多数场景的最佳选择。
  • 善用 reserve:提前分配空间,避免频繁扩容。
  • 警惕迭代器失效:任何可能改变容量的操作后,原来持有的迭代器都可能失效,务必重新获取。
  • 模拟实现是提升内功的最佳途径:亲手实现 reservepush_backinserterase,你会对深拷贝、浅拷贝、异常安全有更深理解。
  • 不要用 memcpy 拷贝非 POD 元素:始终使用赋值或拷贝构造。

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

练习题推荐

  • LeetCode 26. 删除有序数组中的重复项
  • LeetCode 118. 杨辉三角
  • LeetCode 17. 电话号码的字母组合

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

到此这篇关于C++ vector从入门到模拟实现的文章就介绍到这了,更多相关C++ vector实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言数据结构经典10大排序算法刨析

    C语言数据结构经典10大排序算法刨析

    这篇文章主要介绍了C语言中常用的10种排序算法及代码实现,开发中排序的应用需要熟练的掌握,因为是基础内容,那C语言有哪些排序算法呢?本文小编就来详细说说,需要的朋友可以参考一下
    2022-02-02
  • C语言学习之柔性数组详解

    C语言学习之柔性数组详解

    结构体的最后一个元素允许是未知大小的数组,这就叫柔性数组。这篇文中主要为大家详细介绍了C语言中柔性数组的相关知识,需要的可以了解一下
    2023-03-03
  • 详解C++字符串常用操作函数(查找、插入、截取、删除等)

    详解C++字符串常用操作函数(查找、插入、截取、删除等)

    这篇文章主要介绍了C++字符串常用操作函数(查找、插入、截取、删除等),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • C++超详细讲解树与二叉树

    C++超详细讲解树与二叉树

    在之前的文章里,我们学习的一直是一对一的线性结构,可现实中,还有很多一对多的情况需要处理,所以我们需要研究这样一种一对多的数据结构——树
    2022-05-05
  • C语言深入探究sizeof与整型数据存储及数据类型取值范围

    C语言深入探究sizeof与整型数据存储及数据类型取值范围

    在main函数中,sizeof是可以正常工作的,但是在自定义函数中就不可以了。所以本文将为大家详细讲解一下关键字sizeof、整型数据存储深入、数据类型取值范围深入
    2022-07-07
  • C++实现FTP综合应用详解

    C++实现FTP综合应用详解

    这篇文章主要为大家详细介绍了C++实现FTP综合应用,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • C++不使用变量求字符串长度strlen函数的实现方法

    C++不使用变量求字符串长度strlen函数的实现方法

    这篇文章主要介绍了C++不使用变量求字符串长度strlen函数的实现方法,实例分析了strlen函数的实现原理与不使用变量求字符串长度的实现技巧,需要的朋友可以参考下
    2015-06-06
  • C++数据结构之链表详解

    C++数据结构之链表详解

    这篇文章主要介绍了C++数据结构之链表的创建的相关资料,希望通过本文帮助到大家,让大家理解掌握这部分内容,需要的朋友可以参考下
    2021-08-08
  • 嵌入式C程序优质编写全面教程规范

    嵌入式C程序优质编写全面教程规范

    这是一年前我为公司内部写的一个文档,旨在向年轻的嵌入式软件工程师们介绍如何在裸机环境下编写优质嵌入式C程序。感觉是有一定的参考价值,所以拿出来分享,抛砖引玉
    2022-04-04
  • C语言小程序 数组操作示例代码

    C语言小程序 数组操作示例代码

    对数组进行操作,查找、插入、删除
    2013-07-07

最新评论