所谓泛型编程就是以独立于任何特定类型的方式编程。使用时,我们需要提供具体程序实例所操作的类型或值。标准库的容器、迭代器和算法都是泛型编程的例子。
模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。
可以定义自己的函数模板或类模板。
函数模板是独立于类型的函数,根据实参类型产生函数特定类型的版本。模板定义以关键字template开始,后接模板形参列表。它是用尖括号括住的一个或多个模板形参的列表。形参之间以逗号分隔。
模板形参表不能为空。它很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化这些形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。模板形参可以是表示类型的类型形参,也可以是常量表达式的非类型形参。类型形参跟在关键字class或typename之后定义。class和typename均为指明模板形参,没有区别。如
template
使用函数模板时编译器会推断哪个模板实参绑定到模板形参,一旦编译器确定了模板实参,就成为它的实例化了一个函数模板的实例。编译器将确定用什么类型代替每个类型形参,以及用什么值代替非类型形参,然后编译器使用实参代替相应的模板形参,并编译该版本的函数。编译器承担为我们使用的每种类型函数的编写工作。
函数模板可以和非模板函数一样声明为inline。说明符放在模板形参列表之后,返回类型之前。不能放在关键字template之前。如
template
它的作用是:使实例化的函数为inline函数。
类模板也是模板,因此必须以template开头,后接模板形参。在类和类的成员的定义中,可以使用模板形参作为要使用类型的占位符。如
template
class A
{
public:
T cc;
};
要区别类型形参还是非类型形参。如果是类型形参,该形参表示未知类型。非类型形参:其类型已经确定,只是值不确定。
模板形参的名字可以在声明为模板形参之后,模板声明或定义的末尾处使用。它遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参屏蔽全局名字。
用作模板形参的名字不能在模板内重用。即模板形参的名字只能在同一模板形参列表中使用一次。不同模板的模板形参,可以使用相同的名字。
模板也可以只声明而不定义。声明必须指出函数或类是一个模板。
如template
同一模板的声明和定义中,模板形参的名字不必相同。因为它们仅仅是个占位符。如
template
template
必须为每个模板形参带上typename或class,这一点跟函数一样。
类型形参由关键字class或typename后接说明符指定,两个关键字具有相同含义,都指出后面所接的名字表示一个类型。类型形参可作为类型说明符,用在模板的任何地方。与内置类型说明符或类类型说明符使用方式完全相同。它可以指定返回类型或形参类型,以及在函数体中用于变量声明或强制类型转换。
虽然class和typename含义相同,可以相互替换,但使用typename更为直观。原因见后续介绍。
除了定义成员变量和成员函数以外,类还可以定义类型成员。如果在函数函数模板内使用这样的类型,必须显式告诉编译器我们正在使用的指的是一个类型,而不是一个值。如
A::size_t ype*p;
编译器不知道size_type是一个类型的名字还是数据成员的名字。如是类型名,则上句为定义一个指针。如是数据成员的名字,上句是两个变量相乘。因此需要在在模板内显式定义size_type为类型名。如
typename A::size_type*p;//
模板形参不必都是类型。在调用函数时非类型形参将用值代替。值的类型在模板形参列表中指定。模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候可使用非类型形参。
在编写模板代码时,对实参类型的要求要尽量少。以下为编写泛型代码的两个重要原则:
1:模板形参为const引用。这可以防止实参不支持复制或易复制出错的情形。另外对于较大对象也可以提高性能。
2:函数体中的测试只使用<比较。
如if(v1
if(v1>v2)return 1;
但是将代码改写为:
if(v1
if(v2
可以减少对类型的要求,这些类型只需支持<就可以了。不需要再支持>。
模板是一个蓝图,它本身不是类或者函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程成为实例化。类模板的每次实例化都会产生一个独立的类型。
想要使用类模板就必须显式指定模板实参。如
Queue
函数模板实例化时,编译器通常会为我们推断模板实参。如
template
compare(1,0);
compare(2.0,3.2);
这段代码实例化了compare的两个版本。一个用int代替T,一个用double代替。实际上是编译器为我们编写了compare的这两个实例。
int compare(const int &v1,const int &v2){};
double compare(const double &v1,double &v2){};
要确定该实例化那个函数,编译器会来查看每个实参,如果相应形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型。从函数实参类型确定模板实参的类型的过程叫做模板实参推断。
必须为相同的类型形参指明相同的类型实参。
如:short s=2;
compare(s,2.3);
这是错误的,因为模板形参是相同的,而模板实参类型却不同,两个类型不匹配,所以模板推断失败。
如果想要允许实参的常规转换,则应该指定两个不同的模板形参。如
template
一般情况下不转换实参用于匹配已产生的特化,而会重新生成本类型的特化。
编译器在以下两种情况下发生转换,而不生成新的实例化:
1:const转换。接受const引用或const指针的函数,可以分别用非const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用,形参类型和实参都忽略const,无论传递const或非const对象给接受非引用类型的函数,都是用相同的实例化。(与函数相同)
2:数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换,数组实参当做指向其第一个元素的指针,而函数实参当做指向函数类型的指针。
可以使用函数模板对函数指针进行初始化或赋值。这样做时,编译器使用指针的类型实例化具有适当模板实参的模板版本。
如:
template
int (*pf)(const int &,const int &)=compare;
某些情况下,不可能推断出模板实参的类型,这种情况下有必要覆盖模板实参的推断机制,并显式指定为模板形参所用的类型或值。
如template
因此调用者必须每次调用时为返回值指定类型。这可以显式提供,如:
int i=20;
long l=30;
long ret=sum
这一调用显式指定T1的类型,编译器从调用中传递的实参推断T2和T3的类型。
显式模板实参从左到右与对应的模板形参相匹配。第一个模板实参与T1相对应,第二个模板实参与T2向对应,第三个模板实参与T3相对应。由此可以推断只有最右边的形参的显式模板实参可以省略。如果这样写:
template
T3 sum(T1,T2);
则总是必须为三个形参指定显式实参,如:
long ret=sum
当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例。
一般而言当调用函数的时候,编译器只需看到函数的声明。定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的,因此将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
模板不同,要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数定义,需要哪些通常放在源文件中的代码。《C++primer》为编译模板代码定义了两种类型。分别为包含编译模型和分别编译模型。觉得应该是编译器实现模板编译的两种方式吧。暂时用不着,也看不懂,知道这里有即可,不再介绍。
通常在使用类模板名字的时候,必须制定模板实参。但这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。如接下来要介绍的Queue类。复制构造函数本来应声明为:Queue
但是在类外实现时,就不能仅仅使用非限定名了。在类外实现的成员函数的定义具有以下格式:
1:必须以关键template开头,后接类的模板形参表。
2:必须指出它所属的类。
3:类名必须包含模板形参。
如:
Template
这个定义从左至右读作:
用名为T的类型形参定义一个函数模板,它返回void,它是在类模板Queue
类模板的成员函数本身也是函数模板,同其他任何函数模板一样,需要使用类模板的成员函数产生该成员函数的实例化,类模板成员函数的模板形参由调用该函数对象的类型确定。如void Queue
当调用Queue
对象的模板实参能确定成员函数的函数模板实参,这一事实意味着,调用类模板成员函数比调用类似函数模板更为灵活。类模板成员函数在实例化后允许进行常规转换,而函数模板不允许进行转换,只会产生不同的实例。如:Queue
short s=24;
int i=33;
qi.push(s);
qi.push(i);
类模板的成员函数只有被程序调用时才进行实例化。如果某函数从未使用,则不会实例化该成员函数。
非类型模板实参,必须为每个形参提供常量表达式。
类模板可以出现三种友元声明:
1:普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。如:
Template
Friend class FooBar
};
这个声明说FooBar的成员可以访问Bar类的任意实例的private和protected成员。
2:一般模板友元关系
友元可以是类模板或函数模板。
Template
{
Template
};
这些友元声明使用与类本身不同的类型实参。Fool的友元声明是说,Fool的任意实例都可以访问Bar的任意实例。
3:特定的模板友元关系
除了将一个模板的所有实例设为友元,类也可以只授予特定实例的访问权:
Template
Template
{
Friend class Foo2
}
即使Foo2本身是模板,友元关系也只扩展到Foo2的形参类型为char*的特定实例。
但是下面的友元声明更为常见:
Template
Template
{
Friend class Foo3
};
此声明定义了Bar的特定实例与使用同一模板实参的Foo3的实例之间是友元关系。如
Bar
在授予友元关系时,编译器将友元声明当做类或函数的声明对待,因此不需要存在该模板类或函数模板的声明。
注意上面代码Bar类定义上的template
之所以可以在Bar类内写friend class Foo3
如果在Bar类内声明就变成了模板声明的第二种关系了。注意呀。
任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员成为成员模板。
模板成员声明看起来想任意模板的声明一样。
如template
{
Public:
Template
};
成员模板可以定义在包含它的类或类模板的的内部或外部。当在类模板作用域外部定义成员模板时,必须包含两个模板形参表:
Template
Template
Void Queue
{
}
这两个模板形参表,分别为类模板形参和成员模板自己的形参表。
成员模板的应用如标准容器的assign操作,它接受一对其他类型但值兼容的迭代器,将其他容器的元素复制到自己的容器中。
模板特化:一个或多个模板形参的实际类型或实际值是指定的。
特化的形式如下:
1:关键字template跟着一对空的尖括号<>。
2:再接模板名和一对尖括号,尖括号指定这个特化定义的模板形参。
3:函数形参表。
4:函数体。
如
Template<>
Int compare
{
return strcmp(v1,v2);
}
该函数体定义了当模板形参类型绑定到const char*时,compare的特化。此时当为compare传递两个字符指针时,编译器将调用特化版本。
与任意函数一样,函数模板特化可以声明而无须定义。与函数模板特化的定义相比,它仅仅省略了函数体。如:
template<>
int compare
上句显式指定了模板实参,如果可以从函数形参表中推断模板实参,则不必显式指定模板实参。如:
int compare(const char*v1,const char*v2);
在模板特化版本被调用时,实参类型必须与特化版本的形参类型完全匹配。否则编译器将为实参从模板定义实例化一个新实例。
应该在一个头文件中包含模板特化的声明,每个使用该特化的文件包含该源文件。在调用该特化之前,必须有特化的声明。这与普通函数类似。
类模板的特化与函数模板特化类似。如:
Template<>
Class Queue
{
public:
String front();
};
在外部定义成员函数时,不能在成员函数前加template关键字。
除了特化整个类之外,还可以特化类中的某些成员。成员特化声明与函数模板特化一样,以空的模板形参表开头:
Template<>
Void Queue
此声明要放在Queue头文件中。
如果类模板有一个以上的模板形参,我们可以特化某些模板形参而非全部。使用类模板的部分特化可以做到这一点。
类模板的部分特化也是模板。它以关键字template开头,接<>括住的模板形参表 。如:
template
class someclass
{};
template
class someclass
函数模板也可以重载,可以定义有相同名字但形参数目或类型不同的多个函数模板 。也可以定义域函数模板与相同名字的普通函数。
后面介绍的是模板的高级内容:模板特化。暂时用不着,知道在这里有这些东西就可以了。前面的知识点对模板的初步使用已经足够了。以后具体开发中可以再回来参考本章内容。
接下来为使用模板实现自己的Queue容器。
#include
using namespace std; template class QueueItem { public: T data; QueueItem *next; public: QueueItem(T const &d) { data=d; next=NULL; } }; template class Queue { public: QueueItem *head; QueueItem *tail; public: Queue() { head=NULL; tail=NULL; } T&front() { return head->data; } void push(T const &d) { QueueItem *p=new QueueItem (d); if(empty()) { head=tail=p;//注意第一次赋值哦。 } else { tail->next=p; p->next=NULL;//多余哦。 tail=p; } } void pop() { QueueItem *temp=head; head=head->next; delete temp; } bool empty() { if(!head) { return true; } else return false; } T &operator=(Queue &rhs) { QueueItem *h=rhs.head; while(h) { push(h->data); h=h->next; } return *this; } void destroy() { while(!empty()) { delete head; head=head->next; } } }; int main(int argc,char**argv) { Queue qi; qi.push(20); cout<<"front"< qi2=qi; cout<<"front"<