【C++】vector实现(深浅拷贝详细理解,迭代器失效)

本文详细介绍了C++中自定义vector的实现,包括基本框架搭建、函数实现如构造函数、resize、push_back、reserve等,以及迭代器失效问题。特别讨论了拷贝构造函数和深浅拷贝的概念,指出memcpy在处理自定义类型时可能出现的问题,并提供了修正方案。此外,还涉及了迭代器的插入、删除操作及其可能导致的问题。
摘要由CSDN通过智能技术生成

🍅可以先去这个网站看一下个个函数的功能 本文不再详细介绍,vector的底层还是顺序表,我讲的很详细,建议没学过顺序表的先预习一下(主页搜索顺序表,还有配套习题)

C++网站关于vector的接口函数信息

目录

☃️1.简单框架搭建

☃️2.稍难函数和迭代器失效问题

☃️3.深浅拷贝的深度理解

☃️4.容易看不懂的函数的类型复盘和总结


☃️1.简单框架搭建

为了和库里面的vector不冲突,我们自定义一个命名空间

通过看vector的源码,我们发现他的成员变量有

最后一个就是指容量位置的迭代器

很多接口函数可以去文章首的网站里看,本文基本全部实现最常用最经典的 有分析价值的函数 

最基本的构造函数(不止一个 后面还有更复杂的) 析构函数

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

还有很短的begin() end()

运算符重载[ ]

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

 很基础的功能 尾插

void push_back(T val)
		{
			if (capacity() == size())
			{
				//需要扩容
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = val;
			_finish++;
		}

 resize()

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

reserve()   这个函数很坑 这个代码目前是有问题的,但是在现在讲解的阶段实现成这样完全没问题

注意:扩容的时候最好 

void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldsize = size();
				T* tmp = new T[n];
				if (_start)
				{
					memcpy(tmp, _start, sizeof(int) * oldsize);
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + oldsize;
				_endstorage= _start + n;
			}
			//如果n更小不需要扩容
		}

取capacity() empty() 

size_t  capacity()
		{
			return _endstorage- _start;
		}

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

尾删 

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

☃️2.稍难函数和迭代器失效问题

现在是稍微有点挑战难度的函数

这里面的allocator是内存管理器,目前我们还不需要关心这是什么 以后会更新一篇博客专门分析这个问题 

其实我们发现 构造函数有很多形式 现在实现稍微复杂一点的

第二个形式,n个val

val的类型是value_type,在文档里也可以查

这里的成员类型都标注的很清楚

 首先要初始化肯定要开空间,直接reserve(n),然后每一个都赋值成val

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

		}

看起来很美妙是不是,但是这个以后都是坑,因为之前说的reserve有问题


插入insert()

在pos迭代器位置插入val 首先判断pos是否合法

如果容量不够(_finish==_endstorage)就扩容,这里涉及到迭代器失效的问题

 请问这个时候你还敢用扩容之后的pos?

因为一般扩容都是异地,但是这个pos的位置就应该改变但是没变,很尴尬,这个问题不一定出现(如果原地扩容就不会有问题)但是谁能一定保证呢,最好还是更新一下pos的位置

容量没问题之后就开始挪动数据 把前面的顺延到后面,最后把空出来的pos位置填上,把_finish 的位置更新

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

			if (_finish == _endstorage)
			{
				size_t len = pos - _start;
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);

				// 扩容会导致pos迭代器失效,需要更新处理一下
				pos = _start + len;
			}

			// 挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = val;
			++_finish;

			return pos;
		}

删除pos位置,还是先判断位置合法,然后向前覆盖,更新_finish 

为什么文档里的设计要有返回值?

其实可以试一下 实现:删除某个位置的元素,然后再去修改迭代器指针

此时程序会崩溃

为什么?

其实和刚才的pos问题一样,erase的时候pos还是不安全,此时的it是野指针,不能再对it++之类的操作

或者实现一下删除所有偶数

 其实一开始这个vector里面是1234应该是段错误,第二次是122345 但是结果却是1235

显然我们这样没有返回值的erase是不可以的

首先解释第一个段错误(一般Linux这样报错就是越界或者野指针问题)

当走完4 删除之后 _finish ++  但是啊it也++ ,一直向后走,永远没有和end()相等的时候,就会一直走下去 

 

这样就会少删除数据 

 但是怎么解决呢,千万不能根据某个具体情况去更改,不得不说还是大佬厉害,用一个返回值,返回删除元素的下一个位置的迭代器

iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);


			iterator begin = pos + 1;
			while (begin < _finish)
			{
				*(begin - 1) = *(begin);
				++begin;
			}

			--_finish;

			return pos;
		}

clear() swap() 函数就是小case啦

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

拷贝构造的几种写法

其实很秀的这里,我们最传统的写法就是自己开空间 然后拷贝

一个一个拷贝!不要memcpy(剧透:reserve的坑就是memcpy,思考一下为什么C++不继续沿用melloc?free?就是告诉你别再C了,用C++解决问题)

拷贝构造的几种写法
		v1(v2)
		vector(vector<T>& v)  //传统写法
		{
			_start = new T[v.capacity()];  //按照v的大小开空间
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];  //一个一个拷贝
			}
			_finish = _start+v.size();
			_endstorage =_start+v.capacity();
		}

还有一个稍微创新的传统写法,很狡猾嘛,但是可惜了reserve有问题

另一种传统方法
		vector(vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endstorage(nullptr)
		{
			reserve(v.capacity());
			for (auto& e : v)  //一定要加上&,不知道v里面元素类型,如果是自定义类型需要深拷贝
			{
				push_back(e);
			}
		}

来看看创新的写法

刚才看构造函数不还有用迭代器构造的嘛(第三种),我们就实现一下

 然后我直接实例化一个tmp对象,再tmp和this交换一下

//很新的方法
		template <typename Inputiterator>
		vector(Inputiterator first, Inputiterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _endstorage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		vector(vector<T>& v)
			:_start(nullptr)
			, _finish(nullptr)
			, _endstorage(nullptr)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp); //this 和 tmp 交换
		}

那么=运算符就很好写了,直接把v赋给this,但是很少数的情况会v=v,所以是否判断都可以

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

☃️3.深浅拷贝的深度理解

我们都知道浅拷贝就是以bit为单位,一个一个拷贝,但是一些指针问题会导致对同一块空间的两次释放,对于自定义类型 我们来看一下会出现什么神奇的事情

测试这段代码 

void test()
{	
        vector<vector<int>> vv;
		vector<int > v(5, 1); 
        vv.push_back(v);
		vv.push_back(v);
		vv.push_back(v);
		vv.push_back(v);
		vv.push_back(v);
	for (size_t  i = 0; i < vv.size(); i++)
		{
			for (size_t j = 0; j < vv[i].size(); j++)
			{
				cout << vv[i][j] << " ";
			}
			cout << endl;
	    }
}

 结果是

 分析,最开始是这样的

 然后开始push_back ,最开始vv的容量是0,所以需要扩容,但是扩一次是4个还不行,还得扩二倍就是八个, 扩容 调用reserve

 tmp前五个_start还是指向5个v,然后memcpy拷贝,然后delet[ ] _start 

 是不是问题就来了,我vector<int>都mb了啊,我的所有元素都变成野指针了.....

谁的锅!reserve()里面的memcpy,因为他是浅拷贝,如果拷贝的时候可以开一份空间就好了

那么就用赋值好了,_tmp[i]=_start[i] 不管是传统写法还是现代写法都会开空间的(v1=v2,是会开空间再拷贝的)

所以代码应该写成这样

	void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldsize = size();
				T* tmp = new T[n];
				if (_start)
				{
					//memcpy(tmp, _start, sizeof(int) * oldsize); //这个地方有问题,自定义类型的浅拷贝
					for (size_t  i = 0; i < oldsize; i++)
					{
						tmp[i]=_start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + oldsize;
				_endstorage = _start + n;
			}
			//如果n更小不需要扩容
		}

☃️4.容易看不懂的函数的类型复盘和总结

我们这里定义了模板T,这个T在vector <int>  的时候就是int  在vector<vector<int>> 的时候就是vector<int>

迭代器的类型就是和模板类型有关的

 

 

 [ ]的赋值重载就应该是T类型的,至于&就是赋值拷贝的基本操作了,我们之前说过可以不加&的,但是一般来讲我们都更喜欢用筷子吃饭(虽然直接用手也可以)

 他还有一个const成员函数的函数重载,当然返回值也要是const啦

 这个拷贝构造的意思就是用n个val初始化val的值 类型一定是模板类型,因为这个容器里元素类型就是模板T,加上const的意思就是val的值是不能在vector这个构造内部改变的,当然如果你不写那么我们默认是T(),这是一个匿名对象,对于int来说就是0,还有这里有一个引用,意思就是万一你传过来的是指针之类的,我也可以对你直接操作 无需考虑几级指针,形参的改变影不影响实参之类的

 push_back里面这个x同样的,形参的改变会影响实参

 这个函数的参数是一个对象v,他的类型和this的类型一样都是,&也是为了形参的改变影响实参

 其实也就是这个vector 的实现 有上面几个类型不好理解,其他的都很简单,C++的细节就是很多,希望大家不厌其烦,我们一起学好!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值