初识模版(C++)

初识模版(C++)

在这里插入图片描述

模版是C++的一个重大发明,是让C++突飞猛进的原因之一。

  1. 泛型编程

实现一个通用的交换函数?

void Swap(int& left, int& right)
{
    
    
	int temp = left;
	left = right;
	right = temp;
}

void Swap(double& left, double& right)
{
    
    
	double temp = left;
	left = right;
	right = temp;
}

void Swap(char& left, char& right)
{
    
    
	char temp = left;
	left = right;
	right = temp;
}

int main()
{
    
    

	return 0;
}

可以看到以前我们要交换不同数据类型的变量时,需要调用各自不同类型的交换函数,虽然可以重载,但还是写了很多个Swap。

我们很难不想将它们合为一个通用的Swap,对吧?

就像活字印刷的模具,我们能否有一个通用的Swap函数,别人想要交换什么类型,就自动生成为那个类型的Swap函数呢?

//模版类型
template<class T>
void Swap(T& x, T& y)//以前这里是写具体类型,现在写的是模版类型
{
    
    
	T tmp = x;
	x = y;
	y = tmp;
}

int main()
{
    
    
	int i = 1, j = 2;
	double m = 1.1, n = 2.2;
	Swap(i, j);
	Swap(m, n);

	return 0;
}

其实就是让编译器去具体写我们想要类型的函数。

而且当我们用调试时观察可以发现,调用Swap时确实走到void Swap(T& x, T& y)里面去了。

然而事实上调用的不是同一个函数。IDE做了特殊处理,这样是为了方便调试看。

如果我们再深究一下,去看反汇编,就会发现调用的确实不是同一个函数:

这两个函数我们是没有写的,所以是编译器帮助我们生成的。模版的特点就是让C++在有些地方能够半自动化。

生成具体类型函数这个活是免不了的,只是由我们做变成编译器去做了。

  1. 函数模板

模版有两类,函数模版和类模版,我们刚才写的就叫做函数模版。

函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本。

函数模板格式

template<typename T1, typename T2,…,typename Tn>

返回值类型 函数名(参数列表){}

这里的typename关键字是后期增加的,也可以用class。typename,类型名称,会更形象一点。

模版参数列表可以类比着函数参数列表去学习,它们的规则是类似的。

函数参数列表是()圆括号,模版参数列表是<>尖括号。

函数的参数列表定义的是变量或者说对象,(C++不是纯面向对象的因为C++兼容C语言,也可以走面向过程这一套。C++中变量喜欢叫对象,其实像C一样叫变量也没什么问题。)

(类型 变量1,类型 变量2)

而模版的参数列表定义的是关键字

<class 类型1,class 类型2>

class后面不是变量名称了,而是类型。

多个类型:

template<typename T1, typename T2>
void func(const T1& x,const T2& y)
{
    
    

}

int main()
{
    
    
	int i = 1, j = 2;
	double m = 1.1, n = 2.2;

	func(i, m);
	func(i, j);

	return 0;
}

要注意的一点是,在前一个:

template<class T>
void Swap(T& x, T& y)

单类型的函数中,这个T到底是什么类型是由我们传递的实参推理决定的,所以如果我们在实参中传递了两个不同类型的值就会产生歧义,不知道该生成哪个类型的Swap函数:

template<class T>
void Swap(T& x, T& y)
{
    
    
    T tmp = x;
    x = y;
    y = tmp;	
}

int main()
{
    
    
	int i = 1, j = 2;
	double m = 1.1, n = 2.2;
	Swap(i,m);//这是不行的

	return 0;
}

从此以后,我们就可以告辞手写Swap的时代了。C++11下这个函数在头文件中,一般会被简介包含从,所以不需要我们自己包含这个头文件。

是小写字母s开头。

int main()
{
    
    
	int i = 1, j = 2;
	double m = 1.1, n = 2.2;
	
	swap(i, j);
	swap(m,n);

	return 0;
}

以后直接使用即可。

现在我们怎么解决想要传两个不同类型的参数的问题?

第一种办法,强制类型转换

int main()
{
    
    
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;

	cout<<Add(a1, (int)d1)<<endl;
	cout<<Add((double)a1, d1)<<endl;
}

本质解决的是推导冲突的问题。

还有一种方法:显式实例化

//用函数模版生成对应的函数——模版的实例化
template<class T>
T Add(const T& left, const T& right)
{
    
    
	return left + right;
}

int main()
{
    
    
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;

    //推导实例化
	cout<<Add(a1, (int)d1)<<endl;
	cout<<Add((double)a1, d1)<<endl;
    
    //显式实例化
	cout << Add<int>(a1, d1) << endl;
}

在函数名和参数直接加尖括号写类型,不通过推导而是通过显式实例化。这样我们的d1可以传给生成好的int类型Swap是因为可以走隐式类型转换。

当然也可以选择写一个两个类型的Swap。

有些情况下,必须走显式实例化。不能走自动推导:

template<class T>
T* func1(int n)
{
    
    
	return new T[n];
}

int main()
{
    
    
	func1(10);

	return 0;
}

这样的形参无法推导出T是什么类型。

所以必须显式实例化。double* p1= func1<double>(10);

如果一个模版和一个具体类型的函数同时存在,会优先走对应类型的函数。

int a1=10,a2=20;
cout<<Add(a1,a2)<<endl;

会优先去调用第二个类型直接匹配的Add函数。

  1. 类模板

类模板的定义格式
template<class T1, class T2, ..., class Tn> 
class 类模板名
{
    
    
 // 类内成员定义

}; 

这里的class也能用typename替代。

// 类模版
template<typename T>
class Stack
{
    
    
public:
	Stack(int n = 4)
		:_array(new T[n])//不用检查失败,不会返回空
		,_size(0)
		,_capacity(n)
	{
    
    }
	~Stack()
	{
    
    
		delete[] _array;
		_array = nullptr;
		_size = _capacity = 0;
	}
	void Push(const T& data)//用传值传参要拷贝,代价太大,加了引用尽量加 const
	{
    
    
		if (_size == _capacity)//满了
		{
    
    
			//没有renew这种东西,也不能用realloc
			T* tmp = new T[_capacity * 2];
			memcpy(tmp, _array, sizeof(T) * _size);
			delete[] _array;

			_array = tmp;
			_capacity *= 2;
		}

		_array[_size++] = x;
	}
	//……

private:
	T* _array;
	size_t _capacity;
	size_t _size;
};

记得我们以前使用typedef来写栈的,也可以修改栈存放的数据类型,但是为什么现在要用类模版来实现呢?有什么区别呢?

int main()
{
    
    
	Stack st1;//int
	Stack st2;//double

	return 0;
}

用typedef,如果我们想要一个栈存放int类型,一个存放double类型,这是做不到的。得写两份一个存int,一个存double。

同时我们也可以发现,类模板无法推导实例化,只能显式实例化,只有函数才有推导实例化。

int main()
{
    
    
	Stack<int> st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	
	Stack<double> st2;
	st2.Push(1.1);
	st2.Push(1.1);
	st2.Push(1.1);

	return 0;
}

从此我们的栈可以不止存一类数据了。

这两个栈其实不是同一个类型,和之前的函数模版一样,也是“活字印刷”。本质上生成了两个类型的栈类型,只是编译器做了这件事。

如果我们现在想要将类成员函数的声明与定义分离呢?
像以前一样指定类域,不行。

我们定义的模版参数只能给当前的函数或类去用,每个函数模版都得定义自己的模版参数,不一定叫T。

所以出了类,T就不能用了。得在分离出来的定义前面重新定义模版参数:

//类外
template<class T>
void Stack<T>::Push(const T& data)//用传值传参要拷贝,代价太大,加了引用尽量加 const
{
    
    
	if (_size == _capacity)//满了
	{
    
    
		//没有renew这种东西,也不能用realloc
		T* tmp = new T[_capacity * 2];
		memcpy(tmp, _array, sizeof(T) * _size);
		delete[] _array;

		_array = tmp;
		_capacity *= 2;
	}

	_array[_size++] = x;
}

其次要注意的是,模版不支持声明和定义分离到两个文件。会报链接错误。以后再说原因。也有办法分离但是非常麻烦,一般模版放在一个文件。

如果我们将函数定义里的T换为X,不会报错:

为什么?因为这个本来就只是个代号,其实实际在调用Push这个函数时就已经没有这个T或者X的概念了,而是实例化为对应的类型了。调用的也不是模版的这个函数,而是生成后的函数。

这些只是模版的初阶用法,以后再进阶。

本文到此结束=_=

猜你喜欢

转载自blog.csdn.net/2301_82135086/article/details/141469278