【C++ STL】模拟实现 vector

标题:【C++ STL】模拟实现 vector

@水墨不写bug

 (图片来源于网络)


正文开始:

        STL中的vector是一个动态数组,支持随机访问,可以根据需要来扩展数组空间。

        本项目将实现vector的部分常用功能,以增强对vector的熟悉程度,了解STL容器的工作原理,积累项目经验,也为将来自主实现和改造容器奠定坚实的基础。

        STL内实现的vector是一个类模板,也就是vector的数据类型理论上可以是任意的数据类型,本文力求与STl采取类似的方法,通过类模板来实现vector。更接近于STL的实现方法,自然会让你对STL有更深的理解。

一、明确方式,铺平道路

1.文件问题 

以前我们在实现string的时候,采用分文件操作:

string.h

string.cpp

        这是一个非常好的习惯,无疑为在项目进展,和后期维护时提供了便利。但是我们要实现的vector是通过类模板来实现的,如果还将vector类内部的函数的声明与定义分离,就会出现问题:

        由于类模板在编译时不会实例化,所以当我们想要调用这个类的成员函数的时候,就会发现没有匹配的成员函数,没有办法调用成员函数。于是,在用类模板实现vector时,我们不再分为vector.h和vector.cpp两个文件,而是将h和cpp文件合并为一个h文件,这样在同一个类模板中就可以调用成员函数了。

二、vector功能简介

I、构造函数和析构函数

  1. 默认构造函数:创建一个空的vector对象。
  2. 带大小和初始值的构造函数:创建一个包含指定数量元素的vector,每个元素都被初始化为相同的值。
  3. 范围构造函数:通过迭代器或指针的范围来初始化vector。
  4. 拷贝构造函数:使用另一个vector对象来初始化新的vector对象。
  5. 移动构造函数(C++11及以后):使用另一个vector对象的资源来初始化新的vector对象,同时使原对象变为空。
  6. 初始化列表构造函数(C++11及以后):使用初始化列表来初始化vector。
  7. 析构函数:销毁vector对象,释放其占用的内存。

II、迭代器

  1. begin():返回指向vector第一个元素的迭代器。
  2. end():返回指向vector最后一个元素之后位置的迭代器(不是最后一个元素)。
  3. rbegin():返回指向vector最后一个元素的反向迭代器。
  4. rend():返回指向vector第一个元素之前位置的反向迭代器。
  5. cbegin() 和 cend():与begin()和end()类似,但返回的迭代器是const类型,不能用于修改元素。

III、容量操作

  1. size():返回vector中元素的当前数量。
  2. max_size():返回vector能够容纳的最大元素数量(通常是一个很大的值,但具体取决于系统和编译器的实现)。
  3. capacity():返回vector当前分配的存储容量,可能大于或等于size()返回的值。
  4. reserve(n):请求vector的存储容量至少为n,如果当前容量小于n,则重新分配内存。
  5. shrink_to_fit()(C++11及以后):尝试将vector的capacity减少为其当前size的大小,但不一定成功,因为释放内存是可选的。

IV、修改容器

  1. push_back(value):在vector的末尾添加一个元素。
  2. pop_back():移除vector的最后一个元素。
  3. insert(pos, value):在指定位置pos之前插入一个元素value。
  4. erase(pos):移除指定位置pos的元素,并返回指向被移除元素之后位置的迭代器。
  5. clear():移除vector中的所有元素,使其变为空。
  6. assign(first, last):用范围[first, last)内的元素替换vector的内容。
  7. assign(n, value):用n个值为value的元素替换vector的内容。

V、元素访问

  1. operator[]:通过下标访问vector中的元素。
  2. at(pos):通过位置pos访问vector中的元素,并进行范围检查。
  3. front():返回vector中第一个元素的引用。
  4. back():返回vector中最后一个元素的引用。
  5. data():返回指向vector中第一个元素的指针(C++11及以后)。

VI、其他操作

  1. swap(other):交换两个vector的内容。
  2. find(value):在vector中查找值为value的第一个元素,并返回指向该元素的迭代器,如果未找到则返回end()。
  3. sort():对vector中的元素进行排序。
  4. reverse():颠倒vector中元素的顺序。

 三、实现

        通过本文,你可以跟随我的思路来了解实现 vector 的底层思路,以及实现的原理。

        由于我们将vector实现在一个 .h 文件,并且要实现类模板vector,于是我们先写出框架: 先定义模板参数;

template<typename T>
class vector
{private:T* _start;//数组的开始位置T* _finish;//数组内最后一个数据的下一个位置,finish-start表示数组内元素个数T* _end_of_storage;//数组最后一个能存储元素的下一个位置,end_of_storage - start 表示数组的容量
};

         一个类,想要创建一个对象,必须要有构造函数

        STL的vector在实例化之后,默认是已经开辟好了空间,只不过size == 0 ,及内部没有数据而已。

        这里我们化繁为简,在构造函数内部不开辟空间,而是在使用或者说对vector对象进行操作的时候再开辟动态空间,于是我们可以直接在变量声明时给默认值并且使用不传参的默认构造函数:

template<typename T>
class vector
{vector() = default;
private:T* _start = nullptr;T* _finish = nullptr;T* _end_of_storage = nullptr;
};

 

        push_back是对vector进行最基本的操作,想要实现push_back,则需要考虑扩容逻辑:

如果vector的size() == capacity ()则表示vector已经满了,需要进行扩容,扩容是多次进行的,我们就单独将扩容用的reserve()实现出来即可;(上文加黑即为要实现的函数)

 

size_t size() const
{return _finish - _start;
}size_t capacity() const
{return _end_of_storage - _start;
}//保留空间
void reserve(int n)
{//先保存size,防止_start,_finish变化,导致size无法计算int oldsize = size();//要求保留的大于现有的,扩容if (n > capacity()){T* tem = new T[n];if (_start){for (size_t i = 0; i < oldsize; i++){tem[i] = _start[i];}}if(_start)delete[] _start;_start = tem;_finish = _start + oldsize;_end_of_storage = _start + n;}//否则,不缩容
}

在实现了这些函数之后,就可以实现push_back()函数的逻辑了。当然,有了尾插,就少不了尾删:pop_back(),由于尾删的逻辑简单,直接给出代码:


//尾插一个T对象
void push_back(const T& t)
{if (size() == capacity())//需要扩容{//二倍扩容逻辑int Newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(Newcapacity);//改变capacity,不改变size}//扩容完毕,开始尾插*_finish = t;++_finish;
}void pop_back()
{assert(size() > 0);--_finish;
}

        STL内的vector是支持迭代器访问的,也就是支持范围for;由于范围for在编译的时候会自动找begin()end(),所以我们需要定义迭代器iterator,同时实现begin(),end(),为使用范围for做准备:

//迭代器
typedef T* iterator;
typedef const T* const_iterator;iterator begin() 
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}

 到这里我们发现我们还需要先完善默认的成员函数,默认成员不完善,意味着这个vector使用的是编译器默认生成的成员函数,在大多数情况下会出现问题:

        比如:容器内部是指向堆区的指针,使用默认构造会导致浅拷贝的问题。

这就给于我们警示:默认成员函数能自己手动实现,就自己手动实现。

//构造函数,迭代器区间初始化
template<typename inputIterator>
//支持任意容器的迭代器初始化:string
vector(const inputIterator& begin,const inputIterator& end)//左闭右开
{reserve(end-begin);iterator it = begin;while (it != end){push_back(*it);++it;}
}//整形匹配
vector(int n, const T& val = T())
{reserve(n);//reserve不改变capacityfor (int i = 0; i < n; i++){_start[i] = val;}//用多少申请多少,size等于容量_finish = _end_of_storage = _start + n;
}
//自定义类型匹配
vector(size_t n,const T& val = T())
{reserve(n);for (size_t i = 0; i < n; i++){_start[i] = val;}
}
//拷贝构造
vector(const vector<T>& t)//目标,将t深拷贝给*this
{reserve(t.capacity());for (const auto& e : t){push_back(e);}
}
void swap(vector<T> tem)
{std::swap(_finish, tem._finish);std::swap(_start, tem._start);std::swap(_end_of_storage, tem._end_of_storage);
}//赋值重载
vector<T> operator=(const vector<T> tem)//拷贝构造创建一个临时对象,用于交换得到有效数据// 只要完成拷贝构造即可完成赋值重载
{//现代写法swap(tem);return *this;
}
//析构函数
~vector()
{if(_start)delete _start;_finish = _end_of_storage = nullptr;
}

 接下来我们还需要实现vector内部对象的随机访问,由于vector内部的数据类型是模板,数据类型不能确定,这就需要我们重载 [] 操作符,也就是实现operator[]函数:

//一般类型调用,可读可写
T& operator[](size_t pos)
{//空间地址有效assert(pos < size() && pos >= 0);return _start[pos];
}//const对象调用的,read-only
const T& operator[](size_t pos) const
{//空间地址有效assert(pos < size() && pos >= 0);return _start[pos];
}

 仅仅有了尾插和尾删是不够的,我们还要实现任意位置的插入删除:

//在pos位置插入对象
iterator insert(iterator pos, const T& t)//由于可能需要扩容,会发生迭代器失效,对内部而言//迭代器pos在扩容前后指向的对象不再相同,对外部也是同样的会发生
{if (size() == capacity())//需要扩容{int len = pos - _start;int Newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(Newcapacity);//改变capacity,不改变size//记录len,解决迭代器失效的问题pos = _start + len;}//移动对象iterator end = _finish;while (end != pos){*end = *(end - 1);--end;}*pos = t;return pos;
}//一般不会出现迭代器失效的问题
iterator erase(iterator pos)
{iterator oldpos = pos;iterator start = pos + 1;while (start < _finish){*(start - 1) = *start;++start;}--_finish;return oldpos;
}

        接下来,为了避免命名冲突,体现封装,我们要将上述实现的vector封装在我们自己的命名空间中,我的命名空间的名称为:ddsm

STL的vector模拟实现:

#pragma once
#include<iostream>
#include<cstring>
#include<cassert>
using namespace std;
namespace ddsm
{template<typename T>class vector{public:vector() = default;//构造函数,迭代器区间初始化template<typename inputIterator>//支持任意容器的迭代器初始化:stringvector(const inputIterator& begin,const inputIterator& end)//左闭右开{reserve(end-begin);iterator it = begin;while (it != end){push_back(*it);++it;}}//整形匹配vector(int n, const T& val = T()){reserve(n);//reserve不改变capacityfor (int i = 0; i < n; i++){_start[i] = val;}//用多少申请多少,size等于容量_finish = _end_of_storage = _start + n;}//自定义类型匹配vector(size_t n,const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){_start[i] = val;}}//拷贝构造vector(const vector<T>& t)//目标,将t深拷贝给*this{reserve(t.capacity());for (const auto& e : t){push_back(e);}}防止迭代器失效//int _len = t.size();//int _capacity = t.capacity();//将原t内容拷贝到tem//iterator tem = new T[_capacity];//iterator ptem = tem;//iterator start = t._start;//while (start != t._finish)//{//	*ptem = *start;//	++ptem;//	++start;//}tem给*this//_start = tem;//_finish = _start + _len;//_end_of_storage = _start + _capacity;/*iterator tem = new T[capacity()];iterator ptem = tem;iterator start = t._start;//将原vector内容拷贝到新vectorwhile (start != t._finish){*ptem = *start;++ptem;++start;}*/void swap(vector<T> tem){std::swap(_finish, tem._finish);std::swap(_start, tem._start);std::swap(_end_of_storage, tem._end_of_storage);}//赋值重载vector<T> operator=(const vector<T> tem)//拷贝构造创建一个临时对象,用于交换得到有效数据// 只要完成拷贝构造即可完成赋值重载{//现代写法swap(tem);return *this;}//迭代器typedef T* iterator;typedef const T* const_iterator;size_t size() const{return _finish - _start;}size_t capacity() const{return _end_of_storage - _start;}iterator begin() {return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}//保留空间void reserve(int n){//先保存size,防止_start,_finish变化,导致size无法计算int oldsize = size();//要求保留的大于现有的,扩容if (n > capacity()){T* tem = new T[n];if (_start){for (size_t i = 0; i < oldsize; i++){tem[i] = _start[i];}}if(_start)delete[] _start;_start = tem;_finish = _start + oldsize;_end_of_storage = _start + n;}//否则,不缩容}//尾插一个T对象void push_back(const T& t){if (size() == capacity())//需要扩容{int Newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(Newcapacity);//改变capacity,不改变size}//扩容完毕,开始尾插*_finish = t;++_finish;}//一般类型调用,可读可写T& operator[](size_t pos){//空间地址有效assert(pos < size() && pos >= 0);return _start[pos];}//const对象调用的,read-onlyconst T& operator[](size_t pos) const{//空间地址有效assert(pos < size() && pos >= 0);return _start[pos];}void pop_back(){assert(size() > 0);--_finish;}//在pos位置插入对象iterator insert(iterator pos, const T& t)//由于可能需要扩容,会发生迭代器失效,对内部而言//迭代器pos在扩容前后指向的对象不再相同,对外部也是同样的会发生{if (size() == capacity())//需要扩容{int len = pos - _start;int Newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(Newcapacity);//改变capacity,不改变size//记录len,解决迭代器失效的问题pos = _start + len;}//移动对象iterator end = _finish;while (end != pos){*end = *(end - 1);--end;}*pos = t;return pos;}//一般不会出现迭代器失效的问题iterator erase(iterator pos){iterator oldpos = pos;iterator start = pos + 1;while (start < _finish){*(start - 1) = *start;++start;}--_finish;return oldpos;}//析构函数~vector(){if(_start)delete _start;_finish = _end_of_storage = nullptr;}private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};//非成员函数,流插入ostream& operator<<(ostream& out, vector<int> v){for (const auto& e : v){cout << e << " ";}cout << endl;return out;}};

完·~

未经作者同意禁止转载 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/43262.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云 Ubuntu 开启允许 ssh 密码方式登录

以前用的 centos&#xff0c;重置系统为 ubuntu 后在ssh 远程连接时遇到了点问题&#xff1a; 在阿里云控制台重置实例密码后无法使用密码进行 SSH 连接登录 原因&#xff1a;阿里云 Ubuntu 默认禁用密码登录方式 解决办法&#xff1a; 先使用其他用户登录到服务器 这里进来…

国产操作系统安装配置auditd审计工具 _ 统信 _ 麒麟 _ 中科方德

原文链接&#xff1a;国产操作系统安装配置auditd审计工具 | 统信 | 麒麟 | 中科方德 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇在国产桌面操作系统上部署auditd审计工具的文章。auditd是Linux审计系统的核心守护进程&#xff0c;用于记录系统安全相关的事件和…

Python自动化测试系列[v1.0.0][自动化测试报告]

BeautifulReport测试报告 获取BeautifulReport模块 BeautifulReport 源码Clone地址为 BeautifulReport &#xff0c;其中BeautifulReport.py和其template是我们需要的 BeautifulReport 如下代码是BeautifulReport.py的源码&#xff0c;其中几个注释的地方需要注意&#xff…

排序算法(算法篇)

算法之排序算法 排序算法 概念&#xff1a; 我们在的排序工作能在主存中完成的&#xff0c;我们就叫这种算法叫做内部排序不能在主存中完成而必须在磁盘或磁带上完成的排序算法叫做外部排序 冒泡排序 概念&#xff1a; 冒泡排序是一个很简单的排序算法&#xff0c;冒泡排…

【python】PyQt5可视化开发,鼠标键盘实现联动界面交互逻辑与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

一个简单的 Vue 组件例子

https://andi.cn/page/621509.html

YOLOv10改进 | Conv篇 | 利用DualConv二次创新C2f提出一种轻量化结构(轻量化创新)

一、本文介绍 本文给大家带来的改进机制是利用DualConv改进C2f提出一种轻量化的C2f&#xff0c;DualConv是一种创新的卷积网络结构&#xff0c;旨在构建轻量级的深度神经网络。它通过结合33和11的卷积核处理相同的输入特征映射通道&#xff0c;优化了信息处理和特征提取。Dual…

java 参数传递(尤其注意参数是对象的情况)

8大基本数据类型为 值传递 类和数组为 引用传递&#xff0c;传递的是地址 但是要注意虽然类是引用传递&#xff0c;但是要注意&#xff0c;调用方法是新开一个栈 因此如果进行p null或者 Person p new Person()等语句&#xff0c;要格外注意&#xff1a; 如果主函数再次输出…

基于场景的 Java Spring Boot 热门面试问题

随着 Spring Boot 继续主导 Java 生态系统&#xff0c;对熟悉这个强大框架的熟练开发人员的需求正在上升。如果您正在准备 Spring Boot 面试&#xff0c;尤其是作为一名经验丰富的专业人士&#xff0c;那么专注于测试您的实践知识和解决问题能力的基于场景的问题至关重要。本文…

音频demo:将PCM数据与alaw、mulaw、g711数据的相互转换

1、README 前言 (截图来源&#xff1a;https://blog.csdn.net/u014470361/article/details/88837776) 我的理解&#xff1a; 首先需要知道的是u-law/a-law是用于脉冲编码的压缩/解压缩算法。而G.711是指在8KHz采样率&#xff08;单声道&#xff09;中&#xff0c;使用的u-law或…

LLM- 注意力机制

一&#xff1a;什么是注意力机制&#xff0c;以及产生背景&#xff1f; &#xff08;1&#xff09;&#xff1a;RNN模型[RNN模型]的缺点&#xff1a;下图是例如RNN模型解决机器翻译的例子&#xff0c;从这个例子可以看到Encoder最后一个向量&#xff08;eos&#xff09;送给了…

B端全局导航:左侧还是顶部?不是随随便便,有依据在。

一、什么是全局导航 B端系统的全局导航是指在B端系统中的主要导航菜单&#xff0c;它通常位于系统的顶部或左侧&#xff0c;提供了系统中各个模块和功能的入口。全局导航菜单可以帮助用户快速找到和访问系统中的各个功能模块&#xff0c;提高系统的可用性和用户体验。 全局导航…

Kubernetes平台迁移

Kubernetes&&平台迁移 信息收集 信息收集

计算机的错误计算(二十五)

摘要 介绍&#xff08;不&#xff09;停机问题。给了一个算式&#xff0c;当计算机的输出为0时&#xff0c;一般需要提高计算精度继续计算&#xff0c;一直到获得非0值或有效数字。但是&#xff0c;由于事先不清楚算式的准确值是否为0或不为0&#xff0c;因此往往陷入两难境地…

【Java15】继承

继承是面向对象三大特征之一&#xff0c;也是软件代码服用的重要手段。 Java只允许单继承&#xff0c;即每个子类只有一个直接父类。 C中的多继承被Java舍弃了&#xff0c;原因是多继承一方面难以准确表述类之间的关系&#xff0c;另一方面很容易造成代码错误。总结起来就两个…

双系统ubuntu20.04扩容

windows端 打开磁盘管理器&#xff0c;选择需要的盘点击压缩卷 点击未分配的盘&#xff0c;新建简单卷&#xff0c;一致点击下一步即可&#xff0c;记住分配的大小容量 ubuntu端 lsblk 查看所有的磁盘&#xff0c;可以看到新增为nvme0n1p4、nvme1n1p2 win分配的格式为NTFS&a…

【Excel】 批量跳转图片

目录标题 1. CtrlA全选图片 → 右键 → 大小和属性2. 取消 锁定纵横比 → 跳转高度宽度 → 关闭窗口3. 最后一图拉到最后一单元格 → Alt吸附边框![](https://i-blog.csdnimg.cn/direct/d56ac1f41af54d54bb8c68339b558dd1.png)4. CtrlA全选图片 → 对齐 → 左对齐 → 纵向分布!…

全网最适合入门的面向对象编程教程:11 类和对象的Python实现-子类调用父类方法-模拟串口传感器和主机

全网最适合入门的面向对象编程教程&#xff1a;11 类和对象的 Python 实现-子类调用父类方法-模拟串口传感器和主机 摘要&#xff1a; 本节课&#xff0c;我们主要讲解了在 Python 类的继承中子类如何进行初始化、调用父类的属性和方法&#xff0c;同时讲解了模拟串口传感器和…

OpenHarmony 入门——单元测试UnitTest快速入门

引言 OpenHarmony 的单元测试&#xff08;UnitTest&#xff09;是一个关键的软件开发过程&#xff0c;它确保代码的各个部分能够按预期工作&#xff0c;OpenHarmony的测试框架中提供了很多种的单元测试&#xff0c;今天简单介绍下UnitTest 类型的TDD测试。 OpenHarmony 的TDD …

Nacos 国际化

项目需要&#xff0c;后端异常信息需要进行国际化处理。所有想有没有方便易用的可选项。 1、国际化配置调整&#xff0c;不需要重启系统 2、可支持添加不同或自定义语言包&#xff08;就是配置的资源文件&#xff09; 参考&#xff1a; Nacos实现SpringBoot国际化的增强_spr…