C++ primer第二次阅读学习笔记(第16章:模板与泛型编程) .

第十六章:模板与泛型编程

所谓泛型编程就是以独立于任何特定类型的方式编程。使用时,我们需要提供具体程序实例所操作的类型或值。标准库的容器、迭代器和算法都是泛型编程的例子。

模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。

可以定义自己的函数模板或类模板。

函数模板是独立于类型的函数,根据实参类型产生函数特定类型的版本。模板定义以关键字template开始,后接模板形参列表。它是用尖括号括住的一个或多个模板形参的列表。形参之间以逗号分隔。

模板形参表不能为空。它很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化这些形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。模板形参可以是表示类型的类型形参,也可以是常量表达式的非类型形参。类型形参跟在关键字classtypename之后定义。classtypename均为指明模板形参,没有区别。如

template

使用函数模板时编译器会推断哪个模板实参绑定到模板形参,一旦编译器确定了模板实参,就成为它的实例化了一个函数模板的实例。编译器将确定用什么类型代替每个类型形参,以及用什么值代替非类型形参,然后编译器使用实参代替相应的模板形参,并编译该版本的函数。编译器承担为我们使用的每种类型函数的编写工作。

函数模板可以和非模板函数一样声明为inline。说明符放在模板形参列表之后,返回类型之前。不能放在关键字template之前。如

templateinline T min(const T&,const T&);

它的作用是:使实例化的函数为inline函数。

类模板也是模板,因此必须以template开头,后接模板形参。在类和类的成员的定义中,可以使用模板形参作为要使用类型的占位符。如

template//typename T

class A

{

public:

T cc;
};

要区别类型形参还是非类型形参。如果是类型形参,该形参表示未知类型。非类型形参:其类型已经确定,只是值不确定。

模板形参的名字可以在声明为模板形参之后,模板声明或定义的末尾处使用。它遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参屏蔽全局名字。

用作模板形参的名字不能在模板内重用。即模板形参的名字只能在同一模板形参列表中使用一次。不同模板的模板形参,可以使用相同的名字。

模板也可以只声明而不定义。声明必须指出函数或类是一个模板。

template int func(const T&,const T&);

同一模板的声明和定义中,模板形参的名字不必相同。因为它们仅仅是个占位符。如

templateTclac(const T&,const T&);

templateUclac(const U&,const U&);//同一模板的两次声明。

必须为每个模板形参带上typenameclass,这一点跟函数一样。

类型形参由关键字classtypename后接说明符指定,两个关键字具有相同含义,都指出后面所接的名字表示一个类型。类型形参可作为类型说明符,用在模板的任何地方。与内置类型说明符或类类型说明符使用方式完全相同。它可以指定返回类型或形参类型,以及在函数体中用于变量声明或强制类型转换。

虽然classtypename含义相同,可以相互替换,但使用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 qi;

函数模板实例化时,编译器通常会为我们推断模板实参。如

templateT compare(const &T,const &T);

compare1,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;

compares,2.3);

这是错误的,因为模板形参是相同的,而模板实参类型却不同,两个类型不匹配,所以模板推断失败。

如果想要允许实参的常规转换,则应该指定两个不同的模板形参。如

templatetypename T2> T compare(T1 v1,T2 v2);

一般情况下不转换实参用于匹配已产生的特化,而会重新生成本类型的特化。

编译器在以下两种情况下发生转换,而不生成新的实例化:

1const转换。接受const引用或const指针的函数,可以分别用非const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用,形参类型和实参都忽略const,无论传递const或非const对象给接受非引用类型的函数,都是用相同的实例化。(与函数相同)

2:数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换,数组实参当做指向其第一个元素的指针,而函数实参当做指向函数类型的指针。

可以使用函数模板对函数指针进行初始化或赋值。这样做时,编译器使用指针的类型实例化具有适当模板实参的模板版本。

如:

template int compare(const T&,const T&);

int (*pf)(const int &,const int &)=compare;

某些情况下,不可能推断出模板实参的类型,这种情况下有必要覆盖模板实参的推断机制,并显式指定为模板形参所用的类型或值。

template T1 sum(T2,T3);

因此调用者必须每次调用时为返回值指定类型。这可以显式提供,如:

int i=20;

long l=30;

long ret=sum(i,l);

这一调用显式指定T1的类型,编译器从调用中传递的实参推断T2T3的类型。

显式模板实参从左到右与对应的模板形参相匹配。第一个模板实参与T1相对应,第二个模板实参与T2向对应,第三个模板实参与T3相对应。由此可以推断只有最右边的形参的显式模板实参可以省略。如果这样写:

template

T3 sum(T1,T2);

则总是必须为三个形参指定显式实参,如:

long ret=sum(i,lng);

当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例。

一般而言当调用函数的时候,编译器只需看到函数的声明。定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的,因此将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。

模板不同,要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数定义,需要哪些通常放在源文件中的代码。C++primer》为编译模板代码定义了两种类型。分别为包含编译模型和分别编译模型。觉得应该是编译器实现模板编译的两种方式吧。暂时用不着,也看不懂,知道这里有即可,不再介绍。

通常在使用类模板名字的时候,必须制定模板实参。但这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。如接下来要介绍的Queue。复制构造函数本来应声明为:Queue (const Queue&);由于在类作用域内部可以使用非限定名,因此在类内声明时可以写成:Queue(const Queue&);

但是在类外实现时,就不能仅仅使用非限定名了。在类外实现的成员函数的定义具有以下格式:

1:必须以关键template开头,后接类的模板形参表。

2:必须指出它所属的类。

3:类名必须包含模板形参。

如:

Template void Queue::destroy(){};

这个定义从左至右读作:

用名为T的类型形参定义一个函数模板,它返回void,它是在类模板Queue的作用域中。

类模板的成员函数本身也是函数模板,同其他任何函数模板一样,需要使用类模板的成员函数产生该成员函数的实例化,类模板成员函数的模板形参由调用该函数对象的类型确定。如void Queue::push(const int &val);

当调用Queue类型对象的push成员时,实例化此push函数。

对象的模板实参能确定成员函数的函数模板实参,这一事实意味着,调用类模板成员函数比调用类似函数模板更为灵活。类模板成员函数在实例化后允许进行常规转换,而函数模板不允许进行转换,只会产生不同的实例。如:Queue qi;

short s=24;

int i=33;

qi.push(s);

qi.push(i);

类模板的成员函数只有被程序调用时才进行实例化。如果某函数从未使用,则不会实例化该成员函数。

非类型模板实参,必须为每个形参提供常量表达式。

类模板可以出现三种友元声明:

1:普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。如:

Templateclass Bar{

Friend class FooBar

};

这个声明说FooBar的成员可以访问Bar类的任意实例的privateprotected成员。

2:一般模板友元关系

友元可以是类模板或函数模板。

Templateclass Bar

{

Template friend class Fool

}

这些友元声明使用与类本身不同的类型实参。Fool的友元声明是说,Fool任意实例都可以访问Bar任意实例

3:特定的模板友元关系

除了将一个模板的所有实例设为友元,类也可以只授予特定实例的访问权:

Templateclass Foo2;

Template class Bar

{

Friend class Foo2;
}

即使Foo2本身是模板,友元关系也只扩展到Foo2的形参类型为char*的特定实例。

但是下面的友元声明更为常见:

Templateclass Foo3;

Templateclass Bar

{

Friend class Foo3;

};

此声明定义了Bar的特定实例与使用同一模板实参的Foo3的实例之间是友元关系。如

Bar bi;//Foo3Bar的实例间是友元。不同类型模板实参之间为友元实际意义不大。

在授予友元关系时,编译器将友元声明当做类或函数的声明对待,因此不需要存在该模板类或函数模板的声明。

注意上面代码Bar类定义上的templateclass Foo3

之所以可以在Bar类内写friend class Foo3;就是因为前面已经将Foo3声明为模板。因为Bar类模板和Foo3类模板相同的模板实参的实例化才为友元。因此要达到这种目的必须在类前面声明其为类模板。

如果在Bar类内声明就变成了模板声明的第二种关系了。注意呀。

任意类(模板或非模板)可以拥有本身为类模板或函数模板的成员,这种成员成为成员模板。

模板成员声明看起来想任意模板的声明一样。

template Bar

{

Public:

Template void assign(Type t1,Type t2);
};

成员模板可以定义在包含它的类或类模板的的内部或外部。当在类模板作用域外部定义成员模板时,必须包含两个模板形参表:

Template

Template

Void Queue::assign(Type t1,Type t2)

{

}

这两个模板形参表,分别为类模板形参和成员模板自己的形参表。

成员模板的应用如标准容器的assign操作,它接受一对其他类型但值兼容的迭代器,将其他容器的元素复制到自己的容器中。

模板特化:一个或多个模板形参的实际类型或实际值是指定的。

特化的形式如下:

1:关键字template跟着一对空的尖括号<>

2:再接模板名和一对尖括号,尖括号指定这个特化定义的模板形参。

3:函数形参表。

4:函数体。

Template<>

Int compare(const char*v1,const char*v2)

{

return strcmp(v1,v2);

}

该函数体定义了当模板形参类型绑定到const char*时,compare的特化。此时当为compare传递两个字符指针时,编译器将调用特化版本。

与任意函数一样,函数模板特化可以声明而无须定义。与函数模板特化的定义相比,它仅仅省略了函数体。如:

template<>

int compare(const char*v1,const char*v2);

上句显式指定了模板实参,如果可以从函数形参表中推断模板实参,则不必显式指定模板实参。如:

int compare(const char*v1,const char*v2);

在模板特化版本被调用时,实参类型必须与特化版本的形参类型完全匹配。否则编译器将为实参从模板定义实例化一个新实例。

应该在一个头文件中包含模板特化的声明,每个使用该特化的文件包含该源文件。在调用该特化之前,必须有特化的声明。这与普通函数类似。

类模板的特化与函数模板特化类似。如:

Template<>

Class Queue

{

public:

String front();

};

在外部定义成员函数时,不能在成员函数前加template关键字。

除了特化整个类之外,还可以特化类中的某些成员。成员特化声明与函数模板特化一样,以空的模板形参表开头:

Template<>

Void Queue::push(const char*,const char*);

此声明要放在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"<


你可能感兴趣的:(C++ primer第二次阅读学习笔记(第16章:模板与泛型编程) .)