【C++】Vector模拟实现

目录

前言

vector基础定义

Vector构造函数

普通构造函数

n个val初始化构造函数

迭代器构造

拷贝构造函数

Vector析构函数

Vector成员函数

迭代器实现

size,capacity函数

reserve函数

resize函数

push_back函数

insert,erase函数

运算符重载

总结


前言

在上一篇文章Vector详解中,我们对vector有了一个较为完整的认识,而要更深入的了解vector,知道他的一些特性和细节,就需要通过模拟实现,让我们的认知体系更加完整。

vector基础定义

首先,我们要知道,vector一个元素中拥有哪些数据,显然,在经过vector的学习后,我们大致能猜出,vector拥有_start(指向元素开始位置),_end(指向元素结束位置),_endofstroage(capacity,表长度),同时,vector因为要满足不同类型可以使用,所以我们要用类模板来进行定义

template<class T>
class vector
{
public:
        vector()
			:_start(nullptr)
			,_end(nullptr)
			,_endofstorage(nullptr)
		{}
 
private:
		iterator _start;
		iterator _end;
		iterator _endofstorage;
}

基础定义好后,我们可以开始定义构造函数

Vector构造函数

普通构造函数

在上面的代码中,我们其实已经成功定义了一个普通构造函数,但是,不难看出,vector的构造函数不止一个,如果每个构造函数都要像这样写初始化列表,会让代码很冗余,所以,我们可以直接在私有成员给缺省值,就不用初始化列表了,那不用了上面的构造函数啥也没写要不要删?不能删,构造函数写了系统不会生成,不写才会生成默认的,下面要写的拷贝构造都算构造,所以这个不能删,他也是干了事情的

template<class T>
class vector
{
public:
        vector()
        {}
    
private:
		iterator _start=nullptr;
		iterator _end=nullptr;
		iterator _endofstorage=nullptr;
}

n个val初始化构造函数

这个构造函数是通过直接扩容尾插来进行的,比如在末尾插入10个0,要注意的是,参数T& val = T(),这里缺省值要给匿名变量,因为如果给0,就会限定死范围,不能够满足其他类型的使用,当然,也可以用const T& val的写法,因为匿名对象具有常性,且加const后,匿名对象T()的生命周期延长到使用完val。(缺省值调用默认构造函数,但是内置类型比如int没有构造函数,但是模板出现后进行了升级,可以认为内置类型有)

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

迭代器构造

要写迭代器构造,我们需要用模板定义一个InputIterator类型,因为如果直接使用迭代器的话,会导致这个构造函数只能用于初始化vector类型,其他类型不能使用,为了能够满足多种需求,我们需要通过类模板来实现,我们也要知道,在一个类模板里面也可以写模板函数

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

写完后测试运行时我们会发现出了问题:vector<int> v1 = (10, 0)居然报错了?而错误原因追溯到了迭代器构造函数,这是为什么?这种写法的初始化不应该转到n个val的构造函数吗?原来,上面的模板函数可以实例化出int int,而对于vector<int>来说是最匹配的,所以他会优先选择最匹配的,就不会到val初始化构造函数了,这也就导致了报错,且原因追溯到了迭代器构造函数,那有没有什么解决办法?再写一个int类的val的构造函数,让他直接匹配走到这里就可以了

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

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

拷贝构造函数

拷贝构造函数有传统和现代写法

传统写法:通过开辟一个新的空间,进行遍历赋值来深拷贝,这种方法可以用reserve和push_back复用达到效果:

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

现代写法:直接用迭代区间初始化去构造临时对象tmp ,然后让tmp和对象进行交换

vector(const vector<T>& v)           
{
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);
}

当然,写现代写法就需要再写一个swap

swap:

void swap(vector<T> tmp)
{
	std::swap(_start, tmp._start);
	std::swap(_end, tmp._end);
	std::swap(_endofstorage, tmp._endofstorage);
}

swap写好后,我们可以顺便将赋值重载运算符写出来,这里要实现深拷贝:

赋值重载运算符:

//v3 = v1 
//过程:要先进拷贝构造,因为v1传给tmp是拷贝
vector<T>& operator=(vector<T> tmp)
{
	swap(tmp);
	return *this;
}

Vector析构函数

关于析构函数,我们可以先释放掉_start,然后让_start直接给值给其他两个变量

~vector()
{
	delete[] _start;
	_start = _end = _endofstorage;
}

Vector成员函数

迭代器实现

vector类似顺序表,迭代器使用原生指针进行遍历,解引用等操作,同时迭代器拥有普通迭代器和const迭代器两种(注意const迭代器是只读,迭代器可变,指向的内容不变,const迭代器对应的是const T*)

template<class T>
class vector
{
public:
        typedef T* iterator;
		typedef const T* const_iterator;
        //迭代器想给别人用就要放在public
    
private:
		iterator _start=nullptr;
		iterator _end=nullptr;
		iterator _endofstorage=nullptr;
}

定义好后,我们可以用它把begin和end写出来:

iterator begin()
{
	return _start;
}

iterator end()
{
	return _end;
}
		
const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _end;
}

这样,一个简单的迭代器就做好了

size,capacity函数

这两个函数很简单,让他们返回相应的私有成员就好了

size:

size_t size() const
{
	return _end - _start;
}

capacity:

size_t capacity() const
{
	return _endofstorage - _start;
}

对于末尾的const,如果不加的话,const类型调用函数会出现错误

reserve函数

reserve扩容函数,首先我们要检查要扩容的n是否比capacity大,小的话不用扩(注意这里扩的是capacity,所以算endofstorage就用n了),然后new一个新的空间,并判断start是否有空间,有就拷贝销毁

void reserve(size_t n)
{
	if (n > capacity())
	{
		int sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * sz);
			delete[] _start;
		}
		_start = tmp;
		_end = _start + sz;
		_endofstorage = _start + n;
	}
}

写好后,这里的memcpy其实会导致一个比较严重的错误,具体是什么,我们可以通过举例来看看

举例

void test2()
{
	vector<string> v1;
	v1.push_back("111111");
	v1.push_back("111111");
	v1.push_back("111111");
	v1.push_back("111111");
	v1.push_back("111111");

	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

我们会发现,如果只有前四个就可以顺利打印,如果大于四个就会打印错误,这是因为浅拷贝的问题,大于四个数据就会进行扩容,在扩容时,我们会创建新的空间,并拷贝删除原来的空间,而这里使用的memcpy,是浅拷贝,通过监视我们可以发现,tmp创建后,memcpy是浅拷贝,会让tmp中的每个元素的地址和数据与_start中的一样,也就是说,进行memcpy后,delete掉_start时,会把浅拷贝的元素全部delete掉,这就导致tmp中的元素出现问题,找不到对象,也就会打印如烫烫烫的错误,怎么解决?

当然在这里不能用memcpy进行操作,因为我们已经创建了_start和tmp两段空间,所以直接进行赋值就可以了那我们能直接使用拷贝构造吗不能,库里面的是用的拷贝构造,因为库里的函数并没有用new直接创建对象,他类似于malloc是从内存池来的,并没有初始化,所以用定位new调用拷贝构造进行初始化,而这里已经创建好了两个对象进行了初始化

所以,经过改良后的reserve函数应该是这样子的

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

resize函数

resize函数可以改变size值,或者进行扩容改变capacity值,缩容扩容赋值他都做得到。

当n < size()时,进行缩容,改变_end的值即可,n > size()时,调用reserve函数进行扩容,并赋值

void resize(size_t n, T val = T())
{
	if (n <= size())
	{
		_end = _start + n;
	}

	else
	{
		reserve(n);
		while (_end < _start + n)
		{
			*_end = val;
			_end++;
		}
	}

}

关于T val = T()的问题在上文n个val初始化构造处已有阐述,这里不做解释

push_back函数

即尾插函数,逻辑上来讲,我们要先检查扩容,然后赋值进去

void push_back(const T& x)
{
	if (_end == _endofstorage)
	{
		size_t capacity_s = capacity() == 0 ? 4 : capacity() * 2;
		T* tmp = new T[capacity_s];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * sz);
			delete[] _start;
		}
		_start = tmp;
		_end = _start + size();
		_endofstorage = _start + capacity_s;
	}
	*_end = x;
	++_end;
}

现在的代码好像在逻辑和语法上看起来都没有问题,但是一经测试我们发现又出了问题:假如成员都没有值,新建后_end竟没有给值,也就是_start = tmp; _end = _start + size();处出现了错误,原因是什么?因为这里size()是_end - _start,end是旧数据,start是新数据,相减的话减不出来,那是不是换个位置,让start = tmp放后面行不行?也不行,这时start是0,size()减出来的也是0,所以可以给个sz,先把size()存起来使用,就可以避免直接调用出现问题

void push_back(const T& x)
{
	if (_end == _endofstorage)
	{
        size_t sz = size();
		size_t capacity_s = capacity() == 0 ? 4 : capacity() * 2;
		T* tmp = new T[capacity_s];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * sz);
			delete[] _start;
		}
        _start = tmp;
        _end = _start + sz;
        _endofstorage = _start + capacity_s;
	}
	*_end = x;
	++_end;
}

理解了push_back的底层后,我们可以进一步简化,我们已经实现了reserve函数,那是否可以将reserve函数进行复用,节省代码呢?

void push_back(const T& x)
{
	if (_end == _endofstorage)
	{
        reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_end = x;
	++_end;
}

insert,erase函数

插入,删除函数,他们通过pos找到对应的位置,进行操作。在vector的实现当中,我们可以用迭代器帮助实现:

实现insert的逻辑是:insert一般是在pos位置的上一个插入,也就是说,我们需要挪动数据,我们可以创建一个end,让他从尾开始遍历,让从pos位置开始的数据都往后挪动一位,当end移动到pos之前时,说明pos位置的值已经被移动,现在pos位置就没有值了,那么我们就可以填入x,达到inset的目的

insert:

void insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _end);//可以等于,等于了就相当于尾插

	//检查是否需要扩容
	if (_end == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	iterator end_s = _end - 1;

	while (end_s >= pos)
	{
		*(end_s + 1) = *(end_s);
		end_s--;
	}

	*pos = x;
	++_end;
}

这里有一个问题:头插需不需要单独处理?答案是不需要,因为pos的值不可能为0,因为pos是一个迭代器,我们知道,这里的迭代器是用的原生指针重定义的,就算用原生指针,一个原生指针的值不能为0,为0的话就变成空指针了

erase:

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

	iterator it = pos + 1;
	while (it < _end)
	{
		*(it - 1) = *it;
		it++;
	}
	_end--;
}

到这里,我们的重量级来了:迭代器失效问题。迭代器失效是指:如果失效就不能再用这个迭代器,如果使用了,结果是未定义的。insert暂且不谈,当我们在测试erase时,会发现有些情况下他不能删除数据,比如,现在想要找到一串数据中的偶数并删除

void test2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		else
			it++;
	}

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

我们会发现,erase的操作方法是移动覆盖,然后pos++,如果有连续的偶数,删除一个后移动,而移动过来的是一个偶数,这个偶数并没有进行判断,pos会直接++,导致删不完的情况。又如果数据的最后一个数是偶数,进行删除需要移动数据,下一个数据是空的,也是_end的位置,移动后pos指向_end,而_end--,会造成越界的问题,这些都是迭代器失效。

那有没有什么解决方法?在库中erase是iterator类型,他会返回被删除数据的下一个位置,也就是说,如果让it去接收返回值就不会造成迭代器失效,当然,这个是库中的erase的用法,所以,我们也要把类型改为iterator,让他返回下一个位置的数据

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

	iterator it = pos + 1;
	while (it < _end)
	{
		*(it - 1) = *it;
		it++;
	}
	_end--;

	return pos;
}

void test2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
    
    for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
			it++;
	}

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

现在erase就避免了迭代器失效的问题,现在我们转回insert,insert也有迭代器失效的问题:pos野指针问题,我们知道,insert可以复用到尾插,而当在尾插或者任何需要扩容的场景时,会出现pos失效的问题,因为pos位置指向扩容前的空间的位置,我们的扩容是创建一个新的空间,然后拷贝删除原来的空间,在这个操作下,pos指针并没有发生变化,可以形象的理解为:搬家了但是没告诉你新家在哪儿,你还是只有原来的家的情报,这时候pos就变成了野指针,所以想要扩容后正常使用pos找到对应位置,可以在扩容操作时保存pos对于start的相对位置,扩容后用新start加上相对位置,就得到了扩容后的pos的位置

void insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _end);//可以等于,等于了就相当于尾插

	//检查是否需要扩容
	if (_end == _endofstorage)
	{
		int position = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + position;
	}
    iterator end_s = _end - 1;

	while (end_s >= pos)
	{
		*(end_s + 1) = *(end_s);
		end_s--;
	}

	*pos = x;
	++_end;
}

现在代码经过改良,应该是没有问题了,那我们再来进行测试:

void test2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
	vector<int>::iterator it = v.begin() + 2;
	v.insert(it, 10);

	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

又又又出现错误了:这里使用一次后it就失效了,显然这是迭代器失效的问题,为什么?insert的代码中不是改了吗,为什么还会有这个问题?仔细观察,这里it是传值,pos是it的拷贝,pos的改变当然不影响it,那加个引用可不可以?也不行,这样一来v.insert(v.begin() + 2, 30);就用不了了,因为这里的v.begin() + 2是一个临时变量值,临时变量具有常性,引用必须传变量,加了引用就不行了,那再在前面加一个const?还是不行,加了const虽然v.begin() + 2能进来了,但是pos就改变不了了,所以insert的参数类型选择了iterator pos,且insert使用一次后pos可能会失效,要谨慎使用

其实解决了内部pos空指针的问题后,我们可以将insert复用到push_back中,让他进一步简化:

void push_back(const T& x)
{
	insert(end(), x);
}

运算符重载

这里是重载了[]运算符,让[]可以做到解引用

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

const T& operator[](size_t po) const
{
	assert(po < size());
	return _start[po];
}
//这里也需要写一个const的[],因为*it不可以改变

总结

到这里,vector的核心函数和内容都已经模拟实现完了,学习容器的时候,务必要进行模拟实现的步骤,这样可以帮助自己理解容器,更好的学习!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值