C++-Programmierung/ Objektorientierte Programmierung/ Zusammenfassung


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