本篇文章主要面向C++初学者,所介绍内容包括模板由来,函数模板及类模板的使用方法与基本原理相关,属于模板的初阶认识,不涉及模板特化,分离编译等问题。
下面开始介绍。
我们在日常编程时常常会碰到这样一种情况:
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()
{
int i1 = 2;
int i2 = 3;
Swap(i1, i2);
double d1 = 2.3;
double d2 = 3.4;
Swap(d1, d2);
char c1 = 'A';
char c2 = 'B';
Swap(c1, c2);
return 0;
}
现在我要实现一个交换函数,但是为了满足不同类型变量的交换需求,我必须重载出多个交换函数。
使用函数重载虽然可以实现功能,但也有一些不好的地方:
那么有没有一种好的解决方法可以实现一个通用的交换函数?
基于此类情况,于是呼用户就产生了一种需求:我想只负责告诉编译器一个交换模子,让编译器根据不同的类型利用该模子来生成代码。
怎么理解这个模子呢?
一般在进行浇筑的时候,工人通常会根据同一个模板来浇筑出不同的颜色出来。
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(生成具体类型的代码),这样不就可以很大程度上节省效率了。基于这个目的我们的先辈们创建出了泛型编程。
泛型编程:
编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
泛型编程模板主要有两大类,
函数模板和类模板。
下面我们先来看函数模板如何使用。
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
概念的理解比较抽象,不过我想通过下面的用法大家一定可以对模板有一个很好的理解
template
返回值类型 函数名(参数列表){}
template是定义模板的关键字
typename也是一个关键字,定义参数类型
一开始我们先来看看模板是如何来使用的。
template<typename T> //交换函数模板
void Swap(T& n1, T& n2)
{
T tmp = n1;
n1 = n2;
n2 = tmp;
}
int main()
{
int i1 = 2;
int i2 = 3;
double d1 = 2.3;
double d2 = 3.4;
char c1 = 'A';
char c2 = 'B';
//调用模板是相同类型的交换
Swap(d1, d2);
Swap(i1, i2);
Swap(c1, c2);
return 0;
}
上述代码实现了一个定义Swap函数模板的过程,有了模板就可以交换任意类型的函数了。
交换前:
交换后:
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
看完函数模板的使用之后,下面我们再来看函数模板的实现原理究竟是怎样的,为什么这里的一个函数就能起到多个函数的效果?
有这样一句话不知道大家有没有听说过,“懒人创造了这个世界”。
工业革命之后,蒸汽机、火车、纺纱机这些机器产品逐渐淘汰掉了原先的手工产品,使得人类的生活更加便捷。我们有没有思考过,这些工业产品的本质是什么?其本质就是把重复的工作交给了机器去完成!
函数模板的道理同样如此。 函数模板是一个蓝图,它本身并不是函数,是编译器产生特定具体类型函数的模具。所以模板实质上是将本来应该我们做的重复的事情交给了编译器去帮助实现。
下图为编译器底层实现的模拟示意图:
表面上我们只看到了一个函数模板,实际在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。 比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
这个推演的过程我们一般将其称之为 函数模板的实例化
。
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:
隐式实例化
和显式实例化
。
也就是说 交换函数实际在底层调用的并不是模板,而是编译器根据不同类型的参数通过模板实例化出来的实际函数。
为了验证上面的说法,接下来我们可以通过查看汇编代码,观察不同类型参数模板的底层调用。
我们看到,模板的底层实际上还是调用重载出来的交换函数,只不过这个生成重载函数的过程并不需要人去手动实现了,编译器会帮助我们自动在底层实现。
接下来我们在来看实例化的两种类型:隐式实例化和显示实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型
上面所举的交换函数模板的实例化过程就属于隐式实例化过程
来看这样一段代码:
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int i = 10;
double d = 3.14;
Add(i, d); //模板函数参数类型不同
return 0;
}
前面我们调用交换函数的参数类型都是相同的,现在我问对于本段Add函数的模板,我们可不可以进行两个 不同类型变量 的相加?
来看运行结果:
我们看到编译不同,提示没有与之匹配的函数模板。这是因为此处会 出现歧义,编译器在对参数进行隐式实例化的时候难以推导T的类型到底是int还是double。
如何解决利用模板对一个整型变量和浮点型变量相加的问题?
方法1:用户自己来强制类型转换
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int i = 10;
double d = 3.14;
Add(i, (int)d); // <==== 将d强制类型转换成int类型
return 0;
}
方法2:使用模板的显示实例化。
显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int i = 10;
double d = 3.14;
Add<int>(i, d); // <<==== 显示实例化,让模板以int类型去推演
return 0;
}
本段代码中Add模板的实例化过程就是显示实例化,在函数名Add后加的<>中指定模板参数的实际类型为int,让编译器在对参数进行推演的时候默认以int类型去推演。
这个时候如果有的类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
1.一个非模板函数可以和一个同名的函数模板同时存在,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
return 0;
}
2.如果调用时采用模板的显示实例化,即使存在条件相同的同名非模板函数,还是会去调用模板产生一个显示实例化函数。
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要实例化
Add<int>(1, 2); // 调用编译器显示实例化的Add版本
return 0;
}
//栈类简易实现
typedef int Elemtype;
class Stack
{
public:
Stack(int capacity = 4)
:_a(new Elemtype[4])
, _size(0)
, _capacity(4)
{}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _size = 0;
}
private:
Elemtype* _a;
int _size;
int _capacity;
};
本段代码是一个栈类的简易实现,我们看到这个栈内的元素类型被定义为int,因此我可以定义出一个栈元素全为int的对象。
如果我想定义一个栈元素全为double的对象怎么办?很简单,只需要将typedef int改为typedef double即可。
但现在我有这样一个需求:
int main()
{
Stack st1; // <==== int
Stack st2; // <==== double
return 0;
}
我想在一个函数中定义两个栈的对象,并且这两个栈内的元素一个是int,一个是double,怎么办?
上面我们刚学过函数的模板,同样的,对于类同样有类的模板可以供我们使用,使用类模板就可以帮我们完美解决这个问题。
我们来看类模板的定义:
template<class T> //栈类模板
class Stack
{
public:
Stack(int capacity = 4)
:_a(new T[4])
, _size(0)
, _capacity(4)
{}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _size = 0;
}
private:
T* _a;
int _size;
int _capacity;
};
可以看到类模板的定义和函数模板是相同的。
类模板实例化与函数模板实例化不同,
类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可
。这是因为普通类的类型就是类的类名,而类模板的类型是类名<参数类型>
。
例:
//Stack是类名, Stack和Stack才是类的类型
int main()
{
Stack<int> st1; // <==== int
Stack<double> st2; // <==== double
return 0;
}
本篇文章到这里就全部结束了,最后希望这篇文章能够为大家带来帮助。