【Essential C++学习笔记】第六章 以Template进行编程

文章目录

  • 第六章 以Template进行编程
    • 前言:
    • 6.1 被参数化的型别
      • 1)为什么要有类模板呢?
      • 2)具体实施
    • 6.2 模板类的定义
    • 6.3 模板类型参数的处理
    • 6.4 实现一个模板类
      • 1)实现逻辑
        • 1.理清思路
        • 2.`insert()`
        • 3.`remove()`
        • 4.`remove_root()`
        • 5.`remove_value()`
        • 6.`lchild_leaf()`
        • 7`clear()`
      • 2)总结&要点
        • 1.关于new表达式
        • 2.模板类成员函数的定义/声明格式
        • 3.函数参表出现了*&的说明
    • 6.5 一个以模板函数完成的输出运算符
    • 6.6 常量表达式与默认参数值
      • 1)模板参数
    • 6.7 以模板参数作为一种设计策略、成员模板函数=
      • 1) 成员模板函数
        • 1.非模板类里定义成员模板函数
        • 2.模板类里定义成员模板函数

第六章 以Template进行编程

前言:

Template(模板)能根据用户指定的特定值或特定类型,自动产生一个函数或类。

以二叉树class template 为例子,本章需要实现属于自己的template

二叉树:

概念

一棵二叉树是结点的一个有限集合,该集合为空,或者是由一个根节点加上两棵称为左子树和右子树的二叉树组成。

二叉树的特点:

(1)每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
(2)二叉树的子树有左右之分,其子树的次序不能颠倒。

二叉树的四种遍历方式

  • 前序遍历:先访问一棵树的根节点,再访问左子树,最后访问右子树。
  • 中序遍历:先访问一棵树的左子树,再访问根节点,最后访问右子树。
  • 后序遍历:先访问一棵树的左子树,再访问右子树,最后访问根节点。
  • 层序遍历:首先访问第一层的根结点,然后从左到右访问第2层上的节点,接着访问第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

实现:

我们实现的二叉树包含两个class:

  • BinaryTree用以存储一个指针,指向根节点
  • BTnode:用来存储节点实值、以及连接至左、右两个子节点的链接

此处,节点实值的型别( value type) 正是我们希望加以参数化的部分。

需要提供的操作行为:

  • 安插( insert )、移除( remove)、搜寻( find )、清除(clear)、以特定的遍历方式打印( print)整颗树.
  • 支持3种遍历方式:中置( inorder) 、前置( preorder) 、后置(postorder) .

规则:

第一个安插至空白树( empty tree)的值,会成为此树的根节点。接下来的每个节点都必须以特定规则插入:

如果小于根节点,就被置于左侧子树,如果大于根节点,就被置于右侧子树。

任何一个值只能在树中出现一次,但是此树有能力记录同一值的被安插次数。

6.1 被参数化的型别

1)为什么要有类模板呢?

举一个类的例子:
(假设已想好要定义BTnode类,用以存储结点数值,和左右孩子指针)

class string_BTnode{
public:
	//...
private:
	string _val;
	int _cnt;
	string_BTnode *lchlid;
	string_BTnode *rchild;
  • 这是二叉树的结点定义。为了存储不同类型的值,我们必须还要实现不同的BTnode类,比如int_BTnodedouble_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被用作一个占位符,其名称可以任意设定。类型参数可以用在实际类型的使用场合,比如上面的那个valType _val;,就是用类型参数来声明_val 所属类型
  • btibts代表两份BTnode类定义。

2)具体实施

  • 在类模板和类成员的定义中,可不用以模板参数列表进一步限定类模板,其他情况下要用参数列表限定类模板,
    就是templateclass 类名{//...};
    举个例子:
template <typename elemType>
class BinaryTree{
public:
	//...
private:
	BTnode<elemType> *_root;
	//BTnode(模板)类必须以模板参数列表加以限定的例子
  • BinaryTreest;指针_root指向一个结点值类型为stringBTnode类类对象;
  • BinaryTreeit指针_root指向一个结点值类型为intBTnode类类对象
  • BTnode模板类BinaryTree模板类需要配合使用,所以这两个类要建立friend关系,如下
template<typename Type>
class BinaryTree;//BinaryTree类的前置声明
template<typename valType>
class BTnode{
	friend class BinaryTree<valType>
	//...
};

6.2 模板类的定义

以下就是我们的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){}
//注意这个类模板的内联成员函数是带成员初始化列表的构造函数

6.3 模板类型参数的处理

  • 处理模板类型的参数时,无法知晓用户实际要用的类型是否为语言内置类型
  • 规则:将所有的模板类型参数视为class类型处理,这样的话当用户给valType这个模板类型指定一个class类型时,
    保证程序效率最佳!
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
  • 所以,不论传值方式进行class类型的形参参数传递,还是构造函数体内给模板类类型的数据成员赋值都没有错但是会花费很多时间,使程序效率降低。

6.4 实现一个模板类

1)实现逻辑

1.理清思路

每当我们插入某个新值时,都必须建立 BTnode对象,加以初始化,将它连接至二叉树的某处.我们必须自行以new表达式和 delete表达式来管理每个节点的内存配置和释放。

2.insert()

① 如果根节点之值尚未设定,它会由程序的自由空间配置一块新的 BTnode需要的内存空间**(new)**。否则就调用BTnodeinsert_value(),直接将新值插人二叉树中;

② 当根节点存在时,insert_value()才会被调用。小于根节点的所有数值都置放于根节点的左子树,大于根节点的所有数值都置放于根节点的右子树。

insert_value()会通过左侧子节点或右侧子节点递归调用自己,直到以下任何一种情形发生才停止:

  • 合乎条件的子树并不存在
  • 欲安插的数值已在树中

④ 由于每个数值只能在树中出现一次,所以我以BTnodedatamerber _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);
		}
	}
}
3.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);
		}
	}
}
4.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; //移除先前的根节点
}
5.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类类对象)
	}
}
6.lchild_leaf()

无论remove_root()remove_value(),皆会搬移左子节点,使它成为右子节点的左子树的叶节点。我将这一操作抽离至lchild_leaf(),那是 BTnodestatic 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;
}
7clear()

我们还需要另一个函数来移除整棵二叉树。

我把这个名为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;
	}
}

2)总结&要点

1.关于new表达式
  • new表达式可分解为两个操作:
    ①向程序的空闲空间请求内存,若分配到足够空间,则返回指针,指向新对象;若未分配到足够空间,则抛出异常bad_alloc(异常处理)
    ②如果new 类型名(初值),则该新对象被初始化。
    举个class类型的例子:

    _root = new BTnode<elemType>(elem);
    

    其中elem被传入BTnode模板类的类构造函数。分配内存失败,初始化操作(类构造函数操作)不会发生。

2.模板类成员函数的定义/声明格式
注意分三行写这种让代码清晰简洁的写法

template<typename 自定义类型名(占位符)>
(自己决定这里加不加inline) 返回类型 模板类类名<自定义类型名>::
模板类成员函数名(参数表(比如const 自定义类型名 &参数名等));或者{//。。。}
3.函数参表出现了*&的说明

如上述的remove_value()

template<typename valType> void BTnode<valType>::remove_value(const valType &val, BTnode *&prev);
  • 参表的参数以&方式传递,为了避免当模板自定义类型名中的prev被指定为class类型时,因传值(不带&)而产生的昂贵复制开销。
  • 不改变参数值,就再该参数名的声明最前加个const,即const 类型名 参数名…
  • *&即为指针的引用
  • *&出现在函数参表中的意思就是我既想改变传入形参的实参指针(或说形参指针)所指的对象,又想改变实参(或说形参)指针本身

6.5 一个以模板函数完成的输出运算符

  • 非模板函数形式的重载运算符函数是这样(声明)
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类型的)<<运算符
}

6.6 常量表达式与默认参数值

1)模板参数

  • 可以用常量表达式作为模板参数,而且以常量表达式作为模板参数(<>内的参数)还可以给这个模板参数提供默认值,举个例子
    如:
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

6.7 以模板参数作为一种设计策略、成员模板函数=

1) 成员模板函数

1.非模板类里定义成员模板函数
class PrintIt{
public:
	PrintIt(ostream &os):_os(os){}
//成员模板函数的定义
template<typename elemType>
void print(const elemType &elem,char delimiter='\n')//函数提供默认参数值
{
	_os<<elem<<delimiter;
}
  • PrintIt类为非模板类,该类的类对象是一个输出数据流。该类包含成员模板函数print()。那么成员模板函数的好处是啥或说作用是啥呢,就是我们光写一份这样的成员(模板)函数定义就OK了(实际上跟非成员模板函数差不多的定义格式(仅限于模板类内定义成员模板函数),传入成员模板函数print的任何类型的实参都可以应用某种运算符(这里是<<)。
  • 不把要输出(或其它操作)的元素的类型(这个例子里是elem)剥离成参数,就要给不同的元素类型各自创建类,这样的工作量巨大。
2.模板类里定义成员模板函数
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;
};

你可能感兴趣的:(C++学习,c++,学习,笔记)