派生类中的函数的C ++“虚拟”关键字。有必要吗?


221

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

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

方法1:

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

方法2:

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

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


65
在C ++ 11中,您可以编写“ void hello()overlay {}”来显式声明您在重写虚拟方法。如果基本的虚拟方法不存在,则编译器将失败,并且其可读性与在子类上放置“虚拟”时具有相同的可读性。
ShadowChaser

实际上,在gcc的C ++ 11中,在派生类中编写void hello()覆盖{}很好,因为基类已指定方法hello()是虚拟的。换句话说,无论如何,对于gcc / g ++ ,在派生类中不必使用单词virtual 。(我在RPi 3上使用的是gcc 4.9.2版)但是,无论如何,还是要在派生类的方法中包括关键字virtual,这是一个好习惯。
威尔

Answers:


183

他们是完全一样的。它们之间没有什么区别,只是第一种方法需要更多的键入并且可能更清晰。


25
的确如此,但是Mozilla C ++可移植性指南建议始终使用虚拟,因为如果不这样做,“某些编译器”会发出警告。不幸的是,他们没有提到此类编译器的任何示例。
Sergei Tachenov 2011年

5
我还要补充一点,将其显式标记为虚拟将有助于提醒您也将析构函数也设为虚拟。
lfalin 2014年

1
仅提及,同样适用于虚拟析构函数
Atul 2015年

6
@根据SergeyTachenov自己的回答的评论,此类编译器的示例是armcc。
Ruslan

4
@Rasmi,新的可移植性指南在此处,但现在建议使用override关键字。
Sergei Tachenov

83

函数的“虚拟性”是隐式传播的,但是,如果virtual未显式使用关键字,则我使用的至少一个编译器将生成警告,因此,如果仅使编译器保持安静状态,则可能要使用它。

从纯粹的风格角度来看,包括virtual关键字在内显然向用户“宣传”了该功能是虚拟的事实。这对于任何进一步细分B的人都非常重要,而不必检查A的定义。对于较深的类层次结构,这变得尤为重要。


12
这是哪个编译器?
James McNellis 2011年

35
@James:armcc(ARM设备的ARM编译器)
Clifford

55

virtual关键字是不是在派生类中必要的。以下是C ++草案标准(N3337)(重点是我的)的支持文档:

10.3虚函数

2如果虚拟成员函数vf是在类中声明Base和一个类Derived,直接或间接地来源于Base,成员函数vf具有相同的名称,参数类型列表(8.3.5),CV-资格,和ref-限定符(或没有相同)Base::vf,则Derived::vf它也是虚的(无论是否如此声明),并且它会覆盖Base::vf


5
这是到目前为止最好的答案。
很棒的狐狸先生

33

不,不需要virtual派生类的虚函数重写上的关键字。但是值得一提的是相关的陷阱:无法覆盖虚拟函数。

未能覆盖,如果你打算在派生类中重写一个虚函数,但做出一个错误的签名,以便它声明了一个新的和不同的虚函数发生。该函数可能是基类函数的重载,或者名称可能有所不同。无论您是否virtual在派生类函数声明中使用关键字,编译器都将无法告诉您您打算重写基类中的函数。

但是,值得庆幸的是,C ++ 11 显式覆盖语言功能解决了该陷阱,该功能允许源代码明确指定成员函数旨在覆盖基类函数:

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中的函数应该以a float作为参数)。

请参考WP:C ++ 11


11

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


7

当您编写 virtual在派生类中或忽略它。

但是您需要查看基类以获取此信息。因此virtual,如果您想向人类展示该函数是虚拟的,那么我建议在派生类中也添加关键字。


2

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

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()。进行这种重写的通常原因是从基于继承的专业转向基于模板的专业,主要是出于性能方面的考虑。如果您继续支持旧界面,则几乎无法检测(或阻止)旧用法。


1

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,在涉及继承的所有类中的函数之前都应在函数的前面,除非您确定该类不会有需要重写基类的函数的子代。这是一个好习惯。


0

我一定会为子类添加Virtual关键字,因为

  • 一世。可读性。
  • ii。我可以进一步派生该子类,您不希望该进一步派生类的构造函数调用此虚函数。

1
我认为他的意思是,如果不将子功能标记为虚拟,那么后来从子类派生的程序员可能不会意识到该功能实际上是虚拟的(因为他从未看过基类),并且可能在构造过程中调用它(这可能会或可能不会做正确的事情)。
PfhorSlayer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.