读书笔记_Effective_C++_条款三十五:考虑virtual函数以外的其他选择
举书上的例子,考虑一个virtual函数的应用实例:
class GameCharacter
{
private:
int BaseHealth;
public:
virtual int GetHealthValue() const // 返回游戏人物的血量
{
return BaseHealth;
} int GetBaseHealth() const
{
return BaseHealth;
}
}; class KnightBoss : public GameCharacter
{
public:
virtual int GetHealthValue() const
{
return GetBaseHealth() * ;
}
}; int main()
{
GameCharacter *CommonCharacter = new GameCharacter();
GameCharacter *Boss = new KnightBoss();
cout << "CommonCharacter heath = " << CommonCharacter->GetHealthValue() << endl; //返回100
cout << "KnightBoss heath = " << Boss->GetHealthValue() << endl; // 返回200
}
GetHealthValue会根据不同类型的游戏角色来获得相应的血量。但这里将虚函数是public的,NVI(Non-Virutal Interface)的一个流派主张所有的虚函数都是private的,将父类与子类都会使用的前置方法与后置方法单独作一个non-virtual的函数,像下面这样:
class GameCharacter
{
private:
int BaseHealth;
public:
GameCharacter(int bh = ) :BaseHealth(bh){} int GetBaseHealth() const
{
return BaseHealth;
} int GetHealthValue()
{
cout << "可以是一些前置检查" << endl;
int Result = DoGetHealthValue();
cout << "可以是一些后置处理" << endl;
return Result;
} private:
virtual int DoGetHealthValue() const // 返回游戏人物的血量
{
return BaseHealth;
}
}; class KnightBoss : public GameCharacter
{
public:
KnightBoss(int bh) :GameCharacter(bh){} private:
virtual int DoGetHealthValue() const
{
return GetBaseHealth() * ;
}
};
main函数的接口不必有任何的变化。这里是把父类与子类有特异性的方法都写在了各自private范围内。这样的好处,是可以做一些善前善后的事情(如程序中的cout方法所示),善前方法可以是锁定互斥器,打印日志,验证输入数据合法性等,善后的方法可以是解除锁定,验证事后数据合法性等。
注意这里是将虚函数都置成了private的,但编译器生成的虚表指针则不是private的,否则会因private成员变量根本不被继承而无法实现多态。NVI的方式也不是绝对的,比如虚析构函数,它必须是public的,才能确保它的子类,以及子类的子类们能够顺利释放资源。
那么还有其他方法来替代virtual函数吗?virtual函数的本质是由虚指针和虚表来控制,虚指针指向虚表中的某个函数入口地址,就实现了多态。因此,我们也可以仿照一个虚指针指向函数的手法,来做一个函数指针。像下面这样:
class GameCharacter
{
private:
int BaseHealth;
public:
typedef int(*HealthCalcFunc)(const GameCharacter&);
GameCharacter(int bh = , HealthCalcFunc Func= NULL)
:BaseHealth(bh), HealthFuncPoniter(Func){} int GetHealthValue()
{
if (HealthFuncPoniter)
{
return HealthFuncPoniter(*this);
}
else
{
return ;
}
} int GetBaseHealth() const
{
return BaseHealth;
}
private:
HealthCalcFunc HealthFuncPoniter;
}; class KnightBoss : public GameCharacter
{
public:
KnightBoss(int bh, HealthCalcFunc Func) :GameCharacter(bh, Func){} private: }; int HealthCalcForCommonMonster(const GameCharacter& gc)
{
return gc.GetBaseHealth();
} int HealthCalcForKnightBoss(const GameCharacter& gc)
{
return * gc.GetBaseHealth();
}
main函数的内容仍然不需要改变,得到的结果是相同的,其实这里只是用函数指针模拟了虚表指针而已。在父类中声明了这个函数指针的形式,它返回一个int值,但因为需要用到GameCharacter里面的方法,所以形参是GameCharater的引用。父类的有一个私有的成员变量,它就是可以指向具体函数的函数指针。在父类与子类的构造函数里带入不同的函数,就可以实现调用父类GetHealthValue时产生的不同计算方法。
上面是用了typedef进行了函数指针类型声明,然后定义了指定形参与返回值的函数,在构造时将类中的函数指针指向特定的函数,那么我们就会想,能不能将用类(而不是typedef)来做呢?请看下面的示例:
class HealthCalcFunctionBaseClass
{
public:
virtual int CalcHealth(int BaseValue) // 书上这里用的是const GameCharacter&,然后前置声明了class GameCharacter,但因为要用到GameCharacter的具体方法,所以编译器会报错,不知道你们是如何解决这个问题的
{
return BaseValue;
}
}; class HealthCalcFunctionDerivedClass : public HealthCalcFunctionBaseClass
{
public:
virtual int CalcHealth(int BaseValue)
{
return * BaseValue;
}
}; class GameCharacter
{
private:
int BaseHealth;
public:
GameCharacter(int bh = , HealthCalcFunctionBaseClass *Func = NULL)
:BaseHealth(bh), HealthFuncPoniter(Func){} int GetHealthValue()
{
if (HealthFuncPoniter)
{
return HealthFuncPoniter->CalcHealth(GetBaseHealth());
}
return ;
} int GetBaseHealth() const
{
return BaseHealth;
}
private:
HealthCalcFunctionBaseClass *HealthFuncPoniter;
}; class KnightBoss : public GameCharacter
{
public:
KnightBoss(int bh, HealthCalcFunctionBaseClass *Func) :GameCharacter(bh, Func){}
}; // main函数的接口需要改一下
int main()
{
HealthCalcFunctionBaseClass *CommonMonsterCalc = new HealthCalcFunctionBaseClass();
HealthCalcFunctionBaseClass *BossCalc = new HealthCalcFunctionDerivedClass();
GameCharacter *CommonCharacter = new GameCharacter(, CommonMonsterCalc);
GameCharacter *Boss = new KnightBoss(, BossCalc); cout << "CommonCharacter heath = " << CommonCharacter->GetHealthValue() << endl; //返回100
cout << "KnightBoss heath = " << Boss->GetHealthValue() << endl; // 返回200
}
有的读者会说,既然是在讨论如果用其他方法来替换virtual函数,但这里用来替换virtual函数的还是一个带virtual的类啊,的确,这只是作者的应用strategy模式的一种实现方式而已,第一种NVI方法本身也是离不开private的virutal函数,如果你觉得矛盾,那就还是用纯函数指针的方法吧。(作者也许表达的是想用一种特殊的virtual形式,去代替我们通常意义上见到的virtual函数吧)
书上还说到仿函数的替换方法,有兴趣可以看看。下面总结一下:
1. virtual函数的替代方案NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式;
2. 将机能从成员函数转移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
读书笔记_Effective_C++_条款三十五:考虑virtual函数以外的其他选择的更多相关文章
- 读书笔记_Effective_C++_条款三十九:明智而审慎地使用private继承
private继承的意义在于“be implemented in turns of”,这个与上一条款中说的复合模型的第二层含义是相同的,这也意味着通常我们可以在这两种设计方法之间转换,但书上还是更提倡 ...
- 读书笔记_Effective_C++_条款三十四:区分接口继承和实现继承
这个条款书上内容说的篇幅比较多,但其实思想并不复杂.只要能理解三句话即可,第一句话是:纯虚函数只继承接口:第二句话是:虚函数既继承接口,也提供了一份默认实现:第三句话是:普通函数既继承接口,也强制继承 ...
- 读书笔记_Effective_C++_条款三十:了解inline的里里外外
学过基本程序课的同学都知道,inline是内联的关键字,它可以建议编译器将函数的每一个调用都用函数本体替换.这是一种以空间换时间的做法.把每一次调用都用本体替换,无疑会使代码膨胀,但可以节省函数调用的 ...
- 读书笔记_Effective_C++_条款三十二:确定你的public继承继承塑模出is-a关系
这一条款是说的是公有继承的逻辑,如果使用继承,而且继承是公有继承的话,一定要确保子类是一种父类(is-a关系).这种逻辑可能与生活中的常理不相符,比如企鹅是生蛋的,所有企鹅是鸟类的一种,直观来看,我们 ...
- 读书笔记_Effective_C++_条款四十五:运用成员函数模板接受所有兼容类型
比如有一个Base类和一个Derived类,像下面这样: class BaseClass {…}; class DerivedClass : public BaseClass {…}; 因为是父类与子 ...
- 读书笔记_Effective_C++_条款三十六:绝不重新定义继承而来的non-virtual函数
这个条款的内容很简单,见下面的示例: class BaseClass { public: void NonVirtualFunction() { cout << "BaseCla ...
- 读书笔记_Effective_C++_条款二十五: 考虑写出一个不抛出异常的swap函数
在之前的理论上调用对象的operator=是这样做的 void swap(A& x) { std::swap(a, x.a); } A& operator=(const A& ...
- 读书笔记_Effective_C++_条款三十八:通过复合塑模出has-a或者is-implemented-in-terms-of
如果说public是一种is-a的关系的话,那么复合就是has-a的关系.直观来说,复合就是在一个类中采用其他类的对象作为自身的成员变量,可以举个例子,像下面这样: class Person { pr ...
- 读书笔记_Effective_C++_条款三十七:绝不重新定义继承而来的缺省参数值
先看下面的例子: enum MyColor { RED, GREEN, BLUE, }; class Shape { public: ; }; class Rectangle: public Shap ...
随机推荐
- ps查看CPU和内存占用前10的进程
内存增序 ps aux --sort rss 内存减序 ps aux --sort -rss cpu增序 ps auxw --sort=%cpu cpu减序 ps auxw --sort=-%cpu ...
- 编译器是如何实现32位整型的常量整数除法优化的?[C/C++]
引子 在我之前的一篇文章[ ThoughtWorks代码挑战——FizzBuzzWhizz游戏 通用高速版(C/C++ & C#) ]里曾经提到过编译器在处理除数为常数的除法时,是有优化的,今 ...
- Runtime.getRuntime().exec 类 防止阻塞
import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; impor ...
- tf.nn.embedding_lookup函数
tf.nn.embedding_lookup(params, ids, partition_strategy='mod', name=None, validate_indices=True, max_ ...
- java基础75 xpth技术(网页知识)
1.xpth技术 1.1.xpath的作用 主要用于快速获取所需的节点对象. list<Node> selectNodes("xpath"); 查询多个节点对象 ...
- Eclipse 配置语言环境
一.打开https://www.eclipse.org/babel/downloads.php 选择一下版本的Bable(通天塔) 选择 解压 打开Eclipse 软件 选择Help->inst ...
- Codeforces 988F Rain and Umbrellas(DP)
题目链接:http://codeforces.com/contest/988/problem/F 题目大意: 有三个整数a,n,m,a是终点坐标,给出n个范围(l,r)表示这块区域下雨,m把伞(p,w ...
- volatile 学习笔记
全面理解Java内存模型(JMM)及volatile关键字 正确使用 Volatile 变量 Java内存模型 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步.通信是指线程之间 ...
- 2、图文讲解.NET CLR是什么
大家首先要清楚的是,.NET平台与C#不是一回事.这点大家一定要明白,对开发人员来讲他有两个概念.第一,它是C#,VB.net等程序运行的平台.第二,它因为为这些语言提供了丰富的类库(称之为基类库), ...
- 企业级Docker Registry —— Harbor搭建和使用
本节内容: Harbor介绍 安装部署Harbor 环境要求 环境信息 安装部署harbor 配置harbor 配置存储 完成安装和启动harbor 访问Harbor 修改管理员密码 启动后相关容器 ...