C++之vector讲解

Vector


前言

本篇讲解stl库中的vector的模拟实现


一、了解vector的构造

我们有了之前学习过string的经验,学习vector自然是手到擒来,但是vector的内部构造和string不太一样,我们可以通过源码了解一下,如:

我们可以看到,在vector中,有三个成员变量,分别为start、finish、end_of_storage,且她们的类型均为iterator,这与string的char数组、size和capacity是不一样的,vector当然也可以使用string的构造来实现,但是源码采用了这种方式,自然也有自己的道理,那么这个iterator,我们之前说过它是迭代器,在vector中是怎么使用的呢?我们来细细分晓。

我们通过源码来看,可以发现iterator本质上就是模板类型的指针,这与string中迭代器的使用类似

二、进行模拟实现

对vector进行模拟实现,我们需要先猜测并确定vector各成员变量的功能,我们可以在C++官网上找到vector的各种成员函数和非成员函数,通过每个成员变量在不同函数中的作用来分析

成员变量及部分函数

cpp 复制代码
namespace xx
{
	template <class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{}
		~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;
		}
		size_t size() const
		{
			return _finish - _start;
		}
		size_t capacity() const
		{
			return _endofstorage - _start;
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

reserve()

cpp 复制代码
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t _size = size();
				if (_start)
				{
					for (int i = 0; i < size(); i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + _size;
				_endofstorage = _start + n;
			}
		}

依据reserve函数为大家讲解几个自行模拟实现时的易错点

第一,为什么要使用_size来单独存储一次变量

在代码中,我们可以看到在_finish = _start + _szie时使用了一次_size,这是因为,我们的size()函数的运算逻辑就是_finish - _start,如:

cpp 复制代码
size_t size() const
		{
			return _finish - _start;
		}

当我们的_finish和_start初始都为nullptr时,经过扩容赋值之后,_start确实是指向了新的空间,但是_finish呢,他并没有改变,依旧是nullptr,因此我们直接写:

cpp 复制代码
_start = tmp;
_finish = _start + size();
_endofstorage = _start + n;

那么就变成了_finish = _start + nullptr - _start;此时_finish依然是nullptr,这是不符合预期的,而使用一个单独的变量用于存储两者最初的距离便能解决这个问题,正如我们代码中所写

第二,为什么要使用for循环来进行赋值,为什么不使用strcpy或者memcpy?

这个问题非常好,如果我们使用memcpy进行拷贝的话,此处对vector对象的内容的拷贝就是浅拷贝,可能有人会问了,浅拷贝怎么了,int这种内置类型浅拷贝并没有问题的啊,话是如此说,但是不要忘了,我们现在使用的是vector,而我们模拟实现vector自然是兼容性越大越好,不只是针对int这一种类型,也就是说,我们的vector模板可能是vector<int>、vector<string>、vector<vector<int>>等等都有可能,那么memcpy浅拷贝内置类型确实没问题,但是自定义类型呢?比如vector<string>?这自然是不行的,我们到后面会进行演示

resize()

resize与reserve唯一不同的地方在于是否赋值和缩容,因此我们可以直接复用

cpp 复制代码
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;
		}
	}
}

构造函数

我们可以先看构造函数
我们可以看到有三种构造函数,其中的allocator是空间配置器方面的知识,我们以后会讲,我们下面会讲解实现后面三项

1.vector(size_t n, const T& val)

使用n个相同类型的值进行构造初始化

cpp 复制代码
		vector(size_t n, const T& val)
		{
			resize(n, val);
		}

2.template vector(Inputiterator first, Inputiterator last)

使用迭代器来进行构造初始化

cpp 复制代码
template<class Inputiterator>
vector(Inputiterator first, Inputiterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

3.默认无参构造函数 vector()

cpp 复制代码
vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{}

这里我们还要讲解一处易错点,当我们同时定义了第一种构造函数和第二种构造函数时,此时如果我们想初始化为n个相同类型的值,编译器会报错,如:

原因是什么呢?

可以看到,当我们想用10个1去进行初始化时,此时我们应该想调用第一种构造函数,因为我们的值1,类型为int,个数10也是int

而我们的两种构造函数参数里都有模板类型,第一种构造函数的第二个参数类型为模板类型,第二种构造函数的两个参数均为模板类型,也就是说,两个函数都可以识别int类型的参数

而在编译器中,识别参数类型后,会优先调用最适配的类型,

我们传参的类型为int int

第一种构造函数的参数类型为sizet_t 模板识别的int类型

第二种构造函数的参数类型为模板识别出的int类型 模板识别出的intl类型

那么适配度最高的的自然是第二种构造函数,此时我们就会调用第二种构造函数,inputiterator模板识别为int类型

那么为啥会出错呢,我们看进入到函数内部之后,我们要进行遍历了,因为第二种构造函数是给迭代器准备的,而*first,当first的类型被自动识别为int之后,自然是不被允许的,int类型不允许被解引用,此时就会报错

解决办法就是再添加一个参数类型均为int类型的函数

cpp 复制代码
		vector(int n, const T& val)
		{
			resize(n, val);
		}

4.拷贝构造函数 vector(const vector& v)

cpp 复制代码
	vector(const vector<T>& v)
	{
		//reserve(v.capacity());
		//memcpy(_start, v._start, sizeof(T)*v.size());
		_start = new T[v.capacity()];
		for (size_t i = 0; i < v.size(); i++)
		{
			_start[i] = v._start[i];
		}
		_finish = _start + v.size();
		_endofstorage = _start + v.capacity();
	}

我们可以看到,此处我们依旧摒弃了memcpy的用法,这和之前的原因一样,memcpy是浅拷贝

如果我们依旧使用memcpy进行拷贝,那么当我们调用拷贝构造时,会使得原对象与当前对象指向同一块空间,那么就会遭遇二次析构的问题,会报错,譬如我们使用vector<string>时,

此时浅拷贝会两个vector的string对象都指向同一块空间,当释放空间时会重复释放

vector 当T为string这种自定义类型时,会依次调用数组中每个对象的析构函数,最后再将整个数组进行析构,释放整个空间,因为不允许使用memcpy,正确情况应如下:

插入函数与删除函数

cpp 复制代码
		void insert(iterator pos,const T& a)
		{
			assert(pos >= _start && pos <=  _finish);
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newcapacity);
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			*pos = a;
			_finish++;
		}

此处我们需要补充一个变量len来保存pos位置和_start之间的距离,因为我们如果内存不够去进行扩容的话,扩容之后,_start和_finish的位置自然都会发生变化,一般都是异地扩容,那么此时pos的位置就会失效,我们需要根据原本的距离去更新扩容后pos的位置

并且此处使用迭代器代替下标,是有好处的,我们不需要考虑在挪动数据的情况下size_t类型始终不小于0的特殊情况

cpp 复制代码
void erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);
	iterator begin = pos + 1;
	while (begin != _finish)
	{
		*(begin - 1) = *begin;
		++begin;
	}
	_finish--;
}

注:头插头删,尾插尾删都可以直接复用插入删除函数

赋值重载函数

cpp 复制代码
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

此处补充一个reserve函数使用memcpy进行浅拷贝时会发生的情况

此时已经报错,分析程序:

当我们运行到delete[] _start但还未运行时,

可以看到我们的tmp和_start经过memcpy拷贝后,因为数据过长,并没有同时存于buffer数组中,而是存在了ptr空间中,且此时两者地址是相同的,当我们运行该语句后,会变成下图:


这已经说明,我们的memcpy是不可用的,因为memcpy是浅拷贝

同时,当我们的数据量过小比如只有几个字母的时候,我们不一定会容易观察到,因为vs配置情况下,为我们提供了一个buffer数组,当数据量比较小时一般都会存于buffer数组中,这会导致我们观察到的地址并不相同,但是报错原因还是相同的

同样,我们之前讲解过一个pos位置失效问题,当我们在外部进行调用时,依旧会产生这种情况

此时会非常危险,因为这不符合我们的逻辑预期,但是并没有报错,因此不要这么使用

此外,当我们如此调用时:

因为我们代码中默认4是一个节点,当超过4时就会触发一次扩容后,扩容后_start和_finish自然会改变位置,而函数传pos,我们是传值传参,也就是说函数中的pos确实改变了,但是外面的pos没有改变,此时pos位置可能就会失效,会引发错误,我们看到代码第137行报错,查找后发现是assert被触发,根据条件判断,也就是此时pos已经不在范围内了,也就是所谓的迭代器失效

针对这种外部迭代器失效的问题,我们的解决方法就是返回一个pos值,如:

stl库中也是如此解决的,但是当我们insert(p+3)的情况时,需要更加一步考虑,我们后续讲解

相关推荐
计算机安禾6 小时前
【c++面向对象编程】第41篇:函数模板与类模板:泛型编程的基石
开发语言·c++·算法
郝学胜-神的一滴7 小时前
Qt 高级开发 010: 从跨界面传值到自定义信号
开发语言·c++·qt·程序人生·用户界面
天若有情6737 小时前
自研极简C++软交互事件系统:干掉观察者模式、碾压前端事件机制
c++·观察者模式·交互·事件
basketball6167 小时前
C++ 继承完全指南:从 is-a 关系到虚继承的底层真相
开发语言·c++
IOT-Power7 小时前
C++ 工厂模式
c++
Huangjin007_7 小时前
【C++ STL篇(十)】深入理解 AVL 树:代码实现、旋转图解与平衡因子详解
开发语言·c++
小明同学017 小时前
C++后端项目:统一大模型接入 SDK(四)
服务器·开发语言·c++·计算机网络·chatgpt
不吃土豆的马铃薯8 小时前
Spdlog 入门:日志记录器与日志槽基础详解
服务器·开发语言·c++·c·日志·spdlog
此生决int8 小时前
算法从入门到精通——前缀和
c++·算法·蓝桥杯