为什么在C++中我们需要虚函数?

1637
从我所了解的来看,虚函数是基类中的函数,你可以在其派生类中进行重写。
但是早些时候,在学习基本继承时,我能够在派生类中重写基类函数而不使用virtual
我在这里漏掉了什么?我知道虚函数还有更多内容,而且似乎很重要,所以我想明确一下它到底是什么。

9
这可能是虚函数最大的好处——在不修改旧代码的情况下,能够以一种结构化的方式编写代码,使新派生类自动与旧代码兼容! - user3530616
说实话,虚函数是面向对象编程(OOP)的基本特性,用于类型擦除。我认为,非虚方法才是Object Pascal和C++的特殊之处,它们优化了不必要的大型虚函数表(vtable),并允许POD兼容类。许多OOP语言期望_每个_方法都可以被重写。 - Swift - Friday Pie
1
这是一个好问题。事实上,在其他语言(如Java或PHP)中,C++中的虚拟概念被抽象化了。在C++中,您只能获得更多控制权以处理一些罕见情况(请注意多重继承或DDOD的特殊情况)。但是为什么要在stackoverflow.com上发布这个问题呢? - ILCAI
1
我还没有找到比这个更好的教程:https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html - John David
@ILCAI 我不会称其为“抽象化”,而是默认为虚拟的。C++ 中默认情况下没有这个特性,是因为当不需要时它可能会降低性能,而 C++ 的一个原则是不必为你不使用的功能付出性能代价。 - Jimmy T.
显示剩余3条评论
28个回答

2
关于效率,虚函数比早期绑定的函数略微不那么高效。
“这种虚拟调用机制可以几乎与‘普通函数调用’机制一样高效(在25%以内)。它的空间开销是每个具有虚函数的类中一个指针加上每个这样的类的一个vtbl” [引自Bjarne Stroustrup的《C++之旅》]

2
晚期绑定不仅会使函数调用变慢,还会使被调用的函数在运行时变得未知,因此无法应用跨函数调用的优化。这可能会改变一切,例如在值传播删除大量代码的情况下(考虑 if(param1>param2) return cst;,编译器在某些情况下可以将整个函数调用减少为常数)。 - curiousguy

2

虚拟方法被用于接口设计中。例如,在Windows中有一个名为IUnknown的接口,如下所示:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

这些方法需要接口用户自行实现。它们对于创建和销毁某些必须继承IUnknown的对象至关重要。在这种情况下,运行时环境知道这三个方法,并期望在调用它们时将它们实现。因此,在某种意义上,它们充当对象本身和使用该对象的任何内容之间的合同。


运行时知道这三种方法并期望它们被实现。由于它们是纯虚函数,因此无法创建IUnknown的实例,因此所有子类必须实现所有这些方法才能编译。没有不实现它们然后在运行时发现的危险(但显然可以错误地实现它们!)。今天我学到了Windows使用单词“interface”作为宏定义,可能是因为他们的用户不能(A)看到名称中的前缀“I”,或者(B)查看类以查看它是一个接口。唉 - underscore_d

1
这是前两个答案的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的says() "woof"]方法未定义,否则它将首先在类树中搜索,即派生类可以重写其基类[Animal的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++的虚拟定义相同。请注意,dad是两个不同的指针变量,引用/指向同一个Dog实例。表达式(ad is d)返回True,它们的值相同<main.Dog object at 0xb79f72cc>。


1
你是否熟悉函数指针?虚函数是类似的概念,但你可以轻松地将数据绑定到虚函数(作为类成员)。将数据绑定到函数指针不太容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为...多态性!”

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

0

就我对这个问题的理解,它问的是为什么C++需要一个虚拟关键字。

因为编译器可能无法在编译阶段确定要调用哪个实例的方法。

以下代码提供了一个示例:

#include <iostream>
using namespace std;

class Animal {
   public:
    virtual void Say() { cout << "Im animal"; }
};

class Cat : public Animal {
   public:
    void Say() { cout << "Im cat"; }
};

class Dog : public Animal {
   public:
    void Say() { cout << "Im dog"; }
};

Animal* NewAnimal() {
    int v = 1;
    // The input is totally unpredictable.
    cin >> v;
    switch (v) {
        case 1:
            return new Cat();
        case 2:
            return new Dog();
        default:
            return new Animal();
    }
}

int main(void) {
    auto x = NewAnimal();
    // Compiler can't determine what x is (a dog or a cat, or some animal else)
    // in compiling stage. So, to call which Say function, is runtime related.
    // That's to say, the Say function requires dynamically binding.
    // What the keyword virtual does, is to tell the compiler the Say function
    // should be determined at runtime stage, but not compiling stage.
    x->Say();
    return 0;
}

哪种动物的叫声是完全不可预测的。 我们必须在运行时动态决定使用哪一个。


0

归根结底,虚函数使生活更轻松。让我们借鉴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;
 }

虽然我们可以使用成员函数指针来完成一些任务,但它们并不像虚函数那样灵活。在类中使用成员函数指针是有技巧的;至少在我的实践中,几乎总是必须在主函数或从成员函数内部调用成员函数指针,就像上面的例子一样。

另一方面,虚函数虽然可能会带来一些函数指针的开销,但却极大地简化了事情。

编辑:还有一种类似的方法,由eddietree提供:c++虚函数与成员函数指针(性能比较)


-2

继续@user6359267的回答,C++的作用域层次结构是

global -> namespace -> class -> local -> statement

因此,每个类都定义了一个作用域。如果不是这样的话,在子类中重写的函数实际上会重新定义同一作用域中的函数,而链接器不允许这样做:
  1. 每个翻译单元中使用函数之前必须声明该函数
  2. 在整个程序(所有翻译单元)中,给定作用域内的函数只能定义一次
由于每个类都定义了自己的作用域,所以调用函数的对象所属类中定义的函数将被调用。因此,
#include <iostream>
#include <string>

class Parent
{
public:
    std::string GetName() { return "Parent"; }
};

class Child : public Parent
{
public:
    std:::string GetName() { return "Child"; }
};

int main()
{
    Parent* parent = new Parent();
    std::cout << parent->GetName() << std::endl;

    Child* child = new Child();
    std::cout << child->GetName() << std::endl;

    *parent = child;
    std::cout << child->GetName() << std::endl;

    return 0;
}

输出

Parent
Child
Parent

因此,我们需要一种方法来告诉编译器,在运行时而不是编译时确定要调用的函数。这就是虚拟关键字的作用。
这就是为什么函数重载被称为编译时多态(或早期绑定),而虚函数重写被称为运行时多态(或晚期绑定)的原因。
细节:
在内部,当编译器遇到虚函数时,它会创建一个类成员指针,该指针通用地指向类的成员(而不是指向对象中该成员的特定实例),使用.*和->*运算符。它们的作用是允许您在给定成员指针的情况下访问类的成员。这些很少直接由程序员使用(除非您正在编写一个实现"virtual"的编译器)。

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