C++ Kurs

Klassen

Die Themen:

Einführung

Mit diesem Kapitel beginnen wir den Einstieg in die objektorientierte Programmierung (OOP). Eine der Grundideen der OOP ist die Kapselung und das Zusammenfassen von Daten und der sie verarbeitenden Funktionen in einem in der jeweiligen Programmiersprache geeigneten Konstrukt. Und unter C++ ist dieses Konstrukt eine Klasse.

Eine Klasse ist letztendlich nichts anderes als ein neuer Datentyp, der (vereinfacht ausgedrückt) Daten und Funktionen vereint. Konnten bisher Daten unabhängig von den sie verarbeitenden Funktionen bestehen, so bringt eine Klasse nun Daten und Funktionen zusammen, d.h. sie besteht in der Regel aus Daten und Funktionen, die auf irgendeine 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 aber nicht irgendwelche 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, wenn wir unsere erste 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 zunächst dessen Eigenschaften auf. So hat ein Fenster zum Beispiel eine bestimmte Größe und Position. Aber diese Eigenschaften alleine machen ein Fenster noch nicht funktionsfähig. Wir benötigen letztendlich noch Funktionen, um die Eigenschaften des Fensters verändern zu können. Und sowohl die Eigenschaften (Größe und Position) als auch die dazugehörige Funktionen (zum Verschieben und Vergrößern des Fensters) bilden letztendlich die 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 nächsten Begriff, der der Instanz. Eine Instanz ist prinzipiell nichts anderes als die Realisierung einer Klasse. Dazu wird im Prinzip eine 'Variable' der entsprechenden Klasse definiert. In der OOP sprechen wir dann aber nicht von einer Variablen sondern von einem Objekt. Nehmen wir einmal an, wir 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 Window. Von einer Klasse können beliebig viele Objekte definiert werden, die dann alle die gleichen Eigenschaften und Memberfunktionen besitzen. Die Werte bzw. Inhalte der Eigenschaften können dabei aber durchaus 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 gleich noch zu sprechen. Der union ist dann ein eigenes Kapitel gewidmet.

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

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

PgmHeader
class Window
{
   ...     // Hier folgen gleich die
   ...     // Eigenschaften und Memberfunktionen
};
AchtungVergessen 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 der Klasse muss immer eindeutig sein. Wenn später mehrere Klassen definiert werden, so müssen diese unterschiedliche Namen besitzen. Auch dürfen keine weiteren Variablen oder Funktionen mit dem gleichen Namen wie die Klasse definiert sein.

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

Fangen wir sinnvollerweise mit den Eigenschaften an. Welche Eigenschaften besitzt ein Fenster, d.h. was beschreibt ein solches GUI-Fenster? In der Praxis besitzt ein Fenster sicher eine Reihe von Eigenschaften. Wir werden uns aber auf vier Eigenschaften beschränken damit es nicht zu unübersichtlich wird. Unser GUI-Fenster soll nur eine definierte Position und Größe besitzen.

Und damit könnte die Klasse für Fensterobjekte wie nachfolgend dargestellt aussehen. Das Fenster besitzt hier die Eigenschaften, dass es eine bestimmte X- und Y-Position so wie eine definierte Breite und Höhe hat.

PgmHeader
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.

Ferner können die Eigenschaften bei ihrer Definition auch gleich mit einem Initialwert belegt oder auch als konstante Eigenschaften definiert werden.

PgmHeader
class Window
{
   int          xPos=0, yPos=0;          // Position
   unsigned int width=640, height=480;   // Grösse
   const int    DEFCOLOR = 0xFF0000;     // Konstante fuer Standard-Farbe
};
HinweisEine Definition von z.B. xPos oder yPos als auto-Eigenschaft würde fehlschlagen. auto-Eigenschaften sind nur für static const Eigenschaften definiert. Sie sehen also, es ist nicht ganz überflüssig sich mit Datentypen auseinanderzusetzen.

Aber diese Eigenschaften alleine machen das Fenster noch relativ uninteressant. Es würde sich weder verschieben noch in der Größe verändern lassen. Um die Eigenschaften verändern zu können, werden 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 dies auch gleich wieder anhand der Klasse für Fensterobjekte an. Welche Memberfunktionen innerhalb der Klasse notwendig sind, ergibt sich hier aufgrund der Eigenschaften fast von alleine. 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.

PgmHeader
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
};

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

HinweisAnstelle explizit die Datentypen bei den Memberfunktionen anzugeben, hätten Sie z.B. auch folgendes schreiben können:
void Move (decltype(xPos) x, decltype(yPos) y);

Die Gesamtheit aller Memberfunktionen einer Klasse wird auch als deren Schnittstelle (Interface) bezeichnet, da in einer 'sauberen' Implementierung nur über diese Memberfunktionen die Eigenschaften der Klassen 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 gleich an.

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

Nun folgt der nächste Schritt, die Definition von Objekten.

Objekte

Ist der Aufbau eines Fensters vollständig beschrieben, kann das eigentliche 'Erstellen' der Fenster 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 dessen 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 aber 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 gebräuchlichste.

PgmHeader
// 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/Inhalte besitzen. So kann das Fenster firstWin eine andere Größe besitzen als das Fenster secondWin.

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

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

// Objektfeld definieren
Window winArray[10];

OOP-Begriffe

Nach dem wir 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 nun 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.

PgmHeader
class Window
{
   ...        // Eigenschaften
   void Move(int x, int y)
   {
      ...     // Fenster verschieben
   }
   void Size(unsigned int w, unsigned int h)
   {
      ...     // Grösse verändern
   }
};
HinweisBeachten 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 dann aber erforderlich, dass die zu definierende Memberfunktion ihrer Klasse zugeordnet wird. Dazu wird nach dem Returntyp und vor dem Namen der Memberfunktion der entsprechende Klassenname gefolgt vom Gültigkeitsbereichs-Operator :: angegeben.
PgmHeader
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 Definitionen 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 eine Klasse in verschiedenen Modulen (Quellcode-Dateien) verwendet werden soll, so wird die Klassendefinition in einer eigenen Header-Datei abgelegt. 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 die Memberfunktionen dann nur deklariert.

PgmHeader
// 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 eigenen Quellcode-Datei, die ebenfalls sinnvollerweise den Namen der Klasse erhält, deren Memberfunktionen in ihr definiert werden; nur diesmal selbstverständlich mit der Erweiterung .cpp anstelle von .h. In dieser Datei wird dann zuerst die Header-Datei mit der Klassendefinition eingebunden und danach folgen die entsprechenden Definitionen der Memberfunktionen.

PgmHeader
// 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 Anwender Ihrer Klasse muss dann bei der Verwendung der Klasse die Header-Datei einbinden und zu seinem Projekt lediglich noch die entsprechende .obj Datei hinzufügen. Das heißt, Ihr Quellcode bleibt geschützt, da der Anwender den Quellcode der Memberfunktionen selbst nicht unbedingt benötigt um Objekte instantiieren zu können.

Erhält eine Memberfunktion einen Parameter vom Typ einer Eigenschaft, so können Sie auch hier wieder den decltype() Operator einsetzen. Diese bedeutet für Sie zwar etwas mehr Schreibarbeit, erleichtert aber später eine eventuelle Anpassung der Datentypen der Eigenschaften der Klasse.

PgmHeader
class Window
{
   int xpos, ypos;
   ...
   // Deklaration der Memberfunktionen
   void Move(decltype(xpos) x, decltype(ypos) y);
};

// Definition der Memberfunktion Move(...)
void Window::Move(decltype(xpos) x, decltype(ypos) y)
{
   ... // Fenster verschieben
}

Zugriffsrechte in Klassen

Nachdem die Klasse und deren Member definiert sind, könnten wir versuchen, auf die Member zuzugreifen. Diese würde jedoch 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 über ein Objekt der Klasse erlaubt sein oder nicht. Der Sinn und Zweck dieser Zugriffsbeschränkung liegt darin, den Zugriff auf die Member nur noch über eine genau definierte Schnittstelle zu erlauben. Stellen wir uns 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 z.B. die Fensterhöhe auf einen nicht plausiblen Wert, z.B. -1, setzen, was dann wahrscheinlich ziemliche Probleme beim Darstellen des Fensters geben wird. 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 die Member einer Klasse zu sperren, d.h. die Member zu schützen, wird innerhalb der Klasse vor die zu schützenden Member die Anweisung private: gestellt. Alle Member die nach dieser Anweisung folgen sind gegen den Zugriff über ein Objekt der Klasse zunächst einmal geschützt. Dieser Schutz gilt aber, wie gesagt, nur für Zugriffe über Objekte der Klasse. Die Memberfunktionen einer Klasse haben generell immer Zugriff auf alle anderen Member der eigenen Klasse.

PgmHeader
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
};
HinweisBeachten Sie den Doppelpunkt nach der Angabe des Zugriffsrechts!

Da aber eine Klasse mit nur geschützten Member in der Regel nutzlos ist, muss dieser Schutz auch wieder aufgehoben werden können. Dies erfolgt durch die Anweisung public:. Auf alle Member die nach dieser Anweisung folgen kann dann auch über ein Objekt der Klasse 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.

PgmHeader
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 damit haben wir den gewünschten 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 die Memberfunktion Size(...) kann dann eine Überprüfung der übergebenen Daten auf Plausibilität durchführen.

Wie aus den bisherigen Ausführungen entnommen werden kann, ist das Zugriffsrecht innerhalb einer Klasse so lange gültig, bis es durch ein anderes Zugriffsrecht überschrieben wird. Die Anzahl und Reihenfolge der private: und public: Anweisungen innerhalb einer Klasse ist laut C++ Standard beliebig.

HinweisIn 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. Weiterhin sollten Sie versuchen, 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 Klassenmember vordefinierte 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 beim Klassentyp struct die Eigenschaften explizit geschützt werden müssen. Und dieses voreingestellte Standard-Zugriffsrecht ist auch der einzige Unterschied zwischen Klassen vom Typ struct und Klassen vom Typ class!

PgmHeader
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 dritten Klassentyp union kümmern wir uns, wie bereits erwähnt, später noch.

Zugriffsrechte zwischen Objekte der gleichen Klasse

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

AchtungWird einer Memberfunktion ein Objekt ihrer Klasse übergeben, so kann die Memberfunktion auch auf die geschützten Eigenschaften des übergebenen 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 Window Objekts in das aktuelle Objekt übernehmen soll. Hierzu erhält die Memberfunktion eine Referenz auf das zu kopierende Objekt. 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 derselben Klasse angehört wie die aufgerufene Memberfunktion.

PgmHeader
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;       // Die Zugriffs-Syntax wird
   height = source.height;     // wird gleich erklaert
   title = source.title;
}

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

DetailObjekte, deren Klasse nur public-Eigenschaften enthält, lassen sich bei ihrer Definition auch gleich 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 der eigenen Klasse direkt zugegriffen werden, unabhängig davon, ob diese public oder private sind.

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

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

PgmHeader
void Window::WinPos(decltype(xPos) x, decltype(yPos) y,
                    decltype(width) w, decltype(height) h)
{
   Move(x, y);     // Fenster verschieben
   Size(w, h);     // Fenster in der Größe ändern
}

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 wird dies auch Code-Reuse genannt.

Direkter Zugriff nicht über Memberfunktionen

Sehen wir uns jetzt an, wie auf Member einer Klasse außerhalb von Memberfunktionen zugegriffen wird. Dabei ist aber stets zu beachten, dass außerhalb von Memberfunktionen nur der Zugriff auf die public-Member einer Klasse möglich ist!

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

PgmHeader
// Klassendefinition
class Window
{
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(decltype(xPos) x, decltype(yPos) y);    // Verschieben
   void Size(decltype(width) w, decltype(height) 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 selbstverständlich 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 werden die Größen-Eigenschaften von yourWin durch den Aufruf von Size(...) geändert.

HinweisIm Beispiel könnte auch die Eigenschaft xPos verändert werden, wenn xPos als public-Eigenschaft definiert wäre. Die entsprechende Anweisung dazu würde dann wie folgt aussehen:
myWin.xPos = 100;

Da es in der OOP aber fast ein Vergehen ist, Eigenschaften direkt zu ändern, sollten zur Änderung von Eigenschaften auch immer entsprechende Memberfunktionen zur Verfügung gestellt werden.

Indirekter Zugriff nicht über Memberfunktionen

Der indirekten Zugriff, das heißt über einen Objektzeiger, auf ein Member erfolgt durch Angabe des entsprechenden Objektzeigers, gefolgt vom Zeigeroperator -> und anschließendem Namen des Members. Extrem wichtig dabei ist natürlich, 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 ebenfalls ein häufiger Fehlerfall.

PgmHeader
// Klassendefinition
class Window
{
 private:
   int xPos, yPos;             // Position
   unsigned int width, height; // Grösse
 public:
   void Move(decltype(xPos) x, decltype(yPos) y);    // Verschieben
   void Size(decltype(width) w, decltype(height) h); // Grösse verändern
};
// Objektdefinitionen
Window myWin, yourWin;
// Objektzeigerdefinition und Initialisierung
auto *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 von Objekten desselben Typs zugewiesen werden. Es wird dann immer die Memberfunktion 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

Soll auf Eigenschaften vom Typ enum zugegriffen werden, so gilt es einiges zu beachten. Zum einen gehören sie natürlich zur Klasse. D.h. werden diese enum-Konstanten auch außerhalb einer Memberfunktionen verwendet (z.B. als Parameter beim Aufruf einer Memberfunktion), so muss vor dem Namen der enum-Konstante noch den Klassennamen und der Zugriffsoperator :: angegeben werden.

AchtungBeachten Sie, dass die logische Verknüpfung von enum-Konstanten ein int-Ergebnis und kein enum liefert.

Desweiteren muss der enum-Datentyp als public deklariert sein, da ansonsten kein Zugriff darauf möglich ist. Dabei sollte dann aber darauf geachtet werden, dass nur der enum-Datentyp als public deklariert ist und nicht auch die mit ihm verknüpfte enum-Eigenschaft.

PgmHeader
// Klassendefinition
class Window
{
 public:
   // enum-Datentyp definieren
   enum Style {FRAME, CLOSEBOX, SYSMENU};
 private:
   // enum-Eigenschaft definieren
   enum Style winStyle;
   ...
 public:
   void DoAnything(decltype(winStyle) 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. Manchmal finden Sie  Memberfunktionen die wie folgt deklariert sind:

Returntyp MName ([PARAMETER]) const;

Worauf es hierbei ankommt, ist das Schlüsselwort const am Ende der Deklaration. Memberfunktionen mit dieser Deklaration werden als const-Memberfunktionen bezeichnet. Solche const-Memberfunktionen haben die Besonderheit, dass sie keine Eigenschaften ihres Objekts ändern können. Nachfolgend ein kleines Beispiel für die Anwendung einer solchen const-Memberfunktion. Die Memberfunktion GetXPos() liefert lediglich die X-Position des Fensters zurück und besitzt damit nicht die Notwendigkeit, Eigenschaften des Objekts zu ändern.

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

Wie im obigen Beispiel ersichtlich, kann auto auch direkt zur Bestimmung des Returntyps der Memberfunktion eingesetzt werden. Dies ist hier deshalb möglich, da der Compiler beim Übersetzen des Programms den Datentyp von xPos beretis kennt. Alternativ hätten wir hier auch schreiben können:

auto GetXPos() const -> decltype(xPos); bzw.
decltype(xPos) GetXPos() const;
AchtungBeachten Sie, dass sowohl bei der Deklaration als auch bei der Definition der Memberfunktion jeweils const angegeben werden muss.
AchtungDa laut obiger Definition der Aufruf einer const-Memberfunktionen keine Eigenschaften ä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.

PgmHeader
// 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 Memberfunktion
   any = 10L;    // das geht wegen mutable
}
Hinweismutable 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 "gewöhnliche" Funktionen aufgerufen, 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.

PgmHeader
// Funktionsdeklaration
bool CheckIt();
// Klassendefinition
class Any {
   ... // Enthält keine Memberfunktion CheckIt()
   void DoAny()
   {
      ...
      auto 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, ist vor dem Aufruf der Name des Namensraums (z.B. std), gefolgt von zwei Doppelpunkten, zu stellen.

PgmHeader
#include <cmath>    // Bindet u.a. sin() ein
// Klassendefinition
class Any
{
   // sin() Memberfunktion
   double sin(double x)
   {
      ...
   }
   void DoAny()
   {
      ...
      // Aufruf der eigenen sin() Memberfunktion
      auto res1 = sin(1.2);
      // Aufruf der Funktion aus cmath
      auto 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, ist vor dem Funktionsnamen der globale Zugriffsoperator :: (das sind zwei Doppelpunkte) anzugeben.

PgmHeader
// Funktionsdeklaration
bool CheckIt();
// Klassendefinition
class Any {
   // CheckIt() Memberfunktion
   void CheckIt()
   {
      ...
   }
   void DoAny()
   {
      ...
      // Aufruf der CheckIt() Memberfunktion
      CheckIt();
      // Aufruf der globalen Funktion
      auto 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 werden soll, nicht aus, da nur die Signatur betrachtet wird.

DetailWie sich das Ganze 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. Im ersten Ansatz könnten wir jetzt eine Memberfunktion, z.B. CopyObject(...), schreiben, die Eigenschaft für Eigenschaft per Zuweisungen umkopiert.

Doch es geht auch wesentlich einfacher, wenn beide Objekte der gleichen Klasse angehören: wir weisen die Objekte einfach einander zu. Durch die Zuweisungen werden dann alle Eigenschaften kopiert, egal wie viele es sind und welchen Datentyp sie haben.

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

Nur eines ist dabei aber unbedingt zu beachten:

AchtungEnthält eine Klasse dynamische Eigenschaften (das sind in der Regel Eigenschaften auf die über Zeiger zugegriffen wird), so kann dieses Standardverhalten unter Umständen zu fatalen Fehlern führen. Mehr zu Zeigern und dynamischen Eigenschaften ebenfalls später noch.

Objekte als Parameter

Werden Objekte an Funktionen oder Memberfunktionen übergeben, so sollte dies in der Regel über Referenzparameter erfolgen. Eine Übergabe per call-by-value sollte soweit wie möglich vermieden werden. Übergeben Sie ein Objekt per call-by-value, so erhält die Funktion, wie bei den einfachen Daten auch, 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. Und dies kann je nach Objektgröße erheblich Zeit und Speicher benötigen. Bei einer Übergabe per Referenz entfällt dieser Kopiervorgang, da ja 'nur' ein Verweis auf das Objekt übergeben wird.

Innerhalb der aufgerufenen Funktion bzw. Memberfunktion kann dann über den Parameternamen auf alle public-Member des übergebenen Objekts zugegriffen werden. Ist die aufgerufene Funktion eine Memberfunktion die zur gleichen Klasse wie das übergebene Objekt gehört, so hat die Memberfunktion Zugriff auf alle Member, auch auf die private Member, des übergebenen Objekts. Aber dies sollte inzwischen ja bekannt sein.

PgmHeader
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 unbeabsichtigt verändern kann. In manchen Fällen kann es aber durchaus sinnvoll sein, das übergebene Objekt so zu schützen, dass es durch eine Funktion bzw. Memberfunktion nicht verändert werden kann. In diesem Fall ist das Objekt als konstante Referenz zu übergeben, 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 weitere 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.

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

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

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

Objekte als Rückgabewert

Wie wir bereits gesehen haben, können innerhalb von Funktionen und Memberfunktionen lokale Variablen definiert werden. Und selbstverständlich können in Funktionen/Memberfunktionen auch lokale Objekte definiert werden. Vorsicht ist aber geboten, wenn ein Objekt aus einer Funktion/Memberfunktion als Returnwert zurückgegeben werden soll. Sehen wir uns dazu einmal ein Beispiel an:

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

PgmHeader
class Window
{
   ...
};
// So geht es nicht!!!
// Funktion liefert Referenz zurück
// was bis zum Programmabsturz führen kann

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

   return myWin;
}

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

Am Ende von CreateWin() wird aber das lokale Window-Objekt wieder gelöscht, genauso wie alle anderen lokalen Daten auch. Wenn wir jetzt eine Referenz auf dieses lokale Window-Objekt zurückliefern, so würden eine Referenz auf ein nicht mehr existierendes Objekt zurückgegeben werden. Und das ist natürlich ein schwerwiegender Fehler! Je nach verwendetem Compiler erhalten Sie bei einer solchen Vorgehensweise entweder eine Warnung oder, was eigentlich richtiger ist, ein Fehlermeldung.

Doch wie geht's richtig?

Wir geben 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 lokalen 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, in der die Funktion aufgerufen wurde, wird schließlich noch das temporäre Window-Objekt gelöscht. Sie sehen also, es gibt hier sehr viel zu tun.

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

   return myWin;
}

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

Beachten Sie im obigen Beispiel auch, dass der Datentyp des Objekts newWin mittels auto definiert wird. auto kann nicht nur für normale Daten sondern auch für Objekte verwendet werden.

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

Default-Memberfunktionen und -Operatoren

Außer dass eigene Memberfunktionen für eine Klasse definiert werden können, definiert C++ standardmäßig die folgenden Memberfunktionen, die später noch behandelt werden:

Diese Default-Memberfunktionen werden aber nur dann vom Compiler automatisch generiert, wenn kein Kopierkonstruktor, Move-Konstruktor, Move-Zuweisungsoperator oder Destruktor explizit definiert wird. Wird mindestens eine dieser Memberfunktionen explizit definiert, so müssen die anderen bei Bedarf ebenfalls explizit definiert werden.

Doch damit nicht genug. Zusätzlich zu diesen Memberfunktionen definiert der Compiler noch die folgenden Operatoren für eine Klasse:

Diese Default-Memberfunktionen und -Operatoren werden im weiteren Verlauf ebenfalls noch ausführlich behandelt.

Entwicklung einer Klasse

Da die Entwicklung von Klassen die Grundlage der OOP unter C++ ist, wollen wir uns jetzt einmal die Entwicklung einer kompletten Klasse ansehen.

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

Wie vorhin ausgeführt, sollten bei der Entwicklung einer Klasse zunächst deren Eigenschaften bestimmt und definiert werden. Sind alle Eigenschaften bekannt, so ergibt sich daraus fast zwangsläufig die Schnittstelle, also die Memberfunktionen, der Klasse. Fangen wir mit den Eigenschaften an, die die Klasse Rect zur Darstellung eines Rechtecks benötigt. Wir werden diese Klasse 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. Schreiben wir also zunächst den Klassenrahmen und fügen diesem dann die Eigenschaften hinzu. Da wir vielleicht später noch weitere Klassen definieren, die ebenfalls eine Farbinformation enthalten, wird die Farbinformation in einer eigenen einfachen Klasse ohne 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.

PgmHeader
Datei rect.h

// Für verkürzte Schreibweise
using BYTE = unsigned char;

// Struktur für Farbwerte
// Farbanteile werden mit Defaultwerten initialisiert

struct Color
{
   BYTE red = 0;
   BYTE green = 0;
   BYTE blue = 0;
};

// 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 die 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 sein.

HinweisIn einem der nächsten Kapitel werden Sie das Standard-Verfahren kennenlernen, wie ein Objekt bei seiner Definition automatisch initialisiert werden kann (Stichwort: Konstruktor).

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

PgmHeader
Datei rect.h

...
// Klassendefinition
class Rect
{
   // Die Eigenschaften der Klasse (private!)
   ...
   // Die Schnittstelle der Klasse (public!)
 public:
    void Init(decltype(xPos) x, decltype(yPos) y,
              decltype(width) w, decltype(height) h);
    void Move(decltype(xPos) x, decltype(yPos) y);
    void Resize(decltype(width) w, decltype(height) h);
    void SetColor(BYTE r, BYTE g, BYTE b);
    void DrawIt() const;
};
HinweisAnstatt die Datentypen der Parameter mittels decltype(...) zu definieren, hätten wir hier auch die Datentypen der Parameter direkt angeben können, so wie bei der Deklaration der Memberfunktion SetColor(...). Wenn dann später aber die Datentypen der Eigenschaften der Klasse einmal geändert werden, dann muss auch immer daran gedacht werden, dass die Datentypen der Parameter der entsprechenden Memberfunktion ebenfalls mit zu ändern sind.

Nach der Definition der Klasse geht es nun an die Definition der Memberfunktionen. Die Memberfunktionen werden in einer eigenen Datei rect.cpp definiert. Damit der Compiler beim Übersetzen dieser Datei die Klasse Rect auch kennt, ist am Anfang die soeben erstellte Header-Datei rect.h einzubinden.

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

PgmHeader
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
// Die Farbanteile enthalten Defaultwerte

void Rect::Init(decltype(yPos) x, decltype(yPos) y,
                decltype(width) w, decltype(height) h)
{
   xPos = x;
   yPos = y;
   width = w;
   height = h;
}
// Definition der Move() Memberfunktion
void Rect::Move(decltype(xPos) x, decltype(yPos) y)
{
   xPos = x;
   yPos = y;
}
// Definition der Resize() Memberfunktion
void Rect::Resize(decltype(width) w, decltype(height) 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() const
{
   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;
}

Eventuelle Plausibilitätsabfragen der Parameter wurden hier weggelassen, damit das Beispiel noch einigermaßen überschaubar bleibt. In der Praxis sollten solche Plausibilitätsprüfung aber stets durchgeführt werden.

Abschließend wäre nun die Datei rect.cpp noch so zu übersetzen, dass eine obj-Datei oder eine lib-Datei erzeugt wird. Diese Datei kann aber nicht mit einem 'normalen' Compileraufruf übersetzt werden, da ansonsten der Linker versuchen würde, eine ablauffähige EXE-Datei zu erstellen. Und dies wird hier nicht gehen, 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 auch noch entsprechend dokumentiert werden, damit sie später dann auch eingesetzt werden kann.

Nachfolgend ist eine kleine Anwendung aufgeführt, die den Einsatz der Klasse Rect demonstriert. Um Objekte der Klasse instantiieren zu können, ist zum einen die Header-Datei rect.h mit in die Anwendung einzubinden. Und zum anderen muss dem Linker noch mitgeteilt werden, dass er zur Anwendung noch die im vorherigen Schritt erstellte obj- bzw. lib-Datei (mit dem Code der Rect-Memberfunktionen) dazu binden soll.

PgmHeader
// 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 von Klassen und Objekte beenden. Im weiteren Verlaufe des Kurses gibt es noch genügend Möglichkeiten, das Erstellen von Klassen und Objekten ausführlich zu üben.

Die objektorientierte Programmierung (OOP)

Sehen wir uns zum Schluss an, was eine objektorientierte Programmiersprache, wie z.B. C++, von einer prozeduralen Programmiersprache, wie z.B. C, unterscheidet.

Zum einen unterstützt die 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 sollte in der Zwischenzeit ja schon bekannt sein. Somit 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 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 dieser Kapselung der Eigenschaften kann der Anwender nicht mehr direkt die Eigenschaft width unzulässig ändern.

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 enthält dann alle Member der Basisklasse sowie ihre eigenen zusätzlichen Member. Um wieder auf das vorherige Beispiel zurück zu kommen, könnten wir die Klasse Rect als Basisklasse für eine neue Klasse Button verwenden, da ein Button (Schaltfläche) auch eine definierte Ausdehnung, Position und Farbe besitzt. Zusätzlich würde die neue Klasse Button noch die Eigenschaften erhalten, dass ein Button einen bestimmten Zustand, wie gedrückt oder ausgewählt, annehmen und im gedrückten Zustand in einer anderen Farbe dargestellt werden kann.

Und als letztes Merkmal stellt die OOP die Polymorphie (Polymorphism) zur Verfügung. Polymorphie kennzeichnet die Eigenschaft, dass Memberfunktionen in abgeleiteten Klassen zwar den gleichen Namen wie in ihrer Basisklasse besitzen können, jedoch in ihrer Implementierung abweichen können. Um diesen etwas abstrakten Sachverhalt zu veranschaulichen, kehren wir zu unserem Beispiel mit der neuen Klasse Button zurück. Button enthält ja unter anderem die Member seiner Basisklasse Rect, d.h. sowohl Rect als auch Button besitzen u.a. jeweils eine Memberfunktion für die Darstellung des Objekts. Mit Hilfe der Polymorphie kann nun sowohl für die Klasse Rect als auch für die Klasse Button eine Memberfunktion mit dem Namen DrawIt() 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, nach dieser doch sehr langen Einführung kommt nun das Beispiel und dann sind Sie wieder dran.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.