Klassen

8.1 Einführung
8.2 Klassendeklaration
8.3 Konstruktoren
8.4 Konstruktor-Initialisierungslisten
8.5 Zugriffsfunktionen
8.6 Private Memberfunktionen
8.7 Der Kopierkonstruktor
8.8 Der Klassendestruktor
8.9 Konstante Objekte
8.10 Strukturen
8.11 Zeiger auf Objekte
8.12 Statische Memberdaten
8.13 static-Memberfunktionen

Wiederholungsfragen zum Kapitel 8: Klassen

Beispiel 8.1 Implementierung einer Klasse
Beispiel 8.2: Eine in sich abgeschlossene Implementation der Klasse Rational
Beispiel 8.3 Eine Konstruktor-Funktion für die Klasse Rational
Beispiel 8.4: Der Klasse Rational weitere Konstruktoren hinzufügen
Beispiel 8.5: Initialisierungslisten in der Rational-Klasse verwenden
Beispiel 8.6: Vorgabe-Parameterwerte im Konstruktor der Rational-Klasse verwenden
Beispiel 8.7: Zugriffsfunktionen in der Klasse Rational
Beispiel 8.8 Private Memberfunktionen: gcd() und reduce()
Beispiel 8.9 Der Klasse Rational einen Kopierkonstruktor hinzufügen
Beispiel 8.10 Aufruf des Kopierkonstruktors verfolgen
Beispiel 8.11: Ein Destruktor für die Klasse Rational
Beispiel 8.12 Zeiger auf Objekte einsetzen
Beispiel 8.13 Eine Node-Klasse für verkettete Listen
Beispiel 8.14 Ein statisches Memberdatum
Beispiel 8.15: Ein private und static deklariertes Memberdatum
Beispiel 8.16: Eine static-Memberfunktion

8.1 Einführung

Eine Klasse ist Auch wenn beliebige Speicherbereiche generell als "Objekt" betrachtet werden können, wird das Wort gewöhnlich zur Beschreibung von Variablen benutzt, deren Typ eine Klasse ist.

Entsprechend befasst sich die "objektoriernterte Programmierung" mit Programmen, die Klassen verwenden.

Wir betrachten ein Objekt als abgeschlossene Einheit, Die Funktionalität eines Objekts verleiht diesem Leben einen Sinn, dass es "weiß", wie es bestimmte Dinge selbst erledigen kann.

Zur objektorienterten Programmierung gehört viel mehr als nur das Einbeziehen von Klassen in ihre Programme. Das ist jedoch der erste Schritt. Eine angemesseene Behandlung dieses Themenkomplexes sprent den Rahmen einer Einführung bei weitem.

8.2 Klassendeklaration

Hier ist die Deklaration einer Klasse, deren Objekte rationale Zahlen (Brüche) darstellen:

class Rational {             // class ist ein Schlüsselwort

public:             // gewährt Zugriff auch von ausserhalb
      void assign(int, int);
      double convert();
      void invert();
      void print();

privat:             // Zugriff nur innerhalb der Klassen
      int num, den;             // Memberdaten
};

Die Deklaration beginnt mit dem Schlüsselwort class, gefolgt von dem Namen der Klasse, und endet mit dem erforderlichen Semikolon. Der Name dieser Klasse lautet Rational.

Die Funktionen werden Memberfunktionen genannt, weil sie Mitglieder einer Klasse sind.

Ähnlich nennt man die Variablen num und den Memberdaten. Memberdaten werden auch Methoden oder Dienste genannt.

In dieser Klasse sind alle Memberfunktionen public und alle Memberdaten private. Der Unterschied liegt darin, dass auf öffentliche (public) Member auch von ausserhalb der Klasse zugegriffen werden kann, während Zugriffe auf private Member nur innerhalb der Klasse zulässig sind.

Das Verhindern von Zugriffen, die ausserhalb der Klasse vorkommen, nennt man "information hiding" ( Verbergen von Funktionen ). Der Programmierer kann dadurch seine Software in eigenständige Teile untergliedern, was die Verständlichkeit erhöht und Fehlersuche und Wartbarkeit erhöht.

Das folgende Beispiel zeigt, wie die Klasse Rational implemetiert und genutzt wird.

Beispiel 8.1 Implementierung einer Klasse

class Rational {

public:
      void assign(int, int);
      double convert();
      void invert();
      void print();

private:
      int num, den;
};

int main()
{

      Rational x;

      x.assign(22,7);
            cout << "x = " ;
      x.print();
            cout << " = " << x.convert() << endl;
      x.invert();
            cout << "1/x = ";
      x.print();
            cout << endl;

getch();
return 0;
}

void Rational::assign( int numerator, int denominator)
{
      num = numerator;
      den = denominator;
}

double Rational::convert()
{
      return double(num)/den;
}

void Rational::invert()
{
      int temp = num;
            num = den;
            den = temp;
}

void Rational::print()
{
      cout << num << '/' << den;
}

x = 22/7 = 3.14286
1/x = 7/22

x wurde hier als ein Objekt der Klasse Rational deklariert.

Beachten Sie, dass Memberfunktionen wie invert() aufgerufen werden, indem man ihnen den Namen ihres Besitzers voranstellt:

x.invert():


Tatsächlich lassen sich Memberfunktionen nur auf diese Weise aufrufen, weshalb man auch sagt, dass das Objekt x den Aufruf "besitzt".

Ein Objekt x wird einfach wie eine gewöhnliche Variable deklariert. Ihr Typ ist Rational. Wir können uns diesen Typ als "benutzerdefinierten Typ" vorstellen.

C++ ermöglicht es uns, die Definition der Programmiersprache dadurch zu erweitern, dass wir den neuen Typ Rational zur Menge der vordefinierten nummerischen Typen int, float usw. hinzufügen.

Wir können uns das Objekt wie folgt vorstellen:


Beachten Sie die Verwendung des Bezeichners Rational als Präfix bei allen Funktionsnamen. Das ist bei allen Definitionen von Memberfunktionen erforderlich, die ausserhalb der Klassendefinitionen erfolgen.

Der Operator zur Auflösung des Gültigkeitsbereiches :: wird benutzt, um die Funktionsdefinition mit der Klasse Rational zu verbinden.

Ohne diesen Bezeichner würde der Compiler nicht wissen,
dass es sich bei der definierten Funktion um eine Memberfunktion der Klasse Rational handelt.

Das lässt sich vermeiden, wenn man die Funktionsdefinition in die Deklaration einbezieht, wie es nachfolgend in Beispiel 8.2 gezeigt wird.

Wenn Objekte wie das Rational-Objekt x in Beispiel 8.1 deklariert werden, sagt man, Und so, wie man viele Variablen desselben Typs haben kann, kann man auch viele Instanzen derselben Klasse deklarieren:

Rational x, y, z;

Beispiel 8.2: Eine in sich abgeschlossene Implementation der Klasse Rational

class Rational {

public:
      void assign(int n, int d){ num = den; den = d; }
      double convert() {return double((num)/den;}
      void invert() { int temp = num; num = den; den = temp; }
      void print() { cout << num << '/' << den; }

private:
      int num, den;
};

In den meisten Fällen wird man es vorziehen, Memberfunktionen außerhalb der Klassendeklaration zu definieren und dabei den Operator zur Auflösung des Gültigkeitsbereiches wie in Beispiel 8.1 zu verwenden.

Diese Varianten trennt die Funktionsdeklaration von ihren Definitionen, was dem allgemeinen Prinzip von "information hiding" entspricht.

Tatsächlich werden die Funktionen oft in eigenständige Dateien ausgelagert und separtat kompiliert. Entscheidend dafür ist, dass Anwendungsprogramme, die die Klasse verwenden, nur wissen müssen, was die Objekte können, aber nicht wie sie es tun.

Natürlich entspricht dies auch den Arbeitsweise der vordefinierten Typen (int, double, usw.). Wir kennen das Resultat der Division eines float-Wertes durch einen anderen, aber wir wissen nicht, wie diese Division durchgeführt wird. (d.h., welcher Algorithmus implementiert ist). Am wichtigsten ist jedoch, dass wir dies auch gar nicht wissen wollen. Über derartige Details nachzudenken würde uns nur von unserer aktuellen Aufgabe ablenken. Diese Sichtweise wird oft Information Hiding genannt. Sie ist ein wichtiges Prinzip der objektorientierten Programmierung.

Wenn die Memberfunktionsdefinitionen wie in Beispiel 8.1 getrennt von den Deklarationen erfolgen, Das Interface ist Teil der Klasse, den der Programmierer kennen muss, wenn er die Klasse verwenden will.

Die Implementation wird normalerweise in einer separaten Datei "verborgenen", so dass dem Anwender (dem Programmierer) die Informationen, die er nicht benötigt, vorenthalten werden. Die Klassenimplementationen werden üblicherweise von Leuten vorgenommen, die unabhängig von den Programmierern arbeiten,, die diese Klassen später benutzen.

8.3 Konstruktoren

Die in Beispiel 8.1 definierte Klasse Rational verwendet zur Initialisierung ihrer Objekte die assign()-Funktion.

Es wäre natürlicher, wenn diese Initialisierung bei der Deklaration der Objekte erfolgen würde. Gewöhnlich (vordefinierte) Typen arbeiten wie folgt:

int n = 22;
char* s = "Hello";

C++ lässt dieses einfache Initialisierungsverfahren bei Konstruktorfunktionen für Klassenobjekte zu.


Das folgende Beispiel illustriert, wie die assign()-Funktion durch einen Konstruktor ersetzt werden kann.

Beispiel 8.3 Eine Konstruktor-Funktion für die Klasse Rational

class Rational {

public:
      Rational ( int n, int d) { num = n; den = d; }
      void print () { cout << num << '/' << den; }

private:
      int num, den;
};

int main()
{

      Rational x(-1,3), y(22,7);

            cout << "x = ";
            x.print();

            cout << "und y = ";
            y.print();

getch();
return 0;
}

x = -1/3 und y = 22/7

Die Konstruktorfunktion hat dieselbe Wirkung wie die assign()-Funktion in Beispiel 8.1. Die Deklaration
Rational x(-1,3), y(22,7);

entsprechen damit diesen Zeilen:
Rational x, y;
x.assign(-1,3);
y.assign(22,7);

Der Konstruktor einer Klasse "konstruiert" die Klassenobjekte, indem er den Speicher für die Klassenobjekte anfordert und initialisiert und weitere Aufgaben durchführt, die für die Funktion programmiert wurden.

Er erzeugt aus einem "Haufen nutzloser Bits" buchstäblich ein "lebendes Objekt".


Die Beziehung zwischen der Klasse Rational selbst und ihren instanziierten Objekten lässt sich folgendermassen darstellen:



Die Klasse selbst wird von einem Viereck mit abgerundeten Ecken dargestellt, die ihre Memberfunktionen enthält. Alle Funktionen enthalten einen Zeiger namens "this", der auf das aufgerufene Objekt zeigt.

Die dargestellte Momentaufnahme stellt den Zustand während der Ausführung der letzten Programmzeile dar, in der das Objekt y die print()-Funktion mit y.print() aufruft. An dieser Stelle zeigt der "this"-Pointer für den Konstruktor auf kein Objekt, weil der Konstruktor nicht aufgerufen wird.

Eine Klasse kann über mehrere Konstruktoren verfügen. Wie andere überladene Funktionen werden sie aufgrund ihrer verschiedenen Parameterlisten unterschieden.

Beispiel 8.4: Der Klasse Rational weitere Konstruktoren hinzufügen

class Rational {

public:
            Rational() { num = 0; den = 1; }
            Rational( int n ) { num = n; den = 1; }
            Rational ( int n, int d) { num = n; den = d; }
            void print () { cout << num << '/' << den; }

private:
            int num, den;
};

int main()
{

            Rational x, y(4), z(22,7);

                        cout << "x = ";
                        x.print();

                        cout << "\ny = ";
                        y.print();

                        cout << "\nz = ";
                        z.print();

getch();
return 0;
}

Die Ausgabe sieht dann so aus:
x = 0/1
y = 4/1
z = 22/7


Diese Version der Rational-Klasse hat drei Konstruktoren.

Unter den verschiedenen möglichen Konstruktoren einer Klasse ist der ohne Parameter der einfachste. Dieser wird Standardkonstruktor genannt. Wird dieser Konstruktor nicht ausdrücklich in der Klassendefinition deklariert, wird er automatisch vom System für die Klasse erzeugt. Das geschieht in Beispiel 8.1.

8.4 Konstruktor-Initialisierungslisten

Die meisten Konstruktoren initialisieren nur die Memberdaten eines Objekts.

Demzufolge verfügt C+++ über eine spezielle Möglichkeit zur Vereinfachung des Initialisierungscodes für Konstruktoren.

Dabei handelt es sich um eine Initialisierungsliste.


Der umschriebene dritte Konstruktor in Beispiel 8.2, der nun eine Initialisierungsliste benutzt, sieht so aus:

Rational ( int n, int d ) : num(n), den(d) { }

Die Anweisungen mit den Zuweisungen von n an num und d an den im Körper der Funktion entfallen nun. Ihre Aufgabe erfüllt die fett gedruckte Initialisierungsliste. Beachten Sie, dass die Liste mit einem Doppelpunkt beginnt und mit dem nun leeren Funktionskörper endet.

Die umschriebene Rational-Klasse mit ihren drei Konstruktoren benutzt nun eine Initialisierungsliste.

Beispiel 8.5: Initialisierungslisten in der Rational-Klasse verwenden

class Rational {

public:
      Rational() : num(0), den(1) { }
      Rational( int n ) : num(n), den(1) { }
      Rational ( int n, int d) : num(n), den(d) { }

private:
      int num, den;
};

Natürlich sind diese drei separaten Konstruktoren nicht notwendig. Sie lassen sich in einem einzigen Konstruktor zusammenfasssen, der Vorgabe-Parameterwerte benutzt.

Beispiel 8.6: Vorgabe-Parameterwerte im Konstruktor der Rational-Klasse verwenden

class Rational {

public:
      Rational( int n = 0, int d = 0 ) : num(n), den(d) {}

private:
      int num, den;
};

int main()
{

      Rational x, y(4), z(22,7);

getch();
return 0;
}

Hier steht x für 0/1, y für 4/1 und z für 22/7.

Erinnern Sie sich daran, dass die Vorgabewerte nur dann verwendet werden, wenn keine aktuellen Paramter übergeben werden.

Daher erhält bei der Deklaration des Rational-Objektes x, bei der keine Werte übergeben werden, Bei der Deklaration des Objektes y, bei der nur der Wert 4 übergeben wird, Bei der Deklaration von z werden keine Vorgabewerte verwendet.

8.5 Zugriffsfunktionen

Obwohl die Memberdaten einer Klasse üblicherweise als private deklariert werden, um den Zugriff auf sie zu beschränken, ist es auch üblich, public-Memberfunktionen einzubeziehen, die nur das Lesen der Daten gestatten.

Derartige Funktionen werden Zugriffsfunktionen genannt.

Beispiel 8.7: Zugriffsfunktionen in der Klasse Rational

class Rational {

public:
      Rational( int n = 0, int d = 0 ) : num(n), den(d) {}
      int numerator() const { return num; }
      int denominator() const { return den; }

private:
      int num, den;
};

int main()
{

            Rational x(22,7);
            cout << x.numerator() << '/' << x.denominator() << endl;

getch(); return 0;
}

22/7


Die Funktionen numerator() und denominator() geben die Werte der als private deklarierten Memberfunktionen zurück.

Beachten Sie die Verwendeung des Schlüsselwortes const in den Deklarationen der beiden Zugriffsfunktionen. Dies ermöglicht die Anwendung der Funktionen auf konstante Objekte. (siehe Abschnitt 8.9)

8.6 Private Memberfunktionen

Üblicherweise werden Memberdaten von Klassen als private und Memberfunktionen als public deklariert.

Diese Zweiteilung ist aber nicht notwendig. In einigen Fällen ist es nützlich, wenn man eine oder mehrere Memberfunktionen private deklariert. Damit lassen sich die Funktionen nur innerhalb der Klasse selbst verwenden, so dass es sich um lokale Hilfsfunktionen handelt.

Beispiel 8.8 Private Memberfunktionen: gcd() und reduce()

class Rational {

public:
      Rational ( int n = 0, int d = 1 ) : num(n), den(d) { reduce(); }
      void print() { cout << num << '/' << den << endl; }

private:
      int num, den;
      int gcd( int j, int k ) { if ( k == 0) return j; return gcd( k, j%k ); }
      void reduce() {int g = gcd ( num, den ); num /= g; den /= g; }
};

int main()
{

            Rational x(100,360);

            x.print();

getch();
return 0;
}

Die Version enthält zwei private Memberfunktionen.
  1. Die gcd()-Funktion gibt den größten gemeinsamen Teiler der zwei ihr übergebenen Integerzahlen zurück.
  2. Die reduce()-Funktion benutzt gcd(), um den Bruch num/den auf die kleinstmöglichen Werte zu bringen.
Entsprechend wird der Bruch 100/360 als Objekt 5/18 gespeichert.

Anstatt eine separate reduce()-Funktion zu verwenden, hätten wir die Reduktion im Konstruktor vornehmen können. Es gibt aber zwei gute Gründe für die dargestellte Vorgehensweise.

  1. Die Kombination von Konstruktion und Reduktion würde das Software-Prinzip verletzen, nach dem getrennte Aufgaben von separaten Funktionen übernommen werden sollten.
  2. wird die reduce()-Funktion später zur Reduktion der Ergebnisse arithmetischer Operationen benötigt, die mit Rational-Objekten durchgeführt werden.
Beachten Sie, dass die Schlüsselwörten private und public Zugriffsbezeichner genannt werden. Sie geben an, ob man außerhalb der Klassendefinition auf die Member zugreifen kann.

Das Schlüsselwort protected (geschützt) ist der dritte Bezeichner.

8.7 Der Kopierkonstruktor

Jede Klasse hat mindenstens zwei Konstruktoren. Ihre Deklarationen sind eindeutig:
X(); // Standardkonstruktor
X( const X& ); // Kopierinstruktor

Dabei ist X der Klassennname.

Diese beiden speziellen Konstruktoren würden für eine Klasse namens Widget zum Beispiel so deklariert:
Widget(); // Standardkonstruktor
Widget( const Widget& ); // Kopierkonstruktor

Den ersten dieser beiden speziellen Konstruktoren nennt man den Standardkonstruktor. Er wird immer dann automatisch aufgerufen, wenn ein Objekt in der einfachsten Form deklariert wird:

Widget x;


Den zweiten dieser speziellen Konstruktoren nennt man den Kopierkonstruktor. Er wird immer dann automatisch aufgerufen, wenn ein Objekt kopiert (dubliziert) wird.

Widget y(x);


Wenn einer dieser beiden Konstruktoren nicht explizit definiert wird, dann wird er automatisch iimplizit vom System definiert.

Wenn der Kopierkonstruktor aufgerufen wird, kopiert er ein existierendes Objekt komplett in seinem aktuellen Zustand in ein neues Objekt derselben Klasse. Wenn die Klassendefinition nicht ausdrücklich einen Kopierkonstruktor enthält ( wie das bei allen bisherigen Beispielen der Fall war ) dann wird standardmässig automatisch einer vom System erzeugt.

Mit der Möglichkeit, eigene Kopierkonstruktoren zu schreiben, können Sie ihre Software besser maßschneidern.

Beispiel 8.9 Der Klasse Rational einen Kopierkonstruktor hinzufügen

class Rational {

public:
      Rational( int n = 0, int d = 1 ) : num(n), den(d) { reduce();}
      Rational ( const Rational& r ) : num(r.num), den ( r.den ) { }
      void print() { cout << num << '/' << den ; }

private:
      int num, den;
      int gcd( int j, int k ) { if ( k == 0) return j; return gcd( k, j%k );
}       void reduce() {int g = gcd ( num, den ); num /= g; den /= g; }
};

int main()
{

            Rational x(100,360);
            Rational y(x);

                  cout << "x = ";
                  x.print();

                  cout << ", y = ";
                  y.print();

getch();
return 0;
}

x = 5/18, y = 5/18


Der Kopierkonstruktor kopiert die Felder num und den des Parameters n in das zu erzeugende Objekt.

Wenn y deklariert wird, ruft es den Kopierkonstruktor auf, der x in y kopiert.

Beachten Sie die für den Kopierkonstruktor erforderliche Syntax. Er muss einen Parameter haben, der mit der zu deklarierenden Klasse übereinstimmen muss, und er muss als konstante Referenz übergeben werden:

const X&;

Der Kopierkonstruktor wird immer dann automatisch aufgerufen, wenn Beispiel 8.10 Aufruf des Kopierkonstruktors verfolgen

class Rational {

public:
      Rational( int n , int d ) : num(n), den(d) { }
      Rational ( const Rational& r ) : num(r.num), den ( r.den ) { cout << "Kopierkonstruktor aufgerufen !!!\n"; }

private:
      int num, den;
};

Rational f(Rational r)             // ruft den Kopierkonstruktor auf, kopiert y nach r
      {
            Rational s = r;             // ruft den Kopierkonstruktor auf, kopiert r nach s
            return s;            // ruft den Kopierkonstruktor auf
      }

int main()
{

            Rational x(22,7);
            Rational y(x);            // ruft den Kopierkonstruktor auf, kopiert x nach y

            f(y);

getch();
return 0;
}

Kopierkonstruktor aufgerufen !!!
Kopierkonstruktor aufgerufen !!!
Kopierkonstruktor aufgerufen !!!
Kopierkonstruktor aufgerufen !!!


In diesem Beispiel wird der Kopierkonstruktor viermal aufgerufen.
  1. Bei der Deklaration von y kopiert x nach y;
  2. wenn y als Wert an die Funktion f übergeben wird, kopiert er y nach r;
  3. bei der Deklaration von s kopiert er r nach s,
  4. und er wird aufgerufen, wenn die Funktion einen Wert zurückgibt, auch wenn dabei nichts kopiert wird.
Beachten Sie, dass die Initialisierung von s wie eine Zuweisung aussieht. Aber als Teil einer Deklaration wird dabei der Kopierinstruktor aufgerufen, wie es auch bei der Deklaration von y der Fall ist.

Wenn Sie keinen Kopierinstruktor in Ihre Klassendefinition aufnehmen, wird er vom Compiler automatisch deklariert. Dieser "Vorgabe"-Kopierkonstruktor kopiert Objekte einfach bitweise. In vielen Fällen ist das genau das, was man will. Dann besteht kein Bedarf für einen explizit definierten Kopierkonstruktor.

Einigen wichtigen Fällen wird eine bitweise Kopie jedoch nicht gerecht. Die String-Klasse ist ein wichtiges Beispiel. Bei Objekten dieser Klasse enthalten die relevanten Memberdaten nur einen Zeiger auf den eigentlichen String, so dass bei einer bitweisen Kopie nur der Zeiger, aber nicht der String selbst dubliziert wird.

In derartigen Fällen müssen Sie unbeedingt Ihren eigenen Kopierkonstruktor definieren.

8.8 Der Klassendestruktor

Beim Erzeugen eines Objekts wird automatisch ein Konstruktor aufgerufen.

Ähnlich wird auch eine spezielle Memberfunktion aufgerufen, wenn ein Objekt zerstört wird.

Diese Funktion wird Destruktor genannt.

Jede Klasse hat genau einen Destruktor. Wenn dieser nicht ausdrücklich für eine Klasse definiert wird, dann wird der Destruktor ähnlich dem Standardkonstruktor, dem Kopierkonstruktor und dem Zuweisungsoperator automatisch erzeugt.

Beispiel 8.11: Ein Destruktor für die Klasse Rational

class Rational {

public:
          Rational() { cout << "Das Objekt wird geboren. \m"; }
          ~Rational() { cout << "Das Objekt stirbt.\n"; }

private:
          int num, den;
};

int main()
{

          {
                    Rational x;          // Anfang des Gültigkeitsbereiches von x
                    cout << "x ist am Leben. \n";           // Ende des Gültigkeitsbereiches
          }

          cout << "Jetzt sind wir zwischen den Blöcken.\n";

          {
                    Rational y;
                    cout << "Jetzt ist y am Leben. \n";
          }

getch();
return 0;
}     

Das Objekt ist geboren
x ist am Leben
Das Objekt stirbt
Jetzt sind wir zwischen den Blöcken
Das Objekt ist geboren
Jetzt ist y am Leben
Das Objekt stirbt


Die Ausgabe zeigt hier, wann der Konstruktor und der Destruktor aufgerufen werden

Der Klassendestruktor wird für ein Objekt aufgerufen, wenn das Ende dessen Gültigkeitsbereiches erreicht wird. Bei lokalen Objekten ist dies am Ende des Blocks, für den sie deklariert worden sind, der Fall.

Für static-Objekte gilt dies am Ende der main()-Funktion.

Es ist üblich, den Kopierkonstruktor, den Zuweisungsoperator und den Destruktor immer mit in die jeweilige Klassendefinition einzubeziehen, auch wenn sie vom System automatisch generiert werden können.

8.9 Konstante Objekte

Wenn ein Objekt nicht geändert werden soll, sollten Sie sie als konstante Objekte deklarieren.

Dies erreichen Sie mit dem Schlüsselwort const:
const char BLANK = ' ';
const int MAX_INT = 2147483647;
const double PI = 3.141592653589793;
void init(float a[], const int SIZE);

Wie Variablen und Funktionsparameter lassen sich auch konstante Objekte deklarieren:

const Rational PI(22,7);

Dabei beschränkt der Compiler jedoch gleichzeitig den Zugriff auf die Memberfunktion des Objekts. Ber der zuvor definierten Rational-Klasse ließe sich dann die print()-Funktion für dieses Objekt nicht mehr aufrufen:

PI.print();        // Fehler: Aufruf nicht erlaubt

Sofern wir unsere Klassendefinition nicht ändern, sind die Konstruktoren und die Destruktoren tatsächlich die einzigen Memberfunktionen, die sich für const-Objekte aufrufen lassen.

Um diese Beschränkung zu überwinden, müssen wir diese Memberfunktionen, die wir für konstante Objekte einsetzen wollen, als konstant deklarieren.

Eine konstante Funktion wird dadurch deklariert, dass man das Schlüsselwort const zwischen ihrer Parameterliste und ihrem Rumpf einfügt:

void print() const { cout << num << '/' << den << endl; }

Durch diese Änderung der Funktionsdefinition können Sie die Funktion für konstante Objekte aufrufen:

const Rational PI(22,7);
PI.print();        // Nun O.K.

8.10 Strukturen

Klassen in C++ sind eine Verallgemeinerung der Strukturen ( struct ) von C, bei denen es sich um Klassen handelt, die nur public-Member und keine Funktionen haben.

Üblicherweise betrachtet man eine Klasse als eine Struktur, die durch ihre Memberfunktionen ins Leben gerufen wird und deren private Memberdaten geschützt sind.

Um kompatibel zur älteren Sprache C zu bleiben, behält C++ das Schlüsselwort struct bei, mit dem sich Strukturen definieren lassen.

Eine C++ Struktur ist aber im wesentlichen dasselbe wie eine Klasse
.

Der wichtige Unterschied zwischen einer C++ Struktur und einer C++ Klasse betrifft die den Membern zugewiesenen Standardbezeichner.

Auch wenn dies nicht zu empfehlen ist, können Sie Klassen ohne ausdrückliche Angabe ihres Member-Zugriffsbezeichners definieren.

Zum Beispiel ist:
class Rational {
        int num, den;
};

eine zulässige Definition einer Rational-Klasse. Da der Zugriffsbezeichner für ihre Memberdaten num und den nicht angegeben wurde, werden sie vorgabemässig auf private gesetzt.

Wenn wir eine Struktur anstelle einer Klasse definieren würden,
struct Rational {
        int num, den;
};

dann werden die Memberdaten vorgabemässig auf public gesetzt.

Aber das lässt sich einfach korrigieren, wenn man den Zugriffsbezeichner explizit angibt:

struct Rational {

private:
        int num, den;
};

Der Unterschied zwischen class und struct ist daher wirklich nur kosmetischer Natur.

8.11 Zeiger auf Objekte

In vielen Anwendungen ist es von Vorteil, wenn man Zeiger auf Objekte ( und Strukturen ) einsetzt.

Beispiel 8.12 Zeiger auf Objekte einsetzen

class X {

public:
      int data;
      };

int main()
{
            X* p = new X;

            (*p).data = 22; // äquivalent mit p->data = 22;
            cout << "(*p).data = " << (*p).data << " = " << p->data << endl;

            p->data = 44;
            cout << "p->data = " << (*p).data << " = " << p->data << endl;

getch();
return 0;
}
(*p).data = 22 = 22
p->data = 44 = 44

Da p ein Zeiger auf ein X-Objekt ist, ist p* ein X-Objekt, und (*p).data greift auf seine (public) Memberdaten zu.

Beachten Sie, dass die Klammern im Ausdruck (*p).data erforderlich sind, weil der direkte Memberauswahloperator " . " größeren Vorrang als der Dereferenzoperator " * " hat.

Die beiden Schreibweisen
(*p).data
p->data
haben diesselbe Bedeutung.

Beim Arbeiten mit Zeigerobjekten wird der "Pfeil" "->" bevorzugt, weil er einfacher ist und bereits auf die Bedeutung "das Ding, auf das p zeigt" hinweist.

Es folgt nun ein aussagekräftigeres Beispiel.

Beispiel 8.13 Eine Node-Klasse für verkettete Listen

Im Beispiel wird eine Klasse Node definiert, deren Objekte jeweils ein int-Memberdatum und einen next-Pointer enthalten.

Mit dem Programm kann der Anwender eine verkettete Liste in umgekehrter Reihenfolge anlegen. Dann traversiert das Programm die Liste und gibt alle Datenwerte aus.

class Node {

public:
        Node ( int d, Node* p=0) : data(d), next(p) {}
        int data;
        Node* next;
};

int main()
{
        int n;
        Node* p;
        Node* q = 0;

                do {
                        p = new Node ( n, q );
                        q = p;
                } while ( cin >> n );

        cout << endl;

                for ( ; p->next; p = p->next )
                        cout << p ->data << " -> ";
                cout <<" *\n";

getch();
return 0;
}

Beachten Sie zunächst, dass die Definition der Node-Klasse zwei Referenzen auf die Klasse selbst enthält. Dies ist erlaubt, weil die beiden Referenzen eigentlich Zeiger auf die Klasse sind.

Beachten Sie auch, dass der Konstruktor beide Memberdaten initialisiert.

  1. Die while-Schleife liest wiederholt int-Werte in n ein, bis der Anwender das Dateiendezeichen ( Strg-D ) eingibt.
  2. Innerhalb der Schleife wird ein neuer Knoten erstellt, bei dem der int-Wert in die Memberdaten übernommen wird.
  3. Der neue Knoten wird mit dem vorherigen Knoten ( auf den q zeigt ) verbunden.
  4. die Liste und wird fortgesetzt, bis p->next den Wert NUL annimmt ( dann zeigt p auf den letzten Knoten in der Liste ).
Die in diesem Beispiel erzeugte Liste lässt sich so darstellen:



8.12 Statische Memberdaten

Manchmal gilt ein einzelner Wert eines Memberdatums für alle Klassenmember.

In diesem Fall wäre es ineffizient, denselben Wert für jedes Objekt der Klasse zu speichern. Das lässt sich durch eine statische Deklaration der Memberdaten vermeiden. Dazu fügt man am Anfang der Deklaration das Schlüsselwort static ein.

Die Variable muss dann zudem global definiert werden.

Damit sieht die Syntax so aus:
class X {
public:
      static int n;    // Deklaration von n als static-Memberdatum
};

int X::n = 0;      // Definition von n

Statische Variablen werden automatisch mit 0 initialisiert, so dass eine ausdrückliche Initialisierung in der Definition unnötig ist, sofern keine anderen Anfangswerte festgelegt werden sollen.

Beispiel 8.14 Ein statisches Memberdatum

Die Klasse Widget verwaltet das static-Memberdatum count, das jeweils die aktuelle Anzahl der global existierenden Widget-Objekte enthält.

Dieser Zähler wird jedes Mal inkrementiert, wenn ein Widget ( vom Konstruktor ) erzeugt wird, und jedes Mal dekrementiert, wenn ein Widget ( vom Destruktor ) zerstört wird.

class Widget {

public:
      Widget() { ++count; }
      ~Widget() { --count; }
      static int count;
};

int Widget::count = 0;

int main()
{

      Widget w, x;

      cout << "Jetzt gibt es " << w.count << " widgets.\n";
      {

            Widget w, x, y, z;
            cout << "Jetzt gibt es " << w.count << " widgets.\n";
      }

cout << "Jetzt gibt es " << w.count << " widgets.\n";

      Widget y;
      cout << "Jetzt gibt es " << w.count << " widgets.\n";

getch();
return 0;
}

Jetzt gibt es 2 widgets
Jetzt gibt es 6 widgets
Jetzt gibt es 2 widgets
Jetzt gibt es 3 widgets

Beachten Sie, wie im inneren Block vier Widget-Objekte erzeugt werden.

Diese werden wieder zerstört, wenn die Programmkontrolle diesen Block verlässt, so dass sich die globale Anzahl der Widget-Objekte von 6 auf 2 reduziert.

Ein statisches Memberdatum ist mit einer gewöhnlichen globalen Variablen vergleichbar. Unabhängig von der Anzahl der Klasseninstanzen existiert immer nur eine Kopie der Variablen.

Der Hauptunterschied ist, dass es sich um ein Memberdatum der Klasse handelt, das daher private sein kann.

Beispiel 8.15: Ein private und static deklariertes Memberdatum

class Widget {

public:
      Widget() { ++count; }
      ~Widget() { --count; }
      int numWidgets() { return count; }

private:
      static int count;
};

int Widget::count = 0;

int main()
{

      Widget w, x;

      cout << "Jetzt gibt es " << w.numWidgets() << " widgets.\n";

      {
            Widget w, x, y, z;
            cout << "Jetzt gibt es " << w.numWidgets() << " widgets.\n";
      }

      cout << "Jetzt gibt es " << w.numWidgets() << " widgets.\n";

      Widget y;
      cout << "Jetzt gibt es " << w.numWidgets() << " widgets.\n";

getch();
return 0;
}

Die Arbeitsweise des Programms entspricht Beispiel 8.14. Da die statische Variable count nun aber privat ist, muss die Zugriffsfunktion numWidgets() zum Lesen von count in main() eingesetzt werden.

Die Beziehungen zwischen der Klasse, ihren Membern und ihren Objekten lassen sich so darstellen:



Diese Momentaufnahme zeigt den Zustand während der Ausführung der letzten Programmzeile: Beachten Sie, dass sich diese Memberdaten innerhalb der Klasse selbst befinden und die Klassenobjekte keine Daten besitzen.

8.13 static-Memberfunktionen

Die numWidgets()-Funktion in Beispiel 8.15 erfordert - wie alle gewöhnlichen Memberfunktionen - , dass sie irgendeine Instanz der Klasse gehört.

Aber da sie den Wert des static-Memberdatums count zurückgibt, der unabhängig von den individuellen Objekten selbst ist, spielt es keine Rolle, von welchem Objekt sie aufgerufen wird.

Wir haben den Aufruf jeweils w überlassen, aber wir hätten dazu genausogut x, y oder z einsetzen können, sofern sie existieren.

Darüber hinaus können wir die Funktion überhaupt nicht aufrufen, bevor ein Objekt erzeugt worden ist.

Das ist wirklich eigentümlich. Da die Aktion der Funktion von den aktuellen Funktionsobjekten unabhängig ist, wäre es besser, wenn die Aufrufe ebenfalls unabhängig von ihnen wären.

Das lässt sich einfach dadurch erreichen, dass man die Funktion als static deklariert.

Beispiel 8.16: Eine static-Memberfunktion

Die Klasse Widget verwaltet das static-Memberdatum count, das jeweils die aktuelle Anzahl der global existierenden Widget-Objekte enthält. class Widget {

public:
      Widget() { ++count; }
      ~Widget() { --count; }
      static int num() { return count; }

private:
      static int count;
};

int Widget::count = 0;

int main()
{

      cout << "Jetzt gibt es " << Widget::num() << " widgets.\n";
      Widget w, x;

      cout << "Jetzt gibt es " << Widget::num() << " widgets.\n";

      {
            Widget w, x, y, z;
            cout << "Jetzt gibt es " << Widget::num() << " widgets.\n";
      }

      cout << "Jetzt gibt es " << Widget::num() << " widgets.\n";

      Widget y;
      cout << "Jetzt gibt es " << Widget::num() << " widgets.\n";

getch();
return 0;
}

Jetzt gibt es 0 widgets
Jetzt gibt es 2 widgets
Jetzt gibt es 6 widgets
Jetzt gibt es 2 widgets
Jetzt gibt es 3 widgets

Durch die static-Deklaration der num()-Funktion wird diese von den Klasseninstanzen unabhängig.

Nun lässt sie sich als ein Member der Widget-Klasse einfach mit Hilfe des Zugriffsoperators für den Gültigkeitsbereich "::" einsetzen. Dadurch können Sie die Funktion auch aufrufen, bevor Objekte instanziiert worden sind.

Die vorherige Abbildung, die die Beziehungen zwischen der Klasse, ihren Membern und ihren Objekten dargestellt hat, sieht nun so aus:



Der Unterschied ist, dass die Memberfunktion num() nun keinen "this"-Pointer hat. Als static-Memberfunktion ist sie mit der Klasse selbst und nicht mit deren Instanzen verbunden.

Statische Memberfunktionen können nur auf statische Daten ihrer einen Klasse zugreifen.