目录
简介
C++的vector
是一种序列容器,它表示可以改变大小的数组。vector
在头文件<vector>
中定义,是C++标准模板库(STL)的一部分。
特点
- 动态数组:
vector
可以在运行时动态地扩展和收缩。 - 连续存储:
vector
的元素存储在连续的内存位置,这使得通过索引访问元素非常快速。 - 类型安全:
vector
是模板容器,可以存储任何类型的元素。 - 迭代器支持:
vector
提供了双向迭代器,可以用于遍历容器中的元素。 - 容量和大小:
vector
具有当前元素数量(大小)和最大容量两个属性。
实现
下面根据vector的特点,来完成vector的实现。在SGI STL源码文件的vector.h文件中,采用了三个迭代器成员完成了vector的实现,那我们也采用三个迭代器成员去实现vector
由于vector是模板类,因此将实现都放在了头文件中,防止出现编译链接的错误
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
定义好迭代器之后,就可以用迭代器去定义成员变量
成员变量
private:
iterator _begin = nullptr; //都是用迭代器实现的
iterator _end = nullptr;
iterator _end_of_storage = nullptr;
在源码中命名采用的是_start _end _end_of_storage三个成员,为了跟源码区分,我们换了命名。
在C++11中,支持类内初始化,因此我们采用了类内初始化的方式
成员函数
Part 1:迭代器部分函数
由begin()、end()系列构成
iterator begin()
{
return _begin; //保证返回类型
}
iterator end()
{
return _end;
}
const_iterator begin() const
{
return _begin;
}
const_iterator end() const
{
return _end;
}
Part 2 :访问权限相关的函数
解引用operator[]
用来完成数据的访问,借助的是下标。应该重载好只读和读、写的双重版本
T& operator[](size_t n)
{
assert(n < size());
return _begin[n];
}
const T& operator[](size_t n) const
{
assert(n < size());
return _begin[n];
}
capacity函数、size函数
capacity用来观察对象的空间。由于我们采用的是迭代器实现,所以用差值表示空间
size_t capacity() const
{
return _end_of_storage - _begin;
}
size函数用来观察size大小
size_t size() const
{
return _end - _begin;
}
Part 3 :默认成员函数
这里主要是指构造、析构、拷贝构造、复制重载
构造函数
用来完成对象的初始化工作,应该包含1.默认构造 2.完成其他需要的构造函数
默认构造:可以是无参、全缺省、系统生成。由于我们显式定义构造函数,因此采用无参的默认构造函数。其可以采用类内初始化的方式去完成初始化工作。
vector() //作为无参的默认构造函数
{}
其他构造:
官方还给出了采用迭代器去初始化和采用val去初始化的功能。
迭代器版本:
采用了模板函数,借助InputIterator去完成了初始化
template <class InputIterator> //模板函数,参数是输入迭代器
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
指针版本的迭代器不仅可以完成解引用观察数值,还可以采用指针运算去观察元素个数
用n个val去初始化
我们先开辟出n个空间来,避免多次异地扩容导致的时间 空间消耗
vector(size_t n, const T& val = T()) //匿名对象作为缺省对象参数
{
reserve(n); //避免异地扩容
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
对于传参,我们借助匿名对象传参,当对象是内置类型该怎么办呢?
注:((
/*
在C++中,完成了内置类型的升级,变成了对象
对于内置类型(如 int 或 double),T() 通常会产生一个默认初始化的值(对于 int 是 0,对于 double 是 0.0)。
对于自定义类型,T() 将调用该类型的默认构造函数。
*/
))
拷贝构造
我们这里采用了一种新的拷贝构造方式。注意,拷贝构造必须完成深拷贝。思路:借助swap函数去完成拷贝构造
void swap(vector<T>& v)
{
std::swap(_begin, v._begin);
std::swap(_end, v._end);
std::swap(_end_of_storage, v._end_of_storage);
}
v2(v1)对于v1我们只需要内部建立好一个跟v1一样的副本。交换v2与 副本,再释放掉旧v2的空间,对于副本对象,出了作用域会自动销毁。
// v2(v1)
vector(const vector<T>& v) //形参必须是引用
:_begin(nullptr) //进入函数体之前,优先进行初始化
,_end(nullptr)
,_end_of_storage(nullptr)
{
vector tmp(v.begin(), v.end());
swap(tmp);
} //tmp销毁,但是开辟的空间还在
赋值重载
同样借助swap函数,完成赋值重载。赋值重载必须也是深拷贝。
//v2 = v1
vector<T>& operator=(vector<T> tmp) //引用返回
{
swap(tmp);
return *this;
}
析构函数
在C++中,析构空指针通常不会导致严重的问题。析构函数的主要作用是释放对象所占用的资源,例如动态分配的内存。如果一个指针是空的(即它不指向任何对象),调用其析构函数通常不会执行任何操作,因为没有任何资源需要释放
~vector()
{
if (_begin)
delete[] _begin;
_begin = _end = _end_of_storage = nullptr;
}
Part 4 :空间开辟相关、资源申请相关的函数
reserve
用来预开辟空间。修改capacity的大小,一般来说,只能增大空间,不能变小。
reserve函数的几个步骤:1.预开辟空间 2.空间内容的转移(深拷贝) 3.释放就空间 4.更新成员变量
我们建立了一个变量sz来接收size函数的返回值。当使用size函数的时候,由于size函数内部使用迭代器指针完成的,为了防止指针运算带来的弊端,我们采用sz来接受size的大小。
//1.开空间 2.深拷贝 3.清除原来的空间 4.修正
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n]; //开空间
size_t sz = size(); //需要用函数尽量提前建立好返回变量
if (_begin != nullptr) //解引用需要保证不为空
{
for (size_t i = 0; i < sz; ++i) //深拷贝,用赋值重载,不用memcpy(内部浅拷贝)
{
tmp[i] = _begin[i];
}
delete[] _begin; //释放旧空间
}
_begin = tmp;
_end = _begin + sz;
_end_of_storage = _begin + n;
}
}
需要注意的是,内部不可以用memcpy去拷贝内容。memcpy内部是浅拷贝,虽然开出了空间,但是指针还是没有指向新空间。
resize
同样可以进行空间的预开辟,也可以完成初始化操作。
当预开辟的大小n,小于size时,只需要修正即可
当大于size时,可以进行reserve(n)操作,把剩余的内容进行缺省值填充
void resize(size_t n, const T& val = T())
{
size_t sz = size();
if (n <= sz)
{
_end = _begin + n;
}
else
{
reserve(n);
while (_end < _begin + n)
{
*_end = val;
++_end;
}
}
}
push_back
用来完成尾插,这是最重要的插入方式。push_back可以自动调整空间。
步骤:1.判满 (开空间) 2.尾插 3._end++
void push_back(const T& x)
{
//if (_end == _end_of_storage) //尽量去使用成员变量
//{
// size_t newcp = (size() == 0) ? 4 : 2 * capacity();
// reserve(newcp);
//}
//*_end = x;
//++_end;
insert(_end, x);
}
insert函数
用来完成数据的任意位置的插入。像如挪动数据的函数,消耗较大,尽量不要用。首先要assert保证插入的位置正确。
1.判满 2.挪动数据 3._end++
void insert(iterator pos, const T& x)
{
assert(pos <= _end && pos >= _begin);
//判满
if (_end == _end_of_storage)
{
size_t len = pos - _begin;
size_t newcp = (size() == 0) ? 4 : 2 * capacity();
reserve(newcp); //reserve之后,更新迭代器pos
pos = _begin + len;
}
iterator end = _end - 1; //_end不存储任何元素
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x; //插入数据
++_end;
}
使用reserve之后,迭代器失效,需要更新迭代器pos。因此使用insert、erase之后,迭代器需要更新使用。
erase
用来完成数据的删除。在删除的时候,必须能够保证头删。
1.挪动数据 2.--_end;
iterator erase(iterator pos)
{
assert(pos >= _begin && pos < _end);
iterator it = pos + 1; //保证可以完成头删的下标(+1即可)
while (it < _end)
{
*(it - 1) = *it;
++it;
}
--_end;
return pos;
}