假设我在3个A,B和C类中有一个函数print()。 C继承自B继承自A. 关键字virtual仅用于A。
为什么以下两个都使用C?中的print()?
A* ac = new C();
ac->print(); //C's print()
B* bc = new C();
bc->print(); //C's print(), not B's print() even though virtual is not used.
这里的直觉是什么?
如果你想编译/运行它,请在下面填写完整的代码:
#include <iostream>
#include <cstdlib>
using namespace std;
class A{
public:
A(){
cout << "construct A" << endl;
}
virtual void print(){
cout << "A says" << endl;
}
};
class B: public A{
public:
B(){
cout << "construct B" << endl;
}
void print(){
cout << "B says" << endl;
}
};
class C: public B{
public:
C(){
cout << "construct C" << endl;
}
void print(){
cout << "C says" << endl;
}
void print(int x){
cout << "C says " << x << endl;
}
};
int main(){
A* ac = new C();
ac->print();
B* bc = new C();
bc->print();
return 0;
}
答案 0 :(得分:4)
因为这就是C ++的工作方式 - 一旦具有特定签名的函数被标记为virtual
,它在每个派生类中仍然是virtual
,无论它们是否显式使用该关键字。我的偏好是总是使用额外的(冗余的)virtual
来明确发生了什么,但是其他人认为不需要这样做,所以你也可以把它留下来。
(但是,带有不同签名的重载不会自动为虚拟,实际上会隐藏同名的基类方法,除非使用using
指令将基类定义带入派生范围。)
至于推理,我认为没有任何特定的语言理由禁止你“去虚拟化”一个功能。我猜Stroustrup在早期决定宽恕人们忘记派生类中的virtual
,并因此得到意外行为。
答案 1 :(得分:1)
语言可以以任何方式工作 - 这是Stroustrup做出的选择:
按原样,可以轻松覆盖各种推导深度的选择函数,而无需重新列出每个中间类中的所有未重写函数。这可以减少代码演变时的维护负担,当您查看派生类时,它会强调实际更改或添加的内容更少。
如果它看起来像您期望的那样工作,并且您必须在B
中再次显式声明虚函数,那么编译器会期望实现,并且链接器将会失败,因此一些新的表示法需要表明你只是保留了进一步派生类重写函数但不打算替换基类定义的能力。这听起来有点乏味和重复。
默认持续覆盖虚拟功能的能力有助于重复使用,但对于那些相对罕见的情况,当存在某些边界时,例如公司库API,其中允许一些重写,但在哪些修改被故意阻碍之外,然后在C ++ 11中有一个final
关键字,表示不允许派生类覆盖该函数:参见http://en.cppreference.com/w/cpp/language/final
答案 2 :(得分:0)
必须。它是基类表达的合同的一部分。如果它没有传播到派生类,那将违反OO的原则,例如Liskov替换原则。