Vererbung Das Prinzip der Vererbung im tglichen Leben
Vererbung
Das Prinzip der Vererbung im täglichen Leben:
Walter besitzt ein Haus.
Walter erbt nun 10000 Euro. Also besitzt er insgesamt: + 10000
In C++: Erblasser ---> Erbe In C++ darf man einen Erben (abgeleitete Klasse) selbst basteln abgeleitete Basisklasse ---> Klasse
Statt Basisklasse sagt man auch: Oberklasse, Vaterklasse
Statt abgeleiteter Klasse sagt man auch: Unterklasse, Sohnklasse, Subklasse
Eine abgeleitete Klasse wird in C++ wie folgt angegeben: class Ei: public Huhn{ . . . } wird getrennt durch Doppelpunkt Basisklasse Zugriffsart abgeleitete Klasse
Die abgeleitete Klasse (kurz: Ableitung) besitzt automatisch alle Member (Ausnahme: Konstruktor) der Basisklasse.
Beispiel:
Von einem Flachdachhaus wird ein Modell (Draufsicht) erstellt:
Der Einfachheit halber wird nur die Grundfläche (ohne Zimmer, usw. ) des Hauses abgebildet. Aufgabe: Realsieren Sie die Klasse Modell in C++.
class Modell{ private: double l; double b; public: Modell(double ll, double bb); void set. L(double ll); void set. B(double bb); double get. L(); double get. B(); double get. Flaeche(); }
Modell: : Modell(double ll, double bb){ l = ll; b = bb; } void Modell: : set. L(double ll){ l = ll; } void Modell: : set. B(double bb){ b = bb; }
double Modell: : get. L(){ return(l); } double Modell: : get. B(){ return(b); } double Modell: : get. Flaeche(){ return(l*b); }
Jetzt soll ein “genaueres“ Modell des Flachdachhauses erstellt werden: Modell aus Styropor (oder Ton). Aufgabe: Realsieren Sie in C++ die Klasse Gmodell.
Wie kann man sich dabei Schreibarbeit sparen ? Wie kann man Softwareteile dabei wiederverwenden ? Indem man die Vererbung benutzt.
Welche Member der Klasse Modell kann man übernehmen (erben) ? Welche Member kommen neu dazu ?
class Modell{ private: double l; double b; Alle außer dem Konstruktor Welche Member werden vererbt ? Welche Member müssen neu dazu ? public: Modell(double ll, double bb); void set. L(double ll); void set. B(double bb); double get. L(); double get. B(); double get. Flaeche(); }
class Gmodell: public Modell { private: Die reliefförmig double l; dargestellten Member double b; werden geerbt. double h; public: Gmodell(double ll, double bb, double hh); void set. L(double ll); void set. B(double bb); Wie wird eine Vererbung double get. L(); erreicht ? double get. B(); double get. Flaeche(); void set. H(double hh); Die rot dargestellten double get. H(); Member kommen neu double get. Volumen(); dazu. }
Also sieht die Klasse folgendermassen aus:
class Gmodell: public Modell{ private: double h; public: Gmodell(double ll, double bb, double hh); void set. H(double hh); double get. H(); double get. Volumen(); } Die Methoden müssen noch außerhalb der Klassen implementiert werden.
Realisieren Sie die Methoden in C++
void Gmodell: : set. H(double hh){ h = hh; Diese Methode wird von der } Klasse Modell vererbt. double Gmodell: : get. H(){ return(h); } double Gmodell: : get. Volumen(){ return(get. Flaeche()*h); }
Warum ist diese Lösung falsch ? Es gibt Probleme mit dem Zugriffschutz (l und b sind private). Näheres dazu siehe später. double Gmodell: : get. Volumen(){ return(b*l*h); }
Gmodell: : Gmodell(double ll, double bb, double hh): Modell(ll, bb){ set. H(hh); }Da die Klasse Gmodell von der Klasse Modell erbt, muß beim Anlegen eines Objekts der Klasse Gmodell auch der Konstruktor der Klasse Modell aufgerufen werden. Wird dies nicht gemacht, dann wird automatisch (ohne Zutun des Programmierers) der Standardkonstruktor der Klasse Modell aufgerufen. Was passiert, wenn der Standardkonstruktor nicht existiert ? Dann wird dieser automatisch vom Compiler angelegt, außer. . es gibt einen Konstruktor mit (1, 2, 3, . . . ) Parametern. Dann liefert der Compiler eine Fehlermeldung.
Beispiel für die Benutzung der o. g. Klassen in einem Hauptprogramm:
int main(){ Modell m(10, 20); Gmodell g(2, 3, 4); double a, b, c, f, v; a b c f v } = = = m. get. L(); // a=10 m. get. B(); // b=20 m. get. H(); // syntaktisch falsch m. get. Flaeche(); // f=200 m. get. Volumen(); // synt. falsch a = g. get. L(); b = g. get. B(); c = g. get. H(); f = g. get. Flaeche(); v = g. get. Volumen(); return 0; // a=2 // b=3 // c=4 // f=6 // v=24
Verdeckte Member
class Gmodell: public Modell{ Alles gleich wie vorher außer, daß eine private: double h; Methode gleichen Namens wie in der } Basisklasse existiert, die Oberfläche des Modells (Quaders) berechnet. Wie muss sie implementiert werden ? public: Gmodell(double ll, double bb, double hh); void set. H(double hh); double get. H(); double get. Volumen(); double get. Flaeche();
double Gmodell: : get. Flaeche(){ return(2*(get. L()*get. B()+get. L()*h +get. B()*h)); } Warum ist diese Lösung falsch ? Es gibt Probleme mit dem Zugriffschutz (l und b sind private). Näheres dazu siehe später. double Gmodell: : get. Flaeche(){ return(2*(l*b+l*h+b*h)); }
Es wird auf die Methode der Unterklasse und nicht der Oberklasse zugegriffen, weil die Methode der Oberklasse durch die Methode der Unterklasse verdeckt wird. int main(){ Gmodell g(2, 3, 4); double f; Von welcher Klasse wird diese Methode aufgerufen ? f = g. get. Flaeche(); // f=52 f = g. Modell: : get. Flaeche(); // f=6 return 0; } Mit Angabe der Klasse und des sogenannten Scope Operators : : kann man auf die verdeckten Mitglieder zugreifen.
Darstellung der Vererbung durch UML
Basisklasse. . . Attribute Methoden Attribute . . . Methoden Pfeil bedeutet: erbt von Subklasse. . . Analog zum Zugriffsschutz +und bedeutet # : Methode ist protected
Aufgabe: Stellen Sie die Klassen Modell, Gmodell und ihre Vererbungshierarchie in UML dar.
Modell + + + l: double b: double Modell(double ll, double bb) set. L(double ll): void set. B(double bb): void get. L(): double get. B(): double get. Flaeche(): double Statt dem Zugriffschutz + oder kann auch der Zugriffschutz # (siehe später) benutzt werden. Gmodell + + h: double Gmodell(double ll, double bb, double hh) set. H(double hh): void get. H(): double get. Volumen(): double
Zugriffschutz (Zugriffsberechtigung) bei Klassen
Bedeutung der Zugriffsarten:
Zugriff innerhalb einer Klasse bedeutet den Zugriff auf ein Member von innerhalb einer Methode der Klasse. Zugriff auf das Member set. L Beispiel: void Modell: : set. L(double ll){ l = ll; }
Zugriff außerhalb einer Klasse bedeutet den Zugriff auf ein Member von einem Objekt mit Hilfe des Punkts. Zugriff auf das Member set. L Beispiel: int main{ Modell m(2, 8); m. set. L(5); return 0; }
Zugriffschutz (Zugriffsberechtigung) bei Klassen ohne Vererbung
Beispiel:
class Waisenkind{Wo sind die Zugriffe ? Sind diese private: außerhalb/innerhalb ? Sind sie möglich ? int pri. WK; protected: int pro. WK; }; Zugriff innerhalb, möglich public: Zugriff innerhalb, möglich int pub. WK; void f_WK(){ pri. WK=1; pro. WK; pub. WK=3; } Zugriff außerhalb, möglich Zugriff außerhalb, unmöglich int main(){ Waisenkind my. WK; my. WK. f_WK(); Zugriff außerhalb, unmöglich my. WK. pri. WK=10; my. WK. pro. WK=11; Zugriff außerhalb, möglich my. WK. pub. WK=12; return 0; }
class Waisenkind{Welche Regel läßt sich daraus ableiten ? private: int pri. WK; protected: int pro. WK; Zugriff innerhalb ist auf private, protected und public Member möglich. Zugriff außerhalb ist nur auf public Member möglich. public: int pub. WK; void f_WK(){ pri. WK=1; pro. WK; pub. WK=3; } }; int main(){ Waisenkind my. WK; my. WK. f_WK(); my. WK. pri. WK=10; my. WK. pro. WK=11; my. WK. pub. WK=12; return 0; }
Zugriffschutz (Zugriffsberechtigung) bei Klassen mit Vererbung
Auf die nicht vererbten Member einer Klasse gilt die gleiche Regel wie vorher: Zugriff innerhalb ist auf private, protected und public Member möglich. Zugriff außerhalb ist nur auf public Member möglich.
Das Problem ist nur: Wie ist der Zugriffschutz (Zugriffsberechtigung) auf die vererbten Member einer Klasse geregelt ?
Zugriff auf die vererbten Member einer Klasse:
Aus Platzgründen muss im Folgenden auf die Trennung von Deklaration und Implementierung der Methoden verzichtet werden.
Beispiel:
class Mutter{ private: int pri. M; protected: int pro. M; }; public: int pub. M; Zugriffsmanipulation der Member in der Basisklasse und. . .
protected class Kind 1: private Mutter{ private: public void prif. K(){ pri. M=11; pro. M=12; pub. M=13; }. . . Zugriffsmanipulation (der gesamten Basisklasse) bei der Vererbung der Klasse bestimmen die Zugriffsberechtigung: protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } Sind die Zugriffe auf die rotgedruckten Member in der Basisklasse möglich ? }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; }
Insgesamt bestimmen also die "Schalter" Zugriffsmanipulation der Basisklasse und Zugriffsmanipulation der Member der Basisklasse die resultierende Zugriffsberechtigung.
private-Member: Zugriff nur von innerhalb der Klasse möglich protected -Member: Zugriff von innerhalb der Klasse möglich Zugriff von innerhalb einer abgeleiteten Klasse möglich public -Member : Zugriff von innerhalb der Klasse möglich Zugriff von innerhalb einer abgeleiteten Klasse möglich Zugriff von außerhalb einer Klasse aus möglich
int main(){ Zugriff von außerhalb auf ein nicht Kind 1 k 1; vererbtes private Member: nicht erlaubt k 1. prif. K(); k 1. prof. K(); Zugriff von außerhalb auf ein nicht k 1. pubf. K(); vererbtes protected Member: nicht erlaubt k 1. pri. M=105; Zugriff von außerhalb auf ein nicht k 1. pro. M=109; vererbtes public Member: erlaubt k 1. pub. M=113; return 0; } Die restlichen Zugriffe von außerhalb betreffen vererbte Member: bis jetzt noch nicht entscheidbar, muss noch geregelt werden. . . Um welche Zugriffe handelt es sich (innerhalb / außerhalb) ? Bei welchen Anweisungen oben können Sie jetzt schon entscheiden, ob die Zugriffe erlaubt sind oder nicht ? Bei welchen können Sie noch nicht entscheiden ?
Zugriffsberechtigung von der abgeleiteten Klasse auf die vererbten Member der Basisklasse : Vererbung Basisklasse Member in Basisklasse private protected public kein Zugriff protected private protected Man sieht: Die Zugriffsberechtigung der Basisklasse kann durch die Klassenspezifikation vermindert, aber nicht vergrößert werden. public private protected public
Sinn: Member von Klassen, die z. B. für die Gestaltung einer grafischen Oberfläche vom System zur Verfügung gestellt werden, sollen nicht durch eine Vererbung, die ein Programmierer realisiert, vergrößert werden können.
Für die resultierende Zugriffsberechtigung für die vererbten Member gilt die gleiche Regel wie vorher:
Zugriff innerhalb ist auf private, protected und public Member möglich. Zugriff außerhalb ist nur auf public Member möglich. Wobei private, protected und public aus der vorigen Tabelle ermittelt werden müssen !!
Damit gilt dann für den Zugriff von der abgeleiteten Klasse aus:
Vererbung Basisklasse Member in Basisklasse private protected public private - - - protected + + + public + + bedeutet: Zugriff möglich - bedeutet: Kein Zugriff möglich (Compiler)
Damit gilt dann für den Zugriff von außerhalb:
Vererbung Basisklasse Member in Basisklasse private protected public private - - - protected - - - public - - +
Gesamtergebnis: R 1) Zugriff auf ein vererbtes Member: a) Regel anwenden R 2) Zugriff auf ein nicht vererbtes Member: a) Resultierende Zugriffsberechtigung aus Tabelle ermitteln b) Regel anwenden
Zurück zur vorigen Aufgabe. Welche Zugriffe sind möglich und warum ? (kein Zugriff bedeutet: Fehlermeldung des Compilers)
class Mutter{ private: int pri. M; protected: int pro. M; }; public: int pub. M;
class Kind 1: private Mutter{ private: private + private = kein Zugriff (R 2) void prif. K(){ pri. M=11; pro. M=12; pub. M=13; } protected + private public + private = private (R 2) private + private = kein Zugriff (R 2) protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } public + private protected + private = private (R 2) private + private = kein Zugriff (R 2) }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; } protected + private public + private = private (R 2)
int main(){ Kind 1 k 1; k 1. prif. K(); k 1. prof. K(); k 1. pubf. K(); k 1. pri. M=105; k 1. pro. M=109; k 1. pub. M=113; return 0; } no: private (R 1) no: protected (R 1) yes: public (R 1) no: private + private = kein Zugriff (R 2) no: protected + private = private (R 2) no: public + private = private (R 2) no bedeutet : kein Zugriff möglich yes bedeutet : Zugriff möglich Voraussetzungen: In der letzten Folie werden die Anweisungen, die keinen Zugriff verursachen und eine Compilermeldung bringen, entfernt.
Der Klassenzugriffschutz wird von private auf protected geändert. Was passiert ?
class Kind 2: private Mutter{ private: void prif. K(){ pri. M=11; pro. M=12; pub. M=13; } protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; }
class Kind 2: Mutter{ private: void prif. K(){ pri. M=11; pro. M=12; pub. M=13; } protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; }
class Kind 2: protected Mutter{ private: private + protected = void prif. K(){ kein Zugriff (R 2) pri. M=11; pro. M=12; pub. M=13; } protected + protected public + protected = protected (R 2) private + protected = kein Zugriff (R 2) protected (R 2) = protected (R 2) private + protected = kein Zugriff (R 2) protected: void profk(){ pri. M=14; pro. M=15; pub. M=16; } protected + protected = public + protected }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; } protected + protected public + protected = protected (R 2)
int main(){ no: private (R 1) Kind 2 k 2; no: protected (R 1) k 2. prif. K(); k 2. prof. K(); yes: public (R 1) k 2. pubf. K(); k 2. pri. M=105; no: private + protected = kein Zugriff (R 2) k 2. pro. M=109; k 2. pub. M=113; no: protected + protected = protected (R 2) return 0; no: public + protected = protected (R 2) } no bedeutet : kein Zugriff möglich yes bedeutet : Zugriff möglich Voraussetzungen: In der letzten Folie werden die Anweisungen, die keinen Zugriff verursachen und eine Compilermeldung bringen, entfernt.
Der Klassenzugriffschutz wird von protected auf public geändert. Was passiert ?
class Kind 3: protected Mutter{ private: void prif. K(){ pri. M=11; pro. M=12; pub. M=13; } protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; }
class Kind 3: Mutter{ private: void prif. K(){ pri. M=11; pro. M=12; pub. M=13; } protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } }; public: void pubf. K(){ pri. M=17; pro. M=18; pub. M=19; }
class Kind 3: public Mutter{ private: private + public = void prif. K(){ kein Zugriff (R 2) pri. M=11; pro. M=12; pub. M=13; } protected + public = public + public protected (R 2) = public (R 2) private + public = kein Zugriff (R 2) protected: void prof. K(){ pri. M=14; pro. M=15; pub. M=16; } public + public protected + public = protected (R 2) = public (R 2) private + public = kein Zugriff (R 2) }; public: void pubfk(){ pri. M=17; pro. M=18; pub. M=19; } protected + public = public + public protected (R 2) = public (R 2)
int main(){ Kind 3 k 3; k 3. prif. K(); k 3. prof. K(); k 3. pubf. K(); k 3. pri. M=105; k 3. pro. M=109; k 3. pub. M=113; return 0; } no: private (R 1) no: protected (R 1) yes: public (R 1) no: private + public = kein Zugriff (R 2) no: protected + public = protected (R 2) yes: public + public = public (R 2) no bedeutet : kein Zugriff möglich yes bedeutet : Zugriff möglich Voraussetzungen: In der letzten Folie werden die Anweisungen, die keinen Zugriff verursachen und eine Compilermeldung bringen, entfernt.
Konstruktoren, Destruktoren bei der Vererbung
Beispiel:
class Basis. Klasse{ public: Basis. Klasse(int i); ~Basis. Klasse(); }; Basis. Klasse: : Basis. Klasse(int i){ cout << "Aufruf Konstr. BK n"); }; Basis. Klasse: : ~Basis. Klasse(){ cout << "Aufruf Destr. BK n"); };
class Sub. Klasse: public Basis. Klasse{ public: Sub. Klasse(int i); ~Sub. Klasse(); }; Sub. Klasse: : Sub. Klasse(int i): Basis. Klasse(i){ cout << "Aufruf Konstr. SK n"); }; Sub. Klasse: : ~Sub. Klasse(){ cout << "Aufruf Destr. SK n"); };
// Welche Ausgaben werden auf dem // Bildschirm erzeugt ? int main(){ Sub. Klasse s(1); return 0; }
Aufruf Konst. Destr. BK SK SK BK Es gilt des weiteren:
Wenn der Konstruktor der Subklasse nicht (weil es der Programmierer vergessen hat zu implementieren) einen Konstruktor der Basisklasse aufruft, dann ruft er automatisch den Standardkonstruktor der Basisklasse auf.
Der Destruktor in der Subklasse ruft automatisch den Destruktor der Basisklasse auf. Wenn der Destruktor der Subklasse nicht implementiert wird, wird er automatisch vom Compiler erzeugt. Dieser automatisch erzeugte Destruktor ruft dann auch den Destruktor der Basisklasse auf.
Wann soll man eine Vererbung sinnvoll einsetzen ?
wenn die folgende Beziehung besteht: Subklasse "ist ein(e)" Basisklasse.
abgeleitete Klasse Basisklasse Beispiele der Vererbung: Ein LKW ist ein Fahrzeug Eine Katze ist ein Tier Eine Tulpe ist eine Pflanze
Der Prozeß, um von einer Basisklasse durch Detaillierung und Konkretiserung zu einer Subklasse zu kommen, nennt man Spezialisierung. Der Prozeß, um von einer Subklasse durch Verallgemeinerung und Abstraktion zur Basisklasse zu kommen, nennt man Generalisierung.
- Slides: 90