Warum brauchen wir virtuelle Funktionen in C++?

Lesezeit: 14 Minuten

Warum brauchen wir virtuelle Funktionen in C
Jake Wilson

Ich lerne C++ und beschäftige mich gerade mit virtuellen Funktionen.

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, als ich etwas über grundlegende Vererbung lernte, konnte ich Basisfunktionen in abgeleiteten Klassen ohne Verwendung überschreiben virtual.

Also was übersehe ich hier? Ich weiß, dass virtuelle Funktionen mehr sind, und es scheint wichtig zu sein, also möchte ich klarstellen, was es genau ist. Ich finde im Internet einfach keine eindeutige Antwort.

  • Dies ist vielleicht der größte Vorteil virtueller Funktionen – die Fähigkeit, Ihren Code so zu strukturieren, dass neu abgeleitete Klassen automatisch ohne Modifikation mit dem alten Code funktionieren!

    – Benutzer3530616

    29. April 2017 um 13:08 Uhr

  • tbh, virtuelle Funktionen sind ein Hauptmerkmal von OOP zum Löschen von Typen. Ich denke, es sind nicht-virtuelle Methoden, die Object Pascal und C++ so besonders machen, da sie unnötig große vtable optimieren und POD-kompatible Klassen ermöglichen. Viele OOP-Sprachen erwarten das jeden Methode kann überschrieben werden.

    – Swift – Friday Pie

    7. April 2018 um 9:28 Uhr


  • Das ist eine gute Frage. Tatsächlich wird dieses virtuelle Ding in C++ in anderen Sprachen wie Java oder PHP abstrahiert. In C++ erhalten Sie nur für einige seltene Fälle etwas mehr Kontrolle (Beachten Sie die Mehrfachvererbung oder den Sonderfall der DDOD). Aber warum wird diese Frage auf stackoverflow.com gepostet?

    – Edgar Alloro

    21. Mai 2018 um 21:45 Uhr


  • Ich denke, wenn Sie sich die frühe Bindung, die späte Bindung und VTABLE ansehen, wäre dies vernünftiger und sinnvoller. Es gibt also eine gute Erklärung ( learncpp.com/cpp-tutorial/125-the-virtual-table ) Hier.

    – ceyun

    17. Juli 2019 um 7:27 Uhr

  • Eine bessere Anleitung habe ich noch nicht gefunden: nrecursions.blogspot.com/2015/06/…

    – Johannes David

    21. Januar 2021 um 12:30 Uhr

Warum brauchen wir virtuelle Funktionen in C
M. Perry

Hier ist, wie ich verstanden habe, nicht nur was virtual Funktionen sind, aber 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.

Ändern wir es jetzt ein wenig damit eat() wird über eine Zwischenfunktion aufgerufen (nur für dieses Beispiel eine triviale Funktion):

// 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."

Uh oh … wir sind an einer Katze vorbeigegangen func(), aber es frisst keine Ratten. Sollten Sie überladen func() also dauert es a Cat*? Wenn Sie weitere Tiere von Animal ableiten müssten, bräuchten sie alle ihre eigenen func().

Die Lösung ist zu machen eat() von dem Animal Klasse eine virtuelle Funktion:

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

Hauptsächlich:

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

Getan.

  • Also, wenn ich das richtig verstehe, ermöglicht virtual den Aufruf der Unterklassenmethode, auch wenn das Objekt als seine Oberklasse behandelt wird?

    – Kenny Worden

    4. April 2015 um 3:33 Uhr


  • Anstatt die späte Bindung am Beispiel einer Zwischenfunktion “func” zu erklären, ist hier eine einfachere Demonstration — Tier *tier = neues Tier; //Katze *Katze = neue Katze; Tier *Katze = neue Katze; tier->essen(); // gibt aus: “Ich esse generisches Essen.” Katze->essen(); // gibt aus: “Ich esse generisches Essen.” Auch wenn Sie das untergeordnete Objekt (Cat) zuweisen, basiert die aufgerufene Methode auf dem Zeigertyp (Tier) und nicht auf dem Objekttyp, auf den sie zeigt. Deshalb brauchen Sie “virtuell”.

    – rexbelia

    7. Februar 2016 um 4:08 Uhr

  • Bin ich der einzige, der dieses Standardverhalten in C++ nur seltsam findet? Ich hätte erwartet, dass der Code ohne “virtuell” funktioniert.

    – David 天宇 Wong

    7. Juli 2016 um 19:53 Uhr

  • @David天宇Wong denke ich virtual führt einige dynamische Bindungen im Vergleich zu statischen ein und ja, es ist seltsam, wenn Sie aus Sprachen wie Java kommen.

    – Peter Chaula

    6. August 2016 um 15:18 Uhr


  • Erstens sind virtuelle Aufrufe viel, viel teurer als normale Funktionsaufrufe. Die C++-Philosophie ist standardmäßig schnell, daher sind virtuelle Aufrufe standardmäßig ein großes No-Go. 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 diese ihre interne Implementierung einer öffentlichen oder privaten Methode (die intern eine virtuelle Methode aufruft) ändert, ohne das Verhalten der Basisklasse zu ändern.

    – saolof

    23. Februar 2017 um 23:53 Uhr


Ohne „virtuell“ erhalten Sie „Early Binding“. Welche Implementierung der Methode verwendet wird, wird zur Kompilierzeit basierend auf dem Typ des Zeigers entschieden, den Sie aufrufen.

Mit “virtuell” erhalten Sie “spätes Binden”. Welche Implementierung der Methode verwendet wird, wird zur Laufzeit entschieden, basierend auf dem Typ des Objekts, auf das gezeigt wird – als was es ursprünglich konstruiert wurde. Dies ist nicht unbedingt das, was Sie denken würden, basierend auf dem Typ des Zeigers, 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* basePtr = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

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

BEARBEITEN – Siehe diese Frage.

Ebenfalls – dieses Tutorial behandelt die frühe und späte Bindung in C++.

  • Ausgezeichnet, und kommt schnell und mit besseren Beispielen nach Hause. Dies ist jedoch vereinfachend, und der Fragesteller sollte die Seite wirklich nur lesen parashift.com/c++-faq-lite/virtual-functions.html. Andere Leute haben bereits in SO-Artikeln, die von diesem Thread verlinkt sind, auf diese Ressource hingewiesen, aber ich glaube, das ist es wert, noch einmal erwähnt zu werden.

    – Sonny

    8. Juni 2012 um 13:06 Uhr

  • Ich weiß nicht, ob frühzeitig und spät Bindung sind Begriffe, die speziell in der C++-Community verwendet werden, aber die richtigen Begriffe sind statisch (zur Kompilierzeit) und dynamisch (zur Laufzeit) Bindung.

    – Mike

    18. Mai 2015 um 16:34 Uhr

  • @Mike – “Der Begriff “spätes Binden” stammt mindestens aus den 1960er Jahren, wo er in den Mitteilungen der ACM zu finden ist.”. Wäre es nicht schön, wenn es für jedes Konzept ein richtiges Wort gäbe? Leider ist es einfach nicht so. Die Begriffe „frühes Binden“ und „spätes Binden“ stammen aus der Zeit vor C++ und sogar der objektorientierten Programmierung und sind genauso korrekt wie die Begriffe, die Sie verwenden.

    Benutzer180247

    18. Mai 2015 um 18:22 Uhr

  • @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 umschließen der Variablendeklaration und Aufrufe in a main Funktion usw. Zeiger auf abgeleitet implizit wandelt in Pointer-to-Base um (speziellere wandelt implizit in allgemeinere um). Umgekehrt benötigen Sie eine explizite Besetzung, normalerweise a dynamic_cast. Alles andere – sehr anfällig für undefiniertes Verhalten, also stellen Sie sicher, dass Sie wissen, was Sie tun. Meines Wissens nach hat sich dies auch vor C++98 nicht geändert.

    Benutzer180247

    18. Juni 2017 um 19:29 Uhr

  • Beachten Sie, dass C++-Compiler heute häufig eine späte bis frühe Bindung optimieren können – wenn sie sicher sein können, wie die Bindung aussehen wird. Dies wird auch als „Devirtualisierung“ bezeichnet.

    – einpoklum

    2. April 2018 um 0:10 Uhr

1647267018 639 Warum brauchen wir virtuelle Funktionen in C
Henk Holtermann

Sie benötigen mindestens 1 Vererbungsebene und einen Upcast, um dies zu demonstrieren. Hier ein ganz 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
}

  • Ihr Beispiel besagt, dass die zurückgegebene Zeichenfolge davon abhängt, ob die Funktion virtuell ist, aber es sagt nicht aus, welches Ergebnis virtuell und welches nicht virtuell entspricht. Außerdem ist es ein wenig verwirrend, da Sie die zurückgegebene Zeichenfolge nicht verwenden.

    – Roß

    6. März 2010 um 11:10 Uhr

  • Mit virtuellem Schlüsselwort: Schuss. Ohne virtuelles Schlüsselwort: ?.

    – Hescham Eraqi

    11. November 2013 um 11:14 Uhr

  • @HeshamEraqi ohne Virtual ist es früh bindend und es wird “?” der Basisklasse

    – Achmad

    7. August 2017 um 6:03 Uhr

Virtuelle Funktionen werden zur Unterstützung verwendet Laufzeitpolymorphismus.

Das ist, virtuell Schlüsselwort teilt dem Compiler mit die Entscheidung (der Funktionsbindung) nicht zur Kompilierzeit zu treffen, sondern zur Laufzeit aufzuschieben”.

  • Sie können eine Funktion virtuell machen, indem Sie das Schlüsselwort voranstellen virtual in seiner Basisklassendeklaration. Zum Beispiel,

    class Base
    {
       virtual void func();
    }
    
  • Wenn ein Basisklasse eine virtuelle Mitgliedsfunktion hat, kann jede Klasse, die von der Basisklasse erben kann neu definieren die Funktion mit genau der gleiche Prototyp 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 ein Basisklassenobjekt als auch auf ein abgeleitetes Klassenobjekt zu zeigen.

  • Wenn die virtuelle Funktion unter Verwendung eines Basisklassenzeigers aufgerufen wird, entscheidet der Compiler zur Laufzeit, welche Version der Funktion – dh die Basisklassenversion oder die überschriebene abgeleitete Klassenversion – aufgerufen werden soll. Das nennt man Laufzeitpolymorphismus.

Warum brauchen wir virtuelle Funktionen in C
Prost und hth. – Alf

Sie benötigen virtuelle Methoden für sicheren Niedergang, Einfachheitund Prägnanz.

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


Nicht virtuelle Methode ⇒ statische Bindung ======================================

Der folgende Code ist absichtlich „falsch“. Es erklärt nicht die value Methode 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 die Expression::value Methode aufgerufen, weil die statisch bekannter Typ (der zur Kompilierzeit bekannte Typ) ist Expressionund das value Methode ist nicht virtuell.


Virtuelle Methode ⇒ dynamische Bindung. =====================================

Deklarieren value als virtual in der statisch bekannten Art Expression stellt sicher, dass jeder Aufruf überprüft, um welchen tatsächlichen Objekttyp es sich handelt, und die entsprechende Implementierung von aufruft value dafür dynamischer Typ:

#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 6.86 wie es sein sollte, da die virtuelle Methode ist virtuell angerufen. Dies wird auch genannt Dynamische Bindung der Anrufe. Es wird eine kleine Überprüfung durchgeführt, um den tatsächlichen dynamischen Objekttyp zu finden, und die relevante Methodenimplementierung für diesen dynamischen Typ wird aufgerufen.

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

Beachten Sie, dass Methodenimplementierungen in abgeleiteten Klassen hier nicht gekennzeichnet sind virtualsondern sind stattdessen gekennzeichnet override. Sie könnten markiert werden virtual aber sie sind automatisch virtuell. Die override Schlüsselwort stellt sicher, dass, wenn es gibt nicht eine solche virtuelle Methode in einer Basisklasse, dann erhalten Sie einen Fehler (was wünschenswert ist).


Die Hässlichkeit, dies ohne virtuelle Methoden zu tun ======================================== ========

Ohne virtual man müsste einige implementieren Mach es selbst Version der dynamischen Bindung. Dies ist im Allgemeinen mit unsicherem manuellem Downcasting, Komplexität und Ausführlichkeit verbunden.

Für den Fall einer einzelnen Funktion, wie hier, reicht es aus, einen Funktionszeiger im Objekt zu speichern und über diesen Funktionszeiger aufzurufen, aber selbst das bringt einige unsichere Downcasts, Komplexität und Ausführlichkeit mit sich, nämlich:

#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, dann oft eine virtuelle Methode oder Methoden wirklich helfen können.

1647267019 978 Warum brauchen wir virtuelle Funktionen in C
PW

Wenn die Basisklasse ist Baseund eine abgeleitete Klasse ist DerSie können eine haben Base *p Zeiger, der tatsächlich auf eine Instanz von zeigt Der. Wenn du anrufst p->foo();wenn foo ist nicht also virtuell Base‘s Version davon wird ausgeführt, wobei die Tatsache ignoriert wird, dass p weist eigentlich auf a hin Der. Wenn foo ist virtuell, p->foo() führt die “leafmost” Überschreibung von aus foo, wobei die tatsächliche Klasse des Elements, auf das gezeigt wird, vollständig berücksichtigt wird. Der Unterschied zwischen virtuell und nicht virtuell ist also eigentlich ziemlich entscheidend: Ersteres ermöglicht Laufzeit Polymorphismusdas Kernkonzept der OO-Programmierung, während letzteres dies nicht tut.

Uberprufen Sie ob ein Klassenobjekt eine Unterklasse eines anderen Klassenobjekts
Neuron

Ich möchte eine weitere Verwendung der virtuellen Funktion hinzufügen, obwohl sie dasselbe 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 darf nicht bereinigt 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

  • without declaring Base class destructor as virtual; memory for Cat may not be cleaned up. Es ist noch schlimmer. Das Löschen eines abgeleiteten Objekts durch einen Basiszeiger/eine Referenz ist reines undefiniertes Verhalten. Es ist also nicht nur so, dass ein Speicher auslaufen könnte. Vielmehr ist das Programm schlecht geformt, sodass der Compiler es in alles umwandeln kann: Maschinencode, der das passiert gut funktioniert, oder nichts tut, oder Dämonen aus der Nase ruft, oder etc. Deshalb, wenn ein Programm so konzipiert ist, dass einige Benutzer könnte Löschen Sie eine abgeleitete Instanz über eine Basisreferenz, die Basis Muss einen virtuellen Destruktor haben

    – Unterstrich_d

    20. Mai 2017 um 21:51 Uhr


1001900cookie-checkWarum brauchen wir virtuelle Funktionen in C++?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy