当我写这样的时候:
class A {
public: virtual void foo() = 0;
}
class B {
public: void foo() {}
}
... B :: foo()也变为虚拟。这背后的理由是什么?我希望它的行为类似于Java中的final
关键字。
添加:我知道这样的工作方式和vtable如何工作:)问题是,为什么C ++标准委员会没有留下直接调用B :: foo()并避免vtable查找的开头。
答案 0 :(得分:9)
该标准确实留下了一个直接调用B :: foo并打开表查找的开头:
#include <iostream>
class A {
public: virtual void foo() = 0;
};
class B : public A {
public: void foo() {
std::cout <<"B::foo\n";
}
};
class C : public B {
public: void foo() {
std::cout <<"C::foo\n";
}
};
int main() {
C c;
A *ap = &c;
// virtual call to foo
ap->foo();
// virtual call to foo
static_cast<B*>(ap)->foo();
// non-virtual call to B::foo
static_cast<B*>(ap)->B::foo();
}
输出:
C::foo
C::foo
B::foo
因此,您可以按照以下方式获得您所期望的行为:
class A {
virtual void foo() = 0;
// makes a virtual call to foo
public: void bar() { foo(); }
};
class B : public A {
void foo() {
std::cout <<"B::foo\n";
}
// makes a non-virtual call to B::foo
public: void bar() { B::foo(); }
};
现在来电者应该使用bar而不是foo。如果他们有C *,那么他们可以将其转换为A *,在这种情况下bar
会调用C::foo
,或者他们可以将其转换为B *,在这种情况下bar
将致电B::foo
。 C可以在需要时再次覆盖栏,否则不会打扰,在这种情况下,在C *上调用bar()
会调用B::foo()
,如您所料。
但我不知道何时会有人想要这种行为。虚函数的重点是为给定对象调用相同的函数,无论您使用的是什么基类或派生类指针。因此,C ++假设如果通过基类对特定成员函数的调用是虚拟的,那么通过派生类的调用也应该是虚拟的。
答案 1 :(得分:6)
当您声明virtual
方法时,您基本上是在vtable中添加新条目。覆盖virtual
方法会更改该条目的值;它不会删除它。对于像Java或C#这样的语言来说,这基本上也是如此。不同之处在于,使用Java中的final
关键字,您可以要求编译器任意强制无法覆盖它。 C ++不提供此语言功能。
答案 2 :(得分:5)
仅仅因为该类被强制使用vtable,并不意味着编译器被迫使用它。如果对象的类型是静态已知的,则编译器可以自由地绕过vtable作为优化。例如,在这种情况下可能会直接调用B :: foo:
B b;
b.foo();
不幸的是,我知道验证这一点的唯一方法是查看生成的汇编代码。
答案 3 :(得分:2)
因为从技术上讲它无论你做什么都是虚拟的 - 它在表格中占有一席之地。其余的将是一个语法执法,这是C ++与java不同的地方。
答案 4 :(得分:1)
在定义第一个虚函数时,为基类创建一个vtable。在您的示例中,foo()在vtable中有一个条目。当派生类继承自基类时,它也继承了vtable。派生类必须在其vtable中具有foo()的条目,以便在通过基类指针以多态方式引用派生类时,将适当地重定向调用。
答案 5 :(得分:0)
似乎至少Visual Studio能够利用final
关键字来跳过vtable查找,例如这段代码:
class A {
public:
virtual void foo() = 0;
};
class B : public A {
public:
void foo() final {}
};
B original;
B& b = original;
b.foo();
b.B::foo();
为b.foo()
和b.B::foo()
生成相同的代码:
b.foo();
000000013F233AA9 mov rcx,qword ptr [b]
000000013F233AAE call B::foo (013F1B4F48h)
b.B::foo();
000000013F233AB3 mov rcx,qword ptr [b]
000000013F233AB8 call B::foo (013F1B4F48h)
而没有final
它使用查找表:
b.foo();
000000013F893AA9 mov rax,qword ptr [b]
000000013F893AAE mov rax,qword ptr [rax]
000000013F893AB1 mov rcx,qword ptr [b]
000000013F893AB6 call qword ptr [rax]
b.B::foo();
000000013F893AB8 mov rcx,qword ptr [b]
000000013F893ABD call B::foo (013F814F48h)
我不知道其他编译器是否也这样做。