|
|
|
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