程序员人生 网站导航

[置顶] “懒人”的福音---泛型编程

栏目:php教程时间:2016-07-22 08:48:23

       懒得1步1步走楼梯,因而有了电梯;懒得走路,因而他们制造出了汽车、火车、飞机;懒得去计算,因而发现了计算器;懒得重复写代码,因而有了C++当中的泛型编程!

       固然,上面那段话是我瞎掰的,真实情况可能完全不1样,不过却也能够很好地引出今天所要讲的内容---C++中的泛型编程。其它的话也不多说了,开始进入正题吧!今天主要分析1下在泛型编程中的:1、模板函数&模板形参&函数重载 2、模板类  3、模板特化 等概念,最后再说1说模板分离编译

       在没有学习泛型编程,也不知道模板这个概念的时候,如果有人让你写1个通用的计算公式,具体1点就比如让你实现1个通用的加法函数,你会怎样写?通常我们会有以下几种解决方法:

       第1种,也是在没有学习泛型编程的时候最容易想到的方法,使用函数重载。

int Add(const int &_iLeft, const int &_iRight) //整形加法 { return (_iLeft + _iRight); } float Add(const float &_fLeft, const float &_fRight) //浮点类型加法 { return (_fLeft + _fRight); }
       虽然这类方法很容易想到,实现起来也不难,但是也存在很大的缺点,在我看来,它就有以下4个缺点(固然也许还有更多缺点)缺点1:只要有新类型出现,就要重新添加对应函数,太麻烦。缺点2:代码的复用率低。缺点3:如果函数只是返回值类型不同,函数重载不能解决(函数重载的条件:同1作用域,函数名相同,参数列表不同)。缺点4:1个方法有问题,所有的方法都有问题,不好保护

       既然第1种方法不好,那末我们来看1看第2种方法,使用公共基类,将通用的代码放在公共的基础类里面,这类方法也很容易理解,这里就不举例子了,值得1提的是,这类方法也有缺点。首先:借助公共基类来编写通用代码,将失去类型检查的优点,其次:对以后实现的许多类,都必须继承自某个特定的基类,代码保护更加困难

       还有1种方法就是:用特殊的预处理程序,如:

#define ADD(a, b) ((a) + (b))

       不过这类方法局限性太大,而且我们不太提倡使用宏替换的方式,本身宏就有很多的缺点,虽然看上去简洁明了,但是它既不进行参数类型检测,同时它也不容易保护,安全性不高,总之,在C++当中能不使用宏的时候就尽可能不使用,最好多用const或inline关键字来起到宏的效果。    

       基于以上种种限制或说是缺点,C++当中的泛型编程也就应运而生。甚么是泛型编程呢?我们首先来了解1下甚么是泛型编程,简单来概括1下:编写与类型无关的逻辑代码,是代码复用的1种手段。换句话说泛型编程就是以独立于任何特定类型的方式编写代码,而模板是泛型编程的基础。同时又有1个概念模板,甚么是模板呢?来看1张图吧:


                                                                 函数模板     

       下面我们来1个1个分析,首先是函数模板,甚么是函数模板呢?函数模板:代表了1个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。既然函数模板是1个独立于类型的函数,可以产生函数的特定类型版本的东西,那末它怎样样使用呢?

        首先我们来看1下函数模板的格式:

template<typename Param1, typename Param2,...,class Paramn>//返回值类型 函数名(参数列表) { ... }

             固然在格式的处理当中,有几点不能不说:

       typename是用来定义模板参数关键字,也能够使用class。但是建议尽可能使用typename。注意:不能使用struct代替typename

       另外值得1提的是:模板函数也能够定义为inline函数 

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

       说完了格式,自然我们需要用例子来简单说明1下:


       上面是1个简单地使用函数模板的做加法运算的例子,总共4个输出内容,前面两个没有甚么问题。不过这里有个实参推演的概念。

       实参推演:从函数实参肯定模板形参类型和值的进程称为模板实参推断,但是需要注意的是如果有多个参数,多个类型形参的实参必须完全匹配。否则编译就会出错。

       第3个输出语句如果34.12前面没有(int),结果编译通不过,缘由是:你传递两个不同类型参数给了函数模板,但是函数模板没法肯定模板参数T的类型,所以编译报错。

       但是为何是在编译期间出错了呢?换句话说就是函数模板在编译期间究竟做了甚么事情呢?其实函数模板的编译可以分为两个进程(也能够认为模板被编译了两次):

第1次在实例化之前,检查模板代码本身,查看是不是出现语法毛病,不过只是简单地检查,如:遗漏分号。
第2次在实例化期间,检查模板代码,查看是不是所有的调用都有效,如:实例化类型不支持某些函数调用。

       (上面又提出了实例化的概念,模板是1个蓝图,它本身不是类或函数,编译器用模板产生指定的类或函数的特定类型版本,产生模板特定类型的进程称为函数模板实例化

        如果我们这时候候在main函数之上再添加1个函数:

int Add(int left, int right) { return left + right; } int main() { ..... cout<<Add(1.2,2.5)<<endl; return; }

        那末在这时候候我们会想这次编译器会去调用哪个函数呢,是通用类型转化进而去调用我们后面定义的Add函数,还是会调用函数模板产生1个新的函数呢?在这里会不会进行类型形参转换呢?

       1般不会转换实参以匹配已有的实例化,相反会产生新的实例。
        编译器只会履行两种转换
        1、const转换:接收const援用或const指针的函数可以分别用非const对象的援用或指针来调用
        2、数组或函数到指针的转换:如果模板形参不是援用类型,则对数组或函数类型的实参利用常规指针转换。数组实参将当作指向其第1个元素的指针,函数实参当作指向函数类型的指针。

        所以上面的问题很容易得出答案了,编译器不会将1.2和2.5转换为int型,从而调用已有的Add版本,而是重新合成1个double的版本,固然条件是能够生成这么1个模板函数。如果这个模板函数没法生成的话,那末只能调用已有的版本了。

          接下来来看1看模板参数的概念:

        函数模板有两种类型参数:模板参数和调用参数,而模板参数又可以1分为2:

    

         关于模板参数,又有以下需要注意的地方:

1、模板形参名字只能在模板形参以后到模板声明或定义的末尾之间使用,遵守名字屏蔽规则。

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

3、所有模板形参前面必须加上class或typename关键字修饰,(需要注意:在函数模板的内部不能指定缺省的模板实参)

        接着说1说非模板类型参数的概念。

        甚么是非模板类型参数呢?非模板类型形参是模板内部定义的常量,在需要常量表达式的时候,可使用非模板类型参数。比如我们可以将数组的长度指定为非模板类型参数,来看下面1张图:


        在很多时候我们会遇到关于类型等价性的概念,甚么叫类型等价性呢?

const int iByteCnt = 9; int b[iByteCnt+1]; int a[10]; FunTest(a); // FunTest<int, 10> 两个数组等价 FunTest(b); // FunTest<int, 10> 编译器不会合成新的函数
        在这里对模板参数做1个总结:

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

       接下来就是模板函数重载,在这里我偷个懒,参考了1下网上的资料:

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() { Max(10, 20, 30); Max<>(10, 20); //相当于告知编译器,这里需要使用模板,而不是去调用第1个函数 Max(10, 20); Max(10, 20.12); //会产生隐式类型转化,因此会去调用第1个函数 Max<int>(10.0, 20.0); Max(10.0, 20.0); return 0; }
需要注意:函数的所有重载版本的声明都应当位于该函数被调用位置之前。

一样做1个总结:

1、1个非模板函数可以和1个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
2、对非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板函数而不会从该模板产生出1个实例。如果模板可以产生1个具有更好匹配的函数,
那末将选择模板

3、显式指定1个空的模板实参列表,该语法告知编译器只有模板才能来匹配这个调用,而且所有的模板参数都应当根据实参演绎出来。
4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

        最后来了解1下模板函数特化

有时候其实不总是能够写出对所有可能被实例化的类型都最适合的模板,在某些情况下,通用模板定义对某个类型多是完全毛病的,或不能编译,或做1些毛病的事情。

      可以下面这样来定义


模板函数特化情势以下:
1、关键字template后面接1对空的尖括号<>
2、再接模板名和1对尖括号,尖括号中指定这个特化定义的模板形参
3、函数形参表
4、函数体

template<> 返回值 函数名<Type>(参数列表) { // 函数体 }
     但是需要注意以下内容:

       另外以下这两点也需要注意1下:1、在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,如果不匹配,编译器将为实参模板定义中实例化1个实例。2、特化不能出现在模板实例的调用以后,应当在头文件中包括模板特化的声明,然后使用该特化版本的每一个源文件包括该头文件。

                                                         模板类             

       进入到我们的模板类,来看1下怎样使用模板类(以顺序表作为例子):

普通顺序表:

typedef int DataType; //typedef char DataType; class SeqList { private : DataType* _data ; int _size ; int _capacity ; };
模板类顺序表:

template<typename T> class SeqList { private : T* _data ; int _size ; int _capacity ; };
            由于模板类也是模板,必须以关键字template开头,后接模板形参表。

       因此可以总结出模板类的1般格式:

template<class 形参名1, class 形参名2, ...class 形参名n> class 类名 { ... };
          来看1下下面1张图以后,你对模板类就能够初步理解了:


【模板类的实例化】
只要有1种不同的类型,编译器就会实例化出1个对应的类

SeqList<int > sl1; SeqList<double > sl2;
          当定义上述两种类型的顺序表时,编译器会使用int和double分别代替模板形参,重新编写SeqList类,最后创建名为SeqList<int>和SeqList<double>的类。

      其实这里可以引出1个关于适配器的概念,有兴趣的可以去了解1下。

                                                    模板的分离编译

      1般来说,类模版不能分离编译,缘由得从模板的实例化入手。

    1)以分离情势写出的模版类(以tem.h和tem.cpp为例,另外还有主函数main.cpp),在编译main.cpp时由于只能看到模板声明而看不到实现,因此不会创建新的类型,但此时不会报错,由于编译器认为模板定义在其它文件中,就把问题留给链接程序处理。

    2)编译器在编译tem.cpp时可以解析模板定义并检查语法,但不能生成成员函数的代码。由于要生成代码,需要知道模板参数,即需要1个类型,而不是模板本身。

    3)这样,链接程序在main.cpp 或 tem.cpp中都找不到新类型的定义,因而报出无定义成员的毛病。另外,实例化是惰性的,只有用到该函数时才会去对模版中的定义进行实例化。

       所以模板在分离编译的进程当中会产生链接出错,通常是提示没法解析的外部命令,有以下两种解决方法:

1. 在模板头文件 xxx.h 里面显示实例化->模板类的定义后面添加 template class 名字<类型 >; 1般不推荐这类方法,1方面老编译器可能不支持,另外一方面实例化依赖调用者。(不推荐)
2. 将声明和定义放到1个文件 "xxx.hpp" 里面,推荐使用这类方法

      如果对模板的分离编译感兴趣,传送门:http://blog.csdn.net/pongba/article/details/19130

                                                    最后总结1下模板
【优点】
模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
增强了代码的灵活性
【缺点】
模板让代码变得混乱复杂,不容易保护,编译代码时间变长。
出现模板编译毛病时,毛病信息非常混乱,不容易定位毛病



------分隔线----------------------------
------分隔线----------------------------

最新技术推荐