目录
一、vector类的组织
我们使用两个文件,一个是vector_class.h,另一个是test.c,头文件用于定义vector类,cpp文件编写测试代码。
vector类的底层也是顺序表,底层组织是三个指针,一个头,一个有效数据尾,另一个可用空间尾:
vector_class.h
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<stdio.h>
#include<assert.h>
namespace my_vector
{
template<class T>
class vector
{
public:
//成员函数
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
test.c
#include"vector_class"
void test()
{
//测试代码
}
int main()
{
test();
return 0;
}
二、默认成员函数
1.默认构造函数
默认为空,直接赋空指针
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
还可以实现一个初始化为n个val的构造函数
vector(size_type n, const value_type& val = value_type());
vector(size_t n, const T& val = T())//用数据类型的默认构造
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(n);
for (size_t i = 0; i <= n; i++)
{
push_back(val);
}
}
还可以实现一个用迭代器初始化的构造函数
vector(InputIterator first, InputIterator last);
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
while (frist != last)
{
push_back(*frist);
++frist;
}
}
2.拷贝构造函数
(1)传统写法1——接近底层
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
_start = new T[v.capacity()];//因为拷贝构造只需要内部的数据,所以开的空间也可以只开size()大小
_finish = _start + v.size();
_endofstorage = _start + v.capacity;//让尾部到其位置上
}
(2)传统写法2——不用白不用
既然我们在类中都实现了很多的功能了,为什么不使用呢?
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
(3)现代写法
// 用迭代器的构造函数
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
while (frist != last)
{
push_back(*frist);
++frist;
}
}
//拷贝构造函数——现代写法
vector(vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
vector<T> temp(v.begin(), v.begin());
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
3.析构函数
释放内存,指针和变量置零
~vector()
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
二、元素访问
[]的重载分为可读可写和只读的两个,其实at的实现也很类似,不写实现了。
//可以改
T& operator[](size_t pos)
{
assert(_start + pos < _finish);
return *(_start + pos);
}
//不可以改
const T& operator[](size_t pos) const
{
assert(_start + pos < _finish);
return *(_start + pos);
}
三、容量操作
1.容量与有效数据
size_t size() const;——————返回有效数据的长度
size_t capacity() const;——————返回开辟空间的大小
bool empty() const;——————检测当前string是否是空字符串
头指针减尾指针计算元素个数,每一个都给this加上const最好,不会修改数据
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _endofstorage - _start;
}
bool empty()
{
if (_start == _finish)
return true;
else
return false;
}
2.改变空间容量
(1)reserve函数
reserve的空间大于capacity:扩容到新空间大小
reserve的空间小于等于capacity:不进行操作
只用异地扩容
void reserve(size_t n)
{
if (n > _capacity())
{
T* temp = new T[n + 1];
size_t old_size = size();
if (_start)
{
for (int i = 0; i < old_size; ++i)
{
temp[i] = _start[i];
}
delete[] _start;
}
_endofstorage = temp + n;
_start = temp;
_finish = temp + old_size;
}
}
(2)resize函数
三种情况:
当n大于capacity:扩容并在后面填充指定数据
当n小于capacity且大于finish:直接填充数据
当n小于finish:删除多余数据
void resize(size_t n, T val = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())
{
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
else
{
_finish = _start + n;
}
}
四、迭代器
迭代器(iterator)是一种可以遍历容器中元素的数据类型。由于只有少数容器支持下标访问元素,所以C++设计了迭代器,可以通过迭代器指向你想访问容器的元素地址,通过解引用的模式打印出元素值,和我们所熟知的指针极其类似。
迭代器的头和尾(iterator/const_iterator):begin()是指向首元素的迭代器,而end()是指向尾元素的后一个位置的迭代器。同样rbegin()是指向尾元素的迭代器,而rend()是指向首元素的前一个位置的迭代器。
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
五、数据的增删查改
1.尾插一个元素
和顺序表一样,先检查容量,不够就扩容,够就直接插数据。
void push_back(const T& e);
这里的扩容我选择了2倍扩容, 也可以选择1.5倍扩容,Visual Studio的vector选择1.5倍扩容,而Linux机器,也就是g++选择2倍扩容。
//插入字符
void push_back(const T& e)
{
if (_finish == _endofstorage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_finish = e;
++_finish;
}
2.尾删一个元素
尾删元素很简单,空的不能删,有效数据减一。
void pop_back();
void pop_back()
{
assert(!empty());
--_finish;
}
3.pos位置插入一个字符
与之前尾插的扩容原则一致,不过需要将包括pos后的数据向后挪动一位然后插入数据,有效数据加一。在vector中没有提供头插,头插可以通过insert(begin(), n)实现。
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos < _finish);
size_t capacity = _finish - _start;
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapaciity = 0 ? 4 : 2 * capacity;
reserve(newcapaciity);
//扩容导致迭代器失效
pos = _start + len;
}
iterator a = _finish;
while (a != pos)
{
*(a - 1) = *a;
--a;
}
*pos = val;
++_finish;
return pos;
}
4.清除一个迭代器位置的数据
erase清除数据用从前到后一个一个数据向前覆盖的方式就可以了,在vector中也没有提供头删,头删可以通过erase(begin())实现。
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator a = pos + 1;
while (a != _finish)
{
*(a - 1) = *a;
}
--_finish;
return pos;
}
5.清除所有数据
不动空间,将finish移到start就好了
void clear()
{
_finish = _start;
}
六、迭代器失效
1.什么是迭代器失效
迭代器的出现就是让我们在使用中能够在底层数据结构不同的条件下也能正常操作,不破坏封装,其底层可能是一个指针,也可能是对指针的封装。vector的迭代器在我们的模拟实现中就可以使用原生指针,当迭代器底层指针指向的位置已经不指向有效数据时,就发生了迭代器失效。迭代器底层对应指针所指向的空间被释放了或者这个位置在可用空间内但是不储存有效的数据,使用失效的迭代器,可能会造成程序崩溃。
erase在删除pos位置的元素后,pos位置之后的元素会往前挪一个位置,中间的元素被删除后面的元素又会补在这里,迭代器也就没有失效,而如果pos刚好是最后一个元素,删完之后pos刚好是end()的位置,这里没有有效数据,pos就失效了。
为了保证不出现迭代器失效,在删除vector中任意位置上元素后,检查更严格的vs认为这个迭代器失效,而linux是不失效的,这也解释了为什么erase和insert都以迭代器作为返回类型,目的就是传回新的没有失效风险的迭代器。
2.迭代器失效相关代码
#include <iostream>
using namespace std;
#include <vector>
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
//可以使用find查找3的位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
v.erase(pos);
//删除pos位置的数据,pos迭代器的位置存储的不再是有效数据
cout << *pos << endl; //此处的访问就是非法的,属于迭代器失效
return 0;
}
还有一个更简单的代码:
int main()
{
vector<int> v{ 1, 2, 3, 4, 5 };
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << " ";
}
cout << endl;
auto it = v.begin();
cout << "扩容前容量为:" << v.capacity() << endl;
v.reserve(100);//reserve所有的扩容都是异地扩容
cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
//it迭代器指向原空间,而数据会移动到新空间,代码在vs下程序直接崩溃,但是linux下就不会,只是结果完全不对
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
其实在string中也会存在迭代器失效的问题,只是在vector中这个问题对使用的影响大得多。而解决迭代器失效的方法就是及时对迭代器进行重新赋值以避免迭代器失效。