首先基本知识总结于此文函数模板和类模板 模版特化
补充:
1 类模板的成员函数的定义
可以直接在内模板内部定义,也可以在类模板外部定义。在内部定义时,语法与普通类的成员函数一致。在外部定义时,需要声明这个类模板的参数。语法如下:
template<模板参数列表>
返回类型 类模板名<模板参数列表>::函数名(函数参数列表)
{
函数体
}
其中,在类模板名后面的“模板参数列表”可以省略定义参数的关键字。例如,定义Stack类模板的push()和pop()函数,可以分别在类的内部和外部定义,例如:
#include<iostream>
using namespace std;
template <typename T>
class Stack
{
public:
Stack():s_count(0)
{}
void push(T i)
{
s_array[s_count++]=i;
cout<<"after push the s_count value is: "<<s_count<<endl;
}
T pop();
private:
enum {size=100};
T s_array[size];
int s_count;
};
template <typename T>
T Stack<T>::pop()
{
--s_count;
cout<<"after pop the s_count value is: "<<s_count<<endl;
return s_array[s_count];
}
int main()
{
Stack<int> ob1;
ob1.push(8);
ob1.pop();
system("pause");
}
在类模板的数据成员和成员函数的定义时,在模板参数中,类型参数数可以用来声明成员数据的类型,做成员函数的返回类型、参数类型、局部变量类型。非类型参数则可以当做常量进行使用。例如:
#include<iostream>
using namespace std;
template<typename T,int size>
class Array
{
public:
T & operator[](int i)
{
return m_ary[i];
}
void set(int i,const T &e)
{
m_ary[i]=e;
}
T sum()
{
T sum;
for(int i=0;i<size;i++)
{
sum+=m_ary[i];
}
return sum;
}
private:
T m_ary[size];
};
如果在定义类模板时,需要该类的一个实例,并且该实例的参数就是本模板的参数,则可以直接使用类模板名。例如,在类模板Stack中定义指向另一个Stack的成员打针,以及在复制构造函数中用另一个Stack做参数,可以写成如下形式:
Stack<T>* m_ps;//可以写成Stack* m_ps;
Stack (Stack<T> &s);//可以写成Stack (Stack&s);
模板参数的默认实参
与函数模板不同的是,定义类模板时,可以设定其参数的默认值,即参数的默认实参。这包括默认类型参数和默认非类型参数。其语法同函数的默认实参相同。例如Array模板,设定其参数的默认保存数据类型为Int型,并且其数组尺寸的默认值为100,如下:
template<typename T = int, int size = 100> Array
{
...
};
在实例化一个Array模板时,就可以不必指定其模板参数的实参,如下:
Array<> aryInt;
需要注意的是:与函数模板的实例化不同,类模板实例化时,包围实参列表的尖括号不可省略。
生成类模板的实例
类模板只是一个模板,不是实际的类。使用类模板时必须先实例化,即给类模板参数赋值,包括类型参数和非类型参数。
类型参数的模板实例化:
实例化类模板时,对于类型参数,可以用C++内建的类型实例化,也可以用自定义的类型。与函数模板不同的是,类模板在实例化时没有参数指导机制,所有的模板参数必须指定,除非模板参数带有默认实参。例如:
const int size = 100;//常量
Array<int ,size> aryA; //用Int和size实例化类模板
Array<char,100> aryB;//用char和字面常量实例化模板
Array<> aryD;//用类模板的默认值实例化模板
class Someclass//自定义类型
{
...
} ;//
Array<Someclass,400>//用自定义类型实例化模板
需要注意的是,如果用自定义的类型来实例化一个类模板,则对自定义类型有一定的要求。因为定义类模板时,实际上也对其类型参数做出了某些假设,例如定义Array类时,假设其元素的类型支持同值操作(如设置元素的set()函数),以及相加操作(如求和的sum()函数)。因此,实例化时自定义的类型也需要满足这些假设。
类模板实例可以当做普通的类使用,用其定义的对象与一般类定义的对象在使用上没什么差别。需要注意的是,类模板只在需要的时候才实例化。如果程序中只是使用类模板的某个指针(或引用),而没有通过这个指针(或引用)访问对象的数据成员或成员函数,则该类模板不会实例化。例如,在下面的函数中,类模板不会被实例化:
void Print(Stack <int>& vi)
{
Stack<int>* pvi = &vi;//Stack<int>并不会被实例化
}
但是,调用它们的成员函数或是访问数据成员,则会被实例化:
void Print(Stack <int>& vi)
{
Stack<int>* pvi = &vi;
cout<<pvi->pop()<<endl;//模板会被实例化
}
非类型参数的模板实例化
对于模板的非类型参数,在实例化时只要指定其实参即可。需要注意的是,这个实参必须是一个常量。代码如下:
template<int* ptrInt> CRect{};
template<int size> CRectangle{};
const int c_size = 10;
int size=10;
CRect<new int[10]> rect;//错误,new in[10]只有在运行时才能被计算
CRectangle<c_size> rectangleA //正确 c_size是常量表达式
CRectangle<size> rectangleB // 错误,size的值在编译期不能被计算
CRect<&size> rectA //正确,地址是在编译期定下来的
2 类模板的静态成员
与普通类一样,模板也可以用自己的静态成员。不同的是,每种类型的类模板的实例有自己的一组静态成员。与普通类的静态成员初始化一样,类模板的静态成员也需要类内声明,在类外定义。其在类外定义的格式如下:
template <类型名 参数名1,类型名 参数名2,...>
类型名 类名<类型名 参数名1,类型名 参数名2,...> ::静态数据成员 = 初始化值;
template <class T,int size>
class Stack
{
public:
Stack():m_iCount(0)
{
sNumber++;
cout<<Stack::sNumber<<endl;
}
...
~Stack(){}
private:
static int sNumber;
...
};
//静态成员初始化
template<class T,int size>
int Stack<T,size>::sNumber;//编译器会自动赋值为0,等价于vector<T>::sNumber = 0;
template <class T>
class Stack
{
public:
Stack():m_iCount(0)
{
}
...
~Stack(){}
private:
const static int size = 100;//注意加了const
...
};
初始化size可以替换掉模板中的size参数,可以把其视做是内建的宏。
实例代码:
#include<iostream>
using namespace std;
template <class T>
class CTankManager
{
public:
CTankManager(){}
~CTankManager();
bool PushTank(T& tank);
void OutputTankNumber();
private:
enum{size = 100};
static int siCount;//静态成员
T mTankArray[size];
};
template <class T>
CTankManager<T>::~CTankManager()
{}
template<class T>
int CTankManager<T>::siCount;
template<class T>
bool CTankManager<T>::PushTank(T& tank)
{
if(siCount<size)
{
cout<<"Push tank Type: "<<endl;
tank.Print();
mTankArray[siCount] = tank;
++siCount;
return true;
}
else
return false;
}
template<class T>
void CTankManager<T>::OutputTankNumber()
{
cout<<siCount<<endl;
}
//以下是各种自定义的坦克类
class CT91
{
public:
void Print()
{
cout<<"T91 Tank!"<<endl;
}
};
class CT20
{
public:
void Print()
{
cout<<"T20 Tank!"<<endl;
}
};
class CT21
{
public:
void Print()
{
cout<<"T21 Tank!"<<endl;
}
};
int main()
{
CT91 tk910,tk911,tk912;
CTankManager<CT91> TM91;
TM91.PushTank(tk910);
TM91.PushTank(tk911);
TM91.PushTank(tk912);
TM91.OutputTankNumber();
CT20 tk200,tk201;
CTankManager<CT20> TM20;
TM20.PushTank(tk200);
TM20.PushTank(tk201);
TM20.OutputTankNumber();
CT21 tk210,tk211;
CTankManager<CT21> TM21;
TM21.PushTank(tk210);
TM21.PushTank(tk211);
TM21.OutputTankNumber();
system("pause");
}
结果:
上面例子中,每种类型的坦克都有自己的计数器。
3 类模板的友元
类模板的友元和普通类的友元一样,使其友员类或友元函数可以访问到类中private或protected的数据成员和成员函数。有三种友元可以出现在类模板中:
1)非模板的友元类和友元函数。
2)与模板参数不绑定的友元类和友元函数模板。
3)与模板参数绑定的友元类和友元函数模板。
非模板的友元类和友元函数
类模板可以接受一个非模板的类或者函数作为友元。对于这种友元类和友元函数,不需要在声明友元前声明。因为类模板只在需要的时候才实例化,因此在友元类或者友元函数访问类模板的成员时,类模板已实例化,而此时友元类或友元函数也已知,所以非模板的类或者函数不需要提前声明。
但要把类成员函数声明为友元,则前面必须有类的定义(注意不是声明,是定义),因为一个类成员只能由该类的定义引入。例如,类模板中声明一个友元和友元函数。如下:
class A{
void display();
};
template <class T>
class Temp
{
friend void A::display();
friend class B;
friend void print();
...
};
与模板参数不绑定的友元类和友元函数模板
类模板的友元可以是类模板或函数模板的实例。如果后者的模板实参与前者的模板参数没有任何关系,也就是说友元模板与类模板的参数不绑定,则在声明友元前不需要提前声明。其原因同使用非模板的友元一样。例如:
template <class Type>
class Temp{
template<class T>
friend void A<T>::display();
template<class T>
friend class B;
template<class T>
friend void print(Temp <T>);
...
};
与参数模板绑定的友元类和友元函数模板
如果类模板的友元是类模板或者函数模板的实例,而且其模板实参就是类模板模板的参数,则友元模板必须提前声明。例如:
template <class Type>
class B{...};
template <class Type>
void print(Temp <Type>);
template <class Type>
class A{
void display();
...
};
template <class Type>
class Temp{
friend void A<Type>::display();
friend class B<Type>;
friend void print<Type>:: (Temp<Type>);
...
};
在使用类模板的友元时,需要注意以下几点:
1) 与非模板函数或类不同,模板函数或类在声明为友元之前必须在前面声明过,否则无法通过编译。
2) 声明类模板的友元语句中的<Type>不能少。比如对于函数print(),如果少了<Type>,编译器会将其作为非模板函数对待。即对于Temp<int>,编译器会查找void print(Temp<int>);而对template<class T>void print(Temp<T>)视而不见,而且如果没找到非模板函数则会报错。
有时某些类型不能直接用来实例化类模板,或者说直接实例化不能满足需要,此时就要针对这种类型进行特化,包括完全特化和偏特化。
全特化
类模板的特化是为了针对特殊的类型,进行特殊的处理。例如,下面为Stack类模板添加返回最大值的max()函数:
template <class T>
class Stack
{
public:
T max();
};
template <class T>
T Stack<T>::max()
{
T max=s_iArray[0];
for(int i=0;i<s_iCount;++i)
{
if(max<s_iArray[i])
max=s_iArray[i];
}
return max;
}
此时可以发现,max()函数并不具备通用性,因为假定T类型是内建类型或是重载了operator<的自定义类型,这样是不对的。如果比较的是字符串类型,就要进行模板的特化,将上述程序改写如下:
template <>
class Stack<const char*>
{
public:
Stack<const char*>();//特化版的构造函数
const char* max();//返回ACSII码值最大的字符串
...
};
template <>
Stack<const char*>::vector():m_iCount(0){}
template<>
const char* Stack<const char*>::max()
{
const char* max=s_iArray[0];
for(int i=0;i<s_iCount;++i)
{
if(std::strcmp(max,s_iArray[i])<0)//调用标准函数库API
max=s_iArray[i];
}
return max;
}
int main()
{
Stack<const char*> stack1;//实例化特化版的Stack
return 0;
}
特化template后面的方括号是不需要跟任何参数的。同时,要在类声明的后面写下具体的特化类型。需要注意的是,一定要先声明非特化模板,否则会有编译错误。
偏特化
类模板的偏特化是指需要根据模板的某些参数但不是全部的参数进行特化。例如,Dot类假如要求对在y坐标等于80的点进行一些特别的处理。这时就需要对模板参数的y进行常量化。
template<int x>//偏特化声明
class Dot<x,80>//将一个参数用常量代替
{
public:
Dot():m_X(x),m_Y(80){}//初始化数据成员
//对y坐标为80的点进行特殊处理
private:
int m_x;
int m_y;
};
部分特化的模板实参只列出那些模板实参仍然未知的参数。下面是一个偏特化Stack类的示例:
template <int size>//偏特化声明
class Stack<const char*,size>//第一个模板参数是具体的类型
{
public:
Stack<const char*,size>():s_iCount(0){}//注意Stack后面的声明方式
~Stack<const char*,size>(){}
...
private:
const char* s_iArray[size];//数组类型也要换成具体的类型
int s_iCount;
};
用户也可以这样实例化偏特化类模板:
Stack<const char*,10>Stack1;
类模板的匹配规则
类模板的匹配规则遵循“特化程序最高”的模板。具体说,模板参数最准确匹配的拥有最高的实例化优先权(即被编译器优先选择来实例化)。
template <class T> class Stack{};//普通型
template <class T> class Stack<T*>{};//对指针类型进行特化
template <class T> class Stack<const T*>{};//对const型指针特化
template <> class Stack<void *>{}; //对void*进行特化
根据Stack不同的模板参数,会寻找最匹配的版本进行实例化,如果是指针类型会选择第2行定义的模板来实例化;如果一个类型的指针是空类型的,就会选择第4行进行实例化;常量类型的指针会选择第3行的模板。综合例子:
#include<iostream>
using namespace std;
template<class T,class U>
class CTest
{
public:
void f(){cout<<"主模板"<<endl;}
};
template<class U> //强调第一个模板参数是Int的偏特化类型
class CTest<int ,U>
{
public:
void f(){cout<<"T等于int"<<endl;}
};
template<class T>//强调第二个模板参数是double的偏特化类型
class CTest<T,double>
{
public:
void f(){cout<<"U等于double"<<endl;}
};
template<class T,class U> //强调第一个模板参数是指针类型的偏特化类型
class CTest<T*,U>
{
public:
void f(){cout<<"使用了T*"<<endl;}
};
template<class T,class U>//强调第二个模板参数是指针类型的偏特化类型
class CTest<T,U*>
{
public:
void f(){cout<<"使用了U*"<<endl;}
};
template<class T,class U>//强调两个模板参数都是指针类型的偏特化类型
class CTest<T*,U*>
{
public:
void f(){cout<<"使用了T*和U*"<<endl;}
};
template<class T>//强调两个模板参数类型相同的偏特化类型
class CTest<T,T>
{
public:
void f(){cout<<"T等于U"<<endl;}
};
int main()
{
CTest<float,int> ct1;
ct1.f();
CTest<int,float> ct2;
ct2.f();
CTest<float,double> ct3;
ct3.f();
CTest<float,float> ct4;
ct4.f();
CTest<float*,float> ct5;
ct5.f();
CTest<float,float*> ct6;
ct6.f();
CTest<float*,int*> ct7;
ct7.f();
system("pause");
}
结果:
上例中,都是与匹配度最高的模板匹配。但是,如果模板相近,编译器不能区分应该使用哪个模板,会报编译错误,例如:
CTest<int,int> ct8;
ct8.f();//错误,无法区分template<class U>class CTest<int,U>和template<class T>class CTest<T,T>
所以,在编写时一定要避免模棱两可的错误。