为什么我们必须将虚方法声明为虚方法?

6
假设我们有一个名为“Animal”的类和子类“Cat”和“Dog”。
假设我们希望在将它们的对象传递到任何“Animal”的中间函数时,都允许“Cat”和“Dog”发出声音(猫:“喵” - 狗:“汪”)。
为什么我们必须使用虚方法来实现这一点?难道我们不能在“Animal”中不定义虚方法的情况下执行Animal->makeNoise()吗?由于“Cat”和“Dog”都是动物,因此“makeNoise()”是否在引用传递给函数的动物上已经很清楚了?
这只是语法问题还是更深层次的问题?我很确定在Java中我们不需要这样做。

4
@Antonio,虚拟基类与虚拟方法完全没有关系。这只是C++在滥用关键词以实现多种目的方面做得非常出色的案例之一。 - Sergei Tachenov
如果你传递了一个没有 makeNoise() 方法的老鼠,会发生什么? - Martin York
6个回答

10
在Java中,默认情况下所有成员函数都是虚函数(除了静态,私有和final函数)。
在C++中,默认情况下所有成员函数都不是虚函数。将函数设置为虚函数会增加开销, 无论是运行时还是对象大小上, 而C++的哲学是不要为你不使用的东西付费。我的大部分对象都不是多态的,所以我不应该为多态性付费,除非我需要它。因此,需要将 Animal::makeNoise() 设置为虚函数时,必须显式指定其为虚函数。

在Java中,如果一个函数在基类中是非 final 的,那么它可以是 final 虚拟函数。也就是说,一个类可以重写虚拟函数并将其设置为 final 以防止进一步的重写,但是在继承树中该函数仍然是虚拟的。 - Sergei Tachenov
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sergei Tachenov
在C++中,你只支付你所使用的。而在Java中,你支付一切。 - Pete Becker
我前往并编辑了以澄清之前提到的要点。希望你不介意。 - Sergei Tachenov
@SergeyTachenov 这个答案的目标不是列举关于virtual如何工作以及其所有微妙之处的一切。目标只是简单陈述C++和Java在virtual默认值方面设计哲学上的高层次差异。 - Barry
哦,好的,对此很抱歉。但是仍然要说,在Java中声称final函数不是虚函数是不正确的,也许你可以修正一下这个问题? - Sergei Tachenov

1
如果您想推断动物的类型,然后调用make_sound()函数,那么您需要对每个动物对象进行dynamic_cast,包括直接或间接作为Animal类子类的任何类。这样做既难以维护,也很难适应变化,例如添加新的类作为Animal类的子类。
由于C++的哲学是效率,您必须要求编译器提供运行时多态性,因为它是昂贵的。如何做到这一点?通过将make_sound()函数声明为虚函数。这将创建一个vtable(函数指针表),它引用make_sound()的地址,该地址根据对象的类型而异。
不需要进行下转换,因为间接处理了所有内容。可能需要数百行代码,但只需一行代码即可完成。这就是间接性的力量!

1
C++旨在尽可能减少开销,信任程序员做出正确的调用。本质上,它“给了你枪和开枪的选择”,正如我的一个朋友经常说的那样。速度和灵活性至关重要。
为了正确引发真正的多态行为,C++要求进行指定。然而!只需要在基类中指定即可,因为所有派生类都将继承虚拟成员函数。如果成员继承了虚拟成员函数,则在声明中放置“virtual”是一个好习惯,但不是必需的。
ADT通常实现纯虚函数来指示派生类必须实现该函数,例如:
animal makeNoise() = 0; /*Indicates this function contains no implementation.
and must be implemented by derived classes in order to function.*/

再次强调,只要基类中包含了 'virtual',派生类在继承成员时并不需要将其标记为 'virtual'。


0

在Java中,您也可以使用虚拟方法。 它提高了软件的松耦合性。

例如,您可以使用一个库,但不知道它们内部使用哪种动物。有一种动物实现您不知道,但您可以使用它,因为它是一种动物。您可以使用library.getAnimal方法获取该动物。现在,您可以使用他们的动物而不知道它发出什么噪音,因为他们必须实现makeNoise方法。

编辑:所以回答您的问题,C++需要显式声明,在Java中则是隐式的。因此,这是一种特定于语言的怪癖。


在Java中,你不会这样做,也不能这样做。你不能将虚函数声明为虚函数,因为Java中没有这样的关键字。 - Sergei Tachenov
在Java中无法覆盖父类方法吗?在我看来,Java的公共方法默认是虚拟的。 - Ben
没错。由于它们默认是虚拟的,因此您无法将它们声明为虚拟的,也不需要这样做。仔细阅读问题。 - Sergei Tachenov
我从未说过你要声明什么,我是说在Java中你也使用虚拟方法。 - Ben
你的回答以“In java you do this too”开头,但问题是“为什么必须将虚方法声明为这样”。很明显,OP想知道为什么在C++中必须将虚方法声明为virtual,而在Java中不需要这样做——它们会自动成为虚方法。 - Sergei Tachenov
啊,现在我明白你的意思了。我指的是它说在Java中我们不这样做的陈述。我应该改变我的帖子并且说,这是因为C ++需要显式声明,在Java中则是隐式的。所以是的,这是一种特定于语言的怪癖。 - Ben

0

你可以说这是语言规则之一,必须这样做。

但这样做有其好处。

当尝试验证使用Animal的代码时,编译器知道Animal上存在哪些函数。可以在不检查所有派生自Animal的类的情况下判断代码是否正确。因此,该代码不需要依赖于所有这些派生类。如果从Animal派生一个新类,但忘记实现makeNoise函数,那么这是新类中的错误,而不是使用Animal基类的代码中的错误,编译器可以指向该错误。如果Animal中没有声明虚函数,则无法确定是调用代码还是新类出现了错误。

关键点在于,由于C++具有静态类型,这些错误将在编译时被捕获。其他语言可以允许动态类型,这可能会使某些事情变得更容易,但是这些错误只能在运行时被发现。


0
在Java中,默认情况下所有函数都是虚函数。但在C++中,它们不是虚函数。因此,当您在给定类型的指针上调用非虚函数时,该类型的函数实现将使用对象的地址作为“this”来调用。
class Animal {
public:
    void sound() { std::cout << "splat\n"; }
    virtual void appearance() { std::cout << "animaly\n"; }
};

class Cat {
public:
    void sound() { std::cout << "meow\n"; }
    virtual void appearance() { std::cout << "furry\n"; }
};

int main() {
    Animal a;
    Cat c;
    Animal* ac = new Cat;

    a.sound();  // splat
    a.appearance();  // animaly

    c.sound();  // meow
    c.appearance();  // furry

    ac->sound();  // splat
    ac->appearance();  // furry
}

当您想编写一个泛化于"动物"而不是要求特定派生类指针的函数时,就会出现这种情况。


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