为什么要使用虚函数?

45

可能是重复问题:
有人能解释C++虚函数吗?

我有一个关于 C++ 虚函数的问题。

为什么以及何时使用虚函数?有人能给我一个虚函数的实时实现或用途吗?


1
我专门为此创建了一篇博客文章,因为当一个孩子问我“为什么需要虚函数”时,我发现很难解释。http://nrecursions.blogspot.in/2015/06/so-why-do-we-need-virtual-functions.html - Nav
4个回答

60
当您想要覆盖派生类的某个行为(读取方法)而不是基类实现的行为,并且您想通过指向基类的指针在运行时执行此操作时,您使用虚函数。经典示例是当您有一个名为Shape的基类和从它派生的具体形状(类)时。每个具体类都会重写(实现虚方法)名为Draw()的方法。
类层次结构如下:

Class hierarchy

以下代码片段展示了示例的用法;它创建了一个Shape类指针的数组,其中每个指针都指向一个不同的派生类对象。在运行时,调用Draw()方法会导致调用被该派生类重写的方法,并绘制(或呈现)特定的Shape
Shape *basep[] = { &line_obj, &tri_obj,
                   &rect_obj, &cir_obj};
for (i = 0; i < NO_PICTURES; i++)
    basep[i] -> Draw ();

上述程序只是使用指向基类的指针来存储派生类对象的地址。这提供了一种松散耦合,因为如果随时添加一个新的具体派生类shape,程序不必发生巨大变化。原因是实际上仅有很少的代码段使用(依赖于)具体的Shape类型。
上述是著名的SOLID设计原则中开闭原则的一个很好的例子。

10
如果我们不使用virtual,而是在子类中重新定义它们,比如LineTriangle,那么会有什么不同? - Mathew Kurian
4
如果基类中的函数不是虚函数,你将无法通过基类指针访问派生类的版本。 - radensb
2
抱歉,我还有点困惑。是否可以这样说,虚函数是用于在运行时不知道需要哪个版本的函数,但在那之前你也需要一个基类的实例化?因为似乎我可以不使用虚函数,在运行时创建子类并实例化正确的类。所以,“在此之前您还需要一个基类的实例化”是至关重要的,对吗?即:通过基指针调用派生函数。 - krb686

26

当您需要以相同的方式处理不同的对象时,您可以使用虚函数。这被称为多态性。让我们想象您有一些基类 - 类似于经典的形状:

    class Shape
    {
        public:
           virtual void draw() = 0;
           virtual ~Shape() {}
    };

    class Rectange: public Shape
    {
        public:
            void draw() { // draw rectangle here } 
    };


    class Circle: public Shape
    {
        public:
           void draw() { // draw circle here }
    };

现在你可以有不同形状的向量:

    vector<Shape*> shapes;
    shapes.push_back(new Rectangle());
    shapes.push_back(new Circle());

你可以像这样绘制所有形状:

    for(vector<Shape*>::iterator i = shapes.begin(); i != shapes.end(); i++)
    {
          (*i)->draw();
    }
以这种方式,您使用一个虚方法 - draw()来绘制不同的形状。根据指针背后对象的类型的运行时信息,选择正确版本的方法。
请注意,在使用虚函数时,您可以将它们声明为纯虚函数(例如在Shape类中,只需在方法原型后面放置“= 0”)。在这种情况下,您将无法创建具有纯虚函数的对象实例,并且它将被称为抽象类。
同时请注意析构函数前的“virtual”。如果您计划通过指向它们的基类的指针来处理对象,则应该将析构函数声明为虚函数,因此当您为基类指针调用“delete”时,所有析构函数链都将被调用,并且不会发生内存泄漏。

23

想象一下动物类,它的衍生类有猫、狗和牛。动物类有一个

virtual void SaySomething()
{
    cout << "Something";
}

函数。

Animal *a;
a = new Dog();
a->SaySomething();

不要打印“Something”,狗应该说“Bark”,猫应该说“Meow”。在这个例子中,a是一只狗,但有时您可能会有一只动物指针,而不知道它是哪种动物。您不想知道它是什么动物,您只想让动物说些什么。因此,您只需调用虚函数,猫将说“meow”,狗将说“bark”。

当然,SaySomething函数应该是纯虚函数,以避免可能出现的错误。


2
谢谢,您的回答令人满意。 - haris
我不太明白你最后一句话的意思:“SaySomething函数应该是纯虚函数以避免可能的错误。” 你能举个例子吗? - user454083
4
抱歉回复晚了。 考虑这样一个情况,一个开发者创建了一个新类继承我们的动物类(比如说“狐狸”),但忘记覆盖SaySomething方法。如果您使用我提供的虚方法,Fox实例将会说“Something”,这是不正确的。如果我们将SaySomething声明为纯虚方法,我们就不能实例化“狐狸”,包含new Fox(...)的代码将会引发错误。这样,创建Fox类的开发者将在编译时被通知他/她的错误。编译时错误很好,因为它们不浪费时间 :) - holgac
1
但在不支持纯虚方法的语言中,“必须实现”的方法应该引发异常。Java开发人员通常这样做。 然而,这里的主要问题是,狐狸在说什么 :P - holgac

1
你可以使用虚函数来实现“多态”,特别是当你有一个对象时,不知道实际的底层类型是什么,但知道你想要对它执行什么操作,并且这个实现(如何执行)取决于你实际拥有的类型。
基本上,这通常被称为“Liskov替换原则”,以Barbara Liskov的名字命名,她在1983年左右谈论了这个问题。
当你需要使用动态运行时决策时,在调用函数的代码点上,你不知道现在或将来可能通过它传递的类型,这是一个很好的模型。
但这并不是唯一的方法。有各种各样的“回调”可以接受一块数据“blob”,你可能会有依赖于数据中头块的回调表,例如消息处理器。对于这个,没有必要使用虚函数,事实上,你可能会使用类似于v-table的实现方式,只有一个条目(例如,一个只有一个虚函数的类)。

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