我们在学习函数的时候,曾经说过,当一个问题或者功能在一个文件中多次被使用的时候,我们可以将其封装成函数的形式,这样我们就能在再一次使用的时候直接调用。
那么今天也有一个相同的问题,我们要想写一个相同功能的函数,但是其参数类型和返回值类型不同呢?
比如:我么想要写一个加法的函数,但是,不仅仅适用于整型,还要能进行浮点型的运算,那么我们该如何操作呢?
当然最简单直接的方法就是将其都写出来,用的时候在调用:
int Add(int left, int right)
{
return left + right;
}
float Add(float left, float right)
{
return left + right;
}
不过,一堆的为题也接踵而至,首先,如果要是再有一个类型的数据呢?那是不是要再写一个函数呢?再说,如果只是返回值类型不同呢?函数 重载不能解决。还有就是其函数体都一样,代码的复用率不高。
那么如何结觉这个问题呢?
我们可不可以写一个没有类型的函数,这样也就不用为了一个类型的不同就把一个函数功能完全相同的代码,写好几遍了。
这个时候就得引出模板这个概念了,编写与类型无关的逻辑代码,这就是泛型编程,而模板就是泛型编程的基础。
函数模板
函数模板:顾名思义就是一个与类型无关的函数,这样在使用的时候,就可以通过实参类型来产生确定的类型版本。
那么现在来看一下函数模板的格式是什么?
template<typename Param1,typename Param2,…>
返回值类型 函数名(参数列表)
{
……
}
那么我们接下来就是用一个例子来验证一下吧:
template<typename T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(1, 2)<<endl;//整型
cout << Add(1.0, 2.5) << endl;//浮点型
cout << Add('1', '2') << endl;//字符型
return 0;
}
从结果可以看出,当我们的实参是整形的时候,那么T也就变成int,这样结果就会是整型,而参数是浮点型和字符型,结果也是相应类型的数据。
那么,要是两个数的类型那个不相同呢?
template<typename T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add(1, '1') << endl;
return 0;
}
结果是:“模板函数T不明确”,也就是说,参数类表与函数模板的实例参数类型不匹配,这样怎么解决呢?
如果是正常的函数,也就是说没有使用函数模板直接将类型写成 int 的类型时,这个是不会报错的,因为在int类型和char类型之间有隐式的类型转换,但是,模板函数不允许自动类型转换。
那么我们使用显式的类型转换怎么样?
普通的函数中显式类型转换是直接在变量前面加上类型并且使用括号括起来,在模板函中, 显式转换是这样的。
template<typename T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
cout << Add<int>(1, '1') << endl;
return 0;
}
没错,就是在实例化的时候在函数名和参数之间加一个尖括号,里面加上要转换的类型即可。
模板函数和非模板函数要是实现相同的功能,函数名称也相同,那么会优先调用那一个函数呢?
答案是:非模板函数。那么测试一下吧
int Add(int left, int right)
{
cout <<"haha"<< endl;//如果优先调用普通函数,那么会多打印haha
return left + right;
}
template<typename T>
T Add(T left, T right)
{
cout << "hehe" << endl;//如果优先调用模板函数,那么会多打印hehe
return left + right;
}
int main()
{
cout << Add(1, 2)<<endl;
return 0;
}
那么结果,请看:
如果想要调用模板函数而不是普通函数,那么只需要在实例化前面加上尖括号里面是类型即可。
所以说,我们可以得出结论:一个非模板函数可以和一个模板函数同时存在,并且模板函数还可以实例化成非模板函数。对于非模板函数和同名的模板函数,如果其他的条件都相同,那么调用的时候会优先调用非模板函数。
模板参数
关于模板参数列表的参数有几点要注意的地方:
1、模板形参在模板形参列表中只能出现一次
template<typename T, typename T>//重定义参数T
T Add(T left, T right)
{
cout << "hehe" << endl;
return left + right;
}
2、模板形参可以是类型形参也可以是非类型形参,所有类型形参前面必须加上class或者typename关键字修饰
template<typename T, T1>//错误:标识符T1
T Add(T left, T right)
{
cout << "hehe" << endl;
return left + right;
}
模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持。
3、定义模板函数时,模板形参列表不能为空
4、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型
使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换
5、在函数模板的内部不能指定缺省的模板实参
6、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参推演出来
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<typename T>
T FunTest(const T& a, const T& b)
{
T return a + b;
}
int FunTest(int a, int b)
{
return a + b;
}
int main()
{
FunTest(1, 3);//会调用普通函数
FunTest<>(1, 3);//会调用模板函数
return 0;
}
模板函数特化
上面我们一直都是写的两个数的加法运算,但是这个模板并不能适用于所有的类型,比如:字符串类型。
template<typename T>
T Add(T left, T right=10)
{
return left + right;
}
int main()
{
//cout << Add(1,2) << endl;
cout << Add("asd", "fgh");
return 0;
}
结果:错误:“不能添加两个指针”
也就是说,并不是所有的时候我们都能写出合适所有类型的模板,有时候写出的模板对于一些类型编译不过去,或者是出现的结果与我们预想的结果不符合,那么这时候就要进行模板函数的特化了。
模板函数特化的格式:
template<>
返回值类型 函数名<type>(参数列表)
{
……
}
那么接下来我们试一试写下模板函数的特化,让它实现字符串类型的加法:
template <typename T>//原有的函数模板
T Add(T data1, T data2)
{
return data1 + data2;
}
//模板函数特化
template<>
char* Add<char*>(char* const data1, char* const data2)
{
char* tmp = new char[strlen(data1) + strlen(data2) + 1];
strcpy(tmp, data1);
strcat(tmp, data2);
return tmp;
}
int main()
{
char *a = "hello";
char *b = " world";
cout << Add(a, b) << endl;
system("pause");
return 0;
}
模板类
//模板格式:
template <typename 形参名>
class 类名
{
......
};
1、在使用类模板时,需要我们显示的给出模板实参列表,否则编译器无法得知实际的类型。
2、类模板的成员函数可以在类模板的定义中定义(inline函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上template及模板参数)。
3、类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化。
模板的优缺点:
优点:模板复用了代码,节省资源,更快的迭代开发,c++的标准模板库(STL)因此而产生。 增强了代码的灵活型。
缺点: 模板让代码变得凌乱复杂,不易维护,编译代码时间变长。 出现模板编译错误时,错误信息非常凌乱,不易定位错误。