热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

EffectiveC++条款35:继承与面向对象——考虑virtual函数以外的其他选择

假设现在有这样的一个程序:正在写一个视频游戏软件,现在为游戏内的任务设计一个继承体系现在我们定义一个函数healthValue(),用来表
  • 假设现在有这样的一个程序:
    • 正在写一个视频游戏软件,现在为游戏内的任务设计一个继承体系
    • 现在我们定义一个函数healthValue(),用来表示人物的健康状态,其返回一个整数
    • 由于不同的人可能会以不同的方式计算他们的健康指数,因此我们可能会想到将healthValue()函数定义为virtual。如下所示

class GameCharacter {public://返回人物的健康指数,派生类可以重新定义它virtual int healthValue() const;};

  • 上面这个设计方案虽然可行,但是从某个角度来说却成了它的弱点。本文将讨论有没有别的方法来避免使用virtual函数

一、藉由Non-Virtual Interface手法实现Template Method模式
  • 此手法主张的做法:
    • 将healthValue()函数声明为public,并且改为non-virtual函数
    • 再设计一个private virtual函数,将healthValue()原本的功能移至该函数中,然后在healthValue()函数中调用该函数
  • 代码如下:

class GameCharacter {public://派生类不应该重新定义它int healthValue()const {//... //事前工作int retVal = doHealthValue();//.. //事后工作return retVal;}private://返回人物的健康指数,派生类可以重新定义它virtual int doHealthValue()const {}};

  • 注意事项:成员函数在类中进行定义就会变为inline,但是此处不是,此处只是为了演示代码而已

NVI手法特点

  • 令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一个独特表现形式。我们把这个non-virtual 函数称为virtual函数的外覆器
  • NVI手法的优点:我们可以在non-virtual函数中做一些其他事情。例如:
    • 事前工作:可以进行锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等
    • 事后工作:可以进行互斥器解锁、验证函数的事后条件、再次验证class约束条件等等
  • 这些优点是在客户端直接调用virtual函数的情况中做不到的


  • NVI手法可以在派生类中重新定义private virtual函数:
    • 重新定义virtual函数:表示某些事“如何”被完成
    • 调用virtual函数:表示它合适被完成
    • 这两件事情互不干扰。因此NVI手法允许在派生类中重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力

class GameCharacter {//同上private:virtual int doHealthValue()const {std::cout <<"Base" <healthValue(); //打印&#xff1a;BaseGameCharacter *p2 &#61; new Hero;p2->healthValue(); //打印&#xff1a;Derivedreturn 0;}

  • 关于virtual函数的访问级别&#xff1a;
    • 在NVI手法下其实virtual函数不一定得是private的
    • 在有些情况下&#xff0c;要求派生类在virtual函数中调用基类的virtual函数&#xff08;参阅条款27&#xff09;&#xff0c;那么当virtual函数在基类中为private之后&#xff0c;派生类就不可以访问了。因此&#xff0c;在这种情况下&#xff0c;virtual函数可以设置为protected
    • 在NVI手法下&#xff0c;virtual函数不要设置为public&#xff0c;因为设置为public之后&#xff0c;就与NVI手法的初衷相反了&#xff0c;失去了封装性

二、藉由Function Pointers实现Strategy模式
  • 上面介绍的NVI方法虽然可以避免客户端直接调用virtual函数&#xff0c;但是在non-virtual函数中还是调用了virtual函数&#xff0c;这种方法还是没有免去定义virtual函数的情况
  • 现在我们进行另一种设计&#xff0c;要求每个人物的构造函数接受一个指针&#xff0c;指向一个健康计算函数&#xff0c;我们可以调用该函数进行实际计算。代码如下&#xff1a;

class GameCharacter;//默认的&#xff0c;计算健康指数int defaultHealthCala(const GameCharacter& gc);class GameCharacter {public://函数指针别名typedef int(*HealthCalcFunc)(const GameCharacter& gc);//构造函数explicit GameCharacter(HealthCalaFunc hcf &#61; defaultHealthCalc):healthFunc(hcf) {}int healthValue() {//通过函数指针调用函数return healthFunc(*this);}private:HealthCalcFunc healthFunc; //函数指针};

优点

  • ①同一个人物类型之间可以有不同的健康计算函数。例如&#xff1a;

class GameCharacter { //同上 };class EvilBadGuy :public GameCharacter {explicit EvilBadGuy(HealthCalaFunc hcf &#61; defaultHealthCalc):GameCharacter(hcf) {}//..};int loseHealthQuickly(const GameCharacter&);int loseHealthSlowly(const GameCharacter&);int main(){EvilBadGuy ebg1(loseHealthQuickly);EvilBadGuy ebg2(loseHealthQuickly);return 0;}

  • ②已定义的对象&#xff0c;在运行期间可以更改健康指数计算函数。例如&#xff0c;可以在类中再添加一个成员函数&#xff0c;用来更改当前计算健康指数的函数指针


此种方法的争议

  • ①当全局函数可以根据class的public接口来取得信息并且加以计算&#xff0c;那么这种方法是没有问题的。但是如果计算需要访问到class的non-public信息&#xff0c;那么全局函数就不可以使用了。这个争议将持续到本文的结束
  • ②解决上面的问题&#xff0c;唯一方法就是&#xff1a;弱化class的封装。例如将这个全局函数定义为class的friend&#xff0c;或者为其某一部分提供public访问函数
  • 因此&#xff0c;这些争议对于“以函数指针替换virtual函数”其是否利大于弊&#xff1f;取决于你的是继续需求


三、藉由tr1::function完成Strategy模式
  • “二”中介绍使用全局函数替换成员函数&#xff0c;这种成员函数太过死板&#xff0c;因为“健康指数计算”不必非得是个函数&#xff0c;还可以是其他类型的东西&#xff08;例如函数模板、函数对象等&#xff09;&#xff0c;只要其能计算“健康指数”即可&#xff0c;我们可以使用C&#43;&#43;标准库中的function模板来取代全局函数
  • function模板语法参阅&#xff1a;https://blog.csdn.net/qq_41453285/article/details/95184168
  • 例如&#xff1a;

class GameCharacter;int defaultHealthCala(const GameCharacter& gc);class GameCharacter {public://其余部分同上//只是将函数指针改为了function模板&#xff0c;其接受一个const GameCharacter&参数&#xff0c;并返回inttypedef std::tr1::function HealthCalcFunc;explicit GameCharacter(HealthCalcFunc hcf &#61; defaultHealthCalc):healthFunc(hcf) {}int healthValue() {return healthFunc(*this);}private:HealthCalcFunc healthFunc;};

使用场景

  • function模板的语法知识就不再介绍了。现在我们可以不单单调用全局函数来计算“人物的健康指数”&#xff0c;还可以设计很多种方式来计算
  • 现在我们可以自己定义其他方式来计算健康指数。例如&#xff1a;

class GameCharacter { //同上};class EvilBadGuy :public GameCharacter {public:explicit EvilBadGuy(HealthCalcFunc hcf &#61; defaultHealthCalc):GameCharacter(hcf) {}//..};class EyeCandyCharacter :public GameCharacter {//构造函数类似EvilBadGuy};//计算健康指数函数short calcHealth(const GameCharacter&);//函数对象&#xff0c;用来计算健康指数struct HealthCalculator {int operator()(const GameCharacter&)const {}};//其提供一个成员函数&#xff0c;用以计算健康class GameLevel {public:float health(const GameCharacter&)const;};int main(){//人物1&#xff0c;其使用calcHealth()函数来计算健康指数EvilBadGuy ebg1(calcHealth);//人物2&#xff0c;其使用HealthCalculator()函数对象来计算健康指数EyeCandyCharacter ecc1(HealthCalculator());//人物2&#xff0c;其使用GameLevel类的health()成员函数来计算健康指数GameLevel currentLevel;EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));return 0;}

 


  • 将普通函数替换为function模板&#xff0c;为我们提供了更多可调用物

四、古典的Strategy模式
  • 在古典的Strategy设计模式中&#xff0c;会将用来计算健康的函数设计为一个继承体系&#xff0c;并且有virtual函数&#xff0c;这些函数用来计算健康
  • UML图如下&#xff0c;意义如下&#xff1a;
    • GameCharacter是一个继承体系的根类&#xff0c;其派生类有EvilBadGuy、EyeCandyCharacter
    • HealthCalcFunc是一个继承体系的根类&#xff0c;其派生类有SlowHealthLoser、FastHealthLoser
    • 每一个GameCharacter对象都内含一个指针&#xff0c;指向于一个来自HealthCalcFunc继承体系中的对象

  • 下面是代码&#xff1a;

class GameCharacter;class HealthCalcFunc { //计算健康指数的类public:virtual int calc(const GameCharacter& gc)const {}};HealthCalcFunc defaultHealthCalc;class GameCharacter {public:explicit GameCharacter(HealthCalcFunc* hcf &#61; &defaultHealthCalc):pHealthCalc(hcf) {}int healthValue() {return pHealthCalc->calc(*this);}private:HealthCalcFunc* pHealthCalc;};

  • 这个模式也具有弹性&#xff0c;例如为HealthCalcFunc类添加派生类&#xff0c;那么就可以纳入新的计算方法


五、4种方法的总结
  • ①使用non-virtual interface&#xff08;NVI&#xff09;手法&#xff0c;那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性&#xff08;private或protected&#xff09;的virtual函数
  • ②将virtual函数替换为“函数指针成员变量”&#xff0c;这是Strategy设计模式的一种分解表现形式
  • ③以tr1::function成员变量替换virtual函数&#xff0c;因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式
  • ④将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法

六、本文总结
  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式
  • 将机能从成员函数移到class外部函数&#xff0c;带来的一个缺点是&#xff1a;非成员函数无法访问class的non-public成员
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物

推荐阅读
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 在Oracle11g以前版本中的的DataGuard物理备用数据库,可以以只读的方式打开数据库,但此时MediaRecovery利用日志进行数据同步的过 ... [详细]
  • 本文介绍了一种轻巧方便的工具——集算器,通过使用集算器可以将文本日志变成结构化数据,然后可以使用SQL式查询。集算器利用集算语言的优点,将日志内容结构化为数据表结构,SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。本文还详细介绍了具体的实施过程。 ... [详细]
author-avatar
男人还是闷骚点好
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有