【C++STL详解 —— vector的模拟实现】

vector各函数接口总览

namespace qq
{
	//模拟实现vector
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//默认成员函数
		vector();                                           //构造函数
		vector(size_t n, const T& val);                     //构造函数
		vector(int n, const T& val = T())					//构造函数
		template<class InputIterator>                      
		vector(InputIterator first, InputIterator last);    //构造函数
		vector(const vector<T>& v);                         //拷贝构造函数
		vector<T>& operator=(const vector<T>& v);           //赋值运算符重载函数
		~vector();                                          //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size()const;
		size_t capacity()const;
		void reserve(size_t n);
		void resize(size_t n, const T& val = T());
		bool empty()const;

		//修改容器内容相关函数
		void push_back(const T& x);
		void pop_back();
		void insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void swap(vector<T>& v);

		//访问容器相关函数
		T& operator[](size_t i);
		const T& operator[](size_t i)const;

	private:
		iterator _start;        //指向容器的头
		iterator _finish;       //指向有效数据的尾
		iterator _endofstorage; //指向容器的尾
	};
}

vector当中的成员变量介绍

在C++标准库中的vector实现中,通常包含三个主要的成员变量:_start_finish_endofstorage。这些成员变量共同管理着vector的存储空间和元素。为了方便理解,我们可以将vector想象成一个动态数组,其中:

  1. _start:指向数组开始的指针,也就是vector中第一个元素的位置。
  2. _finish:指向数组中最后一个有效元素之后的位置,这个位置是新元素插入的地方。
  3. _endofstorage:指向分配的内存空间结束的位置之后的那个位置。这个位置标记了vector可以在不重新分配内存的情况下扩展到的最远位置。

在这里插入图片描述

Allocated Memory:从start到end_of_storage的区域是vector当前分配的整个内存空间,它决定了vector能够容纳的最大元素数量(容量)。
Current Size:从start到finish的区域表示vector当前实际包含的元素,即vector的当前大小。
Capacity:是指在需要重新分配内存之前,容器可以容纳的元素总数。这个值至少与vector的当前大小相等,通常更大,因为vector为了减少内存重新分配的次数,会预先分配额外的空间。

默认成员函数

构造函数1:

该构造函数构造出一个空的vector

//默认成员函数
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{}

构造函数2

vector还支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。

template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
			while (*first != last)
			{
				push_back(*first);
				first++;
			}
		}

构造函数3

此外,vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。

vector(size_t n, const T& val)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

该函数需要两个重载函数,若按照以下方法来调用该函数会报如下错误:
在这里插入图片描述

在这里插入图片描述
这是因为在构造v1的时候,我们希望调用第二个函数来构造,但是第一个函数的参数更加匹配,从而错调了第一个函数,从而报错,所以我们要再提供两个重载函数。

在这里插入图片描述

vector(long n, const T& val)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n); //调用reserve函数将容器容量设置为n
			for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
			{
				push_back(val);
			}
		}
		vector(int n, const T& val)
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n); //调用reserve函数将容器容量设置为n
			for (int i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
			{
				push_back(val);
			}
		}

拷贝构造函数

在探讨拷贝构造函数之前,我们先探讨下使用memcpy拷贝问题

假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

int main()
{
bite::vector<bite::string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}

问题分析:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且
    自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

vector的构造函数涉及深拷贝问题,这里提供两种深拷贝的写法:
写法一:传统写法
拷贝构造的传统写法的思想是我们最容易想到的:先开辟一块与该容器大小相同的空间,然后将该容器当中的数据一个个拷贝过来即可,最后更新_finish和_endofstorage的值即可。

//传统写法
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
			_start = new T[v.capacity()];
			for (size_t n = 0; n < v.size(); n++)
			{
				_start[n] = v[n];
			}
			_finish = _start + v.size();
			_endofstorage = _start + v.capacity();
		}

写法二:现代写法
拷贝构造函数的现代写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。

//现代写法
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_endofstorage(nullptr)
		{
			reserve(v.capacity());
			for (auto e : v)
			{
				push_back(e);
			}
		}

注意: 在使用范围for对容器v进行遍历的过程中,变量e就是每一个数据的拷贝,然后将e尾插到构造出来的容器当中。就算容器v当中存储的数据是string类,在e拷贝时也会自动调用string的拷贝构造(深拷贝),所以也能够避免出现与使用memcpy时类似的问题。

赋值运算符重载函数

vector的赋值运算符重载当然也涉及深拷贝问题,我们这里也提供两种深拷贝的写法:
写法一:传统写法
首先判断是否是给自己赋值,若是给自己赋值则无需进行操作。若不是给自己赋值,则先开辟一块和容器v大小相同的空间,然后将容器v当中的数据一个个拷贝过来,最后更新_finish和_endofstorage的值即可。

//传统写法
		vector<T>& operator=(const vector<T>& v)
		{
			if (this != v)
			{
				delete[] _start;	//释放原来的空间
				_start = new T[v.capacity()];
				for (size_t i = 0; i < v.size(), i++)
				{
					_start[i] = v[i];
				}
				_finish = _start + v.size();
				_endofstorage = _start + capacity();
			}
			return *this;	//支持连续赋值
		}

写法二:现代写法
运算符重载的现代写法运用了传值传参,即是吧实参的拷贝传进来,那么就可以直接使用swap()来进行交换,交换后也不影响被拷贝对象的值。

//现代写法
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

析构函数
对容器进行析构时,首先判断该容器是否为空容器,若为空容器,则无需进行析构操作,若不为空,则先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针即可。

~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _endofstorage = nullptr;
			}
		}

迭代器相关函数

begin和end

我们这里的迭代器是g++的底层,即是原生指针型的迭代器,所以可以直接返回指针就行。

//迭代器相关函数
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin()const
		{
			return _start;
		}
		const_iterator end()const
		{
			return _finish;
		}

容量和大小相关函数

size和capacity

size和capacity即可由这张图求得,由于两个指针相减的结果,就是这两个指针之间对应类型的数据个数,所以size可以由_finish - _start得到,而capacity可以由_endofstorage - _start得到。在这里插入图片描述

//容量和大小的相关函数
		size_t size()const
		{
			return _finish - _start;
		}
		size_t capacity()const
		{
			return _endofstorage - _start;
		}

reserve

reserve的实现也就与我们之前实现的string一致,都是先判断是否n大于capacity,若大于,则开辟新的空间,吧之前的值插入到新开辟的空间里,最后再释放掉之前的空间即可。

在reserve函数的实现当中有个地方需要注意:
在进行操作之前需要提前记录当前容器当中有效数据的个数。
因为我们最后需要更新_finish指针的指向,而_finish指针的指向就等于_start指针加容器当中有效数据的个数,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。
在这里插入图片描述

void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				for (size_t i = 0; i < size(); i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;

				_start = tmp;
				_finish = tmp + sz;
				_endofstorage = tmp + n;
			}
		}

resize

根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。

void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。

empty

empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。

bool empty()const
		{
			return _start == _finish;
		}

修改容器内容相关函数

push_back

push_back 时先判断是否容量已满,若满了则更新空间,最后给_finish 的位置上插入x即可,_finish++。

//修改容量的相关函数
		void push_back(const T& x)
		{
			if (_finish == _endofstorage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = x;
			_finish++;
		}

pop_back

尾删数据之前也得先判断容器是否为空,若为空则做断言处理,若不为空则将_finish–-即可。

void pop_back()
		{
			assert(!empty());
			_finish--;
		}

insert

insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。

void insert(iterator pos, const T& x)
		{
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				pos = _start + len;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = x;
			++_finish;

			/*iterator it = _finish;
			while (it > pos)
			{
				*(it) = *(it - 1);
				it--;
			}
			*it = val;
			++_finish;*/
		}

这里实现了两个移动元素的方案,因为_finish先指向最后一个元素的下一个位置,所以it = _finish-1 之后,it指向了最后一个位置,然后给循环的判断条件it >= pos,即最终是吧pos位置上的元素向后移动之后再退出循环。
第二个方案也与之类似。

erase

erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。

iterator erase(iterator pos)
		{
			assert(!empty());
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
			}
			_finish--;
			return pos;
		}

swap

swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。

//交换两个容器的数据
		void swap(vector<T>& v)
		{
			//交换容器当中的各个成员变量
			::swap(_start, v._start);
			::swap(_finish, v._finish);
			::swap(_endofstorage, v._endofstorage);
		}

注意: 在此处调用库当中的swap需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。

访问容器相关函数

operator[ ]

vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。

//调用容器的相关函数
		T& operator[](size_t i)
		{
			assert(i < size());

			return _start[i];
		}
		const T& operator[](size_t i)const
		{
			assert(i < size());

			return _start[i];
		}
		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值