Warum brauchen wir virtuelle Funktionen in C ++?

1312

Ich lerne C ++ und steige gerade in virtuelle Funktionen ein.

Nach dem, was ich gelesen habe (im Buch und online), sind virtuelle Funktionen Funktionen in der Basisklasse, die Sie in abgeleiteten Klassen überschreiben können.

Aber früher in diesem Buch konnte ich beim Erlernen der grundlegenden Vererbung Basisfunktionen in abgeleiteten Klassen überschreiben, ohne sie zu verwenden virtual.

Was vermisse ich hier? Ich weiß, dass virtuelle Funktionen mehr beinhalten, und es scheint wichtig zu sein, deshalb möchte ich klarstellen, was es genau ist. Ich kann online einfach keine klare Antwort finden.

Jake Wilson
quelle
13
Ich habe hier eine praktische Erklärung für virtuelle Funktionen erstellt: nrecursions.blogspot.in/2015/06/…
Nav
4
Dies ist vielleicht der größte Vorteil virtueller Funktionen - die Möglichkeit, Ihren Code so zu strukturieren, dass neu abgeleitete Klassen automatisch ohne Änderung mit dem alten Code arbeiten!
user3530616
tbh, virtuelle Funktionen sind das Hauptmerkmal von OOP zum Löschen von Typen. Ich denke, es sind nicht virtuelle Methoden, die Object Pascal und C ++ zu etwas Besonderem machen. Sie optimieren unnötige große vtables und ermöglichen POD-kompatible Klassen. Viele OOP-Sprachen erwarten, dass jede Methode überschrieben werden kann.
Swift - Friday Pie
Das ist eine gute Frage. In der Tat wird diese virtuelle Sache in C ++ in anderen Sprachen wie Java oder PHP abstrahiert. In C ++ erhalten Sie in einigen seltenen Fällen nur ein wenig mehr Kontrolle (beachten Sie die Mehrfachvererbung oder den Sonderfall des DDOD ). Aber warum wird diese Frage auf stackoverflow.com veröffentlicht?
Edgar Alloro
Ich denke, wenn Sie sich die frühe Bindung, die späte Bindung und VTABLE ansehen, wäre dies vernünftiger und sinnvoller. Hier gibt es also eine gute Erklärung ( learncpp.com/cpp-tutorial/125-the-virtual-table ).
Ceyun

Antworten:

2729

So habe ich nicht nur verstanden, was virtualFunktionen sind, sondern warum sie benötigt werden:

Angenommen, Sie haben diese beiden Klassen:

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."; }
};

In Ihrer Hauptfunktion:

Animal *animal = new Animal;
Cat *cat = new Cat;

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

So weit so gut, oder? Tiere fressen generisches Futter, Katzen fressen Ratten, alles ohne virtual.

Lassen Sie es uns jetzt ein wenig ändern, damit es eat()über eine Zwischenfunktion aufgerufen wird (eine triviale Funktion nur für dieses Beispiel):

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

Jetzt ist unsere Hauptfunktion:

Animal *animal = new Animal;
Cat *cat = new Cat;

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

Oh oh ... wir haben eine Katze reingelegt func(), aber sie frisst keine Ratten. Sollten Sie überladen, func()damit es eine dauert Cat*? Wenn Sie mehr Tiere von Animal ableiten müssten, würden sie alle ihre eigenen brauchen func().

Die Lösung besteht darin, eat()aus der AnimalKlasse eine virtuelle Funktion zu machen:

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."; }
};

Main:

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

Erledigt.

M Perry
quelle
165
Wenn ich das richtig verstehe, erlaubt virtual den Aufruf der Unterklassenmethode, selbst wenn das Objekt als seine Oberklasse behandelt wird?
Kenny Worden
147
Anstatt die späte Bindung am Beispiel einer Zwischenfunktion "func" zu erklären, folgt hier eine einfachere Demonstration: Animal * animal = new Animal; // Cat * cat = new Cat; Tier * Katze = neue Katze; Tier-> essen (); // Ausgaben: "Ich esse generisches Essen." cat-> eat (); // Ausgaben: "Ich esse generisches Essen." Obwohl Sie das untergeordnete Objekt (Cat) zuweisen, basiert die aufgerufene Methode auf dem Zeigertyp (Animal) und nicht auf dem Objekttyp, auf den es zeigt. Deshalb brauchen Sie "virtuell".
Rexbelia
37
Bin ich der einzige, der dieses Standardverhalten in C ++ einfach komisch findet? Ich hätte erwartet, dass der Code ohne "virtuell" funktioniert.
David 天宇 Wong
20
@ David 天宇 Wong Ich denke virtual, es wird eine dynamische Bindung gegen statische eingeführt, und ja, es ist seltsam, wenn Sie aus Sprachen wie Java kommen.
Peterchaula
32
Erstens sind virtuelle Anrufe viel, viel teurer als reguläre Funktionsaufrufe. Die C ++ - Philosophie ist standardmäßig schnell, daher sind virtuelle Anrufe standardmäßig ein großes Nein-Nein. Der zweite Grund ist, dass virtuelle Aufrufe dazu führen können, dass Ihr Code beschädigt wird, wenn Sie eine Klasse von einer Bibliothek erben und die interne Implementierung einer öffentlichen oder privaten Methode (die eine virtuelle Methode intern aufruft) ändert, ohne das Verhalten der Basisklasse zu ändern.
Saolof
672

Ohne "virtuell" erhalten Sie "frühzeitige Bindung". Welche Implementierung der Methode verwendet wird, wird zur Kompilierungszeit basierend auf dem Typ des Zeigers, den Sie aufrufen, entschieden.

Mit "virtuell" erhalten Sie "späte Bindung". Welche Implementierung der Methode verwendet wird, wird zur Laufzeit anhand des Typs des Objekts festgelegt, auf das verwiesen wird - wie es ursprünglich konstruiert wurde. Dies ist nicht unbedingt das, was Sie basierend auf dem Typ des Zeigers denken würden, der auf dieses Objekt zeigt.

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* obj = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

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

BEARBEITEN - siehe diese Frage .

Außerdem behandelt dieses Tutorial die frühe und späte Bindung in C ++.

Steve314
quelle
11
Ausgezeichnet und kommt schnell und mit besseren Beispielen nach Hause. Dies ist jedoch simpel und der Fragesteller sollte wirklich nur die Seite parashift.com/c++-faq-lite/virtual-functions.html lesen . Andere Leute haben bereits in SO-Artikeln, die mit diesem Thread verlinkt sind, auf diese Ressource hingewiesen, aber ich glaube, dass dies eine Erwähnung wert ist.
Sonny
36
Ich weiß nicht, ob frühe und späte Bindung Begriffe sind, die speziell in der c ++ - Community verwendet werden, aber die richtigen Begriffe sind statische (zur Kompilierungszeit) und dynamische (zur Laufzeit) Bindung.
Mike
31
@mike - "Der Begriff" späte Bindung "stammt mindestens aus den 1960er Jahren, wo er in Communications of the ACM zu finden ist." . Wäre es nicht schön, wenn es für jedes Konzept ein richtiges Wort gäbe? Leider ist es nicht so. Die Begriffe "frühe Bindung" und "späte Bindung" stammen aus der Zeit vor C ++ und sogar der objektorientierten Programmierung und sind genauso korrekt wie die von Ihnen verwendeten Begriffe.
Steve314
4
@BJovke - Diese Antwort wurde geschrieben, bevor C ++ 11 veröffentlicht wurde. Trotzdem habe ich es einfach in GCC 6.3.0 (standardmäßig mit C ++ 14) ohne Probleme kompiliert - offensichtlich die Variablendeklaration verpackt und eine mainFunktion usw. aufgerufen. Zeiger-zu-abgeleitet wandelt implizit in Zeiger-zu-Basis um (spezialisierter impliziert implizit allgemeinere). Umgekehrt benötigen Sie eine explizite Besetzung, normalerweise a dynamic_cast. Alles andere - sehr anfällig für undefiniertes Verhalten. Stellen Sie also sicher, dass Sie wissen, was Sie tun. Nach meinem besten Wissen hat sich dies seit C ++ 98 nicht geändert.
Steve314
10
Beachten Sie, dass C ++ - Compiler heutzutage häufig spät in die frühe Bindung optimieren können - wenn sie sicher sein können, wie die Bindung aussehen wird. Dies wird auch als "De-Virtualisierung" bezeichnet.
Einpoklum
83

Sie benötigen mindestens 1 Vererbungsstufe und einen Downcast, um dies zu demonstrieren. Hier ist ein sehr einfaches Beispiel:

class Animal
{        
    public: 
      // turn the following virtual modifier on/off to see what happens
      //virtual   
      std::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 Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}
Henk Holterman
quelle
39
Ihr Beispiel besagt, dass die zurückgegebene Zeichenfolge davon abhängt, ob die Funktion virtuell ist, aber nicht, welches Ergebnis virtuell und welches nicht virtuell ist. Außerdem ist es etwas verwirrend, da Sie die zurückgegebene Zeichenfolge nicht verwenden.
Ross
7
Mit virtuellem Schlüsselwort: Woof . Ohne virtuelles Schlüsselwort : ? .
Hesham Eraqi
@HeshamEraqi ohne virtuell ist es früh bindend und es wird "?" der Basisklasse
Ahmad
46

Sie benötigen virtuelle Methoden für sicheres Downcasting , Einfachheit und Prägnanz .

Das ist, was virtuelle Methoden tun: Sie werden sicher mit scheinbar einfachem und präzisem Code heruntergestuft, um die unsicheren manuellen Umwandlungen in dem komplexeren und ausführlicheren Code zu vermeiden, den Sie sonst hätten.


Nicht virtuelle Methode ⇒ statische Bindung

Der folgende Code ist absichtlich "falsch". Es deklariert die valueMethode nicht als virtualund erzeugt daher ein unbeabsichtigtes "falsches" Ergebnis, nämlich 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;
}

In der als "schlecht" kommentierten Zeile wird die Expression::valueMethode aufgerufen, da der statisch bekannte Typ (der zur Kompilierungszeit bekannte Typ ) ist Expressionund die valueMethode nicht virtuell ist.


Virtuelle Methode ⇒ dynamische Bindung.

Durch Deklarieren valuewie virtualbeim statisch bekannten Typ wird Expressionsichergestellt, dass bei jedem Aufruf überprüft wird, um welchen tatsächlichen Objekttyp es sich handelt, und die entsprechende Implementierung valuefür diesen dynamischen Typ aufgerufen wird :

#include <iostream>
using namespace std;

class Expression
{
public:
    virtual
    auto value() const -> double
        = 0;
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const -> double
        override
    { return number_; }

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const -> double
        override
    { 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;
}

Hier ist die Ausgabe so, 6.86wie sie sein sollte, da die virtuelle Methode virtuell aufgerufen wird . Dies wird auch als dynamische Bindung der Anrufe bezeichnet. Es wird eine kleine Überprüfung durchgeführt, bei der der tatsächliche dynamische Objekttyp ermittelt und die entsprechende Methodenimplementierung für diesen dynamischen Typ aufgerufen wird.

Die relevante Implementierung ist die in der spezifischsten (am meisten abgeleiteten) Klasse.

Beachten Sie, dass Methodenimplementierungen in abgeleiteten Klassen hier nicht markiert sind virtual, sondern stattdessen override. Sie könnten markiert werden virtual, sind aber automatisch virtuell. Das overrideSchlüsselwort stellt sicher, dass, wenn es in einer Basisklasse keine solche virtuelle Methode gibt , eine Fehlermeldung angezeigt wird (was wünschenswert ist).


Die Hässlichkeit, dies ohne virtuelle Methoden zu tun

Ohne virtualmüsste man eine Do It Yourself- Version der dynamischen Bindung implementieren . Dies beinhaltet im Allgemeinen unsicheres manuelles Downcasting, Komplexität und Ausführlichkeit.

Für den Fall einer einzelnen Funktion, wie hier, reicht es aus, einen Funktionszeiger im Objekt zu speichern und über diesen Funktionszeiger aufzurufen, aber dennoch sind einige unsichere Downcasts, Komplexität und Ausführlichkeit erforderlich:

#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_;

    static
    auto 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_;

    static
    auto 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;
}

Eine positive Sichtweise ist, dass, wenn Sie wie oben auf unsicheres Downcasting, Komplexität und Ausführlichkeit stoßen, oft eine virtuelle Methode oder virtuelle Methoden wirklich helfen können.

Prost und hth. - Alf
quelle
40

Virtuelle Funktionen werden zur Unterstützung des Laufzeitpolymorphismus verwendet .

Das heißt, virtuelle sagt Schlüsselwort der Compiler nicht die Entscheidung zu treffen (der Funktion Bindung) bei der Kompilierung, sondern verschieben es für Runtime“ .

  • Sie können eine Funktion virtuell machen, indem Sie dem Schlüsselwort virtualin seiner Basisklassendeklaration vorangehen . Zum Beispiel,

     class Base
     {
        virtual void func();
     }
  • Wenn eine Basisklasse eine virtuelle Memberfunktion hat, kann jede Klasse, die erbt von der Basisklasse neu definiert die Funktion mit genau den gleichen Prototyp dh nur Funktionalität neu definiert werden, nicht die Schnittstelle der Funktion.

     class Derive : public Base
     {
        void func();
     }
  • Ein Basisklassenzeiger kann verwendet werden, um sowohl auf ein Basisklassenobjekt als auch auf ein abgeleitetes Klassenobjekt zu verweisen.

  • Wenn die virtuelle Funktion mithilfe eines Basisklassenzeigers aufgerufen wird, entscheidet der Compiler zur Laufzeit, welche Version der Funktion - dh die Basisklassenversion oder die überschriebene abgeleitete Klassenversion - aufgerufen werden soll. Dies wird als Laufzeitpolymorphismus bezeichnet .
Yoon5oo
quelle
34

Wenn die Basisklasse Baseund eine abgeleitete Klasse ist Der, können Sie einen Base *pZeiger haben, der tatsächlich auf eine Instanz von zeigt Der. Wenn Sie aufrufen p->foo();, wenn fooes nicht virtuell ist, wird Basedie Version davon ausgeführt, wobei die Tatsache ignoriert wird, die ptatsächlich auf a verweist Der. Wenn foo ist virtuell, p->foo()führt die „leafmost“ außer Kraft gesetzt foo, vollständig unter Berücksichtigung der tatsächlichen Klasse des spitzen-Punkt. So ist der Unterschied zwischen virtuellen und nicht virtuellen ist eigentlich ziemlich entscheidend: die frühere Laufzeit ermöglicht Polymorphismus , das Kernkonzept der OO - Programmierung, während letzteres nicht der Fall ist.

Alex Martelli
quelle
8
Ich hasse es, Ihnen zu widersprechen, aber Polymorphismus zur Kompilierungszeit ist immer noch Polymorphismus. Selbst das Überladen von Funktionen, die keine Mitglieder sind, ist eine Form des Polymorphismus - Ad-hoc-Polymorphismus unter Verwendung der Terminologie in Ihrem Link. Der Unterschied besteht hier zwischen früher und später Bindung.
Steve314
7
@ Steve314, du bist pedantisch korrekt (als Mitpedant stimme ich dem zu ;-) - bearbeite die Antwort, um das fehlende Adjektiv hinzuzufügen ;-).
Alex Martelli
26

Notwendigkeit einer virtuellen Funktion erklärt [Leicht zu verstehen]

#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();

}

Ausgabe wird sein:

Hello from Class A.

Aber mit virtueller Funktion:

#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();

}

Ausgabe wird sein:

Hello from Class B.

Daher können Sie mit der virtuellen Funktion einen Laufzeitpolymorphismus erzielen.

Ajay GU
quelle
25

Ich möchte eine weitere Verwendung der virtuellen Funktion hinzufügen, obwohl sie das gleiche Konzept wie die oben angegebenen Antworten verwendet, aber ich denke, es ist erwähnenswert.

VIRTUELLER ZERSTÖRER

Betrachten Sie dieses Programm unten, ohne den Destruktor der Basisklasse als virtuell zu deklarieren. Der Speicher für Cat wird möglicherweise nicht bereinigt.

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;
}

Ausgabe:

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;
}

Ausgabe:

Deleting an Animal name Cat
Deleting an Animal
Aryaman Gupta
quelle
11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.Es ist schlimmer als das. Das Löschen eines abgeleiteten Objekts über einen Basiszeiger / eine Referenz ist ein reines undefiniertes Verhalten. Es ist also nicht nur so, dass Speicherplatz verloren geht. Vielmehr ist das Programm schlecht geformt, so dass der Compiler es in alles umwandeln kann: Maschinencode, der zufällig gut funktioniert oder nichts tut, oder Dämonen aus der Nase beschwört oder so weiter. Deshalb, wenn ein Programm so entworfen ist Damit ein Benutzer eine abgeleitete Instanz über eine Basisreferenz löschen kann, muss die Basis einen virtuellen Destruktor haben
underscore_d
21

Sie müssen zwischen Überschreiben und Überladen unterscheiden. Ohne das virtualSchlüsselwort überladen Sie nur eine Methode einer Basisklasse. Das bedeutet nichts als sich zu verstecken. Angenommen, Sie haben eine Basisklasse Baseund eine abgeleitete Klasse, Specializeddie beide implementieren void foo(). Jetzt haben Sie einen Zeiger Baseauf eine Instanz von Specialized. Wenn Sie es aufrufen, foo()können Sie den Unterschied beobachten, der Folgendes virtualausmacht: Wenn die Methode virtuell ist, wird die Implementierung von Specializedverwendet. Wenn sie fehlt, wird die Version von Baseausgewählt. Es wird empfohlen, Methoden aus einer Basisklasse niemals zu überladen. Wenn eine Methode nicht virtuell ist, kann der Autor Ihnen mitteilen, dass ihre Erweiterung in Unterklassen nicht beabsichtigt ist.

h0b0
quelle
3
Ohne virtualSie nicht überladen. Du beschattest . Wenn eine Basisklasse Bhat eine oder mehr Funktionen foo, und die abgeleitete Klasse Ddefiniert einen fooNamen, das foo Häuten aller diese fooes in B. Sie werden B::foomit Scope Resolution erreicht. Um B::fooFunktionen Dzum Überladen zu fördern , müssen Sie verwenden using B::foo.
Kaz
20

Warum brauchen wir virtuelle Methoden in C ++?

Schnelle Antwort:

  1. Es liefert uns eine der benötigten "Zutaten" 1 für die objektorientierte Programmierung .

In Bjarne Stroustrup C ++ - Programmierung: Prinzipien und Praxis, (14.3):

Die virtuelle Funktion bietet die Möglichkeit, eine Funktion in einer Basisklasse zu definieren und eine Funktion mit demselben Namen und Typ in einer abgeleiteten Klasse zu haben, die aufgerufen wird, wenn ein Benutzer die Basisklassenfunktion aufruft. Dies wird häufig als Laufzeitpolymorphismus , dynamischer Versand oder Laufzeitversand bezeichnet, da die aufgerufene Funktion zur Laufzeit basierend auf dem Typ des verwendeten Objekts bestimmt wird.

  1. Es ist die schnellste und effizientere Implementierung, wenn Sie einen virtuellen Funktionsaufruf 2 benötigen .

Um einen virtuellen Anruf zu bearbeiten, benötigt man ein oder mehrere Daten, die sich auf das abgeleitete Objekt 3 beziehen . In der Regel wird die Adresse der Funktionstabelle hinzugefügt. Diese Tabelle wird normalerweise als virtuelle Tabelle oder virtuelle Funktionstabelle bezeichnet und ihre Adresse wird häufig als virtueller Zeiger bezeichnet . Jede virtuelle Funktion erhält einen Steckplatz in der virtuellen Tabelle. Abhängig vom Objekttyp (abgeleitet) des Aufrufers ruft die virtuelle Funktion ihrerseits die entsprechende Überschreibung auf.


1. Die Verwendung von Vererbung, Laufzeitpolymorphismus und Kapselung ist die häufigste Definition der objektorientierten Programmierung .

2. Sie können die Funktionalität nicht so codieren, dass sie schneller ist oder weniger Speicher verwendet, indem Sie andere Sprachfunktionen verwenden, um zur Laufzeit zwischen Alternativen auszuwählen. Bjarne Stroustrup C ++ Programmierung: Prinzipien und Praxis (14.3.1) .

3. Etwas zu sagen, welche Funktion wirklich aufgerufen wird, wenn wir die Basisklasse aufrufen, die die virtuelle Funktion enthält.

Ziezi
quelle
15

Ich habe meine Antwort in Form eines Gesprächs, um sie besser lesen zu können:


Warum brauchen wir virtuelle Funktionen?

Wegen des Polymorphismus.

Was ist Polymorphismus?

Die Tatsache, dass ein Basiszeiger auch auf abgeleitete Typobjekte verweisen kann.

Wie führt diese Definition des Polymorphismus zur Notwendigkeit virtueller Funktionen?

Nun, durch frühes Binden .

Was ist frühe Bindung?

Frühe Bindung (Bindung zur Kompilierungszeit) in C ++ bedeutet, dass ein Funktionsaufruf behoben wird, bevor das Programm ausgeführt wird.

Damit...?

Wenn Sie also einen Basistyp als Parameter einer Funktion verwenden, erkennt der Compiler nur die Basisschnittstelle. Wenn Sie diese Funktion mit Argumenten aus abgeleiteten Klassen aufrufen, wird sie abgeschnitten, was nicht der Fall ist.

Wenn es nicht das ist, was wir wollen, warum ist das erlaubt?

Weil wir Polymorphismus brauchen!

Was ist dann der Vorteil des Polymorphismus?

Sie können einen Basistypzeiger als Parameter einer einzelnen Funktion verwenden und dann zur Laufzeit Ihres Programms ohne Probleme auf jede der abgeleiteten Typschnittstellen (z. B. deren Elementfunktionen) zugreifen, indem Sie die Dereferenzierung dieser einzelnen Funktion verwenden Basiszeiger.

Ich weiß immer noch nicht, wofür virtuelle Funktionen gut sind ...! Und das war meine erste Frage!

Das liegt daran, dass Sie Ihre Frage zu früh gestellt haben!

Warum brauchen wir virtuelle Funktionen?

Angenommen, Sie haben eine Funktion mit einem Basiszeiger aufgerufen, der die Adresse eines Objekts aus einer seiner abgeleiteten Klassen hatte. Wie wir oben bereits erwähnt haben, wird dieser Zeiger zur Laufzeit dereferenziert. Bisher so gut, dass wir jedoch erwarten, dass eine Methode (== eine Mitgliedsfunktion) "von unserer abgeleiteten Klasse" ausgeführt wird! In der Basisklasse ist jedoch bereits dieselbe Methode definiert (eine mit demselben Header). Warum sollte sich Ihr Programm also die Mühe machen, die andere Methode auszuwählen? Mit anderen Worten, ich meine, wie können Sie dieses Szenario von dem unterscheiden, was wir früher normalerweise gesehen haben?

Die kurze Antwort lautet "eine virtuelle Mitgliedsfunktion in der Basis", und eine etwas längere Antwort lautet: "Wenn das Programm in diesem Schritt eine virtuelle Funktion in der Basisklasse sieht, weiß es (erkennt), dass Sie versuchen, sie zu verwenden Polymorphismus "und geht so zu abgeleiteten Klassen (unter Verwendung von v-table , einer Form der späten Bindung), um herauszufinden, dass eine andere Methode mit demselben Header, aber mit - erwartungsgemäß - einer anderen Implementierung.

Warum eine andere Implementierung?

Du Knöchelkopf! Geh und lies ein gutes Buch !

OK, warte, warte, warte, warum sollte man sich die Mühe machen, Basiszeiger zu verwenden, wenn man einfach abgeleitete Typzeiger verwenden könnte? Sie sind der Richter, sind all diese Kopfschmerzen es wert? Schauen Sie sich diese beiden Schnipsel an:

// 1:

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

// 2:

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

OK, obwohl ich denke, dass 1 immer noch besser als 2 ist , könnten Sie 1 auch so schreiben :

// 1:

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

Darüber hinaus sollten Sie sich darüber im Klaren sein, dass dies nur eine erfundene Verwendung all der Dinge ist, die ich Ihnen bisher erklärt habe. Nehmen Sie stattdessen beispielsweise eine Situation an, in der Sie eine Funktion in Ihrem Programm hatten, die die Methoden aus jeder der abgeleiteten Klassen verwendete (getMonthBenefit ()):

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

Versuchen Sie nun, dies ohne Kopfschmerzen neu zu schreiben !

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();

Und tatsächlich könnte dies auch noch ein erfundenes Beispiel sein!

MJ
quelle
2
Das Konzept der Iteration auf verschiedenen Arten von (Unter-) Objekten unter Verwendung eines einzelnen (Super-) Objekttyps sollte hervorgehoben werden. Das ist ein guter Punkt, den Sie gegeben haben, danke
harschvchawla
14

Wenn Sie eine Funktion in der Basisklasse haben, können Sie Redefineoder Overridesie in der abgeleiteten Klasse.

Methode neu definieren : In der abgeleiteten Klasse wird eine neue Implementierung für die Methode der Basisklasse angegeben. Erleichtert nichtDynamic binding .

Überschreiben einer Methode : Redefiningavirtual methodder Basisklasse in der abgeleiteten Klasse. Die virtuelle Methode erleichtert die dynamische Bindung .

Als Sie sagten:

Aber früher in diesem Buch konnte ich beim Erlernen der grundlegenden Vererbung Basismethoden in abgeleiteten Klassen überschreiben, ohne 'virtuell' zu verwenden.

Sie haben es nicht überschrieben, da die Methode in der Basisklasse nicht virtuell war, sondern Sie haben sie neu definiert

nitin_cherian
quelle
11

Es hilft, wenn Sie die zugrunde liegenden Mechanismen kennen. C ++ formalisiert einige Codierungstechniken, die von C-Programmierern verwendet werden. "Klassen" werden durch "Overlays" ersetzt. Strukturen mit gemeinsamen Header-Abschnitten werden verwendet, um Objekte unterschiedlichen Typs zu verarbeiten, jedoch mit einigen gemeinsamen Daten oder Operationen. Normalerweise hat die Basisstruktur des Overlays (der gemeinsame Teil) einen Zeiger auf eine Funktionstabelle, die auf einen anderen Satz von Routinen für jeden Objekttyp verweist. C ++ macht dasselbe, verbirgt jedoch die Mechanismen, dh das C ++, ptr->func(...)in dem func virtuell ist wie C (*ptr->func_table[func_num])(ptr,...), wobei sich der Inhalt von func_table zwischen abgeleiteten Klassen ändert. [Eine nicht virtuelle Methode ptr-> func () übersetzt nur in mangled_func (ptr, ..).]

Das Ergebnis davon ist, dass Sie nur die Basisklasse verstehen müssen, um die Methoden einer abgeleiteten Klasse aufzurufen. Wenn eine Routine Klasse A versteht, können Sie ihr einen abgeleiteten Zeiger der Klasse B übergeben, dann werden die virtuellen Methoden aufgerufen von B statt A, da Sie durch die Funktionstabelle gehen, auf die B zeigt.

Kev
quelle
8

Das Schlüsselwort virtual teilt dem Compiler mit, dass keine frühzeitige Bindung durchgeführt werden soll. Stattdessen sollten automatisch alle Mechanismen installiert werden, die für eine späte Bindung erforderlich sind. Zu diesem Zweck erstellt der typische Compiler1 für jede Klasse, die virtuelle Funktionen enthält, eine einzelne Tabelle (VTABLE genannt). Der Compiler platziert die Adressen der virtuellen Funktionen für diese bestimmte Klasse in der VTABLE. In jeder Klasse mit virtuellen Funktionen wird heimlich ein Zeiger platziert, der als vpointer (abgekürzt als VPTR) bezeichnet wird und auf die VTABLE für dieses Objekt zeigt. Wenn Sie einen virtuellen Funktionsaufruf über einen Basisklassenzeiger ausführen, fügt der Compiler leise Code ein, um den VPTR abzurufen und die Funktionsadresse in der VTABLE nachzuschlagen. Dadurch wird die richtige Funktion aufgerufen und es kommt zu einer späten Bindung.

Weitere Details unter diesem Link http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html

rvkreddy
quelle
7

Das virtuelle Schlüsselwort zwingt den Compiler, die in der Objektklasse und nicht in der Zeigerklasse definierte Methodenimplementierung auszuwählen .

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

Im obigen Beispiel wird Shape :: getName standardmäßig aufgerufen, es sei denn, getName () ist in der Basisklasse Shape als virtuell definiert. Dies zwingt den Compiler, nach der Implementierung von getName () in der Triangle-Klasse und nicht in der Shape-Klasse zu suchen.

Die virtuelle Tabelle ist der Mechanismus, mit dem der Compiler die verschiedenen Implementierungen der Unterklassen für virtuelle Methoden verfolgt. Dies wird auch als dynamischer Versand bezeichnet und ist mit einem gewissen Overhead verbunden.

Warum wird Virtual überhaupt in C ++ benötigt, warum nicht das Standardverhalten wie in Java?

  1. C ++ basiert auf den Prinzipien "Zero Overhead" und "Pay for what you use". Es wird also nicht versucht, einen dynamischen Versand für Sie durchzuführen, es sei denn, Sie benötigen ihn.
  2. Mehr Kontrolle über die Schnittstelle. Indem eine Funktion nicht virtuell gemacht wird, kann die Schnittstelle / abstrakte Klasse das Verhalten in all ihren Implementierungen steuern.
javaProgrammer
quelle
4

Warum brauchen wir virtuelle Funktionen?

Virtuelle Funktionen vermeiden unnötige Typumwandlungsprobleme, und einige von uns können darüber diskutieren, warum wir virtuelle Funktionen benötigen, wenn wir den abgeleiteten Klassenzeiger verwenden können, um die in der abgeleiteten Klasse spezifische Funktion aufzurufen. Die Antwort lautet: Sie macht die gesamte Idee der Vererbung in einem großen System zunichte Entwicklung, bei der ein Einzelzeiger-Basisklassenobjekt sehr erwünscht ist.

Vergleichen wir im Folgenden zwei einfache Programme, um die Bedeutung virtueller Funktionen zu verstehen:

Programm ohne virtuelle Funktionen:

#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;
}

AUSGABE:

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

Programm mit virtueller Funktion:

#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;
}

AUSGABE:

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

Durch genaue Analyse beider Ausgänge kann man die Bedeutung virtueller Funktionen verstehen.

akshaypmurgod
quelle
4

OOP Antwort: Subtyp Polymorphismus

In C ++ werden virtuelle Methoden benötigt, um Polymorphismus zu realisieren , genauer Subtypisierung oder Subtyppolymorphismus, wenn Sie die Definition aus Wikipedia anwenden.

Wikipedia, Subtyping, 2019-01-09: In der Programmiersprachentheorie ist Subtyping (auch Subtyp-Polymorphismus oder Einschlusspolymorphismus) eine Form des Typpolymorphismus, bei der ein Subtyp ein Datentyp ist, der durch einen Begriff mit einem anderen Datentyp (dem Supertyp) verwandt ist der Substituierbarkeit, was bedeutet, dass Programmelemente, typischerweise Unterprogramme oder Funktionen, die geschrieben wurden, um auf Elemente des Supertyps zu arbeiten, auch auf Elemente des Untertyps arbeiten können.

HINWEIS: Subtyp bedeutet Basisklasse und Subtyp bedeutet geerbte Klasse.

Weiterführende Literatur zum Subtyp Polymorphismus

Technische Antwort: Dynamischer Versand

Wenn Sie einen Zeiger auf eine Basisklasse haben, wird der Aufruf der Methode (die als virtuell deklariert ist) an die Methode der tatsächlichen Klasse des erstellten Objekts gesendet. Auf diese Weise wird der Subtyp-Polymorphismus in C ++ realisiert.

Lesen Sie weiter Polymorphismus in C ++ und Dynamic Dispatch

Implementierungsantwort: Erstellt einen vtable-Eintrag

Für jeden Modifikator "virtual" für Methoden erstellen C ++ - Compiler normalerweise einen Eintrag in der vtable der Klasse, in der die Methode deklariert ist. So realisiert der C ++ - Compiler den dynamischen Versand .

Lesen Sie weiter vtables


Beispielcode

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~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 Dogs
        cout << "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;
}

Ausgabe des Beispielcodes

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

UML-Klassendiagramm des Codebeispiels

UML-Klassendiagramm des Codebeispiels

Jörg 'Wuwei' Brüggmann
quelle
1
Nehmen Sie meine Gegenstimme, weil Sie die vielleicht wichtigste Verwendung von Polymorphismus zeigen: Dass eine Basisklasse mit virtuellen Elementfunktionen eine Schnittstelle oder mit anderen Worten eine API angibt . Code, der einen solchen Klassenrahmen verwendet (hier: Ihre Hauptfunktion), kann alle Elemente in einer Sammlung (hier: Ihr Array) einheitlich behandeln und muss, will und kann oft nicht wissen, welche konkrete Implementierung aufgerufen wird zur Laufzeit, zum Beispiel weil es noch nicht existiert. Dies ist eine der Grundlagen für das Herausarbeiten abstrakter Beziehungen zwischen Objekten und Handlern.
Peter - Monica
2

Hier ist ein vollständiges Beispiel, das zeigt, warum die virtuelle Methode verwendet wird.

#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;
}
user3371350
quelle
1

In Bezug auf die Effizienz sind die virtuellen Funktionen etwas weniger effizient als die frühen Bindungsfunktionen.

"Dieser virtuelle Aufrufmechanismus kann fast so effizient gemacht werden wie der" normale Funktionsaufruf "-Mechanismus (innerhalb von 25%). Sein Speicherplatz-Overhead ist ein Zeiger in jedem Objekt einer Klasse mit virtuellen Funktionen plus einem vtbl für jede solche Klasse." [ A. Tour durch C ++ von Bjarne Stroustrup]

Herzog
quelle
2
Eine späte Bindung verlangsamt nicht nur den Funktionsaufruf, sondern macht die aufgerufene Funktion bis zur Laufzeit unbekannt, sodass Optimierungen über den Funktionsaufruf hinweg nicht angewendet werden können. Dies kann alles ändern, z. In Fällen, in denen die Wertweitergabe viel Code entfernt (denken Sie daran, if(param1>param2) return cst;dass der Compiler in einigen Fällen den gesamten Funktionsaufruf auf eine Konstante reduzieren kann).
Neugieriger
1

Virtuelle Methoden werden beim Schnittstellendesign verwendet. In Windows gibt es beispielsweise eine Schnittstelle namens IUnknown wie folgt:

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

Diese Methoden müssen vom Benutzer der Benutzeroberfläche implementiert werden. Sie sind wichtig für die Erstellung und Zerstörung bestimmter Objekte, die IUnknown erben müssen. In diesem Fall kennt die Laufzeit die drei Methoden und erwartet, dass sie beim Aufruf implementiert werden. In gewissem Sinne fungieren sie als Vertrag zwischen dem Objekt selbst und dem, was dieses Objekt verwendet.


quelle
the run-time is aware of the three methods and expects them to be implementedDa sie rein virtuell sind, gibt es keine Möglichkeit, eine Instanz von zu erstellen. Daher müssenIUnknown alle Unterklassen alle diese Methoden implementieren, um lediglich zu kompilieren. Es besteht keine Gefahr, sie nicht zu implementieren und dies nur zur Laufzeit herauszufinden (aber natürlich kann man sie natürlich falsch implementieren!). Und wow, heute habe ich Windows als Makro mit dem Wort gelernt , vermutlich weil ihre Benutzer nicht einfach (A) das Präfix im Namen sehen oder (B) die Klasse betrachten können, um zu sehen, dass es sich um eine Schnittstelle handelt. Ugh#defineinterfaceI
underscore_d
1

Ich denke, Sie beziehen sich auf die Tatsache, dass Sie, sobald eine Methode als virtuell deklariert wurde, das Schlüsselwort 'virtual' nicht mehr für Überschreibungen verwenden müssen.

class Base { virtual void foo(); };

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

Wenn Sie in der foo-Deklaration von Base nicht 'virtual' verwenden, wird das foo von Derived nur beschattet.

edwinc
quelle
1

Hier ist eine zusammengeführte Version des C ++ - Codes für die ersten beiden Antworten.

#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;
}

Zwei verschiedene Ergebnisse sind:

Ohne #define virtual wird es zur Kompilierungszeit gebunden. Animal * ad und func (Animal *) verweisen alle auf die say () -Methode des Tieres.

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

Mit #define virtual wird es zur Laufzeit gebunden. Dog * d, Animal * ad und func (Animal *) zeigen / beziehen sich auf die say () -Methode des Hundes, da Dog ihr Objekttyp ist. Sofern die Methode [Hund sagt () "woof"] nicht definiert ist, wird sie zuerst im Klassenbaum gesucht, dh abgeleitete Klassen können Methoden ihrer Basisklassen überschreiben [Animal sagt ()].

$ 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

Es ist interessant festzustellen, dass alle Klassenattribute (Daten und Methoden) in Python effektiv virtuell sind . Da alle Objekte zur Laufzeit dynamisch erstellt werden, ist weder eine Typdeklaration noch ein virtuelles Schlüsselwort erforderlich. Unten ist Pythons Version des Codes:

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)))

Die Ausgabe ist:

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

Dies ist identisch mit der virtuellen Definition von C ++. Beachten Sie, dass d und ad zwei verschiedene Zeigervariablen sind, die auf dieselbe Dog-Instanz verweisen. Der Ausdruck (ad is d) gibt True zurück und ihre Werte sind dieselben < main .Dog-Objekt bei 0xb79f72cc>.

Leon Chang
quelle
1

Kennen Sie Funktionszeiger? Virtuelle Funktionen sind eine ähnliche Idee, außer dass Sie Daten einfach an virtuelle Funktionen binden können (als Klassenmitglieder). Es ist nicht so einfach, Daten an Funktionszeiger zu binden. Für mich ist dies die wichtigste konzeptionelle Unterscheidung. Viele andere Antworten hier sagen nur "weil ... Polymorphismus!"

user2445507
quelle
0

Wir benötigen virtuelle Methoden zur Unterstützung von "Laufzeitpolymorphismus". Wenn Sie mit einem Zeiger oder einem Verweis auf die Basisklasse auf ein abgeleitetes Klassenobjekt verweisen, können Sie eine virtuelle Funktion für dieses Objekt aufrufen und die Version der Funktion der abgeleiteten Klasse ausführen.

Hautausschläge
quelle
-1

Das Fazit ist, dass virtuelle Funktionen das Leben leichter machen. Lassen Sie uns einige von M Perrys Ideen verwenden und beschreiben, was passieren würde, wenn wir keine virtuellen Funktionen hätten und stattdessen nur Zeiger auf Elementfunktionen verwenden könnten. Wir haben nach normaler Schätzung ohne virtuelle Funktionen:

 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!"

Ok, das wissen wir also. Versuchen wir nun, dies mit Zeigern auf Elementfunktionen zu tun:

 #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;
 }

Wir können zwar einige Dinge mit Zeigern auf Elementfunktionen tun, sie sind jedoch nicht so flexibel wie virtuelle Funktionen. Es ist schwierig, einen Elementfunktionszeiger in einer Klasse zu verwenden. Der Elementfunktionszeiger muss, zumindest in meiner Praxis, fast immer in der Hauptfunktion oder innerhalb einer Elementfunktion wie im obigen Beispiel aufgerufen werden.

Auf der anderen Seite vereinfachen virtuelle Funktionen, obwohl sie möglicherweise einen gewissen Funktionszeiger-Overhead haben, die Dinge dramatisch.

BEARBEITEN: Es gibt eine andere Methode, die eddietree ähnelt: virtuelle C ++ - Funktion vs. Elementfunktionszeiger (Leistungsvergleich) .

Fischerhut
quelle