c++“virtual"派生类中函数的关键字。有必要吗?

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

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

方法# 1:

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

方法# 2:

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

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

94649 次浏览

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

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

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

函数的“虚拟性”是隐式传播的,但是如果没有显式使用virtual关键字,我使用的至少一个编译器会生成警告,所以如果只是为了让编译器保持安静,您可能会使用它。

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

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

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

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

virtual关键字在派生类中不是必需的。下面是c++标准草案(N3337)的支持文档(强调我):

10.3虚函数

如果在类Base和类Derived中声明了一个虚成员函数vf,直接或间接地从Base派生,声明了一个与Base::vf同名、形参类型-list(8.3.5)、cv-qualification和ref-qualifier(或没有相同)的成员函数vf,则Derived::vf也是虚函数(无论是否如此声明),并且它覆盖Base::vf

不,派生类的虚函数重写上的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中的函数应该以float作为参数)。

参考WP: c++ 11

当你有模板并开始将基类作为模板参数时,有一个相当大的区别:

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<>类型之间进行“强制转换”。

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

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