一、泛型编程
如何实现一个通用的Swap函数
void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } void Swap(double& x, double& y) { double tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; printf("交换前:a=%d , b=%d\n", a, b); Swap(a, b); printf("交换后:a=%d , b=%d\n", a, b); double c = 12.5; double d = 24.2; printf("交换前:a=%.2lf , b=%.2lf\n", c, d); Swap(c, d); printf("交换后:a=%.2lf , b=%.2lf\n", c, d); return 0; }
像上面那样使用函数重载虽然可以不知不觉地实现不同的参数类型达到一样的交换效果,但是可以看到,Swap函数除了参数类型不同,其他的运算逻辑都是一样的。
函数重载有两个不好的地方:一是代码的复用性低,只要有新的类型出现就要再重载一个函数,代码冗余太严重。二是维护成本较高,一个重载函数出错就会导致所有的重载函数都出错,修改成本太大。
那么我们能否给编译器一个模子,让编译器根据不同的类型利用该模子来生成一份对应的代码呢?基于这样的问题,C++就出现了模板,之后又推出了泛型编程。
泛型编程:即编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
二、函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在调用函数时被实例化,根据实参类型产生特定类型版本的函数。
2.2 函数模板的格式
template<class T1,class T2,class T3…>
返回值 函数名(参数列表){函数体}
例如Swap函数:
template <class T> void Swap(T& x, T& y) { T tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; printf("交换前:a=%d , b=%d\n", a, b); Swap(a, b); printf("交换后:a=%d , b=%d\n", a, b); double c = 12.5; double d = 24.2; printf("交换前:a=%.2lf , b=%.2lf\n", c, d); Swap(c, d); printf("交换后:a=%.2lf , b=%.2lf\n", c, d); return 0; }
由上图可以看出,写了一个Swap函数模板之后,无论需要交换什么类型的数据,都不用手动再写同样逻辑的代码,而是编译器会自动生成一份对应的代码完成数据的交换,
2.3 函数模板的原理
函数模板本身并不是一个函数,而是一个模具,是编译器使用特定方式产生具体类型函数的模具。就好比我们工厂在制造月饼的时候所用的月饼模,它本身并不是月饼,只不过把材料放到这个模具之后就能形成月饼的形状。换句话说其实模板就是将本来应该我们做的重复的事情交给了编译器做。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于其他类型也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板生成对应的函数,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1、隐式实例化:让编译器根据实参推导出模板参数的实际类型。
2、显式实例化:在函数名后的<>中指定模板参数的实际类型。
template <class T> T Add(const T& x,const T& y) { return x + y; } int main() { 隐式实例化,通过传实参a、b给Add函数模板,自动生成一份 int类型的Add函数代码 int a = 10; int b = 20; int sum1 = Add(a, b); cout << sum1 << endl; 隐式实例化,通过传实参c、d给Add函数模板,自动生成一份 double类型的Add函数代码 double c = 12.5; double d = 24.2; double sum2 = Add(c, d); cout << sum2 << endl; //Add(a, d); //这样写是错误的,因为该函数模板只有一个模板参数T,而a的类型是 //int,d的类型是double;编译器无法确定这里的T应该推导为int还是double从而报错 //在模板中,编译器不会进行类型转换 //这时解决方法有两个: //1、强制类型转换 int sum3 = Add(a, (int)d); cout << sum3 << endl; //2、显式实例化 //如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法成功 // 转换编译器将会报错。 //例如这里的c是double类型,编译器会把它隐式转换成int类型再 //利用模板实例化出int类型的Add函数,再调用 int sum4 = Add<int>(a, c); cout << sum4 << endl; return 0; }
2.5 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会用模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择用模板特化出来一个更匹配的版本。
- 函数模板不支持自动类型转换,但普通函数支持进行自动类型转换。
三、类模板
3.1 类模板的定义格式
下面是一个简单的作为示例的Stack类:
//注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具 template <class T> class Stack { public: Stack(int capacity = 10) :_a(new T[capacity]) ,_capacity(capacity) ,_top(0) {} ~Stack() { if (_a) { delete[] _a; _a = nullptr; } _capacity = _top = 0; } //Push函数类中声明,类外定义 void Push(const T& x); private: T* _a; int _capacity; int _top; }; // 注意:类模板中函数放在类外面定义时,需要加模板参数列表 //并且这个模板参数列表只在这个Push函数内有效,即每定义一个 //成员函数,都需要写一遍模板参数列表(谨记) template <class T> void Stack<T>::Push(const T& x) { _a[_top] = x; _top++; } int main() { //实例化成存放int类型数据的栈 Stack<int> st1; st1.Push(1); st1.Push(2); st1.Push(3); st1.Push(4); //实例化成存放double类型的栈 Stack<double> st2; st2.Push(1.2); return 0; }
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型(int,double,char或者自定义类型等)放在<>中即可,类模板名字不是真正的类,相当于一张图纸,而实例化的结果才是真正的类,实例化得到的具体的类才能用来定义对象。
下面的Stack是类名,Stack<int>和Stack<double>才是类型 即Stack是类模板,Stack<int>才是实例化出来的具体的类 Stack<int> st1; Stack<double> st2;