之前学习函数重载的时候,我们写了一个交换函数Swap
void Swap(int& left, int& right) { int tmp = left; left = right; right = tmp; } void Swap(char& left, char& right) { char tmp = left; left = right; right = tmp; } void Swap(double& left, double& right) { double tmp = left; left = right; right = tmp; } int main() { int a = 0, b = 1; char c = 'A', d = 'B'; double e = 1.1, f = 2.2; Swap(a, b); Swap(c, d); Swap(e, f); return 0; }
虽然C++支持函数重载,让我们可以同时实现多个类型的变量进行交换,但是这仍存在不好的地方:
C++为了解决以上问题,提出泛型编程的概念!!!
泛型编程是指编写与类型无关的通用代码,是代码复用的通用手段。模板是泛型编程的基础
其中模板分为函数模板和类模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
template <class T1, class T2>
返回值类型 函数名(参数列表)
{
//…
}
// 以上面介绍的Swap函数为例 template <class T> //模板参数列表 ——参数类型 void Swap(T& left, T& right) //函数参数列表 ——参数对象 { T tmp = left; left = right; right = tmp; }
注意:typename关键字可以替换class用于定义模板参数,但struct不能代替class。
函数模板并不是一个函数,是编译器用于产生特定类型函数的模具,所以模板将本应该我们进行的复杂操作交给了编译器执行。
思考一个小问题,主函数中的3个Swap函数是一样的吗?
template <class T> void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; } int main() { int a = 0, b = 1; char c = 'A', d = 'B'; double e = 1.1, f = 2.2; Swap(a, b); Swap(c, d); Swap(e, f); return 0; }
答案显然是不一样的,因为每个Swap函数参数类型都不相同,3个Swap函数会依据实参参数类型转换成不同的模板函数,如下图所示
同时我们通过汇编发现不同Swap函数Call的地址不同,也可以验证结果是正确的!
编译器编译阶段:编译器通过传入的实参类型来推演生成对应的模板函数。
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
template <class T> //模板参数列表 ——参数类型 T Add(const T& left, const T& right) //函数参数列表 ——参数对象 { return left + right; } int main() { int a1 = 10, a2 = 20; double d1 = 10.10, d2 = 20.20; // 隐式实例化 cout << Add(a1, a2) << endl; cout << Add(d1, d2) << endl; return 0; }
注意:cout << Add(a1, d2) << endl;
是不对的,因为两个实参类型不同,编译器无法推演出T为int还是double。
此时有两种处理方式:
(1)用户自己强制转换
cout << Add(a1, (int)d2) << endl; cout << Add((double)a1, d2) << endl;`
(2)使用显示实例化
cout << Add<int>(a1, a2) << endl; cout << Add<double>(d1, d2) << endl; cout << Add<int>(a1, d2) << endl; cout << Add<double>(a1, d2) << endl;
// 非模板函数 int Add(int left, int right) { return left + right; } // 函数模板 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数匹配,编译器不需要特化 Add<int>(1, 2); // 调用编译器特化的Add版本 }
Add(1, 2);
会直接与非模板函数匹配,因为编译器检测到存在专门处理int类型的Add函数,便不会用函数模板特化;
Add<int>(1, 2);
是显示实例化,编译器会根据函数模板特化出一个实例Add函数。
// 非模板函数 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函 数 }
Add(1, 2);
与非模板函数完全匹配,不需要模板实例化;
Add(1, 2.0);
虽然可以隐式类型转换,但这里可以用函数模板特化出更加匹配的Add(int, double)
函数。
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 };
以栈为对象,编写一个栈类模板,代码如下:
template <class T> class Stack { public: Stack(int capacity = 4) :_top(0) , _capacity(capacity) { _a = new T[capacity]; } ~Stack() { delete[] _a; _a = nullptr; _top = _capacity = 0; } private: T* _a; int _top; int _capacity; };
PS:此时如果想把类的成员函数在类中声明,类外定义,需要加模板参数列表
template <class T> class Stack { public: Stack(int capacity = 4) :_top(0) , _capacity(capacity) { _a = new T[capacity]; } // 在类中声明,类外定义 ~Stack(); void Push(const T& x); private: T* _a; int _top; int _capacity; }; // 析构函数在类外定义 template <class T> Stack<T>::~Stack() { delete[] _a; _a = nullptr; _top = _capacity = 0; } // 插入函数在类外定义 template <class T> void Stack<T>::Push(const T& x) { //... }
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
int main() { Stack<int> s1; Stack<double> s2; Stack<char> s3; Stack<int*> s4; return 0; }
注意:在之前学习类的时候,类名和类型是一样的。但在类模板中,以栈为例,Stack
为类名,Stack<int>
,Stack<double>
,Stack<char>
和Stack<int*>
为类型