重写虚函数和继承

Posted

技术标签:

【中文标题】重写虚函数和继承【英文标题】:Overriding virtual functions and inheritance 【发布时间】:2018-10-11 20:47:16 【问题描述】:

我无法完全理解 C++ 中的重写虚函数以及调用此类函数时究竟会发生什么。我正在阅读 Bjarne Stroustrup 使用 C++ 编写的 PPP,他提供了以下示例来展示重写和虚函数:

struct B 
    virtual void f() const  cout<<"B::f; 
    void g() const  cout << "B::g";         //not virtual
;

struct D:B 
        void f() const  cout<<"D::f";  //overrides B::f
        void g()  cout<<"D::g"; 
;

struct DD:D 
        void f()  cout<<"DD::f"; 
        void g() const  cout<<"DD::g";    
;

void call(const B& b) 
    //a D is kind of B, so call() can accept D
    //a DD is kind of D and a D is a kind of B, so call() can accept a DD
    b.f();
    b.g();


int main() 
    B b;
    D d;
    DD dd;

    call(b);
    call(d);
    call(dd);

    b.f();
    b.g();
    d.f();
    d.g();
    dd.f();
    dd.g();

输出:B::f B::g D::f B::g D::f B::g B::f B::g D::f D::g DD::f DD::g

我了解 call(b) 如何直接输出 B::f B::g

现在call(d)。我不太明白为什么,但似乎call() 可以将B 的派生类作为参数。行。所以在 call() 中,b.f() 变为 d.f(),因为 D::f 覆盖 B::f。实际上输出显示D::f。但是D::g 不会覆盖B::g,并且由于我无法理解的原因,在我看来D::g 在执行call(d) 时没有任何效果——在这种情况下输出B::g

接下来,我们执行call(dd),输出D::f B::g。应用与上面相同的逻辑(?)很明显DD::f 不会覆盖D::f -- 不是 const -- 并且DD::g 不会覆盖D::gB::g 因为两者都不是virtual .

接下来发生的事情让我感到困惑。 b.f()b.g()d.f()d.g()dd.f()dd.g() 的每个单独调用都会输出结果,就好像根本不存在覆盖一样! 例如,当几秒钟前 d.g() in call() 输出 B::g 时,d.g() 怎么会输出 D::g ? 另外,dd.f() 中的dd.f() 中的call() 中的输出D::f 中的DD::f 怎么可能输出D::f

可以肯定地说我在这里遗漏了一些重要的东西,为此我需要帮助。

【问题讨论】:

你没有意识到一个模式吗?对于 virtual 方法是根据对象的实际类型来选择的。对于非虚拟,要调用的方法是静态选择的。让你困惑的是虚方法和非虚方法的区别 说实话,这段代码让我很困惑,而且我用 C++ 编程已经 30 多年了。事实上,Srtroustrup 并不是一位好老师。 @user463035818,很抱歉,但我不太明白“静态选择调用的非虚拟方法”是什么意思。究竟是如何根据对象的实际类型来选择虚方法的呢? @Neil Butterworth,实际上我认为文字很棒,他花时间正确解释事情。但有时他会为后面的章节保存一些东西,或者在没有解释的情况下使用它们,但会明确地说出来(这将在稍后变得清楚等)。 【参考方案1】:

请耐心阅读本文以获得您的问题的答案。

继承是一个类的对象继承另一个类的对象的属性和行为的概念。

基本介绍

父类也称为基类或超类。 子类也称为派生类或子类。 派生类的对象可以通过基类引用。

例如

#include <iostream>
using namespace std;
class Base ;
class Derived : public Base ;
int main() 
  Base *b = new Derived(); // This is completely valid
  return 0;

C++ 中的方法覆盖

让我们举一个基本的例子

#include <iostream>
using namespace std;
class Base 
public:
  void display()  cout << "Base display called\n"; 
;
class Derived : public Base 
public:
  void display()  cout << "Derived display called\n"; 
;
int main() 
  Base b;
  b.display();
  Derived d;
  d.display();
  Base *bptr = &d;
  bptr->display();
  return 0;

输出:

Base display called
Derived display called
Base display called

现在,从上面的示例中,您可能已经猜到派生类会覆盖基类,但事实并非如此。输出(第 3 行)显示调用了基类函数,因为该函数不是虚拟的。

C++ 中的虚函数

您可以通过在函数开头添加“virtual”关键字来使类的任何函数成为虚拟函数。

让我们考虑虚函数示例

#include <iostream>
using namespace std;
class Base 
public:
  virtual void display()  cout << "Base display called\n"; 
;
class Derived : public Base 
public:
  void display()  cout << "Derived display called\n"; 
;
int main() 
  Base b;
  b.display();
  Derived d;
  d.display();
  Base *bptr = &d;
  bptr->display();
  return 0;

输出:

Base display called
Derived display called
Derived display called

通过上面的例子(输出的第3行),很明显可以使用C++中的虚函数机制来实现方法覆盖。

将函数设为虚有什么效果?

普通函数和虚函数的区别在于普通函数在编译时解析,也称为静态绑定,而虚函数在运行时解析,也称为动态绑定或后期绑定。调用哪个方法(基类显示或派生类显示方法)在运行时解析,因为基类显示函数是虚拟的。您可以通过阅读 v-table 深入了解虚函数机制。

问题解答

    呼叫(d)。我不太明白为什么,但似乎 call() 可以将 B 的派生类作为参数。

    派生类对象可以被基类引用

    但 D::g 并没有覆盖 B::g,原因我无法理解。

    因为 g() 在基类中不是虚拟的。只有虚函数可以被覆盖。 v-table 只有虚函数的条目,因此它们可以在运行时被覆盖。

    调用(dd)

    由于 DD 中的 f() 是一个非常量函数,因此 f()(DD 中的非常量 f())不是父类 D 的重写方法。并且由于它被基类引用B,调用 b.f() 将调用被 D 覆盖的 const f()。因此 D::f 被打印出来。 如果 f() 是 DD 中的 const 方法,则会发生以下情况:当调用 f() 时,首先在基类 B 中搜索该函数, 因为 f() 是虚拟的,使用 v-table 指针,在派生类 D 中被覆盖的 f() 函数被解析。但是由于 f() 在 D 类中不是虚拟的,因此无法解析 DD 中覆盖的 f()。因此 D::f 被打印出来。 但是对于g(),基类本身没有虚g(),所以无法解析其派生类中被覆盖的函数。因此,B::g 被打印出来。

    发生上述多态性是因为派生类被其基类(父类)引用,但在最后的调用中,没有这样的事情。所有对象都被它们各自的类引用,因此它们的相应方法被调用。

考虑这一点的一个基本逻辑是,首先在引用类中查找函数,如果它是虚函数,则将在派生类中搜索该函数(如果被引用的对象是子类)。如果派生类覆盖,则将调用派生方法,否则将调用基方法。您可以进一步扩展应用于基类、派生类的概念,以及检查函数是否为虚拟,然后检查函数是否为虚拟并且对象是派生的(派生的子类,基类的孙子) ) 等等。

希望这可以澄清。

【讨论】:

老实说,我还没有涵盖指针,所以你的例子对我来说并不完全清楚。但到目前为止,我认为我掌握了覆盖/虚拟功能的基础知识。你能理解我在原帖中提出的问题吗? @bruneleski 为了理解您的问题,您必须了解什么是虚拟表以及动态多态性如何与之相关。如果我想缩短我在下面给出的答案,那就是虚拟表中不存在非虚拟函数,因此无法利用运行时多态性。 @Yucel_K,哦,我明白了,与此同时,我阅读了有关虚拟表和虚拟指针的信息,您所说的内容变得更加清晰。非常感谢! 这个答案有一个错误。 DD::f() 不是 B::f() 或 D::f() 的覆盖,因为它具有不同的签名。 B::f() 和 D::f() 都是 const 方法,而 DD::f() 不是。这就是为什么在 call(dd) 中不调用它的原因。如果 DD::f() 被标记为 const,那么它将被 call(dd) 调用。【参考方案2】:

例如,d.g() 怎么能输出 D::g 而就在几秒钟前 call() 中的 d.g() 输出 B::g ?

当您将派生对象作为基类类型的指针或引用传递时,您将保留其多态属性。但是,这并不意味着您仍然可以访问派生类的非虚函数。

考虑这个例子,

B *bp;
bp = &d;
bp->f();
bp->g();

bp-&gt;g()执行时,bg函数将被调用。

【讨论】:

我明白了。因此,当我调用一个方法时,比如说 call(d),它只能看到 B 的成员,因为毕竟这就是参数所指的。我想我明白了。谢谢。 @bruneleski 是的,这是正确的。除非在您的示例中使用了 virtual 关键字,例如您的操作方式。 virtual void f()b 类。如果 virtual void f() 也被覆盖,就像你的 d 正在做的那样,如果 d 作为对调用的引用传递( call(d); )。那么引用参数const B&amp; Ref 中的虚拟表指针的入口将指向dvoid f() const。所以每当你打电话给Ref.f() 时,它将是dvirtual void f()。这方面的术语称为运行时多态性动态多态性

以上是关于重写虚函数和继承的主要内容,如果未能解决你的问题,请参考以下文章

C++:多态(重写,多态原理单继承和多继承的虚函数表)

C++:多态(重写,多态原理单继承和多继承的虚函数表)

C++---多态

C++多态

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表