本篇将会对 Cpp 中的模板进行一个简单的介绍(后序还关系模板进阶,对模板的内容进行更深入的讲解),其中包括模板的使用:函数模板、类模板,以及对于泛型编程的理解。其中的重点为函数模板,介绍了函数模板的原理、隐式实例化和显示实例化、还有模板参数的匹配规则。目录如下:
目录
1. 泛型编程
2. 函数模板
2.1 函数模板的格式
2.2 函数模板原理
2.3 函数模板的实例化
2.4 模板参数的匹配规则
3. 类模板
1. 泛型编程
通常我们在实现一个 Swap 函数的时候,对于不同参数类型的 Swap 函数,我们需要写很多个重载函数来解决这个 Swap 函数的问题,但是当我们遇到一个新的形参类型的时候,我们有需要重新写一个新的重载函数,那么我们有没有其他的办法来解决该问题呢?
我们可以使用模板,只提供一个函数,就可以以上问题,不在需要写很多的重载函数。模板的出现,开辟了泛型编程的道路 ---> 也就为之后的 STL 打下了基础。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。(模板是泛型编程的基础)
如下:
// 函数重载的 Swap 函数 void Swap(double& x, double& y) { double tmp = x; x = y; y = tmp; } void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } void Swap(char& x, char& y) { char tmp = x; x = y; y = tmp; } // Swap 的模板代码 template <typename T> void Swap(T& x, T& y) { T tmp = x; x = y; y = tmp; }
2. 函数模板
对于函数模板的使用,存在很多的细节,将会在下面较为详细的列举。
首先是对于函数模板的概念:函数模板代表了一个函数家族,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.1 函数模板的格式
函数模板格式如下:
template<typename T1, typename T2, ..., typename Tn> 返回值类型 函数名(参数列表){}
注:typename 是用来定义模板参数的关键字,也可以使用 class ,但不能使用 struct 作为定义模板参数的关键字。
样例如下:
template <typename T1, typename T2> void Swap(T1& x, T2& y) { T1 tmp = x; x = y; y = tmp; }
2.2 函数模板原理
函数模板本身是一个蓝图,本身并不是函数,是编译器用使用方式产生特定具体类型的函数模具。其实模板就是将本应该由我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器根据传入的实参类型来推演生成对应类型的函数以提供调用,如:当使用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于内置类型同样如此。如下的反汇编:
如上,我们在汇编层面可以看见,执行到需要运行的函数时,函数模板会自动的推演函数参数类型。推理过程如下:
2.3 函数模板的实例化
当我们使用不同类型的参数使用函数模板的时候,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显示实例化,如下:
对于模板参数而言,只存在一个 T,而这个时候,我们将 int 类型的 a1 和 double 类型的 d2 传入到函数之中去,函数模板无法推断我们需要使用的函数是 int 类型的还是 double 类型,所以编译器会报错。解决以上问题一共有两种办法,分别是隐式转化和显示转化,如下:
如上,红框圈住的为隐式实例化:隐式实例化让编译器根据实参推演模板参数的实际类型,我们可以在传入的参数中将其强转,已到达实参的类型相同。
蓝框圈住的为显式实例化:显式实例化在函数名后的 <> 中指定模板参数的实际类型,这样可以将传入的所有类型都转换为 <> 中的类型。
注:对于显示实例化和隐式实例化而言,当函数中确实存在类型的强转:在强转的时候会产生一个临时变量,而对于该临时变量而言是一个不可以修改的值,所以我们需要使用 const 对其进行修饰,若不使用 const 对其进行修饰,那么就会报错,所以说,这样的显式实例化和隐式实例化就不能用在 Swap 函数中,因为 Swap 函数中的参数需要交换,为需要改变的值,其中产生的临时变量与其函数参数不匹配,就会报错。
2.4 模板参数的匹配规则
对于模板参数的匹配存在以下原则:
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且函数模板也可以被实例化为这个非模板函数,如下:
当调用的函数与非模板函数匹配的时候,编译器不要使用模板进行特化,直接调用存在的函数,但是我们也可以调用模板函数,只需要在函数名后的 <> 添加对应的类型即可,就算已经存在对应的函数,我们仍然可以调用其模板函数。
2. 对于非模板函数和同名函数模板,若其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生一个实例。若模板可以产生一个具有更好匹配的函数,那么我们可以选择模板。如下:
简单来说,会优先匹配已经存在的非模板函数,若没有,则在匹配模板函数。
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
3. 类模板
对于类模板而言,类模板的定义格式如下:
template<typename T1, typename T2, ..., typename Tn> class classname{ // 类成员定义 };
如一个栈模板:
template <class T> class Stack { public: Stack(int capacity = 4) :_a(new T[capacity]) , _size(0) ,_capacity(capacity) {} ~Stack(); void Push(T x){} void Pop() {} T GetTop(){} private: T* _a; int _size; int _capacity; }; // 当模板中函数放在类外定义时,需要加模板参数列表 template <class T> Stack<T>::~Stack() { delete[] _a; _size = 0; _capacity = 0; _a = nullptr; } int main() { Stack<int> s1; Stack<double> s2; return 0; }
对于以上的类模板的使用,我们可以发现,类模板更便于我们使用不同自定义类型的类,如我们可以定义出 double 类型的 stack,也可以定义出 int 类型的 stack。当我们需要在类外对类函数进行定义的时候,我们需要按照一定的形式。