Programmieren in C++: Aufbau


Kapitel 1: Klassen und Objekte


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


1.1 Allgemeines

Konzepte und praktische Umsetzung

In der objektorientierten Programmierung wird mit Begriffen wie Klassen, Objekten, Instanzen, Vererbung, Kapselung und so weiter nur so um sich geschmissen. In diesem Kapitel erhalten Sie einen Überblick über die einzelnen Bestandteile der Objektorientierung, wie diese zusammenhängen und vor allem welchen Sinn sie haben. Diese Übersicht gilt hierbei nicht nur für die Programmiersprache C++, sondern grundsätzlich auch für andere objektorientierte Sprachen.

Nach dem Rundflug über die Objektorientierung werden Sie in die notwendige Syntax und Semantik eingewiesen, um einfache Klassen in C++ selbst zu erstellen.


1.2 Überblick

Objektorientierte Programmierung

Um die objektorientierte Programmierung herrscht - vor allem bei Entwicklern, die nicht objektorientiert programmieren - ein Riesenhype. Die Objektorientierung scheint das Non-Plus-Ultra, der heilige Gral unter den Programmiersprachen zu sein. Objektorientiert-entwickelte Programme scheinen grundsätzlich besser als Anwendungen zu sein, die in einer nicht-objektorientierten Programmiersprache entwickelt sind. Woher kommt dieser Hype um die Objektorientierung?

Bei der Objektorientierung stehen, wie der Name schon sagt, Objekte im Vordergrund. Ein Programm wird nicht mehr als eine Art Wasserfall betrachtet, in dem Informationen von oben nach unten fließen. Dieses Flußkonzept von Daten spielt in der Objektorientierung keine Rolle.

Wird eine Aufgabenstellung objektorientiert gelöst, werden zuerst Objekte, die zur Lösung der Aufgabe miteinander agieren müssen, herausgearbeitet. Um objektorientiert programmieren zu können muss erstmal klar sein, was für Objekte eigentlich genau benötigt werden. Dies hängt natürlich von der Aufgabe ab. Soll beispielsweise eine Software für einen Bankautomaten zum Geldabheben objektorientiert entwickelt werden, könnten sich als notwendige Objekte zur Lösung der Aufgabe Kunde und Bank erweisen. Das Abheben von Geld ist schließlich ein Vorgang, bei dem ein Kunde Daten in einen Bankautomaten eingibt und elektronisch mit einer Bank kommuniziert.

Eine objektorientierte Programmiersprache macht es nun möglich, den Kunden und die Bank hinüber in die Software zu nehmen. Das heißt, in einer objektorientierten Programmiersprache werden die Objekte, die herausgearbeitet wurden, direkt in den Code übernommen und dort nachgebildet. Dieses Nachbilden sieht derart aus, dass für die Lösung des Problems benötigte Eigenschaften und Fähigkeiten der Objekte in den Code übernommen werden.

Zum Beispiel spielt für die Entwicklung der Bankautomatensoftware die Haarfarbe des Kunden keine Rolle. Ob er jedoch die PIN für sein Bankkonto kennt, ist wichtig. Daher könnte dem Objekt Kunde eine Eigenschaft PIN gegeben werden. Eine Eigenschaft Haarfarbe wäre unsinnig.

Um die PIN vom Kunden zu erfahren, muss er sie dem Bankautomaten mitteilen. Der Kunde muss die Fähigkeit besitzen, eine PIN eingeben zu können. Diese Fähigkeit würde also dem entsprechenden Objekt Kunde in der Objektorientierung gegeben werden. Dass der Kunde außerdem beispielsweise tanzen kann, interessiert in diesem Zusammenhang nicht. Eine derartige Fähigkeit wird zum Geldabheben am Bankautomaten nicht benötigt.

Als objektorientierter Entwickler müssen Sie also für die Aufgabenstellung relevante Objekte identifizieren und beschreiben. Die Beschreibung von Objekten besteht aus relevanten Eigenschaften und Fähigkeiten. Wenn Sie wissen, was Sie brauchen, müssen Sie die Beschreibung der Objekte in einer objektorientierten Programmiersprache wie zum Beispiel C++ ausdrücken. Die praktische Umsetzung in C++ wird Ihnen im Folgenden noch gezeigt werden.

Der grosse Vorteil der Objektorientierung ist, dass Sie als Entwickler fast keine Abstraktionsleistung mehr erbringen müssen. Der Weg von der gedanklichen Problemlösung zur Software ist enorm kurz. Während der Assembler-Programmierer sich auf die Eingabe kryptischer Zeichen konzentrieren muss und der Basic-Programmierer sich den Informationsfluss im Programm vergegenwärtigen muss, verwendet der C++-Programmierer einfach im Code die gleichen Objekte wie in seinem Kopf. Die objektorientierte Programmierung geht also einen großen Schritt auf den Menschen, den Programmierer zu, und erwartet nicht, dass der Programmierer die Problemlösung dem Computer in einer Form eingibt, die sich zum grossen Teil an der Hardware-Architektur orientiert. In der Objektorientierung wird es erstmals möglich, dass der Entwickler dem Computer sagt, dass er diese und jene Objekte verwenden möchte. Ist dies geschehen, arbeitet er nur noch mit den Objekten und kümmert sich nicht weiter um die Hardware-Abhängigkeit.

Damit der Computer objektorientierte Programme ausführen kann, muss er wissen, wie die verwendeten Objekte im Programm genau aussehen, welche Eigenschaften und Fähigkeiten sie besitzen. Sie müssen also vor der Verwendung eines Objekts dieses beschreiben. Eine Objektbeschreibung wird in der Objektorientierung als Klasse bezeichnet. Eine Klasse ist nichts anderes als die Beschreibung eines Objekts; also ein Plan, der festlegt, welche Eigenschaften und Fähigkeiten das Objekt besitzen soll.

Während eine Klasse lediglich eine Objektbeschreibung ist, ist ein Objekt sozusagen eine lebendig gewordene Klasse. Ein Synonym für Objekt ist Instanz. Objekte und Instanzen basieren auf einer Klasse, die sie beschreiben. Objekte und Instanzen besitzen die Eigenschaften und Fähigkeiten, die in der Klasse, auf der sie basieren, festgelegt worden sind.

Sie können sich den Unterschied zwischen Klassen und Objekten gut anhand folgendem Beispiel vorstellen. Um ein Haus zu bauen müssen Sie erstmal einen Architekten beauftragen, einen Grundriss zu erstellen. In diesen Plan werden die Räume, der Ort von Fenstern und Türen, Steckdosen und so weiter eingezeichnet. Dieser Plan beschreibt nach seiner Fertigstellung komplett das Haus.

Nur weil Sie jedoch jetzt einen Plan vom Haus haben, ist dieses noch lange nicht erbaut. Einziehen können Sie noch nicht. Das Haus muss erst errichtet werden. Ist dies geschehen und haben die Bauarbeiter ordentlich gearbeitet, entspricht das Haus genau der Zeichnung des Architekten.

In der Objektorientierung ist der Plan des Architekten die Klasse, das errichtete Haus das Objekt bzw. die Instanz. So wie Sie anhand des Grundrisses weitere Häuser bauen können, die alle identisch aussehen, können Sie basierend auf einer Klasse mehrere Objekte erstellen. Es handelt sich wie bei den Häusern um unabhängige und getrennte Objekte, die jedoch alle gleich aussehen und gleiche Eigenschaften und Fähigkeiten besitzen.

Sie können sich den Zusammenhang zwischen Klassen und Objekten auch wie folgt vorstellen: Eine Klasse ist eine Art oder Gattung, und ein Objekt ist ein konkretes Exemplar der Art. Bienen besitzen einen schwarz-gelb gestreiften Körper und die Fähigkeit zu fliegen. Gestochen werden Sie jedoch immer von einer ganz konkreten Biene. Der BMW 316ti hat eine Nennleistung von 85 kW und beschleunigt von 0 auf 100 km/h in 10,9 Sekunden. Der 316ti BMW, der in Ihrer Garage steht, ist ein Objekt dieser Klasse.

Als objektorientierter Programmierer müssen Sie immer wie folgt vorgehen: Zuerst erstellen Sie die Klasse, dann basierend auf der Klasse das oder die benötigten Objekte. Ohne Klasse sprich ohne Objektbeschreibung können Sie logischerweise kein Objekt erstellen.

Das Tolle an der Objektorientierung ist also, dass Sie Objekte aus der Wirklichkeit in Ihr Programm mithinübernehmen können. Anstatt sich mit der Hardware herumzuschlagen und das Spiel nach den Regeln des Computers zu spielen arbeiten Sie einfach innerhalb der Software mit den gleichen Objekten, die Ihnen in der Problemlösung im Kopf rumschwirren.

Die Objektorientierung ist ein Meilenstein verglichen mit der prozeduralen Programmierung. Sie haben es vielleicht schon vermutet: So einfach wie sich alles anhört ist es jedoch in der Praxis nicht. Auch wenn die Objektorientierung viele Probleme löst, so schafft sie auch neue. Als objektorientierter Programmierer stehen Sie vor verschiedenen Problemen: Welche Objekte brauche ich tatsächlich zur Lösung einer Aufgabenstellung? Und welche Eigenschaften und Methoden brauche ich tatsächlich in einem Objekt, um es sinnvoll anwenden zu können?

Wenn Sie sich nochmal die Software für den Bankautomaten vergegenwärtigen: Reichen als Objekte Kunde und Bank wirklich aus? Oder sollten nicht zusätzliche Objekte wie Benutzerschnittstelle, Geldbetrag, Kontostand, Geldkarte und so weiter verwendet werden? Damit könnten die Objekte Kunde und Bank etwas verkleinert werden, indem Informationen in anderen Objekten gespeichert werden. Während die Objekte übersichtlicher werden, wächst jedoch die Anzahl der Objekte im Programm insgesamt, was irgendwann auch unübersichtlich wird.

Während der Kunde keine Eigenschaft Haarfarbe benötigt, hatten wir ihm eine Eigenschaft PIN spendiert. Welche Eigenschaften sind noch wichtig? Kontostand, Limit zum Überziehen des Kontos, Gültigkeitsdatum der Karte?

Das Herausarbeiten der notwendigen Objekte und Festlegen von Eigenschaften und Fähigkeiten kann zum Teil enorm schwierig sein. Es können auch mehrere Lösungen gefunden werden, bei denen am Anfang kaum abzusehen ist, welche die beste ist. Das Herausarbeiten der richtigen Objekte, Eigenschaften und Fähigkeiten und die Konstruktion der Zusammenhänge, um letztendlich zu einem sinnvollen Programm zu gelangen, hat eine eigene Softwareindustrie geschaffen. Programme, die Entwicklern helfen, sinnvolle Objektmodelle zu erstellen und zu pflegen, sind vor allem in größeren Projektvorhaben immens wichtig und können vierstellige Beträge kosten.

Die Objektorientierung hat neben dem ganz entscheidenden Vorteil der Abbildung von Objekten noch andere Stärken.

  • Perfekte Klassen kapseln Daten. Andere Klassen können also nie direkt auf eigene Daten zugreifen. Die Interaktion zwischen Objekten, die auf Klassen basieren, geschieht nicht über Datenzugriffe, sondern über den Zugriff auf Fähigkeiten. Der Kunde in unserem Beispiel ruft die Fähigkeit Kontostand verringern der Klasse Bank auf anstatt selber den Kontostand neu zu berechnen. Das ist allein Aufgabe der Klasse Bank, und keine andere Klasse sollte in dessen Daten reinpfuschen. Die objektorientierte Programmiersprache Smalltalk zwingt den Entwickler zum Erstellen perfekter Objekte, in denen alle Daten gekapselt sind, weil direkte Datenzugriffe nicht unterstützt werden. In C++ entscheidet der Programmierer mit Hilfe sogenannter Zugriffsattribute selbst, ob er das Konzept der Datenkapselung verfolgt oder nicht.

  • Klassen vererben Eigenschaften und Fähigkeiten an andere Klassen. Durch Vererbung können Klassenhierarchien erstellt werden, in denen jede Klasse von einer anderen Klasse erbt und an der Spitze der Klassenhierarchie eine oder mehrere Elternklassen stehen. Vererbung hat den Vorteil, dass Klassen austauschbar werden, weil sie eine gemeinsame Schnittstelle besitzen, wenn sie von der gleichen Elternklasse abstammen. C++ unterstützt im Gegensatz zu Java die Mehrfachvererbung: Eine Klasse kann mehr als eine Elternklasse haben und somit mehrere Schnittstellen erben. Im Kapitel 3, Vererbung lernen Sie die Vererbung genauer kennen.

  • Die Programmiersprache C++ ist keine 100%ig objektorientierte Programmiersprache. Sie können auch C++-Programme erstellen, in denen keine einzige Klasse und kein einziges Objekt vorkommt. Sie können mehr oder weniger beliebig die Regeln der Objektorientierung außer Kraft setzen. Wenn Ihnen etwas nicht passt, wie es in der Objektorientierung vorgesehen ist, dann müssen Sie es in C++ höchstwahrscheinlich auch nicht so machen. Die Programmiersprache Smalltalk ist hier viel rigoroser: Entweder Sie halten sich an die Regeln oder Sie wechseln die Programmiersprache.

Die extrem hohe Flexibilität von C++ ist wohl der wichtigste Grund, warum diese Programmiersprache den größten Marktanteil und die größte Beliebtheit unter den objektorientierten Sprachen besitzt. In C++ können Sie, müssen aber nicht alle Sprachmerkmale verwenden, die für eine objektorientierte Entwicklung zur Verfügung stehen. Das führt bei C++-Anfängern leicht zur Verwirrung: Alles scheint irgendwie optional zu sein und auch anders zu gehen. Hier hilft nur Üben, Üben, Üben. Je mehr Programme Sie in C++ erstellen, umso schneller erkennen Sie Vor- und Nachteile der verschiedenen Ansätze. Erfahrung ist, wie bei allen Programmiersprachen, auch in C++ das Wichtigste.


1.3 Klassen entwickeln

Schlüsselwörter und ihre Bedeutung

Wie Sie bereits wissen, muss, bevor ein Objekt erstellt werden kann, erst eine Klasse entwickelt werden. Ob Sie die Klasse selber programmieren oder ob sie von jemand anderem entwickelt wurde und Sie sie beispielsweise aus dem offiziellen C++-Standard übernehmen spielt keine Rolle - Hauptsache, sie ist da.

In C++ werden Klassendefinitionen normalerweise in Header-Dateien gepackt. Diese Dateien erhalten der Übersicht wegen normalerweise den gleichen Namen wie die Klasse. So können Sie beispielsweise davon ausgehen, dass in einer Header-Datei kunde.h eine Klasse kunde liegt und in bank.h eine Klasse bank. Wann immer Sie nun Objekte basierend auf der Klasse kunde oder der Klasse bank erstellen wollen, fügen Sie einfach über den Präprozessor-Befehl #include die Header-Datei in die Quellcode-Datei ein - und schon können Sie die Klasse verwenden, um darauf basierend ein Objekt zu erstellen.

Wie sieht jedoch eine Klassendefinition aus? Sie wissen, dass eine Klasse Eigenschaften und Fähigkeiten festlegt. Wie definiert man das jedoch in C++?

#include <string> 

class kunde 
{ 
  public: 
    void PIN_eingeben(); 

  private: 
    std::string PIN; 
}; 

Was Sie oben sehen ist eine Klassendefinition. Klassendefinitionen beginnen entweder mit dem Schlüsselwort class oder struct. Den genauen Unterschied zwischen diesen beiden Schlüsselwörtern werden Sie später kennenlernen. Hinter dem Schlüsselwort wird der Name der Klasse angegeben. Obige Klasse heißt also kunde. Hinter dem Namen der Klasse folgen zwei geschweifte Klammern, zwischen denen die Definition der Klasse steht. Vergessen Sie nicht, hinter die geschlossene geschweifte Klammer ein Semikolon zu setzen - sonst meckert der Compiler.

Obige Klasse besitzt eine Eigenschaft PIN und eine Fähigkeit, die PIN_eingeben() heißt. In der objektorientierten Terminologie spricht man bei PIN auch tatsächlich von einer Eigenschaft. Fähigkeiten werden jedoch Methoden genannt.

In technischer Hinsicht ist eine Eigenschaft nichts anderes als eine Variable. Es spielt auch keine Rolle, ob die Variable einen intrinsischen Datentyp besitzt oder nicht. Eine Variable, die innerhalb der geschweiften Klammern einer Klasse definiert ist, wird Eigenschaft genannt.

Methoden sind technisch nichts anderes als Funktionen. Funktionen, die innerhalb einer Klasse definiert sind, heißen Methoden. So können auch objektorientierte Programmierer ganz klar sagen, was sie meinen: Eine freistehende Funktion oder eine Funktion, die in einer Klasse definiert ist.

Wenn Sie sich obige Klassendefinition ansehen, stellen Sie fest, dass die Methodendefinition nicht vollständig ist. Es ist schließlich nur der Methodenkopf angegeben. Was die Methode bei einem Aufruf eigentlich machen soll, steht nirgendwo. Die Klasse ist daher noch nicht ausreichend definiert und kann so wie oben auch nicht eingesetzt werden.

Die Implementation von Methoden wird normalerweise von der Klassendefinition getrennt. Wenn eine Klasse kunde heißt, wird sie normalerweise in einer Datei kunde.h definiert. Die Methodendefinitionen befinden sich dann normalerweise in einer Datei kunde.cpp. So können Klasse und Dateien schnell zugeordnet werden.

#include "kunde.h" 

void kunde::PIN_eingeben() 
{ 
} 

Der Inhalt der Datei kunde.cpp, um die Klasse kunde zu vervollständigen, könnte wie oben aussehen. Die Klasse definiert eine einzige Methode PIN_eingeben(). Der Methodenkopf muss angegeben werden gefolgt von zwei geschweiften Klammern - also genauso wie wenn Sie eine freistehende Funktion definieren würden. Der einzige Unterschied: Nachdem es sich um eine Methode handelt, die ja immer einer Klasse zugeordnet ist, müssen Sie den Klassennamen gefolgt vom Zugriffsoperator :: vor den Methodennamen stellen. Wenn Sie das nicht machen, würde es sich bei PIN_eingeben() um eine freistehende Funktion handeln, die nicht der Klasse kunde zugeordnet ist. Damit wäre die Klasse kunde immer noch nicht vollständig.

Anstatt die Implementation einer Methode von der Klasse, in der die Methode deklariert ist, zu trennen, können Sie auch folgendes schreiben.

#include <string> 

class kunde 
{ 
  public: 
    void PIN_eingeben() 
    { 
    } 

  private: 
    std::string PIN; 
}; 

Nun wird die Methode direkt innerhalb der Klasse kunde definiert und nicht mehr nur wie vorher deklariert.

Es gibt einen minimalen Unterschied zwischen Methoden, die in einer Klasse nur deklariert und in einer Klasse vollständig definiert sind. Methoden, deren Implementation innerhalb einer Klasse liegt, heißen Inline-Methoden. Bei einem Aufruf einer Inline-Methode wird nicht der Code angesprungen, sondern der Compiler kopiert den Code der Methode jeweils an die Stellen der ausführbaren Datei, an denen die Methode verwendet wird. Inline-Methoden tauschen Speicherplatz gegen Geschwindigkeit ein: Es sind keine Sprünge nötig, um Code auszuführen. Dafür liegt der gleiche Code mehrfach in der ausführbaren Datei vor, so dass sich der Speicherbedarf der Anwendung erhöht.

Sie können Inline-Methoden auch dann erstellen, wenn sich die Implementation einer Methode außerhalb der Klasse befindet. Sie müssen dann der Methodendeklaration in der Klasse das Schlüsselwort inline voranstellen.

#include <string> 

class kunde 
{ 
  public: 
    inline void PIN_eingeben(); 

  private: 
    std::string PIN; 
}; 

Die Methode PIN_eingeben() ist in der Klasse zwar nur deklariert und nicht definiert. Dennoch wird der Compiler den Code der Methode an all die Stellen in der ausführbaren Datei kopieren, an denen die Methode aufgerufen wird - jedenfalls in der Theorie. In der Praxis verstehen Compiler Inline-Methoden eher als Empfehlung und nicht als verbindliche Vorgabe. Der Grund ist, dass Compiler normalerweise besser beurteilen können, wie Code optimiert werden soll - unabhängig davon, ob Inline-Methoden verwendet werden oder nicht.

In der Praxis werden für gewöhnlich Klassendefinition und Methodendefinitionen immer in getrennte Dateien gelegt. Selbst für Inline-Methoden wird eher das Schlüsselwort inline verwendet als dass die Methode innerhalb der Klasse komplett definiert wird. Der Vorteil dieser Aufteilung ist, dass Sie anhand einer Klasse die Eigenschaften und Methoden ablesen können, ohne dass Sie sich mit der Implementation der Methoden rumschlagen müssen. Die Implementation interessiert Sie normalerweise nicht, wenn Sie Methoden für ein Objekt aufrufen möchten. Die Implementation ist ja Aufgabe der Klasse. Sie müssen nur wissen, welche Eigenschaften und Methoden die Klasse zur Verfügung stellt, um mit ihr arbeiten zu können. Eine Klassendefinition ist also in diesem Fall eine Art Dokumentation für Programmierer, die Aufschluss darüber gibt, welche Schnittstelle zur Verfügung steht.


1.4 Objekte anlegen

Klassen zum Leben erwecken

Wenn Sie eine Klasse definiert haben, haben Sie den ersten Schritt getan. Der zweite Schritt besteht nun darin, auf die Klasse zuzugreifen und ein Objekt basierend auf der Klasse zu erstellen.

Objekte anlegen ist extrem einfach. Es handelt sich hierbei schlichtweg um Variablen, deren Datentyp eine Klasse ist. Das ist die Definition eines Objekts. Um also ein Objekt zu erstellen, gehen Sie so wie immer vor, wenn Sie eine Variable anlegen. Sie geben zuerst den Datentyp an und dann den Namen der Variablen, in diesem Fall eben den Namen des Objekts.

#include "kunde.h" 

kunde k; 

Damit Sie ein Objekt vom Datentyp kunde erstellen können, müssen Sie den Datentyp selbstverständlich erst bekanntmachen. Wenn Sie die Klasse in der Header-Datei kunde.h definiert haben, fügen Sie diese einfach über den Präprozessorbefehl #include in Ihre aktuelle Quellcode-Datei ein. Im obigen Beispiel wird also eine Variable k definiert, die genaugenommen ein Objekt darstellt, und zwar vom Datentyp der Klasse kunde.

In der Klasse kunde haben Sie angegeben gehabt, dass es eine Methode PIN_eingeben() gibt. Ihr Objekt besitzt also die Fähigkeit, eine PIN eingeben zu können. Wie greifen Sie innerhalb von C++ auf diese Fähigkeit Ihres Objekts k zu?

Auf Eigenschaften und Methoden von Objekten wird über den Zugriffsoperator . zugegriffen. Um also die Methode PIN_eingeben() für Ihr Objekt k aufzurufen, geben Sie folgende Zeile an.

k.PIN_eingeben();

Diese Zeile ist letztendlich nichts anderes als der Aufruf einer Funktion - nur eben nicht einer freistehenden Funktion, sondern einer, die innerhalb des Datentyps definiert ist, auf dem das Objekt basiert, für das Sie die Funktion aufrufen. Das Objekt k basiert auf der Klasse kunde, und diese Klasse besitzt eine Methode namens PIN_eingeben(), deren Parameterliste leer ist. Obiger Methodenaufruf ist dementsprechend völlig korrekt und würde vom Compiler ohne Meckern übersetzt werden.

Sie hatten ein paar Abschnitte zuvor einen anderen Zugriffsoperator kennengelernt, nämlich ::, um bei Methodendefinitionen angegeben zu können, zu welcher Klasse eine Methode überhaupt gehört. Damit Sie nicht durcheinanderkommen: :: ist der Zugriffsoperator für Klassen, . der Zugriffsoperator für Objekte.


1.5 Zugriffsrechte

Zugriff erlaubt oder nicht

Sie wissen, dass Sie über den Zugriffsoperator . auf Eigenschaften und Methoden eines Objekts zugreifen können. Trotzdem würde der Compiler folgende Code-Zeile nicht übersetzen.

k.PIN = "Abc";

Obwohl die Klasse kunde eine Eigenschaft PIN besitzt, können Sie nicht auf diese Eigenschaft zugreifen. Die Eigenschaft PIN ist nämlich privat.

Die Programmiersprache C++ unterstützt zwei Zugriffsattribute: public und private. Auf Eigenschaften und Methoden, die hinter public angegeben sind, darf von außerhalb der Klasse zugegriffen werden. Auf Eigenschaften und Methoden, die hinter private angegeben sind, darf nicht von außerhalb der Klasse zugegriffen werden. Private Eigenschaften und Methoden sind also für die interne Verwendung in der Klasse gedacht. C++ unterstützt noch ein drittes Zugriffsattribut, was jedoch nur im Zusammenhang mit Vererbung von Bedeutung ist und daher erst in einem späteren Kapitel vorgestellt wird.

Erinnern Sie sich noch daran, dass für eine Klassendefinition in C++ zwei Schlüsselwörter zur Verfügung stehen? Das eine Schlüsselwort, das in den allermeisten Fällen verwendet wird, haben Sie bereits kennengelernt - es ist class. Anstatt eine Klasse mit class zu definieren, können Sie auch das Schlüsselwort struct verwenden. Der einzige Unterschied zwischen class und struct: In einer mit class definierten Klasse sind Eigenschaften und Methoden standardmäßig privat, in einer mit struct definierten Klasse sind Eigenschaften und Methoden standardmäßig öffentlich. Nachdem Sie mit den Zugriffsattributen private und public jederzeit die Art des Zugriffs in einer Klasse festlegen können, können Sie Klassen auf unterschiedliche Weise definieren.

class beispiel 
{ 
    int Eigenschaft1; 
    int Eigenschaft2; 

  public: 
    void Methode1(); 
    void Methode2(); 
}; 

Obige Klasse mit dem Namen beispiel definiert zwei private Eigenschaften und zwei öffentliche Methoden. Wenn Sie die Klasse anstatt mit class mit struct definieren möchten, schreiben Sie sie wie folgt.

struct beispiel 
{ 
    void Methode1(); 
    void Methode2(); 

  private: 
    int Eigenschaft1; 
    int Eigenschaft2; 
}; 

Beide Klassendefinitionen sind gleich. Der Compiler generiert jeweils den gleichen Code. Auf welche Weise Sie Ihre Klassen definieren, spielt demnach keine Rolle. Es hat sich eingebürgert, normalerweise class zu verwenden. Sie sollten struct nur dann einsetzen, wenn Sie dokumentieren möchten, dass Ihre Klasse hauptsächlich aus öffentlichen Eigenschaften und Methoden besteht und daher anderen Klassen ein sehr weiträumiger Zugriff eingeräumt wird.

Sie können die Zugriffsattribute public und private auch mehrfach in einer Klassendefinition verwenden. Sie sollten jedoch davon absehen und Eigenschaften und Methoden, die dem gleichen Zugriffsmechanismus unterliegen, in einem Block zusammenstellen, da dies die Übersicht erhöht.


1.6 Praxis-Beispiel

So geht's

In diesem Abschnitt werden die immer mal wieder als Beispiel in diesem Kapitel angesprochenen Klassen kunde und bank verkomplettiert und in einer kleinen Anwendung verwendet. Diese Anwendung simuliert einen Bankautomaten, an dem Sie Geld abheben können.

Der Kunde muss nicht mehr nur die Fähigkeit besitzen, eine PIN eingeben zu können, sondern er muss ebenfalls seine Geldkarte in den Automaten schieben und einen Betrag auswählen können. Die Klasse kunde wird daher erweitert, so dass sie wie folgt aussieht.

#include <string> 

class kunde 
{ 
  public: 
    void geldkarte_einschieben(); 
    void PIN_eingeben(); 
    void betrag_waehlen(); 

  private: 
    std::string PIN; 
}; 

Die Implementierung der Methoden wird wie üblich in einer anderen Datei vorgenommen. Die Methode PIN_eingeben() bekommt nun außerdem einen sinnvolleren Methodenrumpf verpasst, nachdem sie ja bisher keine Anweisungen enthalten hat und daher bei einem Aufruf nichts geschehen wäre.

Die Methode geldkarte_einschieben() kann einen derartigen Vorgang natürlich nur simulieren. Sie haben höchstwahrscheinlich kein Kartenlesegerät an Ihren Computer angeschlossen. Die Programmierung von Kartenlesegeräten ist außerdem extrem kompliziert, nachdem Sie hier in völligem Gegensatz zur Objektorientierung sehr hardwarenah programmieren müssen. Unsere Methode geldkarte_einschieben() simuliert den Vorgang dadurch, dass Sie einfach einen Benutzernamen eingeben müssen.

#include "kunde.h" 
#include <iostream> 
#include <string> 

void kunde::geldkarte_einschieben() 
{ 
  std::string Benutzername; 
  std::cout << "Schieben Sie bitte Ihre Karte in den Automaten: " << std::flush; 
  std::cin >> Benutzername; 
} 

void kunde::PIN_eingeben() 
{ 
  std::cout << "Geben Sie Ihre PIN ein: " << std::flush; 
  std::cin >> PIN; 
} 

void kunde::betrag_waehlen() 
{ 
  int Betrag; 
  std::cout << "Geben Sie ein, wieviel Geld Sie abheben moechten: " << std::flush; 
  std::cin >> Betrag; 
} 

In den drei Methoden wird jeweils auf die Standardein- und -ausgabe zugegriffen, so dass die Header-Datei iostream eingebunden werden muss. Die Methode geldkarte_einschieben() legt ein Objekt vom Typ std::string an. Daher muss außerdem die Header-Datei string mit #include eingebunden werden.

Alle drei Methoden geben einen Text auf die Standardausgabe aus und erwarten eine Eingabe vom Anwender. Sehen Sie sich die Methode PIN_eingeben() genau an. Es werden Daten von der Standardeingabe eingelesen und in einer Variablen PIN gespeichert, die gar nicht in der Methode definiert ist.

Der Compiler meckert trotzdem nicht. PIN ist eine Eigenschaft der Klasse kunde und dort definiert. Jede Methode einer Klasse kann jederzeit auf Eigenschaften in einer Klasse zugreifen. In diesem Fall wird also die Eingabe des Anwenders in einer Eigenschaft gespeichert und nicht in einer lokalen Variable.

Übrigens: Die Eigenschaft PIN ist eine private Eigenschaft. Zugriffsattribute wie private und public schränken den Zugriff auf Eigenschaften und Methoden nur gegenüber anderen Klassen ein. Eine Methode in einer Klasse kann grundsätzlich auf alle Eigenschaften und Methoden zugreifen, völlig egal, mit welchen Zugriffsattributen sie geschützt sind. Zugriffsattribute schränken nur den Zugriff von außen ein.

Der Vorteil von Eigenschaften ist, dass sie einen größeren Gültigkeitsbereich haben als lokale Variablen. Die Eigenschaft PIN existiert genauso lange wie das Objekt, das auf der Klasse kunde basiert. Wenn der Anwender eine PIN eingegeben hat und die Methode PIN_eingeben() beendet wurde, ist die Eingabe des Anwenders immer noch in der Eigenschaft PIN gespeichert. Dies ist bei den Methoden geldkarte_einschieben() und betrag_waehlen() nicht der Fall: Dort werden Eingaben des Anwenders in lokalen Variablen gespeichert, die nach Ende der Methode aus dem Speicher gelöscht werden. Ihr Gültigkeitsbereich erstreckt sich lediglich über die Methoden und nicht weiter. Die Methoden geldkarte_einschieben() und betrag_waehlen() müssen später nachbearbeitet werden, wenn feststeht, wie die Klasse kunde und deren Methoden mit der Klasse bank zusammenarbeiten.

Im nächsten Schritt wird daher die Klasse bank definiert. Die Klasse bank muss Daten zum Konto verwalten, damit zum einen überprüft werden kann, ob der Zugang zum Konto durch den Kunden am Bankautomaten in Ordnung geht, und zum anderen verhindert werden kann, dass der Kunde unendlich viel Geld abhebt. Die Klasse bank wird daher in einem ersten Schritt wie folgt definiert.

#include <string> 

class bank 
{ 
  private: 
    std::string Benutzername; 
    std::string PIN; 
    int Kontostand; 
}; 

Welche Fähigkeiten müssen der Bank spendiert werden? Sie muss überprüfen, ob der vom Kunden eingegebene Benutzername und die PIN zusammenpassen und der Zugang gewährleistet wird. Sie muss außerdem bei einer Bargeldabhebung den Kontostand verringern und gegebenenfalls die Auszahlung verhindern, falls das Konto überzogen wird. Die Klasse bank wird daher wie folgt erweitert.

#include <string> 

class bank 
{ 
  public: 
    bool zugriff_ueberpruefen(std::string benutzername, std::string pin); 
    bool geld_abheben(int betrag); 

  private: 
    std::string Benutzername; 
    std::string PIN; 
    int Kontostand; 
}; 

Die Klasse bank erhält zwei Methoden zugriff_ueberpruefen() und geld_abheben(). Beide Methoden geben einen Wahrheitswert zurück, der angibt, ob der Zugriff in Ordnung geht bzw. ob das Geld abgehoben werden konnte. Um den Zugriff überprüfen zu können, werden der Methode zugriff_ueberpruefen() beim Aufruf zwei Werte übergeben - nämlich der Benutzername und die PIN, die der Anwender eingegeben hat. Der Methode geld_abheben() wird ein Parameter vom Typ int übergeben, der den Betrag angibt, den der Kunde abheben möchte.

Was noch fehlt ist eine Möglichkeit, die Eigenschaften der Klasse bank zu initialisieren. Deswegen wird der Klasse bank eine Methode init() spendiert, die ganz am Anfang des Programms aufgerufen werden muss und festlegt, mit welchen Werten die Eigenschaften Benutzername, PIN und Kontostand vorbelegt werden.

#include <string> 

class bank 
{ 
  public: 
    void init(std::string benutzername, std::string pin, int kontostand); 
    bool zugriff_ueberpruefen(std::string benutzername, std::string pin); 
    bool geld_abheben(int betrag); 

  private: 
    std::string Benutzername; 
    std::string PIN; 
    int Kontostand; 
}; 

Nun fehlt noch die Implementierung der Methoden.

#include "bank.h" 

void bank::init(std::string benutzername, std::string pin, int kontostand) 
{ 
  Benutzername = benutzername; 
  PIN = pin; 
  Kontostand = kontostand; 
} 

bool bank::zugriff_ueberpruefen(std::string benutzername, std::string pin) 
{ 
  if (Benutzername == benutzername && PIN == pin) 
  { 
    return true; 
  } 

  return false; 
} 

bool bank::geld_abheben(int betrag) 
{ 
  if (Kontostand >= betrag) 
  { 
    Kontostand -= betrag; 
    return true; 
  } 

  return false; 
} 

Die Methode init() setzt lediglich die Eigenschaften auf die Werte, die als Parameter übergeben werden. Die Methode zugriff_ueberpruefen() vergleicht den als Parameter übergebenen Benutzernamen und die PIN mit den Eigenschaften des Objekts und gibt true oder false zurück. In der Methode geld_abheben() wird zuerst überprüft, ob der Betrag, den der Kunde abheben möchte, nicht den aktuellen Kontostand übersteigt. Ist dies nicht der Fall, wird der Kontostand neuberechnet und true zurückgegeben - andernfalls false.

Die Klasse bank ist somit fertig. Wir müssen uns nun wieder der Klasse kunde zuwenden und sie vervollständigen bzw. den Zugriff auf die Bank hinzufügen. Die Klasse kunde muss hierzu ein wenig umgebaut werden. Die Eingabe des Benutzernamens, der ja die Geldkarte simuliert, muss in einer Eigenschaft in der Klasse gespeichert werden. Andernfalls kann in der Methode PIN_eingeben() die Bank gar nicht den Zugriff überprüfen, wenn der eingegebene Benutzername nirgendwo dauerhaft gespeichert wird.

Die Methde PIN_eingeben() soll außerdem zurückgeben, ob die Bank den Zugriff gestattet oder nicht. Ist der Zugriff nämlich nicht gestattet, muss der Anwender seine Karte entnehmen und sie neu einlegen. Im Programm muss also der Benutzername neu eingegeben werden. Es darf nicht sein, dass der Kunde eine beliebige PIN eingibt, die Bank den Zugriff nicht gestattet, der Kunde dann trotzdem Geld abheben kann. Die Methode betrag_waehlen() soll ebenfalls zurückgeben, ob die Auszahlung erfolgreich war oder nicht.

Die Definition der Klasse kunde ändert sich minimal. Lediglich Benutzername wird als neue Eigenschaft hinzugefügt, und der Rückgabewert der Methoden PIN_eingeben() und betrag_waehlen() wird angepasst.

#include <string> 

class kunde 
{ 
  public: 
    void geldkarte_einschieben(); 
    bool PIN_eingeben(); 
    bool betrag_waehlen(); 

  private: 
    std::string Benutzername; 
    std::string PIN; 
}; 

In die Implementierung der Methoden der Klasse kunde wird tiefergehend eingegriffen.

#include "kunde.h" 
#include "bank.h" 
#include <iostream> 

bank b; 

void kunde::geldkarte_einschieben() 
{ 
  std::cout << "Schieben Sie bitte Ihre Karte in den Automaten: " << std::flush; 
  std::cin >> Benutzername; 
} 

bool kunde::PIN_eingeben() 
{ 
  std::cout << "Geben Sie Ihre PIN ein: " << std::flush; 
  std::cin >> PIN; 
  return b.zugriff_ueberpruefen(Benutzername, PIN); 
} 

bool kunde::betrag_waehlen() 
{ 
  int Betrag; 
  std::cout << "Geben Sie ein, wieviel Geld Sie abheben moechten: " << std::flush; 
  std::cin >> Betrag; 
  return b.geld_abheben(Betrag); 
} 

Beachten Sie zuallererst, dass in dieser Datei ein globales Objekt b vom Typ bank angelegt wird. Zu diesem Zweck wird außerdem die Header-Datei bank.h eingebunden, um die Klasse bekanntzumachen.

Die Methode geldkarte_einschieben() speichert den Benutzernamen nun nicht mehr in einer lokalen Variablen, sondern in einer Eigenschaft. In der Methode PIN_eingeben() wird dieser Benutzername zusammen mit der PIN an die Methode zugriff_ueberpruefen() des Objekts b weitergegeben. Der Rückgabewert dieser Methode ist gleichzeitig der Rückgabewert der Methode PIN_eingeben().

In der Methode betrag_waehlen() wird ebenfalls auf das Objekt b zugegriffen. Der Methode geld_abheben() wird der vom Anwender eingegebene Geldbetrag übergeben, der abgehoben werden soll. Der Rückgabewert dieser Methode ist wieder gleichzeitig der Rückgabewert der Methode betrag_waehlen().

Das einzige, was noch fehlt, ist die Funktion main(), um das Programm starten zu können. Die Funktion sieht wie folgt aus.

#include "kunde.h" 
#include "bank.h" 
#include <iostream> 

extern bank b; 

int main() 
{ 
  kunde k; 

  b.init("boris", "1234", 1000); 

  for (;;) 
  { 
    k.geldkarte_einschieben(); 

    if (k.PIN_eingeben() == true) 
    { 
      if (k.betrag_waehlen() == true) 
      { 
        std::cout << "Der gewuenschte Betrag wurde abgehoben." << std::endl; 
      } 
    } 
    else 
    { 
      std::cout << "Die PIN ist falsch." << std::endl; 
    } 
  } 
} 

In der Funktion main() wird ein Objekt vom Typ kunde angelegt. Danach wird das Objekt b initialisiert, indem die Methode init() aufgerufen wird. Beachten Sie, wie auf das Objekt zugegriffen wird. Es wird vor der Funktion main() mit dem Schlüsselwort extern bekanntgemacht. Dies bedeutet, dass innerhalb dieser Datei auf ein Objekt b zugegriffen werden kann, das in einer anderen Datei angelegt wird. Sehen Sie sich kunde.cpp an: In der Datei, in der die Methoden der Klasse kunde definiert sind, wird ein Objekt b erstellt. Auf dieses Objekt wird nun in der Funktion main() zugegriffen, um es zu initialisieren. Würden Sie das Schlüsselwort extern weglassen, würden Sie ein neues Objekt namens b anlegen. Der Compiler würde daraufhin den Code nicht übersetzen, da es zwei gleichnamige Objekte im gleichen Gültigkeitsbereich geben würde. Sie müssen daher extern angeben.

Nachdem das Objekt b initialisiert wurde, wird eine Endlosschleife mit for gestartet. Wenn Sie das C++-Programm starten, können Sie es ausschließlich mit der Tastenkombination Strg+C abbrechen. Nachdem ein Bankautomat für gewöhnlich auch nicht beendet oder heruntergefahren wird, ist die Endlosschleife in diesem Fall in Ordnung.

In der Endlosschleife wird auf das Objekt k zugegriffen. Es werden nacheinander die drei Methoden aufgerufen, die für dieses Objekt definiert sind. Für die Methoden PIN_eingeben() und betrag_waehlen() wird jedoch der Rückgabewert überprüft. Denn daran erkennt das Programm, ob der Zugriff auf das Bankkonto gestattet ist und der eingegebene Betrag abgehoben werden konnte. Wird eine falsche PIN eingegeben, wird der Kunde erst gar nicht aufgefordert, einen Betrag zu wählen. Stimmt die PIN und der Kunde versucht, mehr Geld abzuheben als auf dem Konto liegt, erfolgt die Abbuchung nicht. Der Bankautomat zeigt in diesem Fall auch keine entsprechende Bestätigung des Abbuchungsvorgangs an.

Das Programm ist nun fertig. Sie können es starten und ausprobieren. Geben Sie als Benutzernamen boris und als PIN 1234 ein, um Zugriff auf das Bankkonto zu erhalten. Von dort können Sie ingesamt 1000 Euro abheben. Dies sind die Werte, mit denen die Bank in init() initialisiert wurde.


1.7 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erweitern Sie das Praxis-Beispiel dahingehend, dass der Kunde nach Eingabe der richtigen PIN zwischen den Währungen Euro und Dollar wählen kann. Gehen Sie davon aus, dass die Bank Kontostände grundsätzlich in Euro verwaltet. Der Einfachheit halber können Sie mit einem Umrechnungskurs 2 zu 1 von Euro zu Dollar rechnen.

    Damit der Kunde sich für eine Währung entscheiden kann, müssen Sie ihm eine entsprechende Fähigkeit geben. Das heißt, Sie müssen eine neue Methode in der Klasse kunde definieren, die Sie nach Eingabe der richtigen PIN aufrufen. Die Währung, für die sich der Kunde entschieden hat, benötigen Sie nochmal in der Methode kunde::betrag_waehlen(). Denn in dieser Methode übergeben Sie ja einen Euro-Betrag an die Bank - die Bank verrechnet schließlich alles in Euro. Sie müssen daher die Währung als Eigenschaft in der Klasse kunde speichern, damit sie auch in der Methode kunde::betrag_waehlen() auf diese Zugriff haben. Als Datentyp für die neue Eigenschaft bietet sich beispielsweise eine Enumeration an.

  2. Erweitern Sie das Praxis-Beispiel aus Aufgabe 1 insofern, als dass der Kunde nach Eingabe der richtigen PIN zwischen einer Bargeldabhebung und Kontostandanzeige wählen soll. Egal, für welche Option er sich entscheidet, er muss sich danach am Bankautomaten neu anmelden, um eine andere Option wählen zu können. Zeigen Sie den aktuellen Kontostand jeweils in der vorher ausgewählten Währung an.

    Die Auswahl für eine Option ist eine neue Fähigkeit, die der Kunde besitzen muss. Sie benötigen wieder eine neue Methode, die Sie für die Klasse kunde definieren müssen. Hat sich der Kunde für die Anzeige des Kontostands entschieden, müssen Sie wiederum auf eine neu zu definierende Methode der Klasse kunde zugreifen, die den Kontostand am Bildschirm darstellt. Hierbei werden Sie feststellen, dass die Klasse kunde den aktuellen Kontostand gar nicht kennt - den kennt nur die Bank. Sie müssen daher der Klasse bank ebenfalls eine neue Methode spendieren, die den aktuellen Kontostand an die Klasse kunde zurückgeben kann.

  3. Aus Sicherheitsgründen darf die Kommunikation zwischen Kunde und Bank nicht mehr unverschlüsselt stattfinden. Im Detail dürfen Benutzername und PIN nicht mehr ohne Sicherheitsvorkehrungen von der Klasse kunde an die Klasse bank weitergegeben werden.

    Erstellen Sie eine neue Klasse sicherheit, die die Methoden verschluesseln() und entschluesseln() implementiert. Die Methode verschluesseln() besitzt keinen Rückgabewert und erwartet als einzigen Parameter eine Variable vom Typ std::string. Die Methode entschluesseln() besitzt einen Rückgabewert vom Typ std::string und erwartet keinen Parameter.

    Die Methode zugriff_ueberpruefen() der Klasse bank darf ab sofort nur noch Parameter vom Typ sicherheit akzeptieren. Bevor die Klasse kunde diese Methode der Klasse bank aufruft, muss sie Objekte vom Typ sicherheit erstellen, die die an die Bank zu übermittelnden Informationen mit verschluesseln() in den Objekten speichern, und die Objekte dann als Parameter an die Methode weitergeben. Die Methode zugriff_ueberpruefen() der Klasse bank liest die Informationen in den als Parameter übergebenen Objekten, indem sie die Methode entschluesseln() aufruft und die Informationen entschlüsselt.

    Um die Aufgabe nicht unnötig zu verkomplizieren, ist es nicht notwendig, in der Klasse sicherheit tatsächlich Verschlüsselungsverfahren zu implementieren.

    Gehen Sie Schritt für Schritt vor: Beginnen Sie damit, die Klasse sicherheit zu erstellen. Ändern Sie dann die Methode bank::zugriff_ueberpruefen(). Erweitern Sie abschließend die Methode kunde::passwort_eingeben() derart, dass die korrekten Parameter an die Methode bank::zugriff_ueberpruefen() übergeben werden.