覆盖虚拟函数和继承

时间:2018-10-11 20:47:16

标签: c++ inheritance override virtual-functions

我无法完全理解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)时没有任何作用-输出{ {1}}。

接下来,我们执行B::g,它输出call(dd)。应用与上述相同的逻辑(?),很显然D::f B::g不会覆盖DD::f(不是const),并且D::f不会覆盖DD::g或{ {1}},因为两者都不是D::g

接下来发生的事情使我感到困惑。 B::gvirtualb.f()b.g()d.f()d.g()的每个单独调用都会输出结果,好像根本没有覆盖! 例如,几秒钟前dd.f()在call()中输出dd.g()时,d.g()如何输出D::g? 另外,d.g()B::g的{​​{1}}输出dd.f()时如何输出DD::f

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

2 个答案:

答案 0 :(得分:3)

请耐心阅读,以获取对您问题的答案。
继承是一个概念,其中类的对象继承了另一类对象的属性和行为。

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

例如

#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;
}
Output:
Base display called
Derived display called
Base display called

现在,从上面的示例中,您可能已经猜想派生类将覆盖基类,但事实并非如此。
输出(第三行)显示已调用基类函数。

  • 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;
}
Output:
Base display called
Derived display called
Derived display called

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

  • 将函数虚拟化有什么作用?

正常函数和虚函数之间的区别在于,正常函数是在编译时解析的,也称为静态绑定,而虚函数是在运行时解析的,也称为动态绑定或后期绑定。由于基类显示功能是虚拟的,因此在运行时可以解析要调用的方法(基类显示或派生类显示方法)。
您可以通过阅读有关v-table的文章来深入了解虚拟函数机制。


回答问题
1)致电(d)。我不太清楚为什么,但是似乎call()可以将B的派生类作为参数。
=>派生类对象可以由基类引用
2)但是D :: g不会覆盖B :: g,由于我无法理解的原因
=>因为g()在基类中不是虚拟的。只能覆盖虚拟功能。 v-table的条目仅包含虚拟功能,因此可以在运行时覆盖它们。
3)致电(dd) =>调用f()时,首先在基类B中搜索该函数, 由于f()是虚拟的,因此使用v表指针,可解决派生类D中重写的f()函数。但是由于f()在类D中不是虚拟的,因此无法解析DD中重写的f()。因此D :: f被打印。
但是对于g(),基类本身没有虚拟g(),因此无法解析其派生类中的重写函数。因此,B :: g被打印。
4)发生上述多态性是因为派生类被其基类(父类)引用,但是在上一次调用中没有这样的事情。所有对象均由其各自的类引用,因此将调用其适当的方法。


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

答案 1 :(得分:0)

例如,几秒钟前d.g()在call()中的d.g()输出B :: g时,d.g()如何输出D :: g?

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

考虑此示例

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

执行bp->g()时,将调用g的{​​{1}}函数。