vector的模拟实现

本文建立在已经学习过string类模拟实现的读者阅读,
vector和string一样,底层是连续存储的顺序表结构,因此一些成员函数的实现与string大差不差,这里不做过多文笔,主要详细探讨vector的迭代器失效以及深浅拷贝细节详细代码请参考博主gitee仓库—

https://gitee.com/chxchenhaixiao/test_c/blob/master/Vector/Vector/Vector.h#

首先声明vector中有三个私有属性,分别是
_start,
_finish,
_end_of_storage,

分别指向空间起始位置,有效数据下一位置,空间结尾位置

迭代器失效:

void insert(iterator pos,const T& val){

	if(size()==capacity()){
		reserve(2*capacity());
	}

 	iterator end = _finish - 1;
 	while(end != pos){
 		*(end+1)=*end;
 		end--;
 	}

	*pos=val;
	_finish++;
}

对于成员函数insert的模拟实现,一般情况我们会直接想到上述版本,可以很轻易的写出来,但是这样写是存在隐患的。

假设对象初始capacity为4,在插入第五个数据之前,程序不会有错误,但插入第五个数据时,程序便会崩溃,原因就在于reserve语句的调用使得pos迭代器失效了,pos指向了一个无效位置

图解:
vector的模拟实现_第1张图片
这是迭代器失效最常见的一种,扩容产生的野指针

解决办法很简单:及时更新pos:

size_t len=pos - _start;
//扩容……
pos = _start + len;

一些编译器如vs2019对标准库中的insert和erase函数做了严格检查,一般在使用完后pos指向的位置都不被允许访问,pos也不允许被修改,避免出现一些未定义的结果,但是g++环境下是可以的,但是我们不推荐。

举个例子,按理来说erase删除元素时是不会出现异地拷贝的现象的,pos指针指向的依然是一个有效位置,那么是否可以对其进行++,–,访问等操作呢,答案是否

举个简单的例子,
给定一个vector < int > 类型的对象,要求删除对象中所有的偶数

void erase(iterator pos) {
		assert(pos < _finish&& pos >= _start);
		iterator start = _start+(pos-_start)+1;
		while (start != _finish) {
			*(start - 1) = *start;
			++start;
		}
		--_finish; 
}

case 1: [1,2,3,4,5]
case 2: [1,2,3,4]
case 3: [2,2,3,5]

第一种情形结果正确没有问题,
第二种情形运行崩溃,
第三种情形结果与预期不符。

vector的模拟实现_第2张图片
这也是一种迭代器失效问题,指向错误位置。

为了解决这一问题,标准库中设置了返回值,即调用完erase后返回被删除元素的下一位置,必要情况下可更新迭代器。(insert也有返回值,返回值类型也为迭代器)

	iterator erase(iterator pos) {
			assert(pos < _finish&& pos >= _start);
			iterator start = _start+(pos-_start)+1;
			while (start != _finish) {
				*(start - 1) = *start;
				++start;
			}
			--_finish;

			return pos;
	}

结论:在调用完insert和erase函数后,如果pos迭代器未得到更新,我们都认为pos已经失效,不能再使用。

深浅拷贝:

模拟实现vector的拷贝构造函数时我们都只要自定义类型需要进行深拷贝来防止二次析构,
我们很容易写出一个简单的拷贝构造

vector(const vector<T>& v){
	_start = new T[v.capacity()];
	memcpy(_start,v._start,sizeof(T)*v.size());
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

貌似这样看上去很对,但是这仅仅针对了T为内置类型,记住memcpy是按字节拷贝,本质是浅拷贝
如果我们用
—— vector v2(v1);
来测试它,编译器会告诉我们发生了二次析构,原因是什么呢,原因是因为,当我们对v2进行拷贝构造时的确v2中的私有属性是不同于v1的,但是v2中存储的每一个元素类型是一个string类型,而元素是通过memcpy逐个浅拷贝而来的,string中的_str指针没有得到深拷贝,指向与v1中string对象所指向的地址,在程序结束时就会发生二次析构导致程序崩溃。
图解:

vector的模拟实现_第3张图片

为了解决此类问题,我们不得不对vector中的每一个元素都进行深拷贝,而不能使用memcpy

解决方法:
将memcpy语句替换成
for (size_t i=0 ; i _start[i] = v._start[i];
}

自定义类型的赋值操作都是深拷贝

还有一个bug,
参考我gitee上的写法,reserve函数的模拟实现并不像string一样,vector中的reserve异地扩容后写需要进行深拷贝
vector的模拟实现_第4张图片
否则当我们不断插入自定义类型元素时,总会导致异地扩容,如果reserve是浅拷贝,新空间中元素仍然会指向一个已经被释放的位置,最终导致二次析构程序崩溃。

最后再提醒一下:赋值深拷贝的前提是在自定义类型已经进行赋值运算符重载的情况下有效的————加入模拟实现中的vector没有进行手动赋值运算符重载,使用默认生成的,那么在进行vector的时候仍然会崩溃

你可能感兴趣的:(c++,c语言,经验分享,笔记,数据结构)