【c++模板札记二】类模板的介绍以及使用方法
【c++模板笔记二】类模板的介绍以及使用方法



我发现我们用int去实例化类模板A的话,调用show和fun成员函数都没有问题。如果我们用Student这个类去实例化模板的话可不可以呢?我们发现,我们调用show函数时没有问题的。如果我们把30行的注释去掉的话,又会怎样?


Array是一个类模板,可以实例化为多种类型的类,成员变量为含有3个元素的数组,可以把这个实例化后的类用来当做含有3个元素的数组使用。我们实例化了一个int类型的Array类,定义了一个对象。这个对象就是一个含有3元素的一维数组。




2015年2月16日 周一雨
很久没有更新笔记,上一篇博客介绍了什么是模板,主要是告诉大家函数模板。今天我们就再来一起看一看什么是类模板。
函数模板就是把函数作为模板去使用,类模板当然也就是把类作为模板去使用。
——————————————————————分割线————————————————————————
一、什么是类模板?
模板的目的就是为了淡化数据类型的要求,作出通用数据类型的模板。类中的成员变量和成员函数都牵涉到了数据类型。
在成员函数、成员变量以及基类中包含有类型参数的类称为类模板。
和函数模板一样,类模板只是一个模板,并不是真正的类。
二、类模板的定义和使用
1.定义语法:
template<typename 类型形参1, typename 类型形参2, ...>
class 类模板名 [: public 基类] {
class 类模板名 [: public 基类] {
...
};
和函数模板一样,都在上方加上一句template声明模板参数列表。然后在类中就可以使用这些类型形参了。
2.使用语法:
类模板名<类型实参1, 类型实参2, ...> 对象 (构造实参);
类模板名<类型实参1, 类型实参2, ...>& 对象引用 = ...;
类模板名<类型实参1, 类型实参2, ...>* 对象指针 = ...;
类模板名<类型实参1, 类型实参2, ...>& 对象引用 = ...;
类模板名<类型实参1, 类型实参2, ...>* 对象指针 = ...;
我们一起来看个例子大家就懂了:
#include <iostream> #include <typeinfo> using namespace std; template<typename T> class A{ T m_data; public: void show (){ cout<<typeid(m_data).name()<<endl; } }; int main() { A<int> a1, *a2; a1.show(); a2->show(); A<int>& a3 = a1; a3.show(); A<double> a4; a4.show(); return 0; }
A就是一个类模板,a1为A实例化(int)的对象,a2则是一个对象指针,a3是一个对象引用。a4是double实例化A模板的对象。
三、类模板与函数模板的异同
1.类模板和函数模板都需要二次编译
请大家参看:【c++模板笔记一】。我们看到函数模板的原理是二次编译。同样的类模板也需要二次编译。
类模板 -- 实例化为 ->
类 -- 实例化为 -> 对象
类模板先在编译期的时候实例化为类,再在运行的时候实例化为真正的对象。其实类中的这个二次编译也叫二次实例化。
2.类不能隐式推断
我们在上一篇中讲到了函数模板的隐式推断,但是类模板却不能隐式推断。
我们可以看到,如果我们给类模板不提供模板参数列表的话,我们可以看见比编译器报错了(a之前缺少模板参数)
四、类模板的实例化
1.用类去实例化模板
上面说过,类模板的二次编译,其实也是二次实例化。但是如果用一个类类型去实例化一个类模板的话,又要注意什么呢?
用类去实例化一个类模板,规则如下:
1)该类型需要满足模板中对类型化参数的调用规则。
2)对于那些不使用的调用规则,实例化类型可以不予支持。
2)对于那些不使用的调用规则,实例化类型可以不予支持。
#include <iostream> #include <typeinfo> using namespace std; class Student{ int m_age; string m_name; public: Student(int age=0,string name=""):m_age(age),m_name(name){} }; template<typename T> class A{ T m_data; public: void show (){ cout<<"show()"<<endl; } void fun (){ cout<<"fun()"<<endl; cout<<m_data<<endl; } }; int main() { A<int> a1; a1.show(); a1.fun(); A<Student> a2; a2.show(); //a2.fun(); return 0; }
我发现我们用int去实例化类模板A的话,调用show和fun成员函数都没有问题。如果我们用Student这个类去实例化模板的话可不可以呢?我们发现,我们调用show函数时没有问题的。如果我们把30行的注释去掉的话,又会怎样?
如果我们用Student类去实例化类模板A的话,如果调用成员函数fun的话, 程序就开始报错 了。m_data这个类的类型是Student类型的,如果要cout<<m_data的话,我们还需要对“>>”运算符进行重载。
这就是我们上面说的原则。
2.递归实例化
我们一起用模板做一个数组模板类,这种模板能存储任何数据类型。
#include <iostream> using namespace std; template<typename T> class Array{ T m_size[3]; public: T& operator[](int flag){ return m_size[flag]; } }; int main() { Array<int> arr; arr[0]=10; arr[1]=20; arr[2]=30; for(int i=0;i<3;++i) cout<<arr[i]<<" "; cout<<endl; return 0; }
Array是一个类模板,可以实例化为多种类型的类,成员变量为含有3个元素的数组,可以把这个实例化后的类用来当做含有3个元素的数组使用。我们实例化了一个int类型的Array类,定义了一个对象。这个对象就是一个含有3元素的一维数组。
那我们如何去实例化一个3x3的二维数组类,就需要用到递归实例化,如:Array<Array<int> >。这就是递归实例化。
我们把来一起用代码来实现:
#include <iostream> using namespace std; template<typename T> class Array{ T m_size[3]; public: T& operator[](int flag){ return m_size[flag]; } }; int main() { Array<Array<int> > arr; for(int i=0;i<3;++i) for(int j=0;j<3;++j) arr[i][j]=(i+1)*3+(j+1); for(int i=0;i<3;++i){ for(int j=0;j<3;++j) cout<<arr[i][j]<<" "; cout<<endl; } return 0; }
这种实例化还是很实用的,可以适用于很多种递归的数据结构(如二位数组,树等等递归结构)。
五、类模板中的静态成员变量
我们都知道类中可以包含静态数据(静态成员变量和静态成员函数),不懂的请看我的:【c++笔记七】。那类模板能不能也包含静态数据呢?当然没有问题。
但是我们还是要注意点一点,模板不是真正的类,仅仅是模板而已。类模板的静态成员变量既不是一个对象一个,也不是一个模板一个,而是一个实例化类一个。我们一起来看代码:
#include <iostream> using namespace std; template<typename T> class A{ public: static T m_data; }; class B{ public: static int num; }; int B::num = 0; int main() { cout<<"B::num : "<<B::num<<endl; return 0; }
B类是一个普通类,有一个静态成员变量。静态成员变量需要初始化的,所以需要在类外进行初始化,初始化的语法如第13行。但是,A却是一个模板类,但是我们要如何去初始化这个模板类中的静态成员变量呢?
我们前面说过,类模板只是模不是类,不能当类去使用它。只有类才有作用域,所以无法直接在A后面跟上“::”作用域限定符。并且,声明m_data的类型是T,在类外的话没有这个T。所以,初始化模板的静态成员变量应该这么去写:
我们前面说过,类模板只是模不是类,不能当类去使用它。只有类才有作用域,所以无法直接在A后面跟上“::”作用域限定符。并且,声明m_data的类型是T,在类外的话没有这个T。所以,初始化模板的静态成员变量应该这么去写:
#include <iostream> using namespace std; template<typename T> class A{ public: static T m_data; }; template<typename T> T A<T>::m_data = 100; class B{ public: static int num; }; int B::num = 0; int main() { cout<<"A::m_data : "<<A<int>::m_data<<endl; cout<<"B::num : "<<B::num<<endl; return 0; }
六、局部特化
记得我们上一篇提到过模板重载吧?我们说过函数模板可以和普通函数构成重载关系。如:
template<typename T> T Max(T a,T b){ return a>b?a:b; } const char* Max(const char* a,const char* b){ return strcmp(a,b)>0?a:b; }下面的那个普通函数Max和上面函数模板Max构成了重载关系。如果在类模板中也进行这样的重载,就会出现什么情况呢?
编译器报错了,说13行的普通成员函数不能构成重载。那么怎么去解决这个问题呢?一个方法就是全部特化。
#include <iostream> #include <cstring> using namespace std; template<typename T> class A { T m_a; T m_b; public: A (T a, T b) : m_a (a), m_b (b) {} T max () { return m_a > m_b ? m_a : m_b; } }; template<> class A<const char*>{ const char* m_a; const char* m_b; public: A (const char* a, const char* b) : m_a (a), m_b (b) {} const char* max () { return strcmp (m_a, m_b) > 0 ? m_a : m_b; } }; int main() { A<const char*> a("hello","world"); cout<<a.max()<<endl; return 0; }
我们可以看第14行到23行,就是模板A的全局特化。用const char*去特例化模板类A。
另外一种方法就是局部特化。
#include <iostream> #include <cstring> using namespace std; template<typename T> class A { T m_a; T m_b; public: A (T a, T b) : m_a (a), m_b (b) {} T max () { return m_a > m_b ? m_a : m_b; } }; template<> const char* A<const char*>::max(){ return strcmp(m_a,m_b)>0?m_a:m_b; } int main() { A<const char*> a("hello","world"); cout<<a.max()<<endl; return 0; }
第14行到17行,就是局部特化。相比全部特化,省去很多事情。
七、类模板的缺省模板参数
我们经常看见函数有缺省参数,如int max(int a=10,int b=20)。
类模板其实也有缺省的模板参数,但是c++的98标准并没有这一东西,是在c++11标准中新引入的。我们看看代码就行:
#include <iostream> #include <typeinfo> using namespace std; template<typename T=double> void fun(){ T a; cout<<typeid(a).name()<<endl; } int main() { fun(); return 0; }
我们给类模板的模板参数指定默认的参数类型为double型。
————————————————————分割线————————————————————————
类模板的知识就是这些,没有什么比较难的知识点,主要是大家懂了之后要会去使用,为以后的STL打个基础。
总结一下:首先我们介绍什么是类模板,怎么去定义和使用类模板。其次我们比较了类模板和函数模板的异同,然后我们讲解了类模板的实例化。再者我们讲了类模板的静态成员变量和局部特化。特别是要会使用局部特化。最后还讲了类模板的缺省参数,是一个非常有用的东西。