本文唯一重点:使用VS2019写C++,在写模版类中用到了模板参数的成员函数时,该成员函数不能放到.cpp文件里,也就是说头文件不能与cpp文件分离。这就意味着,你头文件定义的含模版的地方必须在头文件中实现,没用模版定义的地方可以放在cpp中实现。
文章目录
我们在前面已经用了很多的模板类了,比如vector<int>等等,本章的目标是打造一个模板类二叉树。
二叉树包含两个class,一个是BinaryTree,用来储存一个指针,指向根结点,另一个是BTnode,用来存储结点值以及链接左、右两棵子树,此处“结点值的类型”便是我们想要加以参数化的地方。
我们希望我们的BinaryTree类提供插入、移除、搜索、清除、前序遍历、中序遍历、后续遍历接口。
我们的插入的实现满足以下性质:
第一个插入空树的值,会成为此树的根结点。接下来每个结点必须以特定规则插入:如果小于根结点,就被放置在左子树,如果大于根结点,就被放置在右子树。任何一个值只能在树中出现一次,但是此树有能力记录同一值的插入此树,也就是说,这是一颗二叉排序树。
6.1 被参数化的类型
我们可以用template定义模板类,可以把类型名给参数化提取出来。
template <typename elemtype>
class BinaryTree {
public:
private:
BTnode<elemtype>* _root;
//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
};
template <typename elemtype>
class BTnode {
public:
friend class BinaryTree<elemtype>;
//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
//这里加上<type>是表明以type实例化的类是以type实例化的BTnode的朋友
private:
BTnode* _left;
BTnode* _right;
type _val;
int count;
};
6.2 Class Template的定义
总体框架:
template<typename elemtype>//先声明BinaryTree是一个模板类
class BinaryTree;
template <typename elemtype>
class BTnode {
public:
friend class BinaryTree<elemtype>;
//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
//这里加上<elemtype>是表明以elemtype实例化的BinaryTree类是类BTnode的朋友
BTnode(const elemtype& val);
void insert_val(const elemtype& val);
void remove_val(const elemtype& val, BTnode*& prev);
static void lchild_leaf(BTnode* leaf, BTnode* subtree);
//辅助删除的函数
void preorder(BTnode* root, ostream& os = cout) const;
void inorder(BTnode* root, ostream& os = cout) const;
void postorder(BTnode* root, ostream& os = cout) const;
private:
BTnode* _left;
BTnode* _right;
elemtype _val;
int _count;
};
template <typename elemtype>
class BinaryTree {
public:
BinaryTree();//默认构造函数
BinaryTree(BinaryTree&);//拷贝构造函数
~BinaryTree();//析构函数
BinaryTree& operator=(const BinaryTree&);
bool empty() { return _root == nullptr; }
void clear()//清空(释放堆区内存)这个链式二叉树的函数接口
{
if (_root)
{
clear(_root);
_root = nullptr;
}
}
void insert(const elemtype& val);//插入
void remove(const elemtype& val);//删除
void preorder(ostream& os = cout);
void inorder(ostream& os = cout);
void postorder(ostream& os = cout);
private:
BTnode<elemtype>* _root;
//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
void copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
//将src所指的子树复制到tar所指子树
void remove_root();//辅助删除的函数
void clear(BTnode<elemtype>*);//递归实现删除链式二叉树的函数
void _copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
//递归实现赋值功能的子函数
};
以template编程时,在类体外给成员函数的定义的语法比较复杂,如下:
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree()
{
_root = nullptr;
}
首先以一个模板开始,第一个BinaryTree的作用是以elemtype实例化BinaryTree,然后提供一个类作用范围标识符,第二个BinaryTree()就在BinaryTree<elemtype>内了,下面是其他几个的实现。
//拷贝构造函数
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree(const BinaryTree& Bit)
{
copy(_root, Bit._root);
}
//析构函数
template <typename elemtype>
inline BinaryTree<elemtype>::~BinaryTree()
{
clear();
}
//赋值操作符重载
template <typename elemtype>
inline BinaryTree<elemtype>& BinaryTree<elemtype>::operator=(const BinaryTree& BiT)
{
if (this != &BiT)
{
clear();
copy(_root, BiT._root);
}
return *this;
}
6.3 Template类型的处理
考虑到我们指定的类型可能是某种类,所以我们在设计如find函数的时候,为了效率最好传引用,并且写构造函数的时候,_val最好用成员逐次初始化列表(这样如果_val的类型是某种类,就可以调用它的构造函数,因为它可能没有重载=运算符)。
template<typename elemtype>
BTnode<type>::BTnode(const elemtype& val):_val(val)
{
_left = _right = nullptr;
_count = 1;
}
//不建议下面这样写
template<typename elemtypetype>
BTnode<type>::BTnode(const elemtypetype& val)
{
_val = val;
_left = _right = nullptr;
_count = 1;
}
放在函数体内在elemtype是类的时候,效率明显比前者低。
因为,构造函数体内对_val的赋值操作可以分成两个步骤:
- 函数体执行前,类的默认构造函数会先作用在_val上。
- 函数体会以赋值运算符将val复制给_val。
如果我们使用放在函数体外的成员逐次初始化列表,就只需要一个步骤就可以完成工作:以复制构造函数将val复制给_val。
6.4 实现Class Template
首先是向树插入结点:(注意模板参数名字要写的一样)
template <typename elemtype>
void BinaryTree<elemtype>::insert(const elemtype& val)
{
if (_root == nullptr)
_root = new BTnode<elemtype>(val);
else
_root->insert_val(val);
}
new表达式的动作可以分成两步:
- 向程序的空闲空间请求内存。如果分配到足够的空间,就返回一个指针,指向新对象。(如果空间不足,就会抛出bad_alloc异常)
- 如果第一步成功了,并且外界制定了一个初值,这个新对象便会以最恰当的方式初始化。
我们这里的恰当方式明显是会调用BTnode的构造函数。
接下来是插入结点,在二叉排序树讲过,就是一个递归的过程。
template<typename elemtype>
void BTnode<type>::insert_val(const elemtype& val)
{
if (_val == val)
{
++_count;
return;
}
if (val < _val)//如果传入的值小于根结点 就插入在左子树
{
if (_left != nullptr)
{
_left->insert_val(val);
}
else
{
_left = new BTnode<elemtype>(val);
return;
}
}
else
{
if (_right != nullptr)
{
_right->insert_val(val);
}
else
{
_right = new BTnode<elemtype>(val);
return;
}
}
}
这里的移除算法也在二叉排序树中讲过,我们就不讲了。
template <typename elemtype>
void BTnode<type>::lchild_leaf(BTnode* leaf, BTnode* subtree)
{
while (subtree->_left)
subtree = subtree->_left;
subtree->_left = leaf;
}
template <typename elemtype>
void BinaryTree<elemtype>::remove_root()
{
if (!_root)
return;
BTnode<elemtype>* tmp = _root;
if (_root->_right)
{
_root = _root->_right;
if (tmp->_left)
{
BTnode<elemtype>* lc = tmp->_left;
BTnode<elemtype>* newlc = _root->_left;
if (!newlc)
_root->_left = lc;
else
BTnode<elemtype>::lchild_leaf(lc, newlc);
}
}
else
_root = _root->_left;
delete tmp;
}
template <typename elemtype>
void BTnode<type>::remove_val(const type& val, BTnode*& prev)
//为了修改指针的值,传
{
if (val < _val)
{
if (!_left)
return;
else
_left->remove_val(val, _left);
}
else if (val > _val)
{
if (!_right)
return;
else
_right->remove_val(val, _right);
}
else
{
if (_right)
{
prev = _right;
if (_left)
if (!prev->_left)
prev->_left = _left;
else
BTnode<type>::lchild_leaf(_left, prev->_left);
}
else
prev = _left;
delete this;
}
}
这里遇到了一个VS编译器的问题,如果头文件中用template声明的部分,必须在头文件中实现。
其他一些功能没有什么新的知识,我们就不单独拿出来直接放到汇总里了。
汇总:
//Template.h
#pragma once
using namespace std;
#include <vector>
#include <string>
#include <algorithm>
#include <iterator>
#include <iostream>
template<typename elemtype>
class BinaryTree;
template <typename elemtype>
class BTnode {
public:
friend class BinaryTree<elemtype>;
//因为BinaryTree需要能访问此类的数据,所以声明成friend比较好
//这里加上<type>是表明以type实例化的类是以type实例化的BTnode的朋友
BTnode(const elemtype& val);
void insert_val(const elemtype& val);
void remove_val(const elemtype& val, BTnode*& prev);
static void lchild_leaf(BTnode* leaf, BTnode* subtree);
void preorder(BTnode* root, ostream& os = cout) const;
void inorder(BTnode* root, ostream& os = cout) const;
void postorder(BTnode* root, ostream& os = cout) const;
private:
BTnode* _left;
BTnode* _right;
elemtype _val;
int _count;
};
template<typename elemtype>
BTnode<elemtype>::BTnode(const elemtype& val):_val(val)
{
_left = _right = nullptr;
_count = 1;
}
template <typename elemtype>
class BinaryTree {
public:
BinaryTree();
BinaryTree(BinaryTree&);
~BinaryTree();
BinaryTree& operator=(const BinaryTree&);
bool empty() { return _root == nullptr; }
void clear()
{
if (_root)
{
clear(_root);
_root = nullptr;
}
}
void insert(const elemtype& val);
void remove(const elemtype& val);
void preorder(ostream& os = cout);
void inorder(ostream& os = cout);
void postorder(ostream& os = cout);
private:
BTnode<elemtype>* _root;
//以<elemtype>类型实例化的BTnode是以<elemtype>类型实例化BinaryTree类的成员
void copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
//将src所指的子树复制到tar所指子树
void remove_root();
void clear(BTnode<elemtype>*);
void _copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src);
};
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree()
{
_root = nullptr;
}
template <typename elemtype>
inline BinaryTree<elemtype>::BinaryTree(BinaryTree& Bit)
{
copy(_root, Bit._root);
}
template <typename elemtype>
inline BinaryTree<elemtype>::~BinaryTree()
{
clear();
}
template <typename elemtype>
inline BinaryTree<elemtype>& BinaryTree<elemtype>::operator=(const BinaryTree& BiT)
{
if (this != &BiT)
{
clear();
copy(_root, BiT._root);
}
return *this;
}
template <typename elemtype>
void BinaryTree<elemtype>::insert(const elemtype& val)
{
if (_root == nullptr)
_root = new BTnode<elemtype>(val);
else
_root->insert_val(val);
}
template<typename elemtype>
void BTnode<elemtype>::insert_val(const elemtype& val)
{
if (_val == val)
{
++_count;
return;
}
if (val < _val)//如果传入的值小于根结点 就插入在左子树
{
if (_left != nullptr)
{
_left->insert_val(val);
}
else
{
_left = new BTnode<elemtype>(val);
//_left->_val = val;
return;
}
}
else
{
if (_right != nullptr)
{
_right->insert_val(val);
}
else
{
_right = new BTnode<elemtype>(val);
//_right->_val = val;
return;
}
}
}
template <typename elemtype>
void BinaryTree<elemtype>::remove(const elemtype& val)
{
if (_root)
{
if (_root->_val == val)
remove_root();
else
_root->remove_val(val, _root);
}
}
template <typename elemtype>
void BTnode<elemtype>::lchild_leaf(BTnode* leaf, BTnode* subtree)
{
while (subtree->_left)
subtree = subtree->_left;
subtree->_left = leaf;
}
template <typename elemtype>
void BinaryTree<elemtype>::remove_root()
{
if (!_root)
return;
BTnode<elemtype>* tmp = _root;
if (_root->_right)
{
_root = _root->_right;
if (tmp->_left)
{
BTnode<elemtype>* lc = tmp->_left;
BTnode<elemtype>* newlc = _root->_left;
if (!newlc)
_root->_left = lc;
else
BTnode<elemtype>::lchild_leaf(lc, newlc);
}
}
else
_root = _root->_left;
delete tmp;
}
template <typename elemtype>
void BTnode<elemtype>::remove_val(const elemtype& val, BTnode*& prev)
{
if (val < _val)
{
if (!_left)
return;
else
_left->remove_val(val, _left);
}
else if (val > _val)
{
if (!_right)
return;
else
_right->remove_val(val, _right);
}
else
{
if (_right)
{
prev = _right;
if (_left)
if (!prev->_left)
prev->_left = _left;
else
BTnode<elemtype>::lchild_leaf(_left, prev->_left);
}
else
prev = _left;
delete this;
}
}
template <typename elemtype>
void BinaryTree<elemtype>::clear(BTnode<elemtype>* pt)
{
if (pt)
{
clear(pt->_left);
clear(pt->_right);
delete pt;
}
}
template <typename elemtype>
void BinaryTree<elemtype>::copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src)
{
if (tar != nullptr)
{
clear(tar);
}
_copy(tar, src);
}
template <typename elemtype>
void BinaryTree<elemtype>::_copy(BTnode<elemtype>*& tar, BTnode<elemtype>*& src)
{
if (src == nullptr)
{
tar = nullptr;
return;
}
tar = new BTnode<elemtype>(src->_val);
_copy(tar->_left, src->_left);
_copy(tar->_right, src->_right);
}
template <typename elemtype>
void BTnode<elemtype>::preorder(BTnode* root, ostream& os) const
{
if (root)
{
os << root->_val << ' ';
if (root->_left)
preorder(root->_left, os);
if (root->_right)
preorder(root->_right, os);
}
}
template <typename elemtype>
void BTnode<elemtype>::inorder(BTnode* root, ostream& os) const
{
if (root)
{
if (root->_left)
preorder(root->_left, os);
os << root->_val << ' ';
if (root->_right)
preorder(root->_right, os);
}
}
template <typename elemtype>
void BTnode<elemtype>::postorder(BTnode* root, ostream& os) const
{
if (root)
{
if (root->_left)
preorder(root->_left, os);
if (root->_right)
preorder(root->_right, os);
os << root->_val << ' ';
}
}
template <typename elemtype>
void BinaryTree<elemtype>::preorder(ostream& os)
{
_root->preorder(_root, os);
}
template <typename elemtype>
void BinaryTree<elemtype>::inorder(ostream& os)
{
_root->inorder(_root, os);
}
template <typename elemtype>
void BinaryTree<elemtype>::postorder(ostream& os)
{
_root->postorder(_root, os);
}
#include "Template.h"
int main()
{
BinaryTree<string> bt;
bt.insert("Piglet");
bt.insert("Eeyore");
bt.insert("Roo");
bt.insert("Tigger");
bt.insert("Chris");
bt.insert("Pooh");
bt.insert("Kanga");
bt.inorder();
cout << '\n';
BinaryTree<string> bt1(bt);
bt1.preorder();
bt.remove("Piglet");
cout << '\n';
bt.preorder();
}
6.5 以一个模板函数完成输出运算符
输出二叉树的元素类型不同,我们自然想到利用template机制实现一个通用类型输出的函数。
template <typename elemtype>
inline ostream& operator<<(ostream& os, BinaryTree<elemtype>& bt)
{
os << "PreorderTraverse: ";
bt.preorder(os);
os << '\n';
os << "InorderTraverse: ";
bt.inorder(os);
os << "\nPosorderTraverse: ";
bt.postorder(os);
os << endl;
return os;
}
6.6 常量表达式做template参数和默认参数值
Template的参数并不一非得是某种类型不可,我们也可以用常量表达式作为template的参数,例如我们对数列类继承体系可以template化,将“对象所含元素个数”参数化。
template <int len>
class num_sequence {
public:
num_sequence(int beg_pos = 1);
//
};
template<int len>
class Fibonacci : public num_sequence<len> {
public:
Fibonacci(int beg_pos = 1)
:num_sequence<int>(beg_pos){}
}
当我们产生Fibonacci对象的时候:
Fibonacci<16> fib1;
Fibonacci<17> fib2(17);
其基类num_sequence会因为参数len而导致元素个数为16和17,同样的,我们还可以把长度和起始位置一起初始化:
template <int len, int beg_pos>
class num_sequence;
大部分数列起始位置是1,如果能为起始位置提供默认值就更好了,template机制是可以做到这一点的。
template <int len, int beg_pos = 1>
以template重写一下之前的数列类,这样的好处是不用再储存长度和起始位置这俩data member了。
//模板类num_sequence的总体框架
template <int len, int beg_pos>
class num_sequence {
public:
virtual ~num_sequence() {};
int elem(int pos) const;
const char* what_am_i() const
{
return typeid(*this).name();
}
static int max_elems() { return _max_elems; }
ostream& print(ostream& os = cout) const;
protected:
virtual void gen_elems(int pos) const = 0;
num_sequence(vector<int>* ps):_pelems(ps) {}
static const int _max_elems = 1024;
vector<int>* _pelems;
};
输出运算符:
template <int len, int beg_pos>
ostream& operator<<(ostream& os, const num_sequence<len, beg_pos>& ns)
{
return ns.print(os);
}
检查以及补充数列元素的函数:
template <int len, int beg_pos>
int num_sequence<len, beg_pos>::elem(int pos) const
{
if (!check_integrity(pos, _pelems->size()))
return 0;
return (*_pelems)[pos - 1];
}
template <int len, int beg_pos>
bool num_sequence<len,beg_pos>::check_integrity(int pos, int size) const
{
if (pos > max_elems() || pos <= 0)
return false;
if (size < pos)
gen_elems(pos);
return true;
}
打印函数:
template <int len, int beg_pos>
ostream& num_sequence<len, beg_pos>::print(ostream& os) const
{
int elem_pos = beg_pos - 1;
int end_pos = elem_pos + len;
if (!check_integrity(end_pos, _pelems->size()))
return os;
os << "("
<< beg_pos << ","
<< len
<< ")";
while (elem_pos < end_pos)
os << (*_pelems)[elem_pos++] << ' ';
return os;
}
Fibonacci类:
template <int len, int beg_pos = 1>
class Fibonacci :public num_sequence<len, beg_pos>
{
public:
Fibonacci() :num_sequence<len, beg_pos>(&_elems);
protected:
static vector<int> _elems;
virtual gen_elems(int pos) const;
};
完成它的虚函数部分
template <int len, int beg_pos>
static vector<int> Fibonacci<len, beg_pos>::_elems;
template<int len, int beg_pos>
void Fibonacci<len, beg_pos>::gen_elems(int pos) const
{
if (_elems.empty())
{
_elems.push_back(1);
_elems.push_back(1);
}
if (_elems.size() < pos)
{
int i = _elems.size();
int pprev = _elems[i - 2];
int prev = _elems[i - 1];
for (; i <= pos; i++)
{
int elem = pprev + prev;
_elems.push_back(elem);
pprev = prev;
prev = elem;
}
}
}
测试
int main()
{
Fibonacci<6,12> fib1;
Fibonacci<1, 10> fib2;
Fibonacci<8> fib3;
Fibonacci<8, 8> fib4;
Fibonacci<8, 12> fib5;
cout << "fib1:" << fib1
<< "\nfib2:" << fib2
<< "\nfib3:" << fib3
<< "\nfib4:" << fib4
<< "\nfib5:" << fib5
;
}
我们知道,函数名是一种地址常量,因此它也可以做为template的参数,如下:
//pf是一个指向特定的函数类型,产生pos个元素,放到vector seq内的函数指针
template<void (*pf) (int pos, vector<int>& seq)>
class num_sequence
{
public:
num_sequence(int len, int beg_pos = 1)
{
assert(pf);
_len = len > 0 ? len : 1;
_beg_pos = beg_pos > 0 ? beg_pos : 1;
pf(beg_pos + len - 1, _elems);
}
private:
int _len;
int _beg_pos;
vector<int> _elems;
};
使用的时候我们就可以只去实现那个产生数列到pos位置的元素并放到vector容器里的函数就行。
void Fibonacci(int pos, vector<int>& seq)
{
if (seq.empty())
{
seq.push_back(1);
seq.push_back(1);
}
if (pos > seq.size())
{
int i = seq.size();
int prev = seq[i - 1];
int pprev = seq[i - 2];
for(;i <= pos; i++)
{
int elem = prev + pprev;
seq.push_back(elem);
pprev = prev;
prev = elem;
}
}
}
int main()
{
num_sequence<Fibonacci> fib(1,10);
return 0;
}
6.7 以Template参数作为一种设计策略
现在我们可以利用template机制把LessThan函数对象要绑定的元素类型参数化化:
template <typename elemtype>
class LessThan
{
public:
LessThan(elemtype& val):_val(val){}
bool operator()(elemtype val) const
{
return val < _val;
}
void re_set(elemtype& val)
{
_val = val;
}
elemtype& get_set() const
{
return _val;
}
private:
elemtype _val;
};
这样写有一个潜在问题,如果用户提供的类型没有指定小于运算符怎么办呢?
一种可行的策略是这样的,我们同样以一个template参数Cmp来传递用户自己定义的小于运算符,如果用户没有定义小于运算符,我们也救不了了,就默认使用less<elemtype>运算符吧。
template <typename elemtype, typename Cmp = less<elemtype>>
class Lessthan
{
public:
Lessthan(elemtype& val):_val(val){}
bool operator()(elemtype val) const
{
return Cmp(val,_val);
}
void re_set(elemtype& val)
{
_val = val;
}
elemtype& get_set() const
{
return _val;
}
private:
elemtype _val;
}
比如用户指定以下比较大小的函数:
bool cmp(string& s1, string& s2)
{
return si.size() < s2.size();
}
由于存在默认参数值,可以这样使用:
Lessthan<int> lt(72);
//相当于Lessthan<int,less<int>> lt(72);
Lessthan<string, cmp> lt("hhahahahaha");
6.8 模板成员函数
类中的成员函数也可以定义为模板函数,以一个打印类为例:
class PrintIt {
public:
PrintIt(ostream& os):_os(os){}
template <typename elemtype>
void print(elemtype& elem, elemtype& delimiter = '\n')
{
_os << elem << delimiter;
}
private:
ostream& _os;
};
甚至我们可以再把输出流也参数化一下:
template <typename Ostream>
class PrintIt
{
public:
PrintIt(Ostream& os):_os(os){}
template <typename elemtype>
void print(elemtype& elem, elemtype& delimiter = '\n')
{
_os << elem << delimiter;
}
private:
Ostream _os;
};