【C++】6:模板初阶

目录

一、泛型编程和模板

二、函数模板

2.1 函数模板的概念

2.2 函数模板的格式

2.3 函数模板的原理

2.4 函数模板的实例化

2.4.1 隐式实例化

2.4.2 显式实例化

2.5 函数模板的匹配原则

三、类模板

3.1 函数模板的格式

3.2 函数模板的实例化


一、泛型编程和模板

在学习泛型编程之前,我们先要来写一个交换int类型的函数,如下所示:

void Swap(int& x, int& y) {
	int tmp = x;
	x = y;
	y = tmp;
}

这个函数只能交换int类型的数据,如果我们想要交换double和char类型的数据呢,就需要使用函数重载来在写两个Swap函数,如下所示:

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;
}

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

C++引入了模板的概念,就像在模具中浇筑一样,我们只需要浇筑不同的材料液,就能到由对应材料的物体。C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(数据类型),来获得不同材料的铸件(即生产具体类型的代码)。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

模板又分为函数模板和类模板。

二、函数模板

2.1 函数模板的概念

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

2.2 函数模板的格式

template<typename T1, typename T2,......,typename Tn>
返回值类型  函数名 ( 参数列表 )
{
        //函数体
}

例如,我们就可以写一个模板来完成上面的交换函数,如下所示:

template<typename T>
void Swap(T& a, T& b) {
	T tmp = a;
	a = b;
	b = tmp;
}

template是C++泛型编程的核心关键字,用于定义与数据类型无关的代码模板,这里的语法是固定的格式,template<typename T1, typename T2,......,typename Tn> typename的意思就是类型名,表示这里的T既可以是int ,也可以是double ,还可以是自定义类型等等

注意:typename是用来定义模板参数关键字也可以使用class

我们使用上述的模板来交换int,double和char类型的数据,代码如下所示:

#include<iostream>
using namespace std;
template<typename T>
void Swap(T& a, T& b) {
	T tmp = a;
	a = b;
	b = tmp;
}
int main() {
	int i1 = 10, i2 = 20;
	double d1 = 2.1, d2 = 5.1;
	char c1 = '0', c2 = '9';
	cout << "交换前:" << endl;
	cout << "i1=" << i1 << " i2=" << i2 << endl;
	cout << "d1=" << d1 << " d2=" << d2 << endl; 
	cout << "c1=" << c1 << " c2=" << c2 << endl;
	Swap(i1, i2);
	Swap(d1, d2);
	Swap(c1, c2);
	cout << "交换后:" << endl;
	cout << "i1=" << i1 << " i2=" << i2 << endl;
	cout << "d1=" << d1 << " d2=" << d2 << endl;
	cout << "c1=" << c1 << " c2=" << c2 << endl;
	return 0;
}

2.3 函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。如下所示:

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

2.4 函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

2.4.1 隐式实例化

让编译器根据实参推演模板参数的实际类型.

例如,我们执行以下代码:

#include<iostream>
using namespace std;
template<typename T>
void Swap(T& x, T& y) {
	T tmp = x;
	x = y;
	y = tmp;
}
int main() {
	int i1 = 10, i2 = 20;
	double d1 = 2.1, d2 = 5.1;
	cout << "交换前:" << endl;
	cout << "i1=" << i1 << " i2=" << i2 << endl;
	cout << "d1=" << d1 << " d2=" << d2 << endl;
	Swap(i1, i2);
	Swap(d1, d2);
	cout << "交换后:" << endl;
	cout << "i1=" << i1 << " i2=" << i2 << endl;
	cout << "d1=" << d1 << " d2=" << d2 << endl;
	return 0;
}

当代码执行到Swap(i1,i2)时,编译器会推演T的类型,把实参i1和i2传给形参x和y时,通过实参i1和i2会将T推演成int类型,然后实例化一个T为int类型的函数。同理当代码执行到Swap(d1,d2)时,会通过实参d1和d2将T推演成double类型,然后实例化一个T为double类型的函数。在调试的时候,执行Swap虽然都会跳到上面的Swap函数,但是我们心里要知道,这两段代码执行的不是同一段代码。

注意,请看如下代码:

template<class T>
T Add(const T& a, const T& b) {
	return a + b;
}
int main() {
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0; Add(a1, a2);
	Add(a1, d1);
	return 0;
}

这样的代码不会通过编译。因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int或者double类型而报错

注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

此时有两种解决方法:

1.自己来强制转换,如Add(i1,(int)d1);

2.使用显示实例化,

2.4.2 显式实例化

在函数名后的<>中指定模板参数的实际类型

接上面错误的例子,我们可以在写代码的时候显示的指定模板参数的实际类型,如下所示:

#include<iostream>
using namespace std;
template<class T>
T Add(const T& a, const T& b) {
	return a + b;
}
int main() {
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0; Add(a1, a2);
	Add<int>(a1, d1);
	return 0;
}

当我们显示指定模板参数的实际类型时,编译器会生成一个模板参数是int类型的函数,当我们传入double类型时,会自动转换为int类型。

注意,既然模板可以根据实参类型可以推演出T的类型,那我们是不是就不需要显式实例化了呢?这个说法显然是错误的,有时候执行函数的时候是没有参数的,如果没有参数就不能推演出T的类型了,这种时候就需要显式实例化了。代码如下所示:

#include<iostream>
using namespace std;
template<class T>
T* func(int n) {
	return new T[n];
}
int main() {
	int* p1=func(10);
    return 0;
}

这种情况就不能隐示实例化了,就需要显式实例化。如下所示:

#include<iostream>
using namespace std;
template<class T>
T* func(int n) {
	return new T[n];
}
int main() {
	int* p1=func<int>(10);
	return 0;
}

2.5 函数模板的匹配原则

如果调用函数的时候既可以走非模板函数,又可以走模板函数,那应该怎么走呢?

1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

对于上述的这种情况,调用函数的时候会走非模板函数,代码如下所示:

#include<iostream>
using namespace std;
//专门用来处理int类型的加法
int Add(int a, int b) {
	return a + b;
}
//通用加法函数
template<class T1,class T2>
T1 Add( T1 a,  T2 b) {
	return a + b;
}
int main() {
	Add(1, 2);
	return 0;
}

Add(1,2)与非模板函数完全一致,因此不需要模板函数实例化。

2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

代码如下所示:

#include<iostream>
using namespace std;
//专门用来处理int类型的加法
int Add(int a, int b) {
	return a + b;
}
//通用加法函数
template<class T1, class T2>
T1 Add(T1 a, T2 b) {
	return a + b;
}
int main() {
	Add(1, 2.0);
	return 0;
}
模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
模板函数和普通函数有一个非常大的区别:模板函数不允许自动类型转化,普通函数可以进行自动类型转化。

三、类模板

3.1 函数模板的格式

template<classT1,classT2,...,classTn>
class 类模板名
{
        //类内成员定义
}

我们可以自己写一个Stack的类模板,实现栈的部分功能,如下所示:

#include<iostream>
using namespace std;
template<class T>
class Stack {
public:
	Stack(int n = 4)
	{
		_array = new T[n];
		_capacity = 4;
		_top = 0;
	}
	void Push(const T& x) {
		if (_top == _capacity) {
			T* tmp = new T[2 * _capacity];
			memcpy(tmp, _array, _capacity*sizeof(T));
			delete[] _array;
			_array = tmp;
			_capacity = 2 * _capacity;
		}
		_array[_top] = x;
		_top++;
	}
private:
	T* _array;
	int _capacity;
	int _top;
};
int main() {
	Stack<int> st1;
	Stack<double> st2;
	return 0;
}

3.2 函数模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的

类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

//Stack 是类名, Stack<int> 才是类型
Stack < int > st1 ;  
Stack < double > st2 ;

注意,我们之前写Stack的时候,会把想要存入栈里面的数据使用类型重定义typedef int DataType。有的同学会感觉typedef和template参不多,这两个是有非常大的区别的,我们使用typedef之后,这个栈里面就只能存int类型的数据了,如果我们想要存入double类型和自定义类型的数据就需要把所有栈的代码拷贝两次,然后修改类型重定义。如果我们使用模板,就不用自己拷贝了,编译器会根据我们显式传入的类型创建一个该类型的栈。Stack是类名,Stack<int>才是类型

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值