C++ STL 中的 vector:从使用到模拟实现

        在 C++ STL(标准模板库)中,vector 是最常用也最重要的容器之一。它以动态数组的形式存在,既能像普通数组一样高效访问元素,又能灵活地动态调整大小,满足不同场景下的数据存储需求。学习 vector,我们可以遵循 STL 的三个境界 ——能用、明理、能扩展,逐步掌握其核心用法与底层原理。本文将从 vector 的基础介绍、常用接口使用,到深度剖析与模拟实现,带你全面吃透 vector。

一、vector 基础:是什么与怎么用

1.1 认识 vector

vector 本质是一个动态数组,其底层通过连续的内存空间存储元素,并支持动态扩容。与普通数组相比,它的核心优势在于:

        无需预先确定固定大小,可根据元素数量自动调整容量;

        提供了丰富的接口(如插入、删除、容量管理等),简化开发;

        支持随机访问(通过operator[]at()),访问效率与普通数组一致(时间复杂度 O (1))。

在实际开发中,我们无需关心 vector 底层内存的分配与释放,只需调用其接口即可完成数据操作 —— 这是 “能用” vector 的第一步。

1.2 vector 核心接口实战

vector 的接口众多,但只需重点掌握常用接口,就能应对 80% 以上的场景。以下按 “定义→迭代器→空间管理→增删查改” 的顺序,梳理核心接口的用法。

1.2.1 vector 的定义(构造函数)

vector 提供了 4 种常用构造方式,具体如下表所示:

构造函数声明接口说明代码示例
vector()(重点)无参构造,创建空 vectorvector<int> vec;(空 vector,size=0,capacity=0)
vector(size_type n, const value_type& val = value_type())构造并初始化 n 个 valvector<int> vec(5, 3);(包含 5 个 3,size=5,capacity=5)
vector(const vector& x)(重点)拷贝构造,复制另一个 vectorvector<int> vec2(vec);(vec2 是 vec 的副本)
vector(InputIterator first, InputIterator last)迭代器构造,复制 [first, last) 区间元素int arr[] = {1,2,3}; vector<int> vec(arr, arr+3);(vec 包含 1、2、3)
1.2.2 迭代器:遍历 vector 的 “工具”

迭代器的作用是让算法(如findsort)无需关心底层数据结构,vector 的迭代器本质是对 “原生态指针” 的封装(如T*)。核心迭代器接口如下:

迭代器接口接口说明代码示例
begin() + end()(重点)begin():指向第一个元素;end():指向最后一个元素的下一个位置(无效位置)for (auto it = vec.begin(); it != vec.end(); ++it) { cout << *it << " "; }(遍历所有元素)
rbegin() + rend()rbegin():指向最后一个元素(反向迭代器起点);rend():指向第一个元素的前一个位置for (auto it = vec.rbegin(); it != vec.rend(); ++it) { cout << *it << " "; }(反向遍历)

注意:end()rend()指向的是 “无效位置”,不能直接解引用(*it)。

1.2.3 空间管理:size 与 capacity 的区别

vector 的 “空间” 分为两种:

        size:当前已存储的元素个数;

        capacity:当前底层内存可容纳的最大元素个数(容量)。

size == capacity时,再插入元素会触发扩容—— 这是 vector 性能优化的关键场景。核心空间接口如下:

空间接口接口说明关键注意点
size()获取当前元素个数时间复杂度 O (1)
capacity()获取当前容量容量 >= 元素个数
empty()判断是否为空(size==0)常用于遍历前判断
resize(size_type n, val)(重点)调整 size 为 n:- 若 n > 原 size,新增元素用 val 填充;- 若 n < 原 size,删除末尾元素会改变 size,可能改变 capacity(n > 原 capacity 时扩容)
reserve(size_type n)(重点)调整 capacity 为 n(仅当 n > 原 capacity 时生效)仅改变 capacity,不改变 size,不初始化元素
扩容机制的差异(面试常考)

不同编译器的 STL 实现,扩容倍数不同:

        VS(PJ 版本 STL):1.5 倍扩容;

        G++(SGI 版本 STL):2 倍扩容。

例如,插入 100 个元素时的扩容日志:

        VS:1 → 2 → 3 → 4 → 6 → 9 → ... → 141;

        G++:1 → 2 → 4 → 8 → 16 → 32 → 64 → 128。

优化建议:如果提前知道元素个数,用reserve(n)预先分配容量,可避免多次扩容(扩容需经历 “开辟新空间→拷贝元素→释放旧空间”,代价较高)。

示例代码(预分配容量优化):

void TestVectorExpandOP() {
    vector<int> v;
    v.reserve(100); // 提前分配100的容量,避免插入时扩容
    for (int i = 0; i < 100; ++i) {
        v.push_back(i); // 无扩容,效率更高
    }
}
1.2.4 增删查改:常用操作接口

vector 的增删查改接口围绕 “尾操作” 和 “任意位置操作” 展开,核心接口如下:

接口名称接口说明时间复杂度代码示例
push_back(val)(重点)尾插元素 valO (1)(无扩容时)/ O (n)(扩容时)vec.push_back(4);(在 vec 末尾加 4)
pop_back()(重点)尾删最后一个元素O(1)vec.pop_back();(删除末尾元素,size 减 1)
find(iterator first, iterator last, val)查找 val 在 [first, last) 的位置(算法模块接口,非 vector 成员O(n)auto pos = find(vec.begin(), vec.end(), 3);(找值为 3 的位置)
insert(pos, val)在 pos 位置前插入 valO (n)(pos 后元素需后移)vec.insert(pos, 5);(在 pos 前插入 5)
erase(pos)删除 pos 位置的元素O (n)(pos 后元素需前移)vec.erase(pos);(删除 pos 位置元素)
swap(vec2)交换两个 vector 的内存空间O (1)(仅交换指针,不拷贝元素)vec.swap(vec2);
operator[](index)(重点)像数组一样访问 index 位置元素O(1)cout << vec[2];(访问第 3 个元素,无越界检查)

注意:operator[]不做越界检查,若需越界检查,可使用at(index)(越界时抛异常)。

1.3 迭代器失效:vector 的 “坑” 与解决办法

迭代器失效是 vector 使用中的常见问题,本质是 “迭代器底层指针指向的内存空间被销毁或失效”,继续使用会导致程序崩溃或结果错误。

哪些操作会导致迭代器失效?
  1. 触发扩容的操作resize()reserve()insert()push_back()assign()等 —— 这些操作可能开辟新内存、释放旧内存,导致原迭代器指向的旧内存无效。

    示例(扩容导致失效):

    vector<int> vec{1,2,3};
    auto it = vec.begin(); // it指向旧内存(地址A)
    vec.reserve(100);      // 开辟新内存(地址B),释放旧内存(A)
    cout << *it;           // 错误!it指向已释放的内存,程序崩溃(VS下)
    
  2. 删除操作(erase ())

    • 若删除的是非末尾元素:pos 后元素前移,pos 迭代器仍指向原位置,但该位置元素已改变,逻辑上失效;
    • 若删除的是末尾元素:pos 迭代器指向end()(无效位置),直接失效。

    示例(erase 导致失效):

    vector<int> vec{1,2,3,4};
    auto pos = find(vec.begin(), vec.end(), 3);
    vec.erase(pos);        // 删除3,pos指向原位置(现在元素是4)
    cout << *pos;          // VS下崩溃,G++下输出4(逻辑错误)
    
迭代器失效的解决办法

核心原则:操作后重新赋值迭代器

        扩容操作后:重新调用begin()/end()获取新迭代器;

  erase()操作后:利用erase()的返回值(返回删除位置的下一个有效迭代器)重新赋值。

正确示例(删除所有偶数):

vector<int> vec{1,2,3,4};
auto it = vec.begin();
while (it != vec.end()) {
    if (*it % 2 == 0) {
        it = vec.erase(it); // 关键:用返回值更新it
    } else {
        ++it;
    }
}
// 结果:vec = {1,3}(正确)

1.4 vector 在 OJ 中的实战应用

vector 是算法题(OJ)中的 “常客”,以下列举两个经典案例,帮助理解接口的实际用法。

案例 1:只出现一次的数字(LeetCode 136)

题目:给定非空数组,除一个元素只出现一次外,其余均出现两次,找出该元素。思路:利用异或运算(^)的性质:a^a=0a^0=a,遍历数组异或所有元素,结果即为目标值。

代码实现:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (auto e : nums) {
            res ^= e; // 遍历异或
        }
        return res;
    }
};
案例 2:杨辉三角(LeetCode 118)

题目:生成前 n 行杨辉三角,每行首尾为 1,中间第 j 个元素 = 上一行第 j-1 个 + 上一行第 j 个。思路:用二维 vector(vector<vector<int>>)存储,先初始化每行大小并填充 1,再计算中间元素。

代码实现:

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv(numRows); // 初始化n行
        // 每行初始化大小为i+1,填充1
        for (int i = 0; i < numRows; ++i) {
            vv[i].resize(i + 1, 1);
        }
        // 计算中间元素(从第3行开始)
        for (int i = 2; i < numRows; ++i) {
            for (int j = 1; j < i; ++j) {
                vv[i][j] = vv[i-1][j-1] + vv[i-1][j];
            }
        }
        return vv;
    }
};

二、vector 深度剖析:底层原理与模拟实现

掌握了 vector 的使用后,我们需要 “明理”—— 理解其底层结构,甚至 “扩展”—— 自己模拟实现核心接口。

2.1 vector 的底层结构

vector 的核心成员变量(SGI 版本)如下:

  T* _start:指向底层内存的起始位置(第一个元素);

  T* _finish:指向底层内存的末尾位置(最后一个元素的下一个位置);

  T* _end_of_storage:指向底层内存的容量末尾位置(容量的最后一个位置)。

三者的关系满足:

  size() = _finish - _start(元素个数);

  capacity() = _end_of_storage - _start(容量);

        当_finish == _end_of_storage时,插入元素触发扩容。

2.2 模拟实现 vector 的核心接口

我们以bit::vector为例,模拟实现 vector 的核心接口(构造、析构、push_back、reserve、resize 等)。

2.2.1 核心成员变量定义

namespace bit {
template <class T>
class vector {
public:
    // 迭代器(本质是指针)
    typedef T* iterator;
    typedef const T* const_iterator;

    // 核心成员变量
    iterator _start;
    iterator _finish;
    iterator _end_of_storage;

    // 构造函数、析构函数、接口...
};
}
2.2.2 构造函数与析构函数

// 无参构造
vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}

// 析构函数(释放内存)
~vector() {
    if (_start) {
        delete[] _start; // 释放动态内存
        _start = _finish = _end_of_storage = nullptr;
    }
}

// 构造n个val
vector(size_t n, const T& val = T()) {
    reserve(n); // 先开辟n的容量
    for (size_t i = 0; i < n; ++i) {
        push_back(val); // 尾插n个val
    }
}

// 拷贝构造(深拷贝)
vector(const vector& v) {
    reserve(v.capacity()); // 开辟与v相同的容量
    // 遍历v,拷贝每个元素(调用T的拷贝构造)
    for (const auto& e : v) {
        push_back(e);
    }
}
2.2.3 reserve 与 resize 的实现

// 调整容量(仅扩容,不初始化)
void reserve(size_t n) {
    if (n > capacity()) {
        size_t old_size = size();
        // 1. 开辟新内存
        T* new_mem = new T[n];
        // 2. 拷贝旧元素到新内存(注意:不能用memcpy!)
        if (_start) {
            // 用循环调用T的赋值运算符,实现深拷贝
            for (size_t i = 0; i < old_size; ++i) {
                new_mem[i] = _start[i];
            }
            // 3. 释放旧内存
            delete[] _start;
        }
        // 4. 更新指针
        _start = new_mem;
        _finish = _start + old_size;
        _end_of_storage = _start + n;
    }
}

// 调整size(初始化元素)
void resize(size_t n, const T& val = T()) {
    if (n > size()) {
        // 若n超过容量,先扩容
        if (n > capacity()) {
            reserve(n);
        }
        // 填充新元素
        while (_finish < _start + n) {
            *_finish = val;
            ++_finish;
        }
    } else {
        // n小于size,直接缩小_finish
        _finish = _start + n;
    }
}
2.2.4 push_back 的实现

void push_back(const T& val) {
    // 若容量不足,扩容(默认2倍,若原容量为0则扩为1)
    if (_finish == _end_of_storage) {
        size_t new_cap = (capacity() == 0) ? 1 : capacity() * 2;
        reserve(new_cap);
    }
    // 尾插元素
    *_finish = val;
    ++_finish;
}

2.3 模拟实现中的坑:memcpy 的浅拷贝问题

reserve的实现中,我们用循环赋值代替memcpy—— 这是因为memcpy是 “二进制浅拷贝”,当 vector 存储的是自定义类型(如 string) 时,会导致内存泄漏或崩溃。

问题分析

vector<string>为例,若用memcpy拷贝元素:

  1. memcpy会直接拷贝string对象的二进制数据(包括string内部的指针_str);
  2. 扩容后释放旧内存时,会调用string的析构函数,释放_str指向的内存;
  3. 新内存中的string对象仍指向已释放的_str,后续访问会导致 “野指针” 错误。
结论

        若 vector 存储的是内置类型(int、char 等)memcpy可正常工作;

        若存储的是自定义类型(含资源管理),必须用 “深拷贝”(如循环赋值、调用拷贝构造),避免浅拷贝问题。

2.4 动态二维数组:vector 的嵌套使用

vector 支持嵌套定义(如vector<vector<int>>),可用来实现 “动态二维数组”—— 每行的长度可独立调整,比普通二维数组(如int arr[5][5])更灵活。

示例:杨辉三角的动态二维数组结构

n=5时,vector<vector<int>> vv(5)的初始结构(每行无元素):

  vv[0]_start=nullptr_finish=nullptr_end_of_storage=nullptr

  vv[1]:同上;

        ...

  vv[4]:同上。

调用vv[i].resize(i+1, 1)后,每行被初始化为i+1个 1,最终结构:

  vv[0][1](size=1,capacity=1);

  vv[1][1, 1](size=2,capacity=2);

  vv[2][1, 2, 1](size=3,capacity=3);

        ...

三、总结

vector 作为 C++ STL 的核心容器,其学习路径清晰:

  1. 能用:掌握构造、迭代器、空间管理、增删查改等核心接口,能在项目和 OJ 中灵活使用;
  2. 明理:理解底层结构(_start/_finish/_end_of_storage)、扩容机制、迭代器失效原因;
  3. 能扩展:模拟实现核心接口,规避浅拷贝(如 memcpy)等问题,甚至根据需求自定义 vector。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值