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

Posted

技术标签:

【中文标题】派生类中函数的 C++“虚拟”关键字。有必要吗?【英文标题】:C++ "virtual" keyword for functions in derived classes. Is it necessary? 【发布时间】:2011-02-04 06:46:57 【问题描述】:

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

struct A 
    virtual void hello() = 0;
;

方法一:

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

方法 #2:

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

这两种重写hello函数的方式有什么区别吗?

【问题讨论】:

在 C++11 中,您可以编写“void hello() override ”来明确声明您正在覆盖虚拟方法。如果基本虚拟方法不存在,编译器将失败,并且它具有与将“虚拟”放在后代类上相同的可读性。 其实在gcc的C++11中,在派生类中写void hello() override 是可以的,因为基类已经指定方法hello()是虚的。换句话说,对于 gcc/g++ 来说,在 derived 类中使用 virtual 这个词并不是必须的/强制的。 (我在 RPi 3 上使用 gcc 版本 4.9.2)但无论如何最好在派生类的方法中包含关键字 virtual。 【参考方案1】:

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

【讨论】:

这是真的,但the Mozilla C++ Portability Guide 建议始终使用虚拟,因为如果您不这样做,“某些编译器”会发出警告。太糟糕了,他们没有提到任何此类编译器的示例。 我还要补充一点,明确将其标记为虚拟将有助于提醒您也将析构函数设为虚拟。 仅提一下,同样适用于virtual destructor @SergeyTachenov 根据 clifford 对his own answer 的评论,此类编译器的示例是 armcc。 @Rasmi, the new portability guide is here,但现在建议使用override 关键字。【参考方案2】:

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

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

【讨论】:

@James: armcc(用于 ARM 设备的 ARM 编译器)【参考方案3】:

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

10.3 虚函数

2 如果一个虚成员函数vf在类Base和类Derived中声明,直接或间接派生自Base,则成员函数vf同名,参数- type-list (8.3.5), cv-qualification, 和 ref-qualifier (或没有相同) 如Base::vf 被声明,然后Derived::vf 也是虚拟的(不管它是否被声明) 并覆盖Base::vf

【讨论】:

这是迄今为止最好的答案。【参考方案4】:

不,派生类的虚函数覆盖上的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。

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

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

【讨论】:

【参考方案7】:

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放在所有涉及继承的类的函数前面,除非你确定该类不会有任何需要覆盖基类函数的子类。这是一个很好的做法。

【讨论】:

【参考方案8】:

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

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&lt;&gt; 类型之间“转换”。

是否应该在t_hello() 中添加对const A&amp; 的支持是有问题的。这种重写的通常原因是从基于继承的专业化转向基于模板的专业化,主要是出于性能原因。如果您继续支持旧接口,则几乎无法检测(或阻止)旧的使用。

【讨论】:

【参考方案9】:

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

i.可读性。 二。这个子类可能会进一步派生,你不希望进一步派生类的构造函数调用这个虚函数。

【讨论】:

我认为他的意思是如果不将子函数标记为虚拟,以后从子类派生的程序员可能不会意识到该函数实际上是虚拟的(因为他从未看过基类)并且可能在施工期间调用它(这可能会或可能不会做正确的事情)。

以上是关于派生类中函数的 C++“虚拟”关键字。有必要吗?的主要内容,如果未能解决你的问题,请参考以下文章

通过“函数参数”中的 const 类型在派生类中具有不同函数参数的虚拟函数会破坏虚拟机制吗? [复制]

派生类可以有一个不在 C++ 基类中的构造函数吗?

用C++定义一个人员类

用C++定义一个人员类

派生类中的静态方法可以在 C++ 中调用受保护的构造函数吗?

virtual虚函数