目录
一.vector类的成员变量:
二.Vector类的初始化方式:
三.vector的基本成员函数
四.vector类的增删查改:
指针失效问题:
insert():
代码解析:
erase():
代码解析:
所以erase()函数的正确写法:
五迭代器:
六:构造函数新写法:
6.2非法寻址报错
解决方法:
七.拷贝构造和赋值重载
7.1拷贝构造:
7.2赋值重载函数
在上篇博客中,我们主要学习了STL的容器之一——vector(顺序表),理解了它的底层原理,在多个例子的测试和运行结果中,我们学会了众多成员函数的特性,感兴趣的小伙伴们可以点击下方的链接看一看:
zzC++STL——vector类_橙予清的zzz~的博客-CSDN博客https://blog.csdn.net/weixin_69283129/article/details/131899708?spm=1001.2014.3001.5502
为了更加深刻的理解vector容器,今天我们就来剖析剖析vector类的底层实现代码!
注:此次实现只是能够大致模拟出vector的底层原理和成员函数实现!
一.vector类的成员变量:
template <class T>
class vector {
public:
typedef T* iterator;
private:
iterator _start;
iterator _finish;
iterator _endof_enocrage;
};
由上可知:vector作为顺序表可以存储任意类型的数据,那么就需要用到泛型模板,而成员变量的类型全都是泛型(T*)指针:
成员1:_start指向的是容器vector里指向数组的首地址;
成员2:_finish指向的是容器vector数组中存放的最后一个数据位置的下一个位置;
成员3:_endof_storage指向的是容器vector数组中总容量的下一个位置。
二.Vector类的初始化方式:
template <class T>
class vector {
public:
typedef T* iterator;
vector() //构造函数——无参构造
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr)
{
}
//扩容函数
void reserve(size_t n) {
if (n > capacity()) {
size_t len = size(); //标记原来的存储长度
T* tmp = new T[n]; //默认为异地扩容
//memcpy(_start, tmp, sizeof(T) * len);
for (size_t i = 0; i < len; ++i) {
tmp[i] = _start[i]; //将原来空间的数据传到新空间中
}
}
delete[] _start; //释放旧空间
_start = tmp;
_endof_enocrage = _start + n; //重新赋值
_finish = _start + len;
}
}
~vector() { //析构
delete[] _start;
_start = _finish = _endof_enocrage = nullptr;
}
private:
iterator _start;
iterator _finish;
iterator _endof_enocrage;
};
因为成员变量都是指针的缘故,构造函数对其成员变量做初始化只需要先构造为空指针即可——利用初始化列表进行。
若是对vector类对象增添数据时,需要提前判断,并且使用reserve扩容函数进行即可。
而析构函数中只需要删除_start指向的堆区空间即可,因为_start永远指向空间的首元素地址,_start一释放,_finish和_endof_enocrage就全变为了野指针,到时候只需要置空即可。
三.vector的基本成员函数
根据这些成员变量我们可以轻松的写出这些函数的底层实现:
bool empty() const { //判空
return _finish == _start;
}
void clear() {
_finish = _start; //只清数据,不释放空间,也不清容量
}
size_t size()const { //获取数据存储长度
return _finish - _start;
}
size_t capacity() const { //获取数组现有容量
return _endof_enocrage - _start;
}
size_t max_size() const { //获取数组能存储的最大数据数量
size_t max = -1;
return (max / 4);
}
void resize(size_t n, T val = T()) { //调整数组已存数据大小,分三种情况
if (n > capacity()) {
reserve(n);
}
if (n > size()) {
while (_finish < _endof_enocrage) {
*_finish = val;
++_finish;
}
}
else {
_finish = _start + n;
}
}
对于size()和capacity()函数来说,使用的都是指针-指针的方式求出整型数据量。
对resize()函数底层看不懂的,可以去上边我发的链接中了解了解resize()函数的功能特性!
四.vector类的增删查改:
template <class T>
class vector {
public:
void push_back(const T& val) { //尾插
//若插入的时候空间不够,先扩容
if (_finish == _endof_enocrage) {
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = val; //插入数据
++_finish;
}
void pop_back() { //尾删
assert(!empty());
--_finish;
}
//[]重载运算符函数——用于查看或者修改vector类对象的数据
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
};
指针失效问题:
这个问题关系到insert()和erase()两个函数的使用:
insert():
iterator insert(iterator pos, const T& val) {
assert(pos >= _start);
assert(pos < _finish);
size_t len = pos - _start;
if (_finish == _endof_enocrage) {
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity); //扩容
//更新pos位置
pos = _start + len;
}
//挪动位置
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//插入数据
*pos = val;
++_finish;
return pos; //返回pos即可
}
代码解析:
语句1: assert(pos >= _start); //允许头插,中间插
语句2: assert(pos < _finish); //不允许尾插,因为有push_back()语句3: size_t len = pos - _start; //记录扩容前的pos位置,这是为下文埋下的伏笔!
语句4: pos = _start + len; //因为扩容后pos的位置会失效,所以需要重新更新pos的指向:pos的类型是指针,且pos也是指向vector数组空间的某个位置的,若是当前vector数组可能已经满容量了,需要进行扩容,意味着会发生异地扩容的情况,异地扩容后,系统会为其重新分配一块空间,那么原来的地址空间就失效了, pos原本指向旧地址空间的特定,旧空间的地址也就不属于vector了,pos成为野指针!,所以需要重新变更pos位置的值才行!!!——这就是指针失效的解决方法。
剩下的就是挪动pos位置的数据,为pos位置留下一个数据空位,供该位置增添数据。
举个例子:
如上图:就是在该数组的元素3位置(pos)处增添一个数据9,pos的值应该是:_start+8处(_start指向的数组首元素地址,pos位置的值就需是指向_start的两个元素(int是4字节)之后的地址) 。
现在由于该数组容量已满,需要扩容,重新划分一块堆区空间:
那么pos的值就不能再指向旧空间的0x1122334c了,需要重新根据_start的地址进行+8处理。,否则就是产生insert指针失效问题!
erase():
void erase(iterator pos) {
assert(pos >= _start);
assert(pos < _finish);
iterator end = pos + 1;
//挪动数据
while (end < _finish) {
*(end - 1) = *end;
++end;
}
--_finish;
}
代码解析:
erase函数的指针失效问题在于案例测试上,代码没有什么需要注意的地方,因为删除元素不会影响扩容缩容等问题,_start和pos位置也就不需要变更。
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上指针不应该会失效。
但是,若是出现了下面这种情况:即pos刚好是指向最后一个元素位置,那么删完之后,成员变量_finish指针就会往前挪一个元素位置,pos也就与该指针指向同一块位置,而endof_enocrage的位置是没有元素存储的,那么pos现在就处于越界状态。这种特殊情况会导致erase后指针失效,那么之后想要再使用之前pos指针就会发生报错等问题!为了统一普通情况和特殊情况pos位置的安全性,我们就认为erase后,pos位置指针失效了!
所以解决方法为:加上erase的函数返回值类型为pos指针类型,每次使用完erase函数后都及时在外部更新pos位置。这样就不会导致其失效了。
所以erase()函数的正确写法:
iterator erase(iterator pos) {
assert(pos >= _start);
assert(pos < _finish);
iterator end = pos + 1;
while (end < _finish) {
*(end - 1) = *end;
++end;
}
--_finish;
return pos;
}
加上返回值和函数返回值类型。
试验代码:
void Test15() {
Cheng::vector<int> v1;
for (int i = 1; i < 5; ++i) {
v1.push_back(i);
}
for (auto& e : v1) {
cout << e << " ";
}
cout << endl;
//删除所有偶数
auto pos = v1.begin();
while (pos != v1.end()) {
if (*pos % 2 == 0) {
pos=v1.erase(pos); //更新it,否则it会失效,出现错误
}
//若*it%2!=0,则++it(跳过)
else {
++pos;
}
}
五迭代器:
template <class T>
class vector {
public:
typedef T* iterator;
//迭代器
iterator begin() {
return _start;
}
iterator end() {
return _finish;
}
//const迭代器
const_iterator begin() const {
return _start;
}
const_iterator end() const {
return _finish;
}
private:
iterator _start;
iterator _finish;
iterator _endof_enocrage;
};
由于迭代器的begin、end等都是指针,那么类型自然与成员变量的类型相同都是泛型指针。
六:构造函数新写法:
如上图,这是C++官方库中给出的关于vector类的构造函数,有很多种构造函数,有了这些构造函数,可以让我们构建不同的vector对象,如下图:
所以我们也可以构建几个常用的vector构造函数进行实现:
vector() //无参构造法
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr)
{
}
//vector带参构造函数法
vector(size_t n,const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr) {
reserve(n);
for (int i = 0; i < n; ++i) {
push_back(val);
}
}
//迭代器区间构造法
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_endof_enocrage(nullptr) {
while (first != last) {
push_back(*first);
++first;
}
}
6.2非法寻址报错
在测试使用这些构造函数时,我发现了一个问题,当我执行下面这两句代码时,代码产生了编译报错!:
vector<int> v1(5);vector<int> v2(5,'a');
class vector{
public:
//vector带参构造函数法
vector(size_t n,const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr) {
reserve(n);
for (int i = 0; i < n; ++i) {
push_back(val);
}
}
//迭代器区间构造法
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_endof_enocrage(nullptr) {
while (first != last) {
push_back(*first);
++first;
}
}
void push_back(const T& val) { //尾插
//若插入的时候空间不够,先扩容
if (_finish == _endof_enocrage) {
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = val; //插入数据
++_finish;
}
private:
iterator _start;
iterator _finish;
iterator _endof_enocrage;
};
int main(){
vector<int> v1;
for (size_t i = 0; i <4; ++i) {
v1.push_back(i);
}
vector<int> v2(5,1); //本意:创建5个元素,且全赋值为1
}
传两个参数报错:非法的间接寻址
原因在于将vector类对象实例化时,编译器会根据类对象后面给出的参数进而寻找最匹配的构造函数,5,1都是int类型,和半缺省的size_t和T虽不是最匹配,但能用;而和迭代器区间构造函数InputIterator最匹配,但这俩参数并不能代入迭代器区间中,编译器不能理解,产生了编译报错!
解决方法:
//vector带参构造函数法
vector(size_t n,const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr) {
reserve(n);
for (int i = 0; i < n; ++i) {
push_back(val);
}
}
//vector带参构造函数
vector(int n,const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr) {
reserve(n);
for (int i = 0; i < n; ++i) {
push_back(val);
}
}
//迭代器区间构造法
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_endof_enocrage(nullptr) {
while (first != last) {
push_back(*first);
++first;
}
}
再写一个int类型的vector带参构造函数,这样创建类对象时vector<int> v2(5,1); 编译器寻找最匹配的构造函数时会先匹配int类型的构造函数,这样就不会报寻址错误了!
七.拷贝构造和赋值重载
7.1拷贝构造:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endof_enocrage(nullptr)
{
_start = new T[v.capacity()];
size_t len = v.size();
memcpy(_start, v._start, sizeof(T) * len);
_finish = _start+len;
size_t capa = v.capacity();
_endof_enocrage = _start + capa;
}
拷贝构造的本质上就是拷贝形参v的所有数据,而默认构造函数的拷贝方式是浅拷贝,对于内置类型来说,浅拷贝是正常的做法;但对于有指针开辟堆区空间的成员来说,浅拷贝无疑会造成内存泄漏,析构同一块空间多次的情况,所以浅拷贝得自己写才行!
所以针对深拷贝的情况,那么就得让拷贝的对象拥有一块自己的空间,剩下的就是复制被拷贝对象的数据了。
上面的代码为传统写法的代码,而拷贝构造还有一种更间接方便的形式:
//交换函数
void Swap(vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endof_enocrage, v._endof_enocrage);
}
//拷贝构造
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endof_enocrage(nullptr)
{
//引用传参就得新创建对象tmp,否则直接传swap(v)就是浅拷贝
vector<T> tmp(v.begin(), v.end());
Swap(tmp);
}
首先,指派一个打工人帮你把被拷贝对象的数据做构造,你什么都不需要干,等打工人帮你把事情做好后,你就可以获取打工人所做的一切成果,然后把你自己的一无所有给了打工人,实现两者的交换,这样就完成了拷贝构造。
注:而对于两者的交换函数,我采用的是std库中的swap函数,利用this指针与形参的各个成员变量值做交换。
创建临时对象tmp时,编译器调用的是迭代器区间构造函数,意味着,tmp是自己开辟的一块堆区空间,并不是指向的v对象的_start空间,tmp也只是拷贝了对象v除_start以外的数据罢了,不会发生一块空间析构两次的情况!
7.2赋值重载函数
赋值重载函数与拷贝构造有着异曲同工之妙,所以赋值重载函数的传统写法我也就不写了。
直接展示新写法:
vector<T>& operator=(const vector<T>& v) {
//(引用传参不会在函数中开空间)
//所以需要创建临时对象(实例化空间)去swap拷贝
vector<T> tmp(v.begin(), v.end());
this->Swap(tmp);
return *this;
}
赋值重载的新写法也是如出一辙,都是利用打工人去帮你做事情,然后使用交换函数,将各个成员变量进行交换,依次得到拷贝好的数据!