什么时候使用虚方法比较合适?

21

我了解到,虚方法允许派生类覆盖从基类继承的方法。何时使用虚方法是恰当/不恰当的?并不总是知道一个类是否会被子类化。是否应该将所有内容都设为虚拟的,只是“以防万一”?还是那样会引起重大开销?

10个回答

10

首先,稍微有点学究气的是在 C++ 的标准中,我们称之为成员函数(member functions),而不是方法(methods),尽管这两个术语是等效的。

我认为有两个原因不应该将成员函数声明为虚函数。

  • “YAGNI” - “你不会需要它”。如果你不确定一个类是否会被继承,那么假设它不会被继承,不要将成员函数声明为虚函数。类中非虚析构函数传达了“不要从我派生”的意思,顺带一提(编辑:在 C++11 及以上版本中,你可以使用 final 关键字,这更好)。同时,这也涉及到意图。如果你没有打算通过多态使用该类,则不要将任何内容声明为虚函数。如果随意将成员函数声明为虚函数,就会引发违反 里氏替换原则 的问题,而这类错误很难跟踪和解决。
  • 性能 / 内存占用。一个没有虚成员函数的类不需要 VTable(虚表,用于通过基类指针重定向多态调用),因此(潜在地)在内存中占用更少的空间。并且,普通成员函数调用(潜在地)比虚成员函数调用更快。不要过早地将成员函数声明为虚函数而让类变得低效。

7
当你设计一个类时,你应该有一个很好的想法,它是代表一个接口(在这种情况下,你标记适当的可重写方法和析构函数为虚拟)还是旨在被直接使用,可能与其他对象进行组合。
换句话说,你对类的意图应该是你的指导。把所有东西都变成虚拟的通常是过度的,并且有时会误导哪些方法旨在支持运行时多态性。

5
这是一个棘手的问题。但是有一些准则/经验法则需要遵循。
  1. 只要不需要从类派生,就不要编写任何虚方法;一旦需要派生,只需将子类中需要自定义的方法设置为virtual。
  2. 如果一个类有一个虚方法,则析构函数应该是virtual(没有讨论的余地)。
  3. 尝试遵循NVI(非虚拟接口)习惯,使虚方法非公共,并提供公共包装器来负责访问前后条件,以便派生类不能意外破坏它们。
我认为这些都足够简单了。我肯定会忽略反射的ABI部分,只有在交付DLL时才有用。

3
如果你的代码遵循特定的设计模式,那么你的选择应该反映DP的原则。例如,如果你在编写 装饰者模式,应该是属于组件接口的函数才应该是虚拟函数。
否则,我希望采用演化方法,也就是说,除非看到你的代码尝试从层次结构中出现,否则我不会使用virtual方法。

3
例如,在Java中的成员函数都是100%虚拟的。而在C++中,这被认为是代码大小/函数调用时间的惩罚。此外,非虚拟函数保证函数实现始终相同(使用基类对象/引用)。Scott Meyers在《Effective C++》一书中更详细地讨论了这个问题。

1
非虚函数“保持不变”的事实对于调用它的其他函数非常重要。当涉及到虚函数时,要正确获取调用函数就更加困难了。 - Bo Persson

2
我通常使用的一个健全性测试是——如果我定义的类在未来被派生,该行为(函数)是否会保持不变或者需要重新定义。如果需要重新定义,那么这个函数很可能适合作为虚函数;否则,不需要。如果我不知道,我可能需要深入了解问题领域以更好地理解我计划实现的行为。大多数情况下,问题领域会给我答案——在它无法给出答案的情况下,该行为通常是非关键的。

1

我猜,快速确定的一种可能方式是考虑你是否要处理一堆类似的类,这些类用于执行相同的任务,唯一变化的是你处理这些任务的方式

一个微不足道的例子是计算各种几何图形的面积问题。你需要正方形、圆形、长方形、三角形等的面积,这里唯一变化的是你使用的数学公式(方式)来计算面积。因此,将每个形状从一个共同的基类中继承出来,并添加一个在基类中返回面积的虚拟方法(然后在每个子类中使用各自的数学公式实现),这将是一个好决策。

对于每个对象都让其成为虚函数“以防万一”,将会使你的对象占用更多的内存。此外,在调用虚函数时还会有一些小的(但非零)开销。因此,在性能/内存约束很重要的情况下(这基本上意味着你编写的每个真实世界程序),让所有东西都成为虚函数“以防万一”是一个不好的想法,也就是说一般并不适用。

然而,这又是有争议的,取决于需求清晰程度以及代码更改的频率。例如,在一个快速粗糙的工具或初始原型中,如果多用一些(不必要的)虚函数来增加灵活性,导致多占用几个字节的内存和几毫秒的时间损失并不会带来太大影响,那么这样做就可以接受。


0
我的观点是,如果你想使用父类指针指向子类实例并使用它们的方法,那么你应该使用虚方法。

当条件为真时,我认为这并不能真正回答“我怎样知道我需要多态性?”这个更大的问题。 - John Dibling

0

虚方法是实现多态性的一种方式。当您想要在更抽象的级别上定义某些操作,以至于实际上无法实现它们,因为它们太普遍时,就会使用它们。只有在派生类中,您才能告诉如何执行该操作。但是,通过定义虚方法,您创建了一个要求,这增加了类层次结构的严格性。这可能是明智的或不明智的,这取决于您想要获得什么以及您自己的品味。


-1

看一下设计模式。如果你的代码/设计是其中之一或类似的,请使用虚函数。否则,请尝试this


2
-1:想要实现GoF设计模式与需要虚拟函数之间没有因果关系。 - John Dibling

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