一、 函数模板基本概念
C++函数模板是一种可以在代码中定义通用函数的方式,其可以用于定义可以接受任意类型的参数的函数。通过使用函数模板,程序员可以避免重复编写相似的代码,提高代码的可重用性和可维护性。
1.1 定义函数模板的语法
template <typename T>
T func_name(T arg1, T arg2, ...)
{
// 函数体
}
template
关键字表示接下来的代码是一个函数模板的定义typename
表示模板参数的类型T
是一种类型参数的通用名字func_name
即函数模板的名称。
在 C++98 添加关键字 typename 之前,C++使用关键字 class 来创建模板。
函数模板中的参数列表可以是任意数量的、任意类型的参数。在函数模板中,可以使用模板参数来表示参数的类型、函数返回值的类型等。
1.2 实现函数模板
函数模板的实现可以在代码文件中或头文件中完成。建议将函数模板的实现放在头文件中,以便其他代码可以直接使用。
template <typename T>
T max(T a, T b)
{
return a > b ? a : b;
}
上述代码实现了一个可以接受任意类型的函数模板max
,其返回两个参数中的最大值。
使用函数模板时,用指定的类型替换函数模板中的模板参数即可。例如,以下代码使用函数模板max
来查找两个整数中的最大值:
int x = 10, y = 20;
int max_val = max(x, y); // 使用函数模板max查找两个整数中的最大值
double a=10.0,b=20.0;
double max_val1 = max(a,b);
函数模板实例化可以让编译器自动推导,也可以在调用的代码中显式的指定。
int x = 10, y = 20;
int max_val<int> = max(x, y); // 使用函数模板max查找两个整数中的最大值
double a=10.0,b=20.0;
double max_val1<double> = max(a,b);
二、函数模板注意事项
-
可以为类的成员函数创建模板,但不能是虚函数和析构函数
class MyClass{ public: template<typename T> MyClass(T a){//构造函数可以使用函数模板 cout<<a<<endl; } template<typename T> void fun(T b){//普通函数可以使用函数模板 cout<<b<<endl; } // template<typename T>虚函数不可以用于函数模板:virtual' cannot be specified on member function templates // virtual void fun1(T c){ // cout<<c<<endl; // } //析构函数不用函数模板,因为析构函数没有参数 }; int main() { MyClass m(3);//3 m.fun(4);//4 }
-
在使用函数模板时,必须明确指定数据类型,以确保实参与函数模板参数匹配
//比如,我们定义了一个函数模板,用于计算两个数的和。函数模板的定义如下: template <typename T> T add(T a, T b) { return a + b; } //如果我们想要计算两个整数的和,可以这样调用函数: int a = 1, b = 2; int c = add<int>(a, b); //或者int c = add(a, b); //这里,我们明确指定了数据类型为 int,保证了实参与函数模板能匹配上。如果我们这样调用函数: int a = 1; double b = 2.0; double c = add<double>(a, b); //这里,我们指定了数据类型为 double,但是实参 a 的数据类型为 int,与函数模板的数据类型不匹配,会出现编译错误。
-
使用函数模板时,推导的数据类型必须适应函数模板中的代码
template <typename T> T fun(T a,T b) {//函数中的代码,不适用MyClass类型 return a+b; } class MyClass{}; int main() { int a=1,b=2; int c=fun(a,b); double x=1.0,y=2.0; double z= fun(x,y); // MyClass aa,bb; // MyClass cc = fun(aa,bb); }
-
在使用自动类型推导时,函数模板不会发生隐式类型转换
template <typename T> T Add(T a, T b) { return a + b; } int main() { int sum = Add(5, 3.2); // 错误:隐式类型转换不适用于自动类型推导 double total = Add<double>(5, 3.2); // 正确:显式指定数据类型进行隐式类型转换 return 0; }
-
函数模板可以支持多个通用数据类型的参数
template <typename T1,typename T2> void fun(T1 a, T2 b) { cout<<a<<endl; cout<<b<<endl; } int main() { fun(3,"123"); }
-
函数模板支持重载,其中可以包含非通用数据类型的参数
void fun(int a){ cout<<"void fun(int a)"<<endl; } template <typename T> void fun(T a) { cout<<"fun(T a)"<<endl; } template <typename T1,typename T2> void fun(T1 a,T2 b) { cout<<"void fun(T1 a,T2 b)"<<endl; } template <typename T1,typename T2> void fun(T1 a,T2 b,int c) { cout<<"void fun(T1 a,T2 b,int c)"<<endl; } int main() { fun(3);//void fun(int a) fun(1.2);//fun(T a) fun(1.2,3);//void fun(T1 a,T2 b) fun(1.2,3,3);//void fun(T1 a,T2 b,int c) }
三、函数模板的具体化
函数模板是一种通用的函数定义,可以用来创建适用于多种数据类型的函数。然而,有时候我们需要针对特定的数据类型提供更特定的实现,这就引入了函数模板的具体化(也叫特化)概念。具体化允许我们为特定的数据类型提供专门的函数实现,以优先于通用的函数模板或普通函数。
下面是具体化的语法示例:
template<>
void 函数模板名<数据类型>(参数列表)
{
// 函数体。
}
或者简化写法:
template<>
void 函数模板名 (参数列表)
{
// 函数体。
}
对于给定的函数名,可以有普通函数、函数模板和具体化的函数模板,以及它们的重载版本。
3.1编译器使用各种函数的规则
-
具体化优先于常规模板,普通函数优先于具体化和常规模板。
-
普通函数优先于具体化和常规模板
void fun(int a){ cout<<"普通函数:void fun(int a)"<<endl; } template <typename T> void fun(T a) { cout<<"函数模板:fun(T a)"<<endl; } template<> void fun<int>(int a){ cout<<"函数模板的具体化:void fun<int>(int a)"<<endl; } int main() { fun(3);//普通函数:void fun(int a) }
-
具体化优先于常规模板
template <typename T> void fun(T a) { cout<<"函数模板:fun(T a)"<<endl; } template<> void fun<int>(int a){ cout<<"函数的具体化:void fun<int>(int a)"<<endl; } int main() { fun(3);//函数模板的具体化:void fun<int>(int a) }
-
-
如果不希望使用普通函数,希望使用函数模板,可以用空模板参数强制使用函数模板。
void fun(int a) { cout<<"普通函数:void fun(int a)"<<endl; } template <typename T> void fun(T a) { cout<<"函数模板:fun(T a)"<<endl; } int main() { fun<>(3);//函数模板:fun(T a) }
void fun(int a) { cout<<"普通函数:void fun(int a)"<<endl; } template <typename T> void fun(T a) { cout<<"函数模板:fun(T a)"<<endl; } template<> void fun<int>(int a){ cout<<"函数的具体化:void fun<int>(int a)"<<endl; } int main() { fun<>(3);//函数的具体化:void fun<int>(int a) }
-
如果函数模板能产生更好的匹配,将优先于普通函数
void fun(int a) { cout<<"普通函数:void fun(int a)"<<endl; } template <typename T> void fun(T a) { cout<<"函数模板:fun(T a)"<<endl; } template <> void fun(int a) { cout<<"函数模板:fun(T a)"<<endl; } int main() { fun('a');//函数模板:fun(T a) }
四、普通函数、函数模板和函数模板具体化详解
4.1 普通函数与函数模板的区别
-
普通函数调用时可以发生自动类型转换(隐式类型转换)
void fun(int a, int b) { cout<<a+b<<endl; } int main() { int a=10; double b=20.0; char c='A'; fun(a,a); fun(a,b);//将double隐式转换成int fun(a,c);//将char隐式转换成int }
-
函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
template<typename T> void fun(T a,T b){ cout<<a+b<<endl; } int main() { int a=10; double b=20.0; char c='A'; fun(a,a); fun(a,b);//报错:使用自动类型推导,不会发生隐式类型转换 fun(a,c);//报错:使用自动类型推导,不会发生隐式类型转换 }
-
如果利用显示指定类型的方式,可以发生隐式类型转换
template<typename T> void fun(T a,T b){ cout<<a+b<<endl; } int main() { int a=10; double b=20.0; char c='A'; fun(a,a); fun<int>(a,b);//如果用显示指定类型,可以发生隐式类型转换 fun<int>(a,c);//如果用显示指定类型,可以发生隐式类型转换 }
总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T
4.2普通函数与函数模板的调用规则
-
如果函数模板和普通函数都可以实现,优先调用普通函数
void fun(int a,int b){ cout<<"普通函数:void fun(int a,int b)"<<endl; } template<typename T> void fun(T a,T b){ cout<<"函数模板:void fun(T a,T b)"<<endl; } int main() { fun(1,2);//普通函数:void fun(int a,int b) }
-
可以通过空模板参数列表来强制调用函数模板
void fun(int a,int b){ cout<<"普通函数:void fun(int a,int b)"<<endl; } template<typename T> void fun(T a,T b){ cout<<"函数模板:void fun(T a,T b)"<<endl; } int main() { //可以通过空模板参数列表来强制调用函数模板 fun<>(1,2);//函数模板:void fun(T a,T b) }
-
函数模板也可以发生重载
template<typename T> void fun(T a,T b){ cout<<"函数模板:void fun(T a,T b)"<<endl; } template<typename T> void fun(T a,T b,T c){ cout<<"函数模板:void fun(T a,T b,T c)"<<endl; } int main() { fun<>(1,2);//函数模板:void fun(T a,T b) fun<>(1,2,3);//函数模板:void fun(T a,T b,T c) }
-
如果函数模板可以产生更好的匹配,优先调用函数模板(和第三节一样的)
总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
4.3 函数模板的局限性
模板的通用性并不是万能的
例如:
template<class T>
void f(T a, T b)
{
a = b;
}
在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了
再例如:
template<class T>
void f(T a, T b)
{
if(a > b) { ... }
}
在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行
因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板
class Dog{
public:
Dog(string name,int age):name_(name),age_(age){
}
string name_;
int age_;
};
//普通函数模板
template<typename T>
bool compare(T &a,T &b){
if(a==b)return true;
return false;
}
//如果要比较两个对象是否相等,就不能只想上面直接判断传入的两个对象相等来判断了
//第一种解决方案:重载Dog类里面的==运算符,稍微有点麻烦
//第二种解决方案:使用函数模板的具体化
template<>
bool compare(Dog &a,Dog &b){
if((a.age_==b.age_)&&(a.name_==b.name_))return true;
return false;
}
int main(){
int a=1,b=2;
bool result1 = compare(a,b);//调用通用函数模板
cout<<result1<<endl;//false
Dog dog1("哈士奇",3);
Dog dog2("哈士奇",3);
bool result2= compare(dog1,dog2);//调用函数模板具体化
cout<<result2<<endl;//true
}
五、函数模板分文件编写
5.1 实体是什么
在函数模板的上下文中,"实体"指的是根据函数模板的描述生成的实际函数代码,也就是特定类型或参数的具体函数。
举例来说,如果有一个函数模板用于交换两个值的函数:
template <typename T>
void swapValues(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
在这个例子中,swapValues
函数模板的实体是在实际调用时根据提供的类型(例如 int、double、string
等)生成的函数。所以当你在代码中调用 swapValues
函数模板时,编译器会根据提供的类型生成对应的实体函数。
实体函数是编译器最终生成的、可以被机器执行的代码。在 C++ 中,函数模板的实体是根据模板参数具体化生成的。实体函数才是真正被程序调用和执行的部分。
5.2 函数模板份文件编写原则:
- 函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。
- 函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中。
- 普通函数:
- test.h
#include<iostream> using namespace std; void fun(int a);//普通函数声明在头文件
- test.cpp
include"test.h" void fun(int a){//普通用函数定义在源文件 cout<<a<<endl; }
- main.cpp
#include "test.h" int main(){ fun(3);//3 }
- test.h
- 函数模板和函数模板具体化
- test.h
#include<iostream> using namespace std; template<typename T>//函数模板的创建写在头文件中 void fun(T a){ cout<<a<<endl; } template<>//函数模板的具体化的声明写在头文件中 void fun(int a);
- test.cpp
#include "test.h" template<>//函数模板的具体化的定义写在头文件中 void fun(int a){ cout<<a<<endl; }
- main.cpp
#include "test.h" int main(){ fun(3);//3 }
- test.h