类模板是用来生成类的蓝图的,与函数模板不同的是,编译器不能为类模板推断模板参数类型T。因此我们为了使用类模板,必须在模板名后的尖括号中提供额外的信息,vector
就是一个类模板。类模板的声明和函数模板的声明类似。在声明前,我们先声明作为类型参数的标识符T
,和函数模板类似,typename
关键词一样可以换成class
。下面给出一个例子(实现一个简单的栈):
// stack1.h stack1头文件前面部分
#include
#include
template <typename T>
class Stack {
private:
std::vector<T> elems; //存储元素的容器
public:
bool empty() const{ // 判断元素是否为空
return elems.empty();
}
Stack(); //构造函数
void push(T const &); //压入元素
T pop(); // 弹出元素
T top() const; //返回栈顶元素
}
我们可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数,上面的empty()
就是一个内联函数,它被其他函数也频繁调用。这里在下面补充了4
个知识点:内联函数、声明、定义、函数后面加const
。
内联函数:以 inline
修饰的函数,编译时C++编译器在调用内联函数的地方直接展开,不再像其他函数一样去调用,没有调用建立栈帧的开销。内联函数一般代码量少,没有for
循环,需要频繁调用,提升程序运行的效率。
声明:告诉编译器函数名称及如何调用函数。
定义:函数的具体实现。
函数后面加const
:不改变数据成员的函数都在函数末尾加上const
关键字进行标识,提高可读性。在函数前面加const
是表示返回值不能修改。在函数入参里面加入const
是为了不改变传入的参数的值。
函数的声明和定义举例如下:
//函数的声明 一个例子,和刚刚的头文件无关。
int XXX(int num1, int num2);
// 函数的定义
int XXX(int num1, int num2) {
return num1 + num2;
}
类模板的成员函数跟普通函数一样,只是类模板的每个实例有自己版本的成员函数。因此类模板的成员函数具有和模板相同的模板参数。在定义类模板之外的成员函数时,需要以关键字template
开始,后接类模板的参数列表。和其他在类外面写成员函数类似,也必须说明属于哪个类。
// stack1.h stack1头文件后面部分
template <typename T>
void Stack<T>::push(T const& data) {
elems.push_back(data); //将传入的实参data拷贝到私有成员elems中
}
// 注意:vector的pop_back()方法只是删除vector对象末尾的元素,不返回vector对象,所以需要重写pop函数
template <typename T>
T Stack<T>::pop() {
if (elems.empty()) {
throw std::out_of_range("Stack<>::pop():empty Stack"); //如果为空,抛出异常
}
T tempdata = elems.back(); //先保存末端元素的拷贝
elems.pop_back(); //删除末端的元素
return tempdata; // 返回对象
}
// 返回最后一个元素
template<typename T>
T Stack<T>::top() const {
if(elems.empty()){
throw std::out_of_range("Stack::top(): empty Stack"); // 如果为空,抛出异常,无法返回末端元素
}
return elems.back(); //返回最后元素的拷贝
}
上面的函数均是在类外面写的定义,如果定义在类声明里面,那么这些函数都会变成内联函数。当我们在类模板外定义其成员时,必须记住,外面并不在类的作用域中,直到遇到类名才表示进入到类的作用域(可参见C++primer第五版263页
类的作用域)。
在使用类模板时,需要显式的指定模板实参,它不能像函数模板那样,通过编译器自动推倒。下面给出一个例子,补充一下try catch
语句的使用:try
语句块是用来判断是否有异常;catch
语句块捕捉异常,并进行处理;throw
是抛出异常。
//
#include
#include
#include
#include "stack1.h" // 把刚刚的代码组成一个头文件
int main {
try {
Stack<int> intStack; // 元素类型为int的栈
Stack<std::string> stringStack; // 元素类型为string的栈
//使用int栈
intStack.push(7);
std::cout << intStack.top() << std::endl; //输出int栈顶的元素
// 使用string栈
stringStack.push("hello");
std::cout << stringStack.top() << std::endl; //输出string栈顶的元素
stringStack.pop(); //删除string栈顶的元素,也就是最后一个元素
stringStack.pop(); //再次删除string栈顶的元素,报错
}
catch (std::exception const& ex){
std::cerr << "Exception:" << ex.what() << std::endl;
return EXIT_FAILURE; //程序退出,且带有ERROR标记
}
}
通过声明类型Stack
,用int
实例化T
,因此intStack
是一个创建自Stack
的对象,它的元素存储在vector
,且类型为int
。Stack
现在就是一个类了。对于所有被调用的成员函数,都会实例化出基于int
类型的函数代码。但是请注意,只有那些被调用的成员函数,才会产生这些函数对应的实例化代码。
因此,对于类模板,成员函数只有被使用时才会被实例化。这样有两个好处:①节省空间和时间 ;②对于那些未能提供所有成员函数的定义的类型,也可以实例化类模板,这时实例化类模板的类型只要不使用未能提供成员函数的定义所对应的成员函数即可。例如,某些类模板中的成员函数会使用操作符operator<
来排序元素,如果不调用这些使用operator<
的成员函数,那么对于这些没有定义operator<
的类型,也可以实例化该类模板。 有没有发现这种使用方式很人性化。
Stack
现在是一个类了,这个类我们称它为int栈
,我们可以像平时使用int
类型那样使用这个int栈
。
//参数data是int栈的类型,注意这个const是为了不让函数体改变data的内容
void foo(Stack<int> const& data) {
Stack<int> intStackArray[10]; // intStackArray是含有10个int栈的数组
...
}
我们也可以用类型别名typedef
,更加方便使用类模板,如:
typedef Stack<int> IntStack;
void foo(IntStack const& data) {
IntStack intStackArray[10]; // 这样的IntStack 看着是不是更加舒服一些,intStackArray是含有10个int栈的数组
...
}
再举几个例子:
Stcak<float*> floatPtrStack; //元素类型为浮点型指针的栈
Satck<Stack<int> > intStackStack; //元素类型为int栈的栈,注意后面有空格,要不然编译器会误认为在使用operator>>
类模板的特化是用模板实参来写一个特殊化的类模板,如果要特化一个类模板,需要特化类模板的所有成员函数。虽然也可以只特化某个成员函数,但这个方式并没有特化整个类,也没有特化整个类模板。
特化一个类模板,需要在起始处声明一个template<>
,下面给个小例子。同时特化的过程中,必须给每个成员函数重新定位为普通函数,原来类模板中的T
也需要被特化的类型取代。注意在使用时需要包含前面定义的头文件stack1.h
。
template<>
class Stack<std::string> {
...
}
下面给个完整的例子:
// stack2.h 第二个头文件
#include
#include
#include
#include "stack1.h"
template <>
class Stack<std::string> {
private:
std::deque<std::string> elems; //存储元素的队列
public:
bool empty() const{ // 判断元素是否为空
return elems.empty();
}
Stack(); //构造函数
void push(std::string const &); //压入元素
std::string pop(); // 弹出元素
std::string top() const; //返回栈顶元素
}
void Stack<std::string>::push(std::string const& data) {
elems.push_back(data); //将传入的实参data拷贝到私有成员elems中
}
// 这里仍然让pop返回了弹出的对象
std::string Stack<std::string>::pop() {
if (elems.empty()) {
throw std::out_of_range("Stack::pop():empty Stack" ); //如果为空,抛出异常
}
std::string tempdata = elems.back(); //先保存末端元素的拷贝
elems.pop_back(); //删除末端的元素
return tempdata; // 返回对象
}
// 返回最后一个元素
std::string Stack<T>::top() const {
if(elems.empty()){
throw std::out_of_range("Stack::top(): empty Stack"); // 如果为空,抛出异常,无法返回末端元素
}
return elems.back(); //返回最后元素的拷贝
}
这里面我们采用的私有成员的用的是std::deque
,而不是原始模板中的vector
,特化的实现是可以和原来的stack1.h
模板不同的。
参考书籍:《C++ Primer 第5版 》和《C++ Templates 中文版》