C++-Programmierung/ OOP
Aus Wikibooks
Zielgruppe:
Anfänger
Lernziel:
Die objektorientierte Programmierung kennen lernen.
Inhaltsverzeichnis |
Vererbung
[Bearbeiten] Einleitung
Wenn von Objektorientierung gesprochen wird fällt früher oder später auch das Stichwort Vererbung. Auf dieser Seite lernen Sie anhand eines Beispieles die Grundprinzipien der Vererbung kennen.
[Bearbeiten] Die Ausgangslage
Stellen wir uns vor, dass wir in einer Firma arbeiten und in einem Programm sämtliche Personen verwalten die mit dieser Firma in einer Beziehung stehen. Über jede Person sind folgende Daten auf jeden Fall bekannt: Name, Adresse, Telefonnummer. Um alles zusammenzufassen haben wir uns eine Klasse geschrieben mit deren Hilfe wir eine einzelne Person verwalten können: (Aus Gründen der Einfachheit benutzen wir strings)
#include<string>
using namespace std;
class Person{
public:
Person(string Name, string Adresse, string Telefon) :m_name(Name), m_addr(Adresse), m_phon(Telfon){}
string getName(){ return m_name; }
string getAddr(){ return m_addr; }
string getPhone(){ return m_phon; }
void info(){ cout << "Name: " << m_name << " Adresse: " << m_addr << " Telfon: " << m_phone << endl; }
private:
string m_name;
string m_addr;
string m_phon;
};
Dies ist natürlich eine sehr minimale Klasse, aber schließlich geht es hier ja auch um die Vererbung :D.
[Bearbeiten] Gemeinsam und doch getrennt
Die obige Klasse funktioniert ja eigentlich ganz gut, nur gibt es ein kleines Problem. In unserer Firma gibt es Mitarbeiter, Zulieferer, Kunden, Chefs, ... . Diese sind zwar alle Personen sie haben jedoch jeweils noch zusätzliche Attribute (z. B. ein Mitarbeiter hat einen Lohn während ein Kunde eine KundenNr. hat). Jetzt könnten wir natürlich für jeden unterschiedlichen Typ eine eigene Klasse schreiben, den bereits vorhanden Code der Klasse Person hineinkopieren und schließlich die entsprechenden Erweiterungen vornehmen. Dieser Ansatz hat jedoch einige Probleme:
- Er ist unübersichtlich
- Es kann leicht zu Kopierfehlern kommen
- Soll Person geändert werden so muss jede Klasse einzeln bearbeitet werden.
Zum Glück bietet uns C++ aber ein mächtiges Hilfsmittel in Form der Vererbung. Anstatt alles zu kopieren können wir den Compiler anweisen die Klasse Person als Grundlage zu verwenden. Dies wird durch ein Beispiel klarer.
[Bearbeiten] Beispiel
public:
Employee(string Name, string Adresse, string Telefon, int Gehalt, int MitNr):
m_salary(Gehalt),
m_number(MitNr)
:Person(Name,Adresse,Telefon) {}
int getSalary(){ return m_salary; }
int getNumber(){ return m_number; }
private:
int m_salary;
int m_number;
};
[Bearbeiten] Erläuterung des Beispiels
Nun wollen wir uns der Analyse des Beispiels zuwenden um genau zu sehen was passiert ist.
Das wichtigste im Code ist ganz am Anfang: class Employee : public Person. Damit weisen wir den Compiler an, alle Elemente aus Person auch in Employee zu übernehmen (z.B. hat Employee jetzt auch eine info Funktion und eine Membervariable m_name). Eine solche Klasse nennen wir abgeleitet/Kindklasse von Person.
Des weitern rufen wir im Konstruktor auch den Konstruktor von Person auf. Wir sparen uns also sogar diesen Code.
Benutzen können wir die Klasse wie gewohnt, mit der Ausnahme, dass wir jetzt auch alle Methoden von Person aufrufen können:
Mit.info();
cout << Mit.getSalary() << endl;
cout << Mit.getName() << endl;
[Bearbeiten] protected
Zum Schluss erweitern wir noch das Prinzip der Datenkapselung auf die Vererbung erweitern. Bis jetzt kennen wir die Schlüsselwörter public und private. Machen wir folgendes Experiment: Schreiben Sie eine neue Membermethode von Employee und versuchen Sie auf m_name aus der Person-Klasse zuzugreifen. Der Compiler wird einen Fehler ausgeben. Warum?
m_name wurde in der Person-Klasse als private deklariert, das heißt es kann nur von Objekten dieser Klasse angesprochen werden, nicht aber von abgeleiteten Klassen wie z. B. Employee. Um zu vermeiden dass wir m_name als public deklarieren müssen gibt es protected. Es verhält sich ähnlich wie private, mit dem Unterschied, dass auch Objekte von Kindklassen auf mit diesem Schlüsselwort versehene Member zugreifen können.
Methoden (nicht) überschreiben
[Bearbeiten] Einleitung
Als Ausgangspunkt nehmen wir die beiden Klassen Person und Employee aus dem vorhergehenden Abschnitt.
[Bearbeiten] 2 gleiche Namen, 1 Aufruf
Wir haben der Klasse Employee zwar schon neue Funktionen hinzugefügt. Bis jetzt hatten diese einzigartige Namen, die nicht in der Basisklasse Person vorgekommen sind (z. B. get_salary()). Nun machen wir folgenden Versuch: Wir fügen Employee eine Methode void info() hinzu. Da ein solcher Member bereits in Person existiert erwarten wir, dass uns der Compiler einen Fehler ausgibt.
Entgegen unserer Erwartung jedoch wird alles fehlerfrei übersetzt. Dies kommt daher, dass die Methode aus der Elternklasse ganz einfach überschrieben wird (Dies ist aber nur der Fall, wenn die Signaturen der Methoden, also Name, Parameter und Rückgabewert übereinstimmen). Das heißt aber auch, dass wir aufpassen müssen, welches void info() wir aufrufen. Wenn wir nämlich in einer Memberfunktion von Employee void info() aufrufen, so wird nicht die ursprüngliche Funktion aus Person, sondern die „neue“ Implementierung aus Employee ausgeführt.
[Bearbeiten] Die Methode des Vaters aufrufen
Um eine überschriebene Methode der Basisklasse aufzurufen benutzen wir folgenden Syntax:
Private und geschützte Vererbung
[Bearbeiten] Einleitung
Bis jetzt haben wir alle Vererbungen mit dem Schlüsselwort public vorgenommen (dies wird „öffentliche Vererbung“ genannt). Nun werden wir lernen was passiert wenn statt public private bzw. protected verwendet werden.
[Bearbeiten] Private Vererbung
Verwenden wir private so bedeutet dies das alle Membervariablen bzw. Memberfunktion aus der Basisklasse private werden, von außen also nicht sichtbar sind.
[Bearbeiten] Geschützte Vererbung
Geschütze Vererbung verläuft analog zur privaten Vererbung und sagt aus, dass alle Member der Elternklasse im Bereich protected stehen.
[Bearbeiten] Wann wird was benutzt?
Um festzustellen wann welche Art von Vererbung eingesetzt wird gibt es zwei unterschiedliche Arten wie eine abgeleitete Klasse im Verhältnis zu ihrer Basisklasse stehen kann.
- "ist ein" Kann man sagen "Klasse B ist eine Klasse A" so erbt Klasse B public von A. (Beispiel: Ein Arbeiter ist eine Person)
- "hat ein" Kann man sagen "Klasse B hat ein Klasse A Object" so erbt Klasse B private von A. (Beispiel: Ein Mensch hat ein Herz)
Virtuelle Methoden
|
Dieses Kapitel ist leider noch nicht vorhanden… |
|
|
Wenn Sie Lust haben können Sie das Kapitel Virtuelle Methoden selbst schreiben oder einen Beitrag dazu leisten. |
Dynamisch Casten
|
Dieses Kapitel ist leider noch nicht vorhanden… |
|
|
Wenn Sie Lust haben können Sie das Kapitel Dynamisch Casten selbst schreiben oder einen Beitrag dazu leisten. |
Virtuelle Freunde
|
Dieses Kapitel ist leider noch nicht vorhanden… |
|
|
Wenn Sie Lust haben können Sie das Kapitel Virtuelle Freunde selbst schreiben oder einen Beitrag dazu leisten. |
Abstrakte Klassen
[Bearbeiten] Einleitung
Abstrakte Klassen sind Klassen:
- die nur aus einer Deklaration bestehen können.
- von denen niemals Objekte erstellt werden. Keine Instantiierung möglich.
- die oft als sog. Schnittstellen in umfangreichen Anwendungen verwendet werden.
- die oft als Startpunkt(e) einer Vererbungshierarchie gedacht sind.
[Bearbeiten] Deklarieren
Eine Klasse wird dadurch abstrakt, indem man eine ihre Mitgliedsmethoden als rein virtuell deklariert. Dazu schreiben Sie =0 hinter die Deklaration einer virtuellen Methode.
Der Versuch, eine Instanz von dieser Klasse zu erstellen, schlägt fehl:
{
StatusAusgeber instanz; //Instanz von abstrakter Klasse kann nicht erstellt werden
return 0;
};
[Bearbeiten] Verwenden
Nun beschreiben wir die Verwendung von abstrakten Klassen anhand eines Beispiels. Dazu verwenden wir die abstrakte Klasse StatusAusgeber aus dem vorigen Abschnitt.
An gewissen Stellen Ihrer Programme wollen Sie Funktionalität einer Klasse sicherstellen und diese Funktionalität verwenden, ohne auf Funktionalitäten abgeleiteter Klassen eingehen zu müssen.
Wir deklarieren die Klassen Drucker und Bildschirm.
{
unsigned int m_nDruckeAusgefuehrt;
unsigned int m_nLuefterAnzahl;
// Diverse druckerspezifische Attribute
public:
// Diverse druckerspezifische Methoden
void printStatus(void)
{
std::cout
<< "Geraet: Drucker"
<< std::endl
<< "Drucke ausgefuehrt: " << m_nDruckeAusgefuehrt
<< std::endl
<< "Verbaute Luefteranzahl: " << m_nLuefterAnzahl
<< std::endl;
}
};
class Bildschirm : public StatusAusgeber
{
unsigned int m_nLeistungsaufnahmeWatt;
unsigned int m_nDiagonaleAusdehnungZoll;
// Diverse bildschirmspezifische Attribute
public:
// Diverse bildschirmspezifische Methoden
void printStatus(void)
{
std::cout
<< "Geraet: Bildschirm"
<< std::endl
<< "Leistungsaufnahme (Watt): " << m_nLeistungsaufnahmeWatt
<< std::endl
<< "Bildschirmgroesse diagonal (Zoll): " << m_nDiagonaleAusdehnungZoll
<< std::endl;
}
};
Vorteil dieser Vorgehensweise ist die spätere Verwendung von Methoden der Basisklasse, bei denen die Implementierung erzwungen wurde. Der Verwender der abgeleiteten Klasse kann sich darauf verlassen, dass die Methode implementiert wurde, ohne die weiteren Teile der Hierarchie zu kennen.
Wir deklarieren die Klasse GeraeteMitStatusSpeicher
{
static const char * _Trennzeile; // Trennzeile zwischen den Statusangaben
public:
void speichern(StatusAusgeber * GeraetMitStatus)
{
this->push_back(GeraetMitStatus); // Gerätezeiger im Vektor speichern
}
void printStatus(void)
{
std::vector<StatusAusgeber*>::const_iterator it = this->begin();
while (it != this->end()) // Solange der Iterator nicht auf das Ende verweist
{ // Iteration über den gesamten Inhalt
(*it)->printStatus(); // Iterator dereferenzieren, enthaltenen Zeiger verwenden
std::cout << _Trennzeile << std::endl; // Trennzeile ausgeben
it++; // Nächsten möglichen Inhalt auswählen
}
}
};
const char * GeraeteMitStatusSpeicher::_Trennzeile = "---------------"; // statisch in GeraeteMitStatusSpeicher
GeraetMitStatusSpeicher ist von std::vector<StatusAusgeber*> abgeleitet, speichert Zeiger auf StatusAusgeber-Objekte.
Dies ist möglich, da Zeiger eine feste Größe haben. Speicherung von Objekten abstrakter Klassen ist hier nicht möglich, da an dieser Stelle unmöglich die Größe der effektiven Objekte zu erkennen ist, auf die diese Zeiger verweisen. Genau das war uns ja von vornherein klar, weil wir nur an der printStatus()-Methode interessiert sind, deren Existenz durch die abstrakte Basisklasse sichergestellt wird. Egal ob hier ein Drucker, Monitor oder irgendein anderes Objekt hineingerät, das von StatusAusgeber abgeleitet wurde, wir können den Status ausgeben.
- speichern(StatusAusgeber *) speichert einen Zeiger auf ein StatusAusgeber-Objekt
- printStatus() ruft die Methode printStatus() für alle gespeicherten Zeiger auf StatusAusgeber-Objekte auf
Mehrfachvererbung
|
Dieses Kapitel ist leider noch nicht vorhanden… |
|
|
Wenn Sie Lust haben können Sie das Kapitel Mehrfachvererbung selbst schreiben oder einen Beitrag dazu leisten. |
Virtuelle Vererbung
|
Dieses Kapitel ist leider noch nicht vorhanden… |
|
|
Wenn Sie Lust haben können Sie das Kapitel Virtuelle Vererbung selbst schreiben oder einen Beitrag dazu leisten. |
Zusammenfassung
|
Zu diesem Abschnitt existiert leider noch keine Zusammenfassung… |
|
|
Wenn Sie Lust haben können Sie die Zusammenfassung zum Abschnitt OOP selbst schreiben oder einen Beitrag dazu leisten. |