yo!这里是STL::vector类简单模拟实现

目录

前言

重要接口模拟实现

默认成员函数

1.构造函数

2.析构函数

3.拷贝构造函数

4.赋值运算符重载

迭代器

简单接口

1.size()

2.capacity()

3.swap()

操作符重载

1.操作符[]

扩容接口

1.reserve()

2.resize()

增删查改接口

1.push_back()

2.pop_back()

3.insert()

4.erase()

迭代器失效问题

1.问题及解决

 重载调用问题

1.问题及解决

后记


前言

        在模拟完string类之后,下一个我们来模拟实现的是STL中的vector类,相当于c语言中的顺序表,在一些接口的实现上可以参考顺序表的实现,所以这篇文章是在讲解vector的重要接口,一些普通接口不过多赘述。

        根据STL库里的vector,成员变量有_start、_finish、_end_of_storage,是三个指针变量,分别指向第一个数据的位置地址、最后一个数据的下一个位置地址、总申请空间的下一个位置地址,实则vector是一个类模板,实现为tempale <class T> class vector{ ... };,这里的细节不多说,重点看看下面接口的实现、底层逻辑以及参杂在其中的问题吧!

重要接口模拟实现

  • 默认成员函数

1.构造函数

        构造函数有多种重载形式,包括普通无参的默认构造函数、传迭代器区间构造函数、用n个值构造函数,

        ①无参的构造函数很简单,将三个指针变量置空即可,在初始化列表可以,在函数体内实现也可;

        ②定义函数模板InputIterator,而不直接使用iterator?因为可以传入不同类型的迭代器区间进行构造,不局限于类模板中的数据类型,这里也是复用的方式用尾插数据;

        注意:一定要在初始化列表置空成员变量,因为pushback一开始肯定会reserve,释放空间delete遇到未初始化指针会报错。

        ③用n个值构造也很好理解,传入值的个数和值,循环尾插值即可,其中,这里T()是匿名对象,若不传值进来,就会调用默认构造初始化val,(相当于int类型不传值就默认是0,指针不传值就默认是nullptr),如果T是自定义类型能理解,如果是内置类型,难道内置类型也有默认构造函数?yes!比如:int a = int(),此时a是0 。

        注意:使用 n个值构造函数构造时,需要加上下面代码中最后一个重载,这涉及到重载调用问题,在文章最后会介绍到。

代码:

    //无参构造函数Vector(): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){}//传迭代器区间拷贝template <class InputIterator>   Vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){while (first != last){push_back(*first);  ++first;}}//n个值构造Vector(size_t n, const T& val= T())  : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);   //reserve里有delete,需要先在初始化列表置空for (size_t i; i < n; i++){push_back(val);}}Vector(int n, const T& val = T())  : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);for (int i = 0; i < n; i++){push_back(val);}}

2.析构函数

        对应的析构函数也不难,因为后面插入数据肯定是要申请空间的,所以在析构函数中需要去释放,也就是_start指向的地址空间,释放之后将三个指针置空即可。

代码:

	~Vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}

3.拷贝构造函数

        对于拷贝构造函数,与模拟实现string时差不多,除了使用传统写法,是不是可以尝试使用现代写法呢?

        先看传统写法,很好理解,先开辟相同大小的地址空间,再将数据拷贝过去,然后将成员变量指向正确的位置,其中,size()是返回元素个数,capacity()是返回容量,值得注意的是,拷贝数据时不能使用memcpy,因为是值拷贝,如果T是自定义类型,就会在析构时出错,而要用赋值的方式(每一次赋值都是深拷贝);

        复用的方法也很简单,将原对象的数据循环尾插到目标对象,也要注意,在范围for那里传引用,因为里面的元素可能是自定义类型(比如string),那么传值给e,又是一次深拷贝,代价很大,所以建议用引用传值;

        现代写法:使用传迭代器构造函数定义一个临时对象,再使用swap函数(下面有介绍)交换两个对象的成员,而临时对象作为局部变量,出了作用域自动调用析构函数销毁,完美拷贝构造了目标对象。

代码:

    //传统方法Vector(const Vector<T>& v){_start = new T[v.capacity()];//memcpy(_start, v._start, sizeof(T) * v.size());  for (size_t i = 0; i < v.size(); i++)   {_start[i] = v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}//复用的方式Vector(const Vector<T>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(v.size());for (const auto& e : v) {push_back(e);}}//现代写法Vector(const Vector<T>& v){Vector<T> tmp(v.begin(), v.end());swap(tmp);}

4.赋值运算符重载

        赋值运算符重载的实现不多赘述,参考拷贝构造函数的现代写法。

代码:

	Vector<T>& operator=(const Vector<T>& v){Vector<T> tmp(v.begin(), v.end());swap(tmp);return *this;}
  • 迭代器

        vector的迭代器也是原生指针,是里面存放的数据类型的指针,与string一样,可以参考http://t.csdn.cn/dYgNp ,同时也要加上const对象可以调用的迭代器。

 代码:

	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;}
  • 简单接口

1.size()

        size()是指数据元素的个数,因为_start指向第一个数据的位置地址、_finish是最后一个数据的下一个位置地址,所以元数个数就是_finish - _start 。

 代码:

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

2.capacity()

        capacity()是指所申请空间的个数,因为_start指向第一个数据的位置地址,_end_of_storage是总申请空间的下一个位置地址,所以元数个数就是_end_of_storage- _start 。

 代码:

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

3.swap()

        由于stl库里algorithm中的swap是深拷贝,会将两个对象的变量连同地址空间一块交换,代价较大,所以一般情况下,每个自定义类型要有自己的swap,交换过程则是复用stl里的swap函数,仅交换指针的指向。

 代码:

    void swap(Vector<T>& v)   {std::swap(_start, v._start);   //这里仅是交换这两个指针的指向std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}
  • 操作符重载

1.操作符[]

        由于vector底层就是个数组,所以stl库里也是提供[]加下标访问元素,实现与string中的[]操作符重载一致,不再赘述。

 代码:

	T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < size());return _start[pos];}
  • 扩容接口

1.reserve()

        实际上,reserve()的实现也是参考string的模拟实现可以写出,注意点已在下方代码中标记,注意即可。

 代码:

	void reserve(size_t cap){if (cap > capacity()){size_t ts = size();iterator tmp = new T[cap];//memcpy(tmp, _start, sizeof(T) * ts);  //也不能用memecpyfor (size_t i = 0; i < ts; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = _start + ts;   //这里不能使用size(),因为此时start位置已经更新,应该使用旧size(),即ts_end_of_storage = _start + cap;}}

2.resize()

        resize()比reserve()多个初始化,即不仅要修改_end_of_storage的指向,还要修改_finish的指向,实现过程中,注意好三种情况:①要求容量比实际容量大;②要求容量比实际容量小,但比元素个数大;③要求容量比元素个数小

 代码:

	void resize(size_t cap, const T& val = T()){size_t ts = size();if (cap > capacity()){reserve(cap);for (size_t i = 0; i < capacity() - ts; i++)  //注意:不能是capacity()-size(),因为size()会变化{push_back(val);}}else{if (cap > ts){for (size_t i = 0; i < cap - ts; i++){push_back(val);}}else{_finish = _start + cap;}}}
  • 增删查改接口

1.push_back()

        push_back ()即尾插,传参最好是引用传参,否则有可能T是string等类型,深拷贝代价特别大,实现简单,不再赘述。

 代码:

	void push_back(const T& t)  {if (_finish == _end_of_storage){reserve(capacity() ? capacity() * 2 : 4);}*_finish = t;_finish++;}

2.pop_back()

        pop_back()即尾删,删除最后一个元素,直接_finish指针--即可。

 代码:

	void pop_back(){assert(_finish > _start);_finish--;}

3.insert()

        在stl库里的insert()实现中,形参是插入位置的迭代器和需插入元素,返回插入元素的迭代器,根据string中insert的实现逻辑,这里的insert() 也是很容易实现出来,但这不是重点,值得注意的是会出现迭代器失效问题,文章最后会介绍并且解决。

代码:

	iterator insert(iterator pos, const T& t){assert(pos >= _start);assert(pos <= _finish);if (pos == _finish){push_back(t);return _finish;}if (_finish == _end_of_storage)  {size_t pos_len = pos - _start;  //①reserve(capacity() ? capacity() * 2 : 4);pos = _start + pos_len;  //②}iterator i = _finish;while (i > pos)  //③{*i = *(i - 1);--i;}*pos = t;++_finish;return pos;}

4.erase()

        erase()则是删除所传入迭代器的元素,规定返回所删除位置的下一个位置的迭代器,实现逻辑参考string模拟实现也不难写出,这里讨论删除一定数量的元素是否应该缩容?

        答:不建议缩容,因为缩容是以时间换空间,效率低下,如果涉及到缩容,就有可能引发迭代器失效问题(看了后面介绍就知道为什么会引发),但也是建议不要删除pos位置的数据之后,再次访问pos迭代器,因为排除不了其他库对erase()的实现没有缩容。

 代码:

    iterator erase(iterator pos){assert(pos >= begin());assert(pos < end());if (pos == end() - 1){pop_back();return end();}iterator i = pos;while (i < _finish - 1){*i = *(i + 1);i++;}--_finish;return pos;}

迭代器失效问题

1.问题及解决

           在上面的insert()、erase()实现中都有提到一个迭代器失效问题,是什么呢?先说结论:在insert()、erase()函数中,不要直接访问pos,访问了要更新,不然会出现意料之外的结果,这就是迭代器失效。看看下面三种情况:

        情况一(野指针的失效):扩容/缩容之后pos就会失效。见代码一,这是insert实现的一段代码,当需要扩容时,调用reserve函数删除旧空间,开辟新空间,就会导致pos指针所指向的空间被释放,进而导致③处进入死循环。
        解决:加上①②处语句,即记录原本pos距离开头的位置,reserve回来之后更新pos位置即可。

        情况二:调用insert()之后,再次使用pos,此时pos可能因为扩容变了,或者插入了数据之后变了,导致失效。看下面代码二,使用了stl中的insert()的一段,看上去很正确,但执行就会报错,如下图:

 而将标记处注释掉,就可以正常运行,如图:

        说明:若insert函数中发生扩容,释放旧空间开辟新空间后,pos指向空间释放或者指向新位置,但由于是一份拷贝,不会影响实参,所以外面的pos没有发生变化,当再次使用pos时就会失效,那么这里为什么insert函数不传pos的引用呢?
         答:首先库里没用,其次就是会有后患,比如再次调用insert(v.begin(),80);其中begin()函数返回迭代器的拷贝,具有常性,无法成为insert函数实参传引用到insert函数里。

         解决:这是一个固有问题,无法解决,只能避免这种情况,即在pos位置插入数据之后,不要再访问迭代器pos,因为pos可能已经失效。

        

        情况三:循环调用insert()、erase()函数(比如删除所有的偶数)时,注意控制去遍历的迭代器变量,控制不好就会发生失效,如下代码三,错误实现会报错:

 正确与错误实现的关键区别在于:erase函数会返回所删除元素的下一个位置的迭代器,不需要再次it++,而没调用erase函数才需要it++。

        解决:insert()、erase()函数都会返回新的迭代器,在使用这两个函数时一定要注意控制迭代器。

代码一:

		if (_finish == _end_of_storage)  {size_t pos_len = pos - _start;  //①reserve(capacity() ? capacity() * 2 : 4);pos = _start + pos_len;  //②}iterator i = _finish;while (i > pos)  //③{*i = *(i - 1);--i;}

代码二:

int main()
{Vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);Vector<int>::iterator pos = find(v1.begin(), v1.end(), 3);if (pos != v1.end()){v1.insert(pos, 80);//再次访问迭代器poscout << *pos << endl;   //标记处}for (auto e : v1){cout << e << " ";}cout << endl;return 0;
}

代码三:

//正确
int main()
{Vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);//v1.push_back(5);auto it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){it = v1.erase(it);}else{++it;}}return 0;
}//错误
int main()
{Vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);//v1.push_back(5);auto it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){it = v1.erase(it);}++it;}return 0;
}

 重载调用问题

1.问题及解决

        介绍:当有多个重载形式时,调用此函数要格外小心,因为会调用形参与实参最匹配的重载形式,导致没能调用到目标函数,而产生意料之外的结果。

        eg:当使用n个值构造函数构造时,会发现Vector<int> v(10, 1)会报错,但Vector<int> v(10, 'a')不会报错,因为什么?
        原因:对于多个重载形式,调用时编译器会用参数匹配度最高的函数,Vector<int> v4(10, 1)中的实参类型相同且都是int,所以会调用传迭代器区间构造函数,而不是n个值构造函数,因为10和1都是int类型,正好符合first和last(又不是一定要传迭代器),而n个值构造函数是size_t和int类型,不够符合,从而进入传迭代器区间构造函数,而函数中有个*first,即对int类型解引用,所以会报间接寻址的错,而Vector<int> v4(10, 'a')由于参数类型不同,正好匹配n个值构造函数,所以不会报错。

        解决:重载下方代码中的第二个构造函数(Vector(int n, const T& val = T()) ),传两个int类型时,就会调用此构造函数,因为比传迭代器区间拷贝函数匹配度更高。

代码:

	//n个值构造Vector(size_t n, const T& val= T())  : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);   //reserve里有delete,需要先在初始化列表置空for (size_t i; i < n; i++){push_back(val);}}Vector(int n, const T& val = T())  : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);for (int i = 0; i < n; i++){push_back(val);}}

后记

        在模拟实现string的基础上,模拟出大部分vector的接口函数并不难,难以捉摸的是此外出现的问题,而模拟实现vector类的重点也正是如此,要不然也并不值得作为一篇博客,希望能够抓住重点,理解vector的重点接口实现以及衍生问题的解决,多多练习,拜拜!


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

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

相关文章

大数据Flink(五十五):Flink架构体系

文章目录 Flink架构体系 一、 Flink中的重要角色 二、Flink数据流编程模型 三、Libraries支持

Flutter Flar动画实战

在Flare动面出现之前,Flare动画大体可以分为使用AnimationController控制的基础动画以及使用Hero的转场动画,如果遇到一些复杂的场景,使用这些动画方案实现起来还是有难度的。不过,随着Flutter开始支持Flare矢量动面,Flutter的动画开发也变得越来越简单。事实上,Flare动画…

实现天气预报走势图

实现效果&#xff1a; 这里我用的天气接口是网上开源的&#xff0c;可以自己找一下。 稍微简单封装了一下axiso以及接口 封装的axios&#xff1a; // import { useUserStore } from /stores/user import axios from axios import router from /router import { ElMessage } f…

9 多进程编程

文章目录 进程以及状态进程进程的状态linux下的进程管理pstopbgkillcrontab 进程的创建两个while同时运行示例获取进程pidProcess 结构给子进程指定的函数传递参数进程间是否共享全局变量补充 进程间通信-Queue常用操作Queue 实例 管道通信&#xff08;了解&#xff09;样例 进…

问题:idea启动项目错误提示【command line is too long. shorten command line】

问题&#xff1a;idea启动项目错误提示【command line is too long. shorten command line】 参考博客 问题描述 启动参数过长&#xff0c;启动项目&#xff0c;错误提示 原因分析 出现此问题的直接原因是&#xff1a;IDEA集成开发环境运行你的“源码”的时候&#xff08…

python机器学习(六)决策树(上) 构造树、信息熵的分类和度量、信息增益、CART算法、剪枝

决策树算法 模拟相亲的过程&#xff0c;通过相亲决策图&#xff0c;男的去相亲&#xff0c;会先选择性别为女的&#xff0c;然后依次根据年龄、长相、收入、职业等信息对相亲的另一方有所了解。 通过决策图可以发现&#xff0c;生活中面临各种各样的选择&#xff0c;基于我们的…

网络安全 Day26-PHP 简单学习

PHP 简单学习 1. 为什么要学习PHP2. PHP语法3. php 变量4. 字符串数据5. PHP 函数6. 数组 1. 为什么要学习PHP php存量多开源软件多很多安全流程 渗透方法 sql注入基于PHP语言入门简单 2. PHP语法 格式: <?php 内容?>或<?内容?>结尾分号例子<?php phpin…

Stable Diffusion - SDXL 1.0 全部样式设计与艺术家风格的配置与提示词

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132072482 来源于 Anna Dittmann 安娜迪特曼&#xff0c;艺术家风格的图像&#xff0c;融合幻想、数字艺术、纹理等样式。 SDXL 是 Stable Diffus…

特性Attribute

本文只提及常用的特性&#xff0c;更多特性请查看官方文档。 AddComponentMenu - Unity 脚本 API 常用特性 AddComponentMenu 添加组件菜单 使用 AddComponentMenu 属性可在“Component”菜单中的任意位置放置脚本&#xff0c;而不仅是“Component > Scripts”菜单。 使用…

go 基本语法(简单案例)

&#xff01;注&#xff1a; go中 对变量申明很是严格&#xff0c;申明了&#xff0c;在没有使用的情况下&#xff0c;也会产生编译错误 1.行分隔符 一行就是代码&#xff0c;无&#xff1b;分割&#xff0c;如果需要在一行展示&#xff0c;需要以&#xff1b;分割&#xff0c;…

Elasticsearchr入门

首先在官网下载elasticsearch8.9版本&#xff0c;以及8.9版本的kibana。 解压&#xff0c;点击es8.9bin目录下的elasticsearch.bat文件启动es 如图所示即为成功。 启动之后打开idea&#xff0c;添加依赖 <dependency><groupId>com.fasterxml.jackson.core</g…

MySQL~mysql基础应用相关题

整卷阅览&#xff1a; 想要获取试卷原版请点击以下链接下载&#xff1a; https://download.csdn.net/download/qq_53142796/88168133https://download.csdn.net/download/qq_53142796/88168133 解题过程&#xff1a; 数据库&#xff1a;studentdb 数据库表如下&#xff1a; …

pycharm运行pytest无法实时输出信息

需要去掉控制台输出。根据查询相关信息显示pycharm运行pytest无法实时输出信息&#xff0c;需要去掉pycharm里面的运行模式&#xff0c;点击减号&#xff0c;再点击加号&#xff0c;添加python执行文件即可实时输出信息。 问题描述&#xff1a; 使用pycharm运行代码时&#x…

.Net6 Web Core API 配置 Autofac 封装 --- 依赖注入

目录 一、NuGet 包导入 二、Autofac 封装类 三、Autofac 使用 四、案例测试 下列封装 采取程序集注入方法, 单个依赖注入, 也适用, 可<依赖注入>的地方配置 一、NuGet 包导入 Autofac Autofac.Extensions.DependencyInjection Autofac.Extras.DynamicProxy 二、Auto…

【数据结构与算法】堆排序

堆排序 基本介绍 堆排序是利用堆这种数据结构而设计的一种排序算法&#xff0c;堆排序是一种选择排序&#xff0c;它的最坏&#xff0c;最好&#xff0c;平均时间复杂度均为 O(n log n)&#xff0c;它也是不稳定排序。堆是具有以下性质的的完全二叉树&#xff1a;每个节点的值…

Qt展示动态波形

Qt展示动态波形 需求描述成品展示实现难点Qt多线程 需求描述 接入串口&#xff0c;配置串口顺序进行接收数据&#xff1b;数据分成两个串口分别传入&#xff0c;使用多线程并发接入&#xff1b;时域数据有两个通道&#xff08;I&#xff0c;Q&#xff09;&#xff0c;分别以实…

Unity进阶--使用PhotonServer实现服务端和客户端通信--PhotonServer(一)

文章目录 Unity进阶--使用PhotonServer实现服务端和客户端通信服务器的安装和配置添加日志客户端的配置客户端和服务器的通信Dlc 出现vscode引用不好使的时候 Unity进阶–使用PhotonServer实现服务端和客户端通信 服务器的安装和配置 Photon的地址&#xff1a;https://www.ph…

高速公路巡检无人机,为何成为公路巡检的主流工具

随着无人机技术的不断发展&#xff0c;无人机越来越多地应用于各个领域。其中&#xff0c;在高速公路领域&#xff0c;高速公路巡检无人机已成为公路巡检的得力助手。高速公路巡检无人机之所以能够成为公路巡检中的主流工具&#xff0c;主要是因为其具备以下三大特性。 一、高速…

Leetcode | 有效的括号、最长有效括号

一、有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应…

webpack复习

webpack webpack复习 webpack基本配置 拆分配置 - 公共配置 生产环境配置 开发环境配置 使用merge webpack-dev-server 启动本地服务 在公共中引入babel-loader处理es6 webpack高级配置 多入口文件 enty 入口为一个对象 里面的key为入口名 value为入口文件路径 例如 pa…