Programmieren in C++ Vererbung
Inhalt Vererbung Konstruktoren und Destruktor von abgeleiteten Klassen Typkonvertierung von Zeigern Identifikation der Klasse eines Objekts Überladen von Methoden Verdecken und Überschreiben Polymorphie Überschreiben von Methoden Vererbung bzw. Überschreiben unterbinden Zuweisungsoperator in abgeleiteten Klassen Zugriffsrechte Mehrfachvererbung 6-2
Vererbung: Beispiel Person/Student Superklasse (Basisklasse, Oberklasse, Vaterklasse) Klasse Person mit Eigenschaften Datenfelder: Nachname, Vorname Methoden: setnachname(), setvorname(), print() Subklasse (abgeleitete Klasse, Unterklasse, Sohnklasse) erbt Eigenschaften der Superklasse fügt eigene Eigenschaften dazu matrikelnummer setmatrikelnummer() printmatrikelnummer() 6-3
Realisierung der Klasse Person in C++ // in h-datei class CPerson { string m_nachname; string m_vorname; // Aggregation: Person hat einen Nachnamen // Aggregation: Person hat einen Vornamen public: void setnachname(const string& nachname) { m_nachname = nachname; } void setvorname(const string& vorname) { m_vorname = vorname; } void print() const; }; // in cpp-datei void CPerson::print() const { cout << "Nachname: " << m_nachname << endl; cout << "Vorname: " << m_vorname << endl; } 6-4
Realisierung der Klasse Student in C++ // in h-datei: Vererbung: ein Student ist eine Person class CStudent : public CPerson { // die Klasse CStudent wird von der Klasse CPerson abgeleitet // und erbt alle Attribute und Methoden der Klasse CPerson int m_matrikelnummer; public: // neue Methoden der Klasse CStudent void setmatrikelnummer(int nr) { m_matrikelnummer = nr; } void printmatrikelnummer() const; }; // in cpp-datei void CStudent::printMatrikelnummer() const { cout << "Matrikelnummer: " << m_matrikelnummer << endl; } 6-5
Einsatz der Klasse CStudent int main () { CPerson pers; pers.setnachname("mueller"); pers.setvorname("peter"); pers.print(); CStudent student; student.setnachname("maier"); student.setvorname("fritz"); student.setmatrikelnummer(56123); student.print(); student.printmatrikelnummer(); } CPerson pers2 = student; pers2.print(); // nicht aber: pers2.printmatrikelnummer(); return 0; 6-6
Konstruktoren in abgeleiteten Klassen Idee jeder abgeleitete Konstruktor initialisiert nur die neuen Datenfelder vererbte Datenfelder werden vom Konstruktor der Basisklasse initialisiert Umsetzung in C++ In der Initialisierungsliste des Konstruktors wird der Konstruktor der Basisklasse aufgerufen falls kein expliziter Aufruf eines Konstruktors der Basisklasse erfolgt, wird der Standardkonstruktor der Basisklasse implizit aufgerufen möglicher Inhalt der Initialisierungsliste Attribute der Klasse, aber keine geerbten Attribute Konstruktoraufruf der Basisklasse 6-7
Beispiel zu abgeleiteten Konstruktoren class CPerson { string m_name; string m_vorname; public: } CPerson(const string& name, const string& vname) : m_name(name), m_vorname(vname) { } cout << "K. von CPerson" << endl; class CStudent: public CPerson { int m_matnr; public: } CStudent(const string& name, const string& vname, int m) : CPerson(name, vname), m_matnr(m) { } cout << "K. von CStudent" << endl; im Testprogramm: CStudent studi("uwe", "Ochsenknecht", 123456); Ausgabe: K. von CPerson K. von CStudent 6-8
Konstruktoren erben (C++11) Grundsatz normalerweise erbt eine Klasse C keine Konstruktoren ihrer Basisklassen somit stehen nur die Konstruktoren der Klasse C zur Initialisierung von Instanzen der Klasse C zur Verfügung In C++11 können alle Konstruktoren der Basisklasse geerbt werden class CStudent: public CPerson { int m_matnr = 0; public: } using CPerson::CPerson; // Initialwert ist hier wichtig CStudent(const string& name, const string& vname, int m) // erbt alle Konstruktoren der Klasse CPerson : CPerson(name, vname, m_matnr(m) { cout << "K. von CStudent" << endl; } Das System verwendet den geerbten Konstruktor der Klasse CPerson und erzeugt einen Konstruktor für die Klasse CStudent mit der gleichen Deklaration. Der erzeugte Konstruktor der Klasse CStudent ruft den Konstruktor der Klasse CPerson mit den gleichen Argumenten auf. 6-9
Destruktor einer abgeleiteten Klasse Konzept der Destruktor einer abgeleiteten Klasse ruft nach Ausführung seines Methodenkörpers den Destruktor der Basisklasse implizit auf dynamische Attribute können im Destruktor zuerst gelöscht werden, bevor Attribute der Basisklasse gelöscht werden Wann soll ein Destruktor ausprogrammiert werden? wenn die Klasse Attribute enthält, welche dynamisch erzeugt worden sind, so sollen diese Attribute im Destruktor wieder gelöscht werden Wird der Destruktor auch bei einem statisch erzeugten Objekt aufgerufen? Ja! Beim Verlassen des Blocks, in dem das Objekt erstellt worden ist, wird zuerst der Destruktor aufgerufen, bevor das Objekt vom Stack entfernt wird. 6-10
Typkonvertierungen von Zeigern Typ einer Zeiger- oder Referenzvariable muss nicht gleich dem Typ des Objektes sein, auf welches die Zeiger-/Referenzvariable verweist bisher: neu: CStudent *pstud = new CStudent(); CPerson *ppers = new CStudent(); implizite (automatische) Zeigertypkonvertierung (Cast) Sohn *psohn = new Sohn(); Vater *pvater = psohn; explizite Zeigertypkonvertierung (Cast) Sohn *psohn = new Sohn(); Vater *pvater = psohn; Sohn *psohn2 = static_cast<sohn *>(pvater); // implizite Typkonvertierung // implizite Typkonvert. // explizite Typkonvert. 6-11
Gültige Up- und Down-Casts Up-Cast Konvertierung in einen Zieltyp, der in der Vererbungshierarchie weiter oben liegt implizite Konvertierung immer möglich, wenn der Zieltyp ein Vorfahre ist Down-Cast Konvertierung in einen Zieltyp, der in der Vererbungshierarchie weiter unten liegt nur explizite Konvertierung möglich nur möglich, wenn die Referenz auf ein Objekt des Zieltyps oder einer abgeleiteten Klasse des Zieltyps zeigt Unzulässige explizite Typkonvertierung Verwendung von C-Cast oder static_cast kann zu einem Fehler zur Laufzeit führen Verwendung von dynamic_cast gibt 0 zurück, bei Zeigervariablen wirft bad_cast Exception bei Referenzvariablen 6-12
Beispiele zu (un-)gültigen Casts class A { int m_a; }; class B: public A { int m_b; }; class C: public B { int m_c; }; class D { int m_d; }; public static void main(string[] args) { B *b = new C(); // Referenz vom Typ B auf ein neues // Objekt der Klasse C. // Impliziter Cast nach B. A *a = b; C *c = (C *) b; // Impliziter Cast zu einer Basisklasse. // Expliziter Cast zur ursprüngl. Klasse C *c2 = (C *) a; // Expliziter Cast von A zur // ursprünglichen Klasse. } D *d = (D *) b; // Fehler, da D nichts mit C zu tun hat. 6-13
Identifikation der Klasse eines Objektes Problem ein Down-Cast muss gültig sein, sonst kann es zu einem Laufzeitfehler kommen wie kann man erkennen, ob ein Down-Cast gültig ist? Lösung Verwendung von Run-Time Type Information (RTTI) dynamic_cast < Zieltyp >( Zeigervariable ) bei ungültiger Konvertierung wird 0 zurückgegeben Beispiele Vater *pv1 = new Vater(); Vater *pv2 = new Sohn(); Sohn *s1 = dynamic_cast<sohn *>(pv2); // gültiger Down-Cast s1 == pv2 Sohn *s2 = dynamic_cast<sohn *>(pv1); // ungültiger Down-Cast s2 == 0 6-14
Typinformation (bei RTTI) Wie funktioniert dynamic_cast? Für jeden verwendeten Typ existiert eine Typinformation. Diese Typinformation kann in einem speziellen Typinformationsobjekt abgelegt sein. Operator typeid Syntax: type_info& t = typeid(* Zeigervariable); gibt Referenz auf Typ-Informationsobjekt zurück Voraussetzungen RTTI wird verwendet #include <typeinfo> Beispiel Vater *pvater = new Vater(); Vater *psohn = new Sohn(); const type_info& t1 = typeid(*pvater); const type_info& t2 = typeid(*psohn); if (t1 == t2) cout << "beide Typen sind gleich" << endl; if (t1.before(t2)) cout << "t1 ist eine Basisklasse von t2" << endl; 6-15
Überladen von Methoden Methoden überladen Methoden mit dem gleichen Namen, aber mit unterschiedlicher Parameterliste werden angeboten Szenario Basisklasse und abgeleitete Klasse enthalten beide eine Methode f void Vater::f(char) void Sohn::f(int) Welche Methode wird aufgerufen? Sohn s; s.f(5); s.f( A ); // Sohn::f wird aufgerufen, ok! // Sohn::f wird aufgerufen, warum? Wie kann f von Vater aufgerufen werden? wenn die Sohn-Klasse die Methode f ihres Vaters anbietet: using Vater::f; oder explizit aufrufen: s.vater::f( A ); 6-16
Verdecken und Überschreiben Verdecken abgeleitete Klassen können Datenfelder enthalten, die den gleichen Namen haben wie Datenfelder in den Basisklassen sollte wenn möglich vermieden werden Überschreiben abgeleitete Klassen können Methoden enthalten, die die genau gleichen Signaturen haben wie Methoden in den Basisklassen wird sinnvoll eingesetzt 6-17
Verwendung verdeckter Attribute class Vater { protected: int x; }; class Sohn: public Vater { int x; public: Sohn(int xx) : x(xx) { cout << "x des Sohnes: " << x << endl; cout << "x des Sohnes: " << this->x << endl; cout << "vom Vater geerbtes x: " << Vater::x << endl; cout << "vom Vater geerbtes x: " << static_cast<vater *>(this)->x<< endl; } }; 6-18
Polymorphie (Vielgestaltigkeit) Polymorphie von Operationen gleiche Methodenaufrufe in verschiedenen Klassen führen zu klassenspezifischen Anweisungsfolgen Beispiel: pperson->print() vs. pstudent->print() Polymorphie von Objekten (nur bei Vererbungshierarchien) an die Stelle eines Objektes in einem Programm kann auch ein Objekt einer abgeleiteten Klasse treten ein abgeleitetes Objekt ist polymorph: es kann sich auch als Objekt einer Basisklasse ausgeben Beispiel: ein Student verhält sich wie ein Student, kann sich aber auch wie eine Person verhalten 6-19
Überschreiben von Methoden (1) Idee in einer abgeleiteten Klasse wird eine Methode überschrieben die überschriebene Methode hat die gleiche Signatur (Name und Parameterliste) Beispiel und den gleichen Rückgabetyp oder bei Referenz-/Zeigertyp auch eine Spezialisierung davon class CPerson { virtual void print() { } }; class Cstudent : public CPerson { }; virtual void print() override { // in C++11 soll die Methode explizit als } CPerson::print(); // überschrieben gekennzeichnet werden // Aufruf der Methode print() aus der Basisklasse 6-20
Überschreiben von Methoden (2) statische Bindung: Methoden sind nicht virtual der statische Typ des Objekts, Zeigers oder Referenz entscheidet über die Wahl der aufgerufenen Methode dynamische Bindung: Methoden sind virtual Polymorphie kommt nicht zum Einsatz falls die Methode nicht über einen Zeiger bzw. Referenz aufgerufen wird Polymorphie kommt zum Einsatz und der dynamische Typ des Zeigers oder der Referenz entscheidet über die Wahl der aufgerufenen Methode Beispiele Vater v, *pv; Sohn s, *ps = new Sohn(); v = s; v.do_it(); pv = ps; pv->do_it(); Vater& rv = s; rv.do_it(); // do_it() der Klasse Vater wird aufgerufen // do_it() der Klasse Sohn wird aufgerufen // do_it() der Klasse Sohn wird aufgerufen 6-21
Vererbung unterbinden (C++11) Vererbung einer Klasse verunmöglichen mit final markierte Klasse kann nicht abgeleitet werden Beispiel class B { }; class C final : B { }; class D : C { }; // führt zu einer entsprechenden Fehlermeldung Überschreiben von Methoden verunmöglichen mit final markierte Methode darf in abgeleiteter Klasse nicht überschrieben werden Beispiel struct B { virtual void f(int) {} }; struct C : B { void f(int) final override {} }; struct D : C { void f(char) {} void f(int) {} }; // neue Methode, weil andere Signatur // führt zu einer entsprechenden Fehlermeldung 6-22
Zuweisungsoperator und Vererbung anonymes Subobjekt der Oberklasse wird als Element aufgefasst falls für eine abgeleitete Klasse kein eigener Zuweisungsoperator definiert ist, so wird das anonyme Subobjekt der Oberklasse mit seinem zugehörigen Zuweisungsoperator zugewiesen und für die neuen Attribute gilt der übliche Ansatz Probleme im Zusammenhang mit Polymorphie Beispiel: CSohn sohn1, sohn2; CVater& rvater1 = sohn1; CVater& rvater2 = sohn2; rvater1 = sohn2; // CVater::operator=(const CSohn&)? Fehlermeldung virtueller Zuweisungsoperator in C++03 implizit generierter Zuweisungsoperator ist nie virtuell virtuelle Methode assign() stattdessen verwenden virtueller Zuweisungsoperator in C++11 virtual CVater& operator=(const CVater&) = default; 6-23
Statische und dynamische Bindung Bindung Zuordnung eines Methodenrumpfes zum Aufruf einer Methode statische (frühe) Bindung Zuordnung erfolgt zum Kompilierzeitpunkt wird für alle nicht-virtuellen Methoden verwendet dynamische (späte) Bindung Zuordnung erfolgt erst zur Laufzeit des Programms sehr mächtiges Konzept, weil es die Wiederverwendung von Programmcode drastisch erhöht muss explizit mit dem Schlüsselwort virtual deklariert werden 6-24
Schlüsselwort virtual Sinnvolle Verwendung wird eine Methode in einer Basisklasse als virtual deklariert, so sind auch alle überschriebenen Varianten davon virtual Destruktoren von Klassen, welche dynamischen Speicher verwenden, sollten virtual sein, um sicherzustellen, dass die Objekte vollständig gelöscht werden, auch dann wenn der statische Typ eines Zeigers nicht vom gleichen Typ wie das zu löschende Objekt ist eine virtuelle Methode kann (pure virtual) also ohne Implementierung und somit abstrakt sein virtual void methodname() = 0; eine Klasse mit mindestens einer abstrakten Methode ist selber abstrakt Unsinnige bzw. unerlaubte Verwendung Konstruktoren können nicht virtual sein virtuelle private Methoden sind unsinnig, da die Zugriffsrechte nicht ausreichen, um überschriebene Methoden der Basisklasse aufzurufen 6-25
Zugriffsrechte Zugriffsrechte der Basisklasse Basisklasse geerbt als Zugriffsrechte der abgeleiteten Klasse public protected private public protected private public protected private public protected private public protected No access 1 protected protected No access 1 private private No access 1 1 Unless friend declarations within the base class explicitly grant access. 6-26
Mehrfachvererbung Beispiels aus der Welt der grafischen Objekte hier mit gemeinsamer Basisklasse (ist nicht notwendig) Probleme: Namenskonflikte, Mehrdeutigkeiten - text «abstract» ObjMitText + zeichnen() «abstract» GraphObj - position + getpos() + zeichnen() Rechteck - breite, hoehe + zeichnen() RechteckMitText + zeichnen() 6-27
Probleme der Mehrfachvererbung Beispiel Rechteck r(0, 0, 20, 50); RechteckMitText br(10, 5, 60, 60, "Text"); r.zeichnen(); // ruft zeichnen() von Rechteck auf br.zeichnen() // ruft zeichnen() von RechteckMitText auf Position rpos = r.getpos(); Position brpos = br.getpos(); GraphObj *pobj = &br; // gibt Ursprung des Rechtecks zurück // Compiler-Fehler // Compiler-Fehler Warum ein Compiler-Fehler? br.getpos() ist nicht eindeutig, denn es könnte getpos() von RechteckMitText oder von Reckteck aufgerufen werden Ursache: Teilobjekt position ist zweimal vorhanden und nicht beide Teilobjekte müssen identisch sein 6-28
Lösung: Virtuelle Basisklassen class Rechteck : virtual public GraphObj { }; // Rest normal class ObjMitText : virtual public GraphObj { }; // Rest normal class RechteckMitText : public ObjMitText, public Rechteck { public: RechteckMitText(int x, int y, int w, int h, string text) : ObjMitText(-2, -2, text), Rechteck(-1, -1, w, h), GraphObj(x, y) {} // Rest normal }; 6-29
Virtuelle Basisklassen und Initialisierung Definition vollständiges Objekt: Objekt, das nicht als Teilobjekt dient, also nicht in einem andern Objekt durch Vererbung enthalten ist Ausgangslage virtuelle Basisklassen bewirken, dass nur 1 Teilobjekt dieser Basisklasse in Instanzen einer abgeleiteten Klasse angelegt wird Problem welcher Konstruktor ist für die Initialisierung dieses einen Teilobjekts zuständig? im Beispiel: Rechteck( ) oder ObjMitText( )? Antwort Konstruktor der Basisklasse, welcher im Konstruktor eines vollständigen Objektes aufgerufen wird wird kein Konstruktor der Basisklasse explizit aufgerufen, so wird der Standardkonstruktor der Basisklasse verwendet im Beispiel: GraphObj(x, y) wird verwendet 6-30