【C++】vector的模拟实现

目录

一、基础模板:

1、基本结构:

2、默认成员函数:

二、初始化构造:

1、迭代器区间构造:

2、使用n个val初始化构造:

3、初始化数组构造:

三、vector的遍历操作:

1、[ ] 符号重载:

2、迭代器遍历(Print):

四、增删操作:

1、reserve 与 resize:

2、push_back 与 pop_back:

3、insert 与  erase:

五、隐藏的浅拷贝问题:


一、基础模板:

1、基本结构:

 

  • _start:指向数据块的开始
  • _finish:指向数据块中有效数据的下一个位置
  • _endofstorage:指向数据块的结尾

代码形式:

// 防止与库中的vector冲突,创建新的命名空间
namespace yue
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

      	size_t size() const // 返回数据块中的有效数据的个数
		{
			return _finish - _start;
		}
		size_t capacity() const // 返回数据块中有效空间容量的大小
		{
			return _endofstorage - _start;
		}
		// 成员函数...
	private:
		iterator _start = nullptr; // 给定缺省值
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
}

2、默认成员函数:

无参构造函数:

        无参的构造函数在定义成员变量时给过缺省值的可以不写。

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

拷贝构造函数:

         思路:利用reserve函数开辟好空间后利用迭代器将 v 中的数据一个个尾插到自身数据块中。

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

swap函数:

        在库中提供的swap函数对于交换自定义类型的代价极大,要进行深拷贝,过程会经历三次拷贝一次析构,所以我们应该手动写一个swap成员函数。

void swap(vector<T>& v)
{
	// 可以直接调用库中的swap函数更改指针的指向,这样就不需要进行深拷贝
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

运算符 = 重载:

        思路:利用传值调用会拷贝构造产生一份形参的特点,用swap函数交换其指向即可达到目的,且修改形参并不影响实参,且传值调用的拷贝的形参出了函数作用域也会自动销毁,使用起来非常方便。

vector<T>& operator=(vector<T> v)
{
    swap(v); // 该swap函数为自己实现的函数,非库中的函数
	return *this;
}

析构函数: 

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

二、初始化构造:

1、迭代器区间构造:

利用任意类型vector的一段区间去构造:

// 类模板的成员函数可以是函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

使用函数模板InputIterator的原因:

        提供泛型编程的能力,使得适应不同的容器,就是这个构造函数可以接受任何类型的迭代器,只要这个迭代器满足InputIterator的概念,例如数组,函数的迭代器等。如下:

string s1("abcd");
vector<int> v1(s1.begin(), s1.end());

        这样vector也能支持string的迭代器,打印进v1中的值会隐式类型转换成int,也就是字符的ASCII值。

2、使用n个val初始化构造:

例如:vector<int> v(10,1)   构建数组v并初始化为10个1。

// 使用匿名对象T(),使其支持内置类型
vector(size_t n, const T& val = T())
{
	reserve(n); // reserve的实现在下面
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

        但是就单独上面这样子写在面对 int (就例如vector<int> v(10,1) )类型的时候就会出问题,会报一个非法的间接寻址的错误,参数类型会优先匹配上一个(迭代器区间构造)函数,因为编译器无法正确识别到我们要用哪一个函数,所以会自动匹配(迭代器区间构造)这个函数。

        因为迭代器类型可以转换为int,而我们写的函数类型是 size_t 和 int(模板),虽然前者要进行类型转换,但相比于后者,转换后的前者类型更匹配,所以在面对int,int类型时,编译器会优先选择前者。

解决的方法其实也很简单粗暴:重载一个int类型的函数即可,如下:

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

这时编译器就会优先选择这个函数。

3、初始化数组构造:

例如:vector<int> v(1,2,3,4,5,6,7,8)   构建数组v并对其进行初始化。

关于 initializer_list:

        initializer_list用于表示某种特定类型的值的数组,和vector一样,initializer_list也是一种模板类型。可以用于容纳同一类型的多个元素。然而,与vector不同的是,initializer_list中的元素是常量值,一旦初始化后就不能改变。此外,initializer_list的拷贝和赋值操作并不会拷贝列表中的元素,而是实现元素的共享

void Test()
{
    // 定义在同名头文件中
	initializer_list<int> x = { 1,2,3,4,5,6,7,8 };
	std::vector<int> v1 = { 1,2,3,4,5,6,7,8 };

	// 有点类似于:
	string str = "1111"; // 构造+拷贝构造 -> 优化: 直接构造
	const string& str1 = "1111"; // 构建临时对象,引用的是临时对象临时对象具有常性要加const
	// 用1111初始化str的原因就是单参数的构造函数支持隐式类型转换
}

代码实现:

// vector<int> v = { 1,2,3,4,5,6,7,8 };
vector(initializer_list<T> l)
{
    // initializer_list中也有迭代器,可以直接用范围for遍历
	reserve(l.size());
	for (auto& e : l)
	{
		push_back(e);
	}
}

// 直接构造:
vector<int> v({ 1,2,3,4,5,6 });

// 隐式类型转换 + 优化:
vector<int> v = { 1,2,3,4,5,6 };

三、vector的遍历操作:

1、[ ] 符号重载:

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

2、迭代器遍历(Print):

        范围for: 其实本质也是迭代器 

template<class T>
void Print(const vector<T>& v)
{
	//typename vector<T>::iterator it = v.begin();
	auto it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

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

四、增删操作:

1、reserve 与 resize:

reserve(指定容量):

这里实现的扩容会有些小问题,放在第五个标题(隐藏的浅拷贝问题)中重新做一个更改。

void reserve(size_t n)
{
	// 如果n大于capacity则进行扩容操作
	if (n > capacity())
	{
		T* tmp = new T[n];

		// 先记录size的值,扩容后旧空间会被释放
		size_t oldsize = size();
		memcpy(tmp, _start, size() * sizeof(T)); // 拷贝数据
		delete[] _start; // 释放旧空间

		_start = tmp;
		_finish = _start + oldsize; // 旧空间被释放,size()里的_finish为野指针,
		_endofstorage = _start + n;
	}
}

resize(扩容+初始化):

// 右参数给匿名对象,在没有右参数时,对应的类会去调用其的默认构造
// 而默认的构造函数会对 val 进行赋值,满足resize的右参数需要
void resize(size_t n, const T& val = T())
{
	if (n > capacity())
	{
		reserve(n);
		// 插入数据
		while (_finish < _start + n)
		{
			*_finish = val;
			_finish++;
		}

	}
	// 当n < _finish 时删除有效数据至n
	else
	{
		_finish = _start + n;
	}
}

2、push_back 与 pop_back:

push_back(尾插):

void push_back(const T& val)
{
	// 检查扩容
	if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = val;
	_finish++;
}

pop_back(尾删):

// 判空
bool empty()
{
	return _start == _finish;
}
void pop_back()
{
	assert(!empty()); // 如果数据块为空,则不能进行删除操作
	_finish--;
}

3、insert 与  erase:

insert(指定位置插入):

void insert(iterator pos, const T& val)
{
	// 检查范围
	assert(pos >= _start); // = 就是头插
	assert(pos <= _finish); // = 就是尾插

	// 检查扩容
	if (_finish == _endofstorage)
	{
		// 先记录数据块中插入数据位置之前的数据的个数
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);

		// 更新pos扩容后所在的位置(指向)
		pos = _start + len;
	}

	// 利用迭代器挪动数据
	iterator it = _finish - 1;
	while (it >= pos)
	{
		*(it + 1) = *it;
		it--;
	}

	// 插入数据
	*pos = val;
	_finish++;
}

注意点:

1、为什么要提前计算并保留len(数据块中插入位置之前的有效数据的个数)?

        因为会引发迭代器失效的问题,pos是迭代器类型,指向要插入数据的位置,如果刚好要进行扩容操作的话,会开辟新空间,旧空间会被释放,而pos指向的是旧空间的位置,所以要同步更新pos的指向。

erase(指定位置删除):

iterator erase(iterator pos)
{
	// 检查范围
	assert(pos >= _start); // = 就是头删
	assert(pos < _finish); // _finish为数据块中最后一位有效数据的下一个位置

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

    return pos;
}

五、隐藏的浅拷贝问题:

当我们使用string类型的数组例如(或者使用vector<vector>类型)的时候程序就会挂掉,如下:

void Test_vector7()
{
	vector<string> v;
	v.push_back("11111");
	v.push_back("22222");
	v.push_back("33333");
	v.push_back("44444");
	v.push_back("55555");
}

直接说结论,会在析构的时候崩掉,因为在扩容的时候出了问题。

        回顾上面所写的reserve功能代码,使用了memcpy这个函数,memcpy实现的功能是从源内存块复制指定字节数的数据到目标内存块,这是一个隐藏浅拷贝,扩容完之后_start会释放旧空间再指向新空间,但是新空间的数据是已经被释放掉了的,所以此时_start就是一个野指针,再次析构就会崩溃。

解决方法:将赋值改为手动赋值(改为for一个个赋值,赋值调用的就是对应容器的深拷贝)

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];

		size_t oldsize = size();

		// memcpy(tmp, _start, size() * sizeof(T)); 
		for (size_t i = 0; i < oldsize; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start; 

		_start = tmp;
		_finish = _start + oldsize; 
		_endofstorage = _start + n;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值