文章目录
【写在前面】
之前在学数据结构时就说过,C 语言没有实现数据结构的库,非常的麻烦,为什么 C 语言没有这样的库呢,因为它不支持泛型编程。所以在 C++ 中它支持了泛型编程,支持了模板 —— 函数模板、类模板。
这里只是模板的入门,只为了先入门 STL。后面 C++ 初阶会对模板进行进阶的学习。
一、泛型编程
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
如何实现一个通用的交换函数 ❓
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } void Swap(double& left, double& right) { double temp = left; left = right; right = temp; } int main() { int a = 0, b = 1; double c = 1.1, d = 2.2; Swap(a, b); Swap(c, d); return 0; }
📝说明
虽然能够达到目的,但是也有缺陷:
- 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数
- 代码的可维护性比较差,一个出错可能所有的重载都出错
能否告诉编译器一个模子,让编译器根据不同的类型利用该模子生成代码 ❓
在 C++ 中,也能够存在这样一个模具,通过给这个模具中填充不同材料 (类型),来获得不同材料的铸件 (生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。
二、函数模板
💦 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
💦 函数模板的格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
template<typename T> template<class T>
📝说明
对于 T 我们可以任意起,如 L、K、Compare 等,这个名称是自己取的,但是一般要符合意境,前期一般喜欢用 T (Type 的意思)。
注意 typename 是用来定义模板参数的关键字,也可以使用 class (这里不能用 struct 代替 class)。typename 是新增的,好多地方都习惯用 class,包括后面学习的 STL (它的源码里都用的 class),现阶段可以认为 typename 和 class 没有区别,到后面会有一点区别,学到了再看。
函数模板实现交换 ❓
//如果写了对应的函数,那么它依然会去调用,但是没必要 /*void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }*/ template<class T> void Swap(T& x1, T& x2) { T tmp = x1; x1 = x2; x2 = tmp; } int main() { int a = 0, b = 1; double c = 1.1, d = 2.2; int* p1 = &a, *p2 = &b; Swap(a, b); Swap(c, d); Swap(p1, p2); return 0; }
📝说明
这里就可以知道它可以针对所有类型完成交换工作。
那它一个函数能完成这里几种函数的功能吗 ???
💦 函数模板的原理
那么如何解决上面的问题呢?大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。
这里的懒人指的是行动上变懒了 (只是不想做重复的事情),思想上并没有滑坡。
马云:世界是懒人创造的
如上代码一个函数能完成这里几种函数的功能吗 ❓
显然是不能的
它们在调用的时候都要执行 Swap 函数,如果它是一段指令,它是没法完成的。经调试每次调用都往 Swap 函数里走,实际上 VS 的编译器为了方便调试所以在调试器上做了手脚,所以实际还是调用了对应的函数,这个过程叫做模板的实例化。
可以看到汇编代码,它们依然去调用对应的函数:
编译器是怎么帮我们完成的呢 ❓
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,可以看到模板就是让你写的时候省劲了,但实际调用还是无差别。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于其它类型也是如此。
对于类型的推演,跟 auto 有关系吗 ???
并没有关系。这里的场景是不一样的:这里是针对调用一个函数或是下面的类时,根据用的角度去指定参数,然后把参数替换生成对应的代码;auto 是不能做参数和返回值的,它是在定义变量的时候用 —— auto e = 3.14; 它是根据 3.14 的类型去推演 e 的类型。可以看到虽然它们使用场景不一样,但是原理还是很相似的。
库里的 swap ❓
其实 C++ 库里有给 swap 的实现 (也就是说不需要自己写了):
int main() { int a = 3, b = 5; swap(3, 5); int i(1); int(10);//匿名 return 0; }
📝说明
到了这里可以认为内置类型是有构造函数的,当然这里只是用法上的一个构造 —— 初始化
比如后面要学的 STL ,不排除 T 就是 int :
💦 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
- 显式实例化:在函数名后的 <> 中指定模板参数的实际类型
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10, a2 = 20; double d1 = 10.1, d2 = 20.2; cout << Add(a1, a2) << endl;//ok cout << Add(d1, d2) << endl;//ok //cout << Add(a1, d2) << endl;//??? //1、 cout << Add(a1, (int)d2) << endl; cout << Add((double)a1, d2) << endl; //2、上面是实参去推演形参的类型,这里不需要推演,显示实例化指定T的类型 cout << Add<int>(a1, d2) << endl; cout << Add<double>(a1, d2) << endl; return 0; }
📝说明
Add(a1, d2):该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型,通过实参 a1 将 T 推演为 int,通过实参 d1 将 T 推演为 double,但模板参数列表中只有一个 T,所以编译器无法确定此处到底该将 T 确定为 int 还是 double 而报错。
怎么解决呢 ❓
假设我们不用模板,那么编译器就不会推演了,而这里能从 double 到 int 的原因是它们是相近类型,其中发生了隐式类型转换。
这里有两种处理方式:
- 用户自己强转
- 使用显式实例化
显式实例化的场景 ❓
class A { public: A(int x) {} }; template<class T> T func(int x) { T a(x); return a; } int main() { //func(1);//err func<A>(1); func<int>(1); return 0; }
📝说明
有些函数模板里面参数中没用模板参数,函数体内才有用,也就意味着无法通过参数推演 T 的类型,只能显示实例化。
💦 函数模板的匹配规则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
//专门处理int的加法函数q int Add(int left, int right) { return left + right; } //通用加法函数 template<class T> T Add(T left, T right) { return left + right; } int main() { //模板匹配原则: //1、有现成完全匹配的,就直接调用,没有现成调用的,实例化模板生成 Add(1, 2); //2、有需要转换匹配的,那么它会优先选择去实例化模板生成 Add(1.1, 2.2); return 0; }
📝说明
Add(1, 2):
比如说,今天你回家了,着急吃饭,家里没有人,你吃饭有两种方式,用妈妈给你的钱点外卖、冰箱里有菜自己做,那肯定是点外卖。编译器也是一样,第一个是现成的,直接调用就行,第二个编译器还要根据实参推导形参生成。所以编译器它会去调用第一个函数。
Add(1.1, 2.2):
镇上没有外卖,只能自己做。所以它会去调用第二个函数。
如果你妈不想让你点外卖,你必须得自己做 ❓
显示实例化:Add<< int >(1, 2);
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
//专门处理int的加法函数 int Add(int left, int right) { return left + right; } //通用加法函数 template<class T1, class T2> T1 Add(T1 left, T2 right) { return left + right; } void Test() { Add(1, 2); //与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); //模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数 }
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
小结:完全匹配 > 模板 > 转换匹配
三、类模板
💦 类模板的定义格式
template<class T1, class T2, ..., class Tn> class 类模板名 { //类内成员定义 };
栈的泛型问题 ❓
typedef int STDataType; class Stack { private: STDataType* _a; int _top; int _capacity; }; int main() { Stack st1;//int Stack st2;//double return 0; }
📝说明
这里想让 st1 是 int,st2 是 double,显然这里做不到。C 语言中的 typedef 只是增强程序的可维护性,不能解决泛型的问题。
类模板解决栈泛型 ❓
struct TreeNode { }; template<class T> class Stack { private: T* _a; int _top; int _capacity; }; int main() { Stack<TreeNode>st1;//TreeNode* Stack<int>st2;//int return 0; }
📝说明
对于函数模板可以根据实参去推演形参的类型,但是类在用的时候,首先是定义对象,所以类模板的使用都是显示实例化。
Stack< TreeNode* >st1 和 Stack< int >st2 用的是一个类 ❓
它们的模板参数不同,用的不是同一个类。
调试后发现,st1 里的 _a 是 TreeNode* 类型, st2 里的 _a 是 int* 类型。
💦 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
//Stack类名,Stack<int>才是类型 Stack<int>s1; Stack<double>s2;
struct TreeNode {}; template<class T> class Stack { public: Stack(int capacity = 4) : _a(new T[capacity]) , _top(0) , _capacity(capacity) {} ~Stack() { delete[]_a; _a = nullptr; _top = _capacity = 0; } //类里面声明,类外面定义呢??? void Push(const T& x); private: T* _a; int _top; int _capacity; }; template<class T> void Stack<T>::Push(const T& x)//指定域,且需要声明模板 {} int main() { Stack<TreeNode*>st1;//TreeNode* Stack<int>st2;//int return 0; }
📝说明
在类模板里声明,类模板外定义,与以前的不同。
普通类,类名就是类型
类模板,类名不是类型,类型是 Stack
函数/类模板不支持把声明写到 .h,定义写到 .cpp 的方式,会报链接错误,原因后面会详细讲。
.h里实例化了,Stack.cpp里没有实例化,test.cpp去找的时候,只有声明,没有定义。所以解决方法就是声明和定义不要分离,当然要分离也有方法,但是这种方法比较 low,在模板的进阶会详细学习。