C++-Programmierung/ Objektorientierte Programmierung
Zielgruppe:
Anfänger
Lernziel:
Weitere wichtige Elemente der objektorientierte Programmierung (OOP) kennen lernen.
Vererbung [Bearbeiten]
Einleitung
BearbeitenWenn von Objektorientierung gesprochen wird, fällt früher oder später auch das Stichwort Vererbung. Auf dieser Seite lernen Sie anhand eines Beispiels die Grundprinzipien der Vererbung kennen.
Die Ausgangslage
BearbeitenStellen 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 string
s)
#include<iostream>
#include<string>
using namespace std;
class Person{
public:
Person(string Name, string Adresse, string Telefon) :m_name(Name), m_adr(Adresse), m_tel(Telefon){}
string getName(){ return m_name; }
string getAdr(){ return m_adr; }
string getTel(){ return m_tel; }
void info(){ cout << "Name: " << m_name << " Adresse: " << m_adr << " Telefon: " << m_tel << endl; }
private:
string m_name;
string m_adr;
string m_tel;
};
Dies ist natürlich eine sehr minimale Klasse, aber schließlich geht es hier ja auch um die Vererbung.
Gemeinsam und doch getrennt
BearbeitenDie 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.
Beispiel
Bearbeitenclass Mitarbeiter : public Person
{
public:
Mitarbeiter(string Name, string Adresse, string Telefon, int Gehalt, int MANr):Person(Name,Adresse,Telefon),
m_gehalt(Gehalt), m_nummer(MANr) {}
int getGehalt(){ return m_gehalt; }
int getNummer(){ return m_nummer; }
private:
int m_gehalt;
int m_nummer;
};
Erläuterung des Beispiels
BearbeitenNun wollen wir uns der Analyse des Beispiels zuwenden, um genau zu sehen, was passiert ist.
Das wichtigste im Code steht ganz am Anfang: class Mitarbeiter : public Person
. Damit weisen wir den Compiler an, alle Elemente aus Person
auch in Mitarbeiter
zu übernehmen (z.B. hat Mitarbeiter
jetzt auch eine info-Funktion und eine Membervariable m_name
). Eine solche Klasse nennen wir abgeleitet/Kindklasse von Person
.
Des Weiteren 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:
Mitarbeiter Mit("Erika Mustermann", "Heidestraße 17, Köln", "123/454", 4523, 12344209);
Mit.info();
cout << Mit.getGehalt() << endl;
cout << Mit.getName() << endl;
Warum wir das Schlüsselwort public
verwenden, wird im Kapitel „Private und geschützte Vererbung“ erklärt.
protected
Bearbeiten
Zum Schluss werden 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 Mitarbeiter
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. Mitarbeiter
. 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]
Im letzten Kapitel haben Sie die Vererbung kennengelernt. Auch ist Ihnen bereits bekannt, dass eine Funktion oder Methode überladen werden kann, indem man für einen Funktionsnamen mehrere unterschiedliche Deklarationen tätigt. Es stellt sich nun also die Frage, wie der Compiler reagiert, wenn wir in einer abgeleiteten Klasse eine Methode deklarieren, die in der Basisklasse unter dem gleichen Namen bereits existiert.
Dieser Vorgang wird als Überschreiben einer Methode bezeichnet. Man könnte nun natürlich annehmen, dass das Überschreiben nur für die entsprechende Signatur gilt, dies ist jedoch nicht der Fall. Eine Methode gleichen Namens, verdeckt alle Überladungen in der Basisklasse. Folgendes Beispiel soll das Verhalten klären:
#include <iostream>
struct Base{
void f(){ std::cout << "void Base::f()" << std::endl; }
void f(int){ std::cout << "void Base::f(int)" << std::endl; }
};
struct A: Base{};
struct B: Base{
void f(double){ std::cout << "void B::f(double)" << std::endl; }
};
int main(){
A a; // Nutzt f() aus Base, da keine eigene Methode f() existiert
B b; // Überschreibt alle Methoden f()
a.f(); // void Base::f();
a.f(5); // void Base::f(int);
// b.f(); // Compilierfehler: kein passendes f() in B; Base::f() ist verdeckt
b.f(5.4); // void B::f(double);
b.f(5); // void B::f(double); (implizite Konvertierung nach double)
// expliziter Aufruf der Basisklassenmethoden
b.Base::f(); // void Base::f();
b.Base::f(5.4); // void Base::f(int); (implizite Konvertierung nach int)
b.Base::f(5); // void Base::f(int);
}
Wie sie sehen, können die Methoden der Basisklasse durch explizite Angabe der selben aufgerufen werden. Alternativ wäre auch ein static_cast
von b
möglich, dies führt jedoch zu schlecht lesbarem und fehleranfälligen Code und sollte daher vermieden werden. Fehleranfällig ist er, weil ein static_cast
natürlich eine willkürliche Konvertierung bewirken kann, also in einen Typen von dem b
gar nicht abgeleitet ist, aber auch wenn das Konvertierungsziel eine Basisklasse ist, können sich unerwartete Effekte einstellen:
Schlechter Stil! Bitte nicht verwenden!
int main(){
B b; // Überschreibt alle Methoden f()
// expliziter Aufruf der Basisklassenmethoden
static_cast< Base& >(b).Base::f(); // Gleichwertig zu "b.Base::f()"
static_cast< Base >(b).Base::f(); // Erzeugt eine temporäre Kopie von a und ruft für diese Base::f() auf
}
Um zu sehen, dass tatsächlich eine Kopie erzeugt wird, können sie Base
einen entsprechenden Kopierkonstruktor hinzufügen. Natürlich benötigt Base
dann auch einen Standardkonstruktor.
Im Kapitel Virtuelle Methoden werden Sie eine scheinbar ähnliche Technik kennenlernen, diese hat jedoch im Gegensatz zum Überschreiben von Methoden nichts mit Funktionsüberladung zu tun und sollte keinesfalls damit verwechselt werden!
Private und geschützte Vererbung [Bearbeiten]
Einleitung
BearbeitenBis 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 wird.
Private Vererbung
BearbeitenVerwenden wir private
, so bedeutet dies, dass alle geschützten und öffentlichen Membervariablen bzw. Memberfunktion aus der Basisklasse privat werden, von außen also nicht sichtbar sind.
Wenn kein Schlüsselwort zur Ableitung angegeben wird, handelt es sich automatisch um private Vererbung.
Geschützte Vererbung
BearbeitenGeschützte Vererbung verläuft analog zur privaten Vererbung und sagt aus, dass alle geschützten und öffentlichen Member der Elternklasse im Bereich protected
stehen.
Wann wird was benutzt?
BearbeitenUm 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-Beziehung: "Klasse B ist eine Klasse A" → Klasse B erbt
public
von A. (Beispiel: Ein Arbeiter ist eine Person) - hat-Beziehung: "Klasse B hat ein Klasse A Objekt" → Klasse B erbt
private
von A. (Beispiel: Ein Mensch hat ein Herz)
Virtuelle Methoden [Bearbeiten]
Einleitung
BearbeitenIn diesem Abschnitt geht es um virtuelle Methoden. Dies sind Methoden, bei denen man erwartet, dass sie in abgeleiteten Klassen redefiniert werden. Der Hauptnutzen von virtuellen Methoden ist die korrekte Verwendung von gleichnamigen Mitgliedsmethoden in einer Vererbungshierarchie. So kann bewirkt werden, dass die Verwendung eines Basisklassenzeigers oder einer Basisklassenreferenz bei Aufruf die Methode am Ende der Hierarchie aufruft, ohne dass diese an der jeweiligen Stelle bekannt sein muss. So wird erreicht, dass das Objekt nicht in einen ungültigen Zustand gerät, wenn eine Instanz einer ggf. mehrfach abgeleiteten Klasse über ihre Basisklassenschnittstelle angesprochen wird.
Was bringen sie?
BearbeitenVirtuelle Methoden ermöglichen es dem Übersetzer, die passendste Methode in der Klassenhierarchie zu finden. Wird auf dieses reservierte Wort verzichtet, so wird im Zweifelsfall immer die Methode mit der gleichen Signatur des Urahnen genommen.
Virtuelle Methoden gibt es nicht in allen objektorientierten Sprachen. So entspricht sie in Java einer ganz normalen Methode, die nicht final
ist.
#include <iostream>
class Tier{
public:
virtual void iss() { std::cout << "Fresse wie ein Tier" << std::endl; };
// void iss() { std::cout << "Fresse wie ein Tier" << std::endl; };
};
class Hund : public Tier{
public:
void iss() { std::cout << "Wuff! Fresse gerade" << std::endl; };
};
class Mensch : public Tier{
public:
void iss() { std::cout << "Esse gerade" << std::endl; };
};
#include <vector>
int main(){
std::vector<Tier*> tiere;
tiere.push_back(new Tier());
tiere.push_back(new Hund());
tiere.push_back(new Mensch());
for (std::vector<Tier*>::const_iterator it = tiere.begin(); it != tiere.end(); it++){
(*it)->iss();
delete *it;
}
return 0;
}
Fresse wie ein Tier
Wuff! Fresse gerade
Esse gerade
Würden wir im obigen Beispiel das virtual
entfernen, so hätten wir das Ergebnis
Virtuelle Destruktoren
BearbeitenEine weitere Eigenheit von C++ sind Destruktoren, die für Tätigkeiten wie Speicherfreigabe verwendet werden. Jede Klasse, deren Attribute nicht primitive Typen sind oder die andere Ressourcen verwendet (wie z.B. eine Datenbankverbindung) sollten diese unbedingt in ihren Destruktoren freigeben. Um auf den richtigen Destruktor immer zugreifen zu können, muss der Destruktor des Urahnen mit dem Wort virtual
beginnen.
Folgendes Beispiel zeigt die Verwendung und Vererbung von nicht-virtuellen Destruktoren, was zu undefiniertem Verhalten führt.
#include <iostream>
class A{
public:
A() { }
~A() { std::cout << "Zerstöre A" << std::endl; }
};
class B : public A{
public:
B() { }
~B() { std::cout << "Zerstöre B" << std::endl; }
};
int main() {
A* b1 = new B;
B* b2 = new B;
delete b1; // Gemäß C++-Standard undefiniertes Verhalten.
// Meist wird nur ~A() aufgerufen, da ~A() nicht virtuell.
delete b2; // Destruktoren ~B() und ~A() werden aufgerufen
return 0;
}
Zerstöre A
Zerstöre B
Zerstöre A
Dynamisch Casten [Bearbeiten]
Einleitung
BearbeitenDas "dynamische Casten" oder die sog. "Typumwandlung zur Laufzeit" ist die Verwendung eines Objekts eines bestimmten Typs als ein Objekt eines anderen Typs. In C++ bezieht sich dies auf Objekte mit Klassen, die in einer Klassenhierarchie vorkommen. Eine dynamische Typumwandlung von Grundtypen in andere Grundtypen gibt es in C++ nicht. Dazu verwendet man gezielt andere Typumwandlungsoperatoren.
Virtuelle Freunde [Bearbeiten]
Virtuelle Freunde werden in C++ u.A. dazu verwendet, den Aufruf zu vereinfachen. Dadurch versucht man, den Code lesbarer zu machen. Hier ein Beispiel:
- Basis.h
#pragma once
#include <iostream>
class Basis {
public:
friend void f(Basis& b)
{ std::cout << "Basis::ruf_f_auf()..." << std::endl; };
protected:
virtual void ruf_f_auf();
};
inline void f(Basis& b) {
b.ruf_f_auf();
}
- Ableitung.h
#pragma once
#include "Basis.h"
class Ableitung : public Basis {
protected:
virtual void ruf_f_auf()
{ std::cout << "Ableitung::ruf_f_auf()..." << std::endl; }; // "Überschreibt" Methode f(Basis& b)
};
- main.cpp
Wie man sieht, wurde die Methode der Basisklasse mit der ruf_f_auf()
-Methode der Klasse Ableitung
überschrieben.
Der Nachteil von befreundeten Methoden ist nicht nur, dass die Datenkapselung umgangen wird, sondern auch, dass für die dynamische Bindung eine zusätzliche Zeile hinzugefügt werden muss. Um den Effekt eines virtuellen Freunds zu bekommen, sollte die virtuelle Methode versteckt sein (in der Regel protected
). Dies wird auch im Englischen virtual friend function idiom genannt.[1]
Bitte beachten Sie im Beispiel oben, dass die Klasse Ableitung
das Verhalten von der Methode protected virtual ruf_f_auf()
überschriebt, ohne seine eigene Variante für die befreundete Methode f(Base &)
zu haben.
Referenzen
BearbeitenAbstrakte Klassen [Bearbeiten]
Einleitung
BearbeitenAbstrakte Klassen sind Klassen in denen mindestens eine Methode als absichtlich nicht erfüllt deklariert wurde. Diese Methodeneigenschaft wird auch als "rein virtuell" bezeichnet. Die Erfüllung nicht-erfüllter Methoden wird den von einer abstrakten Klasse abgeleiteten Klassen überlassen.
Die wichtigsten Eigenschaften abstrakter Klassen sind:
- können aus "reinen" Deklarationen bestehen
- können nicht direkt instanziiert werden (wenngleich auch Objekte dieser Klasse existieren können)
- erfordern für die Verwendung eine nicht-abstrakte abgeleitete Klasse
- werden oft als Schnittstellenbeschreibungen (z.B. in umfangreichen Anwendungen) verwendet
- treten häufig als Startpunkte von Vererbungshierarchien (z.B. in Klassensystemen) auf
Deklarieren
BearbeitenEine Klasse wird dadurch abstrakt, indem man eine ihrer Mitgliedsmethoden als rein virtuell deklariert. Dazu schreiben Sie =0 hinter die Deklaration einer virtuellen Methode.
class StatusAusgeber
{
public:
virtual void printStatus(void) = 0; // Rein virtuelle Deklaration
virtual ~StatusAusgeber() {}
};
Der Versuch, eine Instanz von dieser Klasse zu erstellen, schlägt fehl:
int main (void)
{
StatusAusgeber instanz; //Instanz von abstrakter Klasse kann nicht erstellt werden
return 0;
};
Verwenden
BearbeitenNun 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 die Funktionalität einer Klasse sicherstellen und verwenden, ohne auf die Funktionalitäten abgeleiteter Klassen eingehen zu müssen.
Wir deklarieren die Klassen Drucker und Bildschirm.
class Drucker : public StatusAusgeber
{
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
class GeraeteMitStatusSpeicher : std::vector<StatusAusgeber*>
{
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 [Bearbeiten]
In diesem Abschnitt geht es um mehrfache Vererbung und damit Ableitung von mehreren Basisklassen.
Eine Klasse kann von mehreren Basisklassen erben:
class A
{
int x;
//...
};
class B
{
double y;
//...
};
class C : public A, public B
{
char z;
//...
};
Die Klasse C vereint die Funktionalitäten von A und B und fügt noch etwas hinzu.
Die Idee davon ist, dass verschiedene Funktionalitäten in einer Klasse vereint werden. Im folgenden Beispielprogramm wird dieses Vorgehen dargestellt.
#include <string>
#include <iostream>
class Person
{
std::string m_strName;
public:
std::string Name() const { return m_strName; }
void Name(std::string val) { m_strName = val; }
void Name(const char * val) { m_strName.assign(val); }
};
class Gehaltsempfaenger
{
unsigned int m_nMonatsgehalt;
public:
unsigned int Monatsgehalt() const { return m_nMonatsgehalt; }
void Monatsgehalt(unsigned int val) { m_nMonatsgehalt = val; }
};
class Freizeitbeschaeftigung
{
std::string m_strAktion;
public:
std::string Aktion() const { return m_strAktion; }
void Aktion(std::string val) { m_strAktion = val; }
void Aktion(const char * val) { m_strAktion.assign(val); }
};
class Mitarbeiter :
public Person,
public Gehaltsempfaenger,
public Freizeitbeschaeftigung
{
public:
Mitarbeiter(const char * szName, const unsigned int nMonatsgehalt, const char * szFreizeittaetigkeit)
{ Name(szName); Monatsgehalt(nMonatsgehalt); Aktion(szFreizeittaetigkeit);}
void auszahlenGehalt(void)
{
std::cout << Name() << " bekommt das Monatsgehalt von "
<< Monatsgehalt() << " Euro ausgezahlt" << std::endl;
}
void entspannenFreizeit(void)
{
std::cout << Name() << " " << Aktion() << std::endl;
}
};
int main(int argc, char* argv[])
{
Mitarbeiter ma1("Christian", 1000, "spielt Skat");
ma1.auszahlenGehalt();
ma1.entspannenFreizeit();
return 0;
}
Christian bekommt das Monatsgehalt von 1000 Euro ausgezahlt
Christian spielt Skat
Der Vorteil dieser Vorgehensweise ist eine bessere Wiederverwend-barkeit/-ung und eine Kapselung der Schnittstellen.
Schon anhand dieses Beispiels kann eine Person, die eine Freizeitbeschäftigung hat, aber kein Gehalt bekommt, als neue Klasse angelegt werden.
Die Klasse Kunde
hat damit die Eigenschaften von Person
und Freizeitbeschaeftigung
erhalten, aber nicht von Gehaltsempfaenger
.
Wichtig ist bei allen Mehrfachvererbungen das Einhalten einer sinnvollen Hierarchie.
Was würde passieren, wenn wir von unseren Beispielklassen Kunde
und Mitarbeiter
ableiten? Lesen Sie daher weiter. Im nächsten Abschnitt wird genauer darauf eingegangen.
Ein weiterer wichtiger Punkt ist die Namensgebung in allen parallel verwendeten Klassen. Würde die Freizeitaktion sowie der Personenname jeweils unter m_strName
öffentlich gespeichert, müsste in den abgeleiteten Klassen mit dem Bereichsauflösungsoperator ::
auf die passenden Mitgliedsvariablen zugegriffen werden.
class Hemd
{
public:
std::string m_strFarbe;
};
class Hose
{
public:
std::string m_strFarbe;
};
class Bekleidung : public Hemd, public Hose
{
void ausgeben(void)
{
std::cout << "Hose in " << Hose::m_strFarbe << " Hemd in " << Hemd::m_strFarbe;
}
};
Die Verwendung der Mehrfachvererbung ist nicht in jedem Fall einfach zu realisieren, kann schnell unübersichtlich werden und erfordert bei größeren Projekten eine gute Disziplin. Weiterhin sollte sie nicht in übertriebenem Maße verwendet werden. In einigen Fällen ist eine Mitgliedsvariable an Stelle einer Ableitung die bessere Wahl.
Virtuelle Vererbung [Bearbeiten]
Eine abgeleitete Klasse kann wiederum als Basisklasse einer Vererbungsbeziehung dienen. Auf diese Weise fungiert eine allgemeine Klasse als Ausgangspunkt für einen ganzen „Vererbungsbaum“. Eine interessante Situation tritt ein, wenn die Baumgestalt verloren geht: Dank der Mehrfachvererbung kann es passieren, dass zwei Klassen durch mehrere Vererbungswege verbunden sind. C++ überlässt dem Programmierer die Entscheidung, ob die zur mehrfach vorhandenen Basisklasse gehörenden Teilobjekte zu einem einzigen verschmolzen werden sollen oder nicht.
Wenn Sie getrennte Teilobjekte haben wollen, müssen Sie nichts weiter tun. Eine fiktive Klasse zum Arbeiten mit Dateien:
class Datei {
unsigned int position;
/* ... */
};
class DateiZumLesen : public Datei { /* ... */ };
class DateiZumSchreiben : public Datei { /* ... */ };
class DateiZumLesenUndSchreiben: public DateiZumLesen, public DateiZumSchreiben { /* ... */ };
Jede Instanz der Klasse DateiZumLesenUndSchreiben
hat zwei Teilobjekte der Basisklasse Datei
. Das ist hier ein sinnvoller Ansatz, damit Lese- und Schreibzeiger an verschiedenen Positionen stehen können.
Sollen die Teilobjekte verschmolzen werden, kennzeichnen Sie die Vererbung mit dem Schlüsselwort virtual
:
// Modifiziertes Beispiel "Person"
class Person {
string name;
// ...
};
class Mitarbeiter : public virtual Person { /* ... */ };
class Kunde : public virtual Person { /* ... */ };
class MitarbeiterUndKunde : public Mitarbeiter, public Kunde { /* ... */ };
Jetzt besitzt eine Instanz der Klasse MitarbeiterUndKunde
nur ein Teilobjekt der Basisklasse Person
. Insbesondere ist die Membervariable name
nur einmal vorhanden und kann konfliktfrei unter diesem Namen angesprochen werden. Beim Anlegen einer Instanz vom Typ MitarbeiterUndKunde
wird jetzt allerdings der Konstruktor der Klasse Person
nicht mehr indirekt durch die Konstruktoren der Klassen Mitarbeiter
und Person
aufgerufen, sondern muss explizit aus dem Konstruktor der Klasse MitarbeiterUndKunde
aufgerufen werden.
Zusammenfassung [Bearbeiten]
Objektorientierte Programmierung
BearbeitenC++ unterstützt - anders als beispielsweise C - direkt die objektorientierte Programmierung. Die Vorteile dieses Programmierparadigmas liegen klar auf der Hand:
- Man braucht sich nicht mehr um die Implementierungsdetails zu kümmern und hat eine klar definierte Schnittstelle zu den Objekten (Datenkapselung).
- Ein Mal geschriebener Code kann erneut genutzt werden (Wiederverwendung).
- Klassen können die Implementierung (Methoden und Variablen) von einer anderen Klasse übernehmen, quasi erben (Vererbung). Altes Verhalten kann umgeändert und neues hinzugefügt werden, die grundlegende Schnittstellen bleiben jedoch gleich (Polymorphie).
- Dadurch sind weniger Änderungen durchzuführen. Die zentrale Implementierung muss nur an einer einzigen Stelle umgeschrieben werden.
Vererbung
BearbeitenIn C++ wird das Ableiten einer neuen Klasse (Sub- oder Unterklasse) von einer bereits bestehenden (Super-, Ober- oder Basis-)Klasse mit folgender Notation erreicht:
class SuperClass;
class «SubClass» : »Zugriffsoperator« «SuperClass» {
// Implementierung
};
Im Normalfall wird für Zugriffsoperator
public
verwendet. Damit stehen in SubClass
(fast) alle Member von SuperClass
zur Verfügung. Die abgeleitete Klasse lässt sich dann von "überall" wie ein Objekt der Basisklasse verwenden - die öffentliche Schnittstellen der Basisklasse sind öffentlich sichtbar.
Wird hingegen kein Zugriffsoperator oder explizit private
verwendet, ist die Vererbung privat und alle Member der Basisklasse sind in der abgeleiteten Klasse privat und können somit nicht von außen zugegriffen werden. Analog gilt dies für die protected
-Vererbung - die öffentlichen und geschützten (protected
) Member der Basisklasse stehen im protected
-Bereich der Unterklasse.
Die beiden letzten Vererbungsmechanismen sind sehr selten bis gar nicht anzutreffen und können dementsprechend vernachlässigt werden.
C++ erlaubt (anders als in anderen Programmiersprachen wie Java) Mehrfachvererbung, d. h. eine Klasse erbt von mehreren Basisklassen. Diese werden einfach bei der Klassendefinition durch Kommata getrennt aufgeführt. Ein Nachteil ist, dass Klassenhierarchien dadurch ziemlich unübersichtlich werden können.
Zugriffskontrolle
BearbeitenMit der Datenkapselung sind die Variablen gegen Zugriff von außen geschützt. Manchmal jedoch ist es sinnvoll, die Zugriffsregelung etwas aufzulockern, z. B. bei der Erstellung von Klassenhierarchien. So soll beispielsweise eine abgeleitete Klasse direkt auf die Variablen und/oder Methoden der Basisklasse zugreifen dürfen. Gelöst wird dieses Problem, indem die Deklaration der betreffenden Klassenmember in den protected
Bereich verschoben wird (siehe 1)). Soll hingegen die abgeleitete Klasse nur über spezielle Zugriffsmethoden auf die Variablen zugreifen dürfen, so muss die Deklaration im private
-Bereich stehen. Völlig uneingeschränkter Zugriff lässt sich realisieren, indem die Variable oder Methode im public
-Bereich verschoben wird. Dies ist allerdings meist nur für Methoden gedacht, da öffentliche Variablen eigentlich gegen das Konzept der Datenkapselung verstoßen und damit das Konzept der Objektorientierung aufweichen.
#include <iostream>
using namespace std;
class Super{
public:
Super(int value) : i(value){
cout << "Super class ctor" << endl;
}
protected:
int i;
};
class Sub : public Super{
public:
Sub(int value) : Super(value), otherInt(value*2){
cout << "Sub class ctor" << endl;
}
private:
int otherInt;
};
int main(){
Super s(42);
Sub sub(42);
}
Methoden (nicht) überschreiben
BearbeitenDefiniert eine abgeleitete Klasse eine Methode mit einem Namen, der in der Basisklasse mehrfach überladen wurde, werden alle Überladungen verdeckt. Explizit aufgerufen können die ursprünglichen Methoden über die Angabe der Basisklasse:
#include <iostream>
using namespace std;
struct Base{
void f(){ cout << "void Base::f()" << endl; }
void f(int){ cout << "void Base::f(int)" << endl; }
};
struct A: Base{};
struct B: Base{
void f(double){ cout << "void B::f(double)" << endl; }
};
int main(){
// nutzt f() aus Base, da keine eigene Methode f() existiert
A a;
// überschreibt alle Methoden f()
B b;
// void Base::f();
a.f();
// void Base::f(int);
a.f(5);
// b.f(); // Kompilierfehler: kein passendes f() in B; Base::f() ist verdeckt
// void Base::f(double);
b.f(5.4);
// void Base::f(double); (implizite Konvertierung nach double)
b.f(5);
// expliziter Aufruf der Basisklassenmethoden
// void Base::f();
b.Base::f();
// void Base::f(int); (implizite Konvertierung nach int)
b.Base::f(5.4);
// void Base::f(int);
b.Base::f(5);
}
In C++11 wurde das Schlüsselwort final
eingeführt, welches das Überschreiben von Methoden und das Ableiten von Klassen verhindert. So endet der Versuch, eine mit final
gekennzeichnete Klasse abzuleiten oder eine finale Methode zu überschreiben mit einem Kompilierfehler. Voraussetzung ist ein C++0x-/C++11-konformer Compiler, ggf. muss noch ein Compilerflag gesetzt werden, unter g++
oder clang++
zum Beispiel -std=c++0x oder -std=c++11.
Virtuelle Methoden
BearbeitenVirtuelle Methoden erlauben es dem Compiler, zur Laufzeit die jeweils "passende" Methode zu seinem Objekt aufzurufen. Damit wird bei Klassenhierarchien erreicht, dass Objekte aus Unterklassen nicht in einen ungültigen Zustand geraten. Die speziellen Methoden werden mit dem Schlüsselwort virtual
deklariert, z. B.:
#include <iostream>
using namespace std;
class Base{
public:
virtual void foo(){ cout << "Base::foo()" << endl; }
virtual void bar() =0; // rein virtuelle Methode
};
class Derived : public Base{
public:
void foo(){ cout << "Derived::foo()" << endl; }
void bar(){ cout << "Derived::bar()" << endl; }
};
Eine weitere Besonderheit sind rein virtuelle Methoden. Die Notation =0 besagt, dass in konkreten, abgeleiteten Klassen, von denen Objekte gebildet werden können, diese gekennzeichneten Methoden zwingend definiert werden müssen. In diesem Beispiel hat Base
eine rein virtuelle Methode namens bar()
, wodurch sie nur als abstrakte Basisklasse verwendet werden kann. Derived
muss diese Methode definieren, damit Objekte von ihr erstellt werden können. Andernfalls dient sie ebenfalls als abstrakte Basisklasse.
Des Weiteren muss unbedingt darauf aufgepasst werden, exakt dieselbe Methodensignatur zu verwenden, da ansonsten eine neue virtuelle Methode definiert wird.
Klassen, die Ressourcen verwalten und für Klassenhierarchien vorgesehen sind, müssen auf jeden Fall einen virtuellen Destruktor haben, damit Basisklassen, die möglicherweise auch Ressourcen angefordert haben, diese wieder frei geben können.
In C++11 kommt ein neues Schlüsselwort hinzu, welches hilft, solche "Fehler" schon beim Kompilieren zu finden: override
. Es wird in der Methodendeklaration hinter der Parameterliste geschrieben. So würde in Derived
folgender Fehler beim Übersetzen bemerkt: