【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,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请三连加关注哦,你的支持就是我创作的最大的动力!
相关文章:
【c++深入系列】:万字详解vector(附模拟实现的vector源码)
🔥 本文专栏:c 🌸作者主页:努力努力再努力wz 💪 今日博客励志语录: 种子破土时从不问‘会不会有光’,它只管生长 ★★★ 本文前置知识: 模版 1.什么是vector 那么想必大家都学过顺…...
OpenHarmony平台驱动开发(二),CLOCK
OpenHarmony平台驱动开发(二) CLOCK 概述 功能简介 CLOCK,时钟是系统各个部件运行的基础,以CPU时钟举例,CPU 时钟是指 CPU 内部的时钟发生器,它以频率的形式工作,用来同步和控制 CPU 内部的各…...
Java大厂面试:Java技术栈中的核心知识点
Java技术栈中的核心知识点 第一轮提问:基础概念与原理 技术总监:郑薪苦,你对JVM内存模型了解多少?能简单说说吗?郑薪苦:嗯……我记得JVM有堆、栈、方法区这些区域,堆是存放对象的地方…...
硬件加速模式Chrome(Edge)闪屏
Chrome开启“硬件加速模式”后,打开浏览器会闪屏或看视频会闪屏,如果电脑只有集显,直接将这个硬件加速关了吧,没啥必要开着 解决方法 让浏览器使用独立显卡 在Windows左下角搜索 图形设置 ,将浏览器添加进去&#…...
【ArcGIS微课1000例】0145:如何按照自定义形状裁剪数据框?
文章目录 一、添加数据二、绘制形状三、裁剪格网和经纬网一、添加数据 打开软件,添加配套实验数据包中0145.rar中的影像数据,如下图所示: 二、绘制形状 1. 在数据视图中,使用绘图 工具条上的新建圆工具 可创建一个椭圆,使其包含要在该数据框中显示的数据范围。 修改椭圆…...
深入了解Linux系统—— 环境变量
命令行参数 我们知道,我们使用的指令它本质上也是一个程序,我们要执行这个指令,输入指令名然后回车即可执行;但是对于指令带选项,又是如何实现的呢? 问题:main函数有没有参数? 在我…...
软考-软件设计师中级备考 12、软件工程
一、软件工程概述 定义:软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量软件的学科。它涉及到软件的开发、测试、维护、管理等多个方面,旨在运用一系列科学方法和技术手段,提高软件的质量和开发效率,降低软件开…...
FreeSwitch Windows安装
下载 FreeSwitch 官网下载地址https://files.freeswitch.org/windows/ 根据自己的系统选择不同的版本,如下图 官网下载可能比较慢,请使用下方下载 FreeSWITCH-1.10.12-Release-x64.msi https://download.csdn.net/download/a670941001/90752912 2、…...
南京优质的公司有哪些?
南京有许多优质的公司,以下是一些有代表性的: 制造业 • 南京钢铁集团有限公司 :作为国家战略布局的 18 家重点钢企之一,是中国特大型钢铁联合企业,1993 年 12 月进行公司制改革,2010 年 9 月实现整体上市…...
Spring AI 实战:第十一章、Spring AI Agent之知行合一
引言:智能体的知行辩证法 “知为行之始,行为知之成”,王阳明的哲学智慧在AI时代焕发光彩。智能体(LLM Agent)的进化之路,正是"认知-决策-执行"这一闭环的完美诠释: 知明理:融合大语言模型的推理能力与知识图谱的结构化认知行致用:基于ReAct模式的动态工具调…...
LeetCode 1128 等价多米诺骨牌对的数量 题解
今天的每日一题,我的思路还是硬做,不如评论区通过状压写的简单,但是答题思路加算法实现是没有问题的,且时间复杂度也是可以通过的,毕竟全是o(n) 那么我就来说一下我的思路,根据dominoes[i] [a, b] 与 domi…...
管理配置信息和敏感信息
管理配置信息和敏感信息 文章目录 管理配置信息和敏感信息[toc]一、什么是ConfigMap和Secret二、使用ConfigMap为Tomcat提供配置文件三、使用Secret为MongDB提供配置文件 一、什么是ConfigMap和Secret 在 Kubernetes 中,ConfigMap 和 Secret 是两种用于管理配置数据…...
Rust与C/C++互操作实战指南
目录 1.前言2.动态库调用2.1 动态加载2.2 静态加载3.代码调用4.静态库调用1.前言 本文原文为:Rust与C/C++互操作实战指南 由于rust诞生时间太短,目前生态不够完善,因此大量的功能库都需要依赖于C、C++语言的历史积累。 而本文将要介绍的便是如何实现rust与c乃至c++之间实…...
word批量转pdf工具
word批量转pdf工具 图片 说到了办公,怎能不提PDF转换哦? 这是一款一键就可以批量word转换为PDF的小工具,简直是VB界的一股清流。 图片 操作简单到不行,只要把需要转换的word文件和这个工具放在同一个文件夹里,双击…...
【数据结构】励志大厂版·初阶(复习+刷题)排序
前引:本篇作为初阶结尾的最后一篇—排序,将先介绍八种常用的排序方法,然后开始刷题,小编会详细注释每句代码的作用,不会出现看不懂的情况,这点大家放心,既是写给大家同时也是写给自己的…...
Git推送大文件导致提交回退的完整解决记录
问题背景 在向Gitee推送代码时,因单文件超过平台限制(100MB),推送被拒绝: > git push origin master:master remote: File [6322bc3f1becedcade87b5d1ea7fddbdd95e6959] size 178.312MB, exceeds quota 100MB rem…...
游戏引擎学习第257天:处理一些 Win32 相关的问题
设定今天的工作计划 今天我们本来是打算继续开发性能分析器(Profiler),但在此之前,我们认为有一些问题应该先清理一下。虽然这类事情不是我们最关心的核心内容,但我们觉得现在是时候处理一下了,特别是为了…...
高性能数据库架构探索:OceanBase 分布式技术深入解析
高性能数据库架构探索:OceanBase 分布式技术深入解析 简介 OceanBase 高性能分布式数据库,解决传统数据库在大规模、高并发场景下的性能瓶颈,通过分布式架构、数据自动分片和强一致性协议,提供高可用性、弹性扩展和出色的性能&am…...
【CISCO】Se2/0, Se3/0:串行口(Serial) 这里串口的2/0 和 3/0分别都是什么?
在 Cisco IOS 设备上,接口名称通常遵循这样一个格式: <类型><槽号>/<端口号>类型(Type):表示接口的物理或逻辑类型,比如 Serial(串行)、FastEthernet、GigabitEt…...
GPU集群训练经验评估框架:运营经理经验分析篇
引言 随着深度学习模型规模的持续增长和复杂度的不断提高,单GPU训练已经难以满足现代AI研究和应用的需求。GPU集群训练作为一种有效的扩展方案,能够显著提升训练效率、处理更大规模的数据集和模型。然而,GPU集群训练涉及到分布式训练框架、集群管理工具、性能优化等多个技术…...
函数多项式拟合
函数多项式拟合 用处 不方便使用math时,可以使用多项式拟合法实现比较高效的数学函数,比如使用avx指令时,O3优化,math中的函数会调用FPU指令集,在指令集切换的过程中代码效率大幅降低,为避免使用math中的…...
【Hive入门】Hive与Spark SQL集成:混合计算实践指南
目录 引言 1 Hive与Spark SQL概述 1.1 Hive简介 1.2 Spark SQL简介 2 Hive与Spark SQL集成架构 2.1 集成原理 2.2 配置集成环境 3 混合计算使用场景 3.1 场景一:Hive表与Spark DataFrame互操作 3.2 场景二:Hive UDF与Spark SQL结合使用 3.3 场…...
TFQMR和BiCGStab方法比较
TFQMR(Transpose-Free Quasi-Minimal Residual)和BiCGStab(Bi-Conjugate Gradient Stabilized)都是用于求解非对称线性方程组的迭代方法,属于Krylov子空间方法的范畴。它们分别是BiCG(双共轭梯度法…...
小程序 IView WeappUI组件库(简单增删改查)
IView Weapp 微信小程序UI组件库:https://weapp.iviewui.com/components/card IView Weapp.png 快速上手搭建 快速上手.png iView Weapp 的代码 将源代码下载下来,然后将dict放到自己的项目中去。 iView Weapp 的代码.png 小程序中添加iView Weapp 将di…...
nginx 核心功能 02
目录 1. 正向代理 1.1 编译安装 Nginx 1.2 配置正向代理 2. 反向代理 2.1 配置nginx七层代理 2.2 配置nginx四层代理 3. Nginx 缓存 3.1 缓存功能的核心原理和缓存类型 3.2 代理缓存功能设置 4. Nginx rewrite 和正则 4.1 Nginx正则 4.2 nginx location 4.3 Rewri…...
LeetCode 102题解 | 二叉树的层序遍历
二叉树的层序遍历 一、题目链接二、题目三、算法原理四、编写代码 一、题目链接 二叉树的层序遍历 二、题目 三、算法原理 本题要求把结果放在不规则的二维数组里,即每一层二叉树的数值放在一行数组中。 回顾之前的层序遍历是借助队列实现的,是不考虑…...
Flink基础整理
文章目录 前言1.Flink系统架构2.编程模型(API层次结构)3.DataSet和DataStream区别4.Flink的批流统一5.Flink的状态后端6.Flink有哪些状态类型7.Flink并行度前言 提示:下面是根据网络或AI整理: 1.Flink系统架构 用户在客户端提交作业(Job)到服务端。服务端为分布式的主从…...
C++23 新特性:为 std::pair 的转发构造函数添加默认实参
文章目录 1\. 背景:std::pair 的转发构造函数2\. C23 的改进:添加默认实参示例代码 3\. 带来的好处3.1 更简洁的代码3.2 提高代码的可维护性3.3 与 std::optional 和 std::variant 的协同 4\. 实现细节示例实现(简化版) 5\. 使用场…...
JavaScript性能优化实战(9):图像与媒体资源优化
引言 在当今视觉驱动的网络环境中,图像和媒体资源往往占据了网页总下载量的60%-80%,因此对图像和媒体资源进行有效优化已成为前端性能提升的关键领域。尽管网络带宽持续提升,但用户对加载速度的期望也在不断提高,特别是在移动设备和网络条件不稳定的场景下。 本文作为Jav…...
施磊老师rpc(四)
文章目录 rpc网络服务简介RpcProvider 的设计目标Eventloop不使用智能指针-弃用RpcProvider类似于集群的服务器provider网络实现**src/include/rpcprovider.h****src/include/mprpcapplication.h****src/rpcprovider.cc** 错误1错误2-重点**本项目的 mprpc 是动态库, muduo..是…...
Java学习手册:MyBatis 框架作用详解
一、MyBatis 简介 MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发。它通过将 Java 对象与数据库表之间的映射关系进行配置,使得开发者可以使用简单的 SQL 语句和 Java 代码来完成复杂的数据操作。MyBatis 支持自定义 SQL 语句,提供了灵…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】3.1 数据质量评估指标(完整性/一致性/准确性)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 数据质量评估核心指标:完整性、一致性、准确性实战解析3.1 数据质量评估指标体系3.1.1 完整性:数据是否存在缺失1.1.1 核心定义与业务影响1.1.2 检测…...
分布式系统中的 ActiveMQ:异步解耦与流量削峰(一)
一、引言 在当今数字化时代,分布式系统已成为构建大规模应用的关键架构。随着业务的快速发展和用户量的急剧增长,分布式系统面临着诸多挑战,其中异步通信、系统解耦和流量削峰是亟待解决的重要问题。 以电商系统为例,在秒杀活动中…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】2.5 事务与锁机制(ACID特性/事务控制语句)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 PostgreSQL 事务与锁机制深度解析:ACID 特性与事务控制全流程2.5 事务与锁机制2.5.1 ACID 特性与实现原理2.5.1.1 ACID 核心概念2.5.1.2 MVCC(多版本并发控制)与WAL(预写式日志)协同效应2.5.2 事务…...
STM32教程:ADC原理及程序(基于STM32F103C8T6最小系统板标准库开发)*详细教程*
前言: 本文章介绍了STM32微控制器的ADC外设,介绍了ADC的底层原理以及基本结构,介绍了ADC有关的标准库函数,以及如何编写代码实现ADC对电位器电压的读取。 可以根据基本结构图来编写代码 大体流程: 1、开启RCC时钟&am…...
RabbitMQ 深度解析:从核心组件到复杂应用场景
一.RabbitMQ简单介绍 消息队列作为分布式系统中不可或缺的组件,承担着解耦系统组件、保障数据可靠传输、提高系统吞吐量等重要职责。在众多消息队列产品中,RabbitMQ 凭借其可靠性和丰富的特性,在企业级应用中获得了广泛应用。本研究报告将全…...
linux 使用nginx部署ssl证书,将http升级为https
前言 本文基于:操作系统 CentOS Stream 8 使用工具:Xshell8、Xftp8 服务器基础环境: nginx - 请查看 linux 使用nginx部署vue、react项目 所需服务器基础环境,请根据提示进行下载、安装。 1.下载证书 以腾讯云为例ÿ…...
iview 分页改变每页条数时请求两次问题
问题 在iview page分页的时候,修改每页条数时,会发出两次请求。 iview 版本是4.0.0 原因 iview 的分页在调用on-page-size-change之前会调用on-Change。默认会先调用on-Change回到第一页,再调用on-page-size-change改变分页显示数量 此时就会…...
【Hive入门】Hive与Spark SQL深度集成:Metastore与Catalog兼容性全景解析
目录 引言 1 元数据管理体系架构对比 1.1 Hive Metastore架构解析 1.2 Spark Catalog系统设计 2 元数据兼容性深度剖析 2.1 元数据模型映射关系 2.2 元数据同步机制 3 生产环境配置指南 3.1 基础兼容性配置 3.1.1 Spark连接Hive Metastore 3.1.2 多引擎共享配置 3.…...
C#与西门子PLC通信:S7NetPlus和HslCommunication使用指南
西门子S7协议是用来和PLC进行通讯的一个协议,默认端口是102,数据会保存在一个个DB块中,比较经典的用法是一个DB块专门用来读取,一个用来写入。 DB(数据块) {块号}.DBX/DBD/DBW{字节地址}.{位偏移} 1、数据…...
湖北理元理律师事务所:法律科技融合下的债务管理实践
随着债务纠纷数量攀升,如何通过合法途径化解债务风险成为社会焦点。湖北理元理律师事务所作为国家司法局注册的债事服务机构,尝试以“法律技术”重构传统服务模式,为债务人提供系统性解决方案。 专业化服务架构 该律所设立客服、运营、法务…...
Spring Cloud Gateway MVC 基于 Spring Boot 3.4 以 WAR 包形式部署于外部 Tomcat 实战
一、引言 随着微服务架构的广泛应用,Spring Cloud Gateway 作为网关层的核心组件,为服务间的通信与流量管理提供了强大支持。spring-cloud-starter-gateway-mvc 则进一步助力开发者以熟悉的 MVC 模式进行网关开发。同时,将项目以 WAR 包形式…...
LLM论文笔记 27: Looped Transformers for Length Generalization
Arxiv日期:2024.9.25 关键词 长度泛化 transformer结构优化 核心结论 1. RASP-L限制transformer无法处理包含循环的任务的长度泛化 2. Loop Transformer显著提升了长度泛化能力 Input Injection 显著提升了模型的长度泛化性能,尤其在二进制加法等复杂…...
PCIe TLP | 报头 / 包格式 / 地址转换 / 寄存器 / 配置空间类型
注:本文为 “PCIe TLP” 相关文章合辑。 英文引文,机翻未校。 中文引文,未整理去重。 图片清晰度受引文原图所限。 略作重排,如有内容异常,请看原文。 PCIe - TLP Header, Packet Formats, Address Translation, Conf…...
《AI大模型应知应会100篇》第46篇:大模型推理优化技术:量化、剪枝与蒸馏
第46篇:大模型推理优化技术:量化、剪枝与蒸馏 📌 目标读者:人工智能初中级入门者 🧠 核心内容:量化、剪枝、蒸馏三大核心技术详解 实战代码演示 案例部署全流程 💻 实战平台:PyTor…...
C++/SDL 进阶游戏开发 —— 双人塔防(代号:村庄保卫战 20)
🎁个人主页:工藤新一 🔍系列专栏:C面向对象(类和对象篇) 🌟心中的天空之城,终会照亮我前方的路 🎉欢迎大家点赞👍评论📝收藏⭐文章 文章目录 三…...
【Python生成器与迭代器】核心原理与实战应用
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比二、实战演示环境配置要求核心代码实现案例1:自定义迭代器类案例2:生成器函数案例3:生成器表达式运行结果验证三、性能对比测试方法论量…...
2025年最新嵌入式开发STM32单片机详细教程(更新中)
ARM 处理器架构 ARM 处理器从 1984 ARM-1 发展到 2004 ARM-11 之后,放弃数字命名,用 cortex 来命令处理器产品。 Cortex-A系列 主打高性能 手机,平板,智能电视等 Cortex-R系列 主打实时 汽车,工业控…...
neatchat轻量级丝滑的ai模型web客户端
NeatChat 人工智能模型对话web客户端 前言 此项目是nextchat分支,相比原者更加简洁流畅。 部署 docker部署 name: next-chat services:chatgpt-next-web:ports:- 8080:3000environment:- OPENAI_API_KEYsk-xx543Ef3d- BASE_URLhttps://api.ai.com- GOOGLE_API_K…...
学习黑客分析案例
▶️ Day 2 任务 – 「怪物图鉴」实战 选一条最新安全事件(国内外均可,建议 1 年内) 例:CVE-2024-21887 Ivanti VPN RCE 用下列表格框架,3 句话归纳它的“派系”“CIA 受击点”“一句话原理”: 攻击流派…...