条款35:考虑虚函数以外的其他选择

在开发游戏中,面对不同角色生命值等属性的计算问题,通常会使用虚函数来实现多态。然而,文章探讨了除了虚函数之外的其他设计选择,以更灵活、高效的方式处理角色特性差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们考虑下面这个问题:假设我们在开发一款游戏,游戏中有不同的角色,每个角色有自己的生命值的初始值,生命值的计算方法等等。你会怎么设计这个类呢?我们很自然的就会想到:

class GameCharacter
{
public:
	virtual int healthValue()const;
};



就是说基类里定义了一个计算生命值的函数,派生类通过重新定义这个函数来完成不同类型的角色的生命值的计算。
假如生命值的计算分为如下几步:
1.获得生命值。
2.通过一个函数计算生命值。
3.将生命值返回。


那么每个派生类的healthValue函数都需要完成这几步,我们能不能重构这个代码呢?先看一下重构的结果:

class GameCharacter
{
public:
	int healthValue()
	{
		int val = getInitialVal();
		val = calcVal(val);
		return val;
	}
protected:
	virtual int getInitialVal() = 0;
	virtual int calcVal(int ) = 0;
};

class Soldier:public GameCharacter
{
private:
	int getInitialVal();
	int calcVal(int );
};

class Patient:public GameCharacter
{
private:
	int getInitialVal();
	int calcVal(int );
};


//战士的初始生命值较高
int Soldier::getInitialVal()
{
	return 50;
}

//但是生命值会减半
int Soldier::calcVal(int val)
{
	return val = val/2;
}

//病人的声明值较低
int Patient::getInitialVal()
{
	return 10;
}

//但是生命值会翻倍
int Patient::calcVal(int val)
{
	return val = val*2;
}

这种做法乍看起来不是很习惯,我们要对它的思路仔细说说:
1.它在基类中声明了2个不会被继承的虚函数getInitialVal()和calcVal(int val)。但是在基类的可以被继承的(且不希望被修改的)healthValue函数中调用了。
2.在派生类中定义了healthValue所要调用的函数。


他这样做的好处是:在基类中,限定了先做什么,后做什么。但是具体怎样做,把权力移交给了派生类。
这种思路,称为模板方法模式,它的定义为:定义一个操作中的算法的骨架,而将一些方法实现延迟到子类。模板方法使得子类可以不改变一个算法的结构即可以重定义该算法的某些特定步骤。


但是这样做其实并不灵活,假如我希望同一个类型的不同对象有不同的计算生命值的方法,就麻烦了。换个角度思考,人物健康指数的计算,其实,不一定与人物的特定类型有关,对于同一个类型,也可以有不同的计算方法。由此我们想到,不能让每个类型的声明计算与一个函数相关,而对于不同的对象,可以调用不同的函数来完成这件事。依照这个思路,我们可以这么写:

//人物健康指数的计算与人物类型无关
//要求每个人物的构造函数接受一个指针,指向一个健康计算函数
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
	typedef int (*HealthCalcFunc)(const GameCharacter&);
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc ):healthFunc(hcf){}
	int healthValue()const
	{return healthFunc(*this);}
	virtual int getInitHealth()const = 0;
private:
	HealthCalcFunc healthFunc;
};


class Soldier:public GameCharacter
{
public:
	explicit Soldier(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf){}
	int getInitHealth()const;
};

class Patient:public GameCharacter
{
public:
	explicit Patient(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf){}
	int getInitHealth()const;
};

int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
int recoverHealth(const GameCharacter&);

int Soldier::getInitHealth()const
{
	return 50;
}

int Patient::getInitHealth()const
{
	return 10;
}
int defaultHealthCalc(const GameCharacter& gc)
{
	int health = gc.getInitHealth();
	health = health / 2;
	return health;
}
int loseHealthQuickly(const GameCharacter& gc)
{
	int health = gc.getInitHealth();
	health = health / 4;
	return health;
}

int loseHealthSlowly(const GameCharacter& gc)
{
	int health = gc.getInitHealth();
	health = health / 1.6;
	return health;	
}

int recoverHealth(const GameCharacter& gc)
{
	int health = gc.getInitHealth();
	health = health * 2;
	return health;	
}
此时,人物类型与计算声明的方法就无关了:

int main()  
{  

	Soldier s1;
	cout<<loseHealthQuickly(s1)<<endl;
	Soldier s2(recoverHealth);
	cout<<recoverHealth(s2)<<endl;
	Patient p1;
	cout<<loseHealthSlowly(p1)<<endl;
	return 0;
}

注意,计算生命值的函数并没有访问对象的non-public部分。如果计算生命值的确不需要,那么这样做是没问题的,就想我们例子中,生命值只与初始生命值有关,所以就OK,如果还需要这个类的其他非公有成分,那么就会破坏类的封装性:要么把这些成分设定为public,那么提供get函数访问他们,要么把这个非成员函数设为friend。其实,把一个类内的成员函数变为非成员函数,总会遇到这样的问题,这取决于你的设计思路。


这个例子一下子拓宽了我们的眼界:为什么非得使用某个函数去计算生命值,能不能使用某个类似函数的,可以被调用的东西来计算呢?比如函数对象、类的成员函数等等。通过tr1中的function可以帮你完成这样的设想:
//.h
//前置声明
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:

	//std::tr1::function相当于一个泛化的函数指针
	typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
	explicit GameCharacter( HealthCalcFunc hcf = defaultHealthCalc ):healthFunc(hcf){}
	int healthVaule()const
	{
		return healthFunc(*this);
	}
	virtual int getInitHealth()const = 0;	
private:
	HealthCalcFunc healthFunc;
};



class Soldier:public GameCharacter
{
public:
	explicit Soldier(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf){}
	int getInitHealth()const;	
};

//可以采取以下3种措施调用计算健康值的函数

//计算健康值的函数,其返回类型为short
short HalfHealth(const GameCharacter&);

//计算健康值的函数对象
struct AddHealth
{
	int operator()(const GameCharacter& gc)const
	{
		int health = gc.getInitHealth();
		health = health + 10;
		cout<<"生命值加10"<<endl;
		return health;
	}
};

//类成员函数
class GameLevel
{
public:
	float hard(const GameCharacter&)const;
	float easy(const GameCharacter&)const;
};

//cpp
int Soldier::getInitHealth()const
{
	return 50;
}

int defaultHealthCalc(const GameCharacter& gc)
{
	int health = gc.getInitHealth();
	health = health * 2;
	cout<<"默认计算生命值,为初始值的2倍"<<endl;
	return health;
}
short HalfHealth(const GameCharacter& gc)
{
	short health = gc.getInitHealth();
	health = health / 2;
	cout<<"生命值减半"<<endl;
	return health;
}

float GameLevel::hard(const GameCharacter& gc)const
{
	float health = gc.getInitHealth();
	cout<<"困难模式,生命值为初始值的四分之一"<<endl;
	return health / 4;
}

float  GameLevel::easy(const GameCharacter& gc)const
{
	float health = gc.getInitHealth();
	cout<<"简单模式,生命值为初始值的四倍"<<endl;
	return health * 4;
}

//main

int main()
{
	//调用默认函数生命值翻倍
	Soldier s1;
	cout<<s1.healthVaule()<<endl;
	//生命值减半
	Soldier s2(HalfHealth);
	cout<<s2.healthVaule()<<endl;
	//生命值加10
	AddHealth add;
	Soldier s3(add);
	cout<<s3.healthVaule()<<endl;
	GameLevel level;
	//对非静态成员函数,需要通过bind绑定
	//easy函数有一个参数,所以需要一个占位符
	Soldier s4(std::tr1::bind(&GameLevel::easy,level,std::tr1::placeholders::_1));
	cout<<s4.healthVaule()<<endl;
	Soldier s5(std::tr1::bind(&GameLevel::hard,level, std::tr1::placeholders::_1));
	cout<<s5.healthVaule()<<endl;
	return 0;
}

这个例子跟前面的很类似,但是又有所区别:我们没有定义类型确定的函数指针,而是定义了一个“泛化的”函数指针:HealthCalcFunc,它的返回值为int(或可以转化为int),输入参数为GameCharacter引用(或可以转化为GameCharacter)的可调用物:可以是一般函数、可以是函数对象,也可以是成员函数。
对于普通函数,和函数对象,可以直接用来给HealthCalcFunc赋值。对非静态成员函数,需要通过bind绑定:为了计算s4的健康函数,需要使用GameLevel里面的easy函数,这个函数实际上有两个参数:*this(GameLevel类型)和GameCharacter&,HealthCalcFunc只接受一个参数:GameCharacter&。需要将GameLevel类型中的easy函数与调用它的对象绑定起来,此后每次调用easy函数,都是调用绑定的那个对象(level)的这个函数。其中_1是占位符,表示的是这个函数的第一个参数。


经过上一个想法的洗礼,尤其是调用类成员函数,使我们不禁想到了为什么不把所有的计算生命函数设成一个基类,然后从中派生出各个子类方法,然后让GameCharacter调用这些方法呢?下面是实现的程序:

//healthCalcFunc.h
//前置声明
class GameCharacter;

class HealthCalcFunc
{
public:
	virtual int calc(const GameCharacter& gc)const;

};

//计算生命值方法派生类
class AddHealth:public HealthCalcFunc
{
public:
	int calc(const GameCharacter& gc)const;
};

//计算生命值方法派生类
class DoubleHealth:public HealthCalcFunc
{
public:
	int calc(const GameCharacter& gc)const;
};

//头文件中声明
extern HealthCalcFunc  defaultHealthCalc;

//healthCalcFunc.cpp
#include "healthCalcFunc.h"
#include "gameCharacter.h"
int HealthCalcFunc::calc(const GameCharacter& gc)const
{
		
		cout<<"返回原始生命值"<<endl;
		return gc.getInitHealth();
}


int AddHealth::calc(const GameCharacter& gc)const
{
	cout<<"生命值+10"<<endl;
	return gc.getInitHealth() + 10;
}

int DoubleHealth::calc(const GameCharacter& gc)const
{
	cout<<"生命值翻倍"<<endl;
	return gc.getInitHealth() * 2;
}
//源文件中定义
HealthCalcFunc defaultHealthCalc;

//gameCharacter.h
#include "healthCalcFunc.h"


class GameCharacter
{
public:
	explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc):pHealthCalc(phcf){}
	int healthVaule()const;
	int getInitHealth()const ;
private:
	HealthCalcFunc* pHealthCalc;
};

//gameCharacter.cpp
#include "gameCharacter.h"

int GameCharacter::healthVaule()const
{
	return pHealthCalc->calc(*this);
}

int GameCharacter::getInitHealth()const 
{
	return 50;
}

//main.cpp
int main()  
{  
	GameCharacter gc0;
	cout<<gc0.healthVaule()<<endl;
	GameCharacter gc1(&AddHealth());
	cout<<gc1.healthVaule()<<endl;
	GameCharacter gc2(&DoubleHealth());
	cout<<gc2.healthVaule()<<endl;
	return 0;
}

这个架构的特点就是可扩展性很强,我们可以新加入不同的角色—从GameCharacter中派生,也可以加入新的计算健康值的方法—从HealthCalcFunc中派生。甚至,假如我们要大幅度的修改游戏,比如,给角色装配一个武器,那么在GameCharacter中,增加一个指向武器类的指针,然后定义武器类就行了,是得程序修改更加方便。
这个方法称为strategy模式,它的定义如下:Strategy模式定义了一系列的算法,将它们每一个进行封装,并使它们可以相互交换。Strategy模式使得算法不依赖于使用它的客户端。
这个条款略微有些长,但总结起来,无非是这样对于虚函数,有如下几种替代方案:
1.模版方法模式。在类中确定派生类要做的事情的顺序,然后让派生类自己实现它们。
2.使用函数指针。将虚函数移到类的外部,但是它不能访问类的private成分。
3.使用tr1::function指定“泛型”函数指针,是得我们可以通过函数、函数对象、成员函数来替代虚函数。
4.使用策略模式,将虚函数所要完成的事情封装成类,以便于扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值