泛型编程之模板

来源:互联网 发布:js滚轮时间选择控件 编辑:程序博客网 时间:2024/05/20 16:11

泛型编程

       泛型编程是指编写与类型无关的逻辑代码,是代码复用的一种手段。进行泛型编程的基础是模板。

模板

       这里的模板与我们生活中的模板类似,我们可以用一个模板产生非常多的实例,比如印刷试卷时,机器上用一个模板可以印刷成千上万张试卷,在这里我们也可以用一个模板来产生很多代码。

       模板分类;

函数模板初介绍

        一个函数模板代表着一种函数类型,该模板与类型无关在使用时根据实参类型来产生特定类型的函数。

        来看个例子,我们要对两个数进行求和,而这两个数的类型可以有很多种,如果用平常的做法我们要用到函数重载,就像这样:

int Add(int left, int right){return left + right;}float Add(float left, float right){return left + right;}double Add(double left, double right){return left + right;}char Add(char left, char right){return left + right;}

这里给出了4种类型的函数进行重载,这四个函数除类型不同之外执行的功能都相同,即返回两个数的和,这样实现了对不同类型的操作数执行同一功能的需求,但是这样做我们需要写出大量的代码,在实际运用时用到的类型远不止以上这4中类型,因此这样做并不是个很好的选择。那么有没有能实现相同功能而又能减少代码量的做法呢?我们又会想到可以定义个宏,比如:

#define Add(left, right) ((left) + (right))

这样可以实现我们既对不同类型的两个数求和又节约了代码量的需求,但是宏不会对代码进行类型检查,这是种及不安全的做法。

       那有没有既能处理不同类型的数据又可以减少代码量并且安全的做法呢?有,模板。做法如下:

template<typename T>T Add(T left, T right){return left + right;}

这样行不行呢?我们来调用一下:

int main(){cout << "Add(int, int) = " << Add(10, 20);cout << "Add(double, double) = " << Add(10.0, 20.0);cout << "Add(char, char) = " << Add('1', '2');return 0;}

运行:


可以看见,这个函数模板实现了前面的需求。那么 这个模板是怎么工作的呢?

       我们定义一个函数模板后,模板根据传入实参的具体类型进行分析,判断出需要的类型,然后由编译器产生相应的代码实例(没错,就是由编译器“写”代码),并进行调用。需要注明的是,模板函数并不是一个函数,而是一个蓝图,是一个类似于类的东西,它可以具体实例化,模板本身的功能只是产生特定代码,而不执行其实例的功能。比如我们的实参是int类型的,那么编译器就会产生这样的代码并执行:

int Add(int left, int right){return left + right;}

而如果传入的参数是double 类型的,就产生这样的代码并执行:

double Add(double left, double right){return left + right;}

这些代码都不是模板本身,而是编译器根据模板来产生的。

       接下来对函数模板进行深入探究。

深入探究函数模板

1、函数模板的格式:


在前面的例子中我们已经给出过一个函数模板,我们来分析一下:


我们用关键字template来定义一个模板,在template后面的尖括号中用模板参数关键字typename来定义模板参数,这个关键字也可用class代替(ps:不能用struct来代替typename),但是最好用typename。用typename定义的参数T代表了某种类型,具体是哪种类型要根据具体传入的实参类型来确定。

       也可以将模板函数定义为内联函数,其格式为:


这里关键字inline必须放在返回值类型的前面,不能放在关键字template的前面。

        在编译时,模板函数会经历两次编译。一是在实例化之前会检查模板是否有语法错误,比如遗漏分号等。二是在实例化期间查看模板代码,检查是否所有的调用都有效,比如实例化类型不支持某些函数的调用等。

2、实参推演与类型形参转换

      从函数实参确定模板形参类型和值的过程称为实参推演,在推演过程中,多个形参类型的实参必须完全匹配。

      在进行实参推演时,一般不会转换实参来匹配现有的实例,相反会产生新的实例。而在两种情况下回发生类型形参的转换:

(1)、const转换:接收const引用或指针的函数可以分别用非const对象的指针或引用来调用。

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


3、参数列表

      函数模板有两种类型的参数------模板参数和调用参数。


调用参数和普通函数一样。这里仅介绍模板参数。

       模板形参可以分为类型形参和非类型形参。一个同时包含了类型形参和非类型形参的模板格式如下:



先来看一下类型形参需要注意的地方:

        类型形参列表中不能有相同的名字,比如如下代码就会出现错误:

template<typename T, typename T>void FunTest(T left,T right){cout << typeid(left).name << endl;}

编译:


T重定义,显然这样用是非法的。我们用关键字typename声明了一种类型T,那么这个T的作用域有多大呢?我们用下面的代码来探讨:

typedef char T;template<typename T>void FunTest(T left,T right){cout << "type1 =" << typeid(left).name() << endl;}T a;int main(){FunTest(10, 20);cout << "type2 = " << typeid(a).name() << endl;return 0;}

如果模板中T的作用域为模板定义后的整个区域,那么两个输出都会是int,而如果模板中T的作用域仅限于模板函数中那么输出就会是int 和char,我们来看输出结果:


显然,模板中形参T仅限于模板形参列表到模板声明或定义的末尾,且遵循名字屏蔽原则。

       在定义普通变量时,我们常用

int a, b;

定义两个整型变量,那么在模板形参列表中用同种方式定义两个模板形参行不行呢?我们试一下:

template<typename T, T2>void FunTest(T left,T right){cout << "type1 =" << typeid(left).name() << endl;}

这里只用了一个typename来定义两个形参,来看一下能不能通过编译:


显然,这样是非法的,所以,每个类型形参前面都要加关键字typename或者class。

非模板类型形参:

        非模板类型形参是模板内部定义的常量,当需要用到常量表达式时,可以定义非模板类型形参。例如我们给出如下代码:

template <typename T,int N>void FunTest(T (&array)[N]){for (int i = 0; i < N; i++)array[i] = 0;}

模板的形参列表中同时存在类型参数和非类型参数,在调用参数中声明一个有N个元素类型为T的数组的引用,这里N作为一个常量,在实参进行传参时N的值就被确定了,我们在调用时传入一个整型数组:

int main(){int array[5];FunTest(array);return 0;}

观察反汇编:


可以观察到,在利用模板生成的函数中N已经变成了5。

4、模板实例化

        模板的实例化:

给出一个函数模板:

template<typename T>T Max(T left, T right){return left > right ? left : right;}

进行隐式实例化:

int main(){cout << Max(20, 30) << endl;return 0;}

这里调用函数时没有指定类型,函数模板通过实参推演进行实例化,这种情况下一般不会进行实参的类型转换。再来看显式实例化:

int main(){cout << Max<double>('a', 30) << endl;return 0;}

可以看见,在调用时显式给出了函数的类型,在这种情况下没有进行实参推演形参的类型就已经被确定了,而且调用时可进行实参类型的转换。比如这里实参中的字符型常量'a'和int型常量30在进行传参时就会自动转换成double类型常量,编译器也会自动生成形参类型为double类型的函数。

5、模板函数重载

       模板函数重载是指一个非模板函数和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为该非模板函数。

      例如:

int Max(int left, int right){return left > right ? left : right;}template<typename T>T Max(T left, T right){return left > right ? left : right;}

这里的非模板函数和函数模板构成了重载,并且函数模板可以实例化为这里非模板函数。那么问题来了,这里非模板函数和模板产生的函数都具有相同的功能,那么在实际调用时到底调用那个函数呢?我们这样调用:

int main(){cout << Max(20, 30) << endl;return 0;}

进入汇编代码中:


我们进入这个函数:


显然调用了非模板函数,那么当非模板函数与模板产生的函数功能相同时都调用非模板函数吗?不是的。这里讲究方便原则,即那个方便就调用哪个,在这来方便,那么模板就不用生成其他函数了,而如果模板可以生成一个具有更好匹配的函数,就不会调用非模板函数,而会调用模板生成的函数。里非模板函数调用起

       试用如下代码调用函数:

int main(){cout << Max('a', 30) << endl;return 0;}

实参为一个字符型和一个整行型常量,会调用哪个函数呢?来看一下:


分析一下,这里的实参一个为char类型一个为int类型,而在重载的两个函数中形参类型是一样的,所以在进行传参时实参必然会进行类型转换,而函数模板一般只进行const转换和数组与函数指针的转换,所以这里调用非模板函数。模板函数隐式实例化时不允许自动类型转换,但普通函数可以进行自动类型转换。

6、模板函数特化

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

       举个例子:

template<typename T>int compare(T left,T right){if (left > right)return 1;if (left < right)return -1;return 0;}

需要比较两个操作数的大小,大于返回1,小于则返回-1,如果 正常调用:

int main(){cout << ">>>>" << compare(10, 20) << endl;return 0;}

结果:


再看如下调用:

int main(){cout << ">>>>" << compare("1234", "abcd") << endl;return 0;}

运行:

函数执行结果错误!显然我们前面定义的函数模板并不适用于这里,所以这种情况必须进行特化,如下:

template<>int compare<const char*>(const char* left, const char* right){if (strcmp(left, right) > 0)return 1;if (strcmp(left, right) < 0)return -1;return 0;}

再次调用结果:

显然模板进行特化后能够正确作出判断。

函数模板的特化形式:

1、关键字template后面接一对空的尖括号<>

2、函数名后接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参

3、函数形参表

4、函数体

template<>

返回值 函数名(参数列表)

{

       // 函数体

}

特化的声明必须与特定的模板相匹配,看下面声明:


特化声明与函数模板不匹配,显然是非法的。

特化应该注意:

1、假如少了模板形参表,只是定义了一个普通函数,该函数含有返回类型和与模板实例化相匹配的形参表。

2、应当将特化与重载分离开,重载时编译器调用比较“方便”的函数,而在模板特化中当处于特定类型时只调用特化函数。

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

4,特化不能出现在模板实例的调用之后,应该在头文件中包含模板特化的声明,然后使用该特化版本的每个源文件包含该头文件。



模板类

类模板格式:

template <typename T1, ..., typename Tn>

class 类名

{

       ...

};

比如在顺序表中我们可以定义这样一个模板类:

template<typename T>class SeqList{public:SeqList();~SeqList();private:int _size;int capacity;T *_data;};

模板类的实例化

        对模板类进行实例化时,必须同时指定类型,比如上面模板类的实例化:

int main(){SeqList<int> s1;return 0;}

这就是模板类的实例化。

       模板类中的成员函数都要声明为模板函数。模板类的写法用法等和模板函数类似,在此不作多说。

我们通过上面的探讨可以总结一下模板的优缺点:

有点: 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。增强了代码的灵活性。

缺点: 模板让代码变得凌乱复杂,不易维护,编译代码时间变长。出现模板编译错误时,错误信息非常凌乱,不易定位错误。




原创粉丝点击