C ++“virtual”关键字,用于派生类中的函数。有必要吗?

时间:2011-02-04 06:46:58

标签: c++ override virtual-functions

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

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

方法#1:

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

方法#2:

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

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

9 个答案:

答案 0 :(得分:164)

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

答案 1 :(得分:80)

函数的“虚拟性”是隐式传播的,但是如果未明确使用virtual关键字,我使用的至少一个编译器会生成警告,因此您可能只想保留编译器安静。

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

答案 2 :(得分:47)

派生类中不需要virtual关键字。以下是来自C ++ Draft Standard(N3337)(强调我的)的支持文档:

  

10.3虚拟功能

     

2如果在类vf和类Base中声明虚拟成员函数Derived,直接或间接从Base派生,则成员函数{{ 1}}具有相同的名称,声明参数类型列表(8.3.5),cv-qualification和ref-qualifier(或缺少相同)vf,然后Base::vf也是虚拟(是否如此声明)并覆盖Derived::vf

答案 3 :(得分:31)

不,不需要派生类的虚函数覆盖上的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中的函数应该以{{1​​}}作为参数。)

请参阅WP:C++11

答案 4 :(得分:10)

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

答案 5 :(得分:7)

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

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

答案 6 :(得分:1)

当您拥有模板并开始将基类作为模板参数时,存在相当大的差异:

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);
}

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

请注意,如果这样做,您可能还希望将复制/移动构造函数声明为模板:允许从不同的接口构造允许您在不同的B<>类型之间“转换”。

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

答案 7 :(得分:0)

我肯定会为子类包含Virtual关键字,因为

  • 我。可读性。
  • II。这个子类可以进一步向下导出,你不希望进一步派生类的构造函数调用这个虚函数。

答案 8 :(得分:0)

应将def greet(): print('Hi there mister') kor_Var = input('Enter kor') if kor_Var != "walls": print('Incorrect pin') else: greet() 关键字添加到基类的函数中,以使其可重写。在您的示例中,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放在涉及继承的所有类的函数前面,除非您确定该类不会有任何需要重写基类函数的子级。这是个好习惯。