覆盖虚拟成员函数时,为什么覆盖函数总是变为虚拟?

时间:2009-12-18 16:19:12

标签: c++ inheritance virtual override

当我写这样的时候:

class A {
    public: virtual void foo() = 0;
}

class B {
    public: void foo() {}
}

... B :: foo()也变为虚拟。这背后的理由是什么?我希望它的行为类似于Java中的final关键字。

添加:我知道这样的工作方式和vtable如何工作:)问题是,为什么C ++标准委员会没有留下直接调用B :: foo()并避免vtable查找的开头。

6 个答案:

答案 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)

我不知道其他编译器是否也这样做。