虚拟/纯虚拟解释


345

如果将函数定义为虚函数,并且与纯虚函数相同,那到底是什么意思?

Answers:


338

维基百科的虚拟功能 ...

在面向对象的编程中,在诸如C ++和Object Pascal之类的语言中,虚函数或虚方法是可继承且可重写的函数或方法,因此有助于进行动态调度。这个概念是面向对象编程(OOP)的(运行时)多态性部分的重要组成部分。简而言之,虚函数定义了要执行的目标函数,但是在编译时可能不知道该目标。

与非虚拟函数不同,重写虚拟函数时,派生最广泛的版本将用于类层次结构的所有级别,而不仅仅是创建它的级别。因此,如果基类的一个方法调用虚拟方法,则将使用派生类中定义的版本而不是基类中定义的版本。

这与非虚拟函数相反,非虚拟函数仍可以在派生类中重写,但“新”版本将仅由派生类及其以下版本使用,而根本不会更改基类的功能。

而..

纯虚函数或纯虚方法是如果派生类不是抽象的,则派生类需要实现的虚函数。

当存在纯虚拟方法时,该类是“抽象”的,并且无法单独实例化。而是必须使用实现纯虚拟方法的派生类。在基类中根本没有定义纯虚拟,因此派生类必须对其进行定义,否则该派生类也是抽象的,无法实例化。只能实例化没有抽象方法的类。

虚拟提供了一种重写基类功能的方法,而纯虚拟需要它。


10
那么...纯虚拟是关键字,还是仅使用了一个术语?
贾斯汀2009年

197
virtual void Function()= 0; 是纯虚拟的。“ = 0”表示纯度。
Goz

8
贾斯汀(Justin),“纯虚拟”只是一个术语(不是关键字,请参见下面的答案),它的意思是“该函数不能由基类实现。正如Goz所说,在虚拟末尾添加“ = 0”功能使其“纯净”
尼克·哈达德

14
我相信Stroustrup说过他想添加一个pure关键字,但是Bell Labs即将发布C ++的主要版本,并且他的经理在那个后期不允许这样做。添加关键字很重要。
夸克

14
这不是一个好答案。可以覆盖任何方法,而不仅仅是虚拟方法。请参阅我的答案以获取更多详细信息。
阿西克

212

我想评论一下Wikipedia对虚拟的定义,这里有几个人对此进行了重复。[在编写此答案时,] 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.

派生会覆盖Base的每种方法:不仅是虚拟方法,还包括非虚拟方法。

我们看到,当您拥有派生基指针(bDerived)时,调用NonVirtual会调用基类实现。这在编译时已解决:编译器发现bDerived是Base *,NonVirtual不是虚拟的,因此它对Base类进行解析。

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

我希望这不会太令人困惑。简而言之,任何方法都可以被覆盖,但是只有虚拟方法才能为您提供多态性,即在运行时选择最派生的覆盖。但是,实际上,重写非虚拟方法被认为是不好的做法,很少使用,因此许多人(包括撰写Wikipedia文章的人)认为只能重写虚拟方法。


6
仅仅因为Wikipedia文章(我绝对不能捍卫)定义了一个虚拟方法“作为可以在子类中覆盖的方法”,并不排除可以声明其他同名非虚拟方法的可能性。这称为过载。

26
但是该定义是不正确的。可以在派生类中重写的方法在定义上不是虚拟的;该方法是否可以覆盖与“虚拟”的定义无关。同样,“重载”通常是指在同一类中有多个具有相同名称和返回类型但参数不同的方法。它与“覆盖”有很大的不同,“覆盖”意味着完全相同的签名,但是在派生类中。当它以非多态(非虚拟基础)方式完成时,通常称为“隐藏”。
阿西克

5
这应该是公认的答案。那篇特别的Wikipedia文章(我将花时间在这里链接,因为在这个问题上没有其他人做到了)是完全垃圾。+1,先生。
2014年

2
现在,这很有意义。好的,先生,谢谢您正确地解释了任何方法都可以被派生类覆盖的方法,而变化之处在于编译器如何选择在不同情况下调用什么函数。
Doodad

3
添加一个Derived*具有相同功能的调用以将指针归位可能会有所帮助。否则,很好的答案
杰夫·琼斯

114

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派生的,它都不关心传递给它什么动物对象。编译器会自动将任何动物派生的类强制转换为动物,因为它是基类。

如果我们这样做:

Duck d;
SomeFunction(&d);

它会输出“ 2”。如果我们这样做:

Horse h;
SomeFunction(&h);

它会输出“ 4”。我们不能这样做:

Animal a;
SomeFunction(&a);

因为由于GetNumberOfLegs()虚拟函数是纯函数而无法编译的,这意味着它必须通过派生类(子类)来实现。

纯虚函数通常用于定义:

a)抽象类

这些是基类,您必须从中派生然后实现纯虚函数。

b)接口

这些是“空”类,其中所有功能都是纯虚拟的,因此您必须派生然后实现所有功能。


在您的示例中,您无法执行#4,因为您没有提供纯虚拟方法的实现。并非严格因为该方法是纯虚拟方法。
iheanyi 2014年

@iheanyi您不能在基类中提供对纯虚方法的实现。因此,案例4仍然是错误的。
prasad

32

在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");
    }
}

术语纯虚拟是指需要由子类实现但尚未由基类实现的虚拟功能。通过使用virtual关键字并在方法声明的末尾添加= 0,可以将方法指定为纯虚方法。

因此,如果您想使Shape :: initShape成为纯虚拟的,则可以执行以下操作:

class Shape 
{
 ...
    virtual void initShape() = 0; // pure virtual method
 ... 
};

通过向您的类中添加纯虚拟方法,可以使该类成为抽象基类 ,这对于将接口与实现分开非常方便。


1
关于“必须由子类实现的虚拟函数”,这并不是严格意义上的,但是如果不是,则子类也是抽象的。抽象类无法实例化。同样,“无法由基类实现”似乎具有误导性;我建议“没有过”会更好,因为对在基类中添加实现的代码修改没有任何限制。
NVRAM

2
而且“ getName函数不能由子类实现”不是很正确。子类可以实现该方法(具有相同或不同的签名),但是该实现不会覆盖该方法。您可以将Circle作为子类实现,并实现“ std :: string Circle :: getName()”,然后可以为Circle实例调用任一方法。但是,如果通过Shape指针或引用使用编译器,则会调用Shape :: getName()。
NVRAM

1
两方面都很好。我试图避免在本例中讨论特殊情况,而是将答案修改得更宽容。谢谢!
尼克·哈达德

@NickHaddad旧线程,但是想知道为什么调用变量m_name。什么m_意思
Tqn

1
@Tqn假设NickHaddad遵循约定,则m_name是一个命名约定,通常称为匈牙利表示法。m表示结构/类的成员,整数。
Ketcomp

16

“虚拟”表示该方法可以在子类中重写,但在基类中具有可直接调用的实现。“纯虚拟”表示它是一种虚拟方法,没有直接可调用的实现。这种方法必须在继承层次结构中至少被重写一次-如果类具有任何未实现的虚拟方法,则无法构造该类的对象,并且编译将失败。

@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
编译不会失败。如果没有(纯)虚拟方法的实现,则无法实例化该类/对象。它可能不会链接,但会编译。

@ Glen,@ tim:在哪个编译器上?当我尝试编译构建抽象类的程序时,它不会编译。
约翰·米利金

@John编译只会在您尝试实例化包含PVF的类的实例时失败。您当然可以实例化此类的指针或参考值。

5
另外,约翰,以下说法不太正确:““纯虚拟”表示它是没有实现的虚拟方法。” 纯虚拟方法可以具有实现。但是您不能直接调用它们:您必须在子类中重写并使用基类实现。这使您可以提供实现的默认部分。但是,这不是常见的技术。
夸克

9

虚拟关键字如何工作?

假设人是基本阶级,印第安人是人衍生的。

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.

如果未使用virtual,则取决于调用的对象,编译器将静态确定或静态绑定相同对象。因此,如果Man的对象调用do_work(),则Man的do_work()甚至被认为指向印度对象

我认为,投票最多的答案具有误导性-任何方法(无论虚拟方法)都可以在派生类中具有重写的实现。具体参考C ++,正确的区别是关联函数的运行时(使用虚拟时)绑定和编译时(不使用虚拟但重写方法且将基指针指向派生对象时)绑定。

似乎还有另一种误导性的评论说:

“ Justin,'pure virtual'只是一个术语(不是关键字,请参阅下面的答案),用于表示“该函数不能由基类实现。”

这是错误的!纯粹的虚拟功能也可以具有主体,并且可以实现!事实是抽象类的纯虚函数可以被静态调用!Bjarne Stroustrup和Stan Lippman是两位非常出色的作家。


2
不幸的是,一旦答案开始被接受,所有其他答案将被忽略。即使他们可能会更好。
LtWorf 2014年

3

虚函数是在基类中声明并由派生类重新定义的成员函数。虚函数按继承顺序是分层的。当派生类未覆盖虚拟函数时,将使用其基类中定义的函数。

纯虚函数是不包含相对于基类的定义的虚函数。 在基类中没有实现。任何派生类都必须重写此函数。


2

Simula,C ++和C#默认使用静态方法绑定,程序员可以通过将特定方法标记为虚拟方法来指定特定方法应使用动态绑定。动态方法绑定对于面向对象的编程至关重要。

面向对象的编程需要三个基本概念:封装,继承和动态方法绑定。

封装允许将抽象的实现细节隐藏在简单的接口后面。

继承允许将新的抽象定义为对某些现有抽象的扩展或改进,从而自动获取其某些或全部特征。

动态方法绑定允许新抽象显示新行为,即使在需要旧抽象的上下文中使用时也是如此。


1

可以通过派生类覆盖虚拟方法,但需要在基类中实现(将被覆盖的实现)

纯虚方法没有基类的实现。它们需要由派生类定义。(因此,从技术上讲,覆盖不是正确的用语,因为没有可覆盖的内容)。

当派生类覆盖基类的方法时,虚拟对应于默认的Java行为。

纯虚方法对应于抽象类中抽象方法的行为。而仅包含纯虚方法和常量的类将是Interface的cpp项。


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!

}

这里的sayHellow函数在基类中被标记为虚函数,它表示尝试在派生类中搜索该函数并实现该函数的编译器,如果找不到,则执行基类。


哈哈,花了我30秒钟的时间,才了解这里出了什么问题……HelloW :)
汉斯,

0

“虚拟函数或虚拟方法是一种功能或方法,其行为可以在继承的类中被具有相同签名的函数覆盖”-Wikipedia

这不是对虚拟功能的很好解释。因为,即使成员不是虚拟成员,继承类也可以覆盖它。您可以尝试自己查看。

当函数将基类作为参数时,这种差异会显示出来。当您提供继承类作为输入时,该函数将使用重写函数的基类实现。但是,如果该函数是虚拟的,它将使用在派生类中实现的函数。


0
  • 虚函数必须在基类和派生类中都有定义,但不是必须的,例如ToString()或toString()函数是Virtual,因此您可以通过在用户定义的类中覆盖它来提供自己的实现。

  • 虚函数在普通类中声明和定义。

  • 纯虚函数必须声明为以“ = 0”结尾,并且只能在抽象类中声明。

  • 具有纯虚函数的抽象类不能具有该纯虚函数的定义,因此它意味着必须在从该抽象类派生的类中提供实现。


与@rashedcs相同的注解:实际上,纯虚函数可以有其定义...
Jarek C
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.