C++ vector 的模拟实现

 

目录

1. vector 类的成员变量

2. 无参构造

3. 析构函数 

4.  size_t capacity()

5. size_t size()

6. void reserve(size_t n)

 7. 迭代器

8. void push_back(const T& x) 

9. T& operator[](size_t pos) 

10. iterator insert(iterator pos, const T& val)

11.  iterator erase(iterator pos)

12. void pop_back()

13. void resize(size_t n, const T& val = T()) 

14. void swap(vector& v)

15. 拷贝构造 

16. 赋值运算符重载 

17. vector(size_t n, const T& val = T()) 

18. vector(InputIterator first, InpuIterator last) 


1. vector 类的成员变量

在上一讲我们学习了如何使用vector,我们很可能会认为:vector类的成员变量是和 string 类的成员变量差不多:T* _data,size_t size,size_t _capacity。这样来实现当然没有什么问题,这不就是和顺序表的实现差不多嘛!但是我们会参考库里面 vector 的实现来模拟实现 vector。

我们可以参考 STL_30 中 vector 的源码:

 我们可以看到库里面关于 vector 的实现是维护三个迭代器变量,用这三个迭代器变量来控制成员函数的实现逻辑。根据变量名,我们可以盲猜出这三个变量的含义:

恭喜你,猜对了!库里面的三个迭代器变量就是这么一个意思。在 vector 的使用哪一节,我们已经知道了vector 的迭代器就是 T*。那我们就能够定义出 vector 的基本结构啦!但这里有个问题就是维护三个迭代器变量来实现 vector 有什么好处呢?我们在实现的过程中再来细谈!

namespace Tchey
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;private:iterator _start;iterator _finish;iterator _endofstorage;}
}

2. 无参构造

无参构造不需要做什么事儿,只需要将我们的三个迭代器初始化为 nullptr 就行啦!

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

3. 析构函数 

析构函数就是释放 vector 维护的空间,只有当 _start 不为 nullptr 才需要释放。当然 delete nullptr也没啥问题,但是为了严谨嘛!

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

4.  size_t capacity()

这个函数比较简单呢!vector 的实际容量就是 _endofstorage - _finish 啊!可以结合三个变量的意义来看:

size_t capacity()
{return _endofstorage - _start;
}

5. size_t size()

size 的求法和 capacity 的求法是一样的哇!size = _finish - _start。请参照上图。

size_t size()
{return _finish - _start;
}

6. void reserve(size_t n)

1:判断是否需要扩容!只有当 n > capacity() 的时候才需要扩容!为什么要有这一步检查呢?因为这个 reserve 不仅仅是给其他成员函数使用的,还有可能直接被用户使用!因此还是需要有合法性检查。

2:开辟新空间,拷贝原数据,释放旧空间。

3:更新 _start,_finish,_endofstorage。

void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start) //有数据才拷贝{memcpy(tmp, _start, sz * sizeof(T));delete[] _start;}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}
}

注意:更新 _finish 的时候不能直接写:_start + size(),因为size() 的计算依赖于 _start 和 _finish,因为 _start 已经被修改了,因此不可以直接用size(),需要提前保存 size()。 

但是这样写真的没问题嘛?我们经过测试发现这样的代码会使程序崩掉的:

 

这是为啥呢?我们来看看图解:

 

string 维护的三个成员变量管理着堆区的空间,当我们需要扩容的时候,拷贝 vector 中原来的数据,因为我们用的是 memcpy 知识单纯的赋值,因此拷贝后的数据同样也是指向原先 vector 中的 string 指向的空间,在我们 delete 掉原空间之后,实际上新的 vector 中的 string 指向的空间已经被释放了!等函数调用结束,势必会出现二次析构的问题!

解决的办法很简单,直接 for 循环赋值就行了!

void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start) //有数据才拷贝{for (int i = 0; i < sz; i++)tmp[i] = _start[i];delete[] _start;}_start = tmp;_finish = _start + sz;_endofstorage = _start + n;}
}

对于内置类型,= 赋值会调用赋值运算符重载,这样就没问题啦! 

 7. 迭代器

维护三个迭代器变量 begin() 函数,end() 函数的实现就比较简单啦!

iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}

8. void push_back(const T& x) 

1:检查是否需要扩容。

2:插入数据。

3:更新 _finish。

void push_back(const T& x)
{if (_finish == _endofstorage) //扩容逻辑{size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapaciy);}*_finish = x;_finish++;
}

9. T& operator[](size_t pos) 

1:检查 pos 的合法性。

2:找到 pos 位置对应的迭代器,返回其解引用的值就 OK。可以这么写:*(_start + pos),定睛一看不就等于:_start[pos] 嘛!

T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}

10. iterator insert(iterator pos, const T& val)

1:检查 pos 合法性。

2:扩容逻辑的判断。

3:挪动数据。我们可以发现维护三个迭代器变量的好处就来了,在 string 的模拟实现中,我们移动数据的时候可能会发生死循环的问题,就是当 end == 0 的时候,减一之后变成 -1,因为 pos 是 size_t 类型的,end 会被整形提升,导致陷入死循环。但是使用迭代器之后完全没有这种问题!

于是你写出来了这样的代码:

void insert(iterator pos, const T& val)
{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapaciy);}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;
}

这样做真的没有问题吗!我们来看看下面的这组测试用例:

 

为什么头插一个 0 的时候会出现随机值,并且 0 还没有插入进去呢?这里有一个严重的问题:迭代器失效的问题,我们现在书写的 insert 函数,在扩容的时候就会发生迭代器失效的问题。

我们来分析出现这种情况的原因哈:当我们的 vector 已经有 4 个元素了,在插入一个元素就会扩容,一旦扩容就会开辟新的空间并且会拷贝原空间的数据,那么实参的 begin() 指向的空间已经被释放了,这就造成了迭代器失效的问题!

 应该怎么解决这个问题呢?我们在扩容之前保存 pos 相对于 _start 的偏移量即可。

我们现在解决了 insert() 函数内部迭代器失效的情况,那么如何解决外部迭代器失效的情况呢?

什么是外部迭代器失效?来看下面的例子:

想必你也知道原因了:形参是实参的拷贝,形参的改变不影响实参,即使扩容的时候我们在函数内部修改了形参 pos 的,实参依然是不会改变的!上面的代码中让 *it-- 就发生了内存的非法访问,这是不被允许的!

你可能一下就想到了一个解决办法:把 insert() 函数的参数改为引用不就行啦?但是当我们这样调用 insert() 函数的时候编译就无法通过啦:

insert(a.begin(), 0);  // begin() 函数是值返回,是一个临时对象具有常性不可以被 iterator& 接收。

insert(a.begin() + 3, 0) // 这是一个表达式的计算,计算结果也是一个临时对象,同样不能被 iterator& 的形参接收。

那你可能会说:我用const iterator& 来做形参,那你在函数内部就无法修改形参 pos 了,怎么解决迭代器失效的问题呢?

因此正确的解决办法是参考库函数, 给 insert() 函数加上返回值。

iterator insert(iterator& pos, const T& val)
{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){size_t offset = pos - _start;size_t newCapaciy = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapaciy);pos = _start + offset;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;return pos;
}

11.  iterator erase(iterator pos)

1:检查 pos 位置的合法性。

2:挪动元素。

就很简单哇!但是我们需要考虑的问题是 erase 是否有迭代器失效的问题呢?

看下面的代码,我们删除 4 这个元素之后呢,再去访问 it 迭代器不就是越界访问了嘛。

当你在VS中使用 std::vector 使用 it 迭代器会直接报错,VS 认为无论是否越界访问,使用删除的迭代器就是不正确的。

但是在 Linux 下使用 g++ 编译器均不会报错呢!我们看到不同的编译器对此的处理结果也是不相同的嘞!

为了使得C++代码兼容 g++ 编译器和 VS 的编译器,我们必须让 erase 有返回值,返回删除位置的下一个位置的迭代器,这样就能做到 VS 下访问不报错了!

补充:VS 库中 vector 迭代器的实现其实是经过封装的,并不是原生指针。可见实现 vector 的方式真的很多哇!

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

12. void pop_back()

1:注意有元素才能删除嘛。std::vector 是直接断言检查的!

2:其实也可以复用 erase 函数。

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

13. void resize(size_t n, const T& val = T()) 

实现的思路和 string 的 resize 差不多:

1:当 n < size() 直接修改 _finish 即可。

2:其余情况我们都可以调用 reserve 把空间开好,因为 reserve 的实现做了检查,不需要扩容的时候是不会扩容的!空间好了之后填充 val 就可以啦!

void resize(size_t n, const T& val = T())
{if (n < size()){_finish = _start + n;}else{reserve(n);while (_finish != _start + n){*_finish = val;++_finish;}}
}

14. void swap(vector<T>& v)

这是交换两个 vector 对象,很简单只需要交换 vector 的成员变量就行了!

void swap(vector<T>& v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endofstorage, v._endofstorage);
}

15. 拷贝构造 

类提供的默认拷贝构造会实现直接赋值的浅拷贝,导致析构的时候同一块堆区的空间会被释放两次。这就是经典的浅拷贝,因为我们的 vector 维护了堆区的数据,因此要实现类的深拷贝。

老老实实开空间拷贝数据。很简单的!

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

16. 赋值运算符重载 

传统写法很简单,这里就不写了。

我们的现代写法在 string 的模拟实现哪一节提到过。就是利用自定义类型函数传值调用会调用拷贝构造的特点,然后将拷贝构造出来的形参交换给自己,同时随着形参的销毁,形参右释放了原来的空间,简直就是一举两得!

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

17. vector(size_t n, const T& val = T()) 

这个构造函数直接调用 resize() 就可以啦!

vector(size_t n, const T& val = T())
{resize(n, val);
}

18. vector(InputIterator first, InpuIterator last) 

这个构造函数是使用一段迭代器区间来初始化 vector ,区间:[first, lasr),InpuIterator 是模板参数。

例如:

代码实现:

		template<class InputIterator>vector(InputIterator first, InputIterator last){while (first != last){push_back(*first);first++;}}

但是这么写的话,就会有一个问题:我们这样初始化 vector 就会报错:

vector<int> a(10, 1);

这是因为:参数 10 和 1 均会解析成 int 类型,从而构造函数走的是:迭代器初始化的版本。导致编译错误!为了解决这个问题,我们可以再提供一个构造函数:将 size_t 变成 int,这样就不会报错了。

vector(int n, const T& val = T())
{resize(n, val);
}

 

 

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

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

相关文章

MySQL 三大日志(bin log、redo log、undo log)

redo log redo log (重做日志) 是 InnoDB 存储引擎独有的&#xff0c;它让 MySQL有了崩溃恢复的能力&#xff0c;是事务中实现 持久化的重要操作 比如 MySQL 实例宕机了&#xff0c;重启时&#xff0c;InnoDB 存储引擎会使用 redo log 恢复数据&#xff0c;保证数据的持久性与…

设计模式——七大原则详解

目录 设计模式单一职责原则应用实例注意事项和细节 接口隔离原则应用实例 依赖倒转&#xff08;倒置&#xff09;原则基本介绍实例代码依赖关系传递的三种方式注意事项和细节 里氏替换原则基本介绍实例代码 开闭原则基本介绍实例代码 迪米特法则基本介绍实例代码注意事项和细节…

golang笔记17--编译调试go源码

golang笔记17--编译调试go源码 前置条件编译源码在 fmt 包中加自定义函数说明 当前go语言越来越流行了&#xff0c;各大厂商都有加大go工程师的需求&#xff0c;作为go语言的学习者&#xff0c;我们除了要了解如何使用go语言外&#xff0c;也有必要了解一下如何编译、调试go源码…

解决XXLJOB重复执行问题--Redis加锁+注解+AOP

基于Redis加锁注解AOP解决JOB重复执行问题 现象解决方案自定义注解定义AOP策略redis 加锁实践 现象 线上xxljob有时候会遇到同一个任务在调度的时候重复执行&#xff0c;如下图&#xff1a; 线上JOB服务运行了2个实例&#xff0c;有时候会重复调度到同一个实例&#xff0c;有…

交换机端口灯常亮 端口up状态 服务器设置ip交换机获取不到服务器网卡mac地址 不能通信

环境: 深信服防火墙 8.0.75 AF-2000-FH2130B-SC S6520X-24ST-SI交换机 version 7.1.070, Release 6530P02 问题描述: 交换机一个vlan下有3台服务器,连接端口2、3、4,2和3连接的服务器正常,交换机3端口灯常亮 端口up状态 服务器自动获取不了地址,改为手动设置ip后,交…

Xubuntu16.04系统中安装create_ap创建无线AP

1.背景说明 在Xubuntu16.04系统的设备上安装无线WIFI模块后&#xff0c;想通过设备自身的无线AP&#xff0c;进行和外部设备的连接&#xff0c;需要安装create_ap软件&#xff0c;并设置无线AP的名称和密码&#xff0c;并设置为开机自启动。 create_ap是一个用于在Linux系统上创…

开源贡献难吗?

本文整理自字节跳动 Flink SQL 技术负责人李本超在 CommunityOverCode Asia 2023 上的 Keynote 演讲&#xff0c;李本超根据自己在开源社区的贡献经历&#xff0c;基于他在贡献开源社区过程中的一些小故事和思考&#xff0c;如何克服困难&#xff0c;在开源社区取得突破&#x…

BetaFlight飞控AOCODAF435V2MPU6500固件编译

BetaFlight飞控AOCODAF435V2MPU6500固件编译 1. 源由2. 准备2.1 板子2.2 代码2.3 工具 3. 配置修改4. 编译4.1 获取代码4.2 获取配置4.3 编译固件4.4 DFU烧录4.5 版本核对 5. 总结 1. 源由 刚拿到一块Aocoda F405V2 (MPU6500) AT32F435飞控板(替换主控芯片)。 Aocoda-RC F40…

金融机器学习方法:K-均值算法

目录 1.算法介绍 2.算法原理 3.python实现示例 1.算法介绍 K均值聚类算法是机器学习和数据分析中常用的无监督学习方法之一&#xff0c;主要用于数据的分类。它的目标是将数据划分为几个独特的、互不重叠的子集或“集群”&#xff0c;以使得同一集群内的数据点彼此相似&…

tomcat 服务器

tomcat 服务器 tomcat: 是一个开源的web应用服务器。区别nginx&#xff0c;nginx主要处理静态页面&#xff0c;那么动态请求&#xff08;连接数据库&#xff0c;动态页面&#xff09;并不是nginx的长处&#xff0c;动态的请求会交给tomcat进行处理。 nginx-----转发动态请求-…

【5G PHY】5G SS/PBCH块介绍(一)

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…

使用crul库和R语言的下载器程序

以下是一个使用crul库和R语言的下载器程序&#xff0c;用于从下载音频。此程序使用了jshk.com.cn/get_proxy的代码。 // 导入必要的库 import ("fmt""github.com/cjlapa/crul""io""net/http""net/url""os" )// 主…

在 Python 中使用 Pillow 进行图像处理【3/4】

第三部分 一、腐蚀和膨胀 您可以查看名为 的图像文件dot_and_hole.jpg&#xff0c;您可以从本教程链接的存储库中下载该文件&#xff1a; 该二值图像的左侧显示黑色背景上的白点&#xff0c;而右侧显示纯白色部分中的黑洞。 侵蚀是从图像边界去除白色像素的过程。您可以通过使用…

运算符重载的三种实现方法

一、重载为一般函数 格式&#xff1a;返回类型 operator 运算符(参数列表) struct Complex{//定义一个复数结构&#xff1a;包括实部与虚部两部分 double real;//实部 double imag;//虚部 }; Complex operator(Complex c1,Complex c2){//对加法运算的重载&#xff1a;将运算符…

vue重修之路由【上】

文章目录 单页应用程序: SPA - Single Page Application路由简介Vue Reouter简介VueRouter的使用&#xff08;52&#xff09;组件的存放目录问题组件分类存放目录 路由的封装抽离 单页应用程序: SPA - Single Page Application 单页面应用(SPA): 所有功能在 一个html页面 上 单…

python调用astra进行人脸检测(使用CascadeClassifier)

1、简述 方法&#xff1a;使用opecv中&#xff0c;CascadeClassifier 级联分类器实现人脸检测&#xff0c;CascadeClassifier就是opencv下objdetect模块中用来做目标检测的级联分类器的一个类&#xff0c;它可以帮助我们检测例如车牌、眼睛、人脸等物体。它的大概原理就是判别…

JS类的继承和实现原理详解

一&#xff1a;前言 各位小伙伴在日常开发中&#xff0c;相信一定遇到过Class这种写法。这代表在JS中创建了一个类&#xff0c;并且可以通过这个类去 new 出一个新的对象。其实在JS中&#xff0c;这个类和java中的类是没有区别的&#xff0c;同样具有属性&#xff0c;方法&…

前端多媒体处理工具——ffmpeg的使用

写在前面 在前端领域&#xff0c;FFmpeg 是一个非常有用的工具&#xff0c;它提供了多种媒体格式的封装和解封装&#xff0c;包括多种音视频编码、多种协议的流媒体、多种色彩格式转换、多种采样率转换、多种码率切换等。可以在多种操作系统安装使用。 安装 下载FFmpeg 在网…

深入探讨 Golang 中的追加操作

通过实际示例探索 Golang 中的追加操作 简介 在 Golang 编程领域&#xff0c;append 操作是一种多才多艺的工具&#xff0c;使开发人员能够动态扩展切片、数组、文件和字符串。在这篇正式的博客文章中&#xff0c;我们将踏上一段旅程&#xff0c;深入探讨在 Golang 中进行追加…

【VSCode】解决Open in browser无效

问题描述&#xff1a; 在VSCode中无论是点击右键&#xff0c;选择在默认浏览器中打开&#xff0c;还是按快捷键alt b都没有反应。 解决办法&#xff1a; 右击文件 --> 在文件资源管理器中显示 右击文件&#xff0c;选择属性 点击更改 选择用默认浏览器打开 最后 此时…