前言
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 };
vector<double>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (double e : v)
cout << e << " ";
cout << endl;
for (size_t i = 0; i < v.size(); i++)
cout << v[i] << " ";
cout << endl;
return 0;
}
运行结果如下:
3.数据的插入与删除
针对数据的操作有如下四种,如果对STL熟悉的话只看函数名应该就可以知道这个函数的功能。
代码如下:
template<class T>
void print(const vector<T>& v)
{
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);
vector<int>::iterator it = find(v.begin(), v.end(), 30);
if (it != v.end())
{
v.insert(it, -20);
}
print(v);
v.erase(it);
print(v);
return 0;
}
运行结果如下:
4.resize和reserve
对于一个顺序表v,size是当前v中的数据个数,capacity是v中能存储的最大数据个数。
代码如下:
int main()
{
vector<int> 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()
:_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:
Vector& operator=(const Vector<T>& v)
{
if (this != &v)
{
delete[] _start;
_start = new T[v.capacity()];
_endOfStorage = _start + v.capacity();
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:
void swap(Vector<T> v)
{
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_endOfStorage, v._endOfStorage);
}
Vector& operator=(Vector<T> v)
{
this->swap(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;
}
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();
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:
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)
{
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:
void insert(Iterator pos, const T& x)
{
size_t len = pos - _start;
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)
{
Iterator it = pos;
for (; it < _finish - 1; it++)
*it = *(it + 1);
_finish--;
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 = v.erase(it);
else
it++;
}
print(v);
return 0;
}
运行如下:
感谢阅读,如有错误请批评指正
|