欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 产业 > 【C++】vector的底层封装和实现

【C++】vector的底层封装和实现

2025/5/19 23:32:21 来源:https://blog.csdn.net/apple_61439616/article/details/147052149  浏览:    关键词:【C++】vector的底层封装和实现

目录

  • 目录
    • 前言
    • 基本框架
    • 迭代器
    • 容量
      • 第一个测试,野指针异常
      • 第二轮测试,浅拷贝的问题
    • 元素访问
    • 修改操作
      • push_back
      • insert
        • 迭代器失效问题
      • erase
    • 默认成员函数
      • 构造函数
        • 双重构造引发调用歧义
      • 拷贝构造
      • 赋值重载
      • 析构函数
    • 源码
    • end

目录

前言

废话不多说,我们直接来模拟实现vector的底层实现,有了上篇文章string的底层封装和模拟实现,相信大家已经对stl的容器的底层有了一定的了解

基本框架

namespace bit {template<class T>class vector {public:typedef T* iterator;typedef const T* const_iterator;// 主要接口函数private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}
  • 这里我们的三个成员变量参考源码中的实现方式,用指针来模拟实现。
  • 并且采用缺省参数的形式将它们都初始化为nullptr,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了

迭代器

  • 这里就很简单的返回我们指向开头的指针和指向最后一个元素的指针就行了
iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin()  const
{return _start;
}const_iterator end()	const
{return _finish;
}

容量

  • 如何获取元素个数和内存容量,在C语言中我们讲过指针减去指针就是他们之间的元素个数,如此即可
size_t size()
{return _finish - _start;
}size_t capacity()
{return _end_of_storage - _start;
}
  • 这里我们来重点说一下这个reserve扩容函数,他牵扯到一个很大的问题。

在这里插入图片描述

  • 我们这里再写一个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++;
}

第一个测试,野指针异常

  • 测试代码
bit::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);

结果:
在这里插入图片描述

  • 可以看到,程序崩溃了,那是什么原因了,不妨调试看一下
    在这里插入图片描述
  • 插入的时候对空指针解引用,我们再一步步看看_finish指针为啥是空指针
_finish = _start + size();
  • 这个时候就知道了,我们扩容的时候_start已经更新到tmp新空间的位置,但是更新_finish的时候,size()函数的结构是这样的
    在这里插入图片描述
  • 但是现在我们的_start已经是更新到tmp新空间的位置上去了,这时候_finish是旧空间,两个指针不是同一块空间相减就是一个随机值
    在这里插入图片描述
  • 那么又如何来解决这个问题呢?
  • 根据上面的描述,我们发现其实就是因为更新_finish我们调用了size函数造成的问题,那么这个时候我们其实就是想要知道元素个数,但是必须要在_start更新之前,所以我们就在_start更新之前获取元素个数即可
if (n > capacity())
{// 先保存一下原先的size()size_t sz = size();T* tmp = new T[n];		// 开一块新空间if (_start){memcpy(tmp, _start, sizeof(T) * size());delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;
}

第二轮测试,浅拷贝的问题

  • 下面是我们要进行第二轮测试的代码,内部类型使用的是 string类
void test_vector2()
{bit::vector<string> v;v.push_back("11111");v.push_back("22222");v.push_back("33333");v.push_back("44444");for (auto e : v){cout << e << " ";}cout << endl;
}
  • 运行起来看并没有什么问题
    在这里插入图片描述
  • 但是呢当我再去push_back(“55555”)的时候程序却出现了问题,这个时候发生了扩容

在这里插入图片描述

  • 就是对于自定义类型,虽然我们的确是tmp独立开了一块新空间,但是memcpy函数就是把指针_str的值拷贝过来,导致原来的_str和tmp的_str指向同一块空间。就会发生两个指针指向同一块空间。
  • 那么在执行下面这句代码的时候,先会去把this指针的_str指向的那块空间析构掉,这个时候因为他们指向的是同一块空间,改变_str也同时更改了tmp中的_str
delete[] _start;

在这里插入图片描述

  • 那么知道了是什么问题,我们又该如何解决这个问题呢?
  • 我们知道导致这个原因的就是因为memcpy是浅拷贝,那么我们实现一个深拷贝就行了

这里我们就直接执行下面的代码:

for (size_t i = 0; i < OldSize; i++)
{tmp[i] = _start[i];
}
  • 这里的逻辑就是把_start的每个值赋值给tmp用来更新新的_start,这个时候就要重载一下赋值重载完成深拷贝,这会在后面介绍,请往下看。

接下来再看一下resize这个函数的实现

void resize(size_t n, const T& val = T())
{if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;} }
}
  • 如果重置的长度小于原来的长度,那么直接把_finish更新到_start + n的位置,就是n个长度就行
  • 如果大于原来的长度,直接扩容到n个,如果扩容到n个后_finish还是小于n个的时候,再把_finish更新到n个。

元素访问

  • 对于元素访问的话我们最常用的就是下标 + []的形式,这里给出两种,一个是const版本和非const版本
T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}T& operator[](size_t pos)	const
{assert(pos < size());return _start[pos];
}

修改操作

push_back

void push_back(const T& x)
{if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;
}
  • 这一块的逻辑非常简单,当最后一个元素的指针和容量的指针位置相同的时候,就需要扩容,否则直接插入,更新_finish指针就行了

insert

void insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);// 1.首先考虑扩容逻辑if (_finish == _end_of_storage){size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapacity);}// 2.挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;
}
  • 这一块同理,插入数据先考虑是否需要扩容
  • 然后把pos位置后面的数据往后面挪,知道pos位置插入x即可。
迭代器失效问题

1.这里指定位置插入函数,如果空间不管扩容的时候就出现了迭代器失效的问题
2.如果不更新pos的位置,那我们还在释放的旧空间里面插入,就会发生访问野指针!

在这里插入图片描述

  • 那么如何去解决这个问题呢?

首先我们要明确导致这个问题发生的就是pos没有正确的更新,pos需要更新到新空间和原空间的相应位置,那么我们就算出旧空间中pos的相对位置,在新空间开辟后,把pos更新到新空间的相对位置就行了

  • 但是呢就上面这样还不够,我们只解决了内部迭代器失效的问题,而外部迭代器失效的问题并没有很好地解决。
  • 外部迭代器又是个什么东西,接下来看看这段代码
bit::vector<int>::iterator it = v.begin();
v.insert(it, 33);
bit::print(v);cout << *it << endl;bit::print(v);
  • 可以看到,在使用完这个这个迭代器之后再去访问就出现了问题
    在这里插入图片描述
  • 这是因为,形参迭代器的传值并不能改变外面的实参迭代器。
  • 有的同学就会说,那简单,传个引用就行了,但是你再来看看这个情况
v.insert(v.begin() + 3, 6);

在这里插入图片描述

在这里插入图片描述

erase

  • 对于【erase】来说,我们也是需要先去挪动数据的,但是在这里呢我们需要从前往后挪,也是防止造成覆盖的情况
void erase(iterator pos)
{assert(pos >= _start && pos < _finish);iterator end = pos + 1;// 移动覆盖while (end != _finish){*(end - 1) = *end;++end;}--_finish;
}
  • 立马来测试一下

在这里插入图片描述

  • 看起来并没啥问题,那接下来看看下面的2个场景
  • 场景1:我们在中间插入两个偶数,现在我们要对容器中的偶数进行删除,可以看看下方结果
    在这里插入图片描述
    在这里插入图片描述
  • 可以看到发生了逻辑错误,我们的目的是把所有的偶数都删除了,可是现在容器中还有偶数!
    Why?
    如下图,我们删除一个数后,会把他前面的一个数挪到被删除数字的位置,那么这个时候it++,如果被挪到到删除位置的数是一个偶数就会漏掉这个数的判断。
    在这里插入图片描述
  • 场景2:如果最后一个元素是偶数,那么删除会咋样呢?如图看看我们的代码,那这样把最后一个元素删除后,不就越界访问了吗,it 会永远不等于 _finish
    在这里插入图片描述
  • 这个时候有人说,我们就要修改一下,当删除偶数的时候,不要让it++,这是对的,但还缺点东西
    在这里插入图片描述
  • 可是,万一别人底层实现erase很多次,容器的数据变少了,别人要缩容呢? 缩容是异地缩容,就是重新开一段比之前小的空间,把原来的空间数据拷贝过去,最后释放原来的空间,异地缩容是因为C++开辟内存只能是连续的内存,所以释放的时候不能只释放一部分。这时候it不就又变成,inset中的那样野指针的问题导致迭代器失效了吗?
    在这里插入图片描述
  • 这个时候,就返回被删除元素的下一个元素的位置值,这里返回pos,是因为我们的算法是被删除元素的下一个位置元素移到这个被删除元素的位置,所以被删除元素的下一个元素的位置就是pos本身.当然这里我们没有实现缩容,但是如果这里缩容,就会更新pos的位置,把新pos返回的
    在这里插入图片描述

默认成员函数

构造函数

  • 首先的话一定是构造函数,有参构造是一定要实现的,因为这里的逻辑和resize()是类似的,因此我们直接去做一个复用即可
// 有参构造
vector(size_t n, const T& val = T())
{resize(n, val);
}
  • 那有同学可能会问,三个私有成员变量不需要去做初始化吗?
  • 这个时候我们的缺省值就发挥作用了,很好的避免了忘记初始化三个成员变量的问题
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;
  • 除了上面这种初始化,我再介绍一种方法:那就是使用 迭代器区间
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}
  • 复用push_back接口就行了
  • 我们再补充一个构造
		vector(initializer_list<T> lt):_start(nullptr),_finish(nullptr),_endofstorage(nullptr){//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器for (auto e : lt){push_back(e);}}
  • 这个考前使用迭代器就行,他底层是类似数组
双重构造引发调用歧义

在这里插入图片描述

  • 那么如何解决呢?我们模板匹配还有个原则就是有现成吃现成的,如果有int类型的构造,模板就不用去实例化,所以重载一个int类型的,让他匹配

在这里插入图片描述

拷贝构造

vector(const vector<T>& v)
{reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率for (auto& e : v){push_back(e);}
}
  • push_back扩容的时候完成深拷贝

赋值重载

vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}
  • 这里的逻辑类似string里面的,this想要得到v的资源并且把原来的资源抛弃,得到v的资源直接交换他们指向的指针就行了。由于v是局部变量,调用完会析构,就把this的资源析构了。

在这里插入图片描述

析构函数

~vector()
{if (_start){delete[] _start;	//同一块空间_start = _finish = _endofstorage = nullptr;}
}

源码

#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
namespace bit
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr){}vector(initializer_list<T> lt):_start(nullptr),_finish(nullptr),_endofstorage(nullptr){//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器for (auto e : lt){push_back(e);}}vector(const vector<T>& v){reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率for (auto& e : v){push_back(e);}}template <class InputIterator>	//类模板的成员函数也可以是一个函数模板, 这里的迭代器可以是任何类型如:string, list的迭代器构造vector(InputIterator first, InputIterator last)		  {while (first != last){push_back(*first);first++;}}vector(size_t n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}}vector(int n, const T& val = T()){reserve(n);for (int i = 0; i < n; i++){push_back(val);}}void swap(vector<T>& tmp){std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);}vector<T>& operator=(vector<T> v){swap(v);return *this;}~vector(){if (_start){delete[] _start;	//同一块空间_start = _finish = _endofstorage = nullptr;}}iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}T& operator[](size_t i){assert(i < size());return _start[i];}const T& operator[](size_t i) const{assert(i < size());return _start[i];}size_t size() const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}void reserve(size_t n){size_t	OldSize = size();if (n > capacity()){T* tmp = new T[n];for (size_t i = 0; i < OldSize; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = _start + OldSize;_endofstorage = _start + n;}}void push_back(const T& x){if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;}bool empty() const{return size() == 0;}void pop_back(){assert(!empty()); --_finish;}void resize(size_t n, const T& val = T()){if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;} }}iterator insert(iterator pos, const T& x)	//这里改成引用我希望形参可以改变实参{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());pos = _start + len;}iterator end = _finish;while (end != pos){*end = *(end - 1);end--;}*pos = x;_finish++;return pos;}iterator erase(iterator pos){assert(pos >= _start && pos < _finish);iterator i = pos + 1;while (i < _finish){*(i - 1) = *i;i++;}_finish--;return pos;}private:iterator _start = nullptr;	//写一个类一定要把缺省参数写上,防止初始化列表没初始化导致的一系列越权访问问题iterator _finish = nullptr;iterator _endofstorage = nullptr;};
}

end

感谢阅读,希望对大家有所帮助。快去实现一下吧

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词