C++ Tutorial

Überladen spezieller Operatoren

In diesem Kapitel sehen wir uns einige Operatoren an, die entweder beim Überladen eine Sonderbehandlung erfordern oder die im Zusammenhang mit Objekten eine bestimmte Aufgabe durchführen sollten.

Überladen der Operatoren ++ und - -

Ein Sonderfall beim Überladen von unären Operatoren stellen die Operatoren ++ und -- dar, da sie sowohl als Präfix (++X) und als Suffix (X++) auftreten können.

Um den Präfix-Operator ++ zu überladen, wird die Methode

CAny& CAny::operator ++();

eingesetzt. Die Methode liefert eine Referenz auf das aktuelle Objekt zurück, damit z.B. folgende Operation möglich ist:

comp2 = ++comp1;
1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5: public:
6:    ...
7:    Complex& operator ++();
8: };
9: // Überladener Präfix-Operator ++
10: Complex& Complex::operator++ ()
11: {
12:    ++real;
13:    ++imag;
14:    return *this;
15: }

Beachten Sie beim Präfix-Operator, dass zuerst die Addition ausgeführt und das Ergebnis der Addition zurückgeliefert wird.

Um den Suffix-Operator zu überladen, erhält die Methode einen Dummy-Parameter vom Typ int.

CAny CAny::operator ++(int);

Hier liefert der überladene Operator ein Complex-Objekt zurück und keine Referenz.

1: class Complex
2: {
3:    double real; // Realanteil
4:    double imag; // Imaginäranteil
5: public:
6:    ...
7:    Complex operator ++(int);
8: };
9: // Überladener Suffix-Operator ++
10: Complex Complex::operator++ (int)
11: {
12:    Complex temp(*this); //copy-ctor
13:    ++real;
14:    ++imag;
15:    return temp;
16: }

Beim Überladen des Suffix-Operators ist zu beachten, dass der Ursprungswert des Objekts zurückzugeben ist, da der Suffix-Operator die Addition erst nach der Auswertung des Objekts durchführt.

Überladen des cast-Operators

Durch die Definition eines cast-Operators wird festgelegt, wie ein Objekt in einen anderen Datentyp konvertiert wird. Die Syntax für eine solche Typkonvertierung mittels einer nicht-statischen Methode lautet:

CANY::operator NEWDTYP () const;

NEWDTYP gibt den Datentyp an, in den ein Objekt der Klasse CANY konvertiert werden soll. Um z.B. ein Objekt der Klasse Complex in einen double-Wert umzuwandeln, könnte der cast-Operator wie folgt aussehen:

1: class Complex
2: {
3:    double real, imag;
4: public:
5:    ...
6:    operator double () const;
7: };
8: // Typkonvertierung Complex -> double
9: Complex::operator double () const
10: {
11:    return sqrt(real*real + imag*imag));
12: }

Es ist zu beachten, dass die Operator-Methode keinen Returntyp besitzt, obwohl sie einen Wert zurückliefert.

Überladen der Operatoren >> und <<

Die Operatoren << und >> führen standardmäßig ein bitweise Schieben eines Integer-Datums nach links bzw. rechts durch. Sie wurden bisher auch verwendet, um Daten z.B. an den Ausgabestream cout zu übergeben. Durch Überladen dieser Operatoren kann nun erreicht werden, dass nicht nur intrinsische Datentypen, wie z.B. short oder double, in Streams verarbeitet werden können, sondern beliebige Objekte.

Überladen des Operators >>

Der Operator >> soll so überladen werden, dass ein Objekt mit einem Eingabestream eingelesen werden kann:

std::cin >> anyObject;

Da ein Operator für die Klasse zu überladen ist, deren Objekt links vom Operator steht, ist der Operator >> für die Klasse istream zu überladen. istream ist Basisklasse für die bekannten Streams iostream, ifstream und istringstream. Da ein istream-Objekt zunächst aber keinen Zugriff auf die nicht-public-Eigenschaften des einzulesenden Objekts (zweiter Operand des Operators >>) hat, ist die Operatorfunktion als friend-Funktion zu deklarieren. Dies wird durch folgende Funktionsdeklaration innerhalb der Klasse des einzulesenden Objekts erreicht:

friend istream& operator >> (istream& is, CAny& anyObj);
1: class Complex
2: {
3:    double real;
4:    double imag;
5: public:
6:    ...
7:    friend istream& operator >> (istream& is, Complex& op);
8: };

Die Definition der Operatorfunktion gleicht bis auf das Schlüsselwort friend der Deklaration 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.

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

Der auf diese Weise überladene Operator >> lässt nicht nur das Einlesen von der Standardeingabe zu, sondern auch aus einer Datei. Wie erwähnt ist dies möglich, da sowohl der Standard-Eingabestream cin wie auch der Datei-Eingabestream ifstream die gleiche Basisklasse istream haben.

1: // Klassen- und Funktionsdefinition wie oben angegeben
2: ...
3: int main()
4: {
5:    // Eingabestream mit Datei verbinden
6:    ifstream inFile;
7:    inFile.open("MyFile.dat");
8:    // Objekt der Klasse Complex definieren
9:    Complex myComp;
10:   // Daten für Objekt aus Datei einlesen
11:   inFile >> myComp;

12:   ...
13: }

Überladen des Operators <<

Genauso wie sich der Operator >> für die Eingabe überladen lässt, lässt sich der Operator << für die Ausgabe überladen. Dazu ist ebenfalls eine friend-Funktion wie folgt zu deklarieren:

friend ostream& operator << (ostream& os, CAny& anyObj);

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

1: class Complex
2: {
3:    double real;
4:    double imag;
5: public:
6:    ...
7:    friend ostream& operator << (ostream& os, Complex& op);
8: };
9: // Funktionsdefinition
10: ostream& operator << (ostream& os, Complex& op)
11: {
12:    os << op.real << ' ';    // Real- und Imaginär-
13:    os << op.imag << '\n';   // anteil ausgeben
14:    return os;               // Ref. auf Stream
15: }

Und da ostream auch Basisklasse von ofstream ist, können die Eigenschaften damit auch in einer Datei abgelegt werden.

Funktionsoperator ( )

Objekte, die den Funktionsoperator () überladen werden als Funktionsobjekt oder functor bezeichnet. Überlädt eine Klasse den Funktionsoperator, kann ein Objekt wie eine Funktion aufgerufen werden.

1: #include <iostream>
2: // Klasse mit überladenem Funktionsoperator
3: class Difference
4: {
5:    short value; // Eigenschaft
6: public:
7:    // ctor
8:    Difference(short val): value(val)
9:    { }
10:   // Überladener Funktionsoperator
11:   auto operator() (const short val) const
12:   {
13:      return val-value;
14:   }
15: };
16:
17: int main()
18: {
19:    // Objektdefinition
20:    Difference diff(50);
21:    // Impliziter Aufrufe des überladenen () Operators
22:    std::cout << diff (30) << '\n';
23:    // Expliziter Aufrufe des überladenen () Operators
24:    std::cout << diff.operator()(60) << '\n';
25: }

-20
10

Im Beispiel wird eine Klasse Difference zur Bildung von Differenzen definiert. Wird ein Objekt dieser Klasse definiert, erhält es bei seiner Definition zunächst den Wert übergeben, zu dem die Differenzen gebildet werden sollen (im Beispiel 50). Um die Differenz zu diesem Wert zu bilden, wird der Funktionsoperator aufgerufen, der als Argument den Wert für die Differenzbildung erhält.

Überladen des Indexoperators [ ]

Der Indexoperator [] wird typischerweise für Zugriffe auf Elemente in einem Feld verwendet. Diese Zugriffe werden weder zur Compilezeit noch zur Laufzeit auf ihre Zulässigkeit hin geprüft, d.h., es ist möglich auf Elemente zuzugreifen die außerhalb der Feldgrenzen liegen. Durch Überladen des Indexoperators ist es möglich, den Zugriff auf die Feldelemente zu kontrollieren.

Der Indexoperator wird durch folgende Methode überladen:

RTYP& CAny::operator [] (int index);

RTYP ist der Returntyp der Methode und CAny die Klasse, für die der Operator überladen wird. Der Parameter index enthält den Index des gewünschten Elements.

Der überladene Indexoperator sollte in der Regel eine Referenz auf das gewünschte Element zurückliefern und nicht das Element selbst. Nur so ist gewährleistet, dass der indizierte Zugriff sowohl rechts wie auch links vom Zuweisungsoperator stehen kann.

Das nachfolgende Beispiel zeigt die Anwendung des überladenden Indexoperators anhand eines Safe-Arrays, bei dem Zugriffe über die Feldgrenzen hinaus abgefangen werden.

1: #include <print>
2: // Klassendefinition des Safe-Arrays
3: class SafeArray
4: {
5:    short size;   // Grösse des Feldes
6:    short *pData; // Zeiger auf Datenfeld
7: public:
8:    SafeArray(short size);
9:    ~SafeArray();
10:   short& operator[] (int index);
11: };
12: // Definition der Methoden
13: // Konstruktor
14: SafeArray::SafeArray(short s)
15: {
16:    size = s;                // Grösse des Feldes merken
17:    pData = new short[size]; // Feld anlegen
18: }
19: // Destruktor
20: SafeArray::~SafeArray()
21: {
22:    delete [] pData; // Feld freigeben
23: }
24: // Überladener Indexoperator
25: short& SafeArray::operator [](int index)
26: {
27:    // Falls Index kleiner 0 ist
28:    if (index<0)
29:    {
30:       std::println("Index kleiner 0!");
31:       index = 0;
32:    }
33:    // Falls Index über das Feld hinausgreift
34:    if (index>=size)
35:    {
36:       std::println("Index ueberschreitet obere Grenze!");
37:       index = size-1;
38:    }
39:    // Referenz auf Datum zurückgeben
40:    return pData[index];
41: }
42:
43: // main() Funktion
44: int main()
45: {
46:    constexpr auto ARRAYSIZE = 10; // Feldgrösse
47:
48:    // Feldobjekt mit 10 Einträgen anlegen
49:    SafeArray myArray(ARRAYSIZE);
50:    // Feld füllen
51:    for (auto loop=0; loop<ARRAYSIZE; loop++)
52:       myArray[loop] = loop;
53:
54:    // Feld wieder auslesen
55:    // Es wird hier versucht hinter das Feld zu greifen
56:    for (auto loop=0; loop<=ARRAYSIZE; loop++)
57:       std::println("{}. Wert: {}", loop, myArray[loop]);
58:
59:    // Versuch das Element mit dem Index -1 zu beschreiben
60:    std::println("Versuch das Element -1 zu beschreiben!");
61:    myArray[-1] = 111;
62:    // Erstes Element ausgeben
63:    std::println("Erstes Element {}", myArray[0]);
64: }

0. Wert: 0
1. Wert: 1
2. Wert: 2
3. Wert: 3
4. Wert: 4
5. Wert: 5
6. Wert: 6
7. Wert: 7
8. Wert: 8
9. Wert: 9
Index ueberschreitet obere Grenze!
10. Wert: 9
Versuch das Element -1 zu beschreiben!
Index kleiner 0!
Erstes Element 111

In main() wird ein Safe-Array erstellt und mit Werten gefüllt. Beim Auslesen der Elemente wird versucht, über die obere Feldgrenze hinaus zuzugreifen, was mit einer Fehlermeldung quittiert wird. Aber nicht nur die Lesezugriffe werden verifiziert, sondern auch die Schreibzugriffe. Solche fehlerhaften Schreibzugriffe auf ein Feld sind besonders kritisch, da sie ein undefiniertes Verhalten des Programms zur Folge haben können. Im Beispiel wird versucht das Element -1 zu beschreiben, was ebenfalls mit einer Fehlermeldung quittiert wird. In einer realen Anwendung sollte bei einem unzulässigen Feldzugriff eine Ausnahme ausgelöst werden, die im Kapitel Ausnahmebehandlung behandelt werden.

Ab C++23 kann der Indexoperator noch durch eine Methode überladen werden, die mehrere Parameter erhält. Auf diese Weise kann z.B. ein internes eindimensionales Feld nach außen hin als mehrdimensionales Feld abgebildet werden.

1: #include <print>
2: // Klassendefinition MyArray
3: class MyArray
4: {
5:    int cols;       // Anzahl der Spalten
6:    short *pData;   // Zeiger auf Datenfeld
7: public:
8:    MyArray(short dim1, short dim2);
9:    ~MyArray();
10:   short& operator[] (int dim1, int dim2);
11: };
12: // Definition der Methoden
13: // Konstruktor
14: MyArray::MyArray(short dim1, short dim2)
15: {
16:    cols = dim2;                  // Spaltenzahl merken
17:    pData = new short[dim1*dim2]; // Feld anlegen
18: }
19: // Destruktor
20: MyArray::~MyArray()
21: {
22:    delete [] pData; // Feld freigeben
23: }
24: // Überladener Indexoperator, bildet das eindimensionale
25: // Feld als zweidimensionals ab
26: short& MyArray::operator [](int dim1, int dim2)
27: {
28:    // Hier sollten Plausibiltaetspruefungen
29:    // noch durchgefuehrt werden!
30:    // Referenz auf Datum zurückgeben
31:    return pData[dim1*cols+dim2];
32: }
33:
34: // main() Funktion
35: int main()
36: {
37:    // Feld definieren/initialisieren
38:    MyArray myArray(3,4);
39:    for(auto dim1=0; dim1!=3; ++dim1)
40:    {
41:       for (auto dim2=0; dim2!=4; ++dim2)
42:          // entspricht myArray[dim1][dim2]
43:          myArray[dim1,dim2]= dim1*10+dim2;
44:    }
45:    // Feld ausgeben
46:    for(auto dim1=0; dim1!=3; ++dim1)
47:    {
48:       for (auto dim2=0; dim2!=4; ++dim2)
49:          std::print("{}, ",myArray[dim1,dim2]);
50:    }
51: }

Anwenderdefinierte Literale

Durch Überladen des Operators "" (zwei Anführungszeichen) können anwenderdefinierte Literal-Typen definiert werden, wie z.B. 1.5_km für eine Längenangabe in Kilometer. Anwenderdefinierte Literale haben immer die Form xxx_SUFFIX, wobei xxx das Literal und _SUFFIX das Suffix des anwenderdefinierten Literals ist.

Die Definition des Suffixes für ein anwenderdefiniertes Literal kann auf zwei Arten erfolgen: als cooked literal oder als raw literal (uncooked literal). Der Unterschied zwischen den Literalen besteht darin, wie das vor dem Suffix stehende Literal interpretiert wird. Bei einem cooked Literal erhält der überladene Operator "" das Literal als Wert, während bei einem raw Literal das Literal als C-String übergeben wird. So wird z.B. das Literal 4711 bei einem cooked Literal als Integer-Wert 4711 an den überladenen Operator übergeben und bei einem raw Literal als C-String "4711".

Um ein anwenderdefiniertes Literal zu definieren, ist der Operator "" wie folgt zu überladen:

// cooked Literal
RTYP operator "" SUFFIX(arg);
RTYP operator "" SUFFIX(const char* arg, size_t len);
// raw Literal
RTYP operator "" SUFFIX(const char* arg);

RTYP gibt den Datentyp des zurückgelieferten Werts an und SUFFIX ist das Suffix, welches das anwenderdefinierte Literal charakterisiert, wie z.B. _km für Kilometer. Der führende Unterstrich des Suffixes ist zwingend vorgeschrieben, um ein anwenderdefiniertes Suffix von einem Standard-Suffix zu unterscheiden.

Der Parameter arg enthält das vor dem Suffix stehenden Literal. Bei einem cooked Literal muss arg ein Parameter vom Typ unsigned long long, long double, char sein oder ein Zeiger auf ein char-Feld, wobei der Parameter len die Anzahl der auszuwertenden Zeichen festlegt.

Bei einem raw Literal ist arg immer ein Zeiger auf einen C-String (d.h. eine mit 0 abgeschlossene Zeichenkette).

Sehen wir uns an, wie mithilfe eines raw Literals US-Dollar und Britische Pfund in EUR umgerechnet werden können. Zuerst werden die Suffixe festgelegen, die US-Dollar und Britische Pfund kennzeichnen, im Beispiel _USD und _GBP. Dazu ist der Operator "" wie folgt zu überladen:

1: // Ueberladener Operator "" fuer das Suffix _USD
2: float operator "" _USD(const char* digits)
3: {
4:    ... // hier wird gleich gerechnet
5: }
6: // Ueberladener Operator "" fuer das Suffix _GBP
7: float operator "" _GBP(const char* digits)
8: {
9:    ... // hier wird gleich gerechnet
10: }

Der Operator erhält das Literal als C-String übergeben, dessen Inhalt dann in EUR umgerechnet wird. Die vollständige Implementierung sieht damit wie folgt aus:

1: #include <print>
2: #include <charconv>
3: #include <cstring>
4:
5: // Umrechnungsfunktion
6: auto Calc(const char* digits, float rate)
7: {
8:    float val;
9:    auto res = std::from_chars(digits,
10:                   digits + strlen(digits), val);
11:   if (res.ec != std::errc{})
12:      return 0.0f;
13:   else
14:     return val * rate;
15:
16: }
17: // Ueberladener Operator "" _USD fuer das Suffix
18: // Rechnet US Dollar in EUR um
19: auto operator "" _USD (const char* digits)
20: {
21:    const float KURS = 0.85f; // Umrechnungdskurs
22:    return Calc(digits,KURS);
23: }
24: // Ueberladener Operator "" _GBP fuer das Suffix
25: // Rechnet Britisches Pfund in EUR um
26: auto operator "" _GBP(const char* digits)
27: {
28:    const float KURS = 1.16f;
29:    return Calc(digits,KURS);
30: }
31: // Ueberladener Operator "" _EUR fuer das Suffix
32: float operator "" _EUR(const char* digits)
33: {
34:    const float KURS = 1.f;
35:    return Calc(digits,KURS);
36: }
37: // main() Funktion
38: int main()
39: {
40:    std::println("1 USD = {} EUR", 1.0_USD);
41:    std::println("1 GBP = {} EUR", 1.0_GBP);
42:    // Addition von Euro, US-Dollar und Britische Pfund
43:    auto sum = 100.0_EUR + 100.0_USD + 100.0_GBP;
44:    std::println("100 EUR + 100 USD + 100 GBP = {}", sum);
45: }

Bei einem cooked Literal mit einem Parameter wird das vor dem Suffix stehende Literal als Wert interpretiert und an den überladenen Operator übergeben. Angenommen, es sind Längenangaben zu verarbeiten, welche in Meter, Kilometer, nautischen Meilen oder Yards vorliegen können. Die Bearbeitung 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 _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 wie folgt aussehen:

1: #include <print>
2:
3: // Umrechnung km in m
4: using ullong = unsigned long long;
5: ullong operator "" _km(ullong value)
6: {
7:    return value*1000;
8: }
9: // Umrechnung m in m
10: ullong operator "" _m(ullong value)
11: {
12:    return value;
13: }
14: // Umrechnung nm in m
15: ullong operator "" _nm(ullong value)
16: {
17:    return value*1852;
18: }
19: // Umrechnung yd in m
20: ullong operator "" _yd(ullong value)
21: {
22:    return value*0.914;
23: }
24: // main() Funktion
25: int main()
26: {
27:    // Verschiedene Laengeneinheiten addieren
28:    std::println("Laenge (m):{}",
29:                 2_km + 5_nm + 100_m + 400_yd);
30: }

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

Überladen von new und delete

Beim Überladen der Operatoren new und delete sind vier Fälle zu unterscheiden:

  • Überladen der Operatoren für ein intrinsisches Datum.
  • Überladen der Operatoren für intrinsische Felder.
  • Überladen der Operatoren für ein Objekt
  • Überladen der Operatoren für Objektfelder.

Da der erste und zweite Fall in der Praxis selten eingesetzt wird, sehen wir uns nur das Überladen der Operatoren für Objekte und Objektfelder an.

Die Operatoren new und delete können nur durch statische Methoden überladen werden. Auch wenn die Methoden nicht explizit als statisch deklariert sind, werden sie durch den Compiler immer als solche angelegt.

Überladen von new und delete für ein Objekt

Hierzu sind folgende Methoden zur Klasse hinzuzufügen:

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

Der Operator new erhält die vom Compiler berechnete Anzahl der zu reservierenden Bytes als Parameter übergeben. Innerhalb der Operator-Methode ist dann der erforderliche Speicher zu reservieren, was im nachfolgenden Beispiel mit dem globalen new Operator erfolgt. Als Ergebnis liefert die Methode den Zeiger auf den reservierten Speicher zurück.

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

1: // Klassendefinition
2: class Complex
3: {
4:    ...
5: public:
6:    static void* operator new (size_t size);
7:    static void operator delete(void *pMem);
8:    ...
9: };
10: // Überladener new Operator
11: void* Complex::operator new (size_t size)
12: {
13:    // Speicher reservieren
14:    void *pMem = new char[size];
15:    // Speicher mit 0 initialisieren
16:    memset(pMem,0,size);
17:    // Zeiger auf Speicher zurückgeben
18:    return pMem;
19: }
20: // Überladener delete Operator
21: void Complex::operator delete(void *pMem)
22: {
23:    // Speicher freigeben
24:    delete [] pMem;
25: }

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

Überladen von new und delete für Objektfelder

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

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

Diese Methoden unterscheiden sich nur durch die Angabe des Indexoperators nach dem Operatornamen von den vorherigen Methoden für einzelne Objekte.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz