模板与vector的学习

目录

1.模板

1.1函数模板

1.2类模板

2.vector

2.1vector的使用

2.1.1vector的定义

​编辑

2.1.2vector迭代器的使用

2.1.3vector的空间管理

​编辑

2.1.4vector的增删查改操作

2.2vector迭代器失效

3.vector的模拟实现

3.1构造函数和析构函数

3.2头尾迭代器

3.3空间管理函数

3.4增删查改函数

3.5补充


1.模板

在之前学习函数重载时,我们知道,形参的类型不同可以构成重载,但是如果我们对该函数有很多种输入的要求,例如整型,字符类型,浮点数类型的变量,写交换函数时就需要写三个函数,但是模板可以进行一个泛型编程的操作,把识别类型交给编译器来做

1.1函数模板

如下三个交换函数构成函数重载,很明显这样写很冗余

cpp 复制代码
void swap(int& x, int& y) {
	int tmp = x;
	x = y;
	y = tmp;
}

void swap(double& x, double& y) {
	double tmp = x;
	x = y;
	y = tmp;
}

void swap(char& x, char& y) {
	char tmp = x;
	x = y;
	y = tmp;
}

所以我们就可以使用模板来写函数,模板的写法如下,T1,T2等就是虚拟变量,在使用时由编译器自动匹配类型,class也可以用typename来写,注意模板中也可以存在真实的类型,不全是虚拟变量,例如可以在模板中写size_t n,int x

template<class T1,class T2.....>

代码如下,这就是一个简易的模板函数,当我们需要对某个类型的两个变量进行交换时,只需要将变量直接放入函数中即可,编译器会自动识别类型

cpp 复制代码
template<class T>
void swap(T& x, T& y) {
	T tmp = x;
	x = y;
	y = tmp;
}

注意在这个swap函数中,如果传入的x和y不是一个类型的,就会产生报错,因为只有一个T,无法匹配两个类型,编译器就不能确定T的类型导致编译报错

这就引出模板的隐式实例化和显式实例化 ,例如一个加法函数,只用了一个虚拟变量T,那么如果传入的参数类型不同就会报错,所以第二个输出add(x,z)会显示报错,但是第三个输出就不会,这就是显式实例化,直接规定T的类型,传入参数时会强行转换成对应类型进行操作,不加<int>就是隐式实例化,类型交给编译器自己识别

注意一个点,在形参部分要加上const,因为进行类型强转时,会产生一个临时对象,这个临时对象具有常性,如果不加const就造成了权限的放大

cpp 复制代码
template<class T>
T add(const T& x, const T& y) {
	return x + y;
}

int main(){

	int x = 10;
	int y = 20;
	double z = 1.1;
	cout << add(x, y) << endl;
	cout << add(x, z) << endl;
	cout << add<int>(x, z) << endl;


	return 0;
}

如果既有匹配的普通函数,也可以用模板函数,会优先调用普通函数,因为现成的函数不需要编译器去识别类型,更加方便

cpp 复制代码
template<class T>
T add(const T& x, const T& y) {
	cout << "调用了模板函数" << endl;
	return x + y;
}
int add(const int& x, const int& y) {
	cout << "调用了普通函数" << endl;
	return x + y + 10;
}

int main(){

	int x = 10;
	int y = 20;
	cout << add(x, y) << endl;
	//输出40

	return 0;
}

1.2类模板

和函数模板的使用没有很大区别,例如一个栈,我们之前如果想改变内部存储的数据类型,直接在typedef的位置修改类型即可,但是如果要创建好几个存储不同数据类型的栈,就需要使用到模板,直接进行显式实例化即可,具体的使用可以根据下文讲解vector的时候继续理解

2.vector

vector和之前学的string一样,都是一种存储数据的容器 ,在使用上和string有相似之处,所以学过string之后继续学vector就觉得比较轻松,而vector其实就是顺序表,只不过它可以使用很多函数直接进行操作,比自己创建的数组顺序表要方便很多

2.1vector的使用

2.1.1vector的定义

vector在初始化时常见的有几种方式

1.无参构造

2.通过n个相同类型的val来构造,注意val不一定是内置类型,也可以是其他类或者嵌套vector

3.通过一段迭代器区间,将这一段的数据依次放入vector

4.拷贝构造,直接拷贝现成的vector

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

int main(){

	vector<int> v1;//无参构造
	vector<int> v2(5, 1);//通过n个val来构造,(size_t n,const T& val=T())
	int arr[5] = { 1,2,3,4,5 };
	vector<int> v3(arr, arr + 4);//通过一段迭代器区间
	vector<int> v4(v3);//拷贝构造

	for (auto e : v2) {
		cout << e;
	}
	cout << endl;
	for (auto e : v3) {
		cout << e;
	}
	cout << endl;
	for (auto e : v4) {
		cout << e;
	}
	

	return 0;
}

2.1.2vector迭代器的使用

和string一样,有begin和end函数用于指示开头和末尾

例如用两个迭代器分别正向逆向遍历vector,注意反向迭代器名字要加上reverse,并且++之后是朝着开头位置走的,而不是往末尾

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

int main(){

	vector<int> v1 = { 1,2,3,4,5,6,7 };

	vector<int>::iterator it = v1.begin();
	vector<int>::reverse_iterator it1 = v1.rbegin();
	while (it != v1.end()) {
		cout << *it;
		it++;
	}
	cout << endl;
	while (it1 != v1.rend()) {
		cout << *it1;
		it1++;
	}
	cout << endl;

	

	return 0;
}

2.1.3vector的空间管理

可以使用size函数获取元素个数capacity函数获取空间大小

resize可以改变元素的个数 ,如果传入的参数小于原来的元素个数,那么就会直接删除多余的元素,如果大于原来元素个数,在比空间还大的情况下会进行扩容的操作,大于元素个数小于空间个数,那么只会填充vector,使其元素个数达到目标

reverse函数可以为vector预留空间,指定vector的空间大小

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

int main(){

	vector<int> v1 = { 1,2,3,4,5,6,7 };

	cout << v1.size() << endl;
	cout << v1.capacity() << endl;
	v1.resize(5);
	v1.reserve(10);
	cout << v1.size() << endl;
	cout << v1.capacity() << endl;

	return 0;
}

2.1.4vector的增删查改操作

push_back尾插元素,pop_back尾删元素

insert和erase用于指定位置插入和删除元素 ,注意传入的参数要是迭代器类型,使用迭代器表示位置而不是下标

swap函数可以直接交换两个vector容器中的数据

find函数不是vector的成员接口 ,但是用于查找对应元素下标也可以使用,但是要注意加上头文件**<algorithm>**才能调用算法库的find,返回值是一个迭代器,减去开头的迭代器就可以获取下标

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main() {

	vector<int> v1 = { 1,2,3,4,5,6,7 };
	vector<int> v2 = { 10,9,8,7,6,5,4 };

	cout << "交换前的v1:";
	for (auto e : v1) {
		cout << e << " ";
	}
	cout << endl;
	cout << "交换前的v2:";
	for (auto e : v2) {
		cout << e << " ";
	}
	cout << endl;

	swap(v1, v2);
	cout << "交换后的v1:";
	for (auto e : v1) {
		cout << e << " ";
	}
	cout << endl;
	cout << "交换后的v2:";
	for (auto e : v2) {
		cout << e << " ";
	}
	cout << endl;

	cout << v1[1] << " " << v1[2] << endl;
	v1.push_back(9);
	v1.push_back(10);
	v1.insert(v1.begin() + 1, 20);

	for (auto e : v1) {
		cout << e << " ";
	}
	cout << endl;
	v1.pop_back();
	v1.pop_back();
	v1.erase(v1.begin() + 4);

	for (auto e : v1) {
		cout << e << " ";
	}
	cout << endl;
	auto f = find(v1.begin(), v1.end(), 7);
	cout << f - v1.begin() << endl;

	

	return 0;

}
}

2.2vector迭代器失效

对于改变底层空间的操作可能会引起迭代器失效

1.插入元素时,如果空间不够,那么此时会进行扩容,扩容完的空间和原来不是一个空间,而迭代器还留在旧空间的位置,所以此时迭代器失效

2.删除元素时,删除某个位置的元素后,后面所有的元素均往前移动,此时没有造成空间的改变,但是,如果此时删除的是最后一个元素,此时就会获取到一个空的位置,再进行解引用就会产生报错,而编译器就认为这样的操作是不安全的,为了防止对空指针进行解引用,所以此时的迭代器就失效了,判定它不可继续使用

所以对于下面的代码,当vector进行删除操作时,该位置及其之后的迭代器就会失效

当进行insert插入操作时,如果没有进行扩容迭代器不失效,但是扩容的时候就会失效

cpp 复制代码
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main() {

	vector<int> v1 = { 1,2,3,4,5,6,7 };
	vector<int>::iterator it = v1.begin();
	while (it != v1.end()) {

		v1.erase(it);
		it++;

	}
	vector<int>::iterator it1 = v1.begin();
	while (it1 != v1.end()) {
		if (*it1 == 5) {
			v1.insert(it1, 8);
		}
		it1++;
	}

	

	return 0;

}

为了避免迭代器失效,我们只需要对迭代器重新赋值即可,因为insert和erase函数都会返回下一个位置的迭代器,所以重新获取一个有效的迭代器就行

3.vector的模拟实现

先构造一个命名空间,防止和系统库的vector发生冲突

写出vector的基本结构,因为传入的数据类型比较复杂,所以用迭代器去封装指针,然后让编译器自动识别传入数据的类型即可

所以成员变量有三个,开始位置的迭代器,元素末尾的迭代器以及空间迭代器

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
#include<stdbool.h>
namespace Vec {

	template<class T>
	class vector {
		typedef T* iterator;
		typedef const T* const_iterator;
	public:
        //成员函数

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
		//给上缺省值,避免初始化的问题
	};


}

3.1构造函数和析构函数

无参构造默认给三个迭代器赋值为nullptr,如果不写可以用vector()=default的方式让编译器声明默认构造

对于拷贝构造 ,我们先使用reverse函数为容器开辟相同大小的空间 ,然后使用范围for遍历需要拷贝的vector,依次尾插元素即可,但是之前模拟实现string的时候使用memcpy进行拷贝,为什么这里缺不使用,因为memcpy如果拷贝字符串是浅拷贝,如果内部数据是string类型,那么拷贝的时候只是指向了同一块空间而不是额外开空间,所以这里使用遍历尾插的方式拷贝

第三种方法,新创建一个模板用于兼容任意容器的迭代器,只要传入对应的迭代器,然后让迭代器一边往后走一边将遍历到的元素尾插进vector

析构函数比较简单,如果_start不为空指针,那么就删除该空间,并将三个迭代器置为空

cpp 复制代码
vector()//构造函数
	: _start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{}

vector() = default;
//强制编译器生成默认构造

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

}
template<class InputIterator>
//兼容任意容器的迭代器
vector(InputIterator start, InputIterator finish) {
	while (start != finish) {
		push_back(*start);
		start++;
	}
}

~vector() {
	if (_start) {
		delete[] _start;
		_start = _finish = _endstorage = nullptr;
	}
	//指针不为空才需要释放空间
}

3.2头尾迭代器

写法很简单,只要返回成员变量中的_start和_finish即可,分为普通迭代器和const迭代器,const迭代器指向的内容不可更改

cpp 复制代码
	iterator begin() {
		return _start;
	}

	iterator end() {
		return _finish;
	}

	const_iterator begin() const{
		return _start;
	}

	const_iterator end() const {
		return _finish;
	}

3.3空间管理函数

因为迭代器底层使用了指针,所以可以通过指针的相减返回空间大小和元素个数

reserve函数 ,当传入的n大于当前空间的时候,会进行扩容的操作,此处使用2倍扩容,如果_start不为空那么要先进行赋值,然后释放原空间,注意原空间的元素个数要提前存储,不然后续使用size函数给_finish赋值就会使用新的_start而不是原来的,会产生报错

resize函数 需要用元素填充vector,这里使用T()是为了通用性,如果是内置类型不做特殊处理,如果是其他类,那么会调用其默认构造函数进行初始化,如果需要开空间,可以复用reserve函数,如果不需要开空间,那么直接移动末尾指针_finish即可

empty函数用于判断vector是否为空,那么只要判断size是否为0

cpp 复制代码
	size_t capacity() {
		return _endofstorage - _start;
	}
	size_t size() {
		return _finish - _start;
	}

	void reserve(size_t n) {
		//对空间大小进行调整
		//一般只进行扩容不进行缩容
		//所以传入的n如果比较小是不会进行操作的 
		if (n > capacity()) {
			T* tmp = new T[n];
			size_t old_size = size();//先存储旧空间的元素个数
			if(_start){

				for (size_t i = 0; i < old_size; i++) {
					tmp[i] = _start[i];//依次赋值
				}
				delete[] _start;
				//如果原来有内存空间
				//先进行释放
			}
			_start = tmp;
			_finish = _start + old_size;
			//如果此处不使用old_size而是size()
			//size()中的_start用的是更新后的,_finish还是之前的
			_endofstorage = _start + n;
		}

	}
	void resize(size_t n, T val = T()) {
		//对元素个数进行调整
		
		//如果小于原来元素个数,直接把后续元素删除
		//如果大于原来元素个数,先使用reserve保证有足够空间
		//然后从原末尾开始依次插入val,直到扩充到新的元素个数
		if (n > size()) {
			reserve(n);
			while (_finish != _start + n) {
				*_finish = val;
				_finish++;
			}

		}
		else {
			_finish = _start + n;
		}

	}


    bool empty() {
		return size() == 0;
	}

3.4增删查改函数

push_back尾插函数,如果空间不足那么需要进行扩容,然后在末尾处加上新元素并移动_finish

pop_back尾删函数,要先判断是否存在元素可以删除,然后移动_finish

swap函数,直接调用库里的swap函数,对三个成员变量进行交换

clear函数 ,清除元素,直接令_finish=_start即可,注意并不是把空间一起清除了

insert插入函数,使用迭代器指示位置,同样先判断是否需要扩容,然后从末尾依次移动元素,给指定位置腾出一个空间,然后插入指定元素,并移动_finish

erase函数 ,判断迭代器位置是否在_start和_finish之间,否则视为非法操作,从指定位置开始依次往前赋值,覆盖指定位置的元素,然后将_finish--

cpp 复制代码
void push_back(T& x) {
	if (_finish == _endofstorage) {
		size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
		reserve(newcapacity);
	}
	*finish = x;
	++finish;
}
void pop_back() {
	assert(size() > 0);
	--_finish;
}

void swap(vector<T>& v) {
	//引用减少拷贝
	//但是为了不修改被拷贝的vector
	//调用该函数时,先将被拷贝的vector拷贝一份
	//再进行使用,就可以达到赋值的效果
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofdstorage);
}


void clear() {

	_finish = _start;

}

iterator insert(iterator pos, const T& x) {
	assert(pos < _finish);
	assert(pos >= _start);
	if (_finish == _endofstorage) {
		size_t newcapacity = (capacity() == 0) ? 4 : 2 * capacity();
		reserve(newcapacity);
	}
	iterator end = _finish + 1;
	while (end != pos) {
		*end = *(end - 1);
		end--;
	}
	*pos = x;
	_finish += 1;
	return pos;

}
iterator erase(iterator pos) {
	assert(pos < _finish);
	assert(pos >= _start);
	while (pos != _finish) {
		*pos = *(pos + 1);
		pos++;
	}
	--_finish;
	return pos;

}

3.5补充

写完swap函数后,对于赋值重载就可以写的很简单,使用传值调用传入的vector,所以形参不会改变实参,所以直接将形参传入swap中,然后交换当前类和v的成员变量

还有对于[ ]的两种重载,因为底层使用指针,所以直接用_start[pos]即可访问对应数据

cpp 复制代码
vector<T>& operator=(vector<T> v) {
	//因为形参v的成员要发生改变,不加const
	swap(v);
	return *this;
}

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

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

}
相关推荐
ambition202422 小时前
【算法详解】飞机降落问题:DFS剪枝解决调度问题
c语言·数据结构·c++·算法·深度优先·图搜索算法
EnglishJun2 小时前
ARM嵌入式学习(十八)--- Linux的内核编译和启动
linux·运维·学习
I Promise342 小时前
C++ 基础数据结构与 STL 容器详解
开发语言·数据结构·c++
星幻元宇VR2 小时前
VR旋转蛋椅:沉浸式安全科普新体验
科技·学习·安全·vr·虚拟现实
ZhiqianXia2 小时前
PyTorch 学习笔记(12):ATen C++ 算子引擎的完整架构之旅
pytorch·笔记·学习
旖-旎2 小时前
链表(两两交换链表中的节点)(2)
数据结构·c++·学习·算法·链表·力控
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB的分片管理(17)
数据库·学习·mongodb
世人万千丶2 小时前
Flutter 框架跨平台鸿蒙开发 - 嫉妒分析器应用
学习·flutter·华为·开源·harmonyos·鸿蒙