Sie sind auf Seite 1von 179

Skript zur Vorlesung

Objektorientierte Programmierung

Teil 1: Programmieren in C++

Teil 2: Datenstrukturen und Algorithmen in C++





von

Erich Eich

HS Mannheim

Fakultt Informationstechnik





Stand 25.07.2014











Inhaltsverzeichnis

Teil 1 Programmieren in C++ ................................................................................................. 1
1. Einfhrung ............................................................................................................................. 1
1.1 Grundbegriffe der objektorientierten Programmierung .................................................... 1
1.2 Historie der objektorientierten Programmierung .............................................................. 3
1.3 Notation der Syntax .......................................................................................................... 4
2. Spracherweiterungen gegenber C ........................................................................................ 5
2.1 Kommentare ..................................................................................................................... 5
2.2 Der Datentyp bool ............................................................................................................ 5
2.3 Strukturen und Aufzhlungstypen .................................................................................... 6
2.4 Variablendefinition ........................................................................................................... 6
2.5 Modifikation mit const ..................................................................................................... 6
2.6 Referenzen ........................................................................................................................ 7
2.7 berladen von Funktionen ............................................................................................... 9
2.8 Default-Werte fr Funktionsparameter .......................................................................... 10
2.9 Inline-Funktionen ........................................................................................................... 10
2.10 Der Bereichsoperator :: ................................................................................................. 11
2.11 Namensrume ............................................................................................................... 11
2.12 Freispeicherverwaltung ................................................................................................ 14
2.13 Einfache Ein-/Ausgabe ................................................................................................. 15
2.14 bungsaufgaben ........................................................................................................... 17
3. Klassen und Objekte ............................................................................................................ 18
3.1 Klassen............................................................................................................................ 18
3.1.1 Klassendeklaration ................................................................................................... 18
3.1.2 Objekterzeugung und Komponentenzugriff ............................................................. 20
3.1.3 Elementfunktionen .................................................................................................. 20
3.1.4 Konstruktoren ........................................................................................................... 22
3.1.5 Elementinitialisierungsliste ...................................................................................... 23
3.1.6 Destruktoren ............................................................................................................. 24
3.2 Objekte............................................................................................................................ 24
3.2.1 Globale, statische und automatische Objekte ........................................................... 25
3.2.2 Dynamische Objekte ................................................................................................ 26
3.3 Komposition von Objekten ............................................................................................. 27
3.3.1 Elementobjekte ......................................................................................................... 27
3.3.2 Zeiger auf Objekte als Komponenten ....................................................................... 29
3.3.3 Felder von Objekten ................................................................................................. 30
3.3.4 Dynamisch allokierte Felder von Objekten .............................................................. 31
3.4 Geschachtelte Klassen .................................................................................................... 33
3.5 Friends ............................................................................................................................ 34
3.6 Statische Komponenten .................................................................................................. 35
3.7 bungsaufgaben ............................................................................................................. 37
4. Vererbung ............................................................................................................................ 39
4.1 Abgeleitete Klassen ........................................................................................................ 39
4.2 Zugriffskontrolle ............................................................................................................. 43
4.3 Konvertierbarkeit von Zeigern ....................................................................................... 44
4.4 Virtuelle Funktionen ....................................................................................................... 46
4.4.1 Sptes Binden ........................................................................................................... 47
4.4.2 Abstrakte Klassen ..................................................................................................... 49
4.4.3 Ein Praxisbeispiel ..................................................................................................... 50
4.5 Mehrfachvererbung ........................................................................................................ 53

4.5.1 Mehrdeutigkeiten durch Namenskonflikte ............................................................... 54
4.5.2 Mehrdeutigkeiten durch wiederholtes Vererben ...................................................... 54
4.5.3 Virtuelle Basisklassen .............................................................................................. 56
4.6 bungsaufgaben ............................................................................................................. 58
5. berladen von Operatoren ................................................................................................... 60
5.1 Grundlagen ..................................................................................................................... 60
5.3 friend-Funktionen oder Elementfunktionen ................................................................... 65
5.4 berladen des Index-Operators ...................................................................................... 66
5.5 Zuweisung und Initialisierung ........................................................................................ 67
5.5.1 Zuweisung ................................................................................................................ 67
5.5.2 Initialisierung ........................................................................................................... 68
5.6 bungsaufgaben ............................................................................................................. 70
6. Templates ............................................................................................................................. 71
6.1 Funktions-Templates ...................................................................................................... 71
6.2 Klassen-Templates.......................................................................................................... 74
6.3 bungsaufgaben ............................................................................................................. 76
7. Exceptions ............................................................................................................................ 77
7.1 Exception-Mechanismus ................................................................................................ 77
7.2 Exception Spezifikation ................................................................................................. 80
7.3 Stack-Abwicklung .......................................................................................................... 81
7.4 bungsaufgaben ............................................................................................................. 83
8. Die E/A-Bibliothek ............................................................................................................. 84
8.1 Stream-Klassen ............................................................................................................... 84
8.2 Standard-Ein/Ausgabe von eingebauten Typen ............................................................. 85
8.2.1 Ausgabe .................................................................................................................... 85
8.2.2 Eingabe ..................................................................................................................... 86
8.3 Stream-Status .................................................................................................................. 87
8.4 Standard-Ein-/Ausgabe von benutzerdefinierten Typen ................................................ 88
8.4.1 Ausgabe .................................................................................................................... 88
8.4.2 Eingabe ..................................................................................................................... 89
8.5 Formatierung .................................................................................................................. 91
8.5.1 Formatierung durch Manipulation des Stream-Objekts ........................................... 91
8.5.2 Formatierung mittels Manipulatoren ........................................................................ 93
8.6 Dateien und Streams ....................................................................................................... 97
8.6.1 ffnen von Dateien .................................................................................................. 97
8.6.2 Schlieen von Dateien .............................................................................................. 98
8.7 bungsaufgaben ........................................................................................................... 100

Teil 2 Datenstrukturen und Algorithmen in C++ ................................................................ 101

9. Datentypen und Datenstrukturen ....................................................................................... 101
9.1 Datentypen .................................................................................................................... 101
9.2 Datenstrukturen ............................................................................................................ 101
9.3 Standarddatenstrukturen ............................................................................................... 102
10. Sequenziell gespeicherte lineare Listen ........................................................................... 104
10.1 Definition .................................................................................................................... 104
10.2 Abstraktion ................................................................................................................. 105
10.3 Implementierung ......................................................................................................... 106
10.4 Suchen ........................................................................................................................ 109
10.4.1 Komplexitt von Algorithmen ............................................................................. 111
10.4.2 Komplexitt der Suchalgorithmen ....................................................................... 112
10.5 Sortieren...................................................................................................................... 113

10.5. 1 Quicksort ............................................................................................................. 113
10.6 bungen...................................................................................................................... 116
11. Verkettet gespeicherte lineare Listen ............................................................................... 118
11.1 Speicherung in einem Array ....................................................................................... 118
11.2 Speicherung in dynamischen Variablen ..................................................................... 119
11.2.1 Abstraktion ........................................................................................................... 119
11.2.2 Implementierung .................................................................................................. 120
11.3 Doppelt verkettete Listen............................................................................................ 123
11.4 bungsaufgaben ......................................................................................................... 124
12. Spezielle Listen ................................................................................................................ 125
12.1 Stapel .......................................................................................................................... 125
12.1.1 Abstraktion ........................................................................................................... 125
12.1.2 Implementierung .................................................................................................. 126
12.2 Schlangen.................................................................................................................... 128
12.2.1 Abstraktion ........................................................................................................... 128
12.2.2 Implementierung .................................................................................................. 129
12.3 bungsaufgaben ......................................................................................................... 131
13. Bume .............................................................................................................................. 132
13.1 Grundlagen ................................................................................................................. 132
13.2 Suchbume.................................................................................................................. 133
13.2.1 Abstraktion ........................................................................................................... 133
13.2.2 Implementierung .................................................................................................. 134
13.2 Traversieren von Binrbumen .................................................................................. 139
13.3 bungsaufgaben ......................................................................................................... 141
14. Graphen ............................................................................................................................ 142
14.1 Grundlagen ................................................................................................................. 142
14.2 Wegesuche .................................................................................................................. 143
14.3 Abstraktion ................................................................................................................. 144
14.4 Implementierung ......................................................................................................... 144
14.4 bungsaufgaben ......................................................................................................... 148
15 Die Standard-Template Library (STL) ............................................................................. 149
15.1 Konzept der STL ........................................................................................................ 149
15.2 Container .................................................................................................................... 151
15.3 Iteratoren .................................................................................................................... 153
15.4 Algorithmen ................................................................................................................ 155
15.5 bungsaufgaben ......................................................................................................... 160
16. Datenstrukturen und Dateien ........................................................................................... 161
16.1 Persistenz und Serialisierung ...................................................................................... 161
16.2 Gestreute Speicherung (Hashverfahren) ..................................................................... 163
16.2.1 Abstraktion ........................................................................................................... 165
16.2.2 Implementierung .................................................................................................. 166
16.3 bungsaufgaben ......................................................................................................... 170
Literatur.................................................................................................................................. 171
Index ...................................................................................................................................... 172




1
Teil 1 Programmieren in C++

1. Einfhrung

Die objektorientierte Programmierung (OOP) hat sich in den vergangenen Jahren zur do-
minierenden Programmiertechnik entwickelt. Hierbei spielt die Programmiersprache C++
eine wesentliche Rolle. Objektorientierten Programmen wird ein besonderer Grad an Wohl-
strukturiertheit und Erweiterbarkeit nachgesagt. Um diese Qualittseigenschaften zu errei-
chen, ist ein guter Programmentwurf eine unabdingbare Voraussetzung.

Die objektorientierte Programmierung bringt neue Denkweisen und Programmiertechniken
mit sich, die sich zunchst in einer Reihe neuer Begriffe manifestieren. Im Folgenden werden
die wesentlichen Grundbegriffe kurz erlutert werden. Daran schliet sich ein kurzer ber-
blick ber die Entwicklung der objektorientierten Programmierung an.


1.1 Grundbegriffe der objektorientierten Programmierung

Objekte und Klassen
Ein Objekt ist eine gekapselte Datenstruktur, welche sowohl Datenkomponenten (analog zu
einer struct-Variablen in C) als auch Funktionen zur Bearbeitung der Datenkomponenten
enthlt. Diese Funktionen werden generell als Methoden bezeichnet.

Die Objekte eines objektorientierten Programms reprsentieren reale oder konzeptionelle
Objekte aus dem Problembereich. So knnte ein logisches UND-Gatter durch vier Objekte
modelliert werden. Drei gleichartige Objekte reprsentieren die beiden Eingnge und den
Ausgang, ein weiteres Objekt reprsentiert die eigentliche Schaltung. Die Ein- und Ausgnge
enthalten als Datenkomponente das jeweils anliegende Signal.

Eingang 1
Eingang 2
Ausgang
Schaltung
UND-


Abb. 1.1 Objekte eines UND-Gatters

Die Ausfhrung eines objektorientierten Programms bewirkt den Austausch von Botschaften
unter den Objekten. Der Empfang einer Botschaft bewirkt die Ausfhrung einer gleichnami-
gen Methode. Hierdurch knnen die Datenkomponenten des Objekts verndert oder abgefragt
werden. Auch der Benutzer des Programms kann Botschaften an Objekte senden.

Ein Schaltvorgang in dem UND-Gatter knnte z.B. die in Abb. 1.2 gezeigte Folge von Bot-
schaften auslsen.
2
Eingang 1
Eingang 2
Ausgang
Schaltung
Benutzer holeSignal
holeSignal
setzeSignal
schalte
1
2
3
4
UND-


Abb. 1.2 Botschaftenaustausch anlsslich eines Schaltvorgangs

Eine Klasse ist die programmtechnische Beschreibung eines Objekts oder - anders ausge-
drckt - die Definition eines neuen Datentyps. Sie enthlt Angaben zu den Namen und den
Typen der Datenkomponenten sowie den Programmcode der Methoden. Um dem Prinzip der
Datenkapselung gerecht zu werden, knnen die Komponenten einer Klasse als privat bzw.
ffentlich gekennzeichnet werden. Whrend private Komponenten nur in den Methoden der
Klasse selbst zugreifbar sind, kann auf ffentliche Komponenten - meist Methoden - auch
von auen zugegriffen werden.


Vererbung
Klassen knnen unter Bezugnahme auf eine bereits existierende Klasse definiert werden. In
diesem Falle erbt die neu definierte Klasse alle Komponenten der bereits bestehenden Klasse.
Die neu definierte Klasse wird von der besteheden Klasse abgeleitet. Der Vorteil besteht da-
rin, dass die geerbten Komponenten nicht neu definiert werden mssen. Die bestehende Klas-
se bezeichnet man als Basisklasse, die neu definierte Klasse wird als abgeleitete Klasse be-
zeichnet. Graphisch wird eine Ableitungsbeziehung wie in Abb. 3.1 dargestellt.

Basisklasse
abgeleitete Klasse


Abb. 1.3 Vererbungsbeziehung


Die abgeleitete Klasse kann um Datenkomponenten und/oder Methoden erweitert werden
oder die Komponenten der Basisklasse knnen redefiniert werden. In diesem Falle spricht
man von Spezialisierung.


3
Polymorphismus
Als Polymorphismus bezeichnet man die Eigenschaft von objektorientierten Programmier-
sprachen, dass die Interpretation einer Botschaft mit dem Typ des empfangenden Objekts
variieren kann. Da der Typ des Empfngerobjekts nicht in allen Fllen schon zur berset-
zungszeit feststeht, erfordert die Implementierung des Polymorphismus ein spezielles Binde-
verfahren, welches erst zu Laufzeit stattfindet und als sptes Binden bezeichnet wird.

Polymorphismus ist die wesentliche Grundlage fr eine flexible Erweiterbarkeit objektorien-
tierter Programme. Sie erlaubt im bestimmten Fllen sogar die Erweiterung eines Programms
ohne die Verfgbarkeit des Quellcodes


1.2 Historie der objektorientierten Programmierung

(1) Seinen Ursprung hat die objektorientierte Programmierung in der Sprache Simula 67,
eine objektorientierte Erweiterung von Algol, welche vornehmlich zum Zwecke der
Simulation eingesetzt wurde.

(2) Das heutige Verstndnis der objektorientierten Programmierung wurde vornehmlich
durch die Programmiersprache Smalltalk geprgt. In der Erfordernis fr eine radikal an-
dere Denkweise im Programmdesign sowie in der anfnglich schlechten Laufzeiteffizienz
liegen die Grnde fr die heutige geringe praktische Bedeutung von Smalltalk.

(3) Mit den objektorientierten Spracherweiterungen von C (Objective C und insbesondere
C++) wurde die objektorientierten Programmierung einem greren Anwenderkreis zu-
gnglich. C++ wurde zu Beginn 80-er Jahre von Bjarne Stroustrup (AT&T) vorgestellt.
Es handelt sich um eine echte Obermenge von C, was den Vorteil hat, dass keine grund-
stzlich neue Sprache zu erlernen ist. Unter der Zielsetzung, die Laufzeiteffizienz von C
mglichst wenig zu beeintrchtigen, wurden im Vergleich zu Smalltalk Kompromisse
bei den objektorientierten Konzepten eingegangen. Im Jahre 1998 wurde die Standardi-
sierung durch ANSI abgeschlossen.

(4) Seit 2000 gewinnt die von der Fa. Sun entwickelte, objektorientierte Programmiersprache
Java zunehmend an Bedeutung.


4
1.3 Notation der Syntax

Zur Beschreibung der Syntax wird die Backus-Naur-Notation (BNF) verwendet. Aus didak-
tischen Grnden werden dabei jedoch an einigen Stellen bewusst Vereinfachungen vor-
genommen. Fr eine vollstndige und korrekte Syntaxbeschreibung wird auf die Literatur
verwiesen.


Verwendete Symbolik
: : = wird definiert als
Fettschrift Terminalsymbole
Schrgschrift Nichtterminalsymbole
{ }
1
Auswahl eines alternativen Syntaxbestandteils
| trennt alternative Syntaxbestandteile
{ }
opt
optionales Syntaxbestandteil
{ }
0+
0-malige oder mehrmalige Wiederholung
{ }
1+
1-malige oder mehrmalige Wiederholung

Beispiele:

if-anweisung : : = if ( ausdruck ) anweisung { else anweisung }
opt

var-definition : : = typ { var-name { = const-ausdruck }
opt
}
1+
;
typ : : = { char | int | long | float | double }
1


5
2. Spracherweiterungen gegenber C

C++ enthlt eine Reihe von Spracherweiterungen, welche nicht in unmittelbarem Zusam-
menhang mit den objektorientierten Eigenschaften der Sprache stehen. Diese haben vorrangig
das Ziel, Sicherheit und den Komfort der Programmierung zu erhhen und wurden teilweise
in den ANSI-Standard der Sprache C bernommen.


2.1 Kommentare

In C wird ein - sich mglicherweise ber mehrere Zeilen erstreckender - Kommentar in die
Kommentarzeichen /* und */ eingefasst. Diese Mglichkeit bleibt in C++ weiterhin bestehen.
Das zustzliche Kommentarzeichen // (doppelter Schrgstrich) leitet einen Kommentar ein,
der sich bis zum Zeilenende erstreckt.

i nt x; / / Das i st ei ne Komment ar zei l e.


2.2 Der Datentyp bool

C++ kennt den Datentyp bool, zur Reprsentation von der Wahrheitswerten. Variablen vom
Typ bool knnen nur die Werte true und false annehmen.

bool ok = t r ue;
i f ( ok)
. . . .


Die Vergleichsoperatoren <, <=, >, >=, == und != liefern ein Ergebnis vom Datentyp bool.
Ferner lassen sich Werte vom Typ bool mittels der Operatoren && (UND), || (ODER) und !
(NICHT) in der bekannten Weise verknpfen.

Fr den Datentyp bool existieren implizite Typumwandlungsmglichkeiten von und nach int
(siehe Tabelle 2.1)


Tabelle 2.1 Umwandlung bool <-> int

bool -> int int -> bool
false 0 0 false
true 1 ungleich 0 true


6
2.3 Strukturen und Aufzhlungstypen

Fr alle Typnamen inkl. struct, union und enum gibt es einen gemeinsamen Namensraum.
Dies bedeutet, dass die Typnamen eindeutig sein mssen. Andererseits kann die Angabe der
Schlsselwrter struct, union und enum z.B. bei der Definition von Variablen entfallen.
Damit entfllt die Notwendigkeit zur Verwendung von typedef.

st r uct Per son {
char *name;
char *vor name;
};
Per son anybody = { " Must er mann" , " Ot t o" };


2.4 Variablendefinition

In C++ ist es nicht zwingend vorgeschrieben, Variablen am Anfang eines Blocks zu definie-
ren. Sie knnen beliebig zwischen oder innerhalb von Anweisungen eingestreut werden. Sie
gelten bis an das Ende des nchstinneren Blocks. Im Falle einer for-Anweisung ist ihre Gl-
tigkeit also auf den Block begrenzt, der die for-Anweisung enthlt.

f or ( i nt i = 0; i < 10; i ++)
t uWas( ) ;



2.5 Modifikation mit const

Mittels des Schlsselworts const knnen Variablen als konstant definiert werden. Im Gegen-
satz zu ANSI-C knnen const-Gren auch zur Dimensionierung von Vektoren verwandt
werden.

const i nt LEN=100;
i nt vek[ LEN] ;

const-Gren knnen nur auf dem Wege der Initialisierung mit einem Wert versehen wer-
den. Auerdem kann man die Adresse einer const-Gre nicht einem normalen Zeiger
zuweisen, da sonst eine Modifikation auf indirektem Wege mglich wre:

const i nt LEN = 100;
i nt *p = &LEN; / / Fehl er


Die Zuweisung der Adresse an einen mit const modifizierten Zeiger ist dagegen mglich.
Eine indirekte Modifikation ist in diesem Falle nicht mglich:

const i nt LEN = 100;
const i nt *p = &LEN; / / ok
*p = 200; / / Fehl er

7
Es sei an dieser Stelle angemerkt, dass die const-Modifikation lediglich vor einer Vernde-
rung des adressierten Datenobjekts schtzt. Der Zeiger selbst kann verndert werden. Wollte
man den Zeiger selbst vor Vernderung schtzen, mte er wie folgt definiert werden:

i nt a;
i nt * const p = &a;
p = NULL; / / Fehl er



2.6 Referenzen

Eine Referenz in C++ ist ein alternativer Name fr ein bestehendes Datenobjekt.

i nt i = 5; / / I nt eger - Dat enobj ekt mi t Wer t 5
i nt &r = i ; / / r und i bezi ehen si ch auf den
/ / gl ei chen Spei cher pl at z.
r = 10; / / i ( und r ) haben den Wer t 10.


Dies kann wie folgt mit Hilfe von Zeigern quivalent realisiert werden:

i nt i = 5; / / I nt eger - Dat enobj ekt mi t Wer t 5
i nt *r = &i ; / / *r und i bezi ehen si ch auf den
/ / gl ei chen Spei cher pl at z
*r = 10; / / i ( und *r ) haben den Wer t 10.


Das Beispiel zeigt, dass eine Referenz als ein Zeiger verstanden werden kann, welcher auto-
matisch dereferenziert wird.

Eine Referenz muss wie eine const-Gre immer initialisiert werden. Eine nachtrgliche
nderung ist nicht mglich, da eine Wertzuweisung an eine Referenz nicht den Wert der
Referenz ndert, sondern das referenzierte Datenobjekt.

Syntax: r ef er enzt yp : : = typ & ( al t er nat i v: typ& )

Der Adressoperator & erhlt eine erweiterte Bedeutung und wird in dem Zusammenhang als
Referenzoperator bezeichnet.



8
Parameterbergabe und Wertrckgabe mittels Referenzen
In C werden Parameter grundstzlich als Wertparameter bergeben. Eine bergabe per Refe-
renz muss mit Hilfe von Zeigern nachgebildet werden Adressparameter).

Beispiel 2.1a: (Adressparameter)
#i ncl ude <st di o. h>
voi d swap( i nt *a, i nt *b) ;

i nt mai n( )
{ i nt i = 3, j = 5;

pr i nt f ( " Vor her : i =%d, j =%d\ n" , i , j ) ;
swap( &i , &j ) ;
pr i nt f ( " Nachher : i =%d, j =%d\ n" , i , j ) ;
r et ur n 0;
}

voi d swap( i nt *a, i nt *b)
{ i nt t emp;

t emp = *a;
*a = *b;
*b = t emp;
}


Referenzen gestatten eine einfachere Realisierung von Referenzparametern. Beispiel 2.1b
enthlt eine Reimplementierung des Programms in Beispiel 2.1a mittels Referenzen.

Beispiel 2.1b: (Referenzparameter)
#i ncl ude <st di o. h>
voi d swap( i nt &a, i nt &b) ;

i nt mai n( )
{ i nt i = 3, j = 5;

pr i nt f ( " Vor her : i =%d, j =%d\ n" , i , j ) ;
swap( i , j ) ;
pr i nt f ( " Nachher : i =%d, j =%d\ n" , i , j ) ;
r et ur n 0;
}

voi d swap( i nt &a, i nt &b)
{ i nt t emp;

t emp = a;
a = b;
b = t emp;
}

Bemerkungen
- Die Initialisierung der Referenzparameter erfolgt bei der Argumentbergabe. Die Refe-
renzparameter knnen auch hier als Synonyme fr die korrespondierenden Argumente
interpretiert werden.

9
Parameterbergabe mittels Referenzen findet in 2 Fllen Anwendung:
(1) Eine Funktion liefert zwei oder mehr Werte an den Aufrufer zurck.
(2) Aus Grnden der Laufzeit- und Speicherplatzeffizienz soll ein greres Datenobjekt
nicht per Wert bergeben werden.

Mchte man im letztgenannten Fall vermeiden, dass das bergebene Datenobjekt in der
Funktion irrtmlicherweise verndert wird (Seiteneffekte), so kann die Parameterangabe zu-
stzlich mit dem Modifizierer const versehen werden:

voi d ausgabe( const Bi gSt r uct &bi g) ;

Auch im Hinblick auf das Verstndnis der Funktionsweise einer Funktion ist die const-
Modifikation eines als Referenz bergebenen Parameters sehr hilfreich, da auf diese Weise
erkennbar ist, dass es sich um einen reinen Eingabeparameter handelt.

Der Wert einer Funktion kann ebenfalls vom Referenztyp sein. In diesem Falle kann der
Funktionsaufruf auch auf der linken Seite einer Wertzuweisung auftauchen:

i nt & val At ( const i nt &vek, i nt i ) ;
/ / l i ef er t Ref er enz auf i - t e Komponent e von vek
i nt v[ 10] ;
val At ( v, 0) = 10; / / gl ei chbedeut end mi t : v[ 0] = 10;

Eine Funktion darf keine Referenz auf ein lokal definiertes Datenobjekt zurck liefern.



2.7 berladen von Funktionen

Grundstzlich sollten verschiedene Funktionen verschiedene Namen haben. In gewissen Fl-
len jedoch werden Funktionen mit gleicher Aufgabe fr unterschiedlichen Parametertypen
bentigt. In diesem Fall ist es sinnvoll, diese Funktionsgleichheit auch durch gleiche Funkti-
onsnamen zu verdeutlichen. In C++ sind gleichnamige Funktionen mit unterschiedlichen Pa-
rametertypen erlaubt (berladen von Funktionen):

l ong quadr at ( l ong x) ;
doubl e quadr at ( doubl e x) ;

Beim Aufruf einer berladenen Funktion muss der Compiler anhand der Argumenttypen pr-
fen, welche Funktion gemeint ist. Die "am besten passende" Funktion wird ausgewhlt. Da-
bei kommt der Reihe nach die folgende Rangfolge von Umwandlungsregeln zur Anwendung:

1. Exakte bereinstimmung: keine Umwandlung.
2. Umwandlung von char und short nach int sowie von float nach double
3. Standard-Umwandlungen wie sie auch in C blich sind (u.U. in Verbindung mit Informa-
tionsverlust, z.B. int nach float oder long nach int).

Zum Aufruf kommt die Funktion, welche gem obiger Aufstellung die niederrangiste Um-
wandlung erfordert. Werden zwei Funktionen mit gleichrangiger Umwandlung gefunden,
liegt eine Zweideutigkeit vor, welche zu einem bersetzungsfehler fhrt.
10

quadr at ( 1L) ; Aufruf von quadrat(long)
quadr at ( 1. 0f ) ; Aufruf von quadrat(double)
quadr at ( 1) ; zweideutig, Fehler

Im ersten Fall liegt eine exakte bereinstimmung vor, im zweiten Fall findet eine Um-
wandlung von float nach double gem Regel 2 statt. Der dritte Aufruf ist zweideutig, da
sowohl fr quadrat(long) als auch fr quadrat(double) eine Standardumwandlung statt-
finden mte.

Fr das Auffinden der "am besten passenden" Funktion muss die Typisierung der berla-
denen Funktionen bekannt sein. Aus diesem Grund sind in C++ Funktionsdeklarationen
obligatorisch. C++ wird aus diesem Grunde als Sprache mit strenger Typprfung bezeich-
net.


2.8 Default-Werte fr Funktionsparameter

Funktionsdefinitionen knnen Vorbesetzungen (Default-Werte) von Parametern enthalten.
Diese werden bei fehlenden Argumenten im Funktionsaufruf verwendet.

i nt add( i nt a, i nt b, i nt c=0, i nt d=0)
{
r et ur n a + b + c + d;
}

Nach dem ersten vorbesetzten Parameter mssen alle folgenden Parameter vorbesetzt
sein. Nicht erlaubt wre: int add (int a=0, int b, int c, int d=0)
Im Aufruf drfen nach dem ersten fehlenden aktuellen Parameter keine weiteren Parameter
aufgefhrt werden.

korrekte Aufrufe: nicht korrekte Aufrufe:
add( 1, 2, 3, 4) ; add( 1) ;
add( 1, 2, 3) ; add( 1, 2, , 4) ;
add( 1, 2) ;


2.9 Inline-Funktionen

Fr kleine, hufig verwendete Funktionen werden in C hufig Makros verwandt. Diese sind
jedoch fehleranfllig. In C++ knnen statt der Makros Inline-Funktionen verwendet werden.

i nl i ne i nt quadr at ( i nt x) { r et ur n x*x; }

Analog zu Makros fllt beim Aufruf einer Inline-Funktionen kein Stack-Overhead an. Sie
sind jedoch weniger fehleranfllig, da sie in Syntax und Semantik mit den blichen Funktio-
nen bereinstimmen und der Typprfung unterliegen.

Syntax: inline funktionsdefinition

11
2.10 Der Bereichsoperator ::

In C verdeckt eine Variable in einem inneren Block eine gleichnamige externe Variable. Der
Bereichsoperator ( scope resolution operator) :: von C++ ermglicht den Zugriff auf ver-
deckte externe (globale) Namen.

Beispiel 2.2:
#i ncl ude <st di o. h>

i nt x = 0; / / ext er nes x

i nt mai n( )
{ i nt x; / / 1. l okal es x, ver deckt ext er nes x

x = 1; / / Zuwei sung an 1. l okal es x
: : x = 2; / / Zuwei sung an ext er nes x

{ i nt x; / / 2. l okal es x

x = 3; / / Zuwei sung an 2. l okal es x
: : x = 4; / / Zuwei sung ext er nes x
}
x = 6; / / Zuwei sung an 1. l okal es x

r et ur n 0;
}

Syntax: ::var-name


2.11 Namensrume

Umfangreichere Programme verwenden hufig mehrere Bibliotheken. Dabei kann es zu Na-
menskonflikten kommen. Werden z.B. zwei Typen mit gleichem Namen definiert, so knnen
diese Bibliotheken nicht gemeinsam eingesetzt werden.

Das Programm in Beispiel 2.3 verwendet zwei Bibliotheken, deren Schnittstellen durch die
Header-Dateien lib1.h bzw. lib2.h definiert sind.

Beispiel 2.3:
/ / Dat ei l i b1. h

t ypedef enum{ FALSE, TRUE } Bool ean;
voi d pr i nt Bool ( Bool ean bval ) ;

/ / Dat ei l i b2. h

t ypedef enum{ FALSCH, WAHR } Bool ean;
voi d pr i nt Bool ( Bool ean bval ) ;

Der Namenskonflikt betrifft den Enumerationstyp Boolean und die Funktion printBool().
12
Deklaration von Namensrumen
C++ sieht zur Lsung des Problems so genannte Namensrume (name spaces) vor. Ein Pro-
gramm kann in mehrere Namensrume aufgeteilt werden. Bezeichner mssen nur im jeweili-
gen Namensraum eindeutig sein. Die Deklaration eines Namensraums geschieht ber das
Schlsselwort namespace, gefolgt von einem Bezeichner fr den Namensraum und einem
Block mit den zum Namensraum gehrenden Vereinbarungen.

Syntax: namespace name block

In Beispiel 2.3 werden die beiden Bibliotheken in den Namensrumen lib1 und lib2 definiert.

Beispiel 2.3 (Forts.):
/ / Dat ei l i b1. h

namespace l i b1
{
t ypedef enum{ FALSE, TRUE } Bool ean;
voi d pr i nt Bool ( Bool ean bval ) ;
}

Beispiel 2.3 (Forts.):
/ / Dat ei l i b2. h

namespace l i b2
{
t ypedef enum{ FALSCH, WAHR } Bool ean;
voi d pr i nt Bool ( Bool ean bval ) ;
}

Natrlich mssen die Funktionen auch im jeweiligen Namensraum definiert werden. Hierzu
wird der Namensraum erneut geffnet.

Beispiel 2.3 (Forts.):
/ / Dat ei l i b1. cpp

#i ncl ude <st di o. h>
#i ncl ude " l i b1. h"

namespace l i b1
{
voi d pr i nt Bool ( Bool ean bval )
{
i f ( bval == FALSE)
pr i nt f ( " FALSE" ) ;
el se
pr i nt f ( " TRUE" ) ;
}
} / / namespace l i b1

Bezeichner, welche auerhalb einer namespace-Deklaration vereinbart werden, gehren dem
namenlosen Standard-Namensraum.
13
Zugriff
Innerhalb eines Namensraums knnen dort definierte Bezeichner ohne weitere Angaben an-
gesprochen werden. Auerhalb des Namensraums ist ein qualifizierter Name erforderlich. Ein
qualifizierter Name besteht aus der Angabe des Namensraums, gefolgt von dem Be-
reichsoperator ::, gefolgt von dem eigentlichen Bezeichner.

Syntax: namespace::name

Die Funktion main() in Beispiel 2.3 ist im namenlosen Standard-Namensbereich definiert.
Deshalb muss auf die Bestandteile der Bibliotheken mit qualifiziertem Namen zugegriffen
werden.

Beispiel 2.3 (Forts.):
/ / Dat ei bsp_2_3a. cpp - Qual i f i zi er t e Namen

#i ncl ude " l i b1. h"
#i ncl ude " l i b2. h"

i nt mai n( )
{ l i b1: : Bool ean ok;
i nt i = 1, j = 2;

ok = ( l i b1: : Bool ean) ( i < j ) ;
l i b1: : pr i nt Bool ( ok) ;
}


using-Direktive
Mit Hilfe der using-Direktive kann ein Namensraum in den aktuellen Gltigkeitsbereich
importiert werden. Hierdurch werden auch alle im importierten Namensraum definierten Be-
zeichner ohne weitere Angaben gltig.

Syntax: using namespace name ;

Beispiel 2.3 (Forts.):
/ / Dat ei bsp_2_3b. cpp - usi ng- Di r ekt i ve

#i ncl ude " l i b1. h"
#i ncl ude " l i b2. h"

usi ng namespace l i b1;

i nt mai n( )
{
Bool ean ok;
i nt i = 1, j = 2;

ok = ( Bool ean) ( i < j ) ;
pr i nt Bool ( ok) ;
}

14
Bemerkungen zu Beispiel 2.3
- Bezeichner aus dem Namensraum lib2 mssen weiterhin qualifiziert angesprochen wer-
den.
- Ein zustzlicher Import von Namensraum lib2 wrde erneut zu Namenskonflikten fhren.


2.12 Freispeicherverwaltung

Die Operatoren new und delete ermglichen die dynamische Reservierung bzw. Freigabe
von Speicherplatz im Freispeicher.

i nt *p1 = new i nt ; / / Reser vi er ung von Spei cher pl at z
/ / und I ni t i al i si er ung von p1

Im Erfolgsfall reserviert new Speicherplatz fr einen als Argument angegebenen Typ und
liefert einen entsprechend typisierten Zeiger zurck. Im Fehlerfall wird ein NULL-Zeiger
zurckgeliefert. Mit dem Operator new kann eine Angabe zur Initialisierung des reservierten
Bereichs angegeben werden:

i nt *p2 = new i nt ( 3) ; / / zust zl i ch I ni t i al i si er ung
/ / von *p2 mi t Wer t 3


Die Reservierung von Speicherplatz fr ein Array geschieht mit dem Operator new[] unter
Angabe der gewnschten Dimension.

char *st r = new char [ 10] ; / / 10- el ement i gen char - Ar r ay
i nt ( *mat ) [ 4] = new i nt [ 2] [ 4] ; / / Zwei 4- el ement i ge i nt -
Ar r ays

- Der zurckgelieferte Zeiger zeigt auf das erste Array-Element.
- Eine Initialisierung ist in diesem Fall nicht mglich. Es findet auch keine automatische
Initialisierung statt.


delete gibt den reservierten Speicherplatz zurck. Die Freigabe eines mittels new [] reser-
vierten Speicherbereichs muss mit dem Operator delete[] frei gegeben werden.

del et e p1;
del et e [ ] st r ;

Syntax: new typ {( initialwert ) | [ dimension ] }
1 opt
;
delete { [] }
opt
zeiger ;



15
2.13 Einfache Ein-/Ausgabe

C++ verfgt ber eine E-/A-Bibliothek, welche auf dem Klassenkonzept basiert und so der
objektorientierten Programmiermethodik besser entgegenkommt. Die Schnittstelle der Biblio-
thek befindet sich in der Header-Datei iostream (ohne .h). Sie ist im Namensraum std defi-
niert, der ber eine using-Direktive importiert werden muss.

An dieser Stelle werden die Mglichkeiten der Bibliothek zunchst sehr vereinfacht und ein-
geschrnkt auf die Standard-Ein-/Ausgabe vorgestellt. Eine weiterfhrende Beschreibung
befindet sich in Kapitel 8.

Jedes C++-Programm verfgt ber die folgenden Objekte, welche Ein-/Ausgabestrme
(Streams) reprsentieren
- cin fr die Standardeingabe (von der Tastatur)
- cout fr die Standardausgabe (auf den Bildschirm)
- cerr fr die Fehlerausgabe (auf den Bildschirm).


Eingabe
Auf dem Eingabe-Stream cin ist der Eingabeoperator >> (vergl. Rechts-Shift) definiert.

i nt i ;
ci n >> i ;

Der >>-Operator fhrt die Eingabe durch und konvertiert diese entsprechend dem Typ der
Variablen auf der rechten Seite. Fhrende white-Spaces werden ignoriert. Als Ergebnis des
Ausdrucks cin >> i wird das Objekt cin zurckgeliefert. Deshalb kann der Ausdruck selbst
auf der linken Seite des >>-Operators auftreten.

i nt i ;
doubl e a;
ci n >> i >> a; / / ( ci n >> i ) >> a;

- Die Bestandteile der Eingabe mssen dabei durch ein whitespace-Zeichen getrennt sein.


Ausgabe
Auf den Ausgabe-Streams cout und cerr ist der Ausgabeoperator << (vergl. Links-Shift)
definiert:

i nt i ;
cout << i ;

Hierbei wird die interne Darstellung des jeweiligen Datentyps in eine Folge von Zeichen
konvertiert und auf dem Bildschirm ausgegeben. Auch im Falle des Ausgabeoperators << ist
eine Verkettung mglich:


16
i nt i = 2;
doubl e a = 2. 5;
cout << i << \ n << a;

Eingabe- und Ausgabeoperator akzeptieren als zweiten Operanden Variablen der Standardda-
tentypen char, char*, void*, bool, short, int, long, float und double sowie die vorzeichen-
losen Varianten der Ganzzahltypen.

Die Ein-/Ausgabe in C++ wird abschlieend in einem Beispielprogramm demonstriert:

Beispiel 2.4: (Ein-/Ausgabe)
#i ncl ude <i ost r eam>

usi ng namespace st d;

i nt mai n( )
{
i nt anz;
doubl e pr ei s;
char pr odukt [ 20] ;

cout << " Bezei chnung: " ;
ci n >> pr odukt ;

cout << " Ei nzel pr ei s ( EUR) : " ;
ci n >> pr ei s;

cout << " Anzahl : " ;
ci n >> anz;

cout << anz << " " << pr odukt << " kost en "
<< anz * pr ei s << " EUR\ n" ;

r et ur n 0;
}

Das Programm berechnet im Dialog mit dem Benutzer den Gesamtpreises fr die eingegebe-
ne Menge eines bestimmten Produktes aus.

17
2.14 bungsaufgaben

Aufgabe 2.1 (Referenzen)
Schreiben Sie eine Funktion, welche zwei int-Zeiger vertauscht. Verwenden Sie dabei Refe-
renzparameter.


Aufgabe 2.2 (Referenzen)
Schreiben Sie eine C++-Funktion namens at(), welche wie folgt deklariert ist:

i nt & at ( i nt * vec, unsi gned l en, unsi gned i ) ;

Die Funktion liefert eine Referenz auf das i-te Element des len langen int-Array vec zurck.
Falls i einen ungltigen Index enthlt, bricht das Programm mit der Fehlermeldung index
overflow ab.

Testen Sie die Funktion mit folgendem Hauptprogramm:

#i ncl ude <i ost r eam>
usi ng namespace st d;

i nt mai n( )
{
const i nt LEN = 5 ;
i nt vec[ LEN] = { 0, 1, 2, 3, 4 };

at ( vec, LEN, 1) = 1001;
cout << at ( vec, LEN, 1) << ' \ n' ;
cout << at ( vec, LEN, 5) << ' \ n' ;
r et ur n 0;
}

Aufgabe 2.3 (Allgemeine Fragen)
(a) In einem Programm seien folgende Funktionen definiert.
(1) int Add(int x, double y);
(2) double Add(double x, int y);
Welche Funktion wird bei dem Aufruf Add(10,5); ausgefhrt?

(b) Welche Vorteile haben Inline-Funktionen gegenber normalen C-Funktionen?

(c) Wann sollte man einen Referenzparameter mit dem Attribut const ausstatten?

(d) Welche der folgenden Anweisungen ist bei der Deklaration von
const int v[3] = {1, 2, 3}; zulssig?
(1) *v =3; (2) (*v)++; (3) int q=*v;

18
3. Klassen und Objekte

3.1 Klassen

Eine Klasse stellt, wie in Kapitel 1 bereits ausgefhrt, eine Mglichkeit zur Formulierung
eines anwendungsbezogenen Datentyps dar. Auch in C lassen sich mit Hilfe von Strukturty-
pen anwendungsbezogene Datentypen formulieren. Ein Datentyp zur Modellierung von
UND-Gattern kann in C etwa wie folgt definiert werden:

enumSi gnal { L, H };

st r uct UndGat t er {
enumSi gnal ei n1; / * zwei */
enumSi gnal ei n2; / * Ei ngnge */
enumSi gnal aus; / * Ausgang */
};

Eine Funktion namens schalten() soll die Funktionsweise eines UND-Gatters simulieren:

voi d schal t en( st r uct UndGat t er *g)
{
g- >aus = ( enumSi gnal ) ( g- >ei n1 && g- >ei n2) ;
}

Unschn ist hierbei das Fehlen einer expliziten Verbindung zwischen dem Strukturtyp
UndGatter und der auf diesem Datentyp operierenden Funktion schalten() sowie die fehlen-
de Mglichkeit zur Datenkapselung. Beides wird durch das Klassenkonstrukt in C++ elimi-
niert.


3.1.1 Klassendeklaration

Bei dem Klassenkonstrukt von C++ handelt es sich um eine Weiterentwicklung der Struktur-
typen aus C.

Klassen knnen neben der Deklaration von Variablen auch Funktionsdeklarationen ent-
halten. Die Variablen werden als Instanzvariablen, die Funktionen als Elementfunktio-
nen (member functions) oder Methoden bezeichnet.
Die Komponenten einer Klasse (Instanzvariablen und Elementfunktionen) knnen als
private oder ffentliche Komponenten vereinbart werden.

Privat vereinbarte Komponenten sind lediglich in den Elementfunktionen der Klasse zugreif-
bar. Hierdurch kann insbesondere die interne Reprsentation eines Objekts nach auen zu
verborgen werden (Information Hiding). Der Zugriff auf den Datentyp wird auf die ffent-
lich deklarierten Funktionen begrenzt (Datenkapselung).

19
Beispiel 3.1: (Klassendeklaration)
cl ass UndGat t er {
pr i vat e: / / pr i vat er Tei l
Si gnal ei n1;
Si gnal ei n2;
Si gnal aus;
voi d schal t en( voi d) ;
publ i c: / / oef f ent l i cher Tei l
UndGat t er ( ) ;
UndGat t er ( Si gnal s1, Si gnal s2) ;
i nt set Ei ngangsSi gnal ( unsi gned nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) const ;
};

Bemerkungen
- Die Klasse enthlt neben den privaten Instanzvariablen ein1, ein2 und aus die ffentli-
chen Zugriffsfunktionen setEingangsSignal() und getAusgangsSignal(). Die Element-
funktion schalten() ist ebenfalls privat und kann damit nur in Elementfunktionen der
Klasse UndGatter aufgerufen werden.
- Im Beispiel ist der unmittelbare Zugriff auf die Variablen der Klasse nicht mglich. Die
Vernderung und Abfrage der Variablen erfolgt ber die ffentlich zugnglichen Ele-
mentfunktionen setEingangsSignal() und getAusgangsSignal(); d.h. die im privaten Teil
der Klassendeklaration aufgefhrten Variablen sind gekapselt.
- Falls die Deklaration einer Elementfunktion mit dem Zusatz const versehen ist, kann die
Funktion keine Vernderungen an den Instanzvariablen vornehmen.

Allgemein gilt fr Klassendeklarationen die folgende Syntax:

Syntax: class class-name {
{ private : }
opt

member-liste
public :
member-liste
};

Bemerkungen
- member-liste kann Typ- und Variablendeklarationen (Instanzvariablen) sowie die De-
klaration bzw. Definitionen von Funktionen (Elementfunktionen) enthalten.
- Die Schlsselworte private und public knnen grundstzlich beliebig oft und in jeder
Reihenfolge auftreten. Sie gelten jeweils bis zur nachfolgenden Angabe. In der Praxis
beschrnkt sich die Klassendeklaration auf einen privaten und einen ffentlichen Be-
reich. Bei fehlender Zugriffsangabe sind die nachfolgenden Komponenten standard-
mig private.
- In C++ sind Klassen- und Strukturdeklarationen weitestgehend quivalent. D.h. auch
Strukturdeklarationen knnen Elementfunktionen enthalten. Ferner erlauben sie die
Unterscheidung zwischen privaten und ffentlichen Komponenten. Im Unterschied zu
Klassen sind die Komponenten einer Struktur standardmig public. Meist wird je-
doch das Klassenkonstrukt zur Definition von anwendungsbezogenen Datentypen vor-
gezogen.

20
3.1.2 Objekterzeugung und Komponentenzugriff

Analog zu einer Strukturdeklaration in C kann eine Klasse in C++ zur Erzeugung eines Ob-
jekts herangezogen werden. (Da der Objektbegriff auch in anderen Zusammenhngen sehr
strapaziert wird, werden Objekte im vorliegenden Zusammenhang hufig auch als Klassen-
objekte oder Instanzen einer Klasse bezeichnet.) Ein Objekt kann durch Definition einer
Variablen vom Klassentyp oder dynamisch mit Hilfe des new-Operators erzeugt werden.

UndGat t er gat e;
UndGat t er *dynGat e;
dynGat e = new UndGat t e;

Der Zugriff auf ffentliche Instanzvariablen und Elementfunktionen erfolgt ber die aus C
bekannten Selektionsoperatoren . bzw. ->:

gat e. set Ei ngangsSi gnal ( 1, H) ;
Si gnal si g;
si g = dynGat e- >get AusgangsSi gnal ( ) ;


3.1.3 Elementfunktionen

Elementfunktionen knnen innerhalb und auerhalb der Klassendeklaration definiert werden.
Ist die Funktionsdefinition vollstndig in der Klassendeklaration enthalten, handelt es sich in
jedem Fall um eine Inline-Funktion. Soll eine Elementfunktion nicht als Inline-Funktion rea-
lisiert werden, so muss sie auerhalb der Klassendeklaration definiert werden. Die Klassen-
deklaration enthlt in diesem Falle nur die Funktionsdeklaration (Prototyp). Natrlich knnen
auch extern definierte Elementfunktionen ausdrcklich als Inline-Funktion definiert werden.
In diesem Fall wird die Funktion meist in einer Header-Datei definiert sein.

Da verschiedene Klassen gleichnamige Elementfunktionen haben knnen, muss bei der
(externen) Definition von Elementfunktionen der Bezug zur Klasse hergestellt werden.
Getrennt durch den Bereichsoperator :: wird der Funktionsname mit dem Klassenname
qualifiziert.

Beispiel 3.1 (Forts.): (Elementfunktionen)
i nl i ne voi d UndGat t er : : schal t en( voi d)
{
aus = ( Si gnal ) ( ei n1 && ei n2) ;
}

Der externen Definition von Elementfunktionen liegt damit folgende Syntax zugrunde:

Syntax: { inline }
opt
{ typ }
opt
klassen-name::funktions-name
( { parameter-deklaration }
opt
) block


21
Selbstreferenz ber den Zeiger this
Innerhalb von Elementfunktionen kann auf alle Komponenten des aktuellen Objekts durch
bloe Angabe des Komponentennamens zugegriffen werden. Mit aktuellem Objekt ist das
Objekt gemeint, ber das die Elementfunktion (unter Verwendung von . oder ->) aufgerufen
wurde.

Beim Aufruf einer Elementfunktion wird - fr den C++-Programmierer nicht sichtbar - ein
konstanter Zeiger namens this auf das aktuelle Objekt bergeben.

Fr die Klasse UndGatter ist this wie folgt definiert:

UndGat t er *const t hi s;

Innerhalb einer Elementfunktion erfolgt der Zugriff auf eine Komponente automatisch unter
Verwendung des Zeigers this. In einer Elementfunktion der Klasse UndGatter sind daher die
folgenden Ausdrcke quivalent:

ei n1 und t hi s- >ei n1
schal t en( ) und t hi s- >schal t en( )

In Elementfunktionen einer Klasse X, welche mit dem Zusatz const versehen sind, hat der
Zeiger this den Typ const X *const this. Hierdurch ist sowohl der Zeiger selbst, als auch das
Objekt vor Vernderungen geschtzt.

Der Zeiger this wird auch vom Programmierer in Anspruch genommen, wenn es darum geht,
die Adresse des aktuellen Objekts zu bestimmen oder das aktuelle Objekt als Wert oder als
Referenz im Funktionswert zurckzugeben.


Gltigkeit von Namen in Elementfunktionen
Innerhalb einer Elementfunktion schiebt sich der Gltigkeitsbereich der Klasse zwischen den
globalen Gltigkeitsbereich und den lokalen, auf die Funktion begrenzten Gltigkeitsbereich.
Hieraus resultiert die folgende Rangfolge bzgl. der Gltigkeit von Bezeichnern.
(1) lokal definierte Bezeichner
(2) innerhalb der Klasse definierte Bezeichner
(3) Bezeichner mit Dateigltigkeit

Mit Hilfe des Bereichsoperators :: kann der Zugriff wie folgt modifiziert werden:

(1) cl ass- name::name Zugriff auf Komponente der Klasse
(2) ::name Zugriff auf Bezeichner mit Dateigltigkeit


22
Dies sei an nachfolgendem Beispiel verdeutlicht:

cl ass X {
i nt x;
publ i c:
i nt f ( ) ;
};

i nt x;

i nt X: : f ( )
{ i nt x;

x = 1; / / l okal es x
X: : x = 1; / / I nst anzvar i abl e
: : x = 1; / / gl obal es x
}


3.1.4 Konstruktoren

Klassendeklarationen enthalten meist eine typlose Elementfunktion, welche namensgleich
mit der Klasse ist. Es handelt sich um den Konstruktor.

Ein Konstruktor ist eine spezielle Elementfunktion, welche automatisch beim Erzeugen
eines Objekts aufgerufen wird, und meist Initialisierungsaufgaben wahrnimmt.

Beispiel 3.1 (Forts.): (Standardkonstruktor)
UndGat t er : : UndGat t er ( voi d)
{
ei n1 = L; / / I ni t i al i si er en
ei n2 = L; / / der Ei ngnge
schal t en( ) ; / / Best i mmen des Ausgangs
}

Bemerkungen
- Bei der Definition eines Konstruktors wird kein Ergebnistyp angegeben (auch nicht void).
Eine Wertrckgabe ist ebenfalls nicht mglich.
- Konstruktoren knnen parameterlos oder parametiert sein. Hufig findet man in einer
Klassendeklaration auch mehrere Konstruktoren mit unterschiedlicher Parametrierung
(berladung), so dass mehrere Alternativen zur Initialisierung eines Objekts existieren.
Weil ein parameterloser Konstruktor lediglich feste Standardwerte fr die Initialisierung
verwendet, wird er als Standardkonstruktor bezeichnet.
- Es sei an dieser Stelle bemerkt, dass es nicht Aufgabe eines Konstruktors ist, Speicherplatz
fr das Objekt zu reservieren. Diese Aufgabe bernimmt der Compiler bzw. - bei dyna-
misch erzeugten Objekten - der new-Operator.

Beispiel 3.1 enthlt einen zweiten Konstruktor, welcher als Parameter die Initialwerte fr die
Eingnge als Parameter enthlt.

23

Beispiel 3.1 (Forts.): (berladener Konstruktor)
UndGat t er : : UndGat t er ( Si gnal s1, Si gnal s2 )
{
ei n1 = s1; / / I ni t i al i si er en
ei n2 = s2; / / der Ei ngnge
schal t en( ) ; / / Best i mmen des Ausgangs
}

Falls ein parametrierter Kontruktor zur Initalisierung heran gezogen werden soll, muss die
Objektdefintion eine Initialiserungsliste fr die Konstruktorparameter enthalten:

UndGat t er gat e( L, L) ;
UndGat t er *dynGat e;
dynGat e = new UndGat t e( H, H) ;

- Enthlt eine Klassendeklaration keinen Konstruktor, so generiert der Compiler automa-
tisch ein parameterloser Standardkonstruktor. Die Instanzvariablen des Objekts enthalten
dann unbestimmte Werte. Auf die Bedeutung eines solchen Konstruktors wird spter ein-
gegangen.


3.1.5 Elementinitialisierungsliste

Wie bereits oben erwhnt, besteht die Aufgabe der Konstruktoren meist darin, die Objekte zu
initialisieren. Mit der Elementinitialisierungsliste sieht C++ eine zweite, vereinfachte Mg-
lichkeit zur Initialisierung der Instanzvariablen des Objekts vor. Das folgende Beispiel zeigt
eine weitere Alternative fr einen UndGatter-Konstruktor:

Beispiel 3.1 (Forts.): (Konstruktor mit Elementinitialisierungsliste)
UndGat t er : : UndGat t er ( Si gnal s1, Si gnal s2) : ei n1( s1) , ei n2( s2)
{
schal t en( ) ; / / Best i mmen des Ausgangs
}

Bemerkungen
- Die Elementinitialisierungsliste wird durch einen Doppelpunkt von der Parameterliste in
der Konstruktordefinition getrennt. Sie besteht aus einer Liste von Bezeichnern fr In-
stanzvariablen, gefolgt von einem in Klammern angegebenen Initialwert.
- Die Reihenfolge der Listeneintrge hat fr die Reihenfolge der Initialisierung keine Bedeu-
tung. Entscheidend hierfr ist die Reihenfolge der Deklaration der Instanzvariablen.

Eine besondere Bedeutung hat die Elementinitialisierungsliste fr Initialisierung von In-
stanzvariablen vom Referenztyp. Diese knnen nur ber einen Eintrag in der Elementinitiali-
sierungsliste mit Werten versorgt werden:

24
cl ass Ref er enz {
i nt &r ef ;
publ i c:
Ref er enz( i nt &r ) ;
/ / . . .
};

Ref er enz( i nt &r ) : r ef ( r )
{
}


3.1.6 Destruktoren

In einigen Fllen sind auch beim Lschen eines Objekts bestimmte Aktionen erforderlich.
Dies ist insbesondere dann der Fall, wenn das Objekt einen Zeiger auf einen dynamischen
Speicherbereich enthlt, der mit dem Lschen des Objekts freigegeben werden muss. Zu die-
sem Zweck sieht C++ den Destruktor vor.

Ein Destruktor ist eine spezielle Funktion, welche automatisch beim Lschen eines Ob-
jekts aufgerufen wird.
Analog zum Konstruktor wird bei Fehlen eines Destruktors ebenfalls ein Standardestruk-
tor erzeugt.

Beim Lschen eines Objekts der Klasse UndGatter sind keine Aktionen der oben beschrie-
benen Art erforderlich. Daher ist die Definition eines Destruktors in diesem Fall nicht erfor-
derlich. Unabhngig davon wrde ein Destruktor fr die Klasse UndGatter wie folgt ausse-
hen:

Beispiel 3.1 (Forts.): (Destruktor)
UndGat t er : : ~UndGat t er ( voi d)
{
}

Bemerkungen
- Zur Benennung des Destruktors wird dem Klassennamen der "Not"-Operator ~ vorange-
stellt.
- Destruktoren sind typ- und parameterlos.


3.2 Objekte

Wie andere Datenobjekte in der Programmiersprache C knnen Klassenobjekte unterschied-
lichen Gltigkeitsbereich und Lebensdauer haben.



25
3.2.1 Globale, statische und automatische Objekte

Objekte knnen
- extern
- als statische Objekte innerhalb einer Funktion
- als lokale (automatische) Objekte oder als Parameter
deklariert sein. Ihre Gltigkeit und Lebensdauer ist analog zu der anderer Datenobjekte in C.

- Bei der Definition eines Objekts bernimmt der Klassenname wie bereits in 3.1.2 darge-
stellt, die Rolle der Typangabe.
- Im Falle von Standardkonstruktoren reicht die Angabe eines Objektnamens.
- Verfgt die Klasse ber einen parametrierten Konstruktor, muss bei der Objektdefinition
eine in Klammern eingefasste Initialisierungsliste angegeben werden. Diese enthlt die
Argumente fr den Aufruf des Konstruktors:
UndGat t er gat e( L, L) ;

Allgemein liegt der Definition eines Klassenobjekts die folgende Syntax zugrunde:

Syntax: klassen-name objekt-name
{ ( ausdruck {,ausdruck }
0+
)}
opt
;

Falls der Konstruktor ber nur einen Parameter verfgt, ist auch die aus C bekannte Syntax
zur Initialisierung einer Variablen zulssig:

Syntax: klassen-name objekt-name = ausdruck;

Konstruktoren und Destruktoren werden, wie bereits oben erwhnt, bei der Erzeugung bzw.
beim Lschen eines Objekts aufgerufen.

Falls es sich um ein globales Objekt (extern vereinbart) handelt, wird der Konstruktor
einmalig zum Programmstart und der Destruktor einmal zum Programmende aufgerufen.
Ein C++-Programmierer hat somit die Mglichkeit, Code zu formulieren, welcher vor
bzw. nach der Funktion main() zur Ausfhrung kommt.
Fr statische Objekte, welche innerhalb einer Funktion vereinbart wurden, wird der Kon-
struktor einmalig beim erstmaligen Aufruf der Funktion aufgerufen. Der Destruktor wird -
wie bei externen Objekten - zum Programmende durchlaufen.
Bei automatischen Objekten wird der Konstruktor bei jedem Aufruf der Funktion (d.h. bei
jeder Anlage des Objekts auf dem Stack) aufgerufen. Beim Verlassen der Funktion wird
der zugehrige Destruktor aufgerufen. Konstruktoren und Destruktoren fr derartig ver-
einbarte Objekte sollten deshalb kurze Laufzeit haben und ggf. als Inline-Elementfunktion
definiert sein.

Die Verwendung von automatischen Objekten wird anhand einer einfachen Anwendung zu
Beispiel 3.1 dargestellt.


26
Beispiel 3.1: (automatische Objekte)
/ / Dat ei bsp_b31. cpp
/ / Anwendung der Kl asse UndGat t er

#i ncl ude <i ost r eam>
#i ncl ude " gat e_b31. h"

usi ng namespace st d;

i nt mai n ( )
{ UndGat t er gat e;

gat e. set Ei ngangsSi gnal ( 1, H) ;
gat e. set Ei ngangsSi gnal ( 2, H) ;
cout << " Ausgang: "
<< ( gat e. get AusgangsSi gnal ( ) == L ? ' L' : ' H' )
<< ' \ n' ;
r et ur n 0;
}

Bemerkungen zur Programmstruktur
- Um eine mglichst universelle Nutzung zu erreichen, wird eine Klasse oder eine Menge
von logisch zusammenhngenden Klassen in einem eigenen Programm-Modul, bestehend
aus einer Header-Datei (mit Endung .h) und einer gleichnamigen Implementierungsdatei
(mit Endung .cpp) definiert.
- Die Header-Datei enthlt die Klassendeklaration und die Inline-Elementfunktionen, die
Implementierungsdatei enthlt die Definition der verbleibenden Nicht-Inline-Funktionen.
- Die Anwendung der Klasse mit der main()-Funktion wird in einer eigenen Datei (mit En-
dung .cpp) spezifiziert.


3.2.2 Dynamische Objekte

Im Gegensatz zu Objekten, deren Gltigkeit und Lebensdauer durch die Art der Definition
festgelegt ist, werden dynamische Objekte explizit erzeugt und gelscht.

Dynamische Objekte werden mit Hilfe des new-Operators erzeugt. Hierdurch wird Speicher-
platz zur Ablage des Objekts im Freispeicher reserviert. new liefert als Rckgabewert einen
Zeiger auf das neu erzeugte Objekt.

UndGat t er *gat e;
gat e = new UndGat t er ; / / Er zeugen ei nes Obj ekt s

Die Anwendung des Operators new auf einen Klassentyp bewirkt einen impliziten Aufruf
des Konstruktors der Klasse.
Bei einem parametrierten Konstruktor kann eine Initialisierungsliste angefgt werden.

gat e = new UndGat t er ( L, L) ;


27
Das Lschen eines Objekts (Freigabe des Speichers) erfolgt mittels des delete-Operators un-
ter Angabe des Zeigers auf das Objekt. Hierbei wird der zugehrige Destruktor aufgerufen:

del et e gat e;


3.3 Komposition von Objekten

In konkreten Anwendungen stellen Objekte vielfach sehr komplexe Datenstrukturen dar. Ein
Objekt kann dabei selbst Instanzvariablen vom Klassentyp oder Zeiger auf Objekte vom
Klassentyp enthalten. Diesen Sachverhalt bezeichnet man als Komposition oder Aggregation
von Objekten.


3.3.1 Elementobjekte

Enthlt eine Klasse eine Instanzvariable vom Klassentyp, so spricht man von einem Ele-
mentobjekt.
Als Beispiel sollen die Ein- und Ausgnge eines UND-Gatters als Elementobjekte von Und-
Gatter-Objekten modelliert werden. Hierzu ist eine eigenstndige Klasse fr Ein- bzw. Aus-
gnge erforderlich. Diese wird im Folgenden allgemein als Konnektor bezeichnet.

Beispiel 3.2: (Elementobjekte)
enumSi gnal { L, H };
enumKonTyp { E, A }; / / E = Ei ngang, A = Ausgang

cl ass Konnekt or {
KonTyp t yp;
Si gnal si g;
publ i c:
Konnekt or ( KonTyp t = E) ;
voi d set Si gnal ( Si gnal s) ;
Si gnal get Si gnal ( voi d) ;
};

In der Klasse UndGatter knnen nun die Ein- und Ausgnge als Objekte der Klasse
Konnektor definiert werden.

Beispiel 3.2 (Forts.): (siehe Beiblatt 3.2)
cl ass UndGat t er {
Konnekt or ei n1;
Konnekt or ei n2;
Konnekt or aus;
voi d schal t en( voi d) ;
publ i c:
UndGat t er ( voi d) ;
i nt set Ei ngangsSi gnal ( unsi gned nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) ;
};


28
Die privaten Bestandteile der Konnektor-Objekte bleiben gekapselt; d.h. die Element-
funktionen der Klasse UndGatter haben keinen Zugriff.
Der Speicherbereich des umschlieenden Objekts enthlt die Speicherbereiche der Ele-
mentobjekte (siehe Abb. 3.1) Deshalb werden die Elementobjekte automatisch zusammen
mit dem umfassenden Objekt erzeugt und initialisiert. Die Konstruktoren der Elementob-
jekte werden automatisch vom Konstruktor des umfassenden Objekts angestoen. D.h. der
Code der Konstruktoren der Elementobjekte wird vor dem Code des Konstruktors des um-
fassenden Objekts ausgefhrt. Die Reihenfolge ist bestimmt durch die Reihenfolge ihrer
Deklaration in der Klasse des umfassenden Objekts.

ein1
ein2
aus
Speicherbereich des -O Konnektor ein1 bjekts
Speicherbereich des -Objekts Konnektor ein2
Speicherbereich des -Objekts Konnektor aus
Speicherbereich des -Objekts UndGatter

Abb. 3.1: Speicherabbild eines UndGatter-Objekts mit Elementobjekten


Falls die umschlieende Klasse ber keinen Konstruktor verfgt, ruft ihr vom Compiler
generierter Standardkonstruktor die Konstruktoren der Elementobjekte auf.
Falls die Konstruktoren der member-Objekte Argumente erwarten, knnen diese in der
Elementinitialisierungsliste des Konstruktors der umfassenden Klasse angegeben werden.

Beispiel 3.2 (Forts.): (Elementinitialisierungsliste)
UndGat t er : : UndGat t er ( voi d) : ei n1( E) , ei n2( E) , aus( A)
{
}

Bemerkungen
- Die Eintrge in der Elementinitialisierungsliste enthalten die Namen der Elementobjekte
zusammen mit der Parameterversorgung der Konstruktoren.

Die Definition eines UndGatter-Objekts bewirkt die folgende Ausfhrungsreihenfolge von
Konstruktoren:

1. Konnektor(E) - Konstruktion von ein1
2. Konnektor(E) - Konstruktion von ein2
3. Konnektor(A) - Konstruktion von aus
4. UndGatter() - Konstruktion des UndGatter-Objekts


29
Bei der Versorgung der Konstruktoren mit Argumenten kann ganz oder teilweise auf Default-
Parameter zurckgegriffen werden. Der folgende UndGatter-Konstruktor ist daher gleich-
wertig zu dem oben aufgefhrten.

Beispiel 3.2 (Forts.):
UndGat t er : : UndGat t er ( voi d) : aus( A)
{
}

Mit dem Lschen eines Objekts werden dessen Elementobjekte ebenfalls gelscht.
Die Destruktion der Elementobjekte erfolgt nach der Destruktion des umfassenden Objekts
in der umgekehrten Reihenfolge ihrer Erzeugung.
Falls die umschlieende Klasse ber keinen Destruktor verfgt, bernimmt der Standard-
destruktor die Aktivierung der Destruktoren der Elementobjekte.


3.3.2 Zeiger auf Objekte als Komponenten

Als eine Alternative zu den Elementobjekten kann eine Klasse Zeiger auf Klassenobjekte
enthalten.

Beispiel 3.3: (Zeiger auf Objekte als Komponenten)
cl ass UndGat t er {
Konnekt or *ei n1;
Konnekt or *ei n2;
Konnekt or *aus;
voi d schal t en( voi d) ;
publ i c:
UndGat t er ( voi d) ;
~UndGat t er ( voi d) ;
i nt set Ei ngangsSi gnal ( unsi gned nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) ;
};

Bemerkungen
- Abweichend zu Beispiel 3.2 sind Eingnge und der Ausgang hier durch Zeiger auf Kon-
nektor-Objekte reprsentiert.


ein1
ein2
aus
Speicherbereich des -O Konnektor ein1 bjekts
Speicherbereich des -Objekts Konnektor ein2
Speicherbereich des -Objekts Konnektor aus
Speicherbereich des -Objekts UndGatter


Abb. 3.2: Speicherabbild eines UndGatter-Objekts mit Zeigern auf Konnektor-Objekte

30
- Da es sich bei den ber Zeiger referenzierten Objekten speicherplatzmig um separate,
eigenstndige Konnektor-Objekte im Freispeicher handelt (siehe Abb. 3.2), findet in
diesem Fall keine automatische Erzeugung und Initialisierung statt. Diese muss explizit
im Konstruktor der umfassenden Klasse vorgenommen werden.

Beispiel 3.3 (Forts.): (Explizite Konstruktion der member-Objekte)
UndGat t er : : UndGat t er ( voi d)
{
ei n1 = new Konnekt or ( E) ;
ei n2 = new Konnekt or ( E) ;
aus = new Konnekt or ( A) ;
}

Mit dem Lschen des umfassenden Objekts mssen ber Zeiger referenzierte Objekte eben-
falls gelscht werden. Dieses muss - wie auch die Erzeugung - explizit erfolgen. Zu diesem
Zweck ist ein Destruktor erforderlich, welcher das Lschen der Konnektor-Objekte ber-
nimmt.

Beispiel: 3.3 (Forts.): (explizite Destruktion der member-Objekte)
UndGat t er : : ~UndGat t er ( voi d)
{
del et e ei n1; / / expl i zi t es Lschen
del et e ei n2; / / der ber Zei ger
del et e aus; / / r ef er enzi er t en Obj ekt e
}


3.3.3 Felder von Objekten

Objekte knnen auch in Form von Feldkomponenten auftreten. Das folgende Beispiel defi-
niert ein 2-elementiges Feld von Konnektor-Objekten:

Konnekt or ei n[ 2] ; / / Fel d von 2 Gat t er ei ngngen

Fr jedes Klassenobjekt des Felds wird dessen Konstruktor aufgerufen.
Eine Parameterbergabe an den Konstruktor ist in diesem Falle nicht mglich. D.h., die
Klasse muss ber einen parmeterlosen Standardkonstruktor verfgen.
Das Beispiel 3.2 soll dahingehend gendert werden, dass die Eingnge eines UND-Gatters als
zwei-elementiges Feld von Konnektor-Objekten deklariert werden (siehe auch Abb. 3.3).

Beispiel 3.4: (Felder von Objekten als Komponenten)
cl ass UndGat t er {
Konnekt or ei n[ 2] ; / / 2 Ei ngnge
Konnekt or aus; / / Ausgang
publ i c:
UndGat t er ( voi d) ;
/ / . . .
};



31
ein[0]
ein[1]
aus
Speicherbereich des Felds von -Objekten ein Konnektor
Speicherbereich des -Objekts Konnektor aus
Speicherbereich des -Objekts UndGatter


Abb. 3.3: Speicherabbild eines UndGatter-Objekts mit einem Feld von Konnektor-
Objekten

In diesem Beispiel wird ein parameterloser Standardkonstruktor zur Initialisierung der Objek-
te des Felds ein bentigt. Zudem wird ein parametrierter Konstruktor zur Initialisierung des
Objekts aus bentigt. Erreicht wird dies durch berladen des Konstruktors.

Beispiel 3.4 (Forts.): (Felder von Objekten als Komponenten)
cl ass Konnekt or {
KonTyp t yp;
Si gnal si g;
publ i c:
Konnekt or ( voi d) { t yp = E; si g = L; }
Konnekt or ( KonTyp t ) { t yp = t ; si g = L; }
/ / . . .
};

Bemerkungen
- Der parametrierte Konstruktor darf in diesem Falle aus Grnden der Eindeutigkeit keinen
Default-Wert fr den Parameter t enthalten.
- Der Konstruktor der Klasse UndGatter bentigt in der Elementinitialisierungsliste ledig-
lich einen Eintrag fr den Ausgang aus.

Beispiel 3.4 (Forts.): (Felder von Objekten als Komponenten)
UndGat t er : : UndGat t er ( voi d) : aus( A)
{
}

Beim Lschen eines Felds von Klassenobjekten wird automatisch fr jede Feldkomponen-
te der Destruktor bzw. Standarddestruktor der Klasse aufgerufen.


3.3.4 Dynamisch allokierte Felder von Objekten

Die Klasse UndGatter soll nun dahingehend gendert werden, dass eine variable Anzahl von
Eingngen mglich ist. Hierzu mssen die Konnektor-Objekte in einem variabel langen Feld
angelegt werden. Da dieses dynamisch allokiert werden muss, enthlt die UndGatter-Klasse
- hnlich wie in 3.3.2 - Zeiger auf den Vektor mit Gattereingngen bzw. auf das Objekt fr
den Gatterausgang (siehe auch Abb. 3.4).


32
Beispiel 3.5: (Dynamische Felder von Objekten)
cl ass UndGat t er {
unsi gned anzEi n; / / Anz. der Ei ngnge
Konnekt or *ei n; / / Fel d von Ei ngngen
Konnekt or *aus; / / Ausgang
publ i c:
UndGat t er ( unsi gned anz = 2) ; / / st andar dmi g 2 Ei ngnge
~UndGat t er ( voi d) ;
/ / . . .
};

Bemerkungen
- Die Klasse verfgt ber eine Instanzvariable anzEin, welche die Anzahl der Eingnge des
Objekts angibt.

ein
anzEin
aus
Speicherbereich des Felds mit -O ein Konnektor bjekten
Speicherbereich des -Objekts Konnektor aus
Speicherbereich des -Objekts UndGatter


Abb. 3.4: Speicherabbild eines UndGatter-Objekts mit Zeiger auf ein dynamisch allokiertes
Feld von Konnektor-Objekten

Der Konstruktor erzeugt eine variable Anzahl von Konnektor-Objekten fr die Eingnge
sowie ein Konnektor-Objekt fr den Ausgang.

Beispiel 3.5 (Forts.): (Dynamische Felder von Objekten)
UndGat t er : : UndGat t er ( unsi gned anz)
{
anzEi n = anz;
ei n = new Konnekt or [ anzEi n] ;
aus = new Konnekt or ( A) ;
}

Auch bei der Erzeugung eines Felds von Objekten mittels new[] wird implizit fr jede
Feldkomponente ein parameterloser Standardkonstruktor aufgerufen.
Beim Lschen eines Felds von Objekten mittels delete[] wird fr jede Feldkomponente
der Destruktor aufgerufen.

Beispiel 3.5(Forts.): (Dynamische Felder von Objekten)
UndGat t er : : ~UndGat t er ( voi d)
{
del et e[ ] ei n;
del et e aus;
}


33
3.4 Geschachtelte Klassen

Klassendeklarationen knnen geschachtelt werden. Dies soll an einer Variante von Beispiel
3.2 gezeigt werden:

Beispiel 3.6: (Geschachtelte Klassen)
enumSi gnal { L, H };

cl ass UndGat t er {
enumKonTyp { E, A }; / / E = Ei ngang, A = Ausgang

/ / Dekl ar at i on der i nner en Kl asse
cl ass Konnekt or {
KonTyp t yp;
Si gnal si g;
publ i c:
Konnekt or ( KonTyp t = E) ;
voi d set Si gnal ( Si gnal s) ;
Si gnal get Si gnal ( voi d) ;
};

/ / I nst anzvar i abl en
Konnekt or ei n1;
Konnekt or ei n2;
Konnekt or aus;
voi d schal t en( voi d) ;
publ i c:
UndGat t er ( voi d) ;
i nt set Ei ngangsSi gnal ( unsi gned nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) ;
};

Bemerkungen:
- Die gegenber Beispiel 3.2 unvernderte Klasse Konnektor ist im privaten Teil der Klas-
se UndGatter deklariert. Dies bewirkt, dass die Klasse auerhalb des Gltigkeitsbereichs
von UndGatter nicht sichtbar ist.

Bei der Definition der Elementfunktionen fr die Klasse Konnektor ist eine zweistufige
Qualifizierung notwendig:

Beispiel 3.6 (Forts.): (Geschachtelte Klassen)

i nl i ne voi d UndGat t er : : Konnekt or : : set Si gnal ( Si gnal s)
{
si g = s;
}




34
3.5 Friends

In vielen Fllen ist eine Klasse logisch eng verzahnt mit einer anderen Klasse. Ein Beispiel
hierfr bilden die Klassen UndGatter und Konnektor. Konnektor-Objekte werden immer
in Verbindung mit Objekten der Klasse UndGatter verwendet. Insofern ist es aus Effizienz-
gesichtspunkten denkbar, den Elementfunktionen der Klasse UndGatter direkten Zugriff zu
privaten Komponenten der Klasse Konnektor zu gewhren. Dieses kann mit Hilfe von
friend-Deklarationen erreicht werden.

Eine Funktion, welche in einer Klasse als friend-Funktionen deklariert ist, besitzt Zu-
griff zu privaten Komponenten der jeweiligen Objekte, ist aber selbst keine Komponente
der Klasse.

Die Klasse Konnektor aus Beispiel 3.3 wird derart modifiziert, dass die Elementfunktion
UndGatter::schalten() als friend definiert wird.

Beispiel 3.7: (Friend-Funktionen)
cl ass Konnekt or {
/ / . . .
f r i end voi d UndGat t er : : schal t en( voi d) ;
};

Die Elementfunktion UndGatter::schalten() kann nun wie folgt definiert werden:

Beispiel 3.7 (Forts.): (Friend-Funktionen)
voi d UndGat t er : : schal t en( voi d)
{
aus- >si g = ( Si gnal ) ( ei n1- >si g && ei n2- >si g) ;
}

bersetzungstechnisch stellt dies ein Problem dar, weil die Klasse UndGatter den Typ
Konnektor benutzt. Die Klasse Konnektor ihrerseits verwendet den Typ UndGatter. Bei
einer derart zyklischen Verwendung von Typnamen ist die Vorabdeklaration eines Typna-
mens erforderlich.

Beispiel 3.7 (Forts.): (Friend-Funktionen)
enumSi gnal { L, H };

cl ass Konnekt or ; / / Vor abdekl ar at i on

cl ass UndGat t er {
Konnekt or *ei n1;
Konnekt or *ei n2;
Konnekt or *aus;
publ i c:
/ / . . .
};



35
Unter anderem wegen dieser bersetzungsproblematik wird vielfach eine ganze Klasse als
friend deklariert. In diesem Falle knnen alle Elementfunktionen der als friend deklarierten
Klasse auf private Elemente zugreifen.

Beispiel 3.7 (Forts.): (Friend-Klassen)
cl ass Konnekt or {
/ / . . . .
publ i c:
/ / . . .
f r i end cl ass UndGat t er ; / / f r i end- Kl asse
};

Auch eine beliebige C-Funktion kann als friend einer Klasse definiert werden. friend-
Deklarationen sollten aber nur dort eingesetzt werden, wo der Vorteil des einfacheren Zu-
griffs den Verlust der Datenkapselung rechtfertigt.


3.6 Statische Komponenten

Eine als static deklarierte Datenkomponente wird von allen Objekten der Klasse gemeinsam
benutzt. Die Zugriffsrestriktionen entsprechen denen von nicht-statischen Komponenten; d.h.
auch eine als static deklarierte Komponente ist "von auen" nur zugreifbar, wenn sie als
public deklariert ist.

Die Klasse Konnektor aus Beispiel 3.2 soll um einen Zhler erweitert werden, welcher ber
die Anzahl der erzeugten Objekte Buch fhrt.

Beispiel 3.8: (Statische Komponenten)
cl ass Konnekt or {
KonTyp t yp;
Si gnal si g;
st at i c unsi gnd anz;
publ i c:
Konnekt or ( KonTyp t = E) ;
~Konnekt or ( ) ;
voi d set Si gnal ( Si gnal s) ;
Si gnal get Si gnal ( voi d) ;
st at i c unsi gned get Anz( voi d) ;
};

Mit der Deklaration einer Datenkomponente als static wird noch kein Speicherplatz reser-
viert. Hierzu ist zustzlich eine externe Definition der Variablen notwendig. Das Schls-
selwort static darf hierbei nicht erneut angegeben werden, da dieses mit der in C blichen
Bedeutung von static kollidieren wrde.

Beispiel 3.8: (Forts.)
unsi gned Konnekt or : : anz = 0;


36
Mit jeder Erzeugung eines Objekts von Typ Konnektor wird der Zhler Konnektor::anz im
Konstruktor inkrementiert. Im Destruktor wird Konnektor::anz dekrementiert. Innerhalb der
Klasse knnen statische Komponenten durch bloe Angabe ihres Namens angesprochen wer-
den.

Beispiel 3.8: (Forts.)
Konnekt or : : Konnekt or ( KonTyp t )
{
t yp = t ;
si g = L;
anz ++;
}

Konnekt or : : ~Konnekt or ( )
{
anz- - ;
}

Analog zu statischen Datenkomponenten knnen Elementfunktionen, welche als static dekla-
riert sind, nur in Verbindung mit dem Klassennamen, nicht ber ein Objekt, aufgerufen wer-
den. Da solche Funktionen nicht ber einen this-Zeiger verfgen, haben sie lediglich Zugriff
zu static-Komponenten.
Im Beispiel dient die statische Elementfunktion Konnektor::getAnz() als Zugriffsfunktion
auf die statische Datenkomponente Konnektor::anz.

Beispiel 3.8: (Forts.)
unsi gned Konnekt or : : get Anz( voi d)
{
r et ur n anz;
}

Auerhalb der Klasse erforderte der Zugriff auf eine statische Komponente oder der Aufruf
einer statischen Elementfunktion einen qualifizierten Namen.

Beispiel 3.8: (Forts.)
#i ncl ude <i ost r eam>
#i ncl ude " gat e_b34. h"

usi ng namespace st d;

/ / Anwendung der Kl asse UndGat t er

i nt mai n ( )
{
UndGat t er gat e1, gat e2;

cout << " Es wur den " << Konnekt or : : get Anz( ) << " Konnekt or en er zeugt \ n" ;
r et ur n 0;
}



37
3.7 bungsaufgaben

Aufgabe 3.1
Entwickeln Sie eine Klasse namens IntVector, welche einen int-Array (Typ int*) kapselt.
Die Klasse verfgt ber
- einen Konstruktor, welcher als Parameter die Lnge (Typ unsigned) des dynamisch anzu-
legenden int-Array angibt,
- eine Elementfunktion size(), mit der sich die Lnge der Liste abfragen lsst.
- eine Elementfunktion at(), welche eine Referenz auf das i-te Element des int-Array zurck
liefert, falls i ein gltiger Index ist. Bei einem ungltigen Index bricht die Elementfunktion
das Programm mit der Fehlermeldung index overflow ab.
- eine Ausgabefunktion out(), welche den gekapselten int-Array zeilenweise ausgibt.
Entscheiden Sie selbst, ob die Klasse einen Destruktor bentigt.

Schreiben Sie eine main()-Funktion, welche die IntVector-Klasse analog zur main()-
Funktion in bung 2.2 benutzt.


Aufgabe 3.2
Windows verfgt ber eine Funktion namens Beep(), welche einen Ton ber den Lautspre-
cher ausgibt. Die Funktion ist in der Header-Datei <windows.h> wie folgt deklariert:
bool Beep( unsi gned i nt freq, unsi gned i nt duration) ;
freq gibt die Tonhhe an.
- duration gibt die Dauer des Tons in ms an.

(a) Schreiben Sie eine Klasse namens Beeper, welche wie folgt verwendet werden kann:

i nt mai n( )
{
Beeper *beeper ;
beeper = new Beeper ( 500, 1000) ; / / Fr equenz 500, Dauer 1000 ms
beeper - >beep( ) ; / / Tonausgabe
del et e beeper ;
}

(b) In gewissen Fllen erfordert es eine Anwendung, dass nur genau eine Instanz von einer
Klasse erzeugt werden darf. Dieses kann auf folgende Weise erreicht werden:
- Der Konstruktor wird private deklariert.
- Eine als public definierte static-Elementfunktion create() prft, ob bereits eine In-
stanz existiert.
. Falls nein, erzeugt create() eine neue Instanz und liefert einen Zeiger auf diese zu-
rck.
. Falls ja, liefert create() einen Zeiger auf die bereits existierende Instanz zurck.
(b1) Schreiben Sie die Klasse Beeper so um, dass nur eine Instanz erzeugt werden kann.
(b2) Passen Sie die oben angegebene main()-Funktion in geeigneter Weise an.

38
Aufgabe 3.3
Die Klasse IntVector aus bungsaufgabe 3.1 soll um einen sogenannten Iterator ergnzt
werden. Hierbei handelt es sich um ein allgemeines Prinzip, welches einen sukzessiven Zu-
griff auf alle Elemente eines Containers (im Beispiel ein IntVector-Objekt) ermglicht.

Ein Iterator ist ein Objekt einer Klasse, welche ber eine Elementfunktion next( ) verfgt.
Diese liefert mit jedem Aufruf einen Zeiger auf ein Inhaltselement eines IntVector-
Containers zurck. Beim ersten Aufruf ist dies ein Zeiger auf das 1. Listenelement, beim 2.
Aufruf wird ein Zeiger auf das 2. Listenelement zurck geliefert, usw. Falls das Listenende
erreicht ist, liefert next( ) einen NULL-Zeiger zurck. Der nachfolgende Programmausschnitt
zeigt die Anwendung eines Iterators:

I nt Vect or vect or ;
i nt *el ;

/ / vect or f uel l en

I t er at or i t ( &vect or ) ;

whi l e ( ( el = i t . next ( ) ) ! = NULL)
cout << *el << ' \ n' ;

Hinweise:
- Da das Iterator-Objekt Zugriff auf die privaten Daten des IntVector-Objekts bentigt,
muss die Iterator-Klasse als friend der IntVector-Klasse definiert werden.
- Bei der Erzeugung eines Iterator-Objekts muss der Bezug zum jeweiligen IntVector-
Objekt hergestellt werde. Hierzu muss das IntVector-Objekt als Zeiger oder Referenz an
den Konstruktor der Iterator-Klasse bergeben werden.


Aufgabe 3.4 (Allgemeine Fragen)
(a) Was bewirkt das Attribut const hinter der Deklaration einer Elementfunktion?
(b) In einer Elementfunktion wird der Ausdruck this->x verwandt. Geben Sie eine alternative
Schreibweise fr den Ausdruck an.
(c) Wann bentigt man eine Elementinitialisierungsliste?
(d) Wie viele Konstruktoren kann eine Klasse haben?
(e) Fr einen Klassentyp Student existiert lediglich der folgende Konstruktor:
Student(long matrNr);
Ist die Vereinbarung Student semester[30]; korrekt? Wenn nein, warum nicht?
(f) Welche Aufgabe hat ein automatisch generierter Standardkonstruktor?
(g) Warum kann man aus einer static-Elementfunktion nicht auf Instanzvariablen zugreifen?

39
4. Vererbung

Whrend das Klassenkonzept auch in prozeduralen Programmiersprachen enthalten ist, ist die
Vererbung eine spezielle Eigenschaft der objektorientierten Programmierung.

Der Mechanismus der Vererbung erlaubt es, eine neue Klasse von einer (oder mehreren)
bestehenden Klassen abzuleiten. Die abgeleitete Klasse erbt alle Eigenschaften und Fhig-
keiten der bestehenden Klasse(n).


4.1 Abgeleitete Klassen

Die Klasse UndGatter aus Beispiel 3.1 soll so modifiziert werden, dass bei der Pro-
grammausfhrung ein Log mit den ausgefhrten Schaltvorgngen ausgegeben wird.

Bei herkmmlicher Programmierung wrde man die Klasse UndGatter auf Quellpro-
grammebene in geeigneter Weise modifizieren. Zur Realisierung der Protokollierungsfunkti-
onalitt sind folgende Erweiterungen erforderlich:
(1) Identifikation eines UndGatter-Objekts in Form eines druckbaren Namens
(2) Erweiterung der Elementfunktionen UndGatter::Schalten() um die geforderte Logaus-
gabe.


Deklaration einer abgeleiteten Klasse
Mit den Mglichkeiten der Vererbung kann die bestehende Klasse unverndert bleiben. Die
geforderten Erweiterungen werden in einer abgeleiteten Klasse namens UndGatterMitLog
realisiert. Hierbei wird auf die existierende Klasse UndGatter Bezug genommen.

Beispiel 4.1: ( Deklaration abgeleiteter Klassen)
enumSi gnal { L, H };

/ / Dekl ar at i on der Kl asse UndGat t er
cl ass UndGat t er {
Si gnal ei n1;
Si gnal ei n2;
Si gnal aus;
publ i c:
UndGat t er ( Si gnal s1, Si gnal s2) ;
i nt set Ei ngangsSi gnal ( i nt nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) ;
voi d schal t en( voi d) ;
};



40
Beispiel 4.1 (Forts.): ( Deklaration abgeleiteter Klassen)

/ / Dekl ar at i on der Kl asse UndGat t er Mi t Log
cl ass UndGat t er Mi t Log : publ i c UndGat t er {
char *pr i nt Name; / / dr uckbar er Name
publ i c:
UndGat t er Mi t Log( Si gnal s1, Si gnal s2, char *name=" noname" ) ;
voi d schal t en( voi d) ;
char * get Pr i nt Name( ) ;
};

Bemerkungen
- Die Deklaration einer abgeleiteten Klasse erfolgt unter Bezugnahme auf eine Basisklasse.
- UndGatterMitLog enthlt eine zustzliche Instanzvariable printName, welche einen
druckbaren Namen fr das Gatter enthlt, sowie eine zustzliche Elementfunktion get-
PrintName(), welche den Zugriff auf printName erlaubt. Weiterhin ist die Elementfunkti-
on schalten() redefiniert.


Syntax: class class-name : { access-spec }
opt
class-name {
member-liste
};

access-spec : : = { public | private }
1


- access-spec regelt fr ein Objekt der abgeleiteten Klasse die Zugriffsmglichkeiten zu den
von der Basisklasse geerbten Elementen. Bei Angabe von public wird die Zugriffsspezifi-
kation der Basisklasse bernommen; d.h private-Elemente bleiben private und public-
Elemente bleiben public (nheres siehe 4.2).


Ein Objekt einer abgeleiteten Klasse verfgt ber alle Instanzvariablen und Elementfunk-
tionen der Basisklasse.
In der abgeleiteten Klasse knnen neue, zustzliche Instanzvariablen und Elementfunktio-
nen definiert werden. Darber hinaus knnen geerbte Elementfunktionen redefiniert wer-
den. Dabei verliert die geerbte Elementfunktion die Gltigkeit fr Objekte der abgeleiteten
Klasse.

Ableitungsbeziehungen drcken eine ist ein-Beziehung aus und werden wie folgt grafisch
veranschaulicht:



UndGatter
UndGatterMitLog


Abb. 4.1: Ableitungsbeziehung


41
Wie Abb. 4.2 zeigt enthlt das Speicherabbild eines Objekts einer abgeleiteten Klasse das
Speicherabbild seiner Basisklasse; d.h. es enthlt alle in der Basisklasse deklarierten In-
stanzvariablen.


ein1
ein2
aus
printName
Speicherbereich der Klasse UndGatter
Speicherbereich der Klasse UndGatterMitLog


Abb. 4.2: Speicherabbild der Klasse UndGatterMitLog


Elementfunktionen in abgeleiteten Klassen
Elementfunktionen einer abgeleiteten Klasse knnen auf public-Elemente der Basisklasse
ohne Angabe des Objekts zugreifen; d.h. fr Elemente der Basisklasse wird ebenfalls der
Zeiger this als Bezugsadresse verwendet. Auf private-Elemente der Basisklasse besteht auch
innerhalb von Elementfunktionen der abgeleitete Klasse kein Zugriff.

Eine Elementfunktion einer abgeleiteten Klasse kann eine redefinierte Elementfunktion einer
Basisklasse aufrufen. In diesem Falle ist ein qualifizierter Zugriff mit Angabe der Basisklas-
se erforderlich.

Beispiel 4.1 (Forts.): ( Elementfunktionen abgeleiteter Klassen)
/ / El ement f unkt i onen der Kl asse UndGat t er

UndGat t er : : UndGat t er ( Si gnal s1, Si gnal s2)
{
ei n1 = s1;
ei n2 = s2;
schal t en( ) ;
}

i nt UndGat t er : : set Ei ngangsSi gnal ( i nt nr , Si gnal si g)
{
swi t ch ( nr )
{ case 1 : ei n1 = si g;
br eak;
case 2 : ei n2 = si g;
br eak;
def aul t : r et ur n - 1; / / Fehl er
}
schal t en( ) ;
r et ur n 0;
}

42
Beispiel 4.1 (Forts.): ( Elementfunktionen abgeleiteter Klassen)

/ / El ement f unkt i onen der Kl asse UndGat t er Mi t Log

voi d UndGat t er Mi t Log: : schal t en( voi d)
{
UndGat t er : : schal t en( ) ;
cer r << pr i nt Name << " geschal t et \ n" ;
}


Konstruktoren und Destruktoren
Konstruktoren und Destruktoren werden nicht geerbt. Jede Klasse ist fr die Initialisierung
der in ihr definierten Instanzvariablen selbst verantwortlich. Deshalb werden der Konstruktor
und Destruktor der Basisklasse automatisch vom Konstruktor bzw. Destruktor der abgeleite-
ten Klasse aktiviert. Die Erzeugung eines Objekts vollzieht sich allgemein gem folgender
Konstruktionsreihenfolge:

Der Konstruktor der Basisklasse wird vor den Konstruktoren der Elementobjekte und die-
se wiederum vor dem Konstruktor der abgeleiteten Klasse ausgefhrt.

Wenn der Konstruktor der Basisklasse Argumente erwartet, mssen diese vom Konstruktor
der abgeleiteten Klasse geliefert werden. Die Initialisierung der Basisklasse erfolgt analog zur
Initialisierung von Elementobjekten in der Initialisierungsliste des Konstruktors der abgelei-
teten Klasse. Anstelle des Elementnamens tritt der Name der Basisklasse. Der Konstruktor
der Klasse UndGatterMitLog hat daher das folgende Aussehen:

Beispiel 4.1 (Forts.): ( Konstruktoren abgeleiteter Klassen)
UndGat t er Mi t Log: : UndGat t er Mi t Log( Si gnal s1, Si gnal s2, char * name)
: UndGat t er ( s1, s2)
{
pr i nt Name = name;
}


Die Ausfhrungsreihenfolge der Destruktoren ist umgekehrt zu der der Konstruktoren;
d.h. der Destruktor einer abgeleiteten Klasse wird vor den Destruktoren der Elementob-
jekte und vor dem Destruktor der Basisklasse ausgefhrt.



43
4.2 Zugriffskontrolle

Mit Einfhrung der Vererbung erweitert sich die Fragestellung der Element-
Zugriffskontrolle:

(1) Wie kann in Elementfunktionen einer abgeleiteten Klasse auf Elemente einer Basisklasse
zugegriffen werden?
(2) Wie kann bei einem Objekt einer abgeleiteten Klasse von auen auf Elemente zugegrif-
fen werden, welche in einer Basisklasse deklariert sind?

Zur Klrung von Fragen (1) und (2) ist eine kombinierte Betrachtung des in der Basisklasse
spezifizierten Zugriffs und der Zugriffsspezifikation in der Ableitung erforderlich.


Elementzugriff
In Verbindung mit dem Konzept der Vererbung existiert eine zustzliche Zugriffskategorie
protected fr die Elemente einer Klasse. Die Elemente einer Klasse knnen damit private,
public oder protected sein.

- private: Der Name kann in Elementfunktionen der Klasse und in Friends der Klasse
verwendet werden.
- protected: Der Name kann in Elementfunktionen der Klasse, in Friends der Klasse und
in Elementfunktionen von abgeleiteten Klassen verwendet werden.
- public: Der Name kann in beliebigen Funktionen verwendet werden.


Zugriff auf Basisklassen
Die Basisklasse einer abgeleiteten Klasse kann private oder public sein. Dieses wird durch
die Zugriffsspezifikation in der Ableitung festgelegt. Bei fehlender Zugriffsspezifikation wird
private angenommen. Mit Hilfe der Zugriffsspezifikation in der Ableitung kann fr Objekte
der abgeleiteten Klasse der Zugriff auf geerbte Elemente eingeschrnkt werden (siehe Tabelle
4.1).

Tabelle 4.1: Zugriffskontrolle

Zugriff in Basisklasse Ableitung geerbter Zugriff
public
protected
private

public

public
protected
nicht zugreifbar
public
protected
private

private

private
private
nicht zugreifbar


44
Wie bereits in 4.1 beschrieben, ndert sich bei einer public-Ableitung nichts an den Zugriffs-
gegebenheiten der aus der Basisklasse geerbten Komponenten. Man bezeichnet diese Form
der Ableitung deshalb auch als Vererbung der Schnittstelle. Bei der private-Ableitung

cl ass Der i ved : pr i vat e Base {. . . };

sind ber ein Derived-Objekt keine Komponenten von Base zugreifbar. Auch sind in den von
Derived abgeleiteten Klassen keine Komponenten der Basisklasse Base verfgbar. Diese
totale Abschottung ist nur dann sinnvoll, wenn die Klasse Base ausschlielich zur Implemen-
tierung von Derived dient oder wesentliche Teile von Base in Derived redefiniert werden.
Man bezeichnet diese Ableitung deshalb auch als Vererbung der Implementierung.


4.3 Konvertierbarkeit von Zeigern

Eine Klasse namens Derived sei von einer Klasse namens Base abgeleitet. Einem Zeiger
vom Typ Base* kann ein Zeiger von Typ Derived* ohne explizite Typkonvertierung zuge-
wiesen werden. Die Typkonvertierung findet implizit statt. Bei Zuweisung eines Zeigers vom
Typ Base* an einen Zeiger vom Typ Derived* ist eine explizite Typkonvertierung erforder-
lich (siehe auch Abb. 4.3).




Abb 4.3: Zeigerkonvertierung


Die automatische Konvertierung von Derived* nach Base* ist deshalb unkritisch, weil mit
dem Zeiger Base* nur auf die in der Basisklasse Base deklarierten Komponenten zugegriffen
werden kann, die ja in jedem Fall in einem Objekt der Klasse Derived vorliegen.

UndGat t er Mi t Log gl ( L, L, G1) ;
UndGat t er *pg = &gl ; / / ok

pg- >set Ei ngangsSi gnal ( 1, H) ; / / ok
cout << pg- >get Pr i nt Name( ) ; / / Compi l er f ehl er , get Pr i nt Name( )
/ / i st ber UndGat t er - Zei ger
/ / ni cht zugr ei f bar




45
Die Konvertierung von Base* nach Derived* - auch als Downcast bezeichnet - ist dagegen
kritisch, da ber den Derived-Zeiger zustzlich auf die Komponenten der abgeleiteten Klas-
se zugegriffen werden kann, die jedoch in einem Objekt der Klasse Base nicht existieren.
Dies wird im nachfolgenden Programmabschnitt verdeutlicht:

UndGat t er g( L, L) ;
UndGat t er Mi t Log *pgl = ( UndGat t er Mi t Log *) &g; / / kr i t i sch !
cout << pgl - >get Pr i nt Name( ) ; / / Lauf zei t f ehl er , Var i abl e
/ / pr i nt Name exi st i er t ni cht .

Die beschriebene implizite Konvertierbarkeit eines Zeigers Derived* nach Base* gilt aller-
dings nur fr public-Ableitungen. Bei private-Ableitungen wrde die gewnschte Zugriffs-
einschrnkung in der Ableitung durch eine implizite Zeigerkonvertierung wieder aufgehoben,
wie das nachfolgende Beispiel zeigt:

cl ass Base {
publ i c:
i nt x;
};

cl ass Der i ved : pr i vat e Base {
};

i nt mai n( )
{
Der i ved d;
Base *b = &d; / / Fehl er ( *)

b- >x = 0; / / Fal l s ( *) ok, wr e Zugr i f f auf
/ / pr i vat es El ement x zul ssi g
}



46
4.4 Virtuelle Funktionen

Das Programm in Beispiel 4.1 hat in der vorliegenden Form nicht den gewnschten Effekt.
Bei Ausfhrung des Programms erscheint lediglich die folgende Ausgabe auf dem Bild-
schirm:

Ausgang: H

Der Schaltvorgang wird offensichtlich korrekt ausgefhrt, wie die Ausgabe verdeutlicht. Die
Log-Ausgabe anlsslich der Schaltvorgnge unterbleibt jedoch. Der Grund hierfr liegt in
folgendem Sachverhalt:

Beim Aufruf einer Elementfunktion ber einen Zeiger bestimmt der Typ des Zeigers die
aufgerufene Funktion.

Im Beispiel 4.1 wird die Elementfunktion schalten() in der Elementfunktion UndGatter::
setEingangsSignal() aufgerufen. Der Aufruf erfolgt unter Verwendung des Zeigers this vom
Typ UndGatter*. Aufgerufen wird daher die Elementfunktion UndGatter::schalten(). Das
gewnschte Verhalten in Beispiel 4.1 kann durch eine virtuelle Funktion schalten() erreicht
werden.

Beim Aufruf einer virtuellen Elementfunktion ber einen Zeiger bestimmt der Typ des
Objekts, auf das der Zeiger verweist, die aufgerufene Funktion.

Eine Funktion ist virtuell, wenn sie selbst als virtual deklariert ist oder eine Funktion in der
Basisklasse mit gleicher Signatur (Folge der Paramtertypen) als virtual deklariert ist.

Da es sich bei dem in Beispiel 4.1 verwendeten Gatter um ein Objekt vom Typ UndGatter-
MitLog handelt, muss die Funktion UndGatter::schalten() als virtual vereinbart werden,
um die gewnschte Protokollierungsfunktionalitt zu erhalten.

Beispiel 4.1 (Forts.): (Virtuelle Elementfunktionen)
cl ass UndGat t er {
/ / . . .
vi r t ual voi d schal t en( voi d) ;
};

Die Eigenschaft virtueller Funktionen wird unter folgenden Umstnden wirksam:
(1) Eine Elementfunktion einer Basisklasse wird in einer abgeleiteten Klasse redefiniert.
(2) Der Zugriff auf die Funktion erfolgt ber einen Zeiger oder ein Referenz.

47
Der Unterschied zwischen nichtvirtuellen und virtuellen Funktionen soll durch das folgende
Beispiel weiter verdeutlicht werden.

cl ass Base {
/ / . . .
publ i c:
voi d f ( voi d) ;
vi r t ual voi d g( voi d) ;
};

cl ass Der i ved : publ i c Base {
/ / . . .
publ i c:
voi d f ( voi d) ;
vi r t ual voi d g( voi d) ;
};

i nt mai n( )
{ Der i ved *dp = new Der i ved;
Base *bp = dp;

dp- >f ( ) ; / / Auf r uf von Der i ved: : f ( )
bp- >f ( ) ; / / Auf r uf von Base: : f ( )

dp- >g( ) ; / / Auf r uf von Der i ved: : g( )
bp- >g( ) ; / / Auf r uf von Der i ved: : g( )
}


Das mit Hilfe von virtuellen Funktionen erzielte Verhalten wird in der objektorientierten
Theorie als Polymorphismus bezeichnet. Polymorphismus bedeutet, dass die Interpretation
einer Nachricht mit dem Typ des empfangenden Objekts variieren kann.


4.4.1 Sptes Binden

Da der tatschliche Objekttyp vom Typ des referierenden Zeigers abweichen kann, lsst sich
der Objekttyp zum bersetzungszeitpunkt nicht bestimmen. Hieraus folgt, dass virtuelle
Funktionen zur bersetzungszeit nicht gebunden werden knnen. Der Bindevorgang wird
daher auf den Zeitpunkt des Funktionsaufrufs verschoben, also auf einen Zeitpunkt an dem
der tatschliche Objekttyp feststeht. Man bezeichnet dieses als sptes Binden oder dynami-
sches Binden.

Der Compiler legt fr jede Klasse, welche mindestens eine virtuelle Funktion besitzt, eine
sogenannte virtuelle Methodentabelle (VMT) an. Diese enthlt einen Vektor von Funktions-
zeigern, mit den Adressen der virtuellen Funktionen der Klasse. Dabei verfgen die Versio-
nen einer virtuellen Funktion in den VMTs von Basisklasse und abgeleiteter Klasse ber den
gleichen Tabellenindex. Jedes Objekt einer Klasse mit virtuellen Funktionen enthlt einen
VMT-Zeiger, welcher auf die VMT seiner Klasse verweist. Diese soll an nachfolgendem Bei-
spiel erlutert werden:


48
cl ass A {
/ / . . .
publ i c:
vi r t ual voi d f ( ) ;
vi r t ual voi d g( ) ;
};

cl ass B : publ i c A {
/ / . . .
publ i c:
vi r t ual voi d f ( ) ;
vi r t ual voi d h( ) ;
};

Falls ein Programm zwei Objekte a1 und a2 vom Typ A und ein Objekt b1 vom Typ B er-
zeugt, ergibt sich die in Abb. 4.3 dargestellte Konstellation.


Objekt a1
vom Typ A
Objekt a2
vom Typ A
Objekt b1
vom Typ B
VMT-
Zeiger
...
VMT-
Zeiger
...
VMT-
Zeiger
...
Zeiger auf A::f()
Zeiger auf B::f()
Zeiger auf A::g()
Zeiger auf A::g()
Zeiger auf B::h()
VMT der Klase A
0
0
1
1
2
VMT der Klase B


Abb. 4.4: Implementierung des spten Bindens

Der Aufruf einer virtuellen Funktion erfolgt in 2 Schritten:
- ber das beim Aufruf angegebene Objekt wird dessen VMT bestimmt.
- Mit Hilfe des vom Compiler gelieferten Funktionsindex im Aufruf wird indirekt ber die
VMT zur aufgerufenen Funktion verzweigt.

Virtuelle Funktionen erfordern daher sowohl etwas mehr Rechenzeit beim Aufruf als auch
erhhten Speicherbedarf. Bei der Fragestellung, ob eine Funktion virtuell deklariert werden
soll oder nicht, kann man sich an folgender Regel orientieren:

49
Wenn eine Klasse als Basis fr Ableitungen dient, mssen die Funktionen, deren Imple-
mentierung sich mit der Ableitung ndern kann, virtuell sein. Dies gilt auch fr den De-
struktor.
Konstruktoren drfen jedoch nicht virtuell deklariert werden, da die spte Bindung ein
vollstndig erzeugtes Objekt voraussetzt.


4.4.2 Abstrakte Klassen

Das Standardbeispiel "logische Schaltungen" soll um eine Klasse zur Nachbildung von O-
DER-Gattern erweitert werden. Bei der Realisierung einer Klasse namens OderGatter stellt
man eine groe hnlichkeit mit der Klasse UndGatter fest.

Da Objekte vom Typ OderGatter sich im Hinblick auf die zugrundeliegenden Datenstruktu-
ren nicht von UndGatter-Objekten unterscheiden, besteht naturgem auch berein-
stimmung bei Konstruktor, Destruktor sowie den Zugriffsfunktionen setEingangsSignal()
und getAusgangsSignal(). Der einzige Unterschied besteht in der Elementfunktion schal-
ten(). Ein mglicher Realisierungsansatz knnte darin bestehen, die Klasse OderGatter von
der Klasse UndGatter abzuleiten. Bei genauerem Hinsehen stellt man jedoch fest, dass wei-
tere Gattertypen (z.B. NOT, XOR, AND, NOR, XOR) in den genannten Punkten ebenfalls
identisch sind. Ein konsequenterer Ansatz besteht deshalb darin, eine Klasse Gatter fr ein
abstraktes Objekt zu definieren, in der die Gemeinsamkeiten aller Gattertypen bercksichtigt
sind. Die jeweils unterschiedlichen schalten()-Funktionen werden in den Klassen UndGatter
bzw. OderGatter definiert.

Bei der angedachten Lsung tritt das folgende Problem auf. Die protected Elementfunktion
UndGatter::schalten() bzw. OderGatter::schalten() wird in der Elementfunktion Gat-
ter::setEingangsSignal() aufgerufen. Der hierbei verwendete this-Zeiger ist vom Typ Gat-
ter*. In der Klasse Gatter ist die Elementfunktion schalten() jedoch nicht definiert ist. Es
bedarf daher einer virtuellen Funktion Gatter::schalten(), welche jedoch aufgrund der All-
gemeinheit der Klasse Gatter keine Funktion haben kann.

Eine virtuelle Funktionen, welche in der Deklaration mit 0 initialisiert wird, heit rein vir-
tuelle Elementfunktion. Fr rein virtuelle Elementfunktionen existiert keine Implementie-
rung.

Eine Implementierung erfolgt erst im Rahmen einer Redefinition in einer abgeleiteten Klasse.
Rein virtuelle Funktionen definieren so eine Schnittstelle in einer Basisklasse, welche erst in
den abgeleiteten Klassen zur Verfgung steht.

Eine Klasse mit einer oder mehreren rein virtuellen Funktionen heit abstrakte Klasse.

Von abstrakten Klassen knnen keine Objekte erzeugt werden; sie dienen lediglich als Basis-
klasse. Wird eine rein virtuelle Funktion in einer abgeleiteten Klasse nicht redefiniert, so ist
auch die abgeleitete Klasse eine abstrakte Klasse.

50
Beispiel 4.2: (Abstrakte Klassen)
cl ass Gat t er {
pr ot ect ed:
i nt anzEi n; / / Anzahl der Ei ngaenge
Konnekt or *ei n;
Konnekt or *aus;
vi r t ual voi d schal t en( voi d) = 0;
publ i c:
Gat t er ( i nt anz) ;
~Gat t er ( voi d) ;
i nt set Ei ngangsSi gnal ( i nt nr , Si gnal si g) ;
Si gnal get AusgangsSi gnal ( voi d) ;
};

cl ass UndGat t er : publ i c Gat t er {
pr ot ect ed:
voi d schal t en( voi d) ;
publ i c:
UndGat t er ( i nt anz = 2) ;
};


cl ass Oder Gat t er : publ i c Gat t er {
pr ot ect ed:
voi d schal t en( voi d) ;
publ i c:
Oder Gat t er ( i nt anz = 2) ;
};


Bemerkungen
- In Beispiel 4.2 wird die rein virtuelle Funktion Gatter::schalten() in den abgeleiteten
Klassen UndGatter und OderGatter redefiniert.
- Die Konstruktoren der Klassen UndGatter und OderGatter dienen lediglich zur Weiter-
gabe der Parameter an den Konstruktor der Klasse Gatter.
- Die Klassen UndGatter und OderGatter besitzen je einen vom Compiler generierten
Standarddestruktor, welcher beim Lschen eines Objekts den Destruktor der Klasse Gat-
ter aktiviert.


4.4.3 Ein Praxisbeispiel

Das Beispiel soll den Kern eines Zeichenprogramms nachbilden, in dem grafische Objekte,
z.B. Quadrate und Kreise, gezeichnet werden knnen (siehe Abb.4.5). Der Kern eines solchen
Zeichenprogramms enthlt eine Liste, welche die im Fenster dargestellten grafischen Objekte
verwaltet. In dem Zeichenprogramm sind u.a. die folgenden Operationen mglich:
- Erzeugen eines neuen grafischen Objekts,
- Neuzeichnen aller im Fenster befindlichen grafischen Objekte,
- Identifikation eines mit der Maus selektierten grafischen Objekts.


51
Draw


Abb. 4.5: Zeichenprogramm


Die Eigenschaften (Position und Ausdehnung) der grafischen Objekte sind in den Klassen
Square bzw. Circle definiert. Da die Objekte in einer gemeinsamen Liste verwaltet werden,
wird eine gemeinsame, abstrakte Basisklasse GraphicObject bentigt. GraphicObject defi-
niert eine funktionale Schnittstelle (Zeichnen, Selektieren), welche in den Klassen Square
und Circle implementiert werden muss.

Beispiel 4.3: (Polymorphie)
cl ass Gr aphi cObj ect {
publ i c:
vi r t ual voi d pai nt ( ) = 0;
vi r t ual bool i sI n( i nt x, i nt y) = 0;
};

cl ass Squar e : publ i c Gr aphi cObj ect {
pr ot ect ed:
i nt t op, l ef t , wi dt h;
publ i c:
Squar e( i nt t op, i nt l ef t , i nt wi dt h) ;
vi r t ual voi d pai nt ( ) ;
vi r t ual bool i sI n( i nt x, i nt y) ;
};

cl ass Ci r cl e : publ i c Gr aphi cObj ect {
pr ot ect ed:
i nt x, y, r adi us;
publ i c:
Ci r cl e( i nt x, i nt y, i nt r ) ;
vi r t ual voi d pai nt ( ) ;
vi r t ual bool i sI n( i nt x, i nt y) ;
};


Die Liste zur Verwaltung der grafischen Objekte enthlt Zeiger vom Typ GraphicObject
(siehe Abb. 4.6).


52
Graphic-
Object
Graphic-
Object
Graphic-
Object
...


Abb. 4.6: Liste zur Verwaltung der grafischen Objekte

Beispiel 4.3 (Forts): (Polymorphie)
const unsi gned LEN = 3;
Gr aphi cObj ect * gr aphObj Vect or [ LEN] ;

/ / I nser t gr af i c obj ect s
gr aphObj Vect or [ 0] = new Squar e( 10, 10, 20) ; / / t op, l ef t , wi dt h
gr aphObj Vect or [ 1] = new Squar e( 20, 20, 20) ; / / t op, l ef t , wi dt h
gr aphObj Vect or [ 2] = new Ci r cl e( 20, 20, 10) ; / / x- and y- or i gi n, r adi us

Beim Neuzeichnen aller grafischen Objekte iteriert das Zeichenprogramm ber die Liste der
grafischen Objekte und ruft ber die in den Listenelementen befindlichen Zeiger die poly-
morphe Funktion paint() auf. Die Polymorphie sorgt dafr, dass jeweils die korrekte, typspe-
zifische paint()-Funktionen zur Anwendung kommt.

Beispiel 4.3 (Forts): (Polymorphie)
/ / Pai nt al l gr af i c obj ect s
f or ( unsi gned i =0; i <LEN; i ++)
{ gr aphObj Vect or [ i ] - > pai nt ( ) ;
cout << ' \ n' ;
}

In hnlicher Weise wird bei einem Mausklick ber die Liste iteriert und die polymorphe
Funktion isIn() aufgerufen. Das erste graphische Objekt, welches denWert true zurckliefert,
ist das selektierte Objekt. Falls die neu erzeugten Objekte hinten in der Liste eingefgt wer-
den, ist es wichtig, rckwrts ber die Liste zu iterieren, da im Falle von flchenmigen
berlappungen nur so das oberste grafische Objekt selektiert wird.

Beispiel 4.3 (Forts): (Polymorphie)
cout << " Ent er coor di nat e x, and y \ n" ;
ci n >> x >> y;
f or ( i nt i =LEN- 1; i >=0; i - - ) / / r ever se i t er at i on
i f ( gr aphObj Vect or [ i ] - >i sI n( x, y) )
{ go = gr aphObj Vect or [ i ] ;
go- >pai nt ( ) ;
cout << " sel ect ed\ n" ;
br eak;
}

53
4.5 Mehrfachvererbung

Eine abgeleitete Klasse kann mehrere Basisklassen besitzen. Auf diese Weise wird erreicht,
dass die abgeleitete Klasse die Eigenschaften und Fhigkeiten aller Basisklassen in sich ver-
eint.

Mchte man Objekte der Klasse UndGatter grafisch darstellen, so knnte dies wie folgt er-
reicht werden:
(1) Man realisiere neben der bekannten Klasse UndGatter eine Klasse namens Grafik, wel-
che die grafischen Eigenschaften und Funktionen realisiert.
(2) Man leite eine neue Klasse GrafikUndGatter von den Klassen UndGatter und Grafik
ab.

cl ass UndGat t er {
Si gnal ei n1, ei n2, aus;
publ i c:
UndGat t er ( Si gnal s1, Si gnal s2) ;
/ / . . .
voi d schal t en( voi d) ;
};

cl ass Gr af i k {
i nt x, y; / / Bi l dschi r mkoor di nat en
/ / . . .
publ i c:
Gr af i k( i nt x, i nt y) ;
voi d pai nt ( ) ;
};

cl ass Gr af i kUndGat t er : publ i c UndGat t er ,
publ i c Gr af i k {
publ i c:
Gr af i kUndGat t er ( Si gnal s1, Si gnal s2, i nt x, i nt y) ;
};

Auf Objekte der so abgeleiteten Klasse GrafikUndGatter lassen sich sowohl logische
Funktionen (z.B. schalten()) als auch grafische Funktionen (z.B. paint()) anwenden.

Objekte einer Klasse mit mehreren Basisklassen erben die Instanzvariablen und Elementfunk-
tionen aller Basisklassen. Bei der Erzeugung eines derartigen Objekts werden die Kon-
struktoren aller Basisklassen ausgefhrt (in der Reihenfolge der Ableitung). Fr obiges Bei-
spiel bedeutet dies:

1. UndGat t er ( s1, s2) ;
2. Gr af i k( x, y) ;
3. Gr af i kUndGat t er ( s1, s2, x, y) ;

Die Destruktoren werden in umgekehrter Reihenfolge ausgefhrt.

54
4.5.1 Mehrdeutigkeiten durch Namenskonflikte

Mehrdeutigkeiten durch Namenskonflikte entstehen dann, wenn zwei Basisklassen ber eine
Komponente gleichen Namens verfgen. Ein derartiger Namenskonflikt muss durch Verwen-
dung von qualifizierten Namen in der abgeleiteten Klasse aufgelst werden.

Beispiel 4.4: (Mehrdeutigleiten durch Namenskonflikte)

cl ass A {
publ i c:
i nt i ;
/ / . . .
};

cl ass B {
publ i c:
i nt i ;
/ / . . .
};

cl ass C : publ i c A, publ i c B {
/ / . . .
publ i c:
voi d f ( voi d) { i = 1; } / / Fehl er , wel ches i ?
voi d g( voi d) { A: : i = 1; } / / ok
};

i nt mai n( )
{ C c;

c. i =1; / / Fehl er , wel ches i ?
c. B: : i =1; / / ok
}



4.5.2 Mehrdeutigkeiten durch wiederholtes Vererben

Mehrdeutigkeit durch wiederholtes Vererben entstehen dann, wenn eine Klasse von zwei
Basisklassen abgeleitet wird, die ihrerseits eine gemeinsame Basisklasse haben (siehe Abb.
4.5).

A A
B C
D


Abb. 4.7: Wiederholtes Vererben



55
Die Mehrdeutigkeit entsteht dadurch, dass die abgeleitete Klasse D die Instanzvariablen der
Basisklasse A in zweifacher Ausfhrung enthlt. Eine Mehrdeutigkeit liegt auch dann vor,
wenn im Zusammenhang mit einem Objekt der Klasse D eine in A definierte Elementfunk-
tion aufgerufen wird.

Beispiel 4.5: (Mehrdeutigkeit durch wiederholtes Vererben)

cl ass A {
publ i c:
i nt i ;
/ / . . .
};

cl ass B : publ i c A {
/ / er bt i von A
/ / . . .
};

cl ass C : publ i c A {
/ / er bt i von A
/ / . . .
};

cl ass D : publ i c B, publ i c C {
/ / . . .
publ i c:
voi d f ( voi d) { i = 1; } / / Fehl er , wel ches i ?
voi d g( voi d) { B: : i = 1; } / / ok
};

i nt mai n( )
{ D d;

d. i =1; / / Fehl er , wel ches i ?
d. C: : i =1; / / ok
}


Auch in diesem Falle muss die Eindeutigkeit durch Angabe von qualifizierten Namen herbei-
gefhrt werden.

Die Duplikation der Instanzvariablen bei wiederholtem Vererben kann in gewissen Fllen
erwnscht sein. In einem vielfach aufgefhrten Beispiel werden die beiden Klassen Auto und
Boot von einer Klasse namens Fahrzeug abgeleitet und erben dabei jeweils die Instanzvariab-
le Geschwindigkeit. Nachtrglich wird die Klasse Amphibienfahrzeug von den Klassen Auto
und Boot abgeleitet. Objekte dieser Klasse verfgen ber zwei Instanzvariablen Geschwin-
digkeit, welche die Geschwindigkeiten zu Lande bzw. zu Wasser reprsentieren.




56
4.5.3 Virtuelle Basisklassen

Die in 4.5.2 beschrieben Erfordernis zur Auflsung von Mehrdeutigkeiten (ausgelst durch
mehrfaches Vererben) erfordert Detailkenntnis ber die Basisklassen und steht im Gegensatz
zum Prinzip der Abstraktion bei der Verwendung einer Basisklasse.

Im Gegensatz zu dem oben aufgefhrten Beispiel lassen sich Beispiele von wiederholter Ver-
erbung finden, in denen die mehrfache Anlage von Instanzvariablen und Elementfunktionen
einer gemeinsamen Oberklasse nicht erwnscht ist. Durch Verwendung einer virtuellen Ab-
leitung lsst sich der normale Vererbungsvorgang umgehen und eine gemeinsame Nutzung
einer Basisklasse erzwingen:

cl ass B : publ i c vi r t ual A { . . .

Eine Basisklasse, von der eine virtuelle Ableitung ausgeht, bezeichnet man als virtuelle Ba-
sisklasse. Abb. 4.6 zeigt eine wiederholte Vererbung unter Verwendung einer virtuellen Ba-
sisklasse.

A
B C
D
virtuell virtuell


Abb. 4.8: Virtuelle Basisklasse


Gleichgltig, wie oft eine virtuelle Basisklasse in einer Ableitungshierarchie vorkommt, es
wird immer nur ein Satz von Instanzvariablen erzeugt. Ebenso besteht Eindeutigkeit bei der
Verwendung von Elementfunktionen der virtuellen Basisklasse.

Um eine mehrfache Initialisierung der virtuellen Basisklasse zu vermeiden, unterbindet eine
virtuelle Ableitung den Aufruf der Basisklassenkonstruktoren. Stattdessen wird der Konstruk-
tor der virtuellen Basisklasse vor allen anderen Konstruktoren ausgefhrt. Dies hat Konse-
quenzen fr die Parameterversorgung des Konstruktors der virtuellen Basisklasse. Es existie-
ren zwei mgliche Lsungen:
(a) Die virtuelle Basisklasse besitzt keinen oder einen parameterlosen Konstruktor. Im ersten
Fall tritt der automatisch erzeugte Standardkonstruktor in Kraft.
(b) Die Parameterversorgung des Konstruktors der virtuellen Basisklasse muss in der mehr-
fach vererbten Klasse erfolgen. In Beispiel 4.6 ist dies die Klasse D. Der Compiler ver-
langt auch in diesem Fall zustzlich einen parameterlosen Konstruktor, der jedoch nie
aufgerufen wird.

57


Beispiel 4.6: (Virtuelle Basisklassen)

cl ass A {
publ i c:
i nt i ;
A( voi d) {};
A( i nt i ni t ) {i = i ni t ; }
/ / . . .
};

cl ass B : vi r t ual publ i c A {
/ / er bt i von A
/ / . . .
};

cl ass C : vi r t ual publ i c A {
/ / er bt i von A
/ / . . .
};

cl ass D : publ i c B, publ i c C {
/ / . . .
publ i c:
D( i nt i ni t ) : A( i ni t ) {};
voi d f ( voi d) { i = 1; } / / ok
};

i nt mai n( )
{ D d( 0) ;

d. i =1; / / ok
r et ur n 0;
}


Bemerkung:
Wird eine Klasse sowohl ber virtuelle als auch ber nichtvirtuelle Ableitungspfade von einer
gemeinsamen Basisklasse abgeleitet, so wird fr jeden nichtvirtuellen Ableitungspfad eine
Instanz der Basisklasse erzeugt. Zustzlich wird eine gemeinsame Instanz fr die virtuellen
Ableitungspfade erzeugt. In diesem Falle bestehen die in 4.5.2 beschriebenen Mehrdeutigkei-
ten durch wiederholtes Vererben fort.


58
4.6 bungsaufgaben

Aufgabe 4.1
Erweitern Sie das Programmbeispiel 4.2 um
(a) ein Gatter zur Negation eines Signals (NOT-Gatter)
(b) ein NAND-Gatter
durch Ableitung von einer geeigneten Basisklasse


Aufgabe 4.2
Der Entwurf fr ein allgemein verwendbares Programm zur Berechnung des Gesamtwider-
stand in einem Netz basiert auf folgenden berlegungen:
- Ein Widerstand ist ein elementares Netz und kapselt den Widerstandswert.
- Eine serielle Schaltung ist ein Netz mit zwei seriell geschalteten Teilnetzen. Der Gesamt-
widerstand berechnet sich zu Rges = Rnet1 + Rnet2.
- Eine parallele Schaltung ist ein Netz mit zwei parallel geschalteten Teilnetzen. Der Ge-
samtwiderstand berechnet sich zu Rges = (Rnet1 * Rnet2) / (Rnet1 + Rnet2).
- Der Widerstandswert fr ein Netz kann mit einer public-Funktion namens ohm() abge-
fragt werden.

Abb. 4,9 zeigt ein Beispiel fr ein Netz aus elektrischenWiderstnden.

200 O
300 O
100 O


Abb. 4.9: Beispielschaltung

Die folgende main()-Funktion erzeugt eine objektorientierte Reprsentation des oben darge-
stellten Netzes und gibt dessen Gesamtwiderstand auf dem Bildschirm aus.

i nt mai n( )
{
Resi st or r 1, r 2, r 3; / / 3 Wi der st aende
Par al l el par ( &r 1, &r 2) ; / / R1 und R2 par al l el
Ser i al ser ( &par , &r 3) ; / / R1 | | R2 ser i el l mi t R3

r 1. set Res( 100. 0) ; / / Wer t e der
r 2. set Res( 200. 0) ; / / Ei nzel wi der st nde
r 3. set Res( 300. 0) ; / / f est l egen

cout << " Net zwi der st and: " << ser . ohm( ) / / Net zwi der st and ber echnen
<< " Ohm" << endl ;
}

Realisieren Sie geeignete Klassen Resistor, Serial (fr eine serielle Schaltung) und Parallel
(fr eine parallele Schaltung).

59
Aufgabe 4.3
Erweitern Sie das Zeichenprogramm aus Beispiel 4.3 um eine Klasse Rectangle zur Repr-
sentation von Rechtecken. berlegen Sie sich eine geeignete Positionierung der Klasse in der
bestehenden Vererbungshierarchie.

Aufgabe 4.4
berprfen Sie in dem nachfolgendem Programm die syntaktische Zulssigkeit der Objekt-
zugriffe bzw. Wertzuweisungen..

cl ass A {
i nt a;
publ i c:
voi d f A( ) { a = 0; } / / ( 1) zul ssi g?
};

cl ass B : publ i c A {
i nt b;
publ i c:
voi d f B( voi d) { a = 0; } / / ( 2) zul ssi g?
};

cl ass C : pr i vat e B {
i nt c;
publ i c:
voi d f C( voi d) { f A( ) ; } / / ( 3) zul ssi g?
};

cl ass D {
A mA;
publ i c:
voi d f D( voi d) { mA. a = 0; } / / ( 4) zul ssi g? };

i nt mai n( )
{ C cObj ;
B *bPt r = &cObj ; / / ( 5) zul ssi g?
A *aPt r = bPt r ; / / ( 6) zul ssi g?

cObj . f A( ) ; / / ( 7) zul ssi g
r et ur n 0;
}

Aufgabe 4.5 (Allgemeine Fragen)
(a) Es gelte folgende Ableitungsbeziehung:
class A : public B { ...
Welche automatische Typkonvertierung zwischen A* und B* ist hierdurch definiert?

(b) Wie kann man in einer redefinierten Elementfunktion auf die Elementfunktion der Ba-
sisklasse zugreifen?

(c) Ein Programm erzeugt 3 Instanzen einer Klasse mit virtuellen Funktionen. Wie viele
VMTs (virtual method table) enthlt das Programm?

(d) In welchen Fllen wrden Sie einen virtuellen Destruktor vorsehen?

60
5. berladen von Operatoren

Das berladen von Operatoren ist keine typische Eigenschaft der objektorientierten Pro-
grammierung. Z.B. bietet Java diese Mglichkeit nicht. Umgkehrt kennen Programmierspra-
chen, welche nicht zu den objektorientierten Programmiersprachen zhlen, die Mglichkeit
zur Operatorberladung (z.B. Ada).


5.1 Grundlagen

Programmiersprachen stellen fr eingebaute Typen einen Satz von Operatoren zur Handha-
bung von Datenobjekten dieses Typs zur Verfgung. So sind fr den Datentyp int u.a die
Operatoren +, -, * und / zur Realisierung der arithmetischen Grundrechenarten definiert.

Mit dem berladen von Operatoren wird ein Mechanismus bereitgestellt, den Operatoren der
Sprache C++ auch fr Objekte einer selbstdefinierten Klasse eine Bedeutung zu geben. Dies
ermglicht in manchen Fllen eine bequemere, gelufigere Manipulation von benutzerdefi-
nierten Objekten.

Zur Verdeutlichung betrachten wir zunchst die nachfolgend definierte Klasse zur Realisie-
rung von komplexen Zahlen.

cl ass Compl ex {
doubl e r e, i m;
publ i c:
Compl ex( doubl e r , doubl e i ) ;
f r i end Compl ex cAdd ( Compl ex c1, Compl ex c2) ;
f r i end Compl ex cMul ( Compl ex c1, Compl ex c2) ;
/ / . . .
};


Zur Addition der beiden komplexen Zahlen a und b bedarf es der folgenden syntaktisch we-
nig ansprechenden Formulierung:

c = cAdd( a, b) ;

Eine weniger gewhnungsbedrftige, weil zu den eingebauten Typen analoge Formulierung
wrde lauten:

c = a + b;

Diese Formulierung lsst sich durch berladen der arithmetischen Operatoren +, bzw. * fr
Objekte der Klasse Complex erreichen. Betrachten wir hierzu die nachfolgende Klasse:


61
Beispiel 5.1: (berladen von Operatoren)
cl ass Compl ex {
doubl e r e, i m;
publ i c:
Compl ex( doubl e r , doubl e i ) ;
Compl ex( voi d) ;
f r i end Compl ex oper at or +( Compl ex c1, Compl ex c2) ;
f r i end Compl ex oper at or *( Compl ex c1, Compl ex c2) ;
/ / . . .
};

Mit der Deklaration der von operator+() und operator*() wurde den Operatoren + und *
eine Bedeutung fr Objekte der Klasse Complex gegeben. Derartige Funktionen werden als
Operatorfunktionen bezeichnet.

Der Name einer Operator-Funktion setzt sich aus dem Schlsselwort operator und dem Ope-
ratorsymbol zusammen. Die Operatorschreibweise ist eine bequeme Kurzform fr den expli-
ziten Aufruf der Operator-Funktion.

Syntax: operator-funktionsname : : = operatorop

Mit Ausnahme der Operatoren . (Selektion), : :, ?: , und sizeof knnen alle C++-
Operatoren berladen werden.


Die Element- und Operatorfunktionen knnen dabei wie folgt definiert werden:

Beispiel 5.1 Forts.: (Operatorfunktionen)
i nl i ne Compl ex: : Compl ex( doubl e r , doubl e i )
{
r e = r ;
i m= i ;
}

i nl i ne Compl ex: : Compl ex( voi d)
{
r e = i m= 0. 0;
}

i nl i ne Compl ex oper at or +( Compl ex c1, Compl ex c2)
{
r et ur n Compl ex( c1. r e + c2. r e, c1. i m+ c2. i m) ;
}

Compl ex oper at or *( Compl ex c1, Compl ex c2)
{
r et ur n Compl ex ( c1. r e * c2. r e - c1. i m* c2. i m,
c1. r e * c2. i m+ c1. i m* c2. r e) ;
}


Bei dem Ausdruck Complex(c1.re + c2.re, c1.im + c2.im) handelt es sich um einen explizi-
ten Aufruf des Konstruktors. Ein auf diese Weise erzeugtes Objekt ist ein temporres, namen-
loses Objekt. Seine Lebensdauer ist in diesem Fall auf die Dauer der return-Anweisung be-
grenzt.

62
Das temporre Objekt wird auf dem Stack abgelegt und beim Verlassen der Funktion durch
den Destruktor zerstrt.

Das folgende Beispiel zeigt eine mgliche Verwendung der Operatorfunktionen.

i nt mai n( )
{ Compl ex a( 1. 0, 2. 5) , b( 3. 0, 0. 0) , c;

c = a + b; / / c = oper at or +( a, b) ;
c = c + a * Compl ex( 2. 0, 1. 0) ;
/ / c + ( a * Compl ex( 2. 0, 1. 0) )
}


In Beispiel 5.1 sind die Operatorfunktionen operator+() und operator-() als friend-
Funktionen realisiert. Alternativ knnen Operatorfunktionen auch als Elementfunktionen der
Klasse Complex definiert werden, wie im folgenden anhand des Operators += verdeutlicht
wird:

Beispiel 5.1 Forts.: (Operatorfunktionen)
cl ass Compl ex {
/ / . . .
Compl ex& oper at or +=( Compl ex c) ;
/ / . . .
};

In diesem Fall wird der erste Operand durch *this reprsentiert. Die Elementfunktion kann
wie folgt definiert werden:

Beispiel 5.1 Forts.: (Operatorfunktionen)
i nl i ne Compl ex& Compl ex: : oper at or +=( Compl ex c)
{
r e += c. r e;
i m+= c. i m;
r et ur n *t hi s;
}

Bemerkung
Das Objekt *this wird hier als Referenz zurckgegeben. Dies ist deshalb erforderlich, weil
der vernderte erste Operand das Ergebnis des Ausdrucks ist. Im Gegensatz dazu lsst die
Operatorfunktion operator+() die beiden Operanden unverndert und erzeugt ein neues "Er-
gebnisobjekt".


Auch unre Operatoren knnen entweder als parameterlose Elementfunktionen oder als
friend-Funktion mit einem Parameter realisiert werden. Als Beispiel wird die Operatorfunk-
tion Complex::operator-() zur Vorzeichenumkehrung vorgestellt:


63
Beispiel 5.1 Forts.: (Operatorfunktionen)
cl ass Compl ex {
/ /
Compl ex oper at or - ( ) ;
/ /
};

i nl i ne Compl ex Compl ex: : oper at or - ( )
{
r et ur n Compl ex( - r e, - i m) ;
}


Das nachfolgende Programm zeigt eine Anwendung der beiden zuletzt vorgestellten Opera-
torfunktionen.

i nt mai n( )
{ Compl ex a( 1, 2. 5) , b( 3, 0) ;
a += - b; / / a. oper at or +=( b. oper at or - ( ) ) ;
}


Einschrnkungen beim berladen von Operatoren
(1) Eine Operatorfunktion muss entweder eine Elementfunktion sein oder ein Operand der
Operatorfunktion muss ein Objekt einer Klasse sein. Hierdurch wird sichergestellt, dass
die Operatoren fr Standardtypen unverndert bleiben.

(2) Es knnen keine neuen Operatorsymbole definiert werden. Hierdurch sollen Mehrdeu-
tigkeiten vermieden werden.

(3) Alle Operatoren behalten die in C++ vordefinierte Rangfolge bei.

(4) Alle Operatoren behalten ihre Stelligkeit bei.

(5) Die folgenden Operatoren mssen immer als Elementfunktionen realisiert werden: =
(Zuweisungsoperator), [] (Indexoperator), () (Funktionsaufrufoperator) und -> (Zeiger-
zugriffsoperator).

(6) Zur Unterscheidung zwischen der Postfix- und Prfix-Notation bei dem Inkrementope-
rator ++ und dem Dekrementoperator -- mssen jeweils zwei Operatorfunktionen defi-
niert werden.
- Die Prfix-Notation wird durch eine Funktionen mit einem Parameter realisiert; d.h,
ist a ein Objekt der Klasse T, so ruft ++a eine Elementfunktion T& T::operator++
(void) oder eine freie Funktion T& operator++(T& t) auf.
- Die Postfix-Notation wird durch eine Funktion mit zwei Argumenten realisiert. Das
zweite Argument wird nie verwandt. Es dient ausschlielich zur Kennzeichnung der
Postfix-Variante. Sei also a wieder ein Objekt der Klasse T, so ruft a++ die Element-
funktion T& T::operator++(int i) oder die freie Funktion T& operator++(T& t, int
i) auf.

64
5.2 Benutzerdefinierte Typkonvertierung

Die in Beispiel 5.1 beschriebene Klasse Complex erfllt nicht alle Anforderungen an einen
Datentyp zur Reprsentation der komplexen Zahlen. Insbesondere bercksichtigt sie keine
Verknpfung von komplexen Zahlen mit Gleitkommazahlen (z.B. double) und ganzen Zah-
len (z.B. int).

i nt mai n( )
{ Compl ex a( 2. 5, 0. 0) , b;

b = a + 5. 0; / / Fehl er !
b = a * 20; / / Fehl er !
}

Eine Mglichkeit zur arithmetischen Verknpfung von Complex-Objekten mit Daten anderer
Typen besteht in der Bereitstellung von mehreren berladenen Operatorfunktionen.

cl ass Compl ex {
f r i end Compl ex oper at or +( Compl ex c1, Compl ex c2) ;
f r i end Compl ex oper at or +( Compl ex c1, doubl e d2) ;
f r i end Compl ex oper at or +( doubl e d1, Compl ex c2) ;
/ / . . .
};

Aufgrund der standardmig verfgbaren Typkonvertierung nach double ist mit obigen
Funktionen auch die Verknpfung von komplexen Zahlen mit ganzzahligen Datentypen und
dem Datentyp float mglich.


Typkonvertierung mittels Konstruktoren
Eine Alternative zu der oben beschriebenen Realisierung besteht in der Bereitstellung des
folgenden Konstruktors:

cl ass Compl ex {
Compl ex( doubl e r ) { r e = r ; i m= 0. 0; }
/ / . .
};

Der Konstruktor konvertiert eine double-Zahl in ein Complex-Objekt gem der blichen
Einbettung von reellen Zahlen in die komplexe Ebene. Er ermglicht die folgende Definition:

Compl ex a( 2. 5) ; oder alternativ: Compl ex a = 2. 5;

Unter Zuhilfenahme eines expliziten Konstruktoraufrufs lsst sich dann folgender Ausdruck
formulieren:

b = a + Compl ex( 5. 0) ;

Konstruktoren, welche nur ein Argument bentigen, brauchen nicht explizit aufgerufen zu
werden.

65

Dies ermglicht auch die folgende verkrzte Schreibweise:

b = a + 5. 0;

Da der Compiler ber eine eingebaute Konvertierung von int nach double verfgt, sind auch
Verknpfungen zwischen Complex und int mglich.

b = a * 20; / / b = a * Compl ex( ( doubl e) 20) ;


Der oben beschriebene Konstruktor lsst sich wie folgt in den Konstruktor aus Beispiel 5.1
integrieren:

Beispiel 5.1 (Forts.): (Konvertierungskonstruktor)
cl ass Compl ex {
/ / . . .
Compl ex( doubl e r , doubl e i =0. 0) { r e = r ; i m= i ; }
/ / . .
};


5.3 friend-Funktionen oder Elementfunktionen

An dieser Stelle sollen einige Hinweise gegeben werden, wann eine Operatorfunktion als
friend-Funktion und wann als Elementfunktion realisiert werden sollte. Grundstzlich ist die
Realisierung als Elementfunktion einer Realisierung als friend-Funktion vorzuziehen, da die
Verfgbarkeit des Zeigers this hufig eine krzere Formulierung erlaubt. Einige Operatoren
mssen als Elementfunktion realisiert werden (siehe 5.1). Dieses gilt ebenso, wenn die Ope-
ratorfunktion virtuell ist.

In solchen Fllen, in denen fr alle Operanden eine implizite Typumwandlung erwnscht
ist, muss die Operatorfunktion jedoch als friend-Funktion realisiert sein.

Man betrachte hierzu die als Elementfunktion realisierte Funktion Complex::operator+().

Compl ex Compl ex: : oper at or +( Compl ex c)
{
r et ur n Compl ex( r e +c. r e, i m+ c. i m) ;
}


Im vorliegenden Fall muss der erste Operand in jeden Fall ein Objekt vom Typ Complex
sein; d.h. eine Anwendung der Operatorfunktion wie im folgenden Beispiel ist nicht mglich.


66
i nt mai n( )
{ Compl ex a( 2. 0, 3. 0) , b;

b = 1 + a; / / Fehl er
}

Bei Realisierung der Operatorfunktion als friend-Funktion ist die Anweisung dagegen zuls-
sig, da hier fr beide Summanden eine Typkonvertierung vorgenommen wird.


5.4 berladen des Index-Operators

Der Indexoperator a[i] wird als binrer Operator mit den Operanden a und i interpretiert.
Durch berladen des []-Operators lsst sich der Zugriff auf Komponenten eines Feldes mit
einer berprfung der Indexgrenzen realisieren. Dieses soll anhand der Realisierung einer
String-Klasse gezeigt werden.

Beispiel 5.2: (berladen des Index-Operators)
cl ass St r i ng {
i nt si ze; / / Lnge der Zei chenket t e ( ohne ' \ 0' )
char *s; / / Zei chenket t e
publ i c:
St r i ng( const char * st r ) ;
~St r i ng( voi d) ;
char & oper at or [ ] ( i nt i ) ;
};


Neben der eigentlichen Zeichenkette enthlt ein Objekt der Klasse String eine Angabe zur
Lnge der Zeichenkette, welche im Konstruktor initialisiert wird.

Beispiel 5.2 (Forts.): (berladen des Index-Operators)
St r i ng: : St r i ng( const char *st r )
{
si ze = st r l en( st r ) ;
s = new char [ si ze+1] ;
st r cpy( s, st r ) ;
}

Das Kopieren der Zeichenkette ist hier erforderlich, da nur auf diese Weise eine vollstndige
Kontrolle ber die im String-Objekt enthaltene Zeichenkette mglich ist. Der Destruktor gibt
den reservierten Speicherbereich wieder frei.

Beim Zugriff auf ein Zeichen eines String-Objekts mittels der Operatorfunktion String::-
operator[]() kann eine berprfung auf Lngenberschreitung durchgefhrt werden. Die
Operatorfunktion muss aus dem in 5.1 erwhnten Grunde eine Elementfunktion sein.

67
Beispiel 5.2 (Forts.): (berladen des Index-Operators)
char & St r i ng: : oper at or [ ] ( i nt i )
{
i f ( i >= si ze)
{ cer r << " Fehl er , I ndex ungl t i g\ n" ;
exi t ( 1) ; / / Pr ogr ammabbr uch pr obl emat i sch!
}
el se
r et ur n s[ i ] ;
}

Bemerkungen
- Aufgrund des problematischen Programmabbruchs im Falle eines ungltigen Index ist die
Klasse so nicht allgemein einsetzbar. In Kapitel 7 wird eine geeignetere Lsung zur
Handhabung des Fehlerfalles vorgestellt.


5.5 Zuweisung und Initialisierung

5.5.1 Zuweisung

Man betrachte den folgenden Programmausschnitt unter Zugrundelegung der in Beispiel 5.2
definierten Klasse String:

i nt mai n( )
{
St r i ng s1( " OOP" ) ;
St r i ng s2( " Vor l esung" ) ;

s1 = s2;
}

Es werden zwei String-Objekte erzeugt, welche jeweils einen char-Array anlegen. Die Zu-
weisung s1 = s2; bewirkt ein elementweises Kopieren der Komponenten des Objekts; d.h.
s1.s zeigt auf den gleichen Speicherbereich wie s2.s. Man bezeichnet dies als flache Kopie.
Am Programmende wird der Destruktor fr s1 und s2 aufgerufen, welche beide eine delete-
Operation auf das gleiche Array ausfhren. Zur Beseitigung des Problems ist eine Operator-
funktion String::operator=() erforderlich, welche die Zeichenkette selbst kopiert , also eine
tiefe Kopie durchfhrt.

Beispiel 5.2 (Forts.): (berladen des Wertzuweisungs-Operators)
St r i ng& St r i ng: : oper at or = ( const St r i ng &st r )
{
i f ( t hi s ! = &st r )
{ del et e [ ] s;
si ze = st r . si ze;
s = new char [ si ze+1] ;
st r cpy( s, st r . s) ;
}
r et ur n *t hi s;
}


68
Analog zu Strukturen in C ist die Wertzuweisung zwischen Klassenobjekten gleichen Typs
grundstzlich definiert. Die Instanzvariablen werden hierbei elementweise kopiert. Im Falle
von Objekten der Klasse Complex leistet diese standardmig verfgbare Wertzuweisung
das Gewnschte.

Falls die Semantik der Wertzuweisung fr bestimmte Klassenobjekte durch dieses element-
weise Kopieren nicht abgedeckt ist (siehe Objekte der Klasse String), so muss der Wertzu-
weisungsoperator durch eine Operatorfunktion in geeigneter Weise redefiniert werden.


5.5.2 Initialisierung

Die Klasse String aus Beispiel 5.2 verfgt ber einen Konstruktor, welcher als Parameter
eine C++-Zeichenkette (Typ: char*) erwartet. Mit Hilfe dieses Konstruktors lassen sich
String-Objekte wie folgt erzeugen:

St r i ng s1( " OOP" ) ; oder alternativ St r i ng s1 = " OOP" ;

Zu beachten ist hierbei, dass auch die zweite Schreibweise lediglich einen Konstruktoraufruf
auslst. Der Konstruktor stellt gleichzeitig eine Konvertierungsfunktion dar, welche eine Zei-
chenkette vom Typ char* in ein String-Objekt umwandelt. Er ermglicht damit eine Wert-
zuweisung der folgenden Form:

s1 = " Vor l esung" ; / / s1. oper at or =( St r i ng( " Vor l esung" ) ) ;

In bestimmten Fllen mchte man ein Objekt als Kopie eines bereits existierenden Objekts
gleichen Typs erzeugen.

St r i ng s2( s1) ; oder alternativ St r i ng s2 = s1;

Das String-Objekt s2 wird als Kopie des bereits existierenden String-Objekts s1 erzeugt.


Da C++ grundstzlich zwischen Wertzuweisung und Initialisierung unterscheidet, kann die
Operatorfunktion String::operator=() nicht zur Initialisierung herangezogen werden. Die
Unterscheidung ist darin begrndet, dass bei der Wertzuweisung zwischen Objekten davon
ausgegangen wird, dass beide Objekte bereits erzeugt sind. Auch die Operatorfunktion
String::operator=() geht von existierenden String-Objekten (mit initialisierten Werten fr
size und s) aus.

Die Initialisierung eines Klassenobjekts mit einem typgleichen "Musterobjekt" erfordert
einen speziellen Konstruktor, welcher allgemein als Copy-Konstruktor bezeichnet wird.
Allgemein ist ein Copy-Konstruktor fr eine Klasse X ein Konstruktor mit der Signatur
X(const X&).

Fr jede Klasse wird standardmig ein Standard-Copy-Konstruktor bereitgestellt, welcher -
analog zur Wertzuweisung - lediglich eine flache Kopie des Originals erzeugt.

69
Im Falle der Klasse String htte ein so arbeitender Standard-Copy-Konstruktor die gleichen
Probleme zur Folge wie die standardmige Wertzuweisung. Analog zur Wertzuweisung
muss der Copy-Konstruktor eine tiefe Kopie des Originals erzeugen. Der Copy-Konstruktor
fr die Klasse String kann wie folgt definiert werden:

Beispiel 5.2 (Forts.): (siehe Beiblatt 5.2)
St r i ng: : St r i ng( const St r i ng &st r )
{
si ze = st r . si ze;
s = new char [ si ze+1] ;
st r cpy( s, st r . s) ;
}

Copy-Konstruktoren werden insbesondere bei
- wertmiger Parameterbergabe von Klassenobjekten und
- Funktionsrckgabe von Klassenobjekten
in Anspruch genommen.

Dieses lsst sich anhand der folgenden Funktion subStr() verdeutlichen. Die Funktion extra-
hiert eine Teilzeichenkette aus einem als Parameter bergebenen String-Objekt und liefert
diese in Form eines String-Objekts als Funktionswert zurck.

St r i ng subSt r ( St r i ng st r , i nt f r om, i nt l en)
{ St r i ng st r Hi l f = " " ; / / l okal es Ar bei t sobj ekt
char * s = new char [ l en+1] ; / / Ber ei ch f r Tei l zei chenket t e

f or ( i nt i =0; i <l en; i ++) / / Ext r ah. der Tei l zei chenket t e
s[ i ] =st r [ f r om+i ] ;
s[ l en] = ' \ 0' ;
st r Hi l f = s; / / Umspei cher n
del et e [ ] s; / / Spei cher ber ei ch f r ei geben
r et ur n st r Hi l f ;
}

Eine Anwendung der Funktion knnte wie folgt lauten:

St r i ng s( " Hal l o" ) , ss( ) ;
ss = subSt r ( s, 2, 2) ;

Beim Funktionsaufruf wird das lokale String-Objekt str mit Hilfe des Copy-Konstruktors als
Kopie des String-Objekts s erzeugt. Fr die Wertrckgabe wird ebenfalls ein unbenanntes,
temporres String-Objekt als Kopie von strHilf erzeugt. Die String-Objekte str zur Parame-
terbergabe sowie das temporre String-Objekt zur Wertrckgabe gehren zum Geltungs-
bereich der aufrufenden Funktion und werden erst nach deren Verlassen zerstrt.

70
5.6 bungsaufgaben

Aufgabe 5.1
(a) Schreiben Sie eine Klasse namens Point zur Reprsentation eines Punktes in einem x-y-
Koordinatensystem. Die folgenden Elementfunktionen sollen implementiert werden:
1. Einen Konstruktor, der ein Point-Objekt mit den Koordinatenwerten (x,y) initialisiert.
2. Einen Standardkonstruktor, welcher das Point-Objekt mit dem Wert (0,0) initialisiert.
3. Eine Operatorfunktion +=, welche ein Point-Objekt um ein als Argument angegebe-
nes Point-Objekt verschiebt.
4. Eine Operatorfunktion ==, welche entscheidet, ob zwei Point-Objekte identisch sind.

(b) Benutzen Sie die Klasse Point zur Implementierung einer Klasse Line, welche eine Ge-
rade in einem x-y-Koordinatensystem reprsentiert und die beiden Punkte start und end
verbindet. Implementieren Sie die folgenden Elementfunktionen fr Line:
1. Einen Konstruktor, der ein Line-Objekt anlegt, welches durch einen bestimmten
Start- und Endpunkt geht.
2. Ein Standardkonstruktor, welcher ein Line-Objekt mit standardmig initialisierten
Point-Objekten erzeugt.
3. Eine Operatorfunktion += , welche ein Line-Objekt um ein als Argument angegebe-
nes Point-Objekt verschiebt.
4. Eine Operatorfunktion == , welche entscheidet, ob zwei Line-Objekte identisch sind.

Aufgabe 5.2
(a) Ersetzen Sie in der Lsung zu bungsaufgabe 3.1 die Zugriffsfunktionen at() der Klasse
IntVector durch eine Operatorfunktion operator[]().

(b) Statten Sie die Klasse IntVector zustzlich mit einem Wertzuweisungsoperator und ei-
nem Copy-Konstruktor aus.

Aufgabe 5.3
Realisieren Sie eine Klasse namens Bruch, welche eine rationale Zahl in Form von 2 int
Zahlen (fr Zhler und Nenner) reprsentiert.
- Realisieren Sie einen Konstruktor, welcher gleichzeitig zur Konvertierung von int-Zahlen
in ein Bruch-Objekt dient.
- Realisieren Sie die Operatorfunktion operator*=() fr die Klasse Bruch.
- Realisieren Sie die Operatorfunktion operator*() fr die Klasse Bruch, so dass auch der
folgende Ausdruck ausgefhrt werden kann: Br uch b = 2 * Br uch( 15, 4) ;

Aufgabe 5.4 (Allgemeine Fragen)
(a) Warum knnen in C++ keine neuen Symbole fr Operatorfunktionen definiert werden?
(b) Was ist die Besonderheit von Konstruktoren mit einem Parameter?
(c) Warum unterscheidet C++ zwischen Wertzuweisung und Initialisierung?
(b) Was ist der Unterschied zwischen einer flachen und einer tiefen Kopie?

71
6. Templates

Ein Template ist eine Schablone zur Generierung einer Funktion oder Klasse. Das Template
enthlt einen oder mehrere Platzhalter fr die Parametertypen einer Funktion bzw. die Typen
der Instanzvariablen einer Klasse. Beim Aufruf eines Funktions-Templates oder bei der typi-
sierten Definition eines Klassenobjekts dient das Template als Vorlage fr die Generierung
einer geeignet typisierten Funktions- bzw. Klassendefinition.


6.1 Funktions-Templates

Die Definition eines Funktions-Templates wird durch einen Template-Kopf eingeleitet, auf
den eine bliche Funktionsdefinition folgt.

Beispiel 6.1: (Funktions-Templates)
t empl at e<cl ass T>
voi d swapI t ( T &a, T &b)
{
T h = a;
a = b;
b = h;
}

Syntax:
funktions-template: : =
template<template-parameter {, template-parameter }
0+
>
funktionsdefinition
template-parameter : : = class name

Wie das Beispiel zeigt, enthalten die Template-Parameter Platzhalter fr Typen, welche in
der Funktionsdefinition wieder aufgegriffen werden mssen. Die obige Deklaration stellt
lediglich eine Schablone fr eine zu generierende konkrete Funktionsdefinition dar.

Die Generierung einer Funktionsdefinition durch den Compiler wird durch einen Funkti-
onsaufruf ausgelst. Dabei werden die Template-Parameter durch die im Aufruf verwen-
deten Argumenttypen ersetzt.

Beispiel 6.1 (Forts): (Funktions-Templates)
i nt x=2, y=4;
swapI t ( x, y) ;

Der Aufruf im Beispiel fhrt zur Generierung der folgenden Templatefunktion.

voi d swapI t ( i nt &a, i nt &b)
{
i nt h = a;
a = b;
b = h;
}


72
Templates sind in gewisser Weise mit Makros zu vergleichen. Ein Aufruf der Funktion swa-
pIt() mit von int abweichenden Argumenten fhrt zur Definition einer berladenen Templa-
tefunktion. Als Argumenttypen sind auch Klassen zugelassen.

Beispiel 6.1 (Forts): (Funktions-Templates)
cl ass Poi nt {
i nt x, y;
publ i c:
Poi nt ( i nt x, i nt y) ;
/ / . . .
};

Poi nt p1( 1, 1) , p2( 2, 2) ;
swapI t ( p1, p2) ;


Hierdurch wird die folgende Templatefunktion erzeugt.

voi d swapI t ( Poi nt &a, Poi nt &b)
{
Poi nt h = a;
a = b;
b = h;
}

Funktions-Templates definieren also eine - im Prinzip - unbegrenzte Menge von berladenen
Templatefunktionen. Aus der Tatsache, dass berladene Funktionen sich in der Signatur un-
terscheiden mssen folgt:

Template-Parameter von Funktions-Templates mssen Typen sein.
Template-Parameter mssen in der Funktionsdeklaration in Form von Parametern auftre-
ten.

Das Beispiel zeigt die Problematik von Funktions-Templates: Der Programmierer ist selbst
verantwortlich dafr, dass die generierte Templatefunktion korrekt und sinnvoll ist. Im obi-
gen Beispiel sind z.B. lediglich Argumenttypen zugelassen, auf denen die Wertzuweisung
definiert ist.

Die beschriebene Problemantik wird auch in folgendem Beispiel deutlich. Das Funktions-
Template find realisiert eine lineare Suche in einem Array.


Beispiel 6.2: (Funktions-Templates)
t empl at e<cl ass T>
T *f i nd( T *vec, i nt n, T &ar g)
{
f or ( i nt i =0; i <n; i ++)
i f ( vec[ i ] == ar g ) )
r et ur n &vec[ i ] ;
r et ur n NULL;
}


73
Das Funktionstemplate verwendet den quivalenzoperator fr den Vergleich der Komponen-
ten des Array mit dem Suchargument. Dies bedeutet, dass das Template nur fr solche Typen
anwendbar ist, fr die der quivalenzopeartor definiert ist. Dies gilt z.B. nur bedingt fr den
Datentyp char*. Der quivalenzoperator vergleicht hier lediglich die Werte von Zeigern. Der
beabsichtigte inhaltliche Vergleich findet nicht statt.
In solchen Fllen besteht eine gebruchliche Methode darin, den gewnschten Datentyp in
eine Klasse zu verpacken, und diese mit den erforderlichen Operatorfunktionen auszustatten.
Um das Template find() auch fr Zeicheneketten anwendbar zu machen, bietet es sich an, die
aus Kapitel 5 bekannte Klasse String um die erforderliche Operatorfunktion operator==() zu
erweitern.


Beispiel 6.2: (Funktions-Templates)
cl ass St r i ng {
unsi gned si ze; / / Laenge der Zei chenket t e ( ohne ' \ 0' )
char *s; / / Zei chenket t e
publ i c:
/ / . . .
char * get St r ( ) ; / / Zugr i f f sf unkt i on auf Zei chenket t e
f r i end bool oper at or ==( St r i ng &s1, St r i ng &s2) ;
};


Das nachfolgende Beispielprogramm zeigt die Anwendung des Templates find() fr den so
erweiterten Datentyp String.


Beispiel 6.2 (Forts.): (Funktions-Templates)
i nt mai n( )
{
const i nt LEN = 5;
St r i ng nameVec[ LEN] ;

nameVec[ 0] = " Gabi " ;
nameVec[ 1] = " Ant on" ;
nameVec[ 2] = " Xaver " ;
nameVec[ 3] = " Rudi " ;
nameVec[ 4] = " Al ber t " ;

char *st r i ngAr g = new char [ 100] ;
St r i ng* f oundSt r i ng;

cout << " Suche nach ( Name) : " ;
ci n >> st r i ngAr g;
i f ( ( f oundSt r i ng = f i nd( nameVec, LEN, St r i ng( st r i ngAr g) ) ) ! = NULL)
cout << f oundSt r i ng- >get St r ( ) << " gef unden\ n" ;
el se
cout << st r i ngAr g << " ni cht gef unden\ n" ;
del et e [ ] st r i ngAr g;

r et ur n 0;
}


74
Programmstruktur bei Verwendung von Templates
Vergleichbar mit Makros oder Inline-Funktionen muss bei der Generierung einer Template-
funktion die Definition des Funktions-Templates verfgbar sein. Dies wirft ein Problem auf
fr Programme, die aus mehreren Moduln bestehen. Eine Lsung besteht darin, die komplette
Definition des Funktions-Templates in eine Header-Datei zu packen, welche von den
aufrufenden Moduln inkludiert wird. Dabei entsteht zwar die Gefahr, dass ein und dieselbe
Templatefunktion mehrfach generiert wird. Doch sind die meisten Linker so eingestellt, dass
sie derartige Doppeldefinitionen akzeptieren.


6.2 Klassen-Templates

Wie eingangs bemerkt, lassen sich auch fr Klassendefinitionen Templates schreiben. Diese
werden als Klassen-Templates bezeichnet. Die hieraus generierten Klassendefinitionen
heien Template-Klassen.

Ein groes Anwendungsfeld von Template-Klassen liegt in der generischen Realisierung von
Container-Klassen. Mit Hilfe eines Klassen-Templates lassen sich typsichere Container-
klassen fr Nutzdaten unterschiedlichen Typs definieren. Beispiel 6.3 realisiert einen
Container mit array-hnlichem Zugriff. Es handelt sich um eine Reimplemtierung der Klasse
IntVector aus bungsaufgabe 3.1. Im Gegensatz zur Klasse IntVector, welche sich nur zur
Abspeicherung von int-Werten eignet, wird der Nutzdatentyp in dem Klassen-Template
CommonVector ber einen Template-Parameter T variabel gehalten.

Beispiel 6.3: (Klassen-Templates)
t empl at e <cl ass T, unsi gned l en>
cl ass CommonVect or {
unsi gned l engt h;
T *el ement s;
publ i c:
CommonVect or ( ) ;
~CommonVect or ( ) ;
unsi gned si ze( ) const ;
T& oper at or [ ] ( unsi gned i ) ;
voi d out ( voi d) const ;
};


Ein Klassen-Template besteht syntaktisch aus dem Template-Kopf, gefolgt von einer bli-
chen Klassendefinition. Die Template-Parameter werden innerhalb der Klassendefinition
wieder aufgegriffen. Im Gegensatz zu Funktions-Templates knnen Klassen-Templates auch
Parameter enthalten, welche keine Typen sind. Im Beispiel wird die Lnge des Containers
ber den Template-Parameter len vom Typ unsigned festgelegt.

Jede Elementfunktion einer Template-Klasse ist automatisch eine Template-Funktion mit
einem oder mehreren Parametern des Klassen-Template. Wenn eine Elementfunktion auer-
halb der Klasse definiert wird, muss sie als Funktions-Template geschrieben werden; d.h, sie
muss mit dem Template-Kopf beginnen. Ferner mssen die Template-Parameter im Klassen-
namen aus Grnden der Eindeutigkeit aufgefhrt werden.


75
Beispiel 6.3 (Forts.): (Klassen-Templates)
t empl at e <cl ass T, unsi gned l en>
unsi gned CommonVect or <T, l en>: : si ze( ) const
{
r et ur n l engt h;
}

Eine Template-Klasse wird bei der erstmaligen Erzeugung eines Objekts der Template-
Klasse generiert. Objekttyp ist der Template-Name, an den sich - eingeschlossen in spitze
Klammern - die aktuellen Template-Parameter anschlieen.

Beispiel 6.3 (Forts.): (Klassen-Templates)
CommonVect or <i nt , 10> i nt Vect or ;
CommonVect or <doubl e, 20> doubl eVect or ;

CommonVector<int,10> bzw. CommonVector<double,20> sind die Typbezeichner fr die
konkreten Klassen.


Wie im Falle der Template-Funktionen stellen die Klassen-Templates bestimmte Anforde-
rungen an die verwendbaren Typen. So sind in Beispiel 6.3 nur Nutzdatentypen mglich, fr
die der Wertzuweisungsoperator = und der Ausgabeoperator << definiert sind.

Der Wertzuweisungsoperator wird beim Zugriff ber die Operatorfunktion operator[]() be-
ntigt.

Beispiel 6.3 (Forts.): (Klassen-Templates)
/ / Vect or f uel l en
f or ( unsi gned i =0; i <i nt Vect or . si ze( ) ; i ++)
i nt Vect or [ i ] = i ;


Der Ausgabeoperator wird in der Elementfunktion out() verwendet.

Beispiel 6.3 (Forts.): (Klassen-Templates)
t empl at e <cl ass T, unsi gned l en>
voi d CommonVect or <T, l en>: : out ( voi d) const
{
f or ( unsi gned i =0; i <l engt h; i ++)
cout << el ement s[ i ] << ' \ n' ;
}


Bemerkung:
Das Klassen-Template CommonVector ist vollstndig - inklusive der Definition der Ele-
mentfunktionen - in der Header-Datei commonvector_tmp.h definiert.



76
6.3 bungsaufgaben

Aufgabe 6.1
Ergnzen Sie das Beispiel 6.2 um ein Funktions-Template, welches das Maximum in einem
int-Array bzw. das alphabetisch grte Element in einem char*-Array bestimmt.

Hinweis: berlegen Sie sich eine geeignete Lsung zum Komponentenvergleich.


Aufgabe 6.2
Verwenden Sie das Klassen-Template CommonVector aus Beispiel 6.3 zur Speicherung von
Zeichenketten vom Typ char*. Erweitern Sie hierzu die main()-Funktion aus Beispiel 6.3 um
eine geeignete Codesequenz.


Aufgabe 6.3 (Allgemeine Fragen)
(a) Geben Sie eine Anweisung an, welche fr das folgende Funktionstemplate eine Templa-
tefunktion generiert. template<class T1, classT2>
T1 func(T1 x, T2 y) { ....}

(b) Worauf ist bei der Verwendung von Templates besonders zu achten?

(c) Warum mssen die Template-Parameter bei einem Funktions-Template in der Parame-
terliste auftauchen?

(d) Warum werden Templates blicherweise komplett in einer Header-Datei implementiert?

77
7. Exceptions

7.1 Exception-Mechanismus

Fehlerbehandlung ist in der Programmierung ein vielfach diskutiertes Thema. In der Praxis
werden vorwiegend drei Methoden zur Fehlerbehandlung angewandt.

- Das Programm wird nach einer Fehlermeldung mittels exit() abgebrochen.
- Die Funktion, in welcher der Fehler auftrat, gibt einen Wert zurck, der einen Fehler sig-
nalisiert. Diese Vorgehensweise ist insbesondere dann angebracht, wenn der Aufrufer der
Funktion entscheiden muss, was im Fehlerfall zu tun ist. Diese Situation liegt z.B. bei der
Benutzung von Bibliotheksfunktionen vor.
- Eine Fehlervariable wird besetzt, in der Hoffnung, dass diese Variable an geeigneter Stelle
abgefragt wird. Die in C global verfgbare Variable errno ist fr diesen Fall vorgesehen.

Die genannten Methoden sind nicht unproblematisch. Nicht in jedem Fall ist ein Pro-
grammabbruch unausweichlich. Fehlerrckgaben und Fehlervariablen bleiben aus Bequem-
lichkeit oft unbercksichtigt. Die lokale Fehlerbehandlung ist zudem oft nachtrglich fr die
Transparenz des Programmcodes. Fr objektorientierte Programme sind die Methoden auch
aus anderen Grnden unbefriedigend:
- Konstruktoren und Destruktoren liefern keine Funktionswerte zurck.
- Beim Programmabbruch findet keine Destruktion bestehender Objekte statt. In manchen
Fllen, z.B. wenn diese Objekte Betriebssystem-Ressourcen oder Netzwerkverbindungen
kapseln, ist dieses nicht akzeptabel.

Mit den Exceptions enthlt C++ ein neues Konzept zur Behandlung von Fehlern, welches auf
einer Trennung des normalen Programmablaufs von der Fehlerbehandlung basiert.

Der fehlertrchtige Programmteil wird in einen try-Block eingefasst, an den sich ein oder
mehrere catch-Blcke anschlieen. Ein catch-Block enthlt dabei die Fehlerbehandlung fr
eine bestimmte Fehlerklasse. Er wird deshalb auch als Exception-Handler bezeichnet.

t r y / * f ehl er t r cht i ger Pr ogr ammabschni t t
{
/ / . . .
i f ( / * Fehl er */ )
t hr ow Except i on( ) ; / / Auswer f en ei ner Except i on
/ / . . .
}
cat ch ( Except i on e)
{
/ / Behandl ung von Fehl er n vomTyp Except i on
}


Syntax: try block { catch exception-deklaration block }
1+

exception-deklaration : : = ( typ name )


78
Falls in dem try-Block kein Fehler auftaucht, bleiben die catch-Blcke unbercksichtigt und
die Ausfhrung des Programms wird im Anschluss an die catch-Blcke fortgesetzt. Im Feh-
lerfall wird ein Exception-Objekt eines bestimmten Typs erzeugt und mittels der throw-
Anweisung geworfen.

Syntax: throw-anweisung : : = throw ausdruck ;

Daraufhin wird der try-Block verlassen. Die nachfolgenden catch-Blcke werden der Reihe
nach geprft, ob sie fr die Behandlung der Exception zustndig sind. Der zustndige Excep-
tion-Handler fngt die Exception auf und fhrt die Fehlerbehandlung durch. Falls der
Exception-Handler das Programm nicht abbricht, wird es im Anschluss an den letzten catch-
Block fortgesetzt.


Exception-Matching
Der Typ in der exception-deklaration hinter dem Schlsselwort catch definiert die Zustn-
digkeit des Exception-Handlers fr die geworfene Exception. Es wird der erste Handler auf-
gerufen, dem das geworfene Exception-Objekt wie bei einem gewhnlichen Funktionsaufruf
bergeben werden kann. Die Zustndigkeit ist gegeben, wenn der Typ in der Exception-
Deklaration
- identisch ist mit dem Typ des geworfenen Exception-Objekts oder
- eine Basisklasse der Klasse des geworfenen Exception-Objekts ist.

Letzteres hat Einfluss auf die Reihenfolge der catch-Blcke. Bestehen Ableitungsbeziehun-
gen zwischen den Typen der mglicherweise geworfenen Exceptions, so mssen die catch-
Blcke in umgekehrter Reihenfolge der Ableitungshierarchie aufgefhrt werden.

cl ass Runt i meExcept i on {};
cl ass Di vi deByZer o : publ i c Runt i meExcept i on {};

i nt mai n( )
{
t r y
{
t hr ow Di vi deByZer o( ) ;
}
cat ch ( Runt i meExcept i on e) { / * . . . */ }
cat ch ( Di vi deByZer o e) { / * . . . */ } / / ni e auf ger uf en
}

Der letzte catch-Block kann einen Exception-Handler definieren, welcher alle Exceptions
auffngt. Dieser enthlt eine Folge von drei Punkten (Ellipse) in der Exception-Dekaration.

cat ch ( . . . )
{
/ / St andar d- Fehl er behandl ung
}



79
Wird die Exception von keinem Handler aufgefangen, wird die Laufzeit-Routine terminate()
aufgerufen, welche das Programm mit einer Fehlermeldung beendet. Fr die noch existieren-
den Objekte findet keine Destruktion statt.

Die vorgestellten Sprachkonstrukte sollen anhand einer verbesserten Version von Beispiel 5.2
untermauert werden. Die in der String-Klasse enthaltene Operatorfunktion operator[]()
bricht bei Angabe eines ungltigen Index mangels einer zufriedenstellenden Alternative das
Programm ab. Mit Hilfe von Exceptions lsst sich der Programmabbruch vermeiden.

Beispiel 7.1: (Exceptions)

/ / Dat ei myst r i ng. h

/ / Except i on Kl asse
cl ass Out Of Range {
i nt i ndex;
publ i c:
Out Of Range( i nt i ) { i ndex = i ; }
i nt get I ndex( ) { r et ur n i ndex; }
};

/ / St r i ng- Kl asse
cl ass St r i ng {
i nt si ze; / / Laenge der Zei chenket t e ( ohne ' \ 0' )
char *s; / / Zei chenket t e
publ i c:
/ / Konst r ukt or en und Dest r ukt or
St r i ng( const char * st r ) ;
~St r i ng( voi d) { del et e [ ] s; }
char & oper at or [ ] ( i nt i ) ;
};


/ / Dat ei myst r i ng. cpp

#i ncl ude <st r i ng. h>
#i ncl ude " myst r i ng. h"

/ / . . .

char & St r i ng: : oper at or [ ] ( i nt i )
{
i f ( i >= si ze)
t hr ow Out Of Range( i ) ; / / t hr ow Except i on
r et ur n s[ i ] ;
}


In der Elementfunktion operator[]() wird die Exception lediglich ausgeworfen. Die Excepti-
on-Behandlung findet im aufrufenden Programmteil - im vorliegenden Beispiel ist dies die
main()-Funktion - statt:


80
Beispiel 7.1 (Forts.): (Exceptions)

/ / Dat ei bsp_7_1. cpp

#i ncl ude <i ost r eam>
#i ncl ude " myst r i ng. h"

usi ng namespace st d;

i nt mai n( )
{
St r i ng s = " OOP" ;

f or ( i nt i =0; i <5; i ++)
{
t r y
{
cout << s[ i ] << \ n ;
}
cat ch ( Out Of Range e)
{
cer r << " I ndex " << e. get I ndex( ) << " out of Range" << \ n ;
}
}
r et ur n 0;
}

Bemerkungen
- Wie aus Beispiel 7.1 ersichtlich, ist es mglich, das Exception-Objekt mit der fr die
Fehlerbehandlung notwendigen Information auszustatten.



7.2 Exception Spezifikation

Funktionen oder Elementfunktionen, welche potentiell eine Exception werfen, sind durch ihre
Prototypen nicht vollstndig spezifiziert. Eine vollstndige Spezifikation muss zustzlich
Auskunft ber die potentiell auftretenden Exceptions geben. Nur auf diese Weise kann der
Aufrufer erkennen, dass der Funktionsaufruf in einen try-Block eingefasst und geeignete
Handler spezifiziert werden mssen.

Die Exception-Spezifikation einer Funktion gibt Auskunft ber die potentiell geworfenen
Exceptions. Syntaktisch wird dies mittels einer throw-Klausel erreicht, welche an den Proto-
typ oder den Funktionskopf angehngt wird und eine Liste von Exception-Typen enthlt.

doubl e di vi de( doubl e a, doubl e b)
t hr ow( Di vi deByZer o, AnyExcept i on) ;

Um zu dokumentieren, dass eine Funktion keine Exception wirft, enthlt die throw-Klausel
eine leere Liste.

doubl e di vi de( doubl e a, doubl e b) t hr ow( ) ;


81
Eine Exception-Spezifikation ist verbindlich. Wenn eine Funktion eine Exception wirft, wel-
che nicht in der throw-Klausel aufgefhrt ist und auch keine Unterklasse der aufgefhrten
Exceptions ist, wird die Funktion unexpected() aufgerufen. Diese beendet das Programm
durch Aufruf von terminate().

Bemerkungen:
- Funktionen ohne Exception-Spezifikation knnen jede Exception werfen.
- Die Exception-Spezifikation wird bisher nicht von allen C++-Compilern untersttzt.


7.3 Stack-Abwicklung

Falls das Werfen einer Exception zum Verlassen einer Funktion oder Elementfunktion fhrt,
wird der Strackframe der Funktion wie bei einem regulren Verlassen der Funktion abgebaut.
Lokal definierte Objekte werden dabei zerstrt.


Beispiel 7.2 a: (Stackabwicklung)
cl ass Except i on {};
cl ass Obj ect { / *. . . */ };

voi d f ( )
{
Obj ect obj ;

t hr ow Except i on( ) ;
}

i nt mai n( )
{
t r y {
f ( ) ;
}
cat ch ( Except i on e) {}
}


Dynamisch mittels new angelegte Objekte werden so natrlich nicht zerstrt. In diesem Falle
sollte die Funktion ber eine lokale Fehlerbehandlung verfgen, welche diese Objekte zer-
strt.


82
Beispiel 7.2b: (Explizite Zerstrung dynamischer Objkte)
cl ass Except i on {};

cl ass Obj ect { / *. . . */ };

voi d f ( )
{
Obj ect *obj = new Obj ect ;
t r y
{
t hr ow Except i on( ) ;
}
cat ch( Except i on e)
{ del et e obj ;
t hr ow; / / wi r f t er neut ei n Except i on- Obj ekt aus.
}
del et e obj ;
}

i nt mai n( )
{
t r y {
f ( ) ;
}
cat ch ( Except i on e) {}
}

Bemerkungen
- Der zustzliche lokale Exception-Handler ist notwendig, da der Zeiger obj im Exception-
Handler der main()-Funktion nicht mehr gltig ist.
- Durch das Weiterleiten der Exception im lokalen Exception-Handler bleibt der im voran-
gegangenen Beispiel vorliegende Kontrollfluss erhalten.



83
7.4 bungsaufgaben

Aufgabe 7.1
Realisieren Sie in der Lsung zu Aufgabe 6.2 die Fehlerbehandlung bei Indexberschreitung
mit Hilfe von Exceptions.


Aufgabe 7.2
Verndern Sie die bungsaufgabe 5.3 so, dass auch bei dem Versuch zur Erzeugung eines
Bruchs mit Nenner 0 kein Programmabbruch notwendig ist.


Aufgabe 7.3 (Allgemeine Fragen)
(a) Nennen Sie einen Fall, in dem die Fehlerbehandlung mit herkmmlichem Mitteln nur
unbefriedigend gelst werden kann.

(b) Welche Folgen hat eine geworfene, aber nicht aufgefangene Exception?

(c) Muss eine throw-Anweisung immer in einem try-Block stehen?

(d) Wozu dient die Exception-Spezifikation?



84
8. Die E/A-Bibliothek

8.1 Stream-Klassen

Analog zur Programmiersprache C verfgt C++ nicht ber eingebaute Sprachkonstrukte zur
Ein-/Ausgabe. Die Ein-/Ausgabe wird in Form einer Bibliothek zu Verfgung gestellt, wel-
che in der Sprache selbst realisiert ist.

Die in Abb. 8.1 dargestellte Klassenhierarchie wird durch Inklusion der Header-Datei
<iostream> verfgbar. Die Klassen sind im Namensraum std definiert und realisieren IO-
Streams zur Standard-Ein-/Ausgabe.

ios_base


Abb. 8.1: Klassen-Templates zur Standard-Ein-/Ausgabe


Bei den mit dem Wort basic beginnenden Klassen - diese sind in Abb. 8.1 kursiv darge-
stellt - handelt es sich um Klassen-Templates. Template-Parameter ist der Zeichentyp. Dies
wird im Normalfall der Typ char sein. Durch die Realisierung als Klassen-Templates ist die
Bibliothek aber auch fr andere Zeichenstze, wie z.B. den Unicode, vorbereitet.

Zur Bercksichtigung des blicherweise bentigten Datentyps char werden die Klassen-
Templates ber typedef-Anweisungen wie folgt konkretisiert:

t ypedef basi c_i os<char > i os;
t ypedef basi c_ost r eam<char > ost r eam;
t ypedef basi c_i st r eam<char > i st r eam;
t ypedef basi c_i ost r eam<char > i ost r eam;


Hierdurch entsteht die in Abb. 8.2 dargestellte Klassenhierarchie, auf die sich die nachfolgen-
den Betrachtungen beschrnken.


85
ios_base


Abb. 8.2: Klassen fr den Zeichentyp char


8.2 Standard-Ein/Ausgabe von eingebauten Typen

In Kapitel 2 wurde bereits die Standard-Ein-/Ausgabe von eingebauten Typen eingefhrt,
ohne genauer auf die zugrundeliegenden Mechanismen einzugehen. Nachfolgendes Beispiel
zeigt die Eingabe eines int-Werts von der Standardeingabe (Tastatur) und die nachfolgende
Ausgabe auf der Standardausgabe (Bildschirm):

i nt x;

ci n >> x;
cout << x;

- cout und cerr sind Instanzen der Klasse ostream. Sie realisieren die Standard-Ausgabe
bzw. die Standard-/Fehlerausgabe.
- cin ist eine Instanz der Klasse istream, welche die Standard-Eingabe realisiert.
- cout, cerr und cin werden automatisch in jedem C++-Programm erzeugt und stehen global
zur Verfgung.
- Die Operatoren << und >> werden durch Operatorfunktionen in den Klassen ostream bzw.
istream realisiert.


8.2.1 Ausgabe

Die Klasse ostream definiert den Ausgabeoperator << fr die folgenden Typen und ggf. fr
deren vorzeichenlose Varianten:

char long void * (Zeiger)
short float char * (Zeichenketten)
int double

86
Ein Ausschnitt aus der Klasse ostream soll die Realisierung verdeutlichen.

cl ass ost r eam: publ i c vi r t ual i os {
/ / . . .
publ i c:
ost r eam& oper at or << ( char *) ; / / Zei chenket t e
ost r eam& oper at or << ( char ) ; / / Zei chen
ost r eam& oper at or << ( i nt ) ; / / i nt - Wer t
/ / . . .
};

Jede Operatorfunktion ostream::operator<<() liefert eine Referenz auf das ostream-Objekt
zurck, so dass auf das Ergebnis ein weiterer Operator angewendet werden kann.

cout << x= << 10;

Diese Anweisung wird wie folgt interpretiert:

( cout . oper at or << ( " x=" ) ) . oper at or << ( 10) ;

Die Klasse ostream enthlt weitere Elementfunktionen zur Ausgabe. Die hier stellvertretend
aufgefhrte Funktion put() gibt ein Zeichen auf einem Ausgabe-Stream aus. Sie ist wie folgt
in der Klasse ostream definiert:

ost r eam& ost r eam: : put ( char ch) ;

Das nachfolgende Beispiel ziegt die Verwendung der Funktion:

char ch = x ;
cout . put ( ch) ;



8.2.2 Eingabe

Die Klasse istream definiert die Eingabeoperatoren >>fr die oben angegebenen Datentypen
und ist ansatzweise wie folgt definiert:

cl ass i st r eam: publ i c vi r t ual i os {
/ / . . .
publ i c:
i st r eam& oper at or >> ( char *) ; / / Zei chenket t e
i st r eam& oper at or >> ( char &) ; / / Zei chen
i st r eam& oper at or >> ( i nt &) ; / / i nt - Wer t
/ / . . .
};


87
Auch die Klasse istream enthlt weitere Elementfunktionen zur Eingabe. Die Funktion get()
liest ein Zeichen von der Standard-Eingabe. get() ist in istream wie folgt definiert:

i nt i st r eam: : get ( ) ;

get() ist insbesondere dann geeignet, wenn das berlesen von white characters nicht ge-
wnscht wird.

char ch;
ch = ci n. get ( ) ;


8.3 Stream-Status

Ein whrend einer Ein-/Ausgabeoperation auftretender Fehler fhrt dazu, dass eine Statusva-
riable in dem betreffenden Stream-Objekt mit einem Fehlerwert besetzt wird. Die private Sta-
tusvariable ist in der Basisklasse ios_base wie folgt definiert.

t ypedef i nt i ost at e;
i ost at e _St at e;

Ein in der Klasse ios_base definierter Enumerationstyp gibt die mglichen Werte der Fehler-
variablen an.

enum_I ost at e { / / St r eamSt at usbi t s
goodbi t = 0x00, / / al l es ok
eof bi t = 0x01, / / End of Fi l e
f ai l bi t = 0x02, / / l et zt e Ei n- / Ausgabe war f ehl er haf t
badbi t = 0x04, / / ungl t i ge Oper at i on, gr ober Fehl er
};

Der Status eines Stream kann mit folgenden Elementfunktionen, welche ebenfalls in der
Klasse ios_base definiert sind, abgefragt bzw verndert werden:

i os_base: : oper at or voi d *( ) ; / / NULL- Zei ger , f al l s der St r eam
/ / ni cht i n good- Zust and
bool i os_base: : oper at or ! ( ) ; / / t r ue, f al l s St r eam
/ / ni cht i n good- Zust and
voi d i os_base: : cl ear ( i ost at e st = goodbi t ) ; / / St at us auf st set zen
i ost at e i os_base: : r dst at e( ) ; / / St at us l esen
voi d i os_base: : set st at e( i ost at e st ) ; / / ei nzel nes St at usbi t set zen
bool i os_base: : good( ) ; / / t r ue, f al l s St r eami mgood- Zust and
bool i os_base: : eof ( ) ; / / t r ue, f al l s St r eamauf Dat ei ende
bool i os_base: : f ai l ( ) ; / / t r ue, f al l s f ai l bi t oder badbi t geset zt
bool i os_base: : bad( ) ; / / t r ue, f al l s badbi t geset zt

88
8.4 Standard-Ein-/Ausgabe von benutzerdefinierten Typen

8.4.1 Ausgabe

Fr die Ausgabe eines benutzerdefinierten Typs T wird eine Operatorfunktion operator<<()
mit Operanden vom Typ ostream und T bentigt. Da die vom System bereitgestellte Klasse
ostream nicht verndert werden sollte, ist eine externe Operatorfunktion mit folgender Signa-
tur erforderlich:

ost r eam& oper at or << ( ost r eam&s, T t var ) ;

Meist bentigen die Ausgabeoperatoren jedoch Zugriff zu den privaten Daten des Typs T.
Generell existieren zwei Lsungsmglichkeiten:
(a) Die Operatorfunktion wird als friend der Klasse T definiert.
(b) Die Klasse T wird mit Zugriffsfunktionen fr die ausgaberelevanten Daten ausgestattet.

Lsungsmglichkeit (a) wird an der Klasse String aus Beispiel 5.2 demonstriert:

Beispiel 8.1: (Ausgabe benutzerdefinierter Typen)
cl ass St r i ng { / / ( si ehe Bei spi el 5. 2)
/ / . .
publ i c:

/ / Ei n- Ausgabe- Funkt i onen
f r i end ost r eam& oper at or << ( ost r eam&s, St r i ng st r ) ;
f r i end i st r eam& oper at or >> ( i st r eam&s, St r i ng &st r ) ;

};

ost r eam& oper at or << ( ost r eam&s, St r i ng st r )
{
r et ur n s << st r . s;
}

Zur Demonstration von Lsungsmglichkeit (b) wurde die Klasse Complex aus Beispiel 5.1
wurde um ffentliche Zugriffsfunktionen auf Real- und Imaginrteil gestatten:

Beispiel 8.2: (Ausgabe benutzerdefinierter Typen)
cl ass Compl ex { / / si ehe Bei spi el 5. 1
doubl e r e, i m;
publ i c:
/ / Zugr i f f sf unkt i onen
doubl e r eal ( ) { r et ur n r e; }
doubl e i mag( ) { r et ur n i m; }
};

ost r eam& oper at or << ( ost r eam&s, Compl ex z)
{
r et ur n s << " ( " << z. r eal ( ) << " , " << z. i mag( ) << " ) " ;
}


89
Die Ausgabe von komplexen Zahlen kann nun syntaktisch vllig analog zu eingebauten Ty-
pen erfolgen.

Compl ex x( 1, 2) ;
cout << " x = " << x << ' \ n' ;

Das Beispiel erzeugt die Ausgabe x = ( 1. 0, 2. 0) .



8.4.2 Eingabe

Die Eingabe fr benutzerdefinierte Typen kann auf hnlich Weise realisiert werden wie die
Ausgabe. Allerdings erfordert die Operatorfunktion fr die Eingabe als zweiten Parameter
eine Referenz auf ein Objekt des benutzerdefinierten Typs T.

Die Operatorfunktion operator>>() fr String-Objekte ist als friend-Funktion der Klassen
String wie folgt definiert:

Beispiel 8.1 Forts.): (Eingabe benutzerdefinierter Datentypen)
i st r eam& oper at or >> ( i st r eam&s, St r i ng &st r )
{
char buf [ 100] ; / / Puf f er f r 100 Zei chen
s >> buf ;
st r . si ze = st r l en( buf ) ;
del et e [ ] st r . s;
st r . s = new char [ st r . si ze+1] ;
st r cpy( st r . s, buf ) ;
r et ur n s;
}

Im Falle der Klasse Complex sei das Eingabeformat (d,d), wobei d eine double- oder int-
Konstante ist. Mgliche Syntaxfehler bei der Eingabe mssen in diesem Falle abgefangen und
behandelt werden. Hierfr soll der in Kapitel 6 vorgestellte Exception-Mechanismus einge-
setzt werden. Zu diesem Zweck wird die Klasse Exception wie folgt definiert:

Beispiel 8.2 Forts.): (Eingabe benutzerdefinierter Datentypen)
cl ass Except i on {
char *er r Message;
publ i c:
Except i on( char * t ext ) { er r Message = t ext ; }
char *get Message( ) { r et ur n er r Message; }
};


Die Eingabe-Operatorfunktion wird analog zur Ausgabe als freie C-Funktion definiert.


90
Beispiel 8.2: (Eingabe benutzerdefinierter Typen)
i st r eam& oper at or >>( i st r eam&s, Compl ex &z) t hr ow ( Except i on)
{
doubl e r e = 0, i m= 0;
char ch = ' ' ;

s >> ch; / / er st es Zei chen l esen
i f ( ch ! = ' ( ' )
s. set sat e( i os: : f ai l bi t ) ; / / Fehl er st at us set zen
el se
{ s >> r e >> ch; / / Real t ei l und nchst es Zei chen l esen
i f ( ch ! = ' , ' )
s. set st at e( i os: : f ai l bi t ) ; / / Fehl er st at us set zen
el se
{ s >> i m>> ch; / / I magi nr t ei l und nchst es Zei chen l esen
i f ( ch ! = ' ) ' )
s. set st at e( i os: : f ai l bi t ) ; / / Fehl er st at us set zen
}
}
i f ( s. good( ) ) / / f al l s Ei ngabe er f ol gr ei ch
z = Compl ex( r e, i m) ;
el se
t hr ow Except i on( " Fehl er i n der Ei ngabe- Synt ax" ) ;
r et ur n s;
}

Bemerkungen
- Die Syntaxbestandteile des Eingabeformats werden schrittweise eingelesen. Falls die Ein-
gabesyntax nicht korrekt ist, wird das failbit in der Zustandstandsvariablen von s gesetzt.
- Obwohl der Fehlerwert failbit in der Klasse ios_base definiert ist, wird er hier aus Grn-
den der Kompatibilitt mit Vorgngerversionen der E-/A-Bibliothek ber die von ios_base
abgeleitete Klasse ios qualifiziert, was aufgrund der Vererbungsbeziehung mglich ist.
- Mit dem abschlieenden Test wird sichergestellt, dass z nur dann verndert wird, wenn die
Eingabe erfolgreich war.
- Im Fehlerfall wird eine Exception geworfen, welche im aufrufenden Teil aufgefangen wer-
den muss

Benutzerseitig kann die Eingabe von komplexen Zahlen nun wie folgt realisiert werden:

Compl ex a;

cout << " Kompl exe Zahl ei ngeben: " ;
t r y
{
ci n >> a;
}
cat ch ( Except i on e)
{ cout << e. get Message( ) << ' \ n' ;
ci n. cl ear ( ) ;
}



91
8.5 Formatierung

8.5.1 Formatierung durch Manipulation des Stream-Objekts

Die Formatierung der Ein-/Ausgabe wird Stream-spezifisch durch eine Zustandsvariable ge-
steuert, welche wie folgt in ios_base definiert ist:

t ypedef i nt f mt f l ags;
f mt f l ags _Fmt f l ;

Die Variable _Fmtfl enthlt einen Bit-Vektor, dessen Stellen durch Oder-Verknpfung ge-
setzt werden knnen. Die einzelnen Bitstellen ( Flags) sind ber einen Enumerationstyp in
der Klasse ios_base definiert.

enum_Fmt f l ags {
ski pws = 0x0001, / / whi t e spaces i gnor i er en
uni t buf = 0x0002, / / Puf f er nach j eder Ausgabe l eer en
upper case = 0x0004, / / Gr obuchst aben bei Hex- Ausgabe
showbase = 0x0008, / / Basi s ausgeben ( 0 bei Okt , 0x bei Hex)
showpoi nt = 0x0010, / / Gl ei t punkt zahl en mi t Dezi mal punkt
showpos = 0x0020, / / Ausgabe des posi t i ven Vor zei chens
l ef t = 0x0040, / / l i nksbndi ge Ausgabe
r i ght = 0x0080, / / r echt sbndi ge Ausgabe
i nt er nal = 0x0100, / / Vor zei chen l i nks- , Wer t r echt sbndi g
dec = 0x0200, / / dezi mal
oct = 0x0400, / / okt al
hex = 0x0800, / / hexadezi mal
sci ent i f i c = 0x1000, / / Gl ei t punkt zahl en i mExponent i al - For mat
f i xed = 0x2000, / / Gl ei t punkt zahl en i mFest punkt - For mat
bool al pha = 0x4000, / / al phabet i sche Ausgabe von t r ue/ f al se
adj ust f i el d = 0x01c0, / / Maske: l ef t | r i ght | i nt er nal
basef i el d = 0x0e00, / / Maske: okt | dec | hex
f l oat f i el d = 0x3000, / / Maske: f i xed | sci ent i f i c
};

Die Flags knnen durch die folgenden in ios_base definierten Elementfunktionen abgefragt
bzw. manipuliert werden:

f mt f l ags i os_base: : f l ags( ) ; / / Lesen der For mat - Fl ags
f mt f l ags i os_base: : f l ags( f mt f l ags f l ) ; / / Set zen der For mat - Fl ags
f mt f l ags i os_base: : set f ( f mt f l ags f l ) ; / / Set zen des For mat - Fl ag f l
f mt f l ags i os_base: : set f ( f mt f l ags f l , f mt f l ags mask) ;
/ / Zur ckset zt en der For mat - Fl ags i n mask
/ / und Set zen des For mat - Fl ag f l
voi d i os_base: : unset f ( f mt f l ags mask) ; / / Zur ckset zt en der
/ / For mat - Fl ags i n mask

Die folgenden Anweisungen bewirken beide, dass das positive Vorzeichen bei Ausgabe von
numerischen Werten mit ausgegeben wird:

cout . f l ags( cout . f l ags( ) | i os: : showpos) ;
cout . set f ( i os: : showpos) ;


92
Wenn ein Flag gesetzt werden soll, mssen mglicherweise kollidierende Flags zurckgesetzt
werden. Hierfr eignet sich die Elementfunktion setf() mit zwei Parametern in Verbindung
mit den in _Fmtflags definierten Masken. Soll also die Ausgabe von Dezimalzahlen in Hexa-
dezimaldarstellung erfolgen, so knnte dies wie folgt realisiert werden:

cout . set f ( i os: : hex, i os: : basef i el d) ;


Die nachfolgend aufgefhrten Instanzvariablen der Klasse ios_base enthalten weitere Infor-
mationen zur Formatsteuerung:

i nt _Wi de; / / Anzahl der Ausgabest el l en
i nt _Pr ec; / / Anzahl der si gni f i kant en St el l en
/ / bei Gl ei t punkt zahl en

Sie knnen durch entsprechende Elementfunktionen abgefragt und verndert werden:

i nt wi dt h( ) ; / / l i ef er t akt uel l en Wer t von _Wi de zur ck
i nt wi dt h( i nt _Nw) ; / / set zt _Wi de auf _Nw, l i ef er t al t en Wer t zur ck
i nt pr eci si on( ) ; / / l i ef er t akt uel l en Wer t von _Pr ec zur ck
i nt pr eci si on( i nt _Np) ; / / set zt _Pr ec auf _Np, l i ef er t al t en Wer t zur ck


Ferner enthlt die Klasse ios eine Instanzvariable zur Aufnahme des aktuellen Fllzeichens.
Da diese Variable zeichentyp-abhngig ist, kann sie nicht in der zeichentyp-unabhngigen
Basisklasse ios_base definiert werden

char _Fi l l ch; / / Fl l zei chen

Die ebenfalls in ios definierten Zugriffsfunktionen lauten:

char f i l l ( ) ; / / l i ef er t akt uel l en Wer t von _Fi l l ch zur ck
char f i l l ( char _Nf ) ; / / set zt _Fi l l ch auf _Nf , l i ef er t al t en Wer t zur ck


Die direkte Manipulation von Format-Flaggen und sonstigen fr die Formatierung relevanten
Variablen ist relativ unhandlich. Es wird deshalb hier nicht nher auf diese Mglichkeit ein-
gegangen. Fr weitere Informationen wird auf die Handbcher bzw. die Online-Hilfe des
jeweiligen Compilers verwiesen. Eine elegantere Mglichkeit zur Formatsteuerung bieten die
im folgenden Kapitel beschriebenen Manipulatoren.



93
8.5.2 Formatierung mittels Manipulatoren

Ein Manipulator ist eine Operation, welche direkt in die Ein- oder Ausgabe eingefgt wird
zwecks Vernderung des Formatierungszustands.

Die Klasse ostream enthlt eine inline-Elementfunktion ostream::operator<<() der folgen-
den Form:

ost r eam& oper at or <<( ost r eam& ( *_F) ( ost r eam&) )
{
r et ur n ( ( *_F) ( *t hi s) ) ;
}

Das Argument der Operatorfunktion ist ein Zeiger auf eine Funktion mit der Signatur:

ost r eam& f ( ost r eam&s) ;

Es handelt sich hierbei um einen Manipulator fr Ausgabe-Streams. Die Operatorfunktion
ostream::operator<<() ruft den Manipulator auf und reicht dessen Rckgabewert durch.
Aufgrund der Signatur der oben aufgefhrten Operatorfunktion kann ein Manipulator in die
Ausgabe eingekettet werden.

cout << f << x;

Erluterung:
- Der Manipulator f() verndert den Format-Status des Streams cout, was eine entsprechend
formatierte Ausgabe der Variablen x zur Folge hat.


In analoger Weise enthlt istream eine Operatorfunktion istream::operator>>().

i st r eam& oper at or >>( i st r eam& ( *_F) ( i st r eam&) )
{
r et ur n ( ( *_F) ( *t hi s) ) ;
}

Ein Manipulator fr Eingabe-Streams hat somit folgende Signatur:

i st r eam& f ( i st r eam&s) ;


Standard-Manipulatoren
Die E-/A-Bibliothek von C++ verfgt ber eine Reihe von parameterlosen Standard-
Manipulatoren, welche nach dem oben beschriebenen Prinzip realisiert sind. Diese sind zum
grten Teil in iosbase definiert. Lediglich die zeichentyp-abhngigen Manipulatoren sind in
ostream bzw. istream definiert. Tabelle 8.1 gibt einen berblick ber die parameterlosen
Standard-Manipulatoren:


94
Tabelle 8.1: Parameterlose Standard-Manipulatoren

Name Funktion
bool al pha
nobool al pha
true/false alphabetisch ausgeben bzw. lesen
true/false numerisch (0/1) ausgeben bzw. lesen
dec
hex
oct
dezimal
hexadezimal
oktal
endl neue Zeile (\n wird ausgegeben)
f i xed
sci ent i f i c
Festkomma-Format
Exponential-Format
f l ush Ausgabepuffer leeren
i nt er nal
l ef t
r i ght
Fllzeichen zwischen Vorzeichen und Wert ausgeben
linksbndige Ausgabe
rechtsbndige Ausgabe
showpoi nt
noshowpoi nt
Dezimalpunkt in jedem Fall darstellen
Dezimalpunkt nicht in jedem Fall darstellen
showpos
noshowpos
positives Vorzeichen darstellen
positives Vorzeichen nicht darstellen
ski pws
noski pws
white spaces ignorieren
white spaces bercksichtigen
uni t buf
nouni t buf
Puffer nach jeder Ausgabe leeren
Ausgabe puffern
upper case
noupper case
Grobuchstaben bei Hex-Ausgabe bzw. Hex-Eingabe
Kleinbuchstaben bei Hex-Ausgabe bzw. Hex-Eingabe
ws fhrende white spaces aus der Eingabe entfernen


Das nachfolgende Beispiel zeigt eine Anwendung einiger Standardmanipulatoren.

i nt x = 127;
cout << hex << upper case << x << endl ; / / Ausgabe: 7F<CR>




95
Parametrierte Standard-Manipulatoren
Die parameterlosen Manipulatoren eigenen sich z.B. nicht zur Vernderung der Ausgaben-
breite oder der Genauigkeit. Hierfr sind parametrierte Manipulatoren erforderlich. Tabelle
8.2 listet die parametrierten Manipulatoren auf. Deren Anwendung erfordert die Inklusion der
Header-Datei <iomanip>.


Tabelle 8.2: Parametrierte Standard-Manipulatoren

Name Funktion
r eset i osf l ags( i os: : f mt f l ags M) Flags entsprechend Bitmaske M
zurcksetzen
set i osf l ags( i os: : f mt f l ags M) Flags entsprechend Bitmaske M setzen
set base( i nt B) Basis 8, 10 oder 16 festlegen
set f i l l ( char c) Fllzeichen festlegen
set pr eci si on( i nt n) Genauigkeit bei Gleitpunktzahlen
festlegen
set w( i nt w) Stelligkeit der Ausgabe


Das nachfolgende Beispiel zeigt die Verwendung von parametrierten Manipulatoren:

i nt x = 127;
cout << set w( 5) << r i ght << set f i l l ( 0' ) << x << endl ;
/ / Ausgabe: 00127<CR>

Wie an der Syntax erkennbar ist, handelt es sich bei set w( 5) nicht um einen Funktionszei-
ger. Es liegt also eine von parameterlosen Manipulatoren abweichende Implementierung vor.
Parametrierte Manipulatoren sind in der Headerdatei in <iomanip> mit Hilfe von Templates
definiert. Nachfolgend wird eine alternative, sehr einfache Mglichkeit zu Realisierung von
parametrierten Manipulatoren vorgestellt.


Selbstdefinierte Manipulatoren
Mit den Mglichkeiten zur direkten Vernderung der Format-Flaggen eines Streams knnen
Manipulatoren selbst erstellt werden. Fr parameterlose Manipulatoren muss hierzu einfach
eine Funktion mit der oben beschriebenen Schnittstelle definiert werden. Das nachfolgende
Beispiel zeigt einen Manipulator, welcher die Ausgabe auf hexadezimal mit Grobuchstaben
umstellt.


96
Beispiel 8.3): (selbstdefinierte Manipulatoren)
ost r eam& hexup( ost r eam&s)
{
s. set f ( i os: : hex, i os: : basef i el d) ;
s. set f ( i os: : upper case) ;
r et ur n s;
}

Der Manipulator hexup() kann wie folgt in einer Ausgabe-Anweisung verwendet werden:

cout << hexup << x << endl ;

Etwas komplizierter gestaltet sich die Realisierung von parametrierten Manipulatoren. Dieses
soll anhand eines Manipulators demonstriert werden, welcher eine Linie in Form einer belie-
big langen Folge von _-Zeichen ausgibt. Der Manipulator wird wie folgt aufgerufen:

cout << Li ne( 25) << endl ;

Der Ablauf des Manipulators erfolgt in 2 Schritten:
(1) Li ne( 25) erzeugt ein Objekt einer Klasse namens Line. Dabei wird der Parameter 25
in einer ffentlichen Instanzvariablen des Line-Objekt abgelegt.
(1) Ein fr das Line-Objekt definierter Ausgabeoperator << wird aktiviert, welcher, unter
Verwendung der ffentlichen Instanzvariable, die Ausgabe der 25 _-Zeichen veran-
lasst.

Die Klasse Line und der zugehrige Ausgabeoperator sind in dem folgenden Beispiel aufge-
fhrt.


Beispiel 8.3 (Forts.): (selbstdefinierte Manipulatoren)
/ / Dat ei mani pul a. h

cl ass Li ne {
publ i c:
i nt anz;
Li ne( i nt i ) ;
};

i nl i ne Li ne: : Li ne( i nt i )
{
anz = i ;
}

i nl i ne ost r eam& oper at or << ( ost r eam&os, Li ne &l )
{
f or ( i nt i =0; i < l . anz; i ++)
os << ' _' ;
r et ur n os;
}


97
8.6 Dateien und Streams

Fr die Ein-/Ausgabe auf Dateien stellt die E/A-Bibliothek die Klassen ifstream, ofstream
und fstream zur Verfgung (siehe Abb 8.3). Diese stellen Konkretisierungen der Klassen-
Templates basic_ifstream, basic_ofstream bzw. basic_fstream fr den Zeichentyp char dar
(siehe 8.1). Die Klassen sind in der Header-Datei <fstream> definiert.

ifstream ist von istream abgeleitet und realisiert Eingabedateien. Aufgrund der Vererbung
steht die gesamte Funktionalitt von istream auch fr Dateien zur Verfgung.
ofstream ist von ostream abgeleitet und realisiert Ausgabedateien.
fstream verbindet die Funktionalitt von istream und ostream und realisiert Ein-
/Ausgabedateien


ios_base


Abb. 8.3: Klassen der iostream-Bibliothek



8.6.1 ffnen von Dateien

Die Klassen ifstream, ofstream und fstream enthalten jeweils eine Elementfunktion namens
open(). Fr fstream ist open() wie folgt definiert:

voi d f st r eam: : open( char *s, i nt m= i os: : i n | i os: : out ) ;

- Der erste Parameter gibt den Namen der zu ffnenden Datei an.
- Der zweite Parameter gibt den gewnschten Zugriffsmodus an und ist in den einzelnen
Klassen in geeigneter Weise vorbesetzt. Mgliche Werte sind in der Klasse ios_base als
Enumerations-Typ wie folgt definiert:

98
enumopenmode {
i n = 0x01, / / f f nen zumLesen
out = 0x02, / / f f nen zumSchr ei ben
at e = 0x04, / / Schr ei b- / Lesezei ger bei mf f nen auf Dat ei ende set zen
app = 0x08, / / J ede Schr ei boper at i on amDat ei ende
t r unc = 0x10, / / Best ehenden Dat ei i nhal t bei mf f nen l schen
bi nar y = 0x20 / / Schr ei b- / Leseoper at i onen i mBi nr modus
};


Zustzlich stellen die Klassen Konstruktoren bereit, welche ein automatisches ffnen der
gewnschten Datei bewirken:

f st r eam: : i f st r eam( char *s, i os: : openmode m= i os: : i n) ;
of st r eam: : of st r eam( char *s, i os: : openmode m= i os: : out ) ;
f st r eam: : f st r eam( char *s, i os: : openmode m= i os: : i n |
i os: : out ) ;

Das folgende Beispiel zeigt die Erzeugung und das gleichzeitige ffnen einer Eingabedatei:

i f st r eami n( myi nput . t xt ) ;

Soll die Datei mit dem Erzeugen des Stream-Objekts nicht gleichzeitig geffnet werden, so
steht hierfr ein parameterloser Konstruktor zur Verfgung. Natrlich muss die Datei in die-
sem Fall nachtrglich durch open() geffnet werden.

i f st r eami n;
i n. open( myi nput . t xt ) ;


8.6.2 Schlieen von Dateien

Dateien werden automatisch geschlossen, wenn das Stream-Objekt zerstrt wird. Falls eine
Datei schon vorher nicht mehr benutzt wird, so sollte sie schon vorher geschlossen werden.
Hierfr steht in jeder der drei Klassen eine parameterlose Elementfunktion close() zur Verf-
gung.

i n. cl ose( ) ;

Die Ein-/Ausgabe auf Dateien - und dabei insbesondere das ffnen und Schlieen einer Datei
- soll am Beispiel eines Programms verdeutlicht werden, welches eine Datei in eine andere
kopiert.

99
Beispiel 8.4: (Ein-/Ausgabe von/auf Dateien)
/ / Except i on- Kl asse

cl ass Except i on {
char *er r Message;
publ i c:
Except i on( char * t ext ) ;
char *get Message( ) ;
};

i nl i ne Except i on: : Except i on( char * t ext )
{
er r Message = t ext ;
}

i nl i ne char *Except i on: : get Message( )
{
r et ur n er r Message;
}

/ / mai n- Funkt i on

i nt mai n( i nt ar gc, char *ar gv[ ] )
{ char ch;

t r y
{ i f ( ar gc ! = 3)
t hr ow Except i on( " Fal sche Anzahl an Ar gument en" ) ;

i f st r eamf r om( ar gv[ 1] ) ;
i f ( ! f r om)
t hr ow Except i on( " I nput - Dat ei kann ni cht geoef f net wer den" ) ;
of st r eamt o( ar gv[ 2] ) ;
i f ( ! t o)
t hr ow Except i on( " Out put - Dat ei kann ni cht geoef f net wer den" ) ;

whi l e ( f r om. get ( ch) )
t o. put ( ch) ;

i f ( ! f r om. eof ( ) | | t o. bad( ) )
t hr ow Except i on ( " Fehl er bei mKopi er en" ) ;
}
cat ch ( Except i on e)
{ cout << e. get Message( ) << ' \ n' ;
r et ur n - 1;
}
r et ur n 0;
}


Bemerkungen:
- Die Namen von Quell- und Zieldatei werden ber Kommandozeilenargumente bergeben.
- Das Beispiel verwendet die schon aus Beispiel 8.2 bekannte Exception-Klasse.
- Da die Eingabedatei im Textmodus geffnet wurde, ist das Programm zum Kopieren von
Binrdateien nicht geeignet.

100
8.7 bungsaufgaben

Aufgabe 8.1
Gegeben sei die folgende unvollstndige Klasse zur Ablage von Studentendaten.
cl ass St udent {
char *name, *vor Name;
l ong mat r Nr ;
publ i c:
/ / . . .
};

Auf einer Datei namens student.txt liegt eine unbestimmte Zahl von Studentendatenstzen
im folgenden Format:
Li sa Li ebl i ch 1221111
Rudi Rat l os 1221112
Wal t er Wi cht i g 1221113

Ergnzen Sie die Klasse Student in geeigneter Weise um einen Eingabe- und einen Ausgabe-
operator. Schreiben Sie dazu ein Programm, welches die Daten auf der Datei sukzessive in
ein Objekt vom Typ Student einliest und das so erzeugte Objekt wieder auf dem Bildschirm
ausgibt. Die Bildschirmausgabe soll dabei identisch aussehen, wie die Daten auf der Datei.

Hinweis:
Achten Sie darauf, dass bei der Eingabe die notwendigen Speicherbereiche existieren.


Aufgabe 8.2
Eine Textdatei enthlt eine Folge unbestimmter Lnge von positiven, ganzen Zahlen im Text-
format, welche jeweils durch einen Zeilenvorschub getrennt sind. Schreiben Sie ein C++-
Programm, welches die Zahlen einliest, deren Maximum bestimmt und dieses auf dem Bild-
schirm ausgibt.


Aufgabe 8.3 (Allgemeine Fragen)

(a) Warum kann man den Ausgabe-Operator << wie folgt verketten?
cout<<3<<\n;
(b) Ersetzen Sie in dem folgenden Ausdruck die Operatoren durch Aufrufe der zugehrigen
Operatorfunktion: cout << 3 << '\n';
(c) Warum knnen Ein-/Ausgabeoperatoren fr selbstdefinierte Typen nur als freie C-
Funktionen realisiert werden?
(d) Warum bentigt ein Eingabeoperator fr den Typ T immer ein Referenzparameter vom
Typ T?
(e) Wozu dient die folgende Elementfunktion?
ostream& ostream::operator<< (ostream& (*f)(ostream &));

101
Teil 2 Datenstrukturen und Algorithmen in C++


9. Datentypen und Datenstrukturen

Fr die Qualitt eines Programms spielt eine gute Strukturierung der verarbeiteten Daten
eine wesentliche Rolle. Die Wahl von geeigneten Datenstrukturen bestimmt in entscheidender
Weise die Verstndlichkeit, die nderungsfreundlichkeit und Effizienz eines Programms.


9.1 Datentypen

Ausgangspunkt jeglicher Strukturierung sind die einfachen Datentypen, wie sie die jeweilige
Programmiersprache zur Verfgung stellt.

Ein einfacher Datentyp definiert einen bestimmten Wertebereich, zusammen mit den auf
diesem Wertebereich ausfhrbaren Operationen. Einfache Datentypen werde auch als
elementare oder atomare Datentypen bezeichnet.

Der Datentyp int umfasst den Wertebereich der ganzen Zahlen von -2147483648 bis
2147483647 und der erlaubten Operationen (+ - * / usw.).

Die interne Reprsentation eines einfachen Datentyps (Bitmuster, Gre) ist durch die zu-
grunde liegende Rechner-Hardware und den verwendeten Compiler festgelegt.

Durch Zusammenfassung von elementaren Datentypen entsteht ein strukturierter Daten-
typ.

Beispiele fr strukturierte Datentypen sind Arrays oder Strukturen. Fr die hierbei notwendi-
ge Strukturierung stellen die Programmiersprachen Sprachkonstrukte zur Verfgung. Die
Programmiersprachen selber stellen nur elementare Operationen auf strukturierten Datenty-
pen zur Verfgung. Es sind dies meist Zugriffsoperationen auf die elementaren Komponenten
des strukturierten Datentyps. Beispiele sind der indizierte Zugriff auf eine Array-Komponente
oder die Selektion einer Strukturkomponente.


9.2 Datenstrukturen

Beim Entwurf eines Programms spielt die interne Reprsentation der Daten zunchst noch
eine untergeordnete Rolle. In dieser Phase treten vielmehr anwendungsorientierte Datenkon-
zepte in den Vordergrund.

Im Mittelpunkt der Betrachtungen fr eine Bibliotheksausleihe steht die Liste der Bcher mit
den fr ein Ausleihsystem erforderlichen Merkmalen (Signatur, Titel, Autor, Anschaffungs-
datum, Status der Ausleihe usw.). In unmittelbarem Zusammenhang mit der anwendungsori-
entierten Datenreprsentation stehen die anwendungsorientierten Operationen (Aufnahme
eines neuen Buches, Suchen eines Buches, Ausleihen eines Buches).

Diese anwendungsorientierte Sichtweise in der Entwurfsphase fhrt zu dem Begriff des abs-
trakten Datentyps.

102

Ein abstrakter Datentyp ( ADT) besteht aus einem mglicherweise strukturierten Wer-
tebereich und darauf definierten Operationen. Die Menge der Operationen bezeichnet man
auch als Schnittstelle des Datentyps.

Der Schwerpunkt der Betrachtung liegt bei einem abstrakten Datentyp auf dessen Schnittstel-
le, also darauf, was er kann, und nicht darauf, wie er es realisiert. Die Realisierung eines ADT
fhrt dann schlielich zur begrifflichen Definition einer Datenstruktur.

Eine Datenstruktur ist die Realisierung bzw. Implementierung eines abstrakten Datentyps
in einer bestimmten Programmiersprache.

Dies bedeutet konkret die Auswahl der elementaren und/oder strukturierten Datentypen
zwecks Reprsentation der anwendungsorientierten Daten sowie die Implementierung der
Operationen in Form von bestimmten Algorithmen.

Bemerkung:
Fr die Implementierung eines ADT gibt es in den meisten Fllen mehrere Alternativen. We-
sentlich ist die Einhaltung der Spezifikation der Schnittstelle. Falls dies gelingt, hat eine nach-
trgliche Modifikation der Implementierung keine Auswirkungen auf das brige Programm-
umfeld.


9.3 Standarddatenstrukturen

Bei der Einfhrung der abstrakten Datentypen bzw. deren Implementierung in 9.2 stand der
Anwendungsaspekt im Vordergrund. Ein anwendungsorientierter ADT ist das Ergebnis einer
problemorientierten Anforderungsanalyse und hat keinen Anspruch auf allgemeine Verwend-
barkeit.

In der Informatik hat sich in der Vergangenheit jedoch eine Reihe von immer wiederkehren-
den Problemen oder Teilproblemen herauskristallisiert, fr die es allgemeine Lsungen in
Form von Standarddatenstrukturen gibt.

Die in 9.2 aufgefhrte Buchliste ist eine spezielle Ausprgung des Standarddatentyps Liste
mit den Operationen Einfgen, Lschen, Suchen und Sortieren. Fr die Implementierung des
ADT Liste existieren verschiedene, standardisierte Implementierungen in Form von sequen-
ziell oder verkettetet gespeicherten Listen. Die Daten der Liste knnen im Arbeitsspeicher
oder auf dem Plattenspeicher abgelegt sein. Dieses hat unmittelbare Auswirkungen auf die
Zugriffsgeschwindigkeit und erfordert mglicherweise unterschiedliche Implementierungen.

Fr die genanten Operationen auf Listen, insbesondere fr die aufwendigen Operationen der
Suche und der Sortierung existieren diverse Algorithmen, welche in der Vergangenheit im
Hinblick auf Laufzeiteffizienz und Speicherbedarf hinlnglich untersucht wurden.

Vielfach werden Listen mit speziellem Zugriffsanforderungen bentigt. Dies gilt insbesonde-
re fr die Stapel (Stack) mit einem LIFO-Zugriffsverhalten sowie fr die Schlangen (
Queues) mit einem FIFO-Zugriffverhalten.

Neben der linearen Organisation von Daten, wie sie den Listen zu eigen ist, existieren nichtli-
neare Organisationsformen. Ein Beispiel ist die hierarchische Organisation von Dateien in

103
den heute blichen Dateisystemen. Fr eine derartige hierarchische Organisation eigen sich
Standarddatenstrukturen, die als Bume bezeichneten werden. Die leitenden Verbindungen
auf einer Leiterplatte oder die Straenfhrung in einer Stadt weisen netzartige Struktur auf.
Geeignete Standarddatenstrukturen zur Reprsentation von netzartigen Strukturen sind die
Graphen.


104
10. Sequenziell gespeicherte lineare Listen

10.1 Definition

Bei den linearen Listen handelt es sich um eine der am hufigsten verwendeten Datenstruktu-
ren. Sie knnen im einfachsten Fall sequenziell gespeichert werden. Formal kann eine se-
quenziell gespeicherte, lineare Liste wie folgt definiert werden:


Sei O = {k0,..,kn-1} eine Menge von Datenobjekten eines bestimmten Grundtyps. Die Da-
tenstruktur L = (k0,...,kn-1) heit lineare Liste, wenn folgende Bedingungen gelten:
(1) Fr jedes Element ki existiert genau ein Nachfolgerelement ki+1 (i=0,...,n-2).
(2) Fr jedes Element ki existiert genau ein Vorgngerelement ki-1 (i=1,...,n-1).

Eine lineare Liste ist sequenziell gespeichert, wenn die Speicheradresse eines Elements
wie folgt bestimmt werden kann:
Adresse(ki) = Adresse(ki-1) + Elementlnge
= Adresse(k0) + i * Elementlnge

Man beachte die hnlichkeit der Definition mit der Semantik der Zeigerarithmetik in C++
beim Zugriff auf Arrays. Es ist daher nicht verwunderlich, dass Arrays bevorzugt fr die Im-
plementierung von sequenziell gespeicherten linearen Listen Verwendung finden.

Typische Operationen auf Listen sind das Einfgen und Lschen von Listenelementen.


Einfgen eines Listenelements x an Position p
Alle Elemente ab Position p mssen um eine Position nach hinten gerckt werden. Sei also
L=(k0,...,kn-1) die ursprngliche Liste und 0pn-1, so ist L'=(k0,..,kp-1,x,kp,..,kn-1) die resul-
tierende Liste.


Entfernen des Listenelements an Position p
Hierbei rcken alle Elemente ab Position p+1 um eine Position nach vorn.
Sei also L=(k0,...,kp-1,kp,kp+1,...,kn-1) die ursprngliche Liste, so ist L'=(k0,..,kp-1,kp+1,..,kn-1) die
resultierende Liste.


Bemerkungen:
- Das Einfgen und Lschen am Listenende erfordert keine Verschiebeoperationen und da-
mit den geringsten Aufwand.
- Hufig ist es jedoch erforderlich, dass die Listenelemente in einer bestimmten Weise sor-
tiert sind. Dies macht ein Einfgen im Listeninneren erforderlich.
- Implementierungstechnisch ist das Einfgen eines neuen Elements nur dann mglich, falls
ausreichend Speicherplatz am Ende der Liste verfgbar ist.
- Neben den Einfge- und Lschoperationen tauchen hufig eher primitive Probleme auf,
wie z.B. die Bestimmung der Listenlnge oder die Abfrage, ob die Liste leer bzw. voll ist.

105
10.2 Abstraktion

Ein ADT fr eine sequenziell gespeicherte, lineare Liste spezifiziert die Operationen zum
Erzeugen und Lschen der Liste, zum Eintragen, Lschen und Modifizieren von Listenele-
menten sowie diverse Zugriffsfunktionen. Falls nach bestimmten Listenelementen gesucht
werden soll oder falls die Listenelemente sortiert sein sollen, bentigen diese eine geeignete
Schlsselkomponente. Lsst man den Typ der Listenelemente zunchst offen, so kann der
ADT Sequence in der Syntax eines C++-Templates wie folgt spezifiziert werden:

Beispiel 10.1: (ADT Sequence)
t empl at e<cl ass AD>
cl ass Sequence {
publ i c:
Sequence( unsi gned n) ;
~Sequence( voi d) ;
unsi gned si ze( voi d) const ;
bool i sEmpt y( voi d) const ;
bool i sFul l ( voi d) const ;
AD& oper at or [ ] ( unsi gned pos) t hr ow ( I nval i dI ndex) ;
voi d pushBack( AD &el ) t hr ow( SequenceOver f l ow) ;
voi d i nser t ( AD &el ) t hr ow( SequenceOver f l ow) ;
bool r emove( AD &el ) ;
};

Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der in der Liste verwal-
teten Nutzdaten.
- Fr den Typ AD wird ein Standardkonstruktor, die Wertzuweisung sowie die Operatoren
== (quivalenz) und < (Vergleich kleiner) gefordert.
- Der Konstruktor erzeugt eine Liste fr maximal n Listenelemente. Der Destruktor gibt den
dynamisch allokierten Speicherplatz fr die Liste wieder frei.
- Die Elementfunktion size() fragt die aktuelle Anzahl der Listenelemente ab.
- isEmpty() und isFull() fragen den Leerzustand bzw. den Vollzustand der Liste ab.
- Die Operatorfunktion [] erlaubt einen indizierten Zugriff auf die existierenden Listenele-
mente. Bei fehlerhaftem Index wird eine InvalidIndex-Exception geworfen.
- pushBack() trgt das Listenelement el am Ende der Liste ein, insert() fgt das Listenele-
ment el vor dem Listenelement mit dem nchstgreren Schlssel ein. In beiden Fllen
wird bei voller Liste eine SequenceOverflow-Exception geworfen.
- Die Funktion remove() entfernt das Listenelement el.

Als konkretes Beispiel fr einen Nutzdatentyp dient im Folgenden der Typ Data.

Beispiel 10.1 (Forts.): (Allgemeiner Nutzdatentyp)
cl ass Dat a {
unsi gned key;
/ / . . . ggf wei t er e Dat en
publ i c:
Dat a( unsi gned k=0) : key( k) { }
bool oper at or ==( Dat a &d) { r et ur n key == d. key; }
bool oper at or <( Dat a &d) { r et ur n key < d. key; }
f r i end ost r eam& oper at or <<( ost r eam& s, Dat a &d) ;
};

106
Bemerkungen:
- Fr den vorliegenden Beispielfall ist es ausreichend, wenn der Typ Data als Datenkompo-
nente lediglich einen Sortierschlssel enthlt. Im konkreten Anwendungsfall wird Data
weitere Nutzdatenkomponenten enthalten. Es ist jedoch zu beachten, dass hierbei die
Wertzuweisung erhalten bleiben muss. Falls notwendig, muss ein Zuweisungsoperator
hinzugefgt werden.
- Als Schlssel dient ein Wert vom Typ unsigned. Dies ist auch fr einen konkreten An-
wendungsfall realistisch. Der Schlssel wird bei der Erzeugung des Data-Objekts festge-
legt.
- Die beiden Operatorfunktionen ermglichen den Vergleich von Data-Objekten auf Basis
der Schsselkomponente key.
- Der Ausgabeoperator ermglicht eine C++-konforme Ausgabe von Data-Objekten.



10.3 Implementierung

Als Grundlage fr die Implementierung der Liste bietet sich ein Array vom Nutzdatentyp an.
Die Klasse Sequence wird hierzu um die folgenden privaten Datenelemente ergnzt:

Beispiel 10.1 (Forts.): (Implementierung des ADT Sequence)

t empl at e<cl ass AD>
cl ass Sequence {
AD *seq; / / Spei cher pl at z
unsi gned max; / / maxi mal e Lnge
unsi gned l en; / / akt uel l e Lnge
/ / . .
};

Bemerkungen:
- Der Zeiger seq zeigt auf einen Array der Lnge max zur Aufnahme der Listenelemente.
- len gibt die Anzahl der aktuell gespeicherten Listenelemente an (siehe Abb. 10.1).


seq ...
...
0 1 2 len-1 len max-1
belegte Listenpltze freie Listenpltze


Abb. 10.1: Implementierung der sequenziell gespeicherten Liste


Erzeugung
Bei der Erzeugung eines Sequence-Objekts wird ein Array vom Template-Parametertyp AD
der Lnge n reserviert und ber seq adressiert. Die beiden Instanzvariablen max und len
werden initialisiert.

107
Beispiel 10.1 (Forts.): (Erzeugung)
t empl at e<cl ass AD>
Sequence<AD>: : Sequence( unsi gned n)
{
max = n;
seq = new AD[ max] ;
l en = 0;
}


Indizierter Zugriff
Die Operatorfunktion operator[] ermglicht einen indizierten Zugriff auf die Listenelemente.
Aufgrund der Referenzrckgabe kann sowohl lesend als auch schreibend auf die Listenele-
mente zugegriffen werden. Eine Indexangabe, welche die aktuelle Lnge der Liste bersteigt,
fhrt zu einer InvalidIndex-Exception.

Beispiel 10.1 (Forts.): (Indizierter Zugriff)
t empl at e<cl ass AD>
AD& Sequence<AD>: : oper at or [ ] ( unsi gned pos) t hr ow ( I nval i dI ndex)
{
i f ( pos >= l en)
t hr ow I nval i dI ndex( ) ;
el se
r et ur n seq[ pos] ;
}


Sortiertes Einfgen
Jede Einfgeoperation muss zunchst prfen, ob die Liste nicht bereits voll ist. Ggf. wird eine
SequenceOverflow-Exception geworfen. Nachdem die Einfgeposition gefunden wurde,
mssen alle nachfolgenden Elemente um eine Position nach hinten verschoben werden (siehe
Abb. 10.2).

seq ... ... ...
0 len-1 len max-1


Abb. 10.2: Einfgen eines Listenelements



108
Beispiel 10.1 (Forts.): (Sortiertes Einfgen)
t empl at e<cl ass AD>
voi d Sequence<AD>: : i nser t ( AD &el ) t hr ow ( SequenceOver f l ow)
{
unsi gned pos, i ;

i f ( i sFul l ( ) )
t hr ow SequenceOver f l ow( ) ;
f or ( pos=0; pos<l en; pos++) / / Ei nf geposi t i on suchen
i f ( el < seq[ pos] )
br eak;
f or ( i =l en; i >pos; i - - ) / / Ver schi eben
seq[ i ] = seq[ i - 1] ;
seq[ pos] = el ; / / Ei nf gen
l en++;
}


Entfernen
Falls das zu entfernende Listenelement gefunden wird, werden alle nachfolgenden Listen-
elemente um eine Position nach vorne geschoben und so das zu entfernende Listenelement
berschrieben (siehe Abb. 10.3).

seq ... ... ...
0 len-1 len max-1
zu entfernen


Abb. 10.3: Entfernen eines Listenelements


Beispiel 10.1 (Forts.): (Entfernen)
t empl at e<cl ass AD>
bool Sequence<AD>: : r emove( AD &el )
{ unsi gned i , j ;

f or ( i =0; i <l en; i ++) / / Lschposi t i on suchen
{ i f ( seq[ i ] == el )
{ f or ( j =i ; j <l en- 1; j ++) / / kompr i mi er en
seq[ j ] = seq[ j +1] ;
l en- - ;
r et ur n t r ue;
}
}
r et ur n f al se;
}



109
10.4 Suchen

Definition 10.2: (Suchen)
Suchen in einer linearen Liste bedeutet allgemein die Lokalisierung eines Listenelements
unter Vorgabe eines bestimmten Suchkriteriums. Ist das Suchkriterium derart, dass es das
gesuchte Datenobjekt eindeutig charakterisiert, spricht man von einem Suchschlssel.


Sequenzielle Suche
Bei der sequenziellen Suche, welche auch als lineare Suche bezeichnet wird, werden die
Listenelemente sukzessive im Hinblick auf bereinstimmung mit dem Suchschlssel ber-
prft. Wird ein solches Listenelement gefunden, ist die Suche beendet.

Der ADT Sequence wurde in Beispiel 10.2 um eine Elementfunktion seqSearch() erweitert,
welche eine sequenzielle Suche realisiert. Als Suchkriterium wird hier ein Musterobjekt
vom Template-Parametertyp AD bergeben, welches auf quivalenz mit den Listenelemen-
ten berprft wird. Durch die Verlagerung des Schlsselvergleichs auf die Listenelemente
lsst sich die Liste selbst unabhngig vom jeweiligen Typ des Suchschlssels implementie-
ren. Im Erfolgsfall wird ein Zeiger auf das gefundene Nutzdatenobjekt zurck geliefert, im
Fall der erfolglosen Suche ist das Ergebnis NULL.

Beispiel 10.2: (Sequenzielle Suche)
t empl at e<cl ass AD>
AD *Sequence<AD>: : seqSear ch( AD &el ) const
{ unsi gned i ;

f or ( i =0; i <l en; i ++)
{ i f ( seq[ i ] == el )
r et ur n &seq[ i ] ;
}
r et ur n NULL;
}


Binre Suche
Im Gegensatz zur sequenziellen Suche setzt die binre Suche eine nach Suchschlsseln ge-
ordnete Liste voraus. Der Algorithmus kann formal wie folgt beschrieben werden:

Gegeben sei eine sortierte Liste L=(k0,...,kn-1). Zu suchen ist ein Element mit Suchschlssel k.
(1) Falls L keine Elemente enthlt, ist die Suche erfolglos.
(2) Bestimme Listenposition m = n/2 und vergleiche km mit dem Schlssel k
(a) Falls km == k, Das gesuchte Datenobjekt befindet sich an Listenposition m.
(b) Falls km > k, Wiederhole binre Suche in der Teilliste (k0,...,km-1).
(c) Falls km < k, Wiederhole binre Suche in der Teilliste (km+1,...,kn-1).

Das folgende Beispiel zeigt den Ablauf der binren Suche in einer Liste mit den Schlsseln:
1, 3, 4, 5, 6, 8, 9. Gesucht wird das Listenelement mit Schlssel 6.

110
Beispiel:

1. Schritt 2. Schritt 3. Schritt
0.
1.
2.
3.
4.
5.
6.
1
3
4
5 < 6
6
8
9




6
8 > 6
9






6 == 6


Der ADT Sequence aus Beispiel 10.2 enthlt eine Elementfunktion binSearch(), welche eine
binre Suche realisiert. Analog zur sequenziellen Suche wird das Suchkriterium in Form ei-
nes Musterobjekts vom Template-Parametertyp AD bergeben. Im Erfolgsfall wird ein
Zeiger auf das Nutzdatenobjekt zurck geliefert, ansonsten der Wert NULL. Die nachfolgend
dargestellte Implementierung geht von der Verfgbarkeit eines Vergleichsoperators < klei-
ner auf den Listenelementen aus.


Beispiel 10.2 (Forts.): (Binre Suche)
t empl at e<cl ass AD>
AD *Sequence<AD>: : bi nSear ch( AD &el ) const
{
i nt l =0, r =l en- 1, m;

whi l e ( l <= r )
{
m= ( l +r ) / 2;
i f ( seq[ m] == el )
r et ur n &seq[ m] ;
el se i f ( seq[ m] < el )
l = m+1;
el se i f ( el < seq[ m] )
r = m- 1;
}
r et ur n NULL;
}

111
10.4.1 Komplexitt von Algorithmen

Der Zeitaufwand von Algorithmen lsst sich nicht allgemein bestimmen. Zu viele Faktoren
(Hardware, parallel laufende Programme, Eingabereihenfolge usw.) spielen eine Rolle. In der
Praxis interessiert deshalb vielmehr das Laufzeitverhalten bei wachsender Problemgre.
Diese Problemgre ist mageblich gekennzeichnet durch die Anzahl der zu verarbeitenden
Datenobjekte bzw. der auf diesen Datenobjekten durchzufhrenden Elementaroperationen
(z.B. Gleitkommaoperationen bei einer Matrixmultiplikation, Vergleichsoperationen bei der
Suche).


O-Notation
Eine gngige Praxis ist es, die mit der Objektzahl wachsende Komplexitt eines Algorithmus
als eine grobe Nherung an eine bekannte Funktion anzugeben. Hierfr wird meist die soge-
nannte O-Notation verwendet:

Seien f : und g : , dann gilt: f(n) = (g(n)) falls n0 e und c > 0 exis-
tieren, sodass fr alle n n0 gilt: f(n) c * g(n)

Die O-Notation gibt eine obere Schranke zur Laufzeit eines Algorithmus an, welche ab einem
bestimmten n0 selbst im ungnstigsten Fall nicht berschritten wird.

Beispiel:


Abb. 10.4: f1(n) = 2n
2
+3log2n+15 = O(n
2
)

Ab dem Wert 7 bildet die Funktion f2(n) = 2.5n
2
eine obere Schranke fr die Funktion
f1(n)= 2n
2
+3log2n+15; d.h es gilt: f1(n) f2(n) = 2,5n
2
. Also gilt nach obiger Definition:
f1(n) = O(n
2
).

112
Wie man sieht, konzentriert sich die O-Analyse auf den wachstumstrksten Term und ver-
nachlssigt additive Terme mit geringerem Wachstum. Auerdem eliminiert sie konstante
Faktoren, was eine Abstraktion vom konkreten Rechenumfeld (z.B. Rechenleistung) bedeu-
tet.

In der Praxis sind fr die Abschtzung der Komplexitt von Algorithmen lediglich die in
Abb. 10.5 dargestellten Komplexittsklassen von Bedeutung.

n
log(n)
n log(n)
n
2
2
n
0
5
10
15
20
25
30
35
40
1 2 3 4 5 6 7 8 9 10


Abb. 10.5: Praktisch relevante Komplexittsklassen


10.4.2 Komplexitt der Suchalgorithmen

Als Ma fr die Komplexitt von Suchalgorithmen gilt blicherweise die Anzahl der Ver-
gleichsoperationen.

Bei der sequenziellen Suche sind im statistischen Mittel n/2 Vergleichsoperationen erforder-
lich. Im worst case wird nach n Schritten festgestellt, dass das gesuchte Element nicht ge-
funden wird. Die sequenzielle Suche hat daher die Komplexitt O(n).

Im Falle der binren Suche entspricht die Anzahl der Vergleiche im Wesentlichen der Anzahl
der Listenteilungen.
- Nach der ersten Teilung hat die resultierende Liste die Lnge n/2
1
.
- Nach der zweiten Teilung hat die resultierende Liste die Lnge n/2
2
.
- Nach der k-ten Teilung hat die resultierende Liste die Lnge n/2
k
.
Im worst case terminiert der Algorithmus mit einer Listenlnge 1. In diesem Falle gilt:
n/2
k
= 1. Lst man diese Gleichung nach k auf, so gilt: k = log2(n). Die Komplexitt der bin-
ren Suche betrgt somit O(log2(n)).

113
10.5 Sortieren

Wie das Beispiel der Suche zeigt, ist die Sortiertheit einer Liste eine hufig angestrebte Ei-
genschaft. Mathematisch lsst sich die Sortiertheit wie folgt definieren:

Eine lineare Liste L = (k0,...,kn-1) mit Objektmenge O = {k0,...,kn-1} ist sortiert, falls eine
Ordnungsrelation R auf OxO definiert ist, und fr je zwei Elemente ki,kj gilt: (ki, kj) e R
<=> i < j.


Beispiele fr Ordnungsrelationen sind < bei ganzzahligem Schlssel oder die alphabetische
Ordnung bei alphanumerischem Schssel.

Die Informatik hat eine Reihe von Sortierverfahren hervorgebracht, welche sich in der Lauf-
zeit und den Speicherplatzanforderungen unterscheiden. Bezglich ihrer grundstzlichen
Vorgehensweise lassen sich die Sortierverfahren in folgende Kategorien einteilen:
- Sortieren durch Auswahl (Selection Sort)
- Sortieren durch Einfgen (Insertion Sort)
- Sortieren durch Austauschen (Exchange Sort).

Das nachfolgend vorgestellte Sortierverfahren namens Quicksort gehrt zu der letzgenannten
Gruppe.



10.5. 1 Quicksort

Es existiert eine Reihe von bekannten Sortierverfahren, welche die Sortierung durch gezielten
Austausch von Listenelementen herbeifhren. Der von C.A.R. Hoare stammende Quicksort -
Algorithmus galt lange Zeit als der schnellste Sortieralgorithmus dieser Art. Er basiert - hn-
lich wie die binre Suche - auf der Divide and Conquer Strategie.

Die Grundidee von Quicksort besteht darin, ein willkrlich gewhltes Listenelement, das
sogenannte Pivotelement so zu positionieren, dass
(1) es sich an der korrekten Position der sortierten Liste befindet,
(2) alle Elemente in der Teilliste vor dem Pivotelement kleiner als das Pivotelement sind,
(3) alle Elemente in der Teilliste hinter dem Pivotelement grer als das Listenelement sind.
Die noch unsortierten Teillisten werden nachfolgend nach dem Quicksort-Verfahren sortiert.

Die beschriebene Grundidee wird an folgendem Beispiel verdeutlicht. Als Pivotelement wird
hier das Element auf der letzten Listenposition gewhlt.

114
Beispiel:

Sortierschritte
0. 9 3 3 1 1 1 1 1
1. 5 5 1 3 3 3 3 3
2. 1 1 4 4 4 4 4 4
3. 8 4 5 5 5 5 5 5
4. 4 6 6 6 6 6 6 6
5. 3 9 9 9 9 9 8 8
6. 6 8 8 8 8 8 9 9


Das Zentrieren des Pivotelements geschieht nach folgendem Algorithmus:
(1) Durchsuche die Liste von links (Laufindex li), bis ein Listenelement gefunden wird, wel-
ches grer ist, als das Pivotelement.
(2) Durchsuche die Liste von rechts (Laufindex re), bis ein Listenelement gefunden wird,
welches kleiner ist, als das Pivotelement.
(3) Vertausche die gefundenen Listenelemente.
(4) Wiederhole die Schritte (1)-(3) so lange, bis die li >= re.
(5) Vertausche das Pivotelement mit dem Element an Position li.

Das nachfolgende Diagramm demonstriert das Zentrieren des ersten Pivotelements mit Wert
6 aus obigem Beispiel:

Beispiel

0. li 9 3 3 3 3 3
1. 5 5 5 5 5 5
2. 1 1 1 1 1 1
3. 8 8 li 8 4 4 4
4. 4 4 re 4 8 li, re 8 6
5. re 3 9 9 9 9 9
6. 6 6 6 6 6 8


Das folgende Programmbeispiel zeigt eine Implementierung des Quicksort-Algorithmus im
Form einer Elementfunktion des Klassen-Template Sequence.



115
Beispiel 10.3: (Quicksort)
t empl at e<cl ass AD>
voi d Sequence<AD>: : qui ckSor t ( i nt l ef t , i nt r i ght )
{
i f ( l ef t < r i ght )
{
i nt l i =l ef t - 1, r e=r i ght , pi vot =r i ght ;
f or ( ; ; )
{
whi l e( seq[ ++l i ] < seq[ pi vot ] ) ;
whi l e( l i < r e && seq[ pi vot ] < seq[ - - r e] ) ;
i f ( l i >= r e)
br eak;
swapI t ( seq[ l i ] , seq[ r e] ) ; / / Ver t auschung
}
swapI t ( seq[ l i ] , seq[ pi vot ] ) ; / / Posi t i oni er en des Pi voel ement s

i f ( l ef t < l i - 1)
qui ckSor t ( l ef t , l i - 1) ; / / Sor t i er en l i nke Tei l l i st e
i f ( r e+1 < r i ght )
qui ckSor t ( r e+1, r i ght ) ; / / Sor t i er en r echt e Tei l l i st e
}
}

Komplexittsbetrachtung
Zur Aufwandsabschtzung soll der gnstigste und der ungnstigste Fall betrachtet werden:

- Im gnstigsten Fall wird das Pivot-Element immer so gewhlt, dass die Teillisten der klei-
neren und der greren Elemente gleich gro sind. Nach log2(n) Schritten ist eine Listen-
lnge von 1 erreicht. Der Aufwand fr die Partitionierung ist proportional zur Listenlnge
n. Hieraus ergibt sich im best case ein Aufwand O(n*log2(n)).

- Im ungnstigsten Fall wird das Pivotelement immer so gewhlt, dass es am Listenrand
liegt. Die verbleibende Liste verkrzt sich dabei nur um ein Element. Es sind daher n-1
Schritte erforderlich, bis eine Listenlnge von 1 erreicht ist. Zusammen mit dem Aufwand
fr die Partitionierung ergibt sich ein Aufwand O(n
2
).



116
10.6 bungen

Aufgabe 10.1
(a) Erweitern Sie das Klassen-Template Sequence aus Beispiel 10.3 um folgende Element-
funktion:
t empl at e<cl ass AD>
voi d Sequence<AD>: : r esi ze( unsi gned n) ;
resize() verlngert den Speicherplatz in einem Sequence-Objekt um n Listenpltze. Der
Inhalt der bereits existierenden Listenelemente bleibt erhalten.
(b) Verndern Sie die Methoden pushBack() und insert() derart, dass im Falle eines Listen-
berlaufs eine Verlngerung der Liste um max/2 Listenpltze stattfindet.


Aufgabe 10.2
Verwenden Sie das Sequence-Template aus Beispiel 10.3 zur Abspeicherung der folgenden
Liste von Telefonkontakten.
11111 Huber
22222 Mayer
33333 Muel l er
44444 Schmi t z
55555 Schul z
- Ein Telefonkontakt enthlt die Tel.-Nr. und den Namen des Telefonpartners
- Die Telefonkontakte sollen zunchst unsortiert eingetragen und ausgeben werden.
- Nach einer Sortierung der Liste nach aufsteigenden Telefon-Nummern mittels der quicks-
ort()-Methode soll die Liste erneut ausgegeben werden.
- Schlielich soll nach den Kontakdaten einer vom Benutzer eingegebnenen Telefon-Nr.
gesucht werden. Im Erfolgsfall sollen die gefundenn Kontaktdaten auf dem Bildschirm
ausgegeben werden.

Hinweis:
- Realisieren Sie zunchst eine geeignete Klasse Kontakt zur Aufnahme der Nutzdaten.
- Gehen Sie davon aus, dass die Namen nicht lnger als 40 Zeichen sind. D.h. die Namen
knnen in einem 40 Zeichen langen char-Array unmittelbar im Objektbereich abgelegt
werden.

117
Aufgabe 10.3
Sortieren Sie die folgende Schsselfolge nach dem Verfahren Quicksort. Markieren Sie alle
verwendeten Pivot-Elemente und tragen Sie die Liste nach jeder Vertauschung jeweils in eine
neue Spalte ein.

Sortierschritte
0. 23 2
1. 7 7
2. 2 12
3. 21 16
4. 16 17
5. 25 21
6. 12 23
7. 17 25


Aufgabe 10.4 (Allgemeine Fragen)
(a) Unter welcher Bedingung kann eine sequenzielle Suche in einer linearen Liste kosten-
gnstiger sein als eine binre Suche?
(b) Geben Sie die maximale Suchschrittzahl (Anzahl Vergleiche) bei der erfolglosen Suche
in einer Liste der Lnge 8 bei binrer Suche an.
(c) Nennen Sie zwei Voraussetzungen fr eine binre Suche?
(d) Wann ist das Sortierverfahren Quicksort besonders effizient?

118
11. Verkettet gespeicherte lineare Listen

Das Einfgen und Lschen eines Elements in einer sequenziell gespeicherten linearen Liste
ist aufgrund der notwendigen Verschiebeoperationen mit hohem Aufwand verbunden. Die
sequenzielle Speicherung eignet sich daher besonders fr Listen mit geringer nderungshu-
figkeit. Fr Listen mit hufigen nderungen kann eine verkettete Speicherung von Vorteil
sein.

Eine lineare Liste ist verkettet gespeichert, wenn jedes Element einen Verweis auf das
nachfolgende Listenelement enthlt. Ein Listenanker verweist auf das erste Listenelement.
Der Verweis im letzten Listenelement kennzeichnet dieses als das letzte Listenelement.


11.1 Speicherung in einem Array

Eine mgliche Implementierungsbasis fr verkettet gespeicherte Listen stellen Arrays dar.
Jedes Array-Element besteht aus einer Struktur, welche als Komponenten das eigentliche
Nutzdatum und einen Index auf das logisch folgende Listenelement enthlt. Das folgende
Beispiel zeigt eine Liste von int-Werten, welche verkettet in einem Array gespeichert sind.

(1) Definition der Liste

#def i ne LEN 6

t ypedef st r uct {
Dat a el ;
i nt next ;
} Li st El ;

Li st El l i st e[ LEN] ; / / Vekt or mi t Li st enel ement en
i nt f i r st = - 1; / / Li st enanker


(2) Beispielliste first = 3

el next
0. 7 2
1. 5 0
2. 9 -1
3. 1 5
4. 4 1
5. 3 4


119
Die beschriebene Implementierung ist mit zwei Nachteilen behaftet:
(1) Die Lnge des Arrays ist fest.
(2) Nicht belegte Array-Elemente mssen verwaltet werden.


Die im nchsten Abschnitt beschriebene Speicherung in dynamischen Variablen vermeidet
diese Nachteile.


11.2 Speicherung in dynamischen Variablen

Bei der verketteten Speicherung in dynamischen Variablen wird bei Bedarf Speicherplatz fr
ein Listenelement im Freispeicher reserviert. D.h. die Listenelemente liegen nicht konsekutiv
im Speicher. Die Verkettung wird durch Zeiger auf das jeweils nachfolgende Element reali-
siert.


11.2.1 Abstraktion

Der ADT fr eine verkettet gespeicherte Liste unterscheidet sich nur geringfgig von dem der
sequenziell gespeicherten Liste aus Beispiel 10.1.

Beispiel 11.1: (ADT List)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass Li st {
publ i c:
Li st ( ) ;
~Li st ( voi d) ;
unsi gned si ze( voi d) const ;
bool i sEmpt y( voi d) const ;
AD *seqSear ch( AD &el ) const ;
voi d i nser t ( AD &el ) ;
bool r emove( AD &el ) ;
};

Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der in der Liste verwal-
teten Nutzdaten.
- Fr den Typ AD wird eine Wertzuweisung sowie die Operatoren == (quivalenz) und <
(Vergleich kleiner) gefordert.
- Der Konstruktor erzeugt eine leere Liste. Der Destruktor gibt den Speicherplatz fr die
Listenelemente wieder frei.
- Die Elementfunktion size() fragt die aktuelle Anzahl der Listenelemente ab.
- isEmpty() fragt den Leerzustand der Liste ab.
- Die Funktion seqSearch() sucht nach einem Listenelement, dessen Schlssel mit dem von
el identisch ist. Im Erfolgsfall wird ein Zeiger auf das gesuchte Listenelement zurck ge-
liefert; im Falle der erfolglosen Suche liefert seqSearch() den Zeiger NULL zurck.
- insert() fgt das Listenelement vor dem Listenelement mit dem nchstgreren Schlssel
ein.
- Die Funktion remove() entfernt das Listenelement el.

120
11.2.2 Implementierung

Die Implementierung deklariert den Typ fr die dynamisch allokierten Listenelemente und
definiert den Listenanker. Das Klassen-Template List wird hierzu um die folgenden privaten
Datenelemente ergnzt:

Beispiel 10.1 (Forts.): (Implementierung des ADT Sequence)
t empl at e <cl ass AD>
cl ass Li st {
st r uct Li st El { / / Li st enel ement
AD el ; / / Nut zdat en
Li st El * next ; / / Nachf ol gezei ger
} *f i r st ; / / Zei ger auf er st es und l et zt es Li st enel ement
unsi gned l en; / / akt uel l e Lnge
/ / . .
};

Bemerkungen:
- Die Listenelemente vom Typ ListEl enthalten die eigentlichen Nutzdaten (Typ AD) sowie
einen Zeiger auf das nachfolgende Listenelement. Das letzte Listenelement ist durch den
Wert NULL im Nachfolgezeiger als solches gekennzeichnet (siehe Abb 11.1).
- Der Zeiger first zeigt auf das erste Listenelement.

first


Abb. 11.1: Implementierung einer einfach verketteten Liste


Konstruktion und Destruktion
Bei der Erzeugung eines List-Objekts wird der Zeiger first mit NULL (leere Liste) initiali-
siert und der Elementzhler len auf 0 gesetzt. Der Destruktor muss ber die Liste iterieren
und die Listenelemente einzeln lschen.

Beispiel 11.1 (Forts.): (Konstruktion und Destruktion)
t empl at e<cl ass AD>
Li st <AD>: : Li st ( voi d)
{
f i r st = NULL;
l en = 0;
}

template<class AD>
inline List<AD>::~List(void)
{ ListEl *curr;

while (first)
{ curr = first;
first = curr->next;
delete curr;
}
}


121
Sortierendes Einfgen
Da verkettet gespeicherte, lineare Listen nur schwer zu sortieren sind, ist es vielfach erforder-
lich, die Liste bereits sortiert aufzubauen.
- Nach Allokation eines Listenelements vom Typ ListEl werden die Nutzdaten in el abge-
legt.
- Im allgemeinen Fall muss die Einfgeposition bestimmt werden. Hierzu wird das Element
mit dem nchst hheren Sortierschlssel (Nachfolgerelement) und dessen Vorgngerele-
ment gesucht.
- Der Nachfolgezeiger des Vorgngerelements wird zum Nachfolgezeiger des neuen Listen-
elements. Das Vorgngerelement wird auf das neue Element verzeigert (siehe Abb 11.2 (c)
und (d)).
- Das Einfgen eines Elements in eine noch leere Liste sowie das Einfgen am Listenanfang
sind als Sonderflle zu bercksichtigen, da in diesen Fllen der Listenanker verndert wer-
den muss (siehe Abb. 11.2 (a) und (b)).


(d) Einfgen am Ende
2
1
(a) Einfgen des einzigen Listenelements
first
2
1
(b) Einfgen am Anfang
first
2
1
(c) Einfgen eines laufenden Elements


Abb. 11.2: Einfgen eines Listenelements


Beispiel 11.1 (Forts.): (Sortierendes Einfgen)
voi d Li st <AD>: : i nser t ( AD &el )
{
Li st El *newLi nk = new Li st El ; / / neues Ver ket t ungsel ement er zeugen
newLi nk- >el = el ; / / Nut zdat en abl egen

i f ( f i r st == NULL | | el < f i r st - >el ) / / Ei nket t en amAnf ang
{ newLi nk- >next = f i r st ;
f i r st = newLi nk;
}
el se / / Ei nket t en i n der Mi t t e und amEnde
{ Li st El *pr ed = f i r st , *cur r = f i r st - >next ;
whi l e ( cur r && cur r - >el < el )
{ pr ed = cur r ;
cur r = cur r - >next ;
}
newLi nk- >next = pr ed- >next ;
pr ed- >next = newLi nk;
}
l en++;
}

122
Entfernen
- Das auszukettende Element und dessen Vorgngerelement werden gesucht. Der Nachfol-
gezeiger des auszukettenden Elements wird zum Nachfolgezeiger des Vorgngerelements
(siehe Abb. 11.3 (c) und (d)).
- Das Ausketten am Listenanfang stellt einen Sonderfall dar. Hierbei muss der Zeiger first
korrigiert werden (siehe Abb 11.3 (b)). Dies gilt ebenso fr den Fall, dass das einzige Lis-
tenelement ausgekettet wird (siehe Abb. 11.3 (a)).
- Schlielich wird der Speicherplatz fr das zu entfernende Listenelement wieder frei gege-
ben.


(d) Entfernen am Ende
(a) Entfernen des einzigen Elements
first
(b) Entfernen am Anfang
first
(c) Entfernen eines laufenden Elements


Abb. 11.3: Entfernen eines Listenelements


Beispiel 11.1 (Forts.): (Entfernen)
t empl at e<cl ass AD>
bool Li st <AD>: : r emove( AD& el )
{ Li st El *pr ed, *cur r ;

i f ( f i r st == NULL) / / Ausket t en aus l eer er Li st e
r et ur n f al se;
i f ( f i r st - >el == el ) / / Ausket t en an Anf ang
{ cur r = f i r st ;
f i r st = cur r - >next ;
}
el se / / Ausket t en i n der Mi t t e und amEnde
{ pr ed = f i r st ;
cur r = f i r st - >next ;
whi l e ( cur r && ! ( cur r - >el == el ) )
{ pr ed = cur r ;
cur r = cur r - >next ;
}
i f ( cur r == NULL) / / el ni cht gef unden
r et ur n f al se;
el se
pr ed- >next = cur r - >next ;
}
del et e cur r ; / / Ver ket t ungsel ement l schen
l en- - ;
r et ur n t r ue;
}

123
Suchen
In einer verkettet gespeicherten Liste ist lediglich eine sequenzielle Suche mglich.

Beispiel 11.1 (Forts.): (Suchen)
t empl at e<cl ass AD>
AD *Li st <AD>: : seqSear ch( AD &el ) const
{ Li st El *cur r ;

f or ( cur r = f i r st ; cur r ; cur r = cur r - >next )
i f ( cur r - >el == el )
r et ur n &( cur r - >el ) ;
r et ur n NULL; / / ni cht gef unden
}


11.3 Doppelt verkettete Listen

In einigen Anwendungen ist es vorteilhaft, wenn eine Liste auch rckwrts durchlaufen wer-
den kann. Diese Mglichkeit bieten doppelt verkettet Listen.

Eine Liste ist doppelt verkettet gespeichert, wenn
- jedes Element einen Verweis auf sein Nachfolgerelement enthlt und
- jedes Element einen Verweis auf sein Vorgngerelement enthlt.
Ein Listenanker (first) zeigt auf das erste Element. Ein Listenanker (last) zeigt auf das letz-
te Element.

Wie einfach verkettete, lineare Listen knnen doppelt verkettete, lineare Listen in einem Ar-
ray oder in dynamischen Variablen gespeichert werden. Neben den eigentlichen Nutzdaten
enthlt ein Listenelement einen Zeiger auf das Nachfolgelement und einen Zeiger auf das
Vorgngerelement (siehe Abb. 11.4).


last
first


Abb 11.4: Doppelt verkettete Liste

Bei doppelt verketteten Listen lassen sich neue Listenelemente sowohl am Anfang als auch
am Ende der Liste mit wenig Aufwand ein- und ausketten. Beim Einfgen und Entfernen
eines Elements sind die betroffenen Nachbarelemente ber die Vorwrts- und Rckwrtsver-
kettung direkt bekannt. Auf die konkrete Implementierung der Operationen zum Einfgen
und Lschen eines Listenelements soll hier nicht weiter eingegangen werden.

124
11.4 bungsaufgaben

Aufgabe 11.1
Falls eine verkettet gespeicherte Liste nicht sortiert aufgebaut werden soll, ist es besonders
einfach, neue Listenelemente am Anfang der Liste einzuketten. Ergnzen Sie hierzu das
Klassen-Template aus Beispiel 11.1 um eine Elementfunktion pushFront().


Aufgabe 11.2
Ergnzen Sie das Klassen-Template List um ein Iterator-Template, welches etwa wie folgt
verwendet werden kann:

Beispiel:
t ypedef Li st <Dat a> Dat aLi st ;

i nt mai n( )
{
Dat aLi st dat aLi st ;
Dat a *dat a;

/ / Li st e f l l en

Li st I t er at or <Dat a> i t ( dat aLi st ) ;

whi l e ( dat a = i t . next ( ) )
cout << *dat a << " \ n" ;
}

Hinweis:
- Bei Verwendung von qualifizierten Typnamen, welche einen Template-Parameter enthal-
ten, muss dem Typnamen das Attribut typename vorangestellt werden.
Beispiel: t ypename Li st <AD>: : Li st El *cur r ;


Aufgabe 11.3 (Allgemeine Fragen)
(a) Nennen Sie einen Fall, in dem eine verkettet gespeicherte Liste einer sequenziell gespei-
cherten Liste vorzuziehen ist.
(b) Nennen Sie einen Fall, in dem eine sequenziell gespeicherte Liste einer verkettet gespei-
cherten Liste vorzuziehen ist.
(c) Bentigt eine verkettet gespeicherte Liste in jedem Fall mehr Speicherplatz als eine se-
quenziell gespeicherte Liste?

125
12. Spezielle Listen

In manchen Anwendungsfllen beschrnkt sich die Manipulation einer Liste auf die Elemen-
te, welche sich am Anfang oder am Ende befinden. Bei den nachfolgend dargestellten Stapeln
und Schlangen handelt es sich um Listen mit speziellen Zugriffsgegebenheiten.


12.1 Stapel

Ein Stapel lsst sich veranschaulichen durch einen Stapel von Bchern. Nur das oberste Buch
kann problemlos entfernt werden. Neue Bcher lassen sich nur oben leicht hinzufgen.

Ein Stapel ist eine lineare Liste, bei der Elemente nur am Listenende hinzugefgt und
entnommen werden knnen. Das zuletzt gespeicherte Element wird zuerst wieder entnom-
men. Dieses Verhalten bezeichnet man als LIFO-Prinzip (Last In First Out).


Bezeichnungen: (siehe auch Abb. 12.1)
- Ein Stapel wird hufig auch als Stack oder Kellerspeicher bezeichnet.
- Das Hinzufgen eines Elements heit push-Operation.
- Das Entnehmen eines Elements heit pop-Operation.
- Das Ende des Stapels, an dem hinzugefgt und entnommen wird, heit top of stack.


push( )
top of stack
pop( )


Abb. 12.1: Stapel


12.1.1 Abstraktion

Beispiel 12.1: (ADT Stack)
t empl at e<cl ass AD> / / AD Typ f r St ack- El ement
cl ass St ack {
publ i c:
St ack( unsi gned n) ;
~St ack( voi d) ;
unsi gned si ze( voi d) const ;
bool i sEmpt y( voi d) const ;
bool i sFul l ( voi d) const ;
voi d push( AD &el ) t hr ow( St ackOver f l ow) ;
AD pop( voi d) t hr ow( St ackUnder f l ow) ;
};

126
Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der Nutzdaten.
- Fr den Typ AD werden die Wertzuweisung sowie die Kopierfhigkeit gefordert.
- Der Konstruktor erzeugt einen leeren Stack. Der Destruktor gibt den vom Stack belegten
Speicherplatz frei.
- Die Elementfunktion size() fragt die aktuelle Anzahl der Stack-Elemente ab.
- isEmpty() und isFull() fragen den Leerzustand bzw. den Vollzustand des Stapels ab.
- Die Funktion push() legt das Nutzdatum el auf dem Stapel ab. Im Vollzustand wirft
push() eine StackOverflow-Exception.
- pop() liefert das zuletzt mittels push() abgelegte Nutzdatum zurck. Es gilt also
pop( push( el em) ) == el em
Im Leerzustand wirft pop() eine StackUnderflow-Exception.


12.1.2 Implementierung

Die Implementierung ist sowohl in Form einer sequenziell gespeicherten Liste als auch in
verketteter Form mglich. Nachfolgend wird eine Implementierung mit sequenzieller Spei-
cherung beschrieben.

Beispiel 12.1 (Forts.): (Stack)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass St ack {
AD *st ack; / / Spei cher pl at z
unsi gned max; / / maxi mal e Lnge
unsi gned t os; / / St ackzei ger
/ / . . .
};

Bemerkung:
- stack zeigt auf einen Speicherbereich zur Aufnahme von max Elementen vom Typ AD.
- Der Top-of-Stack-Zeiger tos enthlt zu jedem Zeitpunkt den Index des stack-Elements,
welches das nchste einzufgende Stackelement aufnimmt.


Einfgen eines Elements (push)
- Das einzufgende Element wird im Vektorelement mit Index tos abgespeichert.
- tos wird um 1 erhht.


push( 12) ;
tos
0
1
51
tos
0
1
51
2
12


Abb. 12.2: push-Operation

127
Beispiel 12.1 (Forts.): (push-Operation)
t empl at e<cl ass AD>
voi d St ack<AD>: : push( AD &el ) t hr ow( St ackOver f l ow)
{
i f ( i sFul l ( ) )
t hr ow St ackOver f l ow( ) ;
st ack[ t os++] = el ;
}


Entfernen eines Elements (pop)
- tos wird um 1 erniedrigt.
- Der Wert des Stackelements mit Index tos wird als Funktionswert von pop() zurck-
geliefert.

el em= pop( ) ; / / el em= 12
tos
0
1
51
tos
0
1
51
2
12


Abb. 12.3: pop-Operation


Beispiel 12.1 (Forts.): (pop-Operation)
t empl at e<cl ass AD>
AD St ack<AD>: : pop( voi d) t hr ow( St ackUnder f l ow)
{
i f ( i sEmpt y( ) )
t hr ow St ackUnder f l ow( ) ;
r et ur n st ack[ - - t os] ;
}

Verwendung von Stapeln
Stapel finden in der Programmierung vielfltig Anwendung:

(1) Der Operandenspeicher eines Taschenrechners in UPN-Notation ist als Stapel organi-
siert.
(2) Compiler verwenden Stapel bei der Auswertung von arithmetischen Ausdrcken (z.B.
a*6+b/7). Hierbei werden sowohl die Operanden als auch die Operatoren in Stapeln ge-
speichert.
(3) Der Speicherbereich eines C-Programms zur Aufnahme der automatischen Variablen ist
als Stapel organisiert. Gem der Aufrufverschachtelung der Funktionen werden die
Stackrahmen mit den automatischen Variablen der Funktionen im Stackspeicher abge-
legt. Zwecks effizienter Implementierung verfgen viele Rechner ber spezielle Register
sowie Push- und Pop-Befehle auf der Ebene der Maschinensprache.

128
12.2 Schlangen

Eine Schlange lsst sich anhand einer Warteschlange vor einer Kaufhauskasse veranschauli-
chen. Der am Anfang der Schlange befindliche Kunde verlsst als erster die Warteschlange.
Neu hinzukommende Kunden schlieen sich am Ende der Warteschlange an.

Eine Schlange ist eine lineare Liste, an deren Anfang Elemente entnommen und an deren
Ende Elemente hinzugefgt werden. Bei einer Schlange werden die Elemente in der Rei-
henfolge entnommen, in der sie eingefgt wurden (FIFO-Prinzip, First In First Out).

Bezeichnungen: (siehe Abb. 12.4)
- Ein Schlange wird auch als Queue bezeichnet.
- Das Hinzufgen eines Elements heit put-Operation.
- Das Entnehmen eines Elements heit get-Operation.
- Das Element am Anfang einer Schlange bezeichnet man als Kopf (head).
- Das Element am Ende einer Schlange bezeichnet man als Schwanz (tail).

.......
get( ) put( )
head
tail


Abb. 12.4: Schlange


12.2.1 Abstraktion

Beispiel 12.2: (ADT Queue)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass Queue {
publ i c:
Queue( unsi gned n) ;
~Queue( voi d) ;
unsi gned si ze( voi d) const ;
bool i sEmpt y( voi d) const ;
bool i sFul l ( voi d) const ;
voi d put ( AD &el ) t hr ow( QueueOver f l ow) ;
AD get ( voi d) t hr ow( QueueUnder f l ow) ;
};


Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der Nutzdaten.
- Fr den Typ AD werden die Wertzuweisung sowie die Kopierfhigkeit gefordert.
- Der Konstruktor erzeugt einee leere Schlange. Der Destruktor gibt den von der Schlange
belegten Speicherplatz frei.
- Die Elementfunktion size() fragt die aktuelle Anzahl der Queue-Elemente ab.
- isEmpty() und isFull() fragen den Leerzustand bzw. den Vollzustand der Schlange ab.

129
- Die Funktion put() fgt das Nutzdatum el am Ende der Queue an. Im Vollzustand wirft
put() eine QueueOverflow-Exception.
- get() liefert das Nutzdatum am Kopf der Schlange zurck. Im Leerzustand wirft get() eine
QueueUnderflow-Exception.


12.2.2 Implementierung

Wie im Falle der Stapel ist auch bei Schlangen eine Implementierung in Form einer sequen-
ziell gespeicherten Liste oder als verkettete Liste mglich. Nachfolgend wird eine Implemen-
tierung mit sequenzieller Speicherung beschrieben.


Beispiel 12.2 (Forts.): (Queue)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass Queue {
AD *queue; / / Spei cher pl at z
unsi gned max; / / maxi mal e Lnge
unsi gned l en; / / akt uel l e Lnge
unsi gned head, t ai l ; / / Queue- Zei ger
/ / . . .
};

Bemerkungen:
- Die Schlange kann als Ringspeicher realisiert werden (siehe Abb. 12.5). Wenn Kopf- bzw.
Schwanzzeiger das Ende des Vektors erreicht haben, kehren sie an den Anfang des Vek-
tors zurck.
- Der Kopfzeiger enthlt den Index des als nchstes zu entnehmenden Elements.
- Der Schwanzzeiger enthlt den Index des Elements, welches als nchstes gefllt wird.

0
1
n-1
.
.
.
.
.
2
3
n-2
.
.
.
tail
head
gefllt


Abb. 12.5: Implementierung einer Schlange als Ringspeicher

130
Einfgen eines Elements (put)
- Das Element wird in queue[tail] eingetragen.
- tail wird zyklisch inkrementiert: tail = (tail + 1) % max;
- len wird inkrementiert.
- Im Vollzustand wird eine QueueOverflow-Exception geworfen.


Beispiel 12.2 (Forts.): (put-Operation)
t empl at e<cl ass AD>
voi d Queue<AD>: : put ( AD &el ) t hr ow( QueueOver f l ow)
{
i f ( i sFul l ( ) )
t hr ow QueueOver f l ow( ) ;
queue[ t ai l ] = el ;
t ai l = ( t ai l +1) %max;
l en++;
}


Entfernen eines Elements (get)
- Das Element wird aus queue[head] entnommen.
- head wird zyklisch inkrementiert: head = (head + 1) % max;
- len wird dekrementiert.
- Im Leerzustand wird eine QueueUnderflow-Exception geworfen.


Beispiel 12.2 (Forts.): (get-Operation)
t empl at e<cl ass AD>
AD Queue<AD>: : get ( voi d) t hr ow( QueueUnder f l ow)
{
unsi gned ol dHead = head;

i f ( i sEmpt y( ) )
t hr ow QueueUnder f l ow( ) ;
head = ( head+1) %max;
l en- - ;
r et ur n queue[ ol dHead] ;
}


Verwendung von Schlangen
Schlangen finden hufig in der Systemprogrammierung oder der diskreten Simulationstech-
nik Verwendung. Hierbei ist die Zielsetzung meist die Realisierung von Warteschlangen.
(1) Bei Multitasking-Betriebssystemen werden nicht-aktive Prozesse oder anstehende
Druckauftrge in Warteschlangen verwaltet.
(2) Zeichenpuffer zur Ein-/Ausgabe (z.B. Tastaturpuffer) sind als Schlangen organisiert.
(3) Einen weiteren Anwendungsschwerpunkt von Schlangen findet man bei der Simulation
von Warteschlagen-Systemen (z.B. Verkehrssimulation, Simulation der Kundenabferti-
gung bei Fluggesellschaften, Simulation der Auftragsabwicklung).

131
12.3 bungsaufgaben

Aufgabe 12.1
Implementieren Sie den ADT Queue aus Beispiel 12.2 als verkettet gespeicherte Liste (siehe
Abb. 12. 6). Verwenden Sie das Hauptprogramm aus Beisspiel 12.2 zum Testen des Pro-
gramms.


head
tail


Abb. 12.6: Implementierung einer Schlange als verkettet gespeicherte Liste


Abb. 12.7 veranschaulicht die Implementierung der put- und get-Operation. Beachten Sie den
Sonderfall des Einfgens bei leerer Queue sowie beim Entfernen des einzigen Queue-
Elements.

(b) put
tail
2
1
(a) put() bei leerer Queue
tail
(d) get() des letzten Queue-Elements
tail
(c) get()
head
tail


Abb. 12.7: Queue-Operationen bei verketteter Speicherung


Aufgabe 12.2 (Allgemeine Fragen)
(a) Wie nennt man das Zugriffsprinzip bei einem Stack?

(b) Worin liegt der Schwachpunkt einer als Ringspeicher realisierten Schlange?

132
13. Bume

Bume gehren zu den wichtigsten Datenstrukturen in der Informatik, weil sie die Modellie-
rung von hufig auftretenden hierarchischen Strukturen ermglichen. Beispiele fr hierarchi-
sche Strukturen sind:
- Organisation eines Unternehmens in Abteilungen und Unterabteilungen
- Verzeichnisstruktur eines Dateisystems
- Tag-Struktur in XML-Dokumenten.

Abb. 13.1 zeigt einen Ausschnitt aus der Organisation der Hochschule Mannheim als Baum-
struktur.

HS Mannheim
Elektrotechnik Informationstechnik Informatik
Elektronik
Medizin-
technik
Technische
Informatik
...
...


Abb. 13.1: Organisation der HS Mannheim (Ausschnitt)


13.1 Grundlagen

Ein Baum T=(P,B) besteht aus einer Menge von Knoten P = {p0,p1,...,pn-1} und einer
Menge von Kanten B c PxP, wobei B die folgenden Eigenschaften besitzt:
(1) Fr jeden Knoten pj mit Ausnahme eines Anfangsknotens w existiert genau ein Kno-
ten pi mit (pi,pj) e B.
(2) Fr jeden von w verschiedenen Knoten p existiert genau ein Weg (p0,...,pm) mit
(pi,pi+1) e B, i= 0,m-1 und p0=w und pm=p.

Die Eigenschaft (2) stellt sicher, dass jedes Element auf genau eine Weise von der Wurzel
aus erreichbar ist.

Bezeichnungen:
- Der Anfangsknoten w heit Wurzel des Baumes.
- Knoten, welche keine Nachfolger haben heien Bltter.
- Die Anzahl der Nachfolger eines Knotens p heit Rang von p.
- Ein Baum, bei dem jeder Knoten den Rang <= 2 hat, heit Binrbaum.

Bume sind rekursive Datenstrukturen. Dies soll durch die folgende alternative Definition
verdeutlciht werden:

- Sei T = (C, C) ein leerer Baum und sei sei T = ({w}, C ) ein Baum, der lediglich aus der
Wurzel besteht. Dann lsst sich ein beliebiger Baum durch das folgende rekursive Schema
definieren: T = (w, T1, Tn), wobei w die Wurzel der Teilbume Ti ist.

133
Aufgrund dieser rekursiven Grundstruktur von Bumen sind auch viele Operationen auf
Bumen in Form von rekursiven Algorithmen realisiert. Aus Anwendungsgesichtspunkten
spielen Binrbume eine besondere Rolle. Einen wichtigen Anwendungsfall bilden die Such-
bume.


13.2 Suchbume

Die in Kapitel 12 eingefhrten, verkettet gespeicherten linearen Listen bieten zwar den Vor-
teil der leichten nderbarkeit. Sie haben jedoch den Nachteil, dass nur die relativ langsame
sequenzielle Suche mglich ist. Die Suchbume verbinden die Eigenschaft der leichten n-
derbarkeit mit den Vorteilen der binren Suche.


Ein binrer Baum T=(P,B) ist ein Suchbaum fr die Schlsselmenge K={k1,...,kn} mit
Ordnungsrelation O, falls fr jeden Knoten p e P gilt:
(1) In Knoten p ist ein Schlssel k e K gespeichert.
(2) Fr alle im linken Unterbaum von p gespeicherten Schlssel ki gilt: (ki,k) e O.
(3) Fr alle im rechten Unterbaum von p gespeicherten Schlssel ki gilt: (k,ki) e O.


11
7
3 8 15 20
19


Abb. 13.2: Suchbaum (Beispiel)

Abb. 13.2 zeigt einen Suchbaum mit der Schlsselmenge { 3, 7, 8, 11, 15, 19, 20 }.


13.2.1 Abstraktion

Der ADT fr einen Suchbaum unterscheidet sich nur geringfgig von dem der verkettet ge-
speicherten Liste aus Beispiel 11.1.

Beispiel 13.1: (ADT Suchbaum)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass Tr ee {
publ i c:
Tr ee( ) ;
~Tr ee( voi d) ;
bool i sEmpt y( voi d) const ;
bool i nser t ( AD &el ) ;
bool r emove( AD &el ) ;
AD *sear ch( AD &el ) const ;
voi d i nor der ( voi d) ;
};


134
Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der im Suchbaum ge-
speicherten Nutzdaten.
- Fr den Typ AD wird eine Wertzuweisung sowie die Operatoren == (quivalenz) und <
(Vergleich kleiner) gefordert.
- Der Konstruktor erzeugt einen leeren Baum. Der Destruktor gibt den vom Baum belegten
Speicherplatz wieder frei.
- isEmpty() fragt den Leerzustand des Suchbaums ab.
- insert() fgt das Element el an geeigneter Stelle in den Suchbaum ein.
- Die Funktion remove() entfernt das Element el.
- Die Funktion search() sucht nach einem Element im Suchbaum, dessen Schlssel mit dem
von el identisch ist. Im Erfolgsfall wird ein Zeiger auf das gesuchte Element zurck gelie-
fert, im Falle der erfolglosen Suche liefert search() den Zeiger NULL zurck.
- inorder() durchluft den Baum in einer bestimmten Reihenfolge.


13.2.2 Implementierung

Die Implementierung deklariert den Typ fr die dynamisch allokierten Knoten des Such-
baums und definiert einen Zeiger auf die Wurzel des Baums. Das Klassen-Template Tree
wird hierzu um die folgenden privaten Datenelemente ergnzt:

Beispiel 13.1: (Suchbaum)
t empl at e<cl ass AD> / / AD Typ f r Nut zdat en ( Appl i cat i on Dat a)
cl ass Tr ee {
st r uct Tr eeNode { / / Baumknot en
AD el ;
Tr eeNode *l ef t , *r i ght ;
} *r oot ; / / Wur zel
/ / . . .
}

Bemerkungen:
- Die Baumknoten vom Typ TreeNode enthalten die eigentlichen Nutzdaten (Typ AD) so-
wie zwei Zeiger auf den linken und rechten Unterbaum.
- Der Zeiger root zeigt auf die Wurzel des Baumes. Ein leerer Baum ist durch einen Zeiger
mit Wert Null gekennzeichnet. (siehe Abb 13.3).


11
7
3 15 8 20
19


Abb. 13.3: Speicherung eines Suchbaums in dynamischen Variablen

135
Erzeugung und Zerstrung
Bei der Erzeugung eines Tree-Objekts wird der Zeiger root mit NULL (leerer Baum) initiali-
siert. Fr die Speicherfreigabe wird eine rekursive Hilfsfunktion deltree() bentigt, welche
privat deklariert ist und vom Destruktor aufgerufen wird. Die Funktion deltree() stellt sicher,
dass evtl. noch vorhandene Unterbume eines Knotens gelscht werden, bevor der Knoten
selbst gelscht wird.

Beispiel 13.1 (Forts.): (Destruktion)
t empl at e<cl ass AD>
voi d Tr ee<AD>: : del et eTr ee( Tr eeNode *t r ee)
{
i f ( t r ee == NULL)
r et ur n;
del et eTr ee( t r ee- >l ef t ) ;
del et eTr ee( t r ee- >r i ght ) ;
del et e t r ee;
}

t empl at e<cl ass AD>
Tr ee<AD>: : ~Tr ee( voi d)
{
del et eTr ee( r oot ) ;
}


Suchen
Die Suche nach einem Schlssel x verluft nach folgendem rekursiven Algorithmus:

(1) Falls der Baum leer ist, breche die Suche erfolglos ab.
(2) Falls der Schlssel in w = = x, breche die Suche erfolgreich ab.
(3) Falls Schlssel in w > x, suche im linken Unterbaum.
(4) Falls Schlssel in w < x, suche im rechten Unterbaum.

Zur Implementierung der rekursiven Suche wird eine rekursive Hilfsfunktion search() ben-
tigt, welche als Parameter die Wurzel des zu durchsuchenden (Teil)-Baumes sowie den Such-
schlssel bernimmt:

Beispiel 13.1 (Forts.): (Suchen)
t empl at e<cl ass AD>
AD *Tr ee<AD>: : sear ch( Tr eeNode *t r ee, AD &el ) const
{
i f ( t r ee == NULL)
r et ur n NULL;
el se i f ( t r ee- >el == el )
r et ur n &( t r ee- >el ) ;
el se i f ( el < t r ee- >el )
r et ur n sear ch( t r ee- >l ef t , el ) ;
el se
r et ur n sear ch( t r ee- >r i ght , el ) ;
}


136
Angestoen wird die Suche durch die berladene, public definierte Funktion search():

Beispiel 13.1 (Forts.): (Suchen)
t empl at e<cl ass AD>
AD *Tr ee<AD>: : sear ch( AD &el ) const
{
r et ur n sear ch( r oot , el ) ;
}



Einfgen
Auch das Einfgen eines neuen Schlssels x in den Suchbaum gestaltet sich rekursiv (siehe
Abb. 13.4):

(1) Falls der Baum leer ist, trage x in eine neue Wurzel w des Baums ein.
(2) Falls Schlssel in w = = x, Schlssel x bereits vorhanden.
(3) Falls Schlssel in w > x, trage x in linken Unterbaum ein.
(4) Falls Schlssel in w < x, trage x in rechten Unterbaum ein.


11
7
3 8
15
13
20
19


Abb. 13.4: Einfgen von Schlssel 13


Wie im Falle der Suche wird auch fr das Einfgen eine rekursive Funktion bentigt, welche
die Wurzel des (Teil-)Baumes sowie das einzufgende Element als Parameter bernimmt.
Anders als bei der Suche muss die Wurzel des (Teil-)Baums hier als Adresse oder Referenz
bergeben werden, da der Wurzelzeiger im Falle (1) auf den neu eingefgten Knoten gerich-
tet werden muss. Die rekursive insert()-Funktion wird als private Hilfsfunktion definiert und
ber die ffentliche Elementfunktion insert() des ADT Tree aufgrufen:


137
Beispiel 13.1 (Forts.): (Einfgen)
t empl at e<cl ass AD>
bool Tr ee<AD>: : i nser t ( Tr eeNode *&t r ee, AD &el )
{
i f ( t r ee == NULL)
{ Tr eeNode *node = new Tr eeNode;
node- >el = el ;
node- >l ef t = node- >r i ght = NULL;
t r ee = node;
r et ur n t r ue;
}
el se i f ( t r ee- >el == el )
r et ur n f al se;
el se i f ( t r ee- >el < el )
r et ur n i nser t ( t r ee- >r i ght , el ) ;
el se
r et ur n i nser t ( t r ee- >l ef t , el ) ;
}


Entfernen
Das Entfernen eines Schlssels aus einem Suchbaum entspricht dem Lschen des zugehri-
gen Knotens. Hiebei sind folgende Flle zu bercksichtigen.

(a) Falls sich der zu entfernende Knoten in einem Blatt befindet, wird das Blatt entfernt und
der zugehrige Zeiger im Vorgngerknoten auf NULL gesetzt (siehe Abb. 13.5)
(b) Falls der zu entfernende Knoten nur einen Nachfolger hat, wird der Knoten entfernt und
der zugehrige Zeiger im Vorgnger auf den einzigen Nachfolger gerichet.



11
7
3 8
(a)
11
7
8
(b)


Abb. 13.5: Entfernen der Knoten mit Schlssel 3 und 7


(c) Schwieriger gestaltet sich das Entfernen eines Knotens mit 2 Nachfolgern. In diesem
Fall muss der Inhalt des zu entfernenden Knotens mit dem Inhalt des symmetrischen
Nachfolgers ersetzt werden. Der symmetrische Nachfolger ist der Knoten mit dem
kleinsten Schlssel im rechten Unterbaum. Dieser ist entweder ein Blatt oder er hat nur
einen Nachfolger. Nach der inhaltlichen Ersetzung kann der symmetrische Nachfolger
gem (a) oder (b) entfernt werden (siehe Abb. 13.6).





138


13
15 20
19
(c)
11
15
13
20
19
symmetrischer Nach-
folger von Knoten 11


Abb. 13.6: Entfernen des Knotens mit Schlssel 11


Beispiel 13.1 (Forts.): (Entfernen)
t empl at e<cl ass AD>
bool Tr ee<AD>: : r emove( Tr eeNode *&t r ee, AD &el )
{
Tr eeNode *ent f ;

i f ( t r ee == NULL)
r et ur n f al se;
i f ( el < t r ee- >el )
r et ur n r emove( t r ee- >l ef t , el ) ; / / Ent f . i ml i . Unt er baum
el se i f ( t r ee- >el < el )
r et ur n r emove( t r ee- >r i ght , el ) ; / / Ent f . i mr e Unt er baum
el se / / El ement gef unden
{ i f ( t r ee- >l ef t == NULL) / / kei n l i . Nachf .
{ ent f = t r ee;
t r ee = t r ee- >r i ght ;
del et e ent f ;
}
el se i f ( t r ee- >r i ght == NULL) / / kei n r e. Nachf .
{ ent f = t r ee;
t r ee = t r ee- >l ef t ;
del et e ent f ;
}
el se
{ / / Symmet r i schen Nachf ol ger suchen
Tr eeNode *symNach = t r ee- >r i ght ;
whi l e ( symNach- >l ef t )
symNach = symNach- >l ef t ;
t r ee- >el = symNach- >el ; / / Umspei cher n
r emove( t r ee- >r i ght , symNach- >el ) ; / / Symm. Nachf . ent f .
}
r et ur n t r ue;
}
}

139
Bemerkungen:
- Da die Wurzel eines (Teil-)Baums mglicherweise verndert wird, muss der Zeiger auf die
Wurzel als Adresse bzw. als Refrenz bergeben werden.
- Zunchst wird der zu entfernende Knoten durch rekursiven Aufruf der Funktion remove()
lokalisiert.
- Sodann werden die einfach zu handhabenden Flle beim Entfernen eines Blattknotens oder
eines Knotens mit nur einem Nachfolger behandelt.
- Zuletzt wird der Fall mit zwei Nachfolgeknoten wie oben beschrieben gehandhabt.


Die rekursive Funktion remove wird durch die ffentliche Operation remove() des ADT Tree
aufgerufen.

Beispiel 13.1 (Forts.): (Entfernen)
t empl at e<cl ass AD>
bool Tr ee<AD>: : r emove( AD &el )
{
r et ur n r emove( &r oot , el ) ;
}


13.2 Traversieren von Binrbumen

Hufig ergibt sich die Aufgabe, alle Knoten eines Binrbaumes in irgendeiner Form zu bear-
beiten (z.B. zwecks Ausgabe aller Knoten). Ein Binrbaum kann auf die folgenden drei Arten
systematisch durchlaufen werden. Die Algorithmen sind in allen Fllen rekursiv:

Inorder Reihenfolge
(1) Durchlaufe den linken Unterbaum von Wurzel w.
(2) Bearbeite Wurzel w.
(3) Durchlaufe den rechten Unterbaum von Wurzel w.


Preorder Reihenfolge
(1) Bearbeite Wurzel w.
(2) Durchlaufe den linken Unterbaum von Wurzel w.
(3) Durchlaufe den rechten Unterbaum von Wurzel w.


Postorder Reihenfolge
(1) Durchlaufe den linken Unterbaum von Wurzel w.
(2) Durchlaufe den rechten Unterbaum von Wurzel w.
(3) Bearbeite Wurzel w.


Will man z.B. die Knoten eines Suchbaumes in der Sortierreihenfolge ausgeben, so muss der
Baum in Inorder-Reihenfolge traversiert werden:

140
Beispiel 13.1 (Forts.): (Ausgabe gem Inorder-Reihenfolge)
t empl at e<cl ass AD>
voi d Tr ee<AD>: : i nor der ( Tr eeNode *t r ee)
{
i f ( t r ee- >l ef t )
i nor der ( t r ee- >l ef t ) ;
cout << t r ee- >el << ' , ' ;
i f ( t r ee- >r i ght )
i nor der ( t r ee- >r i ght ) ;
}

t empl at e<cl ass AD>
voi d Tr ee<AD>: : i nor der ( voi d)
{
i f ( r oot == NULL)
{ cout << " Baumi st l eer \ n" ;
r et ur n;
}
el se
i nor der ( r oot ) ;
}



141
13.3 bungsaufgaben

Aufgabe 13.1
Erweitern Sie das Template Tree aus Beispiel 13.1 um eine Elementfunktion zur Ausgabe des
Baumes in Preorder-Reihenfolge.


Aufgabe 13.2
(a) Zeichnen Sie den Suchbaum nach Einfgen der Schlssel: 13, 7, 12, 17, 3, 21, 9, 19, 14
(b) Zeichnen Sie den vernderten Suchbaum nach Lschen der Schlssel 7 und 13.


Aufgabe 13.3 (Allgemeine Fragen)
(a) Welche Variante der Traversion eines Suchbaumes verwendet die search()-Funktion aus
Beispiel 131.?

(b) Wie msste ein Suchbaum mit Schlsselmenge {1, 2, 3, 4, 5, 6, 7 } strukturiert sein, da-
mit die Suche optimal schnell ist?

(c) Wie msste ein Suchbaum mit Schlsselmenge {1, 2, 3, 4, 5, 6, 7 } strukturiert sein, da-
mit die Effizienz der Suche denkbar schlecht ist?


142
14. Graphen

Wie schon die Bume gehren Graphen zu den nichtlinearen Datenstrukturen. Sie eignen sich
zur Modellierung von netzartigen Strukturen, wie sie z.B. beim Straennetz oder bei den
elektrisch leitenden Verbindungen auf einer Leiterplatte gegeben sind. Praktische Problem-
stellungen sind z.B.:
- die Bestimmung eines Weges zwischen zwei Orten.
- die Prfung, ob eine leitende Verbindung zwischen zwei Punkten auf einer Leiterplatte
besteht.
Vor einer Darstellung der programmtechnischen Reprsentation von Graphen und der L-
sungsalgorithmen fr die geschilderten Problemstellungen werden zunchst einige Begriffe
aus der Graphentheorie definiert.


14.1 Grundlagen

Ein gerichteter Graph G = (P,B) besteht aus einer Menge P = {p0,p1,...,pn-1} von Knoten
und einer Menge B c PxP von Kanten. Ein Graph G = (P,B) heit ungerichtet oder sym-
metrisch, falls fr je zwei beliebige Knoten pi,pj e P gilt: (pi,pj) 0 B <=> (pj,pi) 0 B

Gerichtete und ungerichtete Graphen lassen sich in Form eines Knoten-Kanten-Diagramms
grafisch veranschaulichen (siehe Beispiele 14.1 und 14.2). Im Falle von ungerichteten Gra-
phen werden die beiden gegenlufigen, gerichteten Kanten zu einer ungerichteten Kante zu-
sammengefasst.
Voraussetzung fr eine programmtechnische Lsung der oben genannten Problemstellungen
ist eine geeignete Datenstruktur zur Reprsentation eines Graphen. Eine Adjazenzmatrix ist
eine solche Datenstuktur.

Sei G=(P,B) ein gerichteter Graph mit P={p0,..pn-1}. Die n x n Matrix
1 falls (pi,pj) e B
A(G) = (aij) mit aij =
0 sonst
heit Adjazenzmatrix von G.

Die nachfolgenden Beispiele 14.1 und 14.2 enthalten die Knoten-Kanten-Diagramme und
Adjazenzmatrizen fr einen gerichteten bzw. einen ungerichteten Graphen.

Beispiel 14.1: Gerichteter Graph

G = (P, B)
P = { p0, p1, p2, p3, p4 }
B = { (p0,p1), (p1,p2), (p1,p3),
(p1,p4), (p2,p0), (p3,p4) }

p
p
p
p
p
0
1
3
4
2
0 1 0 0 0
0 0 1 1 1
1 0 0 0 0
0 0 0 0 1
0 0 0 0 0
Knoten-Kanten-
Diagramm
Adjazenzmatrix


Abb. 14.1: Gerichteter Graph

143

Beispiel 14.2: Ungerichteter Graph

G = (P, B)
P = { p0, p1, p2, p3}
B = { (p0,p1), (p1,p0), (p1,p2),
(p2,p1), (p2,p3), (p3,p2) }
p p
p p
0 1
3 2
0 1 0 0
1 0 1 0
0 1 0 1
0 0 1 0
Adjazenzmatrix
Knoten-Kanten-
Diagramm


Abb. 14.2: Ungerichteter Graph


14.2 Wegesuche

Sei G=(P,B) ein gerichteter Graph und p,q e P. Ein Weg von p nach q ist eine Folge von
Knoten w=(p0,...,pk) mit p=p0, q=pk und (pi,pi+1) 0 B, i=0,k-1. k ist die Weglnge. Ein Weg
von p nach p heit Zykel.

Es existiert eine Reihe von Verfahren zur Wegesuche in Graphen. Im Folgenden wird nher
auf die sogenannte Tiefensuche eingegangen.

Als Vorberlegung betrachte man alle von einem bestimmten Knoten ausgehenden Wege.
Die Gesamtheit der Wege spannt einen Baum auf, welcher auch als Spannbaum bezeichnet
wird. Abb. 14.3 zeigt die Spannbume zum Graphen aus Beispiel 14.1.

p
1
p
1
p
1
p
1
p
0
p
0
p
0
p
0
p
2
p
2
p
2
p
2
p
4
p
4
p
4
p
4
p
4
p
4
p
4
p
4
p
3
p
3
p
3
p
3


Abb. 14.3: Spannbume zum Graphen aus Beispiel 14.1

Grundstzlich wrden sich die Spannbume bei einem Graphen mit Zykeln unendlich fortset-
zen. Fr die Wegesuche ist es jedoch ausreichend die Spannbume so weit zu expandieren,
bis entweder ein Endknoten oder ein Knoten erreicht ist, der bereits Bestandteil des Weges
im Spannbaum ist. Im letzgenannten Fall liegt ein Zykel vor. Eine weitere Expansion des
Spannbaumes wrde keine neuen Wege aufdecken. Lediglich der Startknoten wird auch ein
zweites Mal als Endknoten eines Weges in den Spannbaum aufgenommen. Dies ermglicht
das Auffinden von Zykeln ausgehend vom Startknoten.

144
Die Tiefensuche entspricht einer Suche des Zielknotens im Spannbaum des Startknotens ge-
m der Preorder-Reihenfolge. Z.B wrde die Suche eines Weges von p0 nach p4 den Weg
(p0, p1, p3, p4) ergeben. Die Tiefensuche liefert also nicht in jedem Fall den Weg mit der kr-
zesten Weglnge.


14.3 Abstraktion

Fr die Wegesuche wird ein Datentyp zur Reprsentation eines Weges bentigt. Da der re-
kursive Algorithmus der Tiefensuche die Knoten des Weges in umgekehrter Reihenfolge
ausweist, eignet sich hierfr der in Kapitel 12 eingefhrte Datentyp Stack. Die maximale
Lnge eines Weges in einem Graphen mit n Knoten betrgt n+1. Es handelt sich um einen
zyklischen Weg, der alle Knoten des Graphen enthlt. Die maximale Stacktiefe betrgt daher
n+1.

Der folgenden ADT Graph beschreibt die Schnittstelle des Datentyps mit einer Funktion zur
Tiefensuche.

Beispiel 14.1 (Forts.): (ADT Graph)
cl ass I nval i dI ndex { };
t ypedef St ack<unsi gned> Pat hSt ack;

cl ass Gr aph {
publ i c:
Gr aph( unsi gned n) ;
~Gr aph( voi d) ;
unsi gned si ze( voi d) const ;
i nt & ar c( unsi gned i , unsi gned j ) t hr ow ( I nval i dI ndex) ;
bool dept hFi r st Sear ch( unsi gned st ar t , unsi gned end, Pat hSt ack &pat h)
t hr ow ( I nval i dI ndex) ;
};

Bemerkungen:
- Der Konstruktor reserviert den Speicherplatz fr die Adjazenzmatrix mit n Knoten. Der
Destruktor gibt den von der Adjazenzmatrix belegten Speicherplatz wieder frei.
- size() liefert die Anzahl der Knoten zurck.
- arc() liefert eine Referenz auf den zur Kante von Knoten i nach Knoten j korrespondie-
renden Eintrag in der Adjazenzmatrix.
- Die Funktion depthFirstSearch() dient zur Wegebestimmung zwischen den Knoten start
und end. Falls ein solcher Weg existiert, liefert depthFirstSearch() den Wert true zurck.
Das als Referenz bergebene PathStack-Objekt path enthlt in diesem Fall die zugehri-
ge Knotenfolge. Falls kein Weg gefunden wird, liefert depthFirstSearch() den Wert false
zurck.


14.4 Implementierung

Bei der Implementierung liegt der Schwepunkt der Beschreibung auf der Funktion depth-
FirstSearch().

145
Beispiel 14.1 (Forts.): (Graph)
cl ass Gr aph {
i nt *adj Mat r i x; / / Adj azenzmat r i x
unsi gned n; / / Knot enzahl
bool Gr aph: : dept hFi r st Sear ch( unsi gned st ar t , unsi gned end,
Pat hSt ack &pat h, bool *mar k) ;
publ i c:
/ / . . .
};

Bemerkungen:
- Der Zeiger adjMatrix verweist auf die Adjazenzmatrix der Dimension n x n.
- Bei der berladenen privaten Methode depthFirstSearch() handelt es sich um die rekursiv
implementierte Tiefensuche. Die Methode wird ber die gleichnamige ffentliche Metho-
de aufgerufen.


Bei depthFirstSearch() handelt es sich um eine mittels Backtracking implementierte, rekur-
sive Suche in dem oben skizzierten Spannbaum. Die public-Methode depthFirstSearch()
dient zm Einstieg in die Rekusion.

Beispiel 14.1 (Forts.): (Tiefensuche, Einstig in die Rekursion)
1. bool Gr aph: : dept hFi r st Sear ch( unsi gned st ar t , unsi gned end,
Pat hSt ack &pat h) t hr ow ( I nval i dI ndex)
2. {
3. i f ( st ar t >= n | | end >= n) / / Ungl t i ge Knot ennummer
4. t hr ow I nval i dI ndex( ) ;
5. bool *mar k = new bool [ n] ; / / " besucht e" Knot en
6. f or ( unsi gned i =0; i <n; i ++)
7. mar k[ i ] = f al se;
8. bool success = dept hFi r st Sear ch( st ar t , end, pat h, mar k) ;
9. del et e mar k;
10. r et ur n success;
11. }


Bemerkungen:
- Die Parameter start und end enthalten den Start- bzw. den Endknoten des gesuchten Weg-
es.
- Der Paramter path enthlt einen Stack der Tiefe n+1 und dient zur Aufnahme der Knoten-
folge des gefundenen Weges.
- Das in Zeile 5-7 erzeugte und mit false-Werten initialisierte Array mark dient zur Markie-
rung von bereits besuchten Knoten. Falls die Suche auf einen bereits markierten Knoten
trifft, liegt ein Zykel vor. Falls also der Zielknoten noch nicht erreicht wurde, wrde auch
eine Weiterverfolgung des Weges ber einen weiteren Durchlauf des Zykels nicht zum
Ziel fhren.
- In Zeile 8 wird die rekursive Methode depthFirstSearch() aufgerufen. Ihr wird als zustz-
licher Parameter der Markierungsvektor mark bergeben.
- depthFirstSearch() gibt true zurck, falls ein Weg gefunden wurde. Nur in diesem Fall
enthlt der Parameter path einen gltigen Weg.



146
Beispiel 14.1 (Forts.): (rekursive Tiefensuche)
1. bool Gr aph: : dept hFi r st Sear ch( unsi gned st ar t , unsi gned end,
Pat hSt ack &pat h, bool *mar k)
2. {
3. mar k[ st ar t ] = t r ue;
4. f or ( unsi gned i =0; i <n; i ++) {
5. i f ( ar c( st ar t , i ) )
6. i f ( i == end) {
7. pat h. push( end) ;
8. pat h. push( st ar t ) ;
9. r et ur n t r ue;
10. }
11. el se i f ( ! mar k[ i ] &&
12. dept hFi r st Sear ch( i , end, pat h, mar k) ) {
13. pat h. push( st ar t ) ;
14. r et ur n t r ue;
15. }
16. }
17. r et ur n f al se;
18. }

Bemerkungen:
- Der Startknoten wird markiert (Zeile 3).
- In der for-Scheife ab Zeile 4 werden alle direkt vom Startknoten aus zu erreichenden Fol-
geknoten untersucht.
- Falls der Folgeknoten der Endkoten ist, werden der Endknoten und der Startknoten im
Stack path abgelegt. depthFirstSearch() endet mit true (Zeilen 6-9).
- Falls der Folgeknoten nicht der Endknoten und noch nicht markiert ist, wird eine rekursive
Suche ab dem Folgenkoten eingeleitet (Zeilen 11-12).
- Im Falle einer positiven rekursiven Suche wird im Verlaufe des Rekursionsabstiegs der
Startkonten im Stack path abgelegt. depthFirstSearch() endet mit true (Zeilen 13-14).
- In allen anderen Fllen endet depthFirstSearch() mit false (Zeile 17).

Abb. 14.4 zeigt den Ablauf der Suche am Beispiel des Weges von p0 nach p4.



Abb. 14.4: Ablauf der Wegesuche

147


Abb. 14.4 (Forts.): Ablauf der Wegesuche

148
14.4 bungsaufgaben

Aufgabe 14.1
Zeichnen Sie die Spannbume fr den Graphen G = (P, B) mit P = { p0, p1, p2, p3, p4 } und
B = { (p0,p1), (p1,p2), (p1,p3), (p2,p4), (p4,p1) }


Aufgabe 14.2 (Allgemeine Fragen)
(a) Welche Eigenschaft hat die Adjazenzmatrix eines ungerichteten Graphen?

(b) Eine Senke in einem Graphen, ist ein Knoten in einem gerichteten Graphen, aus dem
keine Kante herausfhrt.
- Wie erkennt man eine Senke in der Adjazenzmatrix?
- Bei welchem Knoten aus Aufgabe 14.1 handelt es sich um eine Senke?

149
15 Die Standard-Template Library (STL)

Die in der C++-Standardbibliothek enthaltene STL (Standard Template Library) ist eine mit
Hilfe von Templates realisierte Bibliothek, welche im Wesentlichen Container, Iteratoren,
Algorithmen und Funktionsobjekte implementiert.


15.1 Konzept der STL

Die oben genannten Bestandteile der STL bauen im Hinblick auf Implementierung und Ver-
wendung aufeinander auf. Aus diesem Grunde wird kurz auf das Gesamtkonzept eingegan-
gen, bevor eine Auswahl einzelner Bestandteile genauer beschrieben wird.


Container
Den Kern der STL bilden die in Tabelle 15.1 aufgefhrten Containerklassen.

Tabelle 15.1: Containerklassen der STL

Container fr Sequenzen Container fr Assoziationen
vector deque list set / multiset
stack queue priority_queue map / multimap


Die Container fr Sequenzen sind lineare Container; d.h. die Elemente sind in einer linearen
Abfolge gespeichert. Bei den Containern fr Assoziationen sind die darin abgelegten Elemen-
te ber Schlssel adressierbar. Sie sind als balancierte Bume abgespeichert.

- Ein vector verwaltet Elemente in einem dynamisch wachsenden Array. Grundstzlich
kann an beliebiger Stelle zugegriffen, eingefgt und gelscht werden. Einfgen und L-
schen ist jedoch nur am Ende optimal schnell. Beim Einfgen und Lschen am Anfang o-
der in der Mitte mssen alle nachfolgenden Elemente verschoben werden.
- Eine deque (double ended queue) ist ein vector, der an beiden Enden optimal schnell mo-
difiziert werden kann. Eine Deque wchst dynamisch an beiden Enden.
- Eine list ist eine doppelt verkettete Liste. Es besteht kein indizierter Zugriff; dafr ist das
Einfgen und Lschen an beliebiger Stelle mit gleichem Aufwand mglich.
- Bei den Containern stack, queue und priority_queue handelt es sich um sogenannte
Adapter-Container; d.h sie sind auf Basis der elementaren Container fr Sequenzen im-
plementiert.
- Ein set verwaltet eine Menge von Elementen mit Suchschlsseln. In einem multiset ms-
sen die Schlssel nicht eindeutig sein.
- Eine map verwaltet Schlssel/Werte-Paare. In einem multimap knnen Mehrfachschls-
sel auftreten.



150
Iteratoren
Zu den verschiedenen Containerklassen - mit Ausnahme der Adapter-Container - existieren
zugehrige Iterator-Klassen. Jede Containerklasse verfgt ber gleichlautende Elementfunk-
tionen, welche auf den Container abgestimmte Iterator-Objekte erzeugen. Die Schnittstelle
der Iterator-Objekte ist einem Zeiger nachempfunden. Hierdurch gleicht der Zugriff auf einen
Container dem Zugriff auf ein C-Array.


Algorithmen
Die STL enthlt zahlreiche als Funktionstemplates realisierte Algorithmen zur Manipulation
von Mengen und deren Elementen. Dazu gehren Algorithmen zum Suchen, Sortieren und
Kopieren. Die Algorithmen sind nicht als Elementfunktion der Containerklassen implemen-
tiert, sondern als externe C-Funktionen, welche ber Iteratoren auf die in den Containern ab-
gelegten Elemente zugreifen. Der Vorteil dieser Implementierung besteht in der allgemeinen
Verwendbarkeit der Algorithmen fr beliebige Container.


Funktionsobjekte
Funktionsobjekte sind Objekte von Klassen, welche ber eine Operatorfunktion operator()()
verfgen. Derartige Objekte werden in Zusammenhang mit Algorithmen eingesetzt, z.B.
zwecks Formulierung des Suchkriteriums in Such-Algorithmen. Die STL verfgt ber eine
Reihe von vordefinierten Funktionsobjekten.


Header-Dateien der STL
Fr jeden Container-Typ existiert eine eigene Header-Datei, welche das jeweilige Klassen-
Template enthlt. Fr den vector-Container existiert z.B. die Header-Datei <vector>. Die
Funktionsobjekte sind in der Header-Datei <functional> vereinbart. Fr die Algorithmen
existiert ebenfalls eine globale Header-Datei namens <algorithm>. Als Bestandteil der Stan-
dard-Bibliothek ist die STL im Namensraum std definiert.

Beispiel:

#i ncl ude <vect or > / / vect or - Cont ai ner
#i ncl ude <l i st > / / l i st - Cont ai ner
#i ncl ude <al gor i t hm> / / Al gor i t hmen
#i ncl ude <f unct i onal > / / Funkt i onsobj ekt e

usi ng namespace st d ; / / Wahl des Namensr aums st d


151
15.2 Container

Ein Ziel bei der Entwicklung der STL bestand darin, die Schnittstelle der Container-Klassen
mglichst einheitlich zu halten. Aus diesem Grunde verfgen alle Container-Klassen ber
einen einheitlichen Grundbestand an Typdefinitionen und Elementfunktionen. Diese werden
um Elementfunktionen ergnzt, welche spezifisch sind fr den bestimmten Container-Typ.
Tabelle 15.2 gibt einen berblick ber die wichtigsten Zugriffsfunktionen der einzelnen Con-
tainer-Klassen.


Tabelle 15.2: Zugriffsfunktionen der Container-Klassen (unvollstndig)

vector deque list set / multiset map / multimap
push_f r ont ( ) pop_f r ont ( )
f i nd( )
i nser t ( )
er ase( )
oper at or [ ] at ( )
f r ont ( )
back( )
push_back( )
pop_back( )
i nser t ( )
er ase( )
r esi ze( )
begi n( )
end( )
r begi n( )
r end( )
si ze( )
empt y( )
cl ear ( )
oper at or =
oper at or ==
*)
oper at or ! =
*)

oper at or <
*)
oper at or <=
*)

oper at or >
*)

oper at or >=
*)


*)
freie C-Funktionen

Exportierte Datentypen
Unter der Zielsetzung einer weitestgehend einheitlichen Container-Schnittstelle definiert je-
der Container einen Satz von gleichlautenden Datentypen. Tabelle 15.3 gibt einen berblick
ber diese Typen fr eine Container-Klasse Cont und einen Nutzdatentyp T.

Tabelle 15.3: Exportierte Datentypen (unvollstndig)

Datentyp Bedeutung
Cont : : val ue_t ype
T (Nutzdatentyp)
Cont : : r ef er ence
T&(Referenz vom Nutzdatentyp)
Cont : : i t er at or Typ des Iterators
Cont : : r ever se_i t er at or Typ des Rckwrtsiterators
Cont : : di f f er ence_t ype vorzeichenbehafteter integraler Typ
Cont : : si ze_t ype integraler Typ ohne Vorzeichen


Unter Verwendung dieser Typen kann das Klassen-Template vector ansatzweise wie folgt
definiert werden (Der Template-Parameter T gibt den Nutzdatentyp an):


152
Beispiel:
t empl at e <cl ass T>
cl ass vect or {
cl ass I t er ; / / I nt er ne I t er at or kl asse
T* dat a;
i nt si ze;
publ i c:
t ypedef T val ue_t ype;
t ypedef T& r ef er ence;
t ypedef I t er i t er at or ;
t ypedef unsi gned si ze_t ype;
/ / . . . .

vect or ( ) ; / / St andar dkonst r ukt or
vect or ( si ze_t ype n) ;
~vect or ( ) ;
si ze_t ype si ze( ) const ;
r ef er ence at ( si ze_t ype pos) ;
i t er at or begi n( ) ;
/ / . . .
};


Template-Klasse vector
Anhand von Beispiel 15.1 werden einige der wichtigsten Funktionen des vector-Template
vorgestellt. Mit Ausnahme des indizierten Zugriffs sind die Funktionen auf alle Container fr
Sequenzen anwendbar (siehe Tabelle 15.2).

Beispiel 15.1: ( vector-Template)
#i ncl ude <i ost r eam>
#i ncl ude <vect or > / / vect or - Cont ai ner
usi ng namespace st d ;

i nt mai n( )
{
vect or <i nt > vec1, vec2;

f or ( i nt i =0; i <5; i ++)
vec1. push_back( i ) ;

cout << " vec1: " ;
f or ( i =0; i < vec1. si ze( ) ; i ++)
cout << vec1[ i ] << ' ' ;
cout << ' \ n' ;

vec2 = vec1;
i f ( vec1 == vec2)
cout << " vec2 i dent i cal t o vec1\ n" ;

vec1. cl ear ( ) ;
vec2. cl ear ( ) ;

r et ur n 0;
}


153
Bemerkungen:
- Mit Hilfe des Standard-Konstruktors werden zwei leere Container erzeugt.
- Die Zugriffsfunktion push_back() fgt ein Element am Ende des Containers an.
- ber den Wertzuweisungsoperator wird vec2 als Kopie von vec1 angelegt.
- Die freie C-Funktion operator==() fhrt einen Vergleich auf elementweise Identitt
durch.
- Mittels clear() wird der Inhalt beider Container gelscht.
- Tabelle 15.4 enthlt die Prototypen der im Beispiel verwandten Funktionen.


Tabelle 15.4: vector-Funktionen (Auswahl)

Funktionsdeklaration Bedeutung
vect or <T>: vect or ( ) ; Standard-Konstruktor
vect or <T>: : push_back( const T &x) ; Anfgen am Ende
si ze_t ype vect or <T>: : si ze( ) ; Abfrage der Elementzahl
T& vect or <T>: : oper at or [ ] ( si ze_t ype i ) ; Indizierter Zugriff
vect or <T>& vect or <T>: : oper at or =( vect or <T> &v) ; Wertzuweisungsoperator
bool oper at or ==( vect or <T> &v1, vect or <T> &v2) ; quivalenzoperator


Bemerkungen:
Wie alle Templates stellen auch die Container der STL Anforderungen an den konkreten
Nutzdatentyp. Die im Beispiel verwendeten Funktionen setzen insbesondere die Wertzuwei-
sung und den quivalenzoperator voraus. Die Vergleichsoperationen <, <=, > und >= benti-
gen zudem einen <-Operator beim Nutzdatentyp.


15.3 Iteratoren

Jede Container-Klasse - mit Ausnahme der Adapter-Container stack, queue und priori-
ty_queue - enthlt die Deklaration einer auf den Container-Typ zugeschnittenen Iteratorklas-
se. ber die in jedem Container-Typ verfgbaren Funktionen begin() und end() knnen
Iteratoren erzeugt werden. Ein durch begin() erzeugter Iterator verweist auf das erste Contai-
ner-Element, ein durch end() erzeugter Iterator zeigt hinter (!) das letzte Element (siehe Abb.
15.1).


Container
Iterator
...
begin() end()


Abb. 15.1: Zusammenspiel zwischen Container und Iteratoren


154
Die STL definiert Iteratortypen mit abgestufter Funktionalitt. Hier wird lediglich auf den
Bidirectional-Iterator eingegangen, der von den Container-Typen vector, dequeue, list, set,
multiset, map und multimap bereitgestellt wird. Tabelle 15.5 zeigt die wichtigsten Funktio-
nen eines Bidirectional-Iterators.

Tabelle 15.5: Funktionen eines Bidirectional-Iterators

Iterator-Funktion Bedeutung
T& oper at or *( ) ; Zugriff auf das aktuelle Element
*)

I t er & oper at or =( I t er &i ) ; Wertzuweisung
**)

I t er & oper at or ++( )
I t er & oper at or ++( i nt ) ;
Inkrement-Operator (prefix)
Inkrement-Operator (postfix)
I t er & oper at or - - ( ) ;
I t er & oper at or - - ( i nt ) ;
Dekrement-Operator (prefix)
Dekrement-Operator (postfix)
bool oper at or ==( I t er &i ) ;
bool oper at or ! =( I t er &i ) ;
Gleichheit
Ungleichheit

*)
T ist der Nutzdatentyp des Containers
**)
Iter ist Typ des Iterators

Beispiel 15.2 zeigt in vereinfachter Form eine mgliche Realisierung eines Vector-Containers
mit zugehriger Iteratorklasse. Das Beispiel verdeutlicht insbesondere das Zusammenspiel
von Containerklasse und Iteratorklasse.

Beispiel 15.2: (Container- und Iteratorklasse)
t empl at e <cl ass T>
cl ass Vect or {
cl ass I t er ;
publ i c:
t ypedef I t er i t er at or ;

Vect or ( i nt sz) { dat a = new T[ si ze=sz] ; }
~Vect or ( ) { del et e [ ] dat a; }
i t er at or begi n( ) { r et ur n i t er at or ( dat a) ; }
i t er at or end( ) { r et ur n i t er at or ( dat a+si ze) ; }
pr i vat e:
T* dat a;
i nt si ze;

cl ass I t er {
T* pos;
publ i c:
I t er ( T* po) { pos = po; }
T& oper at or *( ) { r et ur n *pos; }
I t er oper at or ++( i nt ) { I t er i t = *t hi s; pos++; r et ur n i t ; };
I t er & oper at or ++( ) { pos++; r et ur n *t hi s; }
bool oper at or ==( const I t er &i t ) { r et ur n pos == i t . pos; }
bool oper at or ! =( const I t er &i t ) { r et ur n pos ! = i t . pos; }
};
};


155
Bemerkungen
- Die Containerklasse definiert den Typ iterator fr den ihm eigenen Iteratortyp.
- Die Iteratorklasse ist als geschachtelte Klasse realisiert und hat als solche vollen Zugriff
auf die privaten Elemente der umfassenden Klasse.

Beispiel 15.3 demonstriert die Anwendung eines Bidirectional-Iterators in Verbindung mit
einem list-Container.

Beispiel 15.3: (Bidirectional-Iterator)
#i ncl ude <i ost r eam>
#i ncl ude <l i st > / / l i st - Cont ai ner

usi ng namespace st d ;

t ypedef l i st <i nt > I nt Li st ;

i nt mai n( )
{ I nt Li st l i st ;
I nt Li st : : i t er at or l i st I t ;

f or ( i nt i =0; i <5; i ++)
l i st . push_back( i ) ;
f or ( l i st I t = l i st . begi n( ) ; l i st I t ! = l i st . end( ) ; l i st I t ++)
cout << *l i st I t << ' ' ;
cout << ' \ n' ;
}


15.4 Algorithmen

Das Beispiele 15.3 verdeutlicht, dass erst durch die Iteratoren ein sehr flexibler Zugriff auf
die ansonsten sprlich mit Zugriffsfunktionen ausgestatteten Container mglich ist. In diesem
Kapitel wird mit den sogenannten Algorithmen eine ca. 100 Funktions-Templates umfassen-
de Sammlung von Funktionen vorgestellt, welche weitere Operationen auf den STL-
Containern ermglichen. Im Folgenden wird kurz auf die wesentlichen Grundkonzepte der
Algorithmen eingegangen. Daran anschlieend werden exemplarisch einige Algorithmen
nher vorgestellt.


Grundkonzepte
- Bei den Algorithmen handelt es sich um Funktions-Templates, welche bestimmte Operati-
onen auf einem oder mehreren STL-Containern durchfhren. Beispiele sind Such- und Sor-
tierfunktionen, Kopierfunktionen und Mengenoperationen.
- Anstatt jeden Container mit einer eigenen Elementfunktion fr eine bestimmte Operation
auszustatten, wird eine auf alle Containertypen anwendbare, freie Template-Funktion zur
Verfgung gestellt.
- Die allgemeine Anwendbarkeit der Algorithmen wird im Wesentlichen durch den Einsatz
von Iteratoren als Bindeglieder zwischen den Containern und den darauf zugreifenden Al-
gorithmen erreicht.



156
Halboffene Intervalle
- Viele Algorithmen arbeiten mit einem oder zwei definierten Bereichen eines Containers.
Ein Bereich kann den gesamten Container oder einen Ausschnitt des Containers beinhalten.
- Ein Bereich wird durch zwei Iteratoren begrenzt. Ein Iterator zeigt auf den Anfang des Be-
reichs, ein zweiter Iterator zeigt hinter das letzte Element des Bereichs. Die beiden Iterato-
ren spannen so ein halboffenes Intervall auf:

i t = f i nd( f i r st , l ast , val ue) ;

Bei first und last und it handelt es sich um Iteratoren. Der Algorithmus find() sucht den
Bereich [first, last) nach dem Wert value ab und liefert ggf. einen Iterator auf das gefunde-
ne Element zurck.


find()
t empl at e<cl ass I nI t , cl ass T>
I nI t find( I nI t f i r st , I nI t l ast , const T &val ) ;

- find() liefert einen Iterator auf das erste Element im Bereich [first, last) mit Wert val.
- Falls kein solches Element gefunden wird, ist der zurckgegebene Iterator identisch mit
last.


Beispiel 15.4: (Algorithmus find())
#i ncl ude <i ost r eam>
#i ncl ude <vect or > / / vect or - Cont ai ner
#i ncl ude <al gor i t hm> / / f i nd- Al gor i t hmus
usi ng namespace st d ;

t ypedef vect or <i nt > I nt Vec;

i nt mai n( )
{
I nt Vec vec;
I nt Vec: : i t er at or r es;
i nt ar g;

f or ( i nt i =0; i <5; i ++)
vec. push_back( i ) ;

cout << " Suche nach: " ;
ci n >> ar g;
i f ( ( r es = f i nd( vec. begi n( ) , vec. end( ) , ar g) ) ! = vec. end( ) )
cout << *r es << " gef unden\ n" ;
el se
cout << ar g << " ni cht gef unden\ n" ;
}



157
find_if()
t empl at e<cl ass I nI t , cl ass Pr ed>
I nI t find_if( I nI t f i r st , I nI t l ast , Pr ed pr ) ;

- find_if () liefert einen Iterator auf das erste Element im Bereich [first,last) zurck, fr das
pr den Wert true ergibt.
- Falls kein solches Element gefunden wird, wird der Iterator last zurck gegeben
- Fr den Template-Parameter pr kann ein Zeiger auf eine bool-Funktion oder ein geeigne-
tes Funktionsobjekt eingesetzt werden.

Beispiel 15.5a: (Algorithmus find_if)
#i ncl ude <i ost r eam>
#i ncl ude <vect or > / / vect or - Cont ai ner
#i ncl ude <al gor i t hm> / / f i nd_i f
usi ng namespace st d ;

t ypedef vect or <i nt > I nt Vec;

bool i sEval ( i nt i )
{
r et ur n ( i %2) == 0;
}

i nt mai n( )
{
I nt Vec vec;
I nt Vec: : i t er at or r es;

f or ( i nt i =0; i <10; i ++)
vec. push_back( i ) ;

r es = vec. begi n( ) ;
do
{ r es = f i nd_i f ( r es, vec. end( ) , i sEval ) ;
i f ( r es ! = vec. end( ) )
{ cout << *r es << ' ' ;
r es++;
}
} whi l e ( r es ! = vec. end( ) ) ;
}

Bemerkungen
- Das Programm sucht alle geraden Zahlen im Container vec und gibt diese aus.
- Hierzu wird find_if() wiederholt aufgerufen, wobei die Suche jeweils wieder hinter dem
zuletzt gefundenen Element aufgenommen wird.
- Beispiel 15.5a verwendet einen Zeiger auf eine bool-Funktion als Prdikat pred. Der
nchste Abschnitt zeigt, dass stattdessen auch ein Funktionsobjekt mglich ist.


Funktionsobjekte
Ein Funktionsobjekt ist eine Instanz einer Klasse, welche ber eine Operatorfunktion opera-
tor()() verfgt.

158
cl ass I sDi vi sor {
i nt di v;
publ i c:
I sDi vi sor ( i nt d) { di v = d; }
bool oper at or ( ) ( i nt &i ) { r et ur n ( i %di v) == 0; }
};

I sDi vi sor i sDi v( 3) ; / / Funkt i onsobj ekt
i nt zahl ;

i f ( i sDi v( zahl ) )
cout << zahl << i st dur ch 3 t ei l bar ;


Funktionsobjekte ermglichen als Prdikate in find_if() eine grere Flexibilitt bei der For-
mulierung von Bedingungen, als dies bei bool-Funktionen der Fall ist. Die Kriterien fr die
Abfrage knnen noch zur Laufzeit in Instanzvariablen des Funktionsobjekts abgelegt und in
der Operatorfunktion operator()() ausgewertet werden. In Beispiel 15.5b wird ein Funktions-
objekt vom Typ IsDivisor als Prdikat eingesetzt, welches die Teilbarkeit einer Zahl durch
einen bestimmten Divisor prft. Der Divisor wird bei der Konstruktion des Funktionsobjekts
festgelegt.

Beispiel 15.5b: (Funktionsobjekte)
#i ncl ude <i ost r eam>
#i ncl ude <vect or > / / vect or - Cont ai ner
#i ncl ude <al gor i t hm> / / f i nd_i f

usi ng namespace st d ;

t ypedef vect or <i nt > I nt Vec;

cl ass I sDi vi sor {
i nt di v;
publ i c:
I sDi vi sor ( i nt d) { di v = d; }
bool oper at or ( ) ( i nt &i ) { r et ur n ( i %di v) == 0; }
};

i nt mai n( )
{
I nt Vec vec;
I nt Vec: : i t er at or r es;
I sDi vi sor i sDi v( 3) ; / / Funkt i onsobj ekt

f or ( i nt i =0; i <10; i ++)
vec. push_back( i ) ;

r es = vec. begi n( ) ;
do
{ r es = f i nd_i f ( r es, vec. end( ) , i sDi v) ;
i f ( r es ! = vec. end( ) )
{ cout << *r es << ' ' ;
r es++;
}
} whi l e ( r es ! = vec. end( ) ) ;
}


159

Wie eingangs erwhnt, verfgt die STL ber eine Reihe von Klassen-Templates fr Funkti-
onsobjekte. Hier soll nicht weiter auf diese Klassen eingegangen werden.


copy() und replace()
t empl at e<cl ass I nI t , cl ass Out I t >
Out I t copy( I nI t f i r st , I nI t l ast , Out I t r esul t ) ;

- copy() kopiert alle Elemente im Bereich [first, last) in den Bereich ab result.
- Zurckgeliefert wird der Iterator result +(last-first).
- Da der Zugriff auf die betroffenen Container nur ber Iteratoren erfolgt, muss der Bereich
hinter result bereits speicherplatzmig existieren.

t empl at e<cl ass FwdI t , cl ass T>
voi d replace( FwdI t f i r st , FwdI t l ast ,
const T &vOl d, const T &vNew) ;

- Alle Elemente im Bereich [first, last) mit Wert vOld werden durch den Wert vNew er-
setzt.


Beispiel 15.6: (Algorithmen copy() und replace())

#i ncl ude <i ost r eam>
#i ncl ude <vect or > / / vect or - Cont ai ner
#i ncl ude <l i st > / / l i st - Cont ai ner
#i ncl ude <al gor i t hm> / / copy, r epl ace

usi ng namespace st d ;

t ypedef vect or <i nt > I nt Vec;
t ypedef l i st <i nt > I nt Li st ;

i nt mai n( )
{
I nt Vec vec;
I nt Li st l i st ;
I nt Li st : : i t er at or l i st I t ;

f or ( unsi gned i =0; i <5; i ++)
vec. push_back( 1) ;

l i st . r esi ze( vec. si ze( ) ) ;

copy( vec. begi n( ) , vec. end( ) , l i st . begi n( ) ) ;
r epl ace( l i st . begi n( ) , l i st . end( ) , 1, 2) ;

f or ( l i st I t = l i st . begi n( ) ; l i st I t ! = l i st . end( ) ; l i st I t ++)
cout << *l i st I t << ' ' ;
}

160
15.5 bungsaufgaben

Aufgabe 15.1
Reimplementieren Sie den STL-Algorithmus find().
Hinweise:
(a) Der Prototyp fr den Algorithmus find lautet wie folgt:
t empl at e<cl ass I nI t , cl ass T>
I nI t find( I nI t f i r st , I nI t l ast , const T &val ) ;
(b) Verwenden Sie fr die Reimplementierung einen abweichenden Funktionsnamen.

Aufgabe 15.2
Schreiben Sie ein Programm, welches einen vector-Container mit 10 Zufallszahlen fllt. Der
Inhalt des Containers soll danach mit Hilfe des Algorithmus sort() (siehe Online-Hilfe) ab-
steigend sortiert und nachfolgend ausgegeben werden.

Aufgabe 15.3 (Allgemeine Fragen)
(a) Welche Vorteile haben Funktionsobjekte bei der Realisierung von Prdikaten im Rahmen
der bedingten Suche gegenber Funktionen vom Typ bool?

(b) Warum knnen die Algorithmen in der STL unabhngig vom zugrunde liegenden Contai-
ner implementiert werden?

161
16. Datenstrukturen und Dateien

Die bisherige Betrachtung von Datenstrukturen beschrnkte sich auf deren Ablage im Ar-
beitsspeicher. Diese Sichtweise ist aus zwei Grnden unzureichend:

(1) In sehr vielen Fllen ist es nicht akzeptabel, dass die Daten nach Abschluss des Pro-
gramms verloren gehen. Sie mssen daher langfristig in Dateien gespeichert werden.
Ebenso ist es nicht praktikabel, grere Datenmengen bei jedem Programmstart erneut in
Form von Benutzereingaben zu erfassen. Sie mssen vielmehr von einem permanenten
Speichermedium, also aus Dateien, eingelesen werden knnen.

(2) Aufgrund der begrenzten Kapazitt des Arbeitsspeichers mssen sehr groe Datenbestn-
de in den kapazittsmig greren Peripherspeichern, meist Plattenspeicher, gespeichert
werden. Bei der Abspeicherung ist insbesondere den Erfordernissen einer effizienten Su-
che Rechnung zu tragen. Ein allseits bekanntes Beispiel fr die Relevanz der Suche in
groen Datenbestnden bilden die Datenbanksysteme.


16.1 Persistenz und Serialisierung

Unter Persistenz wird die Fhigkeit verstanden, in Objekten befindliche Daten in nicht-
flchtigen Speichermedien wie Dateisystemen oder Datenbanken zu speichern.

Persistent wird als ein Synonym fr nichtflchtig verwendet, was bedeutet, dass die Da-
ten auch nach Beenden des Programms vorhanden bleiben, und bei erneutem Aufruf des Pro-
gramms wieder rekonstruiert werden knnen. Eine mgliche Technik zur persistenten Spei-
cherung ist die Serialisierung.

Bei der Serialisierung wird ein im Arbeitsspeicher befindliches Objekt in Form eines seri-
ellen Datenstroms in ein persistentes Speichermedium geschrieben. Bei der Deserialisie-
rung wird das Objekt in Form eines seriellen Datenstroms vom persistenten Speicherme-
dium zurck in den Arbeitsspeicher geladen.

Grundstzlich muss entschieden werden, ob die Serialisierung in textueller oder binrer Form
stattfinden soll. Der Vorteil der textuellen Form ist die allgemeine Lesbarkeit der Daten mit
allgemein verfgbaren Programmen (Texteditoren, Programmen zur Tabellenkalkulation).
Bei der binren Form werden die Instanzvariablen eines Objekts in ihrer bestehenden Repr-
sentation 1:1 in den persistenten Speicher bertragen. Dies erfordert oft weniger Speicher-
platz als die texuelle Form, hat jedoch den Nachteil, dass die Daten nur mit dem Programm
gelesen werden knnen, mit dem sie auch abgespeichert wurden.

In jedem Fall mssen die Komponenten eines Objekts in der gleichen Reihenfolge deseriali-
siert werden, in der auch die Serialisierung stattgefunden hat.



162
Serialisierung in C++
Die Serialiserung erfolgt in C++ unter Verwendung der <iostream>- bzw. der <fstream>-
Bibliothek. Konkret mssen fr die zu serialisierenden Objekte die Ein- und Ausgabe-
Operatoren >> und << realisiert werden, wie dies in 8.4 beschrieben wurde.

Im Folgenden wird die Serialisierung am Beispiel eines Objekts vom Typ Student beschrie-
ben:

Beispiel 16.1: (Serialisierung)
cl ass St udent {
unsi gned mat r Nr ;
char vor name[ 20] ;
char nachname[ 20] ;
unsi gned sem;
publ i c:
St udent ( unsi gned mat r Nr , char *vor name, char *nachname, unsi gned sem) ;
St udent ( unsi gned mat r Nr ) ;
St udent ( ) ;
unsi gned key( voi d) ;
bool oper at or ==( St udent &st ) ;
f r i end ost r eam& oper at or <<( ost r eam&st r , St udent &st udent ) ;
f r i end i st r eam& oper at or >>( i st r eam&st r , St udent &st udent ) ;
};

Die Operatoren >> und << realisieren die Serialisierung in textueller Form; d.h die numeri-
schen Instanzvariablen werden in Zeichenketten umgewandelt.


Beispiel 16.1 (Forts.): (Serialisierung)
ost r eam& oper at or <<( ost r eam&st r , St udent &st udent )
{
r et ur n st r << st udent . mat r Nr << " "
<< st udent . vor name << " "
<< st udent . nachname << " "
<< st udent . sem;
}

i st r eam& oper at or >>( i st r eam&st r , St udent &st udent )
{
r et ur n st r >> st udent . mat r Nr
>> st udent . vor name
>> st udent . nachname
>> st udent . sem;
}

Bemerkungen:
- Bei der Serialisierung in textueller Form ist es wichtig, die Zeichenketten der einzelnen
Komponenten durch white spaces (Blanc, Tab oder Carriage Return) zu trennen. Nur so
ist die Deserialisierung der einzelnen Komponenten mglich.
- Die textuelle Form der Serialisierung hat den Vorteil, dass sie auch fr die Standardein-
/ausgabe verwendet werden kann.

163
Das nachfolgende Programm zeigt die Serialisierung an einem Beispiel. Hierbei wird eine
Datei mit serialisierten Daten vom Typ Student eingelesen und auf dem Bildschirm ausgege-
ben.

Beispiel 16.1 (Forts.): (Serialisierung)
#i ncl ude <f st r eam>
#i ncl ude " st udent . h"

usi ng namespace st d

i nt mai n( )
{
i f st r eami n( " St udent s. t xt " ) ;
St udent st ;

whi l e ( ! i n. eof ( ) )
{ i n >> st ;
cout << st << ' \ n' ;
}
r et ur n 0;
}


Die Datei Students.txt hat das folgende fr die Deserialisierung geeignete Format:

Beispiel 16.1 (Forts.): (Serialisierung)
73424 Er nst Ei t el 7
73999 Hi l de Hol d 7
74444 Li sa Li ebl i ch 6
74889 Rudi Rat l os 6
74899 Ludwi g Lust i g 6
75116 Hei ner Hur t i g 5
75124 Hel mut Hast i g 5
76130 Ot t o Ohnesor g 4
76788 Wal t er Wi cht i g 3
77333 Emma Emsi g 3


16.2 Gestreute Speicherung (Hashverfahren)

Die Ablage von groen Listen auf Peripherspeichern erfordert aufgrund der erheblich hheren
Zugriffszeiten bei Plattenspeichern (5-50 ms) gegenber dem Arbeitspeicher (50-250 ns) spe-
zielle Techniken zur Realisierung von effizienten Suchfunktionen. Ein mglicher Ansatz ist
die Verwendung einer fr die Suche optimierten Form der Speicherung.


Direkt adressierbare Speicherung
In vielen Anwendungsfllen werden Suchschlssel synthetisch vergeben, um deren Eindeu-
tigkeit sicherzustellen (z.B. Matrikelnummer, Kontonummer). Eine ideale Situation fr die
Suche ist dann gegeben, wenn ein numerischer Suchschlssel unmittelbar Aufschluss ber die
Position des zugehrigen Datensatzes in einer sequentiell gespeicherten Liste gibt.


164
Seien K = {k1,...,kn} eine Menge von Suchschlsseln und M = {0,1,...,m-1} eine Menge
von Listenpltzen. Es gilt m > n. Die Datenstze mit Schlsselmenge K lassen sich direkt
adressierbar speichern, wenn eine injektive Speicherfunktion s : K -> M existiert.

Speicherfunktionen zur direkt adressierten Speicherung lassen sich in der Praxis nicht immer
finden (z.B. falls der Suchschlssel alphanumerischen ist). Von praktischer Bedeutung ist
eine Speicherfunktion zudem nur dann, wenn sie einen "hohen Grad an Surjektivitt" auf-
weist, da ansonsten ein hoher Speicherverschnitt durch ungenutzte Listenpltze entsteht. Ein
Kompromiss stellt die nachfolgend beschriebene gestreute Speicherung dar.


Hash-Verfahren
Seien K = {k1,...,kn} eine Menge von Suchschlsseln und M = {0,1,...,m-1} eine Menge
von Listenpltzen. Es gilt m > n. Die Datenstze mit Schlsselmenge K sind gestreut ge-
speichert, wenn eine Hash-Funktion s : K -> M existiert, die einen Ausgangspunkt fr ei-
ne vereinfachte, weiterfhrende Suche liefert.

Bemerkungen:
- Aufgrund der nicht zwingend geforderten Injektivitt der Hash-Funktion knnen mehrere
Suchschlssel auf den gleichen Listenplatz abgebildet werden. Man bezeichnet dies als
Kollision.
- Die Kollisionshufigkeit hngt wesentlich vom Fllgrad der Liste ab. Diese ergibt sich aus
dem Quotienten n/m.
- Gewnschte Eigenschaften fr Hash-Funktionen sind:
. eine einfache Berechnung,
. eine "grtmgliche" Injektivitt, was eine geringe Kollisionshufigkeit zur Folge hat.


Divisionsrestverfahren als Hashfunktion
Das hufig als Hash-Funktion eingesetzte Divisionsrestverfahren geht von einer festen An-
zahl von Listenpltzen aus, welche etwa das 1,5-fache der Maximalzahl der Listenelemente
betragen sollte. Empirische Untersuchungen haben gezeigt, dass die Wahl einer Primzahl fr
die Listenlnge zu einer geringeren Kollisionshufigkeit fhrt. Die Hash-Funktion beim Divi-
sionsrestverfahren berechnet sich wie folgt:

Sei k 0 K und m die Anzahl der Listenpltze. Dann gilt h(k) = k % m.


Offene Adressierung zur Auflsung von Kollisionen
Fr die Auflsung von Kollisionen wird hufig ein Verfahren verwendet, welches als offene
Adressierung bezeichnet wird:

Sei h(ki) der fr Schlssel ki berechnete Listenplatz. Falls h(ki) bereits belegt ist, werden
nachfolgend die Listenpltze ((h(ki)+j) mod m) j=1,m (Sondierungsfolge) untersucht. Der
Datensatz wird auf den nchsten freien Listenplatz abgelegt.


165
Das nachfolgende Beispiel zeigt die Studentendatei aus Beispiel 16.1 in einer gestreut gespei-
cherten Liste der Lnge 17. Hierbei kommen das Divisionsrestverfahrens und die offene
Adressierung zur Kollisionsauflsung zum Einsatz. Die Datenstze enthalten neben den ei-
gentlichen Listenelementen ein Statusfeld, welches Aufschluss ber den Belegungszustand
des Datensatzes gibt. Neben den Werten frei und belegt wird der Wert entfernt ben-
tigt. Dieser kennzeichnet den Datensatz eines entfernten Listenelements. Im Falle der Suche
werden Datenstze mit der Kennung entfernt als zur Sondierungsfolge gehrig betrachtet.


Beispiel 16.2: (Gestreute Speicherung)

k h(k) Kollision Matr.-Nr. Name Sem. Status
73424 1 0 77333 Emma Emsig 3 belegt
73999 15 1 73424 Ernst Eitel 7 belegt
74444 1 2 2 74444 Lisa Lieblich 6 belegt
74889 4 3 75124 Helmut Hastig 5 belegt
74899 14 4 74889 Rudi Ratlos 6 belegt
75116 10 5 76130 Otto Ohnesorg 4 belegt
75124 1 2 3 6 frei
76130 4 5 7 frei
76788 16 8 frei
77333 0 9 frei
10 75116 Heiner Hurtig 5 belegt
11 frei
12 frei
13 frei
14 74899 Ludwig Lustig 6 belegt
15 73999 Hilde Hold 7 belegt
16 76788 Walter Wichtig 3 belegt



16.2.1 Abstraktion

Im Folgenden wird die Schnittstelle einer gestreut gespeicherten Liste mit Ablage in einer
Datei spezifiziert. Die Schnittstelle wird in Form eines ADT namens HashFile als Template
angegeben.

166
Beispiel 16.2: (ADT Hashfile)
t empl at e<cl ass AD>
cl ass HashFi l e {
publ i c:
bool cr eat eHashFi l e( char *f i l eName, unsi gned l engt h) ;
bool openHashFi l e( char *f i l eName) ;
voi d cl oseHashFi l e( voi d) ;
bool i nser t ( AD &el ) ;
AD *sear ch( AD &el ) ;
bool r emove( AD &el ) ;
};


Bemerkungen:
- Der Template-Parameter AD (Application Data) steht fr den Typ der in der Hash-Datei
gespeicherten Listenelemente.
- Der Typ AD muss einen ganzzahliger Schlssel enthalten, der ber die Zugriffsfunktion
key() abrufbar ist. Ferner wird ein Standardkonstruktor, die Mglichkeit zur Wertzuwei-
sung sowie der Operator == (quivalenz) gefordert.
- Die Funktion createHashFile() legt eine Hash-Datei mit Dateinamen fileName an und ini-
tialisiert length viele Listenelemente mittels des Standardkonstruktors von AD.
- Die Funktion openHashFile() ffnet die Hash-Datei zum nachfolgenden Lesen und Schrei-
ben im Binrmodus.
- closeHashFile() schliet die Hash-Datei.
- insert() trgt das Listenelement el in die Hash-Datei ein.
- Die Funktion search() sucht nach dem Listenelement, dessen Schlssel mit dem von el
quivalent ist. Im Erfolgsfall wird ein Zeiger auf das gesuchte Listenelement zurck gelie-
fert. Ansonsten liefert search() den Zeiger NULL zurck.
- Die Funktion remove() entfernt das Listenelement, dessen Schlssel mit dem Schlssel von
el quivalent ist.


16.2.2 Implementierung

Bevor die Implementierung der Operationen des ADT HashFile beschrieben werden, wird
auf die Organisation der Hash-Datei und die elementaren Lese-/Schreibzugriffe eingegangen.


Dateiorganisation und Dateizugriff
Fr den Zugriff zu den einzelnen Datenstzen wird ein Random-Zugriff auf die Hash-Datei
bentigt. Hierzu muss der Lese- bzw. Schreibzhler auf die Byte-Adresse eines Datensatzes
positioniert werden. Voraussetzung fr eine einfache Berechnung dieser Byte-Adresse ist eine
konstante Lnge der Datenstze. Dieses lsst sich am einfachsten durch eine 1:1-bertragung
der Objektreprsentation zwischen Arbeits- und Peripherspeicher erreichen.

Die Satzstruktur der Hash-Datei ist aus der Implementierung des ADT HashFile zu entneh-
men. Ein Satz entspricht dem Speicherabbild der Struktur DataRec. Er enthlt das eigentli-
che Listenelement sowie eine Kennung vom Typ Status, welche den Belegungszustand des
Datensatzes (FREI, BELEGT, ENTFERNT) kennzeichnet.

167
Beispiel 16.2 (Forts:): (HashFile)
t empl at e<cl ass AD>
cl ass HashFi l e {
enumSt at us { FREI =- 3, BELEGT, ENTFERNT };
st r uct Dat aRec {
AD el ;
St at us st at ;
};

f st r eamhf ; / / St r eam- Obj ekt
unsi gned l en; / / Lnge der Hash- Dat ei
Dat aRec buf f er ; / / Lese- / Schr ei b- Puf f er
publ i c:
/ / . . .
};

Fr den satzorientierten Dateizugriff werden die folgenden Methoden der Klassen istream
bzw. ostream bentigt:

i st r eam& i st r eam: : seekg( unsi gned pos) ;
ost r eam& ost r eam: : seekp( unsi gned pos) ;
i st r eam& i st r eam: : r ead( char *buf , unsi gned count ) ;
ost r eam& ost r eam: : wr i t e( char *buf , unsi gned count ) ;

- seekg() setzt den Lesezeiger des Eingabe-Streams auf die Byteposition pos.
- seekp() setzt den Schreibzeiger des Ausgabe-Streams auf die Byteposition pos.
- read() liest count Bytes ab der aktuellen Position des Lesezeigers aus dem Eingabe-
Stream und legt diese ab der Adress buf im Arbeitsspeicher ab.
- write() schreibt count Bytes, welche ab der Adresse buf im Arbeitsspeicher liegen, ab der
aktuellen Position des Schreibzeigers in den Ausgabe-Stream.


Einfgen eines Datensatzes
Beispiel 16.2 (Forts): (Einfgen)
t empl at e<cl ass AD>
bool HashFi l e<AD>: : i nser t ( AD &el )
{
i nt pos = el . key( ) %l en;

i f ( i f sear ch( el ) )
r et ur n f al se; / / Dat ensat z ber ei t s vor handen
hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
whi l e ( buf f er . st at == BELEGT)
{ pos = ( pos+1) %l en;
hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
}
buf f er . el = el ;
buf f er . st at = BELEGT;
hf . seekp( pos * si zeof ( Dat aRec) ) ;
hf . wr i t e( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
r et ur n t r ue;
}

168
Bemerkungen:
- Falls sich im Rahmen der Suche herausstellt, dass bereits ein Listenelement mit quivalen-
tem Schlssel existiert, wird die Funktion erfolglos abgebrochen.
- Der Hash-Wert des Listenelements el wird berechnet.
- Sodann werden die ab dem Hash-Wert beginnenden Datenstze gem der Sondierungs-
folge nach einem freien Datensatz durchsucht.
- Das einzufgende Listenelement wird im ersten freien Datensatz abgelegt. Der Datensatz
wird als BELEGT gekennzeichnet.


Entfernen eines Datensatzes
Beispiel 16.2 (Forts): (Entfernen)

t empl at e<cl ass AD>
bool HashFi l e<AD>: : r emove( AD &el )
{
i nt pos = el . key( ) %l en;

hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
whi l e ( ( buf f er . st at == BELEGT && ! ( buf f er . el == el ) )
| | buf f er . st at == ENTFERNT )
{ pos = ( pos+1) %l en;
hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
}
i f ( buf f er . el == el )
{ buf f er . el = St udent ( ) ; / / nur f r Debug- Zwecke
buf f er . st at = ENTFERNT; / / al s gel scht mar ki er en
hf . seekp( pos * si zeof ( Dat aRec) ) ;
hf . wr i t e( ( char *) &buf f er , si zeof ( Dat aRec) ) ; / / zur ck schr ei ben
r et ur n t r ue;
}
el se
r et ur n f al se;
}

Bemerkungen:
- Der Hash-Wert des Listenelements el wird berechnet.
- Sodann werden die ab dem Hash-Wert beginnenden Datenstze gem der Sondierungs-
folge nach einem Listenelement durchsucht, dessen Schlssel mit dem von el berein-
stimmt. Hierbei brechen als ENTFERNT markierte Datenstze die Suche nicht ab.
- Falls das gesuchte Listenelement in der Sondierungsfolge gefunden wird, wird der Daten-
satz mit dem Status ENTFERNT zurck geschrieben.
- Falls die Suche gem der Sondierungsreihenfolge erfolglos ist, dies ist bei Erreichen eines
als FREI gekennzeichneten Datensatzes der Fall, wird die Funktion mit dem Rckgabewert
false beendet.

169
Suchen eines Datensatzes
Beispiel 16.2 (Forts): (Suchen)
t empl at e <cl ass AD>
AD *HashFi l e<AD>: : sear ch( AD &el )
{
i nt pos = el . key( ) %l en;

hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
whi l e ( ( buf f er . st at == BELEGT && ! ( buf f er . el == el ) )
| | buf f er . st at == ENTFERNT )
{ pos = ( pos+1) %l en;
hf . seekg( pos * si zeof ( Dat aRec) ) ;
hf . r ead( ( char *) &buf f er , si zeof ( Dat aRec) ) ;
}
i f ( buf f er . el == el )
r et ur n &( buf f er . el ) ;
el se
r et ur n NULL; / / ni cht gef unden
}

Bemerkungen:
- Der Hash-Wert des Listenelements el wird berechnet.
- Sodann werden die ab dem Hash-Wert beginnenden Datenstze gem der Sondierungs-
folge nach einem Listenelement durchsucht, dessen Schlssel mit dem von el berein-
stimmt. Hierbei brechen als ENTFERNT markierte Datenstze die Suche nicht ab.
- Falls das gesuchte Listenelement in der Sondierungsfolge gefunden wird, liefert die Funk-
tion einen Zeiger auf die in der Instanzvariablen buffer befindliche Kopie des gefundenen
Listenelements zurck.
- Falls die Suche gem der Sondierungsreihenfolge erfolglos ist, dies ist bei Erreichen eines
als FREI gekennzeichneten Datensatzes der Fall, wird die Funktion mit dem Rckgabewert
NULL beendet.

170
16.3 bungsaufgaben

Aufgabe 16.1
Die Schlsselfolge K = (125, 77, 65 ,1001, 23,14) sollen in einer Hash-Datei der Lnge 9
abgespeichert werden. Als Hash-Funktion kommt das Divisionsrestverfahren zur Anwen-
dung. Die Kollisionsauflsung geschieht durch offene Adressierung.

(a) Bestimmen Sie die Belegung der Datenstze.
(b) Bestimmen Sie die mittlere Zahl der erforderlichen Dateizugriffe bei der erfolgreichen
Suche.


Aufgabe 16.2 (Allgemeine Fragen)
(a) Warum lsst sich die direkt adressierte Speicherung nicht in allen Anwendungsfllen
einsetzen?

(b) Nennen Sie eine wesentliche Eigenschaft einer guten Hash-Funktion.

(c) Welche Bedeutung hat der Fllgrad einer Liste bei gestreuter Speicherung?

(c) Warum muss beim Hash-Verfahren mit offener Adressierung mindestens ein Listenplatz
frei bleiben?

171
Literatur

(a) Programmierung in C++

Stroustrup, B.
Die C++ Programmiersprache
Addison Wesley, 2009

Lippman, S.B. ; Lajoie, J.
C++ Primer
Addison-Wesley, 2007

Breymann, U.
C++ - Eine Einfhrung
Hanser, 2007

Willms, A.
C++ Programmierung, 2 Aufl.
Addison Wesley, 2003


(b) C++ Standard-Bibliothek (STL)

Musser, D.R.; Saini, A.
STL Tutorial and Reference Guide
Addison Wesley, 2001

Kuhlins, S.; Schader, M.
Die C++-Standardbibliothek
Springer, 2005


(c) Datenstrukturen und Algorithmen

Re, H.; Viebeck, G.
Datenstrukturen und Algorithem in C++
Hanser 2003

Sedgewick, R.
Algorithmen in C++
Addison Wesley, 2002



172
Index

Ableitung 2
virtuelle 56
Ableitung
virtuelle 56
Adapter-Container 149
Adjazenzmatrix 142
Adressierung
offene 164
Adressparameter 8
ADT 102
Aggregation 27
Algol 3
Algorithmen 155
Ausgabeoperator 15, 85
Backus-Naur-Notation 4
Basisklasse 2, 40
virtuelle 56
Baum 103
Baum 132
Bereichsoperator 11
Bidirectional-Iterator 154
Binrbaum 132, 134
Binden
dynamisches 47
sptes 3, 47
Bltter 132
BNF 4
Botschaft 1
C++ 3
catch 77
class 19
const 6
Container 38
copy() 159
Copy-Konstruktor 68
Datenkapselung 2, 18
Datentyp
atomarer 101
einfach 101
elementarer 101
strukturierter 101
Datentyp
abstrakter 102
Default-Destruktor 24
Default-Konstruktor 22, 23
delete 14
deque 149
Deserialisierung 161
Destruktor 24
Divisionsrestverfahren 164
Downcast 45
Eingabeoperator 15
Eingabeoperatoren 86
Elementfunktion
rein virtuelle 49
virtuelle 46
Elementfunktionen 18
Elementinitialisierungsliste 23
Elementinitialisierungsliste 28
Elementobjekt 27
Ellipse 78
erben 2, 39
Exception-Handler 77
Exceptions 77
Exception-Spezifikation 80
FIFO-Prinzip 128
find() 156
find_if() 157
Flags 91
friend-Funktion 34
Fllgrad 164
Funktionsobjekt 150, 157
Funktions-Template 71
Get-Operation 128
Graph 103
gerichteter 142
symmetrisch 142
ungerichtet 142
Hash-Funktion 164
Information Hiding 18
Initialisierungsliste 25
Initialisierungsliste 42
Inline-Funktionen 10
Inorder Reihenfolge 139
Instanz 20
Instanzvariablen 18
IO-Streams 84
istream 85
Iterator 38
Java 3
Kanten 142
Kanten 132
Kellerspeicher 125
Klasse 2, 18
abgeleitete 2, 40
abstrakte 49
Klassenobjekte 20
Klassen-Templates 74
Knoten 142
Knoten 132
Kollision 164
Kommentar 5
Komposition 27
Konstruktor 22
Kopf 128
LIFO-Prinzip 125
list 149
Liste
doppelt verkettet 123
lineare 104
sequentiell gespeichert 102
sequenziell gespeichert 104
verkettet gespeichert 102, 118
Listenanker 118
Manipulator 93
map 149
member functions 18
Methode 1

173
Methoden 18
Methodentabelle
virtuelle 47
multmap 149
multset 149
Name
qualifizierter 13
name space 12
Namensraum 12
namespace 12
new 14
Objective C 3
Objekt 1, 20
O-Notation 111
Operatorfunktionen 61
Parameter
vorbesetzte 10
Pivotelement 113
Polymorphismus 3, 47
pop-Operation 125
Postorder Reihenfolge 139
Preorder Reihenfolge 139
priority_queue 149
private 19
protected 43
public 19
push-Operation 125
put-Operation 128
queue 149
Queue 102, 128
Quicksort 113
Rang 132
Referenz 7
Referenzoperator 7
Referenzparameter 8
replace() 159
Ringspeicher 129
Schlange 102, 128
Schnittstelle 102
Schwanz 128
scope resolution operator 11
Senke 148
set 149
Simula 67 3
Smalltalk 3
Sondierungsfolge 164
sortiert 113
Spannbaum 141, 143
Speicherung
direkt adressierbar 164
gestreut 164
Spezialisierung 2
stack 149
Stack 125
Standard Template Library 149
Standard-Copy-Konstruktor 68
Standarddatenstrukturen 102
Standardmanipulator 93
Standard-Namensraum 12
Stapel 125
static-Komponenten 35
STL 149
Stream 15
Suchbaum 133
Suche
binre 109
lineare 109
sequenzielle 109
Suchschlssel 109
Template 71
Templatefunktion 71
Template-Klassen 74
throw 78
throw-Klausel 80
Tiefensuche 141, 143
top of stack 125
try 77
berladen
von Operatoren 60
berladen von Funktionen 9
using-Direktive 13
Vererbung 39
VMT 47
VMT-Zeiger 47
Weg 141, 143
Weg 132
Weglnge 141, 143
Wurzel 132
Zykel 141, 143

Das könnte Ihnen auch gefallen