C++STL:vector的使用及模拟实现

Posted 山舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++STL:vector的使用及模拟实现相关的知识,希望对你有一定的参考价值。


前言

vector和string一样,都是最简单的容器,逻辑和实现上都非常类似,所以本篇内容很多需要注意的地方在【C++】STL:string的模拟实现【C++】STL:string的使用中都已提到过,读者如有问题可阅读这两篇文章对应位置。

一、vector的介绍

vector是表示可变大小数组的序列容器,采用连续的存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。包含在< vector >头文件下。


二、vector的使用

STL之所以好用,不仅是因为它提供了常见的数据结构容器及操作,更是因为对容器的操作大部分相似,学习成本很低。

【C++】STL:string的使用中已经介绍了string的操作,而vector的操作也大同小异,所以下面只给出vector使用的例子,而不再系统地讲解每个操作函数的功能。


1.构造函数

常用的构造一个vector有如下五种方法,通过调试可以看到它们具体的内容。


还有一种特殊的结构vector<vector< int >>,如果是第一次见到可能不容易理解,其实这就是一个二维数组。


2.遍历

这里的遍历仍有三种方法,迭代器是STL中所有容器都支持的遍历方式;范围for是一种简便写法,实质仍是迭代器,也是所有容器都可用;下标+[]是string和vector才能用的,因为它们的地址空间是连续的。同时这三种方法均是可读、可写。

代码如下:

int main()
{
	vector<double> v = { 1.1, 2.2, 3.14, 6.66 };

	//1.迭代器
	vector<double>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	//2.范围for
	for (double e : v)
		cout << e << " ";
	cout << endl;

	//3.下标+[]
	for (size_t i = 0; i < v.size(); i++)
		cout << v[i] << " ";
	cout << endl;

	return 0;
}

运行结果如下:


3.数据的插入与删除

针对数据的操作有如下四种,如果对STL熟悉的话只看函数名应该就可以知道这个函数的功能。

代码如下:

//一个打印v中数据的函数
template<class T>
void print(const vector<T>& v)
{
	//传的v是const修饰的,所以这里要用const迭代器
	vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}
int main()

{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(30);
	v.push_back(4);
	v.push_back(6);
	print(v);
	v.pop_back();
	print(v);

	//find为C++算法库中的查找函数
	//给出迭代器区间和要查找的数据即可查找
	vector<int>::iterator it = find(v.begin(), v.end(), 30);
	if (it != v.end())//找到了
	{
		//在30之前插入-20
		v.insert(it, -20);//insert的第一个参数要用迭代器
	}
	print(v);

	//删除it前面的数据
	v.erase(it);
	print(v);
	return 0;
}

运行结果如下:


3.resize和reserve

对于一个顺序表v,size是当前v中的数据个数,capacity是v中能存储的最大数据个数。

代码如下:

int main()
{
	vector<int> v;
	//size:当前v中的数据个数
	//capacity:v中能存储的最大数据个数
	cout << "size:" << v.size() << endl;
	cout << "capacity:" << v.capacity() << endl << endl;

	v.resize(30);
	cout << "size:" << v.size() << endl;
	cout << "capacity:" << v.capacity() << endl << endl;

	v.reserve(50);
	cout << "size:" << v.size() << endl;
	cout << "capacity:" << v.capacity() << endl << endl;
	return 0;
}

运行结果如下:


三、vector的模拟实现

1.成员变量

数据结构(一):顺序表中实现顺序表时,使用的结构是一个动态开辟的数组+size+capacity,而STL底层实现vector时使用三个指针完成的,这里模仿它来完成。

代码如下:

template<class T>
class Vector
{
public:
	//...
private:
	T* _start;
	T* _finish;
	T* _endOfStorage
};

_start指向这段数据的开始,_finish指向有效数据的末尾的下一个位置,_endOfStorage指向_start开辟的整个空间的末尾。具体如下。

由上图很容易看出,size = _finish - _start,capacity = _endOfStorage - _start。


由于vector存放数据时地址是连续的,所以迭代器也是原生指针,所以可将上面的代码封装如下:

template<class T>
class Vector
{
public:
	typedef T* Iterator;
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

2.构造函数

这里实现多个构造函数重载。

在构造函数这里使用了一些下面会实现的函数,如果对这些函数不是很明白,可以先往下看,明白这些函数的底层实现后再回来看这里。

(1)默认构造函数

一个几乎什么都没做的默认构造函数。

代码如下:

template<class T>
class Vector
{
public:
	typedef T* Iterator;
	//把新构造的vector的三个指针全置空
	Vector()
		:_start(nullptr)
		, _finish(nullptr)
		, _endOfStorage(nullptr)
	{}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

(2)拷贝构造

即用一个已经实例化的vector对象v构造一个新的vector对象this。先给this开辟v的大小的空间(reserve函数),再依次把v的数据插入到*this内(这里用push_back)。

代码如下:

template<class T>
class Vector
{
public:
	Vector(const Vector<T>& v)
		:_start(nullptr)
		, _finish(nullptr)
		, _endOfStorage(nullptr)
	{
		this->reserve(v.capacity());
		for (auto e : v)
			this->push_back(e);
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

(3)构造含有n个val的vector

先用resize函数开辟n个空间,再依次把其值修改为val。或者可以借用上面的思路:先用reserve函数将vector的最大数据个数修改为n,进行n次this->push_back(val)。

代码如下:

template<class T>
class Vector
{
public:
	Vector(size_t n, T val)
		:_start(nullptr)
		, _finish(nullptr)
		, _endOfStorage(nullptr)
	{
		resize(n);
		Iterator it = _start;
		while (it != _finish)
		{
			*it = val;
			it++;
		}
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

3.析构函数

vector的析构比较简单,只需要释放空间并把三个指针置空即可。

代码如下:

template<class T>
class Vector
{
public:
	~Vector()
	{
		//释放空间
		if (_start != nullptr)
			delete[] _start;
		//把三个指针置空
		_start = nullptr;
		_finish = nullptr;
		_endOfStorage = nullptr;
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

4.赋值运算符重载

写法1:处理空间,处理指针,填充数据。

代码如下:

template<class T>
class Vector
{
public:
	//注意这里传const+引用,也就是说这个v就是函数外部的v,这里不能被修改
	//要与下面第2种写法区分
	Vector& operator=(const Vector<T>& v)
	{
		//防止自己给自己赋值
		if (this != &v)
		{
			//处理空间
			delete[] _start;
			_start = new T[v.capacity()];
			//处理指针
			_endOfStorage = _start + v.capacity();
			//填充数据
			//注意这里temp遍历v的空间,_finish遍历*this的空间
			Iterator temp = v._start;
			for (_finish = _start; temp < v._finish; _finish++, temp++)
				*_finish = *temp;
		}
		return *this;
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

写法2:用一个临时产生的对象来与*this的内容交换,具体可看代码中的注释。

代码如下:

template<class T>
class Vector
{
public:
	//重载一个这里需要的swap函数
	void swap(Vector<T> v)
	{
		//加::表示下面的swap函数时全局域内的
		::swap(_start, v._start);
		::swap(_finish, v._finish);
		::swap(_endOfStorage, v._endOfStorage);
	}
	//以v2 = v1为例
	//这里传参的v本身就是外部函数调用时v1的一个临时(深)拷贝,生命周期仅在这一函数内
	//由于v是v1的深拷贝,直接交换v与*this(也即v2)的内容即可
	Vector& operator=(Vector<T> v)
	{
		//将三个指针全部交换,*this内的内容就和
		this->swap(v);
		//*this原来的内容现在在v内,函数结束调用后v自动调用析构函数清理掉这些内容
		return *this;
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

5.一些简单的函数

这里实现一些逻辑简单的函数,如判空(empty),迭代器(begin,end)和const迭代器,size和capacity,operate[]等函数。

代码如下:

template<class T>
class Vector
{
public:
	typedef T* Iterator;
	typedef const T* const_Iterator;
	
	bool empty() const
	{
		return _start == _finish;
	}
	
	Iterator begin()
	{
		return _start;
	}

	Iterator end()
	{
		return _finish;
	}

	const_Iterator begin() const
	{
		return _start;
	}

	const_Iterator end() const
	{
		return _finish;
	}

	size_t capacity()
	{
		return _endOfStorage - _start;
	}

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

	size_t size()
	{
		return _finish - _start;
	}

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

	//注意operator[]可读可写,所以返回值要加引用
	T& operator[](size_t i)
	{
		assert(i < size());
		return _start[i];
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

6.reserve和resize

(1)reserve

reserve函数即预留出一定的空间存放数据,其中要注意一个细节。

代码如下:

template<class T>
class Vector
{
public:
	void reserve(size_t n)
	{
		if (n > capacity())
		{
			T* tmp = new T[n];
			size_t sz = size();//注意:sz要提前拿到
			if (_start != nullptr)
			{
				//拷贝数据
				for (size_t i = 0; i < sz; i++)
					tmp[i] = _start[i];
				delete[] _start;//释放旧空间
			}
			//修改指针
			_start = tmp;
			_finish = _start + sz;
			_endOfStorage = _start + n;
		}
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

注意:sz要在if判断前拿到,if判断结束后,原来的空间被释放,_start也随之被修改,而_finish和_endOfStorage没变,相减得到的size和capacity都是不对的。


(2)resize

resize与string的resize类似,且这里不需要处理\\0,可类比实现,这里不多赘述。

代码如下:

template<class T>
class Vector
{
public:
	//					  给缺省值,T为int时为0,为double时为0.0,为string时为"",其它类型也可类比
	void resize(size_t n, const T& val = T())
	{
		if (n < size())
			_finish = _start + n;
		else
		{
			if (n > capacity())
				reserve(n);
			while (_finish < _start + n)
			{
				*_finish = val;
				_finish++;
			}
		}
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

7.数据的增删查改

顺序表数据的增删查改的细节在数据结构(一):顺序表中已经介绍的很详细,且在数据结构中多次处理,对于之前提到的细节这里不多赘述。

(1)push_back

代码如下:

template<class T>
class Vector
{
public:
	void push_back(const T& x = T())
	{
		//顺序表已满,需扩容
		if (_finish == _endOfStorage)
		{
			//这里以每次2倍扩容为例
			size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			reserve(newcapacity);
		}
		*_finish = x;
		_finish++;
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

(2)pop_back

代码如下:

template<class T>
class Vector
{
public:
	void pop_back()
	{
		assert(!empty());
		_finish--;
	}
private:
	Iterator _start;
	Iterator _finish;
	Iterator _endOfStorage;
};

这里要提一下,STL中对于vector只有push_back和pop_back,而没有push_front和pop_front,因为这两个函数实现时需要挪动数据,导致O(N)的时间复杂度,效率太极,所以不提供。如果真的需要使用,可以调用insert和erase。


(3)insert

在某一位置前插入元素,注意位置要传迭代器作为参数。

代码如下:

template<class T>
class Vector
{
public:
	//在pos位置前插入x
	void insert(Iterator pos, const T& x)
	{
		size_t len = pos - _start;//pos之后元素个数
		if (_finish == _endOfStorage)//需要扩容
		{
			size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
			reserve(newcapacity);
			pos = len + _start;
		}
		//挪动数据
		Iterator end = _finish - 1;
		while (end >= pos)
		{
			*(end + 1) = *end;
			end--;
		}
		*pos C++STL第二篇:vector类的介绍及模拟实现

C++STL之vector的使用和实现

C++初阶:STL —— vectorvector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现

C++初阶:STL —— vectorvector的介绍及使用 | 迭代器失效问题 | vector的深度剖析及模拟实现

C++初阶:STL —— listlist的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比

C++初阶:STL —— listlist的介绍及使用 | list的深度剖析及模拟实现 | list与vector的对比