类的设计(三) 数据抽象、动态绑定、继承和句柄类的 应用示例1+反思

感想:虽然有这种设计思想,但是倘若问题稍微抽象一点或细化一点,就可能想不到继承,多态的思想。

这篇总结给出一个小示例,顺便总结前两节。

问题描述:

         算数表达式的树。例如,表达式  (-5)*(3+4)  对应的树为:

         需求为:创建这样的树,然后打印该树的完整括号,另外用户不想为这些表达式的表现形式操心,更不想关心有关内存分配和回收的事宜。例如:

#include<iostream>
using namespace std;
int main()
{
	Expr t = Expr("*",Expr("-", 5) , Expr("+",3,4));
	cout << t << endl;
	t = Expr("*",t,t);
	cout << t << endl;
	return 0;
}

  打印:

需求罗列:

         1.打印这颗树的完整括号形式。

         2.不想操心表达式的表示形式。

         3.不想关心内存的分配和回收的事宜。

解决方法:

  这里自己试着用自己的想法实现了一下。类说明在定义注释里...

定义一个List的类代表每个节点

struct List
{
	
	List * left;
	List * right;
	//枚举 当前节点是 操作符 还是 值
	typedef enum{ Integer, Operator } Class;
	typedef union{
		char ch;//操作符
		int valu;//值
	} Chun;

	Class cls;
	//一定要初始化,深刻体会。自己在写的时候没有初始化 空指针,Debug了挺久。我这编译器给指针的默认值为0xcdcdcdcd,但 != NULL 
	//另外要注意 不要写成 ptr == NULL的形式。因为NULL是个 宏,在不同的系统中值不同(主要看编译器)。
	List():left(0),right(0){}
	Chun intOp;

};

再看有点傻的实现方法(写的不好 - - ):

//递归寻找,
string find(List * head)
{
	if (!head)return "";
	if (head->cls == List::Class::Operator)//假如是操作符
	{
        //枚举 父节点为操作符,子节点只有一个的情况
        //(-5)或(+5)
		if (!head->left)
		{
			string str;
			str += "(";
			str += (head->intOp.ch == '+' ? "" : "-");
			str += find(head->right) + ")";
			return str;
		}
		if (!head->right)
		{
			string str;
		    str += "(";
			str += (head->intOp.ch == '+' ? "" :"-");
			str += find(head->left) + ")";
			return str;
		}
		return "(" + find(head->left) + head -> intOp.ch + find(head->right) + ")";
	}
	else//假如是数字节点
	{
		stringstream ss;
		string str;
		ss << head->intOp.valu;
		ss >> str;
		return str;
	}
}

然后最后看用户创建树的方法。。(我的天,在吐槽一次)

List * top;//顶部节点
List * temp;
void init()
{


    //在写的时候 已经意识到了 用户很难释放内存,我这样编写反而增加了用户的负担。。。。
    //最后自己也是懒得写释放。虽然有办法 但是不是最好的。也可能发生问题
	//顶节点
	temp = new List;
	temp -> intOp.ch = '*';
	temp -> cls = List::Class::Operator;
	top = temp;
	temp -> left = new List;
	temp = temp -> left;
	temp -> intOp.ch = '-';
	temp -> cls = List::Class::Operator;
	temp -> left = new List;
	temp = temp -> left;
	temp->intOp.valu = 5;
	temp->cls = List::Class::Integer;
	temp = top;
	temp -> right = new List;
	temp -> right -> intOp.ch = '+';
	temp -> right -> cls = List::Class::Operator;
	temp = temp->right;
	temp -> left = new List;
	temp -> left -> intOp.valu = 3;
	temp -> left -> cls = List::Class::Integer;
	temp -> right = new List;
	temp -> right -> intOp.valu = 4;
	temp -> right -> cls = List::Class::Integer;

// 这里在一次构造二元的形式 ((-5)*(3+4))*((-5)*(3+4))
	temp = new List;
	temp->intOp.ch = '*';
	temp->cls = List::Class::Operator;
	temp->left = top;
	temp->right = top;
	top = temp;
	
}

输出结果:

这里思考下:这么写不但把内存交给用户来管理了。往深远讲,用户可能需要求这些表达式的值(因为下一篇就是求值...),那么这个

设计非常难扩展。

 

解决方法二:面对对象 (数据抽象、动态绑定、继承和句柄的思想

    描述(引用书上的原话):树图有两个截然不同的对象:节点(用圆圈表示)和边(箭头表示)。首先只考虑节点。 

        每个节点包含一个值------一个操作数或者一个操作符----并且每个节点又具有零个、一个或两个子节点。这些类有一些共同点:

每个类都要存储一个值以及一些子节点。当然也有不少不同点,比如它们存储的值的种类。子节点的数目。继承可以捕捉这些共同点,

而动态绑定帮助各个节点知晓它们的身份。这样就不必让这些对象的每个操作都必须时刻留心这个问题了。

可以发现:这里有三种节点,一种表示整数表达式,包含一个整数值,无子节点。另外两个分别表示一元表达式和二元表达式,

包含一个操作符,分别有一个或两个子节点。用户希望打印各种节点,但是具体方式需要取决于打印节点的类型而定。可以定义一个virtual函数指明应当如何打印各种节点。动态绑定将会负责 表现形式。

于是我们可以抽象出基类:

 这里Expr为代理类,先声明一下,免得懵,声明在下面

//Expr_node 基类
class Expr_node {
	friend class Expr;
	int use;//引用计数
protected:
	Expr_node() :use(1) {}
    //所要创建的对象都为此类的子类,所以我们把析构函数定义为虚函数
	virtual ~Expr_node() {}
  
    //动态绑定
	virtual void print(std::ostream&) const = 0;



    //把ostream对象声明为友元
	friend ostream& operator << (ostream& o, const Expr_node& e)
	{
		e.print(o);
		return o;
	}
    
};

于是可以抽象出它的子类(数值节点):

class Int_node :public Expr_node {
	int n;
	void print(ostream& o) const { o << n; }
	Int_node(int k) :n(k) {}
};

一元操作符节点:

class Unary_node :public Expr_node {
	friend class Expr;
	string op;
	Expr opnd;
	void print(ostream& o) const
	{
		o << "(" << op << opnd << ")";
	}
	Unary_node(const string& a, Expr b) :op(a), opnd(b) {}
};

二元操作符节点:

class Binary_node :public Expr_node {
	friend class Expr;
	string op;
	Expr left;
	Expr right;
	void print(ostream& o) const
	{
		o << "(" << left << op << right << ")";
	}
	Binary_node(const string& a, Expr b, Expr c) :op(a), left(b), right(c) {}
};

这里还少一个内存分配的问题(我们不知道用户会怎么使用类)。所以有关内存的操作最好不要交给用户。

我们可以定义个句柄类来管理。(Expr.h文件)。这里的句柄类刚好可以抽象成树的边

#include<string>
using namespace std;
//这里只是告诉编译器 有这个类存在,但是具体定义不在这
class Expr_node;
class Expr{
	friend ostream& operator <<(ostream&, const Expr&);
	Expr_node* p;//所以这里可以是 p指针或引用,而不是栈变量,
//因为栈在编译的时候,就会压栈。调用default构造函数,
	//但是前面只是声明,而没有定义,所以栈变量就会出现编译错误。
public:
	Expr(){}
	Expr(int);//数字节点
	Expr(const string&, Expr&);//一元操作符
	Expr(const string&, Expr&, Expr&);//两元操作符
	Expr(const Expr& t);
	Expr& operator=(const Expr&);
	~Expr();
};

Expr.cpp


#include "Expr_node.h"


Expr::~Expr()
{
	if (--p->use == 0)
		delete p;
}
Expr::Expr(int n)
{
	p = new Int_node(n);
}
Expr::Expr(const string& op, Expr& t)
{
	p = new Unary_node(op, t);
}
Expr::Expr(const string& op, Expr& left, Expr& right)
{
	p = new Binary_node(op, left, right);
}
Expr::Expr(const Expr& t)
{
	p = t.p; ++p->use;
}

Expr& Expr::operator=(const Expr& rhs)
{
	rhs.p->use++;
	if (--p->use == 0)
		delete p;
	p = rhs.p;
	return *this;
}
ostream& operator<<(ostream& o, const Expr& t)
{
	t.p->print(o);
	return o;
}
int main()
{

	Expr t = Expr("*",Expr("-", 5) , Expr("+",3,4));
	cout << t << endl;
	t = Expr("*",t,t);
	cout << t << endl;

	return 0;
}

 

这样用户就可以不用关心内存分配的等问题。

输出:

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值