文章目录
一.vector 介绍
- vector 表示可变大小数组的序列式容器
- 就像数组一样,vector 也采用连续的存储空间来存储元素,这也就是意味着可以采用下标对 vector 中的元素进行访问,其效率和数组一样高效。但是它又不像数组,它的大小是可以动态改变的,甚至还有自动扩容的功能。
二. vector 的模拟实现
1. 基本框架
vector 是个类模板,它就可以存储不同数据类型的元素,其迭代器是数据类型的指针。
template<class T>
class vector
{
public:
typedef T* iterator; //普通迭代器
typedef const T* const_iterator;//const迭代器
private:
iterator _first; //指向第一个有效数据的指针
iterator _finish; //指向有效数据的尾(尾不存储有效数据)
iterator _endofstorage; //指向存储容量的尾(尾是未开辟的空间)
};
成员变量解释
2. 迭代器相关接口
2.1 begin 和 cbegin
原型:iterator begin();
作用:返回一个指向第首元素的迭代器
原型:const_iterator cbegin() const;
作用:返回一个指向首元素的 const 迭代器
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
// begin
iterator begin()
{
return _first;
}
// cbegin
const_iterator cbegin() const
{
return _first;
}
private:
iterator _first;
iterator _finish;
iterator _endofstorage;
};
2.2 end 和 cend
原型:iterator end();
作用:返回一个指向最后一个元素的下一个位置的普通迭代器
原型:const_iterator cend() const;
作用:返回一个指向最后一个元素的下一个位置的 const 迭代器
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//end
iterator end()
{
return _finish;
}
//cend
const_iterator cend()const
{
return _finish;
}
private:
iterator _first;
iterator _finish;
iterator _endofstorage;
};
3. 容量操作接口
3.1 size 和 capacity
原型:size_type size() const;
作用:返回 vector 对象中有效数据的个数
原型:size_type capacity() const;
作用:返回 vector 对象的容量大小
注意指针减指针的结果并不是单纯的数字减法的结果,编译器会根据指针类型把数字减法的结果除以其权重 sizeof(T),得到中间元素的个数。
指针相减的原理:
1、计算间隔的字节数
2、除以权重 sizeof(T)。
//size
size_t size()const
{
return _finish - _first;
}
//capacity
size_t capacity()const
{
return _endofstorage - _first;
}
3.2 reserve
原型:void reserve(size_t n);
作用:调整 vector 对象的容量
规则:
- n > 对象的容量(capacity()):开新空间扩容(还要拷贝旧空间的数据、释放旧空间)
- n <= 对象的容量(capacity()):啥事都不干
void reserve(const size_t n)
{
if (n > capacity())
{
// 1、开新空间
size_t len = size();
T* tmp = new T[n];
// 2、拷贝数据
for (size_t i = 0; i < len; ++i)
tmp[i] = _first[i];
// 3、释放旧空间
delete []_first;
_first = tmp;
tmp = nullptr;
_finish = _first + len;
_endofStorage = _first + n;
}
}
问题:拷贝数据时能否用 memecpy 函数?
在回答这个问题之前我们先来看看 memcpy 函数的拷贝原理,其实就是按照字节的内容,逐个字节地去拷贝。
// 传入数据可能是各种类型的,所以用void*接收
void* memcpy(void* dest, const void* src, size_t num)
{
//断言,判断指针的有效性,防止野指针
assert(dest!=NULL);
assert(src!=NULL);
// void* 可以接收所有类型的指针
// 不可以进行解引用和加减的操作,但可以比较大小
void* tmp = dest;
while (num--)
{
//把指针类型转化为char*在解引用和+1/-1时可以访问一个字节
*(char*)dest = *(char*)src;
((char*)dest)++;
((char*)src)++;
}
return tmp;
}
如果我们的元素是内置类型(int、char、double 等)还好,但如果类型是像 string 类对象那样,成员变量是一个指向内存空间的指针的话,memcpy 就会造成浅拷贝。
3.3 resize
原型:void resize(size_t n, const T& val = T());
作用:调整 vector 有效数据的内容和大小
- n > 有效元素个数(size()):多出来的有效空间用 val 去填补,如果不传 val 的话,就填补对应数据类型的默认值 T();如果大于 capacity() 的话还会扩容。
- n <= 有效元素个数(size()):会截断多出来的有效数据
void resize(size_t n, const T& val = T())
{
// 1.当n大于其有效数据长度的处理
if (n > size())
{
if (n > capacity())
{
reserve(n);
}
iterator finish = _first + n;
while (_finish != finish)
{
*_finish = val;
++_finish;
}
}
else // 2.当n小于等于其有效数据长度的处理
{
_finish = _first + n;
}
}
补充:关于内置类型的构造函数
我们 resize 参数列表中有个形参const T& val = T(),这里的 T 是元素的类型,T() 就是这个类型的一个匿名对象。如果 T 是自定义类型的话就调用它的默认构造函数去初始化这个匿名对象,但如果 T 是内置类型的话,像 int,double,char 等,他们也有自己的默认构造函数吗?
4. 默认成员函数
4.1 构造函数
类型一:默认构造函数
原型:vector()
作用:默认构造函数
参数什么都没传,那构造出来的就是空对象。它内部没有开辟空间,其成员变量的值都为 nullptr。
vector()
:_first(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
类型二:初始化构造 n 个 val
原型:vector(size_t n, const T& val = T())
作用:构造并初始化 n 个 val
vector(size_t n, const T& val = T())
// 1.先把成员变量初始化为空
:_first(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
// 直接调用 resize() 完帮助我们成初始化工作
resize(n, val);
}
问题:既然是初始化构造 n 个 val,直接在函数体里构造就行了,在初始化列表把成员变量初始化为空还有必要吗?
4.2 拷贝构造
vector(const vector& v)
:_first(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
// 1.开和v一样大的空间
reserve(v.capacity());
// 2.遍历v,利用迭代器拷贝数据
const_iterator it = v.cbegin();
const_iterator finish = v.cend();
while (it != finish)
{
*_finish++ = *it++;
}
}
补充:拷贝构造的几点说明
4.3 赋值重载
vector<T>& operator=(const vector<T> v)
{
swap(v);
return *this;
}
//成员函数 vector::swap
void swap(vector<T>& v)
{
//相互交换各自指向的地址
//下面的swap是std的swap
std::swap(_first, v._first);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
补充:区分成员函数 vector::swap 和 std 标准库中的 std::swap
补充:参数和返回值说明
5. 修改操作接口
5.1 insert
原型:iterator insert(iterator pos, const T& val);
作用:在 pos 位置插入一个元素
iterator insert(iterator pos, const T& val)
{
// 插入之前检查是否需要扩容
if (_finish == _endofstorage)
{
size_t len = pos-_first;
size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;
reserve(newcapacity);
// 更新迭代器(下面图里有说明)
pos = _first + len;
}
// end为最后一个有效数据的迭代器
iterator end = _finish-1;
// 挪动数据
while (pos <= end)
{
*(end + 1) = *end;
--end;
}
// 插入数据
*pos = val;
++_finish;
return pos;
}
补充:insert 涉及到的迭代器失效问题
①增容导致外面传入的迭代器 pos 变成野指针
因为增容的过程是:开新空间、拷贝数据、释放旧空间。在增容后外面传入的迭代器 pos 原来指向的空间被释,变成了野指针。造成这个问题的原因是原空间被释放,而外面的 pos 不能及时更新。
当然对于 pos 这个参数我们是传值进来的,虽然我们在函数里面更新了它,但是外面的 pos 其实还是失效的,它指向一块已经被释放的空间,变成了野指针。如果是传引用的话就可以解决,不过STL库并没有这样做,而是通过返回值去返回更新后的迭代器。
②插入数据导致迭代器的 pos 的意义改变
这是在没有增容的情况下发生的迭代器失效问题,插入后外面的迭代器 pos 指向的值变成了新插入的 val。造成这个问题的原因是 vector 的存储空间是连续的,外面的 pos 永远指向内存空间的那块位置,而 insert 就是要挪动 pos 以及 pos 位置以后的数据来完成数据插入,最后在 pos 位置上插入新的数据就是 val。
总结:insert 迭代器失效问题总结
在使用 insert 函数后,原来 pos 的迭代器最好别用了,因为我们不确定是否增容导致它变成野指针,还是没增容但是它的意义改变了。要用的话就更新它,即让它接收 insert 的返回值,这个返回值一定是有效的,但是我们要清楚返回的那个迭代器,它的值是指向新插入的那个元素的。
问:插入一个数据,能否用 memset?
总结:memcpy 和 memset 拷贝数据总结
拷贝数据时慎用 memcpy 和 memset,前者如果拷贝的是涉及到内存管理的数据类型,比如 string 的话会造成浅拷贝;后者只是按 value 一个字节的内容来拷贝的。最保险的拷贝方式还是遍历一遍原空间,然后单独拿出空间中的每个一数据实现拷贝。
5.2 erase
原型:iterator erase(iterator pos);
作用:删除 pos 位置上的元素
iterator erase(iterator pos)
{
//把 pos 之后的数据一个一个地往前挪
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
++begin;
}
--_finish;
return pos;
}
erase 涉及到的迭代器失效问题
①缩容导致外面传入的迭代器pos变成野指针
有的编译器,如果你一直删除数据,而导致太多空间空出来不同,这时编译器会把你的容量缩小:开辟小空间、拷贝数据、释放旧的大空间。和 insert 一样,此时外面的迭代器 pos 变成了野指针。
②删除导致外面传入的迭代器 pos 意义改变
在未缩容的情况下,erase 删除一个数据后,外面 pos 的值变成了那个被删除数据的后一个位置元素的值,这时因为 vector 的存储空间是连续的,而 pos 永远指向空间的那个位置,删除一个数据就是把后面位置的数据往前挪,pos 指向的内存空间的值改变了。
关于erase写法的几点说明