为什么C++需要虚函数?

我正在学习C++,我刚刚进入虚拟函数。

根据我所读到的(在书中和在线),虚函数是基类中的函数,您可以在派生类中覆盖它们。

但是在本书的前面,在学习基本继承时,我能够在不使用virtual的情况下覆盖派生类中的基函数。

那么我在这里错过了什么?我知道还有更多关于虚函数的内容,而且它似乎很重要,所以我想弄清楚它到底是什么。我只是在网上找不到直接的答案。

672359 次浏览

您至少需要1级继承和向上转换来演示它。这是一个非常简单的例子:

class Animal{public:// turn the following virtual modifier on/off to see what happens//virtualstd::string Says() { return "?"; }};
class Dog: public Animal{public: std::string Says() { return "Woof"; }};
void test(){Dog* d = new Dog();Animal* a = d;       // refer to Dog instance with Animal pointer
std::cout << d->Says();   // always Woofstd::cout << a->Says();   // Woof or ?, depends on virtual}

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

如果基类是Base,派生类是Der,你可以有一个Base *p指针,它实际上指向Der的实例。当你调用p->foo();时,如果fooDer1虚拟的,那么Base的版本会执行,忽略p实际上指向Der的事实。如果fooDer2虚拟,p->foo()执行foo的“最叶子”覆盖,充分考虑了指向项目的实际类。所以虚拟和非虚拟之间的区别实际上非常关键:前者允许运行时Der3,这是OO编程的核心概念,而后者不允许。

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

使用“虚拟”,你会得到“后期绑定”。使用哪个方法的实现在运行时根据指向对象的类型决定——它最初的构造方式。这不一定是你根据指向该对象的指针类型所想的。

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++中的早期和晚期绑定。

以下是我如何理解#0函数是什么,以及为什么需要它们:

假设你有这两个类:

class Animal{public:void eat() { std::cout << "I'm eating generic food."; }};
class Cat : public Animal{public:void eat() { std::cout << "I'm eating a rat."; }};

在您的主要功能中:

Animal *animal = new Animal;Cat *cat = new Cat;
animal->eat(); // Outputs: "I'm eating generic food."cat->eat();    // Outputs: "I'm eating a rat."

到目前为止还不错,对吧?动物吃普通食物,猫吃老鼠,都没有virtual

现在让我们稍微改变一下,以便通过中间函数调用eat()(对于这个例子来说是一个简单的函数):

// This can go at the top of the main.cpp filevoid func(Animal *xyz) { xyz->eat(); }

现在我们的主要功能是:

Animal *animal = new Animal;Cat *cat = new Cat;
func(animal); // Outputs: "I'm eating generic food."func(cat);    // Outputs: "I'm eating generic food."

啊哦……我们把一只猫传给了func(),但它不会吃老鼠。你应该超载func()所以需要Cat*吗?如果你必须从动物那里获得更多的动物,他们都需要自己的func()

解决方案是使Animal类中的eat()成为一个虚函数:

class Animal{public:virtual void eat() { std::cout << "I'm eating generic food."; }};
class Cat : public Animal{public:void eat() { std::cout << "I'm eating a rat."; }};

主要:

func(animal); // Outputs: "I'm eating generic food."func(cat);    // Outputs: "I'm eating a rat."

成交

当您在基类中有一个函数时,您可以在派生类中RedefineOverride它。

重新定义一个方法:在派生类中给出了基类方法的新实现。并不促进Dynamic binding

重写一个方法Redefining派生类中基类的virtual method。虚拟方法促进动态绑定

所以当你说:

但是在本书的前面,当学习基本继承时,我能够覆盖派生类中的基方法,而无需使用“虚拟”。

您没有覆盖它,因为基类中的方法不是虚拟的,而是重新定义它

C++形式化C程序员使用的一些编码技术,使用“叠加”替换“类”-具有公共标头部分的结构将用于处理不同类型的对象,但具有一些公共数据或操作。通常覆盖的基结构(公共部分)有一个指向函数表的指针,该表指向每个对象类型的不同例程集。C++做同样的事情,但隐藏了机制,即C++ptr->func(...),其中func是虚拟的,因为C将是(*ptr->func_table[func_num])(ptr,...),派生类之间的变化是func_table内容。

这样做的结果是,你只需要理解基类就可以调用派生类的方法,即如果一个例程理解类A,你可以传递给它一个派生类B指针,那么调用的虚拟方法将是B而不是A的方法,因为你通过函数表B指向。

您需要安全下浇注简单简洁的虚拟方法。

这就是虚拟方法所做的:它们安全地向下转换,使用明显简单而简洁的代码,避免在更复杂和冗长的代码中进行不安全的手动转换。


非虚方法静态绑定========================================

以下代码是故意“不正确”的。它没有将value方法声明为virtual,因此会产生意想不到的“错误”结果,即0:

#include <iostream>using namespace std;
class Expression{public:auto value() const-> double{ return 0.0; }         // This should never be invoked, really.};
class Number: public Expression{private:double  number_;    
public:auto value() const-> double{ return number_; }     // This is OK.
Number( double const number ): Expression(), number_( number ){}};
class Sum: public Expression{private:Expression const*   a_;Expression const*   b_;    
public:auto value() const-> double{ return a_->value() + b_->value(); }       // Uhm, bad! Very bad!
Sum( Expression const* const a, Expression const* const b ): Expression(), a_( a ), b_( b ){}};
auto main() -> int{Number const    a( 3.14 );Number const    b( 2.72 );Number const    c( 1.0 );
Sum const       sum_ab( &a, &b );Sum const       sum( &sum_ab, &c );    
cout << sum.value() << endl;}

在注释为“bad”的行中,调用了Expression::value方法,因为静态已知类型(编译时已知的类型)是Expression,而value方法不是虚拟的。


虚拟方法,动态绑定。======================================

在静态已知类型Expression中将value声明为#1可确保每次调用都将检查这是什么实际类型的对象,并为此动态类型调用value的相关实现:

#include <iostream>using namespace std;
class Expression{public:virtualauto value() const -> double= 0;};
class Number: public Expression{private:double  number_;    
public:auto value() const -> doubleoverride{ return number_; }
Number( double const number ): Expression(), number_( number ){}};
class Sum: public Expression{private:Expression const*   a_;Expression const*   b_;    
public:auto value() const -> doubleoverride{ return a_->value() + b_->value(); }    // Dynamic binding, OK!
Sum( Expression const* const a, Expression const* const b ): Expression(), a_( a ), b_( b ){}};
auto main() -> int{Number const    a( 3.14 );Number const    b( 2.72 );Number const    c( 1.0 );
Sum const       sum_ab( &a, &b );Sum const       sum( &sum_ab, &c );    
cout << sum.value() << endl;}

这里的输出应该是6.86,因为虚拟方法是称为虚拟。这也称为调用的动态绑定。执行一点检查,找到对象的实际动态类型,并调用该动态类型的相关方法实现。

相关实现是最具体(派生最多)类中的实现。

请注意,这里派生类中的方法实现没有标记为virtual,而是标记为#1。它们可以标记为virtual,但它们自动是虚拟的。override关键字确保如果某些基类中有没有这样的虚拟方法,那么您会得到错误(这是可取的)。


没有虚拟方法做这件事的丑陋之处================================================

如果没有virtual,就必须实现一些自己动手吧版本的动态绑定。这通常涉及不安全的手动向下转换、复杂性和冗长。

对于单个函数的情况,就像这里一样,在对象中存储一个函数指针并通过该函数指针调用就足够了,但即便如此,它也涉及一些不安全的下播、复杂性和冗长性,即:

#include <iostream>using namespace std;
class Expression{protected:typedef auto Value_func( Expression const* ) -> double;
Value_func* value_func_;
public:auto value() const-> double{ return value_func_( this ); }    
Expression(): value_func_( nullptr ) {}     // Like a pure virtual.};
class Number: public Expression{private:double  number_;    
staticauto specific_value_func( Expression const* expr )-> double{ return static_cast<Number const*>( expr )->number_; }
public:Number( double const number ): Expression(), number_( number ){ value_func_ = &Number::specific_value_func; }};
class Sum: public Expression{private:Expression const*   a_;Expression const*   b_;    
staticauto specific_value_func( Expression const* expr )-> double{auto const p_self  = static_cast<Sum const*>( expr );return p_self->a_->value() + p_self->b_->value();}
public:Sum( Expression const* const a, Expression const* const b ): Expression(), a_( a ), b_( b ){ value_func_ = &Sum::specific_value_func; }};

auto main() -> int{Number const    a( 3.14 );Number const    b( 2.72 );Number const    c( 1.0 );
Sum const       sum_ab( &a, &b );Sum const       sum( &sum_ab, &c );    
cout << sum.value() << endl;}

看待这一点的一个积极方法是,如果您遇到上述不安全的下播、复杂性和冗长,那么通常一个或多个虚拟方法可以真正提供帮助。

需要虚拟功能解释[易于理解]

#include<iostream>
using namespace std;
class A{public:void show(){cout << " Hello from Class A";}};
class B :public A{public:void show(){cout << " Hello from Class B";}};

int main(){
A *a1 = new B; // Create a base class pointer and assign address of derived object.a1->show();
}

输出将是:

Hello from Class A.

但具有虚拟功能:

#include<iostream>
using namespace std;
class A{public:virtual void show(){cout << " Hello from Class A";}};
class B :public A{public:virtual void show(){cout << " Hello from Class B";}};

int main(){
A *a1 = new B;a1->show();
}

输出将是:

Hello from Class B.

因此,使用虚拟函数,您可以实现运行时多态性。

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

“这种虚拟调用机制几乎可以和“普通函数调用”机制一样高效(在25%以内)。它的空间开销是具有虚拟函数的类的每个对象中的一个指针,加上每个此类的一个vtbl”[参观C++ by Bjarne Stroustrup]

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

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

这些方法留给接口用户来实现。它们对于必须继承IUnname的某些对象的创建和销毁至关重要。在这种情况下,运行时知道这三个方法,并期望它们在调用它们时被实现。因此,从某种意义上说,它们充当对象本身和任何使用该对象的东西之间的契约。

关键字虚拟告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。编译器将该类的虚函数的地址放在VTABLE中。在每个有虚函数的类中,编译器都会秘密放置一个指向该对象的VTABLE的指针,称为虚指针(简称VPTR)。当您通过基类指针进行虚函数调用时,编译器会悄悄插入代码以获取VPTR并在VTABLE中查找函数地址,从而调用正确的函数并导致延迟绑定发生。

此链接中的更多详细信息http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html

为什么在C++中需要虚拟方法?

快速解答:

  1. 它为我们提供了面向对象程序设计所需的“成分”之一。

在Bjarne StroustrupC++编程:原理与实践(14.3)中:

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

  1. 如果您需要虚函数调用2,它是最快更有效的实现。

要处理虚拟调用,需要一个或多个与派生对象3相关的数据。通常的方法是添加函数表的地址。该表通常称为虚拟表虚函数表,其地址通常称为虚指针。每个虚拟函数在虚拟表中获得一个槽。根据调用者的对象(派生)类型,虚拟函数依次调用各自的覆盖。


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

2.你不能在运行时使用其他语言特性来选择更快或更少的内存来编写功能。Bjarne StroustrupC++编程:原理和实践。(14.3.1)

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

虚拟函数用于支持运行时多态性

也就是说,虚拟关键字告诉编译器不要在编译时做出(函数绑定的)决定,而是将其推迟到运行时”

  • 您可以通过在基类声明中的关键字virtual之前使函数成为虚拟的。例如,

    class Base{virtual void func();}
  • 基类有一个虚成员函数时,任何从Base Class继承的类都可以重新定义完全相同的原型的函数,即只能重新定义功能,不能重新定义函数的接口。

    class Derive : public Base{void func();}
  • Base类指针可用于指向Base类对象以及派生类对象。

  • 当使用Base类指针调用虚函数时,编译器在运行时决定调用函数的哪个版本——即Base类版本或被覆盖的派生类版本。这称为运行时多态性

我想添加另一个使用虚拟函数,虽然它使用与上述答案相同的概念,但我想它值得一提。

虚拟破坏者

考虑下面的程序,而不将Base类析构函数声明为虚拟;Cat的内存可能无法清理。

class Animal {public:~Animal() {cout << "Deleting an Animal" << endl;}};class Cat:public Animal {public:~Cat() {cout << "Deleting an Animal name Cat" << endl;}};
int main() {Animal *a = new Cat();delete a;return 0;}

输出:

Deleting an Animal
class Animal {public:virtual ~Animal() {cout << "Deleting an Animal" << endl;}};class Cat:public Animal {public:~Cat(){cout << "Deleting an Animal name Cat" << endl;}};
int main() {Animal *a = new Cat();delete a;return 0;}

输出:

Deleting an Animal name CatDeleting an Animal

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

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

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

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

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

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

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

为什么我们需要虚拟函数?

虚函数避免了不必要的类型转换问题,我们中的一些人可能会争论,当我们可以使用派生类指针来调用派生类中特定的函数时,为什么我们需要虚函数!答案是-它取消了大型系统开发中继承的整个想法,其中具有单个指针基类对象是非常需要的。

让我们比较下面两个简单的程序来理解虚函数的重要性:

不带虚函数的程序:

#include <iostream>using namespace std;
class father{public: void get_age() {cout << "Fathers age is 50 years" << endl;}};
class son: public father{public : void get_age() { cout << "son`s age is 26 years" << endl;}};
int main(){father *p_father = new father;son *p_son = new son;
p_father->get_age();p_father = p_son;p_father->get_age();p_son->get_age();return 0;}

输出:

Fathers age is 50 yearsFathers age is 50 yearsson`s age is 26 years

具有虚拟功能的程序:

#include <iostream>using namespace std;
class father{public:virtual void get_age() {cout << "Fathers age is 50 years" << endl;}};
class son: public father{public : void get_age() { cout << "son`s age is 26 years" << endl;}};
int main(){father *p_father = new father;son *p_son = new son;
p_father->get_age();p_father = p_son;p_father->get_age();p_son->get_age();return 0;}

输出:

Fathers age is 50 yearsson`s age is 26 yearsson`s age is 26 years

通过仔细分析这两个输出,可以理解虚函数的重要性。

我以对话的形式回答,以便更好地阅读:


为什么我们需要虚拟函数?

由于多态性。

什么是多态性?

基指针也可以指向派生类型对象的事实。

这种多态的定义如何导致对虚函数的需求?

通过早期绑定

什么是早期绑定?

C++中的早期绑定(编译时绑定)意味着函数调用在程序执行之前是固定的。

那…?

因此,如果您使用基类型作为函数的参数,编译器将只识别基接口,如果您使用派生类中的任何参数调用该函数,它会被切掉,这不是您想要发生的。

如果这不是我们想要的,为什么这是允许的?

因为我们需要多态性!

那么多态性的好处是什么呢?

您可以使用基类型指针作为单个函数的参数,然后在程序的运行时,您可以使用取消引用该单个基指针来访问每个派生类型接口(例如它们的成员函数)而不会出现任何问题。

我还是不知道虚拟功能有什么好处……!这是我的第一个问题!

那是因为你问的太快了!

为什么我们需要虚拟函数?

假设你用基指针调用了一个函数,它有一个来自其派生类之一的对象的地址。正如我们上面讨论的,在运行时,这个指针被取消引用,到目前为止还不错,然而,我们期望“来自我们的派生类”的方法(==一个成员函数)被执行!然而,一个相同的方法(一个具有相同标头的方法)已经在基类中定义了,那么你的程序为什么要费心选择另一个方法呢?换句话说,我的意思是,你怎么能把这种情况与我们以前通常看到的情况区分开来呢?

简单的答案是“base中的虚拟成员函数”,更长的答案是,“在这一步,如果程序在基类中看到一个虚函数,它知道(意识到)您正在尝试使用多态”,因此转到派生类(使用v表,一种后期绑定形式)以找到具有相同标头的另一种方法,但具有不同的实现。

为什么是不同的实现?

你这个傻瓜!去读0号吧!

好吧,等等,等等,当一个人可以简单地使用派生类型指针时,他/她为什么要费心使用基指针呢?你来判断,所有这些头痛值得吗?看看这两个片段:

//1:

Parent* p1 = &boy;p1 -> task();Parent* p2 = &girl;p2 -> task();

//2:

Boy* p1 = &boy;p1 -> task();Girl* p2 = &girl;p2 -> task();

好吧,虽然我认为1仍然比2好,但你也可以这样写1

//1:

Parent* p1 = &boy;p1 -> task();p1 = &girl;p1 -> task();

此外,你应该意识到,这只是我迄今为止向你解释的所有事情的人为使用。相反,假设你的程序中有一个函数分别使用每个派生类的方法(getMonth福利()):

double totalMonthBenefit = 0;std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};for(CentralShop* x : mainShop){totalMonthBenefit += x -> getMonthBenefit();}

现在,试着重写这个,没有任何头痛!

double totalMonthBenefit=0;Shop1* branch1 = &shop1;Shop2* branch2 = &shop2;Shop3* branch3 = &shop3;Shop4* branch4 = &shop4;Shop5* branch5 = &shop5;Shop6* branch6 = &shop6;totalMonthBenefit += branch1 -> getMonthBenefit();totalMonthBenefit += branch2 -> getMonthBenefit();totalMonthBenefit += branch3 -> getMonthBenefit();totalMonthBenefit += branch4 -> getMonthBenefit();totalMonthBenefit += branch5 -> getMonthBenefit();totalMonthBenefit += branch6 -> getMonthBenefit();

实际上,这也可能是一个人为的例子!

下面是一个完整的示例,说明了为什么使用虚拟方法。

#include <iostream>
using namespace std;
class Basic{public:virtual void Test1(){cout << "Test1 from Basic." << endl;}virtual ~Basic(){};};class VariantA : public Basic{public:void Test1(){cout << "Test1 from VariantA." << endl;}};class VariantB : public Basic{public:void Test1(){cout << "Test1 from VariantB." << endl;}};
int main(){Basic *object;VariantA *vobjectA = new VariantA();VariantB *vobjectB = new VariantB();
object=(Basic *) vobjectA;object->Test1();
object=(Basic *) vobjectB;object->Test1();
delete vobjectA;delete vobjectB;return 0;}

我认为你指的是这样一个事实,一旦一个方法被声明为虚拟,你就不需要在覆盖中使用“虚拟”关键字。

class Base { virtual void foo(); };
class Derived : Base{void foo(); // this is overriding Base::foo};

如果你在Base的foo声明中不使用“虚拟”,那么Derive的foo只会隐藏它。

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

#include        <iostream>#include        <string>
using   namespace       std;
class   Animal{public:#ifdef  VIRTUALvirtual string  says()  {       return  "??";   }#elsestring  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;}

两个不同的结果是:

没有#定义虚拟,它在编译时绑定。动物*ad和func(动物*)都指向动物的say()方法。

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

#定义虚拟,它在运行时绑定。Dog*d、动物*ad和func(动物*)指向/引用Dog的说到()方法,因为Dog是他们的对象类型。除非[Dog's说到()"woof"]方法未定义,否则它将是在类树中首先搜索的方法,即派生类可能会覆盖其基类[动物说()]的方法。

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

有趣的是,Python实际上是虚拟的中的所有类属性(数据和方法)。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字虚拟。以下是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      woofAnimal dog ad says  wooffunc(a) :       ??func(d) :       wooffunc(ad):       woof

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

OOP答案:亚型多态性

在C++,需要虚拟方法来实现多态,如果你应用维基百科的定义,更准确地说是分型亚型多态性

维基百科, Subtyping,2019-01-09:在编程语言理论中,子类型(也称为子类型多态性或包含多态性)是类型多态性的一种形式,其中子类型是通过某种可替代性概念与另一个数据类型(超类型)相关的数据类型,这意味着编写对超型的元素进行操作的程序元素(通常是子程序或函数)也可以对子类型的元素进行操作。

注意:Subtype表示基类,subtyp表示继承的类。

阅读更多关于亚型多态性

技术答案:动态调度

如果你有一个指向基类的指针,那么方法的调用(被声明为虚拟)将被分派到创建对象的实际类的方法。这就是实现亚型多态性的C++。

进一步阅读C++和动态调度中的多态性

实现答案:创建vtable条目

对于方法上的每个修饰符“虚拟”,C++编译器通常会在声明方法的类的vtable中创建一个条目。这就是编译器实现动态调度C++常见方式。

进一步阅读vtable


示例代码

#include <iostream>
using namespace std;
class Animal {public:virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classesvirtual ~Animal(){};};
class Cat : public Animal {public:virtual void MakeTypicalNoise(){cout << "Meow!" << endl;}};
class Dog : public Animal {public:virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogscout << "Woof!" << endl;}};
class Doberman : public Dog {public:virtual void MakeTypicalNoise() {cout << "Woo, woo, woow!";cout << " ... ";Dog::MakeTypicalNoise();}};
int main() {
Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };
const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);for ( int i = 0; i < cnAnimals; i++ ) {apObject[i]->MakeTypicalNoise();}for ( int i = 0; i < cnAnimals; i++ ) {delete apObject[i];}return 0;}

输出示例代码

Meow!Woof!Woo, woo, woow! ... Woof!

代码示例的UML类图

代码示例的UML类图

你熟悉函数指针吗?虚函数是一个类似的想法,除了你可以很容易地将数据绑定到虚函数(作为类成员)。将数据绑定到函数指针并不那么容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为…多态!”

底线是虚函数让生活更轻松。让我们使用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 objectb->helloWorld(); // Hello World!void(derived::*hwBase)() = &derived::helloWorld; //create derived memberfunction pointer to base functionderived* d = new derived(); //Create derived object.d->displayHWDerived(hwBase); //Greetings World!
char ch;cin >> ch;}

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

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

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

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

考虑这个战场应用程序:
输入图片描述

#include "iostream"
//This class is created by Gun1's companyclass Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};//This class is created by Gun2's companyclass Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};
//We create an abstract class to interface with WeaponControllerclass WeaponsInterface {public:virtual void shootTarget() = 0;};
//A wrapper class to encapsulate Gun1's shooting functionclass 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 functionclass 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 dynamicallyswitch(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类,您现在可以将任何派生类分配给基类指针WeaponsInterface,并且因为它的函数是虚拟的,当您调用WeaponsInterfaceshootTarget时,将调用派生类shootTarget

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

这是一个简单干净的过程,添加更多gun类也更容易,因为我们只需要创建一个新的WeaponGun3WeaponGun4类,我们可以确信它不会弄乱BattlefieldSoftwareWeaponGun1/WeaponGun2的代码。

根据@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;}

产出

ParentChildParent

因此,我们需要一种方法来告诉编译器要调用的函数应该在运行时而不是编译时确定。这就是虚拟关键字的作用。

这就是为什么函数重载被称为编译时多态(或早期绑定),而虚函数重写被称为运行时多态(或后期绑定)。

详情:

在内部,当编译器看到一个虚函数时,它会使用.*->*运算符创建一个类成员指针,该指针通常指向类的一个成员(而不是对象中该成员的特定实例)。它们的工作是允许你在给定指向该成员的指针的情况下访问类的一个成员。程序员很少直接使用这些(也许除非你正在编写编译器来实现“虚拟”)。