C++ vector使用介绍以及模拟实现

文章目录

    • 何为vector?
    • vector的初始化
    • vector的遍历
    • vector的查找与增删
  • vector的模拟实现
    • 迭代器
    • 构造和析构
    • 关于容量
    • 关于修改
    • 下标访问

何为vector?

vector是一个动态顺序表,可以理解为是一个数组不过该数组的大小是可变的,当数组的容量满了时,再插入数据vector会自动扩容。和string不同,string是一个类模板的实例,但vector是一个类模板,在初始化时需要我们传入模板参数。

vector的初始化

void test1()
{
	// 创建一个存储int对象的vector
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);

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

C++ vector使用介绍以及模拟实现_第1张图片
除了上面的创建vector的方式,还有两种方式创建vector

void test2()
{
	string str = "hello";
	// 使用迭代器创建vector,注意迭代器指向的数据类型要和vecotr存储的数据类型相同
	vector<char> v1(str.begin(), str.end());
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	// 初始化5个10的空间
	vector<int> v2(5, 10);
	for (auto e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;

}

vector的遍历

vector有三种遍历方式(和string一样这里不再赘述)

void test1()
{
	// 创建一个存储int对象的vector
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);

	// vector的遍历:范围for
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	// 下标遍历
	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << ' ';
	}
	cout << endl;

	// 迭代器
	vector<int>::iterator it = v1.begin();
	while (it != v1.end())
	{
		cout << *it << ' ';
		it++;

	}
	cout << endl;
}

vector的查找与增删

vector的查找和删除,vector并没有实现find函数,但在algorithm中实现了一个find函数在这里插入图片描述

传入要查找数据的迭代器区间,以及要查找的数据,该函数会返回一个指向查找到的数据的迭代器,如何我们可以通过vector的erase函数删除数据

void test3()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	for (auto e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
	// 通过find查找数据,再进行删除
	auto it = find(v.begin(), v.end(), 3);
	v.erase(it);

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

C++ vector使用介绍以及模拟实现_第2张图片
vector的插入需要传入一个迭代器,将数据插入到迭代器的位置,其他数据往后移动
C++ vector使用介绍以及模拟实现_第3张图片

void test4()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

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

	// 头插数据到vector中
	v.insert(v.begin(), -1);
	v.insert(v.begin(), -2);
	v.insert(v.begin(), -3);

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

C++ vector使用介绍以及模拟实现_第4张图片

vector的模拟实现

C++ vector使用介绍以及模拟实现_第5张图片

这次的模拟实现不再使用size,capacity来表示数组的大小和容量,而使用_start指针指向数组的第一个元素,_end指向数组最后一个存储数据的元素的下一个位置,_endofstrore指向数组最后元素的下一个位置。之所以用指针实现vector是因为STL库中也是这样实现,使用指针的方式更贴合“模拟”二字。

先给出vector的结构声明,接着一个个的实现声明中的函数

namespace myVector
{
	template <class T>
	class vector
	{
		typedef T* iterator;
		typedef const T* const_iterator;
	public:
		void swap(const vector<T> v);
		// 构造和析构
		vector();
		template <class InputIterator>
		vector(InputIterator first, InputIterator last);
		vector(int n, T val);  // 用n个T构造vector
		vector(const vector<T>& v);
		~vector();
		vector<T>& operator=(vector<T> v);

		// 迭代器
		iterator begin() { return _start; }
		iterator end() { return _finish; }
		const_iterator begin() const { return _start; }
		const_iterator end() const { return _finish; }

		// 容量
		size_t size() const;
		size_t capacity() const;
		void reserve(size_t n);
		void resize(size_t n, T val = T());

		// 修改
		void push_back(const T& val);
		void pop_back();
		void insert(iterator pos, const T& val);
		void erase(iterator pos);

		// 下标访问
		T& operator[](size_t n);
		const T& operator[](size_t n) const;


	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

迭代器

C++ vector使用介绍以及模拟实现_第6张图片
vector的迭代器就是指针,这些函数的作用就是返回指向vector开始或结束处的指针,逻辑简单,代码就写在类里了。

构造和析构

文档中声明vector的构造有三种:1.无参,2.用迭代器区间的数据构造,3.用n个val值初始化。最后一个是拷贝构造。
C++ vector使用介绍以及模拟实现_第7张图片
无参的构造函数就是将vector的三个原生指针置空

template <class T>
myVector::vector<T>::vector()
{
	_start = nullptr;
	_finish = nullptr;
	_endofstore = nullptr;
}

用迭代器first到迭代器last之间的数据构造并初始化一个vector,这里要用first迭代器遍历区间,然后调用push_back将遍历得到的数据尾插到vector中,push_back后面会实现。而迭代器类型肯定是不一样的,有指向内置类型的迭代器,指向自定义类型的迭代器,这就需要使用模板函数,再使用一个模板参数,编译器会根据实参的类型生成对应的函数

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

用n个val构造并初始化vector时,这个val值可以不给,也就是val是默认参数,当使用者不给形参时,初始化vector的val值就是默认的。但这样就有一个问题,val的默认值应该是什么?当val是自定义类型对象时,使用默认构造函数构造的对象就是val的默认值(形参中写:T val = T() ),但val是内置类型对象如int,char时,应该怎样操作?其实C++对于内置类型也支持默认构造函数,比如int i = 0,这是创建int对象并初始化成0,也能写成int i = int(),这里的意思就是创建int对象并调用int的默认构造函数初始化对象。
在这里插入图片描述
可以看到vs下int的默认构造是将int赋值为0。综上,得出结论:C++中的内置类型如int,char可以看成一个类,所以int类有默认构造函数,而C++这样做的原因是为了更好的支持模板

而实现就很简单了,for循环将n个val尾插到vector中

template <class T>
myVector::vector<T>::vector(int n, T val = T())
{
	for (int i = 0; i < n; i++)
	{
		push_back(val);	
	}
}

这里还要注意一个问题,int n最好不要写成size_t n,如果这样,当想用5个5初始化vector时程序会报错
C++ vector使用介绍以及模拟实现_第8张图片
大概就是非法访问内存,原因:vector< int> v(5, 5)中的两个5都是int类型,编译器在编译时不会将第一个5隐式类型转换成size_t,因为vector还有一个构造函数C++ vector使用介绍以及模拟实现_第9张图片
由于该构造的两个参数类型相同,两个5又都是int类型,编译器会生成该模板对应的函数,再调用它,而该函数需要对指针解引用,传个5进去,对5解引用当然是非法访问了。所以最好将n的类型写为int

vector的拷贝构造,用传入的vector对象v,构造函数出tmp对象,再将tmp与this指向的vector交换。

// 两个vector的交换,只要交换它们的三个原生指针,这里的交换调用std的交换
template <class T>
myVector::vector<T>::swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstore, v.endofstore);
}

// 用v拷贝构造
template <class T>
myVector::vector<T>::vector(const vector<T>& v)
{
	_start = nullptr;
	_finish = nullptr;
	_endofstore = nullptr;
	vector tmp(v.begin(), v.end()); // 用v构造tmp对象
	swap(tmp); // 此时的tmp是v的复制,将它交换
}

至于拷贝构造前为什么要将当前的vector的原生指针置空,因为如果不置空三个指针就是随机值,tmp是一个局部变量,出作用域销毁,因为交换此时tmp的三个原生指针指向随机空间,销毁tmp前要是否tmp的动态空间,而释放随机空间非法,所以要先将当前的原生指针置空

析构函数就是将vector的动态空间释放

template <class T>
myVector::vector<T>::~vector()
{
	delete[] _start; // 因为_start指向的空间是连续的,所以用delete[]
}

最后一个是vector的赋值重载,如果不写=的重载,编译器默认生成的重载只是一个浅拷贝,而vector涉及动态内存的申请,所以需要自己实现该重载,完成深拷贝。不把函数参数写成引用,在函数实例化形参时会去调用vector的拷贝构造函数,这时的形参就是一份拷贝,之后只要将当前的vector与形参交换。

template <class T>
myVector::vector<T>& myVector::vector<T>::operator=(vector<T> v)
{
	swap(v);
	return *this; // 返回当前的vector为了支持连续赋值
}

(vector的构造,如果无参:将三个指针置空,如果参数是两个迭代器:用迭代器初始化空间,当first迭代器不等于last迭代器,将first迭代器指向的数据push_back。push_back将数据尾插到vector,当容量满时调用reserve函数扩容,reserve:new n个T(存储的数据类型)空间,使tmp指向该空间,再将原来的数据拷贝到tmp指向的空间,注意这里的拷贝不是简单的浅拷贝,如果存储的数据是string,vector需要管理内存的类型,浅拷贝会使程序崩溃,所以不能直接用memcpy简单的拷贝数据,而是要用=,将数据一个一个的拷贝进去。而=如果不重载,也是一个浅拷贝,所以需要对=进行重载,重载=时使用现代写法,参数是vector但不是引用,这里会复用参数是vector的拷贝构造函数,参数是vector的拷贝构造函数会复用参数是两个迭代器的构造函数,又复用push_back,如果push_back的数据不是内置类型,又会调用=,直到push_back的数据类型是内置类型)

关于容量

C++ vector使用介绍以及模拟实现_第10张图片
容量有四个接口,size()返回当前vector存储数据的个数,capacity()返回当前vector最大能存储数据的个数,这两个接口简单直接贴代码

template <class T>
size_t myVector::vector<T>::size() const
{
	return _finish - _start; // 指针相减得到两指针之间的数据个数
}

template <class T>
size_t myVector::vector<T>::capacity() const
{
	return _endofstore - _start;
}

reserve()设置vector的容量,如果参数n大于当前的capacity,进行扩容,小于或等于,什么都不做。扩容时,先申请n个T的空间,再将原来空间上的数据深拷贝进新开辟的空间中(凡是有申请资源的类,都要注意深拷贝的问题),释放原来空间。

reserve还涉及迭代器失效问题,什么是迭代器失效?两个方面:1.迭代器指向空间被释放,形成野指针但你没发现。2.迭代器指向的位置意义变了。扩容时,在拷贝完原来数据后需要释放原来的空间,这时的迭代器仍然是指向原来的空间(包括原生指针),所以需要对迭代器进行更新,_start指向新开辟的空间,_finish和_endofstore却指向原来的空间,_endofstore只要指向_start后的n个位置就行,但_finish要指向_start后size个位置,而size函数要用_finish - _start,这时的_finish已经是个失效的迭代器,所以不能调用size函数,需要先记录原来的size

template <class T>
void myVector::vector<T>::reserve(size_t n)
{
	if (n > capacity())
	{
		int sz = size();
		T* tmp = new T[n];
		if(_start) // 为空不进行拷贝
		{
			for (int i = 0; i < size(); i++)
			{
				tmp[i] = _start[i]; // 复用=的重载,如果vector存储的数据需要深拷贝,将深拷贝工作交给=的重载,前提是该类型=的重载实现了深拷贝,当然如果没有实现这里会出问题,但这不是我们的事
			}
			delete[] _start; // 拷贝完别忘记释放
		}			
		_start = tmp;
		_finish = _start + sz;
		_endofstore = _start + n;
	}
}

(记录一个在模拟实现中想到的问题,在实现reserve时,最开始没有进行vector为空并不进行拷贝的if条件判断,之后便思考是否有必要进行该判断,_start为空,说明其他两个指针也为空,在for循环的条件判断时调用size函数,以此引出一个疑问,**两个空指针是否能相减?**便敲了代码验证C++ vector使用介绍以及模拟实现_第11张图片
可以看到,两个指针相减的前提是指针类型要统一,而nullptr作为一个空指针是没有类型的。但对于一个空的vector调用size却不会报错,并且还打印出了0,思索了一会才明白这只是C语言的语法基础。两个指针相减得到的是它们之间数据个数,这是怎么做到的?大概就是地址相减后再除以对应类型的字节数得到最后的数据个数,但nullptr没有数据类型,想要跑通这行代码只要用强制类型转换,所以不用判断vector是否为空也行,之后的delete对于空指针不释放)C++ vector使用介绍以及模拟实现_第12张图片
题外话结束,resize,对于n大于size并且大于capacity的情况,resize就是reserve+初始化空间,开辟空间后,尾插数据,改变size。对于n只大于size小于capacity的情况,尾插数据使size到n。对于n小于size的情况,修改_finish指针达到删除数据的效果

template <class T>
void myVector::vector<T>::resize(size_t n, T val = T())
{
	if (n > capacity())
	{
		reserve(n); 
	}
	
	if (n > size())
	{
		while (_finish != _start + n)
		{
			push_back(val);
			_finish++;
		}
	}
	else
	{
		_finish = _start + n;
	}
}

关于修改

C++ vector使用介绍以及模拟实现_第13张图片
clear就是清空vector中的数据,只要将_finish指向_start的位置就行,代码简单直接写在类里。

push_back尾插数据到vector中,插入数据前要检查是否需要扩容,之后解引用_finish,将数据写入,最后++_finish。pop_back就是直接–_finish

template <class T>
void myVector::vector<T>::push_back(const T& val)
{
	// 如果容量为0,直接乘2还是0,所以要判断一下
	if (_finish == _endofstore)
		reserve(capacity() == 0 ? 4 : capacity() * 2);		
	*_finish = val;
	_finish++;
}

template <class T>
void myVector::vector<T>::pop_back()
{
	if (size()) // 不为空了再删除
		--finish;
}

insert和erase都涉及到迭代器失效问题,在pos位置插入一个数据,如果容器满了,扩容后虽然容器的三个迭代器不会失效,但用户传的迭代器pos会失效:指向原来的空间,所以在扩容要前先计算pos位置与_start的相对位置。之后就是移动数据,插入val值。但还有一个问题,有这样一个场景:在所有偶数前插入一个2,然后我们用it迭代器去遍历整个vector,随着2的插入vector进行了扩容,而it指向的还是原来的空间(虽然vector的三个迭代器没有失效,但用来遍历vector的it失效了),标准库是怎么解决这个问题的?通过返回迭代器,一个指向新插入数据的迭代器,这样我们在每次的插入后更新it,it = insert(… , …),就能保证迭代器不失效
C++ vector使用介绍以及模拟实现_第14张图片

template <class T>
typename myVector::vector<T>::iterator myVector::vector<T>::insert(iterator pos, const T& val)
{
	if (_finish == _endofstore)
	{
		int n = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + n; // 更新pos迭代器
	}
	// 数据的挪动
	auto end = _finish;
	while (end > pos) // 挪动数据
	{
		*end = *(end - 1);
		end--;
	}
	*pos = val; // 插入数据
	_finish++;

	return pos;
}

(insert的返回值是一个迭代器,在类外写这个函数时,需要在迭代器前加上类名::,表示该迭代器是定义在这个类下的,但这样与类名::静态变量名,这种访问静态变量的方式冲突,所以编译器不知道这里的返回类型是一个类型还是一个静态变量,编译器会报错,需要加上typenmae显式地告诉编译器这是一个类型

在这里插入图片描述
再来看删除,本质上就是移动数据将被删除的数据覆盖,要注意的是erase返回值是指向被删除位置的下一个位置的迭代器,其实就是还是指向被删除的位置,因为后面的数据向前移动了。

template <class T>
typename myVector::vector<T>::iterator myVecotr::vector<T>::erase(iterator pos)
{
	if (pos >= begin() && pos < end()) // 判断pos的合法性
	{
		auto end = pos + 1;
		while (end < _finish)
		{
			*(end - 1) = *end;
			end++;
		}
		_finish--;
	}
	return pos;
}

下标访问

在这里插入图片描述
通过下标不仅要支持读,还要支持写,所以下标访问函数的返回值是数据的引用

template <class T>
T& myVector::vector<T>::operator[](size_t n)
{
	assert(n < size()); // 断言n的合法性
	return _start[n];
}

template <class T>
const T& myVector::vector<T>::operator[](size_t n) const
{
	assert(n < size()); 
	return _start[n];
}

你可能感兴趣的:(数据结构与算法,c++,算法,数据结构)