文章目录
- 前言
- 一、vector的介绍
- 二、vector的使用
- 1.构造函数
- 2.遍历
- 3.数据的插入与删除
- 3.resize和reserve
- 三、vector的模拟实现
- 1.成员变量
- 2.构造函数
- (1)默认构造函数
- (2)拷贝构造
- (3)构造含有n个val的vector
- 3.析构函数
- 4.赋值运算符重载
- 5.一些简单的函数
- 6.reserve和resize
- (1)reserve
- (2)resize
- 7.数据的增删查改
- (1)push_back
- (2)pop_back
- (3)insert
- (4)erase
- 四、迭代器失效问题
- 1.引起其底层空间改变的操作
- 2.指定位置元素的erase操作
- 感谢阅读,如有错误请批评指正
前言
vector和string一样,都是最简单的容器,逻辑和实现上都非常类似,所以本篇内容很多需要注意的地方在【C++】STL:string的模拟实现和【C++】STL:string的使用中都已提到过,读者如有问题可阅读这两篇文章对应位置。
一、vector的介绍
vector是表示可变大小数组的序列容器,采用连续的存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。包含在< vector >头文件下。
二、vector的使用
STL之所以好用,不仅是因为它提供了常见的数据结构容器及操作,更是因为对容器的操作大部分相似,学习成本很低。
在【C++】STL:string的使用中已经介绍了string的操作,而vector的操作也大同小异,所以下面只给出vector使用的例子,而不再系统地讲解每个操作函数的功能。
1.构造函数
常用的构造一个vector有如下五种方法,通过调试可以看到它们具体的内容。
还有一种特殊的结构vector<vector< int >>,如果是第一次见到可能不容易理解,其实这就是一个二维数组。
2.遍历
这里的遍历仍有三种方法,迭代器是STL中所有容器都支持的遍历方式;范围for是一种简便写法,实质仍是迭代器,也是所有容器都可用;下标+[]是string和vector才能用的,因为它们的地址空间是连续的。同时这三种方法均是可读、可写。
代码如下:
int main()
{
vector<double> v = { 1.1, 2.2, 3.14, 6.66 };
//1.迭代器
vector<double>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//2.范围for
for (double e : v)
cout << e << " ";
cout << endl;
//3.下标+[]
for (size_t i = 0; i < v.size(); i++)
cout << v[i] << " ";
cout << endl;
return 0;
}
运行结果如下:
3.数据的插入与删除
针对数据的操作有如下四种,如果对STL熟悉的话只看函数名应该就可以知道这个函数的功能。
代码如下:
//一个打印v中数据的函数
template<class T>
void print(const vector<T>& v)
{
//传的v是const修饰的,所以这里要用const迭代器
vector<T>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(30);
v.push_back(4);
v.push_back(6);
print(v);
v.pop_back();
print(v);
//find为C++算法库中的查找函数
//给出迭代器区间和要查找的数据即可查找
vector<int>::iterator it = find(v.begin(), v.end(), 30);
if (it != v.end())//找到了
{
//在30之前插入-20
v.insert(it, -20);//insert的第一个参数要用迭代器
}
print(v);
//删除it前面的数据
v.erase(it);
print(v);
return 0;
}
运行结果如下:
3.resize和reserve
对于一个顺序表v,size是当前v中的数据个数,capacity是v中能存储的最大数据个数。
代码如下:
int main()
{
vector<int> v;
//size:当前v中的数据个数
//capacity:v中能存储的最大数据个数
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl << endl;
v.resize(30);
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl << endl;
v.reserve(50);
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl << endl;
return 0;
}
运行结果如下:
三、vector的模拟实现
1.成员变量
在数据结构(一):顺序表中实现顺序表时,使用的结构是一个动态开辟的数组+size+capacity,而STL底层实现vector时使用三个指针完成的,这里模仿它来完成。
代码如下:
template<class T>
class Vector
{
public:
//...
private:
T* _start;
T* _finish;
T* _endOfStorage
};
_start指向这段数据的开始,_finish指向有效数据的末尾的下一个位置,_endOfStorage指向_start开辟的整个空间的末尾。具体如下。
由上图很容易看出,size = _finish - _start,capacity = _endOfStorage - _start。
由于vector存放数据时地址是连续的,所以迭代器也是原生指针,所以可将上面的代码封装如下:
template<class T>
class Vector
{
public:
typedef T* Iterator;
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
2.构造函数
这里实现多个构造函数重载。
在构造函数这里使用了一些下面会实现的函数,如果对这些函数不是很明白,可以先往下看,明白这些函数的底层实现后再回来看这里。
(1)默认构造函数
一个几乎什么都没做的默认构造函数。
代码如下:
template<class T>
class Vector
{
public:
typedef T* Iterator;
//把新构造的vector的三个指针全置空
Vector()
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
(2)拷贝构造
即用一个已经实例化的vector对象v构造一个新的vector对象this。先给this开辟v的大小的空间(reserve函数),再依次把v的数据插入到*this内(这里用push_back)。
代码如下:
template<class T>
class Vector
{
public:
Vector(const Vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
this->reserve(v.capacity());
for (auto e : v)
this->push_back(e);
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
(3)构造含有n个val的vector
先用resize函数开辟n个空间,再依次把其值修改为val。或者可以借用上面的思路:先用reserve函数将vector的最大数据个数修改为n,进行n次this->push_back(val)。
代码如下:
template<class T>
class Vector
{
public:
Vector(size_t n, T val)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
resize(n);
Iterator it = _start;
while (it != _finish)
{
*it = val;
it++;
}
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
3.析构函数
vector的析构比较简单,只需要释放空间并把三个指针置空即可。
代码如下:
template<class T>
class Vector
{
public:
~Vector()
{
//释放空间
if (_start != nullptr)
delete[] _start;
//把三个指针置空
_start = nullptr;
_finish = nullptr;
_endOfStorage = nullptr;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
4.赋值运算符重载
写法1:处理空间,处理指针,填充数据。
代码如下:
template<class T>
class Vector
{
public:
//注意这里传const+引用,也就是说这个v就是函数外部的v,这里不能被修改
//要与下面第2种写法区分
Vector& operator=(const Vector<T>& v)
{
//防止自己给自己赋值
if (this != &v)
{
//处理空间
delete[] _start;
_start = new T[v.capacity()];
//处理指针
_endOfStorage = _start + v.capacity();
//填充数据
//注意这里temp遍历v的空间,_finish遍历*this的空间
Iterator temp = v._start;
for (_finish = _start; temp < v._finish; _finish++, temp++)
*_finish = *temp;
}
return *this;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
写法2:用一个临时产生的对象来与*this的内容交换,具体可看代码中的注释。
代码如下:
template<class T>
class Vector
{
public:
//重载一个这里需要的swap函数
void swap(Vector<T> v)
{
//加::表示下面的swap函数时全局域内的
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_endOfStorage, v._endOfStorage);
}
//以v2 = v1为例
//这里传参的v本身就是外部函数调用时v1的一个临时(深)拷贝,生命周期仅在这一函数内
//由于v是v1的深拷贝,直接交换v与*this(也即v2)的内容即可
Vector& operator=(Vector<T> v)
{
//将三个指针全部交换,*this内的内容就和
this->swap(v);
//*this原来的内容现在在v内,函数结束调用后v自动调用析构函数清理掉这些内容
return *this;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
5.一些简单的函数
这里实现一些逻辑简单的函数,如判空(empty),迭代器(begin,end)和const迭代器,size和capacity,operate[]等函数。
代码如下:
template<class T>
class Vector
{
public:
typedef T* Iterator;
typedef const T* const_Iterator;
bool empty() const
{
return _start == _finish;
}
Iterator begin()
{
return _start;
}
Iterator end()
{
return _finish;
}
const_Iterator begin() const
{
return _start;
}
const_Iterator end() const
{
return _finish;
}
size_t capacity()
{
return _endOfStorage - _start;
}
size_t capacity() const
{
return _endOfStorage - _start;
}
size_t size()
{
return _finish - _start;
}
size_t size() const
{
return _finish - _start;
}
//注意operator[]可读可写,所以返回值要加引用
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
6.reserve和resize
(1)reserve
reserve函数即预留出一定的空间存放数据,其中要注意一个细节。
代码如下:
template<class T>
class Vector
{
public:
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t sz = size();//注意:sz要提前拿到
if (_start != nullptr)
{
//拷贝数据
for (size_t i = 0; i < sz; i++)
tmp[i] = _start[i];
delete[] _start;//释放旧空间
}
//修改指针
_start = tmp;
_finish = _start + sz;
_endOfStorage = _start + n;
}
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
注意:sz要在if判断前拿到,if判断结束后,原来的空间被释放,_start也随之被修改,而_finish和_endOfStorage没变,相减得到的size和capacity都是不对的。
(2)resize
resize与string的resize类似,且这里不需要处理\0,可类比实现,这里不多赘述。
代码如下:
template<class T>
class Vector
{
public:
// 给缺省值,T为int时为0,为double时为0.0,为string时为"",其它类型也可类比
void resize(size_t n, const T& val = T())
{
if (n < size())
_finish = _start + n;
else
{
if (n > capacity())
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
7.数据的增删查改
顺序表数据的增删查改的细节在数据结构(一):顺序表中已经介绍的很详细,且在数据结构中多次处理,对于之前提到的细节这里不多赘述。
(1)push_back
代码如下:
template<class T>
class Vector
{
public:
void push_back(const T& x = T())
{
//顺序表已满,需扩容
if (_finish == _endOfStorage)
{
//这里以每次2倍扩容为例
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
_finish++;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
(2)pop_back
代码如下:
template<class T>
class Vector
{
public:
void pop_back()
{
assert(!empty());
_finish--;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
这里要提一下,STL中对于vector只有push_back和pop_back,而没有push_front和pop_front,因为这两个函数实现时需要挪动数据,导致O(N)的时间复杂度,效率太极,所以不提供。如果真的需要使用,可以调用insert和erase。
(3)insert
在某一位置前插入元素,注意位置要传迭代器作为参数。
代码如下:
template<class T>
class Vector
{
public:
//在pos位置前插入x
void insert(Iterator pos, const T& x)
{
size_t len = pos - _start;//pos之后元素个数
if (_finish == _endOfStorage)//需要扩容
{
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
pos = len + _start;
}
//挪动数据
Iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = x;
_finish++;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
(4)erase
代码如下:
template<class T>
class Vector
{
public:
Iterator erase(Iterator pos)
{
//从pos位置开始用后面的数据覆盖前面的数据
Iterator it = pos;
for (; it < _finish - 1; it++)
*it = *(it + 1);
_finish--;
//注意返回pos,这个在后面会用到
return pos;
}
private:
Iterator _start;
Iterator _finish;
Iterator _endOfStorage;
};
四、迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层就是一个指针或是对指针进行了封装,比如:vector的迭代器就是原生指针T*。
因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了却继续对其访问,而使用一块已经被释放的空间,造成的后果是程序可能会崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
1.引起其底层空间改变的操作
引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、push_back等,因为这些函数在实现时会涉及扩容的问题,可能会引起底层空间改变。
这里以resize为例解释。
代码如下:
下图中it原来指向v.begin(),但resize后v的空间修改了,it已经不等于修改后的v.begin()了,而原来it指向的空间已经被释放,再次访问就会造成崩溃。
图解如下:
解决方法就是在空间被修改后把指向原来地址的变量都更新(这里只需要更新it),这样即可正常运行。
2.指定位置元素的erase操作
下面一段代码希望通过迭代器来删除顺序表中的偶数元素,但是是有问题的。
代码如下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
v.erase(it);
it++;
}
print(v);
return 0;
}
程序崩溃
图解如下:
如下修改后可正常运行:
代码如下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)//是偶数就删除并更新it
it = v.erase(it);//更新it,这也是为什么erase的返回值是迭代器
else//是奇数就往后遍历
it++;
}
print(v);
return 0;
}
运行如下: