C++函数模板(模板函数)的定义和使用
函数模板是函数的抽象,它与普通函数相似,唯一的区别就是函数参数的类型是不确定的,函数参数的类型只有在调用过程中才被确定。本文将针对函数模板的用法进行详细讲解。
定义函数模板的语法格式如下所示:
模板参数不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似。
【示例1】下面通过案例演示函数模板的用法,C++ 代码如下:
由运行结果可知,当调用 add() 函数传入 int 类型参数 1 和 2 时,参数 T 被替换成 int,得到结果为 3;当传入 double 类型参数 1.2 和 3.4 时,参数 T 被替换成 double 类型,得到结果为 4.6。
这就避免了为 int 类型数据定义一个求和函数,再为 double 类型数据定义一个求和函数的问题,实现了代码复用。
注意,不能在函数调用的参数中指定模板参数的类型,对函数模板的调用应使用实参推演。
例如,只能进行 add(2,3) 这样的调用,或者定义整型变量 int a=2,b=3,再将变量 a、b 作为参数,进行 add(a,b) 这样的调用,编译器会根据传入的实参推演出 T 为int 类型,而不能使用 add(int,int) 方式,直接将类型传入进行调用。
在【示例1】中第一次调用 add() 函数模板时,传入的是 int 类型数据 1 和 2,编译器根据传入的实参推演出模板参数类型是 int,就会根据函数模板实例化出一个 int 类型的函数,如下所示:
生成 int 类型的函数后,再传入实参 1 和 2 进行运算。
同理,当传入 double 类型的数据时,编译器先根据模板实例化出如下形式的函数:
显式实例化需要指定函数模板中的数据类型,语法格式如下所示:
例如,显示实例化为 int 类型,则在调用时,不是 int 类型的数据会转换为 int 类型再进行计算,如将【示例1】中的 add() 函数模板显式实例化为 int 类型,代码如下所示:
【示例2】下面通过案例演示函数模板 add() 显式实例化的用法,C++ 代码如下:
在调用 int 类型模板函数时,传入了一个字符 'B',则编译器会将字符类型的 'B' 转换为对应的 ASCII 码值,然后再与 10 相加得出结果。实际上就是隐式的数据类型转换。
需要注意的是,对于给定的函数模板,显式实例化声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义。
由于 C++ 编译器的不断完善,模板实例化的显式声明可以省略,在调用时用
例如,定义交换两个数据的函数模板,示例代码如下:
显式具体化的代码如下所示:
如果函数有多个原型,则编译器在选择函数调用时,非模板函数优先于模板函数,显式具体化模板优先于函数模板,例如下面三种定义:
如【示例1】中两次调用 add() 函数模板,编译器会根据传入参数不同实例化出两个函数,如下所示:
【示例3】下面通过案例演示函数模板重载的用法,C++ 代码如下:
在调用的过程中,如果参数相同,那么优先调用非模板函数而不会用模板产生实例。例如,第 19 行代码调用max() 函数,传入两个 int 类型参数,很好地匹配了非模板函数。
如果函数模板能够实例化出一个更匹配的函数,则调用时将选择函数模板。例如,第 21 行代码调用 max() 函数,利用函数模板实例化一个带有两个 char 类型参数的函数,而不会调用非模板函数 max(int,int)。
注意,模板不允许自动类型转化,如果有不同类型参数,只允许使用非模板函数,因为普通函数可以进行自动类型转换,所以第 22 行代码调用 max() 函数时,调用的是非模板函数,将 3.2 转换成了 int 类型再与 6 进行比较。
1)
例如,下面的函数模板声明是不正确的。
2) 全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。例如:
3) 函数模板中声明的对象或类型不能与模板参数同名。例如:
声明:《C++系列教程》为本站“54笨鸟”官方原创,由国家机构和地方版权局所签发的权威证书所保护。
函数模板的定义
如果定义一个实现两个数相加的函数 add(),要实现 int、float、double 等多种类型的数据相加,则要定义很多个函数,这样的程序就会显得非常臃肿。但使用模板就无须关心数据类型,只定义一个函数模板就可以。定义函数模板的语法格式如下所示:
template<typename 类型占位符>
返回值类型 函数名(参数列表)
{
//函数体;
}
template
是声明模板的关键字,<>
中的参数称为模板参数;typename 关键字用于标识模板参数,可以用class
关键字代替,class 和 typename 并没有区别。模板参数不能为空,一个函数模板中可以有多个模板参数,模板参数和普通函数参数相似。
template
下面是定义的函数模板,函数模板定义方式与普通函数定义方式相同,只是参数列表中的数据类型要使用<>
中的参数名表示。【示例1】下面通过案例演示函数模板的用法,C++ 代码如下:
#include<iostream> using namespace std; template<typename T> //定义函数模板 T add(T t1,T t2) { return t1+t2; } int main() { cout<<add(1,2)<<endl; //传入int类型参数 cout<<add(1.2,3.4)<<endl; //传入double类型参数 return 0; }运行结果:
3
4.6
- 第 3~7 行代码定义了函数模板 add(),用于实现两个数据相加;
- 第 10~11 行代码调用 add() 函数,分别传入两个 int 类型数据和两个 double 类型数据。
由运行结果可知,当调用 add() 函数传入 int 类型参数 1 和 2 时,参数 T 被替换成 int,得到结果为 3;当传入 double 类型参数 1.2 和 3.4 时,参数 T 被替换成 double 类型,得到结果为 4.6。
这就避免了为 int 类型数据定义一个求和函数,再为 double 类型数据定义一个求和函数的问题,实现了代码复用。
注意,不能在函数调用的参数中指定模板参数的类型,对函数模板的调用应使用实参推演。
例如,只能进行 add(2,3) 这样的调用,或者定义整型变量 int a=2,b=3,再将变量 a、b 作为参数,进行 add(a,b) 这样的调用,编译器会根据传入的实参推演出 T 为int 类型,而不能使用 add(int,int) 方式,直接将类型传入进行调用。
函数模板实例化
函数模板并不是一个函数,它相当于一个模子,定义一次即可使用不同类型的参数来调用该函数模板,这样做可以减少代码的书写,提高代码的复用性和效率。注意,函数模板不会减少可执行程序的大小,因为编译器会根据调用时的参数类型进行相应的实例化。所谓实例化,就是用类型参数替换模板中的模板参数,生成具体类型的函数。
实例化可分为隐式实例化与显式实例化,下面分别介绍这两种实例化方式。1) 隐式实例化
隐式实例化是根据函数调用时传入的参数的数据类型确定模板参数 T 的类型,模板参数的类型是隐式确定的,如【示例1】中函数模板 add() 的调用过程。在【示例1】中第一次调用 add() 函数模板时,传入的是 int 类型数据 1 和 2,编译器根据传入的实参推演出模板参数类型是 int,就会根据函数模板实例化出一个 int 类型的函数,如下所示:
int add(int t1,int t2) { return t1 + t2; }编译器生成具体类型函数的这一过程就称为实例化,生成的函数称为模板函数。
生成 int 类型的函数后,再传入实参 1 和 2 进行运算。
同理,当传入 double 类型的数据时,编译器先根据模板实例化出如下形式的函数:
double add(double t1,double t2) { return t1 + t2; }这样,每一次调用时都会根据不同的类型实例化出不同类型的函数,最终的可执行程序的大小并不会减少,只是提高了代码的复用性。
2) 显式实例化
隐式实例化不能为同一个模板参数指定两种不同的类型,如 add(1,1.2),函数参数类型不一致,编译器便会报错。这就需要显式实例化解决类型不一致的问题。显式实例化需要指定函数模板中的数据类型,语法格式如下所示:
template 函数返回值类型 函数名<实例化的类型>(参数列表);
在上述语法格式中,<>中是显式实例化的数据类型,即要实例化出一个什么类型的函数。例如,显示实例化为 int 类型,则在调用时,不是 int 类型的数据会转换为 int 类型再进行计算,如将【示例1】中的 add() 函数模板显式实例化为 int 类型,代码如下所示:
template int add<int>(int t1, int t2);
【示例2】下面通过案例演示函数模板 add() 显式实例化的用法,C++ 代码如下:
#include<iostream> using namespace std; template< typename T> T add(T t1,T t2) { return t1+t2; } template int add<int>(int t1,int t2); //显式实例化为int类型 int main() { cout<<add<int>(10,'B')<< endl; //函数模板调用 cout<<add(1.2,3.4)<< endl; return 0; }运行结果:
76
4.6
在调用 int 类型模板函数时,传入了一个字符 'B',则编译器会将字符类型的 'B' 转换为对应的 ASCII 码值,然后再与 10 相加得出结果。实际上就是隐式的数据类型转换。
需要注意的是,对于给定的函数模板,显式实例化声明在一个文件中只能出现一次,并且在这个文件中必须给出函数模板的定义。
由于 C++ 编译器的不断完善,模板实例化的显式声明可以省略,在调用时用
<>
显式指定要实例化的类型即可,如【示例2】中如果 add(1.2,3.4) 函数调用改为 add<int>(1.2,3.4) 调用,则会得出结果 4。多学一招:显式具体化
函数模板的显式具体化是对函数模板的重新定义,具体格式如下所示:
template< > 函数返回值类型 函数名<实例化类型>(参数列表)
{
//函数体重新定义
}
例如,定义交换两个数据的函数模板,示例代码如下:
template<typename T> void swap(T& t1,T& t2) { T temp = t1; t1 = t2; t2 = temp; }但现在有如下结构体定义,示例代码如下:
struct Student { int id; char name[40]; float score; };现在要调换两个学生的 id 编号,但是又不想交换学生的姓名、成绩等其他信息,那么此时就可以用显式具体化解决这个问题,重新定义函数模板只交换结构体的部分数据成员。
显式具体化的代码如下所示:
template<> void swap<Student>(Student& st1, Student& st2) { int temp = st1.id; st1.id = st2.id; st2.id = temp; }
如果函数有多个原型,则编译器在选择函数调用时,非模板函数优先于模板函数,显式具体化模板优先于函数模板,例如下面三种定义:
void swap(int&, int&); //直接定义 template<typename T>void swap(T& t1, T& t2); //模板定义 template<> void swap<int>(int&, int&); //显式具体化对于 int a,int b,如果存在 swap(a,b) 的调用,则优先调用直接定义的函数;如果没有,则优先调用显式具体化,如果两者都没有才会调用函数模板。
函数模板重载
函数模板可以进行实例化,以支持不同类型的参数,不同类型的参数调用会产生一系列重载函数。如【示例1】中两次调用 add() 函数模板,编译器会根据传入参数不同实例化出两个函数,如下所示:
int add(int t1,int t2) //int类型参数实例化出的函数 { return t1 + t2; } double add(double t1,double t2) //double类型参数实例化出的函数 { return t1+t2; }此外,函数模板本身也可以被重载,即名称相同的函数模板可以具有不同的函数模板定义,当进行函数调用时,编译器根据实参的类型与个数决定调用哪个函数模板实例化函数。
【示例3】下面通过案例演示函数模板重载的用法,C++ 代码如下:
#include<iostream> using namespace std; int max(const int& a,const int& b) //非模板函数,求两个int类型数据的较大值 { return a>b ? a:b; } template< typename T> //定义求两个任意类型数据的较大值 T max(const T& t1,const T& t2) { return t1>t2 ? t1:t2; } template<typename T> //定义求三个任意类型数据的最大值 T max(const T& t1,const T& t2,const T& t3) { return max(max(t1,t2),t3); } int main() { cout<<max(1,2)<<endl; //调用非模板函数 cout<<max(1,2,3)<<endl; //调用三个参数的函数模板 cout<<max('a','e')<<endl; //调用两个参数的函数模板 cout<<max(6,3.2)<<endl; //调用非模板函数 return 0; }运行结果:
2
3
e
6
- 第 3~6 行代码定义了一个函数 max(),用于比较两个 int 类型数据的大小。
- 第 7~11 行代码定义了函数模板 max(),用于比较两个数的大小。
- 第 12~16 行代码定义了函数模板 max(),用于比较三个数的大小。
- 第 19~22 行代码分别传入不同的参数调用函数 max()。
在调用的过程中,如果参数相同,那么优先调用非模板函数而不会用模板产生实例。例如,第 19 行代码调用max() 函数,传入两个 int 类型参数,很好地匹配了非模板函数。
如果函数模板能够实例化出一个更匹配的函数,则调用时将选择函数模板。例如,第 21 行代码调用 max() 函数,利用函数模板实例化一个带有两个 char 类型参数的函数,而不会调用非模板函数 max(int,int)。
注意,模板不允许自动类型转化,如果有不同类型参数,只允许使用非模板函数,因为普通函数可以进行自动类型转换,所以第 22 行代码调用 max() 函数时,调用的是非模板函数,将 3.2 转换成了 int 类型再与 6 进行比较。
使用函数模板要注意的问题
函数模板虽然可以极大地解决代码重用的问题,但读者在使用时仍需注意以下几个方面:1)
<>
中的每一个类型参数在函数模板参数列表中必须至少使用一次。例如,下面的函数模板声明是不正确的。
template<typename T1, typename T2> void func(T1 t) { }函数模板声明了两个参数 T1 与 T2,但在使用时只使用了 T1,没有使用 T2。
2) 全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。例如:
int num; template<typename T> void func(T t) { T num; cout<<num<<endl; //输出的是局部变量num,全局int类型的num被屏蔽 }在函数体内访问的 num 是 T 类型的变量 num,而不是全局 int 类型的变量 num。
3) 函数模板中声明的对象或类型不能与模板参数同名。例如:
template<typename T> void func(T t) { typedef float T; //错误,定义的类型与模板参数名相同 }4) 模板参数名在同一模板参数列表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。例如:
template<typename T, typename T> //错误,在同一个模板中重复定义模板参数 void func1(T t1, T t2){} template<typename T> void func2(T t1){} template<typename T> //在不同函数模板中可重复使用相同的模板参数名 void func3(T t1){}5) 模板的定义和多处声明所使用的模板参数名不是必须相同。例如:
//模板的前向声明 template<typename T> void func1(T t1, T t2); //模板的定义 template<typename U> void func1(U t1, U t2) { }6) 如果函数模板有多个模板参数,则每个模板参数前都必须使用关键字 class 或 typename 修饰。例如:
template<typename T, typename U> //两个关键字可以混用 void func(T t, U u){} template<typename T,U> //错误,每一个模板参数前都必须有关键字修饰 void func(T t, U u){}
声明:《C++系列教程》为本站“54笨鸟”官方原创,由国家机构和地方版权局所签发的权威证书所保护。