条款三十四:区分接口继承和实现继承

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

这里回顾一下这三类函数,如下:

class BaseClass
{
public:void virtual PureVirtualFunction() = 0; // 纯虚函数void virtual ImpureVirtualFunction(); // 虚函数void CommonFunciton(); // 普通函数
};

纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现),所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。

虚函数则必须有实现,否则会报链接错误。虚函数可以在基类和多个派生类中提供不同的版本,利用多态性质,在程序运行时动态决定执行哪一个版本的虚函数(机制是编译器生成的虚表)。virtual关键字在基类中必须显式指明,在派生类中不必指明,即使不写,也会被编译器认可为virtual函数,virtual函数存在的类可以定义实例对象。

普通函数则是将接口与实现都继承下来了,如果在派生类中重定义普通函数,将会出现名称的遮盖(见条款33),事实上,也是极不推荐在派生类中覆盖基类的普通函数的,如果真的要这样做,请一定要考虑是否该把基类的这个函数声明为虚函数或者纯虚函数。
下面是三类成员函数的应用:

class BaseClass
{
public:void virtual PureVirtualFunction() = 0; // 纯虚函数void virtual ImpureVirtualFunction(); // 虚函数void CommonFunciton(); // 普通函数
};
void BaseClass::PureVirtualFunction()
{cout << "Base PureVirtualFunction" << endl;
}
void BaseClass::ImpureVirtualFunction()
{cout << "Base ImpureVirtualFunciton" << endl;
}class DerivedClass1: public BaseClass
{void PureVirtualFunction(){cout << "DerivedClass1 PureVirturalFunction Called" << endl;}
};class DerivedClass2: public BaseClass
{void PureVirtualFunction(){cout << "DerivedClass2 PureVirturalFunction Called" << endl;}
};int main()
{BaseClass *b1 = new DerivedClass1();BaseClass *b2 = new DerivedClass2();b1->PureVirtualFunction(); // 调用的是DerivedClass1版本的PureVirtualFunctionb2->PureVirtualFunction(); // 调用的是DerivedClass2版本析PureVirtualFunctionb1->BaseClass::PureVirtualFunction(); // 当然也可以调用BaseClass版本的PureVirtualFucntionreturn 0;
}

书上提倡用纯虚函数去替代虚函数,因为虚函数提供了一个默认的实现,如果派生类的想要的行为与这个虚函数不一致,而又恰好忘记去覆盖虚函数,就会出现问题。但纯虚函数不会,因为它从语法上限定派生类必须要去实现它,否则将无法定义派生类的对象。
同时,因为纯虚函数也是可以有默认实现的(但是它从语法上强调派生类必须重定义之,否则不能定义对象),所以完全可以替换虚函数。

普通函数所代表的意义是不变性凌驾与特异性,所以它绝不该在派生类中被重新定义。

在设计类成员函数时,一般既不要将所有函数都声明为non-virtual(普通函数),这会使得没有余裕空间进行特化工作;也一般不要将所有函数都声明为virtual(虚函数或纯虚函数),因为一般会有一些成员函数是基类就可以决定下来的,而被所有派生类所共用的。这个设计法则并不绝对,要视实际情况来定。

最后总结一下:

  1. 接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口;

  2. pure virtual函数只具体指定接口继承;

  3. impure virtual函数具体指定接口继承和缺省实现继承;

  4. non-virutal函数具体指定接口继承以及强制性实现继承。

条款三十五:考虑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() * 2;}
};int main()
{GameCharacter *CommonCharacter = new GameCharacter(100);GameCharacter *Boss = new KnightBoss(100);cout << "CommonCharacter heath = " << CommonCharacter->GetHealthValue() << endl; //返回100cout << "KnightBoss heath = " << Boss->GetHealthValue() << endl; // 返回200
}

GetHealthValue会根据不同类型的游戏角色来获得相应的血量。但这里将虚函数是public的,NVI(Non-Virutal Interface)的一个流派主张所有的虚函数都是private的,将父类与子类都会使用的前置方法与后置方法单独作一个non-virtual的函数,像下面这样:

class GameCharacter
{
private:int BaseHealth;
public:GameCharacter(int bh = 100) :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() * 2;}
};

main函数的接口不必有任何的变化。这里是把父类与子类有特异性的方法都写在了各自private范围内。这样的好处,是可以做一些善前善后的事情(如程序中的cout方法所示),善前方法可以是锁定互斥器,打印日志,验证输入数据合法性等,善后的方法可以是解除锁定,验证事后数据合法性等。

注意这里是将虚函数都置成了private的,但编译器生成的虚表指针则不是private的,否则会因private成员变量根本不被继承而无法实现多态。NVI的方式也不是绝对的,比如虚析构函数,它必须是public的,才能确保它的子类,以及子类的子类们能够顺利释放资源。

那么还有其他方法来替代virtual函数吗?virtual函数的本质是由虚指针和虚表来控制,虚指针指向虚表中的某个函数入口地址,就实现了多态。因此,我们也可以仿照一个虚指针指向函数的手法,来做一个函数指针。像下面这样:

class GameCharacter
{
private:int BaseHealth;
public:typedef int(*HealthCalcFunc)(const GameCharacter&);GameCharacter(int bh = 100, HealthCalcFunc Func= NULL) :BaseHealth(bh), HealthFuncPoniter(Func){}int GetHealthValue(){if (HealthFuncPoniter){return HealthFuncPoniter(*this);}else{return 0;}}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 2 * 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 2 * BaseValue;}
};class GameCharacter
{
private:int BaseHealth;
public:GameCharacter(int bh = 100, HealthCalcFunctionBaseClass *Func = NULL):BaseHealth(bh), HealthFuncPoniter(Func){}int GetHealthValue(){if (HealthFuncPoniter){return HealthFuncPoniter->CalcHealth(GetBaseHealth());}return 0;}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(100, CommonMonsterCalc);GameCharacter *Boss = new KnightBoss(100, BossCalc);cout << "CommonCharacter heath = " << CommonCharacter->GetHealthValue() << endl; //返回100cout << "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++ 条款34相关推荐

  1. Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

    Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously) 条款39:明智而审慎地使用private继承 1.pr ...

  2. Effective C# 原则34:创建大容量的Web API(译)

    Effective C# 原则34:创建大容量的Web API Item 34: Create Large-Grain Web APIs 交互协议的开销与麻烦就是对数据媒体的如何使用.在交互过程中可能 ...

  3. effective c++条款11扩展——关于拷贝构造函数和赋值运算符

    effective c++条款11扩展--关于拷贝构造函数和赋值运算符 作者:冯明德 重点:包含动态分配成员的类 应提供拷贝构造函数,并重载"="赋值操作符. 以下讨论中将用到的例 ...

  4. Effective C++条款09:绝不在构造和析构过程中调用virtual函数

    Effective C++条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruc ...

  5. effective c++条款44 将与参数无关的代码抽离templates

    effective c++条款44 将与参数无关的代码抽离templates 首先了解这个条款的含义:使用template可能导致代码膨胀,二进制码会带着重复(或者几乎重复)的代码.数据,或两者.其结 ...

  6. Effective C++ 条款02:尽量使用const,enum,inline替换#define

    Effective C++ 条款02:尽量使用const,enum,inline替换#define 用另一句话说:用编译器代替预处理器比较好. 举个例子:加入定义一个常量: #define ASPEC ...

  7. Effective C++条款05:了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls)

    Effective C++条款05:了解C++默默编写并调用哪些函数(Know what functions C++ silently writes and calls) 条款05:了解C++默默编写 ...

  8. Effective C++条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously)

    Effective C++条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously) 条款40:明智而审慎地使用多重继承 1.多重继承的两个阵营 2 ...

  9. Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value

    Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to p ...

最新文章

  1. Android移动开发之【Android实战项目】在Service中弹出Dialog对话框,即全局性对话框
  2. Android 中的 Service 全面总结(转)
  3. 11 为了进一步_小米11正式官宣!12月28号整装待发,这几点或成关键
  4. debian与cenos常见命令不同处
  5. Java数字图像处理基础知识 - 必读
  6. 计算机语言学考研科目,语言学考研笔记整理(共16页)
  7. JavaScript中的const
  8. 网页设计代码_盘点2020年网站设计工具让设计师插上翅膀
  9. Nginx的启动(start),停止(stop)命令
  10. 啥时候js单元测试变的重要起来?
  11. HCIE Security 防火墙NAT技术 备考笔记(幕布)
  12. mysql创建表空间和用户
  13. 装机软件五:截图工具
  14. 【精益生产】108页PPT搞懂精益生产价值流分析图(VSM)
  15. file open error: [Errno 2] No such file or directory: '\xe6\xb5\x8b\xe8\xaf\x95.txt'
  16. 《大侦探皮卡丘》天龙八部在路上
  17. pfx证书解析公钥私钥
  18. Plotly安装与使用方法
  19. c语言算法有效性,BerForest—C语言学习笔记-《算法》
  20. FGMap学习之--天气预报

热门文章

  1. 非常适合菜鸟练手的Python项目,墙裂建议收藏!
  2. 更改cognos upfront 的外观
  3. 八段锦:让 IT 人士受益一生的运动救生圈
  4. Python-glove学习
  5. 「近世代數概論」(Garrett Birkhoff,Saunders Mac Lane) 3.1.1 習題1
  6. 【博学谷学习记录】学习心得分享
  7. 【语音识别】基于MFCC的小波变换DTW实现说话人识别matlab代码
  8. 风影导航源码 带后台
  9. 产品级项目---智能随访系统
  10. px(像素)与 dp, sp换算公式