我正在学习C++,我刚刚开始学习虚拟函数。

从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。

但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。

那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。


当前回答

我们需要支持“运行时多态性”的虚拟方法。当使用指针或对基类的引用引用派生类对象时,可以为该对象调用虚拟函数并执行派生类版本的函数。

其他回答

下面是前两个答案的C++代码的合并版本。

#include        <iostream>
#include        <string>

using   namespace       std;

class   Animal
{
        public:
#ifdef  VIRTUAL
                virtual string  says()  {       return  "??";   }
#else
                string  says()  {       return  "??";   }
#endif
};

class   Dog:    public Animal
{
        public:
                string  says()  {       return  "woof"; }
};

string  func(Animal *a)
{
        return  a->says();
}

int     main()
{
        Animal  *a = new Animal();
        Dog     *d = new Dog();
        Animal  *ad = d;

        cout << "Animal a says\t\t" << a->says() << endl;
        cout << "Dog d says\t\t" << d->says() << endl;
        cout << "Animal dog ad says\t" << ad->says() << endl;

        cout << "func(a) :\t\t" <<      func(a) <<      endl;
        cout << "func(d) :\t\t" <<      func(d) <<      endl;
        cout << "func(ad):\t\t" <<      func(ad)<<      endl;
}

两种不同的结果是:

如果没有#define virtual,它将在编译时绑定。Animal*ad和func(Animal*)都指向Animal的says()方法。

$ g++ virtual.cpp -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  ??
func(a) :       ??
func(d) :       ??
func(ad):       ??

使用#define virtual,它在运行时绑定。Dog*d、Animal*ad和func(Animal*)指向/引用Dog的says()方法,因为Dog是它们的对象类型。除非未定义[Dog's says()“woof”]方法,否则它将是在类树中首先搜索的方法,即派生类可能会覆盖其基类的方法[Eanimal's says)]。

$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

有趣的是,Python中的所有类属性(数据和方法)都是虚拟的。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字virtual。下面是Python的代码版本:

class   Animal:
        def     says(self):
                return  "??"

class   Dog(Animal):
        def     says(self):
                return  "woof"

def     func(a):
        return  a.says()

if      __name__ == "__main__":

        a = Animal()
        d = Dog()
        ad = d  #       dynamic typing by assignment

        print("Animal a says\t\t{}".format(a.says()))
        print("Dog d says\t\t{}".format(d.says()))
        print("Animal dog ad says\t{}".format(ad.says()))

        print("func(a) :\t\t{}".format(func(a)))
        print("func(d) :\t\t{}".format(func(d)))
        print("func(ad):\t\t{}".format(func(ad)))

输出为:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

这与C++的虚拟定义相同。注意,d和ad是两个不同的指针变量,引用/指向同一个Dog实例。表达式(ad is d)返回True,其值与0xb79f72cc>处的<main.Dog对象相同。

如果你知道潜在的机制,这会有所帮助。C++将C程序员使用的一些编码技术形式化,用“覆盖”代替“类”-具有公共头段的结构将用于处理不同类型的对象,但具有一些公共数据或操作。通常,覆盖的基本结构(公共部分)具有指向函数表的指针,该函数表指向每个对象类型的不同例程集。C++做了同样的事情,但隐藏了机制,即C++ptr->func(…),其中func是C的虚拟(*ptr->func_table[func_num])(ptr,…),派生类之间的变化是func_table内容。[非虚拟方法ptr->func()仅转换为mangled_func(ptr,..)。]

这样做的结果是,您只需要了解基类就可以调用派生类的方法,即,如果例程了解类a,您可以向它传递派生类B指针,那么所调用的虚拟方法将是B的虚拟方法,而不是a的虚拟方法。

如果没有“虚拟”,您将获得“早期绑定”。在编译时,根据您调用的指针的类型来决定使用该方法的哪个实现。

使用“虚拟”,您将获得“后期绑定”。使用方法的哪种实现在运行时根据指向对象的类型来决定,即它最初的构造形式。这不一定是根据指向该对象的指针的类型来考虑的。

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()  {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 ()  {  std::cout << "Derived::Method2" << std::endl;  }
};

Base* basePtr = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

basePtr->Method1 ();  //  Prints "Base::Method1"
basePtr->Method2 ();  //  Prints "Derived::Method2"

编辑-请参阅此问题。

此外,本教程还介绍了C++中的早期和后期绑定。

为什么我们需要C++中的虚拟方法?

快速回答:

它为我们提供了面向对象编程所需的“要素”之一。

在Bjarne Stroustrup C++编程:原理与实践中,(14.3):

虚拟函数提供了在基类中定义函数的能力,并在用户调用基类函数时在派生类中具有相同名称和类型的函数。这通常称为运行时多态性、动态调度或运行时调度,因为调用的函数是在运行时根据所使用的对象类型确定的。

如果您需要虚拟函数调用2,这是最快、更有效的实现。

为了处理虚拟调用,需要一条或多条与派生对象3相关的数据。通常的做法是添加函数表的地址。该表通常称为虚拟表或虚拟函数表,其地址通常称为虚指针。每个虚拟函数在虚拟表中都有一个槽。根据调用者的对象(派生)类型,虚拟函数依次调用相应的重写。


1.使用继承、运行时多态性和封装是面向对象编程的最常见定义。

2.您不能在运行时使用其他语言功能在备选方案中进行选择,从而使功能更快或使用更少的内存。Bjarne Stroustrup C++编程:原理与实践。(14.3.1).

3.当我们调用包含虚拟函数的基类时,可以判断哪个函数真正被调用。

对虚拟函数的解释存在的问题是,它们没有解释如何在实践中使用它,以及它如何有助于维护。我创建了一个虚拟函数教程,人们已经发现它非常有用。此外,它基于战场前提,这让它更令人兴奋:https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html.

考虑这个战场应用:

#include "iostream"

//This class is created by Gun1's company
class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};
//This class is created by Gun2's company
class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};

//We create an abstract class to interface with WeaponController
class WeaponsInterface {
 public:
 virtual void shootTarget() = 0;
};

//A wrapper class to encapsulate Gun1's shooting function
class WeaponGun1 : public WeaponsInterface {
 private:
 Gun1* g;

 public:
 WeaponGun1(): g(new Gun1()) {}
 ~WeaponGun1() { delete g;}
 virtual void shootTarget() { g->fire(); }
};

//A wrapper class to encapsulate Gun2's shooting function
class WeaponGun2 : public WeaponsInterface {
 private:
 Gun2* g;

 public:
 WeaponGun2(): g(new Gun2()) {}
 ~WeaponGun2() { delete g;}
 virtual void shootTarget() { g->shoot(); }
};

class WeaponController {
 private:
 WeaponsInterface* w;
 WeaponGun1* g1;
 WeaponGun2* g2;
 public:
 WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;}
 ~WeaponController() {delete g1; delete g2;}
 void shootTarget() { w->shootTarget();}
 void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically
   switch(gunNumber) {
     case 1: w = g1; break;
     case 2: w = g2; break;
   }
 }
};


class BattlefieldSoftware {
 private:
 WeaponController* wc;
 public:
 BattlefieldSoftware() : wc(new WeaponController()) {}
 ~BattlefieldSoftware() { delete wc; }

 void shootTarget() { wc->shootTarget(); }
 void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); }
};


int main() {
 BattlefieldSoftware* bf = new BattlefieldSoftware();
 bf->shootTarget();
 for(int i = 2; i > 0; i--) {
     bf->changeGunTo(i);
     bf->shootTarget();
 }
 delete bf;
}

我鼓励您首先阅读博客上的文章,了解包装器类创建的原因。

如图所示,有各种火炮/导弹可以连接到战场软件,并且可以向这些武器发出命令,以进行射击或重新校准等。这里的挑战是能够在不必更改蓝色战场软件的情况下更改/更换火炮/导弹,并且能够在运行时切换武器,而无需更改代码并重新编译。

上面的代码显示了问题是如何解决的,以及具有精心设计的包装类的虚拟函数如何封装函数并帮助在运行时分配派生类指针。WeaponGun1类的创建确保了你将Gun1的处理完全分离到类中。无论你对Gun1做了什么改变,你只需要在WeaponGun1中做出改变,并有信心其他职业不会受到影响。

由于WeaponsInterface类,您现在可以将任何派生类分配给基类指针WeaponsIterface,并且因为它的函数是虚拟的,所以当您调用WeaponsIInterface的shootTarget时,派生类shootTarget将被调用。

最好的部分是,您可以在运行时更改枪(w=g1和w=g2)。这是虚拟函数的主要优势,这也是我们需要虚拟函数的原因。

因此,在更换枪支时,不再需要在不同的地方注释代码。现在,这是一个简单而干净的过程,添加更多的枪类也更容易,因为我们只需要创建一个新的WeaponGun3或WeaponGun 4类,我们可以确信它不会破坏BattlefieldSoftware的代码或WeaponGun1/WeaponGun2的代码。