Am Anfang steht wohl die Frage was Objektorientierte Programmierung überhaupt ist. Dies ist nicht ganz einfach zu erklären und es gibt viele Definitionen die dies versuchen.
Grob gesagt ist Objektorientierte Programmierung (OOP) ein Verfahren zur Strukturierung von Programmen, bei dem Programmlogik zusammen mit Zustandsinformationen (Datenstrukturen) in Einheiten, den Objekten, zusammengefasst werden.
Es ist also möglich mehrere Objekte des gleichen Typs zu haben. Jedes dieser Objekte hat dann seinen individuellen Zustand. Dies bietet die Möglichkeit einer besseren Modularisierung der Programme, sowie einer höheren Wartbarkeit des Quellcodes.
Die Klasse (class) ist die zentrale Datenstruktur in C + +. Sie kapselt zusammengehörige Daten und Funktionen vom Rest des Programmes ab. Sie ist das Herz der objektorientierten Programmierung (OOP).
Klassen sind ein erweitertes Konzept von Datenstrukturen denen es erlaubt ist, außer Daten, noch Methoden zu beinhalten. Ein Objekt ist eine Instanz einer Klasse, welches sich die Eigenschaften der Klasse zunutze machen kann. Es ist möglich mehrere Objekte einer Klasse zu instanziieren, wobei jedes dieser Objekte zwar die selben Eigenschaften hat, intern aber einen ganz individuellen Zustand haben kann.
Beispiel: Zwei Instanzen der Klasse „Gegner“ werden erzeugt. Nachdem ein Gegner Schaden erlitten hat sind seine Lebenspunkte auf 50 gesunken. Die des anderen Gegners haben allerdings noch den Wert 100.
Klassen werden normalerweise mit dem Schlüsselwort class deklariert. Außerdem haben sie immer folgendes Format:
class Klassenname { Zugriffsspezifizierung 1: Member 1; ... Zugriffsspezifizierung 2: Member 2; ... ... };
Der Klassenname identifiziert die Klasse, der Objektname ist eine optionale Liste von Namen von Objekten dieser Klasse. Der Körper der Klasse wird durch geschweifte Klammern gekennzeichnet und kann verschiedene Member (Methoden, Membervariablen) beinhalten. Diesen können verschiedene Zugriffsspezifierungen zugewiesen werden.
public: Auf Members kann von überall zugegriffen werden von wo das Objekt sichtbar ist
private: Auf Member kann nur innerhalb anderer Member zugegriffen werden oder aus befreundeten Klassen und Funktionen
protected: Members sind verfügbar für Member der selben Klasse, befreundeten Funktionen/Klassen und Abgeleiteten Klassen
Die Festlegung, welche Daten zu einem Typ gehören, erfolgt bei der Definition der Klasse. Die Objekte enthalten jeweils unabhängig voneinander einen Satz von Datenkomponenten (Attributen). Ihre Startwerte können durch Konstruktoren festgelegt werden. C + + erlaubt auch die Zuweisung von Anfangswerten bei der Definition:
class Enemy { public: //Zugriffsrecht public void setHealth(int h); int getHealth(); string name; int id; private: //Zugriffsrecht private int health; };
Im Beispiel davor wird ein Objekt vom Typ Enemy angelegt. Das Objekt enthält die 3 Variablen health (integer), id (int) und name (String), wie es in der Klassendefinition zu sehen ist. Um auf eine öffentliche Elementvariable zugreifen zu können, wird an den Objektnamen ein Punkt und dann der in der Klassendefinition verwendete Elementname gehängt. Im Beispiel wird der Name auf Andreas gesetzt.
...
//Zugriff auf öffentliche Variable name
e1.name="Andreas";
//Zugriff auf öffentliche Variable id
e1.id=1;
...
//e1.health=100; ist nicht erlaubt, da health das Schlüsselwort private besitzt!!!
Nun können Sie ein Objekt von der Klasse Enemy erzeugen. Aber was nützt Ihnen die schönste Datenstruktur in Ihrem Programm, wenn sie nicht durch Funktionen zum Leben erweckt wird? Sie werden z.B. einen Namen und Lebenspunkte eingeben und ausgeben wollen. Vielleicht wollen Sie die Lebenspunkte verringern oder erhöhen. Kurz gesagt, ein Datenverbund ist nichts wert ohne Funktionen, die auf ihn wirken. Aber die Funktionen sind auch nur im Zusammenhang mit ihrem Datenverbund sinnvoll. Aus diesem Grund werden die Funktionen ebenso in die Klasse integriert wie die Datenelemente. Eine Funktion, die zu einer Klasse gehört, nennt man Elementfunktion oder auf englisch member function. In anderen objektorientierten Programmiersprachen spricht man auch von einer Methode oder Operation. Aufruf So, wie Sie auf Datenelemente nur über ein real existierendes Objekt, also eine Variable dieser Klasse zugreifen können, kann auch eine Funktion nur über ein Objekt aufgerufen werden. Objekt und Funktionsnamen werden dabei durch einen Punkt getrennt. Die Funktion arbeitet mit den Daten des Objekts, über das sie gerufen wurde. Aus prozeduraler Sicht könnte man es so sehen, dass eine Elementfunktion immer bereits einen Parameter mit sich trägt, nämlich das Objekt, über das sie aufgerufen wurde.
Beispiel:
#include <iostream> #include <conio.h> #include <string.h> using namespace std; //Klasse Enemy class Enemy { public: //Zugriffsrecht public void setHealth(int h); int getHealth(); string name; int id; private: //Zugriffsrecht private int health; }; //Methode setHealth zum Setzen der Lebenspunkte void Enemy::setHealth(int h) { health=h; } //Methode getHealth() zum Auslesen der Lebenspunkte int Enemy::getHealth() { return health; } //Hauptprogramm int main() { Enemy e1; //Objekt e1 der Klasse Enemy wird erzeugt //Zugriff auf öffentliche Variable name e1.name="Andreas"; //Zugriff auf öffentliche Variable id e1.id=1; //Aufruf der Methode setHealth e1.setHealth( 100 ); //Aufruf der Methode getHealth und Zugriff auf die öffentliche Variable name cout << "Der Gegner " << e1.name << " hat " << e1.getHealth() << " Lebenspunkte.\n"; //Aufruf der Methode setHealth e1.setHealth( 50 ); //Aufruf der Methode getHealth und Zugriff auf die öffentliche Variable name cout << "Der Gegner " << e1.name << " hat " << e1.getHealth() << " Lebenspunkte.\n"; return 0; }
Alternativ können Sie die Elementfunktion auch direkt in der Klasse definieren. Das wird leicht unübersichtlich, darum sollten Sie das nur bei sehr kurzen Funktionen tun.
#include <iostream> #include <conio.h> #include <string.h> using namespace std; class Enemy { public: //Zugriffsrecht public /***** INLINE - METHODENDEFINITION *****/ //Methode setHealth zum Setzen der Lebenspunkte void setHealth(int h) { health=h; } //Methode getHealth() zum Auslesen der Lebenspunkte int getHealth() { return health; } string name; int id; private: //Zugriffsrecht private int health; }; //Hauptprogramm int main() { Enemy e1; //Objekt e1 der Klasse Enemy wird erzeugt //Zugriff auf öffentliche Variable name e1.name="Andreas" //Aufruf der Methode setHealth e1.setHealth( 100 ); //Aufruf der Methode getHealth und Zugriff auf die öffentliche Variable name cout << "Der Gegner " << e1.name << " hat " << e1.getHealth() << " Lebenspunkte.\n"; //Aufruf der Methode setHealth e1.setHealth( 50 ); //Aufruf der Methode getHealth und Zugriff auf die öffentliche Variable name cout << "Der Gegner " << e1.name << " hat " << e1.getHealth() << " Lebenspunkte.\n"; return 0; }
So, wie Sie auf Datenelemente nur über ein real existierendes Objekt, also eine Variable dieser Klasse zugreifen können, kann auch eine Funktion nur über ein Objekt aufgerufen werden.
Objekt und Funktionsnamen werden dabei durch einen Punkt getrennt. Die Funktion arbeitet mit den Daten des Objekts, über das sie gerufen wurde. Aus prozeduraler Sicht könnte man es so sehen, dass eine Elementfunktion immer bereits einen Parameter mit sich trägt, nämlich das Objekt, über das sie aufgerufen wurde.
....
Enemy e1; //Objekt e1 der Klasse Enemy wird erzeugt
//Aufruf der Methode setHealth
e1.setHealth( 100 );
....
Die Elementfunktion, die beim Erzeugen eines Objekts aufgerufen wird, nennt man Konstruktor. In dieser Funktion können Sie dafür sorgen, dass alle Elemente des Objekts korrekt initialisiert sind. Der Konstruktor trägt immer den Namen der Klasse selbst und hat keinen Rückgabetyp, auch nicht void. Der Standardkonstruktor hat keine Parameter.
class Enemy { public: //Zugriffsrecht public /**** Konstruktor ****/ Enemy() { health=0; name=""; id=0; } string name; int id; private: //Zugriffsrecht private int health; };
Im Falle einer Datumsklasse wäre es sinnvoll, dass der Konstruktor alle Elemente auf 0 setzt. Daran kann jede Elementfunktion leicht erkennen, dass das Datum noch nicht festgelegt wurde. Sie könnten alternativ das aktuelle Datum ermitteln und eintragen. Im Beispiel ist auch ein Destruktor definiert worden, obwohl er im Falle eines Datums keine Aufgabe hat.
class Enemy { public: //Zugriffsrecht public /**** Destruktor ****/ ~Enemy() { cout << "Ressourcen von " << name << " wurden freigegeben!"; } char name[50]; int id; private: //Zugriffsrecht private int health; };
Konstruktoren können genauso überladen werden wie normale Funktionen auch. Es kann neben dem Standardkonstruktor auch mehrere weitere Konstruktoren mit verschiedenen Parametern geben. Der Compiler wird anhand der Aufrufparameter unterscheiden, welcher Konstruktor verwendet wird.
class Enemy { public: //Zugriffsrecht public /**** Konstruktor ****/ Enemy() { name="Max Mustermann"; } Enemy(string n) { name=n; } Enemy(string n, int h) { name=n; health=h; } /**** Destruktor ****/ ~Enemy() { cout << "Ressourcen von " << name << " wurden freigegeben!"; } char name[50]; int id; private: //Zugriffsrecht private int health; };
Je nachdem wie viele Parameter beim Konstruktoraufruf übergeben werden, wird der passende Konstruktor ausgewählt. Existiert gar kein Konstruktor so wird vom Compiler ein leerer Konstruktor eingefügt.
int main (void) { Enemy e; //führt den Standardkonstruktor Enemy() aus -> name="Max Mustermann" Enemy e("Fritz Phantom"); //führt den 2. Konstruktor mit den passenden Argumenten aus -> name="Fritz Phantom" Enemy e("Hans Wurst", 100); //führt den 3. Konstruktor mit den passenden Argumenten aus -> name="Hans Wurst", health=100 return 0; }
Die Informatik steckt voller schöner Analogien. So hat die objektorientierte Programmierung den Begriff der »Vererbung« eingeführt, wenn eine Klasse von einer anderen Klasse abgeleitet wird und deren Eigenschaften übernimmt.
Eine Klasse kann als Basis zur Entwicklung einer neuen Klasse dienen, ohne dass ihr Code geändert werden muss. Dazu wird die neue Klasse definiert und dabei angegeben, dass sie eine abgeleitete Klasse der Basisklasse ist. Daraufhin gehören alle öffentlichen Elemente der Basisklasse auch zur neuen Klasse, ohne dass sie erneut deklariert werden müssen. Man sagt, die neue Klasse erbt die Eigenschaften der Basisklasse.
Durch die Elemente, die in der abgeleiteten Klasse definiert werden, wird die abgeleitete Klasse zu einem besonderen Fall der Basisklasse. Sie besitzt alle Eigenschaften der Basisklasse. Sie können neue Elemente hinzufügen. Am folgenden Beispiel wird deutlich, warum das Hinzufügen von Eigenschaften eine Spezialisierung ist.
Ein Beispiel
Aus Sicht eines Computerprogramms haben alle Personen (=Basisklasse)
Geschäftspartner haben darüber hinaus eine
Da die Geschäftspartner auch Personen sind, haben sie neben ihrer Bankverbindung auch Namen, Adressen und Telefonnummern (von der Basisklasse).
Einige Geschäftspartner können auch Kunden sein. Kunden haben zusätzlich zu den Eigenschaften eines Geschäftspartners noch eine
Lieferanten sind keine Kunden, aber auch Geschäftspartner. Sie haben noch
Selbst Mitarbeiter sind eigentlich Geschäftspartner, denn sie haben eine Bankverbindung. Darüber hinaus haben sie eine
Außendienstler haben alle Eigenschaften eines Mitarbeiters und zusätzlich ihren
Beispiel als UML-Diagramm:
Beispiel in C + +
class Person { public: string Name, Adresse, Telefon; }; class Partner : public Person { public: string Kto, BLZ; }; class Mitarbeiter : public Partner { public: string Krankenkasse; }; class Kunde : public Partner { public: string Lieferadresse; }; class Lieferant : public Partner { public: int anzRechnungen; }; class Aussendienst : public Mitarbeiter { public: string Bezirk; }; int main (void) { Aussendienst a1; a1.Bezirk="Amstetten"; a1.Name="Hans Wurst"; a1.Krankenkasse="Gebietskrankenkasse"; a1.BLZ="14200"; return 0; }
Damit stellt die Ausgangsklasse Person die Verallgemeinerung dar und jede abgeleitete Klasse eine Spezialisierung. Der Vorteil dieser Technik ist, dass der bestehende Code der Basisklasse nicht noch einmal für die neu geschaffene Klasse geschrieben werden muss. Beispielsweise würde eine Prüffunktion der Adresse, die der Klasse Person hinzugefügt wird, automatisch auch allen anderen Klassen, die direkt oder indirekt von Person abgeleitet wurden, hinzugefügt, ohne dass eine Zeile Code mehr geschrieben werden müsste. Eine Änderung in der Klasse Mitarbeiter würde immer auch auf die Außendienstler durchschlagen. Es gibt aber keine Rückwirkung auf die Geschäftspartner.
Die objektorientierte Programmierung hat einige neue Begriffe aufgebracht.
Der zentrale Begriff des Objekts bezeichnet einen Speicherbereich, der durch eine Klasse beschrieben wird. Prinzipiell kann sich der Anfänger ein Objekt als eine besondere Art einer Variablen vorstellen und die Klasse als die Typbeschreibung. Im Buch wird ein Objekt auch hin und wieder als Variable bezeichnet, insbesondere dann, wenn sich ein Objekt an dieser Stelle wie jede andere Variable verhält.
Die Daten-Elemente einer Klasse werden in diesem Buch meist Elementvariable genannt. In der objektorientierten Literatur findet sich dafür auch die Bezeichnung Attribut. Damit wird angedeutet, dass die Elementvariablen die Eigenschaften eines Objekts beschreiben.
Die Funktionen einer Klasse, die hier als Elementfunktionen bezeichnet werden, finden sich in der objektorientierten Literatur unter dem Namen Methode oder Operation wieder. Diese Bezeichnung bringt zum Ausdruck, dass die Funktion nur über das Objekt erreichbar und damit eine Aktion des Objekts ist.
Die Elementfunktion, die beim Erzeugen eines Objekts aufgerufen wird, nennt man Konstruktor. In dieser Funktion können Sie dafür sorgen, dass alle Elemente des Objekts korrekt initialisiert sind. Der Konstruktor trägt immer den Namen der Klasse selbst und hat keinen Rückgabetyp, auch nicht void. Der Standardkonstruktor hat keine Parameter.
Im Falle einer Datumsklasse wäre es sinnvoll, dass der Konstruktor alle Elemente auf 0 setzt. Daran kann jede Elementfunktion leicht erkennen, dass das Datum noch nicht festgelegt wurde. Sie könnten alternativ das aktuelle Datum ermitteln und eintragen. Im Beispiel ist auch ein Destruktor definiert worden, obwohl er im Falle eines Datums keine Aufgabe hat