Überladen spezieller Operatoren
Dieses Kapitel befasst sich mit dem Überladen von Operatoren, die entweder 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) wie auch 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:
cdata2 = ++cdata1;
Für unsere Klasse CData soll der Operator alle Elemente im Datenfeld um eins inkrementieren.
// Alle Elemente um eins inkrementieren
CData& CData::operator ++ (this CData& self)
{
for (size_t index=0; index<self.dSize; index++)
self.pData[index]++;
return self;
}
Die mögliche Anwendung:
import CData;
int main()
{
CData obj1{5}; // Ausgangsobjekt
obj1.Print();
// Alle Elemente in obj1 um eins inkrementieren
// und dann obj2 zuweisen;
CData obj2 = ++obj1;
// Beide Objekte ausgeben
obj1.Print();
obj2.Print();
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Daten Objekt 1:
42, 68, 35, 1, 70,
Daten Objekt 2:
42, 68, 35, 1, 70,
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 CData-Objekt zurück und keine Referenz.
// Liefert das aktuelle Objekt zurueck und
// inkrementiert erst danach
CData CData::operator ++ (this CData& self, int)
{
CData tmp{self}; // Objekt kopieren
// Erst dann inkrementieren
for (size_t index=0; index<self.dSize; index++)
self.pData[index]++;
return tmp;
}
Die Anwendung sähe nun wie folgt aus:
import CData;
int main()
{
CData obj1{5}; // Ausgangsobjekt
obj1.Print();
// Alle Elemente in obj1 dem Objekt obj2
// zuweisen und dann obj1 inkrementieren
CData obj2 = obj1++;
// Beide Objekte ausgeben
obj1.Print();
obj2.Print();
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Daten Objekt 1:
42, 68, 35, 1, 70,
Daten Objekt 2:
41, 67, 34, 0, 69
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 CData in einen int-Wert umzuwandeln, könnte der cast-Operator wie folgt aussehen:
// CData Objekt in einen int-Wert konvertieren
// Liefert hier die Summe alle Daten zurueck
CData::operator int() const
{
int sum = 0;
for (size_t index=0; index<dSize; index++)
sum += pData[index];
return sum;
}
Die mögliche Anwendung:
#include <print>
import CData;
int main()
{
CData obj1{5}; // Ausgangsobjekt
obj1.Print();
// CData Objekt in int-Wert konvertieren
int res;
res = obj1;
std::println("int-Wert von obj1: {}",res);
}
Daten Objekt 1:
41, 67, 34, 0, 69,
int-Wert von obj1: 211
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 aber auch verwendet, um intrinsische 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 >> cdataObject;
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 std::istream& operator >> (std::istream& is,
CAny& anyObj);
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.
Für eine Klasse CAny mit 2 Eigenschaften sieht die Operatorfunktion wie folgt aus:
#include <print>
#include <iostream>
class CAny
{
int elem1 = 0; // Zwei beliebige Daten
short elem2 = 0;
public:
CAny() = default; // ctor
void Print() const // Ausgabe der Daten
{
std::println("elem1: {}, elem2: {}",elem1,elem2);
}
// Operator >> zum Einlesen der Daten
friend std::istream& operator >> (std::istream& in, CAny& obj);
};
// Operator >> zum Einlesen der Daten
std::istream& operator >> (std::istream& in, CAny& obj)
{
in >> obj.elem1 >> obj.elem2;
return in;
}
int main()
{
CAny obj; // Objekt definieren und
obj.Print(); // die Daten ausgeben
// Einlesen des Objekts mittels Operator >>
std::print("Bitte 2 Werte eingeben: ");
std::cin >> obj;
// Daten wieder ausgeben
obj.Print();
}
elem1: 0, elem2: 0
Bitte 2 Werte eingeben: 11 22
elem1: 11, elem2: 22
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.
#include <print>
#include <iostream>
#include <fstream>
// Datei zum Einlesen
constexpr const char* FILENAME = "../../04_Data/verbrauch.txt";
class CAny
{
... Definition wie vorher
};
// Operator >> zum Einlesen der Daten
std::istream& operator >> (std::istream& in, CAny& obj)
{
in >> obj.elem1 >> obj.elem2;
return in;
}
int main()
{
CAny obj; // Objekt definieren und
obj.Print(); // die Daten ausgeben
// Datei zum Lesen oeffnen
std::ifstream inFile(FILENAME);
if (!inFile)
{
std::println("Kann Datei {} nicht oeffnen!", FILENAME);
exit(1);
}
// Daten aus Datei einlesen
inFile >> obj;
// Datei schliessen!!
inFile.close();
// Eingelesene Daten ausgeben
obj.Print();
}
Überladen des Operators <<
Genauso wie der Operator >> für die Eingabe überladen werden kann, lässt sich der Operator << für die Ausgabe überladen. Dazu ist ebenfalls eine friend-Funktion wie folgt zu deklarieren:
friend std::ostream& operator << (std::ostream& out,
const CAny& anyObj);
Für die Klasse CData könnte die Operatorfunktion dann wie folgt aussehen:
// Ausgabe des Datenfeldes
export std::ostream& operator << (std::ostream& out, const CData& obj)
{
std::println("Daten Objekt {}:\t", obj.objNum);
for (size_t index = 0; index < obj.dSize; index++)
std::print("{:3},", obj.pData[index]);
std::println();
return out;
}
Und die Anwendung dann:
#include <iostream>
import CData;
int main()
{
CData obj{5}; // Objekt definieren
std::cout << obj; // und ausgeben
}
Und da ostream auch Basisklasse von ofstream ist, können die Eigenschaften damit auch in einer Datei abgelegt werden.
Wie Eigenschaften, z.B. eines CData-Objekts, mittels
std::println("Objekt-Eigenschaften: {}\n", obj1);
ausgegeben werden können, wird im Kapitel Template-Spezialitäten - std::format() und std::print() für eigene Datentypen erklärt.
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.
#include <print>
#include <cstdlib>
// Klasse zur Begrenzung des Wertebereichs
// von int-Werten
class Limit
{
int lower,upper; // unterer/oberer Grenzwert
public:
// ctor, erhaelt untere und obere Grenze
Limit(int low,int up):
lower(low),upper(up)
{}
// functor, begrenzt den uebergebenen Wert
auto operator ()(const int val) const
{
// Geschachtelter Bedingungsoperator
// (siehe Kapitel if-Verzweigung)
auto res = (val<lower)? lower :
((val>upper)? upper:val);
return res;
};
int main()
{
// Werte auf 30...70 begrenzen
Limit minMax{30,70};
// Erzeuge einige Zufallszahlen
for (auto index=0; index<10; index++)
{
int toCheck = std::rand()%100;
// Gebe Originalwert und begrenzten Wert aus
std::println("Wert: {:2}, begrenzt: {:2}",toCheck,
minMax(toCheck));
}
}
Wert: 41, begrenzt: 41
...
Wert: 0, begrenzt: 30
...
Wert: 78, begrenzt: 70
Funktionsobjekte spielen später bei den Algorithmen der C++-Standardbibliothek eine wichtige Rolle.
Ü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 eine Implementierung des überladenen Indexoperators für die Klasse CData, bei dem Zugriffe über die Feldgrenzen hinaus 'abgefangen' werden.
// Ueberladener Indexoperator
int& CData::operator [] (size_t index)
{
// size_t ist immer ein unsigned Integerwert
// und kann damit niemals <0 sein
if (index >= dSize)
index = dSize-1;
return pData[index];
}
Bei einem Zugriff über die Feldgrenzen hinaus, wird einfach auf den letzten Wert im Feld zugegriffen. In einer realen Anwendung sollte bei einem unzulässigen Feldzugriff eine Ausnahme ausgelöst werden, die aber erst im Kapitel Ausnahmebehandlung behandelt werden.
Nachfolgend die Anwendung des überladenen Indexoperator. Beachten Sie, dass auch die kritischen Schreibzugriffe mit abgefangen werden.
#include <print>
import CData;
int main()
{
CData obj{5}; // Datenfeld mit 5 Elemente
obj.Print(); // Datenfeld ausgeben
// Lesezugriff auf Datenfeld fuer den Bereich 3...6
std::println("Indizierter Zugriff auf die Daten:");
for (int index=3; index < 7; index++)
std::println("Index: {}, Wert: {}", index, obj[index]);
// Nun der kritische Scheibzugriff
std::println("Schreibzugriff auf das Element obj[-1]!!");
obj[-1] = 99;
obj.Print();
}
Daten Objekt 1:
41, 67, 34, 0, 69,
Indizierter Zugriff auf die Daten:
Index: 3, Wert: 0
Index: 4, Wert: 69
Index: 5, Wert: 69
Index: 6, Wert: 69
Schreibzugriff auf das Element obj[-1]!!
Daten Objekt 1:
41, 67, 34, 0, 99,
Ab C++23 kann der Indexoperator ebenfalls 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.
#include <print>
// Klassendefinition MyArray
// Bildes ein internes eindimensionales Feld
// nach außenhin als zweidimensionals Feld ab
class MyArray
{
int cols; // Anzahl der Spalten
short *pData; // Zeiger auf Datenfeld
public:
// ctor
MyArray(short dim1, short dim2);
// dtor
~MyArray();
// Ueberladener Indexoperator
short& operator[] (int dim1, int dim2);
};
// Konstruktor
MyArray::MyArray(short dim1, short dim2)
{
cols = dim2; // Spaltenzahl merken
// Eindimensionales(!) Feld anlegen
pData = new short[dim1*dim2];
}
// Destruktor
MyArray::~MyArray()
{
delete [] pData; // Feld freigeben
}
// Ueberladener Indexoperator, bildet das eindimensionale
// Feld als zweidimensionals ab
short& MyArray::operator [](int dim1, int dim2)
{
// Hier sollten Plausibiltaetspruefungen
// noch durchgefuehrt werden!
// Referenz auf Datum zurueckgeben
return pData[dim1*cols+dim2];
}
int main()
{
// Feld definieren
MyArray myArray{3,4};
// Feld initialisieren
for(auto dim1=0; dim1!=3; ++dim1)
{
for (auto dim2=0; dim2!=4; ++dim2)
// entspricht myArray[dim1][dim2]
myArray[dim1,dim2]= dim1*10+dim2;
}
// Feld ausgeben
for(auto dim1=0; dim1!=3; ++dim1)
{
for (auto dim2=0; dim2!=4; ++dim2)
std::print("{:3}, ",myArray[dim1,dim2]);
std::println();
}
}
0, 1, 2, 3,
10, 11, 12, 13,
20, 21, 22, 23,
Diese Art der Anwendung kommt vor allem dann zum Einsatz, wenn die im Feld abgelegten Daten mithilfe von C++-Standardbibliotheksfunktionen bearbeitet werden sollen. Diese können i.d.R. nur eindimensionale Felder bearbeiten.
Anwenderdefinierte Literale
Durch Überladen des Operators "" (zwei Anführungszeichen) können anwenderdefinierte Literaltypen 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 Standardsuffix 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 oder ein Zeiger auf ein char-Feld sein, 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 cooked literals Berechnung in Kilometer und Meter durchgeführt werden können. Das Ergebnis der Berechnung soll immer in Meter sein.
#include <print>
// Kilometer in Meter umrechnen
long double operator ""_km(long double km)
{
return km * 1000.0;
}
// keine Umrechnung erforderlich, es wird
// nur das Suffix mtr definiert
long double operator ""_mtr(long double mtr)
{
return mtr;
}
int main()
{
auto len = 1.2_km + 300._mtr;
std::println("1.2km + 300.m = {}m", len);
}
1.2km + 300.m = 1500m
Da ganze mithilfe eines raw literals sähe dann wie folgt aus:
#include <print>
#include <cstring>
#include <charconv>
// Kilometer in Meter umrechnen
long double operator ""_km(const char* const digits)
{
long double val;
auto res = std::from_chars(digits,digits+strlen(digits),val);
if (res.ec != std::errc{})
return 0.0l;
else
return val * 1000.0;
}
// keine Umrechnung erforderlich, es wird
// nur das Suffix mtr definiert
long double operator ""_mtr(const char* const digits)
{
long double val;
auto res = std::from_chars(digits,digits+strlen(digits),val);
if (res.ec != std::errc{})
return 0.0l;
else
return val;
}
int main()
{
auto len = 1.2_km + 300._mtr;
std::println("1.2km + 300.m = {}m", len);
}
1.2km + 300.m = 1499.9999999999999556m
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); // und
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 der C++-Bibliotheksfunktion malloc() 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 hier mit dem Gegenstück zu malloc(), der Funktion free().
// Ueberladener new Operator
void* CData::operator new (size_t _size)
{
// Speicher reservieren
void* pMem = std::malloc(_size);
// Speicher mit 0 initialisieren
std::memset(pMem,0,_size);
// Zeiger auf Speicher zurueckgeben
return pMem;
}
// Ueberladener delete Operator
void CData::operator delete(void *pMem)
{
// Speicher freigeben
free (pMem);
}
Ü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); // und
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.
Übungen
osops_01:
Als Ausgangspunkt für diese Übung dient die im vorherigen Kapitel erstellte Klasse Rect zum Abspeichern und Manipulieren von Rechteckdaten (ogen_01).
Zur Ausgabe der Rechteckdaten soll nicht mehr eine Methode verwendet werden, sondern der überladene Operator <<.
Zusätzlich ist der Suffix-Operator ++ zu überladen, der das Rechteck um eine Position in x- und y-Richtung verschiebt.
In main() ist ein Rechteck dynamisch zu erstellen und dessen Eigenschaften auszugeben.
Anschließend ist in einer Anweisung ein neues Rechteck zu erstellen und das erste Rechteck um eins zu verschieben.
Geben Sie das erste Rechteck und das neu erstellte Rechteck aus.
1. Rechteck:
Rechteck auf (10,20), Groesse: (100,200)
1. Rechteck um eins verschoben:
Rechteck auf (11,21), Groesse: (100,200)
Neues Rechteck:
Rechteck auf (10,20), Groesse: (100,200)
osops_02:
Erweitern Sie die in vorherigen Übungen (ogen_02) erstellte Klasse CString um den Operator >> zum Einlesen eines CString-Objekts. Die maximale Anzahl der einzulesenden Zeichen soll 80 Zeichen betragen.
Die Methode GetString() bleibt vorläufig bestehen, da wir i.A. noch keine Objekte mittels print() ausgeben können.
Fügen Sie zur Klasse den überladenen Indexoperator [ ] hinzu, um auf ein Zeichen innerhalb des Strings zuzugreifen. Folgende Anweisungen sollen mit dem Indexoperator erlaubt sein:
char cZeichen = stringObj[2];
stringObj[3] = cZeichen;
Wird ein ungültiger Index an den Indexoperator übergeben, soll dieser '0' zurückliefern.
Erstellen Sie in main() ein CString-Objekt. Lesen Sie von der Tastatur einen Text in das CString-Objekt ein und geben ihn wieder aus.
Wandeln Sie in main() den eingelesenen Text in Großbuchstaben um. 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 den umgewandelten Text wieder aus.
Bitte einen Text eingeben: Ist dies die Eingabe?!?
Eingabe war: Ist dies die Eingabe?!?
Alles in Grossbuchstaben: IST DIES DIE EINGABE?!?