前言
本文将模拟实现vector的常用功能,目的在于更深入理解vector。
一、前置知识
- 在模拟之前先对vector的结构和常用接口学习,有一个大致了解。
- 看源码,本文参考的源码是SGI版本的stl3.0。
- 技巧:
- 看源码不要一行一行的看,要先看框架,了解整体框架
- 看源码要学会猜,根据单词的意思去猜它想表达什么。规范的代码,每一个名字都有它的含义。
- 总结:一看框架;二去猜(带着猜想,去验证)。
- 看框架的步骤:
- 先看成员变量
- 再看成员函数
- 技巧:
- 参考vector的源码:
- vector的成员变量:是三个原生指针变量(设为原生指针类型有什么好处,在模拟时讲解)
- vector的成员函数:vector的常用接口讲解链接
- vector的成员变量:是三个原生指针变量(设为原生指针类型有什么好处,在模拟时讲解)
- STL中的容器因为需要频繁的申请和释放空间,所以STL中提供了内存池(allocator类模板),内存池的本质是先在堆区中申请一定的空间留作备用,当有新的内存需求时,就从内存池中分出一块内存块,若内存块不够再继续申请新的内存,这样可以提高内存分配的效率。现阶段我们只是简单模拟vector,所以我们这没有使用内存池,而是直接在堆区申请空间,后期会讲解内存池的。
二、vector常用接口的模拟
1、vector的成员变量
vector的成员变量是三个原生指针T*:
- _start:开始位置,即指向第一个元素的位置
- _finish:结束位置,即指向最后一个元素的下一个位置
- _end_of_storage:存储结束位置
虽然vector使用的是三个原生指针,但是可以通过指针运算得到size和capacity。
代码示例:
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{
//类模板的实现和定义不分离,后续学到模板进阶会讲解!
template<class T>
class vector
{
public:
typedef T* iterator;
//获取容器中的元素个数
size_t size()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用
{
//指针-指针=两者之间的元素个数
return _finish - _start;
}
//获取为当前容器分配的存储空间
size_t capacity()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用
{
//指针-指针=两者之间的元素个数
return _end_of_storage - _start;
}
private:
iterator _start;//开始位置,指向第一个元素
iterator _finish;//结束位置,指向最后一个元素的下一个位置
iterator _end_of_storage;//指向存储结束位置
};
}
tip:
- 使用命名空间将我们模拟实现的vector封装,避免命名冲突。
- 类模板的定义与实现不分离,后续在模板进阶讲解。
- size和capacity可以通过指针-指针得到,注意指针-指针运算有一个前提是:物理存储空间是连续的。
- const成员:
- const修饰的是*this,即const成员函数的内部不能修改成员变量
- 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用
2、vector的默认成员函数
构造函数
:
- 构造函数:创建类对象时,编译器自动调用,给成员变量赋初值
- 类的成员变量建议在初始化列表初始化
- 成员变量为内置类型需要我们手动初始化,不然为随机值;成员变量为自定义类型不初始化,会去调用它的默认构造(建议每个类,都要有一个默认构造)
- vector的常用构造函数:
- 默认构造函数:一般使用最多,构造一个空的vector,即将每个成员初始化为空
- 构造并初始化n个val:先初始化成员变量,再复用reserve开n的空间,最后再通过尾插将val插入容器
- 使用迭代器初始化构造:先初始化成员变量,再将迭代器区间的数据尾插入容器
析构函数
:
- 析构函数:对象销毁时,编译器自动调用,完成对象中资源的清理
- 编译器生成的析构函数,对内置类型不做处理,自定义类型会去调用它的析构函数
- 当类涉及动态资源的申请时,就需要显式实现析构释放资源。
赋值重载函数
:
- 赋值重载函数:已经存在的两个对象复制拷贝
- 当类涉及资源管理时,就需要自己显示实现完成深拷贝,编译器默认生成的赋值重载函数只能完成浅拷贝
- 赋值重载深拷贝的现代写法:让形参去调用拷贝构造,去帮我们开空间拷贝数据,之后与形参交换(函数结束后形参销毁,也顺便帮我们把旧空间释放了)
- 现代写法无法避免自己给自己赋值的情况,当现实中也很少会出现
拷贝构造函数
:
- 拷贝构造函数:用一个已经存在的对象初始化另一个对象
- 注意:拷贝构造只有一个参数,并且必须是本类型的引用(使用传值会引发无限递归)
- 编译器默认生成的拷贝构造也是只能完成浅拷贝,所以当类涉及资源管理时,就需要自己显式实现完成深拷贝
- 拷贝构造深拷贝的现代写法:自己开空间,自己拷贝数据
总结:当类涉及资源管理时,拷贝构造、赋值重载、析构都需要显式实现。
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{
//类模板的实现和定义不分离,后续学到模板进阶会讲解!
template<class T>
class vector
{
public:
//默认构造函数
vector()
//初始化列表:成员变量定义的地方,建议在初始化列表初始化成员变量
//成员变量为内置类型不初始化,为随机值
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
}
//构造并初始化n个val
vector(size_t n, const T& val = T())//T()调用构造函数
//初始化成员变量
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
//复用reserve,开空间
reserve(n);
//复用push_back,尾插n个val
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
//使用迭代器区间初始化
//类模板的成员函数也可以是函数模板
template<class InputIterator>
vector(InputIterator first, InputIterator last)
//初始化成员变量
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
//复用push_back,将迭代器区间[first,last)的数据尾插进容器
while (first != last)
{
push_back(*first);
++first;
}
}
//当wjs::vector<int> v(10, 1);报错——》 error C2100: 非法的间接寻址
//函数重载,调用时会走最匹配的,wjs::vector<int> v(10, 1)两个参数类型都是int,所以他走使用迭代器构造
//重载一个vector(int n, const T& val = T()),他就会走构造n个val
//构造并初始化n个val
vector(int n, const T& val = T())//T()调用构造函数
//初始化成员变量
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
//复用reserve,开空间
reserve(n);
//复用push_back,尾插n个val
for (int i = 0; i < n; ++i)
{
push_back(val);
}
}
//交换两个vector对象
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish)