首页 文章

派生类中函数的C“virtual”关键字 . 有必要吗?

提问于
浏览
184

使用下面给出的结构定义......

struct A {
    virtual void hello() = 0;
};

方法#1:

struct B : public A {
    virtual void hello() { ... }
};

方法#2:

struct B : public A {
    void hello() { ... }
};

这两种覆盖hello函数的方法有什么区别吗?

9 回答

  • 1

    我肯定会为子类包含Virtual关键字,因为我 . 可读性 . II . 这个子类可以进一步向下导出,你不希望进一步派生类的构造函数调用这个虚函数 .

  • 10

    它们完全一样 . 除了第一种方法需要更多打字并且可能更清楚之外,它们之间没有区别 .

  • 153

    函数的'virtualness'是隐式传播的,但是如果没有显式使用 virtual 关键字,我使用的至少一个编译器会生成警告,所以如果只是为了保持编译器的安静,你可能想要使用它 .

    从纯粹的风格角度来看,包括 virtual 关键字明确地告诉用户该功能是虚拟的 . 这对于任何进一步细分B而不必检查A的定义的人来说都很重要 . 对于深层次的层次结构,这变得尤为重要 .

  • 72

    派生类中不需要 virtual 关键字 . 以下是C草案标准(N3337)(强调我的)的支持文档:

    10.3虚函数2如果虚拟成员函数vf在类Base和Derived中直接或间接派生的类中声明,则成员函数vf具有相同的名称,参数类型列表(8.3.5),声明了base :: vf的cv-qualification和ref-qualifier(或不存在相同的),然后Derived :: vf也是虚拟的(无论是否声明)并且它覆盖Base :: vf .

  • 0

    不,不需要派生类的虚函数覆盖上的 virtual 关键字 . 但值得一提的是一个相关的陷阱:未能覆盖虚函数 .

    如果您打算覆盖派生类中的虚函数,则会发生覆盖失败,但会在签名中出错,以便声明一个新的不同的虚函数 . 此函数可能是基类函数的重载,或者名称可能不同 . 无论您是否在派生类函数声明中使用 virtual 关键字,编译器都无法告诉您打算从基类重写函数 .

    然而,幸运的是,C 11 explicit override语言功能解决了这个缺陷,该功能允许源代码清楚地指定成员函数旨在覆盖基类函数:

    struct Base {
        virtual void some_func(float);
    };
    
    struct Derived : Base {
        virtual void some_func(int) override; // ill-formed - doesn't override a base class method
    };
    

    编译器将发出编译时错误,编程错误将立即显而易见(可能Derived中的函数应该以 float 作为参数) .

    请参阅WP:C++11 .

  • 7

    添加“虚拟”关键字是一种很好的做法,因为它提高了可读性,但并不是必需的 . 在基类中声明为虚拟的函数,并且在派生类中具有相同的签名,默认情况下被视为“虚拟” .

  • 38

    当您在派生类中编写 virtual 或省略它时,编译器没有区别 .

    但是你需要查看基类来获取这些信息 . 因此,我建议在派生类中添加 virtual 关键字,如果您想向人类显示此函数是虚拟的 .

  • 0

    当你有模板并开始将基类作为模板参数时,会有很大的不同:

    struct None {};
    
    template<typename... Interfaces>
    struct B : public Interfaces
    {
        void hello() { ... }
    };
    
    struct A {
        virtual void hello() = 0;
    };
    
    template<typename... Interfaces>
    void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
    {
        b.hello();   // indirect, non-virtual call
    }
    
    void hello(const A& a)
    {
        a.hello();   // Indirect virtual call, inlining is impossible in general
    }
    
    int main()
    {
        B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
        B<None>* pb = &b;
        B<None>& rb = b;
    
        b.hello();          // direct call
        pb->hello();        // pb-relative non-virtual call (1 redirection)
        rb->hello();        // non-virtual call (1 redirection unless optimized out)
        t_hello(b);         // works as expected, one redirection
        // hello(b);        // compile-time error
    
    
        B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
        B<None>* pba = &ba;
        B<None>& rba = ba;
    
        ba.hello();         // still can be a direct call, exact type of ba is deducible
        pba->hello();       // pba-relative virtual call (usually 3 redirections)
        rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
        //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
        hello(ba);
    }
    

    有趣的是,您现在可以在以后定义接口和非接口函数来定义类 . 这对于库之间的交互接口很有用(不要将其作为单个库的标准设计过程) . 你所有的课程都不需要花费任何费用 - 如果你愿意的话,你甚至可能会这样做 .

    请注意,如果执行此操作,您可能还希望将复制/移动构造函数声明为模板:允许从不同的接口构造允许您在不同的 B<> 类型之间进行'cast' .

    是否应该在 t_hello() 中添加对 const A& 的支持是值得怀疑的 . 这种重写的通常原因是从基于继承的专业化转向基于模板的专业化,主要是出于性能原因 . 如果您继续支持旧界面,则几乎无法检测(或阻止)旧用法 .

  • 29

    应将 virtual 关键字添加到基类的函数中,以使它们可以覆盖 . 在您的示例中, struct A 是基类 . virtual 对于在派生类中使用这些函数没有任何意义 . 但是,您希望派生类本身也是基类,并且您希望该函数可以覆盖,那么您必须将 virtual 放在那里 .

    struct B : public A {
        virtual void hello() { ... }
    };
    
    struct C : public B {
        void hello() { ... }
    };
    

    这里 C 继承自 B ,所以 B 不是基类(它也是派生类),而 C 是派生类 . 继承图如下所示:

    A
    ^
    |
    B
    ^
    |
    C
    

    所以你应该将 virtual 放在可能有子节点的潜在基类中的函数前面 . virtual 允许您的孩子覆盖您的功能 . 将 virtual 放在派生类中的函数前面没有任何问题,但这不是必需的 . 但是建议这样做,因为如果有人想继承你的派生类,他们就不会对方法覆盖不能按预期工作感到高兴 .

    因此,将 virtual 放在继承所涉及的所有类中的函数前面,除非您确定该类不会有任何需要覆盖基类函数的子代 . 这是一个很好的做法 .

相关问题