C++ Kurs

Klassen

Die Themen:

Objekte in der Theorie
Die Klasse
Objekte
OOP-Begriffe
Definition von Memberfunktionen
Zugriffsrechte in Klassen
Zugriff auf Klassenmember
const-Memberfunktionen und mutable-Member
Aufruf von Funktionen aus Memberfunktionen
Kopieren von Objekten
Objekte als Parameter
Objekte als Rückgabewert
Entwicklung einer Klasse
Die objektorientierte Programmierung (OOP)
Beispiel und Übung

Objekte in der Theorie

In dieser Lektion beginnen wir den Einstieg in die objektorientierte Programmierung (OOP). Die Grundidee der OOP ist die Zusammenfassung von Daten und die sie verarbeitenden Funktionen in einem so genannten Objekt.

Sehen wir uns zunächst einmal an, was ein Objekt eigentlich ist. Nun, ein Objekt ist eine Instanz einer bestimmten Klasse. Aber ich vermute einmal, dass Sie jetzt noch nicht viel schlauer sind als vorher. Denn was ist eine Instanz oder eine Klasse?

Fangen wir also mit der Beschreibung der Klasse an. Eine Klasse ist letztendlich nichts anderes als ein neuer Datentyp, der (vereinfacht ausdrückt) Daten und Funktionen vereint. Bisher konnten Daten und Funktionen unabhängig voneinander bestehen, d.h. eine Funktion war (bei sauberer Programmierung) nicht auf das Vorhandensein eines bestimmten Datums angewiesen und auch nicht umgekehrt. Eine Klasse bringt nun Daten und Funktionen zusammen. Sie besteht in der Regel aus Daten und Funktionen, die aber auf irgend eine Weise voneinander abhängig sind. Die Daten einer Klassen werden auch als deren Eigenschaften (Properties) bezeichnet und die Funktionen als Memberfunktionen. Diese Memberfunktionen einer Klasse verarbeiten nun aber nicht irgend welche Daten, sondern in der Regel nur die Eigenschaften ihrer Klasse. Alle Eigenschaften und Memberfunktionen einer Klasse werden auch als deren Member bezeichnet. Damit enthält eine Klasse also Member, die sich wiederum aus Eigenschaften und Memberfunktionen zusammensetzen. Später in dieser Lektion, wenn wir einmal eine richtige Klasse entworfen haben, werden wir uns die Begriffe nochmals anhand eines praktischen Beispiels ansehen.

Doch zunächst einmal ein (theoretisches) Beispiel für eine Klasse. Fast alle Betriebssysteme bieten heutzutage eine grafische Oberfläche (GUI = graphic user interface) und eine solche grafische Oberfläche enthält fast immer  Fenster. Wenn wir nun versuchen wollen, ein solches allgemeines Fenster zu beschreiben, dann zählen wir dessen Eigenschaften auf. So hat ein Fenster zum Beispiel eine bestimmte Größe und Position. Aber diese Eigenschaften alleine machen ein Fenster noch nicht voll funktionsfähig. Wir benötigen letztendlich noch Funktionen, um die Eigenschaften des Fensters (Größe und Position) verändern zu können. Haben Sie die letzten Sätze aufmerksam gelesen? Dort tauchen wieder die Begriffe Eigenschaft und Funktion auf. Und sowohl die Eigenschaften (Größe und Position) wie auch die dazugehörige Funktionen (zum Verschieben und Vergrößern) bilden letztendlich eine Klasse Fenster. Wie bereits am Anfang erwähnt ist das Ziel der OOP, mehr oder weniger komplexe Gebilde als Ganzes abzubilden, d.h. die Eigenschaften und die darauf wirkenden Funktionen zusammenzufassen.

Kommen wir nun zum Begriff der Instanz. Eine Instanz ist prinzipiell nichts anderes, als die Realisierung einer Klasse. Dazu wird im Prinzip eine 'Variable' der entsprechenden Klasse definiert. Unter der OOP sprechen wir dann aber nicht von Variablen sondern von Objekten. Nehmen wir einmal an, Sie hätten eine Klasse Window definiert und folgende Anweisung geschrieben:

Window myWindow;

Dann ist myWindow eine Instanz der Klasse Window und man sagt auch: myWindow ist ein Objekt vom Typ. Sie können von einer Klasse beliebig viele Objekte definieren, die dann alle die gleichen Eigenschaften besitzen. Die Werte der Eigenschaften können aber unterschiedlich sein.

Aber sehen wir uns nun den prinzipiellen Aufbau einer Klasse näher an.

Die Klasse

Genauso wie Variablen einen bestimmten Datentyp besitzen (bool, short, double usw.), besitzen auch Objekte einen bestimmten Datentyp. C++ stellt für Objekte die drei Datentypen struct, class und union zur Verfügung. Auf den Unterschied zwischen den Datentypen struct und class kommen wir nachher gleich zu sprechen. Der union ist dann eine eigene Lektion gewidmet.

Bilden wir nun Schritt für Schritt das erwähnte Fenster in einer grafischen Oberfläche als Klasse ab.

Zunächst benötigt jede Klasse einen 'Rahmen', in dem die Eigenschaften und Memberfunktionen zusammengefasst werden können. Dieser Rahmen besteht zunächst aus dem Schlüsselwort class (oder auch struct), gefolgt von einem Namen für die Klasse (im Beispiel unten Window). Anschließend folgt ein Block {...}, der mit einem Semikolon abgeschlossen werden muss.


class Window
{
   ...     // Hier folgen gleich die
   ...     // Eigenschaften und Memberfunktionen
};

Vergessen Sie das Semikolon am Ende der Klassendefinition, so meldet Ihnen der Compiler beim Übersetzen des Programms eine Reihe von Fehlern. Dieses Vergessen des Semikolons ist am Anfang eine häufige Fehlerursache.

Der Name für die Klasse muss immer eindeutig sein. Wenn Sie später mehrere Klassen definieren, so müssen diese unterschiedliche Namen besitzen. Auch dürfen Sie keine weiteren Variablen oder Funktionen mit dem gleichen Namen wie die Klasse definieren.

Fügen wir nun zur Klasse ihre Member (Eigenschaften und Memberfunktionen) innerhalb des Blocks {...} hinzu.

Fangen wir sinnvoller Weise mit den Eigenschaften an. Welchen Eigenschaften besitzt ein Fenster, d.h. was beschreibt ein solches GUI-Fenster? In der Praxis besitzt ein Fenster sicher ein Menge Eigenschaften. Wir werden uns aber auf vier Eigenschaften beschränken, damit es nicht zu unübersichtlich wird. Unser GUI-Fenster soll eine bestimmte Position und Ausdehnung besitzen.

Und damit könnte zum Beispiel im nächsten Schritt die Klasse für Fensterobjekte wie nachfolgend dargestellt aussehen. Ein Fenster besitzt hier die Eigenschaften, dass es eine bestimmte X- und Y-Position so wie ein definierte Breite und Höhe hat.


class Window
{
   int          xPos, yPos;    // Position
   unsigned int width, height; // Grösse
};

Für die Eigenschaft in einer Klasse können alle bisher bekannten Datentypen verwendet werden; so ist es zum Beispiel auch erlaubt, innerhalb einer Klasse als Eigenschaft eine weitere Klasse zu verwenden. Solche eingeschlossenen Klassen werden später noch gesondert behandelt, da sie bestimmte Anforderungen erfüllen müssen.

Sie können bis jetzt aber noch keine Konstanten als Eigenschaften einer Klasse definieren. Wie Konstanten als Eigenschaften definiert werden, erfahren Sie in der Lektion über Konstruktoren und Destruktoren.

Aber diese Eigenschaften alleine machen das Fenster uninteressant. Es würde sich weder verschieben noch in der Größe verändern lassen. Um die Eigenschaften verändern zu können, werden in der OOP Memberfunktionen verwendet. Und auch diese Memberfunktionen werden, da sie vom Konzept her nur zur Manipulation der Eigenschaften angedacht sind, innerhalb der Klasse deklariert bzw. definiert.

Sehen wir uns die auch gleich wieder anhand der Klasse für Fensterobjekte an. Welche Memberfunktionen innerhalb der Klasse notwendig sind, ergibt sich hier fast von alleine aufgrund der Eigenschaften. Wir haben eine Fensterposition, also benötigen wir eine Memberfunktion zum Verschieben des Fensters, die wir im Beispiel mit Move(...) bezeichnen. Ferner besitzt das Fenster noch eine Größe, also benötigen wir zum Verändern der Fenstergröße eine weitere Memberfunktion, im Beispiel Size(...) genannt.


class Window
{
   int          xPos, yPos;    // Position
   unsigned int width, height; // Grösse
   void Move(int x, int y);                    // Position verändern
   void Size(unsigned int w, unsigned int h);  // Grösse verändern
};

Die Datentypen der Parameter der Memberfunktionen ergeben sich hier automatisch aus den Datentypen der Eigenschaften, die nachher mit der Memberfunktion verändert werden sollen.

Die Gesamtheit aller Memberfunktionen einer Klasse wird auch als deren Schnittstelle (Interface) bezeichnet, da in einer 'sauberen' Implementierung einer Klasse nur über diese Memberfunktionen die Eigenschaften verändert werden sollten.

Beachten Sie bitte, dass in der obigen Klassendefinition die Memberfunktionen nur deklariert sind. Wie Memberfunktionen definiert werden, das sehen wir uns nachher an.

Nicht alle Klassen benötigen unbedingt Memberfunktionen für den Zugriff auf die Eigenschaften. Wenn Sie das Symbol links anklicken können Sie sich einmal ein Beispiel für eine Klasse ansehen, die keine Memberfunktionen enthält.

Folgt nun der nächste Schritt, die Definition von Objekten.

Objekte

Ist der Aufbau eines Fensters vollständig beschrieben, kann an das eigentliche 'Erstellen' von Fenstern erfolgen. Bisher haben wir ja lediglich die Eigenschaften und die Schnittstelle eines Fensters festgelegt. Um ein Fenster anzulegen, wird ein Objekt der Fensterklasse Window definiert. Dies erfolgt analog zur bisherigen Definition einer Variablen, d.h. zuerst folgt der Datentyp des Objekts und danach der Name.

Der vollständige Datentyp eines Objekts besteht aus dem Schlüsselwort class bzw struct (je nach dem, welchen Typ die Klasse hat) und dem Klassennamen. C++ erlaubt es auch, bei Eindeutigkeit das Schlüsselwort class bzw. struct einfach wegzulassen, so wie im Beispiel bei der zweiten Objekt-Definition dargestellt. Diese zweite Form der Objekt-Definition ist auch die in der Praxis am gebräuchlichste.


// Klassendeklaration
class Window
{
   ... // Member-Definitionen
};

// Objekte vom Typ class Window definieren
class Window firstWin;
Window secondWin;

Beide Fensterobjekte enthalten zwar die gleichen Eigenschaften, diese können aber (und werden es in der Regel auch) unterschiedliche Werte besitzen. So kann das Fenster firstWin eine andere Größe besitzen als das Fenster secondWin.

Sie können aber nicht nur einfache Objekte definieren, sondern sogar auch Objektfelder. Die Definition eines Objektfeldes erfolgt analog zur Definition eines Feldes mit den Standard-Datentypen.


// Klassendeklaration
class Window
{
   ... // Member-Definitionen
};

// Objektfeld definieren
Window winArray[10];

OOP-Begriffe

Nach dem Sie nun wissen, wie eine Klasse prinzipiell aufgebaut ist, jetzt nochmals die wichtigsten OOP-Begriffe im Überblick:

// Eine Klasse besteht aus Member
class Window
{

// Die Member einer Klasse sind deren Eigenschaften und Memberfunktionen
// Die Eigenschaften einer Klassen sind die Klassendaten
int          xPos, yPos;
unsigned int width, height;

// Die Memberfunktionen bilden die Schnittstelle (Interface) einer Klasse
// nach aussen hin und wirken auf die Eigenschaften
void Move (int x, int y);
void Size (unsigned int w, unsigned int h);
};
// Objekte sind Instanzen von Klassen
Window myWindow;

So weit, so gut. Sehen wir uns als nächstes an, wie die Memberfunktionen einer Klasse definiert werden.

Definition von Memberfunktionen

Definition von Memberfunktionen innerhalb der Klasse

Eine Memberfunktionen kann innerhalb der Klasse selbst definiert werden. Die Definition erfolgt dann prinzipiell gleich wie die Definition einer normalen Funktion, nur jetzt eben innerhalb der Klasse.


class Window
{
   ...        // Eigenschaften
   void Move(int x, int y)
   {
      ...     // Fenster verschieben
   }
   void Size(unsigned int w, unsigned int h)
   {
      ...     // Grösse verändern
   }
};

Beachten Sie, dass nach der schließenden Klammer der Memberfunktionen kein Semikolon steht.

Definition von Memberfunktionen außerhalb der Klasse

Hier sind jetzt zwei Schritte notwendig:

  1. Innerhalb der Klasse wird die Memberfunktion nur deklariert. Die Deklaration erfolgt in der gleichen Art und Weise wie bei Funktionen.
  2. Bei der Definition außerhalb der Klasse ist es nun erforderlich, dass die zu definierende Memberfunktion ihrer Klasse zugeordnet wird. Dazu wird nach dem Returntyp und vor dem Name der Memberfunktion der entsprechende Klassenname gefolgt vom Gültigkeitsbereichs-Operator :: angegeben.

class Window
{
   ...
   // Deklaration der Memberfunktionen
   void Move(int x, int y);
   void Size(unsigned int w, unsigned int h);
};

// Definition der Memberfunktion Move(...)
void Window::Move(int x, int y)
{
   ... // Fenster verschieben
}
// Definition der Memberfunktion Size(...)
void Window::Size(unsigned int w, unsigned int h)
{
   ... // Grösse verändern
}

Da bei der Definition der Memberfunktionen außerhalb der Klasse der Klassenname mit angegeben werden muss, können verschiedene Klassen durchaus Memberfunktionen mit gleichem Namen besitzen. Der Compiler kann die Definition der Memberfunktionen durch die Angabe des Klassennamens immer eindeutig zuordnen.

Definition von Klassen/Memberfunktionen in der Praxis

Sehen wir uns jetzt an, wie Klassen und deren Memberfunktionen in der Praxis definiert werden.

Wenn Sie eine Klasse in verschiedenen Modulen (Quellcode-Dateien) verwenden wollen, so müssen Sie die Klassendefinition in einer entsprechenden Header-Datei ablegen. Es hat sich in der Praxis als sinnvoll erwiesen, der Header-Datei den gleichen Namen wie der in ihr definierten Klasse zu geben. Innerhalb der Klassendefinition werden dann die Memberfunktionen nur deklariert.


// Datei: window.h

// Klassendefinition

class Window
{
   int xPos, yPos;               // Position
   unsigned int width, height;   // Grösse
   void Move(int x, int y);                     // Verschieben
   void Size(unsigned int w, unsigned int h);   // Grösse verändern
};

Die Definitionen der Memberfunktionen erfolgen dann in einer Quellcode-Datei, die ebenfalls sinnvoller Weise den Namen der Klasse erhält, deren Memberfunktionen in ihr definiert werden; nur diesmal selbstverständlich mit der Extension .cpp anstelle von .h. In dieser Datei wird dann zuerst die Header-Datei mit der Klassendefinition eingebunden und danach folgen die entsprechenden Memberfunktionen-Definitionen.


// Datei: window.cpp

// Einbinden der Header-Datei

#include "window.h"

// Definition der Memberfunktion Move(...)
void Window::Move(int x, int y)
{
   ... // Fenster verschieben
}
// Definition der Memberfunktion Size(...)
void Window::Size(unsigned int w, unsigned int h)
{
   ... // Grösse verändern
}

Diese Aufteilung in getrennte Header- und Quellcode-Dateien hat noch einen weiteren Vorteil: wollen Sie eine Klasse weitergeben, so reicht es aus, wenn Sie die Header-Datei und die übersetzte(!) Quellcode-Datei (also die entsprechende .obj Datei) weitergeben. Der Benutzer Ihrer Klasse muss bei der Verwendung der Klasse die Header-Datei einbinden und zu seinem Projekt noch die entsprechende .obj Datei dazu binden. Das heißt, Ihr Quellcode bleibt geschützt, da er den Quellcode der Memberfunktionen nicht erhält.

Zugriffsrechte in Klassen

Nachdem die Klasse und deren Memberfunktionen definiert sind, könnten wir versuchen, auf die Elemente der Klasse zu zugreifen. Diese würde jedoch im Augenblick noch einen Fehler beim Übersetzen des Programms verursachen. Der Grund hierfür liegt darin, dass alle Klassen bestimmte Standard-Zugriffsrechte besitzen, die den Zugriff auf Ihre Member einschränken können. Das heißt, je nach Zugriffsrecht kann der Zugriff auf ein Member von außerhalb der Klasse verboten oder erlaubt sein. Der Sinn und Zweck dieser Zugriffsbeschränkung liegt darin, dass der Zugriff auf die Member nur noch kontrolliert erfolgen sollte. Stellen Sie sich dazu wieder einmal unsere bisherige Klasse Window vor. Diese Klasse enthält unter anderem die Breite und Höhe des Fensters. Ohne Zugriffskontrolle könnte der Anwender bis jetzt einfach hergehen und zum Beispiel die Fensterhöhe auf den (falschen) Wert -1 setzen, was dann wahrscheinlich ziemliche Probleme beim Darstellen des Fensters geben würde. Wird der Zugriff auf die Fenstergröße aber kontrolliert, so kann beim Setzen der Fenstergröße eine Plausibilitätsprüfung durchgeführt und falsche Werte abgewiesen werden.

Sehen wir uns an, welche Zugriffsrechte es gibt und wie sie vergeben werden.

Um den Zugriff auf Member von außerhalb der Klasse zu sperren, wird innerhalb der Klasse vor die zu sperrenden Member die Anweisung private: gestellt. Alle Member die nach dieser Anweisung folgen sind für den Zugriff zunächst einmal gesperrt. Diese Sperrung gilt aber nur für Zugriffe von außerhalb der Klasse. Die Memberfunktionen einer Klasse haben generell immer Zugriff auf alle anderen Member der eigenen Klasse.


class Window
{
 private:
   int xPos, yPos;               // Position
   unsigned int width, height;   // Grösse
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};

Beachten Sie den Doppelpunkt nach der Angabe des Zugriffsrechts!

Da aber eine Klasse mit nur gesperrten Member in der Regel nutzlos ist, muss diese Sperrung auch wieder aufgehoben werden können. Dies erfolgt durch die Anweisung public:. Auf alle Member die nach dieser Anweisung folgen, kann dann auch von außerhalb der Klasse heraus zugegriffen werden. Im nachfolgenden Beispiel sind damit die Eigenschaften der Klasse Window gegen den direkten Zugriff geschützt, während auf die Memberfunktionen zugegriffen werden kann.


class Window
{
 private:
   int xPos, yPos;               // Position
   unsigned int width, height;   // Grösse
 public:
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};

Und somit haben wir unseren kontrollierten Zugriff auf die Fenstereigenschaften! Soll zum Beispiel die Fenstergröße verändert werden, muss dazu die Memberfunktion Size(...) aufgerufen werden, denn width und height sind ja gegen den direkten Zugriff geschützt. Und in der Memberfunktion Size(...) kann dann eine Überprüfung der übergebenen Parameter auf Plausibilität erfolgen.

Wie Sie aus den bisherigen Ausführungen entnehmen können, sind Zugriffsspezifizierer innerhalb einer Klasse so lange gültig, bis sie durch einen anderen 'überschrieben' werden. Die Anzahl und Reihenfolge der private: und public: Anweisungen innerhalb einer Klasse ist laut C++ Standard beliebig.

In der Praxis hat es sich als sinnvoll erwiesen, die Eigenschaften einer Klasse soweit wie möglich innerhalb eines private-Bereichs unterzubringen, um so den Zugriff darauf über die Schnittstelle der Klasse (Memberfunktion) kontrollieren zu können. Versuchen Sie weiterhin eine gewisse Struktur in den Klassenaufbau zu bringen. Geben Sie z.B. zuerst alle public Eigenschaften, dann alle public Memberfunktionen, dann alle private Eigenschaften und zum Schluss die noch fehlenden private Memberfunktionen an.

Standard-Zugriffsrechte

Je nach Klassentyp (struct, union und class) haben die Klassenmember voreingestellte Zugriffsrechte:

Somit ist das Verhalten der beiden unten angegebenen Klassen identisch. Der Unterschied liegt nur in der Reihenfolge der Member. Beim Klassentyp class müssen Sie die Memberfunktionen explizit für den Zugriff freigeben während Sie beim Klassentyp struct die Eigenschaften explizit schützen müssen. Und dieses voreingestellte Standard-Zugriffsrecht ist auch der einzige Unterschied zwischen Klassen vom Typ struct und Klassen vom Typ class!


class Window
{
   // Member sind standardmäßig private
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};

struct Window
{
   // Member sind standardmäßig public
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
};

Um den Klassentyp union kümmern wir uns, wie bereits erwähnt, später noch.

Zugriffsrechte zwischen Objekten der gleichen Klasse

War's bis jetzt mehr oder weniger leicht verständlich, so kommt nun der etwas kompliziertere Teil der Zugriffsrechte. Sehen Sie sich dazu zunächst einmal folgende Aussage an:

Wird einer Memberfunktion ein Objekt der ihrer Klasse übergeben, so kann die Memberfunktion auch auf die geschützten Eigenschaften des übergegebenen Objekts zugreifen.

Sehen wir uns diesen Sachverhalt einmal an. Er spielt bei dem später beschriebenen Kopierkonstruktor eine entscheidende Rolle.

Das nachfolgende Beispiel zeigt die bekannte Klasse Window. Dieser Klasse wurde unter anderem die Memberfunktion CopyData(...) hinzugefügt, die die Eigenschaften eines als Parameter übergebenen Objekts in das aktuelle Objekt übernehmen soll. Hierzu erhält die Memberfunktion eine Referenz auf das zu kopierende Objekt, das natürlich ebenfalls der Klasse Window angehört. Obwohl nun die Eigenschaften des übergebenen Objekts gegen den direkten Zugriff geschützt sind (private), kann die Memberfunktion CopyData(...) auf die geschützten Daten des übergebenen Objekts zugreifen. Wie gesagt, dies funktioniert aber nur, wenn das übergebene Objekt der selben Klasse angehört wie die aufgerufene Memberfunktion.


class Window
{
   // Geschützte Eigenschaften
   short width, height;
   string title;
   // Öffentliche Memberfunktionen
 public:
   void Size(short w, short h);
   void SetTitle(const char *const pT);
   void CopyData(const Window& source);
   // weitere Member der Klasse
   ...
};
// Definition der Memberfunktion CopyData()
void Window::CopyData(const Window& source)
{
   width = source.width;
   height = source.height;
   title = source.title;
}

So, und das war's vorläufig auch schon zu den Zugriffsrechten. Später werden Sie noch ein weiteres Zugriffsrecht kennen lernen, das aber nur im Zusammenhang mit abgeleiteten Klasse eine Rolle spielt.

Objekte von Klassen mit nur public-Eigenschaften lassen sich bei der Definition eines Objekts auch initialisieren. Im weiteren Verlaufe des Kurses werden wir uns aber das allgemeine Verfahren zur Initialisierung von Objekten ansehen. Wenn Sie trotzdem mehr über diese Initialisierungsart erfahren wollen, klicken Sie links das Symbol an.

Zugriff auf Klassenmember

Zugriff aus Memberfunktionen heraus

Innerhalb einer Memberfunktion kann auf alle Member (unabhängig davon, ob public oder private) der eigenen Klasse direkt zugegriffen werden. Hierbei spielt es keine Rolle, ob der Zugriff auf Eigenschaften erfolgt oder aber weitere Memberfunktionen der eigenen Klasse aufgerufen werden. Ausgangpunkt soll folgender Code sein:


// Klassendefinition
class Window
{
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};
// Definition der Memberfunktionen
void Window::Move(int x, int y)
{
   // Position setzen
   xPos = x;
   yPos = y;
}
void Window::Size(unsigned int w, unsigned int h)
{
   // Grösse setzen
   width = w;
   height = h;
}

 Mit obiger Klassendefinition könnte zum Beispiel eine weitere Memberfunktion WinPos(...) der Klasse zum Verändern der Position und Größe eines Fensters wie folgt aussehen:


void Window::WinPos(int x, int y, unsigned int w, unsigned int h)
{
   Move(x, y);
   Size(w, h);
}

Die Wiederverwendung von bereits bestehendem Code, hier der Memberfunktionen Move(...) und Size(...), ist übrigens auch eines der erklärten Ziele der OOP. Auf neu-deutsch heißt dies auch Code-Reuse.

Direkter Zugriff nicht über Memberfunktionen

Sehen wir uns jetzt an, wie auf Member einer Klasse außerhalb von Memberfunktionen zugegriffen wird. Denken Sie aber immer daran, dass Sie außerhalb von Memberfunktionen nur Zugriff auf die public-Member einer Klasse haben!

Auf Member einer Klasse kann in der Regel nur über ein Objekt der Klasse zugegriffen werden. Denn letztendlich existieren die Member einer Klasse nur in Verbindung mit einem bestimmten Objekt. Der direkte Zugriff auf Member erfolgt durch die Angabe des entsprechenden Objekts, gefolgt vom Punktoperator '.' und dem Namen des jeweiligen Members.


// Klassendefinition
class Window
{
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};
// Objektdefinitionen
Window myWin, yourWin;
// Direkter Zugriff auf public-Member
myWin.Move(10,200);
yourWin.Size(640,480);

Im Beispiel werden zwei Objekte myWin und yourWin definiert. Anschließend werden die Positions-Eigenschaften von myWin durch den Aufruf von Move(...) geändert. Diese Änderung der Positions-Eigenschaft von myWin hat aber keine Auswirkung auf die Positions-Eigenschaft von yourWin. Beide Objekte besitzen zwar die gleichen Eigenschaften, die aber völlig unabhängig voneinander sind. Im Anschluss daran wird die Größen-Eigenschaft von yourWin durch den Aufruf von Size(...) verändert.

Selbstverständlich könnte zum Beispiel auch die Eigenschaft xPos verändert werden, wenn xPos als public-Eigenschaft definiert wäre. Die entsprechende Anweisung dazu könnte dann wie folgt aussehen:

myWin.xPos = 100;

Da es in der OOP aber fast ein Vergehen ist, Eigenschaften direkt zu verändern, sollten Sie zur Veränderung von Eigenschaften auch immer entsprechende Memberfunktionen zur Verfügung stellen. Sie erhalten dadurch eine wesentlich bessere Kontrolle über den Inhalt der Eigenschaften.

Indirekter Zugriff nicht über Memberfunktionen

Der indirekten Zugriff, das heißt über einen Zeiger, auf ein Member erfolgt durch Angabe des entsprechenden Objektzeigers, folgt vom Zeigeroperator -> und anschließendem Namen des Members. Extrem wichtig dabei ist, dass dem Zeiger vor dem Zugriff auch eine gültige Adresse, zum Beispiel die eines bestehenden Objekts, zugewiesen wurde. Dieser Punkt mag auf den ersten Blick zwar trivial erscheinen, ist aber in der Praxis ein häufiger Fehlerfall.


// Klassendefinition
class Window
{
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(int x, int y);                   // Verschieben
   void Size(unsigned int w, unsigned int h); // Grösse verändern
};
// Objektdefinitionen
Window myWin, yourWin;
// Objektzeigerdefinition und Initialisierung
Window *pWin = &myWin;
// Indirekter Zugriff auf myWin-Member
pWin->Move(10,200);
// Zeiger umsetzen auf yourWin Objekt
pWin = &yourWin
// Indirekter Zugriff auf yourWin-Member
pWin->Move(100,0);

Einem einmal definierten Objektzeiger können im weiteren Verlaufe des Programms beliebig oft weitere Adressen zugewiesen werden. Es wird dann immer das Member des Objekts aufgerufen, dessen Adresse gerade im Objektzeiger abgelegt ist. So wird im Beispiel zunächst das Objekt myWin und dann das Objekt yourWin indirekt über den Objektzeiger pWin verschoben.

Zugriff auf enum-Eigenschaften

Im Zusammenhang mit dem Zugriff auf Eigenschaften müssen wir uns nochmals mit dem enum-Datentyp befassen. Werden innerhalb einer Klasse enum-Eigenschaften definiert, so gehören diese natürlich zur Klasse. Wollen Sie diese enum-Konstante auch außerhalb einer Memberfunktionen verwenden (z.B. als Parameter beim Aufruf einer Memberfunktion), so müssen Sie vor dem Namen der enum-Konstante noch den Klassennamen und den Zugriffsoperator :: angegeben.

Beachten Sie bitte, dass die logische Verknüpfung von enum-Konstanten ein int-Ergebnis liefert. Siehe auch hier.

Ebenfalls muss der enum-Datentyp als public deklariert sein, da Sie ansonsten keinen Zugriff darauf haben. Beachten Sie dabei aber bitte, dass nur der enum-Datentyp und nicht auch die mit ihr verknüpfte enum-Eigenschaft als public (im Beispiel die Eigenschaft winStyle) deklariert ist.


// Klassendefinition
class Window
{
 public:
   // enum-Datentyp definieren
   enum Style {FRAME, CLOSEBOX, SYSMENU};
 private:
   // enum-Eigenschaft definieren
   enum Style winStyle;
   ...
 public:
   void DoAnything(Style s);
};
// Objekt definieren
Window myWin;
...
// Aufruf einer Memberfunktion
myWin.DoAnything(Window::FRAME);

const-Memberfunktionen und mutable-Member

Sehen wir uns jetzt noch einen besonderen Typ von Memberfunktionen an. Vielfach werden Sie Deklarationen von Memberfunktionen in der folgenden Art finden:

<Returntyp> MName ([Parameter]) const;

Worauf es hierbei ankommt, ist das Schlüsselwort const am Ende der Deklaration. Memberfunktionen mit dieser Deklaration werden auch als const-Memberfunktionen bezeichnet. Solche const-Memberfunktionen können keine Eigenschaften eines Objektes verändern. Nachfolgend sehen Sie ein kleines Beispiel für die Anwendung einer const-Memberfunktion. Die Memberfunktion GetXPos() liefert lediglich die X-Position des Fensters zurück und verändert selbst keine Eigenschaft ihres Objekts.


// Klassendefinition
class Window
{
   int xPos, yPos;
   ...
 public:
   int GetXPos() const;
   // weitere Member der Klasse
   ...
};
// Definition der Memberfunmtion GetXPos()
int Window::GetXPos() const
{
   return xPos;
}
...
int main()
{
   Window myWin;
   ...
   int x = myWin.GetXPos();
   ...
}

Beachten Sie bitte, dass sowohl bei der Deklaration wie auch bei der Definition der Memberfunktion jeweils const angegeben werden muss.

Da laut obiger Definition der Aufruf von const-Memberfunktionen keine Eigenschaften verändern darf, dürfen const-Memberfunktionen auch wiederum nur const-Memberfunktionen aufrufen. Der Aufruf von nicht-const-Memberfunktionen führt zu einer Fehlermeldung während des Übersetzungsvorgangs.

Im Zusammenhang mit const-Memberfunktionen muss noch das Schlüsselwort mutable erwähnt werden. Mit mutable definierte Eigenschaften können auch in const-Memberfunktionen verändert werden, d.h. mutable überschreibt sozusagen das const-Attribut der Memberfunktion für bestimmte Eigenschaften. Außerdem erlaubt mutable selbst dann noch die Veränderung einer Eigenschaft, wenn von der Klasse ein const-Objekt definiert wurde.


// Klassendefinition
class Window
{
   int xPos, yPos;
   mutable long any;
 public:
   void DoSomething() const;
   ...
};
// Definition const-Memberfunktion
void Window::DoSomething() const
{
   xPos = 10;    // nicht erlaubt wegen const
   any = 10L;    // das geht wegen mutable
}

mutable kann nicht mit den Speicherklassen const und static kombiniert werden, d.h. die folgenden Eigenschaften sind nicht zulässig:

       mutable const int MAX=10;
      mutable static short statVar;

Auf die Speicherklasse static im Zusammenhang mit Klassen kommen wir später noch zu sprechen.

Aufruf von Funktionen aus Memberfunktionen

Werden aus Memberfunktionen heraus 'normale' Funktionen aufrufen, so können dabei drei Fälle auftreten:

1.) Innerhalb der Klasse gibt es keine Memberfunktion mit der gleichen Signatur (Name und Parameter) wie die aufzurufende Funktion. Dann erfolgt der Aufruf der Funktion wie gewohnt, d.h. es reicht die alleinige Angabe des Funktionsnamens.


// Funktionsdeklaration
bool CheckIt();
// Klassendefinition
class Any {
   ... // Enthält keine Memberfunktion CheckIt()
   void DoAny()
   {
      ...
      bool result = CheckIt();
   }
};

2.) Innerhalb der Klasse gibt es eine Memberfunktion mit der gleichen Signatur wie die aufzurufende Funktion, jedoch liegt die aufzurufende Funktion in einem eigenen Namensraum (wie z.B. alle Funktionen aus der Standard-Bibliothek, die im Namensraum std liegen). Standardmäßig wird dann beim Aufruf immer die Memberfunktion der eigenen Klasse aufgerufen. Um die Funktion aus dem anderen Namensraum aufzurufen, stellen Sie vor dem Aufruf den Namen des Namensraums (z.B. std), gefolgt von zwei Doppelpunkten.


#include <cmath>    // Bindet u.a. sin() ein
// Klassendefinition
class Any
{
   // sin() Memberfunktion
   double sin(double x)
   {
      ...
   }
   void DoAny()
   {
      ...
      // Aufruf der eigenen sin() Memberfunktion
      double res1 = sin(1.2);
      // Aufruf der Funktion aus cmath
      double res2 = std::sin(0.5);
   }
};

3.) Innerhalb der Klasse gibt es eine Memberfunktion mit der gleichen Signatur wie die aufzurufende Funktion, die jedoch in keinem eigenen Namensraum liegt. Auch hier wird standardmäßig wieder zuerst die Memberfunktion der eigenen Klasse aufgerufen. Um die "globale" Funktion aufzurufen, stellen Sie vor dem Funktionsnamen den globalen Zugriffsoperator :: (das sind zwei Doppelpunkte).


// Funktionsdeklaration
bool CheckIt();
// Klassendefinition
class Any {
   // CheckIt() Memberfunktion
   void CheckIt()
   {
      ...
   }
   void DoAny()
   {
      ...
      // Aufruf der CheckIt() Memberfunktion
      CheckIt();
      // Aufruf der globalen Funktion
      bool res2 = ::CheckIt();
   }
};

Beachten Sie im obigen Beispiel, dass die "globale" Funktion einen anderen Returntyp besitzt als die Memberfunktion. Dies reicht für die Unterscheidung, welche Funktion aufgerufen wird, nicht aus, da nur die Signatur betrachtet wird.

Wie sich das ganze Spiel mit globalen Variablen anstelle von Funktionen verhält, das können Sie sich ansehen wenn Sie das Symbol links anklicken.

Kopieren von Objekten

Verlassen wird jetzt die Definition von Objekten und Memberfunktionen und sehen uns einmal an, wie Objekte kopiert werden. In der Praxis ist es oft notwendig, die Daten von einem Objekt in ein anderes zu übertragen, wobei beide Objekte der gleichen Klasse angehören. Im ersten Ansatz könnten Sie jetzt hergehen und eine Memberfunktion, z.B. CopyObject(...), schreiben, die Eigenschaft für Eigenschaft über Zuweisungen umkopiert.

Doch es geht auch wesentlich einfacher wenn beide Objekte der gleichen Klasse angehören: weisen Sie die Objekte einfach einander zu. Durch die Zuweisungen werden alle Eigenschaften kopiert, egal wie viele es sind. Ganz schön clever vom Compiler, oder?


// Klassendefinition
class Window
{
   ...
};
// main() Funktion
int main()
{
   // Zwei Window Objekt definieren
   Window myWin, yourWin;
   ...
   // Eigenschaften von myWin nach yourWin übernehmen
   yourWin = myWin;
   ...
}

Nur eines müssen Sie dabei aber unbedingt beachten:

Enthält eine Klasse dynamische Eigenschaften (das sind in der Regel Eigenschaften die über Zeiger realisiert sind), so kann dieses Standardverhalten unter Umständen zu fatalen Fehlern führen. Mehr zu Zeigern und dynamischen Eigenschaften aber später im Kurs.

Objekte als Parameter

Parametertyp

Werden Objekte an Funktionen oder Memberfunktionen übergeben, so sollte dies in der Regel über Referenzparameter erfolgen. Eine Übergabe per called-by-value sollte soweit wie möglich vermieden werden. Übergeben Sie ein Objekt per called-by-value, so erhält die Funktion, wie auch bei den einfachen Daten, nur eine Kopie des Objekts, d.h. alle Eigenschaften des zu übergebenden Objekts werden in ein temporäres Objekt umkopiert und dieses dann an die Funktion übergeben.. Dies kann je nach Objektgröße erheblich Zeit und Platz auf dem Stack benötigen. Bei einer Übergabe per Referenz entfällt dieser Kopiervorgang.

Innerhalb der aufgerufenen Funktion bzw. Memberfunktion haben Sie dann über den Parameternamen Zugriff auf alle public-Member des übergebenen Objekts. Ist die aufgerufene Funktion eine Memberfunktion die zur gleiche Klasse wie das übergebene Objekt gehört, so hat die Memberfunktion Zugriff auf alle Member, auch auf die private Member, des übergebenen Objekts (siehe nächsten Abschnitt).


class Window
{
   ...
};
// Funktionsdeklaration
void DoAny (Window& obj);

// Objekt definieren
Window myWin;
int main()
{
   ...
   // Aufruf der Funktion
   DoAny(myWin);
   ...
}
// Funktionsdefinition
void DoAny (Window& obj)
{
   ...
   obj.Size (640,480);    // Übergebenes
   obj.Move (0,0);        // Fenster verändern
}

Den Nachteil den eine Übergabe eines Objekts als Referenzparameter mit sich bringt ist, dass die Funktion oder Memberfunktion das übergebene Objekt eventuell unbeabsichtigt verändern kann. In vielen Fällen kann es aber durchaus sinnvoll sein, dass eine Funktion bzw. Memberfunktion das übergebene Objekt nicht verändern können soll. In diesem Fall übergeben Sie das Objekt als konstante Referenz , so wie nachfolgend an der Funktion DoAny(...) dargestellt. Ein Versuch das Objekt dann innerhalb der Funktion/Memberfunktion zu verändern, führt zu einem Fehler beim Übersetzen des Programms. Dies gilt auch, wenn die Funktion/Memberfunktion eine Memberfunktion des übergebenen Objekts aufruft, die nicht als const-Memberfunktion definiert ist. Der Aufruf einer nicht-const-Memberfunktion würde ja ansonsten wieder eine Veränderung des übergebenen Objekts zulassen (siehe hier).


class Window
{
   ...
 public:
   void NonConstMeth();
   void ConstMeth(...) const;
};

// Normale Funktion mit const-Referenzparameter
void DoAny (const Window& obj)
{
   obj.NonConstMeth();   // Aufruf nicht-constMemberfunktion -> FEHLER!
   obj.ConstMeth();      // Aufruf const Memberfunktion -> OK!
}

// Objekt definieren
Window myWin;
// main() Funktion
int main()
{
   ...
   // Aufruf der Funktion
   DoAny(myWin);
   ...
}

Objekte der eigenen Klasse als Parameter

Normalerweise haben Funktionen/Memberfunktionen aus nur Zugriff auf die public-Member des übergebenen Objekts. Ein spezieller Fall ist jedoch der Aufruf einer Memberfunktion, die als Parameter ein Objekt der eigenen Klasse erhält. Da die aufgerufene Memberfunktion zur gleichen Klasse wie das übergebene Objekt gehört, kann innerhalb der Memberfunktion auch auf die private Eigenschaften des übergebenen Objekts zugegriffen werden. Nachfolgend sehen Sie hierzu ein Beispiel. Die Memberfunktion CopyObj(...) der Klasse Window erhält als Parameter ein Objekt der eigenen Klasse Window. Innerhalb von CopyObj(...) kann nun auch auf die ansonsten geschützten Eigenschaften (z.B.  xPos und yPos) des übergebenen Objekts zugegriffen werden. Diese Sachverhalt spielt später nochmals eine wichtige Rolle.


class Window
{
   int xPos, yPos;
   ...
 public:
   void CopyObj(const Window& obj);
   ...
};
void Window::CopyObj(const Window& obj)
{
   xPos = obj.xPos;
   yPos = obj.yPos;
   ...
}

Objekte als Rückgabewert

Wie Sie bereits erfahren haben, lassen sich innerhalb von Funktionen und Memberfunktionen lokale Variablen definieren. Und selbstverständlich können Sie in Funktionen/Memberfunktionen auch lokale Objekte definieren. Etwas aufpassen müssen Sie nur, wenn Sie ein Objekt aus einer Funktion/Memberfunktion heraus zurückgeben. Sehen wir uns dazu einmal ein Beispiel an:

Die Funktion CreateWin() soll zur Erstellung eines Fenster dienen. Dazu wird innerhalb der Funktion zunächst ein Window-Objekt erzeugt und die Referenz darauf an den Aufrufer zurückgeliefert.

Sie wissen doch noch, dass lokale Variablen (und damit auch lokale Objekte) nur innerhalb der Funktion existieren?


class Window
{
   ...
};
// FALSCH!!!
// Funktion liefert Referenz zurück
// was bis zum Programmabsturz führen kann

Window& CreateWin()
{
   Window myWin;
   ... // irgend etwas mit Fenster tun
       // und dann Referenz zurückliefern

   return myWin;
}

// main() Funktion
int main()
{
   ...
   // Fenster erstellen lassen
   Window newWin = CreateWin();
   ...
}

Am Ende der Funktion wird nun das lokale Window-Objekt wieder gelöscht. Wenn Sie jetzt (wie im Beispiel) eine Referenz auf dieses lokale Window-Objekt zurückliefern, so würden Sie eine Referenz auf ein nicht mehr existierendes Objekt zurückgeben. Und das ist natürlich ein Fehlerfall! Je nach verwendetem Compiler erhalten Sie bei einer solchen Vorgehensweise entweder eine Warnung oder (was eigentlich richtiger ist) einen Fehler.

Doch wie geht's richtig?

Geben Sie anstelle einer Referenz einfach das Objekt selbst zurück. In diesem Fall führt der Compiler intern Folgendes durch: Da das erzeugte Window-Objekt nur bis zum Ende der Funktion existiert, aber trotzdem an den Aufrufer zurückgeliefert werden muss, wird am Ende der Funktion zunächst ein temporäres Window-Objekt erstellt. Dieses temporäre Window-Objekt wird dann mit dem lokale Window-Objekt initialisiert. Anschließend wird das lokale Window-Objekt gelöscht. Das zurückgelieferte temporäre Window-Objekt wird nach der Rückkehr aus der Funktion/Memberfunktion dem Ziel-Window-Objekt (im Beispiel ist dies newWin) zugewiesen. Und am Ende der Anweisung, die die Funktion aufgerufen hat, wird schließlich noch das temporäre Window-Objekt gelöscht. Sie sehen also, der Compiler hat hier (hinter ihrem Rücken) sehr viel zu tun.


class Window
{
   ...
};
// So geht's richtig!!!
// Funktion liefert temporäres Window-Objekt zurück
Window CreateWin()
{
   Window myWin;
   ... // irgend etwas mit Fenster tun
       // und dann Referenz zurückliefern

   return myWin;
}

// main() Funktion
int main()
{
   ...
   // Fenster erstellen lassen
   Window newWin = CreateWin();
   ...
}

Enthält das zurückzugebende Objekt dynamische Daten (Zeiger!), so muss die Klasse des Objekts in der Regel den Kopierkonstruktor und einen überladenen Zuweisungsoperator besitzen! Mehr dazu nachher gleich.

Da die Entwicklung von Klassen die Grundlage der OOP ist, wollen wir uns jetzt einmal die Entwicklung einer komplette Klasse ansehen.

Entwicklung einer Klasse

In diesem Beispiel werden wir eine Klasse zur Darstellung und Manipulation eines Rechtecks für eine fiktive Oberfläche entwickeln.

Wie Sie vielleicht noch wissen, sollten bei der Entwicklung einer Klasse zuerst deren Eigenschaften (Daten) definiert werden. Sind alle Eigenschaften bekannt, so ergibt sich daraus fast zwangsläufig die Schnittstelle (Memberfunktionen) der Klasse. Fangen wir also mit den Eigenschaften an, die die Klasse Rect zur Darstellung eines Rechtecks besitzt. Wir werden die Klasse, und damit ihre Eigenschaften, in einer eigenen Header-Datei rect.h definieren, so wie es in der Praxis üblich ist.

Unser darzustellendes Rechteck soll die die Eigenschaften Position, Größe und Farbe besitzen. Definieren wir also zunächst die Klasse und fügen ihr dann die Eigenschaften hinzu. Da wir vielleicht später noch weitere Klassen definieren, die ebenfalls eine Farbinformation enthalten, wird die Farbinformation in einer getrennten einfachen Klasse ohne eigene Memberfunktionen abgelegt. Diese Farb-Klasse enthält die jeweiligen Rot-, Grün- und Blauanteile der Farbe als unsigned char Werte. Für die Eigenschaften Position und Größe werden short-Datentypen verwendet.


Datei rect.h

// Für verkürzte Schreibweise
typedef unsigned char BYTE;

// Struktur für Farbwerte
struct Color
{
   BYTE red;
   BYTE green;
   BYTE blue;
};

// Klassendefinition
class Rect
{
   short xPos, yPos;       // Position
   short width, height;    // Grösse
   Color rectColor;        // Farbe
   ...
};

Sind die Eigenschaften definiert, kann es an die Definition der Schnittstelle gehen. Beachten Sie bitte, dass die Eigenschaften private sind und die nachfolgenden Memberfunktionen public, so wie es sich in der OOP gehört.

Zuerst benötigen wir eine Memberfunktion um Eigenschaften des Rechtecks mit Defaultwerten zu belegen. Nennen wir diese Memberfunktion Init(...). In unserem Fall erhält die Memberfunktion Init(...) als Parameter nur die Position und Größe des neuen Rechtecks. Die Farbe soll standardmäßig bei der Definition eines Rechteck-Objekts auf schwarz (RGB = 0,0,0) eingestellt werden.

In der nächsten Lektion werden Sie ein Verfahren kennen lernen, wie ein Objekt bei seiner Definition automatisch initialisiert werden kann (Stichwort: Konstruktor).

Als nächstes deklarieren wir die restlichen Memberfunktionen, um die Eigenschaften des Rechtecks gezielt verändern zu können. Da unser Rechteck Eigenschaften für die Position, Größe und Farbe besitzt, definieren wir drei entsprechende Memberfunktionen Move(...), Resize(...) und SetColor(...). Zum Schluss benötigen wir noch eine Memberfunktion um das Rechteck letztendlich darzustellen. Die hierfür verwendete Memberfunktion erhält den Namen DrawIt().


Datei rect.h

...
// Klassendefinition
class Rect
{
   // Die Eigenschaften der Klasse (private!)
   ...
   // Die Schnittstelle der Klasse (public!)
 public:
    void Init(short x, short y, short w, short h);
    void Move(short x, short y);
    void Resize(short w, short h);
    void SetColor(BYTE r, BYTE g, BYTE b);
    void DrawIt();
};

Nach der Definition der Klasse geht's ans Definieren der Memberfunktionen. Die Memberfunktionen werden in einer eigenen Datei rect.cpp definiert. Damit der Compiler beim Übersetzen dieser Datei die Klasse Rect auch kennt, müssen Sie am Anfang die vorhin erstellte Header-Datei rect.h einbinden.

Der Aufbau der Memberfunktionen dürfte aus ihrer Funktion hervorgehen. Lediglich die Memberfunktion DrawIt() wurde etwas vereinfacht, da wir hier keine Grafikprogrammierung betreiben wollen. Sie gibt nur die Eigenschaften des Rechtecks aus.


Datei rect.cpp

// Definition der Memberfunktionen von Rect

// Standard Headerdateien einbinden

#include <iostream>
using std::cout;
using std::endl;

// Klassendefinition einbinden
#include "rect.h"

// Definition der Init() Memberfunktion
void Rect::Init(short x, short y, short w, short h)
{
   xPos = x;
   yPos = y;
   width = w;
   height = h;
   rectColor.red = 0x00;
   rectColor.green = 0x00;
   rectColor.blue = 0x00;
}
// Definition der Move() Memberfunktion
void Rect::Move(short x, short y)
{
   xPos = x;
   yPos = y;
}
// Definition der Resize() Memberfunktion
void Rect::Resize(short w, short h)
{
   width = w;
   height = h;
}
// Definition der SetColor() Memberfunktion
void Rect::SetColor(BYTE r, BYTE g, BYTE b)
{
   rectColor.red = r;
   rectColor.green = g;
   rectColor.blue = b;
}
// Definition der DrawIt() Memberfunktion
void Rect::DrawIt()
{
   cout << "Position: " << xPos << ',' << yPos << endl;
   cout << "Grösse : " << width << ',' << height << endl;
   cout << std::showbase << std::hex; cout << "RGB-Wert: "
        << static_cast<int>(rectColor.red) << ','
        << static_cast<int>(rectColor.green) << ','
        << static_cast<int>(rectColor.blue) << endl;
   cout << std::dec;
}

Im Beispiel wurden eventuelle Plausibilitätsabfragen der Parameter weggelassen, damit das Beispiel noch einigermaßen überschaubar bleibt.

Anschließend ist die Datei rect.h noch so zu übersetzen, dass nur eine obj-Datei oder eine lib-Datei erzeugt wird. Sie können diese Datei nicht mit dem 'normalen' Compileraufruf übersetzen, da ansonsten der Linker versuchen würde, eine ablauffähige EXE-Datei zu erstellen. Und dies geht hier nicht, da die Datei wegen der fehlenden main() Funktion alleine nicht lauffähig ist. Wie Sie eine einzelne obj oder lib-Datei erzeugen, entnehmen Sie bitte der Beschreibung zu Ihrem Compiler.

Und damit sind die Arbeiten an unserer Klasse im Prinzip beendet. Selbstverständlich sollte die Klasse dann auch noch entsprechend dokumentiert werden, damit Sie später dann auch eingesetzt werden kann.

Nachfolgend sehen Sie eine kleine Anwendung, die den Einsatz der neuen Klasse Rect demonstriert. Um Objekte der neuen Klasse bilden zu können, müssen Sie zum einen im Programm die Header-Datei rect.h einbinden. Und zum anderen müssen Sie dem Linker noch mitteilen, dass er zu Ihrem Programm noch die im vorherigen Schritt erstellte obj- bzw. lib-Datei (mit dem Code der Rect-Memberfunktionen) dazu binden soll.


// Zuerst Dateien einbinden
#include <iostream>
#include "Rect.h"

// Kompletten Namensraum std einblenden
using namespace std;

// Zwei Rechteck definieren
Rect rect1, rect2;

// main() Funktion
int main()
{
   // Beide Rechtecke initialisieren
   rect1.Init(10,10,640,480);
   rect2.Init(100,50,800,600);
   // Rechteckdaten ausgeben
   cout << "1. Rechteck:\n";
   rect1.DrawIt();
   cout << "2. Rechteck:\n";
   rect2.DrawIt();
   // 1. Rechteck verschieben
   rect1.Move(20,20);
   // 2. Rechteck vergrössern und Farbe abändern
   rect2.Resize(1024,786);
   rect2.SetColor(0xC0, 0xC0, 0xC0);
   // Rechteckdaten ausgeben
   cout << "1. Rechteck:\n";
   rect1.DrawIt();
   cout << "2. Rechteck:\n";
   rect2.DrawIt();
}

Damit wollen wir die Einführung der Klassen und Objekte beenden. Sie werden im weiteren Verlaufe des Kurses noch genügend Möglichkeiten haben, dies ausführlich zu üben.

Die objektorientierte Programmierung (OOP)

Sehen wir uns nun noch kurz an, was eine objektorientierte Programmiersprache, wie z.B. C++, von einer prozeduralen Programmiersprache, wie z.B. C, unterscheidet.

Zum einen unterstützt OOP die Kapselung von Member (Encapsulation). Durch die Kapselung kann ein Klasse bestimmte Eigenschaften und Memberfunktionen für den direkten Zugriff sperren (private Member). Aber das sollten Sie in der Zwischenzeit ja schon wissen. Damit kann eine Klasse als eine Art Blackbox betrachtet werden, die eine genau definierte Schnittstelle (Interface, public Memberfunktionen) besitzt. Nur über diese Schnittstelle kann der Anwender dann mit der Klasse (eigentlich dem Objekt) agieren. Im vorherigen Beispiel waren z.B. die Memberfunktionen Size(...) und Move(...) zugänglich, während die Eigenschaften wie width oder yPos vor dem Anwender verborgen waren. Aufgrund diese Kapselung der Eigenschaften kann der Anwender nicht mehr direkt durch Manipulation z.B. der Eigenschaft width die Fensterbreite unzulässig verändern. Dies kann nur noch über die Memberfunktion Size(...) erfolgen, die dann natürlich auch entsprechende Plausibilitätsüberprüfungen durchführen sollte.

Als zweites Merkmal stellt die OOP die Vererbung (Inheritance) zur Verfügung. Durch Vererbung werden die Schnittstelle und die Eigenschaften einer Klasse (Basisklasse) an eine andere Klasse (abgeleitete Klasse) übertragen. Diese neue Klasse verfügt dann über alle Member der ursprünglichen Klasse so wie ihre eigenen Member. Um wieder zum vorherigen Beispiel zurück zu kommen, könnten Sie die Klasse Window als Basisklasse für eine neue Klasse Button verwenden, da ein Button (Schaltfläche) auch eine definierte Ausdehnung und Beschriftung besitzt. Zusätzlich würde die neue Klasse Button noch die Eigenschaft erhalten, dass sie z.B. einen bestimmten Zustand wie gedrückt oder ausgewählt annehmen kann.

Und als letzte Eigenschaft stellt OOP die Polymorphie (Polymorphism) zur Verfügung. Polymorphie kennzeichnet die Eigenschaft, dass Memberfunktionen in abgeleiteten Klassen zwar mit gleichem Namen aber unterschiedlichem Ablauf implementiert werden können. Um diesen etwas abstrakten Sachverhalt zu veranschaulichen, kehren wir zum Beispiel mit der neuen Klasse Button zurück. Button besitzt ja unter anderem die Member seiner Basisklasse Window. Sowohl Window wie auch Button benötigen eine Memberfunktion um sich  darstellen zu können. Mithilfe der Polymorphie kann nun sowohl die Klasse Window wie auch die der Klasse Button jeweils eine Memberfunktion mit dem Namen Draw() hierfür verwenden. Wann welche Memberfunktion aufgerufen wird, hängt nur vom Objekttyp ab.

Mehr zu den einzelnen OOP Eigenschaften erfahren Sie aber im Verlaufe des Kurses noch.

So, nun kommt das Beispiel und dann sind Sie wieder dran.

Beispiel und Übung

Beispiel:

Es wird eine Klasse zum Abspeichern von komplexen Zahlen entwickelt. Eine komplexe Zahl besteht aus zwei Teilen: einem Realanteil und einem Imaginäranteil. Für beiden Teile werden double-Werte verwendet.

Damit der Anwender die Eigenschaften (Daten = Real- und Imaginäranteil) nicht direkt verändern kann, sind diese gegen den direkten Zugriff geschützt (private).

Die Klasse enthält u.a. eine Memberfunktion um die komplexe Zahl zu initialisieren ( Init(...) ) und eine Memberfunktion um sie auszugeben ( PrintComplex() ).

Anschließend werden zwei Memberfunktionen definiert, eine um komplexe Zahlen zu addieren und eine sie zu subtrahieren. Werden zwei komplexe Zahlen addiert bzw. subtrahiert, so werden jeweils deren Real- und Imaginäranteile getrennt addiert bzw. subtrahiert (siehe auch Programmausgabe).

In der main() Funktion werden zwei Objekte dieser Klasse definiert und initialisiert. Danach wird die zweite Zahl zur ersten addiert und das Ergebnis ausgegeben. Das so erhaltene Ergebnis wird anschließend von der zweiten Zahl subtrahiert.

Beachten Sie im Beispiel bitte, an welchen Stellen das Schlüsselwort const überall steht.

1. Komplexe Zahl:
Realteil: 1.1 / Imaginärteil: 2.2
2. Komplexe Zahl:
Realteil: 3.3 / Imaginärteil: 4.4

Nach Addition 2. Zahl zur 1. Zahl
Neue 1. Zahl:
Realteil: 4.4 / Imaginärteil: 6.6

Nach Subtraktion der 1. Zahl von der 2. Zahl
Neue 2. Zahl:
Realteil: -1.1 / Imaginärteil: -2.2


// Beispiel zu Klassen

// Dateien einbinden

#include <iostream>
#include <iomanip>

using std::cout;
using std::endl;

// Klassendefinition
class Complex
{
   // Geschuetzte Eigenschaften
   double real;      // Real-Anteil
   double imag;      // Imaginär-Anteil
 public:
   // Memberfunktionen
   void Init(double r, double i);
   void AddComplex(const Complex& op);
   void SubComplex(const Complex& op);
   void PrintComplex() const;
};

// Definition der Memberfunktionen
// Komplexe Zahl initialisieren

void Complex::Init(double r, double i)
{
   real = r;
   imag = i;
}
// 2 komplexe Zahlen addieren
void Complex::AddComplex(const Complex& op)
{
   real += op.real;
   imag += op.imag;
}
// 2 komplexe Zahlen subtrahieren
void Complex::SubComplex(const Complex& op)
{
   real -= op.real;
   imag -= op.imag;
}
// Komplexe Zahl ausgeben
void Complex::PrintComplex() const
{
   cout << "Realteil: " << real;
   cout << " / Imaginärteil: " << imag << endl;
}

// main() Funktion
int main()
{
   // 2 komplexe Zahlen (Objekte) definieren
   Complex Number1, Number2;
   // Objekte initialisieren
   Number1.Init(1.1, 2.2);
   Number2.Init(3.3, 4.4);
   // Objekte ausgeben
   cout << "1. Komplexe Zahl:\n";
   Number1.PrintComplex();
   cout << "2. Komplexe Zahl:\n";
   Number2.PrintComplex();
   // 2. Objekt zum 1. Objekt addieren
   Number1.AddComplex(Number2);
   // und Ergebnis ausgeben
   cout << "\nNach Addition 2. Zahl zur 1. Zahl\n";
   cout << "Neue 1. Zahl:\n";
   Number1.PrintComplex();
   // 1. Objekt vom 2. Objekt subtrahieren
   Number2.SubComplex(Number1);
   // und Ergebnis ausgeben
   cout << "\nNach Subtraktion der 1. Zahl von der 2. Zahl\n";
   cout << "Neue 2. Zahl:\n";
   Number2.PrintComplex();
}

Übung:

Realisieren Sie eine Klasse zur Klassifizierung von Daten. Bei der Klassifizierung wird der Gesamtwertebereich der Daten in kleinere Wertebereiche (Klassen) unterteilt und die Häufigkeit des Auftretens von Daten innerhalb der Klasse gezählt.

Beispiel: Wertebreich: 0..99
Anzahl der Klassen: 20
daraus folgt: Wertebereich der 1. Klasse: 0..4
Wertebereich der 2. Klasse: 5..9
Wertebereich der 3. Klasse: 10..14
...
Wertebereich der 20. Klasse: 95..99

Die zu klassifizierenden Daten liegen als unsigned short Werte vor, welche über eine Memberfunktion an die Klasse zur Klassifizierung übergeben.

Des weiteren soll die Klasse eine Memberfunktion enthalten, die die Häufigkeit des Auftretens der Daten in den Klassen ausgibt.

Für eventuell notwendigen Initialisierung der Eigenschaften der Klasse, ist ebenfalls eine Memberfunktion zu verwenden.

Verwenden Sie keine Literale (magic numbers), sondern statt dessen entsprechende benannte Konstanten. Da bis jetzt noch keine Klassen-Konstanten definiert werden können, verwenden Sie globale benannte Konstanten. Legen Sie mithilfe dieser Konstanten die maximalen Anzahl der Klassen (Bereiche), die obere Grenze des Wertebereichs der Daten (Untergrenze soll 0 sein) und die Anzahl der Daten fest.

Erzeugen Sie in der main() Funktion entsprechend viele Zufallszahlen und geben Sie diese zur Kontrolle aus. Übergeben Sie dann die Zufallszahl an die Klasse zur Klassifizierung. Zum Schluss geben Sie noch die Häufigkeit der Daten in den Klassen aus.

Die nachfolgende Ausgabe geht von 10 Klassen, einem Wertebereich der Daten von 0...99 und 100 Daten aus.

41, 67, 34, 0, 69, 24, 78, 58, 62, 64, 5, 45, 81, 27, 61, 91, 95, 42, 27, 36, 91 , 95, 47, 26, 71, 38, 69, 12, 67, 99, 35, 94, 3, 11, 22, 33, 73, 64, 41, 11, 53, 3, 41, 29, 78, 16, 35, 90, 42, 88, 6, 40, 42, 64, 48, 46, 5, 90, 29, 70, 50, 6, 40, 66, 76, 31, 8, 44, 39, 26, 23, 37, 38, 18, 82, 29, 41,

Verteilung:
0...9: 10
10...19: 7
20...29: 14
30...39: 11
40...49: 17
50...59: 8
60...69: 12
70...79: 6
80...89: 5
90...99: 10

Lösung ansehen!

Da Klassen DIE zentrale Rolle spielen, hier gleich noch eine weitere Übung.

Übung:

Es ist eine Klasse für die Implementierung eines Stack zu schreiben. Ein Stack ist ein Bereich, der zum temporären Sichern von Werte dient, d.h. auf diesem Stack können Werte abgelegt und dann später ausgelesen werden. Hierbei gilt, dass der zuletzt abgelegte Wert als erstes wieder ausgelesen wird. Im Prinzip können Sie sich ein Stack als eine Art Ablage vorstellen, auf dem Sie ein Blatt nach dem anderen ablegen. Wenn Sie die Ablage abarbeiten, holen Sie sich immer das zuoberst liegende Blatt wieder.

Der in dieser Übung zu realisierende Stack soll zur Ablage von short-Wert dienen. Damit die Übung zunächst einfach bleibt, sollen maximal 10 short-Werte auf dem Stack zwischengespeichert werden können. Verwenden Sie zur Definition der maximalen Anzahl von short-Werten wieder eine globale benannte Konstante und kein Literal!

Für die Ablage einer short-Zahl ist eine Memberfunktion Push(...) zu schreiben. Diese Memberfunktion liefert so lange true zurück, wie der ihr übergebene Wert abgelegt werden kann. Ist der Stack komplett belegt, so muss Push(...) false zurückliefern.

Zum Abholen der auf dem Stack abgelegten Werte soll eine Memberfunktion Pop(...) verwendet werden. Enthält der Stack noch Werte, liefert die Memberfunktion als Returnwert true zurück und über einen Parameter den abgeholten short-Wert. Ist der Stack leer, liefert die Memberfunktion false.

Für eine eventuelle Initialisierung des Stacks ist bei Bedarf eine gesonderte Memberfunktion zu erstellen.

Es ist ein globales Stackobjekt zu definieren. In der main() Funktion ist anschließend der Stack so lange mit 'Zufallszahlen' zu füllen, bis er voll ist. Die abgelegten Werte sind zur Kontrolle auszugeben.

Nach dem der Stack vollständig gefüllt ist, sind alle Werte wieder vom Stack abzuholen und erneut auszugeben.

Schiebe Werte auf Stack:
41 67 34 0 69 24 78 58 62 64
Lese Werte vom Stack:
64 62 58 78 24 69 0 34 67 41

Lösung ansehen