C++ Primer笔记(十四)模版与泛型编程

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

模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。可以定义自己的函数模板或类模板。

2、函数模板是独立于类型的函数,根据实参类型产生函数特定类型的版本。

模板定义以关键字template开始,后接模板形参列表。它是用尖括号括住的一个或多个模板形参的列表。形参之间以逗号分隔。

注意:模板形参表不能为空。

它很像函数形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化这些形参。同样,模板形参表示可以在类或函数的定义中使用的类型或值。   

3、模板形参可以是表示类型的类型形参,也可以是常量表达式的非类型形参。类型形参跟在关键字class或typename之后定义。class和typename均为指明模板形参,没有区别。如template<typename T>


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

编译器承担为我们使用的每种类型函数的编写工作。

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

template<typename T>inline T min(const T&,const T&);

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


6、类模板也是模板,因此必须以template开头,后接模板形参。

在类和类的成员的定义中,可以使用模板形参作为要使用类型的占位符。如

template<class T>//或typename T
class A
{
 public:
  T cc;
};

要区别类型形参还是非类型形参。

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

注意:使用类模版时,必须为模版形参显式指定实参。


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

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

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

如template<typename T> int func(const T&,const T&);

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

template<typename T> T clac(const T&,const T&);

template<typename U>U clac(const U&,const U&);//同一模板的两次声明。

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


8、类型形参由关键字class或typename后接说明符指定,两个关键字具有相同含义,都指出后面所接的名字表示一个类型。

类型形参可作为类型说明符,用在模板的任何地方。与内置类型说明符或类类型说明符使用方式完全相同。它可以指定返回类型或形参类型,以及在函数体中用于变量声明或强制类型转换。

虽然class和typename含义相同,可以相互替换,但使用typename更为直观。原因见后续介绍。

9、除了定义成员变量和成员函数以外,类还可以定义类型成员。

如果在函数函数模板内使用这样的类型,必须显式告诉编译器我们正在使用的指的是一个类型,而不是一个值。如

A::size_t ype*p;

编译器不知道size_type是一个类型的名字还是数据成员的名字。

如是类型名,则上句为定义一个指针。如是数据成员的名字,上句是两个变量相乘。因此需要在在模板内显式定义size_type为类型名。如typename A::size_type  *p;


10、模板形参不必都是类型。在调用函数时非类型形参将用值代替。值的类型在模板形参列表中指定。

模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候可使用非类型形参。

在编写模板代码时,对实参类型的要求要尽量少。以下为编写泛型代码的两个重要原则:

1)模板形参为const引用。

这可以防止实参不支持复制或易复制出错的情形。另外对于较大对象也可以提高性能。

2)函数体中的测试只使用<比较。

如if(v1<v2) return -1;

 if(v1>v2)return 1;

但是将代码改写为:

if(v1<v2)return -1;

if(v2<v1)return 1;

可以减少对类型的要求,这些类型只需支持<就可以了。不需要再支持>。


11、模板是一个蓝图,它本身不是类或者函数。编译器用模板产生指定的类或函数的特定类型版本。产生模板的特定类型实例的过程成为实例化。类模板的每次实例化都会产生一个独立的类型。

模版在使用时进行实例化,类模版在引用实际模版类类型时实例化,函数模版在调用它或用它对函数指针进行初始化或赋值时实例化。

想要使用类模板就必须显式指定模板实参。如

Queue<int> qi;

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

template<typename T>T compare(const &T,const &T);

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){};

要确定该实例化那个函数,编译器会来查看每个实参,如果相应形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型。从函数实参类型确定模板实参的类型的过程叫做模板实参推断。

12、必须为相同的类型形参指明相同的类型实参。

如:short s=2;
compare(s,2.3);

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


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

template<typename T1,typename T2> T compare(T1 v1,T2 v2);

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

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

1)const转换。

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

2)数组或函数到指针的转换。

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


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

template<typename T> int compare(const T&,const T&);

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

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

template <typename T,typename T2,typename T3> T1 sum(T2,T3);

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

int i=20;

long l=30;

long ret=sum<long>(i,l);

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

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

如果这样写:template<typename T1,typename T2,typename T3>

T3 sum(T1,T2);

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

long ret=sum<int ,long,long>(i,lng);

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


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

模板不同,要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数定义,需要哪些通常放在源文件中的代码。《C++primer》为编译模板代码定义了两种类型。分别为包含编译模型和分别编译模型。

16、通常在使用类模板名字的时候,必须制定模板实参。但这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。如接下来要介绍的Queue类。

复制构造函数本来应声明为:Queue<Type> (const Queue<Type>&);由于在类作用域内部可以使用非限定名,因此在类内声明时可以写成:Queue(const Queue&);

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

1)必须以关键template开头,后接类的模板形参表。
2)必须指出它所属的类。
3)类名必须包含模板形参。

如:Template<typename T> void Queue<T>::destroy(){};

这个定义从左至右读作:

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


17、类模板的成员函数本身也是函数模板,同其他任何函数模板一样,需要使用类模板的成员函数产生该成员函数的实例化,类模板成员函数的模板形参由调用该函数对象的类型确定。

如void Queue<int>::push(const int &val);

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

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

qi.push(i);

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

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


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

1)普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。如:
Template<typename T>class Bar{
 Friend class FooBar
};

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

2)一般模板友元关系。友元可以是类模板或函数模板。

Template<typename T1>class Bar
{
  Template<typename T2> friend class Fool;
};

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

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

Template<typename T>class Foo2;
Template<typename Type> class Bar
{
 Friend class Foo2<char *>;
    };

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

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

Template<typename T>class Foo3;
Template<typename T>class Bar
{
 Friend class Foo3<T>;
};

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

如:Bar<int> bi;//Foo3<int>与Bar<int>的实例间是友元。

不同类型模板实参之间为友元实际意义不大。

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

注意上面代码Bar类定义上的template<typename T>class Foo3;

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

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


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

模板成员声明看起来想任意模板的声明一样。
如template<typename T> Bar
{
Public:
Template<typename Type> void assign(Type t1,Type t2);
};
成员模板可以定义在包含它的类或类模板的的内部或外部。当在类模板作用域外部定义成员模板时,必须包含两个模板形参表:
Template<typename T>
Template <typename Type>
Void Queue<T>::assign(Type t1,Type t2)
{
}

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


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

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

特化的形式如下:

1)关键字template跟着一对空的尖括号<>。
2)再接模板名和一对尖括号,尖括号指定这个特化定义的模板形参。
3)函数形参表。
4)函数体。
如Template<>
Int compare<const char*>(const char*v1,const char*v2)
{
  return strcmp(v1,v2);
}

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


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

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

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

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

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

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

Template<>
Class Queue<const char*>
{
  public:
   String front();
};

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


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

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

此声明要放在Queue头文件中。

25、如果类模板有一个以上的模板形参,我们可以特化某些模板形参而非全部。使用类模板的部分特化可以做到这一点。

类模板的部分特化也是模板。它以关键字template开头,接<>括住的模板形参表 。如:
template<typename T1,typename T2>
class someclass
{};
template<typename T1>
class someclass<T1,int>
函数模板也可以重载,可以定义有相同名字但形参数目或类型不同的多个函数模板 。也可以定义域函数模板与相同名字的普通函数。
 

26、使用模板实现自己的Queue容器。

[cpp]  view plain copy
  1. <span style="font-size:18px;">#include<iostream>  
  2. using namespace std;  
  3.   
  4. template<typename T>  
  5. class QueueItem  
  6. {  
  7. public:  
  8.     T data;  
  9.     QueueItem *next;  
  10. public:  
  11.     QueueItem(T const &d)  
  12.     {  
  13.         data=d;  
  14.         next=NULL;  
  15.     }  
  16.   
  17. };  
  18. template<typename T>   
  19. class Queue  
  20. {  
  21. public:  
  22.     QueueItem<T> *head;  
  23.     QueueItem<T> *tail;  
  24. public:  
  25.     Queue()  
  26.     {  
  27.         head=NULL;  
  28.         tail=NULL;  
  29.     }  
  30.     T&front()  
  31.     {  
  32.         return head->data;  
  33.     }  
  34.     void push(T const &d)  
  35.     {  
  36.         QueueItem<T> *p=new QueueItem<T>(d);  
  37.         if(empty())  
  38.         {  
  39.             head=tail=p;//注意第一次赋值哦。  
  40.         }  
  41.         else  
  42.         {   tail->next=p;  
  43.             p->next=NULL;//多余哦。  
  44.             tail=p;  
  45.         }  
  46.     }  
  47.     void pop()  
  48.     {  
  49.         QueueItem<T> *temp=head;  
  50.         head=head->next;  
  51.         delete temp;  
  52.   
  53.     }  
  54.     bool empty()  
  55.     {  
  56.         if(!head)  
  57.         {  
  58.             return true;  
  59.         }  
  60.         else  
  61.             return false;  
  62.     }  
  63.     T &operator=(Queue &rhs)  
  64.     {  
  65.           
  66.         QueueItem<T> *h=rhs.head;  
  67.         while(h)  
  68.         {  
  69.             push(h->data);  
  70.             h=h->next;  
  71.         }  
  72.         return *this;  
  73.     }  
  74.     void destroy()  
  75.     {  
  76.         while(!empty())  
  77.         {  
  78.             delete head;  
  79.             head=head->next;  
  80.         }  
  81.     }  
  82. };  
  83. int main(int argc,char**argv)  
  84. {  
  85.     Queue<int> qi;  
  86.   
  87.     qi.push(20);  
  88.     cout<<"front"<<qi.front()<<endl;  
  89.     cout<<"empty:"<<qi.empty()?true:false;  
  90.     cout<<endl;  
  91.     qi.pop();  
  92.     cout<<"empty:"<<qi.empty()?true:false;  
  93.     cout<<endl;  
  94.     qi.push(333);  
  95.     qi.push(33);  
  96.     qi.push(30);  
  97.   
  98.     Queue<int> qi2=qi;  
  99.   
  100.     cout<<"front"<<qi2.front()<<endl;  
  101.     cout<<"empty:"<<qi2.empty()?true:false;  
  102.     cout<<endl;  
  103.     qi2.push(399);  
  104.     cout<<"empty:"<<qi2.empty()?true:false;  
  105.     cout<<endl;  
  106.   
  107.     return 0;  
  108. }</span>  


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值