在C语言阶段,当遇到功能类似的函数时,我们只能定义多个名称不同的函数。
int Add1(int x, int y) { return x + y; } double Add2(double x, double y) { return x + y; }
到了C++我们首先学了函数重载,这使得我们不必再为功能相似的函数起不同的名字。
int Add(int x, int y) { return x + y; } double Add(double x, double y) { return x + y; }
虽然函数重载解决了C语言中命名的问题,但是依然要自己实现多个函数,麻烦,而且函数重载也有几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率较低,只要有新类型出现,用户就要自己增加对应的函数
- 代码的可维护性比较低,一个出错可能导致所有的重载函数出错
那有没有一种方法,我们只需要像活字印刷术那样,将会重复用的字刻成一个一个的模板,等到在需要使用该字的时候,直接用该模板即可,不必再用人工手写。
C++就提供了一种方法来解决C语言以及函数重载对功能相同函数的不足——模板。
一.模板
在日常生活中,处处有模板。我们做PPt时就会使用相对应的模板来使自己的ppt更加高大上。
而在C++中,模板是一个具有某种功能的函数或者类的统称。但是该模板不能够直接被调用,我们需要对其进行实例化,成为某个特定的函数或者类。
C++模板分为函数模板和类模板
二.函数模板
函数模板,实际上是建立一个通用函数,其函数类型和形参的类型不具体指定,用一个虚拟额类型来代表。
一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。上面的Add函数的模板版本像下面这样:
template <typename T>
T Add(T x, T y)
{
return x + y;
}
模板定义以关键字template开始,后面跟一个模板参数列表(template parameter list),这是一个逗号分隔的一个或多个模板参数的列表,用<>包围起来。
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化它们。
类似的,模板参数表示在类或者函数在定义时需要用到的类型。当我们使用模板时,我们(隐式或者显式地)指定模板实参,将其绑定到模板参数上,进行初始化操作。
我们对Add函数指定了一个模板参数T。在Add中,T表示一个类型。而T表示的实际类型则在编译时根据Add的使用情况来确定。
总的来说,模板参数列表中的参数就是未来函数或者是类的参数或者函数的类型
注意:在模板定义中,模板参数列表不能为空
2.1模板参数
函数模板参数的定义很像函数参数的定义。函数参数定义时:类型+变量名。
而模板参数定义时要加上关键字typename/class+模板参数名。
typename/clss 变量名
typename和class都可以定义模板参数,两者功能相同,且可以同时出现。
template <typename T1, class T2>
void func(T1 x, T2 y)
{
//……
}
2.2实例化函数模板
当我们调用一个函数模板时,编译器通常会根据我们传的实参来推断模板的类型。这种实例化方式为隐式实例化。
例如:对于Add函数,我们用下面的方式传参时:
std::cout << Add(1, 2) << std::endl;
我们传入实参(1,2)为整型,编译器就会推断模板的参数为int,并用来初始化T。此时我们的Add函数就变成了:
int Add(int x, int y)
{
return x + y;
}
但是当我们传实参时,传入了两个不同类型的实参,此时编译器就会报错:
std::cout << Add(1, 2.0) << std::endl;
因为我们只有一个模板参数,但是却传了两个不同类型的实参,此时编译器不知道该用哪一个实参的类型作为该模板的类型。
注意:在模板中,编译器一般不会进行类型转换操作。
为了解决上面这种问题,我们有两种解决方案:
1.对实参进行强制类型转换
std::cout << Add(1, (int)2.0) << std::endl;
//或
std::cout << Add((double)1, 2.0) << std::endl;
2.显式实例化
std::cout << Add<int>(1, 2.0) << std::endl;
我们可以先显式的实例化改模板的参数,即该模板此时已经是显式实例化后的模板,此时在传入类型不同的实参时,就会发生隐式类型转换。
2.3显式实例化
显式实例化即在函数名后用<>指定该模板参数的类型。
int a = 1;
double b = 19.2;
std::cout << Add<int>(a, b) << std::endl;//显式实例化
如果类型不匹配,编译器会尝试进行隐式类型转换,如果不能转换则编译器报错。
2.4模板的调用
当我们实例化模板之后,调用的是这个模板还是由该模板生成的新函数?
答案是新函数!
我们通过汇编代码可以看出,如果调用的都是函数模板的话,这两次调用应该是同一个地址。但是这两次调用的函数在不同的地址上,由此可以看出调用模板时调用的是由模板生成的新函数。
2.5模板参数的匹配原则
2.5.1一个非模板函数可以和同名的模板函数同时存在,而且该函数模板可以实例化为该非模板函数
template <typename T>
T Add(T x, T y)
{
return x + y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
Add(1, 2);//调用非模板函数
Add(1.0, 2.5);//调用模板函数
return 0;
}
2.5.2对于非模板函数和同名的模板函数而言,如果其他条件都相同,编译器会优先调用非模板函数,如果模板可以产生一个更合适的函数,则编译器调用模板函数
template <typename T1, typename T2>
T1 Add(T1 x, T2 y)
{
return x + y;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
Add(1, 2);//调用非模板函数
//调用模板函数
//此时如果调用非模板函数会发生隐式类型转换,会丢失精度
//而对于模板来说,可以生成一个针对于该实参的专属的函数,不会丢失精度
//所以对编译器而言此时调用模板函数是一个更好的选择
Add(1, 2.5);
return 0;
}
2.5.3模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
三.类模板
类模板定义方式和函数模板的定义方式类似:
template <typename T1, typename T2>
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
private:
T1 x;
T2 y;
};
其中类中定义的成员函数也可以使用模板参数。
注意:类模板不建议声明和定义分别存放在.h和.cpp文件中,可能导致链接错误
类模板的实例化必须显式实例化
A<int, int> a1;
A<int, double> a2;
实例化方式为:类模板后+<里面放需要的类型>+对象名