Template(模板)能根据用户指定的特定值或特定类型,自动产生一个函数或类。
以二叉树class template
为例子,本章需要实现属于自己的template
。
二叉树:
概念 :
一棵二叉树是结点的一个有限集合,该集合为空,或者是由一个根节点加上两棵称为左子树和右子树的二叉树组成。
二叉树的特点:
(1)每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
(2)二叉树的子树有左右之分,其子树的次序不能颠倒。
二叉树的四种遍历方式
实现:
我们实现的二叉树包含两个class
:
BinaryTree
用以存储一个指针,指向根节点BTnode
:用来存储节点实值、以及连接至左、右两个子节点的链接此处,节点实值的型别( value type) 正是我们希望加以参数化的部分。
需要提供的操作行为:
规则:
第一个安插至空白树( empty tree)的值,会成为此树的根节点。接下来的每个节点都必须以特定规则插入:
如果小于根节点,就被置于左侧子树,如果大于根节点,就被置于右侧子树。
任何一个值只能在树中出现一次,但是此树有能力记录同一值的被安插次数。
举一个类的例子:
(假设已想好要定义BTnode
类,用以存储结点数值,和左右孩子指针)
class string_BTnode{
public:
//...
private:
string _val;
int _cnt;
string_BTnode *lchlid;
string_BTnode *rchild;
这是二叉树的结点定义。为了存储不同类型的值,我们必须还要实现不同的BTnode
类,比如int_BTnode
、double_BTnode
类,这就很麻烦对吧
所以就有了template。template机制把“与类型相关的”和“独立于类型之外的两部分分离开来。
一些成员函数的操作行为,不会随着包含这些成员函数的类不同而不同。
在一个类模板中,与类型相关的部分会被抽取出来,成为一个或多个参数,举个例子:
template <typename valType>class BTnode;//类模板前置声明
//...
template<typename valType>
class BTnode{
pulic:
//...
private:
valType _val; // 用valType声明类型
int _cnt;
BTnode *_lchild;
BTnode *_rchild;
};
// ...
int main()
{
// 通过类模板来实例化一个类,必须在类模板名称之后,紧接一个尖括号并写上实际类型
BTnode<int>bti;
BTnode<string>bts;
}
valType _val;
,就是用类型参数来声明_val 所属类型bti
和bts
代表两份BTnode
类定义。templateclass 类名{//...};
template <typename elemType>
class BinaryTree{
public:
//...
private:
BTnode<elemType> *_root;
//BTnode(模板)类必须以模板参数列表加以限定的例子
BinaryTreest;
指针_root
指向一个结点值类型为string
的BTnode类
类对象;BinaryTreeit
指针_root
指向一个结点值类型为int
的BTnode类
类对象BTnode模板类
和BinaryTree模板类
需要配合使用,所以这两个类要建立friend
关系,如下template<typename Type>
class BinaryTree;//BinaryTree类的前置声明
template<typename valType>
class BTnode{
friend class BinaryTree<valType>
//...
};
以下就是我们的BinaryTree class template
的部分定义:
#include
using namespace std;
template <typename valType>
class BTnode
{
public:
BTnode();
friend class BinaryTree<valType>;
private:
};
template <typename elemType>
class BinaryTree
{
public:
BinaryTree();
BinaryTree(const BinaryTree&);
~BinaryTree();
BinaryTree& operator= (const BinaryTree&);
bool empty(){ return _root == 0 };
void clear();
private:
BTnode<elemType> *_root;
void copy(BTnode<elemType>*tar, BTnode<elemType>*src);
};
/*
1.在类作用域运算符出现后,其后所有东西都被视为类定义范围内
2.第二次出现的BinaryTree被视为类定义范围内,因为在类作用域运算符出现后,所以不需要加template来限定,而第一次出现的BinaryTree前需要加template
*/
template<typename elemType>
inline BinaryTree<elemType>::BinaryTree() :_root(0)
{}
template<typename elemType>
inline BinaryTree<elemType>::BinaryTree(const BinaryTree &rhs)
{
copy(_root,rhs.root)
}
template<typename elemType>
inline BinaryTree<elemType>::~BinaryTree()
{
clear();
}
template<typename elemType>
inline BinaryTree<elemType>& BinaryTree<elemType>::operator=(const BinaryTree &rhs)
{
if (this != &rhs)
{
clear();
copy(_root, rhs._root);
return *this;
}
}
给模板类定义一个内联成员函数,如同给非模板类定义一个内联成员函数一样。注意在类主体外,类模板的内联成员函数定义语法是这样:
template<typename elemType>
inline BinaryTree<elemType>::BinaryTree() : _root(0){}
//注意这个类模板的内联成员函数是带成员初始化列表的构造函数
template<typename valType>
inline BTnode<valType>::BTnode(const valType &val)
:_val(val)
{
//将模板类型valType视为class类型
_cnt=1;
_lchild=_rchild=0;
}
int main()
{
BTnode<int>btni(42);
}
为什么采用成员初始化列表的形式来初始化模板类类数据成员 _val
而非默认构造函数形式即_val=val;
呢
BTnodebtni(42)
)如果是class类型,会首先调用class的默认构造函数作用于类数据成员身上,其次默认构造函数函数体内以拷贝赋值运算符将const class类型(名) &
类型的形参复制给_val,这样就浪费了很多时间和空间,程序效率就下来了const class类型(名) &
类型的形参复制给_val每当我们插入某个新值时,都必须建立 BTnode
对象,加以初始化,将它连接至二叉树的某处.我们必须自行以new
表达式和 delete
表达式来管理每个节点的内存配置和释放。
insert()
① 如果根节点之值尚未设定,它会由程序的自由空间配置一块新的 BTnode
需要的内存空间**(new)**。否则就调用BTnode
的insert_value()
,直接将新值插人二叉树中;
② 当根节点存在时,insert_value()
才会被调用。小于根节点的所有数值都置放于根节点的左子树,大于根节点的所有数值都置放于根节点的右子树。
③ insert_value()
会通过左侧子节点或右侧子节点递归调用自己,直到以下任何一种情形发生才停止:
- 合乎条件的子树并不存在
- 欲安插的数值已在树中
④ 由于每个数值只能在树中出现一次,所以我以BTnode
的 datamerber _cnt
来记录这个节点的被安插次数。
⑤ 以下为insert_value()
的实现内容:
template<typename valType>
void BTnode<valType>::
insert_value(const valType &val)//注意这里的格式
{
if(val==_val)
{
_cnt++;
return;
}//二叉树有这个结点了,就把结点插入次数记录下来即可
if(val<_val)
//插入的结点比当前结点小,就插到当前结点的左孩子(如果当前结点的左孩子没有)
{
if(!_lchild)
{
_lchild=new BTnode(val);
}
else//当前结点的左孩子有了,插入到当前结点的左孩子的左孩子或右孩子结点
{
_lchild->insert_value(val);//递归调用去插入到当前结点的左孩子的左孩子或右孩子结点
}
}
else
//插入的结点比当前结点大,就插到当前结点的右孩子(如果当前结点的右孩子没有)
{
if(!_rchild)
{
_rchild=new BTnode(val);
}
else
{
_rchild->insert_value(val);
}
}
}
remove()
① 以节点的右子节点取代节点本身,然后搬移左子节点,使它成为右子节点的左子树的叶节点。
② 如果此刻并无右子节点,那么就以左子节点取代节点本身。
③ 以下为remove()
的实现内容:
// 类里面声明
template<typename valType> void BTnode<valType>::remove(const valType &val);
//类外定义
template<typename elemType>
inline void BinaryTree<elemType>::
remove(const elemType &elem)//注意这里的格式!
{
if(_root)
{
if(_root->_val==elem)
{
remove_root();
}
else
{
_root->remove_value(elem,_root);
}
}
}
remove_root()
如果根节点拥有任何子节点,remove_root ()
就会重设根节点
如果右子节点存在,就以右子节点取代之;
如果左子节点存在,就直接搬移,或通过lchi1d_leafi)完成;
如果右予节点为null,_root便以左子节点取代;
template<typename valType>
void BinaryTree<valType>::remove_root()
{
if (!= _root)
return;
BTnode<elemType> *tmp = _root;
if (_root->_rchild)
{
_root = _root->_rchild;
if (tmp->_lchild)
{
BTnode<elemType> *lc = tmp->_lchild;
BTnode<elemType> *newlc = _root->_lchild;
if (!newlc)
_root->_lchild = lc;
else
// lchild_leaf()会遍历整个左子树,寻找空的左子节点
BTnode<elemType>::lchild_leaf(lc, newlc);
}
}
else
_root = _root->_lchild;
delete tmp; //移除先前的根节点
}
remove_value()
// 类里面声明
/*
1.删除二叉树的一个结点(非根结点
2.根结点删除另有其他BTnode类成员函数去管,这里不用多虑
*/
template<typename valType> void BTnode<valType>::remove_value(const valType &val, BTnode *&prev);
//类外定义
template <typename valType>
void BTnode<valType>::lchild_leaf(const valType &val, BTnode *&prev)
{
if (val < _val) //未找到:在左子树中
{
if (!_lchild)
return; //该值不在此二叉树中
else
_lchild->remove_value(val, _lchild); // 往左递归寻找
}
else if (val>_val) //未找到:在右左子树中
{
if (!_rchild)
return;//该值不在此二叉树中
else
_rchild->remove_value(val, _rchild); // 往右递归寻找
}
else // 找到了,重置此树,然后删除该节点
{
if (_rchild)
{
/*
1.这里改变了形参(实参)指针的指向,即“又想改变实参(或说形参)指针本身”
2.*prev=....可以理解为想改变传入形参的实参指针(或说形参指针)所指的对象
*/
prev = _rchild;
if (_lchild)
{
if (!prev->_lchild)
prev->lchild = _lchild;
else
BTnode<valType>::lchild_leaf(_lchild, prev->_lchild);
}
}
else
{
prev = _lchild;
}
delete this;//删除二叉树中我想删除的结点(即一个BTnode类类对象)
}
}
lchild_leaf()
无论remove_root()
或remove_value()
,皆会搬移左子节点,使它成为右子节点的左子树的叶节点。我将这一操作抽离至lchild_leaf()
,那是 BTnode
的 static member function
:
// 类里面声明
template<typename valType> void BTnode<valType>::lchild_leaf(BTnode *leaf,BTnode *subtree);
//类外定义
template <typename valType>
void BTnode<valType>::lchild_leaf(BTnode *leaf, BTnode *subtree)
{
while (subtree->_lchild)
{
subtree = subtree->_lchild;
}
subtree->_lchild = leaf;
}
clear()
我们还需要另一个函数来移除整棵二叉树。
我把这个名为clear()的函数分为两份:一个是inline public
函数,可供重载,另一个是前者的重载版本,用以执行实际工作,并置于private
段.
template <typename elemType>
class Binarytree
{
public:
void clear(){
if (_root)
{
clear(_root);
_root = 0;
}
// ...
}
private:
void clear(BTnode<elemType>*);
// ...
};
template <typename elemType>
void BinaryTree<elemType>::clear(BTnode<elemType> *pt)
{
if (pt)
{
clear(pt->_lchild);
clear(pt->_rchild);
delete pt;
}
}
new表达式可分解为两个操作:
①向程序的空闲空间请求内存,若分配到足够空间,则返回指针,指向新对象;若未分配到足够空间,则抛出异常bad_alloc
(异常处理)
②如果new 类型名(初值),则该新对象被初始化。
举个class类型的例子:
_root = new BTnode<elemType>(elem);
其中elem被传入BTnode模板类的类构造函数。分配内存失败,初始化操作(类构造函数操作)不会发生。
注意分三行写这种让代码清晰简洁的写法
template<typename 自定义类型名(占位符)>
(自己决定这里加不加inline) 返回类型 模板类类名<自定义类型名>::
模板类成员函数名(参数表(比如const 自定义类型名 &参数名等));或者{//。。。}
如上述的remove_value()
中
template<typename valType> void BTnode<valType>::remove_value(const valType &val, BTnode *&prev);
中的prev
被指定为class类型时,因传值(不带&)而产生的昂贵复制开销。const
,即const
类型名 参数名…*&
即为指针的引用*&
出现在函数参表中的意思就是我既想改变传入形参的实参指针(或说形参指针)所指的对象,又想改变实参(或说形参)指针本身ostream& operator<<(ostream&,const BinaryTree<int>&);
//声明
ostream& operator<<(ostream&,const BinaryTree<elemType>&);
//定义
template <typename elemType>
inline ostream&
operator<<(ostream &os,const BinaryTree<elemType>&bt)
{
os<<"Tree:"<<endl;
bt.print(os);
return os;
}
int main()
{
BinaryTree<string>bts;
cout<<bts<<endl;
//编译器将elemType指定为string,产生一个对应的(针对BinaryTree类型的)<<运算符。
BinaryTree<int>bti;
cout<<bti<<endl;
//编译器将elemType指定为int,产生一个对应的(针对BinaryTree类型的)<<运算符
}
template<int len,int beg_pos>
class num_sequence{//还记得这是个基类吗,其派生类是Fibonacci
public:
virtual ~num_sequence(){};
int elem(int pos)const;
const char* what_am_i()const;
static int max_elems(){return _max_elems;}
ostream& print(ostream &os=cout) const;
protected:
virtual void gen_elems(int pos) const = 0;
bool check_integrity(int pos,int size)const;
num_sequence(vector<int>*pe):_pelems(pe){}//定义基类类构造函数
static const int _max_elems=1024;
vector<int> *_pelems;
};
template<int len,int beg_pos>ostream&
//<<运算符的模板函数定义,注意第二参数的写法,因为模板类带了两个常量表达式作为了模板参数(<>内的参数),
//所以这个第二参数也要写成带两个常量表达式作为模板参数(<>内的参数)的形式
operator<<(osteram&os,const num_sequence<len,beg_pos>&ns)
{
return ns.print(os);
}
2)模板参数默认值
template<int length,int beg_pos=1>//这里模板参数(<>内的参数)为常量表达式(或者说普通变量),
//然后有的模板参数(<>内的参数)给了默认参数值(你可以选择替换这个默认参数值,不替换就默认)
class Fibonacci:public num_seuqence<length,beg_pos>{
public:
Fibonacci():num_sequence<length,beg_pos>(&_elems){}
protected:
virtual void gen_elems(int pos)const;
static vector<int> _elems;
};
3)全局作用域内的函数及对象用于模板类
全局作用域内的函数和对象的地址是常量表达式(或说是常数),可以用来做模板参数。举个例子:以函数指针作为参数的数列类
template<void (*pf)(int pos,vector<int>&seq)>//函数指针作为模板参数!(<>内的参数)
class numeric_sequence
{
public:
numeric_sequence(int len,int beg_pos=1)//类构造函数定义
{
if(!pf)//检查函数指针指向是否为null
{
//....产生错误信息并退出该构造函数
}
//。。。
private:
int _len;
int _beg_pos;
vector<int> _elems;
};
该作为模板类的模板参数(<>内的参数)函数指针用法如下:
//pf函数指针指向一句特定数列类型,产生pos个元素,放到vector容器seq内的函数
void fibonacci(int pos,vector<int>&seq);
void pell(int pos,vector<int>&seq);
//...
numeric_seuqence<fibonacci> ns_fib(12);//fibonacci函数地址赋给了pf
numeric_seuqnece<pell> ns_peel(18,8);//pell函数地址赋给了pf
class PrintIt{
public:
PrintIt(ostream &os):_os(os){}
//成员模板函数的定义
template<typename elemType>
void print(const elemType &elem,char delimiter='\n')//函数提供默认参数值
{
_os<<elem<<delimiter;
}
template<typename OutStream>
class PrintIt{
public:
PrintIt(OutStream &os):_os(os){}//PrintIt类构造函数的定义
//模板类里定义成员模板函数
template <typename elemType>
void print(const elemType &elem,char delimiter='\n')
{
_os<<elem<<delimiter;
}
private:
ostream& _os;
};