“懒人”的福音---泛型编程

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

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

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

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

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

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

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

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

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

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


                                                                 函数模板     

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

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

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

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

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

       另外值得一提的是:模板函数也可以定义为inline函数 

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

    

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

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

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

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

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

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


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

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

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

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

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

同样做一个总结:

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

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

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

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

      可以下面这样来定义


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

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

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

                                                         模板类             

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

普通顺序表:

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开头,后接模板形参表。

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

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


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

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

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

                                                    模板的分离编译

      一般来讲,类模版不能分离编译,原因得从模板的实例化入手。

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

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

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

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

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

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

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



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值