C++ Kurs

Klassen-Templates

Die Themen:

Einleitung
Definition des Klassen-Templates
Formaler Datentypen in der Signatur von Memberfunktionen
Definition von Memberfunktionen
Überschreiben von Template-Memberfunktionen
Definition von Objekten
Mehrere formale Datentypen
Non-type Parameter
Default-Datentyp
Beispiel und Übung

Einleitung

In der letzten Lektion haben Sie etwas über Funktions-Templates erfahren. Aber Templates können nicht nur für Funktionen eingesetzt werden sondern auch für Klassen. In dieser Lektion erfahren Sie, wie Sie solche Klassen-Templates definieren und anwenden.

Dazu gleich wieder ein Beispiel. Nachfolgend werden zwei Klassen ShortStack und WinStack definiert, die beide das Verhalten eines Stacks implementieren. Der einzige Unterschied zwischen den beiden Klassen liegt nur im Datentyp des Zeigers auf die Stackdaten. Im ersten Fall ist dies ein short-Zeiger und im zweiten Fall ein Zeiger auf Win-Zeiger. Im Konstruktor der Klasse wird nun ein Feld vom Datentyp der Stackdaten angelegt.


// Stack für short-Werte
class ShortStack
{
    short *pData;
    ....
  public:
    ShortStack(int size)
    {
        pData = new short[size];
        ....
    }
}; 
// Stack für Zeiger auf Win-Objekte
class WinStack
{
    Win* *pData;
    ....
  public:
    WinStack(int size)
    {
        pData = new Win*[size];
        ....
    }
};

Da aber die Memberfunktionen zum Ablegen von Daten auf dem Stack (Memberfunktion Push(...)) und zum Auslesen der Daten (Memberfunktion Pop(...)) in ihrer Funktionalität gleich sein werden, bietet es sich hierfür an, ein Klassen-Template zu entwickeln. Im Folgenden  werden wir deshalb eine allgemein gültige Klasse für einen Stack entwickeln.

Wenn Sie bis hierher den Kurs durchgearbeitet haben, so sollten wissen, dass die Standard Bibliothek bereits eine solche Klasse enthält. Die hier entwickelte Stack-Klasse dient daher nur zur Übung.

Definition des Klassen-Templates

Auch hier verläuft die Entwicklung eines Klassen-Templates zunächst in den gleichen Schritten wie bei der Entwicklung eines Funktions-Templates.

Schritt 1:

Im ersten Schritt werden alle Klassendefinitionen bis auf eine entfernt und die Datentypen, die von Klasse zu Klasse unterschiedlich sind, durch einen beliebigen Namen ersetzt (der natürlich aber kein Schlüsselwort sein darf). Dieser beliebige Name wird, wie Sie in der Zwischenzeit wissen, auch als formaler Datentyp bezeichnet. Im Beispiel wurden die Datentypen wieder durch den Namen (Buchstaben) T ersetzt. Fast von selbst versteht es sich, dass dann auch in den jeweiligen Memberfunktionen die unterschiedlichen Datentypen durch den formalen Datentyp ersetzt werden müssen. So wurde beim Aufruf des new Operators im Konstruktor ebenfalls der formale Datentyp T eingesetzt.


// Ersetzen der verschiedenen Datentypen 
// Allgemeine Stackklasse
class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        pData = new T[size];
        ....
    }
};

Schritt 2:

Im zweiten Schritt müssen wir auch hier wieder dem Compiler etwas helfen. Damit er weiß, dass im Beispiel T nur ein Platzhalter für einen später noch festzulegenden Datentyp ist (formaler Datentyp), wird vor die Klassendefinition wieder die Anweisung

template <typename T>

gesetzt. Auch hier kann, wie bei den Funktions-Templates, das Schlüsselwort typename durch das Schlüsselwort class ersetzt werden; bei älteren Programmen werden Sie ausschließlich class bei der Templatedefinition vorfinden.


// Spezifikation des formalen Datentyps
template <typename T> class Stack
{
    T *pData;
    ....
  public:
    CStack(int size)
    {
        pData = new T[size];
        ....
    }
};

Und damit ist die Definition des Klassen-Templates im Prinzip auch schon vollständig.

Formaler Datentypen in der Signatur von Memberfunktionen

Der formale Datentyp kann selbstverständlich auch als Parameter oder als Returntyp von Memberfunktionen auftreten. So erhalten die Memberfunktionen Push(...) und Pop(...) eine Referenz auf den abzulegenden bzw. auszulesenden Wert.


template <typename T> class Stack
{
    T *pData;
    ....
  public:
    CStack(int size)
    {
        pData = new T[size];
        ....
    }
    bool Push(const T& val);
    bool Pop(T& val);
};

Beachten Sie besonders bei Memberfunktion im Zusammenhang mit Klassen-Templates, dass Sie Parameter entweder über Zeigern oder als Referenzen übergeben sollten. Vermeiden nach Möglichkeit die direkte Übergabe eines Werts. Bei der Übergabe eines Objekts werden sonst unter Umständen relativ viel Daten auf den Stack kopiert.

Definition von Memberfunktionen

Womit wir schon beim nächsten Thema sind, der Deklaration bzw. Definition von Memberfunktionen die formale Parameter erhalten oder einen formalen Datentyp als Returntyp besitzen. Und hierbei ist zu unterscheiden, ob eine Memberfunktion innerhalb oder außerhalb der Klasse definiert wird.

Beachten Sie bitte, dass Memberfunktionen bis zu diesem Zeitpunkt durch den Compiler nur 'vorläufig' definiert werden können, da sie ja noch formale Datentypen besitzen. Erst wenn eine Template-Klasse instanziiert wird, werden auch die entsprechenden Memberfunktion angelegt.

Definition von Memberfunktionen innerhalb der Klasse

Werden Memberfunktionen innerhalb der Klasse definiert, so kann die Definition der Memberfunktion wie gewohnt erfolgen. Aber denken Sie auch daran, dass in der Regel innerhalb der Klasse definierte Memberfunktionen als inline-Memberfunktionen betrachtet werden. Und dies kann unter Umständen den Code Ihres Programms beträchtlich vergrößern!


template <typename T> class Stack
{
    T *pData;
    ....
  public:
    Stack(int size)
    {
        ....
    }
    bool Push(const T& val)
    {
        pData[sIndex++] = val;
        ....
    }
    bool Pop(T& val)
    {
        val = pData[--sIndex];
        ....
    }
};

Und noch ein Hinweis: Soll die dargestellte Klasse Stack auch Objekte verarbeiten können, so sollten Sie für die abzulegenden Klassen auch den Zuweisungsoperator '=' definieren, da in den Memberfunktion Push(...) und Pop(...) Objektzuweisung statt finden!

Definition von Memberfunktionen außerhalb der Klasse

Werden die Memberfunktionen außerhalb der Klasse definiert, so ist eine auf den ersten Blick etwas verwirrende Definition erforderlich. Die allgemeine Syntax zur Definition einer Memberfunktion eines Klassen-Templates außerhalb der Klasse lautet:

template <typename T> RTYP CLASS<T>::MNAME(....)

T ist wieder der formale Datentyp, RTYP der Returntyp der Memberfunktion und CLASS der Name des Klassen-Templates. MNAME ist schließlich noch der Name der Memberfunktion. Das nachfolgende Bespiel zeigt die Definitionen der Memberfunktionen Push(...) und Pop(...) der vorherigen Klasse Stack. Beachten Sie, dass zwischen dem Returntyp der Memberfunktion und dem Namen der Memberfunktion der Klassenname steht, gefolgt vom formalen Datentyp in spitzen Klammern.


template <typename T> bool Stack<T>::Push(const T& val)
{....} 
template <typename T> bool Stack<T>::Pop(T& val)
{....}

Es versteht fast von selbst, dass die Memberfunktion vorher im Klassen-Template natürlich deklariert sein muss!

Überschreiben von Template-Memberfunktionen

Steigern wir die Schwierigkeit nun langsam. Wie bei Funktions-Templates können Sie auch für bestimmte Datentypen die Memberfunktionen eines Klassen-Templates überschreiben. Wie dies geht ist nachfolgend dargestellt. Dort werden die Memberfunktionen Push(...) und Pop(...) des Klassen-Templates Stack überschrieben um auf dem Stack char-Zeiger (für für C-Strings) abzulegen. Soll ein C-String mittels Push(...) auf dem Stack abgelegt werden, so wird eine Kopie des C-Strings erzeugt und letztendlich der Zeiger auf diese Kopie auf dem Stack abgelegt. Beim Auslesen des C-Strings mittels Pop(...) erhält die Anwendung dann den Zeiger auf diese Kopie zurück. Die Anwendung ist nun aber auch dafür verantwortlich, dass der Speicherplatz für den zurückgelieferten String irgendwann freigegeben wird. Ein solches Verhalten müssen Sie natürlich gut dokumentieren, damit keine "Speicherlöcher" entstehen. Außerdem darf z.B. an die Memberfunktion Pop(...) nur ein char-Zeiger übergeben werden, der aber auf keinen Fall ein Zeiger auf einen bis dahin reservierten Speicherbereich zeigt. Da der Zeiger von Pop(...) überschrieben wird, können Sie danach nicht mehr den ursprünglich reservierten Speicherbereich freigeben.


// Stack-Memberfunktionen für die Ablage von C-Strings
template<> bool Stack<char*>::Push(char* &ptr)
{
    pData[sIndex] = new char[strlen(ptr)+1];
    strcpy(pData[sIndex],ptr);
    sIndex++;
    ....
} 
template<> bool Stack<char*>::Pop(char* &ptr)
{
    sIndex--;
    ptr = pData[sIndex];
    ....
}

Beachten Sie bitte die Datentypen der Parameter! Da Push(...) und Pop(...) laut Deklaration Referenzen erwarten, müssen Sie an diese Memberfunktionen Referenzen auf char-Zeiger übergeben. Ferner müssen Sie bei einer solchen Lösung auch beachten, dass eventuell nicht alle Werte vom Stack geholt werden. Sie müssen zusätzlich noch einen eigenen Destruktor Stack<char*>::~Stack() erstellen, um den Speicher für die nicht abgeholten C-Strings freizugeben.

Definition von Objekten

Wenn Sie aus der Einführung der Standard-Bibliothek noch wissen, wie Objekte von Klassen-Templates definiert werden und dass ein Template auch mehrere formale Datentypen besitzen kann, dann können Sie gleich zu den non-type Parameter übergehen.

Nach dem Sie gesehen haben wie Klassen-Templates definiert werden, wollen wir jetzt an die Definitionen von Objekten von Klassen-Templates gehen. Unten sehen Sie 'zur Auffrischung' die Definition der 'normalen' Klasse Stack, die zum Ablegen von short-Werten dient. Der Konstruktor der Klasse erhält als Parameter die Anzahl der maximal auf dem Stack abzulegenden Werte. Danach wird dann das Stack Objekt myStack definiert, das maximal 10 short-Werte abspeichern kann.


// Bisherige Definitionen:
// Klassendefinition
class Stack
{
    short *pnData
  public:
    Stack(int size);
    ....
};

// Objektdefinition
Stack myStack(10);

Sehen wir uns jetzt die Definition eines Objekts eines Klassen-Templates an. Da bei der Definition des Klassen-Templates nur der formale Datentyp spezifiziert wurde, muss nun bei der Definition des Objekts der tatsächlich zu verwendende Datentyp angegeben werden. Dieser wird nach dem Klassennamen in spitzen  Klammern angegeben. Im Beispiel wird zuerst ein Stack zur Aufnahme von 10 long-Werten definiert und danach ein Stack zur Aufnahme von 50 char-Zeigern. Im Beispiel zu dieser Lektion finden Sie dann die vollständige Implementierung des Klassen-Templates  Stack.


// Definition eines Objekts eines Klassen-Templates:
// Template-Definition
template <typename T> class Stack
{
    T *pData;
  public:
    CStack(int size);
    ....
};

// Objektdefinition + Template-Instanziierung
Stack<long> longStack(10);
Stack<char*> charStack(50);

Mehrere formale Datentypen

Machen wir jetzt den nächsten Schritt. Genauso wie Funktions-Templates nicht nur einen formalen Datentyp besitzen können, können auch Klassen-Templates mehrere formale Datentypen haben. Die formalen Datentypen werden dann bei der Definition des Klassen-Templates einfach aufgelistet. Beachten Sie dabei bitte, dass das Schlüsselwort typename (oder class) vor jedem formalen Datentyp anzugeben ist. Im Beispiel enthält das Klassen-Template MyClass die beiden formalen Datentypen T1 und T2. Und selbstverständlich müssen dann bei der Definition eines Objekts des Klassen-Templates auch entsprechend viele Datentypen angeben. Auch diese werden in spitzen Klammern einfach aufgelistet.


// Template-Definition
template <typename T1, typename T2> class MyClass
{....}; 
// Objektdefinitionen
MyClass<int, char*> myObj1;
MyClass<float, double> myObj2;

Non-type Parameter

Erweitern wir das Klassen-Template jetzt etwas. Bisher enthielt das Klassen-Template als Template-Parameter nur formale Datentypen. Klassen-Templates können aber auch so genannte non-type Parameter erhalten. Sie können sich unter einem non-type Parameter eine Art Konstante vorstellen, die Sie dem Klassen-Template mit auf den Weg geben. Im Beispiel unten sehen Sie einen Auszug aus einer erweiterten Version der Klasse SArray, die ein  Safe-Array implementiert. Das Safe-Array können Sie sich hier nochmals ansehen. Das Klassen-Template erhält im zweiten Parameter die Größe des anzulegenden Feldes. Innerhalb der Klasse wird dieser zweite Parameter wie eine Konstante behandelt, d.h. Sie können den Wert dieses Parameters nicht mehr verändern (siehe Definition des Datenfeldes in der Template-Definition). Für non-type Parameter sind die folgenden Datentypen zugelassen: ganzzahlige Konstante/Literal, ein non-type Template-Parameter, eine Funktion, ein Objekt, die Adresse einer Funktion oder eines Objekts oder ein Zeiger auf ein Member. D.h. Sie können keine Gleitkommazahl oder gar einen char-Zeiger als non-type Parameter verwenden.


// Definition Klassen-Template
template <typename T, int SIZE> class SArray
{
    T pData[SIZE];     // Datenfeld
  public:
    T& operator[] (int index);
};
// Überladener Indexoperator
template <typename T, int SIZE> T& SArray<T, SIZE>::operator [](int index)
{
    ....
    // Falls Index ueber das Feld hinausgreift
    if (index>=SIZE)
    {
        .....
    }
    ....
}
// Objektdefintion
SArray<short,10> myArray;

Sehen Sie sich auch die Definition des überladenen Indexoperators [ ] an. Auch hier muss nun in der Template-Anweisung der non-type Parameter mit angegeben werden. Innerhalb der Operator-Memberfunktion wird dieser non-type Parameter zum Abprüfen auf die obere Feldgrenze verwendet.

Ebenfalls geändert hat sich auch die Definition eines Objektes des Klassen-Templates. Sie müssen hier jetzt auch den non-type Parameter mit angeben.

Sie können für diesen non-type Parameter auch einen Defaultwert vorgeben. Dann sieht die Definition des Klassen-Templates wie folgt aus:

template <typename T, int SIZE=5> class SArray
{....};

Damit können Sie dann ein Objekt wie folgt definieren:

SArray<float> myFloatArray;

In diesem Fall wird ein Safe-Array für 5 float-Werte erstellt.

Default-Datentyp

Beenden wir (vorläufig) die Behandlung von Klassen-Templates mit der Einführung des Default-Datentyps. Dass Funktionen und Memberfunktionen Parameter mit Defaultwerte besitzen können sollte Ihnen in der Zwischenzeit geläufig sein. Und genau das Gleiche gilt auch für Klassen-Templates, nur dass hier nun nicht ein Defaultwert vorgegeben wird sondern ein Default-Datentyp. Dazu wird nach dem formalen Datentyp der Zuweisungsoperator angegeben und dann der Default-Datentyp. Wird bei der Definition eines Objekts des Klassen-Templates dann innerhalb der spitzen Klammer kein Datentyp spezifiziert, so wird der Default-Datentyp verwendet. Im Beispiel wird zuerst ein Stack für int-Daten erzeugt und dann ein Stack für float-Daten.


// Definition des Klassen-Templates
template <typename T=int> class Stack
{
    ....
};
// Definition von Objekten
Stack<> intStack;
Stack<float> floatStack;

Damit haben Sie nun die Grundlagen von Klassen-Templates kennen gelernt. Nicht verschwiegen werden soll an dieser Stelle, dass Sie mit Klassen-Templates noch wesentlich mehr anfangen können. So können innerhalb eines Klassen-Templates weitere Klassen-Templates definiert werden oder auch Klassen-Templates für bestimmte Datentypen explizit definiert werden. Mehr zu Templates erfahren Sie dann noch später im Kurs.

Beispiel und Übung

Beispiel:

Entwickelt wird ein Klassen-Templates für die Realisierung eines Stacks. Der Stack kann standardmäßig fünf Werte/Objekte aufnehmen. Zum Ablegen von Werten/Objekten dient wie üblich die Memberfunktion Push(...) und zum Auslesen die Memberfunktion Pop(...).

Zum Nachweis, dass auch Objekte auf dem Stack abgelegt werden können, ist eine einfache Klasse Window implementiert. Beachten Sie, dass für die Klasse Window der Standard-Konstruktor definiert sein muss, da Stack ein Objektfeld anlegt. Außerdem ist, wie bei Klassen mit dynamischen Daten in der Praxis üblich, der Zuweisungsoperator '=' zu überschreiben. Innerhalb der Memberfunktionen Push(...) und Pop(...) der Stack Klasse werden ja schließlich Objektzuweisungen durchgeführt.

In main() wird zunächst ein Stack für die Aufnahme von fünf short-Werten definiert. Dieser Stack wird dann komplett gefüllt und wieder geleert.

Anschließend wir ein Stack zum Abspeichern von Window-Objekten definiert. Sie könnten auch einen Stack für die Aufnahme von Objektzeigern definieren, hätten dabei aber den Nachtteil, dass dann Veränderungen an Objekten auch auf die bereits auf dem Stack abgelegten Objekte wirken. Der Stack würde in diesem Fall ja nur die Zeiger auf die Objekte enthalten und nicht die Objekte selbst!

1 abgelegt.
2 abgelegt.
3 abgelegt.
4 abgelegt.
5 abgelegt.
6 konnte nicht abgelegt werden!
5 wieder geholt.
4 wieder geholt.
3 wieder geholt.
2 wieder geholt.
1 wieder geholt.
short-Stack nun leer!
Fülle Window-Stack
Hole Window-Daten vom Stack
Fenster 3
Fenster 2
Fenster 1


// Beispiel zu Klassentemplates

// Zuerst Dateien einbinden
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

// Definition der Template-Klasse Stack
// Defaultmässig wird ein Stack für 5 Einträge erstellt
template <typename T, int SIZE=5>
class Stack
{
  private:
     short  stackPtr;       // Stackindex (Stackpointer)
     T      data[SIZE];     // Zeiger auf Stackobjekte
  public:
     Stack()
     {
          stackPtr = 0;     // Stackindex resetieren
     }
     bool   Push(const T&);
     bool   Pop(T&);
};
// Definition der Memberfunktionen
// Legt Datum auf dem Stack ab
template <typename T, int SIZE> bool Stack<T,SIZE>::Push(const T& nV)
{
    // Falls Stack schon voll ist
    if (stackPtr==SIZE)
        return false;
    // Datum ablegen
    data[stackPtr++] = nV;
    return true;
}
// Liest Datum vom Stack aus
template <typename T, int SIZE> bool Stack<T,SIZE>::Pop(T& nV)
{
    // Falls Stack leer
    if (stackPtr==0)
        return false;
    // Datum holen
    nV = data[--stackPtr];
    return true;
}

// Definition der Demo-Klasse Window
// Window dient nur zum Beweis, dass auf dem Stack jetzt auch
// Objekte bzw. Zeiger auf Objekte abgelegt werden können
class Window
{
    string title;
  public:
    Window(const char *const pT): title(pT)
    {}
    Window()      // Wird für Objektfeld in Stack benötigt!!
    {}
    Window& operator = (const Window& obj2);
    friend std::ostream& operator << (std::ostream&, const Window&);
};
// Definition der Memberfunktionen
// Überladener Zuweisungsoperator
Window& Window::operator =(const Window& obj2)
{
    title = obj2.title;
    return *this;
}
// Überladener Ausgabeoperator << für die Window-Klasse
std::ostream& operator << (std::ostream& os, const Window& obj)
{
     os << obj.title;
     return os;
}

// main() Funktion
int main()
{
    short index;        // Schleifenindex

    // Short-Stack definieren für 5 short-Wert (Defaultgrösse)
    Stack<short>   shortStack;

    // Short-Stack komplett füllen
    for (index=1;;index++)
    {
        // Falls Wert auf dem Stack abgelegt
        if (shortStack.Push(index))
            cout << index << " abgelegt.\n";
        // sonst Schleife verlassen
        else
        {
            cout << index << " konnte nicht abgelegt werden!\n";
            break;
        }
    }

    // Short-Stack wieder auslesen
    for (;;)
    {
        short  val;
        // Falls Wert ausgelesen wurde
        if (shortStack.Pop(val))
            cout << val << " wieder geholt.\n";
        // sonst Schleife verlassen
        else
        {
            cout << "short-Stack nun leer!\n";
            break;
        }
    }

    // Stack für 10 Window-Objekte definieren
    Stack<Window,10>   windowStack;
    // Window-Objekte auf Stack ablegen
    // ACHTUNG! Hier wird ein temp. Window-Objekt erstellt
    // welches dann an Push() übergeben wird. Push() ruft
    // den Zuweisungsoperator von Window auf um das temp.
    // Objekt in den Stack zu übernehmen. Nach der Rückkehr
    // von Push() wird das temp. Objekt wieder gelöscht!
    cout << "Fülle Window-Stack\n";
    windowStack.Push(Window("Fenster 1"));
    windowStack.Push(Window("Fenster 2"));
    windowStack.Push(Window("Fenster 3"));

    // Nun Window-Objekte wieder vom Stack holen
    cout << "Hole Window-Daten vom Stack\n";
    // Window-Objekt erstellen
    // Wird von Pop() mit dem akt. Stack-Objekt
    // 'ausgefüllt' (Aufruf des Zuweisungsoperators)
    Window actWindow;
    while (windowStack.Pop(actWindow))
    {
        // Window ausgeben
        cout << actWindow << endl;
    }

}

Übung:

Entwickeln Sie ein Klassen-Template für ein Safe-Array, das standardmäßig zur Ablage von int-Werten dient. Ein Safe-Array verhält aus der Sicht des Anwenders wie ein normales Feld, d.h. es speichert eine Anzahl von Werten/Objekten ab, auf die indiziert zugegriffen wird. Beim indizierten Zugriff auf ein Feldelement wird jedoch der Index auf Gültigkeit überprüft. Liegt der Index außerhalb des erlaubten Bereichs (kleiner 0 oder über der oberen Feldgrenze), so wird eine Fehlermeldung ausgegeben und das Programm beendet. Dazu überlädt das Safe-Array den Indexoperator [ ]. Dem Konstruktor des Safe-Arrays wird die benötigte Feldgröße als Parameter übergeben.

In main() wird dann, je nach dem ob das Symbol INT_TEST bzw. CLASS_TEST definiert ist, ein Safe-Array für int-Werte bzw. Demo-Objekte angelegt. Das Safe-Array wird dann mit Werten/Objekten belegt. Zur Kontrolle der richtigen Arbeitsweise des Safe-Arrays werden mit gültigem Index verschiedene Werte/Objekte abgelegt und wieder ausgelesen. Anschließend erfolgt dann ein Zugriff mit einem ungültigen Index, was zur Ausgabe einer Fehlermeldung und zum Abbruch des Programms führt.

Ein Großteil der Übung ist im Ausgangslisting bereits fertig vorgegeben, so auch die Klasse Demo. Beachten Sie bei dieser Klasse dass sie dynamische Eigenschaften enthält und damit die Regel der großen 3 erfüllen sollte (oder besser muss), d.h. die Klasse muss einen Destruktor, den Kopierkonstruktor und den überladenen Zuweisungsoperator '=' besitzen.


// Ausgangslisting für Übung zu Klassentemplates

// Zuerst Dateien einbinden
#include <iostream>

using std::cout;
using std::endl;

// Symbol definieren für int-Stack
#define INT_TEST
// ODER dieses Symbol für einen Demo-Stack
//#define CLASS_TEST

// Definition der Template-Klasse SArray
// Defaultmässig wird Safe-Array für int-Daten erstellt
// ... Hier jetzt Klassentemplate definieren


// Demo-Klasse
class Demo
{
    char *pText;
public:
    // Standardkonstruktor, wird benötigt für Felddefinition
    // in der Safe-Array Klasse
    Demo()
    {
        pText = NULL;
    }
    // Konstruktor für Objekterstellung
    Demo(const char* const pT)
    {
        pText=new char[strlen(pT)+1];
        strcpy(pText,pT);
    }
    // Kopierkonstruktor, wird benötigt wenn aus einem
    // Safe-Array Element ein neues Objekt erstellt wird
    Demo(const Demo& Orig)
    {
        pText = new char[strlen(Orig.pText)+1];
        strcpy(pText,Orig.pText);
    }
    // Destruktor
    ~Demo()
    {
        delete [] pText;
    }
    // Überladener Zuweisungsoperator, wird benötigt wenn einem
    // Safe-Array Element ein Demo Objekt zugewiesen wird
    Demo& operator = (const Demo& rhs)
    {
        if (&rhs == this)
            return *this;
        delete [] pText;
        pText = new char[strlen(rhs.pText)+1];
        strcpy(pText,rhs.pText);
        return *this;
    }
    // Ausgabe-Memberfunktion
    void Print() const
    {
        cout << pText << endl;
    }
};

// main() Funktion
int main()
{

#ifdef INT_TEST
    const int ARRAYSIZE = 10;  // Feldgrösse
    int index;                 // Schleifenzähler

    // Safe-Array mit Standard-Datentyp int anlegen
    SArray<> shortArray(ARRAYSIZE);
    // Safe-Array füllen
    for (index=0; index<ARRAYSIZE; index++)
        shortArray[index] = index;
    // Safe-Array wieder auslesen
    for (index=0; index<ARRAYSIZE; index++)
        cout << index << ". Wert: " << shortArray[index] << endl;
    // Versuch das Element mit dem Index -1 zu lesen
    cout << "Versuch das Element -1 zu beschreiben!\n";
    shortArray[-1] = 111;
#endif

#ifdef CLASS_TEST
    // Safe-Array für Demo-Objekte erstellen
    SArray<Demo> demoArray(3);
    // Erstes und letztes Feldelement belegen
    // Ruft überladenen Zuweisungsoperator von Demo auf!
    demoArray[0] = Demo("Fenster-Nummer 0");
    demoArray[2] = Demo("Fenster-Nummer 2");
    // Demo-Objekte im Safe-Array ausgeben
    demoArray[0].Print();
    demoArray[2].Print();
    // Objekt aus Safe-Array kopieren
    // Ruft Kopierkonstruktor von Demo auf!
    Demo newObject = demoArray[2];
    newObject.Print();
    // Safe-Array Elemente zuweisen
    // Ruft überladenen Zuweisungsoperator von Demo auf!
    demoArray[2] = demoArray[0];
    demoArray[2].Print();

    // Nun über obere Grenze hinausgreifen
    demoArray[5] = Demo("Fehler!");
#endif
}

Wenn Ihr Klassen-Template richtig arbeitet, sollten Sie die nachfolgende Ausgabe erhalten. Um das Safe-Array mit den beiden verschiedenen Datentypen zu testen, definieren Sie eines der Symbole INT_TEST oder CLASS_TEST am Programmanfang. Sie können nicht beide Datentypen in einem Durchlauf testen, da bei fehlerhaftem Zugriff auf das Safe-Array das Programm beendet werden soll.

Safe-Array mit int-Werten:

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
Versuch das Element -1 zu beschreiben!
Index kleiner 0!

Safe-Array mit Demo-Objekten:

Fenster-Nummer 0
Fenster-Nummer 2
Fenster-Nummer 2
Fenster-Nummer 0
Index 5 überschreitet obere Grenze 3!

Lösung ansehen!