Vererbung- was geschieht mit Elementfunktionen und virtuellen Elementfunktionen?
-
Hey ihr,
In meinem Buch gehts jetzt um Vererbung - und natürlich kommen jetzt bei mir schon die ersten Verständnisprobleme.
Ich verstehe nämlich nicht so ganz, wie das ist, wenn ich in meiner Basisklasse eine Funktion habe und in der abgeleiteten Klasse eine Funktion mit dem selben Namen.
Habe folgenden Code ausgeführt, welcher mit sagt, dass die neue Funktion die alte einfach überschreibt:#include <iostream> using namespace std; struct Basis { void ausgabe() { cout << "BASISKLASSE" << endl; } }; struct Abgelitten : public Basis { void ausgabe() { cout << "ABGELITTENE KLASSE" << endl; } }; int main() { Basis B; Abgelitten A; B.ausgabe(); A.ausgabe(); system("Pause > NUL"); }Wie erwartet überschreibt die neue Funktion die alte.
Das funktioniert auch dann, wenn ich übergabe und Rückgabetyp ändere.Nun steht in meinem Buch allerdings weiter, dass Funktionen, welche ich in abgeleiteten Klassen vorhabe nochmal zu überschreiben als "virtual" gekennzeichnet werden sollen.
Allerdings verstehe ich nicht ganz, was das bringen soll, weils ja in meinem Beispiel auch ohne funktioniert.Das virtual scheint nur dann etwas zu bringen, wenn ich an eine Funktion, welche eine Referenz auf den Typ der Basisklasse erwartet eine Referenz auf den Typ der abgeleiteten Klasse übergebe.
Dann wird nämlich der "Basisklassenteil" des abgeleiteten Objektes mit der Referenz adressiert und nur virtuelle Funktionen können dann überschrieben werden.
Allerdings dachte ich, dass wenn ich ein Objekt des abgeleiteten Typs erstelle Funktionen des Basistyps, welche auch im abgeleiteten Typ vorkommen ohnehin überschrieben werden.
#include <iostream> using namespace std; struct Basis { virtual void ausgabe() { cout << "BASISKLASSE" << endl; } }; void Ausgabe(Basis&); struct Abgelitten : public Basis { void ausgabe() { cout << "ABGELITTENE KLASSE" << endl; } }; void Ausgabe(Basis& b) { b.ausgabe(); } int main() { Basis B; Abgelitten A; Ausgabe(A); system("Pause > NUL"); }Hier dürfte doch nun die Referenz 'b' nichts mehr mit der Ausgabefunktion in der Basisklasse zutun haben.
Was ist genau der Unterschied zwischen virtuellen und normalen Funktionen?
Ich find das irgendwie schwer nachzuvollziehen
cya
David
-
777 schrieb:
..."ABGELITTENE KLASSE"...

An der Klasse gab es auch wirklich viel zu leiden....

@Topic: Eben ! Gerade WEIL die Funktion Ausgabe() nichts mit Basis::ausgabe() zu tun hat, entscheidet das übergebene Objekt selbst, welchen Typ es hat (= welche Memberfunktion ausgerufen wird).
Gruß,
Simon2.
-
Bei virtuellen Funktionen gehts um Polymorphie. Du kannst, wenn du eine Klasse von einer anderen ableitest, die "abgelittene" (schoenes wortspiel) auch ueber einen Pointer oder eine Referenz auf die Basisklasse ansprechen, da jedes "Abgelitten" auch ein "Basis" ist (z.B. jedes Auto ist ein Fahrzeug):
int main() { Basis B; Abgelitten A; Basis* pb = &A; //Basis-pointer Zeigt auf Abgelitten-Objekt Basis& rb = A; //Basis-referenz bezieht sich auf Abgelitten-Objekt pb->ausgabe(); rb.ausgabe(); }Wenn ausgabe() nicht virtuell deklariert ist, dann ruft der Compiler die Methode abhaengig vom statischen Typ des Zeigers/der Referenz auf. Statischer Typ ist der Typ, mit dem der Zeiger deklariert wurde, hier also Basis. Das obige Beispiel gibt also beidemal "BASISKLASSE" aus.
Wird ausgabe() allerdings virtuell deklariert, dann schaut das Programm zur laufzeit nach, was der dynamische Typ des Zeigers bzw. der Referenz ist. Der dynamische Typ ist der Typ des Objekts auf das sich der Zeiger bezieht, hier also Abgelitten. Wenn ausgabe() also virtuell deklariert ist, dann sieht der Compiler zwar, das pb vom Typ Basis* ist, findet aber zur Laufzeit raus, dass das Objekt dahinter vom Typ Abgelitten ist und ruft daher Abgelitten::ausgabe() auf, das gleiche bei Referenzen.
-
Ohne "virtual" wird die Methode einfach ueberdeckt. Das heisst, es wird immer die "zuletzt" deklarierte Methode aufgerufen. Der Aufruf der Method wird einfach nur durch den statischen Typ der Variable definiert.
struct Foo { void Test(); }; struct Bar: public Foo { void Test(); }; // ... void Test1(Foo* pfoo) { pfoo->Test(); // ruft immer Foo::Test auf, auch wenn pfoo eigentlich auf ein Bar-Objekt zeigt }Bei als virtual deklarierten Methoden wird abhaengig vom dynamischen Typ entschieden, welche Methode aufgerufen wird.
struct Foo { virtual void Test(); }; struct Bar: public Foo { virtual void Test(); }; // ... void Test1(Foo* pfoo) { pfoo->Test(); // ruft je nach dynamischen Typ Foo::Test oder Bar::Test auf }Gruss,
DeSoVoDaMu
-
Super! Ihr seit echt spitze, dass ihr so schnell und ausführlich antwortet!

Ich mag das Wort abgelittene Klasse auch.
Also ich hab mir das ganze Zeug mit Objekten immer so vorgestellt, dass wenn ich eine Klasse instanziere am Ende ein Kontainer rauskommt, welcher Funktionen und Attribute enthält:
In etwa sowas:
_________________ KlasseXY ----------------- int a; int hallo(void); _________________Erbt nun eine Klasse von der KlasseXY, so entsteht der folgende Kasten:
_________________
Leidendeklasse : public KlasseXY ----------------- Basiselemente: int a; int hallo(void); ----------------- Neue Elemente: int b; int c; int was(int); int hallo(int); //Das alte "hallo" wird nicht überschrieben, //sondern es gibt jetzt 2 hallos. //Für Benutzer siehts aber so aus, als würde //"hallo" überschrieben. //Das alte hallo kommt nur dann zum tragen, wenn ich einen Zeiger //des Basistyps auf Leidensklasse zeigen lasse und hallo //über diesen Zeiger aufrufe. //Es wird nur dann die neue Funktion aufgerufen, wenn hallo //virtual ist. _________________Stimmt das so von der reinen Logik her?
Weil es muss ja das alte "hallo" auch noch irgendwie in der Instanz des abgeleiteten Objektes sein, weil über einen Zeiger das alte noch aufgerufen wird:#include <iostream> using namespace std; struct Basis { void ausgabe() { cout << "BASISKLASSE" << endl; } }; void Ausgabe(Basis&); struct Abgelitten : public Basis { void ausgabe() { cout << "ABGELITTENE KLASSE" << endl; } }; void Ausgabe(Basis& b) { b.ausgabe(); } int main() { Abgelitten A; Ausgabe(A); A.ausgabe(); system("Pause > NUL"); }In der Instanz 'A' befinden sich beide Funktionsdefinitionen.
Ich glaub, wenn das jetzt alles so stimmt hab ichs endlich verstanden *uff*^^
-
777 schrieb:
...Ich mag das Wort abgelittene Klasse auch....
So wie in: "Gestern habe ich beim Sport so geschweißt, dass mir die die Nase so juckte, dass ich genossen habe."

Deine Vorstellung ist ein wenig ungenau aber nicht verkehrt.
Für mich wird das Ganze dann klar, wenn ich mir das über "vtables" vorstelle. Das ist eine Technik, wie nicht wenige Compilerhersteller das realisieren, die aber nicht im Standard vorgeschrieben ist.
Grob gesagt taucht überall da, wo im Code eine Funktion aufgerufen wird, im compilierten Code eine "Sprungadresse" auf, die auf das Stückchen Speicher verweist, an der der Code der Funktion steht.
Bei virtual functions geht es da einen Schritt weiter: Dort steht die "Adresse(0) auf eine Adresse(1), an der der Code der Funktion" steht"
An diese Adresse(1) wird dann vom abgeleiteten Objekt ein anderer Wert hineingeschrieben ... = Adresse(0) verweist nicht mehr auf eine Adresse(1) von Basis::f() sondern auf Abgeleitet::f().
Für jede virtual function wird nun eine eigene Adresse(0) angelegt und in einer Tabelle ("virtual function table" = "vtable") organisiert.Ist nur ein grober Abriss, aber er erklärt eine ganze Menge. Z.B. auch, warum der Aufruf virtueller Funktionen im Ctor (= vtable noch nicht endgültig gefüllt) unsicher ist.
Gruß,
Simon2.
-
Ob die Funktion nun im Speicher doppelt oder als einzige Adresse vorkommt ändert ja am Prinzip nichts.
Ich find die von Simon beschriebene Methode jedenfalls einleuchtend und sehr interessant, weil sie ein wenig vom Instanzdenken wegführt.
Aber ich bin froh, dass mir das jetzt endlich einleuchtend vorkommt - C++ lernen ist schon irgendwie ein hartes Stück Arbeit^^Vielen Dank für eure Mühen
cya
David
-
aber im Bezug auf Geschwindigkeit gesehen, sind die vtables schon langsamer, da ja immer quasi noch ein Indirektionsschritt dazu genommen werden muss, um über vtables zu gehen, anstatt die Methode quasi direkt aufzurufen
-
Vorden schrieb:
aber im Bezug auf Geschwindigkeit gesehen, sind die vtables schon langsamer, da ja immer quasi noch ein Indirektionsschritt dazu genommen werden muss, um über vtables zu gehen, anstatt die Methode quasi direkt aufzurufen
Und?

-
Vorden schrieb:
aber im Bezug auf Geschwindigkeit gesehen, sind die vtables schon langsamer, da ja immer quasi noch ein Indirektionsschritt dazu genommen werden muss, um über vtables zu gehen, anstatt die Methode quasi direkt aufzurufen
Tja,
"mehr" gibt's eben nie umsonst ... auch kein "Mehr" an Flexibilität.
(auch wenn noch viele "Entscheider" das meinen)Gruß,
Simon2.