当前位置: 首页 > news >正文

C++STL的vector模拟实现

文章目录

  • 前言
  • 成员变量
  • 成员函数
    • 构造函数
    • push_back
    • pop_back
    • insert
    • erase
    • 析构函数
    • 拷贝构造

前言

成员变量

namespace but
{template<class T>class vector{public:typedef T* iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;};
}

我们之前实现顺序表是用指向数组的的指针和数组个数和容量来维护顺序表的,这里用三个指针来实现其实大差不差。
在这里插入图片描述

成员函数

构造函数

vector():_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{}

push_back

void push_back(const T& x)
{if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : capacity() * 2);//避免容量为0扩容还是0的情况}*_finish = x;++_finish;
}

首先放数据怎么放呢?
直接赋值。

为什么可以直接赋值?
因为空间是new出来的,如果是内置类型,有没有初始化都可以直接赋值。
但是自定义类型没有初始化不能直接赋值。

扩容

void reserve(size_t n)
{//避免缩容//1.缩容的代价太大//2.反复缩容与扩容,降低了性能。if (n > capacity()){size_t sz = size();T* tmp = new T[n];//如果旧空间没有数据就不用拷贝了if (_start){//因为是类型不一定是字符串,所以得使用memcpymemcpy(tmp, _start, sizeof(T)*size());delete[] _start;}_start = tmp;//这里有个小坑//_finish=_start + size();//提前把size()记录下来,防止这里出错//改成tmp也可以,但是有点影响我们原本的理解,不利于维护。_finish = _start + sz;_end_of_storage = _start + n;}
}

我们把其他一些需要用到的代码补上,然后测试一下。

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

迭代器

//这个普通迭代器实现起来也相当简单
iterator begin()
{return _start;
}iterator end()
{return _finish;
}

pop_back

删除数据好像也很简单,直接finish- -就可以,但是也有一些问题,那具体有什么问题呢?我们来看一下。
在这里插入图片描述
我们简单测试一下。
我们一直pop,就出问题了。
在这里插入图片描述
_finish一直减就走到前面去了,这样我们使用迭代器就出问题了。

我们简单改一下。

void pop_back()
{assert(!empty());--_finish;
}
bool empty()
{return _start == _finish;
}

针对const对象的访问
在这里插入图片描述
本质还是涉及到权限的放大,我们把成员函数都改为const就可以了。

resize()

void resize(size_t n, T val = T())//T()默认构造,是匿名对象,具体解释看下面
{if (n < size()){// 删除数据_finish = _start + n;}else{if (n > capacity())reserve(n);while (_finish != _start+n){*_finish = val;++_finish;}}
}

上面resize的缺省值能不能给0?
其实答案很明显, 显然不行,因为T是泛型编程,类型不一定是int,如果是double或者指针,对象都不行。

那问题又来了,int有默认构造函数吗?
我们在之前学习类和对象的时候知道,内置类型是没有构造函数的,但是有了模板之后它需要有。

在这里插入图片描述

insert

在这里插入图片描述
下面这段代码有什么问题?
在这里插入图片描述
如果容量不够,挪动数据就越界了。
在这里插入图片描述
除了容量不够还有没有其他什么问题?
提示一下pos==0; 好吧,其实不会发生之前模拟实现string的时候发生的无符号整型最大值的问题。

在这里插入图片描述
大家看这样子去测试就出问题了
在这里插入图片描述
注意,func(v1)是两次读取
程序运行时崩了。
在这里插入图片描述

先简单分析一下,这可能时内存问题,可能时数组越界。
为什么push_back 5次的时候没问题,push_back 4次就出问题了?
5个和4个的区别是什么?

insert的时候扩容出问题了。

注意看
在这里插入图片描述
在这里插入图片描述
发生了什么,扩容之后,start和finish变了,start 和finish为什么会变?
这是我们遇见的最经典的迭代器失效的问题。

pos变成野指针了。
这也就导致一直在循环的问题。
在这里插入图片描述
那这个怎么解决呢?
更新一下pos.

//void insert(iterator pos, const T& val)
iterator insert(iterator pos, const T& val)
{assert(pos >= _start);assert(pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);// 扩容后更新pos,解决pos失效的问题pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;return pos;
}

接下来,继续问一个问题,看前面的测试图片,insert之后能不能修改迭代器的位置。

(*pos)++;//我想修改这个3的位置。
func(v1);

在这里插入图片描述
程序没有报错,但是很明显不符合我们的预期。为什么?
不是已经把pos更新了吗,为什么外面还是不行呢?怎么不起作用。
因为自己写的insert是传值形参,形参的改变 不会影响实参的改变 。

怎么解决?

能不能用引用传参解决,看起来不错,实际不好。用引用传参会报错,为什么?
在这里插入图片描述

insert 引起迭代器失效的两种情况:
1.野指针问题
2.意义已经变了

通过返回值去解决
但是最好别用,你也不知道是什么时候失效。
insert以后我们认为pos失效了,不能再使用。

erase

在这里插入图片描述
现在又有一个问题,erase以后pos会不会失效?
不会,但是库里面的失效了,并且vs报错非常强烈,看下面。
在这里插入图片描述
报了一个断言错误。

再看一下g++下的运行。
在这里插入图片描述

那到底erase以后pos失效还是不失效?vs比较合理还是g++合理?
如果pos位置是4呢,那这个位置就很不合理了。

所以我们认为失效,不要访问,行为结果未定义,跟具体的编译器有关。
一定要注意,不然会被坑的很惨。

要想解决一下这种情况,我们都是用返回值来处理一下,其实本质就是不要pos跳过。

iterator erase(iterator pos)
{assert(pos >= _start);assert(pos < _finish);iterator start = pos + 1;while (start != _finish){*(start - 1) = *start;++start;}--_finish;return pos;
}

下面这个测试,连续的偶数和最后一个是偶数都能解决,就没什么大问题了。
在这里插入图片描述

1. vs进行强制检查,erase以后,迭代器不能访问。
2.g++没有强制检查,具体问题具体分析,结果未定义。

string有没有迭代器失效?
有,但是string不容易发生迭代器失效问题。
insert和erase不喜欢用迭代器,用的都是下标。

析构函数

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

下面的构造函数。
在这里插入图片描述

给大家看一个大坑。
先看下面的代码,这样写有没有什么问题?
在这里插入图片描述
程序崩了

不初始化会出各种问题。
一调试很容易看到会出现哪些问题。

加上初始化列表

vector(size_t n, const T& val = T())//T()是什么前面已经讲得很清楚了: _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(n);for (size_t i = 0; i < n; ++i){push_back(val);}
}

看一下这个构造函数。
在这里插入图片描述
在这里插入图片描述
如果是用iterator就写死了,你必须用vector的迭代器来进行初始化。

迭代器区间初始化,是必须用vectro的迭代器初始化吗?
一个容器用迭代器区间初始化,需要时任意类型的迭代器。

这里引出了另一个语法,允许一个类的成员函数再是函数模板。

// [first, last)
template <class InputIterator>
vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{while (first != last) //不能用 <=,如果是链表肯定就不行{push_back(*first);++first;}
}

如果我们不写初始化列表,可用c++11的语法,成员变量声明的时候给个缺省值
缺省值是给构造函数的初始化列表用的。
在这里插入图片描述

在这里插入图片描述

奇怪的事情发生了,编译的时候报了一个这样的错误
在这里插入图片描述

为什么匹配错了,匹配到上面写的那个迭代区间初始化那个?
在这里插入图片描述

我们知道编译器会调用最匹配的那个,仔细分析一下其实可以发现,如果推演一下的话,如果要匹配 vector(size_ t n, const T& val =T()); 的话,就会发生类型转换,而调用迭代器区间初始化的话,进行推演,如果类型是int 直接就匹配上了。

然后看迭代器区间初始化的代码,int不能解引用,就直接报错了。

怎么解决?
1.加个u, 代表我这个变量是无符号。
在这里插入图片描述

2…看STL的源码,看源码是怎么解决这个问题的。
我们这里可以用一个很简单的方式去就解决,提供一个重载的版本。

引用返回时有风险的,要谨慎使用,除非你想想operator【】那样,可以去修改。

给大家看一下神奇的东西。
在这里插入图片描述
只要类型能匹配,char可以发生转换。

最神奇的还是可以这么玩。
在这里插入图片描述

甚至可以是数组,为什么可以是数组?
原生指针可以是天然的迭代器,有个前提,这个原生指针是指向数组。
其实vector的迭代器,string的迭代器也可以是原生指针。

接着扩展一下。
sort

默认是升序
在这里插入图片描述
这是一个函数模板,它的名字叫随机访问迭代器,那随机访问是什么呢?一般底层是数组。
在这里插入图片描述
它可以帮我们排序,并且用起来很爽。

如果是降序,我们就这么用。
1.
在这里插入图片描述
2.
在这里插入图片描述
这两个其实是等价了。

拷贝构造

我们还涉及深浅拷贝的问题。
在这里插入图片描述
首先这样写,是我们经典的浅拷贝的问题。

我们先写一个传统写法的深拷贝。
在这里插入图片描述

vector(const vector<T>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{_start = new T[v.capacity()];memcpy(_start, v._start, sizeof(T) * v.size());_finish = _start + v.size();_end_of_storage = _start + v.capacity();
}

看下面的代码,崩了,为什么?
在这里插入图片描述

在这里插入图片描述
也就是说当我们数据是int的时候程序能正常运行,当我们的数据是string的时候就崩的。

因为memcpy也是一种浅拷贝。调用拷贝构造的时候,memcpy干了什么事情。
从起始位置开始依次把所有值拷贝下来。

在这里插入图片描述
这里面还有一层是我们没有考虑的,这是深拷贝里面又套了一层深拷贝。memcpy就是深层次的浅拷贝。
调用析构的时候会崩。

怎么解决呢?
我们肯定是要解决三个问题,数据是int类型,或者string类型,或者vector的vector类型。

怎样完成深拷贝呢?难道是我们自己写吗?
我们不能自己解决,因为T是模板,我们也不知道它具体是什么类型。
不能自己给它写一个深拷贝,因为它们是私有的,动不了里面的。

所以我们这里面调一个深拷贝的函数来完成。
赋值就是深拷贝。
在这里插入图片描述

其实我们还没有完全解决所有的问题 ,除了拷贝构造用了memcpy,扩容也用了memcpy。

修改扩容的代码。

void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start){//memcpy(tmp, _start, sizeof(T)*size());for (size_t i = 0; i < sz; ++i){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;}
}

那接着,哪里还有问题呢?vector里面的数据比如说是vector对象呢,比如vectro<vector >,给大家看一下,一个杨辉三角的例子。
在这里插入图片描述
测试
在这里插入图片描述

在这里插入图片描述
出错了,为什么?还有什么问题没有解决。
外面的vector是深拷贝,里面的vector是浅拷贝。
问题还是出现在拷贝构造,我们并没有自己写赋值。所以编译器还是用的默认生成的,是浅拷贝。

我们自己写个赋值就解决了。
在这里插入图片描述

现代写法
直接复用。

void swap(vector<T>& v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);
}
vector(const vector<T>& v)
{vector<T> tmp(v.begin, v.end());swap(tmp);
}

最后一个小问题,不加模板参数语法上也允许,但是不推荐这样写。
在这里插入图片描述

相关文章:

  • 现代皮质沙发模型材质编辑
  • React中父子之间数据的通信方式
  • 托盘四向穿梭车自动化密集库供应|单机智能向系统智能跨越的HEGERLS托盘四向车系统
  • c# bitmap压缩导致png不透明的问题解决
  • 关于mars3d通过zIndex参数实现控制图层层级叠加效果说明
  • 【go-zero】go-zero使用ent框架 如何使用源生sql完成查询
  • YOLOv8算法改进【NO.86】将主干特征网络替换为2023年顶会CVPR的EfficientViT,助力SCI论文发表
  • Kotlin关键字二——constructor和init
  • 算法通关村第十三关—数学与数学基础问题(青铜)
  • ekho环境Linux通过Docker安装
  • AI 训练框架:Pytorch TensorFLow MXNet Caffe ONNX PaddlePaddle
  • 最大公约数gcd的通俗理解和Java代码的实现
  • 多维时序 | MATLAB实现TSOA-TCN-Multihead-Attention多头注意力机制多变量时间序列预测
  • Redis(三):常见数据类型:List、Set、Zset
  • 在React中使用动态图标
  • [LeetCode] Wiggle Sort
  • 2017前端实习生面试总结
  • 2019年如何成为全栈工程师?
  • CentOS7简单部署NFS
  • Github访问慢解决办法
  • If…else
  • Java知识点总结(JavaIO-打印流)
  • Joomla 2.x, 3.x useful code cheatsheet
  • js对象的深浅拷贝
  • JS实现简单的MVC模式开发小游戏
  • Linux快速复制或删除大量小文件
  • React+TypeScript入门
  • 测试如何在敏捷团队中工作?
  • -- 查询加强-- 使用如何where子句进行筛选,% _ like的使用
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 吴恩达Deep Learning课程练习题参考答案——R语言版
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • $分析了六十多年间100万字的政府工作报告,我看到了这样的变迁
  • (11)MATLAB PCA+SVM 人脸识别
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (超详细)语音信号处理之特征提取
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (附源码)计算机毕业设计SSM智能化管理的仓库管理
  • (免费领源码)Python#MySQL图书馆管理系统071718-计算机毕业设计项目选题推荐
  • (篇九)MySQL常用内置函数
  • (七)理解angular中的module和injector,即依赖注入
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • .apk文件,IIS不支持下载解决
  • .gitattributes 文件
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .NET BackgroundWorker
  • .NET MVC第五章、模型绑定获取表单数据
  • .net Stream篇(六)
  • .net 程序 换成 java,NET程序员如何转行为J2EE之java基础上(9)
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • [ vulhub漏洞复现篇 ] Celery <4.0 Redis未授权访问+Pickle反序列化利用
  • [ vulhub漏洞复现篇 ] struts2远程代码执行漏洞 S2-005 (CVE-2010-1870)
  • [2023-年度总结]凡是过往,皆为序章
  • [ASP]青辰网络考试管理系统NES X3.5
  • [CareerCup] 12.3 Test Move Method in a Chess Game 测试象棋游戏中的移动方法