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

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

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

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


当前回答

virtual关键字强制编译器选择对象类中定义的方法实现,而不是指针类中的方法实现。

Shape *shape = new Triangle(); 
cout << shape->getName();

在上面的示例中,默认情况下将调用Shape::getName,除非getName()在基类Shape中定义为virtual。这迫使编译器在Triangle类而不是Shape类中查找getName()实现。

虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也被称为动态调度,并且存在一些与之相关的开销。

最后,为什么在C++中甚至需要虚拟,为什么不将其作为Java中的默认行为?

C++基于“零开销”和“按需付费”的原则。因此,除非您需要,否则它不会尝试为您执行动态调度。为界面提供更多控制。通过使函数非虚拟化,接口/抽象类可以控制其所有实现中的行为。

其他回答

底线是,虚拟功能使生活更轻松。让我们使用M Perry的一些想法,并描述如果我们没有虚拟函数而只能使用成员函数指针会发生什么。在没有虚函数的正常估计中,我们有:

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

好的,这就是我们所知道的。现在让我们尝试使用成员函数指针:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

虽然我们可以用成员函数指针做一些事情,但它们不如虚拟函数灵活。在类中使用成员函数指针是很棘手的;至少在我的实践中,成员函数指针几乎总是必须在主函数中或从成员函数中调用,如上面的示例所示。

另一方面,虚拟函数虽然可能有一些函数指针开销,但确实大大简化了事情。

EDIT:还有一种方法与eddietree类似:c++虚拟函数与成员函数指针(性能比较)。

关于效率,虚拟函数的效率略低于早期绑定函数。

“这种虚拟调用机制的效率几乎可以与“正常函数调用”机制一样高(25%以内)。它的空间开销是一个具有虚拟函数的类的每个对象中的一个指针加上每个此类类的一个vtbl”[Bjarne Stroustrup的C++教程]

virtual关键字强制编译器选择对象类中定义的方法实现,而不是指针类中的方法实现。

Shape *shape = new Triangle(); 
cout << shape->getName();

在上面的示例中,默认情况下将调用Shape::getName,除非getName()在基类Shape中定义为virtual。这迫使编译器在Triangle类而不是Shape类中查找getName()实现。

虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也被称为动态调度,并且存在一些与之相关的开销。

最后,为什么在C++中甚至需要虚拟,为什么不将其作为Java中的默认行为?

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对象相同。

您必须区分重写和重载。如果没有virtual关键字,则只能重载基类的方法。这只意味着隐藏。假设您有一个基类base和一个派生类Specialized,它们都实现了void foo()。现在有一个指向Base的指针指向Specialized的实例。当您对其调用foo()时,您可以观察到virtual的不同之处:如果该方法是虚拟的,则将使用Specialized的实现,如果缺少,则将选择Base的版本。最好不要重载基类中的方法。使方法非虚拟化是作者告诉你它在子类中的扩展不是有意的。