虚函数/纯虚函数解释

431

如果一个函数被定义为虚函数,那么它到底是什么意思?这和纯虚函数是一样的吗?

12个回答

394
维基百科的虚函数

在面向对象编程中,像 C++ 和 Object Pascal 这样的语言中,虚函数或虚方法是一种可继承和重写的函数或方法,用于实现动态分派。这个概念是面向对象编程中(运行时)多态性的一个重要部分。简而言之,虚函数定义了一个目标函数来执行,但目标函数在编译时可能是未知的。

与非虚函数不同,当虚函数被重写时,最终导出的版本将在类层次结构的所有级别中使用,而不仅仅是在创建它的级别上。因此,如果基类的某个方法调用了虚方法,则会使用在派生类中定义的版本,而不是在基类中定义的版本。

这与非虚函数形成对比,后者仍然可以在派生类中被重写,但“新”版本仅由派生类及以下级别使用,而不会更改基类的功能。

而另一方面..

一个纯虚函数或纯虚方法是一个虚函数,如果派生类不是抽象类,则必须由其实现。

当存在纯虚方法时,该类是“抽象的”,不能单独实例化。必须使用实现纯虚拟方法的派生类。纯虚函数在基类中根本没有定义,因此必须由派生类定义,否则该派生类也是抽象的,无法实例化。只有没有抽象方法的类才能被实例化。

虚函数提供了覆盖基类功能的方法,而纯虚函数则要求它这样做。


15
那么,pure virtual 是一个关键字,还是只是一个术语? - Justin
232
virtual void Function() = 0; 是一个纯虚函数。"= 0" 表示它是纯函数。 - Goz
9
Justin,“纯虚函数”只是一个术语(不是关键字,请参见我下面的回答),意思是“这个函数不能由基类实现”。就像Goz所说,给一个虚函数结尾加上“=0”就变成了“纯虚函数”。 - Nick Haddad
16
我相信Stroustrup曾经说过他想添加一个pure关键字,但是贝尔实验室即将发布C++的重要版本,在那个时候他的经理不允许添加。增加关键词是一件大事。 - quark
15
这不是一个好的回答。任何方法都可以被覆盖,不仅限于虚拟方法。请参阅我的回答以获取更多详细信息。 - Asik
显示剩余11条评论

253

我想就Wikipedia上关于"虚方法"的定义发表一下评论,正如其他几位在这里所说的那样。【在回答写作时】Wikipedia将虚方法定义为可以在子类中被覆盖的方法。这是不正确的:任何方法,而不仅仅是虚方法,都可以在子类中被覆盖。虚方法提供的是多态性,也就是在运行时选择方法的最终派生重载的能力。

考虑以下代码:

#include <iostream>
using namespace std;

class Base {
public:
    void NonVirtual() {
        cout << "Base NonVirtual called.\n";
    }
    virtual void Virtual() {
        cout << "Base Virtual called.\n";
    }
};
class Derived : public Base {
public:
    void NonVirtual() {
        cout << "Derived NonVirtual called.\n";
    }
    void Virtual() {
        cout << "Derived Virtual called.\n";
    }
};

int main() {
    Base* bBase = new Base();
    Base* bDerived = new Derived();

    bBase->NonVirtual();
    bBase->Virtual();
    bDerived->NonVirtual();
    bDerived->Virtual();
}
这个程序的输出是什么?
Base NonVirtual called.
Base Virtual called.
Base NonVirtual called.
Derived Virtual called.

派生类覆盖了基类的每个方法:不仅仅是虚函数,而且还包括非虚函数。

当你有一个指向Derived的Base指针(bDerived)时,调用NonVirtual会调用Base类的实现。这在编译时解决:编译器看到bDerived是一个Base*,NonVirtual不是虚函数,因此它在类Base上进行解析。

然而,调用Virtual会调用Derived类的实现。由于关键字virtual,方法的选择发生在运行时,而不是编译时。编译时发生的情况是,编译器看到这是一个Base*,它正在调用一个虚方法,因此它插入对vtable的调用,而不是class Base。这个vtable在运行时实例化,因此在运行时解析到最终派生的重写。

希望这不会太让人困惑。简而言之,任何方法都可以被重载,但只有虚方法才能给你多态性,也就是最终派生重写的运行时选择。然而,在实践中,重载非虚方法被认为是不好的习惯,很少使用,因此许多人(包括撰写维基百科文章的人)认为只有虚方法可以被重载。


8
维基百科文章(我无论如何不会为其辩护)将虚方法定义为“可以在子类中重写的方法”,并不排除可能存在其他同名但非虚拟的方法可以被声明,这被称为重载。 - anon
33
定义仍然是错误的。在派生类中可以被覆盖的方法在定义上并不是虚拟的;方法是否可以被覆盖与“虚拟”一词的定义无关。此外,“重载”通常指在同一个类中具有相同名称和返回类型但参数不同的多个方法;这与“覆盖”非常不同,后者意味着在派生类中完全相同的签名。当以非多态(非虚基类)方式进行时,它通常被称为“隐藏”。 - Asik
8
这应该是被接受的答案。那篇特定的维基百科文章[我会在这里花时间链接,因为没有其他问题的回答者这样做](http://en.wikipedia.org/wiki/Virtual_function)是完全垃圾。 +1,先生写得好。 - josaphatv
4
现在我明白了。感谢您先生,清楚地解释了任何方法都可以被派生类覆盖,改变的是编译器在不同情况下选择调用哪个函数的行为。 - Doodad
5
可以添加一个 Derived* 并进行相同的函数调用以加强观点表达。否则答案很好。 - Jeff Jones
显示剩余7条评论

119

virtual关键字赋予C++支持多态性的能力。当你有一个指向某个类的对象的指针时,如:

class Animal
{
  public:
    virtual int GetNumberOfLegs() = 0;
};

class Duck : public Animal
{
  public:
     int GetNumberOfLegs() { return 2; }
};

class Horse : public Animal
{
  public:
     int GetNumberOfLegs() { return 4; }
};

void SomeFunction(Animal * pAnimal)
{
  cout << pAnimal->GetNumberOfLegs();
}
在这个(愚蠢的)示例中,GetNumberOfLegs()函数根据调用它的对象的类返回适当的数量。
现在考虑函数“SomeFunction”。它不关心传递给它的动物对象的类型,只要它是从Animal派生出来的即可。编译器将自动将任何派生自Animal类的类转换为Animal,因为它是一个基类。
如果我们这样做:
Duck d;
SomeFunction(&d);

输出结果将为'2'。如果我们执行以下操作:

Horse h;
SomeFunction(&h);

这将输出'4'。我们无法这样做:

Animal a;
SomeFunction(&a);

由于GetNumberOfLegs()虚函数是纯虚函数,必须由派生类(子类)实现,因此它无法编译。

纯虚函数主要用于定义:

a) 抽象类

这些是基类,您必须从它们派生并实现纯虚函数。

b) 接口

这些是“空”的类,其中所有函数都是纯虚函数,因此您必须派生并实现所有函数。


在你的例子中,你不能做#4,因为你没有提供纯虚方法的实现。这并不是严格因为该方法是纯虚的。 - iheanyi
@iheanyi 你不能在基类中为纯虚方法提供实现。因此,情况#4仍然是错误的。 - prasad

36

在C++类中, virtual 是指定方法可以被子类覆盖(即由子类实现)的关键字。例如:

class Shape 
{
  public:
    Shape();
    virtual ~Shape();

    std::string getName() // not overridable
    {
      return m_name;
    }

    void setName( const std::string& name ) // not overridable
    {
      m_name = name;
    }

  protected:
    virtual void initShape() // overridable
    {
      setName("Generic Shape");
    }

  private:
    std::string m_name;
};
在这种情况下,子类可以重写 initShape 函数来执行一些特定的工作:
class Square : public Shape
{
  public: 
    Square();
    virtual ~Square();

  protected:
    virtual void initShape() // override the Shape::initShape function
    {
      setName("Square");
    }
}
术语“纯虚拟(pure virtual)”指的是需要由子类实现但未由基类实现的虚函数。你可以使用关键字“virtual”并在方法声明末尾加上“= 0”来将方法标记为纯虚方法。因此,如果您想使Shape :: initShape成为纯虚方法,则应执行以下操作:
class Shape 
{
 ...
    virtual void initShape() = 0; // pure virtual method
 ... 
};

通过在类中添加一个纯虚方法,您可以使该类成为一个抽象基类,这对于将接口与实现分离非常有用。


1
关于“必须由子类实现的虚函数”——严格来说并非如此,但如果它们没有被实现,那么子类也会是抽象的。而抽象类不能被实例化。此外,“基类无法实现”似乎有误导性;我建议使用“尚未被实现”更好,因为在代码中进行修改以添加基类内的实现并没有任何限制。 - NVRAM
2
“the getName function cannot be implemented by a subclass” 不完全正确。子类可以实现该方法(具有相同或不同的签名),但该实现不会覆盖该方法。您可以将Circle实现为子类并实现 “std :: string Circle :: getName()”,然后可以为Circle实例调用任一方法。但是,如果通过Shape指针或引用使用,则编译器将调用Shape :: getName()。 - NVRAM
1
两方面都很棒。我试图避免讨论此示例的特殊情况,我将修改答案以更具包容性。谢谢! - Nick Haddad
@NickHaddad 这是一个旧帖子,但我想知道为什么你把变量称为 m_namem_ 代表什么意思? - Tqn
1
假设NickHaddad遵循了惯例,m_name是一种常被称为匈牙利命名法的命名约定。m表示结构/类的成员,整数。 - Ketcomp

18

"Virtual"意味着该方法可以在子类中被重写,但在基类中有一个可直接调用的实现。 "Pure virtual"意味着它是一个没有直接可调用实现的虚拟方法。这样的方法必须在继承层次结构中至少被重写一次--如果一个类有任何未实现的虚拟方法,则无法构造该类的对象,编译将失败。

@quark指出,纯虚拟方法可以有一个实现,但由于纯虚拟方法必须被重写,因此默认实现不能直接调用。以下是带有默认值的纯虚拟方法示例:

#include <cstdio>

class A {
public:
    virtual void Hello() = 0;
};

void A::Hello() {
    printf("A::Hello\n");
}

class B : public A {
public:
    void Hello() {
        printf("B::Hello\n");
        A::Hello();
    }
};

int main() {
    /* Prints:
           B::Hello
           A::Hello
    */
    B b;
    b.Hello();
    return 0;
}
根据评论,编译是否会失败取决于具体的编译器。至少在GCC 4.3.3中无法通过编译:
class A {
public:
    virtual void Hello() = 0;
};

int main()
{
    A a;
    return 0;
}

输出:

$ g++ -c virt.cpp 
virt.cpp: In function ‘int main()’:
virt.cpp:8: error: cannot declare variable ‘a’ to be of abstract type ‘A’
virt.cpp:1: note:   because the following virtual functions are pure within ‘A’:
virt.cpp:3: note:   virtual void A::Hello()

如果您想要实例化一个类的实例,那么就必须覆盖它。如果您不创建任何实例,那么代码将编译正常。 - Glen
1
编译不会失败。如果没有实现纯虚方法,那么该类/对象就无法被实例化。它可能无法链接,但编译将不会失败。 - Tim
@Glen,@tim:使用哪个编译器?当我尝试编译一个构建抽象类的程序时,它无法编译。 - John Millikin
@Neil:你确定吗?我也尝试过使用指针进行实例化,例如A *a = new A,但是仍然会出现相同的错误。 @Glen:我再次查看了你的评论,意识到自己误读了。我会更正帖子。 - John Millikin
6
约翰,以下内容不太正确:“'纯虚拟'意味着它是一个没有实现的虚拟方法”。纯虚拟方法可以有实现。但是你不能直接调用它们:你必须重载并在子类中使用基类实现。这允许您提供默认部分实现。虽然这不是常见的技术。 - quark
显示剩余3条评论

12

虚函数是在基类中声明并由派生类重新定义的成员函数。虚函数按继承顺序自上而下排列。如果派生类未重写虚函数,则使用其基类中定义的函数。

纯虚函数是指与基类相对无定义的函数。它在基类中没有实现。任何派生类必须重写此函数。


1
一个纯虚函数可以在基类中有其实现。 - Hari Krishnan U

9

虚拟关键字如何工作?

假设Man是一个基类,Indian是从Man派生而来的。

Class Man
{
 public: 
   virtual void do_work()
   {}
}

Class Indian : public Man
{
 public: 
   void do_work()
   {}
}

声明do_work()为虚函数只意味着:在运行时才能确定调用哪个do_work()函数。假设我这样做:
Man *man;
man = new Indian();
man->do_work(); // Indian's do work is only called.

如果不使用虚拟方法,则根据调用对象的类型,编译器会静态确定或静态绑定相同的方法。因此,如果Man对象调用do_work(),则将调用Man的do_work(),即使其指向印度对象。
我认为得票最高的答案是误导性的-无论是否使用虚拟方法,任何方法都可以在派生类中具有重写实现。特别提到C ++,正确的区别是关联函数的运行时(当使用virtual时)绑定和编译时(当未使用virtual但重写了方法并且基础指针指向派生对象时)绑定。
还有一个误导性的评论说:
“Justin,“pure virtual”只是一个术语(不是关键字,请参见我的答案以下),意思是“基类不能实现此函数。”
这是错的!纯虚函数也可以有主体,并且可以被实现!事实上,抽象类的纯虚函数可以被静态调用!两位非常好的作者是Bjarne Stroustrup和Stan Lippman……因为他们写了这种语言。

2
不幸的是,一旦一个答案开始得到赞同,其他所有答案都将被忽略。即使它们可能更好。 - LtWorf

2
在Simula、C++和C#中,默认使用静态方法绑定,但程序员可以通过将特定的方法标记为虚拟来指定这些方法应该使用动态绑定。动态方法绑定是面向对象编程的核心。
面向对象编程需要三个基本概念:封装、继承和动态方法绑定。
封装允许将抽象的实现细节隐藏在一个简单的接口后面。
继承允许定义一个新的抽象作为某个现有抽象的扩展或细化,自动获得一些或全部特征。
动态方法绑定允许新的抽象在使用旧抽象的上下文中显示其新行为。

1
虚函数可以被派生类重写,但需要在基类(将被重写的类)中有一个实现。
纯虚函数在基类中没有实现。它们需要由派生类定义。(因此,在技术上,“重写”不是正确的术语,因为没有东西可以重写)。
虚函数对应于默认的Java行为,当派生类重写基类的方法时。
纯虚函数对应于抽象类中的抽象方法的行为。而仅包含纯虚函数和常量的类将成为cpp-pendant到接口的类。

0

纯虚函数

尝试这段代码

#include <iostream>
using namespace std;
class aClassWithPureVirtualFunction
{

public:

    virtual void sayHellow()=0;

};

class anotherClass:aClassWithPureVirtualFunction
{

public:

    void sayHellow()
    {

        cout<<"hellow World";
    }

};
int main()
{
    //aClassWithPureVirtualFunction virtualObject;
    /*
     This not possible to create object of a class that contain pure virtual function
    */
    anotherClass object;
    object.sayHellow();
}

在类anotherClass中删除函数sayHellow并运行代码。您将会得到错误!因为当一个类包含一个纯虚函数时,不能从该类创建任何对象,并且如果继承了它,则派生类必须实现该函数。 虚函数 尝试另一段代码。
#include <iostream>
using namespace std;
class aClassWithPureVirtualFunction
{

public:

    virtual void sayHellow()
    {
        cout<<"from base\n";
    }

};

class anotherClass:public aClassWithPureVirtualFunction
{

public:

    void sayHellow()
    {

        cout<<"from derived \n";
    }

};
int main()
{
    aClassWithPureVirtualFunction *baseObject=new aClassWithPureVirtualFunction;
    baseObject->sayHellow();///call base one

    baseObject=new anotherClass;
    baseObject->sayHellow();////call the derived one!

}

在基类中,sayHello函数被标记为虚函数。这意味着编译器会尝试在派生类中搜索并实现该函数。如果未找到,则执行基类函数。谢谢。


1
哈哈,我花了长达30秒的时间才明白这里出了什么问题... HelloW :) - hans

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