C++编程杂谈之三:面向对象(续)

 
上一篇我们涉及了面向对象的一个基本概念--封装,封装是一个相对比较简单的概念,也很容易接受,但是很多的场合下面,仅仅是封装并不能很好的解决很多问题,考虑下面的例子:
假设我们需要设计一个对战游戏的战斗细节,在最初的版本中我们将支持一种动作--fight。假设我们有三种角色:fighter、knight和warrior,每种角色的health、hit point不同,基于封装的基本想法,我们很自然的想到对每个对象使用类的封装,首先明显的元素有2个:health、hit point,另外还有name(我们不能只有三个角色)和战斗的速度speed,方法有:hit、isalive。基于这样的想法,我们给出下面的类:
 
 
class fighter
{
	private:
		int               m_iHealth;
		int              m_iSpeed;//为了方便,这个值使用延时值,越大表示速度越慢
		const int          m_iHitPoint;
		char             m_strName[260];
	public:
		fighter(const char* strName);
		void hit(fighter* pfighter);
		bool isAlive(); 
};   

 
 
    上面的类可以清楚的抽象出我们所需要表达的数据类型之一,这里也许你已经发现了一些问题:
成员函数hit用来处理战斗事件,但是我们有不同的角色,fighter不可能只跟自己同样的对手作战,我们希望它能和任何角色战斗,当然,使用模板函数可以简单的解决这个问题。另外,我们必须去实现三个不同的类,并且这些类必须都实现这些属性和方法。即使这些问题我们都解决了,现在我们要组织两个队伍作战,我们希望使用一种群体类型来描述它们,问题是我们必须针对每一种类建立相应的群体结构,当然你可以认为3个不同的类型不是很多,完全可以应付,那么如果有一天系统升级,你需要管理上百种类型的时候,会不会头大呢?
    在C++中,继承就可以很好的解决这个问题,在C++中,继承表示的是一种IS-A关系,即派生类IS-A基类,很多现实世界中的关系可以这样来描述,如:dog is-a animal,dog是animal的派生(继承),继承产生的对象拥有父(基)对象的所有属性和行为,如animal的所有属性和行为在dog身上都会有相应的表现。在UML的描述中,这种关系被称为泛化(generalization)。一般情况下,当我们需要实现一系列相似(具有一定的共性)然而有彼此不同的类别的时候,使用继承都是很好的解决办法,例如前面的代码虽然也能够实现我们的目标,但是显然很难管理,下面给出使用继承后的实现:
 
class actor//基类
{
	protected:
		int               m_iHealth;
		const int         m_iSpeed;//为了方便,这个值使用延时值,越大表示速度越慢
		const int         m_iHitPoint;
		char              m_strName[260];
	public:
		actor(const char* strName,const int iHealth,const int iSpeed,const int iHitpoint);
	
		int& Health(){ return m_iHealth; };
		const char* getName(){ return m_strName; };
		virtual void hit(actor *Actor) = 0;
		bool isAlive(); 
};  
actor::actor(const char* strName,const int iHealth,const int iSpeed,const int iHitpoint):
m_iHealth(iHealth),
m_iSpeed(iSpeed),
m_iHitPoint(iHitpoint)
{
	strcpy(m_strName,strName);
}

bool actor::isAlive()
{
	return (m_iHealth>0);
}

/////////////////////////////////////////////////////////
//类fighter
class fighter :public actor
{
	public:
		fighter(const char* strName);
		virtual void hit(actor *Actor);
	private:
};

fighter::fighter(const char* strName):
actor(strName,100,20,20)
{
}

void fighter::hit(actor *Actor)
{
	Sleep(m_iSpeed);
	if(!isAlive())
	{
		return ;
	}
	if(Actor&&Actor->isAlive())
	{
	 	//这里我们用一个函数做了左值,因为函数返回的是引用
		if(isAlive())
			Actor->Health() = Actor->Health() - m_iHitPoint;
	}
}

/////////////////////////////////////////////////////////
//类knight
class knight :public actor
{
	public:
		knight(const char* strName);
		virtual void hit(actor *Actor);
	private:
};

knight::knight(const char* strName):
actor(strName,150,20,25)
{
}

void knight::hit(actor *Actor)
{
	Sleep(m_iSpeed);
	if(!isAlive())
	{
		return ;
	}
	if(Actor&&Actor->isAlive())
	{
		if(isAlive())
			Actor->Health() = Actor->Health() - m_iHitPoint;
	}
}

/////////////////////////////////////////////////////////
//类warrior
class warrior :public actor
{
	public:
		warrior(const char* strName);
		virtual void hit(actor *Actor);
	private:
};

warrior::warrior(const char* strName):
actor(strName,150,20,25)
{
}

void warrior::hit(actor *Actor)
{
	Sleep(m_iSpeed);
	if(!isAlive())
	{
		return ;
	}
	if(Actor&&Actor->isAlive())
	{
		if(isAlive())
			Actor->Health() = Actor->Health() - m_iHitPoint;
	}
}

 
 
    C++为我们提供了非常优秀的继承体系(其实这是面向对象的一个非常重要的特征),上面的类图中我们可以很清楚的看到他们的关系,在继承中,基类(有些地方也称为超类)其实是所有派生类的共性提炼的结果,有时候在开发过程中,是先有派生类,然后再提出共性,产生基类的。
    就象遗传一样,派生类拥有基类的所有属性和方法,如基类actor的成员函数和成员变量在每一个派生类中都存在,即fighter、knight和warrior中都存在,在继承关系中还存在一个非常重要的关键字protected,它提供一个界于public和private 之间的一种可见性,与private相同的地方是对于外部来说,它不可见,与public相同的地方是对与继承体系来说,是向下可见的(其派生出来的类可以直接使用),这里有一点是很容易让人迷惑的,就是派生类中基类的成员可见性。对派生类来说,如果使用public继承,那么从基类中继承的所有成员的可见性不变(如果是protected继承,降一级,public变成protected,private继承再降),那么对于一个从基类继承来的private成员来说,派生类是无法访问的(很迷惑,是么?),虽然派生类含有这个变量,但是这是一种间接的拥有关系,在派生类中,含有一个基类的子对象,派生类对基类成员的访问正是通过这个子对象进行的。在思想中,永远不要把继承来的成员看做是自己真正拥有的,虽然使用this可以直接"看到"它们,在派生体系中private和public与其它情况没有任何区别,而protected在体系的内部就和public完全等同。
    由于派生类拥有基类的成员,所以我们可以通过派生而简单的"重用"我们已有的代码,从而大大减少重复劳动。
关于继承的另外一个重要的特征就是虚函数,虚函数是形成类的多态的基础,在上面的类中:
void knight::hit(actor *Actor)
比如下面的伪码:
 
actor* pA;
knight* pKnight = new knight;
pA = pKnight;
pA->hit(…);

 
 
    这里pA是一个基类的指针,而它指向的是一个派生类knight的指针,调用它应该是怎么样的情况呢?答案是hit会调用knight的方法,而不是基类的,因为它是一个虚函数,虚函数的特点是它永远忠实与实际的对象(前提是正确的使用),通过这种多态的特性,我们可以使用基类来对派生类进行正确的操作,这就解决了一个上面的问题:使用一种群体类型来描述它们,所以我们可以这样来遍历数据,而不需要关心里面究竟是一些什么样的数据
 
vector troop1;
vector troop2;
vector::iterator it_tp1 = troop1.begin();
vector::iterator it_tp2 = troop2.begin();
while( (it_tp1!=troop1.end()) && (it_tp2!=troop2.end()) )
{
	while(((*it_tp1)->isAlive())&&(it_tp2!=troop2.end()))
	{
		(*it_tp1)->hit(*it_tp2);
		if(!(*it_tp2)->isAlive())
			it_tp2++;
	}
	it_tp1++;
}

 
 
    面向对象思想中的继承是非常重要的概念,正确的运用它,会为软件的开发过程带来很多便利,同时,COM的核心技术是使用了多重继承来实现的。同时,继承也是面向对象的思想中较难理解的一个概念,这片文章我只能大致的讲述其运用,而真正的融会贯通还需要长时间的学习和实践。
    下面我们给出上面的例子的完整代码,这段代码完成了随机建立两个队伍,并进行战斗,直到有一个队伍全军覆没,最后输出结果。
阅读更多
个人分类: C++/SDK 杂项 WINDOWS
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭