C++ Tutorial

Überladen von Funktionen/Methoden

Überladen von Funktionen

Unter bestimmten Bedingungen ist es möglich, mehrere Funktionen oder Methoden mit gleichem Namen zu definieren. Diese 'Mehrfach-Definition' wird als Überladen bezeichnet.

Wenn aber mehrere Funktionen den gleichen Namen haben, müssen sich diese irgendwie unterscheiden, damit beim Aufruf der Funktion erkannt werden kann, welche aufzurufen ist. Diese Unterscheidung erfolgt über die Signatur der Funktionen, die sich in mindestens einem der folgenden Punkte unterscheiden muss:

  • Die Funktionen besitzen eine unterschiedliche Anzahl von Parameter.
  • Die Datentypen der Parameter sind unterschiedlich.

Obacht gegeben werden muss, wenn auf const correctness Wert gelegt wird. Beim Überladen wird der const-Qualifizierer bei den Parametern nicht mit ausgewertet.

1: void SetData(int);
2: void SetData(const int);   // FEHLER!

Sehen wir uns ein Beispiel für das Überladen an. Die beiden Funktionen Swap() dienen zum Vertauschen von zwei Werten, wobei die erste Funktion zwei short-Werte vertauscht und die zweite Funktion zwei double-Werte.

1: // Vertauscht zwei short-Werte
2: void Swap (short& v1, short& v2);
3: // Vertauscht zwei double-Werte
4: void Swap (double& v1, double& v2);

Beim Aufruf einer überladenen Funktion ist darauf zu achten, dass der Aufruf eindeutig ist.

1: short sVar;
2: double dVar;
3: ...
4: Swap (sVar, dVar);   // FEHLER!

Überladen des Konstruktors

Hatten unsere Klassen bisher nur einen Konstruktor, ermöglicht das Überladen des Konstruktors die Definition eines Objekts mit unterschiedlichen Argumenten. Auch beim Überladen des Konstruktors gilt: Die Konstruktoren müssen sich in der Anzahl der Parameter und/oder Datentypen der Parameter unterscheiden.

1: class Demo
2: {
3:    ...
4: public:
5:    Demo(); // Standard-ctor
6:    Demo(char*, short); // 2. ctor
7:    ...
8: };
9: int main()
10: {
11:    Demo obj1; // Aufruf Standard-ctor
12:    Demo obj2{"Emil",10}; // Aufruf 2. ctor
13:    ...
14: }

Dabei ist zu beachten, dass der Standardkonstruktor in der Regel immer dann benötigt wird, wenn Objektfelder dynamisch angelegt werden sollen.

Ein anderes Einsatzgebiet des überladenen Konstruktors ist die Initialisierung einer Variante. Musste bisher der Datentyp des Initialwertes mit dem Datentyp des ersten Elements in der Variante übereinstimmen, lässt sich durch bereitstellen eines Konstruktors jedes beliebige Variantenelement initialisieren.

1: union Month
2: {
3:    char *pText;    // Monat als Text
4:    char cNum;      // Monat als Wert
5:    // Monatstext initialisieren
6:    Month(char* text): pText(text)
7:    {}
8:    // Monatswert initialisieren
9:    Month(char num): cNum(num)
10:    {}
11: };
12: // main() Funktion
13: int main()
14: {
15:    // Monatswert ablegen
16:    Month actMonth{10};
17:    // Monatstext ablegen
18:    Month newMonth{"November"};
19: }

Und noch ein Hinweis: Jede Klasse besitzt aber nur einen Destruktor, da er immer parameterlos ist!

Kopierkonstruktor

Eine besondere Form des Konstruktors ist der Kopierkonstruktor (copy-ctor). Er wird dann aufgerufen, wenn ein Objekt bei seiner Definition mit einem anderen Objekt der gleichen Klasse initialisiert wird, d.h., das neue Objekt eine Kopie eines bestehenden Objekts ist. Der Kopierkonstruktor hat folgende Syntax:

CAny::CAny(const CAny& source);

CAny ist eine beliebige Klasse und der Referenzparameter source eine Referenz auf das zu kopierende Objekt. Nachfolgend ein Beispiel für einen solchen Kopierkonstruktor.

1: #include <string>
2: #include <print>
3: // Klassendefinition
4:  class Window
5:  {
6:    std::string title;
7: public:
8:    Window(): title{"Titel"}
9:    {
10:      std::println("std-ctor");
11:   }
12:   Window(const Window& source): title{source.title}
13:   {
14:      std::println("copy-ctor");
15:   }
16: };
17:
18: int main()
19: {
20:   // Aufrufe des Standard-ctor
21:    Window firstWin;
22:    auto pAnyWin = new Window;
23:    // Beispiele für den Aufruf des copy-ctor
24:   Window myWin(firstWin);
25:   Window yourWin(*pAnyWin);
26:   auto pWin1 = new Window(firstWin);
27:   auto pWin2 = new Window(*pAnyWin);
28:   delete pWin1;
29:   delete pWin2;
30: }

Bei Zeigerparametern ist zu beachten, dass der Zeiger zu dereferenzieren ist!

Der Compiler definiert den Kopierkonstruktor standardmäßig für jede Klasse. Und dieser Standard-Kopierkonstruktor kopiert jede Eigenschaft des Quell-Objekts in das Ziel-Objekt. Enthält das Quell-Objekt dynamische Eigenschaften, führt dies in der Regel zu einem ungewollten Verhalten, da die Zeiger kopiert werden und nicht die Daten. In einem solchen Fall ist der Kopierkonstruktor explizit zu definieren. Dabei ist zu beachten, dass die anderen standardmäßig durch den Compiler erzeugten Methoden ebenfalls nicht mehr automatisch erzeugt werden. Wie der Compiler trotzdem dazu gebracht werden kann, diese Standard-Methoden zu erzeugen, sehen wir uns gleich an.

Move-Konstruktor (Move-Semantik, Teil 1)

Der Move-Konstruktor ist ein Sonderfall des Kopierkonstruktors und kommt dann zum Einsatz, wenn es um die effiziente Verwaltung von Ressourcen, wie z.B. Speicher, geht. Da sich der Einsatz des Move-Konstruktors am besten anhand eines Beispiels verdeutlichen lässt, sehen wir uns einmal ein Beispiel für dessen Einsatz an.

Die Implementierung einer Swap() Methode zum Vertauschen von zwei Objekte, hier für die Klasse Window, könnte zunächst wie folgt aussehen:

1: // Klassendefinition
2: class Window
3: {
4:    char *pTitle;
5: public:
6:    // Standard-ctor, wird nicht weiter betrachtet
7:    Window();
8:    // copy-ctor
9:    Window(const Window& source);
10:   // dtor, gibt Titelspeicher frei
11:   ~Window();
12:   // Swap Methode
13:   Swap(Window& obj);
14: };
15: // copy-ctor
16: Window::Window(const Window& source)
17: {
18:    // Platz für Fenstertitel reservieren
19:    pTitle = new char[strlen(source.pTitle)+1];
20:    // Fenstertitel umkopieren
21:    strcpy_s(pTitel,strlen(source.pTitle)+1,source.pTitle);
22: }
23: // dtor
24: Window::~Window()
25: {
26:    delete [] pTitle;
27: }
28: // Swap Methode, vertauscht zwei Window-Objekte
29: void Window::Swap(Window& obj)
30: {
31:    // copy-ctor,
32:    Window tmp(*this);
33:    // Kopiere obj-Eigenschaften ins aktuelle Objekt
34:    *this = obj;
35:    // Kopiere gesicherte Eigenschaften nach obj
36:    obj = tmp;
37: }

Die erste Anweisung in der Methode Swap() (Zeile 32) erstellt mithilfe des Kopierkonstruktors ein neues Window-Objekt tmp. Innerhalb des Kopierkonstruktors wird Speicher für die Ablage des Fenstertitels allokiert und der Fenstertitel kopiert. Da aber danach der Fenstertitel nicht mehr benötigt wird, da er in der nachfolgenden Anweisung (Zeile 34) überschrieben wird, wäre es effizienter, wenn der Fenstertitel nicht ins tmp-Objekt kopiert sondern verschoben würde. Und in solchen Fällen kommt der Move-Konstruktor zum Einsatz. Der Move-Konstruktor überträgt den Besitz einer Ressource, im Beispiel den für den Fenstertitel reservierten Speicher, von dem zu kopierenden Objekt an das neu erstellte Objekt. Im Beispiel also vom aktuellen Objekt an das tmp-Objekt.

Bauen wir den Move-Konstruktor Stück für Stück auf. Der Move-Konstruktor erhält, im Gegensatz zum Kopierkonstruktor, als Parameter eine sogenannte rvalue-Referenz übergeben, die dadurch gekennzeichnet wird, dass nach dem Datentyp des Parameters die Symbole && folgen.

1: // Klassendefinition
2: class Window
3: {
4:    char *pTitle;
5: public:
6:    ...
7:    Window (const Window& obj); // copy-ctor
8:    Window (Window&& obj);      // Deklaration Move-ctor
9: };
10: ...
11: // Definition Move-ctor
12: Window::Window (Window&& obj)
13: {
14:    ...
15: }

Innerhalb des Move-Konstruktors werden dann die Eigenschaften von Quell-Objekt an das aktuelle Objekt übertragen. Dabei gilt es stets zu beachten, dass sich das Quell-Objekt nach der Übertragung der Eigenschaften in einem gültigen Zustand befindet. D.h., ein Zugriff auf eine übertragene Ressource über das Quell-Objekt ist zu verhindern.

1: // Klassendefinition
2: class Window
3: {
4:    ...
5: };
6: // Definition Move-ctor
7: Window::Window (Window&& obj)
8: {
9:    pTitle = obj.pTitle; // Speicher-Ressource uebertragen
10:   // Speicher gehoert jetzt nicht mehr dem Quell-Objekt
11:   obj.pTitle = nullptr;
12: }

Entscheidend ist die letzte Anweisung des Move-Konstruktors (Zeile 11), denn diese versetzt das Quell-Objekt wieder in einen gültigen Zustand. Ohne diese Anweisung verweist pTitle im Quell-Objekt und im aktuellen Objekt auf den gleichen Speicherbereich, was bei der Freigabe des Speichers im Destruktor von Window zu einem Fehler führt. Dadurch, dass pTitle ein nullptr zugewiesen wird, gibt das Quell-Objekt den Besitz des Speichers letztendlich frei, denn ein Aufruf von delete mit einem nullptr führt nichts aus.

Doch wenn wir jetzt die Swap() Methode ausführen könnten, würden wir keinen Unterschied zur Ausführung ohne den definierten Move-Konstruktor feststellen. Warum?

1: // Swap Methode
2: void Window::Swap(Window& obj)
3: {
4:    Window tmp(*this); // Sichere aktuelle Eigenschaften
5:    // Kopiere obj-Eigenschaften ins aktuelle Objekt
6:    *this = obj;
7:    // Kopiere gesicherte Eigenschaften nach obj
8:    obj = tmp;
9: }

Nun, woher soll der Compiler beim Übersetzen der Zeile 4 in Swap() wissen, dass die Ressourcen des Quell-Objekts nicht weiter benötigt werden? Die Zeile 6, die die Übertragung der Ressource erst erlaubt, kennt er zu diesem Zeitpunkt nicht. Und hier müssen wir dem Compiler etwas unter die Arme greifen. Die C++-Standardbibliothek enthält eine Funktion move(arg), die arg in eine rvalue-Referenz umwandelt, also in den Datentyp, den der Move-Konstruktor benötigt. Somit können wir die Swap() Methode jetzt wie folgt definieren:

1: // Swap Methode
2: void Window::Swap(Window& obj)
3: {
4:    Window tmp(std::move(*this));   // Aufruf des Move-ctor
5:    *this = obj;
6:    obj = tmp;
7: }

Delegierender Konstruktor

Ein Konstruktor einer Klasse kann einen weiteren Konstruktor seiner Klasse aufrufen, d.h., er kann einen Teil seiner Aufgaben an einen anderen Konstruktor delegieren. Dies ist dann nützlich, wenn zwei Konstruktoren den gleichen Ablauf haben, zumindest teilweise. Sehen wir uns dazu wieder ein Beispiel an:

1: // Klassendefinition
2: class Window
3: {
4:    std::string title;                // Fenstertitel
5:    unsigned int xPos = 0, yPos = 0;  // Fensterposition
6:    unsigned int width, height;       // Fenstergroesse
7:    unsigned int area;                // Fensterflaeche
8: public:
9:    // 1. ctor
10:   Window():
11:        title("Default"s), width(640), height(480)
12:   {
13:      area = width * height;
14:   }
15:   // 2. ctor
16:   Window(std::string t, unsigned int w, unsigned int h):
17:          title(t), width(w), height(h)
18:   {
19:      area = width * height;
20:   }
21:   ...
22: }

Die Klasse Window enthält einen Standardkonstruktor sowie einen zweiten Konstruktor mit Parametern. Beide Konstruktoren enthalten den gleichen Code, d.h., sie setzen den Fenstertitel und die Fensterposition und berechnen anschließend die Fläche des Fensters. Und gleicher Code an mehreren Stellen ist bei Änderungen stets fehleranfällig. Eine Lösung wäre, eine gesonderte Methode Init() zu erstellen, die von beiden Konstruktoren aufgerufen wird und die Initialisierungen durchführt.

Doch es geht auch effektiver. Ein Konstruktor, der delegating ctor, ruft einen anderen Konstruktor, den sogenannten target ctor, auf. Dieser "Aufruf" muss in der Initialisiererliste des delegierenden Konstruktors erfolgen. Und damit lässt sich das Beispiel wie folgt umschreiben:

1: // Klassendefinition
2: class Window
3: {
4:    std::string title;                 // Fenstertitel
5:    unsigned int xPos = 0, yPos = 0;   // Fensterposition
6:    unsigned int width, height;        // Fenstergroesse
7:    unsigned int area;                 // Fensterflaeche
8: public:
9:    // target ctor
10:   Window(std::string t, unsigned int w, unsigned int h):
11:          title(t), width(w), height(h)
12:   {
13:      area = width * height;
14:   }
15:   // delegating ctor
16:   Window(): Window("Default"s, 640, 480)
17:   { }
18:   ...
19: }

Die Anzahl der Delegationen ist nicht begrenzt. So könnte der obige target ctor wiederum einen anderen Konstruktor aufrufen, der weitere Initialisierungen durchführt.

default und delete

Wie bei der Einführung der Klassen erwähnt (31. Klassen und Objekte), generiert der Compiler bestimmte Methoden automatisch, solange keine der dort beschriebenen Bedingung verletzt wird. So erzeugt das nachfolgende Beispiel beim Übersetzen einen Fehler, da durch die Definition des Kopierkonstruktors der Standardkonstruktor nicht mehr automatisch generiert wird.

1: // Klassendefinition
2: class CAny
3: {
4:    ...
5: public:
6:    CAny(const CAny& source); // copy-ctor
7:    ...
8: };
9:
10: int main()
11: {
12:    // Fehler, da Standard-ctor nicht definiert!
13:    CAny firstObj;
14:    // Aufruf des copy-ctors
15:    CAny myObj{firstObj};
16: }

Was aber, wenn zusätzlich ein leerer Standardkonstruktor benötigt wird, weil z.B. Objektfelder definiert werden? Die Triviallösung wäre, den Standardkonstruktor zu definieren. Doch diese Arbeit können wir dem Compiler übertragen, indem bei der Deklaration des Standardkonstruktors nach der leeren Parameterklammer = default angegeben wird.

1: // Klassendefinition
2: class CAny
3: {
4:    ...
5: public:
6:    CAny() = default;         // Standard-ctor erzeugen
7:    CAny(const CAny& source); // copy-ctor
8:    ...
9: };
10:
11: int main()
12: {
13:    // Ok, da Standard-ctor definiert!
14:    CAny firstObj;
15:    // Aufruf des copy-ctors
16:    CAny myObj{firstObj};
17: }

Durch die Angabe von = default wird der Compiler angewiesen, die angegebene Standardmethode weiterhin zu erstellen. Beachten Sie, dass hier Standardmethode steht, d.h., = default darf nur nach einer der Standardmethoden stehen!

Das Gegenstück zu = default ist = delete. Durch die Angabe von = delete wird die automatische Generierung einer Standardmethode unterdrückt. Dieses Verhalten kann zum Beispiel dazu verwendet werden, um zu verhindern, dass Objekte kopiert werden können.

1: // Klassendefinition
2: class CAny
3: {
4:    ...
5: public:
6:    CAny() = default; // Standard-ctor
7:    // Standard copy-ctor unterdruecken
8:    CAny(const CAny& source) = delete;
9:    ...
10: };
11:
12: int main()
13: {
14:    // Ok, da Standard-ctor definiert
15:    CAny firstObj;
16:    // Fehler, da copy-ctor nicht definiert
17:    CAny myObj(firstObj);
18: }

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