一、前言
大家好,在上一文中,我们重点介绍了 STL中的string类,明白了如何去操作字符串。本文我们将要来介绍的是STL中的vector类
二、vector深度剖析及模拟实现【✔】
在介绍完了【vector】的基本接口后,我们就透过源码来深入理解一下
1、源码引入
- 以下我所介绍的都是基于【SGI】版本的STL,对源码有兴趣的同学可以去看看 侯捷老师的《STL源码剖析》
- 然后呢我们就去调出【vector】的一些核心源码,这里我们主要关注的就是这个使用原生指针
value_type*
所定义出来的迭代器iterator
- 然后我们又看到了保护成员:
[start]
、[finish]
、[end_of_stroage]
。看到它们你是否有想起我们在 模拟string 的时候写到过的[a]
、[size]
、[capacity]
;没错,它们就存在着一定的对应关系
- 但是呢,只看上面这些成员变量还不够,我们要将其带入到具体的场景中,例如下面有两个接口分别为【尾插】和【扩容】,对于
push_back()
封装得没有那么厉害,读者结合下面的图应该就能看得懂,分别就是 未满追加的逻辑和已满扩容的逻辑 - 那对于
reserve()
来说,就是一个扩容的逻辑,【allocate_and_copy】是开辟和拷贝空间,那【deallocate】就是释放空间。在扩完容之后不要忘了去对三个成员变量做更新,这一块的模拟实现我在下面马上就会讲到
- 最后的话我们再来看看 构造函数
construct
和析构函数destroy
,光看代码,不知你是否回忆起了我们曾经在 C/C++内存管理 中有讲到【定位new】这个概念,而且提到了 内存池 这个概念 - 其实我们在调用构造函数的时候,都是通过【空间适配器】在 内存池 中开出空间;那在出了作用域之后这些所开的空间都要销毁了,所以就会去调用析构函数完成释放
💬 对于上面的这些源码呢,读者可以在学习了STL一段时间后,配合侯捷老师的《STL源码剖析》再去展开阅读,因为考虑到读者的基础,就不在继续深入讲解了~
2、模拟实现
然后我们就来模拟实现一下【vector】中的各种接口
- 还是一下,我们先简述一下整体的架构。这个
vector
类还是包在【bit】这个命名空间中,而对于这个类而言,我要将其定义为一个 模版类,这一块如果还有同学不太熟悉的话可以去看看 C++模版 - 其他部分可以看到迭代器我定义的就是原生指针类型,然后将
[_start]
、[_finish]
、[_end_of_storage]
也定义为了三个迭代器类型,并且采用提前声明的形式将它们都初始化为nullptr
,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了
namespace bit {
template
class vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
// 主要接口函数
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
1)迭代器
- 首先的话简单一点,来实现一下迭代器,分为
非const版本
和const版本
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
2)容量
- 然后我们来讲讲容量相关的接口,首先的话就是【size】和【capacity】这两个接口
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
- 对于【size】而言指的是当前这个容器中的数据个数,那也就是我们在上面所讲的
_start
和_finish
这两个迭代器之间的距离,我们之前有说到过迭代器它的底层其实就是指针,那要计算出两个指针之间的数据个数的话让它们做一个相减_finish - _start
- 那对于整个容器的容量【capacity】来说,即为
_end_of_storage - _start
。读者通过下图便可一目了然地看出来
- 然后呢再来说说扩容这一块的接口【reserve】,首先在一进来的时候我们要去做一个判断,只有当所传入的值要比原先的
capacity()
来得大的时候,我们才去执行一个扩容的逻辑,在内部的扩容逻辑中可以看到我们使用到了前面所定义的模版参数T
,这样去写的话就可以根据不同的类型参数开出不同的空间 - 接下去我们所执行的就是拷贝的逻辑,采取到的是内存函数
memcpy()
,拷贝完后再去释放原空间,接下去把这些成员变量去做一个更新即可
==看着逻辑很清晰,但是呢下面的代码存在着非常多的漏洞==
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n]; // 开一块新空间
if (_start)
{
memcpy(tmp, _start, sizeof(T) * size());
delete[] _start;
}
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n;
}
}
- 我们这里再写一个
push_back
的接口(后面讲),让代码先跑起来
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_finish = x;
_finish++;
}
💻第一轮测试 — 空指针异常
下面是测试的代码
void test_vector1()
{
bit::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout