Ü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 im Folgenden von Funktionen die Rede ist, sind damit stets auch Methoden gemeint.

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:

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

void SetData(int);
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 zwei double-Werte.

#include <print>

// Vertauschen von short-Daten
void Swap(short& v1, short& v2)
{
    auto temp = v1;
    v1 = v2;
    v2 = temp;
}
// Vertauschen von double-Daten
void Swap(double& v1, double& v2)
{
    auto temp = v1;
    v1 = v2;
    v2 = temp;
}

int main()
{
    short var1 = 10, var2 = 20;
    std::println("var1: {}, var2: {}",var1,var2);
    // Vertauschen der short-Daten
    Swap(var1,var2);
    std::println("var1: {}, var2: {}",var1,var2);

    double dvar1 = 1.11, dvar2 = 2.22;
    std::println("dvar1: {}, dvar2: {}",dvar1,dvar2);
    // Vertauschen der double-Daten
    Swap(dvar1,dvar2);
    std::println("dvar1: {}, dvar2: {}",dvar1,dvar2);
    // Aber das geht nicht!
    // Swap(var1, dvar2);
}

var1: 10, var2: 20
var1: 20, var2: 10
dvar1: 1.11, dvar2: 2.22
dvar1: 2.22, dvar2: 1.11

Jedoch ist beim Aufruf einer überladenen Funktion ist darauf zu achten, dass der Aufruf eindeutig ist. So würde der Aufruf in Zeile 32 zu einem Fehler führen.

Besitzen die überladenen Funktionen Default-Parameter, müssen diese sich in mindestens einem Parameter vor dem Default-Parameter unterscheiden. Nur dann ist beim Aufruf der Funktion eindeutig, welche auszuführen ist.

Übungen:

ofunc_01:

Entwickeln Sie eine Klasse zum Abspeichern der Daten eines Fensters. Das Fenster soll die Eigenschaften Position (x/y) und Titel besitzen.

Zum Setzen der Fenstereigenschaften sind zwei Methoden SetData() zu definieren. Die erste Methode erhält die Fenstereigenschaften als Werte übergeben und die zweite Methode soll die Eigenschaften aus einem übergebenen Fenster übernehmen.

Des Weiteren ist eine Methode Draw() zu schreiben, um die Fenstereigenschaften auszugeben.

In main() sind zwei Fensterobjekte zu definieren, die mit unterschiedlichen Daten zu initialisieren sind. Zur Kontrolle sind die Daten der beiden Objekte auszugeben. Danach soll das zweite Fensterobjekt die Eigenschaften des ersten Fensters übernehmen. Anschließend ist das zweite Objekt nochmals ausgegeben.

1. Fenster:
    Position: (10,20), Titel: Fenster Eins
2. Fenster:
    Position: (11,22), Titel: Fenster Zwei
2. Window gleich 1. Window gesetzt:
    Position: (10,20), Titel: Fenster Eins

1. Fenster:
    Position: (10,20), Titel: Fenster Eins
2. Fenster:
    Position: (11,22), Titel: Fenster Zwei
2. Window gleich 1. Window gesetzt:
    Position: (10,20), Titel: Fenster Eins

ofunc_02:

In dieser und einigen nachfolgenden Übungen wird eine Klasse CString für die Bearbeitung von Strings entwickelt. Diese CString-Klasse besitzt am Ende einen Teil der Funktionalität der in der C++-Bibliothek enthaltenen Klasse string.

Generell gilt: der Speicherplatz für den in der Klasse abzulegenden String ist dynamisch als char-Feld zu reservieren.

In dieser Übung sind außer dem notwendigen Standardkonstruktor und Destruktor folgende Methoden der Klasse CString zu implementieren:

  • SetString(...) um einen per char-Zeiger übergebenen String zu übernehmen.
  • SetString(...) um einen per CString-Referenz übergebenen String zu übernehmen, d.h., es wird eine Kopie erstellt.
  • AddString(...) um einen per char-Zeiger übergebenen String an den aktuellen String anzuhängen.
  • AddString(...) um einen per CString-Referenz übergebenen String an den aktuellen String anzuhängen.
  • GetString() liefert einen const-Zeiger auf das char-Feld in CString.

In main() sind zwei CString-Objekte zu definieren. Das erste CString-Objekt ist mit dem C-String "Eine Investition " zu belegen und das zweite CString-Objekt bleibt leer. Beide CString-Objekte sind zur Kontrolle auszugeben.

Anschließend ist dem ersten CString-Objekt der C-String "in Wissen " und dem zweiten CString-Objekt der C-String "bringt immer noch die besten Zinsen" hinzuzufügen. Beide CString-Objekte sind zur Kontrolle erneut auszugeben.

Zum Schluss ist ein drittes CString-Objekt zu definieren, in welches zunächst das erste CString-Objekt kopiert wird. Anschließend ist diesem CString-Objekt der C-String "\n" und dann das zweite CString-Objekt hinzuzufügen. Geben Sie das dritte CString-Objekt ebenfalls aus.

Verwenden Sie zum Kopieren eines Strings in ein char-Feld die Funktion strcpy_s(), zum Anfügen eines Strings an einen bestehenden String die Funktion strcat_s() und für die Berechnung der Länge eines C-Strings die Funktion strlen().

char* strcpy_s(char* dest, rsize_t max, const char* src);
char *strcat_s(char *dest, rsize_t max, const char *src);
std::size_t strlen(const char* str);

dest ist ein Zeiger auf das Zielfeld und src ein Zeiger auf die Quelle. max gibt die maximale Anzahl der zu kopierenden Zeichen an und ist in der Regel die Größe des Zielfeldes.

In der Übung soll davon ausgegangen werden, dass bei der Übergabe eines char-Zeigers an eine Methode der Zeiger auf einen gültigen C-String verweist.

Ausgangs-Strings:
    String1: 'Eine Investition '
    String2: ''
Nach AddString():
    String1: 'Eine Investition in Wissen '
    String2: 'bringt immer noch die besten Zinsen'
Neuer String:
    String3: 'Eine Investition in Wissen
bringt immer noch die besten Zinsen'

Überladen des Konstruktors

Hatten die 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.

#include <print>
#include <string>
#include <string_view>

class CAny
{
    std::string title;  // einziges Datum
    int objNum;         // laufende Objektnummer
    inline static int objCount{0};  // Objektzahler
public:
    // 1. ctor: Standard
    CAny()
    {
        objNum = ++objCount;
        Trace(">> ctor");
    }
    // 2. ctor: mit Parameter
    CAny(std::string_view _title): title(_title)
    {
        objNum = ++objCount;
        Trace(">> ctor");
    }
    // dtor
    ~CAny()
    {
        Trace("<< dtor");
    }
    // Hilfsmember zur Ablaufverfolgung
    void Trace(std::string_view func)
    {
        // Wenn keine Debugausgabe erfolgen soll,
        // println auskommentieren
        std::println("{} Obj-Nr. {}: {}",func,objNum,title);
    }
};

int main()
{
    CAny obj1;               // Standard-ctor
    CAny obj2{"Der Titel"};  // 2. ctor
}

>> ctor Obj-Nr. 1:
>> ctor Obj-Nr. 2: Der Titel
<< dtor Obj-Nr. 2: Der Titel
<< dtor Obj-Nr. 1:

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.

#include <print>

union Month
{
    const char *pText;  // Monat als Text
    char cNum;          // Monat als Wert
    // ctor, Monatstext initialisieren
    Month(const char* text): pText(text)
    {}
    // ctor, Monatswert initialisieren
    Month(char num): cNum(num)
    {}
};

int main()
{
    // Monatswert ablegen
    Month actMonth{10};
    std::println("Monat: {}",
                 static_cast<int>(actMonth.cNum));
    // Monatstext ablegen
    Month newMonth{"November"};
    std::println("Monat: {}",newMonth.pText);
}

Monat: 10
Monat: November

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 des gleichen Typs 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.

#include <print>
#include <string>
#include <string_view>

class CAny
{
    std::string title;  // einziges Datum
    int objNum;         // laufende Objektnummer
    inline static int objCount{0};  // Objektzahler
public:
    // 1. ctor: Standard
    CAny()
    {
        objNum = ++objCount;
        Trace(">> ctor");
    }
    // 2. ctor: mit Parameter
    CAny(std::string_view _title): title(_title)
    {
        objNum = ++objCount;
        Trace(">> ctor");
    }
    // copy-ctor
    CAny(const CAny& source): title(source.title)
    {
        objNum = ++objCount;
        Trace(">> copy-ctor");
    }
    // dtor
    ~CAny()
    {
        Trace("<< dtor");
    }
    // Hilfsmember zur Ablaufverfolgung
    void Trace(std::string_view func)
    {
        // Wenn keine Debugausgabe erfolgen soll,
        // println auskommentieren
        std::println("{} Obj-Nr. {}: {}",func,objNum,title);
    }
};

int main()
{
    CAny obj1{"Title"}; // 2. ctor
    CAny obj2{obj1};    // copy-ctor
}

>> ctor Obj-Nr. 1: Title
>> copy-ctor Obj-Nr. 2: Title
<< dtor Obj-Nr. 2: Title
<< dtor Obj-Nr. 1: Title

Standardmäßig definiert der Compiler den Kopierkonstruktor 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 diese anderen Standardmethoden trotzdem durch den den Compiler definiert werden 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:

#include <print>
#include <cstring>

// Klassendefinition
class Window
{
    char *pTitle;
  public:
    // ctor
    Window(const char* const title);
    // copy-ctor
    Window(const Window& source);
    // dtor, gibt Titelspeicher frei
    ~Window();
    const char* const GetTitle() const
    {
        return pTitle;
    }
    // Swap Methode
    void Swap(this Window& self, Window& obj);
};
// ctor
Window::Window(const char* const title)
{
    // Platz für Fenstertitel reservieren
    pTitle = new char[strlen(title)+1];
    // Fenstertitel umkopieren
    strcpy_s(pTitle,strlen(title)+1,
             title);
}
// copy-ctor
Window::Window(const Window& source)
{
    // Platz für Fenstertitel reservieren
    pTitle = new char[strlen(source.pTitle)+1];
    // Fenstertitel umkopieren
    strcpy_s(pTitle,strlen(source.pTitle)+1,
             source.pTitle);
}
// dtor
Window::~Window()
{
    delete [] pTitle;
}
// Swap Methode, vertauscht zwei Window-Objekte
void Window::Swap(this Window& self, Window& obj)
{
    // copy-ctor, sichert Eigenschaften des aktuellen Objekts
    Window tmp = self;
    // Kopiere obj-Eigenschaften ins aktuelle Objekt
    self.pTitle = obj.pTitle;
    // Kopiere gesicherte Eigenschaften nach obj
    obj.pTitle = tmp.pTitle;
    // WICHTIG!!!
    tmp.pTitle = nullptr;
}

int main()
{
    Window win1{"Fenster1"};    // 2 Objekte definieren
    Window win2{"Fenster2"};
    // Fenster 'darstellen'
    std::println("win1: {}, win2: {}",win1.GetTitle(),
                 win2.GetTitle());
    // Fenster vertauschen
    win1.Swap(win2);
    // Fenster erneut darstellen
    std::println("win1: {}, win2: {}",win1.GetTitle(),
                 win2.GetTitle());
}

win1: Fenster1, win2: Fenster2
win1: Fenster2, win2: Fenster1

Die erste Anweisung in der Methode Swap() (Zeile 49) erstellt mithilfe des Kopierkonstruktors ein neues Window-Objekt tmp. Und im Kopierkonstruktor wird erneut Speicher für die Ablage des Fenstertitels allokiert und der Fenstertitel dann kopiert. Der ursprüngliche Speicher für den Fenstertitel wird aber eigentlich nicht mehr benötigt, da der Verweis darauf in der nachfolgenden Anweisung (Zeile 51) überschrieben wird.  Effizienter wäre es, wenn der Verweis auf den 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. Diese ist dadurch gekennzeichnet, dass nach dem Datentyp des Parameters die Symbole && folgen.

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.

// Klassendefinition
class Window
{
    char *pTitle;
  public:
    ...
    // Move-ctor
    Window(Window&& source);
};

// Move-ctor
Window::Window(Window&& source)
{
    // Lediglich Verweis kopieren
    pTitle = source.pTitle;
    // Speicher gehoert jetzt nicht mehr dem source-Objekt!!
    source.pTitle = nullptr;
}

Entscheidend ist die letzte Anweisung des Move-Konstruktors (Zeile 17), 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?

void Window::Swap(this Window& self, Window& obj)
{
   Window tmp = self;
   self.pTitle = obj.pTitle;
   obj.pTitle = tmp.pTitle;
   tmp.pTitle = nullptr;
}

Nun, woher soll der Compiler beim Übersetzen der Zeile 3 in Swap() wissen, dass die Ressourcen des Quell-Objekts nicht weiter benötigt werden? Die darauffolgende Zeile, 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:

void Window::Swap(this Window& self, Window& obj)
{
   Window tmp = std::move(self);
   self.pTitle = obj.pTitle;
   obj.pTitle = tmp.pTitle;
   tmp.pTitle = nullptr;
}

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:

#include <print>
#include <cstring>

// Klassendefinition
class Window
{
    char* pTitle;
public:
    // ctor
    Window(const char* const title)
    {
        // Platz für Fenstertitel reservieren
        pTitle = new char[strlen(title) + 1];
        // Fenstertitel umkopieren
        strcpy_s(pTitle, strlen(title) + 1,
            title);
    }
    // copy-ctor
    Window(const Window& source)
    {
        // Platz für Fenstertitel reservieren
        pTitle = new char[strlen(source.pTitle) + 1];
        // Fenstertitel umkopieren
        strcpy_s(pTitle, strlen(source.pTitle) + 1,
            source.pTitle);
    }
    // dtor, gibt Titelspeicher frei
    ~Window()
    {
        delete[] pTitle;
    }
    const char* const GetTitle() const
    {
        return pTitle;
    }
};

int main()
{
    Window win1 {"Ein Fenster"};   // 2 Objekte definieren
    Window win2 = win1;            // copy-ctor

    // Fenster 'darstellen'
    std::println("win1: {}, win2: {}", win1.GetTitle(),
        win2.GetTitle());
}

win1: Ein Fenster, win2: Ein Fenster

Die Klasse Window enthält zwei Konstruktoren: einen Konstruktor der den Fenstertitel per Parameter erhält sowie den Kopierkonstruktor. Beide Konstruktoren enthalten den gleichen Code, d.h., sie setzen allokieren den Speicher für den Fenstertitel und kopieren ihn um. 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:

#include <print>
#include <cstring>

// Klassendefinition
class Window
{
    char* pTitle;
public:
    // ctor
    Window(const char* const title)
    {
        // Platz für Fenstertitel reservieren
        pTitle = new char[strlen(title) + 1];
        // Fenstertitel umkopieren
        strcpy_s(pTitle, strlen(title) + 1,
            title);
    }
    // copy-ctor, ruft ctor auf
    Window(const Window& source) :
        Window(source.pTitle)
    {}
    // dtor, gibt Titelspeicher frei
    ~Window()
    {
        delete[] pTitle;
    }
    const char* const GetTitle() const
    {
        return pTitle;
    }
};

int main()
{
    Window win1 {"Ein Fenster"};   // 2 Objekte definieren
    Window win2 = win1;            // copy-ctor

    // Fenster 'darstellen'
    std::println("win1: {}, win2: {}", win1.GetTitle(),
        win2.GetTitle());
}

win1: Ein Fenster, win2: Ein Fenster

Die Anzahl der Delegierungen 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 (Abschnitt: Default-Methoden und -Operatoren), generiert der Compiler bestimmte Methoden automatisch, solange keine der dort beschriebenen Bedingung verletzt wird.

So wird im nachfolgenden Beispiel z.B. kein Standardkonstruktor (das ist der ohne Parameter) mehr autotmatisch definiert, da der Destruktor explizit definiert ist.

#include <print>
#include <cstring>

class Window
{
    char* pTitle = nullptr;
public:
    // ctor mit Parameter
    Window(const char* const _pTitle)
    {
        auto len = strlen(_pTitle)+1;
        pTitle = new char[len];
        strcpy_s(pTitle,len,_pTitle);
    }
    // dtor
    ~Window()
    {
        delete [] pTitle;
    }
    // Fenster 'zeichnen'
    void Print() const
    {
        if (pTitle != nullptr)
            std::println("Fenstertitel: {}",pTitle);
        else
            std::println("Fenstertitel: default");
    }
};

int main()
{
    // Ein Fenster definieren und zeichnen
    Window win1{"Ein Fenster"};
    win1.Print();
}

Fenstertitel: Ein Fenster

Was aber, wenn zusätzlich der Standardkonstruktor benötigt wird, weil z.B. Objektfelder definiert werden? Eine Lösung wäre, den Standardkonstruktor zusätzlich zu definieren. Und dieser zusätzliche Standardkonstruktor würde im Beispiel nur aus einer leeren Klammer {} bestehen, da die Eigenschaft pTitle bereits initialisiert wird.

Doch die Definition des Standardkonstruktors kann auch auf den Compiler übertragen werden, indem bei der Deklaration des Standardkonstruktors nach der leeren Parameterklammer = default angegeben wird.

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.

Im nachfolgenden Beispiel wir der Standardkonstruktor automatisch generiert und ein kopieren von Window-Objekt durch 'Löschen' des Kopierkonstruktors unterbunden.

#include <print>
#include <cstring>

class Window
{
    char* pTitle = nullptr;
public:
    Window() = default;
    Window(const Window& src) = delete;
    // ctor mit Parameter
    Window(const char* const _pTitle)
    {
        auto len = strlen(_pTitle)+1;
        pTitle = new char[len];
        strcpy_s(pTitle,len,_pTitle);
    }
    // dtor
    ~Window()
    {
        delete [] pTitle;
    }
    // Fenster 'zeichnen'
    void Print() const
    {
        if (pTitle != nullptr)
            std::println("Fenstertitel: {}",pTitle);
        else
            std::println("Fenstertitel: default");
    }
};

int main()
{
    // Ein Fenster definieren und zeichnen
    Window win1{"Ein Fenster"};
    win1.Print();

    // Fensterfeld definieren und Fenster[0] ausgeben
    Window winArray[3];
    for (const auto& elem: winArray)
        elem.Print();

    // Die nachfolgende Anweisung erzeugt einen Fehler!!!!
    Window win2{win1};
}

Übungen

octor_01:

Schreiben Sie die Übung ofunc_01 am Anfang dieses Kapitels so um, dass zum Setzen der Fenstereigenschaften entsprechende Konstruktoren verwendet werden.

In main() ist ein erstes Fenster dynamisch zu erstellen, welches die Daten als Parameter erhält. Anschließend ist ein zweites Fenster ebenfalls dynamisch zu erstellen, das mit den Eigenschaften des ersten Fensters initialisiert wird. Zur Kontrolle sind die Daten der beiden Fenster ausgegeben.

1. Fenster:
     Position: (10,20), Titel: Fenster Eins
2. Fenster:
     Position: (10,20), Titel: Fenster Eins

octor_02:

Schreiben Sie die Übung ofunc_02 so um, dass die Methoden SetString() durch entsprechende Konstruktoren ersetzt werden. Denken Sie daran, dass die Aufgaben eines Konstruktors delegiert werden können!

Versuchen Sie auch die Aufgaben der Methode AddString() entsprechend zu delegieren. Die Methode GetString() können Sie fürs Erste unverändert belassen.

Erstellen Sie in main() ein erstes CString-Objekt, das Sie mit dem C-String "Eine Investition " initialisieren und ein zweites leeres CString-Objekt. Geben Sie beide CString-Objekte aus.

Fügen Sie zum ersten CString-Objekt den C-String "in Wissen " und zum zweiten CString-Objekt den C-String "bringt immer noch die besten Zinsen" hinzu. Geben Sie beide CString-Objekte wieder aus.

Zum Schluss definieren Sie ein drittes CString-Objekt, das Sie mit dem Inhalt des ersten CString-Objekts initialisieren. Diesem neuen CString-Objekt fügen Sie den C-String "\n" sowie das zweite CString-Objekt hinzu. Geben Sie auch dieses Objekt wieder aus.

Ausgangs-Strings:
    String1: 'Eine Investition '
    String2: ''
Nach AddString():
    String1: 'Eine Investition in Wissen '
    String2: 'bringt immer noch die besten Zinsen'
Neuer String:
    String3: 'Eine Investition in Wissen
bringt immer noch die besten Zinsen'