"
Konzept | Implementierung in C++ | in Smalltalk |
Klasse | class | class |
Instanz | Variable oder dynamisch erzeugtes Objekt | dynamisch erzeugtes Objekt |
Instanzvariable | data-member einer Klasse | instance variable einer Klasse |
Klassenvariable | static data-member einer Klasse
| class variable |
Methode und Message | member-function und deren Aufruf | Methode und deren Aufruf |
Klassenmethode | static member-function
| Klassenmethode und deren Aufruf |
Protokoll (Schnittstelle) | Deklaration einer Klasse | Deklaration einer Klasse |
Vererbung | abgeleitete Klassen (Einfach-, Mehrfachvererbung; beliebig viele Hierarchien) | abgeleitete Klassen (Einfachvererbung; eine Hierarchie) |
Abstrakte Klasse | Klasse mit rein virtuellen Methoden, ohne Instanzen | Klasse ohne Instanzen |
Parametrisierte Typen | Schablonen (Templates) | --- |
Polymorphismus | Überladen von Funktionen, Operatoren, virtuelle Methoden | Überladen von Funktionen, Operatoren, virtuelle Methoden |
Dynamisches Binden | Virtuelle Funktionen | alles ist dynamisch |
Zugriffsrechte auf Instanzvariablen und Methoden | private, protected, public Mechanismen | Instanzvariablen sind grundsätzlich private, Methoden public |
Klasse Person (1/2)
#ifndef Person_h
#define Person_h
class Person {
private:
int systemId;
protected:
char *name;
public:
int socSecNo;
Person(int socSecNo); // contructor
~Person(); // destructor
char* Name();
void Print();
int SocSecNo();
int GetUniqueId();
};
#endif
Standardmäßig (d.h ohne Angabe von private, protected oder
public) gelten alle Daten und Methoden einer Klasse als privat. Wie
arbeitet man nun mit dieser Klasse bzw. ihren Objekten?Person *p2; // p2 calls Person::Person; dynamically allocated
Person p(4711); // statically allocated
p2 = new Person(4712); // dynamically allocated
cout << "\nsystem id of p " << p.GetUniqueId() <<
"\n";
cout << "\nsystem id ofp2 " << p2->GetUniqueId()
<< "\n";
Man beachte: -> entspricht `*.', d.h p->GetUniqueId() entspricht
(*p).GetUniqueId().Konstruktoren und Destruktoren
Konstruktoren sind vergleichbar mit Klassenmethoden, die zur
Initialisierung automatisch aufgerufen werden. Konstruktoren können
Argumente erwarten, man kann aber keinen Rückgabedatentyp vereinbaren
(Rückgabedatentyp ist implizit ein Objekt der jeweiligen Klasse).
Wurde kein Konstruktor definiert, legt der Compiler automatisch einen
Default-Konstruktor an.Initialisierung bei Konstruktoren
Person::Person (): id((int)this) // andere Konstruktoren
{
//id = (int) this; zweite Möglichkeit der Zuweisung
}
int f(int i) { cout << i << " "; return i;}
class Foo {
int a,b,c;
public:
Foo() : a(f(1)), c(f(2)), b(f(3)){}
};
Foo foo; // gibt 1 3 2 aus
Destruktor
class IntStack {
int *contents;
int size;
init() { contents = new int[100]; size = 0;)
public:
IntStack() { init();}
~IntStack() { delete [] contents; size = 0;}
};
instances still in ObjectTable
class cnt size
=====================================
GapText 3 60
LineMark 6 28
OrdCollection 2 48
TypingCommand 1 52
-------------------------------------
Total 12
=====================================
Mem statistics
size alloc free diff alloc recycl freedchunks
=======================================================
2 15 12 3
8 81 76 5
24 37 35 2
28 28 22 6
30 8 6 2
40 200 189 11
44 89 86 3
48 228 226 2
50 1 0 1
52 17 16 1
60 155 152 3
72 18 16 2
-------------------------------------------------------
total: 218670 217254 1416
=======================================================
IntStack s;
s.push(1); s.push(2);
IntStack t(s); //entspricht IntStack t = s;
s.push(3);
t.push(0);
class IntStack {
int *contents;
int next; // gegnwärtige Anzahl der Elemente
...
};
...
IntStack t = s;
...
IntStack& operator = (const IntStack &s)
{
if (&s == this) return *this;
/* es müßte auch noch geprüft werden, ob *this den Inhalt von s
überhaupt aufnehmen kann; *this und s könnten ja unterschiedlich
groß sein! */
next = s.next;
for (int i=0; i < next; i++)
contents[i] = s.contents[i];
return *this;
}
IntStack t = s;
int IntStack operator == (const IntStack& t)
{
if (t.next != next) return 0; // unterschiedliche Größe
if (t.cont == cont) return 1; // Fall (1) oder (2)
for (int i=0; i<next; i++)
if (contents[i] != t.contents[i]) return 0;
return 1; // Fall (3)
}
Konstante Komponentenfunktion
class Foo {
protected:
int a;
public:
Foo() : a(1) { }
int TestConst() const; // const führt zu ...
};
int Foo::TestConst() const
{
return a++; //... error: increment of read-only member `int Foo::a'
}Die Definition wird allerdings nicht vererbt, also
class SubFoo : public Foo {
public:
int TestConst();
};
int SubFoo::TestConst()
{
return a++;
}ist erlaubt. Anwendung finden konstante Komponentenfunktionen vor allem bei der Deklaration von Schnittstellen.
static
werden
Instanzvariablen zu Klassenvariablen. Die Initialisierung muß dabei
außerhalb der Klassendefinition erfolgen. Das Auftreten innerhalb der
Klassendefinition wird nämlich als Deklaration angesehen; auch
würde durch die Verwendung des Definitionsteils, also des h-Files, in
mehreren Dateien eine Initialisierung mehrmals erfolgen. Anwendung finden
Klassenvariablen z.B. bei der Verwendung von Default-Werten.Klassenvariable für Personenklasse
Definition (h-File)
class Person {
private:
static int numberOfPersons;
...
};und Implementierung (cc-File)
#include "person.h"
int Person::numberOfPersons = 0;
Person::Person()
{
numberOfPersons++;
id = numberOfPersons;
...Konstante Klassenvariable
class Symbol;
class SGMLApplication : public Application {
static const Symbol cDocTypeSGML;
...und im cc-File
...
const Symbol SGMLApplication::cDocTypeSGML = "SGMLApplication";
...
static
Methoden.Klassenmethoden
class Stack {
static const int stacksize = 100; // Klassenvariable
public:
...
int SizeOfStack() {return stacksize;}
};Die Methode SizeOfStack() nimmt keinen Bezug auf eine Instanz der Klasse Stack, d.h. man könnte die Methode SizeOfStack() auch ohne die Existenz einer Instanz aufrufen.
Aber wie soll man sie ohne Instanz aufrufen?
int x = ???.SizeOfStack();Lösung: Definition der Methode SizeOfStack() als
static
:
...
public:
static int SizeOfStack() {return stacksize;}
...
};
und Aufruf durch direktes Referenzieren der Methode
int x = Stack::SizeOfStack();
friend-Attribut
class IntStack
...
friend int SizeOfStack();Obige Deklaration bedeutet, daß alle Methoden/Funktionen, die dem nach dem friend-Attribut angegebenen Protokoll übereinstimmen, auch auf private bzw. protected Member der Klasse IntStack zugreifen können. Das friend-Attribut kann auch für ganze Klassen vergeben werden
class A {
int i;
friend class B;
};
class B {
A a;
public:
B (int b) { a.i = b;}
};oder auch nur für eine bestimmte Member-Funktion
class Y {
friend char * X::Foo(int);
...
}Dieser Mechanismus, der das Geheimnisprinzip umgeht, sollte möglichst sparsam verwendet werden. Meist werden friend-functions für die Ein- und Ausgabe verwendet, also etwa
class IntStack
...
friend ostream& operator << (ostream &s, const IntStack &s);
...
};In diesem Fall wird dem operator << das friend-Attribut `verliehen', d.h. dieser Operator darf auf alle Member der Klasse IntStack zugreifen. Man beachte, daß in obigem Beispiel ein const IntStack als Parameter übergeben wird: so wird zwar das friend-Attribut an den ostream Operator << vergeben, dieser darf aber auf die Werte der Klasse IntStack nur lesenderweise zugreifen.
Ein- und Ausgabe sind die häufigste Anwendung des friend-Attributs. Das Problem entsteht dadurch, daß bei den Operatoren `<<` und `>>' der Stream immer links steht und rechts das Objekt das ausgegeben oder eingelesen werden soll. Um für neue Klassen dieselbe Ein-/Ausgabeform zu ermöglichen, müßte man daher auf Ebene der Klasse ostream bzw. istream eine Operatorfunktion für das neue Objekt definieren.
class ostream {
...
ostream& operator << (IntStack&);
...
};Im allgemeinen will man aber bestehende (teilweise vielleicht auch standardisierte) Klassen nicht ändern. Man umgeht das nun dadurch, daß man dem ostream operator << das friend-Attribut verleiht.
Das friend Attribut ist nicht vererbbar. Angenommen ...
class IntStack {
friend ostream& operator << (ostream& os, IntStack &stack);
int size;
...
};
class SubStack : public IntStack {
int testFriendship;
...
};gilt, dann darf man beispielsweise in einer main() Funktion folgendes implementieren:
ostream& operator << (ostream &o, IntStack &s)
{
...
o << s.size;
return o;
}nicht erlaubt ist aber
ostream& operator << (ostream &o, SubStack &s)
{
o << endl << s.testFriendship << endl; // private member
return o;
}Wichtig dabei ist, daß ja die Klasse SubStack von IntStack erbt, d.h man kann schon ein Objekt der Klasse SubStack mit Hilfe des Operators `<<` ausgeben. Man darf allerdings nicht auf die privaten Elemente von SubStack zugreifen.
Das friend-Attribut ist auch nicht transitiv, es kann also nicht weitergegeben werden. Wenn A friend von B ist und B friend von C ist, dann heißt das also nicht, daß auch A friend von C ist.
Das Freundschaftsrecht erhält man von einer Klasse, d.h. man kann es sicht nicht nehmen.
List *objList;
objList->Add(firstObj,anotherObj,yetAnotherObj,lastObj);
for (Iterator i(objList); i; i++) {
cout << *i;
}Anstelle des überladenen Operators `++' wird häufig auch die Methode `next()' verwendet.
Iteratoren kann man dabei innerhalb einer Klasse definieren, da sie nur im Zusammenhang mit einer Klasse Sinn machen. Diese Vorgangsweise hat auch den Vorteil, daß die Iterator Klasse für alle Container-Klassen immer `Iterator' heißt, weil sie ja nur lokal bekannt ist und daher keine Namenskonflikte auftreten.
class List {
...
class Iterator {
...
};
...
};Meist werden Iteratoren von einer (abstrakten) Basisklasse abgeleitet. Diese Basisklasse bietet die grundsätzliche Funktionalität für Iteratoren und hat in etwa folgendes Aussehen (beide Beispiele stammen von den ET++ Container-Klassen [5]):
class Iterator {
friend Container;
protected:
Iterator();
public:
~Iterator();
void Reset();
protected:
bool IsActive() { return (started && ! terminated); };
bool Start(); //calls virtual GetContainer()
void Terminate();
virtual Container *GetContainer();
...
protected:
bool Started() { return started; };
bool Terminated() { return terminated; };
private:
void Init() { next= this; prev= this; };
private:
bool started, terminated;
Iterator *next, *prev;
};
Eine davon abgeleitete Klasse für SetIter Sets könnte so
aussehen:class SetIter : public Iterator {
friend class Set;
public:
SetIter(Set *aSet);
~SetIter();
Object *operator()();
void Reset();
protected:
Container *GetContainer();
...
protected:
Set *set;
...
};
Abgeleitete Klassen
Vererbung ist ein Mittel der Implementierung von Spezialisierung. Im
folgenden Beispiel wird eine Unterklasse Student als Spezialisierung der
Klasse Person implementiert.#ifndef Student_h
#define Student_h
#include "person.h"
class Student: public Person {
protected:
char *name;
public:
Student(int);
~Student();
void Print(ostream &o = cout);
};
#endif
und das cc-File#include <iostream.h>
#include "student.h"
Student::Student(int no) : name("unnamed"), Person(no)
{
name = "unnamed"; //andere Initialisierungsmöglichkeit
}
Student::~Student()
{
}
void Student::Print(ostream &o)
{
o << endl << this->name;
}
Die Instanzvariable name wird mit der konstanten Zeichenkette "unnamed"
initialisiert. Der übergebene int-Parameter no wird an den
Konstruktor der Oberklasse Person weitergereicht. Man könnte die
Instanzvariable name auch im Block des Konstruktors initialisieren.
Art der Ableitung / Attribut einer Komponente der Basisklasse | public | protected | private |
public | public | protected | private |
protected | protected | protected | private |
private | private | private | private |
Man beachte, daß statische Komponenten ohne Änderung ihrer
Eigenschaften von der Basisklasse übernommen werden.
private Vererbung
class BasisStack {
const int stacksize;
void** contents;
int next;
protected:
BasisStack(int s) : stacksize(s), contents(new void*[s]), next(0)
{}
~BasisStack() { delete contents;}
BasisStack& Push(void *p) { contents[next++] = p; return *this;}
BasisStack& Pop() { next--; return *this;}
void* Top() const { return contents[next-1];}
int Size() const { return next;}
int IsEmpty () const { return next == 0;}
};
Von dieser Basisklasse wird ein abgeleitetes Template für Stacks
definiert, das private erbt ...#include "basisstack.h"
template <class ElType, int size>
class Stack: private BasisStack {
public:
Stack(): BasisStack(size) {};
Stack<ElType, size>& Push(const ElType &element)
{BasisStack::Push((void*)&element); return *this;}
Stack<ElType, size>& Pop() { BasisStack::Pop(); return
*this;}
ElType& Top() const { return *(ElType*)BasisStack::Top();}
int Size() const { return BasisStack::Size();}
int IsEmpty() const { return BasisStack::IsEmpty();}
};
Unterklassen von Stack können somit nicht mehr direkt auf die
protected Methoden von BasisStack zugreifen. Der Zugriff muß über
die Klasse Stack erfolgen. Hier sind alle Methoden public, sodaß auch
fremde Klassen Stackfunktionalität nutzen können.SecureStack& SecureStack::Push(int element)
{
if (size < stackSize)
IntStack::Push(element);
return *this;
}
Durch IntStack::Push() wird die Methode Push() der Oberklasse IntStack
aufgerufen (sonst Rekursion!).Student::Student(int no) : name("initName"), Person(no)
{
name = "unnamedbutwithid";
}
wobei als Konstruktor ein beliebiger Konstruktor der Basisklasse
aufgerufen werden kann.Konversionen zwischen Unterklassen und Basisklassen
class B { int i;};
class D: public B { int j;};
B b;
D d;
d = b; // geht nicht
b = d;
OSystem *os = new OSystem("Macintosh");
Window *w = new Window(eMotifStyle);
os= (OSystem*)w;
istream& operator >> (istream& in, IntStack& s)
{
char c;
int element;
...
in >> c;
while (c != `>')
in.Putback(c); // zurücklesen von `c'
in >> element;
s.Push(element);
...
}
return in;
}
class Object {
public:
virtual char *Print() {return "\nObject::Print()\n";};
};
class DerivedA: public Object {
public:
char *Print() {return "\nDerivedA::Print()\n";};
};
class DerivedB: public Object {
public:
char *Print() {return "\nDerivedB::Print()\n";};
};
void main()
{
Object *o;
o = new Object();
cout << "\no->Print() results in " << o->Print();
delete o;
o = new DerivedA();
cout << "\no->Print() results in " << o->Print();
delete o;
o = new DerivedB();
cout << "\no->Print() results in " << o->Print();
delete o;
}
o->Print() results in
Object::Print()
o->Print() results in
DerivedA::Print()
o->Print() results in
DerivedB::Print()
class Base {
virtual void f() { cout << "\nBase::f() ";}
public:
Base () {cout << "\nBase::Base() "; f();}
};
class Derived: public Base {
void f() { cout << "\nDerived::f() ";}
};
main()
{
Derived d;
}
Base::Base() Base::f()
class ABC {
protected:
virtual int f() = 0; // auch: = NULL;
};
class Derived: public ABC {};
main()
{
Derived *d = new Derived();
d->ABC::f();
}
> cannot allocate an object of type \QDerived' since the following virtual functions are abstract:
> main.cc:15: int ABC::f()
class A {
public:
virutal void f();
};
class B {
public:
virtual void f();
};
class C : public A, public B {
public:
void f();
};
C *pc = new C();
A *pa = pc; B *pb = pc;
pa->f();
pb->f();
pc->f();
Die Implementierung der Klasse Studienassistent in obiger Hierarchie könnte in C++ folgendermaßen ausschauen:
class Studienassistent: public Assistent, public Student {};Mehrfachvererbung und Fenstersysteme
class Window {...};
class XWindow : public Window {
public:
void Scroll();
void Clear();
...
};
class EditWindow : public Window {
int topLineVal, bottomLineVal;
Editor *e;
...
};
class XEditWindow : public EditWindow, public XWindow { ...};In obigem Beispiel erbt die Klasse XEditWindow von der Klasse XWindow hauptsächlich systemspezifische Funktionalität (Scroll(), Clear()) und von der Klasse EditWindow hauptsächlich Daten (topLineVal, ...).
Folgende Probleme können bei Mehrfachvererbung auftreten: ein Studienassistent erbt zwei mal name, nämlich von Student und von Assistent. name kann dabei
Mit Hilfe des Bereichsoperators kann man auf die gewünschte Komponente name zugreifen, also entweder auf name von Assistent oder von Student.
cout << Assistent::name << " " << Student::name;Ad 2) Virtuelle Basisklassen
Wird im Zuge einer Ableitung eine Basisklasse durch das Schlüsselwort virtual als virtuell erklärt, wird in den folgenden Ableitungen nur ein einziges Exemplar dieser Basisklasse übernommen, auch wenn sie über mehrere Pfade erreicht werden kann.
Virtuelle Ableitung bei Mehrfachvererbung
class Bediensteter: virtual public Person{...};
class Student: virtual public Person {...};
class Studienassistent: virtual public Assistent, virtual public Student {
public:
void Print(ostream &o) { o << name << endl;}
};Voraussetzung ist allerdings, daß die Basisklasse in allen Unterklassen als virtuell erklärt wird:
class B { protected: int x; };
class D1: public virtual B {...};
class D2: public virtual B {...};
class DD: public D1, public D2, public B{...};DD erbt hier zweimal! Virtualität ist also keine Eigenschaft der Klasse, sondern eine Eigenschaft der Ableitung!
Achtung: die virtuelle Ableitung hilft nicht, wenn man von zwei Basisklassen erbt, die keine gemeinsame Wurzel haben:
In obigem Beispiel erbt DD zweimal die Komponente x, einmal von B und einmal von C.
class B { public: int x;};
class D1: public virtual B {};
class D2: public virtual B {};
class C { public: int x;};
class DD: virtual public D1, virtual public D2, public C {};
int main(int argc, char *argv[], char *envp[])
{
...
DD d;
d.x = 4711; // request for member 'x' is ambiguous
...
}Die Komponente X wird über D1 und D2 zwar nur einmal geerbt, allerdings hat ja auch C eine Komponente x. Virtuelle Vererbung hilft hier nichts.
Die Konstruktoren werden in der Reihenfolge ihrer Ableitung aufgerufen. Diese Reihenfolge kann nicht geändert werden.
Einige Besonderheiten treten bei virtueller Mehrfachvererbung auf. Gemäß Ellis/Stroustroup [1] werden virtuelle Basisklassen als erste initialisiert; und weiters wird der sogenannte memory-initializer, also die Initialisierungsangaben beim Konstruktor, der am tiefsten abgeleiteten Klasse verwendet (falls er definiert ist, sonst muß ein Defaultkonstruktor in der virtuellen Basisklasse bestehen. Folgendes Beispiel soll diesen Zusammenhang verdeutlichen.
Virtuelle Mehrfachvererbung und Konstruktorprobleme
class B {
public:
int x;
int y;
public:
B(int xx) : x(xx), y(xx) {cout << "\nB::B() " << xx
<< endl;}
virtual void f() { cout << "\nB::f()\n";}
int GetX() { return x;}
void SetX(int i) { x = i;}
};
class D1: virtual B {
public:
D1(): B((int)this) { cout << "\nD1::D1() " <<
(int)this << "\n";}
B::x;
B::y;
};
class D2: virtual B {
public:
D2(): B((int)this) { cout << "\nD2::D2() " <<
(int)this << "\n";}
void f() { cout << "\nD2::f()\n"; };
};
class C {
public:
int y;
};
Eine Basisklasse B wird definiert, von der D1 und D2 virtuell und private
erben. Damit daher auf die Instanzvariablen x und y in B zugegriffen
werden kann, müssen diese beiden erneut als public definiert
werden....
private:
B::y;
...
funktioniert allerdings nicht, weil man das Zugriffsrecht, das ja in der
Basisklasse schon als public definiert wurde, nicht auf private
rückdefinieren kann.class DD: virtual public D1, virtual public D2, virtual public C {
public:
// DD() : B(5666) {} //eigener constructor, der den konstruktor
//der basisklasse explizit spezifiziert
DD() : D2(), D1(), B(5666) { }
};
Besonders interessant ist hierbei der Konstruktor bzw. das
memory-initialize Statement (D2(), D1(), B(5666)). Wird dieses Statement
nicht definiert, so nimmt er Compiler defaultmäßig D2(), D1(),
B() an, wobei aber der Konstruktor B() nicht definiert ist. Andererseits
muß aber die virtuelle Basisklasse vor den abgeleiteten initialisiert
werden. Man hat somit zwei Möglichkeiten: entweder man definiert
einen Defaultkonstruktor (das kann auch ein Konstruktor mit lauter
defaultmäßig vorbelegten Parametern sein) oder man spezifiziert
das memory-initializer Statement in der tiefsten abgeleiteten Klasse, also
die, von der die Initalisierung ausgeht (in obigem Beispiel DD). Das
Statement lautet D2(), D1(), B(5666). Die Reihenfolge der Initialisierung
ist übrigens unabhängig von der Spezifikation im
memory-initializer Statement, sonder wird rein durch die Reihenfolge der
Ableitung definiert. Vertauschen der Reihenfolge von D1, D2 und B hilft
nichts!...
DD d;
B b(100);
C c;
c.y = 47;
b.SetX(33);
d.D1::x = 4712;
direkter Zugriff auf Variable x in D1 d.D2::x = 4712;
geht aber nicht, weil x in D2 private ist bzw. nur in D1 als public
redefiniert wurde.d.y = 4711;
ist nicht eindeutig ("ambigous"), weil ja auch von C eine Variable x
geerbt wird; daher d.D1::y = 4711;
Das Statement d.f();
resultiert inD2::f()
weil bei virtuellen Methoden und Mehrfachvererbung das am nächsten
liegende f() aufgerufen wird (siehe auch "Virtuelle Methoden und Mehrfachvererbung"
auf Seite 65).
Templates/Schablonen
Schablonen können als Meta-Funktionen aufgefaßt werden, die zur
Übersetzungszeit neue Klassen bzw. neue Funktionen erzeugen.Stack<Window> windowStack;
Stack<int> intStack;
windowStack.Push(editorW);
windowStack.Push(printDialogW);
intStack.Push(4711);
intStack.Push(33);
Stack-Template
template <class ElType, int stacksize>
class Stack {
ElType contents[stacksize];
int size;
public:
Stack() : size(0) {}
virtual ~Stack() {}
virtual Stack<ElType, stacksize>& Push (const ElType& el) {
contents[size++] = el; return *this; }
Stack<ElType,stacksize>& Pop() { size--; return *this;}
ElType Top() const {return contents[size-1];}
int Size() const { return next;}
int IsEmpty () const { return size == 0;}
};Die Definition der Schablone Stack erfolgt durch das Schlüsselwort template sowie eine Liste von formalen Parametern in spitzen Klammern.
Die Methode Pop() als
ElType& Pop() { next--; return *this;}zu definieren geht deshalb nicht, weil Pop() so definiert werden soll, daß eine Instanz der Klasse Stack<ElType,stacksize> zurückgegeben werden soll und nicht der Parameter ElType; dieser Parameter kann ja zur Laufzeit beispielsweise ein Integer sein und dann müßte *this in einen Integer umgewandelt werden.
Auch folgende Variante scheidet aus:
Stack& Pop() { next--; return *this;}
Obiges Beispiel funktioniert insofern nicht, als in diesem Fall ein Stack Objekt zurückgegeben würde und nicht eines von dem Typ, der durch die Parameter instantiiert wurde (Stack<ElType,stacksize>).
Die Verwendung des Stack-templates in main.cc könnte etwa folgendermaßen aussehen:
Stack<int, 100> si;
Stack<float, 100> sf;
typedef Stack<double, 100> DoubleStack;
DoubleStack sd;
si.Push(10);
sf.Push(3.14);
sf.Push(5.14);
sd.Push(1.7E308);
cout << "\nsi Top " << si.Top() << endl;
cout << "\nsf Top " << sf.Top() << endl;
sf.Pop();
cout << "\nsf Top " << sf.Top() << endl;
cout << "\nsd Top " << sd.Top() << endl;ElType und stacksize sind formale, typbehaftete Parameter des Templates. Klassen Parameter symbolisieren einen Typnamen (aber nicht notwendigerweise eine Klasse!). Der Compiler erzeugt zur Übersetzungszeit automatisch die entsprechenden Klassendefinitionen.
Templates können auch bei der Vererbung verwendet werden. In untenstehendem Beispiel ist einmal eine Klasse Derived als Template von einem template-Stack abgeleitet und ein anderes mal eine Klasse DerivedWithoutTemplate, die als einfache Klasse vom template-Stack abgeleitet wurde:
#include "stack.h"
template <class dStack>
class Derived: public Stack<dStack, 100> {
public:
Derived() : Stack<dStack,100>() { ...;}
...
};
class DerivedWithoutTemplate: public Stack<int,100> {
public:
DerivedWithoutTemplate() { ...;}
~DerivedWithoutTemplate() { ...;}
...
};In der Unterklasse von Stack mit Template, also Derived, wird ein formaler Klassenparameter erwartet, der an das Template der Oberklasse (Stack) weitergegeben wird. Der zweite formale Parameter der Klasse Stack (int) wird einfach mit dem Standardwert 100 belegt.
Zwei parametrisierte Klassennamen sind dann gleich, wenn die Schablonen ident sind und die Argumente dieselben Werte aufweisen, also etwa Stack<int, 100> und Stack<int, 10*10>.
Was ist der Preis von Templates? Jede Instanziierung einer Klassenschablone erzeugt natürlich neuen Code für alle Methoden der Klasse. Einen möglichen Ausweg stellt die Implementierung von Schablonen durch Ableitung von generischen Klassen dar. Aufwendiger Code wird dadurch nicht dupliziert; trotzdem bleibt aber die Typsicherheit gewährleistet (siehe auch Beispiel "private Vererbung" auf Seite 60).
template <class T>
T max (int n_arg, T a, T b, ...);
double m, x, y, z;
double m = max(3, x, y, z);
static
sind möglich. Sie
müssen nach dem Schlüsselwort template angegeben werden:
template <class T>
inline T swap(T &first, T &second);
template <class ElemType>
ElemType minimum(ElemType elemField[], int fieldSize)
{
int i;
int min = 0;
for (i=1; i< fieldSize; i++) {
if (elemField[i] < elemField[min]) min = i;
}
return elemField[min];
}
int main()
{
int iF[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 1000};
double dF[] = {1.3, 0.3, 55.5};
cout << "\nMinimum: ";
cout << endl << minimum(iF, (int)(sizeof(iF)/sizeof(int)));
cout << endl << minimum(dF, (int)(sizeof(dF)/sizeof(double)));
}
template <class ElType, int s>
Stack<ElType, s>::Stack() : next(0) {}
template <class ElType, int s>
Stack<ElType, s>::Stack<ElType, s>() : next(0) {}
Bedenken sollte man dabei, daß nicht jeder Fehler automatisch eine Ausnahmebehandlung bedeutet: Ausnahmen sind nur jene Fehler, die an jeder Stelle im Programm zu berücksichtigen zu aufwendig wäre, also z.B. ein Hardware-Fehler beim Schreiben auf eine Festplatte. So ein Fehler kann potentiell überall auftreten, der Programmcode würde aber unleserlich und auch fehleranfällig (sic!), wenn man bei jedem Statement einen möglichen Plattencrash berücksichtigen würde.
Realisiert werden Ausnahmebehandlungen in C++ durch die Schlüsselworte throw, catch und try.
Folgendes Beispiel eines einfachen Kommandointerpreters soll die Verwendung von Ausnahmebehandlungen in C++ verdeutlichen. Die grundsätzliche Funktionsweise eines Kommandointerpreters ist dabei, von einem Eingabestrom Kommandos zu lesen und dann die entsprechende Methode aufzurufen.
Exception Handling und Kommandointerpreter
class Command {
public:
static Command* ReadAndCreate(istream&);
virtual void Perform () = 0;
virtual ~Command();
};
class Copy : public Command {
public:
void Perform();
~Copy();
};
//... other commands
int main()
{
for (;;) {
cout << "\nnächster Befehl: ";
Command *c = ReadAndCreate(cin);
if (c != 0) {
c->Perform();
delete c;
}
}
};Implementierung von Klassen für die Ausnahmebehandlung ...
class DiskFull{};
class DeviceNotReady {
public:
const char *address;
DeviceNotReady(const char *addr) : address(addr) {}
};... die neue Implementierung von main.cc ...
...
Command *c = ReadAndCreate(cin);
if (c != 0) {
try {
c->Perform();
}
catch(DiskFull& d) {
cout << "\nNo Space left on device!";
}
catch(DeviceNotReady& dvnr) {
cout << "\nDevice with address " << dvnr.address;
cout << " does not answer";
}
delete c;
}
...... und die Implementierung von Copy::Perform() ...
void Copy::Perform()
{
File *file = new File(); // im Falle einer Ausnahme wird
// wird der Destructor von file auf-
// gerufen
if (write(file, buf, bufsize) < bufsize)
throw DiskFull();
...Im Falle der Ausnahmebehandlung wird die catch-Routine des nächsten darüberliegenden (= aufrufenden) Blockes aktiviert. Der Vorteil von Ausnahmebehandlungen etwa gegenüber dem setjmp() von C liegt darin, daß alle angelegten Variablen ordnungsgemäß (d.h. mit Destruktor) wieder gelöscht werden (z.B. file im obigen Beispiel). Nach der catch-Routine wird an den auf den try-Block folgenden Block weiterverzweigt und nicht an die throw-Routine weitergeleitet.
Perform() throw(Diskfull, DeviceNotReady(char* addr));
Perform() throw();
g++ -g -fhandle-exceptions -g -fhandle-exceptions -c ncommand.cc
ncommand.cc: In method \Qvoid Dummy::Perform()':
ncommand.cc:20: declaration of \QDummy::Perform()' throws different exceptions...
ncommand.h:18: ...from previous declaration here
make: *** [ncommand.o] Error 1
typedef void (*func)();
void aetsch() { cout << "\naetsch!\n"; throw;}
...
func orig = (set_unexpected(aetsch);
...
try {f();}
catch(...) { cout <<"\nHoppala!\n";}
set_unexpected(orig);
class Animal {
public:
Animal();
virtual ~Animal();
virtual void DoSomething() = 0; // abstract virtual method
};
class Bird : public Animal {
public:
Bird();
~Bird();
void DoSomething();
};
class Dog : public Animal {
public:
Dog();
~Dog();
void DoSomething();
};
void process()
{
char command = `n';
while(toupper(command) != `Y') {
Animal *a = CreateAnimal();
a->DoSomething();
delete a;
cout << "\nQuit?[y/n]\t";
cin >> command;
}
};
void process()
{
char command = `n';
while(toupper(command) != `Y') {
Animal *a = CreateAnimal();
try {
a->DoSomething();
}
catch (...) { //catch any exception
delete a;
throw;
}
delete a;
cout << "\nQuit?[y/n]\t";
cin >> command;
}
};
template<class T>
class auto_ptr {
public:
auto_ptr(T *p = 0): ptr(p) {}
~auto_ptr() {delete ptr;}
private:
T *ptr;
};
void process()
{
char command = `n';
while(toupper(command) != `Y') {
auto_ptr<Animal> a = CreateAnimal();
a->DoSomething();
cout << "\nQuit?[y/n]\t";
cin >> command;
}
};
class Image {
String name;
public:
Image(String &n) : name(n) {};
};
class Audio {
String name;
public:
Audio(String &n) : name(n) {};
};
class BookEntry {
String name;
Image *image;
Audio *audio;
public:
BookEntry(String &n, String &i = "", String &a= "");
~BookEntry() {};
};
BookEntry::BookEntry(String &n, String &i, String &a) : name(n), image(0), audio(0)
{
name = n;
if (i != "") image = new Image(i);
if (a != "") audio = new Audio(a); // throws exception
}
BookEntry::~BookEntry()
{
delete image;
delete audio;
}
if (a!= "") audio = new Audio(a);
BookEntry *bePtr;
try {
bePtr = new BookEntry(...);
}
catch (...) {
delete bePtr; throw;
}
...
BookEntry::BookEntry(String &n, String &i, String &a) : name(n), image(0), audio(0)
{
name = n;
try {
if (i != "") image = new Image(i);
if (a != "") audio = new Audio(a);
}
catch (...) {
delete image; // clean-up
delete audio;
throw; // propagate exception
}
}
class BookEntry {
Image *image;
Audio *audio;
...
};
class BookEntry {
auto_ptr<Image> image;
auto_ptr<Audio> audio;
...
};
class Session {
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
public:
Session();
~Session();
};
Session::~Session()
{
logDestruction(this);
}
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) {
cerr << "\nUnable to log destruction of " << this << "!\n";
}
}
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) {// no propagation
}
}
Session::Session()
{
logCreation(this);
beginTransaction();
}
...
Session::~Session()
{
logDestruction(this); // throws exception
endTransaction();
}
istream operator >> (istream &is, Widget &w);
...
void PassAndThrowWidget()
{
Widget localWidget;
cin >> localWidget;
throw localWidget;
}
void PassAndThrowWidget()
{
static Widget localWidget;
...
throw localWidget;
}
double sqrt(double);
int i = 4;
double sqrtOfInt = sqrt(i);
void f(int i)
{
try {
doSomething(i); // doSomething throws int i
}
catch (double d)
{
...
}
...
}
class Object {};
class SubObject : public Object {};
void f(SubObject &so)
{
try {
doSomething(so); // doSomething throws SubObject so
}
catch (Object &o) // so fits here!
{
...
}
catch (SubObject &sub)
{
...
}
...
}
X *x = new X();
X xx, yy;
cout << "\nx = " << typeid(x).name() << endl;
cout << "\nx = " << typeid(*x).name() << endl;
cout << "\nxx = " << typeid(xx).name() << endl;
cout << "\nxx==yy = " << (typeid(xx) == typeid(yy)) << endl;
x = (null) // x is pointer
x = TD$1X // x*
xx = TD$Fv_1X // xx is of type X
xx == yy 1 // type of xx is type of yy
whcar_type MyWideString[] = "some international text goes here";
MyMacro(1, 2); // error
MyMacro(1, 4) // works
int i = 4711;
double d = 55555555;
d = double(i); // geht unter Unix teilweise nicht
d = (double)i;
case
...
default:
int Number = 5;
...
break;
...
default: {
int Number = 7;
...
}
break;
typedef struct struct1 {
int one;
struct struct2 {
int two;
int three;
} a;
int four;
} struct1_type;
struct1_type foo;
foo.a.two = 100;
//bei PC auch: foo.two = 100;
foo.a;
foo.a();
HITZ Martin, "C++, Grundlagen und Programmierung". Springer Verlag 1992.
MEYERS Scott, "How to Navigate the Treacherous Waters of C++ Exception Handling", Microsoft Systems Journal, November 1995.
MÜLLER Harald M., "Ten Rules for Handling Exception Handling Successfully", C++ Report, January 1996, pp. 23--36.
WEINAND A., GAMMA E., MARTY R. "Design and Implementation of ET++, a Seamless Object-Oreinted Application Framework", Structured Programming, Vol. 10, No. 2, Springer-Verlag 1989.
WISE G. Bowden, "An Overview of The Standard Template Library", ACM Crossroads 2.3, Februar 1996.