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
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.
|
|
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.
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); }; |
|
|
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.
|
|
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!
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!
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]; .... } |
|
|
|
|
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); |
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; |
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.
|
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. |
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.
|
|
1 abgelegt. |
// 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; } } |
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 Safe-Array mit Demo-Objekten:
Fenster-Nummer 0 |