覆盖虚函数和继承

Overriding virtual functions and inheritance

我无法完全理解 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() 怎么会在几秒钟前 d.g() 在 call() 输出 B::g 时输出 D::g ? 另外,dd.f()怎么会输出DD::f when in dd.f() in call() output D::f ?

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

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

继承是一个概念,其中 class 的 object 继承另一个 class 的 object 的属性和行为。

基本介绍

  • Parent class 也称为基础 class 或超级 class.
  • Child class 也称为派生 class 或 subclass.
  • 派生class的object可以通过基class引用。

例如

#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

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

C++ 中的虚函数

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

让我们考虑虚函数示例

#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++中的虚函数机制实现方法重写

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

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

深入了解虚函数机制

问题的答案

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

    • 派生的classobject可以被基引用class
  2. 但是 D::g 不会覆盖 B::g 并且出于我无法理解的原因。

    • 因为 g() 在基 class 中不是虚拟的。只能覆盖虚函数。 v-table 仅具有虚函数的条目,因此它们可以在 运行 时被覆盖。
  3. 呼叫(dd)

    • 由于 DD 中的 f() 是一个 non-const 函数,因此 f() (non-const DD 中的 f() )不是 parent class D。由于它被 Base class B 引用,调用 b.f() 将调用被 D 覆盖的 const f()。因此 D::f 被打印出来.
    • 如果 f() 是 DD 中的 const 方法,那么会发生以下情况:
      当调用 f() 时,首先在基 class B 中搜索该函数, 由于 f() 是虚拟的,因此使用 v-table 指针,解决了派生 class D 中被覆盖的 f() 函数的问题。但是由于f()在classD中不是virtual,所以无法解析DD中重写的f()。因此 D::f 被打印出来。
      但是对于g()来说,baseclass本身并没有virtual g(),所以他们的derivedclasses中的重写函数是无法解析的。因此,B::g 得到打印。
  4. 上面的多态性之所以发生,是因为派生的classes被它们的baseclasses(parentclasses引用了,但是在最后的电话中,没有这样的事情。所有 object 都由它们各自的 class 引用,因此它们的适当方法被调用。

考虑这个问题的一个基本逻辑是,函数首先在引用 class 中查找,如果它是虚拟的,那么函数将在派生的 classes 中搜索(如果被引用的 object 是 child class)。如果派生 classes 重写,那么将调用派生方法,否则将调用基方法。您可以进一步将应用于基础 class 的概念扩展到派生的 class 以及检查函数是否为虚函数以及函数是否为虚函数以及 object是派生的(派生的child,基的grandchild)等等。

希望这能说明问题。

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

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

考虑这个例子,

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

bp->g()执行时,会调用bg函数。