为什么在C++中要使用虚函数?

7
这不是关于它们如何工作和声明的问题,我认为这一点已经非常清楚了。问题在于为什么要实现这个?
我想实际原因是为了简化其他代码的关系和声明它们的基本类型变量,以处理来自许多其他子类的对象及其特定方法?
这是否可以通过模板化和类型检查来完成,就像我在Objective C中所做的那样?如果可以,哪种更有效?即使它是其子类,我发现将对象声明为一个类并将其实例化为另一个类仍然令人困惑。
对不起问些愚蠢的问题,但我还没有在C++中做过任何真正的项目,而且由于我是活跃的Objective C开发人员(它是一种较小的语言,因此在很大程度上依赖于SDK的功能,如OSX、iOS),我需要清晰地了解两者之间的任何平行方式。

1
你能解释一下Objective-C中的“模板化和类型检查”是什么意思吗?最好提供一些例子。 - molbdnilo
2
在 Objective C 中,所有的方法都是隐式虚拟的,所以我不理解你的问题。 - Björn Pollex
抱歉,句子有点误导性:“模板化和类型检查”是针对C++的,在Objective C中,我们使用“id”关键字,然后使用“isKindof”来确定类型,如果必要还可以进行强制转换。 - poksi592
1
@poksi592:这意味着您必须修改代码以适应添加到层次结构中的新类型,是吗?如果您开始扩展层次结构,这很快就会变成维护噩梦。 - David Rodríguez - dribeas
嗯,其实不是这样的。如果有任何原因我想要检查类型时,一般是为了控制或其他类似的目的。但是如果我想要从子类调用覆盖的方法,我只需要在通用的"id"类型对象上调用它,意思是,我不必像在C++中那样声明任何方法为虚函数,当然也不需要将派生类的对象声明为基类。 - poksi592
我认为更好的问题是,“为什么C++中存在非虚拟成员函数?” - JeremyP
6个回答

5

是的,这可以使用模板来实现,但是调用者必须知道对象的实际类型(具体类),这会增加耦合。

使用虚函数,调用者不需要知道实际类 - 它通过指向基类的指针进行操作,因此您可以编译客户端一次,实现者可以尽其所能地更改实际实现,只要接口没有改变,客户端就不必知道这一点。


我认为这是一个重要的观点。如果你从事编写一次编译,多处运行代码的业务(通常是为拥有库等客户服务),那么继承非常有用。然而,如果像我一样,你没有这样的业务需求,所有的代码都是内部的,并且对“紧耦合”没有问题,那么模板更加合理(在我看来)。 - Nim
但是这时你需要将对象声明为基类,对吗?如果你像派生类一样声明它,那还能行吗?如果不行,那么当你声明和初始化派生类时,你肯定必须知道你将在某个地方将其用作基类,对吗? - poksi592

1

我一点也不了解Objective-C,但以下是你想要"将对象声明为一个类并实例化为另一个类"的原因:Liskov替换原则

由于PDF文档,OpenOffice.org文档文档,Word文档也是文档,所以写成下面这样非常自然:

Document *d;
if (ends_with(filename, ".pdf"))
    d = new PdfDocument(filename);
else if (ends_with(filename, ".doc"))
    d = new WordDocument(filename);
else
    // you get the point
d->print();

现在,为了让这个工作起来,print必须是virtual,或者使用virtual函数实现,或者使用粗糙的hack重新发明virtual轮子。程序需要在运行时知道应用哪个不同的print方法。
模板解决的是另一个问题,在这个问题中,您可以在编译时确定将要使用哪个容器(例如)来存储一堆元素。如果您使用模板函数对这些容器进行操作,则无需在切换容器或向程序添加另一个容器时重写它们。

1
虚函数实现多态。我不知道Obj-C,所以无法进行比较,但是激励使用案例是您可以使用派生对象代替基本对象,代码将正常工作。如果您有一个已编译和工作的函数foo,它操作对base的引用,则无需修改它即可使其与derived的实例一起工作。
您可以通过获取参数的真实类型,然后使用短切换直接分派到适当的函数来做到这一点(假设您具有运行时类型信息),但这将要求手动修改每种新类型的开关(高维护成本)或具有反射(在C ++中不可用)以获取方法指针。即使在获得方法指针之后,您也必须调用它,这与虚拟调用一样昂贵。

关于虚函数调用的成本,基本上(在所有实现中都有虚方法表)应用于对象o的虚函数foo的调用:o.foo()被转换为o.vptr [3](),其中3是虚表中foo的位置,这是一个编译时常量。这基本上是双重间接:

从对象o获取指向虚表的指针,索引该表以获取指向函数的指针,然后调用。与直接的非多态调用相比,额外的成本只是表查找。(实际上,在使用多重继承时可能会有其他隐藏成本,因为隐式的this指针可能必须被移动),但虚分派的成本非常小。


0

嗯,这个想法很简单,就是让编译器来代替您进行检查。

就像有很多特性:可以隐藏自己不想自己完成的东西。这就是抽象。

继承、接口等允许您为实现代码提供一个与编译器匹配的接口。

如果没有虚函数机制,您将不得不编写:

class A
{
    void do_something();   
};

class B : public A
{
    void do_something(); // this one "hide" the A::do_something(), it replace it.
};


void DoSomething( A* object )
{
    // calling object->do_something will ALWAYS call A::do_something()
    // that's not what you want if object is B...
    // so we have to check manually:

    B* b_object = dynamic_cast<B*>( object );

    if( b_object != NULL ) // ok it's a b object, call B::do_something();
    {
        b_object->do_something()
    }
    else
    {
        object->do_something(); // that's a A, call A::do_something();
    }
}

这里有几个问题:

  1. 您必须为类层次结构中重新定义的每个函数编写此代码。
  2. 对于每个子类,您还需要添加一个附加的if语句。
  3. 每当您向整个层次结构中添加定义时,都需要再次修改此函数。
  4. 这是可见代码,您很容易出错,每次都可能出错。

因此,将函数标记为虚拟的正确地隐式方式完成,以动态方式自动重定向函数调用到对象的最终类型的正确实现。 您无需编写任何逻辑,因此在此代码中不会出现错误,并且不需要额外担心任何事情。

这是您不想烦恼的事情,因为可以由编译器/运行时完成。


0
虚函数在继承中非常重要。想象一个例子,你有一个CMonster类,然后有一个CRaidBoss和CBoss类从CMonster继承而来。
两者都需要被绘制。CMonster有一个Draw()函数,但是CRaidBoss和CBoss的绘制方式不同。因此,通过利用虚函数Draw,实现留给它们自己去完成。

0

使用模板在理论上也被称为多态。是的,这两种方法都是解决问题的有效途径。所采用的实现技术将更好地解释它们的性能优劣。

例如,Java通过模板擦除来实现模板。这意味着它只是表面上使用了模板,在表面下是普通的多态。

C++具有非常强大的模板。使用模板可以使代码更快,但每次使用模板都会为给定类型进行实例化。这意味着,如果您使用std::vector来存储int、double和string,您将拥有三个不同的向量类:这意味着可执行文件的大小会受到影响。


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