c++ - überschreiben - virtuelle methoden c#




Warum brauchen wir virtuelle Funktionen in C++? (14)

Ich lerne C ++ und komme gerade in virtuelle Funktionen.

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

Aber früher im Buch, als ich etwas über die grundlegende Vererbung lernte, war ich in der Lage, Basisfunktionen in abgeleiteten Klassen ohne Verwendung von virtual zu überschreiben.

Also was fehlt mir hier? Ich weiß, dass es mehr virtuelle Funktionen gibt, und es scheint wichtig zu sein, also möchte ich klarstellen, was genau das ist. Ich kann einfach keine direkte Antwort online finden.


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 verwenden, die aufgerufen wird, wenn ein Benutzer die Basisklassenfunktion aufruft. Dies wird oft als Laufzeitpolymorphismus , dynamischer Dispatch oder Laufzeitversand bezeichnet, da die aufgerufene Funktion zur Laufzeit basierend auf dem Typ des verwendeten Objekts ermittelt wird.

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

Um einen virtuellen Anruf zu behandeln, benötigt man ein oder mehrere Datenstücke, die sich auf das abgeleitete Objekt 3 beziehen. Die übliche Vorgehensweise besteht darin, die Adresse der Funktionstabelle hinzuzufügen. Diese Tabelle wird normalerweise als virtuelle Tabelle oder virtuelle Funktionstabelle bezeichnet und ihre Adresse wird oft als virtueller Zeiger bezeichnet . Jede virtuelle Funktion erhält einen Slot 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 gebräuchlichste Definition von objektorientierter Programmierung .

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

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


Über die Effizienz sind die virtuellen Funktionen etwas weniger effizient als die frühen Bindungsfunktionen.

Dieser virtuelle Aufrufmechanismus kann fast so effizient wie der Mechanismus des "normalen Funktionsaufrufs" gemacht werden (innerhalb von 25%). Sein Platzbedarf ist ein Zeiger in jedem Objekt einer Klasse mit virtuellen Funktionen plus ein Vtbl für jede derartige Klasse Tour von C ++ durch Bjarne Stroustrup


Das Schlüsselwort virtual teilt dem Compiler mit, dass es keine frühe Bindung durchführen soll. Stattdessen sollten automatisch alle Mechanismen installiert werden, die für die späte Bindung erforderlich sind. Um dies zu erreichen, erstellt der typische Compiler1 eine einzelne Tabelle (die VTABLE genannt wird) für jede Klasse, die virtuelle Funktionen enthält. Der Compiler platziert die Adressen der virtuellen Funktionen für diese spezielle Klasse in der VTABLE. In jeder Klasse mit virtuellen Funktionen platziert sie heimlich einen Zeiger, den vpointer (abgekürzt als VPTR), der auf VTABLE für dieses Objekt zeigt. Wenn Sie einen virtuellen Funktionsaufruf über einen Basisklassenzeiger ausführen, fügt der Compiler ruhig Code ein, um den VPTR abzurufen und die Funktionsadresse in VTABLE nachzuschlagen, wodurch die korrekte Funktion aufgerufen wird und eine späte Bindung ausgelöst wird.

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


Es hilft, wenn Sie die zugrunde liegenden Mechanismen kennen. C ++ formalisiert einige Codierungstechniken, die von C-Programmierern verwendet werden, "Klassen", die unter Verwendung von "Überlagerungen" ersetzt wurden - Strukturen mit gemeinsamen Kopfabschnitten würden verwendet werden, um Objekte verschiedener Typen, aber mit einigen gemeinsamen Daten oder Operationen zu behandeln. Normalerweise hat die Basisstruktur der Überlagerung (der gemeinsame Teil) einen Zeiger auf eine Funktionstabelle, die auf einen anderen Satz von Routinen für jeden Objekttyp zeigt. C ++ macht dasselbe, verbirgt aber die Mechanismen, dh C ++ ptr->func(...) wo func virtuell ist wie C (*ptr->func_table[func_num])(ptr,...) , wo sich was ändert zwischen abgeleiteten Klassen ist der Inhalt von func_table. [Eine nicht virtuelle Methode ptr-> func () wird nur in mangled_func (ptr, ..) übersetzt.]

Das Ergebnis davon ist, dass Sie nur die Basisklasse verstehen müssen, um die Methoden einer abgeleiteten Klasse aufzurufen, dh wenn eine Routine Klasse A versteht, können Sie ihr einen abgeleiteten Klassen-B-Zeiger übergeben, dann werden die aufgerufenen virtuellen Methoden diese sein von B eher als von A, da du durch die Funktionstabelle B hindurch gehst.


Ich habe meine Antwort in Form einer Konversation, um besser gelesen zu werden:

Warum brauchen wir virtuelle Funktionen?

Wegen Polymorphismus.

Was ist Polymorphismus?

Die Tatsache, dass ein Basiszeiger auch auf Objekte des abgeleiteten Typs zeigen kann.

Wie führt diese Definition von Polymorphismus dazu, dass virtuelle Funktionen benötigt werden?

Nun, durch frühe Bindung .

Was ist früh verbindlich?

Frühe Bindung (Compile-Time-Bindung) in C ++ bedeutet, dass ein Funktionsaufruf vor der Ausführung des Programms behoben wird.

Damit...?

Wenn Sie also einen Basistyp als Parameter einer Funktion verwenden, erkennt der Compiler nur die Basisschnittstelle, und wenn Sie diese Funktion mit beliebigen 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 der Vorteil von Polymorphismus dann?

Sie können einen Basistyp-Zeiger als Parameter einer einzelnen Funktion verwenden, und dann können Sie in der Laufzeit Ihres Programms auf alle Schnittstellen des abgeleiteten Typs (z. B. ihre Member-Funktionen) ohne Probleme zugreifen, indem Sie die Dereferenzierung dieser Single verwenden Basiszeiger.

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

Nun, das ist, weil 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 oben erwähnt, wird dieser Zeiger in der Laufzeit dereferenziert, so weit, so gut, aber wir erwarten, dass eine Methode (== eine Memberfunktion) "von unserer abgeleiteten Klasse" ausgeführt wird! Allerdings ist dieselbe Methode (eine, die denselben Header hat) bereits in der Basisklasse definiert. Warum sollte Ihr Programm also die andere Methode wä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 ist "eine virtuelle Mitgliedsfunktion in der Basis", und eine etwas längere Antwort lautet: "Wenn das Programm eine virtuelle Funktion in der Basisklasse sieht, weiß es (realisiert), dass Sie es verwenden wollen Polymorphismus "und so geht es zu abgeleiteten Klassen (mit v-table , eine Form der späten Bindung), um diese andere Methode mit dem gleichen Header, aber mit - erwarteter - anderer Implementierung zu finden.

Warum eine andere Implementierung?

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

OK, warte, warte, warum sollte man sich bemühen, Basiszeiger zu verwenden, wenn er / sie einfach abgeleitete Typzeiger verwenden könnte? Du bist der Richter, sind all diese Kopfschmerzen es wert? Schau dir diese zwei 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önntest du 1 auch so schreiben:

// 1:

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

und außerdem solltest du dir bewusst sein, dass dies nur eine erfundene Anwendung all der Dinge ist, die ich dir bisher erklärt habe. Nehmen Sie stattdessen zum Beispiel eine Situation an, in der Sie in Ihrem Programm eine Funktion hatten, die die Methoden jeder abgeleiteten Klasse verwendet (getMonthBenefit ()):

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

Versuchen Sie jetzt, dies neu zu schreiben, ohne irgendwelche Kopfschmerzen!

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 ein künstliches Beispiel sein!


Ich möchte eine andere Verwendung der virtuellen Funktion hinzufügen, obwohl es das gleiche Konzept wie die oben genannten 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; Speicher für Cat kann nicht aufgeräumt werden.

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

Ohne "virtuell" erhält man "früh verbindlich". Welche Implementierung der Methode verwendet wird, wird zur Kompilierzeit basierend auf dem Typ des Zeigers entschieden, den Sie aufrufen.

Mit "virtuell" erhalten Sie "späte Bindung". Welche Implementierung der Methode verwendet wird, wird zur Laufzeit basierend auf dem Typ des zu zeigenden Objekts entschieden - woraus es ursprünglich konstruiert wurde. Dies ist nicht notwendigerweise der, den Sie aufgrund des Typs des Zeigers meinen, 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"

EDIT - siehe diese Frage .

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


Sie benötigen mindestens 1 Vererbung 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

    cout << d->Says();   // always Woof
    cout << a->Says();   // Woof or ?, depends on virtual
}

Sie müssen zwischen Übersteuern und Überladen unterscheiden. Ohne das virtual Schlüsselwort überlasten Sie nur eine Methode einer Basisklasse. Das bedeutet nichts anderes als sich zu verstecken. Nehmen wir an, Sie haben eine Basisklasse Base und eine abgeleitete Klasse Specialized die beide void foo() implementieren. Jetzt haben Sie einen Zeiger auf Base , der auf eine Instanz von Specialized . Wenn Sie foo() aufrufen, können Sie den Unterschied beobachten, den virtual macht: Wenn die Methode virtuell ist, wird die Implementierung von Specialized verwendet, wenn sie fehlt, wird die Version von Base ausgewählt. Es empfiehlt sich, Methoden niemals von einer Basisklasse zu überladen. Eine Methode, die nicht virtuell ist, ist die Methode ihres Autors, um zu sagen, dass ihre Erweiterung in Unterklassen nicht beabsichtigt ist.


Virtuelle Funktionen werden verwendet, um Laufzeitpolymorphismus zu unterstützen.

Das heißt, das virtuelle Schlüsselwort weist den Compiler an, die Entscheidung (der Funktionsbindung) nicht während der Kompilierung zu treffen, sondern sie zur Laufzeit zu verschieben " .

  • Sie können eine Funktion virtuell machen, indem Sie dem Schlüsselwort virtual in der Basisklasse-Deklaration der Klasse vorausgehen. Beispielsweise,

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

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

  • Wenn die virtuelle Funktion mit einem Basisklassenzeiger aufgerufen wird, entscheidet der Compiler zur Laufzeit, welche Version der Funktion - also die Basisklassenversion oder die überschriebene abgeleitete Klassenversion - aufgerufen werden soll. Dies wird als Laufzeitpolymorphismus bezeichnet .

Warum brauchen wir virtuelle Funktionen?

Virtuelle Funktionen vermeiden unnötige Typumwandlungsprobleme, und einige von uns können darüber debattieren, warum wir virtuelle Funktionen benötigen, wenn wir den abgeleiteten Klassenzeiger verwenden können, um die Funktion in abgeleiteten Klassen aufzurufen! Die Antwort ist - sie hebt die Idee der Vererbung im großen System auf Entwicklung, wo es sehr wünschenswert ist, ein einzelnes Pointer-Basisklassenobjekt zu haben.

Lassen Sie uns im Folgenden zwei einfache Programme vergleichen, um die Bedeutung von virtuellen 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 die enge Analyse der beiden Ausgaben kann man die Bedeutung von virtuellen Funktionen verstehen.


Wenn Sie eine Funktion in der Basisklasse haben, können Sie sie in der abgeleiteten Klasse neu Redefine oder Override .

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

Überschreiben einer Methode : Redefining einer virtual method der Basisklasse in der abgeleiteten Klasse. Die virtuelle Methode erleichtert die dynamische Bindung .

Also als du gesagt hast:

Aber früher im Buch, als ich etwas über grundlegende Vererbung lernte, war ich in der Lage, Basismethoden in abgeleiteten Klassen zu überschreiben, ohne "virtuell" zu verwenden.

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


Here is complete example that illustrates why virtual method is used.

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

Wir brauchen 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 abgeleitete Klassenversion der Funktion ausführen.





virtual-functions