【C++】STL详解(四)—从零撸出vector,写完我膨胀了

【C++】STL详解(四)—从零撸出vector,写完我膨胀了
坚持用清晰易懂的图解+代码语言,让每个知识点变得简单!
🚀呆头个人主页详情
🌱 呆头个人Gitee代码仓库
📌 呆头详细专栏系列
座右铭:“不患无位,患所以立。”

【C++】STL详解(四)—从零撸出vector,写完我膨胀了


vector的使用----------请点击

vector文档参考----------请点击


摘要

🚀 欢迎来到《C++修炼之路》!

这里是C++程序员的成长乐园,带你领略从面向对象到现代C++的精彩世界。我们将>用简洁的代码和生动的案例,助你掌握C++核心精髓。

🔍 专栏亮点:现代C++特性解析(C++11/14/17)STL源码剖析与实战应用内存管理与性能优化技巧

💡 收获预期:
✔️ 写出更健壮的C++代码
✔️ 深入理解面向对象设计
✔️ 掌握模板编程基础

📌 编程箴言:“好的C++代码就像好酒,需要时间沉淀。”

(正文开始👇)—本篇讲解vector的模拟实现

目录

一、vector模拟实现的四个关键点

序号主题说明
1类模板与 vector 的模拟实现vector 本质上是一个 类模板,因此在模拟实现时,同样需要采用类模板的形式。由于模板的 声明和定义不能分离,本文会直接采用“声明+定义在一起”的写法。
2模板实例化与类型绑定使用类模板时,需要通过 显示实例化 来确定类型。实例化后,T 会被替换为具体类型。例如 vector<int> 中,T 就是 int,此时 vector 就能专门存放整型数据。
3存储结构与三指针设计与传统顺序表不同,vector 通过 三个指针 管理存储区:
_start:指向起始位置
_finish:指向当前元素末尾的下一个位置
_end_of_storage:指向存储区的末尾
这三个指针可以推导出 size(元素个数)与 capacity(容量),设计更灵活高效。
4成员变量缺省值与初始化C++ 允许在 成员变量声明时设置缺省值。在 vector 模拟实现中,我们可让三个指针默认值为 nullptr,这样构造函数和拷贝构造函数中就无需重复初始化,更加简洁优雅。

在这里插入图片描述

二、默认成员函数

namespace dh {template<typenameT>classvector{public:private: iterator _start; iterator _finish; iterator _endofstorage;};}

无参构造

vector首先支持一个无参的构造函数,对于这个无参的构造函数,我们直接将构造对象的三个成员变量都设置为空指针即可。
vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr){}
注意:当我们想要实现允许用 花括号 {} 的方式直接初始化对象,使用直接的无参构造是不行的,我们默认构造后,还需要初始化列表构造函数,从而实现一下的初始化对象功能

析构

析构函数在对象生命周期结束时自动调用。对于容器来说,最重要的就是 释放堆上申请的内存,避免内存泄漏。
~vector(){if(_start){delete[] _start; _start = _finish = _endofstorage =nullptr;}}
  • if (_start) 判断 _start 是否为空指针,防止对空指针执行 delete[]。如果容器里没有分配过空间,那就啥也不做。
  • delete[] _start释放之前用 new[] 申请的动态数组空间。注意必须用 delete[] 而不是 delete,因为申请时用的是数组形式。
  • _start = _finish = _endofstorage = nullptr把三个指针全部置为 nullptr,防止出现 悬空指针 问题。虽然对象马上要销毁了,但这是一个良好的习惯,尤其是如果你以后想写 clear() 或者 析构后继续 debug,可以避免野指针引发错误。

operator=

//赋值 vector<T>&operator=(vector<T> v){swap(v);return*this;}

三、迭代器相关函数

begin 和 end的iterator / const_iterator

在 STL 中,迭代器是通过 typedefusing 声明出来的,它可能是指针,也可能是一个类对象。在我们模拟实现的 vector 里,可以直接把迭代器类型定义为 T*(指向元素的指针),这样最简单直观。除了普通迭代器 iterator,还需要一个 只读版本 —— const_iterator。它的类型是 const T*,也就是指向常量对象的指针。区别:iterator → 可读可写(适合普通对象)。const_iterator → 只读(适合 const 对象)。

使用场景begin()/end() 返回类型可读/可写权限对应实现方式
普通 vector 对象iterator (即 T*)✅ 可读 ✅ 可写iterator begin()
iterator end()
const vector 对象const_iterator (即 const T*)✅ 只读 ❌ 不可写const_iterator begin() const
const_iterator end() const
没有普通迭代器可用时回退使用 const_iterator✅ 只读 ❌ 不可写编译器自动选择
typedef T* iterator;consttypedef T* const_iterator;//begin end iterator begin(){return _start;} iterator end(){return _finish;} const_iterator begin()const{return _start;} const_iterator end()const{return _finish;}

四、容量大小相关函数

在这里插入图片描述

size 和 capacity

由于仅仅是获取size和capacity的值,并不对vector的成员变量进行修改,所以可以使用const修饰this指针,让普通对象和const对象都可以进行调用
//size大小 size_t size()const{return _finish - _start;}//capacity大小 size_t capacity()const{return _endofstorage - _start;}

reserve

reserve 的实现逻辑拆解

只扩不缩n > capacity 时才扩容。当 n <= capacity 时不做任何操作。因为缩容同样涉及“异地缩容”,代价大,不划算。扩容前保存有效数据个数扩容时会进行“异地扩容”,即申请新空间并释放旧空间。如果直接释放旧空间,再次调用size() 无法再获取有效元素数量。所以扩容前要用变量 old_size 保存当前元素个数。申请新空间使用 new[] 申请 nT 类型的空间。用指针 tmp 指向这片新空间。拷贝旧数据到新空间如果 _start 为空(容器中没数据),就不需要拷贝。如果有数据,需要把旧空间的数据拷贝到新空间。为什么不用 memcpy对于内置类型(int、double 等),memcpy 按字节拷贝没问题。但对自定义类型(需要深拷贝的对象),memcpy 只会浅拷贝,导致错误。如何实现深拷贝?使用 for 循环和下标 [],让每个元素通过 赋值运算符重载 进行拷贝。这样自定义类型就会自动调用它的赋值运算符,完成深拷贝。释放旧空间,更新指针数据拷贝完成后,调用 delete[] 释放原空间。让 _start 指向新空间,同时更新 _finish_endofstorage
//reserve扩容voidreserve(size_t n){if(n >capacity()){ size_t old_size =size(); T* tmp =new T[n];//拷贝旧空间的数据到新空间if(_start){for(size_t i =0; i < old_size; i++){ tmp[i]= _start[i];}//memcpy(tmp, _start, sizeof(T) * old_size);delete[] _start;} _start = tmp; _finish = _start + old_size; _endofstorage = _start + n;}}

resize

vector::resize 的两种情况解析:

vectorresize 实现中,常常会涉及到一个默认参数:

voidresize(size_t n,const T& val =T());
const
表示这个参数在函数内部不会被修改。保证传进来的对象内容只读,避免误操作。T&(引用)
避免发生一次 值传递拷贝
如果写成 T val,调用时会先拷贝一份(可能调用拷贝构造,代价大)。
写成 const T& val,则直接绑定到传入对象上,不会额外拷贝。

这样写可以同时支持:

  • 内置类型(比如 intdouble)——引用绑定到临时值上,效率高。
  • 自定义类型(比如 stringMyClass)——避免了多余的深拷贝。

举例

vector<int> v; v.resize(5);// 等价于 v.resize(5, int()); -> 用 0 填充 v.resize(5,3);// 等价于 v.resize(5, const int& val=3);
vector<string> vs; vs.resize(3);// 等价于 vs.resize(3, string()); -> 用 "" 填充 vs.resize(3,"hi");// 等价于 vs.resize(3, const string& val="hi");

这里的 缺省参数 T() 非常关键。

缺省值为什么能用 T()?对于 自定义类型T() 会调用该类型的默认构造函数,产生一个临时对象(匿名对象),作为缺省值。对于 内置类型:例如 intdouble 等,C++ 语言标准对其进行了“升级”——同样可以使用 T() 来构造缺省值。int() 结果为 0double() 结果为 0.0char() 结果为 '\0'这样一来,无论 T 是内置类型还是自定义类型,T() 都能得到一个有效的缺省值,使得模板代码具备统一性和泛化能力。

  1. resize 的两种情况
情况一:n <= size()无需扩容:因为 n 小于等于当前有效数据个数,不涉及新增元素。vector 的元素访问和遍历是以 有效个数 为准,有效个数通过 _finish - _start 来计算。因此我们只需要让 _finish = _start + n,有效数据范围就自动缩小为前 n 个元素。这就实现了 逻辑上删除尾部多余元素,但并不会释放容量。

情况二:n > size()
这里就要往 vector 里补充新元素。又可以分为两种子情况:n <= capacity()容量足够,直接在现有内存空间中追加元素。n > capacity()容量不足,需要扩容。常规策略是开辟更大的空间(一般按 2 倍扩容,或者直接扩到 n),然后迁移原有元素,再补充新元素。
为了实现逻辑简洁,我们往往 统一处理为扩容流程:直接判断如果 n > capacity() 就扩容。扩容后,从 _finish 开始,依次用 T()(或用户传入的 value)填充,直到 _start + n。最终 _finish = _start + n,有效数据范围更新完毕。
//resize扩容或缩减voidresize(size_t n,const T& val =T())//如果用户没传第二个参数,就用类型的默认值,如果是自定义类 MyClass,就调用它的默认构造函数{if(n >size()){reserve(n);while(_finish != _start + n){*_finish = val;++_finish;}}else{ _finish = _start + n;}}

empty

empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。
//判空boolempty()const{return _start == _finish;}

五、修改容器内相关函数

push_back

vector 中存储的类型是不确定的,可能是 int 这样的内置类型,也可能是 stringMyClass 这样的自定义类型。

如果我们定义为传值:

voidpush_back(T x);// ❌ 低效

对于自定义类型,就会产生一次 额外的拷贝,效率很低。

因此我们要改为 引用传参

voidpush_back(const T& x);// ✅ 推荐写法
解释:引用:避免额外拷贝,提高效率。const 修饰:保证 x 在函数体内不会被修改。
//尾插push_backvoidpush_back(const T& x){if(_finish == _endofstorage){reserve(capacity()==0?4:capacity()*2);}*_finish = x;++_finish;}

pop_back

insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
//尾删pop_backvoidpop_back(){//assert(_strart < _finish);assert(!empty());--_finish;}

insert

insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
//insert pos位置插入 iterator insert(iterator pos,const T& x)//给迭代器{assert(pos >= _start);assert(pos <= _finish);if(_finish == _endofstorage){//reserve(capacity() == 0 ? 4 : capacity() * 2); // 如果——capacity不够reserve会进行扩容,导致空间变化,// 然后pos还指向的旧空间,迭代器会失效 size_t len = pos - _start;reserve(capacity()==0?4:capacity()*2); pos = _start + len;//更新pos位置,解决上述问题} iterator end = _finish -1;//指向最后一个数据while(end >= pos){*(end +1)=*end;//向后移动--end;}*pos = x;++_finish;//更新大小return pos;}

注意: 若需要增容,则需要在增容前记录pos与_start之间的间隔,然后通过该间隔确定在增容后的容器当中pos的指向,否则pos还指向原来被释放的空间。


erase

erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
//数据删除erase iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish); iterator it = pos +1;while(it != _finish){*(it-1)=*it;++it;}--_finish;return pos;//返回删除数据的下一个位置,删除了往前覆盖,就是pos位置}

swap

需要交换的参数 v 类型是 vector<T>,这里必须通过引用传参,避免不必要的拷贝开销,同时 v 本身就作为要交换对象的别名来使用。实际交换时直接调用标准库的 swap 来交换两个对象的三个指针。由于堆上真实存储的数据并没有移动,只是两个对象内部的指针被互换,因此能高效完成两个 vector 的整体交换操作。
//交换voidswap(vector<T>& v){ std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endofstorage, v._endofstorage);}

clear

//clear清理voidclear(){ _finish = _start;}

六、访问容器相关函数

operator[ ] 和 由const修饰的operator[ ]

vector 底层是顺序表数组,在类的私有成员变量中通过 _start 指针指向该数组。访问元素时,_start[pos] 实际等价于 *(_start + pos)。由于 _start 是私有成员,只能在类内直接访问,因此我们在成员函数中返回 _start[pos] 即可。返回值使用引用的方式更合适。因为元素本身存储在对象的空间中,只要对象未销毁,该元素就一直存在,不会因作用域结束而失效。这样不仅避免了拷贝开销,还能支持对元素进行修改。
T&operator[](size_t pos){assert(pos <size());return _start[pos];}const T&operator[](size_t pos)const{assert(pos <size());return _start[pos];}

七、构造函数的延伸

拷贝构造

拷贝构造函数的现代写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。
//拷贝构造v2(v1)vector(const vector<T>& v):_start(nullptr),_finish(nullptr),_endofstorage(nullptr){reserve(capacity());for(auto e : v){push_back(e);}}

使用n个T类型的val进行构造

//n个val构造vector(int n, T val =T()){resize(n,val);}

(使用函数模板构造)使用迭代器区间进行构造

使用迭代器区间这个构造函数只能接受特定类型(例如 vector::iterator)的区间作为输入。本质上就是不断从 [start, end) 区间取元素,通过 push_back 插入到新构造的 vector 中。缺点显而易见:通用性差,只能拷贝相同容器(甚至是相同类型的 vector)的区间。
使用模板参数 InputIterator,它不局限于 vector 的迭代器。任何符合输入迭代器要求的类型(如 list::iterator、deque::iterator、甚至原始指针 int*)都可以作为参数。

这让 vector 的构造函数更泛型化,与 STL 其他容器之间能很好地协作。

//迭代器区间构造/*vector(iterator start, iterator end) { while (start != end) { push_back(*start); ++start; } }*///使用函数模版,任意类型容器迭代器初始化template<classInputIterator>vector(InputIterator first, InputIterator last){while(first != last){push_back(*first);++first;}}
写法优点缺点
固定迭代器构造实现简单,逻辑清晰,不涉及模板编译。局限性大,只能用于相同容器的迭代器,扩展性差。
模板迭代器构造通用性强,支持任意容器迭代器或指针作为区间输入,符合 STL 的设计理念。涉及模板,编译时可能增加复杂度;在某些情况下可能与其他构造函数(如接收 (size_t n, const T& val) 的构造)产生重载歧义

📢 如果你也喜欢这种“不呆头”的技术风格:
👁️ 【关注】 看一个非典型程序员如何用野路子解决正经问题
👍 【点赞】 给“不写八股文”的技术分享一点鼓励
🔖 【收藏】 把这些“奇怪但有用”的代码技巧打包带走
💬 【评论】 来聊聊——你遇到过最“呆头”的 Bug 是啥?
🗳️ 【投票】 您的投票是支持我前行的动力
技术没有标准答案,让我们一起用最有趣的方式,写出最靠谱的代码! 🎮💻

Read more

Java 常见Exception全面解析:出现场景、错误排查与代码修正实战

Java 常见Exception全面解析:出现场景、错误排查与代码修正实战

文章目录 * 课程导言 * 适用对象 * 学习目标 * 课程安排 * 教学方式 * 第一部分:Java异常体系回顾(约10分钟) * 1.1 异常是什么? * 1.2 Java异常体系结构 * 1.3 异常信息解读 * 第二课时(上):运行时异常深度剖析(约30分钟) * 2.1 NullPointerException(空指针异常) * 现象描述 * 出现场景 * 堆栈分析示例 * 排查方法流程图 * 代码修正与预防 * 2.2 ArrayIndexOutOfBoundsException(数组下标越界异常) * 现象描述 * 出现场景 * 堆栈分析示例 * 排查方法 * 代码修正与预防 * 2.3 ClassCastException(类型转换异常) * 现象描述 * 出现场景 * 堆栈分析示例 * 排查方法 * 代码修正与预防 * 2.

By Ne0inhk
腾讯云轻量服务器一键部署 OpenClaw:国内外模型秒切 + 企业微信7×24私人AI助理(保姆级)

腾讯云轻量服务器一键部署 OpenClaw:国内外模型秒切 + 企业微信7×24私人AI助理(保姆级)

2026年最火的开源AI Agent——OpenClaw(前身 Clawdbot / Moltbot),让你拥有一个真正能“动手”的7×24小时私人AI助理! 它不仅能聊天,还能帮你发邮件、管日程、整理文件、执行脚本、浏览网页……关键是数据全在你自己服务器上,隐私0泄露。 腾讯云轻量应用服务器(Lighthouse)已官方推出OpenClaw一键部署模板,新手5–10分钟就能跑起来,支持一键切换国内外大模型(Claude、Gemini、DeepSeek、通义千问、豆包等),再接入企业微信后,手机随时发指令,AI秒执行。 本次实测配置: * CPU:2核 * 内存:4GB * 系统盘:70GB SSD * 流量:600GB/月(带宽6Mbps) 性能够用,月均成本低至几块钱! * @我更多福利大放送 步骤1:购买并一键部署OpenClaw服务器(3–

By Ne0inhk
深入详解人工智能数学基础——概率论中的KL散度在变分自编码器中的应用

深入详解人工智能数学基础——概率论中的KL散度在变分自编码器中的应用

🧑 博主简介:ZEEKLOG博客专家、ZEEKLOG平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用,熟悉DICOM医学影像及DICOM协议,业余时间自学JavaScript,Vue,qt,python等,具备多种混合语言开发能力。撰写博客分享知识,致力于帮助编程爱好者共同进步。欢迎关注、交流及合作,提供技术支持与解决方案。 技术合作请加本人wx(注明来自ZEEKLOG):xt20160813 深入详解人工智能数学基础——概率论中的KL散度在变分自编码器中的应用 在人工智能,尤其是深度学习领域,**变分自编码器(Variational Autoencoders, VAE)**因其出色的生成能力而备受关注。VAE的核心在于其对潜在变量分布的建模,而这一过程离不开概率论中的一个关键概念——Kullback-Leibler散度(KL散度)。本文将以浅

By Ne0inhk
AI测试:自动化测试框架、智能缺陷检测、A/B测试优化

AI测试:自动化测试框架、智能缺陷检测、A/B测试优化

1. 自动化测试框架 1.1 概述 基于AI的自动化测试框架通过机器学习和自然语言处理技术,实现了测试用例的自动生成、执行和优化,显著提升了测试效率和覆盖率。这类框架能够理解需求文档、识别UI元素、预测测试路径,并持续优化测试策略。 1.2 核心组件 1. 需求解析引擎:使用NLP技术分析需求文档 2. 测试用例生成器:基于需求自动生成测试用例 3. 智能执行引擎:动态调整测试执行顺序 4. 结果分析器:使用ML模型分析测试结果 5. 自优化模块:根据历史数据持续改进测试策略 1.3 代码实现 import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from

By Ne0inhk