总述
之前讲的操作符重载是多态的一种
类属多态是另一种多态,对应C++中间的模板机制
模板机制也是一种源代码复用机制
泛型:用独立于任何类型的方式编写代码,使代码支持不同类型
用类型参数来参数化模块
类属函数(template function)
一些信息
- 涵义:同一函数对不同类型的数据完成相同的操作
- 宏实现:
缺点:只能实现简单功能(如比大小),复杂功能难以实现(如排序);没有类型检查#define max(a, b) (((a) > (b))?(a):(b))
不方便使用 - 函数重载实现:
需要定义的重载函数太多,无法定义完全int max(int, int); double max(double, double); A max(A, A);
- 函数指针实现:
缺点:void sort(void* ,unsigned int, unsigned int, int(* cmp)(void*, void* )); //首参:万能指针来接受数组 //函数指针传递具体的比较规则
·需要定义额外参数
·大量指针运算
·实现起来复杂(需要定义很多cmp函数)
·可读性差
模板
引入template目标:实现这个需求的支持类型完全、清晰可读的目标
模板代码:将于类型相关的信息屏蔽掉,变成类型参数:
template<typename T>
void sort(T A[], unsigned int num){
for(int i = 1;i < num;i++){
for(int j = 0;j < num - i;j++){
if(A[j] > A[j + 1]){
T t = A[j];
A[j] = A[j + 1];
A[j + 1] = t;
}
}
}
}
int a[100];
sort(a,100);
double b[100];
sort(b,100);
//自定义类型里要重载操作符,重新定义大小的含义
class C{...};
C c[100];
sort(c,100);
compiler编译时编译的是带具体类型的代码,如上例,编译的是两段代码
编译器可以隐式具体化
也支持自定义类型,不过要谨慎地重载操作符
注意:赋值操作符、拷贝构造函数要谨慎
一些描述:
- 函数模板定义了一类重载函数;compiler自动实例化模板
- 函数模板可带类型参数,也可以带普通参数(但是要写在类型参数后)
这就是直接使用函数模板的普通参数的例子template<typename T, int size> void f(T a){ T temp[size]; ...... } ...... f<int, 10>(1); //类型参数可以推导,故不用显式实例化
- template普通参数可以有默认值(必须是编译时就能够确认的常量表达式);类型参数也可以有默认值,如:
与普通函数的默认参数值相比,函数模板的默认参数值位置更加灵活,可出现在任何位置(而前者的只能出现在参数列表右边的开始)template<typename T1 = int>
- 一个问题:编译器会推导出默认参数值,实际使用函数模板(如f(T1 a, T2 b))是可以推出参数类型的(类参的默认值)
类参的默认值的用处:
当函数模板定义的函数本身有默认值时,如f(T1 a, T2 b),这时compiler要求T1、T2的默认类型也要给出
一例:函数模板的使用
template<typename T>
T max(T a, T b){
return a > b ? a : b;
}
......
int x, y, z;
double l, m, n;
z = max(x, y);
l = max(m, n);
问:max(int, double)的处理如何?
解决:用一个函数重载作为补充(对应该类型的形参列表)
函数模板与函数重载配合使用
- 调用函数的优先级:
优先匹配非模板的重载函数
其次匹配显式具体化模板
最后匹配函数模板 - 实例化:编译器隐式实例化
类属类
类定义带有类型参加
一例:
template<class T>
class Stack{
T buffer[100];
public:
void push(T x);
T pop();
};
template<class T>
void Stack<T>::push(T x){...}
template<class T>
T Stack<T>::pop(){...}
...
//创建时要显式实例化,编译器不会也不能自动推导
//不同的实例化类型是不同的类型
Stack<int> st1;
Stack<double> st2;
- 模板类定义了多个类
- 实例化:需要显式实例化
- 参数:可以有多个类型参数,也可以有多个普通参数,普通参数要写在类型参数后面
- 模板类们的静态成员问题:
普通类的静态成员类内共享;
模板类的静态成员的共享范围:静态成员属于实例化之后的类,每个静态成员只属于自己的类(而非整个模板)
一例:
template<class T, int size> //可以有多个参数,类型参数、普通参数皆可
class Stack {
T buffer[size];
public:
void push(T x);
T pop();
};
template<class T, int size>
void Stack<T,size>::push(T x){...}
template<class T, int size>
T Stack<T,size>::pop(){...}
......
//使用时,类模板必须显式实例化
//这样相当于可以通过模板方式给类赋予一定的初始值
Stack<int, 100> st1;
Stack<double, 200> st2;
- 类型参数也有默认值:
模板类不按从右往左指定默认参数会有编译错误,这与模板函数的默认值机制不同;模板类的默认值机制与函数参数的默认值机制更加一致,而模板函数的更加灵活
有了默认值,虽然还是要显式实例化,但是可以利用默认值
一例:
template<class T = int, int size = 100>
class Stack{
T buffer[size];
public:
void push(T x);
T pop();
};
template<class T, int size>
void Stack<T,size>::push(T x){...}
template<class T, int size>
T Stack<T,size>::pop(){...}
...
Stack<int,100> st1;
Stack<double,200> st2;
Stack<> st3; //表示使用默认值
总结
基础总结
- 模板:源代码复用机制(对不同的实例化,产生了这段代码),实例化之后生成具体函数/类
函数实例化:隐式实现,根据具体的函数调用来实例化模板
类模板实例化:创建对象时显式指定
无论显式还是隐式实现,是否需要实例化由使用的点决定,即调用函数/创造对象时才来决定要不要实例化模板
如果没有用到某个模板实例,编译系统不会生成相应的实例代码 - 泛型编程只是减少编写程序的负担,不影响编译程序的工作量
一例:
//file1.h
template<class T>
class S{
T a;
public:
void f();
};
//file1.cpp
#include "file1.h"
template<class T>
void S<T>::f(){...} //类模板方法定义
template<class T>
T max(T x,T y){return x>y?x:y;} //函数模板
void main(){
int a, b;
max(a, b); //实例化函数模板
S<int> x; //将T实例化成int
x.f();
}
//file2.cpp
#include "file1.h"
extern double max(double, double);
void sub(){
max(1.1, 2.2); //编译时error
S<float> x; //编译时error
s.f();
}
如上代码,file1、file2两个模块一起编译,不可通过编译
C++中模板的完整定义通常出现在头文件中
此要求原因:
以此代码为例,编译过程:
1. include "file1.h"之后进行语法检查,看有无max、S<float>等
2. 再进行链接:
如果可以,就会调用已经编译好的链接代码,但对于模板而言,只是根据调用点上的需求进行实例化
此代码中,file1单元只有两段编译出来的代码,分别为int类的max和int类的S<>
file2编译链接之后,发现他所要的double类型的max和float类型的S<>在file1中间没有被编译,他就会尝试自己去实例化模板
但是,引入到file2元文件中间的只有file1.h头文件,file2找不到max模板,S<>模板也不完全,故报编译错
3. 解决办法:
将模板所有定义都放在头文件中,file2就能自己实例化了
注意:关于是否会有多重定义的问题
编译器、连接器有机制去掉同一模板的多重定义,不会发生链接期错误
4.总结:
编译器遇到模板时没有分配存储空间,而是待实例化时才会为实例化的东西分配存储空间(要求我们使用模板时注意完整定义)
一例:
template<int N> //模板仅有普通参数
class Fib{
public:
enum{
value = Fib<N - 1>::value + Fib<N - 2>::value
};
};
template<>
class Fib<0>{ //一个实例化
enum{
value = 1
};
};
template<>
class Fib<1>{ //另一个实例化
enum{
value = 1
};
};
void main(){ //使用
cout << Fib<8>::value << endl;
}
此例子是基于模板做完整编程
使用模板来算时,Fib<8>的值是在编译时期就确定的,这段代码一旦编译完就得到结果,不用运行(除了cout)
源编程(MetaProgramming)
- 源编程指编写一些生成、直接操作程序的程序,即这个程序可以生成、直接操作其它程序,比一般程序更高层级
- 源编程通过操纵程序实体,在编译时就计算出运行的一些常量、类型、代码
- C++通过模板来实现源编程,程序输入就是模板参数
并期望编译时就得到返回值,故期望使用static const、enum等
代码计算:通过类型计算进而选择具体到哪个类型
选择语句:通过模板的特例化
循环语句:模板的递归
C++的模板是图灵完备的
以上面的例子为例:
通过不同参数值选择不同模板(具体实例),可以给定不同模板计算不同需求