当前位置:文档之家› template

template

1、

C++ 模板基础谈

1. 什么是模板

模板定义:模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现了真正的代码可重用性。

我们知道,C++ 是一种“强类型”的语言,也就是说一个变量,编译器必须确切的知道它的类型,而模板就是构建在这个强类型语言基础上的泛型系统。

2. 模板的语法

模板函数

template < typename {类型参数名称}, [ int {Name}=...][, ...] >

{函数定义}

模板类

template < typename ... , [ int {Name}=...] >

class ...

模板的参数可以是类型,或者是一个int 型的值(或者可以转换为int 型的,比如bool)。

3. 模板的使用

显式类型参数:对于模板函数,在函数名后添加< {类型参数表} >。对于模板类,在类后添加< {类型参数表} >

隐式类型参数:对于模板函数,如果类型参数可以推导,那么可以省略类型参数表

举个例子:

template < typename T >

T max( T a, T b )

{

return a < b ? b : a;

}

这个max 函数就是一个模板函数,它可以传入一个“类型”的参数,以便实现任意类型求最大值的效果。假设我们这样使用它:

int x=5, y=10;

int z=max ( x, y );

这时候发生了什么呢?我们传入的“类型参数”是int,因此编译器在编译这段代码时会使用int 来构造一个新函数:

int max( int a, int b )

{

return a < b ? b : a;

}

后面的事就和编译普通的函数一样了,C++编译器继续使用强类型系统编译这个函数,由强类型系统来检查这个函数是否正确。

这个过程叫做模板的“特化”,它发生在编译期,当编译器发现模板函数、模板类被使用(注意,不是定义)的时候进行的。这个系统实际上比较像宏,但是比宏更为智能。

很明显,编译器必须知道模板如何特化这个函数,因此模板函数的实现,必须在“使用点”之前,因此模板库只能通过头文件库的形式来提供。

4. 模板的类型推导

对于函数,编译器是知道传入参数的类型的,比如上面的max,max < ? >( x, y ),由于第一个参数x 是int 类型的,那么? 这里需要填写什么呢?

我们可以很明显的推断出应该是"int",否则,后面的强类型系统将无法编译这个函数。编译器同样知道x 的类型,因此它也能推导出“类型参数”,这时候我们调用时就可省略模板参数了。

这个推导是按顺序来的,因此如果上面的y 是其他类型,? 仍然会被推导为int,如果y无法隐性转换为int,强类型编译时就会报错。

5. 类型推导的隐式类型转换

在决定模板参数类型前,编译器执行下列隐式类型转换:

左值变换

修饰字转换

派生类到基类的转换

见《C++ Primer》([注2],P500)对此主题的完备讨论。

简而言之,编译器削弱了某些类型属性,例如我们例子中的引用类型的左值属性。举例来说,编译器用值类型实例化函数模板,而不是用相应的引用类型。

同样地,它用指针类型实例化函数模板,而不是相应的数组类型。

它去除const修饰,绝不会用const类型实例化函数模板,总是用相应的非const类型,不过对于指针来说,指针和const 指针是不同的类型。

底线是:自动模板参数推导包含类型转换,并且在编译器自动决定模板参数时某些类型属性将丢失。这些类型属性可以在使用显式函数模板参数申明时得以保留。

6. 模板的偏特化

如果我们打算给模板函数(类)的某个特定类型写一个函数,就需要用到模板的偏特化,比如我们打算用long 类型调用max 的时候,返回小的值(原谅我举了不恰当的例子):

template <> // 这代表了下面是一个模板函数

long max ( long a, long b ) // 对于vc 来说,这里的 是可以省略的

{

return a > b ? b : a;

}

实际上,所谓偏特化,就是代替编译器完成了对指定类型的特化工作,现代的模板库中,大量的使用了这个技巧。

7. 仿函数

仿函数这个词经常会出现在模板库里(比如STL),那么什么是仿函数呢?

顾名思义:仿函数就是能像函数一样工作的东西,请原谅我用东西这样一个代词,下面我会慢慢解释。

void dosome( int i )

这个dosome 是一个函数,我们可以这样来使用它:dosome(5);

那么,有什么东西可以像这样工作么?

答案1:重载了() 操作符的对象,比如:

struct DoSome

{

void operator()( int i );

}

DoSome dosome;

这里类(对C++ 来说,struct 和类是相同的) 重载了() 操作符,因此它的实例dosome 可以这样用dosome(5); 和上面的函数调用一模一样,不是么?所以dosome 就是一个仿函数了。

实际上还有答案2:

函数指针指向的对象。

typedef void( *DoSomePtr )( int );

typedef void( DoSome )( int );

DoSomePtr *ptr=&func;

DoSome& dosome=*ptr;

dosome(5); // 这里又和函数调用一模一样了。

当然,答案3 成员函数指针指向的成员函数就是意料之中的答案了。

8. 仿函数的用处

不管是对象还是函数指针等等,它们都是可以被作为参数传递,或者被作为变量保存的。因此我们就可以把一个仿函数传递给一个函数,由这个函数根据需要来调用这个仿函数(有点类似回调)。

STL 模板库中,大量使用了这种技巧,来实现库的“灵活”。

比如:

for_each, 它的源代码大致如下:

template < typename Iterator, typename Functor >

void for_each( Iterator begin, Iterator end, Fucntor func )

{

for( ; begin!=end; begin++ )

func( *begin );

}

这个for 循环遍历了容器中的每一个元素,对每个元素调用了仿函数func,这样就实现了对“每个元素做同样的事”这样一种编程的思想。特别的,如果仿函数是一个对象,这个对象是可以有成员变量的,这就让仿函数有了“状态”,从而实现了更高的灵活性。

我的一点点理解:关于类之间的模板值传递

先贴一段程序,vs2008,编译通过:

#include

using namespace std;

template

class classOne{

private:

T a;

public:

T getA(T b){

a=b;

return a;

}

};

template

class classTwo{

private:

classOne test;

public:

T out(T b){

return test.getA(b);

}

};

int main(){

classTwo test2;

cout<

return 0;

}

将classOne定义为模板类,我原来一直搞不清楚classTwo如何传递模板值给它,其实和一般传值是一样的了,但似乎网上的教程都没有说,或许是太简单了,只是我理解不透没想到。恩,不作解释了,是在看不懂的,留言回答

当然这里还有一个弱点,就是在访问classTwo中访问classOne实例的a属性时,由于是私有属性,所以在classTwo中不能直接test.a,访问,解决这个问题,用友元类。

#include

using namespace std;

template

class classOne{

friend classTwo;

private:

T a;

public:

T getA(T b){

a=b;

return a;

}

};

template

class classTwo{

private:

classOne test;

public:

T out(T b){

test.a=b;

return test.a;

}

};

int main(){

classTwo test2;

cout<

return 0;

}

但还是报错。原因在于先声明友元类。在classOne上面如此声明就可以了。

template

class classTwo;

2、

函数模板是C++新增的一种性质,它允许只定义一次函数的实现,即可使用不同类型的参数来调用该函数。这样做可以减小代码的书写的复杂度,同时也便于修改(注:使用模板函数并不会减少最终可执行程序的大小,因为在调用模板函数时,编译器都根据调用时的参数类型进行了相应实例化)。下面来看看函数模板的使用过程:

struct job

{

char name[20];

int salary;

};

template //函数模板声明,通用变量类型为T

void swap(T &a, T &b);

void showJob(const job &a);//打印job内容

using std::cin;

using std::cout;

using std::endl;

void main(void)

{

int a = 4;

int b = 5;

cout<<"Before swap a = "<

job jobB = {"manager", 1000};

cout<<"Before swap";

showJob(jobA);

showJob(jobB);

cout<

swap(jobA, jobB);

cout<<"After swap";

showJob(jobA);

showJob(jobB);

cout<

system("pause");

};

template //函数模板实现

void swap(T &a, T &b)

{

T temp;

temp = a;

a = b;

b = temp;

}

void showJob(const job &a)

{

cout<<" "<

// end of temp.h

// start of temp.inl

template void Temp::print() {

...

}

// end of temp.inl

通过这样的变通,即满足了语法的要求,也让头文件更加易读。模板函数也是一样。

普通的类中,也可以有模板方法,例如:

class A{

public:

template void print(const T& t) { ...}

void dummy();

};

对于模板方法的要求与模板类的方法一样,也需要与类声明出现在一起。而这个类的其它方法,例如dummy(),则没有这样的要求。

3. 使用技巧

知道了上面所说的简单语法后,基本上就可以写出自己的模板了。但是在使用的时候还是有些技巧。

3.1 语法检查

对模板的语法检查有一部分被延迟到使用时刻(类被定义[3],或者函数被调用),而不是像普通的类或者函数在被编译器读到的时候就会进行语法检查。因此,如果一个模板没有被使用,则即使它包含了语法的错误,也会被编译器忽略,这是语法检查问题的第一个方面,这不常遇到,因为你写了一个模板就是为了使用它的,一般不会放在那里不用。与语法检查相关的另一个问题是你可以在模板中做一些假设。例如:

template class Temp{

public:

Temp(const T & t): t_(t) {}

void print() { t.print();}

private:

T t_;

};

在这个模板中,我假设了T这个类型是一个类,并且有一个print()方法(t.print())。我们在简介中的min模板中其实也作了同样的假设,即假设T重载了'>'操作符。

因为语法检查被延迟,编译器看到这个模板的时候,并不去关心T这个类型是否有print()方法,这些假设在模板被使用的时候才被编译器检查。只要定义中给出的类型满足假设,就可以通过编译。

之所以说“有一部分”语法检查被延迟,是因为有些基本的语法还是被编译器立即检查的。只有那些与模板参数相关的检查才会被推迟。如果你没有写class结束后的分号,编译器不会放过你的。

3.2 继承

模板类可以与普通的类一样有基类,也同样可以有派生类。它的基类和派生类既可以是模板类,也可以不是模板类。所有与继承相关的特点模板类也都具备。但仍然有一些值得注意的地方。

假设有如下类关系:

template class A{ ... };

|

+-- A aint;

|

+-- A adouble;

则aint和adouble并非A的派生类,甚至可以说根本不存在A这个类,只有A和A这两个类。这两个类没有共同的基类,因此不能通过类A来实现多态。如果希望对这两个类实现多态,正确的类层次应该是:

class Abase {...};

template class A: public Abase {...};

|

+-- A aint;

|

+-- A adouble;

也就是说,在模板类之上增加一个抽象的基类,注意,这个抽象基类是一个普通类,而非模板。

再来看下面的类关系:

template class A{...};

|

+-- A<10> a10;

|

+-- A<5> a5;

在这个情况下,模板参数是一个数值,而不是一个类型。尽管如此,a10和a5仍然没有共同基类。这与用类型作模板参数是一样的。

3.3 静态成员

与上面例子类似:

template class A{ static char a_; };

|

+-- A aint1, aint2;

|

+-- A adouble1, adouble2;

这里模板A中增加了一个静态成员,那么要注意的是,对于aint1和adouble1,它们并没有一个共同的静态成员。而aint1与aint2有一个共同的静态成员(对adouble1和adouble2也一样)。

这个问题实际上与继承里面讲到的问题是一回事,关键要认识到aint与adouble分别是两个不同类的实例,而不是一个类的两个实例。认识到这一点后,很多类似问题都可以想通了。

3.4 模板类的运用

模板与类继承都可以让代码重用,都是对具体问题的抽象过程。但是它们抽象的侧重点不同,模板侧重于对于算法的抽象,也就是说如果你在解决一个问题的时候,需要固定的step1 step2...,那么大概就可以抽象为模板。而如果一个问题域中有很多相同的操作,但是这些操作并不能组成一个固定的序列,大概就可以用类继承来解决问题。以我的水平还不足以在这么高的层次来清楚的解释它们的不同,这段话仅供参考吧。

模板类的运用方式,更多情况是直接使用,而不是作为基类。例如人们在使用STL提供的模板时,通常直接使用,而不需要从模板库中提供的模板再派生自己的类。这不是绝对的,我觉得这也是模板与类继承之间的以点儿区别,模板虽然也是抽象的东西,但是它往往不需要通过派生来具体化。

在设计模式[4]中,提到了一个模板方法模式,这个模式的核心就是对算法的抽象,也就是对固定操作序列的抽象。虽然不一定要用C++的模板来实现,但是它反映的思想是与C++模板一致的。

4、

区别:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定,

其实例化的一般形式是:

类名<数据类型1(或数据),数据类型2(或数据)…> 对象名

例如,Smemory mol;表示将类模板Smemory的类型参数T全部替换成int 型,从而创建一个具体的类,并生成该具体类的一个对象mol。

相关主题
文本预览
相关文档 最新文档