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

【c++深入系列】:万字详解vector(附模拟实现的vector源码)

🔥 本文专栏:c++
🌸作者主页:努力努力再努力wz

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

💪 今日博客励志语录种子破土时从不问‘会不会有光’,它只管生长

★★★ 本文前置知识:

模版


1.什么是vector

那么想必大家都学过顺序表这个数据结构,那么顺序表就是通过开辟一个连续的内存空间来存储数据,在c语言期间,我们如果要自己去实现一个顺序表,那么首先就需要定义一个动态数组,然后还需要定义关于顺序表的增删改查相关操作的函数,比如insert插入函数以及erase删除函数等等

那么假设有这么一个场景,你的程序中涉及到一个存储int类型数据的一个顺序表,那么此时你就得定义一个存储int类型的动态数组,并且还得手写关于该顺序表的增删改查的相关操作的函数,但是如果你的程序还涉及到一个存储double类型的顺序表,那么意味着你现在不仅得定义一个存储double类型的动态数组,并且还要手写一份针对于double类型数据的顺序表的增删改查相关的函数,如果说此时你的代码还涉及到存储char等其他类型数据的顺序表的话,那么想必你肯定非常的痛苦

那么从上面这个场景,我们就能够认识到c语言实现顺序表的一个局限,那么一旦我们程序涉及到多个存储不同数据类型的顺序表,那么意味着我们得针对不同的数据类型的顺序表定义多份函数,并且对于相同操作的函数来说,他们彼此之间的逻辑是相同的,唯一的区别就是处理的数据不同,所以针对存储不同的数据类型的顺序表定义多份相关的函数,不仅会导致代码冗余,而且还不方便维护,那么至于如何解决,那么想必小伙伴们心中早已有答案,那么这个问题正是模版的应用场景

所以为了c++的vector便解决了c语言实现顺序表的窘境,那么vector是一个模版类,它内部维护的就是一个顺序表,并且底层采取的是动态数组的方式实现,并且关于顺序表的增删改查的相关的成员函数,那么vector类内部都有提供,并且支持扩容的逻辑,那么vector类最关键的就是它是一个模板类,所以我们不必要再担心如果我们的代码中涉及到多种不同数据类型的顺序表,需要我们手动造轮子的问题

那么由于vector是模版类,那么编译器会识别到我们代码中创建的vector对象,然后根据其存储的数据类型,来实例化一份存储对应数据类型的vector类,那么对于存储特定数据类型的顺序表的维护以及增删改查,这些内容统统都交给了vector来完成,因为其底层都有对应的实现,那么我们只需要站在巨人的肩膀上,放心使用vector类我们提供的各种成员函数即可

那么在知道了什么是vector之后,那么接下来我将会从两个维度来带你全面认识vector,分别是vector如何使用以及其底层的实现原理,那么废话不多说,我们首先先来认识vector如何使用

2.vector如何使用以及其底层实现原理

那么要知道vector如何使用,那么我们就得从两方面来认识vector,分别是vector的成员变量以及vector的成员函数,那么首先先来介绍一下vector的成员变量

1.成员变量

如果读者学习接触的第一个容器是string的话,那么读者可能会习惯认为vector会采取string的成员变量的设计,也就是vector内部会维护一个指向动态数组的首元素的指针以及一个size变量以及一个capacity变量,但是vector的成员变量其实是三个迭代器,那么这三个迭代器的本质就是指针

那么这里就得注意了,从包括vector开始以及之后学习的容器比如list以及queue等等,我们就得习惯迭代器作为容器的成员变量,因为历史原因,string的诞生是早于STL的,而STL规定了容器的成员变量以及成员函数的规范,那么对于成员变量,那么统一都是以迭代器作为成员变量,因为我们访问STL的容器都是统一通过迭代器来进行访问容器里面存储的元素,其次就是容器中的成员函数,那么STL对容器里的成员函数的命名做了统一,比如返回容器中存储有效数据的个数的函数都被命名为了size()函数,以及返回容器当前的容量则是都命名为capacity()函数等等,那么统一命名规范的好处是我们能够熟悉的使用各个不同的容器,并且还降低了我们学习的成本

而vector内部维护了三个指针,分别是start以及finish和end_of_storage,那么从三个成员变量的名字我们就能够猜到他们的作用了,那么这个三个指针就是用来维护vector中开辟的动态数组,其中start指向动态数组的起始位置,而finish指向动态数组的有效数据结尾的下一个位置,而end_of_storage则是指向动态数组结尾的下一个位置

在这里插入图片描述

那么有的读者可能会对finish以及end_of_storage的指向有一点疑问,为什么finish和end_of_storage不直接指向有效数据的结尾以及动态数组的末尾,那么是因为vector类中遍历一个迭代器区间,统一将这个迭代器区间认为是左闭右开[first,last)的区间,所以这里finish以及end_of_storage得各自指向末尾的下一个元素,那么他们的作用我们可以类比字符串末尾的’\0’来帮组我们理解

2.成员函数

那么知道了成员变量之后,那么接下来就来认识vector的成员函数

构造函数

那么认识一个类,首先得从构造函数说起,那么vector类中提供了多个重载版本的构造函数来满足用户不同的初始化需求,其中就包括无参的构造函数:

vector();

那么无参的构造函数的所做的内容就是将vector类中的三个指针给初始化为空,也就是得到一个空数组的vector对象

模拟实现:

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

其次就是接受n个元素来初始化的构造函数:

vector (size_type n, const value_type& val = value_type());

那么这里就要注意的是,这里第二个参数也就是要初始化的元素值,这里vector的为其提供了缺省值,那么缺省值就是该数据类型的默认值,那么我们知道对于内置类型来说,那么它的默认值给0即可,但这里的元素的类型可能是自定义类型,那么对于自定义类型来说,那么它的初始值是调用无参的构造函数的对象,但是要知道vector是一个模版类,那么其中的数据类型都是被定义为了模版参数,我们不可能对模版参数来进行所谓的比较判断,如果其等于自定义类型,那么就给0的缺省值,而如果其是自定义类型就提供一个调用无参构造的临时对象
不能这么多的原因,首先是语法层面上不允许我们这样做,其次就是模版是一种泛型编程,所谓的泛型编程就是忽略数据类型,而这里我们却添加针对于数据类型的比较逻辑,这明显也和模版的思想相违背

所以为了解决这个问题,那么c++在引入模版之后,提供了一个新的初始化方式:

myclass tmp=myclass();

那么如果tmp是内置类型,那么会将其设置为0,如果tmp是自定义类型,会提供一个调用无参的构造函数初始化的临时对象来赋值给tmp,并且我们还可以在括号内自己设置初始值,那么我们可以写一段简单的代码来熟悉这个语法:

#include<iostream>
#include<string>
int main()
{int a = int();std::cout << "a= " << a << std::endl;std::string b = std::string();std::cout << "b=" << b << std::endl;double c = double(8.7);std::cout << "c=" <<c<< std::endl;std::string d = std::string("1223");std::cout << "d=" << d << std::endl;return 0;
}

在这里插入图片描述

然后则是拷贝构造函数:

那么拷贝构造函数则是接收一个vector对象,那么这里要注意的就是拷贝构造函数实现的是深拷贝而不是浅拷贝,也就是说这里复制的时候,我们会先开辟一个新的动态数组,然后将vector对象中对应的元素给依次拷贝到该动态数组中的对应位置,然后再初始化三个指针start以及finish和end_of_storage指向该动态数组

vector(const vector<T>& v);

那么这里会有一个坑:

这里的拷贝,很多小伙伴估计会直接调用memcpy来进行拷贝,那么对于内置类型是可以的,但是对于自定义类型就会出错,因为memcpy拷贝采取的是值拷贝,而对于需要深拷贝的自定义类型来说,比如以string为例,那么memcpy结束后,此时对应位置的string对象中的动态的字符数组就会有两个string对象共同指向,那么会带来析构两次等问题,所以这里解决的策略就是通过赋值来解决,因为自定义类型内部肯定会提供赋值运算符重载函数,并且其底层是一定支持深拷贝,所以这里直接遍历vector数组中的对应元素来依次赋值,不仅满足内置类型的拷贝,其次也能满足自定义类型的深拷贝的需求。

模拟实现:

 vector(const vector<T>& v1){T* tmp = new T[v1.capacity()];for (size_t i = 0;i < v1.size();i++){tmp[i] = v1[i];}_start = tmp;_finish = _start + v1.size();_end_of_storage = _start + v1.capacity();}

size函数

那么这里的size函数则是返回vector对象中存储有效数据的个数,那么实现的原理就是通过指针相减,那么这里让finish指针减去start指针(finish-start),得到start指针与finish指针之间的元素个数,但是前提是两个指针得指向同一块连续的内存区域

其次vector提供了两个版本的size重载函数,因为有的vector对象会被const修饰,那么被const修饰的对象只能被const修饰的this指针所接收,所以这里提供了两个版本的size函数

size_t size();
size_t size() const;

模拟实现:

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

capacity函数

那么capacity函数则是返回当前容器的容量,也就是动态数组的最大长度,那么其实现原理和size是一样的通过指针的算术运算来实现,也就是end_of_storage减去start得到目前容器能够容纳多少个元素

同样capacity函数也提供了两个版本,一个支持非const的vector对象,一个支持const修饰的vector对象

size_t capacity();
size_t capacity() const

模拟实现:

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

reserve函数

那么reserve函数的作用就是预开辟一定大小的数组,那么他会接收一个size_t的参数,那么该参数就是新的动态数组的大小,那么注意reserve函数底层实现的时候,那么vector不一定是一个空数组,所以存在我们reserve申请开辟的空间可能小于当前数组的容量,但reserve函数没有所谓的缩容的行为,那么这里我们得判断新开辟的空间与capacity的大小,只有比capacity大才能扩容,接着开辟一个新的动态数组,然后拷贝旧空间的数据,那么这里会是会面临和拷贝构造函数一样的问题,如果数组中的元素类型是需要深拷贝的自定义类型,那么这里我们不能通过memcpy来浅拷贝,由于自定义类型有支持深拷贝的赋值运算符重载函数,那么就得依次对应位置采取赋值的方式来拷贝

void reserve(size_t n)

模拟实现:

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

swap函数

那么swap函数则是交换两个vector对象的值,而我们知道由于标准库里面已经实现了一个swap函数,而这里对于vector对象来说,那么它要完成与另一个对象的成员变量的交换,那么就是交换三个迭代器的值,所以这里swap函数可以来调用标准库中的swap函数来交换三个迭代器即可

   void swap( vector<T>& v1)

模拟实现:

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

运算符重载函数

1.[]下标访问运算符重载函数

那么由于vector底层维护的是一个动态数组,而数组的空间是连续并且支持随机访问,那么相比于通过迭代器来访问vector对象中数组的元素,那么直接使用下标访问运算符来访问则更为直观形象,那么下标访问运算符返回的就是一个vector数组中元素的引用,因为我们可以直接通过下标访问的方式来修改vector对象中数组中的元素

那么下标访问运算符重载函数会接收一个size_t的数组索引,在函数内部我们还得判断该索引是否在有效程度之内

  T& operator[](size_t pos)const T& operator[](size_t pos) const

并且这里下标访问运算符重载函数有两个版本,分别支持非const对象以及const对象,那么非const对象能够通过该运算符重载函数来访问并且能够修改元素,而const对象则只能读不能写

模拟实现:

  T& operator[](size_t pos){assert(0 <= pos && pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(0 <= pos && pos < size());return _start[pos];}
2.赋值运算符重载函数

那么赋值运算符重载函数的实现可以采取上文的拷贝构造函数的方式,但在这里我们可以采取一个新的方式来实现,因为这里赋值运算符重载函数是传引用,所以这里我们在构造函数内部可以创建一个临时对象,然后调用该临时对象的拷贝构造函数用传进来的对象的引用来初始化该临时对象,接着我们在调用swap函数,将this指针指向的对象与临时对象进行交换,那么此时临时对象中的维护的就是原来的this指针指向的对象的动态数组,那么一旦函数调用结束,那么局部变量会随着函数栈帧一起被销毁,那么此时编译器会调用该临时变量的析构函数,所以无需我们来手动释放旧空间,而此时this指向的对象保存的则是之前临时对象中的动态数组,那么这里采取的就是一个巧妙的函数复用

那么注意赋值运算符的返回值是this对象,因为我们有连续赋值的语法,也就是通过右侧的赋值运算符重载函数的返回值来继续依次赋值给左侧的对象

 vector<T>& operator=(const vector<T>& v1)

模拟实现:

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

push_back函数

那么push_back函数则是尾插一个元素,那么我们了解了vector类的成员变量之后,那么push_back如何进行尾插的原理其实很简答,那么push_back函数内部会首先判断当前是否需要扩容,也就是如果size()==capacity(),那么代表当前容器已满,那么需要扩容,那么扩容完成之后,由于finish指向的是有效数据结尾的下一个位置,那么我们只需要在finish指向的位置给设置为要插入的值,通过指针的解引用,然后再将finish往后移动一个单位即可

void push_back (const value_type& val);

模拟实现:

 void push_back(const T& val){if (size() == capacity()){size_t newsize;capacity() == 0 ? newsize = 2 : newsize = 2 * capacity();reserve(newsize);}*_finish = val;_finish++;}

要注意这里push_back要进行扩容的时候,那么这里我首先对capacity进行了一个判断,判断其是否为0,因为存在这样的场景,用户可能创建了一个调用无参的构造函数的vector对象,然后再调用push_back函数插入一个元素,由于此时该vector对象是空数组,那么空数组意味着capacity()=0,那么我们扩容的逻辑是采取的是2倍扩容,也就是新的动态数组的容量是2*capacity(),但是如果按照刚才的场景,由于是空数组,此时进入push_back函数内部会先进行一个容量的判断,而对于空数组来说此时size()==capacity()==0,所以肯定会扩容,但是如果我们直接无脑的reserve( 2 * capacity()),那么此时你发现2 * capacity()的值还是0,扩了个寂寞,所以这里我加了一个capacity()的条件判断,如果为空,那么就先分配长度为4的数组,不为空,就可以直接2倍扩容,并且这里我们将新的容量大小都记录在newsize中,然后调用reserve

insert函数

那么push_back函数比较局限,它只能在尾部插入,而不能在动态数组内的有效长度内的任意位置进行插入,但是push_back由于是尾插,那么它的插入的时间代价是O(1),非常高效,不需要元素的移动,而这里如果我们要在动态数组的有效长度内的任意位置插入,那么就需要调用insert函数

那么vector也提供了多个insert函数的重载版本,因为我们插入的可能只有一个元素也可能插入多个元素

对于第一个重载版本,那么insert函数首先会接受一个迭代器,指向vector对象中保存的数组中的有效长度中的某个位置,第二个参数就是插入的元素的值

iterator insert (iterator pos, const value_type& val);

那么这里insert函数内部会首先判断接收的第一个参数,也就是迭代器所指向的位置的合法性,也就是该指针指向的位置是否在[start,finish)区间内,如果合法,接着会判断容量,如果size()==capacity(),那么说明当前容量已满,那么需要扩容,那么扩容完成之后的下一个环节,便是先将[pos,finish)区间中的所有元素整体向后移动一个单位,然后再将pos位置的值给设置为插入元素的值

模拟实现:

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

那么这里模拟实现该版本的insert函数的时候会有一个坑

如果insert函数内部执行了扩容,那么注意,此时会创建一个新的动态数组,然后将原来的动态数组给delete释放掉,而pos迭代器还是指向原本旧的动态数组,所以这里我们就得更新pos,指向新的动态数组,那么学过物理的读者,会知道物理里面有一个叫相对距离的概念,虽然这里开辟了一个新的动态数组,但是pos位置与start位置的相对距离是不会变化的,所以这里在扩容之前,我们需要先记录一下pos与start之间的相对距离len,获取其间隔几个元素,通过简单的指针的算术运算来得到,那么reserve完之后,start指向了新的动态数组的起始位置,那么这里我们就将star+len的值赋给pos从而更新pos位置

而第二个重载版本,那么插入的不是一个元素,而是多个相同值的元素,那么这个版本的重载函数的实现原理相较于上面,区别就是这里我们不是将[pos,finish)区间中的所有元素整体向后移动一个单位,而是向后移动n个单位

iterator insert(iterator pos,size_t n,const value_type& val);

模拟实现:

iterator insert(iterator pos, size_t n, const T& val){assert(_start <= pos && pos <= _finish);if (size() + n >= capacity()){size_t newsize = size() + n + 2;size_t len = pos - _start;reserve(newsize);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + n) = *end;end--;}end = pos;for (int i = 0;i < n;i++){(*end) = val;end++;}return pos;}

而第三个版本就是通过一个迭代器区间初始化,那么前面的步骤和之前的都一样,也就是判断位置合法性,以及是否需要扩容,只不过在扩容之前,我们需要计算一下迭代器区间中元素的个数n,然后判断size()+n==capacity(),接着在遍历迭代器区间,那么遍历的时候,就得注意这里我们采取的是双指针,定义一个src指针指向pos位置处,再定义一个des指针指向pos+n位置处,然后将src位置处的值拷贝到des位置处,那么拷贝完之后,两个指针同时向后移动,直到src指针到达finish指针指向的位置,那么说明此时将[pos,finish)区间的所有元素向后移动了n个单位,那么接着我们再将的迭代器区间中的值拷贝到[pos,pos+n)区间即可

iterator insert (iterator position, iterator first,iterator last);

模拟实现:

 iterator insert(iterator pos, iterator first, iterator last){assert(_start <= pos && pos <= _finish);size_t ns = last - first;if (size() + ns >= capacity()){size_t newsize = size() + ns + 2;size_t len = pos - _start;reserve(newsize);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + ns) = *end;end--;}end = pos;while (first != last){*end = *first;end++;first++;}return  pos;}

那么这就是三个insert的重载函数,那么有的小伙伴可能会发现insert函数的返回值是一个迭代器,那么这里为什么insert函数会返回一个迭代器呢?

那么这就和迭代器失效有关:

那么有的小伙伴在使用insert函数会有这样的习惯,那么在对该vector对象调用insert函数在pos位置插入完了之后,接着继续通过pos迭代器来访问,那么这里就会有一个后果,因为你不知道这里调用insert插入完成之后,你的vector对象是否发生了扩容,那么一旦发生了扩容,我们知道insert是传值拷贝给形参,虽然我们在insert函数内部更新了pos,但是形参的改变不影响实参,所以这里的pos位置仍然指向了是旧的空间,那么此时旧的空间已经被释放,那么我们通过该指针去访问,就是非法访问内存

那么可能有的小伙伴会抬杠说,那么这里如果vector没有进行扩容,那么意味着此时pos还是指向原来的空间,那么这里不就可以访问了吗,并且vs平台下是2倍扩容,那么从0->1->2->4->8->…来依次向后扩容,那么意味着我可以知道vs平台下扩容的时机,意味着我可以来判断什么时候可以继续使用传给insert函数的迭代器去访问,什么时候不能用该迭代器去访问了

那么这里我想说的就是,确实存在有些小伙伴说的那样,如果此时insert没有扩容,那么理论上来说pos确实可以正确访问,但是你的代码并不一定只在vs平台下面跑,也可能在Linux平台等其他平台下跑,那么不同平台,他们底层的vector的实现是有区别的,那么意味着不同平台下vector的扩容的逻辑是不同,所以这里我们统一的认为此时调用完insert之后的pos迭代器是失效的,不能再通过它去访问,并且人家STL库的设计者也考虑到了这个问题,所以会返回一个指向新的pos位置的迭代器,所以我们还是老老实实的使用insert返回值去访问,别去钻这个空子,并且在vs平台下,还进行了严格的检查,不允许我们调用完insert还有继续使用迭代器的行为:

在这里插入图片描述

pop_back函数

那么pop_back函数就是在删除vector对象中数组的最后一个有效元素,那么其实现的原理也很简单,那么首先我们得先判断一下当前数组是否为空,不为空,我们直接将finish指针想前移动一个单位即可,因为finish指针代表的是有效数组结尾的下一个位置,那么我们遍历通常是遍历到finish指向的位置结束,而不会访问包括finish以及finish之后的所有元素,所以我们只需要移动finish即可,而不需要手动覆盖

模拟实现:

   void pop_back(){assert(size() != 0);_finish--;}

erase函数

那么pop_back函数只能删除结尾的元素那么erase则是能够删除vector数组中有效长度内的任意元素,那么vector类中提供了两个重载版本的erase

那么第一个版本就是删除有效长度内的任意一个元素,那么这里erase会接收一个迭代器

 iterator erase(iterator pos)

那么它的底层的实现原理就是首先会检查该迭代器指向的位置是否有效,然后接着会进行元素的移动,那么这里会将[pos,finish)区间的整体向前移动一个单位,那么此时我们可以在erase函数中定义一个end指针,指向pos位置,然后将end指向的位置的下一个位置的值来覆盖当前end指向的位置,然后end往后移动一个单位,直到达到finish位置,那么是说明此时移动完毕,最后我们在将finish指针往前移动一个单位

 iterator erase(iterator pos)

模拟实现:

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

那么第二个重载版本则是接收两个迭代器first和last,那么这两个迭代器分别指向该vector对象内的数组的某个连续区间的起始以及结束位置,那么我们就要删除这个连续的区间

iterator erase(iterator first, iterator last)

那么首先第一步我们还是得判断该区间的合法性,也就是检查这两个迭代器是否在[start,finish)中,并且last要大于first,然后就是元素的移动,那么这里我们首先要计算一下[first,last)区间中的元素个数n,那么通过指针算术运算可以得到,那么这里我采取的是双指针的方式来实现,那么定义一个指针src指向first位置,再定义另一个指针des指向last位置,然后将des指向的位置的值覆盖src指向的位置,然后两个指针同时往后移动,那么知道des到达finish位置,那么说明此时元素已经移动完毕,然后再更新finish的指向

模拟实现:

   iterator erase(iterator first, iterator last){assert(_start <= first && first < _finish && first < last && _start <= last && last < _finish);iterator src = first;iterator des = last;while (des < _finish){*src = *des;src++;des++;}_finish = src;return first;}

那么这里注意这里erase的返回值是一个迭代器,那么之前insert函数返回迭代器,那么我们知道是因为扩容问题会导致的迭代器失效,那么这里为什么erase也会返回一个迭代器呢,而这里erase函数不需要扩容,所以不存在所谓的迭代器失效啊?

那么这里虽然erase函数不需要扩容,但是要注意的就是erase函数它删除完元素之后,那么vector对象中的动态数组的元素会移动,那么此时该迭代器指向的位置的值已经不再是原本的值了,所以我们再用迭代器访问就可能会引发后果

假设你删除的位置是最后一个位置的话,那么此时你删除完最后一个元素,那么再通过迭代器去访问,那么该位置的内容可能是随机值

并且在vs平台下编译器还进行严格的检查,那么不允许你调用erase之后,再使用迭代器来继续访问。所以我们还是得老老实实用erase的返回值来继续去访问

resize函数

那么resize函数就是来调整vector中的有效长度,resize数组会接收一个size_t的参数n,表示vector数组新的有效长度,那么如果n小于size(),那么我们就直接截断,也就是将finish指针指向star+n,而如果n大于size但是小于capacity(),那么这里我们就将[start+size(),start+n)用接收到的第二参数的值来填充,这第二份参数也提供了缺省值,那么这个缺省值在上文的构造函数内容中有详细讲过,最后调整finish指针,而如果n大于capacity(),那么则需要扩容,然后填充[start+size(),start+n)区间的值

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

模拟实现:

void resize(size_t n, const T& val = T()){if (n > size()){if (n > capacity()){reserve(n+1);}for (size_t i = size();i < n;i++){_start[i] = val;}}else {_finish = _start +n;}}

源码

myvector.h

#pragma once
#include<iostream>
#include<assert.h>
namespace wz
{template<typename T>class vector{public:typedef T* iterator;typedef const T* const_iterator;vector():_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){}vector(size_t n, const T& val = T()):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);for (size_t i = 0;i < n;i++){push_back(val);}}vector(const vector<T>& v1){T* tmp = new T[v1.capacity()];for (size_t i = 0;i < v1.size();i++){tmp[i] = v1[i];}_start = tmp;_finish = _start + v1.size();_end_of_storage = _start + v1.capacity();}template<typename inputiterator>vector(inputiterator first, inputiterator last){while (first != last){push_back(*first);first++;}}~vector(){delete[] _start;_start = nullptr;_finish = nullptr;_end_of_storage = nullptr;}size_t size(){return _finish - _start;}size_t size() const{return _finish - _start;}size_t capacity(){return _end_of_storage - _start;}size_t capacity() const{return _end_of_storage - _start;}iterator begin(){return _start;}const_iterator begin() const{return _start;}iterator end(){return _finish;}const_iterator end() const{return _finish;}void reserve(size_t n){if (n > capacity()){T* tmp = new T[n];size_t odsize = size();for (size_t i = 0;i < odsize;i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = _start + odsize;_end_of_storage = _start + n;}}T& operator[](size_t pos){assert(0 <= pos && pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(0 <= pos && pos < size());return _start[pos];}void swap(vector<T>& v1){std::swap(_start, v1._start);std::swap(_finish, v1._finish);std::swap(_end_of_storage, v1._end_of_storage);}vector<T>& operator=(const vector<T>& v1){vector<T> tmp(v1);swap(tmp);return *this;}void push_back(const T& val){if (size() == capacity()){size_t newsize;capacity() == 0 ? newsize = 2 : newsize = 2 * capacity();reserve(newsize);}*_finish = val;_finish++;}void pop_back(){assert(size() != 0);_finish--;}void resize(size_t n, const T& val = T()){if (n > size()){if (n > capacity()){reserve(n+1);}for (size_t i = size();i < n;i++){_start[i] = val;}}else {_finish = _start +n;}}void clear(){_finish = _start;}bool empty(){return _start == _finish;}iterator insert(iterator pos, const T& val){assert(_start <= pos && pos <= _finish);if (size() >= capacity()){size_t newsize;capacity() == 0 ? newsize = 4 : newsize = 2 * capacity();size_t len = pos - _start;reserve(newsize);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;end--;}*pos = val;return pos;}iterator insert(iterator pos, size_t n, const T& val){assert(_start <= pos && pos <= _finish);if (size() + n >= capacity()){size_t newsize = size() + n + 2;size_t len = pos - _start;reserve(newsize);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + n) = *end;end--;}end = pos;for (int i = 0;i < n;i++){(*end) = val;end++;}return pos;}iterator insert(iterator pos, iterator first, iterator last){assert(_start <= pos && pos <= _finish);size_t ns = last - first;if (size() + ns >= capacity()){size_t newsize = size() + ns + 2;size_t len = pos - _start;reserve(newsize);pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + ns) = *end;end--;}end = pos;while (first != last){*end = *first;end++;first++;}return  pos;}iterator erase(iterator pos){assert(_start <= pos && pos < _finish);iterator end = pos;while (end < _finish){*end = *(end + 1);end++;}_finish--;return pos;}iterator erase(iterator first, iterator last){assert(_start <= first && first < _finish && first < last && _start <= last && last < _finish);iterator src = first;iterator des = last;while (des < _finish){*src = *des;src++;des++;}_finish = src;return first;}private:iterator _start;iterator _finish;iterator _end_of_storage;};
}

main.cpp:

#include"myvector.h"
int main()
{wz::vector<int> s1(10, 6);for (int i = 0;i < s1.size();i++){std::cout << s1[i] << " ";}std::cout << std::endl;std::cout << "----------------------" << std::endl;s1.clear();s1.push_back(1);s1.push_back(3);s1.push_back(5);s1.push_back(10);s1.pop_back();auto it = s1.begin();while (it != s1.end()){std::cout << *it << " ";it++;}std::cout << std::endl;std::cout << "--------------------------" << std::endl;s1.insert(s1.begin(), 200);wz::vector<int> s2(2, 400);s1.insert(s1.begin()+1, s2.begin(), s2.end());wz::vector<int>:: iterator it1 = s1.begin();while (it != s1.end()){std::cout << *it << " ";it++;}std::cout << std::endl;std::cout << "--------------------------" << std::endl;s1.resize(1);it = s1.begin();while (it != s1.end()){std::cout << *it<< " ";it++;}std::cout << std::endl;std::cout << "--------------------------" << std::endl;s1.erase(s1.begin());it = s1.begin();while (it != s1.end()){std::cout << *it << " ";it++;}std::cout << std::endl;std::cout << "--------------------------" << std::endl;s1.push_back(1000);wz::vector<int> s3;s3 = s1;wz::vector<int>:: iterator it2 = s3.begin();while (it2 != s3.end()){std::cout << *it2 << " ";it2++;}std::cout << std::endl;std::cout << "---------------------------" << std::endl;return 0;
}

运行截图:
在这里插入图片描述

结语

那么这就是本篇文章关于vector的全部内容,那么带你全方面剖析vector,希望读者下去也能自己实现vector类,那么我的下一期博客将会讲解list,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请三连加关注哦,你的支持就是我创作的最大的动力!

在这里插入图片描述

相关文章:

  • Oracle OCP认证考试考点详解083系列07
  • photoshop学习笔记2
  • 算法每日一题 | 入门-顺序结构-大象喝水
  • Java基于SaaS模式多租户ERP系统源码
  • JavaScript 性能优化之框架 / 工程层面的优化
  • 【深度学习|学习笔记】深度孪生神经网络Deep Siamese neural network(DSCN)的起源、发展、原理和应用场景(附代码)
  • 招聘绩效效果评估方案与优化路径
  • **面试水货程序员马小帅**
  • Ubuntu 安装 Docker
  • LeetCode 1128 等价多米诺骨牌对的数量 题解
  • 【25软考网工】第五章(5)ICMP和ICMPv6、NDP、IP组播技术和MPLS
  • 算法笔记.求约数
  • 【iOS】 分类 拓展 关联对象
  • Spring AI 实战:第九章、Spring AI MCP之万站直通
  • 聊聊对Mysql的理解
  • 每日c/c++题 备战蓝桥杯(洛谷P1015 [NOIP 1999 普及组] 回文数)
  • 从头训练小模型: 4 lora 微调
  • 性能优化实践:内存优化技巧
  • LeetCode 热题 100 994. 腐烂的橘子
  • 宏任务与微任务
  • AI世界的年轻人|横跨教育与产业,他说攻克前沿问题是研究者的使命
  • 抗战回望15︱《五月国耻纪念专号》:“不堪回首”
  • 在“蟑螂屋”里叠衣服,我看见人生百态
  • 消息人士称以色列政府初步同意扩大对加沙军事行动
  • 德国旅游胜地发生爆炸事故,11人受伤
  • 解放日报:浦东夯实“热带雨林”式科创生态