C++ Wochenend Crashkurs (Stephen R. Davis) de
C++ Wochenend Crashkurs (Stephen R. Davis) de
C++
Wochenend Crashkurs
Titelei 31.01.2001 12:16 Uhr Seite II
Titelei 31.01.2001 12:16 Uhr Seite III
C++
Wochenend Crashkurs
Stephen R. Davis
ISBN 3-8266-0692-2
Alle Rechte, auch die der Übersetzung, vorbehalten. Kein Teil des Werkes darf in irgendeiner Form
(Druck, Fotokopie, Mikrofilm oder einem anderen Verfahren) ohne schriftliche Genehmigung des
Verlages reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder
verbreitet werden.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk
berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im
Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher
von jedermann benutzt werden dürften.
Copyright © by mitp-Verlag,
ein Geschäftsbereich der verlag moderne industrie Buch AG & Co.KG, Landsberg
Original English language edition text and art copyright © 2000 by IDG Books Worldwide, Inc.
All rights reserved including the right of reproduction in whole or in part in any form.
This edition published by arrangement with the original publisher IDG Books Worldwide, Inc..
Foster City, California, USA.
Printed in Germany
Stephen R. Davis
Wenn der 43-jährige Vater und Hausmann nicht gerade Fahrrad fährt oder seinen Sohn zu einem
Taekwondo-Wettkampf begleitet, arbeitet und lebt Stephen R. Davis in Greenville, Texas als Pro-
grammierer mit Leib und Seele.
Beim MITP-Verlag ist neben dem C++ Wochenend Crashkurs auch sein Buch C++ für Dummies
erschienen.
Titelei 31.01.2001 12:16 Uhr Seite VI
Titelei 31.01.2001 12:16 Uhr Seite VII
Vorwort
M
it dem C++ Wochenend Crashkurs erlernen Sie an einem einzigen – zugegebenermaßen
arbeitsreichen – Wochenende die Programmsprache C++: in 30 Sitzungen à 30 Minuten,
also 15 Stunden – von Freitagabend bis Sonntagnachmittag.
Am Ende jeden Teils bekommen Sie Gelegenheit für eine Pause und eine Rekapitulation dessen, was
Sie erlernt haben. Viel Glück!
VIII Vorwort
Zu unserem Glück ist C++ eine standardisierte Sprache. Das American National Standards Insti-
tute (ANSI) und die International Standards Organisation (ISO) sind sich einig darüber, was C++ ist.
Sie haben eine detaillierte Beschreibung der Programmiersprache C++ erstellt. Diese standardisierte
Sprache ist unter dem Namen ANSI oder ISO C++, oder einfach Standard-C++, bekannt.
Standard-C++ unterliegt nicht der Kontrolle einer einzelnen Firma, wie z.B. Microsoft oder Sun.
Die Gemeinschaft der Programmierer, die Standard-C++ verwenden, ist nicht abhängig von einem
Software Giganten. Außerdem halten sich die Firmen an den Standard, selbst Microsoft’s Visual C++
hält sich streng an den C++-Standard.
Die Programme im C++ Wochenend Crashkurs können mit jeder Implementierung von Standard-
C++ übersetzt werden.
1.3 Wer.
Der C++ Wochenend Crashkurs richtet sich an Anfänger bis hin zu Lesern auf mittlerem Level.
Es werden keine Vorkenntnisse im Bereich Programmierung und Programmierkonzepte beim
Leser vorausgesetzt. Die ersten Sitzungen erklären anhand realer Beispiele auf nicht-technische
Weise, was Programmierung ist.
Dieses Buch ist auch gut geeignet für den Hobbyprogrammierer. Die vielen Beispiele demon-
strieren Programmiertechniken, die in modernen Programmen eingesetzt werden.
Der ernsthafte Programmierer oder Student muss C++ in seinem Köcher der Programmierfähig-
keiten haben. Fundiertes Wissen in C++ zu haben, kann den Unterschied machen, ob man einen
bestimmten Job bekommt oder nicht.
1.4 Was.
Der C++ Wochenend Crashkurs ist mehr als nur ein Buch. Er ist ein vollständiges Entwicklungspaket.
Eine CD-ROM enthält die berühmte GNU C++-Umgebung.
Sie benötigen ein Textprogramm, wie z.B. Microsoft Word, um Texte bearbeiten zu können. Und
sie brauchen eine C++-Entwicklungsumgebung, um Programme in C++ zu erzeugen und auszufüh-
ren.
Viele Leser werden bereits eine eigene Entwicklungsumgebung besitzen, wie z.B. Microsoft’s
Visual C++. Für die Leser, die noch über keine Entwicklungsumgebung verfügen, enthält der C++
Wochenend Crashkurs das GNU C++.
Titelei 31.01.2001 12:16 Uhr Seite IX
Vorwort IX
GNU C++ ist kein abgespecktes oder laufzeitbeschränktes Programm. Das GNU C++-Paket ist
eine vollwertige Entwicklungsumgebung. Der C++ Wochenend Crashkurs enthält vollständige Anlei-
tungen zur Installation von GNU C++ und Visual C++.
1.5 Wie.
Der C++ Wochenend Crashkurs ist für ein Wochenende gedacht. Fangen Sie am Freitagabend an,
dann sind Sie am Sonntagnachmittag fertig.
Dieses Ein-Wochen-Format ist
• ideal für Studenten, die mit ihren Mitstudenten gleichziehen möchten,
• ideal für den Programmierer, der seine Fähigkeiten erweitern will, und
• ideal für jeden, der C++ lernen möchte, während die Kinder bei der Oma sind.
Natürlich können Sie das Buch auch etwas langsamer durcharbeiten, wenn Sie das lieber tun.
Jeder Teil von 4 bis 6 Sitzungen kann separat gelesen werden.
Der Leser sollte jede der 30 Sitzungen innerhalb von 30 Minuten durcharbeiten können. Zeit-
markierungen helfen, die Zeit im Auge zu behalten.
Am Ende jeder Sitzung befinden sich Fragen, die dem Leser zur Selbsteinschätzung dienen sollen.
Eine Menge schwierigerer Fragen, die helfen sollen, das Erlernte zu festigen, befindet sich am Ende
jeden Buchteils.
1.6 Überblick.
Der C++ Wochenend Crashkurs präsentiert seine Sitzungen in Gruppen von 4 bis 6 Kapiteln, die in 6
Buchteile organisiert sind.
X Vorwort
Dieses und ähnliche Icons zeigen Ihnen, wie weit Sie bereits in der Sitzung
gekommen sind.
20 Min.
Es gibt eine Reihe von Icons, die Sie auf spezielle Informationen hinweisen sollen:
=
= =
= Dieses Zeichen weist auf Informationen hin, die Sie im Gedächtnis behalten
Hinweis sollten. Sie werden Ihnen später noch nützlich sein.
!
Tipp
Hier erhalten Sie hilfreiche Hinweise darauf, wie Sie eine Sache am besten aus-
führen oder erfahren eine Technik, die Ihre Programmierung einfacher macht.
Vorwort XI
.
CD-ROM
Dieses Zeichen weist auf Informationen hin, die Sie auf der CD-ROM finden, die
diesem Buch beiliegt.
Inhalt
Freitag
Teil 1 – Freitagabend ..............................................................2
Inhalt XIII
Samstag
Teil 2 – Samstagmorgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
XIV Inhalt
Inhalt XV
XVI Inhalt
Inhalt XVII
XVIII Inhalt
Sonntag
Teil 5 – Sonntagmorgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
Inhalt XIX
XX Inhalt
ta g
✔ Fr ei
g
st a
✔S a m
g
n ta
Son✔
C++ Lektion 01 31.01.2001 12:07 Uhr Seite 2
Freitagabend
Teil 1
Lektion 1.
Was ist Programmierung?
Lektion 2.
Ihr erstes Programm in Visual C++
Lektion 3.
Ihr erstes C++-Programm mit GNU C++
Lektion 4.
C++-Instruktionen
C++ Lektion 01 31.01.2001 12:07 Uhr Seite 3
Was ist
Programmierung? 1
Lektion
.Checkliste.
✔ Die Prinzipien des Programmierens erlernen
✔ Lernen, ein menschlicher Prozessor zu sein
✔ Lernen, einen Reifen zu wechseln
D
er Webster’s New World College Dictionary bietet mehrere Definitionen
des Substantivs »Programm« an. Die erste lautet »eine Proklamation, ein
Prospekt, oder eine Inhaltsangabe«. Das trifft das, worum es uns geht,
nicht so richtig. Erst die sechste Definition passt besser: »eine logische Sequenz
codierter Instruktionen, die Operationen beschreiben, die von einem Computer
30 Min.
ausgeführt werden sollen, um ein Problem zu lösen oder Daten zu verarbeiten«.
Nach kurzem Nachdenken habe ich festgestellt, dass diese Definition ein
wenig restriktiv ist. Zum einen weiß man in der Phrase »eine logische Sequenz codierter Instruktio-
nen ...«, nicht, ob die Instruktionen gekrypted, d.h. kodiert sind oder nicht, und der Begriff »logisch«
ist sehr einschränkend. Ich selber habe schon Programme geschrieben, die nicht sehr viel gemacht
haben, bevor sie abgestürzt sind. In der Tat stürzen die meisten meiner Programme ab, bevor sie
irgend etwas tun. Das scheint nicht sehr logisch zu sein. Zum anderen »... um ein Problem zu lösen
oder Daten zu verarbeiten«: Was ist mit dem Steuerungscomputer meiner Klimaanlage im Auto? Er
löst kein Problem, das mir bewusst ist. Ich mag die Klimaanlage so wie sie ist – anschalten und aus-
schalten auf Knopfdruck.
Das größte Problem mit Webster’s Definition ist die Phrase »die von einem Computer ausgeführt
werden sollen ...«. Ein Programm hat nicht unbedingt etwas mit Computern zu tun. (Es sei denn, Sie
zählen das konfuse Etwas zwischen Ihrem Stereokopfhörer dazu. In diesem Fall können Sie behaup-
ten, dass alles, was Sie tun, etwas mit Computern zu tun hat.) Ein Programm kann ein Leitfaden sein,
für etwas, das wenigstens ein Körnchen Intelligenz besitzt – sogar für mich. (Vorausgesetzt, ich liege
nicht unterhalb dieser Körnchengrenze.) Lassen Sie uns betrachten, wie wir ein Programm schreiben
würden, um ein menschliches Verhalten anzuleiten.
C++ Lektion 01 31.01.2001 12:07 Uhr Seite 4
4 Freitagabend
(Ich weiß, dass Rad und Reifen nicht das Gleiche sind – Sie entfernen nicht den Reifen vom Auto,
sondern das Rad. Zwischen diesen beiden Begriffen hin und her zu springen, ist verwirrend. Neh-
men Sie daher einfach an, dass das Wort »Reifen« in diesem Beispiel synonym zu »Rad« verwendet
wird.)
Das ist das grundlegende Programm. Ich könnte mit diesen Anweisungen alle meine platten Rei-
fen ersetzen, die ich bisher hatte. Um genauer zu sein, ist dies ein Algorithmus. Ein Algorithmus ist
eine Beschreibung von auszuführenden Schritten, in der Regel auf einem hohen Abstraktionsniveau.
Ein Algorithmus ist für ein Programm wie eine Beschreibung der Prinzipien des Fernsehens für die
Schaltkreise in einem Fernseher.
Auto
Reifen
Radmutter
Wagenheber
Schraubenschlüssel
C++ Lektion 01 31.01.2001 12:07 Uhr Seite 5
(Die beiden letzten Objekte wurden in unserem Algorithmus zum Reifenwechseln nicht erwähnt,
aber sie waren in Phrasen wie »ersetzen Sie den Reifen« enthalten. Das ist das Problem mit Algorith-
Teil 1 – Freitagabend
men – so vieles ist nicht explizit ausgesprochen).
Lassen Sie uns weiterhin annehmen, dass unser Prozessor folgende Verben versteht:
Lektion 1
greifen
bewegen
loslassen
drehen
Schließlich muss unsere Prozessorperson zählen und einfache Entscheidungen treffen können.
Das ist alles, was unsere Prozessorperson fürs Reifenwechseln versteht. Alle weiteren Anweisun-
gen rufen bei ihr nur einen ratlosen Blick hervor.
6 Freitagabend
1. Schraubenschlüssel greifen
2. Schraubenschlüssel auf Radmutter bewegen
3. Solange Radmutter nicht gelöst
4. <
5. Schraubenschlüssel gegen Uhrzeigersinn drehen
6. >
7. Schraubenschlüssel von Radmutter entfernen
8. Schraubenschlüssel loslassen
Das Programm durchläuft die Schritte 1 und 2 wie zuvor. Schritt 3 ist vollständig
anders. Der Prozessor wird angewiesen, die Schritte, die in den auf Schritt 3 folgen-
den Klammern eingeschlossen sind, so lange auszuführen, bis eine bestimmte
Bedingung erfüllt ist. In diesem Fall, bis die Radmutter gelöst ist. Sobald die Rad-
mutter gelöst ist, fährt die Prozessorperson bei Schritt 7 fort. Die Schritte 3 bis 6
10 Min.
werden als Schleife bezeichnet, da der Prozessor sie wie einen Kreis durchläuft.
Diese Lösung ist viel besser, da sie keine Annahmen in Bezug auf die Anzahl der Umdrehungen
macht, die benötigt werden, um eine Radmutter zu lösen. Das Programm ist außerdem nicht ver-
schwenderisch, da keine unnötigen Umdrehungen ausgeführt werden. Und das Programm weist
den Prozessor nie an, eine Schraube zu drehen, die nicht mehr da ist.
So schön das Programm ist, hat es doch noch ein Problem: Es entfernt nur eine einzige Radmut-
ter. Die meisten Autos haben fünf Radmuttern pro Reifen. Wir können die Schritte 2 bis 7 fünfmal
wiederholen, einmal für jede Radmutter. Immer fünf Radmuttern zu entfernen, funktioniert aber
auch nicht. Kleinwagen haben manchmal nur vier Radmuttern, größere Autos und kleine Lastwagen
haben meist sechs Radmuttern.
Das folgende Programm erweitert die letzte Lösung auf alle Radmuttern eines Reifens, unabhän-
gig von der Anzahl der Radmuttern.
1. Schraubenschlüssel greifen
2. Für jede Radmutter
3. <
4. Schraubenschlüssel auf Radmutter bewegen
5. Solange Radmutter nicht gelöst
6. <
7. Schraubenschlüssel gegen Uhrzeigersinn drehen
8. >
9. Schraubenschlüssel von Radmutter entfernen
10. >
11. Schraubenschlüssel loslassen
Das Programm fängt wie immer mit dem Greifen des Schraubenschlüssels an. Danach durchläuft
das Programm eine Schleife zwischen den Schritten 2 bis 10 über alle Radmuttern. Schritt 9 entfernt
den Schraubenschlüssel von der Radmutter, bevor in Schritt 2 mit der nächsten Radmutter fortge-
fahren wird.
Beachten Sie, wie die Schritte 5 bis 8 wiederholt werden, bis die Radmutter gelöst ist. Die Schrit-
te 5 bis 8 werden als innere Schleife bezeichnet, während die Schritte 2 bis 10 als äußere Schleife
bezeichnet werden.
Das gesamte Programm besteht aus einer Kombination gleichartiger Lösungen für jeden der
sechs Schritte im ursprünglichen Programm.
C++ Lektion 01 31.01.2001 12:07 Uhr Seite 7
1.4 Computerprozessoren.
Teil 1 – Freitagabend
Ein Computerprozessor arbeitet sehr ähnlich wie ein menschlicher Prozessor. Ein Computerprozes-
sor folgt wörtlich einer Kette von Kommandos, die mit einem endlichen Vokabular erzeugt wurde.
Einen Reifen von einem Auto zu entfernen, scheint eine einfache Aufgabe zu sein, und unsere
Lektion 1
Prozessorperson benötigt 11 Anweisungen, um ein einzelnen Reifen zu wechseln. Wie viele Anwei-
sungen werden benötigt, um die vielen tausend Pixel auf dem Bildschirm zu bewegen, wenn der
Benutzer die Maus bewegt?
Im Gegensatz zu einem menschlichen Prozessor sind Prozessoren aus Silizium
extrem schnell. Ein Pentium III-Prozessor kann einige 100 Millionen Schritte pro
Sekunde ausführen. Es bedarf einiger Millionen Anweisungen, um ein Fenster zu
bewegen, aber weil der Computerprozessor so schnell ist, bewegt sich das Fenster
flüssig über den Bildschirm.
0 Min.
Zusammenfassung.
Dieses Kapitel hat grundlegende Prinzipien der Programmierung eingeführt anhand eines Beispiel-
programms, um einen sehr dummen, aber außerordentlich folgsamen Mechaniker in die Kunst des
Reifenwechselns einzuweisen.
• Computer tun das, was Sie ihnen sagen – nicht weniger, aber natürlich auch nicht mehr.
• Computerprozessoren haben ein kleines, aber wohldefiniertes Vokabular.
• Computerprozessoren sind clever genug, einfache Entscheidungen zu treffen.
Selbsttest.
1. Nennen Sie Substantive, die ein »menschlicher Prozessor« verstehen müsste, um Geschirr abzu-
waschen.
2. Nenne Sie einige Verben.
3. Welche Art von Entscheidungen müsste ein Prozessor treffen können?
C++ Lektion 02 31.01.2001 12:07 Uhr Seite 8
2
Lektion Ihr erstes Programm
in Visual C++
Checkliste.
✔ Ihr erstes C++-Programm in Visual C++ schreiben
✔ Aus Ihrem C++-Code ein ausführbares Programm erzeugen
✔ Ihr Programm ausführen
✔ Hilfe bei der Programmierung bekommen
K
apitel 1 handelte von Programmen für Menschen. Dieses und das nächste
Kapitel beschreiben, wie Sie einen Computer in C++ programmieren. Dieses
Kapitel behandelt die Programmierung mit Visual C++, während sich das
nächste Kapitel mit dem frei verfügbaren GNU C++ befasst, das auch auf der beilie-
genden CD-ROM zu finden ist.
30 Min.
=
= =
=
Haben Sie keine Angst vor den Bezeichnungen Visual C++ und GNU C++. Beide
Compiler stellen Implementierungen des C++-Standards dar. Jeder der Compiler
Hinweis kann jedes Programm in diesem Buch übersetzen.
Das Programm, das wir schreiben wollen, konvertiert eine Temperatur, die der Benutzer in Grad
Celsius eingibt, in Grad Fahrenheit.
=
= =
=
Visual C++ ist nicht auf der beiliegenden CD-ROM enthalten. Sie müssen Visual
C++ separat erwerben, entweder als Bestandteil von Visual Studio, oder als Ein-
Hinweis
zelprodukt. Den sehr guten GNU C++-Compiler finden Sie auf der CD-ROM.
C++ Lektion 02 31.01.2001 12:07 Uhr Seite 9
Teil 1 – Freitagabend
Ein C++-Programm beginnt sein Leben als Textdatei, die C++-Anweisungen enthält. Ich werde Sie
Schritt für Schritt durch das erste Programm führen.
Starten Sie Visual C++. Für Visual Studio 6.0, klicken Sie auf »Start«, gefolgt von den Menüoptio-
Lektion 2
nen »Programme« und »Microsoft Visual Studio 6.0«. Von dort wählen Sie »Microsoft Visual C++
6.0« aus.
Visual C++ sollte zwei leere Fenster zeigen, die mit Ausgabe und Arbeitsbereich bezeichnet sind.
Wenn noch andere Fenster gezeigt werden, oder die beiden genannten Fenster nicht leer sind, dann
hat jemand bereits Visual C++ auf Ihrem Computer benutzt. Um all dies zu schließen, wählen Sie
»Datei« gefolgt von »Arbeitsbereich schließen« aus.
Erzeugen Sie eine leere Textdatei durch Klicken auf das kleine Icon ganz links in der Leiste, wie in
Abbildung 2.1 zu sehen ist.
Abbildung 2.1: Sie beginnen damit, ein C++-Programm zu schreiben, indem Sie eine neue Textdatei
anlegen.
Machen Sie sich keine Gedanken über das Einrücken – es kommt nicht darauf
!
Tipp
an, ob eine Zeile zwei oder drei Zeichen eingerückt ist. Groß- und Kleinschrei-
bung sind jedoch wichtig. Für C++ sind »Betrügen« und »betrügen« nicht
gleich.
C++ Lektion 02 31.01.2001 12:07 Uhr Seite 10
10 Freitagabend
.
CD-ROM
Sie können sich das Programm Conversion.cpp von der beiliegenden CD-ROM
herunterziehen.
Geben Sie das folgende Programm genau wie hier abgedruckt ein. (Oder kopieren Sie es sich von
der CD-ROM.)
//
// Programm konvertiert Temperaturen von Grad Celsius
// nach Grad Fahrenheit
// Fahrenheit = Celsius * (212 – 32)/100 + 32
//
#include <stdio.h>
#include <iostream.h>
int main(int nNumberofArgs, char* pszArgs[])
<
// Eingabe der Temperatur in Grad Celsius
int nCelsius;
cout << »Temperatur in Grad Celsius:«;
cin >> nCelsius;
return 0;
>
Speichern Sie die Datei unter dem Namen Conversion.cpp. Das Standardverzeichnis ist eines der
Verzeichnisse von Visual Studio. Ich bevorzuge es, in ein von mir selbst generiertes Verzeichnis zu
wechseln, bevor ich die Datei speichere.
Teil 1 – Freitagabend
können, Visual C++ eingeschlossen, sind nichts anderes als Dateien, die Maschinenanweisungen
enthalten.
Lektion 2
=
= =
= Es ist möglich, Programme direkt in Maschinensprache zu schreiben. Dies ist
Hinweis aber viel schwieriger, als das gleiche Programm in C++ zu schreiben.
Die wichtigste Aufgabe von Visual C++ ist, Ihr C++-Programm in eine ausführbare Datei zu über-
setzen. Der Vorgang der Übersetzung in eine ausführbare .EXE-Datei wird als Erzeugen bezeichnet.
Dieser Prozess wird manchmal auch als Kompilieren bezeichnet (es gibt einen Unterschied dieser bei-
den Begriffe, der aber hier nicht relevant ist). Der Teil des C++-Paketes, der die Übersetzung des Pro-
gramms ausführt, wird als Compiler bezeichnet.
Um Ihr Programm Conversion.cpp zu erzeugen, klicken Sie auf »Erstellen« im Menü »Erstellen«.
(Nein, ich habe nicht gestottert.) Visual C++ antwortet darauf mit der Warnung, dass Sie noch kei-
nen Arbeitsbereich angelegt haben, was immer das ist. Dies ist in Abbildung 2.2 zu sehen.
Abbildung 2.2: Ein Arbeitsbereich wird benötigt, bevor Visual C++ Ihr Programm erzeugen kann.
C++ Lektion 02 31.01.2001 12:08 Uhr Seite 12
12 Freitagabend
Klicken Sie auf »Ja«, um eine Arbeitsbereichsdatei zu erzeugen und mit dem Erzeugungsprozess
fortzufahren.
Die .cpp-Quelldatei ist nichts anderes, als eine Textdatei, ähnlich zu dem, was
=
= =
=
Sie etwa mit WordPad erzeugen würden. Der Arbeitsbereich Conversion.pwd,
der von Visual C++ angelegt wird, ist eine Datei, in der Visual C++ spezielle
Hinweis
Informationen über Ihr Programm speichern kann, Informationen, die in die
Datei Conversion.cpp nicht hinein gehören.
Nach einigen Minuten Festplattenaktivität antwortet Visual C++ mit einem zufriedenen Klingel-
zeichen, das anzeigt, dass der Erzeugungsprozess abgeschlossen ist. Das Ausgabefenster sollte eine
Meldung ähnlich zu Abbildung 2.3 enthalten, die anzeigt, dass die Datei Conversion.exe ohne Feh-
ler und ohne Warnungen erzeugt wurde.
Abbildung 2.3: Keine Fehler und keine Warnungen – das Programm wurde erfolgreich erzeugt.
Visual C++ erzeugt einen unangenehmen Ton, wenn es während des Erzeugungsprozesses auf
einen Fehler stößt (wenigstens denkt Microsoft, dass er unangenehm ist – ich habe ihn schon so oft
gehört, dass er fast ein Teil von mir geworden ist). Zusätzlich enthält das Ausgabefenster eine Erklä-
rung, welchen Fehler Visual C++ gefunden hat.
Ich habe ein Semikolon am Ende einer Zeile im Programm entfernt und habe das Programm neu
kompiliert, nur um die Fehlermeldung zu demonstrieren. Das Ergebnis finden Sie in Abbildung 2.4.
C++ Lektion 02 31.01.2001 12:08 Uhr Seite 13
Teil 1 – Freitagabend
Lektion 2
Abbildung 2.4:Visual C++ gibt während des Erzeugungsprozesses eine Fehlermeldung aus.
Die Fehlermeldung in Abbildung 2.4 ist tatsächlich sehr ausführlich. Sie beschreibt das Problem
und den Ort des Fehlers (Zeile 22 in der Datei Conversion.cpp). Ich habe das Semikolon wieder ein-
gefügt und das Programm neu kompiliert, um das Problem zu beheben.
Nicht alle Fehlermeldungen sind so klar wie diese. Oft kann ein einziger Fehler
=
= =
=
mehrere Fehlermeldungen erzeugen. Am Anfang können diese Fehlermeldungen
verwirrend sein. Mit der Zeit bekommen Sie ein Gefühl dafür, was Visual C++
Hinweis
während des Erzeugungsprozesses denkt, und was Visual C++ verwirrt haben
könnte.
Sie werden ohne Zweifel den unangenehmen Ton eines Fehlers hören, der von
=
= =
=
Visual C++ entdeckt wurden, bevor Sie das Programm Conversion.cpp fehlerfrei
eingegeben haben. Wenn Sie es gar nicht schaffen, den Code so einzugeben,
Hinweis
dass Visual C++ damit zufrieden ist, kopieren Sie die Datei Conversion.cpp aus
wecc\Programs\lesson02\Conversion.cpp auf der beiliegenden CD-ROM.
C++ Lektion 02 31.01.2001 12:08 Uhr Seite 14
14 Freitagabend
C++-Fehlermeldungen
Warum sind alle C++-Pakete, Visual C++ eingeschlossen, so pingelig, wenn es um die Syntax von
C++ geht? Wenn Visual C++ erkennt, dass ich ein Semikolon vergessen habe, warum kann es die-
ses Problem nicht einfach selber lösen und fortfahren?
Die Antwort ist einfach aber profund. Visual C++ denkt, dass Sie ein Semikolon vergessen haben.
Ich könnte beliebig viele andere Fehler eingebaut haben, die Visual C++ als Fehlen eines Semiko-
lons fehldiagnostiziert haben könnte. Wenn der Compiler einfach das Problem durch Einfügen
eines Semikolons behebt, würde Visual C++ möglicherweise dadurch das eigentliche Problem ver-
schleiern.
Wie Sie sehen werden, ist das Auffinden eines Fehlers in einem Programm, das ohne Probleme den
Erzeugungsprozess durchläuft, schwierig und zeitaufwendig. Es ist besser, den Compiler Fehler fin-
den zu lassen, wenn möglich.
Diese Lektion war hart zu Beginn. In den frühen Tagen des Computers versuchten Compiler alle
möglichen Fehler zu erkennen und selber zu korrigieren. Dies hatte manchmal lächerliche Züge.
Meine Freunde und ich machten uns einen Spaß daraus, einen »freundlichen« Compiler damit zu
quälen, indem wir ein Programm eingaben, das nichts als die existenzielle Frage IF enthielt. (Rück-
schauend waren meine Freunde und ich ein wenig verrückt). Durch eine Reihe schmerzhafter Dre-
hungen hat der besagte Compiler aus diesem einen Wort eine Kommandozeile generiert, die sich
ohne Fehler übersetzen ließ. Ich weiß, dass der Compiler meine Absicht mit dem Wort IF missver-
standen haben muss, weil ich nichts damit beabsichtigt hatte.
Meine Erfahrung ist, dass jedes Mal, wenn der Compiler versucht hat, ein Problem in einem Pro-
gramm zu beheben, das Ergebnis falsch war. Trotz Fehlinformation war es keine Schwierigkeit, das
Problem zu beheben, wenn der Compiler den Fehler gemeldet hat, bevor er ihn versuchte zu behe-
ben. Compiler, die Fehler behoben haben, ohne entsprechende Fehlermeldungen auszugeben,
haben mehr Schaden angerichtet als dass sie geholfen haben.
=
= =
= Vermeiden Sie das Ausführen-Menü-Kommando oder die äquivalente F5-Taste
Hinweis fürs Erste.
Visual C++ öffnet ein Programmfenster ähnlich zu dem in Abbildung 2.5, das die Eingabe einer
Temperatur in Grad Celsius erwartet.
Geben Sie eine Temperatur ein, z.B. 100 Grad Celsius. Nach Drücken der Entertaste gibt das Pro-
gramm die äquivalente Temperatur in Grad Fahrenheit aus, wie in Abbildung 2.6 zu sehen ist. Die
»Press any key to continue«-Meldung, die vielleicht hinter die Temperaturausgabe gequetscht ist, ist
ästhetisch nicht zufriedenstellend, aber die konvertierte Temperatur ist unmissverständlich – wir
beheben das in Kapitel 5.
C++ Lektion 02 31.01.2001 12:08 Uhr Seite 15
Teil 1 – Freitagabend
Lektion 2
Abbildung 2.5: Das Programm Conversion.exe beginnt mit der Frage nach einer Temperatur.
Abbildung 2.6: Das Programm Conversion.exe wartet auf eine Eingabe, nachdem das Programm beendet
ist.
!
Die »Press any key to continue«-Meldung gibt dem Benutzer Zeit, die Ausgabe
des Programms anzusehen, bevor das Fenster nach Beendigung des Programms
geschlossen wird. Diese Meldung erscheint nicht, wenn Sie das Go-Kommando
Tipp
oder die F5-Taste verwenden.
Glückwunsch! Sie haben Ihr erstes Programm eingegeben, erzeugt und ausgeführt.
2.5 Abschluss.
Es gibt noch zwei Punkte, die erwähnt werden sollten, bevor wir weitergehen. Zum einen könnte Sie
die Ausgabe des Programms Conversion.exe überraschen. Zum anderen bietet Visual C++ viel mehr
Hilfe an als nur Fehlermeldungen.
C++ Lektion 02 31.01.2001 12:08 Uhr Seite 16
16 Freitagabend
2.5.1 Programmausgabe
Windows-Programme haben eine visuell ausgerichtete, Fenster-basierte Ausgabe. Conversion.exe
ist ein 32-Bit-Programm, das unter Windows ausgeführt wird, ist aber kein Windows-Programm im
visuellen Sinne.
=
= =
= Wenn Sie nicht wissen, was die Phrase »32-Bit-Programm« bedeutet, brauchen
Hinweis Sie sich keine Sorgen zu machen.
Wie ich bereits in der Einleitung erläutert habe, ist dies kein Buch über das Schreiben von Win-
dows-Programmen. Die C++-Programme, die Sie in diesem Buch schreiben, haben ein Kommando-
zeilen-Interface, das innerhalb einer DOS-Box ausgeführt wird. Angehende Windows-Programmie-
rer sollten nicht verzweifeln – Sie haben Ihr Geld nicht umsonst ausgegeben. Das Erlernen von C++
ist Grundvoraussetzung für das Schreiben von Windows-Programmen mit C++.
Teil 1 – Freitagabend
Lektion 2
Abbildung 2.7: Die Taste F1 stellt dem C++-Programmierer Hilfe zur Verfügung.
Zusammenfassung.
Visual C++ 6.0 hat eine benutzerfreundliche Umgebung, in der Sie Programme erzeugen und testen
können. Sie verwenden den Editor von Visual C++, um Ihren Quellcode einzugeben. Einmal einge-
geben, werden die C++-Anweisungen durch den Erzeugungsprozess in eine ausführbare Datei über-
führt. Schließlich können Sie Ihr fertiges Programm von Visual C++ aus ausführen.
Im nächsten Kapitel sehen Sie, wie Sie das gleiche Programm mit dem GNU C++-Compiler erzeu-
gen können, den Sie auf der beiliegenden CD-ROM finden. Wenn Sie absolut überzeugt sind von
Visual C++, können Sie zu Sitzung 4 springen, die erklärt, wie das Programm funktioniert, das Sie
gerade eingegeben haben.
Selbsttest.
1. Was für eine Datei ist ein C++-Quellprogramm? (Ist es eine Word-Datei? Eine Excel-Datei? Eine
Textdatei?) (Sehen Sie sich den ersten Abschnitt von »Ihr erstes Programm« an.)
2. Beachtet C++ die Einrückung? Achtet es auf Groß- und Kleinschreibung? (Siehe »Ihr erstes Pro-
gramm«)
3. Was bedeutet »Erzeugen Ihres Programms«? (Siehe »Erzeugen Ihres Programms«)
4. Warum erzeugt C++ Fehlermeldungen? Warum versucht C++ nicht einfach, daraus schlau zu
werden, was ich eingebe? (Siehe Kasten »C++-Fehlermeldungen«)
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 18
3
Lektion Ihr erstes
C++-Programm
mit GNU C++
Checkliste.
✔ GNU C++ von der beiliegenden CD-ROM installieren
✔ Ihr erstes C++-Programm in GNU C++ schreiben
✔ Aus ihrem C++-Code ein ausführbares Programm erzeugen
✔ Ihr Programm ausführen
✔ Hilfe bei der Programmierung bekommen
K
apitel 2 behandelte das Schreiben, Erzeugen und Ausführen von C++-Pro-
grammen mit Visual C++. Viele Leser des C++ Wochenend Crashkurses haben
keinen Zugang zu Visual C++. Für diese Leser enthält dieses Buch den frei ver-
fügbaren GNU C++-Compiler, der auf der CD-ROM zu finden ist.
30 Min.
GNU wird »guh-new« gesprochen. GNU steht für die Ringdefinition »GNU is
=
= =
=
Not Unix«. Dieser Witz geht auf die Anfänge von C++ zurück. Nehmen Sie es
einfach wie es ist. GNU ist eine Reihe von Werkzeugen der Free Software Foun-
Hinweis
dation. Diese Werkzeuge stehen der Öffentlichkeit zur Verfügung, mit einigen
Nutzungseinschränkungen, aber kostenlos.
Dieses Kapitel zeigt Schritt für Schritt, wie das gleiche Programm Conversion.cpp aus Kapitel 2
mit GNU C++ in ein ausführbares Programm verwandelt werden kann. Das Programm, um das es
gehen soll, konvertiert eine Temperatureingabe von Grad Celsius nach Grad Fahrenheit.
Die GNU Umgebung wird von einer Reihe freiwilliger Programmierer gepflegt. Wenn Sie dies
bevorzugen, können Sie die allerneueste Version von GNU C++ vom Web herunterladen.
Teil 1 – Freitagabend
Die GNU Entwicklungsumgebung ist ein sehr großes Paket. GNU enthält eine Anzahl von Hilfs-
programmen und anderen Programmiersprachen außer C++. GNU C++ selber unterstützt eine Viel-
zahl von Computerprozessoren und Betriebssystemen. Glücklicherweise müssen Sie nicht alles von
Lektion 3
GNU herunterladen, wenn Sie nur C++-Programme entwickeln wollen. Die GNU Entwicklungsum-
gebung ist auf verschiedene ZIP-Dateien aufgeteilt. Das Hilfsprogramm ”ZIP-Picker”, das auf der
Website von Delorie-Software zu finden ist, teilt Ihnen mit, welche ZIP-Dateien Sie herunterladen
müssen, basierend auf Ihren Antworten auf eine Reihe einfacherer Fragen.
So installieren Sie GNU C++ vom Web:
1. Gehen Sie zur Webseite http://www.elorie.com/djgpp/zip-picker.html.
2. Die Site zeigt Ihnen die Fragen, die wir im Folgenden wiedergeben. Beantworten Sie die Fragen
des Programms wie fett gedruckt, um eine minimale Konfiguration zu erhalten.
FTP Site
Select a suitable FTP site: Pick one for me
Basic Functionality
Pick one of the following: Build and run programs with DJGPP
Which operating system will you be using? <Ihr Betriebssystem>
Do you want to be able to read the on-line documentation? No
Which programming languages will you be using? <Klicken Sie C++>
Integrated Development Environments and Tools
Which IDE(s) would you ike? <Klicken Sie auf RHIDE. Lassen Sie die emacs-options unausge-
wählt>
Would you like gdb, the text mode GNU debugger? No
Extra Stuff
Please Check off each extra thing that you want. Don’t check anything in this list.
3. Dann klicken Sie auf »Tell me what files I need«. Der ZIP-Picker antwortet mit einigen einfachen
Installationsanweisungen und einer Liste von ZIP-Dateien, die Sie brauchen werden. Das Listing
unten zeigt die Dateien, die für eine minimale Installation, wie sie hier beschrieben wird, benötigt
werden – die Dateinamen, die Sie erhalten, können davon verschieden sein, weil sie eine aktuel-
lere Version anzeigen.
Read the file README.1ST before you do anything else with DJGPP! It has
important installation and usage instructions.
v2/djdev202.zip DJGPP Basic Development Kit 1.4 mb
v2/faq211b.zip Frequently Asked Questions 551 kb
v2apps/rhide14b.zip RHIDE 1.6 mb
v2gnu/bnu281b.zip Basic assembler, linker 1.8 mb
v2gnu/gcc2952b.zip Basic GCC compiler 1.7 mb
v2gnu/gpp2952b.zip C++ compiler 1.6 mb
v2gnu/lgpp295b.zip C++ libraries 484 kb
v2gnu/mak377b.zip Make (processes makefiles) 242 kb
Total bytes to download: 9,812,732
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 20
20 Freitagabend
Beachten Sie, dass die obigen Zeilen davon ausgehen, dass ihr Ordner DJGPP direkt unterhalb
von C:\ steht. Wenn Sie Ihren Ordner an einer anderen Stelle platziert haben, ersetzen Sie bitte
den Pfad in den obigen Kommandos entsprechend.
8. Booten Sie neu, um die Installation abzuschließen.
Der Ordner \BIN enthält die ausführbaren Dateien der GNU-Werkzeuge. Die Datei DJGPP.ENV setzt
eine Reihe von Optionen, um die GNU C++-»Umgebung« von Windows zu beschreiben.
!
Tipp
Bevor Sie GNU C++ benutzen, sehen Sie in der Datei DJGPP.ENV nach, ob der
Support langer Dateinamen angeschaltet ist. Diese Option ausgeschaltet zu
haben ist der meist begangene Fehler bei der Installation von GNU C++.
Öffnen Sie die Datei DJGPP.ENV mit einem Texteditor, z.B. mit Microsoft WordPad. Erschrecken
Sie nicht, wenn Sie nur eine lange Zeichenkette sehen, die von kleinen schwarzen Kästchen unter-
brochen ist. Unix verwendet ein anderes Zeichen für Zeilenumbrüche als Windows. Suchen Sie nach
der Phrase ”LFN=y” oder ”LFN=Y” (Groß- oder Kleinschreibung spielen also keine Rolle). Wenn Sie
stattdessen ”LFN=n” finden (oder ”LFN” überhaupt nicht vorkommt), ändern Sie das ”n” in ”y”.
Speichern Sie die Datei. (Stellen Sie sicher, dass Sie die Datei als Textdatei speichern und nicht in
einem anderen Format, z.B. als Word .DOC-Datei.)
Fügen Sie die folgende Zeile in die Datei DJGPP.ENV im Block [rhide] ein, um rhide in deutscher
Sprache zu verwenden:
LANGUAGE=DE
Das rhide-Interface
Teil 1 – Freitagabend
Das Interface von rhide ist grundsätzlich anders als das von Windows-Programmen. Windows-
Programme »zeichnen« ihre Ausgaben auf den Bildschirm, was ihnen ein feineres Aussehen ver-
schafft.
Lektion 3
Im Vergleich dazu arbeitet das Interface von rhide zeichenbasiert. rhide verwendet eine Reihe
von Block-Zeichen, die auf dem PC zur Verfügung stehen, um ein Windows-Interface zu simulieren,
was rhide weniger elegant aussehen lässt. Z.B. lässt rhide es nicht zu, das Fenster auf eine andere
Größe zu bringen als die Standardeinstellung von 80x25 Zeichen, was Standard für MS-DOS-Pro-
gramme ist.
Für die unter Ihnen, die alt genug sind, um sich daran zu erinnern, sieht das
=
= =
=
rhide-Interface sehr ähnlich aus wie das Interface der Borland-Suite von
Programmierwerkzeugen, die es heute nicht mehr gibt.
Hinweis
Wie dem aus sei, das Interface von rhide funktioniert und gibt bequemen
Zugriff auf die übrigen Werkzeuge von GNU C++.
Erzeugen Sie eine leere Datei, indem Sie »Neu« aus dem Datei-Menü auswählen. Geben Sie das
Programm genau so ein, wie Sie es hier vorfinden.
!
Tipp
Machen Sie sich keine Gedanken über das Einrücken – es kommt nicht darauf
an, ob eine Zeile zwei oder drei Zeichen eingerückt ist oder ob zwei Worte
durch ein oder zwei Leerzeichen getrennt sind.
.
CD-ROM
Sie können natürlich mogeln und die Datei Conversion.cpp von der beiliegen-
den CD-ROM kopieren.
//
// Programm konvertiert Temperaturen in Grad Celsius
// nach Grad Fahrenheit
// Fahrenheit = Celsius * (212 – 32)/100 + 32
//
#include <stdio.h>
#include <iostream.h>
int main(int nNumberofArgs, char* pszArgs[])
{
// Eingabe der Temperatur in Grad Celsius
int nCelsius;
cout << »Temperatur in Grad Celsius:«;
cin >> nCelsius;
// berechne Umrechnungsfaktor von Celsius
// nach Fahrenheit
int nFactor;
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 22
22 Freitagabend
return 0;
}
Wenn Sie damit fertig sind, sollte Ihr rhide-Fenster wie in Abbildung 3.1 aussehen.
Abbildung 3.1: rhide stellt ein zeichenbasiertes Interface bereit, um C++-Programme zu erzeugen.
Wählen Sie »Speichern als...« im Datei-Menü aus, wie in Abbildung 3.2 zu sehen ist, und spei-
chern Sie die Datei unter dem Namen Conversion.cpp.
Abbildung 3.2: Mit dem Kommando »Save As...« kann der Benutzer eine C++-Datei erzeugen.
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 23
Teil 1 – Freitagabend
Wir haben in Sitzung 1 eine begrenzte Anzahl von Anweisungen verwendet, um den menschlichen
Computer anzuweisen, einen Reifen zu wechseln. Obwohl sehr eingeschränkt, werden Sie vom
Durchschnittsmenschen verstanden (zumindest von Deutsch Sprechenden).
Lektion 3
Das Programm Conversion.cpp, das Sie gerade eingegeben haben, enthält C++-Anweisungen,
eine Sprache, die Sie in keiner Tageszeitung finden werden. So kryptisch und grob diese C++-Anwei-
sungen auch auf Sie wirken, versteht der Computer eine Sprache, die noch viel elementarer ist als
C++. Die Sprache, die Ihr Computer versteht, wird als Maschinensprache bezeichnet.
Der C++-Compiler übersetzt Ihr C++-Programm in die Maschinensprache Ihrer Mikroprozessor-
CPU in Ihrem PC. Programme, die Sie unter Windows aufrufen können, GNU C++ eingeschlossen,
sind nichts anderes als Dateien, die Maschinenanweisungen enthalten.
=
= =
= Es ist möglich, Programme direkt in Maschinensprache zu schreiben. Dies ist
Hinweis aber viel schwieriger, als das gleiche Programm in C++ zu schreiben.
Die wichtigste Aufgabe von GNU C++ ist, Ihr C++-Programm in eine ausführbare Datei zu über-
setzen. Der Vorgang der Übersetzung in eine ausführbare .EXE-Datei wird als Erzeugen bezeichnet.
Dieser Prozess wird manchmal auch als Kompilieren bezeichnet (es gibt einen Unterschied zwischen
diesen beiden Begriffen, der aber hier nicht relevant ist). Der Teil des C++-Paketes, der die Überset-
zung des Programms ausführt, wird als Compiler bezeichnet.
Um Ihr Programm Conversion.cpp zu erzeugen, klicken Sie auf »Compile« und dann auf »Make«,
oder drücken Sie F9. rhide öffnet ein kleines Fenster am unteren Rand des Fensters, um den Fort-
schritt des Prozesses anzuzeigen. Wenn alles gut geht, erscheint die Meldung »Creating: Conver-
sion.exe« gefolgt von »no errors« wie in Abbildung 3.3 zu sehen ist.
Abbildung 3.3: rhide zeigt »no errors« an, wenn kein Fehler aufgetreten ist.
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 24
24 Freitagabend
GNU C++-Installationsfehler
Einige häufig gemachte Fehler bei der Installation können Ihnen den Spaß an Ihrer ersten Pro-
grammiererfahrung verderben.
Die Meldung »Bad command or file name« bedeutet, dass MS-DOS die Datei gcc.exe nicht finden
kann, d.h. den GNU C++-Compiler. Entweder haben Sie GNU C++ nicht richtig installiert oder Ihr
Pfad enthält nicht c:\djgpp\bin, wo gcc.exe zu finden ist. Versuchen Sie, GNU C++ erneut zu
installieren und stellen Sie sicher, dass das Kommando SET PATH=c:\djgpp\bin;%PATH% in Ihrer
Datei autoexec.bat vorkommt. Nachdem Sie GNU C++ erneut installiert haben, starten Sie den
Rechner neu.
Die Meldung »gcc.exe: Conversion.cpp: No such file or directory (ENOENT)« zeigt an, dass gcc
nicht weiß, dass Sie lange Dateinamen verwenden (im Gegensatz zu den alten MS-DOS 8.3 Datei-
namen). Um dies zu korrigieren, editieren Sie die Datei c:\djgpp\djgpp.env. Setzen Sie die Eigen-
schaft LFN auf Y, wie in der Abbildung zu sehen.
GNU C++ erzeugt eine Fehlermeldung, wenn es einen Fehler in Ihrem C++-Programm findet. Um
diesen Prozess des Fehlermeldens zu demonstrieren, habe ich ein Semikolon am Ende einer Zeile
entfernt und das Programm neu kompiliert. Das Ergebnis finden Sie in Abbildung 3.4.
Abbildung 3.4: GNU C++ gibt Fehlermeldungen während des Erzeugungsprozesses aus.
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 25
Die Fehlermeldung in Abbildung 3.4 ist ein wenig imposant; sie ist aber einigermaßen ausdrucks-
voll, wenn Sie sie Zeile für Zeile betrachten.
Teil 1 – Freitagabend
Die erste Zeile zeigt an, dass der Fehler entdeckt wurde, während der Code innerhalb von
main( ) analysiert wurde, d.h. der Code zwischen der öffnenden und schließenden Klammer, die auf
das Schlüsselwort main( ) folgen.
Lektion 3
Die zweite Zeile zeigt an, dass nicht verstanden wurde, wie int in Zeile 22 da passt. Natürlich
passt int nicht, aber ohne das Semikolon hat GNU C++ gedacht, dass die Zeilen 18 und 22 eine ein-
zige Anweisung sind. Die übrigen Fehler stammen daher, dass Zeile 22 nicht verstanden werden
konnte.
Um das Problem zu beheben, habe ich zuerst Zeile 22 analysiert (beachten Sie die Zeile 22:5
unten links im Code-Fenster – der Cursor ist in Spalte 5 von Zeile 22). Da Zeile 22 in Ordnung zu sein
scheint, gehe ich zu Zeile 18 zurück und stelle fest, dass ein Semikolon fehlt. Ich füge das Semikolon
ein und kompiliere neu. Diesmal kompiliert GNU C++ ohne Schwierigkeiten.
C++ Fehlermeldungen
Warum sind alle C++-Pakete so pingelig, wenn es um die Syntax von C++ geht? GNU C++ war in
der Lage, das Fehlen des Semikolons in obigem Beispiel zu erkennen. Wenn ein C++-Compiler
erkennt, dass ich ein Semikolon vergessen habe, warum kann es dieses Problem nicht einfach sel-
ber lösen und fortfahren?
Die Antwort ist einfach aber profund. GNU C++ denkt, dass ich ein Semikolon vergessen habe. Ich
könnte beliebig viele andere Fehler eingebaut haben, die GNU C++ als Fehlen eines Semikolons
fehldiagnostiziert haben könnte. Wenn der Compiler einfach das Problem durch Einfügen eines
Semikolons behebt, würde GNU C++ möglicherweise dadurch das eigentliche Problem ver-
schleiern.
Wie Sie sehen werden, ist das Auffinden eines Fehlers in einem Programm, das ohne Probleme den
Erzeugungsprozess durchläuft, schwierig und zeitaufwendig. Es ist besser, den Compiler Fehler fin-
den zu lassen, wenn möglich.
Diese Lektion war hart zu Beginn. In den frühen Tagen des Computers versuchten Compiler, alle
möglichen Fehler zu erkennen und selber zu korrigieren. Dies hatte manchmal lächerliche Züge.
Meine Freunde und ich machten uns einen Spaß daraus, einen »freundlichen« Compiler damit zu
quälen, indem wir ein Programm eingaben, das nichts als die existenzielle Frage IF enthielt. (Rück-
schauend waren meine Freunde und ich ein wenig verrückt). Durch eine Reihe schmerzhafter Dre-
hungen hat der besagte Compiler aus diesem einen Wort eine Kommandozeile generiert, die sich
ohne Fehler übersetzen ließ. Ich weiß, dass der Compiler meine Absicht mit dem Wort IF missver-
standen haben muss, weil ich nichts damit beabsichtigt hatte.
Meine Erfahrung ist, dass jedes Mal, wenn der Compiler versucht hat, ein Problem in einem Pro-
gramm zu beheben, das Ergebnis falsch war. Trotz Fehlinformation war es keine Schwierigkeit, das
Problem zu beheben, wenn der Compiler den Fehler gemeldet hat, bevor er ihn versuchte zu behe-
ben. Compiler, die Fehler behoben haben, ohne entsprechende Fehlermeldungen auszugeben,
haben mehr Schaden angerichtet als genützt.
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 26
26 Freitagabend
10 Min.
Abbildung 3.5: rhide öffnet ein Fenster, in dem das Programm ausgeführt wird.
Sofort erscheint ein Fenster, in dem das Programm die Eingabe einer Temperatur in Grad Celsius
erwartet, wie in Abbildung 3.6.
Geben Sie eine bekannte Temperatur ein, z.B. 100 Grad. Nach Drücken der Returntaste gibt das
Programm die äquivalente Temperatur von 212 Grad Fahrenheit zurück. Weil rhide das Fenster des
Programms sofort schließt, wenn das Programm beendet ist, haben Sie keine Chance, den Inhalt des
Fensters zu lesen, bevor es geschlossen wird. rhide öffnet einen Dialog mit der Meldung, dass das
Programm mit einem Fehlercode von Null beendet wurde. Abgesehen von der Bezeichnung »Feh-
lercode« bedeutet Null, dass kein Fehler aufgetreten ist.
Um die Ausgabe des bereits beendeten Programms einzusehen, klicken Sie auf den Menüpunkt
»Nutzerbildschirm« des Fenster-Menüs oder drücken Sie Alt+F5. Dieses Fenster zeigt das aktuelle
C++ Lektion 03 31.01.2001 12:11 Uhr Seite 27
MS-DOS-Fenster. In diesem Fenster sehen Sie die letzten 25 Zeilen der Ausgabe Ihres Programms,
die auch die Ausgabe der berechneten Temperatur in Fahrenheit enthält, wie in Abbildung 3.6 zu
Teil 1 – Freitagabend
sehen ist.
Glückwunsch! Sie haben Ihr erstes Programm mit GNU C++ eingegeben, erzeugt und ausge-
führt.
Lektion 3
3.5 Abschluss.
Es gibt zwei Punkte, auf die hingewiesen werden sollte. Erstens ist GNU C++ nicht dafür gedacht,
Windows-Programme zu schreiben. Theoretisch könnten Sie mit GNU C++ ein Windows-Programm
schreiben, das wäre aber nicht einfach, ohne die Bibliotheken von Visual C++ zu verwenden. Zwei-
tens bietet GNU C++ eine Art Hilfe an, die sehr nützlich sein kann.
3.5.1 Programmausgabe
Windows-Programme haben eine sehr visuell ausgerichtete, Fenster-basierte Ausgabe. Conver-
sion.exe ist ein 32-Bit-Programm, das unter Windows ausgeführt wird, aber kein Windows-Pro-
gramm im eigentlichen Sinne ist.
!
Tipp
Wenn Sie nicht wissen, was die Phrase »32-Bit-Programm« bedeutet, brauchen
Sie sich keine Sorgen zu machen.
Wie ich bereits in der Einleitung erläutert habe, ist dies kein Buch über das Schreiben von Win-
dows-Programmen. Die C++-Programme, die Sie in diesem Buch schreiben, haben ein Kommando-
zeilen-Interface, das innerhalb einer DOS-Box ausgeführt wird. Angehende Windows-Programmie-
rer sollten nicht verzweifeln – Sie haben Ihr Geld nicht umsonst ausgegeben. Das Erlernen von C++
ist Grundvoraussetzung für das Schreiben von Windows-Programmen in C++.
28 Freitagabend
Abbildung 3.7: rhide stellt Hilfe über F1 und einen Index bereit.
Die Hilfe von GNU C++ ist nicht so umfangreich, wie die von Visual C++. Wenn
Sie z.B. mit dem Cursor auf int gehen und F1 drücken, erscheint ein Fenster,
=
= =
= das den Editor beschreibt. Nicht gerade das, was ich haben wollte. Die Hilfe
Hinweis von GNU C++ konzentriert sich auf Bibliotheksfunktionen und Compiler-
optionen. Glücklicherweise ist die Hilfe von GNU C++ in den meisten Fällen
ausreichend, wenn Sie erst einmal C++ beherrschen.
Zusammenfassung.
GNU C++ stellt eine benutzerfreundliche Umgebung bereit, in der Sie Programme mit Hilfe des
Hilfsprogramms rhide erzeugen und testen können. Sie können rhide in ähnlicher Weise wie Visual
C++ benutzen. Sie können den Editor von rhide verwenden, um den Code einzugeben, und den
rhide-Erzeuger, um den Quelltext in Maschinencode zu überführen. Schließlich ermöglicht es
rhide, das fertige Programm in derselben Umgebung laufen zu lassen.
Das nächste Kapitel geht Schritt für Schritt durch das C++-Programm.
Selbsttest.
1. Was für eine Datei ist ein C++-Quellprogramm? (Ist es eine Word-Datei? Eine Excel-Datei? Eine
Textdatei?) (Sehen Sie sich den ersten Abschnitt von »Ihr erstes Programm« an)
2. Beachtet C++ das Einrücken? Achtet es auf Groß- und Kleinschreibung? (Siehe »Ihr erstes Pro-
gramm«)
3. Was bedeutet »Erzeugen Ihres Programms«? (Siehe »Erzeugen Ihres Programms«)
4. Warum erzeugt C++ Fehlermeldungen? Warum versucht C++ nicht einfach, daraus schlau zu
werden, was ich eingebe? (Siehe Kasten »C++-Fehlermeldungen«)
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 29
C++-Instruktionen
4
Lektion
Checkliste.
✔ Programm »Conversion« aus Sitzungen 2 und 3 erneut betrachten
✔ Die Teile eines C++-Programms verstehen
✔ Häufig verwendete C++-Kommandos einführen
I
n den Sitzungen 2 und 3 haben Sie ein C++-Programm eingegeben. Die Idee
dahinter war, die C++-Umgebung kennen zu lernen (welche Umgebung auch
immer Sie gewählt haben) und weniger, das Programmieren dabei zu erlernen.
In dieser Sitzung wird das Programm Conversion.cpp genauer analysiert. Sie wer-
den sehen, was genau jeder Teil des Programms tut, und wie jeder Programmteil
30 Min.
seinen Beitrag zur Lösung leistet.
=
= =
=
Es gibt mehrere Aspekte des Programms, die Sie erst einmal glauben müssen.
Seien Sie geduldig. Jede Struktur des Programms wird zu ihrer Zeit erklärt wer-
Hinweis den.
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 30
30 Freitagabend
Lektion 4 – C++-Instruktionen 31
Sie brauchen sich über die Details dieses Aufbaus keine Gedanken zu machen – die Details wer-
den später behandelt – aber Sie sollten so einen ersten Eindruck haben, wie sie aussehen. Die ersten
Teil 1 – Freitagabend
beiden Zeilen werden Include-Anweisungen genannt, weil Sie dafür sorgen, dass der Inhalt der
bezeichneten Datei an diesem Punkt in das Programm eingefügt wird. Wir verstehen sie zu diesem
Zeitpunkt einfach als Magie.
Lektion 4
Die nächste Anweisung in dem Programmrahmen ist die Anweisung int main(...). Diese wird
gefolgt von einer öffnenden und einer schließenden Klammer. Ihr Programm steht zwischen diesen
beiden Klammern. Die Ausführung des Programms beginnt nach der öffnenden Klammer und endet
bei der return-Anweisung, die unmittelbar vor der schließenden Klammer steht.
Unglücklicherweise müssen wir eine detailliertere Beschreibung des Programmrahmens auf spä-
tere Kapitel verschieben. Machen Sie sich keine Sorgen ... wir kommen noch dazu, bevor das
Wochenende vorbei ist.
4.2.2 Kommentare
Die ersten Programmzeilen scheinen frei formuliert zu sein. Entweder ist dieser »Code« für das
menschliche Auge gedacht, oder der Computer ist doch schlauer, als wir immer gedacht haben. Die
ersten sechs Zeilen werden als Kommentare bezeichnet. Ein Kommentar ist eine Zeile oder ein Teil
einer Zeile, die vom C++-Compiler ignoriert wird. Kommentare ermöglichen es dem Programmie-
rer, zu erklären, was er oder sie beim Schreiben des Codes gedacht hat.
Ein C++-Kommentar beginnt mit einem Doppelslash (»//«) und endet mit einer neuen Zeile. Sie
können beliebige Zeichen in Ihren Kommentaren verwenden. Kommentare können so lang sein wie
Sie wollen, aber es ist übersichtlicher, wenn sie nicht länger als ca. 80 Zeichen werden; das ist die
Länge, die vom Bildschirm dargestellt werden kann.
Ein Zeilenumbruch würde in den frühen Tagen der Schreibmaschine als »Carriage Return«
bekannt geworden sein, als der Vorgang der Zeicheneingabe in eine Maschine als »Tippen« und
nicht als »Keyboarding« bezeichnet wurde. Ein Zeilenumbruch ist das Zeichen, das eine Komman-
dozeile beendet.
C++ erlaubt eine zweite Art Kommentar, in der alles nach /* und vor dem nächsten */ ignoriert
wird; diese Art des Kommentars wird in C++ normalerweise nicht mehr verwendet.
Es kommt Ihnen vielleicht komisch vor, dass es in C++ oder einer anderen Pro-
grammiersprache ein Kommando gibt, das vom Computer ignoriert wird. Jede
=
= =
=
Programmiersprache hat so etwas in irgendeiner Form. Es ist wichtig, dass der
Programmierer erklärt, was ihm oder ihr durch den Kopf gegangen ist, als der
Hinweis Code geschrieben wurde. Für jemanden, der das Programm nimmt, um es zu
benutzen oder zu modifizieren, muss das sonst nicht offensichtlich sein. In der
Tat kann die Idee hinter dem Programm selbst für den Programmierer nach
einigen Monaten nicht mehr offensichtlich sein.
!
Tipp
Verwenden Sie Kommentare früh und oft.
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 32
32 Freitagabend
4.2.4 Anweisungen
Die erste Zeile, die kein Kommentar ist, ist eine C++-Anweisung. Eine Anweisung ist eine einzel-
ne Menge von Kommandos. Alle Anweisungen, die keine Kommentare sind, enden mit einem Semi-
kolon (;). (Es gibt einen Grund, weshalb Kommentare dies nicht tun, aber er ist obskur. Meiner Mei-
nung nach sollten auch Kommentare mit einem Semikolon enden und wenn es nur wegen der
Konsistenz ist.)
Wenn Sie sich das Programm ansehen, werden Sie bemerken, dass es Leerzeichen, Tabulatorzei-
chen und Zeilenumbrüche enthält. Und tatsächlich habe ich jeder Anweisung im Programm einen
Zeilenumbruch folgen lassen. Diese Zeichen werden unter dem Sammelbegriff Leerraum
zusammengefasst, weil Sie keines dieser Zeichen auf dem Bildschirm sehen können. Ein Leerraum
ist ein Leerzeichen, ein Tabulator, ein vertikaler Tabulator oder ein Zeilenumbruch. C++ ignoriert
Leerraum.
!
Tipp
Sie können Leerraum an jeder beliebigen Stelle in Ihrem Programmtext einfü-
gen, um die Lesbarkeit zu erhöhen, außer innerhalb von Worten.
Während C++ Leerraum ignoriert, unterscheidet es Groß- und Kleinschreibung. Die Variablen
fullspeed und FullSpeed haben nichts miteinander zu tun. Während das Kommando int ver-
standen wird, hat C++ keine Ahnung, was INT bedeuten soll.
4.2.5 Deklarationen
Die Zeile int nCelsius; ist eine Deklarationsanweisung. Eine Deklaration ist eine Anweisung, die
eine Variable definiert. Eine Variable ist ein Platzhalter für Werte eines bestimmten Typs. Eine Varia-
ble enthält einen Wert, wie eine Zahl oder ein Zeichen.
Der Begriff Variable kommt von algebraischen Gleichungen der Form:
x = 10
y = 3 * x
Im zweiten Ausdruck, ist y gleich 3 mal x, aber was ist x? Die Variable x fungiert als Platzhalter für
einen Wert. In diesem Fall ist der Wert von x gleich 10, aber wir hätten den Wert von x ebensogut auf
20, 30 oder -1 setzen können. Die zweite Formel macht immer Sinn, unabhängig vom Wert von x.
In der Algebra ist es erlaubt, mit einer Anweisung wie x = 10 zu beginnen. In C++ muss der Pro-
grammierer eine Variable erst definieren, bevor er sie benutzen kann.
In C++ hat jede Variable einen Typ und einen Namen. Die Zeile int nCelsius; deklariert eine
Variable nCelsius, die einen Integerwert aufnehmen kann. (Warum haben sie es nicht einfach
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 33
Lektion 4 – C++-Instruktionen 33
integer anstatt int genannt? Ich weiß es nicht. Das ist einfach eines der Dinge, mit denen Sie zu
leben lernen müssen.)
Teil 1 – Freitagabend
Der Name einer Variable hat keine besondere Bedeutung für C++. Eine Variable muss mit einem
Buchstaben beginnen (‘A’ bis ‘Z’ oder ‘a’ bis ‘z’). Alle weiteren Zeichen müssen entweder ein Buch-
stabe, eine Ziffer (‘0’ bis ‘9’) oder ein Unterstrich (‘_’) sein. Variablennamen können so lang sein, wie
Lektion 4
es für Sie Sinn macht.
!
Tipp
Es gibt natürlich eine Beschränkung, aber die ist viel größer als die Grenze des
Lesers. Gehen Sie nicht über eine Länge hinaus, die sich der Leser nicht bequem
behalten kann, sagen wir 20 Zeichen.
!
Tipp
Nach Konvention beginnen Variablen mit einem kleinen Buchstaben. Jedes neue
Wort in einer Variablen beginnt mit einem Großbuchstaben wie in der Variable
myVariable. Ich erkläre die Bedeutung des n in nCelsius in Sitzung 5.
!
Tipp
Versuchen Sie, Variablennamen kurz, aber aussagekräftig zu wählen. Vermei-
den Sie Namen wie x, weil x keine Bedeutung hat. Eine Variable mit dem
Namen lengthOfLineSegment ist viel aussagekräftiger.
4.2.6 Eingabe/Ausgabe
Die Zeilen, die mit cin und cout beginnen, werden als Eingabe-/Ausgabe-Anwei-
sungen bezeichnet, oder kurz I/O-Anweisungen (von Input/Output). (Wie alle Inge-
nieure lieben Programmierer Abkürzungen und Kürzel.)
10 Min. Die erste I/O-Anweisung gibt die Phrase »Temperatur in Grad Celsius:« auf cout
(gesprochen »see-out«) aus. cout ist der Name des Standard-Ausgabe-Device von
C++. In diesem Fall ist der Monitor das Ausgabe-Device.
Die nächste Zeile ist genau das Gegenteil. Die Zeile besagt, dass ein Wert aus dem Eingabe-Devi-
ce von C++ bezogen, und in der Variable nCelsius gespeichert werden soll. Das Standard-Eingabe-
Device von C++ ist die Tastatur. Das ist das C++-Analogon zu der algebraischen Formel x=10, die
oben erwähnt wurde. Im Rest des Programms ist der Wert von nCelsius so, wie der Benutzer ihn
hier eingegeben hat.
4.2.7 Ausdrücke
In den nächsten beiden Zeilen, die als Berechnungsausdrücke gekennzeichnet sind, deklariert das
Programm eine Variable nFactor, und weist ihr den Ergebniswert einer Rechnung zu. Die Rechnung
berechnet die Differenz von 212 und 32. In C++ wird eine solche Formel als Ausdruck bezeichnet.
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 34
34 Freitagabend
Ein Operator ist ein Kommando, das einen Wert generiert. Der Operator in dieser Berechnung ist
»-«.
Ein Ausdruck ist ein Kommando, das einen Wert hat. Der Ausdruck ist hier »212 – 32«.
4.2.8 Zuweisung
Die gesprochene Sprache kann mehrdeutig sein. Der Begriff gleich ist eine dieser Mehrdeutigkeiten.
Das Wort gleich kann bedeuten, dass zwei Dinge den gleichen Wert haben, wie etwa 100 Cents
gleich einem Dollar sind. Gleich kann auch wie in der Mathematik eine Zuweisung bedeuten, wie
etwa y gleich 3 mal x.
Um Mehrdeutigkeiten zu vermeiden, rufen C++-Programmierer den Zuweisungsoperator = auf.
Der Zuweisungsoperator speichert den Wert auf der rechten Seite in der Variablen auf der linken Sei-
te. Programmierer sagen, dass nFactor der Wert 212 – 32 zugewiesen wird.
Zusammenfassung.
Sie haben schließlich eine Erklärung des Programms Conversion gesehen, das Sie in den Sitzungen
2 und 3 eingegeben haben. Notwendigerweise waren diese Erklärungen auf einem hohen Abstrak-
tionsniveau. Die Details kommen später.
Lektion 4 – C++-Instruktionen 35
Selbsttest.
Teil 1 – Freitagabend
1. Was tut die folgende C++-Anweisung (Siehe »Kommentare«)
// Ich habe mich verlaufen
2. Was tut die folgende C++-Anweisung (Siehe »Deklarationen«)
Lektion 4
int nQuizYourself; // hilf mir hier raus
3. Was tut die folgende C++-Anweisung (Siehe »Eingabe/Ausgabe«)
cout << »Hilf mir hier raus«
4. Was tut die folgende C++-Anweisung (Siehe »Ausdrücke«)
nHelpMeOutHere = 32;
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 36
1
T EIL Freitagabend – Zusammenfassung
1. Schreiben Sie ein Programm, das ein Auto herunterlässt, unter Verwendung dieser Objekte:
Auto
Reifen
Radmutter
Wagenheber
Schraubenschlüssel
Lassen Sie uns weiterhin annehmen, dass unser Prozessor die folgenden Aktionen versteht:
greifen
bewegen, nach oben bewegen, nach unten bewegen
loslassen
drehen
Hinweise:
a. Sie müssen annehmen, dass die Prozessorperson hoch und runter versteht und dass ein
Wagenheber einen Griff hat.
b. Nicht alle zur Verfügung stehenden Substantive und Verben werden verwendet.
2. Entfernen Sie das Minuszeichen zwischen 212 und 32 und erzeugen Sie das Programm
neu. Zeichnen Sie die Fehlermeldung auf, und erklären Sie sie.
3. Beheben Sie das »Problem« so, wie es von Visual C++/GNU C++ vorgeschlagen wird, und
erzeugen Sie Ihr Programm neu.
4. Führen Sie das Programm aus, und geben Sie einen bekannten Wert ein, wie etwa
100 Grad Celsius. Zeichnen Sie das Ergebnis auf.
6. Erklären Sie, warum dies eine sehr unglückliche Situation wäre, wenn Visual C++/
GNU C++ selbstkorrigierende Compiler wären.
Hinweis: Glauben Sie es oder nicht, »32;« ist ein gültiges Kommando.
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 37
9. Wenn GNU C++ versuchen würde, das Problem automatisch zu beheben, was würde es
tun? Probieren Sie diese Lösung aus, und erzeugen Sie das Programm neu. Achten Sie auf
das Ergebnis.
10. Führen Sie das »berichtigte« Programm aus. Geben Sie eine Temperatur von
100 Grad Celsius ein, und achten Sie auf das Ergebnis.
11. Denken Sie einen Augenblick darüber nach, was passiert wäre, wenn GNU C++ seine eige-
ne Lösung angewendet hätte. Hilft ein solcher Zugang? Ist er schädlich?
Hinweise:
a. GNU C++ denkt, dass eine Zeichenkette bei einem Hochkomma beginnt und bei dem
nächsten Hochkomma endet.
b. Der rhide-Editor hebt Worte hervor in Abhängigkeit davon, als was er die Worte inter-
pretiert. Zeichenketten erscheinen hellblau.
c. Wenn Sie verwirrt sind, gehen Sie zur Antwort von Frage 9 (siehe Anhang A). Wenn Sie
diesen Hinweis verstanden haben, dann gehen sie zurück und beantworten alle diese
Fragen.
C++ Lektion 04 31.01.2001 12:09 Uhr Seite 38
a. twoFeetOfRope
b. 2FeetOfRope
d. engthOf2Ropes
e. &moreRope
14. Schreiben Sie ein Programm, das den Benutzer auffordert, drei Zahlen einzugeben, und
dann die Summe dieser Zahlen ausgibt. Zusatz: Geben Sie das Mittel der eingegebenen
Zahlen aus anstelle ihrer Summe.
Hinweise:
c. Vergessen Sie nicht, dass Division eine höhere Priorität hat als Addition. Es könnte sein,
dass Sie Klammern verwenden müssen, insbesondere im Zusatzprogramm.
C++ Lektion 05 31.01.2001 12:11 Uhr Seite 39
ta g
✔ Fr ei
g
st a
✔S a m
g
n ta
Son✔
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 40
Samstagmorgen
Teil 2
Lektion 5.
Variablentypen
Lektion 6.
Mathematische Operationen
Lektion 7.
Logische Operationen
Lektion 8.
Kommandos zur Flusskontrolle
Lektion 9.
Funktionen
Lektion 10.
Debuggen
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 41
Variablentypen
5
Lektion
Checkliste.
_✔ Variablen deklarieren
_✔ Mit den Grenzen von Integervariablen umgehen
_✔ float-Variablen verwenden
✔ Deklaration und Benutzung anderer Variablentypen
E
in Problem des Programms Conversion in Teil I ist, dass es nur mit Integer-
werten arbeitet. Das ist kein Problem im alltäglichen Leben – es ist unwahr-
scheinlich, dass jemand eine Temperatur wie z.B. 10.5 eingeben wird. Ein
schlimmeres Problem sind die Rundungsfehler. Die meisten Integerwerte in Cel-
30 Min. sius werden auf einen nicht ganzzahligen Wert in Fahrenheit abgebildet. Das Pro-
gramm Conversion kümmert sich nicht darum. Es schneidet einfach die Nach-
kommastellen ab, ohne eine Warnung auszugeben.
=
= =
= Es muss nicht offensichtlich sein, aber das Programm wurde sorgfältig geschrie-
Hinweis
ben, um die Auswirkungen des Rundens auf die Ausgabe so gering wie möglich
zu halten.
Dieses Kapitel untersucht die Begrenzungen von Integervariablen. Auch andere Variablentypen
werden untersucht, die eingeschlossen, die zur Reduktion von Rundungsfehlern eingeführt wurden.
Wir werden uns ihre Vor- und Nachteile anschauen.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 42
42 Samstagmorgen
5.1 Dezimalzahlen.
Integerzahlen sind die Zahlen, an die Sie am meisten gewöhnt sind: 1, 2, 3, usw. und die negativen
Zahlen -1, -2, -3 usw. Im alltäglichen Leben sind Integerzahlen am nützlichsten; leider können sie
keine gebrochenen Zahlen darstellen. Brüche aus Integerzahlen wie z.B. 2⁄3, 15⁄16 oder 3 111⁄126 sind zu
umständlich, um damit zu arbeiten. Wenn zwei Brüche nicht den gleichen Nenner haben, ist es
schwer, sie zu vergleichen. Z.B. bei der Arbeit am Auto hat es lange gedauert, bis ich wusste, welche
Schraube größer ist – 3⁄4 oder 25⁄32 (es ist die letztere).
Mir wurde gesagt, aber ich kann es nicht beweisen, dass das Problem mit den
=
= =
= Integerbrüchen zu der ansonsten unerklärlichen Bedeutung der 12 in unserem
Hinweis Alltag führt. 12 ist die kleinste Integerzahl, die durch 4, 3 und 2 teilbar ist. Ein
Viertel von etwas ist in den meisten Fällen genug.
Die Einführung dezimaler Bruchteile hat sich als große Verbesserung herausgestellt. Es ist klar,
dass 0.75 kleiner ist als 0.78 (das sind die gleichen Werte wie oben, ausgedrückt als Dezimalzahlen.
Außerdem sind mathematische Berechnungen auf Gleitkommazahlen leichter, weil wir keinen klein-
sten gemeinsamen Nenner finden oder anderen Unsinn machen müssen.
Diese Gleichung ist richtig und einsichtig. Lassen Sie uns die folgende ebenfalls korrekte und ein-
sichtige Lösung betrachten:
nValue1/3 + nValue2/3 + nValue3/3
Um zu sehen, welchen Effekt das haben kann, betrachten Sie das folgende einfache Programm,
das beide Methoden zur Berechnung des Mittelwertes benutzt:
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 43
Lektion 5 – Variablentypen 43
Teil 2 – Samstagmorgen
int nValue2;
int nValue3;
cout << »Integerversion\n«;
cout << »Geben Sie drei Zahlen ein.\n«;
Lektion 5
cout << »Drücken Sie Return nach jeder Zahl.\n«;
cout << »#1:«;
cin >> nValue1;
cout << »#2:«;
cin >> nValue2;
cout << »#3:«;
cin >> nValue3;
// die folgende Lösung hat keine so großen
// Probleme mit Rundungsfehlern
cout << »Addieren vor Teilen ergibt Mittelwert:«;
cout << (nValue1 + nValue2 + nValue3)/3;
cout << »\n«;
// diese Version hat große Probleme mit
// Rundungsfehlern
cout << »Teilen vor Addieren ergibt Mittelwert:«;
cout << nValue1/3 + nValue2/3 + nValue3/3;
cout << »\n«;
cout << »\n\n«;
return 0;
}
Dieses Programm bekommt die drei Werte nValue1, nValue2 und nValue3 von cin, d.h. über
die Tastatur. Es gibt dann den Mittelwert aus, der mit der Addition-vor-Division-Methode berechnet
wurde, gefolgt von dem Mittelwert der mit der Division-vor-Addition-Methode berechnet wurde.
Nachdem ich das Programm erzeugt hatte, habe ich es ausgeführt, und die Werte 1, 2 und 3 ein-
gegeben. Erwartet hatte ich, zweimal das Ergebnis 2 zu bekommen. Stattdessen bekam ich das
Ergebnis, das Sie in Abbildung 5.1 sehen.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 44
44 Samstagmorgen
Um den Grund für dieses merkwürdige Verhalten zu verstehen, lassen Sie uns die Werte 1, 2 und
3 direkt in die Gleichung 2 einfügen.
1/3 +> 2/3 + 3/3
Da Integerzahlen keine Brüche darstellen können, ist das Ergebnis einer Integeroperation immer
der abgerundete Bruch.
Dieses Abrunden von Integerzahlen wird auch als Abschneiden bezeichnet.
Unter Berücksichtigung des Integerabschneidens wird der obige Ausdruck zu
0 + 0 + 1
oder 1.
Der Addition-vor-Division-Algorithmus verhält sich entscheidend besser:
(1 + 2 + 3) / 3
Doch auch der Addition-vor-Division-Algorithmus ist nicht immer korrekt. Geben Sie die Werte 1,
1 und 3 ein. Beide Algorithmen geben 1 anstelle von 1 2⁄3 zurück.
=
= =
= Sie müssen sehr vorsichtig sein, wenn Sie Divisionen mit Integerzahlen durch-
Hinweis führen, weil Abschneiden schnell passiert ist.
Eingeschränkter Bereich
Ein zweites Problem mit den int-Variablen ist der beschränkte Bereich. Eine normale Integervariable
kann einen maximalen Wert von 2.147.483.647 und einen minimalen Wert von -2.147.483.648
annehmen, also ungefähr plus/minus zwei Milliarden.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 45
Lektion 5 – Variablentypen 45
=
= =
= Einige ältere (in der Tat »sehr alte«) Compiler beschränken den Bereich von int-
Hinweis Variablen auf -32.768 bis 32.767.
Teil 2 – Samstagmorgen
5.1.2 Lösen des Abschneideproblems
Glücklicherweise versteht C++ Dezimalzahlen. Dezimalzahlen werden in C++ als
Gleitkommazahlen, oder kurz floats, bezeichnet. Der Begriff Gleitkomma kommt
Lektion 5
daher, dass der Dezimalpunkt hin- und hergleiten kann, so wie es notwendig ist, um
20 Min. einen bestimmten Wert darzustellen.
Gleitkommavariablen werden in der gleichen Weise deklariert wie int-Variablen:
float fValue1;
=
= =
= Wenn Sie die Absicht haben, Berechnungen durchzuführen, halten Sie sich an
Hinweis Gleitkommazahlen.
46 Samstagmorgen
Zählen
An einer Stelle können Gleitkommavariablen nicht verwendet werden: wenn gezählt wird. Das
betrifft auch C++-Konstrukte, bei denen ein Zähler verwendet wird. Das liegt daran, dass C++ nicht
sicher sein kann, welche ganze Zahl mit einer Gleitkommazahl gemeint ist. Z.B. ist klar, dass 1.0
gleich 1 ist, aber was ist mit 0.9 und 1.1? Sollen diese auch als 1 angesehen werden?
C++ schließt diese Probleme aus, indem zum Zählen Variablen vom Typ int verwendet werden.
Berechnungsgeschwindigkeit
Historisch gesehen kann ein Computerprozessor ganzzahlige Arithmetik schneller ausführen als
Arithmetik auf Gleitkommazahlen. Wenn also ein Prozessor in einer gegebenen Zeit 1000 ganze
Zahlen addieren kann, so kann es sein, dass dieser Prozessor in der gleichen Zeit nur 200 Gleitkom-
maberechnungen ausführen kann. Das Problem der Berechnungsgeschwindigkeit wurde immer
kleiner durch die Weiterentwicklung der Mikroprozessoren. Die meisten modernen Prozessoren ent-
halten spezielle Schaltkreise, mit denen Sie Gleitkommaberechnungen fast so schnell wie Ganzzah-
lenberechnungen ausführen können.
Genauigkeitsverluste
Auch Gleitkommavariablen lösen nicht alle Berechnungsprobleme. Gleitkommavariablen haben
eine begrenzte Genauigkeit: ungefähr sechs Stellen.
Um zu sehen, warum dies ein Problem ist, betrachten Sie die Zahl 1⁄3, die als 0.333 ... mit Fortset-
zung dieser Reihe dargestellt wird. Das Prinzip einer endlosen Fortsetzung dieser Reihe macht in der
Mathematik Sinn, aber nicht in einem Computer. Der Computer hat eine begrenzte Genauigkeit. So
ist eine Gleitkommaversion von . ungefähr 0.333333. Wenn diese 6 Nachkommastellen wieder mit
3 multipliziert werden, berechnet der Prozessor einen Wert von 0.999999 anstatt des mathematisch
erwarteten Wertes 1. Der Genauigkeitsverlust, der auf die Beschränkungen der Gleitkommazahlen
zurückzuführen ist, wird als Rundungsfehler bezeichnet. C++ kann viele Formen von Rundungsfeh-
lern beheben. Z.B. kann C++ feststellen in der Ausgabe , dass der Benutzer 1 anstelle von 0.999999
gemeint hat. In anderen Fällen jedoch kann sogar C++ die Rundungsfehler nicht beheben.
Lektion 5 – Variablentypen 47
Nur die ersten sechs Ziffern haben eine Bedeutung, weil die restlichen 32 Ziffern
=
= =
=
unter Rundungsfehlern zu leiden haben. Eine Gleitkommavariable kann einen
Wert von 123.000.000 ohne Rundungsfehler speichern, nicht aber
Hinweis
123.456.789.
Teil 2 – Samstagmorgen
5.2 Andere Variablentypen.
C++ stellte andere Variablentypen neben int und float bereit. Diese sind in Tabelle 5-1 zu sehen.
Jeder Typ hat seine Vorteile und seine Grenzen.
Lektion 5
Tabelle 5-1: Andere C++-Variablentypen
string “Zeichenkette“ Eine Kette von Zeichen; ein Satz. Wird benutzt, um
ganze Phrasen zu speichern. (Die doppelten Hoch-
kommata zeigen an, dass es sich um eine Zeichenkette
handelt.)
long 10L Ein großer Integertyp mit einem Bereich von -2 Milliar-
den bis 2 Milliarden.
So deklariert
// deklariere eine long-Variable und setze sie auf 1
long lVariable;
lVariable = 1;
// deklariere eine double-Variable und setze
// sie auf 1.0
double dVariable;
dVariable = 1.0;
die Variable lVariable als Variable vom Typ long und setzt ihren Wert auf 1, während dVaria-
ble vom Typ double ist, und auf den Wert 1.0 gesetzt wird.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 48
48 Samstagmorgen
!
Tipp
Es ist möglich, eine Variable in der gleichen Anweisung zu deklarieren und zu
initialisieren:
int nVariable = 1; // deklariere eine Variable und
// initialisiere sie mit 1
=
= =
=
Hinweis Variablennamen haben für den C++-Compiler keine besondere Bedeutung.
Eine Variable von Typ char kann ein einzelnes Zeichen speichern, während eine Zeichenkette
eine Kette von Zeichen enthält. So ist ‘a’ das Zeichen a, während “a“ eine Zeichenkette ist, die nur
das Zeichen a enthält. (»Zeichenkette« ist eigentlich kein Variablentyp, aber in den meisten Fällen
können Sie sie als solchen behandeln. Sie erfahren mehr Details über Zeichenketten in Sitzung 14).
Das Zeichen ‘a’ und die Zeichenkette “a“ sind nicht das Gleiche. Wenn eine
Warnung Anwendung eine Zeichenkette benötigt, können Sie nicht ein einzelnes Zeichen
zur Verfügung stellen, selbst wenn die erwartete Zeichenkette nur ein einzelnes
Zeichen enthält.
long und double sind erweiterte Formen von int und float – long steht für langes Integer, und
double steht für doppeltes float.
5.2.2 Sonderzeichen
Allgemein können Sie jedes druckbare Zeichen in einer char-Variable oder in einer Zeichenkette
speichern. Es gibt auch eine Menge von nicht druckbaren Zeichen, die so wichtig sind, dass das Glei-
che auch für sie gilt. Tabelle 5-2 listet diese Zeichen auf.
Sie haben bereits das Zeichen für den Zeilenumbruch gesehen. Dieses Zeichen bricht eine Zei-
chenkette in einzelne Zeilen um.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 49
Lektion 5 – Variablentypen 49
Zeichenkonstante Bedeutung
Teil 2 – Samstagmorgen
‘\\’ Backslash
Bis jetzt haben wir einen Zeilenumbruch nur ans Ende einer Zeichenkette gestellt. Dieses Zeichen
Lektion 5
kann aber an jeder beliebigen Stelle in der Zeichenkette vorkommen. Betrachten Sie z.B. die fol-
gende Zeichenkette:
cout << »Das ist Zeile 1\n Das ist Zeile 2«;
In gleicher Weise bewegt das ‘\t’-Zeichen die Ausgabe an die nächste Tabulatorposition. Was das
genau bedeutet, hängt vom Typ des Computers ab, den Sie benutzen.
Da das Backslash-Zeichen verwendet wird, um spezielle Zeichen darzustellen, muss es ein Zei-
chenpaar geben, um den Backslash selber darzustellen. Das Zeichen ‘\\’ stellt den Backslash dar.
Leider erzeugt der Gebrauch des Backslash von MS-DOS und C++ einen Konflikt. Der MS-DOS-
Pfad Root\FolderA\File wird durch die C++-Zeichenkette ”Root\\FolderA\\File” dargestellt. Der
doppelte Backslash wird durch den speziellen Gebrauch des Backslash-Zeichens in C++ notwen-
dig.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 50
50 Samstagmorgen
Ein Ausdruck, in dem die beiden Operanden nicht vom gleichen Typ sind, wird Mischmodusaus-
druck genannt. Mischmodusausdrücke erzeugen einen Wert von dem mächtigeren Typ der beiden
Operanden. In diesem Fall wird nValue1 in ein double konvertiert, bevor die Berechnung fortge-
setzt wird.
In gleicher Weise kann ein Ausdruck eines Typs einer Variablen eines anderen Typs zugewiesen
werden wie in der folgenden Anweisung:
// in der folgenden Anweisung wird der ganzzahlige
// Anteil von fVariable in nVariable gespeichert
fVariable = 1.0;
int nVariable;
nVariable = fVariable;
Genauigkeit oder Teile des Zahlenbereichs können verloren gehen, wenn die
=
= =
= Variable auf der linken Seite der Anweisung »kleiner« ist. Im vorangegangenen
Hinweis Beispiel muss der Wert von fVariable abgeschnitten werden, bevor er in
nVariable gespeichert werden kann.
0 Min.
int nVariable = 1;
double dVariable = nVariable1;
!
Tipp
Mischmodusausdrücke sind keine besonders gute Idee. Sie sollten Ihre eigenen
Entscheidungen treffen, anstatt sie C++ zu überlassen. Es kann sein, dass C++
nicht richtig versteht, was Sie eigentlich wollen.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 51
Lektion 5 – Variablentypen 51
Namenskonventionen
Sie werden bemerkt haben, dass ich jeden Variablennamen mit einem bestimmten Zeichen begin-
ne, das nichts mit dem Namen zu tun zu haben scheint. Diese speziellen Zeichen sind unten dar-
gestellt. Wenn Sie diese Konvention verwenden, können Sie sofort erkennen, dass die Variable
dVariable vom Typ double ist.
Teil 2 – Samstagmorgen
Zeichen Typ
n int
l long
Lektion 5
f float
d double
c char (Zeichen)
sz Zeichenkette
Diese führenden Zeichen helfen dem Programmierer, den Überblick über die Variablentypen zu
behalten. Sie können sofort erkennen, dass es sich bei dem folgenden Ausdruck um einen
Mischmodusausdruck handelt, der eine long-Variable und eine int-Variable enthält.
nVariable = lVariable;
Bedenken Sie jedoch, dass die Verwendung spezieller Zeichen in Variablennamen für C++ keine
besondere Bedeutung hat. Ich hätte genauso q zur Identifizierung von int verwenden können.
Manche Programmierer verwenden überhaupt keine Namenskonvention.
Zusammenfassung.
Wie Sie gesehen haben, sind Integervariable effizient in Bezug auf Rechenzeit, und sie sind einfach
zu handhaben. Sie haben jedoch Begrenzungen, wenn sie in Berechnungen eingesetzt werden.
Gleitkommazahlen sind ideal für die Verwendung in mathematischen Gleichungen, da sie nicht
unter signifikanten Rundungsfehlern oder einer signifikanten Beschränkung ihres Bereiches leiden.
Auf der anderen Seite sind Gleitkommavariablen schwerer zu handhaben und nicht so universell ein-
setzbar wie Integervariablen.
• Integervariablen stellen Zähler dar, wie 1, 2, usw.
• Integervariablen haben einen Bereich von -2 Milliarden bis 2 Milliarden.
• Gleitkommavariablen stellen Dezimalbrüche dar.
• Gleitkommavariablen haben praktisch einen unbeschränkten Bereich.
• Der Variablentyp char wird zur Darstellung von ANSI-Zeichen verwendet.
C++ Lektion 05 31.01.2001 12:12 Uhr Seite 52
52 Samstagmorgen
Selbsttest.
1. Was ist der Bereich von int-Variablen? (Siehe »Eingeschränkter Bereich«)
2. Warum leiden float-Variablen nicht unter signifikanten Rundungsfehlern? (Siehe »Lösen des
Abschneideproblems«)
3. Was ist der Typ der Konstante 1? Was ist der Typ von 1.0? (Siehe »Typen von Konstanten«)
C++ Lektion 06 31.01.2001 12:12 Uhr Seite 53
Mathematische
Operationen 6
Lektion
Checkliste.
✔ Mathematische Operatoren von C++ gebrauchen
✔ Ausdrücke erkennen
✔ Klarheit durch spezielle mathematische Operatoren erhöhen
D
ie Programme Conversion und Average haben einfache mathematische
Operationen wie Addition, Multiplikation und Division verwendet. Ich
habe diese Operatoren verwendet, ohne sie vorher zu beschreiben, weil
sie intuitiv klar sind. Diese Sitzung beschreibt die Menge der mathematischen
30 Min. Operatoren.
Die mathematischen Operatoren sind in Tabelle 6-1 aufgelistet. Die erste Spal-
te listet die Operatoren von oben nach unten geordnet, gemäß ihrer Priorität.
54 Samstagmorgen
ist gleich
IntVaue – (IntValue / IntDivisor) * IndDivisor
=
= =
=
Weil modulo auf Rundungsfehlern basiert, die Integerzahlen inhärent sind, ist
modulo für Gleitkommazahlen nicht definiert.
Hinweis
6.2 Ausdrücke.
Der häufigste Typ von C++-Anweisungen ist der Ausdruck. Ein Ausdruck ist eine Anweisung, die
einen Wert hat. Alle Ausdrücke haben einen Wert.
Z.B. ist eine Anweisung, die einen mathematischen Operator enthält, ein Ausdruck, da alle diese
Operatoren einen Wert zurückgeben. Ausdrücke können einfach oder auch kompliziert sein. In der
Tat ist »1« ein Ausdruck. Es gibt fünf Ausdrücke in der folgenden Anweisung:
z = x * y + w;
1. x * y + w
2. x * y
3. x
4. y
5. w
Ein ungewöhnlicher Gesichtspunkt in C++ ist, dass ein Ausdruck eine vollständige Anweisung ist.
Somit ist das folgende eine gültige C++-Anweisung.
1;
C++ Lektion 06 31.01.2001 12:12 Uhr Seite 55
Alle Ausdrücke haben einen Typ. Wie wir bereits festgestellt haben, ist der Typ des Ausdrucks 1
gleich int. In einer Zuweisung ist der Typ des Ausdrucks auf der rechten Seite der Anweisung gleich
dem Typ der Variablen auf der linken Seite des Ausdrucks – wenn nicht, führt C++ die notwendigen
Konvertierungen durch.
Teil 2 – Samstagmorgen
Jeder der C++-Operatoren hat eine Priorität, mit der er innerhalb von zusammenge-
setzten Ausdrücken (d.h. Ausdrücken mit mehr als einem Operator) ausgewertet
wird. Diese Eigenschaft wird als Vorrang bezeichnet.
Lektion 6
20 Min.
Der Ausdruck
x/100 + 32
teilt x durch 100 bevor 32 addiert wird. In einem gegebenen Ausdruck führt C++ Multiplikatio-
nen und Divisionen vor Additionen und Subtraktionen aus. Wir sagen, dass Multiplikation und Divi-
sion Vorrang vor Addition und Subtraktion haben.
Was ist, wenn der Programmierer x durch 100 plus 32 teilen möchte? Der Programmierer kann
Ausdrücke durch Klammern verbinden:
x/(100 + 32)
Dies hat den gleichen Effekt, wie x durch 132 zu teilen. Der Ausdruck innerhalb der Klammern
wird zuerst ausgewertet. Dies ermöglicht dem Programmierer die Vorrangsregeln einzelner Opera-
toren zu überschreiben.
Der ursprüngliche Ausdruck
x / 100 + 32
Der Vorrang der Operatoren aus Tabelle 6-1 ist in Tabelle 6-2 zu sehen.
56 Samstagmorgen
Operatoren mit der gleichen Priorität werden von links nach rechts ausgewertet. Somit ist der
Ausdruck
x / 10 / 2
(x / 10) / 2
Mehrstufige Klammerungen werden von innen nach außen ausgewertet. Im folgenden Ausdruck
(y / (2 + 3)) / x
wird die Variable y durch 5 geteilt, und das Ergebnis wird durch x geteilt.
=
= =
= Die Inkrement- und Dekrementoperatoren sind auf nicht-Gleitkomma-Variablen
Hinweis beschränkt.
C++ Lektion 06 31.01.2001 12:12 Uhr Seite 57
Die Inkrement- und Dekrementoperatoren sind in der Hinsicht besonders, dass Sie in zwei For-
men vorkommen: einer Präfix-Version und einer Postfix-Version.
=
= =
= Die Präfix-Version des Inkrements wird als ++x geschrieben, während das Post-
Hinweis fix durch x++ ausgedrückt wird.
Teil 2 – Samstagmorgen
Betrachten Sie den Inkrement-Operator (der Dekrement-Operator ist genau analog). Nehmen
Sie an, dass die Variable n den Wert 5 hat. Beide, n++ und ++n, inkrementieren den Wert von n zu 6.
Lektion 6
Der Unterschied ist, dass der Wert von ++n in einem Ausdruck gleich 6 ist, während der Wert von
n++ in einem Ausdruck gleich 5 ist. Das folgende Beispiel demonstriert das:
// deklariere drei int-Variablen
int n1, n2, n3;
// der Wert von n1 und n2 ist gleich 6
n1 = 5;
n2 = ++n1;
Somit bekommt n2 den Wert von n1, nachdem n1 über die Präfix-Version inkrementiert wurde,
während n3 den Wert von n1 erhält, bevor n1 über die Postfix-Version inkrementiert wird.
6.5 Zuweisungsoperatoren.
Die Zuweisungsoperatoren sind binäre Operatoren, die das Argument auf ihrer linken Seite verän-
dern.
Der einfache Zuweisungsoperator ‘=’ ist eine absolute Notwendigkeit in jeder Programmierspra-
che. Der Operator speichert den Wert des Arguments auf der rechten Seite im Argument auf der lin-
ken Seite. Die anderen Zuweisungsoperatoren scheinen irgendeiner Laune entsprungen zu sein.
Die Autoren von C++ haben festgestellt, dass Zuweisungen oft die folgende Form haben:
variable = variable # constant
wobei ‘#’ ein binärer Operator ist. Um also einen Integeroperanden um 2 zu inkrementieren,
könnte der Programmierer schreiben:
nVariable = nVariable + 2;
Dies besagt, dass 2 zum Wert von nVariable hinzugefügt und das Ergebnis in nVariable
gespeichert werden soll.
=
= =
=
Es ist üblich, dieselbe Variable auf der linken und auf der rechten Seite der
Zuweisung stehen zu haben.
Hinweis
C++ Lektion 06 31.01.2001 12:12 Uhr Seite 58
58 Samstagmorgen
Weil die gleiche Variable auf der linken und der rechten Seite des Gleichheitszei-
chens steht, haben sie entschieden, dem Zuweisungsoperator einen weiteren Ope-
rator hinzuzufügen. Alle binären Operatoren haben eine Zuweisungsversion. Somit
kann die obige Anweisung wie folgt geschrieben werden:
0 Min. nVariable += 2;
Noch mal, dies besagt, dass 2 zum Wert von nVariable hinzugefügt werden soll.
!
Anders als die Zuweisung selber, werden diese Zuweisungsoperatoren nicht all-
zu oft benutzt. In bestimmten Fällen kann Ihre Verwendung jedoch das Pro-
gramm deutlich lesbarer machen.
Tipp
Zusammenfassung.
Die mathematischen Operatoren werden in C++-Programmen öfter verwendet als alle anderen
Operatoren. Das ist wenig verwunderlich: C++-Programme konvertieren immer Temperaturen von
Grad Celsius in Grad Fahrenheit und zurück und führen unzählige andere Operationen durch, die
Addition, Subtraktion und Zählen erforderlich machen.
• Alle Ausrücke haben einen Wert und einen Typ.
• Die Ordnung der Auswertung innerhalb eines Ausdrucks wird normalerweise bestimmt durch den
Vorrang der Operatoren. Diese Reihenfolge kann mit Hilfe von Klammern überschrieben werden.
• Für viele der am häufigsten verwendeten Ausdrücke stellt C++ Kurzformen bereit. Der geläufigste
ist der i++-Operator anstelle von i = i + 1.
Selbsttest.
1. Was ist der Wert von 9 % 4? (Siehe »Arithmetische Operatoren«)
2. Ist 9 % 4 ein Ausdruck? (Siehe »Ausdrücke«)
3. Wenn n = 4, was ist der Wert von n + 10 / 2? Warum ist er 9 und nicht 7? (Siehe »Vorrang von
Operatoren«)
4. Wenn n = 4, was ist der Unterschied zwischen ++n und n++? (Siehe »Unäre Operatoren«)
5. Wie würde man n = n + 2 unter Verwendung des Operators += schreiben? (Siehe »Zuweisungs-
operatoren«)
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 59
Logische Operationen
7
Lektion
Checkliste.
✔ Einfache logische Operatoren und Variablen einsetzen
✔ Mit binären Zahlen arbeiten
✔ Bitweise Operationen ausführen
✔ Logischer Zuweisungsanweisungen erzeugen
V
ielleicht mit Ausnahme der Inkrement- und Dekrementoperatoren sind die
mathematischen Operatoren von C++ geläufige Operatoren und kommen
im Alltag vor. Im Vergleich dazu sind die logischen Operatoren von C++
unbekannt.
30 Min. Es ist nicht so, dass sich die Menschen nicht mit logischen Operationen
beschäftigen. Wenn Sie sich an den mechanischen Reifenwechsler in Sitzung 1
erinnern, ist die Fähigkeit, logische Berechnungen auszudrücken, wie »wenn der Reifen platt ist
UND ich einen Schraubenschlüssel habe...« ein Muss. Die Menschen berechnen ständig AND und
OR, sie sind nur nicht daran gewöhnt, das hinzuschreiben.
Logische Operatoren zerfallen in zwei Klassen. Der erste Typ, den ich einfache logische Operato-
ren nenne, sind Operatoren, die im alltäglichen Leben vorkommen. Der zweite Typ, die bitweisen
logischen Operatoren, gibt es nur in der Welt des Computers. Ich werde erst die einfachen Operato-
ren vorstellen, bevor wir uns den bitweisen Operatoren zuwenden.
60 Samstagmorgen
== Gleichheit; wahr, wenn das Argument auf der linken Seite den gleichen Wert wie
das Argument auf der rechten Seite hat
>, < größer als, kleiner als; wahr, wenn das Argument auf der linken Seite größer/klei-
ner als das Argument auf der rechten Seite ist
>=, < = größer oder gleich, kleiner oder gleich; wahr, wenn entweder > oder == wahr sind
/ < oder == wahr sind
&& AND; wahr, wenn beide Argumente auf der rechten und der linken Seite wahr
sind
|| OR; wahr, wenn das linke Argument oder das rechte Argument wahr ist
Die ersten sechs Einträge in Tabelle 7-1 sind die Vergleichsoperatoren. Der Gleichheitsoperator wird
verwendet, um zwei Zahlen miteinander zu vergleichen. Z.B. ist das Folgende wahr (true), wenn der
Wert der Variable nVariable gleich 0 ist:
nVariable == 0;
Die Operatoren Größer-als (>) und Kleiner-als (< ) sind ähnlich geläufig im alltäglichen Leben.
Der folgende logische Vergleichsausdruck ist wahr:
int nVariable1 = 1;
int nVariable2 = 2;
nVariable1 < nVariable2;
Die Operatoren Größer-oder-gleich (>=) und Kleiner-oder-gleich (< =) sind ähnlich, nur dass sie
die Gleichheit mit einschließen, was die anderen Operatoren nicht tun.
Die Operatoren && (AND) und || (OR) sind ähnlich geläufig. Diese Operatoren werden typi-
scherweise mit den anderen logischen Operatoren kombiniert:
// wahr wenn nV2 größer als nV1, aber kleiner als nV3
(nV1 < nV2) && (nV2 < nV3)
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 61
Seien Sie vorsichtig mit der Verwendung des Vergleichsoperators auf Gleitkom-
mazahlen. Betrachten Sie das folgende Beispiel:
Warnung
float fVariable1 = 10.0;
float fVariable2 = (10 / 3) * 3;
Teil 2 – Samstagmorgen
10/3 ist gleich 3.333..., aber C++ kann nicht eine unendliche Anzahl von Nach-
kommastellen darstellen. Als Gleitkommazahl gespeichert, wird 10/3 etwa zu
3.333333. Wenn Sie diese Zahl mit 3 multiplizieren, bekommen Sie 9.999999
als Ergebnis, was nicht genau gleich 10 ist.
Lektion 7
Genauso wie (10.0 / 3) * 3 nicht genau 10.0 ist, kann das Ergebnis einer
Gleitkommarechnung ein klein wenig daneben liegen. Solche kleinen Abwei-
chungen werden Sie oder ich kaum beachten, aber der Computer. Gleichheit
bedeutete exakte Gleichheit.
Ein sicherer Vergleich sieht so aus:
float fDelta = fVariable1 – fVariable2;
fDelta < 0.01 && fDelta > -0.01;
Der Vergleich liefert wahr, wenn die Variablen fVariable1 und fVariable2
innerhalb eines Deltawertes nebeneinander liegen. (Der Begriff »Delta« ist mit
Absicht vage – er soll einen akzeptablen Fehler darstellen.)
Wenn condition1 nicht wahr ist, dann ist das Ergebnis nicht wahr, unabhängig davon, welchen
Wert condition2 hat (d.h. condition2 kann wahr oder falsch sein ohne Einfluss auf das Ergebnis).
In gleicher Weise ist
condition1 || condition2;
wahr, wenn condition1 wahr ist, unabhängig davon, welchen Wert condition2 hat.
Um Zeit zu sparen, wertet C++ condition1 zuerst aus. C++ wertet condition2 nicht aus, wenn
condition1 bereits FALSE ist im Falle von &&, bzw. TRUE im Falle von ||.
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 62
62 Samstagmorgen
!
Tipp
Microsoft Visual Basic verwendet auch Integerwerte zur Speicherung von TRUE
und FALSE, Vergleichsoperatoren geben in Visual Basic jedoch 0 (FALSE) oder -1
(TRUE) zurück.
=
= =
= Weil der Begriff »Ziffer« sich auf ein Vielfaches von 10 bezieht, wird eine binäre
Hinweis Ziffer als Bit bezeichnet. Bit kommt von binary digit. Acht Bits bilden ein Byte.
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 63
Mit einer so kleinen Basis ist es nötig, viele Bits zur Darstellung großer Zahlen zu verwenden. Es
ist unbequem, einen Ausdruck wie 011110112 für die Darstellung eines einfachen Wertes wie 12310
zu verwenden. Programmierer bevorzugen es, Zahlen als Einheiten von jeweils 4 Bits darzustellen.
Eine einzelne 4-Bit-Ziffer ist im Wesentlichen die Basis 16, weil 4 Bits jeden Wert zwischen 0 und
15 darstellen können. Die Basis 16 ist unter dem Namen Hexadezimalsystem bekannt. Hexadezimal
wird oft mit hex abgekürzt.
Hexadezimalzahlen verwenden die gleichen Ziffern für die Zahlen von 0 bis 9. Für die Ziffern zwi-
Teil 2 – Samstagmorgen
schen 9 und 16 verwenden die Hexadezimalzahlen die ersten sechs Buchstaben des Alphabets. A für
10, B für 11, usw. Es ist also 12310 gleich 7B16.
7 * 16 + B (d.h. 11) * 1 = 123
Lektion 7
Weil Programmierer es bevorzugen, Zahlen in 4, 8, 32 oder 64 Bits auszudrücken, bevorzugen
Sie in ähnlicher Weise eine Darstellung hexadezimaler Zahlen mit 1, 2, 4 oder 8 hexadezimalen Zif-
fern, selbst wenn die führenden Ziffern 0 sind.
Schließlich ist es unbequem, eine Hexadezimalzahl wie 7B mit Hilfe des Subscripts 16 auszudrü-
cken, weil Terminals das nicht unterstützen. Selbst mit einem Textprogramm, wie ich es gerade
benutze, ist es unbequem, den Zeichensatz auf Subscript umzuschalten und wieder zurück, nur um
diese beiden Ziffern zu schreiben. Deshalb verwenden Programmierer die Konvention, dass eine
Hexadezimalzahl mit "0x" beginnt (der Grund für eine solch merkwürdige Konvention geht auf die
frühen Tage von C zurück). Dann wird 7B zu 0x7B. Mit dieser Konvention sind die Zahlen 0x123 und
123 voneinander unterscheidbar. (0x123 entspricht 291 dezimal.)
Geschrieben in Tabellenform sieht das wie in den Tabellen 7-2 und 7-3 aus.
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 64
64 Samstagmorgen
In Tabelle 7-2 wird das Ergebnis von 1 AND 0 in Zeile 1 gezeigt (1 in Zeilenkopf) und Spalte 2 (0 im
Spaltenkopf).
Ein anderer logischer Operator, der so im alltäglichen Leben nicht vorkommt, ist der Operator
»oder sonst«, der üblicherweise mit XOR abgekürzt wird. XOR liefert wahr, wenn eines der Argu-
mente wahr ist, aber nicht beide Argumente gleichzeitig wahr sind. XOR ist in Tabelle 7-4 darge-
stellt.
Ausgerüstet mit diesen Einzelbit-Operatoren können wir uns den logischen Operatoren von C++
zuwenden.
Der Operator NOT ist am einfachsten zu verstehen. NOT konvertiert 1 in 0 und 0 in 1. (D.h. 0 ist
NOT 1 und 1 ist NOT 0.)
~01102 (0x6)
10012 (0x9)
Der Operator NOT ist der einzige unäre bitweise logische Operator. Die folgende Rechnung demon-
striert den &-Operator.
Teil 2 – Samstagmorgen
01102
&
00112
00102
Lektion 7
Von links nach rechts: 0 AND 0 ist 0 (erstes Bit), 1 AND 0 ist 0 (zweites Bit), 1 AND 1 ist 1 (drittes Bit)
und 0 AND 1 ist 0 (am wenigsten signifikantes Bit).
Die gleichen Berechnungen können auf Zahlen ausgeführt werden, die als Hexadezimalzahl dar-
gestellt sind, indem sie erst in Binärzahlen verwandelt werden und dann das Ergebnis in Hexadezi-
maldarstellung konvertiert wird.
0x6 01102
& -> &
0x3 00112
00102 -> 0x2
!
Tipp
Solche Hin- und Herkonvertierungen sind viel einfacher mit Hexadezimalzahlen
auszuführen als mit Dezimalzahlen. Glauben Sie es oder nicht, mit ein wenig
Erfahrung können Sie bitweise Operationen im Kopf ausführen.
66 Samstagmorgen
Ausgabe:
Die erste Anweisung, die cout.setf(ios::hex); lautet, setzt das Ausgabeformat von standardmä-
ßig dezimal auf hexadezimal (im Augenblick müssen Sie mir glauben, dass das so funktioniert).
Der Rest des Programms ist einfach. Das Programm liest nArg1 und nArg2 von der Tastatur und
gibt dann alle Kombinationen von bitweisen Berechnungen aus.
Die Ausgabe des Programms bei Eingabe von 0x1234 und 0x00ff sehen Sie oben am Ende des
Listings.
=
= =
= Hexadezimalzahlen werden mit einem führenden 0x geschrieben.
Hinweis
7.3.4 Warum?
Der Sinn der meisten Operatoren ist klar. Niemand würde nach dem Sinn des Plus-Operators oder
des Minus-Operators fragen. Der Sinn der Operatoren < und > ist klar. Für den Anfänger muss nicht
klar sein, wann und warum man bitweise Operatoren verwendet.
Der Operator AND wird oft verwendet, um Information auszumaskieren. Nehmen Sie z.B. an,
dass wir die am wenigsten signifikante Hexadezimalstelle aus einer Zahl mit vier Ziffern extrahieren
möchten.
0x1234 0001 0010 0011 0100
& -> &
0x000F 0000 0000 0000 1111
0000 0000 0000 0100 -> 0x0004
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 67
Eine andere Anwendung ist das Setzen und Auslesen einzelner Bits.
Nehmen Sie an, dass wir in einer Datenbank Informationen über Personen in einem einzelnen
Byte pro Person speichern. Das signifikanteste Bit könnte z.B. gesetzt werden, wenn die Person
männlich ist, das nächste Bit, wenn es ein Programmierer ist, das nächste, wenn die Person attraktiv
ist, und das am wenigsten signifikante Bit, wenn die Person einen Hund hat.
Bit Bedeutung
Teil 2 – Samstagmorgen
0 1 -> männlich
1 1 -> Programmierer
Lektion 7
2 1 -> attraktiv
3 1 -> hat einen Hund
Dieses Byte würde für jede Person kodiert und zusammen mit dem Namen, Versi-
cherungsnummer und allen weiteren legalen Informationen gespeichert.
Ein hässlicher männlicher Programmierer, der einen Hund besitzt, würde als
11012 kodiert. Um alle Einträge in der Datenbank zu testen, um nach nicht attrakti-
ven Programmierern zu suchen, die keinen Hund haben, unabhängig vom
0 Min.
Geschlecht, würden wir den folgenden Ausdruck verwenden:
value & 0x0110) == 0x0100
*^^* ^ -> 0 = nicht attraktiv
^ 1 = ist Programmierer
* -> nicht von Interesse
^ -> von Interesse
=
= =
= In diesem Fall wird der Wert 0110 als Maske bezeichnet, weil er Bits ausmas-
Hinweis kiert, die nicht von Interesse sind.
Zusammenfassung.
Sie haben die mathematischen Operatoren aus Kapitel 6 bereits in der Schule gelernt. Sie haben
dort sicherlich nicht die einfachen logischen Operatoren gelernt, sie kommen aber im alltäglichen
Leben vor. Operatoren wie AND und OR sind ohne Erklärung verständlich. Da C++ keinen logischen
Variablentyp hat, verwendet es 0 zur Darstellung von FALSE und alles andere zur Darstellung von
TRUE.
Im Vergleich dazu sind die binären Operatoren etwas Neues. Diese Operatoren führen die glei-
chen Operationen AND und OR aus, aber auf jedem Bit separat.
C++ Lektion 07 31.01.2001 12:13 Uhr Seite 68
68 Samstagmorgen
Selbsttest.
1. Was ist der Unterschied zwischen den Operatoren && und &? (Siehe »Einfache logische Opera-
toren«)
2. Was ist der Wert von (1 && 5)? (Siehe »Einfache logische Operatoren«)
3. Was ist der Wert von 1 & 5? (Siehe »Bitweise logische Operatoren«)
4. Drücken Sie 215 als Summe von Zehnerpotenzen aus. (Siehe »Binäre Zahlen«)
5. Drücken Sie 215 als Summe von Zweierpotenzen aus. (Siehe »Binäre Zahlen«)
6. Was ist 215 in Hexadezimaldarstellung? (Siehe »Binäre Zahlen«)
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 69
Kommandos zur
Flusskontrolle 8
Lektion
Checkliste.
✔ Kontrolle über den Programmfluss
✔ Wiederholte Ausführung einer Gruppe
✔ Vermeidung von »Endlosschleifen«
D
ie Programme, die bisher im Buch vorgekommen sind, waren sehr ein-
fach. Jedes Programm hat eine Menge von Eingabewerten entgegenge-
nommen, das Ergebnis ausgegeben und die Ausführung beendet. Das ist
ähnlich wie bei unserem computerisierten Mechaniker, wenn wir ihn anweisen,
wie eine Schraube zu lösen ist, ohne ihm die Möglichkeit zu geben, zur nächsten
30 Min.
Schraube oder zum nächsten Reifen zu gehen.
Was in unseren Programmen fehlt, ist eine Form von Flusskontrolle. Wir haben bisher keine Mög-
lichkeit, kleinere Tests zu machen, und können keine Entscheidungen basierend auf diesen Tests
treffen.
Dieses Kapitel widmet sich den verschiedenen C++-Kommandos zur Flusskontrolle.
70 Samstagmorgen
Zuerst wird die Bedingung m > n ausgewertet. Wenn das Ergebnis wahr ist, wird die Kontrolle an
die Anweisungen übergeben, die der öffnenden Klammer { folgen. Wenn m nicht größer ist als n,
wird die Kontrolle an die Anweisungen übergeben, die der öffnenden Klammer unmittelbar nach
dem else folgen.
Der else-Zweig ist optional. Wenn er nicht da ist, verhält sich C++ so, als wäre er da, aber leer.
Tatsächlich sind die Klammern optional, wenn nur eine Anweisung in einem
=
= =
=
Zweig ausgeführt werden soll; es ist aber so einfach, Fehler zu machen, die der
Compiler nicht abfangen kann, wenn nicht die Klammern als Marker verwen-
Hinweis det werden können. Es ist viel sicherer, immer Klammern zu verwenden. Wenn
Ihre Freunde Sie verführen wollen, keine Klammern zu verwenden, sagen Sie
einfach »NEIN«.
return 0;
}
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 71
Hier liest das Programm Integerzahlen von der Tastatur und verzweigt entsprechend. Das Pro-
gramm erzeugt die folgende typische Ausgabe:
Eingabe nArg1: 10
Eingabe nArg1: 8
nArg1 größer als nArg2
8.2 Schleifenkommandos.
Teil 2 – Samstagmorgen
Verzweigungskommandos ermöglichen Ihnen, den Programmfluss den einen oder den anderen
Pfad hinunter zu leiten. Das ist das C++-Äquivalent dazu, den computerisierten Mechaniker ent-
scheiden zu lassen, ob er einen Schraubenschlüssel oder einen Schraubendreher verwendet, abhän-
Lektion 8
gig von der Problemstellung. Das bringt uns aber noch nicht zu dem Punkt, an dem der Mechaniker
den Schraubenschlüssel mehr als einmal drehen kann, mehr als eine Radmutter entfernen kann oder
mehr als einen Reifen des Autos bearbeiten kann. Dafür brauchen wir Schleifenanweisungen.
Die Bedingung condition wird geprüft. Wenn sie wahr ist, dann werden die Anweisungen inner-
halb der Klammern ausgeführt. Sobald die schließende Klammer angetroffen wird, wird die Kon-
trolle an den Anfang zurückgegeben, und der Prozess beginnt von vorne. Der Effekt ist, dass der
C++-Code zwischen den Klammern so lange ausgeführt wird, wie die Bedingung wahr ist.
=
= =
=
Die Bedingung wird nur am Anfang der Schleife überprüft. Selbst wenn die
Bedingung in der Schleife nicht mehr erfüllt ist, wird die Kontrolle die Schleife
Hinweis
nicht verlassen, bis sie wieder an den Anfang der Schleife kommt.
Wenn die Bedingung zum ersten Mal wahr ist, was wird sie dann später falsch machen? Betra-
chen Sie das folgende Programm.
72 Samstagmorgen
WhileDemo beginnt mit der Abfrage einer Schleifenanzahl vom Benutzer, die in der Variablem
nLoopCount gespeichert wird. Wenn dies getan ist, fährt das Programm mit dem Testen von nLoop-
Count fort. Wenn nLoopCount größer ist als null, dann wird nLoopCount um eins erniedrigt, und das
Ergebnis wird auf dem Bildschirm ausgegeben. Das Programm geht dann an den Anfang der Schlei-
fe zurück, um zu testen, ob nLoopCount immer noch positiv ist.
Wenn das Programm WhileDemo ausgeführt wird, liefert es folgende Ausgabe:
Eingabe Schleifenanzahl: 5
Nur 4 weitere Schleifendurchläufe
Nur 3 weitere Schleifendurchläufe
Nur 2 weitere Schleifendurchläufe
Nur 1 weitere Schleifendurchläufe
Nur 0 weitere Schleifendurchläufe
Als ich eine Schleifenanzahl von 5 eingegeben habe, hat das Programm die Schleife fünfmal
durchlaufen und hat jedes Mal den heruntergezählten Wert ausgegeben.
Wenn der Benutzer eine negative Schleifenanzahl eingibt, überspringt das Programm die Schlei-
fe. Weil die Bedingung nie war ist, wird die Schleife nie betreten. Wenn der Benutzer eine sehr gro-
ße Zahl eingibt, läuft das Programm sehr lange in der Schleife, bis es fertig ist.
Eine andere, selten benutzte Version der while-Schleife, bekannt unter dem
Namen do-while, ist identisch mit der while-Schleife, außer dass die Bedingung
=
= =
= am Ende der Schleife getestet wird.
do
Hinweis {
// ... das Innere der Schleife
} while (condition);
Die Logik in dieser Version ist die gleiche wie im Original – der einzige Unterschied ist die Art und
Weise, in der nLoopCount dekrementiert wird.
Weil Autodekrement sowohl das Argument dekrementiert als auch dessen Wert zurückliefert,
kann der Dekrementoperator mit jeder der anderen Anweisungen verknüpft werden. Z.B. ist die fol-
gende Version die bisher kürzeste Schleifenkonstruktion:
while (nLoopCount-- > 0)
{
Teil 2 – Samstagmorgen
cout << »Nur « << nLoopCount
<< » weitere Schleifendurchläufe\n«;
}
Lektion 8
!
Tipp
Das ist die Version, die von den meisten C++-Programmierern verwendet wird.
Das ist die Stelle, wo der Unterschied zwischen Prädekrement und Postdekrement auftaucht.
=
= =
=
Beide, nLoopCount-- und --nLoopCount derementieren nLoopCount; der erste
gibt den Wert von nLoopCount vor dem Dekrementieren zurück, der zweite
Hinweis danach.
Möchten Sie, dass die Schleife ausgeführt wird, wenn der Benutzer als Schleifenanzahl 1 eingibt?
Wenn Sie die Prädekrement-Version verwenden, ist der Wert von --nLoopCount gleich 0 und der
Rumpf der Schleife wird nie betreten. Mit der Postdekrement-Version ist der Wert von nLoopCount--
gleich 1 und die Kontrolle wird an die Schleife übergeben.
Weil der Wert von nLoopCount sich niemals verändert, läuft das Programm in einer endlosen
Schleife. Ein Ausführungspfad, der unendlich oft ausgeführt wird, wird als Endlosschleife bezeichnet.
Eine Endlosschleife tritt dann auf, wenn die Bedingung, die zum Schleifenabbruch führen sollte, nie
erfüllt werden kann – im Allgemeinen durch einen Programmierfehler.
Es gibt viele Wege, eine Endlosschleife zu produzieren, die meisten sind viel komplizierter als das
hier dargestellte Beispiel.
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 74
74 Samstagmorgen
Die Ausführung der for-Schleife beginnt mit der Initialisierung. Die Initialisierung hat diesen
Namen bekommen, weil dort üblicherweise Zählvariablen initialisiert werden. Die Initialisierung
wird nur ein einziges Mal ausgeführt, wenn die for-Schleife zum ersten Mal durchlaufen wird.
Die Ausführung wird bei der Bedingung fortgesetzt. Ähnlich zu der while-Schleife wird die for-
Schleife so lange ausgeführt, wie die Bedingung wahr ist.
Nachdem die Ausführung des Code im Body der for-Schleife abgeschlossen wurde, wird die
Kontrolle an die Inkrementanweisung übergeben. Danach wird die Bedingung erneut geprüft und
der Prozess wiederholt. Die Inkrementklausel enthält normalerweise eine Autoinkrement- oder Auto-
dekrementanweisung zum Updaten der Zählvariablen.
Alle drei Klauseln sind optional. Wenn die Initialisierung oder die Inkrementklausel fehlen, igno-
riert C++ sie. Wenn die Bedingung fehlt, führt C++ die Schleife unendlich oft aus (oder bis sonst
etwas den Kontrollfluss abbricht).
Die for-Schleife kann am Beispiel besser verstanden werden. Das folgende Programm ForDemo
ist nichts anderes als das Programm WhileDemo, nur dass eine for-Schleife verwendet wird.
Diese Version durchläuft die gleichen Schleifen wie zuvor. Der Unterschied ist jedoch, dass nicht
der Wert von nLoopCount verändert, sondern eine Zählvariable verwendet wird.
Die Kontrolle beginnt mit der Deklaration einer Variablen i, die mit 1 initialisiert wird. Die for-
Schleife überprüft dann die Variable i, um sicherzustellen, dass sie kleiner oder gleich dem Wert von
nLoopCount ist. Wenn dies der Fall ist, führt das Programm die Ausgabeanweisung aus, inkremen-
tiert i und fährt mit der Ausführung der Schleife fort.
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 75
Die for-Schleife ist auch bequem, wenn Sie von 0 bis zu einer Schleifenanzahl zählen wollen,
statt von einer Schleifenanzahl auf 1 herunterzuzählen. Dies wird durch eine kleine Änderung in der
Implementierung erreicht:
Teil 2 – Samstagmorgen
#include <iostream.h>
Lektion 8
// Eingabe der Schleifenanzahl
int nLoopCount;
cout << »Eingabe Schleifenanzahl: «;
cin >> nLoopCount;
Anstatt mit der Schleifenanzahl zu beginnen, startet diese Version bei 1 und zählt hoch, bis der
vom Benutzer eingegebene Wert erreicht wird.
Die Verwendung der Variable i für for-Schleifen ist historisch bedingt (aus den
=
= =
= frühen Tagen der Programmiersprache FORTRAN). Das ist der Grund, weshalb
Hinweis diese Schleifenvariablen sich nicht an die Standardkonventionen der Namens-
bildung halten.
!
Tipp
sie nur innerhalb der for-Schleife bekannt. C++-Programmierer sagen, dass
der Gültigkeitsbereich (scope) der Variablen die for-Schleife ist. Im obigen Bei-
spiel ist die Variable i für die return-Anweisung nicht zugreifbar, weil diese
Anweisung nicht in der for-Schleife steht.
76 Samstagmorgen
Für diese Fälle definiert C++ das Kommando break. Wenn break angetroffen wird, wird die
aktuelle Schleife sofort verlassen. D.h. die Kontrolle wird dann an die Anweisung übergeben, die
unmittelbar auf die schließende Klammer folgt.
Das Format der break-Anweisung ist wie folgt:
Ausgerüstet mit diesem neuen Kommando break sieht meine Lösung für das Additionsproblem
wie das Programm BreakDemo aus:
// initialisiere Summe
int nAccumulator = 0;
cout << »Dieses Programms addiert «
<< »die eingegebenen Zahlen.\n«;
cout << »Beenden Sie die Schleife mit «
<< »einer negativen Zahl.\n«;
// »Endlosschleife«
for(;;)
{
// hole eine weitere Zahl
int nValue = 0;
cout << »Nächste Zahl: «;
cin >> nValue;
return 0;
}
Teil 2 – Samstagmorgen
Nachdem dem Benutzer die Spielregeln erklärt wurden (Eingabe einer negativen Zahl zum
Abbruch usw.), durchläuft das Programm eine Endlosschleife.
Lektion 8
!
Tipp
Eine for-Schleife ohne Bedingung ist eine Endlosschleife.
=
= =
=
Diese Schleife ist nicht wirklich endlos, weil sie eine break-Anweisung enthält.
Trotzdem wird sie als Endlosschleife bezeichnet, da die Abbruchbedingung
Hinweis nicht im Kommando selber enthalten ist.
Wenn das Programm BreakDemo erst einmal in der Schleife ist, bekommt es eine Zahl über die
Tastatur. Erst wenn das Programm eine Zahl gelesen hat, kann es bestimmen, ob diese Zahl zum
Abbruch der Schleife führt. Wenn die eingegebene Zahl negativ ist, wird die Kontrolle an break
übergeben, was zum Abbruch der Schleife führt. Wenn die Zahl nicht negativ ist, wird die break-
Anweisung übersprungen und die Kontrolle wird an den Ausdruck übergeben, der diese Zahl zur
Summe addiert.
Wenn das Programm erst einmal die Schleife verlassen hat, gibt es die Gesamtsumme aus, und
beendet die Ausführung. Hier ist die Ausgabe einer Beispielsitzung:
Wenn Sie eine Operation wiederholt auf einer Variablen ausführen, stellen Sie
!
Tipp
sicher, dass die Variable korrekt initialisiert wurde, bevor die Schleife betreten
wird. In diesem Fall setzt das Programm nAccumulator auf Null, bevor die
Schleife betreten wird, in der Werte nValue zu nAccumulator addiert werden.
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 78
78 Samstagmorgen
// Endlosschleife
for(;;)
{
// hole nächste Zahl
int nValue = 0;
cout << »Nächste Zahl: «;
cin >> nValue;
Teil 2 – Samstagmorgen
// ...und beginne mit der nächsten Zahlen-
// folge, wenn die Summe nicht null war.
} while (nAccumulator != 0);
Lektion 8
cout << »Programm wird beendet.\n«;
return 0;
}
switch(expression)
{
case c1:
// gehe hierhin, wenn expression == c1
break;
case c2:
// gehe hierhin, wenn expression == c2
break;
default:
// ansonsten gehe hierhin
}
Der Wert von expression muss ein Integer sein (int, long oder char). Die case-
Werte c1, c2, c3 müssen konstant sein. Wenn die switch-Anweisung angetroffen
wird, wird der Ausruck ausgewertet und verglichen mit den case-Konstanten. Die
Kontrolle wird an die case-Konstante übergeben, die passt. Wenn keine passende
case-Konstante gefunden wird, geht die Kontrolle an default über.
0 Min.
=
= =
= Die break-Anweisungen sind nötig, um das switch-Kommando zu verlassen.
Hinweis Ohne die break-Anweisungen würde der Fluss von einem Fall zum nächsten
fließen.
C++ Lektion 08 31.01.2001 12:18 Uhr Seite 80
80 Samstagmorgen
Zusammenfassung.
Die einfache if-Anweisung ermöglicht es dem Programmierer, den Programmfluss abhängig vom
Wert eines Ausdrucks den einen oder den anderen Pfad entlang fließen zu lassen. Die Schleifenan-
weisungen fügen die Fähigkeit hinzu, Codeblöcke wiederholt auszuführen, so lange bis eine Bedin-
gung falsch wird. Schließlich stellt die break-Anweisung einen Extralevel Kontrolle bereit, die einen
Schleifenabbruch an jeder beliebigen Stelle gestattet.
• Die if-Anweisung wertet einen Ausdruck aus. Wenn der Ausdruck nicht 0 (d.h. er ist true) ist,
wird die Kontrolle an den Block übergeben, der dem if unmittelbar folgt. Wenn nicht, wird die
Kontrolle an den else-Zweig übergeben. Wenn es keinen else-Zweig gibt, geht die Kontrolle
auf die Anweisung über, die der if-Anweisung folgt.
• Die Schleifenkommandos while, do while und for führen einen Codeblock so lange aus, bis eine
Bedingung nicht mehr wahr ist.
• Die break-Anweisung ermöglicht den Abbruch von Schleifen an jeder beliebigen Stelle.
In Sitzung 9 werden wir uns mit Wegen beschäftigen, wie C++-Programme durch die Verwendung
von Funktionen vereinfacht werden können.
Selbsttest.
1. Ist es ein Fehler, den else-Zweig eines if-Kommandos wegzulassen? Was passiert? (Siehe »Das
Verzweigungskommando«)
2. Welches sind die drei Typen von Schleifen-Kommandos? (Siehe »Schleifenkommandos«)
3. Was ist eine Endlosschleife? (Siehe »Die gefürchtete Endlosschleife«)
4. Welches sind die drei »Klauseln«, die eine for-Schleife ausmachen? (Siehe »Die for-Schleife«)
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 81
Funktionen
9
Lektion
Checkliste.
✔ void-Funktionen schreiben
✔ Funktionen mit mehreren Argumenten schreiben
✔ Funktionen überladen
✔ Funktionstemplates erzeugen
✔ Speicherklassen von Variablen bestimmen
E
inige der Beispielprogramme in Sitzung 8 sind schon ein wenig fortgeschrit-
tener, aber wir müssen weiter C++ lernen. Programme mit mehreren Schach-
telungsebenen können schwer zu überblicken sein. Fügen Sie die vielfältigen
und komplizierten Verzweigungen hinzu, wie sie von realen Anwendungen erwar-
tet werden, und die Programme sind überhaupt nicht mehr nachvollziehbar.
30 Min.
Glücklicherweise stellt C++ eine Möglichkeit bereit, selbstständige Blöcke von
Code zu separieren, die als Funktionen bezeichnet werden. In dieser Sitzung werden wir sehen, wie
C++ Funktionen deklariert, erzeugt und benutzt.
82 Samstagmorgen
9.1.1 Sammelcode
Das Programm NestedDemo in Sitzung 8 enthält eine innere Schleife, die eine Folge von Zahlen
summiert, und eine äußere Schleife, die das wiederholt ausführt, bis sich der Benutzer dazu ent-
schließt, das Programm zu beenden. Von der Logik her könnten wir die innere Schleife separieren,
d.i. der Teil des Programms, der eine Folge von Zahlen aufsummiert, von der äußeren Schleife, die
den Prozess wiederholt.
Der folgende Beispielcode zeigt das vereinfachte Programm NestedDemo, in dem die Funktion
sumSequence( ) eingeführt wurde.
!
Tipp
Den Namen von Funktionen folgen normalerweise unmittelbar ein Paar Klam-
mern.
Lektion 9 – Funktionen 83
Teil 2 – Samstagmorgen
do
{
// summiere eine Zahlenreihe, die über
// die Tastatur eingegeben wird
cout << »\Nächste Zahlenreihe\n«;
Lektion 9
nAccumulatedValue = sumSequence();
// Ausgabe der Gesamtsumme
cout << »\nDie Gesamtsumme ist »
<< nAccumulatedValue
<< »\n«;
// ... bis die Summe gleich 0 ist.
} while (nAccumulatedValue != 0);
cout << »Programm wird beendet.\n«;
return 0;
} // Ende Main
ruft die Funktion sumSequence( ) auf und speichert ihren Rückgabewert in der Variablen nAccumu-
latedValue. Dieser Wert wird in den folgenden drei Programmzeilen nacheinander ausgegeben.
Das Programm setzt die Schleife so lange fort, bis die Summe, die von der inneren Funktion zu-
rückgegeben wird, 0 ist, was anzeigt, dass der Benutzer das Programm beenden möchte.
84 Samstagmorgen
=
= =
=
In diesem Fall ist der Aufruf von sumSequence( ) ein Ausdruck, weil er einen
Wert hat. Ein solcher Aufruf kann überall da verwendet werden, wo ein Aus-
Hinweis
druck erwartet wird.
9.2 Funktion.
Das Programm FunctionDemo demonstriert die Definition und den Gebrauch einer einfachen Funk-
tion.
Eine Funktion ist ein logisch separater C++-Codeblock. Die Funktion hat die folgende allgemeine
Form:
Die Argumente einer Funktion sind Werte, die an die Funktion als Eingaben übergeben werden
können. Der Rückgabewert ist ein Wert, der von der Funktion zurückgegeben wird. Z.B. im Aufruf
square(10) ist 10 das Argument der Funktion square( ), der Rückgabewert ist 100.
Sowohl Argumente als auch Rückgabewerte sind optional. Wenn eines von beiden fehlt, wird
stattdessen das Schlüsselwort void verwendet. D.h., wenn eine Funktion eine void-Argumentliste
hat, erwartet die Funktion keine Argumente, wenn sie aufgerufen wird. Wenn der Rückgabewert
void ist, gibt die Funktion keinen Wert an den Aufrufenden zurück.
Im Beispielprogramm FunctionDemo ist der Name der Funktion sumSequence( ), der Typ des
Rückgabewertes ist int, und die Funktion hat keine Argumente.
!
Tipp
Der Default-Argumenttyp einer Funktion ist void. Somit kann eine Funktion
int fn(void) auch als int fn( ) deklariert werden.
!
Tipp
Eine gute Funktion kann in einem Satz beschrieben werden, der nur wenige
Unds und Oders enthält. Z.B. »die Funktion sumSequence( ) summiert eine Fol-
ge von Zahlen, die vom Benutzer eingegeben werden.« Diese Definition ist kurz
und klar.
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 85
Lektion 9 – Funktionen 85
Die Funktionskonstruktion machte es mir möglich, im Wesentlichen zwei verschiedene Teile des
Programms FunctionDemo zu schreiben. Ich habe mich darauf konzentriert, die Summe einer Folge
von Zahlen zu erzeugen, als ich die Funktion sumSequence( ) geschrieben habe. Ich habe mir an
dieser Stelle keine Gedanken um irgendeinen anderen Code gemacht, der diese Funktion aufrufen
könnte.
Genauso konnte ich mich beim Schreiben der Funktion main( ) auf die Behandlung der von
sumSequence( ) zurückgegebenen Werte konzentrieren. Dabei musste ich nur wissen, was die
Teil 2 – Samstagmorgen
Funktion zurückgibt, aber nicht, wie sie intern arbeitet.
Lektion 9
Die einfache Funktion sumSequence( ) gibt einen Integerwert zurück, den sie berechnet hat. Funk-
tionen können jede Art eines regulären Variablentyps zurückgeben. Z.B. kann eine Funktion double
oder char zurückgeben.
Wenn eine Funktion keinen Wert zurückgibt, dann wird der Rückgabewert dieser Funktion mit
void angegeben.
=
= =
=
Eine Funktion kann durch ihren Rückgabewert bezeichnet werden. Eine Funk-
tion, die ein int zurückgibt, wird oft als Integer-Funktion bezeichnet. Eine
Hinweis
Funktion, die keinen Wert zurückgibt, wird als void-Funktion bezeichnet.
Z.B. führt die folgende void-Funktion eine Operation durch, gibt aber keinen Wert zurück.
void echoSquare()
{
int nValue;
cout << »Wert:«;
cin >> nValue;
cout << »\nQuadrat: » << nValue * nValue << »\n«;
return;
}
Die Kontrolle beginnt bei der öffnenden Klammer und wird fortgesetzt bis zur return-Anwei-
sung. Die return-Anweisung in einer void-Funktion darf keinen Rückgabewert enthalten.
=
= =
=
Die return-Anweisung in einer void-Funktion ist optional. Wenn sie nicht da
ist, wird die Kontrolle an die aufrufende Funktion zurückgegeben, wenn die
Hinweis schließende Klammer angetroffen wird.
86 Samstagmorgen
#include <stdio.h>
#include <iostream.h>
// square – gibt das Quadrat ihres Argumentes
// dVar zurück.
double square(double dVar)
{
return dVar * dVar;
}
int sumSequence(void)
{
// Endlosschleife
int nAccumulator = 0;
for(;;)
{
// hole weitere Zahl
doube dValue = 0;
cout << »Nächste Zahl: »;
cin >> dValue;
Lektion 9 – Funktionen 87
Teil 2 – Samstagmorgen
<< »\n«;
Lektion 9
cout << »Programm wird beendet.\n«;
return 0;
} // Ende Main
Das ist das gleiche Programm wie FunctionDemo, außer dass SquareDemo die Quadrate der ein-
gegebenen Werte summiert.
Der Wert von dValue wird an die Funktion square( ) übergeben in der Zeile
int nValue = (int)square(dValue);
innerhalb der Funktion sumSequence( ). Die Funktion square( ) multipliziert den ihr in Zeile 12
übergebenen Wert mit sich selber und gibt das Ergebnis zurück. Das Ergebnis wird in der Variable
dSquare gespeichert, die in Zeile 33 zu der Summe addiert wird.
Wertkonvertierung (Casten)
Zeile 32/33 des Programms SquareDemo enthält einen Operator, den wir vorher noch nicht gese-
hen haben.
nAccumulator = nAccumulator + (int)dValue;
Das (int) vor dValue zeigt an, dass der Programmierer möchte, dass der Wert von dValue von
seinem aktuellen Typ, in diesem Fall double, nach int konvertiert wird.
Ein solcher Cast ist eine explizite Konvertierung von einem Typ in einen anderen. Jeder nummeri-
sche Typ kann in jeden anderen nummerischen Typ konvertiert werden. Ohne diesen Cast hätte
C++ diese Typkonvertierung selber vorgenommen, jedoch ohne eine Warnung ausgegeben.
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 88
88 Samstagmorgen
someFunction( ) ist eine Abkürzung für alle vier Funktionen, in gleicher Weise, wie Stephen eine
Kurzform meines Namens ist. Ob ich nun die Kurzform zur Beschreibung der Funktion wähle, so
kennt C++ doch auch die Funktionen someFunction(void), someFunction(int), someFunc-
tion(double) und someFunction(int, int).
=
= =
= void als Argumenttyp ist optional; someFunction(void) und someFunction( )
Hinweis sind die gleiche Funktion.
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 89
Lektion 9 – Funktionen 89
Teil 2 – Samstagmorgen
// Argumenttypen (aufgerufene Funktion im Kommentar)
someFunction(); // someFunction(void)
someFunction(nVariable1);// someFunction(int)
someFunction(dVariable); // someFunction(double)
someFunction(nVariable1, nVariable2);
Lektion 9
// someFunction(int, int)
In jedem der Fälle stimmt der Typ der Argumente mit dem vollen Namen der drei Funktionen
überein.
Der Rückgabetyp ist nicht Teil des Funktionsnamens. D.h., die beiden folgenden
=
= =
= Funktionen haben den gleichen Namen und können daher nicht im gleichen
Hinweis Programm vorkommen:
int someFunction(int n); // vollständiger Name der
double someFunction(int n); // beiden Funktionen
// ist someFunction(int)
9.3 Funktionsprototypen.
In den bisherigen Beispielprogrammen wurden die Funktionen sumSequence( ) und square( )
beide in Codesegmenten definiert, die vor den Aufrufen der Funktionen standen. Das muss nicht
immer so sein: Eine Funktion kann an beliebiger Stelle in einem Modul definiert werden. Ein Modul
ist ein anderer Name für eine C++-Quelldatei.
Trotzdem muss jemand main( ) den vollständigen Namen der Funktion mitteilen, bevor sie auf-
gerufen werden kann. Betrachten Sie den folgenden Codeschnipsel:
int main(int argc, char* pArgs[])
{
someFunc(1, 2);
}
int someFunc(double dArg1, int nArg2)
{
// ... tue etwas
}
Der Aufruf von someFunc( ) von main( ) aus kennt nicht den vollen Namen der Funktion. Von
den Argumenten her könnte man vermuten, dass der Name someFunc(int, int) und der Rückga-
betyp void ist. Wie sie sehen können, ist das falsch.
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 90
90 Samstagmorgen
Was benötigt wird, ist ein Weg, um main( ) den vollständigen Namen der Funktion someFunc( )
mitzuteilen, bevor er benutzt wird. Was gebraucht wird, ist eine Funktionsdeklaration. Eine Funk-
tionsdeklaration, oder ein Prototyp, sieht so aus wie die Funktion, nur ohne Body. In Gebrauch sieht
ein Prototyp wie folgt aus:
int someFunc(double, int); // Prototyp
int main(int argc, char* pArgs[])
{
someFunc(1, 2);
}
int someFunc(double dArg1, int nArg2)
{
// ... tue etwas
}
Der Aufruf in main( ) weiß nun, dass die 1 erst nach double gecastet werden muss, bevor die
Funktion aufgerufen wird. Weiterhin weiß main( ), dass die Funktion someFunc( ) ein int zu-
rückgibt; dieser Rückgabewert wird hier jedoch ignoriert.
!
Tipp
C++ erlaubt es dem Programmierer, Rückgabewerte zu ignorieren.
Die Variable nLocal existiert nicht, bis die Funktion fn( ) aufgerufen wird.
Außerdem hat nur die Funktion fn( ) Zugriff auf nLocal – andere Funktionen kön-
nen nicht in die Funktion »eindringen« und auf die Variable zugreifen.
Im Vergleich dazu existiert die Variable nGlobal so lange, wie das Programm
läuft. Alle Funktionen haben jederzeit Zugriff auf nGlobal.
0 Min.
Die statische Variable nStatic ist eine Mischung aus einer lokalen und einer glo-
balen Variable. Die Variable nStatic wird erzeugt, wenn die Deklaration erstmalig bei der Ausfüh-
rung angetroffen wird (ungefähr dann, wenn die Funktion fn( ) aufgerufen wird). Zusätzlich ist
nStatic nur innerhalb von fn( ) zugreifbar. Anders als nLocal existiert nStatic auch dann noch,
wenn das Programm fn( ) bereits verlassen hat. Wenn fn( ) der Variablen nStatic einen Wert
zuweist, bleibt dieser Wert erhalten, d.h. er steht auch im nächsten Aufruf der Funktion wieder zur
Verfügung.
C++ Lektion 09 31.01.2001 12:18 Uhr Seite 91
Lektion 9 – Funktionen 91
=
= =
= Es gibt einen vierten Variablentyp, auto, der aber heute die gleiche Bedeutung
Hinweis
hat wie local.
Teil 2 – Samstagmorgen
Zusammenfassung.
Sie sollten jetzt eine Vorstellung davon haben, wie komplex Programme werden können, und wie
kleine Funktionen die Programmlogik vereinfachen können. Wohlgeformte Funktionen sind klein,
Lektion 9
haben im Idealfall weniger als 50 Zeilen, und haben weniger als 7 if- oder Schleifen-Kommandos.
Solche Funktionen sind besser zu verstehen, und damit einfacher zu schreiben und zu debuggen.
Und ist das nicht das Ziel?
• C++-Funktionen sind das Mittel, um den Code in handliche Teile zu zerlegen.
• Funktionen können beliebig viele Argumente haben, über die Werte beim Aufruf der Funktion
übergeben werden.
• Funktionen können einen einzelnen Wert an den Aufrufenden zurückgeben.
• Funktionsnamen können überladen werden, wenn sie durch die Anzahl und die Typen der Argu-
mente unterscheidbar bleiben.
Es ist sehr schön, Ihr Programm mit mehreren Ausdrücken in unterschiedlichen Variablen und auf
mehrere Funktionen verteilt auszudrücken, aber das bringt alles nichts, wenn Sie das Programm
nicht zum Laufen bekommen. In Sitzung 10 werden Sie die elementaren Techniken kennenlernen,
um Fehler in Ihren Programmen zu finden.
Selbsttest.
1. Wie rufen Sie eine Funktion auf? (Siehe »Aufrufen der Funktion sumSequence( )«)
2. Was bedeutet der Rückgabewert void? (Siehe »Funktion«)
3. Warum sollte man Funktionen schreiben? (Siehe »Warum Funktionen«)
4. Was ist der Unterschied zwischen einer lokalen und einer globalen Variable? (Siehe »Verschiedene
Speichertypen«)
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 92
10 Lektion Debuggen
Checkliste.
✔ Fehlertypen unterscheiden
✔ Die »Crash-Meldungen« der C++-Umgebung verstehen
✔ Die Ausgabetechnik des Debuggens beherrschen
✔ Debuggen mit Visual C++ und GNU C++
S
ie werden sicher festgestellt haben, dass die Programme, die Sie als Teil der
Übungen in früheren Kapiteln geschrieben haben, nicht beim ersten Mal funk-
tioniert haben. In der Tat habe ich selten, wenn überhaupt schon einmal, ein
nicht-triviales C++-Programm geschrieben, das nicht irgendeinen Fehler enthielt,
als ich es auszuführen versuchte.
30 Min.
Ein Programm, das auf Anhieb funktioniert, wenn Sie es ausprobieren, wird auch
Goldstern-Programm (gold-star program) genannt.
10.1 Fehlertypen.
Es gibt zwei Typen von Fehlern. Die Fehler, die der Compiler abfangen kann, sind sehr einfach zu
beheben. Der Compiler wird Sie im Allgemeinen zu dem Fehler hinführen. Manchmal ist vielleicht
die Beschreibung des Fehlers nicht korrekt – es ist leicht, einen Compiler zu verwirren – aber wenn
Sie Ihr C++-Paket richtig kennen, ist es nicht schwierig, die Fehler zu beheben.
Eine zweite Sorte von Fehlern umfasst die Fehler, die der Compiler nicht finden kann. Diese Feh-
ler werden erst dann sichtbar, wenn Sie das Programm ausführen. Fehler, die erst beim Ausführen
des Programms sichtbar werden, werden als Laufzeitfehler bezeichnet. Fehler, die der Compiler fin-
den kann, werden als Compilezeitfehler bezeichnet.
Laufzeitfehler sind viel schwieriger zu finden, weil Sie keinen Anhaltspunkt dafür haben, was
falsch gelaufen ist, außer Fehlerausgaben, die Ihr Programm vielleicht generiert hat.
Es gibt zwei verschiedene Techniken, um Bugs zu finden. Sie können Ausgabeanweisungen an
wichtigen Stellen im Programm einbauen und das Programm neu generieren. Sie können eine Vor-
stellung davon bekommen, was falsch gelaufen ist, wenn diese Ausgabeanweisungen ausgeführt
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 93
Lektion 10 – Debuggen 93
werden. Ein mächtigerer Zugang ist die Verwendung eines separaten Programms, das als Debugger
bezeichnet wird. Ein Debugger gibt Ihnen die Möglichkeit, Ihr Programm bei der Ausführung zu
kontrollieren. In dieser Sitzung behandeln wir den Zugang über Ausgabeanweisungen. In Sitzung
16 lernen Sie den Umgang mit den Debuggern von Visual C++ und GNU C++.
Teil 2 – Samstagmorgen
Der Ansatz, zum Debuggen Ausgabeanweisungen in den Code einzufügen, wird als Technik der
Ausgabeanweisungen bezeichnet.
Lektion 10
Diese Technik wird oft als Schreibanweisungsansatz bezeichnet. Diese Bezeich-
=
= =
= nung geht auf die frühen Tage der Programme zurück, die in FORTRAN
Hinweis geschrieben wurden. Ausgaben werden in FORTRAN durch WRITE-Anweisungen
ausgeführt.
Um zu sehen, wie das funktionieren könnte, lassen Sie uns den Fehler in folgendem fehlerhaften
Programm beheben:
94 Samstagmorgen
Nachdem ich das Programm eingegeben habe, erzeuge ich das Programm, um die ausführbare
Datei ErrorProgram.exe zu generieren. Ungeduldig lenke ich den Windows-Explorer auf den Ordner,
der das Programm enthält, und führe voll Vertrauen einen Doppelklick auf ErrorProgram aus, um das
Programm laufen zu lassen. Ich geben die Werte 1, 2 und 3 ein, gefolgt von -1 um die Eingabe abzu-
schließen. Aber anstatt den erwarteten Wert 2 auszugeben, beendet sich das Programm mit der
nicht sehr freundlichen Fehlermeldung in Abbildung 10.1 und ohne jegliche Ausgabe.
Abbildung 10.1: Die erste Version von ErrorProgram bricht plötzlich ab, ohne eine Ausgabe zu erzeugen.
=
= =
= Führen Sie ErrorProgramm noch nicht in der C++-Umgebung aus.
Hinweis
Tatsächlich ist doch ein klein wenig Information enthalten: Oben in der Fehler-
!
Tipp
meldung steht »ERRORPROGRAM verursachte einen Teilungsfehler«. Das würde
normalerweise nicht viel helfen, hier jedoch schon, weil nur eine einzige Divi-
sion im Programm vorkommt. Aber lassen Sie uns diese Meldung aus diesem
Grund ignorieren.
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 95
Lektion 10 – Debuggen 95
Teil 2 – Samstagmorgen
Eines der ersten Dinge, die Sie tun sollten, wenn Sie ein Problem finden wollen
! ist, eine Menge von Operationen zu finden, die Ihr Programm fehlerhaft verlau-
Lektion 10
fen lassen. Indem Sie das Problem reproduzierbar machen, können Sie den Feh-
Tipp ler nicht nur fürs Debuggen immer wieder neu erzeugen, sondern Sie wissen
auch, wann das Problem nicht mehr auftritt.
Visual C++ erzeugt eine Ausgabe wie in Abbildung 10.2 zu sehen ist. Das Fenster zeigt an, dass
das Programm fehlerhaft beendet wurde, weil durch null geteilt wurde.
Abbildung 10.2: Die Fehlermeldung von Visual C++ ist nur wenig besser als die Windowsfehlermeldungen.
Diese Fehlermeldung ist nur ein bisschen besser als die Fehlermeldung in Abbildung 10.1. Wenn
ich jedoch OK klicke, zeigt Visual C++ das Programm ErrorProgram mit einem gelben Pfeil, der auf
die Division zeigt, wie in Abbildung 10.3 zu sehen ist. Das ist die C++-Zeile, in der der Fehler aufge-
treten ist.
Abbildung 10.3: Visual C++ zeigt klar auf die Division durch nNums hin.
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 96
96 Samstagmorgen
Jetzt weiß ich, dass, als die Division durchgeführt wurde, nNums gleich 0 gewesen sein muss.
(Ansonsten kann es dort keinen Fehler durch Teilen durch null geben.) Ich kann sehen, wo nNums mit
0 initialisiert wird, aber wo wird es inkrementiert? Es wird nicht inkrementiert, und das ist der Fehler.
Natürlich soll nNums in der Inkrement-Klausel der for-Schleife inkrementiert werden.
Um den Fehler zu beheben, ersetze ich die for-Schleife wie folgt:
for(int nNums = 0; ; nNums++);
Nach dem Klicken auf OK öffnet rhide ein kleines Fenster am unteren Rand des Displays. Dieses
Fenster, das in Abbildung 10.5 zu sehen ist, teilt mir mit, dass der Fehler im Programm ErrorPro-
gram.cpp(28) in der Funktion main( ) aufgetreten ist. Diese etwas kryptische Fehlermeldung zeigt
an, dass der Fehler in Zeile 28 von ErrorProgram.cpp aufgetreten ist und dass sich diese Zeile inner-
halb der Funktion main( ) befindet (Letzteres hätte ich auch durch einen Blick auf das Listing her-
ausfinden können, aber es ist trotzdem eine schöne Information).
Von hier aus kann ich das Problem auf die gleiche Art und Weise lösen, wie im Fall von Visual C++.
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 97
Lektion 10 – Debuggen 97
Teil 2 – Samstagmorgen
Lektion 10
Abbildung 10.5: Die Fehlermeldung von rhide ist genauso informativ wie die von Visual C++, nur noch
kryptischer.
98 Samstagmorgen
Offensichtlich wurde entweder nSum oder nNums (oder beide) nicht richtig deklariert. Um fortzu-
fahren, benötige ich den Inhalt dieser beiden Variablen. In der Tat würde es helfen, wenn ich auch
den Inhalt von nValue kennen würde, weil nValue verwendet wird, um die Summe nSum zu berech-
nen.
Um die Werte von nSum, nNums und nValue zu erfahren, modifiziere ich die for-Schleife wie
folgt:
nSum += nValue;
}
Beachten Sie die hinzugefügten Ausgabeanweisungen. Diese drei Zeilen geben die Werte von
nSum, nNums und nValue in jeder Iteration der Schleife aus.
Die Ergebnisse der Programmausführung mit der mittlerweile Standard gewordenen Eingabe 1,
2, 3 und -1 sind unten dargestellt. Schon im ersten Schleifendurchlauf scheint der Wert von nSum
nicht korrekt zu sein. In der Tat addiert das Programm bereits beim ersten Schleifendurchlauf einen
Wert zu nSum. An diesem Punkt würde ich erwarten, dass der Wert von nSum gleich 0 ist. Das scheint
das Problem zu sein.
Nächste Zahl: 2
nSum = -858993459
nNums = 1
nValue = 2
Nächste Zahl: 3
nSum = -858993457
nNums = 2
nValue = 3
Nächste Zahl:
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 99
Lektion 10 – Debuggen 99
Eine genaue Untersuchung des Programms zeigt, dass nSum deklariert, aber nicht initialisiert wur-
de. Die Lösung ist, die Deklaration von nSum wie folgt zu verändern:
int nSum = 0;
=
= =
= So lange, bis eine Variable initialisiert wurde, ist der Wert der Variablen unbe-
Teil 2 – Samstagmorgen
Hinweis stimmt.
Lektion 10
Sobald ich selbst davon überzeugt bin, dass das Ergebnis korrekt ist, räume ich das Programm
wie folgt auf:
100 Samstagmorgen
Ich erzeuge das Programm neu, und teste die Folge 1, 2, 3 und -1 erneut. Diesmal sehe ich den
erwarteten Mittelwert von 2:
Dieses Programm funktioniert!
Nächste Zahl: 1
Nächste Zahl: 2
Nächste Zahl: 3
Nächste Zahl: -1
Mittelwert ist: 2
Nachdem ich das Programm mit einer Reihe von Eingaben getestet habe, bin ich
davon überzeugt, dass das Programm nun richtig ist. Ich entferne die zusätzlichen
Ausgabeanweisungen und erzeuge das Programm neu, um das Debuggen des Pro-
gramms abzuschließen.
0 Min.
Zusammenfassung.
Es gibt zwei Arten von Fehlern: Compilezeitfehler, die von C++-Compiler erzeugt werden, wenn er
auf eine nicht logische Code-Struktur trifft, und Laufzeitfehler, die erzeugt werden, wenn das Pro-
gramm eine nicht logische Sequenz legaler Instruktionen ausführt.
Compilezeitfehler sind relativ leicht zu beheben, weil die C++-Compiler Sie direkt an die Fehler-
stelle führen können. Die Umgebungen von Visual C++ und GNU C++ versuchen, Sie so gut wie
möglich dabei zu unterstützen. In dem hier vorgestellten Beispielprogramm waren sie sehr erfolg-
reich, genau auf das Problem zu zeigen.
Wenn die Meldungen zu Laufzeitfehlern, die von der C++-Umgebung erzeugt werden, nicht aus-
reichen, bleibt es dem Programmierer überlassen, den Code zu debuggen. In dieser Sitzung haben
wir die so genannte Technik der Ausgabeanweisungen verwendet.
• C++-Compiler sind sehr penibel bei dem, was sie akzeptieren, um Programmierfehler nach Mög-
lichkeit während der Programmerzeugung zu finden, in der Fehler leichter zu beheben sind.
• Das Betriebssystem versucht ebenfalls, Laufzeitfehler abzufangen. Wenn diese Fehler zu einem
Programmabsturz führen, gibt das Betriebssystem Fehlerinformationen zurück, die Visual C++
und GNU C++ zu interpretieren versuchen.
• Ausgabeanweisungen, die an kritischen Stellen im Programm eingefügt werden, können den Pro-
grammierer zur Quelle des Laufzeitfehlers führen.
Obwohl sehr einfach, ist die Technik mit Ausgabeanweisungen sehr effektiv bei kleinen Program-
men. Sitzung 16 zeigt Ihnen noch effektivere Debug-Techniken.
Selbsttest.
1. Was sind die beiden grundlegenden Fehlertypen? (Siehe »Fehlertypen«)
2. Wie können Ausgabeanweisungen helfen, Fehler zu finden? (Siehe »Die Technik der Ausgabean-
weisungen«)
3. Was ist der Debug-Modus? (Siehe Kasten »Wie kann C++ eine Fehlermeldung an den Source-
code binden?«)
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 101
Samstagmorgen – Zusammenfassung
2
T EIL
1. Ich führe die folgende Funktion aus, um kleine Tonnen in Kilogramm umzurechnen. Wenn
ich den Wert 2 eingebe, erhalte ich ein falsches Ergebnis:
2. Stellen Sie sicher, dass Sie die Anwort zu 1 kennen (sie können spicken, wenn das nötig ist).
Nun, was kann ich tun, um das Problem in 1 zu beheben?
3. Ich habe den folgenden kleinen Bruder der Funktion ton2kg( ) geschrieben:
Weil ich mit großen Schiffen arbeite, übergebe ich der Funktion einen Wert von
5000 Tonnen. Der Wert, den ich zurückbekomme, ist offensichtlich falsch.
a. n1 < n2
b. (n1 + n4) == 5
6. (Das ist eine harte Nuss) Was ist der endgültige Wert von n1?
int n1 = 10;
if (n1 > 11)
{
if (n1 > 12)
{
n1 = 0;
}
else
{
n1 = 1;
}
}
7. Was ist der Unterschied zwischen while( ) und do...while( ) im folgenden Beispiel-
code:
int n1 = 10;
while(n1 < 5)
{
n1++;
}
do
{
n1++;
} while(n1 < 5);
C++ Lektion 10 31.01.2001 12:19 Uhr Seite 103
8. Schreiben Sie eine Funktion double cube(double d) als Ergänzung der ersten Funktion.
10. Fügen Sie schließlich die Funktion double cube(int n) zu den ersten beiden Funktionen
hinzu. Was passiert?
Hinweis: Schreiben Sie die Prototypdeklarationen der drei Funktionen cube( ) auf.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 104
Samstag-
nachmittag
Teil 3
Lektion 11.
Das Array
Lektion 12.
Einführung in Klassen
Lektion 13.
Einstieg C++-Zeiger
Lektion 14.
Mehr zu Zeigern
Lektion 15.
Zeiger auf Objekte
Lektion 16.
Debuggen II
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 105
Das Array
11 Lektion
Checkliste.
✔ Den Datentyp Array einführen
✔ Arrays verwenden
✔ Arrays initialisieren
✔ Das gebräuchlichste Array verwenden – die Zeichenkette
D
ie Programme, die wir bisher geschrieben haben, waren immer nur mit
einer Zahl beschäftigt. Das Summationsprogramm liest immer eine Zahl
von der Tastatur, addiert sie zur bisherigen Summe, die in einer einzigen
Variablen gespeichert ist, und liest die nächste Zahl. Wenn wir zu unserer ersten
30 Min. Analogie zurückkehren, dem menschlichen Programm, richten sich diese Pro-
gramme auf jeweils nur eine Radmutter. Es gibt aber Fälle, in denen wir alle Rad-
muttern speichern wollen, bevor wir damit anfangen, mit ihnen zu arbeiten.
Diese Sitzung untersucht, wie Werte gespeichert werden können, gerade so, wie ein Mechaniker
mehrere Radmuttern gleichzeitig halten oder aufbewahren kann.
106 Samstagnachmittag
Wie Sie sehen, kann dieser Ansatz nicht mehr als ein paar Zahlen handhaben.
Ein Array löst das Problem viel schöner:
int nV;
int nValues[128];
for (int i = 0; ; i++)
{
cin >> nV;
if (nV < 0)
{
break;
}
nValues[i] = nV;
}
Die zweite Zeile des Schnipsels deklariert ein Array nValues. Deklarationen von Arrays beginnen
mit dem Typ der Array-Elemente, in diesem Fall int, gefolgt von dem Namen des Array. Das letzte
Element einer Array-Deklaration ist eine öffnende und eine schließende Klammer, die die maximale
Anzahl Elemente enthält, die im Array gespeichert werden können. Im Sourcecode-Schnipsel ist
nValues als Folge von 128 Integerzahlen deklariert.
Der Schnipsel liest eine Zahl von der Tastatur und speichert sie in einem Element von nValues.
Auf ein einzelnes Element des Array wird zugegriffen über den Namen des Array, gefolgt von Klam-
mern, die den Index enthalten. Die erste Zahl im Array ist nValues[0], die zweite Zahl ist nVa-
lues[1] usw.
=
= =
=
Im Gebrauch repräsentiert nValues[i] das i-te Element. Die Indexvariable i
muss eine Zählvariable sein; d.h. i muss entweder int oder long sein. Wenn
Hinweis nValues ein Array von int ist, dann ist nValues[i] vom Typ int.
Zu weiter Zugriff
Mathematiker zählen in ihren Array von 1 ab. Das erste Element eines mathematischen Arrays ist
x(1). Die meisten Programmiersprachen beginnen auch bei 1. C++ beginnt das Zählen bei 0. Das
erste Element eines C++-Arrays ist nValues[0].
=
= =
= Es gibt einen guten Grund dafür, dass C++ bei 0 anfängt zu zählen, aber Sie
Hinweis müssen sich bis Sitzung 12 gedulden, in der Sie diesen Grund erfahren werden.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 107
Weil die Indizierung von C++ bei 0 beginnt, ist das letzte Element eines Array mit 128 int-Ele-
menten nArray[127].
Unglücklicherweise führt C++ keinen Test durch, ob ein verwendeter Index im Indexbereich des
Array liegt. C++ wird Ihnen bereitwillig Zugriff auf nArray[200] geben. Sogar nArray[-15] ist in
C++ erlaubt.
Als Illustration stellen Sie sich bitte vor, die Abstände auf den Landstraßen werden mit Hilfe von
äquidistant stehenden Strommasten gemessen. (Im Westen von Texas ist das von der Wirklichkeit
nicht weit entfernt.) Lassen Sie uns diese Einheit Mastenlänge nennen. Die Straße zu mir nach Hau-
se beginnt an der Abzweigung von der Hauptstraße und führt auf geradem Wege zu meinem Haus.
Die Länge dieser Strecke beträgt exakt 9 Mastenlängen.
Wenn wir die Nummerierung bei den Masten an der Landstraße beginnen, dann ist der Mast, der
meinem Haus am nächsten steht, der Mast mit der Nummer 10. Dies sehen Sie in Abbildung 1.1.
Teil 3 – Samstagnachmittag
Ich kann jede Position entlang der Straße ansprechen, indem ich Masten zähle. Wenn wir Abstän-
de auf der Hauptstraße messen, berechnen wir einen Abstand von 0. Der nächste diskrete Punkt ist
eine Mastenlänge entfernt usw., bis wir zu meinem Haus kommen, 9 Mastenlängen entfernt.
Ich kann einen Abstand von 20 Mastenlängen von der Hauptstraße entfernt messen. Natürlich
Lektion 11
liegt dieser Punkt nicht auf der Straße. (Erinnern Sie sich, dass die Straße an meinem Haus endet.)
Tatsächlich weiß ich nicht einmal, was Sie dort vorfinden würden. Sie könnten auf der nächsten
Hauptstraße sein, auf freiem Feld, oder im Wohnzimmer meines Nachbarn. Diesen Ort zu untersu-
chen, ist schlimm genug, aber dort etwas abzulegen, ist noch viel schlimmer. Etwas auf freiem Feld
abzulegen, ist eine Sache, aber ins Wohnzimmer meines Nachbarn einzubrechen, könnte Sie in
Schwierigkeiten bringen.
Abbildung 11.1: Man braucht 10 Masten um eine Länge von 9 Mastenlängen abzustecken.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 108
108 Samstagnachmittag
Analog ergibt das Lesen von array[20] eines Array mit 10 Elementen einen mehr oder weniger
zufälligen Wert. In das Element array[20] zu schreiben, hat ein unvorhersehbares Ergebnis. Es kann
gut gehen, es kann zu einem fehlerhaften Verhalten führen oder sogar zum Absturz des Programms.
=
= =
=
Das Element, auf das in einem Array nArray mit 128 Elementen am häufigsten
illegal zugegriffen wird, ist nArray[128]. Obwohl es nur ein Element außerhalb
Hinweis des Arrays liegt, ist der Zugriff darauf ebenso gefährlich wie das Anfassen jeder
anderen nicht korrekten Adresse.
// Prototypdeklarationen
int sumArray(int nArray[], int nSize);
void displayArray(int nArray[], int nSize);
break;
}
Teil 3 – Samstagnachmittag
// displayArray – zeige die Elemente eines
// Array der Länge nSize
void displayArray(int nArray[], int nSize)
{
Lektion 11
cout << »Der Wert des Array ist:\n«;
for (int i = 0; i < nSize; i++)
{
cout.width(3);
cout << i << »: » << nArray[i] << »\n«;
}
cout << »\n«;
}
Das Programm ArrayDemo beginnt mit der Deklaration eines Prototyps der Funktionen sumArray( ) und
displayArray( ). Der Hauptteil des Programms enthält die typischen Eingabeschleifen. Dieses Mal
jedoch werden die Werte im Array nInputValues gespeichert, wobei die Variable nNumValues die
Anzahl der bereits im Array gespeicherten Werte enthält. Das Programm liest keine weiteren Werte
ein, wenn der Benutzer eine negative Zahl eingibt (Kommentar A), oder wenn die Anzahl der Ele-
mente im Array erschöpft ist (das ist der Test bei Kommentar B).
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 110
110 Samstagnachmittag
Das Array nInputValues ist als 128 Integerzahlen lang deklariert. Sie können
=
= =
=
denken, dass das genug ist für jedermann, aber verlassen Sie sich nicht darauf.
Mehr Werte in ein Array zu schreiben, als es speichern kann, führt zu einem feh-
Hinweis lerhaften Verhalten des Programms und oft zum Programmabsturz. Unabhän-
gig davon, wie groß Sie das Array machen, bauen Sie immer einen Check ein,
der sicherstellt, dass Sie nicht über die Grenzen des Array hinauslaufen.
Die Funktion main( ) endet mit der Ausgabe des Array-Inhaltes und der Summe der eingegebe-
nen Zahlen. Die Funktion displayArray( ) enthält die typische for-Schleife, die zum Traversieren
eines Array verwendet wird. Beachten Sie, dass der Index mit 0 und nicht mit 1 initialisiert wird.
Beachten Sie außerdem, dass die for-Schleife abbricht, bevor i gleich nSize ist.
In gleicher Weise iteriert die Funktion sumArray( ) in einer Schleife durch das Array und addiert
alle Werte zu der in nSum enthaltenen Summe. Nur um Nichtprogrammierer nicht weiter raten zu
lassen, der Begriff »iterieren« meint das Traversieren durch eine Menge von Objekten wie das Array.
Wir sagen »die Funktion sumArray( ) iteriert durch das Array.«
=
= =
= Eine nicht initialisierte Variable enthält einen zufälligen Wert.
Hinweis
Dies initialisiert fArray[0] mit 0, fArray[1] mit 1, fArray[2] mit 2, usw. Die Anzahl der Initia-
lisierungskonstanten kann auch gleichzeitig die Größe des Array definieren. Z.B. hätten wir durch
Zählen der Konstanten feststellen können, dass fArray fünf Elemente hat. C++ kann auch zählen.
Die folgende Deklaration ist identisch mit der obigen:
float fArray[] = {0.0, 1.0, 2.0, 3.0, 4.0};
Es ist nicht nötig, den gleichen Wert immer wieder zu wiederholen, um ein großes Array zu initi-
alisieren. Das folgende initialisiert die 25 Einträge in fArray mit 1.0:
float fArray[25] = {1.0};
beiten. Das Hauptprogramm war in der Lage, das Array der Eingabewerte an die Funktion display-
Array( ) zur Darstellung zu übergeben und das gleiche Array an die Funktion sumArray( ) zur
Summenbildung zu übergeben.
Diese Matrix hat zwei Elemente in der einen Dimension und 3 Elemente in der anderen Dimen-
Teil 3 – Samstagnachmittag
sion, also 6 Elemente. Wie Sie erwarten werden, ist eine Ecke der Matrix nMatrix[0][0], während
die andere Ecke nMatrix[1][2] ist.
Lektion 11
=
= =
= Ob Sie nMatrix 10 Elemente lang machen in der einen oder der anderen
Hinweis Dimension, ist eine Frage des Geschmacks.
Eine Matrix kann in der gleichen Weise initialisiert werden, wie ein Array:
int nMatrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
Dies initialisiert das Array nMatrix[0], das drei Elemente besitzt, mit den Werten 1, 2 und 3, und
die drei Elemente des Array nMatrix[1] mit den Werten 4, 5 und 6.
Das folgende kleine Programm gibt meinen Namen auf einem MS-DOS-Fenster aus, der Stan-
dardausgabe.
112 Samstagnachmittag
Das Programm läuft gut, es ist aber unbequem, die Länge eines Array zusammen mit dem Array
selber zu übergeben. Wie haben dieses Problem bei der Eingabe von Integerzahlen dadurch vermie-
den, dass wir die Regel aufgestellt haben, dass eine negative Zahl das Ende der Eingabe bedeuten
soll. Wenn wir hier dasselbe machen könnten, müssten wir nicht die Länge des Array mit übergeben
– wir würden dann wissen, dass das Array zu Ende ist, wenn dieses besondere Zeichen angetroffen
wird.
Lassen Sie uns den Code 0 verwenden, um das Ende eines Zeichenarray zu markieren.
Das Zeichen, dessen Wert 0 ist, ist nicht das gleiche wie 0. Der Wert von 0 ist
=
= =
= 0x30. Das Zeichen, dessen Wert 0 ist, wird oft als \0 geschrieben, um den
Hinweis Unterschied klar zu machen. In gleicher Weise ist \y das Zeichen, das den Wert
y hat. Das Zeichen \0 wird auch Nullzeichen genannt.
// Prototypdeklarationen
void displayString(char sArray[]);
{
for(int i = 0; sArray[i] != 0; i++)
{
cout << sArray[i];
}
}
Die Deklaration von cMyName deklariert das Zeichen-Array mit dem Extrazeichen ‘\0’ am Ende.
Das Programm DisplayString iteriert durch das Zeichen-Array, bis das Nullzeichen angetroffen wird.
Die Funktion displayString( ) ist einfacher zu benutzen als ihr Vorgänger displayCharArray( ).
Es ist nicht mehr nötig, die Länge des Zeichen-Array mit zu übergeben. Weiterhin arbeitet dis-
playString( ) auch dann, wenn die Länge der Zeichenkette zur Compilezeit nicht bekannt ist. Z.B.
wäre das der Fall, wenn der Benutzer eine Zeichenkette über die Tastatur eingeben würde.
Teil 3 – Samstagnachmittag
Ich habe den Begriff Zeichenkette verwendet, als wäre es ein fundamentaler Typ, wie int oder
float. Als ich den Begriff eingeführt habe, habe ich auch erwähnt, dass die Zeichenkette eine Varia-
tion eines bereits existierenden Typs ist. Wie Sie jetzt sehen können, ist eine Zeichenkette ein Null-
terminiertes Zeichenarray.
Lektion 11
C++ unterstützt eine optionale, etwas bequemere Art der Initialisierung von Zeichenarrays durch
eine in Hochkommata eingeschlossene Zeichenkette. Die Zeile
char szMyName[] = »Stephen«;
im vorigen Beispiel.
=
= =
= Die hier verwendete Namenskonvention ist nur eine Konvention, C++ kümmert
Hinweis sich nicht darum. Das Präfix sz steht für eine nullterminierte Zeichenkette.
=
= =
= Die Zeichenkette »Stephen« ist acht Zeichen lang, nicht sieben – das Nullzei-
Hinweis chen nach dem n wird mitgezählt.
114 Samstagnachmittag
// Prototypdeklarationen
void concatString(char szTarget[], char szSource[]);
return 0;
}
while(szSource[nSourceIndex])
{
szTarget[nTargetIndex] =
szSource[nSourceIndex];
nTargetIndex++;
nSourceIndex++;
}
Die Funktion main( ) liest zwei Zeichenketten mittels der Funktion getline( ).
Teil 3 – Samstagnachmittag
=
= =
= Die Alternative cin >> szString liest bis zum ersten Leerraum. Hier wollen wir
Hinweis bis zum Ende der Zeile lesen.
Lektion 11
Die Funktion main( ) verbindet die beiden Zeichenketten mittels der Funktion concatString( )
und gibt dann das Ergebnis aus.
Die Funktion concatString( ) setzt das zweite Argument, szSource, an das Ende des ersten
Argumentes, szTarget.
Die erste Schleife innerhalb von concatString( ) iteriert durch die Zeichenkette szTarget so
lange, bis nTargetIndex auf der Null am Ende der Zeichenkette steht.
!
Tipp
Die Schleife while(value != 0) ist das Gleiche wie while(value), weil value
als falsch interpretiert wird, wenn es gleich 0 ist, und als wahr wenn es
ungleich 0 ist.
Die zweite Schleife interiert durch die Zeichenkette szSource und kopiert die Elemente dieser
Zeichenkette in szTarget, beginnend mit dem ersten Zeichen von szSource und dem Nullzeichen
in szTarget. Die Schleife endet, wenn nSourceIndex auf dem Nullzeichen von szSource steht.
Die Funktion concatString( ) setzt ein abschließendes Nullzeichen ans Ende der Ergebniszei-
chenkette, bevor sie zurückkehrt.
Vergessen Sie nicht, die Zeichenketten, die Sie in Ihren Programmen selber
=
= =
=
erzeugen, durch ein Nullzeichen abzuschließen. In der Regel werden Sie
dadurch feststellen, dass Sie das vergessen haben, dass die Zeichenkette »Müll«
Hinweis
am Ende enthält, oder dadurch, dass das Programm abstürzt, wenn Sie versu-
chen, die Zeichenkette zu manipulieren.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 116
116 Samstagnachmittag
int strlen(string) Gibt die Anzahl der Zeichen einer Zeichenkette zurück
void strcat(target, source) Fügt die source-Zeichenkette ans Ende der target-Zeichen-
kette an
int strstr(source1, source2) Findet das erste Vorkommen von source2 in source1
int stricmp(source1, source2) Vergleicht zwei Zeichenketten, ohne Groß- und Kleinschrei-
bung zu beachten
Im Programm Concatenate hätten wir den Aufruf von concatString( ) durch einen Aufruf von
strcat( ) ersetzen können, was uns ein wenig Aufwand erspart hätte.
strcat(szString, » – »);
=
= =
= Sie müssen die Anweisung #include <string.h> am Anfang Ihres Programms
Hinweis einfügen, das die Funktionen str... verwendet.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 117
Teil 3 – Samstagnachmittag
Funktionen verwenden.
Lektion 11
C++ stellt auch eine Menge von I/O-Funktionen auf einem niedrigen Level bereit. Die nützlichste ist
die Ausgabefunktion printf( ).
=
= =
= Das sind die originalen I/O-Funkionen von C. Streameingabe und -ausgabe
Hinweis kamen erst mit C++.
In seiner einfachsten Form gibt printf( ) eine Zeichenkette auf cout aus.
printf(»Dies ist eine Ausgabe auf cout«);
Die Funktion printf( ) führt Ausgaben unter Verwendung eingebetteter Kommandos zur For-
matkontrolle durch, die jeweils mit einem %-Zeichen beginnen. Z.B. gibt das Folgende den Wert
einer int-Zahl und einer double-Zahl aus:
int nInt = 1;
double dDouble = 3.5;
printf(»Der int-Wert ist %i; der float-Wert ist %f«,
nInt, dDouble);
Der Integerwert wird an der Stelle von %i eingefügt, der double-Wert erscheint an der Stelle von
%f.
Der int-Wert ist 1; der float-Wert ist 3.5
Obwohl die Funktion printf( ) kompliziert in ihrer Benutzung ist, stellt Sie
doch eine Kontrolle über die Ausgabe dar, die man mit Streamfunktionen nur
schwer erreichen kann.
0 Min.
C++ Lektion 11 31.01.2001 12:21 Uhr Seite 118
118 Samstagnachmittag
Zusammenfassung.
Das Array ist nichts anderes als eine Folge von Variablen. Jede dieser Variablen, die alle den gleichen
Typ haben, wird über einen Arrayindex angesprochen – wie z.B. Hausnummern die Häuser in einer
Straße bezeichnen. Die Kombination von Arrays und Schleifen, wie for und while, ermöglichen es
Programmen, eine Menge von Elementen einfach zu bearbeiten. Das bekannteste C++Array ist das
Null-terminierte Zeichenarray, das auch als Zeichenkette bezeichnet wird.
• Arrays ermöglichen es Programmen, schnell und effizient durch eine Anzahl von Elementen
durchzugehen, unter Verwendung eines C++-Schleifenkommandos. Der Inkrementteil einer for-
Schleife z.B. ist dafür gedacht, einen Index heraufzuzählen, während der Bedingungsteil dafür
gedacht ist, auf das Ende des Array zu achten.
• Zugriff auf Elemente außerhalb der Grenzen eines Array ist gleichermaßen häufig anzutreffen wie
gefährlich. Es ist verführerisch, auf das Element 128 eines Array mit 128 Elementen zuzugreifen.
Weil die Zählung jedoch bei 0 startet, hat das letzte Element den Index 127 und nicht 128.
• Das Abschließen eines Zeichenarrays durch ein spezielles Zeichen erlaubt es Funktionen, zu wissen,
wann das Array zu Ende ist, ohne ein spezielles Feld für die Zeichenlänge mitzuführen. Hierfür ver-
wendet C++ das Zeichen ‘\0’, das den Bitwert 0 hat, und kein normales Zeichen ist. Programmie-
rer verwenden den Begriff Zeichenkette oder ASCII-Zeichenkette für eine Null-terminiertes
Zeichenarray.
• Der im Abendland entwickelte 8-Bit-Datentyp char kann die vielen tausend Spezialzeichen, die in
einigen asiatischen Sprachen vorkommen, nicht darstellen. Um diese Zeichen zu verwalten, stellt
C++ den speziellen Datentyp wide character bereit, der als wchar bezeichnet wird. C++ enthält
spezielle Funktionen, die mit wchar arbeiten können; diese sind in der Standardbibliothek von
C++ enthalten.
Selbsttest.
1. Was ist die Definition eines Array? (Siehe »Was ist ein Array«)
2. Was ist der Offset des ersten und es letzten Elementes im Array myArray[128]? (Siehe »Zu weiter
Zugriff«)
3. Was ist eine Zeichenkette? Was ist der Typ einer Zeichenkette? Was beendet eine Zeichenkette?
(Siehe »Arrays von Zeichen«)
C++ Lektion 12 31.01.2001 12:23 Uhr Seite 119
Einführung in Klassen
12 Lektion
Checkliste.
✔ Klassen verwenden, um Variablen verschieden Typs in einem Objekt zu
gruppieren
✔ Programme schreiben, die Klassen verwenden
A
rrays sind großartig, wenn es darum geht, eine Folge von Objekten zu ver-
arbeiten, wie z.B. int-Zahlen oder double-Zahlen. Arrays arbeiten nicht
wirklich gut, wenn Daten verschiedenen Typs gruppiert werden sollen,
wie z.B. die Sozialversicherungsnummer und der Name einer Person. C++ stellt
30 Min. hierfür eine Struktur bereit, die als Klasse bezeichnet wird.
120 Samstagnachmittag
return 1;
}
<< »\n«;
}
Teil 3 – Samstagnachmittag
}
Lektion 12
{
displayData(i);
}
return 0;
}
Die drei Arrays bieten Platz für jeweils 25 Einträge. Vorname und Nachname sind auf 128 Zeichen
begrenzt.
Die Funktion main( ) liest erst die Objekte in der Schleife ein, die mit while(getData(index))
in der Funktion main( ) beginnt. Der Aufruf von getData( ) liest den nächsten Eintrag. Die Schlei-
fe wird verlassen, wenn getData( ) eine 0 zurückgibt, die anzeigt, dass der Eintrag vollständig ist.
Das Programm ruft dann displayData( ) auf, um die eingegebenen Objekte auszugeben.
Die Funktion getData( ) liest Daten von cin in die drei Arrays. Die Funktion gibt 0 zurück, wenn
der Benutzer einen Vornamen ende oder ENDE eingibt. Wenn der Vorname davon verschieden ist,
werden die verbleibenden Daten gelesen und es wird eine 1 zurückgegeben, um anzuzeigen, dass
noch weitere Objekte gelesen werden sollen.
C++ Lektion 12 31.01.2001 12:23 Uhr Seite 122
122 Samstagnachmittag
Vorname:Stephen
Nachname:Davis
Sozialversicherungsnummer:1234
Vorname:Scooter
Nachname:Dog
Sozialversicherungsnummer:3456
Vorname:Valentine
Nachname:Puppy
Sozialversicherungsnummer:5678
Vorname:ende
Einträge:
Stephen Davis/1234
Scooter Dog/3456
Valentine Puppy/5678
Teil 3 – Samstagnachmittag
Eine Klassendefinition beginnt mit dem Schlüsselwort class, gefolgt von dem Namen der Klasse
und einem öffnenden/schließenden Klammernpaar.
Lektion 12
=
= =
=
Das alternative Schlüsselwort struct kann auch verwendet werden. Die beiden
Schlüsselworte class und struct sind identisch, mit der Ausnahme, dass bei
Hinweis struct eine public-Deklaration angenommen wird.
Die erste Zeile innerhalb der Klammern ist das Schlüsselwort public.
=
= =
= Spätere Sitzungen werden die anderen Schlüsselworte von C++ außer public
Hinweis vorstellen.
Nach dem Schlüsselwort public kommen die Einträge, die für die Beschreibung eines Objektes
benötigt werden. Die Klasse NameDataSet enthält den Vor- und Nachnamen zusammen mit der Sozi-
alversicherungsnummer. Erinnern Sie sich daran, dass eine Klassendeklaration alle Daten umfasst, die
ein Objekt beschreiben.
Die letzte Zeile deklariert die Variable nds als einen einzelnen Eintrag der Klasse NameDataSet.
Programmierer sagen, dass nds eine Instanz der Klasse NameDataSet ist. Sie instanziieren die Klasse
NameDataSet, um das Objekt nds zu erzeugen. Schließlich sagen die Programmierer, dass szFirst-
Name und die anderen Elemente oder Eigenschaften der Klasse sind.
Die folgende Syntax wird für den Zugriff auf die Elemente eines Objektes verwendet:
NameDataSet nds;
nds.nSocialSecurity = 10;
cin >> nds.szFirstName;
Hierbei ist nds eine Instanz der Klasse NameDataSet (d.h. ein bestimmtes NameDataSet-Objekt).
Die Integerzahl nds.nSocialSecurity ist eine Eigenschaft des Objektes nds. Der Typ von
nds.nSocialSecurity ist int, während der Typ von nds.szFirstName gleich char[] ist.
C++ Lektion 12 31.01.2001 12:23 Uhr Seite 124
124 Samstagnachmittag
Ein Klassenobjekt kann bei seiner Erzeugung wie folgt initialisiert werden:
NameDataSet nds = {»Vorname«, »Nachname«, 1234};
Der Programmierer kann auch Arrays von Objekten deklarieren und initialisieren:
NameDataSet ndsArray[2] = {{»VN1«, »NN1«, 1234}
{»VN2«, »NN2«, 5678}};
Nur der Zuweisungsoperator ist für Klassenobjekte defaultmäßig definiert. Der Zuweisungsope-
rator weist dem Zielobjekt eine binäre Kopie des Quellobjektes zu. Beide Objekte müssen denselben
Typ besitzen.
12.2.2 Beispielprogramm
Die Klassen-basierte Version des Programms ParallelData sieht wie folgt aus:
return 1;
}
Teil 3 – Samstagnachmittag
{
// alloziere 25 Datensätze
NameDataSet nds[25];
Lektion 12
// lade Vornamen, Nachnamen und
// Sozialversicherungsnummer
cout << »Lies Vornamen, Nachnamen und \n«
<< »Sozialversicherungsnummer\n«;
<< »Geben Sie ‘ende’ als Vorname ein, \n«;
<< »um das Programm zu beenden\n«;
int index = 0;
while (getData(nds[index]))
{
index++;
}
In diesem Fall alloziert die Funktion main( ) 25 Objekte aus der Klasse NameDataSet. Wie zuvor
betritt main( ) eine Schleife, in der Einträge von der Tastatur gelesen werden unter der Verwendung
der Methode getData( ). Statt einen einfachen Index zu übergeben (oder einen Index und ein
Array), übergibt main( ) das Objekt, das getData(NameDataSet) mit Werten füllen soll.
In gleicher Weise verwendet main( ) die Funktion displayData(NameDataSet), um alle Name-
DataSet-Objekte anzuzeigen.
Die Funktion getData( ) liest die Objektinformation in das übergebene Objekt vom Typ Name-
DataSet, das nds heißt.
Die Bedeutung des Und-Zeichens (&), das dem Argument der Funktion getData( )
=
= =
= hinzugefügt wurde, wird in Sitzung 13 vollständig erklärt. An dieser Stelle
Hinweis reicht es aus zu erwähnen, dass dieses Zeichen dafür sorgt, dass Änderungen,
die in getData( ) ausgeführt werden, auch in main( ) sichtbar sind.
C++ Lektion 12 31.01.2001 12:23 Uhr Seite 126
126 Samstagnachmittag
12.2.3 Vorteile
Die grundlegende Struktur des Programms ClassData ist die gleiche wie die von
ParallelData. ClassData muss jedoch nicht mehrere Arrays verwalten. Bei einem
Objekt, das so einfach ist wie NameDataSet, ist es nicht offensichtlich, dass dies ein
0 Min. entscheidender Vorteil ist. Überlegen Sie sich einmal, wie beide Programme aussehen
würden, wenn NameDataSet alle Einträge enthalten würde, die für eine Kreditkarte
benötigt würden. Je größer die Objekte, desto größer der Vorteil.
=
= =
= Je weiter wir die Klasse NameDataSet entwickeln, desto größer wird der Vorteil.
Hinweis
Zusammenfassung.
Arrays können nur Folgen von Objekten gleichen Typs speichern; z.B. ein Array für int oder double.
Die Klasse ermöglicht es dem Programmierer, Daten mit verschiedenen Typen in einem Objekt zu
gruppieren. Z.B. könnte eine Klasse Student eine Zeichenkette für den Namen des Studenten, eine
Integervariable für seine Immatrikulationsnummer und eine Gleitkommazahl für seinen Noten-
durchschnitt enthalten. Die Kombination von Array und Klassenobjekten kombiniert die Vorteile
eines jeden in einer einzelnen Datenstruktur.
• Die Elemente eines Klassenobjektes müssen nicht vom selben Typ sein; wenn Sie verschieden sind,
müssen sie über ihren Namen und nicht über einen Index angesprochen werden.
• Das Schlüsselwort struct kann an Stelle des Schlüsselwortes class verwendet werden; struct ist
ein Überbleibsel aus den Tagen von C.
Selbsttest.
1. Was ist ein Objekt? (Siehe »Gruppieren von Daten«)
2. Was ist der ältere Begriff für das C++-Schlüsselwort class? (Siehe »Das Format einer Klasse«)
3. Was bedeuten die kursiv geschriebenen Worte? (Siehe »Das Format einer Klasse«)
a. Instanz einer Klasse
b. Instanziieren einer Klasse
c. Element einer Klasse
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 127
Einstieg C++-Zeiger
13 Lektion
Checkliste.
✔ Variablen im Speicher adressieren
✔ Den Datentyp Zeiger einführen
✔ Die inhärente Gefahr von Zeigern erkennen
✔ Zeiger an Funktionen übergeben
✔ Objekte frei allozieren, bekannt als Heap
T
eil II führte die C++-Operatoren ein. Operationen wie Addition, Multiplika-
tion, bitweises AND und logisches OR wurden auf elementaren Datentypen
wie int und float ausgeführt. Es gibt einen anderen Variablentyp, den wir
noch betrachten müssen – Zeiger.
Für jemanden, der mit anderen Programmiersprachen vertraut ist, sieht C++
30 Min.
bis jetzt aus wie jede andere Programmiersprache. Viele Programmiersprachen
enthalten nicht die dargestellten logischen Operatoren und C++ bringt seine eigene Semantik mit,
aber es gibt keine Konzepte in C++, die nicht in anderen Sprachen auch vorhanden wären. Bei der
Einführung von Zeigern in die Sprache verabschiedet sich C++ von anderen, konventionelleren
Sprachen.
=
= =
= Zeiger wurden tatsächlich im Vorgänger von C++, der Sprache C eingeführt.
Hinweis Alles in diesem Kapitel geht in C genauso.
Zeiger sind nicht nur ein Segen. Während Zeiger C++ einmalige Fähigkeiten verleihen, können
sie syntaktisch kompliziert sein und sind eine häufige Fehlerquelle. Dieses Kapitel führt den Varia-
blentyp Zeiger ein. Es beginnt mit einigen konzeptionellen Definitionen, geht durch die Syntax von
Zeigern, und stellt dann einige Probleme dar, die Programmierer mit Zeigern haben können.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 128
128 Samstagnachmittag
=
= =
= Es gilt die Konvention, Speicheradressen hexadezimal zu schreiben.
Hinweis
int 4
long 4
float 4
double 8
Betrachten Sie das folgende Testprogramm Layout, das die Anordnung der Variablen im Speicher
illustriert. (Ignorieren Sie den Operator & – es ist ausreichend zu sagen, dass &n die Adresse der Vari-
ablen n zurückgibt.)
return 0;
}
Die Ausgabe dieses Programms finden Sie in Listing 13-1. Sie können sehen, dass die Variable n
Teil 3 – Samstagnachmittag
an der Adresse 0x65fdf0 gespeichert wurde.
=
= =
=
Machen Sie sich keine Sorgen, wenn die Werte, die von Ihrem Programm ausge-
geben werden, davon verschieden sind. Nur der Abstand zwischen den Adres-
Lektion 13
Hinweis sen ist entscheidend.
Aus dem Vergleich der Adressen können wir ableiten, dass die Größe von n gleich 4 Bytes ist
(0x65fdf4 – 0x65fdf0), die Größe der long-Variablen l ebenfalls gleich 4 ist (0x65fdf0 – 0x65fdec)
usw.
Das ist aber nur dann so richtig, wenn wir annehmen können, dass die Variablen unmittelbar
hintereinander im Speicher angeordnet werden, was bei GNU C++ der Fall ist. Für Visual C++ ist
dafür eine besondere Projekteinstellung nötig.
Es gibt nichts in der Definition von C++, das vorschreiben würde, dass die Variablen wie in Listing
13-1 angeordnet sein müssen. Was uns betrifft ist es ein großer Zufall, dass GNU C++ und Visual C++
die gleiche Anordnung der Variablen im Speicher gewählt haben.
130 Samstagnachmittag
Operator Bedeutung
Die Funktion fn( ) beginnt mit der Deklaration von nInt. Die nächste Anweisung deklariert eine
Variable pnInt, die vom Typ »Zeiger auf int« ist.
=
= =
=
Wie die Namen von int-Variablen mit n beginnen, ist der erste Buchstabe von
Zeigervariablen p. Somit ist pnX ein Zeiger auf eine int-Variable X. Auch das ist
Hinweis nur eine Konvention, um den Überblick zu behalten – C++ kümmert sich nicht
um die Namen, die Sie verwenden.
In einem Ausdruck bedeutet der unäre Operator & »Adresse von«. Somit würden wir die erste
Zuweisung lesen als »speichere die Adresse von nInt in pnInt«.
Um es noch konkreter zu machen, nehmen wir an, dass der Speicherbereich von fn( ) bei Adres-
se 0x100 beginnt. Lassen Sie uns weiterhin annehmen, dass nInt an Adresse 0x102 und pnInt an
Adresse 0x106 steht. Die Anordnung hier ist einfacher als die Ausgabe des Programms Layout, aber
die Konzepte sind identisch.
Die erste Zuweisung wird in Abbildung 13.1 dargestellt. Hier können Sie sehen, dass der Wert
von &nInt (0x102) in pnInt gespeichert wird.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 131
Teil 3 – Samstagnachmittag
Lektion 13
Abbildung 13.1: Speichern der Adresse von nInt in pnInt.
Die zweite Zuweisung in dem kleinen Programmschnipsel besagt, »speichere 10 in der Adresse,
auf die pnInt zeigt«. Abbildung 13.2 demonstriert dies. Der Wert 10 wird an der Adresse gespei-
chert, die pnInt enthält; diese ist 0x102 (die Adresse von nInt).
Abbildung 13.2: Speichern von 10 in der Adresse, auf die pnInt zeigt.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 132
132 Samstagnachmittag
In gleicher Weise ist der Typ von *pnInt gleich int, weil pnInt von Typ int* ist.
*pnInt = 10; // beide Seiten sind vom Typ int
Sprachlich ausgedrückt ist der Typ von etwas, auf das pnInt verweist, gleich int.
=
= =
=
Abgesehen davon, dass eine Zeigervariable einen unterschiedlichen Typ hat,
wie int* und double*, ist der Zeiger an sich ein elementarer Typ. Unabhängig,
Hinweis auf was er zeigt, benötigt ein Zeiger auf einem Rechner mit Pentiumprozessor 4
Bytes.
Es ist extrem wichtig, dass Typen zusammenpassen. Bedenken Sie, was passieren könnte, wenn
das Folgende erlaubt wäre:
int n1;
int* pnInt;
pnInt = &n1;
*pnInt = 100.0;
Die zweite Zuweisung versucht, einen 8-Bit-Wert 100.0 von Typ double in den 4 Bytes zu spei-
chern, die von der Variable n1 belegt werden. Das Ergebnis ist, dass Variablen in der Nähe ausge-
löscht werden. Das wird grafisch im folgenden Programm LayoutError dargestellt, das Sie in Listing
13-2 finden. Die Ausgabe dieses Programms finden Sie am Ende des Listings.
*pD = 13.0;
return 0;
}
Ausgabe:
darüber = 0
n = 0
darunter = 0
Teil 3 – Samstagnachmittag
double wird zugewiesen
darüber = 1076494336
n = 0
darunter = 0
Lektion 13
Press any key to continue
Die ersten drei Zeilen in main( ) deklarieren drei int-Variablen auf die bekannte Weise. Wir neh-
men hier an, dass diese drei Variablen hintereinander im Speicher angeordnet werden.
Die drei folgenden Zeilen geben die Werte der drei Variablen aus. Es ist nicht überraschend, dass
alle Variablen Null sind. Die Zuweisung *pD = 13.0; speichert den double-Wert 13.0 in der int-Vari-
ablen n. Die drei Ausgabezeilen zeigen die Werte der Variablen nach dieser Zuweisung.
Nachdem der double-Wert 13.0 der int-Variablen n zugewiesen wurde, ist n unverändert, aber
die benachbarte Variable upper ist mit »Müll« gefüllt.
Das Folgende castet einen Zeiger von einem Typ in einen anderen:
double* pD = (double*)&n;
=
= =
= Hierbei ist &n vom Typ int*, wohingegen die Variable pD vom Typ double* ist.
Hinweis Der Cast (double*) ändert den Typ des Werts &n in den Wert von pD, in gleicher
Weise castet
double d = (double)n;
den int-Wert in n in einen double-Wert.
13.4.1 Wertübergabe
Sie werden bemerkt haben, dass es normalerweise unmöglich ist, den Wert einer Variablen innerhalb
der Funktion zu ändern, an die die Variable übergeben wurde. Betrachten Sie das folgende Code-
segment:
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 134
134 Samstagnachmittag
void parent(void)
{
int n1 = 0;
fn(n1);
// der Wert von n1 ist hier 0
}
Die Funktion parent( ) initialisiert die int-Variable n1 mit 0. Der Wert von n1 wird an fn( )
übergeben. Bei Eintritt in die Funktion ist nArg gleich 10. fn( ) ändert den Wert von nArg, bevor sie
zu parent( ) zurückkehrt. Vielleicht überrascht es Sie, dass der Wert von n1 bei Rückkehr zu parent( )
immer noch 0 ist.
Der Grund dafür ist, dass C++ nicht die Variable selbst an die Funktion übergibt. Stattdessen
übergibt C++ den Wert, den die Variable zum Zeitpunkt des Aufrufes hat. D.h. der Ausdruck wird
ausgewertet, selbst wenn es nur ein Variablenname ist, und das Ergebnis wird übergeben. Den Wert
einer Variable an eine Funktion zu übergeben, wird Wertübergabe genannt.
!
Tipp
Wenn man locker sagt »übergib die Variable x an die Funktion fn( )«, meint
man damit eigentlich »übergib den Wert des Ausdrucks x«.
void parent(void)
{
int n = 0;
fn(&n); // das übergibt die Adresse von n
// der Wert von n ist jetzt 10
}
In diesem Fall wird die Adresse von n an die Funktion fn( ) übergeben und nicht der Wert von n.
Der entscheidende Unterschied ist offensichtlich, wenn Sie die Zuweisung in fn( ) betrachten.
Lassen Sie uns zu unserem früheren Beispiel zurückkehren. Nehmen Sie an, dass n an Adresse
0x102 gespeichert ist. Nicht der Wert 10, sondern der »Wert« 0x106 wird durch den Aufruf fn(&n)
übergeben. Innerhalb von fn( ) speichert die Zuweisung *pnArg = 10 den Wert 10 in der int-Vari-
ablen, die sich an Adresse 0x102 befindet, wodurch der Wert 0 überschrieben wird. Bei Rückkehr zu
parent( ) ist der Wert von n gleich 10, weil n nur ein anderer Name für 0x102 ist.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 135
13.4.3 Referenzübergabe
C++ stellt eine kürzere Methode bereit, Variablen zu übergeben, ohne Zeiger verwenden zu müssen.
Im folgenden Beispiel wird die Variable n als Referenz übergeben. Bei der Referenzübergabe gibt die
Funktion parent( ) eine Referenz auf die Variable anstelle ihres Wertes. Referenz ist ein anderes
Wort für Adresse.
void fn(int& nArg)
{
nArg = 10;
}
void parent(void)
{
int n = 0;
Teil 3 – Samstagnachmittag
fn(n); // Übergabe von n als Referenz
// hier hat n den Wert 10
}
In diesem Fall wird eine Referenz auf n an fn( ) übergeben und nicht der Wert von n. Die Funk-
Lektion 13
tion fn( ) speichert den Wert 10 an der int-Stelle, die durch nArg referenziert wird.
=
= =
= Beachten Sie, dass Referenz kein wirklicher Typ ist. Somit ist der volle Funktions-
Hinweis name fn(int) und nicht fn(int&).
13.5 Heap-Speicher.
Genauso wie es möglich ist, einen Zeiger an eine Funktion zu übergeben, ist es für
eine Funktion möglich, einen Zeiger zurückzugeben. Eine Funktion, die die Adresse
von einem double zurückgibt, würde wie folgt deklariert:
10 Min.
double* fn(void);
Man muss allerdings sehr vorsichtig mit der Rückgabe von Zeigern sein. Um zu verstehen warum,
müssen Sie etwas mehr über Geltungsbereiche von Variablen wissen.
13.5.1 Geltungsbereich
C++-Variablen haben zusätzlich zu ihrem Wert und ihrem Typ eine Eigenschaft, die als Geltungsbe-
reich bezeichnet wird. Der Geltungsbereich ist der Bereich, in dem die Variable definiert ist. Betrach-
ten Sie den folgenden Codeschnipsel:
136 Samstagnachmittag
int nLater = 0;
nParent = nLater;
}
Die Ausführung beginnt bei main( ). Die Funktion main( ) ruft sofort parent( ) auf. Die erste
Sache, die der Prozessor in parent( ) sieht, ist die Deklaration von nParent. An dieser Stelle betritt
nParent seinen Geltungsbereich – d.h. nParent ist definiert und steht im Rest der Funktion parent
( ) zur Verfügung.
Die zweite Anweisung in parent( ) ist der Aufruf von child( ). Auch child( ) deklariert eine
lokale Variable, diesmal nChild. Die Variable nChild ist innerhalb des Geltungsbereiches von
child( ). Technisch gesehen ist nParent nicht innerhalb des Geltungsbereiches von child( ), weil
child( ) keinen Zugriff auf nParent hat. Die Variable nParent existiert aber weiterhin.
Wenn child( ) verlassen wird, verlässt die Variable nChild ihren Geltungsbereich. nChild wird
dadurch nicht nur nicht mehr zugreifbar, sondern existiert gar nicht mehr. (Der Speicher, der von
nChild belegt wurde, wird an einen allgemeinen Pool zurückgegeben, um für andere Dinge ver-
wendet zu werden.)
Wenn parent( ) mit der Ausführung fortfährt, betritt die Variable nLater bei ihrer Deklaration
ihren Geltungsbereich. Wenn parent( ) zu main( ) zurückkehrt, verlassen beide, nParent und
nLater ihren Geltungsbereich.
Der Programmierer kann Variablen außerhalb jeder Funktion deklarieren. Eine globale Variable ist
eine Variable, die außerhalb jeder Funktion deklariert ist. Eine solche Variable bleibt in ihrem Gel-
tungsbereich für die gesamte Dauer des Programms.
Weil nGlobal in diesem Beispiel global deklariert wurde, steht diese Variable allen drei Funktio-
nen zur Verfügung und bleibt für die gesamte Dauer des Programms verfügbar.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 137
void parent(void)
{
double* pdLocal;
pdLocal = child();
*pdLocal = 1.0;
}
Teil 3 – Samstagnachmittag
Das Problem ist, dass dLocalVariable nur innerhalb des Geltungsbereiches der Funktion
child( ) definiert ist. Somit existiert die Variable gar nicht mehr, wenn die Adresse von dLocalVa-
riable von child( ) zurückgegeben wird. Der Speicher, der von dLocalVariable belegt wurde,
Lektion 13
wird möglicherweise bereits für etwas anderes verwendet.
Das ist ein häufiger Fehler, weil er sich auf verschiedene Arten einschleichen kann. Unglücklicher-
weise lässt dieser Fehler das Programm nicht sofort anhalten. In der Tat kann das Programm in den
meisten Fällen korrekt weiterarbeiten, so lange, wie der vorher von dLocalVariable belegte Spei-
cher nicht anders verwendet wird. Solche sprunghaften Probleme sind am schwierigsten zu lösen.
Obwohl die Variable pdLocalVariable ihren Gültigkeitsbereich verlässt, wenn die Funktion
child( ) zurückkehrt, passiert dies nicht mit dem Speicher, auf den pdLocalVariable verweist.
Eine Speicherstelle, die von new zurückgegeben wird, verlässt ihren Gültigkeitsbereich erst, wenn
sie explizit an den Heap mittels den Kommandos delete zurückgegeben wird.
void parent(void)
{
// child() gibt die Adresse eines Blocks
// Speicher vom Heap zurück
double* pdMyDouble = child();
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 138
138 Samstagnachmittag
// ...
// ...
}
Hierbei wird der Zeiger, der von child4 zurückgegeben wird, zur Speicherung
eines double-Wertes verwendet. Nachdem die Funktion mit der Speicherstelle fertig
ist, wird sie an den Heap zurückgegeben. Den Zeiger nach dem delete auf 0 zu set-
zen ist nicht notwendig, aber eine gute Idee. Wenn der Programmierer versehent-
lich versucht, etwas in *pdMyDouble zu speichern, nachdem bereits delete ausge-
0 Min.
führt wurde, stürzt das Programm sofort ab.
!
Tipp
Ein Programm, das sofort abstürzt, wenn ein Fehler aufgetreten ist, ist viel ein-
facher zu korrigieren, als ein Programm, das im Falle eines Fehlers sprunghaft
ist.
Es gibt eine Vielzahl von Einsatzmöglichkeiten solcher Variablen; die Feinheiten der Funktionszeiger
gehen jedoch über den Umfang dieses Buches hinaus.
Zusammenfassung.
Zeigervariablen sind ein mächtiger, wenn auch gefährlicher Mechanismus, um auf Objekte über ihre
Speicheradresse zuzugreifen. Das ist wahrscheinlich das entscheidende Feature, das die Dominanz
von C, und später von C++, gegenüber anderen Programmiersprachen erklärt.
• Zeigervariablen werden durch Hinzufügen eines ‘*’ zum Variablentyp deklariert. Der Stern kann
irgendwo zwischen dem Variablennamen und dem Basistyp stehen. Es ist aber am sinnvollsten,
den Stern ans Ende des Variablentyps zu setzen.
• Der Operator & liefert die Adresse eines Objektes zurück, während der Operator * das Objekt
zurückgibt, auf das eine Adressen- oder Zeigervariable verweist.
C++ Lektion 13 31.01.2001 12:24 Uhr Seite 139
• Variablentypen wie int* sind eigene Variablentypen, und sind nicht äquivalent mit int. Die
Adressenoperator & konvertiert einen Typ wie z.B. int in einen Zeigertyp wie z.B. int*. Der Ope-
rator * konvertiert einen Zeigertyp wie z.B. int* in den Basistyp, wie z.B. int. Ein Zeigertyp kann
mit einem gewissen Risiko in einen anderen Zeigertyp konvertiert werden.
Folgende Sitzungen stellen weitere Wege dar, wie Zeiger die Trickkiste von C++ bereichern können.
Selbsttest.
1. Wenn eine Variable x den Wert 10 enthält, und an der Adresse 0x100 gespeichert ist, was ist der
Wert von x? Was ist der Wert von &x? (Siehe »Einführung in Zeigervariablen«)
2. Wenn x ein int ist, was ist der Typ von &x? (Siehe »Typen von Zeigern«)
3. Warum sollten Sie einen Zeiger an eine Funktion übergeben? (Siehe »Übergabe von Zeigerwer-
Teil 3 – Samstagnachmittag
ten«)
4. Was ist Heapspeicher und wie erhalten Sie Zugriff darauf? (Siehe »Heapspeicher«)
Lektion 13
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 140
Checkliste.
✔ Mathematische Operationen auf Zeichenzeigern einführen
✔ Die Beziehung von Zeigern und Arrays untersuchen
✔ Die Beziehung zur Beschleunigung von Programmen einsetzen
✔ Zeigeroperationen auf verschiede Zeigertypen erweitern
✔ Die Argumente von main( ) im C++- Programmtemplate erklären
D
ie Zeigertypen, die in Sitzung 13 eingeführt wurden, haben einige interes-
sante Operationen ermöglicht. Die Adresse einer Variablen zu speichern, und
dann diese Adresse mehr oder weniger wie die Variable selbst zu benutzen,
ist schon ein interessanter Partytrick, aber sein Nutzen ist begrenzt, außer bei der
permanenten Modifizierung von Variablen, die an eine Funktion übergeben wur-
30 Min. den.
Was Zeiger interessant macht, ist die Fähigkeit, mathematische Operationen aus-
führen zu können. Sicher, weil die Multiplikation zweier Adressen keinen Sinn macht, ist sie auch
nicht erlaubt. Dass zwei Adressen jedoch miteinander verglichen werden können und ein Intege-
roffset zu einer Adresse addiert werden kann, eröffnet interessante Möglichkeiten, die hier unter-
sucht werden.
pointer + offset Zeiger berechne die Adresse offset viele Einträge von Zeiger
pointer entfernt
pointer2 – pointer1 Offset berechne den Abstand zwischen den Zeigern pointer2
und pointer1
Teil 3 – Samstagnachmittag
(Obwohl nicht in Tabelle 14-1 aufgelistet, sind die abgeleiteten Operatoren, wie z.B. +=offset
und pointer++ auch als Variation der Addition definiert.)
Das einfache Speichermodell, das in Sitzung 13 zur Erklärung des Zeigerkonzeptes verwendet
wurde, ist auch hier nützlich, um die Wirkungsweise der Operatoren zu erklären. Betrachten Sie ein
Lektion 14
Array von 32 1-Bit-Zeichen, das wir cArray nennen wollen. Wenn das erste Byte des Array an Adres-
se 0x110 gespeichert wird, dann würde das Array den Speicher von 0x110 bis 0x12f belegen. Das
Element cArray[0] befindet sich an Adresse 0x110, cArray[1] an Adresse 0x111, cArray[2] an
Adresse 0x112 usw.
Nehmen Sie nun an, dass sich ein Zeiger ptr an der Adresse 0x102 befindet. Nachdem die fol-
gende Anweisung
ptr = &cArray[0];
ausgeführt wurde, enthält ptr den Wert 0x110. Dies wird in Abbildung 14.1 gezeigt.
Die Addition einer Integerzahl als Offset zum Zeiger ist so definiert, dass die Beziehungen in
Tabelle 14-2 wahr sind. Abbildung 14.2 zeigt außerdem, warum die Addition eines Offset n zu ptr
die Adresse des n-ten Elementes von cArray berechnet.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 142
142 Samstagnachmittag
Abbildung 14.1: Nach der Zuweisung ptr = &cArray[0] zeigt der Zeiger ptr auf den Anfang des Arrays
cArray.
+0 0x110 cArray[0]
+1 0x111 cArray[1]
+2 0x112 cArray[2]
+n 0x110+n cArray[n]
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 143
Teil 3 – Samstagnachmittag
Lektion 14
Abbildung 14.2: Der Ausdruck ptr + i hat als Wert die Adresse von cArray[i].
Wenn also
char* prt = &cArray[0];
dem Array-Element
cArray[n].
Weil * eine höhere Priorität hat als die Addition, addiert *ptr + n zum Zeiger
=
= =
= ptr n Zeichen. Die Klammern werden benötigt, um zu erzwingen, dass die
Hinweis Addition vor dem Operator * ausgeführt wird. Der Ausdruck *(prt + n) greift
auf das Zeichen zu, das von ptr aus n Zeichen weiter steht.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 144
144 Samstagnachmittag
In der Tat ist der Zusammenhang der beiden Ausdrücke so stark, dass C++ array[n] als nicht
mehr, als nur eine vereinfachte Version von *(ptr + n) ansieht, wobei ptr auf das erste Element von
array zeigt.
array[n] -> interpretiert C++ als -> *(&array[0] + n)
dann ist
carray == &cArray[0]
d.h., der Name des Arrays ohne einen Index repräsentiert die Adresse des Array selber. Wir kön-
nen also die Assoziation weiter vereinfachen:
array[n] -> interpretiert C++ als -> *(array + n)
Das ist eine mächtige Aussage. Z.B. könnte die Funktion displayArray( ) aus Sitzung 11, die den
Inhalt eines int-Array ausgibt, so geschrieben werden:
Die neue Funktion displayArray( ) beginnt damit, einen Zeiger pArray auf das int-Array zu
erzeugen, der auf das erste Element von nArray zeigt.
=
= =
= Gemäß unserer Konvention weist das p auf einen Zeiger hin.
Hinweis
Die Funktion durchläuft dann eine Schleife über jedes Element des Array (wobei nSize als die
Anzahl der Elemente im Array verwendet wird). In jedem Schleifendurchlauf gibt die Funktion dis-
playArray( ) die entsprechende Integerzahl aus, d.h. das int-Element, auf das pArray zeigt, bevor
der Zeiger zum nächsten Element in nArray inkrementiert wird.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 145
Diese Verwendung von Zeigern zum Zugriff auf Arrays wird nirgendwo sonst so häufig verwen-
det wie bei Zeichenarrays.
14.1.2 Zeichenarrays
Sitzung 11 hat auch erklärt, dass C++ Null-terminierte Zeichenarrays wie einen
Datentyp verwendet. C++-Programmierer verwenden oft Zeichenzeiger, um solche
Zeichenketten zu manipulieren. Der folgende Beispielcode vergleicht diese Technik
20 Min. mit der früheren Technik der Array-Indizierung.
Teil 3 – Samstagnachmittag
void concatString(char szTarget[], char szSource[]);
Die Prototypdeklaration beschreibt die Typen der Argumente, die die Funktion entgegennimmt,
sowie den Rückgabetyp. Diese Deklaration sieht wie die Definition der Funktion aus, nur ohne Body.
Lektion 14
Um die Null am Ende des Array szTarget zu finden, iteriert die Funktion concatString( ) durch
die Zeichenkette szTarget mit der folgenden while-Schleife:
void concatString(char szTarget[], char szSource[])
{
// finde das Ende der ersten Zeichenkette
int nTargetIndex = 0;
while(szTarget[nTargetIndex])
{
nTargetIndex++;
}
// ...
Unter Verwendung der Beziehung zwischen Zeigern und Arrays könnte die Funktion concat-
String( ) auch folgenden Prototyp besitzen:
void concatString(char* pszTarget, char* pszSource);
// ...
Die while-Schleife in der Arrayversion von concatString( ) wurde verlassen, sobald szTar-
get[nTargetIndex] gleich 0 war. Diese Version nun iteriert durch das Array, indem pszTarget in
jedem Schleifendurchlauf inkrementiert wird, bis das Zeichen, auf das pszTarget zeigt, gleich null
ist.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 146
146 Samstagnachmittag
=
= =
= Der Ausdruck ptr++ ist eine Kurzform von ptr = ptr + 1.
Hinweis
Wenn die while-Schleife verlassen wurde, zeigt pszTarget auf das Nullzeichen am Ende der Zei-
chenkette szTarget.
!
Tipp
Es ist nicht mehr richtig zu sagen »das Array, auf das pszTarget zeigt«, weil
pszTarget nicht mehr auf den Anfang des Array zeigt.
37. {
38. // finde das Ende der ersten Zeichenkette
39. while(*pszTarget)
40. {
41. pszTarget++;
42. }
43.
44. // hänge die zweite ans Ende der ersten
45. // (kopiere auch die Null des Quellarrays -
46. // dadurch wird das Zielarray mit einem
47. // Nullzeichen abgeschlossen)
48. while(*pszTarget++ = *pszSource++)
49. {
50. }
51.
Teil 3 – Samstagnachmittag
Die Funktion main( ) des Programms unterscheidet sich nicht von ihrer Array-basierten Cousine.
Die Funktion concatString( ) ist jedoch signifikant verschieden.
Wie bereits erwähnt, basiert die äquivalente Deklaration von concatString( ) nun auf Zeigern
Lektion 14
vom Typ char*. Zusätzlich sucht die erste while-Schleife innerhalb von concatString( ) nach
dem abschließenden Null-Zeichen am Ende des Arrays pszTarget.
Die extrem kompakte Schleife, die dann folgt, kopiert das Array pszSource an das Ende des Array
pszTarget. Die while-Klausel macht die ganze Arbeit, indem sie die folgenden Dinge ausführt:
Warum Arrayzeiger?
Die manchmal kryptische Natur von Zeiger-basierter Manipulation von Zeichenketten kann den
Leser schnell zur Frage »warum?« führen. D.h. welchen Vorteil bietet die char*-basierte Zeigerver-
sion von concatString( ) gegenüber der doch leichter lesbaren Indexversion?
=
= =
= Die Zeigerversion von concatString( ) kommt in C++-Programmen häufiger
Hinweis vor als die Array-Version aus Sitzung 11.
Die Antwort ist teilweise historisch und teilweise menschlicher Natur. So kompliziert sie auch für
den menschlichen Leser aussehen mag, kann eine Anweisung wie in Zeile 48 in eine unglaublich
kleine Anzahl von Maschineninstruktionen überführt werden. Ältere Computerprozessoren waren
nicht so schnell wie die heutigen. Als C, der Vorgänger von C++, vor etwa 30 Jahren entwickelt wur-
de, war es toll, einige Maschineninstruktionen einsparen zu können. Das gab C einen großen Vorteil
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 148
148 Samstagnachmittag
gegenüber anderen Sprachen aus dieser Zeit, insbesondere gegenüber FORTRAN, die keine Zeiger-
arithmetik enthielt.
Außerdem mögen es die Programmierer, clevere Programme zu schreiben, damit die Sache nicht
langweilig wird. Wenn C++-Programmierer erst einmal gelernt haben, wie man kompakte und kryp-
tische, aber effiziente Anweisungen schreibt, gibt es kein Mittel, sie wieder zur Suche in Arrays
mittels Index zurückzubringen.
!
neninstruktionen erzeugen:
*pszArray1++ = ‘\0’;
Früher, als die Compiler noch einfacher gebaut waren, hätte die erste Version
sicherlich weniger Instruktionen erzeugt.
=
= =
=
Weil C++-Arrays bei 0 zu zählen beginnen, ist szTarget[5] das sechste
Element des Array.
Hinweis
Es ist nicht offensichtlich, dass Zeigeraddition auch für nArray funktioniert, da jedes Element von
nArray ein int ist und damit 4 Bytes belegt. Wenn das erste Element von nArray an Adresse 0x100
steht, dann steht das sechste Element an Adresse 0x114 (0x100 + (5 * 4) = 0x114).
Glücklicherweise zeigt in C++ array + n auf das Element array[n], unabhängig davon, wie
groß ein einzelnes Element von array ist.
Eine gute Parallele bieten Häuserblocks in einer Stadt. Wenn alle Adressen in
=
= =
=
jeder Straße fortlaufend ohne Lücken, nummeriert wären, dann wäre die Haus-
nummer 1605 das sechste Haus in Block 1600. Um den Postboten nicht zu sehr
Hinweis zu verwirren, wird diese Beziehung eingehalten, unabhängig von der Größe der
Häuser.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 149
Hier belegt cArray 128 Bytes, das ist der Speicher, der für 128 Zeichen benötigt wird. pArray
Teil 3 – Samstagnachmittag
belegt nur 4 Bytes, das ist der Speicher, der für einen Zeiger benötigt wird.
Die folgende Funktion funktioniert nicht:
void arrayVsPointer()
{
Lektion 14
// greife auf Elemente mit Array zu
char cArray[128];
cArray[10] = ‘0’;
*(cArray + 10) = ‘0’;
Der Ausdruck cArray[10] und *(cArray + 10) sind äquivalent und zulässig. Die beiden Aus-
drücke, die pArray enthalten, machen keinen Sinn. Während sie in C++ zulässig sind, enthält das
nicht initialisierte pArray einen zufälligen Wert. Somit versucht das zweite Anweisungspaar ein Null-
zeichen irgendwo in den Speicher zu schreiben.
!
Tipp
Diese Art Fehler wird im Allgemeinen von der CPU abgefangen und resultiert
dann im gefürchteten Fehler einer Segmentverletzung, den Sie hin und wieder
bei Ihren Lieblingsprogrammen antreffen.
Ein zweiter Unterschied ist, dass cArray eine Konstante ist, was auf pArray nicht zutrifft. Somit
arbeitet die folgende for-Schleife, die das Array cArray initialisieren soll, nicht korrekt:
void arrayVsPointer()
{
char cArray[10];
for (int i = 0; i < 10; i++)
{
*cArray = ‘\0’; // das macht Sinn ...
cArray++; // ... das nicht
}
}
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 150
150 Samstagnachmittag
Der Ausdruck cArray++ macht nicht mehr Sinn als 10++. Die korrekte Version sieht so aus:
void arrayVsPointer()
{
char cArray[10];
char* pArray = cArray;
for (int i = 0; i < 10; i++)
{
*pArray = ‘\0’; // das funktioniert
pArray++;
}
}
Gegeben die obige Deklaration, ist pnInts[0] ein Zeiger auf einen int-Wert. Somit ist das Fol-
gende wahr:
void fn()
{
int n1;
int* pnInts[3];
pnInts[0] = &n1;
*pnInts[0] = 1;
}
oder
void fn()
{
int n1, n2, n3;
int* pnInts[3] = {&n1, &n2, &n3};
for (int i = 0; i < 3; i++)
{
*pnInts[i] = 0;
}
}
oder sogar
void fn()
{
int* pnInts[3] = {(new int),
(new int),
(new int)};
for (int i = 0; i < 3; i++)
{
*pnInts[i] = 0;
}
}
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 151
Teil 3 – Samstagnachmittag
{
char* pszReturnValue;
switch(nMonth)
Lektion 14
{
case 1: pszReturnValue = »Januar«;
break;
case 2: pszReturnValue = »Februar«;
break;
case 3: pszReturnValue = »März«;
break;
// ... usw ...
default: pszReturnValue = »invalid«;
}
return pszReturnValue;
}
Wenn 1 übergeben wird, geht die Kontrolle an die erste case-Anweisung über und die Funktion
würde einen Zeiger auf die Zeichenkette »Januar« zurückgeben; wenn eine 2 übergeben wird,
kommt »Februar« zurück usw.
=
= =
= Das switch( )-Kontrollkommando ist vergleichbar mit einer Folge von
Hinweis if-Anweisungen.
Eine elegantere Lösung nutzt den Integerwert als Index für ein Array von Zeigern auf die Monats-
namen. Praktisch sieht das so aus:
// int2month() – gib den Namen des Monats zurück
char* int2month(int nMonth)
{
// überprüfe den Wert auf Gültigkeit
if (nMonth < 1 || nMonth > 12)
{
return »ungültig«;
}
152 Samstagnachmittag
Hierbei überprüft die Funktion int2month( ) zuerst, ob der Wert von nMonth zwischen 1 und 12
ist (die default-Klausel der switch-Anweisung hat das für uns im vorhergehenden Beispiel erle-
digt). Wenn nMonth zulässig ist, benutzt die Funktion diesen Wert als Offset für das Array, das die
Monatsnamen enthält.
MS-DOS führt das Programm, das in der Datei MyProgram.exe enthalten ist, aus und übergibt
ihm die Argumente file.txt und /w.
=
= =
=
Der Nutzen des Begriffs Argument ist ein wenig verwirrend. Die Argumente
eines Programms und die Argumente einer C++-Funktion folgen einer unter-
Hinweis schiedlichen Syntax, aber die Bedeutung ist die gleiche.
// das war es
cout << »Das war es\n«;
return 0;
}
Wie immer akzeptiert die Funktion main( ) zwei Argumente. Das erste ist ein int und trägt den
Namen nArgs. Diese Variable enthält die Anzahl der Argumente, die an das Programm übergeben
Teil 3 – Samstagnachmittag
wurden. Das zweite Argument ist ein Array von Zeigern vom Typ char*, das ich pszArgs genannt
habe. Jedes dieser char*-Elemente zeigt auf ein Argument, das dem Programm übergeben wurde.
Betrachten Sie das Programm PrintArgs. Wenn ich das Programm aufrufe mit
Lektion 14
PrintArgs arg1 arg2 arg3 /w
von der Kommandozeile eines MS-DOS-Fensters aus, wäre nArgs gleich 5 (eins für jedes Argu-
ment). Das erste Argument ist der Name des Programms selber. Somit zeigt psArgs[0] auf Print-
Args. Die restlichen Elemente in pzArgs zeigen auf die Programmargumente. Das Element
pszArgs[1] zeigt auf arg1, pszArgs[2] zeigt auf arg2 usw. Weil MS-DOS /w keine besondere
Bedeutung beimisst, wird diese Zeichenkette in gleicher Weise als ein Argument an das Programm
übergeben.
=
= =
=
Das Gleiche gilt nicht für die Richtungszeichen »<«, »>« und »|«. Diese haben
unter MS-DOS eine besondere Bedeutung und werden nicht als Argument an
Hinweis
das Programm übergeben.
Es gibt verschiedene Wege, Argumente an eine Funktion zu übergeben. Der einfachste Weg ist,
das Programm vom MS-DOS-Prompt aus aufzurufen. Beide Debugger, von Visual C++ und von
GNU C++, stellen einen Mechanismus bereit, um Argumente während des Debuggens zu überge-
ben.
In Visual C++, wählen Sie das Debug-Feld in der Dialog-Box »Project Settings« aus. Geben Sie
Ihre Argumente in das Eingabefenster »Program Arguments« ein wie in Abbildung 14.3 zu sehen ist.
Das nächste Mal, wenn Sie Ihr Programm starten, übergibt Visual C++ diese Argumente.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 154
154 Samstagnachmittag
Abbildung 14.3: Visual C++ verwendet Project Settings zur Übergabe von Argumenten an das Programm
während des Debuggens.
In rhide wählen Sie »Argumente...« im Menü »Start«. Geben sie die Argumente im Fenster ein.
Dies ist in Abbildung 14.4 zu sehen.
Zusammenfassung.
Alle Programmiersprachen basieren die Indizierung von Arrays auf einfachen mathe-
matischen Operationen auf Zeigern. Durch die Möglichkeit für den Programmierer,
auf diese Art Operation direkt zuzugreifen, gibt C++ dem Programmierer eine große
0 Min. semantische Freiheit. Der C++-Programmierer kann die Beziehung zwischen der
Manipulation von Arrays und Zeigern untersuchen und zu seinem Vorteil nutzen.
In dieser Sitzung haben wir gesehen, dass
• die Indizierung auf Arrays einfache mathematische Operationen auf Zeigern beinhaltet. C++ ist
praktisch einzigartig darin, dass der Programmierer diese Operationen selber ausführen kann.
• Zeigeroperationen auf Zeichenarrays boten das größte Potenzial zur Leistungssteigerung bei den
frühen C- und C++-Compilern. Ob das immer noch der Fall ist, darüber lässt sich streiten. Jeden-
falls sind Zeichenzeiger Teil des täglichen Lebens geworden.
C++ Lektion 14 31.01.2001 12:25 Uhr Seite 155
• C++ passt die Zeigerarithmetik an die unterschiedliche Größe der Objekte an, auf die Zeiger ver-
weisen. Somit vergrößert die Inkrementierung eines char-Zeigers dessen Wert um 1, während die
Inkrementierung eines double-Zeigers zu einer Vergrößerung seines Wertes um 8 führt. Die Inkre-
mentierung eines Zeigers auf Klassenobjekte kann zu einer Vergrößerung von Hunderten von
Bytes führen.
• Arrays von Zeigern können signifikant die Effizient eines Programms erhöhen, das einen int-Wert
in eine Konstante eines anderen Typs verwandelt, wie z.B. eine Zeichenkette oder ein Bitfeld.
• Argumente eines Programms werden an die Funktion main( ) als Array von Zeigern auf Zeichen-
ketten übergeben.
Selbsttest.
Teil 3 – Samstagnachmittag
1. Wenn das erste Element eines Array von Zeichen c[] an Adresse 0x100 steht, was ist die Adresse
von c[2]? (Siehe »Operationen auf Zeigern«)
2. Was ist das Indexäquivalent zum Zeigerausdruck *(c + 2)? (Siehe »Zeiger und Array-basierte
Manipulation von Zeichenketten«)
Lektion 14
3. Was ist der Sinn der beiden Argumente von main( )? (Siehe »Die Argumente von main( )«)
C++ Lektion 15 31.01.2001 12:27 Uhr Seite 156
Checkliste.
✔ Zeiger auf Objekte deklarieren und verwenden
✔ Objekte durch Zeiger übergeben
✔ Objekte vom Heap allozieren
✔ Verkettete Listen erzeugen und manipulieren
✔ Verkettete Listen von Objekten und Objektarrays vergleichen
S
itzung 12 demonstrierte, wie Arrays und Klassenstrukturen in Arrays von
Objekten kombiniert werden können, um eine Reihe von Problemen zu lösen.
In gleicher Weise löst die Einführung von Zeigern auf Objekte einige Probleme,
die von Objektarrays nicht so leicht gelöst werden können.
30 Min.
=
= =
= Der Typ von pMS ist »Zeiger auf MyClass«, was auch als MyClass* ausgedrückt
Hinweis werden kann.
Auf Elemente eines solchen Objektes kann wie folgt zugegriffen werden:
(*pMS).n1 = 1;
(*pMS).c2 = ‘\0’;
In Wörtern besagt der erste Ausdruck »weise 1 dem Element n1 des MS-Objektes zu, auf das pMS
verweist.«
Teil 3 – Samstagnachmittag
!
Tipp
Die Klammern sind notwendig, weil ».« eine höhere Priorität hat als »*«. Der
Ausdruck *mc.pN1 bedeutet »die Integerzahl, auf die das pN1-Element des
Objektes mc verweist«.
Lektion 15
Genauso wie C++ eine Kurzform für den Gebrauch von Arrays bereitstellt, definiert C++ beque-
mere Operatoren, um auf die Elemente eines Objektes zuzugreifen. Der Operator -> ist wie folgt
definiert:
(*pMS).n1 ist äquivalent zu pMS->n1
Der Pfeiloperator wird fast ausschließlich verwendet, weil das so leichter zu lesen ist. Die beiden
Formen sind jedoch völlig äquivalent.
158 Samstagnachmittag
Das Hauptprogramm erzeugt ein Objekt aus der Klasse MyClass. Das Objekt wird zuerst an die
Funktion myFunc(MyClass) übergeben, und dann wird die Adresse des Objektes an die Funktion
myFunc(MyClass*) übergeben. Beide Funktionen ändern den Wert des Objektes – nur die Ände-
rungen, die innerhalb von myFunc(MyClass*) durchgeführt wurden, bleiben erhalten.
Im Aufruf von myFunc(MyClass) macht C++ eine Kopie des Objektes. Änderungen am Objekt
mc in dieser Funktion bleiben in main( ) nicht erhalten. Der Aufruf von myFunc(MyClass*) über-
gibt die Adresse auf das ursprüngliche Objekt in main( ). Das Objekt enthält alle gemachten Ände-
rungen, wenn die Kontrolle an main( ) zurückgegeben wird.
Dieser Vergleich von Kopie und Original ist das Gleiche, wie der Vergleich der beiden Funktionen
fn(int) und fn(int*).
=
= =
= Abgesehen vom Erhalt von Änderungen kann die Übergabe eines 4-Byte-Zeigers
Hinweis wesentlich effizienter sein, als die Kopie eines Objektes.
C++ Lektion 15 31.01.2001 12:27 Uhr Seite 159
15.1.2 Referenzen
Sie können Referenzen verwenden, um C++ einige Zeigermanipulationen durchführen zu lassen.
// myFunc – mc behält Änderungen in
// aufrufender Funktion
void myFunc(MyClass& mc)
{
mc.n1 = 1;
mc.n2 = 2;
}
Teil 3 – Samstagnachmittag
// ...
=
= =
= Sie haben dieses Feature bereits gesehen. Das Beispiel ClassData in Sitzung 12
Lektion 15
Hinweis
verwendete eine Referenz auf ein Klassenobjekt im Aufruf getData(NameData-
Set&), um die gelesenen Daten an den Aufrufenden zurückzugeben.
Wenn myFunc( ) zurückkehrt, verlässt das Objekt mc seinen Gültigkeitsbereich. Der Zeiger, der
von myFunc( ) zurückgegeben wird, ist nicht gültig in der aufrufenden Funktion. (Siehe Sitzung 13
zu Details.)
Das Objekt vom Heap zu allozieren, löst das Problem:
MyClass* myFunc()
{
MyClass* pMC = new MyClass;
return pMC;
}
!
Tipp
Der Heap wird verwendet, um Objekte in verschiedenen Situationen zu
allozieren.
C++ Lektion 15 31.01.2001 12:27 Uhr Seite 160
160 Samstagnachmittag
20 Min.
// ...
}
Zusätzlich muss jeder Eintrag im Array vom gleichen Typ sein. Es ist nicht möglich, Objekte der
Klassen MyClass und YourClass im gleichen Array zu speichern.
Schließlich ist es schwierig, ein Objekt mitten in das Array einzufügen. Um ein Objekt hinzuzufü-
gen oder zu löschen, muss das Programm angrenzende Objekte nach oben oder nach unten kopie-
ren, um eine Lücke zu schaffen oder zu schließen.
Es gibt Alternativen zu Arrays, die diese Einschränkungen nicht besitzen. Die bekannteste ist die
verkettete Liste.
Hierbei zeigt pNext auf den nächsten Eintrag in der Liste. Das sehen Sie in Abbildung 15.1.
Teil 3 – Samstagnachmittag
Lektion 15
Abbildung 15.1: Eine verkettete Liste besteht aus einer Anzahl von Objekten; jedes Objekt verweist auf
das nächste Objekt in der Liste.
=
= =
=
Initialisieren Sie jeden Zeiger mit 0, der im Kontext von Zeigern auch als null
bezeichnet wird. Dieser Wert wird als Nullzeiger bezeichnet. In jedem Fall wird
Hinweis
der Zugriff auf die Adresse 0 immer zum Anhalten des Programms führen.
!
Tipp
Der Cast von 0 als Typ int nach LinkableClass* ist nicht nötig. C++ interpre-
tiert 0 als beliebigen Typ, als eine Art »universeller Zeiger«. Ich finde jedoch,
dass es ein guter Stil ist.
162 Samstagnachmittag
Dieser Prozess wird in Abbildung 15.2 grafisch dargestellt. Nach der ersten Zeile zeigt das Objekt
*pLC auf das erste Objekt der Liste (das gleiche, auf das pHead zeigt), dargestellt als Schritt A. Nach der
zweiten Anweisung zeigt der Kopfzeiger auf das übergebene Objekt *pLC, dargestellt als Schritt B.
pLC
pNext
B A
pHead
pNext
pNext ø
Abbildung 15.2: Ein Element wird in zwei Schritten am Kopf der Liste eingefügt.
Die Funktion addTail( ) beginnt mit einer Iteration, um das Element zu finden, dessen pNext-
Zeiger gleich null ist – das ist das letzte Element in der Liste. Wenn dieses Element gefunden ist, ver-
kettet addTail( ) das Objekt *pLC mit dem Ende der Liste.
(Tatsächlich enthält die Funktion addTail( ) so, wie wir sie geschrieben haben, einen Bug. Ein
spezieller Test muss hinzugefügt werden, um festzustellen, ob pHead selbst null ist, was anzeigen
würde, dass die Liste leer war).
Die Funktion remove( ) ist ähnlich. Die Funktion entfernt das spezifizierte Objekt aus der Liste
Teil 3 – Samstagnachmittag
und gibt 1 zurück, wenn dies erfolgreich war, sonst 0.
Lektion 15
// wenn die Liste leer ist, ist *pLC
// offensichtlich nicht in der Liste
if (pCurrent == (LinkableClass*)0)
{
return 0;
}
Die Funktion remove( ) überprüft zuerst, ob die Liste auch nicht leer ist – wenn sie leer ist, gibt
die Funktion den Fehlercode zurück, da offensichtlich das Objekt *pLC nicht in der Liste enthalten
ist. Wenn die Liste nicht leer ist, iteriert remove( ) durch alle Elemente, bis das Objekt gefunden
wird, das auf *pLC verweist. Wenn dieses Objekt gefunden wird, setzt remove( ) den Zeiger pCur-
rent->pNext an *pLC vorbei. Dieser Prozess wird in Abbildung 15.3 grafisch veranschaulicht.
C++ Lektion 15 31.01.2001 12:27 Uhr Seite 164
164 Samstagnachmittag
Abbildung 15.3: »Umgehe« einen Eintrag, um ihn aus der Liste zu entfernen
Teil 3 – Samstagnachmittag
// Sozialversicherungsnummer
class NameDataSet
{
public:
char szFirstName[128];
Lektion 15
char szLastName [128];
int nSocialSecurity;
166 Samstagnachmittag
Teil 3 – Samstagnachmittag
{
// Anzeige des aktuellen Eintrags
displayData(pNDS);
Lektion 15
// gehe zum nächsten Eintrag
pNDS = pNDS->pNext;
}
return 0;
}
Obwohl es in gewisser Hinsicht lang ist, ist das Programm LinkedListData doch recht einfach.
Die Funktion main( ) beginnt mit dem Aufruf von getData( ), um einen NameDataSet-Eintrag vom
Benutzer zu bekommen. Wenn der Benutzer ende eingibt, gibt getData( ) null zurück. main( ) ruft
dann addTail( ) auf, um den Eintrag, der von getData( ) zurückgegeben wurde, an das Ende der
verketteten Liste anzufügen.
Sobald es vom Benutzer keine weiteren NameDataSet-Objekte gibt, iteriert main( ) durch die
Liste, und zeigt jedes Objekt mittels der Funktion displayData( ) an.
Die Funktion getData( ) alloziert zuerst ein leeres NameDataSet-Objekt von Heap. getData( )
liest dann den Vornamen des einzufügenden Eintrags. Wenn der Benutzer als Vornamen ende oder
ENDE eingibt, löscht die Funktion das Objekt, und gibt null an den Aufrufenden zurück. getData( )
fährt mit dem Lesen des Nachnamens und der Sozialversicherungsnummer fort. Schließlich setzt
getData( ) den Zeiger pNext auf null, bevor die Funktion zurückkehrt.
!
Tipp
Lassen Sie Zeiger niemals uninitialisiert. Wenden Sie die alte Programmierer-
regel an: »Im Zweifelsfalle ausnullen«.
Die Funktion addTail( ), die hier auftaucht, ist der Funktion addTail( ) sehr ähnlich, die
bereits in diesem Kapitel dargestellt wurde. Anders als die ältere Version, überprüft diese Version von
addTail( ), ob die Liste leer ist, bevor sie startet. Wenn pHead null ist, dann setzt addTail( ) den
Zeiger pHead auf den aktuellen Eintrag und terminiert.
Die Funktion displayData( ) ist eine Zeiger-basiere Version der früheren Funktionen display-
Data( ).
C++ Lektion 15 31.01.2001 12:27 Uhr Seite 168
168 Samstagnachmittag
Zusammenfassung.
Zeiger auf Klassenobjekte machen es möglich, den Wert von Klassenobjekten innerhalb von Funk-
tionen zu verändern. Eine Referenz auf ein Klassenobjekt zu übergeben, ist bedeutend schneller, als
ein Klassenobjekt als Wert zu übergeben. Ein Zeigerelement in eine Klasse einzufügen, ermöglicht
eine Verkettung der Objekte in einer verketteten Liste. Die Struktur der verketteten Liste bietet meh-
rere Vorteile gegenüber Arrays, während andererseits Effizienz eingebüßt wird.
• Zeiger auf Klassenobjekte arbeiten im Wesentlichen wie Zeiger auf andere Datentypen. Dies
umfasst die Fähigkeit, Objekte als Referenz an eine Funktion zu übergeben.
• Ein Zeiger auf ein lokales Objekt ist nicht mehr gültig, wenn die Kontrolle von der Funktion zurück-
gegeben wurde. Objekte, die vom Heap alloziert wurden, haben keine solche Beschränkung ihres
Gültigkeitsbereiches und können daher von Funktion zu Funktion übergeben werden. Es obliegt
jedoch dem Programmierer, Objekte, die vom Heap alloziert wurden, an den Heap zu-
rückzugeben; geschieht dies nicht, sind fatale und schwer zu findende Speicherlöcher die Folge.
• Objekte können in einer verketteten Liste miteinander verbunden werden, wenn ihre Klasse einen
Zeiger auf ein Objekt ihres eigenen Typs enthält. Es ist einfach, Elemente in die verkettete Liste ein-
zufügen, und Elemente aus der verketteten Liste zu löschen. Obwohl wir das hier nicht gezeigt
haben, ist das Sortieren einer verketteten Liste einfacher als das Sortieren eines Array. Objektzeiger
sind auch nützlich bei der Erzeugung anderer Container, die hier nicht vorgestellt wurden.
Selbsttest.
1. Gegeben sei die folgende Klasse:
class MyClass
{
int n;
}
MyClass* pM;
Wie würden Sie das Datenelement n vom Zeiger pM referenzieren? (Siehe »Zeiger auf Objekte«)
2. Was ist ein Container? (Siehe »Andere Container«)
3. Was ist eine verkettete Liste? (Siehe »Verkettete Listen«)
4. Was ist ein Kopfzeiger? (Siehe »Verkettete Listen«)
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 169
Debuggen II
16 Lektion
Checkliste.
✔ Schrittweise durch ein Programm
✔ Haltepunkte setzen
✔ Variablen ansehen und modifizieren
✔ Ein Programm mit einem Debugger debuggen
S
itzung 10 hat eine Technik zum Debuggen von Programmen vorgestellt, die
auf der Ausgabe von Schlüsselinformationen auf cout basiert. Wir haben
diese so genannte Technik der Ausgabeanweisungen verwendet, um das
zugegebenermaßen einfache Beispielprogramm ErrorProgram zu debuggen.
Für kleinere Programme arbeitet diese Technik gut. Probleme mit dieser Tech-
30 Min.
nik treten erst auf, wenn der Umfang der Programme über den der hier darge-
stellten Beispielprogramme hinausgeht.
In größeren Programmen weiß der Programmierer oft nicht, wo er Ausgabeanweisungen hinset-
zen muss. Ein strenger Zyklus von Einfügen von Ausgabeanweisungen, Erzeugen des Programms,
Ausführen des Programms, Einfügen von Ausgabeanweisungen usw. ist nervig. Um Ausgabeanwei-
sungen zu verändern, muss das Programm stets neu erzeugt werden. Bei einem großen Programm
kann selbst die Zeit für die Erzeugung schon beachtlich sein.
Ein zweiter, ausgeklügelterer Ansatz basiert auf einem separaten Werkzeug, dem Debugger. Die-
ser Zugang vermeidet viele der Nachteile, die in der Technik der Ausgabeanweisungen enthalten
sind. Diese Sitzung führt Sie in die Verwendung des Debuggers ein, indem wir den Bug in einem
kleinen Programm finden.
Ein großer Teil dieses Buches ist dem Studium der Programmierfähigkeiten
gewidmet, die durch Zeigervariablen ermöglicht werden. Zeiger haben jedoch
=
= =
= auch ihren Preis: Zeigerfehler sind leicht zu begehen, und extrem schwierig zu
Hinweis finden. Die Technik der Ausgabeanweisungen taugt für das Finden und Entfer-
nen von Zeigerfehlern nicht. Nur ein guter Debugger kann bei solchen Fehlern
helfen.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 170
170 Samstagnachmittag
Build Shift+F8 F9
Step In F11 F7
Go F5 Ctl+F9
.
CD-ROM
Die Datei ist auf der beiliegenden CD-ROM unter dem Namen
Concatenate(Error).cpp zu finden.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 171
Teil 3 – Samstagnachmittag
cin.getline(szString1, 128);
Lektion 16
cout << »Zeichenkette #2:«;
cin.getline(szString2, 128);
return 0;
}
172 Samstagnachmittag
Das Programm kann ohne Fehler erzeugt werden. Ich führe das Programm aus. Das Programm fragt
nach Zeichenkette #1, und ich gebe »das ist eine Zeichenkette« ein. Für Zeichenkette #2 gebe ich
DAS IST EINE ZEICHENKETTE ein. Aber anstatt die korrekte Ausgabe zu erzeugen, bricht das Pro-
gramm mit Fehlercode 0xff ab. Ich drücke OK. Der Debugger versucht, mir ein wenig Trost zu spen-
den, indem er das Fenster unterhalb des Eingabefensters öffnet, wie in Abbildung 16.1 zu sehen ist.
Die erste Zeile im Nachrichtenfenster zeigt an, dass rhide denkt, dass der Fehler in Zeile 46 des
Moduls Concatenate(error1) aufgetreten ist. Außerdem wurde die Funktion, die abgestürzt ist,
von Zeile 29 im gleichen Modul aufgerufen. Das weist scheinbar darauf hin, dass die initiale while-
Schleife innerhalb von concatString( ) fehlerhaft ist.
Abbildung 16.1: Der rhide-Debugger gibt einen Hinweis auf die Fehlerquelle, wenn ein Programm
abstürzt.
Weil ich in dieser Anweisung keinen Fehler finden kann, mache ich den Debugger zu meinem
Gehilfen.
=
= =
= Tatsächlich sehe ich das Problem, ausgehend von den Informationen, die rhide
Hinweis mir liefert. Doch gehen Sie weiter mit mir durch.
Ein Programm zeilenweise auszuführen wird auch als Einzelschrittausführung eines Programms
bezeichnet.
Als ich Step Over für das Kommando cin.getline( ) ausführe, bekommt der Debugger die
Kontrolle vom MS-DOS-Fenster nicht wie üblich zurück. Stattdessen scheint das Programm beim
Prompt eingefroren zu sein und darauf zu warten, dass die erste Zeichenkette eingegeben wird.
In der Nachbetrachtung merke ich, dass der Debugger die Kontrolle vom Programm erst dann
zuruckbekommt, wenn die C++-Anweisung ausgeführt ist – eine Anweisung, die den Aufruf get-
line( ) enthält, ist erst dann ausgeführt, wenn ich einen Text über die Tastatur eingegeben habe.
Ich gebe eine Zeile Text ein und drücke die Entertaste. Der rhide-Debugger hält das Programm
bei der nächsten Anweisung an: cout << »Zeichenkette #2:«. Wieder führe ich einen Einzelschritt
aus, indem ich die zweite Zeile Text eingebe als Antwort auf den zweiten Aufruf von getline( ).
Teil 3 – Samstagnachmittag
Wenn der Debugger anzuhalten scheint, ohne zurückzukommen, wenn Sie in
! Einzelschritten durch ein Programm gehen, wartet Ihr Programm darauf, dass
etwas Bestimmtes passiert. Am wahrscheinlichsten ist, dass das Programm auf
Lektion 16
Tipp
eine Eingabe wartet, entweder von Ihnen oder von einem externen Device.
Schließlich gehe ich im Einzelschrittmodus durch den Aufruf von concatString( ), wie in Abbil-
dung 16.2 zu sehen ist. Als ich Step Over für den Aufruf versuche, stürzt das Programm wie zuvor ab.
Das sagt mit nicht mehr als ich schon vorher wusste. Was ich brauche, ist die Möglichkeit, Einzel-
schritte in der Funktion auszuführen, statt einen Schritt über die Funktion hinweg zu machen.
!
Tipp
Denken Sie immer daran, Program Reset zu drücken, bevor Sie erneut begin-
nen. Es tut nicht weh, den Knopf oft zu drücken. Sie können sich daran gewöh-
nen, Program Reset jedes Mal auszuführen, bevor Sie den Debugger starten.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 174
174 Samstagnachmittag
Wieder gehe ich in Einzelschritten durch das Programm unter Verwendung von Step Over, bis ich
den Aufruf von concatString( ) erreiche. Diesmal rufe ich nicht Step Over, sondern Step In auf,
um in die Funktion zu gelangen. Sofort bewegt sich die Markierung zur ersten ausführbaren Zeile
innerhalb von concatString( ) wie in Abbildung 16.3 zu sehen ist.
!
Tipp
Es gibt keinen Unterschied zwischen Step Over und Step In, wenn kein Funk-
tionsaufruf ausgeführt wird.
Abbildung 16.3: Das Kommando Step In bewegt die Kontrolle zur ersten ausführbaren Zeile von concat-
String( ).
Wenn Sie Step In versehentlich für eine Funktion ausgeführt haben, kann es
sein, dass der Debugger Sie nach der Quelldatei einer Datei fragen wird, von
der Sie vorher noch nie gehört haben. Das ist die Datei des Bibliothekmoduls,
!
Tipp
das die Funktion enthält, in die Sie hineingehen wollten. Drücken Sie Cancel,
und Sie erhalten eine Liste von Maschineninstruktion, die selbst für die härtes-
ten Techniker nicht sehr hilfreich ist. Um wieder in einen gesunden Zustand zu
kommen, öffnen Sie das Eingabefenster, setzen einen Haltepunkt, wie im nächs-
ten Abschnitt beschrieben, auf die Anweisung direkt nach dem Aufruf und
drücken Go.
Mit großer Hoffnung drücke ich Step Over, um die erste Anweisung in der Funktion auszuführen.
Der rhide-Debugger antwortet mit der Meldung eines Segmentfehlers wie in Abbildung 16.4 zu
sehen ist.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 175
Abbildung 16.4: Ein Einzelschritt auf der ersten Zeile von concatString( ) erzeugt einen Segmentfehler.
Teil 3 – Samstagnachmittag
!
Ein Segmentfehler zeigt im Allgemeinen an, dass ein Programm auf ein ungülti-
ges Speichersegment zugegriffen hat, entweder weil ein Zeiger ungültig gewor-
den ist, oder ein Array außerhalb seiner Grenzen adressiert wurde. Um es inter-
Lektion 16
Tipp
essanter zu machen, lassen Sie uns annehmen, dass ich das nicht weiß.
Jetzt weiß ich sicher, das etwas in der while-Schleife nicht korrekt ist, und dass bereits die erste
Ausführung der Schleife zum Absturz führt. Um herauszufinden, was schiefgeht, muss ich das Pro-
gramm unmittelbar vor der fehlerhaften Zeile anhalten.
Ein Haltepunkt teilt dem Debugger mit, bei dieser Anweisung anzuhalten, wenn die Kontrolle jemals
dorthin gelangt. Ein Haltepunkt lässt ein Programm wie gewohnt ablaufen bis zu diesem Punkt, an dem
wir die Kontrolle übernehmen möchten. Haltepunkte sind nützlich, wenn wir wissen, wo wir anhalten
möchten, oder wenn wir das Programm normal ausführen möchten, bis es Zeit zum Anhalten ist.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 176
176 Samstagnachmittag
Nachdem ich den Haltepunkt gesetzt habe, drücke ich Go. Das Programm scheint normal aus-
geführt zu werden bis zum Aufruf von while. An diesem Punkt springt das Programm zurück zum
Debugger.
Abbildung 16.6: Dieses Fenster erlaubt es dem Programmierer, gültige Variablen anzusehen und ihren
Wert zu verändern.
Mit den nun initialisierten Indexvariablen gehe ich in Einzelschritten durch die while-Schleife
hindurch. Jeder Aufruf von Step Over oder Step In führt eine Iteration der while-Schleife aus. Weil
der Cursor nach dem Aufruf da steht, wo er vorher auch war, tritt scheinbar keine Veränderung auf;
nach einem Schleifendurchlauf hat nTargetIndex jedoch den Wert 1.
Weil ich mir nicht die Arbeit machen möchte, den Wert von nTargetIndex nach jeder Interation
zu überprüfen, führe ich einen Doppelklick auf nTargetIndex aus, und führe das Kommando Add
Watch aus. Es erscheint ein Fenster mit der Variable nTargetIndex und dem Wert 1 rechts daneben.
Ich drücke mehrmals Step In, und in jeder Iteration wird der Wert von nTargetIndex um eins
erhöht. Nach einigen Iterationen wird die Kontrolle an eine Anweisung außerhalb der Schleife über-
geben.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 177
Ich setze einen Haltepunkt auf die schließende Klammer der Funktion concatString( ) und drü-
cke Go. Das Programm hält unmittelbar vor der Rückkehr der Funktion an.
Um die erzeugte Zeichenkette zu überprüfen, führe ich eine Doppelklick auf szTarget aus und
drücke View Variable. Die Ergebnisse, die in Abbildung 16.7 zu sehen sind, sind nicht so, wie ich es
erwartet habe.
!
Tipp
Die 0x6ccee0 ist die Adresse der Zeichenkette im Speicher. Diese Information
kann hilfreich sein, wenn es um Zeiger geht. Diese Information könnte z.B. sehr
hilfreich sein beim Debuggen einer Anwendung, die verkettete Listen verwendet.
Teil 3 – Samstagnachmittag
Es sieht so aus, als ob die Zielzeichenkette nicht verändert worden wäre, obwohl ich genau weiß,
dass die zweite while-Schleife ausgeführt wurde. Mit einer kleinen Chance, dass die zweite Zei-
chenkette doch da ist, sehe ich hinter der initialen Zeichenkette nach. szTarget + 27 sollte die
Adresse des ersten Zeichens nach der Null der Zeichenkette »DIES IST EINE ZEICHENKETTE«, die ich
Lektion 16
eingegeben habe, sein. Und tatsächlich, das » – « steht da, gefolgt von einem »d«, das korrekt zu
sein scheint. Das ist in Abbildung 16.8 zu sehen.
Abbildung 16.7: Die Zielzeichenkette scheint nach Rückkehr der Funktion concatString( ) nicht verän-
dert zu sein.
Abbildung 16.8: Die Quellzeichenkette scheint an einer falschen Stelle an die Zielzeichenkette angefügt
worden zu sein.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 178
178 Samstagnachmittag
Nach reiflicher Überlegung ist es offensichtlich, dass szSource hinter dem abschließenden Null-
zeichen an szTarget angefügt wurde. Zusätzlich ist klar, dass die resultierende Ergebniszeichenket-
te überhaupt nicht abgeschlossen wurde (daher das »D« am Ende).
!
Tipp
Eine Zeichenkette hinter dem Nullzeichen zu verändern, oder das terminieren-
de Nullzeichen zu vergessen, sind die häufigsten Fehler beim Umgang mit Zei-
chenketten.
Weil ich jetzt zwei Fehler kenne, drücke ich Program Reset und berichtige die Funktion concat-
String( ), so dass die zweite Zeichenkette an der richtigen Stelle eingefügt wird und die Ergebnis-
zeichenkette mit einem Nullzeichen abgeschlossen wird. Die geänderte Funktion concatString( )
sieht wie folgt aus:
Weil ich vermute, dass es noch ein weiteres Problem gibt, beobachte ich szTarget und nTarget-
Index, während die zweite Schleife ausgeführt wird. Nun wird die Zeichenkette korrekt ans Ende der
Zielzeichenkette kopiert, wie in Abbildung 16.9 zu sehen ist. (Abbildung 16.9 zeigt den zweiten Auf-
ruf von concatString( ), weil er besser zu verstehen ist.)
Sie müssen das wirklich selber ausführen. Das ist der einzige Weg, wie Sie ein
!
Tipp
Gefühl dafür bekommen könne, wie hübsch es ist, einer Zeichenkette beim
Wachsen zuzusehen, während die andere Zeichenkette in jeder Interation der
Schleife schrumpft.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 179
Abbildung 16.9: Zeigt wie die Quellzeichenkette an das Ende der Zielzeichenkette gehängt wird.
Teil 3 – Samstagnachmittag
Bei der erneuten Untersuchung der Zeichenkette unmittelbar vor dem Anfügen der terminieren-
den Null, stelle ich fest, dass die Zeichenkette szTarget korrekt ist mit Ausnahme des Extrazeichens
am Ende, wie in Abbildung 16.10 zu sehen ist.
Lektion 16
Abbildung 16.10: Vor dem Hinzufügen des Nullzeichens enthält die Ergebniszeichenkette am Ende weite-
re Zeichen.
Sobald ich Step Over drücke, fügt das Programm das Nullzeichen an und die »Rauschzeichen«
verschwinden aus dem Fenster.
180 Samstagnachmittag
Ein zweiter Unterschied ist die Art und Weise, mit der der Visual C++-Debugger das Ansehen von
Variablen löst. Wenn die Ausführung an einem Haltepunkt gestoppt ist, kann der Programmierer ein-
fach den Cursor über eine Variable bewegen. Wenn die Variable im Gültigkeitsbereich ist, zeigt der
Debugger ihren Wert in einem kleinen Fenster, wie in Abbildung 16.11 zu sehen ist.
Abbildung 16.11: Visual C++ zeigt den Wert einer Variable, wenn der Cursor über sie bewegt wird.
Zusätzlich bietet der Debugger von Visual C++ einen bequemen Weg, um lokale
Variablen (das sind Variable, die lokal in einer Funktion deklariert sind) anzusehen.
Wählen Sie View, dann Debug Windows und schließlich Variables. Vom Fenster Vari-
ables aus wählen Sie die Locals-Schaltfäche. Alternativ können Sie Alt+4 drücken.
Dieses Fenster markiert sogar die Variablen, die seit dem letzen Haltepunkt verän-
0 Min.
dert wurden. Abbildung 16.12 zeigt dieses Fenster, während Einzelschritte durch
das Kopieren der Quelle in die Zielzeichenkette ausgeführt werden.
=
= =
= Das Zeichen »I« am Ende von szTarget spiegelt die Tatsache wieder, dass die
Hinweis Zeichenkette noch nicht terminiert wurde.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 181
Abbildung 16.12: Dieses Fenster des Visual C++-Debuggers zeigt den Wert von Variablen, während Ein-
zelschritte ausgeführt werden.
Zusammenfassung.
Lassen Sie uns den Debugger-Zugang zum Finden eines Problems vergleichen mit dem Zugang
Teil 3 – Samstagnachmittag
über Ausgabeanweisungen, den wir in Sitzung 10 eingeführt haben. Der Debugger-Zugang ist nicht
einfach zu erlernen. Ich bin mir sicher, dass Ihnen viele der hier eingegebenen Kommandos fremd
vorgekommen sind. Wenn Sie sich jedoch erst einmal an den Debugger gewöhnt haben, können Sie
Lektion 16
ihn nutzen, um viel über Ihr Programm zu erfahren. Die Fähigkeit, langsam durch Ihr Programm
durchzugehen, während Sie Variablen ansehen und verändern können, ist ein mächtiges Werkzeug.
!
Tipp
Ich bevorzuge es, den Debugger beim ersten Aufruf eines neuen Programms
aufzurufen. Schrittweise durch ein Programm durchzugehen, schafft ein gutes
Verständnis dafür, was wirklich ausgeführt wird.
Ich war gezwungen, das Programm mehrmals auszuführen, als ich den Debugger verwendet habe.
Das Programm musste ich jedoch nur einmal kompilieren und erzeugen, obwohl ich mehr als einen
Fehler gefunden habe. Das ist ein großer Vorteil, wenn Sie ein großes Programm debuggen, das
einige Minuten für seine Erzeugung benötigt.
Ich habe schon an Projekten mitgearbeitet, bei denen der Computer die ganze
!
Tipp
Nacht damit beschäftigt war, das System neu zu erzeugen. Während das eher
die Ausnahme ist, sind 5 bis 30 Minuten für die Erzeugung realer Applikationen
nicht außergewöhnlich.
Schließlich gibt der Debugger Ihnen Zugriff auf Informationen, die Sie nicht so einfach sehen
könnten, wenn Sie den Zugang der Ausgabeanweisungen wählen. Z.B. ist das Ansehen eines Zei-
gerinhaltes einfach mit einem Debugger. Es ist zwar möglich, doch sehr umständlich, immer wieder
Adresseninformationen auszugeben.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 182
182 Samstagnachmittag
Selbsttest.
1. Was ist der Unterschied zwischen Step Over und Step In? (Siehe »Einzelschritte in eine Funktion
hinein«)
2. Was ist ein Haltepunkt? (Siehe »Verwendung von Haltepunkten«)
3. Was ist eine Watch? (Siehe »Ansehen und Modifizieren von Variablen«)
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 183
Samstagnachmittag –
Zusammenfassung 3
T EIL
1. Definieren Sie eine Studentenklasse, die den Nachnamen des Studenten, den Grad (erster
Grad, zweiter Grad, usw.) und den mittleren Grad speichern kann.
2. Schreiben Sie eine Funktion, um ein Studentenobjekt zu lesen und seine Informationen aus-
zugeben.
3. Geben Sie drei Grade ein, und mitteln Sie diese, bevor Sie das Objekt wieder ausgeben.
Hinweise:
a. Verwenden Sie eine Abbildung von Zahlen auf Grade. Z.B. sei 1 der erste Grad, 2 der
zweite Grad, usw.
4. Wenn die folgenden Variablen im Speicher ohne Zwischenräume angeordnet sind, wie
viele Bytes Speicher beansprucht jede Variable in einem Programm, das mit Visual C++
oder GNU C++ erzeugt wurde:
a. int n1; long l1; double d1;
b. int nArray[20];
6. Beschreiben Sie die Speicheranordnung von double dArray[3]. Nehmen Sie an, dass das
Array bei Adresse 0x100 beginnt.
7. Unter Verwendung des Arrays in Aufgabe 6, beschreiben Sie den Effekt des Folgenden:
double dArray[3];
double* pdPtr = &dArray[1];
*pdPtr = 1.0; // Zuweisung #1
int* pnPtr = (int*)&dArray[2];
*pnPtr = 2; // Zuweisung #2
8. Schreiben Sie eine Funktion LinkableClass* removeHead( ), die das erste Element einer
Liste von LinkableClass-Objekten entfernt, und an den Aufrufenden zurückgibt.
Hinweise:
a. Vergessen Sie nicht, das die Liste bereits leer sein könnte.
c. Wen Sie Schwierigkeiten mit leeren Listen haben, fangen Sie damit an, dass Sie die Liste
als nicht leer annehmen. Nachdem Ihre Funktion fertig ist, versuchen Sie den Spezialfall
einzubauen, dass die Liste leer ist.
10. Schreiben Sie eine Funktion LinkableClass* returnTail( ), die den letzten Eintrag ent-
fernt und an den Aufrufenden zurückgibt.
Hinweis: Erinnern Sie sich daran, dass der pNext-Zeiger des letzten Elementes gleich null
ist.
Hinweise:
a. Versuchen Sie, die Funktion returnPrevious( ) zu verwenden. Sie sollte in der Lage
sein, die meiste Arbeit zu erledigen.
b. Wenn der Vorgängereintrag des letzen Elements null ist, dann hat die Liste nur einen
Eintrag.
C++ Lektion 16 31.01.2001 12:30 Uhr Seite 185
11. Updaten Sie das Programm Concatenate mit der folgenden Zeigerversion von concat-
String, nachdem Sie mittels eines Debuggers (GNU C++ oder Visual C++) den darin ent-
haltenen Fehler entfernt haben:
Samstagabend
Teil 4
Lektion 17.
Objektprogrammierung
Lektion 18.
Aktive Klassen
Lektion 19.
Erhalten der Klassenintegrität
Lektion 20.
Klassenkonstruktoren II
C++ Lektion 17 31.01.2001 12:31 Uhr Seite 187
Objekt-
programmierung 17 Lektion
Checkliste.
✔ Objekte in der realen Welt identifizieren
✔ Objekte in Klassen klassifizieren
✔ Objektorientierter und funktionaler Ansatz im Vergleich
✔ Nachos herstellen
B
eispiele für Objektprogrammierung können im täglichen Leben gefunden
werden. In der Tat sind Objekte überall. Direkt vor mir steht ein Stuhl, ein
Tisch, ein Computer und ein halbgegessenes Brötchen. Objektprogram-
mierung wendet diese Konzepte auf die Welt des Programmierens an.
30 Min.
188 Samstagabend
• Selbst wenn ich ein Mikrowellenentwickler wäre, und alle Dinge über die internen Abläufe kennen
würde, ihre Software eingeschlossen, würde ich nicht darüber nachdenken, wenn ich die Mikro-
welle benutze.
Das sind keine profunden Beobachtungen. Über so vieles können wir nicht gleichzeitig nachden-
ken. Um die Anzahl der Dinge zu reduzieren, mit denen wir uns beschäftigen müssen, arbeiten wir
auf einem bestimmten Detailniveau.
Mit objektorientierten (OO) Begriffen ausgedrückt, wird der Level, auf dem wir arbeiten, als
Abstraktionslevel bezeichnet. Wenn wir mit Nachos arbeiten, betrachte ich meine Mikrowelle als Box.
So lange ich die Mikrowelle über ihr Interface verwende (die Knopfplatte), sollte nichts, was ich tue,
die Mikrowelle dahin bringen, dass sie
1. in einen inkonsistenten Zustand gerät und abstürzt
2. oder, was viel schlimmer wäre, mein Nacho in eine schwarze, brennende Masse verwandelt
3. oder, was am schlimmsten wäre, Feuer fängt und das Haus in Brand setzt.
Nachdem wir die Objekte erfolgreich codiert und getestet haben, können wir uns dem nächsten
Abstraktionslevel zuwenden. Wir können auf dem Level der Nacho-Herstellung denken und nicht
mehr auf Mikrowellenlevel. An dieser Stelle können wir die Anweisungen meines Sohnes leicht in
C++-Code überführen.
Tatsächlich können wir die Leiter weiter hochsteigen. Der nächste Level nach
=
= =
= oben könnte Dinge enthalten wie aufstehen, zur Arbeit gehen, nach Hause
Hinweis kommen, essen, ausruhen und schlafen, wobei der Verzehr von Nachos irgend-
wo in den Bereich von essen und ausruhen gehören würde.
Teil 4 – Samstagabend
Mein Sohn versteht, dass unsere spezielle Mikrowelle ein Beispiel ist für den Typ der Dinge, die als
Mikrowellen bezeichnet werden. Er versteht auch, dass die Mikrowelle ein spezieller Typ Ofen ist,
und dieser wiederum ein spezielles Küchengerät ist.
Lektion 17
Technisch gesagt, ist meine Mikrowelle eine Instanz der Klasse Mikrowelle. Die Klasse Mikrowelle
ist eine Unterklasse der Klasse Ofen, und die Klasse Ofen ist eine Superklasse der Klasse Mikrowelle.
Menschen klassifizieren. Alles in unserer Welt ist in Klassen eingeteilt. Wir tun das, um die Anzahl
der Dinge, die wir uns merken müssen, klein zu halten. Erinnern Sie sich z.B. daran, wann Sie zum
ersten Mal den Ford-basierten Jaguar (oder den neuen Neon) gesehen haben. Die Werbung nannte
den Jaguar »revolutionär, eine neue Art Auto«. Aber Sie und ich wissen, dass das nicht stimmt. Ich
mag das Aussehen des Jaguars – ich mag es sogar sehr – aber es ist nur ein Auto. Als solches teilt es
alle (oder zumindest die meisten) Eigenschaften mit anderen Autos. Er hat ein Lenkrad, Sitze, einen
Motor, Bremsen, usw. Ich wette, ich könnte sogar einen Jaguar ohne Hilfe fahren.
Ich will den wenigen Platz, den ich in diesem Buch zur Verfügung habe, nicht mit all den Dingen
verschwenden, die ein Jaguar mit anderen Autos gleich hat. Ich muss nur an den Satz »ein Jaguar ist
ein Auto, das ...« denken und die wenigen Dinge, die einzigartig für Jaguar sind. Autos sind eine
Unterklasse von bereiften Fahrzeugen, von denen es auch andere gibt, wie z.B. LKW und Pickups.
Vielleicht sind bereifte Fahrzeuge eine Unterklasse von Fahrzeuges, die auch Boote und Flugzeuge
einschließt. Das lässt sich beliebig fortsetzen.
190 Samstagabend
den ganzen Unsinn z.B. zum Auftauen, sparen. Die Mikrowelle könnte winzig sein. Sie müsste nur
einen kleinen Teller aufnehmen können. Nur sehr wenig Platz würde so für Nachos verschwendet.
In diesem Sinne, lassen Sie uns das Konzept »Mikrowellenofen« vergessen, Alles, was wir wirklich
brauchen, sind die Interna eines Ofens. In einem Rezept fassen wir dann alle Instruktionen zusam-
men, um ihn zum Laufen zu bringen: »Legen Sie die Nachos in die Box. Verbinden Sie das rote und
das schwarze Kabel. Sie hören ein leichtes Summen«. So etwas in der Art.
Wie dem auch sei, der funktionale Ansatz hat einige Probleme:
• Zu komplex. Wir wollen die Details des Ofenbauens nicht vermischen mit den Details der Nacho-
zubereitung. Wenn wir die Details nicht finden und aus der Fülle der Details herausziehen können,
um sie separat zu bearbeiten, müssen wir immer mit der Gesamtkomplexität des Problems arbei-
ten.
• Nicht flexibel. Wenn wir den Mikrowellenofen durch einen anderen Typ Ofen ersetzen müssen,
könnten wir das tun, solange der neue Ofen das gleiche Interface wie der alte Ofen hat. Ohne ein
klar definiertes Interface wird es unmöglich, einen Objekttyp sauber zu entfernen und durch einen
anderen zu ersetzen.
• Nicht wiederverwendbar. Öfen werden verwendet, um verschiedene Gerichte zuzubereiten. Wir
müssen nicht jedes Mal einen neuen Ofen kreieren, wenn wir ein neues Rezept haben. Wenn ein
Problem gelöst ist, wäre es schön, die Lösung auch in zukünftigen Programmen wiederverwenden
zu können.
Es kostet mehr, ein generisches Objekt zu schreiben. Es wäre billiger, eine Mikro-
welle nur für Nachos zu bauen. Wir könnten auf die teure Zeituhr und Knöpfe
=
= =
= verzichten, die für Nachos nicht benötigt werden. Nachdem wir ein generisches
Hinweis Objekt in mehr als einer Anwendung verwendet haben, kommt der Vorteil einer
etwas teureren generischen Klasse gegenüber der wiederholten Entwicklung bil-
ligerer Klassen für jede neue Applikation zum Tragen.
=
= =
= Paradigma ist ein anderes Wort für Programmiermodell.
Hinweis
Viele der Features von C++, die in den folgende Kapiteln beschrieben werden, behandeln die
Fähigkeit von Klassen, sich selber gegen fehlerhafte Programme zu schützen, die nur auf einen
Absturz warten.
Zusammenfassung.
In dieser Sitzung haben Sie die fundamentalen Konzepte der objektorientierten
Programmierung gesehen.
0 Min.
• Objektorientierte Programme bestehen aus locker verbundenen Objekten.
• Jede Klasse repräsentiert ein Konzept der realen Welt.
• Objektorientierte Klassen werden so geschrieben, dass sie in gewisser Hinsicht unabhängig sind
Teil 4 – Samstagabend
von dem Programm, das sie benutzt.
• Objektorientierte Klassen sind verantwortlich für ihr eigenes Wohlbefinden.
Lektion 17
Selbsttest.
1. Was bedeutet der Begriff Abstraktionslevel? (Siehe »Abstraktion und Mikrowellen«)
2. Was ist mit Klassifizierung gemeint? (Siehe »Klassifizierung und Mikrowellen«)
3. Was sind die drei fundamentalen Probleme des funktionalen Programmiermodells, die vom objek-
torientierten Programmiermodell zu lösen versucht werden? (Siehe »Warum solche Objekte bil-
den?«)
4. Wie stellt man Nachos her? (Siehe »Siehe »Abstraktion und Mikrowellen«)
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 192
Checkliste.
✔ Klassen durch Elementfunktionen in aktive Agenten verwandeln
✔ Elementfunktionen mit Namen versehen
✔ Elementfunktionen innerhalb und außerhalb der Klasse definieren
✔ Elementfunktionen aufrufen
✔ Klassendefinitionen in #include-Dateien sammeln
✔ Auf this zugreifen
O
bjekte in der realen Welt sind unabhängige Agenten (abgesehen von ihrer
Abhängigkeit von Strom, Luft usw.). Eine Klasse sollte so unabhängig wie
möglich sein. Es ist für eine struct-Struktur unmöglich, von ihrer Umgebung
unabhängig zu sein. Die Funktionen, die ihre Daten manipulieren, müssen außer-
halb der Struktur definiert sein. Aktive Klassen haben die Fähigkeit, diese Manipula-
30 Min.
tionsfunktionen in sich selbst zu bündeln.
18.1 Klassenrückblick.
Ein Klasse ermöglicht die Gruppierung von verwandten Datenelement in einer Einheit. Z.B. könnten
wir eine Klasse Student wie folgt definieren:
class Student
{
public:
int nSemesterHours; // Semesterstunden bis
// zur Graduierung
float dAverage; // mittlerer Grad
};
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 193
void fn(void)
{
Student s1;
Student s2;
s1.nSemesterHours = 1; // ist nicht gleich ...
s2.nSemesterHours = 2; // ... mit diesem
}
Die beiden nSemesterHours sind verschieden, weil sie zu verschiedenen Studenten gehören (s1
und s2).
Eine Klasse mit nichts als Datenelementen wird auch als Struktur bezeichnet und wird über das
Schlüsselwort struct definiert, das von der Programmiersprache C herkommt. Eine struct ist iden-
tisch mit einer Klasse, außer dass das Schlüsselwort public nicht benötigt wird.
Es ist möglich, Zeiger auf Klassenobjekte zu definieren, und diese Objekte vom Heap zu allozie-
ren, wie das folgende Beispiel zeigt:
void fn()
{
Student* pS1 = new Student;
pS1->nSemesterHours = 1;
Teil 4 – Samstagabend
}
Lektion 18
void studentFunc(Student s);
void studentFunc(Student* pS);
void fn()
{
Student s1 = {12, 4.0};
Student* pS = new Student;
194 Samstagabend
dieser Klasse ist, dass sie nur passive Eigenschaften von Studenten enthält. D.h., ein Student hat eine
Eigenschaft für die Anzahl der Semester und seinen mittleren Grad. (Ein Student hat auch einen
Namen, eine Sozialversicherungsnummer usw., aber es ist in Ordnung, diese Eigenschaften wegzu-
lassen, wenn sie nicht zu dem Problem gehören, das wir lösen möchten.) Studenten beginnen Vor-
lesungen, brechen Vorlesungen ab und beenden Vorlesungen erfolgreich. Das sind aktive Eigen-
schaften der Klasse.
Diese Lösung funktioniert – in der Tat ist das die Lösung in nicht objektorientierten Programmier-
sprachen wie C. Es gibt jedoch ein Problem mit dieser Lösung.
In der Art und Weise, in der dieser Auszug geschrieben ist, hat Student nur passive Eigenschaften.
Es gibt da ein nebulöses »Ding«, das aktive Agenten hat, wie startCourse( ), das auf Objekten aus
der Klasse Student arbeitet (und Objekten aus der Klasse Course). Dieses nebulöse Ding hat keine
eigenen Dateneigenschaften. Obwohl sie funktioniert, spiegelt diese Beschreibung nicht die Realität
wider.
Wir würden gerne die aktiven Eigenschaften aus diesem undefinierten Ding herausnehmen und
sie der Klasse Student selber hinzufügen, so dass jede Instanz der Klasse Student mit einem kom-
pletten Satz von Eigenschaften ausgerüstet ist.
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 195
=
= =
=
Das Hinzufügen aktiver Eigenschaften zu einer Klasse, statt diese aus der Klasse
herauszulassen, scheint Ihnen vielleicht nebensächlich zu sein, es wird aber im
Hinweis weiteren Verlauf dieses Buches immer wichtiger.
20 Min.
class Student
{
public:
// die passiven Eigenschaften der Klasse
int nSemesterHours; // Semesterstunden bis
// zur Graduierung
float dAverage; // mittlerer Grad
Teil 4 – Samstagabend
// die aktiven Eigenschaften der Klasse
float startCourse(Course*);
void dropCourse(int nCourseNumber);
Lektion 18
void completeCourse(int nCourseNumber);
};
Die Funktion startCourse(Course*) ist eine Eigenschaft der Klasse, wie nSemesterHours und
dAverage.
Eine Funktion, die Element einer Klasse ist, wird Elementfunktion genannt. Aus
=
= =
=
historischen Gründen, die nur sehr wenig mit C++ zu tun haben, werden Ele-
mentfunktionen auch Methoden genannt. Wahrscheinlich weil der Begriff
Hinweis
»Methode« der verwirrendere der beiden Begriffe ist, wird er am häufigsten ver-
wendet.
=
= =
=
Es gibt keinen Namen für Funktionen oder Daten, die nicht Element einer Klasse
sind. Ich bezeichne sie als Nichtelemente. Alle Funktionen, die sie bisher gese-
Hinweis hen haben, sind Nichtelemente gewesen, weil sie nicht zu einer Klasse gehören.
!
Tipp
einer Klasse. Die Datenelemente können vor oder nach den Elementfunktionen
stehen, sie können auch miteinander vermischt werden. Ich selber bevorzuge es,
die Datenelemente vor die Elementfunktionen zu platzieren.
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 196
196 Samstagabend
=
= =
=
Tatsächlich ist der vollständige Name der Nichtelementfunktion addCourse( )
gleich ::addCourse( ). Die beiden Doppelpunkte :: ohne Klassennamen auf
Hinweis ihrer linken Seite weisen ausdrücklich darauf hin, dass es sich um eine Nichtele-
mentfunktion handelt.
Datenelemente verhalten sich nicht anders als Elementfunktionen in Bezug auf erweiterte
Namen. Außerhalb einer Struktur ist es nicht ausreichend, nur nSemesterHours zu verwenden. Das
Datenelement nSemesterHours macht nur im Kontext der Klasse Student Sinn. Der erweiterte
Name von nSemesterHours ist Student::nSemesterHours.
Der Operator :: wird auch Bereichsauflösungsoperator genannt, weil er die Klasse identifiziert, zu
der ein Element gehört. Der Operator :: kann mit einer Nichtelementfunktion und mit einem leeren
Strukturnamen aufgerufen werden. Die Nichtelementfunktion startCourse( ) sollte als
::startCourse( ) angesprochen werden.
Der Operator ist optional, außer wenn es zwei Funktionen mit gleichem Namen gibt. Hier ein
Beispiel dazu:
float startCourse(Course*);
class Student
{
public:
int nSemesterHours; // Semesterstunden bis zur
// Graduierung
float dAverage; // mittlerer Grad
Hinzufügen des Operators :: an den Anfang des Aufrufs leitet den Aufruf wie gewünscht an die glo-
bale Funktion weiter:
class Student
{
public:
int nSemesterHours; // Semesterstunden bis zur
// Graduierung
float dAverage; // mittlerer Grad
Der vollständig erweiterte Name einer Nichtelementfunktion enthält also nicht nur die Argu-
mente, wie wir in Sitzung 9 gesehen haben, sondern auch den Namen der Klasse, zu der die Funk-
tion gehört – der ist leer für Nichtelementfunktionen.
Teil 4 – Samstagabend
18.4 Definition einer Elementfunktion einer Klasse.
Eine Elementfunktion kann entweder in der Klasse oder separat definiert werden. Betrachten Sie die
folgende Definition der Methode addCourse(int, float) innerhalb der Klasse:
Lektion 18
class Student
{
public:
int nSemesterHours; // Semesterstunden bis zur
// Graduierung
float dAverage; // mittlerer Grad
Der Code von addCourse(int, float) sieht nicht anders aus als der jeder anderen Funktion,
außer dass er eingebettet ist in die Klasse.
Elementfunktionen, die innerhalb der Klasse definiert sind, werden standardmäßig als Inline-
Funktionen behandelt (s.u.). Hauptsächlich ist dies der Fall, weil Elementfunktionen, die in der Klas-
se definiert sind, sehr kurz sind, und kurze Funktionen die primären Kandidaten für eine Behandlung
als Inline-Funktion sind.
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 198
198 Samstagabend
Inline-Funktionen
Normalerweise veranlasst eine Funktionsdefinition den C++-Compiler, Maschinencode an einer
bestimmten Stelle des ausführbaren Programms zu platzieren. Jedes Mal, wenn die Funktion auf-
gerufen wird, fügt C++ eine Art Sprung an die Stelle ein, an der die Funktion gespeichert ist. Wenn
die Funktion durchlaufen wurde, geht die Kontrolle zurück zu dem Punk, von wo aus die Funktion
aufgerufen wurde.
C+ definiert einen speziellen Typ Funktion, der als Inline-Funktion bezeichnet wird. Wenn eine Inli-
ne-Funktion aufgerufen wird, generiert der C++-Compiler den Code direkt an dieser Stelle. Jeder
Aufruf der Inline-Funktion erhält seine eigene Kopie des Maschinencodes.
Inline-Funktionen werden schneller ausgeführt, weil der Computer nicht an eine andere Stelle
springen und Initialisierungen ausführen muss, um die Verarbeitung fortzufahren. Denken Sie
jedoch daran, dass Inline-Funktionen mehr Speicher benötige. Wenn eine Inline-Funktion zehnmal
aufgerufen wird, befinden sich 10 Kopien des Maschinencodes im Speicher.
Weil der Unterschied zwischen Inline-Funktionen und konventionellen Funktionen, die manchmal
auch als Outline-Funktionen bezeichnet werden, in der Ausführungsgeschwindigkeit klein ist, sind
nur kleine Funktionen Kandidaten für eine Inline-Behandlung. Zusätzlich zwingen bestimmte Kon-
struktionen eine Inline-Funktion zu einer Behandlung als Outline-Funktion.
class Student
{
public:
int nSemesterHours; // Semesterstunden bis zur
// Graduierung
float dAverage; // mittlerer Grad
Hier sehen wir, dass die Klassendefinition nichts weiter als eine Prototypdeklaration der Funktion
addCourse( ) enthält. Die tatsächliche Funktionsdefinition steht separat.
=
= =
=
Eine Deklaration definiert den Typ einer Sache. Eine Definition definiert den
Inhalt einer Sache.
Hinweis
Die Analogie mit einer Prototypdeklaration ist exakt. Die Deklaration in der Struktur ist eine Pro-
totypdeklaration und ist, wie alle Prototypdeklarationen, notwendig.
Als die Funktion innerhalb der Klasse Student definiert wurde, war es nicht nötig, dass der Klas-
senname im Namen der Funktion enthalten ist; es wurde der Name der enthaltenden Klasse ange-
nommen. Wenn die Funktion alleine steht, wird der Klassenname benötigt. Es ist wie bei mir zu Hau-
se. Meine Frau ruft mich nur bei meinem Vornamen (vorausgesetzt ich bin nicht in der Hundehütte).
Innerhalb der Familie wird der Nachname angenommen. Außerhalb der Familie (und meinem Be-
kanntenkreis) rufen mich andere mit meinem vollen Namen.
18.5.1 Include-Dateien
Es ist üblich, Klassendefinitionen und Funktionsprototypen in eine Datei zu schreiben, die die
Teil 4 – Samstagabend
Endung .h trägt, getrennt von der .cpp-Datei, die die tatsächlichen Funktionsdefinitionen enthält.
Die .h-Datei wird dann in der .cpp-Datei »included« (=eingebunden), wie im Folgenden zu sehen
Lektion 18
ist.
Die Datei student.h wird am besten so definiert:
class Student
{
public:
int nSemesterHours; // Semesterstunden bis zur
// Graduierung
float dAverage; // mittlerer Grad
#include »student.h«;
float Student::addCourse(int hours, float grade)
{
float weighted;
weighted = nSemesterHours * dAverage;
200 Samstagabend
Die Direktive #include besagt »ersetze diese Direktive durch den Inhalt der Datei student.h«.
=
= =
=
Die Direktive #include hat nicht das Format einer C++-Anweisung, weil es von
einem separaten Interpreter verarbeitet wird, der vor dem C++-Compiler ausge-
Hinweis führt wird.
10 Min.
#include »student.h«
Student s;
void fn(void)
{
// Zugriff auf Datenelemente von s
s.nSemesterHours = 10;
s.dAverage = 3.0;
}
Wir müssen ein Objekt zusammen mit dem Elementnamen spezifizieren, wenn wir ein Objektele-
ment ansprechen wollen. In anderen Worten, das Folgende macht keinen Sinn:
#include »student.h«
void fn(void)
{
Student s;
void fn()
{
Student s;
Eine Elementfunktion ohne ein Objekt aufzurufen, macht nicht mehr Sinn, als ein Datenelement
ohne ein Objekt anzusprechen. Die Syntax für den Aufruf einer Elementfunktion sieht aus wie eine
Kreuzung der Syntax für Zugriffe auf Datenelemente und dem Aufruf konventioneller Funktionen.
Teil 4 – Samstagabend
Die gleiche Parallele wie für die Objekte selber kann auch für Zeiger auf Objekte gezogen werden.
Das Folgende referenziert ein Datenelement eines Objektes über einen Zeiger:
Lektion 18
#include »student.h«
int main()
{
Student s;
someFn(&s);
return 0;
}
Eine Elementfunktion mit einer Referenz auf ein Objekt aufzurufen, erscheint identisch mit der
Benutzung des Objektes selber. Erinnern Sie sich daran, dass bei der Übergabe oder Rückgabe einer
Referenz als Argument einer Funktion C++ nur die Adresse des Objektes übergibt. Bei Verwendung
einer Referenz dereferenziert C++ die Adresse automatisch, wie das folgende Beispiel zeigt:
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 202
202 Samstagabend
#include »student.h«
Student s;
int main()
{
someFn(s);
return 0;
}
#include »student.h«
float Student::addCourse(int hours, float grade)
{
float weighted;
weighted = nSemesterHours * dAverage;
Wenn addCourse( ) mit dem Objekt s aufgerufen wird, beziehen sich alle anderen unqualifizier-
ten Elemente innerhalb von addCourse( ) auf s. Somit wird nSemesterHours zu s.nSemes-
terHours, dAverage wird zu s.dAverage. Im Aufruf t.addCourse( ) in der nächsten Zeile bezie-
hen sich die gleichen Referenzen auf t.nSemesterHours und t.dAverage.
Das Objekt, mit dem die Elementfunktion aufgerufen wird, ist das »aktuelle« Objekt, und alle
unqualifizierten Referenzen auf Klassenelemente beziehen sich auf dieses Objekt. Oder anders
gesagt, beziehen sich unqualifizierte Referenzen auf Klassenelemente auf das aktuelle Objekt.
Wie wissen Elementfunktionen, was das aktuelle Objekt ist? Es ist keine Magie – die Adresse des
Objektes wird an die Elementfunktion übergeben als implizites und nicht sichtbares erstes Argu-
ment. In anderen Worten findet die folgende Konvertierung statt:
s.addCourse(3, 2.5)
wird zu
Student::addCourse(&s, 3, 2.5);
(Sie können diese interpretative Syntax so nicht verwenden; sie ist nur ein Weg, um zu verstehen,
was C++ tut.)
Innerhalb der Funktion hat dieser implizite Zeiger auf das aktuelle Objekt einen Namen, für den
Fall, dass Sie darauf zugreifen müssen. Dieser versteckte Objektzeiger heißt this (=dieses). Der Typ
Teil 4 – Samstagabend
von this ist immer ein Zeiger auf ein Objekt der entsprechenden Klasse. Innerhalb der Klasse Stu-
dent ist this vom Typ Student*.
Jedes Mal, wenn eine Elementfunktion auf ein anderes Element der gleichen Klasse verweist,
Lektion 18
ohne explizit ein Objekt zu benennen, nimmt C++ an, dass this gemeint ist. Sie können this auch
explizit verwenden. Wir hätten Student::addCourse( ) auch so schreiben können:
#include »student.h«
float Student::addCourse(int hours, float grade)
{
float weighted;
Ob wir this explizit hinschreiben oder es implizit lassen, wie wir es bereits getan haben, hat den
gleichen Effekt.
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 204
204 Samstagabend
class Student
{
public:
// grade – aktueller mittlerer Grad
float grade();
class Slope
{
public:
// grade – Grad des Gefälles
float grade();
Jeder Aufruf von main( ) aus ist im Kommentar mit dem erweiterten Namen der aufgerufenen
Funktion versehen.
Wenn überladene Funktionen aufgerufen werden, werden sowohl die Argumente der Funktion,
und der Typ des Objektes (falls vorhanden), mit dem die Funktion aufgerufen wird, verwendet, um
den Aufruf unzweideutig zu machen.
Der Begriff unzweideutig ist ein objektorientierter Begriff, der sagen soll »entscheide zur Compi-
lezeit, welche überladene Funktion aufgerufen werden soll«. Wir können auch sagen, dass die Auf-
rufe aufgelöst werden.
C++ Lektion 18 31.01.2001 12:34 Uhr Seite 205
Zusammenfassung.
Je näher Sie das Problem, das Sie mit C++ lösen wollen, modellieren können, desto leichter kann das
Problem gelöst werden. Diejenigen Klassen, die nur Datenelemente enthalten, können nur passive
Eigenschaften von Objekten modellieren. Das Hinzufügen von Elementfunktionen macht die Klasse
mehr zu einem Objekt wie in der realen Welt, in dem Sinne, dass es nun auf die Welt »außen«, d.h.
den Rest des Programms, reagieren kann. Außerdem kann die Klasse für ihr eigenes Wohlergehen
verantwortlich gemacht werden, in der gleichen Weise, wie Objekte in der realen Welt sich selbst
schützen.
• Elemente einer Klasse können Funktionen oder Daten sein. Solche Elementfunktionen machen
eine Klasse aktiv. Der vollständige Name einer Elementfunktion schließt den Namen der Klasse ein.
• Elementfunktionen können entweder innerhalb oder außerhalb der Klasse definiert werden. Ele-
Teil 4 – Samstagabend
mentfunktionen, die außerhalb einer Klasse definiert sind, sind der Klasse schwerer zuzuordnen,
tragen aber zu einer besseren Lesbarkeit der Klasse bei.
• Innerhalb einer Elementfunktion kann das aktuelle Objekt über das Schlüsselwort this referen-
Lektion 18
ziert werden.
Selbsttest.
1. Was ist falsch daran, Funktionen außerhalb einer Klasse zu deklarieren, die Datenelemente der
Klasse direkt manipulieren? (Siehe »Eine funktionale Lösung«)
2. Eine Funktion, die ein Element einer Klasse ist, wird wie bezeichnet? Es gibt zwei Antworten auf
diese Frage. (Siehe »Definition einer aktiven Klasse«)
3. Beschreiben Sie die Bedeutung der Reihenfolge von Funktionen innerhalb einer Klasse. (Siehe
»Definition einer aktiven Klasse«)
4. Wenn eine Klasse X ein Element Y(int) besitzt, was ist der vollständige Name der Funktion?
(Siehe »Schreiben von Elementfunktionen außerhalb einer Klasse«)
5. Warum wird eine Include-Datei so genannt? (Siehe »Include-Dateien«)
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 206
Checkliste.
✔ Einen Konstruktor schreiben und benutzen
✔ Datenelemente konstruieren
✔ Einen Destruktor schreiben und benutzen
✔ Zugriff auf Datenelemente kontrollieren
E
in Objekt kann nicht für sein Wohlergehen verantwortlich gemacht werden,
wenn es keine Kontrolle darüber hat, wie es erzeugt und verwendet wird. In
dieser Sitzung untersuchen wir die Möglichkeiten in C++, die Integrität von
Objekten zu erhalten.
30 Min.
class Student
{
public:
int nSemesterHours;
float dAverage;
};
void fn()
{
Student s = {0, 0};
//... Fortsetzung der Funktion ...
}
Wir könnten die Klasse mit einer Initialisierungsfunktion ausstatten, die von der Anwendung auf-
gerufen wird, sobald ein Objekt erzeugt wird. Das gibt der Klasse die Kontrolle darüber, wie ihre
Datenelemente initialisiert werden. Die Lösung sieht dann so aus:
class Student
{
public:
// data members
int nSemesterHours;
float dAverage;
// Elementfunktionen
// init – initialisiere ein Objekt mit einem
// gültigen Zustand
void init()
{
semesterHours = 0;
dAverage = 0.0;
}
};
void fn()
{
Teil 4 – Samstagabend
// erzeuge gültiges Student-Objekt
Student s; // erzeuge das Objekt...
s.init(); // ... und initialisiere es
Lektion 19
//... Fortsetzung der Funktion ...
}
Das Problem mit der »init«-Lösung ist, dass sich die Klasse auf die Anwendung verlassen muss,
dass die Funktion init( ) aufgerufen wird. Das ist nicht die angestrebte Lösung. Was wir eigentlich
wollen, ist ein Mechanismus, der ein Objekt automatisch initialisiert, wenn es erzeugt wird.
=
= =
=
Ein Konstruktor ist eine Elementfunktion, die automatisch aufgerufen wird,
wenn ein Objekt erzeugt wird. In gleicher Weise wird ein Destruktor aufgerufen,
Hinweis
wenn ein Objekt vernichtet wird.
C++ führt einen Aufruf eines Konstruktors immer aus, wenn ein Objekt erzeugt wird. Der Kon-
struktor trägt den gleichen Namen wie die Klasse. Auf diese Weise weiß der Compiler, welche Ele-
mentfunktion ein Konstruktor ist.
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 208
208 Samstagabend
Die Entwickler von C++ hätten auch eine andere Regel aufstellen können, z.B.
=
= =
=
»Der Konstruktor muss init( ) heißen.« Die Programmiersprache Java verwen-
det eine solche Regel. Eine andere Regel würde keinen Unterschied machen,
Hinweis
solange wie der Compiler den Konstruktor von anderen Elementfunktionen
unterscheiden kann.
Mit einem Konstruktor sieht die Klasse Student wie folgt aus:
class Student
{
public:
// Datenelemente
int nSemesterHours;
float dAverage;
// Elementfunktionen
Student()
{
nSemesterHours = 0;
dAverage = 0.0;
}
};
void fn()
{
Student s; // erzeuge und initialisiere Objekt
// ... Fortsetzung der Funktion ...
}
An der Stelle, an der s deklariert wird, fügt der Compiler einen Aufruf des Konstruktors Stu-
dent::Student( ) ein.
Dieser einfache Konstruktor wurde als Inline-Elementfunktion geschrieben. Konstruktoren kön-
nen auch als Outline-Funktionen geschrieben werden, z.B.:
class Student
{
public:
// Datenelemente
int nSemesterHours;
float dAverage;
// Elementfunktionen
Student();
};
Student::Student()
{
nSemesterHours = 0;
dAverage = 0.0;
}
int main(int nArgc, char* pszArgs)
{
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 209
Ich habe eine kleine Funktion main( ) hinzugefügt, damit Sie das Programm ausführen können.
Sie sollten mit Ihrem Debugger in Einzelschritten durch das Programm gehen, bevor sie hier fortfah-
ren.
Wenn Sie in Einzelschritten durch dieses Beispielprogramm geben, kommt die Kontrolle schließ-
lich zur Deklaration Student s;. Führen Sie Step In einmal aus, und die Kontrolle springt magi-
scherweise nach Student::Student( ). Gehen Sie in Einzelschritten durch den Konstruktor. Wenn
die Funktion fertig ist, kehrt die Kontrolle zur Anweisung nach der Deklaration zurück.
Mehrere Objekte können in einer Zeile deklariert werden. Gehen Sie nochmals in Einzelschritten
durch die Funktion main( ), die wie folgt deklariert ist:
int main(int nArgc, char* pszArgs)
{
Student s[5]; // erzeuge Array von Objekten
}
Der Konstruktor wird fünfmal aufgerufen, einmal für jedes Element des Array.
Teil 4 – Samstagabend
Wenn Sie den Debugger nicht zum Laufen bringen (oder sich damit einfach
Lektion 19
Tipp aufgerufen wird. Der Effekt ist nicht so dramatisch, aber überzeugend.
void fn()
{
Student s; // erzeuge und initialisiere Objekt
// ... weitere Anweisungen ...
s.Student(); // initialisiere Objekt erneut –
// das funktioniert nicht
}
Der Konstruktor hat keinen Rückgabewert, noch nicht einmal void. Die Konstruktoren, die Sie
hier sehen, haben alle eine void-Argumentliste.
=
= =
= Der Konstruktor ohne Argumente wird auch als Defaultkonstruktor bezeichnet.
Hinweis
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 210
210 Samstagabend
Der Konstruktor kann andere Funktionen aufrufen. Wenn wir ein Objekt neu initialisieren wollen,
schreiben wir das Folgende:
class Student
{
public:
// Datenelemente
int nSemesterHours;
float dAverage;
// Elementfunktionen
// Konstruktor – initialisiert das Objekt bei
// Erzeugung automatisch
Student()
{
init();
}
void fn()
{
Student s; // erzeuge und initialisiere Objekt
Hierbei ruft der Konstruktor eine allgemein verfügbare Funktion init( ) auf, die die Initialisie-
rung durchführt.
class Teacher
{
public:
Teacher()
{
cout << »Konstruktor Teacher\n«;
}
Teil 4 – Samstagabend
};
class TutorPair
Lektion 19
{
public:
Student student;
Teacher teacher;
int noMeetings;
TutorPair()
{
cout << »Konstruktor TutorPair \n«;
noMeetings = 0;
}
};
TutorPair tp;
212 Samstagabend
Bei der Erzeugung von tp in main( ) wird der Konstruktor von TutorPair automatisch aufgeru-
fen. Bevor die Kontrolle an den Body des Konstruktors TutorPair übergeht, werden die Konstruk-
toren der beiden Elementobjekte – student und teacher – aufgerufen.
Der Konstruktor von Student wird zuerst aufgerufen, weil das Element student zuerst deklariert
ist. Dann wird der Konstruktor von Teacher aufgerufen. Nachdem die Objekte erzeugt sind, geht
die Kontrolle auf die öffnende Klammer über, und der Konstruktor von TutorPair darf den Rest des
Objektes initialisieren.
=
= =
=
Es würde nicht gehen, TutorPair für die Initialisierung von student und
teacher verantwortlich zu machen. Jede Klasse ist für die Initialisierung ihrer
Hinweis Objekte selber zuständig.
class Student
{
public:
// Datenelemente
int nSemesterHours;
float dAverage;
// Elementfunktionen
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 213
Teil 4 – Samstagabend
};
Wenn mehr als ein Objekt vernichtet wird, werden die Destruktoren in der umgekehrten Reihen-
Lektion 19
folge der Konstruktoren aufgerufen. Das Gleiche gilt bei der Vernichtung von Objekte, die Klassen-
objekte als Datenelemente enthalten. Listing 19-2 zeigt die Ausgabe des Programms aus Listing
19-1 mit hinzugefügten Destruktoren in den drei Klassen:
.
CD-ROM
Das gesamte Programm ist auf der beiliegenden CD-ROM enthalten.
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 214
214 Samstagabend
Der Konstruktor von TutorPair wird bei der Deklaration von tp aufgerufen. Die Student- und
Teacher-Datenobjekte werden in der Reihenfolge erzeugt, wie sie in TutorPair enthalten sind,
bevor die Kontrolle an den Body von TutorPair( ) übergeben wird. Wenn die schließende Klammer
von main( ) erreicht wird, verlässt tp seinen Gültigkeitsbereich. C++ ruft ~TutorPair( ) auf, um tp
zu vernichten. Nachdem der Destruktor das TutorPair-Objekt vernichtet hat werden die Destruk-
toren ~Student( ) und ~Teacher( ) aufgerufen, um die Datenelemente zu vernichten.
19.2 Zugriffskontrolle.
Ein Objekt mit einem bekannten Zustand zu initialisieren, ist nur die halbe Miete.
Die andere Hälfte besteht darin, sicherzustellen, dass externe Funktionen nicht in
das Objekt »eindringen« können und Unsinn mit seinen Datenelementen anstellen
10 Min. können.
Externen Funktionen den Zugriff auf die Datenelemente zu erlauben, ist das
=
= =
= Gleiche, wie den Zugriff auf die Interna meiner Mikrowelle zu gestatten. Wenn
Hinweis ich die Mikrowelle aufmache, und die Verkabelung ändere, kann ich den Ent-
wickler der Mikrowelle nur schwerlich für die Folgen verantwortlich machen.
=
= =
= Ein Klassenelement ist protected, wenn es nur von anderen Elementen der
Hinweis Klasse angesprochen werden kann.
!
Tipp
Der Gegensatz zu protected ist public. Ein public-Element kann von Element-
und Nichtelementfunktionen angesprochen werden.
Z.B. in der folgenden Version von Student sind nur die Funktionen grade(double, int) und
grade( ) für externe Funktionen zugreifbar.
// Student
class Student
{
protected:
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 215
double dCombinedScore;
int nSemesterHours;
public:
Student()
{
dCombinedScore = 0;
nSemesterHours = 0;
}
Teil 4 – Samstagabend
return grade();
}
Lektion 19
// grade – gib den Grad zurück
double grade()
{
return dCombinedScore / nSemesterHours;
}
return 0;
}
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 216
216 Samstagabend
Diese Version von Student hat zwei Datenelemente. dCombinedScore zeigt die Summe der
gewichteten Grade, während nSemesterHours die Gesamtsumme der Semesterstunden widerspie-
gelt. Die Funktion grade(double, int) führt ein Update für die Summe der gewichteten Grade
und die Anzahl der Semesterstunden aus. Die Funktion grade( ) gibt, ihrem Namen entsprechend,
den derzeitigen Grad zurück, den sie als Verhältnis der mittleren Grade und der Gesamtanzahl der
Semesterstunden berechnet.
grade(double, int) addiert den Effekt eines neuen Kurses zum gesamten
!
Tipp
Grad, während grade(void) den aktuellen Grad zurückgibt. Diese Zweiteilung,
bei der eine Funktion einen Wert updatet, während die andere nur den Wert
zurückgibt, ist häufig anzutreffen.
Eine Funktion grade( ), die den Wert eines Datenelementes zurückgibt, wird als Zugriffsfunktion
bezeichnet, weil sie einen kontrollierten Zugriff auf ein Datenelement bereitstellt.
Obwohl die Funktion grade(double, int) nicht fehlersicher ist, zeigt Sie doch ein wenig, wie
eine Klasse sich selber schützen kann. Die Funktion führt eine Reihe rudimentärer Tests aus, um
sicherzustellen, dass die übergebenen Daten Sinn machen. Die Klasse Student weiß, dass gültige
Grade zwischen 0 und 4 liegen. Außerdem weiß die Klasse, dass die Anzahl der Semesterstunden
eines Kurses zwischen 0 und 5 liegen (die Obergrenze ist meine eigene Erfindung).
Die elementaren Überprüfungen, die von der Methode grade( ) durchgeführt werden, stellen
eine gewisse Integrität der Daten sicher, vorausgesetzt, die Datenelemente sind für externe Funktio-
nen nicht zugreifbar.
=
= =
=
Es gibt noch einen weiteren Kontroll-Level, der als private bezeichnet wird. Der
Unterschied von protected und private wird deutlich, wenn wir in Sitzung 32
Hinweis
die Vererbung diskutieren.
Die Elementfunktion semesterHours( ) tut nicht mehr, als den Wert von nSemesterHours
zurückzugeben.
Eine Funktion, die nichts anderes tut, als externen Funktionen Zugriff auf die Werte von Daten-
elementen zu geben, wird Zugriffsfunktion genannt. Eine Zugriffsfunktion erlaubt es Nichtelement-
funktionen, den Wert eines Datenelementes zu lesen, ohne es verändern zu können.
Eine Funktion, die Zugriff auf die protected-Elemente einer Klasse hat, wird als vertrauenswürdige
Funktion bezeichnet. Alle Elementfunktionen sind vertrauenswürdig. Auch Nichtelementfunktionen
können als vertrauenswürdig bezeichnet werden durch die Verwendung des Schlüsselwortes
friend (Freund). Eine Funktion, die als Freund einer Klasse bezeichnet ist, ist vertrauenswürdig. Alle
Elementfunktionen einer friend-Klasse sind Freunde. Der richtige Gebrauch von friend geht über
die Zielsetzung dieses Buches hinaus.
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 217
class LinkedList
{
protected:
// deklariere pHead als Element der Klasse,
// aber gleich für alle Objekte
static LinkedList* pHead;
Teil 4 – Samstagabend
LinkedList* pNext;
Lektion 19
void addHead()
{
// verkette den aktuellen Eintrag und
// den Kopf der Liste
pNext = pHead;
218 Samstagabend
Die statische Deklaration in der Klasse macht pHead zu einem Element, alloziert aber keinen Spei-
cher dafür. Dies muss außerhalb der Klasse geschehen, wie oben zu sehen ist.
Die gleiche Funktion addHead( ) greift auf pHead zu, wie sie auf jedes andere Datenelement
zugreifen würde. Zuerst lässt sie den pNext-Zeiger des aktuellen Objekts auf den Anfang der Liste
zeigen – der Eintrag, auf den pHead zeigt. Danach verändert es diesen Zeiger, auf den aktuellen Ein-
trag zu zeigen.
Erinnern Sie sich daran, dass die Adresse des aktuellen Eintrags über das Schlüsselwort this ange-
sprochen werden kann.
!
Tipp
So einfach addHead( ) ist, untersuchen Sie die Funktion dennoch genau: Alle
Objekte der Klasse LinkedList haben das gleiche Element pHead, jedes Objekt
hat jedoch seinen eigenen pNext-Zeiger.
=
= =
= Es ist auch möglich, eine Elementfunktion statisch zu deklarieren; in diesem
Hinweis Buch verwenden wir jedoch keine solchen Funktionen.
Zusammenfassung.
Der Konstruktor ist eine spezielle Elementfunktion, die C++ automatisch aufruft, wenn ein Objekt
erzeugt wird, ob nun eine lokale Variable ihren Gültigkeitsbereich betritt oder ob ein Objekt vom
Heap alloziert wird. Der Konstruktor ist verantwortlich dafür, die Datenelemente mit einem gültigen
Zustand zu initialisieren. Die Datenelemente einer Klasse werden automatisch erzeugt, bevor der
Konstruktor der Klasse aufgerufen wird. C++ ruft eine spezielle Funktion auf, wenn ein Objekt ver-
nichtet wird, die als Destruktor bezeichnet wird.
• Der Klassenkonstruktor gibt der Klasse die Kontrolle darüber, wie Objekte erzeugt werden. Das
verhindert, dass Objekte ihr Leben in einem illegalen Zustand beginnen. Konstruktoren werden
wie die anderen Elementfunktionen deklariert, außer dass sie den Namen der Klasse tragen und
keinen Rückgabetyp besitzen (nicht einmal void).
• Der Klassendestruktor gibt der Klasse die Chance, Ressourcen, die von einem Konstruktor belegt
wurden, wieder freizugeben. Die häufigste Ressource ist Speicher.
• Eine Funktion protected zu deklarieren, macht sie nicht zugreifbar für nicht vertrauenswürdige
Funktionen. Elementfunktionen werden automatisch als vertrauenwürdig angesehen.
C++ Lektion 19 31.01.2001 12:41 Uhr Seite 219
Selbsttest.
1. Was ist ein Konstruktor? (Siehe »Der Konstruktor«)
2. Was ist falsch daran, eine Funktion init( ) zur Initialisierung eines Objektes aufzurufen, wenn es
angelegt wird? (Siehe »Der Konstruktor«)
3. Was ist der vollständige Name eines Konstruktors der Klasse Teacher? Was ist der Rückgabetyp?
(Siehe »Der Konstruktor«)
4. In welcher Reihenfolge werden Datenelemente eines Objektes angelegt? (Siehe »Konstruktion
von Datenelementen«)
5. Was ist der vollständige Name des Destruktors der Klasse Teacher? (Siehe »Der Destruktor«)
6. Was ist die Bedeutung des Schlüsselwortes static, wenn es in Verbindung mit Datenelementen
verwendet wird? (Siehe »Statische Datenelemente«)
Teil 4 – Samstagabend
Lektion 19
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 220
20 Lektion Klassenkonstruktoren II
Checkliste.
✔ Konstruktoren mit Argumenten erzeugen
✔ Argumente an die Konstruktoren von Datenelementen übergeben
✔ Konstruktionsreihenfolge der Datenelemente bestimmen
✔ Spezielle Eigenschaften des Kopierkonstruktors bestimmen
E
in sehr einfacher Konstruktor arbeitet gut in der einfachen Klasse Student in
Sitzung 19. Ohne dass die Klasse Student sehr komplex wird, treten die
Begrenzungen des Konstruktors nicht zu Tage. Bedenken Sie z.B., dass ein Stu-
dent einen Namen und eine Sozialversicherungsnummer hat. Der Konstruktor aus
Sitzung 19 hat keine Argumente, so hat er keine andere Wahl, als das Objekt »leer«
30 Min.
zu initialisieren. Der Konstruktor soll ein gültiges Objekt erzeugen – ein namenloser
Student ohne Sozialversicherungsnummer stellt sicherlich keinen gültigen Studenten dar.
// Student
class Student
{
public:
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 221
~Student()
{
delete pszName;
pszName = 0;
}
protected:
Teil 4 – Samstagabend
char* pszName;
int nID;
};
Lektion 20
void fn()
{
// erzeuge einen lokalen Studenten
Student s1(»Stephen Davis«, 12345);
Der Konstruktor Student(char*, int) beginnt mit der Allozierung eines Speicherblocks vom
Heap, um eine Kopie der übergebenen Zeichenkette zu speichern. Die Funktion strlen( ) gibt die
Anzahl der Bytes in der Zeichenkette zurück; weil strlen( ) das Nullzeichen nicht mitzählt, müssen
wir 1 addieren, um die Anzahl Bytes zu erhalten, die wir vom Heap benötigen. Der Konstruktor Stu-
dent( ) kopiert die übergebene Zeichenkette in diesen Speicherblock. Schließlich weist der Kon-
struktor den Studenten die ID zu.
Die Funktion fn( ) erzeugt zwei Student-Objekte, eins lokal in der Funktion und eins vom Heap.
In beiden Fällen übergibt fn( ) den Namen und die ID an das Student-Objekt, das erzeugt wird.
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 222
222 Samstagabend
Konstruktoren können überladen werden, wie Funktionen mit Argumenten überladen werden
können.
=
= =
=
Erinnern Sie sich, dass das Überladen einer Funktion bedeutet, dass Sie zwei
Funktionen mit dem gleichen Namen haben, die sich durch ihre unterschied-
Hinweis
lichen Argumenttypen unterscheiden.
C++ wählt den entsprechenden Konstruktor, basierend auf den Argumenten in der Deklaration.
Z.B. kann die Klasse Student gleichzeitig drei Konstruktoren haben, wie im folgenden Schnipsel:
#include <iostream.h>
#include <string.h>
class Student
{
public:
// die verfügbaren Konstruktoren
Student();
Student(char* pszName);
Student(char* pszName, int nID);
~Student();
protected:
char* pszName;
int nID;
};
Weil das Objekt noName ohne Argument erscheint, wird es von dem Konstruktor Student::Stu-
dent( ) erzeugt. Dieser Konstruktor wird als Defaultkonstruktor (oder auch void-Konstruktor) bezeich-
net. (Ich persönlich bevorzuge den zweiten Begriff, aber der erste ist üblicher, weshalb er in diesem
Buch verwendet wird.)
Es ist oft der Fall, dass der einzige Unterschied zwischen einem Konstruktor und einem anderen
das Hinzufügen eines Defaultwertes für ein fehlendes Argument ist. Nehmen Sie z.B. an, dass ein
Student, der ohne ID erzeugt wird, die ID 0 erhält. Um Extraarbeit zu vermeiden, ermöglicht es C++
dem Programmierer, einen Defaultwert zuzuweisen. Ich hätte die beiden letzten Konstruktoren wie
folgt kombinieren können:
Student(char* pszName, int nID = 0);
Beide, s1 und s2, werden durch den gleichen Konstruktor Student(char*, in) konstruiert. Im
Falle von Stella, wird ein Defaultwert von 0 dem Konstruktor bereitgestellt.
Teil 4 – Samstagabend
Tipp zahl.
Lektion 20
20.2 Konstruktion von Klassenelementen.
C++ konstruiert Datenelementobjekte zur gleichen Zeit, wie das Objekt selber. Es gibt keinen offen-
sichtlichen Weg, Argumente bei der Konstruktion von Datenelementen zu übergeben.
Betrachten Sie Listing 20-2, in dem die Klasse Student ein Objekt der Klasse StudentId enthält.
Ich habe beide mit Ausgabeanweisungen in den Konstruktoren ausgestattet, um zu sehen, was pas-
siert.
Listing 20-2: Konstruktor Student mit einem Elementobjekt aus der Klasse StundentId
// DefaultStudentId – erzeuge ein Student-Objekt mit
// der nächsten verfügbaren ID
#include <stdio.h>
#include <iostream.h>
#include <string.h>
// StudentId
int nNextStudentId = 0;
class StudentId
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 224
224 Samstagabend
{
public:
StudentId()
{
nId = ++nNextStudentId;
cout << »Konstruktor StudentId » << nId << »\n«;
}
~StudentId()
{
cout << »Destruktor StudentId »
<< nId << »\n«;
protected:
int nId;
};
// Student
class Student
{
public:
// Constructor – erzeuge Student mit
// dem gegebenen Namen
// (»kein Name« als Default)
Student(char *pszName = »kein Name«)
{
cout << »Konstruktor Student » << pszName << »\n«;
~Student()
{
cout << »Destruktor Student » << pszName << »\n«;
delete pszName;
pszName = 0;
}
protected:
char* pszName;
StudentId id;
};
Eine Studenten-ID wird jedem Studenten zugeordnet, wenn ein Student-Objekt erzeugt wird.
In diesem Beispiel werden IDs fortlaufend vergeben unter Verwendung der globalen Variable next-
StudentId, die die nächste zu vergebende ID enthält.
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 225
Konstruktor StudentId 1
Konstruktor Student Randy
Destruktor Student Randy
Destruktor StudentId 1
Beachten Sie, dass die Meldung vom Konstruktor StudentId( ) vor der Ausgabe des Konstruktors
Student( ) erscheint, und die Meldung des StudentId-Destruktors nach der Ausgabe des Student-
Destruktors erscheint.
Wenn alle Konstruktoren hier etwas ausgeben, könnten sie denken, dass jeder
!
Tipp
Konstruktor etwas ausgeben muss. Die meisten Konstruktoren geben nichts aus.
Konstruktoren in Büchern tun es, weil die Leser normalerweise den guten Rat
des Autors ignorieren, Schritt für Schritt durch die Programme zu gehen.
Wenn der Programmierer keinen Konstruktor bereitstellt, ruft der Defaultkonstruktor, der von
C++ automatisch bereitgestellt wird, die Defaultkonstruktoren der Datenelemente auf. Das Gleiche
gilt für den Destruktor, der automatisch die Destruktoren der Datenelemente aufruft, die Destrukto-
ren haben. Der Defaultdestruktor von C++ tut das Gleiche.
Teil 4 – Samstagabend
Das ist alles großartig für den Defaultkonstruktor. Aber was ist, wenn wir einen anderen Kon-
struktor als den Defaultkonstruktor aufrufen wollen? Wo tun wir das Objekt hin? Um das zu verdeut-
lichen, lassen Sie uns annehmen, dass, statt der Berechnung der Studenten-ID, diese als Argument
Lektion 20
des Konstruktors bereitgestellt wird.
Lassen Sie uns ansehen, wie es nicht funktioniert. Betrachten Sie das Programm in Listing 20-3.
// StudentId
int nNextStudentId = 0;
class StudentId
{
public:
StudentId(int nId = 0)
{
this->nId = nId;
cout << »Konstruktor StudentId » << nId << »\n«;
}
~StudentId()
{
cout << »Destruktor StudentId »
<< nId << »\n«;
protected:
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 226
226 Samstagabend
int nId;
};
// Student
class Student
{
public:
// Konstruktor – erzeuge Student mit
// dem gegebenen Namen und
// Studenten-ID
Student(char *pszName = »kein Name«,
int ssId = 0)
{
cout << »Konstruktor Student » << pszName << »\n«;
~Student()
{
cout << »Destruktor Student » << pszName << »\n«;
delete pszName;
pszName = 0;
}
protected:
char* pszName;
StudentId id;
};
Der Konstruktor für StudentId wurde so verändert, dass er einen Wert von außen entgegen-
nimmt (der Defaultwert ist notwendig, um das Beispiel kompilieren zu können aus Gründen, die
bald klar werden). Innerhalb des Konstruktors von Student hat der Programmierer (das bin ich) (cle-
ver) versucht, ein Objekt StudentId mit Namen id zu erzeugen.
Die Ausgabe des Programms zeigt ein Problem:
Konstruktor StudentId 0
Konstruktor Student Randy
Konstruktor StudentId 1234
Destruktor StudentId 1234
Nachricht von main
Destruktor Student Randy
Destruktor StudentId 0
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 227
Zum einen scheint der Konstruktor zweimal aufgerufen zu werden, das erste Mal mit null, bevor
der Konstruktor Student beginnt, und das zweite Mal mit dem erwarteten Wert 1234 vom Kon-
struktor Student aus. Offensichtlich wurde eine zweites Objekt StudentId erzeugt innerhalb des
Konstruktors Student, das vom StudentId-Datenelement verschieden ist.
Die Erklärung für dieses bizarre Verhalten ist, dass das Datenelement id bereits existiert, wenn der
Body des Konstruktors betreten wird. Anstatt ein existierendes Datenelement id zu erzeugen,
erzeugt die Deklaration im Konstruktor ein lokales Objekt mit dem gleichen Namen. Dieses lokale
Objekt wird vernichtet, wenn der Konstruktor wieder verlassen wird.
Wir brauchen einen anderen Mechanismus um anzuzeigen »konstruiere das existierende Ele-
ment; erzeuge kein neues.« C++ definiert die neue Konstruktion wie folgt:
class Student
{
public:
Student(char* pszName = »no name«, int ssId = 0)
: id(ssId) // konstruiere Datenelement id mit
// dem angegebenen Wert
{
cout << »Konstruktor Student » << pszName << »\n«;
this->pszName = new char[strlen(pszName) + 1];
strcpy(this->pszName, pszName);
Teil 4 – Samstagabend
}
// ...
};
Lektion 20
Beachten Sie insbesondere die erste Zeile des Konstruktors. Wir haben das vorher noch nicht
gesehen. Der Doppelpunkt »:« bedeutet, dass Aufrufe von Konstruktoren der Datenelemente der
aktuellen Klasse folgen. Für den C++-Compiler liest sich die Zeile so: »Konstruiere das Element id mit
dem Argument ssId des Student-Konstruktors. Alle Datenelemente, die nicht so behandelt wer-
den, werden mit ihrem Defaultkonstruktor konstruiert.«
.
CD-ROM
Das gesamte Programm befindet sich auf der beiliegenden CD-ROM unter dem
Namen StudentId.
228 Samstagabend
Das Objekt s wird in main( ) erzeugt mit dem Studentennamen »Randy« und einer Studenten-
ID von 1234. Dieses Objekt wird am Ende von main( ) automatisch vernichtet. Die »:«-Syntax muss
auch bei der Wertzuweisung an Elemente vom Typ const oder mit Referenztyp verwendet werden.
Betrachten Sie die folgende dumme Klasse:
class SillyClass
{
public:
SillyClass(int& i) : nTen(10), refI(i)
{
}
protected:
const int nTen;
int& refI;
};
Wenn das Programm mit der Ausführung des Bodys des Konstruktors beginnt, sind die beiden
Elemente nTen und refI bereits angelegt. Diese Datenelemente müssen initialisiert sein, bevor die
Kontrolle an den Body des Konstruktors übergeht.
!
Tipp
Jedes Datenelement kann mit der »im-Voraus«-Syntax deklariert werden.
=
= =
=
Ein Seiteneffekt ist eine Zustandsänderung, die durch eine Funktion verursacht
wird, die aber nicht zu den Argumenten oder dem zurückgegebenen Objekt
Hinweis gehört. Wenn z.B. eine Funktion einer globalen Variablen einen Wert zuweist,
wäre das ein Seiteneffekt.
Teil 4 – Samstagabend
void fn(int i)
{
Lektion 20
static DoNothing staticDN(1);
DoNothing localDN(2);
}
Die Variable staticDN wird beim ersten Aufruf von fn( ) konstruiert. Beim zweiten Aufruf von
fn( ) konstruiert das Programm nur localDN.
230 Samstagabend
Meistens ist die Reihenfolge gleichgültig. Hin und wieder können aber dadurch Bugs entstehen,
die sehr schwierig zu finden sind. (Es passiert jedenfalls oft genug, um in einem Buch Erwähnung zu
finden.)
Betrachten Sie das Beispiel:
class Student
{
public:
Student (unsigned id) : studentId(id)
// unsigned ist vorzeichenloses int
{
}
const unsigned studentId;
};
class Tutor
{
public:
// Konstruktor – weise dem Tutor einen Studenten
// zu durch Auslesen seiner ID
Tutor(Student &s)
{
tutoredId = s.studentId;
}
protected:
unsigned tutoredId;
};
Hier weist der Konstruktor von Student eine Studenten-ID zu. Der Konstruktor von Tutor kopiert
sich diese ID. Das Programm deklariert einen Studenten randy und weist dann diesem Student den
Tutor jenny zu.
Das Problem ist, dass wir implizit annehmen, dass randy vor jenny konstruiert wird. Nehmen wir
an, es wäre anders herum. Dann würde jenny mit einem Speicherblock initialisiert, der noch nicht
in ein Student-Objekt verwandelt wurde und daher »Müll« als Studenten-ID enthält.
Dieses Beispiel ist nicht besonders schwer zu durchschauen und mehr als nur ein
!
Tipp
wenig konstruiert. Trotzdem können Probleme mit globalen Objekten, die in kei-
ner bestimmten Ordnung konstruiert werden, sehr subtil sein. Um diese Proble-
me zu vermeiden, sollten Sie es dem Konstruktor eines globalen Objektes nicht
gestatten, sich auf den Inhalt eines anderen globalen Objektes zu verlassen.
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 231
In diesem Beispiel wird sId konstruiert, bevor sAge überhaupt daran denken kann, als zweites in
der Initialisierungsliste des Konstruktors zu stehen. Sie würden nur dann einen Unterschied in der
Reihenfolge der Konstruktion bemerken, wenn beide Elemente Objekte wären, deren Konstruktoren
gegenseitige Seiteneffekte haben.
Teil 4 – Samstagabend
Schließlich werden die Destruktoren, egal in welcher Reihenfolge die Konstruktoren aufgerufen wur-
den, in der umgekehrten Reihenfolge aufgerufen. (Es ist schön zu wissen, dass es in C++ wenigstens
eine Regel ohne Wenn und Aber gibt.)
Lektion 20
20.4 Der Kopierkonstruktor.
Ein Kopierkonstruktor ist ein Konstruktor, der den Namen X::X(&X) trägt, wobei X
ein Klassenname ist. D.h. er ist der Konstruktor einer Klasse X, und nimmt als Argu-
ment eine Referenz auf ein Objekt der Klasse X. Nun, ich weiß, dass das wirklich
10 Min. nutzlos klingt, aber denken Sie einen Augenblick darüber nach, was passiert, wenn
Sie eine Funktion wie die folgende aufrufen:
void fn1(Student fs)
{
// ...
}
int fn2()
{
Student ms;
fn1(ms);
return 0;
}
Wie sie wissen, wird eine Kopie des Objektes ms – und nicht das Objekt selber – an die Funktion
fn1( ) übergeben. C++ könnte einfach eine exakte Kopie des Objektes machen und diese an fn1( )
übergeben.
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 232
232 Samstagabend
Das ist in C++ nicht akzeptabel. Erstens wird zur Konstruktion eines Objektes ein Konstruktor
benötigt, selbst für das Kopieren eines existierenden Objektes. Zweitens, was ist, wenn wir keine ein-
fache Kopie des Objektes haben möchten? (Lassen wir das »Warum?« für eine Weile.) Wir müssen in
der Lage sein, anzugeben, wie die Kopie konstruiert werden soll.
Das Programm CopyStudent in Listing 20-4 demonstriert diesen Punkt.
class Student
{
public:
// Initialisierungsfunktion
void init(char* pszName, int nId, char* pszPreamble = »\0«)
{
int nLength = strlen(pszName)
+ strlen(pszPreamble)
+ 1;
this->pszName = new char[nLength];
strcpy(this->pszName, pszPreamble);
strcat(this->pszName, pszName);
this->nId = nId;
}
// Konventioneller Konstruktor
Student(char* pszName = »noname«, int nId = 0)
{
cout << »Konstruktor Student » << pszName << »\n«;
init(pszName, nId);
}
// Kopierkonstruktor
Student(Student &s)
{
cout << »Kopierkonstruktor von »
<< s.pszName
<< »\n«;
init(s.pszName, s.nId, »Kopie von »);
}
~Student()
{
cout << »Destruktor » << pszName << »\n«;
delete pszName;
}
protected:
char* pszName;
int nId;
};
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 233
// fertig
return 0;
}
Teil 4 – Samstagabend
Konstruktor Student Randy
Aufruf von fn( )
Kopierkonstruktor Student Randy
In Funktion fn( )
Lektion 20
Destruktor Kopie von Randy
Rückkehr von fn( )
Destruktor Randy
Beginnend bei main( ) sehen wir, wie dieses Programm arbeitet. Der normale Konstruktor
erzeugt die erste Nachricht. main( ) erzeugt die Nachricht »Aufruf von ...«. C++ ruft den Kopier-
konstruktor auf, um eine Kopie von randy an fn( ) zu übergeben, was die nächste Zeile der Ausga-
be erzeugt. Die Kopie wird am Ende von fn( ) vernichtet. Das originale Objekt randy wird am Ende
von main( ) vernichtet.
(Dieser Kopierkonstruktor macht ein wenig mehr, als nur eine Kopie des Objekts; er trägt die
Phrase “Kopie von“ am Anfang des Namens ein. Das war zu Ihrem Vorteil. Normalerweise sollten
sich Kopierkonstruktoren darauf beschränken, einfach Kopien zu konstruieren. Aber prinzipiell kön-
nen sie alles tun.)
234 Samstagabend
auf den gleichen Besitz. Das wird noch schlimmer, wenn der Destruktor für beide Objekte aufgeru-
fen wird und sie beide versuchen, den Speicher zurückzugeben. Um das konkreter werden zu lassen,
betrachten Sie die Klasse Student nochmals:
class Student
{
public:
Person(char *pszName)
{
pszName = new char[strlen(pszName) + 1];
strcpy(pName, pN);
}
~Person()
{
delete pszName;
}
protected:
char* pszName;
};
Hier alloziert der Konstruktor Speicher vom Heap, um den Namen der Person zu speichern – der
Destruktor gibt freundlicherweise den Speicher zurück, wie er soll. Wenn dieses Objekt als Wert an
eine Funktion übergeben wird, würde C++ eine Kopie des Student-Objektes machen, den Zeiger
pszName eingeschlossen. Das Programm hat dann zwei Student-Objekte, die auf den gleichen Spei-
cherbereich zeigen, der den Namen des Studenten enthält. Wenn das erste Objekt vernichtet wird,
wird der Speicherbereich zurückgegeben. Bei der Vernichtung des zweiten Objektes wird versucht,
den gleichen Speicher erneut freizugeben – ein fataler Vorgang für ein Programm.
Speicher vom Heap ist nicht der einzige Besitz, der eine tiefe Kopie erforderlich
macht, aber der häufigste. Offene Dateien, Ports und allozierte Hardware (wie
!
Tipp
z.B. Drucker) benötigen ebenfalls tiefe Kopien. Das sind dieselben Typen, die der
Destruktor zurückgeben muss. Eine gute Daumenregel ist, dass ihre Klasse einen
Kopierkonstruktor benötigt, wenn sie einen Destruktor benötigt, um Ressourcen
wieder freizugeben.
=
= =
= Wenn Sie keinen Kopierkonstruktor selber erzeugen, legt C++ einen eigenen
Hinweis Kopierkonstruktor an.
Im folgenden Beispiel kann C++ keine Kopie der Klasse BankAccount anlegen, wodurch sicher-
gestellt ist, dass das Programm nicht versehentlich eine Überweisung auf die Kopie eines Kontos und
nicht auf das Konto selber ausführt.
class BankAccount
{
protected:
int nBalance;
BankAccount(BankAccount& ba)
{
}
public:
// ... Rest der Klasse ...
};
Teil 4 – Samstagabend
Zusammenfassung.
Der Konstruktor hat die Aufgabe, Objekte zu erzeugen und mit einem gültigen
Lektion 20
Zustand zu initialisieren. Viele Objekte haben jedoch keinen gültigen Default-
Zustand. Z.B. ist es nicht möglich, ein gültiges Student-Objekt ohne einen Namen
0 Min. und eine ID zu erzeugen. Ein Konstruktor mit Argumenten ermöglicht es dem Pro-
gramm, initiale Werte für neu erzeugte Objekte zu übergeben. Diese Argumente können auch an
andere Konstruktoren der Datenelemente der Klasse weitergegeben werden. Die Reihenfolge, in der
die Datenelemente konstruiert werden, ist im C++-Standard definiert (eines der wenigen Dinge, die
wohldefiniert sind).
• Argumente von Konstruktoren erlauben die Festlegung eines initialen Zustandes von Objekten
durch das Programm; für den Fall, dass diese initialen Werte nicht gültig sind, muss die Klasse
einen Reservezustand haben.
• Es muss vorsichtig mit Konstruktoren umgegangen werden, die Seiteneffekte haben, wie z.B. das
Ändern globaler Variablen, weil einer oder mehrere Konstruktoren kollidieren können, wenn sie
ein Objekt erzeugen, das Datenelemente enthält, die selber Objekte sind.
• Ein spezieller Konstruktor, der Kopierkonstruktor, hat den Prototyp X::X(&X), wobei X der Name
der Klasse ist. Dieser Konstruktor wird dazu verwendet, eine Kopie eines existierenden Objektes zu
erzeugen. Kopierkonstruktoren sind extrem wichtig, wenn das Objekt Ressourcen enthält, die im
Destruktor an einen Ressourcen-Pool zurückgegeben werden.
Obwohl die Prinzipien der objektorientierten Programmierung erläutert wurden, sind wir noch nicht
da, wo wir hin wollen. Wir haben ein Mikrowelle gebaut, wir haben eine Box um die arbeitenden Tei-
le gebaut, und wir haben ein Benutzerinterface definiert; aber wir haben keine Beziehung zwischen
Mikrowellen und anderen Ofentypen hergestellt. Das ist das Wesentliche der objektorientierten Pro-
grammierung, und wird in Teil 5 diskutiert.
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 236
236 Samstagabend
Selbsttest.
1. Warum sollte eine Klasse wie Student einen Konstruktor der Form Student(char* pszName,
int nId) haben? (Siehe »Konstruktoren mit Argumenten«)
2. Wie können Sie etwas über die Reihenfolge aussagen, in der globale Objekte konstruiert werden?
(Siehe »Keine bestimmte Reihenfolge für globale Objekte«)
3. Warum benötigt eine Klasse einen Kopierkonstruktor? (Siehe »Der Kopierkonstruktor«)
4. Was ist der Unterschied zwischen einer flachen Kopie und einer tiefen Kopie? (Siehe »Flache
Kopie gegen tiefe Kopie«)
C++ Lektion 20 31.01.2001 12:42 Uhr Seite 237
Samstagabend –
Zusammenfassung 4
T EIL
1. Denken Sie über den Inhalt Ihres Kleiderschrankes nach. Beschreiben Sie informell, was Sie
dort finden.
3. Benennen Sie zwei verschieden Typen von Schuhen. Was ist der Effekt, einen anstelle des
anderen zu verwenden?
4. Schreiben Sie einen Konstruktor für eine Klasse, die wie folgt definiert ist:
class Link
{
static Link* pHead;
Link* pNextLink;
};
5. Schreiben Sie einen Kopierkonstruktor und einen Destruktor für die folgende Klasse
LinkedList:
#include <stdio.h>
#include <iostream.h>
#include <string.h>
class LinkedList
{
protected:
LinkedList* pNext;
static LinkedList* pHead;
char* pszName;
public:
// Konstruktor – kopiere den Namen
LinkedList(char* pszName)
{
int nLength = strlen(pszName) + 1;
this->pszName = new char[nLength];
strcpy(this->pszName, pszName);
pNext = 0;
}
// Destruktor -
~LinkedList()
{
// ... was kommt hier hin?
}
LinkedList(LinkedList& l)
{
// ... und hier?
}
};
Hinweise:
a. Bedenken Sie, was passiert, wenn das Objekt immer noch in der verketteten Liste ist,
nachdem es vernichtet wurde.
b. Erinnern Sie sich daran, dass Speicher, der vom Heap alloziert wurde, zurückgegeben
werden sollte, bevor der Zeiger darauf verloren geht.
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 239
ta g
✔ Fr ei
g
st a
✔S a m
g
n ta
Son✔
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 240
Sonntagmorgen
Teil 5
Lektion 21.
Vererbung
Lektion 22.
Polymorphie
Lektion 23.
Abstrakte Klassen und Faktorieren
Lektion 24.
Mehrfachvererbung
Lektion 25.
Große Programme II
Lektion 26.
C++-Präprozessor II
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 241
Vererbung
21Lektion
Checkliste.
✔ Vererbung definieren
✔ Von einer Basisklasse erben
✔ Die Basisklasse konstruieren
✔ die Beziehungen IS_A und HAS_A vergleichen
I
n dieser Sitzung diskutieren wir Vererbung. Vererbung ist die Fähigkeit einer Klas-
se, auf Fähigkeiten oder Eigenschaften einer anderen Klasse zurückzugreifen. Ich
z.B. bin ein Mensch. Ich erbe von der Klasse Mensch bestimmte Eigenschaften,
wie z.B. meine Fähigkeit zu (mehr oder weniger) intelligenter Konversation, und
30 Min. meine Abhängigkeit von Luft, Wasser und Nahrung. Diese Eigenschaften sind
nicht einzigartig für Menschen. Die Klasse Mensch erbt diese Abhängigkeit von
Luft, Wasser und Nahrung von der Klasse Säugetier.
242 Sonntagmorgen
Wichtiger ist das verwandte Reizwort der 90-er Jahre, Wiederverwendung. Softwarewissenschaft-
ler haben vor einer gewissen Zeit festgestellt, dass es keinen Sinn macht, in jedem Softwareprojekt
bei Null zu beginnen, und die gleichen Softwarekomponenten immer wieder neu zu schreiben.
Vergleichen Sie diese Situation bei der Software mit anderen Industrien. Wie viele Autohersteller
fangen bei jedem Auto ganz von vorne an? Und selbst wenn sie das täten, wie viele würden beim
nächsten Modell wieder ganz von vorne beginnen? Praktiker in anderen Industrien haben es sinn-
voller gefunden, bei Schrauben, Muttern und auch größeren Komponenten wie Motoren und Kom-
pressoren zu beginnen.
Unglücklicherweise ist, mit Ausnahme der sehr kleinen Funktionen in der Standardbibliothek von
C, nur sehr wenig Wiederverwendung von Softwarekomponenten zu sehen. Ein Problem ist, dass es
fast unmöglich ist, eine Komponente in einem früheren Programm zu finden, die exakt das tut, was
Sie brauchen. Im Allgemeinen müssen diese Komponenten angepasst werden.
Es gibt eine Daumenregel die besagt »Wenn Sie es geöffnet haben, haben Sie es zerbrochen«.
Mit anderen Worten, wenn Sie eine Funktion oder Klasse modifizieren müssen, um sie an Ihre
Anwendung anzupassen, müssen Sie wieder alles neu testen, nicht nur die Teile, die Sie hinzugefügt
haben. Änderungen können irgendwo im Code Bugs verursachen. (»Wer den Code zuletzt ange-
fasst hat, muss den Bug fixen«.)
Vererbung ermöglicht es, bestehende Klassen an neue Anwendungen anzupassen ohne sie ver-
ändern zu müssen. Von der bestehenden Klasse wird eine neue Unterklasse abgeleitet, die alle nöti-
gen Zusätze und Änderungen enthält.
Das bringt einen dritten Vorteil. Nehmen Sie an, wir erben von einer existierenden Klasse. Später
finden wir heraus, dass die Basisklasse einen Fehler enthält und korrigiert werden muss. Wenn wir die
Klasse zur Wiederverwendung modifiziert haben, müssen wir in jeder Anwendung einzeln auf den
Fehler testen und ihn korrigieren. Wenn wir von der Klasse ohne Änderungen geerbt haben, können
wir die berichtigte Klasse sicher ohne Weiteres übernehmen.
Die Vererbungsbeziehung ist jedoch transitiv. Wenn ich z.B. eine neue Klasse GraduateStudent
als Unterklasse von Student einführe, muss GraduateStudent auch Person sein. Es muss so ausse-
hen: wenn GraduateStudent IS_A Student und Student IS_A Person, dann GraduateStudent
IS_A Person.
Teil 5 – Sonntagmorgen
{
public:
Student()
{
Lektion 21
// initialer Zustand
pszName = 0;
nSemesterHours = 0;
dAverage = 0;
}
~Student()
{
// wenn es einen Namen gibt ...
if (pszName != 0)
{
// ... dann gib den Puffer zurück
delete pszName;
pszName = 0;
}
}
244 Sonntagmorgen
protected:
char* pszName;
int nSemesterHours;
double dAverage;
double qualifier( )
{
return dQualifierGrade;
}
protected:
// alle graduierten Studenten haben einen Advisor
Advisor advisor;
double dQualifierGrade;
};
Die Klasse Student wurde in der gewohnten Weise deklariert. Die Deklaration der Klasse Gra-
duateStudent unterscheidet sich davon. Der Name der Klasse, gefolgt von dem Doppelpunkt,
gefolgt von public Student deklariert die Klasse GraduateStudent als Unterklasse von Student.
=
= =
=
Das Schlüsselwortes public impliziert, dass es sicherlich auch eine protected-
Vererbung gibt. Das ist der Fall, ich möchte jedoch diesen Typ Vererbung für
Hinweis
Teil 5 – Sonntagmorgen
den Moment aus der Diskussion heraus lassen.
Die Funktion main( ) deklariert zwei Objekte, llu und gs. Das Objekt llu ist ein konventionelles
Lektion 21
Student-Objekt, aber das Objekt gs ist etwas Neues. Als ein Mitglied einer Unterklasse von Student,
kann gs alles tun, was llu tun kann. Es hat die Datenelemente pszName, nSemesterHours und
dAverage und die Elementfunktion addCourse( ). Buchstäblich gilt, gs IS_A Student – gs ist nur
ein wenig mehr als ein Student. (Sie werden es am Ende des Buches sicher nicht mehr ertragen kön-
nen, dass ich »IS_A« so oft benutze.) In der Tat hat die Klasse GraduateStudent die Eigenschaft
qualifier( ), die Student nicht besitzt.
Die nächsten beiden Zeilen fügen den beiden Studenten llu und gs einen Kurs hinzu. Erinnern
Sie sich daran, dass gs auch ein Student ist.
Eine der letzten Zeilen in main( ) ist nicht korrekt. Es ist in Ordnung die Methode qualifier( )
für das Objekt gs aufzurufen. Es ist nicht in Ordnung, die Eigenschaft qualifier für das Objekt llu
zu verwenden. Das Objekt llu ist nur ein Student und hat nicht die Eigenschaften, die für
GraduateStudent einzigartig sind.
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 246
246 Sonntagmorgen
Beachten Sie, dass die Funktion fn( ) ein Objekt vom Typ Student als Argument erwartet. Der
Aufruf von main( ) übergibt der Funktion ein Objekt aus der Klasse GraduateStudent. Das ist in Ord-
nung, weil (um es noch einmal zu wiederholen) »ein GraduateStudent IS_A Student.«
Im Wesentlichen entstehen die gleichen Bedingungen, wenn eine Elementfunktion von Student
mit einem GraduateStudent-Objekt aufgerufen wird. Z.B.:
int main(int nArgc, char* pszArgs[])
{
GraduateStudent gs;
gs.addCourse(3, 2.5);//ruft Student::addCourse( )
return 0;
}
Teil 5 – Sonntagmorgen
// GraduateStudent – diese Klasse ist auf die
// Studenten beschränkt, die ihr
// Vordiplom haben
Lektion 21
class GraduateStudent : public Student
{
public:
// Konstruktor – erzeuge graduierten Studenten
// mit einem Advisor, einem Namen
// und einem Qualifizierungsgrad
GraduateStudent(
Advisor &adv,
char* pszName = 0,
double dQualifierGrade = 0.0)
: Student(pName),
advisor(adv)
{
// wird erst ausgeführt, nachdem die anderen
// Konstruktoren aufgerufen wurden
dQualifierGrade = 0;
}
protected:
// alle graduierten Studenten haben einen Advisor
Advisor advisor;
248 Sonntagmorgen
Hier wird ein GraduateStudent-Objekt mit einem Advisor erzeugt, dessen Name »Marion Haste«
und dessen Grad gleich 2.0 ist. Der Konstruktor von GraduateStudent ruft den Konstruktor Stu-
dent auf, und übergibt den Namen des Studenten. Die Basisklasse wird konstruiert vor allen ande-
ren Elementobjekten; somit wird der Konstruktor von Student vor den Konstruktor von Advisor
aufgerufen. Nachdem die Basisklasse konstruiert wurde, wird das Advisor-Objekt advisor mittels
Kopierkonstruktor konstruiert. Erst dann kommt der Konstruktor von GraduateStudent zum Zuge.
Die Tatsache, dass die Basisklasse zuerst erzeugt wird, hat nichts mit der Ord-
=
= =
=
nung der Konstruktoranweisungen hinter dem Doppelpunkt zu tun. Die Basis-
klasse wäre auch dann vor den Datenelementen konstruiert worden, wenn die
Hinweis Anweisungen advisor(adv), Student(pszName) gelautet hätten. Es ist jeden-
falls eine gute Idee, diese Klauseln in der Reihenfolge zu schreiben, in der sie
ausgeführt werden, nur um niemanden zu verwirren.
=
= =
= Der Destruktor der Basisklasse Student wird ausgeführt, obwohl es keinen
Hinweis expliziten Destruktor ~GraduateStudent gibt.
Das ist logisch. Der wenige Speicher, der schließlich zu einem GraduateStudent-Objekt wird,
wird erst in ein Student-Objekt konvertiert. Dann ist es die Aufgabe des Konstruktors GraduateS-
tudent, seine Transformation in ein GraduateStudent-Objekt zu vervollständigen. Der Destruktor
kehrt diesen Prozess einfach um.
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 249
Beachten Sie einige wenige Dinge in diesem Beispiel. Erstens wurden Defaultargu-
mente im Konstruktor GraduateStudent bereitgestellt, um diese Fähigkeit an die
=
= =
= Basisklasse Student weiterzugeben. Zweitens können Defaultwerte für Argumen-
Hinweis
te nur von rechts nach links angegeben werden. Das Folgende ist nicht möglich:
GraduateStudent(char* pszName = 0, Advisor& adv) ...
Die Argumente ohne Defaultwerte müssen zuerst kommen.
Beachten Sie, dass die Klasse GraduateStudent ein Advisor-Objekt in der Klasse enthält. Es ent-
hält keinen Zeiger auf ein Advisor-Objekt. Letzteres würde so geschrieben werden:
Hierbei wird die Basisklasse Student zuerst erzeugt (wie immer). Der Zeiger wird innerhalb des
Body des Konstruktors GraduateStudent initialisiert.
Teil 5 – Sonntagmorgen
21.5 Die Beziehung HAS_A.
Lektion 21
Beachten Sie, dass die Klasse GraduateStudent die Elemente der Klasse Student und Advisor ein-
schließt, aber auf verschiedene Weisen. Durch die Definition eines Datenelementes aus der Klasse
Advisor wissen wir, dass ein GraduateStudent alle Datenelemente von Advisor in sich enthält,
und wir drücken das aus, indem wir sagen, GraduateStudent HAS_A Advisor. Was ist der Unter-
schied zwischen dieser Beziehung und Vererbung?
Lassen Sie uns ein Auto als Beispiel nehmen. Wir könnten logisch ein Auto als Unterklasse von
Fahrzeug definieren und dadurch allgemeine Eigenschaften von Fahrzeugen erben. Gleichzeitig hat
ein Auto auch einen Motor. Wenn Sie ein Auto kaufen, können Sie logisch davon ausgehen, dass Sie
auch einen Motor kaufen.
Wenn nun einige Freunde am Wochenende eine Rallye mit dem Fahrzeug der eigenen Wahl ver-
anstalten, wird sich niemand darüber beschweren, wenn Sie mit ihrem Auto kommen, weil Auto
IS_A Fahrzeug. Wenn Sie aber zu Fuß kommen und Ihren Motor unter dem Arm tragen, haben sie
allen Grund, erstaunt zu sein, weil ein Motor kein Fahrzeug ist. Ihm fehlen einige wesentliche Eigen-
schaften, die alle Fahrzeuge haben. Es fehlen dem Motor sogar Eigenschaften, die alle Autos haben.
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 250
250 Sonntagmorgen
Vom Standpunkt der Programmierung aus ist es ebenso einfach. Betrachten sie das Folgende:
class Vehicle
{
};
class Motor
{
};
class Car : public Vehicle
{
public:
Motor motor;
};
void VehicleFn(Vehicle &v);
void motorFn(Motor &m);
int main(int nArgc, char* pszArgs[])
{
Car c;
vehicleFn(c); // das ist erlaubt
motorFn(c); // das ist nicht erlaubt
motorFn(c.motor); // das jedoch schon
return 0;
}
Der Aufruf vehicleFn(c) ist erlaubt, weil c IS_A Vehicle. Der Aufruf motorFn(c)
ist nicht erlaubt, weil c kein Motor ist, obwohl es einen Motor enthält. Wenn beab-
sichtigt ist, den Teil Motor von c an eine Funktion zu übergeben, muss dies explizit
ausgedrückt werden, wie im Aufruf motorFn(c.motor).
0 Min.
=
= =
= Natürlich ist der Aufruf motorFn(c.motor) nur dann erlaubt, wenn c.motor
Hinweis public ist.
Ein weiterer Unterschied: Die Klasse Car hat Zugriff auf die protected-Elemente von Vehicle,
aber nicht auf die protected-Elemente von Motor.
C++ Lektion 21 31.01.2001 12:43 Uhr Seite 251
Zusammenfassung.
Das Verständnis von Vererbung ist wesentlich für das Gesamtverständnis der objektorientierten Pro-
grammierung. Es wird auch benötigt, um das nächste Kapitel verstehen zu können. Wenn Sie den
Eindruck haben, dass sie es verstanden haben, gehen Sie weiter zu Kapitel 22. Wenn nicht, lesen Sie
dieses Kapitel erneut.
Selbsttest.
1. Was ist die Beziehung zwischen einem graduierten Studenten und einem Studenten. Ist es eine
Beziehung der Form IS_A oder HAS_A? (Siehe »Die Beziehung HAS_A«)
2. Nennen Sie drei Vorteile davon, dass Vererbung in der Programmiersprache C++ vorhanden ist.
(Siehe »Vorteile von Vererbung«)
3. Welcher der folgenden Begriffe passt nicht? Erbt, Unterklasse, Datenelement und IS_A? (Siehe
»Faktorieren von Klassen«)
Teil 5 – Sonntagmorgen
Lektion 21
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 252
22 Lektion Polymorphie
Checkliste.
✔ Elementfunktionen in Unterklassen überschreiben
✔ Polymorphie anwenden (alias späte Bindung)
✔ Polymorphie mit früher Bindung vergleichen
✔ Polymorphie speziell betrachten
V
ererbung gibt uns die Möglichkeit, eine Klasse mit Hilfe einer anderen Klasse
zu beschreiben. Genauso wichtig ist, dass dadurch die Beziehung zwischen
den Klassen deutlich wird. Nochmals, eine Mikrowelle ist ein Typ Ofen. Es
fehlt jedoch noch ein Teil im Puzzle.
Sie haben das bestimmt bereits bemerkt, aber eine Mikrowelle und ein her-
30 Min.
kömmlicher Ofen sehen sich nicht besonders ähnlich. Die beiden Ofentypen arbei-
ten auch nicht gleich. Trotzdem möchte ich mir keine Gedanken darüber machen, wie jeder einzel-
ne Ofen das »Kochen« ausführt. Diese Sitzung beschreibt, wie C++ dieses Problem behandelt.
=
= =
= Vererbung liefert eine weitere Möglichkeit: Eine Elementfunktion in einer Unter-
Hinweis klasse kann eine Elementfunktion der Basisklasse überladen.
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 253
class Student
{
public:
// berechnet das Schulgeld
double calcTuition()
{
return 0;
}
};
class GraduateStudent : public Student
{
public:
double calcTuition()
{
return 1;
}
};
Teil 4 – Sonntagmorgen
{
// der folgende Ausdruck ruft
// Student::calcTuition();
Student s;
Lektion 22
cout << »Der Wert von s.calcTuition ist »
<< s.calcTuition()
<< »\n«;
Ausgabe
Der Wert von s.calcTuition ist 0
Der Wert von gs.calcTuition ist 1
Wie bei jedem anderen Fall von Überschreiben, muss C++ entscheiden, welche Funktion calc-
Tuition( ) gemeint ist, wenn der Programmierer calcTuition( ) aufruft. Normalerweise reicht
die Klasse aus, um den Aufruf aufzulösen, und es ist bei diesem Beispiel nicht anders. Der Aufruf
s.calcTuition( ) bezieht sich auf Student::calcTuition( ), weil s als Student deklariert ist,
wobei gs.calcTuition( ) sich auf GraduateStudent::calcTuition( ) bezieht.
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 254
254 Sonntagmorgen
Die Ausgabe des Programms EarlyBinding zeigt, dass der Aufruf überschriebener Elementfunk-
tionen gemäß dem Typ des Objektes aufgelöst wird.
=
= =
= Das Auflösen von Aufrufen von Elementfunktionen basierend auf dem Typ des
Hinweis Objektes wird Bindung zur Compilezeit oder auch frühe Bindung genannt.
class Student
{
public:
double calcTuition()
{
return 0;
}
};
class GraduateStudent : public Student
{
public:
double calcTuition()
{
return 1;
}
};
{
// der folgende Ausdruck ruft
// Student::calcTuition();
Student s;
cout << »Der Wert von s.calcTuition bei\n«
<< »Aufruf durch fn() ist »
<< fn(s)
<< »\n«;
Der einzige Unterschied zwischen Listing 22-1 und 22-2 ist, dass die Aufrufe von calcTuition( )
über eine Zwischenfunktion fn( ) ausgeführt werden. Die Funktion fn(Student& fs) ist so dekla-
riert, dass sie ein Student-Objekt übergeben bekommt, aber abhängig davon, wie fn( ) aufgerufen
wird, kann fs ein Student oder ein GraduateStudent sein. (Erinnern Sie sich? GraduateStudent
IS_A Student.) Aber diese beiden Typen von Objekten berechnen ihr Schulgeld verschieden.
Weder main( ) noch fn( ) kümmern sich eigentlich darum, wie das Schulgeld berechnet wird.
Wir hätten gerne, dass fs.calcTuition( ) die Funktion Student::calcTuition( ) aufruft, wenn
fs ein Student ist, aber GraduateStudent::calcTuition( ), wenn fs ein GraduateStudent ist.
Aber diese Entscheidung kann erst zur Laufzeit getroffen werden, wenn der tatsächliche Typ des
Teil 4 – Sonntagmorgen
übergebenen Objektes bestimmt werden kann.
Im Falle des Programms AmbiguousBindung sagen wir, dass der Compiletyp von fs, der immer
Student ist, verschieden ist vom Laufzeittyp, der GraduateStudent oder Student sein kann.
256 Sonntagmorgen
Erinnern Sie sich, wie ich Nachos im Ofen hergestellt habe? In diesem Sinne habe ich als später
Binder gearbeitet. Im Rezept steht: »Nachos im Ofen erwärmen.« Da steht nicht: »Wenn der Ofen
eine Mikrowelle ist, tun Sie das; wenn es ein herkömmlicher Ofen ist, tun sie das; wenn der Ofen ein
Elektroofen ist, tun sie noch etwas anderes.« Das Rezept (der Code) verlässt sich auf mich (den spä-
ten Binder) zu entscheiden, welche Tätigkeit (Elementfunktion) erwärmen bedeutet, angewendet
auf einen Ofen (die spezielle Instanz von Oven) oder eine ihrer Varianten (Unterklassen), wie z.B.
Mikrowellen (Microwave). Das ist die Art und Weise, in der Leute denken, und eine Sprache in dieser
Weise zu entwickeln, ermöglicht es der Sprache, besser zu beschreiben, was Leute denken.
Es gibt da noch die beiden Aspekte der Pflege und Wiederverwendbarkeit. Nehmen Sie an, ich
habe dieses großartige Programm beschrieben, das die Klasse Student verwendet. Nach einigen
Monaten des Entwurfs, der Implementierung und des Testens erstelle ich ein Release der Anwen-
dung.
Es vergeht einige Zeit und mein Chef bittet mich, dem Programm GraduateStudent-Objekte hin-
zuzufügen, die sehr ähnlich zu Studenten sind, aber nicht identisch damit. Tief im Programm ruft die
Funktion someFunktion( ) die Elementfunktion calcTuition( ) wie folgt auf:
void someFunction(Student &s)
{
//... was immer sie tut ...
s.calcTuition();
//... wird hier fortgesetzt ...
}
Wenn C++ keine späte Bindung ausführen würde, müsste ich die Funktion someFunction( ) edi-
tieren, um auch GraduateStudent-Objekte verarbeiten zu können. Das könnte etwa so aussehen:
#define STUDENT 1
#define GRADUATESTUDENT 2
void someFunction(Student &s)
{
//... was immer sie tut ...
// füge ein Typelement hinzu, das den
// tatsächlichen Typ des Objekts angibt
switch (s.type)
{
STUDENT:
s.Student::calcTuition();
break;
GRADUATESTUDENT:
s.GraduateStudent::calcTuition();
break;
}
//... alles Weitere hier ...
}
=
= =
=
Durch Verwendung des vollständigen Namens der Funktion zwingt der Aus-
druck s.GraduateStudent::calcTuition( ) den Aufruf, die GraduateStudent-
Hinweis
Version der Funktion zu verwenden, selbst wenn s als Student deklariert ist.
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 257
Ich würde dann ein Element type in der Klasse einführen, das ich im Konstruktor von Student auf
STUDENT setzen würde, und auf GRADUATESTUDENT im Konstruktor von GraduateStudent. Der Wert
von type würde den Laufzeittyp von s darstellen. Ich würde dann den Test im Codeschnipsel einfü-
gen, um die dem Wert dieses Elements entsprechende Funktion aufzurufen.
Das hört sich nicht schlecht an, mit Ausnahme von drei Dingen. Erstens ist das hier nur eine Funk-
tion. Nehmen Sie an, dass calcTuition( ) von vielen Stellen aus aufgerufen wird, und nehmen Sie
an, dass calcTuition( ) nicht der einzige Unterschied der beiden Klassen ist. Die Chancen stehen
nicht sehr gut, dass ich alle Stellen finde, an denen ich etwas ändern muss.
Zweitens muss ich Code, der bereits fertiggestellt wurde, editieren (d.h. »brechen«), wodurch
die Möglichkeit für neue Fehler gegeben ist. Das Editieren kann zeitaufwendig und langweilig sein,
was wiederum die Gefahr von Fehlern erhöht. Irgendeine meiner Änderungen kann falsch sein oder
nicht in den existierenden Code passen. Wer weiß das schon?
Schließlich, nachdem das Editieren, das erneute Debuggen und Testen abgeschlossen sind, muss
ich zwei Versionen unterstützen (wenn ich nicht die Unterstützung für die Originalversion aufgeben
kann). Das bedeutet zwei Quellen, die editiert werden müssen, wenn Bugs gefunden werden, und
eine Art Buchhaltung, um die beiden Systeme gleich zu halten.
Was passiert, wenn mein Chef eine weitere Klasse eingefügt haben möchte? (Mein Chef ist so.)
Ich muss nicht nur diesen Prozess wiederholen, ich habe dann auch drei Versionen.
Mit Polymorphie habe ich eine gute Chance, dass ich nur die neue Klasse einfügen und neu
kompilieren muss. Es kann sein, dass ich die Basisklasse selber ändern muss, das ist aber wenigstens
alles an einer Stelle. Änderungen an der Anwendung sollten wenige bis keine sein.
Das ist noch ein weiterer Grund, Datenelemente protected zu halten, und auf sie über als public
deklarierte Elementfunktionen zuzugreifen. Datenelemente können nicht durch Polymorphie in
einer Unterklasse überschrieben werden, so wie es für Elementfunktionen möglich ist.
Teil 4 – Sonntagmorgen
22.4 Wie funktioniert Polymorphie?.
Lektion 22
Nach allem, was ich bisher gesagt habe, kann es verwundern, dass in C++ die frühe Bindung die
Defaultmethode ist. Die Ausgabe des Programms AmbiguousBinding sieht wie folgt aus:
Der Wert von s.calcTuition bei
Aufruf durch fn() ist 0
Der Wert von gs.calcTuition bei
Aufruf durch fn() ist 0
Der Grund ist einfach. Polymorphie bedeutet ein wenig Mehraufwand, sowohl beim Speicherbe-
darf und beim Code, der den Aufruf ausführt. Die Erfinder von C++ waren in Sorge darüber, dass ein
solcher Mehraufwand ein Grund sein könnte, die Programmiersprache C++ nicht zu verwenden,
und so machten sie die frühe Bindung zur Defaultmethode.
Um Polymorphie anzuzeigen, muss der Programmierer das Schlüsselwort virtual verwenden,
wie im Programm LateBinding zu sehen ist, das Sie in Listing 22-3 finden.
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 258
258 Sonntagmorgen
class Student
{
public:
virtual double calcTuition()
{
return 0;
}
};
class GraduateStudent : public Student
{
public:
virtual double calcTuition()
{
return 1;
}
};
Das Schlüsselwort virtual, das der Deklaration von calcTuition( ) zugefügt wurde, erzeugt
eine virtuelle Elementfunktion. Das bedeutet, dass Aufrufe von calcTuition( ) spät gebunden
werden, wenn der Typ des aufrufenden Objektes nicht zur Compilezeit bestimmt werden kann.
Das Programm LateBindung enthält den gleichen Aufruf der Funktion fn( ) wie in den beiden
früheren Versionen. In dieser Version geht der Aufruf von calcTuition( ) an Student::calcTui-
tion( ), wenn fs ein Student ist und an GraduateStudent::calcTuition( ), wenn fs ein Gradu-
ateStudent ist.
Die Ausgabe von LateBinding sehen Sie unten. Die Funktion calcTuition( ) als virtuell zu
deklarieren, lässt fn( ) Aufrufe anhand des Laufzeittyps auflösen.
Der Wert von s.calcTuition bei
virtuellem Aufruf durch fn() ist 0
Der Wert von gs.calcTuition bei
virtuellem Aufruf durch fn() ist 1
Bei der Definition einer virtuellen Elementfunktion steht das Schlüsselwort virtual nur bei der
Deklaration und nicht bei der Definition, wie im folgenden Beispiel zu sehen ist:
class Student
{
public:
// deklariere als virtual
virtual double calcTuition()
{
return 0;
}
};
Teil 4 – Sonntagmorgen
// ‘virtual’ kommt in der Definition nicht vor
double Student::calcTuition()
{
return 0;
Lektion 22
}
260 Sonntagmorgen
#include <iostream.h>
class Base
{
public:
virtual void fn(int x)
{
cout << »In Base int x = »
<< x << »\n«;
}
};
class SubClass : public Base
{
public:
virtual void fn(float x)
{
cout << »In SubClass, float x = »
<< x << »\n«;
}
};
fn( ) in Base ist als fn(int) deklariert, während die Version in der Unterklasse als fn(float)
deklariert ist. Weil die Funktionen verschiedene Argumente haben, gibt es keine Polymorphie. Der
erste Aufruf geht an Base::fn(int) – das ist nicht verwunderlich, weil b vom Typ Base und i ein
int ist. Doch auch der nächste Aufruf geht an Base::fn(int), nachdem float in int konvertiert
wurde. Es wird kein Fehler erzeugt, weil dieses Programm legal ist (abgesehen von der Warnung, die
Konvertierung von f betreffend). Die Ausgabe eines Aufrufs von test( ) zeigt keine Polymorphie:
In Base, int x = 1
In Base, int x = 2
Die Argumente passen nicht exakt, es gibt keine späte Bindung – mit einer Ausnahme: Wenn die
Elementfunktion in der Basisklasse einen Zeiger oder eine Referenz auf ein Objekt der Basisklasse
zurückgibt, kann eine überschriebene Elementfunktion in einer Unterklasse einen Zeiger oder eine
Referenz auf ein Objekt der Unterklasse zurückgeben. Mit anderen Worten, das Folgende ist erlaubt:
class Base
{
public:
Base* fn();
};
In der Praxis ist das ganz natürlich. Wenn eine Funktion mit Subclass-Objekten umgeht, scheint
es natürlich zu sein, dass sie damit fortfährt.
Eine als virtual deklarierte Funktion kann nicht inline sein. Um eine Inline-Funktion zu expandie-
ren, muss der Compiler zur Compilezeit wissen, welche Funktion expandiert werden soll. Daher
waren auch alle Elementfunktionen, die Sie in den bisherigen Beispielen gesehen haben, outline
deklariert.
Konstruktoren können nicht virtual sein, weil es kein (fertiges) Objekt gibt, das zur Typbestim-
mung verwendet werden kann. Zum Zeitpunkt, an dem der Konstruktor aufgerufen wird, ist der
Speicher, der von dem Objekt belegt wird, nur eine formlose Masse. Erst nachdem der Konstruktor
fertig ist, ist das Objekt ein Element der Klasse im eigentlichen Sinne.
Im Vergleich dazu sollten Destruktoren normalerweise als virtual deklariert werden. Wenn nicht,
gehen Sie das Risiko ein, dass ein Objekt nicht korrekt vernichtet wird, wie in folgender Situation:
class Base
Teil 4 – Sonntagmorgen
{
public:
~Base();
Lektion 22
};
class SubClass : public Base
{
public:
~SubClass();
};
void finishWithObject(Base *pHeapObject)
{
// ... arbeite mit Objekt ...
// jetzt gib es an den Heap zurück
delete pHeapObject; // ruft ~Base() auf,
} // unabhängig von Laufzeit-
// typ von pHeapObject
Wenn der Zeiger, der an finishWithObject( ) tatsächlich auf ein Objekt aus der Klasse SubClass
zeigt, wird der SubClass-Destruktor trotzdem nicht korrekt aufgerufen. Den Destruktor virtuell zu
deklarieren, löst das Problem.
C++ Lektion 22 31.01.2001 12:44 Uhr Seite 262
262 Sonntagmorgen
Wann würden Sie also einen Destruktor nicht virtuell deklarieren? Es gibt nur eine solche Situa-
tion. Ich habe bereits erwähnt, dass virtuelle Funktionen einen gewissen »Mehraufwand« bedeuten.
Lassen Sie mich ein wenig genauer sein. Wenn der Programmierer die erste virtuelle Funktion in
einer Klasse definiert, fügt C++ einen zusätzlichen, versteckten Zeiger hinzu – nicht einen Zeiger pro
virtueller Funktion, nur einen Zeiger, falls die Klasse mindestens eine virtuelle Funktion besitzt. Eine
Klasse, die keine virtuellen Funktionen besitzt (und keine virtuellen Funktionen von einer Basisklasse
erbt), besitzt keinen solchen Zeiger.
Nun, ein Zeiger klingt nicht nach sehr viel und ist es auch nicht, es sei denn die folgenden zwei
Bedingungen sind erfüllt:
• Die Klasse hat nicht viele Datenelemente (so dass ein Zeiger viel ist im Vergleich zum Rest).
• Sie beabsichtigen, viele Objekte dieser Klasse zu erzeugen (ansonsten macht der zusätzliche Spei-
cher keinen Unterschied).
Wenn diese beiden Bedingungen erfüllt sind und Ihre Klasse nicht bereits eine
virtuelle Elementfunktion besitzt, können Sie Ihren Destruktor als nicht virtual dekla-
rieren.
Normalerweise sollten Sie aber den Destruktor virtual deklarieren. Wenn Sie das
mal nicht tun, dokumentieren Sie die Gründe dafür!
0 Min.
Zusammenfassung.
Vererbung an sich ist schön, ist aber begrenzt in ihren Möglichkeiten. In Kombination mit Polymor-
phie, ist Vererbung ein mächtiges Programmierwerkzeug.
• Elementfunktionen in einer Klasse können Elementfunktionen überschreiben, die in der Basisklas-
se definiert sind. Aufrufe dieser Funktionen werden zur Compilezeit aufgelöst basierend auf der
zur Compilezeit bekannten Klasse. Das wird frühe Bindung genannt.
• Eine Elementfunktion kann als virtual deklariert werden, wodurch Aufrufe basierend auf dem Lauf-
zeittyp aufgelöst werden. Das wird Polymorphie oder späte Bindung genannt.
• Aufrufe, von denen bekannt ist, dass der Laufzeittyp und der Compiletyp gleich sind, werden früh
gebunden, unabhängig davon, ob die Elementfunktion als virtual deklariert ist oder nicht.
Selbsttest.
1 Was ist Polymorphie? (Siehe »Einstieg in Polymorphie«)
2. Was ist ein anderes Wort für Polymorphie? (Siehe »Einstieg in Polymorphie«)
3. Was ist die Alternative und wie wird sie genannt? (Siehe »Elementfunktionen überschreiben«)
4. Nennen Sie drei Gründe, weshalb C++ Polymorphie enthält. (Siehe »Polymorphie und objekto-
rientierte Programmierung«)
5. Welches Schlüsselwort wird verwendet, um Elementfunktion polymorph zu deklarieren? (Siehe
»Wie funktioniert Polymorphie?«)
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 263
Checkliste.
✔ Gemeinsame Eigenschaften in eine Basisklasse faktorieren
✔ Abstrakte Klassen zur Speicherung faktorierter Informationen nutzen
✔ Abstrakte Klassen und dynamische Typen
B
is jetzt haben wir gesehen, wie Vererbung benutzt werden kann, um existie-
rende Klassen für neue Anwendungen zu erweitern. Vererbung verlangt
vom Programmierer die Fähigkeit, gleiche Eigenschaften verschiedener Klas-
sen zu kombinieren; dieser Prozess wird Faktorieren genannt.
30 Min.
23.1 Faktorieren.
Um zu sehen, wie Faktorieren funktioniert, lassen Sie uns die beiden Klassen Checking (Girokonto)
und Savings (Sparkonto) in einem hypothetischen Banksystem betrachten. Diese sind in Abbildung
23.1 grafisch dargestellt.
264 Sonntagmorgen
Um diese Abbildung und die folgenden Abbildungen lesen zu können, halten Sie im Gedächtnis,
dass
• die große Box eine Klasse ist, mit dem Klassennamen ganz oben,
• die Namen in den Boxen Elementfunktionen sind,
• die Namen ohne Box Datenelemente sind,
• die Namen, die teilweise außerhalb der Boxen liegen, öffentlich zugängliche Elemente sind; die
anderen protected deklariert sind.
• ein dicker Pfeil die Beziehung IS_A repräsentiert und
• ein dünner Pfeil die Beziehung HAS_A repräsentiert.
Abbildung 23.1 zeigt, dass die Klassen Checking und Savings vieles gemein haben. Weil sie
jedoch nicht identisch sind, müssen es zwei getrennte Klassen bleiben. Dennoch sollte es einen Weg
geben, Wiederholungen zu vermeiden.
Wir könnten eine der Klassen von der anderen erben lassen. Savings hat ein Extraelement, es
macht daher mehr Sinn, Savings von Checking abzuleiten, wie Sie in Abbildung 23.2 sehen, als
umgekehrt. Die Klasse wird vervollständigt durch das Hinzufügen des Datenelementes noWithdra-
wals und dem virtuellen Überladen der Elementfunktion withdrawal( ).
Obwohl die Lösung Arbeit einspart, ist sie nicht zufriedenstellend. Das Hauptproblem ist, dass es
die Wahrheit falsch darstellt. Diese Vererbungsbeziehung impliziert, dass ein Sparkonto (Savings)
ein spezieller Typ eines Girokontos (Checking) ist, was nicht der Fall ist.
»Na und?« werden Sie sagen. »Es funktioniert und spart Aufwand.« Das ist wahr, aber meine Vor-
behalte sind mehr als sprachliche Trivialitäten. Solche Fehldarstellungen verwirren den Programmie-
rer, den heutigen und den von morgen. Eines Tages wird ein Programmierer, der sich mit dem Pro-
gramm nicht auskennt, das Programm lesen, und verstehen müssen, was der Code macht.
Irreführende Tricks sind schwer zu durchschauen und zu verstehen.
Außerdem können solche Fehldarstellungen zu späteren Problemen führen. Nehmen Sie z.B. an,
dass die Bank ihre Policen für Girokonten ändert. Sagen wir, die Bank entscheidet, dass sie eine Bear-
beitungsgebühr nur dann verlangt, wenn der mittlere Kontostand im Monat unter einem gegebe-
nen Wert liegt.
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 265
Eine solche Änderung kann mit minimalen Änderungen an der Klasse Checking leicht durchge-
führt werden. Wir müssen ein neues Datenelement in die Klasse Checking einführen, das wir minim-
umBalance nennen wollen.
Das erzeugt aber ein Problem. Weil Savings von Checking erbt, bekommt Savings ebenfalls ein
solches Datenelement. Die Klasse hat aber für ein solches Element keine Verwendung, weil der mini-
male Kontostand das Sparkonto nicht beeinflusst. Ein zusätzliches Datenelement macht nicht so viel
aus, aber es verwirrt.
Änderungen wie diese akkumulieren sich. Heute ist es ein zusätzliches Datenelement, morgen ist
es eine geänderte Elementfunktion. Schließlich hat die Klasse Savings einen großen Ballast, der nur
auf die Klasse Checking angewendet werden kann.
Wie vermeiden wir dieses Problem? Wir können beide Klassen auf einer neuen Klasse basieren las-
sen, die speziell für diesen Einsatz gebaut ist; lassen Sie uns diese Klasse Account (=Konto) nennen.
Diese Klasse enthält alle Eigenschaften, die Savings und Checking enthalten, wie in Abbildung 1.3
zu sehen ist.
Wie löst das unser Problem? Erstens ist das eine viel treffendere Beschreibung der realen Welt
(was immer das ist). In meinem Konzept gibt es etwas, das als Konto bezeichnet wird. Girokonto
und Sparkonto sind Spezialisierungen dieses fundamentaleren Konzeptes.
Teil 5 – Sonntagmorgen
Lektion 23
Abbildung 23.3: Checking und Savings auf Klasse Account basieren lassen
Zusätzlich bleibt die Klasse Savings von Änderungen an der Klasse Checking unberührt (und
umgekehrt). Wenn die Bank eine grundlegende Änderung an allen Konten durchführen möchte,
können wir die Klasse Account modifizieren und alle abgeleiteten Klassen erben diese Änderung
automatisch. Aber wenn die Bank ihre Policen nur für Girokonten ändert, bleibt die Klasse Savings
von dieser Änderung verschont.
Dieser Prozess, gleiche Eigenschaften aus ähnlichen Klassen zu extrahieren, wird als Faktorieren
bezeichnet. Das ist ein wichtiges Feature objektorientierter Sprachen aus den bereits genannten
Gründen, plus einem neuen: Reduktion von Redundanz.
In Software ist nutzlose Masse eine üble Sache. Je mehr Code Sie generieren, desto mehr müssen
Sie auch debuggen. Es lohnt nicht, Nachtschichten einzulegen, um cleveren Code zu generieren,
der hier und da ein paar Zeilen Code einspart – diese Art Schlauheit entpuppt sich oft als Bumerang.
Aber das Faktorieren redundanter Information durch Vererbung kann den Programmieraufwand tat-
sächlich reduzieren.
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 266
266 Sonntagmorgen
Faktorieren ist nur zulässig, wenn die Vererbungsbeziehung der Realität ent-
!
Tipp
spricht. Zwei Klassen Mouse und Joystick zu faktorieren ist legitim, weil es beide
Klassen sind, die Zeigerhardware beschreiben. Zwei Klassen Mouse und Display zu
faktorieren, weil sie elementare Systemfunktionen des Betriebssystems benutzen,
ist nicht legitim – Maus und Bildschirm teilen keine Eigenschaft in der realen Welt.
Faktorieren kann, und wird es auch in der Regel, zu mehreren Abstraktionsstufen führen. Ein Pro-
gramm, das z.B. für eine fortschrittlichere Bank geschrieben wurde, könnte eine Klassenstruktur ent-
halten, wie in Abbildung 23.4 zu sehen ist.
Es wurde eine weitere Klasse zwischen den Klassen Checking und Savings und der allgemeine-
ren Klasse Account eingeführt. Diese Klasse Conventional enthält die Features konventioneller Kon-
tos. Andere Kontotypen wie z.B. Aktiondepots, sind ebenso vorgesehen.
Solche mehrarmigen Klassenstrukturen sind üblich und anzustreben, so lange ihre Beziehungen
die Wirklichkeit widerspiegeln. Es gibt jedoch nicht nur eine korrekte Klassenhierarchie für eine
gegebene Menge von Klassen.
Nehmen Sie an, dass unsere Bank es ihren Kunden ermöglicht, Girokonten und Aktiendepots
online zu verwalten. Transaktionen für andere Kontotypen können nur bei der Bank getätigt wer-
den. Obwohl die Klassenstruktur in Abbildung 23.4 natürlich erscheint, ist die Hierarchie in Abbil-
dung 23.5 mit dieser Information ebenfalls gerechtfertigt. Der Programmierer muss entscheiden,
welche Klassenhierarchie am besten zu den Daten passt, und zu der saubersten und natürlichsten
Implementierung führen wird.
Teil 5 – Sonntagmorgen
niveau – es gibt keine Instanz von Säugetier.
= =
Lektion 23
Das Konzept Säugetier unterscheidet sich grundlegend vom Konzept Hund.
= =
Hinweis
»Hund« ist ein Name, den wir einem existierenden Objekt gegeben haben. Es
gibt nichts in der realen Welt was nur Säugetier ist.
Wir möchten nicht, dass der Programmierer ein Objekt Account (=Konto) oder eine Klasse Mam-
mal (=Säugetier) erzeugt, weil wir nicht wissen, was wir damit anfangen sollen. Um diesem Problem
zu begegnen, erlaubt es C++ dem Programmierer, eine Klasse zu deklarieren, von der kein Objekt
instanziiert werden kann. Der einzige Sinn einer solchen Klasse ist, dass sie vererben kann.
Eine Klasse, die nicht instanziiert werden kann, heißt abstrakte Klasse.
23.2.1 Deklaration einer abstrakten Klasse
Eine abstrakte Klasse ist eine Klasse mit einer oder mehreren rein virtuellen Funktionen. Eine rein vir-
tuelle Funktion ist eine virtuelle Elementfunktion, die so markiert ist, dass sie keine Implementierung
besitzt.
Eine rein virtuelle Funktion hat keine Implementierung, weil wir nicht wissen, wie wir sie imple-
mentieren sollen. Z.B. wissen wir nicht, wie wir withdrawal( ) in einer Klasse Account ausführen
sollen. Das macht einfach keinen Sinn. Wir können jedoch nicht einfach die Definition von withdra-
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 268
268 Sonntagmorgen
wal( ) weglassen, weil C++ annehmen wird, dass wir vergessen haben, die Funktion zu definieren
und uns einen Linkfehler ausgeben wird, der uns mitteilt, dass eine Funktion fehlt (wahrscheinlich
vergessen).
Die Syntax zur Deklaration einer rein virtuellen Funktion – die C++ mitteilt, dass die Funktion kei-
ne Definition hat – wird in folgender Klasse Account demonstriert:
// Zugriffsfunktionen
int accountNo();
Account* first();
Account* next();
// Transaktionsfunktionen
virtual void deposit(float fAmount) = 0;
virtual void withdrawal(float fAmount) = 0;
protected:
// speichere Kontoobjekte in einer Liste, damit
// es keine Beschränkung der Anzahl gibt
static Account* pFirst;
Account* pNext;
Die =0 hinter der Deklaration von deposit( ) und withdrawal( ) zeigt an, dass der Program-
mierer nicht beabsichtigt, diese Funktionen zu definieren. Die Deklaration ist ein Platzhalter für die
Unterklassen. Von den konkreten Unterklassen von Account wird erwartet, dass sie diese Funktionen
mit konkreten Funktionen überladen.
=
= =
= Eine konkrete Elementfunktion ist eine Funktion, die nicht rein virtuell ist. Alle
Hinweis Elementfunktion vor dieser Sitzung waren konkret.
Eine abstrakte Klasse kann nicht mit einem Objekt instanziiert werden. D.h. sie können kein
Objekt aus einer abstrakten Klasse anlegen. Z.B. ist das Folgende nicht möglich:
void fn()
{
// deklariere ein Konto
Account acnt(1234); // das ist nicht erlaubt
acnt.withdrawal(50); // was soll das tun?
}
Wenn die Deklaration erlaubt wäre, würde das resultierende Objekt unvollständig sein, und eini-
ge Eigenschaften vermissen lassen. Was soll z.B. der obige Aufruf von acnt.withdrawal(50) tun? Es
gibt keine Funktion Account::withdrawal( ).
Abstrakte Klassen dienen als Basisklassen für andere Klassen. Ein Account enthält alle die Eigen-
schaften, die wir einem generischen Konto zuschreiben, die Möglichkeit des Abhebens und des Ein-
zahlens eingeschlossen. Wir können nur nicht definieren, wie ein generisches Konto solche Dinge
ausführt – es bleibt den Unterklassen, das zu definieren. Anders ausgedrückt, ein Konto ist so lange
kein Konto, bis der Benutzer Einzahlungen und Abhebungen machen kann, selbst wenn solche Ope-
rationen nur in speziellen Kontotypen definiert werden können, wie z.B. Girokonten und Sparkon-
ten.
Wir können weitere Typen vom Konten durch Ableitung von Account erzeugen, aber sie können
nicht durch ein Objekt instanziiert werden, so lange sie abstrakt bleiben.
Teil 5 – Sonntagmorgen
und withdrawal( ) mit perfekten Definitionen überlädt.
// Account – das ist eine abstrakte Basisklasse
Lektion 23
// für alle Kontoklassen
class Account
{
public:
Account(unsigned nAccNo);
// Zugriffsfunktionen
int accountNo();
Account* first();
Account* next();
// Transaktionsfunktionen
virtual void deposit(float fAmount) = 0;
virtual void withdrawal(float fAmount) = 0;
protected:
// speichere Kontoobjekte in einer Liste, damit
// es keine Beschränkung der Anzahl gibt
static Account* pFirst;
Account* pNext;
270 Sonntagmorgen
protected:
float fBalance;
};
Ein Objekt der Klasse Savings weiß, wie Einzahlungen und Abhebungen ausgeführt werden,
wenn sie aufgerufen werden. Das Folgende macht also Sinn:
void fn()
{
Savings s(1234);
s.deposit(100.0);
}
Die Klasse Account hat einen Konstruktor, obwohl sie abstrakt ist. Alle Konten
werden mit einer ID erzeugt. Die konkrete Kontoklasse Savings übergibt die ID
=
= =
= an die Basisklasse Account, während Sie den initialen Kontostand selbst über-
Hinweis nimmt. Das ist Teil unseres Objektmodells – die Klasse Account enthält das Ele-
ment für die Kontonummer, somit wird es Account überlassen, dieses Feld zu
initialisieren.
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 271
Teil 5 – Sonntagmorgen
: Account(nAccNo)
{
fBalance = fInitialBalance;
}
// Transaktionsfunktionen
// deposit – alle Geldkonten erwarten Lektion 23
// Einzahlungen als Betrag
virtual void deposit(float fAmount)
{
fBalance += fAmount;
}
// Zugriffsfunktionen
float balance()
{
return fBalance;
}
protected:
float fBalance;
};
272 Sonntagmorgen
{
public:
Savings(unsigned nAccNo,
float fInitialBalance = 0.0F)
: CashAccount(nAccNo, fInitialBalance)
{
// ... was immer Savings tun muss,
// was ein Account noch nicht getan hat ...
}
// fn – eine Testfunktion
void fn()
{
// eröffne ein Sparkonto mit $200 darauf
Savings savings(1234, 200);
// Einzahlung $100
savings.deposit(100);
Die Klasse CashAccout bleibt abstrakt, weil sie die Funktion deposit( ), aber nicht die Funktion
withdrawal( ) überlädt. Savings ist konkret, weil sie die verbleibende rein virtuelle Elementfunk-
tion überlädt.
Die Testfunktion fn( ) erzeugt ein Savings-Objekt, tätigt eine Einzahlung und dann eine Abhe-
bung.
void otherFn()
{
Savings s;
Hier wird pAccount als Zeiger auf Account deklariert. Die Funktion fn( ) darf pAccount->with-
drawal( ) aufrufen, weil alle Konten wissen, wie sie Auszahlungen vornehmen. Es ist aber auch klar,
dass die Funktion beim Aufruf die Adresse eines Objektes einer nichtabstrakten Unterklasse überge-
ben bekommt, wie z.B. Savings.
Es ist wichtig, hier darauf hinzuweisen, dass jedes Objekt, das fn( ) übergeben wird, entweder
aus Savings kommt, oder aus einer anderen nichtabstrakten Unterklasse von Account. Die Funktion
fn( ) kann sicher sein, dass wir niemals ein Objekt aus der Klasse Account übergeben werden, weil
wir so ein Objekt nie erzeugen können. Das Folgende kann also nie passieren, weil C++ es nicht
erlauben würde:
void otherFn()
{
// das Folgende ist nicht erlaubt, weil Account
// eine abstrakte Klasse ist
Account a;
fn(&a);
}
Der Schlüssel ist, dass es fn( ) erlaubt war, withdrawal( ) mit einem abstrakten Account-
Teil 5 – Sonntagmorgen
Objekt aufzurufen, weil jede konkrete Unterklasse von Account weiß, wie sie die Operation with-
drawal( ) ausführen muss.
=
= =
= Eine rein virtuelle Funktion stellt ein Versprechen dar, eine bestimmte Eigen- Lektion 23
Hinweis schaft in den konkreten Unterklassen zu implementieren.
274 Sonntagmorgen
Lassen Sie uns die folgenden kleineren Änderungen an Account ausführen, um das Problem zu
demonstrieren:
class Account
{
// wie zuvor, doch ohne Deklaration von
// withdrawal()
};
class Savings : public Account
{
public:
virtual void withdrawal(float fAmount);
};
Die Funktion otherFn( ) arbeitet wie zuvor. Wie vorher auch, versucht die Funktion fn( ) die
Funktion withdrawal( ) mit dem Account-Objekt aufzurufen, das sie erhält. Weil die Funktion
withdrawal( ) kein Element von Account ist, erzeugt der Compiler jedoch einen Fehler.
In diesem Fall hat die Klasse Account kein Versprechen abgegeben, eine Elementfunktion with-
drawal( ) zu implementieren. Es könnte eine konkrete Unterklasse von Account geben, die keine
solche Operation withdrawal( ) definiert. In diesem Fall hätte der Aufruf pAcc->withdrawal( )
keinen Zielort – das ist eine Möglichkeit, die C++ nicht akzeptieren kann.
Zusammenfassung.
Klassen von Objekten auf der Basis wachsender Gemeinsamkeiten in Hierarchien
aufzuteilen,, wird als Faktorieren bezeichnet. Faktorieren führt fast unausweichlich
zu Klassen, die eher konzeptionell als konkret sind. Ein Mensch ist ein Primat, ist ein
0 Min. Säugetier; die Klasse Mammal (=Säugetier) ist jedoch konzeptionell und nicht kon-
kret – es gibt keine Instanz von Mammal, die nicht zu einer bestimmten Spezies gehört.
Sie haben ein Beispiel dafür mit der Klasse Account gesehen. Während es Sparkonten und Giro-
konten gibt, gibt es kein Objekt, das einfach nur ein Konto ist. In C++ sagen wir, dass Account eine
abstrakte Klasse ist. Eine Klasse wird abstrakt, sobald eine ihrer Elementfunktionen keine Definition
besitzt. Eine Unterklasse wird konkret, wenn sie alle Eigenschaften definiert, die in der abstrakten
Basisklasse offengelassen wurden.
C++ Lektion 23 31.01.2001 12:44 Uhr Seite 275
• Eine Elementfunktion, die keine Implementierung hat, wird als rein virtuell bezeichnet. Rein vir-
tuelle Funktionen werden mit »=0« am Ende ihrer Deklaration bezeichnet. Rein virtuelle Funktio-
nen haben keine Definition.
• Eine Klasse, die eine oder mehrere rein virtuelle Funktionen enthält, wird als abstrakte Klasse
bezeichnet.
• Eine abstrakte Klasse kann nicht instanziiert werden.
• Eine abstrakte Klasse kann Basisklasse anderer Klassen sein.
• Unterklassen einer abstrakten Klasse werden konkret (d.h. nicht abstrakt), wenn sie alle rein vir-
tuellen Funktionen überschrieben haben, die sie erben.
Selbsttest.
1. Was ist Faktorieren? (Siehe »Faktorieren«)
2. Was ist das unterscheidende Merkmal einer abstrakten Klasse in C++? (Siehe »Deklaration einer
abstrakten Klasse«)
3. Wie erzeugen Sie aus einer abstrakten Klasse eine konkrete Klasse? (Siehe »Erzeugen einer konkre-
ten Klasse aus einer abstrakten Klasse«)
4. Warum ist es möglich, eine Funktion fn(MyClass*) zu deklarieren, wenn MyClass abstrakt ist?
(Siehe »Ein abstraktes Objekt an eine Funktion übergeben«)
Teil 5 – Sonntagmorgen
Lektion 23
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 276
24 Lektion Mehrfachvererbung
Checkliste.
✔ Mehrfachvererbung einführen
✔ Uneindeutigkeiten bei Mehrfachvererbung vermeiden
✔ Uneindeutigkeiten bei virtueller Vererbung vermeiden
✔ Ordnungsregeln für mehrere Konstruktoren wiederholen
I
n den bisher diskutierten Klassenhierarchien hat jede Klasse von einer einzelnen
Elternklasse geerbt. Das ist die Art, wie es auch normalerweise in der realen Welt
zugeht. Eine Mikrowelle ist ein Typ Ofen. Man könnte argumentieren, dass eine
Mikrowellen Gemeinsamkeiten mit einem Radar hat, der auch Mikrowellen verwen-
det, aber das ist wirklich ein bißchen weit hergeholt.
30 Min.
Einige Klassen jedoch stellen die Vereinigung zweier Klassen dar. Ein Beispiel
einer solchen Klasse ist das Schlafsofa. Wie der Name bereits impliziert, ist es ein Sofa und auch ein
Bett (wenn auch kein sehr komfortables). Somit sollte das Schlafsofa Eigenschaften eines Bettes und
Eigenschaften eines Sofas erben. Um dieser Situation zu begegnen, erlaubt es C++, eine Klasse von
mehr als einer Basisklasse abzuleiten. Das wird Mehrfachvererbung genannt.
Der Code zur Implementierung von SleeperSofa sieht wie folgt aus:
class Bed
{
public:
Bed()
{
cout << »Teil Bett\n«;
Teil 5 – Sonntagmorgen
}
void sleep()
{
cout << »Versuche zu schlafen\n«;
Lektion 24
}
int weight;
};
class Sofa
{
public:
Sofa()
{
cout << »Teil Sofa\n«;
}
void watchTV()
{
cout << »Sehe fern\n«;
}
int weight;
};
278 Sonntagmorgen
int main()
{
SleeperSofa ss;
// sie können auf einem Schlafsofa fernsehen ...
ss.watchTV(); // Sofa::watchTV()
//... und Sie können es ausklappen ...
ss.foldOut(); // SleeperSofa::foldOut()
//... und darauf schlafen (irgendwie)
ss.sleep(); // Bed::sleep()
return 0;
}
Die Namen der beiden Klassen – Bed und Sofa – kommen nach dem Namen SleeperSofa, was
anzeigt, dass SleeperSofa die Elemente der beiden Basisklassen erbt. Somit sind beide Aufrufe
ss.sleep( ) und ss.watchTV( ) gültig. Sie können ein SleeperSofa entweder als Bed oder als
Sofa benutzen. Zusätzlich kann die Klasse SleeperSofa eigene Elemente haben, wie z.B. foldOut( ).
Die Ausführung des Programms liefert die folgende Ausgabe:
Teil Bett
Teil Sofa
Zusammenfügen der beiden
Sieh fern
Klappe das Bett aus
Versuche zu schlafen
Der Teil Bett des Schlafsofas wird zuerst konstruiert, weil die Klasse Bed zuerst in der Klassenliste
steht, von denen SleeperSofa erbt (es hängt nicht an der Reihenfolge, in der die Klassen definiert
sind). Danach wird der Teil Sofa des Schlafsofas konstruiert. Schließlich legt SleeperSofa selber los.
Nachdem ein SleeperSofa-Objekt erzeugt wurde, greift main( ) nacheinander auf die Element-
funktionen zu – erst wird ferngesehen auf dem Sofa, dann wird das Sofa umgebaut, und dann wird
auf dem Sofa geschlafen. (Offensichtlich hätten die Elementfunktionen in jeder Reihenfolge aufge-
rufen werden können.)
Die Antwort ist »beide«. SleeperSofa erbt ein Element Bed::weight und ein Element
Sofa::weight. Weil sie den gleichen Namen haben, sind unqualifizierte Referenzen uneindeutig.
Der folgende Schnipsel demonstriert das Prinzip:
int main()
{
// gib das Gewicht eines Schlafsofas aus
SleeperSofa ss;
cout << »Gewicht des Schlafsofas = »
<< ss.weight // das funktioniert nicht!
<< »\n«;
return 0;
}
Das Programm muss eine der beiden Gewichtsangaben über den entsprechenden Namen der
Basisklasse ansprechen. Der folgende Codeschnipsel ist korrekt:
#include <iostream.h>
void fn()
{
SleeperSofa ss;
cout << »Gewicht des Schlafsofas = »
<< ss.Sofa::weight // Angabe, welches weight
<< »\n«;
}
Obwohl diese Lösung das Problem löst, ist die Angabe einer Basisklasse in einer Anwendungs-
funktion nicht wünschenswert, weil dadurch Informationen über die Klasse in den Anwendungsco-
de verlagert werden. In diesem Fall muss fn( ) wissen, dass SleeperSofa von Sofa erbt.
Teil 5 – Sonntagmorgen
Diese Typen sogenannter Kollisionen können bei einfacher Vererbung nicht auftreten, sind aber
bei Mehrfachvererbung eine ständige Gefahr.
Lektion 24
24.3 Virtuelle Vererbung.
Im Falle von SleeperSofa, war die Kollision der Elemente weight mehr, als nur ein
Unfall. Ein SleeperSofa hat kein Bettgewicht, unabhängig von seinem Sofagewicht
– es hat nur ein Gewicht. Die Kollision entsteht, weil diese Klassenhierarchie die rea-
20 Min. le Welt nicht vollständig beschreibt. Insbesondere wurden die Klassen nicht voll-
ständig faktoriert.
Wenn man etwas mehr nachdenkt, wird klar, dass Betten und Sofas Spezialfälle eines grundle-
genderen Konzeptes sind: Möbel. (Natürlich könnte ich das Konzept noch viel fundamentaler
machen z.B. mit einer Klasse ObjectWithMass (=Objekte mit Masse), aber Furniture (=Möbel) ist
fundamental genug.) Gewicht ist eine Eigenschaft von allen Möbelstücken. Diese Beziehung ist in
Abbildung 24.2 zu sehen:
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 280
280 Sonntagmorgen
Furniture
weight
Bed Sofa
sleep() watchTV()
Sleeper Sofa
foldOut()
Die Klasse Furniture zu faktorieren sollte die Namenkollision auflösen. Sehr erleichtert, und mit
großer Hoffnung auf Erfolg, realisiere ich die folgende C++-Hierarchie im Programm AmbiguousIn-
heritance:
// AmbiguousInheritance – beide Klassen Bed und Sofa
// können von einer Klasse
// Furniture erben
#include <stdio.h>
#include <iostream.h>
class Furniture
{
public:
Furniture()
{
cout << »Erzeugen des Konzeptes Furniture«;
}
int weight;
};
int main()
Teil 5 – Sonntagmorgen
{
// Ausgabe des Gewichts eines Schlafsofas
SleeperSofa ss;
Lektion 24
cout << »Gewicht des Schlafsofas = »
<< ss.weight // das funktioniert nicht!
<< »\n«;
return 0;
}
Unglücklicherweise hilft das gar nicht – weight ist immer noch uneindeutig. »OK«, sage ich
(wobei ich nicht wirklich verstehe, warum weight immer noch uneindeutig ist), »Ich werde es ein-
fach nach Furniture casten«.
#include <iostream.h>
void fn()
{
SleeperSofa ss;
Furniture *pF;
pF = (Furniture*)&ss; // nutze Zeiger auf
// Furniture...
cout << »weight = » //... um an das Gewicht
<< pF->weight // zu kommen
<< »\n«;
};
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 282
282 Sonntagmorgen
Auch das funktioniert nicht. Jetzt bekomme ich eine Fehlermeldung, dass der Cast von Sleeper-
Sofa* nach Furniture* uneindeutig ist. Was geht hier eigentlich vor?
Die Erklärung ist einfach. SleeperSofa erbt nicht direkt von Furniture. Beide Klassen, Bed und
Sofa, erben von Furniture, und SleeperSofa erbt dann von beiden. Im Speicher sieht ein Slee-
perSofa-Objekt wie in Abbildung 24.3 aus:
Sie sehen, dass SleeperSofa aus einem vollständigen Bed besteht, gefolgt von einem vollständi-
gen Sofa, gefolgt von Dingen, die für SleeperSofa spezifisch sind. Jedes dieser Teilobjekte in Slee-
perSofa hat seinen eigenen Furniture-Teil, weil jeder von Furniture erbt. Somit enthält ein Slee-
perSofa zwei Furniture-Objekte.
Ich habe nicht die Hierarchie in Abbildung 24.2 erzeugt, sondern eine Hierarchie, wie sie in
Abbildung 24.4 zu sehen ist.
Abbildung 24.4: Tatsächliches Ergebnis des ersten Faktorierens von Bed und Sofa.
Das ist aber Unsinn. SlepperSofa braucht nur eine Kopie von Furniture. Ich möchte, dass
SleeperSofa nur eine Kopie von Furniture erbt, somit möchte ich, dass Bed und Sofa sich diese
eine Kopie teilen.
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 283
C++ nennt das virtuelle Vererbung, weil sie das Schlüsselwort virtual verwendet.
=
= =
= Ich mag dieses Überladen des Begriffs virtual nicht, weil virtuelle Vererbung
Hinweis nichts mit virtuellen Funktionen zu tun hat.
Ich kehre zur Klasse SleeperSofa zurück, und implementiere sie wie folgt:
class Furniture
{
public:
Furniture()
{
cout << »Erzeugen des Konzeptes Furniture«;
}
int weight;
};
Teil 5 – Sonntagmorgen
Bed()
{
cout << »Teil Bett\n«;
}
Lektion 24
void sleep()
{
cout << »Versuche zu schlafen\n«;
}
int weight;
};
284 Sonntagmorgen
int main()
{
// Ausgabe des Gewichts eines Schlafsofas
SleeperSofa ss;
cout << »Gewicht des Schlafsofas = «
<< ss.weight // das funktioniert!
<< »\n«;
return 0;
}
Beachten Sie, dass das Schlüsselwort virtual bei der Vererbung von Furniture in Bed und Sofa
eingefügt wurde. Das drückt aus »Gib mir eine Kopie von Furniture, wenn noch keine solche vor-
handen ist, ansonsten verwende diese.« Ein SleeperSofa sieht im Speicher schließlich so aus:
Ein SleeperSofa erbt von Furniture, dann von Bed minus Funiture, gefolgt von Sofa minus
Furniture. Dadurch werden die Elemente in SleeperSofa eindeutig. (Das muss nicht die Reihen-
folge der Elemente im Speicher sein, das ist aber für unsere Zwecke auch nicht wichtig.)
Die Referenz in main( ) auf weight ist nicht länger uneindeutig, weil ein SleeperSofa nur eine
Kopie von Furniture enthält. Indem von Furniture virtuell geerbt wird, bekommen sie die Verer-
bungsbeziehung wie in Abbildung 24.2.
Wenn virtuelle Vererbung das Problem so schön löst, warum wird sie dann nicht immer verwen-
det? Dafür gibt es zwei Gründe. Erstens werden virtuell geerbte Basisklassen intern sehr verschieden
von normal geerbten Klassen behandelt und diese Unterschiede schließen einen Mehraufwand ein.
(Kein sehr großer Mehraufwand, aber die Erfinder von C++ waren fast paranoid, wenn es um Mehr-
aufwand ging.) Zweitens möchten Sie manchmal zwei Kopien der Basisklasse haben (obwohl das
unüblich ist).
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 285
=
= =
= Ich denke, virtuelle Vererbung sollte die Regel sein.
Hinweis
Als ein Beispiel für einen Fall, in dem sie keine virtuelle Vererbung haben möchten, betrachten Sie
das Beispiel TeacherAssistent, der gleichzeitig Student und Teacher ist; beide Klassen sind Unter-
klassen von Academician. Wenn die Universität jedem TeachingAssistent zwei IDs gibt – eine Stu-
dent-ID und eine Teacher-ID – muss die Klasse TeacherAssistant zwei Kopien der Klasse Academi-
cian enthalten.
Teil 5 – Sonntagmorgen
mentobjekte in der Klasse erscheinen.
• Schließlich wird der Konstruktor der Klasse selber aufgerufen.
• Basisklassen werden in der Reihenfolge konstruiert, in der von ihnen geerbt wird, und nicht in der
Lektion 24
Reihenfolge in der Konstruktorzeile.
286 Sonntagmorgen
#include <iostream.h>
class Base1 {int mem;};
class Base2 {int mem;};
class SubClass : public Base1, public Base2 {};
pB1 und pB2 sind nummerisch nicht gleich, obwohl sie vom selben Originalwert pSC kommen.
Aber die Meldung »Elemente nummerisch gleich« kommt nicht. (Tatsächlich wird fn( ) eine Null
übergeben, weil C++ diese Konvertierungen auf null nicht ausführt. Sehen Sie, wie merkwürdig das
werden kann?)
Zusammenfassung.
Ich empfehle Ihnen, Mehrfachvererbung nicht zu benutzen, bis sie C++ gut beherr-
schen. Einfache Vererbung stellt bereits genügend Ausdrucksstärke bereit, die
genutzt werden kann. Später können sie die Handbücher studieren, bis Sie ganz
0 Min. sicher sind, dass Sie verstehen, was passiert, wenn Sie Mehrfachvererbung verwen-
den. Eine Ausnahme ist die Verwendung der Microsoft Foundation Classes (MFC), die Mehrfachve-
rerbung nutzen. Diese Klassen wurden getestet und sind sicher. (Sie werden im Allgemeinen nicht
einmal merken, dass Bibliotheken wie MFC Mehrfachvererbung nutzen.)
• Eine Klasse kann von mehr als einer Klasse erben, indem deren Klassennamen durch Kommata
getrennt hinter dem »:« stehen. Obwohl in den Beispielen nur zwei Basisklassen verwendet wur-
den, gibt es keine Beschränkung für die Anzahl der Basisklassen. Vererbung von mehr als zwei
Basisklassen ist jedoch sehr ungewöhnlich.
• Elemente, die in den Basisklassen gleich sind, sind in der Unterklasse uneindeutig. D.h. wenn bei-
de, BaseClass1 und BaseClass2 eine Elementfunktion f( ) enthalten, dann ist f( ) uneindeutig
in SubClass.
• Uneindeutigkeiten in den Basisklassen können über einen Klassenanzeiger aufgelöst werden, d.h.
eine Unterklasse kann sich auf BaseClass1::f( ) und BaseClass2::f( ) beziehen.
• Wenn beide Basisklassen von einer gemeinsamen Basisklasse abgeleitet sind, in der gemeinsame
Eigenschaften faktoriert sind, kann das Problem mit virtueller Vererbung gelöst werden.
C++ Lektion 24 31.01.2001 12:46 Uhr Seite 287
Selbsttest.
1. Was könnten wir als Basisklassen für eine Klasse wie CombinationPrinterCopier benutzen? (Ein
Druck-Kopierer ist ein Laserdrucker, der auch als Kopierer verwendet werden kann.) (Siehe Einlei-
tungsabschnitt)
2. Vervollständigen Sie die folgende Klassenbeschreibung, indem Sie die Fragezeichen ersetzen:
class Printer
{
public:
int nVoltage;
// ... weitere Elemente ...
}
class Copier
{
public:
int nVolatage;
// ... weitere Elemente ...
}
class CombinationPinterCopier ?????
{
// .... Weiteres ...
}
3. Was ist das Hauptproblem beim Zugriff auf voltage eines CombinationPrinterCopier-Objek-
tes? (Siehe »Uneindeutigkeiten bei Vererbung«)
4. Gegeben, dass beide, Printer und Copier, ElectronicEquipment sind, was kann getan wer-
den, um das voltage-Problem zu lösen? (Siehe »Virtuelle Vererbung«)
Teil 5 – Sonntagmorgen
5. Nennen Sie einige Gründe, warum Mehrfachvererbung keine gute Sache sein kann. (Siehe »Eine
Meinung dagegen«)
Lektion 24
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 288
Checkliste.
✔ Programme in mehrere Module teilen
✔ Die #include-Direktive verwenden
✔ Dateien einem Projekt hinzufügen
✔ Andere Kommandos des Präprozessors
A
lle bisherigen Programme waren klein genug, um sie in eine einzige .cpp-
Datei zu schreiben. Das ist für die Beispiele in einem Buch wie C++-Wochen-
end-Crashkurs in Ordnung, wäre aber für reale Anwendung eine ernsthafte
Beschränkung. Diese Sitzung untersucht, wie ein Programm in mehrere Teile aufge-
teilt werden kann durch die clevere Verwendung von Projekt- und Include-Dateien.
30 Min.
=
= =
= Der Prozess, separat kompilierte Module zu einer ausführbaren Datei
Hinweis zusammenzufügen, wird als Linken bezeichnet.
Es gibt mehrere Gründe dafür, ein Programm in handlichere Teile zu teilen. Erstens ermöglicht
das Teilen in Module eine höhere Kapselung. Klassen mauern ihre Elemente ein, um einen gewissen
Grad von Sicherheit zu erreichen. Programme können dasselbe mit Funktionen tun.
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 289
=
= =
= Erinnern Sie sich daran, dass Kapselung einer der Vorteile von objektorientierter
Hinweis Programmierung ist.
Zweitens ist ein Programm, das aus einer Anzahl gut durchdachter Module besteht, leichter zu
verstehen, und damit leichter zu schreiben und zu debuggen, als ein Programm, das nur eine Quell-
datei besitzt, in der alle Klassen und Funktionen enthalten sind.
Dann kommt die Wiederverwendung. Ich habe das Argument der Wiederverwendbarkeit
gebraucht, um Ihnen die objektorientierte Programmierung zu verkaufen. Es ist extrem schwierig,
eine einzelne Klasse zu pflegen, die in mehreren Programmen verwendet wird, wenn jedes Pro-
gramm seine eigene Kopie der Klasse enthält. Es ist viel besser, wenn ein einziges Klassenmodul
automatisch von den Programmen geteilt wird.
Schließlich gibt es noch ein Zeitargument. Compiler wie Visual C++ oder GNU C++ brauchen
nicht sehr lange für das Kompilieren der Beispielprogramme in diesem Buch auf einem so schnellem
Rechner wie dem Ihren. Kommerzielle Programme bestehen manchmal aus einigen Millionen Zeilen
Quelltext. Ein solches Programm zu erzeugen kann mehr aus 24 Stunden in Anspruch nehmen! (Fast
so lange wie sie benötigen, dieses Buch zu lesen!) Kein Programmierer würde es hinnehmen, ein sol-
ches Programm wegen jeder kleinen Änderung neu kompilieren zu müssen. Die Zeit zum Kompilie-
ren ist wesentlich länger als die Zeit zum Linken.
Teil 5 – Sonntagmorgen
programm.
Dieser Abschnitt beginnt mit dem Beispiel EarlyBinding aus Sitzung 22, und trennt die Definition der
Lektion 25
Klasse Student vom Rest des Programms. Um Verwechslungen zu vermeiden, lassen Sie uns das
Ergebnis SeparatedClass nennen.
290 Sonntagmorgen
{
// weil calcTuition() virtual deklariert ist,
// wird der Laufzeittyp von fs verwendet, um
// den Aufruf aufzulösen
return fs.calcTuition();
}
Unglücklicherweise kann das Modul nicht erfolgreich kompiliert werden, weil nichts in Separa-
tedClass.cpp die Klasse Student definiert. Wir könnten natürlich die Definition von Student wieder
in die Datei SeparatedClass.cpp einfügen, aber das ist nicht, was wir wollen. Wir würden damit dort-
hin zurückkehren, wo wir hergekommen sind.
protected:
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 291
int nID;
};
class GraduateStudent : public Student
{
public:
virtual double calcTuition()
{
return 1;
}
protected:
int nGradId;
};
Die neue Version der Quelldatei SeparatedClass.cpp unserer Anwendung sieht wie folgt aus:
Teil 5 – Sonntagmorgen
#include <stdio.h>
#include <iostream.h>
#include »student.h«
!
Tipp
Die #include-Direktive muss in der ersten Spalte beginnen und darf nur eine
Zeile umfassen.
Wenn Sie den Inhalt von student.h physikalisch in die Datei SeparatedClass.cpp einfügen, kom-
men Sie zu der gleichen Datei LateBinding.cpp, mit der wir gestartet sind. Das ist genau das, was
während des Erzeugungsprozesses passiert – C++ fügt student.h in SeparatedClass.cpp ein und
kompiliert das Ergebnis.
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 292
292 Sonntagmorgen
Die #include-Direktive hat nicht die gleiche Syntax wie die anderen C++-Kom-
=
= =
=
mandos. Das liegt daran, dass es überhaupt keine C++-Direktive ist. Ein Prä-
prozessor geht zuerst über das C++-Programm, bevor der C++-Compiler mit
Hinweis der Ausführung beginnt. Es ist der Präprozessor, der die #include-Direktive
interpretiert.
#include »student.h«
double fn(Student& fs)
{
// weil calcTuition() virtual deklariert ist,
// wird der Laufzeittyp von fs verwendet, um
// den Aufruf aufzulösen
return fs.calcTuition();
}
#include »student.h«
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 293
Beide Quelldateien binden die gleichen .h-Dateien ein, weil beide Zugriff auf die Definition der
Klasse Student und der Funktionen in der C++-Standardbibliothek benötigen.
25.2.4 Projektdatei
Voller Erwartung öffne ich die Datei SeparatedMain.cpp und wähle »Build« aus.
Teil 5 – Sonntagmorgen
10 Min.
! Wenn Sie das zu Hause versuchen. stellen Sie sicher, dass sie die Projektdatei Lektion 25
SeparatedClass geschlossen haben.
Tipp
Eine Fehlermeldung »undeclared identifier« erscheint. C++ weiß nicht, was fn( ) ist, wenn Sepa-
ratedMain.cpp kompiliert wird. Das macht Sinn, weil die Definition von fn( ) in einer anderen Datei
steht.
Natürlich muss ich eine Prototypdeklaration von fn( ) in die Quelldatei SeparatedMain.cpp ein-
fügen:
Die resultierende Quelldatei lässt sich kompilieren, erzeugt aber während des Linkens einen Feh-
ler, dass die Funktion fn(Student) in den .o-Dateien nicht gefunden werden kann.
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 294
294 Sonntagmorgen
=
= =
=
Ich könnte (und sollte wahrscheinlich auch) einen Prototyp von main( ) in die
Datei SeparatedFn.cpp einfügen; das ist jedoch nicht nötig, weil fn( ) die
Hinweis Funktion main( ) nicht aufruft.
Was benötigt wird, ist eine Möglichkeit, C++ mitzuteilen, beide Quelldateien im gleichen Pro-
gramm zusammenzubinden. Solch eine Datei wird Projektdatei genannt. Es gibt mehrere Wege, wie
eine Projektdatei angelegt werden kann. Die Techniken unterscheiden sich in den zwei Compilern.
=
= =
= Ich habe nicht behauptet, dies sei der eleganteste Weg. Aber es ist der
Hinweis einfachste.
Abbildung 25.1: Klicken Sie auf den rechten Mausbutton, um Dateien in das Projekt einzufügen.
!
Die Projektdatei SeparatedMain auf der beiliegenden CD-ROM enthält bereits
beide Quelldateien.
Tipp
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 295
Teil 5 – Sonntagmorgen
Lektion 25
Abbildung 25.2: Die rhide-Umgebung zeigt die kompilierten Dateien und das erzeugte Programm an.
296 Sonntagmorgen
beiden nicht synchronisiert werden – eine Definition könnte geändert werden und die andere nicht.
Die Definition von Student in eine einzige Datei zu schreiben und diese Datei in zwei Module
einzubinden, macht die Entstehung verschiedener Definitionen unmöglich.
protected:
int nID;
};
Ein Problem tritt auf, wenn der Programmierer beide Dateien, die Klasse und die Elementfunktio-
nen, in die gleiche .h-Datei einzubinden versucht. Die Funktion Student::calcTuition( ) wird ein
Teil von SeparatedMain.o und SeparatedFn.o. Wenn diese Dateien gelinkt werden, wird sich der
C++-Linker darüber beschweren, dass calcTuition( ) zweimal definiert ist.
Wenn eine Elementfunktion innerhalb einer Klasse definiert ist, unternimmt C++
=
= =
= gewisse Anstrengungen, Doppeldefinitionen von Funktionen zu vermeiden. C++
Hinweis kann dieses Problem nicht verhindern, wenn die Elementfunktion außerhalb der
Klasse definiert ist.
Externe Elementfunktionen müssen in ihrer eigenen .cpp-Datei definiert werden, wie in der fol-
genden Datei Student.cpp:
#include »student.h«
// definiere den Code separat von der Klasse
double Student::calcTuition();
{
return 0;
}
C++ Lektion 25 31.01.2001 12:47 Uhr Seite 297
Zusammenfassung.
Diese Sitzung hat Ihnen gezeigt, wie der Programmierer Programme in mehrere
Quelldateien aufspalten kann. Kleinere Quelldateien sparen Zeit zur Programmge-
nerierung, weil der Programmierer nur die Sourcemodule kompilieren muss, die
0 Min. tatsächlich geändert wurden.
• Separat kompilierte Module steigern die Kapselung von Paketen ähnlicher Funktionen. Wie Sie
bereits gesehen haben, sind separate, gekapselte Pakete, einfacher zu schreiben und zu debug-
gen. Die C++-Standardbibliothek ist ein solches gekapseltes Paket.
• Der Generierungsprozess besteht aus zwei Phasen. In der ersten, der Kompilierungsphase, werden
die C++-Anweisungen in maschinenlesbare, aber unvollständige Objektdateien übersetzt. In der
zweiten Phase, der Linkphase, werden diese Objektdateien zu einer einzigen ausführbaren Datei
zusammengefügt.
• Deklarationen, Klassendefinitionen eingeschlossen, müssen zusammen mit jeder C++-Quelldatei
kompiliert werden, die diese Funktion oder Klasse deklariert. Der einfachste Weg, dies zu bewerk-
stelligen, ist der verwandte Deklarationen in eine .h-Datei zu schreiben, die dann in den .cpp-
Quelldateien mittels einer #include-Direktive eingebunden wird.
• Die Projektdatei listet die Module auf, die das Programm bilden. Die Projektdatei enthält weitere
programmspezifische Einstellungen, die Einfluss darauf haben, wie die C++-Umgebung das Pro-
gramm erzeugt.
Selbsttest.
Teil 5 – Sonntagmorgen
1. Wie wird eine C++-Quelldatei in eine maschinenlesbare Objektdatei überführt? Wie nennt man
diesen Vorgang? (Siehe »Warum Programme aufteilen?«)
2. Wie nennt man den Prozess, der diese Objektdateien zu einer einzigen ausführbaren Datei
Lektion 25
zusammenfügt? (Siehe »Warum Programme aufteilen?«)
3. Welche Aufgaben hat die Projektdatei? (Siehe »Projektdatei«)
4. Was ist die Hauptaufgabe der #include-Direktive? (Siehe »Die #include-Direktive«)
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 298
26 Lektion C++-Präprozessor
Checkliste.
✔ Häufig benutzte Konstanten mit Namen versehen
✔ Compilezeitmakros definieren
✔ Den Kompilierungsprozess kontrollieren
D
ie Programme in Sitzung 25 nutzten die #include-Direktive des Präprozes-
sors, um die Definition von Klassen in mehrere Quelldateien einzubinden,
die gemeinsam das Programm bildeten. In der Tat haben alle bisher gesehe-
nen Programme stdio.h und iostream.h eingebunden, in denen Funktionen der
30 Min. Standardbibliothek definiert sind. In dieser Sitzung untersuchen wir die #include-
Direktive in Verbindung mit anderen Präprozessorkommandos.
=
= =
=
Die Programmiersprache C nutzt den gleichen Präprozessor, so dass alles, was
wir hier über den C++-Präprozessor sagen, auch für C gilt.
Hinweis
!
Tipp
Die Include-Datei muss nicht mit .h enden, aber es kann den Programmierer
und den Präprozessor verwirren, wenn sie das nicht tut.
Die Include-Datei sollte keine C++-Funktionen enthalten, weil sie separat durch das Modul
expandiert und kompiliert werden, das die Datei einbindet. Der Inhalt der Include-Datei sollte auf
Teil 5 – Sonntagmorgen
die Klassendefinition, Definition von globalen Variablen und andere Präprozessor-Direktiven
beschränkt sein.
Lektion 26
26.3 Die Direktive #define.
Die Direktive #define definiert eine Konstante oder ein Makro. Das folgende Bei-
spiel zeigt, wie #define zur Definition einer Konstanten gebraucht wird:
20 Min.
300 Sonntagmorgen
!
Das Beispiel demonstriert die Namenskonvention für #define-Konstanten.
Namen werden in Großbuchstaben mit Unterstrichen zur Trennung geschrie-
Tipp ben.
Wenn sie auf diese Weise verwendet wird, ermöglicht es die #define-Direktive dem Program-
mierer, konstante Werte mit aussagekräftigen Namen zu versehen; MAX_NAME_LENGTH sagt dem Pro-
grammierer mehr als 256. Konstanten auf diese Weise zu definieren, macht Programme leichter
modifizierbar. Z.B. kann die maximale Anzahl Zeichen in einem Namen programmweit limitiert sein.
Diesen Wert von 256 auf 128 zu ändern ist einfach, indem nur das #define-Kommando geändert
werden muss, unabhängig davon, an wie vielen Stellen die Konstante verwendet wird.
#define square(x) x * x
void fn()
{
int nSquareOfTwo = square(2);
// ... usw. ...
}
Der Präprozessor macht hieraus:
void fn()
{
int nSquareOfTwo = 2 * 2;
// ... usw. ...
}
#define square(x) x * x
void fn()
{
int nSquareOfTwo = square(1 + 1);
}
Der Präprozessor generiert hieraus :
void fn()
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 301
{
int nSquareOfTwo = 1 + 1 * 1 + 1;
}
Weil Multiplikation Vorrang vor Addition hat, wird der Ausdruck interpretiert, als wenn er so
geschrieben wäre:
void fn()
{
int nSquareOfTwo = 1 + (1 * 1) + 1;
}
Der Ergebniswert von nSquareOf ist 3 und nicht 4.
Eine vollständige Qualifizierung des Makros durch großzügige Verwendung von Klammern
schafft Abhilfe, weil Klammern die Reihenfolge der Auswertung kontrollieren. Mit einer Definition
von square in der folgenden Weise gibt es keine Probleme:
Doch auch das löst das Problem nicht in jedem Falle. Z.B. kann das Folgende nicht zum Laufen
gebracht werden:
Teil 5 – Sonntagmorgen
Sie können erwarten, dass nV2 den Ergebniswert 4 statt 6 und nV1 den Wert 3 statt 4 erhält
wegen der folgenden Expansion des Makros:
Lektion 26
void fn()
{
int nV1 = 2;
int nV2;
nV2 = nV1++ * nV1++;
}
Makros sind nicht typsicher. Das kann in Ausdrücken mit unterschiedlichen Typen zu Verwirrun-
gen führen:
Weil nSquareOfTwo ein int ist, könnten Sie erwarten, dass der Ergebniswert 4 statt dem tatsäch-
lichen Wert 6 (2.5 * 2.5 = 6.25) ist.
C++-Inline-Funktionen vermeiden das Problem mit den Makros.
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 302
302 Sonntagmorgen
Die Inline-Version von square( ) erzeugt nicht mehr Code als ein Makro, hat aber nicht die
Nachteile der Präprozessor-Variante.
26.4 Compilerkontrolle.
Der Präprozessor stellt auch Möglichkeiten bereit, den Compilevorgang zu steuern.
10 Min.
26.4.1 Die #if-Direktive
Die Präprozessor-Direktive, die C++ am ähnlichsten ist, ist die #if-Anweisung. Wenn der konstante
Ausdruck nach dem #if nicht gleich null ist, werden alle Anweisungen bis zu #else an den Compi-
ler übergeben. Wenn der Ausdruck null ist, werden die Anweisungen zwischen #else und #endif
übergeben. Der #else-Zweig ist optional. Z.B.:
#define SOME_VALUE 1
#if SOME_VALUE
int n = 1;
#else
int n = 2;
#endif
wird konvertiert in
int n = 1;
!
Tipp
Denken Sie daran, dass dies Entscheidungen zur Compilezeit sind und keine
Laufzeitentscheidungen. Die Ausdrücke nach #if enthalten Konstanten und
#define-Direktiven – Variablen und Funktionsaufrufe sind nicht erlaubt.
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 303
#define SOME_VALUE 1
#ifdef SOME_VALUE
int n = 1;
#else
int n = 2;
#endif
konvertiert in:
int n = 1;
Die Direktive
#ifdef SOME_VALUE
int n = 1;
#else
int n = 2;
#endif
jedoch wird konvertiert in
int n = 2;
Das #ifndef ist auch definiert mit der genau umgekehrten Definition.
Teil 5 – Sonntagmorgen
Der häufigste Einsatz von #ifdef ist die Kontrolle über die Einbeziehung von Code. Ein Symbol kann
nicht mehr als einmal definiert werden. Das Folgende ist ungültig:
class MyClass
Lektion 26
{
int n;
};
class MyClass
{
int n;
};
Wenn MyClass in der Include-Datei myclass.h definiert ist, wäre es ein Fehler, diese Datei zwei-
mal in die .cpp-Quelldatei einzubinden. Sie könnten denken, dass dieses Problem leicht vermeidbar
ist. Es ist aber üblich, dass Include-Dateien andere Include-Dateien einbinden, wie in folgendem Bei-
spiel:
#include »myclass.h«
class mySpecialClass : public MyClass
{
int m;
}
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 304
304 Sonntagmorgen
Ein nichtsahnender Programmierer könnte leicht beide, ass.h und myspecialclass.h, in dieselbe
Quelldatei einbinden, was durch die doppelte Definition zu einem Compilerfehler führt.
// das kann nicht kompiliert werden
#include »myclass.h«
#include »myspecialclass.h«
void fn(MyClass& mc)
{
// ... kann ein Objekt aus der Klasse MyClass
// oder MySpecialClass sein
}
Dieses spezielle Beispiel lässt sich leicht korrigieren. In einer großen Anwendung können die
Beziehungen zwischen den Include-Dateien viel komplexer sein.
Der wohlüberlegte Einsatz der #ifdef-Direktive verhindert dieses Problem, indem myclass.h wie
folgt geschrieben wird:
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass
{
int n;
};
#endif
Wenn myclass.h zum ersten Mal eingebunden wird, ist MYCLASS_H nicht definiert und #ifndef
ist wahr. Die Konstante MYCLASS_H wird jedoch innerhalb von myclass.h definiert. Das nächste Mal,
wenn myclass.h während des Kompilierens angefasst wird, ist MYCLASS_H definiert, und die Klas-
sendefinition wird weggelassen.
Jedes Mal, wenn diese Funktion aufgerufen wird, druckt dumpState( ) den Inhalt des MySpeci-
alClass-Objektes in die Standardausgabe. Ich kann überall in meinem Programm Aufrufe dieser
Funktion einbauen, um den Zustand von MySpecialClass-Objekten zu kontrollieren. Wenn das
Programm fertig ist, muss ich alle diese Aufrufe wieder entfernen. Das ist nicht nur ermüdend, son-
dern birgt das Risiko in sich, dass hierbei neue Fehler in das System gelangen. Außerdem kann es
sein, dass ich die Anweisungen zum erneuten Debuggen des Systems wieder benötige.
Ich könnte eine Art Flag definieren, das steuert, ob das Programm den Status der MySpecial-
Class-Objekte ausgibt. Aber die Aufrufe selber stellen einen Mehraufwand dar, der die Funktionen
verlangsamt. Ein besserer Ansatz ist der folgende:
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 305
#ifdef DEBUG
void dumpState(MySpecialClass& msc)
{
cout <<»MySpecialClass:«
<<»m = » <<msc.m
<<»n = » <<msc.n;
}
#else
inline dumpState(MySpecialClass& mc)
{
}
#endif
Wenn der Parameter DEBUG definiert ist, wird die Funktion dumpState( ) kom-
piliert. Wenn DEBUG nicht definiert ist, wird eine Inline-Version von dumpState( )
kompiliert, die nichts tut. Der C++-Compiler konvertiert jeden Aufruf dieser Funk-
tion zu nichts.
0 Min.
Wenn die inline-Version der Funktion nicht funktioniert, liegt das vielleicht dar-
an, dass der Compiler keine Inline-Funktionen unterstützt. Dann verwenden Sie
!
Tipp
die folgende Makrodefinition:
#define dumpState(x)
Visual C++ und GNU C++ unterstützen beide diesen Ansatz. Konstanten kön-
nen in den Projekteinstellungen ohne Hinzufügen der #define-Direktive in den
Teil 5 – Sonntagmorgen
Sourcecode definiert werden. In der Tat ist die Konstante _DEBUG automatisch
definiert, wenn im Debug-Modus kompiliert wird.
Zusammenfassung. Lektion 26
Der häufigste Einsatz des Präprozessors ist das Einbinden der gleichen Klassendefinition oder der
gleichen Funktionsprototypen in mehrere .cpp-Quelldateien mit der #include-Direktive. Die Prä-
prozessor-Direktiven #if und #ifdef erlauben die Kontrolle darüber, welche Zeilen des Codes kom-
piliert werden und welche nicht.
• Der Name der Datei, die mittels #include eingebunden wird, sollte mit .h enden. Das nicht zu
tun, verwirrt andere Programmierer und vielleicht sogar den Compiler. Dateinamen, die in Hoch-
kommata (””) eingeschlossen sind, werden im aktuellen (oder in einem anderen benutzerdefi-
nierten) Verzeichnis gesucht, wohingegen Klammern (<>) zur Referenzierung von Include-
Dateien von C++ verwendet wird.
• Wenn der konstante Ausdruck nach der #if-Direktive nicht null ist, dann werden die folgenden
C++-Anweisungen an den Compiler übergeben; andernfalls werden sie nicht übergeben. Die
Direktive #ifdef x ist wahr, wenn die #define-Konstante x definiert ist.
• Alle Präprozessor-Direktiven kontrollieren, welche C++-Anweisungen der Compiler »sieht«. Alle
werden zur Compilezeit ausgewertet und nicht zur Laufzeit.
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 306
306 Sonntagmorgen
Selbsttest.
1. Was ist der Unterschied zwischen #include ”file.h” und #include <file.h>? (Siehe »Die
#include-Direktive«)
2. Was sind die beiden Typen der #define-Direktive? (Siehe »Die #define-Direktive«)
3. Gegeben die Makrodefinition #define square(x) x * x, was ist der Wert von square(2 + 3)?
(Siehe »Die #define-Direktive«)
4. Nennen Sie einen Vorteil von Inline-Funktionen gegenüber einer äquivalenten Makrodefinition.
(Siehe »Häufige Fehler bei der Verwendung von Makros«)
5. Was ist ein häufiger Einsatz der #ifndef-Direktive? (Siehe »Verwendung von #ifdef/#ifndef zur
Einschlusskontrolle«)
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 307
Sonntagmorgen –
Zusammenfassung 5
T EIL
class Student
{
public:
Student() : adv(»Student Datenelement«)
{
cout << »Student\n«;
new Advisor(»Student lokal«);
}
Advisor adv;
};
class GraduateStudent : public Student
{
public:
GraduateStudent() :
adv(»GraduateStudent Datenelement«)
{
cout << » GraduateStudent\n«;
new Advisor(»GraduateStudent lokal«);
}
protected:
Advisor adv;
};
{
GraduateStudent gs;
return 0;
}
2. Gegeben sei, dass ein GraduateStudent einen Grad von 2.5 oder besser erreichen muß,
um zu bestehen, und 1.5 für reguläre Studenten ausreicht. Schreiben Sie eine Funktion
pass( ) unter Verwendung der Klassen, die wir in dieser Sitzung geschrieben haben, die
einen Grad akzeptiert, und 0 zurückgibt für einen Studenten, der durchgefallen ist, und 1,
wenn er bestanden hat.
3. Schreiben Sie eine Klasse Checking (Girokonto), die von Account und CashAccount wie
oben gezeigt erbt. Ein Girokono ist einem Sparkonto sehr ähnlich, außer dass eine Gebühr
für jedes Abheben anfällt. Machen Sie sich keine Gedanken zu Überziehungen.
.
CD-ROM
Wenn Sie Zeit sparen möchten, können Sie die Klassen Account, CashAccout
und Savings aus dem Verzeichnis ExerciseClasses der beiliegenden CD-ROM
kopieren. Sie können diese als Startpunkt verwenden.
return 0;
}
C++ Lektion 26 31.01.2001 12:48 Uhr Seite 309
4. Schreiben Sie ein Programm, das mit Mehrfachvererbung ein Objekt pc aus der Klasse
CombinationPrinterCopier erzeugt. Dieses Programm sollte unter Verwendung von pc
drucken. Zusätzlich sollte das Programm die elektrische Spannung (voltage) von pc aus-
geben.
Hinweise:
b. Sie sind nicht in der Lage, die Spannung durch die Konstruktoren nach oben weiterzu-
geben. Setzen Sie stattdessen die voltage im Konstruktor CombinationPrinterCopier.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 310
Sonntag-
nachmittag
Teil 6
Lektion 27.
Überladen von Operatoren
Lektion 28.
Der Zuweisungsoperator
Lektion 29.
Stream-I/0
Lektion 30.
Ausnahmen
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 311
Überladen von
Operatoren 27 Lektion
Checkliste.
✔ Überladen von C++-Operatoren im Überblick
✔ Diskussion Operatoren und Funktionen
✔ Implementierung von Operatoren als Elementfunktion und Nichtelement-
funktion
✔ Der Rückgabewert eines überladenen Operators
✔ Ein Spezialfall: Der Cast-Operator
D
ie Sitzungen 6 und 7 diskutierten die mathematischen und logischen Ope-
ratoren, die C++ für die elementaren Datentypen definiert.
Die elementaren Datentypen sind die, die in die Sprache eingebaut sind, wie
30 Min. int, float, double usw. und die Zeigertypen.
Zusätzlich zu den elementaren Operatoren erlaubt es C++ dem Programmie-
rer, Operatoren für Klassen zu definieren, die der Programmierer geschrieben hat. Das wird Überla-
den von Operatoren genannt.
Normalerweise ist das Überladen von Operatoren optional und sollte nicht von C++-Anfängern
probiert werden. Eine Menge erfahrener C++-Programmierer denken, dass das Überladen von Ope-
ratoren keine so tolle Sache ist. Es gibt jedoch drei Operatoren, deren Überladen Sie erlernen müs-
sen: Zuweisung (=), Linksshift (<<) und Rechtsshift (>>). Sie sind wichtig genug, um ein eigenes
Kapitel zu bekommen, das diesem Kapitel unmittelbar folgt.
!
Tipp
Das Überladen von Operatoren kann Fehler verursachen, die schwer zu finden
sind. Seien Sie ganz sicher, wie das Überladen von Operatoren funktioniert,
bevor Sie es einsetzen.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 312
312 Sonntagnachmittag
Wenn der entsprechende Operator überladen wird, sieht die gleiche Funktion wie folgt aus:
Bevor wir untersuchen, wie Operatoren überladen werden, müssen wir die Beziehung zwischen
Operatoren und Funktionen verstehen.
Der Programmierer kann alle Operatoren überladen, außer ., ::, * (Dereferenzierung) und &,
durch Überladen ihres funktionalen Namens mit den folgenden Einschränkungen:
• Der Programmierer kann keine neuen Operatoren einführen. Sie können keinen Operator x $ y
einführen.
• Das Format der Operatoren kann nicht geändert werden. Somit können Sie keinen Operator %i
definierten, weil % ein binärer Operator ist.
• Der Vorrang der Operatoren kann nicht geändert werden. Ein Programm kann nicht erzwingen,
dass operator+ vor dem operator* ausgeführt wird.
• Schließlich können Operatoren nicht neu definiert werden, wenn sie auf elementare Typen ange-
wendet werden. Existierende Operatoren können nur für neue Typen überladen werden.
Listing 27-1: USDollar mit überladenen Operatoren für Addition und Inkrementierung
314 Sonntagnachmittag
{
cout << »$«
<< dollars
<< ».«
<< cents;
}
protected:
int dollars;
int cents;
};
USDollar::USDollar(int d, int c)
{
// speichere die initialen Werte
dollars = d;
cents = c;
rationalize();
}
++d3;
cout << »Nach Inkrementierung gleich »;
d3.output();
cout << »\n«;
return 0;
}
Die Klasse USDollar ist so definiert, dass sie einen ganzzahligen Dollarbetrag und einen ganz-
zahligen Centbetrag kleiner 100 speichert. Beim Durcharbeiten der Klasse von vorne nach hinten,
sehen wir die Operatoren operator+( ) und operator++( ), die als Freunde der Klasse deklariert
sind.
Erinnern Sie sich daran, dass ein Klassenfreund eine Funktion ist, die Zugriff auf
=
= =
=
die protected-Elemente der Klasse hat. Weil operator+( ) und operator++( )
als herkömmliche Nichtelementfunktionen implementiert sind, müssen sie als
Hinweis Freunde der Klasse deklariert sein, um Zugriff auf die protected-Elemente der
Klasse zu erhalten.
Der Konstruktor von USDollar erzeugt ein Objekt aus den ganzzahligen Angaben von Dollars
und Cents, für die es beide Defaultwerte gibt. Einmal gespeichert ruft der Konstruktor die Funktion
rationalize( ) auf, die den Betrag normalisiert, indem Cent-Anzahlen größer als 100 dem Dollar-
betrag zugeschlagen werden. Die Funktion output( ) schreibt das USDollar-Objekt nach cout.
Der operator+( ) wurde mit zwei Argumenten definiert, weil Addition ein binärer Operator ist
(d.h. der zwei Argumente hat). Der operator+( ) beginnt damit, die Beträge von Dollar und Cent
ihrer beiden USDollar-Argumente zu addieren. Sie erzeugt dann ein neues USDollar-Objekt mit
diesen Werten und gibt es an den Aufrufenden zurück.
=
= =
= Jede Operation auf einem Wert eines USDollar-Objekts sollte rationalize( ) auf-
Hinweis rufen, um sicherzustellen, dass der Centbetrag nicht größer oder gleich 100 ist. Die
Funktion operator+( ) ruft rationalize( ) über den Konstruktor USDollar auf. Teil 6 – Sonntagnachmittag
Der Inkrementoperator operator++( ) hat nur ein Argument. Diese Funktion inkrementiert die
Anzahl Cents im Objekt s um eins. Die Funktion gibt dann eine Referenz auf das Objekt zurück, das
Lektion 27
Im Gebrauch sehen die Operatoren sehr natürlich aus. Was könnte einfacher sein, als d3 = d1 +
d2 oder ++d3?
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 316
316 Sonntagnachmittag
weil hierbei s1 verändert wird. Nach einer Addition s1 + s2 wäre der Wert von s1 verändert. Das
Folgende funktioniert auch nicht:
Obwohl das ohne Probleme kompiliert werden kann, erzeugt es ein falsches Ergebnis. Das Pro-
blem ist, dass die zurückgegebene Referenz result auf ein Objekt verweist, deren Gültigkeitsbe-
reich lokal in der Funktion ist. Somit hat result seinen Gültigkeitsbereich bereits verlassen, wenn es
von der aufrufenden Funktion verwendet werden kann.
Warum dann nicht einfach einen Speicherbereich vom Heap allozieren?
// das funktioniert
USDollar& operator+(USDollar& s1, USDollar& s2)
{
int cents = s1.cents + s2.cents;
int dollars = s1.dollars + s2.dollars;
return *new USDollar(dollars, cents);
}
Das wäre gut, außer, dass es keinen Mechanismus gibt, den allozierten Speicher wieder an den
Heap zurückzugeben. Solche Speicherlöcher sind schwer zu finden. Ganz langsam geht bei jeder
Addition dem Heap ein wenig Speicher verloren.
Die Wertrückgabe zwingt den Compiler, selber ein eigenes temporäres Objekt anzulegen, und es
auf den Stack des Aufrufenden zu packen. Das Objekt, das in der Funktion erzeugt wurde, wird dann
in das Objekt kopiert, als Teil von operator+( ). Aber wie lange existiert das temporäre Objekt von
operator+( )? Ein temporäres Objekt muss so lange gültig bleiben, bis der »erweiterte Ausdruck«,
in dem es vorkommt, fertig ist. Der erweiterte Ausdruck ist alles bis zum Semikolon.
Betrachten Sie z.B. den folgenden Schnipsel:
SomeClass f();
LotsAClass g();
void fn()
{
int i;
i = f() + (2 * g());
Das temporäre Objekt, das von f( ) zurückgegeben wird, existiert weiter, während g( ) aufge-
rufen wird und die Multiplikation durchgeführt wird. Dieses Objekt verliert beim Semikolon seine
Lektion 27
Gültigkeit.
Um zu unserem USDollar-Beispiel zurückzukehren, in dem das temporäre Objekt nicht gesi-
chert wird, funktioniert das Folgende nicht:
d1 = d2 + d3 + ++d4;
Das temporäre Ergebnis aus der Addition von d2 und d3 muss gültig bleiben, während d4 inkre-
mentiert wird und umgekehrt.
=
= =
=
C++ spezifiziert nicht die Reihenfolge, in der Operatoren ausgeführt werden.
Somit wissen wir nicht, ob d2 + d3 oder ++d4 zuerst ausgeführt wird. Sie müs-
Hinweis sen Ihre Funktionen so schreiben, dass es darauf nicht ankommt.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 318
318 Sonntagnachmittag
Anders als operator+( ) modifiziert operator++( ) sein Argument. Es gibt daher keinen Grund,
ein temporäres Objekt zu erzeugen und als Wert zurückzugeben. Das übergebene Argument wird
als Referenz an den Aufrufenden zurückgegeben. Die folgende Funktion, die als Wert zurückgibt,
enthält einen subtilen Bug:
Indem s als Wert zurückgegeben wird, zwingt die Funktion den Compiler, eine Kopie des
Objekts zu machen. In den meisten Fällen ist das in Ordnung. Aber was passiert in einem zugegebe-
nermaßen ungewöhnlichen aber zulässigen Ausdruck wie ++(++a)? Wir würden erwarten, dass a
um 2 erhöht wird. Mit der vorangegangenen Definition wird a jedoch um 1 erhöht und dann wird
eine Kopie von a – und nicht a selber – um 1 erhöht.
Die allgemeine Regel sieht so aus: Wenn der Operator den Wert des Arguments ändert, überge-
ben Sie das Argument als Referenz, so dass das Original modifiziert werden und das Argument als
Referenz zurückgegeben werden kann, für den Fall, dass das gleiche Objekt in nachfolgenden Ope-
rationen verwendet wird. Wenn der Operator nicht den Wert seiner Argumentes verändert, erzeu-
gen Sie ein neues Objekt zur Speicherung des Ergebnisses und geben Sie dieses Objekt als Wert
zurück. Die Eingabeargumente können bei Operatoren mit zwei Argumenten immer als Referenzen
übergeben werden, um Zeit zu sparen, aber keines der Argumente sollte dann verändert werden.
=
= =
= Es gibt binäre Operatoren, die den Wert ihrer Argumente verändern, wie die
Hinweis speziellen Operatoren +=, *=, usw.
.
CD-ROM
Die vollständige Version des Programms in Listing 27-2 finden Sie in der Datei
USDollarMemberAdd auf der beiliegenden CD-ROM.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 319
rationalize();
}
// rationalize – normalisiere den Centbetrag
// durch Addition eines Dollars pro
// 100 Cents
void rationalize()
{
dollars += (cents / 100);
cents %= 100;
}
// output – schreibe den Wert des Objektes
// in die Standardausgabe
void output()
{
cout << »$«
<< dollars
<< ».«
<< cents;
}
//operator+ – addiere das aktuelle Objekt
// zu s2 und gib das Ergebnis
// in einem neuen Objekt zurück
USDollar operator+(USDollar& s2)
{
Teil 6 – Sonntagnachmittag
int cents = this->cents + s2.cents;
int dollars = this->dollars + s2.dollars;
return USDollar(dollars, cents);
Lektion 27
}
//operator++ – inkrementiere das aktuelle
// Objekt
USDollar& operator++()
{
cents++;
rationalize();
return *this;
}
protected:
int dollars;
int cents;
};
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 320
320 Sonntagnachmittag
// operator+ – Nichtelementversion
USDollar operator+(USDollar& s1, USDollar& s2)
{
int cents = s1.cents + s2.cents;
int dollars = s1.dollars + s2.dollars;
USDollar t(dollars, cents);
return t;
}
//operator+ – Elementversion
USDollar USDollar::operator+(USDollar& s2)
{
int cents = this->cents + s2.cents;
int dollars = this->dollars + s2.dollars;
USDollar t(dollars, cents);
return t;
}
Wir können sehen, dass die Funktionen fast identisch sind. Jedoch dort, wo die Nichtelementver-
sion s1 und s2 addiert, addiert die Elementversion das aktuelle Objekt – auf das this zeigt – und s2.
Die Elementversion eines Operators hat immer ein Argument weniger als die Nichtelementver-
sion – das Argument auf der linken Seite ist implizit.
Die zweite Version ruft einfach die erste Version auf mit der entsprechenden Reihenfolge der Ope-
randen. Die Deklaration als inline spart jeden Zusatzaufwand.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 321
Um eine Elementfunktion sein zu können, muss operator(float, USDollar&) ein Element der
Klasse double sein. Wie bereits früher erwähnt, können wir keine Operatoren zu elementaren Klas-
sen hinzufügen. Somit muss ein Operator, für den nur das zweite Argument aus der Klasse ist, als
Nichtelement implementiert werden.
Operatoren, die das Objekt, auf dem sie arbeiten, verändern, wie z.B. operator++( ), sollten Ele-
ment der Klasse sein.
27.8 Cast-Operator.
Auch der Cast-Operator kann überschrieben werden. Das Programm USDollarCast in Listing 27-3
zeigt die Definition und den Gebrauch eines Cast-Operators, der ein USDollar-Objekt in ein double
konvertiert, und wieder zurück.
class USDollar
{
public:
// Konstruktor, der USDollar aus double erzeugt
USDollar(double value = 0.0);
// Cast-Operator
operator double()
{
return dollars + cents / 100.0;
}
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 322
322 Sonntagnachmittag
protected:
int dollars;
int cents;
};
int main()
{
USDollar d1(2.0), d2(1.5), d3, d4;
d3 = USDollar((double)d1 + (double)d2);
double dVal3 = (double)d3;
d3.display(»d3 (Summe d1+d2 mit Casts)«, dVal3);
return 0;
{
Ein Cast-Operator ist das Schlüsselwort operator, gefolgt von dem entsprechenden Typ. Die Ele-
mentfunktion USDollar::operator double( ) stellt einen Mechanismus bereit, um ein Objekt
der Klasse USDollar in ein double zu konvertieren. (Aus einem mir unbekannten Grund, haben
Cast-Operatoren keinen Rückgabetyp.) Der Konstruktor USDollar(double) stellt den Konvertie-
rungspfad von double zu USDollar her.
Wie das vorangegangene Beispiel zeigt, kann die Konvertierung mittels Cast-Operators entweder
explizit oder implizit aufgerufen werden. Lassen Sie uns den impliziten Fall genauer betrachten.
Um den Ausdruck d4 = d1 + d2 im Programm USDollarCast mit Sinn zu versehen, durchläuft
C++ die folgenden Schritte:
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 323
d1 = $2.0 (2)
d2 = $1.50 (1.5)
d3 (Summe d1+d2 mit Casts) = $3.50 (3.5)
d4 (Summe d1+d2 ohne Casts) = $3.50 (3.5)
Das zeigt sowohl den Vorteil als auch den Nachteil davon, Cast-Operatoren bereitzustellen. Die
Bereitstellung eines Konvertierungspfades von USDollar nach double befreit den Programmierer
davon, einen vollständigen Satz von Operatoren bereitstellen zu müssen. USDollar kann einfach auf
die für double definierten Operatoren zurückgreifen.
Auf der anderen Seite nimmt das dem Programmierer die Kontrolle darüber, welche Operatoren
definiert werden. Durch den Konvertierungspfad nach double, bekommt USDollar alle Operatoren
von double, ob sie nun Sinn machen oder nicht. Ich hätte genauso gut d4 = d1 * d2 schreiben
können. Außerdem kann es sein, dass diese zusätzliche Konvertierung nicht besonders schnell ist.
Diese einfache Addition z.B. enthält drei Typkonvertierungen mit all den verbundenen Funktions-
aufrufen, Multiplikationen, Divisionen usw.
Passen Sie auf, dass Sie nicht zwei Konvertierungspfade zum gleichen Typ bereitstellen. Das Fol-
gende muss Probleme erzeugen:
Teil 6 – Sonntagnachmittag
class A
{
public:
A(B& b);
};
Lektion 27
class B
{
public:
operator A();
};
Wenn ein Objekt der Klasse B in ein Objekt der Klasse A konvertiert werden soll,
weiß der Compiler nicht, ob er den Cast-Operator B::operator A( ) von B oder
den Konstruktor a::A(B&) von A verwenden soll, die beide von einem B ausgehen
und bei einem A ankommen.
0 Min. Vielleicht ist das Ergebnis der beiden Pfade das Gleiche, aber der Compiler weiß
das nicht. C++ muss wissen, welchen Konvertierungspfad Sie meinen. Der Compi-
ler gibt eine Meldung aus, wenn er den Pfad nicht unzweideutig bestimmen kann.
C++ Lektion 27 31.01.2001 12:49 Uhr Seite 324
324 Sonntagnachmittag
Zusammenfassung.
Eine neue Klasse mit den entsprechenden Operatoren zu überladen, kann zu einfachem und ele-
gantem Anwendungscode führen. In den meisten Fällen ist das Überladen von Operatoren jedoch
nicht nötig. Die folgenden Sitzungen untersuchen zwei Fälle, in denen das Überladen von Operato-
ren kritisch ist.
• Das Überladen von Operatoren ermöglicht es dem Programmierer, existierende Operatoren für
seine eigenen Klassen neu zu definieren. Der Programmierer kann jedoch keine neuen Operatoren
hinzufügen, noch die Syntax bestehender Operatoren ändern.
• Es ist ein entscheidender Unterschied zwischen Übergabe und Rückgabe eines Objekts als Wert
oder als Referenz. Abhängig vom Operator kann dieser Unterschied kritisch sein.
• Operatoren, die das Objekt verändern, sollten als Element implementiert werden. Einige Operato-
ren müssen als Element implementiert werden. Operatoren, bei denen auf der linken Seite ein ele-
mentarer Datentyp und keine benutzerdefinierte Klasse steht, können nicht als Elementfunktionen
implementiert werden. Ansonsten macht es keinen großen Unterschied.
• Der Cast-Operator erlaubt es dem Programmierer, C++ mitzuteilen, wie ein benutzerdefiniertes
Klassenobjekt in einen elementaren Typ konvertiert werden kann. Z.B. könnte die Konvertierung
von Student nach int die ID des Studenten zurückgeben (ich habe nicht gesagt, dass diese Kon-
vertierung eine gute Idee ist, sondern nur, dass sie möglich ist.) Dann können eigene Klassen mit
den elementaren Datentypen in Ausdrücken gemischt werden.
• Benutzerdefinierte Operatoren erlauben es dem Programmierer, Programme zu schreiben, die
leichter zu lesen und zu pflegen sind. Eigene Operatoren können jedoch trickreich sein und sollten
mit Vorsicht verwendet werden.
Selbsttest.
1. Es ist wichtig, dass Sie drei Operatoren für jede Klasse überschreiben können. Welche Operatoren
sind das? (Siehe Einleitung)
2. Wie könnte der folgende Code Sinn machen? (Siehe »Warum soll ich Operatoren überladen?«)
USDollar dollar(100, 0);
DM& mark = !dollar;
3. Gibt es einen anderen Weg, das Obige nur durch »normale« Funktionsaufrufe zu schreiben, ohne
Operatoren zu verwenden, die vom Programmierer geschrieben wurden? (Siehe »Warum soll ich
Operatoren überladen?«)
C++ Lektion 28 31.01.2001 12:50 Uhr Seite 325
Der Zuweisungs-
operator 28 Lektion
Checkliste.
✔ Einführung in den Zuweisungsoperator
✔ Warum und wann der Zuweisungsoperator nötig ist
✔ Ähnlichkeiten von Zuweisungsoperator und Kopierkonstruktor
O
b Sie nun anfangen, Operatoren zu überladen oder nicht, Sie müssen
schon früh lernen, den Zuweisungsoperator zu überladen. Der Zuwei-
sungsoperator kann für jede benutzerdefinierte Klasse überladen werden.
Wenn Sie sich an das hier vorgestellte Muster halten, werden Sie sehr bald Ihre
30 Min. eigene Version von operator=( ) schreiben.
void fn()
{
MyStruct source, destination;
destination = source;
}
Diese Defaultimplementierung ist jedoch nicht korrekt für Klassen, die Ressourcen allozieren wie
z.B. Speicher vom Heap. Der Programmierer muss operator=( ) überladen, um den Transfer von
Ressourcen zu realisieren.
C++ Lektion 28 31.01.2001 12:50 Uhr Seite 326
326 Sonntagnachmittag
Die Erzeugung von newMC folgt dem Standardmuster, ein neues Objekt unter Verwendung des
Kopierkonstuktors MyClass(MyClass&) als ein Spiegelbild des Originals zu erzeugen. Nicht so offen-
sichtlich ist, dass C++ das zweite Format erlaubt, bei dem newerMC mittels Kopierkonstruktor erzeugt
wird.
newestMC wird mittels des Default-Konstruktors erzeugt und dann durch den Zuweisungsopera-
tor mit mc überschrieben. Der Unterschied ist, dass bei Aufruf des Kopierkonstruktors für newerMC
dieses Objekt noch nicht existierte. Bei Aufruf des Zuweisungsoperators für newestMC war es bereits
ein MyClass-Objekt im besten Sinne.
!
Tipp
Die Regel sieht so aus: Der Kopierkonstruktor wird benutzt, wenn ein neues
Objekt erzeugt wird. Der Zuweisungsoperator wird verwendet, wenn das
Objekt auf der linken Seite bereits existiert.
Wie der Kopierkonstruktor sollte ein Zuweisungsoperator immer dann bereitgestellt werden,
wenn eine flache Kopie nicht angebracht ist. (Sitzung 20 enthält eine umfangreiche Diskussion von
flachen und tiefen Konstruktoren.) Es reicht aus zu sagen, dass ein Kopierkonstruktor und ein
Zuweisungsoperator dann definiert werden sollten, wenn die Klasse Ressourcen alloziert, damit es
nicht dazu kommt, dass zwei Objekte auf die gleichen Ressourcen zeigen.
=
= =
= Denken Sie daran, dass der Zuweisungsoperator ein Element der Klasse sein
Hinweis muss.
C++ Lektion 28 31.01.2001 12:50 Uhr Seite 327
protected:
void copyName(char *pszN);
void deleteName();
char *pszName;
};
328 Sonntagnachmittag
// überschreibe n2 mit n1
n2 = n1;
displayNames(n1, » wurde zugewiesen an »,
n2, »\n«);
return 0;
}
C++ Lektion 28 31.01.2001 12:50 Uhr Seite 329
Ausgabe:
Die Klasse Name hält den Namen einer Person im Speicher, der vom Heap alloziert wurde. Die
Konstruktoren und der Destruktor der Klasse Name sind denen sehr ähnlich, die in den Sitzungen 19
und 20 vorgestellt wurden. Der Konstruktor Name(char*) kopiert den gegebenen Namen in das
Datenelement pszName. Dieser Konstruktor ist auch der Defaultkonstruktor. Der Kopierkonstruktor
Name(&Name) kopiert den Namen des übergebenen Objektes in den Namen des aktuellen Objektes
durch einen Aufruf der Funktion copyName( ). Der Destruktor gibt die pszName-Zeichenkette durch
einen Aufruf von deleteName( ) an den Heap zurück.
Die Funktion main( ) demonstriert jede dieser Elementfunktionen. Die Ausgabe von DemoAssign
finden Sie oben am Ende von Listing 28-1.
Schauen Sie sich den Zuweisungsoperator genau an. Die Funktion operator=( ) sieht doch
wirklich aus wie ein Destruktor, unmittelbar gefolgt von einem Kopierkonstruktor. Das ist typisch.
Betrachten Sie die Zuweisung im Beispiel n2 = n1. Das Objekt n2 hat bereits einen Namen (»Greg«).
In der Zuweisung muss der Speicher, den der ursprüngliche Name belegt, an den Heap zurückge-
geben werden durch einen Aufruf von deleteName( ), bevor neuer Speicher mittels copyName( )
alloziert und zugewiesen wird, in dem der neue Name (»Claudette«) gespeichert wird.
Der Kopierkonstruktor musste deleteName( ) nicht aufrufen, weil das Objekt noch nicht exis-
tierte. Es war daher noch kein Speicher belegt, als der Konstruktor aufgerufen wurde.
Im Allgemeinen hat ein Zuweisungsoperator zwei Teile. Der erste Teil baut den Destruktor in dem
Sinne nach, dass er die belegten Ressourcen des Objektes freigibt. Der zweite Teil baut den Kopier-
konstruktor nach in dem Sinne, dass er neue Ressourcen alloziert.
stammen. Im folgenden Beispiel ist der Wert von operator=( ) gleich 2.0 und der Typ ist double:
Der Wert 2.0 der Zuweisung d1 = 2.0 und ihr Typ double werden an den nächsten Zuwei-
sungsoperator übergeben. Im zweiten Beispiel wird der Wert der Zuweisung d2 = 3.0 an die Funk-
tion fn( ) übergeben.
C++ Lektion 28 31.01.2001 12:50 Uhr Seite 330
330 Sonntagnachmittag
Ich hätte auch void zum Rückgabetyp von Name::operator=( ) machen können. Wenn ich das
jedoch tue, funktioniert das obige Beispiel nicht mehr:
void otherFn(Name&);
void fn()
{
Name n1, n2, n3;
Das Ergebnis der Zuweisung n1 = n2 ist void – der Rückgabetyp von operator=( ) – was nicht
mit dem Prototyp von otherFn( ) übereinstimmt. Die Deklaration von operator=( ) mit einer
Referenz auf das aktuelle Objekt und die Rückgabe von *this bleiben die Semantik für den Zuwei-
sungsoperator bei elementaren Typen.
Das zweite Detail ist, dass operator=( ) als Elementfunktion geschrieben wurde. Anders als
andere Operatoren kann der Zuweisungsoperator nicht mit einer Nichtelementfunktion überladen
werden. Die speziellen Zuweisungsoperatoren, wie += und *=, haben keine besonderen Einschrän-
kungen und können Nichtelemente sein.
Dieser Kopierschutz für Klassen erspart Ihnen den Ärger mit dem Überladen des
Zuweisungsoperators, aber reduziert die Flexibilität Ihrer Klasse.
0 Min.
Wenn Ihre Klasse Ressourcen alloziert, wie z.B. Speicher vom Heap, müssen Sie
=
= =
= entweder einen entsprechenden Zuweisungsoperator und Kopierkonstruktor
Hinweis schreiben oder beide protected machen und verhindern, dass die von C++
bereitgestellte Defaultmethode verwendet wird.
Zusammenfassung.
Der Zuweisungsoperator ist der einzige Operator, den Sie überschreiben müssen, aber nur unter
bestimmten Bedingungen. Glücklicherweise ist es nicht schwer, einen Zuweisungsoperator für Ihre
Klasse zu definieren, wenn Sie dem Muster folgen, das in dieser Sitzung beschrieben wurde.
• C++ stellt einen Default-Zuweisungsoperator bereit, der eine Element-zu-Element-Kopie durch-
führt. Diese Version der Zuweisung ist für viele Klassentypen in Ordnung; Klassen jedoch, die
Ressourcen allozieren, müssen einen Kopierkonstruktor und einen überladenen Zuweisungsope-
rator enthalten.
• Die Semantik des Zuweisungsoperators entspricht im Wesentlichen einem Destruktor, gefolgt von
einem Kopierkonstruktor. Der Destruktor entfernt alle Ressourcen, die möglicherweise bereits exis-
tieren, während der Kopierkonstruktor eine tiefe Kopie der zugewiesenen Ressourcen erstellt.
• Den Zuweisungsoperator protected zu deklarieren, reduziert die Gefahr, aber beschränkt Ihre
Klasse, indem mit Ihrer Klasse keine Zuweisungen ausgeführt werden können.
Selbsttest.
1. Wann muss Ihre Klasse einen Zuweisungsoperator enthalten? (Siehe »Warum ist das Überladen Teil 6 – Sonntagnachmittag
des Zuweisungsoperators kritisch?«)
2. Der Rückgabetyp des Zuweisungsoperators sollte immer mit dem Klassentyp übereinstimmen.
Lektion 28
29 Lektion Stream-I/O
Checkliste.
✔ Stream-I/O als überladenen Operator wiederentdecken
✔ Streamdatei-I/O verwenden
✔ Streampuffer-I/O verwenden
✔ Eigene Inserter und Extraktor schreiben
✔ Hinter den Kulissen von Manipulatoren
B
is jetzt haben alle Programme ihre Eingaben über das cin-Eingabeobjekt und
ihre Ausgaben über das cout-Ausgabeobjekt erledigt. Vielleicht haben Sie
nicht viel darüber nachgedacht, aber diese Technik der Eingabe/Ausgabe ist
eine Teilmenge dessen, was als Stream-I/O bezeichnet wird.
Diese Sitzung erklärt Stream-I/O im Detail. Ich muss Sie warnen: Stream-I/O ist
30 Min.
ein zu großes Thema, um in einer einzigen Sitzung behandelt werden zu können –
ganze Bücher sind diesem Thema gewidmet. Ich kann Ihnen jedoch zu einem Anfang verhelfen, so
dass Sie die Hauptoperationen durchführen können.
Wenn operator>>( ) für I/O überladen wird, wird er Extraktor genannt; operator<<( ) wird
Inserter genannt.
Lassen Sie uns im Detail ansehen, was passiert, wenn ich Folgendes schreibe:
#include <iostream.h>
void fn()
{
cout << »Ich heiße Randy\n«;
}
Das Objekt cout ist ein Objekt der Klasse ostream (mehr dazu später). Somit bestimmt C++, dass
die Funktion operator<<(ostream&, char*) am besten übereinstimmt. C++ erzeugt einen Aufruf
dieser Funktion, dem so genannten char*-Inserter und übergibt der Funktion das ostream-Objekt
cout und die Zeichenkette »Ich heiße Randy\n« als Argument. D.h. es wird aufgerufen opera-
tor<<(cout, »Ich heiße Randy\n«). Der char*-Inserter, der Teil der C++-Standardbibliothek ist,
führt die angeforderte Ausgabe durch.
Die Klassen ostream und istream sind die Basis für eine Menge von Klassen, die den Anwen-
dungscode mit der Außenwelt verbinden, Eingabe vom und Ausgabe ins Dateisystem eingeschlos-
sen. Woher wusste der Compiler, dass cout aus der Klasse ostream ist? Diese und einige andere glo-
bale Objekte sind in iostream.h deklariert. Eine Liste dieser Objekte finden Sie in Tabelle 29-1. Diese
Objekte werden bei Programmstart automatisch erzeugt, bevor main( ) die Kontrolle erhält.
Unterklassen von ostream und istream werden für die Eingabe von und die Ausgabe in Dateien
und interne Puffer verwendet.
334 Sonntagnachmittag
Die Klasse ofstream, die für die Dateiausgabe verwendet wird, hat mehrere Konstruktoren, von
denen der folgende der nützlichste ist:
ofstream::ofstream(char *pszFileName,
int mode = ios::out,
int prot = filebuff::openprot);
Das erste Argument ist ein Zeiger auf den Namen der Datei, die geöffnet werden soll. Das zweite
und dritte Argument geben an, wie die Datei geöffnet werden soll. Die gültigen Werte für mode fin-
den Sie in Tabelle 29-2 und die für prot in Tabelle 29-3. Diese Werte sind Bitfelder, die durch OR ver-
bunden sind (die Klassen ios und filebuff sind beide Elternklasse von ostream).
!
Tipp
Der Ausdruck ios::out bezieht sich auf ein statisches Element der Klasse ios.
filebuf::openprot Kompatibilitäts-Sharing-Modus
Das folgende Programm z.B. öffnet eine Datei MYNAME und schreibt ein paar wichtige und
absolut der Wahrheit entsprechende Informationen hinein:
#include <fstream.h>
void fn()
{
// öffne die Textdatei MYNAME zum Schreiben –
// überschreibe, was in der Datei steht
ofstream myn(»MYNAME«);
myn << »Randy Davis ist höflich und hübsch\n«;
}
void fn()
{
// öffne die Binärdatei BINFILE zum Schreiben;
// wenn sie bereits existiert, füge ans Ende an
ofstream bfile(»BINFILE«, ios::binary | ios::ate);
//... Fortsetzung wie eben ...
}
= = Teil 6 – Sonntagnachmittag
= =
Hinweis
Streamausgabe geht der Ausnahmen-basierten Technik der Fehlerbehandlung
voraus, die in Sitzung 30 erklärt wird.
Lektion 29
Um zu überprüfen, ob die Dateien MYNAME und BINFILE in dem früheren Beispiel korrekt geöff-
net wurden, könnte ich schreiben:
#include <fstream.h>
void fn()
{
ofstream myn(»MYNAME«);
if (myn.bad()) // wenn das Öffnen fehlschlägt ...
{
cerr << »Fehler beim Öffnen von MYNAME\n«;
return; //... Fehler ausgeben und fertig
}
myn << »Randy Davis ist höflich und hübsch\n«;
}
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 336
336 Sonntagnachmittag
Alle Versuche, Ausgaben mit einem ofstream-Objekt durchzuführen, das einen Fehler enthält,
haben keinen Effekt, bis der Fehler durch einen Aufruf der Elementfunktion clear( ) gelöscht wird.
!
Tipp
Dieser letzte Paragraph ist wörtlich gemeint – es ist keine Ausgabe möglich, so
lange das Fehlerflag nicht null ist.
Der Destruktor der Klasse ofstream schließt die Datei automatisch. Im vorangegangenen Bei-
spiel wurde die Datei bei Verlassen der Funktion geschlossen.
Die Klasse ifstream arbeitet auf die gleiche Weise bei der Eingabe, wie das folgende Beispiel
zeigt:
#include <fstream.h>
void fn()
{
// öffnet Datei zum Lesen; erzeuge die
// Datei nicht, wenn sie nicht existiert
ifstreambankStatement(»STATEMNT«, ios::nocreate);
if (bankStatement.bad())
{
cerr << »Datei STATEMNT nicht gefunden\n«;
return;
}
while (!bankStatement.eof())
{
bankStatement >> nAccountNumber >> amount;
// ... verarbeite Abhebung
}
}
Die Funktion öffnet die Datei STATEMNT durch die Erzeugung des Objektes bankStatement.
Wenn die Datei nicht existiert, wird sie erzeugt. (Wir nehmen an, dass die Datei Informationen für
uns hat, es würde daher keinen Sinn machen, eine neue, leere Datei zu erzeugen.) Wenn das Objekt
fehlerhaft ist (z.B. wenn das Objekt nicht erzeugt wurde), gibt die Funktion eine Fehlermeldung aus
und beendet die Ausführung. Andernfalls durchläuft die Funktion eine Schleife und liest dabei nAc-
countNumber und den Abhebungsbetrag amount, bis die Datei leer ist (end-of-file ist wahr).
Der Versuch, aus einem ifstream-Objekt zu lesen, das einen Fehler enthält, kehrt sofort zurück,
ohne etwas gelesen zu haben.
Lassen Sie mich erneut warnen. Es wird nicht nur nichts zurückgegeben, wenn
aus einem Eingabestream gelesen wird, der einen Fehler enthält, sondern der
!
Tipp
Puffer kommt unverändert zurück. Das Programm kann leicht den falschen
Schluss daraus ziehen, dass die gleiche Eingabe wie zuvor gelesen wurde.
Schließlich wird eof( ) nie true liefern auf einem Stream, der sich im Fehlerzu-
stand befindet.
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 337
Die Klasse fstream ist wie eine Klasse, die ifstream und ofstream kombiniert (in der Tat erbt sie
von beiden). Ein Objekt der Klasse fstream kann zur Eingabe oder zur Ausgabe geöffnet werden
oder für beides.
#include <strstrea.h>
// <strstream.h> für GNU C++
char* parseString(char *pszString)
{
// assoziiere ein istrstream-Objekt mit der
// Eingabezeichenkette
istrstream inp(pszString, 0);
return pszBuffer;
Lektion 29
Die Funktion scheint komplizierter zu sein, als sie sein müsste, parseString( ) ist jedoch einfach
zu schreiben aber sehr robust. Die Funktion parseString( ) kann jeden Typ Input behandeln, den
der C++-Extraktor behandeln kann, und sie hat alle Formatierungsfähigkeiten des C++-Inserters.
Außerdem ist die Funktion tatsächlich sehr einfach, wenn Sie verstanden haben, was sie tut.
Lassen Sie uns z.B. annehmen, dass pszString auf die folgende Zeichenkette zeigt:
»1234 100.0«
Die Funktion parseString( ) assoziiert das Objekt inp mit der Eingabezeichenkette, indem die-
ser Wert an den Konstruktor von istrstream übergeben wird. Das zweite Argument des Konstruk-
tors ist die Länge der Zeichenkette. In diesem Beispiel ist das Argument gleich 0, was bedeutet »lies
bis zum terminierenden Nullzeichen«.
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 338
338 Sonntagnachmittag
Die Extraktor-Anweisung inp >> liest erst die Kontonummer, 1234, in die int-Variable nAc-
countNumber, genauso, als wenn sie von der Tastatur oder aus einer Datei gelesen würde. Der zwei-
te Teil liest den Wert 100.0 in die Variable dBalance.
Bei der Ausgabe wird das Objekt out assoziiert mit dem 128 Zeichen umfassenden Puffer, auf den
pszBuffer zeigt. Auch hier gibt das zweite Argument die Länge des Puffers an – für diesen Wert
kann es keinen Default-Wert geben, weil ofstream keine Möglichkeit hat, die Länge des Puffers sel-
ber festzustellen (es gibt hier kein abschließendes Nullzeichen). Ein drittes Argument, das dem
Modus entspricht, hat ios::out als Default-Wert. Sie können dieses Argument jedoch auf ios::ate
setzen, wenn Sie die Ausgabe an das hängen möchten, was sich bereits im Puffer befindet, anstatt
den Puffer zu überschreiben.
Die Funktion gibt dann das out-Objekt aus – das erzeugt die formatierte Ausgabe in den Puffer
der 128 Zeichen. Schließlich gibt die Funktion parseString( ) den Puffer zurück. Die lokal defi-
nierten Objekte inp und out werden bei Rückkehr der Funktion vernichtet.
=
= =
= Die Konstante ends, die am Ende des Inserter-Kommandos steht, ist nötig, um
Hinweis den Null-Terminator an das Ende der Pufferzeichenkette anzufügen.
Der Puffer, der durch den vorangegangenen Codeschnipsel zurückgegeben wurde, enthält die
folgende Zeichenkette:
protected:
int nDollars;
int nCents;
};
USDollar::USDollar(int d, int c)
{
// speichere die initialen Werte
nDollars = d;
nCents = c;
rationalize();
}
strcpy(pszBuffer, »$«);
strcat(pszBuffer, cDollarBuffer);
strcat(pszBuffer, ».«);
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 340
340 Sonntagnachmittag
strcat(pszBuffer, cCentsBuffer);
return pszBuffer;
}
return 0;
}
Ausgabe
Dollar d1 = $1.60
Dollar d2 = $1.05
Das Programm ToStringWOStream stützt sich nicht auf die Streamroutinen, um den Text für das
USDollar-Objekt zu erzeugen. Die Funktion USDollar::output( ) macht intensiven Gebrauch
von der Funktion ltoa( ), die long in eine Zeichenkette verwandelt, und von den Funktionen
strcpy( ) und strcat( ), die direkte Manipulationen auf Zeichenketten ausführen. Die Funktio-
nen müssen selber mit dem Fall zurecht kommen, dass die Anzahl Cents kleiner als 10 ist und daher
nur eine Stelle belegt. Die Ausgabe des Programms finden Sie am Ende des Listings.
Das Folgende zeigt eine Version von USDollar::output( ), die die Klasse ostrstream verwen-
det.
char* USDollar::output()
{
// alloziere einen Puffer
char* pszBuffer = new char[128];
return pszBuffer;
}
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 341
.
CD-ROM
Diese Version ist im Programm ToStringWStreams auf der beiliegenden CD-
ROM enthalten.
Diese Version assoziiert den Ausgabestream out mit einem lokal definierten Puffer. Sie schreibt
dann die nötigen Werte unter Verwendung der üblichen Stream-Inserter und gibt den Puffer
zurück. Das Setzen der Breite auf 2 stellt sicher, dass die Anzahl der verwendeten Stellen auch dann
zwei ist, wenn der Wert kleiner als 10 ist. Die Ausgabe dieser Version ist identisch mit der Ausgabe
von Listing 29-1. Das out-Objekt wird vernichtet, wenn die Kontrolle die Funktion output( ) ver-
lässt.
Ich finde, dass die Stream-Version von output( ) viel besser verfolgt werden kann und weniger
langweilig ist als die frühere Version, die keine Streams nutzte.
29.4 Manipulatoren.
Bis jetzt haben wir gesehen, wie Stream-I/O verwendet werden kann, um Zahlen und Zeichenkette
auszugeben unter Verwendung von Default-Formaten. Normalerweise sind die Defaults in Ord-
nung, aber manchmal treffen sie es einfach nicht. Weil dies so ist, stellt C++ zwei Wege bereit, um
die Formatierung der Ausgabe zu kontrollieren.
Erstens kann der Aufruf einer Reihe von Elementfunktionen des Stream-Objektes das Format steu-
ern. Sie haben das in einer früheren Elementfunktion display( ) gesehen, in der fill(‘0’) und
width(2) die minimale Breite und das Linksfüllzeichen eines ostrstream-Objektes gesetzt haben.
=
= =
=
Das Argument out stellt ein ostream-Objekt dar. Weil ostream Basisklasse für
ofstream und ostrstream ist, funktioniert die Funktion gleich gut für die Aus-
Hinweis gabe in eine Datei und einen im Programm bereitgestellten Puffer.
Teil 6 – Sonntagnachmittag
Ein zweiter Zugang ist der über Manipulatoren. Manipulatoren sind Objekte, die in der Include-
Datei iomanip.h definiert sind, die den gleichen Effekt haben wie Aufrufe von Elementfunktionen.
Lektion 29
Der einzige Vorteil von Manipulatoren ist, dass das Programm sie direkt in den Stream einfügen
kann und keinen separaten Funktionsaufruf ausführen muss.
Die Funktion display( ) kann mit Manipulatoren wie folgt umgeschrieben werden:
char* USDollar::output()
{
// alloziere einen Puffer
char* pszBuffer = new char[128];
342 Sonntagnachmittag
return pszBuffer;
}
Die geläufigsten Manipulatoren und ihre Bedeutung finden Sie in Tabelle 29-4.
Sehen Sie nach dem Breitenparameter (Funktion width( ) und Manipulator setw( )). Die meis-
ten Parameter behalten ihren Wert, bis sie durch einen weiteren Aufruf neu gesetzt werden, aber der
Breitenparameter verhält sich so nicht. Der Breitenparameter wird auf seinen Default-Wert gesetzt,
sobald die nächste Ausgabe erfolgt. Sie könnten z.B. von dem Folgenden erwarten, dass Integer-
zahlen mit acht Zeichen erzeugt werden:
#include <iostream.h>
#include <iomanip.h>
void fn()
{
cout << setw(8) // Breite ist 8...
<< 10 // ... für die 10, aber...
<< 20 // ... default für die 20
<< »\n«;
}
Was Sie jedoch erhalten, ist eine Integerzahl mit acht Zeichen, gefolgt von einer Integerzahl mit
zwei Zeichen. Um auch für die zweite Zahl acht Zeichen zu bekommen, ist das Folgende notwendig:
#include <iostream.h>
#include <iomanip.h>
void fn()
{
cout << setw(8) // setze die Breite ...
<< 10
<< setw(8) // ... und setze sie wieder
<< 20
C++ Lektion 29 31.01.2001 12:51 Uhr Seite 343
<< »\n«;
}
Was ist besser, Manipulatoren oder Aufrufe von Elementfunktionen? Elementfunktionen erlauben
etwas mehr Kontrolle, weil es mehr davon gibt. Außerdem geben die Elementfunktionen die vorhe-
rigen Einstellungen zurück, was Sie nutzen können, um die Werte wieder zurückzusetzen, wenn Sie
das möchten. Schließlich hat jede Funktion eine Version ohne Argumente, um den aktuellen Wert
zurückzugeben, falls Sie die Einstellungen später wieder zurücksetzen möchten.
return out;
}
344 Sonntagnachmittag
return 0;
}
Der Inserter führt die gleichen elementaren Operationen aus wie die frühere Funktion display( ),
wobei hier direkt in das ostream-Ausgabeobjekt ausgegeben wird, das übergeben wurde. Die Funk-
tion main( ) ist jedoch noch einfacher als vorher. Dieses Mal kann das USDollar-Objekt direkt in
den Ausgabestream eingefügt werden.
Sie wundern sich vielleicht, warum operator<<( ) das ostream-Objekt zurückgibt, das überge-
ben wurde. Der Grund ist, dass dadurch Einfügeoperationen verkettet werden können. Weil opera-
tor<<( ) von links nach rechts bindet, wird der folgende Ausdruck
interpretiert als
Die erste Eingabe gibt die Zeichenkette »Dollar d1 = « nach cout aus. Das Ergebnis dieses Aus-
drucks ist das Objekt cout, das dann an operator<<(ostream&, USDollar&) übergeben wird. Es
ist wichtig, dass dieser Operator sein ostream-Objekt zurückgibt, so dass dieses Objekt an den nächs-
ten Inserter übergeben werden kann, der das Zeilenendezeichen »\n« ausgibt.
{
friend ostream& operator<<(ostream& out, Currency& d);
public:
Currency(int p = 0, int s = 0)
{
nPrimary = p;
nSecondary = s;
}
protected:
int nPrimary;
int nSecondary;
};
{
}
// Ausgaberoutine
virtual ostream& display(ostream& out)
{
char old = out.fill();
out << »$«
<< nPrimary
<< ».«
<< setfill(‘0’) << setw(2)
<< nSecondary;
346 Sonntagnachmittag
return out;
}
};
return 0;
}
Die Klasse Currency definiert eine Inserter-Funktion, die ein Nichtelement ist, und daher mit
Polymorphie nichts zu tun hat. Aber statt wirklich etwas zu tun, stützt sich der Inserter auf eine vir-
tuelle Elementfunktion display( ), die die eigentliche Arbeit ausführt. Die Unterklasse USDollar
muss nur die Funktion display( ) bereitstellen; das ist alles. Diese Version des Programms erzeugt
die gleiche Ausgabe wie am Ende von Listing 29-1 zu sehen ist.
Dass die Einfügeoperation in der Tat polymorph ist, wird bei der Erzeugung der Ausgabefunktion
fn(Currency&, char*) deutlich. Die Funktion fn( ) kennt nicht den Typ der Währung, den sie
übergeben bekommt, und stellt die übergebene Währung mit den für USDollar geltenden Regeln
dar. main( ) gibt d1 direkt und d2 über diese neue Funktion fn( ) aus. Die virtuelle Ausgabe von
fn( ) sieht genauso aus, wie die des polymorphen Bruders.
Andere Unterklassen von Currency, wie z.B. DMark, FFranc oder Euro, können erzeugt werden,
obwohl sie unterschiedliche Darstellungsregeln haben, indem einfach die entsprechenden dis-
play( )-Funktionen bereitgestellt werden. Der Basiscode kann weiterhin ungestraft Currency ver-
wenden.
Drittens bindet der Linksshiftoperator von links nach rechts. Das erlaubt es uns, Ausgabeanwei-
sungen zu verketten. Die vorige Zeile wird z.B. interpretiert als:
Trotz all dieser Gründe ist der eigentliche Grund sicherlich, dass es schön ist. Das
doppelte Kleinerzeichen << sieht so aus, als wenn etwas den Code verlassen wollte,
und das doppelte Größerzeichen >> sieht so aus, als wenn etwas hereinkommen
wollte. Und, warum eigentlich nicht?
0 Min.
Zusammenfassung.
Ich habe diese Sitzung mit einer Warnung begonnen, dass Stream-I/O zu komplex ist, um in einem
Kapitel eines Buches abgehandelt zu werden. Sie können die Dokumentation Ihres Compilers bemü-
hen, um eine vollständige Liste aller Elementfunktionen zu erhalten, die sie aufrufen können. Die
relevanten Include-Dateien, wie iostream.h und iomanip.h, enthalten Prototypen mit erklärenden
Kommentaren für alle Funktionen.
• Stream-I/O basiert auf den Klassen istream und ostream.
• Die Include-Datei iostream.h überlädt den Linksshift-Operator, um Ausgaben nach ostream aus-
zuführen und überlädt den Rechtsshift-Operator, um Eingaben von istream auszuführen.
• Die Unterklasse fstream wird für Datei-I/O verwendet.
• Die Unterklasse strstream führt I/O auf internen Speicherpuffern durch, unter Verwendung der
gleichen Einfüge- und Extraktionsoperatoren.
• Der Programmierer kann die Einfüge- und Extraktionsoperatoren überladen für seine eigenen
Klassen. Diese Operatoren können polymorph gemacht werden durch die Verwendung virtueller
Zwischenmethoden.
• Die Manipulatorobjekte, die in iomanip.h definiert werden, können verwendet werden, um For-
matfunktionen von stream aufzurufen.
Teil 6 – Sonntagnachmittag
Selbsttest.
1. Wie werden die beiden Operatoren << und >> genannt, wenn sie für Stream-I/O verwendet wer-
den? (Siehe »Wie funktioniert Stream-I/O?«)
Lektion 29
2. Was ist die Basisklasse der beiden Default-I/O-Objekte cout und cin? (Siehe »Wie funktioniert
Stream-I/O?«)
3. Wofür wird die Klasse fstream verwendet? (Siehe »Die Unterklassen fstream«)
4. Wofür wird die Klasse strstream verwendet? (Siehe »Die Unterklassen strstream«)
5. Welcher Manipulator setzt den nummerischen Ausgabemode auf hexadezimal? Was ist die zuge-
hörige Elementfunktion? (Siehe »Manipulatoren«)
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 348
30 Lektion Ausnahmen
Checkliste.
✔ Fehlerbedingungen zurückgeben
✔ Ausnahmen verwenden, ein neuer Mechanismus zur Fehlerbehandlung
✔ Auslösen und Abfangen von Ausnahmen
✔ Überladen der Ausnahmeklasse
Z
usätzlich zu dem allgegenwärtigen Ansatz der Fehlerausgaben enthält C++
einen einfacheren und verlässlicheren Mechanismus zur Fehlerbehandlung.
Diese Technik, die Ausnahmebehandlung genannt wird, ist der Gegenstand
dieser Sitzung.
30 Min.
.
CD-ROM
Die Fakultätsfunktion finden Sie auf der beiliegenden CD-ROM im Programm
FactorialProgram.cpp.
do
{
nFactorial *= nBase;
} while (—nBase > 1);
Obwohl die Funktion sehr einfach ist, fehlt ihr ein kritisches Feature: Die Fakultät von 0 ist 1, wäh-
rend die Fakultät einer negativen Zahl nicht definiert ist. Die obige Funktion sollte einen Test enthal-
ten für negative Argumente und eine Fehlermeldung ausgeben, falls ein solches übergeben wird.
Der klassische Weg, einen Fehler in einer Funktion anzuzeigen, ist die Rückgabe eines Wertes, der
sonst nicht von der Funktion zurückgegeben werden kann. Z.B. ist es nicht möglich, dass die Fakul-
tät negativ ist. Wenn der Funktion also ein negativer Wert übergeben wird, könnte sie z.B. -1 zurück-
geben. Die aufrufende Funktion kann den Rückgabewert überprüfen – wenn er negativ ist, weiß die
aufrufende Funktion, dass ein Fehler aufgetreten ist und kann eine entsprechende Aktion auslösen
(was immer das dann ist).
Das ist die Art und Weise der Fehlerbehandlung, die seit den frühen Tagen von FORTRAN prakti-
ziert wurde. Warum sollte das geändert werden?
gabewert zu speichern.
Drittens ist die Behandlung von Fehlern optional. Nehmen Sie an, jemand schreibt factorial( )
so, dass sie die Argumente überprüft und einen negativen Wert zurückgibt, wenn das Argument
negativ ist. Wenn der Code, der diese Funktion benutzt, den Rückgabewert nicht überprüft, hilf das
gar nichts. Natürlich sprechen wir alle möglichen Drohungen aus »Sie werden Ihre Fehlerrückgaben
überprüfen oder ...« aber wir wissen alle, dass die Sprache (und Ihr Chef) nichts tun kann, wenn Sie
es unterlassen.
Selbst wenn ich die Fehlerrückgabe von factorial( ) oder einer anderen Funktion überprüfe,
was kann meine Funktion mit dem Fehler anfangen? Sicherlich nicht mehr, als eine Fehlermeldung
auszugeben oder selber einen Fehlercode an die aufrufende Funktion zurückzugeben. Schnell sieht
der Code dann so aus:
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 350
350 Sonntagnachmittag
nErrorRtn = someOtherFunc();
if (nErrorRtn)
{
errorOut(»Fehler beim Aufruf von someOtherFunc()«);
return MY_ERROR_2;
}
{
// wenn nBase < 0...
if (nBase <= 0)
{
// ... löse einen Fehler aus
throw »Ausnahme ungültiges Argument«;
}
int nFactorial = 1;
do
{
nFactorial *= nBase;
} while (—nBase > 1);
return 0;
}
Die Funktion main( ) beginnt mit einem Block, der mit dem Schlüsselwort try markiert ist. Einer
oder mehr catch-Blöcke stehen unmittelbar hinter dem try-Block. Das Schlüsselwort try wird von
einem einzigen Argument gefolgt, das wie eine Funktionsdefinition aussieht.
Innerhalb des try-Blocks kann main( ) tun, was sie will. In diesem Fall geht main( ) in eine Schlei-
fe, die die Fakultät von absteigenden Zahlen berechnet. Schließlich übergibt das Programm eine
negative Zahl an die Funktion factorial( ).
Wenn unsere schlaue Funktion factorial( ) eine solche falsche Anfrage bekommt, »wirft« sie
eine Zeichenkette, die eine Beschreibung des Fehlers enthält, unter Verwendung des Schlüsselwor-
tes throw.
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 352
352 Sonntagnachmittag
An diesem Punkt sucht C++ nach einem catch-Block, dessen Argument zu dem ausgelösten
Objekt passt. Der Rest des try-Blocks wird nicht fertig abgearbeitet. Wenn C++ kein catch in der
aktuellen Funktion findet, kehrt C++ zum Ausgangspunkt des Aufrufes zurück und setzt die Suche
dort fort. Der Prozess wird fortgesetzt, bis ein passender catch-Block gefunden wird oder die Kon-
trolle main( ) verlässt.
In diesem Beispiel wird die ausgelöste Fehlermeldung durch den catch-Block am Ende der Funk-
tion main( ) abgefangen, der eine Meldung ausgibt. Die nächste Anweisung ist das return-Kom-
mando, wodurch das Programm beendet wird.
protected:
char label;
};
void f1();
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 353
void f2();
void f1()
{
try
{
Obj c(‘c’);
f2();
}
catch(char* pMsg)
{
cout << »Zeichenkette abgefangen« << endl;
}
} Teil 6 – Sonntagnachmittag
void f2()
{
Obj d(‘d’);
Lektion 30
throw 10;
}
Ausgabe:
Konstruktor a
Konstruktor b
Konstruktor c
Konstruktor d
Destruktor d
Destruktor c
Destruktor b
int abgefangen
Destruktor a
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 354
354 Sonntagnachmittag
Zuerst werden die vier Objekte a, b, c und d konstruiert, wenn die Kontrolle ihre Deklarationen
antrifft, bevor f2( ) die int 10 auslöst. Weil kein try-Block definiert ist innerhalb von f2( ), wickelt
C++ den Stack von f2 ab, was zur Vernichtung von d führt. f1( ) definiert einen try-Block, aber ihr
einziger catch-Block verarbeitet char*, und passt daher nicht zu dem ausgelösten int, und C++
setzt die Suche fort. Das wickelt den Stack von f1( ) ab, wodurch das Objekt c vernichtet wird.
Zurück in main( ) findet C++ einen weiteren try-Block. Das Verlassen dieses Block vernichtet b.
Der erste catch-Block verarbeitet float, und passt daher nicht. Der nächste catch-Block passt
exakt. Der letzte catch-Block, der jedes beliebige Objekt verarbeiten würde, wird nicht mehr betre-
ten, weil bereits ein passender catch-Block gefunden wurde.
=
= =
=
Eine Funktion, die als fn(...) deklariert ist, akzeptiert eine beliebige Anzahl
von Argumenten mit beliebigem Typ. Das gleiche gilt für catch-Blöcke. Ein
Hinweis catch(...) fängt alles ab.
#include <iostream.h>
#include <string.h>
// Exception – generische Klasse für Fehlerbehandlung
class Exception
{
public:
// konstruiere ein Ausnahmeobjekt mit einem
// Beschreibungstext des Problems, zusammen mit
// der Datei und der Zeilennummer, wo das
// Problem aufgetreten ist
Exception(char* pMsg, char* pFile, int nLine)
{
this->pMsg = new char[strlen(pMsg) + 1];
strcpy(this->pMsg, pMsg);
protected:
// Fehlermeldung
char* pMsg;
=
= =
= FILE__ und LINE__ sind elementare #defines, die auf den Namen der Quellda-
Hinweis tei und die aktuelle Zeile in der Datei gesetzt sind.
void myFunc()
{
Lektion 30
try
{
//... was auch immer aufgerufen wird
}
356 Sonntagnachmittag
Der catch-Block nimmt das Exception-Objekt und verwendet dann die eingebaute Element-
funktion display( ) zur Anzeige der Fehlermeldung.
.
CD-ROM
Die Version von factorial, die von der Klasse Exception Gebrauch macht, ist auf
der beiliegenden CD-ROM als FactorialThrow.cpp enthalten.
factorial(6) = 720
factorial(5) = 120
factorial(4) = 24
factorial(3) = 6
factorial(2) = 2
factorial(1) = 1
Error <Negatives Argument für factorial>
in Zeile #59,
in Datei C:\wecc\Programs\lesson30\FactorialThrow.cpp
Die Klasse Exception stellt eine generische Klasse zum Melden von Fehlern dar. Sie können
Unterklassen von dieser Klasse ableiten. Ich könnte z.B. eine Klasse InvalidArgumentException
ableiten, die den unzulässigen Argumentwert speichert, zusammen mit dem Fehlertext und dem
Ort des Fehlers.
protected:
int invArg;
};
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 357
!
Tipp
Die Funktion InvalidArgumentException::display( ) stützt sich auf die
Basisklasse Exception, um den Teil des Objektes auszugeben, der von Exception
stammt.
void myFunc()
{
try
{
// ... was auch immer aufgerufen wird
}
// fange eine Zeichenkette ab
catch(char* pszString)
{
cout << »Fehler: » << pszString << »\n«;
}
// fange ein Exception-Objekt ab
catch(Exception x)
{
x.display(cerr);
}
Teil 6 – Sonntagnachmittag
}
In diesem Beispiel wird ein ausgelöstes Objekt, wenn es eine einfache Zeichenkette ist, vom ersten
catch-Block abgefangen, der die Zeichenkette ausgibt. Wenn das Objekt keine Zeichenkette ist,
Lektion 30
wird es mit der Exception-Klasse verglichen. Wenn das Objekt eine Exception ist oder aus einer
Unterklasse von Exception stammt, wird es vom zweiten catch-Block verarbeitet.
Weil sich dieser Prozess seriell vorgeht, muss der Programmierer mit den spezielleren Objektty-
pen anfangen und bei den allgemeineren aufhören. Es ist daher ein Fehler, das Folgende zu tun:
void myFunc()
{
try
{
// ... was auch immer aufgerufen wird
}
catch(Exception x)
{
x.display(cerr);
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 358
358 Sonntagnachmittag
}
catch(InvalidArgumentException x)
{
x.display(cerr);
}
}
0 Min.
!
Tipp
Der Compiler fängt diesen Programmierfehler nicht ab.
=
= =
=
Es macht im obigen Beispiel auch keinen Unterschied, dass display( ) virtuell
ist. Der catch-Block für Exception ruft die Funktion display( ) in Abhängig-
Hinweis keit vom Laufzeittyp des Objektes auf.
=
= =
= Weil der generische catch-Block catch(...) jede Ausnahme abfängt, muss er
Hinweis als letzter in einer Reihe von catch-Blöcken stehen. Jeder catch-Block hinter
einem generischen catch ist unerreichbar.
Zusammenfassung.
Der Ausnahmemechanismus von C++ stellt einen einfachen, kontrollierten und erweiterbaren
Mechanismus zur Fehlerbehandlung dar. Er vermeidet die logische Komplexität, die bei dem Stan-
dardmechanismus der Fehlerrückgabewerte entstehen kann. Er stellt außerdem sicher, dass Objekte
korrekt vernichtet werden, wenn sie ihre Gültigkeit verlieren.
• Die konventionelle Technik, einen ansonsten ungültigen Wert zurückzugeben, um dem Typ des
Fehlers anzuzeigen, hat ernsthafte Begrenzungen. Erstens kann nur eine begrenzte Menge an
Information kodiert werden. Zweitens ist die aufrufende Funktion gezwungen, den Fehler zu
behandeln, indem er verarbeitet oder zurückgegeben wird, ob sie nun etwas an dem Fehler
machen kann oder nicht. Schließlich haben viele Funktionen keinen ungültigen Wert, der so
zurückgegeben werden könnte.
• Die Technik der Ausnahmefehler ermöglicht es Funktionen, eine theoretisch nicht begrenzte An-
zahl von Informationen zurückzugeben. Wenn eine aufrufende Funktion einen Fehler ignoriert,
wird der Fehler die Kette der Funktionsaufrufe nach oben propagiert, bis eine Funktion gefunden
wird, die den Fehler behandeln kann.
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 359
• Ausnahmen können Unterklassen sein, was die Flexibilität für den Programmierer erhöht.
• Es können mehrere catch-Blöcke verkettet werden, um die aufrufende Funktion in die Lage zu
versetzen, verschiedene Fehlertypen zu behandeln.
Selbsttest.
1. Nennen Sie drei Begrenzungen der Fehlerrückgabetechnik. (Siehe »Konventionelle Fehlerbe-
handlung«)
2. Nennen Sie die drei Schlüsselwörter, die von der Technik der Ausnahmebehandlung verwendet
werden. (Siehe »Wie arbeiten Ausnahmen?«)
Teil 6 – Sonntagnachmittag
Lektion 30
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 360
6
T EIL Sonntagnachmittag –
Zusammenfassung
1. Schreiben Sie den Kopierkonstruktor und den Zuweisungsoperator für das Modul Assign-
Problem. Eine Ressource muss geöffnet werden mit dem nValue-Wert des MyClass-
Objekts, und diese Ressource muss
Nehmen Sie an, dass die Prototypfunktionen irgendwo anders definiert sind.
class MyClass;
~MyClass()
{
C++ Lektion 30 31.01.2001 12:52 Uhr Seite 361
resource.close();
}
protected:
Resource resource;
2. Schreiben Sie einen Inserter für das folgende Programm, das den Nachnamen, den Vorna-
men und Studenten-ID für die folgende Student-Klasse ausgibt.
// StudentInserter
#include <stdio.h>
#include <iostream.h>
#include <string.h>
// Student
class Student
{
public:
Student(char* pszFName, char* pszLName, int nSSNum)
{
strncpy(szFName, pszFName, 20);
strncpy(szLName, pszLName, 20);
this->nSSNum = nSSNum;
}
protected:
char szLName[20];
char szFName[20];
int nSSNum;
};
fragen
Freitagabend.
1. Unsere Lösung ist einfacher als das Programm zum Entfernen von Reifen. Wir müssen den
Wagenheber nicht erst greifen. Weil das Auto bereits in der Luft ist, gehalten vom Wagenhe-
ber, können wir davon ausgehen, dass wir wissen, wo sich der Wagenheber befindet.
2. Das Entfernen des Minuszeichens zwischen 212 und 32 veranlasst Visual C++ zur fehlerhaf-
ten Fehlermeldung »missing ;«.
3. Ich habe das Problem behoben, indem ich ein Semikolon nach 212 eingefügt habe und das
Programm neu erzeugt habe. Visual C++ erzeugt das »korrigierte« Programm ohne Bean-
standung.
4. Das korrigierte Programm berechnet eine Temperatur in Fahrenheit von 244, was offen-
sichtlich falsch ist.
5. Offensichtlich sind nFactor = 212; und 32; legale Kommandos. Der falsche Ergebniswert von
nFactor kommt von der fehlerhaften Durchführung der Konvertierung.
6. Ein Minuszeichen zu vergessen, ist ein verständlicher Fehler für einen so schlechten Tipper wie
mich. Hätte Visual C++ den Fehler so behoben wie es dachte, dass ein Semikolon fehlt, wäre
das Programm ohne Fehler erzeugt worden; die Ausführung liefert jedoch ein falsches Ergebnis.
Ich hätte selber durch das Programm gehen müssen, um den Fehler in der Berechnung zu fin-
den. Das schafft Misstrauen in die Umrechnungsformel zwischen Celsius und Fahrenheit, wäh-
rend das eigentliche Problem ein Tippfehler ist. Das ist Zeitverschwendung, wenn doch die Feh-
lermeldung von Visual C++, wenn auch nicht korrekt, mich direkt zum Ort des Fehlers führt.
Anhang 31.01.2001 11:47 Uhr Seite 364
364 Anhang A
7. Erneutes Erzeugen von Conversion.cpp nach dem Entfernen der Hochkommata erzeugt die-
se Fehlermeldung:
Conversion.cpp(31) Error: unterminated string or character constant
Diese Fehlermeldung zeigt, dass GNU C++ denkt, dass der Fehler in Zeile 31 aufgetreten ist.
(Das ist die Bedeutung von »31« in der Fehlermeldung.)
8. Weil GNU C++ keine Hochkommata gefunden hat, um die Zeichenkette in Zeile 28 zu
beenden, dachte es, dass die Zeichenkette bis zu den nächsten Hochkommata geht, was in
Zeile 31 der Fall ist. (Diese Hochkommata sind aber tatsächlich der Anfang einer neuen
Zeichenkette, aber GNU C++ weiß das nicht.)
Samstagmorgen.
1.
• Teilen von nTons durch 1,1 verursacht einen Rundungsfehler.
• 2/1,1 ist gleich 1,8, was auf 1 abgerundet wird. Die Funktion gibt 1000 kg zurück.
• Das Ergebnis der Division von 2 durch 1,1 ist ein double-Wert. Die Zuweisung dieses Werts
an nLongTons resultiert in einer Demotion, die vom Compiler bemerkt werden sollte.
Zusatz: »Assign double to int. Possible loss of significance.« oder etwas in dieser Art.
2. Die folgende Funktion hat weniger Probleme mit Rundungsfehlern als ihr Vorgänger:
3.
• 5000 Tonnen werden in 4.500.0000.0000 g konvertiert. Diese Zahl liegt außerhalb des
Bereiches von int auf einem Intel-basierten PC.
• Die einzige mögliche Lösung ist die Rückgabe eines float oder eines double anstelle von
int. Der Bereich von float ist viel größer als der Bereich von int.
4.
• falsch
• wahr
• wahr
• unbestimmt, aber wahrscheinlich wahr
Sie können nicht darauf zählen, dass zwei unabhängig voneinander berechnete Gleitkom-
mavariablen gleich sind.
Anhang 31.01.2001 11:47 Uhr Seite 365
• falsch
Der Wert von n4 ist gleich 4. Weil die linke Seite von && falsch ist, wird die rechte Seite nicht
ausgewertet und == wird nie ausgeführt (siehe den Abschnitt über kurze Schaltkreise).
5.
• 0x5D
• 93
• 1011 10102
Es ist am einfachsten, die Addition binär auszuführen. Erinnern Sie sich an die Regeln:
0 + 0 -> 0
1 + 0 -> 1
0 + 1 -> 1
1 + 1 -> 0, übertrage die 1
Alternativ konvertieren Sie die 93 * 2 = 186 zurück ins Binärformat.
Zusatz: 0101 11012 * 2 hat das gleiche Bitmuster, nur um eine Position nach links geshiftet,
und eine 0 an der Position ganz rechts.
• 0101 11112
Konvertiere 2 ins Binärformat 0000 00102 und verknüpfe die beiden Zahlen mit OR.
• wahr
Das Bit 0000 00102 ist nicht gesetzt. Somit ergibt eine AND-Verknüpfung der beiden
Null (0).
6. Erinnern Sie sich daran, dass C++ Leerraum ignoriert, Tabulatoren eingeschlossen. Während
der else-Zweig scheinbar zum äußeren if gehört, gehört es aber tatsächlich zum inneren
if, so, als wenn es so geschrieben worden wäre:
int n1 = 10;
if (n1 > 11)
{
if (n1 > 12)
{
n1 = 0;
}
else
{
n1 = 1;
}
}
Die äußere if-Anweisung hat keinen else-Zweig, und n1 bleibt daher unverändert.
Anhang 31.01.2001 11:47 Uhr Seite 366
366 Anhang A
7. Weil n1 nicht kleiner als 5 ist, wird der Body von while( ) nie ausgeführt. Im Falle von
do...while( ) wird der Body einmal ausgeführt, obwohl n1 nicht kleiner ist als 5.
In diesem Fall erhält n1 den Wert 11.
Der Unterschied zwischen den beiden Schleifen ist, dass do...while( ) seinen Body immer
mindestens einmal ausführt, selbst wenn die Bedingung gleich zu Beginn falsch ist.
8.
double cube(double d)
{
return d * d * d;
}
9. Der Ausdruck cube(3.0) passt zu der Funktion cube(double); somit wird cube(double)
der Wert 3.0 übergeben, die den Wert 9.0 zurückgibt, der zu 9 demotiert wird und der
Variablen n zugewiesen wird. Obwohl das Ergebnis das gleiche ist, ist der Weg dorthin ver-
schieden.
10. Der Compiler erzeugt einen Fehler, weil die erste Funktion cube(int) und diese neue Funk-
tion den gleichen Namen haben. Erinnern Sie sich daran, dass der Rückgabetyp nicht Teil
des Namens ist.
Samstagnachmittag.
1.
class Student
{
public:
char szLastName[128];
int nGrade; // 1-> 1. Grad, 2-> 2. Grad ...
double dAverage;
}
Anhang 31.01.2001 11:47 Uhr Seite 367
2.
void readAndDisplay()
{
Student s;
// Eingabe Studenteninformation
cout << »Name des Studenten:«;
cin.getline(s.szLastName);
cout << »Grad (1, 2, ...)\n«;
cin >> s.nGrade;
cout << »Durchschnitt:«;
cin >> s.dGPA;
3.
void readAndDisplayAverage()
{
Student s;
// Eingabe Studenteninformationen
cout << »Name des Stundenten:«;
cin.getline(s.szLastName);
cout << »Grad (1, 2, ...)\n«;
cin >> s.nGrade;
cout << »Durchschnitt:«;
368 Anhang A
4.
• 16 Bytes (4 + 4 + 4)
• 80 Bytes (4 * 20)
• 8 (4 + 4) Erinnern Sie sich daran, dass die Größe eines Zeigers 4 Bytes ist, unabhängig
davon, auf was der Zeiger zeigt.
5.
• Ja.
• Sie alloziert Speicher vom Heap, gibt diesen aber nicht zurück, bevor die Funktion verlassen
wird (so etwas wird als Speicherloch bezeichnet).
• Bei jedem Aufruf geht ein wenig Speicher verloren, bis der Heap aufgebraucht ist.
• Es kann sehr lange dauern, bis der Speicher aufgebraucht ist. Bei einem solch kleinen Spei-
cherloch muss das Programm viele Stunden laufen, nur damit das Problem überhaupt sicht-
bar wird.
6. dArray[0] ist bei 0x100, dArray[1] ist bei 0x108 und dArray[2] ist bei 0x110. Das Array
erstreckt sich von 0x100 bis 0x118.
7. Zuweisung 1 hat den gleichen Effekt wie dArray[1] = 1,0. Die Zuweisung 2 zerstört den
Gleitkommawert in dArray[2], das ist aber nicht fatal, weil 4 Bytes, die für int benötigt wer-
den, in die 8 Bytes, die für double alloziert wurden, hineinpassen.
8.
LinkableClass* removeHead()
{
LinkableClass* pFirstEntry;
pFirstEntry = pHead;
if (pHead != 0)
{
pHead = pHead->pNext;
}
return pFirstEntry;
}
Die Funktion removeHead( ) überprüft erst, ob der Kopfzeiger null ist. Wenn dies der Fall
ist, ist die Liste bereits leer. Wenn nicht, speichert die Funktion einen Zeiger auf das erste
Element in pFirstEntry. Sie bewegt dann pHead ein Element weiter, und gibt schließlich
pFirstEntry zurück.
Anhang 31.01.2001 11:47 Uhr Seite 369
9.
Die Funktion returnPrevious( ) gibt den Eintrag in der Liste zurück, der unmittelbar vor
*pTarget steht. Die Funktion beginnt damit, zu überprüfen, ob die Liste leer ist. Wenn sie
leer ist, gibt es keinen Vorgängereintrag, und die Funktion gibt null zurück.
returnPrevious( ) iteriert dann durch die Liste, wobei pCurrent auf den aktuellen Listen-
eintrag zeigt. In jedem Schleifendurchlauf überprüft die Funktion, ob das nächste Element
von pCurrent aus gleich pTarget ist. Ist dies der Fall, gibt die Funktion pCurrent zurück.
Wenn returnPrevious( ) durch die gesamte Liste durchgegangen ist, ohne pTarget zu
finden, gibt die Funktion null zurück.
10.
LinkableClass* returnTail()
{
// gib den letzten Eintrag der
// Liste zurück; das ist der Eintrag,
// dessen Nachfolger null ist
return returnPrevious(0);
}
Anhang 31.01.2001 11:47 Uhr Seite 370
370 Anhang A
Der Eintrag, auf den null folgt, ist der letzte Eintrag in der Liste.
Zusatzaufgabe:
LinkableClass* removeTail()
{
// finde den letzten Eintrag der Liste; wenn er
// null ist, dann ist die Liste leer
LinkableClass* pLast = returnPrevious(0);
if (pLast == 0)
{
return 0;
}
Die Funktion removeTail( ) entfernt das letzte Element in einer verketteten Liste. Sie
beginnt damit, die Adresse des letzten Elements durch einen Aufruf von returnPrevious(0)
zu bestimmen. Sie speichert diese Adresse in pLast. Wenn pLast gleich null ist, dann ist die
Liste leer und die Funktion gibt sofort null zurück.
Das letzte Element zu finden reicht nicht aus. Um das letzte Element zu entfernen, muss
removeTail( ) den Eintrag vor pLast finden, damit die beiden getrennt werden können.
removeTail( ) bestimmt die Adresse des Eintrages von pLast durch einen Aufruf return-
Previous(pLast). Wenn es keinen solchen Eintrag gibt, enthält die Liste nur ein Element.
removeTail( ) setzt den entsprechenden Zeiger auf null und gibt dann pLast zurück.
Anhang 31.01.2001 11:47 Uhr Seite 371
11. Ich habe für diese Lösung Visual C++ verwendet, um einen Vergleich zu rhide zu bekom-
men.
Ich beginne damit, das Programm auszuführen, einfach um zu sehen, was passiert. Wenn
das Programm funktioniert, dann ist es ja gut. Das Ergebnis bei Eingabe von »diese Zei-
chenkette« und »DIESE ZEICHENKETTE« sieht wie folgt aus:
DIE
Press any key to continue
Weil ich mir sicher bin, dass das Problem in concatString( ) liegt, setzte ich einen Halte-
punkt auf den Anfang der Funktion, und starte erneut. Nachdem der Haltepunkt angetrof-
fen wurde, scheinen die beiden Zeichenketten, Quelle und Ziel, korrekt zu sein, wie sie in
Abbildung 1.1 sehen können.
Abbildung A.1: Das Fenster zeigt die Quellzeichenkette und die Zielzeichenkette an.
Anhang 31.01.2001 11:47 Uhr Seite 372
372 Anhang A
Ausgehend vom Haltepunkt gehe ich in Einzelschritten vor. Zuerst scheinen die lokalen
Variablen in Ordnung zu sein. Sobald jedoch die while-Schleife betreten wird, wird sofort
klar, dass die Quellzeichenkette die Zielzeichenkette überschreibt; die Funktion concat-
String( ) ist eher eine Überschreibefunktion.
Die Funktion concatString( ) sollte damit anfangen, pszTarget an das Ende der Zei-
chenkette zu bewegen, bevor der Kopierprozess beginnt. Das Problem lässt sich nicht so
einfach im Debugger beheben, und ich füge den Extracode in das Programm ein.
Tatsächlich ist es möglich, dieses Problem mit Hilfe des Debuggers zu lösen. In
=
= =
=
der Anzeige der lokalen Variablen, klicken Sie auf den Wert des Zeigers in der
Spalte rechts neben pszTarget. Was immer dort für ein Wert steht, addieren Sie
Hinweis 0x12 (es gibt 18 Zeichen in »diese Zeichenkette«). Der Zeiger pszTarget zeigt
jetzt auf das Ende der Zielzeichenkette, und das Kopieren der Zeichen kann
beginnen.
Die geänderte Funktion concatString( ) sieht wie folgt aus:
Ich setze den Haltepunkt auf das zweite while, das ist die Schleife, die das Kopieren aus-
führt, und starte das Programm erneut mit der gleichen Eingabe. Die lokalen Variablen
sind korrekt, wie in Abbildung A.2 zu sehen ist.
Anhang 31.01.2001 11:47 Uhr Seite 373
Abbildung A.2: pszTarget sollte auf eine Null zeigen, bevor das Kopieren durchgeführt wird.
=
= =
= Die Variable pszTarget sollten auf die Null am Ende der Zielzeichenkette zei-
Hinweis gen, bevor das Programm die Quellzeichenkette kopieren kann.
Voller Vertrauen, versuche ich, durch die while-Schleife hindurchzugehen. Sobald ich
jedoch »Step Over« drücke, überspringt das Programm die Schleife. Offensichtlich ist die
while-Bedingung selbst zu Beginn nicht wahr. Nach kurzem Nachdenken stelle ich fest, dass
die Bedingung falsch ist. Anstatt anzuhalten, wenn pszTarget gleich null ist, sollte ich
anhalten, wenn pszSource gleich null ist. Umschreiben der while-Schleife, wie folgt, löst das
Problem:
while(*pszSource)
Ich starte das Programm erneut mit der gleichen Eingabe, und der Debugger zeigt an, dass
alles in Ordnung zu sein scheint. Und das Programm erzeugt auch die richtige Ausgabe.
Samstagabend.
1. Ich finde Hemden und Hosen, die Unterklassen von Kleidungsstück sind, was Unterklasse
von Bekleidung ist. In der Reihe der Bekleidungen stehen auch Schuhe und Sockenpaare. Die
Sockenpaare können weiter unterteilt werden in die Paare, die zusammenpassen, und in die
Paare, die nicht passen, usw.
2. Schuhe haben zumindest eine Öffnung, um den Fuß hineinzustecken. Schuhe haben eine Art
Befestigungssystem, um sie am Fuß zu halten. Schuhe haben eine Sohle, um sie gegen den
Untergrund zu schützen. Das ist alles, was ich über meine Schuhe sagen kann.
3. Ich habe Anzugsschuhe und Fahrradschuhe. Ich kann meine Fahrradschuhe zur Arbeit
anziehen, es ist aber sehr schwer, darin zu laufen. Sie umschließen jedoch die Füße, und die
Arbeit würde dadurch nicht zum Erliegen kommen.
Anhang 31.01.2001 11:47 Uhr Seite 374
374 Anhang A
So nebenbei – ich habe auch ein paar Kombinationsschuhe, die das Verbindungselement zur
Pedale in die Sohle eingelassen haben. In diesen Schuhen zur Arbeit zu gehen wäre nicht
ganz so schlecht.
4. Der Konstruktor eines Objektes der verketteten Liste muss sicherstellen, dass der Zeiger auf
das nächste Element auf null zeigt, wenn das Objekt konstruiert wird. Es ist nicht notwen-
dig, etwas mit den statischen Elementen zu tun.
class Link
{
static Link* pHead;
Link* pNextLink;
Link()
{
pNextLink = 0;
}
};
Der Punkt ist, dass das statische Element pHead nicht im Konstruktor initialisiert werden
kann, weil es sonst jedes Mal initialisiert wird, wenn ein neues Objekt erzeugt wird.
Wenn das Objekt in einer verketteten Liste steht, dann muss der Destruktor es entfernen,
bevor das Objekt wiederverwendet und der Zeiger pNext verloren ist. Wenn das Objekt
zusätzlich einen Speicherbereich »besitzt«, muss dieser zurückgegeben werden. In gleicher
Weise führt der Kopierkonstruktor eine tiefe Kopie aus, indem er Speicher vom Heap allo-
ziert, in dem der Name gespeichert wird. Der Kopierkonstruktor fügt das Objekt nicht in die
existierende Liste ein (obwohl er das könnte).
Sonntagmorgen.
1. Die Ausgabe lautet:
Advisor:Student Datenelement
Student
Advisor:Student lokal
Advisor:GraduateStudent Datenelement
GraduateStudent
Advisor: GraduateStudent lokal
Die Kontrolle geht an den Konstruktor von GraduateStudent und von dort an den Konstruk-
tor der Basisklasse Student über.
Ein lokales Objekt aus der Klasse Advisor wird vom Heap erzeugt.
Die Kontrolle kehrt zum Konstruktor GraduateStudent zurück, der das Datenelement
GraduateStudent::adv erzeugt.
Die Kontrolle betritt den Konstruktor GraduateStudent, der ein Advisor-Objekt vom Heap
alloziert.
Anhang 31.01.2001 11:47 Uhr Seite 376
376 Anhang A
2.
class Student
{
public:
virtual int pass(double dGrade)
{
// wenn es zum Bestehen reicht ...
if (dGrade > 1.5)
{
// ... bestanden
return 1;
}
// ... sonst durchgefallen
return 0;
}
};
class GraduateStudent : public Student
{
public:
virtual int pass(double dGrade)
{
if (dGrade > 2.5)
{
return 1;
}
return 0;
}
};
// Abhebung ausführen
fBalance -= fAmount;
// Gebühr erheben
fBalance -= 1;
}
};
.
CD-ROM
Das gesamte Programm ist auf der beiliegenden CD-ROM enthalten unter dem
Namen AbstractProblem.
378 Anhang A
public:
PrinterCopier(int nVoltage) : Copier(), Printer()
{
this->nVoltage = nVoltage;
}
};
// erst drucken
ss.print();
// dann kopieren
ss.copy();
// Spannung ausgeben
cout << »Spannung = » << ss.nVoltage << »\n«;
return 0;
}
Sonntagnachmittag.
1. Meine Version der beiden Funktionen sieht wie folgt aus:
MyClass(MyClass& mc)
{
nValue = mc.nValue;
resource.open(nValue);
}
MyClass& operator=(MyClass& s)
{
resource.close();
nValue = s.nValue;
resource.open(nValue);
}
Der Kopierkonstruktor öffnet das aktuelle Objekt mit dem Wert des Quellobjektes.
Der Zuweisungsoperator schließt zuerst die aktuelle Ressource, weil sie mit einem anderen
Wert geöffnet wurde. Sie öffnet dann die Ressource wieder mit dem neuen Wert, der überge-
ben wurde.
Anhang 31.01.2001 11:47 Uhr Seite 379
// Student
class Student
{
friend ostream& operator<<(ostream& out, Student& d);
public:
Student(char* pszFName, char* pszLName, int nSSNum)
{
strncpy(szFName, pszFName, 20);
strncpy(szLName, pszLName, 20);
this->nSSNum = nSSNum;
}
protected:
char szLName[20];
char szFName[20];
int nSSNum;
};
// Inserter – Zeichenkettenbeschreibung
ostream& operator<<(ostream& out, Student& s)
{
out << s.szLName
<< », »
<< s.szFName
<< »(»
<< s.nSSNum
<< »)«;
return out;
}
Anhang 31.01.2001 11:47 Uhr Seite 380
vakat
Anhang 31.01.2001 11:47 Uhr Seite 381
Ergänzende
Probleme B
Anhang
D
ieser Anhang enthält ergänzende Probleme für verschiedene Teile des Buches, die Ihnen
zusätzliche Erfahrung bei der Arbeit mit C++ bringen und ihre neuen Fähigkeiten festigen
sollen. Die Probleme finden Sie in Kapiteln, die den einzelnen Buchteilen zugeordnet sind,
jeweils gefolgt von einem Abschnitt mit Lösungen für die Probleme.
1.1 Probleme.
1.1.1 Samstagmorgen
1. Welche der folgenden Anweisungen sollte
b. Warnungen erzeugen?
c. Fehler erzeugen?
1. int n1, n2, n3;
2. float f1, f2 = 1;
3. double d1, d2;
4. n1 = 1; n2 = 2; n3 = 3; f2 = 1;
5. d1 = n1 * 2.3;
6. n2 = n1 * 2.3;
7. n2 = d1 * 1;
8. n3 = 100; n3 = n3 * 1000000000;
9. f1 = 200 * f2;
a. n1 / 3
b. n1 % 3
c. n1++
d. ++n1
e. n1 %= 3
f. n1 -= n1
g. n1 = -10; n1 = +n1; was ist n1?
Anhang 31.01.2001 11:47 Uhr Seite 382
382 Anhang B
5. Beschreiben Sie genau, was passiert, wenn die Funktion aus Problem 4 wie folgt verwendet
wird:
int n = cube(3.0);
6. Das folgende Programm zeigt eine sehr wenig durchdachte Funktion, die die Integerqua-
dratwurzel einer Zahl berechnet, durch Vergleiche mit dem Quadrat eines Zählers. Mit
anderen Worten, es ist 4 * 4 = 16, woraus folgt, dass 4 die Quadratwurzel von 16 ist.
Diese Funktion berechnet 3 als Quadratwurzel von 15, weil 3 * 3 kleiner ist als 15, aber
4 * 4 größer ist als 15.
Das Programm erzeugt jedoch unerwartete Ergebnisse. Beheben Sie den Fehler!
// Endlosschleife
for(;;)
{
// überprüfe Quadrat des aktuellen Werts
// (und inkrementiere zum nächsten)
if ((nRoot++ * nRoot) > n)
Anhang 31.01.2001 11:47 Uhr Seite 383
{
// so nah wie möglich, gib diesen
// Wert zurück
return nRoot;
}
}
1.1.2 Samstagnachmittag
1. Die C++-Bibliotheksfunktion strchr( ) gibt den Index eines Zeichens in einer Zeichenkette
zurück. So gibt z.B. strchr(»abcdef«, ‘c’) den Index 2 zurück. strchr( ) gibt -1 zurück,
wenn das Zeichen in der Zeichenkette nicht vorkommt.
Schreiben Sie eine Funktion myStrchr( ) die das Gleiche tut wie strchr( ). Wenn Sie den-
ken, dass Ihre Funktion fertig ist, testen Sie sie mit dem Folgenden:
384 Anhang B
return 0;
}
Hinweis: Achten Sie darauf, dass Sie nicht aus der Zeichenkette herauslaufen, wenn das
Zeichen nicht gefunden wird.
2. Obwohl das Folgende schlecht programmiert ist, verursacht es doch keine Probleme.
Erklären Sie warum nicht.
void fn(void)
{
double d;
int* pnVar = (int*)&d;
*pnVar = 10;
}
3. Schreiben Sie eine Zeigerversion der folgenden Funktion displayString( ). Nehmen Sie an,
dass das eine Null-terminierte Zeichenkette ist (übergeben Sie nicht die Länge als Argument
an die Funktion).
void displayCharArray(char sArray[], int nSize)
{
for(int i = 0; i < nSize; i++)
{
cout << sArray[i];
}
}
Anhang 31.01.2001 11:47 Uhr Seite 385
4. Kompilieren Sie das folgende Programm, und führen Sie es aus. Erklären Sie die Ergebnisse:
#include <stdio.h>
#include <iostream.h>
// MyClass – Testklasse ohne Bedeutung
class MyClass
{
public:
int n1;
int n2;
};
1.1.3 Sonntagmorgen
1. Schreiben Sie eine Klasse Car, die von der Klasse Vehicle erbt, und die einen Motor besitzt.
Die Anzahl der Reifen wird im Konstruktor der Klasse Vehicle angegeben, und die Anzahl
der Zylinder im Konstruktor von Motor. Beide Werte werden an den Konstruktor der Klasse
Car übergeben.
1.2 Antworten.
1.2.1 Samstagmorgen
1. 1. Kein Problem.
2. Keine Warnung; sie können initialisieren, wenn sie wollen.
3. Alles klar.
4. Kein Problem.
5. Kein Problem; n1 wird automatisch in ein double verwandelt, um die Multiplikation
ausführen zu können. Die meisten Compiler werden diese Konvertierung nicht kommen-
tieren.
6. n1 * 2.3 ist ein double. Die Zuweisung an eine int-Variable führt zu einer Warnung
wegen Demotion.
Anhang 31.01.2001 11:47 Uhr Seite 386
386 Anhang B
7. Ähnlich wie 6. Obwohl 1 ein int ist, ist d1 ein double. Das Ergebnis ist ein double, das
nach int konvertiert wird (Demotion).
8. Keine Warnung, aber es funktioniert nicht. Das Ergebnis liegt außerhalb des Bereiches
von int.
9. Das sollte eine Warnung erzeugen (Visual C++ tut es auch, GNU C++ tut es nicht). Das
Ergebnis einer Multiplikation von int und float ist double (alle Berechnungen werden
in double ausgeführt). Das Ergebnis muss nach float konvertiert werden (Demotion).
2. a. 3 – Rundungsfehler, die in Sitzung 5 erklärt wurden, konvertieren das erwartete Ergeb-
nis 3.3 nach 3.
b. 1 – Der Teiler, der 10 / 3 am nächsten ist, ist 3. 10 – (3 * 3) ist 1, der Rest der Division.
c. 10 – n1++ gibt den Wert von n1 zurück, bevor n1 inkrementiert wird. Nach der Auswer-
tung des Ausdrucks ist n1 = 11.
f. 0 – Das ist das Gleiche wie n1 = n1 – n1, aber n1 – n1 ist immer Null.
g. -10 – Der unäre Plusoperator (+) hat keinen Effekt. Insbesondere ändert er nicht das
Vorzeichen einer negativen Zahl.
3. Kein Unterschied. Die Inkrementklausel einer if-Anweisung wird als separater Ausdruck
behandelt. Der Wert von i ist nach einem Präinkrement und nach einem Postinkrement
gleich (nur der Wert des Ausdrucks ist verschieden).
4. int cube(int n)
{
return n * n * n;
}
5. Der double-Wert 3.0 wird demotiert in den int-Wert 3, und das Ergebnis wird als Integer-
wert 9 zurückgegeben.
6. Fehler #1: Das Programm kann nicht kompiliert werden, weil die Funktion squareRoot( )
so deklariert wurde, dass sie void als Rückgabetyp hat. Ändern Sie den Rückgabewert auf
int und erzeugen Sie das Programm erneut.
Fehler #2: Das Programm behauptet nun, dass die Quadratwurzel von 16 gleich 6 ist. Um
das Problem zu verstehen, splitte ich die zusammengesetzte if-Bedingung, damit ich den
Ergebniswert jeweils ausgeben kann:
Anhang 31.01.2001 11:47 Uhr Seite 387
// Endlosschleife
for(;;)
{
// überprüfe Quadrat des aktuellen Wertes
// (und inkrementiere zum nächsten)
int nTest = nRoot++ * nRoot;
cout << »Testwurzel ist »
<< nRoot
<< » Quadrat ist »
<< nTest
<< »\n«;
if (nTest > n)
{
// so nah wie möglich, gib diesen
// Wert zurück
return nRoot;
}
}
Die Ausgabe des Programms ist überhaupt nicht korrekt. Eine genaue Untersuchung zeigt
jedoch, dass die linke Seite um eins verschoben ist. Das Quadrat von 3 ist 9, aber der ange-
zeigte Wert von nRoot ist 4. Das Quadrat von 4 ist 16, aber der angezeigte Wert von nRoot
ist 5. Durch die Inkrementierung von nRoot im Ausdruck, ist nRoot um eins größer als das
nRoot, das in der Berechnung verwendet wird. Somit muss nRoot nach der if-Anweisung
inkrementiert werden.
// Endlosschleife
for(;;)
{
// überprüfe Quadrat des aktuellen Wertes
int nTest = nRoot * nRoot;
cout << »Testwurzel ist »
<< nRoot
<< » Quadrat ist »
<< nTest
<< »\n«;
if (nTest > n)
{
// so nah wie möglich, gib diesen
Anhang 31.01.2001 11:47 Uhr Seite 388
388 Anhang B
// Wert zurück
return nRoot;
}
// versuche nächsten Wert für nRoot
nRoot++;
}
Das Autoinkrement wurde hinter den Test platziert (das Autoinkrement war die ganze Zeit
verdächtig). Die Ausgabe des neuen, verbesserten Programms sieht wie folgt aus:
Das Quadrat wirk korrekt berechnet, aber aus irgendeinem Grund stoppt die Funktion
nicht, wenn nRoot gleich 4 ist. Es gilt aber doch 4 * 4 == 16. Das ist genau das Problem –
der Test überprüft nTest > n, wo er eigentlich nTest >= n überprüfen sollte. Das korrigierte
Programm erzeugt die erwartete Ausgabe:
Nachdem ich verschiedene Werte getestet habe, bin ich davon überzeugt, dass das
Programm korrekt ist, und ich entferne die Ausgabeanweisungen.
1.2.2 Samstagnachmittag
1. Die folgende Funktion myStrchr( ) ist meine Lösung des Problems:
Die Funktion myStrchr( ) beginnt damit, durch die Zeichenkette target zu iterieren,
wobei gestoppt wird, wenn das aktuelle Zeichen target[index] gleich 0 ist, was bedeutet,
dass die Funktion das Ende der Zeichenkette erreicht hat. Dieser Test stellt sicher, dass die
Funktion nicht zu weit geht, wenn das Zeichen nicht gefunden wird.
Innerhalb dieser Schleife vergleicht die Funktion das aktuelle Zeichen mit dem gesuchten
Zeichen testChar. Wenn das Zeichen gefunden wird, verläßt die Funktion die Schleife vor-
zeitig.
Wenn die Schleife verlassen worden ist, wurde entweder das Ende der Zeichenkette ange-
troffen oder das zu suchende Zeichen wurde gefunden. Wenn das Ende der Zeichenkette
der Grund ist, dann ist target[index] gleich 0, bzw. ‘\0’ um das Zeichenäquivalent zu
verwenden. In diesem Fall wird index auf -1 gesetzt.
390 Anhang B
Das obige Programm kann dadurch vereinfacht werden, dass die Funktion verlassen wird,
wenn das Zeichen gefunden wird:
Wenn das Zeichen gefunden wird, wird es sofort zurückgegeben. Wenn die Kontrolle die
Schleife anders verläßt, kann das nur bedeuten, dass das Ende der Zeichenkette gefunden
wurde, ohne das Zeichen zu finden.
Ich persönlich bevorzuge diesen Stil. Es gibt aber Organisationen, bei denen mehrere Rük-
kgaben pro Funktion verboten sind.
2. Ein double belegt 8 Bytes, ein int belegt 4 Bytes. Es ist möglich, dass C++ 4 von den
8 Bytes verwendet, um den int-Wert 10 zu speichern, ohne die anderen 4 Bytes zu ver-
wenden. Das verursacht keinen Fehler; sie sollten jedoch nicht davon ausgehen, dass sich
Ihr Compiler so verhält.
4. Die Ausgabe von pmc->n1 und pmc->n2 sind vollkommen falsch, weil der Zeiger pmc nicht
initialisiert wurde, auf etwas zu zeigen. In der Tat kann das Programm abstürzen, ohne
überhaupt eine Ausgabe zu erzeugen, wegen dieses nicht initialisierten Zeigers.
1.2.3 Sonntagmorgen
1. class Vehicle
{
public:
Vehicle(int nWheels)
{
}
};
class Motor
{
public:
Motor(int nCylinders)
{
}
};
Motor motor;
};
Anhang 31.01.2001 11:47 Uhr Seite 392
vakat
Anhang 31.01.2001 11:47 Uhr Seite 393
D
ie CD-ROM, die diesem Buch beiliegt, enthält Material, das Ihnen bei dem Durcharbeiten der
Sitzungen und beim Erlernen von C++ innerhalb eines Wochenendes hilft:
=
= =
=
Es wurde angenommen, dass das Verzeichnis DJGPP direkt unter C:\ steht.
Wenn Sie Ihr Verzeichnis DJGPP an einer anderen Stelle platziert haben, müssen
Hinweis
Sie den Pfad in den obigen Kommandos entsprechend anpassen.
=
= =
=
Bevor Sie GNU C++ verwenden, stellen Sie sicher, dass in DJGPP.ENV lange
Dateinamen angeschaltet sind. Diese Option ausgeschaltet zu haben ist der
Hinweis
häufigste Fehler bei der Installation von GNU C++.
Anhang 31.01.2001 11:47 Uhr Seite 394
394 Anhang C
Öffnen Sie die Datei DJGPP.ENV mit einem Texteditor, z.B. mit Microsoft WordPad. Erschrecken
Sie nicht, wenn Sie nur eine lange Zeichenkette sehen, die von kleinen schwarzen Kästchen unter-
brochen ist. Unix verwendet ein anderes Zeichen für Zeilenumbrüche als Windows. Suchen Sie nach
der Phrase »LFN=y« oder »LFN=Y« (Groß- und Kleinschreibung spielt also keine Rolle). Wenn Sie
stattdessen »LFN=n« finden (oder »LFN« überhaupt nicht vorkommt), ändern Sie das »n« in »y«.
Speichern Sie die Datei. (Stellen Sie sicher, dass Sie die Datei als Textdatei speichern und nicht in
einem anderen Format, z.B. als Word .DOC-Datei.)
1.2 Beispielprogramme.
Das Verzeichnis \Programs enthält den Sourcecode aller Programme in diesem Buch. Ich empfehle
Ihnen, den gesamten Ordner und alle Unterverzeichnisse auf Ihre Festplatte zu kopieren; sie können
aber auch einzelne Dateien kopieren, wenn Sie das möchten.
Die Programme sind nach Sitzungen aufgeteilt. Der Ordner einer Sitzung enthält die .CPP-
Dateien und die ausführbaren Programme; letztere sollen es dem Leser bequemer machen. Sie kön-
nen die Programme direkt von der CD-ROM aus ausführen. Die ausführbaren .EXE-Dateien wurden
mit GNU C++ erzeugt. Die mit Visual C++ erzeugten .EXE-Dateien befinden sich in einem Unterver-
zeichnis DEBUG.
Sitzung 2 enthält Anweisungen, wie Programme mit Visual C++ erzeugt werden. Sitzung 3 erklärt
das Gleiche für GNU C++.
Hinweis: Zwei Quelldateien wurden abweichend von diesem Buch modifiziert:
1. Die Funktion ltoa( ) wurde durch einen Aufruf der funktional äquivalenten Funktion itoa( ) in
ToStringWOStream.cpp in Sitzung 29 ersetzt.
2. GNU C++ verwendet den vollständigen Namen von strstream.h, während Visual C++ das 8.3-
Format strstrea.h verwendet. Die Referenz auf diese Include-Datei muss in ToStringWStream.cpp
entsprechend angepasst werden. Der Sourcecode verwendet ein #ifdef, um zu entscheiden, wel-
che Include-Datei eingebunden werden soll. Der Sourcecode in diesem Buch weist mit einem
Kommentar auf dieses Problem hin.
Index Sicher 31.01.2001 11:48 Uhr Seite 395
Index
396 Index
Index 397
398 Index
B C
C++
\ Backslash ( ) 49
Code eingeben 20 - 23
Basisklassen Compiler 11, 298, 299
abstrakte Klassen 268 Präprozessor
Destruktor 248
C++ Programmiersprache
konstruieren 248
Conversion.cpp Programm-Code 30
Bedingungen überprüfen 71 EXE-Programme 10
Begrenzungen Fehlermeldungen 14, 25, 26
Gleitkomma-Variablen 46 GNU C++ 8
int Variablentyp 42 - 45 Programm, Ausgabe 16
Beispiel-Code, Funktionen 81, 83 Programme, ausführen 14
Programme, erzeugen 10, 14
Benutzer-Interfaces rhide 20 - 28
Unterscheidung Groß- und Kleinschreibung 9
Bereich, begrenzt für int Variable 44 Visual C++ 8
Bereichsauflösungsoperator 196 Zeilen einrücken 9
Bibliotheken, MSDN Bibliothek 16 CashAccount
BIN Verzeichnis 19 Klasse, abstrakte Unterklasse 271,272
Programm-Code 271,272
binäre Operatoren 55 ,56, 318
Cast 87
binäre Zahlen 62, 63
Cast-Operator 321 - 323
Binden zur Compile-Zeit 253
catch-Phrasen 354 - 357
Bindung
Bits, definiert 62 CD-ROM
Compile-Zeit 253 C++-Wochenend-Crashkurs 8
EarlyBinding, Programm-Code 253 Concatenate(Error).cpp Datei 171
frühe 253 ConstructMembers Programm 213
späte 255 Conversion.cpp Datei 10, 21
factorial( ) Funktion 348
bitweise logische Operatoren 58, 63, 64 - 67
FactorialThrow.cpp 355;365
Code zum Testen 65, 66
StudentID Programm 227
Einzelbit-Operatoren 63
ToStringWStreams Programm 340;341
Masken 67
Sinn 64, 66, 67 cerr Objekt 333
char Variable 47
Index Sicher 31.01.2001 11:48 Uhr Seite 399
Index 399
400 Index
Datei MYNAME öffnen, Code zum Öffnen und concatString( )-Funktion 178, 179
Schreiben 334, 335 debug-Funktion 304, 305
Dateien debug-Kommandos 170
Concatenate (Error).cpp 171 debug-Modus 97
Conversion.cpp CD-ROM 10, 21 Debugger-Programm 92, 93
Conversion.exe, ausführen 14, 15 Debugger Visual C++ 179, 180
Conversion.pdw Arbeitsbereich 11 Debugger, Aktion anhalten 173
cpp mit C++-Präprozessor bearbeiten 298, 299 Einzelschritte durch Programme 172, 173
cpp Quellcode 12 ErrorProgam korrigieren 99, 100
DJGPP.ENV 20 ErrorProgram 93, 94, 95
einbinden 199, 200, 291 Fehlertypen 92, 93
einbinden, Funktionen 298, 299 Funktion 304, 305
EXE erzeugen 11 GNU C++ 96, 97
fstream.h 334 Haltepunkt 175
.h #include Direktive 298, 299 Kommandos 170
.h 295 lokale Variablen ansehen 179, 180
Include-Dateien #include Direktive 200 Modus 97
ios Konstanten zum Öffnen 334 nNums auswerten 98
iostream.h Protoypen 333 Probleme, reproduzieren 95
leer, Code zu Erzeugen 21, 22 Program Reset 174
leere Textdatei, Code zum Erzeugen 10, 11 Programme in Einzelschritten 172, 173
myClass.h 303, 204 Programme mit Kommando Step Over 172, 173
MYNAME, Code zum Öffnen und Schreiben Release-Modus 97
334, 335 rhide-Debugger 172
Projekt erzeugen in GNU C++ 295 Segmentverletzung 175
Projekt erzeugen in Visual C++ 294 Step In Kommando 174
Quellmodul 89, 90 Testprogramm 171, 172
README.1ST 19 Variablen modifizieren 175 - 179
SeparatedFn.cpp 292 Variablen, Werte 99
SeparatedKlasse.cpp 288, 289, 291, 292 verkettete Liste, Anwendung 176
strstrea.h, Definition der Unterklassen strstream Visual C++ 95
337 Visual C++, Debugger 179, 180
student.cpp Datei 199 Visual C++, nNums 95, 96
Tex, erzeugen 9 - 11 Zeichenketten mit null terminieren 177
Zeigerfehler, Ausgabeanweisungen 170
Daten gruppieren 119 - 122
Zielzeichenkette 177
Datenelemente Zugang der Ausgabeanweisungen 92, 93
Klassen, konstruieren 210 - 212
Debugger
Klassen, static 217, 218
Aktion anhalten 173
Objekte, konstruieren 223 - 228
Konstruktoren, Ausgabeanweisungen einfügen
static, Syntax zum Deklarieren 217, 218
209
Syntax zum Deklarieren 228
Programm 92, 93
DEBUG Parameter 305 Visual C++ 179, 180
Debug Windows Kommando (View Menü) 179, dec Manipulatoren 342
180
Default
debuggen 169 Definition, operator=( ) 325, 326
Ausgabeanweisungen 92 - 95 Konstruktoren 222, 223, 209
Code, #ifdef Direktive 304, 205 Werte, Argumente 223
Index Sicher 31.01.2001 11:48 Uhr Seite 401
Index 401
402 Index
Index 403
404 Index
Index 405
H inline
Inkrement-Operatoren 56 ,57
406 Index
Index 407
408 Index
L Makros
#define Direktive 299, 300 - 302
last-in-first-out (LIFO) 168 Code zum Definieren 300 - 302
LateBinding-Programm-Code 257 - 259 Fehler 300 - 302
Laufzeitfehler 92, 93 Manipulation von Zeichenketten: Arrays gegen
Zeiger 145, 146
Layout Programm-Code 128, 129
Manipulatoren
LayoutError Programm-Code 132, 133
dec 342
leere Datei, Code zur Erzeugung 21, 22 definieren 341
leere Textdatei, Code zur Erzeugung 10, 11 display( )-Funktion 341
Leerraum (Anweisungs-) 32 hex 342
oct 342
Lesbarkeit, verbessern mit Überladen von Operato-
setfill(c) 342
ren 311, 312
setprecision(c) 342
LFN=y Phrase 21 setw(n) 342
LIFO (last-in-first-out) 168 stream I/O 341 - 343
linkbare Klassen deklarieren 160 Manipulieren von Zeichenketten 114 - 117
LinkedListData Programm-Code 165 - 167 Maschine
Links-Shift (<<) Operator 311, 312 Anzahl Instruktionen ind Anzahl Anweisungen
147
Links-Shift Operator 346, 347
Sprachen 23
logische Operatoren
Masken 67
! (einfacher) 60
!= (einfacher) 60 mathematische Operatoren 53
&& (einfacher) 60 % 53
& (einfacher) 60 %= 53
< (einfacher) 60 - (unär) 53
<= (einfacher) 60 — (unär) 53
== (einfacher) 60 -, 53
> (einfacher) 60 * 53
>= (einfacher) 60 *= 53
bitweise 58, 63 - 67 /, 53
bitweise, binäre Zahlen 63 + (unär) 53
++ (unär) 53
einfacher 58, 60 - 62
+= 53
kurze Schaltkreise 62 = 53
logische Variablen 62 -= 53
lokale Objekte konstruieren 229 arithmetische 53, 54
Ausdrücke 54
lokale Variablen ansehen 179, 180
binärer 55 ,56
long-Variable 47, 128 Dekrement 56 ,57
Inkrement 56 ,57
unär 55 - 58
Zuweisung 57, 58
M mathematischer Operator 53
Matrix, Arrays 111, 112
main( )-Funktion 88 - 90, 115, 152, 154
aufrufen 200- 203
Make Kommando (Compile Menü) 295
Index Sicher 31.01.2001 11:48 Uhr Seite 409
Index 409
410 Index
Index 411
412 Index
Index 413
414 Index
public Schlüsselwort
Vererbung 245 S
Zugriffskontrolle für Objekte 214 Savings-Klasse 263, 264
abstrakte Unterklasse 271, 272
public Schlüsselwort 123, 192, 193
konkrete Klasse, erzeugen aus abstrakter Klasse
269, 270
schlaue Inserter 344 - 347
Schleifen
Q Bedingungen überprüfen 71
Quellcode-Dateien, do while 72
endlos 73, 74
Quellcode-Dateien, cpp 12
for 74 - 76
Quelldateien, Module 89, 90 for modifizieren 98
geschachtelte 78, 79
Kontrollen 76, 77
nLoopCount-Variable 72
Variablen 77
R while 71 - 74, 115
Rahmen für C++-Programme 30, 32 Schleifen-Kommandos oder Anweisungen 71 - 77
rationalize( ) Funktion 316, 317 Schleifen-Kommandos, geschachtelte Schleifen 78,
README.1ST Datei 19 79
Rechts-Shift (>) Operator 311, 312 Schlüsselwörter
Referenzen, Zeiger 158, 159 Klasse 123
private, Zugriffkontrolle auf Objekte 216
Reihenfolge der Konstruktion von Objekten 228 -
protected, Zugriffkontrolle auf Objekte 214,
231
215, 217
rein virtuelle Funktionen 274 public 123, 192, 193
Features in konkreten Unterklassen implementie- public, Vererbung 245
ren 273 public, Zugriffkontrolle auf Objekte 214
Syntax 267, 268 struct 123, 192, 193
überladen 272 try 351
Zweck bitweiser Operatoren 66, 67 virtual 257, 258, 284
Release-Modus 97 void 84
remove( )-Funktion 162, 163 schreiben
return-Anweisung, void-Funktion 85 Elementfunktionen 198 - 200
MYNAME Datei, Code zum Öffnen und Schrei-
rhide
ben 334, 335
Benutzer-Interface, GNU C++ Hilfe 27, 28
Debugger 172 Segmentverletzung 175
Fenster 22 Seiteneffekt 229
Interface 21 selbstenthaltende Klassen 190, 191
Oxff Exit-Code 96
Semikolon (;) 12, 24, 32
Programmargumente 154
SeparatedClass Programm-Code 288 - 290
Rückgabetyp, Funktionen 85
SeparatedClass.cpp-Datei 288, 289, 291 - 293
Runden von Integer 42 - 45
SeparatedFn.cpp-Datei 292
Rundungsfehler 46
Set Breakpoint-Kommando 169
Index Sicher 31.01.2001 11:48 Uhr Seite 415
Index 415
416 Index
struct-Schlüsselwort 123, 192 - 194 tiefe Kopie gegen flache Kopie 233, 234
Struktur, Klassen 192, 193 Tilde (~) 132
Student-Klasse Ton, Visual C++ 12
Elementfunktionen, Code zur Deklaration außer- ToStringWStreams-Programm CD-ROM 340, 341
halb der Klasse 296
try-Schlüsselwort 351
Konstruktoren 222, 223
try-Block, mit catch-Phrasen verbinden 357
Student-Konstruktor, Klasse StudentID-Element-
Objekt 223 - 225 Typen von
Fehlern 92, 93
Student Programm-Code 246 - 248, 290
Variablen 46 - 49
student.cpp-Datei 199 Variablen, int Begrenzungen 42 - 45
StudentID-Programm CD-ROM 227 Zeiger 132, 133
Subtraktions-Operatoren 53
sumArray( )-Funktion 110
sumSequence( )-Funktion, aufrufen oder definieren
83 U
Superklassen 189 überladen
switch Elementfunktionen 203 - 205
Anweisung 79 Elementfunktionen in Unterklassen 252
Kommando, break-Anweisungen 79 Funktionen 222, 223
switch( ), Kontrollkommando 151 Konstruktoren 221, 222
rein virtuelle Funktionen 272
Syntax
Datenelement deklarieren 228 überladen des Zuweisungsoperator 325, 326
Objekteigenschaften, zugreifen 123 Code 327 - 329
rein virtuelle Funktionen 267, 268 protected-Funktionen 330, 331
statische Datenelemente, deklarieren 217, 218 Überladen von Operatoren 311, 313, 315
binäre Operatoren, Verändern von Werten 318
Cast-Operator 321 - 323
Elementfunktionen, Operatoren 321
Funktionen, nicht Element 320
T Funktionen Operatoren, ihre Beziehung 312
Tastaturkürzel Funktionen, rationalize( ) 316
Alt+4 179, 180 Heap-Speicher 317
Alt+F4 26 Klassen, Freunde 315
Ctrl+F5 14 Lesbarkeit, verbessern 311, 312
Ctrl+F9 26 Links-Shift (<<)-Operator 311, 312
Nichtelement-Funktions 320, 321
Tasten 17, 27
Objekte, konverieren 323
temporäre Objekte 317 Objekte, temporär 317
terminieren operator+( ) 315 - 318
null, Zeichenketten 177 operator++( ) Funktion 315
Zeichenketten 115 Postfix-Operator x++ 316
testen Präfix-Operator ++x 316
bitweise Operatoren, Code 65, 66 rationalize( )-Funktion 316
Programm debuggen 171, 172 Rechts-Shift (>)-Operator 311, 312
temporäre Objekte 317
Textdateien, erzeugen 9 - 11
Index Sicher 31.01.2001 11:48 Uhr Seite 417
Index 417
418 Index
Index 419
420 Index
Die folgende deutsche Übersetzung wurde im Auftrag der SuSE GmbH [suse@suse.de] von Katja Lach-
mann Übersetzungen [na194@fim.uni-erlangen.de] erstellt und von Peter Gerwinski
[peter.gerwinski@uni-essen.de] (31. Oktober 1996) modifiziert. Diese Übersetzung wird mit der Absicht
angeboten, das Verständnis der GNU General Public License (GNU-GPL) zu erleichtern. Es handelt sich
jedoch nicht um eine offizielle oder im rechtlichen Sinne anerkannte Übersetzung.
Die Free Software Foundation (FSF) ist nicht der Herausgeber dieser Übersetzung, und sie hat die-
se Übersetzung auch nicht als rechtskräftigen Ersatz für die Original-GNU GPL anerkannt. Da die
Übersetzung nicht sorgfältig von Anwälten überprüft wurde, können die Übersetzer nicht garantie-
ren, dass die Übersetzung die rechtlichen Aussagen der GNU GPL exakt wiedergibt. Wenn Sie sicher-
gehen wollen, dass von Ihnen geplante Aktivitäten im Sinne der GNU GPL gestattet sind, halten Sie
sich bitte an die englischsprachige Originalversion. Die Free Software Foundation möchte Sie darum
bitten, diese Übersetzung nicht als offizielle Lizenzbedingungen für von Ihnen geschriebene Pro-
gramme zu verwenden. Bitte benutzen Sie hierfür stattdessen die von der Free Software Foundation
herausgegebene englischsprachige Originalversion.
Vorwort
Die meisten Softwarelizenzen sind daraufhin entworfen worden, Ihnen die Freiheit zu nehmen, Software
weiterzugeben und zu verändern. Im Gegensatz dazu soll Ihnen die GNU General Public License, die all-
gemeine öffentliche GNU-Lizenz, eben diese Freiheit garantieren. Sie soll sicherstellen, dass die Software
für alle Benutzer frei ist. Diese Lizenz gilt für den Großteil der von der Free Software Foundation heraus-
gegebenen Software und für alle anderen Programme, deren Autoren ihr Werk dieser Lizenz unterstellt
haben. Auch Sie können diese Möglichkeit der Lizenzierung für Ihre Programme anwenden. (Ein anderer
Teil der Software der Free Software Foundation unterliegt statt dessen der GNU Library General Public
License, der allgemeinen öffentlichen GNU-Lizenz für Bibliotheken.)
Index Sicher 31.01.2001 11:48 Uhr Seite 422
Die Bezeichnung »freie« Software bezieht sich auf Freiheit, nicht auf den Preis. Unsere Lizenzen
sollen Ihnen die Freiheit garantieren, Kopien freier Software zu verbreiten (und etwas für diesen Ser-
vice zu berechnen, wenn Sie möchten), die Möglichkeit, die Software im Quelltext zu erhalten oder
den Quelltext auf Wunsch zu bekommen. Die Lizenzen sollen garantieren, dass Sie die Software
ändern oder Teile davon in neuen freien Programmen verwenden dürfen – und dass Sie wissen, dass
Sie dies alles tun dürfen.
Um Ihre Rechte zu schützen, müssen wir Einschränkungen machen, die es jedem verbietet, Ihnen
diese Rechte zu verweigern oder Sie aufzufordern, auf diese Rechte zu verzichten. Aus diesen Ein-
schränkungen folgen bestimmte Verantwortlichkeiten für Sie, wenn Sie Kopien der Software ver-
breiten oder sie verändern.
Beispielsweise müssen Sie den Empfängern alle Rechte gewähren, die Sie selbst haben, wenn Sie
– kostenlos oder gegen Bezahlung – Kopien eines solchen Programms verbreiten. Sie müssen sicher-
stellen, dass auch sie den Quelltext erhalten bzw. erhalten können. Und Sie müssen ihnen diese
Bedingungen zeigen, damit sie ihre Rechte kennen.
Wir schützen Ihre Rechte in zwei Schritten: (1) Wir stellen die Software unter ein Urheberrecht
(Copyright), und (2) wir bieten Ihnen diese Lizenz an, die Ihnen das Recht gibt, die Software zu ver-
vielfältigen, zu verbreiten und/oder zu verändern.
Um die Autoren und uns zu schützen, wollen wir darüber hinaus sicherstellen, dass jeder erfährt,
dass für diese freie Software keinerlei Garantie besteht. Wenn die Software von jemand anderem
modifiziert und weitergegeben wird, möchten wir, dass die Empfänger wissen, dass sie nicht das
Original erhalten haben, damit von anderen verursachte Probleme nicht den Ruf des ursprünglichen
Autors schädigen.
Schließlich und endlich ist jedes freie Programm permanent durch Software-Patente bedroht. Wir
möchten die Gefahr ausschließen, dass Distributoren eines freien Programms individuell Patente
lizenzieren – mit dem Ergebnis, dass das Programm proprietär würde. Um dies zu verhindern, haben
wir klargestellt, dass jedes Patent entweder für freie Benutzung durch jedermann lizenziert werden
muss oder überhaupt nicht lizenziert werden darf.
Es folgen die genauen Bedingungen für die Vervielfältigung, Verbreitung und Bearbeitung:
Andere Handlungen als Vervielfältigung, Verbreitung und Bearbeitung werden von dieser Lizenz
nicht berührt; sie fallen nicht in ihren Anwendungsbereich. Der Vorgang der Ausführung des Pro-
gramms wird nicht eingeschränkt, und die Ausgaben des Programms unterliegen dieser Lizenz nur,
wenn der Inhalt ein auf dem Programm basierendes Werk darstellt (unabhängig davon, dass die
Ausgabe durch die Ausführung des Programmes erfolgte). Ob dies zutrifft, hängt von den Funktio-
nen des Programms ab.
Paragraph 1
Sie dürfen auf beliebigen Medien unveränderte Kopien des Quelltextes des Programms, wie Sie ihn
erhalten haben, anfertigen und verbreiten. Voraussetzung hierfür ist, dass Sie mit jeder Kopie einen ent-
sprechenden Copyright-Vermerk sowie einen Haftungsausschluss veröffentlichen, alle Vermerke, die sich
auf diese Lizenz und das Fehlen einer Garantie beziehen, unverändert lassen und des Weiteren allen
anderen Empfängern des Programms zusammen mit dem Programm eine Kopie dieser Lizenz zukom-
men lassen.
Sie dürfen für den eigentlichen Kopiervorgang eine Gebühr verlangen. Wenn Sie es wünschen,
dürfen Sie auch gegen Entgeld eine Garantie für das Programm anbieten.
Paragraph 2
Sie dürfen Ihre Kopie(n) des Programms oder eines Teils davon verändern, wodurch ein auf dem Pro-
gramm basierendes Werk entsteht; Sie dürfen derartige Bearbeitungen unter den Bestimmungen
von Paragraph 1 vervielfältigen und verbreiten, vorausgesetzt, dass zusätzlich alle folgenden Bedin-
gungen erfüllt werden:
(a) Sie müssen die veränderten Dateien mit einem auffälligen Vermerk versehen, der auf die von
Ihnen vorgenommene Modifizierung und das Datum jeder Änderung hinweist.
(b) Sie müssen dafür sorgen, dass jede von Ihnen verbreitete oder veröffentlichte Arbeit, die ganz
oder teilweise von dem Programm oder Teilen davon abgeleitet ist, Dritten gegenüber als Gan-
zes unter den Bedingungen dieser Lizenz ohne Lizenzgebühren zur Verfügung gestellt wird.
(c) Wenn das veränderte Programm normalerweise bei der Ausführung interaktiv Kommandos ein-
liest, müssen Sie dafür sorgen, dass es, wenn es auf dem üblichsten Wege für solche interaktive
Nutzung gestartet wird, eine Meldung ausgibt oder ausdruckt, die einen geeigneten Copyright-
Vermerk enthält sowie einen Hinweis, dass es keine Gewährleistung gibt (oder anderenfalls, dass
Sie Garantie leisten), und dass die Benutzer das Programm unter diesen Bedingungen weiter ver-
breiten dürfen. Auch muss der Benutzer darauf hingewiesen werden, wie eine Kopie dieser
Lizenz ansehen kann. (Ausnahme: Wenn das Programm selbst interaktiv arbeitet, aber normaler-
weise keine derartige Meldung ausgibt, muss Ihr auf dem Programm basierendes Werk auch kei-
ne solche Meldung ausgeben).
Diese Anforderungen betreffen das veränderte Werk als Ganzes. Wenn identifizierbare Abschnit-
te des Werkes nicht von dem Programm abgeleitet sind und vernünftigerweise selbst als unabhängi-
ge und eigenständige Werke betrachtet werden können, dann erstrecken sich diese Lizenz und ihre
Bedingungen nicht auf diese Abschnitte, wenn sie als eigenständige Werke verbreitet werden. Wenn
Sie jedoch dieselben Abschnitte als Teil eines Ganzen verbreiten, das ein auf dem Programm basie-
rendes Werk darstellt, dann muss die Verbreitung des Ganzen nach den Bedingungen dieser Lizenz
erfolgen, deren Bedingungen für weitere Lizenznehmer somit auf die Gesamtheit ausgedehnt wer-
den – und damit auf jeden einzelnen Teil, unabhängig vom jeweiligen Autor.
Index Sicher 31.01.2001 11:48 Uhr Seite 424
Somit ist es nicht die Absicht dieses Abschnittes, Rechte für Werke in Anspruch zu nehmen oder
zu beschneiden, die komplett von Ihnen geschrieben wurden; vielmehr ist es die Absicht, die Rech-
te zur Kontrolle der Verbreitung von Werken, die auf dem Programm basieren oder unter seiner aus-
zugsweisen Verwendung zusammengestellt worden sind, auszuüben.
Ferner bringt ein einfaches Zusammenstellen eines anderen Werkes, das nicht auf dem Programm
basiert, zusammen mit dem Programm oder einem auf dem Programm basierenden Werk auf ein- und
demselben Speicher- oder Vertriebsmedium nicht in den Anwendungsbereich dieser Lizenz.
Paragraph 3
Sie dürfen das Programm (oder ein darauf basierendes Werk gemäß Paragraph 2) als Objektcode
oder in ausführbarer Form unter den Bedingungen von Paragraph 1 und 2 vervielfältigen und ver-
breiten – vorausgesetzt, dass Sie außerdem eine der folgenden Leistungen erbringen:
(a) Liefern Sie das Programm zusammen mit dem vollständigen zugehörigen maschinenlesbaren
Quelltext auf einem für den Datenaustausch üblichen Medium aus, wobei die Verteilung unter
den Bedingungen der Paragraphen 1 und 2 erfolgen muss. Oder:
(b) Liefern Sie das Programm zusammen mit einem mindestens drei Jahre lang gültigen schrift-
lichen Angebot aus, jedem Dritten eine vollständige maschinenlesbare Kopie des Quelltextes zur
Verfügung zu stellen – zu nicht höheren Kosten als denen, die durch den physischen Kopiervor-
gang anfallen -, wobei der Quelltext unter den Bedingungen der Paragraphen 1 und 2 auf
einem für den Datenaustausch üblichen Medium weitergegeben wird. Oder:
(c) Liefern Sie das Programm zusammen mit dem schriftlichen Angebot der Zurverfügungstellung
des Quelltextes aus, das Sie selbst erhalten haben. (Diese Alternative ist nur für nicht-kommer-
zielle Verbreitung zulässig und nur, wenn Sie das Programm als Objektcode oder in ausführbarer
Form mit einem entsprechenden Angebot gemäß Absatz b erhalten haben.)
Unter dem Quelltext eines Werkes wird diejenige Form des Werkes verstanden, die für Bearbei-
tungen vorzugsweise verwendet wird. Für ein ausführbares Programm bedeutet »der komplette
Quelltext«: Der Quelltext aller im Programm enthaltenen Module einschließlich aller zugehörigen
Modulschnittstellen-Definitionsdateien sowie der zur Kompilation und Installation verwendeten
Skripte. Als besondere Ausnahme jedoch braucht der verteilte Quelltext nichts von dem zu enthal-
ten, was üblicherweise (entweder als Quelltext oder in binärer Form) zusammen mit den Haupt-
komponenten des Betriebssystems (Kernel, Compiler usw.) geliefert wird, unter dem das Programm
läuft – es sei denn, diese Komponente selbst gehört zum ausführbaren Programm.
Wenn die Verbreitung eines ausführbaren Programms oder von Objektcode dadurch erfolgt, dass
der Kopierzugriff auf eine dafür vorgesehene Stelle gewährt wird, so gilt die Gewährung eines
gleichwertigen Zugriffs auf den Quelltext als Verbreitung des Quelltextes, auch wenn Dritte nicht
dazu gezwungen sind, den Quelltext zusammen mit dem Objektcode zu kopieren.
Paragraph 4
Sie dürfen das Programm nicht vervielfältigen, verändern, weiter lizenzieren oder verbreiten, sofern
es nicht durch diese Lizenz ausdrücklich gestattet ist. Jeder anderweitige Versuch der Vervielfälti-
gung, Modifizierung, Weiterlizenzierung und Verbreitung ist nichtig und beendet automatisch Ihre
Rechte unter dieser Lizenz. Jedoch werden die Lizenzen Dritter, die von Ihnen Kopien oder Rechte
unter dieser Lizenz erhalten haben, nicht beendet, solange diese die Lizenz voll anerkennen und
befolgen.
Index Sicher 31.01.2001 11:48 Uhr Seite 425
Paragraph 5
Sie sind nicht verpflichtet, diese Lizenz anzunehmen, da Sie sie nicht unterzeichnet haben. Jedoch
gibt Ihnen nichts anderes die Erlaubnis, das Programm oder von ihm abgeleitete Werke zu verän-
dern oder zu verbreiten. Diese Handlungen sind gesetzlich verboten, wenn Sie diese Lizenz nicht
anerkennen. Indem Sie das Programm (oder ein darauf basierendes Werk) verändern oder verbrei-
ten, erklären Sie Ihr Einverständnis mit dieser Lizenz und mit allen ihren Bedingungen bezüglich der
Vervielfältigung, Verbreitung und Veränderung des Programms oder eines darauf basierenden
Werks.
Paragraph 6
Jedes Mal, wenn Sie das Programm (oder ein auf dem Programm basierendes Werk) weitergeben,
erhält der Empfänger automatisch vom ursprünglichen Lizenzgeber die Lizenz, das Programm ent-
sprechend den hier festgelegten Bestimmungen zu vervielfältigen, zu verbreiten und zu verändern.
Sie dürfen keine weiteren Einschränkungen der Durchsetzung der hierin zugestandenen Rechte des
Empfängers vornehmen. Sie sind nicht dafür verantwortlich, die Einhaltung dieser Lizenz durch Drit-
te durchzusetzen.
Paragraph 7
Sollten Ihnen infolge eines Gerichtsurteils, des Vorwurfs einer Patentverletzung oder aus einem
anderen Grunde (nicht auf Patentfragen begrenzt) Bedingungen (durch Gerichtsbeschluss, Ver-
gleich oder anderweitig) auferlegt werden, die den Bedingungen dieser Lizenz widersprechen, so
befreien Sie diese Umstände nicht von den Bestimmungen dieser Lizenz. Wenn es Ihnen nicht mög-
lich ist, das Programm unter gleichzeitiger Beachtung der Bedingungen in dieser Lizenz und Ihrer
anderweitigen Verpflichtungen zu verbreiten, dann dürfen Sie als Folge das Programm überhaupt
nicht verbreiten. Wenn zum Beispiel ein Patent nicht die gebührenfreie Weiterverbreitung des Pro-
gramms durch diejenigen erlaubt, die das Programm direkt oder indirekt von Ihnen erhalten haben,
dann besteht der einzige Weg, sowohl das Patentrecht als auch diese Lizenz zu befolgen, darin, ganz
auf die Verbreitung des Programms zu verzichten.
Sollte sich ein Teil dieses Paragraphen als ungültig oder unter bestimmten Umständen nicht
durchsetzbar erweisen, so soll dieser Paragraph seinem Sinne nach angewandt werden; im übrigen
soll dieser Paragraph als Ganzes gelten.
Zweck dieses Paragraphen ist nicht, Sie dazu zu bringen, irgendwelche Patente oder andere
Eigentumsansprüche zu verletzen oder die Gültigkeit solcher Ansprüche zu bestreiten; dieser Para-
graph hat einzig den Zweck, die Integrität des Verbreitungssystems der freien Software zu schützen,
das durch die Praxis öffentlicher Lizenzen verwirklicht wird. Viele Leute haben großzügige Beiträge
zu dem großen Angebot der mit diesem System verbreiteten Software im Vertrauen auf die konsis-
tente Anwendung dieses Systems geleistet; es liegt am Autor/Geber zu entscheiden, ob er die Soft-
ware mittels irgendeines anderen Systems verbreiten will; ein Lizenznehmer hat auf diese Entschei-
dung keinen Einfluss.
Dieser Paragraph ist dazu gedacht, deutlich klarzustellen, was als Konsequenz aus dem Rest die-
ser Lizenz betrachtet wird.
Index Sicher 31.01.2001 11:48 Uhr Seite 426
Paragraph 8
Wenn die Verbreitung und/oder die Benutzung des Programms in bestimmten Staaten entweder
durch Patente oder durch urheberrechtlich geschützte Schnittstellen eingeschränkt ist, kann der
Urheberrechtsinhaber, der das Programm unter diese Lizenz gestellt hat, eine explizite geografische
Begrenzung der Verbreitung angeben, in der diese Staaten ausgeschlossen werden, sodass die Ver-
breitung nur innerhalb und zwischen den Staaten erlaubt ist, die nicht ausgeschlossen sind. In
einem solchen Fall beinhaltet diese Lizenz die Beschränkung, als wäre sie in diesem Text niederge-
schrieben.
Paragraph 9
Die Free Software Foundation kann von Zeit zu Zeit überarbeitete und/oder neue Versionen der
General Public License veröffentlichen. Solche neuen Versionen werden vom Grundprinzip her der
gegenwärtigen entsprechen, können aber im Detail abweichen, um neuen Problemen und Anfor-
derungen gerecht zu werden.
Jede Version dieser Lizenz hat eine eindeutige Versionsnummer. Wenn in einem Programm ange-
geben wird, dass es dieser Lizenz in einer bestimmten Versionsnummer oder »jeder späteren Ver-
sion« (»any later version«) unterliegt, so haben Sie die Wahl, entweder den Bestimmungen der
genannten Version zu folgen oder denen jeder beliebigen späteren Version, die von der Free Soft-
ware Foundation veröffentlicht wurde. Wenn das Programm keine Versionsnummer angibt, können
Sie eine beliebige Version wählen, die je von der Free Software Foundation veröffentlicht wurde.
Paragraph 10
Wenn Sie den Wunsch haben, Teile des Programms in anderen freien Programmen zu verwenden,
deren Bedingungen für die Verbreitung andere sind, schreiben Sie an den Autor, um ihn um die
Erlaubnis zu bitten. Für Software, die unter dem Copyright der Free Software Foundation steht,
schreiben Sie an die Free Software Foundation; wir machen zu diesem Zweck gelegentlich Ausnah-
men. Unsere Entscheidung wird von den beiden Zielen geleitet werden, zum einen den freien Status
aller von unserer freien Software abgeleiteten Werke zu erhalten und zum anderen das gemein-
schaftliche Nutzen und Wiederverwenden von Software im Allgemeinen zu fördern.
Keine Gewährleistung.
Paragraph 11
Da das Programm ohne jegliche Kosten lizenziert wird, besteht keinerlei Gewährleistung für das Pro-
gramm, soweit dies gesetzlich zulässig ist. Sofern nicht anderweitig schriftlich bestätigt, stellen die
Copyright-Inhaber und/oder Dritte das Programm so zur Verfügung, »wie es ist«, ohne irgendeine
Gewährleistung, weder ausdrücklich noch implizit, einschließlich – aber nicht begrenzt auf – Markt-
reife oder Verwendbarkeit für einen bestimmten Zweck. Das volle Risiko bezüglich Qualität und Leis-
tungsfähigkeit des Programms liegt bei Ihnen. Sollte sich das Programm als fehlerhaft herausstellen,
liegen die Kosten für notwendigen Service, Reparatur oder Korrektur bei Ihnen.
Index Sicher 31.01.2001 11:48 Uhr Seite 427
Paragraph 12
In keinem Fall, außer wenn durch geltendes Recht gefordert oder schriftlich zugesichert, ist irgend-
ein Copyright-Inhaber oder irgendein Dritter, der das Programm wie oben erlaubt modifiziert oder
verbreitet hat, Ihnen gegenüber für irgendwelche Schäden haftbar, einschließlich jeglicher allge-
meiner oder spezieller Schäden, Schäden durch Seiteneffekte (Nebenwirkungen) oder Folgeschä-
den, die aus der Benutzung des Programms oder der Unbenutzbarkeit des Programms folgen (ein-
schließlich – aber nicht beschränkt auf – Datenverluste, fehlerhafte Verarbeitung von Daten,
Verluste, die von Ihnen oder anderen getragen werden müssen, oder dem Unvermögen des Pro-
gramms, mit irgendeinem anderen Programm zusammenzuarbeiten), selbst wenn ein Copyright-
Inhaber oder Dritter über die Möglichkeit solcher Schäden unterrichtet worden war.
Index Sicher 31.01.2001 11:48 Uhr Seite 428