举书上的例子,考虑一个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函数以外的其他选择的更多相关文章

  1. 读书笔记_Effective_C++_条款三十九:明智而审慎地使用private继承

    private继承的意义在于“be implemented in turns of”,这个与上一条款中说的复合模型的第二层含义是相同的,这也意味着通常我们可以在这两种设计方法之间转换,但书上还是更提倡 ...

  2. 读书笔记_Effective_C++_条款三十四:区分接口继承和实现继承

    这个条款书上内容说的篇幅比较多,但其实思想并不复杂.只要能理解三句话即可,第一句话是:纯虚函数只继承接口:第二句话是:虚函数既继承接口,也提供了一份默认实现:第三句话是:普通函数既继承接口,也强制继承 ...

  3. 读书笔记_Effective_C++_条款三十:了解inline的里里外外

    学过基本程序课的同学都知道,inline是内联的关键字,它可以建议编译器将函数的每一个调用都用函数本体替换.这是一种以空间换时间的做法.把每一次调用都用本体替换,无疑会使代码膨胀,但可以节省函数调用的 ...

  4. 读书笔记_Effective_C++_条款三十二:确定你的public继承继承塑模出is-a关系

    这一条款是说的是公有继承的逻辑,如果使用继承,而且继承是公有继承的话,一定要确保子类是一种父类(is-a关系).这种逻辑可能与生活中的常理不相符,比如企鹅是生蛋的,所有企鹅是鸟类的一种,直观来看,我们 ...

  5. 读书笔记_Effective_C++_条款四十五:运用成员函数模板接受所有兼容类型

    比如有一个Base类和一个Derived类,像下面这样: class BaseClass {…}; class DerivedClass : public BaseClass {…}; 因为是父类与子 ...

  6. 读书笔记_Effective_C++_条款三十六:绝不重新定义继承而来的non-virtual函数

    这个条款的内容很简单,见下面的示例: class BaseClass { public: void NonVirtualFunction() { cout << "BaseCla ...

  7. 读书笔记_Effective_C++_条款二十五: 考虑写出一个不抛出异常的swap函数

    在之前的理论上调用对象的operator=是这样做的 void swap(A& x) { std::swap(a, x.a); } A& operator=(const A& ...

  8. 读书笔记_Effective_C++_条款三十八:通过复合塑模出has-a或者is-implemented-in-terms-of

    如果说public是一种is-a的关系的话,那么复合就是has-a的关系.直观来说,复合就是在一个类中采用其他类的对象作为自身的成员变量,可以举个例子,像下面这样: class Person { pr ...

  9. 读书笔记_Effective_C++_条款三十七:绝不重新定义继承而来的缺省参数值

    先看下面的例子: enum MyColor { RED, GREEN, BLUE, }; class Shape { public: ; }; class Rectangle: public Shap ...

随机推荐

  1. 浅谈mysql配置优化和sql语句优化【转】

    做优化,我在这里引用淘宝系统分析师蒋江伟的一句话:只有勇于承担,才能让人有勇气,有承担自己的错误的勇气.有承担错误的勇气,就有去做事得勇气.无论做什么事,只要是对的,就要去做,勇敢去做.出了错误,承担 ...

  2. Task多线程进行多进程

    using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using Sys ...

  3. 2013 NEERC

    2013 NEERC Problem A. ASCII Puzzle 题目描述:完成一个拼图. solution 暴搜,但好像挺难打的,但听说因为题目限制比较多,其实很多奇怪的情况都不存在. Prob ...

  4. CEC2017 benchmark function调用接口

    CEC2017 benchmark function可以从这里下载. 导师最近给了个课题让我自己研究,跟智能优化算法相关的,必不可免的要用到最新的CEC2017 benchmark function, ...

  5. os.path.isdir(path)异常

    Window 10家庭中文版,Python 3.6.4, 当一个路径以多个斜杠(/)或反斜杠字符(\\)结尾时,os.path.isdir(path)函数仍然将它们判断为目录: >>> ...

  6. 利用mysql的binlog恢复数据

    MySQL Binary Log也就是常说的bin-log, ,是mysql执行改动产生的二进制日志文件,其主要作用有两个: * 数据回复 * 主从数据库.用于slave端执行增删改,保持与maste ...

  7. Linux入门(一)root密码设置和用户切换

    从这学期开始,本人将会亲自开一个Linux 专题学习包括Linux 常用命令,常见问题的一些解决方法,以及Linux系统下C和C++一些学习经验 下面这张图片是首次安装Ubuntu后第一次设置root ...

  8. 拉格朗日(Lagrange)插值算法

    拉格朗日插值(Lagrange interpolation)是一种多项式插值方法,指插值条件中不出现被插函数导数值,过n+1个样点,满足如下图的插值条件的多项式.也叫做拉格朗日公式.  这里以拉格朗日 ...

  9. 2款适合HTML做音频和视频的插件

    Flowplayer-视频播放器 Flowplayer 是一个开源(GPL 3的)WEB视频播放器.您可以将该播放器嵌入您的网页中,如果您是开发人员,您还可以自由定制和配置播放器相关参数以达到您要的播 ...

  10. 【51nod】1149 Pi的递推式

    题解 我们把这个函数的递归形式画成一张图,会发现答案是到每个出度为0的点的路径的方案数 这个可以用组合数算 记录一下P[i]为i减几次PI减到4以内 如果P[i + 1] > P[i],那么转向 ...