C++--vector底层,leetcode118杨辉三角实战,对于传统reserve的坑点,迭代器失效


博主名称:deM0one ⭐️C++专栏: C++

文章目录

  • [深入理解 vector:从二维数组到 STL 源码剖析](#深入理解 vector:从二维数组到 STL 源码剖析)
    • 一、引言
    • [二、二维数组:vector 的嵌套使用](#二、二维数组:vector 的嵌套使用)
      • [2.1 基本用法](#2.1 基本用法)
      • [2.2 二维 vector 的底层结构](#2.2 二维 vector 的底层结构)
    • [三、模拟实现:从 operator\[\] 看模板实例化](#三、模拟实现:从 operator[] 看模板实例化)
      • [3.1 vector<int> 的 operator\[\]](#3.1 vector 的 operator[])
      • [3.2 vector<vector<int>> 的 operator\[\]](#3.2 vector<vector> 的 operator[])
      • [3.3 对比总结](#3.3 对比总结)
    • [四、实战应用:LeetCode 118 杨辉三角](#四、实战应用:LeetCode 118 杨辉三角)
      • [4.1 题目要求](#4.1 题目要求)
      • [4.2 C 语言解法(指针数组)](#4.2 C 语言解法(指针数组))
      • [4.3 C++ vector 解法](#4.3 C++ vector 解法)
      • [4.4 两种解法对比](#4.4 两种解法对比)
    • [五、源码剖析:push_back 的实现](#五、源码剖析:push_back 的实现)
      • [5.1 push_back 完整源码(SGI STL)](#5.1 push_back 完整源码(SGI STL))
      • [5.2 construct:定位 new(placement new)](#5.2 construct:定位 new(placement new))
      • [5.3 迭代器即指针](#5.3 迭代器即指针)
    • [六、源码剖析:insert_aux 的扩容机制](#六、源码剖析:insert_aux 的扩容机制)
      • [6.1 分支一:空间充足](#6.1 分支一:空间充足)
      • [6.2 分支二:需要扩容](#6.2 分支二:需要扩容)
      • [6.3 扩容流程图解](#6.3 扩容流程图解)
      • [6.4 内存图解](#6.4 内存图解)
    • [七、reserve 的模拟实现](#七、reserve 的模拟实现)
    • 八、迭代器失效
    • 九、总结与延伸
      • [9.1 核心要点回顾](#9.1 核心要点回顾)
      • [9.2 延伸思考](#9.2 延伸思考)

深入理解 vector:从二维数组到 STL 源码剖析

一、引言

  • 为什么学习 vector 的底层实现
  • 本篇博客的学习路径:应用 → 模拟实现 → 源码剖析
  • 参考书目:《STL 源码剖析》(侯捷)

二、二维数组:vector 的嵌套使用

2.1 基本用法

cpp 复制代码
vector<int> v(5, 1);
vector<vector<int>> vv(10, v);  // 10 行,每行 5 个 1
vv[2][1] = 2;                   // 修改第 3 行第 2 列

2.2 二维 vector 的底层结构

  • vector<vector<int>> 的外层元素是 vector<int> 对象
  • 外层连续存储"内层 vector 对象",内层各自管理自己的堆内存
  • 图解:外层 _start/_finish/_end_of_storage 指向内层对象数组

三、模拟实现:从 operator\[\] 看模板实例化

3.1 vector 的 operator\[\]

cpp 复制代码
class vector {
    int& operator[](int i) {
        assert(i < _size);
        return _a[i];
    }
private:
    int* _a;
    size_t _size;
    size_t _capacity;
};

3.2 vector<vector> 的 operator\[\]

cpp 复制代码
class vector<vector<int>> {
    vector<int>& operator[](int i) {
        assert(i < _size);
        return _a[i];
    }
private:
    vector<int>* _a;
    size_t _size;
    size_t _capacity;
};
  • 定义方式:先构造一维 vector<int> v(5, 1),再构造 vector<vector<int>> vv(10, v),得到一个 10 行 5 列的二维数组
  • 访问方式:vv[2][1],外层 operator[] 返回 vector<int>&,内层再返回 int&
  • 本质:二维 vector 就是"每个元素是一个 vector 的数组",对应 C 语言里的指针数组思想

3.3 对比总结

  • 同一个类模板,实例化出不同的具体类型
  • 返回值从 int& 变为 vector<int>&,指针从 int* 变为 vector<int>*
  • 模板的本质:编译期生成代码

四、实战应用:LeetCode 118 杨辉三角

4.1 题目要求

  • 生成前 numRows 行的杨辉三角

4.2 C 语言解法(指针数组)

cpp 复制代码
int** generate(int numRows, int* returnSize, int** returnColumnSizes) {
    *returnColumnSizes = malloc(numRows * sizeof(int));
    // ... 手动管理每一行的内存
}
  • 缺点:手动 malloc/free,内存管理复杂,容易出错

4.3 C++ vector 解法

cpp 复制代码
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv(numRows);
        for (size_t i = 0; i < vv.size(); ++i) {
            vv[i].resize(i + 1, 1);  // vv[i][0] = vv[i][i] = 1;
        }
        for (int i = 2; i < vv.size(); ++i) {
            for (int j = 1; j < vv[i].size() - 1; ++j) {
                vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
            }
        }
        return vv;
    }
};
  • vector<vector<int>> generate(int numRows)
  • 步骤:
    1. vector<vector<int>> vv(numRows) 先确定行数
    2. 每行 vv[i].resize(i + 1, 1) 初始化
    3. 首尾元素置 1:vv[i].front() / vv[i].back()
    4. 递推填充中间元素:vv[i][j] = vv[i-1][j-1] + vv[i-1][j]

4.4 两种解法对比

  • 代码量:C++ 版显著更短
  • 内存管理:C++ 自动管理,C 需要手动释放
  • 安全性:vector 带越界检查(assert),C 指针无保护

五、源码剖析:push_back 的实现

5.1 push_back 完整源码(SGI STL)

cpp 复制代码
void push_back(const T& x) {
    if (finish != end_of_storage) {
        construct(finish, x);
        ++finish;
    } else {
        insert_aux(end(), x);
    }
}

5.2 construct:定位 new(placement new)

cpp 复制代码
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
    new(p) T1(value);  // 在指定内存上构造对象
}
  • malloc 的区别:malloc 只分配内存,construct 负责构造对象
  • 为什么不用 *p = value:T1 可能没有默认构造函数/赋值运算符

5.3 迭代器即指针

cpp 复制代码
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return finish - start; }
size_type capacity() const { return end_of_storage - start; }
  • vector 的迭代器本质就是原生指针
  • _start_finish_end_of_storage 三个指针的语义

六、源码剖析:insert_aux 的扩容机制

6.1 分支一:空间充足

cpp 复制代码
if (finish != end_of_storage) {  // 有足够空间
    construct(finish, x);
    ++finish;
    copy_backward(position, finish - 1, finish);  // 元素后移
}

6.2 分支二:需要扩容

cpp 复制代码
const size_type old_size = size();
const size_type len = old_size ? 2 * old_size : 2;  // 两倍扩容
iterator new_start = data_allocator::allocate(len);
iterator new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);       // 在中间构造新元素
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish);
// 释放旧空间,更新指针
destroy(start, finish);
data_allocator::deallocate(start, old_size);
start = new_start;
finish = new_finish;
end_of_storage = start + len;

6.3 扩容流程图解

复制代码
原数组:  [1][2][3][4][5]   容量 5,插入 30 到 pos
扩容后:  [1][2][30][3][4][5][_][_][_][_]   容量 10
                ↑pos         ↑new_finish
  • 三步走:前段拷贝 → 构造新元素 → 后段拷贝
  • 扩容倍率:2 * old_size(GCC 两倍增长)
  • 迭代器失效的本质:旧空间被释放,旧迭代器成为野指针

6.4 内存图解

  • _start → 首元素地址
  • _finish → 最后一个元素的下一个位置
  • _end_of_storage → 容量终点

七、reserve 的模拟实现

  • 手写 reserve 流程
    • n > capacity() 才扩容
    • new T[n]memcpy 拷贝旧数据 → delete[] start
    • 更新三个指针:start = tmp; finish = start + old_size; end_of_storage = start + n;
  • 调试器观察
    • 监视窗口查看 _start_finish_end_of_storage 地址变化
    • 新分配内存中的 0xCDCDCDCD 等调试填充值
  • 坑点:memcpy 是浅拷贝,对 vector<string> 等非 POD 类型会出错
    • 调试时出现"读取字符串字符时出错"
    • 正确做法:for 循环逐元素赋值(深拷贝),或使用 uninitialized_copy
cpp 复制代码
void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];
        memcpy(tmp, _start, size() * sizeof(T));
        delete[] _start;
        _start = tmp;
        _finish = _start + size();
        _end_of_storage = _start + n;
    }
}
  • 只在 n 大于当前容量时才扩容
  • 三指针更新顺序:先建新空间,再搬运,后释放旧空间
  • 注意:memcpy 是浅拷贝,对 string 等含资源的类型不安全(C++11 后应使用拷贝构造/移动语义)

八、迭代器失效

  • insert 后数据发生挪动,原迭代器 pos 指向的元素已变,视为失效
  • 失效迭代器不要访问;VS2019 会强制检查,访问即报错
  • erase 返回新位置迭代器:指向被删除元素的后一个元素

九、总结与延伸

9.1 核心要点回顾

  • vector 的本质:三个指针管理的动态数组
  • 扩容策略:两倍增长,均摊 O(1) 的 push_back
  • placement new 在 STL 中的重要性
  • 迭代器失效问题的根源

9.2 延伸思考

  • 为什么 STL 用 construct/destroy 而不是 new[]/delete[]
  • uninitialized_copy 与普通 copy 的区别
  • C++11 之后 vector 的内存管理有何变化(移动语义、右值引用)
  • 对比 GCC 与 MSVC 的扩容策略差异
  • 动手实现一个自己的 my_vector(完整版:构造/析构/拷贝/赋值/迭代器)

任何的批评与建议欢迎指出,我们共同进步!

相关推荐
曹牧1 小时前
C#:字符串做20位截断
开发语言·c#
醉城夜风~2 小时前
(超详细)C++多态(Polymorphism)
开发语言·c++
这是个栗子2 小时前
【JS代码分析】前端鉴权基础:Token 的本地存储与状态重置实践
开发语言·前端·javascript
wuyk5552 小时前
7.AVL 树:第一个自平衡二叉搜索树
开发语言·stm32·单片机·算法
小小龙学IT2 小时前
现代 C++ 内存管理与资源安全深度解析:从 RAII 到所有权模型
c++·安全
SomeB1oody2 小时前
【RustyML入门】5.3. 聚类指标
开发语言·后端·机器学习·rust·教程
欧特克_Glodon2 小时前
OpenCV计算机视觉开发入门与实践<十三>:图像转换之灰度图、二值图
c++·人工智能·opencv·计算机视觉
OPEN-F2 小时前
Python进阶教程:Web开发入门(Flask)
开发语言·python
神仙别闹2 小时前
基于C++ MFC实现动态分区分配存储管理
开发语言·c++·mfc