C++ vector深度剖析与模拟实现:探索模板的泛型应用

C++ vector深度剖析与模拟实现:探索模板的泛型应用

前引:在C++标准模板库STL中,vector是最常用的容器之一。它以动态数组的形式提供联系内存存储,支持随机访问和高效的尾部插入\删除操作。然而,其底层实现远非简单的数组封装,而是通过精妙的内存管理策略和数据结构设计,平衡了性能与灵活性。本文将深入探讨vector的底层实现原理,包括其核心数据结构、动态扩容机制、迭代器设计,以及实际应用中的注意事项~

在上一个容器 string 的模拟实现中,我们发现 string 的模拟实现有些单调,它仅仅操作字符串,通过划分空间+类实现它的各种接口功能即可,难度还比较正常,思维逻辑正确代码不是问题;今天的 vector 作为新学的一个容器,它比 string 要复杂一些,因为它可以接纳各种类型,这就需要用到我们之前学的模板,不仅仅是写一个类这么简单~下面开始今天的vector实现,正文开始!

目录

vector的核心数据结构

模板框架搭建

构造初始化

 析构函数

尾插数据 

扩容

扩容+初始化

​编辑​编辑​编辑

打印

迭代器

删除指定位置元素

插入元素在指定位置之前

迭代器失效

拷贝构造

赋值重载


vector的核心数据结构

vector 的底层实现依赖于三个关键指针(或者迭代器),它们共同管理内存空间与元素布局:

_start:指向数组的起始位置,即第一个元素的内存地址

_finish:指向最后一个元素的下一个位置,用于标记已分配但未使用的空间边界

_end_of_storage:指向当前分配内存的末尾,标记整个连续内存块的结束

 这三个指针的动态调整是 vector 实现高效内存管理的核心。例如:当插入元素空间不足时,_finish会触发扩容逻辑,分配更大的内存块并迁移数据!下面我们来通过2倍扩容来实现 vector

模板框架搭建

既然 vector 的实现是依靠模板来的,那么推理出来就是:在自定义空间中用模板实现类

namespace Seek { //vector模板 template<class T> class vector { public: //实现 private: T* _start; T* _finish; T* _end_of_storage; }; }

类模板的实例化:空间声明+模板类类型

Seek::vector<int> S1;

构造初始化

观察库里面的 Vector 我们发现在没有任何参数时capacity也是0,例如:

那么我们开始只需要将三个指针全部初始化为空就行了

//构造初始化 vector() :_start(nullptr) ,_finish(nullptr) ,_end_of_storage(nullptr) { }

 析构函数

析构不能释放空,因此需要先判断指针是否开辟了空间,然后再置空

//析构 ~vector() { //判断 assert(_start); //释放空间 delete[]_start; //置空 _start = _finish = _end_of_storage = nullptr; }

尾插数据 

首次学习 vector 实现我们以整形为主进行学习

在尾插时我们可能需要更改三个指针的位置,因此需要先计算一下:size()、capacity

原理:指针-指针=中间的元素个数(通过这样我们可以控制三个指针的移动) 

//size size_t size()const { return _finish - _start; } //capacity size_t capacity()const { return _end_of_storage - _start; }

在尾插时需要考虑:如果空间已满 或者 _start为空。如果_start为空,那么无法使用memcpy

//尾插 void push_back(const T pc) { if (_finish == _end_of_storage) { //说明此时空间已满 或者 空间为空 size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //如果_start为空,那么无法使用memcpy if (_start == nullptr) { _start = new T[newcapacity]; _finish = _start; _end_of_storage = _start + newcapacity; } else //开空间 reserve(newcapacity); } //存 *_finish = pc; _finish++; }

扩容

扩容就是 reserve ,传入指定数量的空间,reserve负责开辟,然后更新三个指针指向新空间

//扩容 void reserve(size_t newcapacity) { assert(newcapacity > 0); //记录 size_t size_tmp = size(); //开辟空间 T* tmp = new T[newcapacity]; //拷贝内容 memcpy(tmp, _start, sizeof(T) * newcapacity); //释放空间 delete[]_start; //更新指针 _start = tmp; _finish = _start + size_tmp; _end_of_storage = _start + newcapacity; }

注意:我们开辟新空间后,很容易忘记_finish的指向,而在释放原空间之前,需要标记元素个数

扩容+初始化

如果resize小于_finish,那么就保留resize及其以前的数据

如果resize大于_finish,那么就需要扩容+初始化(如果没有给初始值,调用构造函数)

//扩容+初始化 void resize(size_t n, const T& val=T()) { if (n < size()) { //直接删 while (size() > n) { _finish--; } } else { //扩容 reserve(n); while (n != size()) { *_finish = T(); _finish++; } } }

效果展示:

打印

如果实现流提取,虽然可以用友元解决,因为是模板无法识别变量类型,所以在成员函数内完成

而vector的类型太多了,如果去为它实现一个流提取是没有必要的,不向string是确定的 

数组属于连续存储的,因此直接解引用\下标就可以拿到数组元素,连续的内存存储也支持加加的

//打印 void Print()const { assert(_start); for (T* tmp = _start ; tmp != _finish ; tmp++) { cout << *tmp << " "; } cout << endl; }

效果展示:

迭代器

迭代器应该是返回有效元素的这个区间的指针指向,而不是放大到容量

这里数据的开始刚好是_start

末尾的下一个位置刚好是_finish \ _start+元素个数

//迭代器 iterator begin()const { return _start; } iterator end()const { return _start + size(); }

删除指定位置元素

可以看到删除指定位置元素的参数是迭代器类型:iterator,这里需要注意的是_finish的位置

//删除指定位置元素 void erase(iterator pos) { //判断有效性 assert(pos >= _start && pos < _finish); //挪动元素 while (pos + 1 < _finish) { *pos = *(pos + 1); pos++; } _finish--; }

效果展示:

插入元素在指定位置之前

 可看到它的位置也是根据迭代器去做参数的

//插入元素在指定位置之前 void insert(iterator pos, T tmp) { //检查范围 assert(pos > _start && pos <= _finish); //看是否需要扩容 if (_finish == _end_of_storage) { size_t newcapacity = capacity() * 2; reserve(newcapacity); } //挪动元素 iterator it = _finish; while (it >= pos) { *it = *(it - 1); it--; } *(pos - 1) = tmp; _finish++; }

迭代器失效

 迭代器失效的场景:

(1)insert 插入触发内存扩容,所有原有迭代器、指针、引用失效

(2)insert 插入未触发内存扩容,但元素位置移动,之后的迭代器失效

(3)erase 删除元素之后,被删除元素及其之后的所有迭代器失效

(4)clear 清理元素之后,所有迭代器失效

迭代器失效的原因(以下两种都会使原来的迭代器全部失效):

(1)内存的重新分配,vector是基于动态数组实现的容器,当元素超过容量时实现扩容,vector             重新分配内存(比如 insert )

(2)元素移动:即使没有内存重新分配,insert 会使插入的元素后移,erase 会使删除的元素前移

具有代表的就是 insert 和 erase 接口,迭代器失效与内部实现有关,比如STL里面的:

如果要解决可以接收 erase 的返回值获取下一个有效的迭代器,例如:

拷贝构造

拷贝构造是用一个已经存在的对象去创建+初始化另一个对象 

注意:不能使用memcpy,因为memcpy是按字节拷贝,如果是自定义类型会发生浅拷贝情况

           虽然这里的 T 是int类型,但是为了泛化使用,避免自定义类型发生浅拷贝

           对于连续的地址,可以使用下标[ ] 或者 直接解引用访问内容

//拷贝构造 vector(const vector<T>& V) { _start = _finish = _end_of_storage = nullptr; //开空间 _start = new T[V.capacity()]; //拷贝 iterator tmp = V._start; iterator it = _start; while (tmp != V._finish) { *it = *tmp; it++ ; tmp++; } _finish = _start + V.size(); _end_of_storage = _start + V.capacity(); }

效果展示:

赋值重载

我们可以通过拷贝构造出中间变量,再去交换临时变量的指向来完成初始化

//赋值运算符重载 vector<T>& operator=(const vector<T>& V1) { swap(V1); return *this; } void swap(const vector<T>& V1) { //V2是临时的,出了函数会销毁 vector<T> V2(V1); std::swap(_start, V2._start); std::swap(_finish, V2._finish); std::swap(_end_of_storage, V2._end_of_storage); }

注意:开始我们构造出的*this是空的,我们利用另一个对象V1去深拷贝出V2,再将V2的内容给交             换给*this,当出了swap函数去释放V2的时候,V2已经全部是空了,所以不要使用断言

                                                 【雾非雾】期待与你的下次相遇! 

Read more

【C++】C++入门

【C++】C++入门

第一篇我们先了解一下C++的历史渊源,俗话说的好,学术不思源,半吊打一年。 我们来看一下 C++课程包含 * C++语法 * STL * 高阶数据结构 特点 * C++兼容C语言,C语言后缀是.c,C++后缀是.cpp或者.cc * ANSI/ISO委员会维护编译器 * 标题越粗,版本更新越大。 C++20和C++23趣事‘ 20现状: C++更新也分为小版本和大版本 委员会在起草C++标准化第一个草案后,STL被普惠实验室开发了,在C++标准化时,把STL添加到C++标准化中。 23期望值 结果没达到,遭诟病 * C++参考文档:简洁版cpluscplus.com(推荐用英文版的) C++的排行榜 TIOBE排行榜 C/

By Ne0inhk
【C++】第十九节—一文万字详解 | AVL树实现

【C++】第十九节—一文万字详解 | AVL树实现

好久不见,我是云边有个稻草人,偶尔中二博主与你分享C++领域专业知识^(* ̄(oo) ̄)^ 《C++》—本篇文章所属专栏—持续更新中—欢迎订阅~喔 目录 一、AVL的概念 二、AVL树的实现 2.1 AVL树的结构 2.2 AVL树的插入 【AVL树插入⼀个值的大概过程】 【平衡因⼦更新】 【插⼊结点及更新平衡因⼦的代码实现】  2.3 旋转 【旋转的原则】 【右单旋+两个坑+代码实现】 【左单旋+代码实现】 【左右双旋+代码实现】 【右左双旋+代码实现】 2.4 AVL树的查找 2.5 AVL树平衡检测 2.6 AVL树的删除

By Ne0inhk
DocxFactory: 一个C++操作word的开源库(不依赖office控件)

DocxFactory: 一个C++操作word的开源库(不依赖office控件)

目录 1.简介 2.环境搭建与依赖配置 3.模板设计核心技巧 4.常用场景示例 4.1.示例 1:简单文本替换(基础场景) 4.2.示例 2:动态生成表格(结构化数据场景) 4.3.示例 3:插入图片(含资源场景) 5.高级功能与技巧 6.常见问题与解决方案 7.与其他库的对比 8.总结 1.简介         DocxFactory 是一个专注于处理 Microsoft Word 文档(.docx 格式)的 C++ 库,主要用于动态创建、修改和生成 docx

By Ne0inhk
模板进阶:从非类型参数到分离编译,吃透 C++ 泛型编程的核心逻辑

模板进阶:从非类型参数到分离编译,吃透 C++ 泛型编程的核心逻辑

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 非类型模板参数:让模板支持 “编译期常量配置” * 1.1 什么是非类型模板参数? * 1.2 必须遵守的 2 个关键规则 * 二. 模板特化:解决 “特殊类型” 的适配问题 * 2.1 解决 “通用模板失效” 的例子 * 2.2 类模板特化:比函数特化更常用 * 2.2.1 全特化:所有模板参数都确定 * 2.3.2 偏特化:对模板参数做 “条件限制”

By Ne0inhk