C++ Kurs

Überladen spezieller Operatoren

Die Themen:

In diesem Kapitel werden wir uns einige weitere 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 >>

Überladen des Operators >>

Die Operatoren << und >> sind standardmäßig für das bitweise Schieben eines Operanden nach links bzw. rechtszuständig. Sie wurden bisher aber auch schon dazu verwendet, um zum Beispiel Daten an den Ausgabestream cout zu übergeben. 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.

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. von der Tastatur, wie folgt eingelesen werden kann:

cin >> anyObject;

Da ein Operator immer für die Klasse überladen wird, dessen Objekt links vom Operator steht, müssen wir den Operator >> für die Klasse istream überladen, die die Basis für die bekannten Streamklassen iostream, ifstream und istringstream ist. Hierbei ist jedoch ein kleines Problem zu lösen. Da cin ein Objekt der Klasse iostream ist hat es zunächst einmal keinen Zugriff auf die nicht-public Eigenschaften des einzulesenden Objekts (zweiter Operand von >>). Beim Einlesen der Eigenschaften eines Objekts müssen wir dem überladenen Operator aber auch den Zugriff auf die nicht-public Eigenschaften gestattet. Dies wird durch folgende Funktionsdeklaration innerhalb der Klasse des einzulesenden Objekts erreicht:

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

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

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

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

PgmHeaderistream& 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 Standardeingabe zu sondern auch aus einer Datei. Dies ist deshalb möglich, da sowohl der Tastatur-Eingabestream cin als auch der Datei-Eingabestream ifstream Instanzen mit der gleichen Basisklasse istream sind. Und genau für diese Streamklasse wurde der Operator überladen.

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

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 ebenfalls eine entsprechende friend-Funktion wie folgt deklariert:

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

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

PgmHeaderclass 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
}

Somit 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 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 dargestellt, wie dies für einen Objektzeiger auf ein Complex-Objekt aussehen würde.

PgmHeaderclass 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
}

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

cout << pCompl1;

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

Funktionsoperator ( )

Und auch der Funktionsoperator ( ), ja, das ist eine leere Parameterklammer, lässt sich überladen. Objekte welche den Funktionsoperator überladen, werden als Funktionsobjekt oder functor bezeichnet. 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.

PgmHeader// 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 des Funktionsobjekts

Difference diff(50);
short val = 30;
...
// Aufrufe des überladenen () Operators
cout << diff.operator()(val) << endl;
val = 60;
cout << diff(val) << endl;
....
Programmausgabe-20
10

Im Beispiel wird eine Klasse Difference definiert, deren Zweck es ist, die Differenz zu einem bestimmten Wert zu bilden. Wird ein Objekt dieser Klasse definiert, so erhält es bei seiner Definition zunächst den Wert übergeben, von dem die Differenz gebildet werden soll (im Beispiel 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:

Any.operator()(var); bzw.
Any(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 nun z.B. erreicht werden, dass mit Hilfe des Indexoperators auch indiziert auf eine verkettete Liste zugegriffen werden kann. Sehen wir uns dazu zunächst 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 einer solchen verketteten Liste in der Regel dynamisch erzeugt werden, wird für die Kennzeichnung des Listenendes im letzten Objekt in der Liste häufig als Zeiger auf seinen Nachfolger ein nullptr 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 letzten Listenelement enthält dieser Zeiger einen nullptr. 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 werden, wird dessen Zeiger auf den Nachfolger mit dem nullptr belegt. 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 nullptr. Diesem bisherigen letzten Listenelement wird dann der Zeiger auf das neu erstellte Objekt zugewiesen, das damit zur Liste hinzugefügt wurde.

Der Nachteil einer solchen verketteten Liste ist, dass die Liste nur in einer Richtung, vom Anfang zum Ende hin, durchlaufen werden kann. Wenn Sie z.B. gerade das 3. Element in der Liste bearbeiten 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 einem 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 Elemente verarbeitet werden soll. Die Anzahl der möglichen Elemente hängt dann 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 und der Inhalt der Nutzdaten nicht angerührt werden muss.

Ein Beispiel für eine einfach verkettete Liste können Sie sich hier ansehen.

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.

PgmHeaderauto VList::GetElement(int index) const ->decltype(*pData)
{
    // Index-Zeiger auf 1. Element
    const auto *pElement = this;
    // Liste durchlaufen
    auto 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 == nullptr)
                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 des Elements. 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 eines solchen Fehlers 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. Und der Parameter index gibt schließlich noch den Index des gewünschten Elements an.

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

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

Sollen auch const Objekte in einer solchen Liste verarbeitet werden, so muss der Index-Operator durch eine weitere Memberfunktion überladen werden:

auto operator[] (int iIndex) const ->decltype(CDATA);

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.

HinweisWürde die Memberfunktion CreateList(...) anstelle einer Referenz auf die Liste einen Listenzeiger zurückliefern, so wäre der Indexoperator dann wie folgt aufzurufen:
pList->operator[](index);  bzw.
(*pList)[index];

Überladener Indexoperator und das Safe-Array

Mit Hilfe des überladenen Indexoperators können auch sogenannte Safe-Arrays erstellt werden. Wie bekannt, nimmt der Compiler beim Zugriff auf die Elemente in einem Feld keinerlei Überprüfung des Index vor, sodass auch Zugriffe über das Feld hinaus möglich sind. Bei einem Safe-Array werden diese unzulässigen Zugriffe nun abgefangen. Ein Beispiel für ein solches Safe-Array können Sie sich hier ansehen.

Anwender-definierte Literale

Durch Überladen des Operators "" (das sind zwei Anführungszeichen) können zum einen eigene Literal-Typen definiert werden, wie z.B. 01010_B für ein Binär-Literal, oder auch Konvertierung durchgeführt werden, um z.B. 1.5_km in Meter umzurechnen. Anwender-definierte Literale haben immer die Form XXX_SUFFIX, wobei XXX das Literal und _SUFFIX die Kennung des anwender-definierten Literals ist.

Die Definition eines anwender-definiertes Literals kann auf zwei Arten erfolgen: als sogenanntes cooked literal oder als raw literal (uncooked literal). Der Unterschied zwischen einem cooked und eine raw Literal besteht darin, wie das vor dem Suffix stehende Literal interpretiert wird. Bei einem cooked Literal erhält der überladene Operator den bereits interpretierten Wert während bei einem raw Literal das Literal als Zeichenketten übergeben wird. So wird z.B. das Literal 4711 bei einem cooked Literal als Ganzzahlzahl 4711 an den überladenen Operator übergeben und bei einem raw Literal als C-String "4711".

Um ein anwender-definiertes Literal zu definieren ist der Operator "" wie folgt zu überladen:

RVAL operator "" SUFFIX(ARG);  bzw.
RVAL operator "" SUFFIX(const char* ARG1, size_t ARG2); bzw.
RVAL operator "" SUFFIX(const char* ARG1);

Die ersten beiden Deklarationen deklarieren ein cooked literal und die letzte Deklaration ein raw literal.

RVAL gibt den Datentyp des zurückgelieferten Werts an und SUFFIX ist das Suffix, welches das anwender-definierte Literal kennzeichnet, wie z.B. _B für binär oder _km für Kilometer. Der führende Unterstrich (Underscore) des Suffix ist laut C++ Standard nicht vorgeschrieben, sollte jedoch stets angegeben werden, um ein anwender-definiertes Suffix von einem Standard Suffix unterscheiden zu können. Der GNU C++ Compiler verlangt die Angabe des Undescore sogar.

Der Parameter ARG enthält das vor dem Suffix stehenden Literal. Bei einem cooked Literal muss ARG

Bei einem raw Literal ist ARG immer ein const char*, der wie üblich auf einen mit 0 abgeschlossenen C-String zeigt.

Hinweis Die nachfolgenden Ausführungen dienen lediglich dazu, Ihnen den Einsatz eines raw Literals zu demonstrieren. C++ stellt ab der Version C++14 standardmäßig Binär-Literale zur Verfügung.

Sehen wir uns zunächst an, wie mit Hilfe eines raw Literals ein anwender-definiertes Literal zum Rechnen mit binären Zahlen definiert werden kann. D.h. wir wollen nachher z.B. zwei binäre Literale 0101 und 1000 addieren können. Dazu legen wir zuerst das Suffix fest, das binären Zahlen kennzeichnen soll. Im Beispiel soll dies das Suffix _b sein. Laut obiger Ausführung ist nun der Operator "" wie folgt zu überladen:

PgmHeader// Überladener Operator "" _b fuer das Suffix
// zur Kennzeichnung von binären Zahlen

unsigned long long operator "" _b(const char* digits)
{
   .... // hier wird gleich gerechnet
}

Der Operator erhält das Literal als const char* übergeben, dessen Inhalt wir in einen entsprechenden Ganzzahl-Wert umrechnen, im Beispiel in ein  unsigned long long, und dann zurückgeben müssen. Die vollständige Implementierung sieht damit wie folgt aus:

PgmHeader// Überladener Operator "" _b fuer das Suffix
// zur Kennzeichnung von binären Zahlen

unsigned long long operator "" _b(const char* digits)
{
    // Ergebnis initialisieren
    unsigned long long value = 0;
    // char-Feld komplett durchlaufen
    while (*digits != 0)
    {
        // Ergebnis mit 2 multiplizieren
        value <<= 1;
        // Wenn binare '1' im Literal, dann Ergebnis inkrementieren
        if (*digits == '1')
            value++;
        // naechstes Zeichen untersuchen
        digits++;
    }
    // Ergebnis der Konvertierung zurueckliefern
    return value;
}
// main() Funktion
int main()
{
    auto erg = 0101_b + 1000_b;
    cout << erg << endl;
}

Bei einem cooked literal wird das vor dem Suffix stehende Literal als Wert interpretiert und an den überladenen Operator übergeben. Ein solches cooked literal kann z.B. sehr gut dazu eingesetzt werden, um Literale mit verschiedenen Maßeinheiten zu verarbeiten. Nehmen wir einmal an, wir wollen Längenangaben verarbeiten, welche in Meter, Kilometer, nautischen Meilen oder Yards vorliegen können. Die Verarbeitung dieser Längenangaben soll dabei stets in Metern erfolgen. Hierzu sind zunächst die Suffixe für die verschiedenen Längeneinheiten zu definieren, im Beispiel also _m, _km, _nm und _yd. Anschließend sind die entsprechenden Operatorfunktionen zu implementieren, welche die jeweilige Längeneinheit in Meter umrechnet. Das fertige Programm könnte dann wie folgt aussehen:

PgmHeader// Umrechnung km in m
unsigned long long operator "" _km(unsigned long long value)
{
    return value*1000;
}
// Umrechnung m in m
unsigned long long operator "" _m(unsigned long long value)
{
    return value;
}
// Umrechnung nm in m
unsigned long long operator "" _nm(unsigned long long value)
{
    return value*1852;
}
// Umrechnung yd in m
unsigned long long operator "" _yd(unsigned long long value)
{
    return value*0.914;
}
// main() Funktion
int main()
{
    // Verschiedene Laengeneinheiten addieren
    cout << "Länge (m):" << 2_km + 5_nm + 100_m + 400_yd << endl;
}

Ein anderer Einsatz wäre z.B. auch die Umrechnung von Grad in Bogenmaß, sodass die Funktion sin() auch mit Grad-Angaben aufgerufen werden könnte: auto erg = sin(90.0_deg);. Versuchen Sie einmal selbst, die für diese Berechnung notwendige Operator-Funktion zu implementieren.

Überladen von new und delete

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

  1. Überladen der Operatoren für ein einzelnes Datum.
  2. Überladen der Operatoren für 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.

PgmHeadervoid* 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 Beginn 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).

AchtungBeachten Sie bitte, Sie den Wert 0 bzw. NULL zurückgeben müssen und keinen nullptr! Dies ist historisch bedingt.

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 [ ]. Und auch hier erhält new wieder die Anzahl der zu reservierenden Bytes für das Feld und delete einen Zeiger auf den freizugebenden Speicherbereich.

PgmHeadervoid* 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 überladen werden. Auch wenn die Memberfunktion nicht explizit als statisch deklariert wird, wird sie durch den Compiler immer als solche angelegt.

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.

PgmHeader// 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 der new Operator ein char-Feld reserviert hat 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. Dafür 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 übersteigen. 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 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 unter Umständen sehr lohnend sein.

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