C++ Kurs

Überladen spezieller Operatoren

Die Themen:

Einleitung
Überladen des Operators >>
Überladen des Operators <<
Überladen der Operatoren << und >> für Objektzeiger
Funktionsoperator ( )
Überladen des Indexoperators [ ]
Überladen von new und delete
Beispiel und Übung

Einleitung

In dieser Lektion werden wir uns einige Operatoren ansehen, die entweder beim Überladen eine Sonderbehandlung erfordern oder aber die im Zusammenhang mit einer Klasse eine bestimmte Aufgabe durchführen sollten.

Allgemeines zum Überladen der Operatoren << und >>

Diese Operatoren sind standardmäßig für das bitweise Schieben eines Operanden nach links bzw. rechts zuständig. Sie wurden bisher aber auch schon dazu verwendet, um zum Beispiel Daten an den Ausgabestream cout zu senden. Durch Überladen dieser Operatoren kann nun erreicht werden, dass nicht nur die Standard-Datentypen wie z.B. short oder double mit Streams eingelesen bzw. ausgegeben werden können, sondern sogar beliebige Objekte.

Überladen des Operators >>

Beginnen wir mit dem Überladen des Operators >>. Der Operator >> soll nun so überladen werden, dass im Zusammenspiel mit einem Eingabestream ein beliebiges Objekt z.B. wie folgt von der Tastatur eingelesen werden kann:

cin >> anyObject;

Wie Sie bestimmt noch wissen, wird ein Operator immer für die Klasse überladen, deren Objekt links vom Operator steht. In diesem Fall müssen wir also den Operator >> für die Klasse istream überladen. Hierbei ist ein kleines Problem zu lösen. Da cin ein Objekt der Klasse istream ist hat es zunächst einmal keinen Zugriff auf die nicht-public Eigenschaften des einzulesenden Objekts (zweiter Operand von >>). Da beim Einlesen aber die Eigenschaften des Objekts natürlich verändert werden, müssen wir dem überladenen Operator den Zugriff auf nicht-public Daten gestattet. Dies wird durch folgende Funktionsdeklaration innerhalb der einzulesenden Klasse erreicht:

friend istream& operator >> (istream& is, Any& anyObject);

Wir benötigen hier also auch eine friend-Funktion um diesen Operator zu überladen.

Nachfolgend ist wieder ein Auszug aus der bekannten Klasse Complex dargestellt. Um deren Daten mithilfe des cin-Streams einlesen zu können, wird zuerst die entsprechende friend-Funktion innerhalb der Klasse Complex deklariert.


class Complex
{
    double real;
    double imag;
  public:
    ....
    friend istream& operator >> (istream& is, Complex& op);
};

Nach dem die Funktion deklariert ist, muss sie noch definiert werden. Die Definition gleicht bis auf das Schlüsselwort friend der Funktionsdeklaration innerhalb der Klasse. Die Funktion erhält im ersten Parameter eine Referenz auf den Eingabestream und im zweiten Parameter eine Referenz auf das einzulesende Objekt. 


istream& operator >> (istream& is, Complex& op)
{
    is >> op.real; // Real- und Imaginär-
    is >> op.omag; // anteil einlesen
    return is;     // Referenz auf Stream zurückliefern
}

Beachten Sie, dass die Funktion vollen Zugriff auf alle Member des Objekts besitzt! Da die Funktion eine Referenz auf das Streamobjekt zurückliefert, können auch mehrere Objekte oder Daten innerhalb einer Eingabeweisung eingelesen werden:

cin >> comp1 >> var >> comp2;

Der auf diese Weise überladene Operator >> lässt aber nicht nur das Einlesen der Eigenschaften von Objekten von der Tastatur zu, sondern auch das Einlesen aus einer Datei. Das Einzige was Sie hierfür tun müssen,  ist einen Datei-Eingabestream (z.B. ifstream) mit einer Datei zu verbinden und den Stream cin aus dem vorherigen Beispiel durch den Dateistream zu ersetzen. Dies ist deshalb möglich, da sowohl der Tastatur-Eingabestream cin wie auch der Datei-Eingabestream ifstream Instanzen mit der gleichen Basisklasse istream sind. Und genau für diese Streamklasse wurde der Operator überladen.


// Klassen- und Funktionsdefinition wie oben angegeben
...
int main()
{
    // Eingabestream mit Datei verbinden
    ifstream inFile;
    inFile.open("MyFile.dat");
    // Objekt der Klasse Complex definieren
    Complex myComp;
    // Daten für Objekt aus Datei einlesen
    inFile >> myComp;
    ....
}

Überladen des Operators <<

Und genauso wie sich der Operator >> für die Eingabe überladen lässt, lässt sich auch der Operator << für die Ausgabe überladen. Dazu wird die entsprechende friend-Funktion wie folgt deklariert:

friend ostream& operator << (ostream& os, Any& anyObject);

Für die Klasse Complex ergibt sich die nachfolgend dargestellte Klassendefinition und Definition der friend-Funktion.


class Complex
{
    double real;
    double imag;
  public:
    ....
    friend ostream& operator << (ostream& os, Complex& op);
};
// Funktionsdefinition
ostream& operator << (ostream& os, Complex& op)
{
    os << op.real; // Real- und Imaginär-
    os << op.imag; // anteil ausgeben
    return os;     // Ref. auf Stream
}

Damit ist z.B. folgende Anweisung nun erlaubt:

cout << comp1;

Für die Ausgabe von Daten in eine Datei gilt das Gleiche wie beim Einlesen aus einer Datei, lediglich der linke Operand muss nun den Typ ofstream besitzen anstelle von ifstream.

Noch ein Hinweis: Wie Sie vielleicht vermuten, ist die Funktionalität der in dieser Lektion behandelten Operatoren << und >> nicht fest vorgegeben. Sie könnten also durch Überladen dieser Operatoren auch ganz andere Dinge durchführen. Bedenken Sie dabei aber, dass der 'normale' Anwender in der Regel diese Operatoren mit einer Ein-/Ausgabe oder Schiebeoperation verbindet.

Überladen der Operatoren << und >> für Objektzeiger

In den bisherigen Beispielen wurde stets das Objekt selbst eingelesen bzw. ausgegeben. Sollen Objektzeiger für die Ein-/Ausgabe verwendet werden, so müssen Sie entweder den Zeiger vorher dereferenzieren oder aber eine weitere Operatorfunktion zur Verfügung stellen. Im Beispiel ist einmal beispielhaft dargestellt, wie dies für einen Objektzeiger auf ein Complex-Objekt aussehen würde.


class Complex
{
    double real;
    double imag;
  public:
    ....
    friend ostream& operator << (ostream& os, Complex *pObj);
};
// Funktionsdefinition
ostream& operator << (ostream& os, Complex *pObj)
{
    os << pObj->real; // Real- und Imaginär-
    os << pObj->imag; // anteil ausgeben
    return os;        // Ref. auf Stream
}

Mithilfe der dieser Funktion können dann die Eigenschaften des Objekts ausgegeben, auf das der Zeiger pComp1 verweist:

cout << pCompl1;

Ohne diese überladenen Operator würde ansonsten der Inhalt des Zeigers ausgegeben werden.

Funktionsoperator ( )

Und auch der Funktionsoperator ( ) (ja, das sind leere Parameterklammern) lässt sich überladen. Objekte welche den Funktionsoperator überladen, werden als Funktionsobjekte bezeichnet (auf neudeutsch function object oder functor). Hat eine Klasse den Funktionsoperator überladen, so kann ein Objekt dieser Klasse wie eine Funktion verwendet werden. Funktionsobjekte sind quasi das C++ Gegenstück zu Funktionszeiger.


// Klasse mit überladenem Funktionsoperator
class Difference
{
    short value;    // Eigenschaft
public:
    // ctor
    Difference(short val): value(val)
    { }
    // Überladener Funktionsoperator
    short operator() (short val)
    {
        return val-value;
    }
};
// weiter unten im Programm
// Definition der Funktionsobjekts
Difference diff(50);
short val = 30;
...
// Aufrufe des überladenen () Operators
cout << diff.operator()(val) << endl;
val = 60;
cout << diff(val) << endl;
....

-20
10

Im Beispiel wird die Klasse Difference definiert deren Zweck es ist, die Differenz zu einem bestimmten Wert zu bilden. Dazu erhält der Konstruktor der Klasse den Wert übergeben, zu dem später Differenzen zu bilden sind. Die Differenzbildung selbst erfolgt dann in der Memberfunktion des überladenen Funktionsoperators.

Wird im Programm dann Objekt dieser Klasse definiert, so erhält es bei seiner Definition zunächst den Wert übergeben, von dem später die Differenz gebildet werden soll (im Beispiel also 50). Um nun die Differenz zu diesem Wert zu bilden, wird der Funktionsoperator aufgerufen, der als Parameter den Wert für die Differenzbildung erhält. Der Aufruf des überladenen Funktionsoperators kann hierbei, wie bei überladenen Operatoren üblich, auf zweierlei Arten erfolgen:

Cany.operator()(var); bzw.
Cany(var);

Im letzten Aufruf wird also das Objekt wie eine Funktion angewandt. Solche Funktionsobjekte spielen in der C++ Bibliothek Standard Template Library (STL) eine wichtige Rolle.

Überladen des Indexoperators [ ]

Der Indexoperator [] wird standardmäßig dazu verwendet, um indiziert auf Feldelemente zuzugreifen. Durch Überladen dieses Operators kann z.B. erreicht werden, dass mithilfe des Indexoperators auch indiziert auf eine verkettete Liste zugegriffen werden kann. Aber sehen wir uns erst einmal die Funktionsweise einer verketteten Liste an.

Die verkettete Liste

Eine verkettete Liste ist eine Aneinanderreihung von mehreren Objekten, wobei jedes Objekt mindestens einen Zeiger auf ein anderes Objekt in der Liste besitzt. Im einfachsten Fall besitzt ein Objekt nur einen Zeiger auf seinen Nachfolger in der Liste. Da die Elemente innerhalb einer verketteten Liste in der Regel dynamisch erzeugt werden, wird für die Kennzeichnung des Listenendes häufig als Zeiger auf den Nachfolger der Wert NULL eingetragen.

Das nachfolgende Bild zeigt den prinzipiellen Aufbau einer einfach verketteten Liste.

Der Zeiger pTNext verweist hier auf das nachfolgende Element in der Liste und im letzte Listenelement enthält dieser Zeiger den Wert NULL. Welche Nutzdaten innerhalb der Liste abgelegt sind, spielt für die Arbeitsweise der Liste zunächst keine Rolle.

Wird ein neues Element zu einer bestehenden Liste hinzugefügt, so wird zunächst dieses Element erstellt. Soll das neue Element ans Ende der Liste angefügt wird, wird dessen Zeiger auf den Nachfolger auf NULL gesetzt. Danach wird die komplette Liste so lange durchlaufen, bis das bisherige letzte Listenelement gefunden ist. Dieses hat ja immer noch als Zeiger auf seinen Nachfolger den Wert NULL. In diesem bisherigen letzten Listenelement wird dann der Zeiger auf das neu erstellte Objekt eingetragen, das damit dann zur Liste hinzugefügt wurde.

Der Nachteil einer solchen verketteten Liste ist, dass die Liste nur in einer Richtung, vom Anfang zum Ende, durchlaufen werden kann. Wenn Sie z.B. auf dem 3. Element in der Liste stehen und danach das 2. Element verarbeiten wollen, so müssen Sie die Liste erneut von vorne durchlaufen. Dieser Nachteil lässt sich beheben, indem eine doppelt verkettete Liste eingesetzt wird. Hierbei hat jedes Element außer einen Zeiger auf seinen Nachfolger auch noch einen Zeiger auf seinen Vorgänger.

Solche verketteten Listen werden häufig dann eingesetzt, wenn eine nicht von Anfang an definierte Anzahl von Elementen verarbeitet werden soll. Die Anzahl der möglichen Elemente hängt nur noch vom verfügbaren Speicher ab.

Das Entfernen von Elementen, und ebenso das Sortieren, kann bei einer solchen Liste relativ schnell erfolgen, da hierzu 'nur' die Zeiger auf die Nachfolger (bei doppelt verketteten Listen zusätzlich auf den Vorgänger) umgesetzt werden müssen.

Ein Beispiel für eine einfach verkettete Liste können Sie sich hier ansehen. Sehen Sie sich im Beispiel einmal an, wie die Liste erzeugt und entfernt wird. Um die verkettete Liste zu erstellen wird die statische Memberfunktion CreateList(...) aufgerufen. In dieser Memberfunktion wird dann mittels new das erste Listenelement erstellt, das aber noch kein Nutzdatum besitzt. Beachten Sie im Beispiel, dass CreateList(...) eine Referenz auf das erste Listenelement zurückliefert und dieser Rückgabewert auch in einer Referenzvariablen abgelegt werden muss.

Damit eine solche verkettete Liste nicht direkt erstellt werden kann sondern nur über die Memberfunktion CreateList(...), wurde der Konstruktor als private deklariert.

Um Nutzdaten zur Liste hinzuzufügen, wird die Memberfunktion Add2List(...) der Liste aufgerufen. Diese erstellt dann ein weiteres Element der verketteten Liste und fügt zu diesem das Nutzdatum hinzu. 

Das Entfernen der Liste am Ende des Programms erfolgt ebenfalls über eine statische Memberfunktion, um so eine Rekursion innerhalb des Destruktors zu vermeiden. Im ersten Ansatz kommt man unweigerlich in Versuchung, im Destruktor des ersten Listenelements ein delete auf den Nachfolger durchzuführen. Dieses würde dann aber seinerseits wieder zum Aufruf des Destruktors führen, der dann wiederum seinen Nachfolger löschen würde usw. D.h. Sie hätten hier innerhalb des Destruktors eine nicht gleich erkennbare Rekursion. Beachten Sie, dass beim Löschen eines Listenelements ebenfalls das dazugehörige Nutzdatum gelöscht wird.

So viel zur Einführung der verketteten Liste. Die Standard Template Library (STL) enthält übrigens bereits eine fertige Klasse für eine solche verkettete Liste. Aber dazu kommen wir später noch.

Zugriff auf Elemente der verketteten Liste

Das nachfolgende Beispiel zeigt den Zugriff auf ein beliebiges Element innerhalb einer solchen verketteten Liste ohne Überladen des Operators [ ] und ist eine Erweiterung des vorherigen Beispiels.


CData& VList::GetElement(int index) const
{
    // Index-Zeiger auf 1. Element
    const VList *pElement = this;
    // Liste durchlaufen
    bool found = false;
    while (!found)
    {
        // Falls Index 0 ist, Element gefunden
        if (index <= 0)
            found = true;
        else
        {
            // Falls Listenende erreicht, letztes Element als
            // gesuchtes Element betrachten
            if (pElement->pNext == NULL)
                found = true;
            else
            {
                // nächstes Element holen
                pElement = pElement->pNext;
                // Index dekrementieren
                index--;
            }
        }
    }
    // Referenz auf Element zurückgeben
    return *(pElement->pData);
}

Als Parameter erhält die Memberfunktion GetElement(...) den Index des gesuchten Elements übergeben und liefert als Ergebnis eine Referenz auf das Nutzdatum. Liegt der Index außerhalb des erlaubten Bereichs, so wird entweder das Nutzdatum des ersten Elements (Index ist kleiner 0) oder das des letzten Elements (Index größer als Anzahl der Elemente) zurückgegeben. Beim Auftreten einer dieser Fehler würde in einer realen Anwendung eine Ausnahme (Exception) ausgelöst werden. Was es mit diesen Ausnahmen auf sich hat erfahren Sie später noch.

Wenn Sie wollen, können Sie die angegebene Memberfunktion in das Beispiel übernehmen und vor dem Löschen der Liste einmal versuchen, ein bestimmtes Element auszugeben.

myList.GetElement(3).PrintIt();
myList.GetElement(8).PrintIt();

Überladen des Indexoperators [ ]

Sehen wir uns jetzt an, wie zum Zugriff auf ein gewünschtes Element in der verketteten Liste der Indexoperator eingesetzt werden kann. Um den Indexoperator zu überladen muss eine nicht-statische Memberfunktion verwendet werden. Diese Memberfunktion besitzt folgende Deklaration:

RVAL CAny::operator [] (int index);

RVAL ist der Returntyp der Memberfunktion und CAny die Klasse, für die der Operator überladen werden soll. Der überladene Indexoperator sollte in der Regel immer eine Referenz auf das gewünschte Element zurückliefern und nicht das Element selbst. Nur so ist gewährleistet, dass der indizierte Zugriff auf das Element sowohl rechts wie auch links vom Zuweisungsoperator stehen kann. Der Parameter index gibt den Index des gewünschten Elements an.

Für das vorherige Beispiel ergibt sich damit die folgende Operator-Memberfunktion.


// Definition der Klasse für die verkettete Liste
class VList
{
    CData *pData;                          // Zeiger auf Nutzdatum 
    ....
public:
    ....
    CData& VList::operator[](int index);   // Überladener Operator []

};
// Definition des überladenen Operators []
CData& VList::operator[](int index)
{
    // Index-Zeiger auf 1. Element
    const VList *pElement = this;
    // Liste durchlaufen
    bool found = false;
    while (!found)
    {
        // Falls Index 0 ist, Element gefunden
        if (index <= 0)
            found = true;
        else
        {
            // Falls Listenende erreicht, letztes Element als
            // gesuchtes Element betrachten
            if (pElement->pNext == NULL)
                found = true;
            else
            {
                // nächstes Element holen
                pElement = pElement->pNext;
                // Index dekrementieren
                index--;
            }
        }
    }
    // Referenz auf Element zurückgeben
    return *(pElement->pData);
}

Wollen Sie auch const Objekte in einer solchen Liste verarbeiten, so müssen Sie den Index-Operator durch eine weitere Memberfunktion überladen:

CData operator[] (int iIndex) const;

Indizierte const-Objekte können selbstverständlich nur rechts vom Zuweisungsoperator stehen. Beachten Sie auch, dass nun das Objekt selbst und keine Referenz mehr zurückgeliefert wird.

Würde die Memberfunktion CreateList(...) anstelle einer Referenz auf die Liste einen Listenzeiger zurückliefern, so müsste der Indexoperator dann wie folgt aufgerufen werden:

pList->operator[](index);  bzw.
(*pList)[index];

Überladener Indexoperator und das Safe-Array

Mithilfe des überladenen Indexoperators können auch so genannte Safe-Arrays erstellt werden. Wie Sie bestimmt noch wissen, nimmt der Compiler beim Zugriff auf Elemente in einem Feld keinerlei Überprüfung der zulässigen Bereichsgrenzen vor, so dass auch Zugriffe über das Feld hinaus möglich sind. Bei einem Safe-Array können diese unzulässigen Zugriffe abgefangen werden. Ein Beispiel für eine solches Safe-Array können Sie sich hier ansehen.

Überladen von new und delete

Beim Überladen der Operatoren new und delete müssen vier Fälle unterschieden werden:

  1. Überladen der  Operatoren zur Reservierung eines einzelnen Datum.
  2. Überladen der Operatoren zur Reservierung von Felder.
  3. Überladen der Operatoren für ein einzelnes Objekt.
  4. Überladen der Operatoren für Objektfelder.

Der erste und zweite Fall wird in der Praxis selten eingesetzt, da hierbei etliche Probleme auftreten können. Sehen wir uns deshalb diese Fälle nur in der Theorie an.

Überladen von new und delete für einzelne Daten

Soll der new und delete Operator für einzelne Daten überladen werden, so werden die beiden unten dargestellten Funktionen hierzu verwendet.


void* new (size_t size)
{
    void *pMem;
    pMem = .....   // hier Speicher reservieren
    return pMem;
}
void delete (void *pMem)
{
    ....           // hier Speicher freigeben
}

Der überladene new Operator erhält im Parameter size die Anzahl der zu reservierenden Bytes. Der Datentyp size_t ist ein vom Compiler vorgegebener Datentyp zur Speicherreservierung und in der Regel über ein typedef entweder ein unsigned int oder unsigned long. Konnte entsprechend Speicher reserviert werden, so muss der Operator einen Zeiger auf den Anfang des reservierten Speicherbereichs zurückliefern. War nicht genügend Speicher vorhanden, muss NULL zurückgegeben werden. Das Problem beim Überladen des new Operators besteht darin, den erforderlichen Speicher zu reservieren. Sie dürfen dazu innerhalb der Operator-Memberfunktion auf keinen Fall selbst wieder new verwenden, da Sie sonst die Funktion erneut aufrufen würden (Endlos-Rekursion).

Der überladene delete Operator erhält als Parameter einen Zeiger auf den freizugebenden Speicherbereich.

Überladen new und delete für Felder

Soll der new und delete Operator für die Reservierung von Speicher für Felder überladen werden, müssen etwas abgewandelte Funktionen definiert werden. Nach dem Operatornamen folgt in beiden Fällen der leere Indexoperator [ ]. Auch hier erhält new wieder die Anzahl der zu reservierenden Bytes für das gesamte Feld und delete einen Zeiger auf den freizugebenden Speicherbereich.


void* new [](size_t size)
{
    void *pMem;
    pMem = .....   // hier Speicher reservieren
    return pMem;
}
void delete [](void *pMem)
{
    ....           // hier Speicher freigeben
}

Überladen von new und delete für ein einzelnes Objekt

Die Operatoren new und delete können für eine Klasse nur durch eine statische Memberfunktion der Klasse überladen werden. Auch wenn die Memberfunktion nicht explizit als statisch deklariert wird, wird sie durch den Compiler immer als solche angelegt. Wenn Sie sich nicht mehr sicher sind was statische Memberfunktionen sind, dann schauen Sie hier nach.

Beginnen wir mit dem Überladen des new und delete Operators für einzelne Objekte. Hierzu sind folgende Memberfunktionen zur Klasse hinzuzufügen:

static void operator new (size_t size);     bzw.
static void operator delete (void *pMem);

Der Operator new erhält die Anzahl der für das Objekt zu reservierenden Bytes als Parameter übergeben. Innerhalb der Operator-Memberfunktion muss dann der erforderliche Speicher reserviert werden, was im nachfolgenden Beispiel mit dem globalen new Operator erfolgt. Als Ergebnis liefert die Memberfunktion den Zeiger auf den reservierten Speicher zurück.


// Klassendefinition
class Complex
{
    ....
public:
    static void* operator new (size_t size);
    static void operator delete(void *pMem);
    ....
};
// Überladener new Operator
void* Complex::operator new (size_t size)
{
    // Speicher reservieren
    void *pMem = new char[size];
    // Speicher mit 0 initialisieren
    memset(pMem,0,size);
    // Zeiger auf Speicher zurückgeben
    return pMem;
}
// Überladener delete Operator
void Complex::operator delete(void *pMem)
{
    // Speicher freigeben
    delete [] pMem;
}

Im Beispiel wird bei erfolgreicher Speicherreservierung zusätzlich der komplette Speicherbereich mit 0 initialisiert.

Der überladene delete Operator erhält als Parameter einen void-Zeiger auf den freizugebenden Speicher. Die Freigabe des Speichers erfolgt in der Regel wiederum mit dem globalen delete Operator. Beachten Sie dabei, dass im new Operator ein char-Feld reserviert wurde und deshalb beim Aufruf des globalen delete Operators die eckigen Klammern mit angegeben werden müssen!

Überladen von new und delete für Objektfelder

Soll der new und delete Operator für Objektfelder überladen werden, so sind hierfür folgende Memberfunktionen einzusetzen:

static void operator new [] (size_t size);    bzw.
static void operator delete [](void *pMem);

Diese Memberfunktionen unterscheiden sich nur durch die Angabe des Indexoperators nach dem Operatornamen von den vorherigen Memberfunktionen für einzelne Objekte. Der new Operator erhält im Parameter wiederum die Anzahl der zu reservierenden Bytes für das gesamte Objektfeld.

Vielleicht fragen Sie sich jetzt, wozu das Überladen von new und delete gut sein soll? Die Antwort darauf ist: aus Geschwindigkeits- und Speicherplatzgründen. Die Standard-Operatoren new und delete sind so ausgelegt, dass Sie für alle erdenklichen Fälle die Speicherverwaltung übernehmen können. Dazu muss aber zwischen new und delete eine gewisse 'Kommunikation' stattfinden. delete benötigt zur Freigabe des Speichers verschiedene Informationen, so z.B. die Größe des freizugebenden Speichers. Dazu reserviert new etwas mehr Speicherplatz als für das eigentliche Datum benötigt wird. In diesem zusätzlichen Speicherplatz wird dann u.a. die Speichergröße abgelegt. Bei sehr kleinen Daten oder Objekten kann nun die Größe des zusätzlich benötigten Verwaltungsspeichers die des Nutzdatenspeichers übertreffen. Für solche Fälle bietet es sich an, den new und delete Operator zu überschreiben. Im überschriebenen new Operator wird dann z.B. Speicher für mehrere Objekte auf einmal reserviert. Dieser Speicher wird intern verwaltet und bei jedem Aufruf des new Operators ein 'Speicherstück' daraus an die Anwendung zurückgegeben. Der delete Operator stellt dann den freizugebenden Speicher wieder in diesen großen Speicher zurück. Zugegeben, solche eine eigene Speicherverwaltung ist ein nicht ganz triviales Unterfangen, aber kann auch sehr lohnend sein.

Beispiel und Übung

Beispiel:

Als Ausgangspunkt für dieses Beispiel dient die in der vorherigen Lektion vorgestellte Klasse Rect zur Abspeicherung und Manipulation von Rechteckdaten.

Zur Ausgabe der Rechteckdaten wird nun aber nicht mehr die Memberfunktion PrintRect(...) verwendet, sondern der überladene Operator <<. Da in main() die Ausgabe der Rechteckdaten sowohl über Objektzeiger wie auch direkt erfolgt, werden zwei überladene Funktionen verwendet.

1. Rechteck:
Rechteck auf (10,20) Grösse: (100,200)
2. Rechteck:
Rechteck auf (50,50) Grösse: (100,200)
Rechtecke sind unterschiedlich
1. Rechteck um eins verschoben:
Rechteck auf (11,21) Grösse: (100,200)
Rechtecke überschneiden sich! Gemeinsame Fläche:
Rechteck auf (50,50) Grösse: (61,171)


// Beispiel zu überladenen Operatoren

// Zuerst Dateien einbinden
#include <iostream>

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

// Klassendefinition
class Rect
{
    short   xPos, yPos;             // Position
    short   width, height;          // Breite und Höhe
  public:
    Rect();                         // ctors
    Rect(short x, short y, short w, short h);
    Rect(const Rect& source);
    Rect operator ++(int);          // Verschiebt Rechteck um eine X/Y-Position
    Rect operator & (const Rect& op2) const;  // Verundet zwei Rechtecke
    bool operator ==(const Rect& op2) const;  // Vergleicht zwei Rechtecke
    bool operator !() const;        // Prüft auf leeres Rechteck ab
    // Ausgabeoperatoren
    friend ostream& operator << (ostream& os, const Rect& op2);
    friend ostream& operator << (ostream& os, const Rect *pOp2);
};
// Definition der Memberfunktionen
// 1. Konstruktor (Standard Konstruktor)
Rect::Rect()
{
    xPos = yPos = width = height = 0;
}
// 2. Konstruktor
// Erhält als Parameter die Rechteck-Daten als 4 short Werte
Rect::Rect(short x, short y, short w, short h)
{
    xPos = x; yPos = y;
    width = w; height = h;
}
// 3. Konstruktor
// copy-ctor
Rect::Rect(const Rect& source)
{
    xPos = source.xPos; yPos = source.yPos;
    width = source.width; height = source.height;
}
// Verschiebt Rechteck um eine X/Y-Position
// Achtung Postfixoperator!
// Darf keine const-Memberfunktion sein da das Objekt
// ja verändert wird!
Rect Rect::operator ++(int)
{
    // Hilfsobjekt zur Aufnahme der akt. Werte
    // Ruft copy-ctor auf
    Rect orig(*this);
    // Nun erst Position verändern
    xPos++;
    yPos++;
    // Ursprungswerte zurückgeben
    return orig;
}
// Verundet zwei Rechtecke
// Das daraus resultierende Rechtecke enthält die Fläche,
// die beiden Rechtecken gemeinsam ist
Rect Rect::operator & (const Rect& op2) const
{
    // Resultierendes Rechteck
    Rect result;
    // Hilfsvariablen
    short end1, end2;
    // Grösste X-Position ist resultierende X-Position
    result.xPos = (op2.xPos>xPos) ? op2.xPos : xPos;
    // X-Positionen der rechten Kante berechnen
    end1 = xPos+width;
    end2 = op2.xPos+op2.width;
    // Kleinste X-Position bestimmte Breite des result. Rechtecks
    result.width = (end1>end2) ? end2-result.xPos : end1-result.xPos;
    // Nun das gleiche Spiel mit der Y-Position und der Höhe
    result.yPos = (op2.yPos>yPos) ? op2.yPos : yPos;
    end1 = yPos+height;
    end2 = op2.yPos+op2.height;
    result.height = (end1>end2) ? end2-result.yPos : end1-result.yPos;
    // Falls keine gemeinsame Fläche vorhanden ist
    // leeres Rechteck setzen
    if ((result.width <= 0) || (result.height <= 0))
    {
        result.width = result.height = 0;
        result.xPos = result.yPos = 0;
    }
    return result;
}
// Vergleicht zwei Rechecke
bool Rect::operator ==(const Rect& op2) const
{
    // Rechtecke sind gleich wenn Koordinaten und Ausdehnung gleich sind
    if ((xPos == op2.xPos) && (width == op2.width) &&
        (yPos == op2.yPos) && (height == op2.height))
        return true;
    else
        return false;
}
// Prüft ab, ob das Rechteck eine Fläche besitzt
bool Rect::operator ! () const
{
    if ((width == 0) || (height == 0))
        return true;
    else
        return false;
}
// Überladene Operatorfunktion zur Ausgabe der Rechteckdaten
// Diese Funktion wird aufgerufen, wenn ein Objekt ausgegeben wird
ostream& operator << (ostream& os, const Rect& op2)
{
    os << "Rechteck auf (" << op2.xPos << "," << op2.yPos << ") ";
    os << "Grösse: (" << op2.width << "," << op2.height << ")\n";
    return os;
}
// Diese Funktion wird aufgerufen, wenn ein Objekt über einen
// Objektzeiger ausgegeben wird
ostream& operator << (ostream& os, const Rect *pOp2)
{
    os << "Rechteck auf (" << pOp2->xPos << "," << pOp2->yPos << ") ";
    os << "Grösse: (" << pOp2->width << "," << pOp2->height << ")\n";
    return os;
}


// main () Funktion
int main()
{
    // Zwei Rect Objekte erstellen
    Rect *pFirstRect = new Rect(10,20,100,200);
    Rect *pSecondRect = new Rect(50,50,100,200);

    // Rechteckdaten ausgeben
    cout << "1. Rechteck:\n";
    cout << pFirstRect;
    cout << "2. Rechteck:\n";
    cout << pSecondRect;

    // Rechtecke vergleichen
    if (*pFirstRect == *pSecondRect)
        cout << "Rechtecke sind gleich\n";
    else
        cout << "Rechtecke sind unterschiedlich\n";

    // Erstes Objekt verschieben
    // Klammerung unbedingt beachten!
    (*pFirstRect)++;
    cout << "1. Rechteck um eins verschoben:\n";
    cout << pFirstRect;

    // Neues Rechteck aus der gemeinsamen Fläche der beiden
    // Rechtecke bilden
    Rect mergedRect = *pFirstRect & *pSecondRect;
    // Abprüfen, ob beide Rechtecke eine gemeinsame Fläche hatten
    if (!mergedRect)
        cout << "Keine Überschneidung der beiden Rechtecke!\n";
    else
    {
        cout << "Rechtecke überschneiden sich! Gemeinsame Fläche:\n";
        cout << mergedRect;
    }

    delete pFirstRect;
    delete pSecondRect;
}

Übung:

Erweitern Sie die in vorherigen Übungen erstellte Klasse CString um die Operatoren << und >> zur Ausgabe bzw. zum Einlesen eines CString-Objekts. Da nachher nur Objekte ausgegeben bzw. eingelesen werden sollen, müssen Sie die Operatoren für Objektfelder nicht unbedingt definieren. Beachten Sie beim Einlesen eines CString-Objekts, dass Sie im Voraus nicht wissen wie lange die Eingabe ist.

Da für die Ausgabe der überladene Operator << verwendet wird, können Sie die Memberfunktion Print(...) aus der Klasse CString entfernen.

Erstellen Sie in main() zwei CString-Objekte, denen Sie bei ihrer Definition einen beliebigen Text zuweisen.

Geben Sie beide CString-Objekte mithilfe des überladenen Operators << aus.

Anschließend lesen Sie von der Tastatur mittels des Eingabestreams cin einen neuen Text für das erste CString-Objekt ein (Operator >>) und geben den eingelesenen String zur Kontrolle gleich wieder aus.

Zusatz:

Falls Sie die Ausführungen zum Überladen des Indexoperators gelesen haben, können Sie sich noch an Folgendem versuchen:

Fügen zur Klasse noch den überladenen Indexoperator [ ] hinzu, um einzelne Zeichen innerhalb des Strings manipulieren zu können. Folgende Anweisungen sollen mit dem Indexoperator erlaubt sein:

char cZeichen = CStringObj[2];
CStringObj[3] = cZeichen;

Im ersten Fall wird das dritte(!) Zeichen des Strings zurückgegeben und im zweiten Fall das vierte(!) Zeichen neu gesetzt.

Wandeln Sie dann den eingelesenen Text in Großbuchstaben um, wobei Sie auf die einzelnen Stringzeichen über den Operator [ ] zugreifen. Zur Umwandlung von Kleinbuchstaben in Großbuchstaben kann die Bibliotheksfunktion toupper(...) verwendet werden. Die Funktion erhält als Parameter das zu konvertierende Zeichen als int-Wert und liefert das konvertierte Zeichen ebenfalls als int-Wert zurück. Geben Sie dann den umgewandelten Text wieder aus.

Das Problem hierbei besteht darin, dass Sie das Stringende irgend wie erkennen müssen. Sie könnten hierfür eine weitere Memberfunktion der Klasse CString hinzufügen, die die Länge des Strings zurückliefert. Versuchen aber einmal ohne zusätzliche Memberfunktion auszukommen. Die Lösung dieses Problems liegt im überladenen Indexoperator.

Ausgangs-Strings
1.String: Es waren einmal zwei Ameisen
2.String: die wollten nach Amerika reisen

Bitte neuen Text für erstes String Objekt eingeben:
Das ist der neue Text
Die Eingabe war:
1.String: Das ist der neue Text
Alles in Grossbuchstaben:
1.String: DAS IST DER NEUE TEXT

Lösung ansehen!