C++泛型编程2——类模板,容器适配器,仿函数
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用模板时,比编译器才生成代码。
这一特性影响了我们如何组织代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。
类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。
因此我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常即包括声明也包括定义。
总结一下:
模板的具体实现被称为实例化或具体化。
因为模板不是函数,他们不能单独编译,模板必须与特定的模板实例化请求一起使用。
因此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
编译错误报告
大多数编译错误在实例化期间报告
模板知道实例化时才生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。
通常,编译器会在三个阶段报告错误:
1.第一个阶段是编译模板本身时。
在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分好或者变量名写错等等。
2.第二个阶段是编译器遇到模板使用时:
在此阶段,编译器仍然没有很多检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。他还能检查参数类型是否匹配,对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
3.第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。
依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
类模板
模板类也是模板,必须以关键字template开头,后接模板形参表。
//模板类格式 template class 类名 { ... };
关键字template告诉编译器,将要定义一个模板,尖括号中的内容相当于函数的参数列表。
下面用模板类实现动态顺序表
以模板方式实现动态顺序表
template class seqlist { public : seqlist(); ~ seqlist(); private : int _size ; int _capacity ; t* _data ; }; template seqlist :: seqlist() : _size(0) , _capacity(10) , _data(new t[ _capacity]) {} template seqlist ::~ seqlist() { delete [] _data ; } void test1 () { seqlist sl1; seqlist sl2; }
模板类的实例化
类模板是用来生成类的蓝图的。
与函数模板的不同之处是:编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
显式模板实参列表
只要有一种不同的类型,编译器就会实例化出一个对应的类。
seqlist sl1; seqlist sl2;
当定义上述两种类型的顺序表时,编译器会使用int和double分别代替模板形参,重新编写seqlist类,最后创建名为seqlist和seqlist的类。
当编译器从我们的seqlist模板实例化出一个类时,他会重写seqlist模板,将模板参数t的每个实例替换为给定的模板实参。
上面的代码中,编译器生成了两个不同的类。
一个类模板的每个实例都形成一个独立的类。
类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的成员函数本身是一个普通函数,但是类模板的每个实例都有其自己版本的成员函数,因此类模板的成员函数具有和模板相同的模板参数。
因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
类模板成员函数实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
如果一个成员函数没有被使用,则他不会被实例化。
成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。
就是在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
template class seqlist { public : seqlist(); ~ seqlist(); seqlist& operator++() {} private : int _size ; int _capacity ; t* _data ; };
为了举例子,我没有实现++运算符重载函数,但是要注意的其实是,返回值seqlist&,而不是seqlist&。
当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像是我们已经提供了与模板参数匹配的实参一样。
在类模板外使用类模板名
当我们在类模板外定义其成员时,我们并不在类的作用域中,知道遇到类名才表示进入类的作用域。
所以在类外定义函数时:
seqlist& seqlist::operator++() {}
非类型模板参数
类似于函数模板,类模板也可以有非类型模板参数。
我们现在来使用非类型模板参数构造一个顺序表:
template class seqlist { private: t a[n]; }
上面的代码就实现了一个数组,剩下的函数我都省略了。
模板参数——实现容器适配器
已经知道,模板可以包含类型参数和非类型参数,现在还要加一个,类模板可以包含本身,也就是模板的参数。
这种参数是模板新增的特性,用于实现stl。
看下面的代码:
template class seqlist { private : int _size ; int _capacity ; t* _data ; }; // template class container> //不带缺省参数 template class container = seqlist> // 缺省参数 class stack { public : void push(const t& x ); void pop(); const t& top(); bool empty(); private : container _con; }; void test() { stack s1; stack s2; }
在上面的代码中,我们使用了模板参数:
template class container
使用template来标示这个参数是一个模板参数。
在上面的例子中,我们给了它一个缺省参数为seqlist,如果不给缺省参数,直接传参数也是可以的。
上面的例子中,我们使用顺序表构造了一个栈的类型。
这就是stl中的容器适配器。
下面我们简单介绍一下stl六大中的配接器。
stl
stl是标准模板库的英文缩写,stl有六大组件:
1.容器
2.算法
3.迭代器
4.空间配置器
5.配接器
6.仿函数
这篇中我们只介绍配接器与仿函数,剩下的不做提及。
配接器
配接器在stl组件的灵活组合运用功能上,扮演者转换器的角色。
配接器分为:
1.应用于容器的container adapters
2.应用于迭代器的iterator adapters
3.应用于仿函数的:functor adapters
容器配接器
首先在上面的代码:
template class seqlist { private : int _size ; int _capacity ; t* _data ; }; // template class container> //不带缺省参数 template class container = seqlist> // 缺省参数 class stack { public : void push(const t& x ); void pop(); const t& top(); bool empty(); private : container _con; }; void test() { stack s1; stack s2; }
我们就实现了容器配接器。
在stl中,stl提供的两个容器queue和stack,其实都是一种配接器。他们修饰deque的接口而成就出另一种容器风貌。
仿函数
functor adapters是所有配接器中数量最庞大的一个族群,它的价值在于,通过他们之间的绑定,组合,修饰能力,几乎可以无限制的创造出各种可能的表达式,搭配stl算法一起。
我以冒泡排序为例:
void bubblesort(int *a,int size) { assert(a); int max = a[0]; for(int i = 0; i < size;i++) { int ret = -1; for(int j = i ;j < i;j++) { if(a[j] > a[j+1]) { ret = 1; swap(a[j],a[j+1]); } } if(ret == -1) break; } }
上面的代码实现了递增排序的冒泡排序,那么如果我们想要递减呢?
我们还得再去定义一个冒泡排序,这样十分不方便,但是我们可以用实现仿函数解决这个问题。
class greater { bool operator()(int a,int b) { return a>b?true:false; } } class less { bool operator()(int a,int b) { return a void bubblesort(int *a,int size) { com com; assert(a); int max = a[0]; for(int i = 0; i < size;i++) { int ret = -1; for(int j = i ;j < i;j++) { if(com(a[j],a[j+1]) { ret = 1; swap(a[j],a[j+1]); } } if(ret == -1) break; } }
看上面的代码,我们可以分析一下
我们通过参数com生成了一个对象,这个对象默认为greater,我们重载了greater和less类的()运算符,给他传入两个参数以判断大小。
通过上面的代码就实现了仿函数。
类模板的特化
全特化
template class seqlist { public : seqlist(); ~ seqlist(); private : int _size ; int _capacity ; t* _data ; }; template seqlist :: seqlist() : _size(0) , _capacity(10) , _data(new t[ _capacity]) { cout<<"seqlist" < seqlist ::~ seqlist() { delete[] _data ; }
我们定义了seqlist类,下面对它进行全特化:
template <> class seqlist { public : seqlist(int capacity); ~ seqlist(); private : int _size ; int _capacity ; int* _data ; }; // 特化后定义成员函数不再需要模板形参 seqlist :: seqlist(int capacity) : _size(0) , _capacity(capacity ) , _data(new int[ _capacity]) { cout<<"seqlist" <::~ seqlist() { delete[] _data ; } void test1 () { seqlist sl2; seqlist sl1(2); }
顾名思义,全特化就是对模板参数列表中的所有参数都进行特化,不论有几个参数,都要进行特化,这个函数模板的特化相同。
偏特化
template class data { public : data(); private : t1 _d1 ; t2 _d2 ; }; template data::data() { cout<<"data" < class data { public : data(); private : t1 _d1 ; int _d2 ; }; template data::data() { cout<<"data" <下面的例子可以看出,偏特化并不仅仅是指特殊部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
局部特化两个参数为指针类型// 局部特化两个参数为指针类型 template <typename t2="" typename=""> class data <t1*,> { public : data(); private : t1 _d1 ; t2 _d2 ; t1* _d3 ; t2* _d4 ; }; template <typename t2="" typename=""> data<t1>:: data() { cout<<"data<t1*,>" <<endl; pre="">;>,>,>;>,>
局部特化两个参数为引用
// 局部特化两个参数为引用 template class data { public : data(const t1& d1, const t2& d2); private : const t1 & _d1; const t2 & _d2; t1* _d3 ; t2* _d4 ; }; template data:: data(const t1& d1, const t2& d2) : _d1(d1 ) , _d2(d2 ) { cout<<"data" < d1; data d2; data d3; data d4(1, 2); }
模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在。
模板的分离编译
解决办法:
1.在模板头文件 xxx.h 里面显示实例化->模板类的定义后面添加 template class seqlist; 一般不推荐这种方法,一方面老编译器可能不支持,另一方面实例化依赖调用者。(不推荐) 2.将声明和定义放到一个文件 “xxx.hpp” 里面,推荐使用这种方法。
总结
优点
模板复用了代码,节省资源,更快的迭代开发,c++的标准模板库(stl)因此而产生。增强了代码的灵活性。
缺点
模板让代码变得凌乱复杂,不易维护,编译代码时间变长。 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
上一篇: oracle服务器的结构和组成讲解
下一篇: redis中事务机制及乐观锁的实现