Dynamische Objekte



  • Hallo,

    ich bin heute über folgenden C++ Code gestolpert:

    class A{
    public:
          A() {cout << "A()" << endl; }
          virtual ~A() {cout << "~A()" << endl;}
    
          void print1() {cout << "A::print1()" << endl;}
          virtual void print2() {cout << "A::print2()" << endl;}
    };
    
    class B : public A{
    public:
          B() {cout << "B()" << endl; }
          virtual ~B() {cout << "~B()" << endl;}
    
          void print1() {cout << "B::print1()" << endl;}
          virtual void print2() {cout << "B::print2()" << endl;}
    };
    

    und dann in der main() Funktion der folgende Aufruf:

    A* b2 = new B()
    b2->print1();
    b2->print2();
    delete b2;
    

    Was dann folgende Ausgabe liefert:

    A()
    B()
    A::print1()
    B::print2()
    ~B()
    ~A()
    

    Mein Problem hier ist die Ausgabe A::print1(). Wenn ich das hier richtig verstehe wird ein dynamisches Objekt der Klasse B erstellt, allerdings mit einem Zeiger auf ein Objekt der Klasse A (bin mir nicht sicher ob das die korrekte Formulierung ist). Wenn ich jetzt aber ein Objekt der Klasse B habe, müsste dann nicht auch die Funktion print1() aus der Klasse B aufgerufen werden, print1() aus A müsste ja eigentlich überdeckt sein?



  • Necrotos schrieb:

    Wenn ich jetzt aber ein Objekt der Klasse B habe,

    Hast Du aber nicht. Du hast eine Variable vom Typ: Zeiger auf A
    Und weil die aufgerufenen Methode in A nicht virtuell ist, wird die Methode der Klasse A aufgerufen.



  • Aber was genau macht hier dann new B()? Ich dachte das sei der Befehl zum Erzeugen eines Objektes der Klasse B.



  • Necrotos schrieb:

    Aber was genau macht hier dann new B()? Ich dachte das sei der Befehl zum Erzeugen eines Objektes der Klasse B.

    new erzeugt ein B-Objekt und gibt einen Zeiger auf dieses zurück.

    A* a = new B(); // ein Zeiger vom Typ A auf ein B-Objekt
    


  • Aber wie beeinflusst die Tatsache dass es ein Zeiger vom Typ A ist denn die Funktionsaufrufe? Stelle das irgendwie andere Informationen zur Verfügung als ein Zeiger vom Typ B? Was genau ist da anders?


  • Mod

    Necrotos schrieb:

    Aber wie beeinflusst die Tatsache dass es ein Zeiger vom Typ A ist denn die Funktionsaufrufe? Stelle das irgendwie andere Informationen zur Verfügung als ein Zeiger vom Typ B? Was genau ist da anders?

    Das Objekt weiß nicht selber, von welchem Typ es ist. Wenn der Zeiger vom Typ "Zeiger auf A" ist, dann wird das Objekt dahinter wie ein Objekt vom Typ A behandelt Egal was da wirklich ist! Da kann auch ein int stehen oder auch einfach nur uninitialisierter Speicher. Es wird trotzdem angenommen, dass da ein A steht. Daher sollte man vorsichtig sein, wenn man mit Zeigern herum spielt.

    Die große Ausnahme davon sind virtuelle Funktionen. Für die gibt es wirklich irgendwo eine Tabelle¹, in der drin steht, welche Funktion wirklich aufgerufen werden muss, damit sie zu dem Objekt passt. In dem Fall ist es also quasi wirklich so, also ob ein Objekt seinen Typ kennen würde.

    ¹: Das muss nicht unbedingt mit einer Tabelle passieren, aber das ist eine übliche Art und Weise, den Effekt von virtuellen Funktionen zu erreichen.



  • Also ohne virtual werden die Funktionen aus A aufgerufen, weil es als Objekt vom Typ A gehandhabt wird, aber mit virtual wird dem Objekt mitgeteilt, dass es eigentlich doch von B ist, was ihm vorher nicht bekannt war, und dann werden eben die Funktionen aus B aufgerufen?


  • Mod

    Necrotos schrieb:

    Also ohne virtual werden die Funktionen aus A aufgerufen, weil es als Objekt vom Typ A gehandhabt wird, aber mit virtual wird dem Objekt mitgeteilt, dass es eigentlich doch von B ist, was ihm vorher nicht bekannt war, und dann werden eben die Funktionen aus B aufgerufen?

    Mitgeteilt wird gar nichts. Es geht nur darum welche Funktion aufgerufen wird.

    Ohne virtual sieht es so aus, als wolle man eine Funktion von A aufrufen. Schließlich hat man einen Zeiger auf ein A. Wenn die Funktion aber virtual ist, dann wird geguckt, ob an der Stelle vielleicht ein von A abgeleitetes Objekt steht und ggf. dessen abgeleitete Version dieser Funktion aufgerufen.



  • Necrotos schrieb:

    Also ohne virtual werden die Funktionen aus A aufgerufen, weil es als Objekt vom Typ A gehandhabt wird, aber mit virtual wird dem Objekt mitgeteilt, dass es eigentlich doch von B ist, was ihm vorher nicht bekannt war, und dann werden eben die Funktionen aus B aufgerufen?

    Klassen mit virtuellen Funktionen bekommen in der Regel (siehe SeppJ's Kommentar dazu) eine Struktur zugewiesen, in der Überschreibungen für diese virtuellen Funktionen drinstehen. In diese Struktur schaut der Compiler rein, wenn du b2->print2() machst. Weil print2 überschrieben wurde, ruft er also die Funktion auf, die er an der Stelle findet - und das ist nun mal B::print2 .

    print1() hat so eine Überschreibung nicht bekommen, es existiert kein Eintrag in der vtable . Deswegen ruft der Compiler, weil das Objekt ein A ist, halt auch A::print1 auf.

    Ich glaube, damit bleiben keine Fragen offen.

    EDIT: C# ist (zumindest, als ich die Sprache gelernt habe) ein bisschen deutlicher in der Namensgebung. Funktionen in Basisklassen werden als virtual deklariert, Funktionen in abgeleiteten Klassen mit override (weil die Funktionen halt überschrieben werden). In C++ gibt man nur virtual in der Basisklasse an - das virtual in der abgeleiteten Klasse ist optional.



  • dachschaden schrieb:

    EDIT: C# ist (zumindest, als ich die Sprache gelernt habe) ein bisschen deutlicher in der Namensgebung. Funktionen in Basisklassen werden als virtual deklariert, Funktionen in abgeleiteten Klassen mit override (weil die Funktionen halt überschrieben werden). In C++ gibt man nur virtual in der Basisklasse an - das virtual in der abgeleiteten Klasse ist optional.

    Aber auch nur vor C++11. Mit C++11 gibt es das override keyword.
    Damit kann man in einer abgeleiteten Klasse eine methode als "überschreibt die gleiche methode der basis klasse" markieren.
    Wenn der compiler auf dieses keyword stößt so kann er überprüfen ob in der Basis klasse wirklich die gleiche Methode existert und als virtual markiert ist.
    Und im Fehlerfall wird eine Warnung/Fehlermeldung beim Übersetzen generiert.



  • In diese Struktur schaut der Compiler rein, wenn du b2->print2() machst.

    Also um das mit meinen eigenen Worten zu formulieren: Bei der Funktion ohne virtual wird die Funktion aus A ausgeführt, weil b2 ein Zeiger vom Typ A ist.
    Mit virtual wird nachgeschaut welche der beiden Funktionen ausgeführt werden soll. in dem Fall hier wird dann die Funktion aus B ausgeführt, weil da ein von A abgeleitetes Objekt ist (begingt durch new B() ).

    Entschuldigt bitte wenn die Formulierung nicht einwandfrei ist, ich hab mit C++ erst vor kurzem angefangen, deswegen bin ich mir auf einigen Gebieten noch nicht wirklich sicher.



  • Necrotos schrieb:

    Also um das mit meinen eigenen Worten zu formulieren: Bei der Funktion ohne virtual wird die Funktion aus A ausgeführt, weil b2 ein Zeiger vom Typ A ist.
    Mit virtual wird nachgeschaut welche der beiden Funktionen ausgeführt werden soll.

    Jein.
    Wenn auch nur eine Funktion in deiner Klasse virtual markiert wurde, wird die vtable oder was auch immer angelegt. Und dann wird immer da reingeschaut bei Funktionsaufrufen. Und wenn eine Funktion nicht drin ist, weil da halt nur virtuelle Funktionen reinkommen, dann wird die Basisklassenfunktion aufgerufen, wie sonst halt auch. Aber sobald eine überschriebene Funktion da gefunden wurde, wird die aufgerufen.

    Vom Endergebnis ist die Erklärung die gleiche wie deine Zusammenfassung, aber die Implementierung ist unterschiedlich.



  • Bei jedem Funktionsaufruf wird also nachgeschaut ob die Funktion überschrieben wurde. Wenn eine überschriebene Funktion gefunden wird, wird die auch aufgerufen.

    Diese Formulierung verwirrt mich leicht, das klingt so als ob die virtuelle Funktion immer aufgerufen wird, unabhängig davon ob das Objekt zu A oder B gehört.



  • Necrotos schrieb:

    Bei jedem Funktionsaufruf wird also nachgeschaut ob die Funktion überschrieben wurde. Wenn eine überschriebene Funktion gefunden wird, wird die auch aufgerufen.

    Muss halt, nicht?
    Früher (was heißt früher; so macht man das auch noch in C) hat man Funktionszeiger verwendet, wenn man zur Laufzeit bestimmen wollte, welche Funktion aufgerufen wird. Und Interpretercode, bei dem es eine Menge Instruktionen gibt, macht man durch Branch Prediction, was CPUs heutzutage unterstützen, richtig lahm, deswegen macht man dann sowas branchless. Und dann kommen Funktionszeiger dran.

    Und das Problem ist ja, dass der Compiler nicht weiß, was für ein Objekt da jetzt hintersteckt. Du kannst ja deine Basisklasse noch ganz woanders ableiten lassen und Objekte davon in einen Zeiger auf die Basisklasse packen. Deswegen muss bei Funktionsaufrufen nachgeschaut werden, welche Funktion aufgerufen wird. Das sind dann die Funktionszeiger, die du so gar nicht mehr siehst, aber sie sind da.

    Necrotos schrieb:

    Diese Formulierung verwirrt mich leicht, das klingt so als ob die virtuelle Funktion immer aufgerufen wird, unabhängig davon ob das Objekt zu A oder B gehört.

    Nee, nee. In die Tabelle/Struktur/Whatever wird immer reingeschaut. Was gefunden wird, wird aufgerufen. Und wenn's nur der Default ist, auch bekannt als die Funktion der Basisklasse.



  • Necrotos schrieb:

    Bei jedem Funktionsaufruf wird also nachgeschaut ob die Funktion überschrieben wurde.

    Nein. Zumindest nicht so, wie ich den Satz verstehen würde. Jedes Objekt hat einen Zeiger auf eine komplette vtable. Der Compiler kümmert sich zur Kompilierzeit darum. Wenn die Klasse eine Funktion der Basisklasse überschreibt, kommt an der entsprechenden Stelle in der Tabelle ein Zeiger auf die Funktion der abgeleiteten Klasse, ansonsten ein Zeiger auf die Funktion der Basisklasse.
    Es ist auf jeden Fall nur ein Funktionszeiger, zur Laufzeit muss man so gesehen nichts "nachschauen".



  • Vielen Dank Leute! Die Frage hat viel weiter geführt als gedacht.



  • ---> Unsinn.



  • Gut, vielleicht versteh ichs nicht unbedingt. Aber ich hab jetzt eine grobe Ahnung was im Hintergrund passiert. Sehr grob.


Anmelden zum Antworten