【C++STL】模拟实现vector容器


前言

本文带你进入vector的模拟实现,对于vector,是我们深入学习STL的必要途径。


一、vector的成员函数

根据库的实现方式,成员函数如下:

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

c++11开始可以对成员变量使用缺省值,在这里我们可以使用缺省值。

二、增删查改工作

说明size()和capapcity()

在这里插入图片描述

size的大小为_finish - _start
capacity的大小为_end_of_storage - _start

2.1reserve()

该函数的作用是:扩容。

void reserve (size_type n);
  • 思路:
  • 1.如果要扩容的大小n小于capacity,则不会缩容。
  • 2.否则执行扩容。
    • 先申请一块n大小的空间
    • 拷贝原空间的数据到新空间,并释放原空间
    • 将新空间的数据交给_start管理。
//扩容
void reserve(size_t n)
{
    
    
	//防止有些情况是直接调用
	if (n > capacity())
	{
    
    
		size_t oldsize = size();//必须要记录旧的size,旧的size是用旧的_finish - _start获得的
		iterator tmp = new T[n];
		//memcpy(tmp, _start, sizeof(T) * size());
		//这里如果使用memcpy函数的话,如果vector对象是自定义类型,比如string,会造成浅拷贝问题。
		for (size_t i = 0; i < oldsize;i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
		{
    
    
			tmp[i] = _start[i];
		}
		delete[] _start;
		_start = tmp;
		//_finish = _start + size();
		//如果这样用的话,size()是用旧的_finish - 新的_start,不是连续的空间,没有意义
		_finish = _start + oldsize;//_finish必须更新,旧的_finish迭代器已经失效了
		_end_of_storage = _start + n;
	}
	//tmp不用处理,出了作用域自动调用析构
}

注意:在扩容时,如果是自定义类型,比如string,使用memcpy函数进行拷贝数据,会出现浅拷贝问题。
在这里插入图片描述
v1会拷贝给v2,v2的指针仍然指向v1的string对象所指向的空间。这就造成了两次调用析构函数释放同一块空间的问题。
正确的解法是:
在拷贝时调用string的赋值运算符重载,完成深拷贝。

在这里插入图片描述

还有一个点是:

注意:在这里会出现迭代器失效的问题。

在这里插入图片描述

此时空间已满,则需要重新申请空间。

在这里插入图片描述

重新申请空间后,_start和_end_of_stortage都会指向新的空间,唯独_finish还指向旧的已经被释放的空间,此时如果这样更新_finish位置:

_finish = _start + size();

这里的size()是用旧的_finish - 新的_start得到,是没有意义的。

解决方案:
保存旧的size,随后_start加上旧的size即为_finish的位置。

2.2 resize()

该函数的功能是:重新改变vector容器的大小,让其到达n。新的空间默认初始化为val
思路:

  • 1.如果n小于size,则让_finish指向_start + n位置。
  • 2.如果n大于等于size,先调用reserve函数进行扩容,再将新的空间填充为val。
void resize(size_t n, const T& val = T())
{
    
    
	if (n < size())
	{
    
    
		_finish = _start + n;
	}
	else
	{
    
    
		//扩容到n,如果n<capacity(),则啥事不做,否则,扩容到n
		reserve(n);
		//将多的空间全部设置成缺省值
		while (_finish != _start + n)
		{
    
    
			*_finish = val;
			++_finish;
		}
	}
}

注意这里的缺省值为T(),是一个T类型的匿名对象。
不管T是什么,都可以使用模板实例化出具体的匿名对象,即使是内置类型,比如int,也可以实例化出int(),这里的值为0

2.3 insert()

在这里插入图片描述
在这里我们只实现最常用的接口。

该函数的作用是:

在pos位置插入模板对象val。

思路:

  • 插入元素前先检查容量
  • 如果容量满了,则调用reserve函数执行扩容操作
  • 然后从pos位置开始将其之后的元素全部都往后挪。
  • 再在pos位置插入val。
void insert(iterator pos, const T& val)
{
    
    
	assert(pos <= _finish);
	//插入前先检查扩容
	if (_finish == _end_of_storage)
	{
    
    
		size_t oldpos = pos - _start;//记录相对位置
		size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
		reserve(newcapacity);

		pos = _start + oldpos;
	}

	//记录旧的size,防止出现迭代器失效问题。

	//将pos之后的位置往后挪,再插入
	iterator end = _finish - 1; // reserve之后,_finish也跟着更新了。
	while (end >= pos)
	{
    
    
		*(end + 1) = *end;
		--end;
	}

	*pos = val;
	_finish += 1;
}

注意:insert在扩容时仍然会出现迭代器失效的问题。
解决方案:提前记录pos的相对位置,即oldpos = pos - _start;
然后再扩容完成后,更新pos 的位置,pos = _start + oldpos;
否则在扩容后,旧的pos迭代器就已经失效了。

2.4 erase()

该函数的功能是:删除pos位置的值。

//在删除最后一个位置或者只剩下一个待删除元素时,会出现迭代器失效问题。
iterator erase(iterator pos)
{
    
    
	assert(pos < _finish);
	iterator end = pos;
	while (end != _finish)
	{
    
    
		*end = *(end + 1);
		++end;
	}
	--_finish;

	return pos;
}

有一个需要注意的点:当我们删除元素后,pos迭代器就会失效,因为空间已经释放,这时候不能使用pos迭代器。
在这里插入图片描述

解决方法:返回删除位置的下一个元素的位置,即pos位置。
然而,在只剩下一个待删除元素或者删除最后一个位置的元素时,不能再使用pos迭代器。

2.5 push_back()和pop_back()

顾名思义,这两个函数的功能是在最后一个位置插入元素和删除最后一个位置的操作。
直接复用insert和erase函数即可,这里不再赘述。

三、[]重载和迭代器

typedef T* iterator;
typedef const T* const_iterator;

3.1begin迭代器

返回_start地址即可。

iterator begin()
{
    
    
	return _start;
}

const_iterator begin() const
{
    
    
	return _start;
}

3.2 end迭代器

返回_finish地址即可。

iterator end()
{
    
    
	return _finish;
}

const_iterator end() const
{
    
    
	return _finish;
}

3.3 []运算符重载

思路:只需给定pos下标即可。
在此之前需要判断pos位置不能超过整个顺序表的size大小。

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

//不可修改的
const T& operator[]( size_t pos) const
{
    
    
	assert(pos < size());
	return *(_start + pos);
}

四、默认成员函数

4.1构造函数

构造函数可分为3种:

  • 无参构造
  • 构造n个val对象
  • 根据迭代区间构造

对于无参的构造,我们只需要初始化成员函数即可。

  • 1.无参构造
//无参构造函数
vector()
//:_start(nullptr)
//,_finish(nullptr)
//,_end_of_storage(nullptr)
{
    
    }
  • 2.构造n个val对象
//构造n个对象
vector(size_t n, const T& val = T())
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
    
    
	//1.开空间
	reserve(n);
	//2.将空间填充为val对象
	for (size_t i = 0; i < capacity(); i++)
	{
    
    
		_start[i] = val;
	}
	//也可以复用resize
	//resize(n, val);
}

注意:这里在构造时必须初始化,如果不初始化,在调用reserve函数开空间时会出现访问野指针的问题。
也可以调用resize函数进行拷贝构造n个对象。

  • 3.使用迭代区间进行构造

同理,如果不初始化,会出现扩容后访问野指针的情况。

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

4.2 拷贝构造函数

  • 1.拷贝构造传统写法
//拷贝构造,必须实现深拷贝
//传统写法
//v1(v)
vector(const vector<T>& v)
	//这里必须初始化,否则扩容时会访问不属于自己的空间
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
	//如果私有成员给了缺省值,就不用再再次初始化了
{
    
    
	reserve(v.capacity());
	//memcpy(_start, v._start, sizeof(T) * v.size());
	//这里使用memcpy的错误跟扩容的错误一样,不再赘述
	for (size_t i = 0; i < v.size(); i++) // 如果是string类,会调用string的赋值重载,赋值重载调用string的拷贝构造,完成深拷贝
	{
    
    
		_start[i] = v._start[i];
	}

	_finish = _start + v.size();
}

拷贝构造需要注意的几个问题:

  • 1.拷贝构造同构造函数一样,如果不初始化,在开空间时会出现访问野指针的情况。
  • 2.拷贝构造必须进行深拷贝,否则如果vector的对象是自定义类型,比如string,会出现两次释放同一块空间的问题。

4.3 析构函数

释放_start指向的空间,然后全部置为空即可。

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

总结

vector的模拟实现就到此结束了,如果对你有帮助的话,不妨来一个关注吧!

猜你喜欢

转载自blog.csdn.net/w2915w/article/details/131743540