云计算百科
云计算领域专业知识百科平台

对于模拟实现C++vector类的详细解析—下

开篇介绍:

hello 大家,咱们 “模拟实现 C++ vector 类” 的上半部分已经把基础骨架搭起来了:从类模板的设计、三个核心指针(mstart/mfinish/mend_of_storage)的作用,到size/capacity/operator[]这些基础接口,再到reserve/push_back/resize这些核心功能的实现…… 相信大家对 vector“如何管理内存、如何动态扩容、如何操作元素” 有了更具象的认识。

不过,vector 的 “精髓” 可不止这些!如果说上半部分是 “搭骨架”,那下半部分就是 “填血肉”—— 咱们要攻克构造函数(如何初始化一个 vector)、拷贝构造(如何复制一个 vector,还不踩浅拷贝的坑)、insert(如何在任意位置插入元素,这涉及到内存移动的大问题)、erase(如何删除任意位置元素,同样要处理内存和迭代器的细节)这些更考验底层逻辑的内容。

这些函数看似只是 “多了几种操作方式”,但背后藏着 C++ 对 “资源深拷贝”“迭代器失效”“内存高效利用” 的深层思考。比如拷贝构造,要是像普通对象一样直接复制指针,两个 vector 就会共享同一块内存,析构时必然重复释放导致崩溃;再比如 insert,在中间位置插入元素时,得把后面的元素一个个 “挪位置”,这过程中怎么保证数据不丢、效率还能接受?

别担心,这些问题咱们都会一一拆解~就像上半部分里,我们从 “为什么用三个指针” 讲到 “为什么reserve不能用memcpy” 一样,下半部分也会把每个函数的设计动机和实现细节掰碎了讲。

所以,准备好继续探索 vector 更 “硬核” 的内部机制了吗?咱们这就往下走~

对vector的构造函数的实现:

那么这一个部分,就是模拟实现vector类的重中之重了,而且我们还会学习到一个新的构造函数的方法——传入迭代器的构造函数,接下来我们就来进行分析。

首先呢,我们要知道,在我们平时使用vector的构造函数创建变量,是传什么参数,然后系统又是生成什么样的vector,这一些在我们的对vector的详细介绍那一篇博客中,都有详细的介绍,所以我在这里就不赘述了。

普通构造函数的实现:

那么其实我们实现普通的构造函数是不难的,甚至可以说是简单的,如果大家有把上一篇博客吃透的话,那么我们接下来来看看实现步骤:

一、参数设计与语义定义

构造函数的参数列表为(size_t n = 1, const type1& val = type1()),需明确两个参数的设计逻辑:

  • 第一个参数 n:指定容器初始包含的元素个数,默认值为 1。其作用是定义容器创建时的有效元素数量(即初始 size 为 n)。
  • 第二个参数 val:
    • 采用const type1&类型(对 type1 对象的只读引用),目的是避免传递参数时产生对象拷贝开销(若直接传type1 val,会生成临时副本,浪费资源),同时通过const修饰保证函数内部无法修改 val 所引用的外部对象,确保初始值的安全性。
    • 默认值为type1():这是适配所有类型的通用初始化方式。对于自定义类型,type1()会调用其默认构造函数;对于内置类型(如 int、double、指针),C++ 标准规定type1()会执行 “值初始化”(int ()→0,double ()→0.0,指针→nullptr)。这种设计无需手动指定具体默认值(如 0),可兼容任意数据类型,解决了 “未知存储类型” 的初始化难题。
  • 二、提前分配内存空间

    构造函数首先调用reserve(n)开辟内存:

    • reserve(n)的作用是确保容器容量(capacity)至少为 n,即分配能容纳 n 个 type1 类型元素的连续内存。这一步的核心目的是 “避免后续插入元素时的频繁扩容”—— 若不提前分配,循环插入 n 个元素可能触发多次扩容(每次扩容需复制旧元素、释放旧空间),导致效率降低。
    • 调用reserve(n)后,容器的mend_of_storage指针会指向新空间的末尾(mstart + n),为后续插入 n 个元素预留足够内存。

    三、循环插入元素完成初始化

    通过for循环(i从 0 到 n-1),每次调用push_back(val)插入元素:

    • 循环次数为 n 次,对应需要初始化的 n 个元素。
    • 复用push_back函数的逻辑:push_back会将 val 赋值到当前mfinish指向的位置(即下一个待插入的空白位置),然后将mfinish指针后移一位。由于此前已通过reserve(n)确保容量充足,循环中push_back不会触发扩容,每次插入仅需执行 “赋值 + 指针移动”,效率极高。
    • 这一步的本质是 “批量初始化”:通过循环将 val(或默认值)写入预留的 n 个内存位置,使容器从 “空状态” 变为 “包含 n 个有效元素的状态”。

    四、构造完成后容器状态的保证

    循环结束后,容器的三个核心指针自动处于正确状态:

    • mstart:指向提前分配的内存空间起始位置(由reserve(n)设置)。
    • mend_of_storage:指向内存空间末尾(mstart + n,由reserve(n)设置),确保容量为 n。
    • mfinish:经过 n 次push_back后,从初始位置(与mstart重合)后移 n 位,最终指向mstart + n,确保有效元素个数(size)为 n。

    此时,容器的 size 与 capacity 均为 n,且每个元素的值均为 val(或 type1 () 的默认值),完全符合构造函数 “创建包含 n 个初始化元素的容器” 的设计目标。

    怎么样大家,其实是不复杂的,和我们前面实现的reserve函数和push_back函数息息相关,所以这也是为什么我们要先去实现reserve函数和push_back函数之后,再去实现构造函数,下面我们就可以看详细代码:

    // 实现构造函数
    vector(size_t n = 1, const type1& val = type1())
    {
    // 第一个参数表示要创建的元素个数,第二个参数表示元素的初始值
    // type1& val:引用类型,避免对象拷贝开销(引用本质是对象别名)
    // const修饰:确保函数内部不能修改val引用的对象,保证初始值安全
    // 默认参数type1():适配所有类型的初始化
    // – 自定义类型:调用其默认构造函数
    // – 内置类型:执行值初始化(int()→0,double()→0.0,指针→nullptr)
    // 解决未知类型的默认值问题,无需手动指定具体值

    // 提前开辟能容纳n个元素的空间,避免后续插入时频繁扩容
    reserve(n);

    // 循环插入n个元素,每个元素初始化为val
    for (size_t i = 0; i < n; ++i)
    {
    push_back(val); // 复用尾插逻辑,自动处理元素赋值和mfinish指针移动
    // 初始状态下mfinish与mstart重合,尾插会依次填充空间
    }

    // 无需手动更新指针:
    // – mend_of_storage由reserve(n)设置为mstart + n
    // – mfinish经n次push_back后自动移动到mstart + n
    }

    那么给大家扩展一下,有些人可能会说,为什么非要用push_back函数呢,直接对mfinsh解引用赋值然后++不就行了吗,那么我们想说的是,既然它和尾插函数的作用一致,那为什么我们还要去自己多写几个代码呢,是吧。

    说明:

    还有一个问题就是,有人会说,在string的构造函数中就是使用new去给mstr开辟空间,那么为什么不能在vector的构造函数里面去使用new去给mstart开辟空间,然后存放数据呢?

    一、string 构造能直接用 new 的底层逻辑

    string 是专门存储 char 类型的容器(或宽字符,如 wchar_t),而 char 属于 C++ 内置基础类型,具备以下特性:

  • 无自定义构造 / 析构逻辑:char 是硬件级支持的类型,内存中仅占 1 字节(或对应宽字符长度),不存在 “构造时需初始化动态资源”“析构时需释放资源” 的需求。
  • 内存操作简单直接:对 char 数组的操作(如赋值、拷贝)可通过字节级内存函数(如 memcpy、strcpy)完成,无需调用 “对象的构造 / 赋值运算符”。
  • 因此,string 的构造函数可以:

    • 用 new char[n] 直接分配一块连续内存(仅需为原始字节分配空间);
    • 再通过 memcpy/strcpy 等函数,把字符数据 “字节对字节” 地拷贝到新空间中。整个过程无需触发 “对象的构造函数”,因为 char 本身没有构造逻辑需要执行。

    二、vector 构造不能直接用 new 的核心矛盾

    vector 是类模板,存储的 type1 可以是任意类型(如 int、double、自定义类 Student、甚至嵌套的 vector 等)。这导致 vector 必须兼容 “需要复杂资源管理的自定义类型”,而直接用 new type1[n] 会破坏 C++ 的对象构造机制与资源管理约定。

    矛盾 1:new type1[n] 仅分配内存,不调用 “元素的构造函数”

    C++ 中,new 操作分为两个阶段:

    • 阶段 1:分配原始内存:new type1[n] 会向系统申请一块能容纳 n 个 type1 对象的原始内存(仅分配内存,不初始化对象)。
    • 阶段 2:调用构造函数:若 type1 是自定义类型,new 会自动为每个元素调用默认构造函数,完成对象初始化;若 type1 是内置类型(如 int),则直接跳过构造(内置类型无构造函数,内存为 “原始值”)。

    但问题在于:vector 需要的是 **“初始化后的有效对象”**,而不是 “原始内存”。若 type1 是自定义类型(如包含 char* 动态内存的 MyString 类),仅分配原始内存会导致(即自定义类型中没有默认构造函数,导致new不能完成对象初始化):

    • 对象的成员变量未初始化(如 MyString 的 char* 可能是随机值,成为野指针);
    • 后续对这些 “未构造的对象” 进行操作(如赋值、析构),会触发未定义行为(如野指针解引用崩溃、重复释放内存)。

    矛盾 2:直接写内存会跳过 “自定义类型的赋值 / 构造逻辑”

    假设我们强行用 new type1[n] 分配内存后,尝试 “直接往内存里写数据”(比如 mstart[i] = val;),会面临两种场景:

    • 场景 A:type1 是内置类型(如 int)看似可以工作(因为 int 赋值是字节级拷贝),但这是 **“恰好兼容”**—— 仅因为内置类型无资源管理逻辑。一旦 type1 换成自定义类型,就会出问题。

    • 场景 B:type1 是自定义类型(如 MyString,内部有 char* 动态内存)mstart[i] = val; 本质是调用 MyString 的赋值运算符。若 MyString 实现了深拷贝的赋值运算符(这是自定义类型资源管理的常规操作),则能正确拷贝资源;但如果直接跳过 “赋值运算符调用”,改为 “字节级内存拷贝”(如 memcpy(&mstart[i], &val, sizeof(type1)),就会变成浅拷贝:

      • val 的 char* 和 mstart[i] 的 char* 会指向同一块动态内存;
      • 当 val 或 mstart[i] 析构时,会 “重复释放同一块内存”,直接导致程序崩溃。

    那么其实这个矛盾,才是最重要的

    矛盾 3:vector 需要 “动态触发元素的构造 / 赋值逻辑”

    vector 的核心设计目标是兼容任意类型,这要求它在 “添加元素” 时,必须触发元素自身的构造或赋值逻辑(由元素类型自己决定如何初始化 / 拷贝资源)。

    而 push_back(const type1& val) 这类接口的作用正是:

    • 先确保空间充足(通过 reserve 扩容);
    • 再通过 *mfinish = val; 调用 type1 的赋值运算符(若 type1 是自定义类型,赋值运算符会执行深拷贝等资源管理操作);
    • 最后移动 mfinish 指针,标记有效元素范围。

    如果直接用 new 分配内存后 “硬写数据”,就会跳过这一 “触发元素自身逻辑” 的关键步骤,导致自定义类型的资源管理完全失控。

    三、vector 构造的正确逻辑:“先分配内存,再逐个构造元素”

    vector 要初始化 n 个元素,正确的流程是:

  • 用 reserve(n) 分配内存:reserve 内部会调用 new type1[n] 分配原始内存,并更新 mstart 和 mend_of_storage(标记容量)。
  • 逐个调用 push_back(val) 构造元素:push_back 会触发 type1 的赋值运算符,确保每个元素都经过 “合法的构造 / 赋值流程”,尤其是自定义类型能正确初始化资源。
  • 这样做的核心目的是:让每个元素都有机会执行自己的构造 / 赋值逻辑,从而兼容任意类型的资源管理需求。

    四、总结:设计目的决定实现方式

    • string 是 “专用字符容器”,char 的简单性让它可以直接用 new + 内存函数完成构造;
    • vector 是 “通用容器模板”,必须兼容任意类型,因此必须通过 “先分配内存,再触发元素自身构造 / 赋值逻辑” 的方式初始化,而不能跳过 “元素的构造流程” 直接写内存。

    这一个问题还是很多人都会有的,希望大家能够理解。

    实现迭代器构造函数:

    还有使用传入迭代器进行初始化的构造函数,其作用是用另一个容器的数据来初始化 vector。比如说,当我们需要把 list(链表)中的数据传入 vector 时,无法直接使用上述构造函数,因为后者只能将整个 list 对象作为元素重复放入 vector,却不能提取 list 内部存储的数据(data)放进 vector。而使用迭代器的话,就能直接将数据放入 vector,因为迭代器直接指向容器的数据。

    之前的构造函数(vector (int n, const type1& val))只能实现 “n 个相同元素” 的初始化(要么是默认值,要么是传入的单个 val 拷贝 n 次);而如果想把 “另一个容器中已有的、可能不同的一系列元素”(比如 list 里的 1、3、5、7,或其他 vector 里的字符串)直接搬进新 vector,之前的构造函数完全做不到,因为它无法 “遍历另一个容器的所有元素”。

    迭代器的本质是 “容器元素的通用访问接口”,不管底层容器是 list(链表)、deque(双端队列)还是其他 vector,只要它提供迭代器,就能通过 * it 访问元素、++it 移动到下一个元素,这就给 vector 提供了 “遍历其他容器、拷贝元素” 的通用方式。

    但我们无法预知传入的是哪个容器的数据,换句话说,不知道要用哪种容器的迭代器,所以就要使用函数模板,让编译器自动推测迭代器的类型。当然,这也可用于下面拷贝构造函数的改善。

    一、参数设计:用函数模板保证迭代器通用性

    构造函数定义为template <typename inputiterator>

    vector(inputiterator begin, inputiterator end),其中inputiterator是函数模板参数,作用是:

    • 兼容任意容器的迭代器类型:无论是 list 的双向迭代器、deque 的随机访问迭代器,还是其他 vector 的迭代器,只要能通过*it访问元素、++it移动位置、it != it2判断是否结束,都可作为参数传入。
    • 无需提前知道源容器类型:函数模板让编译器在编译时自动推导inputiterator的具体类型(如list<int>::iterator、vector<string>::iterator等),实现 “对任意容器的通用初始化”。

    二、计算元素数量:用std::distance获取迭代器范围长度

    步骤:通过std::distance(begin, end)计算begin到end之间的元素个数,结果存入oldcapacity(类型为ptrdiff_t,这是 C++ 标准中专门用于表示两个指针 / 迭代器距离的有符号整数类型,兼容不同迭代器的距离计算结果)。

    原理:

    • std::distance是 C++ 标准库的通用函数,能根据迭代器类型自动选择高效计算方式:对随机访问迭代器(如 vector 的迭代器),直接用end – begin计算,时间复杂度 O (1);对双向迭代器(如 list 的迭代器),通过循环++it计数,时间复杂度 O (n)。
    • 这一步的目的是 “提前知道需要拷贝的元素总数”,为后续开辟空间提供依据,避免盲目扩容。

    三、提前开辟空间:调用reserve避免频繁扩容

    步骤:调用reserve(oldcapacity),为当前 vector 分配能容纳oldcapacity个元素的内存空间。

    作用:

    • 确保容量充足:reserve会让 vector 的mend_of_storage指针指向mstart + oldcapacity,保证后续插入oldcapacity个元素时不会触发扩容(若不提前开辟,循环插入时可能因空间不足多次调用reserve,每次扩容需复制旧元素,导致效率降低)。
    • 仅分配内存不初始化元素:reserve只负责内存分配,不构造元素,元素的初始化交给后续的插入步骤,符合 “按需构造” 的原则。

    四、遍历迭代器范围:逐个拷贝元素到 vector

    步骤:通过while (begin != end)循环遍历迭代器范围,每次循环执行:

  • push_back(*begin):通过*begin获取当前迭代器指向的元素值,调用push_back将其插入到 vector 尾部。

    • *begin的作用:解引用迭代器,获取源容器中当前位置的元素(对于自定义类型,会触发元素的拷贝构造或赋值,确保资源正确复制)。
    • push_back的作用:复用已实现的尾插逻辑,自动完成 “元素赋值(调用type1的赋值运算符)+ 移动mfinish指针”,保证插入后mfinish始终指向最后一个有效元素的下一个位置。
  • ++begin:移动迭代器到下一个元素,继续循环,直到begin == end(遍历完所有元素)。

  • 五、自动维护容器状态:无需手动更新核心指针

    循环结束后,vector 的三个核心指针自动处于正确状态:

    • mstart:指向reserve分配的内存起始位置,即新容器的首元素地址。
    • mend_of_storage:由reserve(oldcapacity)设置为mstart + oldcapacity,容量等于元素总数(无冗余空间,也无空间不足)。
    • mfinish:经过oldcapacity次push_back后,从初始位置(与mstart重合)移动到mstart + oldcapacity,有效元素个数(size)等于oldcapacity,与源容器迭代器范围的元素数量一致。

    核心设计逻辑总结

    该构造函数通过 “函数模板适配任意迭代器→std::distance计算元素数量→reserve提前开辟空间→push_back逐个拷贝元素” 的流程,实现了 “从任意容器拷贝元素初始化 vector” 的通用功能。其核心优势在于:

    • 通用性:不依赖源容器类型,只要有迭代器就能拷贝,体现了 C++ 迭代器作为 “容器通用接口” 的设计思想。
    • 高效性:提前计算元素数量并开辟空间,避免循环中多次扩容,降低时间开销。
    • 安全性:复用push_back确保元素正确构造(尤其是自定义类型的资源深拷贝),避免直接操作内存导致的资源管理错误。

    其实还是比较简单的,我们下面看详细代码:

    // 实现通过迭代器范围初始化的构造函数
    // 用途:用另一个容器的数据初始化当前vector
    // 例如将list中的数据传入vector时,无法直接使用上述构造函数——
    // 前者只能将整个list对象作为元素重复放入vector,无法提取list内部数据;
    // 而迭代器直接指向容器数据,可实现数据的直接拷贝。

    // 此前的构造函数(vector(int n, const type1& val))仅能初始化n个相同元素,
    // 无法处理“另一个容器中已有的、可能不同的一系列元素”(如list里的1、3、5、7),
    // 因它无法遍历其他容器的所有元素。

    // 迭代器是容器元素的通用访问接口,无论底层是list、deque还是其他vector,
    // 只要提供迭代器,就能通过*it访问元素、++it移动位置,
    // 为vector提供了遍历其他容器并拷贝元素的通用方式。

    // 由于无法预知传入的是哪种容器的迭代器,需使用函数模板,
    // 由编译器自动推导迭代器类型,此设计也可用于优化拷贝构造函数。
    template <typename inputiterator>
    vector(inputiterator begin, inputiterator end)
    {
    // 计算迭代器范围中的元素个数
    ptrdiff_t element_count = std::distance(begin, end);
    // 提前开辟足够空间,避免插入时频繁扩容
    reserve(element_count);

    // 遍历迭代器范围,逐个拷贝元素到当前vector
    while (begin != end)
    {
    push_back(*begin); // 尾插当前元素
    ++begin; // 移动迭代器至下一个元素
    }

    // 无需手动更新mend_of_storage和mfinish:
    // – mend_of_storage由reserve(element_count)设置为mstart + element_count;
    // – mfinish经element_count次push_back后,自动移动到mstart + element_count。
    }

    希望大家理解。

    实现vector的拷贝构造函数:

    那么大家,其实这个函数,就是真的和string类中的拷贝构造函数差不多了。

    现代写法:

    一、前置准备:实现swap成员函数

    swap函数是现代拷贝构造的核心辅助工具,作用是交换两个 vector 对象的所有成员变量,步骤为:

  • 调用std::swap交换mstart指针:将当前对象的mstart与参数temp的mstart互换,使当前对象指向temp的内存空间,temp指向当前对象原空间。
  • 同理交换mfinish指针:确保交换后,当前对象的mfinish指向temp原空间中有效元素的结束位置,temp的mfinish指向当前对象原空间的对应位置。
  • 同理交换mend_of_storage指针:保证交换后,当前对象的容量(mend_of_storage – mstart)与temp原容量一致,temp的容量与当前对象原容量一致。
  • swap函数的关键特性:仅交换指针,不涉及内存分配或元素拷贝,操作高效(时间复杂度 O (1)),且能确保交换后两个对象的状态均保持合法(mstart <= mfinish <= mend_of_storage)。

    二、拷贝构造函数的核心逻辑:“用临时对象拷贝,再交换资源”

    拷贝构造函数vector(const vector<type1>& v)的实现分为两步,充分利用了已有的迭代器构造函数和swap函数:

    步骤 1:构造临时对象temp,完成深拷贝

    通过vector<type1> temp(v.begin(), v.end())调用迭代器范围构造函数,让temp成为v的完整副本:

    • v.begin()和v.end()提供v的元素迭代器范围,temp会遍历v的所有元素,逐个拷贝到自身(通过push_back实现,确保每个元素都是深拷贝,尤其是自定义类型)。
    • 迭代器构造函数会自动为temp分配足够内存(容量等于v的元素个数),并正确初始化temp的mstart、mfinish、mend_of_storage指针,确保temp与v的元素完全一致,但内存空间独立(无共享资源)。

    这一步彻底避免了浅拷贝:temp的所有元素和内存都是独立于v的新资源,即使type1是自定义类型(如包含动态内存的string),也能通过push_back触发元素的深拷贝逻辑。

    步骤 2:调用swap(temp),交换当前对象与temp的资源

    通过swap(temp)将当前对象(正在构造的对象)与temp的成员指针互换:

    • 交换后,当前对象的mstart、mfinish、mend_of_storage指向temp原有的内存空间(即v的深拷贝副本),此时当前对象已拥有v的所有元素,且资源独立。
    • temp则指向当前对象构造前的默认空间(通常是nullptr,因构造函数执行时对象尚未初始化)。

    三、临时对象自动销毁,释放冗余资源

    拷贝构造函数执行结束后,临时对象temp会被自动销毁(触发其析构函数):

    • temp此时指向的是当前对象构造前的默认空间(无有效资源,或nullptr),析构时delete[] mstart不会导致错误(释放nullptr是安全的)。
    • 最终,当前对象持有v的深拷贝资源,temp的销毁不会影响当前对象,实现了 “安全释放冗余资源” 的效果。

    下面是详细代码:

    // 实现拷贝构造函数(现代写法),逻辑与string类的现代拷贝构造一致
    // 核心思路:通过临时对象深拷贝 + 指针交换,实现安全高效的拷贝,避免浅拷贝问题

    // 辅助交换函数:交换两个vector对象的核心成员指针
    void swap(vector<type1>& temp)
    {
    // 分别交换三个核心指针,仅交换指针指向,不涉及内存分配/释放,操作高效(O(1))
    std::swap(mstart, temp.mstart);
    std::swap(mfinish, temp.mfinish);
    std::swap(mend_of_storage, temp.mend_of_storage);
    }

    // 拷贝构造函数:const引用参数避免额外拷贝,同时确保源对象不被修改
    vector(const vector<type1>& v)
    {
    // 步骤1:用源对象v的迭代器范围构造临时对象temp
    // 复用迭代器构造函数的逻辑:遍历v的所有元素,深拷贝到temp,自动分配独立内存
    vector<type1> temp(v.begin(), v.end());

    // 步骤2:交换当前对象与temp的成员指针
    // 交换后,当前对象持有temp的深拷贝资源(即v的副本),temp持有当前对象的默认空资源
    swap(temp);
    }

    传统写法:

    一、获取源对象的容量与元素个数

    首先通过源对象v的capacity()和size()函数,分别获取其容量(已分配的内存大小)和有效元素个数(需拷贝的元素数量),并将结果存储在局部变量中。

    • 作用:明确需要分配的内存空间大小(参考源对象的容量,避免后续频繁扩容),以及需要拷贝的元素总数(确保所有有效元素都被复制)。

    二、为当前对象分配内存空间

    调用reserve(capacity)为当前对象分配内存,其中capacity是源对象的容量。

    • 目的:提前开辟与源对象容量相同的内存空间,确保后续拷贝元素时不会因空间不足触发扩容(若不提前分配,循环中push_back可能多次调用reserve,导致重复分配内存和复制元素,降低效率)。
    • 特性:reserve仅分配内存,不初始化元素,元素的初始化由后续拷贝步骤完成。

    三、循环遍历源对象,逐个拷贝元素

    通过for循环(i从 0 到size-1)遍历源对象v的所有有效元素,每次循环执行push_back(v[i]):

  • v[i]:通过operator[]获取源对象v中第i个元素的值(对于自定义类型,v[i]返回元素的引用,确保能正确访问元素的资源)。
  • push_back(v[i]):调用尾插函数将元素拷贝到当前对象中。
    • 对于内置类型(如int),push_back会直接将v[i]的值复制到当前对象的内存中。
    • 对于自定义类型(如string),push_back会调用元素的赋值运算符,触发深拷贝(如string的赋值会复制底层字符数组,而非仅复制指针),确保当前对象与源对象的元素资源完全独立(无共享内存)。
  • 四、构造完成后容器状态的保证

    循环结束后,当前对象的三个核心指针自动处于正确状态:

    • mstart:指向reserve分配的内存起始位置,即当前对象的首元素地址。
    • mend_of_storage:由reserve(capacity)设置为mstart + capacity,容量与源对象一致。
    • mfinish:经过size次push_back后,从初始位置(与mstart重合)移动到mstart + size,有效元素个数(size)与源对象一致,且每个元素都是源对象对应元素的深拷贝。

    核心设计逻辑与局限

    该传统写法通过 “先分配内存,再逐个拷贝元素” 的流程,实现了深拷贝,确保当前对象与源对象v的资源完全独立(避免浅拷贝导致的重复释放等问题)。但相比现代写法,存在一定局限:

    • 代码冗余:需手动获取容量、遍历元素,未复用迭代器构造函数的逻辑,增加了代码维护成本。
    • 异常安全性较低:若在循环拷贝过程中发生异常(如内存分配失败),当前对象可能处于 “部分初始化” 状态(已分配内存但未完成所有拷贝),析构时可能导致资源泄漏。

    下面是详细代码:

    // 拷贝构造函数的传统实现方式
    // 逻辑与构造函数类似,通过手动分配内存和逐个拷贝元素实现深拷贝
    vector(const vector<type1>& v)
    {
    // 步骤1:获取源对象的容量和有效元素个数
    size_t oldcapacity = v.capacity(); // 源对象的容量(用于分配内存)
    size_t oldsize = v.size(); // 源对象的有效元素个数(用于遍历拷贝)

    // 步骤2:提前开辟与源对象容量相同的内存空间
    // 避免后续插入元素时频繁扩容,提升效率
    reserve(oldcapacity);

    // 步骤3:循环遍历源对象的所有有效元素,逐个尾插至当前对象
    // 借助push_back完成元素的深拷贝(尤其适配自定义类型的资源管理)
    for (size_t i = 0; i < oldsize; ++i)
    {
    push_back(v[i]); // v[i]通过operator[]获取源对象第i个元素,push_back实现拷贝
    }
    }

    很简单的只能这么说。

    对vector的=的运算符重载函数的实现:

    OK大家,很简单,和拷贝构造函数的实现一模一样,我们直接看代码:

    // 赋值运算符重载函数:实现vector对象间的赋值操作
    // 返回引用类型,支持连续赋值(如a = b = c)
    vector<type1>& operator=(const vector<type1>& v)
    {
    // 核心逻辑:通过临时对象深拷贝源对象,再交换资源,避免自赋值问题
    // 步骤1:调用拷贝构造函数,用源对象v构造临时对象temp(temp为v的深拷贝副本)
    vector<type1> temp(v);

    // 步骤2:交换当前对象与temp的资源(指针互换)
    // 交换后,当前对象持有v的深拷贝资源,temp持有当前对象原资源
    // 函数结束后temp自动析构,释放当前对象原资源,避免内存泄漏
    swap(temp);

    // 返回当前对象的引用,支持连续赋值
    return *this;
    }

    无需多言。

    实现vector的insert函数:

    那么大家,其实vector的insert函数和string的insert函数是差不多的,但是vector的insert函数是要求传入的参数是迭代器类型的,而不是string的insert函数的整型类型,这是一大区别,而也正是这一大区别,也就有了迭代器失效这么一个大问题,接下来我们就来看看。

    那么其实我们知道,insert函数要达到的目的就是要在下标为pos的位置去插入数据罢了,然后无非就是判断空间够不够,不够就扩容,然后去循环移动数据,将pos对应的那一个位置给空出来。

    那么迭代器失效的问题,其实也就是出现在扩容那里。

    一、先明确核心前提:insert 函数的基础逻辑

    insert 函数的核心功能是 “在迭代器 pos 指向的位置插入新元素”,其基础流程本应是:

  • 检查当前空间是否充足(mfinish == mend_of_storage);
  • 若空间不足,调用 reserve 扩容(通常是原容量的 2 倍);
  • 将 pos 位置及之后的元素向后移动一位,腾出插入空间;
  • 在 pos 位置插入新元素,更新 mfinish 指针。
  • 但直接按 “空间不足就扩容” 的简单逻辑写(如 if (mfinish == mend_of_storage) reserve(2*capacity());),会直接触发迭代器失效,导致程序崩溃或未定义行为。

    二、迭代器失效的根源:扩容导致 pos 成为野指针

    要搞懂失效原因,需先明确两个关键事实:

  • 迭代器的本质(模拟实现中):我们的 vector 迭代器是用 “数据类型指针” 模拟的(typedef type1* iterator)。因此,insert 函数的参数 pos 本质是一个指针,它指向 vector 底层数组中某个具体的内存地址(即要插入元素的目标位置)。
  • 扩容的底层操作(reserve 函数的逻辑):当调用 reserve 扩容时,会执行以下步骤:
    • 分配一块新的、更大的内存空间(比如原容量 3,扩容后 6,新空间地址与原空间完全不同);
    • 将原空间中的所有元素复制到新空间;
    • 释放原空间(delete[] mstart),原空间的内存被系统回收;
    • 更新 mstart、mfinish、mend_of_storage 指针,让它们指向新空间的对应位置。
  • 此时,pos 迭代器(指针)的命运发生了致命变化:

    • pos 最初指向的是原空间中的某个地址(比如原 mstart + 2 的位置);
    • 扩容后,原空间被释放,该地址不再属于当前 vector,甚至可能被系统分配给其他程序;
    • 但 pos 指针的值(即原地址)没有任何变化,依然指向那块 “已被释放的无效内存”—— 此时 pos 成为野指针。

    后续再通过 pos 进行操作(比如移动元素、插入新元素),本质是对 “无效内存” 进行读写,直接触发程序崩溃或未定义行为,这就是 “迭代器失效” 的核心原因。

    三、对比 reserve 函数的处理逻辑:为何 mfinish 不会失效?

    这里很多人会疑惑:reserve 函数中 mfinish 和 mend_of_storage 也是指针,扩容后原空间释放,它们为何不会失效?

    答案是:reserve 函数中手动更新了这两个指针的指向—— 扩容后,mstart 指向新空间起始位置,mfinish 被更新为 mstart + old_size(old_size 是扩容前的有效元素个数),mend_of_storage 被更新为 mstart + new_capacity。

    简单说:reserve 函数知道 “原空间会被释放”,所以提前记录了 mfinish 相对于原 mstart 的偏移量(old_size = mfinish – 原mstart),扩容后用 “新 mstart + 偏移量” 重新计算 mfinish 的新地址,避免了失效。

    而 pos 是 insert 函数的外部传入参数,reserve 函数不知道它的存在,自然不会主动更新它 —— 这就是 pos 会失效,而 mfinish 不会失效的关键区别。

    四、解决方案:提前记录偏移量,扩容后重新校准 pos

    要避免 pos 失效,核心思路是借鉴 reserve 函数处理 mfinish 的逻辑:在扩容前记录 pos 相对于原 mstart 的 “位置偏移量”,扩容后用新 mstart + 偏移量 重新计算 pos 的新地址,让 pos 指向新空间中的对应位置。

    具体步骤拆解:

  • 扩容前:计算 pos 相对于原 mstart 的偏移量偏移量 = pos – mstart(因为 pos 和 mstart 都是指针,指针相减的结果是 “两个地址之间的元素个数”,即 pos 距离原 mstart 有多少个元素)。示例:原 mstart 地址是 0x100,pos 地址是 0x108(假设 type1 是 int,占 4 字节),则偏移量 = (0x108 – 0x100)/4 = 2,即 pos 是原数组的第 2 个元素位置。

  • 执行扩容(调用 reserve)这一步会释放原空间、分配新空间、更新 mstart 等指针(新 mstart 地址可能是 0x200)。

  • 扩容后:重新校准 pos 的指向新 pos = 新 mstart + 偏移量示例:新 mstart 是 0x200,偏移量是 2,则新 pos 地址 = 0x200 + 2*4 = 0x208,恰好是新空间中对应原 pos 的位置。

  • 后续操作:用校准后的新 pos 执行元素移动和插入此时 pos 指向新空间的有效地址,再进行元素移动、插入操作就不会出现野指针问题,彻底解决了迭代器失效。

  • 总结

    insert 函数中不能简单地 “空间不足就扩容”,核心是因为:

    • 迭代器 pos 本质是指向原空间的指针;
    • 扩容会释放原空间,导致 pos 成为野指针(迭代器失效);
    • 必须像 reserve 处理 mfinish 那样,提前记录 pos 相对于原 mstart 的偏移量,扩容后重新计算 pos 的新地址,才能保证后续操作的安全性。

    这一处理逻辑的核心,是理解 “迭代器与底层内存的绑定关系” —— 迭代器不是 “元素的索引”,而是 “元素的内存地址指针”,一旦底层内存被释放或移动,迭代器就会失效,必须重新校准。

    即如下代码所示:

    // 检查当前空间是否已满(有效元素可插入的空间)
    if (mfinish == mend_of_storage)
    {
    // 步骤1:记录pos相对于原空间起始位置的偏移量(元素个数)
    // pos是指向插入位置的迭代器(指针),mstart是原空间起始指针
    // len = 插入位置与原空间起点的元素距离(如pos在原数组第3个位置,len=3)
    size_t len = pos – mstart;

    // 步骤2:获取当前容量(旧容量)
    int oldcapacity = capacity();

    // 步骤3:计算新容量(扩容策略)
    // 若原容量为0(空容器),新容量设为2;否则扩容为原容量的2倍
    size_t newcapacity = oldcapacity == 0 ? 2 : 2 * oldcapacity;

    // 步骤4:执行扩容,分配新空间并迁移元素,释放旧空间
    // 扩容后,mstart、mfinish、mend_of_storage均指向新空间
    reserve(newcapacity);

    // 步骤5:重新校准pos的指向(解决迭代器失效)
    // 扩容后原空间已释放,pos需指向新空间中与原位置偏移量相同的位置
    // 新pos = 新空间起始指针mstart + 原偏移量len
    pos = mstart + len;
    }

    这个是一个很重要的点,希望大家注意,关于迭代器失效的更多解析,在我们前面的对于vector的详细介绍中也有说到,大家可以去看看。

    那么在解决了扩容的问题之后,我们就可以来通过循环去空出pos所指向的位置,然后去对pos进行解引用赋值,下面是实现步骤:

    一、核心前提:已完成扩容与迭代器校准

    在执行以下步骤前,需确保两个关键条件已满足:

  • 空间充足(若之前空间已满,已通过扩容逻辑分配新空间);
  • 迭代器 pos 已校准(扩容后已通过 “偏移量” 重新指向新空间的目标位置),不存在野指针问题。此时,pos 指向的是 “要插入新元素的目标位置”,该位置及之后的元素需向后移动一位,为新元素腾出空间。
  • 二、步骤 1:初始化移动指针,定位元素移动的起始点

    首先创建迭代器 it,让其指向当前 vector 中最后一个有效元素的位置:

    • 因为 mfinish 指向 “最后一个有效元素的下一个位置”,所以 it = mfinish – 1 等价于 “最后一个有效元素的地址”(迭代器本质是指针,指针减 1 即向前移动一个元素位置)。
    • 作用:it 是元素后移的 “起始操作指针”,从最后一个元素开始向前遍历,避免覆盖未移动的元素。

    三、步骤 2:while 循环实现元素后移(核心环节,附例子)

    循环的核心目的:将 pos 位置及之后的所有元素,逐个向后移动一位,腾出 pos 位置用于插入新元素。

    循环逻辑拆解:

    • 循环条件:while (it != pos) → 只要 it 还没移动到 pos 指向的位置,就继续循环(即还需移动当前 it 指向的元素)。
    • 循环体操作:
    • *(it + 1) = *(it):将 it 指向的元素,赋值给 it + 1 指向的位置(即 “当前元素向后移一位”);
    • –it:it 向前移动一位,准备处理前一个元素。

    实例辅助理解(直观看懂移动过程):

    假设当前 vector 的状态如下(type1 为 int,元素为 [1, 3, 5, 7]):

    • mstart 指向 1(地址简化为 0x100),mfinish 指向 7 的下一个位置(0x110);
    • 要在 pos 指向 3(地址 0x104,即第 2 个元素位置)插入新元素 2;
    • 初始化 it = mfinish – 1 → 指向 7(地址 0x10C)。

    循环执行过程:

  • 第一次循环(it = 7,it != pos):

    • *(it + 1) = *(it) → 将 7 赋值给 0x110 位置(7 后移一位);
    • –it → it 指向 5(地址 0x108)。此时元素状态:[1, 3, 5, 7, 7]。
  • 第二次循环(it = 5,it != pos):

    • *(it + 1) = *(it) → 将 5 赋值给 0x10C 位置(5 后移一位);
    • –it → it 指向 3(地址 0x104)。此时元素状态:[1, 3, 5, 5, 7]。
  • 循环终止判断:it = 3,it == pos(pos 也指向 3 的地址 0x104),循环结束。最终元素状态:[1, 3, 5, 5, 7],pos 指向的 0x104 位置已腾出(原 3 已后移到 0x108 位置)。

  • 四、步骤 3:在目标位置插入新元素

    循环结束后,pos 指向的位置已被腾空(无重复元素覆盖风险),执行 *pos = val:

    • 解引用 pos(获取目标位置的内存地址),将新元素 val 赋值到该位置;
    • 对于自定义类型(如 string),这一步会调用元素的赋值运算符,触发深拷贝,确保新元素的资源与其他元素独立(无浅拷贝问题)。

    承接上面的例子:*pos = 2 → 将 2 赋值到 0x104 位置,此时元素状态变为 [1, 2, 3, 5, 7],新元素插入成功。

    五、步骤 4:更新有效元素个数指针

    插入新元素后,有效元素个数增加 1,因此执行 ++mfinish:

    • mfinish 从 “原最后一个有效元素的下一个位置” 向后移动一位,确保 mfinish 始终指向 “当前最后一个有效元素的下一个位置”;
    • 此时 vector 的 size()(mfinish – mstart)自动加 1,符合 “插入一个元素后 size 递增” 的语义。

    承接例子:mfinish 从 0x110 移动到 0x114,size 从 4 变为 5,与实际元素个数一致。

    六、步骤 5:返回插入位置的迭代器

    最后返回 pos,原因是:

    • STL 标准规定 insert 函数需返回 “指向插入元素的迭代器”,方便用户后续操作(如继续在该位置附近插入、修改元素);
    • 此处 pos 已指向新插入的元素(赋值后 *pos = val),返回后用户可直接通过该迭代器访问或操作新元素。

    核心设计逻辑总结

  • 元素后移采用 “从后往前” 的顺序:避免从 pos 开始移动导致后面的元素被覆盖(若从 pos 向后移,pos 位置的元素会先覆盖 pos+1 的元素,导致数据丢失);
  • 用 while 循环适配迭代器操作:迭代器是通用接口,while (it != pos) 比 for 循环更直观(无需计算循环次数,直接基于迭代器位置判断);
  • 严格遵循 STL 语义:插入后更新 mfinish 并返回 pos,保证接口一致性,让用户使用体验与标准库 vector 一致。
  • OK,下面我就给出完整代码:

    // 实现vector的insert函数:在指定迭代器位置插入元素
    // 返回值为iterator类型,支持链式调用(如vec.insert(pos, val).insert(pos2, val2))
    // 参数1:pos – 指向插入位置的迭代器(需在[mstart, mfinish]范围内)
    // 参数2:val – 待插入的元素值,默认值为type1()(调用类型的默认构造/值初始化)
    iterator insert(const iterator pos, const type1& val = type1())
    {
    // 1. 校验传入迭代器的合法性:必须在有效元素范围内(含mfinish,即尾后位置)
    // 避免传入非法迭代器(如nullptr、其他容器的迭代器)导致越界访问
    assert(pos <= mfinish);
    assert(pos >= mstart);

    // 2. 处理扩容逻辑(核心:解决扩容导致的迭代器失效问题)
    // 先判断当前空间是否已满(有效元素已占满全部容量)
    if (mfinish == mend_of_storage)
    {
    // 2.1 记录pos相对于原空间起始位置的偏移量(元素个数)
    // 扩容后原空间会释放,pos需通过偏移量重新校准
    size_t len = pos – mstart;

    // 2.2 获取当前旧容量
    int oldcapacity = capacity();

    // 2.3 计算新容量:空容器(oldcapacity=0)设为2,非空容器扩容为原容量的2倍
    size_t newcapacity = oldcapacity == 0 ? 2 : 2 * oldcapacity;

    // 2.4 执行扩容:分配新空间、复制旧元素、释放旧空间,更新mstart等指针
    reserve(newcapacity);

    // 2.5 重新校准pos指向:新空间中与原位置偏移量相同的位置,解决迭代器失效
    pos = mstart + len;
    }

    // 3. 元素后移:为插入新元素腾出pos位置
    // 从最后一个有效元素开始,向前遍历至pos位置,逐个向后移动一位
    iterator it = mfinish – 1; // it初始指向最后一个有效元素
    while (it != pos) // 未遍历到pos位置时持续移动
    {
    *(it + 1) = *(it); // 将当前元素赋值给后一个位置
    –it; // 迭代器向前移动,处理前一个元素
    }

    // 4. 插入新元素:在腾出的pos位置赋值
    // 自定义类型会调用赋值运算符,确保深拷贝(资源独立)
    *pos = val;

    // 5. 更新有效元素指针:插入后有效元素个数+1
    ++mfinish;

    // 6. 遵循STL标准:返回指向插入元素的迭代器,方便用户后续操作
    return pos;
    }

    希望大家理解。

    实现vector的erase函数:

    那么大家,其实这个也和string类中的erase实现思路差不多的,只不过是使用迭代器去实现罢了。

    一、步骤 1:参数与容器状态合法性校验

    在执行删除操作前,先通过 assert 确保操作的安全性,避免非法访问或操作:

  • assert(!empty()):校验容器不为空 —— 若容器无有效元素(size() == 0),删除操作无意义,直接触发断言报错,避免后续无效逻辑执行。
  • assert(pos < mfinish) 和 assert(pos >= mstart):校验传入的迭代器 pos 处于有效范围 ——pos 必须指向容器内的 “有效元素”(即 mstart 到 mfinish – 1 之间),不能是尾后迭代器(mfinish)或容器外的非法迭代器,否则会导致越界访问。
  • 这三步校验是基础,确保删除操作的前提合法,避免程序因非法参数崩溃。

    二、步骤 2:计算删除位置的下标偏移量

    通过 size_t len = pos – mstart 计算 pos 对应的 “数组下标”:

    • 迭代器 pos 本质是指针(模拟实现中),mstart 是容器底层数组的起始指针,两者相减的结果是 “pos 距离起始位置的元素个数”,即 pos 指向元素的下标(如 mstart 地址为 0x100,pos 地址为 0x108,type1 为 int 时,len = (0x108 – 0x100)/4 = 2,即下标为 2)。
    • 目的:将迭代器位置转换为数组下标,方便后续通过 “下标遍历” 实现元素前移(也可直接用迭代器遍历,核心逻辑一致)。

    三、步骤 3:for 循环实现元素前移(核心环节,附例子)

    循环的核心目的:将 pos 位置之后的所有元素向前移动一位,用后一个元素覆盖当前元素,从而 “抹去”pos 位置的元素,保证数据连续性(无空缺)。

    循环逻辑拆解:

    • 循环起点:i = len → 从删除位置的下标开始(即 pos 对应的元素下标);
    • 循环条件:i < oldsize – 1 → 遍历到 “倒数第二个元素” 为止(oldsize 是删除前的有效元素个数,oldsize – 1 是倒数第一个元素的下标,若遍历到倒数第一个元素,i + 1 会超出有效范围);
    • 循环体操作:mstart[i] = mstart[i + 1] → 用当前下标 i + 1 位置的元素,覆盖下标 i 位置的元素(即元素向前移一位)。

    实例辅助理解(直观看懂前移过程):

    假设当前 vector 的状态如下(type1 为 int,元素为 [1, 2, 3, 4, 5]):

    • mstart 指向 1(下标 0),mfinish 指向 5 的下一个位置(下标 5);
    • oldsize = 5(删除前有效元素个数);
    • 要删除 pos 指向的 3(下标 len = 2)。

    循环执行过程:

  • 初始 i = 2(删除位置下标),满足 i < 5 – 1(2 < 4):

    • mstart[2] = mstart[3] → 用 4 覆盖 3,此时元素变为 [1, 2, 4, 4, 5];
    • i++ → i = 3。
  • 第二次循环 i = 3,满足 3 < 4:

    • mstart[3] = mstart[4] → 用 5 覆盖 4,此时元素变为 [1, 2, 4, 5, 5];
    • i++ → i = 4。
  • 循环终止判断:i = 4,不满足 4 < 4,循环结束。此时,原删除位置的 3 已被 4 覆盖,后续元素依次前移,仅最后一个元素 5 重复(但后续会通过更新 mfinish 忽略该重复元素)。

  • 四、步骤 4:更新有效元素个数指针

    删除一个元素后,有效元素个数减少 1,因此执行 –mfinish:

    • mfinish 从 “原最后一个有效元素的下一个位置” 向前移动一位,此时 mfinish 指向的 “原重复元素 5” 不再被视为有效元素(size() = mfinish – mstart 从 5 变为 4);
    • 最终容器的有效元素为 [1, 2, 4, 5],删除操作完成,数据连续无空缺。

    核心设计逻辑总结

  • 合法性校验优先:通过 assert 拦截 “空容器删除”“非法迭代器删除” 等风险场景,提升代码鲁棒性;
  • 元素前移逻辑:采用 “从删除位置向后遍历” 的顺序,用后一个元素覆盖前一个元素,确保数据连续性(若从后往前遍历会导致未处理的元素被覆盖,数据丢失);
  • 状态更新简洁:仅需移动 mfinish 指针即可完成有效元素个数的更新,无需释放内存(仅逻辑上忽略末尾重复元素,后续插入可复用该空间);
  • 兼容性:支持自定义类型 —— 元素前移时会调用 type1 的赋值运算符,若自定义类型有动态资源(如 string),会触发深拷贝,确保资源独立,无浅拷贝问题。
  • 潜在问题:

    需注意的潜在问题:删除后原 pos 迭代器会失效(指向的元素已被覆盖或变为无效元素),后续不可再使用该迭代器访问容器。

    效后的风险三个维度展开,结合实例让逻辑更清晰:

    一、先明确核心前提:迭代器与元素的绑定关系

    在 vector 的模拟实现中,迭代器本质是指向底层数组元素的指针(typedef type1* iterator)。原 pos 迭代器的核心意义是:绑定到容器中某一个具体元素的内存地址,而非 “元素的逻辑位置(如下标)”。

    当 erase 执行删除操作时,底层数组的元素分布或元素状态发生改变,导致 pos 绑定的 “内存地址” 对应的元素不再是原来的元素,或该地址对应的元素已被视为 “无效”—— 这就是迭代器失效的本质。

    二、原 pos 迭代器失效的两种具体场景(结合实例)

    假设当前 vector 的状态:底层数组元素为 [1, 2, 3, 4, 5],mstart 指向 1(地址 0x100),mfinish 指向 5 的下一个位置(地址 0x114),pos 指向 3(地址 0x108,下标 2),执行 erase(pos) 后:

    场景 1:pos 指向的内存地址被 “后续元素覆盖”(最常见)

    erase 的核心逻辑是 “元素前移”:pos 之后的所有元素(4、5)依次向前移动一位,用 4 覆盖 3 的位置,5 覆盖 4 的位置。

    • 原 pos 指向的地址 0x108 依然存在(未被释放),但该地址对应的元素已从 3 变成 4;
    • 此时 pos 迭代器的 “语义失效”:用户原本通过 pos 指向的是 “元素 3”,但删除后它指向的是 “元素 4”,与用户预期的 “原元素” 完全不符,后续使用 *pos 会获取错误数据。

    场景 2:pos 指向的内存地址变为 “无效元素”(极端情况)

    若删除的是最后一个有效元素(比如 pos 指向 5,地址 0x110):

    • 元素前移逻辑不执行(无后续元素可移动),但会执行 –mfinish,mfinish 从 0x114 移动到 0x110;
    • 原 pos 指向的地址 0x110 此时等于 mfinish(尾后迭代器位置),而 vector 的有效元素范围是 [mstart, mfinish)(左闭右开),即 mfinish 指向的位置不属于 “有效元素”;
    • 此时 pos 成为 “尾后迭代器”,后续执行 *pos(解引用)会触发越界访问(访问无效元素),直接导致程序崩溃。

    三、迭代器失效后的严重风险(为什么不能再用)

    删除后若继续使用原 pos 迭代器,会引发两种致命问题,均属于 C++ 未定义行为(程序可能崩溃、数据错乱或看似正常但隐藏隐患):

    1. 访问错误数据(语义错误)

    如场景 1 中,原 pos 指向 3,删除后指向 4。若用户后续执行 cout << *pos,期望输出 3 但实际输出 4,导致业务逻辑错误(尤其在依赖元素位置的场景,如排序、查找后删除)。

    2. 越界访问或崩溃(内存错误)

    • 若删除最后一个元素后,用原 pos 解引用(*pos):pos 等于 mfinish,指向的是 “有效元素之外的内存”,可能是随机值或已被系统回收的内存,解引用会触发段错误(程序崩溃)。
    • 若删除后继续移动迭代器(如 ++pos):pos 本就指向无效位置,移动后会进一步偏离有效范围,后续操作(如插入、赋值)会破坏容器的底层内存结构,导致更严重的内存错乱。

    四、补充:与 insert 扩容导致失效的区别(避免混淆)

    insert 扩容导致的迭代器失效,是因为原空间被释放,pos 指向 “已释放的无效内存”(野指针);而 erase 导致的失效,是因为原地址的元素被覆盖或变为无效,pos 指向的内存地址存在,但对应的元素语义或有效性已改变 —— 两者失效的 “原因不同”,但 “后果一致”:原迭代器均不可再使用。

    五、如何规避该问题(实用建议)

    若删除后仍需操作容器,需通过 “重新获取有效迭代器” 规避失效问题:

  • 不保存原 pos 迭代器:删除后若需继续访问元素,重新通过 begin()、end() 或下标计算获取有效迭代器(如 auto new_pos = vec.begin() + len,len 是原删除位置的下标);
  • 参考 STL 标准:标准库 vector::erase 会返回 “指向删除元素下一个元素的有效迭代器”,可通过 pos = vec.erase(pos) 直接获取新的有效迭代器,避免失效(我们的模拟实现未返回,是简化版,实际工程中建议模仿标准库设计)。
  • 总结

    erase 后原 pos 迭代器失效的核心:迭代器绑定的是 “元素的内存地址”,而非 “元素的逻辑位置”。删除操作导致该地址对应的元素被覆盖(语义失效)或变为无效(范围失效),后续使用该迭代器会引发语义错误或内存崩溃。因此,删除后必须丢弃原 pos 迭代器,如需继续操作,需重新获取有效迭代器。

    下面我就给出实现erase函数的完整代码:

    // 实现vector的erase函数:删除指定迭代器指向的单个元素
    void erase(iterator pos)
    {
    // 1. 校验操作合法性
    assert(!empty()); // 确保容器非空(无元素时删除无意义)
    assert(pos < mfinish); // 确保pos指向有效元素(不能是尾后位置)
    assert(pos >= mstart); // 确保pos在容器的有效范围之内

    // 2. 计算删除位置的下标偏移量
    // 将迭代器pos转换为相对于起始指针mstart的下标(元素个数)
    size_t len = pos – mstart;

    // 3. 获取删除前的有效元素个数
    size_t oldsize = size();

    // 4. 元素前移:覆盖待删除元素,保证数据连续性
    // 从删除位置开始,到倒数第二个元素结束,逐个用后一个元素覆盖当前元素
    for (size_t i = len; i < oldsize – 1; ++i)
    {
    mstart[i] = mstart[i + 1]; // 后一个元素前移一位
    }

    // 5. 更新有效元素指针:删除后有效元素个数减少1
    –mfinish;
    }

    OK,到这里,我们对模拟实现vector的分析就全部结束了,下面我就给出所有的完整代码:

    完整代码:

    #define _CRT_SECURE_NO_WARNINGS 1
    #include <iostream>
    #include <string>
    #include <algorithm>
    #include <assert.h>
    #pragma once

    //来模拟实现一下vector
    //那么要注意,其实vector和string的本质差不多,都算是顺序表,也就是数组
    //但是呢,vector里面可以存储的类型是极多的,无论是int、double、string等等,都可以
    //所以这也就告诉我们,我们是不能直接使用int*之类的成员变量
    //为了强大的兼容性,很明显,我们得使用类模版
    //通过用户的显式实例化去指定所要存储的数据类型,就是这样
    //然后呢就是因为我们是用类模版
    //所以是不能头文件和源文件一起写的
    //意思就是说vector这个类,我们得在头文件里面写完
    //不能跑到源文件去写vector这个类的成员函数
    //这是C++的规定

    namespace win
    {
    //使用类模版
    template <typename type1>
    class vector//不包含vector头文件,我们就可以直接写vector,不难不行,编译器会报错
    {
    public:
    typedef type1* iterator;//直接定义迭代器,因为后面的容器基本都是用迭代器
    //在我们的模拟实现中,其实迭代器就相当于是数据类型的指针
    typedef const type1* const_iterator;//定义不可变的迭代器

    //理论上来说,我们应该先写构造函数
    //但是呢,由于我们的vector的构造函数
    //其实是要用到尾插push_back的,因为vector要存储的数据类型不确定
    //所以我们创建新vector去赋值交换什么的,或者是直接赋值,是不现实的
    //那么其实最好的方法就是,去利用循环进行尾插,一个一个尾插进数据
    //前面实现string类所使用的方法,其实在vector中就不适用了
    //本质原因就是string类中所存储的数据类型已知
    //而vector中存储的数据类型不是已知的

    //所以可以先写个析构函数
    ~vector()
    {
    if (mstart)
    {
    delete[] mstart;//释放所mstart所指向的那一块数组空间
    mstart = nullptr;//将指针置空
    mfinish = nullptr;//将指针置空
    mend_of_storage = nullptr;//将指针置空
    }
    }

    //那么在写尾插函数之前,还得先去实现reserve函数
    //说白了就是去空间不够就扩容或者开辟指定空间的函数
    //再直白点就是扩容函数
    void reserve(size_t n)
    {
    //如果指定空间大小大于原本的空间大小,才去开辟
    size_t capacity = mend_of_storage – mstart;
    if (n > capacity)
    {
    size_t old_size = size();
    //直接开n个空间大小
    iterator temp = new type1[n];
    //将原本的msart的数据都丢给temp
    //for (size_t i = 0; i < old_size; i++)//其实是相当于*this.size(),也可以直接用old_size
    //{
    //temp[i] = mstart[i];//一个一个丢进去
    //}

    //除了使用循环,也可以使用C++中std里面的copy函数
    //它的语法为 std::copy(源起始迭代器, 源结束迭代器, 目标起始迭代器),
    //作用是:把从「源起始迭代器」到「源结束迭代器前一个位置」的元素,
    //复制到以「目标起始迭代器」为起点的内存区域。
    std::copy(begin(), begin() + old_size, temp);
    //它的作用是:把 “从容器的 begin()(起始位置)开始、
    //连续 old_size 个元素”,
    //复制到 “以指针 temp 为起始位置” 的内存区域中。
    //不要忘记了,temp是指向整个数组,但是一般是指向数组的头元素的

    //接下来就是要把原本的mstart所指向的空间给释放掉
    delete[] mstart;
    //将temp赋值给mstart,此时二者都指向前面给temp开辟的空间
    mstart = temp;
    //更新成员变量
    mend_of_storage = mstart + n;//其实就是最开始的元素加上开辟的空间数
    mfinish = mstart + old_size;
    //那么最后一个有效数据的下一个位置其实就是和原本的mfinish一样的
    //但是嘞,我们是要重新指定的,那么是为什么呢?
    //其实就是因为我们设置的成员变量是迭代器类型的
    //换句话说就是指针类型的,
    //那么在开辟temp之前,
    //mfinish和mend_of_storage都是指向原本的mstart所指向的那一块空间的对应的位置
    //但是我们后面是把原本的mstart所指向的空间给释放掉
    //所以就导致,mfinish和mend_of_storage所指向的位置,没了
    //就是相当于mfinish和mend_of_storage成了野指针了
    //不要想着说在将temp赋值给mstart之后,mfinish和mend_of_storage可以自动转移
    //这是不可能的,所以呢,是需要我们自己去手动指定指向的
    //也就是我们也要对mfinish和mend_of_storage进行赋值
    //这样子才能将mfinish和mend_of_storage改为指向新的空间的对应的位置
    //那么怎么赋值呢,其实就是按照上面的
    //因为我们是开辟了n个空间,那么自然mend_of_storage 就是等于 mstart + n;
    //而mfinish其实就是和原本的离mstart的距离一样
    //但是由于我们对mstart重新赋值,所以我们要去保留之前的mfinish离mstart的距离
    //即old_size,那么后面加上这个变量,就是和原本的mfinsh指向的位置一样了
    //指向的空间不一样哦

    //最后将temp指针赋值为空,也可以不,毕竟是个局部变量
    temp = nullptr;

    //那么在string类中,其实是使用strcpy函数来进行的
    //但是为什么在vector里面,我们不用memcpy函数呢?
    //其实这是因为vector里面存储的数据类型可能是string类等这一些自定义类型
    //那么这个时候,使用memcpy就不行了
    //因为memcpy的本质是浅拷贝,即一个一个字节的拷贝过去,那么针对内置类型还行
    //但是对自定义类型这一些可能会有申请空间的类型,就不行了
    //因为它只是把字节拷贝过去,但是并没有去开辟新空间
    //拿string类的mstr来说,它也是指针,它指向着存储字符数据的空间
    //那么当使用memcpy来进行拷贝的话,编译器就会让temp里面的string的mstr
    //值与原本的mstart的一样,但是,它们两个指向的空间也还是一样的
    //说白了就是没有进行深拷贝,只是把表面的数据给拷贝过去
    //但是两个指向的空间还是同一块,即都指向原本的mstart的mstr指向空间
    //再直白一点就是,使用memcpy函数之后
    //temp里面的mstr和原本的mstart的mstr指向的是同一块空间
    //那么当mstart释放了之后,temp里面的mstr就成为了野指针
    //结论:如果对象中涉及资源管理(如动态内存、文件句柄、网络连接、数据库连接、锁资源等),
    //千万不能使用memcpy进行对象之间的拷贝!
    //因为memcpy是纯粹的字节级浅拷贝,仅复制对象的内存二进制数据,
    //不会调用对象的拷贝构造函数或赋值运算符,
    //完全无视对象的资源管理逻辑,最终必然引发严重问题

    }
    }

    //实现尾插函数
    void push_back(const type1& val)
    {
    //先检查剩余空间够不够
    if (size() == capacity())//或者是mfinish==mend_of_storage
    {
    int oldcapacity = capacity();
    size_t newcapacity = oldcapacity == 0 ? 2 * oldcapacity : 2;
    reserve(newcapacity);
    }
    //因为我们的成员变量是设置为迭代器,也就是指针类型
    //所以不再是用以前的[]了
    //直接解引用赋值就行
    *mfinish = val;
    //别忘记了++
    ++mfinish;
    }

    //判断vector是不是为空
    //其实就是看mstart所指向的内存空间是不是为空
    bool empty() const
    {
    if (mfinish == mstart)
    {
    return true;
    }
    return false;
    }

    //清空函数
    void clear()
    {
    mfinish = mstart;//就是这么简单
    }

    //实现尾删函数
    void pop__back()
    {
    //先检查看看是不是为空
    assert(!empty());

    //很简单,让mfinish往前移一位即可
    //类似msize-1
    –mfinish;
    }

    //接下来就可以实现构造函数了
    vector(size_t n = 1, const type1& val = type1())
    {

    //第一个参数表示要创建多少个空间,第二个参数表示要将数据初始化为什么

    // type1& val:表示 val 是引用,引用 type1 类型的对象。
    //引用的本质是 “对象的别名”,传递引用可以避免对象的拷贝开销
    //(如果直接传 type1 val,会生成对象的副本,耗时耗内存)。
    //const 修饰:const type1& val 表示 “val 是对 type1 对象的只读引用”——
    //函数内部不能通过 val 修改它所引用的对象,
    //保证了 “初始值不会被函数意外篡改” 的语义。

    //默认参数: = type1()
    //type1():调用 type1 类的默认构造函数,
    //创建一个 type1 类型的临时对象
    //(如果 type1 是 int,int() 会生成值为 0 的临时 int;
    //如果是自定义类,会调用其无参构造)。
    //= type1():给参数 val 指定默认值—— 如果调用函数时没有传入第二个参数,
    //val 会自动引用这个 “默认构造的临时对象”。
    //因为我们是不知道要存储进vector的数据类型是什么
    //那么我们自然不能直接给缺省值为0,因为万一传入的是float数据呢?
    //所以,我们就给缺省值为传入的数据类型的默认构造函数
    //这么一来,就不必操心要给什么缺省值了,编译器会自动调用
    //对于内置类型,其实也是有构造函数的
    //对于内置类型(如 int、double、char* 等),
    //虽然它们没有类那样的 “成员函数形式的默认构造函数”,
    //但 C++ 标准规定了:当使用 T()(T 为内置类型,比如 int()、double())时,
    //会进行 “值初始化” —— 结果是将其初始化为零值等价形式:
    //int() → 0
    //double() → 0.0
    //指针类型(如 int* ())→ nullptr(空指针)
    //所以我们就可以直接给缺省值为 数据类型(),即为调用该数据类型的构造函数

    //开辟空间
    reserve(n);
    //使用循环去将默认值全部丢到每一个小格子
    for (size_t i = 0; i < n; ++i)
    {
    //类似STL里面的构造函数,将每一个空间都填充传入数据,即第二个参数
    push_back(val);
    //直接使用尾插就行,因为一开始的mfinish就是为0
    //嘎嘎好使的一个方法
    }

    // 无需手动更新mend_of_storage和mfinish:
    // – mend_of_storage:reserve(n)已自动设为mstart + n;
    // – mfinish:n次push_back后,自动自增为mstart + n;
    }

    //接下来就可以实现构造函数了
    vector(int n = 0, const type1& val = type1())
    {
    //开辟空间
    reserve(n);
    //使用循环去将默认值全部丢到每一个小格子
    for (size_t i = 0; i < n; ++i)
    {
    //类似STL里面的构造函数,将每一个空间都填充传入数据,即第二个参数
    push_back(val);
    //直接使用尾插就行,因为一开始的mfinish就是为0
    //嘎嘎好使的一个方法
    }
    }

    //还有使用传入迭代器去进行初始化的构造函数
    //那么为什么要设置一个这个函数呢?
    //其实是用于用用另一个容器的数据去初始化vector
    //比如说,我们要把list(链表)中的数据,传进vector里面
    //那么我们就不能直接使用上面的工作函数
    //因为上面的构造函数是只能去将一个一个list放进vector中
    //却不能将list里面的数据(data)放进vector中
    //而使用迭代器的话,就能将数据直接放进vector中
    //因为迭代器是直接指向容器的数据的
    //之前的构造函数(vector(int n, const type1& val))
    //只能实现 “n 个相同元素” 的初始化(要么是默认值,要么是传入的单个 val 拷贝 n 次);
    //而如果想把 “另一个容器中已有的、可能不同的一系列元素”
    //(比如 list 里的 1、3、5、7,或其他 vector 里的字符串)
    //直接搬进新 vector,之前的构造函数完全做不到
    //因为它无法 “遍历另一个容器的所有元素”。
    //而迭代器的本质是 “容器元素的通用访问接口”,
    //不管底层容器是 list(链表)、deque(双端队列)、
    //还是其他 vector,只要它提供迭代器,就能通过 * it访问元素、++it移动到下一个元素,
    //这就给 vector 提供了 “遍历其他容器、拷贝元素” 的通用方式。
    //但是呢,是将哪一个容器的数据传入,我们是不知道的
    //换句话说就是,我们是不知道要用哪个容器的迭代器的
    //所以,我们就要使用函数模版,让编译器去自己推测是哪个容器的迭代器
    //当然,这个也可以用于下面的拷贝构造函数
    template <typename inputiterator>
    vector(inputiterator begin, inputiterator end)
    {
    //先开辟空间
    ptrdiff_t oldcapacity = std::distance(begin,end);
    //使用std里面的distance函数去进行计算begin和end之间的距离
    //更方便,也更正确
    reserve(oldcapacity);

    //只要begin没走到最后,就一直存放数据
    while (begin != end)
    {
    push_back(*begin);//尾插进
    ++begin;//移动begin到下一个位置
    }
    //同样不需要我们去手动更新mend_of_storage和mfinish
    }

    //实现拷贝构造函数
    //那么其实就是和string类的拷贝构造函数差不多了
    //现代写法
    void swap(vector<type1>& temp)
    {
    std::swap(mstart, temp.mstart);
    std::swap(mfinish, temp.mfinish);
    std::swap(mend_of_storage, temp.mend_of_storage);
    }

    vector(const vector<type1>& v)
    {
    //现代写法
    vector<type1> temp(v.begin(),v.end());//使用迭代器构造函数去初始化temp
    swap(temp);//然后再将temp的所有成员变量都交换给this
    }

    //传统写法
    //其实就和构造函数差不多
    vector(const vector<type1>& v)
    {
    size_t oldcapacity = v.capacity();
    size_t oldsize = v.size();
    reserve(oldcapacity);
    for (size_t i = 0; i < oldsize; ++i)
    {
    push_back(v[i]);
    }
    }

    //对=的运算符重载函数
    vector<type1>& operator=(const vector<type1>& v)
    {
    //其实很简单,和拷贝构造函数的实现差不多
    vector<type1> temp(v);//直接调用拷贝构造函数去将v的成员变量赋值给temp
    swap(temp);
    }

    //实现resize函数
    //无需返回值,因为链式调用,和reserve函数差不多
    void resize(const size_t& n, const type1& val = type1())
    {
    if (n <= size())
    {
    //如果小于的话,就保留前n个数据
    mfinish = mstart + n;
    }
    else
    {
    //如果大于原本的数据个数的话,就进行扩大数据个数
    //多余的数据全部初始化为传入的val
    //不管怎么样,先开辟出大小为n的空间
    //至于要不要扩容,reserve里面自然会去判断
    reserve(n);

    //使用尾插去插入数据
    //其实就是从原本的mfinish位置开始,去尾插数据
    //直到插到第n个数据结束
    //也就是下标为n的前面一个位置结束
    size_t old_size = size();
    for (size_t i = old_size; i < n; ++i)//举例得出
    {
    //要注意不能将i初始化为size(),
    //因为随着尾插,其实size()是会发生改变的,因为mfinish被改变了
    //所以就会导致i的起始值变来变去的
    //不符合for循环
    //所以我们要用一个变量去接收最开始的size()
    push_back(val);
    }

    //mfinish自然会在尾插里面被更新
    }
    }

    //实现insert函数
    //可以链式调用,所以返回值不为void
    //都是用迭代器去进行指定位置的
    iterator insert(const iterator pos, const type1& val = type1())
    {
    //检验传入参数合法性
    assert(pos <= mfinish);
    assert(pos >= mstart);

    //和string的insert函数差不多
    //其实就是在下标为pos的位置去插入数据罢了
    //但是要注意,接下来会有迭代器失效的问题了

    //先判断空间够不够
    //if (mfinish == mend_of_storage)
    //{
    //reserve(2 * capacity());
    //}

    //如果就是上面这么简单的扩容的话,那么程序肯定就要出问题了
    //问题就是最经典的迭代器失效问题
    //那么其实我们知道,迭代器本质上和指向某一个位置的指针很像
    //在我们的模拟实现中,我们就是用指针去代表的
    //所以呢,pos本质上是个指针,
    //它指向原本的mstart所对应的数组的某一个位置的空间
    //那么当我们进行扩容的时候,在reserve函数中
    //很明显,我们是把原本的mstart所指向的数组的那一块空间给释放掉了
    //并且是给了mstart一块新的空间
    //那么那么,问题就出现了,因为pos指向的空间,被释放了
    //因为上面说了pos指向原本的mstart所对应的数组的某一个位置的空间
    //而那一块空间已经被释放了
    //所以就会导致pos变成了野指针,这也就是迭代器失效
    //所以呢,为了避免这个情况,我们就得向reserve函数里面对待mfinish和mend一样
    //去记录pos相对于mstart的位置,在扩容后去重新加上
    if (mfinish == mend_of_storage)
    {
    size_t len = pos – mstart;
    int oldcapacity = capacity();
    size_t newcapacity = oldcapacity == 0 ? 2 * oldcapacity : 2;
    reserve(newcapacity);
    pos = mstart + len;
    }

    //先借助循环去把位置往后移一下
    //和string不同的点在于我们是只往后一位
    //就不用for循环了,因为是迭代器,用while循环会更直观
    //那么其实实现思路和string中的是一模一样的
    iterator it = mfinish – 1;
    while (it != pos)
    {
    *(it + 1) = *(it);
    –it;
    }
    //对指定位置进行赋值
    *pos = val;

    //更新finish
    ++mfinish;

    //在STL中,要返回pos
    return pos;
    }

    //实现erase函数
    void erase(iterator pos)
    {
    assert(!empty());
    assert(pos < mfinish);
    assert(pos >= mstart);

    //其实就是和string类的erase函数差不多的
    //只不过是只删除一个数据罢了

    //为了直观
    //其实就是往前移动一位

    //我们下面是用下标实现的
    //实际上也是可以使用迭代器实现的,但是无所谓,两个差不多
    size_t len = pos – mstart;
    size_t oldsize = size();
    for (size_t i = len; i < oldsize – 1; ++i)
    {
    mstart[i] = mstart[i + 1];
    }

    //更新mfinish
    –mfinish;

    }

    //实现一下返回空间大小,数据个数的函数
    size_t size() const//不会在函数内改变数据,所以要为const成员函数
    {
    return mfinish – mstart;
    }

    size_t capacity() const//不会在函数内改变数据,所以要为const成员函数
    {
    return mend_of_storage – mstart;
    }

    //再设置一下begin、end函数
    iterator begin()
    {
    //不能设置为const成员函数,因为是可读可写的迭代器
    //可能外部会通过返回值去修改
    //而const成员函数要求是既不能在成员函数内部修改成员变量
    //也不能通过返回值等在外部进行修改成员变量
    return mstart;
    }

    iterator end()
    {
    return mfinish;
    }

    //不可修改迭代器
    const_iterator cbegin() const
    {
    //不会在函数内改变类的成员变量
    //且无法在外部通过该函数的返回值去修改成员变量
    //所以要为const成员函数

    //同时将返回类型设置为const迭代器
    //确保不会被修改,
    return mstart;
    }

    const_iterator cend() const
    {
    //不会在函数内改变类的成员变量
    //且无法在外部通过该函数的返回值去修改成员变量
    //所以要为const成员函数

    //同时将返回类型设置为const迭代器
    //确保不会被修改,

    //为什么设置了为const成员函数之后,返回类型还要设置为const
    //这是为了双重保证 const 语义的完整性——const 成员函数和
    //const 迭代器(const_iterator)分别从 “函数内部行为” 和 “外部访问权限” 两个维度,
    //确保 const 对象的 “只读性” 不被破坏,二者缺一不可。
    //具体原因拆解:
    //1. const 成员函数的作用:限制 “函数内部” 的修改行为
    //cend() const 中的 const 修饰,是向编译器承诺:
    //函数内部不会直接修改类的非 mutable 成员变量(如 mstart、mfinish 等);
    //函数内部不会调用非 const 成员函数(避免间接修改对象)。
    //但这只能约束 “函数内部的操作”,无法限制 “外部通过函数返回值进行的修改”。

    //2. const_iterator 的作用:限制 “外部通过返回值” 的修改行为
    //const_iterator 是 “只读迭代器”,它的核心特性是:通过它只能读取元素,
    //无法修改元素(例如 * it = 10; 会编译报错)。
    //const 成员函数 + const_iterator 的组合,才能彻底保证:
    //函数内部:不修改对象(const 成员函数的约束);
    //外部访问:无法通过返回的迭代器修改对象(const_iterator 的约束)。
    //这就像给 const 对象加了 “双重锁”:既防止内部 “自毁”,也防止外部 “入侵”

    //那么总结一下其实就是,我们设置为了const成员函数之后
    //只能限定函数内部不能对成员变量进行修改
    //const 成员函数并没有 “主动限定外界通过返回值修改”,
    //它的约束是 “间接的、依赖返回类型配合” 的。
    //如果返回类型是 “可写的”(比如普通 iterator、非 const 引用),
    //外界依然能绕过 const 成员函数的约束,修改对象成员变量。
    //简单说:const 成员函数管的是 “函数内部不能改”,不管 “外部拿到返回值后能不能改”。

    //所以就需要将返回类型设置为const去打匹配,
    //这样子才能确保外部不能通过返回值等去修改成员变量

    return mfinish;
    }

    //对[]的运算符重载函数
    //可读可写的[]
    type1& operator[](const size_t pos)
    {
    //不可设置为const成员函数,因为虽然没有在这个成员函数内部去修改成员变量
    //但是本质上这个成员函数是给外部提供了一个可以修改指指定下标位置数据的通道
    //即外部可以通过这个成员函数的返回值去修改数据
    //而const成员函数是要求既不能在函数内部修改成员变量
    //也不能让外部可以通过该成员函数去修改数据
    //所以这个可读可写的成员函数不能设置为const成员函数
    return mstart[pos];
    }
    //只读不写的[]
    const type1& operator[](const size_t pos) const//不会在函数内改变类的成员变量,所以要为const成员函数
    {
    return mstart[pos];//也可以返回引用,但是是限制了const,即不可改变的引用
    }

    private:
    //那么和string类以及之前所实现的顺序表不同
    //我们在模拟实现vector里面,是不用什么size,capacity的
    //我们用更牛波一的一个东西
    //就是三个指针,第一个指针指向数组的第一个元素,其实也是相当于数组名
    //只不过一般来说是指向数组的第一个元素,
    //那么其实这个指针就是相当于是前面string类的mstr亦或是顺序表的arr
    //其实就是指向存放数据的数组
    iterator mstart = nullptr;//给声明缺省值,大保底
    //第二个指针是指向数组中最后一个有效数据的后面一个位置
    //同样的,也不是整型,是指针类型的,指向最后一个有效数据的后面一个位置那一块空间
    //那么其实呢,根据前面所学的,其实这个指针减去mstart的结果就是数组的数据个数
    //即等同于前面的msize
    iterator mfinish = nullptr;
    //那么很明显,第三个指针的功能就是类似mcapacity
    //而且是用这第三个指针减去数组头元素的指针所得到的结果是mcapacity
    //那么这个指针其实就是定位到数组的最后一个位置
    //注意:是数组空间的最后一个位置,而不是什么有效数据的后一个
    iterator mend_of_storage = nullptr;
    //那么其实之所以我们不用之前的整型类型
    //就是因为后面基本都是用迭代器,迭代器还是会更有优势
    //所以我们成员变量统一用迭代器
    //在我们的模拟实现中,其实迭代器就是相当于数据类型的指针

    //可以这么等效理解:
    //mfinish-mstart=msize;
    //mend_of_storage-mstart=mcapacity;

    //三个指针必须满足:mstart <= mfinish <= mend_of_storage
    //当mfinish == mstart时,容器为空(size = 0);
    //当mfinish == mend_of_storage时,容器已满(size == capacity),
    //此时插入元素需要扩容(重新分配更大空间,更新三个指针)。

    };
    }

    希望大家能有所收获。

    结语:

    敲完最后一个分号,看着屏幕上那串完整的vector模拟实现代码,你或许会轻轻舒一口气 —— 从三个指针的设计到迭代器失效的处理,从深拷贝的坑到扩容机制的优化,这一路像是在拆解一台精密的钟表,每一个齿轮的咬合、每一根指针的跳动,都藏着底层逻辑的严谨与巧妙。

    其实,当我们花这么多时间去模拟实现vector,本质上是在做一件更有意义的事:透过这个 “动态数组” 的外壳,触摸 C++ 的核心脉络。

    你一定记得,最初我们为 “三个指针” 的设计困惑过:mstart、mfinish、mend_of_storage,这三个看似简单的指针,却像三把尺子,精准丈量着容器的 “有效元素范围”“总容量范围” 和 “内存边界”。它们的关系 ——mstart <= mfinish <= mend_of_storage,不仅是代码里的约束,更藏着 “空间利用率” 与 “操作效率” 的平衡哲学:当mfinish追上mend_of_storage,我们需要扩容;当mfinish退回mstart,容器回归空态。这种 “用指针标记边界” 的思路,后来在insert的元素后移、erase的元素前移中反复出现,原来底层逻辑从不是孤立的,而是像一张网,彼此牵连,相互支撑。

    你也一定为 “深拷贝” 掉过头发。最初或许觉得 “直接复制指针多简单”,直到发现两个vector共享同一块内存时,析构函数的两次delete[]会让程序崩溃 —— 这才明白,C++ 里的 “拷贝” 从不是简单的字节复制,而是 “资源的独立拥有”。于是我们学会了用push_back逐个拷贝元素,让自定义类型的赋值运算符去处理它们的动态资源;学会了用现代拷贝构造的 “临时对象 + swap” 技巧,既避免了自赋值的坑,又让代码简洁优雅。这背后,是 C++ 对 “资源管理” 的极致追求:每个对象都该对自己的资源负责,既不侵占别人的,也不被别人侵占。

    迭代器失效的问题,大概是最让人 “刻骨铭心” 的。当insert扩容后,原pos迭代器突然变成野指针;当erase删除元素后,原pos指向的元素早已被覆盖 —— 这些 “意外” 逼着我们去理解:迭代器不是 “元素的位置编号”,而是 “内存地址的别名”。内存一旦移动或释放,迭代器的意义就不复存在。于是我们学会了在扩容前记录偏移量,在reserve后重新校准指针;学会了在删除后放弃原迭代器,用新的begin()或下标重新获取有效位置。这像极了编程中的 “边界思维”:任何操作都有其前提,忽视前提,就会踏入未定义行为的雷区。

    再回头看那些 “复用” 的设计:构造函数复用reserve和push_back,拷贝构造复用迭代器构造函数,insert和erase复用指针移动的逻辑 —— 原来优秀的代码从不是重复造轮子,而是像搭积木一样,用已有的可靠模块搭建新功能。这种 “复用” 背后,是对 “逻辑一致性” 的坚持:push_back已经处理好了元素的赋值和mfinish的移动,那构造函数何必再重复写一遍?reserve已经解决了扩容的内存分配,那insert只需专注于元素移动即可。这种思维,会让代码越来越简洁,也越来越容易维护。

    写到这里,你或许会发现:vector的模拟实现,更像是一场 “底层逻辑的修行”。它没有炫技的语法糖,没有复杂的设计模式,只有一行行贴着内存、贴着编译器行为的代码。但正是这些代码,让我们看清了 C++ 的 “真面目”:它既允许你像 C 语言一样直接操作内存,又通过类和模板提供了封装与泛型的便利;它既要求你关注资源的申请与释放,又通过构造函数、析构函数等机制提供了自动化管理的可能。

    当你能从容处理vector的每一个细节,再去看 STL 中的其他容器 ——list的节点指针、deque的分段数组、map的红黑树 —— 会突然有种 “触类旁通” 的感觉。因为它们的底层逻辑虽有差异,但核心问题始终围绕着 “如何高效管理内存”“如何保证迭代器的可靠性”“如何兼容任意类型的元素”。而这些问题,你在实现vector的过程中,早已一一拆解过。

    或许你会说:“实际开发中,谁会自己写vector呢?STL 已经够好用了。” 但这恰恰是我们学习的意义 —— 不是为了重复发明轮子,而是为了知道轮子为什么是圆的,为什么用这种材料,为什么这样转动。当你知道push_back在扩容时可能触发元素的拷贝,就会理解为什么大规模插入时要先reserve;当你知道erase会导致迭代器失效,就会避免在循环中犯 “删除后继续使用迭代器” 的错误;当你知道拷贝构造的深拷贝开销,就会懂得在合适的时候使用移动语义…… 这些藏在 “好用” 背后的细节,才是区分 “会用” 和 “精通” 的关键。

    最后,想对你说:学习底层实现的过程,注定是枯燥且充满挫败感的。可能一个assert报错会让你调试一下午,可能一个迭代器失效的 bug 会让你怀疑人生,可能对着 “深拷贝” 和 “浅拷贝” 的区别发呆很久…… 但请相信,每一次调试成功后的豁然开朗,每一次理解底层逻辑后的拍案叫绝,都是在为你的编程功底 “添砖加瓦”。

    编程这条路,从来没有捷径。但当你亲手搭建起vector这样的基础组件,当你能清晰地说出每个函数的设计动机,当你能预判代码可能遇到的坑 —— 你会发现,自己已经从 “只会调用 API 的使用者”,慢慢变成了 “能理解设计本质的思考者”。而这种转变,会让你在未来面对更复杂的问题时,多一份从容与底气。

    所以,别停下脚步。接下来,去看看list如何用双向链表实现任意位置的高效插入删除,去研究string与vector在字符处理上的细微差异,去探索模板元编程如何让容器更灵活…… 底层的世界还有很多精彩,等待你去发现。

    毕竟,真正的编程能力,从来不是记住多少 API,而是看透多少本质。而今天你为vector付出的每一份努力,都在悄悄为这份能力积蓄力量。

    继续加油吧 —— 在代码的世界里,每一次深入底层的探索,都是向更高级的编程境界迈进的一步。

     

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 对于模拟实现C++vector类的详细解析—下
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!