初识C++ · 模拟实现vector

目录

前言:

1 部分简单函数的实现

2 push_back和pop_back

3 reserve和resize

4 Print_vector

5 insert和erase

6 拷贝构造

7 构造

8 赋值

9 memcpy的问题

10 迭代器失效


前言:

继上文模拟实现了string之后,接着就模拟实现vector,因为有了使用string的基础之后,vector的使用就易如反掌了,所以这里直接就模拟实现了,那么实现之前,我们先看一下源代码和文档:

源码不多看,因为很容易绕晕进去,我们从这两个部分得到的信息就够了,首先,vector是一个类模板,即我们可以往里存放许多不同的数据,比如实现一个二维数组,我们可以用vector里面套一个vector,就实现了一个二维数组,为什么说二维数组是一维数组的集合,因为vector的每个空间的start指针指向的空间又是一块连续的空间,即这就是二维数组的构造。

在public的下面有一堆的typedef,这也是不要多看源码的一个原因,很容易绕进去的,比如vector的成员变量的类型是iterator,我们看到value_type*是重命名为iterator了,那么value_type又是T的重命名,即start的类型就是T*,现在我们搞懂了一个问题即模拟实现vector的时候成员变量的类型是模板指针。

那么现在就需要搞懂三个成员变量是什么?

这里就明说了,start是空间的起始位置,finish是空间的最后一个数据的下一个位置,end_of_storage是空间的最后一个位置,所以我们实现size(),capacity()的时候就两两相减就可以了。

基本类型我们了解了,现在就开始模拟实现吧。


1 部分简单函数的实现

这里实现以下函数,size(),capacity(),begin(),end()以及const版本,operator[]以及const版本,empty(),析构函数,注意的就是返回值返回类型即可:

iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin()const
{
	return _start;
}
const_iterator end()const
{
	return _finish;
}
size_t size()
{
	return _finish - _start;
}
size_t capacity()
{
	return _end_of_storage - _start;
}
T& operator[](size_t pos)
{
	assert(pos <= _finish);
	return *(_start + pos);
}
const T& operator[](size_t pos)const
{
	assert(pos <= _finish);
	return *(_start + pos);
}
bool empty()
{
	return _start == _finish;
}
~vector()
{
	delete[] _start;
	_start = _finish = _end_of_storage = nullptr;
}

因为是[]重载,返回的是可以修改的类型所以用引用,这里模拟实现的时候一般都返回size_t类型,这里文档里面的原型,最好保持一致。


2 push_back和pop_back

尾插的时候要注意空间的扩容,扩容的判断条件即是_finish = _end_of_storage的时候,扩容方式和前面实现顺序表链表的时候没有什么区别,使用2倍扩容,但是实际上vs下的vector扩容的时候是使用的1.5倍扩容,我们模拟实现的是linux下的vector,在string模拟实现的时候,我们拷贝数据使用的是strcpy,在这里因为是模板,所以不能使用字符串函数,使用内存拷贝,即使用memcpy,当然memset也可以,但是memcpy就可以了,因为不存在空间重叠的问题。

这里有个隐藏的坑,到后面插入string类的时候才会显式出来,这里先不说,目前插入内置类型是没有问题的:

	void push_back(const T& val)
	{
		//判断扩容
		if (_finish == _end_of_storage)
		{
			size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
			T* tmp = new T[newcapacity];
			memcpy(tmp, _start, sizeof(T) * size());
			delete[] _start;
			_start = tmp;
            _finish = _start + size();
			_end_of_storage = tmp + newcapacity;
		}
		*_finish = val;
		_finish++;
	}

好了,实现好了吗?当然没有,这里也有个坑,即size()的问题,因为实现size()的时候,实现方式是_finish - _start,但是扩容之后,_start的位置已经改变,所以此时size()的实现变成了原来的_finish - 新的_start了,所以会报错,解决方式就是把size()先存起来再使用:

	void push_back(const T& val)
	{
		//判断扩容
		if (_finish == _end_of_storage)
		{
			size_t old_size = size();
			size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
			T* tmp = new T[newcapacity];
			memcpy(tmp, _start, sizeof(T) * size());
			delete[] _start;
			_start = tmp;
			_finish = tmp + old_size;//容易出错
			_end_of_storage = tmp + newcapacity;
		}
		*_finish = val;
		_finish++;
	}

这里如果忘了memcpy的使用可以看看文档。

那么push_back就还有一个坑没有解决了,欲知后事如何,请看下去咯。

pop_back的实现相对来说就简单多了,尾删不代表真正的删除数据,指针指向的位置改变就是尾删了,当然,删除操作想要执行就vector就不能为空:

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

3 reserve和resize

reserve的实现我们刚才其实已经实现过了,reserve即是扩容,push_back的时候我们已经考虑了扩容,所以代码差不了多少:

void reserve(size_t n)
{
	if (capacity() < n)
	{
		size_t old_size = size();
		T* tmp = new T[n];
		memcpy(tmp, _start, sizeof(T) * size());
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}

当然,这里的坑和push_back是一样的,只有在插入自定义类型的时候才会出现,先不急,那么这里的扩容就是把newcapacity换成了n而已,其余的是没有差别的。

resize模拟实现的时候就要注意了,如果resize实现的是缩容,就直接移动_finish指针就好了,如果实现的是扩容,那么默认参数我们怎么给呢?

文档里面记录的缺省值是value_type(),而value_type是typedef之后的T,所以我们可以看成T val = T(),看着是有点奇怪?其实一点都不奇怪,比如:

int main()
{
	//Test7_vector();
	int a = int();
	cout << a;
	char ch = char();
	cout << ch;
	return 0;
}

这种代码运行是完全没有问题的,这里的a是0,这里的ch是'\0',这是一种初始化方式,所以我们传参的时候就不能写成0,因为不是所有的vector都是vevtor<int>类型的,所以这里我们采用文档的方式,使用这种缺省值,那么扩容的时候多余的空间就给上这个缺省值就好了:

void resize(size_t n, const T& val = T())
{
	if (size() < n)
	{
		//扩容
		reserve(n);
		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}
	}
	else
	{
		//删减
		_finish = _start + n;
	}
}

实际上我们也可以一直尾插T(),但是我们既然知道了我们要多少空间,我们不如直接把空间开完,然后进行赋值就好了,赋值的循环条件是_finish指针到最后扩容的指针的位置,也是比较好理解的。


4 Print_vector

当插入和扩容操作都完成了,下一步就是打印了,当然会有人说初始化怎么办,不急,我们先构造一个空的vector就可以了:

vector()
{}

iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;

先使用上缺省值,方便测试即可。

Print_vector是用来打印vector的,这里其实变相的考了遍历方式,遍历就是三件套:

void Print_vector(const T& t)
{
	for (auto e : t)
	{
		cout << e << " ";
	}
	cout << endl;

	//typename 告诉编译器这是个类型
	typename vector<T>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}

	for (int i = 0;i < size();i++)
	{
		cout << v[i] << " ";
	}
}

这是三种遍历方式,下标访问和迭代器使用都是没有问题的,唯独需要注意的是typename那里,如果那里我们使用auto it = v.begin()是没有问题的,但是如果我们使用vector<T>::iterator it = v.begin()就会有问题了,这里是因为编译器不知道这里的vector<T>::iterator是类型还是变量了,编译器的原则是不会去类模板里面找东西,而这里使用了模板,那么从编译器的角度出发,它不知道这个究竟是个类型还是变量,所以使用typename。


5 insert和erase

push_back和pop_back是尾插尾删,想要任意位置插入就需要用到insert和erase,文档里面erase有删除一段区间的,这里我们就实现任意位置删除一个数据即可,因为删除一段区间的原理是一样的,因为是任意位置删除和插入,所以实现了之后在push_back和pop_back上也可以复用

insert实现的原理很简单,挪动数据添加数据即可,当然要注意是否要扩容:

void insert(iterator pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);

	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : 2 * capacity());
		pos = len + _start;//易错点
	}
	iterator it1 = _finish - 1;
	while (it1 >= pos)
	{
		*(it1 + 1) = *it1;
		it1--;
	}
	*pos = val;
	_finish++;
}

当然,为了位置的合法性,这里使用assert断言一下,然后就是判断扩容,但是这里和push_back的size是一个道理,一旦发生扩容,那么pos位置就会变化,while循环的判断就会出问题,所以我们扩容后要更新一下pos的位置,这样才能保证循环的进行。

这里相对string还有一个很好的地方就是我们不用担心end >= pos的那种隐式类型转换比较的发生。

erase的实现原理是一样的,把删除位置的数据往后移动即可。

iterator erase(iterator pos)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	iterator it1 = pos + 1;
	while (it1 < _finish)
	{
		*(it1 - 1) = *it1;
		it1++;
	}
	_finish--;
	return pos;
}

那么这里的复用就是:

void push_back(const T& val)
{
	insert(end(), val);
}
void pop_back()
{
	assert(!empty());
	_finish--;
}

6 拷贝构造

这里的拷贝构造没有string拷贝那么复杂了,不用想深拷贝的问题,开空间一直插入就可以了:

vector(const vector<T>& v)
{
	reserve(capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

拷贝构造的最基本要求不能忘记,参数必须是const 的引用类型,不然会发生无限递归的问题。


7 构造

构造是最终boss。

第一个构造,即构造一个空的顺序表,我们可以专门写个函数:

vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{}

当然,没有必要,我们给上三个缺省值就可以了,这是一个构造重载。

第二个重载是给n个element,也很好操作,一直尾插就好了:

vector(size_t n, const vector<T>& val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
	{
		push_back(val);
	}
}

第二个也没多大问题,再来看第三个区间构造,实际上第三个重载的最后一个参数可以完全不用管的,没有多大意义,区间构造会再用到一个模板,即类模板的成员函数也可以是个模板:

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

以为结束了?还没有,看这段代码:

vector<int> v3 = { 1,2,3,4,5,6 };

这个构造是不是觉得有点奇怪,好像对不上上面三个构造中的任何一个,因为花括号里面的类型是initializer_list

这是一个类,它的成员变量也就几个迭代器和size,但是这是c++11引进的,即c++11准许如下的初始化方式:

void Test_vector()
{
	//有关initializer_list
	int a = 1;
	int b = { 1 };
	int c{ 1 };
	//上面写法均正确 但是不推荐后面两种
	vector<int> v1 = { 1,2,3,4,5,6 };//隐式类型转化
	vector<int> v2({ 1,2,3,4,5,6,7,8 });//直接构造
}

但是不推荐这种方式,代码可读性没那么高,但是我们看到这种初始化方式应该知道这是使用的initializer_list类型来初始化的,这里的初始化也是涉及到了隐式类型转换,先构造一个临时对象,再拷贝构造这个临时对象,那么编译器会优化为直接构造。

所以构造就是这样构造的:

vector(initializer_list<T> il)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

但是,这里会出问题了:

vector(initializer_list<T> il)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

vector(size_t n, const vector<T>& val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
	{
		push_back(val);
	}
}

void Test()
{
    vector<int> v(10,1);
    vector<int> v(10u,1);
    vector<int> v(10,'a');
}

下面的两个构造就不会出问题,第二个的u是无符号的标志,即size_t,第一个会出问题,因为10和1默认的类型是int,那你说,调用上面的没问题吧?调用下面的,虽然第一个的参数类型是size_t,但是将就将就吧也可以,那你说,编译器调用哪个?

所以这里会报错,那么源代码里面解决的就很简单粗暴:

vector(int n, const vector<T>& val = T())
{
	reserve(n);
	for (int i = 0;i < n;i++)
	{
		push_back(val);
	}
}

类型转换一下~ 


8 赋值

这里的赋值我们可以直接采用现代写法,不用传统的先开空间,再进行数据拷贝,我们利用swap函数,可以达到很好的效果:

	void swap(vector<T>& v)
	{
		std::swap(_start,v._start);
		std::swap(_finish,v._finish);
		std::swap(_end_of_storage,v._end_of_storage);
	}

这里需要注意到的是swap的调用需要用到域名访问限定符,不然编译器会调用离函数最近的函数,即它本身,而我们使用的是std库里面的交换函数,所以需要使用::。

有了交换,我们构造一个临时对象,再进行交换即可:

vector<int>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

我们提及到过连续赋值的条件是返回值应该是赋值过后的值,所以我们需要返回赋值之后的对象。


9 memcpy的问题

先看这样一段代码:

void Test5_vector()
{
	vector<string> v;
	v.push_back("11111");	
	v.push_back("22222");
	v.push_back("33333");
	v.push_back("44444");
	v.push_back("55555");
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

如果我们只存入4个或者及以下的数据是没有问题的,但是存入5个数据之后,就会存在访问出错的问题,这一切都是因为memcpy,也就是在扩容,尾插的时候会涉及到的问题。

我们了解memcpy的底层之后,就知道memcpy拷贝的方式是逐字节的拷贝,所以当_start指向的空间是自定义类型的时候,经过扩容之后,就会导致_start指向的空间被销毁,但是拷贝过来的指针依旧是指向那块被销毁的空间的,我们再次访问自然就会报错。

所以这里我们最好的方法就是进行赋值:

void reserve(size_t n)
{
	if (capacity() < n)
	{
		size_t old_size = size();
		T* tmp = new T[n];
		//memcpy(tmp, _start, sizeof(T) * size());
		for (int i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}

本质就是拷贝构造一个临时对象,就不会导致越界访问的问题。


10 迭代器失效

void Test6_vector()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(6);
	v1.push_back(7);
	v1.push_back(8);
	vector<int>::iterator it = v1.begin() + 3;
	v1.insert(it, 40);
	Print_vector(v1);
	//it = v1.begin() + 3;
	cout << *it << endl;
}

当我们多次插入之后,伴随着扩容的发生,所以it的位置会发生改变,it指向的是原来的空间位置,所以此时我们打印it的值的时候就会打印出来错误的值,所以如果发生了空间的改变导致迭代器失效,如果我们还要使用迭代器,我们就需要重置迭代器-》即那段注释。

void Test7_vector()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(4);
	vector<int>::iterator it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
		    v1.erase(it);
            it++;
		}
	}
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

且看这段删除偶数的代码,好像没有问题?有以下几种情况就会出错了,11252,1223。

因为删除之后会有移动数据的发生,比如1223,删除了第一个2,第二个2往前面移动,然后再pos位置的指针往后移动,就相当于it指针移动了两次,所以会错过,那么如果最后一个是偶数,即11252,就会完美错过end,那么循环条件就无法结束,就会死循环了。

解决方法也很简单,让it删除之后原地踏步就好了:

while (it != v1.end())
{
	if (*it % 2 == 0)
	{
		it = v1.erase(it);//解决方法
	}
	else
	{
		++it;
	}
}

这也是为什么erase的返回值是iterator的原因,整个代码都是前后呼应的。


感谢阅读!

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值