- 一. 泛型编程
- 二.函数模板
- 1.基础知识
- 2.函数模板的实例化
- (1)隐式实例化
- (2)显式实例化:
- (3)模板参数的匹配原则
- 3.多个模板参数
- 4.模板参数也可以给缺省值
- 三.类模板
- 1.基础知识
- 2.类模板的实例化
- 3.类模板成员函数实现的格式
- 三.模板的声明和定义
如何实现一个通用的交换函数呢?
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; }
使用函数重载虽然可以实现,但是有以下几个不好的地方:
2. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
3. 代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来自动生成代码呢?
泛型编程: 编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
模板分为函数模板和类模板,下面将会依次介绍。
二.函数模板 1.基础知识函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板格式:
template
返回值类型 函数名(参数列表){}
templatevoid Swap(T& left, T& right) { T temp = left; left = right; right = temp; }
注意:typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于int类型和字符类型也是如此。
函数模板是没有地址的,程序调用的是通过模板生成的函数,而不是调用模板。
2.函数模板的实例化用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
(1)隐式实例化所谓的隐式实例化就是让编译器根据实参推演模板参数的实际类型。(,模板类型也可以做返回值,但是我们只能根据实参去推,而不能根据返回值去推模板类型)
template(2)显式实例化: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; Add(a1, a2); Add(d1,d2); return 0; }
所谓的显示实例化就是指在函数名后的<>中指定模板参数的实际类型。
函数模板的类型就一定是推演的吗?
函数模板中有T类型的参数,那么编译器可以根据我们传的参数来推演T究竟是个什么类型,但是如果函数模板中没有T类型的参数,那么编译器就没法根据我们传的参数来推演T究竟是个什么类型了。
比如下面这段代码,int类型的形参n接受了我们传递的10,因为形参中没有T类型的参数,所以编译器无法推演出T的类型,所以编译器报错了。
此时我们就可以用显示实例化来解决“因为没有T类型的形参,导致编译器无法根据实参来推演类型”这个问题。
所以如果编译器无法自动推演类型,那么我们就需要显示实例化,指定模板参数。
如果类型不匹配,这就需要我们显示实例化了,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
templateT Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10, a2 = 20; double d1 = 10.0, d2 = 20.0; Add(a1, d2); // 此时有两种处理方式: //1. 用户自己来强制转化 Add(a1, (int)d2); Add((double)a1,d2); //2. 使用显式实例化 Add (a1,d2);//double隐式类型转换为int Add (a1,d2);//int隐式类型转换为double //我指定T就是int或者double,不需要编译器去推。 return 0; }
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
(3)模板参数的匹配原则一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 templateT Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数匹配,编译器不需要特化 Add (1, 2); // 调用编译器特化的Add版本 }
对于非模板函数和同名函数模板,它们可以同时存在,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例,但是如果你想使用模板函数,那么只需要显示实例化即可。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } template3.多个模板参数T1 Add(T1 left, T2 right) { return left + right; } void Test() { Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add (1,2);//使用的是模板函数 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数 }
模板函数也可以有多个模板参数,如下:
template4.模板参数也可以给缺省值void Func(const K& key, const V& value) { } int main() { //隐式实例化 Func(1, 1);//推出K是int,V也是int Func(1, 1.1);//推出K是int,V是double //显示实例化 Func (1, 'A'); return 0; }
和参数一样,缺省值必须从右往左缺省,可以全缺省,也可以半缺省
templatevoid Func(const K& key, const V& value) { } int main() { Func(1, 1);//推出K是int,V也是int Func(1, 1.1);//推出K是int,V是double Func (1, 'A'); return 0; }
如果是全缺省,就不需要给参数了
template三.类模板 1.基础知识void Func(const K& key, const V& value) { } int main() { Func(1, 1); return 0; }
templateclass 类模板名 { // 类内成员定义 };
// 动态顺序表 // 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具 template2.类模板的实例化class Vector { public : Vector(size_t capacity = 10) : _pData(new T[capacity]) , _size(0) , _capacity(capacity) {} // 使用析构函数演示:在类中声明,在类外定义。 ~Vector(); void PushBack(const T& data); void PopBack(); // ... size_t Size() {return _size;} T& operator[](size_t pos) { assert(pos < _size); return _pData[pos]; } private: T* _pData; size_t _size; size_t _capacity; }; // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表 template Vector ::~Vector() { if (_pData) delete[] _pData; _size = _capacity = 0; }
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。函数模板需要传参,类模板不需要传参
// Vector类名,Vector才是类型 Vector s1; Vector s2; //它们不是同一个类,类中的T将会被替换为int或者double
要注意类模板与函数模板的区别,函数模板的类型一般是编译器根据所传的参数推演而来的,类模板的类型是明确指定的,指定了类型的类模板就成为了真正的类,编译器会把其中的T转化为我们所指定的类型。
3.类模板成员函数实现的格式类模板中成员函数的实现也需要加类域,形式如下:
template三.模板的声明和定义class Vector { public: Vector(size_t capacity = 10); void PushBack(const T& x); private: int a; }; template void Vector ::PushBack(const T& x) { } template Vector ::Vector(size_t capacity) { }
值得注意的是模板可以声明和定义分离,但是模板是不支持声明和定义放到两个文件中(.h和.cpp)的,会出现链接错误
为什么模板声明和定义分离了,它们连接不上呢?
因为在.cpp中定义函数模板的时候,T究竟是什么还没有确定,所以编译器无法对这个文件进行处理,没办法获取函数模板的地址,因此在链接阶段,就没有办法找根据模板生成的函数的地址,因此会出现链接错误。
那我们该如何处理模板的声明和定义呢?
解决方案1:
在template.cpp(模板的定义文件)中通过显示实例化来告诉编译器我们要使用的模板类型T是什么,这样编译器就可以找到根据模板生成的函数的地址,就可以链接上了。
就比如我们在主函数中需要使用参数为int类型的Swap函数,以及T为int和T为double的Vector类,那么我们就可以在template.cpp中对它们进行显示实例化来告诉编译器我们要使用的模板类型,编译器将它们带入定义中,就可以获取根据模板所生成的函数地址,从而链接成功,如下:
template void Swap(int& left,int& right); template class Vector ; template class Vector ;
解决方案2:
我们可以将模板的声明和定义放在一个头文件中,有些地方会把头文件后缀改为hpp意为声明和定义在一个文件中,这只是命名上的一个规范,没有这个的强制要求,后缀就是.h也是可以的,但是.hpp的寓意更好。
为什么放到一起就没有链接错误了?
因为主函数所在文件中包含了.h/.hpp文件,也就是说主文件包含了声明和定义,在调用函数模板所生成的函数时就已经实例化了,编译器就直接获得了地址,根本不需要去其它文件中找地址,即不需要链接了,所以也就没有了链接错误。