Zum Inhalt springen

C++-Programmierung/ Objektorientierte Programmierung

Aus Wikibooks
Objektorientierte Programmierung

Zielgruppe:

Anfänger

Lernziel:
Weitere wichtige Elemente der objektorientierte Programmierung (OOP) kennen lernen.


Vererbung [Bearbeiten]

Einleitung

[Bearbeiten]

Wenn 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

[Bearbeiten]

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<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

[Bearbeiten]

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.

Beispiel

[Bearbeiten]
class 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

[Bearbeiten]

Nun 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;
Thema wird später näher erläutert…

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.

Hinweis

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

[Bearbeiten]

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 wird.

Private Vererbung

[Bearbeiten]

Verwenden 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

[Bearbeiten]

Geschü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?

[Bearbeiten]

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.

  1. ist-Beziehung: "Klasse B ist eine Klasse A" → Klasse B erbt public von A. (Beispiel: Ein Arbeiter ist eine Person)
  2. hat-Beziehung: "Klasse B hat ein Klasse A Objekt" → Klasse B erbt private von A. (Beispiel: Ein Mensch hat ein Herz)
Hinweis

Meistens wird mithilfe von public vererbt. Andere Typen von Vererbung werden nur selten bis gar nicht benutzt. Oft ist es sinnvoll statt einer privaten Vererbung ein Memberobjekt der entsprechenden Klasse zu verwenden.

Virtuelle Methoden [Bearbeiten]

Einleitung

[Bearbeiten]

In 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?

[Bearbeiten]

Virtuelle 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.

Tipp

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;
}
Ausgabe:
Fresse wie ein Tier
Wuff! Fresse gerade
Esse gerade

Würden wir im obigen Beispiel das virtual entfernen, so hätten wir das Ergebnis

#include <iostream>

class Tier{
public:
    void iss() { std::cout << "Fresse wie ein Tier" << std::endl; };
};

[...]
Ausgabe:
Fresse wie ein Tier
Fresse wie ein Tier
Fresse wie ein Tier

Virtuelle Destruktoren

[Bearbeiten]

Eine 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;
}
Ausgabe:
Zerstöre A
Zerstöre B
Zerstöre A

Dynamisch Casten [Bearbeiten]

Einleitung

[Bearbeiten]

Das "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
#include "Ableitung.h"

void userCode(Basis& b) {
	f(b);
}

int main() {
	Ableitung b;

	std::cout << "Rufe userCode auf..." << std::endl;
	userCode(b);
}
Ausgabe:
Rufe userCode auf...
Ableitung::ruf_f_auf()...

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

[Bearbeiten]
  1. 14.3: What are some (dis)advantages of using friend functions?

Abstrakte Klassen [Bearbeiten]

Einleitung

[Bearbeiten]

Abstrakte 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

[Bearbeiten]

Eine 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

[Bearbeiten]

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 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.

Hinweis

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;
}
Ausgabe:
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.

class Kunde: public Person, public Freizeitbeschaeftigung
{
	/* ... */ 
};

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]

Nicht virtuelle Vererbung

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 { /* ... */ };
Virtuelle Vererbung

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

[Bearbeiten]

C++ 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

[Bearbeiten]

In C++ wird das Ableiten einer neuen Klasse (Sub- oder Unterklasse) von einer bereits bestehenden (Super-, Ober- oder Basis-)Klasse mit folgender Notation erreicht:

Syntax:
class SuperClass;

class «SubClass» : »Zugriffsoperator« «SuperClass» {
// Implementierung
};
«Nicht-C++-Code», »optional«

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

[Bearbeiten]

Mit 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

[Bearbeiten]

Definiert 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);
  }
Hinweis

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

[Bearbeiten]

Virtuelle 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.

Hinweis

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.

Hinweis

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:

class Derived : public Base{
// ...
        void foo(int i) override { cout << "Derived::foo(), i = " << i << endl; }
};

Virtuelle Vererbung

[Bearbeiten]