【C++】vector模拟实现


🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++  🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


文章目录

  • vector中的成员变量
  • 默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 赋值运算符重载函数
  • 迭代器函数
    • begin和end函数
  • 容量大小相关函数
    • capacity函数
    • size函数
    • empty函数
    • reserve函数
    • resize函数
  • 修改操作相关函数
    • push_back函数
    • pop_back函数
    • swap函数
    • insert函数
    • erase函数
  • 元素访问函数
    • []运算符重载

vector中的成员变量

为了防止与标准库当中的vector产生命名冲突,模拟实现时需放在自己的命名空间当中。

SGI版本下库中的vector成员变量:

typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type* iterator;
typedef const value_type* const_iterator;iterator start;
iterator finish;
iterator end_of_storage;

从库中的代码可以看出iterator是T类型的指针,但是VS中的不是原生指针,所以不要只认为iterator就是指针。

start指向的是容器中第一个元素;

finish指向的就是最后一个元素的下一个位置;

end_of_storage指向的是整个数组的下一个位置。

image-20230629162815227

typeid().name()函数能看出指定对象类型。

image-20230629163530917

所以当我们模拟实现vector时,可以将成员变量设置为模板类型的指针_start_finishend_of_storage

模拟实现vector中类的成员变量:

namespace Niu {template<class T>//类模板参数class vector {public:typedef T* iterator;typedef const T* const_iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;};
}

默认成员函数

构造函数

无参构造函数

vector支持一个无参的构造函数,将构造对象的三个成员变量都设置为空指针。

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

构造生成n个T类型的val值

需要先使用后面要实现的reserve函数提前开空间,以及push_back函数进行尾插。

//生成n个T类型的的val值
vector(size_t n, const T& val = T()): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++){push_back(val);//尾插}
}

这里第二个参数使用了匿名对象,初始化的时候不能给0,因为T是一个模板参数,可以是自定义类型string也可以是其他类型,所以要用匿名对象。而且const T& val = T()使用引用接收,我们之前说匿名对象的生命周期只在当前行,那是因为没有去使用匿名对象,我们使用const引用可以延长匿名对象的生命周期,相当于就是给了匿名对象一个名字,可以将匿名对象的生命周期延长到对象a销毁。

因为匿名对象和临时对象都具有常性,所以必须使用const。

class A {
public:void Print() const{cout << " A" << endl;}
};int main()
{//A& a = A();//error,匿名对象具有常性const A& a = A();//能够继续使用对象aa.Print();//输出A
}

该构造函数最好用reserve函数一次性开辟好空间,避免调用push_back函数时需要增容多次,导致效率降低。

为了避免和迭代器构造函数的使用造成冲突,这里再重载一个 int 类型参数的函数(只是将第一个参数类型改为int):

//生成n个T类型的的val值
vector(int n, const T& val = T()): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++){push_back(val);//尾插}
}

如果不重载,使用vector<int> v(1,2)时会去调用迭代器区间构造函数定义对象,函数体内需要解引用造成程序崩溃。

使用迭代器区间进行对象的构造

迭代器区间可以是其他容器的迭代器区间,也就是该函数接收到的迭代器的类型是不确定的,所以需要一个函数模板,在函数体内将该迭代器区间的数据尾插到容器当中。

//使用迭代器区间构造
template<class InputIterator>//函数模板
vector(InputIterator first, InputIterator last):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{//将迭代器区间在[first,last)的数据尾插到容器中while (first != last){push_back(*first);first++;}
}

析构函数

对容器进行析构时先判断该容器是否为空容器,若为空容器,则无需进行析构操作,若不为空,则先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针。

~vector()
{if (_start)//判断空间是否为空{delete[] _start; //释放容器存储数据的空间_start = nullptr;//置空_finish = nullptr;//置空_end_of_storage = nullptr;//置空}
}

拷贝构造函数

拷贝构造函数涉及深拷贝问题:

传统写法:

先开辟一块与该容器大小相同的空间,然后将要拷贝容器当中的数据拷贝到新容器中,最后更新_finish_end_of_storage的值。

vector(const vector<T>& v):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{_start = new T[v.capacity()];//开辟要拷贝数组的容量大小的空间for (size_t i = 0; i < v.size(); i++){_start[i] = v[i];//将容器v中的数据赋值给新容器中}_finish = _start + v.size();//指向容器中有效数据的下一个位置_end_of_storage = _start + v.capacity();//指向结尾位置的下一个位置
}

注意:将容器中的数据拷贝过来的时候不能使用memcpy,因为memcpy是按照字节进行拷贝的,当vector容器中存储的是内置类型的时候使用memcpy函数进行复制不会产生错误,但是当vector容器中存储的是string这种自定义类型时,如果使用memcpy函数进行拷贝将会导致程序出现错误。

image-20230630162643835

因为memcpy将已有的vector容器中的string类对象中的指针拷贝到新的vector容器的string中,导致两个vector容器中string指向的是同一个字符串。

for (size_t i = 0; i < v.size(); i++)
{_start[i] = v[i];//将容器v中的数据赋值给新容器中
}

而上面的拷贝构造代码中使用=将已有vector容器中的值赋值到新的容器中,即便是当遇到自定义类型时,也可以通过调用自定义类型对应的赋值运算符重载函数做到深拷贝。

image-20230630163345276

如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

现代写法:

通过遍历将已有容器中的数据尾插到新的容器中。在使用范围for对已有容器进行遍历的时候,e是容器中数据的拷贝,如果是自定义类型会自动调用拷贝构造完成深拷贝,然后将e插入到新容器中。

vector(const vector<T>& v):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{reserve(v.capacity());//开空间for (auto e : v)//范围for遍历{push_back(e);//尾插到新容器}
}

我们也可以使用迭代器区间构造函数:

vector(const vector<T>& v)
{vector<T> tmp(v.begin(),v.end());swap(tmp);//交换this指针
}

小提示:类里面写可以不加模板参数,但是还是建议加上,虽然库里面没加。

vector(const vector& v)//拷贝构造
vector& operator=(const vector& v)//赋值运算符重载

赋值运算符重载函数

赋值运算符重载也涉及到深浅拷贝。

传统写法:

先检查是否是自己给自己赋值,然后根据右操作数的容量开空间,然后再通过for循环进行赋值,注意这里也不能使用memcpy函数进行拷贝,最后更新_finish_end_of_storage,支持连续赋值,最后返回*this。引用返回减少拷贝。

vector<T>& operator=(const vector<T>& v)
{if (this != &v)//检查是否自己给自己赋值{delete[] _start;//释放之前的空间_start = new T[v.capacity()];for (size_t i = 0; i < v.size(); i++){_start[i] = v[i];//赋值}//更新_finish和_end_of_storage_finish = _start + v.size();_end_of_storage = _start + v.capacity();}return *this;//支持连续赋值
}

现代写法:

现代写法中函数传参没有使用引用传参,而是使用传值传参,调用拷贝构造函数,然后将拷贝构造形成的对象和要赋值的对象通过swap函数进行交换,此时就已经完成了赋值操作。拷贝构造完成了深拷贝工作,所以这里我们不需要管。

vector<T>& operator=(vector<T> v)//编译器自动调用其拷贝构造函数
{swap(v);//交换这两个对象return *this;//支持连续赋值
}

迭代器函数

begin和end函数

begin函数返回指向容器中首个元素的指针,end函数返回指向容器中有效数据个数下一个位置的指针。

const函数和非const函数构成重载,以满足不同对象的需求。

//迭代器函数begin,返回指向第一个元素的指针
iterator begin()
{return _start;
}
const_iterator begin() const
{return _start;
}
//迭代器函数end,返回指向最后一个元素下一个位置的指针
iterator end()
{return _finish;
}
const_iterator end() const
{return _finish;
}

容量大小相关函数

capacity函数

capacity函数能够得到该容器当前所能存储的最多元素个数。

_end_of_storage - _start;指针减指针得到的是两者之间的元素个数,我们可以认为区间就是左闭右开的。

使用const常函数以满足当对象为const对象时的调用。

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

size函数

size函数能够得到当前容器中有效数据个数。

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

empty函数

判断容器是否为空

bool empty()
{return _start == _finish;//指向同一个位置即为空
}

reserve函数

扩容到指定大小。

reserve规则:
1、当n大于当前的capacity时,将capacity扩大到n。
2、当n小于当前的capacity时,什么也不做。

先判断所给n是否大于当前capacity,如果大于先记录当前有效数据个数size(),然后开辟能够存储n个T类型的空间,将原容器当中的数据拷贝到新空间中,再将原空间释放,并将新开辟的空间赋值给该容器,并且更新更新_finish_end_of_storage

void reserve(size_t n) 
{if (n > capacity()){size_t sz = size();//记录有效数据个数T* tmp = new T[n];//开空间//判断是否为空if (_start){//如果不是空,将数据拷贝到新空间for (size_t i = 0; i < sz; i++){tmp[i] = _start[i];}//拷贝完成之后销毁之前的空间delete[] _start;}//将tmp 赋值给 _start;_start = tmp;//更新_finish和_end_of_storage_finish = _start + sz;_end_of_storage = _start + n;}
}

特别注意的是在开空间赋值之前需要提前记录当前容器中有效数据的个数,并且赋值时也是深拷贝。

因为size的大小是根据_finish_start计算出来的,当我们开空间赋值之后 _start = tmp就已经改变了_start,那当我们要去更新_finish时,size的大小已经不是原来的数值了。

同样对于vector当中如果是自定义类型string,如果是浅拷贝那么delete[] _start;将会调用string的析构函数,将string指向的字符串销毁,所以在for循环中赋值运算符重载完成深拷贝。

resize函数

扩容并且初始化

resize规则:
1、当n大于当前的size时,将size扩大到n,初始化为val,若val未给出,使用缺省值(默认构造函数所构造出来的值)。
2、当n小于当前的size时,将size缩小到n。

先判断n是否小于容器当前的size,如果小于,可以通过改变_finish,将容器的size缩小到n;如果n大于size,先判断该容器是否需要扩容,然后再将扩大的空间初始化为val。

在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T()即可。

void resize(size_t n,const T& val = T())
{if (n < size())//当n小于size时{_finish = _start + n;//将size缩小到n}else{if (n > capacity()){reserve(n);//扩容}while (_finish < _start + n)//循环并且初始化{*_finish = val;//赋值_finish++;}}
}

这里要注意迭代器失效的问题,最经典的迭代器失效就是扩容造成的野指针问题导致的。

image-20230630232721532

当扩容之后, _start _finish_end_of_storage都指向了新的位置,而指定的pos位置却没有更新,将会导致pos变为野指针。

修改操作相关函数

push_back函数

尾插

注意尾插时需要检查是否需要扩容。

void push_back(const T& val)
{//判断容器是否还有剩余空间if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍reserve(newcapacity); }*_finish = val;//插入数据元素_finish++;	//向后移动一个位置
}

pop_back函数

尾删

void pop_back()
{//判断是否为空assert(!empty());_finish--;
}

swap函数

swap函数用于交换两个容器中的数据,实现时可以调用库当中的swap函数进行交换。

void swap(vector<T>& v)
{//调用库中的函数进行交换std::swap(v._start, _start);std::swap(v._finish, _finish);std::swap(v._end_of_storage, _end_of_storage);
}

如果在swap函数前加上::(作用域限定符),就是告诉编译器优先在全局范围查找swap函数,如果没有加编译器会认为调用的就是正在实现的swap函数(就近原则)。

void swap(vector<T>& v)
{//调用库中的函数进行交换::swap(v._start, _start);::swap(v._finish, _finish);::swap(v._end_of_storage, _end_of_storage);
}

insert函数

insert函数是在指定的迭代器位置插入数据。需要先判断位置是否合法,然后移动数据,最后再插入。

iterator insert(iterator pos, const T& val)
{//检查合法性assert(pos <= _finish);assert(pos >= _start);//判断是否需要扩容if (_finish == _end_of_storage){size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍reserve(newcapacity);}//挪动数据iterator end = _finish;while (end > pos){*end = *(end - 1);end--;}*pos = val;//指定位置插入数据_finish++;//更新_finish位置return pos;
}

但是上面的代码存在问题,因为当需要扩容的时候存在迭代器失效问题,pos迭代器没有更新,而其他的指针都更新了,所以在扩容之前我们需要记录一下pos的位置,完善之后代码如下:

iterator insert(iterator pos, const T& val)
{assert(pos <= _finish);assert(pos >= _start);//判断是否需要扩容if (_finish == _end_of_storage){size_t len = pos - _start;//记录pos的相对位置size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍reserve(newcapacity);pos = _start + len;//更新pos位置}iterator end = _finish;while (end > pos){*end = *(end - 1);end--;}*pos = val;_finish++;return pos;
}

我们在函数体内把pos更新了,但是因为是传值传参所以在函数外面并没有改变pos指针,而且传递pos时不能使用引用传参,当insert第一个参数是begin()时,因为 begin() 是传值返回,是使用临时变量来返回的,而临时变量具有常性,不可被改变,因此如果在 insert 函数中的参数 pos 使用传引用传参,就会造成权限放大的问题,我们可以使用返回值的方法解决这个问题,返回的是迭代器,指向新插入元素的位置。

insert 函数设置返回值,这样做是为了在调用完 insert 函数后,能够重新对迭代器进行赋值,避免迭代器失效。

当迭代器每次使用完之后要重新赋值才能使用,不然很容易出现迭代器失效问题。

erase函数

删除指定迭代器位置的数据。

iterator erase(iterator pos)
{//判断合法性assert(pos >= _start);assert(pos < _finish);iterator begin = pos + 1;//挪动数据while (begin != _finish){*(begin - 1) = *begin;begin++;}--_finish;//更新_finishreturn pos;//返回pos
}

erase函数会存在迭代器失效问题,所以我们认为erase之后,pos失效,行为结果未定义(跟具体编译器实现有关)。

erase删除 pos 位置元素后, pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不会失效,但是要考虑到一种特殊情况:如果 pos指向的是最后一个元素,删完之后 pos 指向的位置刚好是_end_of_storage指向的位置,而该位置是没有元素的,那此时 pos 就失效了。所以在库中认为使用erase后,迭代器就失效了。

erase 函数设置返回值,是为了在调用完erase 函数后,能够重新对迭代器进行赋值,避免迭代器失效。

元素访问函数

[]运算符重载

重载[]运算符能够让该容器通过[]+下标的方式访问元素。

需要重载一个const函数,const对象只能够访问对象而不能修改,所以返回值也要改为const T&,使用引用是为了减少拷贝。

T& operator[](size_t pos)
{assert(pos < size());//检测下标是否合法return _start[pos];//返回当前下标元素
}//重载一个const函数
const T& operator[](size_t pos) const
{assert(pos < size());//检测下标是否合法return _start[pos];//返回当前下标元素
}

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

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

相关文章

比亚迪车载Android开发岗三面经历~

前言 首先&#xff0c;我想说一下我为什么会想去比亚迪这样的车企做车载Android开发。我是一名有5年经验的Android开发工程师&#xff0c;之前一直在互联网软件公司工作&#xff0c;做过移动端App和IoT产品的开发。但我一直对汽车领域很感兴趣&#xff0c;也希望自己的技术能应…

(并查集) 685. 冗余连接 II ——【Leetcode每日一题】

并查集基础 并查集&#xff08;Union-find Sets&#xff09;是一种非常精巧而实用的数据结构&#xff0c;它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的Kruskal算法和求最近公共祖先&#xff08;LCA&#xff09;等。 并查集的基本操作主…

【python】jupyter notebook的快捷键

Jupyter Notebook 提供了许多快捷键以方便操作。这些快捷键分为两类&#xff1a;命令模式快捷键和编辑模式快捷键。 命令模式快捷键&#xff1a; 在命令模式下&#xff0c;细胞边框是蓝色的。这些快捷键主要用于操作单元格。 Enter&#xff1a;进入编辑模式Shift Enter&…

如何用Airtest脚本无线连接Android设备?

1. 前言 之前我们已经详细介绍过如何用AirtestIDE无线连接Android设备&#xff1a; 手把手教你用AirtestIDE无线连接手机&#xff01; &#xff0c;它的关键点在于&#xff0c;需要先 adb connect 一次&#xff0c;才能点击 connect 按钮无线连接上该设备&#xff1a; 但是有很…

当量因子法、InVEST、SolVES模型等多技术融合在生态系统服务功能社会价值评估中的应用

第一章 理论基础与研究热点分析 1. 生态系统服务与生态系统服务价值介绍 ​ 2. 生态系统服务价值研究方法 3. 生态系统服务价值研究热点 Citespace文献可视化分析 VOSviewer文献可视化分析 第二章 空间数据来源及预处理 1. 空间数据简介 2. ArcGIS Pro数据采集与分析 数…

Mybaits-Plus学习5 代码生成器

引入代码生成器依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.1</version> </dependency> <dependency><groupId>org.freemarker</groupId…

初级保育员专业知识生活管理考试题库及答案

​本题库是根据最新考试大纲要求&#xff0c;结合近年来考试真题的重难点进行汇编整理组成的全真模拟试题&#xff0c;考生们可以进行专项训练&#xff0c;查漏补缺巩固知识点。本题库对热点考题和重难点题目都进行了仔细的整理和编辑&#xff0c;相信考生在经过了针对性的刷题…

前端Vue自定义手机号文本格式化组件手机号码文本转星号

前端Vue自定义手机号文本格式化组件, 下载完整代码请访问uni-app插件市场地址&#xff1a;https://ext.dcloud.net.cn/plugin?id13231 效果图如下: # cc-format-phone #### 使用方法 使用方法 <!-- phone:手机号 isStar: 是否转星号 --> <cc-format-phone :phon…

springboot项目外卖管理 day08-缓存优化

文章目录 一、缓存优化问题说明环境搭建导入maven坐标配置yml文件设置序列化器&#xff0c;编写配置类 缓存短信验证码缓存菜品数据实现思路 SpringCacheSpring Cache介绍Spring Cache常用注解Spring Cache使用方式 缓存套餐数据实现思路 一、缓存优化 问题说明 环境搭建 导入…

附件1.服务器操作系统安全加固要求及配置建议【上】

文章目录 加固文件说明【重启auditd服务后/etc/audit/audit.rules文件内容消失怎么处理】【用户的的本地登录和远程登录默认都会被审计&#xff1b; 2&#xff0c;可配置对chown、chmod、chcon等命令的执行进行审计&#xff1b;【ssh会话默认会被审计&#xff1b;】【可添加审计…

ImportError: numpy.core.multiarray failed to import

遇到的问题&#xff1a; 解决方法&#xff1a; 根据你的opencv版本&#xff0c;去百度搜索对应的 numpy 版本&#xff0c;卸载掉现有的numpy &#xff0c;安装其他版本: sudo pip install numpy1.19.0或者直接升级到numpy的最新版本&#xff1a; sudo pip install --upgrade…

Python 批量修改或替换文本内容

Python 批量修改或替换文本内容 前言源码效果图鸣谢 前言 遇到一个需要将一堆代码中部分文字替换或删除的需求。 需要处理像下面上百个脚本 需要删除每个脚本中类似下图的内容 可以用python批量处理所有的代码&#xff0c;并且删除我想要删除的内容。 源码 其中使用charde…