代码之家  ›  专栏  ›  技术社区  ›  Johan Kotlinski

当重写虚拟成员函数时,为什么重写函数总是变成虚拟的?

  •  4
  • Johan Kotlinski  · 技术社区  · 15 年前

    当我这样写的时候:

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

    …B::foo()也变为虚拟的。这背后的理由是什么?我希望它表现得像 final 关键字在Java中。

    Ad:我知道这样的工作方式和VTALE是如何工作的:问题是,为什么C++标准委员会没有留下一个直接调用B::FoE()的机会,并且避免了VTABLE查找。

    6 回复  |  直到 10 年前
        1
  •  9
  •   Steve Jessop    15 年前

    标准没有留下直接调用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(); }
    };
    

    现在打电话的人应该用酒吧代替foo。如果他们有一个c*,那么他们可以将其强制转换为a*,在这种情况下 bar 将呼叫 C::foo 或者他们可以将其强制转换为b*,在这种情况下 酒吧 将呼叫 B::foo . 如果需要,C可以再次覆盖BAR,否则不麻烦,在这种情况下调用 bar() 关于C*调用 B::foo() 如你所料。

    不过,我不知道什么时候会有人想要这种行为。虚拟函数的关键是为给定的对象调用相同的函数,不管您使用的是什么基或派生类指针。因此,C++假定如果通过基类对特定成员函数的调用是虚的,那么通过派生类调用也应该是虚的。

        2
  •  6
  •   Mehrdad Afshari    15 年前

    当你宣布 virtual 方法,基本上是在vtable中添加一个新条目。重写一个 事实上的 方法更改该项的值;它不会将其移除。这基本上适用于Java或C语言等语言。区别在于, final 在爪哇中,你可以任意地请求编译器。 执行 无法覆盖它。C++不提供这种语言特征。

        3
  •  5
  •   Mark Ransom    15 年前

    仅仅因为类被强制使用vtable,并不意味着编译器被强制使用它。如果对象的类型是静态的,编译器可以作为优化绕过vtable。例如,在这种情况下,可能会直接调用b::foo:

    B b;
    b.foo();
    

    不幸的是,我知道验证这一点的唯一方法是查看生成的程序集代码。

        4
  •  2
  •   Michael Krelin - hacker    15 年前

    因为从技术上讲,无论你做什么,它都是虚拟的,它在表格中占有一席之地。其余的将是一个语法执法,这是C++与Java不同的地方。

        5
  •  1
  •   William Bell    15 年前

    定义第一个虚拟函数时,将为基类创建vtable。在示例中,foo()在vtable中有一个条目。当派生类从基类继承时,它也继承vtable。派生类在其vtable中必须有一个foo()的条目,这样当派生类通过基类指针以多态方式引用时,调用将被适当地重定向。

        6
  •  0
  •   zvone    10 年前

    似乎至少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)
    

    鉴于没有 最终的 它使用查找表:

            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)
    

    不过,我不知道其他编译器是否也这么做。