我刚看到一篇文章,指出我们不应该过度使用虚函数。人们认为,较少的虚函数往往会有较少的错误,减少维护成本。
由于虚函数需要进行v表查找,可能比普通函数慢,这是我能想到的一个原因。
关于C++或Java的上下文,会有哪些虚函数的错误和缺点呢?
我刚看到一篇文章,指出我们不应该过度使用虚函数。人们认为,较少的虚函数往往会有较少的错误,减少维护成本。
由于虚函数需要进行v表查找,可能比普通函数慢,这是我能想到的一个原因。
关于C++或Java的上下文,会有哪些虚函数的错误和缺点呢?
您发表了一些笼统的言论,我认为大多数务实的程序员会视其为误解或曲解。但是,确实存在反虚拟化狂热者,他们的代码对性能和维护而言可能同样糟糕。
在Java中,默认情况下所有内容都是虚拟的。说你不应该过度使用虚函数相当强硬。
在C++中,必须声明一个函数为虚函数,但在适当的情况下使用它们是完全可以接受的。
我刚刚读到,我们不应该过度使用虚函数。
很难定义“过度”… 当然,“在适当的时候使用虚函数”是好建议。
人们觉得少用虚函数 tend to have fewer bugs and reduces maintenance. 我无法理解由于虚函数可能出现什么样的错误和缺点。
设计不良的代码难以维护。没有别的。
如果您是库维护者,则在高高的类层次结构中嵌入的代码调试可能很困难,没有强大的IDE的帮助,通常很难确定哪个类覆盖了行为。这可能导致在跟踪继承树时在文件之间来回跳转。
因此,有一些原则,都有例外:
事实上,虚函数非常有用,这些疑虑很可能并不来自平衡的来源 - 虚函数已经被广泛应用了很长时间。比起其他语言,更多较新的语言将其作为默认选项。
class Widget
{
private WidgetThing _thing;
public virtual void Initialize()
{
_thing = new WidgetThing();
}
}
class DoubleWidget : Widget
{
private WidgetThing _double;
public override void Initialize()
{
// Whoops! Forgot to call base.Initalize()
_double = new WidgetThing();
}
}
在这里,DoubleWidget打破了父类的结构,因为Widget._thing
是空的。有一种相当标准的方法来解决这个问题:
class Widget
{
private WidgetThing _thing;
public void Initialize()
{
_thing = new WidgetThing();
OnInitialize();
}
protected virtual void OnInitialize() { }
}
class DoubleWidget : Widget
{
private WidgetThing _double;
protected override void OnInitialize()
{
_double = new WidgetThing();
}
}
我怀疑你误解了这个陈述。
"过度"是一个非常主观的词,我认为在这种情况下它意味着“当你不需要它时”,而不是在它有用时应该避免使用它。
根据我的经验,一些学生在学习虚函数并因忘记将函数设置为虚函数而受挫后,认为最好是将每个函数都设置为虚函数。
由于虚函数确实会对每个方法调用产生成本(在C++中通常无法避免,因为存在分离编译),因此您现在要为每个方法调用付出代价,并防止内联。许多教师不鼓励学生这样做,尽管“过度”这个词是一个非常不恰当的选择。
在Java中,“虚拟”行为(动态分派)是默认值。然而,JVM可以在运行时优化代码,并且在编译时可以将最终类中的最终方法或方法解析为单个目标。
最近我们有一个完美的例子说明了虚函数的误用会引入bug。
有一个共享库提供了消息处理程序:
class CMessageHandler {
public:
virtual void OnException( std::exception& e );
///other irrelevant stuff
};
class YourMessageHandler : public CMessageHandler {
public:
virtual void OnException( std::exception& e ) { //custom reaction here }
};
错误处理机制使用一个CMessageHandler*
指针,因此它不关心对象的实际类型。该函数是虚拟的,因此每当存在重载版本时,后者将被调用。
很酷,对吧?是的,直到共享库的开发人员更改了基类:
class CMessageHandler {
public:
virtual void OnException( const std::exception& e ); //<-- notice const here
///other irrelevant stuff
};
...并且重载函数停止工作。
你知道发生了什么吗?在基类被更改后,从C++的角度来看,重载函数停止成为重载函数 - 它们变成了新的、其他的、不相关的函数。
基类具有默认实现,没有标记为纯虚函数,因此派生类没有被强制重载默认实现。最终,该函数仅在错误处理情况下调用,而这种情况并非经常出现。因此,这个错误被悄悄地引入并长时间未被注意到。
彻底消除它的唯一方法是搜索整个代码库并编辑所有相关的代码片段。
虚函数会带来轻微的性能损失。通常情况下,这种损失太小以至于不会有任何影响,但在紧密循环中可能会显著。
虚函数会增加每个对象的大小一个指针。同样,这通常是微不足道的,但如果你创建数百万个小对象,则可能成为一个因素。
带有虚函数的类通常用于继承。派生类可以替换某些、所有或没有虚函数。这可能会增加额外的复杂性,而复杂性是程序员的死敌。例如,派生类可能会糟糕地实现虚函数。这可能会破坏依赖于虚函数的基类的某个部分。
现在让我明确一点:我并不是说“不要使用虚函数”。它们是C++的重要组成部分。只是要注意潜在的复杂性。
我在大约7年的时间里零散地作为一名顾问在同一个C++系统上工作,检查了大约4-5个程序员的工作。每次回去时,系统都变得越来越糟糕。在某个时候,有人决定删除所有虚函数,并用非常晦涩的基于工厂/RTTI的系统替换它们,这本质上做了虚函数已经在做但更糟糕、更昂贵、代码多达数千行、需要大量工作和测试的事情。完全毫无意义,显然是出于对未知的恐惧。
他们还手写了几十个复制构造函数,带有错误,而编译器会自动产生没有错误的版本,只有三个例外需要手写版本。
道德:不要与语言作斗争。它给你东西:使用它们。
virtual
关键字,但所有方法(函数)都是虚拟的,除了那些标记为最终的静态方法和私有实例方法。使用虚拟函数并不是一种坏习惯,但通常它们无法在编译时解析,编译器无法对它们执行优化,因此它们往往会变得稍微慢一些。JVM必须在运行时找出需要调用的确切方法。注意,这绝不是一个大问题,只有在您的目标是创建非常高性能的应用程序时才需要考虑它。