C++-Programmierung/ OOP

Aus Wikibooks

Wechseln zu: Navigation, Suche
OOP

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)

Crystal Clear app terminal.png
#include<iostream>
#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

Crystal Clear app terminal.png
class Employee : public Person {
   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:

Crystal Clear app terminal.png
Employee Mit("Erika Mustermann", "Heidestraße 17, Köln", "123/454", 4523, 12344209);
Mit.info();
cout << Mit.getSalary() << endl;
cout << Mit.getName() << endl;
Symbol move vote.svg
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.

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

Crystal Clear app tutorials.png
Syntax:
«Klassenname»::«Methodenname»(«Parameter...»);
Symbol opinion vote.svg
Hinweis
Obwohl es möglich ist jede Funktion einer Basisklasse zu überschreiben, ist dies nicht empfehlenswert. Besser ist die Benutzung von virtuelle Methoden die genau zu diesem Zweck vorhanden sind.

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.

  1. "ist ein" Kann man sagen "Klasse B ist eine Klasse A" so erbt Klasse B public von A. (Beispiel: Ein Arbeiter ist eine Person)
  2. "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)
Symbol opinion vote.svg
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

Baustelle.svg

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

Baustelle.svg

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

Baustelle.svg

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.

Crystal Clear app terminal.png
class StatusAusgeber
{
public:
        virtual void printStatus(void) = 0;     // Rein virtuelle Deklaration
};

Der Versuch, eine Instanz von dieser Klasse zu erstellen, schlägt fehl:

Crystal Clear action button cancel.png
int main (void)
{
        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.

Crystal Clear app terminal.png
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

Crystal Clear app terminal.png
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.

Symbol opinion vote.svg
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

Baustelle.svg

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

Baustelle.svg

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

Baustelle.svg

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.


Persönliche Werkzeuge