热门标签 | 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对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物

推荐阅读
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了最长上升子序列问题的一个变种解法,通过记录拐点的位置,将问题拆分为左右两个LIS问题。详细讲解了算法的实现过程,并给出了相应的代码。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
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社区 版权所有