上一章我们介绍了函数模板,今天这章我们来学习类模板。
类模板声明
template是声明类模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个,可以是**「类型参数」** ,也可以是**非类型参数。**类型参数由关键字class或typename及其后面的标识符构成。非类型参数由一个普通参数构成,代表模板定义中的一个常量(后面再单独学习模板参数)。
// T为类型参数,size为非类型参数
template<class T, int size>
class Stack {
// ...
};
一个简单的类模板声明如下:
// 定义单个模板参数的模板类
template<typename T>
class Stack {
public:
viod push(const T&);
void pop();
T top() const;
private:
std::vector<T> elems;
};
类模板的声明跟函数模板很相似,在类Stack内部,类型T可以像其他任意类型一样,用来声明成员变量或者函数。但是需要**「注意:」**
-
如果在全局域中声明了与模板参数同名的类型变量,则该全局的变量被隐藏掉,类内部使用模板参数。
-
模板参数名不能在类模板中重复定义相同名称的成员变量或类型。
-
同一个模板参数名在模板参数表中只能出现一次,多个模板参数时,名称不能相同。
-
在不同的类模板或声明中,模板参数名可以被重复使用。
// 全局类型声明
typedef std::string type;
template<typename type,int size>
class Stack {
type node; // node不是string类型,是模板参数type类型
typedef int type; // 错误:成员名不能与模板参数type同名
};
template<typename type, typename type> // 错误:重复使用名为type的参数
class Person;
template<typename type> //参数名”type”在不同模板间可以重复使用
class Vehicle;
类模板的成员函数
类模板的成员函数可以在类模板的定义时直接实现(inline函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上template及模板参数)。
类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化。接着上面的Stack类定义一个在类内直接实现函数empty()
,也在类的外部实现Push()
函数,如下:
template<typename T>
class Stack {
public:
viod push(const T&);
bool empty() const {
return elems.empty();
}
private:
std::vector<T> elems;
};
// 成员函数的实现
template<typename T>
void Stack<T>::Push(const T& elem) {
elems.push_back(elem);
}
// ...
这个模板类的类型是Stack<T>
,所以在成员函数实现时,必须要使用Stack<T>::
作为域。
类模板静态数据成员
模板类的静态数据成员和普通类,普通函数的静态成员一样,我们想在函数调用后留些信息,而且这些信息随着函数调用的次数发生改变,也就说函数或者类对象执行完后,并没有完全消除而是留下了一下踪迹,比如:函数调用次数,对象声明次数……等等。以类为例,这些变量为静态变量,他在所有类对象中存在,我们可以在每个对象中对其作出修改,可以作为对象之间沟通的桥梁。
类模板中可以定义静态成员,从该类模板实例化得到的所有类都包含同样的静态成员。
#include<iostream>
using namespace std;
template <class T>
class A
{
T m;
static T n;
static int count;
public:
A() {}
A(T a):m(a){
cout<<"Call A(T a)"<<m<<endl;
n+=m;
}
void disp() {cout<<"m="<<m<<", n="<<n<<endl;}
void print() {cout<<"count="<<count<<endl;}
};
template <class T>
T A<T>::n = 0; //静态数据成员的初始化
template<>
int A<int>::count = 10;
template<>
int A<double>::count = 2;
int main()
{
A<int> a(2), b(3);
a.disp();
b.disp();
A<double> c(1.2),d(4.6);
c.disp();
d.disp();
A<int> e;
e.print();
A<double> f;
f.print();
return 0;
}
对静态成员变量在类外部加以声明是必需的。
对于静态变量n,它还是一个“模板变量”,每个A的实例都有它自己的n变量,也就是A,A<double>和A都有单独的n,它们之间并不会共享。
就如上面得具体实例化静态变量count一样,A和 A是两个不同的类。虽然它们都有静态成员变量count,但是显然,A的对象e和A<double>的对象f不会共享一份count。从下面的运行结果也能明显看得出来:
Call A(T a)2
Call A(T a)3
m=2, n=5
m=3, n=5
Call A(T a)1.2
Call A(T a)4.6
m=1.2, n=5.8
m=4.6, n=5.8
count=10
count=2
类模板的static成员函数或变量只有在使用时才会实例化。
类模板实例化
从通用的类模板定义中生成类的过程称为模板实例化。
模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。
而实例化又可以分为隐式实例化和显示实例化,通常模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为隐式实例化,如下:
Stack<int> intStack;
Stack<std::string> strStack;
通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化;我们必须将显式实例化的代码放在包含了模板定义的源文件中(.cpp),而不是仅仅包含了模板声明的头文件中。这样一来,就可以把模板的声明和定义放在不同文件里面了。显式实例化如下:
template class Stack<int>;
template class Stack<std::string>;
当隐式实例化类模板时,同时也实例化了该模板的每个成员声明,但并没有实例化相应的定义,然而,存在例外:
-
如果类模板包含了一个匿名的union,那么该union定义的成员同时也被实例化了;
-
作为实例化类模板的结果,虚函数的定义可能被实例化,但也可能没有,这依赖于具体实现;
C++支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。
一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。
总起来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。
类模板特例化
特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。
有时候,编写单一的模板,它能适应大众化,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化。
template<>
class Stack<std::string> {
private:
std::queue<std::string> elems;
public:
void push(std::string const&);
void pop();
std::string const& top() const;
bool empty() const {
return elems.empty();
}
};
void Stack<std::string>::push(std::string const& elem) {
elems.push_back(elem);
}
void Stack<std::string>::pop() {
assert(!elems.empty());
elems.pop_back();
}
std::string const& Stack<std::string>::top() const {
assert(!elems.empty());
return elems.back();
}
进行特例化必须在起始声明处声明一个”template<>“,表明将原模板的所有模板参数提供实参。
如上面的代码,特化版本的成员函数将被定义为普通成员函数,其中模板参数T被代替为特化的类型。
针对std::string类型的特化改为使用std::queue容器来管理元素(只是用于说明特化版本的实现和模板的实现可以不一样)。
「注意」:模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。
类模板局部特例化
类模板的部分特例化:对于有多个模板参数的类,只为其中某些模板参数提供实参,指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参。
下面我们定义一个多模板参数的类和几种不同方式的局部特例化:
// 多参数类模板
template<typename T1, typename T2>
class Stack {
// ...
};
// 局部特例化T2为int类型
template<typename T>
class Stack<T, int> {
...
};
// 局部特例化T1和T2必须为同样的类型T
template<typename T>
class Stack<T, T> {
...
};
// 局部特例化只接受指针类型
template<typename T1, typename T2>
class Stack<T1*, T2*> {
...
};
下面我们来看下标准库中很经典的使用特例化的std::remove_const类型:
// remove_const原型定义
template< class T >
struct remove_const {
typedef T type;
};
// const特例化,只接受const类型
template< class T >
struct remove_const<const T> {
typedef T type;
};
我们写个例子验证一下上面的功能:
int main() {
int a = 1;
const int b = 2;
remove_const<decltype(a)>::type aa = 3;
remove_const<decltype(b)>::type bb = 4;
std::cout << std::is_same<decltype(aa), int>::value << std::endl;
std::cout << std::is_same<decltype(bb), int>::value << std::endl;
return 0;
}
输出:
true
true
如果没有上面的那个const局部特例化,那么remove_const接收b的类型就是const int,所以第二个输出应该是false,因为decltype(bb)应该等于const int而不是int。
缺省模板
类模板参数可以有缺省实参,给参数提供缺省实参的顺序是先右后左。
// 如果没有传第二个参数,默认为std::vector<T1>, 当然也可以传递std::deque
template<typename T1, typename T2 = std::vector<T1>>
class Stack {
...
};
// 如果没有传第二个参数,默认为size=8
template<typename T, int size = 8>
class Vector {
...
};
实例化如下:
// int栈,默认使用vector管理元素
Stack<int> intStack;
// double栈,它使用std::deque来管理元素
Stack<double,std::deque<double> > dblStack;
小结
-
「在类模版的实现过程中,最好不要将class与函数实现拆成两个文件。」 直接在.h文件中将函数实现还有类做完成。
-
如果拆成两个文件,必须将两个文件全部include到调用函数的文件中,要不然就找不到对应的实现代码,也就无法生成一个实体版的可执行的函数。
-
在类模板的实现中,可以有一个或多个类型还没有被指定。
-
为了使用类模板,你可以传入某个具体类型作为模板实参;然后编译器将会基于该类型来实例化类模板。
-
对于类模板而言,只有那些被调用的成员函数才会被实例化。
-
你可以用某种特定类型特化类模板。
-
你可以用某种特定类型局部特化类模板。
-
你可以为类模板的参数定义缺省值,这些值还可以引用之前的模板参数。