|
|
I-PROGR2 SS03 - PROGRAMMIEREN2 - Vorlesung mit Übung
|
Die Vorlesung folgt der sehr klaren Darstellung von Kap.7 in [JOSUTTIS 1999]. Die angeführten Kodebeispiele sind den öffentlich zugänglichen Quelldateien zu diesem Buch entnommen. Sie wurden für die VL nur geringfügig geändert.
In diesem Beispiel wird von einer einfachen Ausnahmebehandlung Gebrauch gemacht, die erst in dernächsten VL ausführlich behandelt werden wird. Es ist aber ein schönes Beispiel für einen Kontext, in dem eine Ausnahmebehandlung ohne die neuen Techniken von C++, wenn überhaupt, dann nur sehr umständlich zu realisieren wäre.
Im Unterschied zu den bisherigen Klassen wird in einem Template der Klasse der Ausdruck
template <typename T> class Stack {
vorangestellt. Dadurch wird die Klasse gewissermassen mit der Typ-Variablen 'T' parametrisiert (statt T könnte auch jede andere Bezeichnung stehen).
In der usage-Datei wird dann gezeigt, wie man für T z.B. 'int' oder 'std::string' einsetzen kann.
g++ -o stack1_usage stack1_usage.cpp
stack1.hpp
//************************************************ // // Die folgenden Code-Beispiele stammen aus dem Buch: // Objektorientiertes Programmieren in C++ // Ein Tutorial für Ein- und Umsteiger // von Nicolai Josuttis, Addison-Wesley München, 2001 // // (C) Copyright Nicolai Josuttis 2001. // Diese Software darf kopiert, verwendet, modifiziert und verteilt // werden, sofern diese Copyright-Angabe in allen Kopien vorhanden ist. // Diese Software wird "so wie sie ist" zur Verfügung gestellt. // Es gibt keine explizite oder implizite Garantie über ihren Nutzen. //********************************************************************** // // Modifiziert fuer Vorlesung: gerd doeben-henisch // //**************************************************************** #include <iostream> #include <vector> #include <cstdlib> #include <string> namespace Bsp { // ******** Beginn Namensbereich Bsp:: template <typename T> class Stack { private: std::vector<T> elems; // Elemente public: Stack(); // Konstruktor void push(const T&); // Element einkellern T pop(); // Element auskellern T top() const; // oberstes Element }; // Konstruktor template <typename T> Stack<T>::Stack () { // nichts mehr zu tun } template <typename T> void Stack<T>::push (const T& elem) { elems.push_back(elem); // Kopie einkellern } template<typename T> T Stack<T>::pop () { if (elems.empty()) { throw "Stack<>::pop(): der Stack ist leer"; //ein return EXIT_FAILURE geht hier // nicht, da T nicht vom Typ int sein muss !! } T elem = elems.back(); // oberstes Element merken elems.pop_back(); // oberstes Element auskellern return elem; // gemerktes oberstes Element zurückliefern } template <typename T> T Stack<T>::top () const { if (elems.empty()) { throw "Stack<>::top(): der Stack ist leer"; //ein return EXIT_FAILURE geht hier // nicht, da T nicht vom Typ int sein muss !! } return elems.back(); // oberstes Element als Kopie zurückliefern } } // ******** Ende Namensbereich Bsp::
stack1_usage.cpp
In dieser Usage-Datei treten die Schlüsselworte 'try' und 'catch' auf; sie gehören zur Ausnahmebehandlung von C++, die in der nächsten VL behandelt wird. Hier nur soviel, dass im try{...}-Block derjenige Kode steht, der ausgeührt werden soll. Hinter dem try-Block befinden sich dann beliebig viele catch()-Aufrufe, die auf solche Ausnahmen reagieren, die in den vorausgehenden Klassen und Funktionen durch das Schlüsselwort 'throw' markiert worden sind.
try{...... std::cout << intStack.pop() << std::endl; ...... std::cout << stringStack.pop() << std::endl; std::cout << stringStack.pop() << std::endl; //Fehler, da leer } catch (const char* msg) { std::cerr << "Exception: " << msg << std::endl; return EXIT_FAILURE; }
#include "stack1.hpp" using namespace Bsp; int main() { try{ Bsp::Stack<int> intStack; // Stack für Integer Bsp::Stack<std::string> stringStack; // Stack für Strings // Integer-Stack manipulieren intStack.push(7); std::cout << intStack.pop() << std::endl; // String-Stack manipulieren std::string s = "hallo"; stringStack.push(s); std::cout << stringStack.pop() << std::endl; std::cout << stringStack.pop() << std::endl; //Fehler, da leer } catch (const char* msg) { std::cerr << "Exception: " << msg << std::endl; return EXIT_FAILURE; } }
Eine explizite Spezialisierung von Templates erscheint aus praktischer sicht eher überflüssig, da sie ja in jdem Fall möglich ist, doch bietet C++ dazu eine offizielle Möglichkeit, indem der Klassenkopf entsprechend gestaltet wird:
template<> class Stack<std::string> {
Das Schlüsselwort 'template' wird von einer leeren spitzen Klammer gefolgt, dafür wird eine explizite konkrete Typangabe an den Klassenbezeichner angehängt. Alles andere läuft nach dem gleichen Muster wie bei einem 'normalen' Template; statt der allgemeinen Typangabe 'T' muss man jetzt immer die spezielle Typangabe 'std::string' (in diesem konkreten Beispiel) machen.
stack2.hpp
#include <deque> #include <string> namespace Bsp { // ******** Beginn Namensbereich Bsp:: template<> class Stack<std::string> { private: std::deque<std::string> elems; // Elemente public: Stack() { // Konstruktor } void push(const std::string&); // Element einkellern std::string pop(); // Element auskellern std::string top() const; // oberstes Element }; void Stack<std::string>::push (const std::string& elem) { elems.push_back(elem); // Kopie einkellern } std::string Stack<std::string>::pop () { if (elems.empty()) { cerr << "Stack<std::string>::pop(): der Stack ist leer"; } std::string elem = elems.back(); // oberstes Element merken elems.pop_back(); // oberstes Element auskellern return elem; // gemerktes oberstes Element zurückliefern } std::string Stack<std::string>::top () const { if (elems.empty()) { cerr << "Stack<std::string>::top(): der Stack ist leer"; } return elems.back(); // oberstes Element als Kopie zurückliefern } } // ******** Ende Namensbereich Bsp::
Von der Sprache C her kennt man die Möglichkeit, mittels 'typedef' komplexe Typangaben in einfache Typangaben umzuformen. Dies ist auch mit dem schon eingeführten Schlüsselwort 'typename' möglich; man kann damit nicht nur einfach irgendeinen Ausdruck zu einem Typnamen erklären, man kann diese 'Typisierung' auch mit einer Zuweisung verknüpfen:
typename CONT = std::vector
Durch diese Zuweisung wird dem Ausdruck 'CONT' der Ausdruck 'std::vector' als Wert zugewiesen und zugleich zu einer Typangabe erklärt.
stack3.hpp
#include <vector> #include <iostream> #include <deque> #include <cstdlib> namespace Bsp { // ******** Beginn Namensbereich Bsp:: template <typename T, typename CONT = std::vector<T> > class Stack { private: CONT elems; // Elemente public: Stack(); // Konstruktor void push(const T&); // Element einkellern T pop(); // Element auskellern T top() const; // oberstes Element }; // Konstruktor template <typename T, typename CONT> Stack<T,CONT>::Stack () { // nichts mehr zu tun } template <typename T, typename CONT> void Stack<T,CONT>::push (const T& elem) { elems.push_back(elem); // Kopie einkellern } template <typename T, typename CONT> T Stack<T,CONT>::pop () { if (elems.empty()) { throw "Stack<>::pop(): der Stack ist leer"; } T elem = elems.back(); // oberstes Element merken elems.pop_back(); // oberstes Element auskellern return elem; // gemerktes oberstes Element zurückliefern } template <typename T, typename CONT> T Stack<T,CONT>::top () const { if (elems.empty()) { throw "Stack<>::top(): der Stack ist leer"; } return elems.back(); // oberstes Element als Kopie zurückliefern } } // ******** Ende Namensbereich Bsp::
stack3_usage.cpp
#include "stack3.hpp" using namespace Bsp; int main() { try{ Bsp::Stack<int> intStack; // Stack für Integer Bsp::Stack<double,std::deque<double> > dblStack; // Stack für Gleitkommawerte // Integer-Stack manipulieren intStack.push(7); std::cout << intStack.pop() << std::endl; // Gleitkommawerte-Stack manipulieren dblStack.push(42.42); std::cout << dblStack.pop() << std::endl; std::cout << dblStack.pop() << std::endl; } catch (const char* msg) { std::cerr << "Exception: " << msg << std::endl; return EXIT_FAILURE; } }
Aus praktischer Sicht interessant ist auch der Fall, dass man einem Template neben allgemeinen Typangaben auch Konstanten übergeben kann (vergleichbar dem Fall 'define CONST value' in C).
stack4.hpp
#include <iostream> #include <string> #include <cstdlib> namespace Bsp { // ******** Beginn Namensbereich Bsp:: template <typename T, int MAXSIZE> class Stack { private: T elems[MAXSIZE]; // Elemente int numElems; // aktuelle Anzahl eingetragener Elemente public: Stack(); // Konstruktor void push(const T&); // Element einkellern T pop(); // Element auskellern T top() const; // oberstes Element }; // Konstruktor template <typename T, int MAXSIZE> Stack<T,MAXSIZE>::Stack () : numElems(0) // kein Elemente { // nichts mehr zu tun } template <typename T, int MAXSIZE> void Stack<T,MAXSIZE>::push (const T& elem) { if (numElems == MAXSIZE) { throw "Stack<>::push(): der Stack ist voll"; } elems[numElems] = elem; // Element eintragen ++numElems; // Anzahl der Elemente erhöhen } template<typename T, int MAXSIZE> T Stack<T,MAXSIZE>::pop () { if (numElems <= 0) { throw "Stack<>::pop(): der Stack ist leer"; } --numElems; // Anzahl der Elemente herabsetzen return elems[numElems]; // bisheriges oberstes Element zurückliefern } template <typename T, int MAXSIZE> T Stack<T,MAXSIZE>::top () const { if (numElems <= 0) { throw "Stack<>::top(): der Stack ist leer"; } return elems[numElems-1]; // oberstes Element zurückliefern } } // ******** Ende Namensbereich Bsp::
stack4_usage.cpp
#include "stack4.hpp" using namespace Bsp; int main() { try{ Bsp::Stack<int,20> int20Stack; // Stack für 20 ints Bsp::Stack<int,40> int40Stack; // Stack für 40 ints Bsp::Stack<std::string,40> stringStack; // Stack für 40 Strings // Integer-Stack manipulieren int20Stack.push(7); std::cout << int20Stack.pop() << std::endl; // String-Stack manipulieren std::string s = "hallo"; stringStack.push(s); std::cout << stringStack.pop() << std::endl; std::cout << stringStack.pop() << std::endl; //Fehler, da leer } catch (const char* msg) { std::cerr << "Exception: " << msg << std::endl; return EXIT_FAILURE; } }
Das Schlüsselwort typename, das jetzt schon mehrfach benutzt wurde, bietet neben den bisherigen Möglichkeiten der Typisierung auch noch die Möglichkeit, zu einem allgemeinen Typ T --der z.B. für einen Container-Typ wie 'vector stehen kann-- auch noch einen SubType zu definieren, nämlich das zum jeweiligen container gehörige Iterator-Objekt 'iterator' oder 'const_iterator'. Dadurch kann man dann Variablen einführen, die auf diese speziellen Iterator-Typen bezug nehmen. Im nachfolgenden Beispiel wird davon Gebrauch gemacht:
typename T::const_iterator pos;
hier wird die Variable 'pos' als Objekt vom Type 'T::const_iterator' eingeführt und dann entsprechend benutzt.
ausgeben.hpp
#include <iostream> // Elemente eines STL-Containers ausgeben template <typename T> void ausgeben (const T& menge) { // Anzahl der Elemente ausgeben std::cout << "Anzahl Elemente: " << menge.size() << std::endl; // Elemente selbst ausgeben std::cout << "Elemente: "; typename T::const_iterator pos; for (pos=menge.begin(); pos!=menge.end(); ++pos) { std::cout << *pos << ' '; } std::cout << std::endl; }
START
7. Dynamische vs. Statische Polymorphie
7.1 Gebundene dynamische Polymorphie
Damit ist der Fall gemeint, dass man verschiedene Funktionen einführen kann, die auf eine bestimmte Basisklasse Bezug nehmen, aber dynamisch sind, d.h. erst während der Laufzeit wird entschieden, welchen konkreten Typen sie aktuell verarbeiten (siehe unten).
Die im nachfolgenden Beispiel benutzte Klasse 'GeoObj' wurde schon in VL10 eingeführt.
// beliebiges geometrisches Objekt zeichnen void zeichne (const GeoObj& obj) { obj.draw(); } // Abstand zwischen zwei geometrischen Objekten berechnen Koord abstand (const GeoObj& x1, const GeoObj& x2) { Koord a = x1.position() - x2.position(); return a.abs(); } // inhomogene Menge von geometrischen Objekten ausgeben void ausgeben (const std::vector<GeoObj*>& elems) { for (unsigned i=0; i<elems. size(); ++i) { elems[i]->draw(); } } int main() { Linie l; Kreis k, k1, k2; zeichne(l); // zeichne(GeoObj&) => Linie::draw() zeichne(k); // zeichne(GeoObj&) => Kreis::draw() abstand(k1,k2); // abstand(GeoObj&,GeoObj&) abstand(l,k); // abstand(GeoObj&,GeoObj&) std::vector<GeoObj*> menge; // inhomogene Menge menge.push_back(&l); // Linie einfügen menge.push_back(&k); // Kreis einfügen ausgeben(menge); // Menge ausgeben }
START
7.2 Ungebundene statische Polymorphie
Im Falle von Templates liegt keine feste Bindung an eine bestimmte Basisklasse vor, dafür wird aber die Zuordnung zur Kompilierzeit vorgenommen wird und daher inhomogene Mengen (siehe oben) nicht mehr möglich sind.
// beliebiges geometrisches Objekt zeichnen template <typename GeoObj> void zeichne (const GeoObj& obj) { obj.draw(); } // Abstand zwischen zwei geometrischen Objekten berechnen template <typename GeoObj1, typename GeoObj2> Koord abstand (const GeoObj1& x1, const GeoObj2& x2) { Koord a = x1.position() - x2.position(); return a.abs(); } // inhomogene Menge von geometrischen Objekten ausgeben template <typename GeoObj> void ausgeben (const std::vector<GeoObj>& elems) { for (unsigned i=0; i<elems.size(); ++i) { elems[i].draw(); } } int main() { Linie l; Kreis k; Kreis k1, k2; zeichne(l); // zeichne<Linie>(GeoObj&) => Linie::draw() zeichne(k); // zeichne<Kreis>(GeoObj&) => Kreis::draw() abstand(k1,k2); // abstand<Kreis,Kreis>(GeoObj&,GeoObj&) abstand(l,k); // abstand<Linie,Kreis>(GeoObj&,GeoObj&) //std::vector<GeoObj*> menge; // FEHLER: keine inhomogene Menge möglich std::vector<Linie> menge; // OK: homogene Menge menge.push_back(l); // Linie einfügen ausgeben(menge); // Menge ausgeben }
START
8. Templates in der Praxis
// Deklaration des Funktions-Templates max() template <typename T> const T& max (const T& a, const T& b); // Deklaration des Klassen-Templates Stack<> #include <vector> namespace Bsp { // ******** Beginn Namensbereich Bsp:: template <typename T> class Stack { private: std::vector<T> elems; // Elemente public: Stack(); // Konstruktor void push(const T&); // Element einkellern T pop(); // Element auskellern T top() const; // oberstes Element }; } // ******** Ende Namensbereich Bsp::
#include "expl1.hpp" // Definition des Funktions-Templates max() template <typename T> const T& max (const T& a, const T& b) { return (a > b ? a : b); } // Definition der Funktionen des Klassen-Templates Stack<> namespace Bsp { // ******** Beginn Namensbereich Bsp:: // Konstruktor template <typename T> Stack<T>::Stack () { // nichts mehr zu tun } template <typename T> void Stack<T>::push (const T& elem) { elems.push_back(elem); // Kopie einkellern } template<typename T> T Stack<T>::pop () { if (elems.empty()) { cerr << "Stack<>::pop(): der Stack ist leer"; } T elem = elems.back(); // oberstes Element merken elems.pop_back(); // oberstes Element auskellern return elem; // gemerktes oberstes Element zurückliefern } template <typename T> T Stack<T>::top () const { if (elems.empty()) { cerr <<< "Stack<>::top(): der Stack ist leer"; } return elems.back(); // oberstes Element als Kopie zurückliefern } } // ******** Ende Namensbereich Bsp::
#include <string> #include "expldef1.hpp" // benötigte Funktions-Templates explizit instanziieren template const int& max (const int&, const int&); // benötigte Klassen-Templates explizit instanziieren template Bsp::Stack<int>; template Bsp::Stack<std::string>;
#include <iostream> #include <string> #include <cstdlib> #include "expl1.hpp" int main() { Bsp::Stack<int> intStack; // Stack für Integer Bsp::Stack<std::string> stringStack; // Stack für Strings // Integer-Stack manipulieren intStack.push(7); intStack.push(max(intStack.top(),42)); // max() für ints std::cout << intStack.pop() << std::endl; // String-Stack manipulieren std::string s = "hallo"; stringStack.push(s); std::cout << stringStack.pop() << std::endl; std::cout << stringStack.pop() << std::endl; }
#include <string> #include "expldef1.hpp" using namespace Bsp; // benötigte Funktions-Templates explizit instanziieren template const int& max (const int&, const int&); // benötigte Funktionen von Stack<> für int explizit instanziieren template Bsp::Stack<int>::Stack(); template void Bsp::Stack<int>::push (const int&); template int Bsp::Stack<int>::top () const; template int Bsp::Stack<int>::pop (); // benötigte Funktionen von Stack<> für std::string explizit instanziieren // - top() wird nicht benötigt template Bsp::Stack<std::string>::Stack(); template void Bsp::Stack<std::string>::push (const std::string&); template std::string Bsp::Stack<std::string>::pop ();
START
9. Aufgaben
START