本博客对于vector类的实现参照的是 STL 当中的vector;而且只是简单模拟实现,对于vector当中的介绍请参照以下博客:
C++ - 初识vector类_chihiro1122的博客-CSDN博客
vector模拟实现
在STL当中当中的 vector 的源码框架大概如下图所示:
使用 start finish end_of_storage 三个成员变量指针,来维护这块空间,而 size 和 capacity 是用成员函数的形式来表示的,所以模拟实现的vector也是要由上述三个指针来维护。
STL当中的vector 实现了模版,这是一个强大的功能,在实现的时候同样要跟着实现。
外部框架如下所示:
namespace My_vector
{
template<class T>
class vector
{
typedef T* iterator;
public:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
iterator begin() const
{
return _start;
}
iterator end() const
{
return _finish;
}
protected:
iterator _start;
iterator _finish;
iterator _endOfStorage;
};
}
构造函数和析构函数
无参数的构造函数:
vector()
:_start(0),
_finish(0),
_endOfStorage(0)
{}
都是指针,无参数来构造vector对象的话,直接给三个指针赋值为 0,也就是 nullptr;空间也不需要开,在之后的插入当中判断,然后扩容就行。
构造n个初始化为val的构造函数
vector(size_t n , const T& val = T())
:_start(nullptr),
_finish(nullptr),
_endOfStorage(nullptr)
{
resize(n, val);
}
因为实现了resize()函数,这里直接使用resize函数来实现。
用vector自己,或其他类型的迭代器区间来初始化的构造函数
在上一篇博客当中,我们了解到,vector不仅仅可以使用自己的迭代器来初始化,还可以使用其他自定义类型的迭代器区间来初始化,其实这里也是运用了模版来实现的,一个类模版当中还可以再嵌套模版,如下实现:
// 迭代器区间初始化 [first , last)
template <class Inputiterator>
vector(Inputiterator first, Inputiterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
但是这样做有一个问题,上述是两个同类型的模版参数,那么这两个模版类型可以是int,这是也就是两个int的参数,但是我们上述实现了一个构造函数是这样写的:
vector(size_t n , const T& val = T())
这也就意味着,有人会在外面这样定义vector对象:
My_vector::vector<int> v1(10, 1);
我们看,这样的写的话我们期望是调用下面这个构造函数,而不是迭代器区间构造函数,但是上述两个都是int,如果出现 10 这样的整数,编译器会首先认为是 int 类型的,而不是 size_t ;所以这样写编译器就会取调用 迭代器区间构造函数,那么这样就不好。
为了解决上述问题,我们在定义一个 参数列表为 (int , int)的构造n个初始化为val的构造函数,这样就可以解决了:
vector(int n, const T& val = T())
:_start(nullptr),
_finish(nullptr),
_endOfStorage(nullptr)
{
resize(n, val);
}
拷贝构造函数
vector(const vector<T>& v)
:_start(nullptr),
_finish(nullptr),
_endOfStorage(nullptr)
{
T* tmp = new T[v.capacity()]; // 这里开空间的大小可以是 size 或 capacity
//memcpy(tmp, v._start, sizeof(T) * v.size());
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_start = tmp;
_finish = _start + v.size();
_endOfStorage = _start + v.capacity();
}
///
vector(const vector<T>& v)
:_start(nullptr),
_finish(nullptr),
_endOfStorage(nullptr)
{
reserve(v.capacity());
for (auto ch : v)
{
push_back(ch);
}
}
析构函数:
~vector()
{
if (_start)
{
delete _start;
_start = _finish = _endOfStorage = nullptr;
}
}
上述如果不判断 _start 维护的空间是否为空也可以,因为为空的空指针 也可以进行 delete 释放操作,只不过为了严谨性,重复释放空间的空间不严谨,所以还是进行判空操作。
iterator 迭代器 capacity() size()
因为 vector 和 string 两者的实现非常的相似,两者的迭代器其实是一样的:
typedef T* iterator;
public:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
iterator begin() const
{
return _start;
}
iterator end() const
{
return _finish;
}
实现了迭代器,范围for也就实现了,因为范围for的底层就是用迭代器实现的,只是傻瓜式的进行替换。
capacity()和 size()函数实现:
// 取出空间的容量大小
size_t capacity() const
{
return _endOfStorage - _start;
}
// 取出空间的有效字符个数
size_t size() const
{
return _finish - _start;
}
增删查改
reserve()扩容函数:
// 扩容函数
void reserve(size_t n)
{
if (capacity() < n)
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * oldsize);
delete _start;
}
_start = tmp;
_finish = _start + oldsize;
_endOfStorage = _start + n;
}
}
vector的扩容和string差不多。
需要注意的是,在函数的最下面对 三个成员变量的修改,不能直接用 size()和 capacity()两个函数,因为这两个函数也是根据 这三个成员变量来进行计算的,比如上述的 _finish 的修改,如果改为 _finish = _strat + size() 的话就会出错!!!
因为 size() 的计算规则是 _finish - _start ;但是此时 _finish 没有进行赋值,还是扩容之前的 _finish 所指向的指针,那么在减去 _start 已经更新过的指针的值,就会出错;同样的 _endOfStorage 这个变量的修改也不能直接用 capacity(),上述用的就是 reserve ()函数传入的需要扩容到的空间容量 n。
其实,上述的reserve()还有一个非常严肃的问题,先来看下面这个例子:
My_vector::vector<string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
v.push_back("4444");
v.push_back("5555");
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
当vector当中存储的类型不是内置类型,而是自定义类型的时候,如果扩了容,那么就会报错!!
原因就出在reserve()当中的 memcpy()函数和 delete,如下图所示:
当需要扩容的时候,reserve()函数就会新开辟一块空间,然后使用memcpy把成员指针维护的空间当中的值直接值拷贝给新空间,然后直接delete掉原空间。
上述的vector当中存储的结构可以理解为 vector的数组当中存储了 5 个string类,那么直接memcpy()只是把这5 个string类的指针拷贝到新的空间当中,在之后的delete 就会直接把 这 5 个 string类的空间直接释放掉;
因为看似delete 释放的是 _start 这块空间,其实编译器还会自动调用 string 类的析构函数,那么string类的空间释放掉了,新空间当中的存储的 5 个string类的指针就是野指针了。
引发上述问题的原因其实不在于delete,而是在于memcpy()函数只是一个值拷贝(浅拷贝),解决办法就是走深拷贝,但是不同的自定义类型的需要深拷贝的数量是不一样的,不能做到统一,所以使用如下方式来解决:
for (size_t i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
使用上述一个非常简单的循环就可以搞定,但是虽然实现很简单,但是在底层当中的逻辑还是比较复杂的。
这地方那个的本质是,调用string的赋值运算符重载函数(operator=),从而达到深拷贝的效果。
insert()在pos位置插入一个元素函数
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
// 扩容
if (_finish == _endOfStorage)
{
size_t len = pos - _start;
int newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
// 把数据往后挪一位
iterator end = _finish - 1;
while (pos <= end)
{
*(end + 1) = *end;
end--;
}
*pos = x;
++_finish;
return pos;
}
insert() 的实现很简单,先判断需不需要扩容,然后把pos后每一个元素都往后面挪一个位置,再把需要插入的元素插入即可。
需要注意的是,insert()函数,如果需要扩容的话,pos迭代器会失效,失效的话,后面在解引用访问的pos指针就是一个野指针就会报错。解决方法就是如果需要扩容,就更新pos迭代器。但是这仅仅解决了内部函数内部迭代器的失效问题,如果是外部迭代器失效的话,函数内部是无法进行更新的,所以这里需要外部使用的人手动更新;我们把insert()函数做了一个返回值,这个返回值返回但就是pos迭代器,以返回值的形式让使用的人从外部就可以更新这个迭代器。
关于迭代器失效,可以查看下面这个博客:
(1条消息) C++ - 初识vector类 迭代器失效_chihiro1122的博客-CSDN博客
push_back() 尾插一个元素
// 尾插函数
void push_back(const T& x)
{
/* if (_finish == _endOfStorage)
{
int newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
++_finish;*/
insert(end(), x);
}
关于push_back ()函数的实现,可以自己实现(如上述注释的部分);由于我们上述实现了insert()函数,所以我们可以直接套用insert()函数来实现 尾插的功能。
pop_back() 尾删函数
void pop_back()
{
erase(--end());
}
erase ()删除pos位置的元素函数
// 指定位置删除元素
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator end = pos + 1;
while (end <= _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
return pos;
}
如上所示,直接把pos之后的元素往前挪一个即可,这样pos位置的元素就会被覆盖,达到删除的目的。
erase()函数也会出现迭代器失效的问题,在VS当中对于迭代器失效检查比较严格,而在g++当中基本不会有检测,所以我们经常会发现这样一种情况:如上述的在g++ 当中能通过的代码,可能在当时测试的时候没有问题,但是测试的相同代码和相同例子,换做在 VS下就直接 assert 打断了。其实这时可能就是上述说的情况,迭代器失效了,对于迭代器失效VS当中会进行判断,而在个++当中不会进行判断,就算例子编译过了,但是在VS当中就不行。
像上述的函数实现一样,把这个迭代器作为函数的返回值返回,这样别人就可以对外部的迭代器进行更新,防止迭代器失效。
总结:在vector当中如果在外部使用迭代器来 调用 erase( ) 函数 和 insert()函数,建议尽量都不要再使用这个迭代器了,防止出现迭代器失效的问题。我们认为这样的迭代器失效了,是未定义的变量。
resize()函数修改 capacity 和 size
// 修改 capacity 和 size 的函数
void resize(size_t n , const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
++_finish;
}
}
}
因为vector 所使用的类型使用模版来实现的,那么我们在使用resize()函数的时候,有需求不给初始化的值,然后让resize()函数给一个缺省值进行初始化。
但是由于vector 当中的类型是用模版实现的,如果我们定义的缺省值的是0 的话,是肯定不行的,因为 类型可能是 内置类型,也可能是 string类 vector类 这样的自定义类型,那么 缺省值设置为0,就肯定不行了。
所以这里的缺省值使用的是 T的匿名对象,这样的话就会调用这个自定义类型的默认构造函数来初始化匿名对象从而赋值,所以这里就要求使用的自定义类型必须要有默认构造函数。
但是,如果是使用匿名对象的话,那么内置类型按道理是会报错的;但是C++ 在这块做了特殊处理,在模版推出之后,C++把内置类型进行了升级,让内置类型也有了默认构造函数,这样这个内置类型就有了编译器自己实现的默认构造函数,从而可以让 int 之类的内置类型可以使用匿名对象来创建。
由上述,举一个例子,对于int类型变量的定义可以这样定义:
int i = 1;
int j = int(8);
两种方式都可以。
operator[] 下标访问 运算符重载函数
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
如上,只要做好pos指针的范围判断,直接返回pos指针指向位置的元素即可。因为要针对 普通对象,和const的对象,所以有 普通 和 const 两个版本。
operator= 赋值运算符重载函数
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfStorage, v._endOfStorage);
}
vector<T>& operator= (vector<T>& v)
{
swap(v);
return *this;
}
这里使用了 传值参数要产生临时对象,临时对象最后要销毁的特性,具体可以查看下面博客当中,string类的 operator= 函数实现的过程: