目录
一、基础模板:
1、基本结构:
- _start:指向数据块的开始
- _finish:指向数据块中有效数据的下一个位置
- _endofstorage:指向数据块的结尾
代码形式:
// 防止与库中的vector冲突,创建新的命名空间
namespace yue
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
size_t size() const // 返回数据块中的有效数据的个数
{
return _finish - _start;
}
size_t capacity() const // 返回数据块中有效空间容量的大小
{
return _endofstorage - _start;
}
// 成员函数...
private:
iterator _start = nullptr; // 给定缺省值
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
2、默认成员函数:
无参构造函数:
无参的构造函数在定义成员变量时给过缺省值的可以不写。
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
}
拷贝构造函数:
思路:利用reserve函数开辟好空间后利用迭代器将 v 中的数据一个个尾插到自身数据块中。
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(v.capacity());
for (const auto e : v)
{
push_back(e);
}
}
swap函数:
在库中提供的swap函数对于交换自定义类型的代价极大,要进行深拷贝,过程会经历三次拷贝一次析构,所以我们应该手动写一个swap成员函数。
void swap(vector<T>& v)
{
// 可以直接调用库中的swap函数更改指针的指向,这样就不需要进行深拷贝
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
运算符 = 重载:
思路:利用传值调用会拷贝构造产生一份形参的特点,用swap函数交换其指向即可达到目的,且修改形参并不影响实参,且传值调用的拷贝的形参出了函数作用域也会自动销毁,使用起来非常方便。
vector<T>& operator=(vector<T> v)
{
swap(v); // 该swap函数为自己实现的函数,非库中的函数
return *this;
}
析构函数:
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
二、初始化构造:
1、迭代器区间构造:
利用任意类型vector的一段区间去构造:
// 类模板的成员函数可以是函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
使用函数模板InputIterator的原因:
提供泛型编程的能力,使得适应不同的容器,就是这个构造函数可以接受任何类型的迭代器,只要这个迭代器满足InputIterator的概念,例如数组,函数的迭代器等。如下:
string s1("abcd");
vector<int> v1(s1.begin(), s1.end());
这样vector也能支持string的迭代器,打印进v1中的值会隐式类型转换成int,也就是字符的ASCII值。
2、使用n个val初始化构造:
例如:vector<int> v(10,1) 构建数组v并初始化为10个1。
// 使用匿名对象T(),使其支持内置类型
vector(size_t n, const T& val = T())
{
reserve(n); // reserve的实现在下面
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
但是就单独上面这样子写在面对 int (就例如vector<int> v(10,1) )类型的时候就会出问题,会报一个非法的间接寻址的错误,参数类型会优先匹配上一个(迭代器区间构造)函数,因为编译器无法正确识别到我们要用哪一个函数,所以会自动匹配(迭代器区间构造)这个函数。
因为迭代器类型可以转换为int,而我们写的函数类型是 size_t 和 int(模板),虽然前者要进行类型转换,但相比于后者,转换后的前者类型更匹配,所以在面对int,int类型时,编译器会优先选择前者。
解决的方法其实也很简单粗暴:重载一个int类型的函数即可,如下:
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
这时编译器就会优先选择这个函数。
3、初始化数组构造:
例如:vector<int> v(1,2,3,4,5,6,7,8) 构建数组v并对其进行初始化。
关于 initializer_list:
initializer_list用于表示某种特定类型的值的数组,和vector一样,initializer_list也是一种模板类型。可以用于容纳同一类型的多个元素。然而,与vector不同的是,initializer_list中的元素是常量值,一旦初始化后就不能改变。此外,initializer_list的拷贝和赋值操作并不会拷贝列表中的元素,而是实现元素的共享。
void Test()
{
// 定义在同名头文件中
initializer_list<int> x = { 1,2,3,4,5,6,7,8 };
std::vector<int> v1 = { 1,2,3,4,5,6,7,8 };
// 有点类似于:
string str = "1111"; // 构造+拷贝构造 -> 优化: 直接构造
const string& str1 = "1111"; // 构建临时对象,引用的是临时对象临时对象具有常性要加const
// 用1111初始化str的原因就是单参数的构造函数支持隐式类型转换
}
代码实现:
// vector<int> v = { 1,2,3,4,5,6,7,8 };
vector(initializer_list<T> l)
{
// initializer_list中也有迭代器,可以直接用范围for遍历
reserve(l.size());
for (auto& e : l)
{
push_back(e);
}
}
// 直接构造:
vector<int> v({ 1,2,3,4,5,6 });
// 隐式类型转换 + 优化:
vector<int> v = { 1,2,3,4,5,6 };
三、vector的遍历操作:
1、[ ] 符号重载:
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
2、迭代器遍历(Print):
范围for: 其实本质也是迭代器
template<class T>
void Print(const vector<T>& v)
{
//typename vector<T>::iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
// 范围for:
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
四、增删操作:
1、reserve 与 resize:
reserve(指定容量):
这里实现的扩容会有些小问题,放在第五个标题(隐藏的浅拷贝问题)中重新做一个更改。
void reserve(size_t n)
{
// 如果n大于capacity则进行扩容操作
if (n > capacity())
{
T* tmp = new T[n];
// 先记录size的值,扩容后旧空间会被释放
size_t oldsize = size();
memcpy(tmp, _start, size() * sizeof(T)); // 拷贝数据
delete[] _start; // 释放旧空间
_start = tmp;
_finish = _start + oldsize; // 旧空间被释放,size()里的_finish为野指针,
_endofstorage = _start + n;
}
}
resize(扩容+初始化):
// 右参数给匿名对象,在没有右参数时,对应的类会去调用其的默认构造
// 而默认的构造函数会对 val 进行赋值,满足resize的右参数需要
void resize(size_t n, const T& val = T())
{
if (n > capacity())
{
reserve(n);
// 插入数据
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
// 当n < _finish 时删除有效数据至n
else
{
_finish = _start + n;
}
}
2、push_back 与 pop_back:
push_back(尾插):
void push_back(const T& val)
{
// 检查扩容
if (_finish == _endofstorage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
pop_back(尾删):
// 判空
bool empty()
{
return _start == _finish;
}
void pop_back()
{
assert(!empty()); // 如果数据块为空,则不能进行删除操作
_finish--;
}
3、insert 与 erase:
insert(指定位置插入):
void insert(iterator pos, const T& val)
{
// 检查范围
assert(pos >= _start); // = 就是头插
assert(pos <= _finish); // = 就是尾插
// 检查扩容
if (_finish == _endofstorage)
{
// 先记录数据块中插入数据位置之前的数据的个数
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
// 更新pos扩容后所在的位置(指向)
pos = _start + len;
}
// 利用迭代器挪动数据
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
it--;
}
// 插入数据
*pos = val;
_finish++;
}
注意点:
1、为什么要提前计算并保留len(数据块中插入位置之前的有效数据的个数)?
因为会引发迭代器失效的问题,pos是迭代器类型,指向要插入数据的位置,如果刚好要进行扩容操作的话,会开辟新空间,旧空间会被释放,而pos指向的是旧空间的位置,所以要同步更新pos的指向。
erase(指定位置删除):
iterator erase(iterator pos)
{
// 检查范围
assert(pos >= _start); // = 就是头删
assert(pos < _finish); // _finish为数据块中最后一位有效数据的下一个位置
// 挪动数据
iterator it = pos;
while (it < _finish)
{
*it = *(it + 1);
it++;
}
_finish--;
return pos;
}
五、隐藏的浅拷贝问题:
当我们使用string类型的数组例如(或者使用vector<vector>类型)的时候程序就会挂掉,如下:
void Test_vector7()
{
vector<string> v;
v.push_back("11111");
v.push_back("22222");
v.push_back("33333");
v.push_back("44444");
v.push_back("55555");
}
直接说结论,会在析构的时候崩掉,因为在扩容的时候出了问题。
回顾上面所写的reserve功能代码,使用了memcpy这个函数,memcpy实现的功能是从源内存块复制指定字节数的数据到目标内存块,这是一个隐藏浅拷贝,扩容完之后_start会释放旧空间再指向新空间,但是新空间的数据是已经被释放掉了的,所以此时_start就是一个野指针,再次析构就会崩溃。
解决方法:将赋值改为手动赋值(改为for一个个赋值,赋值调用的就是对应容器的深拷贝)
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t oldsize = size();
// memcpy(tmp, _start, size() * sizeof(T));
for (size_t i = 0; i < oldsize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + oldsize;
_endofstorage = _start + n;
}
}