• C++入门第七篇--STL模板--vector模拟实现


    前言:

    有了前面的string库的介绍,在这里我就不再介绍vector库了,而是直接模拟实现了。

    vector库的概念和作用:

    vector库是针对于数组的数据类型的容器,它有点类似我们曾经实现过的顺序表,你完全可以按照顺序表去理解vector,针对顺序表,我们自然少不了增删查改的功能,所以接下来让我们模拟实现一下vector库。

    模拟实现过程:

    1.私有成员变量的设置:

    在这里,我们这样设置我们的私有成员变量,由于文档中C/C++库的函数大部分是用迭代器实现的,故我们模拟的时候也使用迭代器去操作,故成员如下:

    private:
    	iterator _start;
    	iterator _finish;
    	iterator _endofstorage;
    
    • 1
    • 2
    • 3
    • 4

    其中_start指向顺序表开头,_finish指向顺序表的数字的结尾,而_endofstorage则控制容量,指向容量的结尾
    但是我们C++中是没有所谓的iterator的,但是我们知道iterator的本质是指针,故我们对类型重命名如下:

    typedef T* iterator;
    typedef const T* const_iterator;
    
    • 1
    • 2

    2.构造函数 析构函数 拷贝构造函数:

    私有成员建立好后,我们下一个便是构建基本的三个函数:构造,析构,拷贝构造。
    首先是构造函数:

    vector()
    	:_start(nullptr)
    	, _finish(nullptr)
    	, _endofstorage(nullptr)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里就是常规的全让指针为空,因为我们会在调整容量的位置去为这三个成员变量赋值
    析构函数:

    ~vector()
    {
    	delete[] _start;
    	_start = _finish = _endofstorage = nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里需要注意的点就是:我们是需要在堆区上动态开辟空间的,故我们的析构函数是必须显式实例化的,要让析构函数释放掉我们的堆区空间。
    拷贝构造函数:

    vector(const vector<T>& s)
    	:_start(nullptr)
    	, _finish(nullptr)
    	, _endofstorage(nullptr)
    {
    	for (auto& ch : s)
    	{
    		push_back(ch);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再拷贝构造这里,我使用了遍历尾插的方式,或许你会说,直接memcpy不是更好么,但是我们的顺序表不仅仅要存储内置类型,有时也要存储自定义类型,而memcpy对应的是一种浅拷贝,一旦涉及到指针的问题,就会有多次释放的危险,故我们在这里采取尾插的方式,即自定义类型会调用其赋值运算符重载,内置类型则直接赋值,这样就很好的避免了多次释放的问题。

    3.赋值运算符重载:

    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> tmp)
    {
    	swap(tmp);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我在这里采取的现代写法,和string一样,采用一个变量tmp来打工的方式将值转给*this,大体的写法概念不变,我这里不过多赘述了。

    4.size长度 capacity容量:

    size用来返回顺序表的长度,而capacity用来返回顺序表的容量

    size_t size() const
    {
    	return _finish - _start;
    }
    size_t capacity() const
    {
    	return _endofstorage - _start;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5.首尾迭代器返回:

    iterator begin()
    {
    	return _start;
    }
    iterator end()
    {
    	return _finish;
    }
    const_iterator begin()const
    {
    	return _start;
    }
    const_iterator end()const
    {
    	return _finish;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里我们写了两个版本,一个是可读可写版本,一个是可读不可写版本,分别返回不同的迭代器

    6.下标访问操作符[]重载:

    其基本和我们字符串的写法区别不大:

    T& operator[](size_t pos)
    {
    	assert(pos < size());
    	return _start[pos];
    }
    const T& operator[](size_t pos) const
    {
    	assert(pos < size());
    	return _start[pos];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    同样也是两个版本,可改与不可改

    7.扩容!!!(本次实现的重点!)

    扩容,是我们本次实现的重点,在这里牵扯到一个很关键的问题:迭代器失效。
    何为迭代器失效呢?
    先看下面的代码:

    void reserve(size_t n)
    {
    	if (n > capacity())
    	{
    		T* tmp = new T[n];
    		if (_start)
    		{
    			int i = 0;
    			for (i = 0; i < size(); i++)
    			{
    				tmp[i] = _start[i];
    			}
    			delete _start;
    		}
    		_start = tmp;
    		_finish = _start + size();
    		_endofstorage = _start + capacirty();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这段代码就涉及到严重的迭代器失效的问题,问题出在我们的delete被销毁后,我们对应的size()和capacity()本质上指向的是之前的被销毁的数组的地址,这样我们使用函数是得不到正确的长度的,因为迭代器此时的地址是无效的,这便是我们所谓的迭代器失效,你可以看这张图理解:
    在这里插入图片描述
    所以,我们可以这样去修改程序:

    void reserve(size_t n)
    {
    	size_t sz = size();
    	if (n > capacity())
    	{
    		T* tmp = new T[n];
    		if (_start)
    		{
    			int i = 0;
    			for (i = 0; i < size(); i++)
    			{
    				tmp[i] = _start[i];
    			}
    			delete _start;
    		}
    		_start = tmp;
    		_finish = _start + sz;
    		_endofstorage = _start + n;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    即先用一个变量将长度存储起来,而不是再用失效的迭代器返回长度和容量,在这里就是sz来存储,这样,我们就不会出现我们的长度是错误的问题了。

    7.改变数组长度:

    	void resize(size_t n, const T& x = T())//改变数组长度
    	{              //注意,内置类型是可以调用构造函数的,在模板这个章节是支持的,在这里别忘了加一个const,因为我们的缺省值是常量,不加const引用权限会放大
    		if (n <= capacity())
    		{
    			_finish = _start + n;
    		}
    		else
    		{
    			reserve(n);
    			//剩下来填数据:
    			while (_finish < _start + n)
    			{
    				*_finish = x;
    				_finish++;
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    解决了扩容问题,我们其他的都很好解决了,这里也是一样,我们考虑两种情况即可,但是注意依旧用赋值不要用memcpy,因为涉及到浅拷贝的问题。

    8.尾插 尾删:

    尾插:

    void push_back(const T& x)  //尾插
    {
    	if (_finish == _endofstorage)
    	{
    		reserve(capacity() == 0 ? 4 : 2 * capacity());
    	}
    	*_finish = x;
    	_finish++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    尾删:

    void push_pop()
    {
    	_finish--;
    }
    
    • 1
    • 2
    • 3
    • 4

    没什么好说的,顺手就应该写出来。

    9.任意插 任意删:

    任意删:

    	iterator& erase(iterator pos)
    	{
    		assert(pos >= _start);
    		assert(pos <= _finish);
    
    		iterator cur = pos + 1;
    		while (cur < _finish)
    		{
    			*(cur - 1) = *cur;
    			cur++;
    		}
    		_finish--;
    		return pos;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    任意删的思路和我们顺序表的任意删差不多,直接覆盖即可,强调一下别忘了对我们传入的迭代器进行检验就好。
    但是任意删有一个细节就是,我们会涉及到迭代器失效的问题,即这个位置被删除后再想针对这个位置删除就是出现问题,所以我们返回pos,即删除后的下一个位置的指针,这样就可以一直删,不会删除一次就失效了。
    任意插:

    void insert(iterator pos, const T& x)//任意插
    {
    	assert(pos >= _start);
    	assert(pos <= _finish);//这里可以等于,方便尾插
    	if (_finish == _endofstorage)
    	{
    		size_t range = pos - _start;//在这里先存储一个长度变量方便后续迭代器失效时重新指定位置
    		reserve(capacity() == 0 ? 4 : 2 * capacity());
    		pos = _start + range;//由于扩容之后pos失效,那样的话pos不在新数组上,故我们要存储一个整型,方便扩容之后把pos重新带到新数组上来,别忘了任意插也存在扩容后迭代器失效的问题,我们的pos也会停留在之前的数组上被销毁之后就丢失了,要重新给到新的数组上
    	}
    	iterator end = _finish - 1;
    	while (end >= pos)//这里要等于,保证其能在pos的位置之前插而不是正好插入pos位置
    	{
    		*(end + 1) = *end;
    		end--;
    	}
    	*pos = x;
    	_finish++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在任意插这里,我们需要注意一个扩容的问题,凡是涉及到扩容和删除的问题,当我们使用迭代器去操作的时候,就要最好看一看是否涉及到迭代器失效的问题,在任意删这里就涉及到了,po针对的是被删除的数组的地址,但我们扩容后,pos的原位置直接失效了,故我们需要在扩容后调整pos到新数组的对应位置上,即:

    if (_finish == _endofstorage)
    	{
    		size_t range = pos - _start;//在这里先存储一个长度变量方便后续迭代器失效时重新指定位置
    		reserve(capacity() == 0 ? 4 : 2 * capacity());
    		pos = _start + range;//由于扩容之后pos失效,那样的话pos不在新数组上,故我们要存储一个整型,方便扩容之后把pos重新带到新数组上来,别忘了任意插也存在扩容后迭代器失效的问题,我们的pos也会停留在之前的数组上被销毁之后就丢失了,要重新给到新的数组上
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后一个常规的插入pos即可,这里最关键的便是针对迭代器失效我们应该如何处理。

    总结:

    以上便是我们vector模拟实现的全部内容,和string一样,我们模拟实现vector最关键的目的是学会一些思路,以及熟练的去使用vector,这是最关键的。
    补充一句,WBG加油!!!!

  • 相关阅读:
    JUnit介绍
    Linux下的网络编程——C/S模型 UDP(三)
    如何跨虚拟专用网络连接到你的RTSA
    Linux多线程
    用于gin框架的CORS中间件,解决身份凭证和通配符不能同时设置问题,可同时配置附带身份凭证的请求和*通配符,chrome插件CORS跨域请求通配符
    anaconda 安装 pytorch 和 tensorflow
    leetcode-每日一题623. 在二叉树中增加一行(DFS)
    【你也能从零基础学会网站开发】Web建站之javascript入门篇 JavaScript中的Screen屏幕对象与Navigator浏览器信息对象
    计算机毕业设计ssm汽车租赁管理系统n5s69系统+程序+源码+lw+远程部署
    记录在BOSS直聘上遇见一位恶心的面试官——付润豪(华如科技的C++工程师)
  • 原文地址:https://blog.csdn.net/hbw040115/article/details/134436661