admin管理员组

文章数量:1794759

模板的那丢丢事儿

模板的那丢丢事儿

注意:以下所有代码均在VS2010环境下调试运行

一、模板的引入

首先,请你认真思考一个问题,如何编写一个通用的加法函数呢?

1.对于我来说,首先第一反应是写一个宏函数来实现此功能,如:

#define ADD(x,y) ((x) + (y)) 那么,这样写的缺点是什么呢?

(1)它没有参数检测,这个原因致使它能够完成该要求,同时也造就了它致命的缺陷,不能进行参数检测,故安全性不高。

(2)它不能像函数一样进行调试,而仅仅是在编译期间进行简单的参数替换,况且如果代码过长,会大幅增加代码量。

(3)宏函数可能会有副作用,在不该替换的时候进行替换,假设括号没加全。

(4)宏函数只能处理整数或枚举类型的数据,对于其他追加字符串,或其他外置类型的数据处理不了。

#include<iostream> using namespace std; #define ADD(x,y) ((x) + (y)) int main() { cout<<ADD(1,3)<<endl; cout<<ADD(1.3,2.5)<<endl; cout<<ADD('A','G')<<endl; system("pause"); return 0; }

运行结果:

错误证明:

2、运用前面提到的函数重载解决通用加法函数,针对每个所需相同行为的不同类型重新实现它。

#include<iostream> using namespace std; int Add(const int &_iLeft, const int &_iRight) { return (_iLeft + _iRight); } float Add(const float &_fLeft, const float &_fRight) { return (_fLeft + _fRight); } int main() { cout<<Add(1,3)<<endl; cout<<Add(1.3f,2.5f)<<endl; system("pause"); return 0; }

运行结果:

这种方法的缺点:

(1)只要有新类型出现,就要重新添加对应函数。 (2)除类型外,所有函数的函数体都相同,代码的复用率不高 (3)如果函数只是返回值类型不同,函数重载不能解决 (4)一个方法有问题,所有的方法都有问题,不好维护。

3、通过多态实现,使用公共基类,将需要用到的虚函数代码放在公共的基础类里面,通过基类的对象指针进行调用,派生类可重写也可不重写。

class B { public: virtual int add(int _x,int _y) { return (_x+_y); } virtual float add(float _x,float _y) { return (_x+_y); } }; class INT_ADD:public B {}; class FLOAT_ADD:public B {}; int main() { B *b; INT_ADD i; FLOAT_ADD f; b = &i; cout<<b->add(1,3)<<endl; b = &f; cout<<b->add(1.8f,3.6f)<<endl; system("pause"); return 0; }

运行结果:

缺点:

(1)借助基类虚函数来编写适合各种类型的加法函数,只要有新类型出现,就要重新添加对应函数,代码利用率不高;(2)对于以后实现的许多派生类,都必须调用各自某个特定的基类虚函数,代码维护更加困难。

4、通过模板解决。

 模板的分类:模板分为函数模板和类模板,他们分别允许用户构造模板函数和模板类。

      

既然上面的几种方法都有各自的缺陷,于是就引入了模板的概念,使用我们的模板解决上述问题,那将是最好不过了,下面用函数模板实现通用加法函数。

template <typename T> T ADD(T x,T y) { return (x+y); } int main() { cout<<ADD(1,3)<<endl; cout<<ADD(1.3,2.5)<<endl; system("pause"); return 0; }

运行结果:

优点:利用模板机制可以显著减少冗余信,能大幅度的节约程序代码,进一步提高面向对象程序的可重用性和可维护性。

二、函数模板

函数模板:

        所谓函数模板,实际上是建立了一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板(注意它不是真正的函数)。代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

(一)函数模板声明的一般格式:

注意:不能使用struct代替typename。

(二)函数模板的使用

(1)模板函数的实例化:

       当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参,此模板实参来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建模板的一个新“实例”,此过程称为模板的实例化。

函数模板的实例化有两种调用方式,即显式实例化和隐式实例化。

template <typename T> T ADD(T x,T y) { return (x+y); } int main() { cout<<ADD(1,3)<<endl;//隐式实例化 cout<<ADD<float>(1.3f,2.5f)<<endl;//显式实例化 system("pause"); return 0; }

自定义类型不能直接用模板函数进行实例化,除非自己实现重载。

注意:模板被编译了两次:

a>实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号

b>在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持     某些函数调用

(2)模板函数的使用规则

a>模板函数也可以定义为内联函数

template <typename T> inline T ADD(T x,T y) { return (x+y); } 注意:inline关键字必须放在模板形参表之后,返回值之前,不能放在template之前

b>模板函数的不足之处:不能自动进行类型转换,由于需要生成代码,所以编译速度慢

要解决上述类型不同的问题,有以下三种方法:

1,采用显式实例化方式

template <typename T> T ADD(T x,T y) { return (x+y); } int main() { cout<<ADD<int>(1,'d'); system("pause"); return 0; }

2,将其中一个参数进行强制类型转换

template <typename T> T ADD(T x,T y) { return (x+y); } int main() { cout<<ADD(1,(int)'d'); system("pause"); return 0; }

3,多带一个参数模板(在类型中允许使用多个类型参数)

template <typename T,typename G> T ADD(T x,G y) { return (x+y); } int main() { cout<<ADD(1,'d'); system("pause"); return 0; }

c>在template语句与函数模板定义语句之间不允许插入别的语句。

template <typename T> int i;//错误,在template语句与函数模板定义语句之间不允许插入别的语句 T ADD(T x,T y) { return (x+y); }

d>模板参数

<1>【实参推演】        从函数实参确定模板形参类型和值的过程称为模板实参推断,即函数模板实例化过程中,编译器用函数实参来为我们推断模板实参的过程。

注意:多个类型形参的实参必须完全匹配

<2>【类型形参转换】

      一般不会转换实参以匹配已有的实例化,相反会产生新的实例(即b>提到的不能自动地进行类型转换)。

编译器只会执行两种转换: 1、const转换:接收const引用或者const指针的函数可以分别用非const对象的引用或者指针来调用。

template <typename T> T ADD(const T& x,const T& y) { return (x+y); } int main() { int a = 10; int b = 20; int &ra = a; int &rb = b; cout<<"a + b ="<<ADD(ra,rb)<<endl; system("pause"); return 0; } 运行结果:

2、数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。

template <typename T> T ADD(const T* x,const T* y) { return (*x + *y); } int main() { int a[5] = {0}; int b[5] = {5,4,3,2,1}; cout<<"a + b = "<<ADD(a,b)<<endl; system("pause"); return 0; } 运行结果:

<3>模板参数的分类

函数模板有两种类型参数:模板参数和调用参数

模板形参:类型形参和非类型形参

        

       模板形参名字只能在模板形参之后到模板声明或定义的末尾之间使用,遵循名字屏蔽规则,即我们常用的就近原则。

typedef int T;//将int类型重命名为T template <typename T>//函数模板 void Funtest(T t) { cout<<"t type = "<<typeid(t).name()<<endl;//打印形参t的类型 } T gloab;//定义全局变量gloab int main() { Funtest(10); cout<<"gloab type = "<<typeid(gloab).name()<<endl; system("pause"); return 0; } 代码分析:

模板形参的名字在同一模板形参列表中只能使用一次

template <typename T,typename T>//error C2991: 重定义 模板 参数“T” void Funtest(T t1,T t2) { } 所有模板形参前面必须加上class或者typename关键字修饰

template <typename T,G>//error C2061: 语法错误: 标识符“G” void Funtest(T t,G g)//error C2061: 语法错误: 标识符“G” {} 在函数模板的内部不能指定缺省的模板实参。

template<class T, U, typename V>// error C2061: 语法错误: 标识符“U” void f(T, U, V);// error C2061: 语法错误: 标识符“U”

<4>常见的函数模板声明:

template<class T, U, typename V> void f1(T, U, V);//在函数模板的内部不能指定缺省的模板实参 template<class T> T f2(int &T);//形参的名字与模板的类型参数同名容易引起混淆 template<class T> T f3 (T, T);//正确 typedef int TYPENAME; template<typename TYPENAME> TYPENAME f4(TYPENAME);//类型重命名与模板的类型参数同名,使用时遵循就近原则

总结:

模板形参说明 1、模板形参表使用<>括起来 2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同 3、模板形参表不能为空 4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后 5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型 使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换 6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。 但关键字typename是作为C++标准加入到C++中的,旧的编译器可能不支持。

e>函数模板的重载

1、同一般函数一样,函数模板也可以重载。

template<typename T> T Max(const T& left, const T& right) { return left>right? left:right; } template<typename T> T Max(const T& a, const T& b, const T& c) { return Max(Max(a, b), c); }; int main() { cout<<Max(1,2)<<endl; cout<<Max(1.2,3.4,5.6)<<endl; system("pause"); return 0; }

运行结果:

2、模板函数与同名的非模板函数可以重载。   

       在这种情况下,函数的调用顺序是:首先寻找一个参数完全匹配的非模板函数,如果找到了就调用它此种情况下,编译器并不会为相应的函数模板产生匹配的模板函数,如果想让编译器为模板函数生成代码,则必须显示实例化,且生成的代码与普通函数不是同一份代码;若没有找到参数完全匹配的非模板函数,则寻找函数模板,将其实例化产生一个匹配的模板函数。

int Max(const int& left, const int & right)//普通函数 { return left>right? left:right; } template<typename T> T Max(const T& left, const T& right) { return left>right? left:right; } template<typename T> T Max(const T& a, const T& b, const T& c) { return Max(Max(a, b), c); }; int main() { cout<<Max(10, 20, 30)<<endl; cout<<Max<>(10, 20)<<endl; cout<<Max(10, 20)<<endl; cout<<Max(10, 20.12)<<endl; cout<<Max<int>(10.0, 20.0)<<endl; cout<<Max(10.0, 20.0)<<endl; system("pause"); return 0; } 运行结果:

注意:函数的所有重载版本的声明都应该位于该函数被调用位置之前。 【说明】 1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例 化为这个非模板函数。 2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板 函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。 3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用, 而且所有的模板参数都应该根据实参演绎出来。 4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

f>模板函数特化

 1、定义:

有时候并不总是能够写出对所有可能被实例化的类型都最合适的模板,在某些情况下,通用模板定义对于某个类型可能是完全错误的,或者不能编译,或者做一些错误的事情,这时候就需要对模板函数进行特化(即具体化)。

template <typename T> int Compare(T s1, T s2) { if(s1<s2) return -1; else if(s1>s2) return 1; else return 0; } int main() { char* str1 = "abcd"; char* str2 = "ghf"; cout<<Compare(str1,str2)<<endl; system("pause"); return 0; } 代码分析:

template <typename T> int Compare(T s1, T s2) { if(s1<s2) return -1; else if(s1>s2) return 1; else return 0; } template<>//模板函数的特化 int Compare<const char*>(const char* s1,const char* s2) { return strcmp(s1,s2); } int main() { const char* str1 = "abcd"; const char* str2 = "ghf"; cout<<Compare(str1,str2)<<endl; system("pause"); return 0; } 运行结果:

2、模板函数特化形式如下:

1>关键字template后面接一对空的尖括号<> 2>再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参 3>函数形参表 4>函数体 template<> 返回值 函数名<Type>(参数列表) {      // 函数体 }

3、特化的声明必须与特定的模板相匹配,否则将产生以下错误

其次,需要注意的是给函数模板传参时一定要与特化参数列表中的参数格式一模一样,否则有可能即使写了特化,运行过程中也不会调用。

template <typename T> int Compare(T s1, T s2) { if(s1<s2) return -1; else if(s1>s2) return 1; else return 0; } template<>//模板函数的特化 int Compare<const char*>(const char* s1,const char* s2)//注意参数类型为const char* { return strcmp(s1,s2); } int main() { char* str1 = "abcd";//定义实参类型为char* char* str2 = "ghf";//定义实参类型为char* cout<<Compare(str1,str2)<<endl; system("pause"); return 0; } 运行结果:

从上面的例子看,特化传参时一定要注意,不要因小失大。

4、假如少了模板形参表会出现什么情况呢?

int compare(int v1, int v2) { return 0; }

     相当于只是定义了一个普通函数,该函数含有返回类型和与模板实例化相匹配的形参表而已,所以还是细心最重要。

注意:

1.在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配, 如果不匹配,编译器将为实参模板定义中实例化一个实例。

2.特化不能出现在模板实例的调用之后,应该在头文件中包含模板特化的声明,然

后使用该特化版本的每个源文件包含该头文件。

三、类模板

与函数模板类似,模板类也是模板,必须以关键字template开头,后接模板形参表。

【普通顺序表】

typedef int DataType; //typedef char DataType; class SeqList { private : DataType* _data ; int _size ; int _capacity ; };

【模板类格式】 template<class 形参名1, class 形参名2, ...class 形参名n> class 类名 { ... };

template<typename T> class SeqList { private : T* _data ; int _size ; int _capacity ; }; // 以模板方式实现动态顺序表 template<typename T> class SeqList { public : SeqList(); ~ SeqList(); private : int _size ; int _capacity ; T* _data ; }; template <typename T> SeqList <T>:: SeqList() : _size(0) , _capacity(10) , _data(new T[ _capacity]) {} template <typename T> SeqList <T>::~ SeqList() { delete [] _data ; } void test1 () { SeqList<int > sl1; SeqList<double > sl2; } 【模板类的实例化】 只要有一种不同的类型,编译器就会实例化出一个对应的类。 SeqList<int > sl1; SeqList<double > sl2; 当定义上述两种类型的顺序表时,编译器会使用int和double分别代替模板形参,重新编写SeqList类,最后创建名为SeqList<int>和SeqList<double>的类。 【非类型的类模板参数】 // 静态顺序表 //template<typename T, size_t MAX_SIZE> template <typename T, size_t MAX_SIZE = 10> //带缺省模板参数 class SeqList { public : SeqList(); private : T _array [MAX_SIZE]; int _size ; }; template <typename T, size_t MAX_SIZE> SeqList <T, MAX_SIZE>::SeqList() : _size(0) {} void Test() { SeqList<int> s1; SeqList<int , 20> s2; } 注意:浮点数和类对象是不允许作为非类型模板参数的 【类模板的特化】 全特化 template <typename T> class SeqList { public : SeqList(); ~ SeqList(); private : int _size ; int _capacity ; T* _data ; }; template<typename T> SeqList <T>:: SeqList() : _size(0) , _capacity(10) , _data(new T[ _capacity]) { cout<<"SeqList<T>" <<endl; } template<typename T> SeqList <T>::~ SeqList() { delete[] _data ; } template <> class SeqList <int> { public : SeqList(int capacity); ~ SeqList(); private : int _size ; int _capacity ; int* _data ; }; // 特化后定义成员函数不再需要模板形参 SeqList <int>:: SeqList(int capacity) : _size(0) , _capacity(capacity ) , _data(new int[ _capacity]) { cout<<"SeqList<int>" <<endl; } 偏特化(局部特化) template <typename T1, typename T2> class Data { public : Data(); private : T1 _d1 ; T2 _d2 ; }; template <typename T1, typename T2> Data<T1 , T2>::Data() { cout<<"Data<T1, T2>" <<endl; } // 局部特化第二个参数 template <typename T1> class Data <T1, int> { public : Data(); private : T1 _d1 ; int _d2 ; }; template <typename T1> Data<T1 , int>::Data() { cout<<"Data<T1, int>" <<endl; } 偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限 制所设计出来的一个特化版本。 // 局部特化两个参数为指针类型 template <typename T1, typename T2> class Data <T1*, T2*> { public : Data(); private : T1 _d1 ; T2 _d2 ; T1* _d3 ; T2* _d4 ; }; template <typename T1, typename T2> Data<T1 *, T2*>:: Data() { cout<<"Data<T1*, T2*>" <<endl; } // 局部特化两个参数为引用 template <typename T1, typename T2> class Data <T1&, T2&> { public : Data(const T1& d1, const T2& d2); private : const T1 & _d1; const T2 & _d2; T1* _d3 ; T2* _d4 ; }; template <typename T1, typename T2> Data<T1 &, T2&>:: Data(const T1& d1, const T2& d2) : _d1(d1 ) , _d2(d2 ) { cout<<"Data<T1&, T2&>" <<endl; } void test2 () { Data<double , int> d1; Data<int , double> d2; Data<int *, int*> d3; Data<int&, int&> d4(1, 2); } 模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在。 【模板的分离编译】 解决办法: 1. 在模板头文件 xxx.h 里面显示实例化->模板类的定义后面添 加 template class SeqList<int >; 一般不推荐这种方法,一方面老编译器可能不支持,另一方 面实例化依赖调用者。(不推荐) 2. 将声明和定义放到一个文件 "xxx.hpp" 里面,推荐使用这种方法。 模板总结 【优点】 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。 增强了代码的灵活性。 【缺点】 模板让代码变得凌乱复杂,不易维护,编译代码时间变长。 出现模板编译错误时,错误信非常凌乱,不易定位错误。 模板参数--实现容器适配器 模板的模板参数--容器适配器 <下文实现>

本文标签: 事儿模板