模板(Template)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张 "图纸"。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数的过程叫做模板的实例化,相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例。
在学习模板以前,如果想针对不同的类型使用相同的算法,就必须定义多个及其相似的函数或类,这样不但做了很多重复性的工作,还导致代码维护困难,用于交换两个变量的值的 Swap() 函数就是一个典型代表。而有了模板后,这些工作都可以交给编译器了,编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。
模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
例如,给定下面的函数模板和函数调用:
1 | template<typename T> void Swap(T &a, T &b) { |
编译器会根据不同的实参实例化出不同版本的 Swap() 函数。对于 Swap(n1, n2) 调用,编译器会生成并编译一个 Swap() 版本,其中 T 被替换为 int:
1 | void Swap(int &a, int &b) { |
对于 Swap(f1, f2) 调用,编译器会生成另一个 Swap 版本,其中 T 被替换为 float。对于 Swap(n3, n4) 调用,编译器不会再生成新版本的 Swap() 了,因为刚才已经针对 int 生成了一个版本,直接拿来使用即可。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。
请看下面的例子:
1 |
|
输出:
1 | x=40, y=50 |
p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。
值得提醒的是,Point<int, int> 和 Point<char, char> 是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如 p1 = p2; 这样的语句是错误的,除非重载了 = 运算符。