目录
模板初阶
一、泛型编程
二、函数模板
2.1 函数模板概念
2.2 函数模板格式
2.3 函数模板的原理
2.4 函数模板的实例化
2.5 模板参数的匹配原则
三、类模板
3.1 类模板的定义格式
3.2 类模板的实例化
模板初阶
一、泛型编程
代码示例 1.1
// C语言实现多种参数类型的同一交换函数Swap // 函数名不能相同,传地址需要创建指针变量 void SwapI(int* left, int* right) { int temp = *left; *left = *right; *right = temp; } void SwapD(double* left, double* right) { double temp = *left; *left = *right; *right = temp; } void SwapC(char* left, char* right) { char temp = *left; *left = *right; *right = temp; } // C++实现多种参数类型的同一交换函数Swap // 函数同名可以重载,可以使用引用无需再创建变量 void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } void Swap(double& left, double& right) { double temp = left; left = right; right = temp; } void Swap(char& left, char& right) { char temp = left; left = right; right = temp; }代码示例1.1中是我们在C语言中会碰到的经典问题,在C++中得到了一定的改善,无需为函数名烦恼,也无需为了传地址额外创建指针变量,引用即是对象本身,不额外开辟空间。
但使用函数重载仍然显得冗余,还有几个不好的地方:
- 1. 重载的函数仅仅是类型不同,函数体基本一致,代码复用率低,只要有新类型出现,用户就得手动增加对应的函数。
- 2. 代码的可维护性低,一个出错可能所有的重载均出错。
能否告诉编译器一个模子,让编译器根据不同的类型,利用该模子来生成代码?
像钢水浇筑工艺,一个通用的模板(函数体逻辑),浇筑不同的材料(参数类型),生成不同材料的铸件(具体类型的代码)。
在C++中,将其称为泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
二、函数模板
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,使用时根据实参类型确定参数的具体类型,生成具体函数。需要强调,有的地方也会称呼函数模板为模板函数,但需要严格区分,函数模板是模板,而不是函数。
2.2 函数模板格式
代码示例 2.2.1
// 1、在函数头上加 template<> 表明这是一个函数模板 // 2、在尖括号<>内可以声明一些通用类型给函数使用 // 3、typename和class都是定义模板参数类型的关键字(不可使用struct) // 4、class是早期使用的关键字,typename是后面加的,两者没有区别 // 5、模板参数类型的名称随意,Type的缩写是T所以用T,T1、T2等,也有用Key的缩写K的 // 6、T1、T2、Tn、K等模板参数类型,不是int、char等具体类型,使用时根据情况替换 template<typename T1, class T2, ......, typename Tn, typename K> 返回值类型 函数名( 函数参数列表 ) { // ... } // 例如Swap交换函数模板 // 1、template<>表明下方定义的是函数模板 // 2、尖括号<>内是通用类型声明,不是具体类型,使用时具体类型代替通用类型 // 3、若实际调用Swap时,实参是int类型,则int类型替换T类型 template<typename T> void Swap(T& left, T& right) { T temp = left; left = right; right = temp; } // 或是Add加法函数模板 // 可以定义多个模板参数类型T1、T2 template<typename T1, typename T2> T1 Add(T1& left, T2& right) { return left + right; }2.3 函数模板的原理
函数模板是蓝图,它本身不是函数,是编译器在实际使用时,根据实参类型产生相应有具体类型的函数代码的模板。所以模板就是将本应我们做的重复工作交给了编译器。
在编译阶段,对于函数模板,编译器会根据传入的实参进行推演,生成对应类型的函数代码以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的函数代码,再调用该函数完成运算,对于字符等其他类型也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板生成具体函数代码,称为函数模板的实例化。模板参数类型的推演过程,称为模板参数实例化,模板参数可以隐式实例化,也可以显式实例化。
1、隐式实例化:让编译器根据实参类型推演模板参数类型
代码示例 2.4.1
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { // 隐式实例化 int a1 = 10, a2 = 20; double d1 = 10.0, d2 = 20.0; // a1、a2都为int,推演出T为int Add(a1, a2); // d1、d2都为double,推演出T为double Add(d1, d2); // a1为int,d1为double,只有一个模板参数类型T // Add(a1, d1); // 1、手动进行强制类型转换 // Add(a, (int)d); // Add((double)a, d); return 0; }Add(a1, d1);不能通过编译。因为在编译期间,函数模板实例化时,编译器需要推演实参类型,通过实参a1推演T为int,通过实参d1推演T为double,但模板参数列表中只有一个T,无法确定T是int还是double,存在歧义而报错。在模板中,编译器不会擅自进行类型转换操作,无法自主选择将a1类型转换成double,或是将d1类型转换成int。
除开定义多个模板参数类型,此时有两种处理方式:
- 用户自己进行强制类型转化,如上面的代码示例2.4.1;
- 显式实例化模板参数,如下方的代码示例2.4.2。
2、显式实例化:调用函数时,在函数名后使用尖括号<>指定模板参数的实际类型
代码示例 2.4.2
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a = 10; double b = 20.0; // 显式实例化,函数名后使用尖括号<>指定模板参数T的类型为int Add<int>(a, b); return 0; } // 若函数模板的参数列表有多个参数,如: // template<typename T1, typename T2, typename T3> // 显式实例化时,格式如下: // FunctionName<int, double, char>(a, b, c); // 依次指定参数类型即可指定了模板参数类型,如果再发生类型不匹配,编译器就有依据对实参进行隐式类型转换,但如果实参无法类型转换成指定的模板参数类型,编译器将会报错。
必须显式实例化的情况
代码示例 2.4.3
#include<iostream> using namespace std; // 存在部分必须显式实例化的情况 // 例如func函数:n为元素个数,创建n个元素的连续空间,并将其地址作为返回值 template<class T> T* func(int n) { // T为元素的类型 return new T[n]; } int main() { // 需要存储5个int元素,可以通过func动态申请一块空间,通过指针接收空间地址 // 此处必须显式实例化,因为不可以从实参推断模板参数T的具体类型 int* ptr = func<int>(5); for (int i = 0; i < 5; i++) ptr[i] = i + 1; for (int i = 0; i < 5; i++) { cout << ptr[i] << endl; } return 0; }运行结果:
2.5 模板参数的匹配原则
- 1.有具体类型的函数可以和它的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
代码示例 2.5.1
#include<iostream> using namespace std; // 专门处理int的Add加法函数,非模板函数 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } // Add加法函数模板 template<class T> T Add(T left, T right) { cout << "T Add(T left, T right)" << endl; return left + right; } // 特化:函数模板实例化为有具体类型的函数,称为特化 int main() { // 编译器处理函数与模板特化的原则是:有现成的用现成的,有更匹配的用更匹配的 // 与非模板函数匹配,编译器不需要特化 cout << Add(1, 2) << endl; // 通过显式实例化调用编译器特化的Add版本 cout << Add<int>(3, 4) << endl; return 0; }运行结果:
- 2. 对于非模板函数和函数模板的特化,如果其他条件都相同,优先调用非模板函数,而非模板的特化。如果模板可以产生一个更匹配的函数,那么将选择模板的特化。
代码示例 2.5.2
#include<iostream> using namespace std; // 专门处理int的Add加法函数,非模板函数 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } // Add加法函数模板 template<typename T1, typename T2> T1 Add(T1 left, T2 right) { cout << "T1 Add(T1 left, T2 right)" << endl; return left + right; } int main() { // 与非模板函数完美匹配,编译器无需特化 cout << Add(1, 2) << endl; // 函数模板可以生成更匹配的版本,编译器根据实参生成更加匹配的Add函数 // int Add(int left, double right) cout << Add(2, 3.1) << endl; return 0; }一句话总结:有现成的用现成的,有更匹配的用更匹配的。
如果现成的就能匹配上,编译器就不会对函数模板进行特化,只会调用现成的。如果现成的不是特别匹配,但函数模板可以生成更匹配的实例,编译器就会对函数模板进行特化,调用生成的这个更匹配的函数。
- 3. 函数模板在使用时不允许自动类型转换,但普通函数可以进行自动类型转换。
代码示例 2.5.3
#include<iostream> using namespace std; //// Add加法函数模板 //template<typename T1, typename T2> //T1 Add(T1 left, T2 right) //{ // cout << "T1 Add(T1 left, T2 right)" << endl; // return left + right; //} // 专门处理int的Add加法函数,非模板函数 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; cout << "left:" << left << " right:" << right << endl; return left + right; } int main() { // 函数模板使用时,类型不匹配,只能1、手动类型强转或2、显式实例化 // 1、Add(a, (int)d); 2、Add<int>(a, b); // 普通函数调用时,编译器无需判断参数类型,可以自动进行类型转换 // 函数形参left、right都是int,则double类型的实参3.1自动转换为int类型 cout << Add(2, 3.1) << endl; return 0; }运行结果:
三、类模板
类模板代表一个类家族,该类模板与类型无关,使用时编译器根据需要生成有具体类型的类的代码。由于类是类型,和函数不同,不需要传参,无从推演模板参数的类型,因此使用时模板参数必须显式实例化。需要强调,有的地方也会称呼类模板为模板类,但需要严格区分,类模板是模板,而不是类。
3.1 类模板的定义格式
代码示例 3.1.1
// 1、在头上写template<>,表明下方是类模板的定义 // 2、typename和class两个关键字都可以定义模板参数类型 // 3、类的定义格式照常,除了不能用具体类型,而用模板参数的通用类型代替 // 4、和函数模板一样,T1、T2、Tn只是名称,只是代号,命名是随意的,但用T命名更大众化 template<typename T1, class T2, ..., typename Tn> class 类模板名称 { // 类内成员定义 };代码示例 3.1.2
#include<iostream> using namespace std; // 类模版:是模板,不是类 // 模板参数T为栈中存储的数据的类型 template<typename T> class Stack { public: // 默认构造 Stack(size_t capacity = 4) :_arr(new T[capacity]) ,_capacity(capacity) ,_top(0) {} // 析构 ~Stack() { delete[] _arr; _arr = nullptr; _top = _capacity = 0; } // 类模板中成员函数的声明与定义分离:压栈方法的声明 void Push(const T& data); void Print() { for (size_t i = 0; i < _top; i++) { cout << _arr[i] << " "; } cout << endl; } private: T* _arr; // 栈的存储空间 size_t _capacity; // 空间大小 size_t _top; // 栈顶 }; // 使用模版就不建议成员函数的声明与定义分离到两个文件.h头文件和.cpp源文件,会出现链接错误 // 类模板中成员函数的声明与定义分离:压栈方法函数模板的定义 template<class T> void Stack<T>::Push(const T& data) { // 扩容 if (_top == _capacity) { T* tmp = new T[2 * _capacity]; memcpy(tmp, _arr, sizeof(T) * _top); delete[] _arr; _arr = tmp; _capacity *= 2; } // 数据入栈,同时更新栈顶 _arr[_top++] = data; } int main() { Stack<int> st1; // int st1.Push(1); st1.Push(2); st1.Push(3); st1.Push(4); st1.Print(); Stack<double> st2; // double st2.Push(1.1); st2.Push(2.1); st2.Push(3.1); st2.Push(4.1); st2.Print(); return 0; }运行结果:
3.2 类模板的实例化
类模板的实例化与函数模板的实例化不同。类模板实例化必须显式实例化,在类模板名字后跟尖括号<>,括号内指定模板参数类型。单纯类模板的名字(如Stack)不是真正的类,只有有具体类型,完成实例化的结果(如Stack<int>、Stack<double>)才是真正的类。
// Stack是类模板名,是模板的名称 // Stack<int>、Stack<double>才是类,类的名称,类类型的名称 Stack<int> st1; // int Stack<double> st2; // double