Sie sind auf Seite 1von 628

Sandini Bib

Objektorientiertes Programmieren in C++


Sandini Bib

Programmer’s Choice
Sandini Bib

Nicolai Josuttis

Objektorientiertes
Programmieren in C++
Ein Tutorial für Ein- und Umsteiger

2., aktualisierte und völlig überarbeitete Auflage

An imprint of Pearson Education


München • Boston • San Francisco • Harlow, England
Don Mills, Ontario • Sydney • Mexico City
Madrid • Amsterdam
Sandini Bib

Die Deutsche Bibliothek – CIP–Einheitsaufnahme

Ein Titeldatensatz für diese Publikation ist bei


Der Deutschen Bibliothek erhältlich.

Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht.
Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten
und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen
werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische
Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind
Verlag und Herausgeber dankbar.
Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien.
Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig.
Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig auch
eingetragene Warenzeichen oder sollten als solche betrachtet werden.
Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor
Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.

10 9 8 7 6 5 4 3 2 1
03 02 01

ISBN 3-8273-1771-1

c 2001 Addison–Wesley Verlag,


ein Imprint der Pearson Education Deutschland GmbH,
Martin-Kollar-Straße 10–12, 81829 München/Germany
Alle Rechte vorbehalten
Einbandgestaltung: Christine Rechl, München


Titelbild: Euphorbia pithyus, Wolfsmilch,
c Karl Blossfeld Archiv – Ann und Jürgen Wilde, Zülpich/VG Bild-Kunst Bonn, 2001
Lektorat: Susanne Spitzer, susanne.spitzer@gmx.net
Korrektorat: Friederike Daenecke, Zülpich
Herstellung: TYPisch Müller, Arcevia, Italien, typmy@freefast.it
Satz: Nicolai Josuttis, Braunschweig
Druck und Verarbeitung: Kösel, Kempten (www.KoeselBuch.de)
Printed in Germany
Sandini Bib

Inhaltsverzeichnis

Vorwort zur zweiten Auflage 1

Vorwort zur ersten Auflage 2

1 Über dieses Buch 3


1.1 Warum dieses Buch? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Voraussetzungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Systematik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Wie liest man dieses Buch? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Zugriff auf die Quellen zu den Beispielen . . . . . . . . . . . . . . . . . . . . . 6
1.6 Anregungen und Kritik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

2 Einleitung: C++ und objektorientierte Programmierung 9


2.1 Die Sprache C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.1 Designkriterien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.2 Sprachversionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 C++ als objektorientierte Programmiersprache . . . . . . . . . . . . . . . . . . 10
2.2.1 Objekte, Klassen und Instanzen . . . . . . . . . . . . . . . . . . . . . . 11
2.2.2 Klassen in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.3 Datenkapselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.4 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.5 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3 Weitere Konzepte von C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3.1 Ausnahmebehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.3.2 Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3.3 Namensbereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.4 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

v
Sandini Bib

vi Inhaltsverzeichnis

3 Grundkonzepte von C++-Programmen 25


3.1 Das erste Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.1.1 Hallo, Welt!“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

3.1.2 Kommentare in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.1.3 Hauptfunktion main() . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.1.4 Ein-/Ausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.1.5 Namensbereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.1.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.2 Datentypen, Operatoren, Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . 33
3.2.1 Ein erstes Programm, das wirklich etwas berechnet . . . . . . . . . . . 33
3.2.2 Fundamentale Datentypen . . . . . . . . . . . . . . . . . . . . . . . . 36
3.2.3 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2.4 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.3 Funktionen und Module . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.3.1 Headerdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.3.2 Quelldatei mit der Implementierung . . . . . . . . . . . . . . . . . . . 52
3.3.3 Quelldatei mit dem Aufruf . . . . . . . . . . . . . . . . . . . . . . . . 52
3.3.4 Übersetzen und binden . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.3.5 Dateiendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
3.3.6 Systemdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.3.7 Präprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.3.8 Namensbereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
3.3.9 Das Schlüsselwort static . . . . . . . . . . . . . . . . . . . . . . . . 60
3.3.10 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.4 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.4.1 Ein erstes einfaches Beispielprogramm mit Strings . . . . . . . . . . . 63
3.4.2 Ein weiteres Beispielprogramm mit Strings . . . . . . . . . . . . . . . 66
3.4.3 String-Operationen im Überblick . . . . . . . . . . . . . . . . . . . . . 71
3.4.4 Strings und C-Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
3.4.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
3.5 Verarbeitung von Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
3.5.1 Beispielprogramm mit Vektoren . . . . . . . . . . . . . . . . . . . . . 74
3.5.2 Beispielprogramm mit Deques . . . . . . . . . . . . . . . . . . . . . . 76
3.5.3 Vektor versus Deque . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
3.5.4 Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.5.5 Beispielprogramm mit einer Liste . . . . . . . . . . . . . . . . . . . . 80
3.5.6 Beispielprogramme mit assoziativen Containern . . . . . . . . . . . . . 81
3.5.7 Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.5.8 Algorithmen mit mehreren Bereichen . . . . . . . . . . . . . . . . . . 90
Sandini Bib

Inhaltsverzeichnis vii

3.5.9 Stream-Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.5.10 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
3.5.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
3.6 Ausnahmebehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.6.1 Motivation für das Konzept der Ausnahmebehandlung . . . . . . . . . . 98
3.6.2 Das Konzept der Ausnahmebehandlung . . . . . . . . . . . . . . . . . 100
3.6.3 Standard-Ausnahmeklassen . . . . . . . . . . . . . . . . . . . . . . . . 101
3.6.4 Ausnahmebehandlung am Beispiel . . . . . . . . . . . . . . . . . . . . 101
3.6.5 Behandlung nicht abgefangener Ausnahmen . . . . . . . . . . . . . . . 106
3.6.6 Hilfsfunktionen zur Behandlung von Ausnahmen . . . . . . . . . . . . 107
3.6.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
3.7 Zeiger, Felder und C-Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.7.1 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
3.7.2 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
3.7.3 C-Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
3.7.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.8 Freispeicherverwaltung mit new und delete . . . . . . . . . . . . . . . . . . . 120
3.8.1 Der Operator new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
3.8.2 Der Operator delete . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
3.8.3 Dynamische Speicherverwaltung für Felder . . . . . . . . . . . . . . . 122
3.8.4 Fehlerbehandlung für new . . . . . . . . . . . . . . . . . . . . . . . . . 124
3.8.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
3.9 Kommunikation mit der Außenwelt . . . . . . . . . . . . . . . . . . . . . . . . 126
3.9.1 Argumente aus dem Programmaufruf . . . . . . . . . . . . . . . . . . . 126
3.9.2 Zugriff auf Umgebungsvariablen . . . . . . . . . . . . . . . . . . . . . 127
3.9.3 Abbruch von Programmen . . . . . . . . . . . . . . . . . . . . . . . . 128
3.9.4 Aufruf von weiteren Programmen . . . . . . . . . . . . . . . . . . . . 129
3.9.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

4 Programmieren von Klassen 131


4.1 Die erste Klasse: Bruch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
4.1.1 Vorüberlegungen zur Implementierung . . . . . . . . . . . . . . . . . . 132
4.1.2 Deklaration der Klasse Bruch . . . . . . . . . . . . . . . . . . . . . . 135
4.1.3 Die Klassenstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.1.4 Elementfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.1.5 Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.1.6 Überladen von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . 141
4.1.7 Implementierung der Klasse Bruch . . . . . . . . . . . . . . . . . . . . 142
4.1.8 Anwendung der Klasse Bruch . . . . . . . . . . . . . . . . . . . . . . 147
4.1.9 Erzeugung temporärer Objekte . . . . . . . . . . . . . . . . . . . . . . 153
4.1.10 UML-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
4.1.11 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Sandini Bib

viii Inhaltsverzeichnis

4.2 Operatoren für Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156


4.2.1 Deklaration von Operatorfunktionen . . . . . . . . . . . . . . . . . . . 156
4.2.2 Implementierung von Operatorfunktionen . . . . . . . . . . . . . . . . 159
4.2.3 Anwendung von Operatorfunktionen . . . . . . . . . . . . . . . . . . . 166
4.2.4 Globale Operatorfunktionen . . . . . . . . . . . . . . . . . . . . . . . 168
4.2.5 Grenzen bei der Definition eigener Operatoren . . . . . . . . . . . . . . 169
4.2.6 Besonderheiten spezieller Operatoren . . . . . . . . . . . . . . . . . . 170
4.2.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
4.3 Laufzeit- und Codeoptimierungen . . . . . . . . . . . . . . . . . . . . . . . . . 175
4.3.1 Die Klasse Bruch mit ersten Optimierungen . . . . . . . . . . . . . . . 175
4.3.2 Default-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
4.3.3 Inline-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
4.3.4 Optimierungen aus Anwendersicht . . . . . . . . . . . . . . . . . . . . 182
4.3.5 Using-Direktiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
4.3.6 Deklarationen zwischen Anweisungen . . . . . . . . . . . . . . . . . . 185
4.3.7 Copy-Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
4.3.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
4.4 Referenzen und Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
4.4.1 Copy-Konstruktor und Parameterübergabe . . . . . . . . . . . . . . . . 189
4.4.2 Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
4.4.3 Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
4.4.4 Konstanten-Elementfunktionen . . . . . . . . . . . . . . . . . . . . . . 195
4.4.5 Die Klasse Bruch mit Referenzen . . . . . . . . . . . . . . . . . . . . 195
4.4.6 Zeiger auf Konstanten und Zeigerkonstanten . . . . . . . . . . . . . . . 199
4.4.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
4.5 Ein- und Ausgabe mit Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
4.5.1 Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
4.5.2 Umgang mit Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
4.5.3 Zustand von Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
4.5.4 I/O-Operatoren für eigene Datentypen . . . . . . . . . . . . . . . . . . 212
4.5.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
4.6 Freunde und andere Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
4.6.1 Automatische Typumwandlungen . . . . . . . . . . . . . . . . . . . . . 224
4.6.2 Schlüsselwort explicit . . . . . . . . . . . . . . . . . . . . . . . . . 226
4.6.3 Friend-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
4.6.4 Konvertierungsfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . 234
4.6.5 Probleme bei der automatischen Typumwandlung . . . . . . . . . . . . 235
4.6.6 Andere Anwendungen des Schlüsselworts friend . . . . . . . . . . . . 238
4.6.7 friend kontra objektorientierte Programmierung . . . . . . . . . . . . 239
4.6.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Sandini Bib

Inhaltsverzeichnis ix

4.7 Ausnahmebehandlung für Klassen . . . . . . . . . . . . . . . . . . . . . . . . . 241


4.7.1 Motivation für eine Ausnahmebehandlung in der Klasse Bruch . . . . . 241
4.7.2 Ausnahmebehandlung am Beispiel der Klasse Bruch . . . . . . . . . . 242
4.7.3 Fehlerklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
4.7.4 Weitergabe von Fehlern . . . . . . . . . . . . . . . . . . . . . . . . . . 251
4.7.5 Ausnahmen in Destruktoren . . . . . . . . . . . . . . . . . . . . . . . 251
4.7.6 Ausnahmen in Schnittstellen-Deklarationen . . . . . . . . . . . . . . . 252
4.7.7 Hierarchien von Fehlerklassen . . . . . . . . . . . . . . . . . . . . . . 253
4.7.8 Design von Fehlerklassen . . . . . . . . . . . . . . . . . . . . . . . . . 257
4.7.9 Standard-Ausnahmen auslösen . . . . . . . . . . . . . . . . . . . . . . 258
4.7.10 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259

5 Vererbung und Polymorphie 261


5.1 Einfache Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
5.1.1 Die Klasse Bruch als Basisklasse . . . . . . . . . . . . . . . . . . . . . 263
5.1.2 Vorüberlegungen zur abgeleiteten Klasse KBruch . . . . . . . . . . . . 266
5.1.3 Deklaration der abgeleiteten Klasse KBruch . . . . . . . . . . . . . . . 267
5.1.4 Vererbung und Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . 270
5.1.5 Implementierung von abgeleiteten Klassen . . . . . . . . . . . . . . . . 273
5.1.6 Anwendung von abgeleiteten Klassen . . . . . . . . . . . . . . . . . . 276
5.1.7 Konstruktoren für Objekte der Basisklasse . . . . . . . . . . . . . . . . 278
5.1.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
5.2 Virtuelle Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
5.2.1 Probleme beim Überschreiben von Funktionen der Basisklasse . . . . . 280
5.2.2 Statisches und dynamisches Binden von Funktionen . . . . . . . . . . . 283
5.2.3 Überladen kontra Überschreiben . . . . . . . . . . . . . . . . . . . . . 288
5.2.4 Zugriff auf Parameter der Basisklasse . . . . . . . . . . . . . . . . . . 289
5.2.5 Virtuelle Destruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . 290
5.2.6 Vererbung richtig angewendet . . . . . . . . . . . . . . . . . . . . . . 291
5.2.7 Weitere Fallen beim Überschreiben von Funktionen . . . . . . . . . . . 297
5.2.8 Private Vererbung und reine Zugriffsdeklarationen . . . . . . . . . . . . 299
5.2.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
5.3 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
5.3.1 Was ist Polymorphie? . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
5.3.2 Polymorphie in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
5.3.3 Polymorphie in C++ an einem Beispiel . . . . . . . . . . . . . . . . . . 306
5.3.4 Die abstrakte Basisklasse GeoObj . . . . . . . . . . . . . . . . . . . . 310
5.3.5 Anwendung von Polymorphie in Klassen . . . . . . . . . . . . . . . . . 319
5.3.6 Polymorphie ist keine feste Fallunterscheidung . . . . . . . . . . . . . 325
5.3.7 Rückumwandlung eines Objekts in seine Klasse . . . . . . . . . . . . . 326
5.3.8 Design by Contract . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
5.3.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Sandini Bib

x Inhaltsverzeichnis

5.4 Mehrfachvererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333


5.4.1 Beispiel für Mehrfachvererbung . . . . . . . . . . . . . . . . . . . . . 333
5.4.2 Virtuelle Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
5.4.3 Das Problem der Identität . . . . . . . . . . . . . . . . . . . . . . . . . 343
5.4.4 Dieselbe Basisklasse mehrfach ableiten . . . . . . . . . . . . . . . . . 346
5.4.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
5.5 Design-Fallen bei der Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . 348
5.5.1 Vererbung kontra Verwendung . . . . . . . . . . . . . . . . . . . . . . 348
5.5.2 Design-Fehler: Einschränkende Vererbung . . . . . . . . . . . . . . . . 348
5.5.3 Design-Fehler: Wertverändernde Vererbung . . . . . . . . . . . . . . . 350
5.5.4 Design-Fehler: Wertinterpretierende Vererbung . . . . . . . . . . . . . 351
5.5.5 Vermeide Vererbung!“ . . . . . . . . . . . . . . . . . . . . . . . . . . 352

5.5.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353

6 Dynamische und statische Komponenten 355


6.1 Dynamische Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
6.1.1 Implementierung der Klasse String . . . . . . . . . . . . . . . . . . . 356
6.1.2 Konstruktoren bei dynamischen Komponenten . . . . . . . . . . . . . . 362
6.1.3 Implementierung eines Copy-Konstruktors . . . . . . . . . . . . . . . . 364
6.1.4 Destruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364
6.1.5 Implementierung des Zuweisungsoperators . . . . . . . . . . . . . . . 365
6.1.6 Weitere Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
6.1.7 Einlesen eines Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
6.1.8 Kommerzielle Implementierung von String-Klassen . . . . . . . . . . . 372
6.1.9 Weitere Anwendungsmöglichkeiten dynamischer Komponenten . . . . 374
6.1.10 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
6.2 Weitere Aspekte dynamischer Komponenten . . . . . . . . . . . . . . . . . . . 377
6.2.1 Dynamische Komponenten bei konstanten Objekten . . . . . . . . . . . 377
6.2.2 Konvertierungsfunktionen für dynamische Komponenten . . . . . . . . 380
6.2.3 Konvertierungsfunktionen für Bedingungen . . . . . . . . . . . . . . . 382
6.2.4 Konstanten werden zu Variablen . . . . . . . . . . . . . . . . . . . . . 385
6.2.5 Vordefinierte Funktionen verbieten . . . . . . . . . . . . . . . . . . . . 388
6.2.6 Proxy-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
6.2.7 Ausnahmebehandlung mit Parametern . . . . . . . . . . . . . . . . . . 391
6.2.8 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
6.3 Vererbung von Klassen mit dynamischen Komponenten . . . . . . . . . . . . . 397
6.3.1 Die Klasse Bsp::String als Basisklasse . . . . . . . . . . . . . . . . 397
6.3.2 Die abgeleitete Klasse FarbString . . . . . . . . . . . . . . . . . . . 399
6.3.3 Ableiten von Friend-Funktionen . . . . . . . . . . . . . . . . . . . . . 402
6.3.4 Quelldatei der abgeleiteten Klasse FarbString . . . . . . . . . . . . . 405
6.3.5 Anwendung der Klasse FarbString . . . . . . . . . . . . . . . . . . . 406
6.3.6 Ableiten der Spezialfunktionen für dynamische Komponenten . . . . . 407
6.3.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
Sandini Bib

Inhaltsverzeichnis xi

6.4 Klassen verwenden Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410


6.4.1 Objekte als Komponenten anderer Klassen . . . . . . . . . . . . . . . . 410
6.4.2 Implementierung der Klasse Person . . . . . . . . . . . . . . . . . . . 410
6.4.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
6.5 Statische Komponenten und Hilfstypen . . . . . . . . . . . . . . . . . . . . . . 418
6.5.1 Statische Klassenkomponenten . . . . . . . . . . . . . . . . . . . . . . 418
6.5.2 Typdeklarationen innerhalb von Klassen . . . . . . . . . . . . . . . . . 424
6.5.3 Aufzählungstypen als statische Klassenkonstanten . . . . . . . . . . . . 427
6.5.4 Eingebettete und lokale Klassen . . . . . . . . . . . . . . . . . . . . . 428
6.5.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429

7 Templates 431
7.1 Motivation für Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
7.1.1 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
7.2 Funktionstemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433
7.2.1 Definition von Funktionstemplates . . . . . . . . . . . . . . . . . . . . 433
7.2.2 Aufruf von Funktionstemplates . . . . . . . . . . . . . . . . . . . . . . 434
7.2.3 Praktische Hinweise zum Umgang mit Templates . . . . . . . . . . . . 435
7.2.4 Automatische Typumwandlung bei Templates . . . . . . . . . . . . . . 435
7.2.5 Überladen von Templates . . . . . . . . . . . . . . . . . . . . . . . . . 436
7.2.6 Lokale Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
7.2.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
7.3 Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
7.3.1 Implementierung des Klassentemplate Stack . . . . . . . . . . . . . . 440
7.3.2 Anwendung des Klassentemplate Stack . . . . . . . . . . . . . . . . . 444
7.3.3 Spezialisieren von Klassentemplates . . . . . . . . . . . . . . . . . . . 445
7.3.4 Default Template-Parameter . . . . . . . . . . . . . . . . . . . . . . . 448
7.3.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
7.4 Werte als Template-Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
7.4.1 Beispiel für die Verwendung von Werte-Parametern . . . . . . . . . . . 452
7.4.2 Einschränkungen bei Werte-Parametern . . . . . . . . . . . . . . . . . 454
7.4.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
7.5 Weitere Aspekte von Templates . . . . . . . . . . . . . . . . . . . . . . . . . . 456
7.5.1 Das Schlüsselwort typename . . . . . . . . . . . . . . . . . . . . . . . 456
7.5.2 Komponenten als Templates . . . . . . . . . . . . . . . . . . . . . . . 457
7.5.3 Statische Polymorphie mit Templates . . . . . . . . . . . . . . . . . . . 460
7.5.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
7.6 Templates in der Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
7.6.1 Übersetzen von Template-Code . . . . . . . . . . . . . . . . . . . . . . 465
7.6.2 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470
7.6.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472
Sandini Bib

xii Inhaltsverzeichnis

8 Die Standard-I/O-Bibliothek im Detail 473


8.1 Die Standard-Stream-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . 474
8.1.1 Stream-Klassen und -Objekte . . . . . . . . . . . . . . . . . . . . . . . 474
8.1.2 Fehlerzustände, Stream-Status . . . . . . . . . . . . . . . . . . . . . . 476
8.1.3 Standardoperatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
8.1.4 Standardfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
8.1.5 Manipulatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
8.1.6 Formatdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 486
8.1.7 Setzen und Abfragen von Formatflags . . . . . . . . . . . . . . . . . . 486
8.1.8 Internationalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
8.1.9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 499
8.2 Dateizugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
8.2.1 Stream-Klassen für Dateien . . . . . . . . . . . . . . . . . . . . . . . . 500
8.2.2 Beispiel für die Verwendung der Stream-Klassen für Dateien . . . . . . 500
8.2.3 Datei-Flags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503
8.2.4 Explizites Öffnen und Schließen . . . . . . . . . . . . . . . . . . . . . 504
8.2.5 Wahlfreier Zugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
8.2.6 Umleiten der Standardkanäle in Dateien . . . . . . . . . . . . . . . . . 508
8.2.7 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
8.3 Stream-Klassen für Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511
8.3.1 String-Stream-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . 511
8.3.2 Lexical-Cast-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . 514
8.3.3 char*-Stream-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . 516
8.3.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518

9 Weitere Sprachmittel und Details 519


9.1 Weitere Details zur Standardbibliothek . . . . . . . . . . . . . . . . . . . . . . 520
9.1.1 Operationen von Vektoren . . . . . . . . . . . . . . . . . . . . . . . . 520
9.1.2 Gemeinsame Operationen aller STL-Container . . . . . . . . . . . . . . 526
9.1.3 Liste aller STL-Algorithmen . . . . . . . . . . . . . . . . . . . . . . . 528
9.1.4 Numerische Limits . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
9.1.5 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 538
9.2 Definition besonderer Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . 539
9.2.1 Smart-Pointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 539
9.2.2 Funktionsobjekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
9.2.3 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
9.3 Weitere Aspekte von new und delete . . . . . . . . . . . . . . . . . . . . . . . 548
9.3.1 Nothrow-Versionen von new und delete . . . . . . . . . . . . . . . . 548
9.3.2 Placement-New . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 548
9.3.3 New-Handler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 549
9.3.4 Überladen von new und delete . . . . . . . . . . . . . . . . . . . . . 554
Sandini Bib

Inhaltsverzeichnis xiii

9.3.5 Operator new mit zusätzlichen Parametern . . . . . . . . . . . . . . . . 557


9.3.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 558
9.4 Funktions- und Komponentenzeiger . . . . . . . . . . . . . . . . . . . . . . . . 559
9.4.1 Funktionszeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 559
9.4.2 Komponentenzeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560
9.4.3 Komponentenzeiger für Schnittstellen nach außen . . . . . . . . . . . . 563
9.4.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565
9.5 Kombination mit C-Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 566
9.5.1 Externe Bindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 566
9.5.2 Headerdateien für C und C++ . . . . . . . . . . . . . . . . . . . . . . . 567
9.5.3 Übersetzen von main() . . . . . . . . . . . . . . . . . . . . . . . . . . 567
9.5.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567
9.6 Weitere Schlüsselwörter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
9.6.1 Varianten mit union . . . . . . . . . . . . . . . . . . . . . . . . . . . 568
9.6.2 Aufzählungstypen mit enum . . . . . . . . . . . . . . . . . . . . . . . 568
9.6.3 Das Schlüsselwort volatile . . . . . . . . . . . . . . . . . . . . . . . 570
9.6.4 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570

10 Zusammenfassung 571
10.1 Hierarchie der C++-Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . 571
10.2 Klassenspezifische Eigenschaften von Operationen . . . . . . . . . . . . . . . . 574
10.3 Regeln zur automatischen Typumwandlung . . . . . . . . . . . . . . . . . . . . 575
10.4 Sinnvolle Programmierrichtlinien und Konventionen . . . . . . . . . . . . . . . 575

Literaturverzeichnis 579

Glossar 583

Index 589
Sandini Bib
Sandini Bib

Vorwort zur zweiten Auflage

Eigentlich wollte ich das Buch ja nur mal eben auf den aktuellen Stand bringen (nach sieben
Jahren wird es ja auch wirklich mal Zeit dafür). Es sollte alle Eigenschaften und Vorteile des
C++-Standards verwenden und nicht mehr auf C als Voraussetzung aufsetzen.
Herausgekommen ist eine vollständige Überarbeitung dieses Buchs. Sie verdeutlicht, dass
der neue C++-Standard auch zu einer neuen Form der Programmierung mit C++ geführt hat.
Statt Zeiger und Felder braucht man nun zunächst einmal Strings und Container.
Somit richtet sich dieses Buch nun an alle Einsteiger und Umsteiger, die vom Potential der
standardisierten Sprache C++ und ihrer Standardbibliothek profitieren möchten. Im ersten Teil
wird verdeutlicht, wie man in C++ als Anwendungsprogrammierer Klassen zu einem Programm
kombinieren kann. Im zweiten Teil werden dann alle Aspekte zur Implementierung eigener Klas-
sen und Klassenhierarchien vorgestellt. Abgerundet wird das Buch durch ein umfangreiches Ka-
pitel über Templates, das deren Potential – auch im Rahmen der objektorientierten Programmie-
rung – verdeutlicht.
Ich wünsche allen Lesern so viel Spaß, wie ich beim Schreiben gehabt habe.

Danke
Erneut möchte ich allen Personen danken, die durch unterschiedlichstes Feedback zu dieser
Überarbeitung beigetragen haben.
Dazu gehören vor allem viele Reviewer, die den neuen Text schon vorab gelesen haben und
maßgeblich zur Qualität des Buchs beigetragen haben. Vielen Dank an Johannes Hampel, Peter
Heilscher, Olaf Janßen, Jan Langer, Bernd Mohr, Olaf Petzold, Michael Pitz, André Pönitz, Da-
niel Rakel, Kurt Watzka, Andreas Wetjen und Sascha Zapf. Dazu gehören aber auch die vielen
Leser, die mir Rückmeldungen zur vorherigen Auflage gegeben haben.
Ein gehöriger Dank gebührt auch meinen Ansprechpartnern von Addison Wesley. Susanne
Spitzer, Margrit Seifert und Friederike Daenecke haben mich bei der Realisierung dieser Über-
arbeitung maßgeblich geholfen.
Schließlich danke ich natürlich meiner Familie, die dieses Projekt wie immer mit viel Geduld
und Verständnis unterstützt haben (auch wenn ich, wie beim Formulieren dieser Zeilen, mal
wieder zu spät zum Essen kam). Ganz herzlichen Dank, Ulli, Lucas, Anica und Frederic.

Hondelage, April 2001


Nicolai Josuttis

1
Sandini Bib

Vorwort zur ersten Auflage

Lang, lang hat es gedauert, bis dieses Buch fertig geworden ist. Aber nun ist es doch so weit.
Ich habe meine Erfahrungen mit C++ und der objektorientierten Programmierung, die ich in
mehrjähriger Projekt- und Schulungsarbeit gewonnen habe, in einem Buch für C++-Program-
mierer festgehalten.
Dabei hat der verspätete Termin der Fertigstellung den Vorteil, dass die neuesten Änderungen
der Sprachspezifikation in das Buch integriert werden konnten. Außerdem konnten die wesent-
lichen Sprachmittel, Templates und Ausnahmebehandlung, mit getesteten Beispielen aufgezeigt
und durch praktische Erfahrungen ergänzt werden.

Danke
Ich möchte mich hiermit bei allen Personen bedanken, die mir dabei geholfen haben, dieses Buch
zu schreiben.
Besonders danke ich den Mitarbeitern der Firma BREDEX. Achim Brede, Ulrich Obst,
Achim Lörke, Bernhard Witte und die anderen Mitarbeiter haben mir mit viel zeitlichem und
materiellen Aufwand sehr geholfen.
Weiter möchte ich allen Personen danken, mit denen ich Themen und Inhalt des Buchs disku-
tiert habe. Dazu gehören insbesondere zahlreiche Personen, die das Buch zur Probe gelesen und
wertvolle Korrekturhinweise gegeben haben (ich kann sie gar nicht alle aufzählen). Dazu gehören
aber auch sehr, sehr viele Personen, mit denen ich zum Teil weltweit (über das UNIX-Netzwerk
Internet) verschiedene Aspekte der Sprache diskutiert habe.
Außerdem danke ich Susanne Spitzer und Judith Muhr von Addison-Wesley für die konstruk-
tive Zusammenarbeit und die große Geduld.
Schließlich danke ich meiner Familie, Ulli, Lucas und Anica, denen ich aufgrund dieses
Buchs eine Weile viel zu selten zur Verfügung stand.

Braunschweig, März 1994


Nicolai Josuttis

2
Sandini Bib

Kapitel 1
Über dieses Buch

Dieses Kapitel erläutert die Motivation und den konzeptionellen Ansatz dieses Buchs und gibt
einen Überblick über den Inhalt.

1.1 Warum dieses Buch?


Objektorientiert“, das ist das Schlagwort, das heutzutage zu jeder guten Software gehört. C++

hat mit zu diesem Durchbruch von Objektorientierung beigetragen. Doch ist C++ keine reine ob-
jektorientierte Sprache. Als Weiterentwicklung von C handelt es sich um einen Sprach-Hybriden,
der objektorientierte und nicht-objektorientierte Programmierung ermöglicht. Diese Flexibilität
hat Vor- und Nachteile: Durch die Unterstützung verschiedener Programmierparadigmen kann
man mit Hilfe von C++ auf fast jedes Problem angemessen reagieren. Dafür fällt es Einstei-
gern und Umsteigern schwer, einen Überblick über die angemessene Verwendung von C++ zu
erhalten.
Dieses Buch führt bewusst in die objektorientierte Programmierung mit C++“ ein. C++

soll in der vollen Mächtigkeit des objektorientierten Paradigmas genutzt werden. Neben der
Einführung neuer Sprachmittel bedeutet das für viele Leser auch, dass Sie eine neue Begriffs-
welt und ein neues Denken kennen lernen werden. Deshalb werden die verschiedenen objekt-
orientierten Sprachmittel mit entsprechenden Problemen aus der Praxis motiviert und durch die
Diskussion von Design-Aspekten abgerundet.
Doch das ist nicht alles. Das Buch stellt auch die wesentlichen nicht-objektorientierten Ele-
mente von C++ vor. Auch diese haben in der Praxis ihre Berechtigung. So stellt z.B. das Sprach-
mittel der Templates ein wesentliches Konzept für generische Probleme dar. Auch darauf wird
mit entsprechenden Beispielen ausführlich eingegangen. Außerdem werden ausgewählte Kom-
ponenten aus der Standardbibliothek vorgestellt.
Das Ziel besteht darin, den Leser Schritt für Schritt in die Welt von C++ einzuführen und
dabei vor allem auf eine objektorientierte Sicht der Sprachmittel zu achten. Dabei besteht trotz-
dem der Anspruch auf Vollständigkeit. Es werden also alle für die Praxis relevanten Sprachmittel
vorgestellt.

3
Sandini Bib

4 Kapitel 1: Über dieses Buch

1.2 Voraussetzungen
Voraussetzung für das Verständnis des Buchs ist es, dass der Leser mit den Konzepten höherer
Programmiersprachen vertraut ist. Begriffe wie Schleife, Prozedur oder Funktion, Anweisung,
Variable und so weiter werden als bekannt vorausgesetzt.
Kenntnisse der Konzepte der objektorientierten Programmierung sind nicht notwendig. Diese
Konzepte werden implizit bei der Vorstellung der Sprachmittel, die diese Konzepte in C++ rea-
lisieren, vermittelt. Sicherlich sind vorhandene Kenntnisse der objektorientierten Konzepte aber
von Vorteil, da das Buch diese zwangsläufig ein wenig durch die C++-Brille betrachtet.
Den Lesern, die bereits C oder Java kennen, werden vor allem die fundamentalen Sprachmit-
tel bereits bekannt sein. Doch Vorsicht, auch hier gibt es im Detail relevante Unterschiede, auf
die jeweils auch explizit hingewiesen wird.

1.3 Systematik
Der große Vorteil von C++ besteht in der Möglichkeit, dass man fast jede Art von Datentypen,
Klassen, Funktionen (Prozeduren) oder Bibliotheken implementieren kann. Damit kann man für
fast jede Problematik eine geeignete einfache Schnittstelle definieren. Dies hat den Vorteil, dass
Teilprobleme nur einmal gelöst und Dienstleistungen nur einmal implementiert werden müssen.

Anwendungs- und Systemprogrammierer


Aus diesem Grund kann man C++ aus verschiedenen Blickwinkeln betrachten:
 Auf der einen Seite gibt es die Sichtweise der Anwendungsprogrammierer, die Datentypen
und Klassen mit Hilfe von Funktionen und Kontrollstrukturen zu laufenden Programmen
kombinieren.
 Auf der anderen Seite gibt es die Sichtweise der Systemprogrammierer, die neue Schnittstel-
len in Form von Funktionen, Datentypen und Klassen zur Verfügung stellen.
Für die Sichtweise der Systemprogrammierer sollte man alle Details der Sprache kennen und
beherrschen. Für die Sichtweise der Anwendungsprogrammierer reicht es, die Schnittstellen zu
kennen und einfach anzuwenden.
Das vorliegende Buch ist entsprechend der Bandbreite dieser beiden Sichtweisen organisiert.
Wir bewegen uns didaktisch immer weiter von der Sichtweise des Anwendungsprogrammierers
zur Sichtweise des Systemprogrammierers.
Natürlich lassen sich diese beiden Sichtweisen in der Praxis nicht völlig trennen. Dies gilt
insbesondere deshalb, weil man im Zuge der Abstrahierung von Problemen immer wieder Da-
tentypen und Klassen verwendet, um höhere Datentypen und Klassen zu programmieren. Doch
wird im Buch darauf geachtet, dass man zunächst die generellen Konzepte und die Anwendungs-
sicht kennen lernt. Die hinter den Datentypen und Klassen verborgene Flexibilität, die C++ so
wertvoll macht, wird erst nach und nach erläutert.
Sandini Bib

1.3 Systematik 5

Aufbau des Buchs


Der Aufbau des Buchs basiert konzeptionell auf zahlreichen Schulungen, die ich zu diesem The-
ma bereits gehalten habe. Dabei wird grundsätzlich darauf geachtet, dass keine spezielle Sprach-
version, sondern die Sprache anhand ihrer derzeitigen Spezifikation vorgestellt wird.
Im Einzelnen behandeln die nachfolgenden Kapitel folgende Themen:
 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung
bietet eine prinzipielle Einführung in das Thema und führt die Grundbegriffe ein, die für das
Verständnis von C++ erforderlich sind.
 Kapitel 3: Grundkonzepte von C++-Programmen
stellt die Grundkonzepte von C++ aus dem Blickwinkel der Anwendungsprogrammierer vor.
Neben der Einführung der fundamentalen Datentypen, Funktionen, Operatoren und Kontroll-
strukturen werden auch die wichtigsten Komponenten der Standardbibliothek eingeführt.
 Kapitel 4: Programmieren mit Klassen
stellt die beiden objektorientierten Grundkonzepte Klassen und Datenkapselung mit allen
damit verbundenen Sprachelementen vor.
 Kapitel 5: Vererbung und Polymorphie
stellt die objektorientierten Grundkonzepte Vererbung und Polymorphie mit allen damit ver-
bundenen Sprachelementen vor.
 Kapitel 6: Dynamische und statische Komponenten
stellt alle speziellen Aspekte vor, die bei der Programmierung komplizierterer Klassen mit
dynamischen und statischen Komponenten beachtet werden müssen.
 Kapitel 7: Templates
stellt alle Aspekte der generischen Programmierung mit Hilfe von Templates vor.
 Kapitel 8: Die Standard-I/O-Bibliothek im Detail
beschreibt die Standardbibliothek für Ein- und Ausgaben im Detail. Dazu gehört auch der
Umgang mit Dateien.
 Kapitel 9: Weitere Sprachmittel und Details
geht auf alle dann noch offenen und für die Praxis wichtigen Sprachmittel und Standardkom-
ponenten von C++ ein.
 Kapitel 10: Zusammenfassung
enthält eine zusammenfassende Schlussbetrachtung von C++.
 Im Anhang befinden sich:
– ein Literaturverzeichnis
– ein Glossar
– ein Index
Sandini Bib

6 Kapitel 1: Über dieses Buch

1.4 Wie liest man dieses Buch?


Da dieses Buch alle Sprachmittel und deren Anwendung nach und nach einführt, sollte der Leser,
der in die Thematik einsteigt, das Buch einfach von vorn bis hinten durchlesen. Vorwärtsverweise
ließen sich allerdings nicht ganz vermeiden, sollten aber kein allzu großes Problem darstellen.
Gerade für den Leser, der neu in die Materie einsteigt, stellt sich das Problem, dass eine Fülle
von neuen Begriffen eingeführt und verwendet wird, die erst einmal verstanden und auseinander
gehalten werden müssen. Dabei kommt erschwerend hinzu, dass es bei der objektorientierten
Programmierung und speziell in C++ eine gewisse Begriffsverwirrung gibt. Das Glossar im An-
hang soll hier Klarheit verschaffen. Es erläutert die wichtigsten Begriffe und stellt auch gleich
andere deutsche und englischsprachige Bezeichnungen für den gleichen Sachverhalt vor, um
Querverweise zu anderer Literatur zu erleichtern.
Leser, die sofort in die eigentliche objektorientierte Programmierung mit Klassen einsteigen
wollen, können auch sofort mit Kapitel 4 beginnen. Dieser Teil ist der Einstieg in die Sichtweise
der Systemprogrammierer und betrachtet die dazugehörigen Sprachmittel auch unter dem Blick-
winkel ihrer Verwendung. Insofern wird die prinzipielle Sicht der Anwendungsprogrammierer
noch einmal mit entsprechendem Hintergrund erläutert.
Wer das Buch später oder von vornherein als Nachschlagewerk für die Programmierung mit
C++ verwendet, kann am besten über das Inhaltsverzeichnis oder über den Index Einstiegspunkte
für die einzelnen Themenbereiche finden. Ich habe mich bemüht, beides relativ ausführlich zu
gestalten.

Wie vermeidet man es, das Buch zu lesen?


Diese vielleicht etwas unerwartete Frage bezieht sich auf die Tatsache, dass viele Menschen ei-
nen Sachverhalt lieber anhand eines Beispiels als durch trockenes Lesen eines Buchs lernen (ich
gehöre auch dazu). Diesen Lesern empfehle ich, die einzelnen Beispiele durchzugehen. Außer-
dem besitzt jeder Abschnitt, in dem neue Sprachmittel eingeführt werden, am Ende eine kurze
Zusammenfassung, die man auch dazu verwenden kann, um zu entscheiden, ob das Kapitel im
Detail gelesen werden sollte.

1.5 Zugriff auf die Quellen zu den Beispielen


Die Quellen der wichtigsten Beispiele stehen im Internet auf der Webseite zu diesem Buch zur
Verfügung. Diese Webseite hat folgende Adresse (URL):
http://www.josuttis.de/cppbuch/
Dort sind die Beispiele unter dem jeweils am Anfang eines Listings angegebenen Dateinamen zu
finden.

1.6 Anregungen und Kritik


Es gibt immer etwas zu verbessern. Für Anregungen und Kritik zu diesem Buch wäre ich deshalb
dankbar. Meine Adresse lautet:
Sandini Bib

1.6 Anregungen und Kritik 7

Nicolai Josuttis
Berggarten 9
D–38108 Braunschweig
Tel: 05309 / 57 47
Fax: 05309 / 57 74
Am einfachsten bin ich per E-Mail zu erreichen. Die E-Mail-Adresse zu diesem Buch lautet:
cppbuch@josuttis.de
Damit alle Leser von Rückmeldungen profitieren, werden eventuelle Verbesserungen auf der
oben genannten Webseite zu diesem Buch aufgelistet.
Sandini Bib
Sandini Bib

Kapitel 2
Einleitung: C++ und
objektorientierte Programmierung

Dieses Kapitel bietet eine prinzipielle Einführung in das Thema und führt die Grundbegriffe für
das Verständnis von C++ ein. Nach einer Einführung in die Geschichte und die Design-Aspekte
von C++ werden dabei in einer Art Schnelldurchlauf“ die wichtigsten Sprachmittel von C++ im

Überblick kurz eingeführt, bevor es dann in den folgenden Kapiteln in die Details geht.

2.1 Die Sprache C++


C++ ist eine objektorientierte Sprache, die in relativ kurzer Zeit eine große Verbreitung gefunden
hat und heute eine der Standardsprachen für die objektorientierte Programmierung darstellt.
Zu dieser Verbreitung hat hauptsächlich eine der wesentlichsten Eigenschaften von C++ bei-
getragen: die Kompatibilität zu C. Entwickelt von AT&T unter der Federführung von Bjarne
Stroustrup galt es, eine objektorientierte Weiterführung von C zu entwickeln, die es erlaubt, vor-
handenen C-Code so weit wie möglich weiterverwenden zu können.

2.1.1 Designkriterien
C++ wurde als objektorientierte Weiterentwicklung von C entworfen. Die Sprache C sollte um
objektorientierte Sprachmittel erweitert werden. Da die Kompatibilität zu C einen sehr hohen
Stellenwert hatte, kann im Prinzip jedes C-Programm als C++-Programm übersetzt werden (klei-
ne Modifikationen wie etwa bei Namenskonflikten durch neue Schlüsselwörter ausgenommen).
Dies bedeutet aber, dass C++ keine rein objektorientierte Sprache, sondern ein Hybride ist.
Man kann mit C++ sowohl datenflussorientiert als auch objektorientiert programmieren. Jeder
Programmierer muss deshalb bewusst darauf achten, dass er C++ als objektorientierte Sprache
verwendet. Auf der anderen Seite hat er den Vorteil, dass er bei Problemen, für die keine objekt-
orientierte Lösung angemessen ist, nicht gleich die Sprache wechseln muss.

9
Sandini Bib

10 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

2.1.2 Sprachversionen
C++ ist eine inzwischen standardisierte Sprache. Ein ANSI/ISO-Komitee1 hat 1998 eine welt-
weit einheitliche Sprachspezifikation verabschiedet. Dieses Dokument ist im Internet unter der
Nummer ISO/IEC 14882-1998 im Electronic Standards Store, ESS, unter
http://www.ansi.org
für $ 18,– als PDF-Datei erhältlich. In Deutschland ist der Standard bei DIN2 ebenfalls verfügbar,
kostet dort aber knapp DM 500,–.
Da die Compilerbauer neue Spezifikationen der Sprache immer erst implementieren müs-
sen, hinken die Compiler der aktuellen Spezifikation zur Zeit leider immer noch etwas hinterher.
Dies führt allerdings nur in wenigen Einzelfällen zu Einschränkungen. So kann man inzwischen
davon ausgehen, dass im Prinzip alle hier im Buch vorgestellten Programme mit den neuesten
Versionen der verschiedenen Compiler übersetzbar und lauffähig sind.

2.2 C++ als objektorientierte Programmiersprache


Der Begriff objektorientiert“ ist inzwischen zum Gütesiegel geworden, ohne dass immer klar

ist, was darunter zu verstehen ist. Es zeigt sich sogar, dass es verschiedene Interpretationen des
Begriffs gibt und dass der Begriff mitunter auch missbraucht wird, um Software mit diesem
Gütesiegel zu versehen.
Genauso umstritten wie der Begriff ist auch die Frage, ob und wann etwas objektorientiert ist.
Es gibt zwar verschiedene Kennzeichen objektorientierter Sprachen, aber es ist nicht eindeutig
festgelegt, ob alle Kennzeichen oder nur eine Auswahl davon vorhanden sein müssen.
C++ unterstützt alle Sprachmittel, die nach vorwiegender Meinung objektorientierte Spra-
chen kennzeichnen. Trotzdem gibt es zum Teil Diskussionen darüber, ob C++ eine echte ob-
jektorientierte Sprache ist. Dies liegt daran, dass C++ im Gegensatz zu Sprachen wie Smalltalk
oder Java keine rein objektorientierte Sprache ist. C++ unterstützt zwar eine objektorientierte
Programmierung, durch die Aufwärtskompatibilität zu C kann man aber auch C++-Programme
schreiben, die die objektorientierten Sprachmittel nicht nutzen. Hinzu kommt die Möglichkeit,
mit Hilfe von Templates auch generisch zu programmieren.
Bevor diese Frage in einen Glaubenskrieg ausartet, sollen nun aber die wesentlichen Kenn-
zeichen von objektorientierter Programmierung vorgestellt werden. Diese Kennzeichen lassen
sich im Grunde auf vier Begriffe zurückführen:

 Klassen (abstrakte Datentypen)


 Datenkapselung
 Vererbung
 Polymorphie

1
ANSI: American National Standard Institute, ISO: International Organization for Standardization
2
DIN: Deutsches Institut für Normung e.V.
Sandini Bib

2.2 C++ als objektorientierte Programmiersprache 11

2.2.1 Objekte, Klassen und Instanzen


Ein gängiges Mittel, Komplexität zu reduzieren, ist die Abstraktion. Dinge und Vorgänge werden
auf das Wesentliche reduziert bzw. zusammengefasst oder mit einem Oberbegriff versehen. Auf
diese Weise werden auch komplexe Dinge handhabbar.
Ein Beispiel für eine in mehrfacher Hinsicht durchgeführte Abstrahierung ist der Begriff
Auto“:

 Ein Auto ist die Zusammenfassung verschiedener Einzelteile, wie Motor, Karosserie, vier
Räder und so weiter.
 Ein Auto ist aber auch ein Oberbegriff für verschiedene Auto-Typen, wie VW, Opel und
Trabbi oder auch Sportwagen, Limousine und Geländewagen.
Solange die Einzelteile von Autos oder Unterschiede einzelner Autos nicht von Interesse sind,
wird zusammenfassend der Begriff Auto verwendet. Man baut Autotransporter oder fährt mit
einem Auto von A nach B.

Strukturen
Ein wesentlicher Fortschritt in der Geschichte der Programmiersprachen war die Möglichkeit,
mehrere Daten zu einem Datenverbund zusammenzufassen und so verschiedene zusammen-
gehörende Eigenschaften in einen Datentyp zu bündeln. Solche Strukturen oder Records erlauben
es, dass eine Variable alle Daten enthält, die zu dem von der Variablen repräsentierten Sachver-
halt gehören.
Strukturen bieten damit die Möglichkeit eine mögliche Art der Abstrahierung, nämlich die
Zusammenfassung verschiedener Einzelteile, in Programmen auszudrücken. Eine Struktur für
den Datentyp Auto besteht aus den Einzelelementen (Komponenten) wie Motor, Karosserie und
vier Rädern.

Objekte
Im Mittelpunkt der objektorientierten Programmierung steht das Objekt. Ein Objekt ist etwas,
das betrachtet wird, das verwendet wird, das eine Rolle spielt. Wenn man objektorientiert pro-
grammiert, versucht man, die Objekte, die im Problemfeld des Programms eine Rolle spielen, zu
ermitteln und zu implementieren. Dabei steht zunächst nicht der interne Aufbau eines Objekts
im Vordergrund. Wichtig ist, dass in einem Programm ein Objekt, wie z.B. ein Auto, eine Rolle
spielt.
Je nach Aufgabenstellung sind verschiedene Aspekte eines Objekts von Interesse. Ein Auto
kann sich z.B. aus Einzelteilen wie Motor und Karosserie zusammensetzen oder durch Eigen-
schaften wie Kilometerstand und Höchstgeschwindigkeit beschrieben werden. Diese Attribute
kennzeichnen die Objekte. Entsprechend kann auch eine Person als Objekt betrachtet werden,
von dem verschiedene Attribute von Interesse sind. Je nach Problemstellung können das Vor-
und Nachname, Adresse, Personalnummer, Haarfarbe und/oder das Gewicht sein.
Ein Objekt muss also nicht unbedingt etwas Konkretes, Greifbares sein. Es kann beliebig
abstrakt sein und z.B. auch einen Vorgang beschreiben. Ein stattfindendes Fußballspiel kann z.B.
als Objekt betrachtet werden. Die Attribute dieses Objekts könnten die Spieler, der Spielstand
und die abgelaufene Zeit sein.
Sandini Bib

12 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

Strukturen ermöglichen es, Objekte in Programmen zu verwalten. Sie bieten die Möglichkeit,
ein Objekt letztlich in die einzelnen Attribute aufzuspalten und mit Hilfe der von einer Program-
miersprache unterstützten Datentypen zu programmieren.

Abstrakte Datentypen
In Strukturen oder Records können die einzelnen Eigenschaften von Objekten jeweils in den
Komponenten festgehalten werden. Wenn Objekte betrachtet werden, interessiert allerdings nicht
nur, wie sie aufgebaut sind bzw. was sie repräsentieren, sondern genauso wichtig ist das, was man
mit ihnen machen kann, also die Operationen, die für ein Objekt aufgerufen werden können.
Hier kommt der Begriff abstrakter Datentyp ins Spiel: Ein abstrakter Datentyp beschreibt
nicht nur die Attribute eines Objekts, aus denen es sich zusammensetzt, sondern auch dessen
Verhalten (die Operationen). Dazu kann z.B. auch eine Beschreibung gehören, welche Zustände
das Objekt überhaupt annehmen kann.
Auf das Beispiel des Fußballspiels bezogen, wird ein solches Objekt nicht nur durch die
Spieler, den Spielstand und die abgelaufene Zeit beschrieben. Dazu gehören auch Vorgänge, die
mit dem Objekt passieren können (Einwechseln eines Spielers, Erzielen eines Tors, Abpfeifen),
oder Randbedingungen (das Spiel startet mit dem Spielstand 0:0 und dauert 90 Minuten).
Dieser Sachverhalt wird aber in der strukturierten Programmierung nur unzureichend unter-
stützt. Es gibt zwar eine Unterstützung für den Sachverhalt, dass sich ein Objekt aus Attributen
zusammensetzt, es gibt aber keine Unterstützung dafür, dass auch Operationen und Randbedin-
gungen dazugehören. Diese müssen als Ergänzung separat programmiert werden.

Klassen
Zur Realisierung von abstrakten Datentypen hat man in der objektorientierten Programmierung
den Begriff Klasse eingeführt. Eine Klasse ist die Implementierung eines abstrakten Datentyps.
Sie beschreibt für ein Objekt im Gegensatz zur Struktur nicht nur dessen Attribute (Daten), son-
dern auch dessen Operationen (Verhalten).
Eine Klasse Auto definiert z.B., dass ein Auto aus Motor, Karosserie und vier Rädern besteht
und dass man in ein Auto einsteigen, damit fahren und daraus aussteigen kann.
Eine Klasse kann also als Umsetzung des Begriffs abstrakter Datentyp“ betrachtet werden.

Dabei ist der genaue Unterschied allerdings umstritten. Manchmal werden die Begriffe als Syn-
onyme verwendet. Manchmal werden die Begriffe dahingehend unterschieden, dass Eigenschaf-
ten einer Klasse im Gegensatz zu einem abstrakten Datentyp an andere Klassen vererbbar sind.
Speziell in der C++-Welt wird der Begriff abstrakter Datentyp auch gern als Abgrenzung zu
dem Begriff fundamentaler Datentyp und somit gleichbedeutend mit (selbst definierter) Klasse
verwendet.

Instanzen
Eine Klasse beschreibt ein Objekt. Es ist also im Sinne von Programmen wirklich ein Datentyp.
Von diesem Datentyp können dann mehrere Variablen erzeugt werden. In der objektorientierten
Programmierung nennt man diese Variablen Instanzen.
Instanzen sind also die Realisierung oder Ausprägung der Objekte, die in einer Klasse be-
schrieben werden. Diese Instanzen bestehen aus den in der Klasse beschriebenen Daten oder
Elementen und können mit den dort definierten Operationen manipuliert werden.
Sandini Bib

2.2 C++ als objektorientierte Programmiersprache 13

Oft (und vor allem in C++) werden die Begriffe Objekt und Instanz synonym verwendet. Wenn
eine Variable vom Typ (von der Klasse) Auto deklariert wird, wird ein Auto-Objekt (eine Instanz
der Klasse Auto) angelegt. In manchen Sprachen wird allerdings zwischen Objekt und Instanz
unterschieden, da auch eine Klasse als Objekt betrachtet werden kann. Objekt ist dann mehr der
Oberbegriff für Klasse und Instanz.

Methoden
In der objektorientierten Sprachwelt nennt man die für Objekte definierten Operationen auch
Methoden. Jede Operation, die für ein Objekt aufgerufen wird, wird als Nachricht an das Objekt
interpretiert, zu deren Verarbeitung es eine bestimmte Methode verwendet.
Letztlich wird ein objektorientiertes Programm also durch Nachrichten an Objekte realisiert,
die jeweils wieder Nachrichten an andere Objekte auslösen können. Wenn für ein Auto die Ope-
ration fahren“ aufgerufen wird, wird dem Auto die Nachricht fahren“ gesendet, die mit der
” ”
entsprechenden Methode verarbeitet wird.
Bei dieser Denkweise wird das Objekt in den Vordergrund gestellt. Beim objektorientierten
Design macht man sich Gedanken über die im Problembereich vorhandenen Objekte. Abhängig
von ihrer jeweiligen Rolle werden ihr Aufbau und ihr Verhalten beschrieben. Daraus ergibt sich
der Programmablauf fast von selbst.

2.2.2 Klassen in C++


Bei der Umsetzung der objektorientierten Konzepte in C++ muss immer berücksichtigt werden,
dass C++ keine rein objektorientierte Sprache, sondern ein Sprach-Hybride ist. Dies spiegelt
sich schon in der Namensgebung wider. Methoden werden auch weiterhin als Funktionen und
Instanzen weiterhin als Variablen oder auch einfach nur als konkrete Objekte bezeichnet. Das
bereits in der objektorientierten Welt herrschende Begriffschaos ist in C++ perfekt (ich gehe in
Abschnitt 2.4 noch genauer darauf ein).
In C++ ist eine Klasse eine Struktur, die als Komponenten auch Funktionen (Methoden)
enthalten kann. Dies wird nachfolgend am Beispiel der Klasse Auto verdeutlicht.

Auto als C++-Klasse


In C++ wird ein Auto z.B. wie folgt definiert:
class Auto {
int baujahr; // Baujahr als ganzzahliger Wert
float kmstand; // aktueller Kilometerstand als Gleitkommazahl
string kennzeichen; // aktuelles Kennzeichen

int lieferBaujahr (); // Baujahr abfragen


void fahren (float km); // km Kilometer fahren
float lieferKMStand (); // Kilometerstand abfragen
...

void Auto(); // automatische Initialisierung


void ~Auto() // automatisches Aufräumen bei der Zerstörung
};
Sandini Bib

14 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

Sowohl der Aufbau als auch das Verhalten, einschließlich Initialisierung und Zerstörung, ist in
C++ Teil der Klassenstruktur:
 Intern besitzt ein Auto z.B. folgende Attribute:
– baujahr definiert das Baujahr des Autos. Der dazu verwendete Datentyp int steht in
C++ für ganzzahlige Werte zur Verfügung.
– kmstand definiert den aktuellen Kilometerstand des Autos. Der dazu verwendete Daten-
typ float steht in C++ für Gleitkommawerte zur Verfügung.
– kennzeichen definiert das aktuelle Kennzeichen des Autos. Der dazu verwendete Da-
tentyp string steht in C++ für Strings (Zeichenfolgen) zur Verfügung.
 Folgende Operationen sind definiert:
– lieferBaujahr() dient dazu, das Baujahr abzufragen und als ganzzahligen Wert zurück-
zuliefern.
– fahren() dient dazu, mit dem Auto eine als Gleitkommawert km übergebene Strecke
zurückzulegen.
– lieferKMStand() dient dazu, den aktuellen Kilometerstand des Autos abzufragen und
als Gleitkommawert zurückzuliefern.
 Hinzu kommen spezielle Funktionen, mit denen man das Verhalten beim Erzeugen und Zer-
stören eines Autos beeinflussen kann:
– Auto() definiert, was beim Erzeugen eines Autos automatisch passieren soll. Dazu gehört
z.B., dass Baujahr, Kilometerstand und Kennzeichen entsprechend initialisiert werden.
– ~Auto() definiert, was beim Zerstören von einem Auto passieren soll. Hier kann z.B.
definiert werden, dass intern speziell angelegter Speicherplatz automatisch freigegeben
wird.
Eine derartige Definition hat mehrere Vorteile:
 Die Operationen (Funktionen/Methoden) belegen nicht den globalen Namensbereich. Der
einzige globale Datentyp ist der Datentyp Auto.
 Ein Parameter Auto entfällt für Operationen. Da die Funktionen grundsätzlich nur für Autos
aufgerufen werden können, ist damit automatisch immer ein dazugehöriges Auto vorhanden.
 Weder Initialisierungs- noch Aufräumungsarbeiten müssen explizit aufgerufen werden. Da-
mit kann der Anwender diese Dinge nicht vergessen.
Ein entsprechendes Anwendungsprogramm sieht in C++ wie folgt aus:
void autotest ()
{
Auto a; // Auto a anlegen und initialisieren

a.fahren(77); // 77 Kilometer fahren


...
float stand = a.lieferKMStand(); // Kilometerstand abfragen
Sandini Bib

2.2 C++ als objektorientierte Programmiersprache 15

...
if (a.lieferBaujahr() < 1900) { // Baujahr abfragen und auswerten
}
...
} // automatisches Aufräumen und Zerstören am Blockende
Mit der Deklaration von a wird eine Instanz (ein konkretes Objekt) als Auto angelegt und auto-
matisch initialisiert. Der Zugriff erfolgt dann über die für Autos definierten Funktionen (Metho-
den). Dabei ist ein Funktionsaufruf ein Zugriff auf die Komponente der Klassenstruktur, der über
den Punkt-Operator erfolgt. Am Blockende, wenn das Objekt seinen Gültigkeitsbereich verlässt,
wird eine für das Aufräumen definierte Funktion automatisch aufgerufen.

2.2.3 Datenkapselung
Ein Problem von herkömmlichen Strukturen besteht darin, dass jederzeit auf alle Komponenten
einer solchen Struktur zugegriffen werden kann. Jedes Programmstück bietet also für alle An-
wender die Möglichkeit, den Zustand eines Objekts beliebig zu verändern. Damit besteht die
Gefahr, bei Anwendung einer Klasse Inkonsistenzen und Fehler zu produzieren. So könnte ein
Anwendungsprogramm von Auto z.B. einfach das Baujahr ändern oder den Kilometerstand her-
absetzen.
Daraus entstand die Idee der Datenkapselung (englisch: encapsulation3 ): Um sicherzustellen,
dass nicht jeder Anwender mit einem Objekt machen kann, was er will, wird einfach der Zugriff
auf ein solches Objekt auf eine wohldefinierte Schnittstelle reduziert. Jeder Anwender, der mit
einem solchen Objekt umgeht, soll nur die Operationen durchführen können, die der Designer
des entsprechenden Datentyps für einen öffentlichen Zugriff vorgesehen hat. Auf Interna, die zur
Verwaltung des Objekts und seiner Daten im Programm dienen, besteht kein Zugriff.
In C++ kann der Zugriff auf die Komponenten einer Klassenstruktur über spezielle Zugriffs-
schlüsselwörter definiert werden:4
class Auto {
private:
int baujahr; // Baujahr als ganzzahliger Wert
float kmstand; // aktueller Kilometerstand als Gleitkommazahl
string kennzeichen; // aktuelles Kennzeichen

public:
int lieferBaujahr (); // Baujahr abfragen
void fahren (float km); // km Kilometer fahren

3
Manchmal wird statt encapsulation auch der Begriff information hiding verwendet. Dies ist aber – zu-
mindest auf C++ bezogen – nicht korrekt, da die internen Daten nur vor einem Zugriff von außen gesperrt,
nicht aber versteckt werden.
4
Ohne die Zugriffsschlüsselwörter ist für alle Komponenten kein Zugriff von außen möglich (private:
ist Default). Insofern funktioniert das zuvor vorgestellte Anwendungsprogramm erst mit dieser Version.
Sandini Bib

16 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

float lieferKMStand (); // Kilometerstand abfragen


...

void Auto(); // automatische Initialisierung


void ~Auto() // automatisches Aufräumen bei Zerstörung
};
Die nach private: deklarierten Komponenten sind vor dem Zugriff durch den Anwender einer
Klasse geschützt. Der Anwender kann für die Objekte einer Klasse nur auf die hinter public:
deklarierten öffentlichen Komponenten zugreifen bzw. die entsprechenden Funktionen aufrufen.
In diesem Fall ist es einem Anwender der Klasse also nicht mehr möglich, auf baujahr oder
kmstand direkt zuzugreifen. Zugriff besitzen nur die Funktionen, die in der Klassenstruktur
deklariert werden.
Es ist typisch, dass der Zugriff auf Attribute nur über Funktionen möglich ist. Auf diese Art
und Weise kann bei jedem Zugriff genau geprüft werden, ob die Art des Zugriffs in der aktuellen
Situation und mit den aktuellen Parametern sinnvoll ist.

Trennung von Schnittstelle und Implementierung


Das Konzept der Datenkapselung basiert letztlich auf der Idee, dass es für den Anwender ei-
nes Objekts unerheblich ist, wie dieses intern repräsentiert wird. Entscheidend ist nur, dass das
Richtige gemacht wird.
Im Beispiel Auto ist es für den Anwender z.B. nicht von Interesse, auf welche Weise das
Baujahr und der Kilometerstand intern verwaltet werden. Entscheidend ist, dass ein Auto erzeugt
werden kann, dass man damit fahren kann und bestimmte Attribute abfragen kann. Abbildung 2.1
verdeutlicht diese Schnittstelle grafisch. Dabei wird die für die objektorientierte Programmierung
übliche UML-Notation verwendet.

A u t o

l i e f e r B a u j a h r ( ) : i n t
f a h r e n ( k m : f l o a t )
l i e f e r K M S t a n d ( ) : f l o a t

Abbildung 2.1: Öffentliche Schnittstelle von Autos

Für den Anwender steht nur das WAS im Vordergrund. Die öffentliche Schnittstelle legt fest,
was mit einem Objekt gemacht werden kann. Das WIE interessiert nur den, der diese Klasse
schließlich implementiert. Bei gleich bleibender Schnittstelle kann die Implementierung belie-
big verändert werden, ohne dass dies Auswirkungen auf den Anwendungscode hätte. Auf diese
Weise kann eine erste Implementierung z.B. später aus Performance-Gründen verbessert werden.
Sandini Bib

2.2 C++ als objektorientierte Programmiersprache 17

2.2.4 Vererbung
Wenn man das Verhalten von Objekten beschreibt, gibt es oft Eigenschaften, die verschiedenar-
tige Objekte gemeinsam haben. Im natürlichen Sprachgebrauch werden dafür Oberbegriffe ver-
wendet. Ein blauer PKW Baujahr 1984 mit Dieselmotor wird schlicht zu einem Auto, solange
seine Details nicht von Interesse sind. Er kann sogar nur als Fahrzeug betrachtet werden, wenn
die Tatsache, dass es sich um ein Auto handelt, zeitweise keine Rolle spielt.
Eine derartige Verallgemeinerung findet z.B. bei einem Autotransporter statt. Für diesen ist
es unerheblich, was für Autos transportiert werden; allenfalls die geometrischen Maße der Autos
sind von Interesse. Dennoch werden verschiedene Autos transportiert, und deren Unterschiede
können nach dem Ausladen der Autos sehr wohl wieder von Interesse werden.
Für dieses Abstraktionsmittel von Generalisierung und Spezialisierung gibt es in den her-
kömmlichen Programmiersprachen kein äquivalentes Datenmodell. In der objektorientierten Pro-
grammierung gibt es dafür das Konzept der Vererbung und der Polymorphie.
Verschiedene Klassen können in einer hierarchischen Beziehung zueinander stehen. Dabei
gilt, dass eine abgeleitete“ Klasse immer eine Spezialisierung ihrer Basisklasse“ ist. Umge-
” ”
kehrt ist die Basisklasse die Generalisierung der abgeleiteten Klasse. Das bedeutet, dass alle
Eigenschaften (Attribute und Operationen) der Basisklasse jeweils übernommen (geerbt) und in
der Regel durch genauere Eigenschaften ergänzt werden.
Abbildung 2.2 zeigt ein entsprechendes Beispiel. Die Klasse Fahrzeug beschreibt als Basis-
klasse, welche Eigenschaften ein Fahrzeug hat. Dazu gehören z.B. das Attribut baujahr oder
die Operation lieferBaujahr(). Für alle davon abgeleiteten Klassen wie Auto, Fahrrad oder
Laster gelten diese Eigenschaften ebenfalls. Die Klasse Fahrzeug ist also der Oberbegriff,
unter dem sich die gemeinsamen Eigenschaften zusammenfassen lassen.
Die unterschiedlichen Eigenschaften, die speziell Autos oder speziell Laster haben, werden
dann in den jeweils davon abgeleiteten Klassen implementiert. So besitzen Fahrzeuge mit Tacho
z.B. zusätzlich das Attribut kmstand sowie die Operationen fahren() und lieferKMStand().
Laster haben zusätzlich die Operationen beladen() und ausladen(). Auf diese Weise kann
immer weiter verfeinert werden. Es entsteht eine Klassenhierarchie. Grundsätzliches Kennzei-
chen der Vererbung ist dabei die is-a-Beziehung. Ein Laster ist ein Fahrzeug mit Tacho, ein
Fahrzeug mit Tacho ist ein Fahrzeug.
Was sind nun die Vorteile der Vererbung? Zunächst dient sie der Konsistenz und zur Ein-
sparung von Code. Gemeinsame Eigenschaften verschiedener Klassen müssen nur einmal im-
plementiert und gegebenenfalls auch nur an einer Stelle geändert werden. Der andere Vorteil ist,
dass das Abstraktionsmittel des Oberbegriffs unterstützt wird. Dies wird in Abschnitt 2.2.5 bei
der Einführung in die Polymorphie noch verdeutlicht.
In der Praxis kann es auch notwendig sein, einzelne geerbte Eigenschaften zu ändern. Auch
dies ist mit Einschränkungen möglich. Die Vererbung wird natürlich sinnlos, wenn nichts mehr
übernommen wird. In diesem Fall sollte man eine separate Klasse implementieren. Dabei gibt es
natürlich Grenzfälle.
Sandini Bib

18 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

F a h r z e u g

b a u j a h r : i n t

l i e f e r B a u j a h r ( ) : i n t

F a h r z e u g M i t T a c h o F a h r r a d

k m s t a n d : f l o a t a u f p u m p e n ( )

f a h r e n ( k m : f l o a t )
l i e f e r K M S t a n d ( ) : f l o a t

A u t o L a s t e r B u s

k o f f e r r a u m O e f f n e n ( ) b e l a d e n ( ) s i t z p l a e t z e : i n t
a u s l a d e n ( )

Abbildung 2.2: Beispiel für eine Klassenhierarchie

2.2.5 Polymorphie
Von den Vorteilen, die sich aus der Vererbung ergeben, wurden bisher nur die Konsistenz und die
Einsparung von Code aufgeführt. Die Vererbung ist aber außerdem eine wichtige Grundvoraus-
setzung, um Polymorphie zu ermöglichen. Die Arbeit mit Oberbegriffen wird nämlich dann erst
sinnvoll, wenn sich Objekte zeitweise auf ihren Oberbegriff reduzieren lassen.
Nehmen wir an, es soll eine Menge von Autos z.B. für eine KFZ-Werkstatt betrachtet wer-
den. Das können völlig verschiedene Autos sein, unterschiedliche Fabrikate, unterschiedliche
Modelle, unterschiedliche Ausführungen. Es handelt sich also um eine inhomogene Menge“

von Autos. Wenn nun bei verschiedenen Autos der Motor gewechselt werden soll, so wird zwar
im Prinzip das gleiche gemacht, aber je nach Auto sieht dies unter Umständen sehr verschie-
den aus. Die im täglichen Sprachgebrauch verwendete Aufforderung Wechsle bei den Autos den

Motor“ wird also zu unterschiedlichen Aktionen führen.
Um diesen Sachverhalt widerzuspiegeln, kann man nun für jede Klasse, die eine Art von Au-
tos beschreibt, eine eigene Version des Motor-Wechselns implementieren. Das Besondere an der
Polymorphie ist nun, dass für eine solche inhomogene Menge von verschiedenen Autos jeweils
die Operation motorWechseln() aufgerufen werden kann und automatisch je nach Auto-Typ
Sandini Bib

2.2 C++ als objektorientierte Programmiersprache 19

die richtige Funktion aufgerufen wird. Polymorphie bedeutet letztlich die Fähigkeit, dass eine
Operation vom Objekt selbst interpretiert wird, und zwar unter Umständen erst zur Laufzeit, da
beim Kompilieren nicht unbedingt bekannt ist, welche Operation gemeint ist.
Es gibt verschiedene Möglichkeiten, Polymorphie zu implementieren. Den radikalsten Weg
geht der Prototyp aller objektorientierten Sprachen, Smalltalk. Dort sind Variablen an keinen
Typ gebunden und können somit jede Art von Objekt repräsentieren. Wenn für eine Variable eine
Operation aufgerufen wird, wird zur Laufzeit festgestellt, um welche Art von Objekt es sich bei
der Variablen in dem Moment handelt. Abhängig davon wird entweder die zum Objekt gehörende
Operation aufgerufen (die von der entsprechenden Klasse definierte Methode durchgeführt), oder
es wird ausgegeben, dass die Operation nicht sinnvoll ist.
In C++ nutzt man zur Realisierung der Polymorphie im Normalfall die Vererbung.5 Wenn
ähnliche Klassen die gleiche Operation verschieden implementieren, dann kann diese bereits in
einer gemeinsamen Basisklasse deklariert werden, ohne dass sie dort unbedingt implementiert
werden muss. Verwendet man eine inhomogene Menge von Objekten verschiedener Klassen,
deklariert man diese Menge mit dem Typ der gemeinsamen Basisklasse. Wenn für ein Element in
der Menge dann eine Operation aufgerufen wird, kann veranlasst werden, dass erst zur Laufzeit
geprüft wird, zu welcher Klasse das Objekt tatsächlich gehört, und die dazugehörende Funktion
aufgerufen werden.
Ein Anwendungsprogramm könnte entsprechend eine inhomogene Menge von Autos dekla-
rieren, die verschiedenen Objekte zuweisen und für alle Objekte die Funktion motorWechseln()
aufrufen. In dem Fall wird automatisch die richtige Funktion aufgerufen, die sich aus der tatsäch-
lichen Klasse des jeweiligen Autos ergibt:
AutoMenge autos; // Menge von Autos
VW v; // Objekt der Klasse VW
Opel o; // Objekt der Klasse Opel

autos.insert(v); // Auto-Menge enthält VW


autos.insert(o); // Auto-Menge enthält Opel

for (i=0; i<autos.anzahl(); ++i) {


/ * Motor von allen Autos wechseln
* - ruft je nach Objekt-Typ
* motorWechseln() für VW,
* motorWechseln() für Opel, ...
* auf
*/
autos[i].motorWechseln();
}

5
Man kann Polymorphie auch mit Hilfe von Templates implementieren. Darauf wird in Abschnitt 7.5.3
eingegangen.
Sandini Bib

20 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

Dabei ist zu beachten, dass es für die Implementierung unerheblich ist, was es für abgeleitete
Klassen von Auto gibt. Es gibt keine fest verdrahtete Fallunterscheidung. Wenn man sich ent-
schließt, eine neue Auto-Klasse als abgeleitete Klasse von Auto zu implementieren, so kann
autos auch diese enthalten, ohne neu kompiliert werden zu müssen.

2.3 Weitere Konzepte von C++


In C++ gibt es drei weitere wesentliche Konzepte, die die objektorientierten Sprachmittel un-
terstützen bzw. ergänzen: die Ausnahmebehandlung, Templates und Namensbereiche.

2.3.1 Ausnahmebehandlung
Auch bei abstrakten Datentypen kann es zu unplanmäßigen Problemen kommen. So kann z.B.
eine Deklaration einer Menge nicht gelingen, wenn der dafür anzulegende Speicherplatz nicht
vorhanden ist. Deklarationen besitzen allerdings keinen Rückgabewert.
In einem solchen Fall gibt es zunächst die Möglichkeit, das Programm mit einer Fehlermel-
dung zu beenden oder eine entsprechende Warnung auszugeben und fortzufahren. Ob und wie
weit das sinnvoll ist, hängt allerdings sehr oft von der Situation ab und kann nicht generell sinn-
voll festgelegt werden.
Aus diesem Grund wurde ein Konzept für die Ausnahmebehandlung, die so genannte Aus-
nahmebehandlung (englisch: exception handling), in C++ integriert. Wenn eine unerwartete Si-
tuation eintritt, wird eine entsprechende Ausnahme definiert, die vom Anwendungsprogramm
abgefangen und ausgewertet werden kann. Auf diese Weise wird insbesondere die Erkennung
einer Ausnahmesituation von deren Behandlung getrennt. Erkannt wird eine Ausnahme in Funk-
tionen oder Klassen. Behandelt wird sie vom Anwender der Funktion oder der Klasse. Dieser
kennt die Situation, in der die Ausnahme entstand, und kann entsprechend sinnvoll reagieren.
Da das Konzept der Ausnahmebehandlung nicht Parameter und Rückgabewerte von Funktio-
nen verwendet, ergibt sich noch ein anderer Vorteil dieser Art von Fehlerbehandlung: Der norma-
le Datenfluss wird von der Fehlerbehandlung entkoppelt. In herkömmlichen Programmen müssen
oft Rückgabewerte auf Fehlerfälle geprüft werden und dienen damit nicht nur dem Zweck des
normalen Datenflusses. Das hat zur Folge, dass der normale Datenfluss ständig von Anweisun-
gen für den Fehlerfall unterbrochen wird. Mit der Ausnahmebehandlung wird sauber zwischen
normalem Datenfluss und Fehlerbehandlung getrennt.
Da wir uns in einer objektorientierten Sprachumgebung befinden, ist es nur konsequent, eine
solche Ausnahme als Objekt zu definieren. Entsprechend gibt es Klassen, die gleichartige Objek-
te, also gleichartige Ausnahmen mit deren Eigenschaften, definieren. Sie können sogar in einer
hierarchischen Beziehung zueinander stehen, wodurch verschiedene Typen von Ausnahmen ver-
allgemeinert oder spezialisiert und entsprechend auch allgemein oder speziell behandelt werden
können.
Sandini Bib

2.3 Weitere Konzepte von C++ 21

2.3.2 Templates
Manche objektorientierten Sprachen besitzen Typfreiheit. Das bedeutet, eine Variable ist nicht
von einem bestimmten Typ, sondern kann für alles Mögliche stehen. So kann einer Variable z.B.
einmal ein ganzzahliger Wert, danach eine Menge und dann ein String zugewiesen werden.
In C++ gibt es diese Typfreiheit nicht. Variablen besitzen in C++ also immer einen bestimm-
ten Typ. Dies ist nicht unbedingt ein Nachteil, da dadurch immer schon beim Kompilieren si-
chergestellt wird, dass Typen nicht vermischt werden und eine Operation für ein Objekt auch
aufrufbar ist.
Eine Typbindung kann aber auch sehr hinderlich sein, wie das Beispiel der Automenge zeigt.
Für alle möglichen Datentypen, für die eine Menge gebraucht wird, muss auch eine entsprechen-
de Mengenklasse implementiert werden.
Um diese Nachteile der Typbindung zu umgehen, wurden in C++ Templates (Schablonen)
eingeführt. Damit können einzelne Funktionen oder ganze Klassen ohne Festlegung auf einen
bestimmten Typ implementiert werden. Es wird eine Schablone für einen oder mehrere angenom-
mene Typen implementiert. Je nach Bedarf kann eine solche Schablone dann für verschiedene
Typen zu realem Programmcode werden.
Auf diese Weise lässt sich z.B. eine Klasse Menge zur Verwaltung aller möglichen Objekt-
arten implementieren:6
template <typename T> // Schablone für angenommenen Datentyp T
class Menge {
private:
T* elems; // dynamisches Feld (Array) von Ts
unsigned anzahl; // aktuelle Anzahl

public:
void insert(T); // Objekt vom Typ T einfügen
T operator[](int); // Indexoperator definieren
int anzahl(); // Anzahl der Elemente liefern

void Menge(); // automatische Initialisierung


void ~Menge() // automatisches Aufräumen bei Zerstörung
};
Der Anwender kann dann definieren, für welche Typen er eine Menge verwenden will:
Menge<Auto> autos; // Menge von Autos
VW v; // Objekt der Klasse VW
Opel o; // Objekt der Klasse Opel

autos.insert(v); // Auto-Menge enthält VW

6
Die hier vorgestellte Implementierung lässt noch einige für das wirkliche Funktionieren notwendige De-
tails außer Acht.
Sandini Bib

22 Kapitel 2: Einleitung: C++ und objektorientierte Programmierung

autos.insert(o); // Auto-Menge enthält Opel

for (i=0; i<autos.anzahl(); ++i) {


// Motor von allen Autos wechseln
autos[i].motorWechseln();
}
Der Vorteil von Templates liegt vor allem darin, dass Datenstrukturen und Algorithmen, auch
wenn sie für verschiedene Typen angewendet werden, nur einmal implementiert werden müssen.
Damit sind beliebige andere Mengen verwendbar:
Menge<Auto> autos; // Menge von Autos
Menge<int> intmenge; // Menge von ganzzahligen Werten
Menge<string> stringmenge; // Menge von Strings
Das spart nicht nur Code, sondern unterstützt auch die Wiederverwendbarkeit. Bei einer Neuim-
plementierung besteht die Gefahr, dass neue Fehler implementiert werden. Bei der Wiederver-
wendung einer existierenden, getesteten Mengen-Implementierung besteht diese Gefahr nicht.

2.3.3 Namensbereiche
Mit dem Konzept der Namensbereiche kann man Symbole logisch gruppieren. Damit werden
einerseits Namenskonflikte vermieden, und andererseits wird deutlich gemacht, welche Symbole
logisch zusammengehören (ein Paket oder eine Komponente bilden).
So sind z.B. alle Symbole der Standardbibliothek im Namensbereich std definiert. Um einen
String aus der Standardbibliothek zu verwenden, muss man diesen Datentyp also genau genom-
men noch entsprechend qualifizieren:
std::string s; // String der Standardbibliothek
Entsprechend sollte man alle selbst definierten Symbole in entsprechenden eigenen Namensbe-
reichen definieren.

2.4 Terminologie
In der objektorientierten Sprachwelt herrscht ein gewisses begriffliches Chaos. Ein und dersel-
be Sachverhalt wird mit unterschiedlichen Begriffen belegt, und ein Begriff wird mitunter sogar
verschieden angewendet. In C++ wird dieses Chaos noch durch die Tatsache erweitert, dass ob-
jektorientierte Begriffe mit Begriffen der herkömmlichen prozeduralen Programmierung, speziell
mit C-Begriffen, vermischt werden. Ich werde deshalb im Folgenden die wichtigsten Begriffe für
dieses Buch abgrenzen.
Eine Instanz wird, da es sich um ein konkretes Objekt einer Klasse handelt, oft auch einfach
nur als Objekt bezeichnet. Dies widerspricht allerdings der Tatsache, dass Objekt manchmal als
Oberbegriff verstanden wird, der mehr meint. Auch eine Klasse kann man als Objekt betrach-
ten. In der Sprache C++ gibt es allerdings keinen Unterschied zwischen den Begriffen Instanz
und Objekt. In der Sprachspezifikation wird der Begriff Instanz überhaupt nicht verwendet. Um-
Sandini Bib

2.4 Terminologie 23

so verwirrender ist es, dass es im Zusammenhang mit Templates den Begriff instantiieren gibt.
Damit ist dann nicht das Anlegen eines Objekts einer Klasse, sondern das Generieren des eigent-
lichen zu kompilierenden Codes aus Template-Code gemeint. Schon aus diesem Grund schließe
ich mich der gängigen Sprachregelung von C++ an und bezeichne Instanzen einer Klasse in der
Regel einfach nur als Objekte der Klasse.
Eine Klasse ist in C++ eine Typbeschreibung. Insofern wird auch oft formuliert, dass ein Ob-
jekt einen bestimmten Typ besitzt, womit dann dessen Klasse gemeint ist. Man könnte festlegen,
dass nur der Begriff Klasse verwendet werden darf. Es gibt aber in C++ auch Typen, die keine
Klassen sind. Wenn in C++ etwas für verschiedene Typen“ gilt, sind nicht nur Klassen, sondern

auch fundamentale Datentypen gemeint. Deshalb werde auch ich den Begriff Typ oder Datentyp
im Sinne von irgendein Datentyp“, der auch eine Klasse sein kann, verwenden.

Bei den Elementen von Klassen gibt es die größte Sprachverwirrung. Sie werden im Um-
feld der objektorientierten Programmierung und im Umfeld von C++ Elemente, Komponenten,
Member, Attribute, Daten usw. genannt. Die für Objekte aufrufbaren Operationen bezeichnet
man als Methoden, Elementfunktionen oder Memberfunktionen. Ich verwende in diesem Buch
für Klassenelemente als Oberbegriff die Bezeichnung Komponente mit der Unterscheidung in
Datenelement und Elementfunktion.
Man kann über diese Namensgebung sicherlich streiten. Ein Leser, der aus der objektorien-
tierten Sprachwelt kommt, fragt sich z.B. mit Recht, warum Elementfunktion statt Methode und
Objekt statt Instanz verwendet wird. Ich habe mich für die C++-Sprachwelt entschieden, da we-
niger das Konzept der objektorientierten Programmierung am Beispiel von C++, sondern mehr
die Verwendung der Sprache C++ aus objektorientierter Sicht im Vordergrund steht.
Auch bei anderen Begriffen gibt es verschiedene Bezeichnungen. So existieren z.B. oft über-
nommene angloamerikanische parallel zu eingedeutschten Bezeichnungen. Im Zweifelsfall ver-
wende ich den Begriff, der meiner Meinung nach verbreiteter ist, worüber man aber auch ohne
Ende streiten kann. So heißt es in diesem Buch Compiler statt Übersetzer und Template statt
Schablone, aber Rückgabewert statt Returnwert und Ausnahme statt Exception.
Letztlich ist die Namensgebung auch einfach eine Frage des Geschmacks und der Gewohn-
heit, die die Lesbarkeit des Buchs hoffentlich nicht allzu sehr beeinflusst. Und eines ist auch
klar: Eine Beibehaltung aller angloamerikanischen Begriffe ist genauso wenig hilfreich wie eine
Übersetzung aller Begriffe. Wie meinte doch ein Reviewer“ so richtig: Kein Weltraum links
” ”
auf dem Gerät“ ist wesentlich unverständlicher als No space left on device“ ;-).

Sandini Bib
Sandini Bib

Kapitel 3
Grundkonzepte von
C++-Programmen

C++ kann man aus verschiedenen Blickwinkeln betrachten. Auf der einen Seite gibt es die Sicht-
weise der Anwendungsprogrammierer, die Datentypen und Klassen mit Hilfe von Funktionen
und Kontrollstrukturen zu laufenden Programmen kombinieren. Auf der anderen Seite gibt es
die Sichtweise der Systemprogrammierer, die neue Datentypen und Klassen zur Verfügung stel-
len und damit Details abstrahieren. Viele Programmierer haben in der Praxis oft beide Rollen.
Dies gilt insbesondere deshalb, weil sie im Zuge der Abstrahierung von Problemen immer wieder
Datentypen und Klassen verwenden, um höhere Datentypen und Klassen zu programmieren.
In diesem Kapitel beschränken wir uns zunächst auf die Grundkonzepte, die für die Sicht-
weise der Anwendungsprogrammierer relevant sind. Dazu gehören der prinzipielle Aufbau von
C++-Programmen, die Syntax von Funktionen und Funktionsaufrufen, die Verwendung der fun-
damentalen Datentypen, die Aufteilung von Programmen in Module und die Verwendung der
wichtigsten Datentypen und Klassen aus der Standardbibliothek.
In den folgenden Kapiteln werden wir dann vor allem sehen, wie man eigene Datentypen und
Klassen implementiert.

25
Sandini Bib

26 Kapitel 3: Grundkonzepte von C++-Programmen

3.1 Das erste Programm


Dieser Abschnitt enthält das erste C++-Programm. Anhand dieses Beispiels werden die dazu-
gehörigen grundlegenden Sprachmittel eingeführt.

3.1.1 Hallo, Welt!“



Das folgende erste Programm gibt einfach den String Hallo, Welt!“ aus:1

// allg/hallo.cpp
/* Das erste C++-Programm
* - gibt einfach nur ”Hallo, Welt!” aus
*/

#include <iostream> // Deklarationen für Ein-/Ausgaben

int main () // Hauptfunktion main()


{
/ * ”Hallo, Welt!” auf Standard-Ausgabekanal std::cout
* gefolgt von einem Zeilenende (std::endl) ausgeben
*/
std::cout << "Hallo, Welt!" << std::endl;
}
Grob formuliert arbeitet das Programm wie folgt:
 Mit
#include <iostream>
werden alle notwendigen Deklarationen für Ein- und Ausgaben eingebunden. Damit besteht
Zugriff auf spezielle Symbole und Operationen für Ein- und Ausgaben. Eine derartige An-
weisung befindet sich typischerweise in allen Programmen, die Ein-/Ausgaben tätigen.
 Der Programmablauf wird in der Hauptfunktion main() definiert:
int main()
{
...
}

1
Falls sich dieses Programm nicht übersetzen lässt, kann dies daran liegen, dass ein C++-Compiler verwen-
det wird, der noch nicht standardkonform ist. In dem Fall könnte eine modifizierte Form dieses Programms
funktionieren, die in Abschnitt 3.1.5 auf Seite 31 vorgestellt wird.
Sandini Bib

3.1 Das erste Programm 27

Eine Funktion mit diesem Namen muss in allen C++-Programmen vorhanden sein. Sie wird
zum Programmstart automatisch aufgerufen. Mit dem Verlassen dieser Funktion wird das
Programm beendet.
 Innerhalb von main() gibt es nur eine Anweisung, die wie jede Anweisung mit einem Semi-
kolon endet:
std::cout << "Hallo, Welt!" << std::endl;
Diese Anweisung gibt auf den Standard-Ausgabekanal std::cout nacheinander den String
"Hallo, Welt!" und das Symbol std::endl aus. Das Symbol std::endl steht für En-

de der Zeile“ ( endline“). Es sorgt dafür, dass ein Zeilenumbruch ausgegeben wird und der

Ausgabepuffer geleert wird.
Ergänzt wird das Programm von verschiedenen Kommentaren, die entweder durch /* und */
eingegrenzt werden oder von // bis zum Zeilenende reichen.
Die folgenden Abschnitte gehen auf die hier verwendeten Sprachmittel genauer ein.

3.1.2 Kommentare in C++


In C++ sind zwei Arten der Kommentierung möglich:
 Die eine (von C übernommene) Art erlaubt eine Kommentierung über beliebig viele Zeilen.
Sie wird durch die Zeichenfolgen /*“ und */“ eingegrenzt:
” ”
/* Dies ist ein Kommentar
über mehrere Zeilen */

 Die andere Art (die in C++ neu hinzugekommen ist) ist die Möglichkeit, den Rest einer Zeile
auszukommentieren. Zwei Schrägstriche //“ führen (solange sie sich nicht in einem String

befinden) einen Kommentar ein, der bis zum Zeilenende reicht:
// Dies ist ein Kommentar, der bis zum Zeilenende reicht

Die beiden Arten der Kommentierung sind voneinander unabhängig. Ein mit //“ begonnener

Kommentar wird mit */“ nicht beendet:

// Dieser Kommentar */ reicht wirklich bis zum Zeilenende

/* Dieser Kommentar mit // darin


endet erst hier: */
Kommentare können nicht geschachtelt werden:
/* Der hier begonnene Kommentar
/* endet also genau hier: */
Die verschiedenen Möglichkeiten der Kommentierung führen natürlich zu der Frage, welche
Art der Kommentierung wofür einzusetzen ist. Dies ist im Wesentlichen Geschmackssache. Als
kleine Richtlinie kann vielleicht die Empfehlung dienen, mehrzeiligen Kommentar durch /*
und */ einzugrenzen und für einzeiligen Kommentar // zu verwenden. Dabei bevorzuge ich
Sandini Bib

28 Kapitel 3: Grundkonzepte von C++-Programmen

eine mehrzeilige Kommentarform, bei der jede weitere Kommentarzeile jeweils mit einem Stern
beginnt:
/* erste Kommentar-Zeile
* weitere Kommentar-Zeile
* ...
* letzte Kommentar-Zeile
*/
Diese Form hebt sich vom restlichen Code hervorragend ab.

3.1.3 Hauptfunktion main()


Ein C++-Programm setzt sich aus verschiedenen Funktionen zusammen. Der Begriff Funktion
wird dabei für jede Art von Unterprogramm“ (Sub-Routine, Prozedur, oder welche Bezeichnung

man auch immer aus anderen Programmiersprachen kennt) verwendet. Einer Funktion können
Argumente als Parameter übergeben werden und sie kann (muss aber nicht) einen Rückgabe-
wert zurückliefern. Eine Prozedur ist in C++ also eine Funktion, die nichts zurückliefert (dafür
existiert der Rückgabetyp void).
In jedem C++-Programm existiert eine besondere Funktion, die Funktion main(). Diese
Funktion muss in einem C++-Programm immer genau einmal vorhanden sein. Zum Programm-
start wird diese Funktion automatisch aufgerufen. Mit dem Beenden von main() wird das Pro-
gramm als Ganzes beendet. main() stellt somit die Klammer um den gesamten Programmablauf
dar.
Am Beispiel von main() kann man den Aufbau von Funktionen erkennen:
 Der Funktionskopf besteht aus
– dem Rückgabetyp (dem Datentyp des Rückgabewerts),
– dem Funktionsnamen und
– der Parameterliste, die durch runde Klammern eingeschlossen wird.
 Der Funktionsrumpf besteht aus einem Block von Anweisungen. Dieser Block wird durch
geschweifte Klammern eingeschlossen. Innerhalb des Blocks befinden sich Anweisungen,
die jeweils mit einem Semikolon abgeschlossen werden. Im Unterschied zu anderen Sprachen
muss auch die letzte Anweisung jeweils mit einem Semikolon enden. Der Block selbst wird
aber nicht mit einem Semikolon abgeschlossen.
Der Rückgabetyp von main() ist immer int. int steht für integer“ und repräsentiert einen

ganzzahligen Datentyp (siehe die Liste der Datentypen auf Seite 36). Der dazugehörige Rück-
gabewert wird vom Programm an die aufrufende Umgebung zurückgeliefert. main() liefert also
einen ganzzahligen Wert an die Aufrufumgebung von main() zurück. Damit kann in der Auf-
rufumgebung des Programms auswertet werden, ob das Programm erfolgreich gelaufen ist. Als
Konvention bedeutet der Rückgabewert 0 bei main(), dass das Programm erfolgreich gelaufen
ist; jeder andere Wert bedeutet, dass das Programm nicht erfolgreich gelaufen ist. Dabei kann
jeder Programmierer selbst festlegen, welche von 0 verschiedenen Rückgabewerte von main()
was bedeuten.
Sandini Bib

3.1 Das erste Programm 29

Eigentlich müsste jede Funktion, die einen Rückgabewert deklariert, auch eine dazugehörige
Return-Anweisung besitzen, mit der der Wert zurückgeliefert wird. Doch main() bildet hier
eine Ausnahme. Am Ende von main() befindet sich immer eine implizite Anweisung, die 0
zurückliefert:
return 0;
Jedes Programm ist also als Default immer erfolgreich gelaufen. Man kann die Anweisung
natürlich auch explizit hinschreiben:2
#include <iostream> // Deklarationen für Ein-/Ausgaben

int main () // Hauptfunktion main()


{
std::cout << "Hallo, Welt!" << std::endl;
return 0; // bei main() eigentlich unnötig
}
Hier unterscheidet sich C++ von C. In C gibt es kein implizites return 0 am Ende von main().
Dies bedeutet, dass C-Programme, die in main() keine Return-Anweisung enthalten, im Ge-
gensatz zu C++-Programmen einen undefinierten Wert (im Sinne von irgendeinen beliebigen“

Wert) an die Aufrufumgebung des Programms liefern.
Einem Programm können vom Aufrufer auch Argumente übergeben werden, die in main()
als Parameter ankommen. Darauf wird in Abschnitt 3.9.1 auf Seite 126 eingegangen.

3.1.4 Ein-/Ausgaben
Die Funktionalität von Ein-/Ausgaben ist in C++ kein eigentliches Sprachmittel, sondern eine
Anwendung von Elementen, die mit Hilfe der Sprachmittel programmiert wurden und über eine
Standardbibliothek angeboten werden. Für die Ein-/Ausgabe werden also Sprachmittel angewen-
det, die in diesem Buch erst noch vermittelt werden. Aus diesem Grund wird an dieser Stelle nur
ganz rudimentär auf die Grundkonzepte der Ein-/Ausgabe eingegangen, so dass man einfache
Ein- und Ausgaben formulieren kann.
Der Mechanismus ist aus Anwendersicht relativ einfach. Zunächst müssen die standardisier-
ten Sprachmittel von C++ für Ein- und Ausgaben im Programm bekannt gemacht werden. Dies
geschieht mit Hilfe einer Include-Anweisung:
#include <iostream>
Diese Include-Anweisung wird vom so genannten Präprozessor bearbeitet. Dieser fügt die in
iostream befindlichen Deklarationen ein, als wären sie direkt hier von Hand hingeschrieben
worden.
Mit der Einbindung der Deklarationen zu den IOStreams (Datenströme zur Ein- und Ausga-
be) werden unter anderem die in Tabelle 3.1 aufgeführten Symbole definiert.

2
Falls Ihr Compiler meint, dass im ersten Beispiel am Ende von main() eine entsprechende Return-
Anweisung fehlt, ist er leider noch nicht standardkonform.
Sandini Bib

30 Kapitel 3: Grundkonzepte von C++-Programmen

Symbol Bedeutung
std::cin Standard-Eingabekanal (typischerweise die Tastatur)
std::cout Standard-Ausgabekanal (typischerweise der Bildschirm)
std::cerr Standard-Fehlerausgabekanal (typischerweise der Bildschirm)
std::endl Symbol für die Ausgabe eines Zeilenumbruchs und das wirkliche Ab-
senden der Ausgabe ( newline“ und flushing“)
” ”
Tabelle 3.1: Fundamentale Symbole der Standard-Ein-/Ausgabe

Die Eingabe erfolgt über den Standard-Eingabekanal, dem im Allgemeinen die Tastatur zuge-
ordnet ist. Die Ausgabe erfolgt auf den Standard-Ausgabekanal, dem im Allgemeinen der Bild-
schirm zugeordnet ist. Beides kann aber vom Betriebssystem oder mit Hilfe von Systemfunktio-
nen auch umgelenkt“ werden.

Für Fehlermeldungen existiert zusätzlich der Standard-Fehlerausgabekanal, dem im Allge-
meinen ebenfalls der Bildschirm zugeordnet ist. Durch Trennung von normalen und Fehleraus-
gaben können diese Ausgaben von der Umgebung, in der das Programm läuft, unterschiedlich
behandelt werden. Auf diese Weise können z.B. Fehlermeldungen in einer Datei protokolliert
werden.
Um Daten auszugeben, müssen diese einfach mit dem Operator << an den Ausgabekanal
gesendet“ werden. Dabei ist es möglich, mehrere Ausgaben zu verketten:

#include <iostream>
...
std::cout << "Jetzt kommt eine Zahl: " << 42 << std::endl;
Die ganze Zahl 42 wird dabei automatisch in die Zeichenfolge 4“ gefolgt von 2“ konvertiert,
” ”
und std::endl hängt ein Zeilenende an und sorgt dafür, dass der Ausgabepuffer auch geleert
wird.
Entsprechend kann man mit dem Operator >> von einem Eingabekanal lesen. Dazu folgen
noch Beispiele.

3.1.5 Namensbereiche
Beide Symbole der Ein-/Ausgabe-Bibliothek beginnen mit std::. Dieses Präfix legt fest, dass
cout und endl jeweils im Namensbereich std definiert sind. Dieser Namensbereich steht für
die Standardbibliothek von C++.
Mit dem Konzept der Namensbereiche kann man Symbole logisch gruppieren. Damit werden
einerseits Namenskonflikte vermieden und andererseits wird deutlich gemacht, welche Symbole
logisch zusammengehören (ein Paket oder eine Komponente bilden). Mit std::cout verwenden
wir also das Symbol cout der Komponente mit dem Namen std.
Das Konzept der Namensbereiche wurde erst im Rahmen der Standardisierung von C++ ein-
geführt. In den ersten Versionen von C++ gab es kein derartiges Konzept. Entsprechend hatten
die Headerdateien, die Symbole für die Anwendung von existierenden Bibliotheken und Kompo-
nenten definieren, einen anderen Aufbau. Insofern kann es vorkommen, dass man auf Programme
Sandini Bib

3.1 Das erste Programm 31

trifft, die einen leicht modifizierten Aufbau haben. Die frühere Version von main() sah in etwa
wie folgt aus:
// allg/halloalt.cpp
/* Das erste C++-Programm
* - Version für noch nicht standardkonforme Umgebungen
*/

#include <iostream.h> // Deklarationen für Ein-/Ausgaben

int main () // Hauptfunktion main()


{
/* ”Hallo, Welt!” auf Standard-Ausgabekanal cout
* gefolgt von einem Zeilenende (endl) ausgeben
*/
cout << "Hallo, Welt!" << endl;
}
Im Wesentlichen gibt es folgende Unterschiede:
 Alle Symbole der Standardbibliothek wurden global und nicht in einem besonderen Namens-
bereich definiert. Die Standard-Ausgabe wird somit mit cout statt mit std::cout angespro-
chen.
 Entsprechend wurden andere Headerdateien verwendet, die die Endung .h besitzen. Statt
#include <iostream>
wurde also
#include <iostream.h>
eingebunden.

3.1.6 Zusammenfassung
 C++-Programme besitzen die Hauptfunktion main(). Diese Funktion wird zum Programm-
start automatisch aufgerufen, und mit dem Verlassen dieser Funktion wird das Programm
beendet.
 main() kann mit return einen ganzzahligen Wert zurückliefern, der angibt, ob das Pro-
gramm erfolgreich gelaufen ist. Die Anweisung
return 0;
steht für ein Programmende nach einem erfolgreichen Programmverlauf; jeder andere Wert
steht für einen nicht erfolgreichen Programmverlauf.
Sandini Bib

32 Kapitel 3: Grundkonzepte von C++-Programmen

 main() besitzt am Ende ein implizites


return 0;

 Jede Anweisung muss grundsätzlich mit einem Semikolon abgeschlossen werden.


 Durch #include <iostream> sind die Variablen std::cout und std::endl definiert, die
mit Hilfe des Operators << Ausgaben ermöglichen.
 Alle Symbole der Standardbibliothek befinden sich im Namensbereich std, was durch die
Qualifizierung mit std:: verdeutlicht wird.
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 33

3.2 Datentypen, Operatoren, Kontrollstrukturen


Dieser Abschnitt stellt die elementaren Sprachmittel von C++ vor, die in jeder höheren Program-
miersprache angeboten werden: fundamentale Datentypen, Operatoren und Kontrollstrukturen.
Zunächst wird dazu ein weiteres Programm vorgestellt. Im Anschluss werden die darin vor-
kommenden einzelnen Sprachmittel generell erläutert.

3.2.1 Ein erstes Programm, das wirklich etwas berechnet


Zur Einführung der elementaren Sprachmittel soll zunächst ein einfaches Programm dienen. Die-
ses Programm berechnet Folgendes: Es gibt einige wenige vierstellige Zahlen, die, wenn man die
ersten beiden Stellen von den letzten beiden Stellen abtrennt, die abgetrennten zweistelligen Zah-
len quadriert und dann wieder addiert, wieder die ursprüngliche vierstellige Zahl ergeben. Dies
trifft z.B. auf die Zahl 1233 zu:
2 2
1233 ist das Gleiche wie 12 + 33
Hier folgt das Programm, das diese Zahlen berechnet:
// allg/vierstellig.cpp
#include <iostream> // C++-Headerdatei für Ein-/Ausgaben

int main ()
{
int zaehler = 0; // aktuelle Anzahl der gefundenen Zahlen

// für jede Zahl zahl von 1000 bis 9999


for (int zahl=1000; zahl<10000; ++zahl) {

// die vorderen und hinteren beiden Ziffern abspalten


int vorn = zahl/100; // die ersten beiden Ziffern
int hinten = zahl%100; // die letzten beiden Ziffern

// Falls die Summe der Quadrate die ursprüngliche Zahl ergibt,


// Zahl ausgeben und Zähler inkrementieren
if (vorn*vorn + hinten*hinten == zahl) {
std::cout << zahl << " == "
<< vorn << "*" << vorn << " + "
<< hinten << "*" << hinten << std::endl;
++zaehler;
}
}

// Anzahl der gefundenen Zahlen ausgeben


std::cout << zaehler << " Zahlen gefunden" << std::endl;
}
Sandini Bib

34 Kapitel 3: Grundkonzepte von C++-Programmen

Auch in diesem Programm wird das Verhalten des Programms innerhalb der Funktion main()
programmiert:
int main()
{
...
}
Dort wird zunächst eine Variable definiert, die dazu dient zu zählen, wie viele vierstellige Zahlen
gefunden wurden, die die geforderte Eigenschaft besitzen:
int zaehler = 0;
Der Datentyp int steht für ganzzahlige Werte. Es handelt sich um einen fundamentalen Datentyp
der Sprache, der von C übernommen wurde. zaehler ist der Name der Variablen. Eine Variable
ist ab dem Zeitpunkt ihrer Deklaration bis zum Ende des Blocks, in dem sie deklariert wird,
bekannt. Ein Block wird jeweils durch geschweifte Klammern eingeschlossen. zaehler ist also
bis zum Ende der Funktion main() bekannt.
zaehler wird mit 0 initialisiert. Gibt es keine derartige Initialisierung, dann ist der initiale
Wert dieser Variablen nicht definiert! Es gibt für derartige lokale Variablen also nicht unbedingt
immer einen Default-Wert. Aus diesem Grund sollte man den Startwert einer Variablen, sofern
dieser relevant ist, immer explizit angeben.
Den Hauptteil von main() bildet eine For-Schleife:
// für jede Zahl zahl von 1000 bis 9999
for (int zahl=1000; zahl<10000; ++zahl) {
...
}
For-Schleifen sind ein in C++ verallgemeinerter Mechanismus für Schleifen, die über bestimmte
Werte iterieren. Eigentlich ist eine For-Schleife nichts anderes als eine Kombination von drei
Anweisungen:
1. Die erste Anweisung wird am Anfang einmal durchgeführt:
int zahl=1000 // Laufvariable zahl mit 1000 initialisieren

2. Die mittlere Anweisung definiert die Bedingung, unter der die Schleife läuft:
zahl<10000 // so lange zahl kleiner als 10000 ist,

3. Die letzte Anweisung wird nach jedem Schleifendurchlauf einmal durchgeführt:


++zahl // zahl inkrementieren (um eins erhöhen)

Man könnte die gesamte Schleife auch wie folgt schreiben:


{
int zahl=1000;
while (zahl<10000) {
...
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 35

++zahl;
}
}
Die Schleife initialisiert also eine Laufvariable zahl mit dem Wert 1.000 und lässt diese Varia-
ble für jeden Wert, der kleiner als 10.000 ist, die entsprechenden Anweisungen in der Schleife
durchführen. Die Schleife wird also für alle Werte von 1.000 bis 9.999 aufgerufen, wobei der
jeweils aktuelle Wert in zahl steht.
Innerhalb der Schleife wird jeweils untersucht, ob das Quadrat der beiden vorderen Ziffern
plus das Quadrat der beiden hinteren Ziffern wieder die ursprüngliche Zahl ergibt. Dazu werden
zunächst die beiden vorderen und hinteren Ziffern abgespalten:
int vorn = zahl/100; // die ersten beiden Ziffern
int hinten = zahl%100; // die letzten beiden Ziffern
Der Operator / ist der Divisionsoperator und teilt zahl ganzzahlig durch Hundert. Der Operator
% ist der Modulo-Operator und liefert den Rest einer ganzzahligen Division.
Mit einer If-Anweisung wird dann geprüft, ob die Summe der Quadrate dieser beiden Zahlen
wieder die ursprüngliche Zahl ergibt:
if (vorn*vorn + hinten*hinten == zahl) {
...
}
Der Operator * ist der Multiplikationsoperator, und + bildet die Summe. Ungewöhnlich mag
die Verwendung von zwei Gleichheitszeichen zum Vergleichen sein. Dies liegt daran, dass das
einfache Gleichheitszeichen für Zuweisungen verwendet wird:
a = b; // a bekommt den Wert von b
Die Designer von C hatten für Zuweisungen das einfache Gleichheitszeichen gewählt, da dieser
Operator mit Abstand am häufigsten vorkommt und es somit unnötig Zeit und Platz kostet, dafür
durch eine Schreibweise wie := zwei Zeichen zu verwenden.
Ist die Bedingung erfüllt, wird eine entsprechende Meldung ausgegeben:
std::cout << zahl << " == "
<< vorn << "*" << vorn << " + "
<< hinten << "*" << hinten << std::endl;
In dieser mehrzeiligen Anweisung werden nacheinander der Wert von zahl, gefolgt von den
Zeichen == “, gefolgt von dem Wert von vorn, gefolgt von dem Zeichen *“, nochmal gefolgt
” ”
von dem Wert von vorn und so weiter ausgegeben. Eine Ausgabezeile sieht z.B. wie folgt aus:
1233 == 12*12 + 33*33
Anschließend wird der Zähler für das Auftreten der Eigenschaft mit dem Inkrement-Operator
um eins erhöht:
++zaehler;
Sandini Bib

36 Kapitel 3: Grundkonzepte von C++-Programmen

Am Ende der Schleife wird dessen Wert dann ausgegeben:


std::cout << zaehler << " Zahlen gefunden" << std::endl;
Die folgenden Abschnitte gehen auf die hier verwendeten Sprachmittel wie Variablen, funda-
mentale Datentypen und Kontrollstrukturen im Einzelnen ein.

3.2.2 Fundamentale Datentypen


C++ verfügt über die in Tabelle 3.2 aufgelisteten fundamentalen Datentypen (FDTs). Bis auf
bool wurden diese Datentypen von C übernommen. Die folgenden Unterabschnitte geben spe-
zielle Hinweise zu den einzelnen Datentypen.

Datentyp Bedeutung
int ganzzahliger Wert (typische Wortgröße des Rechners)
float Gleitkommawert einfacher Genauigkeit
double Gleitkommawert doppelter Genauigkeit
char Zeichen (kann auch als ganzzahliger Wert verwendet werden)
bool Boolescher Wert (true oder false)
enum für Aufzählungstypen (Namen, die ganzzahlige Werte repräsentieren)
void nichts“ (für Funktionen ohne Rückgabewert

und leere Parameterlisten in ANSI-C)

Tabelle 3.2: Liste der fundamentalen Datentypen

Numerische Datentypen
Die numerischen Datentypen (int, float, double) können auf verschiedenen Plattformen eine
unterschiedliche Größe und damit einen unterschiedlichen Wertebereich besitzen. Die tatsächli-
che Größe der numerischen Datentypen ist in C++ also nicht festgelegt. Dahinter steckt das von
C übernommene Konzept, dass int die für die darunter liegende Hardware typische Größe be-
sitzt. Bei einem 32-Bit-Rechner hätte int also typischerweise 32 Bits, bei einem 64-Bit-Rechner
entsprechend typischerweise 64 Bits.
Allerdings kann die Größe und auch die Frage, ob es sich um einen vorzeichenfreien oder
vorzeichenbehafteten Datentyp handelt, durch die in Tabelle 3.3 aufgelisteten Attribute genauer
qualifiziert werden.

Attribut Bedeutung
short eventuell kleiner als normal“

long eventuell größer als normal“

unsigned vorzeichenfrei
signed vorzeichenbehaftet

Tabelle 3.3: Qualifizierende Attribute zu fundamentalen Datentypen


Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 37

Bei der Angabe eines qualifizierenden Attributs kann die Angabe des eigentlichen fundamentalen
Datentyps dann sogar entfallen. In diesem Fall wird int angenommen:
int x1; // Integer mit normalem Wertebereich
long int x2; // Integer mit eventuell größerem Wertebereich
long x3; // dito
unsigned x4; // vorzeichenfreier Integer mit normalem Wertebereich
Unter Standard-C++ kann dabei von den in Tabelle 3.4 aufgelisteten Mindestgrößen ausgegangen
werden.

Datentyp Mindestgröße
char 1 Byte (8 Bits)
short int 2 Byte (16 Bits)
int 2 Byte (16 Bits)
long int 4 Byte (32 Bits)
float 6 Stellen bis 1037
double 10 Stellen bis 1037
long double 10 Stellen bis 1037

Tabelle 3.4: Mindestgrößen für fundamentale Datentypen

Die genauen Größen werden mit Hilfe der numeric_limits in der Headerdateien <limits> mit
zahlreichen weiteren Informationen zum Datentyp definiert (siehe Abschnitt 9.1.4 auf Seite 533).

Zeichen
Für einzelne Zeichen wird der Datentyp char verwendet. Zeichen-Literale werden jeweils in
einfache Anführungsstriche eingeschlossen. 'a' steht also für das Zeichen a“. Zwischen den

Anführungsstrichen darf jedes beliebige Zeichen bis auf einen Zeilenumbruch stehen. Man-
che Zeichen müssen allerdings mit einem Rückstrich (Backslash) maskiert werden. Außerdem
können mit Hilfe des Rückstrichs Sonderzeichen verwendet werden (siehe Tabelle 3.5). Das Lite-
ral '\n' steht also für einen Zeilenumbruch, das Literal '\'' steht für den einfachen Anführungs-
strich, und das Literal '\\' steht für den Rückstrich selbst.
Diese Sonderzeichen dürfen auch in String-Literalen verwendet werden. Der String
"Einige Sonderzeichen: \"\t\'\n\\\100\x3F\n"
steht also für folgende Zeichenfolge (im ASCII-Zeichensatz hat @ den oktalen Wert 100 und das
Fragezeichen den hexadezimalen Wert 3F):
Einige Sonderzeichen: " '
\@?
Zeichen werden intern wie ganzzahlige Werte verwaltet. Genau genommen ist '\n' nur eine an-
dere Repräsentation für den Wert des Zeichens für den Zeilenumbruch im aktuellen Zeichensatz.
Man könnte den Typ char somit auch für Werte mit der Größe 1-Byte verwenden. Mit dem qua-
Sandini Bib

38 Kapitel 3: Grundkonzepte von C++-Programmen

Zeichen Bedeutung
\' einfacher Anführungsstrich
\" doppelter Anführungsstrich
\\ Rückstrich (Backslash)
\n Zeilenumbruch / Newline
\t Tabulator-Zeichen
\oct-ziffern Zeichen mit oktalem Wert aus ein bis drei Ziffern
\xhex-ziffern Zeichen mit hexadezimalem Wert
\b Backspace
\f Form-Feed
\r Carriage-Return

Tabelle 3.5: Sonderzeichen

lifizierenden Attribut signed oder unsigned könnte man dabei festlegen, ob der Wertebereich
von -128 bis 127 oder von 0 bis 255 geht.3 Ohne Qualifizierung ist dies nicht definiert.
Mit diesem Wissen kann man ein einfaches Programm schreiben, das den aktuellen Zeichen-
satz ausgibt. Das folgende Programm gibt alle Zeichen mit den Werten 32 bis 126 aus (die Werte
unter 32 und der Wert 127 sind im ASCII-Zeichensatz Sonderzeichen):
// allg/charset.cpp
/* Zeichensatz ausgeben
*/

#include <iostream> // Deklarationen für Ein-/Ausgaben

int main ()
{
// für jedes Zeichen c mit einem Wert von 32 bis 126
for (unsigned char c=32; c<127; ++c) {
// Wert als Zahl und als Zeichen ausgeben:
std::cout << "Wert: " << static_cast<int>(c)
<< " Zeichen: " << c
<< std::endl;
}
}
Im Programm wird c als ganzzahliger Wert verwendet, der über die Werte ab 32 iteriert. Innerhalb
der Schleife wird c einmal als Zahl und einmal als Zeichen ausgegeben:
std::cout << ... << static_cast<int>(c) << ... << c << ...;

3
Da vom Standard nur die Mindestgröße von chars festgelegt wird, kann der tatsächliche Wertebereich
auch darüber hinausgehen.
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 39

Da c den Datentyp char hat, wird es standardmäßig als Zeichen ausgegeben. Durch den Aus-
druck
static_cast<int>(c)
wird c explizit in den Datentyp int umgewandelt. static_cast ist ein Operator, der Grundsätz-
lich zur logischen Typumwandlung verwendet werden kann, sofern diese Typumwandlung auch
möglich ist. Auf diese Weise wird sichergestellt, dass der ganzzahlige Wert von c ausgegeben
wird. Die Ausgabe lautet (bei einem ASCII-Zeichensatz):
Wert: 32 Zeichen:
Wert: 33 Zeichen: !
Wert: 34 Zeichen: "
Wert: 35 Zeichen: #
Wert: 36 Zeichen: $
Wert: 37 Zeichen: %
Wert: 38 Zeichen: &
...
Wert: 120 Zeichen: x
Wert: 121 Zeichen: y
Wert: 122 Zeichen: z
Wert: 123 Zeichen: {
Wert: 124 Zeichen: |
Wert: 125 Zeichen: }
Wert: 126 Zeichen: ~

Boolesche Werte
Für Boolesche Werte gab es lange Zeit in C++ wie in C keinen speziellen Datentyp. Sie wurden
mit Hilfe von ganzzahligen Werten verwaltet. Dabei steht 0 für false und jeder andere Wert für
true.
In C++ wurde für Boolesche Werte der Datentyp bool mit den Literalen true und false
eingeführt. Um Rückwärtskompatibilität zu gewährleisten, können neben den Konstanten true
und false auch weiterhin noch ganzzahlige Werte verwendet werden. Dabei steht 0 grundsätz-
lich für false und jeder andere Wert für true. Bedingungen in Kontrollstrukturen sind somit
immer dann erfüllt, wenn der darin stehende Ausdruck den Wert true oder einen von null ver-
schiedenen ganzzahligen Wert besitzt.

3.2.3 Operatoren
C++ bietet eine ungewöhnlich große Anzahl von Operatoren. Die grundlegenden Operatoren,
die von C übernommen wurden, werden hier zunächst kurz vorgestellt. Im weiteren Verlauf des
Buchs werden alle anderen Operatoren eingeführt. Auf Seite 571 befindet sich eine Liste aller
C++-Operatoren.
Sandini Bib

40 Kapitel 3: Grundkonzepte von C++-Programmen

Grundlegende Operatoren
Tabelle 3.6 listet die grundlegenden Operatoren auf, die man in jeder Programmiersprache zur
Verknüpfung von Werten wiederfindet.

Operator Bedeutung
+ Addition, positives Vorzeichen
- Subtraktion, negatives Vorzeichen
* Multiplikation
/ Division
% Modulo-Operator (Rest nach Division)
< kleiner
<= kleiner gleich
> größer
>= größer gleich
== gleich
!= ungleich
&& logisches UND (Auswertung bis zum ersten false)
|| logisches ODER (Auswertung bis zum ersten true)
! logische Negation

Tabelle 3.6: Grundlegende Operatoren

Bemerkenswert ist dabei der Aspekt, dass der Test auf Gleichheit mit zwei Gleichheitszeichen
erfolgt. Ein einzelnes Gleichheitszeichen wird als Zuweisungsoperator verwendet.
Vorsicht: Wenn man bei Bedingungen versehentlich nur ein Gleichheitszeichen verwendet,
ist dies durchaus gültiger Code; er macht nur nicht, was er soll. Mit der Zeile
if (x = 42) // korrekter Code, aber unerwünschte Semantik
wird nicht etwa getestet, ob x den Wert 42 hat, sondern es wird x der Wert 42 zugewiesen und
die Bedingung wird als erfüllt betrachtet. Da eine Zuweisung den zugewiesenen Wert liefert,
liefert der Ausdruck x = 42“ insgesamt den Wert 42, was ungleich null ist und somit als true

interpretiert wird. Insofern handelt es sich um korrekten Code, der immer x den Wert 42 zuweist
und bei dem die Bedingung unabhängig vom vorherigen Wert von x immer erfüllt ist.
Von manchen Programmierern wird diese Möglichkeit ausgenutzt, um besonders prägnanten
Code zu schreiben, über dessen Lesbarkeit man streiten kann. So ist es durchaus nicht unüblich,
folgende Bedingungen zu formulieren:
if (x = f()) // weist x den Rückgabewert von f() zu und
// testet gleichzeitig, ob dieser Wert ungleich 0 ist
Derartiger Code ist sicherlich ein Grund dafür, dass C und C++ manchmal einen etwas zwei-
felhaften Ruf haben. Man kann sehr prägnant formulieren, diesen Code aber auch als sehr un-
leserlich empfinden. Dies liegt aber auch daran, dass man derartige Möglichkeiten aus anderen
Sprachen nicht gewohnt ist. Man sollte also mit einer Verdammung derartiger Formulierungen
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 41

warten, bis man sich in C++ etwas zu Hause fühlt. Das bedeutet aber nicht, dass man sich an
alles gewöhnen sollte. Oberstes Ziel ist nach wie vor die Lesbarkeit des Codes.

Zuweisungen
Wie im vorigen Absatz schon erwähnt wurde, ist eine Zuweisung ein Operator, der durch ein
Gleichheitszeichen formuliert wurde.4 Doch C++ definiert wie C nicht nur einen Zuweisungs-
operator. Neben der reinen Zuweisung kann eine Variable mit Hilfe kombinierter Zuweisungs-
operatoren relativ zu ihrem aktuellen Wert modifiziert werden. Tabelle 3.7 listet alle Zuweisungs-
operatoren auf.

Operator Bedeutung
= einfache Zuweisung
*=
/=
%=
+=
-= a op= b“ entspricht im Allgemeinen a = a op b“
” ”
<<=
>>=
&=
^=
|=

Tabelle 3.7: Zuweisungsoperatoren

Für alle Zuweisungsoperatoren gilt, dass eine Zuweisung Teil eines größeren Ausdrucks sein
kann. Ein Ausdruck mit einem Zuweisungsoperator liefert den Wert bzw. die Variable, der ein
neuer Wert zugewiesen wurde. Damit sind insbesondere verkettete Zuweisungen möglich:
x = y = 42; // x und y erhalten den Wert 42
Der Ausdruck wird als
x = (y = 42);
ausgewertet. In der Klammer wird y also zunächst 42 zugewiesen. Der Ausdruck in der Klammer
liefert dann y, was schließlich x zugewiesen wird. Auf diese Weise erhält also auch x den Wert
42.
Entsprechend kann man eine Zuweisung zu einem Teil eines größeren Ausdrucks machen:
if ((x = f()) < 42) // x den Rückgabewert von f() zuweisen und
// diesen Wert mit 42 vergleichen

4
Die Designer von C meinten mit Recht, dass es eigentlich keinen Sinn machte, dem mit Abstand am
häufigsten vorkommenden Operator einen Namen zu geben, der wie in anderen Programmiersprachen
üblich aus zwei Zeichen (wie etwa :=) besteht.
Sandini Bib

42 Kapitel 3: Grundkonzepte von C++-Programmen

Die anderen Zuweisungsoperatoren sind abkürzende Schreibweisen für die Verknüpfung zweier
Operatoren: dem Operator, der vor dem Gleichheitszeichen befindlichen Zeichen, und der Zu-
weisung. Dabei gilt für fundamentale Datentypen immer:
x op= y entspricht x = x op y
Diese Operatoren wurden eingeführt, da Compiler zum Zeitpunkt der Erfindung von C noch nicht
optimierten und C aber zur Implementierung eines laufzeitkritischen Betriebssystems (UNIX)
entwickelt wurde. Indem man statt
x = x + 7; // x um 7 erhöhen
nun
x += 7; // x um 7 erhöhen
schreiben konnte, konnte man wirklich Laufzeit sparen, da der Compiler auch ohne Codeana-
lyse wusste, dass der Wert, zu dem 7 addiert wird, sich an der gleichen Stelle befindet, an der
das Ergebnis der Addition eingetragen werden muss. Heutzutage sollten Compiler derartig tri-
viale Optimierungen in der Regel beherrschen. Die Schreibweise wird aber dennoch weiterhin
verwendet, da sie prägnanter ist, ohne unleserlich zu sein.

Inkrement- und Dekrement-Operatoren


In C wurden auch spezielle Operatoren zum Inkrementieren (erhöhen um 1) und Dekrementieren
(vermindern um 1) eingeführt (siehe Tabelle 3.8). Auch hier lag der Grund für die Einführung
der Operatoren in der Möglichkeit, spezielle Befehle von Prozessoren auch ohne optimierende
Compiler unterstützen zu können.

Operator Bedeutung
++ Postfix-Inkrement ( a++“)

-- Postfix-Dekrement ( a--“)

++ Präfix-Inkrement ( ++a“)

-- Präfix-Dekrement ( --a“)

Tabelle 3.8: Inkrement- und Dekrement-Operatoren

Dabei gibt es aber jeweils zwei Varianten des Inkrement- und Dekrement-Operators, eine Präfix-
und eine Postfix-Version. Beide Versionen erhöhen bzw. vermindern den Wert einer Variablen
um 1. Der Unterschied besteht darin, was der Ausdruck als Ganzes liefert. Die Präfix-Version
(die Version, bei der der Operator vor der Variablen steht) liefert den Wert der Variablen nach der
Erhöhung:
x = 42;
std::cout << ++x; // gibt 43 aus (erst erhöhen, dann Wert von x)
std::cout << x; // gibt 43 aus
Die Postfix-Version (die Version, bei der der Operator hinter der Variablen steht) liefert den Wert
der Variablen vor der Erhöhung:
x = 42;
std::cout << x++; // gibt 42 aus (erst Wert von x, dann erhöhen)
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 43

std::cout << x; // gibt 43 aus


Von diesem Operator leitet sich auch der Name von C++ her: ein Schritt weiter als C“.5

Sofern Inkrement- und Dekrement-Operatoren nicht als Teil eines größeren Ausdrucks ver-
wendet werden, sollte man sich angewöhnen, die Präfix-Version, also z.B. ++x, zu verwenden.
Die andere Version kann dazu führen, dass ein temporärer Wert für den alten Wert von x erzeugt
wird, der nicht immer wegoptimiert werden kann.

Bit-Operatoren
Die von C übernommene Hardware-Nähe von C++ kann man auch an der Tatsache erkennen,
dass es spezielle Operatoren gibt, die bitweise operieren. Sie werden in Tabelle 3.9 aufgelistet.

Operator Bedeutung
<< Links-Shift (es werden Nullen nachgeschoben)
>> Rechts-Shift (es werden Nullen nachgeschoben)
& bitweises UND
^ bitweises XOR (exklusives oder)
| bitweises ODER
~ Bit-Komplement

Tabelle 3.9: Bitweise operierende Operatoren

Die Operatoren << und >> sind uns schon bei der Ein-/Ausgabe begegnet. Die dortige Verwen-
dung ist, wenn man so will, eine Zweckentfremdung dieser Shift-Operatoren für spezielle Daten-
typen, den so genannten I/O-Streams. Diese Verwendung von << und >> führt inzwischen sogar
dazu, dass diese Operatoren in C++ auch Ausgabe- und Eingabeoperatoren genannt werden.6
Entscheidend ist somit, welchen Datentyp jeweils die Operanden besitzen:
 Solange beide Operanden einen fundamentaler ganzzahligen Datentyp besitzen, handelt es
sich um eine Shift-Operation:
int x;
x << 2; // shiftet die Bits in x um zwei Stellen
Der rechte Operand darf dabei nicht negativ sein.
 Ist der erste Operand ein I/O-Stream wie z.B. std::cout, handelt es sich um eine Ein- oder
Ausgabe:
std::cout << 2; // gibt die Zahl 2 aus

5
Eine interessante Frage ist, warum die Sprache nicht ++C“ genannt wurde. Verwendet man C++ als Teil

eines größeren Ausdrucks, liefert dieser Ausdruck den alten Wert, also C :-).
6
Auf die genaue Motivation dieser Zweckentfremdung wird bei der Diskussion der Ein-/Ausgabetechnik
von C++ in Abschnitt 4.5 noch ausführlich eingegangen.
Sandini Bib

44 Kapitel 3: Grundkonzepte von C++-Programmen

Spezielle Operatoren
Drei weitere Operatoren verdienen eine speziellere Beachtung. Sie werden in Tabelle 3.10 auf-
gelistet.

Operator Bedeutung
?: bedingte Bewertung
, Folge von Ausdrücken
sizeof (...) Speicherplatz

Tabelle 3.10: Spezielle Operatoren

 Bedingte Bewertung
Der Operator ?: ermöglicht eine so genannte bedingte Bewertung. Abhängig von einer Be-
dingung wird einer von zwei möglichen Werten geliefert. Es handelt sich dabei um den ein-
zigen dreistelligen Operator von C++.
Der Ausdruck
bedingung ? ausdruck1 : ausdruck2
liefert ausdruck1, wenn die Bedingung erfüllt ist, und sonst ausdruck2.
Dadurch können If-Abfragen abgekürzt werden, bei denen ein Wert sowohl im Then- als
auch im Else-Fall manipuliert wird.
if (x < y) { // z das Minimum von x und y zuweisen
z = x;
}
else {
z = y;
}
kann man kurz Folgendes schreiben:
z = x < y ? x : y;
Wenn x kleiner als y ist, wird x, sonst y zugewiesen.
Ein anderes Beispiel wäre:
std::cout << (x==0 ? "false" : "true") << std::endl;
Falls x den Wert null hat wird false, sonst true ausgegeben. Da dieser Operator eine sehr
geringe Priorität hat, muss er in diesem Fall geklammert werden.
 Komma-Operator
Der Komma-Operator erlaubt es, zwei Anweisungen zu einer Anweisung zusammenzuzie-
hen. Statt
x = 42;
y = 33;
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 45

könnte man auch


x = 42, y = 33;
schreiben. Dabei liefert dieser Gesamtausdruck das Ergebnis des hinter dem Komma liegen-
den Teilausdrucks.
Sinn macht dieser Operator an Stellen, an denen man mehrere Anweisungen durchführen
will, aber nur die Angabe einer Anweisung möglich ist. Ein typisches Beispiel dafür sind
For-Schleifen (siehe Seite 47). Normalerweise sollte man diesen Operator nicht verwenden.
 sizeof-Operator
Der sizeof-Operator liefert die Größe eines Objekts oder Datentyps in Bytes. Dieser Ope-
rator wurde in C vor allem gebraucht, um für Objekte im laufenden Programm Speicherplatz
anzufordern. In C++ wird dieser Operator so gut wie nie benötigt.

Prioritäten und Klammerung


Diese und alle weiteren Operatoren, die im weiteren Verlauf dieses Buches noch eingeführt wer-
den, haben unterschiedliche Prioritäten und Auswertungsreihenfolgen. Die Tabelle aller Opera-
toren auf Seite 571 listet diese auf. Dabei gelten die üblichen Regeln, wie Punktrechnung geht

vor Strichrechnung“.
Die Priorität und damit die Auswertungsreihenfolge kann durch Klammerung verändert wer-
den. Dazu folgen hier zwei (mehr oder weniger sinnvolle) Beispiele:
(x + y) * z // ohne Klammerung würde erst y mit z multipliziert werden
while ((x += 2) < 42 ) // ohne Klammerung würde x um true bzw. 1
// (das Ergebnis von 2 < 42) erhöht werden

3.2.4 Kontrollstrukturen
Alle Arten von Kontrollstrukturen wurden in C++ von C übernommen. Es gibt einfache und
mehrfache Fallunterscheidungen, verschiedene Schleifen und die Möglichkeit, mehrere Anwei-
sungen zu einem Block zusammenzufassen.

Fallunterscheidungen
In C++ gibt es folgende Kontrollstrukturen für Fallunterscheidungen:
 if
dient zur einfachen Fallunterscheidung:
if (x < 7) {
std::cout << "x ist kleiner als 7" << std::endl;
}
else {
std::cout << "x ist groesser oder gleich 7" << std::endl;
}
Der Else-Zweig kann entfallen.
Sandini Bib

46 Kapitel 3: Grundkonzepte von C++-Programmen

 switch
dient zur mehrfachen Fallunterscheidung, wobei nur mit konstanten Werten verglichen wer-
den darf:
switch (x) {
case 7:
std::cout << "x ist 7" << std::endl;
break;
case 17:
case 18:
std::cout << "x ist 17 oder 18" << std::endl;
break;
default:
std::cout << "x ist weder 7 noch 17 noch 18" << std::endl;
break;
}
Mit dem Label case werden jeweils die Stellen definiert, bei denen ein Fall beginnt. Stimmt
der bei switch übergebene Ausdruck mit diesem Wert überein, wird mit den Anweisungen
an dieser Stelle fortgefahren. Dabei können auch mehrere Case-Labels hintereinander stehen.
Die Anweisung break; dient innerhalb einer Switch-Anweisung dazu, einen Fall zu be-
enden und mit den Anweisungen hinter der Switch-Anweisung fortzufahren. Wird ein Fall
nicht durch eine Break-Anweisung abgeschlossen, wird mit den Anweisungen des nächsten
Falls fortgefahren.
Das optionale Label default: kennzeichnet alle anderen Fälle“. Dieser Default-Fall

kann an beliebiger Stelle stehen. Ist kein Default-Fall aufgeführt und passt keiner der ange-
gebenen Fälle, wird gleich hinter der Switch-Anweisung fortgefahren.

Schleifen
In C++ gibt es folgende Kontrollstrukturen für Schleifen:
 while
In der While-Schleife wird vor jedem Durchlauf eine Schleifenbedingung getestet, die jeweils
erfüllt sein muss:
while (x < 7) {
std::cout << "x ist (immer noch) kleiner als 7" << std::endl;
++x; // x wird um 1 erhöht
}
std::cout << "x ist groesser oder gleich 7" << std::endl;

 do-while
In der Do-While-Schleife wird dagegen nach jedem Durchlauf eine Schleifenbedingung ge-
testet, die zum Fortgang der Schleife jeweils erfüllt sein muss. Der Schleifenkörper wird im
Gegensatz zur While-Schleife also mindestens einmal durchlaufen.
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 47

Im Gegensatz zur Repeat-Until-Schleife von Pascal handelt es sich aber auch hier um eine
Bedingung, die für das Weiterlaufen der Schleife erfüllt sein muss, und nicht um eine Ab-
bruchbedingung:
// x wird auf jeden Fall einmal inkrementiert und ausgegeben
do {
++x;
std::cout << "x: " << x << std::endl;
} while (x < 7);

 for
In der For-Schleife werden im Schleifenkopf sämtliche Bedingungen für den Verlauf der
Schleife festgelegt:
for (initialisierung; bedingung; reinitialisierung)
Die Initialisierung wird am Anfang einmal durchgeführt. Solange dann die Bedingung erfüllt
ist, werden die Anweisungen im Schleifenkörper durchlaufen. Nach diesen Anweisungen
wird jedes Mal, bevor die Bedingung erneut bewertet wird, die Reinitialisierung durchgeführt.
Dies kann typischerweise dazu verwendet werden, eine Schleife über eine Wertfolge ite-
rieren zu lassen:
// Schleife, bei der i alle Werte von 0 bis 6 durchläuft
for (i=0; i<7; ++i) {
std::cout << "i hat den Wert: " << i << std::endl;
}
i wird am Anfang 0 zugewiesen. Die Schleife läuft, solange i einen Wert kleiner als 7 be-
sitzt. Mit dem Inkrement-Operator ++ wird i dabei jedes Mal nach dem Durchlaufen der
Anweisungen im Schleifenkörper um eins erhöht.
Da die Ausdrücke im Schleifenkopf der For-Schleife beliebig sein können, kann ein
Schleifenverlauf auch komplexer definiert werden:
/* Schleife, die für jeden zweiten Wert von 100 bis 0
* durchlaufen wird (100, 98, 96, 94, ..., 0)
*/
for (i=100; i>=0; i-=2) {
std::cout << "i hat den Wert: " << i << std::endl;
}
i startet mit dem Wert 100 und wird nach jedem Durchgang um 2 vermindert. Die Schleife
läuft, solange i größer oder gleich 0 ist.
Die einzelnen Ausdrücke im Kopf einer For-Schleife können auch wegfallen. Ist keine
Bedingung angegeben, bedeutet das, dass die Bedingung immer erfüllt ist. Das kann für End-
losschleifen verwendet werden:
// Endlosschleife
for (;;) {
std::cout << "das geht endlos so weiter" << std::endl;
}
Sandini Bib

48 Kapitel 3: Grundkonzepte von C++-Programmen

Mit Hilfe des Komma-Operators (siehe Seite 44) kann die Initialisierung oder die Reinitia-
lisierung auch aus mehreren Anweisungen bestehen. Damit kann man z.B. bei einem Feld
(Array) in einer Schleife zwei Indexe aufeinander zu laufen lassen (Felder werden zwar erst
in Abschnitt 3.7.2 eingeführt, der vorliegende Code sollte aber dennoch verständlich sein):
// Werte in einem Feld vertauschen
int feld[100]; // Feld von 100 ganzzahligen Werten

// die Indexe i und j laufen in der Schleife von aussen in die


// Mitte und vertauschen jeweils die Werte
for (int i=0, int j=99; i<j; ++i, --j) {
int tmp = feld[i];
feld[i] = feld[j];
feld[j] = tmp;
}
Am Anfang der Schleife wird i der Wert 0 (das ist der Index des ersten Elements) und j
der Wert 99 (das ist der Index des letzten Elements) zugewiesen. Solange i kleiner als j ist,
werden dann die Anweisungen im Schleifenkörper durchgeführt. Dabei wird i nach jedem
Durchgang um eins erhöht und j um eins vermindert.
In den Schleifen können auch noch zwei besondere Anweisungen stehen:
 break
dient neben dem Abbruch eines Falls in einer Switch-Anweisung auch zum sofortigen Ab-
bruch einer Schleife. Diese Anweisung bewirkt im Prinzip einen Sprung hinter die Schleife
und sollte nur in Sonderfällen eingesetzt werden.
 continue
dient zum sofortigen Neueintritt in eine Schleife. Auch dies ist im Prinzip ein Sprung, nämlich
an die Stelle, an der die Schleife neu bewertet wird (bzw. bei For-Schleifen ein Sprung zur
Reinitialisierung), und sollte ebenfalls nur in Sonderfällen verwendet werden.

Blöcke
Bei allen Kontrollstrukturen wurden in den Beispielen die Körper durch geschweifte Klammern
eingeschlossen. Durch diese geschweiften Klammern werden jeweils mehrere Anweisungen zu
einem Block von Anweisungen zusammengefasst. Dies ist bei Kontrollstrukturen meistens auch
notwendig, da der Körper jeweils nur eine Anweisung oder eben ein Block von Anweisungen
sein kann. Bei zwei einzelnen Anweisungen würde nur die erste Anweisung zur Kontrollstruktur
gehören. Die zweite Anweisung wäre bereits die erste Anweisung hinter der Kontrollstruktur:
if (x < 7)
std::cout << "x ist kleiner als 7" << std::endl;
std::cout << "diese Anweisung wird in jedem Fall ausgefuehrt"
<< std::endl;
Obwohl es eigentlich unnötig ist, empfehle ich, auch bei nur einer Anweisung im Körper von
Kontrollstrukturen geschweifte Klammern zu verwenden:
Sandini Bib

3.2 Datentypen, Operatoren, Kontrollstrukturen 49

if (x < 7) {
std::cout << "x ist kleiner als 7" << std::endl;
}
std::cout << "diese Anweisung wird in jedem Fall ausgefuehrt"
<< std::endl;
Es ist nicht nur besser lesbar, sondern schützt auch vor dem Problem, dass beim Einfügen einer
zweiten Anweisung im Schleifenkörper die Klammern leicht vergessen werden.

3.2.5 Zusammenfassung
 C++ bietet verschiedene fundamentale Datentypen für Zeichen, ganze Zahlen, Gleitkomma-
zahlen und Boolesche Werte.
 Der genaue Wertebereich der Zahlen ist systemspezifisch.
 C++ verfügt über zahlreiche Operatoren. Dazu gehören auch Operatoren zum Inkrementieren
und Dekrementieren sowie modifizierende Zuweisungen.
 Der Operator = ist die Zuweisung; der Operator == ist der Test auf Gleichheit.
 C++ verfügt über die üblichen Kontrollstrukturen (Fallunterscheidungen, Schleifen).
 Die For-Schleife erlaubt es, sehr flexibel über Werte zu iterieren.
 Mehrere Anweisungen können durch geschweifte Klammern zu einem Anweisungsblock zu-
sammengefasst werden.
Sandini Bib

50 Kapitel 3: Grundkonzepte von C++-Programmen

3.3 Funktionen und Module


Ein C++-Programm setzt sich aus einer strukturierten Folge von Anweisungen zusammen. Auf
unterster Ebene werden diese Anweisungen durch Kontrollstrukturen und Blöcke gruppiert. Die
nächsthöhere Gliederungsebene sind Funktionen. Der Begriff Funktion steht dabei für jede Art
von selbst definierter aufrufbarer Operation. Mögliche andere Bezeichnungen in anderen Kon-
texten wären Unterprogramm, Prozedur oder Subroutine.
Um Funktionen zu strukturieren, gibt es zwei Möglichkeiten, eine physikalische und eine
logische Gruppierung. Für eine physikalische Gruppierung werden die Funktionen auf unter-
schiedliche Übersetzungseinheiten oder Module aufgeteilt. Dabei handelt es sich einfach um
verschiedene Dateien, die unabhängig voneinander übersetzt (kompiliert) werden. Diese Grup-
pierung hat unter anderem den Vorteil, dass man bei einer Änderung in einer Funktion nicht den
gesamten Code neu übersetzen muss. Es wird nur die Datei neu übersetzt, in der die Funktion
implementiert ist. Diese übersetzte Datei wird dann mit den anderen übersetzten Dateien zu-
sammengebunden (gelinkt). Diese Gruppierung hat bietet auch den Vorteil, dass die Funktionen
damit eine besondere Einheit bilden und als Ganzes z.B. in mehreren Programmen verwendet
werden können. Insofern geht eine derartige physikalische Gruppierung in der Regel auch im-
mer mit einer logischen Gruppierung einher.
Neben dieser physikalisch basierten Strukturierung (die von C übernommen wurde) gibt es
in C++ aber auch mehrere Möglichkeiten der logischen Strukturierung. So gibt es zum einen
die Möglichkeit, zusammengehörigen Code unabhängig von physikalischen Grenzen durch ei-
nen Namensbereich als zusammengehörig zu kennzeichnen. Eine weitere Möglichkeit bietet das
objektorientierte Paradigma. Es ordnet Funktionen einem gemeinsamen Datentyp (einer so ge-
nannten Klasse) zu. Alle Operationen, die man für Objekte dieser Klasse aufrufen kann, gehören
logisch zu dieser Klasse. Derartige Funktionen nennt man auch Elementfunktionen oder, nach
objektorientierter Sprachregelung, Methoden. Eine derartige logische Gruppierung ist im Prin-
zip unabhängig von der physikalischen Gruppierung. Es ist aber in der Regel so, dass man alle
Funktionen einer Klasse auch physikalisch in der gleichen Datei verwaltet.
In diesem Abschnitt wird zunächst die Möglichkeit vorgestellt, Funktionen (die hier noch
keiner Klasse zugeordnet sind) physikalisch auf verschiedene Module zu verteilen. Anschlie-
ßend wird auf die Möglichkeit einer logischen Strukturierung mit Hilfe von Namensbereichen
eingegangen.

3.3.1 Headerdateien
C++ ist eine Sprache mit Typprüfung. Das bedeutet, dass man zur Übersetzungszeit herausfinden
will, ob Operationen wie Funktionsaufrufe zumindest vom Datentyp her sinnvoll sind. Verteilt
man Funktionen auf unterschiedliche Module, die getrennt übersetzt werden, stellt sich die Frage,
wie man eine derartige Typprüfung gewährleisten kann.
Damit die Übergabe von Daten zwischen Funktionen verschiedener Module funktioniert,
wird die Schnittstelle einer derartigen Funktion typischerweise in einer separaten Headerdatei
deklariert. Diese Datei wird dann sowohl von dem Modul, das die Funktion implementiert, als
auch von allen Modulen, die die Funktion aufrufen, eingebunden.
Sandini Bib

3.3 Funktionen und Module 51

Das folgende Programm zeigt dafür ein Beispiel. Es besteht aus drei Dateien, die, wie in Abbil-
dung 3.1 skizziert, folgende Rolle spielen:
 In quer.hpp wird die Funktion quersumme() deklariert. Damit ist sie in allen Modulen, die
diese Datei einbinden, bekannt und aufrufbar.
 In quer.cpp wird die in quer.hpp deklarierte Funktion quersumme() implementiert. Durch
Einbindung von quer.hpp wird sichergestellt, dass sich Deklaration und Implementierung
nicht widersprechen.
 In quertest.cpp wird die in quer.hpp deklarierte Funktion quersumme() aufgerufen.
Durch Einbindung von quer.hpp wird sichergestellt, dass die Funktion beim Aufruf bekannt
ist.

q u e r . h p p :

# i f n d e f Q U E R _ H P P
# d e f i n e Q U E R _ H P P

i n t q u e r s u m m e ( l o n g z a h l ) ;

# e n d i f

q u e r . c p p : q u e r t e s t . c p p :

# i n c l u d e " q u e r . h p p " # i n c l u d e " q u e r . h p p "

i n t q u e r s u m m e ( l o n g z a h l ) i n t m a i n ( )
{ {
. . . . . .
} x = q u e r s u m m e ( y ) ;
. . .
}

Abbildung 3.1: Deklaration, Implementierung und Aufruf von quersumme()

Der genaue Aufbau der Headerdatei sieht wie folgt aus:


// allg/quer.hpp
#ifndef QUER_HPP
#define QUER_HPP

// Funktion, die die Quersumme aus einer ganzen Zahl berechnet


int quersumme (long zahl);

#endif
Sandini Bib

52 Kapitel 3: Grundkonzepte von C++-Programmen

Die komplette Headerdatei wird durch Präprozessor-Anweisungen eingeschlossen, mit deren


Hilfe vermieden wird, dass die darin befindlichen Deklarationen bei mehrfachem Einbinden der
Datei mehrfach durchgeführt werden:
#ifndef QUER_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define QUER_HPP // QUER_HPP wird beim ersten Durchlauf definiert
...
#endif
Dabei wird typischerweise der großgeschriebene Dateiname als Symbol verwendet. Jede Hea-
derdatei sollte grundsätzlich von einer derartigen Klammer“ eingeschlossen werden. Auf die

genauen Gründe wird auf Seite 57 eingegangen.

3.3.2 Quelldatei mit der Implementierung


Die Quelldatei quer.cpp mit der Implementierung von quersumme() hat folgenden Aufbau:
// allg/quer.cpp
#include "quer.hpp"

// Funktion, die die Quersumme aus einer ganzen Zahl berechnet


int quersumme (long zahl)
{
int quer = 0;

while (zahl > 0) {


quer += zahl % 10; // Einer auf Quersumme addieren
zahl = zahl / 10; // weiter mit restlichen Ziffern
}

return quer;
}
In dieser Datei wird mit
#include "quer.hpp"
zunächst einmal die Datei mit der Deklaration eingebunden. Dadurch kann der Compiler fest-
stellen, ob es Widersprüche zwischen der Deklaration und der Implementierung der Funktion
gibt.

3.3.3 Quelldatei mit dem Aufruf


Jede Anwendung dieser Funktion bindet ebenfalls die Headerdatei mit der Deklaration ein, da
jede Funktion vor einem Aufruf bekannt sein muss:
Sandini Bib

3.3 Funktionen und Module 53

// allg/quertest.cpp
// Headerdatei mit der Deklaration von quersumme() einbinden
#include "quer.hpp"

// Headerdatei für I/O einbinden


#include <iostream>

// Vorwärtsdeklaration von gibQuersummeAusVon()


void gibQuersummeAusVon(long);

// Implementierung von main()


int main()
{
gibQuersummeAusVon(12345678);
gibQuersummeAusVon(0);
gibQuersummeAusVon(13*77);
}

// Implementierung von gibQuersummeAusVon()


void gibQuersummeAusVon (long zahl)
{
std::cout << "Die Quersumme von " << zahl
<< " ist " << quersumme(zahl) << std::endl;
}
In diesem Fall enthält das Testprogramm zwei Funktionen, die Hauptfunktion main() und die
Funktion gibQuersummeAusVon(). Innerhalb der Funktion main() wird dreimal die Funktion
gibQuersummeAusVon() aufgerufen. Da diese erst nach ihrem Aufruf implementiert wird, muss
sie vor main() deklariert werden. Ansonsten gibt es eine Fehlermeldung, dass eine Funktion auf-
gerufen wird, die nicht bekannt ist. Wie man sieht, kann man bei der Deklaration der Parameter
den Namen der Parameter weglassen. Die Namen werden aus Dokumentationsgründen häufig
aber dennoch angegeben (wie dies z.B. bei der Deklaration von quersumme() in der Headerda-
tei quer.hpp der Fall ist).
Jeder Aufruf von gibQuersummeAusVon() übergibt also die Zahl, deren Quersumme aus-
gegeben werden soll. Innerhalb der Funktion werden die Zahl selbst und deren mit Hilfe von
quersumme() berechnete Quersumme ausgegeben. Die Parameterübergabe erfolgt im Normal-
fall by-value“. Das bedeutet, dass zahl in gibQuersummeAusVon() eine Kopie des überge-

benen Arguments ist, die man in der Funktion lokal ohne Auswirkung für den Aufrufer noch
verändern könnte.
Sandini Bib

54 Kapitel 3: Grundkonzepte von C++-Programmen

3.3.4 Übersetzen und binden


Für dieses Programm ergibt sich das in Abbildung 3.2 dargestellte Übersetzungsschema: Die
Headerdatei quer.hpp, die quersumme() deklariert, wird sowohl von der Quelldatei mit der
Implementierung der Funktion als auch von der Quelldatei mit dem Anwendungsprogramm ein-
gebunden. Die beiden Quelldateien werden vom C++-Compiler zunächst in Objektdateien über-
setzt (diese haben typischerweise die Endung .o oder .obj). Von einem Linker (oder Binder“)

werden die Objektdateien dann zum ausführbaren Programm quertest (oder quertest.exe)
zusammengebunden.

C o m p ile r
q u e r . o o d e r
q u e r . c p p
q u e r . o b j
L in k e r
# i n c l u d e

q u e r . h p p q u e r t e s t o d e r
q u e r t e s t . e x e

# i n c l u d e
L in k e r

q u e r t e s t . o o d e r
q u e r t e s t . c p p
q u e r t e s t . o b j
C o m p ile r

Abbildung 3.2: Übersetzen und binden

Mit dem so erstellten ausführbaren Programm ( Executable“) kann das Programm gestartet wer-

den. Seine Ausgabe lautet:
Die Quersumme von 12345678 ist 36
Die Quersumme von 0 ist 0
Die Quersumme von 1001 ist 2

3.3.5 Dateiendungen
In C++ werden also typischerweise zwei Arten von Dateien verwendet:
 Headerdateien (auch Include-Dateien, Header-Files oder H-Files genannt), die dazu dienen,
Definitionen, die beim Kompilieren in verschiedenen Modulen gebraucht werden, zentral zu
verwalten. Sie enthalten typischerweise die Deklarationen von globalen Konstanten, Varia-
blen, Funktionen und Datentypen und werden von den Dateien, in denen diese Deklarationen
gebraucht werden, jeweils eingebunden.
 Quelldateien (auch Source-Dateien, Source-Files oder C-Files genannt), die die eigentlichen
Module (Übersetzungseinheiten) darstellen, in denen der Programmablauf definiert wird.
Darin befinden sich vor allem die Implementierungen der einzelnen Funktionen.
Sandini Bib

3.3 Funktionen und Module 55

Unglücklicherweise gibt es keine standardisierten Dateiendungen. Aus historischen Gründen ha-


ben sich verschiedene Endungen etabliert, die auch nach wie vor mehr oder weniger verbreitet
sind. Inzwischen kristallisieren sich aber übliche Endungen heraus.
 Für Quelldateien lautet die Endung inzwischen typischerweise .cpp. Manchmal wird aber
auch .cc, .C oder .cxx verwendet.
 Entsprechend lautet die Endung für Headerdateien inzwischen typischerweise .hpp. Analog
zu den Endungen der Quelldateien gibt es aber auch .hh, .H oder .hxx.
Das Durcheinander wird noch größer, da die Systemdateien in C++ früher wie in C die Endung
.h besaßen. Dies führte dazu, dass man nicht zwischen C- und C++-Headerdateien unterscheiden
konnte. Inzwischen haben Systemdateien von C++ gar keine Endung mehr.7 An diesem Beispiel
sollte man sich aber bei eigenen Dateien nicht orientieren. Zur bequemen Handhabung und Ver-
waltung von C++-Dateien sind eindeutige Endungen wie .cpp und .hpp dringend angeraten.

3.3.6 Systemdateien
Beim Linken werden nicht nur die eigenen Module zusammengebunden. Hinzu kommen auch
alle anderen Module, die Funktionen und Symbole für das Programm bereitstellen. Dazu gehören
insbesondere die Module der Standardbibliothek.
In diesem Fall wird z.B. ein Modul mit der Implementierung der Standard-I/O-Schnittstelle
von C++ dazugebunden. Derartige Module werden typischerweise aus Bibliotheken dazugebun-
den, die mehrere zusammengehörige Module verwalten können. Sie besitzen je nach System
Endungen wie .a, .so, .lib oder .dll.
Auch für die Funktionen und Symbole der Ein- und Ausgabe gilt, dass alles für deren Ver-
wendung vor der Verwendung bekannt sein muss. Deshalb wird vom Testprogramm nach dem
gleichen Muster <iostream> eingebunden. Dabei fallen zwei Besonderheiten auf:
 Als Systemdatei hat die Datei im Include-Befehl keine Endung.
 Durch die spitzen Klammern wird die Datei nicht im lokalen Verzeichnis sondern nur in den
Systemverzeichnissen gesucht.

3.3.7 Präprozessor
Zu C++ gehört ein Präprozessor, mit dem der Code vor dem eigentlichen Kompilieren noch
modifiziert werden kann. Mit seiner Hilfe können z.B. Dateien eingebunden oder bestimmte
Teile des Codes ignoriert werden.
Die Anweisungen für den Präprozessor beginnen alle mit dem Zeichen # und müssen in einer
Zeile jeweils das erste Zeichen sein, das kein Leer- oder Tabulatorzeichen ist.

7
Genau genommen muss es diese System-Headerdateien auch gar nicht als Dateien geben. Der Standard
garantiert nur, dass mit #include <iostream> alle notwendigen Deklarationen zur Ein-/Ausgabe zur
Verfügung stehen. Wie dies von einer C++-Umgebung umgesetzt wird, ist Sache der Umgebung. In der
Praxis findet man allerdings in der Regel tatsächlich entsprechende Dateien ohne Endung in einem System-
verzeichnis des Compilers.
Sandini Bib

56 Kapitel 3: Grundkonzepte von C++-Programmen

Einbinden von anderen Dateien


Wie bereits erläutert, kann mit #include der Inhalt einer anderen Datei eingebunden werden.
Dabei kann der dazugehörige Dateiname in doppelte Anführungsstrichen oder in spitze Klam-
mern eingeschlossen werden:
#include <header1.hpp>
#include "header2.hpp"
In beiden Fällen werden diese Dateien in den Systemverzeichnisse für Headerdateien gesucht.
Wo diese genau liegen, ist systemspezifisch. Auf vielen Systemen kann man den Pfad für die
Systemverzeichnisse bei Kompilieren mit Optionen wie -I beeinflussen.
Verwendet man doppelte Anführungsstriche, werden die Dateien zusätzlich zunächst im lo-
kalen Verzeichnis gesucht.

Bedingte Übersetzung
Neben der Einbindung von Dateien kann man mit dem Präprozessor noch beeinflussen, welche
Teile einer Datei wirklich kompiliert werden sollen. Da Konstanten einem Compiler auch als
Parameter übergeben werden können, kann man damit zum Beispiel das Kompilieren bestimmter
Zeilen ein- oder ausschalten:
void f ()
{
...
#ifdef DEBUG
std::cout << "x hat den Wert: " << x << std::endl;
#endif
...
}
Die Ausgabeanweisung wird nur dann kompiliert, wenn die Konstante DEBUG definiert ist. Die
Konstante kann nicht nur im Quellcode, sondern auch beim Aufruf des Compilers definiert wer-
den (typischerweise als Option -Dkonstante).
Da für jedes System bestimmte Konstanten definiert sind, kann man auf diese Weise im Not-
fall auch Systemunterschiede berücksichtigen. Zum Beispiel:
#if defined __GNUC__
// Sonderbehandlung für GNU-Compiler
# if __GNUC__ == 2 && __GNUC_MINOR__ <= 95
// bis Version 2.95
...
# else
// danach
...
# endif
#elif defined _MSC_VER
// Sonderbehandlung für Microsoft Visual-C++-Compiler
...
#endif
Sandini Bib

3.3 Funktionen und Module 57

Bedingte Übersetzung von Headerdateien


Ein anderes Beispiel für die Verwendung der bedingten Übersetzung ist die typische Klammer
um Code in Headerdateien:
#ifndef QUER_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define QUER_HPP // QUER_HPP wird beim ersten Durchlauf definiert
...
#endif
Bindet eine Quelldatei diese Headerdatei ein, werden die dortigen Anweisungen übernommen.
Aus
#include "quer.hpp"
wird also:
#ifndef QUER_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define QUER_HPP // QUER_HPP wird beim ersten Durchlauf definiert
...
#endif
Das hat zur Folge, dass alle Zeilen aus quer.hpp nur dann berücksichtigt werden, wenn die
Konstante QUER_HPP noch nicht definiert ist (#ifndef steht für if not defined“). Dies ist beim

ersten Einbinden der Fall. Wird die Datei aber ein zweites Mal eingebunden,
#include "quer.hpp"
#include "quer.hpp"
wird daraus:
#ifndef QUER_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define QUER_HPP // QUER_HPP wird beim ersten Durchlauf definiert
...
#endif
#ifndef QUER_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define QUER_HPP // QUER_HPP wird beim ersten Durchlauf definiert
...
#endif
In diesem Fall wird der Inhalt von quer.hpp nur bei der ersten Expandierung berücksichtigt.
Beim zweiten Mal werden alle Zeilen aus quer.hpp ignoriert. Auf diese Weise werden Fehler-
meldungen über doppelte Definitionen vermieden.
Sicherlich wird man nicht direkt zweimal hintereinander die gleiche Headerdatei einbinden.
Es kann aber immer passieren, dass zwei eingebundene Headerdateien jeweils die gleiche Hea-
derdatei einbinden, was zum gleichen Szenario führt. Da es also immer passieren kann, dass
Headerdateien über unterschiedliche Wege von einer Quelldatei mehrfach eingebunden werden,
sollten Definitionen in Headerdateien immer durch derartige Anweisungen eingeschlossen wer-
den.
Sandini Bib

58 Kapitel 3: Grundkonzepte von C++-Programmen

Definitionen
Man kann mit einer Define-Anweisung auch selbst Konstanten im Code definieren:
#define DEBUG
Dabei kann man auch einen Wert angeben:
#define VERSION 13
Diese Konstanten kann man dann im Code entsprechend auswerten:
void foo()
{
#ifdef DEBUG
std::clog << "foo() aufgerufen" << std::endl;
#endif
...
#if VERSION < 10
...
#else
...
#endif
}
In älteren C-Programmen wurden Präprozessor auch zur Definition von Konstanten und Makros
verwendet:
#define ANZ 100 // SCHLECHT
...
int array[ANZ]; // Feld mit ANZ Elementen anlegen
for (int i=0; i<ANZ; ++i) { // Feld durchlaufen
...
}
Eine derartige Zweckentfremdung des Präprozessors für den eigentlichen Datenfluss kann und
sollte man in C++ aber unbedingt vermeiden. Es handelt sich nämlich um dummen“ Textersatz,

der weder den Gesetzen der Typprüfung unterliegt noch Gültigkeitsbereiche berücksichtigt. In
C++ gibt es dafür Konstanten:
const int anz = 100; // OK
...
int array[anz]; // Feld mit anz Elementen anlegen
for (int i=0; i<anz; ++i) { // Feld durchlaufen
...
}
Man kann bei derartigen Definitionen sogar Parameter übergeben. Diese so genannten Präpro-
zessor-Makros sollte man aber ebenfalls vermeiden. Dazu gibt es in C++ ebenfalls ausreichende
Sprachmittel, wie Inline-Funktionen (siehe Abschnitt 4.3.3) und Funktionstemplates (siehe Ab-
schnitt 7.2).
Sandini Bib

3.3 Funktionen und Module 59

3.3.8 Namensbereiche
Wie in Abschnitt 3.1.5 bereits erläutert gibt es in C++ das Konzept der Namensbereiche (eng-
lisch: namespaces) zur logischen Gruppierung von Datentypen und Funktionen. Alle Symbole
können einem Namensbereich zugeordnet werden. Wird dieses Symbol dann außerhalb des Na-
mensbereichs verwendet, muss es mit dem Namensbereich qualifiziert werden.
Auf diese Weise werden auf der einen Seite Namenskonflikte vermieden und auf der anderen
Seite wird deutlich gemacht, welche Symbole logisch zusammengehören (ein logisches Paket
oder eine Komponente bilden).
Das folgende Beispiel zeigt die Verwendung von Namensbereichen:
namespace A {
typedef ... String; // definiert Datentyp A::String
void foo (String); // definiert A::foo(A::String)
}

namespace B {
typedef ... String; // definiert Datentyp B::String
void foo (String); // definiert B::foo(B::String)
}
Das Schlüsselwort namespace ordnet die darin definierten Symbole dem angegebenen Gültig-
keitsbereich zu. Damit können diese Symbole nicht mehr mit gleichnamigen Symbolen aus an-
deren Namensbereichen oder globalen Symbolen kollidieren.
Zur Verwendung der Symbole außerhalb des Namensbereiches muss der Namensbereich
dann einfach qualifiziert werden:
A::String s1; // String aus Namensbereich A anlegen
B::String s2; // String aus Namensbereich B anlegen

A::foo(s1); // foo() aus Namensbereich A aufrufen


B::foo(s2); // foo() aus Namensbereich B aufrufen
Auf die Qualifizierung der Funktionsaufrufe kann dabei verzichtet werden. Wird einer Funktion
ein Argument eines Namensbereiches übergeben, wird die Funktion automatisch auch in diesem
Namensbereich gesucht:
A::String s1; // String aus Namensbereich A anlegen
B::String s2; // String aus Namensbereich B anlegen

foo(s1); // OK, findet A::foo(), da s1 zu A gehört


foo(s2); // OK, findet B::foo(), da s1 zu B gehört
Auf diese und weitere Möglichkeiten, auf die Qualifizierung von Namensbereiche zu verzichten,
wird in Abschnitt 4.3.5 noch ausführlich eingegangen.
Sandini Bib

60 Kapitel 3: Grundkonzepte von C++-Programmen

3.3.9 Das Schlüsselwort static


C++ hat von C das Schlüsselwort static übernommen. Dieses Schlüsselwort kann dort zwei
verschiedenen Zwecken dienen:
1. Es kann die Lebensdauer von lokalen Variablen beeinflussen, indem deren Lebendauer auf
die gesamte Laufzeit eines Programms ausgedehnt wird.
2. Es kann den Sichtbarkeit von Symbolen auf ein Modul beschränken.

Statische lokale Variablen


Normalerweise werden alle lokalen Variablen und Objekte einer Funktion oder eines Blocks
angelegt, wenn die entsprechende Stelle im Programmablauf erreicht wird. Wird der Block bzw.
die Funktion verlassen, werden die darin deklarierten lokalen Variablen und Objekte automatisch
wieder zerstört:
void foo ()
{
int x; // wird mit undefiniertem Inhalt angelegt,
// wenn diese Stelle erreicht wird
...
std::string s; // wird mit einem Leerstring als Inhalt angelegt,
// wenn diese Stelle erreicht wird
...
} // zerstört x und s wieder
Mit static kann festgelegt werden, dass eine lokale Variable beim ersten Durchlauf des Kon-
trollflusses einmal einmal initialisiert und erst mit dem Programmende zerstört wird. In dem
Fall behält die Variable auch nach Verlassen einer Funktion ihren Wert, und dieser Wert kann
beim nächsten Aufruf der Funktion wieder ausgewertet werden. Es handelt sich sozusagen um
eine globale Variable, auf die nur lokal innerhalb einer Funktion zugegriffen werden kann. Das
folgende Beispiel verdeutlicht den Sachverhalt:
void foo ()
{
static int anzahlAufrufe = 0; // wird beim ersten Aufruf initialisiert und
// zum Programmende zerstört

++anzahlAufrufe;
std::cout << "Die Funktion wurde zum " << anzahlAufrufe
<< ". Mal aufgerufen" << std::endl;
...
}
Die Variable anzahlAufrufe wird beim ersten Aufruf von foo() mit dem zur Initialisierung
übergebenen Wert initialisiert und bleibt für die gesamte Dauer des Programms erhalten. Nach
dem Programmstart spielt der Initialwert keine Rolle mehr. Mit jedem Aufruf der Funktion wird
Sandini Bib

3.3 Funktionen und Module 61

zunächst der Wert der Variablen um eins erhöht. In der Variablen wird also die Anzahl der Auf-
rufe dieser Funktion gezählt. Diese Anzahl wird dann jeweils auch ausgegeben.
Derartige statische Variablen oder Objekte sind immer dann sinnvoll, wenn man einen Zu-
stand zwischen zwei Funktionsaufrufen erhalten will, ohne dass Daten explizit durchgereicht
werden. Es ist allerdings besser, derartige Variablen zu vermeiden. So gibt es z.B. erhebliche
Probleme, wenn in Multithreading-Programmen parallel auf diese Variablen zugegriffen werden
kann.

Modulspezifische Variablen und Funktionen


Die andere Anwendung von static dient dazu, die Sichtbarkeit einer Variablen oder einer
Funktion auf ein Modul zu beschränken. Man kann damit Hilfsvariablen oder Hilfsfunktionen
programmieren, deren Namen nicht mit Symbolen anderer Module kollidieren können.
Im folgenden Beispiel werden eine Variable zustand und eine Funktion hilfsfunktion()
deklariert, auf die nur innerhalb des Moduls zugegriffen werden kann:
static std::string zustand; // nur in diesem Modul bekannt

static void hilfsfunktion (); // nur in diesem Modul bekannt

void foo () // global bekannt


{
...
++zustand;
...
}

static void hilfsfunktion ()


{
...
if (zustand == 17) {
hilfsfunktion();
}
...
}
Der Versuch, hilfsfunktion() außerhalb des Moduls aufzurufen oder auf zustand außerhalb
des Moduls zuzugreifen, führt zu einem Fehler. Von außen kann lediglich foo() aufgerufen
werden.
In C++ kann und sollte für diesen Zweck auch ein so genannter anonymer Namensbereich
verwendet werden. Dabei handelt es sich um einen Namensbereich, für den kein Name vergeben
wird. Das obige Beispiel sieht damit wie folgt aus:
namespace { // alles hier ist nur in diesem Modul bekannt
std::string zustand;
void hilfsfunktion ();
}
Sandini Bib

62 Kapitel 3: Grundkonzepte von C++-Programmen

void foo () // global bekannt


{
...
++zustand;
...
}

namespace { // alles hier ist nur in diesem Modul bekannt


void hilfsfunktion ()
{
if (zustand == 17) {
hilfsfunktion();
}
...
}
}
Die Verwendung von anonymen Namensbereichen hat den Vorteil, dass static nur noch dazu
dient, die Lebensdauer einer Variablen zu beeinflussen. Die Semantik zur Einschränkung der
Sichtbarkeit bleibt anderen Sprachmitteln überlassen. Insofern ist die Verwendung anonymer
Namensbereiche der Verwendung von static vorzuziehen. static sollte in C++ nur noch zur
Beeinflussung der Lebensdauer von Variablen verwendet werden.

3.3.10 Zusammenfassung
 In C++ verwendet man zwei Arten von Dateien: Headerdateien für Deklarationen und Quell-
dateien für die eigentlichen Implementierungen. Die Headerdateien werden dabei von den
Quelldateien zum Kompilieren jeweils eingebunden.
 Headerdateien haben typischerweise die Endung .hpp.
 Quelldateien haben typischerweise die Endung .cpp.
 Mit dem Präprozessor können vor dem eigentlichen Kompilieren Headerdateien eingebunden
werden.
 Mit dem Präprozessor kann Code geschrieben werden, der nur unter bestimmten Bedingun-
gen kompiliert wird.
 Headerdateien sollten grundsätzlich von Präprozessor-Anweisungen umschlossen sein, die
Fehler durch mehrfache Einbindung vermeiden.
 Für Makros und zur Definition von Konstanten gibt es in C++ bessere Möglichkeiten als den
Präprozessor.
 Mit Namensbereichen können Symbole logisch gruppiert werden.
 Mit static können lokale Variablen definiert werden, deren Lebensdauer über die gesamte
Programmdauer reicht.
 Mit anonymen Namensbereichen kann die Sichtbarkeit von Variablen und Funktionen auf
Module eingeschränkt werden.
Sandini Bib

3.4 Strings 63

3.4 Strings
In C++ gibt es keinen eingebauten Datentyp zur Verwendung von Strings. Im Gegensatz zu C
gibt es aber einen Datentyp string, der in der Standardbibliothek angeboten wird. Genau ge-
nommen handelt es sich um eine so genannte Klasse, die die Eigenschaften und Fähigkeiten von
Strings definiert. Diese Klasse ist so implementiert, dass man mit Strings wie mit fundamenta-
len Datentypen operieren kann. So kann man Strings mit = zuweisen, mit == vergleichen und
mit + aneinander hängen. Hinzu kommen weitere Operationen, die man, wie bei Klassen üblich,
jeweils für Objekte (also Strings) aufrufen kann.
Die Tatsache, dass eine String-Klasse vorhanden ist, die diesen Datentyp wie einen funda-
mentalen Datentyp zur Verfügung stellt, bietet einen großen Vorteil. Die moderne Datenverarbei-
tung ist heutzutage zu einem Großteil eine String-Verarbeitung. Namen, Daten und Texte werden
als Zeichenfolgen erfasst, durchgereicht und ausgewertet. In Sprachen, bei denen es keinen ein-
fachen String-Datentyp gibt (z.B. in C oder Fortran) sind Strings oft die Ursache für allerlei
Probleme, die in C++ nicht mehr auftreten.

3.4.1 Ein erstes einfaches Beispielprogramm mit Strings


Das folgende Beispiel zeigt, welche elementaren Operationen für Strings durchgeführt werden
können:
// allg/string1.cpp
#include <iostream> // C++-Headerdatei für I/O
#include <string> // C++-Headerdatei für Strings

int main ()
{
// zwei Strings anlegen
std::string vorname = "bjarne";
std::string nachname = "stroustrup";
std::string name;

// Strings manipulieren
vorname[0] = 'B';
nachname[0] = 'S';

// Strings verketten
name = vorname + " " + nachname;

// Strings vergleichen
if (name != "") {
// Strings ausgeben
std::cout << name
Sandini Bib

64 Kapitel 3: Grundkonzepte von C++-Programmen

<< " ist der Urvater von C++" << std::endl;


}

// Anzahl der Zeichen in einem String ermitteln


int anz = name.length();
std::cout << "\"" << name << "\" hat " << anz
<< " Zeichen" << std::endl;
}
Zunächst wird neben der Standard-Headerdatei für Ein- und Ausgaben auch die Standard-Header-
datei für den Datentyp string eingebunden:
#include <iostream> // C++-Headerdatei für Ein-/Ausgaben
#include <string> // C++-Headerdatei für Strings
Anschließend werden drei Strings angelegt:
std::string vorname = "bjarne";
std::string nachname = "stroustrup";
std::string name;
Die ersten beiden Strings werden mit "bjarne" bzw. "stroustrup" initialisiert. Dem dritten
String wird kein Wert zur Initialisierung übergeben. Damit erhält er als Startwert den Leerstring.
Was hier passiert, kann man unterschiedlich bezeichnen:
 Aus Sicht der klassischen Programmierung wird hier eine Variable vom Typ std::string
angelegt.
 Aus Sicht der objektorientierten Programmierung wird hier ein Objekt bzw. eine Instanz der
Klasse std::string angelegt.
Im objektorientierten Umfeld nennt man Datentypen also Klassen. Legt man konkrete Variablen
einer Klasse an, bezeichnet man diese Variablen als Objekte oder auch Instanzen. Der Effekt ist
aber so oder so der gleiche: Es gibt etwas, das unter einem bestimmten Namen eine Zeichenfolge
repräsentiert.
In der folgenden Anweisung werden die Strings manipuliert:
vorname[0] = 'B';
nachname[0] = 'S';
Das erste Zeichen in den beiden initialisierten Strings wird jeweils korrigiert. Wie man sieht,
kann man auf das i-te Zeichen eines Strings jeweils mit dem Indexoperator zugreifen. Wie immer
in C und C++ hat das erste Zeichen dabei den Index 0.
Anschließend werden die beiden Namensteile mit einem Leerzeichen dazwischen hinterein-
ander gehängt und dem String name zugewiesen:
name = vorname + " " + nachname;
Man kann also den Operator + verwenden, um zwei Strings zu verketten.
Sandini Bib

3.4 Strings 65

In der folgenden If-Abfrage wird geprüft, ob der String name leer ist:
if (name != "") {
...
}
Wie man sieht, kann man auch für Strings einfach die für Vergleiche üblichen Operatoren != und
== verwenden.
Ausgeben kann man Strings wie alle anderen Datentypen auch:
std::string name;
...
std::cout << name
<< " ist der Urvater von C++" << std::endl;
Schließlich wird noch die Anzahl der Zeichen in dem String name abgefragt:
name.length();
Diese Zeile mag etwas ungewöhnlich aussehen. Ohne Kenntnisse der objektorientierten Pro-
grammierung würde man vielleicht einen Aufruf wie getLength(name) erwarten. Hier sehen
wir zum ersten Mal die objektorientierte Syntax für Operationen, die in Klassen für Objekte de-
finiert werden. Man wendet sich an das Objekt name und ruft für dieses Objekt die Operation
length() auf. Eine derartige Funktion nennt man in C++ Elementfunktion (englisch: member
function).
In der objektorientierten Terminologie handelt es sich um den Aufruf einer so genannten
Methode. Man sendet die Nachricht length ohne Argumente an das Objekt name. Dieses Objekt
hat dann eine Methode, auf diese Nachricht zu reagieren.
Die Auswirkungen dieses Aufrufs hängen von der Implementierung dieser Operation in der
Klasse string ab. In diesem Fall wird auf die Nachricht so reagiert, dass das Objekt die Anzahl
seiner Zeichen zurückliefert.
Insgesamt liefert das Programm also folgende Ausgabe:
Bjarne Stroustrup ist der Urvater von C++
"Bjarne Stroustrup" hat 17 Zeichen

Strings und String-Literale


Wie man sieht, können Strings wie fundamentale Datentypen verwendet werden:
 Mit == und != können Strings verglichen werden.
 Mit + können Summenstrings gebildet werden.
 Mit = können Strings einander zugewiesen werden.
Dies mag offensichtlich sein. Für C-Programmierer ist es das aber nicht, denn in C kann man
auf diese Weise leider nicht mit Strings programmieren (siehe auch Seite 116). Und auch in Java
darf man einen String nicht einfach mit == mit einem Leerstring vergleichen.
Man beachte, dass dabei sowohl Objekte vom Typ string als auch String-Literale wie
"bjarne" oder "" verwendet wurden. Dabei muss allerdings beachtet werden, dass String-
Sandini Bib

66 Kapitel 3: Grundkonzepte von C++-Programmen

Literale aus historischen Gründen nicht den Datentyp string besitzen.8 Aus diesem Grund kann
man nicht einfach zwei String-Literale verketten:
"hallo" + " " + "welt" // FEHLER
In diesem Fall muss man mindestens einen der ersten beiden Operanden explizit in einen String
umwandeln:
string("hallo") + " " + "welt" // OK

Wert-Semantik
Noch ein Hinweis, der sich insbesondere an Programmierer richtet, die bisher mit Sprachen wie
Java oder Smalltalk gearbeitet haben: C++ hat bei allen Datentypen eine Wert-Semantik. Das be-
deutet, dass bei der Deklaration einer Variablen auch gleich Speicherplatz für die dazugehörigen
Daten angelegt wird. Dies ist ein deutlicher Unterschied zur Referenz-Semantik von Sprachen
wie Java und Smalltalk. Dort legt eine Deklaration einer Variablen nur einen Verweis an, der
zunächst auf nichts (NIL oder null) verweist. Erst mit einem Aufruf von new wird eine echte
Instanz mit Speicherplatz für die dazugehörigen Daten angelegt. In C++ gibt es zwar auch ein
new. Dies ist aber nur notwendig, um Objekte anzulegen, auf die unabhängig von Blockgrenzen
zugegriffen werden muss. Darauf wird in Abschnitt 3.8 eingegangen.

3.4.2 Ein weiteres Beispielprogramm mit Strings


Das folgende Programm demonstriert weitere Fähigkeiten und die Arbeitsweise mit den String-
Typen. Es extrahiert aus den jeweils eingegebenen Zeilen HTML-Links und gibt diese wieder
aus.
Das Programm hat folgenden Aufbau:
// allg/html.cpp
#include <iostream> // C++-Headerdatei für Ein-/Ausgaben
#include <string> // C++-Headerdatei für Strings

int main ()
{
const std::string anfang("http:"); // Beginn eines Links
const std::string trenner(" \"\t\n<>"); // Zeichen, die den Link beenden
std::string zeile; // aktuelle Zeile
std::string link; // aktueller HTML-Link
std::string::size_type anfIdx, endIdx; // Indizes

// für jede erfolgreich gelesene Zeile


while (getline(std::cin,zeile)) {

8
Der tatsächliche Datentyp von String-Literalen ist const char*. Darauf, was dieser Datentyp bedeutet,
und auf die genauen Konsequenzen dieser Tatsache wird in Abschnitt 3.7.3 auf Seite 115 noch eingegangen.
Sandini Bib

3.4 Strings 67

// suche erstes Vorkommen von "http:"


anfIdx = zeile.find(anfang);

// solange "http:" in der Zeile gefunden wurde,


while (anfIdx != std::string::npos) {
// Ende des Links finden
endIdx = zeile.find_first_of(trenner,anfIdx);

// Link extrahieren
if (endIdx != std::string::npos) {
// Ausschnitt von gefundenem Anfang bis gefundenem Ende
link = zeile.substr(anfIdx,endIdx-anfIdx);
}
else {
// Kein Ende gefunden: Rest der Zeile
link = zeile.substr(anfIdx);
}

// Link ausgeben
// - "http:" ohne weitere Zeichen ignorieren
if (link != "http:") {
link = string("Link: ") + link;
std::cout << link << std::endl;
}

// weiteren Link in der Zeile suchen


if (endIdx != std::string::npos) {
// suche weiteres Vorkommen von "http:" ab gefundenem Ende
anfIdx = zeile.find(anfang,endIdx);
}
else {
// Ende war Zeilenende: kein neuer Anfang in der Zeile auffindbar
anfIdx = std::string::npos;
}
}
}
}
Bei einer Eingabe der Form
In diesem Text sind verschiedene Links untergebracht.
Zu diesem Buch gehort der Link "http://www.josuttis.de/cppbuch".
Meine Home-Page findet man unter
http://www.josuttis.de und http://www.josuttis.com
Sandini Bib

68 Kapitel 3: Grundkonzepte von C++-Programmen

liefert es folgende Ausgabe:


Link: http://www.josuttis.de/cppbuch
Link: http://www.josuttis.de
Link: http://www.josuttis.com

Datentypen
In main() werden wiederum einige Variablen deklariert bzw. Objekte instantiiert:
 Ein nicht änderbarer String anfang als Suchkriterium für den Anfang eines HTML-Links:
const std::string anfang("http:"); // Beginn eines Links
Es werden also alle Wörter gesucht, die mit http: beginnen.
 Ein String, in dem alle Zeichen angegeben werden, die einen HTML-Link beenden:
const std::string trenner(" \"\t\n<>"); // Zeichen, die den Link beenden
In diesem Fall wird ein Link durch ein Leerzeichen, das Zeichen ", einen Tabulator, ein
Newline (eigentlich unnötig, da wir zeilenweise lesen) sowie die Zeichen < und > beendet
(siehe die Tabelle der Sonderzeichen auf Seite 38).
 Zwei Variablen für die aktuelle Zeile bzw. den aktuellen Link:
std::string zeile; // aktuelle Zeile
std::string link; // aktueller HTML-Link

 Zwei Variablen für den Index des Anfangs und den Index des Endes eines Links:
std::string::size_type anfIdx, endIdx; // Indizes

Als Datentypen werden für Strings der Datentyp std::string (string in der Standardbiblio-
thek) und std::string::size_type verwendet. size_type ist ein Hilfstyp von Strings für
deren Index. Es ist ein spezieller Datentyp, da es einen speziellen Wert kein Index“ von diesem

Typ gibt: std::string::npos ( no position“).

Zeilenweise einlesen
Das eigentliche Hauptgerüst des Programms bildet eine Schleife, die zeilenweise aus der Stan-
dardeingabe einliest:
std::string zeile;

// für jede erfolgreich gelesene Zeile


while (getline(std::cin,zeile)) {
...
}
Die Zeilen werden von std::cin gelesen und jeweils in zeile eingetragen. Das Zeichen für
das Zeilenende, '\n', wird dabei nicht in zeile eingetragen.
Sandini Bib

3.4 Strings 69

while testet jeweils den Rückgabewert von getline(). Dabei handelt es sich um den überge-
benen Standard-Eingabekanal. Diese Kanäle haben die Eigenschaft, dass man sie zur Zustand-
sabfrage als Bedingungen verwenden kann. Dazu werden bereits einige spezielle Sprachmittel
verwendet, die erst später vorgestellt werden. Uns reicht hier erst einmal die Tatsache, dass man
das erfolgreiche Einlesen einer Zeile auf diese Weise gleich testen kann.

String-Operationen
Innerhalb der Schleife werden verschiedene String-Operationen aufgerufen. Sie haben alle wie-
der die Syntax einer Elementfunktion, also zeile.operation(). Für die jeweils aktuelle Zeile
zeile wird also jeweils eine Operation aufgerufen, die diese Zeile in irgendeiner Form bearbei-
tet.
Zunächst wird mit find() das erste Vorkommen des Beginns eines Links gesucht:
const std::string anfang("http:"); // Beginn eines Links
std::string zeile; // aktuelle Zeile
std::string::size_type anfIdx; // Index
...
// suche erstes Vorkommen von "http:"
anfIdx = zeile.find(anfang);
Diese Operation liefert das erste Vorkommen des übergebenen Strings in dem String, für den
diese Operation aufgerufen wird. In diesem Fall wird also das erste Vorkommen von http:
in zeile gesucht. Zurückgeliefert wird der Index des ersten Zeichens der gefundenen Stelle.
Ist der String nicht vorhanden, wird die schon angesprochene Konstante std::string::npos
zurückgeliefert. Dies wird in der nächsten Zeile geprüft:
// solange "http:" in der Zeile gefunden wurde,
while (anfIdx != std::string::npos) {
...
}
Solange die Suche nach einem Beginn eines HTML-Links in der aktuellen Zeile erfolgreich war,
läuft diese Schleife. Innerhalb der Schleife wird dazu jeweils das nächste Vorkommen von http:
gesucht.
Haben wir den Anfang gefunden, wird innerhalb der inneren Schleife das Ende des HTML-
Links gesucht. Die Operation find_first_of() ermöglicht es, nach mehreren Zeichen gleich-
zeitig zu suchen. Sobald eines der als Parameter übergebenen Zeichen gefunden wird, wird des-
sen Index zurückgeliefert. Auch hier wird std::string::npos zurückgeliefert, wenn keines
der Zeichen mehr vorkommt.
In diesem Fall wird in zeile nach einem der oben definierten Trennzeichen gesucht:
const std::string trenner(" \"\t\n<>"); // Zeichen, die den Link beenden
std::string zeile; // aktuelle Zeile
std::string::size_type anfIdx, endIdx; // Indizes
...
endIdx = zeile.find_first_of(trenner,anfIdx);
Sandini Bib

70 Kapitel 3: Grundkonzepte von C++-Programmen

Der optionale zweite Parameter legt fest, ab welchem Zeichen die Trenner gesucht werden. Damit
die Zeichen vor dem Beginn dieses HTML-Links keine Rolle spielen, wird erst ab dem Beginn
des HTML-Links gesucht.
In der nächsten Anweisung wird dann der eigentliche HMTL-Link aus der Zeile extrahiert.
Je nachdem, ob ein Trenner gefunden wurde, werden entweder alle Zeichen vom ersten bis zum
letzten Index oder bis zum Zeilenende extrahiert:
// Link extrahieren
if (endIdx != std::string::npos) {
// Ausschnitt von gefundenem Anfang bis gefundenem Ende
link = zeile.substr(anfIdx,endIdx-anfIdx);
}
else {
// Kein Ende gefunden: Rest der Zeile
link = zeile.substr(anfIdx);
}
Mit einem Parameter aufgerufen, liefert substr() den Teilstring aller Zeichen vom übergebenen
Index bis zum Ende des Strings. Ein optionaler zweiter Parameter dient zur Angabe der maxima-
len Anzahl zu extrahierender Zeichen. In diesem Fall ist das die Differenz zwischen Ende- und
Anfangsindex.
Die nächste Anweisung gibt den Link aus. Dabei wird einschränkend geprüft, ob hinter
http: überhaupt noch Zeichen folgen.
// Link ausgeben
// - "http:" ohne weitere Zeichen ignorieren
if (link != "http:") {
link = string("Link: ") + link;
std::cout << link << std::endl;
}
Hier werden wieder die schon im ersten Beispiel vorgestellten fundamentalen Operatoren für
Strings, !=, + und =, verwendet.
Es bleibt schließlich noch die Suche nach einem eventuell vorhandenen weiteren HTML-
Link in der gleichen Zeile, was natürlich nur der Fall sein kann, wenn das Ende des vorherigen
Links nicht das Zeilenende war:
// weiteren Link in der Zeile suchen
if (endIdx != std::string::npos) {
// suche weiteres Vorkommen von "http:" ab gefundenem Ende
anfIdx = zeile.find(anfang,endIdx);
}
else {
// Ende war Zeilenende: kein neuer Anfang in der Zeile auffindbar
anfIdx = std::string::npos;
}
Sandini Bib

3.4 Strings 71

Sofern anfIdx danach nicht den Wert std::string::npos hat, läuft die innere Schleife zur
Bearbeitung der aktuellen Zeile weiter.

3.4.3 String-Operationen im Überblick


Nach diesen einführenden Beispielen bekommt man sicherlich schon einen ersten Eindruck von
den Fähigkeiten des Standard-String-Typs. Tabelle 3.11 listet die wichtigsten Operationen noch
einmal auf.

Operation Effekt
=, assign() Neuen Wert zuweisen
swap() Werte zwischen zwei Strings vertauschen
+=, append(), push_back() Zeichen anhängen
insert() Zeichen einfügen
erase() Zeichen löschen
clear() Alle Zeichen löschen (String leeren)
resize() Anzahl der Zeichen verändern
(Zeichen am Ende löschen oder anfügen)
replace() Zeichen ersetzen
+ Summenstring bilden
==, !=, <, <=, >, >=, compare() Strings vergleichen
length(), size() Anzahl der Zeichen in einem String liefern
empty() Testen, ob ein String leer ist
[ ], at() Auf ein einzelnes Zeichen zugreifen
>>, getline() String einlesen
<< String ausgeben
find-Funktionen Teilstring oder Zeichen suchen
substr() Teilstring liefern
c_str() String als C-String verwenden
copy() Zeichen in einen Zeichenpuffer kopieren

Tabelle 3.11: Die wichtigsten String-Operationen

Für eine genaue Beschreibung der Operationen (Parameter, Rückgabewert, Semantik) sei auf
entsprechende Referenzhandbücher zur C++-Klassenbibliothek verwiesen.
In Abschnitt 3.6 wird allerdings noch auf einen wichtigen allgemeinen Aspekt der String-
Klasse eingegangen, das Verhalten im Fehlerfall. Das Verhalten im Fehlerfall spielt insbesonde-
re beim Zugriff auf ein Zeichen im String eine Rolle. Beim Indexoperator führt die Verwendung
eines fehlerhaften Index zu einem undefinierten Verhalten. Insofern muss man im Programm
sicherstellen, dass der Index, mit dem auf ein Zeichen im String zugegriffen wird, auch gültig
ist. Verwendet man stattdessen die Elementfunktion at(), wird bei einem ungültigen Index eine
so genannte Ausnahme ausgelöst, die im Programm automatisch zu einer geordneten Fehlerbe-
handlung führt. Mehr zur Fehlerbehandlung beim Zugriff auf ein Zeichen findet man vor allem
in Abschnitt 3.6.4.
Sandini Bib

72 Kapitel 3: Grundkonzepte von C++-Programmen

3.4.4 Strings und C-Strings


strings kapseln die Tatsache, dass die Zeichen intern mit Feldern (Arrays) und Zeigern ver-
waltet werden. In C muss stattdessen der Datentyp char* mit all seinen Nachteilen verwendet
werden. Um diesen Datentyp kommt man auch in C++ nicht herum, da String-Literale einen
entsprechenden Typ besitzen (genau genommen besitzen String-Literale den Datentyp const
char* ). Zur Abgrenzung bezeichne ich im weiteren Verlauf des Buchs Strings vom Datentyp
char* oder const char* auch als C-Strings. In Abschnitt 3.7.3 wird näher auf C-Strings ein-
gegangen.
Auf zwei Dinge soll hier aber schon einmal hingewiesen werden:
1. Die Funktionen zum Umwandeln oder Kopieren in einen C-String sind für den Fall konzi-
piert, dass man noch die alte String-Schnittstelle von C benötigt. Dies ist z.B. dann der Fall,
wenn Strings an Funktionen aus einer C-Bibliothek übergeben werden müssen. Ein entspre-
chender Aufruf sieht wie folgt aus:
void cfunktion(const char*); // Vorwärtsdeklaration
...
std::string s;
...
cfunktion(s.c_str()); // String als C-String übergeben
Falls fälschlicherweise der Parameter in der C-Funktion ohne const deklariert wurde, ob-
wohl der String in der Funktion gar nicht verändert wird, muss man Folgendes schreiben:

void cfunktion(char*); // Vorwärtsdeklaration


...
std::string s;
...
cfunktion(const_cast<char*>(s.c_str())); // String als C-String übergeben
const_cast<typ>(wert) entfernt die Konstantheit. Dieser Operator ist mit Vorsicht zu ge-
nießen, da er Zugriff auf Objekte zulässt, die vom Typ her nicht verändert werden sollen. Er
sollte deshalb nur in Ausnahmefällen wie diesen Verwendet werden.
2. Man beachte, dass strings beliebige Zeichen, also auch Sonderzeichen, enthalten können.
Im Gegensatz zu C-Strings ist das Zeichen '\0' in strings kein Endekennzeichen. Die
Umwandlung in einen C-String mit c_str() hängt allerdings ein Endekennzeichen an.

3.4.5 Zusammenfassung
 Für Strings bietet die C++-Standardbibliothek den Datentyp std::string.
 Damit können Strings wie fundamentale Datentypen verwendet werden.
 Etliche Suchfunktionen erlauben es, Zeichen oder Teilstrings in Strings zu suchen.
 Für String-Indizes gibt es den Datentyp std::string::size_type.
Sandini Bib

3.4 Strings 73

 Der Wert std::string::npos steht für kein Index“.



 Zur Umwandlung in C-Strings dient c_str().
 Strings können beliebige Zeichen (auch binäre Zeichen) enthalten.
 In objektorientierter Terminologie bezeichnet man Datentypen als Klassen und Variablen als
Objekte oder Instanzen.
 Alle Datentypen von C++ haben eine Wert-Semantik.
In den Abschnitten 6.1 und 6.2.1 werden am Beispiel einer eigenen Implementierung der Stan-
dard-String-Klasse weitere Aspekte und interne Details der Klasse string vorgestellt.
Sandini Bib

74 Kapitel 3: Grundkonzepte von C++-Programmen

3.5 Verarbeitung von Mengen


In der Datenverarbeitung werden heutzutage nicht nur einzelne Daten, Variablen oder Objekte,
sondern Mengen davon verarbeitet. In den meisten herkömmlichen Programmiersprachen muss
man zur Verwaltung von Mengen die entsprechenden Datenstrukturen wie Felder, verkettete Li-
sten, Bäume oder Hash-Tabellen selbst programmieren. In C++ ist dies nicht notwendig.
Die C++-Standardbibliothek bietet ein eigenes Framework zur Arbeit mit Mengen, die so
genannte Standard Template Library oder kurz STL. Dieses Framework stellt Mechanismen zur
Verfügung, um durch einfache Typdeklarationen unterschiedliche Datenstrukturen zur Mengen-
verarbeitung (so genannte Container) verwenden zu können. Darüber hinaus werden Mechanis-
men angeboten, um auf derartige Mengen unabhängig von der darunter liegenden Datenstruktur
Operationen (so genannte Algorithmen) anwenden zu können.
Der große Vorteil der STL besteht darin, dass sie ein Interface anbietet, bei dem man sich zur
Verarbeitung von Mengen nicht mehr um die Details der Datenstrukturen und der dazugehöri-
gen Speicherverwaltung kümmern muss. Diese Details werden abstrahiert. Dabei wurde darauf
geachtet, dass dies nicht auf Kosten der Performance geht.
Eine komplette Einführung in die STL wäre Stoff für ein eigenes Buch.9 Die Verwendung der
STL zur einfachen Verwaltung von Mengen stellt jedoch keine großen Aufwand dar und wird in
diesem Abschnitt erläutert.

3.5.1 Beispielprogramm mit Vektoren


Das erste Beispiel zeigt die Verwendung des einfachsten Datentyps zur Mengenverarbeitung, ei-
nes so genannten Vektors. Die Bezeichnung Vektor“ hat nichts mit dem mathematischen Begriff

zu tun, sondern wird in der STL für einen so genannten Container verwendet, der seine Elemente
wie ein dynamisches Feld (Array) verwaltet. Das bedeutet z.B., dass man mit dem Indexopera-
tor auf die Elemente zugreifen kann und ein Einfügen und Löschen von Elementen dann am
schnellsten ist, wenn sich diese Elemente am Ende der Menge befinden (ansonsten müssen die
nachfolgenden Elemente entsprechend verschoben werden).
Das folgende Beispiel definiert als Menge einen Vektor von ganzen Zahlen, fügt sechs Ele-
mente darin ein und gibt alle Elemente der Menge wieder aus:
// stl/vector1.cpp
#include <iostream>
#include <vector>

int main()
{
std::vector<int> menge; // Vektor-Container für ints

9
Nicht ganz objektiv empfehle ich dazu mein bei Addison-Wesley erschienenes Buch The C++ Standard

Library – A Tutorial and Reference“ (ich habe es in englisch geschrieben; eine deutsche Übersetzung ist
mangels Zeit derzeit leider nicht erhältlich).
Sandini Bib

3.5 Verarbeitung von Mengen 75

// Elemente mit den Werten 1 bis 6 einfügen


for (int i=1; i<=6; ++i) {
menge.push_back(i);
}

// alle Elemente jeweils gefolgt von einem Leerzeichen ausgeben


for (unsigned i=0; i<menge.size(); ++i) {
std::cout << menge[i] << ' ';
}

// zum Abschluss ein Zeilenende ausgeben


std::cout << std::endl;
}
Mit
#include <vector>
wird die Headerdatei für den Datentyp vector eingebunden. Bei diesem Datentyp handelt es sich
genau genommen um ein Klassentemplate (eine Klassenschablone). Das bedeutet, es handelt sich
um eine Klasse, bei der noch nicht alle Details feststehen. Konkret muss mit spitzen Klammern
der Datentyp der in der Menge verwalteten Elemente angegeben werden. Die Deklaration
std::vector<int> menge;
legt auf diese Weise einen derartigen Container an. In diesem Fall handelt es sich also um einen
Vektor, der Elemente vom Typ int enthält. Der angelegte Container ist zunächst leer.
Durch den Aufruf der Elementfunktion push_back() für den Container kann man Elemente
am Ende einfügen:
menge.push_back(i);
In der Schleife zur Ausgabe der Elemente wird mit size() jeweils die aktuelle Anzahl der
Elemente im Container abgefragt:
for (unsigned i=0; i<menge.size(); ++i) {
...
}
Als Laufvariable i wird ein vorzeichenfreier Datentyp verwendet. Wird i einfach als int de-
klariert, kann der Vergleich mit menge.size() zu der Warnung führen, das ein vorzeichenfreier
mit einem vorzeichenbehafteten Wert verglichen wird.
Innerhalb der Schleife wird jeweils das Element mit dem Index i ausgegeben. Dazu wird die
typische Feld-Syntax verwendet:
cout << menge[i] << ' ';
Wie üblich hat das erste Element den Index 0 und das letzte Element den Index size()-1.
Sandini Bib

76 Kapitel 3: Grundkonzepte von C++-Programmen

Das Programm liefert folgende Ausgabe:


1 2 3 4 5 6
Einfach, oder? Doch Vorsicht, STL-Container sind nicht idiotensicher“. Die Container sind so

programmiert, dass man von der darunter liegenden Datenstruktur nichts sehen soll. Man hat
aber immer noch darauf geachtet, dass die Datentypen möglichst performant sind. So wird bei
einem Zugriff auf ein Element mit [i] wie bei Strings nicht geprüft, ob der Index korrekt ist.
Dies muss der Programmierer sicherstellen. Auf die Prüfung wurde verzichtet, da sie Zeit kostet.
Beim Abwägen der Vor- und Nachteile war die wesentliche Argumentation, dass man diesen
Datentyp jederzeit mit einem prüfenden Mengentyp kapseln könnte. Umgekehrt kann man aber
keine Kapsel um einen prüfenden Datentyp legen, die dann doch nicht prüft.

3.5.2 Beispielprogramm mit Deques


Das vorherige Beispiel lässt sich leicht variieren. Dazu soll das folgende Programm ein Beispiel
liefern. Es erzeugt wieder eine Menge, fügt sechs Elemente ein und gibt alle Elemente wieder
aus. Im Unterschied zum vorherigen Beispiel sind die Elemente diesmal allerdings Strings, als
Container wird eine Deque (gesprochen Deck“ ) verwendet, und die Elemente werden jeweils

vorn eingefügt:
// stl/deque1.cpp
#include <iostream>
#include <deque>
#include <string>

int main()
{
std::deque<std::string> menge; // Deque-Container für strings

// Elemente jeweils vorn einfügen


menge.push_front("oefter");
menge.push_front("immer");
menge.push_front("aber");
menge.push_front("immer");
menge.push_front("nicht");

// alle Elemente jeweils gefolgt von einem Leerzeichen ausgeben


for (unsigned i=0; i<menge.size(); ++i) {
std::cout << menge[i] << ' ';
}

// zum Abschluss ein Zeilenende ausgeben


std::cout << std::endl;
}
Sandini Bib

3.5 Verarbeitung von Mengen 77

In diesem Fall wird mit


#include <deque>
die Headerdatei für die Deque eingebunden.
Die Deklaration
std::deque<std::string> menge;
erzeugt eine leere Menge von String-Elementen.
Die Elemente werden jeweils mit push_front() eingefügt:
menge.push_front("oefter");
...
menge.push_front("nicht");
Da die Elemente jeweils vorn eingefügt werden, dreht sich die Reihenfolge der Elemente um. Das
zuletzt eingefügte Wort befindet sich ganz vorn. Das Programm hat somit folgende Ausgabe:
nicht immer aber immer oefter

3.5.3 Vektor versus Deque


In den beiden vorherigen Beispielen wurden zwei verschiedene STL-Container verwendet, ein
Vektor und eine Deque. Worin unterscheiden sich diese beiden Datentypen?
Die Bezeichnung deque“ ist eine Abkürzung für double-ended queue“. Es handelt sich
” ”
dabei um eine etwas trickreichere Datenstruktur, die im Prinzip die Eigenschaften eines dynami-
schen Feldes hat, dabei aber in beide Richtungen wachsen kann.
Wenn man bei einem Vektor vorn ein neues erstes Element einfügt, müssen alle bisherigen
Elemente einen Speicherplatz nach hinten verschoben werden. Bei einer Deque ist das nicht der
Fall. Die interne Implementierung verwaltet typischerweise einfach ein Feld von Feldern, bei
dem beide Enden wachsen und schrumpfen können (siehe Abbildung 3.3).

Abbildung 3.3: Typische interne Struktur einer Deque


Sandini Bib

78 Kapitel 3: Grundkonzepte von C++-Programmen

Ein Vektor ist also schnell, wenn man Elemente am Ende einfügt oder löscht. Ein Einfügen von
Elementen am Anfang ist dagegen langsam. Eine Deque ist dagegen an beiden Enden schnell
(wenn auch durch die interne Struktur im Vergleich zu Vektoren etwas langsamer). In beiden
Containern ist das Einfügen und Löschen von Elementen in der Mitte aber relativ langsam, da
immer Elemente verschoben werden müssen.
Dieser Unterschied in den Fähigkeiten spiegelt sich auch in der jeweiligen Schnittstelle der
beiden Klassen wider. Die Deque bietet Funktionen zum Einfügen und Löschen an beiden Enden.
Insofern kann man bei einer Deque die Elemente auch mit push_back() einfügen. Die Funktion
push_front() wird dagegen für Vektoren nicht angeboten.
Generell bieten STL-Container spezielle Funktionen, die die speziellen Fähigkeiten der Con-
tainer widerspiegeln und ein gutes Zeitverhalten besitzen. Dies soll Programmierer etwas davor
bewahren, Funktionen oder Container mit schlechtem Zeitverhalten zu verwenden. Man kann
allerdings trotzdem in beide Container Elemente mit einer allgemeinen Einfügefunktion an be-
liebiger Stelle einfügen (dies wird später auf Seite 89 erläutert).

3.5.4 Iteratoren
Im Allgemeinen wird auf STL-Container mit Hilfe von Iteratoren zugegriffen. Iteratoren sind
Objekte, die über Container iterieren“ (wandern) können. Jedes Iterator-Objekt repräsentiert

eine Position in einem Container.
Der entscheidende Vorteil von Iteratoren ist, dass sie es erlauben, für alle Container die glei-
che Schnittstelle zum Zugriff auf die Elemente zu verwenden. So setzt der Inkrement-Operator
einen Iterator immer eine Position weiter. Dies ist unabhängig davon, ob er über ein Feld, eine
verkettete Liste oder eine andere Datenstruktur wandert. Dies ist möglich, da die eigentlichen
Iterator-Typen zu jedem Container passend zur Verfügung gestellt werden und somit die Daten-
organisation im Container berücksichtigen.
Die folgenden fundamentalen Operationen definieren das Verhalten eines Iterators:
 Operator *
liefert das Element, an dessen Position sich der Iterator befindet.
 Operator ++
setzt den Iterator ein Element weiter.
 Operator == und Operator !=
liefert, ob zwei Iteratoren dasselbe Objekt repräsentieren (gleicher Container, gleiche Positi-
on).
 Operator =
weist einem Iterator die Position eines anderen Iterators zu.
Um mit Iteratoren arbeiten zu können, stellen Container entsprechende Elementfunktionen be-
reit. Die beiden wichtigsten sind:
 begin()
liefert einen Iterator, der die Position des ersten Elements im Container repräsentiert.
 end()
liefert einen Iterator, der die Position hinter dem letzten Element (engl.: past-the-end“) im

Container repräsentiert.
Sandini Bib

3.5 Verarbeitung von Mengen 79

b e g i n ( ) e n d ( )

Abbildung 3.4: begin() und end() bei Containern

Der Bereich von begin() bis end() ist also eine halboffene Menge (siehe Abbildung 3.4).
Dies hat den Vorteil, dass sich eine einfache Ende-Bedingung für Iteratoren, die über Container
wandern, definieren lässt: Die Iteratoren wandern, bis end() erreicht ist. Ist begin() gleich
end(), ist die Menge leer.
Das folgende Beispielprogramm gibt mit Iteratoren alle Elemente in einem Vektor-Container
aus. Es ist das Beispiel von Seite 74, umgestellt auf die Verwendung von Iteratoren:
// stl/vector2.cpp
#include <iostream>
#include <vector>

int main()
{
std::vector<int> menge; // Vektor-Container für ints

// Elemente mit den Werten 1 bis 6 einfügen


for (int i=1; i<=6; ++i) {
menge.push_back(i);
}

// alle Elemente jeweils gefolgt von einem Leerzeichen ausgeben


// - Iterator wandert dazu über alle Elemente
std::vector<int>::iterator pos;
for (pos = menge.begin(); pos != menge.end(); ++pos) {
std::cout << *pos << ' ';
}

// zum Abschluss ein Zeilenende ausgeben


std::cout << std::endl;
}
Sandini Bib

80 Kapitel 3: Grundkonzepte von C++-Programmen

Nachdem zunächst wieder die Menge mit den Werten 1 bis 6 gefüllt wurde, werden alle Elemente
in der For-Schleife mit Hilfe eines Iterators ausgegeben. Der Iterator pos wird dabei zunächst als
Iterator der dazugehörigen Containerklasse definiert:
std::vector<int>::iterator pos;
Der Typ des Iterators ist also iterator zum Container vector<int> in der Standardbibliothek

std“. Dies zeigt, dass Iteratoren jeweils von der dazugehörigen Containerklasse zur Verfügung
gestellt werden.
In der For-Schleife selbst wird der Iterator mit der Position des ersten Elements initialisiert
und wandert dann über alle Elemente, solange er nicht das Ende (also die Position hinter dem
letzten Element) erreicht hat (vgl. Abbildung 3.5):
for (pos = menge.begin(); pos != menge.end(); ++pos) {
cout << *pos << ' ';
}
Dabei wird mit
*pos
jeweils auf das aktuelle Element zugegriffen.

b e g i n ( ) p o s + + e n d ( )

Abbildung 3.5: Über einen Vektor wandernder Iterator pos

Als Datentyp des Iterators kann auch const_iterator verwendet werden:


std::vector<int>::const_iterator pos;
Dieser Datentyp stellt sicher, dass Elemente durch einen Iterator-Zugriff nicht verändert werden
und kann insbesondere auch bei nichtänderbaren Containern verwendet werden. Auf Seite 456
wird ein const_iterator in einer Funktion verwendet, die in der Lage ist, die Elemente belie-
biger STL-Container auszugeben.

3.5.5 Beispielprogramm mit einer Liste


Mit Hilfe von Iteratoren kann man auch auf Container zugreifen, für die der Indexoperator nicht
angeboten wird. Dabei handelt es sich um alle Container, deren Elemente als Knoten mit Verwei-
sen auf Nachfolger und Vorgänger verwaltet werden. Ein Beispiel dafür ist ein List-Container.
Ein List-Container – oder kurz eine Liste“ – ist als doppelt verkettete Liste von Elementen

implementiert. Das bedeutet, dass jedes Element in der Menge auf genau einen Vorgänger und
Sandini Bib

3.5 Verarbeitung von Mengen 81

genau einen Nachfolger zeigt. Dadurch ist kein wahlfreier Zugriff mehr möglich. Um auf das
zehnte Element zuzugreifen, müssen erst die ersten neun aufgesucht werden. Ein Wechsel auf
benachbarte Elemente ist in beiden Richtungen in konstanter (immer der gleichen) Zeit möglich.
Der Zugriff auf ein bestimmtes Element dauert aber in linearer Zeit (ist proportional zur Entfer-
nung zum Anfang). Dafür ist das Einfügen und Löschen von Elementen an allen Stellen gleich
schnell. Es müssen lediglich die entsprechenden Zeiger umgehängt werden.
Das folgende Beispiel definiert eine Liste für Zeichen, fügt die Zeichen von 'a' bis 'z' ein
und diese mit Hilfe eines Iterators wieder aus:
// stl/list1.cpp
#include <iostream>
#include <list>

int main()
{
std::list<char> menge; // List-Container für chars

// Elemente mit den Werten 'a' bis 'z' einfügen


for (char c='a'; c<='z'; ++c) {
menge.push_back(c);
}

// alle Elemente jeweils gefolgt von einem Leerzeichen ausgeben


// - Iterator wandert dazu über alle Elemente
std::list<char>::iterator pos;
for (pos = menge.begin(); pos != menge.end(); ++pos) {
std::cout << *pos << ' ';
}

// zum Abschluss ein Zeilenende ausgeben


std::cout << std::endl;
}
Die Schleife zum Ausgeben aller Elemente sieht genauso wie vorher aus. Lediglich der Datentyp
des Iterators hat sich geändert. Da dieser Datentyp zu einer Liste gehört, weiß er, dass er, um bei
++ zum nächsten Element zu kommen, nicht einfach einen Index erhöhen, sondern einen Verweis
traversieren muss (siehe Abbildung 3.6).

3.5.6 Beispielprogramme mit assoziativen Containern


Die vorgestellte Iterator-Schleife kann (bei entsprechender Anpassung des Iterator-Typs) für alle
Container verwendet werden. Dies gilt für alle bisher vorgestellten Container vom Typ Vektor,
Sandini Bib

82 Kapitel 3: Grundkonzepte von C++-Programmen

b e g i n ( ) p o s + + e n d ( )

Abbildung 3.6: Über eine Liste wandernder Iterator pos

Deque und Liste. Diese Container werden auch als sequenzielle Container bezeichnet, da der
Anwendungsprogrammierer die Position der Elemente im Container bestimmt.
Daneben gibt es auch noch die assoziativen Container. In diesen Containern werden die
Elemente automatisch sortiert. Der Wert der Elemente bestimmt also deren Position. Es gibt
folgende Arten von assoziativen Containern:
 Set
Ein Set-Container – oder kurz ein Set“ – ist eine Menge, in der die Elemente nur jeweils

einmal vorkommen dürfen und automatisch nach ihrem Wert sortiert werden.
 Multiset
Ein Multiset-Container – oder kurz ein Multiset“ – entspricht einem Set mit dem Unter-

schied, dass Elemente mit gleichem Wert mehrfach vorkommen dürfen. Auch hier werden
sie automatisch nach ihrem Wert sortiert.
 Map
Ein Map-Container – oder kurz eine Map“ – verwaltet Schlüssel/Wert-Paare. Zu jedem Ele-

ment gehört ein identifizierender Schlüssel, nach dem sortiert wird, und ein dazugehöriger
Wert. Jeder Schlüssel darf hier nur einmal vorkommen.
 Multimap
Ein Multimap-Container – oder kurz eine Multimap“ – entspricht einer Map mit dem Un-

terschied, dass die Schlüssel mehrfach vorkommen dürfen.
Alle diese Container verwenden intern typischerweise einen Binärbaum als Datenstruktur. Auch
zu diesen Containern folgen zwei Beispiele.

Beispiel mit Sets


Das erste Beispiel zeigt die entsprechende Verwendung eines Sets:
// stl/set1.cpp
#include <iostream>
#include <set>

int main()
{
std::set<int> menge; // Set-Container für ints
Sandini Bib

3.5 Verarbeitung von Mengen 83

// sieben Elemente mit den Werten 1 bis 6 ungeordnet einfügen


menge.insert(3);
menge.insert(1);
menge.insert(5);
menge.insert(4);
menge.insert(6);
menge.insert(2);
menge.insert(1);

// alle Elemente jeweils gefolgt von einem Leerzeichen ausgeben


std::set<int>::iterator pos;
for (pos = menge.begin(); pos != menge.end(); ++pos) {
std::cout << *pos << ' ';
}

// zum Abschluss ein Zeilenende ausgeben


std::cout << std::endl;
}
Mit
#include <set>
wird die Headerdatei für Sets eingebunden.
Mit
std::set<int> menge;
wird ein leeres Set für Elemente vom Typ int angelegt. An dieser Stelle könnte man auch ein
Sortierkriterium angeben. Da dies nicht der Fall ist, werden die Elemente als Default mit dem
Operator < aufsteigend sortiert.
Mit der Elementfunktion insert() wird jeweils ein Element eingefügt und an der richtigen
Stelle einsortiert:
menge.insert(3);
menge.insert(1);
...
menge.insert(1);
Nach dem Einfügen aller Werte ergibt sich im Prinzip der in Abbildung 3.7 dargestellte Zustand:
Die Elemente wurden so in die interne Baumstruktur des Containers einsortiert, dass sich jeweils
links kleinere und rechts größere Elemente befinden. Da ein Set und kein Multiset verwendet
wurde, ist das mehrfach eingefügte Element mit dem Wert 1 trotzdem nur einmal in der Menge
vorhanden. Bei einem Multiset wäre es zweimal vorhanden.
Die Ausgabe aller Elemente funktioniert dann nach demselben Muster wie beim vorherigen
Beispiel. Ein Iterator wandert über alle Elemente und gibt sie jeweils aus:
Sandini Bib

84 Kapitel 3: Grundkonzepte von C++-Programmen

2 6

1 3 5

Abbildung 3.7: Ein Set mit sechs Elementen

std::set<int>::iterator pos;
for (pos = menge.begin(); pos != menge.end(); ++pos) {
std::cout << *pos << ' ';
}
Dabei ist der Inkrement-Operator des Iterators so definiert, dass dieser unter Berücksichtigung
der Baumstruktur des Containers jeweils das richtige Nachfolgeelement findet. Vom dritten Ele-
ment wird hoch zum vierten und wieder hinunter zum fünften Element traversiert (siehe Abbil-
dung 3.8).

pos ++

2 6

1 3 5

Abbildung 3.8: Über ein Set wandernder Iterator pos

Die Ausgabe des Programms lautet entsprechend:


1 2 3 4 5 6

Beispiel mit Maps


Bei Maps ist zu beachten, dass ein Element der Menge ein Schlüssel/Wert-Paar ist. Dies beein-
flusst die Deklaration, das Einfügen und den Zugriff auf Elemente:
Sandini Bib

3.5 Verarbeitung von Mengen 85

// stl/mmap1.cpp
#include <iostream>
#include <map>
#include <string>

int main()
{
// Datentyp der Menge
typedef std::multimap<int,std::string> IntStringMMap;

IntStringMMap menge; // Multimap-Container für int/string-Wertepaare

// einige Elemente ungeordnet einfügen


// - zwei Werte haben den Schlüssel 1
menge.insert(std::make_pair(5,"feucht"));
menge.insert(std::make_pair(2,"besten"));
menge.insert(std::make_pair(1,"Die"));
menge.insert(std::make_pair(4,"sind:"));
menge.insert(std::make_pair(5,"lang"));
menge.insert(std::make_pair(3,"Parties"));

/ * die Werte aller Elemente ausgeben


* - ein Iterator wandert über alle Elemente
* - mit second wird auf den Wert der Elemente zugegriffen
*/
IntStringMMap::iterator pos;
for (pos = menge.begin(); pos != menge.end(); ++pos) {
std::cout << pos->second << ' ';
}
std::cout << std::endl;
}
Da der Typ des Containers an mehreren Stellen benötigt wird, wird er einmal als Datentyp defi-
niert:
typedef std::multimap<int,std::string> IntStringMMap;
Statt mit
std::multimap<int,std::string> menge;
kann die Menge nun mit
IntStringMMap menge;
angelegt werden.
Sandini Bib

86 Kapitel 3: Grundkonzepte von C++-Programmen

Der Datentyp definiert eine Multimap, deren Elemente den Datentyp int als Schlüssel und den
Datentyp string als Wert besitzen.
Da die Elemente Wertepaare sind, werden sie mit Hilfe von make_pair() eingefügt:
menge.insert(std::make_pair(5,"feucht"));
menge.insert(std::make_pair(2,"besten"));
...
make_pair() erzeugt Objekte vom Typ std::pair, die einfach nur Wertepaare darstellen.
Entsprechend kann man die Elemente auch nicht einfach ausgeben. Greift man über einen
Iterator auf die Elemente mit *pos zu, erhält man wiederum den Datentyp std::pair. Bei
einem solchen Wertepaar kann man mit .first auf den ersten Teil des Wertepaares (in diesem
Fall also auf den Schlüssel) und mit .second auf den zweiten Teil des Wertepaares (in diesem
Fall also auf den zum Schlüssel gehörenden Wert) zugreifen. Zum Ausgeben von Schlüssel und
Wert könnte man also Folgendes schreiben:
std::cout << (*pos).first << ' '; // Schlüssel des Elements ausgeben
std::cout << (*pos).second << ' '; // Wert des Elements ausgeben
Man beachte, dass man den Ausdruck mit dem Stern-Operator klammern muss, da er eine nied-
rigerer Priorität als der Punkt-Operator besitzt. Es gibt für diese Kombination von Operatoren
allerdings den Operator -> als Abkürzung, weshalb der Wert der Elemente wie hier im Beispiel
auch einfach mit
std::cout << pos->second << ' '; // Wert des Elements ausgeben
ausgegeben werden kann.
Die Ausgabe des Programms könnte wie folgt lauten:
Die besten Parties sind: feucht lang
Man beachte, dass es zwei Elemente mit dem Schlüssel 5 gibt. Je nach Implementierung der
Standardbibliothek ist also auch folgende Ausgabe möglich:
Die besten Parties sind: lang feucht

3.5.7 Algorithmen
Wie die bisherigen Beispiele zeigen, wird die tatsächliche Datenstruktur von STL-Containern
weitgehend gekapselt. Es mag zwar einige spezielle Operationen geben (wie den Indexoperator
für Vektoren und Deques), doch kann man mit Iteratoren bei allen Containern auf die gleiche
Weise auf die Elemente zugreifen.
Diesen Umstand kann man ausnutzen, um Funktionen zu schreiben, die universell mit allen
Datenstrukturen umgehen können. Die STL bietet auch gleich eine Reihe von derartigen Funk-
tionen. Diese werden dort Algorithmen genannt. Es gibt unter anderem Algorithmen zum Finden,
Vertauschen, Sortieren, Kopieren, Aufaddieren und Modifizieren von Elementen.
Sandini Bib

3.5 Verarbeitung von Mengen 87

Ein kleines Beispiel soll einige mögliche Algorithmen und deren Anwendung zeigen:
// stl/algo1.cpp
#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
std::vector<int> menge; // Vektor-Container für ints
std::vector<int>::iterator pos; // Iterator

// Elemente 1 bis 6 unsortiert in die Menge einfügen


menge.push_back(2);
menge.push_back(5);
menge.push_back(4);
menge.push_back(1);
menge.push_back(6);
menge.push_back(3);

// kleinstes und größtes Element ausgeben


pos = std::min_element (menge.begin(), menge.end());
std::cout << "min: " << *pos << std::endl;
pos = std::max_element (menge.begin(), menge.end());
std::cout << "max: " << *pos << std::endl;

// alle Elemente aufsteigend sortieren


std::sort (menge.begin(), menge.end());

// Reihenfolge der Elemente umkehren


std::reverse (menge.begin(), menge.end());

// alle Elemente ausgeben


for (pos=menge.begin(); pos!=menge.end(); ++pos) {
std::cout << *pos << ' ';
}
std::cout << std::endl;
}
Sandini Bib

88 Kapitel 3: Grundkonzepte von C++-Programmen

Im Beispiel wird zunächst eine Menge mit sechs unsortierten Integern angelegt:
std::vector<int> menge;

menge.push_back(2);
...
Als erstes werden die beiden Algorithmen std::min_element() und std::max_element()
aufgerufen. Sie erhalten als Parameter jeweils einen durch zwei Iteratoren definierten Bereich, in
dem das minimale bzw. maximale Element gesucht werden soll. Zurückgeliefert wird ein Iterator
für die Position dieses Elements. Bei der Zuweisung
pos = min_element (menge.begin(), menge.end());
liefert min_element() also einen Iterator für das kleinste Element in der gesamten Menge (gibt
es mehrere, wird das erste kleinste genommen) und weist es dem Iterator pos zu. Dieses Element
wird dann ausgegeben:
std::cout << "min: " << *pos << std::endl;
Natürlich wäre das auch gleich in einem Schritt möglich:
std::cout << *std::max_element(menge.begin(),menge.end())
<< std::endl;
Der nächste angewendete Algorithmus ist std::sort(). Er sortiert die Elemente in dem Be-
reich, der durch die übergebenen Iteratoren definiert wird. Da der Bereich in diesem Fall alle
Elemente umfasst, werden auch alle Elemente sortiert:
std::sort (menge.begin(), menge.end());
Der letzte im Beispiel verwendete Algorithmus ist std::reverse(). Er dreht die Reihenfolge
aller Elemente im angegebenen Bereich um:
std::reverse (menge.begin(), menge.end());
Die Ausgabe des Programms lautet entsprechend:
min: 1
max: 6
6 5 4 3 2 1
Dieses Beispiel zeigt, wie bequem es nun geworden ist, verschiedene Datenstrukturen zu ver-
wenden. Man kann insbesondere durch Verwendung anderer Datentypen die Datenstrukturen
bequem wechseln. Das gleiche Beispiel läuft auch, wenn man vector jeweils durch deque oder
list ersetzt. Nur bei der Verwendung von einem Set gibt es Schwierigkeiten. Da ein Set selbst
die Reihenfolge der Elemente bestimmt, kann man sort() und reverse() nicht aufrufen. Der
Versuch wird durch eine leider oft sehr kryptische Fehlermeldung des Compilers quittiert. Au-
ßerdem muss push_back() bei Sets durch insert() ersetzt werden.
Sandini Bib

3.5 Verarbeitung von Mengen 89

Finden und einfügen


Ein weiteres Beispiel soll demonstrieren, wie mit Hilfe von Algorithmen und Iteratoren Elemen-
te vor bestimmten anderen Elementen eingefügt werden können. Das Programm hat folgenden
Aufbau:
// stl/algo2.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main()
{
std::vector<std::string> menge; // Container für Strings
std::vector<std::string>::iterator pos; // Iterator

// Verschiedene Städtenamen einfügen


menge.push_back("Hamburg");
menge.push_back("Munchen");
menge.push_back("Berlin");
menge.push_back("Braunschweig");
menge.push_back("Duisburg");
menge.push_back("Leipzig");

// alle Elemente aufsteigend sortieren


std::sort (menge.begin(), menge.end());

/* ”Hannover” vor ”Hamburg” einfügen


* - Position von ”Hamburg” suchen
* - ”Hannover” davor einfügen
*/
pos = find (menge.begin(), menge.end(), // Bereich
"Hamburg"); // Suchkriterium
if (pos != menge.end()) {
menge.insert(pos,"Hannover");
}
else {
std::cerr << "Huch, Hamburg ist gar nicht vorhanden"
<< std::endl;
menge.push_back("Hannover");
}
Sandini Bib

90 Kapitel 3: Grundkonzepte von C++-Programmen

// alle Elemente ausgeben


for (pos=menge.begin(); pos!=menge.end(); ++pos) {
std::cout << *pos << ' ';
}
std::cout << std::endl;
}
Zunächst werden in einen Vektor verschiedene Städtenamen eingefügt und sortiert. Die interes-
sante Stelle ist die Anweisung, mit der versucht wird, Hannover vor Hamburg einzufügen. Dazu
wird zunächst der Find-Algorithmus aufgerufen, um die Position von Hamburg herauszufinden:
pos = find (menge.begin(), menge.end(), // Bereich
"Hamburg"); // Suchkriterium
Wie bei Algorithmen üblich, werden zunächst der Anfang und das Ende des Bereichs, der durch-
sucht werden soll, übergeben. Der dritte Parameter ist der Wert, der gesucht wird. Er wird mit
dem Operator == jeweils mit allen Elementen verglichen.
Wird ein entsprechendes Element gefunden, wird dessen Position in Form eines Iterators
zurückgeliefert. Wird kein passendes Element gefunden, wird das Ende der Menge zurückgelie-
fert, was entsprechend geprüft wird:
if (pos != menge.end()) {
...
}
Sofern Hamburg gefunden wird, wird die Position dazu verwendet, Hannover davor einzufügen.
Dazu wird die Elementfunktion insert() verwendet, bei der als erster Parameter ein Iterator
für die Position und als zweiter Parameter der einzufügende Wert übergeben wird:
menge.insert(pos,"Hannover");
Die Ausgabe des Programms lautet entsprechend:
Berlin Braunschweig Duisburg Hannover Hamburg Leipzig M
unchen

3.5.8 Algorithmen mit mehreren Bereichen


Bei den meisten Algorithmen, die mehrere Bereiche bearbeiten, muss nur beim ersten Bereich
sowohl der Anfang als auch das Ende angegeben werden. Bei allen anderen Bereichen ist die
Angabe des Anfangs ausreichend. Das Ende folgt dann aus dem Zusammenhang bzw. der Ope-
ration.
Dies gilt insbesondere bei Algorithmen, die Elemente gegebenenfalls modifiziert in eine an-
dere Menge kopieren. Hierbei ist eine Warnung angebracht: Es ist unbedingt darauf zu achten,
dass die Zielmengen groß genug sind! Das folgende Programm verdeutlicht das Problem:
// stl/copy1.cpp
#include <iostream>
#include <vector>
#include <list>
Sandini Bib

3.5 Verarbeitung von Mengen 91

#include <algorithm>

int main()
{
std::list<int> menge1;
std::vector<int> menge2;

// Elemente 1 bis 6 in die erste Menge einfügen


for (int i=1; i<=6; i++) {
menge1.push_back(i);
}

/* LAUFZEITFEHLER:
* - Elemente in die zweite Menge kopieren
*/
std::copy (menge1.begin(), menge1.end(), // Quellbereich
menge2.begin()); // Zielbereich
}
Der Algorithmus std::copy() erhält als Parameter den Anfang und das Ende des Quellbereichs
und den Anfang eines Zielbereichs, in den die Elemente des Quellbereichs kopiert werden. Er
setzt voraus, dass der Zielbereich bereits groß genug ist, um alle Elemente aufzunehmen. Wenn
der Zielbereich aber wie in diesem Beispiel leer ist, wird auf Speicher zugegriffen, der nicht
verfügbar ist, was im besten Fall zu einem Absturz führt (dann merkt man wenigstens, dass
etwas schief gegangen ist).
Um solche Fehler zu vermeiden, gibt es zwei Möglichkeiten:
1. Man muss dafür sorgen, dass der Zielbereich groß genug ist.
2. Man muss einfügende Iteratoren verwenden.
Damit der Zielbereich groß genug ist, muss er entweder gleich mit der richtigen Größe ange-
legt oder explizit auf die richtige Größe gesetzt werden. Beides ist aber nur bei sequenziellen
Containern (Vektoren, Deques, Listen) möglich.
Das folgende Programm zeigt dies an einem Beispiel:
// stl/copy2.cpp
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>
Sandini Bib

92 Kapitel 3: Grundkonzepte von C++-Programmen

int main()
{
std::list<int> menge1;
std::vector<int> menge2;

// Elemente 1 bis 6 in die erste Menge einfügen


for (int i=1; i<=6; i++) {
menge1.push_back(i);
}

// Platz für die zu kopierenden Elemente schaffen


menge2.resize(menge1.size());

// Elemente in die zweite Menge kopieren


std::copy (menge1.begin(), menge1.end(), // Quellbereich
menge2.begin()); // Zielbereich

/* dritte Menge ausreichend groß definieren


* - Die Startgröße wird als Parameter übergeben
*/
std::deque<int> menge3(menge1.size());

// Elemente in die dritte Menge kopieren


std::copy (menge1.begin(), menge1.end(), // Quellbereich
menge3.begin()); // Zielbereich
}
Man beachte, dass durch das Definieren der Größe auch Elemente angelegt werden. Diese Ele-
mente werden jeweils mit ihrem Default-Wert erzeugt.

Einfügende Iteratoren
Die andere Möglichkeit, einfügende Iteratoren zu verwenden, zeigt folgendes Beispiel:
// stl/copy3.cpp
#include <iostream>
#include <vector>
#include <list>
#include <deque>
#include <algorithm>

int main()
{
Sandini Bib

3.5 Verarbeitung von Mengen 93

std::list<int> menge1;
std::vector<int> menge2;
std::deque<int> menge3;

// Elemente 1 bis 6 in die erste Menge einfügen


for (int i=1; i<=6; i++) {
menge1.push_back(i);
}

// Elemente hinten einfügend in die zweite Menge kopieren


std::copy (menge1.begin(), menge1.end(), // Quellbereich
std::back_inserter(menge2)); // Zielbereich

// Elemente vorn einfügend in die dritte Menge kopieren


std::copy (menge1.begin(), menge1.end(), // Quellbereich
std::front_inserter(menge3)); // Zielbereich
}
Hier werden zwei speziell vordefinierte Iteratoren (so genannte Iterator-Adapter) verwendet:
 Back-Inserter
Ein Back-Inserter fügt die Elemente jeweils am Ende ein (hängt sie also an). Mit jeder Zuwei-
sung eines Elements wird dieses mit push_back() in den übergebenen Container eingefügt.
Durch den Aufruf
std::copy (menge1.begin(), menge1.end(), // Quellbereich
std::back_inserter(menge2)); // Zielbereich
werden also alle Elemente aus menge1 jeweils am Ende von menge2 eingefügt.
Die Operation kann nur bei Containern aufgerufen werden, bei denen die Elementfunktion
push_back() vorhanden ist. Dies sind Vektoren, Deques und Listen.
 Front-Inserter
Ein Front-Inserter fügt die Elemente jeweils am Anfang eines Containers durch Aufruf von
push_front() ein. Dies hat zur Folge, dass die Reihenfolge der einzufügenden Elemente
umgedreht wird. Durch den Aufruf
std::copy (menge1.begin(), menge1.end(), // Quellbereich
std::front_inserter(menge3)); // Zielbereich
werden also alle Elemente aus menge1 jeweils am Anfang von menge3 eingefügt.
Die Operation kann nur bei Containern aufgerufen werden, bei denen die Elementfunktion
push_front() vorhanden ist. Dies sind Deques und Listen.
Sandini Bib

94 Kapitel 3: Grundkonzepte von C++-Programmen

3.5.9 Stream-Iteratoren
Eine weitere Form von Iterator-Adaptern sind Stream-Iteratoren. Dies sind Iteratoren, die aus
einem Stream lesen bzw. in einen Stream schreiben. Die Eingaben von der Tastatur oder die
Ausgaben auf den Bildschirm bilden bei diesen Iteratoren die Menge“ oder den Container“,
” ”
deren bzw. dessen Elemente durch die Algorithmen der STL bearbeitet werden. Das folgende
Beispiel zeigt, wie dies konkret aussehen kann:
// stl/ioiter1.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

int main()
{
using namespace std; // Alle Symbole in std sind global

vector<string> menge; // Vektor-Container für Strings

/* Strings von der Standard-Eingabe bis zum Ende der Daten einlesen
* - von der ‘‘Eingabe-Menge’’ cin einfügend in menge kopieren
*/
copy (istream_iterator<string>(cin), // Beginn Quellbereich
istream_iterator<string>(), // Ende Quellbereich
back_inserter(menge)); // Zielbereich

// Elemente in menge sortieren


sort (menge.begin(), menge.end());

/* alle Elemente ausgeben


* - von menge in die ‘‘Ausgabe-Menge’’ cout kopieren
* - jeder String auf einer Zeile (durch "\n" getrennt)
*/
copy (menge.begin(), menge.end(), // Quellbereich
ostream_iterator<string>(cout,"\n")); // Zielbereich
}
Zunächst wird in diesem Programm eine spezielle Using-Direktive verwendet:
using namespace std;
Diese Direktive besagt, dass im aktuellen Gültigkeitsbereich auf alle Symbole aus std global
zugegriffen werden kann. Damit entfällt die Notwendigkeit der Qualifizierung aller Symbole aus
Sandini Bib

3.5 Verarbeitung von Mengen 95

der Standardbibliothek. Vorsicht, eine derartige Direktive sollte nur in einem Kontext verwendet
werden, in dem man weiß, dass dadurch keine Konflikte oder unerwünschten Seiteneffekte her-
vorgerufen werden. Insofern sollten Using-Direktiven nie in Headerdateien verwendet werden.
In der Anweisung
copy (istream_iterator<string>(cin), // Beginn Quellbereich
istream_iterator<string>(), // Ende Quellbereich
back_inserter(menge)); // Zielbereich
werden gleich zwei spezielle Iteratoren erzeugt:
 Der Ausdruck
istream_iterator<string>(cin)
erzeugt für den Input-Stream cin einen Iterator, der string-Elemente einliest. Diese Ele-
mente werden vom Iterator jeweils mit dem Eingabeoperator >> eingelesen.
 Der Ausdruck
istream_iterator<string>()
erzeugt, da kein Parameter übergeben wurde, einen speziellen End-Of-Stream-Iterator. Er
steht für einen Stream, von dem nicht mehr gelesen werden kann.
Der copy()-Algorithmus lässt den übergebenen ersten Iterator operieren, solange er ungleich
dem zweiten Iterator ist. Das bedeutet, dass von cin so lange gelesen wird, bis keine Daten mehr
vorliegen bzw. gelesen werden können. Insgesamt definiert der übergebene Bereich also alle

von cin gelesenen strings“. Diese werden mittels copy() mit Hilfe eines Back-Inserters in
menge eingefügt.
Nachdem die eingelesenen Elemente in der Menge sortiert worden sind, werden in der An-
weisung
copy (menge.begin(), menge.end(), // Quellbereich
ostream_iterator<string>(cout,"\n")); // Zielbereich
alle Elemente der Menge in den Zielbereich“ cout kopiert. Der Ausdruck

ostream_iterator<string>(cout,"\n")
erzeugt dabei für den Output-Stream cout einen Iterator, der string-Elemente ausgibt. Auch
hier kann ein beliebiger Typ angegeben werden, für den dann jeweils der Ausgabe-Operator <<
aufgerufen wird. Der optionale zweite Parameter definiert, welche Zeichenfolge jeweils zwischen
zwei Elementen eingefügt wird. In diesem Fall ist es ein Zeilentrenner, der dafür sorgt, dass jeder
String auf einer eigenen Zeile ausgegeben wird.
Insgesamt liest das Programm also alle Strings von der Standard-Eingabe ein und gibt sie
sortiert, jeden String auf einer Zeile, wieder aus.
Dieses Beispiel zeigt, wie prägnant in C++ mit Hilfe der STL programmiert werden kann.
Durch ein Auswechseln der Datentypen kann außerdem schnell und einfach geprüft werden,
ob andere Datenstrukturen als Vektoren die Aufgabe schneller erledigen. So könnte man zum
Beispiel versuchsweise einen Set-Container verwenden. Da dieser automatisch sortiert, kann und
muss der Aufruf von sort() in dem Fall entfallen. Probieren Sie es ruhig aus.
Sandini Bib

96 Kapitel 3: Grundkonzepte von C++-Programmen

3.5.10 Ausblick
Wie schon am Anfang des Abschnitts erwähnt, war dies nur ein allgemeiner Einstieg in die
STL. Viele Details, Hintergrundinformationen und ergänzende Techniken wurden nicht erläutert.
Dennoch reichen diese Beispiele aus, um erst einmal Mengen programmieren zu können.
Im weiteren Verlauf werden STL-Container noch an verschiedener Stelle verwendet. In Kapi-
tel 9 werden noch weitere Details und hilfreiche Techniken zum Umgang mit der STL vorgestellt:
 In Abschnitt 9.1.1 werden alle Operationen von Vektoren im Einzelnen vorgestellt.
 In Abschnitt 9.1.3 werden alle Standard-Algorithmen kurz aufgelistet.
 In Abschnitt 9.2.1 wird vorgestellt, wie man mit Hilfe von Smart-Pointern in STL-Containern
Verweise auf Objekte verwalten kann.
 In Abschnitt 9.2.2 wird erläutert, wie man mit Hilfsfunktionen und Funktionsobjekten Verar-
beitungskriterien für STL-Algorithmen definieren kann.
Für weitere Details sei auf spezielle Bücher zur C++-Standardbibliothek und zur STL verwiesen.

3.5.11 Zusammenfassung
 Die STL-Komponente der C++-Standardbibliothek bietet verschiedene Container, Iteratoren
und Algorithmen, mit deren Hilfe man bequem Mengen in unterschiedlichen Datenstrukturen
verwalten kann.
 Im Einzelnen existieren folgende Datentypen mit ihren dazugehörigen typischen Datenstruk-
turen:
Container Typische Datenstruktur
vector dynamisches Feld (Array)
deque nach beiden Enden offenes dynamisches Feld
list doppelt verkettete Liste
set Binärbaum ohne Duplikate
multiset Binärbaum mit Duplikaten
map Binärbaum für Schlüssel/Wert-Paare ohne Duplikate
multimap Binärbaum für Schlüssel/Wert-Paare mit Duplikaten

 Iteratoren erlauben es, bei allen Containern unabhängig von der dazugehörigen Datenstruktur
auf die Elemente zuzugreifen.
 Verschiedene Algorithmen sind für Standardzugriffe auf Mengen vordefiniert.
 Inserter sind spezielle Iteratoren, die einfügend operieren.
 Ohne Inserter muss darauf geachtet werden, dass Zielmengen von Algorithmen ausreichen
groß sind.
 Stream-Iteratoren erlauben es, die Standard-Ein- und -Ausgabe als Menge bzw. Container zu
verwenden.
 Mit typedef kann man einen eigenen Namen für Datentypen definieren.
Sandini Bib

3.5 Verarbeitung von Mengen 97

 Mit
using namespace std;
kann man die Symbole der Standardbibliothek global verfügbar machen. Diese Möglichkeit
sollte nie in Headerdateien verwendet werden.
Sandini Bib

98 Kapitel 3: Grundkonzepte von C++-Programmen

3.6 Ausnahmebehandlung
Dieser Abschnitt führt in die so genannte Ausnahmebehandlung (als Anglizismus auch Exception-
Handling genannt) ein. Mit der Ausnahmebehandlung wird eine neue Art der Behandlung von
Fehlern und Ausnahmesituationen ermöglicht. Im Gegensatz zu herkömmlichen Sprachen, in
denen die Fehlerbehandlung bei Funktionsaufrufen über die normalen Schnittstellen, Parameter
und Rückgabewerte, stattfindet, handelt es sich um ein Sprachmittel, bei dem Fehler oder Aus-
nahmen parallel zum Datenfluss mit einem separaten Mechanismus behandelt werden. Durch
diese Trennung wird das Programmverhalten überschaubarer und sicherer.
In diesem Kapitel wird nach einer konzeptionellen Einführung vor allem auf den Umgang
mit Ausnahmen in Programmen eingegangen. In Abschnitt 4.7 werden dann weitere Details vor
allem aus der Sicht von Klassen, in denen Ausnahmen erkannt und ausgelöst werden, erläutert.

3.6.1 Motivation für das Konzept der Ausnahmebehandlung


In jedem Programm kann es vorkommen, dass Situationen eintreten, die normalerweise nicht
vorgesehen sind. Anwender können unsinnige Werte eingeben, Verbindungen zu anderen Pro-
zessen können unterbrochen werden, Speicherplatz kann verbraucht sein, und last but not least
können einfach Fehler auftreten, da bei der Implementierung nicht alle Fälle bedacht oder gete-
stet wurden.
Damit Programme dann nicht einfach abstürzen, müssen Fehlerbehandlungen eingebaut wer-
den. Dies gilt insbesondere für den Aufruf von Funktionen, die mit der Außenwelt“ Kontakt

aufnehmen: Ein Anwender gibt nicht das ein, was er eingeben darf, eine Systemfunktion kann
nicht das leisten, was sie soll. Aber auch bei internen Funktionsaufrufen und Zugriffen auf im
Programm befindliche Daten können Fehler auftreten.
Um mit Fehlern oder unvorhergesehenen Situationen umzugehen, werden in herkömmlichen
Programmiersprachen Status-Flags, Error-Handler und spezielle Rückgabewerte verwendet. Im
Programm-Code müssen dann ständig diese Fehlerfälle getestet und behandelt werden. Damit
gibt es keine klare Trennung von normalem Datenfluss und Fehlerbehandlung. Insbesondere bei
Funktionsaufrufen werden der normale Datenfluss und die Fehlerbehandlung über ein und die-
selbe Schnittstelle (Parameter und Rückgabewert) abgewickelt (siehe Abbildung 3.9).

Datenfluss

Funktionsaufruf Funktionsaufruf

Fehlerbehandlung

Abbildung 3.9: Datenfluss und Fehlerbehandlung herkömmlich


Sandini Bib

3.6 Ausnahmebehandlung 99

Es ist z.B. typisch, dass spezielle Rückgabewerte Fehlerfälle anzeigen. Dies führt dazu, dass für
den Rückgabewert immer zusätzlich auch eine Sonderbehandlung implementiert werden muss.
Wenn diese Sonderbehandlung entfällt, kann das Programm in einen undefinierten Zustand ge-
raten, der fatale Folgen haben kann. Für die Fehlerbehandlung wird dabei sogar manchmal der
Wertebereich für die Rückgabewerte erweitert. Ein Beispiel dafür ist die C-Funktion getchar(),
die ein Zeichen liest und zurückliefert. Damit neben jedem beliebigen Zeichen auch ein Fehler-
wert zurückgeliefert werden kann, ist der Typ des Rückgabewerts nicht char, sondern int.
In objektorientierten Sprachen wie C++ kommt nun noch der Sonderfall hinzu, dass man-
che Operationen überhaupt keinen Rückgabewert haben können, mit dem ein Fehler angezeigt
werden kann. Ein Beispiel dafür ist das Anlegen von Objekten durch eine einfache Deklarati-
on. Sofern für Objekte bei einer Initialisierung z.B. Speicherplatz angefordert wird, Argumente
verarbeitet oder Dateien geöffnet werden, kann dies schief gehen:
std::string s("hallo"); // kann fehlschlagen
Auch ein Zugriff mit dem Indexoperator kann zu einem Fehlerfall führen:
s[20] = 'q'; // kann fehlschlagen
Derartige Fehlerfälle können mit den herkömmlichen Sprachmitteln nur schlecht oder gar nicht
getestet und behandelt werden; denn wie soll getestet werden, ob beim Erzeugen eines tem-
porären Objekts mitten in einem Ausdruck alles geklappt hat?
Das Dilemma besteht darin, dass der Fehler zwar in den Operationen erkannt, nicht aber
geeignet behandelt werden kann. Damit bleibt mit herkömmlichen Sprachmitteln eigentlich nur
die Möglichkeit, eine Warnung auszugeben und irgendwie weiterzumachen oder das Programm
mit einer Fehlermeldung zu beenden.
Das Grundproblem ist also das folgende: Es treten Fehlersituationen auf, die zwar erkannt,
nicht aber sinnvoll behandelt werden können, da der Kontext, aus dem heraus der Fehler ver-
ursacht wurde, nicht bekannt ist. Andererseits kann der Fehler auch nicht an die Aufrufumge-
bung zurückgemeldet werden, da Rückgabewerte entweder gar nicht definiert sind oder anderen
Zwecken dienen.
Es wird also ein Mechanismus benötigt, der eine Trennung von Fehlererkennung und Feh-
lerbehandlung ermöglicht und dabei nicht die Übergabe von Parametern oder Rückgabewerten
verwendet. Die Fehlerbehandlung sollte vom normalen Datenfluss komplett entkoppelt werden
(siehe Abbildung 3.10).

Datenfluss

Funktionsaufruf Funktionsaufruf

Fehlerbehandlung

Abbildung 3.10: Datenfluss und Fehlerbehandlung nach dem Konzept der Ausnahmebehandlung
Sandini Bib

100 Kapitel 3: Grundkonzepte von C++-Programmen

Das Konzept der Ausnahmebehandlung bietet einen solchen Mechanismus: An beliebiger Stelle
im Code können Fehler erkannt und über einen eigenen Mechanismus an die jeweilige Aufruf-
umgebung gemeldet werden. In dieser Umgebung kann der Fehler dann abgefangen und entspre-
chend der aktuellen Situation sinnvoll behandelt werden. Geschieht dies nicht, wird der Fehler
nicht einfach ignoriert, sondern führt zu einem (definierbaren) geregelten Programmende (und
keinem Programmabbruch).
Um einer möglichen Fehlinterpretation vorzubeugen, sei darauf hingewiesen, dass der Be-
griff Ausnahmebehandlung (Exception-Handling) in C++ bedeutet, dass Fehler oder Situationen
behandelt werden, die ausnahmsweise im normalen Programmablauf auftreten. Es ist kein Me-
chanismus für Interrupts oder Signale, also Nachrichten, die von außen das laufende Programm
unterbrechen und zur Folge haben, dass mitten im Programm an eine ganz andere Stelle zur Un-
terbrechungsbehandlung gesprungen wird. Die Ausnahmen, die für die Ausnahmebehandlung
eine Rolle spielen, werden im Programm durch spezielle C++-Anweisungen ausgelöst und in-
nerhalb der aktuellen Gültigkeitsbereiche verarbeitet.
Man beachte auch, dass der Mechanismus Ausnahmebehandlung“ und nicht Fehlerbehand-
” ”
lung“ genannt wird. Die Ausnahmen müssen nicht unbedingt Fehler sein, und nicht jeder Fehler
ist eine Ausnahmesituation (bei Benutzereingaben sind Fehler sogar eher die Regel). Der Mecha-
nismus kann und sollte dazu verwendet werden, in außergewöhnlichen Situationen, die nicht der
Bandbreite eines normalen“ Programmablaufs entsprechen, geordnet und flexibel zu reagieren.

Fehlerhafte Benutzereingaben gehören eher nicht dazu.

3.6.2 Das Konzept der Ausnahmebehandlung


Ausnahmen werden in C++ nach folgendem Konzept verarbeitet:
 Wenn bei der Implementierung einer Funktion eine außergewöhnliche Situation auftritt, wird
dies der Aufrufumgebung mit einer speziellen Anweisung mitgeteilt. Es wird sozusagen vom
normalen Datenfluss auf die Ausnahmebehandlung umgeschaltet.
Diese Ausnahmebehandlung sieht so aus, dass nach und nach alle aufgerufenen Blöcke
bzw. Funktionen verlassen werden, bis ein Block gefunden wird, in dem es für die Ausnahme
eine Behandlungsmöglichkeit gibt.
 Bei der Durchführung von Anweisungen kann definiert werden, was passieren soll, wenn
durch die Anweisungen oder innerhalb darin aufgerufener Funktionen eine Ausnahmesitua-
tion auftritt. Ein eigener Block von Anweisungen kann dann festlegen, wie mit der Situation
umgegangen wird.

Ausnahmeklassen
Bei der Ausnahmebehandlung wird konsequenterweise ein objektorientierter Ansatz verwendet:
 Ausnahmen werden als Objekte betrachtet. Tritt eine Ausnahme auf, wird ein entsprechendes
Objekt erzeugt, das die Ausnahmesituation beschreibt.
 Ausnahmeklassen definieren die Eigenschaften dieser Objekte. Als Attribute können erläu-
ternde Eigenschaften einer Ausnahme (etwa ein fehlerhafter Index oder eine erläuternde Mel-
dung) definiert werden. Operationen können dazu dienen, weitere Informationen zu einer
Ausnahme abzufragen. Für verschiedene Arten von Ausnahmen gibt es entsprechend auch
Sandini Bib

3.6 Ausnahmebehandlung 101

verschiedene Klassen. Jede Ausnahmeklasse beschreibt also eine bestimmte Art von Aus-
nahmesituationen.
Tritt eine Ausnahme auf, wird einfach ein Objekt der entsprechenden Ausnahmeklasse erzeugt.
Diese Objekte können, wie alle Objekte, auch Komponenten besitzen, die die Ausnahmesitua-
tion oder die Umstände, unter denen sie auftrat, beschreiben. Es handelt sich sozusagen um die
Parameter der Ausnahme.
Durch Vererbung können sogar Hierarchien von Ausnahmeklassen gebildet werden. Damit
ist es z.B. möglich, eine Ausnahmeklasse für mathematische Fehler“ noch in speziellere Klas-

sen für Division durch 0“ oder Wurzel aus negativer Zahl“ zu unterteilen. Ein Anwendungs-
” ”
programm besitzt dann die Freiheit, mathematische Fehler im Allgemeinen oder speziell eine
Division durch 0 als Fehler behandeln zu können. Dies kann auch wieder situationsabhängig
erfolgen.

Schlüsselwörter
Für die Ausnahmebehandlung wurden drei Schlüsselwörter eingeführt.
 throw dient dazu, ein entsprechendes Ausnahmeobjekt in die Programmumgebung zu wer-
fen, wenn eine Ausnahmesituation auftritt. Damit wird vom normalen Datenfluss auf die Aus-
nahmebehandlung umgeschaltet.
 catch dient dazu, ein solches Ausnahmeobjekt abzufangen und auf die Ausnahmesituation zu
reagieren. Damit wird also implementiert, was passieren soll, wenn der normale Datenfluss
nicht funktioniert hat.
 try dient dazu, einen Gültigkeitsbereich anzugeben, innerhalb dessen etwaige Ausnahmen
mit catch abgefangen und behandelt werden. Es wird versucht, die darin aufgeführten An-
weisungen fehlerfrei durchzuführen. Mit try wird also der Gültigkeitsbereich für eine Aus-
nahmebehandlung definiert.

3.6.3 Standard-Ausnahmeklassen
Wie schon erläutert, werden Ausnahmen durch Objekte repräsentiert. Die dazugehörigen Da-
tentypen müssen von den Klassen oder Bibliotheken definiert werden, die diese Ausnahmen
auslösen. In C++ wird dabei eine Reihe von Standard-Ausnahmeklassen definiert, die in Abbil-
dung 3.11 dargestellt werden.
Alle Ausnahmeklassen besitzen die gemeinsame Basisklasse std::exception. Dies ist ein
Datentyp, der stellvertretend für alle Standard-Ausnahmen verwendet werden kann. Für Objekte
dieser Klassen wird nur eine Operation definiert: Man kann mit what() eine implementierungs-
spezifische Meldung abfragen.

3.6.4 Ausnahmebehandlung am Beispiel


Zur Behandlung einer Ausnahme muss der Bereich, in dem auf Ausnahmen reagiert werden soll
mit einem Try-Block umschlossen werden. Die Reaktion auf die Ausnahmen wird dann direkt
hinter dem Try-Block durch einen oder mehrere Catch-Blöcke definiert. Das folgende Beispiel
zeigt, wie dies konkret aussehen kann:
Sandini Bib

102 Kapitel 3: Grundkonzepte von C++-Programmen

b a d _ a l l o c
d o m a i n _ e r r o r
b a d _ c a s t
i n v a l i d _ a r g u m e n t
b a d _ t y p e i d
l e n g t h _ e r r o r

l o g i c _ e r r o r
o u t _ o f _ r a n g e
e x c e p t i o n

i o s _ b a s e : : f a i l u r e
r a n g e _ e r r o r

r u n t i m e _ e r r o r o v e r f l o w _ e r r o r

u n d e r f l o w _ e r r o r
b a d _ e x c e p t i o n

Abbildung 3.11: Hierarchie der Standard-Ausnahmeklassen

// allg/ehbsp1.cpp
#include <iostream> // Headerdatei für I/O
#include <string> // Headerdatei für Strings
#include <cstdlib> // Headerdatei für EXIT_FAILURE
#include <exception> // Headerdatei für Ausnahmen

int main()
{
try {
// zwei Strings anlegen
std::string vorname("bjarne"); // kann std::bad_alloc auslösen
std::string nachname("stroustrup"); // kann std::bad_alloc auslösen
std::string name;

// Strings manipulieren
vorname.at(20) = 'B'; // löst std::out_of_range aus
nachname[30] = 'S'; // FEHLER: undefiniertes Verhalten

// Strings verketten
name = vorname + " " + nachname; // könnte std::bad_alloc auslösen
}
Sandini Bib

3.6 Ausnahmebehandlung 103

catch (const std::bad_alloc& e) {


// Spezielle Ausnahme: Speicherplatz alle
std::cerr << "Speicherplatz alle" << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}
catch (const std::exception& e) {
// sonstige Standard-Ausnahmen
std::cerr << "Standard-Exception: " << e.what() << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}
catch (...) {
// alle (bisher nicht behandelten) Ausnahmen
std::cerr << "unerwartete sonstige Exception" << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}

std::cout << "OK, bisher ging alles gut" << std::endl;


}
Durch try wird ein Bereich definiert, für den eine gemeinsame Ausnahmebehandlung definiert
wird. Es wird sozusagen versucht“, die im Try-Block befindlichen Anweisungen durchzuführen:

try {
// zwei Strings anlegen
std::string vorname("bjarne"); // könnte std::bad_alloc auslösen
std::string nachname("stroustrup"); // könnte std::bad_alloc auslösen
std::string name;

// Strings manipulieren
vorname.at(20) = 'B'; // löst std::out_of_range aus
nachname[30] = 'S'; // FEHLER: undefiniertes Verhalten

// Strings verketten
name = vorname + " " + nachname; // könnte std::bad_alloc auslösen
}
Tritt eine Ausnahme auf, wird der gesamte Try-Block sofort verlassen. Wird hier im Beispiel in
einer der Deklarationen eine Ausnahme ausgelöst, werden die nachfolgenden Anweisungen also
nicht mehr durchgeführt. In diesem Beispiel wird der Try-Block deshalb spätestens beim Aufruf
von
vorname.at(20) = 'B';
Sandini Bib

104 Kapitel 3: Grundkonzepte von C++-Programmen

verlassen, da hier versucht wird, bei vorname auf das Zeichen mit dem Index 20 zuzugreifen,
und es dieses Zeichen nicht gibt. Im Gegensatz zum Indexoperator prüft at() bei Strings, ob der
Index korrekt ist, und löst gegebenenfalls eine entsprechende Ausnahme aus. Beim Indexope-
rator wird auf die Prüfung aus Performance-Gründen verzichtet (die nachfolgende Zeile würde
also keine Ausnahme auslösen, sondern zu einem Absturz oder einem anderen undefinierten
Verhalten führen).
Wie die Reaktion auf eine Ausnahme aussieht, wird durch die Catch-Blöcke definiert, die
dem Try-Block folgen. In diesem Fall gibt es drei Catch-Blöcke:
catch (const std::bad_alloc& e) {
// Spezielle Ausnahme: Speicherplatz alle
std::cerr << "Speicherplatz alle" << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}
catch (const std::exception& e) {
// sonstige Standard-Ausnahmen
std::cerr << "Standard-Exception: " << e.what() << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}
catch (...) {
// alle (bisher nicht behandelten) Ausnahmen
std::cerr << "unerwartete sonstige Exception" << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}
Damit werden nacheinander Reaktionen auf Ausnahmen vom Typ std::bad_alloc (sie kenn-
zeichnen Speicherplatzmangel), auf Ausnahmen vom Typ std::exception (sie kennzeichnen
alle Standard-Ausnahmen) und auf alle sonstigen Ausnahmen definiert. Der letzte Catch-Block
zeigt die spezielle Möglichkeit, auf beliebige Ausnahmen zu reagieren:
catch (...) {
// alle (bisher nicht behandelten) Ausnahmen
...
...
}
Eine Folge von drei Punkten in einer Catch-Anweisung steht also für jede beliebige Ausnahme“.

Da man über die Art der Ausnahme in dem Fall keinerlei Aussage machen kann, kann man hier
nur ganz generell reagieren.
Diese Reihenfolge der Catch-Blöcke ist kein Zufall. Bei einer Ausnahme in einem Try-Block
werden die nachfolgenden Catch-Blöcke in der Reihenfolge, in der sie angegeben werden, nach
einer passenden Behandlung durchsucht. Die Anweisungen des ersten passenden Catch-Blocks
werden durchgeführt. Die Reihenfolge der Catch-Blöcke muss also so definiert sein, dass spe-
zielle vor allgemeinen Ausnahmeklassen stehen. Ein Catch-Block für beliebige Ausnahmen, der
durch
Sandini Bib

3.6 Ausnahmebehandlung 105

catch (...) {
...
}
definiert wird, sollte also immer der letzte Catch-Block sein.
Innerhalb eines Catch-Blocks können beliebige Anweisungen stehen. In diesem Fall wird
z.B. jeweils eine Fehlermeldung auf dem Standard-Fehlerausgabekanal cerr ausgegeben und
die Funktion main() durch return mit einem Fehlerstatus verlassen:
std::cerr << ... << std::endl;
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
Die in <cstdlib definierte Konstante EXIT_FAILURE dient dazu, als Rückgabewert von main()
einen fehlerhaften Programmablauf zu kennzeichnen (siehe Abschnitt 3.9.3).
Sofern ein Ausnahmeobjekt als Parameter definiert ist, kann insbesondere auf dieses Objekt
zugegriffen werden, um weitere Informationen zur Ausnahmesituation zu erhalten. Bei Standard-
Ausnahmen ist allerdings lediglich ein Aufruf von what() möglich:
catch (const std::exception& e) {
... e.what() ...
}
Der Rückgabewert von what() ist ein implementierungsspezifischer String. Da in diesem Bei-
spiel eine Ausnahme durch den Aufruf von at() ausgelöst wird und es sich dabei um eine
Standard-Ausnahme vom Typ std::out_of_range handelt, wird genau dieser zweite Block
als passende Ausnahmebehandlung gefunden. Die dazu implementierungsspezifische Ausgabe
könnte z.B. wie folgt aussehen:
Standard-Exception: pos >= length ()
Sofern die Funktion nicht im Catch-Block verlassen wird, wird das Programm nach der Behand-
lung der Ausnahme hinter dem letzten Catch-Block fortgesetzt. Ohne die Return-Anweisungen
würde in diesem Beispiel also bei einer Ausnahme noch die folgende Ausgabeanweisung aufge-
rufen werden.
Man beachte, dass die Ausnahmeobjekte im Catch-Block in der Form const typ&“ über-

geben werden sollten. Dabei handelt es sich um eine so genannte konstante Referenz, die si-
cherstellt, dass von dem Ausnahmeobjekt keine unnötige Kopie angelegt wird. In Abschnitt 4.4
werden die dazugehörigen Sprachmittel im Detail erläutert.
Wie schon angesprochen, werden alle zwischen Erkennung und Behandlung liegenden Blöcke
verlassen, und deren angelegten lokalen Objekte zerstört. Handelt es sich um Objekte von Klas-
sen, für die Operationen zum Aufräumen definiert sind (so genannte Destruktoren), werden diese
auch aufgerufen.
Das folgende Beispiel verdeutlicht das Szenario noch einmal:
// allg/ehbsp2.cpp
char f1 (const std::string s, int idx)
{
std::string tmp = s; // lokales Objekt, das bei einer Ausnahme
Sandini Bib

106 Kapitel 3: Grundkonzepte von C++-Programmen

... // automatisch zerstört wird


char c = s.at(idx); // könnte Ausnahme auslösen
...
return c;
}

void foo()
{
try {
std::string s("hallo"); // wird bei einer Ausnahme zerstört
f1(s,11); // löst eine Ausnahme aus
f2(); // wird bei einer Ausnahme in f1() nicht aufgerufen
}
catch (...) {
std::cerr << "Exception, wir machen aber weiter" << std::endl;
}

// hier geht es nach der Ausnahme in f1() weiter


...
}
Aus foo() heraus wird f1() mit dem String "hallo" und dem Index 11 aufgerufen. In f1()
löst dies beim Aufruf von at() eine Ausnahme aus. In dem Fall wird f1() sofort verlassen,
wobei das lokale Objekt tmp aufgeräumt wird. Auch der Try-Block von foo() wird sofort ver-
lassen. f2() wird auf jeden Fall nicht mehr aufgerufen. Existiert ein passender Catch-Block,
werden dessen Anweisungen durchgeführt, und dann wird hinter allen Catch-Blöcken fortgefah-
ren. Existiert kein passender Catch-Block, wird auch foo() sofort verlassen. Dies geschieht so
lange, bis ein passender Catch-Block gefunden wird.

3.6.5 Behandlung nicht abgefangener Ausnahmen


In einem kommerziellen Programm sollte es nicht passieren, dass eine Ausnahme nicht abge-
fangen wird. Falls es doch vorkommt, führt eine nicht abgefangene Ausnahme im Allgemei-
nen zu einem außergewöhnlichen Programmabbruch. Konkret wird in diesem Fall die Funktion
std::terminate() aufgerufen, die wiederum std::abort() aufruft.
Die Funktion std::abort() veranlasst einen außergewöhnlichen Programmabbruch in der
Art eines Notfalls. Typischerweise werden in Abhängigkeit vom Betriebssystem Laufzeitinfor-
mationen wie etwa ein Speicherabzug (Core-Dump) ausgegeben (siehe auch Abschnitt 3.9.3).
Um zu verhindern, dass std::terminate() die Funktion std::abort() aufruft, kann mit
der Funktion std::set_terminate() eine alternative Terminate-Funktion definiert werden.
Die bisherige Terminate-Funktion wird jeweils zurückgeliefert. Eine mit set_terminate()
übergebene Terminate-Funktion darf weder Parameter noch einen Rückgabewert besitzen.
Sandini Bib

3.6 Ausnahmebehandlung 107

3.6.6 Hilfsfunktionen zur Behandlung von Ausnahmen


In der Praxis ist es häufig sinnvoll, Ausnahmen an verschiedenen Stellen auf die gleiche Art
und Weise zu behandeln. Nun ist es aber recht mühsam, jedes Mal die gleichen Catch-Blöcke
zu implementieren. Stattdessen sollte man die Behandlung der Ausnahmen in eine Hilfsfunktion
auslagern. Es gibt nur ein Problem: Wie übergibt man die Ausnahmen, die behandelt werden
sollen, an diese Funktion? Das Problem ist, dass es keinen gemeinsamen Datentyp für alle Aus-
nahmen gibt.
An dieser Stelle hilft ein Trick, der wie folgt aussieht:
// allg/ehbsp3.cpp
#include <iostream> // Headerdatei für I/O
#include <string> // Headerdatei für Strings
#include <cstdlib> // Headerdatei für EXIT_FAILURE
#include <exception> // Headerdatei für Ausnahmen

void processException()
{
try {
throw; // zu behandelnde Ausnahme nochmal auslösen, damit sie
// hier ausgewertet werden kann
}
catch (const std::bad_alloc& e) {
// Spezielle Ausnahme: Speicherplatz alle
std::cerr << "Speicherplatz alle" << std::endl;
}
catch (const std::exception& e) {
// sonstige Standard-Ausnahmen
std::cerr << "Standard-Exception: " << e.what() << std::endl;
}
catch (...) {
// alle (bisher nicht behandelten) Ausnahmen
std::cerr << "unerwartete sonstige Ausnahme" << std::endl;
}
}

int main()
{
try {
// zwei Strings anlegen
std::string vorname("bjarne"); // kann std::bad_alloc auslösen
std::string nachname("stroustrup"); // kann std::bad_alloc auslösen
std::string name;
Sandini Bib

108 Kapitel 3: Grundkonzepte von C++-Programmen

// Strings manipulieren
vorname.at(20) = 'B'; // löst std::out_of_range aus
nachname[30] = 'S'; // FEHLER: undefiniertes Verhalten

// Strings verketten
name = vorname + " " + nachname; // könnte std::bad_alloc auslösen
}
catch (...) {
// alle Ausnahmen in Hilfsfunktion behandeln
processException();
return EXIT_FAILURE; // main() mit Fehlerstatus beenden
}

std::cout << "OK, bisher ging alles gut" << std::endl;


}
Für alle Ausnahmen wird ein gemeinsamer Catch-Block definiert, der zur eigentlichen Behand-
lung dieser Ausnahme die Hilfsfunktion processException() aufruft:
try {
...
}
catch (...) {
...
processException();
...
}
Innerhalb von processException() wird in einem Try-Block throw ohne Parameter aufgeru-
fen. Dies hat zur Folge, dass die Ausnahme, die gerade behandelt wird, erneut ausgelöst wird.
Damit werden wiederum alle Blöcke verlassen, bis eine passende Catch-Anweisung gefunden
wird. Durch die nachfolgenden Catch-Blöcke ist dies noch innerhalb von processException()
der Fall.

3.6.7 Zusammenfassung
 Ausnahmebehandlung (Exception-Handling) ist ein Sprachmittel zur Behandlung von Feh-
lern und Ausnahmen, die im Programmverlauf auftreten.
 Sie bietet die Möglichkeit, den normalen Datenfluss von der Fehlerbehandlung zu trennen.
 Da bei diesem Mechanismus keine Parameter oder Rückgabewerte von Funktionen verwen-
det werden, kann das Konzept insbesondere zur Fehlerbehandlung beim Erzeugen von Ob-
jekten eingesetzt werden.
 Ausnahmen sind Objekte, deren Datentyp in entsprechenden Klassen definiert wird.
Sandini Bib

3.6 Ausnahmebehandlung 109

 Wird eine Ausnahme ausgelöst, wird ein entsprechendes Objekt erzeugt, und es werden alle
übergeordneten Blöcke geordnet verlassen, bis das Objekt zur Ausnahmebehandlung abge-
fangen und damit die Ausnahme behandelt wird.
 Wird eine Ausnahme nicht behandelt, ist das ein Programmfehler, der mit einem außer-
gewöhnlichen Abbruch durch std::terminate() und std::abort() behandelt wird.
 In C++ werden diverse Standard-Ausnahmeklassen definiert. Für diese Ausnahmeobjekte lie-
fert what() einen implementierungsspezifischen String.
 Mit throw kann man Ausnahmen, die gerade behandelt werden, erneut auslösen. Damit kann
man allgemeine Funktionen zur Ausnahmebehandlung implementieren.
 Ausnahmen sollten im Catch-Block immer in der Form const typ&“ (als konstante Refe-

renz) deklariert werden.
 Wird mit dem Indexoperator auf Strings oder Vektoren zugegriffen, muss man sicherstellen,
dass der Index gültig ist. Verwendet man at() zum Zugriff, wird bei einem ungültigen Index
eine Ausnahme ausgelöst.
Sandini Bib

110 Kapitel 3: Grundkonzepte von C++-Programmen

3.7 Zeiger, Felder und C-Strings


Dieses Kapitel stellt die Elemente von C und C++ vor, die maßgeblich dazu beigetragen haben,
dass C von einigen als komplizierte Sprache eingestuft wird. Nur wer Zeiger und Felder (Arrays)
verstanden hat, wird erfolgreich in C programmieren können.
In C++ werden diese Sprachmittel nach wie vor angeboten. Vor allem in der Standardbiblio-
thek gibt es aber etliche Komponenten, die die Verwendung von Zeigern und Feldern überflüssig
machen. Durch eine geschickte Programmierung werden die Low-level“-Aspekte von Zeigern

und Feldern so gekapselt, dass neue höhere Datentypen entstehen, für deren Verwendung man
nichts über Zeiger und Felder wissen muss.
Das Thema Strings ist ein klassisches Beispiel dafür: In C sind Strings Felder von Zeichen,
die mit Zeigern verwaltet werden. Das schafft entsprechende Probleme. In C++ gibt es den
Datentyp string, der diese Probleme kapselt und eine intuitive Programmierung mit Strings
ermöglicht (siehe Abschnitt 3.4).
Insofern können alle, die die in diesem Kapitel angesprochenen Themen auf die Schnelle
nicht verstehen, beruhigt sein. Falls wir im weiteren Verlauf auf Themen dieses Kapitels zurück-
kommen, wird noch einmal darauf eingegangen.

3.7.1 Zeiger
Wesentlich für das Verständnis von C ist das Verständnis von Zeigern (auch Pointer genannt).
Zeiger sind Verweise auf Daten, die an einer anderen Stelle stehen und auf die sozusagen ge-

zeigt“ wird. Physikalisch handelt es sich einfach nur um Adressen von anderen Variablen.
Zur Verwaltung von Zeigern dienen die Operatoren & und *:
 Der einstellige Operator & liefert die Adresse von einem Datum bzw. einen Zeiger auf ein
Datum.
 Der einstellige Operator * dereferenziert einen Zeiger, was bedeutet, dass er das liefert, wor-
auf der Zeiger zeigt, bzw. das, was an seiner Adresse steht.
Beispiel:
 Nach den folgenden Deklarationen
int x = 42; // int
int* xp; // Zeiger auf int
existieren eine Variable x mit dem initialen Wert 42 und ein Zeiger xp mit undefiniertem
Wert:

x : x p :
4 2 ?
Sandini Bib

3.7 Zeiger, Felder und C-Strings 111

 Mit der Anweisung


xp = &x; // xp zeigt nun auf x
wird xp die Adresse von x zugewiesen. Damit verweist xp auf x:

x : x p :
4 2

 Damit steht der Ausdruck *xp für x, da er das liefert, worauf xp zeigt. Über diesen Zeiger
kann x zum Beispiel einen neuen Wert erhalten:
*xp = 7; // das, worauf xp zeigt, also x, erhält den Wert 7
Damit ergibt sich folgender Zustand:

x : x p :
7

 Entsprechend könnte man den Wert von x mit Hilfe von xp ausgeben:
std::cout << *xp << std::endl; // das, worauf xp zeigt, ausgeben

Deklaration von Zeigern


Zeiger werden deklariert, indem ihrem Namen jeweils ein Stern vorangestellt wird. Da C++ eine
formatfreie Sprache ist, gibt es in der Praxis verschiedene Möglichkeiten, Zeiger zu definieren.
Die folgenden Möglichkeiten sind alle äquivalent:
int *xp; // K&R-Notation
int * xp;
int*xp;
int* xp; // meine typische Notation
In C wird meistens die erste Form verwendet, die schon durch die Autoren von C, Kernighan und
Ritchie, eingeführt wurde.
Ich bevorzuge allerdings wie viele im C++-Umfeld die letztere Notation, was den einen oder
anderen Leser vielleicht am Anfang etwas verwirren kann. Sie hat vor allem den Vorteil, dass
der Typ (Zeiger auf int) klar vom Variablennamen (xp) getrennt wird. Die Notation hat aber
auch einen schwerwiegenden Nachteil: Sie darf nie zur Deklaration mehrerer Zeiger verwendet
werden, da bei der Deklaration
int* p1, p2; // NEIN, keine zwei Zeiger, deshalb vermeiden!
der Stern nur zur ersten Variablen gehört. p1 wird also als Zeiger auf einen int, p2 aber als int
deklariert, was durch die K&R-Schreibweise besser deutlich wird:
int *p1, p2; // Zeiger und einfache Variable
Sandini Bib

112 Kapitel 3: Grundkonzepte von C++-Programmen

Die Konstante NULL


Die spezielle Konstante NULL steht für einen Zeiger, der nirgendwohin zeigt. Das ist im Gegen-
satz zu einem nicht initialisierten Zeiger, der irgendwohin zeigt, ein definierter Zustand. NULL
kann man zuweisen und abfragen:
int* xp = NULL; // xp ist ein Zeiger, der nirgendwohin zeigt
...
if (xp != NULL) { // zeigt xp irgendwohin?
...
}
Hinter NULL steckt eigentlich der Wert 0, mit dem die Konstante in etlichen Standard-Header-
dateien definiert wird. 0 ist der einzige ganzzahlige Wert, der Zeigern zugewiesen werden darf.
Man kann statt NULL also einfach den Wert 0 verwenden:
int* xp = 0; // xp ist Zeiger der nirgendwohin zeigt
...
if (xp != 0) { // zeigt xp irgendwohin?
...
}
Da 0 dem Wert false entspricht und jeder andere ganzzahlige Wert true entspricht, kann man
Zeiger auch logisch testen:
if (xp) { // zeigt xp irgendwohin?
...
}
...
if (!xp) { // zeigt xp nirgendwohin?
...
}

3.7.2 Felder
Ein Feld (Array) ist eine Menge mehrerer Elemente gleichen Typs, die hintereinander angeord-
net werden. Beim Anlegen eines Feldes muss die Anzahl der Elemente angegeben werden. Der
Zugriff erfolgt mit dem Operator [ ]:
int werte[10]; // Feld von zehn ints

werte[0] = 77; // erstes Element initialisieren


werte[9] = werte[0]; // letztes Element erhält Wert vom ersten Element

for (int i=0; i<10; ++i) { // alle Werte initialisierten


werte[i] = 42;
}
Sandini Bib

3.7 Zeiger, Felder und C-Strings 113

Der Index-Bereich geht immer von 0 bis größe-1. Ein Programmierer muss dabei selbst darauf
achten, dass auf Felder nicht mit einem unerlaubten Index zugegriffen wird. Im Programm wird
es aus Performance-Gründen nicht geprüft. Das Ergebnis eines unerlaubten Zugriffs ist nicht
definiert und hängt vom Zufall ab.

Felder und Zeiger


Man kann auf Felder über Zeiger zugreifen. In dieser Möglichkeit spiegelt sich der Sachverhalt
wider, dass eine Feld-Variable intern im Programm eigentlich ein Zeiger auf das erste Element
ist (also die Adresse des ersten Elements in dem Feld enthält). Durch die Deklaration
int werte[10]; // Feld von zehn ints
wird also der in Abbildung 3.12 dargestellte Zustand aufgebaut.

w e r t e :

Abbildung 3.12: Zustand nach Anlegen des Feldes werte[ ]

Insofern kann man auf Felder auch mit Zeigerschreibweisen zugreifen:


int* wp = werte; // entspricht: wp = &werte[0]
*werte = 88; // verändert werte[0]
Durch die Zuweisung von werte an wp wird wp die Adresse des ersten Elements im Feld zuge-
wiesen. wp zeigt damit auf werte[0]. Durch die Zuweisung von 88 an das, worauf werte zeigt,
wird werte[0] also 88 zugewiesen.

Zeigerarithmetik
Die Analogie zwischen Feldern und Zeigern geht sogar so weit, dass man Zeiger über alle Ele-
mente eines Feldes wandern lassen kann. Dazu ist es möglich, für Zeiger arithmetische Opera-
tionen aufzurufen:
 Addiert man zu einem Zeiger einen ganzzahligen Wert n, wird der Zeiger n Elemente weiter-
gesetzt. Entsprechend setzt der Operator ++ einen Zeiger ein Element weiter.
 Bildet man die Summe aus einem Zeiger und einem ganzzahligen Wert n, erhält man einen
Zeiger auf den n-ten Wert hinter dem Zeiger.
 Subtrahiert man zwei Zeiger, erhält man den Abstand der Elemente, auf die sie zeigen.
Damit kann man folgende Schleife schreiben, die alle Elemente eines Feldes ausgibt:
int werte[10]; // Feld von zehn ints
Sandini Bib

114 Kapitel 3: Grundkonzepte von C++-Programmen

for (int* p=werte; p<werte+10; ++p) {


std::cout << *p << std::endl;
}
Am Anfang wird p als Zeiger auf das erste Element von werte initialisiert. Solange dieser Zeiger
kleiner als ein um zehn Elemente weiter gesetzter Zeiger ist, wird auf das aktuelle Element mit
*p zugegriffen und der Zeiger mit ++p jeweils ein Element weiter gesetzt (siehe Abbildung 3.13).

w e r t e : w e r t e + 1 0 :

. . .
p :

Abbildung 3.13: Über das Feld werte[ ] wandernder Zeiger p

Bildet man innerhalb der Schleife die Differenz von p und werte , bekommt man den Abstand
der Elemente, was dem aktuellen Index entspricht:
std::cout << "Index: " << p-werte
<< " Wert: " << *p << std::endl;
Durch diese Regeln ist es absolut gleichwertig, ob man auf Elemente eines Feldes mit dem Inde-
xoperator oder mit der Zeiger-Schreibweise zugreift Statt
werte[5]
kann man z.B. auch mit
*(werte+5)
auf das sechste Element des Feldes werte zugreifen.

Zeiger und Iteratoren


Kommt Ihnen die Art und Weise, wie Zeiger über Felder wandern können, bekannt vor? Rich-
tig, das Interface entspricht dem Iterator-Interface (siehe Seite 78). Dies ist kein Zufall. Beim
Design der STL hat man das Verhalten von Feldern und Zeigern abstrahiert: Mit dem gleichen
Interface, mit dem Zeiger auf Felder zugreifen können, können Iteratoren auf Container zugrei-
fen. Man kann Iteratoren deshalb auch als schlaue Zeiger“ (englisch: smart pointer“, siehe
” ”
Abschnitt 9.2.1) bezeichnen. Sie verhalten sich nach außen wie Zeiger, sind aber so intelligent,
die Operationen passend für die Datenstruktur, auf der sie operieren, umzusetzen.
Sandini Bib

3.7 Zeiger, Felder und C-Strings 115

3.7.3 C-Strings
Die Standardtypen von C++ für Strings kapseln die von C übernommene Art und Weise, mit
Strings zu operieren. In C werden Strings als Felder von Zeichen verwaltet. Als Endekennung
besitzen C-Strings das Zeichen '\0' (dahinter steckt auch wieder der Wert 0). Die Länge eines
Strings ist also die Anzahl der Zeichen bis zum Auftreten von '\0'.
Entsprechend sind auch in C++ String-Literale Felder von Zeichen, die mit '\0' abgeschlos-
sen werden. Die Stringkonstante
"hallo"
wird intern wie in Abbildung 3.14 dargestellt abgelegt.

" h a l l o " :

' h ' ' a ' ' l ' ' l ' ' o ' ' \ 0 '

Abbildung 3.14: Interne Repräsentation des String-Literals "hallo"

Entsprechend ist der Datentyp eines String-Literals const char*, also ein Zeiger auf nicht
änderbare Zeichen. Als Konzession an die Tatsache, dass viele C-Funktionen const nicht ver-
wenden, darf man ein String-Literal auch einfach als char* verwenden. Ein Ändern der Zeichen
ist aber auch in diesem Fall nicht erlaubt.
Wenn man in C++ Strings verwendet, sieht man von dieser Tatsache nichts mehr, da der
Datentyp string die Tatsache kapselt, dass es sich eigentlich um Felder von Zeichen handelt.
Durch die Deklaration:
std::string s = "hallo";
wird genau genommen ein Objekt des Standarddatentyps string mit einem Wert vom Datentyp
const char* initialisiert. Statt dessen kann man deshalb auch folgendes schreiben:
std::string s("hallo");
Bleibt man allerdings beim Datentyp const char*, kann man auf alle Zeichen eines C-Strings
zugreifen, indem man einen Zeiger über diese Zeichen wandern lässt:
// Schleife, die alle Zeichen eines C-Strings s auf einer eigenen Zeile ausgibt
const char* s = "hallo";
for (const char* p=s; *p != '\0'; ++p) {
std::cout << *p << std::endl;
}
Sandini Bib

116 Kapitel 3: Grundkonzepte von C++-Programmen

Am Anfang wird p der C-String s zugewiesen, was bedeutet, dass p auf das erste Zeichen vom
String zeigt. Solange p nicht auf das Stringendezeichen '\0' zeigt, werden jeweils die Anwei-
sungen im Schleifenkörper durchgeführt und der Zeiger wird mit ++p um ein Zeichen weiterge-
setzt.

Problematik von C-Strings


Ohne die Verwendung eines Datentyps, der C-Strings kapselt, wird man mit der gesamten Pro-
blematik von Feldern und Zeigern konfrontiert. Durch die Verwaltung als Feld können Strings
nicht wie fundamentale Datentypen behandelt werden. Wird ein String einem anderen String
mit Hilfe des Zuweisungsoperators zugewiesen, werden nämlich nicht die Elemente, sondern die
Adressen kopiert (Felder sind schließlich eigentlich Zeiger auf das jeweils erste Element, und
diese Zeiger werden einander zugewiesen).
Werden z.B. zwei Strings wie folgt deklariert:
const char* s = "hallo";
const char* t = "Welt";
ergibt sich folgender Zustand:

s :
' h ' ' a ' ' l ' ' l ' ' o ' ' \ 0 '

t :
' W ' ' e ' ' l ' ' t ' ' \ 0 '

Mit der Zuweisung


s = t; // VORSICHT: Zeiger werden zugewiesen
wird daraus:

s :
' h ' ' a ' ' l ' ' l ' ' o ' ' \ 0 '

t :
' W ' ' e ' ' l ' ' t ' ' \ 0 '

Es werden also nicht die Zeichen, sondern nur die Zeiger (Adressen der C-Strings) zugewiesen.
s und t werden damit ein und derselbe C-String.
Um wirklich die C-Strings zu kopieren, müssen die einzelnen Zeichen kopiert werden. Hin-
zu kommt, dass der C-Programmierer immer selbst darauf achten muss, dass genügend Spei-
cherplatz für die Strings vorhanden ist. Aus diesem Grund sehen C-Programme, die mit Strings
operieren, auch immer recht grausam aus. Zum Beispiel:
Sandini Bib

3.7 Zeiger, Felder und C-Strings 117

// allg/cstring.cpp
// C-Headerdatei für I/O
#include <stdio.h>

// C-Headerdatei für die String-Behandlung


#include <string.h>

void f()
{
const char* k = "Eingabe: "; // String-Konstante
char text[81]; // String-Variable für 80 Zeichen
char s[81]; // String-Variable für die Eingabe (bis 80 Zeichen)

/* String s einlesen
* - aus Platzgründen nicht mehr als 80 Zeichen
*/
if (scanf ("%80s", s) != 1) {
// Einlesefehler
...
}

// String mit Leerstring vergleichen


if (strcmp(s,"") == 0) {
/* String-Literal an String text zuweisen
* - VORSICHT: text muss ausreichend groß sein
*/
strcpy (text, "keine Eingabe");
}
else {
/* String-Konstante k, gefolgt von eingelesenem String,
* an text zuweisen
* - VORSICHT: text muss ausreichend groß sein
*/
if (strlen(k)+strlen(s) <= 80) {
strcpy (text, k);
strcat (text, s);
}
}
...
}
Sandini Bib

118 Kapitel 3: Grundkonzepte von C++-Programmen

Die Probleme, die schon dieses kleine Programm aufzeigt, sind allen C-Programmierern bekannt:
 Für eine Zuweisung muss die Funktion strcpy() verwendet werden. Dabei muss der Pro-
grammierer sicherstellen, dass der Speicherplatz des Strings, dem ein neuer Wert zugewiesen
wird, groß genug ist.
 Zum Anhängen eines Strings an einen anderen dient die Funktion strcat(), wobei auch
hier für ausreichend Speicherplatz zu sorgen ist.
 Zum Vergleichen zweier Strings muss die Funktion strcmp() verwendet werden.
 Beim Einlesen eines Strings muss angegeben werden, dass nur für eine bestimmte Anzahl
von Zeichen Platz vorhanden ist.
Die String-Verarbeitung ist in C also nicht nur umständlich, sondern birgt auch erhebliche Ge-
fahren. Man muss für alle Operationen spezielle Funktionen aufrufen (die wichtigsten werden in
Tabelle 3.12 aufgelistet) und sich selbst um die Speicherverwaltung kümmern.

Funktion Bedeutung
strlen() liefert die Anzahl der Zeichen in einem C-String
strcpy() weist einen C-String einem anderen zu
strncpy() weist bis zu n Zeichen eines C-Strings einem anderen zu
strcat() hängt einen C-String an einen anderen an
strncat() hängt bis zu n Zeichen eines C-Strings an einen anderen an
strcmp() vergleicht zwei C-Strings
strncmp() vergleicht bis zu n Zeichen von zwei C-Strings
strchr() sucht in einem C-String ein bestimmtes Zeichen

Tabelle 3.12: Die wichtigsten Funktionen für C-Strings

All diese Probleme sind mit C++ behoben. Durch Verwendung des in Abschnitt 3.4 eingeführten
Datentyps string kann man Standardoperatoren verwenden und braucht sich über die Spei-
cherverwaltung keine Gedanken zu machen. Das obige C-Programm würde in C++ wie folgt
aussehen:
// allg/string2.cpp
#include <iostream> // C++-Headerdatei für I/O
#include <string> // C++-Headerdatei für Strings

int main ()
{
const std::string k = "Eingabe: "; // String-Konstante
std::string text; // String-Variable
std::string s; // String-Variable für Eingabe

// String s einlesen
if (! (std::cin >> s)) {
Sandini Bib

3.7 Zeiger, Felder und C-Strings 119

// Einlesefehler
...
}

// String mit Leerstring vergleichen


if (s == "") {
// String-Literal an String text zuweisen
text = "keine Eingabe";
}
else {
/* String-Konstante k, gefolgt von eingelesenem String,
* an text zuweisen
*/
text = k + s;
}
...
}
Entsprechend kompliziert ist die Verwaltung von dynamischen Feldern in C. Auch hier bietet
C++ den Vorteil, dass die Standardbibliothek mit den STL-Containern Klassen zur Verfügung
stellt, die diese Problematik wegkapseln.

3.7.4 Zusammenfassung
 Zeiger sind Variablen, die auf andere Variablen verweisen. Sie enthalten somit die Adressen
dieser Variablen.
 Zur Operation mit Zeigern dienen die einstelligen Operatoren * und &.
 Hat ein Zeiger den Wert 0 oder NULL, verweist er nirgendwohin. Das ist etwas anderes als ein
nicht initialisierter Zeiger, der irgendwohin zeigt.
 Felder werden mit eckigen Klammern angelegt. Ihr Index-Bereich erstreckt sich von 0 bis
größe-1.
 Der Datentyp einer Feld-Variablen entspricht einem Zeiger auf das erste Element.
 Zeiger können durch arithmetische Operatoren über Felder wandern.
 C-Strings sind Felder von Zeichen.
 Die Problematik von C-Strings wird mit Hilfe des C++-Typs für Strings, string, beseitigt.
Sandini Bib

120 Kapitel 3: Grundkonzepte von C++-Programmen

3.8 Freispeicherverwaltung mit new und delete


Dieser Abschnitt führt in das Konzept der Freispeicherverwaltung in C++ ein. Dabei werden die
in C++ dafür vorhandenen Operatoren new und delete vorgestellt, und ihre Anwendung wird
erläutert. Dazu wird auch auf die Möglichkeit eingegangen, ihr Verhalten zu überwachen und
neu zu definieren.
Normalerweise besitzen Variablen einen klar abgegrenzten Gültigkeitsbereich. Sobald der
durch geschweifte Klammern definierte Block, in dem sie deklariert werden, verlassen wird,
werden die Variablen ungültig. Damit wird der dazugehörige Speicherplatz automatisch freige-
geben, und die darin befindlichen Daten werden ungültig. Handelt es sich bei den Variablen um
komplexe Objekte, werden die dazugehörigen Aufräumarbeiten durchgeführt (steht die Variable
z.B. für eine geöffnete Datei, wird sie automatisch geschlossen).
Mitunter ist es aber notwendig, dass Variablen oder Objekte über Blockgrenzen hinaus gültig
bleiben. Eine globale Variable hilft da nicht unbedingt weiter, da sie zum Programmstart initia-
lisiert wird. Es kann aber sein, dass die Daten zur Initialisierung zu diesem Zeitpunkt noch gar
nicht vorliegen. Es wird also eine Möglichkeit gebraucht, zur Laufzeit explizit Objekte anzu-
legen und später explizit wieder zu zerstören. Dazu gehört insbesondere auch die Möglichkeit,
Speicherplatz für diese Objekte anzufordern und freizugeben.

Die Operatoren zur Freispeicherverwaltung


Für die dynamische Speicherverwaltung und das explizite Erzeugen und Zerstören von Objekten
existieren in C++ die Operatoren new und delete. Mit new wird Speicherplatz angefordert und
ein neues Objekt explizit angelegt; mit delete wird ein solches Objekt zerstört und Speicher-
platz freigegeben.
Die mit new und delete verwalteten Objekte können sowohl zu einer Klasse gehören als
auch einen fundamentalen Datentyp besitzen. Genauso ist es möglich, Felder (Arrays) von Ob-
jekten beliebiger Art anzulegen.

Abgrenzung zu C-Funktionen
Die neuen Operatoren ersetzen die in C bekannten Speicherplatzfunktionen malloc() und
free(). Dies hat mehrere Vorteile:
 Als Operatoren sind new und delete Teil der Sprache C++ und gehören nicht zu einer
Standardumgebung.
 Aus diesem Grund kann der Typ der anzulegenden Objekte als Operand direkt angegeben
werden und muss nicht als Parameter geklammert und mit sizeof versehen werden.
 Außerdem liefert new im Gegensatz zu malloc() immer einen Zeiger auf den richtigen Typ
zurück. Sein Rückgabewert muss also nicht erst explizit in den richtigen Typ umgewandelt
werden.
 Sofern mit new Objekte erzeugt werden, werden diese vollständig initialisiert. Bei der An-
forderung von Speicherplatz für fundamentale Datentypen gilt allerdings weiterhin wie bei
lokalen Variablen, dass der Speicherplatz nicht initialisiert ist.
Sandini Bib

3.8 Freispeicherverwaltung mit new und delete 121

 Der Rückgabewert muss nicht unbedingt auf NULL getestet werden, da für den Fall, dass es
nicht möglich ist, Speicherplatz oder Objekte anzulegen, eine separate Fehlerbehandlung mit
Hilfe von Ausnahmen angestoßen wird.
 Schließlich kann der Operator new für eigene Datentypen selbst implementiert werden. Da-
durch ist eine beliebige Optimierung bei der Speicherverwaltung möglich.
Durch diese Vorteile kann viel Code gespart werden. Aus C-Code wie
personPointer = (Person*) malloc (sizeof(Person));
if (personPointer == NULL) {
// FEHLER
...
}
initPerson(personPointer,"Nicolai","Josuttis");
wird in C++ einfach nur noch:
personPointer = new Person("Nicolai","Josuttis");
Man gibt in C++ also nur noch an, was für ein Objekt man auf dem Freispeicher anlegen will,
und übergibt optional gleich die Parameter zur Initialisierung.

3.8.1 Der Operator new


Mit dem Operator new kann explizit Speicherplatz für ein Objekt eines bestimmten Typs angelegt
werden.
Als Operand hinter new wird einfach der Typ des Objekts angegeben, das explizit angelegt
werden soll. Zurückgeliefert wird ein Zeiger auf dieses Objekt:
float* fp = new float; // legt einen Gleitkommawert an
std::string* sp = new std::string; // legt einen String an
Sofern es sich bei dem Typ um eine Klasse handelt, wird das angelegte Objekt auch gleich
initialisiert (dazu wird ein so genannter Konstruktor der Klasse aufgerufen). Zu diesem Zweck
können in Klammern Argumente übergeben werden:
std::string* sp1;
sp1 = new std::string; // initialisiert den String mit dem Default-Wert
std::string* sp2;
sp2 = new std::string("hallo"); // initialisiert den String mit "hallo"

3.8.2 Der Operator delete


Der Operator delete dient dazu, mit new angeforderten Speicherplatz wieder freizugeben. Als
Operand muss der von new zurückgelieferte Zeiger auf das Objekt (die Adresse des Objekts)
übergeben werden:
float* fp = new float; // Gleitkommawert anlegen
std::string* sp = new std::string; // String anlegen
...
Sandini Bib

122 Kapitel 3: Grundkonzepte von C++-Programmen

delete fp; // Gleitkommawert freigeben


delete sp; // String freigeben

std::string* p1 = new std::string("hallo"); // String anlegen (p1 zeigt darauf)


...
std::string* p2 = p1; // p2 zeigt nun auch darauf
...
delete p2; // String freigeben
Der Aufruf von delete für ein nicht mit new erzeugtes Objekt ist nicht definiert und kann be-
liebig fatale Folgen haben. Insbesondere ist es falsch, einen mit malloc() angelegten Speicher-
platz mit delete bzw. einen mit new angelegten Speicherplatz mit free() freizugeben. Dies
funktioniert zwar manchmal, da new und delete oft über malloc() und free() implementiert
werden – ein solches Programm ist allerdings nicht mehr portabel, da new und delete auch
beliebig anders definiert sein können.
Ein delete für 0 (bzw. NULL) ist erlaubt und hat keinen Effekt. Auf diese Weise kann beim
Aufräumen ein delete für einen mit NULL initialisierten Zeiger aufgerufen werden, unabhängig
davon, ob er in der Zwischenzeit auf mit new angelegten Speicherplatz zeigt:
Person* feld = NULL;
...

if (bedingung-die-vielleicht-erfüllt-ist) {
feld = new Person;
}
...

delete feld; // OK
Man beachte, dass lokale Zeiger ohne explizite Initialisierung keinen definierten Startwert besit-
zen. Ohne die explizite Initialisierung von feld mit NULL wäre das Beispiel also fehlerhaft, da
mit delete bei nicht erfüllter Bedingung für irgendeine in feld stehende Adresse aufgerufen
wird.

3.8.3 Dynamische Speicherverwaltung für Felder


Durch Verwendung von Feld-Klammern können auch Felder von Objekten angelegt und wieder
freigegeben werden.
Beim Anlegen mit new[] wird ein ebenfalls Zeiger auf das erste Objekt zurückgeliefert:
char* s = new char[len+1]; // len+1 chars
std::string* strings = new std::string[42]; // 42 Strings
float** werte = new float*[10]; // 10 Zeiger auf floats
Auch in diesem Fall werden damit angelegte Objekte automatisch initialisiert. Die 42 Strings
werden also alle mit ihrem Default-Wert, dem Leerstring, initialisiert. Fundamentale Datenty-
Sandini Bib

3.8 Freispeicherverwaltung mit new und delete 123

pen (chars, floats usw.) erhalten aber wie üblich einen undefinierten Startwert. Bei mit new
angelegten Feldern ist es nicht möglich, Argumente zur Initialisierung zu übergeben.
Für die Freigabe von Feldern gibt es eine eigene Syntax des Operators delete, die verlangt,
dass ebenfalls Feld-Klammern angegeben werden müssen:
char* s = new char[len+1]; // len+1 chars
std::string* strings = new std::string[42]; // 42 Strings
float** werte = new float*[10]; // 10 Zeiger auf floats
...
delete [] s; // chars freigeben
delete [] strings; // Strings freigeben
delete [] werte; // float-Zeiger freigeben
Ein Programmierer muss selbst darauf achten, ob ein Zeiger auf ein einzelnes Objekt oder ein
Feld von Objekten zeigt, um die richtige Syntax zur Freigabe verwenden zu können:
void freigeben (std::string* sp)
{
// Was ist gemeint?:
delete sp; // bei: sp = new std::string
delete [] sp; // bei: sp = new std::string[10]
}
Wird die falsche Syntax verwendet, so ist der Effekt nicht definiert.
Diese etwas unschöne Eigenschaft der Sprache liegt darin begründet, dass in C++ anhand
des Datentyps nicht zwischen einem Zeiger auf ein Objekt und einem Feld von Objekten un-
terschieden werden kann (eine Folge der Kompatibilität zu C). Die Unterscheidung ist aber we-
sentlich, da unter Umständen verschiedene Operationen aufgerufen werden. Hinter dem Aufruf
von delete bzw. delete[] stecken nämlich unterschiedliche Funktionen, die sogar durch ei-
gene Funktionen ersetzt werden können (darauf wird in Abschnitt 9.3.4 eingegangen). Da der
Compiler also weder anhand des Datentyps noch am Kontext entscheiden kann, ob ein einzel-
nes Objekt oder ein Feld von Objekten zerstört werden soll (new und delete können in völlig
verschiedenen Modulen übersetzt werden), muss dies vom Programmierer angegeben werden.

Mehrdimensionale Felder
Mit new können auch mehrdimensionale Felder angelegt werden: Auch hier wird beim Anlegen
ein Zeiger auf das erste Objekt zurückgeliefert:
std::string(*zweidim)[7] = new std::string[10][7]; // 10*7 Strings
...
delete [] zweidim; // Strings freigeben
Durch den Aufruf von new werden 70 Strings angelegt und jeweils mit dem Default-Wert der
Klasse initialisiert. Die Delete-Anweisung gibt diese Strings wieder frei.
Man beachte, dass durch
std::string(*zweidim)[7] = new std::string[anz][7]; // anz mal (7 Strings)
Sandini Bib

124 Kapitel 3: Grundkonzepte von C++-Programmen

vom Datentyp her ein Zeiger auf anz Elemente vom Typ Feld von sieben Strings“ geliefert wird.

Dies ist nicht das gleiche wie ein Feld von anz mal sieben Strings:
std::string* p = new std::string[anz*7]; // (anz * 7) Strings
Aufgrund der Tatsache, dass jede weitere Dimension Teil des zurückgeliefertyen Datentyps ist,
müssen alle bis auf die erste Dimension als konstante Werte übergeben werden:
new std::string[anz][7]; // OK
new std::string[10][anz] // FEHLER
Wegen der komplizierten Datentypen bei mehrdimensionalen Feldern verwendet man in der Re-
gel eindimensionale Felder und verwaltet die Dimensionen selbst.

Felder ohne Elemente


Es ist erlaubt, mit new Felder mit null Elementen anzulegen. Damit muss für den Fall, dass
dynamische Felder auch leer sein können, keine Sonderbehandlung durchgeführt werden:
int size = 0;
...
// size kann immer noch 0 sein
int* zahlen = new int[size];
...
delete [] zahlen; // OK

3.8.4 Fehlerbehandlung für new


Der Aufruf von new kann prinzipiell immer fehlschlagen, da es zumindest bei herkömmlichen
Multiuser-Betriebssystemen immer passieren kann, dass kein Speicherplatz mehr zur Verfügung
steht. Kann new keinen Speicherplatz bekommen, wird ein so genannter New-Handler aufgeru-
fen. Der Default-New-Handler löst im Rahmen der in C++ üblichen Fehlerbehandlung eine so
genannte Ausnahme vom Typ std::bad_alloc aus.
In dieses Default-Verhalten kann man sich auf vielfältige Art und Weise einmischen:
1. Man kann einen derartigen Fehler im Programm im Rahmen des Konzepts der Ausnahmebe-
handlung abfangen und behandeln. Darauf wird in Abschnitt 3.6.4 eingegangen.
2. Man kann eigene New-Handler installieren, die z.B. zunächst versuchen, weiteren Speicher
anzufordern oder mit einer entsprechenden Warnung den Reserve-Speicher“ anzubrechen.

Darauf wird in Abschnitt 9.3.3 eingegangen.
3. Man kann new so aufrufen, dass im Fehlerfall kein New-Handler aufgerufen wird und statt-
dessen NULL zurückgeliefert wird. Darauf wird in Abschnitt 9.3.1 eingegangen.
4. Man kann den Operator new anders implementieren. Darauf wird in Abschnitt 9.3.4 einge-
gangen.
Man beachte, dass ein Mangel an Speicherplatz in der Regel ein derart fundamentales Problem
darstellt, dass es ohnehin nicht mehr sinnvoll ist, das Programm weiterlaufen zu lassen. Inso-
Sandini Bib

3.8 Freispeicherverwaltung mit new und delete 125

fern wird in der Praxis meistens im Rahmen der Ausnahmebehandlung eine globale Reaktion
definiert, die das Programm mit einer geeigneten Fehlermeldung beendet (siehe z.B. Seite 101).

3.8.5 Zusammenfassung
 Zur Speicherverwaltung wurden in C++ die Operatoren new und delete eingeführt.
 Für Felder müssen new und delete jeweils mit eckigen Klammern aufgerufen werden.
 Die Speicherverwaltung von C++ mit new und delete sollte nie mit den C-Funktionen
malloc(), free() usw. durcheinander gewürfelt werden.
 Kann mit new kein Speicherplatz angelegt werden, wird im Normalfall eine Ausnahme vom
Typ std::bad_alloc ausgelöst. Programme sollten Ausnahmen diesen Typs entsprechend
auswerten.
Sandini Bib

126 Kapitel 3: Grundkonzepte von C++-Programmen

3.9 Kommunikation mit der Außenwelt


Zum Abschluss der Sicht der C++-Anwendungsprogrammierer werden in diesem Kapitel Mög-
lichkeiten vorgestellt, mit der Aufrufumgebung zu kommunizieren. Dazu gehören Argumente
aus dem Programmaufruf, Umgebungsvariablen, das Starten anderer Programme und Rückmel-
dungen mit Exit-Codes.
Auch hierbei handelt es sich um Konzepte und Schnittstellen, die im Wesentlichen von C
übernommen wurden. Deshalb werden dort leider nicht die höheren Datentypen von C++, wie
string, verwendet.

3.9.1 Argumente aus dem Programmaufruf


Die Funktion main() kann auf zweierlei Arten deklariert werden:
 einmal ohne Parameter:
int main() {
...
}

 zum anderen mit zwei Parametern:


int main (int argc, char* argv[]) {
...
}

Im zweiten Fall werden in argv die Argumente aus dem Aufruf des Programms als Feld (Ar-
ray) von C-Strings übergeben. In argc steht die Anzahl der Elemente in diesem Feld. Das Feld
enthält als erstes Element immer den Programmnamen; die weiteren Elemente sind Programm-
Parameter.
Ein einfaches Beispiel für die Auswertung zeigt folgendes Programm:
// allg/argv.cpp
#include <iostream> // C++-Headerdatei für Ein-/Ausgaben
#include <string> // C++-Headerdatei für Strings

int main (int argc, char* argv[])


{
// Programmname und Anzahl der Parameter ausgeben
std::string progname = argv[0];
if (argc > 1) {
std::cout << progname << " hat " << argc-1 << " Parameter: "
<< std::endl;
}
else {
Sandini Bib

3.9 Kommunikation mit der Außenwelt 127

std::cout << progname << " wurde ohne Parameter aufgerufen"


<< std::endl;
}

// Programmparameter ausgeben
for (int i=1; i<argc; ++i) {
std::cout << "argv[" << i << "]: " << argv[i] << std::endl;
}
}
Wird das Programm nur unter dem Namen argv und ohne Parameter aufgerufen, lautet die Aus-
gabe:
argv wurde ohne Parameter aufgerufen
Wird das Programm wie folgt aufgerufen:
argv hallo "zwei Worte" 42
lautet die Ausgabe:
argv hat 3 Parameter:
argv[1]: hallo
argv[2]: zwei Worte
argv[3]: 42

3.9.2 Zugriff auf Umgebungsvariablen


Mit der in <cstdlib> bzw. <stdlib.h> definierten Funktion getenv() kann der Wert von
Umgebungsvariablen abgefragt werden:
#include <cstdlib>

string path;
const char* path_cstr = std::getenv("PATH");
if (path_cstr == NULL) {
...
}
else {
path = path_cstr;
}
Zurückgeliefert wird entweder der Wert der Umgebungsvariablen als C-String oder NULL. Man
beachte, dass man NULL nicht einfach C++-Strings zuweisen kann. Aus diesem Grund muss der
Rückgabewert geprüft werden, bevor er als C++-String verwendet wird.
Sandini Bib

128 Kapitel 3: Grundkonzepte von C++-Programmen

3.9.3 Abbruch von Programmen


Ein C++-Programm wird entweder durch Verlassen von main() oder durch Aufruf einer Funk-
tion zum Abbruch des Programms beendet.
Drei von C übernommene Funktionen, die in <cstdlib> bzw. <stdlib.h> deklariert wer-
den, sind in diesem Zusammenhang wichtig (siehe Tabelle 3.13): Man kann ein C++-Programm
jederzeit durch Aufruf von exit() oder abort() abbrechen. Außerdem kann man mit atexit()
Funktionen registrieren, die bei einem Programmende automatisch aufgerufen werden.

Funktion Bedeutung
exit() bricht ein Programm geordnet“ ab

atexit() installiert eine Funktion, die beim Programmende aufgerufen wird
abort() bricht ein Programm ungeordnet“ ab

Tabelle 3.13: Funktionen zum Programmende

Mit exit() kann man ein C++-Programm zur Laufzeit abbrechen. Es handelt es sich dabei
um kein geordnetes Programmende, sondern um einen Abbruch mitten im Programm. Gewisse
Aufräumarbeiten werden zwar noch durchgeführt (z.B. Ausgabepuffer geleert), jedoch können
z.B. angelegte temporäre Dateien erhalten bleiben. Genau genommen werden alle globalen Ob-
jekte aufgeräumt, lokale Objekte jedoch nicht. Dies kann zu unerwünschten Effekten führen,
weshalb man auch den Aufruf von exit() nach Möglichkeit vermeiden sollte.
exit() erhält als Parameter einen ganzzahligen Wert, der an die Aufrufumgebung durch-
gereicht wird und mit dem angezeigt werden kann, ob das Programm erfolgreich lief (der so
genannte Exit-Code). Diese Werte entsprechen den Rückgabewerten von main(). Wird 0 oder
die Konstante EXIT_SUCCESS übergeben, handelt es sich um ein erfolgreiches“ Programmende.

Die Konstante EXIT_FAILURE kennzeichnet ein nicht erfolgreiches“ Programmende:

#include <cstdlib>

if (massives-problem) {
std::exit(EXIT_FAILURE); // Abbruch mit teilweisem Aufräumen
}
EXIT_SUCCESS und EXIT_FAILURE können auch als Rückgabewerte von main() verwendet
werden. Vor allem die Verwendung von EXIT_FAILURE ist sinnvoll, da damit deutlich gekenn-
zeichnet wird, dass etwas nicht geklappt hat:
int main()
{
...
if (fehler) {
return EXIT_FAILURE; // Programm mit Fehlerstatus beenden
}
...
}
Sandini Bib

3.9 Kommunikation mit der Außenwelt 129

Mit atexit() kann eine Funktion definiert werden, die automatisch unmittelbar vor einem Pro-
grammende durch exit() aufgerufen wird. Dies wird typischerweise dazu genutzt, um ein Pro-
gramm im Fehlerfall geordnet zu beenden. In der installierten Funktion können z.B. geöffnete
Dateien geschlossen, Puffer geleert und Verbindungen zu anderen Prozessen geordnet beendet
werden. Diese Funktionen werden nicht nur bei einer Beendigung mit exit() sondern auch bei
einem regulären Ende von main() aufgerufen.
abort() dient dazu, ein Programm bei einem fatalen Fehler sofort ohne jedes weitere Auf-
räumen abzubrechen. Die genaue Reaktion ist systemspezifisch. Typisch ist das Erzeugen eines
Speicherabzugs (Core-Dumps). abort() wird ohne Parameter aufgerufen:
#include <cstdlib>

if (jede-weitere-operation-ist-nicht-mehr-sinnvoll) {
std::abort(); // Abbruch mit Speicherabzug
}

3.9.4 Aufruf von weiteren Programmen


Mit der in <cstdlib> bzw. <stdlib.h> definierten Funktion system() kann ein anderes Pro-
gramm gestartet werden. Der Name wird als C-String übergeben. Zurückgeliefert wird der Exit-
Status des Programms:
#include <cstdlib>

int status;
if (ist-unix-system) {
status = std::system("ls -l"); // Dateien unter UNIX auflisten
}
else {
status = std::system("dir"); // Dateien unter Windows auflisten
}

if (status == EXIT_SUCCESS) {
...
}
Zurückgeliefert wird der Exit-Code des aufgerufenen Programms bzw. ein Fehler-Code, wenn
das Programm gar nicht aufgerufen werden konnte.

3.9.5 Zusammenfassung
 Einem C++-Programm können Argumente übergeben werden, die als Parameter von main()
ausgewertet werden können.
 Mit getenv() können Werte von Umgebungsvariablen ausgewertet werden.
Sandini Bib

130 Kapitel 3: Grundkonzepte von C++-Programmen

 Mit exit() und abort() können Programme abgebrochen werden. Diese Funktionen soll-
ten im Normalfall vermieden werden.
 Mit system() können weitere Programme aufgerufen werden.
 Hat ein C-String den Wert NULL, kann man ihn nicht einfach einem string zuweisen. Der
Wert muss gesondert behandelt werden.
Sandini Bib

Kapitel 4
Programmieren von Klassen

Dieses Kapitel stellt zwei Grundkonzepte der objektorientierten Programmierung vor: die Pro-
grammierung mit Klassen in Verbindung mit dem Konzept der Datenkapselung.
Anhand von Beispielen werden die verschiedenen Sprachmittel und Programmiertechniken,
die in C++ für die Verwendung von Klassen benötigt werden, nach und nach eingeführt und
erläutert.
Als einführendes Beispiel dient dabei die Klasse Bruch. Diese Klasse beschreibt, wie der
Name schon sagt, Objekte, die Brüche repräsentieren. Es handelt sich bewusst um ein sehr einfa-
ches Objekt, damit bei der Einführung der verschiedenen Sprachmittel nicht das Beispiel selbst
zum Problem wird.

131
Sandini Bib

132 Kapitel 4: Programmieren von Klassen

4.1 Die erste Klasse: Bruch


In diesem Abschnitt wird als erste C++-Klasse die Klasse Bruch implementiert und angewendet.
Die Klasse Bruch bietet dabei ein kleines, überschaubares Beispiel. Der Aufbau eines Bruchs
und der Umgang mit Brüchen kann im Wesentlichen als bekannt vorausgesetzt werden. Dennoch
gibt es einige Fallen, die in den nachfolgenden Versionen eine Verwendung von nichttrivialen
Sprachmitteln erfordern.
Es kann gut sein, dass sich ein Leser, der bereits einiges über C++ weiß, über diese erste
Version wundern wird. Mit fortgeschrittenen Kenntnissen der Sprache würde man die Klasse
sicherlich anders implementieren. Aber alle Eigenschaften der Sprache können unmöglich auf
einmal vorgestellt werden. Im weiteren Verlauf des Buchs wird die Klasse Bruch von Version zu
Version immer weiter verbessert. Einige Programmiertechniken, die hier als Grundlage aufge-
zeigt werden, werden aufgrund der gewonnenen Erkenntnisse dann in Frage gestellt oder durch
andere Mechanismen ersetzt.

4.1.1 Vorüberlegungen zur Implementierung


Wie bei jeder Klasse müssen zur Implementierung der Klasse Bruch zwei Dinge geklärt werden:
 Was soll man mit Objekten der Klasse machen können?
 Welche Daten repräsentieren Objekte dieser Klasse?
Die erste Frage betrifft die Anwendung der Klasse und definiert somit ihre Schnittstelle nach
außen. Sie wird anhand der Anforderungen, die an die Klasse gestellt werden, in einer entspre-
chenden Spezifikation beschrieben.
Die zweite Frage führt zum internen Aufbau der Klasse und ihrer Instanzen. Die Daten
müssen in irgendeiner Form als Attribute verwaltet werden. Diese Frage wird vor allem bei der
Implementierung der Klasse relevant und muss so beantwortet werden, dass sie die Anforderun-
gen, die sich aus der ersten Frage ergeben, möglichst geschickt lösen kann.

Anforderungen an die Klasse Bruch


Für unser erstes Beispiel sollen die Anforderungen an die Klasse Bruch möglichst gering ge-
halten werden. Es soll ja nur das Prinzip verdeutlicht werden. Aus diesem Grund wird nur eine
einzige Operation realisiert:
 das Ausgeben eines Bruchs
In den folgenden Abschnitten kommen dann weitere Operationen hinzu, mit denen Brüche z.B.
multipliziert oder eingelesen werden können.
Eines darf allerdings nicht vergessen werden: Um mit Brüchen eine Operation ausführen zu
können, muss es diese Brüche erst einmal geben. Außerdem sollte ein Bruch, wenn er nicht
mehr gebraucht wird, zerstört werden können. Es werden also noch zwei weitere Operationen
gebraucht:
 Erzeugen (und Initialisieren) eines Bruchs
 Zerstören eines Bruchs
Insgesamt ergibt sich so die in Abbildung 4.1 dargestellte Schnittstelle.
Sandini Bib

4.1 Die erste Klasse: Bruch 133

erzeugen

Bruch ausgeben

zerstören

Abbildung 4.1: Erste Schnittstelle der Klasse Bruch

Aufbau von Brüchen


Bei der Frage nach den Attributen und dem internen Aufbau eines Objekts geht es letztlich dar-
um, ein solches beliebig abstraktes Gebilde direkt oder indirekt auf bereits vorhandene Datenty-
pen zurückzuführen. Der Informationsgehalt und der Zustand des Objekts müssen durch mehrere
einzelne Komponenten, die bereits vorhandene Datentypen besitzen, beschrieben werden.
Dabei ist zunächst einmal die Frage wichtig, wie der Informationsgehalt eines Objekts grund-
sätzlich beschrieben wird. Was repräsentiert das Objekt? Daraus ergeben sich meist schon die
ersten Komponenten, aus denen das Objekt besteht. Im Laufe der Implementierung können dann
weitere Hilfskomponenten hinzukommen, die einen internen Objektzustand widerspiegeln und
dadurch z.B. die Implementierung erleichtern oder Laufzeit einsparen.
Was ein Bruch repräsentiert bzw. woraus er besteht, kann relativ leicht beantwortet werden:
Ein Bruch besteht aus einem Zähler und einem Nenner. Dafür können vorhandene fundamentale
Datentypen wie int verwendet werden. Damit sind die beiden wesentlichen Komponenten eines
Bruchs festgelegt:
 Ein int für den Zähler
 Ein int für den Nenner
Es kann durchaus sein, dass im Rahmen der Implementierung noch weitere Hilfskomponenten
gebraucht werden. So könnte eine Komponente intern z.B. festhalten, ob Zähler und Nenner im
aktuellen Zustand gekürzt werden können. Die für den Informationsgehalt wesentlichen Kompo-
nenten für den Zähler und den Nenner müssen also nicht unbedingt die einzigen Komponenten
bleiben.
Insgesamt ergibt sich der in Abbildung 4.2 dargestellte Aufbau: Ein Bruch besteht aus einem
Zähler und einem Nenner und kann erzeugt, ausgegeben und zerstört werden.
Sandini Bib

134 Kapitel 4: Programmieren von Klassen

erzeugen

Bruch:
zaehler ausgeben
nenner

zerstören

Abbildung 4.2: Schnittstelle und Aufbau der Klasse Bruch

Terminologie
In objektorientierter Lesart sind Zähler und Nenner die Daten oder Attribute, aus denen sich
eine Instanz der Klasse Bruch zusammensetzt. Das Erzeugen, Ausgeben und Zerstören sind die
Methoden, die für die Klassen definiert sind.
In der Lesart von C++ bedeutet das, dass eine Klasse Bruch gebraucht wird, die sich aus den
Komponenten Zähler und Nenner sowie den Funktionen zum Erzeugen, Ausgeben und Zerstören
zusammensetzt. Die Komponenten einer Klasse werden auch Elemente oder Klassenelemente
genannt. Dies alles sind verschiedene Übersetzungen der englischen Bezeichnung member, die
mitunter auch im Deutschen verwendet wird (siehe dazu auch die Anmerkungen zur Namensge-
bung auf Seite 22).
Die Methoden, also die Operationen, die in einer Klasse definiert sind, nennt man in C++
Elementfunktionen. Eine andere weit verbreitete Bezeichnung dafür ist Memberfunktion.
Für die Komponenten, die keine Elementfunktionen sind, gibt es in C++ eigentlich gar kei-
ne richtige Bezeichnung. Man spricht im Englischen im Allgemeinen nur von member und dem
Spezialfall member function. Ich werde sie nachfolgend als Attribute oder Datenelemente be-
zeichnen.
Die Komponenten der Klasse Bruch unterteilen sich also in die Datenelemente Zähler und
Nenner und die Elementfunktionen zum Erzeugen, Ausgeben und Zerstören.

Datenkapselung
Brüche besitzen eine grundsätzliche Eigenschaft: Ihr Nenner darf nicht 0 sein, da durch 0 nicht
geteilt werden darf. Diese Randbedingung soll natürlich auch für die Klasse Bruch gelten.
Wenn aber für alle Anwender unbeschränkter Zugriff auf alle Komponenten der Klasse Bruch
besteht, ist es möglich, den Nenner eines Bruchs auf 0 zu setzen. Aus diesem Grund ist es sinn-
voll, bei der Verwendung eines Bruchs keinen direkten Zugriff auf den Nenner zu ermöglichen.
Sandini Bib

4.1 Die erste Klasse: Bruch 135

Solange der Zugriff nur über Funktionen möglich ist, kann der Versuch, dem Nenner 0 zuzuwei-
sen, mit einer entsprechenden Fehlermeldung quittiert werden.
Generell ist es eine gute Idee, den Zugriff auf Objekte nur über dafür definierte Operatio-
nen zuzulassen. Durch deren Implementierung können nicht nur Fehler gefunden, sondern auch
Inkonsistenzen vermieden werden. Wird für einen Bruch z.B. intern in einer Hilfskomponente
festgehalten, ob er kürzbar ist, kann bei einer Änderung des Sachverhalts durch eine Zuweisung
des Zählers von außen intern eine entsprechende Anpassung stattfinden.
Für Brüche ist es deshalb sinnvoll, den Zugriff auf die Komponenten Zähler und Nenner nur
über die zur Klasse Bruch gehörenden Funktionen zu erlauben. Damit wird auch ein anderer
Vorteil erreicht: Der Aufbau und das Verhalten eines Bruchs können intern modifiziert werden,
ohne dass sich die Schnittstelle nach außen ändert.

4.1.2 Deklaration der Klasse Bruch


Um eine Klasse in C++ verwenden zu können, muss sie zunächst deklariert werden. Dies ge-
schieht, indem eine Struktur deklariert wird, in der die prinzipiellen Eigenschaften der Klasse
aufgelistet werden. Sie enthält sowohl die besprochenen Komponenten für den Zähler und den
Nenner als auch die Funktionen, die die Schnittstelle ausmachen.
Damit die Deklaration zum Kompilieren aller Module, in denen die Klasse Bruch gebraucht
wird, verwendet werden kann, wird sie in einer Headerdatei untergebracht. Sinnvollerweise trägt
die Datei den Namen der Klasse und erhält deshalb am besten den Namen bruch.hpp.
Die erste Version der Headerdatei bruch.hpp hat insgesamt folgenden Aufbau, auf den an-
schließend im Einzelnen eingegangen wird:
// klassen/bruch1.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse Bruch
*/
class Bruch {

/* privat: kein Zugriff von außen


*/
private:
int zaehler;
int nenner;

/* öffentliche Schnittstelle
*/
Sandini Bib

136 Kapitel 4: Programmieren von Klassen

public:
// Default-Konstruktor
Bruch();

// Konstruktor aus int (Zähler)


Bruch(int);

// Konstruktor aus zwei ints (Zähler und Nenner)


Bruch(int, int);

// Ausgabe
void print();
};

} // **** ENDE Namespace Bsp ********************************

#endif /* BRUCH_HPP */

Präprozessor-Anweisungen
Die komplette Headerdatei wird durch Präprozessor-Anweisungen eingeschlossen, mit deren
Hilfe vermieden wird, dass die darin befindlichen Deklarationen bei mehrfachem Einbinden der
Datei mehrfach durchgeführt werden:
#ifndef BRUCH_HPP // ist nur beim ersten Durchlauf erfüllt, denn
#define BRUCH_HPP // BRUCH_HPP wird beim ersten Durchlauf definiert
...
#endif
Mit #ifndef ( if not defined“) werden die folgenden Zeilen bis zum korrespondierenden #endif

nur verarbeitet, wenn die Konstante BRUCH_HPP nicht definiert ist. Da sie bei der ersten Verar-
beitung definiert wird, werden die Anweisungen bei jedem weiteren Mal ignoriert.
Da es immer passieren kann, dass Headerdateien über unterschiedliche Wege von einer Quell-
datei mehrfach eingebunden werden, sollten Deklarationen in Headerdateien immer durch derar-
tige Anweisungen eingeschlossen werden.

4.1.3 Die Klassenstruktur


Die eigentliche Deklaration einer Klasse erfolgt in einer so genannten Klassenstruktur. Dort
werden alle Komponenten (Attribute und Funktionen) der Klasse deklariert.

Das Schlüsselwort class


Die Deklaration beginnt mit dem Schlüsselwort class. Es folgen ein Symbol für den Namen der
Klasse und der in geschweifte Klammern eingeschlossene und durch ein Semikolon abgeschlos-
sene Klassenrumpf, in dem die Eigenschaften der Klasse deklariert werden:
Sandini Bib

4.1 Die erste Klasse: Bruch 137

class Bruch {
...
};
Wenn man eine Bibliothek definiert, die aus mehreren Klassen besteht, stellt jeder Klassenname
ein globales Symbol dar, das überall verwendet werden kann. Dies kann bei der Verwendung
unterschiedlicher Bibliotheken schnell zu Namenskonflikten führen. Aus diesem Grund wird die
Klasse Bruch innerhalb eines Namensbereiches (englisch: namespace) deklariert:
namespace Bsp { // Beginn Namensbereich Bsp

class Bruch {
...
};

} // Ende Namensbereich Bsp


Damit befindet sich lediglich das Symbol Bsp im globalen Gültigkeitsbereich. Genau genommen
definieren wir also Bruch in Bsp“. Die Notation, um diese Klasse anzusprechen, lautet in C++

Bsp::Bruch.
Alle Klassen und auch andere Symbole sollte man immer einem Namensbereich zuordnen.
Dies vermeidet nicht nur Konflikte, sondern macht auch deutlich, welche Elemente eines Pro-
gramms logisch zusammengehören. In der objektorientierten Modellierung bezeichnet man eine
derartige Gruppierung auch als Paket (englisch: package).
Weitere Details zu Namensbereichen werden in Abschnitt 3.3.8 und Abschnitt 4.3.5 erläutert.

Zugriffsschlüsselwörter
Innerhalb der Klassenstruktur befinden sich Zugriffsschlüsselwörter, die die einzelnen Kompo-
nenten gruppieren. Damit wird festgelegt, welche Komponenten der Klasse intern sind und auf
welche auch von außen, bei der Anwendung der Klasse, zugegriffen werden kann.
Die Komponenten zaehler und nenner sind privat:
class Bruch {
private:
int zaehler;
int nenner;
...
};
Durch das Schlüsselwort private werden alle folgenden Komponenten als privat deklariert.
In Klassen ist dies zwar die Defaulteinstellung, es sollte zur besseren Lesbarkeit aber dennoch
explizit herausgestellt werden.
Die Tatsache, dass die Komponenten privat sind, bedeutet, dass auf sie nur im Rahmen der Im-
plementierung der Klasse Zugriff besteht. Der Anwender, der einen Bruch verwendet, hat keinen
direkten Zugriff darauf. Er kann diesen nur indirekt erhalten, indem entsprechende Funktionen
zur Verfügung gestellt werden.
Sandini Bib

138 Kapitel 4: Programmieren von Klassen

Zugriff hat der Anwender der Klasse Bruch nur auf die öffentlichen Komponenten. Diese wer-
den durch das Schlüsselwort public gekennzeichnet. Es handelt sich typischerweise um die
Funktionen, die die Schnittstelle der Objekte dieser Klasse nach außen zum Anwender bilden:
class Bruch {
...
public:
void print ();
};
Es ist nicht zwingend, dass alle Elementfunktionen öffentlich und alle anderen Komponenten
privat sind. Bei größeren Klassen ist es durchaus nicht untypisch, dass es klasseninterne Hilfs-
funktionen gibt, die als private Elementfunktionen deklariert werden.
Genauso ist es prinzipiell möglich, Komponenten, die keine Funktionen sind, mit einem
öffentlichen Zugriff zu versehen. Dies ist aber im Allgemeinen nicht sinnvoll, da damit der Vor-
teil, die Objekte nur über eine wohldefinierte Schnittstelle manipulieren zu können, aufgehoben
wird.
Zugriffsdeklarationen können in einer Klassendeklaration beliebig oft auftreten, wodurch der
Zugriff auf die folgenden Komponenten mehrfach wechseln kann:
class Bruch {
private:
...
public:
...
private:
...
};

Strukturen und Klassen


Aus der Sicht eines C-Programmierers kann eine Klasse als eine C-Struktur betrachtet werden,
die einfach nur mit zusätzlichen Eigenschaften ausgestattet wurde (Zugriffskontrolle und Funk-
tionen als Komponenten).
Es ist sogar so, dass die Eigenschaften von Klassen in C++ auch für Strukturen eingeführt
werden. Statt des Schlüsselwortes class kann deshalb auch immer das Schlüsselwort struct
verwendet werden. Der einzige Unterschied ist, dass mit dem Schlüsselwort struct alle Kom-
ponenten defaultmäßig öffentlich sind. Jede C-Struktur ist im C++-Sinne also eine Klasse mit
lauter öffentlichen Komponenten.
In der Praxis empfehle ich, um unnötige Verwirrung zu vermeiden, für Klassen immer das
Schlüsselwort class zu verwenden. Lediglich wenn in C++ Strukturen im herkömmlichen C-
Sinne eingesetzt werden (ohne Funktionen als Komponenten und mit öffentlichem Zugriff auf
alle Komponenten), verwende ich zur Abgrenzung das Schlüsselwort struct. Leider gibt es
Literatur, in der selbst für Beispiele zur Programmierung mit Klassen das Schlüsselwort struct
verwendet wird.
Sandini Bib

4.1 Die erste Klasse: Bruch 139

4.1.4 Elementfunktionen
Eine Komponente, die in der Struktur der Klasse Bruch deklariert wird, ist die Elementfunktion
print():
class Bruch {
...
void print (); // Ausgabe eines Bruchs
...
};
Sie dient dazu, einen Bruch auszugeben. Da sie keinen Wert zurückliefert, hat sie den Rückga-
betyp void.
Man beachte dabei, dass der Bruch, der jeweils ausgegeben werden soll, nicht als Parameter
übergeben wird. Er ist in einer Elementfunktion automatisch vorhanden, da der Aufruf einer
Elementfunktion immer an ein bestimmtes Objekt dieser Klasse gebunden ist:
Bsp::Bruch x;
...
x.print(); // Elementfunktion print() für Objekt x aufrufen
Die Funktion print() wird mit dem Punkt-Operator für einen Bruch ohne (weitere) Parameter
aufgerufen. Im objektorientierten Sprachgebrauch wird durch den Aufruf von x.print() dem
Objekt x als Instanz der Klasse Bruch die Nachricht print ohne Parameter geschickt.

4.1.5 Konstruktoren
Die ersten drei Funktionen, die in der Klasse Bruch deklariert werden, sind spezielle Funktionen.
Sie legen fest, auf welche Weise ein Bruch erzeugt ( konstruiert“) werden kann. Ein solcher Kon-

struktor (englisch: constructor) ist daran zu erkennen, dass er als Funktionsnamen den Namen
der Klasse trägt.
Aufgerufen wird ein Konstruktor immer dann, wenn eine Instanz (ein konkretes Objekt) der
entsprechenden Klasse angelegt wird. Das ist z.B. beim Deklarieren einer entsprechenden Va-
riablen der Fall. Nachdem für das Objekt der notwendige Speicherplatz angelegt wurde, werden
die Anweisungen im Konstruktor ausgeführt. Sie dienen typischerweise dazu, das angelegte Ob-
jekt zu initialisieren, indem die Datenelemente des Objekts mit sinnvollen Startwerten versehen
werden.
Bei der Klasse Bruch werden drei Konstruktoren deklariert:
class Bruch {
...
// Default-Konstruktor
Bruch ();

// Konstruktor aus int (Zähler)


Bruch (int);
Sandini Bib

140 Kapitel 4: Programmieren von Klassen

// Konstruktor aus zwei ints (Zähler und Nenner)


Bruch (int, int);
...
};
Das bedeutet, dass ein Bruch auf dreierlei Arten angelegt werden kann:
 Ein Bruch kann ohne Argument angelegt werden.
Der Konstruktor wird z.B. aufgerufen, wenn ein Objekt der Klasse Bruch ohne weitere
Parameter deklariert wird:
Bsp::Bruch x; // Initialisierung mit Default-Konstruktor
Einen Konstruktor ohne Parameter nennt man auch Default-Konstruktor.
 Ein Bruch kann mit einem Integer als Argument erzeugt werden. Wie wir bei der Implemen-
tierung des Konstruktors noch sehen werden, wird dieser Parameter als ganze Zahl interpre-
tiert, mit der der Bruch initialisiert wird.
Ein solcher Konstruktor wird z.B. durch eine Deklaration aufgerufen, bei der der Bruch
mit einem Objekt des passenden Typs initialisiert wird:
Bsp::Bruch y = 7; // Initialisierung mit int-Konstruktor
Diese Art der Initialisierung wurde von C übernommen.
In C++ wurde aber auch eine neue Schreibweise zur Initialisierung eines Objekts ein-
geführt, die genauso verwendet werden kann:
Bsp::Bruch y(7); // Initialisierung mit int-Konstruktor

 Ein Bruch kann mit zwei ganzzahligen Argumenten erzeugt werden. Diese Parameter werden
zum Initialisieren von Zähler und Nenner verwendet.
Um einen solchen Konstruktor aufrufen zu können, wurde die gerade vorgestellte neue
Schreibweise zur Initialisierung eines Objekts eingeführt, denn sie erlaubt es, mehrere Argu-
mente zu übergeben:
Bsp::Bruch w(7,3); // Initialisierung mit int/int-Konstruktor

Konstruktoren haben noch eine ungewöhnliche Besonderheit: Sie besitzen keinen Rückgabetyp
(auch nicht void). Es sind also keine Funktionen oder Prozeduren im herkömmlichen Sinne.

Klassen ohne Konstruktoren


Werden für eine Klasse keine Konstruktoren definiert, können trotzdem Instanzen (konkrete Ob-
jekte) der Klasse angelegt werden. Diese Objekte werden dann allerdings nicht initialisiert. Es
gibt also sozusagen einen vordefinierten Default-Konstruktor, der nichts macht.
Damit Objekte keinen undefinierten Zustand erhalten, sollte dieser Fall unbedingt vermie-
den werden. Wenn es den Zustand nicht initialisiert“ geben soll, dann sollte stattdessen eine

Boolesche Komponente eingeführt werden, die diesen Sachverhalt über den Konstruktor defini-
tiv festhält. Werden dann Operationen für einen Bruch mit nicht definiertem Wert aufgerufen,
können entsprechende Fehlermeldungen ausgegeben werden. Bei einem Bruch, der, da er nicht
initialisiert wurde, einen beliebigen Zustand besitzen kann, ist das nicht möglich.
Sandini Bib

4.1 Die erste Klasse: Bruch 141

Sobald Konstruktoren definiert werden, können Objekte nur noch über diese angelegt werden.
Wird kein Default-Konstruktor, wohl aber ein anderer Konstruktor definiert, ist ein Anlegen eines
Objekts der Klasse ohne Parameter also nicht mehr möglich. Auf diese Weise kann die Übergabe
von Werten zur Initialisierung erzwungen werden.

4.1.6 Überladen von Funktionen


Die Tatsache, dass – wie im Fall der Konstruktoren – mehrere Funktionen den gleichen Namen
besitzen dürfen, ist eine grundsätzliche Eigenschaft von C++. Funktionen (nicht nur Element-
funktionen) dürfen beliebig überladen werden.
Das Überladen (englisch: overloading) von Funktionen bedeutet, dass diese den gleichen Na-
men tragen und sich nur in ihren Parametern unterscheiden. Welche Funktion aufzurufen ist, wird
durch Anzahl und Typ der Parameter entschieden. Eine Unterscheidung nur durch den Rückga-
betyp ist nicht zulässig.
Das folgende Beispiel zeigt das Überladen einer globalen Funktion zum Berechnen des Qua-
drats für Integer und Gleitkommawerte:
// Deklaration
int quadrat (int); // Quadrat eines ints
double quadrat (double); // Quadrat eines doubles
void f ()
{
// Aufruf
quadrat(713); // ruft Quadrat eines ints auf
quadrat(4.378); // ruft Quadrat eines doubles auf
}
Die Funktion ließe sich im gleichen Programm zusätzlich für die Klasse Bruch überladen:
// Deklaration
int quadrat (int); // Quadrat eines ints
double quadrat (double); // Quadrat eines doubles
Bsp::Bruch quadrat (Bsp::Bruch); // Quadrat eines Bruchs

void f ()
{
Bsp::Bruch b;
...
// Aufruf
quadrat(b); // ruft Quadrat eines Bruchs auf
}
Beim Überladen von Funktionen sollte natürlich darauf geachtet werden, dass die überladenen
Funktionen im Prinzip das Gleiche machen. Da jede Funktion für sich implementiert wird, liegt
dies in der Hand des Programmierers.
Sandini Bib

142 Kapitel 4: Programmieren von Klassen

Parameter-Prototypen
Damit eine Unterscheidung der Funktionen über die Parameter überhaupt möglich ist, müssen
diese mit Parameter-Prototypen deklariert werden. Das bedeutet, der Typ eines Parameters muss
bei der Deklaration angegeben werden und steht bei der Definition einer Funktion direkt in der
Liste der Parameter:
// Deklaration
int quadrat (int, int);

// Definition
int quadrat (int a, int b)
{
return a * b;
}
Parameter-Prototypen können auch in ANSI-C definiert werden. In C brauchten die Parameter
bei einer Deklaration allerdings nicht unbedingt angegeben zu werden. Im Vergleich zu C gibt es
deshalb einen wichtigen Unterschied: Die Deklaration
void f ();
definiert in C eine Funktion mit beliebig vielen Parametern. In C++ bedeutet dies aber, dass die
Funktion auf jeden Fall keine Parameter besitzt. Die Schreibweise von ANSI-C zur Deklaration
einer Funktion ohne Parameter
void f (void);
kann in C++ ebenfalls verwendet werden, ist aber unüblich.
Falls eine Funktion eine variable Anzahl von Argumenten besitzt, kann dies in der Deklara-
tion durch drei Punkte (mit oder ohne Komma davor) angegeben werden:
int printf (char*, ...);
Eine solche Angabe nennt man im englischen Ellipsis, was man im Deutschen etwa mit Auslas-
sung oder Unvollständigkeit übersetzen kann.1

4.1.7 Implementierung der Klasse Bruch


Die in der Deklaration der Klasse Bruch deklarierten Elementfunktionen müssen natürlich auch
implementiert werden. Dies geschieht typischerweise in einem eigenen Modul, das für die Klasse
angelegt wird. Es trägt den Namen Bruch.cpp.
Es ist von Vorteil, für jede Klasse eine eigene Quelldatei zu verwenden. Damit müssen Pro-
gramme nur die Module der Klassen dazubinden, die auch verwendet werden. Zusätzlich un-
terstützt diese Vorgehensweise natürlich auch die Idee der objektorientierten Programmierung,

1
In vielen Büchern wird der Begriff ellipsis“ auch als Ellipse“ übersetzt, womit offensichtlich nicht das
” ”
mathematische Objekt, sondern die sprachwissenschaftliche Bezeichnung für eine Ersparung von Redetei-

len“ gemeint ist (ein Duden ist manchmal eine wirklich interessante Sache).
Sandini Bib

4.1 Die erste Klasse: Bruch 143

da jede Art von Objekt für sich betrachtet wird (es gibt natürlich auch gute Gründe, von dieser
Regel abzuweichen, etwa wenn zwei Klassen logisch und technisch sehr eng zusammengehören).
Die Quelldatei der ersten Version der Klasse Bruch sieht wie folgt aus:
// klassen/bruch1.cpp
// Headerdatei mit der Klassen-Deklaration einbinden
#include "bruch.hpp"

// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Default-Konstruktor
*/
Bruch::Bruch ()
: zaehler(0), nenner(1) // Bruch mit 0 initialisieren
{
// keine weiteren Anweisungen
}

/* Konstruktor aus ganzer Zahl


*/
Bruch::Bruch (int z)
: zaehler(z), nenner(1) // Bruch mit z 1tel initialisieren
{
// keine weiteren Anweisungen
}

/* Konstruktor aus Zähler und Nenner


*/
Bruch::Bruch (int z, int n)
: zaehler(z), nenner(n) // Zähler und Nenner wie übergeben initialisieren
{
// 0 als Nenner ist allerdings nicht erlaubt
if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
}
Sandini Bib

144 Kapitel 4: Programmieren von Klassen

/* print
*/
void Bruch::print ()
{
std::cout << zaehler << '/' << nenner << std::endl;
}

} // **** ENDE Namespace Bsp ********************************


Am Anfang wird die Deklaration der Klasse eingebunden, deren Elementfunktionen hier defi-
niert werden:
#include "bruch.hpp"
Dies ist notwendig, damit der Compiler z.B. weiß, welche Komponenten die Klasse Bruch be-
sitzt.
Anschließend werden verschiedene andere Headerdateien eingebunden, die die Datentypen
und die Funktionen deklarieren, die von der Klasse verwendet werden. In diesem Fall sind das
zwei Dateien:
#include <iostream>
#include <cstdlib>
Die erste Include-Anweisung bindet die Standardelemente von C++ für Ein- und Ausgaben ein
(siehe Seite 29 und Seite 202). Die zweite Include-Anweisung bindet eine Headerdatei ein, in
der verschiedene Standardfunktionen deklariert werden, die auch schon in C zur Verfügung ste-
hen. In C trägt diese Datei den Namen <stdlib.h>. Damit die Symbole zum Namensbereich
der Standardbibliothek std gehören, werden sie für C++ in der Datei <cstdlib> deklariert.
Generell stehen die Standardfunktionen von C auch in C++ zur Verfügung. Die dazugehörigen
Headerdateien haben statt der Endung .h das Präfix c.
In diesem Programm wird <cstdlib> konkret für die Funktion std::exit() und die dazu-
gehörige Konstante EXIT_FAILURE eingebunden. Mit std::exit() kann man ein Programm
im Fehlerfall abbrechen (siehe Seite 128).
Man beachte, dass die Include-Anweisungen zwei unterschiedliche Formen haben. Im einen
Fall wird die einzubindende Datei in doppelten Anführungszeichen, im anderen Fall in spitzen
Klammern eingeschlossen:
#include "bruch.hpp"
#include <iostream>
Die Form mit den spitzen Klammern ist für externe Dateien vorgesehen. Die dazugehörigen
Dateien werden in den Verzeichnissen gesucht, die vom Compiler als System-Verzeichnis an-
gesehen werden.2 Die in doppelten Anführungsstrichen angegebenen Dateien werden zusätzlich

2
Auf Unix-Systemen kann man diese Verzeichnisse meistens mit der Option -I definieren. Microsoft
Visual C++ bietet dazu die Möglichkeit der Angabe zusätzlicher Include-Verzeichnisse“.

Sandini Bib

4.1 Die erste Klasse: Bruch 145

auch im lokalen Verzeichnis gesucht. Falls sie dort nicht gefunden werden, wird ebenfalls in den
System-Verzeichnissen nachgesehen (siehe auch Seite 56).

Der Bereichsoperator
Beim Betrachten der Funktionen fällt zunächst auf, dass vor sämtlichen Funktionsnamen der
Ausdruck
Bruch::
steht. Der Operator :: ist der Bereichsoperator. Er ordnet einem nachfolgenden Symbol den
Gültigkeitsbereich der davorstehenden Klasse zu. Er besitzt höchste Priorität.
In diesem Fall wird damit zum Ausdruck gebracht, dass es sich jeweils um eine Elementfunk-
tion der Klasse Bruch handelt. Das bedeutet, dass die Funktion nur für Objekte dieser Klasse
aufgerufen werden kann. Damit ist aber gleichzeitig definiert, dass es innerhalb der Funktionen
immer einen Bruch gibt, für den die Funktion aufgerufen wurde und auf dessen Komponenten
zugegriffen werden kann.

Implementierung der Konstruktoren


Zunächst werden die Konstruktoren implementiert. Sie sind, wie schon erwähnt, daran zu erken-
nen, dass der Funktionsname gleich dem Klassennamen ist, also Bruch::Bruch lautet.
Der Default-Konstruktor initialisiert den Bruch mit 0 (genauer mit 01 ):
Bruch::Bruch ()
: zaehler(0), nenner(1) // Bruch mit 0 initialisieren
{
// keine weiteren Anweisungen
}
Bei jedem Erzeugen eines Bruchs ohne Argumente wird dieser Konstruktor aufgerufen. Damit
wird sichergestellt, dass auch ein Bruch, der ohne Argumente zur Initialisierung angelegt wird,
keinen undefinierten Wert besitzt.
An dieser Stelle zeigt sich gleich eine Besonderheit der Konstruktoren. Da sie zur Initiali-
sierung der Objekte dienen, verfügen sie über die Möglichkeit, noch vor den eigentlichen An-
weisungen der Funktion die initialen Werte der Komponenten festzulegen. Getrennt durch einen
Doppelpunkt und vor dem eigentlichen Funktionskörper können für jedes Attribut die initialen
Werte angegeben werden. Es handelt sich dabei um eine so genannte Initialisierungsliste. Sie
wirkt so, als hätte man für Brüche, die unter diesen Umständen angelegt werden, die folgenden
Deklarationen in Bezug auf ihre Komponenten getätigt:
int zaehler(0);
int nenner(1);
Alternativ hätte man die Werte auch mit Hilfe herkömmlicher Anweisungen zuweisen können:
Bruch::Bruch ()
{
zaehler = 0;
nenner = 1;
}
Sandini Bib

146 Kapitel 4: Programmieren von Klassen

Es gibt jedoch kleine Unterschiede in der Semantik dieser beiden Formen – so wie es Unter-
schiede zwischen
int x = 0; // x anlegen und dabei sofort mit 0 initialisieren
bzw.
int x(0); // x anlegen und dabei sofort mit 0 initialisieren
auf der einen und
int x; // x anlegen
x = 0; // in einem separaten Schritt 0 zuweisen
auf der anderen Seite gibt. Diese Unterschiede spielen hier zwar noch keine Rolle; man sollte
sich aber gleich an die Syntax mit der Initialisierungsliste gewöhnen.
Der zweite Konstruktor wird aufgerufen, wenn beim Erzeugen des Objekts ein Integer über-
geben wird. Dieser Parameter wird als ganze Zahl interpretiert. Entsprechend wird der Zähler
mit dem übergebenen Parameter und der Nenner mit 1 initialisiert:
Bruch::Bruch (int z)
: zaehler(z), nenner(1) // Bruch mit z 1tel initialisieren
{
// keine weiteren Anweisungen
}
Der dritte Konstruktor erhält zwei Parameter, die er zum Initialisieren von Zähler und Nenner
verwendet. Dieser Konstruktor wird immer dann aufgerufen, wenn beim Anlegen eines Bruchs
zwei Integer übergeben werden. Falls als Nenner allerdings 0 übergeben wird, wird das Pro-
gramm mit einer entsprechenden Fehlermeldung beendet:
Bruch::Bruch (int z, int n)
: zaehler(z), nenner(n) // Zähler und Nenner wie übergeben initialisieren
{
// 0 als Nenner ist allerdings nicht erlaubt
if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
}

Fehlerbehandlung in Konstruktoren
Die Tatsache, dass in diesem Beispiel bei einer fehlerhaften Initialisierung das Programm beendet
wird, ist sehr radikal. Es wäre besser, den Fehler zu melden und abhängig von der Programm-
situation darauf zu reagieren. Das Problem ist nur, dass Konstruktoren keine herkömmlichen
Funktionen sind, die explizit aufgerufen werden und einen Rückgabewert zurückliefern können.
Sie werden implizit durch Deklarationen aufgerufen – und Deklarationen haben keinen Rückga-
bewert.
Sandini Bib

4.1 Die erste Klasse: Bruch 147

Das Programm wird deshalb mangels besserer Alternativen abgebrochen. Um einen solchen Pro-
grammabbruch zu vermeiden, muss also in allen Programmen, in denen die Klasse verwendet
wird, darauf geachtet werden, dass als Nenner nicht 0 übergeben wird.
Es gibt natürlich noch andere Alternativen zum Abbruch. So könnte der Bruch auch einen
Default-Wert erhalten. Dies erscheint aber nicht sinnvoll, da damit ein offensichtlicher Fehler
(Nenner dürfen nicht 0 sein) nicht behandelt, sondern einfach nur ignoriert wird.
Es gibt auch die Möglichkeit, eine weitere Hilfskomponente für Brüche einzuführen, die
festhält, dass der Bruch noch nicht korrekt initialisiert wurde. Diese Komponente müsste dann
aber auch ausgewertet und bei jeder Operation beachtet werden.
Die beste Möglichkeit, mit solchen Fehlern umzugehen, stellt das C++-Konzept zur Aus-
nahmebehandlung dar, das bereits in Abschnitt 3.6 vorgestellt wurde. Dieses Konzept erlaubt
es, auch bei einer Deklaration eine Fehlerbehandlung auszulösen, die aber nicht unbedingt zum
Abbruch führen muss, sondern von der Programmumgebung, in der sie auftritt, abgefangen und
entsprechend der aktuellen Situation behandelt werden kann. Wir werden die Klasse Bruch in
Abschnitt 4.7 auf diesen Mechanismus umstellen.

Implementierung von Elementfunktionen


Als Nächstes folgt die Definition der Elementfunktion print(), die einen Bruch ausgibt. Da-
zu gibt diese Funktion einfach den Zähler und den Nenner mit Hilfe der Techniken der C++-
Standardbibliothek auf dem Standard-Ausgabekanal, std::cout, in der Form zähler/nenner“

aus:
void Bruch::print ()
{
std::cout << zaehler << '/' << nenner << std::endl;
}
Da es sich um eine Elementfunktion handelt, muss sie immer für ein bestimmtes Objekt aufgeru-
fen werden, damit zaehler und nenner zugeordnet werden können. Dies bedeutet, dass es bei
Elementfunktionen eine neue Art von Gültigkeitsbereich gibt, der bei der Zuordnung von Sym-
bolen berücksichtigt wird: den Gültigkeitsbereich der Klasse, zu der die Funktion gehört. Die
Deklaration von einem in einer Elementfunktion verwendeten Symbol wird zunächst im loka-
len Gültigkeitsbereich der Funktion, dann in der Klassendeklaration und schließlich im globalen
Gültigkeitsbereich gesucht.
Durch w.print() wird diese Funktion für das Objekt w aufgerufen. Daraus folgt, dass in
diesem Fall in der Funktion mit zaehler der Zähler von w, also w.zaehler, angesprochen wird.
Bei einem Aufruf der Funktion durch x.print() wird entsprechend x.zaehler angesprochen.
Man beachte, dass die Komponente zaehler im Anwendungsprogramm nicht direkt ange-
sprochen werden kann, da sie als privat deklariert ist. Nur die Elementfunktionen einer Klasse
haben Zugriff auf die privaten Komponenten dieser Klasse.

4.1.8 Anwendung der Klasse Bruch


Die vorgestellte erste Version der Klasse Bruch kann bereits in einem Anwendungsprogramm
eingesetzt werden.
Sandini Bib

148 Kapitel 4: Programmieren von Klassen

Dazu muss in der entsprechenden Quelldatei die Headerdatei der Klasse eingebunden werden.
Aufgrund der Typbindung von C++ wird ansonsten die Deklaration einer Variablen vom Typ
Bruch abgelehnt. Der Compiler braucht außerdem Kenntnis über die Komponenten eines Bruchs,
damit entsprechend viel Speicherplatz angelegt werden kann. Mit Hilfe der Klassendeklaration
wird auch geprüft, ob alle Zugriffe auf Objekte der Klasse Bruch zulässig sind.
Unter der Annahme, dass das Anwendungsprogramm btest.cpp heißt, ergibt sich das in
Abbildung 4.3 dargestellte Übersetzungsschema: Die Headerdatei bruch.hpp mit der Klassen-
deklaration wird von der Quelldatei mit der Klassenimplementierung und der Quelldatei mit
dem Anwendungsprogramm eingebunden. Die beiden Quelldateien werden vom C++-Compiler
zunächst in Objektdateien übersetzt (diese haben typischerweise die Endung .o oder .obj).
Von einem Linker werden die Objektdateien dann zum ausführbaren Programm btest (oder
btest.exe) zusammengebunden. Das Übersetzen und Binden ist auf den meisten Systemen
auch in einem Schritt möglich.

Klassen- C++
Bruch.cpp Bruch.o
Implementierung Compiler

Klassen-
Bruch.hpp Linker btest
Spezifikation

C++
Anwendung btest.cpp Compiler
btest.o

Abbildung 4.3: Übersetzungsschema für die Klasse Bruch

Das folgende Programm ist ein erstes Anwendungsbeispiel der ersten Version der Klasse Bruch
und zeigt, was mit Brüchen bereits alles gemacht werden kann:
// klassen/btest1.cpp
// Headerdateien für die verwendeten Klassen einbinden
#include "bruch.hpp"

int main()
{
Bsp::Bruch x; // Initialisierung durch Default-Konstruktor
Bsp::Bruch w(7,3); // Initialisierung durch int/int-Konstruktor

// Bruch w ausgeben
Sandini Bib

4.1 Die erste Klasse: Bruch 149

w.print();

// Bruch w wird an Bruch x zugewiesen


x = w;

// 1000 in einen Bruch umwandeln und w zuweisen


w = Bsp::Bruch(1000);

// x und w ausgeben
x.print();
w.print();
}
Die Ausgabe des Programms lautet:
7/3
7/3
1000/1

Deklarationen
Im Programm werden zunächst die Variablen x und w deklariert:
Bsp::Bruch x; // Initialisierung durch Default-Konstruktor
Bsp::Bruch w(7,3); // Initialisierung durch int/int-Konstruktor
Der Vorgang, der hier stattfindet, kann unterschiedlich benannt werden:
 Im nicht-objektorientierten Sprachgebrauch werden zwei Variablen vom Typ Bsp::Bruch
angelegt.
 In objektorientierter Lesart werden zwei Instanzen (konkrete Objekte) der Klasse Bruch an-
gelegt.
Konkret wird sowohl für x als auch für w Speicherplatz angelegt und der entsprechende Kon-
struktor aufgerufen. Dies geschieht für x in folgenden Schritten:
 Zunächst wird der Speicherplatz für das Objekt angelegt. Dessen Zustand ist zunächst nicht
definiert:

x: zaehler: ?
nenner: ?

 Da x ohne einen Wert zur Initialisierung deklariert wird, wird anschließend der Default-
Konstruktor aufgerufen:
Bruch::Bruch ()
: zaehler(0), nenner(1) // Bruch mit 0 initialisieren
Sandini Bib

150 Kapitel 4: Programmieren von Klassen

{
// keine weiteren Anweisungen
}
Durch dessen Initialisierungsliste wird der Bruch x also mit 0 ( 01 ) initialisiert:

x: zaehler: 0
nenner: 1

Die Initialisierung von w geschieht entsprechend in folgenden Schritten:


 Zunächst wird wieder der Speicherplatz für das Objekt angelegt, dessen Zustand zunächst
nicht definiert ist:

w: zaehler: ?
nenner: ?

 Da w mit zwei Argumenten initialisiert wird, wird der Konstruktor aufgerufen, der für zwei
Integer als Parameter definiert ist:
/* Konstruktor aus Zähler und Nenner
*/
Bruch::Bruch (int z, int n)
: zaehler(z), nenner(n) // Zähler und Nenner wie übergeben initialisieren
{
// 0 als Nenner ist allerdings nicht erlaubt
if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
}
Mit den übergebenen Parametern initialisiert der Konstruktor den Zähler und den Nenner und
legt somit den Bruch 73 an:

w: zaehler: 7
nenner: 3

Anschließend wird noch geprüft, ob der Nenner 0 ist.


Sandini Bib

4.1 Die erste Klasse: Bruch 151

Zerstören von Objekten


Zerstört werden die angelegten Objekte automatisch, wenn vom Programm der Gültigkeitsbe-
reich der Objekte verlassen wird. Genau so, wie beim Anlegen automatisch Speicherplatz für
das Objekt angelegt wird, wird er bei der Zerstörung des Objekts auch wieder automatisch frei-
gegeben.
Bei lokalen Objekten geschieht dies, wenn der Block, in dem sie deklariert werden, wieder
verlassen wird:
void f ()
{
Bsp::Bruch x; // Erzeugung und Initialisierung von x
Bsp::Bruch w(7,3); // Erzeugung und Initialisierung von w
...
} // automatische Zerstörung von x und w
Es ist allerdings möglich, Funktionen zu definieren, die unmittelbar vor der Freigabe des Spei-
cherplatzes aufgerufen werden. Sie bilden das Gegenstück zu den Konstruktoren und heißen De-
struktoren. Sie können z.B. dazu dienen, eine entsprechende Rückmeldung auszugeben oder ei-
nen Zähler für die Anzahl der existierenden Objekte zurückzusetzen. Typisch ist auch die Freiga-
be von explizit angelegtem Speicherplatz, der zu einem Objekt gehört. Da bei der Klasse Bruch
kein Bedarf für Destruktoren besteht, werden sie erst später in Abschnitt 6.1 eingeführt.

Statische und globale Objekte


Statische oder globale Objekte werden beim Programmstart angelegt und beim Programmende
zerstört. Dies hat eine wichtige Konsequenz: Die Funktion main() ist nicht unbedingt die erste
Funktion eines Programms, die aufgerufen wird. Vorher werden alle Konstruktoren für statische
Objekte aufgerufen, die wiederum beliebige Hilfsfunktionen aufrufen können. Ebenso können
nach dem Beenden von main() oder einem Aufruf von exit() noch Destruktoren für statische
und globale Objekte aufgerufen werden.
Nachfolgend wird vor dem Aufruf von main() für das globale Objekt zweiDrittel der
Konstruktor der Klasse Bruch aufgerufen:
/* globaler Bruch
* - Erzeugung und Initialisierung (über Konstruktor) beim Programmstart
* - automatische Zerstörung (über Destruktor) beim Programmende
*/
Bruch zweiDrittel(2,3);

int main ()
{
...
}
Da Konstruktoren beliebige Hilfsfunktionen aufrufen können, können auf diese Weise bei kom-
plexen Klassen vor dem Aufruf von main() bereits erhebliche Dinge geschehen. Ein Beispiel
dafür wäre eine Klasse für Objekte, die geöffnete Dateien repräsentieren. Wird ein globales Ob-
Sandini Bib

152 Kapitel 4: Programmieren von Klassen

jekt dieser Klasse deklariert, wird durch den Aufruf des Konstruktors noch vor dem Aufruf von
main() eine entsprechende Datei geöffnet.
Felder von Objekten
Konstruktoren werden für jedes erzeugte Objekt einer Klasse aufgerufen. Wenn ein Feld (Array)
von zehn Brüchen deklariert wird, wird also auch zehnmal der Default-Konstruktor aufgerufen:

Bsp::Bruch werte[10]; // Feld von zehn Brüchen


Dabei können auch Werte zur Initialisierung der Elemente im Feld angegeben werden, wie das
folgende Beispiel zeigt:
Bsp::Bruch bm[5] = { Bsp::Bruch(7,2), Bsp::Bruch(42),
Bsp::Bruch(), 13 };
Angelegt werden fünf Brüche (bm[0] bis bm[4]). bm[0] wird mit dem int/int-Konstruktor
initialisiert, da zwei Parameter übergeben werden. bm[1] und bm[3] werden mit dem int-Kon-
struktor initialisiert, da ein Integer übergeben wird. bm[2] und bm[4] werden mit dem Default-
Konstrukor initialisiert, da kein Argument angegeben wird (bei bm[2] wegen der leeren Klam-
mer und bei bm[4], da dafür überhaupt kein Wert zur Initialisierung mehr angegeben wird).
Auch diese fünf Brüche werden zerstört, wenn der Gültigkeitsbereich, in dem sie erzeugt
werden, verlassen wird.

Aufruf von Elementfunktionen


Mit der Anweisung
w.print()
wird für w die Elementfunktion print() aufgerufen, die den Bruch ausgibt.
Für w wird sozusagen auf dessen Klassenkomponente print() zugegriffen. Da das in diesem
Fall eine Funktion ist, bedeutet das, dass diese dadurch für w aufgerufen wird.
In C++ kann auch ein Zeiger (siehe Abschnitt 3.7.1) auf einen Bruch definiert werden. In
einem solchen Fall kann der Zugriff auf eine Komponente bzw. der Aufruf einer Elementfunktion
über den Operator -> erfolgen:
Bsp::Bruch x; // Bruch
Bsp::Bruch* xp; // Zeiger auf Bruch

xp = &x; // xp zeigt auf x

xp->print(); // print() für das, worauf xp zeigt, aufrufen

Zuweisungen
Mit der Anweisung
x = w;
wird dem Bruch x der Bruch w zugewiesen.
Sandini Bib

4.1 Die erste Klasse: Bruch 153

Eine solche Zuweisung ist möglich, obwohl sie in der Klassenspezifikation nicht deklariert wor-
den ist. Für jede Klasse ist nämlich automatisch ein Default-Zuweisungsoperator vordefiniert. Er
ist so implementiert, dass er komponentenweise zuweist. Das bedeutet, dass dem Zähler von x
der Zähler von w und dem Nenner von x entsprechend der Nenner von w zugewiesen wird.
Der Zuweisungsoperator kann für eine Klasse auch selbst definiert werden. Dies ist vor allem
dann notwendig, wenn ein komponentenweises Zuweisen nicht korrekt wäre. Typischerweise ist
das dann der Fall, wenn Zeiger als Komponenten verwendet werden. In Abschnitt 6.1.5 wird
anhand der Klasse String ausführlich darauf eingegangen.

4.1.9 Erzeugung temporärer Objekte


In der folgenden Anweisung wird dem Bruch w der in einen Bruch umgewandelte Wert 1000
zugewiesen:
// 1000 in einen Bruch umwandeln und w zuweisen
w = Bsp::Bruch(1000);
Der Ausdruck
Bsp::Bruch(1000)
erzeugt einen temporären Bruch, wobei der dazugehörige Konstruktor für einen Parameter auf-
gerufen wird. Es handelt sich im Prinzip um eine erzwungene Typumwandlung der ganzzahligen
Konstante 1000 in den Typ Bruch.
Durch die Anweisung
w = Bsp::Bruch(1000);
wird also ein temporäres Objekt der Klasse Bruch angelegt, über den entsprechenden Konstruk-
tor mit 1000
1 initialisiert, dem Objekt w zugewiesen und nach Beendigung der Anweisung wieder
zerstört.
Beim Erzeugen temporärer Objekte einer Klasse muss ein entsprechender Konstruktor de-
finiert sein, dem die Argumente zur Umwandlung übergeben werden. In diesem Fall können
deshalb temporäre Brüche mit keinem, einem oder zwei Argumenten erzeugt werden:
Bsp::Bruch() // temporären Bruch mit dem Wert 0/1 erzeugen
Bsp::Bruch(42) // temporären Bruch mit dem Wert 42/1 erzeugen
Bsp::Bruch(16,100) // temporären Bruch mit dem Wert 16/100 erzeugen

4.1.10 UML-Notation
Bei der Modellierung von Klassen hat sich die UML-Notation als Standard etabliert. Sie dient
dazu, Aspekte von objektorientierten Programmen grafisch darzustellen. Abbildung 4.4 stellt die
UML-Notation der Klasse Bruch dar.
Bei der UML-Notation werden Klassen durch Rechtecke dargestellt. In den Rechtecken be-
finden sich, jeweils durch horizontale Linien getrennt, der Klassenname, die Attribute und die
Operationen. Führende Minuszeichen bei Attributen und Operationen bedeutet, dass diese privat
Sandini Bib

154 Kapitel 4: Programmieren von Klassen

B s p : : B r u c h

- z a e h l e r : i n t
- n e n n e r : i n t

+ B r u c h ( )
+ B r u c h ( z : i n t )
+ B r u c h ( z : i n t , n : i n t )
+ p r i n t ( )

Abbildung 4.4: UML-Notation der Klasse Bruch

sind. Führende Pluszeichen stehen für öffentlichen Zugriff. Falls eine Operation einen Rückga-
betyp besitzt, wird dieser durch einen Doppelpunkt getrennt, hinter der Operation angegeben.
Je nach Stand der Modellierung können bei der UML-Notation Details entfallen. So können
z.B. der Paketname (der Namensbereich), die führenden Zeichen zur Sichtbarkeit, die Datenty-
pen der Attribute oder die Parameter der Operationen entfallen. Auch kann ganz auf die Angabe
von Attributen und Operationen verzichtet werden. Die kürzeste Form einer UML-Notation für
die Klasse Bruch wäre also ein Rechteck, in dem nur das Wort Bruch steht.

4.1.11 Zusammenfassung
 Klassen werden durch Klassenstrukturen mit dem Schlüsselwort class deklariert.
 Klassen sollten wie alle anderen globalen Symbole in einem Namensbereich deklariert wer-
den.
 Die Komponenten einer Klasse besitzen Zugriffsrechte, die durch die Schlüsselwörter public
und private vergeben werden. Der Default-Zugriff ist private.
 Eine Struktur ist in C++ das gleiche wie eine Klasse, nur dass statt class das Schlüsselwort
struct verwendet wird und der Default-Zugriff public ist.
 Die Komponenten der Klassen können auch Funktionen (so genannte Elementfunktionen)
sein. Sie bilden typischerweise die öffentliche Schnittstelle zu den Komponenten, die den
Zustand des Objekts definieren (es kann aber auch private Hilfsfunktionen geben). Element-
funktionen haben Zugriff auf alle Komponenten ihrer Klasse.
 Für jede Klasse ist ein Default-Zuweisungsoperator vordefiniert, der komponentenweise zu-
weist.
Sandini Bib

4.1 Die erste Klasse: Bruch 155

 Konstruktoren sind spezielle Elementfunktionen, die beim Erzeugen von Objekten einer Klas-
se aufgerufen werden und dazu dienen, diese zu initialisieren. Sie tragen als Funktionsnamen
den Namen der Klasse und besitzen keinen Rückgabetyp, auch nicht void.
 Alle Funktionen müssen mit Parameter-Prototypen deklariert werden.
 Funktionen können überladen werden. Das bedeutet, dass der gleiche Funktionsname mehr-
fach vergeben werden darf, solange sich die Parameter in ihrer Anzahl oder ihren Typen
unterscheiden. Eine Unterscheidung nur durch den Rückgabetyp ist nicht erlaubt.
 Der Bereichsoperator ::“ ordnet einem Symbol den Gültigkeitsbereich einer bestimmten

Klasse zu. Er besitzt höchste Priorität.
 Zur Modellierung von Klassen wird standardmäßig die UML-Notation verwendet.
Sandini Bib

156 Kapitel 4: Programmieren von Klassen

4.2 Operatoren für Klassen


In C++ gibt es die Möglichkeit, Operatoren für eigene Klassen zu definieren. Dadurch ist es z.B.
möglich, Brüche wie andere Zahlen zu behandeln:
Bsp::Bruch a, b, c;
...
if (a < c) {
c = a * b;
}
In diesem Abschnitt wird deshalb eine neue Version der Klasse Bruch vorgestellt, die es ermög-
licht, bei der Arbeit mit Brüchen Operatoren zu verwenden.
In diesem Zusammenhang wird auch das Schlüsselwort this eingeführt, mit dem in einer
Elementfunktion das Objekt, für das diese aufgerufen wurde, als Ganzes angesprochen werden
kann.

4.2.1 Deklaration von Operatorfunktionen


Die Möglichkeit, Operatoren für Klassen zu definieren und anzuwenden, wird bei der Klasse
Bruch exemplarisch an drei Operatoren aufgezeigt:
 Operator * zum Multiplizieren zweier Brüche
 Operator *= zum multiplikativen Zuweisen eines Bruchs
 Operator < zum Vergleichen zweier Brüche auf kleiner als“

Die Klassenstruktur der Klasse Bruch enthält nun zusätzlich die Deklaration der neuen Opera-
toren und dadurch insgesamt den folgenden Aufbau, der anschließend erläutert wird:
// klassen/bruch2.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse Bruch
*/
class Bruch {
/* privat: kein Zugriff von außen
*/
private:
int zaehler;
int nenner;
Sandini Bib

4.2 Operatoren für Klassen 157

/* öffentliche Schnittstelle
*/
public:
// Default-Konstruktor
Bruch ();

// Konstruktor aus int (Zähler)


Bruch (int);

// Konstruktor aus zwei ints (Zähler und Nenner)


Bruch (int, int);

// Ausgabe
void print ();

// neu: Multiplikation mit anderem Bruch


Bruch operator * (Bruch);

// neu: multiplikative Zuweisung


Bruch operator *= (Bruch);

// neu: Vergleich mit anderem Bruch


bool operator < (Bruch);
};

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP

Hinzugekommen sind die Deklarationen für die Operatoren. Diese werden mit Hilfe des Schlüs-
selworts operator deklariert.
Man beachte, dass bei der Deklaration der Operatoren jeweils nur ein Operand als Parameter
angegeben ist, obwohl es sich in allen Fällen um zweistellige Operatoren handelt. Denn auch
bei Operatorfunktionen, die für eine Klasse definiert werden, existiert ein Objekt, für das die
Operation jeweils aufgerufen wird. Dies ist immer der erste bzw. bei einstelligen Operatoren der
einzige Operand und er wird auch hier nicht als Parameter übergeben. Der übergebene Parameter
ist also jeweils der zweite Operand.
Die Deklaration
class Bruch {
...
Bruch operator * (Bruch);
...
};
Sandini Bib

158 Kapitel 4: Programmieren von Klassen

ist also so zu verstehen, dass der Operator * für den Fall deklariert wird, dass der erste Operand
ein Bruch ist (da die Deklaration in class Bruch stattfindet), und dass der zweite Operand
ebenfalls ein Bruch ist (da als Parameter der Typ Bruch deklariert wird), Da vor operator *
der Typ Bruch angegeben wird, wird als Ergebnis ein neuer Bruch zurückgeliefert.
Dies ist die konsequente Anwendung des objektorientierten Ansatzes, dass jede Operation
eigentlich nur eine Nachricht an einen Empfänger ist, die eventuell Parameter besitzt. In objekt-
orientierter Lesart wird nicht global die Multiplikation zweier Brüche sondern an einen Bruch
die Nachricht bilde Produkt mit übergebenem Bruch“ gesendet. Die Nachricht ist in C++ die

Funktion, und der Empfänger der Nachricht ist das Objekt, für das die Funktion aufgerufen wird.
Entsprechend bedeutet die Deklaration
class Bruch {
...
bool operator < (Bruch);
...
};
dass der Operator < für den Fall deklariert wird, dass der erste Operand und der zweite Parame-
ter Brüche sind (Deklaration innerhalb der Klasse Bruch und Bruch als Parametertyp) und ein
Boolescher Wert zurückgeliefert wird.
Da C++ eine formatfreie Sprache ist, können die Leerzeichen bei der Deklaration von Ope-
ratorfunktionen auch wegfallen:
class Bruch {
...
Bruch operator*(Bruch);
bool operator<(Bruch);
...
};

Überladen von Operatoren


Auch Operatoren können überladen werden. Man könnte z.B. die Multiplikation eines Bruchs
mit verschiedenen Typen für den zweiten Operanden deklarieren:
class Bruch {
...
Bruch operator * (Bruch); // Produkt mit Bruch als zweitem Operanden
Bruch operator * (int); // Produkt mit int als zweitem Operanden
...
};
Dies gilt aber nur für eigene Datentypen. Die Operationen der fundamentalen Datentypen, die
von C++ (wie auch von C) vordefiniert werden (char, int, float etc.), sind fest vordefiniert
und können auf diese Weise nicht erweitert werden.
Sandini Bib

4.2 Operatoren für Klassen 159

4.2.2 Implementierung von Operatorfunktionen


Auch die Operatorfunktionen müssen natürlich implementiert werden. Die erste Version der
Klasse Bruch muss deshalb um die deklarierten Operatorfunktionen erweitert werden und erhält
folgenden Aufbau:
// klassen/bruch2.cpp
// Headerdatei mit der Klassen-Deklaration einbinden
#include "bruch.hpp"

// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Default-Konstruktor
*/
Bruch::Bruch ()
: zaehler(0), nenner(1) // Bruch mit 0 initialisieren
{
// keine weiteren Anweisungen
}

/* Konstruktor aus ganzer Zahl


*/
Bruch::Bruch (int z)
: zaehler(z), nenner(1) // Bruch mit z 1tel initialisieren
{
// keine weiteren Anweisungen
}

/* Konstruktor aus Zähler und Nenner


*/
Bruch::Bruch (int z, int n)
{
/* neu: Ein negatives Vorzeichen im Nenner kommt in den Zähler
* Dies vermeidet u.a. eine Sonderbehandlung beim Operator <
*/
if (n < 0) {
zaehler = -z;
nenner = -n;
}
Sandini Bib

160 Kapitel 4: Programmieren von Klassen

else {
zaehler = z;
nenner = n;
}

// 0 als Nenner ist allerdings nicht erlaubt


if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
}

/* print
*/
void Bruch::print ()
{
std::cout << zaehler << '/' << nenner << std::endl;
}

/* neu: Operator *
*/
Bruch Bruch::operator * (Bruch b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}

/* neu: Operator *=
*/
Bruch Bruch::operator *= (Bruch b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt, für das die Operation aufgerufen wurde (erster Operand), zurückliefern
return *this;
}
Sandini Bib

4.2 Operatoren für Klassen 161

/* neu: Operator <


*/
bool Bruch::operator< (Bruch b)
{
/* Ungleichung einfach mit Nennern ausmultiplizieren
* - da die Nenner nicht negativ sein können, kann
* der Vergleich dadurch nicht umgedreht werden
*/
return zaehler * b.nenner < b.zaehler * nenner;
}

} // **** ENDE Namespace Bsp ********************************


Bei allen drei Operatoren spielen zwei Brüche eine Rolle, da es sich jeweils um zweistellige
Operatoren handelt, die definiert werden. Durch die Zuordnung an die Klasse Bruch über den
Bereichsoperator (Bruch::) wird festgelegt, dass die Operatorfunktion für einen Bruch als ersten
Operanden aufgerufen wird. Der Parametertyp Bruch legt jeweils den Typ des zweiten Operan-
den fest.
Der erste Operand ist also automatisch vorhanden, da die Funktion für ihn aufgerufen wird.
Das bedeutet, dass seine Komponenten direkt angesprochen werden können. Mit zaehler und
nenner wird also der erste Operand angesprochen. Jeder andere Zähler und Nenner muss über
den Namen des dazugehörigen Objekts angesprochen werden. Beim zweiten Operanden wird der
Zähler deshalb über den Namen des Parameters mit b.zaehler und der Nenner entsprechend
mit b.nenner angesprochen.
Die Tatsache, dass überhaupt ein Zugriff auf private Komponenten eines anderen Objekts
gleichen Typs möglich ist, ist nicht selbstverständlich. Die Datenkapselung ist in C++ nur ty-
pbezogen. Andere objektorientierte Sprachen wie Smalltalk definieren sie objektbezogen, was
bedeutet, dass auch auf die Parameter der implementierten Klasse nur über öffentliche Funktio-
nen zugegriffen werden darf.
Da zu jeder Funktion noch spezielle Vorüberlegungen oder Anmerkungen notwendig sind,
wird nach diesen allgemeinen Hinweisen nun auf die Implementierung der Operatorfunktionen
im Einzelnen eingegangen.

Multiplikation mit dem Operator 


Der Operator * multipliziert ein Bruch-Objekt mit einem anderen Bruch. Der erste Operand ist
das Objekt, für das die Operation aufgerufen wird, der zweite Operand wird als Parameter über-
geben. Zurückgeliefert wird der Ergebnis-Bruch, wozu Zähler und Nenner jeweils multipliziert
werden. Auf ein Kürzen wird der Einfachheit halber verzichtet.
Im ersten Anlauf könnte die Funktion folgendermaßen implementiert werden:
Sandini Bib

162 Kapitel 4: Programmieren von Klassen

Bruch Bruch::operator * (Bruch b)


{
Bruch ergebnis; // Ergebnis-Bruch anlegen

// Produkt jeweils zuweisen


ergebnis.zaehler = zaehler * b.zaehler;
ergebnis.nenner = nenner * b.nenner;

// Ergebnis zurückliefern
return ergebnis;
}
Diese Implementierung hat jedoch einen Laufzeitnachteil: Es wird unnötigerweise ein zusätzli-
ches lokales Bruch-Objekt ergebnis verwendet. Dieses Objekt muss angelegt und wieder frei-
gegeben werden. Beim Anlegen wird auch noch der Default-Konstruktor aufgerufen, der das
Objekt zunächst fehlerhaft“ mit 01 initialisiert. Anschließend werden erst die richtigen“ Werte
” ”
zugewiesen.
Bei der tatsächlichen Implementierung
Bruch Bruch::operator * (Bruch b)
{
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}
entfällt die falsche“ Initialisierung des lokalen Objekts. Stattdessen wird ein temporärer neuer

Bruch erzeugt, der auch gleich als Rückgabewert zurückgeliefert wird.
Indem man auf diese Weise das unnötige Erzeugen von Objekten vermeidet, kann zum Teil
erheblich Zeit gespart werden. Vor allem bei komplexen Klassen mit zeitaufwändigen Initialisie-
rungen macht sich dies bemerkbar.

Vergleich mit dem Operator <


Der Vergleichsoperator < stellt fest, ob ein Bruch, für den die Operation aufgerufen wurde (erster
Operand), kleiner ist als der übergebene Parameter (zweiter Operand). An dieser Stelle gibt es
eine kleine Falle: Um bei der Rechnung im Zahlenbereich der ganzen Zahlen zu bleiben, bietet es
sich an, die Ungleichung auf beiden Seiten zunächst mit den beiden Nennern zu multiplizieren.
Dabei muss allerdings beachtet werden, dass sich bei der Multiplikation mit einem negativen
Nenner das Vergleichszeichen jeweils umkehrt.
Es gilt:
8 
< a d < c  b falls b und d beide positiv oder beide negativ sind
a
<
c
,:
b d
 a d > c  b falls entweder b oder d negativ ist
Sandini Bib

4.2 Operatoren für Klassen 163

Deshalb müsste die Implementierung der Operation eigentlich eine entsprechende Fallunter-
scheidung enthalten:
int Bruch::operator< (Bruch b)
{
if (nenner * b.nenner > 0) {
// die Multiplikation mit Nennern ist OK
return zaehler * b.nenner < b.zaehler * nenner;
}
else {
// die Multiplikation mit Nennern dreht den Vergleich um
return zaehler * b.nenner > b.zaehler * nenner;
}
}
Diese Fallunterscheidung lässt sich aber vermeiden, indem sichergestellt wird, dass der Nen-
ner eines Bruchs nicht negativ sein kann. Da nicht nur sämtliche Operationen für existierende
Brüche, sondern auch das Initialisieren neuer Brüche selbst definiert wird, ist dies möglich.
Die einzige Stelle, an der ein Bruch mit negativem Nenner entstehen kann (nämlich im
int/int-Konstruktor), wird deshalb so umgeschrieben, dass ein eventuell vorhandenes negatives
Vorzeichen des Nenners in den Zähler gezogen wird:
Bruch::Bruch (int z, int n)
{
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
...
}
Damit kann das Umdrehen des Vergleichs bei negativem Nenner entfallen und der Vergleichs-
operator folgendermaßen implementiert werden:
bool Bruch::operator < (Bruch b)
{
return zaehler * b.nenner < b.zaehler * nenner;
}
Dies ist ein gutes Beispiel für den Vorteil der Datenkapselung: Solange die Schnittstelle nicht
verändert wird, kann die Verwaltung eines Bruchs intern in der Klasse beliebig implementiert
Sandini Bib

164 Kapitel 4: Programmieren von Klassen

und unter verschiedenen Gesichtspunkten optimiert werden. Hätte der Anwender eines Bruchs
direkten Zugriff auf die Komponenten zaehler und nenner, wäre dies nicht möglich.

Multiplikative Zuweisung mit dem Operator  =


C++ besitzt zahlreiche Zuweisungsoperatoren. Neben der eigentlichen Zuweisung mit dem Ope-
rator = gibt es noch die wertverändernden Zuweisungen der Form +=, *= und so weiter (siehe
Abschnitt 3.2.3). Auch diese wertverändernden Zuweisungen können für eigene Klassen defi-
niert werden.
Geschieht dies nicht, gibt es keinen Automatismus, der etwa dafür sorgt, dass mit der Defini-
tion der Multiplikation auch die multiplikative Zuweisung nach obigem Muster definiert ist. Im
Gegenteil, der Programmierer einer Klasse muss einen solchen Operator selbst definieren und
bei der Implementierung auch selbst darauf achten, dass für seine Datentypen auch die Analogie
der fundamentalen Datentypen weiterhin gilt:
x op= y entspricht x = x op y
Andersherum formuliert, ist für abstrakte Datentypen also keineswegs mehr sichergestellt, dass
diese Analogie gilt. Klassen, für die das nicht der Fall ist, verstoßen aber sicherlich gegen das
Ziel lesbaren Codes.
Betrachten wir nach diesen Vorbemerkungen nun die Implementierung der multiplikativen
Zuweisung. Sie ist so implementiert, dass sie sich wie eine Multiplikation mit Zuweisung verhält:
Bruch Bruch::operator *= (Bruch b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt, für das die Operation aufgerufen wurde, zurückliefern


return *this;
}
An dieser Stelle wird das Schlüsselwort this verwendet. Der Ausdruck *this steht in allen
Elementfunktionen für das Objekt, für das diese Funktion bzw. Operation aufgerufen wurde.
In jeder Elementfunktion ist this automatisch als Zeiger auf das Objekt definiert, für das die
Funktion aufgerufen wurde (Zeiger werden in Abschnitt 3.7.1 eingeführt). Der Ausdruck
*this
dereferenziert diesen Zeiger und liefert das Objekt, auf das dieser Zeiger zeigt. Damit steht dieser
Ausdruck für das Objekt, für das eine Elementfunktion jeweils aufgerufen wurde. Handelt es
sich dabei um eine Operatorfunktion, bezeichnet *this also den ersten Operanden. Wenn also
die Operation
x *= y
aufgerufen wird, dann steht *this für den ersten Operanden x, während der zweite Operand y
als Parameter b übergeben wird.
Sandini Bib

4.2 Operatoren für Klassen 165

*this wird außerdem in einer Return-Anweisung am Ende der Operation verwendet. Das bedeu-
tet, die Operation *= liefert das Objekt, dem der neue Wert zugewiesen wurde, als Rückgabewert
zurück. Ein Aufruf wie
x *= y
ist also nicht unbedingt eine abgeschlossene Anweisung, sondern liefert auch etwas und kann
somit Teil eines größeren Ausdrucks sein. So könnte man z.B. eine Bedingung
if ((x *= 2) < 10) // Falls x nach Verdoppelung kleiner als 10 ist
formulieren.
Die Tatsache, dass das Objekt, für das eine Zuweisung aufgerufen wurde, zurückgeliefert
wird, ist eine Eigenschaft, die in C++ wie in C grundsätzlich für alle fundamentalen Datentypen
gilt. Verwendet wird dies vor allem beim Zuweisungsoperator, um Ausdrücke wie
x = y = 10; // 10 an y und dann y (also 10) an x zuweisen
oder
// Rückgabewert von fopen() an fp zuweisen und dann auf NULL testen
if ((fp = fopen(...)) != NULL) {
...
}
zu ermöglichen.
Auch die Default-Zuweisungsoperatoren von Klassen liefern das Objekt, dem etwas zuge-
wiesen wurde, zurück. Dies kann man ausnutzen, um den Operator *= noch prägnanter zu for-
mulieren:
Bruch Bruch::operator *= (Bruch b)
{
// ”x *= y” ==> ”x = x * y” und x zurückliefern
return *this = *this * b;
}
Man kann über die Lesbarkeit solcher Ausdrücke sicherlich streiten. Für die einen ist das die
Eleganz, für die anderen das Chaotische an C.
Der Programmierer hat in C++ den Vorteil, dass er für seine eigenen Klassen selbst definieren
kann, ob die Zuweisungsoperatoren das Objekt zurückliefern sollen oder nicht.
Wer dies nicht will, kann die multiplikative Zuweisung einfach so definieren, dass kein Rück-
gabewert zurückgeliefert wird:
void Bruch::operator *= (Bruch b)
{
// ”x *= y” ==> ”x = x * y”
return *this = *this * b;
}
Man beachte aber, dass dies in C++ eher unüblich ist. Anwender der Klasse werden üblicherweise
davon ausgehen, dass Zuweisungsoperatoren ein Teil eines größeren Ausdrucks sein können.
Sandini Bib

166 Kapitel 4: Programmieren von Klassen

Laufzeit kontra Konsistenz


Der Operator *= ist in der vorgestellten Version als Umsetzung auf die Operatoren = und *
implementiert. Damit ist sichergestellt, dass die Anweisung x *= a immer dasselbe leistet wie
die Anweisung x = x * a. Diese Implementierung birgt aber einen möglichen Laufzeitnachteil:
Es wird noch eine weitere Funktion für die Multiplikation aufgerufen, die ein temporäres Objekt
für den Rückgabewert erzeugt, das dann noch zugewiesen wird.
Man kann die Semantik des Operators natürlich auch direkt implementieren:
Bruch Bruch::operator *= (Bruch b)
{
// Zähler und Nenner direkt ausmultiplizieren
zaehler *= b.zaehler;
nenner *= b.nenner;

// Objekt, für das die Operation aufgerufen wurde, zurückliefern


return *this;
}
Dies birgt aber die Gefahr der Inkonsistenz. Wenn z.B. die Definition der Multiplikation geändert
wird (z.B. weil zusätzlich gekürzt wird), muss auch die Definition dieser Operation geändert
werden.
Beide Arten der Implementierung sind möglich und haben Vor- und Nachteile. In der Praxis
muss hier deshalb zwischen der Gefahr von Inkonsistenzen und dem Laufzeitnachteil abgewogen
werden. Eine derartige Abwägung ist typisch und tritt beim Design von Klassen (und Software
im Allgemeinen) häufig auf.
Wenn man sich dafür entscheidet, den Operator direkt zu implementieren, sollte man aber
unbedingt darauf achten, dass die multiplikative Zuweisung der Multiplikation mit Zuweisung
entspricht. Dazu gehört auch, dass der Operator *= das Objekt, für das er aufgerufen wird, (also
den ersten Operanden) verändert, der Operator * dies aber nicht tut.

4.2.3 Anwendung von Operatorfunktionen


Nachdem es nun möglich ist, Brüche zu multiplizieren und zu vergleichen, kann diese Fähigkeit
auch in Anwendungsprogrammen eingesetzt werden. Das folgende Programm zeigt eine mögli-
che Anwendung der neuen Version der Klasse Bruch:
// klassen/btest2.cpp
// Headerdateien für die verwendeten Klassen einbinden
#include "bruch.hpp"

int main()
{
Bsp::Bruch x; // Bruch x deklarieren
Bsp::Bruch w(7,3); // Bruch w deklarieren
Sandini Bib

4.2 Operatoren für Klassen 167

// Bruch w ausgeben
w.print();

// x das Quadrat von w zuweisen


x = w * w;

// solange x < 1000


while (x < Bsp::Bruch(1000)) {

// x mit a multiplizieren
x *= w;

// und ausgeben
x.print();
}
}
Mit der Anweisung
x = w * w;
wird dem Bruch x das Quadrat des Bruchs w zugewiesen.
Dies sind zwei Operationen:
 Zunächst wird die Operation * mit zwei Brüchen als Operanden (jeweils w) aufgerufen. Die
Operation liefert, wie in der Klasse deklariert, als Ergebnis einen neuen temporären Bruch
zurück.
 Dieser zurückgelieferte Bruch wird anschließend dem Bruch x zugewiesen. Hier wird wieder
der Default-Zuweisungsoperator verwendet, der komponentenweise zuweist.
Die Schreibweise mit den Operatoren ist nur eine einfachere Art, die in der Spezifikation dekla-
rierte Operatorfunktion aufzurufen.
Eigentlich findet folgender Aufruf statt:
x.operator=(w.operator*(w)); // entspricht: x = w * w;
Für w wird die Elementfunktion operator* mit dem Parameter w aufgerufen. Dies verdeutlicht
erneut den Sachverhalt, dass in der objektorientierten Betrachtungsweise der erste Operand je-
weils das Objekt ist, dem als Nachricht die Aufforderung zu einer Operation mit dem zweiten
Operanden als Parameter gesendet wird. Wir bilden also nicht global das Produkt von zwei Wer-
ten, sondern wenden uns an den ersten Operanden mit der Bitte, das Produkt von sich selbst mit
einem zweiten übergebenen Operanden auszurechnen und zurückzuliefern. Das Ergebnis wird
als Parameter verwendet, der beim Aufruf von operator= für das Objekt x übergeben wird. Wir
wenden uns also an x mit der Bitte, den Wert des als Parameter übergebenen zweiten Operanden
zu übernehmen.
Die Schreibweise mit Angabe von operator kann in C++-Programmen auch wirklich zum
Aufruf von Operatorfunktionen verwendet werden. Wenn man so will, ist die Tatsache, dass statt
w.operator*(w) auch w * w geschrieben werden kann, eine Konzession an die Tatsache, dass
wir es gewöhnt sind, mit Operatoren so umzugehen, und es auch als übersichtlicher empfinden.
Sandini Bib

168 Kapitel 4: Programmieren von Klassen

4.2.4 Globale Operatorfunktionen


Wie alle Funktionen können auch Operatorfunktionen nicht objektorientiert (also als Kompo-
nente einer Klasse), sondern als globale Funktion definiert werden. Wenn der Compiler auf den
Ausdruck
x * y
trifft, hat er grundsätzlich zwei Möglichkeiten der Interpretation:
 eine prozedurale Interpretation als operator*(x,y) und
 eine objektorientierte Interpretation als x.operator*(y).
Die objektorientierte Interpretation hat den Vorteil, dass sie direkt zur Klasse gehört und damit
Zugriff auf private Komponenten von x und, sofern y zur gleichen Klasse gehört, y hat.
Die prozedurale Interpretation gehört nicht zur Klasse. In diesem Fall ist der erste bzw. ein-
zige Operand nicht das Objekt, für das die Operation aufgerufen wird, sondern alle Operanden
sind Parameter. Entsprechend ist dann innerhalb der Operatorfunktion auch kein this definiert.
Da derartige Implementierungen von Operatorfunktionen nicht zur Klasse gehören, können sie
auch später als Hilfsfunktion“ vom Anwender ergänzt werden. Die folgende Operatorfunktion

könnte z.B. vom Anwender der Klasse Bruch als Ergänzung definiert werden:
/* Produkt von int mit Bruch
* - global definiert
*/
Bsp::Bruch operator* (int i, Bsp::Bruch b)
{
return Bsp::Bruch(i) * b;
}
Da es sich nicht um eine Elementfunktion handelt (daran erkennbar, dass Bruch:: vor operator
fehlt), müssen beide Operanden als Parameter angegeben werden. Es handelt sich also um die
zweistellige Operation *, die Multiplikation, die hier für einen Integer als ersten und einen Bruch
als zweiten Operanden definiert wird.
Da die Operation nicht zur Klasse Bruch gehört, besteht natürlich auch kein Zugriff auf
private Komponenten des zweiten Operanden. Es kann also nicht einfach der Zähler von b mit i
multipliziert werden. Deshalb wird der erste Operand i in einen Bruch umgewandelt, und dann
wird die in der Klasse Bruch für zwei Brüche definierte Multiplikation aufgerufen.
Ein weiteres Beispiel zeigt die folgende Definition:
/* Negation
* - global definiert
*/
Bsp::Bruch operator - (Bsp::Bruch b)
{
return b * Bsp::Bruch(-1);
}
Sandini Bib

4.2 Operatoren für Klassen 169

Da es sich auch hier nicht um eine Elementfunktion handelt (Bruch:: fehlt), ist der angegebene
Parameter der einzige Operand. Es handelt sich also um die einstellige Operation -, die Negation,
die hier für einen Bruch definiert wird.
Da auch diese Operation nicht zur Klasse gehört, besteht natürlich auch hier kein Zugriff auf
private Komponenten des Operanden. Es kann also nicht einfach der Zähler von b mit -1 mul-
tipliziert werden. Deshalb wird der ganze Bruch mit Hilfe der für Brüche öffentlich definierten
Multiplikation mit einer in einen Bruch umgewandelten -1 multipliziert.
Anwendbar wären die Operationen z.B. wie folgt:
Bsp::Bruch x, y;
...
y = 3 * -x;
Der Bruch x wird mit der global definierten Negation negiert und als zweiter Operand mit dem
Integer 3 multipliziert. Das Ergebnis wird dem Bruch y zugewiesen.

4.2.5 Grenzen bei der Definition eigener Operatoren


Die bisher vorgestellten Beispiele zeigten die Möglichkeit, eigene Operatoren zu definieren. Bei
der Definition von Operatorfunktionen gibt es aber allerdings Einschränkungen. So ist es z.B.
nicht möglich, jeden beliebigen Operator für eigene Klassen zu definieren.
Im Einzelnen bleibt zu selbst definierten Operatoren deshalb Folgendes festzuhalten:
 Die Menge möglicher Operatoren ist festgelegt und kann nicht erweitert werden. So ist es
z.B. nicht möglich, einen neuen Operator **“ zum Potenzieren zu definieren.

Diese Einschränkung erfolgte aus mehreren Gründen. Zum einen erleichtert sie die Arbeit
der Compiler erheblich. Zum anderen hätte man eine Regelung für die Festlegung von Syntax,
Priorität usw. von neuen Operatoren gebraucht, wofür es keine triviale Lösung gibt.
 Die Priorität, die Syntax und die Auswertungsreihenfolge der Operatoren sind ebenfalls fest-
gelegt und können nicht geändert werden. So hat die Multiplikation immer eine höhere Prio-
rität als die Addition und wird von rechts nach links ausgewertet. Ein einstelliger Operator =
kann nicht definiert werden.
Dies ist sicherlich von Vorteil, da dadurch der Programm-Code für Objekte fremder Klas-
sen bezüglich der Auswertung von Ausdrücken besser nachzuvollziehen ist. Außerdem wer-
den dadurch die Anforderungen an einen Compiler vereinfacht.
 Die für die fundamentalen Datentypen vordefinierten Operationen werden nicht automatisch
auf abstrakte Datentypen übertragen. So ist z.B. mit der Multiplikation und der Zuweisung
nicht automatisch der Operator *= definiert, sondern er muss, wie am Beispiel der Klasse
Bruch gezeigt, explizit definiert werden.
Das bedeutet aber auch, dass die für fundamentale Datentypen geltende Eigenschaft
x *= a entspricht x = x * a“ nicht automatisch für Klassen gilt, da die Operatoren auch

anders implementiert werden können. Dies sollte aus Gründen der Lesbarkeit natürlich unbe-
dingt vermieden werden.
Sandini Bib

170 Kapitel 4: Programmieren von Klassen

 Die Operatoren für die fundamentalen Datentypen (int, char, Zeiger etc.) sind festgelegt
und können weder umdefiniert noch erweitert werden. Allerdings kann die Verknüpfung ei-
nes abstrakten Datentyps mit einem fundamentalen Datentyp als globale Funktion oder als
Elementfunktion mit den hier angegebenen Einschränkungen frei definiert werden.
 Folgende Operatoren können nicht für eigene Datentypen überladen werden:
.
::
sizeof
.*
?:

Ein Überladen ist nicht erlaubt, da die Operatoren bereits eine vordefinierte Bedeutung für
alle Objekte besitzen oder, wie im Fall des Operators ?:, die Möglichkeit des Überladens
einfach nicht als lohnenswert erachtet wurde (?: ist der einzige dreistellige Operator).
Der Operator .* wird in Abschnitt 9.4 vorgestellt.
Auf Seite 571 befindet sich eine Liste aller C++-Operatoren.

4.2.6 Besonderheiten spezieller Operatoren


Für einige spezielle Operatoren gelten Besonderheiten, die im Folgenden aufgezeigt werden.

Der Zuweisungsoperator =
Für jede Klasse ist ein Default-Zuweisungsoperator definiert. Er weist komponentenweise zu und
liefert das Objekt zurück, dem der neue Wert zugewiesen wurde. Falls das nicht sinnvoll ist, kann
der Zuweisungsoperator für eine Klasse auch selbst definiert werden.
Soll keine Zuweisung möglich sein, muss der Zuweisungsoperator einfach nur privat dekla-
riert werden. Die folgende Deklaration würde z.B. eine Zuweisung zweier Brüche unmöglich
machen:3
class Bruch {
...
private:
// Zuweisung verboten, da privat
Bruch operator = (Bruch);
};
Da eine komponentenweise Zuweisung bei Klassen mit Zeigern als Komponenten meistens feh-
lerhaft ist, muss der Zuweisungsoperator für solche Klassen selbst definiert werden. Detailliertere
Informationen und Beispiele dazu befinden sich in Abschnitt 6.1.5.

3
Der Zuweisungsoperator sollte eigentlich mit einer anderen Syntax definiert werden, wozu uns aber hier
noch das Sprachmittel der Referenz fehlt. Auf Seite 366 befindet sich die korrekte Syntax.
Sandini Bib

4.2 Operatoren für Klassen 171

Inkrement- und Dekrement-Operatoren


Die einstelligen Inkrement- und Dekrement-Operatoren ++ und -- gibt es für die fundamentalen
Datentypen jeweils in zwei Versionen, als Präfix- und als Postfix-Notation (siehe auch Seite 42).
In der Präfix-Notation
++x
liefert der Ausdruck ++x bei fundamentalen Datentypen den Wert von x nach der Inkrementie-
rung (erst wird erhöht, dann wird der Wert geliefert). In der Postfix-Notation
x++
liefert der Ausdruck x++ bei fundamentalen Datentypen den Wert von x vor der Inkrementierung
(erst wird der Wert geliefert, dann wird erhöht).
Dieser Unterschied wird z.B. gern bei einem Zugriff auf ein Element in einem Feld (Array)
angewendet:
x = elems[i++];
In diesem Beispiel wird der Index i inkrementiert, und wird x der Wert des Elements mit dem
Index i vor der Inkrementierung zugewiesen.
Diese Unterscheidung kann auch für eigene Datentypen implementiert werden. Da es sich
in beiden Fällen aber um einstellige Operatoren handelt, wurde zur Unterscheidung festgelegt,
dass der Postfix-Operator im Gegensatz zum Präfix-Operator grundsätzlich mit einem Dummy-
Parameter vom Typ int deklariert werden muss.
Die nachfolgende Deklaration definiert z.B. einen Präfix- und einen Postfix-Inkrement-Ope-
rator für die Klasse X:
class X {
...
public:
operator ++ (); // Präfix-Notation (++x)
operator ++ (int); // Postfix-Notation (x++)
};
Analog zu fundamentalen Datentypen sollten beide Implementierungen ein Objekt in irgendeiner
Weise inkrementieren. Der Unterschied sollte auch hier nur im Rückgabewert der Operation
bestehen.
Für den Dekrement-Operator gilt Entsprechendes.

Der Indexoperator [ ]
Auch der Indexoperator4 [ ], mit dem über einen Index auf ein bestimmtes Element innerhalb ei-
nes Feldes zugegriffen werden kann, kann für eigene Klassen definiert werden. Dadurch erhalten
solche Objekte einen Feld-Charakter, obwohl es gar keine Felder sind.
Der Indexoperator ist ein zweistelliger Operator, wobei der übergebene Index der zweite
Operand ist. Bei Elementfunktionen wird der Index also als Parameter übergeben.

4
Er wird auch als Indizierungsoperator oder Subskriptionsoperator bezeichnet.
Sandini Bib

172 Kapitel 4: Programmieren von Klassen

Ein typisches Beispiel dafür sind Klassen für Mengen, in denen dann mit [ ] auf das i-te Element
in der Menge zugegriffen werden kann. Die folgende Deklaration definiert z.B. den Operator [ ]
für eine Klasse, deren konkrete Objekte (Instanzen) jeweils eine Menge von Personen sind:
class MengeVonPersonen {
...
public:
// i-tes Element aus der Menge (also eine Person) zurückliefern
Person operator [] (int i);
...
};
Auf ein Objekt der Klasse MengeVonPersonen kann dann mit dem Operator [ ] zugegriffen
werden, womit dann ein Element der Menge, also eine Person, angesprochen wird:
void f (MengeVonPersonen alleAngestellten)
{
Person p;

// erstes Element aus Menge alleAngestellten an person zuweisen


person = alleAngestellten[0];
...
}
Da der Typ des Parameters frei definierbar ist, kann der Index einen beliebigen Datentyp be-
sitzen. Durch die Möglichkeit des Überladens kann der Operator auch für verschiedene Index-
Datentypen unterschiedlich implementiert werden. Auf diese Weise können so genannte asso-
ziative Felder (assoziative Arrays) implementiert werden.
So wäre es beispielsweise auch möglich, beim Zugriff auf die Menge von Personen mit dem
Operator [ ] als Index keinen ganzzahligen Wert, sondern den Namen der Person zu übergeben,
die zurückgeliefert werden soll. Die entsprechende Deklaration sieht dann wie folgt aus:
class MengeVonPersonen {
...
public:
// i-tes Element aus der Menge (also eine Person) zurückliefern
Person operator [] (int i);

// Person zu übergebenem Namen aus der Menge zurückliefern


Person operator [] (std::string name);
...
};
Damit ist dann folgender Aufruf möglich:
Sandini Bib

4.2 Operatoren für Klassen 173

void f (MengeVonPersonen alleAngestellten)


{
Person p;

// Element "nico" aus Menge alleAngestellten an person zuweisen


person = alleAngestellten["nico"];
...
}
In Abschnitt 6.2.1 wird ein Beispiel für die Definition eines Indexoperators vorgestellt.

Die Zeiger-Operatoren * und ->


Genauso wie es sinnvoll sein kann, Objekte zu definieren, die sich wie Felder (Arrays) verhalten,
kann es sinnvoll sein, Objekte zu definieren, die sich wie Zeiger verhalten. Da man die Semantik
der Operationen frei definieren kann, kann man derartige zeigerartige Objekte mit gewisser In-
telligenz versehen. Aus diesem Grund werden derartige Objekte auch Smart-Pointer genannt. In
Abschnitt 9.2.1 wird darauf genauer eingegangen.

Der Aufruf-Operator ()
Auch der Funktionsaufruf ist ein Operator, der für eigene Klassen definiert werden kann. Diese
sicherlich etwas überraschende Tatsache auszunutzen, hat allerdings seltsame Konsequenzen.
Seine Deklaration sieht wie folgt aus:
class X {
...
public:
void operator () (); // Operator () ohne Parameter deklarieren
}
Mit einer solchen Deklaration ist folgender Aufruf möglich:
void f()
{
X a;

a(); // NEIN, nicht Funktion a(), sondern


// Operation () für Objekt a aufrufen
}
Es sieht so aus, als würde eine globale Funktion a() aufgerufen; es handelt sich aber um den
Aufruf des Operators () für das Objekt a.
Dies ist sicherlich so verwirrend, dass es nicht sinnvoll erscheint, von der Möglichkeit, diesen
Operator zu definieren, Gebrauch zu machen. Es kann aber Sinn machen, Objekte zu definieren,
die sich wie Funktionen verhalten. Derartige Objekte nennt man auch Funktionsobjekte, oder
kurz Funktoren. Sie werden auch in der C++-Standardbibliothek verwendet. Darauf wird in Ab-
schnitt 9.2.2 eingegangen.
Sandini Bib

174 Kapitel 4: Programmieren von Klassen

4.2.7 Zusammenfassung
 Für die Objekte einer Klasse können Operatoren definiert werden. Sie werden mit dem
Schlüsselwort operator als Operatorfunktionen deklariert.
 Operatoren können für Klassen überladen werden.
 Die Menge möglicher Operatoren, ihre Priorität, Syntax und Auswertungsreihenfolge ist fest
definiert und kann nicht geändert werden.
 Operatoren sollten immer so implementiert werden, dass sie tun, was man grundsätzlich von
ihnen erwartet. Das entspricht in der Regel dem, was mit ihnen bei fundamentalen Datentypen
verbunden wird.
Wenn Operatoren für eigene Klassen definiert werden, sollten z.B. folgende Beziehungen
weiterhin gelten:
x + y verändert weder x noch y
x - y entspricht x + (-y)
x op= y entspricht x = x op y
x = x + 1 entspricht x += 1 entspricht ++x
p->k entspricht (*p).k entspricht p[0]
 In einer Elementfunktion bezeichnet this einen Zeiger auf das Objekt, für das die Funktion
aufgerufen wurde. *this ist also immer das Objekt, für das eine Elementfunktion jeweils
aufgerufen wurde. Bei Operatoren, die als Elementfunktion definiert werden, ist dies der
erste bzw. einzige Operand.
Auf Seite 571 befindet sich eine Liste aller C++-Operatoren.
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 175

4.3 Laufzeit- und Codeoptimierungen


In den bisherigen Abschnitten wurde gezeigt, wie eine Klasse implementiert werden kann, die
bestimmten Anforderungen genügen soll (erzeugen, multiplizieren, ausgeben etc.). Das Was“

(Was soll eine Klasse leisten?) stand im Vordergrund der Überlegungen, die zu den ersten Ver-
sionen führten. In diesem und dem folgenden Abschnitt wird nun der Blick auf das Wie“ gelenkt

(Wie leistet es die Klasse?).
Dabei steht vor allem ein Aspekt im Vordergrund, der bereits bei der Verbreitung von C eine
nicht unerhebliche Rolle gespielt hat: das Laufzeitverhalten. Auch in C++ hat das Laufzeitverhal-
ten eine sehr hohe Priorität. So gibt es einige Sprachmittel, die aus objektorientierter Sicht nicht
notwendig sind und nur dazu dienen, die Laufzeit zu verbessern. Ihr Einsatz kann im Zweifelsfall
auch auf Kosten objektorientierter Konzepte gehen.
Zur Verbesserung der Laufzeit wird in diesem Abschnitt zunächst auf Inline-Funktionen und
Deklarationen im Block eingegangen. In Abschnitt 4.4 wird dann ein weiteres wesentliches
Sprachmittel zur Laufzeitverbesserung, nämlich Referenzen, eingeführt.
Zusätzlich werden in diesem Abschnitt Default-Argumente und Using-Anweisungen vorge-
stellt. Beide Sprachmittel dienen allerdings weniger zur Verbesserung der Laufzeit (eher im Ge-
genteil), sondern zur Einsparung von Quellcode.

4.3.1 Die Klasse Bruch mit ersten Optimierungen


In der neuen Version der Klasse Bruch wird Folgendes geändert:
 Die drei Konstruktoren werden mit Hilfe von Default-Argumenten für den Zähler und für den
Nenner zu einer Funktion zusammengefasst.
 Zwei Funktionen (print() und operator*()) werden als so genannte Inline-Funktionen in
der Headerdatei nicht nur deklariert, sondern auch gleich implementiert.
Damit kann der Compiler Code generieren, der die Funktionen nicht aufruft, sondern
die Anweisungen der Funktionen übernimmt. Da Funktionsaufrufe Zeit kosten, kann dies
Laufzeit sparen.
 Da es keine Namenskonflikte gibt, werden bei der Verwendung der Klasse Bruch alle Sym-
bole im Namensbereich Bsp als global definiert. Dies erspart es, Bsp:: jedes Mal angeben
zu müssen.

Headerdatei
Die Headerdatei der Klasse Bruch erhält mit der Einführung der neuen Sprachmittel nun insge-
samt folgenden Aufbau:
// klassen/bruch3.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>
Sandini Bib

176 Kapitel 4: Programmieren von Klassen

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse Bruch
*/
class Bruch {

private:
int zaehler;
int nenner;

public:
/* neu: Default-Konstruktor, Konstruktor aus Zähler und
* Konstruktor aus Zähler und Nenner in einem
*/
Bruch (int = 0, int = 1);

/* Ausgabe
* - neu: inline definiert
*/
void print () {
std::cout << zaehler << '/' << nenner << std::endl;
}

// Multiplikation
Bruch operator * (Bruch);

// multiplikative Zuweisung
Bruch operator *= (Bruch);

// Vergleich
bool operator < (Bruch);
};

/* Operator *
* - neu: inline definiert
*/
inline Bruch Bruch::operator * (Bruch b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 177

*/
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP

Bevor auf die Änderungen im Einzelnen eingegangen wird, soll noch die dazugehörige neue
Quelldatei vorgestellt werden.

Quelldatei
In der Quelldatei der Klasse Bruch wird entsprechend den Änderungen in der Headerdatei nur
noch ein Konstruktor definiert. Die Definition der beiden inline definierten Operatoren entfällt:
// klassen/bruch3.cpp
// Headerdatei der Klasse einbinden
#include "bruch.hpp"

// Standard-Headerdateien einbinden
#include <cstdlib>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* neu: Default-Konstruktor, Konstruktor aus ganzer Zahl und


* Konstruktor aus Zähler und Nenner in einem
* - Default für z: 0
* - Default für n: 1
*/
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
if (n < 0) {
zaehler = -z;
Sandini Bib

178 Kapitel 4: Programmieren von Klassen

nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

/* Operator *=
*/
Bruch Bruch::operator *= (Bruch b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}

/* Operator <
*/
bool Bruch::operator < (Bruch b)
{
// da die Nenner nicht negativ sein können, reicht:
return zaehler * b.nenner < b.zaehler * nenner;
}

} // **** ENDE Namespace Bsp ********************************


Die in der Deklaration und in der Implementierung erfolgten Änderungen der Klasse Bruch
werden in den folgenden Abschnitten erläutert.

4.3.2 Default-Argumente
In der neuen Version der Klasse Bruch werden die bisher verwendeten drei Konstruktoren zu
einer Funktion zusammengefasst: Für den Fall, dass der zweite Parameter (der Nenner) nicht
übergeben wird, wird der Default-Wert 1 verwendet. Wird auch der erste Parameter (der Zähler)
nicht übergeben, wird dafür 0 angenommen:
class Bruch {
...
Bruch (int = 0, int = 1);
...
};
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 179

Die angegebenen Default-Werte werden vom Compiler automatisch eingesetzt, wenn zu wenig
Parameter übergeben werden. Da dies bereits beim Aufruf geschieht, ist es für die Implementie-
rung der Funktion unerheblich, woher die Parameter kommen. Die Implementierung geht deshalb
von zwei Parametern aus:
Bruch::Bruch (int z, int n)
{
...
}
Damit eine eindeutige Zuordnung möglich ist, dürfen Default-Werte nur am Ende einer Parame-
terliste definiert werden. Beim Aufruf werden die übergebenen Argumente den Parametern der
Reihe nach zugewiesen. Das erste Argument ist in jedem Fall der erste Parameter, das zweite
Argument ist der zweite Parameter und so weiter. Sind alle Argumente aufgebraucht, erhalten
die restlichen Parameter ihre Default-Werte. Es ist also eindeutig definiert, dass es sich bei der
Übergabe von nur einem Argument immer um den ersten Parameter handelt. Selbstverständlich
können auch globale Funktionen Default-Werte besitzen.
Die folgende globale Funktion max() liefert z.B. das Maximum von zwei bis vier vorzei-
chenfreien ganzen Zahlen:
unsigned max (unsigned, unsigned, unsigned = 0, unsigned = 0);
Beim Aufruf der Funktion mit zwei Argumenten wird für die letzten beiden Parameter jeweils
der Default-Wert 0 eingesetzt.
Bei der Deklaration von Default-Argumenten muss beachtet werden, dass ein Leerzeichen
vor dem Zuweisungsoperator steht, wenn es sich bei einem Parameter um einen Zeiger handelt,
void f (char* = "hallo"); // OK
Ansonsten wird angenommen, dass der Operator *= verwendet wird, und es wird eine entspre-
chende Fehlermeldung ausgegeben:
void f (char*= "hallo"); // FEHLER
Es ist nicht erlaubt, Default-Argumente in einer zweiten Deklaration neu zu definieren. Für selbst
definierte Operatoren sind Default-Argumente nicht zugelassen.
Der Datentyp einer Funktion wird durch die Tatsache, dass manche Parameter Default-Werte
besitzen, nicht beeinflusst. Ein Zeiger auf die Funktion max() müsste als Zeiger auf eine Funk-
tion deklariert werden, der vier unsigneds übergeben werden:
unsigned (* funkzeiger) (unsigned, unsigned, unsigned, unsigned);
Um auch hier wieder Default-Werte verwenden zu können, müssen diese wiederum definiert
werden:
unsigned (* funkzeiger) (unsigned, unsigned,
unsigned = 0, unsigned = 0);
Wenn eine Funktion auch über einen Funktionszeiger aufgerufen werden kann, können zwar
unterschiedliche Default-Werte verwendet werden – man sollte dies aber nie tun. Ein direkter
Aufruf von max() mit zwei Parametern führt sonst zu einem anderen Ergebnis als der Aufruf
über den Funktionszeiger, was sehr verwirrend sein kann.
Sandini Bib

180 Kapitel 4: Programmieren von Klassen

Default-Argumente können Laufzeit kosten


Eines sollte jedoch beachtet werden: Default-Argumente fassen mehrere Funktionen zusammen,
ersparen Schreibarbeit, führen zu weniger Programm-Code und erhöhen die Konsistenz (Ände-
rungen müssen nur an einer Stelle durchgeführt werden). Die Laufzeit kann sich durch Default-
Argumente aber verlängern. Wird die Funktion max() z.B. mit zwei Argumenten aufgerufen,
werden dennoch alle vier Parameter verglichen.
Die neue Version der Klasse Bruch liefert dafür ebenfalls ein Beispiel. Wird der Konstruk-
tor ohne Argumente aufgerufen, wird der dann für den Nenner eingesetzte Default-Wert, obwohl
dies nicht notwendig ist, auf 0 und einen negativen Wert geprüft. Um einen solchen Laufzeitnach-
teil zu vermeiden, dürfen nur zwei Konstruktoren der Klasse Bruch zusammengefasst werden:
class Bruch {
...
public:
/* Default-Konstruktor und Konstruktor für ganzzahligen Wert
* - inline deklariert
*/
Bruch (int z = 0)
: zaehler(z), nenner(1)
{
}

/* Konstruktor aus Zähler und Nenner


* - mit Sonderbehandlung für Nenner <= 0
*/
Bruch (int, int);
...
};

4.3.3 Inline-Funktionen
Funktionen können inline deklariert werden. Das Wort inline“ bedeutet dabei, dass ein Funk-

tionsaufruf durch die Anweisungen der aufgerufenen Funktion ersetzt werden kann. Damit dies
möglich ist, werden bei der Deklaration in der Headerdatei auch gleich die entsprechenden An-
weisungen definiert.
Inline-Funktionen können auf zweierlei Arten definiert werden:
 Entweder wird die Funktion mit dem Schlüsselwort inline in der Headerdatei definiert:
class Bruch {
...
bool operator < (Bruch);
...
};
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 181

inline bool Bruch::operator < (Bruch b)


{
return zaehler * b.nenner < b.zaehler * nenner;
}

 oder die Funktion wird bei der Deklaration direkt in der Klassenstruktur implementiert:
class Bruch {
...
void print () {
std::cout << zaehler << '/' << nenner << std::endl;
}
...
};

In beiden Fällen wird damit dem Compiler die Möglichkeit gegeben, statt eines Funktionsaufrufs
Code zu generieren, der die Anweisungen in der Funktion direkt ausführt. Ein Aufruf wie
b.print()
kann der Compiler also als
std::cout << b.zaehler << '/' << b.nenner << std::endl;
übersetzen, da er weiß, dass dies semantisch den gleichen Effekt hat.
Inline-Funktionen ersetzen damit die in C üblichen Makros und haben einen wichtigen Vor-
teil: Inline-Funktionen sind kein blinder“ Textersatz, sondern besitzen semantisch betrachtet

keinen Unterschied zu normalen Funktionen. So findet genauso wie bei Funktionen eine Typ-
überprüfung statt, und es existieren genau die gleichen Gültigkeitsbereiche. Auch sind keine
Seiteneffekte durch eine fehlende Klammerung oder durch doppelten Ersatz eines Parameters
wie n++ möglich. Für die Semantik eines Programms ist es deshalb absolut unerheblich, ob
Funktionen inline deklariert werden oder nicht. Lediglich die Laufzeit wird dadurch beeinflusst.
Aus diesem Grund steht es auch jedem Compiler frei, Inline-Funktionen doch nicht inline
zu generieren oder Nicht-Inline-Funktionen inline zu generieren. Ein guter Compiler kann selbst
entscheiden, ob es sich lohnt, für einen Funktionsaufruf Code zu generieren, der den Aufruf
tatsächlich durchführt oder die Anweisungen der Funktion direkt ausführt. Oft kann dies auch
über Compiler-Optionen beeinflusst werden.
Um eine Ersetzung vornehmen zu können, muss allerdings die Implementierung der Funk-
tion bereits beim Kompilieren des Funktionsaufrufs bekannt sein. Aus diesem Grund müssen
Inline-Funktionen, die in mehr als einer Quelldatei verwendet werden sollen, in der Headerdatei
implementiert werden. Damit erklärt sich auch, weshalb das Schlüsselwort inline überhaupt
angegeben werden muss: Es verhindert die Fehlermeldung, dass eine Funktion aufgrund mehr-
facher Einbindung der Headerdatei in verschiedenen Quelldateien mehrfach definiert ist.
Die Tatsache, dass Inline-Funktionen in Headerdateien implementiert werden, hat allerdings
auch Nachteile. So bietet sich z.B. die Möglichkeit zur Manipulation der Implementierung ei-
ner Inline-Funktion. Diese Möglichkeit besteht bei einer Implementierung in einer kompilierten
Quelldatei nicht. Hier zeigt sich bereits, dass in C++ ein Laufzeitvorteil zu Lasten streng ob-
Sandini Bib

182 Kapitel 4: Programmieren von Klassen

jektorientierter Konzepte gehen kann. Implementierungsdetails in der Klassenspezifikation un-


terzubringen, kann man aus streng objektorientierter Sicht als fragwürdig betrachten. Dies ist ein
Punkt, der immer wieder zu heftigen Diskussionen Anlass gibt. Für die einen disqualifiziert sich
C++ damit als objektorientierte Sprache, für die anderen ist der Laufzeitvorteil das, was C++ erst
interessant macht.

4.3.4 Optimierungen aus Anwendersicht


Für ein Anwendungsprogramm haben die in diesem Kapitel vorgestellten Änderungen an der
Implementierung der Klasse Bruch keine Bedeutung. Es kann weiterhin die auf Seite 166 vor-
gestellte Version verwendet werden.
Hier soll aber dennoch ein leicht abgeändertes Anwendungsprogramm vorgestellt werden,
anhand dessen die Möglichkeit aufgezeigt wird, Deklarationen inmitten von Blöcken vorzuneh-
men und Angaben von Namensbereichen nur einmal machen zu müssen:
// klassen/btest3.cpp
// Headerdateien für die verwendeten Klassen einbinden
#include "bruch.hpp"

int main()
{
using namespace Bsp; // neu: alle Symbole des Namensbereichs std sind global

Bruch w(7,3); // Bruch w deklarieren

w.print(); // Bruch w ausgeben

// neu: x deklarieren und mit dem Quadrat von w initialisieren


Bruch x = w*w;

// solange x < 1000


while (x < Bruch(1000)) {
// x mit w multiplizieren und ausgeben
x *= w;
x.print();
}
}
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 183

4.3.5 Using-Direktiven
Namensbereiche wurden eingeführt, um Namenskonflikte zu vermeiden und es deutlich zu ma-
chen, wenn mehrere Symbole zusammengehören. Dabei können diese Symbole auch auf mehrere
Dateien verteilt werden. Mit Hilfe des Schlüsselworts using kann man allerdings die mitunter
ermüdende immer wiederkehrende Angabe des Namensbereiches vermeiden. Dabei gibt es zwei
Möglichkeiten:
 eine Using-Deklaration
 eine Using-Direktive

Using-Deklaration
Mit einer Using-Deklaration wird ein Name aus einem Namensbereich lokal zugreifbar. Durch
die Angabe
using Bsp::Bruch;
wird Bruch im aktuellen Gültigkeitsbereich zu einem lokalen Synonym für Bsp::Bruch.
Derartige Deklarationen entsprechen einer Deklaration lokaler Variablen und können auch
globale Variablen überdecken. Beispiel:
namespace X { // Namensbereich X
int i, j, k;
}

int k; // globales k

void usingDeklarationen()
{
int j = 0;
using X::i; // Name i aus Namensbereich X lokal zugreifbar
using X::j; // FEHLER: j ist in f2() zweimal deklariert
using X::k; // überdeckt globales k

++i; // X::i
++k; // X::k
}

Using-Direktiven
Eine Using-Direktive ermöglicht es, alle Namen eines Namensbereiches ohne Qualifizierung
anzusprechen. Alle Symbole, die sich in diesem Namensbereich befinden, sind damit auch global
definiert, was entsprechende Konflikte auslösen kann. Durch die Direktive
using namespace Bsp;
werden alle Symbole des Namensbereichs Bsp im aktuellen Gültigkeitsbereich zu globalen Va-
riablen.
Sandini Bib

184 Kapitel 4: Programmieren von Klassen

Befindet sich im globalen Gültigkeitsbereich bereits ein Symbol, das sich auch im global gewor-
denen Namensbereich befindet, wird bei dessen Verwendung eine Fehlermeldung ausgegeben.
Beispiel:
namespace X { // Namensbereich X
int i, j, k;
}

int k; // globales k

void usingDirektive()
{
int j = 0;
using namespace X; // Namensbereich X global zugreifbar
++i; // X::i
++j; // lokales j
++k; // FEHLER: X::k oder globales k?
}
Man beachte, dass man Using-Direktiven nie in einem Kontext verwenden sollte, in dem nicht
klar ist, welche Symbole bekannt sind. Eine Using-Direktive kann ansonsten dazu führen, dass
sich die Sichtbarkeit anderer Symbole ändert, was zu Mehrdeutigkeiten und sogar zu geänder-
tem Verhalten führen kann (ein Funktionsaufruf findet plötzlich eine ganz andere Funktion als
ohne die Direktive). Generell ist die Verwendung von Using-Direktiven deshalb mit Vorsicht zu
genießen. Insbesondere deren Verwendung in Headerdateien ist untragbar schlechtes Design.

Koenig-Lookup
Auch ohne die Verwendung von using ist es nicht immer notwendig, den Namensbereich eines
Symbols mit anzugeben. Sofern man eine Operation mit Argumenten aufruft, wird die Opera-
tion auch in allen Namensbereichen der übergebenen Argumente gesucht. Diese Regel wird als
Koenig-Lookup bezeichnet. Beispiel:
#include "bruch.hpp"

// ergänzende Definition von globalen Operationen in Namensbereich Bsp


namespace Bsp {
void hilfsfunktion (Bruch);
}
...

Bsp::Bruch x;
...
hilfsfunktion(x); // OK, Bsp::hilfsfunktion() wird gefunden
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 185

4.3.6 Deklarationen zwischen Anweisungen


In der neuen Version des Anwendungsprogramms ist außerdem bemerkenswert, dass die Dekla-
ration von x nicht am Anfang, sondern erst mitten im Anweisungsblock, nachdem bereits einige
Anweisungen durchgeführt wurden, stattfindet:
w.print(); // Anweisung

Bruch x = w*w; // Deklaration mit Initialisierung


In C++ ist es möglich, dass Variablen an beliebiger Stelle in einem Block deklariert werden. Ihr
Gültigkeitsbereich reicht dann von der Deklaration bis zum Blockende.
Eingeführt wurde dies, um zu vermeiden, dass Variablen unter Umständen zu einem Zeit-
punkt deklariert werden müssen, zu dem sie noch nicht initialisiert werden können. Wenn zum
Anlegen eines Objekts ein Parameter gebraucht wird, der erst noch ermittelt werden muss, kann
die Deklaration aufgeschoben werden, bis die entsprechenden Anweisungen durchgeführt wur-
den.
Die Alternative wäre ein falsches“ Initialisieren etwa mit dem Default-Konstruktor, wodurch

das Objekt zwar initialisiert wird, diese Initialisierung später aber wieder korrigiert werden muss.
Dies kostet vor allem bei größeren Objekten unnötig Zeit. Und wenn kein Default-Konstruktor
definiert ist, könnte gar kein Objekt angelegt werden. Man kann außerdem Klassen definieren, de-
ren Objekte ihren Wert oder Teile ihrer Daten nicht modifizieren können. Diese Objekte können
erst dann erzeugt werden, wenn ihr Initialwert bekannt ist.
Diese Art der Deklaration birgt natürlich die Gefahr, dass Programme schwerer lesbar wer-
den, da die Deklaration von Variablen jetzt nicht mehr auf den Beginn der geöffneten Blöcke
beschränkt bleibt. Aus diesem Grund sollte von der Möglichkeit, Deklarationen nicht am An-
fang eines Blocks vorzunehmen, nur im Ausnahmefällen Gebrauch gemacht werden. Es sollte
sich zumindest immer um den Beginn eines logischen Abschnitts handeln.

Deklarationen im Kopf der For-Schleife


Eine in der Praxis häufig vorkommende Anwendung von Deklarationen mitten im Code ist die
For-Schleife. Sie lässt sich in C++ so implementieren, dass die Laufvariable erst im Schleifen-
kopf deklariert wird:
for (int i=0; i<anz; i++) {
...
}
i ist dabei nur innerhalb der For-Schleife bekannt. Eine zweite Schleife mit einer weiteren De-
klaration von i wäre problemlos. Die hier gezeigte For-Schleife ist also gleichbedeutend mit
folgender Implementierung:
{
int i;
for (i=0; i<anz; i++) {
...
}
}
Sandini Bib

186 Kapitel 4: Programmieren von Klassen

Eine derartige Deklaration hat in der Praxis aber (noch) ein kleines Problem: In alten C++-
Versionen war i noch bis zum Ende des Blocks, in dem sich die gesamte For-Schleife befindet,
bekannt. i war also auch noch nach der For-Schleife deklariert. Der Schleifenkopf zählte nämlich
nicht zu dem Block, der den Schleifenkörper enthält. Eine zweite For-Schleife im gleichen Block
konnte somit nicht genauso implementiert werden:
for (int i=0; i<anz; i++) {
...
}
...
for (int i=0; i<anz; i++) { // FEHLER: i zum zweiten Mal deklariert
...
}
Diese sicherlich unschöne Eigenschaft wurde im Rahmen der Standardisierung von C++ inzwi-
schen korrigiert. In Bedingungen von If-, Switch-, While- und For-Anweisungen dürfen Deklara-
tionen erscheinen, die dann aber nur für den Gültigkeitsbereich dieser Anweisung gelten. Für die
gesamte Anweisung wird sozusagen ein eigener Block eröffnet. Aufgrund der relativen späten
Änderung der Sprache gibt es aber leider immer noch Systeme, für die i auch nach der Schlei-
fe noch bekannt ist, was bei der Implementierung portablen Codes unter Umständen beachtet
werden muss.

4.3.7 Copy-Konstruktoren
Im Beispielprogramm wird der Bruch x bei seiner Deklaration auch gleich mit dem temporären
Bruch, der sich aus der Operation a * a ergibt, initialisiert:
Bruch w(7,3);
...
Bruch x = w*w; // Deklaration und Initialisierung von x
Diese Schreibweise zur Initialisierung von x ist gleichbedeutend mit der Schreibweise
Bruch x(w*w); // Deklaration und Initialisierung von x
und bedeutet, dass x mit einem Bruch (dem temporären Ergebnis von w*w) als Argument angelegt
wird. Bei der Erzeugung von x wird also ein Konstruktor aufgerufen, der als Parameter wiederum
einen Bruch enthält.
Einen solchen Konstruktor, der ein Objekt anhand eines existierenden Objekts gleichen Typs
erzeugt, nennt man Copy-Konstruktor, da er im Prinzip eine Kopie eines existierenden Objekts
anlegt.
Wie der Zuweisungsoperator ist auch dies eine Funktion, die nicht explizit deklariert werden
muss, da sie für jede Klasse automatisch definiert wird. Der Default-Copy-Konstruktor kopiert
komponentenweise. Jede Komponente des zu erzeugenden Objekts wird mit der entsprechenden
Komponente des als Parameter übergebenen Objekts initialisiert. Ist eine solche Komponente
selbst ein Objekt einer Klasse, so wird auch dafür der Copy-Konstruktor aufgerufen. In unserem
Sandini Bib

4.3 Laufzeit- und Codeoptimierungen 187

Beispiel werden also der Zähler und der Nenner von x mit dem Zähler und dem Nenner vom
Ergebnis von w*w initialisiert.
Auch hier kann es sein, dass ein komponentenweises Kopieren zum Anlegen von Kopien
nicht geeignet ist. Dies ist typischerweise bei Verwendung von Zeigern als Komponenten der
Fall. Deshalb kann und muss der Copy-Konstruktor für bestimmte Zwecke manchmal selbst
definiert werden. In Abschnitt 6.1.3 wird darauf eingegangen.

Copy-Konstruktor und Zuweisung


Man beachte, dass es in C++ einen Unterschied zwischen einer Deklaration mit gleichzeitiger
Initialisierung und einer Deklaration ohne Initialisierung mit späterer Zuweisung gibt.
Die Initialisierung
Bsp::Bruch tmp;
...
Bsp::Bruch x = tmp; // Copy-Konstruktor
bzw.
Bsp::Bruch tmp;
...
Bsp::Bruch x(tmp); // Copy-Konstruktor
ist etwas völlig anderes als die Zuweisung:
Bsp::Bruch tmp;
...
Bsp::Bruch x; // Default-Konstruktor
x = tmp; // Zuweisung
Im ersten Fall wird nämlich der Copy-Konstruktor aufgerufen, während im zweiten Fall zunächst
der Default-Konstruktor und danach der Zuweisungsoperator aufgerufen wird.
Prinzipiell kann es sogar sein, dass x im ersten Fall am Ende einen anderen Zustand be-
sitzt als im zweiten Fall, da Konstruktoren und Zuweisungsoperator selbst implementiert werden
und somit zu verschiedenen Ergebnissen führen können. Aus Gründen der Lesbarkeit sollte dies
natürlich im Allgemeinen nicht der Fall sein.

4.3.8 Zusammenfassung
 Für die Parameter von Funktionen können Default-Argumente definiert werden. Diese wer-
den verwendet, wenn die entsprechenden Parameter beim Funktionsaufruf nicht übergeben
werden.
 Funktionen und Operatorfunktionen können inline deklariert werden. Damit erhält der Com-
piler die Möglichkeit, einen Funktionsaufruf durch die Anweisungen der Funktion zu erset-
zen. Dies ist für die Semantik eines Programms völlig unerheblich und dient nur dazu, die
Laufzeit zu verkürzen.
 Deklarationen müssen nicht unbedingt am Blockanfang, sondern können an beliebiger Stelle
erfolgen. Ihr Gültigkeitsbereich erstreckt sich von der Deklaration bis zum Blockende.
Sandini Bib

188 Kapitel 4: Programmieren von Klassen

 Einen Konstruktor, der ein neues Objekt anhand eines existierenden Objekts gleichen Typs
erzeugt, nennt man Copy-Konstruktor.
 Es existiert ein Default-Copy-Konstruktor, der komponentenweise kopiert.
 Mit Hilfe von Using-Deklarationen und Using-Direktiven kann die Qualifizierung eines Na-
mensbereichs entfallen.
 Eine Funktion wird automatisch auch in allen Namensbereichen der übergebenen Argumente
gesucht.
 Using-Direktiven sollten nie in Headerdateien verwendet werden.
Sandini Bib

4.4 Referenzen und Konstanten 189

4.4 Referenzen und Konstanten


Dieser Abschnitt führt in ein neues Sprachmittel von C++ ein, die Referenz.
Dabei muss zunächst klargestellt werden, dass es sich bei Referenzen in C++ um etwas an-
deres als Zeiger (siehe Abschnitt 3.7.1) handelt. Die mitunter im Umfeld von C vorgenommene
Gleichstellung der Begriffe Zeiger und Referenz sollte in C++ deshalb nicht beibehalten werden,
um unnötige Verwirrung zu vermeiden. Beides sind allerdings Verweise auf andere Objekte. Aus
diesem Grund werde ich den Begriff Verweis als Oberbegriff, der für Zeiger oder Referenz“

steht, verwenden.
Referenzen sind ein wesentliches Sprachmittel zur Laufzeitverbesserung. Es gibt aber auch
noch andere Gründe, die ihre Einführung in C++ notwendig machten. C-Programmierer werden
dabei feststellen, dass die Verwendung von Referenzen auf Kosten einer unter C bestehenden
Sicherheit bei der Parameterübergabe geht.

4.4.1 Copy-Konstruktor und Parameterübergabe


In C++ gilt, dass Parameter (wenn nicht anders angegeben) immer Kopien der übergebenen Ar-
gumente sind (Call-by-value-Mechanismus). Das bedeutet, dass mit jedem Funktionsaufruf, bei
dem ein Bruch übergeben wird, automatisch der Copy-Konstruktor aufgerufen wird. Das Gleiche
gilt für die Übergabe eines Rückgabewerts.
Der Aufruf des Operators *= ist ein Beispiel dafür:
Bei dem Ausdruck
x *= w
der gleichbedeutend ist mit
x.operator*= (w)
wird von w, da es als Parameter übergeben wird, eine Kopie angelegt, die in der Operatorfunktion
verwendet wird. Der Parameter b in der Implementierung der Operator-Funktion ist also eine
Kopie von w, die mit Hilfe des Default-Copy-Konstruktors erzeugt wurde und ohne Auswirkung
auf die Aufrufumgebung innerhalb der Funktion manipuliert werden kann:
Bruch Bruch::operator *= (Bruch b)
// b ist lokale Kopie vom zweiten Operanden
// und könnte beliebig manipuliert werden
{
*this = *this * b; // ”x *= y” ==> ”x = x * y”

return *this;
// Kopie vom ersten Operanden wird zurückgeliefert
}
Genauso wird von der Funktion eine Kopie von *this (in unserem Beispiel also eine Kopie von
x) zurückgeliefert. Diese Kopie wird ebenfalls mit Hilfe des Copy-Konstruktors als temporäres
Objekt erzeugt.
Sandini Bib

190 Kapitel 4: Programmieren von Klassen

Ein Anlegen von Kopien ist für einfache Datentypen wie ints, floats und Zeiger zunächst
akzeptabel. Bei Objekten mit vielen mehr oder weniger komplexen Komponenten kann dies aber
zu einem erheblichen Laufzeitnachteil führen, da mit jeder Parameterübergabe eine Kopie des
Objekts angelegt werden muss.
Die unter C übliche Alternative wäre es, Zeiger zu übergeben. Die Parameter sind dann
Adressen der zu verändernden Objekte. Von diesen werden zwar ebenfalls Kopien angelegt, doch
zeigt auch eine Kopie einer Adresse auf dasselbe Objekt und ermöglicht es so, auf dessen Wert
zuzugreifen. Zeiger emulieren damit das Gegenstück zu call-by-value, nämlich call-by-reference.
Wenn die Laufzeitnachteile, die durch das Anlegen von Kopien bei der Parameterübergabe
entstehen, nur mit Zeigern umgangen werden könnten, wäre es nicht möglich, mit eigenen Daten-
typen wie mit fundamentalen Datentypen rechnen zu können. Eine Multiplikation von Brüchen
müsste dann mit einem Zeiger als zweitem Operanden deklariert und aufgerufen werden. Im
Anwendungsprogramm hätte eine Multiplikation dann die Form:
x * &a // x mit a (ohne Kopie) multiplizieren
Aus diesem Grund wurden Referenzen eingeführt. Sie ermöglichen es, Argumente zu übergeben,
ohne dass Kopien angelegt oder Zeiger verwendet werden müssen.
Es gibt nun aber zwei Probleme:
 Da die Möglichkeit besteht, dass das Objekt, das als Parameter übergeben wurde, verändert
wird, kann keine Konstante mehr übergeben werden. Dies betrifft z.B. temporäre Objekte,
die grundsätzlich konstant sind.
 Es droht die Gefahr, dass Objekte, die zur Laufzeitersparnis als Referenz übergeben werden,
in der aufgerufenen Funktion versehentlich geändert werden.
Hier kommt uns jedoch die Möglichkeit zugute, Objekte als Konstanten deklarieren zu können.
Damit wird per Deklaration ausgeschlossen, dass der Wert eines übergebenen Parameters verän-
dert werden kann.

4.4.2 Referenzen
Eine als Referenz deklarierte Variable ist nichts weiter als ein zweiter Name, der für ein exi-
stierendes Objekt vergeben wird. Deklariert wird eine Referenz durch ein zusätzliches &. Die
folgende Anweisung deklariert z.B. r als Referenz (zweiter Name) für a:
int& r = x; // r ist eine Referenz (ein zweiter Name) für x
Obwohl die Schreibweise es C-Programmierern suggeriert, sollte man sich unbedingt von der
Vorstellung lösen, dass es sich um einen Zeiger handelt. Eine Referenz ist vom Typ her kein
Zeiger, sondern hat den gleichen Typ wie das Objekt, für das sie steht. Ihre Anwendung unter-
scheidet sich in keiner Weise von der für das Objekt, für das sie einen zweiten Namen definiert.
Nach obiger Deklaration ist es völlig unerheblich, ob im Folgenden r oder x verwendet wird.
Die Deklaration einer Referenz erzeugt also kein neues Objekt, sondern definiert für ein exi-
stierendes Objekt eine alternative Bezeichnung. Das Objekt, für das sie steht, muss bei der De-
klaration angegeben werden und kann nicht gewechselt werden.
Sandini Bib

4.4 Referenzen und Konstanten 191

Das folgende Beispiel soll dies verdeutlichen:


int x = 7; // normale Variable mit 7 initialisiert
int y = 13; // normale Variable mit 13 initialisiert
int& r = x; // Referenz (zweiter Name) für x
Mit der Deklaration der Referenz r wird diese auch gleich mit x initialisiert:

x 7 y 13

Wenn hier nun r verwendet wird, ist dies gleichbedeutend mit der Verwendung von x:
r = y; // r bzw. x wird der Wert von y zugewiesen
Eine Zuweisung an r entspricht somit einer Zuweisung an x. Im Gegensatz zur Initialisierung
wird hier r also kein neues Objekt zugeordnet, sondern ein neuer Wert zugewiesen, den somit
auch x erhält:

x 13 y 13

Referenzen als Parameter


Wenn man Parameter als Referenzen deklariert, werden diese bei einem Funktionsaufruf mit
den übergebenen Argumenten initialisiert. Es handelt sich dann aber um keine Kopie, sondern
um einen zweiten Namen für das übergebene Argument. Jede Änderung, die in der Funktion am
Parameter durchgeführt wird, wird damit auch am Argument, das übergeben wurde, durchgeführt
(Call-by-reference-Mechanismus).5
Auf diese Weise lässt sich zum Beispiel eine Funktion implementieren, die zwei Argumente
vertauscht:
void swap (int& a, int& b)
{
int tmp;

tmp = a;
a = b;
b = tmp;
}

5
Pascal-Programmierer kennen Referenzen als Parameter bereits. Sie werden dort durch das Schlüsselwort
var bei der Parameterdeklaration realisiert.
Sandini Bib

192 Kapitel 4: Programmieren von Klassen

Durch einen Aufruf mit zwei Integer-Variablen werden wirklich deren Werte vertauscht:
int main ()
{
using namespace std;

int x = 7;
int y = 13;

cout << "x: " << x << " y: " << y << endl; // x: 7 y: 13

swap (x, y); // vertauscht Werte von x und y

cout << "x: " << x << " y: " << y << endl; // x: 13 y: 7
}
a wird beim Aufruf von swap() ein zweiter Name für x und b ein zweiter Name für y. Eine
Zuweisung an a ändert also x und eine Zuweisung an b ändert y.
Ein derartiges Sprachmittel hat Vor- und Nachteile, und für C-Programmierer, denen das
Sprachmittel der Referenzen nicht zur Verfügung steht, kann dies je nach Sichtweise die Erfül-
lung eines Traums oder das Wahrwerden eines Alptraums sein:
 Der Vorteil von Referenzen ist, dass zum Verändern von Argumenten keine Zeiger mehr über-
geben werden müssen. Für die obige Funktion reicht der Aufruf swap(x,y)“, und die Werte

werden vertauscht. In C muss man für den gleichen Effekt swap(&x,&y)“ aufrufen und die

swap()-Funktion mit Zeigern implementieren.
 Der Nachteil von Referenzen ist, dass man in C++ damit nicht mehr am Aufruf erken-
nen kann, ob die übergebenen Argumente verändert werden können. Bei einem Aufruf von
swap(x,y)“ ist in C garantiert, dass beide Argumente nicht modifiziert werden können,

da auf jeden Fall Kopien angelegt werden. Um in C++ zu wissen, ob ein Argument inner-
halb einer Funktion manipuliert werden kann, muss man sich die Deklaration der Funktion
ansehen.
Man beachte, dass die Tatsache, dass es sich um Referenzen und nicht um Kopien handelt, wirk-
lich einzig und allein von der Deklaration der Parameter abhängt. Die Anweisungen in der Funk-
tion verwenden a und b, als wären sie als einfache Integer definiert.

Anwendung von Referenzen bei Klassen


Bei der Klasse Bruch bietet es sich z.B. an, die Operatorfunktion *= mit Referenzen zu dekla-
rieren, um das Anlegen von Kopien zu vermeiden:
Bruch& Bruch::operator *= (Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;
Sandini Bib

4.4 Referenzen und Konstanten 193

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}
Durch die Deklaration von b als Referenz ist b nun keine Kopie vom zweiten Operanden mehr,
sondern ein zweiter Name für ihn.
Auch der Rückgabewert ist nun eine Referenz, also ein zweiter Name für das Objekt, das
zurückgeliefert wird. Es wird also der geänderte erste Operand selbst und keine Kopie von ihm
zurückgeliefert.
Man beachte, dass die Anweisungen in der Funktion nicht geändert wurden. Wer zunächst
Schwierigkeiten beim Lesen der Deklaration hat, sollte sich zum Umgang mit Referenzen einfach
vorstellen, die Zeichen für die Referenz-Deklaration & wären nicht vorhanden.

4.4.3 Konstanten
Referenzen als Konstanten
Bei der Operatorfunktion *= gibt es nun allerdings noch ein Problem: Da es per Deklaration
möglich ist, dass der Wert des übergebenen Parameters geändert wird, muss es sich um ein Objekt
handeln, dem etwas zugewiesen werden darf. Damit aber scheiden sowohl Konstanten als auch
temporäre Objekte als Parameter aus. Ein Aufruf wie
x *= Bsp::Bruch(2,3);
oder
x *= w * w;
wäre nicht möglich.
Aus diesem Grund sollte ein als Referenz übergebener Parameter, wenn er nicht verändert
wird, unbedingt als Konstante deklariert werden.
Es ergibt sich die folgende Definition des Operators *=:
Bruch& Bruch::operator *= (const Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}
Damit kann als zweiter Operand auch eine Konstante oder ein temporäres Objekt verwendet
werden.
Sandini Bib

194 Kapitel 4: Programmieren von Klassen

Auch den Rückgabewert kann man als konstante Referenz deklarieren:


const Bruch& Bruch::operator *= (const Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}
Dies bedeutet, dass von der Operation der erste Operand nicht änderbar zurückgeliefert wird.
Man kann dem zurückgelieferten Wert dann zum Beispiel nichts mehr zuweisen. Der Aufruf
if ((x *= 2) < 7) // falls x nach Verdoppelung kleiner als 7 ist
wäre also möglich; der Aufruf
(x *= 2) *= x // x verdoppeln und danach quadrieren
ergibt aber eine Fehlermeldung, die ohne die Deklaration des Rückgabewerts als Konstante nicht
entsteht.
Um Missverständnissen vorzubeugen sei darauf hingewiesen, dass die Verwendung x in dem
Fall nur im Rahmen seiner Verwendung als erster Operand vom Operator *= eingeschränkt wer-
den würde. Selbstverständlich kann x nach der Operation weiter verändert werden:
x *= 2;
x *= x; // OK
Insofern wäre es zu überlegen, Objekte, die als Referenz zurückgeliefert werden, immer auch
als Konstanten zurückzuliefern, um Manipulationen von Rückgabewerten innerhalb der gleichen
Anweisung auszuschließen. Es ist in C++ bei Zuweisungen allerdings üblich, den ersten Ope-
randen als nicht-konstante Referenz zurückzuliefern.

Konstanten allgemein
Jedes Symbol kann in C++ als Konstante definiert werden. Bei seiner Deklaration muss einfach
zusätzlich das Schlüsselwort const angegeben werden. Compiler testen in diesem Fall, ob der
Wert auch wirklich nicht verändert wird.
Beispiele:
const int MAX = 3;
const double pi = 3.141592654;
Konstanten ersetzen die in C übliche Praxis, für den Programm-Code geltende Konstanten über
die Präprozessor-Anweisung #define zu definieren. Die blinde“ Ersetzung durch den Präpro-

zessor wird durch eine Deklaration eines Symbols ersetzt, für das genau wie bei Variablen eine
strenge Typprüfung durchgeführt wird und ein Gültigkeitsbereich vorgegeben ist. Der einzige
aber entscheidende Unterschied zu Variablen ist, dass ihr Wert nicht verändert werden darf. Dar-
aus folgt, dass ihr Wert bei der Deklaration festgelegt werden muss. Jede Konstante muss also
bei der Deklaration initialisiert werden.
Sandini Bib

4.4 Referenzen und Konstanten 195

Auf diese Weise lässt sich auch ein Bruch als Konstante deklarieren:
const Bsp::Bruch mwst(16,100);
Wenn für die Operation *= als zweiter Operand eine Konstante zugelassen ist, ist dafür auch der
Aufruf x *= mwst erlaubt.

4.4.4 Konstanten-Elementfunktionen
Was ist aber mit dem ersten Operanden? Bei der Operation *= wird dieser verändert, und es
macht somit Sinn, dass als Operand keine Konstante verwendet werden kann. Bei der reinen
Multiplikation (w * w) macht es hingegen Sinn, dass auch der erste Operand, für den der Ope-
rator aufgerufen wird, eine Konstante sein kann. Er wird dadurch schließlich nicht verändert.
Damit dies möglich ist, muss eine Elementfunktion, die für konstante Objekte aufgerufen
werden kann, als solche gekennzeichnet werden. Eine solche Konstanten-Elementfunktion (eng-
lisch: constant member function) wird durch das Schlüsselwort const zwischen der Parameter-
liste und dem Funktionskörper deklariert.
Die Multiplikation muss dazu wie folgt deklariert werden:
Bruch Bruch::operator * (const Bruch& b) const
{
return Bruch (zaehler*b.zaehler, nenner*b.nenner);
}
Das const am Ende der Deklaration legt fest, dass der Bruch, für den diese Operation aufgerufen
wird (also der erste Operand), nicht verändert wird. Jeder Versuch, zaehler oder nenner zu
manipulieren, wird deshalb zu einem Syntaxfehler führen.
Man beachte, dass bei der Multiplikation keine Referenz zurückgeliefert werden darf, da es
sich beim Rückgabewert um ein lokales Objekt handelt. Ansonsten wird ein zweiter Name für
etwas zurückgeliefert, das mit Beendigung der Funktion gar nicht mehr existiert, nämlich der
durch den Ausdruck
Bruch (zaehler*b.zaehler, nenner*b.nenner)
erzeugte temporäre Bruch. Die meisten Compiler erkennen einen derartigen Fehler und geben
eine entsprechende Warnung aus.
Da eine Kopie und keine Referenz zurückgeliefert wird, muss der Rückgabewert nicht mit
const deklariert werden. Temporäre Objekte sind grundsätzlich konstant.

4.4.5 Die Klasse Bruch mit Referenzen


Nun ist es an der Zeit, sich die neue Version der Klasse Bruch mit einem entsprechend geänder-
ten Anwendungsprogramm anzusehen.

Headerdatei
Die Headerdatei deklariert nun die Funktionen, soweit es sinnvoll ist, mit Referenzen und Kon-
stanten und hat folgenden Aufbau:
Sandini Bib

196 Kapitel 4: Programmieren von Klassen

// klassen/bruch4.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

private:
int zaehler;
int nenner;

public:
/* Default-Konstruktor, Konstruktor aus Zähler und
* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);

// Ausgabe (inline definiert)


void print () const {
std::cout << zaehler << '/' << nenner << std::endl;
}

// Multiplikation
Bruch operator * (const Bruch&) const;

// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

// Vergleich
bool operator < (const Bruch&) const;
};

/* Operator *
* - inline definiert
*/
Sandini Bib

4.4 Referenzen und Konstanten 197

inline Bruch Bruch::operator * (const Bruch& b) const


{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Die Elementfunktionen print(), operator*() und operator<() verändern das Objekt, für
das sie aufgerufen werden (bzw. den ersten Operanden), nicht und werden deshalb als Konstan-
ten-Elementfunktionen deklariert. Bei allen drei Operatorfunktionen wird der Parameter jetzt
außerdem als Referenz übergeben. Durch die Deklaration als Konstante kann es sich dabei eben-
falls um Konstanten oder temporäre Objekte handeln.

Quelldatei
In der Quelldatei werden die Deklarationen entsprechend angepasst. Die Implementierung der
Funktionen musste aber nirgendwo geändert werden:
// klassen/bruch4.cpp
// Headerdatei der Klasse einbinden
#include "bruch.hpp"

// Standard-Headerdateien einbinden
#include <cstdlib>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Default-Konstruktor, Konstruktor aus ganzer Zahl,


* Konstruktor aus Zähler und Nenner
* - Default für z: 0
* - Default für n: 1
*/
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
Sandini Bib

198 Kapitel 4: Programmieren von Klassen

if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

/* Operator *=
*/
const Bruch& Bruch::operator *= (const Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}

/* Operator <
*/
bool Bruch::operator < (const Bruch& b) const
{
// da die Nenner nicht negativ sein können, reicht:
return zaehler * b.nenner < b.zaehler * nenner;
}

} // **** ENDE Namespace Bsp ********************************

Anwendung
Die geänderten Deklarationen haben auf das Anwendungsprogramm überhaupt keinen Einfluss.
Allerdings können auch hier nun, soweit es sinnvoll ist, konstante Objekte verwendet werden:
Sandini Bib

4.4 Referenzen und Konstanten 199

// klassen/btest4.cpp
// Headerdateien für die verwendeten Klassen einbinden
#include "bruch.hpp"

int main()
{
using namespace Bsp; // alle Symbole des Namensbereichs std sind global

// neu: Bruch w als Konstante deklarieren


const Bruch w(7,3);

w.print(); // Bruch a ausgeben

// x deklarieren und mit dem Quadrat von w initialisieren


Bruch x = w * w;

// solange x < 1000


while (x < Bruch(1000)) {
// x mit w multiplizieren und ausgeben
x *= w;
x.print();
}
}
print() kann für die Bruch-Konstante a nur aufgerufen werden, weil die Funktion als Konstan-
ten-Elementfunktion deklariert wird.
Man beachte außerdem, dass der Vergleich x < Bruch(1000) nur deshalb möglich ist, weil
der als zweite Operand an die Operatorfunktion < übergebene Parameter eine Konstante sein
darf. Der Ausdruck Bruch(1000) liefert nämlich ein temporäres Objekt, das wie alle temporären
Objekte grundsätzlich konstant ist.

4.4.6 Zeiger auf Konstanten und Zeigerkonstanten


Zum Schluss dieses Abschnitts noch einige Anmerkungen zu der Arbeit mit Konstanten: Auch
Zeiger können auf Konstanten verweisen. Konstantheit ist allerdings Teil des Datentyps. Insofern
muss schon bei der Deklaration festgelegt werden, dass das, auf was sie zeigen, nicht verändert
werden darf. Solche Zeiger auf Konstanten dürfen dann allerdings auch auf Variablen zeigen
(aber natürlich nicht umgekehrt).
Beispiel:
int i; // int-Variable
const int c = 77; // int-Konstante
Sandini Bib

200 Kapitel 4: Programmieren von Klassen

const int* ip; // Zeiger auf int-Konstante

ip = &c; // OK: ip zeigt auf Konstante c

ip = &i; // OK: ip zeigt jetzt auf Variable i

*ip = 33; // FEHLER: i darf über ip nicht verändert werden


In diesem Fall wird das, worauf ip verweist, als konstant betrachtet (unabhängig davon, ob dies
wirklich der Fall ist). Der Versuch, das, worauf ip verweist, zu manipulieren, wird deshalb mit
einer entsprechenden Fehlermeldung vom Compiler quittiert.
Im Gegensatz dazu wird eine Zeigerkonstante, also ein Zeiger, der immer auf das gleiche
Objekt zeigt, wie folgt deklariert:
int i; // int-Variable
const int c = 77; // int-Konstante

int* const ip = &i; // Zeigerkonstante auf i

ip = 33; // OK: i wird 33 zugewiesen

ip = &c; // FEHLER: ip darf nicht auf etwas anderes verweisen


In diesem Fall verweist der Zeiger ip konstant auf das Objekt, das ihm bei Initialisierung über-
geben wurde. Der Versuch, ihn auf etwas anderes zeigen zu lassen, wird vom Compiler mit einer
entsprechenden Fehlermeldung quittiert. Das, worauf er verweist, darf aber manipuliert werden.
Selbstverständlich ist auch eine Kombination beider Konstantheiten möglich. Im folgenden
Beispiel zeigt s konstant auf eine Zeichenfolge, deren Inhalt ebenfalls konstant ist:
const char* const s = "hallo";

Positionierung von const


Das Schlüsselwort const kann sowohl vor als auch hinter dem dazugehörigen Datentyp stehen:
const int i2 = 13; // OK
int const i1 = 12; // OK
Meistens wird das const aber vorangestellt.
Entsprechend können auch konstante Referenzen unterschiedlich deklariert werden:
void f1 (const int &); // OK
void f2 (int const &); // OK
Hinter dem Referenz-Zeichen ist es allerdings nicht erlaubt:
void f2 (int & const); // FEHLER
Steht das const bei Zeigern zwischen dem Datentyp und dem Stern gehört es zum Datentyp, auf
den verwiesen wird. Die Deklaration
Sandini Bib

4.4 Referenzen und Konstanten 201

int const * ip; // definiert Zeiger auf konstanten int


ist also äquivalent zur folgenden Deklaration:
const int * ip; // definiert Zeiger auf konstanten int

Konstanten werden zu Variablen


Eine Warnung soll in Verbindung mit Konstanten noch ausgesprochen werden: Konstanten bieten
keine absolute Sicherheit vor Veränderung, da es über eine explizite Typumwandlung prinzipiell
möglich ist, Konstanten zu Variablen zu machen. Sofern sich die Konstante nicht im Read-Only-
Bereich eines Programms befindet, wird das funktionieren.
Es gibt sogar Beispiele, in denen die Umwandlung einer Konstanten in eine Variable sinn-
voll oder notwendig sein kann. Abschnitt 6.2.4 geht genauer darauf ein und stellt in dem Zu-
sammenhang das Schlüsselwort mutable vor. In Abschnitt 1 wurde dazu außerdem schon das
Schlüsselwort const_cast vorgestellt.
An dieser Stelle muss auch darauf hingewiesen werden, dass nicht alle Compiler die Verwen-
dung von Konstanten als Variablen als Fehler melden. Manche Compiler geben in einem solchen
Fall auch nur eine Warnung aus (der GCC gehört zum Beispiel dazu). Derartige Warnungen
sollten allerdings immer als Fehler betrachtet werden.

4.4.7 Zusammenfassung
 Durch die Angabe von & im Typ einer Deklaration wird eine Referenz deklariert. Eine Refe-
renz ist ein (temporärer) zweiter Name“ für ein existierendes Objekt.

 Durch Verwendung von Referenzen bei der Deklaration von Parametern und Rückgabewerten
kann das Anlegen von Kopien verhindert werden (call-by-reference statt call-by-value).
 Referenzen müssen bei der Deklaration initialisiert werden und können das Objekt, für das
sie stehen, nicht wechseln.
 Durch das Schlüsselwort const können Konstanten deklariert werden.
 Ein const hinter der Parameterliste einer Elementfunktion definiert eine so genannte Kons-
tanten-Elementfunktion. In ihr darf das Objekt, für das sie aufgerufen wurde, nicht verändert
werden. Damit kann dieses Objekt auch eine Konstante sein.
 const sollte immer verwendet werden, wenn etwas nicht verändert werden soll oder kann.
Damit wird insbesondere sichergestellt, dass man auch temporäre Objekte als Argumente
verwenden kann.
Sandini Bib

202 Kapitel 4: Programmieren von Klassen

4.5 Ein- und Ausgabe mit Streams


Wir verwenden bereits seit längerem Objekte und Symbole der C++-Standardbibliothek zur Ein-
und Ausgabe, wie std::cout, std::cerr und std::endl. Die Objekte und die dazugehörigen
Klassen gehören nicht zum Sprachkern von C++. Ihre Programmierung ist eine standardisierte
Anwendung der Sprache, die mit den bisher vorgestellten Sprachmitteln und einigen Ergänzun-
gen möglich ist.
In diesem Abschnitt wird nun näher auf die grundsätzlichen Eigenschaften der Klassen und
Objekte zur Ein- und Ausgabe eingegangen. Dazu gehört insbesondere die Möglichkeit, die
Ein- und Ausgabetechnik auf eigene Datentypen auszudehnen. Da zum vollen Verständnis der
Stream-Bibliotheken wichtige Sprachmittel bekannt sein müssen, die noch nicht vorgestellt wur-
den (vor allem die Vererbung), wird in Kapitel 8 genauer darauf eingegangen und auch ein Blick
hinter die Kulissen der Stream-Bibliothek geworfen. Dort werden dann auch Techniken für for-
matierte Ein- und Ausgaben sowie für den Zugriff auf Dateien mit Hilfe von Streams vorgestellt.

4.5.1 Streams
Die Ein- und Ausgabe wird in C++ mit Hilfe von Streams durchgeführt. Ein Stream ist sozusa-
gen ein Datenstrom“, in dem Zeichenfolgen entlangfließen“. Als konsequente Anwendung der
” ”
objektorientierten Idee sind Streams Objekte, deren Eigenschaften in Klassen definiert werden.
Für die Standardkanäle zur Ein- und Ausgabe sind verschiedene globale Objekte vordefiniert.

Stream-Klassen
Da es verschiedene Arten von I/O gibt (Eingabe, Ausgabe, Dateizugriff etc.), gibt es auch ver-
schiedene Klassen dafür. Die beiden wichtigsten sind:
 istream
Die Klasse istream ist ein Eingabestrom“ (input stream), von dem Daten gelesen wer-

den können.
 ostream
Die Klasse ostream ist ein Ausgabestrom“ (output stream), auf den Daten ausgegeben

werden können.
Beide Klassen werden, wie alle Elemente der Standardbibliothek im Namensbereich std defi-
niert. Dies kann man sich in etwa wie folgt vorstellen:
namespace std {
class istream {
...
};
class ostream {
...
};
...
}
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 203

Dies ist allerdings stark vereinfacht. In Wirklichkeit existieren zahlreiche weitere Klassen, und
die Klassen werden auch etwas komplizierter definiert. In Abschnitt 8.1 werden die tatsächlichen
Klassen im Einzelnen vorgestellt und ihre Anwendungsmöglichkeiten aufgezeigt.

Globale Stream-Objekte
Zu den Klassen istream und ostream existieren drei globale Objekte, die eine zentrale Rolle
bei der Ein- und Ausgabe spielen und die von der Stream-Bibliothek definiert werden:
 cin
cin (Klasse istream) ist der Standard-Eingabekanal, von dem ein Programm in der Re-
gel die Eingaben einliest. Er entspricht der C-Variablen stdin und wird von den Betriebssy-
stemen typischerweise der Tastatur zugeordnet.
 cout
cout (Klasse ostream) ist der Standard-Ausgabekanal, auf den ein Programm in der
Regel die Ausgaben schreibt. Er entspricht der C-Variablen stdout und wird von den Be-
triebssystemen typischerweise dem Bildschirm zugeordnet.
 cerr
cerr (Klasse ostream) ist der Standard-Fehlerausgabekanal, auf den ein Programm in
der Regel die Fehlermeldungen schreibt. Er entspricht der C-Variablen stderr und wird von
den Betriebssystemen typischerweise ebenfalls dem Bildschirm zugeordnet. Die Ausgaben
auf cerr werden nicht gepuffert.
Durch die Trennung der Ausgaben in normale“ Ausgaben und Fehlermeldungen ist es möglich,

diese in der Betriebssystemumgebung, durch die das Programm aufgerufen wird, unterschiedlich
zu behandeln. Dies resultiert aus dem UNIX-Konzept der Ein- und Ausgabeumlenkung, für das
C ursprünglich geschrieben wurde. Die normalen Ausgaben eines Programms können damit in
eine Datei gelenkt werden. Die Fehlermeldungen werden aber weiterhin auf dem Bildschirm
ausgegeben.
Auch diese drei Stream-Objekte werden im Namensbereich std definiert, was man sich ver-
einfacht in etwa wie folgt vorstellen kann:
std::istream cin;
std::ostream cout;
std::ostream cerr;
Es gibt noch ein viertes global vordefiniertes Objekt, clog, das allerdings keine so wesentliche
Rolle spielt. Es ist für Protokollausgaben vorgesehen und gibt seine Ausgaben ebenfalls auf dem
Standard-Fehlerausgabekanal, allerdings gepuffert, aus.

4.5.2 Umgang mit Streams


Die Verwendung dieser Objekte für Ein- und Ausgaben wird nachfolgend an einem einfachen
Programm verdeutlicht. Dieses Programm liest zwei ganze Zahlen ein, teilt die erste durch die
zweite und gibt das Ergebnis dieser Division aus:
Sandini Bib

204 Kapitel 4: Programmieren von Klassen

// io/iobsp.cpp
// Headerdatei für I/O mit Streams
#include <iostream>

// allgemeine Headerdatei für EXIT_FAILURE


#include <cstdlib>

int main()
{
int x, y;

// Start-String ausgeben
std::cout << "Ganzzahlige Division (x/y)\n\n";

// x einlesen
std::cout << "x: ";
if (! (std::cin >> x)) {
/* Fehler beim Einlesen
* => Fehlermeldung und Programmabbruch mit Fehlerstatus
*/
std::cerr << "Fehler beim Einlesen eines Integers"
<< std::endl;
return EXIT_FAILURE;
}

// y einlesen
std::cout << "y: ";
if (! (std::cin >> y)) {
/* Fehler beim Einlesen
* => Fehlermeldung und Programmabbruch mit Fehlerstatus
*/
std::cerr << "Fehler beim Einlesen eines Integers"
<< std::endl;
return EXIT_FAILURE;
}

// Fehler, falls y null ist


if (y == 0) {
/* Division durch null
* => Fehlermeldung und Programmabbruch mit Fehlerstatus
*/
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 205

std::cerr << "Fehler: Division durch 0" << std::endl;


return EXIT_FAILURE;
}

// Operanden und Ergebnis ausgeben


std::cout << x << " durch " << y << " ergibt "
<< x / y << std::endl;
}
Auch hier wird zunächst die Headerdatei für die Stream-Klassen und globalen Stream-Objekte,
<iostream>, eingebunden:
// Headerdatei für I/O mit Streams
#include <iostream>
In der Zeile
std::cout << "Ganzzahlige Division (x/y):\n\n";
wird zunächst ein String-Literal auf den Standard-Ausgabekanal ausgegeben. Dies geschieht,
indem für das Objekt std::cout die Operation << mit einem String als zweitem Operanden
aufgerufen wird. Der Operator << ist der Ausgabeoperator für Streams und wird so definiert,
dass das, was jeweils als zweiter Operand übergeben wird, in den Stream geschrieben wird (die
Daten werden sozusagen in Richtung des Pfeiles geschickt).
Das Besondere an diesem Operator ist, dass er nicht nur für alle Standarddatentypen über-
laden ist, sondern auch für beliebige eigene Datentypen überladen werden kann. Der zweite
Operand kann damit jeden beliebigen Datentyp besitzen. Abhängig vom Datentyp wird automa-
tisch die dazugehörige Funktion aufgerufen, die den zweiten Operanden (umgewandelt in eine
Zeichenfolge) ausgibt.
Beispiele:
int i = 7;
std::cout << i; // gibt ‘‘7’’ aus

float f = 4.5;
std::cout << f; // gibt ‘‘4.5’’ aus

Bsp::Bruch b(3,7);
std::cout << b; // gibt, sofern so definiert, ‘‘3/7’’ aus
Dies ist eine wesentliche Verbesserung im Vergleich zur Ein- und Ausgabetechnik in C mit
printf():
 Das Format des auszugebenden Objekts muss nicht extra angegeben werden, sondern ergibt
sich automatisch aus dessen Datentyp.
 Der Mechanismus ist nicht auf Standarddatentypen beschränkt und somit universell einsetz-
bar.
Sandini Bib

206 Kapitel 4: Programmieren von Klassen

Mit dem Operator << können mehrere Objekte in einer Anweisung ausgegeben werden. Die Aus-
gabeoperation liefert jeweils den ersten Operanden (also den Stream) zur weiteren Verwendung
zurück. Dies ermöglicht dann einen verketteten Aufruf des Ausgabeoperators, was in der letzten
Zeile des Beispiels gezeigt wird:
std::cout << x << " durch " << y << " ergibt "
<< x / y << std::endl;
Da der Operator << von links nach rechts ausgewertet wird, wird zunächst
std::cout << x
ausgeführt. Da dieser Ausdruck wieder std::cout zurückliefert, wird anschließend
cout << " durch "
ausgeführt. Entsprechend werden anschließend der Integer y, gefolgt von der Stringkonstante
" ergibt " und dem Ergebnis der Operation x / y (der Divisionsoperator hat eine höhere
Priorität) ausgegeben.
Besitzen x und y z.B. die Werte 91 und 7, wird Folgendes ausgegeben:
91 durch 7 ergibt 13
Wir könnten uns mit dem jetzigen Wissen bereits die dazu notwendige Implementierung der
Klasse ostream vorstellen. Für Objekte dieses Typs wird der Operator << mit allen fundamen-
talen Datentypen als zweiten Operanden überladen:
namespace std {
class ostream {
public:
ostream& operator<< (char); // Zeichen ausgeben
ostream& operator<< (int); // ganze Zahl ausgeben
ostream& operator<< (long); // ganze Zahl ausgeben
ostream& operator<< (double); // Gleitkommazahl ausgeben
ostream& operator<< (const char*); // C-String ausgeben
...
};
}
Beim Aufruf dieser Operatoren wird jeweils der erste Operand (also der Stream selbst) zurück-
geliefert:
namespace std {
ostream& ostream::operator<< (char) {
... // Low-level-Funktion zum Ausgeben des Zeichens
return *this; // Stream zur Verkettung zurück
}
...
}
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 207

Manipulatoren
Am Ende der meisten Ausgabeanweisungen wird ein so genannter Manipulator ausgegeben:
std::cout << std::endl
Manipulatoren sind spezielle Objekte, deren Ausgabe, wie der Name schon sagt, eine Manipu-
lation am Stream durchführt. Damit können z.B. Ausgabeformate definiert oder Puffer geleert
werden. Es wird damit also nicht unbedingt etwas ausgegeben.
Der Manipulator endl steht für endline“ und macht zwei Dinge:

 Er gibt ein Newline (Zeichen '\n') aus und
 leert anschließend den Ausgabepuffer (schickt die Ausgabe also auch wirklich mit flush()
ab).
Die wichtigsten vordefinierten Manipulatoren werden in Tabelle 4.1 aufgelistet.

Manipulator Klasse Bedeutung


std::flush std::ostream Ausgabepuffer leeren
std::endl std::ostream '\n' ausgeben und Ausgabepuffer leeren
std::ends std::ostream '\0' ausgeben und Ausgabepuffer leeren
std::ws std::istream Trennzeichen (Whitespaces) überlesen

Tabelle 4.1: Die wichtigsten vordefinierten Manipulatoren

Was Manipulatoren genau sind, welche es noch gibt und wie man eigene definieren kann, wird
in Abschnitt 8.1.5 erläutert.

Der Eingabeoperator >>


In der Zeile
if (! (cin >> x)) {
...
}
wird der Integer x eingelesen. Dies geschieht mit dem Gegenstück zum Ausgabeoperator, dem
Eingabeoperator >>:
cin >> x
Der Operator >> ist für Streams so definiert, dass das, was als zweiter Operand übergeben wird,
eingelesen wird (auch hier werden die Daten sozusagen in Richtung des Pfeiles geschickt).
Auch der Eingabeoperator kann prinzipiell für beliebige Datentypen überladen und verkettet
aufgerufen werden:
float f;
Bsp::Bruch b;

std::cin >> f >> b;


Dabei gilt grundsätzlich, dass führende Trennzeichen (Whitespaces) jeweils überlesen werden.
Sandini Bib

208 Kapitel 4: Programmieren von Klassen

Man beachte, dass beim Einleseoperator der zweite Operand ein Parameter ist, dessen Wert
verändert wird. Die Implementierung dieser Operatoren erfolgt deshalb im Prinzip genauso wie
beim Ausgabeoperator – mit der Besonderheit, dass Referenzen verwendet werden:
namespace std {
class istream {
public:
istream& operator>> (char&); // Zeichen einlesen
istream& operator>> (int&); // ganze Zahl einlesen
istream& operator>> (long&); // ganze Zahl einlesen
istream& operator>> (double&); // Gleitkommazahl einlesen
...
};
}

Streams und Boolesche Bedingungen


Im obigen Beispiel wird der Eingabeoperator allerdings nicht verkettet aufgerufen, da anschlie-
ßend sofort getestet wird, ob das Einlesen gelungen ist (das Einlesen kann immer schief ge-
hen). Dies geschieht durch einen Aufruf des Operators !. Der Operator ist so definiert, dass er
zurückliefert, ob das Einlesen fehlerhaft war. In unserem Beispielprogramm führt das zu einer
entsprechenden Fehlermeldung und zu einem Programmabbruch mit Fehlerstatus:
if (! (std::cin >> x)) {
std::cerr << "Fehler beim Einlesen eines Integers" << std::endl;
return EXIT_FAILURE;
}
Was hier eigentlich geschieht, ist recht trickreich: Der Ausdruck
std::cin >> x
liefert keinen Booleschen Wert, sondern, damit ein verketteter Aufruf möglich ist, zunächst wie-
der std::cin. Erst der Operator !, angewendet auf das Stream-Objekt std::cin, liefert einen
Booleschen Wert, der angibt, ob dieses Objekt einen fehlerhaften Zustand besitzt.
Die Anweisung
if (! (std::cin >> x)) {
...
}
entspricht also eigentlich:
std::cin >> x;
if (! std::cin) {
...
}
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 209

Für Stream-Objekte ist also der Operator ! so definiert, dass er jeweils einen Booleschen Wert
zurückliefert, der aussagt, ob sich der Stream in einem fehlerhaften Zustand befindet:
namespace std {
class istream {
public:
bool operator! (); // liefert true, falls Stream nicht in Ordnung ist
...
};
}
Über einen ähnlichen Trick ist auch der positive Test möglich: Boolesche Bedingungen in If-
oder While-Anweisungen verlangen in C++ nämlich entweder einen integralen Typ (sämtliche
Formen von int oder char) oder eine eindeutige Typumwandlung eines Objekts einer Klasse in
einen arithmetischen Typ oder einen Zeiger.
Da für Streams eine solche Konvertierungsfunktion definiert wird, ist auch der positive Test
möglich:
if (std::cin >> x) {
// Einlesen hat geklappt
...
}
Auch hier steckt dahinter eigentlich:
std::cin >> x;
if (std::cin) {
...
}
Dadurch, dass eine Funktion zur impliziten Typumwandlung definiert wird, wird die Boolesche
Verwendung des Objekts erst möglich. Was hier genau passiert, wird in den Abschnitten über
Konvertierungsfunktionen (siehe Seite 234) und deren Anwendung bei Klassen mit dynamischen
Komponenten (siehe Seite 382) erläutert.
Ein typisches Anwendungsbeispiel für die Möglichkeit, den Zustand eines Streams durch
Verwendung als Bedingung abzufragen, ist eine Schleife, die Objekte einliest und verarbeitet:
// solange obj eingelesen werden kann
while (std::cin >> obj) {
// obj verarbeiten (in diesem Fall ausgeben)
std::cout << obj << std::endl;
}
Dies ist das klassische Filtergerüst von C für C++-Objekte. Dabei muss aber beachtet werden,
dass der Operator >> führende Trennzeichen überliest. Die Version mit char als obj muss des-
halb auf eine andere Art implementiert werden (siehe Seite 483).
So schön die Anwendung dieser speziellen Operatoren in Booleschen Ausdrücken auch ist,
eines muss beachtet werden: Die doppelte Verneinung hebt sich hier nicht auf:
Sandini Bib

210 Kapitel 4: Programmieren von Klassen

 ”
std::cin“ ist ein Stream-Objekt der Klasse std::istream.
 ”
!!std::cin“ ist ein Boolescher Wert, der den Zustand von std::cin beschreibt.
Dieses Beispiel zeigt, dass der Mechanismus mit Vorsicht verwendet werden muss (und auch als
fragwürdig betrachtet werden kann). Der Ausdruck in der If-Bedingung ist zunächst nicht das,
was normalerweise erwartet wird, nämlich ein Boolescher Wert. Erst eine implizite Typumwand-
lung, deren Vorhandensein und Bedeutung zum Verständnis des Codes bekannt sein muss, macht
den Ausdruck sinnvoll.
Wie schon in C kann man auch hier ohne Ende streiten, ob das nun guter Programmierstil ist
oder nicht. Unzweifelhaft ist aber sicherlich, dass die Verwendung der Elementfunktion good()
(sie liefert zurück, ob ein Stream in einem fehlerfreien Zustand ist) das Programm (noch) lesbarer
machen würde:
std::cin >> x;
if (! std::cin.good()) {
...
}

4.5.3 Zustand von Streams


Die Verwendung des Operators ! zeigt, dass sich ein Stream in verschiedenen Zuständen befin-
den kann.
Für die Darstellung des prinzipiellen Zustands eines Streams werden verschiedene Bitkon-
stanten als Flags definiert, die in einer internen Stream-Komponente verwaltet werden (siehe
Tabelle 4.2).

Bitkonstante Bedeutung
goodbit alles in Ordnung
eofbit End-Of-File (Ende der Daten)
failbit Fehler: letzter Vorgang nicht korrekt abgeschlossen
badbit fataler Fehler: Zustand nicht definiert

Tabelle 4.2: Bitkonstanten für den Stream-Zustand

Der Unterschied zwischen failbit und badbit besteht im Wesentlichen darin, dass badbit
fataler ist:
 failbit wird gesetzt, wenn ein Vorgang nicht korrekt durchgeführt werden konnte, der
Stream aber prinzipiell noch verwendet werden kann.
 badbit wird gesetzt, wenn der Stream prinzipiell nicht mehr in Ordnung ist oder Daten
verloren gegangen sind.
Der Zustand der Flags kann mit dazugehörigen Elementfunktionen good(), eof(), fail() und
bad() ermittelt werden. Zurückgeliefert wird jeweils als Boolescher Wert, ob ein oder mehrere
Flags gesetzt sind. Daneben gibt es noch zwei generelle Elementfunktionen zum Setzen und
Abfragen dieser Flags, rdstate() und clear() (siehe Tabelle 4.3).
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 211

Elementfunktion Bedeutung
good() alles in Ordnung (ios::goodbit gesetzt)
eof() End-Of-File (ios::eofbit gesetzt)
fail() Fehler (ios::failbit oder ios::badbit gesetzt)
bad() fataler Fehler (ios::badbit gesetzt)
rdstate() liefert die aktuell gesetzten Flags
clear() löscht oder setzt einzelne Flags

Tabelle 4.3: Funktionen zum Abfragen und Setzen von Zustandsflags

Beim Aufruf von clear() ohne Parameter werden alle Fehlerflags (auch ios::eofbit) gelöscht
(daher kommt auch der Name clear“):

// alle Fehlerflags (einschließlich eofbit) zurücksetzen
strm.clear();
Wird aber ein Parameter übergeben, werden die darin übergebenen Flags gesetzt und die anderen
zurückgesetzt. Das folgende Beispiel testet für den Stream strm, ob das Failbit-Flag gesetzt ist,
und löscht es gegebenenfalls:
if (strm.rdstate() & std::ios::failbit) {
std::cout << "Failbit war gesetzt" << std::endl;

// alles außer ios::failbit wieder setzen


strm.clear (strm.rdstate() & ~std::ios::failbit);
}
Hier werden die Bit-Operatoren & und ~ angewendet:
 Der Operator & verknüpft die Bits über die UND-Funktion. Nur die Bits, die bei beiden
Operanden gesetzt sind, bleiben stehen. Da im zweiten Operanden nur das Failbit gesetzt ist,
ist der Ausdruck nur dann ungleich 0 (und damit erfüllt), wenn im ersten Operanden auch das
Failbit gesetzt ist.
 Der Operator ~ liefert das Bit-Komplement. Der Ausdruck ~ios::failbit liefert also ein
Ergebnis, in dem alle Flags bis auf das Failbit gesetzt sind. Durch die UND-Verknüpfung mit
allen derzeit gesetzten Flags (rdstate()) bleiben also alle gesetzten Flags bis auf das Failbit
stehen.
Neben den Zustandsflags und den Elementfunktionen zum Setzen und Abfragen existieren zahl-
reiche weitere Komponenten und Funktionen für Streams. Mit der Elementfunktion get() kann
z.B. zeichenweise gelesen werden, ohne dass führende Leerzeichen überlesen werden. Mit an-
deren Funktionen kann das Ausgabeformat beeinflusst werden. In Abschnitt 8.1 wird detailliert
darauf eingegangen.
Sandini Bib

212 Kapitel 4: Programmieren von Klassen

4.5.4 I/O-Operatoren für eigene Datentypen


Wie bereits angesprochen wurde, besteht ein wesentlicher Vorteil von Streams darin, dass der
Ein- und Ausgabe-Mechanismus auf eigene Datentypen ausgedehnt werden kann. Dazu müssen
die Operatoren << und >> für eigene Datentypen überladen werden. Dabei gibt es nur ein Pro-
blem: Man kann nicht einfach in die Deklaration der Stream-Klassen gehen und dort seinen
Operator << bzw. >> definieren. Die Stream-Klassen sind Teil einer geschlossenen Bibliothek.
An dieser Stelle hilft eine besondere Eigenschaft von C++ bei der Interpretation des Aufrufs
von binären Operatoren. Trifft ein Compiler auf einen Ausdruck der Form
a * b
kann er diesen auf zweierlei Arten auswerten:
 Er kann ihn streng objektorientiert im Sinne von
a.operator*(b)
betrachten.
 Er kann ihn aber auch als globale Verknüpfung zweier gleichwertiger Operanden betrachten:

operator*(a,b)

Im ersten Fall muss ein entsprechender Operator in der Klasse des Objekts a definiert sein.
Im zweiten Fall muss außerhalb jeder Klasse eine Operatorfunktion definiert sein, die beide
Operanden verknüpft. In diesem Fall ist dann auch wieder der erste Operand ein Parameter.
Entsprechendes gilt für einen Aufruf wie:
std::cout << x
x muss entweder einen Datentyp haben, für den innerhalb der Klasse std::ostream der Ope-
rator << definiert ist:
namespace std {
class ostream {
public:
ostream& operator<< (typ); // Parameter ist zweiter Operand
...
};
}
Oder außerhalb jeder Klasse muss ein Operator definiert sein, der beide Operanden mit << ver-
knüpft:
// beide Operanden sind Parameter:
std::ostream& operator<< (std::ostream&, typ);
Das Erstere gilt für alle fundamentalen Datentypen. Die zweite Möglichkeit nutzt man, um diesen
Mechanismus auf eigene Datentypen auszudehnen. Braucht man dabei Zugriff auf interne Daten
des eigenen Typs, ruft man darin einfach eine Elementfunktion der dazugehörigen Klasse auf.
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 213

Wie dies genau aussieht, wird nun anhand der folgenden neuen Version der Klasse Bruch ver-
deutlicht.

Headerdatei der Klasse Bruch mit Stream-I/O


Die Headerdatei der Klasse Bruch erhält nun folgenden Aufbau:
// klassen/bruch5.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

private:
int zaehler;
int nenner;

public:
/* Default-Konstruktor, Konstruktor aus Zähler und
* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);
// Multiplikation
Bruch operator * (const Bruch&) const;

// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

// Vergleich
bool operator < (const Bruch&) const;

// neu: Ausgabe mit Streams


void printOn (std::ostream&) const;

// neu: Eingabe mit Streams


void scanFrom (std::istream&);
};
Sandini Bib

214 Kapitel 4: Programmieren von Klassen

/* Operator *
* - inline definiert
*/
inline Bruch Bruch::operator * (const Bruch& b) const
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}

/* neu: Standard-Ausgabeoperator
* - global überladen und inline definiert
*/
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

/* neu: Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Damit Brüche mit dem Standard-Stream-Mechanismus verwendet werden können, werden die
Ein- bzw. Ausgabeoperatoren (<< bzw. >>) global überladen:
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 215

inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}
Da wir zum Ein- und Ausgeben Zugriff auf die internen Komponenten des Bruchs (zaehler und
nenner) brauchen, wenden sich beide Operatoren an den Bruch und rufen dort eine geeignete
Elementfunktion auf.
Die Elementfunktion zum Ausgeben ist eine modifizierte Form von print(), bei der nun als
zusätzlicher Parameter der Stream, auf den der Bruch ausgegeben werden muss, zu übergeben
ist:
class Bruch {
...
// Ausgabe mit Streams
void printOn (std::ostream&) const;
...
};
Neu hinzugekommen ist eine Elementfunktion zum Einlesen eines Bruchs: Ihr Parameter ist ein
Input-Stream:
class Bruch {
...
// Eingabe mit Streams
void scanFrom (std::istream&);
...
};
Bei der Implementierung der globalen Operatorfunktionen ist unbedingt darauf zu achten, dass
nie Kopien des manipulierten Streams angelegt werden. Dies würde nämlich nicht nur Zeit kos-
ten, sondern auch zu Fehlern führen: Der Stream wird durch die Operation manipuliert (Puffer
ändern sich, der Status kann in den Fehlerzustand wechseln usw.). Diese Manipulationen würden
in einer Kopie aber verloren gehen und können zu Inkonsistenzen führen. Wenn in einem späteren
Ausdruck wieder das Original verwendet wird, ist dessen Zustand nicht geändert und entspricht
somit nicht der Realität. Aus diesem Grund müssen Stream-Parameter und -Rückgabewerte im-
mer als Referenzen deklariert werden.

Quelldatei der Klasse Bruch mit Stream-I/O


In der Quelldatei der Klasse Bruch wurden entsprechende Änderungen vorgenommen.
Zunächst muss der Konstruktor dahingehend geändert werden, dass die Fehlermeldung, die
ausgegeben wird, wenn der Nenner 0 ist, mit dem Stream-Mechanismus auf den Standard-Feh-
lerausgabekanal cerr ausgegeben wird:
Sandini Bib

216 Kapitel 4: Programmieren von Klassen

if (n == 0) {
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
Dann muss die bisherige Ausgabefunktion print() durch die Elementfunktion printOn() er-
setzt werden. Sie gibt Zähler und Nenner des Bruchs auf dem übergebenen Ausgabe-Stream
strm in der Form zähler/nenner“ aus:

void Bruch::printOn (std::ostream& strm) const
{
strm << zaehler << '/' << nenner;
}
Schließlich kommt die Elementfunktion scanFrom() hinzu, die den Bruch vom übergebenen
Eingabe-Stream strm einliest, indem nacheinander Zähler und Nenner als Integer eingelesen
werden:
// klassen/bruch5scan.cpp
// **** BEGINN Namespace Bsp ********************************
namespace Bsp {
...

/* neu: scanFrom()
* - Bruch von Stream strm einlesen
*/
void Bruch::scanFrom (std::istream& strm)
{
int z, n;

// Zähler einlesen
strm >> z;

// optionales Trennzeichen ’/’ und Nenner einlesen


if (strm.peek() == '/') {
strm.get();
strm >> n;
}
else {
n = 1;
}

// Lesefehler?
if (! strm) {
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 217

return;
}

// Nenner == 0?
if (n == 0) {
// Fail-Bit setzen
strm.clear (strm.rdstate() | std::ios::failbit);
return;
}

/* OK, eingelesene Werte zuweisen


* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

} // **** ENDE Namespace Bsp ********************************


Damit das Eingabe- zum Ausgabeformat passt, muss zwischen dem Zähler und dem Nenner das
Zeichen '/' stehen. Die Stream-Funktion peek() liefert das nächste Zeichen, ohne es auszule-
sen. Die Stream-Funktion get() liest das Zeichen dann aus (vgl. Seite 480).
Die einzulesenden Integerwerte werden zunächst in Hilfsvariablen eingelesen. Dahinter steckt
die Absicht, dass ein Objekt nur nach einem erfolgreichen Einlesen verändert werden soll (eine
gängige C++-Konvention). Hierbei können aber mehrere Fehler auftreten:
 Zum einen kann es sein, dass die Integer nicht eingelesen werden können, weil z.B. das
nächste Zeichen ein Buchstabe ist. In dem Fall wechselt der Stream in einen Fehlerzustand,
der durch die folgende If-Abfrage abgeprüft wird:
// Lesefehler?
if (! strm) {
return;
}
Wie darauf reagiert werden soll, hängt eigentlich von der Situation ab. Man könnte z.B. das
Programm mit einer Fehlermeldung beenden oder versuchen, den Wert nach der Ausgabe
einer Fehlermeldung neu einzulesen. Je nach Situation kann das eine, das andere oder auch
Sandini Bib

218 Kapitel 4: Programmieren von Klassen

beides nicht sinnvoll sein. Durch die Möglichkeit der Eingabeumlenkung kann der Wert z.B.
auch aus einer Datei oder von einem anderen Prozess kommen. Deshalb sollte der Fehler
immer von der Aufrufumgebung behandelt werden. Da der Stream in einen Fehlerzustand
wechselt, kann dieser auch von der Aufrufumgebung erkannt und ausgewertet werden.
Genau das wird in diesem Fall auch gemacht. Damit ist das Verhalten beim Einlesen
eines Bruchs äquivalent zum Einlesen einer ganzen Zahl. Der Vorteil dabei ist, dass das An-
wendungsprogramm die Umstände kennt, unter denen die Funktion aufgerufen wurde, und
entsprechend reagieren kann. Der Nachteil ist, dass, wenn das Anwendungsprogramm diesen
Test nicht durchführt, der Lesefehler zunächst unbemerkt bleibt.
Mit dem Konzept der Ausnahmebehandlung wird ein besserer Mechanismus für die Feh-
lerbehandlung eingeführt. In Abschnitt 4.7 wird darauf eingegangen und eine entsprechend
modifizierte Version dieser Einlesefunktion vorgestellt.
 Zum anderen kann auch bei einem erfolgreichen Einlesen ein Fehler auftreten: Der Nenner
kann 0 sein.
Auch dieser Fall wird hier als Formatfehler beim Einlesen behandelt: Dazu wird das Feh-
lerflag des Streams gesetzt, das einen Formatfehler anzeigt:6
// Nenner == 0?
if (n == 0) {
// Fail-Bit setzen
strm.clear (strm.rdstate() | std::ios::failbit);
return;
}
Auch diesen Fall muss das Anwendungsprogramm behandeln.
Insgesamt ergibt sich folgender Aufbau der Quelldatei der Klasse Bruch:
// klassen/bruch5.cpp
// Headerdatei der Klasse einbinden
#include "bruch.hpp"

// Standard-Headerdateien einbinden
#include <cstdlib>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Default-Konstruktor, Konstruktor aus ganzer Zahl,


* Konstruktor aus Zähler und Nenner
* - Default für z: 0

6
Der Bitoperator | verknüpft die Bits mit der ODER-Funktion und liefert damit alle Bits, die in beiden
Operanden gesetzt sind.
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 219

* - Default für n: 1
*/
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n == 0) {
// neu:
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

/* Operator *=
*/
const Bruch& Bruch::operator *= (const Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}

/* Operator <
*/
bool Bruch::operator < (const Bruch& b) const
{
// da die Nenner nicht negativ sein können, reicht:
return zaehler * b.nenner < b.zaehler * nenner;
}
Sandini Bib

220 Kapitel 4: Programmieren von Klassen

/* neu: printOn()
* - Bruch auf Stream strm ausgeben
*/
void Bruch::printOn (std::ostream& strm) const
{
strm << zaehler << '/' << nenner;
}

/* neu: scanFrom()
* - Bruch von Stream strm einlesen
*/
void Bruch::scanFrom (std::istream& strm)
{
int z, n;

// Zähler einlesen
strm >> z;

// optionales Trennzeichen ’/’ und Nenner einlesen


if (strm.peek() == '/') {
strm.get();
strm >> n;
}
else {
n = 1;
}

// Lesefehler ?
if (! strm) {
return;
}

// Nenner == 0 ?
if (n == 0) {
// Fail-Bit setzen
strm.clear (strm.rdstate() | std::ios::failbit);
return;
}

/* OK, eingelesene Zahlen zuweisen


* - ein negatives Vorzeichen des Nenners kommt in den Zähler
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 221

*/
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

} // **** ENDE Namespace Bsp ********************************

Anwendung der Klasse Bruch mit Stream-I/O


Das Testprogramm führt nun seine Ausgaben mit Hilfe der Stream-Mechanismen durch:
// klassen/btest5.cpp
// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// Headerdateien für die verwendeten Klassen einbinden


#include "bruch.hpp"

int main()
{
const Bsp::Bruch a(7,3); // Bruch-Konstante a deklarieren
Bsp::Bruch x; // Bruch-Variable x deklarieren

// Bruch a (neu: mit Stream-Operator) ausgeben


std::cout << a << std::endl;

// neu: Bruch x einlesen


std::cout << "Bruch eingeben (zaehler/nenner): ";
if (! (std::cin >> x)) {
// Eingabefehler: Programmabbruch mit Fehlerstatus
std::cerr << "Fehler beim Bruch-Eingeben" << std::endl;
return EXIT_FAILURE;
}
std::cout << "Eingabe war: " << x << std::endl;
Sandini Bib

222 Kapitel 4: Programmieren von Klassen

// solange x < 1000


while (x < Bsp::Bruch(1000)) {
// x mit a multiplizieren und (neu: mit Stream-Operator) ausgeben
x = x * a;
std::cout << x << std::endl;
}
}
Der Aufruf
std::cout << a << std::endl;
ruft zunächst für cout und a den für die Klasse Bruch global überladenen Ausgabeoperator
<< auf. Dieser ruft dann für a die Elementfunktion printOn() auf, die den Bruch schließlich
ausgibt.
Hinzu kommt, dass im Programm nun geprüft wird, ob das Einlesen des Bruchs erfolgreich
war. Falls der Stream nach dem Einlesen nicht in einem fehlerfreien Zustand ist, wird eine ent-
sprechende Fehlermeldung ausgegeben und das Programm beendet:
if (! (std::cin >> x)) {
// Eingabefehler: Programmabbruch
std::cerr << "Fehler beim Bruch-Eingeben" << std::endl;
std::exit(EXIT_FAILURE);
}
An dieser Stelle könnte auch zunächst die Art des Fehlers untersucht und entsprechend reagiert
werden:
// klassen/btest5y.cpp
while (! (std::cin >> x)) {
char c;

if (std::cin.bad()) {
// fataler Eingabefehler: Programmabbruch
std::cerr << "fataler Fehler bei Bruch-Eingabe" << std::endl;
std::exit(EXIT_FAILURE);
}
if (std::cin.eof()) {
// End-Of-File: Programmabbruch
std::cerr << "EOF bei Bruch-Eingabe" << std::endl;
std::exit(EXIT_FAILURE);
}
/* nicht fataler Fehler:
* - Failbit zurücksetzen
* - alles bis zum Zeilenende auslesen und
Sandini Bib

4.5 Ein- und Ausgabe mit Streams 223

* nochmal probieren (Schleife!)


*/
std::cin.clear();
while (std::cin.get(c) && c != '\n') {
}
std::cerr << "Fehler bei Bruch-Eingabe, probier es nochmal: "
<< std::endl;
}
Falls beim Einlesen ein Fehler auftritt, der nicht fatal ist, wird mit
cin.clear();
das Fehlerflag zurückgesetzt und mit
while (cin.get(c) && c != '\n') {
}
der Rest der Zeile ausgelesen. Die Elementfunktion get() liest dabei jeweils ein Zeichen ein
und weist es dem übergebenen Parameter c zu.
Wie das Beispiel zeigt, ist die Fehlerbehandlung beim Einlesen eine Aufgabe, die für eine
fehlertolerante Anwendung beliebig kompliziert werden kann. Hier bietet es sich an, Hilfsfunk-
tionen zu schreiben, die bestimmte Datentypen mit der dazugehörigen Fehlerbehandlung einle-
sen.

4.5.5 Zusammenfassung
 Die Ein- und Ausgabe (I/O) ist nicht Teil der Sprache C++, sondern wird durch eine Standard-
Klassenbibliothek realisiert.
 Es existieren verschiedene Stream-Klassen. Die wichtigsten sind istream für Objekte, von
denen gelesen werden kann, und ostream für Objekte, auf die etwas ausgegeben werden
kann.
 Streams besitzen einen Zustand, der durch Ein-/Ausgabeoperationen geändert und abgefragt
werden kann.
 Die globalen Objekte cin, cout und cerr sind als Standard-Ein/Ausgabekanäle vordefiniert.
 Zur Ein- und Ausgabe werden typischerweise die Operatoren >> und << verwendet. Sie
können verkettet aufgerufen werden.
 Manipulatoren erlauben es, Streams in einer Ein- oder Ausgabeoperation zu manipulieren.
 Durch globales Überladen der I/O-Operatoren kann das Ein/Ausgabe-Konzept auch auf eige-
ne Datentypen übertragen werden.
 Streams sollten immer als Referenzen übergeben werden.
Sandini Bib

224 Kapitel 4: Programmieren von Klassen

4.6 Freunde und andere Typen


Dieses Kapitel beschäftigt sich mit dem Thema der (automatischen) Typumwandlung. Es geht
darum, wie sie definiert werden, wann sie stattfinden und unter welchen Umständen man sie
vermeiden sollte.
In diesem Zusammenhang wird auch eines der umstrittensten Sprachmittel von C++, das
Schlüsselwort friend, eingeführt.

4.6.1 Automatische Typumwandlungen


Das bisherige Anwendungsprogramm kann ohne Änderung der Klasse Bruch noch etwas lesba-
rer gestaltet werden:
// klassen/btest6.cpp
// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// Headerdateien für die verwendeten Klassen einbinden


#include "bruch.hpp"

int main()
{
const Bsp::Bruch a(7,3); // Bruch-Konstante a deklarieren
Bsp::Bruch x; // Bruch-Variable x deklarieren

std::cout << a << std::endl; // Bruch a ausgeben

// Bruch x einlesen
std::cout << "Bruch eingeben (zaehler/nenner): ";
if (! (std::cin >> x)) {
// Eingabefehler: Programmabbruch mit Fehlerstatus
std::cerr << "Fehler beim Bruch-Eingeben" << std::endl;
return EXIT_FAILURE;
}
std::cout << "Eingabe war: " << x << std::endl;

// solange x < 1000


while (x < 1000) { // neu: statt while (x < Bsp::Bruch(1000))
// x mit a multiplizieren und ausgeben
x = x * a;
std::cout << x << std::endl;
}
}
Sandini Bib

4.6 Freunde und andere Typen 225

Der kleine, aber feine Unterschied ist der direkte Vergleich eines Bruchs mit der Zahl 1000:
while (x < 1000) { // neu: statt while (x < Bsp::Bruch(1000))
...
}
Dies ist möglich, da jeder Konstruktor, der mit genau einem Argument aufgerufen werden kann,
automatisch eine entsprechende automatische Typumwandlung (implizite Typumwandlung) defi-
niert. Dazu zählen auch Konstruktoren mit mehr als einem Parameter, die aber jeweils Default-
Argumente besitzen.
Der Konstruktor der Klasse Bruch, dem man mit einem int als Parameter aufrufen kann,
ermöglicht also eine automatische Typumwandlung eines ints in einen Bruch:
namespace Bsp {
class Bruch {
...
// Konstruktor aus ganzer Zahl definiert automatische Typumwandlung
Bruch (int = 0, int = 1);
...
};
}

int main()
{
Bsp::Bruch x;
...
while (x < 1000) { // autom. Typumwandlung: 1000 => Bsp::Bruch(1000)
...
}
}
Das bedeutet, dass immer dann, wenn als Parameter ein Bruch verlangt wird, stattdessen ein
Integer verwendet werden kann. Um dies auch für andere Datentypen zu ermöglichen, müssen
also nur entsprechende Konstruktoren definiert werden. So wäre es z.B. denkbar, für Brüche eine
automatische Typumwandlung für Gleitkommazahlen zu definieren:
namespace Bsp {
class Bruch {
...
// Konstruktor für automatische Typumwandlung von float nach Bsp::Bruch
Bruch (float);
};
}
int main()
{
Sandini Bib

226 Kapitel 4: Programmieren von Klassen

Bsp::Bruch x;
...
if (x < 3.7) { // automatische Typumwandlung: 3.7 => Bsp::Bruch(3.7)
...
}
...
}
Eine automatische Typumwandlung kann durch eine direkte Implementierung der Operation mit
den richtigen Datentypen vermieden werden. Eine überladene Funktion, bei der die Parameter-
typen genau passen, hat eine höhere Priorität als eine Version, für die eine Typumwandlung not-
wendig ist. Damit kann ein durch die Typumwandlung entstandener Laufzeitnachteil vermieden
werden:
namespace Bsp {
// Vergleich mit einem int
bool Bruch::operator < (int i) const
{
// da die Nenner nicht negativ sein können, reicht:
return zaehler < i * nenner;
}
}
Hier muss wieder zwischen dem Implementierungsaufwand und dem Laufzeitnachteil abgewo-
gen werden.

4.6.2 Schlüsselwort explicit


Die Definition einer automatischen Typumwandlung durch einen Konstruktor kann man auch
unterbinden. Dazu muss man den Konstruktor mit dem Schlüsselwort explicit deklarieren:
namespace Bsp {
class Bruch {
...
// Konstruktor nur für explizite Typumwandlung von float nach Bsp::Bruch
explicit Bruch (float);
};
}

int main()
{
Bsp::Bruch x;
...
if (x < 3.7) { // FEHLER: keine automatische Typumwandlung möglich
...
}
Sandini Bib

4.6 Freunde und andere Typen 227

if (x < Bsp::Bruch(3.7)) { // OK
...
}
...
}
Man beachte, dass in diesem Fall der Unterschied zwischen den beiden Formen zur Initialisierung
eines Objekts bei Deklaration zum Tragen kommt: Eine Deklaration der Form
X x;
Y y(x); // explizite Typumwandlung
stellt eine explizite Typumwandlung dar. Eine Deklaration der Form
X x;
Y y = x; // implizite Typumwandlung
stellt dagegen eine implizite Typumwandlung dar. Ist der Konstruktor mit explicit deklariert,
ist die zweite Form also nicht möglich.

4.6.3 Friend-Funktionen
Bei der automatischen Typumwandlung für Elementfunktionen gibt es allerdings ein Problem:
Der Vergleich x < 1000 ist zwar möglich, der Vergleich 1000 < x aber nicht:
namespace Bsp {
class Bruch {
...
bool operator < (const Bruch&) const;
...
};
}

int main()
{
Bsp::Bruch x;
...
if (x < 1000) // OK
...
if (1000 < x) // FEHLER
...
}
Der Vergleich
x < 1000
Sandini Bib

228 Kapitel 4: Programmieren von Klassen

wird als
x.operator< (1000)
interpretiert. Da es sich bei 1000 um einen Parameter handelt und es eine eindeutige Typum-
wandlung gibt, wird daraus automatisch:
x.operator< (Bsp::Bruch(1000))
Für den Vergleich
1000 < x
gibt es keine entsprechende Interpretation. Die Interpretation als
1000.operator< (x) // FEHLER
ist ein Fehler, da ein fundamentaler Datentyp wie int keine Elementfunktionen haben kann.
Das Problem besteht also darin, dass eine automatische Typumwandlung nur für Parameter
möglich ist. Da der erste Operand bei der Implementierung der Operation als Elementfunktion
kein Parameter ist, ist sie für diesen also nicht möglich. Eine explizite Typumwandlung durch
den Aufruf
Bsp::Bruch(1000) < x // OK
ist aber möglich.
Es gibt allerdings auch hier die Möglichkeit, den Ausdruck
1000 < x
auf eine andere Weise zu interpretieren (so wie dies schon auf Seite 212 zur Definition des
Ausgabeoperators für eigene Datentypen ausgenutzt wurde). Man kann den Ausdruck auch als
globale Verknüpfung zweier Operanden betrachten, die beide als Parameter übergeben werden:
operator< (1000, x)
Eine solche Operation muss global und nicht als Elementfunktion deklariert werden:
bool operator < (const Bsp::Bruch&, const Bsp::Bruch&)
Da diese Operation keine Elementfunktion ist, besteht kein Zugriff auf private Komponenten der
Klasse Bsp::Bruch. Sofern dieser gebraucht wird, kann auch in diesem Fall eine Hilfselement-
funktion der Klasse aufgerufen werden, die den eigentlichen Vergleich vornimmt:
namespace Bsp {
class Bruch {
...
bool vergleicheMit (const Bruch&) const;
...
};
}
bool operator < (const Bsp::Bruch& op1, const Bsp::Bruch& op2) {
return op1.vergleicheMit(op2);
}
Sandini Bib

4.6 Freunde und andere Typen 229

Es gibt aber auch noch eine andere Möglichkeit: Man kann die Operation mit Hilfe des Schlüssel-
worts friend als Freund“ der Klasse deklarieren. Alle Freunde“ einer Klasse besitzen auch
” ”
Zugriff auf die privaten Komponenten ( vor Freunden gibt es keine Geheimnisse“).

Über den Umweg einer Deklaration als globale Friend-Funktion ist eine automatische Typ-
umwandlung also sowohl für den ersten als auch für den zweiten Parameter möglich:
namespace Bsp {
class Bruch {
...
friend bool operator < (const Bruch&, const Bruch&);
...
};
}

int main()
{
Bsp::Bruch x;
...
if (x < 1000) // OK
...
if (1000 < x) // auch OK !
...
}

Die Klasse Bruch mit Friend-Funktionen zur automatischen Typumwandlung


Um eine automatische Typumwandlung bei der Klasse Bruch an den sinnvollen Stellen zu er-
möglichen, sollte die Deklaration der Klasse wie folgt verändert werden:
// klassen/bruch6.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

private:
int zaehler;
int nenner;
Sandini Bib

230 Kapitel 4: Programmieren von Klassen

public:
/* Default-Konstruktor, Konstruktor aus Zähler und
* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);

/* Multiplikation
* - neu: globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend Bruch operator * (const Bruch&, const Bruch&);

// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

/* Vergleich
* - neu: globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend bool operator < (const Bruch&, const Bruch&);

// Ausgabe mit Streams


void printOn (std::ostream&) const;

// Eingabe mit Streams


void scanFrom (std::istream&);
};

/* Operator *
* - neu: globale Friend-Funktion
* - inline definiert
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}

/* Standard-Ausgabeoperator
Sandini Bib

4.6 Freunde und andere Typen 231

* - global überladen und inline definiert


*/
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

/* Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Hier wird nicht nur der Operator <, sondern auch der Operator * als globale Friend-Funktion de-
klariert. Dem liegt die entsprechende Überlegung zugrunde, dass, wenn der Ausdruck x * 1000
durch eine automatische Typumwandlung für den zweiten Operanden möglich ist, auch 1000 * x
möglich sein sollte.
Dies bedeutet nun aber nicht, dass alle Operatoren als globale Friend-Funktionen definiert
werden sollten. Kandidaten für Friend-Funktionen sind vielmehr im Allgemeinen nur zweistelli-
ge Operatoren, bei denen der erste Operand nicht verändert wird:
 Für einstellige Operatoren macht es keinen Sinn, da es sich um eine Funktion der Klasse
Bruch handelt und somit zumindest ein Bruch beteiligt sein sollte.
 Für zweistellige Operatoren, deren Aufruf den ersten Operanden verändert (dies sind im All-
gemeinen die Zuweisungsoperatoren), soll das originale Objekt und nicht ein durch eine au-
tomatische Typumwandlung temporär erzeugtes Objekt manipuliert werden.

Implementierung von Friend-Funktionen für Klassen


Die geänderte Version der Headerdatei zeigt auch, dass die Friend-Funktionen nun anders im-
plementiert werden müssen. Es gibt kein implizit übergebenes Objekt mehr, dessen Komponen-
ten direkt angesprochen werden können. Die in der Headerdatei inline definierte Multiplikation
erhält somit einen anderen Aufbau.
Statt der bisherigen Implementierung
Sandini Bib

232 Kapitel 4: Programmieren von Klassen

/* Bruch-Multiplikation als Elementfunktion


*/
inline Bruch Bruch::operator * (const Bruch& b) const
{
return Bruch (zaehler * b.zaehler, nenner * b.nenner);
}
lautet die Implementierung nun:
/* Bruch-Multiplikation als globale Friend-Funktion
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}
Es gibt nun also zwei Parameter a und b, von denen jeweils Zähler und Nenner angesprochen
werden. Dieser Zugriff ist erlaubt, da es sich um eine Friend-Funktion der Klasse Bruch handelt.
Da es kein Objekt gibt, für das die Funktion aufgerufen werden kann, darf in der Funktion weder
this verwendet werden, noch dürfen zaehler oder nenner ohne Qualifizierung für ein Objekt
angesprochen werden.
Entsprechend muss, da aus der Definition des Vergleichsoperators als Elementfunktion nun
eine Definition als globale Funktion geworden ist, auch deren Implementierung in der Quelldatei
geändert werden:
// klassen/bruch6.cpp
// Headerdatei der Klasse einbinden
#include "bruch.hpp"
...
/* Operator <
* - neu: globale Friend-Funktion
*/
bool operator < (const Bruch& a, const Bruch& b)
{
// da die Nenner nicht negativ sein können, reicht:
return a.zaehler * b.nenner < b.zaehler * a.nenner;
}
...

Auch hier werden jetzt beide Operanden als Parameter übergeben, deren Komponenten jeweils
nur über den Namen des Parameters angesprochen werden können.
Man beachte, dass man an der Implementierung einer Funktion nicht erkennen kann, ob und
– wenn ja – für welche Klassen sie eine Friend-Funktion ist. Entscheidend ist einzig und allein
die entsprechende Deklaration in der Klasse.
Sandini Bib

4.6 Freunde und andere Typen 233

Normale“ Elementfunktionen als Friend-Funktionen



Die hier vorgestellte Lösung – Operatoren global zu definieren, um eine automatische Typum-
wandlung für den ersten Operator zu ermöglichen – wird in kommerziellen Klassen sehr häufig
verwendet, da die meisten zweistelligen Operatoren den ersten Operanden nicht verändern. Eine
globale Definition kann aber auch für normale“ Elementfunktionen, die keine Operatoren defi-

nieren, sinnvoll sein. Dabei muss allerdings beachtet werden, dass sich dadurch die Aufrufsyntax
ändert.
Betrachten wir das am Beispiel einer Funktion, die zu einem Bruch seinen Kehrwert ausgibt.
Als Elementfunktion deklariert, sieht ihre Anwendung wie folgt aus:
namespace Bsp {
class Bruch {
...
Bruch kehrwert () const; // Kehrwert liefern
...
};
}
int main()
{
Bsp::Bruch x, y;
...
y = x.kehrwert(); // Aufruf als Elementfunktion
// (keine Typumwandlung möglich)
}
Durch eine Deklaration als globale Friend-Funktion wird die Funktion nicht mehr für ein be-
stimmtes Objekt, sondern global aufgerufen, und es ist eine automatische Typumwandlung mög-
lich:
namespace Bsp {
class Bruch {
...
friend Bruch kehrwert (const Bruch&); // Kehrwert liefern
...
};
}

int main()
{
Bsp::Bruch x, y;
...
y = kehrwert(x); // Aufruf als globale Funktion

y = kehrwert(7); // automatische Typumwandlung über


// int-Konstruktor möglich (=> 1/7)
}
Sandini Bib

234 Kapitel 4: Programmieren von Klassen

Spätestens hier sollte aber darauf hingewiesen werden, dass eine automatische Typumwandlung
aus Sicht der objektorientierten Programmierung als fragwürdig betrachtet werden kann. Objekte
erhalten dadurch Fähigkeiten, die außerhalb der eigentlichen Klassenstruktur liegen. Stellt man
nicht sicher, dass diese Funktionen zumindest logisch zur Klasse gehören und mit ihr zusammen
übersetzt werden, kann dies zu einem nicht mehr nachvollziehbarem Verhalten führen. Hinzu
kommt, dass Friend-Funktionen bei der Vererbung zu einem Problem werden können. Auf diese
Aspekte wird unter anderem in den Abschnitten 4.6.5 und 4.6.7 noch genauer eingegangen.
Es gibt jedenfall immer auch die Möglichkeit, auf eine derartige Deklaration einer Friend-
Funktion zu verzichten. Wie bei den Ein-/Ausgabefunktionen kann man einfach globale Funk-
tionen definieren, die öffentliche Hilfsfunktionen aufrufen. So könnte man z.B. auch die Multi-
plikation als globale Operation implementieren, indem man die Multiplikation in einen Aufruf
der Operation *= umsetzt:
Bsp::Bruch operator * (const Bsp::Bruch& a, const Bsp::Bruch& b)
{
Bsp::Bruch ergebnis(a);
return a *= b;
}
Die global deklarierte Funktion verwendet nun keine Interna mehr, sondern ruft nur öffentliche
Elementfunktionen auf (Copy-Konstruktor und Operator *=). Ein eventuell vorhandener Lauf-
zeitnachteil kann durch eine Implementierung als Inline-Funktion weitgehend vermieden wer-
den.

4.6.4 Konvertierungsfunktionen
Konstruktoren, die mit einem Parameter aufgerufen werden, definieren, wie ein Objekt der da-
zugehörigen Klasse aus einem Objekt eines fremden Typs erzeugt werden kann. Es gibt auch
die Möglichkeit einer umgekehrten Konvertierung, also die Umwandlung des Objekts in einen
fremden Datentyp zu definieren. Dies geschieht mit Hilfe so genannter Konvertierungsfunktionen
(englisch: conversion functions).
Die Deklaration einer Konvertierungsfunktion erfolgt mit dem Schlüsselwort operator, ge-
folgt von dem Typ, in den umgewandelt werden soll.
Bei der Klasse Bruch ist es z.B. möglich, eine Typumwandlung eines Bruchs in einen Gleit-
kommawert zu definieren, indem als operator double () eine Funktion definiert wird, die
den Wert des Bruchs in den Typ double umwandelt und diesen zurückliefert:7
namespace Bsp {
class Bruch {
...
// automatische Typumwandlung nach double

7
Man beachte, dass bei der Bildung des Quotienten ohne die expliziten Typumwandlungen von Zähler
und Nenner in den Datentyp double die ganzzahlige Division aufgerufen würde. Deshalb ist die explizite
Typumwandlung von zaehler und nenner in den Datentyp double notwendig.
Sandini Bib

4.6 Freunde und andere Typen 235

operator double () const;


...
};
}

inline Bruch::operator double () const


{
// Quotient aus Zähler und Nenner zurückliefern
return double(zaehler)/double(nenner);
}
Eine Konvertierungsfunktion wird wie ein Konstruktor ebenfalls ohne Rückgabetyp (auch nicht
mit void) deklariert. Die Funktion liefert aber einen Rückgabewert, nämlich ein Objekt des Typs,
in den sie konvertieren soll, zurück. Insofern wird der Typ des Rückgabewerts implizit durch den
Namen der Konvertierungsfunktion definiert.
Der Typ, in den konvertiert wird, kann (analog zu Konstruktoren) auch ein Typ einer an-
deren Klasse sein. Weder Konstruktoren noch Konvertierungsfunktionen können aber Friend-
Funktionen sein, weshalb es in den Funktionen keinen direkten Zugriff auf private Komponenten
der fremden Klasse geben kann (dazu kann es jedoch Hilfsfunktionen geben).
Durch die Konvertierungsfunktion der Klasse Bruch kann ein Bruch immer dann verwendet
werden, wenn ein Objekt vom Typ double gebraucht wird. Zum Beispiel kann die Wurzelfunk-
tion der mathematischen C-Bibliothek aufgerufen werden:
// klassen/sqrt1.cpp
#include <iostream>
#include <cmath>
#include "bruch.hpp"

int main()
{
Bsp::Bruch x(1,4);
...
std::cout << std::sqrt(x) << std::endl; // Wurzel aus x als double ausgeben
}

4.6.5 Probleme bei der automatischen Typumwandlung


Mit der Möglichkeit, Integer in Brüche und Brüche in Gleitkommawerte umwandeln zu können,
lässt sich die Klasse Bruch gleich viel besser verwenden – sollte man meinen. Doch bereits das
bisherige Anwendungsbeispiel zeigt die Schattenseiten der automatischen Typumwandlung auf.
Sandini Bib

236 Kapitel 4: Programmieren von Klassen

Das Anwendungsprogramm, das am Anfang dieses Abschnitts vorgestellt wurde, kann damit
nämlich nicht mehr kompiliert werden:8
// klassen/btest6.cpp
// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// Headerdateien für die verwendeten Klassen einbinden


#include "bruch.hpp"

int main()
{
const Bsp::Bruch a(7,3); // Bruch-Konstante a deklarieren
Bsp::Bruch x; // Bruch-Variable x deklarieren

std::cout << a << std::endl; // Bruch a ausgeben

// Bruch x einlesen
std::cout << "Bruch eingeben (zaehler/nenner): ";
if (! (std::cin >> x)) {
// Eingabefehler: Programmabbruch mit Fehlerstatus
std::cerr << "Fehler beim Bruch-Eingeben" << std::endl;
return EXIT_FAILURE;
}
std::cout << "Eingabe war: " << x << std::endl;

// solange x < 1000


while (x < 1000) { // neu: statt while (x < Bsp::Bruch(1000))
// x mit a multiplizieren und ausgeben
x = x * a;
std::cout << x << std::endl;
}
}
Das Problem ist der am Anfang dieses Abschnitts eingeführte Ausdruck
x < 1000
Er ist mit der aktuellen Klassendeklaration nun mehrdeutig geworden:

8
Die Aussage, dass das Programm nicht mehr kompiliert werden kann, bezieht sich auf korrekte Compiler.
In der Praxis ist das Programm mitunter trotzdem übersetzbar. In dem Fall handelt es sich um einen Fehler
des Compilers.
Sandini Bib

4.6 Freunde und andere Typen 237

 Auf der einen Seite kann er wie besprochen als


x < Bruch(1000)
interpretiert werden.
 Die Tatsache, dass nun eine Konvertierungsfunktion zum Typ double existiert, ermöglicht
aber auch eine Interpretation als:
double(x) < double(1000)

Beide Interpretationen sind möglich und gleichwertig. Dies liegt daran, dass jede Interpretati-
on über selbst definierte Konvertierungen (sei es über Konstruktoren oder über Konvertierungs-
funktionen) unabhängig von weiteren Standard-Konvertierungen wie von int nach double die
gleiche Priorität besitzt. Wenn es zwei verschiedene gleichwertige Interpretationsmöglichkeiten
gibt, ist der Ausdruck immer mehrdeutig.
Die genauen Regeln zur automatischen Typumwandlung gehören zu den schwierigsten The-
men in C++ und werden in Abschnitt 10.3 erläutert.

Vermeide automatische Typumwandlungen!“



Dieses Beispiel lehrt eines: Funktionen zur automatischen Typumwandlung sollten vermieden
werden. Insbesondere Zyklen sind bei Funktionen zur automatischen Typumwandlung unbedingt
zu vermeiden.
Das bedeutet vor allem, dass es zu einem Konstruktor, der mit einem Argument aufgeru-
fen werden kann, keine Konvertierungsfunktion geben sollte, die die entsprechende umgekehrte
Umwandlung vornimmt. Genauso muss darauf geachtet werden, dass, wenn eine Klasse einen
Konstruktor für Objekte einer anderen Klasse definiert, diese nicht den umgekehrten Konstruktor
definiert.
Mit der Vermeidung von Funktionen zur automatischen Typumwandlung wird nicht nur die
Gefahr der Mehrdeutigkeit minimiert. Gleichzeitig wird der objektorientierten Idee stärker Rech-
nung getragen. Eigene Datentypen werden ja dazu geschaffen, um eine feste Menge von erlaub-
ten Operationen zu besitzen. Automatische Typumwandlungen weichen dieses Konzept auf.
Dies bedeutet nicht, dass grundsätzlich keine Typumwandlungen mehr möglich sind. Sie soll-
ten nur nach Möglichkeit durch explizite Funktionsaufrufe realisiert werden.
Gängige Praxis dazu ist es, Elementfunktionen wie asTyp()“ oder toTyp()“ zu definieren.
” ”
Im Beispiel der Klasse Bruch sollte also statt der Konvertierungsfunktion operator double()
besser die Elementfunktion toDouble () definiert werden:
namespace Bsp {
class Bruch {
...
// explizite Typumwandlung nach double
double toDouble () const;
};
}
Damit wird auch die Anwendung einer solchen Konvertierung deutlicher:
Sandini Bib

238 Kapitel 4: Programmieren von Klassen

// klassen/sqrt2.cpp
#include <iostream>
#include <cmath>
#include "bruch.hpp"

int main()
{
Bsp::Bruch x(1,4);
...
std::cout << std::sqrt(x.toDouble()) // Wurzel aus x als double ausgeben
<< std::endl;
}

4.6.6 Andere Anwendungen des Schlüsselworts friend


Mit dem Schlüsselwort friend kann im Prinzip jeder Funktion Zugriff auf die Interna einer
Klasse gewährt werden. Eine Ausnahme bilden nur die Operatoren =, (), [ ] und -> (die Ta-
belle auf Seite 574 liefert einen genauen Überblick, welche Funktionen Friend-Funktionen sein
dürfen).
Es ist aber auch möglich, eine andere Klasse vollständig zum Freund zu erklären.
Durch die Deklaration:
class X {
friend class Y;
...
};
bzw.
class Y;

class X {
friend Y;
...
};
erhalten alle Funktionen der Klasse Y Zugriff auf die Klasse X. Das überträgt sich aber nicht auf
Friend-Funktionen von Y ( der Freund meines Freundes ist nicht automatisch mein Freund“).

Eine Friend-Beziehung ist also nicht transitiv.
Eine Klasse zum Freund zu erklären wird mitunter dazu verwendet, um zwei Klassen, die
sehr eng zusammengehören, effektiver implementieren zu können (ein typisches Beispiel wären
die Klassen Matrix und Vektor).
Sandini Bib

4.6 Freunde und andere Typen 239

4.6.7 friend kontra objektorientierte Programmierung


Einer der Lieblingsstreitpunkte über C++ ist die Frage, ob die Verwendung des Schlüsselworts
friend mit der Idee der objektorientierten Programmierung zu vereinbaren ist. Dazu ist Folgen-
des zu bemerken:
C++ ist eine Sprache, die nicht zu objektorientierter Programmierung zwingt, sondern sie nur
unterstützt. Diese Unterstützung kann auf vielfältige Weise umgangen werden. Die Frage, ob das
Schlüsselwort friend mit objektorientierter Programmierung zu vereinbaren ist, kann somit –
wie die Frage, ob C++ objektorientiert ist – weder mit nein noch mit ja beantwortet werden. Es
hängt davon ab, wie man es verwendet.
Zunächst kann das Schlüsselwort friend dazu verwendet werden, durch die globale Dekla-
ration einer Funktion die Aufrufmöglichkeiten einer Elementfunktion zu erweitern. Das zeigen
die in diesem Abschnitt besprochenen Beispiele der Multiplikation oder der Kehrwert-Funktion.
In diesem Fall ist die Verwendung von friend zunächst unkritisch, da es sich nur um eine an-
dere Form der Implementierung der Operationen für eine Klasse handelt. Da diese Funktionen
mit der Klasse definiert werden, wird insbesondere der Zugriff auf interne Daten in keiner Weise
erweitert. In solch einem Fall ist die Verwendung der Friend-Funktion eine Konzession an die
Tatsache, dass einiges in C++ (aus Gründen der Effizienz oder der Kompatibilität zu C) über
Hintertürchen“ zu programmieren ist. Letztlich ist es dann auch Geschmackssache, ob man

Funktionen mit der globalen Syntax oder der Syntax für Elementfunktionen aufrufen will.
Friend-Funktionen können allerdings bei Vererbung und Polymorphie Probleme bereiten.
Darauf wird in Abschnitt 6.3.3 noch eingegangen. Insofern stehen Friend-Funktionen den Kon-
zepten der objektorientierten Idee im Wege.
Eine andere Möglichkeit von Friend-Deklarationen ist es, den Zugriff auf private Daten ei-
ner Klasse generell auf eine andere Klasse zu übertragen. An dieser Stelle wird das Prinzip der
strengen Datenkapselung aufgeweicht. Der Zugriff auf die privaten Daten einer Klasse ist da-
mit nicht mehr ausschließlich auf die Funktionen beschränkt, die als Elementfunktion oder als
globale Friend-Funktion in der Klassenstruktur deklariert werden.
Es hat sich aber gezeigt, dass damit unter Umständen ein erheblicher Laufzeitvorteil zu er-
reichen ist. Wenn z.B. das Produkt eines Vektors mit einer Matrix gebildet werden soll, muss auf
Interna beider Klassen zugegriffen werden. Ohne einen direkten Zugriff müsste jeder Zugriff auf
ein Element im Vektor bzw. in der Matrix über eine Elementfunktion durchgeführt werden.
Eine solche Verwendung der Friend-Funktionen weicht also das Konzept der strengen Da-
tenkapselung zugunsten eines Laufzeitvorteils auf. Da die Laufzeit immer noch ein wichtiges
Kriterium von Programmen ist, wird dies in der Praxis oft (und gern) in Kauf genommen.
Jeder sollte sich klar machen, dass die Verwendung einer Friend-Funktion die Überprüfung
und Wartung einer Klasse erheblich erschweren kann, da der Zugriff auf außenliegende Funk-
tionen übertragen wird. Am besten verwendet man eine Friend-Deklaration immer mit einem
schlechten Gewissen, damit man nicht irgendwann anfängt, das Schlüsselwort friend nur aus
Bequemlichkeit einzuführen.
Unstrittig ist auf jeden Fall, dass man mit dem Schlüsselwort friend jede Menge Unsinn
anstellen kann. Aber jeder kennt die Erfahrung, wenn man sich die falschen Personen, pardon
Klassen, zum Freund macht. Aus diesem Grund sollte eine Deklaration von Friend-Klassen im-
mer deutlich gekennzeichnet werden.
Sandini Bib

240 Kapitel 4: Programmieren von Klassen

Um Missverständnissen vorzubeugen, sei ausdrücklich darauf hingewiesen, dass es auch mit


friend nicht möglich ist, den Zugriff auf die privaten Komponenten einer Klasse nachträglich
zu erweitern. Die Klasse selbst entscheidet in ihrer Deklaration, wen sie zum Freund hat. Es kann
sich niemand zum Freund erklären.

4.6.8 Zusammenfassung
 Ein Konstruktor, der mit nur einem Parameter aufgerufen werden kann, definiert damit die
Möglichkeit einer automatischen oder expliziten Typumwandlung von dem Typ des Para-
meters in ein Objekt der Klasse.
 Mit explicit kann die Definition einer automatischen Typumwandlung durch einen Kon-
struktor verhindert werden.
 Konvertierungsfunktionen bieten eine Möglichkeit, ein Objekt einer Klasse implizit oder ex-
plizit in einen anderen Typ umzuwandeln. Sie werden durch operator typ() deklariert und
besitzen eine Return-Anweisung, werden aber ohne Rückgabetyp deklariert.
 Bei Funktionen zur automatischen Typumwandlung drohen Mehrdeutigkeiten. Konvertie-
rungsfunktionen sollten deshalb vermieden und Konstruktoren mit nur einem Parameter mit
Vorsicht eingesetzt werden.
 Durch das Schlüsselwort friend können einzelne Operationen oder ganze Klassen Zugriff
auf alle privaten Komponenten einer Klasse erhalten.
 Um eine automatische Typumwandlung für den ersten Operanden einer als Elementfunk-
tion implementierten Operatorfunktion zu ermöglichen, muss diese global deklariert werden.
Durch die Deklaration als friend kann deren Implementierung trotzdem Zugriff auf private
Komponenten erhalten.
 friend ist nicht transitiv ( der Freund meines Freundes ist nicht unbedingt mein Freund“).

Sandini Bib

4.7 Ausnahmebehandlung für Klassen 241

4.7 Ausnahmebehandlung für Klassen


Nachdem in Abschnitt 3.6 bereits das Konzept der Ausnahmebehandlung erläutert wurde, wird
in diesem Abschnitt gezeigt, wie man in Klassen Ausnahmen auslöst und dazu passende Aus-
nahmeklassen entwirft. Hinzu kommen weitere Details zum Umgang mit Ausnahmen.

4.7.1 Motivation für eine Ausnahmebehandlung in der Klasse Bruch


Eines der typischen Probleme der herkömmlichen Fehlerbehandlung wird schon bei der ersten
Implementierung der Klasse Bruch (siehe auch Abschnitt 4.1.7) deutlich: Bei der Implementie-
rung der Klasse Bruch kann im Konstruktor zwar der Fehler, dass 0 zum Initialisieren des Nen-
ners übergeben wird, erkannt werden, eine vernünftige Fehlerbehandlung ist aber nicht möglich.
Dies liegt daran, dass bei der Implementierung der Klasse über die Umstände, unter denen sie
angewendet wird, im Allgemeinen keine Annahmen gemacht werden können. Entsprechend ist
auch nicht bekannt, in welcher Situation 0 zur Initialisierung des Nenners übergeben wurde. In
der Praxis ist es sogar so, dass es verschiedene Ursachen für einen fehlerhaften Aufruf geben
kann und somit gar keine sinnvolle einheitliche Reaktion möglich ist. Aus diesem Grund wurde
das Programm bisher einfach mit einer entsprechenden Fehlermeldung beendet:
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
*/
if (n == 0) {
// Programm mit Fehlermeldung beenden
std::cerr << "Fehler: Nenner ist 0" << std::endl;
std::exit(EXIT_FAILURE);
}
...
}
Das Problem beschränkt sich aber nicht nur auf Konstruktoren. Ein ähnliches Problem existiert
z.B. für eine String-Klasse oder eine Mengenklasse, wenn mit dem Operator [ ] auf ein Zeichen
oder Element zugegriffen wird. In der Funktion kann zwar erkannt werden, wenn ein fehlerhafter
Index übergeben wird, es besteht aber keine Möglichkeit, den Fehler sinnvoll zu behandeln, da
die Aufrufumstände nicht bekannt sind und keine geeigneten Möglichkeiten zur Fehlermeldung
bestehen. Wie soll im Ausdruck
s[i] = 'q'; // hoffentlich ist der Index i nicht zu groß
getestet werden, ob der Index i für s zu groß ist?
Das Grundproblem ist in beiden Fällen gleich: Es treten Fehlersituationen auf, die zwar er-
kannt, nicht aber sinnvoll behandelt werden können, da der Kontext, aus dem heraus der Fehler
verursacht wird, nicht bekannt ist. Andererseits kann der Fehler auch nicht an die Aufrufumge-
Sandini Bib

242 Kapitel 4: Programmieren von Klassen

bung zurückgemeldet werden, da Rückgabewerte entweder gar nicht definiert sind oder anderen
Zwecken dienen.
Hier hilft das Konzept der Ausnahmebehandlung: An beliebiger Stelle im Code können Feh-
ler erkannt und als Ausnahme an die jeweilige Aufrufumgebung gemeldet werden. In dieser
Umgebung kann der Fehler dann abgefangen und entsprechend der aktuellen Situation sinnvoll
behandelt werden. Geschieht dies nicht, wird der Fehler nicht einfach ignoriert, sondern führt zu
einem (definierbaren) geregelten Programmende und nicht zu einem Programmabbruch.

4.7.2 Ausnahmebehandlung am Beispiel der Klasse Bruch


Da es zu jeder Art von Ausnahme oder Fehler eine Klasse geben muss, müssen bei der Dekla-
ration einer Klasse, die die Ausnahmebehandlung verwendet, auch die dazugehörigen Ausnah-
meklassen deklariert werden. Wenn Fehlerklassen nicht für mehrere Klassen verwendet werden,
geschieht dies sinnvollerweise, indem die Fehlerklassen als eingebettete Klassen innerhalb des
Gültigkeitsbereichs deklariert werden, zu dem die Fehler gehören (eingebettete Klassen werden
in Abschnitt 6.5.4 vorgestellt).
Die folgende Version der Klasse Bruch definiert für die Fehlerbehandlung die Fehlerklasse
NennerIstNull:
// klassen/bruch8.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

private:
int zaehler;
int nenner;

public:
/* neu: Fehlerklasse
*/
class NennerIstNull {
};

/* Default-Konstruktor, Konstruktor aus Zähler und


* Konstruktor aus Zähler und Nenner
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 243

*/
Bruch (int = 0, int = 1);

/* Multiplikation
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend Bruch operator * (const Bruch&, const Bruch&);

// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

/* Vergleich
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend bool operator < (const Bruch&, const Bruch&);

// Ein- und Ausgabe mit Streams


void printOn (std::ostream&) const;
void scanFrom (std::istream&);

// Typumwandlung nach double


double toDouble () const;
};

/ * Operator *
* - globale Friend-Funktion
* - inline definiert
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}

/* Standard-Ausgabeoperator
* - global überladen und inline definiert
*/
Sandini Bib

244 Kapitel 4: Programmieren von Klassen

inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

/* Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Wie man sieht, wird die Fehlerklasse NennerIstNull innerhalb der Klasse Bruch deklariert:
class Bruch {
...
public:
class NennerIstNull {
};
...
};
Dabei handelt es sich um die kürzeste Deklaration, die für Klassen überhaupt möglich ist: Es wer-
den überhaupt keine Komponenten deklariert. Aufgrund des vordefinierten Default-Konstruktors
kann ein Objekt der Klasse aber dennoch erzeugt werden. Der ganze Sinn eines Objekts dieser
Klasse besteht nur in seiner Existenz.
Bei der Implementierung wird nun, wenn der Nenner 0 ist, ein entsprechendes Fehlerobjekt
erzeugt:
// klassen/bruch8.cpp
// Headerdatei der Klasse einbinden
#include "bruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 245

/* Default-Konstruktor, Konstruktor aus ganzer Zahl,


* Konstruktor aus Zähler und Nenner
* - Default für z: 0
* - Default für n: 1
*/
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n == 0) {
// neu: Ausnahme: Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

/* Operator *=
*/
const Bruch& Bruch::operator *= (const Bruch& b)
{
// ”x *= y” ==> ”x = x * y”
*this = *this * b;

// Objekt (erster Operand) wird zurückgeliefert


return *this;
}

/* Operator <
* - globale Friend-Funktion
*/
bool operator < (const Bruch& a, const Bruch& b)
{
Sandini Bib

246 Kapitel 4: Programmieren von Klassen

// da die Nenner nicht negativ sein können, reicht:


return a.zaehler * b.nenner < b.zaehler * a.nenner;
}

/* printOn
* - Bruch auf Stream strm ausgeben
*/
void Bruch::printOn (std::ostream& strm) const
{
strm << zaehler << '/' << nenner;
}

/* scanFrom
* - Bruch von Stream strm einlesen
*/
void Bruch::scanFrom (std::istream& strm)
{
int z, n;

// Zähler einlesen
strm >> z;

// optionales Trennzeichen ’/’ und Nenner einlesen


if (strm.peek() == '/') {
strm.get();
strm >> n;
}
else {
n = 1;
}

// Lesefehler?
if (! strm) {
return;
}

// Nenner == 0?
if (n == 0) {
// neu: Ausnahme mit Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 247

/* OK, eingelesene Werte zuweisen


* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}

// Typumwandlung nach double


double Bruch::toDouble () const
{
// Quotient aus Zähler und Nenner zurückliefern
return double(zaehler)/double(nenner);
}

} // **** ENDE Namespace Bsp ********************************

Wie man sieht, wurde der Code im Konstruktor nun entsprechend modifiziert. Tritt der Fehler
auf, wird das Fehlerobjekt erzeugt und in die Umgebung des Programms geworfen“:

Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
*/
if (n == 0) {
// neu: Ausnahme mit Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}
...
}
Die Anweisung throw entspricht im Programmablauf einer Folge von Return-Anweisungen, die
in allen Blöcken, in denen sich das Programm zur Zeit befindet, unmittelbar aufgerufen werden.
Sämtliche Gültigkeitsbereiche, in denen sich ein Programm zu dem Zeitpunkt befindet, werden
also sofort verlassen, bis der Fehler entweder in irgendeinem Block abgefangen und behandelt
oder das Programm beendet wird.
Sandini Bib

248 Kapitel 4: Programmieren von Klassen

Es handelt sich allerdings um keinen Sprung zum nächsten übergeordneten catch, das den Feh-
ler behandelt, sondern um einen geordneten Rückzug“. In allen Blöcken werden für die darin

angelegten lokalen Objekte die Destruktoren aufgerufen. Objekte, die explizit mit new ange-
legt wurden, bleiben aber erhalten und müssen im Catch-Bereich gegebenenfalls explizit zerstört
werden. Dieser Vorgang wird auch als Stack-Unwinding bezeichnet. Der Programm-Stack wird
abgearbeitet, bis eine Fehlerbehandlung definiert ist.
Selbst wenn der Fehler nicht behandelt wird und zum Programmende führt, wird das Pro-
gramm also nicht einfach verlassen, sondern geordnet beendet. Im Gegensatz zu exit() werden
alle Destruktoren aufgerufen (exit() ruft nur die Destruktoren der statischen Objekte auf). An-
gesichts der Tatsache, dass ein lokales Objekt eine geöffnete Datei oder eine laufende Datenban-
kanfrage repräsentieren kann, ist dies ein wichtiger Unterschied.

Fehlerbehandlung
Die Behandlung des Fehlers geschieht in der Programmumgebung, die den Fehler direkt oder
indirekt verursacht hat. Das folgende Anwendungsprogramm zeigt, wie dies aussehen kann:
// klassen/btest8.cpp
// Standard-Headerdateien einbinden
#include <iostream>
#include <cstdlib>

// Headerdateien für die verwendeten Klassen einbinden


#include "bruch.hpp"

int main()
{
Bsp::Bruch x; // Bruch-Variable

/* Versuche, den Bruch x einzulesen, und fange


* Ausnahmen vom Typ NennerIstNull ab
*/
try {
int z, n;
std::cout << "Zaehler: ";
std::cin >> z;
std::cout << "Nenner: ";
std::cin >> n;
x = Bsp::Bruch(z,n);
std::cout << "Eingabe war: " << x << std::endl;
}
catch (const Bsp::Bruch::NennerIstNull&) {
/* Programm mit einer entsprechenden
* Fehlermeldung beenden
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 249

*/
std::cerr << "Eingabefehler: Nenner darf nicht Null sein"
<< std::endl;
return EXIT_FAILURE;
}

// diese Stelle wird nur erreicht, wenn x erfolgreich eingelesen wurde


...
}
Für den durch try eingeschlossenen Bereich wird eine spezielle Fehlerbehandlung installiert,
die durch die folgende Catch-Anweisung definiert wird:
try {
int z, n;
std::cout << "Zaehler: ";
std::cin >> z;
std::cout << "Nenner: ";
std::cin >> n;
x = Bsp::Bruch(z,n);
std::cout << "Eingabe war: " << x << std::endl;
}
catch (const Bsp::Bruch::NennerIstNull&) {
...
}
Tritt irgendwo im Try-Block eine beliebige Ausnahme auf, wird dieser Block sofort verlassen.
Handelt es sich bei dieser Ausnahme um eine Ausnahme vom Typ Bruch::NennerIstNull,
werden die Anweisungen im dazugehörigen Catch-Block ausgeführt. Nach der Behandlung im
Catch-Block geht es mit der nächsten Anweisung hinter dem Catch-Block weiter.
Wird im konkreten Beispiel etwa null als Nenner n eingegeben, dann wird durch die Anwei-
sung
x = Bsp::Bruch(z,n);
eine entsprechende Ausnahme im Konstruktor auslöst. Damit werden der Konstruktor und der
gesamte Try-Block sofort verlassen. Die nachfolgende Ausgabeanweisung
std::cout << "Eingabe war: " << x << std::endl;
wird also nicht mehr durchgeführt.
Bei jeder anderen Ausnahme wird mangels Behandlung die gesamte Funktion main() ver-
lassen.
Man beachte, dass die Ausnahme im Catch-Block als konstante Referenz übergeben wird. Für
Catch-Blöcke gilt das Gleiche, was für Funktionen mit Parametern gilt: Sofern nichts anderes
angegeben wird, werden Kopien übergeben. Durch die Verwendung einer Referenz wird das
unnötige Kopieren des Ausnahmeobjekts vermieden.
Sandini Bib

250 Kapitel 4: Programmieren von Klassen

Ausnahme oder I/O-Fehler?


Eine interessante Frage ist, ob man bei der Klasse Bruch überhaupt eine Ausnahme auslösen
sollte, wenn man beim Einlesen eines Bruchs eine Null als Nenner bekommt:
if (n == 0) {
// Ausnahme mit Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}
Anstatt ein entsprechendes I/O-Flag zu setzen, würde in dem Fall also auch die Ausnahme-
behandlung verwendet werden. Ein-/Ausgaben sind aber ein gutes Beispiel für den Unterschied
zwischen Ausnahmen und Fehlern. Eingabefehler sind etwas ganz Normales. Es ist somit üblich,
bei fehlerhaften Formaten ein entsprechendes Status-Flag im Stream zu setzen (vgl. die Imple-
mentierung auf Seite 216ff.). Man könnte auf diese Weise zwar andere Eingabefehler von dem
speziellen Fehler der Eingabe einer Null als Nenner unterscheiden, doch verkompliziert das an-
dererseits auch die Schnittstelle. Die Grenzen zwischen Ausnahme und Fehler sind natürlich
fließend und insofern ist eine derartige Entscheidung immer auch eine Designfrage.

4.7.3 Fehlerklassen
Fehlerklassen sind Klassen wie alle anderen auch. Das bedeutet, sie können Komponenten und
Elementfunktionen besitzen. Prinzipiell kann jeder Datentyp (Klassen und fundamentale Daten-
typen) als Datentyp für Ausnahmen dienen. Die besondere Bedeutung erhalten diese Klassen
erst dadurch, dass sie bei throw oder catch verwendet werden. Man kann z.B. auch Strings als
Ausnahmeobjekte verwenden. Die Anweisung
throw "Ausnahme: ...";
lösen eine Ausnahme vom Typ const char* aus, die man dann z.B. mit
catch (const char* s) {
std::cerr << s << std::endl;
}
behandeln kann.
Wenn Ausnahmeklassen Komponenten besitzen, sind dies die Attribute der Ausnahme. Da-
mit können Ausnahmen bzw. Fehler parametrisiert werden. Selbstverständlich werden dann auch
Konstruktoren benötigt, um diese Komponenten zu initialisieren. Werden dynamische Kompo-
nenten verwendet, kann auch ein Destruktor definiert werden. Dieser wird nach der Fehlerbe-
handlung bei der Zerstörung des Fehlerobjekts aufgerufen.
Die Verwendung eines unerlaubten Index in einem Feld (Array) ist ein klassisches Beispiel
für die Verwendung von Parametern in Fehlerobjekten. Wenn auf einen String mit einem un-
erlaubten Index zugegriffen wird, ist für die Fehlerbehandlung im Allgemeinen von Interesse,
welcher Wert fälschlicherweise als Index verwendet wurde. Im Abschnitt 6.2.7 wird ein entspre-
chendes Beispiel vorgestellt und erläutert.
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 251

4.7.4 Weitergabe von Fehlern


Es ist möglich, einen Fehler, der in einem Catch-Bereich behandelt wird, wieder auszulösen, um
dafür eine übergeordnete Fehlerbehandlung anzustoßen. Dies ist vor allem dann sinnvoll, wenn
auf einen Fehlerfall zwar reagiert werden muss, der Fehler aber nicht behandelt werden kann.
Ein typisches Beispiel dafür ist das Schließen von geöffneten Dateien oder das Freigeben
von angelegtem Speicher, sofern dies nicht von Destruktoren übernommen wird. Das folgende
Beispiel gibt auf diese Weise im Fehlerfall explizit angelegten Speicher frei, ohne den Fehler zu
behandeln:
std::string* createNewString ()
{
std::string* newString = NULL; // Zeiger auf (angelegten) String

try {
newString = new std::string("Tasse"); // String explizit anlegen
... // und modifizieren
}
catch (...) {
/* bei jeder Ausnahme explizit angelegten String freigeben
* und Ausnahme zur eigentlichen Behandlung weiter nach außen reichen
*/
delete newString;
throw;
}

// Zeiger auf angelegten String (normalerweise) zurückliefern


return newString;
}
Die Anweisung throw ohne die Angabe einer Klasse sorgt bei der Fehlerbehandlung dafür, dass
der Fehler erneut in das Programm geworfen wird, um von weiter außerhalb behandelt zu werden.
Ein derartiges rethrow“ wird auch benötigt, um eine Hilfsfunktion zur Behandlung von ver-

schiedenen Ausnahmen zu implementieren. Darauf wird in Abschnitt 3.6.6 eingegangen.

4.7.5 Ausnahmen in Destruktoren


Eine Überlagerung zweier Ausnahmen ist nicht möglich. Wenn in einem Destruktor, der auf-
grund einer Ausnahme aufgerufen wird, wiederum eine Ausnahme ausgelöst wird, wird deshalb
die normale Ausnahmebehandlung abgebrochen und die Funktion std::terminate() aufge-
rufen (siehe Abschnitt 3.6.5). Insofern sollte man grundsätzlich darauf achten, dass Destruktoren
keine Ausnahmen auslösen.
Sandini Bib

252 Kapitel 4: Programmieren von Klassen

4.7.6 Ausnahmen in Schnittstellen-Deklarationen


Die Menge möglicher Ausnahmen gehört zu einer Funktionsbeschreibung wie Parameter und
Rückgabewerte. Um dies zu unterstützen, kann für eine Funktion deklariert werden, welche Aus-
nahmen nach außen gereicht werden können. Dazu wird nach der Parameterliste mit throw eine
Ausnahmenspezifikation oder Ausnahmenliste (englisch: throw specification) angegeben.
Dies kann z.B. wie folgt aussehen:
void f () throw (Bruch::NennerIstNull, String::RangeError);
Die Deklaration legt fest, dass von der Funktion nur die angegebenen Ausnahmen ausgeworfen
werden können. Wird keine Ausnahmenspezifikation deklariert, können alle möglichen Ausnah-
men auftreten. Eine leere Liste zeigt an, dass keine Ausnahmen ausgelöst werden können:
void f () throw ();

Unerwartete Ausnahmen
Die Ausnahmenspezifikation beschreibt nur, welche Ausnahmen nach außen gereicht werden
können. Intern können andere Ausnahmen auftreten und behandelt werden. Tritt innerhalb der
Funktion eine Ausnahme auf, die weder behandelt wird noch in der Liste nach außen gereich-
ter Ausnahmen enthalten ist, wird die Funktion std::unexpected() aufgerufen. Diese Funk-
tion ist so vordefiniert, dass sie wiederum std::terminate() (und damit im Allgemeinen
std::abort()) aufruft.
Auch hier kann mit std::set_unexpected() eine alternative Unexpected-Funktion defi-
niert werden, die weder Parameter noch Rückgabewerte besitzen darf. Die bisherige Unexpected-
Funktion wird jeweils zurückgeliefert.

Klasse std::bad_exception
Falls die Klasse std::bad_exception Teil einer Ausnahmenspezifikation ist, löst die Funk-
tion std::unexpected() automatisch eine Ausnahme dieses Typs aus, wenn eine unerwartete
Ausnahme auftritt:
void f () throw (Bruch::NennerIstNull, std::bad_exception)
{
...
throw String::RangeError(); // ruft unexpected() auf,
... // was std::bad_exception auslöst
}
Wenn eine Ausnahmenspezifikation also die Klasse std::bad_exception umfasst, wird jede
Ausnahme, die nicht zu einem der sonst aufgelisteten Typen gehört innerhalb der Funktion durch
eine Ausnahme vom Typ std::bad_exception ersetzt. (es sein denn, mit set_unexpected()
wurde eine Funktion installiert, die das nicht macht).
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 253

4.7.7 Hierarchien von Fehlerklassen


Fehlerklassen können in Hierarchien organisiert werden. Damit können allgemeine Fehlerarten
spezialisiert bzw. verschiedene Fehlerarten unter einem Oberbegriff zusammengefasst werden.
Ein Anwendungsprogramm besitzt dann eine einfache Möglichkeit, je nach Situation alle Fehler
gemeinsam oder spezielle Fehler einzeln zu behandeln.
Zum Bilden von Fehlerklassen-Hierarchien wird das Konzept der Vererbung verwendet, das
allerdings erst in Kapitel 5 eingeführt wird. Insofern ist das Folgende ein Vorgriff auf Themen,
die eigentlich erst später behandelt werden. Die hier verwendeten Beispiele sollten aber dennoch
verständlich sein.

Beispiel für eine Fehlerklassen-Hierarchie


Als Aufhänger für eine Fehlerklassen-Hierarchie soll wieder die Klasse Bruch verwendet wer-
den. Bei der Verwendung der Klasse können unterschiedliche Fehler auftreten. Um die Fehler
unterschiedlich behandeln zu können, wird eine entsprechende Hierarchie definiert (siehe Abbil-
dung 4.5).

B r u c h : : B r u c h f e h l e r

B r u c h : : N e n n e r I s t N u l l B r u c h : : L e s e f e h l e r

Abbildung 4.5: Einfache Hierarchie von Fehlerklassen

Unter der allgemeinen Fehlerart Bruchfehler gibt es die Spezialfälle NennerIstNull und
Lesefehler. Entsprechend werden drei Klassen deklariert, wobei die Spezialfälle von dem all-
gemeinen Fall abgeleitet werden.
Die Deklaration der Klasse Bruch erhält dazu folgenden Aufbau:
// klassen/bruch10.hpp
class Bruch {
private:
int zaehler;
int nenner;

public:
/* Fehlerklassen:
* - neu: allgemeine Basisklasse mit zwei abgeleiteten Klassen
*/
Sandini Bib

254 Kapitel 4: Programmieren von Klassen

class Bruchfehler {
};
class NennerIstNull: public Bruchfehler {
};
class Lesefehler : public Bruchfehler {
};

/* Default-Konstruktor, Konstruktor aus Zähler und


* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);

/* Ein- und Ausgabe mit Streams


*/
void printOn (std::ostream&) const;
void scanFrom (std::istream&);
...
};
In der Quelldatei werden im Fehlerfall die entsprechenden speziellen Fehlerobjekte generiert:
// klassen/bruch10.cpp
/* Default-Konstruktor, Konstruktor aus ganzer Zahl,
* Konstruktor aus Zähler und Nenner
* - Default für z: 0
* - Default für n: 1
*/
Bruch::Bruch (int z, int n)
{
/* Zähler und Nenner wie übergeben initialisieren
* - 0 als Nenner ist allerdings nicht erlaubt
* - ein negatives Vorzeichen des Nenners kommt in den Zähler
*/
if (n == 0) {
// Ausnahme: Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}
if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 255

zaehler = z;
nenner = n;
}
}

/* scanFrom
* - Bruch von Stream strm einlesen
*/
void Bruch::scanFrom (istream& strm)
{
int z, n;

// Zähler einlesen
strm >> z;

// optionales Trennzeichen und Nenner ’/’ einlesen


if (strm.peek() == '/') {
strm.get();
strm >> n;
}

// Lesefehler?
if (! strm) {
return;
}

// Nenner == 0?
if (n == 0) {
// Ausnahme mit Fehlerobjekt für 0 als Nenner auslösen
throw NennerIstNull();
}

// OK, eingelesene Zahlen zuweisen


if (n < 0) {
zaehler = -z;
nenner = -n;
}
else {
zaehler = z;
nenner = n;
}
}
Sandini Bib

256 Kapitel 4: Programmieren von Klassen

Im Anwendungsprogramm kann nun in main() mit einer Catch-Anweisung jede Art von Fehler
der Klasse Bruch behandelt werden, indem die allgemeine Basisklasse der Hierarchie verwendet
wird:
// klassen/btest10.cpp
int main()
{
try {
Bsp::Bruch x;
...
x = liesBruch();
...
}
catch (Bsp::Bruch::Bruchfehler) {
// main() mit Fehlermeldung und Fehlerstatus beenden
std::cerr << "Exception durch Fehler in Klasse Bruch"
<< std::endl;
return EXIT_FAILURE;
}
}
Speziell beim Einlesen kann ein Eingabefehler, der in scanFrom() auftritt, als Anlass zum er-
neuten Einlesen behandelt werden:
// klassen/btest10b.cpp
Bsp::Bruch liesBruch ()
{
Bsp::Bruch x; // Bruch-Variable
bool fehler; // Fehler aufgetreten?

do {
fehler = false; // zunächst mal kein Fehler

/* Versuche, den Bruch x einzulesen, und fange


* Fehler vom Typ NennerIstNull ab
*/
try {
std::cout << "Bruch eingeben (zaehler/nenner): ";
std::cin >> x;
std::cout << "Eingabe war: " << x << std::endl;
}
catch (Bsp::Bruch::NennerIstNull) {
/* Fehlermeldung ausgeben und Schleife nochmal
*/
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 257

std::cout << "Eingabefehler: Nenner darf nicht Null sein"


<< std::endl;
fehler = true;
}
} while (fehler);

return x; // eingelesenen Bruch zurückliefern


}

4.7.8 Design von Fehlerklassen


Nach dem gleichen Muster sollte für zusammengehörende Fehlerarten immer eine allgemeine
Fehlerklasse als Basisklasse deklariert werden, von der alle speziellen Fehlerklassen abgeleitet
werden (siehe Abbildung 4.6).

F e h l e r

M a t h e m a t i s c h e r F e h l e r I O F e h l e r

K e i n e Z a h l N u l l D i v i s i o n U e b e r l a u f L e s e f e h l e r S c h r e i b f e h l e r

Abbildung 4.6: Beispiel für eine Fehlerklassen-Hierarchie

Zum einen ergibt sich daraus der Vorteil, dass verschiedene Fehler mit nur einer Catch-Anweisung
abgefangen werden können. Zum anderen müssen in einer Ausnahmenspezifikation einer Funk-
tion, die mehrere Ausnahmen zurückliefern kann, nicht alle Ausnahmen deklariert werden. Statt
void f () throw (KeineZahl, NullDivision, Ueberlauf);
reicht die Angabe der gemeinsamen Basisklasse:
void f () throw (MathematischerFehler);
Soll für mehrere Klassen mit der Ausnahmebehandlung eine gemeinsame Fehlerbehandlung im-
plementiert werden, empfiehlt es sich ebenfalls, dafür eine gemeinsame Basisklasse für alle Aus-
nahmen zu definieren.
Sandini Bib

258 Kapitel 4: Programmieren von Klassen

Fehlermeldung als Komponente


Eine gemeinsame Basisklasse für alle Fehlerklassen sollte eine Komponente für eine Fehlermel-
dung definieren, die dann zu jedem Fehlerobjekt gehört und ausgegeben werden kann, wenn der
Fehler nicht anderweitig behandelt wird:
namespace Bsp {
class Fehler {
public:
std::string message; // Default-Fehlermeldung

// Konstruktor: Fehlermeldung initialisieren


Fehler (const string& s) message(s) {
}
...
};
}
In main() könnte dann die Fehlermeldung einfach ausgegeben werden:
int main()
{
try {
...
}
catch (const Bsp::Fehler& fehler) {
std::cerr << "FEHLER: " << fehler.message << std::endl;
return EXIT_FAILURE;
}
catch (...) {
std::cerr << "FEHLER: unbekannte Exception" << std::endl;
return EXIT_FAILURE;
}
}
Bei den Standard-Ausnahmen übernimmt what() diese Aufgabe (siehe Abschnitt 3.6.4).

4.7.9 Standard-Ausnahmen auslösen


Man kann auch selbst Standard-Ausnahmen auslösen. Dies hat den Vorteil, dass Anwendungs-
programmierer diese Ausnahmen wie andere Standard-Ausnahmen behandeln können. Der Nach-
teil besteht darin, dass man nicht ohne weiteres erkennen kann, ob eine Ausnahme von der Stan-
dardbibliothek oder einer anderen Klasse ausgelöst wurde.
Um eine Standard-Ausnahme auszulösen, muss man throw nur ein mit einem string ini-
tialisiertes Objekt der Standard-Ausnahmeklasse (siehe Seite 101) übergeben. Der übergebene
Sandini Bib

4.7 Ausnahmebehandlung für Klassen 259

String ist der String, der dann bei Auswertung der Ausnahme von what() geliefert wird. Bei-
spiel:
std::string s;
...
throw std::out_of_range(s);
Da es eine implizite Typumwandlung von const char* nach string gibt, kann man auch
String-Literale zur Initialisierung verwenden:
throw std::out_of_range("Index ist zu gross");
Die Standard-Ausnahmeklassen, die diese Fähigkeit unterstützen, sind die Klassen logic_error
und runtime_error sowie die davon abgeleiteten Klassen. Alle anderen Standard-Ausnahme-
klassen sind dafür nicht vorgesehen.

4.7.10 Zusammenfassung
 Klassen sollten im Fehlerfall Ausnahmen auslösen. Als Datentyp für Ausnahmen sollten (in-
terne) Hilfsklassen dienen.
 Destruktoren sollten keine Ausnahmen auslösen.
 Mit Hilfe von Ausnahmenspezifikationen kann für Funktionen deklariert werden, welche Aus-
nahmen sie auslösen können.
 Fehlerklassen für die Ausnahmebehandlung können hierarchisch angeordnet werden, um all-
gemeine Fehlerarten durch spezielle Fehlerarten zu verfeinern oder verschiedene Fehlerarten
zusammenfassen zu können.
 Klassen können auch Standard-Ausnahmen auslösen.
Sandini Bib
Sandini Bib

Kapitel 5
Vererbung und Polymorphie

Dieses Kapitel stellt nach der Programmierung mit Klassen und der Datenkapselung zwei wei-
tere wesentliche Merkmale der objektorientierten Programmierung vor: die Vererbung und die
Polymorphie.
Die Vererbung ermöglicht es, Eigenschaften einer Klasse von einer anderen Klasse abzulei-
ten. Das bedeutet, dass für neue Klassen nur noch Neuerungen implementiert werden müssen.
Eigenschaften, die schon einmal in einer anderen Klasse implementiert wurden, können über-
nommen werden und müssen nicht noch einmal implementiert werden. Dies führt neben einer
Einsparung an Code zu einem Konsistenzvorteil, da gemeinsame Eigenschaften nicht an mehre-
ren Stellen stehen und somit nur einmal geprüft oder geändert werden müssen.
Die Polymorphie ermöglicht es, mit Oberbegriffen zu arbeiten (ein Abstraktionsmittel, das
im täglichen Leben ständig verwendet wird). Verschiedenartige Objekte können zeitweise unter
einem Oberbegriff zusammengefasst werden, ohne dass ihre unterschiedlichen Eigenschaften
verloren gehen.
Vererbung ist eine Voraussetzung für Polymorphie, da für den jeweiligen Oberbegriff eine
Klasse definiert werden muss, von der die Klassen, die sich unter diesem Oberbegriff zusam-
menfassen lassen, jeweils abgeleitet werden. Wird ein Objekt unter seinem Oberbegriff verwaltet,
nimmt es zeitweise den Typ der dafür definierten Klasse an. Die Eigenschaft, dass es in Wirk-
lichkeit aber ein spezielles Objekt ist, geht dabei nicht verloren. Bei einem Funktionsaufruf wird
zur Laufzeit automatisch festgestellt, welche Klasse ein Objekt tatsächlich hat und die jeweils
dafür definierte Funktion aufgerufen.

Terminologie zur Vererbung


Die Begriffswelt für die Vererbung ist sehr vielfältig, was zum Teil daran liegt, dass jede objekt-
orientierte Programmiersprache ihre eigenen Bezeichnungen einführt. Ich werde mich nachfol-
gend auf die Begriffswelt von C++ beschränken, aber auch kurz andere Bezeichnungen vorstel-
len.
Wenn eine Klasse die Eigenschaften einer anderen Klasse übernimmt, dann spricht man von
Vererbung (englisch: inheritance). Eine neue Klasse erbt die Eigenschaften einer bereits vorhan-
denen Klasse.

261
Sandini Bib

262 Kapitel 5: Vererbung und Polymorphie

Die Klasse, von der geerbt wird, nennt man Basisklasse (andere Bezeichnungen: Oberklasse,
Elternklasse). Die Klasse, die erbt, nennt man abgeleitete Klasse (andere Bezeichnungen: Un-
terklasse, Kind-Klasse).
Da eine abgeleitete Klasse selbst eine Basisklasse sein kann, kann eine ganze Klassenhierar-
chie entstehen, wie das Beispiel in Abbildung 5.1 zeigt.

F a h r z e u g

A u t o B o o t L a s t e r

S p o r t w a g e n R u d e r b o o t M o t o r b o o t S e g e l s c h i f f

Abbildung 5.1: Beispiel für eine Klassenhierarchie

Die Klasse Fahrzeug beschreibt die Eigenschaften von Fahrzeugen. Die Klasse Auto erbt diese
Eigenschaften und erweitert sie. Die Klasse Sportwagen erbt die Eigenschaften von Auto (also
die erweiterten Eigenschaften von Fahrzeug) und erweitert sie weiter. Entsprechend können
beliebige andere Zweige angelegt werden.
Die Vererbung ist durch die so genannte is-a-Beziehung gekennzeichnet: Ein Sportwagen ist
ein Auto. Ein Auto ist ein Fahrzeug.
Wie man sieht, wird in der UML-Notation als Symbol für Vererbung ein gleichseitiges Drei-
eck mit der Spitze zur Basisklasse verwendet.

Mächtigkeit von Vererbung


Es gibt unterschiedliche Mächtigkeiten der Vererbung:
 einfache Vererbung (englisch: single inheritance)
 Mehrfachvererbung (englisch: multiple inheritance)
Bei der einfachen Vererbung kann eine abgeleitete Klasse nur eine Basisklasse besitzen, bei der
Mehrfachvererbung sind mehrere Basisklassen möglich. Daraus folgt, dass bei der einfachen
Vererbung nur baumartige Hierarchien auftreten können; bei der Mehrfachvererbung sind es ge-
richtete Grafen. C++ unterstützt die Mehrfachvererbung.
Die Mehrfachvererbung ist sehr nützlich, kann aber zu Komplikationen führen. Aus diesem
Grund wird nachfolgend zunächst auf die einfache Vererbung eingegangen. Besonderheiten der
Mehrfachvererbung werden in Abschnitt 5.4 vorgestellt.
Sandini Bib

5.1 Einfache Vererbung 263

5.1 Einfache Vererbung


Als Beispiel für die einfache Vererbung soll die im vorangegangenen Kapitel eingeführte Klasse
Bruch abgeleitet werden. Dabei wird auf der in Abschnitt 4.7 vorgestellten Version aufgebaut.
Die Klasse Bruch soll um eine Eigenschaft erweitert werden, die ein Bruch bisher nicht hatte:
Er soll gekürzt werden können. Man kann diese Eigenschaft natürlich auch direkt in der Klasse
Bruch implementieren, aber wir gehen davon aus, dass die Klasse Bruch nicht verändert werden
soll, weil es z.B. auch Sinn macht, Brüche zu verwalten, die grundsätzlich nicht gekürzt werden
können, oder weil die bisherige Version der Klasse Bruch Teil einer geschlossenen Klassenbi-
bliothek ist.
In diesem Abschnitt wird zunächst das Grundkonzept der Vererbung vorgestellt. Die hier be-
schriebene erste Version einer abgeleiteten Klasse kann aber bei ihrer Anwendung zu Problemen
führen. Auf diese Probleme und deren Vermeidung wird in Abschnitt 5.2 eingegangen.

5.1.1 Die Klasse Bruch als Basisklasse


Bevor die Basisklasse Bruch abgeleitet werden kann, muss an der bisherigen Implementierung
eine kleine Modifikation vorgenommen werden. Die bisherigen Versionen der Klasse Bruch
waren für die Vererbung nämlich nicht vorgesehen und würden die hier vorgesehene Ableitung
unmöglich machen.
Dazu sind zwei Anmerkungen erforderlich:
 Wenn die bisherigen Versionen der Klasse Bruch Teil einer geschlossenen Klassenbibliothek
wären, müssten sie die Modifikationen natürlich schon enthalten.
 Aufgrund der oben angesprochenen möglichen Anwendungsprobleme, auf die erst im näch-
sten Abschnitt eingegangen wird, müssen eigentlich noch weitere Modifikationen vorgenom-
men werden. Insofern ist diese Version noch nicht die endgültige für die Vererbung geeignete
Implementierung der Klasse Bruch.
Eine Version der Klasse Bruch, die auch als Basisklasse für andere Klassen dienen kann, braucht
folgende geänderte Klassendeklaration:
// vererb/bruch91.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {
protected:
int zaehler;
int nenner;
Sandini Bib

264 Kapitel 5: Vererbung und Polymorphie

public:
/* Fehlerklasse
*/
class NennerIstNull {
};

/* Default-Konstruktor, Konstruktor aus Zähler und


* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);

/* Multiplikation
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend Bruch operator * (const Bruch&, const Bruch&);

// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

/* Vergleich
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend bool operator < (const Bruch&, const Bruch&);

// Ein- und Ausgabe mit Streams


void printOn (std::ostream&) const;
void scanFrom (std::istream&);

// Typumwandlung nach double


double toDouble () const;
};

/ * Operator *
* - globale Friend-Funktion
* - inline definiert
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
/* Zähler und Nenner einfach multiplizieren
Sandini Bib

5.1 Einfache Vererbung 265

* - das Kürzen sparen wir uns


*/
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}

/* Standard-Ausgabeoperator
* - global überladen und inline definiert
*/
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

/* Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP

Das Zugriffsschlüsselwort protected


Der entscheidende Unterschied zur bisherigen Deklaration der Klasse Bruch ist, dass jetzt vor
den Komponenten zaehler und nenner statt private das Zugriffsschlüsselwort protected
steht:
namespace Bsp {
class Bruch {
protected:
int zaehler;
int nenner;
...
};
...
}
Sandini Bib

266 Kapitel 5: Vererbung und Polymorphie

Durch protected geschützte Komponenten sind wie bei private vor Zugriffen durch den An-
wender der Klasse geschützt. Abgeleitete Klassen haben jedoch darauf Zugriff. Bei privaten
Komponenten ist selbst das nicht möglich.
Da die Vererbung ein wesentliches Konzept von C++ ist, sollte der Klassen-Designer dies
im Allgemeinen durch die Verwendung von protected statt private als Zugriffsschutz un-
terstützen. Da man, um kürzen zu können, Zugriff auf diese Komponenten braucht, wäre ein
Ableiten in diesem Fall sonst nicht möglich. Man hätte nur neue Eigenschaften, für die kein
Zugriff auf die geerbten Komponenten notwendig ist, hinzufügen können.
In der Praxis sollte es relativ selten notwendig sein, Komponenten auch vor dem Zugriff durch
die Vererbung zu schützen. In C++ gibt es in der Praxis ohnehin keinen absoluten Schutz vor
unvorhergesehenem Zugriff, da öffentliche Zeiger auf private Komponenten zeigen oder auch
Headerdateien manipuliert werden können (C++ spezifiziert dazu nur, dass das Verhalten der
Sprache nicht definiert ist, wenn für eine Klasse verschiedene Headerdateien verwendet werden).
Insofern schützt C++ nicht vor unerlaubtem Zugriff durch böswillige Manipulation (dafür gibt
es Sprachen wie Ada).

5.1.2 Vorüberlegungen zur abgeleiteten Klasse KBruch


Nachdem die Klasse Bruch nun für eine Vererbung vorbereitet wurde, kann sie durch die Klas-
se KBruch abgeleitet werden (siehe Abbildung 5.2). Der Name KBruch“ steht für kürzbarer
” ”
Bruch“. Das bedeutet, dass die Objekte dieser Klasse Brüche sind, die prinzipiell gekürzt wer-
den können.

B r u c h

K B r u c h

Abbildung 5.2: Klasse KBruch als abgeleitete Klasse von Bruch

Neu eingeführte Operationen


Als neue Operationen kommen drei Funktionen hinzu:
 Die Funktion istKuerzbar() liefert zurück, ob der Bruch noch kürzbar ist.
 Die Funktion ggt() liefert den größten gemeinsamen Teiler von Zähler und Nenner, mit dem
der Bruch gekürzt werden könnte, zurück.
 Die Funktion kuerzen() kürzt den Bruch (sofern er nicht schon gekürzt ist).
Sandini Bib

5.1 Einfache Vererbung 267

Neu implementierte Operationen


Neben den neu eingeführten Operationen müssen für die Klasse KBruch aus verschiedenen
Gründen die Operationen neu definiert werden, die bereits für die Klasse Bruch implementiert
wurden:
 Die Konstruktoren müssen erneut definiert werden, da sie grundsätzlich nicht geerbt werden.
 Der Operator *= und die Einlesefunktion scanFrom() müssen erneut definiert werden, da
ihre Implementierung aus der Klasse Bruch nicht übernommen werden kann und überschrie-
ben werden muss (die Gründe dafür werden noch erläutert).

Neue Komponenten
Neben den Komponenten zaehler und nenner, die durch die Vererbung von der Basisklasse
Bruch übernommen werden, bietet es sich an, die neue Boolesche Komponente kuerzbar ein-
zuführen. Sie hält für ein Objekt jeweils fest, ob der Bruch noch kürzbar oder bereits gekürzt
ist. Man könnte dies auch jedes Mal, wenn diese Eigenschaft von Interesse ist, neu ausrechnen.
Dadurch dass die Eigenschaft nur einmal bei einer Wertänderung ausgerechnet wird, kann aber
Zeit gespart werden.
Insgesamt ergibt sich für Objekte der Klasse KBruch der in Abbildung 5.3 dargestellte Auf-
bau.

KBruch:
Datenelemente: Elementfunktionen:
geerbt zaehler operator * ()
von Bruch: operator < ()
nenner
printOn()
asDouble()
neu: kuerzbar KBruch()
istKuerzbar()
ggt()
operator *= ()
scanFrom()

Abbildung 5.3: Geerbte und neue Komponenten der Klasse KBruch

5.1.3 Deklaration der abgeleiteten Klasse KBruch


Wie üblich befindet sich die Klassendeklaration in einer eigenen Headerdatei. Sie hat insgesamt
folgenden Aufbau, der anschließend im Einzelnen erläutert wird:
Sandini Bib

268 Kapitel 5: Vererbung und Polymorphie

// vererb/kbruch1.hpp
#ifndef KBRUCH_HPP
#define KBRUCH_HPP

// Headerdatei der Basisklasse


#include "bruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse KBruch
* - abgeleitet von Bruch
* - der Zugriff auf geerbte Komponenten wird nicht
* eingeschränkt (public bleibt public)
*/
class KBruch : public Bruch {
protected:
bool kuerzbar; // true: Bruch ist kürzbar

// Hilfsfunktion: größter gemeinsamer Teiler von Zähler und Nenner


unsigned ggt() const;

public:
/* Default-Konstruktor, Konstruktor aus Zähler
* und Konstruktor aus Zähler und Nenner
* - Parameter werden an Bruch-Konstruktor durchgereicht
*/
KBruch (int z = 0, int n = 1) : Bruch(z,n) {
kuerzbar = (ggt() > 1);
}

// multiplikative Zuweisung (neu implementiert)


const KBruch& operator*= (const KBruch&);

// Eingabe mit Streams (neu implementiert)


void scanFrom (std::istream&);

// Bruch kürzen
void kuerzen();

// Kürzbarkeit testen
Sandini Bib

5.1 Einfache Vererbung 269

bool istKuerzbar() const {


return kuerzbar;
}
};

} // **** ENDE Namespace Bsp ********************************

#endif // KBRUCH_HPP

Zunächst muss die Headerdatei der Basisklasse eingebunden werden, da die Deklaration der
abgeleiteten Klasse darauf aufbaut:
#include "bruch.hpp"
Entscheidend für die Tatsache, dass es sich um eine abgeleitete Klasse handelt, ist dann die
eigentliche Klassendeklaration:
class KBruch : public Bruch {
...
};
Eine abgeleitete Klasse wird dadurch deklariert, dass ihre Basisklasse hinter einem Doppelpunkt
und einem optionalen Zugriffsschlüsselwort angegeben wird. Dabei spielt es keine Rolle, ob die
Basisklasse selbst eine abgeleitete Klasse ist.
Die Syntax zur Deklaration einer abgeleiteten Klasse lautet also:1
class abgeleiteteKlasse : [zugriff ] basisKlasse f
deklarationen
g;
Das optionale Zugriffsschlüsselwort gibt an, ob und inwiefern der Zugriff auf geerbte Kompo-
nenten weiter eingeschränkt wird:
 Bei public gibt es keine weiteren Einschränkungen: Öffentliche Komponenten der Basis-
klasse sind auch in der abgeleiteten Klasse öffentlich, protected bleibt protected, und
private bleibt private.
 Bei protected gibt es die Einschränkung, dass alle öffentlichen Komponenten der Basis-
klasse zu Komponenten werden, auf die der Anwender keinen Zugriff mehr hat;, davon ab-
geleitete Klassen haben aber weiterhin Zugriff (public wird also protected, protected
bleibt protected, und private bleibt private).
 Bei private werden alle Komponenten der Basisklasse zu privaten Komponenten. Weder
der Anwender noch davon wieder abgeleitete Klassen haben auf diese Komponenten Zugriff.
Der Default-Wert für den Zugriff ist private, sollte aber der besseren Lesbarkeit halber trotz-
dem immer explizit angegeben werden (manche Compiler geben sogar eine Warnung aus, wenn
bei der Basisklasse kein Zugriffsschlüsselwort angegeben ist).

1
Wie wir in Abschnitt 5.4.1 noch sehen werden, können auch mehrere Basisklassen angegeben werden.
Sandini Bib

270 Kapitel 5: Vererbung und Polymorphie

Die Klasse KBruch schränkt den Zugriff auf geerbte Komponenten der Klasse Bruch nicht wei-
ter ein. Das bedeutet, dass die geerbten öffentlichen Elementfunktionen wie printOn(), die
Multiplikation und der Vergleich mit dem Operator < auch bei der Anwendung eines KBruchs
aufgerufen werden können:
namespace Bsp {
class Bruch {
...
public:
void printOn (std::ostream&) const;
...
};

class KBruch : public Bruch {


...
};
}

void f()
{
Bsp::KBruch kb;
...
kb.printOn(std::cout); // OK: printOn() ist auch für KBruch public
}

5.1.4 Vererbung und Konstruktoren


Konstruktoren spielen bei der Vererbung eine gesonderte Rolle. Sie werden grundsätzlich nicht
geerbt. Alle Konstruktoren für Objekte einer abgeleiteten Klasse müssen neu definiert werden.
Werden keine Konstruktoren definiert, existiert auch hier nur der Default-Konstruktor.
Obwohl Konstruktoren von Basisklassen nicht geerbt werden, spielen sie bei der Initiali-
sierung eines Objekts doch eine Rolle. Konstruktoren werden nämlich verkettet top-down auf-
gerufen. Beim Anlegen eines Objekts einer abgeleiteten Klasse wird zunächst der Konstruktor
der Basisklasse aufgerufen, der sozusagen erst den geerbten Objektteil der Basisklasse initiali-
siert. Erst danach wird der Konstruktor der abgeleiteten Klasse aufgerufen, der zumindest die
neu hinzugekommenen Komponenten initialisiert. Er kann aber auch dazu verwendet werden,
Initialisierungen des Konstruktors der Basisklasse zu korrigieren, die aus Sicht der abgeleiteten
Klasse nicht mehr sinnvoll sind. In unserem Beispiel wird beim Anlegen eines Objekts der Klas-
se KBruch also zunächst ein Bruch-Konstruktor und anschließend erst ein KBruch-Konstruktor
aufgerufen.
Verwirrend ist dabei vielleicht die Tatsache, dass die Argumente zur Konstruktion des Objekts
nicht automatisch an den Konstruktor der Basisklasse übergeben werden. Sofern nicht anders
angegeben, wird vielmehr der Default-Konstruktor der Basisklasse aufgerufen. Dies liegt daran,
Sandini Bib

5.1 Einfache Vererbung 271

dass Parameter für den Konstruktor der abgeleiteten Klasse eine ganz andere Bedeutung haben
können, als sie bei der Basisklasse besitzen. Es kann sich auch um eine andere Anzahl oder
andere Typen handeln.
Auch hier können wieder Initialisierungslisten verwendet werden. Sie bieten die Möglich-
keit, Argumente an den indirekt aufgerufenen Konstruktor der Basisklasse zu übergeben bzw.
durchzureichen.
Dies zeigt die Deklaration des ersten Konstruktors der Klasse KBruch:
class KBruch : public Bruch {
public:
KBruch (int z = 0, int n = 1) : Bruch(z,n) {
kuerzbar = (ggt() > 1);
}
...
};
Der Konstruktor wird mit den gleichen Default-Argumenten deklariert, wie bei der Klasse Bruch.
Es können also ein Zähler und ein Nenner als Integer übergeben werden. Erfolgt dies nicht, wer-
den auch hier als Default-Werte 0 und 1 angenommen. Diese Parameter werden dann aber an den
Konstruktor der Basisklasse Bruch durchgereicht, wo sie zur Initialisierung von zaehler und
nenner verwendet werden.
Erst nach dem Aufruf des Bruch-Konstruktors werden die Anweisungen im KBruch-Kon-
struktor ausgeführt. In diesem Fall wird in kuerzbar festgehalten, ob der KBruch kürzbar ist.

Beispiel für die Initialisierung eines Objekts der Klasse KBruch


Der genaue Ablauf der Initialisierung soll nachfolgend an einem Beispiel erläutert werden.
Durch die Deklaration
Bsp::KBruch x(91,39);
wird das Objekt x der Klasse KBruch angelegt und mit 91
39 initialisiert.
Dies geschieht in folgenden Schritten:
 Zunächst wird der Speicherplatz für das Objekt angelegt, dessen Zustand wie immer nicht
definiert ist:

x: zaehler: ?
nenner: ?
kuerzbar: ?

 Anhand der Initialisierungsliste des Konstruktors wird festgestellt, dass der int/int-Kon-
struktor der Klasse Bruch aufgerufen werden muss:
Sandini Bib

272 Kapitel 5: Vererbung und Polymorphie

class KBruch : public Bruch {


...
KBruch (int z = 0, int n = 1) : Bruch(z,n) {
kuerzbar = (ggt() > 1);
}
...
};

 Also wird der int/int-Konstruktor der Klasse Bruch mit den Parametern 91 und 39 aufge-
rufen, der den von Bruch geerbten Teil des Objekts initialisiert:

x: zaehler: 91
nenner: 39

kuerzbar: ?

 Schließlich werden die Anweisungen des KBruch-Konstruktors ausgeführt, die die Kompo-
nente kuerzbar durch Aufruf von ggt() initialisieren:

x: zaehler: 91
nenner: 39

kuerzbar: true

Die Tatsache, dass die Anweisungen im Basis-Konstruktor vor den Anweisungen im Konstruktor
der abgeleiteten Klasse aufgerufen werden, ist wichtig. Nur so kann die abgeleitete Klasse, wie
in diesem Beispiel, die initialisierten Komponenten der Basisklasse auswerten. Damit besteht
auch die Möglichkeit, die Initialisierung zu korrigieren, indem z.B. Sonderfälle anders behandelt
werden. Dies darf aber nicht dazu führen, dass die Komponenten eine neue Bedeutung erhalten
(in Abschnitt 5.5 werden einige Design-Fallen dazu vorgestellt).
Der ganze Mechanismus ist rekursiv. Ist die Basisklasse selbst eine abgeleitete Klasse, wird
also zunächst der Konstruktor ihrer Basisklasse aufgerufen. Die Initialisierungsliste der Basis-
klasse gibt dabei an, welche Parameter an den Konstruktor der Basisklasse durchgereicht werden.
Eine Initialisierungsliste kann Argumente jeweils nur an ihre direkte Basisklasse übergeben.
Wenn Konstruktoren sowohl für Basisklassen als auch für Komponenten aufgerufen werden
müssen, werden grundsätzlich zuerst die Konstruktoren der Basisklassen und dann die Konstruk-
toren für die Komponenten aufgerufen.
Sandini Bib

5.1 Einfache Vererbung 273

Vererbung und Destruktoren


Destruktoren werden umgekehrt verkettet, also bottom-up, aufgerufen. Zuerst werden die An-
weisungen der abgeleiteten Klasse und dann die der Basisklasse durchgeführt.
Wenn Destruktoren für Basisklassen und Destruktoren für Komponenten aufgerufen werden
müssen, werden zuerst die Destruktoren für die Komponenten und danach die Destruktoren der
Basisklasse aufgerufen.

5.1.5 Implementierung von abgeleiteten Klassen


Die Implementierung einer abgeleiteten Klasse sieht aus wie bei jeder anderen Klasse auch. Die
Quelldatei der Klasse KBruch hat insgesamt folgenden Aufbau:
// vererb/kbruch1.cpp
// Headerdatei für min() und abs()
#include <algorithm>
#include <cstdlib>

// Headerdatei der eigenen Klasse einbinden


#include "kbruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* ggt()
* - größter gemeinsamer Teiler von Zähler und Nenner
*/
unsigned KBruch::ggt () const
{
if (zaehler == 0) {
return nenner;
}

/* größte Zahl ermitteln, die sowohl den Zähler als auch


* den Nenner ohne Rest teilt
*/
unsigned teiler = std::min(std::abs(zaehler),nenner);
while (zaehler % teiler != 0 || nenner % teiler != 0) {
--teiler;
}
return teiler;
}
Sandini Bib

274 Kapitel 5: Vererbung und Polymorphie

/* kuerzen()
*/
void KBruch::kuerzen ()
{
// falls kürzbar, Zähler und Nenner durch GGT teilen
if (kuerzbar) {
int teiler = ggt();

zaehler /= teiler;
nenner /= teiler;

kuerzbar = false; // damit nicht mehr kürzbar


}
}

/* Operator *=
* - neu implementiert
*/
const KBruch& KBruch::operator*= (const KBruch& b)
{
// wie bei der Basisklasse:
zaehler *= b.zaehler;
nenner *= b.nenner;

// weiterhin gekürzt?
if (!kuerzbar) {
kuerzbar = (ggt() > 1);
}

return *this;
}

/* scanFrom()
*/
void KBruch::scanFrom (std::istream& strm)
{
Bruch::scanFrom (strm); // scanFrom() der Basisklasse aufrufen

kuerzbar = (ggt() > 1); // Kürzbarkeit testen


}

} // **** ENDE Namespace Bsp ********************************


Sandini Bib

5.1 Einfache Vererbung 275

Zunächst wird die neu eingeführte Elementfunktion zur Berechnung des größten gemeinsa-
men Teilers implementiert. Falls der Zähler 0 ist, ist der GGT der Nenner ( 70 hat also den
GGT 7). Ansonsten wird in einer Schleife, absteigend vom vorzeichenfreien Minimum von
Zähler und Nenner, die erste Zahl gesucht, die sowohl den Zähler als auch den Nenner oh-
ne Rest teilt (der Modulo-Operator % liefert den Rest nach Division). Spätestens bei 1 ist das
der Fall. Für den Startwert der Schleife wird die in <algorithm> definierte Standardfunktion
std::min(), die das Minimums zweier Werte liefert, und die in <cstdlib> definierte Stan-
dardfunktion std::abs(), die den Absolutwert (Wert ohne Vorzeichen) eines Integers liefert,
verwendet.2
Die Funktion kuerzen() teilt, sofern der Bruch nicht schon gekürzt ist, Zähler und Nenner
durch den berechneten GGT.

Neuimplementierung geerbter Funktionen


Als Besonderheit kann es wie hier passieren, dass abgeleitete Funktionen und Operatoren er-
neut implementiert werden müssen, da ihre Implementierung in der Basisklasse für Objekte der
abgeleiteten Klasse nicht korrekt ist. Dies gilt in diesem Beispiel für den Operator *= und die
Einlesefunktion scanFrom().
In beiden Fällen kann es nämlich passieren, dass der damit manipulierte KBruch ohne Neuim-
plementierung in einen inkonsistenten Zustand gerät. Dies liegt daran, dass beide Funktionen
zaehler und nenner manipulieren. Deren Inhalt bestimmt aber den Wert von kuerzbar. Da
die Implementierung der Basisklasse kuerzbar jedoch nicht anpasst (kuerzbar ist dort ja noch
nicht bekannt), kann es passieren, dass die Komponente keinen korrekten Wert besitzt.
Beim Operator *= kann der KBruch, für den diese Operation aufgerufen wird, von einem
nichtkürzbaren in einen kürzbaren Zustand wechseln. Dies ist zum Beispiel dann der Fall, wenn
das nichtkürzbare Objekt 73 mit 37 multipliziert wird. Ohne Neuimplementierung würde kuerzbar
auch für den Wert des KBruchs 21 21 weiterhin auf false stehen.
Bei scanFrom() wird der Wert des KBruchs ohne Neuimplementierung ebenfalls ohne An-
passen der Komponente kuerzbar beliebig geändert. Damit kann ebenfalls die Inkonsistenz
entstehen, dass die Komponente kuerzbar nicht korrekt gesetzt ist. Die Information, ob der
KBruch kürzbar ist, muss deshalb jedes Mal, wenn ein neuer Wert eingelesen wird, neu ermittelt
werden.
Bei der Neuimplementierung gibt es zwei Möglichkeiten:
 komplette Neuimplementierung
 Aufruf der Implementierung der Basisklasse mit anschließender Korrektur
Auch hier muss wieder zwischen einem möglichen Laufzeitvorteil und einem damit verbundenen
Konsistenznachteil abgewogen werden. Die komplette Neuimplementierung, wie sie beim Ope-
rator *= erfolgt, spart Zeit, muss aber bei Änderungen in der Basisklasse auch geändert werden.
Bei einem Aufruf der Implementierung der Basisklasse, wie sie bei scanFrom() durchgeführt
wird, müssen nur die für die abgeleitete Klasse zusätzlich notwendigen Schritte neu implemen-
tiert werden.

2
Es gibt zweifellos bessere Algorithmen zur Berechnung eines GGTs.
Sandini Bib

276 Kapitel 5: Vererbung und Polymorphie

5.1.6 Anwendung von abgeleiteten Klassen


Abgeleitete Klassen werden wie jede andere Klasse angewendet. Objekte werden durch Kon-
struktoren erzeugt und über geerbte oder neue bzw. korrigierte Operationen manipuliert.
Das folgende Beispielprogramm soll dies verdeutlichen:
// vererb/kbruchtest1.cpp
// Headerdateien
#include <iostream>
#include "kbruch.hpp"

int main()
{
// kürzbaren Bruch deklarieren
Bsp::KBruch x(91,39);

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;

// x kürzen
x.kuerzen();

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;

// x mit 3 multiplizieren
x *= 3;

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;
}
Es liefert folgende Ausgabe:
91/39 (kuerzbar)
7/3 (unkuerzbar)
21/3 (kuerzbar)
Sandini Bib

5.1 Einfache Vererbung 277

Zunächst wird durch die Deklaration


Bsp::KBruch x(91,39);
das Objekt x der Klasse KBruch angelegt und mit 91
39 initialisiert. Dies geschieht auf die schon
auf Seite 271 erläuterte Art und Weise.
Anschließend wird x ausgegeben:
std::cout << x;
Nach der Ausgabe von x wird jeweils ausgegeben, ob x kürzbar ist. Der Ausdruck
x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)"
klärt dabei, ob (kuerzbar)“ oder (unkuerzbar)“ ausgegeben wird. Da der Operator ?: (er
” ”
wird auf Seite 44 erläutert) eine geringere Priorität als der Ausgabeoperator << besitzt, muss der
Ausdruck geklammert werden.
Nach dem Aufruf der Operation, die den KBruch kürzt, wird der Zustand des KBruchs erneut
ausgegeben. Schließlich wird der KBruch mit 3 multipliziert und erneut ausgegeben. Die Multi-
plikation mit einer ganzen Zahl ermöglicht der KBruch-Konstruktor. Da er auch mit nur einem
Integer-Argument aufgerufen werden kann, definiert er eine automatische Typumwandlung von
Integer nach KBruch (siehe Abschnitt 4.6.1), wodurch die 3 in den KBruch 31 umgewandelt und
dem Operator *= als Parameter übergeben wird.
Da der Ausgabeoperator für die Klasse KBruch nicht neu implementiert wurde, wird auto-
matisch die Operation verwendet, die in der Klasse Bruch deklariert wurde:
std::ostream& operator << (std::ostream& strm, const Bruch& b);

Anwendung der is-a-Beziehung


Beim Ausgeben eines KBruchs mit
std::cout << x;
ist eine Tatsache bemerkenswert: Obwohl die Operatorfunktion nur für die Klasse Bruch als
zweiten Operanden definiert ist, kann auch ein KBruch übergeben werden. Man kann also ein
Objekt der Klasse KBruch als Bruch verwenden.
Dabei handelt es sich um die konsequente Anwendung der is-a-Beziehung. Da die Klasse
KBruch von Bruch abgeleitet ist, ist ein KBruch ein Bruch und kann somit jederzeit als solcher
verwendet werden. Durch Vererbung wird grundsätzlich eine automatische Typumwandlung von
einem Objekt einer abgeleiteten Klasse in ein Objekt seiner Basisklasse definiert. Dabei wird das
Objekt aber auf die Eigenschaften reduziert, die die Objekte der Basisklasse besitzen. Insbeson-
dere besitzt es nur noch die Datenelemente der Basisklasse. Die bei der Vererbung hinzugekom-
menen Komponenten fallen einfach weg bzw. werden ignoriert.
In diesem Fall ist das kein Problem, denn auch bei der Ausgabe des KBruchs sollen nur Zähler
und Nenner ausgegeben werden. Es gibt aber Fälle, in denen das problematisch sein kann. Im
Abschnitt 5.5 werden entsprechende Fallen erläutert.
Sandini Bib

278 Kapitel 5: Vererbung und Polymorphie

5.1.7 Konstruktoren für Objekte der Basisklasse


Die Multiplikation wurde für die Klasse KBruch zwar von Bruch geerbt, sie kann aber nicht rich-
tig eingesetzt werden. Dies liegt daran, dass zwei Objekte der Klasse KBruch zwar miteinander
multipliziert werden können, das Ergebnis aber keinem KBruch zugewiesen werden darf:
Bsp::KBruch kb;
...
std::cout << kb * kb; // OK: gibt Quadrat von kb aus
...
kb = kb * kb; // FEHLER: Zuweisung an KBruch nicht erlaubt
Die von der Klasse Bruch geerbte Multiplikation liefert nämlich einen Bruch und keinen KBruch
zurück. Ein Bruch ist aber kein KBruch (die is-a-Beziehung gilt nur umgekehrt). Somit kann ein
Bruch auch nicht einfach einem KBruch zugewiesen werden.
Dieses Problem könnte durch Neuimplementierung der Multiplikation behoben werden. Das
Problem lässt sich aber auch durch die Definition einer Typumwandlung von einem Bruch in
einen KBruch lösen, die über die Implementierung eines weiteren Konstruktors ermöglicht wird:
class KBruch : public Bruch {
public:
/* Konstruktor zur Typumwandlung eines Bruchs in einen KBruch
* - Parameter wird an den Copy-Konstruktor von Bruch durchgereicht
*/
KBruch (const Bruch& b) : Bruch(b) {
kuerzbar = (ggt() > 1);
}
...
};
Auch hier wird der KBruch dadurch initialisiert, dass zunächst ein Konstruktor der Klasse Bruch
und dann der Konstruktor der Klasse KBruch aufgerufen wird. Da dem Bruch-Konstruktor der
Bruch, aus dem ein KBruch erzeugt werden soll, übergeben wird, wird der Copy-Konstruktor der
Klasse Bruch aufgerufen.
Da der Konstruktor nur einen Parameter besitzt, wird dadurch eine automatische Typum-
wandlung von Bruch nach KBruch definiert. Damit kann man einen Bruch als Ergebnis einer
Multiplikation auch einem KBruch zuweisen:
Bsp::KBruch kb;
...
kb = kb * kb; // OK: Typumwandlung von Bruch nach KBruch definiert
Genauso kann ein Bruch als Parameter bei der multiplikativen Zuweisung verwendet werden:
Bsp::KBruch kb;
Bsp::Bruch b;
...
kb *= b; // OK: Typumwandlung für b nach KBruch definiert
Sandini Bib

5.1 Einfache Vererbung 279

Ansonsten müsste der Parameter des Operators *= als Bruch deklariert werden:
class KBruch : public Bruch {
...
const KBruch& KBruch::operator*= (const Bruch&);
...
};

Einschränkungen bei Konstruktoren für Objekte der Basisklasse


Es ist eher selten sinnvoll, einen Konstruktor zu haben, der ein Objekt der Basisklasse in ein
Objekt der abgeleiteten Klasse umwandeln kann. Er sollte nur dann vorhanden sein, wenn es für
die neu hinzugekommenen Attribute immer eine sinnvolle Möglichkeit gibt, diese Attribute zu
initialisieren. In diesem Beispiel ist das ausnahmsweise der Fall, da sich die Tatsache, ob der
Bruch kürzbar ist, aus den bereits in Brüchen vorhandenen Komponenten (Zähler und Nenner)
ergibt.
Typischerweise kommen aber Attribute hinzu, deren Werte unabhängig von den bisherigen
Attributen ist. Hätten wir z.B. eine Klasse FarbigerBruch eingeführt, also einen Bruch, dem
als zusätzliches Attribut eine bestimmte Farbe für die Ausgabe zugeordnet ist, so wäre eine Typ-
umwandlung von Bruch weniger sinnvoll. Die Farbe eines Bruchs ist dann eine Eigenschaft, die
in keiner Beziehung zu den bisherigen Eigenschaften steht. Man könnte zwar eine Default-Farbe
annehmen, sinnvoller ist es aber meistens, keine Umwandlung eines Bruchs in einen farbigen
Bruch zuzulassen, damit in einem Programm, das farbige Brüche verwendet, nicht aus Versehen
Brüche ohne Farbe als farbige Brüche verwendet werden.
Man mache sich auch hier klar, dass jede automatische Typumwandlung die Gefahr eines un-
gewollten Verhaltens erhöht, da der Compiler dadurch nicht mehr erkennen kann, ob ein Objekt
fälschlicherweise als Objekt einer anderen Klasse verwendet wird. Auch hier kann eine Funktion
zur expliziten Typumwandlung sinnvoll sein. Es ist letztlich eine Design-Entscheidung, bei der
zwischen der Gefahr einer ungewollten Typumwandlung und dem Vorteil einer vereinfachten
Typumwandlung abgewogen werden muss.

5.1.8 Zusammenfassung
 Klassen können in einer Vererbungsbeziehung stehen.
 Eine abgeleitete Klasse übernimmt (erbt) alle Eigenschaften der Basisklasse und ergänzt die-
se typischerweise um neue Eigenschaften.
 Objekte abgeleiteter Klassen besitzen die Komponenten der Basisklasse und die neu hinzu-
gekommenen Komponenten.
 Kennzeichen der Vererbung ist die is-a-Beziehung: Ein Objekt einer abgeleiteten Klasse ist
ein Objekt der Basisklasse (mit zusätzlichen Eigenschaften).
 Ein Objekt einer abgeleiteten Klasse darf jederzeit als Objekt der Basisklasse verwendet wer-
den. Es reduziert sich dann auf die Eigenschaften der Basisklasse.
 Konstruktoren werden nicht geerbt, aber verkettet top-down aufgerufen. Destruktoren werden
entsprechend verkettet bottom-up aufgerufen.
 Mit Initialisierungslisten können den Konstruktoren einer Basisklasse Parameter übergeben
werden. Erfolgt dies nicht, wird jeweils der Default-Konstruktor der Basisklasse aufgerufen.
Sandini Bib

280 Kapitel 5: Vererbung und Polymorphie

5.2 Virtuelle Funktionen


Die im vorigen Abschnitt eingeführte Klasse KBruch ( kürzbarer Bruch“) besitzt noch einige

potenzielle Fehlerquellen: Ihre Verwendung kann zu Inkonsistenzen bei Objekten der Klasse
führen. Dies liegt im Wesentlichen daran, dass bei der Implementierung der Klasse Funktionen
der Basisklasse überschrieben wurden (was allerdings auch schon dazu diente, mögliche Inkon-
sistenzen zu vermeiden). Für Objekte der abgeleiteten Klasse können nämlich unter bestimmten
Umständen trotzdem die überschriebenen Funktionen der Basisklasse aufgerufen werden.
Welche Probleme beim Überschreiben von Funktionen einer Basisklasse auftreten können
und wie diese Probleme behoben werden, wird in diesem Abschnitt aufgezeigt. Dazu wird ein für
die Vererbung und die Polymorphie wesentliches Sprachmittel vorgestellt: virtuelle Funktionen.

5.2.1 Probleme beim Überschreiben von Funktionen der Basisklasse


Die im vorigen Abschnitt vorgestellte Klasse KBruch überschreibt zwei Funktionen der Basis-
klasse Bruch, da deren Implementierung nicht für KBrüche geeignet ist (siehe Abschnitt 5.1.5).
Dabei handelt es sich um die Funktionen operator*=() und scanFrom(). In der Basisklasse
Bruch lautete deren Deklaration:
class Bruch {
...
public:
// multiplikative Zuweisung
const Bruch& operator *= (const Bruch&);

// Eingabe mit Streams


void scanFrom (std::istream&);
...
};
In der abgeleiteten Klasse KBruch lautete sie:
class KBruch : public Bruch {
...
public:
// multiplikative Zuweisung (neu implementiert)
const KBruch& operator*= (const KBruch&);

// Eingabe mit Streams (neu implementiert)


void scanFrom (std::istream&);
...
};
Diese Neuimplementierung kann allerdings Probleme verursachen, die zumindest erkannt und
möglichst behoben werden sollten.
Sandini Bib

5.2 Virtuelle Funktionen 281

Das folgende Anwendungsprogramm deckt diese Probleme auf:


// vererb/kbruchtest2.cpp
// Headerdateien
#include <iostream>
#include "kbruch.hpp"
#include "bruch.hpp"

int main()
{
// KBruch deklarieren
Bsp::KBruch x(7,3);

// Bruch mit Kehrwert von x deklarieren


Bsp::Bruch b(3,7);

// Zeiger auf Bruch zeigt darauf


Bsp::Bruch* xp = &x;

*xp *= b; // PROBLEM: ruft Bruch::operator*=() auf

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;

std::cout << "Bruch eingeben (zaehler nenner): ";

std::cin >> x; // PROBLEM: ruft indirekt Bruch::scanFrom() auf

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;
}
Zwei Anweisungen des Programms sind problematisch. In beiden Fällen steckt der Mechanismus
dahinter, dass Objekte einer abgeleiteten Klasse auch als Objekte der Basisklasse verwendet
werden können.

Manipulation über Zeiger der Basisklasse


Im ersten Fall beruht das Problem darauf, dass die Manipulation von KBruch x über einen Zeiger
durchgeführt wird, der allerdings als Zeiger auf Objekte der Basisklasse deklariert ist:
Sandini Bib

282 Kapitel 5: Vererbung und Polymorphie

Bsp::KBruch x(7,3);
Bsp::Bruch b(3,7);

Bsp::Bruch* xp = &x;

*xp *= b; // PROBLEM: ruft Bruch::operator*=() auf


Die Tatsache, dass ein Zeiger vom Typ Bruch* auf einen KBruch zeigt, ist völlig in Ordnung.
Es handelt sich dabei um die konsequente Anwendung der is-a-Beziehung, die die Vererbung
kennzeichnet:
 Ein KBruch ist ein Bruch.
 Ein KBruch kann deshalb jederzeit als Bruch verwendet werden.
In diesem Fall wird der KBruch x als Bruch verwendet, auf den xp zeigt.
Wenn nun aber der KBruch x über den Bruch-Zeiger xp manipuliert wird, wird die Element-
funktion der Klasse Bruch und nicht die der Klasse KBruch aufgerufen. Dies liegt daran, dass
der Compiler beim Übersetzen die Operation *= für Brüche aufruft, da es sich um einen Zeiger
auf einen Bruch handelt. Diese Implementierung kennt aber nur die Komponenten zaehler und
nenner und passt kuerzbar nicht an.
Im vorliegenden Programm entsteht deshalb eine Inkonsistenz. Der Bruch 73 wird zwar mit 37
multipliziert, die Komponente kuerzbar behält aber ihren alten Wert, auch wenn dies falsch ist.
Dies wird mit der folgenden Ausgabeanweisung auch deutlich.
Ausgegeben wird:
21/21 (unkuerzbar)

Manipulation über Referenzen der Basisklasse


Ein entsprechendes, wenn auch nicht so leicht erkennbares Problem ist das Einlesen des KBruchs
x:
Bsp::KBruch x(7,3);
...
std::cin >> x; // PROBLEM: ruft indirekt Bruch::scanFrom() auf
Auch hier wird fälschlicherweise die Einlesefunktion der Basisklasse verwendet. In diesem Fall
liegt das daran, dass der KBruch x von einer Referenz der Klasse Bruch verwendet wird. Auf-
gerufen wird nämlich die bereits in der Basisklasse Bruch definierte Einleseoperation:
inline std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom (strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}
Auch hier handelt es sich wieder um die konsequente Anwendung der is-a-Beziehung. Der
KBruch x ist ein Bruch und kann somit zur Initialisierung der Bruch-Referenz b verwendet wer-
den.
Sandini Bib

5.2 Virtuelle Funktionen 283

Da b aber ein zweiter Name für einen Bruch ist, wird zum Einlesen von b die Elementfunktion
scanFrom() der Klasse Bruch aufgerufen. Diese liest zwar zaehler und nenner ein, setzt aber
nicht die Komponente kuerzbar, da diese für Brüche nicht bekannt ist. Der KBruch behält in der
Komponente kuerzbar deshalb unabhängig vom eingelesenen neuen Wert den alten Zustand,
auch wenn dies falsch ist.
Die neu implementierte Funktion zum Einlesen von Objekten der Klasse KBruch wird also
zumindest über den Einlese-Operator >> gar nicht erst aufgerufen. Nur ein direkter Aufruf von
scanFrom() würde funktionieren.

5.2.2 Statisches und dynamisches Binden von Funktionen


Statisches Binden von Funktionen
Hinter beiden Beispielen steckt das gleiche Grundproblem:
 Ein Objekt einer abgeleiteten Klasse darf aufgrund der is-a-Beziehung jederzeit als Objekt
der Basisklasse verwendet werden.
 Dies gilt auch dann, wenn es über Zeiger oder Referenzen auf Objekte der Basisklasse mani-
puliert wird.
 Da der Compiler davon ausgeht, dass es sich um Objekte der Basisklasse handelt, wird die
unter Umständen fehlerhafte Elementfunktion der Basisklasse aufgerufen, auch wenn sie für
Objekte der abgeleiteten Klasse neu implementiert wurde.
Der Compiler sorgt dafür, dass die Elementfunktion der Basisklasse aufgerufen wird, da er Funk-
tionsaufrufe normalerweise statisch bindet. Das bedeutet, dass er feststellt, von welchem Typ ein
Objekt ist, für das eine Funktion aufgerufen wird, und dass er Code generiert, der die Funktion
aufruft, die zu diesem Typ gehört. Dies gilt im Allgemeinen auch für Zeiger und Referenzen.
Dieses statische Binden führt dazu, dass auch dann, wenn die Zeiger auf Objekte einer abge-
leiteten Klasse zeigen und deshalb eine andere Elementfunktion aufgerufen werden müsste, die
falsche Elementfunktion der Basisklasse aufgerufen wird. Entsprechendes gilt für Referenzen.
Die Information, dass eigentlich Objekte einer abgeleiteten Klasse manipuliert werden, geht also
bei Funktionsaufrufen durch das statische Binden verloren.

Dynamisches Binden durch virtuelle Funktionen


Damit die Information, dass Objekte einer abgeleiteten Klasse manipuliert werden, bei Funk-
tionsaufrufen nicht verlorengeht, darf nicht statisch gebunden werden. Stattdessen muss zur
Laufzeit, in Abhängigkeit vom tatsächlichen Typ eines Objekts, die richtige Funktion aufge-
rufen werden. Dieser Sachverhalt wird als dynamisches Binden (englisch: dynamic binding) oder
auch spätes Binden (late binding) bezeichnet.3
Dynamisches Binden ist auch in C++ möglich. Dazu muss das Schlüsselwort virtual ver-
wendet werden. Wenn eine Elementfunktion als virtuelle Funktion deklariert wird, wird bei
der Verwendung von Zeigern und Referenzen erst zur Laufzeit entschieden, welche Funktion

3
Der Begriff dynamisches Binden wird auch für den Mechanismus von Shared Libraries verwendet, hat
damit aber nichts zu tun.
Sandini Bib

284 Kapitel 5: Vererbung und Polymorphie

tatsächlich aufgerufen wird. In Abhängigkeit von der Klasse, die das Objekt besitzt, wird die
dazugehörige Funktion aufgerufen.
Aus diesem Grund müssen zumindest die Funktionen, die in der abgeleiteten Klasse KBruch
neu implementiert werden, in der Basisklasse Bruch als virtuelle Funktionen deklariert werden.
Sinnvollerweise werden aber alle Funktionen der Basisklasse, die von abgeleiteten Klassen über-
schrieben werden können, als virtuelle Funktionen deklariert. Nur so ist eine Klasse generell zur
Vererbung geeignet.
Werden die Funktionen der Basisklasse nicht als virtuelle Funktionen deklariert, können die
vorher aufgezeigten Probleme auftreten. Aus diesem Grund sollte man sich bei der Vererbung an
folgende Regeln halten:
 Nichtvirtuelle Funktionen dürfen nicht überschrieben werden.
 Wenn dies zur Implementierung einer abgeleiteten Klasse notwendig ist, sollte nicht abgelei-
tet werden.

Die Deklaration der Basisklasse Bruch sollte deshalb besser wie folgt aussehen:
// vererb/bruch92.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

protected:
int zaehler;
int nenner;

public:
/* Fehlerklasse
*/
class NennerIstNull {
};

/* Default-Konstruktor, Konstruktor aus Zähler und


* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);
Sandini Bib

5.2 Virtuelle Funktionen 285

/* Multiplikation
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend Bruch operator * (const Bruch&, const Bruch&);

/* multiplikative Zuweisung
* - neu: virtuell
*/
virtual const Bruch& operator *= (const Bruch&);

/* Vergleich
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend bool operator < (const Bruch&, const Bruch&);

/* Ein- und Ausgabe mit Streams


* - neu: virtuell
*/
virtual void printOn (std::ostream&) const;
virtual void scanFrom (std::istream&);

/* Typumwandlung nach double


* - neu: virtuell
*/
virtual double toDouble () const;
};

/* Operator *
* - globale Friend-Funktion
* - inline definiert
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}
Sandini Bib

286 Kapitel 5: Vererbung und Polymorphie

/* Standard-Ausgabeoperator
* - global überladen und inline definiert
*/
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

/* Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Alle Funktionen, die aus welchen Gründen auch immer in abgeleiteten Klassen neu implemen-
tiert werden könnten, werden als virtuell deklariert. Ausgenommen davon sind nur Konstruktoren
und Friend-Funktionen, die grundsätzlich nicht virtuell sein können.

Einlesen eines KBruchs mit virtuellen Funktionen


Beim Einlesen eines kürzbaren Bruchs funktioniert damit alles wie erwartet: Es wird nun tat-
sächlich die scanFrom()-Implementierung der abgeleiteten Klasse KBruch verwendet, da die
Information, dass eigentlich ein KBruch eingelesen wird, in der Funktion operator>>() erhal-
ten bleibt und somit die Funktion scanFrom() der Klasse KBruch aufgerufen wird:
namespace Bsp {
class Bruch {
...
public:
virtual void scanFrom (std::istream&);
...
};

inline
Sandini Bib

5.2 Virtuelle Funktionen 287

std::istream& operator >> (std::istream& strm, Bruch& b)


{
b.scanFrom (strm); // da virtual: wird richtiges scanFrom() aufgerufen
return strm; // Stream zur Verkettung zurückliefern
}

class KBruch : public Bruch {


...
public:
virtual void scanFrom (std::istream&);
...
};
}

int main()
{
Bsp::KBruch x (7,3);
...
std::cin >> x; // OK: ruft indirekt KBruch::scanFrom() auf
...
}
Wenn die für Brüche überladene Operatorfunktion >> für ihren Parameter b die Elementfunk-
tion scanFrom() aufruft, wird, da b eine Referenz ist und scanFrom() in der Klasse virtuell
deklariert wird, erst zur Laufzeit entschieden, welche Klasse b tatsächlich besitzt. Falls es sich
um ein Objekt einer abgeleiteten Klasse handelt, wird automatisch die Implementierung von
scanFrom() dieser Klasse aufgerufen, sofern sie eine eigene besitzt.
Die Funktion scanFrom() muss in der abgeleiteten Klasse dazu nicht unbedingt virtuell
deklariert werden. Da die Funktion in der Basisklasse virtuell ist, ist sie es in der abgeleiteten
Klasse automatisch auch. Mit der erneuten Deklaration als virtuell wird aber in der abgeleiteten
Klasse deutlich gemacht, dass es sich bei dieser Funktion um eine virtuelle Funktion handelt.

Laufzeitnachteile durch virtual


Man beachte, dass ein Funktionsaufruf durch die Deklaration als virtuelle Funktion zum Teil
deutlich länger dauern kann. Statt eines direkten Funktionsaufrufs muss für Zeiger und Referen-
zen Code generiert werden, der zur Laufzeit erst feststellt, welche Funktion aufzurufen ist.
Besonders krass ist der Unterschied bei Inline-Funktionen. Da bei Zeigern und Referenzen
erst zur Laufzeit entschieden wird, welche Funktion aufzurufen ist, kann der Funktionsaufruf
nicht schon beim Kompilieren durch die Anweisungen in der Funktion ersetzt werden. Der Lauf-
zeitvorteil von Inline-Funktionen entfällt damit für Zeiger und Referenzen.
Aus diesem Grund kann es in der Praxis sinnvoll sein, Klassen nur mit einigen oder ganz ohne
virtuelle Funktionen auszustatten. Die Klassen sind dann zwar nur noch schlecht zur Vererbung
geeignet, das Laufzeitverhalten ist dafür aber zum Teil deutlich besser. Letztendlich handelt es
sich hierbei wieder um eine Design-Entscheidung.
Sandini Bib

288 Kapitel 5: Vererbung und Polymorphie

5.2.3 Überladen kontra Überschreiben


In der Erwartung, dass mit virtuellen Funktionen alles funktioniert, mag man das Anwendungs-
programm von Seite 281 erneut starten und Erstaunliches feststellen: Das eine Problem ist zwar
gelöst, das andere aber nicht. Die multiplikative Zuweisung über den Zeiger xp funktioniert auch
mit virtuellen Funktionen nicht.
Dies liegt daran, dass die virtuelle Operatorfunktion der Basisklasse nicht überschrieben,
sondern überladen wurde. Die Parameter sind nämlich nicht gleich:
namespace Bsp {
class Bruch {
...
public:
virtual const Bruch& operator *= (const Bruch&);
...
};

class KBruch : public Bruch {


...
public:
virtual const KBruch& operator*= (const KBruch&);
...
};
}

int main()
{
Bsp::KBruch x(7,3);
Bsp::Bruch b(3,7);
Bsp::Bruch* xp = &x;
...
*xp *= b; // PROBLEM: ruft immer noch Bruch::operator*=() auf
...
}
In der Basisklasse besitzt der Parameter von Operator *= die Klasse Bruch, in der abgeleiteten
Klasse aber die Klasse KBruch. Damit ist die Implementierung der abgeleiteten Klasse kein
echter Ersatz für die Implementierung der Basisklasse, sondern nur eine Ergänzung.
Für Objekte der abgeleiteten Klasse wird zwar die neue Implementierung aufgerufen, für
Zeiger und Referenzen der Klasse Bruch existiert aber nach wie vor nur die Implementierung
der Basisklasse. Sie wurde in der abgeleiteten Klasse nicht überschrieben. Es wird zwar erst zur
Laufzeit entschieden, welche Funktion aufgerufen werden muss; da es aber keine neue Imple-
mentierung der in der Basisklasse definierten Operation gibt, wird auch für KBrüche die Imple-
mentierung der Basisklasse Bruch verwendet.
Sandini Bib

5.2 Virtuelle Funktionen 289

Beim Überschreiben von geerbten Funktionen ist deshalb auch folgende Regel zu beachten:
 Eine virtuelle Funktion der Basisklasse wird in einer abgeleiteten Klasse nur dann wirklich
überschrieben, wenn die Anzahl und die Typen der Parameter identisch sind.
Um das Problem zu umgehen, muss der Operator *= in der abgeleiteten Klasse mit den gleichen
Parametern und dem gleichen Rückgabetyp deklariert werden:
namespace Bsp {
class KBruch : public Bruch {
...
public:
virtual const Bruch& operator*= (const Bruch&);
...
};
}
Wie grundsätzlich beim Überladen von Funktionen ist auch hier eine Unterscheidung nur durch
den Rückgabetyp unzulässig.
Zu dieser Regel gibt es allerdings ein Ausnahme: Liefert eine virtuelle Funktion einer Basis-
klasse ein Objekt dieser Basisklasse zurück, darf bei einer Neuimplementierung in einer abgelei-
teten Klasse statt auch ein Objekt dieser abgeleiteten Klasse zurückgeliefert werden. Ein Zeiger
muss aber ein Zeiger und eine Referenz muss eine Referenz bleiben.
Damit ist auch die folgende Neuimplementierung möglich:
namespace Bsp {
class KBruch : public Bruch {
...
public:
virtual const KBruch& operator*= (const Bruch&);
...
};
}

5.2.4 Zugriff auf Parameter der Basisklasse


Mit der hier vorgestellten Lösung, dass der Parameter ein Objekt der Basisklasse ist, entsteht
allerdings sofort ein anderes Problem: Bei der Implementierung des Operators ist es nun nicht
mehr möglich, auf die nichtöffentlichen Komponenten des Parameters zuzugreifen:
const Bruch& KBruch::operator*= (const Bruch& b)
{
zaehler *= b.zaehler; // FEHLER: kein Zugriff auf b.zaehler
nenner *= b.nenner; // FEHLER: kein Zugriff auf b.nenner
...
}
Sandini Bib

290 Kapitel 5: Vererbung und Polymorphie

Der Parameter b ist nämlich ein Objekt einer anderen Klasse als der Klasse KBruch, für die hier
eine Elementfunktion implementiert wird. Dabei spielt es keine Rolle, ob die Klasse eine Basis-
klasse ist und welcher Zugriff auf Komponenten der Basisklasse besteht. Die Klasse KBruch ist
in Bezug auf Parameter nur ein Anwender der Klasse Bruch und hat nur auf deren öffentliche
Komponenten Zugriff.
Es gilt also noch eine Regel:
 Für als Parameter übergebene Objekte von Basisklassen besteht in abgeleiteten Klassen nur
Zugriff auf öffentliche Komponenten.
Aus diesem Grund kann der Zugriff auf die Komponenten des Parameters nur über dessen öffent-
liche Schnittstelle durchgeführt werden. Da keine Elementfunktionen zum Abfragen der Werte
von Zähler und Nenner existieren (kommerzielle Klassen hätten diese sicherlich), muss in die-
sem Fall die Implementierung der Basisklasse aufgerufen werden, die den KBruch zunächst mit
dem übergebenen Parameter ausmultipliziert. Anschließend können eventuell vorhandene Inkon-
sistenzen beseitigt werden:
const Bruch& KBruch::operator*= (const Bruch& b)
{
/* neu: Implementierung der Basisklasse aufrufen
* - auf nichtöffentliche Komponenten von b besteht kein Zugriff
*/
Bruch::operator*= (b);

// weiterhin gekürzt?
if (!kuerzbar) {
kuerzbar = (ggt() > 1);
}

return *this;
}

5.2.5 Virtuelle Destruktoren


Es gibt noch ein weiteres Problem, das bei der Vererbung auftreten kann. Es betrifft die explizite
Speicherverwaltung.
Wenn ein Objekt einer abgeleiteten Klasse mit new angelegt wird, ist klar, welchen Typ es
besitzt:
Bsp::KBruch* kbp = new Bsp::KBruch; // Objekt der Klasse KBruch anlegen
Bei der Freigabe des Objekts mit delete kann es aber vorkommen, dass diese Klarheit nicht
besteht. Da es als Objekt einer Basisklasse verwendet werden kann, kann es auch als Objekt
einer Basisklasse freigegeben werden:
Sandini Bib

5.2 Virtuelle Funktionen 291

Bsp::Bruch* bp;

bp = new Bsp::KBruch; // KBruch anlegen


...
delete bp; // KBruch über Bruch-Zeiger freigeben
Hier tritt das gleiche Problem auf, das auch sonst bei abgeleiteten Klassen auftreten kann: Der
Delete-Operator führt zum Aufruf eventuell vorhandener Destruktoren. Sofern aber kein dyna-
misches Binden stattfindet, geht die Information, dass eigentlich ein Objekt einer abgeleiteten
Klasse freigegeben wird, verloren, und es wird nur der Destruktor der Klasse Bruch aufgerufen.
Wenn also für KBrüche ein Destruktor implementiert wird, wird dieser dann nicht aufgerufen.
Wenn der Destruktor einer abgeleiteten Klasse z.B. für das Objekt angelegten Speicherplatz frei-
gibt, würde dieser nicht freigegeben.
Damit dieser Fall nicht eintritt, muss in der Basisklasse ein virtueller Destruktor deklariert
werden (der Default-Destruktor ist nicht virtuell). Dies gilt auch dann, wenn die Basisklasse
eigentlich keinen Destruktor braucht.
Eine weitere Regel zur Vererbung lautet also:
 Eine Klasse ist nur dann generell zur Vererbung geeignet, wenn ein virtueller Destruktor
deklariert wird.
In unserem Beispiel bedeutet das, dass die Basisklasse Bruch einen virtuellen Destruktor besit-
zen muss, der allerdings keine Anweisungen enthält:
namespace Bsp {
class Bruch {
...
public:
virtual ~Bruch () {
}
...
};
}

5.2.6 Vererbung richtig angewendet


Zusammenfassend wird nun noch einmal das Beispiel der Klassen Bruch und KBruch unter
Berücksichtigung aller möglichen Probleme und deren Lösung vorgestellt.

Deklaration der Basisklasse Bruch


Die Basisklasse Bruch muss die Bedingungen erfüllen, die sie für eine Vererbung geeignet ma-
chen:
 Alle Funktionen, die überschrieben werden könnten, müssen als virtuell deklariert werden.
 Es muss ein virtueller Destruktor definiert werden.
Es ergibt sich daher folgender Aufbau der Headerdatei für die Basisklasse Bruch:
Sandini Bib

292 Kapitel 5: Vererbung und Polymorphie

// vererb/bruch93.hpp
#ifndef BRUCH_HPP
#define BRUCH_HPP

// Standard-Headerdateien einbinden
#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Bruch {

protected:
int zaehler;
int nenner;

public:
/* Fehlerklasse
*/
class NennerIstNull {
};

/* Default-Konstruktor, Konstruktor aus Zähler und


* Konstruktor aus Zähler und Nenner
*/
Bruch (int = 0, int = 1);

/* Multiplikation
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
friend Bruch operator * (const Bruch&, const Bruch&);

/* multiplikative Zuweisung
* - neu: virtuell
*/
virtual const Bruch& operator *= (const Bruch&);

/* Vergleich
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
Sandini Bib

5.2 Virtuelle Funktionen 293

*/
friend bool operator < (const Bruch&, const Bruch&);

/* Ein- und Ausgabe mit Streams


* - neu: virtuell
*/
virtual void printOn (std::ostream&) const;
virtual void scanFrom (std::istream&);

/* Typumwandlung nach double


* - neu: virtuell
*/
virtual double toDouble () const;

// neu: virtueller Destruktor (ohne Anweisungen)


virtual ~Bruch () {
}
};

/* Operator *
* - globale Friend-Funktion
* - inline definiert
*/
inline Bruch operator * (const Bruch& a, const Bruch& b)
{
/* Zähler und Nenner einfach multiplizieren
* - das Kürzen sparen wir uns
*/
return Bruch (a.zaehler * b.zaehler, a.nenner * b.nenner);
}

/* Standard-Ausgabeoperator
* - global überladen und inline definiert
*/
inline
std::ostream& operator << (std::ostream& strm, const Bruch& b)
{
b.printOn(strm); // Elementfunktion zur Ausgabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}
Sandini Bib

294 Kapitel 5: Vererbung und Polymorphie

/* Standard-Eingabeoperator
* - global überladen und inline definiert
*/
inline
std::istream& operator >> (std::istream& strm, Bruch& b)
{
b.scanFrom(strm); // Elementfunktion zur Eingabe aufrufen
return strm; // Stream zur Verkettung zurückliefern
}

} // **** ENDE Namespace Bsp ********************************

#endif // BRUCH_HPP
Die dazugehörige Implementierung entspricht der Version auf Seite 244.

Deklaration der abgeleiteten Klasse KBruch


In der abgeleiteten Klasse muss Folgendes beachtet werden:
 Die abgeleitete Klasse darf nur Funktionen überschreiben, die in der Basisklasse als virtuell
deklariert wurden. Dabei müssen die Parameter und der Rückgabetyp übereinstimmen.
 Wenn die abgeleitete Klasse selbst wieder zur Vererbung geeignet sein soll, muss auch bei ihr
jede neu hinzugekommene und zum Überschreiben geeignete Funktion als virtuell deklariert
werden.
Es ergibt sich daher folgender Aufbau der Headerdatei für die abgeleitete Klasse KBruch:
// vererb/kbruch3.hpp
#ifndef KBRUCH_HPP
#define KBRUCH_HPP

// Headerdatei der Basisklasse


#include "bruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse KBruch
* - abgeleitet von Bruch
* - der Zugriff auf geerbte Komponenten wird nicht
* eingeschränkt (public bleibt public)
* - zur Weiter-Vererbung geeignet
*/
Sandini Bib

5.2 Virtuelle Funktionen 295

class KBruch : public Bruch {


protected:
bool kuerzbar; // true: Bruch ist kürzbar

// Hilfsfunktion: größter gemeinsamer Teiler von Zähler und Nenner


unsigned ggt() const;

public:
/* Default-Konstruktor, Konstruktor aus Zähler
* und Konstruktor aus Zähler und Nenner
* - Parameter werden an Bruch-Konstruktor durchgereicht
*/
KBruch (int z = 0, int n = 1) : Bruch(z,n) {
kuerzbar = (ggt() > 1);
}

// multiplikative Zuweisung (neu implementiert)


virtual const KBruch& operator*= (const Bruch&);

// Eingabe mit Streams (neu implementiert)


virtual void scanFrom (std::istream&);

// Bruch kürzen
virtual void kuerzen();

// Kürzbarkeit testen
virtual bool istKuerzbar() const {
return kuerzbar;
}
};

} // **** ENDE Namespace Bsp ********************************

#endif // KBRUCH_HPP

Implementierung der abgeleiteten Klasse


In der Quelldatei der abgeleiteten Klasse werden die neuen und überschriebenen Funktionen
implementiert. Dabei muss darauf geachtet werden, dass auch auf Objekte der Basisklasse, die
als Parameter übergeben werden, nur öffentlicher Zugriff besteht:
Sandini Bib

296 Kapitel 5: Vererbung und Polymorphie

// vererb/kbruch3.cpp
// Headerdatei für min() und abs()
#include <algorithm>
#include <cstdlib>

// Headerdatei der eigenen Klasse einbinden


#include "kbruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* ggt()
* - größter gemeinsamer Teiler von Zähler und Nenner
*/
unsigned KBruch::ggt () const
{
if (zaehler == 0) {
return nenner;
}

/* Größte Zahl ermitteln, die sowohl Zähler als auch


* Nenner ohne Rest teilt
*/
unsigned teiler = std::min(std::abs(zaehler),nenner);
while (zaehler % teiler != 0 || nenner % teiler != 0) {
teiler--;
}
return teiler;
}

/* kuerzen()
*/
void KBruch::kuerzen ()
{
// falls kürzbar, Zähler und Nenner durch GGT teilen
if (kuerzbar) {
int teiler = ggt();

zaehler /= teiler;
nenner /= teiler;
Sandini Bib

5.2 Virtuelle Funktionen 297

kuerzbar = false; // damit nicht mehr kürzbar


}
}

/* Operator *=
* - zum Überschreiben mit Typen der Basisklasse neu implementiert
*/
const KBruch& KBruch::operator*= (const Bruch& b)
{
/* Implementierung der Basisklasse aufrufen
* - auf nichtöffentliche Komponenten von b besteht kein Zugriff
*/
Bruch::operator*= (b);

// weiterhin gekürzt ?
if (!kuerzbar) {
kuerzbar = (ggt() > 1);
}

return *this;
}

/* scanFrom()
*/
void KBruch::scanFrom (std::istream& strm)
{
Bruch::scanFrom (strm); // scanFrom() der Basisklasse aufrufen

kuerzbar = (ggt() > 1); // Kürzbarkeit testen


}

} // **** ENDE Namespace Bsp ********************************

5.2.7 Weitere Fallen beim Überschreiben von Funktionen


Es gibt noch einige Fallen, die beim Überschreiben von Elementfunktionen der Basisklasse be-
achtet werden müssen. Sie werden im Folgenden vorgestellt.

Unterschiedliche Default-Argumente
Die Default-Argumente von Funktionen werden beim Kompilieren eingesetzt. Sie sind damit
immer statisch.
Sandini Bib

298 Kapitel 5: Vererbung und Polymorphie

Wenn nun virtuelle Funktionen in der Basisklasse einen anderen Default-Wert besitzen als in
der überschriebenen Implementierung der abgeleiteten Klasse, wird zwar die richtige Funktion
aufgerufen, aber der falsche Default-Wert übergeben.
Nehmen wir z.B. an, für Brüche sei eine Funktion init() definiert, die als Default-Argument
0 besitzt. Die abgeleitete Klasse KBruch überschreibt die Funktion, vergibt als Default-Argument
aber 1:
class Bruch {
...
virtual void init (int = 0);
};

class KBruch : public Bruch {


...
virtual void init (int = 1);
};
Ein Aufruf der Funktion init() kann dann dazu führen, dass unterschiedliche Default-Werte
verwendet werden, obwohl die Implementierung der abgeleiteten Klasse aufgerufen wird:
Bsp::KBruch x;
x.init(); // ruft für x auf: KBruch::init(1)

Bsp::Bruch* xp = &x;
xp->init(); // ruft für x auf: KBruch::init(0)
Um dies zu vermeiden, gilt die Regel:
 Überschriebene Funktionen sollten die gleichen Default-Argumente wie ihre Basisklassen
besitzen.

Überladen heißt überdecken


Wir haben schon gesehen, dass beim Überschreiben einer geerbten Funktion die Parametertypen
gleich sein sollten, da sonst kein echtes Überschreiben, sondern ein Überladen stattfindet. Nun
kann es aber auch sein, dass genau das gewollt ist. Eine Funktion soll geerbt werden und zusätz-
lich um eine gleichnamige Funktion mit anderen Parametern ergänzt werden. In dem Fall ist zu
beachten, dass die Funktion der Basisklasse zwar nicht überschrieben wird, dass sie trotzdem
aber nicht mehr von Objekten der abgeleiteten Klasse aufgerufen werden kann.
Betrachten wir dazu folgendes Beispiel: Die Basisklasse Bruch definiert die Elementfunk-
tion init() für einen int als Parameter. Die abgeleitete Klasse KBruch definiert die gleiche
Funktion dagegen mit dem Typ Bruch:
class Bruch {
...
void init (int = 0);
};
Sandini Bib

5.2 Virtuelle Funktionen 299

class KBruch : public Bruch {


...
void init (const Bruch&);
};
Für Objekte der abgeleiteten Klasse kann init() nur noch mit einem Bruch als Parameter
aufgerufen werden:
Bsp::KBruch x;
Bsp::Bruch a;
...
x.init(a); // OK
...
x.init(7); // FEHLER: Bruch::init(int) wurde überdeckt
Überladen heißt in abgeleiteten Klassen also immer auch überdecken. Diese Eigenschaft wurde
in C++ zur Sicherheit eingeführt, damit ein versehentlich falscher Parametertyp besser als Fehler
erkannt werden kann.
Soll die Funktion der Basisklasse weiterhin verwendbar sein, muss sie erneut definiert wer-
den:
class Bruch {
...
void init (int);
};

class KBruch : public Bruch {


...
void init (const Bruch&);
void init (int i = 0) {
Bruch::init (i);
}
};

5.2.8 Private Vererbung und reine Zugriffsdeklarationen


So wie die Klasse KBruch bisher deklariert wurde, kann noch ein Problem bei der Anwendung
der Klasse auftreten: Der Operator *= wurde zwar überschrieben, um nach einer multiplikativen
Zuweisung zu testen, ob das Objekt immer noch kürzbar ist, die überschriebene Implementierung
der Basisklasse kann aber weiterhin aufgerufen werden.
Durch Verwendung des Bereichsoperators kann nämlich explizit verlangt werden, dass die
Implementierung der Basisklasse aufgerufen wird:
Sandini Bib

300 Kapitel 5: Vererbung und Polymorphie

// vererb/kbruchtest4.cpp
// Headerdateien
#include <iostream>
#include "kbruch.hpp"

int main()
{
// KBruch deklarieren
Bsp::KBruch x(7,3);

/* x mit 3 multiplizieren
* ABER: Operator der Basisklasse Bruch verwenden
*/
x.Bsp::Bruch::operator *= (3);

// x ausgeben
std::cout << x;
std::cout << (x.istKuerzbar() ? " (kuerzbar)" : " (unkuerzbar)")
<< std::endl;
}
Dies kann dann wie hier im Beispiel dazu führen, dass Inkonsistenzen entstehen (der Bruch x ist
nach der Multiplikation kürzbar, trägt aber die Information, er wäre nicht kürzbar).
Ein solcher Aufruf ist möglich, da bei der Deklaration der Klasse angegeben wurde, dass
öffentliche Funktionen der Basisklasse weiterhin öffentlich bleiben. Dies gilt selbst dann, wenn
sie überschrieben werden.
Man kann nun streiten, ob dies ein echtes Problem ist. Da explizit angegeben werden muss,
dass die Implementierung der Basisklasse verwendet werden soll, wird eine solche Inkonsistenz
nicht aus Versehen vorkommen. Wer dies also ausdrücklich wünscht, sollte sich auch über die
Konsequenzen im Klaren sein. Andererseits werden Klassen unter anderem dazu eingeführt, um
durch eine wohldefinierte Schnittstelle zu Objekten keine Inkonsistenzen erzeugen zu können.
Das Problem kann vermieden werden, indem durch Verwendung der Zugriffsschlüsselwörter
private oder protected nichtöffentlich abgeleitet wird:
class KBruch : protected Bruch {
...
};
Dies hat zur Folge, dass alle Elementfunktionen der Basisklasse nichtöffentlich werden. Sie
können vom Anwender der Klasse damit nicht mehr aufgerufen werden oder müssen erneut
implementiert werden.
Im Beispiel der Klasse KBruch würde das z.B. bedeuten, dass unter anderem die I/O-Ope-
rationen noch einmal öffentlich deklariert und damit auch noch einmal implementiert werden
müssten. In diesem Fall würde aber die Vererbung an sich in Frage gestellt. Wenn sowieso jede
Funktion neu definiert werden muss, ist es meist einfacher, eine neue Klasse zu implementieren,
da die angesprochenen Probleme dann nicht auftauchen können.
Sandini Bib

5.2 Virtuelle Funktionen 301

Um dieses Dilemma zu lösen, gibt es die Möglichkeit, Komponenten mit unterschiedlichen Ein-
schränkungen zu erben. Eine Klasse muss dazu grundsätzlich protected oder private geerbt
werden, diese Einschränkung kann dann aber für einzelne Komponenten wieder aufgehoben wer-
den. Dies geschieht durch reine Zugriffsdeklarationen (englisch: access declarations).
Damit kann die Deklaration der Klasse KBruch wie folgt aussehen:
// vererb/kbruch5.hpp
#ifndef KBRUCH_HPP
#define KBRUCH_HPP

// Headerdatei der Basisklasse


#include "bruch.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse KBruch
* - abgeleitet von Bruch
* - neu: der Zugriff auf geerbte Komponenten wird eingeschränkt
* (public wird protected)
* - damit gilt die is-a-Beziehung nicht mehr
*/
class KBruch : protected Bruch {
protected:
bool kuerzbar; // true: Bruch ist kürzbar

// Hilfsfunktion: größter gemeinsamer Teiler von Zähler und Nenner


unsigned ggt() const;

public:
/* Default-Konstruktor, Konstruktor aus Zähler
* und Konstruktor aus Zähler und Nenner
* - Parameter werden an Bruch-Konstruktor durchgereicht
*/
KBruch (int z = 0, int n = 1) : Bruch(z,n) {
kuerzbar = (ggt() > 1);
}

// neu: reine Zugriffsdeklaration für Operationen, die public bleiben


Bruch::printOn;
Bruch::toDouble;
Sandini Bib

302 Kapitel 5: Vererbung und Polymorphie

// multiplikative Zuweisung
virtual const KBruch& operator*= (const Bruch&);

// Eingabe mit Streams


virtual void scanFrom (std::istream&);

// Bruch kürzen
virtual void kuerzen();

// Kürzbarkeit testen
virtual bool istKuerzbar() const {
return kuerzbar;
}
};

} // **** ENDE Namespace Bsp ********************************

#endif // KBRUCH_HPP

Mit dieser Implementierung kann die multiplikative Zuweisung der Basisklasse nicht mehr für
Objekte der abgeleiteten Klasse aufgerufen werden. I/O-Operationen und die Konvertierung in
einen Gleitkommawert sind durch die reine Zugriffsdeklaration aber weiterhin ohne Neuimple-
mentierung möglich:
namespace Bsp {
class KBruch : protected Bruch {
public:
// neu: reine Zugriffsdeklaration für Operationen, die public bleiben
Bruch::printOn;
Bruch::asDouble;
...
};
}

Private Vererbung kontra is-a-Beziehung


Private Vererbung reduziert die allgemeinen Konzepte der Vererbung auf reine Code-Wiederver-
wendung. Insbesondere gilt die is-a-Beziehung nicht mehr. Es ist keine automatische Typum-
wandlung von einem Objekt der abgeleiteten in ein Objekt der Basisklasse definiert:
Bsp::KBruch x(91,39);
...
Bsp::Bruch* xp = &x; // FEHLER: keine is-a-Beziehung bei privater Vererbung
Sandini Bib

5.2 Virtuelle Funktionen 303

Aus dem gleichen Grund können auch weder die Friend-Funktionen noch die Standard-I/O-
Operatoren der Klasse Bruch verwendet werden. Sie müssen für die Klasse KBruch alle neu
definiert werden. Ohne Neuimplementierung des Ausgabeoperators für KBrüche, kann dieser
dann also nicht ausgegeben werden:
Bsp::KBruch x(91,39);
...
std::cout << x; // FEHLER: kein operator<<(ostream,KBruch) definiert
Semantisch handelt es sich bei dieser Form der Vererbung nicht um eine Vererbung sondern
um eine Wiederverwendung. Durch diese Art der Implementierung können auf einfache Weise
Komponenten und Elementfunktionen übernommen werden. Eine konzeptionelle Vererbung, die
z.B. zur Arbeit mit Oberbegriffen dient und schon beim Programm-Design eine Rolle spielt
(siehe Abschnitt 5.3), kann nur durch öffentliche Vererbung implementiert werden.

5.2.9 Zusammenfassung
 Sofern nicht anders angegeben, werden Funktionsaufrufe statisch gebunden. Es wird die Ele-
mentfunktion aufgerufen, die für die Klasse definiert ist, mit der das Objekt deklariert wurde.
 Mit dem Schlüsselwort virtual kann das statische Binden für Zeiger und Referenzen auf-
gehoben werden. Es wird Code generiert, durch den erst zur Laufzeit in Abhängigkeit von
der Klasse, die ein Objekt tatsächlich hat, die richtige Funktion aufgerufen wird.
 Durch virtuelle Funktionen ist es möglich, Funktionen von Basisklassen zu überschreiben,
ohne dass durch die Verwendung von Zeigern oder Referenzen der Basisklasse Probleme
auftreten können.
 Eine Klasse, die uneingeschränkt zur Vererbung geeignet ist, sollte folgende Voraussetzungen
erfüllen:
– Alle Funktionen, die von abgeleiteten Klassen überschrieben werden könnten, müssen als
virtuelle Funktionen deklariert werden.
– Es muss ein virtueller Destruktor definiert werden.
 Bei der Implementierung einer abgeleiteten Klasse sollte Folgendes beachtet werden:
– Das Überschreiben von Funktionen, die in der Basisklasse nicht virtuell sind, kann sehr
problematisch sein.
– Funktionen der Basisklasse gelten nur dann als überschrieben, wenn die Typen bei der
Deklaration gleich sind.
– Überschriebene Funktionen sollten die gleichen Default-Argumente besitzen.
– Mit dem Bereichsoperator können überschriebene virtuelle Funktionen aufgerufen wer-
den.
– Auf als Parameter übergebene Objekte einer Basisklasse besteht nur öffentlicher Zugriff.
 Durch eine private Vererbung kann die is-a-Beziehung bei Vererbung umgangen werden. Die
Vererbung dient in diesem Fall nur noch zur Code-Wiederverwendung.
Sandini Bib

304 Kapitel 5: Vererbung und Polymorphie

5.3 Polymorphie
In diesem Abschnitt wird das Konzept der Polymorphie und seine Realisierung in C++ vorge-
stellt.

5.3.1 Was ist Polymorphie?


Polymorphie (oder auch Vielgestaltigkeit) beschreibt die Fähigkeit, dass eine Operation für ver-
schiedenartige Objekte aufgerufen werden kann und dabei zu unterschiedlichen Auswirkungen
führt. Der Aufruf einer Operation für ein Objekt wird als Nachricht an dieses Objekt betrachtet,
die von ihm selbst interpretiert wird. Da eine Operation für unterschiedliche Objekte unterschied-
lich definiert sein kann, führt die gleiche Operation dann zu völlig unterschiedlichen Reaktionen.
Auf C++ übertragen bedeutet das, dass Objekte, die verschiedene Klassen besitzen, auf den
gleichen Funktionsaufruf unterschiedlich reagieren können. Die Tatsachen, dass der gleiche Funk-
tionsname in unterschiedlichen Klassen verwendet werden kann und dass Funktionen überladen
werden können, sind polymorphe Sprachmittel von C++, die wir bereits kennen gelernt haben.

Abstraktion durch Oberbegriffe


Eine besondere Qualität, um die es vor allem in diesem Abschnitt geht, ist die Tatsache, dass
die klassenspezifischen Unterschiede von Objekten zeitweise verloren gehen können. Dies führt
zu einem wesentlichen Punkt, in dem objektorientierte Sprachen den herkömmlichen Sprachen
überlegen sind: Sie unterstützen das Abstraktionsmittel des Oberbegriffs: Verschiedenartige Ob-
jekte können zeitweise unter einem gemeinsamen Oberbegriff betrachtet und auch manipuliert
werden. Bei der Manipulierung gehen die Unterschiede aber nicht verloren.
Anwendungsbeispiele für die Verwendung von Oberbegriffen gibt es in Hülle und Fülle:
 Verschiedene Autos, Busse, Laster und Fahrräder werden unter dem Oberbegriff Fahrzeug
betrachtet. Als solches können sie sich z.B. an einem bestimmten Ort befinden und zu einem
anderen Ort fahren.
 Verschiedene Integer, Gleitkommawerte, Brüche und komplexe Zahlen werden allgemein als
numerische Werte betrachtet, die addiert und multipliziert werden können.
 Verschiedene Kreise, Rechtecke, Linien usw. werden als geometrische Objekte betrachtet,
die sich in einer Grafik befinden, eine Farbe besitzen und ausgegeben werden können.
 Verschiedene Arten von Personen (wie Kunde, Angestellter, Chef, Abteilungsleiter) werden
als Personen betrachtet, die einen Namen und andere Eigenschaften haben.
Unterstützung von Oberbegriffen bedeutet dabei, dass in Programmen, in denen verschiedenarti-
ge Objekte unter einem Oberbegriff behandelt werden, keinerlei Rücksicht auf deren tatsächliche
Klasse genommen werden muss.
 Eine Funktion für Fahrzeuge verwendet nur den Typ Fahrzeug. Damit kann sie automatisch
Autos, Busse, Laster usw. verwalten.
 Ein grafischer Editor verwendet nur den Typ GeometrischesObjekt. Damit kann der Editor
automatisch Kreise, Rechtecke, Linien usw. verwalten, ausgeben und manipulieren.
Sandini Bib

5.3 Polymorphie 305

Aufgrund der Polymorphie spielen die Unterschiede aber dennoch eine Rolle, da die Operationen
von den Objekten, die verwaltet werden, unterschiedlich interpretiert werden:
 Wenn ein Fahrzeug fährt (die Elementfunktion fahren() aufgerufen wird), wird automatisch
in Abhängigkeit vom tatsächlichen Typ des Objekts die dazugehörige Operation aufgerufen.
Ist das Fahrzeug ein Laster, wird das für Laster definierte Fahren durchgeführt (fahren()
der Klasse Laster aufgerufen). Ist es ein Auto, wird automatisch das für Autos definierte
Fahren durchgeführt.
 Soll ein grafisches Objekt ausgegeben werden, wird automatisch in Abhängigkeit von dessen
Art die entsprechende Operation aufgerufen. Handelt es sich um einen Kreis, wird die Opera-
tion zum Ausgeben eines Kreises aufgerufen, handelt es sich dagegen um ein Rechteck, wird
die dafür definierte Ausgabeoperation verwendet.
Bei der Implementierung der Anwendungssoftware für den Oberbegriff muss noch nicht einmal
bekannt sein, welche Typen unter dem Oberbegriff zusammengefasst werden:
 Wenn Boote als neue Fahrzeugart eingeführt werden, kann die Funktion für Fahrzeuge auto-
matisch auch Boote fahren lassen.
 Wenn Dreiecke oder Textelemente als neue Art geometrische Objekte eingeführt werden,
kann der grafische Editor automatisch auch diese verwalten.
Die Polymorphie ermöglicht es also, dass verschiedenartige Objekte in inhomogenen Mengen
zumindest zeitweise unter einem gemeinsamen Oberbegriff verarbeitet werden. Dabei wird in
Abhängigkeit vom tatsächlichen Typ des Objekts automatisch die jeweils richtige Funktion auf-
gerufen.

5.3.2 Polymorphie in C++


Die Polymorphie kann in verschiedenen Sprachen unterschiedlich realisiert sein. In Smalltalk,
dem Urbeispiel einer objektorientierten Sprache, wird die Polymorphie als Sprachkonzept durch
Typfreiheit realisiert: Variablen haben grundsätzlich keinen Typ, sondern können beliebige Ob-
jekte verwalten. Wenn für ein Objekt eine Operation aufgerufen wird, wird immer zur Laufzeit
in Abhängigkeit vom jeweiligen Objekttyp die dazugehörige Funktion aufgerufen. Gibt es kei-
ne entsprechende Funktion, wird eine Fehlermeldung ausgegeben.4 Insofern ist Smalltalk eine
interpretierende Sprache.

Polymorphie mit Typüberprüfung


In C++ dagegen gibt es eine Typüberprüfung. Schon der Compiler prüft, ob ein Funktionsaufruf
erlaubt ist. Dieses Konzept wird auch bei der Polymorphie aufrechterhalten. Für einen Oberbe-
griff muss eine Klasse definiert werden, die festlegt, welche Funktionen für Objekte unter dem
Oberbegriff aufgerufen werden können. Diese müssen allerdings noch nicht implementiert wer-
den.

4
Korrekterweise muss es in objektorientierter Lesart heißen, dass Objekten in Smalltalk jeweils eine Nach-

richt“ geschickt wird, die von einer dafür definierten Methode“ behandelt wird.

Sandini Bib

306 Kapitel 5: Vererbung und Polymorphie

Wenn ein Objekt einer Klasse angelegt wird, das unter dem Oberbegriff verwaltet werden kann,
wird geprüft, ob auch für alle unter dem Oberbegriff definierten Operationen eine Implementie-
rung existiert. Damit stellt bereits der Compiler sicher, dass jeder Funktionsaufruf zur Laufzeit
möglich ist. Im Gegensatz zu Smalltalk ist C++ also keine interpretierende Sprache. Laufzeit-
Fehlermeldungen über nicht vorhandene Funktionen entfallen.
Eine derartige Polymorphie kann man in C++ mit zwei verschiedenen Sprachmitteln im-
plementieren: mit Vererbung und mit Templates. In weiteren Verlauf dieses Abschnitts wird
zunächst die übliche Form von Polymorphie mit Vererbung vorgestellt. In Abschnitt 7.5.3 wird
dann auf die Implementierung von Polymorphie mit Templates eingegangen.

Sprachmittel für Polymorphie


Die Sprachmittel für die Polymorphie sind an sich schon bekannt:
 Für gemeinsame Eigenschaften verschiedener Klassen werden einfach gemeinsame Basis-
klassen verwendet. In diesen Klassen werden die Attribute und die Operationen definiert, die
für den jeweiligen Oberbegriff gelten.
Die verschiedenen Objektarten, die unter dem Oberbegriff betrachtet werden können,
werden jeweils als Klasse von der Klasse für den Oberbegriff abgeleitet. Dabei können die in
der Basisklasse definierten Operationen in den abgeleiteten Klassen unterschiedlich imple-
mentiert werden.
 Aufgrund der is-a-Beziehung können die Objekte der verschiedenen abgeleiteten Klassen
zeitweise als Objekte der Basisklasse betrachtet und damit unter ihrem Oberbegriff verwendet
werden.
Damit zur Laufzeit in Abhängigkeit von der Klasse, die ein Objekt tatsächlich besitzt, die
richtige Funktion aufgerufen wird, werden die Funktionen als virtuell deklariert.
Hinzu kommt die Besonderheit, dass in der Basisklasse für einen Oberbegriff Funktionen dekla-
riert werden können, ohne dass sie implementiert werden müssen. Es wird nur festgelegt, dass
eine bestimmte Operation aufgerufen werden kann. Was beim Aufruf wirklich geschieht, muss
in den davon abgeleiteten Klassen jeweils implementiert werden.

Abstrakte und konkrete Klassen


Basisklassen für Oberbegriffe sind typischerweise so genannte abstrakte Klassen. Abstrakte
Klassen sind Klassen, von denen es keinen Sinn macht, konkrete Objekte (Instanzen) zu er-
zeugen.
C++ unterstützt abstrakte Klassen insofern, als dass sichergestellt werden kann, dass auch
wirklich keine Objekte der Klasse erzeugt werden können (das leisten nicht alle objektorientier-
ten Sprachen).
Das Gegenstück zu einer abstrakten Klasse, also eine Klasse, von der konkrete Objekte er-
zeugt werden sollen, nennt man konkrete Klasse.

5.3.3 Polymorphie in C++ an einem Beispiel


Als Beispiel für die Verwendung von Polymorphie soll die Verwaltung verschiedener geometri-
scher Objekte betrachtet werden (ein klassisches Beispiel).
Sandini Bib

5.3 Polymorphie 307

Es gibt zunächst zwei Arten geometrischer Objekte:


 Linie
 Kreis
Diese können unter dem Oberbegriff GeoObj als geometrisches Objekt betrachtet, manipuliert
und gemeinsam verwendet werden.
Ein geometrisches Objekt hat z.B. einen Referenzpunkt und kann mit move() verschoben
und mit draw() ausgegeben werden. Ein Kreis besitzt dann zusätzlich einen Radius und im-
plementiert die Funktion zur Ausgabe. Die Funktion zum Verschieben wird geerbt. Eine Linie
besitzt neben dem Referenzpunkt einen zweiten Punkt und implementiert sowohl die Funktion
zum Verschieben als auch die Funktion zur Ausgabe neu.

G e o : : G e o O b j G e o : : K o o r d

r e f p u n k t : K o o r d x : i n t
y : i n t
m o v e ( K o o r d )
d r a w ( )

G e o : : K r e i s G e o : : L i n i e

r a d i u s : u n s i g n e d p 2 : K o o r d

d r a w ( ) m o v e ( K o o r d )
d r a w ( )

Abbildung 5.4: Klassenhierarchie für das Polymorphie-Beispiel

Abbildung 5.4 verdeutlicht die daraus resultierende Klassenhierarchie in UML-Notation. Die


kursive Darstellung der Basisklasse GeoObj bedeutet, dass es sich um eine abstrakte Klasse
handelt. Analog wird in der Basisklasse auch die Operation draw() kursiv dargestellt. Dies be-
deutet, dass es sich um eine abstrakte Operation handelt. Sie muss damit von konkreten Klassen
auf jeden Fall implementiert werden.

Hilfsklasse Koord
Als Hilfsklasse wird eine Klasse Koord verwendet. Sie betrachtet einfach nur zwei zusammen-
gehörige X- und Y-Koordinaten als gemeinsames Objekt. Es handelt sich um kein geometrisches
Objekt (das entsprechende geometrische Objekt wäre der Punkt), sondern um ein Wertepaar, das
absolut (als Position) oder relativ (als Offset) verwendet werden kann.
Der Aufbau und die Operationen sind dermaßen einfach, dass die Klasse komplett in einer
Headerdatei implementiert werden kann:
Sandini Bib

308 Kapitel 5: Vererbung und Polymorphie

// vererb/koord1.hpp
#ifndef KOORD_HPP
#define KOORD_HPP

// Headerdatei für I/O


#include <iostream>

namespace Geo {

/* Klasse Koord
* - Hilfsklasse für geometrische Objekte
* - nicht zur Vererbung geeignet
*/
class Koord {
private:
int x; // X-Koordinate
int y; // Y-Koordinate

public:
// Default-Konstruktor und Konstruktor aus zwei ints
Koord () : x(0), y(0) { // Default-Werte: 0
}
Koord (int newx, int newy) : x(newx), y(newy) {
}

Koord operator + (const Koord&) const; // Addition


Koord operator - () const; // Negation
void operator += (const Koord&); // +=
void printOn (ostream& strm) const; // Ausgabe
};

/* Operator +
* - X- und Y-Koordinaten addieren
*/
inline Koord Koord::operator + (const Koord& p) const
{
return Koord(x+p.x,y+p.y);
}

/* einstelliger Operator -
* - X- und Y-Koordinaten negieren
Sandini Bib

5.3 Polymorphie 309

*/
inline Koord Koord::operator - () const
{
return Koord(-x,-y);
}

/* Operator +=
* - Offset auf X- und Y-Koordinaten aufaddieren
*/
inline void Koord::operator += (const Koord& p)
{
x += p.x;
y += p.y;
}

/* printOn()
* - Koordinaten als Wertepaar ausgeben
*/
inline void Koord::printOn (ostream& strm) const
{
strm << '(' << x << ',' << y << ')';
}

/* Operator <<
* - Umsetzung für Standard-Ausgabeoperator
*/
inline ostream& operator<< (ostream& strm, const Koord& p)
{
p.printOn (strm);
return strm;
}

} // namespace Geo

#endif // KOORD_HPP
Die Klasse sollte eigentlich im Wesentlichen selbsterklärend sein. Bemerkenswert ist vielleicht
die Tatsache, dass zum Anlegen nur kein oder zwei Parameter übergeben werden können. Wird
kein Parameter übergeben, wird ein Objekt mit den Werten 0 als X- und Y-Koordinate initialisiert.
Sandini Bib

310 Kapitel 5: Vererbung und Polymorphie

5.3.4 Die abstrakte Basisklasse GeoObj


Die Klasse GeoObj ist die Basisklasse für den Oberbegriff. Sie legt damit fest, welche Eigen-
schaften alle geometrischen Objekte besitzen. Dazu gehören gemeinsame Attribute und Opera-
tionen, die für alle geometrischen Objekte aufgerufen werden können.
Als gemeinsames Datenelement wird definiert:
 ein Referenzpunkt
Als Operationen werden definiert:
 das Verschieben des Objekts um einen relativen Offset
 das Ausgeben des Objekts
Auch diese Klasse wird der Einfachheit halber komplett in der Headerdatei definiert. Sie hat
folgenden Aufbau:
// vererb/geoobj1.hpp
#ifndef GEOOBJ_HPP
#define GEOOBJ_HPP

// Headerdatei für Koord


#include "koord.hpp"

namespace Geo {

/* abstrakte Basisklasse GeoObj


* - gemeinsame Basisklasse für geometrische Objekte
* - zur Vererbung vorgesehen
*/
class GeoObj {
protected:
// Jedes GeoObj hat einen Referenzpunkt
Koord refpunkt;

/* Konstruktor für Startwert vom Referenzpunkt


* - nichtöffentlich
* - damit ist kein Default-Konstruktor vorhanden
*/
GeoObj (const Koord& p) : refpunkt(p) {
}

public:
// GeoObj um relativen Offset verschieben
virtual void move (const Koord& offset) {
Sandini Bib

5.3 Polymorphie 311

refpunkt += offset;
}

/* GeoObj ausgeben
* - rein virtuelle Funktion
*/
virtual void draw () const = 0;

// virtueller Destruktor
virtual ~GeoObj () {
}
};

} // namespace Geo

#endif // GEOOBJ_HPP

Zunächst werden die Komponente für den Referenzpunkt und der Konstruktor der Klasse nicht-
öffentlich definiert:
class GeoObj {
protected:
Koord refpunkt;

GeoObj (const Koord& p) : refpunkt(p) {


}
...
};
Die Tatsache, dass der Konstruktor nichtöffentlich ist, verdeutlicht schon, dass es sich um eine
abstrakte Klasse handelt, denn dadurch können keine Objekte der Klasse angelegt werden. Da
der Konstruktor einen Parameter besitzt, mit dem der Referenzpunkt initialisiert wird, existiert
kein Default-Konstruktor. Dies hat zur Folge, dass direkt abgeleitete Klassen in ihren Konstruk-
toren über Initialisierungslisten immer eine Koordinate zur Initialisierung des Referenzpunktes
übergeben müssen.
Anschließend werden die öffentlichen Funktionen deklariert, die für alle geometrischen Ob-
jekte definiert sind. Dazu gehört der virtuelle Destruktor, damit die Klasse zur Vererbung geeignet
ist.
Außerdem wird das Verschieben eines Objekts deklariert und auch schon implementiert:
class GeoObj {
...
public:
virtual void move (const Koord& offset) {
refpunkt += offset;
Sandini Bib

312 Kapitel 5: Vererbung und Polymorphie

}
...
};
Die Implementierung verschiebt einfach den Referenzpunkt, indem der übergebene Offset aufad-
diert wird. Bei Objekten, bei denen die absolute Position nur durch den Referenzpunkt definiert
wird, reicht das aus. Objekte, deren Position durch andere absolute Koordinaten definiert wird,
müssen diese Implementierung überschreiben.

Rein virtuelle Funktionen


Schließlich wird auch noch die Funktion zum Ausgeben deklariert:
class GeoObj {
...
public:
virtual void draw () const = 0;
...
};
An dieser Stelle geschieht etwas Ungewöhnliches: Statt einer Implementierung wird der Funk-
tion der Wert 0 zugewiesen. Dies bedeutet, dass die Funktion für die Klasse zwar deklariert,
aber nicht implementiert wird. Die Implementierung muss damit von davon abgeleiteten Klassen
vorgenommen werden.
Eine solche Funktion bezeichnet man als rein virtuelle Funktion (englisch: pure virtual func-
tion).5 Rein virtuelle Funktionen erfüllen einen wichtigen Zweck: Sie legen fest, dass für eine
Klasse, die als Oberbegriff dient, eine bestimmte Funktion aufgerufen werden kann, obwohl sie
noch nicht definiert werden kann. In der objektorientierten Modellierung bezeichnet man eine
derartige Operation als abstrakte Operation.
Solange eine Klasse eine rein virtuelle Funktion besitzt, ist sie unvollständig definiert. Es
können keine Objekte dieser Klasse angelegt werden. Damit wird die Klasse also automatisch
zur abstrakten Klasse.
Dies gilt auch für davon abgeleitete Klassen. Der Compiler achtet darauf, dass in davon ab-
geleiteten Klassen rein virtuelle Funktionen auch wirklich implementiert werden. Der Test, ob
alle Funktionen auch definiert sind, findet aber erst bei dem Versuch statt, ein Objekt der Klasse
anzulegen. Eine abgeleitete Klasse muss eine rein virtuelle Funktion nämlich nicht unbedingt
implementieren, da sie selbst wieder nur eine abstrakte Klasse sein kann.
Rein virtuelle Funktionen können in Basisklassen dennoch eine (Default-)Implementierung
besitzen. Darauf wird auf Seite 330 eingegangen.
Auch wenn keine rein virtuellen Funktionen vorhanden sind, können abstrakte Klassen defi-
niert werden. Dazu müssen die Konstruktoren einfach, wie hier ebenfalls geschehen, nichtöffent-
lich deklariert werden.

5
Die Übersetzungen von pure virtual function variieren. Ich habe z.B. auch schon reine virtuelle Funktion
oder pure virtuelle Funktion gefunden.
Sandini Bib

5.3 Polymorphie 313

Die abgeleitete Klasse Kreis


Nun kann die Klasse Kreis als abgeleitete Klasse von GeoObj definiert werden. Von dieser
Klasse macht es Sinn, konkrete Objekte (Instanzen) zu erzeugen. Es handelt sich also um eine
konkrete Klasse.
Ein Kreis besteht aus einem Mittelpunkt, der gleichzeitig Referenzpunkt ist, und einem Ra-
dius, der als Komponente neu hinzukommt (siehe Abbildung 5.5).

Kreis:
Datenelemente: Elementfunktionen:
geerbt refpunkt move()
von GeoObj:
neu: radius Kreis()
draw()

Abbildung 5.5: Komponenten der Klasse Kreis

Auch die Implementierung der Klasse Kreis befindet sich vollständig in der Headerdatei. Sie
hat folgenden Aufbau:
// vererb/kreis1.hpp
#ifndef KREIS_HPP
#define KREIS_HPP

// Headerdatei für I/O


#include <iostream>

// Headerdatei der Basisklasse


#include "geoobj.hpp"

namespace Geo {

/* Klasse Kreis
* - abgeleitet von GeoObj
* - ein Kreis besteht aus:
* - Mittelpunkt (Referenzpunkt, geerbt)
* - Radius (neu)
*/
class Kreis : public GeoObj {
protected:
unsigned radius; // Radius
Sandini Bib

314 Kapitel 5: Vererbung und Polymorphie

public:
// Konstruktor für Mittelpunkt und Radius
Kreis (const Koord& m, unsigned r)
: GeoObj(m), radius(r) {
}

// Ausgabe (jetzt auch implementiert)


virtual void draw () const;

// virtueller Destruktor
virtual ~Kreis () {
}
};

/* Ausgabe
* - inline definiert
*/
inline void Kreis::draw () const
{
std::cout << "Kreis um Mittelpunkt " << refpunkt
<< " mit Radius " << radius << std::endl;
}

} // namespace Geo

#endif // KREIS_HPP
Der Konstruktor verlangt einen Mittelpunkt und einen Radius als Parameter zur Initialisierung.
Der Mittelpunkt wird über die Initialisierungsliste an den Konstruktor der Basisklasse GeoObj
übergeben, wo er zur Initialisierung des Referenzpunktes verwendet wird. Mit dem als zweiten
Parameter übergebenen Radius wird die entsprechende Komponente initialisiert.
Die Klasse implementiert die Ausgabefunktion draw(). Dadurch besitzt die Klasse keine rein
virtuelle Funktion mehr und kann als konkrete Klasse zum Anlegen von Objekten verwendet
werden. Die Funktion selbst simuliert die Ausgabe durch das Schreiben eines entsprechenden
Textes.
Die Funktion move() wird von der Basisklasse übernommen, da nur der Mittelpunkt die
Position des Kreises definiert und ein Verschieben dieses Punktes ausreicht.

Die abgeleitete Klasse Linie


Die Klasse Linie besitzt im Prinzip den gleichen Aufbau wie die Klasse Kreis. Eine Linie
besteht bei dieser Implementierung allerdings aus zwei absoluten Koordinaten, dem Anfangs-
und dem Endpunkt. Zusätzlich zum Referenzpunkt kommt also noch ein zweiter Punkt hinzu
(siehe Abbildung 5.6).
Sandini Bib

5.3 Polymorphie 315

Linie:
Datenelemente: Elementfunktionen:
geerbt refpunkt
von GeoObj:
neu: p2 Linie()
move()
draw()

Abbildung 5.6: Komponenten der Klasse Linie

Auch die Implementierung der Klasse Linie befindet sich vollständig in der Headerdatei. Sie
hat folgenden Aufbau:
// vererb/linie1.hpp
#ifndef LINIE_HPP
#define LINIE_HPP

// Headerdatei für I/O


#include <iostream>

// Headerdatei der Basisklasse


#include "geoobj.hpp"

namespace Geo {

/* Klasse Linie
* - abgeleitet von GeoObj
* - ein Linie besteht aus:
* - einem Anfangspunkt (Referenzpunkt, geerbt)
* - einem Endpunkt (neu)
*/
class Linie : public GeoObj {
protected:
Koord p2; // zweiter Punkt, Endpunkt

public:
// Konstruktor für Anfangs- und Endpunkt
Linie (const Koord& a, const Koord& b)
: GeoObj(a), p2(b) {
}
Sandini Bib

316 Kapitel 5: Vererbung und Polymorphie

// Ausgabe (jetzt auch implementiert)


virtual void draw () const;

// Verschieben (neu implementiert)


virtual void move (const Koord&);

// virtueller Destruktor
virtual ~Linie () {
}
};

/* Ausgabe
* - inline definiert
*/
inline void Linie::draw () const
{
std::cout << "Linie von " << refpunkt
<< " bis " << p2 << std::endl;
}

/* Verschieben
* - inline neu implementiert
*/
inline void Linie::move (const Koord& offset)
{
refpunkt += offset; // entspricht GeoObj::move(offset);
p2 += offset;
}

} // namespace Geo

#endif // LINIE_HPP
Der Konstruktor verlangt in diesem Fall zwei Koordinaten als Parameter zur Initialisierung. Die
erste Koordinate wird über die Initialisierungsliste an den Konstruktor der Basisklasse GeoObj
übergeben, wo sie zur Initialisierung des Referenzpunktes verwendet wird. Mit der zweiten Ko-
ordinate wird die Komponente für den neu hinzugekommenen zweiten Punkt initialisiert.
Auch diese Klasse implementiert die Ausgabefunktion draw() derart, dass ein entsprechen-
der Text ausgegeben wird.
In diesem Fall wird allerdings auch die Funktion move() neu implementiert, da sowohl der
Anfangs- als auch der Endpunkt absolute Koordinaten besitzen. Man hätte den zweiten Punkt ge-
nausogut als Offset auf den ersten Punkt implementieren können, wodurch die Funktion move()
Sandini Bib

5.3 Polymorphie 317

von der Basisklasse übernommen werden könnte. Dann müssten bei anderen Funktionen, wie
etwa der Ausgabe, allerdings jeweils Umrechnungen zwischen relativem Offset und absoluter
Koordinate stattfinden.

Anwendungsbeispiel
Ein kleines Beispielprogramm soll nun die Anwendung der Polymorphie verdeutlichen. In dem
Beispiel werden Kreise und Linien in einer gemeinsamen Menge als geometrische Objekte be-
trachtet und manipuliert. Beim Verschieben und Ausgeben werden aber immer die Funktionen
aufgerufen, die für die tatsächliche Klasse der Objekte definiert sind.
Das Programm hat folgenden Aufbau:
// vererb/geotest1.cpp
// Headerdateien für verwendete Klassen
#include "linie.hpp"
#include "kreis.hpp"
#include "geoobj.hpp"

// Vorwärtsdeklaration
void geoObjAusgeben (const Geo::GeoObj&);

int main()
{
Geo::Linie l1 (Geo::Koord(1,2), Geo::Koord(3,4));
Geo::Linie l2 (Geo::Koord(7,7), Geo::Koord(0,0));
Geo::Kreis k (Geo::Koord(3,3), 11);

// inhomogene Menge von geometrischen Objekten:


Geo::GeoObj* menge[10];

menge[0] = &l1; // Menge enthält: - Linie l1


menge[1] = &k; // - Kreis k
menge[2] = &l2; // - Linie l2

/ * Elemente in der Menge ausgeben und verschieben


* - es wird automatisch die richtige Funktion aufgerufen
*/
for (int i=0; i<3; i++) {
menge[i]->draw();
menge[i]->move(Geo::Koord(3,-3));
}

// Linien einzeln über Hilfsfunktion ausgeben


Sandini Bib

318 Kapitel 5: Vererbung und Polymorphie

geoObjAusgeben(l1);
geoObjAusgeben(k);
geoObjAusgeben(l2);
}

void geoObjAusgeben (const Geo::GeoObj& obj)


{
/* es wird automatisch die richtige Funktion aufgerufen
*/
obj.draw();
}
Nach dem Erzeugen von zwei Linien und einem Kreis werden diese in einem Feld (Array) von
Zeigern auf den Typ GeoObj als geometrische Objekte verwendet. Die Variable menge repräsen-
tiert also eine inhomogene Menge von geometrischen Objekten.
In einer Schleife wird dann jedes in der Menge befindliche Element ausgegeben und ver-
schoben. Weil virtuelle Funktionen verwendet werden, wird zur Laufzeit jeweils geprüft, welche
Klasse das geometrische Objekt tatsächlich hat, und die entsprechende Funktion aufgerufen.
Genauso wird bei Verwendung einer Referenz automatisch die richtige Funktion aufgerufen,
was bei der Funktion geoObjAusgeben() ausgenutzt wird.
Die Ausgabe des Programms lautet insgesamt:
Linie von (1,2) bis (3,4)
Kreis um Mittelpunkt (3,3) mit Radius 11
Linie von (7,7) bis (0,0)
Linie von (4,-1) bis (6,1)
Kreis um Mittelpunkt (6,0) mit Radius 11
Linie von (10,4) bis (3,-3)
Polymorphie ist nur möglich, wenn die Identität eines Objekts nicht verloren geht. Das bedeutet,
ein Objekt darf nur über Zeiger und Referenzen zeitweise als Objekt der Klasse GeoObj betrach-
tet werden. Eine Variable vom Typ GeoObj ist und bleibt vom Typ GeoObj, auch wenn ein Kreis
oder eine Linie zugewiesen wird.
In diesem Fall kann keine Variable vom Typ GeoObj deklariert werden, da es sich um eine
abstrakte Klasse handelt (sie besitzt eine rein virtuelle Funktion und keinen öffentlichen Kon-
struktor). Es sind aber auch Basisklassen denkbar, von denen es durchaus auch Sinn macht,
Objekte zu erzeugen. In dem Fall verliert ein Objekt seine Identität, wenn es z.B. durch Zuwei-
sung oder Typumwandlung zum Objekt der Basisklasse wird. Polymorphie kann dann nicht mehr
stattfinden.
Damit dies nicht aus Versehen geschieht, sollte man darauf achten, dass bei Polymorphie die
Basisklassen immer abstrakt sind. Dies ist immer möglich, indem alle Konstruktoren nichtöffent-
lich deklariert werden. Wenn es Sinn macht, von einer Basisklasse für Polymorphie auch kon-
krete Objekte zu erzeugen, können sie durch eine davon abgeleitete Klasse erzeugt werden, die
keine neuen Komponenten, sondern nur die entsprechenden Konstruktoren mit öffentlichem Zu-
griff definiert.
Sandini Bib

5.3 Polymorphie 319

5.3.5 Anwendung von Polymorphie in Klassen


Das vorherige Beispiel zeigte zwar die Technik der Polymorphie, der wesentliche Vorteil ist aber
vielleicht nur zu erahnen.
Eine mögliche Anwendung, die die Vorteile von Polymorphie sehr viel besser verdeutlicht, ist
die Verwendung von Polymorphie in Klassen. Dies wird im Folgenden am Beispiel der Zusam-
menfassung verschiedener geometrischer Objekte zu einer Objekt-Gruppe gezeigt. Dazu wird
eine Klasse GeoGruppe definiert. Da auch eine Gruppe geometrischer Objekte selbst wieder als
geometrisches Objekt betrachtet werden kann, wird die Klasse selbst von GeoObj abgeleitet. Die
Klasse GeoGruppe beschreibt also als geometrisches Objekt eine Gruppe geometrischer Objekte.
Die Klassenhierarchie erhält dadurch den in Abbildung 5.7 dargestellten Aufbau.

G e o : : G e o O b j G e o : : K o o r d

r e f p u n k t : K o o r d x : i n t
y : i n t
m o v e ( K o o r d )
d r a w ( )

G e o : : K r e i s G e o : : L i n i e G e o : : G e o G r u p p e

r a d i u s : u n s i g n e d p 2 : K o o r d e l e m s : v e c t o r < G e o O b j * >

d r a w ( ) m o v e ( K o o r d ) d r a w ( )
d r a w ( ) a d d ( G e o O b j )
r e m o v e ( G e o O b j ) : b o o l

Abbildung 5.7: Polymorphie-Beispiel mit polymorpher Klasse GeoGruppe

Auch die Klasse GeoGruppe implementiert die Funktion draw() und erbt die Funktion move().
Hinzu kommen ein Vektor (siehe Abschnitt 3.5.1) für die darin verwalteten Elemente und Funk-
tionen zum Einfügen und Löschen eines Elements. Natürlich gehören in der Praxis weitere Funk-
tionen dazu, hier geht es nur um das Prinzip.

Headerdatei der Klasse GeoGruppe


Die Headerdatei der Klasse GeoGruppe enthält wie üblich die Klassendeklaration. Sie hat fol-
genden Aufbau:
// vererb/gruppe2.hpp
#ifndef GEOGRUPPE_HPP
#define GEOGRUPPE_HPP
Sandini Bib

320 Kapitel 5: Vererbung und Polymorphie

// Headerdatei der Basisklasse einbinden


#include "geoobj.hpp"

// Headerdatei für die interne Verwaltung der Elemente


#include <vector>

namespace Geo {

/* Klasse GeoGruppe
* - abgeleitet von GeoObj
* - eine GeoGruppe besteht aus:
* - einem Referenzpunkt (geerbt)
* - einer Menge von geometrischen Elementen (neu)
*/
class GeoGruppe : public GeoObj {
protected:
std::vector<GeoObj*> elems; // Menge von Zeigern auf GeoObjs

public:
// Konstruktor mit optionalem Referenzpunkt
GeoGruppe (const Koord& p = Koord(0,0)) : GeoObj(p) {
}

// Ausgabe (jetzt auch implementiert)


virtual void draw () const;

// Element einfügen
virtual void add (GeoObj&);

// Element entfernen
virtual bool remove (GeoObj&);

// virtueller Destruktor
virtual ~GeoGruppe () {
}
};

} // namespace Geo

#endif // GEOGRUPPE_HPP
Sandini Bib

5.3 Polymorphie 321

Intern wird zur Verwaltung der Elemente ein Vektor verwendet:


class GeoGruppe : public GeoObj {
protected:
std::vector<GeoObj*> elems; // Menge von Zeigern auf GeoObjs
...
};
Im Vektor selbst werden dabei jeweils Zeiger (Verweise) auf die darin befindlichen geometri-
schen Objekte eingetragen. Dies ist notwendig, um den tatsächlichen Datentyp der Elemente zu
erhalten. Es wäre auch gar nicht möglich, GeoObj als Elementtyp zu verwenden, da es sich dabei
um eine abstrakte Klasse handelt. Die Verwendung von Referenzen ist ebenfalls nicht möglich,
da bei ihnen zur Initialisierungszeit festgelegt werden muss, für welches Objekt sie stehen. In
diesem Fall werden aber zur Laufzeit Elemente eingetragen oder auch entfernt.
Der Konstruktor enthält einen Parameter, mit dem über den Konstruktor der Basisklasse der
Referenzpunkt initialisiert wird:
class GeoGruppe : public GeoObj {
public:
// Konstruktor mit optionalem Referenzpunkt
GeoGruppe (const Koord& p = Koord(0,0)) : GeoObj(p) {
}
...
};
Er enthält als Default-Argument den Ursprung (die Koordinate (0,0)). Dieser wird bei der Geo-
Gruppe als Offset auf die Elemente der Gruppe betrachtet. Die Koordinaten der Elemente sind
damit relativ und beziehen sich auf den Referenzpunkt der GeoGruppe.

Quelldatei der Klasse GeoGruppe


In der Quelldatei der Klasse GeoGruppe werden die Funktionen zum Einfügen und Entfernen
eines Elements sowie zum Ausgeben aller Elemente definiert:
// vererb/gruppe2.cpp
#include "gruppe.hpp"
#include <algorithm>

namespace Geo {

/* add
* - Element einfügen
*/
void GeoGruppe::add (GeoObj& obj)
{
// Adresse vom geometrischen Objekt eintragen
Sandini Bib

322 Kapitel 5: Vererbung und Polymorphie

elems.push_back(&obj);
}

/* remove
* - Element löschen
*/
bool GeoGruppe::remove (GeoObj& obj)
{
// erstes Element mit dieser Adresse finden
std::vector<GeoObj*>::iterator pos;
pos = std::find(elems.begin(),elems.end(),
&obj);
if (pos != elems.end()) {
elems.erase(pos);
return true;
}
else {
return false;
}
}

/* draw
* - alle Elemente unter Berücksichtigung des Referenzpunktes ausgeben
*/
void GeoGruppe::draw () const
{
for (unsigned i=0; i<elems.size(); ++i) {
elems[i]->move(refpunkt); // Offset für den Referenzpunkt addieren
elems[i]->draw(); // Element ausgeben
elems[i]->move(-refpunkt); // Offset wieder entfernen
}
}

} // namespace Geo

Die Funktion zum Einfügen trägt einfach nur die Adresse des eingefügten Elements in die interne
Menge ein:
void GeoGruppe::add (GeoObj& obj)
{
// Adresse vom geometrischen Objekt eintragen
elems.push_back(&obj);
}
Sandini Bib

5.3 Polymorphie 323

Da für das übergebene geometrische Objekt nur Referenzen und Zeiger verwendet werden, bleibt
dessen Klassenzugehörigkeit erhalten.
Dies wird dann beim Ausgeben der Elemente ausgenutzt:
void GeoGruppe::draw () const
{
for (unsigned i=0; i<elems.size(); ++i) {
elems[i]->move(refpunkt); // Offset für den Referenzpunkt addieren
elems[i]->draw(); // Element ausgeben
elems[i]->move(-refpunkt); // Offset wieder entfernen
}
}
In einer Schleife werden einfach alle Elemente der GeoGruppe ausgegeben, indem für diese unter
anderem die Funktion draw() aufgerufen wird. Durch die Verwendung der virtuellen Funktio-
nen wird dabei jeweils automatisch die richtige Funktion aufgerufen: Handelt es sich bei dem
Element um einen Kreis, wird Kreis::draw() aufgerufen. Handelt es sich bei dem Element
um eine Linie, wird Linie::draw() aufgerufen. Und handelt es sich um eine Gruppe geome-
trischer Objekte (die ja selbst als geometrisches Objekte wiederum in einer Gruppe sein kann),
wird GeoGruppe::draw() aufgerufen, das dann für alle darin befindlichen Elemente wiederum
automatisch das richtige draw() aufruft.
Vor und nach dem Ausgeben mit draw() wird das Objekt zur Berücksichtigung des Off-
sets in der Gruppe noch entsprechend verschoben bzw. zurückgeschoben. In der Praxis würde
man stattdessen die Funktion draw() sicherlich mit einem Offset als Parameter versehen. Aber
auch hier wird je nach tatsächlichem Datentyp der Elemente automatisch die passende move()-
Implementierung aufgerufen.
Abgerundet wird die Klasse durch die Funktion zum Entfernen eines Elements:
bool GeoGruppe::remove (GeoObj& obj)
{
// erstes Element mit dieser Adresse finden
std::vector<GeoObj*>::iterator pos;
pos = std::find(elems.begin(),elems.end(),
&obj);
if (pos != elems.end()) {
elems.erase(pos);
return true;
}
else {
return false;
}
}
In diesem Fall wird mit Hilfe des Algorithmus find() die Position des übergebenen geometri-
schen Objekts gesucht. Man beachte, dass die Adressen verglichen werden, was bedeutet, dass
Sandini Bib

324 Kapitel 5: Vererbung und Polymorphie

es sich um das identische Objekt handeln muss. Wird ein derartiges Objekt gefunden, wird es
mit erase aus der Menge entfernt, und es wird true zurückgeliefert. Wird das Objekt nicht
gefunden, liefert die Funktion false.

Anwendung
Das erste Anwendungsprogramm für geometrische Objekte von Seite 317 kann nun für die Ver-
wendung einer Gruppe von geometrischen Objekten umgeschrieben werden:
// vererb/geotest2.cpp
// Headerdatei für I/O
#include <iostream>

// Headerdateien für verwendete Klassen


#include "linie.hpp"
#include "kreis.hpp"
#include "gruppe.hpp"

int main()
{
Geo::Linie l1 (Geo::Koord(1,2), Geo::Koord(3,4));
Geo::Linie l2 (Geo::Koord(7,7), Geo::Koord(0,0));
Geo::Kreis k (Geo::Koord(3,3), 11);

Geo::GeoGruppe g;

g.add(l1); // GeoGruppe enthält: - Linie l1


g.add(k); // - Kreis k
g.add(l2); // - Linie l2

g.draw(); // GeoGruppe ausgeben


std::cout << std::endl;

g.move(Geo::Koord(3,-3)); // GeoGruppen-Offset verschieben


g.draw(); // GeoGruppe nochmal ausgeben
std::cout << std::endl;

g.remove(l1); // GeoGruppe enthält nur noch k und l2


g.draw(); // GeoGruppe nochmal ausgeben
}
Zunächst werden geometrische Objekte unterschiedlicher Art in die Gruppe eingefügt:
g.add(l1); // GeoGruppe enthält: - Linie l1
Sandini Bib

5.3 Polymorphie 325

g.add(k); // - Kreis k
g.add(l2); // - Linie l2
Der Aufruf
g.draw();
ruft jeweils die Funktion draw() der Klasse GeoGruppe auf, wodurch die Schleife aufgerufen
wird, die für die darin enthaltenen Elemente jeweils das richtige draw() aufruft.
Das Programm hat also folgende Ausgabe:
Linie von (1,2) bis (3,4)
Kreis um Mittelpunkt (3,3) mit Radius 11
Linie von (7,7) bis (0,0)

Linie von (4,-1) bis (6,1)


Kreis um Mittelpunkt (6,0) mit Radius 11
Linie von (10,4) bis (3,-3)

Kreis um Mittelpunkt (6,0) mit Radius 11


Linie von (10,4) bis (3,-3)
Da eine GeoGruppe selbst ein geometrisches Objekt ist, könnte sie sich selbst auch wiederum in
einer GeoGruppe befinden:
GeoGruppe g2;

g2.add(g); // GeoGruppe g2 enthält GeoGruppe g


Dabei sollte eine GeoGruppe aber nicht direkt oder indirekt sich selbst enthalten, da dies zu
Endlosrekursionen führen kann.

5.3.6 Polymorphie ist keine feste Fallunterscheidung


Auf einen wichtigen Aspekt der Klasse GeoGruppe soll nun noch eingegangen werden: Wie
man sieht, operiert die GeoGruppe wirklich als inhomogene Menge für geometrische Objekte.
Die Klasse GeoGruppe enthält dabei aber keinerlei Code, der sich auf die konkrete Datentypen
für die geometrischen Objekte festlegt. Die Klasse verwendet für die verwalteten geometrischen
Objekte nur die Klasse GeoObj, die als Oberbegriff für alle geometrischen Objekte dient. Damit
kann die Klasse auch Objekte beliebiger anderer Klassen verwalten, solange diese von GeoObj
abgeleitet sind.
Wenn also neue geometrische Objekte wie Dreiecke oder Texte gebraucht werden, müssen
nur entsprechende Klassen von GeoObj abgeleitet werden. Die Klasse GeoGruppe bleibt un-
verändert und muss auch nicht neu übersetzt werden. Dies ist ein ganz wesentlicher Vorteil für
die Praxis. Solange sich die Anforderungen an den Oberbegriff nicht ändern, ist das System
erweiterbar, ohne dass existierende Implementierungen für den Oberbegriff angetastet werden
müssen. Komplexe Vorgänge wie das Verschieben, Ausgeben und Zusammenfassen von geome-
trischen Objekten müssen nur einmal grundsätzlich implementiert werden.
Sandini Bib

326 Kapitel 5: Vererbung und Polymorphie

5.3.7 Rückumwandlung eines Objekts in seine Klasse


Bei der Anwendung von Polymorphie gibt es allerdings mitunter folgendes Problem: Durch die
strenge Typüberprüfung geht die tatsächliche Klasse eines Objekts, das unter seinem Oberbe-
griff betrachtet wird, syntaktisch verloren. Wird z.B. eine GeoGruppe als geometrisches Objekt
behandelt, kann dafür keine speziell für GeoGruppen definierte Funktion aufgerufen werden:
Geo::GeoGruppe g; // GeoObj-Gruppe

g.add(l1); // OK: Gruppe enthält Linie l1


...

Geo::GeoObj& geoobj = g; // Gruppe wird als geometrische Objekt betrachtet

geoobj.add(l1); // FEHLER: add() für GeoObj nicht definiert


Wenn verschiedenartige Objekte zeitweise unter der gleichen Oberbegriff-Klasse betrachtet wer-
den, tritt also das Problem auf, dass sie nicht ohne weiteres wieder als das betrachtet werden
können, was sie eigentlich sind. Diese strenge Typprüfung ist auch gut so, denn sie stellt sicher,
dass man zur Laufzeit nicht add() für Linien oder Kreise aufrufen kann.
Für polymorphe Datentypen ist es in C++ allerdings möglich, so genannte Laufzeit-Typin-
formationen abzurufen. Der dazugehörige Mechanismus wird als Abkürzung der englischen Be-
zeichnung runtime type information auch kurz mit RTTI bezeichnet. Dabei gibt es zwei Möglich-
keiten:
 Man kann versuchen, ein Objekt mit einem Downcast in seinen tatsächlichen Datentyp um-
zuwandeln.
 Man kann die Klasse eines Objekts konkret abfragen.
Beides ist aber nur bei polymorphen Datentypen möglich. Das sind Datentypen, die mindestens
eine virtuelle Elementfunktion besitzen.

Downcasting mit dem Operator dynamic_cast


Mit dem Operator dynamic_cast kann man ein Objekt wieder in seinen tatsächlichen Datentyp
umwandeln. Seine Anwendung sieht wie folgt aus:
void inGruppeEinfuegen (Geo::GeoObj& obj, const Geo::GeoObj& elem)
{
dynamic_cast<GeoGruppe&>(obj).add(elem);
// Exception, wenn obj keine GeoGruppe ist
}
Dem Operator dynamic_cast wird als erstes Argument in spitzen Klammern der Datentyp über-
geben, in den das Objekt umgewandelt werden soll. Dabei muss es sich um einen Referenz-
Datentyp handeln. Als zweiter Datentyp wird in runden Klammern das Objekt übergeben, das in
diesen Datentyp umgewandelt werden soll:
dynamic_cast<GeoGruppe&>(obj)
Sandini Bib

5.3 Polymorphie 327

Die Tatsache, dass der Datentyp eine Referenz sein muss, soll verdeutlichen, dass hier kein neues
Objekt, sondern nur eine temporäre zweite Betrachtung des Objekts zurückgeliefert wird. Eine
derartige Betrachtung ist nur möglich, wenn es sich bei dem übergebenen Objekt wirklich um ein
Objekt der angegebenen Klasse oder einer davon abgeleiteten Klasse handelt. Ist dies nicht der
Fall, wird eine Ausnahme vom Typ std::bad_cast ausgelöst. Sie kann wie in Abschnitt 3.6
beschrieben behandelt werden.
Man kann mit dynamic_cast auch Zeiger auf polymorphe Objekte umwandeln. In diesem
Fall wird im Fehlerfall 0 bzw. NULL zurückgeliefert:
void sonderbehandlung (Geo::GeoObj* objptr)
{
Geo::GeoGruppe* p = dynamic_cast<GeoGruppe*>(objptr);
if (p != NULL) {
// OK: *objptr ist GeoGruppe
p->add(...);
}
else {
// *objptr ist keine GeoGruppe
}
}
Diese Form kann man auch dazu verwenden, den tatsächlichen Datentyp von Objekten, die keine
Zeiger sind, zur Laufzeit auszuwerten:
void sonderbehandlung (const Geo::GeoObj& obj)
{
Geo::GeoGruppe* p = dynamic_cast<GeoGruppe*>(&obj);
if (p != NULL) {
// OK: obj ist GeoGruppe
Geo::GeoGruppe& gruppe = *p;
...
}
else {
// obj ist keine GeoGruppe
}
}
Geht es nur darum herauszufinden, ob ein Objekt von einem bestimmten Typ ist, reicht auch
Folgendes:
void typabfrage (const Geo::GeoObj& obj)
{
if (dynamic_cast<GeoGruppe*>(&obj) != NULL) {
// obj ist GeoGruppe
...
}
Sandini Bib

328 Kapitel 5: Vererbung und Polymorphie

else {
// obj ist keine GeoGruppe
...
}
}

Konkrete Typabfrage mit dem Operator typeid


Mit dem Operator typeid kann man bei polymorphen Datentypen zur Laufzeit den Datentyp
ermitteln. Das folgende Beispiel zeigt, wie dies aussehen kann:
#include <typeinfo>

void foo (const Geo::GeoObj& obj)


{
// Datentyp ausgeben
std::cout << typeid(obj).name() << std::endl;

// Datentyp vergleichen
if (typeid(t) == typeid(Geo::GeoGruppe)) {
...
}
}
typeid liefert zu einem Objekt bzw. zu einer Klasse ein Beschreibungsobjekt, das den Datentyp
std::type_info besitzt. Dieser Datentyp wird in <typeinfo> definiert. Für ein derartiges
Objekt kann man folgende Operationen aufrufen:
 name() liefert den Namen der Klasse als C-String. Wie dieser String genau aussieht, ist
implementierungsspezifisch.
 == und != liefern, ob zwei Datentypen gleich sind. Da dem Operator typeid als Argument
ein Objekt oder eine Klasse übergeben werden kann, kann man sowohl herausbekommen, ob
der Datentyp zweier Objekte gleich ist, als auch ermitteln, ob ein Objekt einen bestimmten
Datentyp hat.
 Mit before() kann ein Datentyp außerdem mit einem anderen zum Zwecke der Sortierung
verglichen werden.
Handelt es sich bei einem Argument für typeid um einen Zeiger und hat dieser Zeiger den Wert
0 bzw. NULL, dann wird eine Ausnahme vom Typ std::bad_typeid ausgelöst. Sie kann wie in
Abschnitt 3.6 beschrieben behandelt werden.

RTTI und Design


Die Verwendung von RTTI ist mit Vorsicht zu betrachten. Insbesondere bei der Verwendung von
typeid macht man Code von ganz konkreten Datentypen abhängig. Eine damit implementierte
Fallunterscheidung über verschiedene Datentypen zeugt von äußerst schlechtem Design:
Sandini Bib

5.3 Polymorphie 329

int flaeche (const Geo::GeoObj& obj) // ganz schlechtes Beispiel


{
if (typeid(obj) == typeid(Geo::Kreis) ||
typeid(obj) == typeid(Geo::Rechteck)) {
// Fläche berechnen
...
}
else {
return 0;
}
}
Durch eine derartige Programmierung wird der ganze Vorteil des Programmierens mit Oberbe-
griffen zunichte gemacht. Diese Funktion muss mit jeder neuen Art von geometrischen Objekten
verifiziert und gegebenenfalls angepasst werden.
Etwas besser wäre ein Design, das unter der Klasse GeoObj eine weitere abstrakte Klasse
GeoFlaeche einführt, von der dann alle konkreten flächenartigen Objekte abgeleitet sind. In
dem Fall muss der Code nicht bei einer neuen Art von Fläche umgeschrieben werden:
int flaeche (const Geo::GeoObj& obj) // etwas besseres Beispiel
{
GeoFlaeche* fp = dynamic_cast<GeoFlaeche*>(&obj)
if (fp != NULL) {
// Fläche berechnen
return fp->berechneFlaeche();
}
else {
return 0;
}
}
Aber auch hier muss darauf geachtet werden, dass nicht plötzlich geometrische Objekte entwor-
fen werden, für die es zwar Sinn macht, eine Fläche zu berechnen, die aber nicht von GeoFlaeche
abgeleitet werden.
Am besten ist da sicherlich ein Design, das es erlaubt, von allen Objekten die Flächen zu
berechnen:
int flaeche (const Geo::GeoObj& obj) // OK
{
return obj.berechneFlaeche(); // jedes Objekt einfach nach seiner Fläche fragen
}
Dazu kann in Geo::GeoObj bereits eine Default-Implementierung vorgesehen werden, die für
alle Objekte, die keine Fläche darstellen, 0 zurückliefert:
Sandini Bib

330 Kapitel 5: Vererbung und Polymorphie

class GeoObj {
public:
virtual int berechneFlaeche () const {
return 0;
}
...
};
Um zu verhindern, dass man vergisst, für abgeleitete Klassen diese Funktion auch zu implemen-
tieren, kann man auch eine Default-Implementierung vorsehen und die Funktion dennoch als rein
virtuell deklarieren:
class GeoObj {
public:
virtual int berechneFlaeche () const = 0; // rein virtuell
...
};
...

int GeoObj::berechneFlaeche () const // aber vorhanden


{
return 0;
}
Abgeleitete Klassen müssen diese Funktion dann implementieren, können dazu aber die Imple-
mentierung der Basisklasse aufrufen:
class Linie : public GeoObj {
public:
virtual int berechneFlaeche () const {
// Default-Implementierung der Basisklasse übernehmen
return GeoObj::berechneFlaeche();
}
...
};

5.3.8 Design by Contract


Abstrakte Basisklassen sind das wohl wichtigste Mittel, um in großen Programmen Module und
Komponenten voneinander zu entkoppeln. Als Schnittstelle definieren sie einen Vertrag zwi-
schen Objekten, die eine bestimmte Dienstleistung erbringen, und Objekten, die diese Dienst-
Sandini Bib

5.3 Polymorphie 331

leistung abrufen. Ist der Vertrag einmal als abstrakte Basisklasse definiert, können beide Seiten
unabhängig voneinander entwickelt und modifiziert werden.6
Eine derartige Schnittstelle könnte z.B. wie folgt lauten:
class Druckbar {
public:
virtual void drucken() = 0;
};
Man kann nun einerseits beliebige Klassen implementieren, die diese Schnittstelle implementie-
ren:
class XYZ : public Druckbar {
...
public:
virtual void drucken() {
...
}
};
Andererseits kann man Funktionen schreiben, die für alle Objekte, die diesen Vertrag erfüllen
und also von Druckbar abgeleitet sind, entsprechende Funktionen aufrufen:
void foo (const Druckbar& obj)
{
...
obj.drucken();
...
}
Im englischsprachigen Umfeld werden derartige abstrakte Basisklassen auch einfach ABCs ge-
nannt (abstract base classes). Es ist mitunter eine gute Richtlinie, Basisklassen grundsätzlich nur
als derartige abstrakte Basisklassen zu implementieren.

5.3.9 Zusammenfassung
 Polymorphie bezeichnet die Fähigkeit, dass die gleichen Operationen bei unterschiedlichen
Objekten zu unterschiedlichen Reaktionen führen.
 C++ unterstützt Polymorphie durch
– gleichnamige Komponenten bei verschiedenen Klassen
– das Überladen von Funktionen
– virtuelle Funktionen

6
Java-Programmierer kennen ein derartiges Konzept als spezielles Sprachmittel. Dort wird es mit Hil-
fe der Schlüsselwörter interface und implements angeboten. In C++ gibt es dazu keine speziellen
Schlüsselwörter.
Sandini Bib

332 Kapitel 5: Vererbung und Polymorphie

 Mit Polymorphie kann das Abstraktionsmittel des Oberbegriffs implementiert werden.


 Abstrakte Klassen sind Klassen, bei denen nicht vorgesehen ist, dass von ihnen konkrete
Objekte erzeugt werden. Sie dienen dazu, gemeinsame Eigenschaften verschiedener Klassen
zusammenzufassen, um mit Oberbegriffen arbeiten zu können. Das Gegenstück dazu – also
eine Klasse, von der Objekte erzeugt werden sollen – bezeichnet man als konkrete Klasse.
 Rein virtuelle Funktionen erlauben es, Funktionen zu deklarieren, die in abgeleiteten Klassen
implementiert werden müssen. Sie werden durch eine Zuweisung von 0 definiert.
 Für rein virtuelle Funktionen sind Default-Implementierungen möglich.
 Für die Typabfrage zur Laufzeit und für die Typumwandlung vom Oberbegriff in die tatsächli-
che Klasse sind die Operatoren typeid und dynamic_cast vorgesehen. Ihre Verwendung
sollte in der Regel vermieden werden.
Sandini Bib

5.4 Mehrfachvererbung 333

5.4 Mehrfachvererbung
In diesem Abschnitt wird auf die Möglichkeit eingegangen, dass eine Klasse zwei oder mehr
Basisklassen besitzen kann. Dieser Sachverhalt wird als Mehrfachvererbung (englisch: multiple
inheritance) bezeichnet. Wie die einfache Vererbung ist auch die Mehrfachvererbung durch die
is-a-Beziehung gekennzeichnet. Nur gilt dies dann mehrfach: Ein Objekt einer doppelt abgelei-
teten Klasse ist zum einen dies und zum anderen das.
Die Mehrfachvererbung führt zu einigen Problemen, die in der Praxis berücksichtigt wer-
den müssen (z.B. zu Namenskonflikten). In diesem Abschnitt werden die Probleme erläutert und
Lösungsmöglichkeiten vorgestellt. Generell kann schon hier gesagt werden, dass die Mehrfach-
vererbung nur mit Vorsicht und nach sorgfältigen Design-Überlegungen eingesetzt werden sollte.

5.4.1 Beispiel für Mehrfachvererbung


Ein einleuchtendes einfaches Beispiel für die Mehrfachvererbung ist eine Klasse für Amphibi-
enfahrzeuge, die von den Klassen Auto und Boot abgeleitet wird (siehe Abbildung 5.8). Die
is-a-Beziehung gilt hier mehrfach:
 Ein Amphibienfahrzeug ist ein Auto.
 Ein Amphibienfahrzeug ist ein Boot.

A u t o B o o t

A m p h

Abbildung 5.8: Beispiel für Mehrfachvererbung

Anhand dieses Beispiels werden wir im Folgenden die Probleme betrachten, die speziell bei der
Mehrfachvererbung entstehen können. Dies sind im Wesentlichen Namenskonflikte, da Kompo-
nenten der verschiedenen Basisklassen die gleichen Bezeichnungen besitzen können und somit
keine eindeutige Zuordnung in der abgeleiteten Klasse mehr möglich ist.

Die Basisklassen Auto und Boot


Zunächst werden die Basisklassen betrachtet. Sie werden in diesem Beispiel bewusst recht ein-
fach gehalten.
Ein Auto besteht in diesem Beispiel nur aus einer Komponente für die bereits gefahrenen
Kilometer. Die Anzahl der gefahrenen Kilometer kann beim Anlegen eines Objekts der Klasse
Auto angegeben werden oder sie wird als Default-Wert mit 0 initialisiert. Das Auto kann dann
eine bestimmte Anzahl von Kilometern fahren und die Anzahl der gefahrenen Kilometer kann
ausgegeben werden:
Sandini Bib

334 Kapitel 5: Vererbung und Polymorphie

// vererb/auto1.hpp
#ifndef AUTO_HPP
#define AUTO_HPP

// Headerdatei für I/O einbinden


#include <iostream>

namespace Bsp {

/* Klasse Auto
* - zur Vererbung geeignet
*/
class Auto {
protected:
int km; // gefahrene Kilometer

public:
// Default- und int-Konstruktor
Auto (int d = 0) : km(d) { // gefahrene Kilometer initialisieren
}

// bestimmte Anzahl von Kilometern fahren


virtual void fahre (int d) {
km += d; // Kilometer aufaddieren
}

// Anzahl gefahrener Kilometer ausgeben


virtual void printGefahren () {
std::cout << "Das Auto ist "
<< km << " km gefahren" << std::endl;
}

// virtueller Destruktor (ohne Anweisungen)


virtual ~Auto () {
}
};

} // namespace Bsp

#endif // AUTO_HPP

Für die Klasse Boot gilt im Prinzip genau das Gleiche, nur dass statt Kilometer Seemeilen ver-
waltet werden:
Sandini Bib

5.4 Mehrfachvererbung 335

// vererb/boot1.hpp
#ifndef BOOT_HPP
#define BOOT_HPP

// Headerdatei für I/O einbinden


#include <iostream>

namespace Bsp {

/* Klasse Boot
* - zur Vererbung geeignet
*/
class Boot {
protected:
int sm; // gefahrene Seemeilen

public:
// Default- und int-Konstruktor
Boot (int d = 0) {
sm = d; // gefahrene Seemeilen initialisieren
}

// bestimmte Anzahl von Seemeilen fahren


virtual void fahre (int d) {
sm += d; // Seemeilen aufaddieren
}

// Anzahl gefahrener Seemeilen ausgeben


virtual void printGefahren () {
std::cout << "Das Boot ist "
<< sm << " sm gefahren" << std::endl;
}

// virtueller Destruktor (ohne Anweisungen)


virtual ~Boot () {
}
};

} // namespace Bsp

#endif // BOOT_HPP
Sandini Bib

336 Kapitel 5: Vererbung und Polymorphie

Deklaration der abgeleiteten Klasse Amph


Die Klasse für Amphibienfahrzeuge, Amph, wird von den Klassen Auto und Boot wie folgt
abgeleitet:
// vererb/amph1.hpp
#ifndef AMPH_HPP
#define AMPH_HPP

// Headerdateien der Basisklassen einbinden


#include "auto.hpp"
#include "boot.hpp"

namespace Bsp {

/* Klasse Amph
* - abgeleitet von Auto und Boot
* - zur Weiter-Vererbung geeignet
*/
class Amph : public Auto, public Boot {
public:
/* Default-, int und int/int-Konstruktor
* - mit erstem Parameter wird Auto-Konstruktor aufgerufen
* - mit zweitem Parameter wird Boot-Konstruktor aufgerufen
*/
Amph (int k = 0, int s = 0) : Auto(k), Boot(s) {
// damit ist nichts mehr zu tun
}

// Anzahl gefahrener Kilometer und Seemeilen ausgeben


virtual void printGefahren () {
std::cout << "Das Amphibienfahrzeug ist " << km << " km und "
<< sm << " sm gefahren" << std::endl;
}

// virtueller Destruktor (ohne Anweisungen)


virtual ~Amph () {
}
};

} // namespace Bsp

#endif // AMPH_HPP
Sandini Bib

5.4 Mehrfachvererbung 337

Wie man sieht, werden alle Basisklassen, durch einen Doppelpunkt getrennt, hinter dem Namen
der Klasse angegeben:
class Amph : public Auto, public Boot {
...
};
Dabei kann für jede Klasse einzeln angegeben werden, inwiefern der Zugriff auf die geerbten
Komponenten eingeschränkt wird (siehe Abschnitt 5.1.3).

Anwendung der abgeleiteten Klasse Amph


Da die Klasse Amph die zwei Basisklassen Auto und Boot besitzt, werden sowohl die Eigenschaf-
ten der Klasse Auto als auch die Eigenschaften der Klasse Boot geerbt. Ein Amphibienfahrzeug
besitzt somit die Komponenten km und sm. Diese können z.B. über die neu implementierte Ele-
mentfunktion printGefahren() ausgegeben werden, wie das folgende Programm zeigt:
// vererb/amphtest1.cpp
// Headerdatei für die Klasse Amph
#include "amph.hpp"

int main ()
{
/ * Amphibienfahrzeug anlegen und mit
* 7 Kilometer und 42 Seemeilen initialisieren
*/
Bsp::Amph a(7,42);

// gefahrene Kilometer und Seemeilen ausgeben


a.printGefahren();
}
Die Deklaration
Bsp::Amph a(7,42);
legt ein Objekt der Klasse Amph an.
Die Konstruktoren der Basisklassen werden dabei in der Reihenfolge aufgerufen, in der die
Basisklassen deklariert werden. Die Reihenfolge in der Initialisierungsliste ist unerheblich. Diese
Tatsache sollte allerdings im Allgemeinen keine Rolle spielen. Eine Reihenfolge wird nur des-
halb definiert, um sicherzustellen, dass die Destruktoren in umgekehrter Reihenfolge aufgerufen
werden, was z.B. für die Implementierung einer eigenen Speicherverwaltung wichtig sein kann.
Sandini Bib

338 Kapitel 5: Vererbung und Polymorphie

Konkret passiert Folgendes:


 Zunächst wird der Speicherplatz für das Objekt angelegt:

a: km: ?

sm: ?

 Dann wird der int-Konstruktor der Klasse Auto aufgerufen, der den von Auto geerbten Teil
des Objekts initialisiert:

a: km: 7

sm: ?

 Anschließend wird der int-Konstruktor der Klasse Boot aufgerufen, der den von Boot ge-
erbten Teil des Objekts initialisiert:

a: km: 7

sm: 42

 Da der Konstruktor der Klasse Amph ohne Anweisungen ist (es sind ja auch keine neuen
Komponenten hinzugekommen), ist das Objekt damit vollständig initialisiert.
Wenn sowohl für Basisklassen als auch für Komponenten Konstruktoren aufgerufen werden
müssen, werden zuerst die Konstruktoren der Basisklassen und dann die Konstruktoren für die
neuen Komponenten aufgerufen.
Die Anweisung
a.printGefahren();
ruft die Elementfunktion printGefahren() auf, die in der Klasse Amph neu implementiert wird
und gefahrene Kilometer und Seemeilen ausgibt.

Zuordnungskonflikte
Ein Aufruf von fahre() ist für Amphibienfahrzeuge dagegen nicht möglich. Diese Element-
funktion wird sowohl von der Klasse Auto als auch von der Klasse Boot geerbt und kann somit
nicht eindeutig zugeordnet werden:
Sandini Bib

5.4 Mehrfachvererbung 339

Bsp::Amph a(7,42);
...
a.fahre(77); // FEHLER: mehrdeutig
Auch hier kann wieder der Bereichsoperator eine eindeutige Zuordnung ermöglichen:
Bsp::Amph a(7,42);
...
a.Auto::fahre(77); // OK: fahre als Auto
a.Boot::fahre(23); // OK: fahre als Boot
Um die Verwendung des Bereichsoperators zu vermeiden, könnte die abgeleitete Klasse die
Funktionen auch unter einem anderen Namen anbieten:
class Amph : public Auto, public Boot {
...
virtual void fahreAlsAuto (int d) {
// Umbenennung von Auto::fahre()
Auto::fahre(d);
}
virtual void fahreAlsBoot (int d) {
// Umbenennung von Boot::fahre()
Boot::fahre(d);
}
};
Natürlich ist es auch möglich, die Funktionen direkt zu implementieren. Dies kann aber bei
Änderungen an der Implementierung der Basisklasse zu Inkonsistenzen führen. Hier muss, wie
so oft, zwischen einem Konsistenzvorteil und einem Laufzeitnachteil abgewogen werden.
Wird das Amphibienfahrzeug als Auto oder Boot betrachtet, dann ist die Zuordnung von
fahre() allerdings klar und der Aufruf somit ohne Qualifizierung möglich:
Bsp::Amph a(7,42);
...
Bsp::Boot& b(a); // Amphibienfahrzeug wird als Boot betrachtet
b.fahre(23); // OK: fahre als Boot

5.4.2 Virtuelle Basisklassen


Wenn eine Klasse mehrere Basisklassen besitzt, können diese wiederum direkt oder indirekt von
der gleichen Basisklasse abgeleitet sein. In diesem Fall stellt sich die Frage, ob die mehrfach
geerbten Komponenten der Basisklasse einmal oder mehrfach vorhanden sind.
Nehmen wir z.B. an, die Klassen Auto und Boot sind beide von der Klasse Fahrzeug abge-
leitet (siehe Abbildung 5.9). Wenn nun die Klasse Fahrzeug z.B. eine Komponente baujahr de-
finiert, dann besitzt sowohl ein Auto als auch ein Boot ein Baujahr. Ein Amphibienfahrzeug sollte
nun aber nicht, nur weil es alle Eigenschaften von Auto und alle Eigenschaften von Boot erbt,
Sandini Bib

340 Kapitel 5: Vererbung und Polymorphie

F a h r z e u g

A u t o B o o t

A m p h

Abbildung 5.9: Ableiten einer Basisklasse über mehrere Wege

zwei Baujahre besitzen. Wenn wir allerdings andererseits annehmen, dass die Klasse Fahrzeug
eine Komponente maxspeed für die Höchstgeschwindigkeit definiert, dann kann es sinnvoll sein,
diese Komponente beim Amphibienfahrzeug zweimal, einmal als Auto und einmal als Boot, zu
besitzen.
C++ bietet hier beide Möglichkeiten: mehrfach geerbte Komponenten können gemeinsam
verwendet oder getrennt geerbt werden.

Nichtvirtuelle Basisklassen
Ohne spezielle Angabe sind über verschiedene Klassen geerbte Komponenten aus gemeinsamen
Basisklassen in C++ mehrfach vorhanden. Bei der folgenden Definition besitzt ein Amphibien-
fahrzeug deshalb zweimal die Komponente maxspeed:
class Fahrzeug {
protected:
int maxspeed; // Höchstgeschwindigkeit
...
};

class Auto : public Fahrzeug {


...
};

class Boot : public Fahrzeug {


...
};

class Amph : public Auto, public Boot {


...
};
Sandini Bib

5.4 Mehrfachvererbung 341

Da maxspeed sowohl von Auto als auch von Boot geerbt wird und somit zweimal vorhanden
ist, kann darauf für Amphibienfahrzeuge nur über den Bereichsoperator zugegriffen werden:
void Amph::f()
{
maxspeed = 100; // FEHLER: mehrdeutig
Auto::maxspeed = 100; // OK: maxspeed von Auto
Boot::maxspeed = 70; // OK: maxspeed von Boot
Fahrzeug::maxspeed = 100; // FEHLER: mehrdeutig
}

Virtuelle Basisklassen
Um zu erreichen, dass eine Komponente, die über verschiedene Klassen von einer Basisklasse
geerbt wird, nicht mehrfach vorhanden ist, müssen die direkt abgeleiteten Klassen der gemein-
samen Basisklasse als virtuelle Basisklassen definiert werden. Dies geschieht, indem bei der
Angabe der Basisklasse zusätzlich das Schlüsselwort virtual verwendet wird:
class Fahrzeug {
protected:
int baujahr; // Baujahr
...
};

class Auto : virtual public Fahrzeug {


...
};

class Boot : virtual public Fahrzeug {


...
};

class Amph : public Auto, public Boot {


...
};
Durch die Angabe von virtual für die Basisklasse Fahrzeug sind deren Komponenten nur
einmal vorhanden, wenn die abgeleiteten Klassen wieder zusammenkommen. Dabei ist die Rei-
henfolge, ob virtual vor oder hinter dem Zugriffsschlüsselwort angegeben wird, unerheblich.
Da baujahr sowohl von Auto als auch von Boot virtuell geerbt wird, ist die Komponente
in Amphibienfahrzeugen nur einmal vorhanden. Ein Zugriff ohne Bereichsoperator ist somit
eindeutig und deshalb möglich:
void Amph::f()
{
baujahr = 1983; // OK
Sandini Bib

342 Kapitel 5: Vererbung und Polymorphie

Auto::baujahr = 1983; // OK
Boot::baujahr = 1983; // OK
Fahrzeug::baujahr = 1983; // OK
}
Entscheidend für die gemeinsame Verwendung der Komponenten einer Klasse ist, dass die direkt
abgeleiteten Klassen jeweils die Basisklasse virtuell deklarieren. Das Schlüsselwort virtual
muss also, wie hier geschehen, bei Auto und Boot angegeben werden.

Virtuelle und nichtvirtuelle Basisklassen


Was aber ist, wenn bei Amphibienfahrzeugen baujahr einmal, maxspeed aber mehrfach vor-
handen sein soll? Hier benötigt man eine Hilfsklasse, um die Komponenten, die gemeinsam
verwendet werden sollen, von den mehrfach vorhandenen zu trennen.
Das könnte in unserem Beispiel wie in Abbildung 5.10 dargestellt aussehen. Da alle von
FahrzeugVirtual direkt abgeleiteten Klassen (hier nur die Klasse FahrzeugNonVirtual) vir-
tuell abgeleitet werden, sind dessen Komponenten nur einmal vorhanden. Da die von Fahrzeug-
NonVirtual direkt abgeleiteten Klassen nicht virtuell sind, gibt es deren Komponenten mehr-
fach.

F a h r z e u g V i r t u a l

b a u j a h r

{ v i r t u a l }

F a h r z e u g N o n V i r t u a l

m a x s p e e d

A u t o B o o t

A m p h

Abbildung 5.10: Klasse Fahrzeug als virtuelle und nichtvirtuelle Basisklasse


Sandini Bib

5.4 Mehrfachvererbung 343

5.4.3 Das Problem der Identität


Bisher wurde immer davon ausgegangen, dass Objekte identisch sind, wenn ihre Adressen gleich
sind. Aufgrund dessen wurde bei der Implementierung eines eigenen Zuweisungsoperators eine
Zuweisung an sich selbst durch einen Vergleich der Adressen festgestellt (siehe Abschnitt 6.1.5):
// Zuweisung an sich selbst ?
if (this == &obj) {
return *this; // Objekt unverändert zurückliefern
}
Dies geht bei der Mehrfachvererbung nicht mehr, denn bei der Mehrfachvererbung können Ob-
jekte identisch sein, obwohl ihre Adressen nicht gleich sind. Dies liegt an der Art und Weise, wie
Objekte zur Laufzeit in Programmen typischerweise verwaltet werden.
Der Aufbau eines Objekts der Klasse Amph setzt sich im Allgemeinen aus den Komponen-
ten der daran beteiligten Klassen zusammen. Typischerweise werden die Komponenten in der
Reihenfolge ihrer Deklaration verwaltet (siehe Abbildung 5.11). Ein Objekt einer abgeleiteten
Klasse kann aber grundsätzlich als Objekt seiner Basisklasse verwendet werden. In einem sol-
chen Fall wird das Objekt auf die Komponenten reduziert, die in der Basisklasse bereits bekannt
sind.

Amph: Anteil von Fahrzeug

Anteil von Auto

Anteil von Boot

Anteil von Amph

Abbildung 5.11: Typischer Aufbau eines Objekts der Klasse Amph

Bei der einfachen Vererbung werden die Komponenten der abgeleiteten Klasse, die am Ende
des Objekts liegen, einfach nicht mehr berücksichtigt. Die Adresse des Objekts bleibt aber die
gleiche. Bei Mehrfachvererbung geschieht dies auch, solange nur hinten liegende Objektteile
wegfallen. Dies passiert beim Amphibienfahrzeug z.B. typischerweise dann, wenn es als Auto
betrachtet wird (siehe Abbildung 5.12).
Wenn das Amphibienfahrzeug aber als Boot betrachtet wird, funktioniert das Weglassen hin-
terer Teile nicht mehr, da der Objektteil von Auto zwischen Fahrzeug und Boot liegt.
Aus diesem Grund wird dann typischerweise ein Schattenobjekt“ erzeugt, das den für Boote

richtigen Aufbau hat. Die Objektteile darin sind aber eigentlich nur interne Verweise auf das Ori-
ginalobjekt (siehe Abbildung 5.13). Entscheidend ist, dass das Objekt damit eine zweite Adresse
erhält, unter der es dann angesprochen wird.
Sandini Bib

344 Kapitel 5: Vererbung und Polymorphie

Anteil von Fahrzeug Anteil von Fahrzeug

Anteil von Auto

Anteil von Boot


=> Anteil von Auto

als Auto
Anteil von Amph

Abbildung 5.12: Verwendung von Amph als Auto

Anteil von Fahrzeug Anteil von Fahrzeug

Anteil von Auto


=> Anteil von Boot

Anteil von Boot


als Boot
Anteil von Amph

Abbildung 5.13: Verwendung von Amph als Boot

Selbst wenn dann die Betrachtung als Auto und die Betrachtung als Boot jeweils als Fahrzeug
verwendet werden, bleiben die Adressen verschieden. Der Objektteil Fahrzeug wird dann ein-
mal vom Original- und einmal vom Schattenobjekt verwendet. Trotz gleichen Typs kann dasselbe
Objekt also eine unterschiedliche Adresse besitzen.
Zum Testen dieses Sachverhalts kann folgendes Beispiel verwendet werden:
// vererb/ident1.cpp
void fFahrzeug (const Bsp::Fahrzeug& a)
{
std::cout << " als Fahrzeug: "
<< static_cast<const void*>(&a) << std::endl;
}

void fAuto (const Bsp::Auto& a)


{
std::cout << "&a als Auto: "
<< static_cast<const void*>(&a) << std::endl;
fFahrzeug(a);
}
Sandini Bib

5.4 Mehrfachvererbung 345

void fBoot (const Bsp::Boot& a)


{
std::cout << "&a als Boot: "
<< static_cast<const void*>(&a) << std::endl;
fFahrzeug(a);
}

int main ()
{
Bsp::Amph a;

fAuto(a);
fBoot(a);
}
Die mit Hilfe von f1() über zwei verschiedene Wege aufgerufene Funktion fFahrzeug() wird
für das Objekt a mit ziemlicher Sicherheit zwei unterschiedliche Adressen ausgeben.
Statt mit Referenzen können Sie den Test auch mit Zeigern machen:
// vererb/ident2.cpp
int main ()
{
using std::cout;
using std::endl;

Bsp::Amph a;

// Adresse von a
cout << "&a: " << (void*)&a << "\n" << endl;

// Adresse von a => als Auto, als Boot


cout << "(Bsp::Auto*) &a: " << (void*)(Bsp::Auto*)&a << "\n";
cout << "(Bsp::Boot*) &a: " << (void*)(Bsp::Boot*)&a << "\n\n";

// Adresse von a => als Auto => als Fahrzeug


cout << "(Bsp::Fahrzeug*) (Bsp::Auto*) &a: "
<< (void*)(Bsp::Fahrzeug*)(Bsp::Auto*)&a << endl;

// Adresse von a => als Boot => als Fahrzeug


cout << "(Bsp::Fahrzeug*) (Bsp::Boot*) &a: "
<< (void*)(Bsp::Fahrzeug*)(Bsp::Boot*)&a << '\n' << endl;
}
Sandini Bib

346 Kapitel 5: Vererbung und Polymorphie

Es ist noch nicht einmal sichergestellt, dass nur zwei verschiedene Adressen ausgegeben werden.
Die Tatsache, dass ein Objekt immer die gleiche Adresse besitzt, ist keine Sprachspezifikation
von C++, sondern beruht darauf, dass Compiler Objekte im Allgemeinen soweit es geht an der
gleichen Adresse verwalten.
Um identische Objekte mit Sicherheit zu erkennen, gibt es nur eine Möglichkeit: Man muss
entsprechende Mechanismen selbst implementieren. Jedes Objekt muss als Komponente eine
eindeutige ID und eine Elementfunktion zum Vergleichen dieser IDs besitzen. Abschnitt 6.5.1
zeigt, wie das aussehen kann.

5.4.4 Dieselbe Basisklasse mehrfach ableiten


Es ist nicht möglich, ein und dieselbe Klasse mehrfach direkt abzuleiten (siehe Abbildung 5.14).
Ansonsten würden unlösbare Namenskonflikte entstehen. Bei Initialisierungslisten kann dann
z.B. keine eindeutige Zuordnung mehr getroffen werden, welche Basisklasse gemeint ist.

Abbildung 5.14: Die gleiche Basisklasse direkt mehrfach ableiten

Dieser Sachverhalt kann aber unter Umständen sinnvoll eingesetzt werden. Ich habe allerdings
noch kein schlüssiges Beispiel dafür gesehen. Es würde bedeuten, dass ein Objekt vom Typ B
einerseits ein Objekt vom Typ A, andererseits aber ein anderes Objekt vom Typ A ist.
Wer in der Praxis vor einem solchen Vererbungsproblem steht, sollte deshalb zunächst einmal
prüfen, ob sein Design in Ordnung ist und er nicht meint, dass ein Objekt vom Type B aus zwei
Objekten vom Typ A besteht und also zwei solche Objekte als Komponenten besitzt.
Falls es aber doch sinnvoll ist, ein und dieselbe Basisklasse mehrfach direkt abzuleiten, soll-
ten Sie zwei Dinge tun:
 Mir schreiben, damit ich Ihr Beispiel in der nächsten Auflage als sinnvolles Beispiel verwen-
den kann,7 und
 Dummy-Klassen einführen, da bei gleichnamigen Basisklassen keine eindeutige Zuordnung
durch den Klassennamen möglich ist (siehe Abbildung 5.15).

7
Das einzige halbwegs sinnvolle Beispiel, das mir einfällt, ist eine von der Klasse Person zweimal abge-
leitete Klasse GespaltenePersoenlichkeit.
Sandini Bib

5.4 Mehrfachvererbung 347

A 1 A 2

Abbildung 5.15: Die gleiche Basisklasse indirekt mehrfach ableiten

5.4.5 Zusammenfassung
 C++ erlaubt die Mehrfachvererbung. Abgeleitete Klassen können mehrere Basisklassen be-
sitzen.
 Entstehen dadurch Namenskonflikte für den Zugriff auf Komponenten, kann nur mit dem
Bereichsoperator darauf zugegriffen werden.
 Basisklassen können über Mehrfachvererbung mehrfach verwendet werden. Durch virtuelle
Basisklassen sind die darin definierten Komponenten in den Objekten dann trotzdem nur
einmal vorhanden.
 Insbesondere bei der Mehrfachvererbung kann ein und dasselbe Objekt verschiedene Adres-
sen besitzen. Dies ist sogar möglich, wenn es unter dem gleichen Typ betrachtet wird. Um
die Identität eines Objekts immer zweifelsfrei sicherstellen zu können, müssen deshalb ent-
sprechende Komponenten eingeführt werden.
Sandini Bib

348 Kapitel 5: Vererbung und Polymorphie

5.5 Design-Fallen bei der Vererbung


Der Aufbau einer Klassenhierarchie ist eine wesentliche Design-Entscheidung bei der Software-
entwicklung, die später nur schwer wieder korrigiert werden kann. Umso wichtiger ist es, dass
dabei keine Fehler gemacht werden. Dieser Abschnitt stellt verschiedene Fehlerquellen vor, die
beim konzeptionellen Einsatz von Vererbung existieren, so dass sie erkannt und korrigiert werden
können.

5.5.1 Vererbung kontra Verwendung


In objektorientierten Sprachen gibt es zwei Möglichkeiten zu abstrahieren: Vererbung und Ver-
wendung. Für die dahinter steckende Beziehung gibt es verschiedene Bezeichnungen:

Verwendung Vererbung
Beziehung has_a is_a
besteht_aus ist_ein
part_of kind_of
containment inheritance
Aggregation Generalisierung
Sprachmittel Komponente, Verweis abgeleitete Klasse

Betrachten wir das am Beispiel eines Autos:


 Ein Auto ist ein Fahrzeug
 Ein Auto besteht aus Motor, Karosserie, der Möglichkeit zu fahren, ...
Beim Auto ist das sicher unstrittig, doch das ist nicht immer der Fall. Im Folgenden werden ver-
schiedene Beispiele vorgestellt, bei denen nicht immer klar ist, ob Vererbung oder Verwendung
angemessen ist.

5.5.2 Design-Fehler: Einschränkende Vererbung


Wie lautet Ihre Antwort, wenn ich Sie frage: In welcher Beziehung stehen die Klassen Rechteck
und Quadrat zueinander?
Falls Sie annehmen, es handelt sich um eine Vererbungsbeziehung, liegen Sie falsch. Die
Vererbung ist zwar durch die is-a-Beziehung gekennzeichnet, die deutsche Sprache verwendet
diese Beziehung aber auch in einem anderen Sinne.
Was mit der is-a-Beziehung semantisch nämlich ausgedrückt wird, ist die Tatsache, dass alles,
was mit einem Objekt einer Basisklasse gemacht werden kann, auch für ein Objekt der abgelei-
teten Klasse möglich und sinnvoll ist. Wenn ich nun im objektorientierten Sinne behaupte, ein

Quadrat ist ein Rechteck“, sage ich damit, dass ich mit einem Quadrat alles machen kann, was ich
auch mit einem Rechteck machen kann. Doch das stimmt nicht! Ich kann nämlich die Breite und
Höhe eines Rechtecks unterschiedlich verändern. Bei einem Quadrat ist das aber nicht möglich.
Das Quadrat wäre danach kein Quadrat mehr.
Sandini Bib

5.5 Design-Fallen bei der Vererbung 349

Der Design-Fehler ist der, dass an die geerbten Komponenten eine spezialisierende Bedingung
geknüpft wird, nämlich, dass Breite und Höhe, durch die ein Rechteck gekennzeichnet ist, immer
gleich sein müssen.
Eine Regel zur Vererbung lautet also:
 Alle Zustände, die ein Objekt einer Basisklasse annehmen kann, muss auch ein Objekt der
abgeleiteten Klasse annehmen können.
Dummerweise wird die Vererbungsbeziehung häufig auch als Spezialisierung bezeichnet. Dies
ist aber nicht im einschränkenden Sinne, sondern im konkretisierenden Sinne gemeint. Ich be-
zeichne die Vererbungsbeziehung deshalb lieber als Konkretisierung.

Praktischer Umgang mit einschränkender Vererbung


Für den praktischen Umgang mit einschränkender Vererbung gibt es drei Möglichkeiten:
 Mögliche Auswirkungen ignorieren
Obwohl es als problematisch bekannt ist, kann man eine Klasse Quadrat von einem Rechteck
ableiten und nach dem Motto Wer versucht, die Breite und Höhe von Quadraten unterschied-

lich zu verändern, ist selbst schuld.“ dem Anwendungsprogrammierer den schwarzen Peter
überlassen.
Diese Art von Design ist sicherlich ganz schlecht, da der Fehler auftreten kann und dann
noch nicht einmal festgestellt wird, sondern zunächst nur zu einer Inkonsistenz führt.
 Mögliche Auswirkungen im Einzelfall beheben
Eine andere Möglichkeit besteht darin, ebenfalls Quadrat von Rechteck abzuleiten, die
Funktionen, die zu Inkonsistenzen führen können, in der Klasse Quadrat aber neu zu imple-
mentieren. Diese neuen Implementierungen könnten
– eine Fehlermeldung ausgeben,
– den Aufruf ignorieren (gar nichts tun),
– versuchen, den Aufruf auf den speziellen Fall hin zu interpretieren (z.B. das Quadrat mit
dem Mittelwert aus dem Faktor für die Breite und Höhe skalieren).
Diese Möglichkeit ist zwar nicht ganz so schlecht wie die erste, da der Fehler wenigstens
nicht zu Inkonsistenzen führt. Sie ist aber nichtsdestotrotz immer noch schlecht, da der Fehler
immer noch auftreten kann.
An dieser Stelle muss immer die Polymorphie beachtet werden. Man kann dann ein Qua-
drat zeitweise als Rechteck verwenden oder sogar in einem Feld (Array) von verschiedenen
Rechtecken verwalten. Wenn es für Rechtecke die Möglichkeit gibt, die Breite und Höhe un-
terschiedlich zu skalieren, dann sollte das für jedes Objekt gelten, das als Rechteck verwen-
det wird. Ansonsten müsste der Anwendungsprogrammierer sich Gedanken darüber machen,
welche Spezialfälle durch ungeschickte Vererbung auftreten können. Man beachte schließ-
lich, dass Klassen auch abgeleitet werden können, wenn sie bereits an anderer Stelle Verwen-
dung finden.
 Potentielle Fehlerquelle durch besseres Design beseitigen
Der richtige Umgang mit spezialisierender Vererbung besteht darin, ein mögliches Auftreten
des Fehlers erst gar nicht zuzulassen, indem der Vererbungsfehler erst gar nicht gemacht wird.
Sandini Bib

350 Kapitel 5: Vererbung und Polymorphie

Da ein Quadrat also kein Rechteck und ein Rechteck ohnehin kein Quadrat ist, gibt es über-
haupt keine Vererbungsbeziehung. Wer die zweifellos vorhandenen gemeinsamen Eigen-
schaften aber nur einmal implementieren will, kann eine gemeinsame abstrakte Basisklasse
wie Viereck implementieren.

5.5.3 Design-Fehler: Wertverändernde Vererbung


Wie lautet Ihre Antwort, wenn ich Sie frage: In welcher Beziehung stehen die Klassen Bruch
und BruchMitGanzerZahl (wie 3 41 ) zueinander?
Sie sind nun vorsichtig geworden und antworten sicherheitshalber, dass Sie es nicht wissen,
was keine schlechte Antwort ist, da die Antwort ohnehin nicht ganz eindeutig ist.
Die Implementierung könnte im Prinzip so aussehen:
class Bruch {
private:
int zaehler;
int nenner;
...
};

class BruchMitGanzerZahl : public Bruch {


private:
int zahl;
...
};
Der entscheidende Punkt ist, dass eine neue Komponente hinzukommt, die den bisherigen Kom-
ponenten eine neue Bedeutung gibt. Wenn das Objekt mit 13
4 initialisiert wird, sind die Werte von
zaehler und nenner bei Bruch und BruchMitGanzerZahl verschieden. Der Bruch 13 4 wäre
als BruchMitGanzerZahl 3 41 (siehe Abbildung 5.16).

Bruch: BruchMitGanzerZahl:

zaehler: 13 zaehler: 1
nenner: 4 nenner: 4
zahl: 3

Abbildung 5.16: Bruch versus BruchMitGanzerZahl

Nun muss beachtet werden, dass die Vererbungsbeziehung auch bedeutet, dass ein abgeleitetes
Objekt jederzeit als Objekt der Basisklasse verwendet werden kann. In diesem Fall reduziert sich
Sandini Bib

5.5 Design-Fallen bei der Vererbung 351

das Objekt auf die Komponenten, die in der Basisklasse schon existieren. Also würde 3 41 als 14
verwendet, was zu entsprechend unsinnigen Ergebnissen führen kann. So würde letztlich jede
Zuweisung an einen Bruch aus dem initialisierten Wert 13 1
4 den Wert 4 machen.
Der Design-Fehler besteht darin, dass die geerbten Komponenten um neue Komponenten
ergänzt werden, durch die sie für den gleichen Informationsgehalt neue Werte erhalten.
Eine weitere Regel zur Vererbung lautet also:
 Der Zustand der Komponenten, die ein Objekt einer Basisklasse für einen bestimmten Wert
annimmt, darf durch neue Komponenten einer abgeleiteten Klasse nicht verändert werden.
Auch in diesem Fall sollte nicht abgeleitet werden, um ein mögliches Auftreten des Fehlers gar
nicht erst zu ermöglichen.
Es handelt sich hier nämlich eindeutig um eine has-a-Beziehung. Schon der Name der Klasse,
BruchMitGanzerZahl, drückt ja bereits aus, dass zwei Objekte, eine Bruch und eine ganze Zahl,
als gemeinsames Objekt betrachtet werden. Die Deklaration der Klasse sähe also besser so aus:
class BruchMitGanzerZahl {
private:
Bruch bruch;
int zahl;
...
};
In diesem Beispiel gibt es allerdings noch eine andere Möglichkeit der Implementierung. Es
kann nämlich geerbt werden, ohne dass die Werte der geerbten Komponenten für den gleichen
Informationsgehalt geändert werden.
Dazu müsste die Implementierung dahingehend geändert werden, dass der Wert 13 4 auch bei
der Klasse BruchMitGanzerZahl dazu führt, dass die Komponente zaehler mit 13 und die
Komponente nenner mit 4 initialisiert wird. Der einzige Unterschied würde darin bestehen, dass
ein Bruch als Bruch mit ganzer Zahl ausgegeben wird. Es würde also nur die Ausgabeoperation
umgeschrieben werden.

5.5.4 Design-Fehler: Wertinterpretierende Vererbung


Nehmen wir an, die Klasse Bruch wäre nur für positive Brüche definiert:
class Bruch {
private:
unsigned int zaehler;
unsigned int nenner;
...
};
In diesem Fall wäre eine Ableitung durch die Klasse BruchMitVorzeichen falsch:
class BruchMitVorzeichen : public Bruch {
private:
bool istNegativ;
...
};
Sandini Bib

352 Kapitel 5: Vererbung und Polymorphie

Der Informationsgehalt eines Bruchs wird hier nämlich um eine Information ergänzt, die die
Bedeutung der Wertbelegung ändern kann, was ohne diese Komponente nicht möglich wäre.
Dies liegt daran, dass der potenzielle Wertebereich damit verdoppelt wird (alle Bruch-Werte
positiv und negativ).
Wenn nun aber ein negativer Bruch einem Bruch zugewiesen wird, wird aus 13 4 plötzlich
13 , was zumindest sicherlich problematisch ist. Ich hör schon die Leser, die sagen, dass dies doch
4
ein nettes Feature“ wäre. Die Frage ist nur, ob ein derartiges Verhalten so intuitiv ist, dass es

ohne explizite Veranlassung stattfinden darf. Es kann schon kritisch werden, wenn Sie in Ihrem
Programm aus Versehen Absicht aus 12 Millionen Euro Schulden ein Guthaben von 12 Millionen
Euro machen.
Der Design-Fehler besteht darin, dass die geerbten Komponenten um neue Komponenten
ergänzt werden, durch die ihre Werte unterschiedliche Bedeutungen erhalten.
Eine weitere Regel zur Vererbung lautet also:
 Die Wertebelegung, die ein Objekt einer Basisklasse für einen bestimmten Informationsge-
halt annehmen kann, darf durch neue Komponenten einer abgeleiteten Klasse keine neue
Bedeutung erhalten.
Dieses Design-Problem ist auch daran zu erkennen, dass nicht für jeden Wert eines Objekts einer
abgeleiteten Klasse eine Zuweisung an ein Objekt einer Basisklasse sinnvoll ist, womit wie diese
Art der Vererbung auch wieder als einschränkende Vererbung entlarvt haben.
Auch hier ist die einzig sinnvolle Lösung die Implementierung als eigenständige Klasse mit
Bruch und Vorzeichen als Komponenten:
class BruchMitVorzeichen {
private:
Bruch bruch;
bool istNegativ;
...
};
Das Problem kann auch schon durch eine bessere Namensgebung erkannt werden: Wenn die
Klasse Bruch in diesem Fall BruchOhneVorzeichen genannt wird, ist bereits offensichtlich,
dass ein BruchMitVorzeichen kein BruchOhneVorzeichen ist.

5.5.5 Vermeide Vererbung!“



Insgesamt zeigt sich bei Einsteigern in die objektorientierte Programmierung ein Hang, das neue
Sprachmittel Vererbung zu oft einzusetzen. Dies ist immer problematisch, denn eine Vererbung
stellt eine engere Bindung dar als eine Verwendung. Werden Klassen nur verwendet, passiert
nichts solange sich die öffentliche Schnittstelle nicht ändert. Da abgeleitete Klassen auch Zugriff
auf protected Komponenten haben, sind Änderungen in Basisklassen kritischer und können
leichter zu Inkonsistenzen führen. Insbesondere ein Ableiten von Klassen, die nicht unter der
eigenen Kontrolle liegen, ist immer kritisch.
Sandini Bib

5.5 Design-Fallen bei der Vererbung 353

Ich neige deshalb zu der Empfehlung: Vermeide Vererbung!“. Das bedeutet, dass man Verer-

bung nur dann einsetzt, wenn man einen Sachverhalt nicht anders sinnvoll implementieren kann.
Zuerst sollte man sich immer Fragen, ob nicht eigentlich eine Verwendungsbeziehung vorliegt.
Wenn man dann ableitet, sollte man die Semantik von Operationen und Komponenten nie
einschränken oder verändern. Objekte abgeleiteter Klassen sollten ohne Einschränkung jeder-
zeit als Objekte der Basisklasse verwendet werden können. Dieses Prinzip ist auch als Liskov
Substitution Principle bekannt.8
Manchmal mag es als Implementierungsdetail sinnvoll sein, eine Verwendungsbeziehung
durch private Vererbung (siehe Abschnitt 5.2.8) zu implementieren. Dies Art der Vererbung wird
mitunter auch Implementierungsvererbung genannt und verdeutlicht den Sachverhalt: Es geht
nicht um eine konzeptionelle Vererbung im Sinne der is-a-Beziehung, sondern um eine einfache-
re Implementierung eines Sachverhalts durch implizite Wiederverwendung von Code.
Auch dies sollte man vermeiden. Auf lange Sicht lohnt sich robustes Design mit möglichst
wenig Abhängigkeiten immer.

5.5.6 Zusammenfassung
 Nicht jede im Sprachgebrauch vorhandene is-a-Beziehung ist ein Kennzeichen für die Verer-
bung. Insbesondere ist (öffentliche) Vererbung nicht korrekt,
– wenn nicht mehr alle Operationen möglich sind,
– wenn Operationen nicht mehr die gleiche semantische Bedeutung haben,
– wenn die Bedeutung von Komponenten der Basisklasse durch die Vererbung verändert
wird oder
– wenn die Eigenschaften von Komponenten der Basisklasse eingeschränkt werden.
 Für alle möglichen Objekte einer abgeleiteten Klasse muss es sinnvoll sein, sie einem Ob-
jekt einer Basisklasse zuzuweisen, wobei die neuen Komponenten einfach wegfallen bzw.
ignoriert werden.
 Vermeide Vererbung.

8
Nach Barbara Liskov, die dieses Prinzip zum ersten Mal 1988 formal vorstellte.
Sandini Bib
Sandini Bib

Kapitel 6
Dynamische und statische
Komponenten

In den bisherigen Kapiteln wurden Klassen vorgestellt, die einfache“ Komponenten besaßen.

Damit ist gemeint, dass es sich um Komponenten wie int, string oder vector handelt, die
man problemlos kopieren und zuweisen kann. Dies ist jedoch nicht bei allen Datentypen der Fall.
Besonders, wenn man Zeiger als Komponenten verwendet, kann man diese Komponenten nicht
einfach kopieren oder einander zuweisen. Aus diesem Grund muss man sich in die als Default
vorhandenen Operationen, wie das Kopieren, Zuweisen und auch in das Aufräumen einmischen.
Dieses Kapitel behandelt dieses Thema unter dem Stichwort dynamische Komponenten“.

Außerdem werden in diesem Kapitel statische Komponenten erläutert. Dabei handelt es sich
um Komponenten, die nur einmal im Programm enthalten sind und von allen Objekten/Instanzen
gemeinsam verwendet werden.
Als Beispielklassen für dieses Kapitel dienen eine eigene Implementierung einer String-
Klasse und eine einfache Klasse zur Personenverwaltung.

355
Sandini Bib

356 Kapitel 6: Dynamische und statische Komponenten

6.1 Dynamische Komponenten


Dieser Abschnitt stellt die Verwaltung und Anwendung von Klassen mit dynamischen Kompo-
nenten vor. Anhand einer eigenen einfachen Implementierung einer Klasse für Strings werden die
Besonderheiten vorgestellt, die bei der Verwendung dynamischer Komponenten beachtet werden
müssen.

6.1.1 Implementierung der Klasse String


Im Folgenden wird eine Implementierung der Klasse Bsp::String vorgestellt, mit der einfache
Operationen analog zum Standarddatentyp std::string möglich sind. Somit könnte in einfa-
chen Anwendungsfällen mit Hilfe einer einfachen Typdefinition von der Standardklasse auf diese
eigene Klasse gewechselt werden.
Der Implementierungsansatz setzt alle Operationen dieser String-Klasse um auf von C über-
nommene Funktionen für Strings und Felder (Arrays) zurück (diese werden zum Teil in Ta-
belle 3.12 auf Seite 118 vorgestellt). So wird z.B. die Zuweisung auf die Funktion memcpy()
zurückgeführt. Für die Speicherverwaltung werden die in C++ vorhandenen Operatoren new und
delete (siehe Seite 120) verwendet.
Headerdatei der Klasse Bsp::String
Die Headerdatei der Klasse Bsp::String hat insgesamt folgenden Aufbau, auf den nachfolgend
im Einzelnen eingegangen wird:
// dyna/string1.hpp
#ifndef STRING_HPP
#define STRING_HPP

// Headerdatei für I/O


#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class String {

private:
char* buffer; // Zeichenfolge als dynamisches Array
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer

public:
// Default- und char*-Konstruktor
String (const char* = "");
Sandini Bib

6.1 Dynamische Komponenten 357

// Aufgrund dynamischer Komponenten:


String (const String&); // Copy-Konstruktor
String& operator= (const String&); // Zuweisung
~String(); // Destruktor

// Vergleichen von Strings


friend bool operator== (const String&, const String&);
friend bool operator!= (const String&, const String&);

// Hintereinanderhängen von Strings


friend String operator+ (const String&, const String&);

// Ausgabe mit Streams


void printOn (std::ostream&) const;

// Eingabe mit Streams


void scanFrom (std::istream&);

// Anzahl der Zeichen


unsigned length () const {
return len;
}

private:
/* Konstruktor aus Länge und Buffer
* - intern für Operator +
*/
String (unsigned, char*);
};

// Standard-Ausgabeoperator
inline std::ostream& operator << (std::ostream& strm, const String& s)
{
s.printOn(strm); // String auf Stream ausgeben
return strm; // Stream zurückliefern
}

// Standard-Eingabeoperator
inline std::istream& operator >> (std::istream& strm, String& s)
{
s.scanFrom(strm); // String von Stream einlesen
Sandini Bib

358 Kapitel 6: Dynamische und statische Komponenten

return strm; // Stream zurückliefern


}

/* Operator !=
* - als Umsetzung auf Operator == inline implementiert
*/
inline bool operator!= (const String& s1, const String& s2) {
return !(s1==s2);
}

} // **** ENDE Namespace Bsp ********************************

#endif // STRING_HPP
Betrachten wir zunächst die Komponenten, die den Aufbau eines Strings beschreiben:
class String {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
...
}
Ein String-Objekt besteht aus einem dynamischen Zeiger buffer, der die eigentlichen Zeichen
des Strings als Feld von chars verwaltet, sowie aus zwei Komponenten, die jeweils die aktuelle
Anzahl der Zeichen in dem String und die Größe des dazugehörigen Speicherplatzes verwalten.
Die Zeichenfolge selbst gehört nicht direkt zum Objekt, sondern wird als dynamischer Spei-
cherplatz vom Objekt verwaltet. Abbildung 6.1 verdeutlicht diesen Sachverhalt: Der String s
enthält dort im aktuellen Zustand die Zeichenfolge hallo mit fünf Zeichen. Diese Zeichen be-
finden sich in einem separaten Speicherplatz für acht Zeichen, auf den buffer verweist.

s :
b u f f e r : ' h ' ' a ' ' l ' ' l ' ' o ' ? ? ?
l e n : 5
s i z e : 8

Abbildung 6.1: Interner Aufbau von Bsp::Strings

Man könnte alternativ ein Feld mit einer festen Größe deklarieren. Dies hat jedoch den Nachteil,
dass die Anzahl der Zeichen im String entweder merklich eingeschränkt ist und/oder unverant-
wortlich viel Speicherplatz für kleine Strings verschwendet wird.
Sandini Bib

6.1 Dynamische Komponenten 359

Der über buffer verwaltete dynamische Speicherplatz gehört nicht automatisch zum Objekt,
sondern muss explizit beim Erzeugen des Objekts angelegt werden. Entsprechend muss der Spei-
cherplatz bei Änderungen gegebenenfalls angepasst und beim Zerstören des Objekts wieder frei-
gegeben werden. Dies ist möglich, da die Implementierung sämtlicher Operationen in der Hand
des Programmierers liegt.
Jeder Konstruktor muss also explizit Speicherplatz anlegen, um diesen für die Zeichenfolge
verwenden zu können. Dazu gehört auch der Copy-Konstruktor (Copy-Konstruktoren wurden
in Abschnitt 4.3.7 eingeführt), der aus diesem Grund selbst implementiert werden muss. Der
Default-Copy-Konstruktor würde, da er komponentenweise kopiert, nämlich nur den Zeiger ko-
pieren, anstatt ein neues Feld anzulegen und die Zeichenfolge der Vorlage wirklich zu kopieren.
Aus dem gleichen Grund muss nun auch der Zuweisungsoperator implementiert werden. Der
Default-Zuweisungsoperator, der komponentenweise zuweist, würde nur die Zeiger und nicht
die Zeichenfolgen, auf die diese zeigen, zuweisen.
Schließlich muss der Speicherplatz, der explizit zu jedem Objekt angelegt wird, auch wieder
freigegeben werden, wenn das Objekt zerstört wird. Aus diesem Grund muss das Gegenstück zu
den Konstruktoren, der Destruktor, definiert werden. Ein Destruktor wird aufgerufen, wenn ein
Objekt zerstört wird, und bietet die Möglichkeit, vorher noch aufzuräumen“. Man erkennt ihn

daran, dass er als Funktionsnamen den Klassennamen mit dem vorangestellten Zeichen ~ trägt:

class String {
public:
...
~String(); // Destruktor
};

Quelldatei der Klasse Bsp::String


Die Klasse Bsp::String wird mit Hilfe der in in Abschnitt 3.8 eingeführten Operatoren zur
dynamischen Speicherverwaltung, new[ ] und delete[ ], implementiert. Die Quelldatei hat bis
auf die Einlesefunktion (sie wird erst in Abschnitt 6.1.7 erläutert) folgenden Aufbau:
// dyna/string1a.cpp
// Headerdatei der eigenen Klasse
#include "string.hpp"

// C-Headerdateien für String-Funktionen


#include <cstring>
#include <cctype>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Konstruktor aus C-String (const char*)


* - Default für s: ""
*/
Sandini Bib

360 Kapitel 6: Dynamische und statische Komponenten

String::String (const char* s)


{
len = std::strlen(s); // Anzahl an Zeichen
size = len; // Zeichenanzahl bestimmt Speicherplatzgröße
buffer = new char[size]; // Speicherplatz anlegen
std::memcpy(buffer,s,len); // Zeichen in Speicherplatz kopieren
}

/* Copy-Konstruktor
*/
String::String (const String& s)
{
len = s.len; // Anzahl an Zeichen übernehmen
size = len; // Zeichenanzahl bestimmt Speicherplatzgröße
buffer = new char[size]; // Speicherplatz anlegen
std::memcpy(buffer,s.buffer,len); // Zeichen kopieren
}

/* Destruktor
*/
String::~String ()
{
// mit new[] angelegten Speicherplatz wieder freigeben
delete [] buffer;
}

/* Operator =
* - Zuweisung
*/
String& String::operator= (const String& s)
{
// Zuweisung eines Strings an sich selbst hat keinen Effekt
if (this == &s) {
return *this; // String zurückliefern
}

len = s.len; // Anzahl an Zeichen übernehmen

// falls Platz nicht reicht, vergrößern


if (size < len) {
delete [] buffer; // alten Speicherplatz freigeben
Sandini Bib

6.1 Dynamische Komponenten 361

size = len; // Zeichenanzahl bestimmt neue Größe


buffer = new char[size]; // Speicherplatz anlegen
}

std::memcpy(buffer,s.buffer,len); // Zeichen kopieren

return *this; // geänderten String zurückliefern


}

/* Operator ==
* - vergleicht zwei Strings
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
bool operator== (const String& s1, const String& s2)
{
return s1.len == s2.len &&
std::memcmp(s1.buffer,s2.buffer,s1.len) == 0;
}

/* Operator +
* - hängt zwei Strings hintereinander
* - globale Friend-Funktion, damit eine automatische
* Typumwandlung des ersten Operanden möglich ist
*/
String operator+ (const String& s1, const String& s2)
{
// Puffer für den Summenstring erzeugen
char* buffer = new char[s1.len+s2.len];

// Summenzeichenfolge darin initialisieren


std::memcpy (buffer, s1.buffer, s1.len);
std::memcpy (buffer+s1.len, s2.buffer, s2.len);

// daraus Summenstring erzeugen und zurückliefern


return String (s1.len+s2.len, buffer);
}

/* Konstruktor für uninitialisierten String bestimmter Länge


* - intern für Operator +
*/
Sandini Bib

362 Kapitel 6: Dynamische und statische Komponenten

String::String (unsigned l, char* buf)


{
len = l; // Anzahl an Zeichen übernehmen
size = len; // ist gleichzeitig auch Speicherplatzgröße
buffer = buf; // Speicherplatz übernehmen
}

/* Ausgabe auf Stream


*/
void String::printOn (std::ostream& strm) const
{
// Zeichenfolge einfach ausgeben
strm.write(buffer,len);
}

} // **** ENDE Namespace Bsp ********************************


Wie bereits erwähnt, wird intern eine dynamische Speicherverwaltung implementiert, die dafür
sorgt, dass die Strings intern immer ausreichend viel Speicherplatz für die jeweilige Zeichenfol-
ge besitzen. Aus diesem Grund muss bei den Funktionen, die den Speicherplatz anlegen oder
manipulieren, besonders viel Sorgfalt verwendet werden.

6.1.2 Konstruktoren bei dynamischen Komponenten


Sehen wir uns dazu zunächst den Konstruktor für einen C-String (Typ const char*) als Argu-
ment an:
String::String (const char* s)
{
len = std::strlen(s); // Anzahl an Zeichen
size = len; // Zeichenanzahl bestimmt Speicherplatzgröße
buffer = new char[size]; // Speicherplatz anlegen
std::memcpy(buffer,s,len); // Zeichen in Speicherplatz kopieren
}
Dieser Konstruktor wird aufgerufen, wenn das neue String-Objekt angelegt wurde. Dies ge-
schieht z.B. durch eine entsprechende Deklaration:
Bsp::String s = "hallo";
Angelegt wird nur Speicherplatz für die ganzzahligen Werte len und size sowie für den Zeiger
buffer. Die Werte sind, wie immer bei fundamentalen Datentypen, zunächst nicht definiert:
Sandini Bib

6.1 Dynamische Komponenten 363

s :
b u f f e r : ?
l e n : ?
s i z e : ?

Zunächst werden len und size initialisiert. Dabei wird mit strlen() die Länge des übergebe-
nen C-Strings ermittelt:
len = std::strlen(s);
size = len;
Danach wird durch die Anweisung
buffer = new char[size];
Speicherplatz angelegt, der groß genug für die Zeichenfolge ist und dessen Adresse dem String-
Objekt in buffer zugewiesen wird. Auch der Inhalt dieses Speicherplatzes ist zunächst nicht
definiert:

s :
b u f f e r : ? ? ? ? ?
l e n : 5
s i z e : 5

Mit der Anweisung


std::memcpy(buffer,s,len);
werden die einzelnen Zeichen der Zeichenfolge schließlich kopiert. Die von C übernommene
Standardfunktion memcpy() kopiert jeweils die übergebene Anzahl an Zeichen eines Feldes aus
chars (bzw. Bytes). Damit werden alle len Zeichen von s in den Speicherbereich kopiert, auf
den buffer zeigt:

s :
b u f f e r : ' h ' ' a ' ' l ' ' l ' ' o '
l e n : 5
s i z e : 5

Das String-Objekt ist nun als eigenständiges Objekt initialisiert.

Verwendung als Default-Konstruktor


Als Default-Konstruktor wird der C-String-Konstruktor mit dem Default-Argument "" aufgeru-
fen. Das bedeutet, dass auf entsprechende Weise ein Objekt angelegt wird, in dem buffer auf
eine Zeichenfolge ohne Elemente zeigt:
Sandini Bib

364 Kapitel 6: Dynamische und statische Komponenten

s :
b u f f e r :
l e n : 0
s i z e : 0

Hier nutzen wir die Tatsache aus, dass man mit new auch ein Feld mit null Elementen anlegen
kann. In der Praxis wäre allerdings wohl eine Sonderbehandlung sinnvoll, die buffer in diesem
Fall intern NULL zuweist. Allerdings muss dies dann auch vor jedem Zugriff auf die Zeichen der
Zeichenfolge abgefragt werden.
Man beachte, dass als Default-Argument für den String-Konstruktor nicht NULL verwendet
werden kann, da die intern verwendeten String-Funktionen nicht mit NULL als Parameter aufge-
rufen werden dürfen (strlen(NULL) würde auf vielen Systemen zum Absturz führen). Bei der
Verwendung von NULL wäre eine Sonderbehandlung notwendig. Da die Standard-String-Klasse
eine derartige Sonderbehandlung allerdings nicht durchführt, wird auch hier darauf verzichtet.

6.1.3 Implementierung eines Copy-Konstruktors


Der Copy-Konstruktor arbeitet im Prinzip wie der einfache Konstruktor. Der Unterschied ist
nur, dass die Zeichenfolge, mit der das erzeugte String-Objekt initialisiert werden soll, nicht der
Parameter selbst ist, sondern im Parameter übergeben wird:
String::String (const String& s)
{
len = s.len; // Anzahl an Zeichen übernehmen
size = len; // Zeichenanzahl bestimmt Speicherplatzgröße
buffer = new char[size]; // Speicherplatz anlegen
std::memcpy(buffer,s.buffer,len); // Zeichen kopieren
}
Auch hier ist der Hinweis angebracht, dass der Default-Copy-Konstruktor nicht verwendet wer-
den kann, da dieser nur komponentenweise kopiert. Der Zeiger buffer würde dann nicht auf
eigenständigen Speicherplatz, sondern auf den gleichen Speicherplatz zeigen, auf den buffer in
dem String zeigt, der kopiert wurde.

6.1.4 Destruktoren
Der Destruktor dient bei der String-Klasse dazu, den explizit angelegten Speicherplatz mit der
Zerstörung des Strings freizugeben. Da es sich um ein Feld von Zeichen handelt, muss die Feld-
Syntax des Delete-Operators verwendet werden:
String::~String ()
{
// mit new[] angelegten Speicherplatz wieder freigeben
delete [] buffer;
}
Sandini Bib

6.1 Dynamische Komponenten 365

Aufgerufen wird der Destruktor, unmittelbar bevor der Speicherplatz für das eigentliche Objekt
freigegeben wird. Dies ist z.B. der Fall, wenn der Gültigkeitsbereich eines lokal angelegten Ob-
jekts verlassen wird:
void f()
{
Bsp::String s = "hallo"; // Konstruktor für s
...
} // Am Blockende: Destruktor für s
Der Destruktor wird für jedes angelegte Objekt aufgerufen. Das gilt auch für statische, temporäre
und explizit angelegte Objekte:
static Bsp::String s; // Konstruktor zum Programmstart
// Destruktor zum Programmende

void f ()
{
Bsp::String namen[10]; // 10 Aufrufe des Default-Konstruktors

namen[0] = neuerString("hallo");
// Zuweisung des temporären Rückgabewerts an namen[0]
// Destruktor für temporären Rückgabewert

} // Am Blockende: 10 Aufrufe des Destruktors

Bsp::String neuerString (const char* s)


{
Bsp::String neu = s; // char*-Konstruktor

return neu;
// Copy-Konstruktor für temporären Rückgabewert
} // Am Blockende: Destruktor für neu

6.1.5 Implementierung des Zuweisungsoperators


Der Zuweisungsoperator ist im Prinzip eine Kombination aus Destruktor und Copy-Konstruktor.
Ein existierender String wird durch eine Kopie eines anderen ersetzt. Dabei kann allerdings oft
optimiert werden, indem z.B. nur dann der bisherige Speicherplatz freigegeben und neuer reser-
viert wird, wenn der bisherige Speicherplatz nicht mehr ausreicht:
String& String::operator= (const String& s)
{
// Zuweisung eines Strings an sich selbst hat keinen Effekt
if (this == &s) {
Sandini Bib

366 Kapitel 6: Dynamische und statische Komponenten

return *this; // String zurückliefern


}

len = s.len; // Anzahl an Zeichen übernehmen

// falls Platz nicht reicht, vergrößern


if (size < len) {
delete [] buffer; // alten Speicherplatz freigeben
size = len; // Zeichenanzahl bestimmt neue Größe
buffer = new char[size]; // Speicherplatz anlegen
}

std::memcpy(buffer,s.buffer,len); // Zeichen kopieren

return *this; // geänderten String zurückliefern


}
Bei der Implementierung eines eigenen Zuweisungsoperators sollte Folgendes beachtet werden:
 Ein Zuweisungsoperator sollte immer das Objekt, für das die Operation aufgerufen wurde,
als Referenz zurückliefern. Dies ermöglicht verkettete Zuweisungen oder Tests nach Zuwei-
sungen, wie sie schon in C üblich sind:
Bsp::String a, b, c;
...
a = b = c;
...
if ((a = b) != Bsp::String("")) {
...
}
Allgemein lautet der typische Funktionskopf eines Zuweisungsoperators also:
klasse& klasse::operator = (const klasse& obj)
 Da es in der Regel keinen Sinn macht, am Rückgabewert im gleichen Ausdruck weitere
Manipulationen durchzuführen, kann man den Rückgabewert auch als Konstante deklarieren.
Der Funktionskopf eines Zuweisungsoperators kann deshalb auch wie folgt aussehen:
const klasse& klasse::operator = (const klasse& obj)
Diese Möglichkeit wird in der Praxis allerdings nicht oft verwendet.
 Im Zuweisungsoperator sollte als Erstes getestet werden, ob ein Objekt sich selbst zugewie-
sen wird. Eine Zuweisung eines Objekts an sich selbst kann in einem Anwendungsprogramm
über Zeiger immer passieren:
Bsp::String s; // String
Bsp::String* sp; // Zeiger auf String
...
Sandini Bib

6.1 Dynamische Komponenten 367

sp = &s; // sp zeigt auf s


...
*sp = s; // s wird über sp an sich selbst zugewiesen
Wird der Test nicht durchgeführt, kann es nicht nur passieren, dass unnötigerweise Rechen-
zeit verbraucht wird, es ist auch möglich, dass das Programm fehlerhaft weiterläuft. Dies ge-
schieht z.B., wenn beim Zuweisungsoperator Speicherplatz freigegeben wird. Damit würde
nämlich gleichzeitig der Speicherplatz des Objekts freigegeben, dessen Daten anschließend
zugewiesen werden.
Alle Implementierungen von Zuweisungsoperatoren sollten deshalb mit den folgenden
Zeilen beginnen:
// Zuweisung an sich selbst?
if (this == &obj) {
return *this; // Objekt unverändert zurückliefern
}
Man beachte, dass hier nicht verglichen wird, ob zwei Objekte gleich, sondern ob sie identisch
sind. Es werden nämlich die Adressen verglichen. Bei einem derartigen Vergleich gibt es
allerdings ein Problem: Bei Mehrfachvererbung können Objekte identisch sein, obwohl ihre
Adressen verschieden sind. In Abschnitt 5.4.3 wird darauf genauer eingegangen.

6.1.6 Weitere Operatoren


Die anderen Operatoren und Funktionen der String-Klasse sind einfach nur Umsetzungen in die
entsprechenden C-Funktionen.

Test auf Gleichheit


Zwei Strings sind dann gleich, wenn die Zeichenfolgen gleich sind. Zum Vergleich kann die
Funktion memcmp() verwendet werden (sie liefert bei Gleichheit 0 zurück). Zuvor wird jedoch
erst einmal geprüft, ob überhaupt die Anzahl der Zeichen gleich ist:
bool operator== (const String& s1, const String& s2)
{
return s1.len == s2.len &&
std::memcmp(s1.buffer,s2.buffer,s1.len) == 0;
}
Der UND-Operator && bricht beim ersten falschen Teilausdruck ab. Ist die Anzahl der Zeichen
also verschieden, werden die Zeichen selbst gar nicht mehr verglichen.
Die Funktion ist als globale Friend-Funktion deklariert, um eine automatische Typumwand-
lung für den ersten Operanden zu ermöglichen (siehe auch Seite 227).
Bsp::String s;
...
if ("hallo" == s) // => if (Bsp::String("hallo") == s)
Sandini Bib

368 Kapitel 6: Dynamische und statische Komponenten

Der Test auf Ungleichheit wird als Inline-Funktion einfach auf den Test auf Gleichheit zurück-
geführt:
class String {
public:
...
friend bool operator== (const String&, const String&);
friend bool operator!= (const String&, const String&);
};
...
inline bool operator!= (const String& s1, const String& s2) {
return !(s1==s2);
}

Hintereinanderhängen durch den Operator +


Der Operator + wird zum Hintereinanderhängen von zwei Strings verwendet. Auch er wird als
globale Friend-Funktion definiert, um eine automatische Typumwandlung auch für den ersten
Operanden zu ermöglichen:
class String {
public:
...
friend String operator+ (const String&, const String&);
};
Bei der Implementierung muss ein neues Objekt für den Summenstring erzeugt werden. Eine
naive Implementierung könnte dies wie folgt tun:
String operator+ (const String& s1, const String& s2)
{
// Summenstring anlegen
String sum;

// ausreichend Speicherplatz anfordern


delete [] sum.buffer;
sum.buffer = new char[s1.len+s2.len];

// Zeichen kopieren
std::memcpy(sum.buffer,s1.buffer,s1.len);
std::memcpy(sum.buffer+s1.len,s2.buffer,s2.len);

// Summenstring zurückliefern
return sum;
}
Sandini Bib

6.1 Dynamische Komponenten 369

Diese Implementierung enthält aber gleich mehrere Laufzeitnachteile: Zunächst wird ein Sum-
menstring mit initialem Speicherplatz angelegt, der aber sofort wieder freigegeben wird, um ihn
zu vergrößern (etwas, was auch leicht vergessen wird und dann ein Speicherplatz-Leck (Memory-
Leak) darstellt). Schließlich wird mit der Return-Anweisung der eigentliche Rückgabewert als
Kopie von sum angelegt und sum wieder zerstört. Damit wird insgesamt dreimal new und zwei-
mal delete aufgerufen.
Es wäre natürlich besser, wenn nur einmal new aufgerufen würde. Dies gilt umso mehr, als
dass new und delete zu den teureren“ Operationen gehören. Zu diesem Zweck wird ein spe-

zieller Konstruktor definiert, der einen String anlegt, dem sowohl die Startgröße als auch der
vollständig initialisierte Puffer mit der Zeichenfolge des Summenstrings übergeben wird:
String::String (unsigned l, char* buf)
{
len = l; // Anzahl an Zeichen übernehmen
size = len; // ist gleichzeitig auch Speicherplatzgröße
buffer = buf; // Speicherplatz übernehmen
}
Dieser Konstruktor ist eine spezielle Optimierung, die als öffentliche Schnittstelle gefährlich
wäre. Man könnte nicht sicherstellen, dass als zweiter Parameter immer mit new[] angelegter
und initialisierter Speicherplatz der Länge len übergeben wird. Aus diesem Grund kann der
Konstruktor von der Außenwelt nicht aufgerufen werden:
class String {
...
private:
String (unsigned, char*);
};
Der Operator + kann damit so implementiert werden, dass gleich der Puffer des Summenstrings
mit der richtigen Größe erzeugt und initialisiert wird:
String operator+ (const String& s1, const String& s2)
{
// Puffer für den Summenstring erzeugen
char* buffer = new char[s1.len+s2.len];

// Summenzeichenfolge darin initialisieren


std::memcpy (buffer, s1.buffer, s1.len);
std::memcpy (buffer+s1.len, s2.buffer, s2.len);

// daraus Summenstring erzeugen und zurückliefern


return String (s1.len+s2.len, buffer);
}
Da der spezielle Konstruktor, der aus der Länge und dem Puffer erst einen wirklichen String
erzeugt, Teil der Return-Anweisung ist, wird diese Operation typischerweise so optimiert, dass
Sandini Bib

370 Kapitel 6: Dynamische und statische Komponenten

dieser Summenstring gleich als Rückgabewert erzeugt wird. Damit wird nur das eine new aufge-
rufen.
Dieses Beispiel zeigt, welches Optimierungspotenzial man hat, wenn man Zugriff auf alle
Operationen eines Datentyps hat. Man beachte, dass diese Optimierung keinen Einfluss auf die
Schnittstelle des Anwenders hat. Der Anwender kann so oder so einfach Strings hintereinander
hängen. Bei dieser Implementierung geht es allerdings merklich schneller.

6.1.7 Einlesen eines Strings


Das Einlesen eines Objekts mit dynamischen Komponenten ist in der Regel nicht ganz einfach.
Ein Problem ist, dass beim Beginn des Einlesens noch nicht klar ist, wie groß der Speicherplatz
wird, den das Objekt braucht. Auch muss definiert werden, wann das Einlesen eines Strings
beginnt und wodurch es beendet wird. Der nachfolgend vorgestellten Implementierung der Ein-
lesefunktion liegen also einige Überlegungen zugrunde, auf die anschließend eingegangen wird.
Die Einlesefunktion der Klasse Bsp::String hat folgenden Aufbau:
// dyna/string1b.cpp
// **** BEGINN Namespace Bsp ********************************
namespace Bsp {

void String::scanFrom (std::istream& strm)


{
char c;

len = 0; // zunächst ist der gelesene String leer

strm >> std::ws; // führende Trennzeichen überlesen

/* solange der Input-Stream strm nach dem Einlesen


* eines Zeichens in c in Ordnung ist
*/
while (strm.get(c)) { // >> würde Trennzeichen überlesen

/* ein Trennzeichen schließt die Stringeingabe ab


* => RETURN
*/
if (std::isspace(c)) {
return;
}

/* falls der Platz nicht mehr ausreicht,


* muss mehr geschaffen werden
*/
Sandini Bib

6.1 Dynamische Komponenten 371

if (len >= size) {


char* tmp = buffer; // Zeiger auf alten Speicherplatz
size = size*2 + 32; // Speicherplatz mehr als verdoppeln
buffer = new char[size]; // neuen Speicherplatz anlegen
std::memcpy(buffer,tmp,len); // Zeichen kopieren
delete [] tmp; // alten Speicherplatz freigeben
}

// neues Zeichen eintragen


buffer[len] = c;
++len;
}

// Einlese-Ende durch Fehler oder EOF


}

} // **** ENDE Namespace Bsp ********************************


Grundsätzlich ist die Einlesefunktion so implementiert, dass sie einen String als Wort einliest,
wobei führende Trennzeichen überlesen werden. Abgeschlossen wird die Eingabe also durch ein
Trennzeichen (Whitespace) oder das Ende der Eingabe (EOF, End-Of-File).
Nachdem die Länge des einzulesenden Strings auf 0 gesetzt wurde, werden deshalb zunächst
führende Trennzeichen überlesen:
std::strm >> std::ws;
Der Manipulator std::ws übernimmt diese Arbeit. Der Name steht für Whitespace“, womit die

typischen Trennzeichen Newline, Tabulator und Leerzeichen gemeint sind.
Anschließend wird eine Schleife durchlaufen, die jeweils ein Zeichen liest und verarbeitet:
while (strm.get(c)) {
// c verarbeiten
...
}
Die Elementfunktion get() dient dabei dazu, das jeweils nächste Zeichen einzulesen. Der Ope-
rator >> kann hier nicht verwendet werden, da er, selbst wenn er nur ein Zeichen einliest, führen-
de Trennzeichen überliest. Wir müssen aber wissen, ob ein Trennzeichen gelesen wurde, da dies
das Ende der Stringeingabe wäre.
Die Funktion get() liefert jeweils den Stream zurück, von dem gelesen wurde. Dieser wird
dann als Bedingung verwendet, ob die Schleife weiterlaufen soll. Wie in Abschnitt 4.5.2 bereits
erläutert wurde, ist die Bedingung dann erfüllt, wenn sich der Stream in einem korrekten Zustand
befindet (weder End-Of-File noch Fehler). Die Schleife läuft also zunächst so lange, wie ein
einzelnes Zeichen erfolgreich eingelesen werden kann.
Sandini Bib

372 Kapitel 6: Dynamische und statische Komponenten

In der Schleife wird zunächst getestet, ob ein Trennzeichen eingegeben wurde. Wenn dies der
Fall ist, wird es als Eingabeende betrachtet, und die wird Funktion beendet:1
if (std::isspace(c)) {
return;
}
Wurde kein Trennzeichen eingegeben, muss das neue Zeichen eingetragen werden. Hierbei muss
aber sichergestellt werden, dass noch genug Platz für das neue Zeichen vorhanden ist. Schon in C
ist es ein verbreiteter Fehler anzunehmen, eine Eingabe könne nur 80 Zeichen lang sein. Tatsache
ist, dass eine Eingabe beliebig lang werden kann (z.B. weil die Eingabe von einem Datenstrom
gelesen wird, der erst nach 10.000 Zeichen ein Trennzeichen hat). Wenn der Platz nicht mehr
ausreicht, wird deshalb ein neuer, mehr als doppelt so großer Speicherplatz angefordert, und die
bisherigen Zeichen werden dorthin kopiert:
if (len >= size) {
char* tmp = buffer; // Zeiger auf alten Speicherplatz
size = size*2i + 32; // Speicherplatz mehr als verdoppeln
buffer = new char[size]; // neuen Speicherplatz anlegen
std::memcpy(buffer,tmp,len); // Zeichen kopieren
delete [] tmp; // alten Speicherplatz freigeben
}
Nun kann das neue Zeichen eingetragen und die Anzahl der Zeichen entsprechend erhöht werden:

buffer[len] = c;
++len;

6.1.8 Kommerzielle Implementierung von String-Klassen


Die vorliegende Implementierung von Bsp::String stellt nur einen rudimentären Auszug aus
einer Implementierung einer standardkonformen String-Klasse dar. So fehlt z.B. eine der wich-
tigsten Operationen, nämlich die Möglichkeit, mit dem Indexoperator auf ein Zeichen zuzugrei-
fen. Darauf wird in Abschnitt 6.2.1 eingegangen.
Auch kann man die Klasse sicherlich noch weiter optimieren. Wir haben zwar schon ein
wenig optimiert; da die Klasse aber von fundamentaler Bedeutung ist, lohnt sich sicherlich jede
Optimierung, die zu einem besseren Laufzeitverhalten führt.
Eine Konsequenz der Optimierung ist, dass die meisten Funktionen Inline-Funktionen sind.
Da Headerdateien lesbar sind (sie müssen ja eingebunden werden können), empfehle ich, einmal
einen Blick in eine derartige Headerdatei zu werfen.

1
Hinweis: Auf manchen Systemen wird isspace() auch in <cctype> fälschlicherweise global, also ohne
std:: definiert, was hier zu einer Fehlermeldung führt.
Sandini Bib

6.1 Dynamische Komponenten 373

Reference-Counting
Eine Optimierungsmöglichkeit, die auch verdeutlicht, welche mächtige Möglichkeiten sich durch
die Programmierung von Klassen in C++ eröffnen, ist die Verwendung einer Technik namens
Reference-Counting. Diese Technik basiert auf der Überlegung, dass das Kopieren und Zuweisen
von Strings teuer ist, sehr häufig vorkommt und dass Strings relativ selten manipuliert werden
(also einzelne Zeichen darin nur selten verändert werden).
Der Trick besteht darin, dass ein String-Objekt selbst nur ein einfaches Handle ist, das auf
das eigentliche String-Objekt (den so genannten Body) verweist. Die eigentlichen Daten eines
Strings mit der eigentlichen Zeichenfolge werden also in eine Hilfsklasse ausgelagert:
class StringBody {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
unsigned refs; // Anzahl Strings, die diesen Body verwenden
...
};
Die Objekte der Hilfsklasse StringBody verwalten die eigentlichen Zeichenfolgen und können
von mehreren Strings, die alle den gleichen Wert haben, gemeinsam verwendet werden. Die
Komponente references hält jeweils fest, wie viele Strings ein solches Objekt verwenden.
Mit jeder Initialisierung wird ein Body-Objekt mit den eigentlichen Daten und ein Handle
erzeugt. Im Body wird eingetragen, dass es genau einen Besitzer gibt (refs wird auf 1 gesetzt).
Das Handle selbst ist ein ganz einfaches Objekt, das einfach nur auf den Body verweist:
class String {
private:
StringBody* body; // Zeiger auf den eigentlichen String
...
};
Wenn ein String nun kopiert wird, wird nur das Handle kopiert, und im Body wird eingetragen,
dass nun ein Handle mehr auf ihn verweist. Zwei Strings verwenden dann also intern dieselbe
Zeichenfolge. Wenn ein String zerstört wird, wird in seinem Body die Anzahl der Strings, die auf
ihn verweisen, entsprechend dekrementiert. Ist die Anzahl damit 0, wird auch das Body-Objekt
zerstört.
Jeder lesende Zugriff auf einen String (etwa die Abfrage seiner Länge) wird einfach an den
Body weitergereicht. Nur wenn ein String verändert wird, dessen Body mehrfach verwendet
wird, wird eine echte Kopie des Bodys angelegt und dem geänderten String zugeordnet.
Da das Durchreichen von Strings (Kopieren, Zuweisen) typischerweise weitaus häufiger ge-
schieht als das Ändern einzelner Zeichen, werden mit diesem Mechanismus Speicherplatz-Funk-
tionen vermieden, die vergleichsweise viel Zeit kosten.
Andere typische Optimierungen sind spezielle Funktionalitäten für Teilstrings, die ebenfalls
über interne Hilfsklassen verwaltet werden.
Sandini Bib

374 Kapitel 6: Dynamische und statische Komponenten

Reference-Counting kann man auch mit Hilfe von speziellen Smart-Pointern implementieren.
Darauf wird in Abschnitt 9.2.1 eingegangen.

Doch kein Reference-Counting


Nach der Erläuterung der Optimierungsmöglichkeit mit Reference-Counting mag man erwarten,
dass nun jede Implementierung der Klasse std::string auf diese Weise optimiert ist. Doch
weit gefehlt.
Es hat sich in jüngster Zeit gezeigt, dass derartige Optimierungen bei String-Klassen in
Multithreading-Programmen kontraproduktiv sein können. Der Preis der erhöhten Komplexität
und der Locking-Verwaltung ist oft höher als der Gewinn durch das Vermeiden des Kopierens.
Aus diesem Grund haben nach und nach alle String-Implementierungen eine derartige Optimie-
rung wieder entfernt. Sie würde nur dann Sinn machen, wenn sichergestellt ist, dass Strings zu
ihren Lebzeiten nicht verändert werden.2
Stattdessen gibt es heutzutage andersartige Optimierungen, die vor allem auf die Tatsache
abzielen, dass die meisten Strings nur aus sehr wenigen Zeichen bestehen. Das Gute an C++
ist, dass man von derartigen Erkenntnissen und Verbesserungen bei der Verwendung der String-
Klassen automatisch profitiert. Das Interface ändert sich dadurch nicht.

6.1.9 Weitere Anwendungsmöglichkeiten dynamischer


Komponenten
Der Begriff dynamische Komponente kann weit mehr bedeuten als nur eine Komponente, die mit
new und delete verwaltet wird. Eine der faszinierendsten Eigenschaften von C++ ist, dass belie-
big komplizierte Anweisungen aufgerufen werden können, um ein Objekt in seinen Startzustand
zu bringen.
So kann z.B. eine Dateiverwaltung komplett in einem abstrakten Datentyp versteckt wer-
den. Der Konstruktor öffnet die als Argument übergebene Datei, und der Destruktor schließt sie
wieder:
class Datei {
private:
FILE* fp; // Zeiger auf geöffnete Datei

public:
Datei (char*); // char*-Konstruktor für den Dateinamen
~Datei (); // Destruktor
...
};

2
Hier zeigt sich der Vorteil des String-Ansatzes von Java, bei dem zwischen einer Klasse für Strings, bei
denen keine einzelnen Zeichen geändert werden können, und Strings, bei denen jede Änderung möglich ist,
unterschieden wird. Dafür kann man dort aber Strings nicht einfach mit dem Operator == vergleichen. (Ich
glaube, ich muss doch noch meine eigene String-Klasse oder Programmiersprache schreiben.)
Sandini Bib

6.1 Dynamische Komponenten 375

Datei::Datei (char* dateiname)


{
// Datei öffnen
fp = fopen(dateiname,...);
...
}

Datei::~Datei ()
{
// Datei schließen
fclose(fp);
}
Im Anwendungsprogramm wird die Datei dann automatisch mit der Deklaration eines Objekts
geöffnet und beim Verlassen des Blocks automatisch wieder geschlossen:
void f ()
{
Datei d("testprog.cc"); // Konstruktor öffnet die Datei
...

} // Destruktor am Blockende schließt die Datei automatisch


Die in der Standard-I/O-Bibliothek definierten Stream-Klassen zum Zugriff auf Dateien arbeiten
nach diesem Prinzip (sie rufen dazu allerdings Low-level-Funktionen zum Öffnen und Schließen
von Dateien auf). Diese Klassen werden in Abschnitt 8.2 vorgestellt.
Auf entsprechende Art und Weise können Datenbankzugriffe, Pipes, Prozesse und andere
beliebig komplexe Objekte oder Vorgänge so abstrahiert werden, dass ihre Anwendung erheb-
lich vereinfacht wird. Im Gegensatz zu Java gibt es hier auch den Vorteil, dass die Destruktoren
nicht irgendwann, sondern beim Verlassen des Gültigkeitsbereichs aufgerufen werden. Dadurch
können auch abzuschließende Vorgänge als Objekte programmiert werden. Ein typisches Bei-
spiel wäre eine Klasse die laufende Transaktionen repräsentiert. In C++ könnte man sie wie folgt
anwenden:
void foo()
{
Transaktion t;
... // Ausnahme löst t.cancel() aus
t.commit();
}
Der Clou an dieser Verwendung besteht darin, dass man im Falle eines Fehlers und vorzeitigen
Abbruchs (etwa durch eine Ausnahme) die Transaktion nicht explizit mit cancel() abbrechen
muss. Dies macht automatisch der Destruktor von t.
Bei der Implementierung solcher Klassen muss man allerdings mit gewisser Vorsicht vor-
gehen. Speziell die Implementierung von Copy-Konstruktor und Zuweisungsoperator muss sehr
Sandini Bib

376 Kapitel 6: Dynamische und statische Komponenten

gut überlegt werden. (Was soll beim Kopieren einer Datei oder einer Transaktion passieren?) Auf
jeden Fall darf man sich hier nicht auf die Default-Implementierung verlassen. Diese arbeitet in
der Regel nicht korrekt.
Als Richtlinie gilt:
 Eine Klasse braucht einen Copy-Konstruktor, einen Zuweisungsoperator und einen Destruk-
tor oder nichts davon.
Das heißt: Braucht man für eine Klasse eine eigene Implementierung von einem Copy-Kon-
struktor, einem Zuweisungsoperator oder einem Destruktor, dann braucht man in der Regel eine
eigene Implementierung für alle diese Operationen.
Ist nicht klar, ob man einen Copy-Konstruktor oder Zuweisungsoperator braucht, kann man
das Kopieren und Zuweisen auch einfach verbieten. Dazu müssen der Copy-Konstruktor bzw.
der Zuweisungsoperator einfach als private deklariert werden. Darauf wird in Abschnitt 6.2.5
eingegangen.

6.1.10 Zusammenfassung
 Klassen können dynamische Komponenten besitzen. Das sind Komponenten, die nicht ein-
fach durch Zuweisung kopiert werden können (typischerweise Zeiger).
 Klassen mit dynamischen Komponenten brauchen eine eigene Implementierung des Copy-
Konstruktors, des Zuweisungsoperators und eines Destruktors.
 Ein Destruktor ist eine Funktion, die bei der Zerstörung des Objekts aufgerufen wird.
 Ein Zuweisungsoperator sollte mit einem Test beginnen, der feststellt, ob ein Objekt sich
selbst zugewiesen wird.
 Ein Zuweisungsoperator sollte das Objekt zurückliefern, dem etwas zugewiesen wurde (also
*this).
 Durch dynamische Komponenten können beliebig komplexe Vorgänge so abstrahiert werden,
dass sie über ein Objekt einfach zu handhaben sind.
 Das Kopieren und Zuweisungen können verboten werden.
 Eine Klasse braucht in der Regel einen Copy-Konstruktor, einen Zuweisungsoperator und
einen Destruktor oder nichts davon.
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 377

6.2 Weitere Aspekte dynamischer Komponenten


Klassen mit dynamischen Komponenten zeigen eine wesentliche Stärke des Konzepts der objekt-
orientierten Programmierung auf: Komplizierte dynamische Vorgänge können in das Verhalten
einer Klasse verlagert werden, so dass das Anwendungsprogramm sich nur noch auf das We-
sentliche konzentrieren kann. Beim Implementieren derartiger Klassen lauern allerdings auch
Gefahren. So kann man z.B. ungewollt die Datenkapselung einer Klasse aufheben. Derartige
Aspekte der Implementierung eigener Klassen werden in diesem Abschnitt angesprochen.

6.2.1 Dynamische Komponenten bei konstanten Objekten


Die im vorigen Abschnitt vorgestellte Klasse Bsp::String wird in diesem Abschnitt um einige
sinnvolle Operationen erweitert. Dabei wird ein Problem angesprochen, das für die Implemen-
tierung von Klassen mit dynamischen Komponenten von großer Wichtigkeit ist: der Umgang mit
konstanten Objekten, die dynamische Komponenten besitzen. Es ist besondere Sorgfalt notwen-
dig, um die Konstantheit der Objekte nicht zu unterlaufen.

Strings mit dem Operator [ ]


Da Strings als Feld (Array) von Zeichen betrachtet werden, ist es üblich, auch für String-Klassen
mit Hilfe des Indexoperators den Zugriff auf einzelne Zeichen zu ermöglichen.
Eine Tatsache ist dabei allerdings bemerkenswert: Bei nichtkonstanten Strings bietet der Ope-
rator Zugriff auf ein Zeichen im Objekt, wodurch es sogar verändert werden kann. Nach den
Anweisungen
Bsp::String s = "Tasse";

s[0] = 'K';
sollte in s der String "Kasse" stehen. Dazu muss der im Ausdruck s[0] aufgerufene Operator
[ ] einen internen Teil des Strings zurückliefern, so dass dieser manipuliert werden kann.
Dies ist nur möglich, wenn der Operator [ ] nicht eine Kopie, sondern Zugriff auf das Origi-
nalzeichen des Strings liefert, damit es manipuliert werden kann. Das bedeutet, dass die Opera-
torfunktion eine Referenz zurückliefern muss.
Der Operator muss deshalb wie folgt deklariert werden:
namespace Bsp {
class String {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
...
public:
char& operator [] (unsigned); // Zugriff auf ein Zeichen
...
};
}
Sandini Bib

378 Kapitel 6: Dynamische und statische Komponenten

Die Implementierung könnte dann wie folgt aussehen:


// dyna/stridxvar.cpp
/* Operator [] für Variablen
*/
char& String::operator [] (unsigned idx)
{
// Index nicht im erlaubten Bereich ?
if (idx >= len) {
throw std::out_of_range("string index out of range");
}

return buffer[idx];
}
Nach dem Test, ob der übergebene Index überhaupt im erlaubten Bereich liegt, wird das Zeichen
mit dem Index idx der internen Komponente buffer als Referenz zurückgeliefert. Das zurück-
gelieferte Zeichen der Funktion ist also keine Kopie, sondern das Originalzeichen des Strings
und kann damit auch manipuliert werden.

Der Operator [ ] für konstante Strings


Die soeben vorgestellte Implementierung des Operators [ ] für Strings ist allerdings für kon-
stante Strings nicht geeignet, da der Operator damit eine Manipulation am String ermöglichen
würde.
Es wäre aber dennoch möglich, sie als Konstanten-Elementfunktion zu deklarieren und damit
auch für Konstanten den Aufruf zu ermöglichen:
namespace Bsp {
class String {
...
public:
char& operator [] (unsigned) const;
...
};
}
Dies führt aber dazu, dass eine Konstante manipuliert werden kann:
const String s = "Tasse";

s[0] = 'K'; // wird nicht als Fehler erkannt


Nun könnte der Einwand kommen, dass doch eigentlich schon der Compiler erkennen kann, dass
auf ein konstantes Objekt zugegriffen wird und somit eine Manipulation über den Operator [ ]
sowieso nicht möglich ist. Dies ist aber falsch! Das Problem besteht darin, dass das eigentliche
Objekt gar nicht verändert wird. Seine Komponente buffer ist ja nur der Zeiger auf die Zei-
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 379

chenkette, und der bleibt konstant. Das, worauf der Zeiger zeigt, ist aber nicht konstant. Wenn
der Operator [ ] in seiner bisherigen Implementierung auch für konstante Objekte erlaubt würde,
könnten diese deshalb den Inhalt des Strings verändern.
Der Operator sollte aber für konstante Objekte definiert sein. Es wäre recht uneinsichtig,
wenn es nur bei variablen Strings möglich wäre, ein Zeichen über den Operator [ ] abzufragen.
Das Folgende sollte also möglich sein:
const Bsp::String s = "Tasse";
...
char c = s[0];
Das bedeutet, dass eigentlich zwei Implementierungen des Operators [ ] benötigt werden: ei-
ne für konstante und eine für variable Strings. Eine solche unterschiedliche Implementierung
ist möglich. Eine Funktion kann sowohl für variable als auch für konstante Objekte überladen
werden (nur wenn es keine eigene Funktion für variable Objekte gibt, wird die Funktion für
konstante Objekte verwendet).
Aus diesem Grund sollte die Klasse Bsp::String eine zweite Deklaration für den Operator
[ ] erhalten:
namespace Bsp {
class String {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
...
public:
// Operator [] für Variablen
char& operator [] (unsigned);

// Operator [] für Konstanten


char operator [] (unsigned) const;
...
};
}
Die Anweisungen im Funktionskörper für konstante Strings unterscheiden sich nicht von der
Version für variable Strings:
// dyna/stridxconst.cpp
/* Operator [] für Konstanten
*/
char String::operator [] (unsigned idx) const
{
// Index nicht im erlaubten Bereich ?
Sandini Bib

380 Kapitel 6: Dynamische und statische Komponenten

if (idx >= len) {


throw std::out_of_range("string index out of range");
}

return buffer[idx];
}
Entscheidend ist nur, dass das Zeichen nicht mehr als Referenz, sondern als Kopie zurückgeliefert
wird. Handelt es sich bei dem Rückgabewert um ein größeres Objekt, wäre aus Zeitgründen eine
Referenz vorzuziehen, die dann aber natürlich als konstant deklariert werden sollte:
namespace Bsp {
class String {
...
public:
const char& operator [] (unsigned) const;
...
};
}

6.2.2 Konvertierungsfunktionen für dynamische


Komponenten
Eine ähnliche Sorgfalt verlangt die Implementierung von Konvertierungsfunktionen. Dabei spielt
es keine Rolle, ob es sich um Funktionen zur automatischen oder zur expliziten Typumwandlung
handelt.
Die Klasse String liefert wieder ein schönes Beispiel. Es ist sicherlich sinnvoll, wenn man
aus einem Objekt der Klasse String wieder einen C-String machen kann, da zahlreiche Funk-
tionen einen C-String als Parameter verlangen.
Aufgrund der Überlegungen aus Abschnitt 4.6.5, dass Funktionen zur automatischen Typ-
umwandlung vermieden werden sollten, wird das Problem anhand einer Funktion zur expliziten
Typumwandlung betrachtet.
Auch hier muss darauf geachtet werden, dass über die Konvertierungsfunktion kein Zugriff
auf ein internes Objekt entsteht. Dies sollte sowohl für konstante als auch für variable Objekte
gelten, da sonst beliebige Manipulationen am String-Objekt möglich werden. Die Schnittstelle
der Funktionen, die den Zugriff auf das Objekt kontrollieren und verifizieren, würde aufgehoben,
und es könnten Inkonsistenzen entstehen.
Der Zugriff auf die internen Elemente des Strings kann auf zweierlei Arten vermieden wer-
den:
 Es kann eine Kopie verwendet werden.
 Es kann eine Konstante verwendet werden.
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 381

Kopien als Rückgabewerte


Wenn eine Kopie zurückgeliefert wird, sehen die Deklaration und die Implementierung z.B. wie
folgt aus:
class String {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
...
public:
char* toCharPtr() const;
};

char* String::toCharPtr() const


{
// Speicherplatz für Kopie anlegen
char* p = new char[len+1];

// Zeichen kopieren
std::memcpy (p, buffer, len);

// Stringendekennzeichen anhängen
p[len] = '\0';

// und zurückliefern
return p;
}
Diese Lösung hat allerdings mehrere Nachteile:
 Zum einen kostet das explizite Anlegen von Speicherplatz Zeit.
 Zum anderen muss, da extra Speicherplatz angelegt wird, das Anwendungsprogramm darauf
achten, diesen auch wieder freizugeben. Eine Forderung, die früher oder später sicherlich zu
einem Speicherplatzproblem führen wird.
Falls eine solche Implementierung zur Konvertierung verwendet wird, sollte diese deshalb
zumindest im Namen den Hinweis enthalten, dass Speicherplatz angelegt wird, der freizuge-
ben ist (etwa durch den Namen asNewCharPtr()).

Konstanten als Rückgabewerte


Besser ist es daher meistens, die interne Zeichenfolge zurückzuliefern und den Rückgabewert so
zu deklarieren, dass die Daten nicht verändert werden können:
Sandini Bib

382 Kapitel 6: Dynamische und statische Komponenten

class String {
private:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer
...
public:
const char* toCharPtr() const;
};

const char* String::toCharPtr() const


{
// Zeichenfolge zurückliefern
return buffer;
}
In diesem Fall wird einfach die Zeichenfolge des Strings als konstante Zeichenfolge zurückge-
liefert. Auch hier kann es allerdings Probleme geben:
 Zum einen wird eine Adresse zurückgeliefert, die ungültig werden kann. Da das String-
Objekt bei einer Wertänderung eventuell einen neuen Speicherplatz für die Zeichenfolge er-
hält, besteht die Gefahr, dass die hier zurückgelieferten Zeichen dann nicht mehr definiert
sind (ein Problem, das beim Anlegen einer Kopie nicht entsteht).
 Man kann nicht einfach ein Stringendekennzeichen anhängen.
Die Standard-String-Klasse std::string bietet zwei derartige Funktionen:
 c_str() liefert die eigentliche Zeichenfolge als C-String (also mit angehängtem Stringen-
dekennzeichen '\0', siehe Seite 72).
 data() liefert die interne Zeichenfolge so, wie sie ist.
Dabei verwenden Implementierungen der Klasse typischerweise folgenden Trick: Intern wird
einfach immer automatisch ein Stringendekennzeichen angehängt. Dazu wird immer für minde-
stens ein Zeichen mehr Speicherplatz reserviert. Sowohl c_str() als auch data() liefern dann
einfach diesen internen Puffer.3 Dieser Rückgabewert ist dann natürlich nur bis zur nächsten
Operation definiert, die den String manipulieren oder ungültig machen könnte. Man muss sich
deshalb immer sofort eine Kopie anlegen.

6.2.3 Konvertierungsfunktionen für Bedingungen


Bei Klassen mit dynamischen Komponenten verwendet man oft die automatische Typumwand-
lung zum Testen von Bedingungen. In Anlehnung an traditionellen C-Code wie

3
Vorsicht, das bedeutet nicht, dass man davon ausgehen kann, dass auch data() mit dem Stringendekenn-
zeichen endet. Es ist nur bei den meisten Implementierungen so. Wer ein Stringendekennzeichen braucht,
sollte also immer c_str() aufrufen.
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 383

FILE* fp; // Zeiger auf die geöffnete Datei

fp = fopen ("hallo", "r");

if (fp) {
// Daten lesen
...
}
else {
// FEHLER: fp ist NULL
...
}
wird z.B. bei einer Klasse für geöffnete Dateien Entsprechendes ermöglicht:
Bsp::Datei f("hallo"); // Konstruktor öffnet die Datei (hoffentlich)

if (f) {
// Daten lesen
...
}
else {
// Fehler: f ist nicht in Ordnung
...
}
Diese Technik basiert in C auf dem Sachverhalt, dass NULL bei Zeigern oft einen Fehlerstatus
anzeigt. NULL ist aber nichts anderes als der Wert 0, welcher wiederum für false steht. Der Test
if (fp)
ist also eigentlich eine Abkürzung für:
if (fp != NULL)
Man kann hierbei streiten, ob dadurch die Lesbarkeit der Programme erhöht wird, da diese
Schreibweise dazu führt, dass in Bedingungen nicht mehr nur Boolesche Ausdrücke oder Werte
stehen können.
Die Übertragung auf C++ kann man nun unter dem gleichen Blickwinkel betrachten. Extra
dafür wurde definiert, dass Bedingungen in Kontrollstrukturen auch dann möglich sind, wenn
für ein Objekt einer Klasse eine automatische Typumwandlung in einen ganzzahligen Typ oder
einen Zeigertyp definiert ist. Besitzt der ganzzahlige Typ oder der Zeigertyp den Wert 0, ist die
Bedingung nicht erfüllt.
Um also die obige If-Abfrage zu ermöglichen, muss z.B. eine Typumwandlung in einen Zei-
gertyp implementiert werden. Dies könnte bei einer Klasse für geöffnete Dateien z.B. wie folgt
aussehen:
Sandini Bib

384 Kapitel 6: Dynamische und statische Komponenten

namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei

public:
...
operator FILE* () { // automatische Umwandlung in File*
return fp;
}
};
}
Nach der Deklaration
Bsp::Datei f("hallo");
entspricht der Aufruf
if (f)
damit:
if ((f.operator FILE*()) != NULL)
Diese Umsetzung birgt aber die Gefahr, die ganzen Vorteile einer sicheren Schnittstelle zu verlie-
ren. Mit einer so definierten Typkonvertierung besteht die Möglichkeit, Zugriff auf eine private
Komponente zu erhalten und diese zu modifizieren. Ohne Fehlermeldung wäre z.B. Folgendes
möglich:
Bsp::Datei f;
FILE* fp;
...
fp = f;
Eine erste Verbesserung wäre deshalb die Umwandlung des Zeigers in den Datentyp void*:
namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei

public:
...
operator void* () {
return (void*)fp;
}
};
}
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 385

Doch auch damit wird eine interne Komponente nach außen gegeben. Noch besser ist es daher
sicherzustellen, dass keine interne Adresse nach außen gegeben wird:
namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei

public:
...
operator void* () {
return fp != NULL ? reinterpret_cast<void*>(32)
: static_cast<void*>(0);
}
};
}
In diesem Fall werden die Werte 32 bzw. 0 mit Hilfe von Operatoren zur Typumwandlung in
Adressen umgewandelt. Während die Verwendung von 0 als Adresse möglich ist und somit für
die Umwandlung die Verwendung von static_cast ausreicht, muss 1 mit dem heftigsten“ al-

ler Typumwandlungsoperatoren, dem Operator reinterpret_cast, umgewandelt werden. Dies
ist zwar kein sehr guter Programmierstil, aber er funktioniert.
Das Beste ist, man vermeidet Funktionen zur automatischen Typumwandlung. Eine Element-
funktion zur expliziten Typumwandlung wie istOK() funktioniert ebenfalls und führt zudem zu
lesbarerem Anwendungscode:
if (f.istOK())
Die Standardklassen für I/O verwenden allerdings die gerade vorgestellte Technik mit der auto-
matischen Typumwandlung. Dabei wird auch der Operator ! überladen, um so etwas wie
if (! f)
zu ermöglichen. In Abschnitt 4.5.2 wurde bereits genauer darauf eingegangen.

6.2.4 Konstanten werden zu Variablen


Bei komplexen Klassen lohnt es sich mitunter, nicht jede Komponente, die von Interesse sein
könnte, bei der Initialisierung des Objekts zu berechnen. Erst wenn die Information auch wirklich
zum ersten Mal gebraucht wird, kann eine entsprechende Berechnung durchgeführt werden. Eine
derartige Programmiertechnik bezeichnet man als späte Auswertung (lazy evaluation).
Ein Beispiel liefert wieder die Klasse für geöffnete Dateien: Um festzustellen, wie viele Zei-
len eine Datei besitzt, muss die ganze Datei durchlaufen und die Anzahl der Zeilentrenner auf-
summiert werden. Dies kostet bei großen Dateien nicht unerheblich Zeit. Aus diesem Grund
bietet es sich an, die Anzahl der Zeilen zwar in einer internen Komponente festzuhalten, sie aber
erst dann zu ermitteln, wenn sie zum ersten Mal gebraucht wird:
Sandini Bib

386 Kapitel 6: Dynamische und statische Komponenten

namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei
int zeilen; // Zeilenanzahl (-1: noch nicht bekannt)

public:
// Konstruktor
Datei (...) : zeilen(-1) { // Zeilenanzahl zunächst nicht bekannt
...
}

int zeilenanzahl (); // liefert die Zeilenanzahl


...
};
}
...

int Datei::zeilenanzahl ()
{
// bei der ersten Nachfrage die Zeilenanzahl ermitteln
if (zeilen == -1) {
zeilen = zeilenanzahlErmitteln();
}
return zeilen;
}
Hier entsteht allerdings ein Problem: Die Zeilenanzahl kann nicht für konstante Objekte der
Klasse ermittelt werden:4
const Bsp::Datei f("prog.dat");

std::cout << f.zeilenanzahl(); // FEHLER: keine Konstanten-Elementfunktion


Das Problem ist, dass zeilenanzahl() keine Konstanten-Elementfunktion sein kann, da das
Objekt intern durch den Aufruf manipuliert wird. Eigentlich handelt es sich aber um eine Funk-
tion für ein logisch konstantes Objekt, denn das, was es repräsentiert, wird durch den Funktions-
aufruf ja nicht verändert.
Für solche Fälle wurde das Schlüsselwort mutable eingeführt. Immer wenn man eine Kom-
ponente hat, die für die logische Konstantheit eines Objekts keine Rolle spielt, kann man mit
mutable dafür sorgen, dass diese auch von Konstanten-Elementfunktionen verändert werden
darf.

4
Dieses Beispiel geht davon aus, dass die Konstantheit einer Datei bestimmt, ob schreibend zugegriffen
werden darf. Die Standarddatentypen für den Dateizugriff bieten verschiedene Datentypen für lesenden
und schreibenden Zugriff.
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 387

Damit könnte die Klasse Datei wie folgt deklariert werden:


namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei
mutable int zeilen; // Zeilenanzahl (-1: noch nicht bekannt)
// - neu: auch für Konstanten intern änderbar

public:
// Konstruktor
Datei (...) : zeilen (-1) { // Zeilenanzahl zunächst nicht bekannt
...
}

// Elementfunktion für Konstanten


int zeilenanzahl () const;
...
};
}
...

int Datei::zeilenanzahl () const


{
// bei der ersten Nachfrage Zeilenanzahl ermitteln
if (zeilen == -1) {
// OK: zeilen auch für konstante Objekte änderbar
zeilen = zeilenanzahlErmitteln();
}
return zeilen;
}
Nun ist die Ermittlung der Zeilenanzahl auch bei als konstant deklarierten Dateien möglich:
const Bsp::Datei f("prog.dat");

std::cout << f.zeilenanzahl(); // OK


Es gäbe auch noch andere Möglichkeiten, die Konstantheit von Variablen zu entfernen. Doch
handelt es sich dabei um eine explizite Umwandlung des Datentyps (siehe Seite 72). Insofern ist
diese Lösung sauberer.
mutable braucht man nicht nur für eine späte Auswertung. Wann immer trotz Manipulation
eine logische Konstantheit besteht, ist mutable angemessen. Ein anderes Beispiel wäre eine
Funktion, die während der Durchführung lokal in einem Objekt festhält, dass sich das Objekt
gerade in einer Operation befindet, die das Objekt allerdings nicht verändert. Auf diese Weise
kann man rekursive Aufrufe bei lesenden Zugriffen erkennen.
Sandini Bib

388 Kapitel 6: Dynamische und statische Komponenten

6.2.5 Vordefinierte Funktionen verbieten


Die Klasse für geöffnete Dateien ist ein gutes Beispiel für einen weiteren wichtigen Punkt, der
bei Klassen mit dynamischen Komponenten immer beachtet werden sollte.
Wie im vorherigen Abschnitt erläutert wurde, sind die Operatoren, die als Default für jede
Klasse vorhanden sind, für Klassen mit dynamischen Komponenten ungeeignet. So müssen in der
Regel ein eigener Copy-Konstruktor und ein eigener Zuweisungsoperator implementiert werden.
Wenn nun das Anlegen einer Kopie oder eine Zuweisung nicht sinnvoll ist, sollte nicht einfach
auf die Implementierung der Operatoren verzichtet werden, da dann die Gefahr besteht, dass sie
trotzdem verwendet werden. Besser ist es, die Operationen zu verbieten. Dies geschieht recht
einfach, indem sie als private deklariert werden.
Für die Klasse von geöffneten Dateien könnte das Kopieren und Zuweisen auf folgende Weise
unmöglich gemacht werden:
namespace Bsp {
class Datei {
private:
FILE* fp; // Zeiger auf die geöffnete Datei
...
public:
...
private:
Datei (const Datei&);
Datei& operator= (const Datei&);
};
}
Es reicht wirklich die Deklaration dieser Standardoperationen als private; eine Implementie-
rung ist nicht notwendig.
Eine Parameterübergabe ist dann natürlich nur noch mit Hilfe von Referenzen möglich:
void mitKopie (Bsp::Datei);
void ohneKopie (const Bsp::Datei&);
...
void f()
{
Bsp::Datei f("prog.dat");
Bsp::Datei g("prog.old");

ohneKopie(f); // OK
mitKopie(f); // FEHLER: Kopie anlegen nicht erlaubt
g = f; // FEHLER: Zuweisungen nicht erlaubt
}
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 389

6.2.6 Proxy-Klassen
Die meisten Probleme, die in diesem Abschnitt erläutert wurden, haben damit zu tun, dass man
die Kontrolle über die Operationen verliert, die für ein Objekt aufgerufen werden. Liefert man
einen Zeiger auf interne Daten, kann dieser zweckentfremdet werden. Liefert man über den In-
dexoperator eine Referenz auf ein Zeichen, hat man keine Kontrolle mehr, was mit dem Zeichen
im String passiert. So kann man Zeichen in einem String z.B. auch wie folgt manipulieren:
Bsp::String s("hallo");

++s[2]; // drittes Zeichen inkrementieren


Mit Hilfe einer Proxy-Klasse kann man in all diesen Fällen die Kontrolle behalten. Als Proxy
wird eine Kapsel bezeichnet, die die Kontrolle über etwas ermöglicht, über das man normaler-
weise keine Kontrolle hat, und dabei die Schnittstelle nicht verändert.
In der String-Klasse kann man den Indexoperator z.B. auch so implementieren, dass er einen
eigenen Datentyp zurückliefert, der sich soweit sinnvoll wie ein char verhält, dennoch aber nicht
alle Operationen zulässt. Dies sieht wie folgt aus:
class String {
public:
/* Proxy-Klasse für Zugriff auf einzelne Zeichen
*/
class reference {
friend class String; // String hat Zugriff auf private Komponenten
private:
char& ch; // interner Verweis auf ein Zeichen im String

// Konstruktor (nur für Klasse String aufrufbar)


reference(char& c) : ch(c) { // Verweis anlegen
}

reference(const reference&); // Kopieren verboten


public:

// Zuweisungen von char und von anderer Referenz sind OK


reference& operator= (char c) {
ch = c;
return *this;
}
reference& operator= (const reference& r) {
ch = r.ch;
return *this;
}
Sandini Bib

390 Kapitel 6: Dynamische und statische Komponenten

// Verwendung als char legt Kopie an


operator char() {
return ch;
}
};

public:
// Zugriff auf ein Zeichen im String
reference operator [] (unsigned);
char operator [] (unsigned) const;
...
};
Innerhalb der Klasse String wird die eingebettete Klasse reference definiert. Diese repräsen-
tiert Referenzen auf ein Zeichen in einem String.
Ein derartiges Objekt wird von dem Indexoperator für Variablen zurückgeliefert:
String::reference String::operator [] (unsigned idx)
{
// Index nicht im erlaubten Bereich ?
if (idx >= len) {
throw std::out_of_range("string index out of range");
}

return reference(buffer[idx]);
}
Bei der Initialisierung des Rückgabewerts wird dazu das Zeichen im String übergeben, auf das
verwiesen werden soll. Die Referenz-Komponente ch wird damit initialisiert. Auf diese Weise,
besteht auch weiterhin Kontrolle über die Operationen, die mit dem Rückgabewert des Index-
operators durchgeführt werden.
Die Verwendung dieser Referenz zeigt folgendes Beispiel:
// dyna/stringtest2.cpp
#include <iostream> // C++-Headerdatei für I/O
#include "string.hpp" // C++-Headerdatei für Strings

int main ()
{
typedef Bsp::String string;

// zwei Strings anlegen


string vorname = "Jicolai";
string nachname = "Nosuttis";
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 391

string name;

// die ersten Zeichen der Strings vertauschen


char c = vorname[0];
vorname[0] = nachname[0];
nachname[0] = c;

std::cout << vorname << ' ' << nachname << std::endl;
}
In der Anweisung
char c = vorname[0];
liefert der Ausdruck vorname[0] eine reference, die mit der Konvertierungsfunktion operator
char () (siehe Seite 234) automatisch in ein char umgewandelt werden kann.
Bei
vorname[0] = nachname[0];
liefern beide Seiten der Zuweisung eine Referenz. Der Zuweisungsoperator lässt dann eine ent-
sprechende Zuweisung zu.
Bei
nachname[0] = c;
wird die zweite Form des Zuweisungsoperators verwendet, die es erlaubt, einer String-Referenz
ein char zuzuweisen.
Das Kopieren und jede weitere Operation mit dem Rückgabewert des Indexoperators ist nicht
erlaubt:
++vorname[0]; // FEHLER, ++ für Referenzen nicht definiert
Nach diesem Muster kann man jederzeit auch geschachtelte Ausdrücke unter Kontrolle halten.
Man muss nur die Fähigkeiten von Rückgabewerten kontrollieren, indem man als Rückgabetyp
immer eine selbst definierte Klasse verwendet.

6.2.7 Ausnahmebehandlung mit Parametern


Wenn innerhalb einer Klasse eine Ausnahme ausgelöst wird, ist es oft sinnvoll, einem Fehlerob-
jekt als Parameter Daten über diesen Fehler mitzugeben. Beim Indexoperator von Strings ist es
sicherlich sinnvoll den fehlerhaften Index und auch den String, für den der Index ungültig war,
zu übergeben. Damit ist es sehr viel einfacher, eine Ausnahme sinnvoll auszuwerten.

Übergabe des fehlerhaften Index als Parameter


Die Klasse String (sie wird in Abschnitt 6.1 eingeführt und in Abschnitt 6.2.1 um den Operator
[ ] erweitert) könnte dazu eine eine spezielle Fehlerklasse definieren, in der die Fehlerobjekte
den fehlerhaften Index als Komponente besitzen:
Sandini Bib

392 Kapitel 6: Dynamische und statische Komponenten

// dyna/string3.hpp
namespace Bsp {
class String {
public:
// Fehlerklasse:
class RangeError {
public:
int index; // fehlerhafter Index

// Konstruktor (initialisiert index)


RangeError (int i) : index(i) {
}
};
...

// Operator [] für Variablen und Konstanten


char& operator [] (unsigned);
const char operator [] (unsigned) const;
};
}
Der Konstruktor der Klasse Bsp::String::RangeError stellt über die angegebene Initialisie-
rungsliste5 sicher, dass die Komponente index beim Erzeugen eines Fehlerobjekts mit dem als
Parameter i übergebenen fehlerhaften Index initialisiert wird.
In der Quelldatei der Klasse String muss bei einer Bereichsüberschreitung dann nur noch
ein entsprechendes Objekt als Ausnahme erzeugt werden, dem der fehlerhafte Index übergeben
wird:
// dyna/string3.cpp
/* Operator [] für Variablen
*/
char& String::operator [] (unsigned i)
{
// Index nicht im erlaubten Bereich?
if (i >= len) {
// Ausnahme mit fehlerhaftem Index auslösen
throw RangeError(i);
}

return cstring[i];
}

5
Initialisierungslisten werden in den Abschnitten 4.1.7 und 6.4.2 eingeführt.
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 393

/* Operator [] für Konstanten


*/
const char String::operator [] (unsigned i) const
{
// Index nicht im erlaubten Bereich?
if (i >= len) {
// Ausnahme mit fehlerhaftem Index auslösen
throw RangeError(i);
}

return cstring[i];
}
Wenn nun der Fehler auftritt, kann das Anwendungsprogramm nicht nur feststellen, dass der
Fehler aufgetreten ist, sondern auch den fehlerhaften Index auswerten. So wäre es z.B. möglich,
den fehlerhaften Index in einer Fehlermeldung auszugeben:
// dyna/stringtest3.cpp
int main()
{
try {
...
}
catch (const String::RangeError& error) {
// main() mit Fehlermeldung und Fehlerstatus beenden
std::cerr << "FEHLER: fehlerhafter Index " << error.index
<< " bei Zugriff auf String" << std::endl;
return EXIT_FAILURE;
}
}
Wie zu sehen ist, muss für das Objekt im Catch-Block wie bei Funktionen ein Name deklariert
werden, um auf die Komponenten zugreifen zu können. Man beachte außerdem, dass im Catch-
Bereich direkt auf die Komponente index im Fehlerobjekt zugegriffen wird. Aus diesem Grund
wird sie als public deklariert. Es ist nicht notwendig, die Komponente private zu deklarieren
und für den Zugriff eine Elementfunktion zu definieren, da das Fehlerobjekt nach der Auswertung
ohnehin zerstört wird.

Informationen über das Objekt, das einen Fehler auslöst


Will man als Parameter des Ausnahmeobjekts den String übergeben, der den Fehler ausgelöst
hat, ergibt sich ein Problem: Dieser String kann nur kopiert werden, da es sich um ein lokales
Objekt handeln kann, das im Gültigkeitsbereich einer Ausnahmebehandlung gar nicht mehr exis-
tiert. Man darf den String also nicht als Zeiger oder Referenz deklarieren, sondern muss in der
Ausnahmeklasse eine ganz normale Komponente vom Typ String deklarieren:
Sandini Bib

394 Kapitel 6: Dynamische und statische Komponenten

// dyna/string4.hpp
namespace Bsp {
class String {
public:
// Fehlerklasse:
class RangeError {
public:
int index; // fehlerhafter Index
String value; // String dazu

// Konstruktor (initialisiert index)


RangeError (String s, int i) : value(s), index(i) {
}
};
...
};
}
Im Indexoperator werden nun bei einem falschen Index sowohl der String als auch der fehlerhafte
Index zur Initialisierung des Ausnahmeobjekts übergeben:
// dyna/string4.cpp
/* Operator [] für Variablen
*/
char& String::operator [] (unsigned i)
{
// Index nicht im erlaubten Bereich ?
if (i >= len) {
/* Ausnahme:
* - neu: String selbst und fehlerhaften Index übergeben
*/
throw RangeError(*this,i);
}

return cstring[i];
}

/* Operator [] für Konstanten


*/
const char String::operator [] (unsigned i) const
{
// Index nicht im erlaubten Bereich ?
Sandini Bib

6.2 Weitere Aspekte dynamischer Komponenten 395

if (i >= len) {
/* Ausnahme:
* - neu: String selbst und fehlerhaften Index übergeben
*/
throw RangeError(*this,i);
}

return cstring[i];
}
Eine Auswertung kann nun auf beide Komponenten zugreifen:
// dyna/stringtest4.cpp
int main()
{
try {
...
}
catch (const String::RangeError& error) {
// main() mit Fehlermeldung und Fehlerstatus beenden
std::cerr << "FEHLER: fehlerhafter Index " << error.index
<< " bei Zugriff auf String \"" << error.string
<< "\"" << std::endl;
return EXIT_FAILURE;
}
}
Man beachte, dass durch die Tatsache, dass das Objekt, das eine Ausnahme auslöst, nicht als
Parameter der Ausnahme übergeben werden kann, man auch nicht mehr die Identität des Objekts
feststellen kann. Wird diese Möglichkeit benötigt, muss man entweder Objekt-IDs durchreichen
(siehe Seite 418) oder die Adresse des betroffenen Objekts als Zeiger vom Typ const void*
durchreichen. Dann kann man beim Behandeln dieser Ausnahme diese Adresse mit den Adressen
der bekannten Objekte vergleichen.

6.2.8 Zusammenfassung
 Funktionen können für variable und konstante Objekte unterschiedlich überladen werden.
 Liefert eine Elementfunktion die Möglichkeit zu einem schreibenden Zugriff auf interne
Komponenten, muss sichergestellt werden, dass die Funktion nicht für Konstanten aufge-
rufen werden kann.
 Funktionen, die dynamische Komponenten in einen anderen Typ konvertieren, können mit
unterschiedlichen Vor- und Nachteilen eine Kopie oder eine Konstante zurückliefern.
Sandini Bib

396 Kapitel 6: Dynamische und statische Komponenten

 Für Bedingungen in Kontrollstrukturen können automatische Typumwandlungen definiert


werden, die es erlauben, ein Objekt direkt als Bedingung zu verwenden. Dies sollte mit Vor-
sicht eingesetzt werden. Insbesondere sollte damit kein Zugriff auf interne Komponenten
möglich sein.
 Das Schlüsselwort mutable erlaubt die Modifikation einer Komponente auch für Konstanten-
Elementfunktionen. Semantisch bedeutet das, dass die Komponente für die logische Konstant-
heit unerheblich ist.
 Default-Funktionen, die nicht sinnvoll sind, sollten ausdrücklich verboten werden. Dazu reicht
es, sie als private zu deklarieren.
 Proxy-Klassen erlauben es, auch in geschachtelten Operationen die Kontrolle zu behalten.
 Ausnahmeobjekte können Komponenten besitzen. Wird dabei das Objekt übergeben, das eine
Ausnahme auslöst, muss davon eine Kopie angelegt werden.
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 397

6.3 Vererbung von Klassen mit dynamischen


Komponenten
Dieser Abschnitt geht auf einige weitere Punkte ein, die beim Ableiten von Klassen beachtet
werden müssen. Dazu gehören vor allem Aspekte, die bei Klassen mit dynamischen Komponen-
ten eine Rolle spielen. Betrachtet wird auch die Problematik beim Überschreiben von Friend-
Funktionen.
Verdeutlicht werden diese Aspekte am Beispiel einer Ableitung der Klasse Bsp::String.
Diese wird um die Eigenschaft erweitert, eine Farbe zu besitzen. Als Datentyp für die Farbe wird
ebenfalls die Klasse Bsp::String verwendet. Auf diese Weise ist die abgeleitete Klasse auch
gleich eine Anwendung der Basisklasse.

6.3.1 Die Klasse Bsp::String als Basisklasse


Damit die Klasse Bsp::String abgeleitet werden kann, muss sie so implementiert werden, dass
sie zur Vererbung geeignet ist (die Standard-String-Klasse std::string ist nicht zur Vererbung
geeignet). Ausgehend von der Version der Klasse Bsp::String aus Abschnitt Abschnitt 6.1
(ohne Indexoperator) muss die Klassendeklaration dazu wie folgt geändert werden:
 Die privaten Komponenten erhalten das Schlüsselwort protected (der private Konstruktor
für den Summenstring bleibt allerdings privat).
 Die Funktionen zum Einlesen und Ausgeben werden virtual deklariert.
 Der Destruktor wird als virtual deklariert.
Daraus ergibt sich für die Basisklasse Bsp::String folgende Headerdatei:
// dyna/string5.hpp
#ifndef STRING_HPP
#define STRING_HPP

// Headerdatei für I/O


#include <iostream>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class String {

protected:
char* buffer; // Zeichenfolge als dynamisches Feld (Array)
unsigned len; // aktuelle Anzahl an Zeichen
unsigned size; // Speicherplatzgröße von buffer

public:
Sandini Bib

398 Kapitel 6: Dynamische und statische Komponenten

// Default- und char*-Konstruktor


String (const char* = "");

// Aufgrund dynamischer Komponenten:


String (const String&); // Copy-Konstruktor
String& operator= (const String&); // Zuweisung
virtual ~String(); // Destruktor (neu: virtuell)

// Vergleichen von Strings


friend bool operator== (const String&, const String&);
friend bool operator!= (const String&, const String&);

// Hintereinanderhängen von Strings


friend String operator+ (const String&, const String&);

// Ausgabe mit Streams


virtual void printOn (std::ostream&) const;

// Eingabe mit Streams


virtual void scanFrom (std::istream&);

// Anzahl der Zeichen


// Beachte: darf beim Ableiten nicht überschrieben werden
unsigned length () const {
return len;
}

private:
/* Konstruktor aus Länge und Puffer
* - intern für Operator +
*/
String (unsigned, char*);
};

// Standard-Ausgabeoperator
inline std::ostream& operator << (std::ostream& strm, const String& s)
{
s.printOn(strm); // String auf Stream ausgeben
return strm; // Stream zurückliefern
}
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 399

// Standard-Eingabeoperator
inline std::istream& operator >> (std::istream& strm, String& s)
{
s.scanFrom(strm); // String von Stream einlesen
return strm; // Stream zurückliefern
}

/* Operator !=
* - als Umsetzung auf Operator == inline implementiert
*/
inline bool operator!= (const String& s1, const String& s2) {
return !(s1==s2);
}

} // **** ENDE Namespace Bsp ********************************

#endif // STRING_HPP

Virtuell oder nicht virtuell ?


Bemerkenswert ist die Tatsache, dass die Elementfunktion length() nicht als virtuell deklariert
wurde. Dies ist eine Design-Entscheidung zugunsten besseren Laufzeitverhaltens. Sie bedeutet,
dass es Probleme gibt, wenn eine abgeleitete Klasse diese Funktion überschreibt und ein Objekt
der abgeleiteten Klassen dann unter der Basisklasse verwendet wird. Da das Abfragen der An-
zahl der Zeichen eigentlich nicht anders implementiert werden kann, ist die damit verbundene
Einschränkung aber vertretbar (man könnte natürlich die Bedeutung der Komponente buffer
außer Kraft setzen; dies wäre allerdings eine Verletzung der Grundregel, dass Komponenten in
abgeleiteten Klassen ihre Bedeutung behalten sollten, siehe Seite 351). Der Vorteil besteht dafür
darin, dass dieser Funktionsaufruf wirklich inline ersetzt werden kann. Bei virtuellen Funktionen
muss Code generiert werden, der zur Laufzeit den tatsächlichen Datentyp prüft und dann auch
eine Funktion aufruft.

6.3.2 Die abgeleitete Klasse FarbString


Abgeleitet wird die String-Klasse als String, der eine bestimmte Farbe besitzt. Entsprechend
heißt die abgeleitete Klasse FarbString. Hinzu kommt als zusätzliche Komponente eine Farbe,
die dem String jeweils zugeordnet wird.
Wie schon erwähnt wurde, ist die Farbe selbst ein Objekt der Klasse Bsp::String. Man hätte
auch einen Aufzählungstyp verwenden können, doch diese Variante ermöglicht es, gleichzeitig
die Verwendung der String-Klasse als Basisklasse und als angewendete Klasse zu verdeutlichen.

Headerdatei der Klasse FarbString


Die Headerdatei der von String abgeleiteten Klasse FarbString hat folgenden Aufbau, der
anschließend noch im Einzelnen erläutert wird:
Sandini Bib

400 Kapitel 6: Dynamische und statische Komponenten

// dyna/fstring1.hpp
#ifndef FARBSTRING_HPP
#define FARBSTRING_HPP

// Headerdatei der Basisklasse


#include "string.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Klasse FarbString
* - abgeleitet von String
*/
class FarbString : public String {
protected:
String farb; // Farbe, die der String besitzt

public:
// Default-, String- und String/String-Konstruktor
FarbString (const String& s = "", const String& f = "black")
: String(s), farb(f) {
}

// Farbe abfragen und setzen


const String& farbe () {
return farb;
}
void farbe (const String& neueFarbe) {
farb = neueFarbe;
}

// Ein- und Ausgabe mit Streams


virtual void printOn (std::ostream&) const;
virtual void scanFrom (std::istream&);

// Vergleichen von FarbStrings


friend bool operator== (const FarbString& s1,
const FarbString& s2) {
return static_cast<const String&>(s1)
== static_cast<const String&>(s2)
&& s1.farb == s2.farb;
}
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 401

friend bool operator!= (const FarbString& s1,


const FarbString& s2) {
return !(s1==s2);
}
};

} // **** ENDE Namespace Bsp ********************************

#endif // FARBSTRING_HPP
Zunächst wird die neue Komponente für die Farbe definiert. Da das Symbol farbe als Funktion
zum Setzen und Abfragen der Farbe verwendet wird, trägt die Komponente den Namen farb
(sinnvolle Namen bei Schnittstellen sollten vorgehen):6
class FarbString : public String {
protected:
String farb; // Farbe, die der String besitzt
...
};
Als Nächstes wird der Konstruktor definiert. Als Parameter können dabei bis zu zwei Strings
übergeben werden. Der erste String ist die Zeichenfolge (Default: Leerstring), und der zweite
String ist die Farbe (Default: "black"):
class FarbString : public String {
...
public:
// Default-, String- und String/String-Konstruktor
FarbString (const String& s = "", const String& f = "black")
: String(s), farb(f) {
}
...
};
In der Initialisierungsliste wird der erste String s als initiale Zeichenfolge an die Basisklasse
String hochgereicht. Mit der als zweiten Parameter übergebenen Farbe f wird die Komponente
farb initialisiert.
Die Elementfunktion farbe() wurde so überladen, dass sie sowohl zum Setzen als auch zum
Abfragen der Farbe verwendet werden kann. Dies ist eine gängige Technik in C++. Wird kein
Parameter übergeben, liefert sie die aktuelle Farbe, wird ein Parameter übergeben, ist dies die
neue Farbe:

6
Es gibt keine generell übliche Lösung für das Dilemma des Namenskonflikts zwischen einer einfachen
Komponente und der Elementfunktion, die diese Komponente setzt und abfragt. Manchmal wird vor die
Komponente ein Unterstrich gesetzt, manchmal wird sie einfach abgekürzt und manchmal beginnen die
Elementfunktionen mit Präfixen wie get und set.
Sandini Bib

402 Kapitel 6: Dynamische und statische Komponenten

class FarbString : public String {


...
public:
const String& farbe () { // Farbe abfragen
return farb;
}
void farbe (const String& neueFarbe) { // Farbe setzen
farb = neueFarbe;
}
...
};

Virtuell oder nicht virtuell ?


Auch die Elementfunktionen zum Setzen und Abfragen der Farbe werden in diesem Fall nicht
virtuell definiert. Auch hier gilt, dass sie damit beim Ableiten nicht zum Überschreiben geeignet
sind. Dies ist erneut eine Design-Entscheidung zugunsten besseren Laufzeitverhaltens. Da das
Setzen und Abfragen der Farbe eigentlich nicht anders implementiert werden kann, ist die damit
verbundene Einschränkung vertretbar. Es bedeutet aber, dass, wenn in abgeleiteten Klassen beim
Ändern der Farbe andere Dinge anzupassen sind, dies nicht mehr möglich ist (oder zumindest
nur um den Preis, dass in Programmen keine Zeiger oder Referenzen auf Basisklassen verwendet
werden sollten).
Die Funktionen zum Einlesen und Ausgeben werden dagegen, wie auch schon in der Basis-
klasse, virtuell definiert, damit jeweils die richtige Eingabe- bzw. Ausgabefunktion aufgerufen
wird:
class FarbString : public String {
...
public:
virtual void printOn (std::ostream&) const;
virtual void scanFrom (std::istream&);
...
};

6.3.3 Ableiten von Friend-Funktionen


Um eine automatische Typumwandlung auch für den ersten Operator zu ermöglichen, wurden
die Operatoren zum Testen auf Gleichheit und Ungleichheit bereits in der Basisklasse String
als globale Friend-Funktionen definiert. Sie müssen in der abgeleiteten Klasse allerdings über-
schrieben werden, da neben den Zeichenfolgen auch die Farben von zwei FarbStrings gleich
sein müssen.
Um den Test auf Gleichheit so einfach wie möglich zu implementieren, wird er auf die Im-
plementierung der Basisklasse zurückgeführt und um den Test auf Gleichheit für die neue Kom-
ponente ergänzt. Da es sich aber um eine globale Funktion handelt, kann dies nicht über den
Bereichsoperator, sondern nur über eine explizite Typumwandlung realisiert werden:
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 403

class FarbString : public String {


...
public:
friend bool operator== (const FarbString& s1,
const FarbString& s2) {
return static_cast<const String&>(s1)
== static_cast<const String&>(s2)
&& s1.farb == s2.farb;
}
...
};
Mit dem Operator static_cast<> werden die Parameter s1 und s2 explizit in den Typ const
String& umgewandelt. Damit wird sichergestellt, dass der Vergleichsoperator für den Datentyp
der Basisklasse String aufgerufen wird. Hinzu kommt der zusätzliche Test auf Gleichheit für
die Farbe, der ebenfalls zum Aufruf des Vergleichsoperators der Klasse String führt, da die
Komponente farb diesen Typ besitzt.
Es gibt nun also eine Vergleichsoperation für zwei Strings und zwei FarbStrings. Was pas-
siert aber, wenn ein String mit einem FarbString verglichen wird? In diesem Fall gibt es zwei
Möglichkeiten:
 Der FarbString wird implizit in einen String umgewandelt.
Dies ist möglich, da jedes Objekt einer abgeleiteten Klasse grundsätzlich als Objekt der
Basisklasse verwendet werden kann und somit eine automatische Typumwandlung von
FarbString nach String definiert ist.
 Der String wird in einen FarbString umgewandelt.
Dies ist möglich, da es für die Klasse FarbString einen Konstruktor gibt, dem ein String
als Parameter übergeben werden kann. Dieser Konstruktor definiert automatisch eine entspre-
chende Typumwandlung.
Man könnte nun meinen, damit sei der Ausdruck mehrdeutig und somit nicht übersetzbar. In C++
hat eine von der Sprache vordefinierte automatische Typumwandlung aber eine höhere Priorität
als eine, die durch vom Anwender definierte Funktionen (Konstruktoren, Konvertierungsfunktio-
nen) ermöglicht wird (siehe auch Seite 575). Also wird der Vergleich für zwei Strings aufgerufen.

Friend und virtual


Friend-Funktionen sind grundsätzlich nicht virtuell. Beim Aufruf einer Friend-Funktion wird
auch bei Zeigern und Referenzen immer die Funktion aufgerufen, die für den Typ der Parameter
definiert ist.
Dies bedeutet, dass der Vergleichsoperator für den Datentyp String aufgerufen wird, wenn
zwei FarbStrings über Zeiger oder Referenzen auf Strings verglichen werden. Dies ist der Preis
für die Möglichkeit der automatischen Typumwandlung für den ersten Operanden als Friend-
Funktion.
Sandini Bib

404 Kapitel 6: Dynamische und statische Komponenten

Wer will, dass in Abhängigkeit vom Typ zur Laufzeit die richtige Funktion aufgerufen wird, sollte
den Operator als Elementfunktion definieren. Zusätzlich kann ja ein globaler Operator definiert
werden, der ein Objekt vom Typ char* mit einem String bzw. mit einem FarbString vergleicht.
Eine andere Möglichkeit wäre die Implementierung des Operators als globale Funktion, die keine
Friend-Funktion ist und stattdessen eine interne Hilfsfunktion aufruft (dazu folgt gleich noch ein
Beispiel).
Da Friend-Funktionen nicht virtuell sein können, muss auch der Test auf Ungleichheit erneut
implementiert werden, obwohl er genauso wie in der Basisklasse implementiert wird. Er liefert
einfach das negierte Ergebnis des Tests auf Gleichheit:
class FarbString : public String {
...
public:
friend bool operator!= (const FarbString& s1,
const FarbString& s2) {
return !(s1==s2);
}
...
};
Da die Implementierung der Basisklasse keine virtuelle Funktion ist, ruft ihr Test auf Ungleich-
heit immer den Test auf Gleichheit für Objekte vom Typ String auf. Dies würde dann auch für
FarbStrings gelten. Die Farbe würde also keine Rolle mehr spielen. Aus diesem Grund muss
der Operator erneut implementiert werden. Auch hier gilt, dass bei Zeigern oder Referenzen auf
Strings unabhängig vom tatsächlichen Datentyp immer die String-Funktion aufgerufen wird.

Vermeidung von Friend-Funktionen


Grundsätzlich bleibt also festzuhalten, dass Friend-Funktionen bei der Vererbung problematisch
sind. Sie werden nicht vererbt, können aber für Objekte der abgeleiteten Klasse aufgerufen wer-
den. Dabei werden die Objekte aber zu Objekten der Basisklasse, was beim Aufruf von Hilfs-
funktionen in den Friend-Funktionen zum Aufruf der falschen Funktionen führen kann.
Es ist auch nicht erlaubt, eine Operation sowohl als Elementfunktion als auch als Friend-
Funktion zu definieren:
class String {
...
public:
bool operator== (const String& s2);
friend bool operator== (const String& s1,
const String& s2); // FEHLER
...
};
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 405

Sofern eine Klasse zur Vererbung vorgesehen ist, sollte deshalb auf Friend-Funktionen grund-
sätzlich verzichtet werden. Die automatische Typumwandlung für den ersten Operanden kann
man auch durch zusätzliche globale Hilfsfunktionen ermöglichen.
Eine andere Möglichkeit ist die Technik, die auch schon bei den Ein- und Ausgabeopera-
toren angewendet wird. Man definiert den Operator als globale Funktion, die nur eine virtuelle
Elementfunktion aufruft, die die eigentliche Arbeit übernimmt. Dies sähe in der Basisklasse z.B.
wie folgt aus:
class String {
...
public:
virtual bool equals (const String& s2);
...
};

inline bool operator== (const String& s1, const String& s2)


{
return s1.equals(s2);
}
inline bool operator!= (const String& s1, const String& s2)
{
return !s1.equals(s2);
}
Eine abgeleitete Klasse muss dann nur noch die neue Version von equals() implementieren:
class FarbString : public String {
...
public:
virtual bool equals (const FarbString& s2) {
return String::equals(s2) && farb == s2.farb;
}
...
};

6.3.4 Quelldatei der abgeleiteten Klasse FarbString


Implementiert wird der Zuweisungsoperator in der Quelldatei der abgeleiteten Klasse Farb-
String . Auch hier macht es in der Regel Sinn, die Implementierung der Basisklasse aufzurufen
und die neuen Komponenten zusätzlich zuzuweisen. Hinzu kommen die relativ trivialen Imple-
mentierungen der Funktionen zum Einlesen und Ausgeben:
Sandini Bib

406 Kapitel 6: Dynamische und statische Komponenten

// dyna/fstring1.cpp
// Headerdatei der eigenen Klasse
#include "fstring.hpp"

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* Ausgabe auf Stream


*/
void FarbString::printOn (std::ostream& strm) const
{
// Zeichenfolge mit Farbe in Klammern ausgeben
String::printOn(strm);
strm << " (in " << farb << ')';
}

/* Einlesen eines FarbStrings von einem Input-Stream


*/
void FarbString::scanFrom (std::istream& strm)
{
// Inhalt und Farbe nacheinander einlesen
String::scanFrom (strm);
farb.scanFrom (strm);
}

} // **** ENDE Namespace Bsp ********************************

Die Funktion zum Einlesen liest einfach nur zwei Strings für den Inhalt und die Farbe ein. Ei-
ne aufwändigere Implementierung könnte die Farbe als optionale Angabe interpretieren, indem
öffnende und schließende Klammern entsprechend ausgewertet werden.

6.3.5 Anwendung der Klasse FarbString


Angewendet wird die Klasse FarbString wie jede andere Klasse auch, wie das folgende Bei-
spiel zeigt:
// dyna/fstest1.cpp
// Headerdatei für I/O
#include <iostream.h>

// Headerdatei für die Klasse FarbString


#include "fstring.hpp"
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 407

int main ()
{
Bsp::FarbString f("hallo"); // FarbString mit Default-Farbe
Bsp::FarbString r("rot","red"); // FarbString mit Rot als Farbe

std::cout << f << " " << r << std::endl; // FarbStrings ausgeben

f.farbe("green"); // Farbe von f auf Grün setzen

std::cout << f << " " << r << std::endl; // FarbStrings ausgeben

std::cout << "Summenstring: " << f + r << std::endl;


}
Das Programm liefert als Ausgabe:
hallo (in black) rot (in red)
hallo (in green) rot (in red)
Summenstring: hallorot
Die Addition wurde für FarbStrings nicht neu implementiert. Welche Farbe sollte der Sum-
menstring auch besitzen? Da FarbStrings aber als Strings verwendet werden können, ist eine
Addition möglich, wobei die Implementierung der Basisklasse aufgerufen wird. Der Rückgabe-
wert ist damit ein String und kein FarbString, wie die Ausgabe des Programms auch zeigt.

6.3.6 Ableiten der Spezialfunktionen für dynamische Komponenten


Man mag sich fragen, warum in der Klasse FarbString nicht auch die speziellen Funktionen für
dynamische Komponenten erneut implementiert wurden. Schließlich bestand in der Basisklasse
die Notwendigkeit, deren Default-Implementierungen durch eigene Implementierungen zu erset-
zen, und die Komponenten wurden ja geerbt. Die Default-Implementierungen sind allerdings so
definiert, dass diese Notwendigkeit nicht besteht:
 Der Copy-Konstruktor wird wie alle Konstruktoren verkettet top-down aufgerufen. Dabei
ruft der Default-Copy-Konstruktor einer abgeleiteten Klasse automatisch den gültigen Copy-
Konstruktor der Basisklasse auf. Der Copy-Konstruktor der Basisklasse kümmert sich also
automatisch um die Behandlung der geerbten dynamischen Komponenten.
Neu hinzugekommene Komponenten werden komponentenweise kopiert. In diesem Fall
wird also außerdem automatisch der String farb kopiert. Da es sich um einen Datentyp mit
selbst definiertem Copy-Konstruktor handelt, wird dieser automatisch aufgerufen.
 Der Destruktor wird wie alle Destruktoren verkettet bottom-up aufgerufen. Damit werden
alle Aufräumungsarbeiten der Basisklasse automatisch durchgeführt. Da der Destruktor der
Basisklasse virtuell ist, sind auch die Destruktoren aller abgeleiteten Klassen automatisch
virtuell.
Sandini Bib

408 Kapitel 6: Dynamische und statische Komponenten

 Für den Zuweisungsoperator gilt Entsprechendes. Der generierte Default-Zuweisungsopera-


tor weist zunächst die geerbten Komponenten zu, indem er den Zuweisungsoperators der
Basisklasse aufruft. Anschließend werden dann alle neuen Komponenten komponentenweise
zugewiesen. Damit wird für die geerbten Komponenten der selbst implementierte Zuwei-
sungsoperator und für farb ebenfalls der selbst implementierte Zuweisungsoperator auto-
matisch aufgerufen.
Wie man sieht, behandeln die generierten Default-Operationen den Sachverhalt, dass man ge-
gebenenfalls dynamische Komponenten erbt, ausreichend gut. Dies ist wichtig, da man damit in
einer Basisklasse auch später erst dynamische Komponenten einführen kann, ohne dass damit
alle abgeleiteten Klassen geändert werden müssen.
Für den Fall, dass man einen Copy-Konstruktor einmal in einer abgeleiteten Klasse imple-
mentieren muss, sei hier exemplarisch gezeigt, wie dieser für die Klasse FarbString aussehen
müsste: Mit Hilfe einer Initialisierungsliste wird einfach der Copy-Konstruktor der Basisklasse
aufgerufen, und dann wird jede neue Komponenten kopiert:
class FarbString : public String {
...
public:
FarbString (const FarbString& s) // Copy-Konstruktor
: String(s), farb(s.farb) {
}
...
};
Entsprechend sieht die Implementierung eines eigenen Zuweisungsoperators aus. Zunächst wird
der Zuweisungsoperator der Basisklasse aufgerufen, und dann werden alle neuen Komponenten
zugewiesen:
FarbString& FarbString::operator= (const FarbString& s)
{
// Zuweisung eines FarbStrings an sich selbst hat keinen Effekt
if (this == &s) {
return *this; // FarbString zurückliefern
}

// Zuweisungsoperator der Basisklasse aufrufen


String::operator = (s);

// neue Komponenten zuweisen


farb = s.farb;

return *this; // geänderten FarbString zurückliefern


}
Sandini Bib

6.3 Vererbung von Klassen mit dynamischen Komponenten 409

6.3.7 Zusammenfassung
 Friend-Funktionen können niemals virtuell sein.
 Aus diesem Grund sind Friend-Funktionen bei Vererbung problematisch und sollten vermie-
den werden.
 Ist ein Destruktor in der Basisklasse virtuell, ist er das auch in allen davon abgeleiteten Klas-
sen.
 Sind in einer Basisklasse Copy-Konstruktor, Destruktor und Zuweisungsoperator notwen-
dig, müssen diese nicht in einer davon abgeleiteten Klasse notwendig sein. Die Default-
Implementierungen dieser Operationen in abgeleiteten Klassen rufen die selbst definierten
Implementierungen der Basisklasse automatisch auf.
Sandini Bib

410 Kapitel 6: Dynamische und statische Komponenten

6.4 Klassen verwenden Klassen


In diesem Abschnitt wird eine Klasse für eine einfache Personenverwaltung eingeführt, anhand
derer hier und im folgenden Abschnitt einige weitere Eigenschaften von Klassen vorgestellt wer-
den.
In diesem Abschnitt wird dabei zunächst noch einmal deutlich gemacht, was passiert, wenn
Objekte aus anderen Objekten bestehen. Damit werden auch die Vorteile von Initialisierungslis-
ten erläutert.

6.4.1 Objekte als Komponenten anderer Klassen


In den bisherigen Beispielen waren die Komponenten einer Klasse meistens fundamentale Da-
tentypen wie int und char. Klassen können aber natürlich auch selbst wieder in anderen Klassen
Verwendung finden. String-Klassen sind das beste Beispiel dafür, da Komponenten von Objekten
sehr häufig Strings sind.
In einem solchen Fall werden mit der Initialisierung eines Objekts auch Teilobjekte (Sub-
objekte) initialisiert, aus denen das Objekt besteht. Das bedeutet, dass mehrere Konstruktoren
aufgerufen werden. Dieser Umstand soll nachfolgend genauer behandelt werden.

Konstruktoren rufen Konstruktoren auf


Wenn Objekte einer Klasse aus Objekten anderer Klassen bestehen, werden bei der Initialisie-
rung solcher Objekte gleich mehrere Konstruktoren aufgerufen. Zusätzlich zum Konstruktor des
Gesamtobjekts müssen die Objekte, aus denen dieses besteht, initialisiert werden.
Dabei gilt:
 Bevor die Anweisungen im Rumpf eines Konstruktors ausgeführt werden, werden erst die
Konstruktoren für die Komponenten des Objekts aufgerufen.
Diese Reihenfolge ist auch vernünftig, da nur der Konstruktor des Gesamtobjekts die Bedeutung
der Teilobjekte kennt und somit die Möglichkeit haben muss, die Startzustände der Teilobjekte
auszuwerten oder gegebenenfalls zu korrigieren.
Darüber hinaus ist definiert, dass die Reihenfolge beim Aufruf der Konstruktoren für die
Komponenten, aus denen ein Objekt besteht, der Deklarationsreihenfolge entspricht. Die Rei-
henfolge wird nicht durch die Initialisierungsliste definiert. Diese Tatsache sollte allerdings nicht
ausgenutzt werden, da sonst völlig unlesbare Programme entstehen. Es würde bedeuten, dass
durch ein Vertauschen der Deklarationsreihenfolge das Programmverhalten geändert wird. Die
Reihenfolge wird nur definiert, um sicherzustellen, dass die Destruktoren immer in umgekehrter
Reihenfolge aufgerufen werden, was z.B. für die Implementierung einer eigenen Speicherver-
waltung wichtig sein kann.

6.4.2 Implementierung der Klasse Person


Die Verwendung von Objekten anderer Klassen wird am Beispiel einer Klasse Person betrach-
tet. Eine Person soll dabei zunächst nur aus einem Vor- und Nachnamen bestehen, die jeweils Ob-
jekte einer String-Klasse sind. Als String-Klasse wird die Standard-String-Klasse std::string
verwendet. Sofern aber das interne Verhalten von Strings beschrieben wird, wird dies am inneren
Aufbau von Objekten der in diesem Kapitel eingeführten Klasse Bsp::String erläutert.
Sandini Bib

6.4 Klassen verwenden Klassen 411

Die Headerdatei der Klasse Person hat folgenden Aufbau:


// dyna/person1.hpp
#ifndef PERSON_HPP
#define PERSON_HPP

// Headerdateien für Hilfsklassen


#include <string>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Person {
private:
std::string vname; // Vorname der Person
std::string nname; // Nachname der Person

public:
// Konstruktor aus Nachname und optional Vorname
Person (const std::string&, const std::string& = "");

// Abfragen von Eigenschaften


const std::string& nachname () const { // Nachname liefern
return nname;
}
const std::string& vorname () const { // Vorname liefern
return vname;
}

// vergleichen
bool operator == (const Person& p) const {
return vname == p.vname && nname == p.nname;
}
bool operator != (const Person& p) const {
return vname != p.vname || nname != p.nname;
}
...
};

} // **** ENDE Namespace Bsp ********************************

#endif // PERSON_HPP
Sandini Bib

412 Kapitel 6: Dynamische und statische Komponenten

An dieser Stelle sei zunächst darauf hingewiesen, dass es sich bei der Klasse Person um keine
Klasse mit dynamischen Komponenten handelt, obwohl sie aus Teilobjekten besteht, die dynami-
sche Komponenten besitzen. Die Probleme dynamischer Komponenten sind nämlich erfolgreich
auf die Teilobjekte abgewälzt worden. Dort wurde dafür gesorgt, dass die Zuweisung, der Copy-
Konstruktor und der Destruktor korrekt arbeiten. Die String-Klasse kann nun wie der Datentyp
int verwendet werden. Wenn eine Person einer anderen Person zugewiesen wird, kann deshalb
die Default-Implementierung, die komponentenweise zuweist, verwendet werden. Das gleiche
gilt für das Anlegen einer Kopie. Entsprechend werden bei der Zerstörung eines Objekts der
Klasse Person automatisch die Destruktoren der Teilobjekte aufgerufen.
Der Konstruktor ist in der Quelldatei definiert und initialisiert die Person einfach mit den
übergebenen Parametern:
// dyna/person1.cpp
/* Konstruktor aus Nachname und Vorname
* - Default für Vorname: ""
*/
Person::Person (const std::string& nn, const std::string& vn)
: nname(nn), vname(vn) // Vor- und Nachname anhand übergebener
// Parameter initialisieren
{
// mehr ist nicht zu tun
}

Anlegen einer Person in Einzelschritten


Wenn nun eine Person durch
Bsp::Person nico ("Josuttis", "Nicolai");
angelegt wird, geschieht durch die Verwendung der Initialisierungsliste Folgendes (dabei wird
davon ausgegangen, dass die Klasse std::string genauso wie die Klasse Bsp::String aus
Abschnitt 6.1.1 implementiert ist):
 Zunächst wird Speicherplatz für das Objekt angelegt:

v n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?

n n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?
Sandini Bib

6.4 Klassen verwenden Klassen 413

 Dann wird für die Komponente vname der Copy-Konstruktor der String-Klasse aufgerufen,
da zur Initialisierung jetzt der String vn übergeben wird. Dieser initialisiert den Vornamen
mit dem übergebenen String (siehe Abschnitt 6.1.3):

v n a m e :
b u f f e r : ' N ' ' i ' ' c ' ' o ' ' l ' ' a ' ' i '
l e n : 7
s i z e : 7

n n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?

Dass zunächst vname initialisiert wird, liegt nicht an der Initialisierungsliste, sondern an der
Tatsache, dass die Komponente vname in der Klasse Person vor der Komponente nname
deklariert wird.
 Anschließend wird für die Komponente nname ebenfalls der Copy-Konstruktor der String-
Klasse aufgerufen, da zur Initialisierung der String nn übergeben wird:

v n a m e :
b u f f e r : ' N ' ' i ' ' c ' ' o ' ' l ' ' a ' ' i '
l e n : 7
s i z e : 7

n n a m e :
b u f f e r : ' J ' ' o ' ' s ' ' u ' ' t ' ' t ' ' i ' ' s '
l e n : 8
s i z e : 8

 Schließlich werden die Anweisungen des Konstruktors der Klasse Person ausgeführt. Doch
da die Komponenten bereits vollständig initialisiert sind, ist in diesem Fall gar nichts mehr
zu tun (was durchaus nicht untypisch ist).
Man beachte, dass eine Initialisierungsliste nicht Teil der Deklaration, sondern Teil der Imple-
mentierung einer Funktion ist.
Da oft nach der Initialisierung gar keine Anweisungen mehr durchgeführt werden müssen,
wird der Konstruktor in der Praxis häufig gleich als Inline-Funktion in der Headerdatei definiert:
namespace Bsp {
class Person {
...
public:
Sandini Bib

414 Kapitel 6: Dynamische und statische Komponenten

Person (const std::string& nn, const std::string& vn = "")


: nname(nn), vname(vn) {
}
...
};
}

Einzelschritte ohne Initialisierungslisten


An dieser Stelle kann man auch verdeutlichen, warum die Verwendung von Initialisierungslisten
wichtig ist. Stellen wir uns vor, wir hätten den Konstruktor von Person so implementiert, dass
keine Initialisierungslisten verwendet werden:
/* Konstruktor aus Nachname und Vorname
* - schlecht: ohne Initialisierungsliste
*/
Person::Person (const std::string& nn, const std::string& vn)
{
nname = nn;
vname = vn;
}
Wenn in diesem Fall eine Person durch
Bsp::Person nico ("Josuttis", "Nicolai");
angelegt wird, passiert nämlich im Einzelnen Folgendes:
 Zunächst wird auch in diesem Fall der Speicherplatz für das Objekt angelegt:

v n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?

n n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?

 Dann wird für die Komponente vname allerdings der Default-Konstruktor der String-Klasse
aufgerufen (es wird ja kein Argument zur Initialisierung übergeben). Dieser initialisiert den
Vornamen mit dem Leerstring (siehe Abschnitt 6.1.2):
Sandini Bib

6.4 Klassen verwenden Klassen 415

v n a m e :
b u f f e r : ?
l e n : ?
s i z e : ?

n n a m e :
b u f f e r :
l e n : 0
s i z e : 0

 Danach wird die Komponente nname ebenfalls durch den Default-Konstruktor der String-
Klasse mit dem Leerstring initialisiert:

v n a m e :
b u f f e r :
l e n : 0
s i z e : 0

n n a m e :
b u f f e r :
l e n : 0
s i z e : 0

 Schließlich werden die Anweisungen des Konstruktors der Klasse Person ausgeführt, die
den Komponenten vname und nname dann die richtigen Werte zuweisen:

v n a m e :
b u f f e r : ' N ' ' i ' ' c ' ' o ' ' l ' ' a ' ' i '
l e n : 7
s i z e : 7

n n a m e :
b u f f e r : ' J ' ' o ' ' s ' ' u ' ' t ' ' t ' ' i ' ' s '
l e n : 8
s i z e : 8

Dazu wird der Zuweisungsoperator der String-Klasse aufgerufen, der den Speicherplatz für
den eben initialisierten Leerstring wieder freigibt sowie neuen allokiert und initialisiert (siehe
Abschnitt 6.1.5).
Dieses Beispiel verdeutlicht einige Nachteile, die durch die Nichtverwendung einer Initiali-
sierungsliste entstehen: Beide Komponenten werden über den Default-Konstruktor mit einem
Default-Wert belegt, der aber falsch ist und durch eine Zuweisung anschließend sofort ersetzt
Sandini Bib

416 Kapitel 6: Dynamische und statische Komponenten

werden muss. Im vorliegenden Beispiel bedeutete dies, dass völlig unnötigerweise Speicherplatz
angelegt und sofort wieder freigegeben wird, da er zur Initialisierung der übergebenen Namen
nicht ausreicht. Dies bedeutet einen erheblichen Laufzeitnachteil (Speicherplatzverwaltung ist
zeitaufwändig), der sich bei Objekten, die mehr oder komplexere Komponenten besitzen, noch
vergrößert.
Eine solche fehlerhafte Initialisierung mit dem Default-Wert kann aber nicht nur sehr viel Zeit
kosten, sondern auch irreparable Schäden anrichten. Man denke z.B. an eine Klasse für geöffnete
Dateien, die zunächst eine verkehrte Default-Datei öffnet.
Schließlich kann es auch sein, dass für Klassen kein aufrufbarer Default-Konstruktor existiert.
Dies ist immer dann der Fall, wenn zur Initialisierung mindestens ein Argument gebraucht wird.
Solche Objekte könnten dann nicht mehr als Teilobjekte in einer anderen Klasse dienen.
Die Klasse Person hat z.B. keinen Default-Konstruktor, da es nach ihrer Spezifikation keinen
Sinn macht, eine Person anzulegen, ohne zumindest den Namen zu übergeben. Die Klasse könnte
ohne Initialisierungslisten nicht mehr in anderen Klassen verwendet werden (man denke z.B. an
eine Klasse Projekt, in der ein Projektleiter als Person eingetragen wird).

Initialisierung von Konstanten und Referenzen als Komponenten


Durch die Initialisierungslisten ist es auch möglich, die Komponente für den Nachnamen als
Konstante zu deklarieren, da sie vom Konstruktor initialisiert und später nicht mehr verändert
wird:
// dyna/person2.hpp
#ifndef PERSON_HPP
#define PERSON_HPP

// Headerdateien für Hilfsklassen


#include <string>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Person {
private:
std::string vname; // Vorname der Person
const std::string nname; // Nachname der Person (neu: Konstante)

public:
// Konstruktor aus Nachname und optional Vorname
Person (const std::string& nn, const std::string& vn = "")
: nname(nn), vname(vn) {
}

// Abfragen von Eigenschaften


Sandini Bib

6.4 Klassen verwenden Klassen 417

const std::string& nachname () const { // Nachname liefern


return nname;
}
const std::string& vorname () const { // Vorname liefern
return vname;
}
...
};

} // **** ENDE Namespace Bsp ********************************

#endif // PERSON_HPP
Doch Vorsicht: Komponenten als Konstanten zu deklarieren bedeutet, dass sie wirklich nicht
verändert werden können, solange das Objekt existiert. Das bedeutet insbesondere, dass keine
Zuweisung mit dem Default-Zuweisungsoperator möglich ist, der allen Komponenten neue Wer-
te zuweist. Insofern ist die Deklaration des Nachnamens für die Klasse Person nicht sinnvoll.
Ein typisches Beispiel für konstante Komponenten sind Objekt-IDs (siehe Abschnitt 6.5.1).
Genauso ist es möglich, dass eine Klasse Referenzen als Komponenten besitzt. Auch diese
müssen über eine Initialisierungsliste initialisiert werden. Man muss dabei allerdings sicherstel-
len, dass das Objekt, für das die Referenz steht, nicht kürzer existiert als das Objekt, das die
Referenz als Komponenten besitzt. Insofern kann man Komponenten eigentlich nur dann als Re-
ferenzen verwenden, wenn die Lebensdauer des referenzierten Objekts von dem Objekt, das die
Referenz besitzt, kontrolliert wird oder wenn das referenzierte Objekt während der gesamten
Laufzeit des Programms existiert.

6.4.3 Zusammenfassung
 Komponenten einer Klasse können einen beliebigen Datentyp besitzen. Ob es sich dabei um
einen Datentyp mit dynamischen Komponenten handelt, spielt für die Implementierung der
Klasse keine Rolle.
 Sofern nicht anders angegeben, wird für Teilobjekte abstrakter Datentypen jeweils der Default-
Konstruktor aufgerufen. Ist dieser nicht definiert oder aufrufbar, kann kein Objekt der Klasse
angelegt werden.
 Initialisierungslisten ermöglichen es, den Konstruktoren für Teilobjekte Argumente zu über-
geben, wodurch statt des Default-Konstruktors ein entsprechend anderer Konstruktor aufge-
rufen wird.
 Initialisierungslisten sind Zuweisungen vorzuziehen.
 Klassen können auch Konstanten und Referenzen als Komponenten besitzen. Diese müssen
im Konstruktor mit Hilfe einer Initialisierungsliste initialisiert werden.
Sandini Bib

418 Kapitel 6: Dynamische und statische Komponenten

6.5 Statische Komponenten und Hilfstypen


Anhand der im vorigen Abschnitt vorgestellten Beispielklasse Person wird hier nun gezeigt,
dass Klassen einen eigenen Gültigkeitsbereich besitzen. Das kann dazu verwendet werden, klas-
senspezifische Variablen zu definieren, die keinem bestimmten Objekt zugeordnet sind. In dem
Zusammenhang wird auch aufgezeigt, wie innerhalb von Klassen Typen und Hilfsklassen defi-
niert werden können. Semantisch wird die Klasse Person um eine ID, die als konstante Kompo-
nente, und eine Anrede, die als Aufzählungstyp implementiert wird, erweitert.

6.5.1 Statische Klassenkomponenten


Eine Personenverwaltung bietet ein typisches Beispiel für zwei Eigenschaften von Klassen, die
immer wieder gebraucht werden:
 eine eindeutige ID für die Objekte
 eine Information über die Anzahl der existierenden Objekte
Beide Eigenschaften lassen sich allerdings nur über Variablen implementieren, die nicht zu einem
konkreten Objekt, sondern allgemein zur Klasse gehören. Solche so genannte Klassenvariablen7
sind globale Variablen, die aber zum Gültigkeitsbereich der Klasse gehören.
In C++ können Klassenvariablen durch die Deklaration von statischen Klassenkomponenten
implementiert werden. Statische Klassenkomponenten werden mit dem Schlüsselwort static
in der Klassenstruktur deklariert. Die Variablen gehören damit zum Gültigkeitsbereich der Klas-
se und müssen von außen über den Bereichsoperator angesprochen werden (was natürlich nur
möglich ist, wenn sie als öffentliche Komponente deklariert werden).
Es handelt sich aber im Gegensatz zu nicht-statischen Komponenten um Variablen, die nur
einmal für die gesamte Programmlaufzeit und nicht für jedes einzelne Objekt der Klasse angelegt
werden. Jedes Objekt der Klasse hat aber Zugriff auf diese Variablen.
Die folgende Spezifikation deklariert für die im vorigen Abschnitt vorgestellte Klasse Person
die statischen Klassenkomponenten personenMaxID als Zähler zur Vergabe von eindeutigen
Personen-IDs und personenAnzahl als Variable, in der die aktuelle Anzahl von existierenden
Personen verwaltet wird:
// dyna/person3.hpp
#ifndef PERSON_HPP
#define PERSON_HPP

// Headerdateien einbinden
#include <string>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

7
Die Bezeichnung Klassenvariable stammt aus Smalltalk.
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 419

class Person {
/* neu: statische Klassenkomponenten
*/
private:
static long personenMaxID; // höchste ID aller Personen
static long personenAnzahl; // aktuelle Anzahl aller Personen
public:
// aktuelle Anzahl aller Personen liefern
static long anzahl () {
return personenAnzahl;
}

// nicht-statische Klassenkomponenten
private:
std::string vname; // Vorname der Person
std::string nname; // Nachname der Person
const long pid; // neu: eindeutige ID der Person

public:
// Konstruktor aus Nachname und optional Vorname
Person (const std::string&, const std::string& = "");

// neu: Copy-Konstruktor
Person (const Person&);

// neu: Destruktor
~Person ();

// neu: Zuweisung
Person& operator = (const Person&);

// Abfragen von Eigenschaften


const std::string& nachname () const { // Nachname liefern
return nname;
}
const std::string& vorname () const { // Vorname liefern
return vname;
}
long id () const { // neu: ID liefern
return pid;
}
Sandini Bib

420 Kapitel 6: Dynamische und statische Komponenten

friend bool operator == (const Person& p1, const Person& p2) {


return p1.vname == p1.vname && p2.nname == p2.nname;
}
friend bool operator != (const Person& p1, const Person& p2) {
return !(p1==p2);
}
...
};

} // **** ENDE Namespace Bsp ********************************

#endif // PERSON_HPP
Entscheidend für die Tatsache, dass es sich bei personenMaxID und personenAnzahl nicht
um Komponenten handelt, die jedes Objekt der Klasse für sich verwaltet, ist die Deklaration der
Komponenten als statische Komponenten:
class Person {
private:
static long personenMaxID; // höchste ID aller Personen
static long personenAnzahl; // aktuelle Anzahl aller Personen
...
};
Das bedeutet, dass diese Variablen nur genau einmal für die Klasse Person existieren und von
allen Objekten der Klasse gemeinsam verwendet werden.
Auf statische Komponenten kann nicht nur in normalen“ Elementfunktionen, sondern auch

in speziellen statischen Elementfunktionen (in der objektorientierten Terminologie auch Klassen-
methoden genannt) zugegriffen werden:
namespace Bsp {
class Person {
...
public:
// aktuelle Anzahl aller Personen liefern
static long anzahl () {
return personenAnzahl;
}
...
};
}
Auch hier handelt es sich im Prinzip um globale Funktionen, die allerdings nur zum Gültigkeits-
bereich der Klasse Bsp::Person gehören. Sie haben damit Zugriff auf private statische Kom-
ponenten der Klasse Person und besitzen im Gegensatz zu nicht-statischen Elementfunktionen
den Vorteil, auch ohne Bindung an ein Objekt dieser Klasse aufgerufen werden zu können.
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 421

Das folgende Anwendungsprogramm soll dies verdeutlichen:


// dyna/ptest3.cpp
#include <iostream>
#include "person.hpp"

int main ()
{
std::cout << "Personenanzahl: " << Bsp::Person::anzahl()
<< std::endl;

Bsp::Person nico ("Josuttis", "Nicolai");

std::cout << "Personenanzahl: " << Bsp::Person::anzahl()


<< std::endl;
}
Die Personenanzahl wird hier ohne Bindung an ein konkretes Objekt der Klasse ermittelt, indem
die statische Funktion anzahl() des Gültigkeitsbereichs Bsp::Person aufgerufen wird. Auf
diese Weise kann die Anzahl auch ermittelt werden, ohne dass überhaupt eine Person existiert.
Im objektorientierten Sinne bedeuten statische Komponenten, dass die Klassen selbst Attri-
bute und Operationen besitzen und insofern als Objekte betrachtet werden können, Eine statische
Elementfunktion ist die Methode einer Klasse. Der Aufruf der statischen Elementfunktion ist ei-
ne Nachricht an diese Klasse, auf die mit der Methode reagiert wird.
Wenn ein konkretes Objekt der Klasse Person existiert, dann ist auch der Aufruf über ein
solches Objekt möglich:
Bsp::Person nico("Josuttis","Nicolai");
...
std::cout << "Personenanzahl: " << nico.anzahl() << std::endl;
Da dies allerdings dazu führt anzunehmen, es sei eine bestimmte Eigenschaft einer konkreten
Person angesprochen, ist von einem solchen Aufruf in der Regel abzuraten.

Initialisierung von statischen Klassenkomponenten


Statische Klassenkomponenten werden in Klassenstrukturen nur deklariert und müssen, wie jede
statische Variable, einmal definiert und gegebenenfalls initialisiert werden. Dies muss außerhalb
der Klassenstruktur in einer Quelldatei (sinnvollerweise in der Quelldatei der Klasse) geschehen.
Eine Initialisierung innerhalb der Klassenstruktur ist falsch.
Betrachten wir dazu die Implementierung der Klasse Person:
// dyna/person3.cpp
// Headerdatei der Klasse einbinden
#include "person.hpp"
Sandini Bib

422 Kapitel 6: Dynamische und statische Komponenten

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

/* neu: statische Klassenkomponenten initialisieren


*/
long Person::personenMaxID = 0;
long Person::personenAnzahl = 0;

/* Konstruktor aus Nachname und Vorname


* - Default für Vorname: ""
* - Vor- und Nachname werden mit Initialisierungsliste initialisiert
* - neu: Die ID wird ebenfalls direkt initialisiert
*/
Person::Person (const std::string& nn, const std::string& vn)
: nname(nn), vname(vn), pid(++personenMaxID)
{
++personenAnzahl; // Anzahl der existierenden Personen erhöhen
}

/* neu: Copy-Konstruktor
*/
Person::Person (const Person& p)
: nname(p.nname), vname(p.vname), pid(++personenMaxID)
{
++personenAnzahl; // Anzahl der existierenden Personen erhöhen
}

/* neu: Destruktor
*/
Person::~Person ()
{
--personenAnzahl; // Anzahl der existierenden Personen herabsetzen
}

/* neu: Zuweisung
*/
Person& Person::operator = (const Person& p)
{
if (this == &p) {
return *this;
}
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 423

// alles außer ID zuweisen


nname = p.nname;
vname = p.vname;

return *this;
}

} // **** ENDE Namespace Bsp ********************************

Auch hier gilt, dass die Variable für den entsprechenden Gültigkeitsbereich über den Bereichs-
operator angesprochen wird:
namespace Bsp {
long Person::personenMaxID = 0;
long Person::personenAnzahl = 0;
...
}
Selbstverständlich darf jede statische Klassenkomponente nur an einer Stelle im Programm ini-
tialisiert werden.
Der Konstruktor initialisiert die Komponenten der Klasse Person über die Initialisierungs-
liste:
Person::Person (const std::string& nn, const std::string& vn)
: nname(nn), vname(vn), pid(++personenMaxID)
{
++personenAnzahl; // Anzahl der existierenden Personen erhöhen
}
Die Komponente nname wird mit dem ersten Parameter, nn, und die Komponente vname wird
mit dem zweiten Parameter, vn, initialisiert. Zur Initialisierung der neu hinzugekommenen Kom-
ponente pid wird die inkrementierte Klassenkomponente mit der bisher höchsten Personen-ID,
personenMaxID, verwendet. Da die Komponente pid konstant ist, muss dieser Wert mit Hilfe
einer Initialisierungsliste initialisiert werden; eine nachträgliche Zuweisung wäre nicht möglich.
Im Funktionskörper wird außerdem der Zähler für die Anzahl der existierenden Personen ent-
sprechend erhöht.
Damit die Anzahl jeweils stimmt, muss der Zähler für die Anzahl existierender Personen bei
der Zerstörung eines Objekts der Klasse Person auch wieder herabgesetzt werden. Aus diesem
Grund ist nun auch ein entsprechender Destruktor definiert:
Person::~Person ()
{
--personenAnzahl; // Anzahl der existierenden Personen herabsetzen
}
Sandini Bib

424 Kapitel 6: Dynamische und statische Komponenten

Der Destruktor wird für jedes Objekt aufgerufen, das existierte und zerstört wird. Entsprechend
muss auch wirklich für jedes Objekt, das angelegt wird, der Zähler heraufgesetzt werden. Insbe-
sondere darf man nicht vergessen, auch den Copy-Konstruktor zu definieren, damit auch, wenn
ein Objekt der Klasse als Kopie eines existierenden Objekts angelegt wird, eine neue Objekt-ID
vergeben und die Personenanzahl entsprechend erhöht wird.
Person::Person (const Person& p)
: nname(p.nname), vname(p.vname), pid(++personenMaxID)
{
// Anzahl der existierenden Personen erhöhen
++personenAnzahl;
}
Ohne Implementierung des Copy-Konstruktors wird beim Kopieren eines Objekts die ID ko-
piert (damit hätten zwei Objekte die gleiche ID) und die Anzahl nicht erhöht (wohl aber bei der
Zerstörung des Objekts vermindert).
Außerdem muss der Zuweisungsoperator implementiert werden, damit nicht auch die ID zu-
gewiesen wird. Er muss also so implementiert werden, dass alle Komponenten bis auf die ID
zugewiesen werden:
const Person& Person::operator = (const Person& p)
{
if (this == &p) {
return *this;
}

// alles außer ID zuweisen


nname = p.nname;
vname = p.vname;

return *this;
}
Da die ID als Konstante deklariert wurde, wäre eine Zuweisung ansonsten gar nicht möglich.
Der Default-Zuweisungsoperator würde auch die ID zuweisen, was aber ein Fehler ist.

6.5.2 Typdeklarationen innerhalb von Klassen


Die im vorigen Abschnitt vorgestellte Möglichkeit, in Klassen statische Komponenten und Funk-
tionen zu definieren, deutet schon darauf hin, dass Klassen einen eigenen Gültigkeitsbereich be-
sitzen.
Dies kann auch dazu verwendet werden, klassenspezifische Typen zu definieren, die auf den
Gültigkeitsbereich der Klasse beschränkt sind. Das hat im Wesentlichen den Vorteil, dass globale
Deklarationen vermieden werden. Der globale Namensbereich wird nicht unnötig mit Symbolen
belastet, die zu Namenskonflikten führen könnten. Es trägt auch dem objektorientierten Ansatz
Rechnung, eine Klasse als vollständigen, in sich abgeschlossenen Bereich zu betrachten.
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 425

Bei einer Klasse zur Personenverwaltung kann dies z.B. dazu verwendet werden, einen klassen-
spezifischen Aufzählungstyp (siehe Abschnitt 9.6.2) für die Anrede zu definieren.
Die Klasse zur Personenverwaltung hätte dann z.B. folgende Spezifikation:
// dyna/person4.hpp
#ifndef PERSON_HPP
#define PERSON_HPP

// Headerdateien einbinden
#include <string>

// **** BEGINN Namespace Bsp ********************************


namespace Bsp {

class Person {
/* statische Klassenkomponenten
*/
private:
static long personenMaxID; // höchste ID aller Personen
static long personenAnzahl; // aktuelle Anzahl aller Personen
public:
// aktuelle Anzahl aller Personen liefern
static long anzahl () {
return personenAnzahl;
}

public:
// neu: spezieller Aufzählungstyp für die Anrede
enum Anrede { herr, frau, firma, leer };

// nicht-statische Klassenkomponenten
private:
Anrede anr; // neu: Anrede (kann auch leer sein)
std::string vname; // Vorname der Person
std::string nname; // Nachname der Person
const long pid; // eindeutige ID der Person

public:
// Konstruktor aus Nachname und optional Vorname
Person (const std::string&, const std::string& = "");

// Copy-Konstruktor
Person (const Person&);
Sandini Bib

426 Kapitel 6: Dynamische und statische Komponenten

// Destruktor
~Person ();

// Zuweisung
Person& operator = (const Person&);

// Abfragen von Eigenschaften


const std::string& nachname () const { // Nachname liefern
return nname;
}
const std::string& vorname () const { // Vorname liefern
return vname;
}
long id () const { // ID liefern
return pid;
}
const Anrede& anrede () const { // neu: Anrede liefern
return anr; // (eigener Aufzählungstyp)
}

// vergleichen
friend bool operator == (const Person& p1, const Person& p2) {
return p1.vname == p1.vname && p2.nname == p2.nname;
}
friend bool operator != (const Person& p1, const Person& p2) {
return !(p1==p2);
}
...
};

} // **** ENDE Namespace Bsp ********************************

#endif // PERSON_HPP
Der Aufzählungstyp Anrede wird einfach in der Klassendeklaration definiert und auch verwen-
det. Dies hat den Vorteil, dass für die verwendeten Symbole Anrede, herr, frau, firma und
leer keine globalen Namenskonflikte drohen. Die öffentliche Deklaration ermöglicht es aber
dennoch, diese Symbole außerhalb der Klasse anzusprechen. Dies geschieht mit dem Bereichs-
operator, wie das folgende Anwendungsbeispiel zeigt:
// dyna/ptest4.cpp
#include <iostream>
#include "person.hpp"
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 427

int main ()
{
Bsp::Person nico("Josuttis","Nicolai");

/* Variable vom Typ Anrede der Klasse Bsp::Person deklarieren


* und mit dem Wert leer der Klasse Bsp::Person initialisieren
*/
Bsp::Person::Anrede keineAnrede = Bsp::Person::leer;
...
if (nico.anrede() == keineAnrede) {
std::cout << "Anrede von Nico wurde nicht gesetzt"
<< std::endl;
}
}
Wie die Deklaration von keineAnrede zeigt, ist die Qualifizierung mit dem Bereichsoperator
sowohl bei der Verwendung des Datentyps als auch bei der Verwendung eines Werts notwendig,
da sowohl der Datentyp als auch der Wert zum Gültigkeitsbereich der Klasse Person gehört.
Auf die gleiche Art und Weise können in Klassen eigene Datentypen mit typedef angelegt
werden.

6.5.3 Aufzählungstypen als statische Klassenkonstanten


Mitunter wird eine deklarierte statische Komponente noch in der Klassenstruktur benötigt. Dies
ist möglich, wenn es sich um einen ganzzahligen Typ oder um einen Aufzählungstyp handelt:
class BspKlasse {
private:
static const int MAXANZ = 100; // OK, da ganzzahlig und konstant
int elems[MAXANZ]; // OK
...
};
Diese Möglichkeit wurde erst recht spät im Rahmen der Standardisierung eingeführt. Bis dahin
half der Trick, einen Aufzählungstyp zu verwenden. Mitunter wird man also auch noch folgenden
Code sehen:
class BspKlasse {
private:
enum { MAXANZ = 100 }; // OK
int elems[MAXANZ]; // OK
...
};
Der Wert für einen Aufzählungstyp ist nämlich kein konstantes Objekt, sondern nur ein symbo-
lischer Name, für den bei der Deklaration auch ein bestimmter Wert festgelegt werden darf.
Sandini Bib

428 Kapitel 6: Dynamische und statische Komponenten

6.5.4 Eingebettete und lokale Klassen


Die Tatsache, dass Klassen einen eigenen Gültigkeitsbereich besitzen, ermöglicht es auch, Klas-
sen (oder Strukturen) auf den Gültigkeitsbereich einer Klasse einzuschränken.
Eine solche eingebettete Klasse (englisch member class oder nested class) wird z.B. wie folgt
definiert:
class BspKlasse {
public:
/* eingebettete Hilfsklasse
*/
class Hilfsklasse {
private:
int x;
public:
void f();
...
};
...
};
Auf die Komponenten einer eingebetteten Klasse wird von ganz außen durch eine geschachtelte
Qualifizierung mit dem Bereichsoperator zugegriffen. Die Funktion f() müsste z.B. wie folgt
definiert werden:
void Klasse::Hilfsklasse::f ()
{
...
}
Dies ist vor allem bei komplexen Klassen nützlich, um bei der Definition dazugehöriger Hilfs-
klassen nicht den globalen Namensbereich mit Hilfsklassen zu belegen. Dieses Vorgehen vermei-
det somit Namenskonflikte und unterstützt die Datenkapselung (die Hilfsklassen können auch als
private deklariert werden).
Ein typisches Beispiel für eingebettete Klassen sind Fehlerklassen, wie sie bei der Ausnah-
mebehandlung vorgestellt wurden (siehe Abschnitt 4.7).
Es ist sogar möglich, dass Klassen lokal für den Gültigkeitsbereich einer Funktion deklariert
werden. Dies wird nur selten gebraucht. Man mache sich aber klar, dass eine Struktur in C++
nichts anderes als eine Klasse ist, die als Default öffentliche Komponenten besitzt. Vor diesem
Hintergrund bedeutet das also, dass innerhalb von Funktionen lokale Strukturen deklariert wer-
den können, was schon etwas sinnvoller klingt.

Vorwärtsdeklaration von eingebetteten Klassen


Die Komponenten einer eingebetteten Klasse können auch außerhalb der umgebenden Klasse
deklariert werden. Dadurch können eingebettete Klassen sogar in anderen Modulen definiert
werden.
Sandini Bib

6.5 Statische Komponenten und Hilfstypen 429

Das obige Beispiel sieht dann wie folgt aus:


class BspKlasse {
public:
/* eingebettete Hilfsklasse
*/
class Hilfsklasse;
...
};

class BspKlasse::Hilfsklasse {
private:
int x;
public:
void f();
...
};

6.5.5 Zusammenfassung
 Klassen können statische Komponenten besitzen. Dabei handelt es sich um Objekte, die pro
Klasse nur einmal existieren und von allen Objekten der Klasse gemeinsam verwendet wer-
den. Sie entsprechen globalen Objekten, die auf den Namens- und Gültigkeitsbereich einer
Klasse beschränkt sind.
 Statische Klassenkomponenten müssen außerhalb der Klassendeklaration genau einmal de-
finiert und gegebenenfalls initialisiert werden.
 Statische Elementfunktionen erlauben es, auf statische Komponenten zuzugreifen, ohne dass
ein dazugehöriges Objekt der Klasse angegeben werden muss. Sie entsprechen globalen
Funktionen, die auf den Namens- und Gültigkeitsbereich einer Klasse beschränkt sind.
 Mit Aufzählungstypen lassen sich statische Klassenkonstanten definieren, die auch zur De-
klaration anderer Komponenten verwendet werden können.
 Klassen bilden einen eigenen Gültigkeitsbereich, in dem es möglich ist, eigene Datentypen
wie Aufzählungstypen oder Hilfsklassen (eingebettete Klassen) zu definieren.
 Für Objekt-IDs müssen alle Konstruktoren (auch der Copy-Konstruktor) und der Zuwei-
sungsoperator einer Klasse selbst definiert werden. Die Komponente für die ID sollte als
Konstante deklariert werden.
 Für Objektzähler müssen alle Konstruktoren (auch der Copy-Konstruktor) und der Destruktor
einer Klasse selbst definiert werden.
Sandini Bib
Sandini Bib

Kapitel 7
Templates

Dieses Kapitel stellt das Konzept der Templates vor. Mit Templates kann Quellcode für verschie-
dene Datentypen parametrisiert werden. Dadurch kann man eine Minimum-Funktion oder eine
Mengenklasse so implementieren, dass der Datentyp der Parameter bzw. Elemente noch nicht
festgelegt wird. Es handelt sich aber um keinen Code für beliebige Objekte; steht der Datentyp
der Parameter bzw. Elemente fest, findet eine vollständige Typprüfung für alle Operationen statt.
In diesem Kapitel werden zunächst Funktionstemplates vorgestellt. Es folgen Klassentem-
plates. Abgerundet wird das Kapitel durch Hinweise zum Umgang mit Templates in der Praxis
und spezielle Design-Techniken in Verbindung mit Templates.

7.1 Motivation für Templates


In Programmiersprachen mit typgebundenen Variablen kommt es relativ oft vor, dass Funktio-
nen, die das Gleiche machen, mehrfach definiert werden müssen, da sich die Datentypen der Pa-
rameter unterscheiden. Ein typisches Beispiel ist eine Funktion, die das Maximum zweier Werte
zurückliefert. Sie muss für jeden Datentyp, für den das Maximum zweier Werte gebraucht wird,
implementiert werden. In C kann dieser Zwang zwar durch die Definition von Makros umgan-
gen werden; da ein Makro aber ein einfacher Textersatz ist, kann die Verwendung des Makros zu
Problemen führen (überhaupt keine Typprüfung mehr, Seiteneffekte usw.).
Der Aufwand für eine mehrfache Implementierung existiert nicht nur bei Funktionen, son-
dern auch bei speziellen Datentypen mit den dazugehörigen Funktionen, wie etwa bei Contai-
nerklassen. Wenn die Verwaltung für eine Menge von Objekten implementiert wird, muss immer
festgelegt werden, welchen Datentyp diese Objekte haben. Wenn eine im Prinzip gleiche Men-
genverwaltung für verschiedene Datentypen benötigt wird, müsste ohne ein spezielles Sprach-
mittel also jedes Mal alles neu implementiert werden.
Ein typisches Beispiel für die mehrfache Implementierung bei der Verwaltung unterschiedli-
cher Objekte ist die Implementierung eines Stacks (Kellers). Bei einem Stack müssen Elemente
eines bestimmten Typs ein- und wieder ausgekellert werden. Mit den bisherigen Sprachmitteln
muss dies für jeden einzelnen Datentyp, für den ein Stack gebraucht wird, neu implementiert
werden, da bei der Deklaration des Kellers der Datentyp der Objekte, die verwaltet werden, an-
gegeben werden muss. Aus diesem Grund werden immer wieder Stacks neu implementiert, ob-

431
Sandini Bib

432 Kapitel 7: Templates

wohl der Algorithmus eigentlich jedes Mal gleich ist. Dies kostet nicht nur unnötig Zeit, sondern
ist auch eine permanente Fehlerquelle.
C++ besitzt aus diesem Grund das Sprachmittel der Templates (deutsch: Schablonen). Tem-
plates sind Funktionen und Klassen, die nicht für einen bestimmten Typ, sondern für einen noch
festzulegenden Typ implementiert werden. Im Anwendungsprogramm kann dann eine solche
Implementierung für einen bestimmten Datentyp, für den die Funktion oder Klasse gebraucht
wird, realisiert werden. Auf diese Weise muss eine Maximum-Funktion für zwei Objekte mit be-
liebigem Typ nur einmal exemplarisch implementiert werden. Containerklassen wie Stacks, ver-
kettete Listen usw. müssen ebenfalls nur einmal implementiert und getestet werden und können
dann für jeden beliebigen Datentyp verwendet werden.
Die Definition und Anwendung von Templates wird im Folgenden zunächst für Funktionen
am Beispiel der Maximum-Funktion und dann für Klassen am Beispiel der Klasse Stack vorge-
stellt.

7.1.1 Terminologie
Auch beim Thema Templates sind die Begriffe nicht klar definiert. So wird z.B. bei einer Funk-
tion, bei der Datentypen parametrisiert sind, mal von einem Funktionstemplate (englisch: functi-
on template), mal von einer Template-Funktion (englisch: template function) gesprochen. Da es
sich aber eben noch nicht um eine fertige Funktion handelt, sollte man den Begriff Funktions-
template verwenden. Analog sollte man bei einer Klasse, für die Datentypen parametrisiert sind,
von einem Klassentemplate statt von einer Template-Klasse sprechen.
Noch verwirrender ist die Verwendung des Wortes instantiieren. Im Umfeld von Templates
wird damit der Vorgang bezeichnet, der aus Template-Code den tatsächlich zu übersetzenden
Code generiert. Diese Namensgebung ist insofern unglücklich, da dieser Begriff im objektorien-
tierten Umfeld für das Anlegen von Objekten einer Klasse verwendet wird. Im Umfeld von C++
hängt die Bedeutung des Wortes instantiieren also immer vom genauen Kontext ab.
Sandini Bib

7.2 Funktionstemplates 433

7.2 Funktionstemplates
Wie bereits in der Einführung zu diesem Abschnitt erwähnt wurde, dienen Funktionstemplates
dazu, mehrere Funktionen, die sich nur im Datentyp ihrer Parameter unterscheiden, nur einmal
zu definieren und sie dann für verschiedene Datentypen zu verwenden.
Im Unterschied zu Makros sind Funktionstemplates aber kein blinder“ Textersatz, sondern

Funktionen, die semantisch geprüft und ohne unerwünschte Seiteneffekte kompiliert werden. Die
Gefahr, dass ein Parameter n++ wie bei einem Makro mehrfach ersetzt wird und somit aus einer
einfachen eine mehrfache Inkrementierung wird, besteht nicht.

7.2.1 Definition von Funktionstemplates


Definiert werden Funktionstemplates wie normale Funktionen für einen angenommenen Typ,
der vor der Deklaration angegeben wird. Ein Maximum-Template kann z.B. wie folgt deklariert
werden:
template <typename T>
const T& max (const T&, const T&);
Die erste Zeile definiert T für die folgende Deklaration als Typparameter:
template <typename T>
Hier wird das Schlüsselwort typename verwendet, das festlegt, dass es sich beim folgenden
Symbol um einen Datentyp handelt. Dieses Schlüsselwort wurde erst relativ spät in C++ aufge-
nommen. Vorher hat man an dieser Stelle das Schlüsselwort class verwendet:
template <class T>
Semantisch gibt es an dieser Stelle zwischen diesen beiden Wörtern keinen Unterschied. Auch
bei der Verwendung des Schlüsselwortes class muss der Datentyp nicht unbedingt eine Klasse,
sondern kann auch ein fundamentaler oder ein mit typedef definierter Datentyp sein.
Die Verwendung des Symbols T für einen Template-Typ ist dabei nicht zwingend, aber
sehr typisch. In der folgenden Deklaration kann (und muss) T als Datentyp in einer Parameter-
Deklaration verwendet werden und steht dann für den Typ des Parameters, der beim Aufruf
übergeben wird.
Die Definition wird entsprechend implementiert:
// tmpl/max1.hpp
template <typename T>
const T& max (const T& a, const T& b)
{
return (a > b ? a : b);
}
Die Anweisungen innerhalb der Funktion unterscheiden sich in keiner Weise von Anweisungen
anderer Funktionen. In diesem Fall wird also das Maximum zweier Werte eines angenommenen
Typs T unter Anwendung des Vergleichsoperators zurückgeliefert. Wie in Kapitel 4.4 bereits
erwähnt wurde, wird durch die Verwendung von konstanten Referenzen (const T&) das Anlegen
von Kopien bei der Übergabe der Parameter und des Rückgabewerts verhindert.
Sandini Bib

434 Kapitel 7: Templates

7.2.2 Aufruf von Funktionstemplates


Angewendet wird ein Funktionstemplate wie jede andere Funktion. Dies zeigt folgendes Bei-
spielprogramm:
// tmpl/max1.cpp
#include <iostream>
#include <string>
#include "max.hpp"

int main()
{
int a, b; // zwei Variablen vom Datentyp int
std::string s, t; // zwei Variablen der Klasse std::string
...
std::cout << max(a,b) << std::endl; // max() für zwei ints
std::cout << max(s,t) << std::endl; // max() für zwei Strings
}
In dem Moment, in dem die Funktion max() für zwei Objekte eines Typs aufgerufen wird, wird
die Schablone zur Realität. Das bedeutet, der Compiler verwendet die Template-Definition und
realisiert sie, indem er für T jeweils int bzw. std::string einsetzt und entsprechenden Code
erzeugt. Ein Template wird also nicht als Code übersetzt, der mit beliebigen Typen umgehen
kann, sondern nur als Schablone intern festgehalten, die dazu dient, im Anwendungsfall für die
entsprechenden Typen jeweils den entsprechenden Code zu realisieren. Wird max() für sieben
verschiedene Typen aufgerufen, werden also auch sieben Funktionen kompiliert.
Den Vorgang, bei dem der zu übersetzende Code aus dem Template-Code generiert wird,
nennt man auch Instantiierung. Diese Namensgebung ist insofern unglücklich, da dieser Begriff
im objektorientierten Umfeld für das Anlegen von Objekten einer Klasse verwendet wird. Ich
werde diesen Vorgang deshalb im weiteren Verlauf des Buches auch weiterhin mitunter als Rea-
lisierung bezeichnen.
Aufruf und Realisierung sind nur möglich, wenn alle in dem Funktionstemplate angewende-
ten Operationen für den Datentyp der Parameter auch definiert sind. Um die Maximum-Funktion
für Objekte der Klasse std::string aufrufen zu können, muss für Strings also der Vergleichs-
operator < definiert sein.
Man beachte, dass Templates im Gegensatz zu Makros kein blinder Textersatz sind. Der
Aufruf
max (x++, z *= 2);
ist zwar nicht sehr sinnvoll, funktioniert aber. Es wird wirklich das Maximum der beiden als
Argumente angegebenen Ausdrücke geliefert, und jeder Ausdruck wird nur einmal ausgewertet
(x also z.B. nur einmal inkrementiert).
Sandini Bib

7.2 Funktionstemplates 435

7.2.3 Praktische Hinweise zum Umgang mit Templates


Templates sprengen das herkömmliche Compiler/Linker-Modell mit getrennten Übersetzungs-
einheiten. Man kann nicht einfach Templates in einem Modul definieren und übersetzen und
getrennt davon die Anwendung eines Templates übersetzen und dann beides zusammenbinden.
Es ist vielmehr so, dass erst bei der Anwendung von Templates klar ist, für welche Datentypen
ein Template übersetzt werden muss.
Es gibt unterschiedliche Möglichkeiten, mit diesem Problem umzugehen. Die einfachste und
portabelste Möglichkeit ist dabei die, sämtlichen Template-Code in Headerdateien unterzubrin-
gen. Durch die Einbindung der Headerdateien bei der Anwendung ist sichergestellt, dass der
Code zum Übersetzen für die dann feststehenden Datentypen auch zur Verfügung steht.
Insofern ist es kein Zufall, dass die Definition des max()-Templates im vorliegenden Beispiel
in einer Headerdatei stand. Bemerkenswert ist dabei allerdings, dass das Wort inline (siehe
Abschnitt 4.3.3) nicht angegeben werden muss. Funktionstemplates sind implizit inline.
Auf weitere Aspekte des Umgangs mit Templates in der Praxis wird in Abschnitt 7.6 noch
eingegangen.

7.2.4 Automatische Typumwandlung bei Templates


Bei der Klärung des Datentyps eines Template-Parameters ist keine automatische Typumwand-
lung möglich. Falls in einem Funktionstemplate also mehrere Parameter vom Typ T deklariert
werden, müssen die übergebenen Argumente den gleichen Datentyp besitzen. Ein Aufruf der
Maximum-Funktion mit zwei Objekten verschiedenen Typs ist also nicht möglich:
template <typename T>
const T& max (const T&, const T&); // beide Parameter haben Typ T
...
int i;
long l;
...
max(i,l) // FEHLER: i und l haben verschiedene Datentypen

Explizite Qualifizierung
Man kann beim Aufruf von Templates aber durch eine explizite Qualifizierung festlegen, für
welchen Datentyp ein Template verwendet werden soll:
max<long>(i,l) // OK, ruft max() mit long als T auf
In diesem Fall wird das Funktionstemplate max<>() für den Typ long als T realisiert. Nun wird
wie bei herkömmlichen Funktionen nur noch geprüft, ob die übergebenen Argumente als long
verwendet werden können, was durch eine implizite Typumwandlung möglich ist.

Templates mit mehreren Parametern


Man könnte alternativ auch das Template so definieren, dass es Parameter unterschiedlichen Typs
erlaubt:
Sandini Bib

436 Kapitel 7: Templates

template <typename T1, typename T2>


T1 max (const T1&, const T2&); // Parameter können unterschiedliche Typen haben
...
int i;
long l;
...
max(i,l) // OK: liefert int
Das Problem ist dabei, dass man sich in diesem Fall beim Rückgabetyp für einen der Parame-
tertypen entscheiden. Dies ist in diesem Beispiel schlecht, da man nicht sagen kann, welcher der
beiden Argumenttypen mächtiger ist und sicherlich diesen Datentyp zurückliefern sollte. Au-
ßerdem wird durch eine Typumwandlung des zweiten Parameters in den Rückgabetyp ein neues
lokales temporäres Objekt erzeugt. Dies darf aber nicht als Referenz zurückgeliefert werden. Der
Rückgabetyp muss deshalb wie hier angegeben T statt const T& lauten.
Insofern ist die Möglichkeit der expliziten Qualifizierung in diesem Fall sicherlich besser.

7.2.5 Überladen von Templates


Templates können für bestimmte Datentypen überladen werden. Damit ist es möglich, eine
Template-Implementierung für bestimmte Datentypen durch eine andere Implementierung zu
ersetzen. Daraus ergeben sich mehrere Vorteile:
 Funktionstemplates können für andere Datentyp-Kombinationen aufrufbar gemacht werden
(es könnte z.B. eine Funktion für das Maximum von float und Bsp::Bruch definiert wer-
den).
 Implementierungen können für einen speziellen Datentyp optimiert werden.
 Datentypen, für die die Template-Implementierung untauglich ist, können eine korrekte Im-
plementierung erhalten.
Beim Beispiel des Funktionstemplates für das Maximum wäre beispielsweise ein Aufruf mit
C-Strings (Datentyp const char*) fehlerhaft:
const char* s1;
const char* s2;
...
const char* maxstring = max(s1,s2); // FEHLER: vergleicht Adressen
Im Template werden nämlich mit > die Adressen und nicht die Inhalte der Strings verglichen
(siehe Abschnitt 3.7.3), was bei C-Strings sicherlich nicht sinnvoll ist.
Das Problem wird durch das Überladen des Templates für C-Strings behoben:
inline const char* max (const char* a, const char* b)
{
return (std::strcmp(a,b) > 0 ? a : b);
}
Sandini Bib

7.2 Funktionstemplates 437

Ein Überladen wäre in diesem Fall auch für Zeiger im Allgemeinen möglich. Auf diese Weise
könnte man festlegen, dass beim Aufruf von max() für Zeiger nicht die Zeiger selbst (also ihre
Adressen) verglichen werden, sondern die Werte der Objekte, auf die sie verweisen. Dies sähe
wie folgt aus:
template <typename T>
T* const & max (T* const & a, T* const & b)
{
return (*a > *b ? a : b);
}
Man beachte, dass zur Übergabe des Zeigers als konstante Referenz das const hinter dem Stern
angegeben werden muss. Ansonsten handelt es sich um einen Zeiger, der auf eine Konstante
verweist (siehe auch Seite 199).
Verwendet man also folgende überladene Funktionen:
// tmpl/max2.hpp
#include <iostream>
#include <cstring>

template <typename T>


const T& max (const T& a, const T& b)
{
std::cout << "max<>() fuer T" << std::endl;
return (a > b ? a : b);
}

template <typename T>


T*const& max (T*const& a, T*const& b)
{
std::cout << "max<>() fuer T*" << std::endl;
return (*a > *b ? a : b);
}

inline const char* max (const char* a, const char* b)


{
std::cout << "max<>() fuer char*" << std::endl;
return (std::strcmp(a,b) > 0 ? a : b);
}
und ruft diese durch folgendes Programm auf:
// tmpl/max2.cpp
#include <iostream>
Sandini Bib

438 Kapitel 7: Templates

#include <string>
#include "max.hpp"

int main ()
{
int a=7; // zwei Variablen vom Datentyp int
int b=11;
std::cout << max(a,b) << std::endl; // max() für zwei ints

std::string s="hallo"; // zwei Strings


std::string t="holla";
std::cout << ::max(s,t) << std::endl; // max() für zwei Strings

int* p1 = &b; // zwei Zeiger


int* p2 = &a;
std::cout << *max(p1,p2) << std::endl; // max() für zwei Zeiger

const char* s1 = "hallo"; // zwei C-Strings


const char* s2 = "otto";
std::cout << max(s1,s2) << std::endl; // max() für zwei C-Strings
}
erhält man folgende Ausgabe:
max<>() fuer T
11
max<>() fuer T
holla
max<>() fuer T*
11
max<>() fuer char*
otto
Der Aufruf von max() für strings wird explizit für den globalen Namensbereich qualifiziert,
da auch in der Standardbibliothek ein std::max() definiert wird. Da strings zu std gehören,
wird ohne Qualifizierung auch diese Funktion gefunden, was zu einer entsprechenden Mehrdeu-
tigkeit führt (siehe Koenig-Lookup auf Seite 184).
Als Zeiger-Variante kann man auch einfach folgende Funktion definieren:
template <typename T>
T* max (T* a, T* b)
{
return (*a > *b ? a : b);
}
Sandini Bib

7.2 Funktionstemplates 439

Allerdings haben einige Compiler noch das Problem, dass sie beim Aufruf von max() mit zwei
Zeigern diese Version und die Version für const T& als gleichwertig ansehen und eine Mehr-
deutigkeit melden. Dies ist aber ein Fehler der Compiler.

7.2.6 Lokale Variablen


Funktionstemplates dürfen intern beliebige Variablen des Schablonentyps besitzen. So kann z.B.
ein Funktionstemplate, das die Werte zweier Parameter vertauscht, wie folgt implementiert wer-
den1 :
template <class T>
void swap (T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
Die lokalen Variablen dürfen auch statisch sein. In dem Fall wird für jeden Typ, für den die
Funktion aufgerufen wird, im Programm eine statische Variable angelegt.

7.2.7 Zusammenfassung
 Templates sind Schablonen für Code, der nach der Festlegung der tatsächlichen Datentypen
übersetzt wird.
 Das Generieren von übersetzbarem Code aus Template-Code nennt man in C++ instantiieren.
 Templates dürfen mehrere Template-Parameter besitzen.
 Funktionstemplates dürfen überladen werden.

1
Vgl. die Implementierung der Funktion swap() auf Seite 191
Sandini Bib

440 Kapitel 7: Templates

7.3 Klassentemplates
Genauso wie Typen von Funktionen parametrisiert werden können, ist es auch möglich, einen
oder mehrere Typen in Klassen zu parametrisieren. Dies ist vor allem bei Containerklassen sinn-
voll, die dazu dienen, andere Objekte zu verwalten. Klassentemplates bieten die Möglichkeit,
diese zu implementieren, obwohl der Typ der verwalteten Objekte noch nicht feststeht. Typische
Beispiele für Klassentemplates sind Klassen zur Mengenverwaltung, Stacks, verkettete Listen
und so weiter. Im Rahmen der objektorientierten Modellierung werden Templates auch als para-
metrisierbare Klasse bezeichnet.
Nachfolgend wird die Implementierung eines Klassentemplates am Beispiel einer Klasse für
Stacks betrachtet. Diese verwendet intern ein Klassentemplate der Standardbibliothek, vector<>
(siehe Abschnitt 3.5.1 und Abschnitt 9.1.1).

7.3.1 Implementierung des Klassentemplate Stack


Auch für das Klassentemplate für Stacks gilt, dass sich die Deklaration und die Definition in
einer Headerdatei befinden:
// tmpl/stack1.hpp
#include <vector>

namespace Bsp { // ******** Beginn Namensbereich Bsp::

template <typename T>


class Stack {
private:
std::vector<T> elems; // Elemente

public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element
};

// Konstruktor
template <typename T>
Stack<T>::Stack ()
{
// nichts mehr zu tun
}
Sandini Bib

7.3 Klassentemplates 441

template <typename T>


void Stack<T>::push (const T& elem)
{
elems.push_back(elem); // Kopie einkellern
}

template<typename T>
T Stack<T>::pop ()
{
if (elems.empty()) {
throw "Stack<>::pop(): der Stack ist leer";
}
T elem = elems.back(); // oberstes Element merken
elems.pop_back(); // oberstes Element auskellern
return elem; // gemerktes oberstes Element zurückliefern
}

template <typename T>


T Stack<T>::top () const
{
if (elems.empty()) {
throw "Stack<>::top(): der Stack ist leer";
}
return elems.back(); // oberstes Element als Kopie zurückliefern
}

} // ******** Ende Namensbereich Bsp::

Deklaration des Klassentemplates


Beim Template-Code für Klassen gilt zunächst das Gleiche wie bei Funktionstemplates: Die De-
klaration wird von einer Anweisung angeführt, in der T zum Typparameter erklärt wird (selbst-
verständlich können auch bei Klassentemplates mehrere Typparameter definiert werden):
template <typename T>
class Stack {
...
};
Auch hier kann statt typename alternativ das Schlüsselwort class verwendet werden:
template <class T>
class Stack {
...
};
Sandini Bib

442 Kapitel 7: Templates

In der Klasse kann dann der Typ T wie jeder andere Datentyp zur Deklaration von Klassenkom-
ponenten und Elementfunktionen verwendet werden. Im vorliegenden Beispiel wird festgelegt,
dass die Elemente des Stacks intern mit Hilfe eines Vektors für Elemente vom Typ T verwaltet
werden (das Template wird also mit Hilfe eines anderen Templates programmiert); die Funktion
push() verwendet eine konstante T-Referenz als Parameter, und pop() und top() liefern ein
Objekt vom Typ T zurück.
Der Datentyp der Klasse ist Stack<T>, wobei T ein Template-Parameter ist. Deshalb muss
dieser Datentyp auch überall dort verwendet werden, wo der Datentyp der Klasse angegeben
werden muss. Nur bei der Angabe des Klassennamens
class Stack {
...
};
sowie bei den Namen der Konstruktoren und des Destruktors muss nur Stack angegeben werden,
da eindeutig ist, was gemeint ist.
Bei Parametern und Rückgabewerten besteht diese Eindeutigkeit nicht. Sollten diese angege-
ben werden müssen, sähe dies am Beispiel der Deklaration von Copy-Konstruktor und Zuwei-
sungsoperator wie folgt aus:2
template <typename T>
class Stack {
...
Stack (const Stack<T>&); // Copy-Konstruktor
Stack<T>& operator= (const Stack<T>&); // Zuweisungsoperator
...
};

Implementierung der Elementfunktionen


Bei der Definition der Elementfunktionen zu einem Klassentemplate muss ebenfalls jeweils an-
gegeben werden, dass es sich um Funktionstemplates handelt. Auch diese Funktionen sind nur
Schablonen, die bei einer Realisierung der Klasse zu entsprechendem Code führen. Daher muss,
wie das Beispiel push() zeigt, auch der vollständige Datentyp Stack<T> zur Qualifizierung
verwendet werden:
template <typename T>
void Stack<T>::push (const T& elem)
{
elems.push_back(elem); // Kopie einkellern
}

2
Es gibt im Standard allerdings einige Regelungen, die festlegen, wo in einer Klassendeklaration die Anga-
be von Stack statt Stack<T> reicht. Wer wie ich sicher gehen will, verwendet Stack<T> im Zweifelsfall
überall, wo der Datentyp der Klasse benötigt wird.
Sandini Bib

7.3 Klassentemplates 443

Die Funktionen werden dabei auf entsprechende Funktionen des Vektors umgesetzt. Das oberste
Element befindet sich immer hinten.
Man beachte, dass pop_back() zwar das hinterste Element entfernt, es aber nicht zurücklie-
fert. Insofern muss dieses Element vor dem Entfernen zwischengespeichert werden:
template<typename T>
T Stack<T>::pop ()
{
if (elems.empty()) {
throw "Stack<>::pop(): der Stack ist leer";
}
T elem = elems.back(); // oberstes Element merken
elems.pop_back(); // oberstes Element auskellern
return elem; // gemerktes oberstes Element zurückliefern
}
Außerdem muss beachtet werden, dass beim Vektor die Elementfunktionen pop_back() und
back() (letztere liefert das letzte Element) ein undefiniertes Verhalten haben, wenn der Vektor
keine Elemente enthält (siehe Seite 523 und Seite 525). Insofern wird dies bei beiden Funktionen
geprüft, und gegebenenfalls eine Ausnahme vom Typ const char* ausgelöst:
template<typename T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw "Stack<>::top(): der Stack ist leer";
}
return elems.back(); // oberstes Element als Kopie zurückliefern
}
Selbstverständlich könnten die Funktionen auch innerhalb der Klassendeklaration implementiert
werden:
template <typename T>
class Stack {
...
void push (const T& elem) {
elems.push_back(elem); // Kopie einkellern
}
...
};
Sandini Bib

444 Kapitel 7: Templates

7.3.2 Anwendung des Klassentemplate Stack


Wenn ein Objekt des Klassentemplates deklariert oder definiert wird, muss jeweils explizit ange-
geben werden, welcher Typ als Parameter verwendet werden soll:
// tmpl/stest1.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "stack.hpp"

int main()
{
try {
Bsp::Stack<int> intStack; // Stack für Integer
Bsp::Stack<std::string> stringStack; // Stack für Strings

// Integer-Stack manipulieren
intStack.push(7);
std::cout << intStack.pop() << std::endl;

// String-Stack manipulieren
std::string s = "hallo";
stringStack.push(s);
std::cout << stringStack.pop() << std::endl;
std::cout << stringStack.pop() << std::endl;
}
catch (const char* msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Auch hier wird mit der Deklaration die Schablone für den entsprechenden Datentyp realisiert.
Mit der Verwendung von stringStack wird für die Klasse und alle aufgerufenen Elementfunk-
tionen entsprechender Code für den Typ std::string generiert.
Die Betonung liegt dabei auf alle aufgerufenen Elementfunktionen“. Bei Klassentemplates

gilt die Regel, dass nur für die Funktionen Template-Code generiert wird, die auch aufgerufen
werden. Dies hat nicht nur den Vorteil, Zeit und Platz zu sparen. Man kann Klassentemplates
auch für Datentypen verwenden, die gar nicht alle Fähigkeiten aller Elementfunktionen haben,
solange diese nicht wirklich benötigt werden. Ein Beispiel wäre eine Klasse, in der manche
Elementfunktionen mit dem Operator < sortieren. Solange diese Elementfunktionen nicht aufge-
Sandini Bib

7.3 Klassentemplates 445

rufen werden, muss der als Template-Parameter übergebene Datentyp auch nicht den Operator <
definiert haben.
Insgesamt wird in diesem Fall also für zwei Klassen Code generiert. Besitzt ein Klassentem-
plate statische Komponenten, werden entsprechend auch zwei verschiedene statische Kompo-
nenten angelegt.
Ein Klassentemplate bildet für jeden eingesetzten Datentyp einen eigenen Typ, der überall
verwendet werden kann:
void foo (const Bsp::Stack<int>& s) // Parameter s ist int-Stack
{
Bsp::Stack<int> istack[10]; // istack ist ein Feld von zehn int-Stacks
...
}
Dabei wird in der Praxis häufig eine Typedef-Anweisung verwendet, um die Anwendung von
Klassentemplates lesbarer (und flexibler) zu halten:
typedef Bsp::Stack<int> IntStack;

void foo (const IntStack& s) // Parameter s ist int-Stack


{
IntStack istack[10]; // istack ist ein Feld von zehn int-Stacks
...
}
Die Template-Argumente können beliebige Datentypen sein, beispielsweise float-Zeiger oder
sogar ein Stack von Stacks für ints:
Bsp::Stack<float*> floatPtrStack; // Stack von float-Zeigern
Bsp::Stack<Bsp::Stack<int> > intStackStack; // Stack von int-Stacks
Entscheidend ist, dass alle aufgerufenen Operationen für diese Datentypen erlaubt sind.
Man beachte, dass zwei aufeinander folgende schließende Template-Klammern durch ein
Trennzeichen getrennt werden müssen. Ansonsten handelt es sich um den Operator >>, der an
dieser Stelle einen Syntaxfehler auslösen würde:
Bsp::Stack<Bsp::Stack<int>> intStackStack; // FEHLER: >> nicht erlaubt

7.3.3 Spezialisieren von Klassentemplates


Klassentemplates können spezialisiert werden. Das bedeutet, das man ein Klassentemplate für
spezielle Datentypen individuell implementiert. Wie beim Überladen von Funktionstemplates
(siehe Seite 436) ist dies auch wieder sinnvoll, um Implementierungen für bestimmte Daten-
typen zu optimieren oder um bei Datentypen, für die die Template-Implementierung zu einem
Fehlverhalten führt, dieses Fehlverhalten zu vermeiden. Man kann allerdings nicht nur einzelne
Elementfunktionen spezialisieren, sondern muss die Klasse als Ganzes spezialisieren.
Sandini Bib

446 Kapitel 7: Templates

Für eine explizite Spezialisierung muss man vor der Klassendeklaration template<> schreiben
und den festgelegten Template-Datentyp hinter dem Klassennamen angeben:
template<>
class Stack<std::string> {
...
};
Jede Elementfunktion muss entsprechend mit template<> beginnen, und T muss jeweils durch
den festgelegten Template-Datentyp ersetzt werden:
template<>
void Stack<std::string>::push (const std::string& elem)
{
elems.push_back(elem); // Kopie einkellern
}
Hier ist ein vollständiges Beispiel einer Spezialisierung des Klassentemplates Stack<> für den
Datentyp std::string:
// tmpl/stack2.hpp
#include <deque>
#include <string>

namespace Bsp { // ******** Beginn Namensbereich Bsp::

template<>
class Stack<std::string> {
private:
std::deque<std::string> elems; // Elemente

public:
Stack() { // Konstruktor
}
void push(const std::string&); // Element einkellern
std::string pop(); // Element auskellern
std::string top() const; // oberstes Element
};

template<>
void Stack<std::string>::push (const std::string& elem)
{
elems.push_back(elem); // Kopie einkellern
}
Sandini Bib

7.3 Klassentemplates 447

template<>
std::string Stack<std::string>::pop ()
{
if (elems.empty()) {
throw "Stack<std::string>::pop(): der Stack ist leer";
}
std::string elem = elems.back(); // oberstes Element merken
elems.pop_back(); // oberstes Element auskellern
return elem; // gemerktes oberstes Element zurückliefern
}

template<>
std::string Stack<std::string>::top () const
{
if (elems.empty()) {
throw "Stack<std::string>::top(): der Stack ist leer";
}
return elems.back(); // oberstes Element als Kopie zurückliefern
}

} // ******** Ende Namensbereich Bsp::


In diesem Fall wurde für Strings der interne Vektor für die Elemente durch eine Deque ersetzt.
Dies hat zwar keinen entscheidenden Vorteil, doch zeigt es, dass die Implementierung eines
Klassentemplates für einen speziellen Datentyp völlig anders aussehen kann.
Die in der Standardbibliothek definierten numerischen Limits sind ein weiteres Beispiel für
die Verwendung von Template-Spezialisierungen (siehe Abschnitt 9.1.4).

Partielle Spezialisierungen
Man kann Templates auch nur teilweise spezialisieren (partiell spezialisieren).
Zum Klassentemplate
template <typename T1, typename T2>
class MeineKlasse {
...
};
kann es z.B. folgende partielle Spezialisierungen geben:
// partielle Spezialisierung: beide Typen sind gleich
template <typename T>
class MeineKlasse<T,T> {
...
};
Sandini Bib

448 Kapitel 7: Templates

// partielle Spezialisierung: der zweite Typ ist ein int


template <typename T>
class MeineKlasse<T,int> {
...
};

// partielle Spezialisierung: beide Typen sind Zeiger


template <typename T1, typename T2>
class MeineKlasse<T1*,T2*> {
...
};
Diese Templates werden nun wie folgt angesprochen:
MeineKlasse<int,float> mif; // MeineKlasse<T1,T2>
MeineKlasse<float,float> mff; // MeineKlasse<T,T>
MeineKlasse<float,int> mfi; // MeineKlasse<T,int>
MeineKlasse<int*,float*> mp; // MeineKlasse<T1*,T2*>
Passen mehrere partielle Spezialisierungen gleich gut, ist der Aufruf mehrdeutig:
MeineKlasse<int,int> m; // FEHLER: passt auf MeineKlasse<T,T>
// und MeineKlasse<T,int>
MeineKlasse<int*,int*> m; // FEHLER: passt auf MeineKlasse<T,T>
// und MeineKlasse<T1*,T2*>
Die letzte Mehrdeutigkeit würde aufgehoben werden, wenn auch eine partielle Spezialisierung
für Zeiger vom gleichen Typ definiert wäre:
template <typename T>
class MeineKlasse<T*,T*> {
...
};

7.3.4 Default Template-Parameter


Auch für Template-Parameter kann man Default-Werte definieren. Diese können sich auch auf
vorherige Template-Parameter beziehen.
So kann man z.B. den Container, mit dem Stacks die Elemente verwalten, parametrisieren
und dabei als Default einen Vektor festlegen:
// tmpl/stack3.hpp
#include <vector>

namespace Bsp { // ******** Beginn Namensbereich Bsp::


Sandini Bib

7.3 Klassentemplates 449

template <typename T, typename CONT = std::vector<T> >


class Stack {
private:
CONT elems; // Elemente

public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element
};

// Konstruktor
template <typename T, typename CONT>
Stack<T,CONT>::Stack ()
{
// nichts mehr zu tun
}

template <typename T, typename CONT>


void Stack<T,CONT>::push (const T& elem)
{
elems.push_back(elem); // Kopie einkellern
}

template <typename T, typename CONT>


T Stack<T,CONT>::pop ()
{
if (elems.empty()) {
throw "Stack<>::pop(): der Stack ist leer";
}
T elem = elems.back(); // oberstes Element merken
elems.pop_back(); // oberstes Element auskellern
return elem; // gemerktes oberstes Element zurückliefern
}

template <typename T, typename CONT>


T Stack<T,CONT>::top () const
{
if (elems.empty()) {
throw "Stack<>::top(): der Stack ist leer";
Sandini Bib

450 Kapitel 7: Templates

}
return elems.back(); // oberstes Element als Kopie zurückliefern
}

} // ******** Ende Namensbereich Bsp::


Diesen Stack könnte man wie die bisherigen Stacks verwenden. Zusätzlich kann man aber selbst
den Container für die Elemente festlegen:
// tmpl/stest3.cpp
#include <iostream>
#include <deque>
#include <cstdlib>
#include "stack.hpp"

int main()
{
try {
Bsp::Stack<int> intStack; // Stack für Integer
Bsp::Stack<double,std::deque<double> >
dblStack; // Stack für Gleitkommawerte

// Integer-Stack manipulieren
intStack.push(7);
std::cout << intStack.pop() << std::endl;

// Gleitkommawerte-Stack manipulieren
dblStack.push(42.42);
std::cout << dblStack.pop() << std::endl;
std::cout << dblStack.pop() << std::endl;
}
catch (const char* msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Mit
Bsp::Stack<double,std::deque<double> >
wird z.B. ein Stack für Gleitkommawerte deklariert, der intern als Container eine Deque verwen-
det.
Sandini Bib

7.3 Klassentemplates 451

7.3.5 Zusammenfassung
 Auch Klassen können als Templates für noch nicht festgelegte Datentypen implementiert
werden.
 Mit Klassentemplates können insbesondere Containerklassen, die andere Objekte verwalten,
in Hinblick auf den Typ der verwalteten Objekte parametrisiert werden.
 Für Klassentemplates werden nur die Funktionen instantiiert, die auch benötigt werden.
 Implementierungen von Klassentemplates können für bestimmte Datentypen spezialisiert
werden. Dabei ist auch eine partielle Spezialisierung möglich.
 Für Template-Parameter ist es möglich, Default-Werte anzugeben.
Sandini Bib

452 Kapitel 7: Templates

7.4 Werte als Template-Parameter


Template-Argumente müssen nicht unbedingt Datentypen, sondern können wie bei Funktionen
auch ganz normale Werte sein. Dies ist recht nützlich, wenn nicht nur der Datentyp einer Klasse
parametrisiert werden soll. Diese Möglichkeit existiert sowohl für Klassen- als auch für Funkti-
onstemplates.

7.4.1 Beispiel für die Verwendung von Werte-Parametern


So ließe sich z.B. ein Stack-Template definieren, bei dem der Stack für seine Elemente ein Feld
(Array) erhält, dessen Größe beim Erzeugen des Stacks festgelegt wird. Dies hätte den Vorteil,
dass man im Stack den Aufwand der dynamischen Speicherverwaltung einspart.
Die Klassendeklaration sieht dann wie folgt aus:
// tmpl/stack4.hpp
namespace Bsp { // ******** Beginn Namensbereich Bsp::

template <typename T, int MAXSIZE>


class Stack {
private:
T elems[MAXSIZE]; // Elemente
int numElems; // aktuelle Anzahl eingetragener Elemente

public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element
};

// Konstruktor
template <typename T, int MAXSIZE>
Stack<T,MAXSIZE>::Stack ()
: numElems(0) // kein Elemente
{
// nichts mehr zu tun
}

template <typename T, int MAXSIZE>


void Stack<T,MAXSIZE>::push (const T& elem)
{
if (numElems == MAXSIZE) {
Sandini Bib

7.4 Werte als Template-Parameter 453

throw "Stack<>::push(): der Stack ist voll";


}
elems[numElems] = elem; // Element eintragen
++numElems; // Anzahl der Elemente erhöhen
}

template<typename T, int MAXSIZE>


T Stack<T,MAXSIZE>::pop ()
{
if (numElems <= 0) {
throw "Stack<>::pop(): der Stack ist leer";
}
--numElems; // Anzahl der Elemente herabsetzen
return elems[numElems]; // bisheriges oberstes Element zurückliefern
}

template <typename T, int MAXSIZE>


T Stack<T,MAXSIZE>::top () const
{
if (numElems <= 0) {
throw "Stack<>::top(): der Stack ist leer";
}
return elems[numElems-1]; // oberstes Element zurückliefern
}

} // ******** Ende Namensbereich Bsp::


Der zweite Parameter des Stack-Templates, MAXSIZE, wird verwendet, um die Größe des Stacks
festzulegen. Dieser Parameter wird jetzt nicht nur zum Deklarieren des Feldes, sondern auch in
push() verwendet, um festzustellen, ob der Stack bereits voll ist.
Bei der Anwendung dieses Stack-Templates müssen sowohl der Typ der verwalteten Elemen-
te als auch die Größe des Stacks angegeben werden:
// tmpl/stest4.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "stack.hpp"

int main()
{
try {
Sandini Bib

454 Kapitel 7: Templates

Bsp::Stack<int,20> int20Stack; // Stack für 20 ints


Bsp::Stack<int,40> int40Stack; // Stack für 40 ints
Bsp::Stack<std::string,40> stringStack; // Stack für 40 Strings

// Integer-Stack manipulieren
int20Stack.push(7);
std::cout << int20Stack.pop() << std::endl;

// String-Stack manipulieren
std::string s = "hallo";
stringStack.push(s);
std::cout << stringStack.pop() << std::endl;
std::cout << stringStack.pop() << std::endl;
}
catch (const char* msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Zu beachten ist, dass int20Stack und int40Stack in diesem Fall verschiedene Typen besitzen
und folglich einander nicht zugewiesen oder wechselseitig verwendet werden können.
Selbstverständlich könnte man auch in diesem Fall für die Template-Parameter Default-Werte
festlegen:
template <typename T = int, int MAXSIZE = 100>
class Stack {
...
};
Ich halte dies in diesem Fall jedoch für nicht sinnvoll. Default-Werte sollten intuitiv richtig sein.
Der Datentyp int als Elementtyp und eine Maximalgröße von 100 sind aber jeweils nicht intuitiv.
Insofern ist es besser, wenn der Anwendungsprogrammierer beides sichtbar explizit festlegen
muss.

7.4.2 Einschränkungen bei Werte-Parametern


Bei der Verwendung anderer Template-Argumente gibt es allerdings Einschränkungen: Argu-
mente von Templates dürfen neben Typen nur konstante Ausdrücke oder Adressen von Objekten
oder Funktionen sein, auf die im Programm global zugegriffen werden kann.
Klassen sind als Werte-Parameter nicht erlaubt:
template <class T, std::string name> // FEHLER: Klassen als Parameter-Typ
class KLASSE { // nicht erlaubt
...
};
Sandini Bib

7.4 Werte als Template-Parameter 455

Aber auch die Verwendung von String-Literalen führt zu Problemen:


template <class T, const char* name>
class KLASSE {
...
};

KLASSE<int,"hallo"> x; // FEHLER: String-Literal "hallo" nicht erlaubt


String-Literale gelten nämlich nicht als globale Objekte, die überall im Programm verfügbar
sind. Wenn in zwei verschiedenen Modulen "hallo" definiert wird, handelt es sich um zwei
verschiedenen Strings.
Da hilft auch die Verwendung eines globalen Zeigers nicht:
template <class T, const char* name>
class KLASSE {
...
};

const char* s = "hallo";


KLASSE<int,s> x; // FEHLER: "hallo" ist immer noch statisch
Der Zeiger s mag zwar ein globaler Zeiger sein; das, worauf er zeigt, ist aber immer noch nicht
global.
Die folgende Anwendung ist dagegen möglich:
template <class T, const char* name>
class KLASSE {
...
};

extern const char s[] = "hallo";

void foo() {
KLASSE<int,s> x; // OK
...
}
Das globale Feld s wird mit der Zeichenfolge "hallo" initialisiert, wodurch s die globale Zei-
chenfolge "hallo" ist.

7.4.3 Zusammenfassung
 Templates können auch Parameter besitzen, die keine Datentypen, sondern Werte sind.
 Diese Werte dürfen keine Objekte und nicht lokal sein.
 Damit ist die Verwendung von String-Literalen als Template-Argumente nur eingeschränkt
möglich.
Sandini Bib

456 Kapitel 7: Templates

7.5 Weitere Aspekte von Templates


In diesem Abschnitt werden weitere, noch nicht erläuterte aber für die praktische Arbeit rele-
vante Aspekte von Templates vorgestellt. Dabei handelt es sich zum einen um das Schlüsselwort
typename und zum anderen um die Möglichkeit, Komponenten als Templates zu definieren.
Schließlich wird noch auf die Möglichkeit eingegangen, Polymorphie mit Hilfe von Templates
zu implementieren.

7.5.1 Das Schlüsselwort typename


Das Schlüsselwort typename wurde im Rahmen der Standardisierung von C++ eingeführt, um
in Code, der Templates verwendet, definieren zu können, dass ein Symbol im Template ein Da-
tentyp ist.
Man betrachte dazu folgendes Beispiel:
template <typename T>
class MeineKlasse {
typename T::SubTyp * ptr;
...
};
In diesem Fall wird typename verwendet, um festzulegen, dass SubTyp ein Datentyp ist, der
in der Klasse T definiert wird. Entsprechend deklariert diese Stelle ptr als Zeiger auf diesen
SubTyp.
Ohne typename würde davon ausgegangen werden, dass es sich bei SubTyp um einen stati-
schen Wert (Variable oder Objekt) der Klasse handelt. In dem Fall wäre
T::SubTyp * ptr
die Multiplikation dieses Wertes mit dem Wert ptr.
Eine typisches Beispiel für die Verwendung von typename sind Funktionstemplates, in denen
über Iteratoren auf STL-Container zugegriffen wird (vgl. Seite 79):
// tmpl/ausgeben.hpp
#include <iostream>

// Elemente eines STL-Containers ausgeben


template <typename T>
void ausgeben (const T& menge)
{
// Anzahl der Elemente ausgeben
std::cout << "Anzahl Elemente: " << menge.size() << std::endl;

// Elemente selbst ausgeben


std::cout << "Elemente: ";
typename T::const_iterator pos;
Sandini Bib

7.5 Weitere Aspekte von Templates 457

for (pos=menge.begin(); pos!=menge.end(); ++pos) {


std::cout << *pos << ' ';
}
std::cout << std::endl;
}
In diesem Funktionstemplate wird als Parameter ein STL-Container vom Typ T erwartet. Die
Ausgabe der Elemente erfolgt mit Hilfe eines darin definierten Iterators. Dieser Iterator ist ein
Hilfsdatentyp, der vom Container definiert wird. Entsprechend muss seine Deklaration mit dem
Schlüsselwort typename durchgeführt werden:
typename T::const_iterator pos; // const_iterator ist Hilfstyp von T

7.5.2 Komponenten als Templates


Auch Komponenten von Klassen können Templates sein. Dies gilt sowohl für innere Hilfsklassen
als auch für Elementfunktionen.
Den Einsatz und den Nutzen dieser Möglichkeit kann man wieder am Beispiel der Klasse
stack<> verdeutlichen. Normalerweise kann man Stacks nur an Stacks zuweisen, deren Elemen-
te den gleichen Datentyp besitzen. Eine Zuweisung eines Stacks mit Elementen eines anderen
Datentyps ist nicht möglich, da der Default-Zuweisungsoperator verlangt, dass beide Seiten der
Zuweisung den gleichen Datentyp haben, dies aber bei Stacks für verschiedene Elementtypen
nicht der Fall ist.
Indem ein Zuweisungsoperator als Template definiert wird, kann man diese Möglichkeit aber
schaffen. Dazu wird die Klasse stack<> wie folgt deklariert:
template <typename T>
class Stack {
private:
std::deque<T> elems; // Elemente

public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element

typename std::deque<T>::size_type size() const {


return elems.size();
}

// Stack für Elemente vom Typ T2 zuweisen


template <typename T2>
Stack<T>& operator= (const Stack<T2>&);
};
Sandini Bib

458 Kapitel 7: Templates

Folgende Dinge wurden geändert:


 Die Deklaration einer Zuweisung eines Stacks von Elementen eines anderen Datentyps T2.
 Eine Elementfunktion size(), die die aktuelle Anzahl der Elemente im Stack liefert. Sie
wird, wie wir noch sehen werden, zur Implementierung der Zuweisung benötigt. Als Rückga-
betyp wird einfach der Rückgabetyp des verwendeten Containers für die Elemente verwendet
(man beachte die auch hier notwendige Verwendung von typename).
 Der Stack verwendet intern eine Deque. Auch dies liegt an der Implementierung des Zuwei-
sungsoperators.
Der Zuweisungsoperator für Stacks von Elementen eines anderen Typs wird wie folgt implemen-
tiert:
template <typename T>
template <typename T2>
Stack<T>& Stack<T>::operator= (const Stack<T2>& op2)
{
Stack<T2> tmp(op2); // Kopie des zuzuweisenden Stacks anlegen

elems.clear(); // bisherige Elemente entfernen


while (tmp.size() > 0) { // alle Elemente kopieren
elems.push_front(tmp.pop()); // Kopie jeweils vorn einfügen
}
return *this;
}
Man betrachte zunächst die Syntax zur Definition von Templates, die in Klassentemplates de-
finiert werden. Im Template mit dem Parameter T wird ein Template mit dem Parameter T2
definiert:
template <typename T>
template <typename T2>
...
Bei der Implementierung könnte man meinen, dass es ausreicht, einfach auf alle Elemente des
übergebenen Stacks op2 direkt zuzugreifen und sie in den eigenen Stack zu kopieren. Doch an
dieser Stelle ist erneut die Tatsache zu berücksichtigen, dass Templates für unterschiedliche Da-
tentypen selbst unterschiedliche Datentypen sind. Aus diesem Grund kann in dieser Funktion
nicht auf Interna von op2 zugegriffen werden. Es kann nur die öffentliche Schnittstelle von op2
verwendet werden. Die einzige Chance, an die Elemente über die öffentliche Schnittstelle her-
anzukommen, besteht aber über die Elementfunktion pop(). Aus diesem Grund wird erst eine
Kopie des übergebenen Stacks angelegt, aus der die Elemente jeweils mit pop() herausgeholt
werden und in den eigenen Stack eingetragen werden. Da mit dem ersten pop() das Element
geliefert wird, das zuletzt eingekellert werden muss, wird nicht push() oder push_back(),
sondern push_front() aufgerufen. Dies erfordert aber einen Container, der push_front()
unterstützt. Deshalb wird für die Elemente nun eine Deque verwendet.
Sandini Bib

7.5 Weitere Aspekte von Templates 459

Wer meint, durch diese Funktion sei die Typprüfung ausgeschaltet worden, der irrt. Man kann
nach wie vor keine Stacks einander zuweisen, deren Elementtypen nicht einander zuweisbar sind.
Wird z.B. versucht, einen Stack von Strings einem Stack von ints zuzuweisen, wird für die Zeile
elems.push_front(tmp.pop()); // Kopie jeweils vorn einfügen
eine Fehlermeldung erzeugt, da tmp.pop() einen String liefert, den man nicht als int verwen-
den kann.
Man beachte auch, dass eine Template-Version eines Zuweisungsoperators nicht den Default-
Zuweisungsoperator überdeckt. Der Default-Zuweisungsoperator wird nach wie vor generiert
und bei einer Zuweisung zwischen zwei Stacks mit dem gleichen Elementtyp auch aufgerufen.
Man kann die Implementierung auch hier so verändern, dass als Elemente-Container auch
ein Vektor verwendet werden kann. Dazu muss die Deklaration wie folgt aussehen:
template <typename T, typename CONT = std::deque<T> >
class Stack {
private:
CONT elems; // Elemente

public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element

typename CONT::size_type size() const {


return elems.size();
}

// Stack für Elemente vom Typ T2 zuweisen


template <typename T2, typename CONT2>
Stack<T,CONT>& operator= (const Stack<T2,CONT2>&);
};
Da bei einem Klassentemplate nur die wirklich verwendeten Funktionen instantiiert und über-
setzt werden, kann auch ein Stack angelegt werden, der für die Elemente einen Vektor verwendet:

// Stack für ints, der einen Vektor verwendet


Bsp::Stack<int,std::vector<int> > vStack;
...
vStack.push(42);
vStack.push(7);
std::cout << vStack.pop() << std::endl;
Sandini Bib

460 Kapitel 7: Templates

Solange nicht versucht wird, einen Stack mit Elementen eines anderen Datentyps zuzuweisen,
handelt es sich um ein korrektes Programm.
Das komplette Beispiel befindet sich in den beiden Dateien tmpl/stack5.hpp und
tmpl/stest5.cpp. Wundern Sie sich nicht, wenn Ihr Compiler Fehler meldet. Wir nutzen hier
fast alle wichtigen Sprachmittel von Templates aus.

7.5.3 Statische Polymorphie mit Templates


Normalerweise wird die Polymorphie in C++ durch die Vererbung implementiert (siehe Ab-
schnitt 5.3). Es gibt jedoch auch die Möglichkeit, Polymorphie mit Templates zu implementieren.
Darauf wird in diesem Abschnitt eingegangen.

Dynamische Polymorphie
Verwendet man die Vererbung zur Implementierung von Polymorphie, dann definiert eine (ab-
strakte) Basisklasse die gemeinsame Schnittstelle (den Oberbegriff) zu einer Reihe von konkreten
Datentypen.
Hat man z.B. eine Gruppe von geometrischen Klassen, könnte es (wie in Abschnitt 5.3.3
vorgestellt) eine abstrakte Basisklasse GeoObj geben, von der alle möglichen konkreten Klassen
abgeleitet sind (siehe Abbildung 7.1).

G e o O b j
d r a w ( )
p o s i t i o n ( )
. . .

P u n k t L i n i e K r e i s
d r a w ( ) d r a w ( ) d r a w ( )
p o s i t i o n ( ) p o s i t i o n ( ) p o s i t i o n ( )
. . . . . . . . .

Abbildung 7.1: Mit Vererbung implementierte dynamische Polymorphie

Um mit dem gemeinsamen Oberbegriff zu programmieren, werden dann einfach Referenzen oder
Zeiger auf Objekte der Basisklasse verwendet. Dies mag z.B. wie folgt aussehen:
// tmpl/dynapoly.cpp
// beliebiges geometrisches Objekt zeichnen
void zeichne (const GeoObj& obj) {
obj.draw();
}
Sandini Bib

7.5 Weitere Aspekte von Templates 461

// Abstand zwischen zwei geometrischen Objekten berechnen


Koord abstand (const GeoObj& x1, const GeoObj& x2) {
Koord a = x1.position() - x2.position();
return a.abs();
}

// inhomogene Menge von geometrischen Objekten ausgeben


void ausgeben (const std::vector<GeoObj*>& elems) {
for (unsigned i=0; i<elems.size(); ++i) {
elems[i]->draw();
}
}

int main()
{
Linie l;
Kreis k, k1, k2;

zeichne(l); // zeichne(GeoObj&) => Linie::draw()


zeichne(k); // zeichne(GeoObj&) => Kreis::draw()

abstand(k1,k2); // abstand(GeoObj&,GeoObj&)
abstand(l,k); // abstand(GeoObj&,GeoObj&)

std::vector<GeoObj*> menge; // inhomogene Menge


menge.push_back(&l); // Linie einfügen
menge.push_back(&k); // Kreis einfügen
ausgeben(menge); // Menge ausgeben
}
Die Funktionen werden jeweils für den Datentyp GeoObj übersetzt. Je nachdem, welche Ar-
gumente zur Laufzeit übergeben werden, werden diese Argumente dann jeweils als GeoObjs
betrachtet. Damit wird z.B. in zeichne() zur Laufzeit entschieden, welches draw() jeweils für
das geometrische Objekt aufgerufen werden muss. Entsprechend wird z.B. in abstand() zur
Laufzeit entschieden, welche Elementfunktion position() jeweils für ein geometrisches Ob-
jekt aufgerufen werden muss. Eine inhomogene Menge von geometrischen Objekten kann mit
Hilfe eines Zeigers auf den Datentyp GeoObj deklariert werden (alternativ empfiehlt sich die
Verwendung eines Smart-Pointers, siehe Seite 539).

Statische Polymorphie
Man kann statt der Vererbung aber auch Templates für den Einsatz von Polymorphie verwenden.
In diesem Fall gibt es keine Basisklasse, die die gemeinsamen Eigenschaften explizit festlegt.
Stattdessen werden die notwendigen Eigenschaften implizit als Operationen eines entsprechen-
den Template-Parameters festgelegt.
Sandini Bib

462 Kapitel 7: Templates

Das vorherige Beispielprogramm kann dann wie folgt umgeschrieben werden:


// tmpl/staticpoly.cpp
// beliebiges geometrisches Objekt zeichnen
template <typename GeoObj>
void zeichne (const GeoObj& obj) {
obj.draw();
}

// Abstand zwischen zwei geometrischen Objekten berechnen


template <typename GeoObj1, typename GeoObj2>
Koord abstand (const GeoObj1& x1, const GeoObj2& x2) {
Koord a = x1.position() - x2.position();
return a.abs();
}

// inhomogene Menge von geometrischen Objekten ausgeben


template <typename GeoObj>
void ausgeben (const std::vector<GeoObj>& elems) {
for (unsigned i=0; i<elems.size(); ++i) {
elems[i].draw();
}
}

int main()
{
Linie l;
Kreis k;
Kreis k1, k2;

zeichne(l); // zeichne<Linie>(GeoObj&) => Linie::draw()


zeichne(k); // zeichne<Kreis>(GeoObj&) => Kreis::draw()

abstand(k1,k2); // abstand<Kreis,Kreis>(GeoObj&,GeoObj&)
abstand(l,k); // abstand<Linie,Kreis>(GeoObj&,GeoObj&)

// std::vector<GeoObj*> menge; // FEHLER: keine inhomogene Menge möglich


std::vector<Linie> menge; // OK: homogene Menge
menge.push_back(l); // Linie einfügen
ausgeben(menge); // Menge ausgeben
}
Sandini Bib

7.5 Weitere Aspekte von Templates 463

Bei den ersten beiden Funktionen, zeichne() und abstand(), wurde der Datentyp GeoObj zu
einem Template-Parameter. Dabei wurde in abstand() durch die Verwendung von zwei ver-
schiedenen Template-Parametern auch die Übergabe zweier verschiedener Arten von geometri-
schen Objekten ermöglicht:
abstand(l,c); // abstand<Linie,Kreis>(GeoObj&,GeoObj&)
Eine inhomogene Menge ist mit Templates nicht mehr möglich. Allerdings muss dafür der Da-
tentyp der Elemente in der Menge nicht mehr als Zeiger deklariert werden:
std::vector<Linie> menge; // OK: homogene Menge
Dies kann signifikante Vorteile haben.

Dynamische versus statische Polymorphie


Die beiden Formen von Polymorphie in C++ kann man wie folgt beschreiben:
 Mit Hilfe von Vererbung implementierte Polymorphie ist gebunden und dynamisch:
– Gebunden bedeutet, dass die konkreten Datentypen von einem anderen Datentyp (nämlich
der Basisklasse) abhängen.
– Dynamisch bedeutet, dass die Bindung zwischen Funktionsaufrufen und den dazugehöri-
gen Funktionen zur Laufzeit entschieden wird.
 Mit Hilfe von Templates implementierte Polymorphie ist ungebunden und statisch:
– Ungebunden bedeutet, dass die konkreten Datentypen keinerlei Abhängigkeiten zu ande-
ren Datentypen besitzen.
– Statisch bedeutet, dass die Bindung zwischen Funktionsaufrufen und den dazugehörigen
Funktionen zur Kompilierzeit entschieden wird.
Dynamische Polymorphie ist somit eigentlich eine abkürzende Bezeichnung für gebundene dy-
namische Polymorphie. Entsprechend ist statische Polymorphie somit eigentlich eine abkürzende
Bezeichnung für ungebundene statische Polymorphie. In anderen Programmiersprachen existie-
ren andere Kombinationen (Smalltalk unterstützt z.B. die ungebundene dynamische Polymor-
phie).
Die Vorteile von dynamischer Polymorphie sind:
 Sie ermöglicht inhomogene Mengen.
 Sie benötigt weniger Code (die Funktionen werden nur einmal für den Typ GeoObj kompi-
liert).
 Polymorphe Operationen können als Objektcode ausgeliefert werden (Template-Code muss
als Quellcode ausgeliefert werden).
 Die Fehlerbehandlung der Compiler ist besser (siehe Abschnitt 7.6.2).
Die Vorteile von statischer Polymorphie sind dagegen:
 Sie führt zu besserem Laufzeitverhalten (da es keine virtuellen Funktionen gibt, sind bessere
Optimierungen möglich). Erfahrungen zeigen, dass ein Faktor von zwei bis zehn möglich ist.
Sandini Bib

464 Kapitel 7: Templates

 Sie ist ungebunden (Klassen sind von keinem anderen Code abhängig). Damit ist insbeson-
dere auch die Verwendung von fundamentalen Datentypen möglich.
 Sie vermeidet die Verwendung von Zeigern.
 Konkrete Datentypen müssen nicht die gesamte Schnittstelle bereitstellen (Templates brau-
chen nur die Operationen, die auch aufgerufen werden).
In Hinsicht auf Typsicherheit haben beide Formen ebenfalls Vor- und Nachteile: Bei dynami-
scher Polymorphie muss man den Oberbegriff explizit angeben. Damit kann es nicht passieren,
dass eine Klasse als geometrisches Objekt verwendet werden kann, nur weil sie die entsprechen-
den Operationen anbietet. Auf der anderen Seite wird die Typsicherheit von homogenen Mengen
bei dynamischer Polymorphie nicht gewährleistet. Um sicherzustellen, dass sich in einer Men-
ge geometrischer Objekte nur Linien befinden, müssen entsprechende Abfragen programmiert
werden, die zur Laufzeit stattfinden.
Beide Arten haben also ihre Vor- und Nachteile. Aufgrund des möglichen besseren Laufzeit-
verhaltens sollte man in der Praxis aber darüber nachdenken, statische Polymorphie zu verwen-
den, wenn Parameter zur Kompilierzeit bekannt sind und keine inhomogenen Mengen benötigt
werden.

7.5.4 Zusammenfassung
 Wird bei der Verwendung eines Template-Parameters auf einen dazu definierten Hilfstyp zu-
gegriffen, muss dieser mit typename qualifiziert werden.
 Auch innere Klassen und Elementfunktionen können Templates sein. Damit kann man auch
bei Operationen von Klassentemplates implizite Typumwandlungen ermöglichen. Eine Typ-
prüfung findet aber nach wie vor statt.
 Template-Versionen des Zuweisungsoperators überdecken nicht den Default-Zuweisungs-
operator.
 Mit Templates kann man Polymorphie implementieren. Dies hat Vor- und Nachteile.
Sandini Bib

7.6 Templates in der Praxis 465

7.6 Templates in der Praxis


Templates sind eine neue Form von Quellcode. Er kann syntaktisch geprüft werden, zu Objekt-
code kann er aber erst werden, wenn die Datentypen feststehen. Dieses neue Konzept schafft
einige Probleme, auf die in diesem Abschnitt eingegangen wird.

7.6.1 Übersetzen von Template-Code


Templates werden eigentlich zweimal geprüft: einmal syntaktisch beim Kompilieren des Tem-
plate-Codes und ein weiteres Mal beim Kompilieren des für die tatsächlichen Datentypen ge-
nerierten Codes. Wie in Abschnitt 7.2.3 bereits erläutert wurde, sprengt dies das herkömmliche
Compiler/Linker-Modell.
Aus diesem Grund ist es am einfachsten, Templates komplett in Headerdateien unterzubrin-
gen. Diese Vorgehensweise hat allerdings auch erhebliche Nachteile:
 Zum einen wird immer wieder der gleiche Code kompiliert. Jedes Modul, das Stacks für
Elemente vom Typ int verwendet, kompiliert den dafür generierten Code erneut. Dies kostet
nicht nur Übersetzungszeit, sondern hat auch zur Folge, dass der übersetzte Code in meh-
reren Objektdateien vorhanden ist. Bindet man diese Objektdateien zu einem ausführbaren
Programm zusammen, braucht man entweder Linker, die den mehrfach vorhandenen Code
entfernen, oder man erhält unnötig große ausführbare Programme.
 Zum anderen ergibt sich das Problem, dass man Template-Code nur als Quellcode ausliefern
kann. Dies ist vor allem dann kritisch, wenn der Code einen wesentlichen Teil des Know-
hows einer Firma darstellt, das man natürlich nicht preisgeben kann, da man ansonsten seinen
Wettbewerbsvorteil verliert.
Es gibt zwei andere portable Möglichkeiten, mit Templates umzugehen, die hier nun vorgestellt
werden. Jede dieser Möglichkeiten hat allerdings auch Nachteile. Eine perfekte Lösung gibt es
also (bisher) nicht.
Hinzu kommen mögliche proprietäre Lösungen einzelner Hersteller. So könnte man z.B.
spezielle Präprozessor-Anweisungen für den Umgang mit Templates definieren. Auf derartige
Lösungen wird hier nicht eingegangen.

Explizite Instantiierung
Eine portable Möglichkeit, die mehrfache Übersetzung des gleichen Template-Codes zu vermei-
den, bietet die Technik der expliziten Instantiierung oder expliziten Generierung.
In diesem Fall werden die Templates in Headerdateien wirklich nur noch deklariert:
// tmpl/expl1.hpp
// Deklaration des Funktions-Templates max()
template <typename T>
const T& max (const T& a, const T& b);

// Deklaration des Klassen-Templates Stack<>


#include <vector>
Sandini Bib

466 Kapitel 7: Templates

namespace Bsp { // ******** Beginn Namensbereich Bsp::

template <typename T>


class Stack {
private:
std::vector<T> elems; // Elemente
public:
Stack(); // Konstruktor
void push(const T&); // Element einkellern
T pop(); // Element auskellern
T top() const; // oberstes Element
};

} // ******** Ende Namensbereich Bsp::


Die Definition der Templates befindet sich in einer separaten Headerdatei, die die Headerdatei
mit den Deklarationen einbindet:
// tmpl/expldef1.hpp
#include "expl.hpp"

// Definition des Funktions-Templates max()


template <typename T>
const T& max (const T& a, const T& b)
{
return (a > b ? a : b);
}

// Definition der Funktionen des Klassen-Templates Stack<>

namespace Bsp { // ******** Beginn Namensbereich Bsp::

// Konstruktor
template <typename T>
Stack<T>::Stack ()
{
// nichts mehr zu tun
}

template <typename T>


void Stack<T>::push (const T& elem)
{
Sandini Bib

7.6 Templates in der Praxis 467

elems.push_back(elem); // Kopie einkellern


}

template<typename T>
T Stack<T>::pop ()
{
if (elems.empty()) {
throw "Stack<>::pop(): der Stack ist leer";
}
T elem = elems.back(); // oberstes Element merken
elems.pop_back(); // oberstes Element auskellern
return elem; // gemerktes oberstes Element zurückliefern
}

template <typename T>


T Stack<T>::top () const
{
if (elems.empty()) {
throw "Stack<>::top(): der Stack ist leer";
}
return elems.back(); // oberstes Element als Kopie zurückliefern
}

} // ******** Ende Namensbereich Bsp::


Nun kann man nur durch die Einbindung der Deklarationen die Templates verwenden:
// tmpl/expltest1.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "expl.hpp"

int main()
{
try {
Bsp::Stack<int> intStack; // Stack für Integer
Bsp::Stack<std::string> stringStack; // Stack für Strings

// Integer-Stack manipulieren
intStack.push(7);
intStack.push(max(intStack.top(),42)); // max() für ints
Sandini Bib

468 Kapitel 7: Templates

std::cout << intStack.pop() << std::endl;

// String-Stack manipulieren
std::string s = "hallo";
stringStack.push(s);
std::cout << stringStack.pop() << std::endl;
std::cout << stringStack.pop() << std::endl;
}
catch (const char* msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Die benötigten Instantiierungen kann man separat explizit generieren:
// tmpl/expldef1.cpp
#include <string>
#include "expldef.hpp"

// benötigte Funktions-Templates explizit instanziieren


template const int& max (const int&, const int&);

// benötigte Klassen-Templates explizit instanziieren


template Bsp::Stack<int>;
template Bsp::Stack<std::string>;
Eine derartige explizite Instantiierung erkennt man daran, dass nach dem Schlüsselwort template
nicht sofort die spitze öffnende Klammer folgt. Wie man sieht kann man einzelne Funktionen
und auch ganze Klassen explizit instantiieren. Bei Klassen werden dabei alle Elementfunktionen
instantiiert.
Man kann bei Klassen auch explizit nur einzelne Funktionen instantiieren. Das vorliegende
Anwendungsprogramm kann auch mit folgenden Instantiierungen aufgerufen werden:
// tmpl/expldef2.cpp
#include <string>
#include "expldef.hpp"

// benötigte Funktions-Templates explizit instanziieren


template const int& max (const int&, const int&);

// benötigte Funktionen von Stack<> für int explizit instanziieren


template Bsp::Stack<int>::Stack();
Sandini Bib

7.6 Templates in der Praxis 469

template void Bsp::Stack<int>::push (const int&);


template int Bsp::Stack<int>::top () const;
template int Bsp::Stack<int>::pop ();

// benötigte Funktionen von Stack<> für std::string explizit instanziieren


// - top() wird nicht benötigt
template Bsp::Stack<std::string>::Stack();
template void Bsp::Stack<std::string>::push (const std::string&);
template std::string Bsp::Stack<std::string>::pop ();
Abbildung 7.2 zeigt die Organisation des Sourcecodes noch einmal im Überblick anhand eines
Funktionstemplates max().

m a x . h p p :

# i f n d e f M A X _ H P P
# d e f i n e M A X _ H P P

t e m p l a t e < t y p e n a m e T >
c o n s t T & m a x ( c o n s t T & , c o n s t T & ) ;

# e n d i f

m a x d e f . h p p :

# i f n d e f M A X D E F _ H P P
# d e f i n e M A X D E F _ H P P

# i n c l u d e " m a x . h p p "

t e m p l a t e < t y p e n a m e T >
c o n s t T & m a x ( c o n s t T & a , c o n s t T & b )
{
r e t u r n a > b ? a : b ;
}

# e n d i f

m a x t e s t . c p p :

# i n c l u d e " m a x . h p p "

i n t f o o ( i n t x , i n t y )
{
. . .
z = m a x ( x , y ) ;
. . .
}

m a x d e f . c p p :

# i n c l u d e " m a x d e f . h p p "

t e m p l a t e c o n s t i n t & m a x ( c o n s t i n t & , c o n s t i n t & ) ;

Abbildung 7.2: Code-Organisation von Templates am Beispiel


Sandini Bib

470 Kapitel 7: Templates

Durch die Vorgehensweise der expliziten Instantiierung kann mitunter signifikant Zeit gespart
werden. Es lohnt sich also, Template-Code auf zwei getrennte Headerdateien (eine für die De-
klaration und eine für die Definition) aufzuteilen. Will man nicht explizit instantiieren, muss man
im Anwendungsprogramm einfach nur statt der Headerdatei mit den Deklarationen die Header-
datei mit den Definitionen einbinden. Insofern schafft dieser Ansatz erheblich mehr Flexibilität,
ohne dass man signifikante Nachteile in Kauf nehmen muss.
Will man den Vorteil von Inline-Funktionen beibehalten, muss man diese wie bei anderen
Klassen dann auch in die Headerdatei mit den Deklarationen implementieren.

Template-Compilation-Modell
Eine andere Möglichkeit zur Behandlung von Templates wurde im Standard mit Hilfe des Schlüs-
selwortes export als so genanntes Template-Compilation-Modell definiert. Diese Möglichkeit
legt fest, dass ein Template durch export qualifiziert werden kann. Damit wird es automatisch
in eine Template-Datenbank bzw. in ein Template-Repository exportiert.
Wird nun ein derartiges Template verwendet, wird mit Hilfe dieses Repositorys geklärt, ob
das Template für die jeweiligen Datentypen schon übersetzt wurde, und, falls das nicht der Fall
ist, wird es automatisch übersetzt und zum laufenden Programm dazugebunden.
Wie dies genau aussieht, ist implementierungsspezifisch. Außerdem gibt es bisher noch so
gut wie keinen Compiler, der dieses Sprachmittel unterstützt. Insofern ist diese Möglichkeit in
der Praxis derzeit leider (noch) nicht anwendbar.

7.6.2 Fehlerbehandlung
Templates schaffen auch Probleme bei der Fehlerbehandlung. Das Hauptproblem liegt in der Tat-
sache, dass Compiler Typfehler erst beim Übersetzen des Templates für bestimmte Datentypen
erkennen. Dann melden sie aber wie üblich die Stelle, an der der Fehler erkannt wurde. Mit einer
derartigen Fehlermeldung kann man nur mit viel Glück etwas anfangen. In kommerziellem Code
sind die durch Templates vorgenommenen Ersetzungen und Ergänzungen viel zu kompliziert,
um nachvollziehbar zu sein.
Die folgenden Anweisungen aus der STL enthalten zum Beispiel den Fehler, dass im Such-
kriterium als Parameter für das Template greater<> fälschlicherweise int statt std::string
übergeben wurde:
std::list<std::string> menge;
...
// erstes Element größer als "A" finden
std::list<std::string>::iterator pos;
pos = find_if (menge.begin(),menge.end(), // Bereich
std::bind2nd(std::greater<int>(),"A")); // Suchkriterium
Dies kann z.B. folgende Fehlermeldung auslösen:
/local/include/stl/_algo.h: In function `struct _STL::_List_iterator<_STL::basic
_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_tra
its<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >
_STL::find_if<_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<cha
Sandini Bib

7.6 Templates in der Praxis 471

r>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::
char_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int
> > >(_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>,_STL:
:allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_tra
its<char>,_STL::allocator<char> > > >, _STL::_List_iterator<_STL::basic_string<c
har,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL:
:basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >, _STL::bi
nder2nd<_STL::greater<int> >, _STL::input_iterator_tag)':
/local/include/stl/_algo.h:115: instantiated from `_STL::find_if<_STL::_List_i
terator<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,
_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::all
ocator<char> > > >, _STL::binder2nd<_STL::greater<int> > >(_STL::_List_iterator<
_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_N
onconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<c
har> > > >, _STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>
,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::ch
ar_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int>
>)'
testprog.cpp:18: instantiated from here
/local/include/stl/_algo.h:78: no match for call to `(_STL::binder2nd<_STL::grea
ter<int> >) (_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<cha
r> > &)'
/local/include/stl/_function.h:261: candidates are: bool _STL::binder2nd<_STL::g
reater<int> >::operator ()(const int &) const
Es handelt sich in der Tat nur um eine Fehlermeldung, die besagt, dass es
 in einer Funktion in /local/include/stl/_algo.h
– instantiiert von Zeile 115 in /local/include/stl/_algo.h
– instantiiert von Zeile 18 in testprog.cpp
 in Zeile 78 keine passende Funktion zum Aufruf von etwas gibt,
 für das sich in Zeile 261 von /local/include/stl/_function.h ein Kandidat befindet,
der möglicherweise gemeint ist.
Wichtig ist zunächst einmal, dass Compiler mit dem Fehler auch die Stelle im Anwendungscode
melden, die zu diesem Fehler geführt hat. Bei der vorliegenden Fehlermeldung ist das z.B. in der
Zeile
testprog.cpp:18: instantiated from here
der Fall (wie man sieht, kann schon die Suche dieser Zeile ein Problem sein).3
Nun muss man nur noch das Problem verstehen. Mit viel Übung und Hintergrundwissen
mag man darauf kommen, dass irgendetwas mit basic_string gesucht wird, aber nur etwas
mit int vorhanden ist. Wen man nicht darauf kommt, bleibt immerhin die Möglichkeit, sich die

3
Leider gibt es immer noch C++-Compiler, die noch nicht einmal die Information liefern, welche eigene
Zeile das Problem verursacht hat. Beschweren Sie sich! Es gibt auch vorbildliche Compiler, die nur eine
Fehlermeldung zur betroffenen Zeile im eigenen Code ausgeben.
Sandini Bib

472 Kapitel 7: Templates

auslösende Stelle im eigenen Code noch einmal genau anzusehen und hoffentlich auf die Lösung
zu kommen, dass int in der Zeile einfach durch std::string ersetzt werden muss.
Die beispielhafte Fehlermeldung zeigt auch noch ein anderes Problem auf: Templates können
selbst zu Warnungen oder Fehlern führen. Dies liegt daran, dass durch die massiven Ersetzun-
gen sehr lange interne Symbolnamen erzeugt werden. Symbole mit bis zu 10.000 Zeichen sind
möglich. Nicht alle Compiler und Linker verkraften derart lange Symbole. Manche geben auch
einfach nur eine Warnung aus, die man dann getrost ignorieren kann.

7.6.3 Zusammenfassung
 Templates sprengen das herkömmliche Compiler/Linker-Modell.
 Templates können explizit instantiiert (realisiert) werden.
 Durch eine Aufteilung von Templates auf Headerdateien für die Deklaration und Headerda-
teien für die Definition kann man Templates in der Praxis flexibler handhaben.
 Für Templates existiert ein Compilation-Modell, das in der Praxis leider noch nicht verwendet
werden kann.
 Fehlermeldungen von Templates sind ein Problem. Wichtig ist es, die auslösende Stelle im
eigenen Code zu finden und dort das Problem zu erkennen.
 Templates können zu sehr langen Symbolen führen.
Sandini Bib

Kapitel 8
Die Standard-I/O-Bibliothek im
Detail

Nachdem bereits in den Abschnitten 3.1.4 und 4.5 die Standardtechniken zur Ein- und Ausgabe
mit Stream-Klassen eingeführt wurden, wird in diesem Abschnitt auf Details der Standard-I/O-
Bibliothek eingegangen. Damit bekommt man langsam einen Eindruck von dem vollen Umfang
der Möglichkeiten, die diese Bibliothek bietet.
Zu diesem Thema gehören auch die Möglichkeiten, auf Dateien und auf Strings formatierend
zuzugreifen.
Alle Möglichkeiten der I/O-Bibliothek vorzustellen, würde den Rahmen dieses Buches spren-
gen. Hier konzentriere ich mich auf die Details, die für die tägliche Arbeit bei der Ein- und
Ausgabe wichtig sind. Für weitergehende Betrachtungen sei auf spezielle Bücher zur Standard-
bibliothek oder zur I/O-Bibliothek verwiesen (z.B. mein Buch The C++ Standard Library oder
das Buch Standard C++ IOStreams and Locales von Langer und Kreft, siehe Literaturverzeich-
nis).

473
Sandini Bib

474 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

8.1 Die Standard-Stream-Klassen


In diesem Abschnitt werden die allgemeinen Klassen zur Ein- und Ausgabe, istream, ostream
und iostream im Detail vorgestellt.

8.1.1 Stream-Klassen und -Objekte


Klassen und Klassenhierarchie
Die Klassenhierarchie für die I/O-Klassen wird in Abbildung 8.1 dargestellt. Bei allen Klassen-
templates werden dabei in der oberen Zeile der Datentyp des Templates selbst und in der unteren
Zeile die Datentypen der Realisierungen für die Zeichentypen char und wchar_t dargestellt.

i o s _ b a s e b a s i c _ s t r e a m b u f < >
s t r e a m b u f / w s t r e a m b u f

b a s i c _ i o s < >
i o s / w i o s

{ v i r t u a l }

b a s i c _ i s t r e a m < > b a s i c _ o s t r e a m < >


i s t r e a m / w i s t r e a m o s t r e a m / w o s t r e a m

b a s i c _ i o s t r e a m < >
i o s t r e a m / w i o s t r e a m

Abbildung 8.1: Hierarchie der fundamentalen Stream-Klassen

Die Klassen dieser Hierarchie haben folgende Aufgaben:


 Die Basisklasse ios_base definiert die Eigenschaften aller Streamklassen, die unabhängig
vom Zeichentyp sind. Im Wesentlichen handelt es sich um Komponenten und Funktionen für
Status- und Formatflags.
 Das Klassentemplate basic_ios<> wird von ios_base abgeleitet und definiert die ge-
nerellen Eigenschaften aller Streamklassen, die vom Zeichentyp abhängig sind. Dazu gehört
auch die Definition der Puffer, die von den Streams verwendet werden. Ein solcher Puffer
ist ein Objekt einer von der Klasse basic_streambuf<> abgeleiteten Klasse. Sie dient
zum eigentlichen Einlesen und Ausgeben der einzelnen Zeichen. Die anderen Stream-Klassen
kümmern sich um die dazugehörige Formatierung der Daten (als z.B. um die Umwandlung
der Zahl 42 in die Zeichenfolge '4' '2').
Sandini Bib

8.1 Die Standard-Stream-Klassen 475

 Die Klassentemplates basic_istream<> und basic_ostream<> werden virtuell von


basic_ios<> abgeleitet und definieren Objekte, die zum Einlesen bzw. Ausgeben verwen-
det werden können. Die im angloamerikanischen Sprachraum meistens verwendeten Klassen
istream und ostream sind die dazugehörigen Instantiierungen für den Datentyp char.
 Das Klassentemplate basic_iostream<> wird sowohl von basic_istream<> als auch
von basic_ostream<> abgeleitet und definiert Objekte, die sowohl zum Einlesen als auch
zum Ausgeben verwendet werden können.
Für den Zugriff auf Dateien und auf Strings werden zusätzliche Klassen definiert, auf die in den
Abschnitten 8.2 und 8.3 eingegangen wird.
Wie bereits in Abschnitt 4.5.1 erläutert wurde, werden zu diesen Klassen die in Tabelle 8.1
aufgelisteten globalen Objekte definiert. Dabei sind auch die zu cin, cout usw. äquivalenten
Objekte für die Standardkanäle für Wide-Characters vom Typ wchar_t aufgelistet.

Symbol Datentyp Bedeutung


cin istream Standard-Eingabekanal (typischerweise die Tastatur)
cout ostream Standard-Ausgabekanal (typischerweise der Bildschirm), gepuffert
cerr ostream Standard-Fehlerausgabekanal (typischerweise der Bildschirm), un-
gepuffert
clog ostream Standard-Protokollausgabekanal (typischerweise der Bildschirm),
gepuffert
wcin wistream Standard-Eingabekanal (typischerweise die Tastatur)
wcout wostream Standard-Ausgabekanal (typischerweise der Bildschirm), gepuffert
wcerr wostream Standard-Fehlerausgabekanal (typischerweise der Bildschirm), un-
gepuffert
wclog wostream Standard-Protokollausgabekanal (typischerweise der Bildschirm),
gepuffert

Tabelle 8.1: Globale Objekte der Standard-Ein-/Ausgabe

Aufgabe der Streambuffer-Klassen


Die I/O-Stream-Bibliothek wurde so entworfen, dass sie Verantwortlichkeiten strikt trennt. Die
von basic_ios abgeleiteten Klassen kümmern sich nur“ um die Formatierung der Daten.1 Zum

eigentlichen Einlesen und Ausgeben der einzelnen Zeichen dienen die Streambuffer-Klassen, die
von den von basic_ios abgeleiteten Objekten verwendet werden.
Streambuffer-Klassen spielen deshalb eine wichtige Rolle, wenn man Ein-/Ausgaben für neue
Geräte definieren, Streams auf andere Geräte umleiten oder die Zeichenfolgen selbst auf be-
sondere Weise behandeln will (etwa automatisch alle Kleinbuchstaben in Großbuchstaben um-
wandeln will). Streambuffer-Klassen kümmern sich auch um die eigentliche Synchronisierung
gleichzeitiger Zugriffe mehrerer Streams auf die gleichen Geräte.

1
Die Formatierung machen sie aber genau genommen auch nicht selbst, sondern sie delegieren sie zum
Teil an spezielle Klassen, die die Besonderheiten der Internationalisierung berücksichtigen.
Sandini Bib

476 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Um Ein-/Ausgaben für spezielle Geräte zu ermöglichen, darf man also keine speziellen, von
basic_ios abgeleiteten Klassen implementieren, sondern muss eine spezielle Streambuffer-
Klasse implementieren und diese in den von basic_ios abgeleiteten Stream-Objekten verwen-
den.

Headerdateien
Die Definitionen der Stream-Klassen werden auf verschiedene Headerdateien aufgeteilt:
 <iosfwd>
enthält Vorwärtsdeklarationen der Streamklassen.
 <streambuf>
enthält die Definition der Basisklasse für Streambuffer, basic_streambuf<>.
 <istream>
Enthält die Definitionen der Klassen, die nur Eingaben unterstützen (basic_istream<>)
und für die Klassen, die sowohl Ein- als auch Ausgaben unterstützen (basic_iostream<>).
 <ostream>
Enthält die Definitionen der Klassen, die nur Ausgaben (basic_ostream<>) unterstützen.
 <iostream>
Enthält die Definitionen der globalen Stream-Objekte wie cin und cout.
Diese Aufteilung mag etwas merkwürdig erscheinen. Es ist in der Tat ausreichend, bei der Ver-
wendung von Streams für Ein- und Ausgaben nur die Headerdatei <istream> einzubinden. Nur
wenn wirklich die globalen Stream-Objekte benötigt werden, muss <iostream> eingebunden
werden. Dieses Design hat historische Gründe. Bei der Verwendung der globalen Stream-Objekte
in einem Modul ist spezieller Code notwendig, der sicherstellt, dass diese Objekte auch initiali-
siert sind. Dieser Code wird über <iostream> eingebunden. Da dieser Code etwas Zeit kostet,
hat man sich entschieden, dass man auf diese Headerdatei verzichten kann, wenn man zwar
iostreams, nicht aber die globalen Objekte verwendet.
Sofern man also nur die Datentypen der Streamklassen in Deklarationen benötigt, sollte man
einfach nur <iosfwd> einbinden. Wenn die Operationen zum Einlesen oder Ausgeben benötigt
werden, sollte man <istream> bzw. <ostream> einbinden. Und nur wenn man die globalen
Stream-Objekte verwendet, sollte man <iostream> einbinden.
Für spezielle Stream-Features wie parametrisierte Manipulatoren, Dateizugriff oder String-
Streams existieren weitere Headerdateien (<iomanip>, <fstream>, <sstream> und
<strstream>). Auf diese wird bei der Vorstellung der entsprechenden Techniken noch einge-
gangen.

8.1.2 Fehlerzustände, Stream-Status


Für den prinzipiellen Zustand eines Streams werden in der Klasse ios_base Bitkonstanten ein-
geführt. Obwohl bereits in Abschnitt 4.5.3 darauf eingegangen wurde, werden sie der Vollstän-
digkeit halber hier noch einmal kurz vorgestellt und zum Teil mit genaueren Informationen ver-
sehen.
Sandini Bib

8.1 Die Standard-Stream-Klassen 477

Zustandsbits
Die verschiedenen Bitkonstanten werden in Tabelle 8.2 aufgelistet. Sie werden über den Da-
tentyp iostate in der Klasse std::ios_base als Flags definiert und dort in einer internen
Komponente verwaltet.

Bitkonstante Bedeutung
goodbit alles in Ordnung, kein (anderes) Bit gesetzt
eofbit End-Of-File (Ende der Daten)
failbit Fehler: letzter Vorgang nicht korrekt abgeschlossen
badbit fataler Fehler: Zustand nicht definiert

Tabelle 8.2: Bitkonstanten vom Typ std::ios_base::iostate

Bei ios::goodbit von einem Flag zu reden ist etwas verwirrend, da es typischerweise den Wert
0 besitzt und damit eigentlich nur anzeigt, ob kein anderes Flag gesetzt ist.
failbit und badbit unterscheiden sich wie folgt:
 failbit wird gesetzt, wenn ein Vorgang nicht korrekt durchgeführt werden konnte, der
Stream aber prinzipiell in Ordnung ist. Typisch dafür sind Formatfehler beim Einlesen. Wenn
z.B. ein Integer eingelesen werden soll, das nächste Zeichen aber ein Buchstabe ist, wird
dieses Flag gesetzt.
 badbit wird gesetzt, wenn der Stream prinzipiell nicht mehr in Ordnung ist oder Zeichen
verlorengegangen sind. Dieses Flag wird z.B. beim Positionieren vor einen Dateianfang ge-
setzt.
Man beachte, dass eofbit normalerweise zusammen mit failbit gesetzt wird, da die Be-
dingung EOF (Ende der Daten) erst dann erkannt wird, wenn der Versuch, weiter einzulesen,
fehlschlägt.
Zusätzlich existiert auf manchen Systemen das Flag ios::hardfail. Dies ist allerdings
nicht standardisiert.
Da die Flags in der Klasse ios_base definiert werden, müssen sie entsprechend qualifiziert
werden:
std::ios_base::eofbit
Man kann sie aber auch über ios qualifizieren:
std::ios::eofbit
Dies ist möglich, da ios eine Realisierung der von ios_base abgeleiteten Klasse basic_ios<>
ist. Da es früher nur die Klasse ios gab, ist diese Art der Qualifizierung rückwärtskompatibel
und wird relativ häufig angetroffen.

Zugriff auf Zustandsbits


Zu den Flags gehören Elementfunktionen, die zum Teil ebenfalls bereits in Abschnitt 4.5.3 ein-
geführt wurden (siehe Tabelle 8.3).
Sandini Bib

478 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Funktion Bedeutung
good() true, wenn alles in Ordnung (ios::goodbit gesetzt) ist
eof() true bei End-Of-File (ios::eofbit gesetzt)
fail() true bei Fehler (ios::failbit oder ios::badbit gesetzt)
bad() true bei fatalem Fehler (ios::badbit gesetzt)
rdstate() liefert die Kombination der gesetzten Flags zurück
clear() löscht alle Flags
clear(bits) löscht alle Flags und setzt bits als Zustand
setstate(bits) setzt zusätzlich bits als Zustand

Tabelle 8.3: Elementfunktionen für Stream-Zustände

Die ersten vier Elementfunktionen liefern als Booleschen Wert, ob bestimmte Flags gesetzt sind.
Man beachte, dass fail() liefert, ob failbit oder badbit gesetzt ist. Dies ist aus historischen
Gründen so und hat den Vorteil, dass ein Aufruf zum Test auf einen Fehlerfall reicht.
Wird clear() kein Parameter übergeben, werden alle Fehlerflags (auch ios::eofbit) ge-
löscht. Wird ein Parameter übergeben, werden die darin übergebenen Flags gesetzt. Auf Seite
211 befindet sich ein kleines Anwendungsbeispiel dazu.
Man beachte, dass clear() bei jeder fehlgeschlagenen Stream-Operation aufgerufen werden
muss. Dies ist anders als bei der I/O-Schnittstelle von C. Dort konnte man nach dem aufgrund
eines Formatfehlers fehlgeschlagenen Versuch, einen int einzulesen, sofort den Rest der Zeile
auslesen. In den Stream-Klassen wird in dem Fall das failbit gesetzt, das dafür sorgt, dass
jeder weitere Leseversuch so lange fehlschlägt, bis es explizit wieder gelöscht wird.
Schließlich werden für die Bewertung von Booleschen Ausdrücken in Kontrollstrukturen die
bereits in Abschnitt 4.5.2 vorgestellten Konvertierungsfunktionen definiert (siehe Tabelle 8.4).

Elementfunktion Bedeutung
operator void* () liefert zurück, ob der Stream in Ordnung ist
(entspricht dem Aufruf von !fail())
operator ! () liefert zurück, ob der Stream nicht in Ordnung ist
(entspricht dem Aufruf von fail())

Tabelle 8.4: Konvertierungsoperationen für Streams

Fehlerzustände und Ausnahmen


Im Rahmen der Standardisierung wurde die Möglichkeit eingeführt, dass die Zustandsbits der
Streams automatisch Ausnahmen auslösen können. Dies bedeutet, dass man einstellen kann,
ob ein Bit, wenn es gesetzt wird, automatisch eine Ausnahme auslöst. Welche Bits Ausnahmen
auslösen können wird mit der Funktion exceptions() gesetzt und abgefragt (siehe Tabelle 8.5).
Mit dem folgenden Aufruf wird bei allen Fehlern“ für den Stream strm festgelegt, dass

automatisch Stream-Ausnahmen ausgelöst werden:
Sandini Bib

8.1 Die Standard-Stream-Klassen 479

Elementfunktion Bedeutung
exceptions() liefert die Bits, die automatisch Ausnahmen auslösen
exceptions(bits) setzt die Bits, die automatisch Ausnahmen auslösen

Tabelle 8.5: Elementfunktionen zum Auslösen von Ausnahmen

// Ausnahmen für alle ‘‘Fehler’’ auslösen


strm.exceptions (std::ios::eofbit | std::ios::failbit |
std::ios::badbit);
Sind in diesem Moment bereits diese Bits gesetzt, werden auch sofort entsprechende Ausnahmen
ausgelöst.
Durch die Übergabe von 0 oder goodbit kann das automatische Auslösen von Stream-
Ausnahmen wieder abgeschaltet werden:
// keine Ausnahmen auslösen
strm.exceptions(0);
Als Ausnahmen werden Objekte vom Typ std::ios_base::failure ausgelöst. Für diese Ob-
jekte ist nur definiert, dass mit what() ein implementierungsspezifischer String abgefragt wer-
den kann (siehe Abschnitt 3.6.3).
Man beachte, dass hier von Ausnahmen“ und nicht von Fehlern“ die Rede ist. Gerade im
” ”
Umfeld von Ein-/Ausgaben ist dies nicht das Gleiche. Fehlerhafte Eingaben sind eher normal
und sollten lokal im Rahmen des normalen Datenflusses behandelt werden. Ausnahmen sollte
man bei Streams eigentlich nur für das badbit auslösen lassen oder wenn man Daten aus einer
vordefinierten Konfigurationsdatei liest, bei der es keine Lesefehler geben sollte.

8.1.3 Standardoperatoren
Als Standardoperatoren für die Ein- und Ausgabe werden in den Klassen istream und ostream
die Operatoren >> und << definiert und für alle fundamentalen Datentypen einschließlich des
Typs char* überladen.
Der Operator << bietet auch die Möglichkeit, Zeiger auszugeben. Wird dem Operator als
Parameter der Typ void* übergeben, wird jeweils die Adresse des Zeigers in einer maschinen-
abhängigen Form ausgegeben. Die folgende Anweisung gibt z.B. den Inhalt eines Strings und
dessen Adresse aus:
const char* cstring = "hallo";

std::cout << "String: " << cstring << " steht an Adresse: "
<< static_cast<const void*>(cstring) << std::endl;
Dies kann z.B. zu folgender Ausgabe führen:
String: hallo steht an Adresse: 0x10000018
Man kann diese Adresse auch entsprechend mit dem Input-Operator wieder einlesen. Man be-
achte dabei aber, dass Adressen flüchtige Werte in einem Programm sind. Das gleiche Objekt
kann in einem neu gestarteten Programm eine völlig andere Adresse besitzen.
Sandini Bib

480 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

8.1.4 Standardfunktionen
Statt der Standardoperatoren für Streams, << und >>, können auch verschiedene Elementfunk-
tionen zum Einlesen oder Ausgeben verwendet werden. Diese Elementfunktionen werden im
Folgenden vorgestellt. Dabei wird die Semantik für die Realisierung der Stream-Klassen für
chars, also istream und ostream, erläutert. Besonderheiten, die aus der Tatsache resultieren,
dass es sich um Funktionen für basic_istream<> und basic_ostream<> handelt, werden
nicht berücksichtigt.

Elementfunktionen zur Eingabe


In der Klasse istream werden neben dem Operator >> zahlreiche weitere Elementfunktionen
definiert, die zum Einlesen von Zeichen dienen:
 get() ohne Parameter
int get();
liefert als Rückgabewert das nächste gelesene Zeichen oder EOF zurück. Es entspricht damit
getchar() oder getc() von C.
Man beachte, dass der Rückgabewert einem int zugewiesen wird. Ansonsten kann das Zei-
chen mit dem Wert -1 bzw. mit dem Wert 255 nicht von EOF unterschieden werden, da EOF
typischerweise mit dem Wert -1 definiert ist.
 get() mit char als Parameter
istream& get (char& c);
weist das nächste Zeichen dem übergebenen Parameter c zu und liefert den Stream zurück.
Sein Zustand gibt dann darüber Auskunft, ob das Lesen erfolgreich war.
 get() mit char*, int und optional char als Parameter
istream& get (char* zf, int anz, char ende = '\n');
liest bis zu anz-1 Zeichen in die Zeichenfolge zf ein, wobei spätestens mit dem Zeichen
ende abgebrochen wird. Das Stringende-Zeichen '\0' wird automatisch dahinter gesetzt.
Das Zeichen ende wird nicht eingelesen. Bei zu langen Zeilen liefert der nächste Aufruf von
get() die nachfolgenden Zeichen.
 getline() mit char*, int und optional char als Parameter
istream& getline (char* zf, int anz, char ende = '\n');
liest bis zu anz-1 Zeichen in die Zeichenfolge zf ein, wobei spätestens mit dem Zeichen
ende abgebrochen wird. Das Stringende-Zeichen '\0' wird automatisch dahinter gesetzt.
Das Zeichen ende wird dabei mit eingelesen und eingetragen. Bei zu langen Zeilen wird
failbit gesetzt.
 read() mit char* und int als Parameter
Sandini Bib

8.1 Die Standard-Stream-Klassen 481

istream& read (char* zf, int anz);


liest anz Zeichen in die Zeichenfolge zf ein. Das Stringende-Zeichen '\0' wird nicht dahinter
gesetzt. Bei zu wenigen Zeichen werden eofbit und failbit gesetzt.
 readsome() mit char* und int als Parameter
int readsome (char* zf, int anz);
liest bis zu anz Zeichen in die Zeichenfolge zf ein. Liefert die Anzahl gelesener Zeichen
zurück. Das Stringende-Zeichen '\0' wird nicht dahinter gesetzt.
 gcount() ohne Parameter
int gcount ();
liefert zurück, wieviele Zeichen beim letzten Lesebefehl gelesen wurden.
 ignore() mit zwei optionalen int als Parameter
istream& ignore (int anz = 1, int ende = EOF);
überliest maximal anz Zeichen, bis ende (Zeichen oder EOF) auftritt. Um alle Zeichen bis
ende auszulesen, muss als anz der Ausdruck numeric_limits<int>::max() (siehe Ab-
schnitt 9.1.4) übergeben werden.
 peek() ohne Parameter
int peek ();
liefert das nächste Zeichen aus dem Stream oder EOF, ohne es auszulesen. Beim nächsten
Lesen ist das Zeichen also immer noch vorhanden.
 unget() ohne Parameter
istream& unget ();
stellt das zuletzt gelesene Zeichen in den Eingabestrom zurück, damit es beim nächsten Mal
(erneut) gelesen wird.
 putback() mit char als Parameter
istream& putback (char c);
stellt das zuletzt gelesene Zeichen c in den Eingabestrom zurück, damit es beim nächsten
Mal (erneut) gelesen wird. c muss wirklich das zuletzt gelesene Zeichen sein; ansonsten wird
badbit gesetzt.
Bei all diesen Funktionen werden Trennzeichen (Whitespaces) nicht überlesen.
Da immer eine Obergrenze angegeben werden muss, sind diese Funktionen zum Einlesen von
Strings sicherer als die Verwendung des Operators >>. Es ist zwar möglich, vor dem Einlesen mit
dem Operator >> eine maximale Anzahl von Zeichen zu definieren (siehe Seite 489), dies kann
aber leicht vergessen werden.
Die Implementierungen der Funktionen zum Einlesen eines Bruchs in Abschnitt 4.5.4 und
eines Strings in Abschnitt 6.1.7 enthalten Anwendungsbeispiele für verschiedene Einlesefunk-
tionen.
Sandini Bib

482 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Elementfunktionen zur Ausgabe


In der Klasse ostream werden neben dem Operator << ebenfalls einige Elementfunktionen de-
finiert, die zur Ausgabe von Zeichen dienen:
 put() mit char als Parameter
ostream& put (char c);
gibt den übergebenen Parameter c als nächstes Zeichen aus und liefert den Stream zurück.
Sein Zustand gibt dann darüber Auskunft, ob das Schreiben erfolgreich war.
 write() mit char* und int als Parameter
ostream& write (char* zf, int anz);
gibt anz Zeichen der Zeichenfolge zf aus.
 flush() ohne Parameter
ostream& flush ();
leert den Ausgabepuffer, indem alle darin stehenden Zeichen ausgegeben werden.

Verknüpfen von Input- und Output-Stream


Mit einer speziellen Elementfunktion können ein Input- und ein Output-Stream miteinander ver-
knüpft werden:
 tie() mit ostream* als Parameter
ostream* tie (ostream*);
verknüpft einen Stream mit einem übergebenen Output-Stream.
Dies bedeutet, dass immer dann, wenn für den Stream eine Eingabeoperation aufgerufen wird,
zunächst der Ausgabepuffer des daran gebundenen Output-Streams geleert wird.
Für die Standard-Ein- und -Ausgabe ist dies bereits vordefiniert:
// Eingabepuffer an Ausgabepuffer koppeln
std::cin.tie (&std::cout);
Dadurch wird sichergestellt, dass eine Aufforderung zur Eingabe auch ausgegeben worden ist,
bevor auf die Eingabe gewartet wird:
std::cout << "Bitte x eingeben: ";
std::cin >> x;
Vor dem Einlesen von x wird für cout ein implizites flush() aufgerufen.
Die Tatsache, dass der Ausgabepuffer vom Standard-Ausgabekanal im Allgemeinen vor dem
Einlesen vom Standard-Eingabekanal geleert wird, ist keineswegs garantiert, sondern hängt letzt-
lich von der Betriebssystemumgebung ab.2

2
Eigentlich muss aus dem gleichen Grund hinter jeder Ausgabe in C-Programmen – zumindest wenn sie
nicht mit einem Newline endet – ein fflush() aufgerufen werden. Aber wer macht das schon?
Sandini Bib

8.1 Die Standard-Stream-Klassen 483

Man beachte, dass als Parameter die Adresse eines Output-Streams übergeben werden muss.
Dadurch kann auch 0 bzw. NULL übergeben werden, was eine bestehende Bindung aufhebt:
// Kopplung von cin an alle Output-Streams aufheben
std::cin.tie (static_cast<std::ostream*>(0));
Zurückgeliefert wird jeweils die alte Bindung (Zeiger auf Output-Stream oder 0). Ein Aufruf
ohne Parameter liefert ebenfalls die alte Bindung.

Anwendungsbeispiele
Das klassische Filtergerüst, das einfach nur alle gelesenen Zeichen wieder ausgibt, sieht in C++
wie folgt aus:
// io/charcat1.cpp
#include <iostream>

int main()
{
char c;

// solange ein Zeichen c gelesen werden kann


while (std::cin.get(c)) {
// Zeichen ausgeben
std::cout.put(c);
}
}
Man beachte, dass im Gegensatz zur C-Version das jeweils eingelesene Zeichen nicht auf EOF
getestet und somit c auch nicht als int deklariert werden muss, da in der Bedingung nicht das
gelesene Zeichen, sondern der Stream auf seinen korrekten Zustand getestet wird.
Als kleine Verbesserung kann zum Schluss noch getestet werden, ob das Programm wirklich
durch EOF beendet wurde und ob der Output-Stream in einem fehlerfreien Zustand ist:
// io/charcat2.cpp
#include <iostream>
#include <cstdlib>

int main()
{
char c;

// solange ein Zeichen c gelesen werden kann


while (std::cin.get(c)) {
// Zeichen ausgeben
std::cout.put(c);
}
Sandini Bib

484 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

if (! std::cin.eof()) {
std::cerr << "Lesefehler" << std::endl;
return EXIT_FAILURE;
}
if (! std::cout.good()) {
std::cerr << "Schreibfehler" << std::endl;
return EXIT_FAILURE;
}
}

8.1.5 Manipulatoren
Wie bereits in Abschnitt 4.5.2 erwähnt wurde, werden für Streams verschiedene Manipulatoren
definiert. Dabei handelt es sich um globale Objekte, die, wenn sie mit dem Standard-Ein- bzw.
Standard-Ausgabeoperator aufgerufen werden, den Stream in irgendeiner Form manipulieren,
was nicht unbedingt heißt, dass etwas ein- oder ausgelesen wird. Tabelle 8.6 listet alle grundle-
genden Manipulatoren aus <istream> und <ostream> auf.

Manipulator Klasse Bedeutung


flush ostream Ausgabepuffer leeren
endl ostream '\n' ausgeben und Ausgabepuffer leeren
ends ostream '\0' ausgeben und Ausgabepuffer leeren
ws istream Trennzeichen (Whitespaces) überlesen

Tabelle 8.6: Fundamentale Manipulatoren

Hinzu kommen weitere Manipulatoren, die z.B. zur Umschaltung von Ein- und Ausgabeformaten
dienen. Sie werden in den nachfolgenden Abschnitten vorgestellt.

Arbeitsweise von Manipulatoren


Hinter den Manipulatoren steckt ein eigentlich ganz einfacher Trick, der gleichzeitig aufzeigt,
wie mächtig die Möglichkeit zum Überladen von Funktionen ist: Manipulatoren sind nichts an-
deres als Funktionen, die aufgerufen werden, sofern sie einem Ein- bzw. Ausgabeoperator als
Parameter übergeben werden.
Für die Klasse ostream wird der Ausgabeoperator z.B. im Prinzip wie folgt überladen (hier
handelt es sich um Pseudo-Code für eine angenommene Definition der Klasse ostream, der
tatsächliche Code der Klasse basic_ostream<> ist entsprechend der Template-Parameter para-
metrisiert):
class ostream : ... {
...
ostream& operator << (ostream& (*func)(ostream&)) {
return (* func)(*this);
}
...
};
Sandini Bib

8.1 Die Standard-Stream-Klassen 485

Wenn dem Operator << als zweiter Operand eine Funktion func übergeben wird, die als Parame-
ter ein ostream-Objekt erhält und ein ostream-Objekt zurückliefert, wird diese Funktion mit
dem ostream-Objekt aufgerufen, für das der Operator << aufgerufen wird.
Das klingt sehr kompliziert, ist aber eigentlich relativ einfach: Die Funktion (und damit der
Manipulator) endl() ist im Prinzip folgendermaßen definiert (auch hier sind die Besonderheiten
für die Tatsache, dass statt ostream eigentlich die Klasse basic_ostream<> verwendet wird,
weggelassen):
namespace std {
ostream& endl (ostream& strm)
{
// Newline ausgeben
strm << '\n';

// Ausgabepuffer leeren
strm.flush();

// strm für Verkettung zurückliefern


return strm;
}
}
Durch die Anweisung
std::cout << std::endl
wird der Operator << mit dem Parameter std::endl aufgerufen:
std::cout.operator<<(std::endl)
Da std::endl eine Funktion ist, wird dieser Aufruf so umgesetzt, dass diese Funktion mit dem
Stream als Parameter aufgerufen wird:
std::endl(std::cout);
Und dieser Aufruf gibt schließlich einen Zeilentrenner aus und leert den Ausgabepuffer.

Selbst definierte Manipulatoren


Es ist jederzeit möglich, eigene Manipulatoren zu definieren. Dazu muss nur eine entsprechende
Funktion in der Art von endl() geschrieben werden.
Mit der folgenden Funktion wird z.B. ein Manipulator zum Auslesen einer Zeile bis zum
Zeilenende definiert:
// io/ignore.hpp
#include <iostream>
#include <limits>
Sandini Bib

486 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

inline
std::istream& ignoreLine (std::istream& strm)
{
char c;

// alle Zeichen bis zum Zeilenende ignorieren


strm.ignore(std::numeric_limits<int>::max(),'\n');

// strm für Verkettung zurückliefern


return strm;
}
Die Funktion ignore() überliest dabei alle Zeichen bis zum Zeilenende (siehe Seite 481).
Die Anwendung des Manipulators ist denkbar einfach:
// Rest der Zeile ignorieren
std::cin >> ignoreLine;
Durch mehrfaches Aufrufen könnten damit auch mehrere Zeilen ignoriert werden:
// zwei Zeilen ignorieren
std::cin >> ignoreLine >> ignoreLine;

8.1.6 Formatdefinitionen
Für die Definition von Ein- und Ausgabeformaten werden in der Klasse ios_base verschiede-
ne Komponenten definiert, die z.B. eine minimale Feldbreite oder die Anzahl der Stellen von
Gleitkommazahlen definieren. In einer dieser Komponenten wird eine Reihe von Formatflags als
Bitkonstanten verwaltet. Mit diesen Formatflags kann z.B. die Ausgabe eines positiven Vorzei-
chens veranlasst werden.
Manche dieser Formatflags gehören zusammen, wie beispielsweise die Flags, die die ok-
tale, dezimale oder hexadezimale Ausgabe einschalten. Um die Verwaltung solcher zusammen-
gehöriger Flags zu vereinfachen, werden jeweils Bitfelder definiert, mit denen nur diese Flags
angesprochen werden können.

8.1.7 Setzen und Abfragen von Formatflags


Für den Zugriff auf die Formatflags gibt es die in Tabelle 8.7 aufgelisteten Funktionen.
Die Funktionen setf() und unsetf() setzen bzw. löschen Flags. Die Flags müssen dabei
mit dem Operator | verknüpft werden. setf() kann dabei eine Maske mitgegeben werden, die
dazu führt, dass in der dazugehörigen Gruppe vorher alle Flags gelöscht werden. Beispiele:
// Flags showpos und uppercase setzen
std::cout.setf (std::ios::showpos | std::ios::uppercase);

// Nur das Flag hex in der Gruppe basefield setzen


std::cout.setf (std::ios::hex, std::ios::basefield);
Sandini Bib

8.1 Die Standard-Stream-Klassen 487

Elementfunktion Bedeutung
setf(flags) Setzt flags als zusätzliche Formatflags und liefert den vor-
herigen Zustand aller Formatflags zurück
setf(flags, maske) Setzt flags als die neuen Formatflags der durch maske
identifizierten Gruppe und liefert den vorherigen Zustand
aller Formatflags zurück
unsetf(flags) löscht flags
flags() liefert alle gesetzten Formatflags
flags(flags) Setzt flags als neue Formatflags und liefert den vorheri-
gen Zustand aller Formatflags zurück
copyfmt(stream) kopiert alle Formatdefinitionen aus stream

Tabelle 8.7: Elementfunktionen für den Zugriff auf Formatflags

// Flag uppercase löschen


std::cout.unsetf (std::ios::uppercase);
Mit flags() kann man alle gesetzten Flags abfragen und einen neuen Zustand von gesetzten
Flags setzen. So kann man sich z.B. einen Zustand gesetzter Flags merken und diesen wieder
restaurieren:
using std::ios;
using std::cout;

// aktuelle Formatflags merken


ios::fmtflags oldFlags = cout.flags();

// dies und das ändern


cout.setf(ios::showpos | ios::showbase | ios::uppercase);
cout.setf(ios::internal, ios::adjustfield);
...

// alten Flags-Zustand wieder restaurieren


cout.flags(oldFlags);
Man kann die Flags auch mit den in Tabelle 8.8 aufgelisteten Manipulatoren ändern.

Manipulator Effekt
setiosflags(flags) setzt flags als Formatflags
(ruft für den Stream setf(flags) auf)
resetiosflags(maske) löscht alle Formatflags der durch maske identifizierten Gruppe
(ruft für den Stream setf(0,maske) auf)

Tabelle 8.8: Manipulatoren für Formatflags


Sandini Bib

488 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Für diese Manipulatoren muss, wie bei allen Manipulatoren mit Parametern, die Headerdatei
<iomanip> eingebunden werden. Beispiel:
#include <iostream>
#include <iomanip>
...
std::cout << resetiosflags(std::ios::adjustfield) // Ausrichtungsflags löschen
<< setiosflags(std::ios::left); // linksbündig setzen
Für einige Flag-Manipulationen existieren außerdem spezielle Manipulatoren, die nachfolgend
vorgestellt werden. Sie sind in der Handhabung in der Regel deutlich einfacher und lesbarer und
sollten deshalb nach Möglichkeit verwendet werden.

Feldbreite, Ausrichtung und Füllsymbol


Zur Definition von Feldbreite und Füllsymbol existieren die Elementfunktionen width() und
fill() (siehe Tabelle 8.9).

Elementfunktion Bedeutung
width() liefert die aktuelle Feldbreite zum Ein- und Auslesen
width(wert) setzt die aktuelle Feldbreite auf wert und liefert die bisherige Feldbreite
fill() liefert das aktuelle Füllsymbol
fill(c) definiert c als aktuelles Füllsymbol und liefert das bisherige Füllsymbol

Tabelle 8.9: Elementfunktionen für Feldbreite und Füllsymbol

Mit der Elementfunktion width() kann für eine Ausgabe eine minimale Feldbreite definiert
werden. Eine solche Definition bezieht sich allerdings nur auf die direkt folgende Ausgabe, so-
fern es sich dabei nicht um die Ausgabe eines char handelt. Ohne Parameter wird die aktuelle
minimale Feldbreite zurückgeliefert; mit Parameter wird eine neue minimale Feldbreite gesetzt
und die bisherige zurückgeliefert. Der Default-Wert ist 0.
Mit der Elementfunktion fill() kann dazu ein Füllsymbol definiert und abgefragt werden.
Das Default-Füllsymbol ist das Leerzeichen.
Zur Ausrichtung innerhalb des Feldes existieren drei Flags. Sie werden in der Klasse ios mit
einem dazugehörigen Bitfeld, wie in Tabelle 8.10 aufgelistet, definiert.

Bitfeld Flags Bedeutung


adjustfield left linksbündig
right rechtsbündig (Default)
internal Vorzeichen linksbündig, Wert rechtsbündig

Tabelle 8.10: Flags zur Steuerung der Ausrichtung

Im Gegensatz zur Feldbreite gelten Füllsymbol und Ausrichtung bis auf weiteres, also bis ein
neuer Befehl etwas anderes definiert.
Tabelle 8.11 zeigt die Auswirkungen der Funktionen und Flags bei der Ausgabe verschiede-
ner Konstanten. Als Füllsymbol wird jeweils der Unterstrich verwendet.
Sandini Bib

8.1 Die Standard-Stream-Klassen 489

Ausrichtung width() -42 0.12 "Q" ’Q’


left 6 -42___ 0.12__ Q_____ Q_____
right 6 ___-42 __0.12 _____Q _____Q
internal 6 -___42 __0.12 _____Q _____Q

Tabelle 8.11: Beispiele für die Auswertung der Ausrichtung

Für width() und fill() werden zusätzlich die in Tabelle 8.12 dargestellten Manipulatoren
definiert. Da es sich um Manipulatoren mit Parametern handelt, muss dazu die Headerdatei
<iomanip> eingebunden werden.

Manipulator Bedeutung
setw() setzt die Feldbreite zum Ein- und Auslesen
(entspricht width())
setfill() definiert ein Füllsymbol (entspricht fill())
left erzwingt die linksbündige Ausgabe
right erzwingt die rechtsbündige Ausgabe
internal erzwingt Vorzeichen linksbündig, Wert rechtsbündig

Tabelle 8.12: Manipulatoren für die Ausrichtung

Die Anweisungen
#include <iostream>
#include <iomanip>
...
std::cout << std::setfill('_');
std::cout << std::left << std::setw(8) << "1."
<< std::right << std::setw(8) << 3.14 << std::endl;
std::cout << std::left << std::setw(8) << "2."
<< std::right << std::setw(8) << 7.33 << std::endl;
std::cout << std::left << std::setw(8) << "Summe:"
<< std::right << std::setw(8) << 10.47 << std::endl;
geben z.B.
1.__________3.14
2.__________7.33
Summe:_____10.47
aus.
Mit width() kann auch eine maximale Feldbreite zum Einlesen eines Strings (Datentyp
char*) mit dem Operator >> definiert werden. Ist der Wert ungleich 0, werden höchstens
width()-1 Zeichen gelesen. width() sollte sicherheitshalber immer verwendet werden, wenn
C-Strings mit dem Operator >> eingelesen werden, da sonst der hinter dem C-String liegende
Speicherplatz bei zu vielen Zeichen einfach überschrieben wird:
Sandini Bib

490 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

char puffer[81];

// maximal 80 Zeichen als C-String einlesen


std::cin >> std::setw(sizeof(puffer)) >> puffer;
Besser und einfacher ist natürlich die Verwendung eines strings. Strings kann man im Gegen-
satz zu C-Strings bedenkenlos einlesen, da sie intern automatisch entsprechend Speicherplatz
reservieren:
std::string s;

std::cin >> s; // OK

Positives Vorzeichen und Großbuchstaben


Zwei Formatflags bieten die Möglichkeit, die Ausgabe von Zahlen allgemein zu beeinflussen:
showpos und uppercase (siehe Tabelle 8.13).

Flag Bedeutung
showpos positives Vorzeichen mit ausgeben
uppercase Großbuchstaben verwenden

Tabelle 8.13: Flags für Vorzeichen und Buchstaben von numerischen Werten

Mit dem Flag showpos kann die Ausgabe eines positiven Vorzeichens erzwungen werden. Das
Flag uppercase bietet die Möglichkeit zu definieren, dass bei der Ausgabe von Zahlen Groß-
buchstaben statt Kleinbuchstaben verwendet werden. Dies betrifft ganze Zahlen, die hexadezi-
mal ausgegeben werden, und Gleitkommazahlen, die in wissenschaftlicher Notation ausgegeben
werden.
Die Anweisungen
std::cout << 12345678.9 << std::endl;

std::cout.setf (std::ios::showpos | std::ios::uppercase);


std::cout << 12345678.9 << std::endl;
liefern z.B. folgende Ausgabe:
1.23457e+07
+1.23457E+07
Beide Flags können mit den in Tabelle 8.14. aufgelisteten Manipulatoren gesetzt oder gelöscht
werden.

Zahlensystem
Drei zusammengehörige Flags legen fest, in welchem Zahlensystem ganze Zahlen eingelesen
und ausgegeben werden. Die Flags werden in der Klasse ios mit dem dazugehörigen Bitfeld,
wie in Tabelle 8.15 aufgelistet, definiert.
Sandini Bib

8.1 Die Standard-Stream-Klassen 491

Manipulator Bedeutung
showpos erzwingt die Ausgabe eines positiven Vorzeichens
(setzt das Flag ios::showpos)
noshowpos verhindert die Ausgabe eines positiven Vorzeichens
(löscht das Flag ios::showpos)
uppercase erzwingt die Verwendung von Großbuchstaben
(setzt das Flag ios::uppercase)
nouppercase erzwingt die Verwendung von Kleinbuchstaben
(löscht das Flag ios::uppercase)

Tabelle 8.14: Manipulatoren für Vorzeichen und Buchstaben von numerischen Werten

Bitfeld Flags Bedeutung


basefield oct Ein-/Ausgabe oktal
dec Ein-/Ausgabe dezimal (Default)
hex Ein-/Ausgabe hexadezimal
ohne Ausgabe dezimal und
Eingabe gemäß der führenden Zeichen ganzzahliger Werte

Tabelle 8.15: Flags für das Zahlensystem ganzzahliger Werte

Die Umschaltung gilt jeweils für alle folgenden ganzen Zahlen, bis das Format erneut umge-
schaltet wird. Dezimal ist der Default-Wert.
Falls kein Flag für das Zahlensystem gesetzt ist (dies ist nicht der Default-Fall), hängt das
Zahlensystem eingelesener Werte von den führenden Zeichen dieser Werte ab. Beginnt ein Wert
mit 0x oder 0X, wird die Zahl hexadezimal eingelesen. Beginnt sie nur mit 0, wird sie oktal
eingelesen, ansonsten wird sie dezimal eingelesen.
Wenn die Flags direkt gesetzt werden, muss darauf geachtet werden, dass immer nur ein Flag
gesetzt ist. Zum direkten Umschalten gibt es zwei Möglichkeiten:
1. Ein Flag setzen und das andere zurücksetzen:
std::cout.unsetf (std::ios::dec);
std::cout.setf (std::ios::hex);

2. Ein Flag setzen und dabei automatisch alle anderen Flags des Bitfelds zurücksetzen:
std::cout.setf (std::ios::hex, std::ios::basefield);

Zusätzlich können die in Tabelle 8.16 aufgelisteten Manipulatoren verwendet werden.


Die folgenden Anweisungen geben z.B. x und y hexadezimal und z dezimal aus:
int x, y, z;
...
std::cout << std::hex << x << std::endl;
std::cout << y << " " << std::dec << z << std::endl;
Sandini Bib

492 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Manipulator Bedeutung
oct Ein-/Ausgabe oktal
dec Ein-/Ausgabe dezimal
hex Ein-/Ausgabe hexadezimal

Tabelle 8.16: Manipulatoren für das Zahlensystem ganzzahliger Werte

Ein zusätzliches Flag bietet die Möglichkeit, bei der Ausgabe die im Quellcode übliche Notation
zum Kennzeichnen des Zahlensystems zu verwenden (siehe Tabelle 8.17).

Flag Bedeutung
showbase Zahlensystem kennzeichnen

Tabelle 8.17: Flag zum Kennzeichnen des Zahlensystems für ganzzahlige Werte

Wenn das Flag showbase gesetzt ist, werden oktale Zahlen mit einer führenden 0 und hexadezi-
male Zahlen mit führendem 0x (bzw. wenn ios::uppercase gesetzt ist, 0X) ausgegeben.
Die Anweisungen
std::cout << 127 << ' ' << 255 << std::endl;

std::cout << std::hex << 127 << ' ' << 255 << std::endl;

std::cout.setf(std::ios::showbase);
std::cout << 127 << ' ' << 255 << std::endl;

std::cout.setf(std::ios::uppercase);
std::cout << 127 << ' ' << 255 << std::endl;
liefern z.B. folgende Ausgabe:
127 255
7f ff
0x7f 0xff
0X7F 0XFF
showbase kann außerdem mit den in Tabelle 8.18 aufgelisteten Manipulatoren gesetzt oder
gelöscht werden.

Manipulator Bedeutung
showbase Zahlensystem kennzeichnen (setzt das Flag ios::showbase)
noshowbase Zahlensystem nicht kennzeichnen (löscht das Flag ios::showbase)

Tabelle 8.18: Manipulatoren zum Kennzeichnen des Zahlensystems für ganzzahlige Werte
Sandini Bib

8.1 Die Standard-Stream-Klassen 493

Gleitkomma-Notation
Für die Ausgabe von Gleitkommawerten existieren mehrere Formatflags und Komponenten. Die
in Tabelle 8.19 aufgelisteten Flags definieren zunächst grundsätzlich, ob die Ausgabe in Dezimal-
oder Exponentialdarstellung erfolgt. Sie werden in der Klasse ios mit dem dazugehörigen Bit-
feld definiert. Falls ios::fixed gesetzt ist, werden Gleitkommawerte mit der dezimalen Nota-
tion ausgegeben. Falls ios::scientific gesetzt ist, wird die wissenschaftliche (exponentielle)
Notation verwendet.

Bitfeld Flags Bedeutung


floatfield fixed Dezimaldarstellung
scientific Exponentialdarstellung

Tabelle 8.19: Flags für die Darstellung von Gleitkommawerten

Zur Definition der Genauigkeit existiert die Elementfunktion precision() (siehe Tabelle 8.20).

Elementfunktion Bedeutung
precision() liefert die aktuelle Genauigkeit bei der Ausgabe von
Gleitkommawerten
precision(wert) setzt wert als neue Genauigkeit bei der Ausgabe von
Gleitkommawerten und liefert den bisherigen Wert

Tabelle 8.20: Elementfunktionen für die Genauigkeit bei der Ausgabe von Gleitkommawerten

Ohne Parameter aufgerufen, wird die Genauigkeit zurückgeliefert. Mit Parameter aufgerufen,
wird dieser als neue Genauigkeit verwendet (und der alte Wert zurückgeliefert). Der Default-
Wert für die Genauigkeit ist 6.
Ist das Flag ios::fixed gesetzt, werden Gleitkommazahlen in Dezimaldarstellung ausge-
geben; ist das Flag ios::scientific gesetzt, werden sie in Exponentialdarstellung ausgege-
ben. Die mit precision() gesetzte Genauigkeit definiert dabei in beiden Fällen die Anzahl der
Stellen nach dem Dezimalpunkt. Man beachte dabei, dass nicht einfach abgeschnitten, sondern
gerundet wird.
Weder ios::fixed noch ios::scientific ist defaultmäßig gesetzt. In einem solchen Fall
werden sämtliche sinnvolle Stellen, maximal aber precision() Stellen, ausgegeben. Führende
Nullen vor dem Dezimalpunkt und abschließende Nullen nach dem Dezimalpunkt spielen dabei
keine Rolle. Das bedeutet, dass abschließende Nullen und unter Umständen sogar der Dezimal-
punkt nicht ausgegeben werden. Reichen precision() Stellen aus, wird die Dezimaldarstellung
benutzt, ansonsten wird die Exponentialdarstellung verwendet.
Mit showpoint kann explizit verlangt werden, dass der Dezimalpunkt und abschließende
Nullen ausgegeben werden, bis precision() Stellen erreicht sind (siehe Tabelle 8.21).

Flag Bedeutung
showpoint erzwingt die Ausgabe des Dezimalpunktes

Tabelle 8.21: Flag für die Ausgabe des Dezimalpunktes


Sandini Bib

494 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Die etwas komplizierten Abhängigkeiten der Flags und der Genauigkeit zeigt Tabelle 8.22 am
Beispiel zweier konkreter Werte.

precision() 421.0 0.0123456789


normal 2 4.2e+02 0.012
6 421 0.0123457
mit showpoint 2 4.2e+02 0.012
6 421.000 0.0123457
fixed 2 421.00 0.01
6 421.000000 0.012346
scientific 2 4.21e+02 1.23e-02
6 4.210000e+02 1.234568e-02

Tabelle 8.22: Beispiele für das Formatieren von Gleitkommawerten

Auch für Gleitkommawerte kann mit dem Flag showpos die Ausgabe eines positiven Vorzei-
chens veranlasst werden und mit dem Flag uppercase bei der wissenschaftlichen Notation ein
großes E erzwungen werden.
Die Notation und die Genauigkeit von Gleitkommawerten können auch mit den in Tabel-
le 8.23 aufgelisteten Manipulatoren geändert werden. Da setprecision ein Manipulator mit
Parameter ist, muss zu seiner Verwendung die Headerdatei <iomanip> eingebunden werden.

Manipulator Bedeutung
fixed verwendet die dezimale Notation
scientific erzwingt die wissenschaftliche Notation
setprecision(wert) setzt wert als neuen Wert für die Genauigkeit
showpoint erzwingt die Ausgabe des Dezimalpunktes
(setzt das Flag ios::showpoint)
noshowpoint der Dezimalpunkt wird nur bei Bedarf ausgegeben
(löscht das Flag ios::showpoint)

Tabelle 8.23: Manipulatoren für Gleitkommawerte

Die Anweisung
std::cout << std::scientific << std::showpoint
<< std::setprecision(8)
<< 0.123456789 << std::endl;
liefert z.B. folgende Ausgabe:
1.23456789e-01
Sandini Bib

8.1 Die Standard-Stream-Klassen 495

Ein-/Ausgabeformat von Boolschen Werten


Das Flag boolalpha definiert das Format, mit dem Boolesche Werte eingelesen und ausgege-
ben werden. Es legt fest, ob eine numerische oder textuelle Darstellung verwendet wird (siehe
Tabelle 8.24).

Flag Bedeutung
boolalpha falls gesetzt, textuelle Darstellung;
falls nicht gesetzt, numerische Darstellung

Tabelle 8.24: Flag für die Darstellung Boolescher Werte

Falls das Flag nicht gesetzt ist (dies ist das vordefinierte Verhalten), wird beim Ein- und Auslesen
von Booleschen Werten die numerische Darstellung verwendet. In dem Fall wird 0 für false und
1 für true verwendet. Falls in dem Fall ein Boolescher Wert eingelesen wird und das eingelesene
Zeichen weder 0 noch 1 ist, wird dies als Fehler betrachtet und das failbit im Stream gesetzt.
Falls das Flag gesetzt ist, werden Boolesche Werte in einer textuellen Form dargestellt. Dazu
wird die zur jeweiligen Sprachumgebung gehörige textuelle Repräsentation von true und false
verwendet. In der Standard-Sprachumgebung von C++ sind dies die Zeichenfolgen "true" und
"false"; in anderen Sprachumgebungen können es andere Zeichenfolgen sein (beispielsweise
"wahr" und "falsch").
Zum bequemen Manipulieren dieses Flags innerhalb einer Ein- oder Ausgabeanweisung
können auch entsprechende Manipulatoren verwendet werden (siehe Tabelle 8.25).

Manipulator Bedeutung
boolalpha erzwingt die textuelle Darstellung
(setzt das Flag ios::boolalpha)
noboolalpha erzwingt die numerische Darstellung
(löscht das Flag ios::boolalpha)

Tabelle 8.25: Manipulatoren für die Darstellung Boolescher Werte

Mit den folgenden Anweisungen wird z.B. der Wert der Booleschen Variablen b einmal nume-
risch und einmal textuell ausgegeben:
bool b;
...
std::cout << std::noboolalpha << b << " == "
<< std::boolalpha << b << std::endl;
Je nach dem Wert von b und der aktuellen Sprachumgebung lautet die Ausgabe zum Beispiel:
0 == false
Sandini Bib

496 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Allgemeine Formatdefinitionen
Zwei Flags fehlen noch in der Liste der Formatflags: skipws und unitbuf (siehe Tabelle 8.26).

Flag Bedeutung
skipws führende Trennzeichen (Whitespaces) mit Operator >> überlesen
unitbuf Ausgabepuffer nach jeder Ausgabeoperation leeren

Tabelle 8.26: Weitere Flags zur Formatierung

Das Flag skipws ist als Default eingeschaltet. Es führt dazu, dass mit dem Operator >> führen-
de Leerzeichen überlesen werden. Dies ist hilfreich, um z.B. beim Einlesen von numerischen
Werten die trennenden Leerzeichen automatisch zu überlesen. Es bedeutet aber, dass mit dem
Operator >> keine Leerzeichen gelesen werden können. Um dies zu ermöglichen, muss das Flag
ausgeschaltet werden.
unitbuf steuert die Pufferung von Ausgaben. Ist unitbuf gesetzt, werden Ausgaben grund-
sätzlich nicht gepuffert ausgegeben. Nach jeder Ausgabeoperation wird der Ausgabepuffer auto-
matisch geleert. Dieses Flag ist als Default normalerweise nicht gesetzt, was bedeutet, dass die
Ausgaben gepuffert werden. Bei den Standard-Streams für Fehlerausgaben, cerr und wcerr, ist
dieses Flag allerdings automatisch gesetzt, um die ungepufferte Ausgabe zu erzwingen.
Beide Flags können mit den in Tabelle 8.27 aufgelisteten Manipulatoren gesetzt und gelöscht
werden.

Manipulator Bedeutung
skipws überliest führende Leerzeichen mit dem Operator >>
(setzt das Flag ios::skipws)
noskipws überliest führende Leerzeichen mit dem Operator >> nicht
(löscht das Flag ios::skipws)
unitbuf leert den Ausgabepuffer nach jeder Ausgabeoperation
(setzt das Flag ios::unitbuf)
nounitbuf leert den Ausgabepuffer nicht nach jeder Ausgabeoperation
(löscht das Flag ios::unitbuf)

Tabelle 8.27: Manipulatoren zu den weiteren Flags zur Formatierung

8.1.8 Internationalisierung
Man kann die Formatierung der Ein-/Ausgabe an nationale Besonderheiten anpassen. Eine derar-
tige Internationalisierung (englisch: internationalization, oder kurz i18n3 ) wird durch die Klasse
std::locale unterstützt.
Jeder Stream gehört zu einer Sprachumgebung, indem er mit einem Locale-Objekt assozi-
iert ist. Die initiale Sprachumgebung ist dabei eine Kopie der globalen Sprachumgebung zu dem

3
i18n ist eine gängige Abkürzung für i gefolgt von 18 Zeichen, gefolgt von n“.

Sandini Bib

8.1 Die Standard-Stream-Klassen 497

Zeitpunkt, als der Stream erzeugt wurde. Sie kann mit den in Tabelle 8.28 aufgelisteten Funktio-
nen abgefragt und verändert werden.

Elementfunktion Bedeutung
getloc() liefert die aktuelle Sprachumgebung
imbue(loc) setzt loc als aktuelle Sprachumgebung

Tabelle 8.28: Elementfunktionen zur Internationalisierung

Das folgende Beispiel zeigt, wie man diese Elementfunktionen verwenden kann:
// io/loc1.cpp
#include <iostream>
#include <locale>

int main()
{
// verwende die klassische Sprachumgebung, um Daten
// von der Standard-Eingabe zu lesen
std::cin.imbue(std::locale::classic());

// verwende eine deutsche Sprachumgebung, um Daten zu schreiben


std::cout.imbue(std::locale("de_DE"));

// Gleitkommawerte in einer Schleife lesen und ausgeben


double wert;
while (std::cin >> wert) {
std::cout << wert << std::endl;
}
}
Die Anweisung
std::cin.imbue(std::locale::classic());
weist dem Standard-Eingabekanal cin die klassische“ Sprachumgebung zu, die sich wie klas-

sisches C verhält. Der Ausdruck
std::locale::classic()
erzeugt dabei ein entsprechendes Objekt der Klasse std::locale.
Die Anweisung
std::cout.imbue(std::locale("de_DE"));
weist dem Standard-Ausgabekanal entsprechend die Sprachumgebung de_DE zu. Der String
de_DE steht für deutsch in Deutschland“ und hat das übliche Format für Internationalisierungen,

das normalerweise wie folgt aufgebaut ist:
sprache[_gebiet[.zeichenkodierung]]
Sandini Bib

498 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Tabelle 8.29 listet andere Sprachumgebungen auf.

Sprachumgebung Bedeutung
C Default: ANSI-C-Konventionen (Englisch, 7 Bit)
de_DE Deutsch in Deutschland
de_DE.88591 Deutsch in Deutschland mit ISO Latin-1-Kodierung
de_AT Deutsch in Österreich
de_CH Deutsch in der Schweiz
en_US Englisch in den USA
en_GB Englisch in Großbritannien
en_AU Englisch in Australien
en_CA Englisch in Kanada
fr_FR Französisch in Frankreich
fr_CH Französisch in der Schweiz
fr_CA Französisch in Kanada
ja_JP.jis Japanisch in Japan mit Japanese Industrial Standard (JIS) Kodierung
ja_JP.sjis Japanisch in Japan mit Shift JIS Kodierung
ja_JP.ujis Japanisch in Japan mit UNIXifizierter JIS Kodierung
ja_JP.EUC Japanisch in Japan mit Extended UNIX Code Kodierung
ko_KR Koreanisch in Korea
zh_CN Chinesisch in China
zh_TW Chinesisch in Taiwan
lt_LN.bit7 ISO Latin, 7 Bit
lt_LN.bit8 ISO Latin, 8 Bit
POSIX POSIX-Konventionen (Englisch, 7 Bit)

Tabelle 8.29: Auswahl von Namen für Sprachumgebungen

Der Aufruf
std::locale("de_DE"));
ist nur dann erfolgreich, wenn diese Sprachumgebung auch vom System unterstützt wird. Ist dies
nicht der Fall, wird eine Ausnahme vom Typ std::runtime_error ausgelöst.
Falls alles klappt, werden die Eingaben also nach klassischen (englischen) Konventionen
gelesen und die Ausgaben nach deutschen Konventionen geschrieben. Damit liest die Schleife
also Gleitkommawerte im englischen Format und gibt sie im deutschen Format wieder aus. So
wird z.B. der eingelesene Wert
47.11
als
47,11
ausgegeben.
Sandini Bib

8.1 Die Standard-Stream-Klassen 499

Normalerweise definiert ein Programm Sprachumgebungen nur dann, wenn z.B. Konfigurations-
daten aus einer Datei gelesen werden und man sicherstellen muss, dass diese Daten unabhängig
von der aktuellen Sprachumgebung lesbar sind (mit deutscher Sprachumgebung würde der Ver-
such, 47.11 als Gleitkommawert einzulesen, den Wert 47 liefern, da der Punkt den Gleitkomma-
wert beendet). Stattdessen wird normalerweise die Sprachumgebung anhand der Umgebungsva-
riablen LANG ermittelt. Übergibt man einem Locale-Objekt einen Leerstring zur Initialisierung,
wird dazu das entsprechende Objekt erzeugt:
std::locale langLocale(""); // Sprachumgebung anhand LANG
Mit der Elementfunktion name() kann man den Namen einer Sprachumgebung als string ab-
fragen:
// Name der Sprachumgebung ausgeben
std::cout << langLocale.name() << std::endl;

8.1.9 Zusammenfassung
 Für die Ein- und Ausgabe stehen standardmäßig Stream-Klassen zur Verfügung. Dabei wird
zwischen Klassen zur Eingabe, Ausgabe und Ein- und Ausgabe unterschieden.
 Für Stream-Klassen werden zahlreiche Funktionen vordefiniert. Sie umfassen neben der ei-
gentlichen Ein- und Ausgabe Möglichkeiten zur Formatdefinition, zum Zugriff auf Zustands-
bits, zum automatischen Auslösen von Ausnahmen und zur Internationalisierung.
 Die Formatierung wird nicht allein durch Elementfunktionen, sondern auch durch zahlreiche
Flags gesteuert.
 Manipulatoren erlauben es, Streams innerhalb von Ein- oder Ausgabeanweisungen zu mani-
pulieren. Sie können auch selbst definiert werden.
Sandini Bib

500 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

8.2 Dateizugriff
Dieser Abschnitt beschreibt die Besonderheiten der Stream-Klassen, mit denen auf Dateien zu-
gegriffen werden kann.

8.2.1 Stream-Klassen für Dateien


Für den Zugriff auf Dateien stehen folgende spezielle Stream-Datentypen zur Verfügung:
1. Das Klassentemplate basic_ifstream<> mit den beiden Spezialisierungen ifstream und
wifstream dient zum lesenden Zugriff auf Dateien ( input file stream“).

2. Das Klassentemplate basic_ofstream<> mit den beiden Spezialisierungen ofstream und
wofstream dient zum schreibenden Zugriff auf Dateien ( output file stream“).

3. Das Klassentemplate basic_fstream<> mit den Spezialisierungen fstream und wfstream
wird für Dateien benutzt, auf die sowohl lesend als auch schreibend zugegriffen werden kann.
4. Das Klassentemplate basic_filebuf<> mit den Spezialisierungen filebuf und wfilebuf
wird von den anderen Datei-Stream-Klassen für das eigentliche Lesen und Schreiben verwen-
det.
Diese Datentypen werden in der Headerdatei <fstream> definiert und stehen mit den Basis-
Stream-Klassen in der Beziehung, die in Abbildung 8.2 dargestellt ist.
Ein wesentlicher Vorteil der Datei-Stream-Klassen gegenüber den Mechanismen für den Da-
teizugriff von C ist, dass die Dateien beim Deklarieren der Objekte automatisch geöffnet und –
wenn das Objekt zerstört wird – automatisch geschlossen werden. Dies wird durch entsprechen-
de Konstruktoren und Destruktoren sichergestellt. Da das Öffnen aber immer fehlschlagen kann,
sollte dies grundsätzlich getestet werden.

8.2.2 Beispiel für die Verwendung der Stream-Klassen für Dateien


Das folgende Beispielprogramm öffnet die Datei charset.out, um darin den aktuellen Zei-
chensatz (alle Zeichen zu den Werten 32 bis 255) auszugeben:
// io/charset2.cpp
#include <string> // für Strings
#include <iostream> // für I/O
#include <fstream> // für Datei-I/O
#include <iomanip> // für setw()
#include <cstdlib> // für EXIT_FAILURE

// Vorwärtsdeklarationen
void zeichensatzInDateiSchreiben (const std::string& dateiname);
void dateiAusgeben (const std::string& dateiname);
Sandini Bib

8.2 Dateizugriff 501

i o s _ b a s e b a s i c _ s t r e a m b u f < >
s t r e a m b u f / w s t r e a m b u f

b a s i c _ i o s < >
i o s / w i o s b a s i c _ f i l e b u f < >
f i l e b u f / w f i l e b u f
{ v i r t u a l }

b a s i c _ i s t r e a m < > b a s i c _ o s t r e a m < >


i s t r e a m / w i s t r e a m o s t r e a m / w o s t r e a m

b a s i c _ i o s t r e a m < >
i o s t r e a m / w i o s t r e a m

b a s i c _ i f s t r e a m < > b a s i c _ o f s t r e a m < >


i f s t r e a m / w i f s t r e a m o f s t r e a m / w o f s t r e a m

b a s i c _ f s t r e a m < >
f s t r e a m / w f s t r e a m

Abbildung 8.2: Hierarchie der Stream-Klassen zum Dateizugriff

int main ()
{
try {
zeichensatzInDateiSchreiben("charset.out");
dateiAusgeben("charset.out");
}
catch (const std::string& msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Sandini Bib

502 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

void zeichensatzInDateiSchreiben (const std::string& dateiname)


{
// Datei zum Schreiben öffnen
std::ofstream datei(dateiname.c_str());

// wurde die Datei wirklich geöffnet?


if (! datei) {
// NEIN, Ausnahme auslösen
throw "kann Datei \"" + dateiname
+ "\" nicht zum Schreiben oeffnen";
}

// Zeichensatz in Datei schreiben


for (int i=32; i<127; ++i) {
// Wert als Zahl und als Zeichen ausgeben:
datei << "Wert: " << std::setw(3) << i << " "
<< "Zeichen: " << static_cast<char>(i) << std::endl;
}

} // schließt die Datei automatisch

void dateiAusgeben (const std::string& dateiname)


{
// Datei zum Lesen öffnen
std::ifstream datei(dateiname.c_str());

// wurde die Datei wirklich geöffnet?


if (! datei) {
// NEIN, Ausnahme auslösen
throw "kann Datei \"" + dateiname
+ "\" nicht zum Lesen oeffnen";
}

// alle Zeichen der Datei nach std::cout kopieren


char c;
while (datei.get(c)) {
std::cout.put(c);
}

} // schließt die Datei automatisch


Sandini Bib

8.2 Dateizugriff 503

In zeichensatzInDateiSchreiben() sorgt der Konstruktor der Klasse ofstream dafür, dass


die Datei, deren Name übergeben wurde, mit dem Erzeugen des Objekts geöffnet wird. Mit der
folgenden Abfrage wird festgestellt, ob der Zustand des Streams in Ordnung ist, das Öffnen also
erfolgreich war. Anschließend werden in einer Schleife alle Werte von 32 bis 126 mit den da-
zugehörigen Zeichen in diese Datei geschrieben. Am Ende der Funktion wird die Datei automa-
tisch wieder geschlossen, da der Gültigkeitsbereich des Objekts verlassen wird. Der Destruktor
der Klasse ofstream ist entsprechend definiert.
Entsprechend wird in dateiAusgeben() die übergebene Datei zum Lesen geöffnet. Falls
dies nicht möglich ist, wird auch hier eine entsprechende Fehlerbehandlung angestoßen. An-
sonsten werden alle Zeichen aus der Datei gelesen und auf den Standard-Ausgabekanal cout
ausgegeben. Auch hier wird die geöffnete Datei mit dem Verlassen der Funktion automatisch
geschlossen.
Soll eine Datei über Funktionsgrenzen hinweg erhalten bleiben, kann ein Dateiobjekt explizit
angelegt und wieder zerstört werden:
std::ofstream* dateiPtr = new std::ofstream("datei.txt");
...
delete dateiPtr;

8.2.3 Datei-Flags
Für den genauen Modus der Dateibearbeitung existieren wiederum einige Flags, die in der Klasse
ios_base definiert werden (siehe Tabelle 8.30).

Flag Bedeutung
in zum Lesen öffnen (Default bei ifstream)
out zum Schreiben öffnen (Default bei ofstream)
app beim Schreiben immer anhängen
ate nach dem Öffnen an das Dateiende positionieren ( at end“)

trunc bisherigen Dateiinhalt löschen
binary spezielle Zeichen nicht ersetzen

Tabelle 8.30: Flags zum Öffnen von Dateien

binary sollte bei einem Zugriff auf Binärdateien (also zum Beispiel ausführbare Program-
me) gesetzt werden. Damit wird sichergestellt, dass die Zeichen so, wie sie sind, eingelesen
und geschrieben werden. Ist binary nicht gesetzt, wird automatisch berücksichtigt, dass in der
Windows-Welt das Zeilenende nicht durch ein Newline-Zeichen, sondern durch die Zeichen-
folge CarriageReturn/Linefeed (CR/LF) gekennzeichnet wird. Ohne binary wird deshalb auf
Windows-Systemen beim Einlesen die Zeichenfolge CR/LF in das Zeichen Newline umgesetzt
und beim Schreiben ein Newline als Zeichenfolge CR/LF geschrieben.
Zusätzlich existieren bei manchen Compilern spezielle Flags, wie nocreate oder noreplace.
Diese Flags sind aber nicht standardisiert, und ihre Verwendung ist insofern nicht portabel.
Sandini Bib

504 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Die Flags können mit dem Operator | verknüpft werden. Sie werden dann dem Konstruktor als
optionales zweites Argument übergeben. Die folgende Anweisung öffnet z.B. eine Datei zum
Schreiben und sorgt dafür, dass der geschriebene Text an das Ende der Datei angehängt wird:
std::ofstream datei("xyz.out", std::ios::out | std::ios::app);
Tabelle 8.31 stellt die verschiedenen Kombinationen dieser Flags den Strings der C-Schnittstellen
zum Öffnen von Dateien, fopen(), gegenüber. Auf die Kombinationen mit binary und ate
wird nicht eingegangen. Ein gesetztes binary entspricht dem String aus dem C-Modus mit ei-
nem angehängten b. Ein gesetztes ate bedeutet nur, dass unmittelbar nach dem Öffnen der Datei
an das Ende gegangen wird. Andere, nicht aufgelistete Kombinationen, wie trunc|app, sind
nicht erlaubt.

ios_base Flags Bedeutung C-Modus


in lesen (Datei muss existieren) "r"
out leeren und schreiben (gegebenenfalls auch erzeugen) "w"
out|trunc leeren und schreiben (gegebenenfalls auch erzeugen) "w"
out|app anhängen (gegebenenfalls auch erzeugen) "a"
in|out lesen und schreiben (Datei muss existieren) "r+"
in|out|trunc leeren, lesen und schreiben (gegebenenfalls auch er- "w+"
zeugen)

Tabelle 8.31: Bedeutung der Flag-Kombinationen zum Öffnen von Dateien

8.2.4 Explizites Öffnen und Schließen


Wenn ein Datei-Objekt angelegt wird, ohne es zu initialisieren, wird keine Datei geöffnet. Dies
kann später mit der Elementfunktion open() explizit nachgeholt werden. Entsprechend kann ei-
ne geöffnete Datei mit close() explizit geschlossen werden. Außerdem kann abgefragt werden,
ob eine Datei geöffnet ist (siehe Tabelle 8.32).

Elementfunktion Bedeutung
open(name) öffnet eine Datei mit dem Default-Modus des Streams
open(name,flags) öffnet eine Datei mit flags als Modus
close() schließt eine Datei
is_open() liefert, ob eine Datei geöffnet ist

Tabelle 8.32: Elementfunktionen zum expliziten Öffnen und Schließen von Dateien

Das folgende Beispiel zeigt eine Verwendung. Es öffnet nacheinander alle als Parameter in der
Kommandozeile übergebenen Dateien und gibt deren Inhalt aus (dies entspricht dem UNIX-
Programm cat):
Sandini Bib

8.2 Dateizugriff 505

// io/cat1.cpp
// Headerdateien
#include <fstream>
#include <iostream>
using namespace std;

/* für alle in der Kommandozeile übergebenen Dateinamen


* - Datei öffnen, Inhalt ausgeben und Datei schließen
*/
int main (int argc, char* argv[])
{
// Datei-Stream zum Lesen anlegen (ohne eine Datei zu öffnen)
std::ifstream datei;

// für alle Argumente aus der Kommandozeile


for (int i=1; i<argc; ++i) {

// Datei öffnen
datei.open(argv[i]);

// Inhalt der Datei ausgeben


char c;
while (datei.get(c)) {
std::cout.put(c);
}

// eofbit und failbit löschen (wurden wegen EOF gesetzt)


datei.clear();

// Datei schließen
datei.close();
}
}
In dem Programm werden für den Stream datei nacheinander die übergebenen Dateinamen
geöffnet, ausgegeben und wieder geschlossen. Man beachte, dass dabei nach der Ausgabe des
Inhalts jeweils clear() aufgerufen wird. Dies ist notwendig, weil am Ende der Datei eofbit
und failbit gesetzt werden und diese vor jeder weiteren Operation gelöscht werden müssen.
Auch open() löscht diese Bits nicht.
Sandini Bib

506 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

8.2.5 Wahlfreier Zugriff


Zur Positionierung in Dateien existieren in C++ die in Tabelle 8.33 aufgelisteten Elementfunk-
tionen.

Elementfunktion Bedeutung
tellg() liefert eine Leseposition
seekg(pos) setzt pos als absolute Leseposition
seekg(offset,rpos) setzt pos als Leseposition relativ zu offset
tellp() liefert eine Schreibposition
seekp(pos) setzt pos als absolute Schreibposition
seekp(offset,rpos) setzt pos als Schreibposition relativ zu offset

Tabelle 8.33: Elementfunktionen zum Positionieren in Dateien

Zur Sicherheit wird zwischen der Lese- und der Schreibposition unterschieden (g steht für get“

und p steht für put“). Die Lesefunktionen werden in istream, die Schreibfunktionen dagegen

in ostream definiert. Dies bedeutet aber nicht, dass sie für alle Objekte der Klasse istream bzw.
ostream aufgerufen werden dürfen. Ein Aufruf für cin, cout oder cerr ist z.B. nicht definiert.
Die Tatsache, dass die Funktionen nicht erst in den Datei-Stream-Klassen definiert werden, be-
ruht darauf, dass die Klassen beim Lesen und Schreiben oft als istream oder ostream betrachtet
werden, da dafür die meisten I/O-Operationen definiert sind.
Aufgerufen werden seekg() und seekp() mit absoluten oder relativen Positionsangaben:
 Ein absoluter Wert ist die Position eines Zeichens, die als long übergeben wird (manchmal
wird dafür ein anderer Datentyp wie etwa streampos definiert).
tellg() bzw. tellp() dienen dazu, solche Positionen zu ermitteln. Ihre Verwendung
ist empfehlenswert, da sich mitunter die logische und die tatsächliche Zeichenposition un-
terscheiden können. Dies geschieht z.B. bei DOS-Dateien, in denen das Zeichen Newline
tatsächlich aus zwei Zeichen (CR und LF) besteht.
Beispiel:
// aktuelle Position merken
std::ios::pos_type pos = datei.tellg();
...
// zur gemerkten Position zurückkehren
datei.seekg(pos);
Man beachte, dass es einen speziellen Datentyp für Dateipositionen, std::ios::pos_type
gibt. Eine Verwendung eines ganzzahligen Datentyps wie long ist dagegen nicht erlaubt.4
Man kann allerdings stattdessen auch std::streampos verwenden.
 Bei relativen Werten können zu drei Konstanten Offsets angegeben werden. Die Konstanten
werden von der Klasse ios_base wie in Tabelle 8.34 aufgelistet definiert.

4
Dies war in C++-Versionen von der Standardisierung möglich.
Sandini Bib

8.2 Dateizugriff 507

Konstante Bedeutung
beg relativ zum Dateianfang ( begin“)

cur relativ zur aktuellen Position ( current“)

end relativ zum Dateiende ( end“)

Tabelle 8.34: Konstanten für Dateipositionen

Beispiel:
// an den Anfang positionieren
datei.seekp (0, std::ios::beg);
...
// 20 Zeichen weiter positionieren
datei.seekp (20, std::ios::cur);
...
// 10 Zeichen vor das Ende positionieren
datei.seekp (-10, std::ios::end);

Grundsätzlich muss darauf geachtet werden, dass nicht vor den Dateianfang oder hinter das Da-
teiende positioniert wird. Ansonsten ist das Verhalten undefiniert.
Das folgende Beispiel zeigt die Verwendung der Funktionen zur Positionierung anhand einer
Funktion, die zweimal hintereinander die gleiche Datei ausgibt:
// io/cat2.cpp
// Headerdateien
#include <iostream>
#include <fstream>
#include <string>

void dateiZweimalAusgeben (const std::string& dateiname)


{
char c;

// Datei zum Lesen öffnen


std::ifstream datei(dateiname.c_str());

// Inhalt der Datei zum ersten Mal ausgeben


while (datei.get(c)) {
std::cout.put(c);
}

// eofbit und failbit löschen (wurden wegen EOF gesetzt)


datei.clear();
Sandini Bib

508 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

// Leseposition auf den Dateianfang setzen


datei.seekg(0);

// Inhalt der Datei zum zweiten Mal ausgeben


while (datei.get(c)) {
std::cout.put(c);
}
}

int main (int argc, char* argv[])


{
// alle in der Kommandozeile übergebenen Dateien zweimal ausgeben
for (int i=1; i<argc; ++i) {
dateiZweimalAusgeben(argv[i]);
}
}
Auch hier muss beachtet werden, dass am Dateiende eofbit und failbit gesetzt werden. Da-
mit ist auch eine Positionierung des Streams nicht mehr möglich. Aus diesem Grund muss vor
dem Aufruf von seekg() clear() aufgerufen werden.

8.2.6 Umleiten der Standardkanäle in Dateien


Durch Manipulierung der Streambuffer kann man Ein- und Ausgaben innerhalb eines Programms
umleiten. Das bedeutet, dass man z.B. dafür sorgen kann, dass alle Ausgaben nach cout nicht
auf die Standard-Ausgabe, sondern in eine Datei geschrieben werden.5
Der Trick besteht darin, dem Standard-Stream einfach einen anderen Streambuffer zuzuord-
nen:
std::ofstream datei("cout.txt"); // Datei öffnen
std::cout.rdbuf(datei.rdbuf()); // Streambuffer cout zuweisen
Dabei kann man mit copyfmt() optional alle Formatinformationen übernehmen:
std::ofstream datei("cout.txt"); // Datei öffnen
datei.copyfmt(std::cout); // Formatierungen von cout übernehmen
std::cout.rdbuf(datei.rdbuf()); // Streambuffer cout zuweisen
Vorsicht! Wenn ein Datei-Stream beim Verlassen des Gültigkeitsbereichs zerstört wird, wird
auch der dazugehörige Streambuffer zerstört. Damit darf nach der obigen Anweisung nach der
Zerstörung von datei auf keinen Fall mehr auf cout zugegriffen werden. Sogar eine Zerstörung

5
In älteren Versionen von C++ war dies durch eine einfache Zuweisung möglich, da die Objekte cin,
cout und cerr den Spezialtyp istream_withassign bzw. ostream_withassign hatten. Diese Typen
wurden im Rahmen der Standardisierung entfernt, und der Mechanismus zum Umleiten arbeitet nun wie
hier beschrieben.
Sandini Bib

8.2 Dateizugriff 509

von cout am Programmende ist kritisch. Aus diesem Grund sollte nach einer Umleitung von
Streams immer wieder der Zustand vor der Umleitung hergestellt werden. Das folgende Pro-
gramm zeigt ein vollständiges Beispiel:
// io/redirect.cpp
#include <iostream>
#include <fstream>
#include <string>

void redirect(std::ostream&,const std::string&);

int main()
{
std::cout << "erste Zeile" << std::endl;

redirect(std::cout, "redirect.txt"); // cout nach redirect.txt umleiten

std::cout << "letzte Zeile" << std::endl;


}

void redirect (std::ostream& strm, const std::string& dateiname)


{
// Datei (mit dazugehörigem Puffer) zum Schreiben öffnen
std::ofstream datei(dateiname.c_str());

// Ausgabepuffer des übergebenen Streams merken


std::streambuf* strm_puffer = strm.rdbuf();

// Ausgaben in die Datei umlenken


strm.rdbuf(datei.rdbuf());

datei << "direkt in die Datei geschriebene Zeile" << std::endl;


strm << "auf den umgelenkten Stream geschriebene Zeile"
<< std::endl;

// alten Ausgabepuffer des übergebenen Streams restaurieren


strm.rdbuf(strm_puffer);

} // schließt die Datei und den dazugehörigen Puffer

Die Ausgabe des Programms lautet:


erste Zeile
letzte Zeile
Sandini Bib

510 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Der Inhalt der Datei redirect.txt lautet danach:


direkt in die Datei geschriebene Zeile
auf den umgelenkten Stream geschriebene Zeile

8.2.7 Zusammenfassung
 Spezielle Stream-Klassen erlauben Ein- und Ausgaben in Dateien.
 Spezielle Elementfunktionen erlauben das Positionieren in Dateien.
 Wird bis zum Ende einer Datei gelesen, werden eofbit und failbit gesetzt. Diese müssen
vor weiteren Operationen mit dem Stream gelöscht werden.
 Die Standardkanäle können durch Zuweisung anderer Streambuffer umgeleitet werden. Der-
artige Umleitungen sollten danach unbedingt restauriert werden.
Sandini Bib

8.3 Stream-Klassen für Strings 511

8.3 Stream-Klassen für Strings


Die Mechanismen der Stream-Klassen können auch dazu verwendet werden, von Strings zu le-
sen bzw. auf Strings zu schreiben. Dies ist vor allem dann nützlich, wenn es sich bei den Strings
um eigene Ein- oder Ausgabepuffer handelt. Es kann z.B. sinnvoll sein, zunächst eine komplette
Zeile einer Datei einzulesen und diese dann erst zu verarbeiten. Ein anderes Beispiel ist das for-
matierte Schreiben in einen String, um z.B. eine Fehlermeldung mit dem Wert von ganzen Zahlen
oder anderen Objekten zurückzuliefern. Schließlich kann man durch formatiertes Schreiben in
und formatiertes Lesen aus Strings auch ganz allgemeine Typumwandlungen definieren.
Für die standardkonforme Umgebung existieren Klassen, die strings als Streams verwen-
den. Daneben werden auch kurz die Klassen vorgestellt, die C-Strings als Streams verwenden.
Sie sind eigentlich veraltet, werden aber dennoch angewendet und bergen einige Fallen, die man
kennen sollte.

8.3.1 String-Stream-Klassen
Analog zu den Stream-Klassen für Dateien gibt es Stream-Klassen für Strings:
 Das Klassentemplate basic_istringstream<> mit den Spezialisierungen istringstream
und wistringstream zum formatierten Lesen aus Strings ( input string stream“)

 Das Klassentemplate basic_ostringstream<> mit den Spezialisierungen ostringstream
und wostringstream zum formatierten Schreiben in Strings ( output string stream“)

 Das Klassentemplate basic_stringstream<> mit den Spezialisierungen stringstream
und wstringstream zum formatierten Lesen aus und Schreiben in Strings
 Das Klassentemplate basic_stringbuf<> mit den beiden Spezialisierungen stringbuf
und wstringbuf wird von den anderen String-Stream-Klassen für das eigentliche Lesen und
Schreiben verwendet.
Diese Datentypen werden in der Headerdatei <sstream> definiert und stehen mit den Basis-
Stream-Klassen in der in Abbildung 8.3 dargestellten Beziehung.
Die wesentliche Funktion, die bei String-Streams hinzukommt, ist die Funktion str(), mit
der der String des String-Streams gesetzt bzw. abgefragt werden kann (siehe Tabelle 8.35).

Elementfunktion Bedeutung
str() liefert den Inhalt des String-Streams als string
str( string) setzt den Inhalt des String-Streams auf string

Tabelle 8.35: Fundamentale Operationen für String-Streams

Beispiel für formatiertes Schreiben in Strings


Das folgende Programm zeigt die Verwendung von String-Streams zum formatierten Schreiben
in Strings:
Sandini Bib

512 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

i o s _ b a s e b a s i c _ s t r e a m b u f < >
s t r e a m b u f / w s t r e a m b u f

b a s i c _ i o s < >
i o s / w i o s b a s i c _ s t r i n g b u f < >
s t r i n g b u f / w s t r i n g b u f
{ v i r t u a l }

b a s i c _ i s t r e a m < > b a s i c _ o s t r e a m < >


i s t r e a m / w i s t r e a m o s t r e a m / w o s t r e a m

b a s i c _ i o s t r e a m < >
i o s t r e a m / w i o s t r e a m

b a s i c _ i s t r i n g s t r e a m < > b a s i c _ o s t r i n g s t r e a m < >


i s t r i n g s t r e a m / w i s t r i n g s t r e a m o s t r i n g s t r e a m / w o s t r i n g s t r e a m

b a s i c _ s t r i n g s t r e a m < >
s t r i n g s t r e a m / w s t r i n g s t r e a m

Abbildung 8.3: Hierarchie der String-Stream-Klassen

// io/sstr1.cpp
#include <iostream>
#include <sstream>

int main()
{
// String-Stream zum formatierten Schreiben anlegen
std::ostringstream os;

// ganzzahlige Werte dezimal und hexadezimal schreiben


os << "dec: " << 15 << std::hex << " hex: " << 15 << std::endl;

// String-Stream als String ausgeben


std::cout << os.str() << std::endl;
Sandini Bib

8.3 Stream-Klassen für Strings 513

// Gleitkommawerte anhängen
os << "float: " << 4.67 << std::endl;

// String-Stream als String ausgeben


std::cout << os.str() << std::endl;

// String-Stream vorn mit oktalem Wert überschreiben


os.seekp(0);
os << "oct: " << std::oct << 15;

// String-Stream als String ausgeben


std::cout << os.str() << std::endl;
}
Die Ausgabe des Programms lautet wie folgt:
dec: 15 hex: f

dec: 15 hex: f
float: 4.67

oct: 17 hex: f
float: 4.67

Zunächst wird der Wert 15 dezimal und hexadezimal formatiert in den String-Stream geschrie-
ben und der String ausgegeben. Anschließend wird ein Gleitkommawert in den String-Stream
geschrieben, was bedeutet, dass dieser Gleitkommawert an den bisherigen Inhalt angehängt wird.
Schließlich wird die Schreibposition mit seekp() an den Anfang gesetzt, und mit der folgen-
den Ausgabe des oktalen Wertes von 15 wird der bisher dort stehende Inhalt des Strings über-
schrieben. Wie die anschließende Ausgabe zeigt, bleiben die restlichen Zeichen im String damit
erhalten.
Da der String selbst nach jeder Zeile durch das Schreiben von std::endl einen Zeilenum-
bruch enthält und bei seiner Ausgabe nochmal std::endl ausgegeben wird, wird bei jeder Aus-
gabe eine abschließende Leerzeile ausgegeben.
Will man den String leeren, muss man den Leerstring als neuen Inhalt zuweisen:
std::ostringstream os; // String-Stream anlegen
...
os.str(""); // String-Stream leeren
Sandini Bib

514 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Beispiel für formatiertes Lesen aus Strings


Entsprechend kann man aus Input-String-Streams formatiert lesen:
// io/sstr2.cpp
#include <iostream>
#include <sstream>

int main()
{
// String anlegen, aus dem formatiert gelesen werden soll
std::string s = "Pi: 3.1415";

// String-Stream zum formatierten Lesen anlegen


// und mit dem String initialisieren
std::istringstream is(s);

// ersten String und Wert dazu formatiert auslesen


std::string name;
double wert;
is >> name >> wert;

// eingelesene Daten ausgeben


std::cout << "Name: " << name << std::endl;
std::cout << "Wert: " << wert << std::endl;
}
Die Ausgabe des Programms lautet:
Name: Pi:
Wert: 3.1415

8.3.2 Lexical-Cast-Operator
Mit Hilfe von String-Streams und Templates kann man sich bequem einen eigenen Operator zur
logischen Typumwandlung implementieren:
// io/lexcast.hpp
#include <sstream>

template<typename ZielTyp, typename QuellTyp>


ZielTyp lexical_cast(QuellTyp in)
{
std::stringstream interpreter;
ZielTyp out;
Sandini Bib

8.3 Stream-Klassen für Strings 515

if(!(interpreter << in) || !(interpreter >> out) ||


!(interpreter >> std::ws).eof()) {
throw "bad lexical cast";
}

return out;
}
Der erste Template-Parameter ist der Zieltyp und wird explizit festgelegt (siehe Seite 435). Der
zweite Template-Parameter wird implizit durch das übergebene Argument festgelegt. In der
Funktion wird das übergebene Argument in einen String-Stream geschrieben und als Zieltyp
wieder ausgelesen. Die letzte Abfrage stellt fest, ob es noch Zeichen gibt, die bei der Umwand-
lung nicht verarbeitet wurden. Dies stellt sicher, dass der String 42.42 nicht erfolgreich in einen
int umgewandelt wird.
Der Aufruf dieses Operators kann z.B. wie folgt aussehen:
// io/lexcast.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include "lexcast.hpp"

int main (int argc, char* argv[])


{
try {
if (argc > 1) {
// erstes Argument als int auswerten
int wert = lexical_cast<int>(argv[1]);

// int als String verwenden


std::string msg;
msg = "Der uebergebene Wert ist: "
+ lexical_cast<std::string>(wert);
std::cout << msg << std::endl;
}
}
catch (const char* msg) {
std::cerr << "Exception: " << msg << std::endl;
return EXIT_FAILURE;
}
}
Im Boost-Repository für ergänzende C++-Bibliotheken befindet sich eine kommerzielle Version
dieses ergänzenden Operators (siehe http://www.boost.org).
Sandini Bib

516 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

8.3.3 char*-Stream-Klassen
Die Stream-Klassen, die formatiert auf C-Strings zugreifen, sind eigentlich nur aus Gründen der
Rückwärtskompatibilität vorhanden. Sie werden allerdings zum Teil immer noch verwendet, und
da dabei einige Fallen lauern, sollen sie kurz vorgestellt werden.
Analog zu den String-Stream-Klassen gibt es Stream-Klassen für den Datentyp char*:
 Die Klasse istrstream für Strings, aus denen gelesen wird ( input string stream“)

 Die Klasse ostrstream für Strings, in die geschrieben wird ( output string stream“)

 Die Klasse strstream für Strings, die zum Lesen und Schreiben verwendet werden
 Die Klasse strstreambuf als Puffer für char*-Streams
Sie werden in der Headerdatei <strstream> definiert.

Lesen aus char*-Streams


Ein istrstream kann mit einem String (Typ char*) initialisiert werden, der entweder das En-
dezeichen '\0' besitzt oder für den zusätzlich dessen Anzahl von Zeichen übergeben wird.
Ein typisches Beispiel ist das Lesen und anschließende Verarbeiten einer ganzen Zeile:
char puffer[1000]; // Puffer für maximal 999 Zeichen

// Zeile lesen
std::cin.get(puffer,sizeof(puffer));

// Zeile als Stream verarbeiten


std::istrstream eingabe(puffer);
...
eingabe >> x;

Schreiben in char*-Streams
Ein char*-Stream zum Schreiben kann entweder von sich aus wachsen oder mit einem Puffer
fester Größe initialisiert werden. Mit ios::app oder ios::ate kann man die in den String-
Stream geschriebenen Ausgaben an einen bereits im Puffer befindlichen String anhängen.
Mit der Elementfunktion str() wird auch hier der Inhalt des Strings geliefert. Allerdings
muss hier Folgendes beachtet werden:
 Sofern der char*-Stream nicht mit einem Puffer fester Größe initialisiert wurde, geht er mit
str() in den Besitz des Aufrufers über. Damit der Speicherplatz wieder freigegeben wird,
muss er mit freeze() wieder dem char*-Stream übergeben werden.
 Nach dem Aufruf von str() ist der Stream eingefroren und kann nicht weiter manipuliert
werden. Auch hier kann mit freeze() erreicht werden, dass der char*-Stream wieder ma-
nipuliert werden kann.
 str() hängt kein Stringendekennzeichen '\0' an. Wird dies benötigt, muss es mit Hilfe des
Manipulators std::ends explizit angehängt werden.
Sandini Bib

8.3 Stream-Klassen für Strings 517

Das folgende Programm zeigt, wie char*-Streams für das formatierte Schreiben verwendet wer-
den können:
// io/charstr1.cpp
#include <iostream>
#include <strstream>

int main()
{
// dynamischen char*-Stream zum Schreiben erzeugen
std::ostrstream puffer;

// formatiert beschreiben und mit Stringendekennzeichen abschließen


puffer << "Pi: " << 3.1415 << std::ends;

/* Zeichenfolge ausgeben
* - str() friert char*-Stream ein
*/
std::cout << puffer.str() << std::endl;

// das Einfrieren aufheben


puffer.freeze(false);

// so positionieren, dass std::ends überschrieben wird


puffer.seekp(-1,std::ios::end);

// char* stream weiter beschreiben


puffer << " oder auch: " << std::scientific << 3.1415
<< std::ends;

/* Zeichenfolge ausgeben
* - str() friert char*-Stream ein
*/
std::cout << puffer.str() << std::endl;

// das Einfrieren aufheben, damit Speicherplatz freigegeben wird


puffer.freeze(false);
}
Das Programm hat folgende Ausgabe:
Pi: 3.1415
Pi: 3.1415 oder auch: 3.141500e+000
Sandini Bib

518 Kapitel 8: Die Standard-I/O-Bibliothek im Detail

Man beachte, dass der jeweilige Aufruf von str() den Stream einfriert, was für weitere Ände-
rungen oder für die korrekte Freigabe des dazugehörigen Speicherplatzes jeweils mit
puffer.freeze(false);
aufgehoben wird.
Man beachte auch, dass jeweils std::ends angehängt wird, damit der String als C-String
verwendet werden kann. Da dieses Stringendekennzeichen aber mitten im String den C-String
beenden würde, muss es für weitere angehängte Ausgaben überschrieben werden. Mit
puffer.seekp(-1,std::ios::end);
wird die Schreibposition entsprechend gesetzt.
All diese Probleme hat man mit den in Abschnitt 8.3.1 vorgestellten String-Streams nicht.

8.3.4 Zusammenfassung
 Spezielle Stream-Klassen erlauben Ein- und Ausgaben in Strings (Datentyp string).
 In String-Streams kann man Lese- und Schreibpositionen ebenfalls explizit setzen.
 Um die Rückwärtskompatibilität zu wahren, besteht auch die Möglichkeit, in C-Strings vom
Typ char* zu schreiben und aus ihnen zu lesen. Diese Möglichkeit ist allerdings fehler-
trächtig und sollte vermieden werden.
 Mit dem selbst definierten Operator lexical_cast kann man bequem Datentypen logisch
umwandeln.
Sandini Bib

Kapitel 9
Weitere Sprachmittel und Details

In diesem Kapitel werden all die Sprachmittel erläutert, die im bisherigen Verlauf des Buchs noch
nicht erläutert wurden, in der Praxis aber eine wichtige Rolle spielen (können). Hinzu kommen
einige Details zur Standardbibliothek.
Im Einzelnen wird auf STL-Container (insbesondere Vektoren), numerische Limits, Smart-
Pointer, Funktionsobjekte, verschiedene weitere Aspekte der Verwendung von new und delete,
Funktions- und Komponentenzeiger, die Kombination von C++-Code mit C-Code und einige
noch offene Schlüsselwörter eingegangen.

519
Sandini Bib

520 Kapitel 9: Weitere Sprachmittel und Details

9.1 Weitere Details zur Standardbibliothek


In diesem Abschnitt werden verschiedene Komponenten aus der Standardbibliothek nochmal im
Detail erläutert. Dazu gehören Vektoren und numerische Limits. Außerdem werden die für alle
STL-Container verfügbaren Operationen und alle STL-Algorithmen aufgelistet.
Auch hier sei noch einmal darauf hingewiesen, dass dieses Buch unmöglich alle Details der
Standardbibliothek erläutern kann. Dazu benötigt man ein eigenes Buch. Für weitere Details sei
insbesondere auf mein Standardwerk The C++ Standard Library – A Tutorial and Reference“

verwiesen (siehe Literaturliste).

9.1.1 Operationen von Vektoren


Nachdem die Klasse vector<> bereits in Abschnitt 3.5.1 eingeführt und an verschiedenen Stel-
len im Buch verwendet wurde, werden hier noch einmal die möglichen Operationen im Einzelnen
vorgestellt.

Erzeugen, kopieren, zerstören


Tabelle 9.1 listet die Konstruktoren und den Destruktor für Vektoren auf. Vektoren können mit
und ohne Elemente zur Initialisierung erzeugt werden. Auch ohne Elemente kann eine Startgröße
angegeben werden, was dazu führt, dass die Elemente jeweils mit ihrem Default-Konstruktor
erzeugt werden.

Ausdruck Bedeutung
vector<typ> v erzeugt einen leeren Vektor ohne Elemente
vector<typ> v1(v2) erzeugt einen Vektor als Kopie eines anderen Vektors
(alle Elemente werden kopiert)
vector<typ> v(n) erzeugt einen Vektor mit n Elementen, die jeweils mit
dem Default-Konstruktor erzeugt werden
vector<typ> v(n,elem) erzeugt einen Vektor mit n Elementen, die jeweils
Kopien von elem sind
vector<typ> v(anf,end) erzeugt einen Vektor und initialisiert ihn mit Kopien der
Elemente aus dem Bereich [anf;end)
v.~vector< typ>() löscht alle Elemente und gibt den Speicherplatz wieder
frei

Tabelle 9.1: Konstruktoren und Destruktoren von Vektoren

Die Initialisierung der Elemente durch einen übergebenen Bereich kann vor allem dazu verwen-
det werden, einen Vektor mit Elementen aus einem anderen Container zu initialisieren:1
std::list<Elem> l; // l ist verkettete Liste von Elementen
...
// Vektor v mit Elementen der Liste l initialisieren
std::vector<Elem> v(l.begin(),l.end());

1
Sofern ein System noch keine Elementfunktionstemplates unterstützt, kann der Bereich, anhand dessen
initialisiert wird, typischerweise nur vom gleichen Typ sein.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 521

Vergleiche
Tabelle 9.2 listet die Operationen zum Vergleichen auf.

Ausdruck Bedeutung
v1 == v2 liefert, ob v1 gleich v2 ist
v1 != v2 liefert, ob v1 ungleich v2 ist
(entspricht: !(v1==v2))
v1 < v2 liefert, ob v1 kleiner als v2 ist
v1 > v2 liefert, ob v1 größer als v2 ist
(entspricht: v2<v1)
v1 <= v2 liefert, ob v1 kleiner oder gleich v2 ist
(entspricht: !(v2<v1))
v1 >= v2 liefert, ob v1 größer oder gleich v2 ist
(entspricht: !(v1<v2))

Tabelle 9.2: Vergleichsoperatoren von Vektoren

Vergleiche mit den üblichen Vergleichsoperatoren sind nur zwischen Vektoren mit gleichem Ele-
menttyp möglich. Zum Vergleichen von zwei Containern verschiedener Klassen bzw. Typen,
müssen STL-Algorithmen verwendet werden (siehe Abschnitt 9.1.3).

Größe und Kapazität


Für einen effektiven und korrekten Einsatz von Vektoren ist es wichtig, dass man das Zusammen-
spiel von Größe und Kapazität verstanden hat. Tabelle 9.3 listet die dazugehörigen Operationen
auf.

Ausdruck Bedeutung
v.size() liefert die aktuelle Anzahl von Elementen
v.empty() liefert, ob der Container leer ist
(entspricht: size()==0)
v.max_size() liefert die maximal mögliche Anzahl von Elementen
v.capacity() liefert die Kapazität (maximale Anzahl von Elementen ohne
Reallokierung)
v.reserve(anz) vergrößert gegebenenfalls die Kapazität auf mindestens anz Elemente

Tabelle 9.3: Elementfunktionen zur Größe und Kapazität von Vektoren

Drei Größenangaben existieren für Vektoren:


 size()
liefert jeweils die aktuelle Anzahl von Elementen, die sich im Vektor befinden.
Die Elementfunktion empty() ist eine Kurzform, um zu prüfen, ob die Anzahl der Ele-
mente 0 ist (der Vektor also leer ist).
Sandini Bib

522 Kapitel 9: Weitere Sprachmittel und Details

 max_size()
liefert die höchstmögliche Anzahl von Elementen, die ein Vektor aufnehmen kann. Dieser
Wert ist implementierungsabhängig. Da ein Vektor alle Elemente typischerweise in einem
Speicherblock verwaltet, kann es vor allem auf PCs relevante Einschränkungen geben. An-
sonsten ist es im Allgemeinen der maximale Wert, der als Index möglich ist.
 capacity()
liefert die Anzahl von Elementen, die ein Vektor aufnehmen kann, ohne neuen Speicher anzu-
fordern. Dieser Wert ist nicht nur für das Zeitverhalten wichtig, denn durch eine Anforderung
von neuem Speicher (eine so genannte Reallokierung) werden alle Verweise auf Elemente
ungültig. Das bedeutet konkret, dass alle Iteratoren und Referenzen (das, was der Indexope-
rator liefert) ungültig werden.
Das Anfordern von neuem Speicher kann aber auch viel Zeit kosten, da alle Elemente
vom alten in den neuen Speicher kopiert werden. Eine Reallokierung bedeutet konkret, dass
für alle Elemente jeweils ein Copy-Konstruktor und ein Destruktor aufgerufen wird. Der
Copy-Konstruktor kopiert sie in den neuen Speicher, und der Destruktor zerstört sie im alten
Speicher. Dies kann, falls diese Operationen nicht trivial sind, recht teuer werden.
Es ist natürlich sinnvoll, eine Anforderung von neuem Speicher nach Möglichkeit zu vermeiden.
Dazu gibt es mehrere Möglichkeiten:
 Zum einen kann ein Vektor beim Erzeugen gleich mit einer bestimmten Anzahl von Ele-
menten initialisiert werden. Diese Werte können auf verschiedene Art und Weise übergeben
werden.
Ohne Werte zur Initialisierung werden die Elemente mit dem Default-Konstruktor er-
zeugt:
std::vector<Elem> v(5); // erzeugt einen Vektor mit fünf Elementen
// (fünfmal Default-Konstruktor: Elem())
Dies bedeutet natürlich, dass ein Default-Konstruktor definiert sein muss. Und falls dieser
nicht trivial ist, kann auch das schon recht teuer sein.
 Eine andere Möglichkeit besteht darin, schon einmal Platz für eine bestimmte Anzahl von
Elementen zu reservieren. Dazu dient reserve():
std::vector<Elem> v; // erzeugt einen leeren Vektor
v.reserve(5); // Platz für fünf Elemente reservieren

Es ist implementierungsabhängig, auf welche Weise genau versucht wird, ein optimales Lauf-
zeitverhalten in Bezug auf Geschwindigkeit und Speicherplatz zu erhalten. Das bedeutet, dass
einzelne Implementierungen jederzeit die Kapazität in größeren Schritten erhöhen können.
Der Sprachstandard legt allerdings fest, dass die Kapazität von Vektoren nie verkleinert wird
(also auch mit reserve() nicht). Insofern ist sichergestellt, dass beim Löschen von Elementen
Verweise auf die davor liegenden Elemente gültig bleiben.

Zuweisungen
Tabelle 9.4 listet die verschiedenen Möglichkeiten auf, Vektoren komplett neue Elemente zuzu-
weisen.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 523

Ausdruck Bedeutung
v1 = v2 weist v1 alle Elemente von v2 zu
v.assign(n) weist n Elemente zu, die mit dem Default-Konstruktor erzeugt
werden
v.assign(n,elem) weist n Elemente zu, die jeweils Kopien von elem sind
v.assign(anf,end) weist Kopien der Elemente aus dem Bereich [anf;end) zu
v1.swap(v2) vertauscht die Elemente von v1 mit denen von v2
swap(v1,v2) ” (als globale Funktion)

Tabelle 9.4: Zuweisungsoperationen von Vektoren

Der Zuweisungsoperator weist alle Elemente eines Vektors einem anderen zu. Dabei werden, je
nach Situation und Implementierung, für die Elemente Zuweisungsoperatoren, Copy-Konstruk-
toren (falls die Menge größer wird) und Destruktoren (falls die Menge kleiner wird) aufgerufen.
Die Varianten der assign()-Funktionen entsprechen den Möglichkeiten der Konstruktoren.
Speziell die Zuweisung mit einem Bereich kann vor allem dazu verwendet werden, einem Vektor
Elemente aus einem anderen Container zuzuweisen:2
std::list<Elem> l;
std::vector<Elem> v;
...
v.assign(l.begin(),l.end());
Für das Laufzeitverhalten sehr interessant ist die Möglichkeit der Zuweisung durch Vertauschen
mit Hilfe von swap(). Diese Operation ist die einzige Zuweisungsform, die für die Elemente
keine Operationen aufruft. Sie hat deshalb ein ausgezeichnetes (weil konstantes) Zeitverhalten.
Wenn das zugewiesene Objekt anschließend nicht mehr gebraucht wird, sollte deshalb immer
swap() verwendet werden.
Elementzugriff
Zum direkten Zugriff auf die Elemente eines Vektors existieren die in Tabelle 9.5 dargestellten
Möglichkeiten.

Ausdruck Bedeutung
v.at(idx) liefert das Element mit Index idx (mit Bereichsüberprüfung)
v[idx] liefert das Element mit Index idx (ohne Bereichsüberprüfung)
v.front() liefert das erste Element (ohne Überprüfung, ob ein Element existiert)
v.back() liefert das letzte Element (ohne Überprüfung, ob ein Element existiert)

Tabelle 9.5: Direkter Elementzugriff bei Vektoren

2
Sofern ein System noch keine Elementfunktionstemplates unterstützt, kann der Bereich typischerweise
nur vom gleichen Typ sein.
Sandini Bib

524 Kapitel 9: Weitere Sprachmittel und Details

Zunächst ist zu beachten, dass bis auf at() keine Überprüfung stattfindet, ob ein entsprechendes
Element überhaupt existiert. Bei einem leeren Vektor wäre der Aufruf von Operator [], front()
und back() also undefiniert:
std::vector<Elem> v; // leer!

v[5] = elem; // LAUFZEITFEHLER => undefiniertes Verhalten


std::cout << v.front(); // LAUFZEITFEHLER => undefiniertes Verhalten
Das bedeutet, dass der Programmierer sicherstellen muss, dass der Index beim Indexoperator
korrekt ist bzw. der Container bei front() und back() nicht leer ist:
std::vector<Elem> v; // leer!

if (v.size() > 5) {
v[5] = elem; // OK
}
if (!v.empty()) {
std::cout << v.front(); // OK
}
v.at(5) = elem; // FEHLER => Ausnahme std::out_of_range
at() prüft dagegen Bereichsgrenzen. Findet eine Bereichsverletzung statt, wird eine Ausnahme
vom Typ std::out_of_range (siehe Abschnitt 3.6.3) generiert.

Verwendung der Elemente als Feld (Array)


Der Standard garantiert, dass man alle Elemente eines Vektors als Feld (Array) verwenden kann.
Damit kann man auch immer dann einen Vektor verwenden, wenn man einer Schnittstelle ein
Feld übergeben muss.
Braucht eine C-Funktion z.B. ein Feld von Windows, kann man dieses über einen Vektor
verwalten:
void raise (Window* fenster, int anzahl);

std::vector<Window> fenster;
...

// Elemente als Feld übergeben


raise (&fenster[0], fenster.size());
Die Adresse eines Elements kann also immer als Feld verwendet werden. Damit besteht im
Normalfall keine Notwendigkeit mehr, (dynamische) Felder selbst zu implementieren.

Iterator-Funktionen
Zum indirekten Zugriff auf die Elemente des Vektors existieren die üblichen Iterator-Funktionen,
die in Tabelle 9.6 dargestellt sind.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 525

Ausdruck Bedeutung
v.begin() liefert einen Iterator für das erste Element
v.end() liefert einen Iterator für die Position hinter dem letzten Element
v.rbegin() liefert einen Reverse-Iterator für das erste Element eines umgekehrten
Durchlaufs (also das letzte Element)
v.rend() liefert einen Reverse-Iterator für die Position hinter dem letzten
Element eines umgekehrten Durchlaufs (also die Position vor dem
ersten Element)

Tabelle 9.6: Iterator-Funktionen von Vektoren

Die Iteratoren bleiben so lange gültig, bis ein Element mit kleinerem Index eingefügt oder
gelöscht oder die Kapazität durch das Einfügen von Elementen oder einen Aufruf von reserve()
erhöht wird.

Einfügen und löschen


Tabelle 9.7 zeigt die Elementfunktionen, die bei Vektoren zum Einfügen und Löschen von Ele-
menten dienen.

Ausdruck Bedeutung
v.insert(pos) fügt an der Iterator-Position pos ein Element ein, das mit
dem Default-Konstruktor erzeugt wird, und liefert die
Position des neuen Elements
v.insert(pos,elem) fügt an der Iterator-Position pos eine Kopie von elem ein
und liefert die Position des neuen Elements
v.insert(pos,anf,end) fügt an der Iterator-Position pos Kopien der Elemente des
Bereichs [anf;end) ein
v.push_back(elem) hängt eine Kopie von elem an das Ende an
v.erase(pos) löscht das Element an der Iterator-Position pos und liefert
die Position des Folgeelements
v.erase(anf,end) löscht alle Elemente des Teilbereichs [anf;end) und liefert
die Position des Folgeelements
v.pop_back() löscht das letzte Element (ohne es zurückzuliefern)
v.resize(anz) ändert die Anzahl der Elemente auf anz, wobei bei einer
Vergrößerung neue Elemente mit dem Default-Konstruktor
erzeugt werden
v.resize(anz,elem) ändert die Anzahl der Elemente auf anz, wobei bei einer
Vergrößerung neue Elemente als Kopien von elem erzeugt
werden
v.clear() löscht alle Elemente (leert den Container)

Tabelle 9.7: Modifizierende Operationen von Vektoren


Sandini Bib

526 Kapitel 9: Weitere Sprachmittel und Details

Bei all diesen Elementfunktionen ist zunächst zu beachten, dass keine Überprüfung auf fehler-
hafte Parameter stattfindet. Der Programmierer muss sicherstellen, dass Iteratoren wirklich auf
Elemente des Vektors verweisen, dass der Anfang eines Bereichs vor dem Ende liegt und dass
nicht versucht wird, ein Element aus einer leeren Menge zu löschen.
Für das Laufzeitverhalten ist zu beachten, dass das Einfügen oder Löschen schneller erfolgt,
 wenn vorher die Kapazität ausreichend groß definiert wurde,
 wenn mehrere Elemente mit nur einer Operation eingefügt werden und
 wenn Elemente am Ende eingefügt und gelöscht werden.
Durch das Einfügen und Löschen von Elementen werden Verweise auf nachfolgende Elemente
(Iteratoren und Referenzen) undefiniert. Wird dabei neuer Speicherplatz angefordert, weil die
vorherige Kapazität überschritten wurde, sind sogar alle Verweise undefiniert.
Das Löschen von Elementen mit einem bestimmten Wert wird nicht direkt unterstützt. Statt-
dessen gibt es die Möglichkeit, Algorithmen wie folgt einzusetzen:
std::vector<Elem> v;
...
// alle Elemente mit Wert elem löschen
v.erase (std::remove(v.begin(),v.end(),elem),
v.end());
Dabei entfernt std::remove() die Elemente logisch“ und liefert das neue Ende zurück.

erase() entfernt dann die Elemente physikalisch“.

Soll nur das erste Element mit einem bestimmten Wert gelöscht werden, kann die Element-
funktion nicht verwendet werden. Eine mögliche Alternative sieht wie folgt aus:
std::vector<Elem> v;
...
// erstes Element mit Wert elem löschen
std::vector<Elem>::iterator pos;
pos = std::find (v.begin(), v.end(), // Position des Elements finden
elem);
if (pos != v.end()) { // falls gefunden, Element löschen
v.erase(pos);
}

9.1.2 Gemeinsame Operationen aller STL-Container


Einige, aber nicht alle Vektor-Operationen stehen als Schnittstelle zu allen STL-Containern zur
Verfügung. Tabelle 9.8 listet alle diese Operationen auf.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 527

Ausdruck Bedeutung
ContTyp<typ> c erzeugt einen leeren Container ohne Elemente
ContTyp<typ> c1(c2) erzeugt einen Container als Kopie eines anderen
ContTyp<typ> c(anf,end) erzeugt einen Container und initialisiert ihn mit Kopien der
Elemente aus dem Bereich [anf;end)
c.~ ContTyp<typ>() löscht alle Elemente und gibt den Speicherplatz frei
c.size() liefert die aktuelle Anzahl von Elementen
c.empty() liefert, ob der Container leer ist
(entspricht: size()==0, kann aber schneller sein)
c.max_size() liefert die maximal mögliche Anzahl von Elementen
c1 == c2 liefert, ob c1 gleich c2 ist
c1 != c2 liefert, ob c1 ungleich c2 ist
(entspricht: !(c1==c2))
c1 < c2 liefert, ob c1 kleiner als c2 ist
c1 > c2 liefert, ob c1 größer als c2 ist
(entspricht: c2<c1)
c1 <= c2 liefert, ob c1 kleiner oder gleich c2 ist
(entspricht: !(c2<c1))
c1 >= c2 liefert, ob c1 größer oder gleich c2 ist
(entspricht: !(c1<c2))
c1 = c2 weist v1 alle Elemente von v2 zu
c1.swap(c2) vertauscht die Elemente von c1 mit denen von c2
swap(c1,c2) ” (als globale Funktion)
c.begin() liefert einen Iterator für das erste Element
c.end() liefert einen Iterator für die Position hinter dem letzten
Element
c.rbegin() liefert einen Reverse-Iterator für das erste Element eines
umgekehrten Durchlaufs (also das letzte Element)
c.rend() liefert einen Reverse-Iterator für die Position hinter dem
letzten Element eines umgekehrten Durchlaufs (also die
Position vor dem ersten Element)
c.insert(pos,elem) fügt eine Kopie von elem ein
(der Rückgabewert und die Bedeutung von pos sind
unterschiedlich)
c.erase(pos) löscht das Element an der Iterator-Position pos
c.erase(anf,end) löscht alle Elemente des Teilbereichs [anf;end) und liefert
die Position des Folgeelements
c.clear() löscht alle Elemente (leert den Container)

Tabelle 9.8: Operationen, die alle STL-Container besitzen


Sandini Bib

528 Kapitel 9: Weitere Sprachmittel und Details

9.1.3 Liste aller STL-Algorithmen


In diesem Abschnitt werden alle Algorithmen der STL kurz aufgelistet, damit man einen gewis-
sen Überblick über die Bandbreite dieser Funktionen erhält.
Allen Algorithmen ist gemeinsam, dass sie immer einen durch Iteratoren eingegrenzten Be-
reich in irgendeiner Form bearbeiten. Diese Bearbeitung kann lesend und oder schreibend erfol-
gen. Die Algorithmen können dennoch in verschiedene Gruppen aufgeteilt werden: nicht-modi-
fizierend, modifizierend, sortierend und so weiter.
Dabei ist zu beachten, dass die Kriterien für die Suche nach Elementen, für Größenvergleiche,
für die Verwendung von Elementen und sogar für die Arbeitsweise der Algorithmen durch Funk-
tionen und Funktionsobjekte erheblich variiert werden können (siehe Abschnitt 9.2.2). Einige
Beispiele:
 Für Algorithmen, die Elemente bearbeiten, können Bedingungen für die Bearbeitung mitge-
geben werden (zum Beispiel alle geraden Elemente löschen oder das siebte Element mit dem
Wert 3 finden).
 Bei numerischen Algorithmen kann die numerische Operation definiert werden. Dadurch
kann accumulate(), das Elemente aufsummiert, auch dazu dienen, Elemente auszumul-
tiplizieren.
Einige dieser Algorithmen besitzen spezielle Versionen mit der zusätzlichen Endung _if. Diese
wird verwendet, wenn eine Variante des ursprünglichen Algorithmus für Funktionen oder Funkti-
onsobjekte existiert, ohne dass dadurch ein zusätzlicher Parameter notwendig wird. Das bedeutet
nicht, dass Varianten von Algorithmen für Funktionen und Funktionsobjekte immer die Endung
_if besitzen. Sofern diese als zusätzlicher Parameter hinzukommen, wurde der ursprüngliche
Name beibehalten.
Eine andere häufig wiederkehrende Endung im Namen ist _copy. Sie kennzeichnet Algorith-
men, die bei einer Verarbeitung einen Quell- und einen Zielbereich besitzen. Dabei werden die
Elemente des Quellbereichs analog zum ohne _copy gekennzeichneten Algorithmus verarbeitet
und in den Zielbereich kopiert.

Nicht-modifizierende Algorithmen
Die nicht-modifizierenden Algorithmen werden in Tabelle 9.9 aufgelistet. Sie ändern weder
die Reihenfolge noch den Wert der Elemente, für die sie aufgerufen werden. Die Algorithmen
können bei allen Containern angewendet werden.
Am wichtigsten ist sicherlich der Algorithmus for_each(). Er erlaubt es, beliebige, auch
sehr komplexe Aktionen mit den Elementen durchzuführen. Ein Beispiel für seine Verwendung
befindet sich auf Seite 541.

Modifizierende Algorithmen
Modifizierende Algorithmen verändern den Wert von Elementen. Dies kann für den bearbeiteten
Bereich oder einen Zielbereich des Algorithmus gelten. Solange es nur für einen Zielbereich gilt
und dieser nicht gleich dem Quellbereich ist, wird der Quellbereich nicht verändert. Tabelle 9.10
listet die modifizierenden Algorithmen auf.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 529

Name Funktionalität
for_each() ruft für jedes Element eine Operation auf
count() zählt die Elemente
count_if() zählt die Elemente, die ein Kriterium erfüllen
min_element() liefert die Position des kleinsten Elements
max_element() liefert die Position des größten Elements
find() sucht bestimmtes Element
find_if() sucht bestimmtes Element, das ein Kriterium erfüllt
search_n() sucht n aufeinander folgende bestimmte Werte
search() sucht die erste Teilfolge von bestimmten Werten
find_end() sucht die letzte Teilfolge von bestimmten Werten
find_first_of() sucht eines von mehreren möglichen Elementen
adjacent_find() sucht zwei benachbarte Elemente mit bestimmten
Eigenschaften
equal() testet zwei Bereiche auf Gleichheit
mismatch() liefert die beiden ersten unterschiedlichen Elemente
zweier Bereiche
lexicographical_compare() vergleicht zwei Bereiche zum Sortieren

Tabelle 9.9: Nicht-modifizierende Algorithmen

Name Funktionalität
for_each() ruft für jedes Element eine Operation auf
copy() kopiert einen Bereich
copy_backward() kopiert einen Bereich angefangen von hinten
transform() modifiziert die Elemente eines oder zweier Bereiche
merge() fasst die Elemente zweier Bereiche zusammen
swap_ranges() vertauscht die Elemente zweier Bereiche
fill() gibt allen Elementen einen festen Wert
fill_n() gibt n Elementen einen festen Wert
generate() gibt allen Elementen einen jeweils generierten Wert
generate_n() gibt n Elementen einen jeweils generierten Wert
replace() ersetzt bestimmte Werte durch einen festen neuen Wert
replace_if() ersetzt Werte, die ein Kriterium erfüllen, durch einen
festen neuen Wert
replace_copy() kopiert einen Bereich und ersetzt dabei Elemente
replace_copy() kopiert einen Bereich und ersetzt dabei Elemente, die
ein Kriterium erfüllen

Tabelle 9.10: Modifizierende Algorithmen

Da die Elemente von assoziativen Containern als Konstanten betrachtet werden, können diese
Algorithmen nicht verwendet werden, wenn der manipulierte Bereich bzw. der Zielbereich in
einem assoziativen Container liegt.
Sandini Bib

530 Kapitel 9: Weitere Sprachmittel und Details

Löschende Algorithmen
Löschende Algorithmen entfernen Elemente aus einem Bereich. Dies betrifft entweder den be-
arbeiteten Bereich oder einen Zielbereich, in den die nicht entfernten Elemente kopiert werden.
Solange es einen Zielbereich gibt und dieser nicht gleich dem Quellbereich ist, wird der Quell-
bereich nicht verändert. Tabelle 9.11 listet die löschenden Algorithmen auf.

Name Funktionalität
remove() löscht Elemente mit einem bestimmten Wert
remove_if() löscht Elemente, die ein Kriterium erfüllen
remove_copy() kopiert einen Bereich und löscht dabei Elemente mit
einem bestimmten Wert
remove_copy_if() kopiert einen Bereich und löscht dabei Elemente, die
ein Kriterium erfüllen
unique() löscht aufeinander folgende Duplikate
unique_copy() kopiert und löscht dabei aufeinander folgende
Duplikate

Tabelle 9.11: Löschende Algorithmen

Da die Elemente von assoziativen Containern als Konstanten betrachtet werden, können diese
Algorithmen nicht verwendet werden, wenn der manipulierte Bereich bzw. der Zielbereich in
einem assoziativen Container liegt.
Bei diesen Algorithmen ist zu beachten, dass die bearbeiteten Bereiche nicht kleiner werden.
Die Elemente werden nur entsprechend umsortiert, und das neue Ende wird zurückgeliefert.
Die gelöschten Elemente müssen daher unter Umständen noch wirklich entfernt werden (siehe
Seite 526).

Mutierende Algorithmen
Mutierende Algorithmen verändern die Reihenfolge von Elementen, ohne deren Wert zu ändern.
Dies bedeutet aber nicht, dass keine Zuweisungen stattfinden. Im Gegenteil, bei jeder Umsortie-
rung werden Elemente einander zugewiesen, da die Speicherplätze erhalten bleiben. Tabelle 9.12
listet die mutierenden Algorithmen auf.
Da die Elemente von assoziativen Containern als Konstanten betrachtet werden, können auch
diese Algorithmen nicht verwendet werden, wenn der manipulierte Bereich bzw. der Zielbereich
in einem assoziativen Container liegt.

Sortierende Algorithmen
Sortierende Algorithmen verändern die Reihenfolge von Elementen so, dass für die weitere Ver-
arbeitung eine zumindest teilweise Sortierung vorliegt. Das Sortierkriterium kann dabei jeweils
als Funktionsobjekt übergeben werden. Als Default-Sortierkriterium werden die Werte der Ele-
mente mit dem Operator < verglichen. Tabelle 9.13 listet die sortierenden Algorithmen auf.
Da die Elemente von assoziativen Containern als Konstanten betrachtet werden, können diese
Algorithmen nicht verwendet werden, wenn der manipulierte Bereich bzw. der Zielbereich in
einem assoziativen Container liegt.
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 531

Name Funktionalität
reverse() kehrt die Reihenfolge von Elementen um
reverse_copy() kopiert und kehrt die Reihenfolge der Elemente um
rotate() rotiert die Elemente
rotate_copy() kopiert und rotiert dabei die Elemente
next_permutation() permutiert die Reihenfolge in eine Richtung
prev_permutation() permutiert die Reihenfolge in die andere Richtung
random_shuffle() bringt die Reihenfolge der Elemente durcheinander
partition() verschiebt bestimmte Elemente nach vorn
stable_partition() verschiebt bestimmte Elemente nach vorn und behält dabei
relative Reihenfolgen bei

Tabelle 9.12: Mutierende Algorithmen

Name Funktionalität
sort() sortiert Elemente
stable_sort() sortiert Elemente unter Beibehaltung der Reihenfolge von
Elementen mit gleichem Wert
partial_sort() sortiert die ersten n Elemente
partial_sort_copy() kopiert die ersten n sortierten Elemente
nth_element() sortiert in Hinsicht auf ein bestimmtes Element
make_heap() macht einen Bereich zu einem Heap
push_heap() integriert ein Element in einen Heap
pop_heap() löst ein Element aus einem Heap
sort_heap() sortiert einen Heap (damit ist er kein Heap mehr)

Tabelle 9.13: Sortierende Algorithmen

Neben den traditionellen kompletten Sortierungen gibt es verschiedene Algorithmen, die nur teil-
weise sortieren. Da das Sortieren in der Regel aufwändig ist, lohnt es sich häufig, die Sortierung
nicht zu vollenden. Wenn z.B. das mittlere Element gesucht oder die Grenze zwischen der ersten
und der zweiten Hälfte einer Sortierung gebraucht wird, kann dies auch mit nth_element()
berechnet werden, ohne alle Elemente komplett zu sortieren.

Algorithmen für sortierte Bereiche


Sofern ein oder mehrere Bereiche vollständig sortiert sind, können die hier aufgeführten Algo-
rithmen verwendet werden. Es sind sowohl nicht-manipulierende als auch manipulierende Algo-
rithmen. Tabelle 9.14 listet die Algorithmen für sortierte Bereiche auf.
Der Vorteil dieser Algorithmen entspricht dem Vorteil von assoziativen Containern gegenüber
sequenziellen Containern: Sie weisen ein erheblich besseres Zeitverhalten auf. Sofern Ergebnis-
mengen oder Zielbereiche betroffen sind, sind diese ebenfalls wieder sortiert.
Sandini Bib

532 Kapitel 9: Weitere Sprachmittel und Details

Name Funktionalität
binary_search() liefert, ob ein Element enthalten ist
includes() liefert, ob alle Elemente einer Teilmenge enthalten sind
lower_bound() liefert das erste Element >= einem Wert
upper_bound() liefert das erste Element > einem Wert
equal_range() liefert einen Bereich von Elementen mit einem
bestimmten Wert
merge() fasst die Elemente zweier Bereiche zusammen
set_union() bildet die Vereinigungsmenge zweier Bereiche
set_intersection() bildet die Schnittmenge zweier Bereiche
set_difference() bildet die Differenzmenge zweier Bereiche
set_symmetric_difference() bildet die Komplementärmenge zweier Bereiche
inplace_merge() verschmilzt zwei direkt hintereinander liegende
Teilbereiche

Tabelle 9.14: Algorithmen für sortierte Bereiche

Numerische Algorithmen
Eine besondere Form von Algorithmen bilden die numerischen Algorithmen. Sie werden in Ta-
belle 9.15 aufgelistet.

Name Funktionalität
accumulate() verknüpft alle Elemente
partial_sum() verknüpft ein Element jeweils mit allen Vorgängern
adjacent_difference() verknüpft jeweils ein Element mit seinem Vorgänger
inner_product() verknüpft alle Verknüpfungen von jeweils zwei Elementen
zweier Bereiche

Tabelle 9.15: Numerische Algorithmen

Numerische Algorithmen dienen dazu, die Elemente eines Bereichs in verschiedener Form nu-
merisch miteinander zu verknüpfen. Dabei sollte man sich vom Namen nicht zu sehr festlegen
lassen. accumulate() bildet zwar als Default die Summe aller Elemente, kann aber durch ein
einfaches Umdefinieren der Verknüpfung auch das Produkt der Elemente bilden. Entsprechend
bildet adjacent_difference() zwar als Default jeweils die Differenz eines Elements zu sei-
nem Vorgänger, durch ein Umdefinieren der Verknüpfung kann aber auch die Summe, das Pro-
dukt oder jede andere denkbare Verknüpfung berechnet werden.
adjacent_difference() und inner_product() dienen auch dazu, eine Folge von abso-
luten Werten in eine Folge von relativen Werten und umgekehrt umzuwandeln.
Die Algorithmen accumulate() und inner_product() liefern als Ergebnis einen Wert und
sind insofern nicht-manipulierend. Die beiden anderen Algorithmen liefern mehrere Werte, die
in einen Zielbereich geschrieben werden, und sind insofern für den Zielbereich manipulierend.
Für weitere Details zu den Algorithmen sei insbesondere auf mein Standardwerk, The C++

Standard Library – A Tutorial and Reference“ verwiesen (siehe Literaturliste).
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 533

9.1.4 Numerische Limits


Die wichtigsten Implementierungsdetails zu den fundamentalen numerischen Datentypen kön-
nen mit Hilfe des Klassentemplates numeric_limits<> abgefragt werden. Im Einzelnen lie-
gen diese numerischen Limits (englisch: numeric limits) für folgende Datentypen vor: bool,
char, signed char, unsigned char, wchar_t, short, unsigned short, int, unsigned
int , long, unsigned long, float, double und long double. Diese numerischen Limits
werden in der Headerdatei <limits> definiert und ersetzen damit die in den C-Headerdateien
<climits> bzw. <limits.h> und <cfloat> bzw. <float.h> definierten Präprozessor-Kon-
stanten.
Für die Größendefinitionen wird das Klassentemplate numeric_limits<> verwendet. Es
enthält statische Komponenten, die verschiedene numerische Eigenschaften der Datentypen defi-
nieren. Tabelle 9.16 listet die einzelnen Komponenten auf. Dabei sind gegebenenfalls vorhandene
entsprechende C-Konstanten mit aufgeführt.

Komponente Bedeutung C-Konstanten


is_specialized der Datentyp besitzt eine Spezialisierung für numeri-
sche Limits
is_signed ist vorzeichenbehaftet
is_integer ist ganzzahlig
is_exact es gibt keine Rundungsfehler
(true bei allen ganzzahligen Datentypen)
is_bounded hat endliche Wertemenge
(true bei allen eingebauten Datentypen)
is_modulo Addition zweier positiver Werte kann negativ sein
is_iec559 ist konform zu den Standards IEC 559 und IEEE 754
min() minimaler Wert des Datentyps INT_MIN,FLT_MIN,
(hat Bedeutung, falls is_bounded||is_signed) CHAR_MIN,...
max() maximaler Wert des Datentyps INT_MAX,FLT_MAX,
(hat Bedeutung, falls is_bounded) CHAR_MAX,...
digits Zeichen, Integer: Anzahl der Bits (binäre Ziffern)
Gleitkommawert: Anzahl der Ziffern der Mantisse zur FLT_MANT_DIG,...
Basis radix
digits10 Anzahl der Ziffern im Zehnersystem FLT_DIG,...
(hat Bedeutung, falls is_bounded)
radix Integer: internes Zahlensystem (fast immer zwei)
Gleitkommawert: internes Zahlensystem der Mantisse FLT_RADIX
min_exponent kleinster negativer ganzzahliger Exponent zur Basis FLT_MIN_EXP,...
radix
max_exponent größter positiver ganzzahliger Exponent zur Basis FLT_MAX_EXP,...
radix

Tabelle 9.16: Die Komponenten der numeric_limits<>


Sandini Bib

534 Kapitel 9: Weitere Sprachmittel und Details

Komponente Bedeutung C-Konstanten


min_exponent10 kleinster negativer ganzzahliger Exponent zur Ba- FLT_MIN_10_EXP,...
sis zehn
max_exponent10 größter positiver ganzzahliger Exponent zur Basis FLT_MAX_10_EXP,...
zehn
epsilon() Differenz zwischen eins und dem kleinsten Wert FLT_EPSILON,...
größer als eins
round_style Rundungsstil
round_error() maximaler Rundungsfehler
has_infinity Datentyp hat Wert für unendlich“

infinity() Darstellung von unendlich“, falls vorhanden

has_quiet_NaN Datentyp hat nicht-signalauslösenden Wert für
Not a Number“ (z.B. für 00 )

quiet_NaN() Darstellung vom nicht-signalauslösenden Wert für
Not a Number“, falls vorhanden

has_signaling_NaN Datentyp hat signalauslösenden Wert für Not a

Number“ (z.B. für 00 )
signaling_NaN() Darstellung vom signalauslösenden Wert für Not

a Number“, falls vorhanden
has_denorm erlaubt denormalisierte Werte (variable Anzahl
von Bits für Exponenten)
has_denorm_loss ein Genauigkeitsverlust wird als Verlust von De-
normalisierung und nicht als unexaktes Ergebnis
gekennzeichnet
denorm_min() kleinster positiver denormalisierter Wert
traps trapping“ ist implementiert

tinyness_before tinyness“ wird vor dem Runden erkannt

Tabelle 9.17: Die Komponenten der numeric_limits<>, Fortsetzung

Interessant ist natürlich vor allem der Wertebereich von Datentypen. Das folgende Beispiel zeigt
eine mögliche Auswertung dieser Informationen und gibt die Maxima für alle möglichen Daten-
typen mit einigen weiteren Informationen aus:
// sonst/limits.cpp
#include <iostream>
#include <limits>
#include <string>

int main()
{
using namespace std;
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 535

// textuelle Darstellung für bool verwenden


cout << boolalpha;

// Maxima der ganzzahligen Datentypen ausgeben


cout << "max(short): " << numeric_limits<short>::max() << endl;
cout << "max(int): " << numeric_limits<int>::max() << endl;
cout << "max(long): " << numeric_limits<long>::max() << endl;
cout << endl;

// Maxima der Gleitkommadatentypen ausgeben


cout << "max(float): "
<< numeric_limits<float>::max() << endl;
cout << "max(double): "
<< numeric_limits<double>::max() << endl;
cout << "max(long double): "
<< numeric_limits<long double>::max() << endl;
cout << endl;

// Ist char vorzeichenbehaftet?


cout << "is_signed(char): "
<< numeric_limits<char>::is_signed << endl;
cout << endl;

// Existieren numerische Limits zum Datentyp string?


cout << "is_specialized(string): "
<< numeric_limits<string>::is_specialized << endl;
}
Eine (natürlich implementierungsabhängige) Ausgabe könnte z.B. wie folgt aussehen:
max(short): 32767
max(int): 2147483647
max(long): 2147483647

max(float): 3.40282e+38
max(double): 1.79769e+308
max(long double): 1.79769e+308

is_signed(char): true

is_specialized(string): false
Sandini Bib

536 Kapitel 9: Weitere Sprachmittel und Details

Die letzte Ausgabe besagt, dass es zum Datentyp std::string keine derartigen Angaben gibt.
Das macht Sinn, da ein String kein numerischer Datentyp ist. Trotzdem ist diese Information,
wie man sieht, für alle Typen abfragbar. Das gilt sogar für selbst definierte Datentypen. Dies ist
durch eine Programmiertechnik in Verbindung mit Templates möglich: Für alle Datentypen wird
eine allgemeine Default-Implementierung definiert:
namespace std {
// allgemeine Definition des Templates für numerische Limits
template <typename T>
class numeric_limits {
public:
// im Allgemeinen existieren keine numerischen Limits
static const bool is_specialized = false;

... // alle weiteren Komponenten mit unbedeutenden Werten


};
}
Für die Datentypen, zu denen numerische Limits definiert sind, gibt es jeweils eine Spezialisie-
rung des Templates (siehe Abschnitt 7.3.3):
namespace std {
// Spezialisierung der numerischen Limits für int
// - Werte sind implementierungsspezifisch
template<> class numeric_limits<int> {
public:
// in diesem Fall existieren die numerischen Limits
static const bool is_specialized = true;

static T min() throw() {


return -2147483648;
}
static T max() throw() {
return 2147483647;
}
static const int digits = 31;
...
};
}
Der folgende Code zeigt eine mögliche komplette Spezialisierung der numerischen Limits für
den Datentyp float. Damit werden auch die exakten Signaturen der Komponenten gezeigt:
Sandini Bib

9.1 Weitere Details zur Standardbibliothek 537

namespace std {
template<> class numeric_limits<float> {
public:
static const bool is_specialized = true;

static float min() throw() { return 1.17549435E-38F; }


static float max() throw() { return 3.40282347E+38F; }

static const int digits = 24;


static const int digits10 = 6;
static const int radix = 2;

static const bool is_signed = true;


static const bool is_integer = false;
static const bool is_exact = false;
static const bool is_bounded = true;
static const bool is_modulo = false;
static const bool is_iec559 = true;

static float epsilon() throw() { return 1.19209290E-07F; }

static const float_round_style round_style = round_to_nearest;


static float round_error() throw() { return 0.5F; }

static const int min_exponent = -125;


static const int max_exponent = +128;
static const int min_exponent10 = -37;
static const int max_exponent10 = +38;

static const bool has_infinity = true;


static float infinity() throw() { return ...; }
static const bool has_quiet_NaN = true;
static float quiet_NaN() throw() { return ...; }
static const bool has_signaling_NaN = true;
static float signaling_NaN() throw() { return ...; }
static const float_denorm_style has_denorm = denorm_absent;
static const bool has_denorm_loss = false;
static float denorm_min() throw() { return min(); }

static const bool traps = true;


static const bool tinyness_before = true;
};
}
Sandini Bib

538 Kapitel 9: Weitere Sprachmittel und Details

9.1.5 Zusammenfassung
 Vektoren besitzen eine Größe und eine Kapazität.
 Vektoren prüfen beim Elementzugriff normalerweise nicht, ob das Element überhaupt exis-
tiert.
 Ein Vektor kann (und sollte) einem (dynamischen) Feld vorgezogen werden.
 Mit numerischen Limits kann man die Eigenschaften numerischer Datentypen auswerten.
Sandini Bib

9.2 Definition besonderer Operatoren 539

9.2 Definition besonderer Operatoren


In diesem Abschnitt wird auf die Möglichkeit eingegangen, für Objekte Operatoren zu definieren,
so dass diese sich wie Zeiger bzw. wie Funktionen verhalten.

9.2.1 Smart-Pointer
In diesem Abschnitt werden zunächst Objekte vorgestellt, die sich wie schlaue Zeiger“ verhal-

ten.

Motivation für Smart-Pointer


In C++ ist es möglich, für Objekte die Operatoren * und -> zu definieren. Auf diese Weise kann
man Objekten eine zeigerartige Schnittstelle geben. Da man diese Operatoren selbst implemen-
tiert hat man es so in der Hand, Zeiger zu implementieren, die mitdenken“. Aus diesem Grund

nennt man derartige Objekte auch Smart-Pointer ( schlaue Zeiger“).

Eine Art von Smart-Pointer haben wir bereits in Zusammenhang mit der STL kennen ge-
lernt. Iteratoren sind Smart-Pointer, die in der Lage sind, mit einer einfachen Schnittstelle über
komplexe Datenstrukturen zu iterieren (siehe Seite 78).

Smart-Pointer für Referenz-Semantik


Eine ganz andere Art von Smart-Pointern wird immer wieder benötigt: Zeiger, die mit new an-
gelegte Objekte verwalten und diese Objekte automatisch zerstören, wenn der letzte Zeiger auf
diese Objekte zerstört wird. Auf diese Weise kann man Referenz-Semantik implementieren. Das
bedeutet, dass man beim Kopieren von Objekten nur Verweise auf diese Objekte kopiert, dass die
Daten von Original und Kopie aber gemeinsam verwendet werden. Auf diese Weise kann man
z.B. Objekte gleichzeitig in mehreren STL-Containern verwalten.
Das folgende Klassentemplate definiert einen Smart-Pointer mit Referenz-Semantik:
// sonst/countptr.hpp
#ifndef COUNTED_PTR_HPP
#define COUNTED_PTR_HPP

/* Klassentemplate für Smart-Pointer mit Referenz-Semantik


* - zerstört das Objekt, auf das verwiesen wird, wenn der letzte CountedPtr
* der darauf verweist, zerstört wird
*/
template <class T>
class CountedPtr {
private:
T* ptr; // Zeiger auf das eigentliche Objekt
long* count; // Verweis auf die Anzahl der darauf verweisenden Zeiger

public:
Sandini Bib

540 Kapitel 9: Weitere Sprachmittel und Details

// mit herkömmlichem Zeiger initialisieren


// - p muss ein von new zurückgelieferter Wert sein
explicit CountedPtr (T* p=0)
: ptr(p), count(new long(1)) {
}

// Copy-Konstruktor
CountedPtr (const CountedPtr<T>& p) throw()
: ptr(p.ptr), count(p.count) { // Objekt und Zähler übernehmen
++*count; // Anzahl der Verweise inkrementieren
}

// Destruktor
~CountedPtr () throw() {
freigabe(); // als letzter Verweis Objekt zerstören
}

// Zuweisung
CountedPtr<T>& operator= (const CountedPtr<T>& p) throw() {
if (this != &p) { // falls keine Zuweisung an sich selbst
freigabe(); // als letzten Verweis altes Objekt zerstören
ptr = p.ptr; // neues Objekt übernehmen
count = p.count; // Zähler vom neuen Objekt übernehmen
++*count; // Anzahl der Verweise inkrementieren
}
return *this;
}

// Zugriff auf das Objekt


T& operator*() const throw() {
return *ptr;
}

// Zugriff auf eine Komponente des Objekts


T* operator->() const throw() {
return ptr;
}

private:
void freigabe() {
if (--*count == 0) { // falls letzter Verweis
Sandini Bib

9.2 Definition besonderer Operatoren 541

delete count; // Zähler zerstören


delete ptr; // Objekt zerstören
}
}
};

#endif /*COUNTED_PTR_HPP*/
Man kann nun jedes beliebige Objekt mit new erzeugen und einem solchen Zeiger die Kontrolle
übergeben:
CountedPtr<Bsp::Bruch> bp = new Bsp::Bruch(16,100);
bp kann man nun beliebig kopieren und zuweisen. Dabei werden jeweils nur weitere Verweise
auf das Objekt erzeugt.
Man beachte, dass sowohl der Operator * als auch der Operator -> implementiert werden.
Man kann also mit beiden Operatoren auf das Objekt zugreifen:
std::cout << *bp << std::endl;
...
double d = bp->toDouble();
Implementiert man den Operator -> für eigene Klassen, muss man dabei etwas zurückliefern,
für das dann erneut der Operator -> aufgerufen wird. Wird wie hier für ein Objekt vom Typ
CountedPtr<Bsp::Bruch> der Operator -> aufgerufen, liefert dieser den Typ Bsp::Bruch*,
für den dann erneut der Operator -> aufgerufen wird.
Das folgende Programm zeigt, wie man diese Klasse dazu verwenden kann, Objekte gleich-
zeitig in mehreren STL-Containern zu verwalten:
// sonst/refsem1.cpp
#include <iostream>
#include <list>
#include <deque>
#include <algorithm>
#include "countptr.hpp"
using namespace std;

void printCountedPtr (CountedPtr<int> elem)


{
cout << *elem << ' ';
}

int main()
{
// Datentyp für Smart-Pointer dazu
typedef CountedPtr<int> IntPtr;
Sandini Bib

542 Kapitel 9: Weitere Sprachmittel und Details

// zwei verschiedene Mengen


deque<IntPtr> coll1;
list<IntPtr> coll2;

// Array von initialen ints


static int values[] = { 3, 5, 9, 1, 6, 4 };

/* neu erzeugte ints in die Mengen mit Referenz-Semantik einfügen


* - gleiche Reihenfolge in coll1
* - umgekehrte Reihenfolge in coll2
*/
for (unsigned i=0; i<sizeof(values)/sizeof(values[0]); ++i) {
IntPtr ptr(new int(values[i]));
coll1.push_back(ptr);
coll2.push_front(ptr);
}

// Inhalt beider Mengen ausgeben


for_each (coll1.begin(), coll1.end(),
printCountedPtr);
cout << endl;
for_each (coll2.begin(), coll2.end(),
printCountedPtr);
cout << endl << endl;

/* Elemente der Mengen an verschiedenen Stellen modifizieren


* - dritten Wert in coll1 quadrieren
* - ersten Wert in coll1 negieren
* - ersten Wert in coll2 auf 0 setzen
*/
*coll1[2] *= *coll1[2];
(**coll1.begin()) *= -1;
(**coll2.begin()) = 0;

// Inhalt beider Mengen erneut ausgeben


for_each (coll1.begin(), coll1.end(),
printCountedPtr);
cout << endl;
for_each (coll2.begin(), coll2.end(),
printCountedPtr);
cout << endl;
}
Sandini Bib

9.2 Definition besonderer Operatoren 543

Das Programm hat folgende Ausgabe:


3 5 9 1 6 4
4 6 1 9 5 3

-3 5 81 1 6 0
0 6 1 81 5 -3
Im Boost-Repository für ergänzende C++-Bibliotheken befinden sich mehrere Smart-Pointer-
Klassen für unterschiedliche Zwecke (siehe http://www.boost.org).

9.2.2 Funktionsobjekte
Es ist sogar auch möglich und sinnvoll, Objekte zu definieren, die sich wie Funktionen verhal-
ten. In diesem Fall wird für die Objekte der Operator ()“ definiert. Diese sicherlich mysteriös

klingende Technik soll zunächst einmal an einem Beispiel motiviert werden.
Man kann den STL-Algorithmen zur Verarbeitung eine Operation übergeben. Dies kann z.B.
eine Funktion sein. Das folgende Beispiel verdeutlicht dies:
// sonst/transform1.cpp
#include <set>
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>

int add10 (int elem)


{
return elem + 10;
}

int main()
{
std::set<int> coll1;
std::vector<int> coll2;

// Elemente mit den Werten 1 bis 9 in coll1 einfügen


for (int i=1; i<=9; ++i) {
coll1.insert(i);
}

// Elemente in coll1 ausgeben


copy (coll1.begin(), coll1.end(), // Quelle: coll1
std::ostream_iterator<int>(std::cout," ")); // Ziel: cout
Sandini Bib

544 Kapitel 9: Weitere Sprachmittel und Details

std::cout << std::endl;

// jedes Element in coll1 nach coll2 transformieren


// - dabei jeweils 10 addieren
transform (coll1.begin(),coll1.end(), // Quelle
std::back_inserter(coll2), // Ziel (einfügend)
add10); // Operation

// Elemente in coll2 ausgeben


copy (coll2.begin(), coll2.end(), // Quelle: coll1
std::ostream_iterator<int>(std::cout," ")); // Ziel: cout
std::cout << std::endl;

}
In diesem Beispiel wird durch den Aufruf
transform (coll1.begin(),coll1.end(), // Quelle
std::back_inserter(coll2), // Ziel (einfügend)
add10);
jedes Element aus coll1 einfügend nach coll2 transformiert, wobei durch den Aufruf der
Funktion add10() jeweils 10 addiert wird. Mit add10 wird dabei die Funktion als Operation
an transform() übergeben (siehe Abbildung 9.1).
Die Ausgabe des Programms lautet wie folgt:
1 2 3 4 5 6 7 8 9
11 12 13 14 15 16 17 18 19

c o l l 1 Ite ra to r t r a n s f o r m ( ) Ite ra to r c o l l 2

i n t a d d 1 0 ( i n t e l e m )
{
r e t u r n e l e m + 1 0 ;
}

Abbildung 9.1: Arbeitsweise von transform()

Was passiert aber nun, wenn man verschiedene Werte addieren will? Sofern diese Werte zur
Kompilierzeit feststehen, kann man dazu ein Template verwenden (vgl. Abschnitt 7.4):
Sandini Bib

9.2 Definition besonderer Operatoren 545

template <int W>


int add (int elem)
{
return elem + W;
}

int main()
{
...
transform (coll1.begin(),coll1.end(), // Quelle
std::back_inserter(coll2), // Ziel (einfügend)
add<10>); // Operation
...
}
Es ist allerdings derzeit nicht ganz klar, ob man ein Template einfach auf diese Weise als Opera-
tion übergeben kann.3 Unter Umständen muss man noch den genauen Datentyp der übergebenen
Operation spezifizieren:
transform (coll1.begin(),coll1.end(), // Quelle
std::back_inserter(coll2), // Ziel (einfügend)
(int(*)(int))add<10>); // Operation
Der Ausdruck
int(*)(int)
steht dabei für eine Funktion, die einen int bekommt und einen int zurückliefert. Durch
(int(*)(int))add<10>
wird explizit festgelegt, dass add<10> diesen Typ besitzt.
Doch was passiert, wenn man den zu addierenden Wert erst zur Laufzeit erfährt? Eigentlich
braucht man diesen Wert als zweiten Parameter, doch transform() ruft diese Operation nur mit
einem Argument, nämlich dem jeweiligen Element, auf. Damit müssen wir den zu addierenden
Wert irgendwie anders an add() übergeben.
Man könnte natürlich eine globale Variable verwenden, doch ist das sicherlich in mehrfacher
Hinsicht bedenklich.
An dieser Stelle helfen Funktionsobjekte. Dabei handelt es sich um Objekte, die sich wie
Funktionen verhalten, wie Objekte aber weitere Daten verwalten können. Wir können ein Objekt
erzeugen, dem wir den zu addierenden Wert übergeben, und dieses Objekt dann als Operation
transform() übergeben. Dies sieht wie folgt aus:

3
Der Standard ist in diesem Punkt nicht eindeutig und Compiler verhalten sich deshalb unterschiedlich.
Sandini Bib

546 Kapitel 9: Weitere Sprachmittel und Details

// sonst/transform2.cpp
#include <set>
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>

// Funktionsobjekt, das den übergebenen Wert addiert


class Add {
private:
int val; // zu addierender Wert
public:
// Konstruktor (initialisiert den zu addierenden Wert)
Add (int w) : val(w) {
}

// ‘‘Funktionsaufruf’’ addiert den zu addierenden Wert


int operator() (int elem) const
{
return elem + val;
}
};

int main()
{
std::set<int> coll1;
std::vector<int> coll2;

// Elemente mit den Werten 1 bis 9 in coll1 einfügen


for (int i=1; i<=9; ++i) {
coll1.insert(i);
}

// Elemente in coll1 ausgeben


copy (coll1.begin(), coll1.end(), // Quelle: coll1
std::ostream_iterator<int>(std::cout," ")); // Ziel: cout
std::cout << std::endl;

// jedes Element in coll1 nach coll2 transformieren


// - dabei jeweils 10 addieren
Sandini Bib

9.2 Definition besonderer Operatoren 547

transform (coll1.begin(),coll1.end(), // Quelle


std::back_inserter(coll2), // Ziel (einfügend)
Add(10)); // Operation

// Elemente in coll2 ausgeben


copy (coll2.begin(), coll2.end(), // Quelle: coll1
std::ostream_iterator<int>(std::cout," ")); // Ziel: cout
std::cout << std::endl;

}
Der Ausdruck
Add(10)
erzeugt ein temporäres Objekt der Klasse Add und initialisiert dieses mit 10. Durch
transform (coll1.begin(),coll1.end(), // Quelle
std::back_inserter(coll2), // Ziel (einfügend)
Add(10)); // Operation
wird ein derartiges temporäres Objekt als Operation op an transform() übergeben. Innerhalb
von transform() wird die übergebene Operation aufgerufen:
op (aktuellesElement)
Handelt es sich bei op um eine Funktion, wird diese also mit dem aktuellen Element aufgerufen.
Handelt es sich wie hier um ein Funktionsobjekt, entspricht dies dem Aufruf:
op.operator () (aktuellesElement)
Dieser Operator ist bei Objekten vom Typ Add so definiert, dass der bei der Initialisierung über-
gebene Wert addiert wird.

9.2.3 Zusammenfassung
 Man kann für Objekte die Operatoren * und -> definieren. Damit lassen sich Smart-Pointer
implementieren.
 Man kann für Objekte den Operator () definieren. Damit lassen sich Funktionsobjekte im-
plementieren.
Sandini Bib

548 Kapitel 9: Weitere Sprachmittel und Details

9.3 Weitere Aspekte von new und delete


Zu der in Abschnitt 3.8 eingeführten Freispeicherverwaltung mit den Operatoren new, new[ ],
delete und delete[ ] gibt es noch einige spezielle Aspekte, die in diesem Abschnitt vorgestellt
werden.

9.3.1 Nothrow-Versionen von new und delete


Mann kann die Operatoren new, new[ ], delete und delete[ ] auch so aufrufen, dass sie keine
Ausnahme auslösen, sondern bei zu wenig Speicherplatz 0 bzw. NULL zurückliefern.
Dazu muss new als Parameter der Wert std::nothrow übergeben werden:
Bsp::Bruch* bp = new(std::nothrow) Bsp::Bruch(16,100);
if (bp == NULL) {
std::cerr << "Speicherplatz alle" << std::endl;
...
}

Bsp::Bruch* bruchfeld = new(std::nothrow) Bsp::Bruch[10];


if (bruchfeld == NULL) {
std::cerr << "Speicherplatz alle" << std::endl;
...
}
Derartiger Speicherplatz muss ebenfalls mit delete und delete[ ] freigegeben werden:
delete bp;
delete [] bruchfeld;

9.3.2 Placement-New
Übergibt man new als Parameter eine Adresse vom Datentyp void*, so wird dies als Aufruf von
new verstanden, bei dem der Speicherplatz bereits zur Verfügung steht. Damit kann man Objekte
in bereits vorhandenem Speicherplatz initialisieren. Das Erzeugen eines Objekts mit new
Bsp::Bruch* bp = new Bsp::Bruch(16,100);
kann man also wie folgt in zwei Schritten durchführen:
// uninitialisierten Speicherplatz anfordern:
void* p = ::operator new(sizeof(Bsp::Bruch));

// Speicherplatz als Objekt initialisieren:


Bsp::Bruch* bp = new(p) Bsp::Bruch(16,100);
Umgekehrt kann man auch das Zerstören und Freigeben von Objekten in zwei einzelne Schritte
aufteilen:
Sandini Bib

9.3 Weitere Aspekte von new und delete 549

// Objekt aufräumen:
bp->~Bsp::Bruch();

// Speicherplatz freigeben:
::operator delete((void*)bp);

9.3.3 New-Handler
Schlägt das Anfordern von Speicherplatz mit new fehl, wird in Normalfall eine Ausnahme vom
Typ std:bad_alloc ausgelöst.
Man kann sich allerdings in diese Situation einmischen. Kann new keinen Speicherplatz re-
servieren, wird nämlich eine Funktion aufgerufen, die die dazugehörige Ausnahme auslöst. Eine
derartige Funktion nennt man New-Handler.
Mit std::set_new_handler() kann man einen eigenen New-Handler installieren. Diese
Funktion wird in der speziellen C++-Headerdatei <new> deklariert.
Der New-Handler ist als globale Funktion zu deklarieren, die ohne Parameter aufgerufen
wird und auch keinen Rückgabewert zurückliefert. Das folgende Beispiel zeigt einen solchen
New-Handler und seine Verwendung:
// sonst/newhdl1.cpp
// Headerdatei für den New-Handler
#include <new>

// Standard-Headerdateien
#include <iostream>
#include <cstdlib>

/* eigenerNewHandler()
* - gibt Fehlermeldung aus und beendet das Programm
*/
void eigenerNewHandler ()
{
// Fehlermeldung auf Standard-Fehlerkanal ausgeben
std::cerr << "Speicherplatz alleeeeeeeeee..." << std::endl;

// entsprechende Exception auslösen


throw std::bad_alloc();
}

int main()
{
try {
Sandini Bib

550 Kapitel 9: Weitere Sprachmittel und Details

// eigenen New-Handler installieren


std::set_new_handler(&eigenerNewHandler);

// und mit Endlosschleife, die Speicherplatz anfordert, testen


for (;;) {
new char[1000000];
}

// kein Rechner kann unendlich Speicherplatz haben


std::cout << "Huch, Zauberei!" << std::endl;
}
catch (const std::bad_alloc& e) {
std::cerr << "Exception: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}
Am Anfang des Programms wird in main() durch den Aufruf von std::set_new_handler()
die Funktion eigenerNewHandler() als New-Handler installiert:4
std::set_new_handler(&eigenerNewHandler);
Da es von C her keinen Unterschied zwischen einer Funktion und der Adresse einer Funktion
gibt (der Name einer Funktion wird als ihre Adresse betrachtet), kann der Operator & dabei auch
wegfallen:
std::set_new_handler(eigenerNewHandler);
Anschließend wird versucht, in einer Endlosschleife Speicherplatz anzulegen:
for (;;) {
new char[1000000];
}
Früher oder später wird der Aufruf von new deshalb zum Aufruf des installierten New-Handlers
führen. Dieser meldet den Sachverhalt mit einer entsprechenden Fehlermeldung und löst eine
entsprechende Ausnahme aus:
void eigenerNewHandler ()
{
// Fehlermeldung auf Standard-Fehlerkanal ausgeben
std::cerr << "Speicherplatz alleeeeeeeeee..." << std::endl;

// entsprechende Exception auslösen


throw std::bad_alloc();
}

4
Wie set_new_handler() intern implementiert ist, wird in Abschnitt 9.4.1 noch genauer erläutert.
Sandini Bib

9.3 Weitere Aspekte von new und delete 551

Dieser erste New-Handler sollte allerdings noch ein wenig umgeschrieben werden. Es kann
nämlich passieren, dass über den New-Handler selbst versucht wird, neuen Speicherplatz an-
zufordern. Es ist nicht ausgeschlossen, dass im Rahmen des Ausgabeoperators << oder durch die
Erzeugung des Ausnahme-Objekts new aufgerufen wird. Dies würde eine rekursive Schleife von
New-Handler-Aufrufen zur Folge haben und schließlich zum Absturz des Programms führen.

Temporäre New-Handler
Die Funktion std::set_new_handler() liefert jeweils den vorher installierten New-Handler
zurück. Dies kann dazu verwendet werden, New-Handler nur für einen begrenzten Programmab-
schnitt zu installieren. Der zurückgelieferte New-Handler wird dazu später wieder installiert:
// sonst/newhdl3.cpp
void f ()
{
std::new_handler alterNewHandler; // Zeiger auf New-Handler

// neuen New-Handler installieren und alten merken


alterNewHandler = std::set_new_handler(&eigenerNewHandler);
...
// Operation mit new aufrufen
...
// alten New-Handler wieder installieren
std::set_new_handler(alterNewHandler);
}
Der Datentyp std::new_handler ist dabei nichts anderes als ein Funktionszeiger (siehe Ab-
schnitt 9.4.1) für eine Funktion ohne Parameter und ohne Rückgabewert:
namespace std {
typedef void (*new_handler)();
}

New-Handler mit Reserve-Speicherplatz


Bei kommerziellen Programmen genügt es in der Regel nicht, bei Fehlern das Programm ein-
fach zu beenden. Auch bei Speicherplatzmangel müssen typischerweise noch Sicherungsarbeiten
durchgeführt werden. Dabei ist es nicht selten der Fall, dass dazu noch Speicherplatz gebraucht
wird.
In dem Fall empfiehlt es sich, am Anfang des Programms Speicher anzufordern, der freigege-
ben wird, wenn der New-Handler aufgerufen wird. Dies könnte zum Beispiel in einem eigenen
Modul implementiert werden:
// sonst/newhdl4.cpp
// Headerdatei für den New-Handler
#include <new>
Sandini Bib

552 Kapitel 9: Weitere Sprachmittel und Details

// Standard-Headerdateien
#include <iostream>

// Vorwärtsdeklaration
static void eigenerNewHandler();

// Reserve-Speicherplatz
static char* reserveSpeicherplatz;

void initNewHandler ()
{
// je nach Bedarf Speicherplatz allokieren
reserveSpeicherplatz = new char[100000];

// New-Handler installieren
std::set_new_handler(&eigenerNewHandler);
}

static void eigenerNewHandler ()


{
// Reserve-Speicherplatz freigeben
delete [] reserveSpeicherplatz;

// Fehlermeldung auf Standard-Fehlerkanal ausgeben


std::cerr << "Speicherplatz alle (Restspeicher freigegeben)"
<< std::endl;

// entsprechende Exception auslösen


throw std::bad_alloc();
}
Im Programm muss dann am Anfang nur einmal initNewHandler() aufgerufen werden.

New-Handler, die das Programm nicht unbedingt beenden


Noch besser ist es natürlich, wenn bei Speicherplatzmangel zunächst versucht wird, das Pro-
gramm weiterlaufen zu lassen. Dazu kann der New-Handler so implementiert werden, dass das
Programm nicht beendet wird, sondern nach dem Aufruf des New-Handlers versucht weiterzu-
laufen.
Wenn der mangels Speicherplatz aufgerufene New-Handler das Programm nicht beendet und
keine Ausnahme auslöst, wird nämlich erneut versucht, den angeforderten Speicherplatz anzufor-
dern. Der Operator new ist also so vordefiniert, dass, wenn kein Speicherplatz angefordert werden
kann, entweder std::bad_alloc ausgelöst wird oder, falls ein New-Handler installiert ist, die-
Sandini Bib

9.3 Weitere Aspekte von new und delete 553

ser in einer Schleife immer wieder aufgerufen wird, bis der angeforderte Speicherplatz verfügbar
ist oder die Schleife durch eine Ausnahme oder einen Programmabbruch beendet wird.
Ein Programm kann also einen New-Handler installieren, der das Programm nicht beendet,
sondern aufräumt und dadurch Speicherplatz freigibt. Genauso ist es möglich, dass der erste
Aufruf des New-Handlers reservierten Speicherplatz freigibt und eine Warnung ausgibt, dass
der Speicher fast verbraucht ist. Das Programm läuft aber weiter und wird erst dann endgültig
beendet, wenn der Reserve-Speicher“ verbraucht ist:

// sonst/newhdl5.cpp
// Headerdatei für den New-Handler
#include <new>

// Standard-Headerdateien
#include <iostream>

// Vorwärtsdeklaration
static void eigenerNewHandler();

// Reserve-Speicherplatz
static char* reserveSpeicherplatz1;
static char* reserveSpeicherplatz2;

void initNewHandler ()
{
// je nach Bedarf Reserve-Speicherplatz anfordern
reserveSpeicherplatz1 = new char[1000000];
reserveSpeicherplatz2 = new char[100000];

// New-Handler installieren
std::set_new_handler(&eigenerNewHandler);
}

static void eigenerNewHandler ()


{
static bool firstKiss = true;

// - beim ersten Mal: Reserve-Speicherplatz bereitstellen


// - beim zweiten Mal: Ausnahme auslösen
if (firstKiss) {
// Programm läuft weiter bis zum zweiten Aufruf
firstKiss = false;
Sandini Bib

554 Kapitel 9: Weitere Sprachmittel und Details

// ersten Speicherplatz für New-Handler freigeben


delete [] reserveSpeicherplatz1;

// Warnung auf Standard-Fehlerkanal ausgeben


std::cerr << "Warnung: Speicherplatz bald alle" << std::endl;
}
else {
// zweiten Reserve-Speicherplatz für New-Handler freigeben
delete [] reserveSpeicherplatz2;

// Fehlermeldung auf Standard-Fehlerkanal ausgeben


std::cerr << "Speicherplatz endgueltig alle" << std::endl;

// entsprechende Exception auslösen


throw std::bad_alloc();
}
}

9.3.4 Überladen von new und delete


Die Operatoren new und delete werden in C++ als globale Operatoren vordefiniert. Es besteht
aber die Möglichkeit, diese globalen Operatoren durch eigene Versionen zu ersetzen. Dabei ist
auch eine Ersetzung nur für eine bestimmte Klasse möglich.
Durch eigene Implementierungen kann die dynamische Speicherverwaltung für eigene Be-
dürfnisse und/oder eigene Datentypen beliebig optimiert werden, oder es können Sonderfälle
eingebaut werden. Denkbar ist zum Beispiel,
 die dynamische Speicherverwaltung mit Hilfe von Ausgabeanweisungen zu verifizieren,
 den Zeitaufwand für das Anfordern von Speicherplatz für viele kleine Objekte durch Anfor-
derung von großen Blöcken im Voraus zu verringern oder
 eine automatische Speicherverwaltung (Garbage Collection) zu implementieren.

Globales Überladen von new und delete


Die vordefinierten new- und delete-Operatoren werden in der Sprache wie folgt deklariert:
// new und delete, die gegebenenfalls Ausnahmen auslösen
void* operator new (std::size_t size) throw(std::bad_alloc);
void* operator new[] (std::size_t size) throw(std::bad_alloc);
void operator delete (void* ptr) throw();
void operator delete[] (void* ptr) throw();

// new und delete, die gegebenenfalls NULL zurückliefern


void* operator new (std::size_t size, const std::nothrow_t&) throw();
Sandini Bib

9.3 Weitere Aspekte von new und delete 555

void* operator new[] (std::size_t size, const std::nothrow_t&) throw();


void operator delete (void* ptr, const std::nothrow_t&) throw();
void operator delete[] (void* ptr, const std::nothrow_t&) throw();
Durch eigene Implementierungen kann z.B. die Speicherverwaltung für alle Objekte umgestellt
werden. Dabei muss man darauf achten, dass zu einem new auch das entsprechende delete
ersetzt wird.
Der Operator new erhält als Parameter die Größe des Objekts, das angelegt werden soll. Dafür
wird der Typ size_t verwendet, der in <cstddef> implementierungsabhängig definiert wird.
Man beachte, dass der Rückgabetyp immer void* (und nicht BspKlasse*) sein muss. Es wird
noch kein Objekt initialisiert, sondern erst Speicher dafür bereitgestellt.
Der Operator delete erhält als Parameter die Adresse des freizugebenden Speicherplatzes.
Der Rückgabetyp muss immer void sein.
Die mit nothrow_t deklarierten Operatoren, sind für den Fall vorhanden, dass new mit
std::nothrow aufgerufen wird (sieheSeite 548). Der Delete-Operator dazu wird dann aufge-
rufen, wenn der Speicherplatz für ein mit new(std::nothrow) erzeugtes Objekt erfolgreich
angefordert wurde, der Konstruktor des Objekts aber eine Ausnahme ausgelöst hat. Damit kann
der angeforderte Speicherplatz wieder freigegeben werden, bevor new NULL zurückgeliefert.

Überladen von new und delete bei Klassen


new und delete können auch individuell für einzelne Klassen implementiert werden. Auf fol-
gende Weise können z.B. für eine Klasse BspKlasse new und new[ ] mit einer zusätzlichen
Ausgabeanweisung versehen werden:
// sonst/mynew.cpp
#include <iostream>
#include <cstddef>

void* BspKlasse::operator new (std::size_t size)


{
// Meldung ausgeben
std::cout << "BspKlasse::new aufgerufen" << std::endl;

// globales new für Speicher der Größe size aufrufen


return ::new char[size];
}

void* BspKlasse::operator new[] (std::size_t size)


{
// Meldung ausgeben
std::cout << "BspKlasse::new[] aufgerufen" << std::endl;

// globales new für Speicher der Größe size aufrufen


return ::new char[size];
}
Sandini Bib

556 Kapitel 9: Weitere Sprachmittel und Details

Man beachte auch hier, dass der Rückgabetyp immer void* (und nicht BspKlasse*) sein muss.
Es wird noch kein Objekt der Klasse angelegt, sondern erst Speicher dafür bereitgestellt.
Bei der Implementierung des Operators wird durch die Verwendung des Bereichsoperators ::
sichergestellt, dass innerhalb der Implementierung der globale New-Operator aufgerufen wird.
Sofern die eigene Implementierung nicht dazu dient, die Speicherverwaltung völlig umzustel-
len, ist es immer von Vorteil, sie auf den globalen New-Operator zurückzuführen. Damit wird
sichergestellt, dass im Fehlerfall der installierte New-Handler aufgerufen wird, so wie es vom
Anwender auch erwartet wird.
Zum Anfordern des Speicherplatzes muss immer size verwendet werden. Da der Operator
new vererbt wird, kann er nämlich auch für größere Objekte aus abgeleiteten Klassen aufgerufen
werden. Es ist also ein Fehler, Speicher nur für ein Objekt der entsprechenden Klasse bereitzu-
stellen:
return ::new char[sizeof(BspKlasse)]; // FEHLER
Genauso falsch ist es, einfach explizit das globale new für ein Objekt der Klasse aufzurufen:
return ::new BspKlasse; // FEHLER
Hier wird ebenfalls unter Umständen zu wenig Speicher angelegt und außerdem der Konstruktor
der Klasse bereits aufgerufen (was eigentlich erst nach dem Aufruf von new passieren sollte).
Wer den Operator new wirklich nur für Objekte dieser Klasse implementieren will, kann für
abgeleitete Klassen das globale new direkt aufrufen. Dazu wird einfach geprüft, ob sich die Größe
des angeforderten Speichers mit der Größe der Objekte dieser Klasse deckt:
void* BspKlasse::operator new (size_t size)
{
// für abgeleitete Klassen globales new aufrufen
if (size != sizeof(BspKlasse)) {
return ::new char [size];
}
...
}
Bei abgeleiteten Klassen ohne neue Komponenten wird der Test allerdings fehlschlagen.
Auf entsprechende Art und Weise kann für jede Klasse der Operator delete mit einer Aus-
gabeanweisung implementiert werden:
// sonst/mydelete.cpp
#include <cstddef>

void BspKlasse::operator delete (void* p)


{
// Meldung ausgeben
std::cout << "BspKlasse::delete aufgerufen" << std::endl;

// globales delete aufrufen


::delete [] p;
}
Sandini Bib

9.3 Weitere Aspekte von new und delete 557

void BspKlasse::operator delete[] (void* p)


{
// Meldung ausgeben
std::cout << "BspKlasse::delete[] aufgerufen" << std::endl;

// globales delete aufrufen


::delete [] p;
}
Wie das Beispiel zeigt, erhält der Operator delete als Parameter die Adresse des freizugebenden
Speicherplatzes. Der Rückgabetyp muss auch hier immer void sein.
Man beachte, dass in beiden Fällen das globale new für Felder aufgerufen wurde. Aus diesem
Grund muss auch das globale delete für Felder aufgerufen werden.
Man kann optional einen zweiten Parameter vom Typ std::size_t deklarieren. Dieser
erhält dann ebenfalls die Größe des Objekts, das freigegeben werden soll:
void BspKlasse::operator delete (void* p, std::size_t size)
{
...
}

new und realloc()


Es gibt in C++ keinen Operator zum Verkleinern und Vergrößern von Speicherplatz. Diese Funk-
tionalität muss im Allgemeinen über new und delete realisiert werden. Auf C übertragen be-
deutet das, dass ein realloc() durch malloc()und free() implementiert werden muss.
Wenn aber durch eine eigene Definition sichergestellt wird, dass new und delete intern über
malloc() und free() implementiert sind, kann für mit new angelegte Objekte auch realloc()
aufgerufen werden. Dazu sollte allerdings eine Funktion wie renew() definiert werden, damit
C- und C++-Techniken nicht durcheinander gewürfelt werden. Außerdem sollte darauf geachtet
werden, dass auch diese Funktion im Fehlerfall den New-Handler aufruft. Dazu kann man den
aktuellen New-Handler mit std::set_new_handler() abfragen (siehe Seite 551).
Ich warne ausdrücklich davor, realloc() direkt für mit new angelegte Objekte aufzurufen.
Es funktioniert zwar meistens, da new typischerweise über malloc() implementiert ist, es ist
aber nicht portabel und sehr gefährlich, da zum einen new() auch anders definiert sein kann und
zum anderen der New-Handler dann nicht verwendet wird.

9.3.5 Operator new mit zusätzlichen Parametern


Der Operator new kann durch die Deklaration zusätzlicher Parameter überladen werden. Die
Argumente für diese Parameter müssen dann beim Aufruf des Operators in Klammern zwischen
new und dem Datentyp angegeben werden.
Die Deklaration
void* BspKlasse::operator new (size_t, float, int)
Sandini Bib

558 Kapitel 9: Weitere Sprachmittel und Details

ermöglicht z.B. den Aufruf:


new(3.5,42) BspKlasse
Die beiden Argumente werden dabei nach dem ohnehin vorhandenen ersten Parameter überge-
ben. Aufgerufen wird also:
BspKlasse::operator new (sizeof(BspKlasse), 3.5, 42)
Für delete ist die Verwendung zusätzlicher Parameter nicht zugelassen.

9.3.6 Zusammenfassung
 Mit std::nothrow kann man erreichen, dass new bei zu wenig Speicherplatz keine Ausnah-
me auslöst, sondern NULL zurückliefert.
 Mit Placement-New kann man Objekte in vorhandenem Speicherplatz initialisieren.
 Für den Fall, dass mit new kein Speicherplatz angefordert werden kann, kann ein New-
Handler definiert werden, der dann automatisch aufgerufen wird und die entsprechende Feh-
lerbehandlung durchführt.
 Die Operatoren new und delete können überladen werden.
 Beim Überladen von new sollte darauf geachtet werden, dass im Fehlerfall ein installierter
New-Handler aufgerufen wird.
 Man kann eigene Versionen von new mit zusätzlichen Parametern definieren.
Sandini Bib

9.4 Funktions- und Komponentenzeiger 559

9.4 Funktions- und Komponentenzeiger


In diesem Abschnitt wird gezeigt, wie Zeiger auf Funktionen und Zeiger auf Komponenten von
Klassen definiert und angewendet werden können. In diesem Zusammenhang werden auch spe-
zielle Operatoren eingeführt, mit denen auf Klassenkomponenten zugegriffen werden kann.

9.4.1 Funktionszeiger
Schon in C ist es möglich, Zeiger auf Funktionen zu definieren. Auf diese Weise werden z.B.
häufig Error-Handler verwaltet.
Error-Handler ( Fehler-Behandler“) sind Funktionen, die irgendwo installiert werden, um

dann aufgerufen zu werden, wenn Fehler auftreten. Dazu wird ein Error-Handler einem Funkti-
onszeiger zugewiesen. Wenn dann ein Fehler auftritt, wird die Funktion aufgerufen, auf die der
Funktionszeiger zeigt.
Ein Beispiel für derartige Error-Handler sind die in Abschnitt 9.3.3 vorgestellten New-Hand-
ler. Ein Funktionszeiger zeigt als globale Variable jeweils auf die Funktion, die im Fehlerfall
aufzurufen ist:
// Datentyp für Zeiger auf New-Handler definieren
namespace std {
typedef void (*new_handler) ();
}

// Zeiger auf aktuellen New-Handler


new_handler myNewHandler;
Diesem Funktionszeiger kann mit der Funktion set_new_handler() ein New-Handler überge-
ben werden:
new_handler set_new_handler (new_handler neuerNewHandler)
{
// alten New-Handler zum Zurückliefern merken
new_handler alterNewHandler = myNewHandler;

// übergebene Funktion als neuen New-Handler installieren


myNewHandler = neuerNewHandler;

// alten New-Handler zurückliefern


return alterNewHandler;
}
Wenn beim Aufruf von new kein Speicher reserviert werden kann, wird, sofern ein New-Handler
installiert ist, dieser aufgerufen:
Sandini Bib

560 Kapitel 9: Weitere Sprachmittel und Details

// sonst/newimpl.cpp
// mögliche Implementierung von Operator new
void* operator new (std::size_t size)
{
void* p; // Zeiger für neuen Speicher

// solange es nicht klappt, neuen Speicher zu bekommen,


// New-Handler aufrufen oder Ausnahme auslösen
while ((p = holSpeicher(size)) == 0) {
// kein ausreichender Speicherplatz verfügbar
if (MyNewHandler != 0) {
// New-Handler aufrufen
(*myNewHandler)();
}
else {
// Ausnahme auslösen
throw std::bad_alloc();
}
}

// OK, neuen Speicherplatz zurückliefern


return p;
}
Nach dem gleichen Muster können Funktionen für beliebige Situationen installiert werden. Sie
werden einfach in einem Funktionszeiger festgehalten und, wenn die entsprechende Situation
eintritt, aufgerufen.

9.4.2 Komponentenzeiger
Sollen Elementfunktionen als Parameter verwendet werden, werden Zeiger auf Elementfunktio-
nen benötigt. C++ führt dazu Komponentenzeiger ein. Komponentenzeiger sind Zeiger auf Kom-
ponenten einer bestimmten Klasse. Durch Verwendung des Bereichsoperators bei der Deklarati-
on sind sie an eine bestimmte Klasse gebunden. Der Zugriff erfolgt mit speziellen Operatoren,
die nur für diesen Zweck eingeführt werden.

Komponentenzeiger für einfache Datentypen


Zur Deklaration eines Komponentenzeigers muss der Bereichsoperator verwendet werden, der
den Zeiger dem Gültigkeitsbereich einer Klasse zuordnet:
// Zeiger auf int-Komponente der Klasse BspKlasse
int BspKlasse::* intPtr;
Dem Zeiger kann dann die Adresse einer Integer-Komponente der Klasse zugewiesen werden:
Sandini Bib

9.4 Funktions- und Komponentenzeiger 561

intPtr = & BspKlasse::xyz;


Dazu muss aber natürlich Zugriff auf die Komponente bestehen (sei es, dass sie öffentlich ist,
oder sei es, dass sich die Anweisung im Gültigkeitsbereich einer Element- oder Friend-Funktion
befindet).
Mit dem C++-Operator .* kann dann darauf zugegriffen werden:
BspKlasse obj;
...
// bei obj auf die Komponente zugreifen, auf die intPtr zeigt
obj.*intPtr = 7;
Wenn ein Zeiger auf ein Objekt der Klasse zeigt, kann der C++-Operator ->* entsprechend
verwendet werden:
BspKlasse obj;
BspKlasse* op = &obj;
...
// bei dem Objekt, auf das op zeigt, auf die Komponente zugreifen, auf die intPtr zeigt
op->*intPtr = 7;

Komponentenzeiger für Elementfunktionen


Komponentenzeiger werden vor allem zur Parametrisierung von Elementfunktionen verwendet.
Es handelt sich dann um Funktionszeiger für Klassen.
Zur Deklaration eines Zeigers auf eine Elementfunktion muss ebenfalls der Bereichsoperator
verwendet werden, der den Zeiger dem Gültigkeitsbereich einer Klasse zuordnet:
void (BspKlasse::*funcPtr) ();
In diesem Fall wird funcPtr als Zeiger auf eine Elementfunktion der Klasse BspKlasse dekla-
riert, die ohne Parameter aufgerufen wird und als Rückgabetyp void besitzt.
Hier empfiehlt es sich, wie meistens bei Funktionszeigern, einen eigenen Datentyp zu defi-
nieren, damit die verschiedenen Deklarationen einfacher und übersichtlicher werden:
typedef void (BspKlasse::*BspKlassenFunktion) ();

BspKlassenFunktion funcPtr;
Nach der Zuweisung einer Komponente muss zum Zugriff auf die Komponente ebenfalls der
C++-Operator .* verwendet werden:
BspKlasse obj;
...
// Elementfunktion, auf die funcPtr zeigt, für Objekt obj aufrufen
(obj.*funcPtr) ();
Da der Funktionsaufruf eine höhere Priorität hat, muss der Ausdruck mit dem Operator .* ge-
klammert werden.
Entsprechend wird für Zeiger auf Objekte der Operator ->* verwendet:
Sandini Bib

562 Kapitel 9: Weitere Sprachmittel und Details

BspKlasse obj;
BspKlasse* objPtr = &obj;
...
/* Elementfunktion, auf die funcPtr zeigt, für Objekt aufrufen, auf das objPtr zeigt
*/
(objPtr->*funcPtr)();
Auf diese Art und Weise kann z.B. eine Elementfunktion als Parameter übergeben und in einer
Variablen verwaltet werden.
Das folgende Beispiel verdeutlicht die Verwendung von Komponentenzeigern:
// sonst/kompptr1.cpp
#include <iostream>

class BspKlasse {
public:
void func1() {
std::cout << "Aufruf von func1()" << std::endl;
}
void func2() {
std::cout << "Aufruf von func2()" << std::endl;
}
};

// Datentyp: Zeiger auf Elementfunktion der Klasse BspKlasse


// ohne Parameter und Rückgabewert
typedef void (BspKlasse::*BspKlassenFunktion) ();

int main ()
{
// Funktionszeiger auf Elementfunktion der Klasse BspKlasse
BspKlassenFunktion funcPtr;

// Objekt der Klasse BspKlasse


BspKlasse x;

// Komponentenzeiger zeigt auf func1()


funcPtr = & BspKlasse::func1;

// Elementfunktion, auf die der Zeiger zeigt, für Objekt x aufrufen


(x.*funcPtr) ();
Sandini Bib

9.4 Funktions- und Komponentenzeiger 563

// Komponentenzeiger zeigt auf func2()


funcPtr = & BspKlasse::func2;

// Elementfunktion, auf die der Zeiger zeigt, für Objekt x aufrufen


(x.*funcPtr) ();
}
Die Elementfunktionen func1() und func2() werden nacheinander dem Komponentenzeiger
funcPtr zugewiesen und über diesen aufgerufen. Die Ausgabe des Programms lautet somit:
Aufruf von func1()
Aufruf von func2()

9.4.3 Komponentenzeiger für Schnittstellen nach außen


Ein Anwendungsbeispiel für die Verwendung von Komponentenzeigern sind Schnittstellen zu
anderen Prozessen oder Sprachumgebungen, in denen Elementfunktionen von Objekten aufruf-
bar sein sollen.
Das folgende Beispiel zeigt, wie dies im Prinzip möglich ist:
// sonst/kompptr2.cpp
#include <iostream>

class BspKlasse {
public:
void func1() {
std::cout << "Aufruf von func1()" << std::endl;
}
void func2() {
std::cout << "Aufruf von func2()" << std::endl;
}
};

// Datentyp: Zeiger auf Elementfunktion der Klasse BspKlasse


// ohne Parameter und Rückgabewert
typedef void (BspKlasse::*BspKlassenFunktion) ();

// exportiertes Objekt und exportierte Elementfunktion


void* objPtr;
void* objfpPtr;

void exportObjectAndFunction (void* op, void* fp)


{
Sandini Bib

564 Kapitel 9: Weitere Sprachmittel und Details

objPtr = op;
objfpPtr = fp;
}

void callBspKlassenFunc (void* op, void* fp)


{
// Typen zurückwandeln
BspKlasse* bspObjPtr = static_cast<BspKlasse*>(op);
BspKlassenFunktion* bspObjfpPtr
= static_cast<BspKlassenFunktion*>(fp);

// übergebene Elementfunktion für das übergebene Objekt aufrufen


(bspObjPtr->*(*bspObjfpPtr)) ();
}

int main ()
{
// Objekt der Klasse BspKlasse
BspKlasse x;

// Funktionszeiger auf Elementfunktion der Klasse BspKlasse


BspKlassenFunktion funcPtr;

// Komponentenzeiger zeigt auf func1()


funcPtr = & BspKlasse::func1;

// Objekt und Elementfunktion exportieren


exportObjectAndFunction (&x, &funcPtr);

// exportierte Elementfunktion für exportiertes Objekt aufrufen


callBspKlassenFunc (objPtr, objfpPtr);
}
In C++ können sowohl die Adressen von Objekten als auch die Adressen der dazugehörigen
Komponentenzeiger als void* exportiert werden:
void exportObjectAndFunction (void* op, void* fp);
...
BspKlasse x;
BspKlassenFunktion funcPtr;
...
exportObjectAndFunction (&x, &funcPtr);
Sandini Bib

9.4 Funktions- und Komponentenzeiger 565

Umgekehrt können diese Adressen wieder als Objekte und Komponentenzeiger verwendet wer-
den:
void callBspKlassenFunc (void* op, void* fp)
{
// Typen zurückwandeln
BspKlasse* bspObjPtr = static_cast<BspKlasse*>(op);
BspKlassenFunktion* bspObjfpPtr
= static_cast<BspKlassenFunktion*>(fp);

// übergebene Elementfunktion für das übergebene Objekt aufrufen


(bspObjPtr->*(*bspObjfpPtr)) ();
}
Man beachte, dass dabei die Adresse einer Variablen, die auf die Elementfunktion verweist,
exportiert wird. Dabei muss natürlich darauf geachtet werden, dass diese Variable gültig bleibt.
Es ist nicht möglich, die Adresse der Elementfunktion zu exportieren.

9.4.4 Zusammenfassung
 In C++ lassen sich Zeiger auf Klassenkomponenten definieren.
 Die C++-Operatoren .* und ->* erlauben es, von Objekten oder Objektzeigern auf Verweise
auf Klassenkomponenten zuzugreifen.
 Über Zeiger auf Elementfunktionen lassen sich Elementfunktionen parametrisieren oder für
einen Aufruf von außerhalb exportieren.
Sandini Bib

566 Kapitel 9: Weitere Sprachmittel und Details

9.5 Kombination mit C-Code


C++-Code kann mit C-Code kombiniert werden. Diese Tatsache wird in C++ garantiert.

9.5.1 Externe Bindung


Die Schnittstelle zwischen C++ und C darf natürlich nur Sprachmittel verwenden, die in beiden
Sprachen vorhanden sind. Zusätzlich muss diese Schnittstelle in C++ speziell markiert werden.
Dies geschieht durch die Verwendung des Schlüsselworts extern. Die Deklaration extern "C"
deklariert Funktionen (auch Datentypen und Variablen), die Teil der Schnittstelle zur Sprache C
sind. Diesen Vorgang bezeichnet man als externe Bindung (englisch: external linkage).
Die folgende Deklaration definiert z.B. die Funktion foo() als Schnittstelle zwischen C und
C++:
extern "C" {
// Deklaration der Schnittstelle zwischen C und C++
void foo (long, double);
...
}
Dabei ist es unerheblich, ob diese Funktion in einem C- oder in einem C++-Modul implementiert
wird. Es kann sich also um eine importierte C-Funktion oder auch um eine exportierte C++-
Funktion handeln.
Durch die Verwendung von extern "C" wird insbesondere dafür gesorgt, dass der Funk-
tionsname foo als Symbol erhalten bleibt. Normalerweise bestehen interne Symbole für Funktio-
nen aus Informationen, die neben dem Funktionsnamen auch die Datentypen der Parameter ent-
halten. Dadurch wird sichergestellt, dass überladene Funktionen unterschieden werden können.
Dieser Vorgang wird als Name-Mangling“ bezeichnet.

Beim GCC werden z.B. die Deklarationen
void foo ();
void foo (long, double);
void foo (const Bsp::Bruch&);
intern durch folgende Symbole verwaltet:
foo__Fv
foo__Fld
foo__FRCQ23Bsp5Bruch
Die generierten Symbole sind nicht standardisiert. Aus diesem Grund ist es in der Regel nicht
möglich, Code verschiedener C++-Compiler zu kombinieren.
Da in C keine Funktionen überladen werden können, wird dort das Name-Mangling nicht
verwendet. Durch extern "C" wird es auch in C++-Dateien ausgeschaltet.
Durch eine entsprechende Extern-Anweisung ist prinzipiell auch die Bindung zu anderen
Sprachen möglich. Dies wird aber nicht grundsätzlich von der Sprache C++ garantiert, sondern
hängt vom verwendeten Compiler und Linker ab. Da es aber normalerweise immer möglich ist,
Code einer Sprache mit C-Code zu kombinieren, kann man diesen Code durch Verwendung von
extern "C" auch immer mit C++ kombinieren.
Sandini Bib

9.5 Kombination mit C-Code 567

9.5.2 Headerdateien für C und C++


Es ist auch möglich, Headerdateien sowohl für die Verwendung in C als auch für die Verwen-
dung in C++ vorzusehen. Dies geschieht durch die Auswertung der nur für C++ vordefinierten
Präprozessor-Konstante __cplusplus5 . Eine solche Datei könnte wie folgt aussehen:
#ifdef __cplusplus
extern "C" {
#endif
void foo (long, double);
...
#ifdef __cplusplus
}
#endif
Mit den Präprozessor-Anweisungen wird sichergestellt, dass die Anweisung
extern "C" {
...
}
mit den dazugehörigen Klammern nur dann beim Kompilieren berücksichtigt wird, wenn die
Konstante __cplusplus definiert ist (es sich also um eine C++-Kompilation handelt). Ansons-
ten werden die Funktionen wie in C bzw. ANSI-C mit Parameter-Prototypen deklariert. Die
Standard-Headerdateien von C sind für C++ in der Regel auf diese Weise implementiert.

9.5.3 Übersetzen von main()


Auf einen wichtigen Punkt muss dabei allerdings noch hingewiesen werden: main() muss in
der Regel mit einem C++-Compiler übersetzt werden. Dies liegt daran, dass mit dem Aufruf von
main() die Konstruktoren der globalen C++-Objekte aufgerufen werden. Wird main() nicht
mit einem C++-Compiler übersetzt, kann es deshalb Probleme geben. Auf manchen Systemen
gibt es dazu Workarounds (spezielle Hilfsfunktionen zur Initialisierung der C++-Umgebung).
Die Verwendung derartiger Lösungen ist aber nicht portabel.

9.5.4 Zusammenfassung
 C-Code kann mit C++-Code kombiniert werden.
 Importierte und exportierte Funktionen (Typen, Variablen) müssen dazu mittels extern "C"
deklariert werden.
 C++-Symbole unterliegen normalerweise dem Name-Mangling.
 Code unterschiedlicher C++-Compiler kann normalerweise nicht kombiniert werden.
 main() sollte immer mit einem C++-Compiler übersetzt werden.
 In C++ ist grundsätzlich die Präprozessorkonstante __cplusplus definiert.

5
Um Namenskonflikte zu vermeiden, beginnen vom System vordefinierte Konstanten typischerweise mit
zwei Unterstrichen.
Sandini Bib

568 Kapitel 9: Weitere Sprachmittel und Details

9.6 Weitere Schlüsselwörter


In diesem Abschnitt werden einige noch fehlende Schlüsselwörter vorgestellt.

9.6.1 Varianten mit union


Eine Variante (englisch: union) ist eine Datenstruktur, in der sich alle Komponenten an der glei-
chen Adresse befinden. Dadurch belegt die Variante nur so viel Speicherplatz wie ihr größtes
Element. Entsprechend kann die Variante auch immer nur einen der möglichen Werte gleichzei-
tig enthalten.
Die folgende Variante kann z.B. wahlweise die Adresse eines Zeichens oder einen ganzzah-
ligen Wert enthalten:
union Wert {
const char* adr;
int zahl;
};
Die Sprache verwaltet nicht, welche Art von Wert sich in einer Variante befindet. Schreibt man
eine Adresse hinein, muss man selbst sicherstellen, dass man auch eine Adresse ausliest:
Wert wert;

wert.adr = "hallo"; // Adresse in Variante wert eintragen


...
const char* t = wert.adr; // Adresse aus Variante wert auslesen

wert.zahl = 42; // Zahl in Variante wert eintragen


...
int i = wert.zahl; // Zahl aus Variante wert auslesen
Dazu merkt man sich typischerweise in einer separaten Variable oder Komponente, was sich
gerade in einer Variante befindet.

9.6.2 Aufzählungstypen mit enum


Man kann sich in C++ auch Aufzählungstypen (so genannte enumeration types) definieren. Dabei
handelt es sich um Datentypen, die verschiedene Aufzählungswerte annehmen können. Damit
kann man z.B. verwalten, was sich gerade in einer Variante befindet:
enum WertArt { Adresse, Zahl };

struct Eintrag {
WertArt art;
Wert wert;
};
Sandini Bib

9.6 Weitere Schlüsselwörter 569

if (...) {
wert.art = Adresse; // Adresse in Variante wert eintragen
wert.adr = "hallo";
}
else {
wert.art = Zahl; // Zahl in Variante wert eintragen
wert.zahl = 42;
}
...

switch (wert.art) {
case Adresse:
const char* t = wert.adr; // Adresse aus Variante wert auslesen
break;
case Zahl:
int i = wert.zahl; // Zahl aus Variante wert auslesen
...
break;
}
Man beachte, dass sowohl der Aufzählungstyp als auch die dazugehörigen Werte als Symbo-
le im jeweiligen Gültigkeitsbereich definiert werden. Man sollte also darauf achten, dass man
Aufzählungstypen nicht im globalen Namensbereich definiert.
Den Werten von Aufzählungstypen können auch ganzzahlige Werte zugeordnet werden:
enum Jahreszeiten { Fruehling=1, Sommer=2, Herbst=4, Winter=8 };
Ohne diese Zuordnung fangen die Werte bei null an.
Aufzählungstypen dürfen alle Werte zwischen null und dem auf die nächste Zweierpotenz
aufgerundeten größten ganzzahligen Wert annehmen. Hat ein Aufzählungswert einen negati-
ven Wert, gilt Entsprechendes (es werden immer so viele Bits verwendet, wie im Wertebereich
benötigt werden).
Entsprechend können ganzzahlige Werte explizit in Aufzählungswerte konvertiert werden:
enum Bereich { min = 0, max = 100 } // Bereich: 0 bis 127
Bereich b;
b = min; // OK
b = max; // OK
b = Bereich(17); // zwischen min und max
b = Bereich(120); // OK, gleiche Anzahl Bits wie 100
b = Bereich(200); // FEHLER (undefiniertes Verhalten)
Ein weiteres Beispiel für die Verwendung von Aufzählungstypen befindet sich auf Seite 425.
Sandini Bib

570 Kapitel 9: Weitere Sprachmittel und Details

9.6.3 Das Schlüsselwort volatile


Um für ein Programm ungeeignete Optimierungen zu vermeiden, können Funktionen und Varia-
blen mit dem Schlüsselwort volatile deklariert werden. Die genaue Bedeutung einer solchen
Deklaration hängt vom Compiler ab.
Im Wesentlichen geht es dabei aber immer darum, dass in Programmen Variablen oder Funk-
tionen verwendet werden, auf die auf irgendeine für den Compiler nicht nachvollziehbare Art
und Weise noch von außerhalb des Programms Zugriff besteht. Wenn diese Variablen oder Funk-
tionen wegoptimiert würden, wäre ein solches Programm nicht mehr lauffähig.
Greift man in einem Programm z.B. auf eine Ressource zu, die nicht nur der Kontrolle des
Compilers unterliegt, sollte man diese mit volatile schützen:
extern const volatile wert;
Ohne volatile würde der Compiler davon ausgehen, dass sich der Wert von wert nach dem
ersten Lesen nicht geändert haben kann, und könnte somit weitere Leseversuche wegoptimieren.
Mit volatile wird sichergestellt, dass der Wert von wert immer wieder neu abgefragt wird.

9.6.4 Zusammenfassung
 Mit union kann man Varianten definieren. Das sind Objekte, die mit dem gleichen Speicher-
platz Daten unterschiedlichen Typs verwalten können.
 Mit enum kann man Aufzählungstypen definieren.
 Mit volatile kann man Bereiche, die außerhalb der Kontrolle des Compilers liegen, vor
allzu aggressiven Optimierungen schützen.
Sandini Bib

Kapitel 10
Zusammenfassung

Da die Sprachmittel von C++ unter didaktischen Gesichtspunkten nach und nach eingeführt wur-
den, sollen hier noch einmal die wichtigsten Eigenschaften geschlossen aufgelistet werden. Da-
zu gehören neben den Operatoren und ihren Eigenschaften auch sinnvolle Programmier- und
Design-Richtlinien.

10.1 Hierarchie der C++-Operatoren


Die Tabelle auf den folgenden beiden Seiten enthält alle C++-Operatoren. Sie ist nach folgendem
Prinzip aufgebaut:
 Horizontale Linien trennen Operatoren mit unterschiedlicher Priorität. Die Tabelle ist dabei
nach absteigender Priorität geordnet.
 Die Spalte Ausw.“ gibt die Auswertungsreihenfolge bei gleicher Priorität an.

– L-R bedeutet, dass die Operatoren bei gleicher Priorität von links nach rechts ausgewertet
werden.
– R-L bedeutet, dass die Operatoren bei gleicher Priorität von rechts nach links ausgewertet
werden.
 Die letzte Spalte gibt an, ob der Operator in C++ neu eingeführt wurde. Operatoren, die nicht
mit neu“ gekennzeichnet sind, gibt es auch schon in C.

571
Sandini Bib

572 Kapitel 10: Zusammenfassung

Operator Bedeutung Ausw.


(...) Klammerung
:: Bereichsauflösung, Bereichszuordnung neu
. Zugriff auf Komponente L-R
-> Zugriff auf Komponente von Zeiger
(a->b entspricht im Allgemeinen (*a).b)
[...] Feld-Indizierung
fname(...) Funktionsaufruf
++ Postfix-Inkrement ( a++“)

-- Postfix-Dekrement ( a--“)

typ(...) Typumwandlung (funktionale Notation) neu
static_cast<typ>(...) Typumwandlung (logisch) neu
const_cast<typ>(...) Typumwandlung (Konstantheit entfernen) neu
dynamic_cast<typ>(...) Typumwandlung (Downcast, zur Laufzeit) neu
reinterpret_cast<typ>(...) Typumwandlung (Neuinterpretation von Bits) neu
typeid (...) Klassen-ID neu
++ Präfix-Inkrement ( ++a“) R-L

-- Präfix-Dekrement ( --a“)

~ Bit-Komplement
! logische Negation
+ positives Vorzeichen neu
- negatives Vorzeichen
& Adresse von
* Verweis auf (Dereferenzierung)
new Objekt anlegen neu
delete Objekt freigeben neu
new[] Feld (Array) von Objekten anlegen neu
delete[] Feld (Array) von Objekten freigeben neu
(typ)... Typumwandlung (Cast-Notation)
sizeof (...) Speicherplatz
.* Komponente über Komponentenzeiger L-R neu
->* Komponente von Zeiger über neu
Komponentenzeiger
* Multiplikation L-R
/ Division
% Modulo (Rest nach Division)
+ Addition L-R
- Subtraktion
<< Links-Shift L-R
>> Rechts-Shift
Sandini Bib

10.1 Hierarchie der C++-Operatoren 573

Operator Bedeutung Ausw.


< kleiner L-R
<= kleiner gleich
> größer
>= größer gleich
== gleich L-R
!= ungleich
& bitweises UND L-R
^ bitweises XOR (exklusives ODER) L-R
| bitweises ODER L-R
&& logisches UND (Ausw. bis zum ersten false) L-R
|| logisches ODER (Ausw. bis zum ersten true) L-R
?: bedingte Bewertung R-L
= Zuweisung R-L
*=
/=
%=
+=
-= a op= b“ entspricht i.A. a = a op b“
” ”
<<=
>>=
&=
^=
|=
, Folge von Ausdrücken L-R
Sandini Bib

574 Kapitel 10: Zusammenfassung

10.2 Klassenspezifische Eigenschaften von


Operationen
Die folgende Tabelle zeigt verschiedene Eigenschaften von klassenspezifischen Funktionen, die
in der Praxis wichtig sind:
wird kann Rückga- kann kann Default
vererbt virtuell betyp Kompo- Friend wird
sein nente sein generiert
sein

Default-Konstr. – – – ja – ja1
Copy-Konstr. – – – ja – ja
andere Konstr. – – – ja – –

Destruktor – ja – ja – ja

Konvertierung ja ja –2 ja – –

new, new[] ja – void*3 static – –4


delete , delete[] ja – void static – –4

= – ja ja ja – ja
(), [], -> ja ja ja ja – –
op= ja ja ja ja ja –
andere Operatoren ja ja ja ja ja –

andere Elementfkt. ja ja ja ja – –

friend – – ja – ja –

1 wenn kein anderer Konstruktor definiert ist


2 liefert aber etwas zurück
3 liefert aber typ* zurück
4 ist aber als globale Operation für alle Klassen vordefiniert
Sandini Bib

10.3 Regeln zur automatischen Typumwandlung 575

10.3 Regeln zur automatischen Typumwandlung


Die folgende Liste zeigt stichwortartig, welche Prioritäten bei der automatischen Typumwand-
lung gelten. Die Liste ist nach absteigender Priorität angeordnet.
1. Exakte Übereinstimmung bzw. triviale Typumwandlungen
Zu trivialen Typumwandlungen zählen:
– Nicht-Referenz <=> Referenz
– Feld-Schreibweise => Zeiger-Schreibweise
– Variable => Konstante
– Variable => Volatile
2. Passende Funktionstemplates ohne Typumwandlung
3. Informationserhaltende Standardumwandlungen
– char, short int, enum => int
– float => double
4. Sonstige Standardumwandlungen
– Standardumwandlungen von C
– Umwandlungen zu Objekten der Basisklassen (nur Referenzen und Zeiger)
5. Selbst definierte Typumwandlungen
– Konstruktoren mit einem Argument
– Konvertierungsfunktionen
Dabei gilt:
– Es ist maximal eine selbst definierte Typumwandlung pro Objekt möglich
(eine mehrfache automatische Typumwandlung ist nicht möglich).
– Vorher und nachher sind beliebige Standardumwandlungen möglich.
– Standardumwandlungen haben auf die Priorität keinen Einfluss.
– Konstruktoren und Konvertierungsfunktionen sind gleichwertig.
Wenn so noch keine Übereinstimmung vorhanden ist, wird noch geprüft, ob es eine passen-
de Funktion mit einer variablen Anzahl von Argumenten gibt. Ansonsten ist der Aufruf nicht
möglich.

10.4 Sinnvolle Programmierrichtlinien und Konventionen


In C++ existieren zahlreiche Aspekte, auf die bei der Verwendung der Sprache geachtet wer-
den sollte. Darüber hinaus haben sich im Umgang mit C++ etliche Konventionen etabliert. Die
wichtigsten Programmierrichtlinien und Konventionen werden hier stichwortartig aufgelistet:
 Die Dateiendung für Headerdateien sollte .hpp lauten (siehe Abschnitt 3.3.5).
 Die Dateiendung für Quelldateien sollte .cpp lauten (siehe Abschnitt 3.3.5).
Sandini Bib

576 Kapitel 10: Zusammenfassung

 Headerdateien sollten grundsätzlich von Präprozessor-Anweisungen umschlossen sein, die


Fehler durch mehrfache Einbindung vermeiden (siehe Abschnitt 3.3.7).
 Using-Direktiven sollten nie in Headerdateien verwendet werden (siehe Abschnitt 3.5.9 und
Abschnitt 4.3.5).
 Datenelemente von Klassen sollten, zumindest wenn sie keine fundamentalen Datentypen
sind, über Initialisierungslisten initialisiert werden (siehe Abschnitte 4.1.7 und 6.4.2).
 Operatoren sollten immer so implementiert werden, dass sie tun, was man grundsätzlich von
ihnen erwartet. Das entspricht in der Regel dem, was mit ihnen bei fundamentalen Datentypen
verbunden wird (siehe Abschnitt 4.2).
 Funktionen sollten, um das Anlegen von Kopien bei der Übergabe von Argumenten zu ver-
meiden, die Parameter als konstante Referenzen deklarieren (siehe Abschnitt 4.4).
 Konstante Objekte sollten mit const deklariert werden (siehe Abschnitt 4.4.3).
 Elementfunktionen, die ihre Objekte nicht verändern, sollten als Konstanten-Elementfunktio-
nen deklariert werden (siehe Abschnitt 4.4.4).
 Einleseoperatoren sollten ein Objekt nur dann ändern, wenn das Einlesen erfolgreich war
(siehe Abschnitt 4.5.4).
 Eingabeoperatoren einer Klasse sollten in der Lage sein, das mit dem Ausgabeoperator der
Klasse geschriebene Format zu lesen (siehe Abschnitt 4.5.4).
 Funktionen zur automatischen Typumwandlung sollten mit Vorsicht eingesetzt werden, da sie
zu unerwarteten Seiteneffekten und Mehrdeutigkeiten führen können (siehe Abschnitt 4.6.5).
 Ausnahmen sollten in Catch-Blöcken als konstante Referenzen deklariert werden (siehe Ab-
schnitt 4.7.2).
 Klassen mit virtuellen Funktionen sollten unbedingt einen virtuellen Destruktor besitzen (sie-
he Abschnitt 5.2.5).
 Elementfunktionen sollten bei der Vererbung nicht einmal virtuell und ein anderes Mal nicht-
virtuell deklariert werden (siehe Abschnitt 5.2.2).
 Bei der Implementierung von abgeleiteten Klassen sollte Folgendes beachtet werden (siehe
Abschnitt 5.2):
– Elementfunktionen, die in der Basisklasse nicht virtuell deklariert sind, sollten in abge-
leiteten Klassen nicht überschrieben werden.
– Zum Überschreiben von Elementfunktionen einer Basisklasse müssen die Parameter die
gleichen Typen besitzen.
– Überschriebene Funktionen sollten die gleichen Default-Argumente besitzen.
 Beim Design von Klassenhierarchien sollte Folgendes beachtet werden (siehe Abschnitt 5.5):
– Objekte abgeleiteter Klassen sollten weder in Bezug auf ihre Attribute noch in Bezug auf
ihre Operationen irgendwelchen Einschränkungen gegenüber Objekten der Basisklasse
unterliegen.
– Die neu hinzugekommenen Attribute dürfen die Bedeutung der geerbten Datenelemente
nicht verändern.
Sandini Bib

10.4 Sinnvolle Programmierrichtlinien und Konventionen 577

 Vermeide Vererbung (siehe Abschnitt 5.5.5).


 Eine Klasse braucht in der Regel einen Zuweisungsoperator, einen Copy-Konstruktor und
einen Destruktor oder nichts davon. Wenn eine der drei Operationen implementiert wird,
muss deshalb genau geprüft werden, ob nicht auch die anderen implementiert werden müssen
(siehe Abschnitt 6.1).
 Ein selbst implementierter Zuweisungsoperator sollte
– mit der Abfrage beginnen, ob ein Objekt sich selbst zugewiesen wird,
– das Objekt, dem ein anderes Objekt zugewiesen wurde, als (konstante) Referenz zurück-
liefern.
(siehe Abschnitt 6.1.5)
 Bei Template-Code sollten Deklarationen und Implementierungen auf getrennte Headerda-
teien aufgeteilt werden (siehe Abschnitt 7.6.1).
 Statt dynamische Felder (Arrays) sollte man STL-Container wie z.B. Vektoren verwenden
(siehe Abschnitt 9.1.1).
 Alle Datentypen und Klassen sollten mit Großbuchstaben anfangen.
 Funktionsnamen, Variablen und Objekte sollten mit Kleinbuchstaben anfangen.
 Alle Präprozessor-Konstanten sollten komplett aus Großbuchstaben bestehen.
 Kommerzielle Klassen, Bibliotheken und Komponenten sollten immer in einem eigenen Na-
mensbereich definiert werden.
 Der Programmierer sollte immer wissen, was er tut.
Sandini Bib
Sandini Bib

Literaturverzeichnis

In diesem Verzeichnis liste ich die Literatur auf, auf die ich mich in diesem Buch beziehe und die
ich für einen tieferen Einstieg in C++ für sinnvoll halte. Es handelt sich nicht um ein vollständiges
Literaturverzeichnis zu C++.
Diese Literaturliste wird auf der Webseite zum Buch unter
http://www.josuttis.de/cppbuch
auf dem Laufenden gehalten.

Der Standard
ISO
Information Technology – Programming Languages – C++
Document Number ISO/IEC 14882-1998
ISO/IEC, 1998
Dieses Dokument ist im Internet im Electronic Standards Store, ESS, unter
http://www.ansi.org
für $ 18,– als PDF-Datei erhältlich. In Deutschland ist der Standard bei DIN ebenfalls verfügbar,
kostet dort aber knapp DM 500,–.

Bücher zur Sprache


Bjarne Stroustrup
Die C++ Programmiersprache
Deutsche Ausgabe der Special Edition
Addison-Wesley, München, 2000

Andrew Koenig, Barbara E. Moo


Accelerated C++
Practical Programming by Example
Addison-Wesley, Reading, 2000

579
Sandini Bib

580 Literaturverzeichnis

Scott Meyers
Effektiv C++ programmieren
50 Wege zur Verbesserung Ihrer Programme und Entwürfe
Addison-Wesley, München, 1998

Scott Meyers
Mehr Effektiv C++ programmieren
35 neue Wege zur Verbesserung Ihrer Programme und Entwürfe
Addison-Wesley, München, 1998

Herb Sutter
Exceptional C++
47 technische Denkaufgaben, Programmierprobleme und ihre Lösungen
Addison-Wesley, München, 2000

Andrei Alexandrescu
Modern C++ Design
Generic Programming and Design Patterns Applied
Addison-Wesley, Reading, 2001

Krzysztof Czarnecki, Ulrich W. Eisenecker


Generative Programming
Methods, Tools, and Applications
Addison-Wesley, Reading, 2000

Bücher zur Standardbibliothek und STL


Nicolai M. Josuttis
The C++ Standard Library
A Tutorial and Reference
Addison-Wesley, Reading, 1999

Angelika Langer, Klaus Kreft


Standard C++ IOStreams and Locales
Addison-Wesley, Reading, 2000

Matthew H. Austern
Generic Programming and the STL
Using and Extending the C++ Standard Template Library
Addison-Wesley, Reading, 1998

Ulrich Breymann
Komponenten entwerfen mit der STL
Addison-Wesley, München, 1999
Sandini Bib

Literaturverzeichnis 581

Etwas veraltete, aber immer noch hilfreiche Bücher


Bjarne Stroustrup
The Design and Evolution of C++
Addison-Wesley, Reading, 1994

Margaret A. Ellis, Bjarne Stroustrup


The Annotated C++ Reference Manual (ARM)
Kommentiertes Reference-Manual
Addison-Wesley, Reading, 1990

C und C-Schnittstellen
Brian W. Kernighan, Dennis M. Ritchie
Programmieren in C
Hanser Verlag, 1983

X/Open Company Limited


X/Open Portability Guide
Volume 3: XSI System Interfaces and Headers
Prentice-Hall, 1989
Sandini Bib
Sandini Bib

Glossar

Dieses Glossar ist eine Zusammenstellung der wichtigsten Fachbegriffe dieses Buchs. Neben
kurzen Erklärungen enthält es sowohl die in diesem Buch verwendete deutsche als auch die
englische Bezeichnung, wie sie in der internationalen Literatur benutzt wird. Auf diese Weise
wird ein Übertragen der Begriffe in die jeweils andere Sprache erleichtert.

abgeleitete Klasse (derived class)


Klasse, die als Konkretisierung Eigenschaften einer Basisklasse übernimmt (erbt).
Andere Bezeichnungen für abgeleitete Klasse sind Unterklasse oder Subklasse.

abstrakter Datentyp (abstract data type)


Formale Beschreibung von einem bestimmten Typ von Datum (Objekt). Zu der Beschreibung
gehören alle Eigenschaften, wie etwa Attribute, Operationen und Randbedingungen.

abstrakte Klasse (abstract class)


Klasse, die nicht dafür vorgesehen ist, dass von ihr konkrete Objekte (Instanzen) erzeugt werden.
Abstrakte Klassen dienen dazu, gemeinsame Eigenschaften verschiedener Klassen unter einem
Oberbegriff zusammenzufassen.

ANSI
American National Standard Institute, eines der Komitees, die an der Sprachspezifikation von
C++ arbeiten.

Attribut (attribute)
Eigenschaft, Datum, Datenelement eines konkreten Objekts einer Klasse. Jedes konkrete Objekt
einer Klasse besitzt für seine Attribute Speicherplatz, dessen Belegung den Zustand des Objekts
widerspiegelt.

Basisklasse
Eine Klasse, von der abgeleitete Klassen Eigenschaften erben. Basisklassen sind häufig abstrakte
Klassen.
Andere Bezeichnungen für Basisklasse sind Oberklasse oder Superklasse.

Botschaft siehe Nachricht

583
Sandini Bib

584 Glossar

Cast
Bezeichnung für eine mögliche Form einer erzwungenen Typumwandlung, die auch schon in C
existiert.

class siehe Klasse

constructor siehe Konstruktor

Containerklasse (container class)


Klasse, die Objekte beschreibt, die andere Objekte aufnehmen und verwalten können.

Copy-Konstruktor (copy constructor)


Funktion, die aufgerufen wird, wenn ein neues Objekt als Kopie eines existierenden Objekts
angelegt wird. Dies geschieht insbesondere bei der Übergabe von Parametern und Rückgabe-
werten, wenn keine Referenzen verwendet werden.
In C++ ist für jede Klasse ein Default-Copy-Konstruktor vordefiniert, der komponentenweise
kopiert. Er kann durch eine eigene Implementierung ersetzt werden.

Datenelement
Attribut eines Objekts bzw. eine Komponente, die keine Elementfunktion ist.
Jedes konkrete Objekt einer Klasse besitzt für seine Datenelemente Speicherplatz, dessen
Belegung den Zustand des Objekts widerspiegelt.

Datenkapselung (encapsulation)
Konzept, über das der Zugriff auf Dinge bzw. Objekte eingeschränkt wird. Durch die Einschrän-
kung kann ein Objekt nur über eine wohldefinierte Schnittstelle manipuliert werden. Damit
können Schnittstellen besser verifiziert und Inkonsistenzen vermieden werden.
C++ unterstützt die Datenkapselung durch spezielle Zugriffsschlüsselwörter.

Default-Konstruktor (default constructor)


Funktion, die aufgerufen wird, wenn ein neues Objekt ohne Parameter zur Initialisierung angelegt
wird.

Destruktor (destructor)
Funktion, die für ein Objekt einer Klasse aufgerufen wird, wenn es zerstört wird.

Element siehe Komponente

Elementfunktion (member function)


Eine Elementfunktion ist eine Funktion, die für ein Objekt einer Klasse aufgerufen werden kann.
Sie wird als Komponente in der Klassenstruktur deklariert. Andere Bezeichnungen sind Mem-
berfunktion und als objektorientierter Begriff Methode.

Funktionstemplate (function template)


Schablone einer Funktion, die für unterschiedliche Datentypen oder Werte parametrisiert ist.
Sandini Bib

Glossar 585

Headerdatei (header file)


Datei, in der Konstanten, Variablen, Datentypen, Funktionen etc. deklariert werden, um in Pro-
grammen verwendet werden zu können. Als andere Bezeichnung wird auch Include-Datei ver-
wendet.

Include-Datei siehe Headerdatei

Initialisierungsliste
Zu einem Konstruktor gehörende Liste von Werten, mit der die Initialisierung eines Objekts
gesteuert wird. Die angegebenen Werte können zur Initialisierung von Komponenten einer Klasse
oder zum Aufruf des Konstruktors einer Basisklasse dienen.

Instanz (instance)
Objektorientierte Bezeichnung für ein konkretes Objekt einer Klasse. Eine Instanz besitzt die in
der Klasse angegebenen Daten (Attribute, Elemente) und kann mit den dafür definierten Opera-
tionen manipuliert werden.
In C++ und in diesem Buch ist der Begriff gleichbedeutend mit einem konkreten Objekt einer
Klasse. Im objektorientierten Sprachgebrauch wird mitunter zwischen Objekt und Instanz unter-
schieden. Objekt“ ist dann eher ein Oberbegriff für Klasse oder Instanz, da auch eine Klasse als

Objekt betrachtet werden kann.

ISO
International Organization for Standardization, eines der Komitees, die an der Sprachspezifika-
tion von C++ arbeiten.

Iterator
Objekt, das über Elemente iterieren kann.

Klasse (class)
Beschreibung einer bestimmten Art von Dingen oder Vorgängen (Objekten). In einer Klasse
werden alle Eigenschaften definiert, die für ein Objekt eine Rolle spielen. Dazu gehören sowohl
dessen Daten (Attribute, Elemente) als auch die durchführbaren Operationen (Methoden).
Man kann eine Klasse auch als abstrakten Datentyp oder als die Umsetzung eines abstrakten
Datentyps in eine Programmiersprache betrachten.
In C++ sind Klassen Struktur-Datentypen, deren Komponenten auch Funktionen sein können
und Zugriffsbeschränkungen unterliegen.

Klassentemplate (class template)


Schablone einer Klasse, die für unterschiedliche Datentypen oder Werte parametrisiert ist.
Klassentemplates werden konzeptionell auch als parametrisierbare Klassen bezeichnet.

Komponente (member)
Element einer Klasse. Eine Komponente kann ein Datenelement (Attribut) sein, das ein Objekt
der Klasse jeweils besitzt und den Zustand des Objekts widerspiegelt, oder eine Elementfunktion
Sandini Bib

586 Glossar

(Methode) sein, die beschreibt, welche Operationen für ein Objekt der Klasse aufgerufen werden
können.
Ein Spezialfall sind statische Komponenten, die nicht Eigenschaften eines einzelnen Objekts,
sondern Eigenschaften der ganzen Klasse beschreiben.

konkrete Klasse
Klasse, die im Gegensatz zu einer abstrakten Klasse dafür vorgesehen ist, dass von ihr konkrete
Objekte (Instanzen) erzeugt werden.

Konstanten-Elementfunktion (constant member function)


Elementfunktion, die auch für konstante und temporäre Objekte einer Klasse aufgerufen werden
kann, da sie die Komponenten des Objekts nicht verändert.

Konstruktor (constructor)
Elementfunktion einer Klasse, die beim Erzeugen eines Objekts der Klasse aufgerufen wird und
dazu dient, das neu angelegte Objekt in einen sinnvollen Startzustand zu bringen.

Konvertierungsfunktion (conversion function)


Elementfunktion, mit der definiert wird, wie ein Objekt einer Klasse in einen anderen Datentyp
umgewandelt werden kann. Dies kann auch durch eine automatische Typumwandlung gesche-
hen.

Mehrfachvererbung (multiple inheritance)


Die Fähigkeit, dass eine Klasse von mehr als einer Basisklasse abgeleitet werden kann.

Member siehe Komponente

Memberfunktion siehe Elementfunktion

Mengenklasse
Klasse, die dazu dient, eine Menge von Objekten zu verwalten. In C++ werden Mengenklassen
auch als Container bezeichnet.

message siehe Nachricht

Methode (method)
Objektorientierte Bezeichnung für die Implementierung einer Operation, die für ein Objekt einer
Klasse aufgerufen werden kann.
In C++ wird eine Methode als Elementfunktion (oder Memberfunktion) bezeichnet.

Nachricht (message)
Objektorientierte Bezeichnung für den Aufruf einer Operation für ein Objekt. Der Aufruf wird
als Nachricht (oder Botschaft) betrachtet, die an das Objekt gesendet wird und bei diesem Objekt
zu einer Reaktion führt.

Oberklasse siehe Basisklasse


Sandini Bib

Glossar 587

Objekt (object)
Zentraler Begriff der objektorientierten Programmierung. Ein Objekt ist ein Informationsträger,
der verschiedene Daten repräsentiert, die einen bestimmten Zustand (Wertebelegung) besitzen.
Ein Objekt kann dabei beliebig abstrakt, wie zum Beispiel ein Vorgang sein. Ein Objekt ist ein-
fach etwas“, das in einem Programm von Interesse ist, beschrieben wird und eine Rolle spielt.

Die Eigenschaften von Objekten (die Daten, die sie repräsentieren, und die Operationen, die
mit ihnen durchgeführt werden können) werden in Klassen beschrieben.

parametrisierbare Klasse siehe Klassentemplate

Persistenz (persistence)
Die Fähigkeit, dass Objekte mit ihren Zuständen beim Beenden und Neustarten eines Prozesses
erhalten bleiben. Persistente Objekte müssen also gesichert und wieder geladen werden können.

Pointer siehe Zeiger

Polymorphie (polymorphism)
Die Fähigkeit, dass für verschiedenartige Objekte die gleiche Operation aufgerufen werden kann,
die aber je nach Objekt zu unterschiedlichen Reaktionen führt.
Im objektorientierten Sprachgebrauch bedeutet das, dass verschiedenartige Objekte die glei-
che Nachricht erhalten können, diese aber unterschiedlich interpretiert wird, da die Klassen un-
terschiedliche Methoden für die Nachricht besitzen.
In C++ wird damit insbesondere die Fähigkeit bezeichnet, dass unterschiedliche Objekte zeit-
weise unter einer gemeinsamen Klasse als Oberbegriff verwaltet werden können, wobei ein und
derselbe Funktionsaufruf in Abhängigkeit von der tatsächlichen Klasse unterschiedliche Ele-
mentfunktionen aufruft.

Quelldatei (source file)


Die Datei, in der die eigentlichen Anweisungen eines Programms stehen.
Die Quelldateien eines Programms werden als Module (Übersetzungseinheiten) einzeln über-
setzt und zum ausführbaren Programm zusammengebunden. Sie können gemeinsame Headerda-
teien einbinden, in denen verwendbare Konstanten, Funktionen, Klassen usw. zentral deklariert
werden.

Referenz (reference)
Ein zweiter Name“, der für ein existierendes Objekt vergeben werden kann.

Referenzen dienen insbesondere dazu, bei der Übergabe von Parametern und Rückgabe-
werten keine Kopien anzulegen ( call-by-reference“).

Source-Datei siehe Quelldatei

Struktur (structure)
Zusammenstellung verschiedener Einzelteile zu einem gemeinsamen Gesamtobjekt.
Strukturen bieten eine Möglichkeit zu abstrahieren, indem z.B. ein Motor, eine Karosserie,
vier Räder usw. zu einem Auto zusammengefasst und verwaltet werden.
Sandini Bib

588 Glossar

Subklasse siehe abgeleitete Klasse

Superklasse siehe Basisklasse

Template
Schablone einer Funktion (Funktionstemplate) oder einer Klasse (Klassentemplate), die für un-
terschiedliche Typen verwendet werden kann.
Mit Templates müssen Funktionen oder Klassen, die sich nur in Hinblick auf verwendete
Datentypen unterscheiden, nur einmal implementiert werden.

Unterklasse siehe abgeleitete Klasse

Vererbung (inheritance)
Zurückführen eines Typs bzw. einer Klasse auf einen anderen Datentyp, indem nur die neu hin-
zugekommenen Eigenschaften zusätzlich definiert werden.
Die Vererbung bietet die Möglichkeit, gemeinsame Eigenschaften verschiedener Objektarten
zusammenzufassen und nur einmal zu implementieren. Damit wird auch das Abstraktionsmittel
des Oberbegriffs unterstützt, indem die gemeinsamen Eigenschaften der Objekte, die durch den
Oberbegriff ausgedrückt werden, als gemeinsame Basisklasse definiert werden, von denen die
einzelnen Varianten abgeleitet werden.

Verweis
Ein Oberbegriff für Referenz oder Zeiger, die beide dazu dienen, auf ein Objekt zu verweisen.

Whitespace
Gemeinsame Bezeichnung für die Standard-Trennzeichen unter C, C++ und UNIX. Dies sind in
der Regel das Zeichen Newline, das Leerzeichen und das Tabulator-Zeichen.

Zeiger (pointer)
Objekt bzw. Variable, dasbzw. die eine Programmadresse enthält und somit auf etwas anderes im
Programm zeigt. Ein Zeiger ist z.B. die Adresse einer Funktion oder eines Objekts, auf das dann
indirekt zugegriffen werden kann.
Sandini Bib

Index

~ 43, 211, 572 < 40, 573


- 40, 572 für STL-Container 526
-- 42, 171, 572 für Vektoren 521
-= 164 << 43, 205, 212, 479, 572
-> 86, 173, 541, 572 <= 40, 573
->* 561, 572 für STL-Container 526
, 44, 573 für Vektoren 521
:: 59, 145, 339, 421, 556, 572 = 41, 152, 170, 366, 388, 424, 573
! 40, 208, 209, 572 für Iteratoren 78
!= 40, 368, 573 für STL-Container 526
für Iteratoren 78 für Vektoren 522
für STL-Container 526 == 40, 367, 573
für Vektoren 521 für Iteratoren 78
?: 44, 573 für STL-Container 526
/ 40, 572 für typeid 328
. 572 für Vektoren 521
.* 561, 572 > 40, 573
^ 43, 573 für STL-Container 526
() 173, 543, 546, 572 für Vektoren 521
[ ] 112, 171, 377, 572 >= 40, 573
für Strings 103 für STL-Container 526
für Vektoren 523 für Vektoren 521
überladen 377 >> 43, 207, 212, 479, 572
* 40, 110, 173, 572 | 43, 218, 486, 504, 573
für Iteratoren 78 || 40, 573
*= 164
& 43, 110, 190, 211, 572, 573
&& 40, 367, 573 A
% 40, 275, 572
+ 40, 368, 572 ABC 331
++ 42, 171, 572 abgeleitete Klasse 261, G583
für Iteratoren 78 anwenden 276
+= 164 deklarieren 267, 294, 336
Sandini Bib

590 Index A

Funktionen der Basisklasse ANSI-C


überschreiben 280 Unterschied bei Parameter-Prototypen
mit unterschiedlichem Zugriff erben 142
299 ANSI-Komitee 10
Referenz der Basisklasse 282 Anweisung 27, 50
Zeiger der Basisklasse 281 Anwendung
ableiten abgeleiteter Klassen 276
von Friend-Funktionen 402 von Elementfunktionen 152
abort() 106, 128 von Operatorfunktionen 166
abs() 275 Anwendungsprogrammierer 4
Absolutwert berechnen 275 Anzahl
existierender Objekte 418
abstrakte Klasse 306, 310, G583
app 503
UML-Notation 307
argc 126
abstrakte Operation 307, 312
Argumente aus der Kommandozeile 126
abstrakter Datentyp 12, G583 argv 126
accumulate() 532 Arithmetik von Zeigern 113
adjacent_difference() 532 Array siehe Feld, 112
adjacent_find() 529 als Komponente 427
adjustfield 488 anlegen mit new 122
Adresse 110 Container 524
ausgeben 479 mehrdimensional 123
eines Objekts 343, 367 ohne Elemente 124, 364
einlesen 479 und Zeiger 113
Aggregation 348 von Objekten 152
Algorithmus 74, 86, 528 zerstören mit delete 122
copy() 90 Arrayoperator siehe Indexoperator
find() 89, 323 assign()
for_each() 541 für Vektoren 522
für mehrere Bereiche 90 assoziativer Container 81
für sortierte Bereiche 531 assoziatives Array 172
löschend 530 assoziatives Feld 172
at()
modifizierend 528
für Strings 103
mutierend 530
für Vektoren 523
nicht-modifizierend 528
ate 503
numerisch 532 atexit() 128
sortierend 530 Attribut 11, 134, G583
transform() 543 Aufruf
anlegen vom Copy-Konstruktor 189
eines Objekts 121, 149 von Elementfunktionen 152
von Arrays 122 von Operatorfunktionen 166
von Feldern 122 Aufruf-Operator 173, 543
anonymer Namensbereich 61 Aufzählungstyp 425, 568
ANSI G583 als statische Klassenkonstante 427
Sandini Bib

Index B 591

Aufzählungswert 425, 568 Stack-Unwinding 248


Ausgabe throw 101
Ausrichtung definieren 488 try 101
Feldbreite definieren 488 what() 105
Füllsymbol definieren 488 Ausnahmeklasse 100
Gleitkomma-Notation definieren 493 Ausnahmenliste 252
hexadezimal 490 Ausnahmenspezifikation 252
in C++ 202, 474 Ausnahmeobjekt
numerisch 490 erzeugen 101
oktal 490 verarbeiten 101
Operator << 205, 479 Ausrichtung
positives Vorzeichen 490 bei der Ausgabe 488
Standardfunktionen 482 automatische Typumwandlung 575
von Adressen 479 bei Templates 435
von bool 495 durch Friend-Funktion 227
Zahlensystem definieren 490 durch Konstruktor 224, 225
Auslassung 142 durch Konvertierungsfunktion 234, 391
Ausnahme 98, 241 in Objekt der Basisklasse 277
bad_alloc 101, 124 Probleme 235
bad_cast 101, 327
bad_exception 101, 252
B
bad_typeid 101, 328 back()
deklarieren 252 für Vektoren 440, 443, 523
domain_error 101 Back-Inserter 93
durch Ausnahmen 251 bad_alloc 101, 124
exception 101 bad()
failure 101 für Streams 211, 478
für Streams 478 badbit 210, 477
invalid_argument 101 bad_cast 101, 327
ios_base::failure 101 bad_exception 101, 252
Komponenten 391 bad_typeid 101, 328
length_error 101 basefield 491
logic_error 101 basic_filebuf 500
out_of_range 101 basic_fstream 500
overflow_error 101 basic_ifstream 500
Parameter 391 basic_ios 474
range_error 101 basic_iostream 474
runtime_error 101 basic_istream 474
unbehandelt 106 basic_istringstream 511
underflow_error 101 basic_ofstream 500
unerwartet 106, 252 basic_ostream 474
weitergeben 251 basic_ostringstream 511
Ausnahmebehandlung 20, 98, 101, 241, 242 basic_streambuf 474
catch 101 basic_stringbuf 511
mit Parametern 391 basic_stringstream 511
Sandini Bib

592 Index C

Basisklasse 261, G583 C


deklarieren 263, 284, 291
mehrfach ableiten 346
C++ 9
virtuell 339
Begriffe 22, 134
bedingte Bewertung 44
Designkriterien 9
Bedingung Namensgebung 22, 134
in Kontrollstrukturen 209, 383 Terminologie 22, 134
in Schleifen 208 Versionen 10
Konvertierungsfunktionen 382 call-by-reference 190, 191
before() call-by-value 189
für typeid 328 capacity()
beg 507 für Vektoren 521, 522
begin() case 46, 568
für Container 78 Cast siehe Typumwandlung, 153, G583
für Vektoren 524 catch 101
Begriffe 22, 134 C-Bindung 566
zur Vererbung 261 C-Code verwenden 566
Bereich <cctype> 372
bei Iteratoren 79 cerr 203, 215, 475
mehrere 90 umleiten 508
Bereichsoperator 145, 339 C-File siehe Quelldatei
binary 503 <cfloat> 533
binary_search() 532 char 36, 37
binden 54 numerische Limits 533
mit C-Code 566 char* 115
von Elementfunktionen 283 als Template-Parameter 454
Block 48 cin 203, 475
umleiten 508
Body 373
class siehe Klasse, 136, 238, G584
boolalpha 495
bei Templates 433
bool 36, 39
classic()
Ausgabe 495
für Locales 497
Ausgabeformat 495
clear() 211, 218, 517
Eingabe 495 für Streams 211, 478
Eingabeformat 495 für Vektoren 525
numerische Limits 533 <climits> 533
Boolesche Bedingung clog 203, 475
aus Stream 208 close() 504
in Schleifen 208 für Streams 504
Boolescher Wert 39 Collection 74
Botschaft siehe Nachricht, G583 Compiler 54, 435, 465
break 46, 48 verschiedene 566
Bruch 132 constant member function siehe
Bücher 579 Konstanten-Elementfunktion
Sandini Bib

Index D 593

const 194 Dateizugriff 500


Position 200 Datenelement 134, G584
const_cast 72, 572 Begriff 23
const_iterator 456 Datenkapselung 15, 134, G584
constructor siehe Konstruktor, G584 Sicherheit 266
Container 74, 431 Datentyp
begin() 78 Begriff 23
Elemente ausgeben 456 für Fehlerobjekte 250
Element finden 323 fundamental 36
end() 78 Maximum 533
insert() 83, 86 Minimum 533
Operationen 526 polymorph 326
Containerklasse G584 Wertebereiche 533
continue 48 Wert-Semantik 66
conversion function siehe dec 491, 492
Konvertierungsfunktion Default-Argument 178
copy() 90, 529 bei Templates 448
copy_backward() 529 bei Vererbung 297
copyfmt() default 46
für Streams 487, 508 Default-Copy-Konstruktor 186
Copy-Konstruktor 186, 364, 424, G584 Default-Konstruktor 140, G584
ableiten 407, 408 Default-Zuweisungsoperator 153, 170
aufrufen 189 #define 52, 57, 58, 136, 194
Default 186 Definition
privat 388 von Elementfunktionen 147
und Zuweisung 187 von Manipulatoren 485
verbieten 388 von Operatorfunktionen 159
verwenden 189 Deklaration 185
count() 529 implizit vs. explizit 227
count_if() 529 in For-Schleife 185
cout 203, 222, 475 mit const 200
umleiten 508 von abgeleiteten Klassen 267, 294, 336
__cplusplus Konstante 567 von Ausnahmen 252
<cstddef> 555 von Basisklassen 263, 284, 291
<cstdlib> 127, 128, 129, 144 von Instanzen 149
c_str() von Klassen 135
für Strings 72, 382 von Laufvariablen 185
C-String 72, 115 von Objekten 149
cur 507 von Operatorfunktionen 156
von Referenzen 190
D von Zeigern 111
dekrementieren 42
data() Dekrement-Operator 42, 171
für Strings 382 delete[] 122, 572
Dateiendungen 54 nothrow 548
Sandini Bib

594 Index E

selbst implementieren 554 dynamische Speicherverwaltung 120


überladen 554 in Objekten 356
delete 120, 121, 572
kontra free() 120 E
nothrow 548 eigene Speicherverwaltung 554
NULL 122 Ein-/Ausgabe siehe I/O
selbst implementieren 554 einfache Vererbung 262, 263
über Basisklasse 290 Einfügen von Elementen 525
überladen 554 Eingabe
denorm_min() 534 Feldbreite 489
Dereferenzier-Operator 173 hexadezimal 491
Design in C++ 202, 474
bei Vererbung 348 oktal 491
von Fehlerklassen 257 Operator >> 207, 479
design by contract 330 Standardfunktionen 480
Designkriterien von C++ 9 von Adressen 479
Destruktor 151, 359, 364, 423, G584 von bool 495
ableiten 407 Zahlensystem definieren 491
Aufruf durch Ausnahme 248 eingebettete Klasse 428
für Vektoren 520 Einlesen
mit Ausnahmen 251 eines Strings 370, 489
ohne Speicherplatzfreigabe aufrufen von Objekten in einer Schleife 209
548 Einschränkungen
verketteter Aufruf 273 bei Operatorfunktionen 169
virtuell 290 Element siehe Komponente, 134, G584
digits 533 Elementfunktion 65, 134, 139, G584
digits10 533 als Operator 156
do 46 als Parameter 561
domain_error 101 als Template 457
double 36 anwenden 152
numerische Limits 533 aufrufen 152
Do-While-Schleife 46 automatische Typumwandlung 224
Downcast 326 Begriff 23
dynamic_cast 326, 572 definieren 147
dynamische Komponenten 356 dynamisch binden 283
bei Konstanten 377 für Konstanten 195
Konstruktor 362 für Konstanten und Variablen 379
Konvertierungsfunktionen 380 implementieren 147
Vererbung 397 mit unterschiedlichem Zugriff erben
dynamisches Array 299
Container 524 Namenskonflikt mit Komponente 401
dynamisches Binden Neuimplementierung in abgeleiteten
von Elementfunktionen 283 Klassen 280
dynamisches Feld Parameter der Basisklasse 289
Container 524 spät binden 283
Sandini Bib

Index F 595

statisch 420 Exception-Handling siehe


statisch binden 283 Ausnahmebehandlung, 98, 101, 241,
this 164 242
verwenden 152 exceptions()
virtuell 283 für Streams 479
Zugriff bei Parametern 289 Executable 54
Elementklasse 428 exit() 128
#elif 56 Exit-Code 128
Ellipse 142 EXIT_FAILURE 128
EXIT_SUCCESS 128
else 45
explicit 226
#else 56
explizite Instantiierung 465
Elternklasse siehe Basisklasse, 261
explizite Qualifizierung
empty() bei Templates 435, 514
für Vektoren 440, 521 explizite Typumwandlung 153
encapsulation siehe Datenkapselung, 15 bei Templates 435
end 507 in Basisklasse 402
end() in tatsächliche Klasse 326
für Container 78 mit const_cast 72
für Vektoren 524 mit dynamic_cast 326
#endif 52, 56, 136 mit lexical_cast 514
endl 207, 484, 485 mit reinterpret_cast 385
End-Of-Stream-Iterator 95 mit static_cast 385, 402
ends 207, 484, 517 export 470
Endungen von Dateien 54 external linkage 566
enum 36, 425, 427, 568 extern 566
eof() externe Bindung 566
für Streams 211, 478, 514
EOF Konstante 210, 477 F
eofbit 210, 477
epsilon() 534 fail()
für Streams 211, 478
equal() 529
failbit 210, 477
equal_range() 532
failure 101
erase()
Fallunterscheidung
für Vektoren 525 kontra Polymorphie 325
Error-Handler 559 Fallunterscheidungen 45
erzeugen false 39
eines Objekts 121, 149 FarbString 399
von Arrays 122 fclose() 374
von Feldern 122 Fehlerbehandlung 98, 241
erzwungene Typumwandlung siehe explizite bei Templates 470
Typumwandlung, 153 Fehlerklasse 100, 250
Exception siehe Ausnahme, 98, 241 deklarieren 244
für Streams 478 Design 257
Sandini Bib

596 Index G

Hierarchien 253 For-Schleife 47


mit Komponenten 250 Laufvariable deklarieren 185
Vererbung 253 free() 120
Fehlermeldung Freispeicher 120
in C++ 203, 215 friend 229, 238
Fehlerobjekt Bewertung 239
erzeugen 101 und virtual 403
verarbeiten 101 Friend-Funktion 227
Feld 112 ableiten 402
als Komponente 427 überschreiben 402
anlegen mit new 122 und virtual 403
Container 524 vermeiden 234
mehrdimensional 123 zur Typumwandlung 227
ohne Elemente 124, 364 Friend-Klasse 238
und Zeiger 113 front()
von Objekten 152 für Vektoren 523
zerstören mit delete 122 Front-Inserter 93
Feldoperator siehe Indexoperator fstream 500
filebuf 500 <fstream> 500
fill() 529 fundamentaler Datentyp 36
für Streams 488 Funktion 50
fill_n() 529 als Komponente 139
Filter 209, 483 als Objekt 543
find() 89, 323, 529 als Operator 156
find_end() 529 als Template siehe Funktionstemplate,
find_first_of() 529 433
find_if() 470, 529 Aufbau 28
fixed 493, 494 Default-Argumente 178
flags() deklarieren mit Ausnahmen 252
für Streams 486, 487 inline 180
float 36 Name-Mangling 566
numerische Limits 533 ohne festgelegte Parameter 142
floatfield 493 Parameterübergabe 189
<float.h> 533 Referenzparameter 191
flush 207, 484 rein virtuell 312, 330
flush() überladen 141
für Streams 482 Funktionsaufruf-Operator 173, 543
fopen() 374 Funktionsobjekt 543
for 47 Funktionstemplate 433, G584
Bedingung 209 Funktionszeiger 559
Laufvariable deklarieren 185 G
for_each() 529, 541
formatierte Ein-/Ausgabe 486 Garbage Collection 554
Formatierung gcount()
von bool 495 für Streams 481
Sandini Bib

Index H 597

Geltungsbereich siehe Gültigkeitsbereich für I/O 476


Generalisierung 348 für Streams 476
generate() 529 <iomanip> 488
generate_n() 529 <iosfwd> 476
GeoGruppe 319 <iostream> 144, 205, 476
GeoObj 306, 310 <istream> 476
get() 216 <limits> 37, 533
für Streams 370, 480 <limits.h> 533
getchar() 99 <new> 549
getenv() 127 <ostream> 476
getline() <sstream> 511
für Streams 480 <stdlib.h> 127, 128, 129
getloc() <streambuf> 476
für Streams 497 <strstream> 516
globale Operatorfunktion 168 <typeinfo> 328
globales Objekt 151 Header-File siehe Headerdatei
Glossar 583 hexadezimale Ein-/Ausgabe 490
good() 210 hex 491, 492, 511
für Streams 211, 478 H-File siehe Headerdatei
goodbit 210, 477 Hierarchie
Größe von Fehlerklassen 253
von Vektoren 521 von Klassen 262
Gültigkeitsbereich
Namensbereich 59 I
von Klassen 424, 428
i18n 496
H Identität 343
testen 366
halboffen 79 IDs 418
Handle 373 If-Anweisung 45
has_denorm 534 if 45
has_denorm_loss 534 #if 56
has_infinity 534 if
has_quiet_NaN 534 Bedingung 209
has_signaling_NaN 534 #ifdef 56
Headerdatei 54, 148, G584 #ifndef 52, 57, 136
bedingte Übersetzung 57 ifstream 500
<cctype> 372 ignore()
<cfloat> 533 für Streams 481, 486
<climits> 533 imbue() 497
<cstddef> 555 für Streams 497
<cstdlib> 127, 128, 129, 144 Implementierung
<float.h> 533 von Elementfunktionen 147
<fstream> 500 von Klassen 16, 142
für C und C++ 567 von Manipulatoren 485
Sandini Bib

598 Index I

von new 552 int 36


von Operatorfunktionen 159 numerische Limits 533
Implementierungsvererbung 353 Interface 330
implizite Typumwandlung siehe internal 488, 489
automatische Typumwandlung Internationalisierung 496
in 503 invalid_argument 101
#include 29, 56, 144 I/O
Include-Datei siehe Headerdatei, G585 Filtergerüst 209, 483
includes() 532 formatiert 486
Indexoperator 171, 377 für Klassen 212
überladen 377 Headerdateien 476
Indizierungsoperator siehe Indexoperator in C++ 202, 203, 474, 483
infinity() 534 Klassen 474
information hiding 15 Manipulator 207, 484
inheritance siehe Vererbung mit Streams 203
inhomogene Menge 318 Operatoren 479
Initialisierung Operatoren überladen 212
bei Vererbung 271 Standardbibliothek 474
durch Konstruktor 271, 362, 412 Standardkanal umleiten 508
von Konstanten 194, 416 <iomanip> 488
von Objekten 139 ios::adjustfield 488
von Referenzen 416 ios::app 503
von statischen Komponenten 421 ios::ate 503
Initialisierungsliste 145, 271, 412, 421, ios 474
G585 ios::badbit 210, 477
für Basis-Konstruktor 271 ios_base 474
für Komponenten 412 ios_base::failure 101, 479
inkrementieren 42 ios::basefield 491
für Iteratoren 78 ios::beg 507
Inkrement-Operator 42, 171 ios::binary 503
inline 180 ios::boolalpha 495
und Templates 435 ios::cur 507
und virtual 287 ios::dec 491
Inline-Funktion 180, 273 ios::end 507
virtuell 287 ios::eofbit 210, 477
inner_product() 532 ios::failbit 210, 477
inplace_merge() 532 ios::fixed 493
insert() ios::floatfield 493
für Container 83, 86 <iosfwd> 476
für Vektoren 525 ios::goodbit 210, 477
Inserter 92 ios::hex 491
Insert-Iterator 92 ios::in 503
instantiieren 432, 434 ios::internal 488
explizit 465 ios::left 488
Instanz 12, 64, G585 ios::nocreate 503
Sandini Bib

Index K 599

ios::noreplace 503 Inserter 92


ios::oct 491 von Vektoren 524
ios::out 503 Zuweisung 78
ios::right 488 Iterator-Adapter
ios::scientific 493 Back-Inserter 93
ios::showbase 492 Front-Inserter 93
ios::showpoint 493 für Streams 94
ios::showpos 490 Inserter 92
ios::skipws 496
iostream 474 K
<iostream> 144, 205, 476 Kapazität
iostream_withassign 508 von Vektoren 521
ios::trunc 503 KBruch 266
ios::unitbuf 496 Kind-Klasse siehe abgeleitete Klasse, 261
ios::uppercase 490 Klasse 12, 13, 64, 132, G585
is-a-Beziehung 262, 277 abfragen 326
umgehen 302 abstrakt 306, 310
is_bounded 533 als Friend-Klasse 238
is_exact 533 als Proxy 389
is_iec559 533 als Template siehe Klassentemplate,
is_integer 533 440
is_modulo 533 anwenden 147
ISO G585 Anzahl existierender Objekte 418
ISO-Komitee 10 Array als Komponente 427
is_open() Ausnahmebehandlung 241
für Streams 504 bad_alloc 101, 124
is_signed 533 bad_cast 101, 327
is_specialized 533 bad_exception 101, 252
ist-ein-Beziehung 262 bad_typeid 101, 328
istream 202, 474 basic_filebuf 500
Iterator 94 basic_fstream 500
<istream> 476 basic_ifstream 500
Istream-Iterator 94 basic_ios 474
istream_withassign 508 basic_iostream 474
istringstream 511 basic_istream 474
istrstream 516 basic_istringstream 511
Iterator 78, 456, G585 basic_ofstream 500
!= 78 basic_ostream 474
* 78 basic_ostringstream 511
++ 78 basic_streambuf 474
= 78 basic_stringbuf 511
== 78 basic_stringstream 511
Bereich 79 Bruch 132
für Streams 94 deklarieren 135
inkrementieren 78 delete[] 555
Sandini Bib

600 Index K

delete 555 ostream 202, 474


domain_error 101 ostream_withassign 508
dynamische Komponenten 356 ostringstream 511
dynamische Speicherverwaltung 356 ostrstream 516
eigene Speicherverwaltung 554 out_of_range 101
eingebettet 428 overflow_error 101
Exception-Handling 241 parametrisierbar 440
failure 101 Person 410
FarbString 399 polymorph 319
Fehlerbehandlung 241 range_error 101
Feld als Komponente 427 runtime_error 101
filebuf 500 Schnittstelle 16
fstream 500 Stack 440
GeoGruppe 319 statische Komponenten 418
GeoObj 306, 310 statische Konstante 427
Gültigkeitsbereich 424, 428 streambuf 475
ifstream 500 string 356
implementieren 16, 142 stringbuf 511
invalid_argument 101 stringstream 511
I/O-Funktionen 212 strstream 516
ios 474 strstreambuf 516
ios_base 474 type_info 328
ios_base::failure 101, 479 UML-Notation 153
iostream 474 underflow_error 101
iostream_withassign 508 vector siehe Vektor
istream 202, 474 verwendet Klasse 410
istream_withassign 508 Wert-Semantik 66
istringstream 511 wfilebuf 500
istrstream 516 wfstream 500
KBruch 266 wifstream 500
konkret 306 wios 474
Koord 307 wiostream 474
length_error 101 wistream 474
locale 496, 497 wistringstream 511
logic_error 101 wofstream 500
lokal 428 wostream 474
new[] 555 wostringstream 511
new 555 wstringbuf 511
numeric_limits 533 wstringstream 511
Objekt anlegen 121, 149 Klassenhierarchie 17, 262
Objekt zerstören 121, 151 der Datei-Stream-Klassen 500
ofstream 500 der Stream-Klassen 474
ohne Komponenten 244 der String-Stream-Klassen 511
Operationen im Überblick 574 Klassenmethode 420
Operatoren 156 Klassenstruktur 136
Sandini Bib

Index L 601

Klassentemplate 440, G585 ohne Speicherplatzanforderung aufrufen


Klassenvariable siehe statische Komponente 548
Koenig-Lookup 184 privat 369
Kommentar 27 und Zuweisung 187
kompilieren 54 Vererbung 270
Komponente 11, 134, G585 verketteter Aufruf 271
als Konstante 416, 418 zur Typumwandlung 225
als Parameter 560 Kontrollstrukturen 45
als Referenz 417 Konventionen 575
als Template 457 Konvertierungsfunktion 234, 391, G586
Begriff 23 für Bedingungen 382
in Ausnahmen 391 für dynamische Komponenten 380
Namenskonflikt mit Elementfunktion Koord 307
401 Kopieren verbieten 388
Objekt 410
statisch 418
L
Komponentenzeiger 560 LANG 499
konkrete Klasse 306, G586 Laufvariable
Konkretisierung 349 deklarieren 185
Konsistenz Laufzeit 399
kontra Laufzeit 166 kontra Konsistenz 166
Konstante 193 Laufzeit-Typinformationen 326
als Komponente 416, 418 lazy evaluation 385
als Variable 201, 385 left 488, 489
__cplusplus 567 length_error 101
dynamische Komponenten 377 lexical_cast 514
EOF 210, 477 lexicographical_compare() 529
in C 58 <limits> 37, 533
NULL 112, 364 <limits.h> 533
und Zeiger 199 Limits von Datentypen 533
Konstanten-Elementfunktion 195, G586 linken 54
Konstruktor 139, 412, G586 Linker 435, 465
Aufruf durch new 121 Liskov Substitution Principle 353
Aufruf für Basisklassen 271 List-Container siehe Liste
Aufruf für Komponenten 412 Liste 80
Aufruf über Initialisierungsliste 271, Literaturverzeichnis 579
412 locale 496
der Basisklasse 270 classic() 497
dynamische Komponenten 362 imbue() 497
für Komponenten 410 Klasse 497
für Objekte der Basisklasse 278 name() 499
für Vektoren 520 Typ 497
implementieren 145 löschende Algorithmen 530
Initialisierung 271, 362, 412 Löschen von Elementen 525, 530
nichtöffentlich 311 logic_error 101
Sandini Bib

602 Index M

lokale Klasse 428 showpoint 494


lokale Variable 60 showpos 491
long 36 skipws 496
numerische Limits 533 unitbuf 496
lower_bound() 532 uppercase 491
ws 207, 370, 484, 514
M max() 533
max_element() 529
main() 28, 126, 151, 567 max_exponent10 534
Rückgabewert 128 max_exponent 533
make_heap() 531 Maximum
make_pair() 86 von Datentypen 533
Makro 58 max_size()
in C++ 181 für Vektoren 521, 522
malloc() 120 Mehrdeutigkeit
Mangling 566 bei Typumwandlung 235
Manipulator 207, 484 mehrdimensionales Array 123
Arbeitsweise 484 mehrdimensionales Feld 123
boolalpha 495 Mehrfachvererbung 262, 333, G586
dec 492 Problem der Identität 343
definieren 485 Member siehe Komponente, 134, G586
endl 207, 484 Memberfunktion siehe Elementfunktion, 65,
ends 207, 484, 517 134, G586
fixed 494 memcmp() 367
flush 207, 484 memcpy() 363
hex 492 Mengenklasse 74, 431, G586
implementieren 485 Mengenverarbeitung 74
internal 489 merge() 529, 532
left 489 message siehe Nachricht, G586
noboolalpha 495 Methode 13, 65, 134, G586
noshowbase 492 einer Klasse 420
noshowpoint 494 min() 275, 533
noshowpos 491 min_element() 529
noskipws 496 min_exponent10 534
nounitbuf 496 min_exponent 533
nouppercase 491 Minimum
oct 492 von Datentypen 533
resetiosflags() 488 Minimum berechnen 275
right 489 mismatch() 529
scientific 494 modifizierende Algorithmen 528
setfill() 489 Modul 50
setiosflags() 488 multiple inheritance siehe
setprecision() 494 Mehrfachvererbung
setw() 489, 500 mutable 386
showbase 492 mutierende Algorithmen 530
Sandini Bib

Index N 603

N noshowpoint 494
noshowpos 491
Nachricht 13, 65, G586 noskipws 496
name() nothrow 548
für Locales 499 nothrow_t 548
für typeid 328 nounitbuf 496
Name-Mangling 566 nouppercase 491
Namensbereich 22, 59, 137, 183 npos
anonym 61 für Strings 68
Koenig-Lookup 184 nth_element() 531
using 183 NULL Konstante 112, 364
Using-Deklaration 183 und delete 122
Using-Direktive 94, 183 numeric limits 533
Namensgebung 22, 134 numeric_limits 533
Namenskonflikt 59 numerische Algorithmen 532
bei Mehrfachvererbung 338 numerische Ausgabe 490
Komponente und Elementfunktion 401 numerische Limits 533
namespace siehe Namensbereich, 59, 137
nested class 428
O
new[] 122, 572 Oberbegriff 304
nothrow 548 Oberklasse siehe Basisklasse, 261, G586
selbst implementieren 554 Objekt 11, 64, G587
überladen 554 Adresse 343, 367
new 120, 121, 572 als Komponenten 410
für Array ohne Elemente 124, 364 als Objekt der Basisklasse 277, 343
für Feld ohne Elemente 124, 364 Anzahl 418
Implementierung 552 Arrays 152
kontra malloc() 120 Begriff 22
mit Parametern 557 Felder 152
nothrow 548 global 151
Placement-New 548 IDs 418
Rückgabewert 549 statisch 151
selbst definierte Parameter 557 zerstören 121, 151, 248, 290
selbst implementieren 554 Objektdatei 54
Speicherplatzgröße ändern 557 oct 491, 492, 511
überladen 554 ofstream 500
<new> 549 oktale Ein-/Ausgabe 490
New-Handler 549, 559 op= 41, 164, 573
next_permutation() 531 open() 504
nicht-modifizierende Algorithmen 528 für Streams 504
noboolalpha 495 Operation
nocreate 503 abstrakt 307, 312
nontype Template-Parameter 452, 544 Operator 39, 571
noreplace 503 / 40, 572
noshowbase 492 -- 42, 171, 572
Sandini Bib

604 Index P

- 40, 572 sizeof 44, 572


-= 164 static_cast 385, 402, 572
-> 86, 173, 541, 572 typeid 328, 572
->* 561, 572 überladen 158
, 44, 573 operator 157
:: 59, 145, 339, 421, 556, 572 Operatorfunktion 156
! 40, 208, 209, 572 anwenden 166
!= 40, 368, 573 aufrufen 166
?: 44, 573 deklarieren 156
. 572 Einschränkungen 169
.* 561, 572 global 168
^ 43, 573 implementieren 159
() 173, 543, 546, 572 überladen 158
[ ] 112, 171, 377, 572 verwenden 166
~ 43, 211, 572 ostream 202, 474
* 40, 110, 173, 572 Iterator 94
*= 164 <ostream> 476
& 43, 110, 190, 211, 572, 573 Ostream-Iterator 94
&& 40, 367, 573 ostream_withassign 508
% 40, 275, 572 ostringstream 511
+ 40, 368, 572 ostrstream 516
++ 42, 171, 572 out 503
+= 164 out_of_range 101
< 40, 573 overflow_error 101
<< 43, 205, 212, 479, 572 overloading siehe überladen
<= 40, 573
P
= 41, 152, 170, 366, 388, 424, 573
== 40, 367, 573 package 137
> 40, 573 pair 86
>= 40, 573 Paket 137
>> 43, 207, 212, 479, 572 Parameter
| 43, 218, 486, 504, 573 als Referenz 191
|| 40, 573 der Basisklasse 289
automatische Typumwandlung 224 für Fehler 250
const_cast 72, 572 für new 557
delete[] 122, 572 für Templates 452, 544
delete 121, 572 Parameter-Prototypen 142
dynamic_cast 326, 572 Unterschied zu ANSI-C 142
für Klassen 156 Parameterübergabe 189
für Konstanten und Variablen 379 von Streams 215
im Überblick 571 parametrisierbare Klasse siehe
new[] 122, 572 Klassentemplate, 440, G587
new 121, 572 partial_sort() 531
op= 41, 164, 573 partial_sort_copy() 531
reinterpret_cast 385, 572 partial_sum() 532
Sandini Bib

Index Q 605

partielle Spezialisierung 447 pure virtual siehe rein virtuell


partition() 531 push_back()
past-the-end 78 für Vektoren 440, 525
peek() 216 push_heap() 531
für Streams 481 put()
Persistenz G587 für Streams 482
Person 410 putback()
Placement-New 548 für Streams 481
Pointer siehe Zeiger, 110, G587
Smart-Pointer 539 Q
polymorpher Datentyp 326
Quelldatei 54, 148, G587
Polymorphie 18, 261, 304, G587
quiet_NaN() 534
kontra Fallunterscheidung 325
mit Templates 460 R
Sprachmittel 306
pop_back() radix 533
für Vektoren 440, 443, 525 random_shuffle() 531
pop_heap() 531 range_error 101
positionieren rbegin()
in Dateien 506 für Vektoren 524
pos_type rdbuf() 517
für Streams 506 für Streams 508
Präprozessor 29, 55, 136 rdstate() 211, 218
Makro 58 für Streams 211, 478
precision() read()
für Streams 493 für Streams 480
prev_permutation() 531 readsome()
private 137, 269, 369 für Streams 481
Copy-Konstruktor 388 realisieren 434
Konstruktor 369 realloc() 557
Vererbung 299 Reallokierung 522
Zuweisungsoperator 388 Record 11
Programm Reference-Counting 373
Abbruch 128 Referenz 189, 190, G587
Beginn 28, 151 als Komponente 390, 417
Ende 28, 128, 151 als Konstante 193
Parameter 126 als Parameter 191
Programmierrichtlinien 575 als Rückgabewert 377
protected 265, 269 auf Objekte abgeleiteter Klassen 282
Vererbung 299 Referenz-Semantik 66
Prototypen 142 reine Zugriffsdeklarationen 299
Proxy-Klasse 389 reinterpret_cast 385, 572
Prozedur 50 rein virtuelle Funktion 312
public 138, 269 Default-Implementierung 330
Pufferung 496 Rekursion erkennen 387
Sandini Bib

606 Index S

remove() 530 Schlüsselwort


remove_copy() 530 bool 36, 39
remove_copy_if() 530 break 46, 48
remove_if() 530 case 46
rend() catch 101
für Vektoren 524 char 36
replace() 529 class 136, 238
replace_copy() 529 const 194
replace_if() 529 const_cast 72
reserve() continue 48
für Vektoren 521, 522 default 46
resetiosflags() 488 delete 120
resize() 91 do 46
für Vektoren 525 double 36
rethrow 251 dynamic_cast 326
return 29, 189 else 45
Returntyp siehe Rückgabetyp enum 36, 425, 427, 568
Returnwert siehe Rückgabewert explicit 226
reverse() 531 export 470
reverse_copy() 531 extern 566
Richtlinien 575 false 39
right 488, 489 float 36
rotate() 531 for 47
rotate_copy() 531 friend 229, 238
round_error() 534 if 45
round_style 534 inline 180
RTTI 326 int 36
und Design 328 long 36
Rückgabetyp 28 mutable 386
bei Vererbung 289 namespace 59
Rückgabewert 189 new 120
von main() 128 operator 157
runtime_error 101 private 137, 269, 369
runtime type information 326 protected 265, 269
public 138, 269
S reinterpret_cast 385
return 29, 189
Schablone siehe Template short 36
Schleife 46 signed 36
abbrechen 48 sizeof 45
Bedingung 208 static 418
Laufvariable deklarieren 185 static_cast 39, 385
neu bewerten 48 struct 138
Stream-Zustand testen 208 switch 46
zum Einlesen von Objekten 209 template 433, 441
Sandini Bib

Index S 607

this 164 showbase 492


throw 101, 251 showpoint 493, 494
true 39 showpos 490, 491
try 101 Sicherheit 266
typedef 445 signaling_NaN() 534
typeid 328 signed 36
typename 433, 456 size()
union 568 für Vektoren 521
unsigned 36 sizeof 44, 45, 572
using 183 size_t 555
virtual 283, 312, 341 skipws 496
void 36 Smart-Pointer 539
volatile 570 sort() 531
wchar_t 475 sort_heap() 531
while 46 sortieren
Schnittstelle von Bereichen 530
einer Klasse 16 sortierende Algorithmen 530
zu anderen Prozessen 563 Source-Datei siehe Quelldatei, G587
zu anderen Sprachen 563, 566 Source-File siehe Quelldatei
zu C 566 späte Auswertung 385
scientific 493, 494 spätes Binden
search() 529 von Elementfunktionen 283
search_n() 529 Speicherplatz
seekg() Größe ändern 557
für Streams 506 initialisieren 548
seekp() 511, 517 Mangel 549
für Streams 506 Speicherverwaltung
sequenzieller Container 81 dynamisch 120
set_difference() 532 selbst definiert 554
setf() spezialisieren von Templates 445, 536
für Streams 486, 487 Sprachumgebung 496
setfill() 489 <sstream> 511
set_intersection() 532 stable_partition() 531
setiosflags() 488 stable_sort() 531
set_new_handler() 549 Stack 440
setprecision() 494 Stack-Unwinding 248
setstate() Standard 10
für Streams 478 Ausnahmeklassen 101
set_symmetric_difference() 532 Standard-Ausgabekanal 30, 203
set_terminate() 106 umleiten 508
set_unexpected() 252 Standard-Ausnahme
set_union() 532 auslösen 258
setw() 489, 500 Standard-Eingabekanal 30, 203
short 36 umleiten 508
numerische Limits 533 Standard-Fehlerausgabekanal 30, 203
Sandini Bib

608 Index S

umleiten 508 Ausnahmen 478


Standardfunktionen Ausrichtung definieren 488
für Streams 480 bad() 211, 478
Standard-I/O 474 badbit 210, 477
Standardisierung von C++ 10 basefield 491
Standardoperatoren beg 507
für I/O 479 Beispiele 483
Standard Template Library siehe STL, 74 binary 503
Statement siehe Anweisung boolalpha 495
static 60, 418 clear() 211, 478
static_cast 39, 385, 402, 572 close() 504
statische Elementfunktion 420 copyfmt() 487, 508
Aufruf 420 cur 507
statische Komponente 418 Dateizugriff 500
bei Templates 445 dec 491
Initialisierung 421 Elementfunktionen 480
statische Konstante 427 end 507
statische Polymorphie 460 eof() 211, 478, 514
statisches Binden eofbit 210, 477
von Elementfunktionen 283 Exceptions 478
statisches Objekt 151 exceptions() 479
Status fail() 211, 478
von Streams 210, 222, 476 failbit 210, 477
stderr 203 Feldbreite definieren 488
stdin 203 fill() 488
<stdlib.h> 127, 128, 129 fixed 493
stdout 203 Flags abfragen 486
STL 74 flags() 486, 487
Algorithmen 74, 86, 528, 543 Flags setzen 486
Container 74, 526 floatfield 493
Container-Elemente ausgeben 456 flush() 482
Iteratoren 78, 456 Formatdefinitionen 486
Vektor 74, 520 Formatierung von bool 495
str() Füllsymbol definieren 488
für char*-Streams 516 gcount() 481
strcat() 116, 118, 368 get() 370, 480
strchr() 118 getline() 480
strcmp() 116, 118, 436 getloc() 497
strcpy() 116, 118, 368 Gleitkomma-Notation definieren 493
Stream 202, 203, 370, 474 good() 211, 478
adjustfield 488 goodbit 210, 477
als Parameter 215 Headerdateien 476
als Rückgabewert 215 hexadezimal 490
app 503 hex 491
ate 503 i18n 496
Sandini Bib

Index S 609

ignore() 481, 486 testen 208, 217, 483


imbue() 497 tie() 482
in 503 Trennzeichen überlesen 496
internal 488 trunc 503
Internationalisierung 496 unitbuf 496
is_open() 504 unsetf() 486, 487
Klassen 474 uppercase 490
left 488 verknüpfen 482
Manipulator 207, 484 Verwendung 203
nocreate 503 width() 488
noreplace 503 write() 482
oct 491 Zahlensystem definieren 490
oktal 490 Zustand 210, 476
open() 504 Zustand testen 208, 217, 222, 483
Operator ! 478 streambuf 475
Operator void* 478 <streambuf> 476
out 503 Stream-Iterator 94
peek() 481 streampos
positionieren 506 für Streams 506
pos_type 506 String 63
precision() 493 [ ] 103
Pufferung 496 als Template-Parameter 454
put() 482 at() 103
putback() 481 einlesen 370, 489
rdbuf() 508 in C 115
rdstate() 211, 478 kommerzielle Klassen 372
read() 480 Stream-Funktionen 511
readsome() 481 string 63, 356
right 488 c_str() 72, 382
scientific 493 data() 382
seekg() 506 npos 68
seekp() 506 stringbuf 511
setf() 486, 487 stringstream 511
setstate() 478 String-Stream-Klassen 511
showbase 492 strlen() 118
showpoint 493 strncat() 118
showpos 490 strncmp() 118
skipws 496 strncpy() 118
Standardfunktionen 480 Stroustrup 9
Standardkanal umleiten 508 strstream 516
Status 210, 476 <strstream> 516
streampos 506 strstreambuf 516
Strings 511 struct 138, 568
tellg() 506 Struktur 11, G587
tellp() 506 in C++ 138
Sandini Bib

610 Index T

Subklasse siehe abgeleitete Klasse, G588 und inline 435


Subroutine 50 von Template 445
Subskriptionsoperator siehe Indexoperator Werte-Parameter 452, 544
Superklasse siehe Basisklasse, G588 template 433, 441
swap() Template-Funktion siehe Funktionstemplate
für Vektoren 522 Template-Klasse siehe Klassentemplate
swap_ranges() 529 temporäres Objekt 153
Switch-Anweisung 46 terminate() 106
switch 46, 568 Terminologie 22, 134
Symbol bei Templates 432
intern 472 zur Vererbung 261
system() 129 this 164
Systemprogrammierer 4 throw 101, 251
Ausnahmenliste 252
T Ausnahmenspezifikation 252
throw specification 252
tellg() tie()
für Streams 506 für Streams 482
tellp() tinyness_before 534
für Streams 506 transform() 529, 543
Template 21, 431, 536, G588 traps 534
Compilation-Modell 470 Trennzeichen 588
Default-Parameter 448 überlesen 496
Elementfunktion 457 true 39
explizite Instantiierung 465 trunc 503
explizite Qualifizierung 435, 514 try 101
export 470 Typ
Fehlerbehandlung 470 abfragen 326
Funktion siehe Funktionstemplate, 433 Begriff 23
Klasse siehe Klassentemplate, 440 bool 36
kompilieren 465 char 36
Komponenten 457 double 36
mehrere Parameter 435 enum 36
Parameter 452, 544 float 36
partielle Spezialisierung 447 int 36
Polymorphie 460 polymorph 326
praktische Hinweise 435, 465 size_t 555
spezialisieren 445, 536 void 36
statische Komponente 445 typedef 85, 445
String-Parameter 454 typeid 328, 572
Terminologie 432 == 328
typename 456 before() 328
Typumwandlung 435 name() 328
überladen 436 type_info 328
übersetzen 465 <typeinfo> 328
Sandini Bib

Index U 611

typename 433, 456, 457 Vererbung 262


Typinformationen zur Laufzeit 326 underflow_error 101
Typüberprüfung 305 unexpected() 252
Typumwandlung union 568
automatisch 224, 575 unique() 530
bei Templates 435 unique_copy() 530
durch Friend-Funktion 227 unitbuf 496
durch Konstruktor 225 unsetf()
durch Konvertierungsfunktion 234 für Streams 486, 487
erzwungen 153 unsigned 36
explizit 153 Unterklasse siehe abgeleitete Klasse, 261,
implizit 224, 575 G588
in Basisklasse 402 Unterprogramm 50
in Objekt der abgeleiteten Klasse 278 upper_bound() 532
in Objekt der Basisklasse 277 uppercase 490, 491
in tatsächliche Klasse 326 using 94, 183
Konstante in Variable 201 Deklaration 183
mit const_cast 72 Direktive 94, 183
mit dynamic_cast 326 Using-Deklaration 183
mit lexical_cast 514 Using-Direktive 94, 183
mit reinterpret_cast 385
mit static_cast 385, 402
V
Probleme 235 Variable
lokal 60
U Wert-Semantik 66
vector siehe Vektor
überladen 141 Vektor 74, 321, 440, 520
durch Funktionen als Parameter 484 [ ] 523
für Konstanten und Variablen 379 als dynamisches Feld (Array) 524
von delete[] 554 assign() 522
von delete 554 at() 523
von I/O-Operatoren 212 back() 440, 523
von new[] 554 begin() 524
von new 554 capacity() 521, 522
von Operatoren 158 clear() 525
von Templates 436 Destruktor 520
zum Setzen und Abfragen 401 Element finden 323
überschreiben Elementfunktionen 520
von Friend-Funktionen 402 Elementzugriff 523
von Funktionen der Basisklasse 280 empty() 440, 521
Übersetzen 54 end() 524
Übersetzungseinheit 50 erase() 525
Umgebungsvariable 127 front() 523
UML 16, 307 Größe 521
Klasse 153 insert() 525
Sandini Bib

612 Index W

Iteratoren 524 virtual 283, 312, 341


Kapazität 521 Basisklasse 339
Konstruktor 520 Laufzeit 287
max_size() 521, 522 und friend 403
Operationen 520 und inline 287
pop_back() 440, 525 virtuell
push_back() 440, 525 oder nicht virtuell 399, 402
rbegin() 524 virtuelle Elementfunktion 283, 306
rend() 524 inline 287
reserve() 521, 522 virtueller Destruktor 290
resize() 525 void 36
size() 521 volatile 570
swap() 522 Vorzeichen ausgeben 490
Vergleichsoperatoren 521
Zuweisungen 522
W
Verbund 568 wahlfreier Zugriff
Vererbung 17, 261, G588 auf Dateien 506
Begriffe 261 wcerr 475
bei Fehlerklassen 253 wchar_t 475
Design-Fallen 348 numerische Limits 533
Destruktor 273 wcin 475
dynamische Komponenten 397 wclog 475
einfach 262, 263 wcout 475
Initialisierung 271 Wert als Template-Parameter 452
Konstruktor 270 Wertebereich
Liskov Substitution Principle 353 von Datentypen 533
Mächtigkeit 262 Wert-Semantik 66
mehrfach 262, 333 wfilebuf 500
private 299 wfstream 500
protected 299 what()
richtig 291 für Ausnahmen 105
Rückgabetyp 289 while 46
Terminologie 261 Bedingung 209
UML-Notation 262 While-Schleife 46
vermeiden 352 Whitespace G588
Zugriff auf geerbte Komponenten 269 überlesen 496
Versionen width()
von C++ 10 für Streams 488
Verweis 189, G588 wifstream 500
Verwendung wios 474
vom Copy-Konstruktor 189 wiostream 474
von Elementfunktionen 152 wistream 474
von Operatorfunktionen 166 wistringstream 511
von Streams 203 wofstream 500
Vielgestaltigkeit siehe Polymorphie wostream 474
Sandini Bib

Index Z 613

wostringstream 511 von Feldern 122


write() von Objekten 121, 151, 248, 290
für Streams 482 Zugriff
ws 207, 370, 484, 514 auf geerbte Komponenten 269
wstringbuf 511 auf interne Daten 377
wstringstream 511 auf private Komponenten 137, 229
bei Parametern 289
Z in Elementfunktionen 289
Zeichen 37 Zugriffsdeklarationen 299
Zeiger 110, 190, 321, G588 Zugriffsschlüsselwort 137
als Konstanten 199 private 137
Arithmetik 113 protected 265
auf Funktionen 559 public 138
auf Komponente 560 Zustand
auf Konstanten 199 von Streams 210, 222, 476
auf Objekte abgeleiteter Klassen 281 Zuweisung 41, 152, 424
ausgeben 479 ableiten 407
deklarieren 111 für Iteratoren 78
einlesen 479 für Vektoren 522
Smart-Pointer 539 modifizierend 41, 164
und Array 113 und Konstruktor 187
und Felder 113 verbieten 388
und Referenz 189 wertverändernd 41, 164
Zeigerkonstanten 199 Zuweisungsoperator 365, 366
Zeiger-Operator 173 ableiten 408
Zerstörung Default 153, 170
von Arrays 122 privat 388
Sandini Bib

Copyright
Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen
eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir
lediglich als persönliche Einzelplatz-Lizenz zur Verfügung!
Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und
Informationen, einschliesslich
• der Reproduktion,
• der Weitergabe,
• des Weitervertriebs,
• der Platzierung im Internet,
in Intranets, in Extranets,
• der Veränderung,
• des Weiterverkaufs
• und der Veröffentlichung
bedarf der schriftlichen Genehmigung des Verlags.
Insbesondere ist die Entfernung oder Änderung des vom Verlag vergebenen
Passwortschutzes ausdrücklich untersagt!

Bei Fragen zu diesem Thema wenden Sie sich bitte an: info@pearson.de

Zusatzdaten
Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die
Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige Leistung
des Verlags. Der Rechtsweg ist ausgeschlossen.
Hinweis
Dieses und viele weitere eBooks können Sie rund um die Uhr
und legal auf unserer Website

http://www.informit.de
herunterladen

Das könnte Ihnen auch gefallen