C++ vector及实现自定义vector以及allocator和iterator方式
作者:Dutkig
简介
作用:vector是一个能够存放任意类型的 动态数组
,能够增加和压缩数据。
vector是表示可以改变大小的数组的序列容器。
与数组一样,vector对元素使用连续的存储位置,这意味也可以使用指向其元素的常规指针上的偏移量来访问它们的元素,并且与在数组中一样高效。
但是与数组不同,它们的大小可以动态变化,容器会自动处理它们的存储。
在内部,vector 使用个动态分配的数组来存储它们的元素。这个数组可能需要重新分配,以便在插入新元素时增大大小,这意味着分配一个新数组并将所有元素移动到其中。就处理时间而言,这是一项相对昂贵的任务,因此,向量不会在每次向容器添加元素时重新分配。
相反,vector 容器可以分配一些 额外的存储空间以适应可能的增长 , 因此容器的实际容量可能大于严格需要的存储容量(即容器的大小)。
对于不同的插入位置,可以在不同的时间间隔内实现不同的插入策略,但只能在不同的位置上实现内存大小的平衡。
因此,与array相比,向量消耗更多的内存,以 换取管理存储和以高效方式动态增长
的能力。与其他动态序列容器( deques
、 list
和 forward_list
)相比,vectors 可以非常高效地访问其元素(就像数组一样),并相对高效地从其末尾添加或删除元素。
对于涉及在结尾以外的位置插入或删除元素的操作,它们的性能比其他操作差,迭代器和引用的一致性也不如list 和forward_list。
注意 :
使用时有以下的注意事项:
- 1.如果你要表示的向量长度较长(需要为向量内部保存很多数),容易导致内存泄漏,而且效率会很低;
- 2.Vector作为函数的参数或者返回值时,需要注意它的写法:
double Distance(vector<int>&a, vector<int>&b)
其中的“ &
”绝对不能少!!!
- 3.建立一个vector,int为数组元素的数据类型,test为动态数组名
简单的使用方法如下:
vector<int>test;//建立一个vector test.push_back(1); test.push_back(2);//把1和2压入vector,这样test[0]就是1,test[1]就是2
基本操作
- 头文件#include.
- 创建vector对象,
vector<int> vec
; - 尾部插入数字:vec.
push_back
(a); - 使用下标访问元素,cout<<
vec[0]
<<endl;记住下标是从0开始的。 - 使用迭代器访问元素
vector<int>::iterator it; for(it=vec.begin();it!=vec.end();it++) cout<<*it<<endl;
- 插入元素: vec.insert(vec.begin()+i,a);在第i+1个元素前面插入a;
- 删除元素: vec.erase(vec.begin()+2);删除第3个元素
- vec.erase(vec.begin()+i,vec.end()+j);删除区间[i,j-1];区间从0开始
- 向量大小:vec.size();
- 清空:vec.clear();
注意 :
vector的元素不仅仅可以是int,double,string,还可以是结构体,但是要注意:结构体要定义为全局的,否则会出错
算法
1.逆序排列: 使用 reverse
将元素翻转:需要头文件 #include<algorithm>
、 reverse(vec.begin(),vec.end());
将元素翻转,即逆序排列!
2.sort排序:同样需要引入头文件 #include<algorithm>
,sort(vec.begin(),vec.end());(默认是按升序排列,即从小到大).
若想降序排列,就必须重写排序比较函数按降序排列,定义排序比较函数:
bool Comp(const int &a,const int &b) { return a>b; }
调用时: sort(vec.begin(),vec.end(),Comp)
,这样就降序排序。
3.拷贝,例如:a中的从a.begin()(包括它)到a.end()(不包括它)的元素复制到b中,从b.begin()+1的位置(包括它)开始复制,覆盖掉原有元素
copy(a.begin(),a.end(),b.begin()+1);
4.预留容量: reserve()
int main() { vector<int> vec; vec.reserve(100); //预留100的容量 cout<<vec.size()<<" "<<vec.capacity()<<endl; // 0 100 //若要输出第一个元素,则会报错 return 0; }
输出的三种方式
vector<float> vecClass; int nSize = vecClass.size();
方法1:下标访问
for(int i=0;i<nSize;i++) { cout<<vecClass[i]<<" "; } cout<<endl;
方法2: .at(i)
访问
for(int i=0;i<nSize;i++) { cout<<vecClass.at(i)<<" "; } cout<<endl;
方法3:迭代器访问,但是输出某一个指定数值时不方便
for(vector<float>::iterator it = vecClass.begin();it!=vecClass.end();it++) { cout<<*it<<" "; } cout<<endl;
二维使用
#include "stdafx.h" #include <cv.h> #include <vector> #include <iostream> using namespace std; int main() { using namespace std; int out[3][2] = { 1, 2, 3, 4, 5, 6 }; vector <int*> v1; v1.push_back(out[0]); v1.push_back(out[1]); v1.push_back(out[2]); cout << v1[0][0] << endl;//1 cout << v1[0][1] << endl;//2 cout << v1[1][0] << endl;//3 cout << v1[1][1] << endl;//4 cout << v1[2][0] << endl;//5 cout << v1[2][1] << endl;//6 return 0; }
再来看一个例子,应用实例:
#include<vector> void ForwardPrint(const vector<int> &ar) //ar为引用,为了防止改变设置const { vector<int> :: const_iterator it = ar.begin(); //迭代器也要加上const for(; it != ar.end(); it++) { cout<<*it<<" "; } cout<<endl; } int main() { int ar[] = {12,23,34,45,56,67,78,89,90,100}; int n = sizeof(ar) / sizeof(ar[0]); vector<int> veca; vector<int> vecb(10); //有10个数据,都为0 vector<int> vecc(10, 23);//有10个数据,每个数都是23 vector<int> vecd(ar, ar+n);//算头不算尾,将ar中的数据从12到100存入vecd中 vector<int> vece = {12,23,34,45,56,67,78,89,90,100}; //数组形式将值存入vece中 ; vs2019 C11 vector<int> vecf {12,23,34,45,56,67,78,89,90,100}; vector<int> :: iterator it = vece.begin(); //it迭代器,要比指针安全。当越界时,会有检查 for(; it != vece.end(); it++) { *it += 10; //也可以通过迭代器改变vector中的值 cout<< *it <<endl; } //逆向迭代器,逆向打印 vector<int>:: reverse_iterator rit = vecf.rbegin(); for(; it != vecf.rend(); it++) { cout<<*rit<<" "; } ForwardPrint(vece); return 0; }
了解了vector的基本使用后,我们来对这一容器进行深层次的理解:
初步
vector会分配一些额外的存储空间,以适应可能的增长,容量要大于元素个数。
.size_type max_size() const {} //返回可容纳的最大元素个数 size_type clear() {} //仅清除元素,未清除空间 void shrink_to_fit() {} //当不清除数量clear()时,调用此函数缩小内存
shrink_to_fit会将容量调整与元素个数相等。
插入
vec.insert(it, 23); //在第一个元素位置插入23 vec.insert(it, 10, 23); //在第一个元素位置插入10个23 vec.insert(it, ar, ar+n) //算头不算尾,可以插入整个数组 vec.insert(it, {1,2,3,4,5,6}) //插入一个列表元素
删除
it = vec.begin(); vec.erase(it); //删除首元素 vec.erase(it, it+5); //连续删除
空间预留
reserver函数用来给vector预分配存储区大小,即 capacity
的值,但是没有给这段内存进行初始化。reserve的参数n是推荐预分配内存的大小,实际分配的可能等于或大于这个值。
当调用函数时,n的值如果大于capacity的值,就会重新分配内存,使得capacity的值会大于n。这样,当调用 push_ back
函数便得size超过原来的默认分配的capacity值时避免了内存重分配开销。
需要注意的是: reserve 函数分配出来的内存空间未初始化对象,只是表示vector可以利用这部分内存空间,但vector不能有效地访问这些内存空间,如访问的时候就会出现越界现象,导致程序崩溃。 改变容器大小 resize函数重新分配大小,改变容器的大小,并且创建对象。
void resize(const size_type Newsize); void resize(const size_type Newsize, const T& x);
对于下面这个vector,我们来看看使用resize会发生什么?
vector<int> vec {12,23,34,5,4,55,67}
①当n小于当前size()值时候,vector首先会减少size()值保存前n个元素,然后将超出n的元素删除.
eg: vec.resize(5, 100),
则不会把100插入,并且会删除至5个元素。
②当n大于当前size()值时候,vector会插入相应数量的元素使得size()值达到n,并对这些元素进行初始化,如果调用上面的第二个resize函数,指定val,vector会用val来初始化这些新插入的元素。 eg: vec.resize(15,100)
③当n大于capacity()值的时候,会自动分配重新分配内存存储空间。
迭代器
vector<int> ::iterator it = vec.begin(); vec.push_back(); //前期的迭代器已失效 vec.pop_back(); //也会导致失效 cout<<*it<<endl; vec.insert(it, 23); cout<<*it<<endl; //迭代器失效
只要插入或删除数据,都会使前期的迭代器失效 ,必须对迭代器 再次 初始化或者赋值。
了解了这些之后,我们就来实现一个自己的vector:在STL中,我们可以查看系统自带的vector的实现,主要包含了三个指针:
- 指向数组起始位置的指针;
- 指向数组中最后一个有效元素位置的后继位置;
- 指向数组空间的后继位置。
代码如下:
template<class T> class vector { public: vector(int size = 10) { _first = new T[size]; _last = _first; _end = _first + size; } ~vector() { delete[] _first; _first = _last = _end = nullptr; } vector(const vector<T> &rhs)//防止浅拷贝的拷贝构造 { int size = rhs._end - rhs._first; _first = new T[size]; //拷贝有效元素 int len = rhs._last - rhs._first; for(int i = 0;i<len;++i) { _first[i] = rhs._first[i]; } _last = _first + len; _end = _first + size; } vector<T>& operator=(const vector<T> &rhs) { if(this == &rhs) { return *this; } delete[] _first; int size = rhs._end - rhs._first; _first = new T[size]; int len = rhs._last - rhs._first; for(int i = 0;i<len;++i) { _first[i] = rhs._first[i]; } _last = _first + len; return *this; } void push_back(const T &val) { if(full()) expand(); *_last++ = val; } void pop_back() { if(empty()) return; --_last; } T back()const//返回容器末尾的元素的值 { return *(_last -1); } bool full()const{return _last == _end;} bool empty()const{return _first == _last;} int size()const{return _last - _first;} T & operator[](int index) { if(index < 0 || index >= size()) { throw "OutofRangeException"; } return _first[index]; } private: T* _first; //数组起始位置 T* _last; //指向数组中有效元素的后继位置 T* _end; //指向数组空间的后继位置 void expand()//容器的二倍扩容操作接口 { int size = _end - _first; T *ptmp = new T[2*size]; for(int i = 0;i < size;++i) { ptmp[i] = _first[i]; } delete[] _first; _first = ptmp; _last = _first + size; _end = _first +2*size; } };
但是上述代码还是存在一些问题:
- 对于一个容器来说,里面存放的数据类型可以是系统自带的默认类型,比如:int、char等等,也可以是自己定义的class XXX类型。
- 对于默认类型来说,我们上面的代码是没有问题的(可以自行验证),但是对于自定义类型来说,就会出现问题:
比如我们加上自己定义的简单类型,分别给构造函数、析构函数、拷贝构造函数都加上打印信息:
class Test { public: Test() { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } Test(const Test&) { cout << "Test(const Test&)" << endl; } };
接下来用test类型实例化一下这个容器:
int main() { vector<Test> vec; return 0; }
结果如下:
我们会发现,我只是定义了一个空的容器,但是它却自动添加了十个对象。
解决上述问题的方法:Allocator空间配置器
系统自带的vector中:
可以看到,系统的实现,除了数据类型外,还有一个 allocator
。而这个,就是解决问题的办法了!我们再来回过头看看问题,其实问题主要出在了构造函数的new上:我们都知道new这个关键字会完成两项任务:
- 开辟空间
- 数据初始化(对自定义类型来说就调用构造函数)
可问题是,我们既然选择把它作为容器,那么就只需要它提供一个场所(空间),至于这个场所里存放什么数据,是由程序员来决定的,不应该由容器来擅作主张。
那么,问题的关键就在于将 开辟空间 和 构造对象 分离开。析构函数也不应该做上述delete操作,而是将 有效内存释放
即可,应该将 释放空间
和 析构对象
分离开。
因此,空间配置器应该有以下四个功能:
- 内存开辟
allocate
(底层调用malloc); - 内存释放
deallocate
(底层调用free); - 对象构造
construct
(调用构造函数); - 对象析构
destroy
(调用析构函数)。
// 定义容器的空间配置器,和C++标准库的allocator实现一样 template<typename T> struct Allocator { T* allocate(size_t size) // 负责内存开辟 { return (T*)malloc(sizeof(T) * size); } void deallocate(void* p) // 负责内存释放 { free(p); } void construct(T* p, const T& val) // 在指定内存上负责对象构造 { new (p) T(val); // 定位new } void destroy(T* p) // 负责对象析构 { p->~T(); // ~T()代表了T类型的析构函数 } };
于是,修改我们之前的vector如下:
template<typename T, typename Alloc = Allocator<T>> class vector { public: vector(int size = 10) { // 需要把内存开辟和对象构造分开处理 _first = _allocator.allocate(size); _last = _first; _end = _first + size; } ~vector() { // 析构容器有效的元素,然后释放_first指针指向的堆内存 for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作 } _allocator.deallocate(_first); // 释放堆上的数组内存 _first = _last = _end = nullptr; } vector(const vector<T>& rhs) { int size = rhs._end - rhs._first; _first = _allocator.allocate(size); int len = rhs._last - rhs._first; for (int i = 0; i < len; ++i) { _allocator.construct(_first + i, rhs._first[i]); } _last = _first + len; _end = _first + size; } vector<T>& operator=(const vector<T>& rhs) { if (this == &rhs) return *this; for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); // 把_first指针指向的数组的有效元素进行析构操作 } _allocator.deallocate(_first); int size = rhs._end - rhs._first; _first = _allocator.allocate(size); int len = rhs._last - rhs._first; for (int i = 0; i < len; ++i) { _allocator.construct(_first + i, rhs._first[i]); } _last = _first + len; _end = _first + size; return *this; } void push_back(const T& val) // 向容器末尾添加元素 { if (full()) expand(); _allocator.construct(_last, val); _last++; } void pop_back() // 从容器末尾删除元素 { if (empty()) return; // 不仅要把_last指针--,还需要析构删除的元素 --_last; _allocator.destroy(_last); } T back()const // 返回容器末尾的元素的值 { return *(_last - 1); } bool full()const { return _last == _end; } bool empty()const { return _first == _last; } int size()const { return _last - _first; } T & operator[](int index) { if(index < 0 || index >= size()) { throw "OutofRangeException"; } return _first[index]; } private: T* _first; // 指向数组起始的位置 T* _last; // 指向数组中有效元素的后继位置 T* _end; // 指向数组空间的后继位置 Alloc _allocator; // 定义容器的空间配置器对象 void expand() // 容器的二倍扩容 { int size = _end - _first; T* ptmp = _allocator.allocate(2 * size); for (int i = 0; i < size; ++i) { _allocator.construct(ptmp + i, _first[i]); } for (T* p = _first; p != _last; ++p) { _allocator.destroy(p); } _allocator.deallocate(_first); _first = ptmp; _last = _first + size; _end = _first + 2 * size; } };
我们接下来实现以下vector容器的迭代器:
注意事项 :
迭代器一般实现成容器的嵌套类型;因为不同的容器在其底层的数据结构是不一样的,由此我们迭代的方式也就不一样。
//迭代器 class iterator { public: iterator(T* p = nullptr):_ptr(p){} bool operator!=(const iterator &it)const { return _ptr != it._ptr; } void operator++()//迭代器的前置++运算符重载,不用后置++的原因是不用产生临时量 { _ptr++; } T & operator*() { return *_ptr; } const T & operator*()const { return *_ptr; } private: T* _ptr; }; //需要给容器提供begin和end方法 iterator begin(){return iterator (_first);} iterator end(){return iterator (_end);}
使用正常:
int main() { vector<int>vec; for(int i = 0;i<20;++i) { vec.push_back(rand()%100+1); } int size = vec.size(); for(int i = 0;i<size;++i) { cout<<vec[i]<<" "; } cout<<endl; cout<<"----------------------------------------------------------"<<endl; vector<int>::iterator it = vec.begin(); for(;it != vec.end();++it) { cout<<*it<<" "; } cout<<endl; cout<<"----------------------------------------------------------"<<endl; for(int val:vec)//foreach的底层原理也是通过容器的迭代器来实现容器遍历 { cout<<val<<" "; } cout<<endl; return 0; }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。