文章目录

  • 1 公有继承时三种类型的函数行为
    • 1.1 纯虚函数 (pure virtual)
    • 1.2 普通虚函数
      • **1.2.1 方法一**
      • **1.2.2 方法二**
    • 1.3 非虚函数
  • 2 重写 (override)
  • 小结

1 公有继承时三种类型的函数行为

公有继承包含两部分:一是 “函数接口” (interface),二是 “函数实现” (implementation)。为何这里只谈公有继承,因为私有继承没办法被继承啊,而保护继承虽然说可以,但是类外无法访问啊,所以只有公有继承可以实现接口继承。

如 Shape 类中,三个成员函数,对应三种继承方式:

class Shape {public:virtual void Draw() const = 0;    // 1) 纯虚函数virtual void Error(const string& msg);  // 2) 普通虚函数int ObjectID() const;  // 3) 非虚函数
};class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

1.1 纯虚函数 (pure virtual)

纯虚函数,继承的是基类中成员函数的接口,且要在派生类中,重写成员函数的实现。即纯虚函数规定了接口的模板,让派生类按照这个模板去实现,这个模板包含了函数返回值和形参列表,这两样必须一致。

Shape *ps1 = new Rectangle;
ps1->Draw(); // calls Rectangle::DrawShape *ps2 = new Ellipse;
ps2->Draw(); // calls Ellipse::Draw

基类中纯虚函数可以不给出实现,也可以给出实现,但实现一般没有意义,因为纯虚函数的实现不会被子类继承,所以开发的时候就不会写父类实现了。

虽然说不会被子类继承,但子类仍然可以调用基类的纯虚函数,须加类作用域操作符 ::

ps1->Shape::Draw(); // calls Shape::draw

1.2 普通虚函数

普通虚函数,会在基类中,定义一个缺省的实现 (default implementation),表示继承的是基类成员函数接口和缺省实现,由派生类选择是否重写该函数。

实际上,允许普通虚函数 同时继承接口和缺省实现是危险的。 如下, ModelA 和 ModelB 是 Airplane 的两种飞机类型,且二者的飞行方式完全相同:

class Airplane {public:virtual void Fly(const string& destination);
};
class ModelA: public Airplane { ... };
class ModelB: public Airplane { ... };

这是典型的面向对象设计,两个类共享一个特性 – Fly,则 Fly 可在基类中实现,并由两个派生类继承之。现增加另一个飞机型号 ModelC,其飞行方式与 ModelA,ModelB 不相同,如果不小心忘记在 ModelC 中重写新的 Fly 函数

class ModelC: public Airplane {... // no fly function is declared
};

则调用 ModelC 中的 fly 函数,就是调用 Airplane::Fly,但是 ModelC 的飞行方式和缺省的并不相同

Airplane *pa = new ModelC;
pa->Fly(Qingdao); // calls Airplane::fly!

即前面所说的,普通虚函数同时继承接口和缺省实现是危险的,最好是基类中实现缺省行为 (behavior),但只有在派生类要求时才提供该缺省行为。

1.2.1 方法一

一种方法是 纯虚函数 + 缺省实现,因为是纯虚函数,所以只有接口被继承,其缺省的实现不会被继承。派生类要想使用该缺省的实现,必须显式的调用。

class Airplane {public:virtual void Fly(const string& destination) = 0;
};void Airplane::Fly(const string& destination)
{ // a pure virtual function default code for flying an airplane to the given destination
}class ModelA: public Airplane {public:virtual void Fly(const string& destination) { Airplane::Fly(destination); }
};

这样在派生类 ModelC 中,即使一不小心忘记重写 Fly 函数,也不会调用 Airplane 的缺省实现。

class ModelC: public Airplane {public:virtual void Fly(const string& destination);
};void ModelC::Fly(const string& destination)
{// code for flying a ModelC airplane to the given destination
}

1.2.2 方法二

可以看到,上面问题的关键就在于,一不小心在派生类 ModelC 中忘记重写 fly 函数,C++11 中使用关键字 override,可以避免这样的“一不小心”。

1.3 非虚函数

非虚成员函数没有 virtual 关键字,表示派生类不但继承了接口,而且继承了一个强制实现 (mandatory implementation)。

既然继承了一个强制的实现,则在派生类中,无须重新定义 (redefine) 继承自基类的成员函数,如下:

使用指针调用 ObjectID 函数,则都是调用的 Shape::ObjectID()

Rectangel rc; // rc is an object of type RectangleShape *pB = &rc; // get pointer to rc
pB->ObjectID(); // call ObjectID() through pointerRectangle *pD = &rc; // get pointer to rc
pD->ObjectID(); // call ObjectID() through pointer

如果在派生类中重新定义了继承自基类的成员函数 ObjectID 呢?

class Rectangel : public Shape {public:int ObjectID() const; // hides Shape::ObjectID
};pB->ObjectID(); // calls Shape::ObjectID()
pD->ObjectID(); // calls Rectagle::ObjectID()

此时,派生类中重新定义的成员函数会 “隐藏” (hide) 继承自基类的成员函数

这是因为非虚函数是 “静态绑定” 的,pB 被声明的是 Shape* 类型的指针,则通过 pB 调用的非虚函数都是基类中的,既使 pB 指向的是派生类

与“静态绑定”相对的是虚函数的“动态绑定”,即无论 pB 被声明为 Shape* 还是 Rectangle* 类型,其调用的虚函数取决于 pB 实际指向的对象类型

2 重写 (override)

在 1.2.2 中提到 override 关键字,可以避免派生类中忘记重写虚函数的错误

下面以重写虚函数时,容易犯的四个错误为例,详细阐述之:

class Base {public:virtual void mf1() const;virtual void mf2(int x);virtual void mf3() &;void mf4() const;    // is not declared virtual in Base
};class Derived: public Base {public:virtual void mf1();        // declared const in Base, but not in Derived.virtual void mf2(unsigned int x);    // takes an int in Base, but an unsigned int in Derivedvirtual void mf3() &&;    // is lvalue-qualified in Base, but rvalue-qualified in Derived.void mf4() const;
};

在派生类中,重写 (override) 继承自基类成员函数的实现 (implementation) 时,要满足如下条件:

一虚:基类中,成员函数声明为虚拟的 (virtual)

二容:基类和派生类中,成员函数的返回类型和异常规格 (exception specification) 必须兼容

四同:基类和派生类中,成员函数名、形参类型、常量属性 (constness) 和 引用限定符 (reference qualifier) 必须完全相同

如此多的限制条件,导致了虚函数重写如上述代码,极容易因为一个不小心而出错

C++11 中的 override 关键字,可以显式的在派生类中声明,哪些成员函数需要被重写,如果没被重写,则编译器会报错。

class Derived: public Base {public:virtual void mf1() override;virtual void mf2(unsigned int x) override;virtual void mf3() && override;virtual void mf4() const override;
};

这样,即使不小心漏写了虚函数重写的某个苛刻条件,也可以通过编译器的报错,快速改正错误

class Derived: public Base {public:virtual void mf1() const override;  // adding "virtual" is OK, but not necessaryvirtual void mf2(int x) override;void mf3() & override;void mf4() const override;
};

小结

  1. 公有继承

纯虚函数 => 继承的是:接口 (interface)

普通虚函数 => 继承的是:接口 + 缺省实现 (default implementation)

非虚成员函数 =>继承的是:接口 + 强制实现 (mandatory implementation)

  1. 不要重新定义一个继承自基类的非虚函数 (never redefine an inherited non-virtual function)

既然要重写,那就将其声明为虚函数。这也是《Effective C++》这本书提到的规则之一。

  1. 在声明需要重写的函数后,加关键字 *override*,这个目的是防止形参或者返回值类型写错,或者忘记重写了。

C++ 论公有继承时纯虚函数、虚函数、普通函数的行为表现及虚函数的重写(深度好文)相关推荐

  1. 派生类对基类成员的访问控制之公有继承

    公有继承 前面说过,派生类从基类继承时有三个步骤, 第一个步骤是吸收基类成员,吸收了基类中除构造函数和析构函数之外的所有数据成员和函数成员, 第二个步骤就是修改基类成员,包括修改对基类成员的访问属性和 ...

  2. 关于C++中公有继承、私有继承、保护继承的讨论

    一.文章来由 简单明了的理由,老生常谈但是没真正解决的问题,想搞清楚这个问题. 二.冗长的定义 我们先来看看这些冗长的定义: 公有继承: 当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性 ...

  3. 公有继承/私有继承/保护继承的区别

    1.公有继承–public 公有继承时,对基类的公有成员和保护成员的访问属性不变,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员.派生类的对象只能访问派生类的公有成员(包 ...

  4. C++公有继承,保护继承,私有继承的区别

    1.公有继承–public 公有继承时,对基类的公有成员和保护成员的访问属性不变,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员.派生类的对象只能访问派生类的公有成员(包 ...

  5. 23.C++- 继承的多种方式、显示调用父类构造函数、父子之间的同名函数、virtual虚函数...

     上章链接: 22.C++- 继承与组合,protected访问级别 继承方式 继承方式位于定义子类的":"后面,比如: class Line : public Object // ...

  6. C++虚继承时的构造函数

    在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数.对最终的派生类来说,虚基类是间接基类,而不是直接基类.这跟普通继承不同,在普通继承中,派生类构造函 ...

  7. c++远征之继承篇——多重继承,多继承,虚继承,多继承时的重复定义解决方法

    以下内容源于慕课网的学习整理,如有侵权,请告知删除. 1.多重继承 (1)概念理解 2.多继承 (1)概念理解 3.虚继承 (1)问题的引出:多重继承和多继承的结合,造成数据的冗余.     (2)解 ...

  8. C++ | 公有,私有,受保护成员以及继承时的作用

    1.public:public表明该数据成员.成员函数是对所有用户开放的,所有用户都可以直接进行调用 2.private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直 ...

  9. python3 多继承时,父类有相同一个函数的选择

    class People:name = ''age = 0__weight = 0def __init__(self, name, age, weight):print("People 初始 ...

最新文章

  1. 怎么调用获取被创建的预制体_Uber 开源 Plato:扩展性极强的开发测试会话 AI 平台,可实现多智能体并行训练...
  2. unity 实现文本选中_Unity中如何读取TXT文本内容
  3. 基于 OSGi 的面向服务的组件编程
  4. parseInt(),parseFloat(),parse()
  5. Uboot启动过程详解
  6. python——面向对象篇之异常和反射
  7. linux x86板级文件,Linux driver 板级文件跟踪一般方法
  8. hosts多个ip对应一个主机名_一个简单的Web应用程序,用作连接到ssh服务器的ssh客户端...
  9. mysql架设_主从mysql架设
  10. 企业如何建设BI商业智能系统
  11. 字符串匹配【模板】(luogu 3375)
  12. 广度优先搜索(BFS)——抓住那头牛(POJ 4001)
  13. vs中bitmap等图标文件你在哪?
  14. 应该用怎样的态度对待孩子?
  15. 微软推出Azure区块链开发套件,重点解决两大难题
  16. linux各路径(目录)的解释(转载)
  17. 如何使用Java帮助文档
  18. Automating Android with Ant
  19. Lattice FPGA ---IP应用总结之“edp”
  20. bim学习—— 第7章 放置幕墙门窗

热门文章

  1. html做万用表效果,牛人DIY高精度六位半数字万用表 - 电子制作 - 电子发烧友网...
  2. 基于51单片机密码锁设计LCD1602液晶仿真DIY电子制作智能
  3. Hbuilder X 自动保存代码
  4. 网络工程师在现实中的意义
  5. ChatGPT在安全研究领域的应用实践
  6. jQuery库入门学习
  7. Linux内核--ISA总线原理
  8. 计算机网络HTTP篇(一)HTTP 常见面试题
  9. upqc matlab,统一电能质量调节器(UPQC)的分析及其控制策略的研究
  10. 计算机图表应用样式,将在 Microsoft Office 早期版本中创建的图表转换为 SmartArt 图形或形状...