重写虚函数和继承
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::g
和B::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->g()
执行时,b
的g
函数将被调用。
【讨论】:
我明白了。因此,当我调用一个方法时,比如说 call(d),它只能看到 B 的成员,因为毕竟这就是参数所指的。我想我明白了。谢谢。 @bruneleski 是的,这是正确的。除非在您的示例中使用了 virtual 关键字,例如您的操作方式。virtual void f()
的 b
类。如果 virtual void f()
也被覆盖,就像你的 d
正在做的那样,如果 d
作为对调用的引用传递( call(d);
)。那么引用参数const B& Ref
中的虚拟表指针的入口将指向d
的void f() const
。所以每当你打电话给Ref.f()
时,它将是d
的virtual void f()
。这方面的术语称为运行时多态性或动态多态性以上是关于重写虚函数和继承的主要内容,如果未能解决你的问题,请参考以下文章