C++ 类模板


本文章主要介绍C++的类模板,方便初学者建立类模板的概念,想看函数模板的同学可以点击链接: C++ 函数模板

1.声明

类模板是用来生成类的蓝图的,与函数模板不同的是,编译器不能为类模板推断模板参数类型T。因此我们为了使用类模板,必须在模板名后的尖括号中提供额外的信息,vector就是一个类模板。类模板的声明和函数模板的声明类似。在声明前,我们先声明作为类型参数的标识符T,和函数模板类似,typename关键词一样可以换成class。下面给出一个例子(实现一个简单的栈):

// stack1.h stack1头文件前面部分
#include <vector>
#include <stdexcept>
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; //返回栈顶元素		
}

2.成员函数的实现

我们可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数,上面的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页类的作用域)。

3.类模板的使用

在使用类模板时,需要显式的指定模板实参,它不能像函数模板那样,通过编译器自动推倒。下面给出一个例子,补充一下try catch语句的使用:try语句块是用来判断是否有异常;catch语句块捕捉异常,并进行处理;throw是抛出异常。

// 
#include <iostream>
#include <string>
#include <cstdlib>
#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>,用int实例化T,因此intStack是一个创建自Stack<int>的对象,它的元素存储在vector,且类型为intStack<int>现在就是一个类了。对于所有被调用的成员函数,都会实例化出基于int类型的函数代码。但是请注意,只有那些被调用的成员函数,才会产生这些函数对应的实例化代码。
因此,对于类模板,成员函数只有被使用时才会被实例化。这样有两个好处:①节省空间和时间 ;②对于那些未能提供所有成员函数的定义的类型,也可以实例化类模板,这时实例化类模板的类型只要不使用未能提供成员函数的定义所对应的成员函数即可。例如,某些类模板中的成员函数会使用操作符operator<来排序元素,如果不调用这些使用operator<的成员函数,那么对于这些没有定义operator<的类型,也可以实例化该类模板。 有没有发现这种使用方式很人性化。
Stack<int>现在是一个类了,这个类我们称它为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栈的栈,注意<int>后面有空格,要不然编译器会误认为在使用operator>>

4.类模板的特化

类模板的特化是用模板实参来写一个特殊化的类模板,如果要特化一个类模板,需要特化类模板的所有成员函数。虽然也可以只特化某个成员函数,但这个方式并没有特化整个类,也没有特化整个类模板。
特化一个类模板,需要在起始处声明一个template<>,下面给个小例子。同时特化的过程中,必须给每个成员函数重新定位为普通函数,原来类模板中的T也需要被特化的类型取代。注意在使用时需要包含前面定义的头文件stack1.h

template<>
class Stack<std::string> {
...
}

下面给个完整的例子:

// stack2.h  第二个头文件
#include <deque>
#include <string>
#include <stdexcept>
#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<std::string>::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<std::string> elems,而不是原始模板中的vector<std::string> elems,特化的实现是可以和原来的stack1.h模板不同的。
参考书籍:《C++ Primer 第5版 》和《C++ Templates 中文版》

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值