C++中派生类函数的"virtual"关键字,是否必要?

268

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

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

方法一:

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

方法二:

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

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


74
在C++11中,你可以写 "void hello() override {}" 明确声明你正在覆盖一个虚方法。如果基类的虚方法不存在,编译器将会失败,并且使用这种方式与在派生类上放置 "virtual" 具有相同的可读性。 - ShadowChaser
实际上,在gcc的C++11中,写在派生类中的void hello() override {}是可以的,因为基类已经指定了hello()方法是虚拟的。换句话说,在派生类中使用单词virtual并不是必要的/强制性的,对于gcc/g++来说是这样的。(我正在使用RPi 3上的gcc版本4.9.2)但是在派生类的方法中包含关键字virtual是一个好习惯。 - Will
9个回答

235

它们完全相同。除了第一种方法需要更多的打字并可能更清晰之外,它们没有任何区别。


32
这是正确的,但是Mozilla的C++可移植性指南建议始终使用虚拟函数,因为“某些编译器”会在不这样做时发出警告。遗憾的是,他们没有提及任何此类编译器的示例。 - Sergei Tachenov
6
我想补充一点,明确将其标记为虚拟的也有助于提醒您将析构函数也设为虚拟的。 - lfalin
1
仅提一下,同样适用于虚析构函数。 - Atul
6
根据Clifford在他自己的答案评论中提到的,这种编译器的例子是armcc。 - Ruslan
6
@Rasmi,新的可移植性指南已经发布,但现在它建议使用override关键字。 - Sergei Tachenov
显示剩余4条评论

98

函数的“虚拟性”是自动传播的,然而我使用的至少一种编译器会生成警告,如果没有显式地使用virtual关键字,所以你可能想使用它,即使只是为了让编译器保持安静。

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


35
@James:armcc是ARM设备上的编译器。 - Clifford
在C++和C#中,派生类中的重写函数通常被默认认为是虚函数。因此,关键字virtual是不必要的。即使在深层次的类层次结构中,这一点也是非常明确的。 - undefined
@SecondPersonShooter:这难道不正是这个答案已经说过的吗?你所提出的观点不是已经被提及了吗?当然,对编译器来说是清楚的,但对人类读者来说却不清楚,这也已经明确说明了。 - undefined

74
virtual 关键字在派生类中不是必需的。以下是来自C++草案标准(N3337)的支持文件(强调我的部分):
“10.3 虚函数 如果在一个类Base中和一个直接或间接从Base派生的类Derived中声明了虚成员函数vf,则具有与Base::vf相同的名称、参数类型列表(8.3.5)、CV限定符和ref-qualifier(或其缺少)的成员函数vf被声明为,那么Derived::vf也是虚拟的(无论是否这样声明),它会覆盖Base :: vf。”

6
这绝对是目前为止这里最好的回答。 - Fantastic Mr Fox

43
不,派生类的虚函数重载上的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
};

编译器将会在编译时发出错误提示,程序中的错误很快就会显现出来(也许派生类中的函数应该以float作为参数)。请参考WP:C++11

14

添加 "virtual" 关键字是一个好习惯,因为它提高了可读性,但并不是必需的。在基类中声明为虚函数,并且在派生类中具有相同签名的函数默认被认为是 "虚拟 "的。


8

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

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


4

为了使基类函数可以被覆盖,应该在其前面添加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

因此,你应该在可能有子类的潜在基类中,在函数前面加上virtualvirtual允许子类覆盖你的函数。在派生类中的函数前面放置virtual没有任何问题,但并不需要这样做。尽管如此,建议这样做,因为如果有人想要从你的派生类继承,他们会不满意方法重写不像预期那样工作。

因此,在涉及继承的所有类中,在函数前面放置virtual,除非你确定该类不会有任何需要覆盖基类函数的子类。这是一种良好的实践。


3

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

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重新定义为某些内容。
请注意,如果这样做,您可能还想将复制/移动构造函数声明为模板:允许从不同接口构造允许在不同的B类型之间进行“转换”。
是否应该在t_hello()中添加对const A&的支持是值得商榷的。进行此重写的通常原因是为了从基于继承的专业化转向基于模板的专业化,主要是出于性能原因。如果继续支持旧接口,则很难检测到(或阻止)旧用法。

0

我一定会在子类中包含Virtual关键字,因为

  • i. 可读性。
  • ii. 这个子类可能会被进一步派生,你不希望更深层次的派生类的构造函数调用这个虚函数。

1
我认为他的意思是,如果不将子函数标记为虚函数,那么后来从子类派生的程序员可能不会意识到该函数实际上是虚函数(因为他从未查看过基类),并且在构造期间可能会潜在地调用它(这可能会或可能不会做正确的事情)。 - PfhorSlayer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接