我们来看看关于模板函数的内容:
先来看看如何编写一个通用的加法函数:
#include<iostream>
using namespace std;
int Add(const int &left, const int &right)
{
return (left + right);
}
float Add(const float &left, const float &right)
{
return (left + right);
}
char Add(const char &left, const char &right)
{
return (left + right);
}
int main()
{
int a=Add(1, 2);
float b=Add(3.0f, 4.0f);
char c=Add('1', '2');
cout << a << endl;
cout << b << endl;
cout << c << endl;
system("pause");
return 0;
}
我们再来看看运行结果:
A . 如上使用的是函数重载的方法,写了整数加法,浮点数的加法和字符加法,虽然也实现了我们起初的想法:对不同类型的两个数进行相加,但是我们会发现其中它还是存在了很多的缺点:
1.只要有新的类型出现,就要添加相应的函数;
2.除了返回值和参数类型之外,其他的代码都是一样,导致代码的复用率不高;
3.如果只要求返回值不同,函数重载不能解决问题;
4.当一个函数内的实现有问题时,其他与之对应的函数都有问题,维护起来很难。
B. 我们还会想到一个办法就是——继承。把它写在一个公共基类里,但是这会导致我们失去类型的检测。而且,对于以后实现的许多类,都必须继承基类维护起来会更加麻烦。
C . 用宏也可以解决加法问题,但是它不是函数,同样不能够进行类型检测。
在这里我们就提出 函数模板 这个概念,使用函数模板可以避免上述的问题,同样可以实现一个通用的加法函数。
一、函数模板
什么是函数模板?
函数模板:代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
二、模板函数的格式
template<typename Param1, typename Param2,......typename Paramn>
/*返回值类型 函数名(参数列表)
{
...
}
例如:
template<typename T>
T Add(T left,T right)
{
return (left+right);
}
*/
注意:typename是用来定义模板参数关键字的,也可以使用class,但是不能使用struct!!!
模板函数也可以定义为Inline函数,如下:
template<typename T>
inline T Add(T left, T right)
{
return (left + right);
}
注意:inline关键字必须放在模板形参列表之后,返回值之前,不能放在template之前!!!
三、函数模板的实例化
模板本身不是类或者函数,编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。
template<typename T>
T Add(T left, T right)
{
return (left + right);
}
int main()
{
cout << Add(1, 2) << endl;//整数相加
cout << Add(3.0f, 4.0f) << endl;//单精度浮点数相加
cout << Add(5.12, 5.22) << endl;//双精度浮点数相加
cout << Add(10, (int)1.21) << endl;//将浮点数强转为整形,二者相加
cout << Add<int>(2, 2.2) << endl;//将浮点数强转为整形,二者相加
system("pause");
return 0;
}
通过汇编代码可以看到,底层的实现调用了Add<int>这种形式。模板函数不会转换实参以匹配已有的实例化,所以上述整形和浮点数的相加,可以分别通过最后两种显式或者隐式的类型转换来实现。
编译器只会进行两种转换:
1.const转换:接收const引用或者const指针的函数可以分别用非const对象的引用或者指针来调用。
2.数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或者函数类型的实参应用常规指针转换。数组实参将当作指向其第一个元素的指针,函数 实参当作指向函数类型的指针。
四、名字屏蔽规则
模板形参名字只能在模板形参之后到模板声明或者定义的末尾之间使用,遵循名字屏蔽规则。
typedef float T;
template <typename T>
void FunTest(T t)
{
cout << "t Type=" << typeid(t).name() << endl;
}
T gloab;
int main()
{
FunTest(10);
cout << "gloab Type=" << typeid(gloab).name() << endl;
system("pause");
return 0;
}
来看看程序执行结果:
五、非模板类型参数
我们先来看一段代码,理解一下非模板类型参数:
template<typename T,int N>//N是一个非模板类型参数,在这里代表了数组的大小
void Funtest(T(&arr)[N])
{
for (int i = 0; i < N; i++)
{
arr[i] = 0;
}
}
int main()
{
int a[5];
float b[5];
Funtest(a);
Funtest(b);
system("pause");
return 0;
}
来看看a,b里的值和相应的汇编代码:
可以从汇编代码里的两条call指令看出,数组在底层是通过Funtest<数组类型,非模板参数>这样的形式构成的。
六、模板函数的重载
我们来实现一个比较数字大小的函数:
int Max(const int &a, const int &b)//A--非模板函数
{
return a > b ? a : b;
}
template<typename T>
T Max(const T &a, const T &b)//B--模板函数
{
return a > b ? a : b;
}
template<typename T>
T Max(const T &a, const T &b,const T&c)//C--模板函数
{
return Max(a, b) > c ? Max(a, b) : c;
}
int main()
{
cout << Max(10, 20, 30) << endl;//C
cout << Max(10, 20) << endl;//A调用时先调用了非模板函数
cout << Max<>(10, 20) << endl;//B加了<>的一定会去调用模板函数
cout << Max(10, 20.1) << endl;//A调用非模板函数,非模板函数可以进行类型的转换
cout << Max<int>(10.0, 20.0) << endl;//B
cout << Max(10.0, 20.0) << endl;//B
system("pause");
return 0;
}
说明:
1.对于非模板函数和模板函数同名,如果其他条件相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。
2.模板函数不允许自动类型转换,但是普通函数可以进行自动类型转换。
提一个问题:上面的方法还可以比较两个字符串吗?答:不能!!!
这就有一个新的概念:模板函数的特化
有时候并不是总能够写出对所有可能被实例化的类型最适合的模板,这就需要被模板函数进行特化。
template<typename T>
int compare(T t1, T t2)
{
if (t1 > t2) return 1;
if (t1 < t2) return -1;
return 0;
}
int main()
{
char *str1 = "efgh";
char *str2 = "abcd";
cout << compare(str1, str2) << endl;
system("pause");
return 0;
}
来看看结果:
为什么会出现这种结果?按预期应该会输出 1 而不是 -1 ,我们再来看看它们二者的地址:
发现str1的地址小于str2的地址,所以会出现 1 的情况。
为了解决这种情况可以修改代码如下:
template<typename T>
int Compare(T t1, T t2)
{
if (t1 > t2) return 1;
if (t1 < t2) return -1;
return 0;
}
template<>
int Compare<const char*>(const char* const p1, const char* const p2)
{
return strcmp(p1, p2);
}
int main()
{
const char *str1 = "efgh";
const char *str2 = "abcd";
cout << Compare(str1, str2) << endl;
system("pause");
return 0;
}
可见结果正确!
注意:在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不匹配,编译器将为实参模板定义中实例化出一个实例。