1.函数模板
模板是C++中一个非常重要的东西,也是下一步学stl的最后一块拼图。那看看最后一块拼图是什么呢?C++祖师爷在写C语言时遇到了有个非常难受的地方:
遇到有很多类型变量交换的时候就要写不同的交换函数,再新增类型的交换还要继续写交换函数,这样就比较繁琐,那有什么好办法吗?这里引入一个小话题:比如有一天我们知道自己要穿越到古代了,穿越到古代的时候可以带现代的东西回去,我们已经提前知道我们穿越回去的工作就是刻字,每天把别人写好的文章刻在石板上为了将文章传出去,那此时我们最想带回去的就是造纸术和活字印刷术,这样可以大大提高效率。那同理祖师爷也就想我们能不能像活字印刷术一样写个模具,需要什么直接印出来就行,所以就有了新语法--模板。模板就像模具,它在这会搞出两个东西,一个叫函数模板,一个叫类模板。
先看看函数模板:
这里要新学一个关键字叫template,他后面跟一个尖括号,因为上面三个交换函数逻辑相同,参数类型不同,所以在尖括号里定义个类型,可以给typename,它是来定义模板参数的关键字,也可以给class,现阶段认为它们是一样的。类型可以叫T/Tp等都可以,名字随意起,日常更喜欢叫T,有type的意思。定义好了T类型,就可以在Swap里面用了,下面写好函数逻辑,这就是一个函数模板。这里面的T是什么具体类型我们不知道,我们只知道它是一个类型。模板厉害的地方是我们不需要写上面三个函数了,只要交换两个变量就可以用上面的函数模板:
用模板和用普通函数一样,只是模板里面类型定义成了模板参数。函数模板和普通函数不一样,函数的参数定义的是变量,是对象,函数模板定义的是类型。那上图两个Swap调的是不是同一个函数呢?不是的,就如同在古代把文章用模具印了很多份出来,老百姓看的文章不是模具,而看的是模具印刷出的复制品。
调式的时候按F11进去看似进了模板,实际不是,这里做了优化,可通过汇编来验证:
那怎么做到看起来调同一个函数实际不是同一个呢?这里其实调的是模板生成的具体函数:
这个过程叫函数模板的实例化。我们在用模板调用时编译器会自动推,如上图传了两个double,推出T是double,然后生成一份T是double的代码;再如传了两个int,推出T是int,然后生成一份T是int的代码。根据参数推出参数T是什么,然后生成对应的代码叫模板的实例化。模板的实例化指用模板生成对应的函数,所以它在这调用的还是生成的函数。因此实际上调用的不是模板,是模板实例化生成的函数,函数是编译器生成的,最终还是编译器承担了一切。这里假设有两个日期类对象也是可以用模板交换的:
这会推出T是Date,然后实例化出交换Date的函数,里面调用拷贝构造和赋值来完成交换。以后再有类似Swap的情况,就不需依次写具体函数,写函数模板就行,这个模板是给编译器用的。其实从此以后不用自己写Swap模板,因为库里面有:
以后想交换直接用就可以,只不过是小写的swap:
可以发现指针交换也不用自己写模板,可以直接用,T可实例化为int*。那不是以前说交换int*要用int**吗,那是C语言的玩法,C++这里有引用,函数形参是实参的别名。
有些地方把用模板完成的东西叫泛型编程,那什么是泛型编程呢?就是我们写的代码是针对广泛的类型,不是针对某种具体的类型。前面main函数里完成a和b的交换,调的不是模板,实际调用的是编译器用函数模板实例化生成的具体函数,也就是交换就要写多个函数,只是模板这个玩法把本来应该由我们干的活交给编译器去做。那能不能定义多个模板参数呢?也是可以的,比如说这里假设有打印不同类型数据的需求:
此时就可以定义多个模板参数,T1推为int,T2推为double,实例化出in和double的函数。前面都用模板类型做参数,也可以用模板类型做返回值:
函数模板根据调用,自己推导模板参数的类型,实例化出对应函数。
下一个要看的是函数模板的实例化,函数模板的实例化有两种方式:第一种叫隐式实例化,也就是编译器根据类型自己去推;第二种叫显示实例化。
上图就是隐式实例化,只有一个模板参数,一个推为int,一个推为double。但有些地方推不了:
如果出现上图这样的方式会出现什么问题呢?这时a1让T推为int,d1让T推为double,那到底推为什么呢?这时编译器就为难了,报错说模板参数T不明确。那怎么解决这样的问题呢?一种方式是让它们达成一致:
用强制类型转化,要么都听a1的转化为int,要么都听d1的转化为double,这样就不存在歧义了。还有一种方式是显示实例化:
隐式实例化是通过实参传递的类型来推演T的类型,显示实例化是用指定的类型实例化,上图第一个就是指定实例化为int,第二个就是指定实例化为double。在函数名和参数之间加尖括号就叫显示实例化,如第一个指定T为int时d1会涉及到隐式类型转化。其实大多数场景下都用不上显示实例化,一般自动推就行,但有一种场景需要用显示实例化,假设有这样一个函数:
这个函数如果不用显示实例化无法使用,用Alloc(10)直接调用都没法推,所以就只能Alloc<double>(10)像这样显示实例化,这里就是有些函数无法自动推,只能显示实例化。
2.类模板
比如我们有了类和对象,我们写一个栈,能不能写一个库出来呢?不能,因为这的类型还是写死的。有时候觉得写的这个东西还可以,想让栈存int就把DataType前写为int,想让栈存double就把DataType前改为double就可以了,这样没什么问题。但有没有可能有这样一个场景:
在同一个程序中我的栈s1要存int,栈s2要存double,此时就不可以了,除非写StackInt和StackDouble两个类,但这样太麻烦。上面这些栈无非就是类型不同,所以一个程序中要不同类型的类,我们不想自己写太多的类,那就交给编译器去完成,给编译器一个模板,这个模板叫类模板:
用的地方不能直接Stack s1这样传参数告诉编译器说用int,因为构造函数这不一定用T参数,所以没法通过推演实例化,所以用类模板要显示实例化。如果有需求类模板也是可以定义多个模板参数的:
最后要说的一个东西是类模板函数声明和定义分离是有些不一样的,如构造函数实现声明和定义分离:
类里面声明,类外面定义,发现按照传统方式声明和定义分离是编不过的,类里面用T知道是的模板参数,类外面不知道T是哪里来的,Stack::Stack(size_t capacity)还是不认识T,所以要声明模板参数。声明后还是不可以,这里有个要注意的地方,类模板和以前有个不一样的地方是:以前对于普通类而言,类名就是类型。对于类模板,类名和类型不一样,如这里Stack是类名,类型是Stack<T>。就比如main中有Stack<int> s1,这里模板实例化出的是Stack<int>类型,如果直接Stack::Stack(size_t capacity)就相当于还没有实力化。类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。因此要这样写:
再比如把Push分离写: