【C++STL详解(四)】--------vector的模拟实现

前言

还是那句话:模拟实现不是为了比库里面更好,而是去学习它的一些底层,能够让自己有更深的了解,为了更好的熟用!

vector底层图

由上图可以看出,它的底层实际上就是原生的指针T*,只不过会存在三个标记不同的位置,start标记的是有效数据的开始,finish标志的有效数据的结尾,end_of_storage标记的是整个空间的最尾部!

一、接口概述

namespace Ve
{template<class T>class vector{public:// vector的迭代器是一个原生指针,所以这里统一叫iteratortypedef T* iterator;typedef const T* const_iterator;// 默认成员函数vector();vector(size_t n, const T& value = T());template<class InputIterator>vector(InputIterator first, InputIterator last);vector(const vector<T>& v);vector<T>& operator= (vector<T> v);~vector();// capacitysize_t size() const;size_t capacity() const;void reserve(size_t n);void resize(size_t n, const T& value = T());bool empty();//迭代器与遍历iterator begin()iterator end()const_iterator begin()constconst_iterator end() constT& operator[](size_t pos);const T& operator[](size_t pos)const;///修改/void push_back(const T& x);void pop_back();void swap(vector<T>& v);void insert(iterator pos, const T& x);void erase(iterator pos);private:iterator _start; // 指向数据块的开始iterator _finish; // 指向有效数据的尾iterator _endOfStorage; // 指向存储容量的尾};
}

注意:这里采用命名空间的原因的string的模拟实现是类似的,同时也可以看到vector实际上是一个类模板,T可以是任意类型!

二、各接口具体实现

Ⅰ、默认成员函数

1.、构造函数

  • 无参构造
vector():_start(nullptr),_finish(nullptr),_endOfStorage(nullptr)
{}

  • 特定值初始化
vector(size_t n, const T& value = T())
{reserve(n);//这里为了防止容量不够而进行的扩容操作,即使小了也不影响for (int i = 0; i < n; i++){push_back(value);}
}

这里需要注意一个参数:const T& value = T(),这样写是因为T可以是任意类型,也就是说可能会是string这样的自定义类型,这时T()就相当于string(),一个匿名对象构造,那么就会去调用对应自定义类型的默认成员函数完成构造初始化工作!

而对于内置类型int,double等等,它们实际也存在着析构、默认构造等默认成员函数!这里可以证明一下,看代码

int main()
{int i=1;int j=int();int k=int(2);return 0;
}
k=?   j=?

结果:

  • 迭代器区间初始化
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}

操作十分的简单,就是一个尾插操作即可!同时需要注意类成员函数也可以是函数模板!

构造函数这里还有一点注意的地方:迭代器区间初始化和特定值初始化

vector(size_t n, const T& value = T())
{reserve(n);for (int i = 0; i < n; i++){push_back(value);}
}//类成员函数也可以是函数模板
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}

上面这两段代码在vector<int> v(10,1)情况下会发生冲突,因为两个都可以匹配,它会匹配到下面的那个模板函数,因为更加的符合,所以在底层会将第一段代码在写一个int的重载函数

vector(int n, const T& val = T())
{reserve(n);for (int i = 0; i < n; i++){push_back(val);}
}

这里就能对vector<int> v(10,1)进行很好的处理!

2、拷贝构造

vector(const vector<T>& v){reserve(v.capacity());for (auto& a : v)//这里加个引用,就是因为T的类型未知,可能是string等其他自定义类型,最好加上{push_back(a);}
}

同样,为了防止当前对象容量不够,所以最好先弄一个和拷贝对象一样大的空间,然后直接尾插即可!

3、赋值重载

这里直接采用和string类一样的现代写法,复用拷贝构造函数!!再交换两者即可

vector<T>& operator= (vector<T> v)//引用返回支持连续赋值
{swap(v);return *this;
}

4、析构函数

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

Ⅱ、容量

1.size()

从上述的vector底层图可以看出,size实际上就是两指针相减,即finish-start!

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

2.capacity()

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

3.reserve()

再回顾一下规则:如果要调整的空间小于原空间那就什么都不做,否则,就扩到和要调整空间一样大!

细节点:

扩容实际是重新开一个空间,再将原数据拷贝过去,释放原来的空间!那么当前的三个成员变量都得更新,关键_finish指针该怎么去更新?按底层图来看应该是start+当前有效的数据个数,所以应该要先去保存原来的有效数据个数!

②数据的拷贝问题,我们会使用memcpy,但是呢实际上memcpy只是简单的值拷贝,但是对于string这样的需要深拷贝的类型来说必定会出现问题!!所以这里的解决方案也很简单,使用for循环将原数据一个一个放到新空间即可!

基于上述规则和细节,具体实现如下:

void reserve(size_t n)
{if (n > capacity()){T* tmp = new T[n];//临时空间size_t old_size = size();//这里要保存原来的值,因为后面_start 和 _finish改变空间了,那么原来size就会消失!//memcpy(tmp, _start, old_size * sizeof(T));//值得注意的是,这里memcpy会存在一个问题,就是它是按照字节去拷贝的,也就是值拷贝,那对于string这样的需要深拷贝的类型必定出错!//所以可以改成循环的方式for (int i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_endOfStorage = tmp+n;}
}

这一操作后,一定要注意迭代器问题,要使用需更新!!!

4.resize()

再来回顾规则:如果小于当前的size(),那就缩到与目标size()一样大,反之就扩大!如果n不仅仅大于原来的size(),还大于当前的capacity(),那就要考虑扩容操作了!

void resize(size_t n, const T& value = T())
{if(n > size()){reserve(n);//保险起见最好扩容//直接尾插数据即可while (_finish < _start + n){(*_finish) = value;_finish++;}}//小于直接改变finish的指向即可!else{_finish = _start + n;}
}

可以看到一点:reserve只负责开空间,resize()负责开空间+初始化

5.empty()

bool empty()
{return _start == _finish;
}

Ⅲ、迭代器与遍历

begin+end

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

这里一个是专门为const对象提供的const迭代器!

这里注意个细节问题:

这里都是传值返回,返回的是临时对象,具有常性,可读不可写,意味着不能begin()++,++要改变的返回的那个临时对象,由于常性的特点只能begin()+1;

operator【】

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

同样也为const对象设置了一个!

Ⅳ、增删查改操作

1.insert()

在pos位置前进行插入操作。首先最需要判断的就是位置的合法性,然后就是容量问题,当finish=endofstorage的时候,那就需要进行扩容操作了!紧接着就是向后挪动数据,结束条件就是把pos位置上的值给挪开即可!

细节问题:扩容前应该先保存pos所在的位置,扩容后在更新即可!

上面仅仅是实现当个数据的插入,当然还可以一次插入多个元素,或者一个迭代区间,原理类似

void insert(iterator pos, const T& x)
{assert(pos >= _start);assert(pos <= _finish);if (_finish == _endOfStorage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容了pos要更新pos = _start + len;}iterator it = _finish - 1;while (it >= pos){*(it + 1) = (*it);--it;}*pos = x;_finish++;}

注意:在此之后使用迭代器也是要更新的!

2.erase()

删除pos位置的值。实际上就是将后面的元素向前挪动去覆盖pos位置的值即可。注意位置的合法性!

代码如下:

void erase(iterator pos)
{assert(pos >= _start);assert(pos <= _finish);iterator it = pos + 1;while (it < _finish){*(it - 1) = (*it);it++;}--_finish;
}

3.push_back()

直接复用insert即可。

void push_back(const T& x)
{insert(_finish, x);
}

4.pop_back()

finish指针向前挪动即可!即不把最后一个元素看在眼里!

void pop_back()
{assert(!empty());--_finish;
}

5.swap()

这里的swap实际上就是引用全局的swap进行交换!

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

今天的分享就到这里。感谢你的观看!

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

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

相关文章

可视化大屏也在卷组件化,组件绝对是效率利器呀。

组件化设计在B端上应用十分普遍&#xff0c;其实可视化大屏组件更为规范&#xff0c;本期分享组件化设计的好处&#xff0c;至于组件源文件如何获取&#xff0c;大家都懂的。 组件化设计对可视化大屏设计有以下几个方面的帮助&#xff1a; 提高可重用性&#xff1a; 组件化设…

SpringBoot指标监控

一.SpringBoot指标监控_添加Actuator功能 Spring Boot Actuator可以帮助程序员监控和管理SpringBoot应用&#xff0c;比如健康检查、内存使用情况统计、线程使用情况统计等。我 们在SpringBoot项目中添加Actuator功能&#xff0c;即可使用Actuator监控 项目&#xff0c;用法如…

启明云端2.4寸屏+ESP32-S3+小型智能调速电动家用除草机案例 触控三档调速,能显示电压故障码

今天给大家分享个启明云端2.4寸屏ESP32-S3小型智能调速电动家用除草机案例&#xff0c;国外有草坪文化&#xff0c;这个机器能智能触控三档调速&#xff0c;带屏能显示电压故障码&#xff0c;数显档位&#xff08;3档最大&#xff09;&#xff0c;触控屏&#xff0c;长按3秒就能…

C#技巧之窗体去鼠标化

简介 在窗体程序中不用鼠标&#xff0c;直接使用键盘完成想要的操作。 实现的方法有两种&#xff0c;一种是使用键盘上的Tab键使控件获得焦点&#xff0c;然后用enter键触发该控件上的事件&#xff08;一般为click事件&#xff09;。另一种是&#xff0c;为控件添加快捷键&am…

附录6-1 黑马优购项目-组件与过滤器

目录 1 过滤器-格式化价格 2 组件-搜索框 3 组件-数量框 4 组件-商品概况 4.1 格式化价格 4.2 选择性使用勾选框和数量框 4.3 源码 1 过滤器-格式化价格 这个项目中仅用到格式化价格这一种过滤器。过滤器文件位置为store/filter.wxs 文件内容是这样的&#xf…

APT预警攻击平台截获Nday

APT预警攻击平台截获Nday 2024年4月26日 设备漏洞【漏洞利用】H3C Magic R100任意代码执行漏洞(CVE-2022-34598) 0000 &#xff1a; 0010 &#xff1a; 0020 &#xff1a; 0030 &#xff1a; 0040 &#xff1a; 0050 &#xff1a; 0060 &#xff1a; 0070 &#xff1a;6F 72…

MySQL基础学习(待整理)

MySQL 简介 学习路径 MySQL 安装 卸载预安装的mariadb rpm -qa | grep mariadb rpm -e --nodeps mariadb-libs安装网络工具 yum -y install net-tools yum -y install libaio下载rpm-bundle.tar安装包&#xff0c;并解压&#xff0c;使用rpm进行安装 rpm -ivh \ mysql-communi…

【热闻速递】Google 裁撤 Python研发团队

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 【&#x1f525;热闻速递】Google 裁撤 Python研发团队引入研究结论 【&#x1f5…

Liunx发布tomcat项目

Liunx在Tomcat发布JavaWeb项目 1.问题2.下载JDK3.下载Tomcat4.Tomcat本地JavaWeb项目打war包、解压、发布5.重启Tomcat,查看项目 1.问题 1.JDK 与 Tomcat 版本需匹配&#xff0c;否则页面不能正确显示 报错相关&#xff1a;Caused by: java.lang.ClassNotFoundException: java…

通过Servlet和JSP,结合session和application实现简单网络聊天室(文末附源码)

目录 一.成品效果 二.代码部分 chat.jsp ChatServlet 一.成品效果 在启动成功后&#xff0c;我们就可以在任意俩个浏览器页面中相互发消息&#xff0c;如图所示左边屏幕使用的是Edge浏览器&#xff0c;右图使用的是火狐浏览器。当然笔者这里只是简单实现最基本的一些功能&…

企业如何通过定制AI智能名片B2B2C商城系统革新营销手段

在日新月异的商业环境中&#xff0c;企业想要立足并蓬勃发展&#xff0c;就必须紧跟时代的步伐&#xff0c;不断革新营销手段。而定制开发AI智能名片B2B2C商城系统正是企业实现这一目标的重要武器。接下来&#xff0c;我们将深入探讨企业如何通过这一系统&#xff0c;在与客户交…

阿里云企业邮箱API的使用方法?调用限制?

阿里云企业邮箱API性能如何优化&#xff1f;配置邮箱API的优势&#xff1f; 阿里云企业邮箱以其稳定、高效和安全的特点&#xff0c;受到了众多企业的青睐。而阿里云企业邮箱API的开放&#xff0c;更是为企业提供了更加灵活、便捷的管理和操作方式。下面&#xff0c;我AokSend…