我无法完全理解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::g
,virtual
,b.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
?
可以肯定地说我在这里缺少一些重要的东西,为此我需要帮助。
答案 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;
}
#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
现在,从上面的示例中,您可能已经猜想派生类将覆盖基类,但事实并非如此。
输出(第三行)显示已调用基类函数。
您可以通过在函数的开头添加“ 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}}函数。