Inhaltsverzeichnis C++ Kurs

Werbung
Inhaltsverzeichnis C++ Kurs
Inhaltsverzeichnis
Sie können sich nun in der nachfolgenden Aufstellung eine beliebige Lektion aussuchen. Wenn
Sie den Kurs zum ersten Mal besuchen, beginnen Sie am Besten mit der Lerneinheit über die
Entwicklungsumgebung.
Über den (in den Lektionen vorhandenen) Button
am oberen und unteren Rand kommen
Sie von jeder Seite aus wieder zum Inhaltverzeichnis zurück.
Grundlagen
Lerneinheit 1
Entwicklungsumgebung
Aufbau eines C++ Programms
Kommentare
Ausgabestream cout
Lerneinheit 2
Variablen
Konstanten
Zuweisungen
Lerneinheit 3
Grundrechenoperationen
Bit- und Schiebeoperationen
Vergleichs-/Logikoperatoren
Rangfolge der Operatoren
Zeiger
Lerneinheit 4
Typkonvertierungen
Eingabestream cin
Dateizugriffe über Streams
Ablaufsteuerung und erweiterte Datentypen
http://www.cpp-tutor.de/cpp/toc.htm (1 von 3) [17.05.2005 11:37:35]
Inhaltsverzeichnis C++ Kurs
Lerneinheit 5
if-else Verzweigung
switch-case Verzweigung
for-Schleife
while-Schleife
break und continue
Lerneinheit 6
Felder und C-Strings
Strings
String-Konvertierungen
Lerneinheit 7
Funktionen
Gültigkeitsbereiche
Speicherklassen und Qualifizierer
Lerneinheit 8
typedef Anweisung
enumerated Daten
Bitfelder
Präprozessor-Direktiven
Klassen, Objekte und dynamische Speicherverwaltung
Lerneinheit 9
Klassen und OOP
Memberzeiger
Unions (Varianten)
Lerneinheit 10
Konstruktor und Destruktor
Inline-Funktionen/-Memberfunktionen
Default-Parameter
Statische Member
this-Zeiger
Eingeschlossene Objekte
Lerneinheit 11
Operatoren new und delete
Dynamische Eigenschaften und Objekte
Überladen von Funktionen/Memberfunktionen/Operatoren,
Einführung der Standard-Bibliothek
Lerneinheit 12
Überladen von Funktionen/Memberfunktionen
Überladen des Konstruktors
Überladen des Zuweisungsoperators
Allgemeines Überladen von Operatoren
Überladen der Operatoren <<, >>, ( ), [ ], new und delete
http://www.cpp-tutor.de/cpp/toc.htm (2 von 3) [17.05.2005 11:37:35]
Inhaltsverzeichnis C++ Kurs
Lerneinheit 13
Standard-Bibliothek (Standard Template Library) Überblick
pair Datentyp
Standard-Bibliothek Hilfstemplates
STL Container (Teil 1)
string (Teil 2)
C++ für Fortgeschrittene
Lerneinheit 14
Abgeleitete Klassen
Mehrfach abgeleitete Klassen
Lerneinheit 15
Funktions-Templates
Klassen-Templates
Virtuelle Memberfunktionen
Virtuelle Basisklassen
Lerneinheit 16
friend-Funktionen und -Klassen
Ausnahmebehandlung (Exception-Handling)
Laufzeit-Typinformationen
Weitere Typkonvertierungen
Namensbereiche
Templates und die Standard Bibliothek (Teil 2)
Lerneinheit 17
Template-Vertiefungen
Standard-Exceptions
Lerneinheit 18
STL Container (Teil 2)
Iteratoren
Funktionsobjekte
Algorithmen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/toc.htm (3 von 3) [17.05.2005 11:37:35]
Hinweise
Hinweise
Vorraussetzungen
Damit Sie den Kurs bearbeiten können benötigen Sie folgende Dinge:
Einen möglichst standard-konformen C++ Compiler. Die
Betonung liegt hierbei auf standard-konformen.
Sie brauchen aber für den Anfang nicht gleich mehrere
hundert Mark für einen der kommerziellen Compiler, wie
z.B. den MICROSOFT VC++ oder den BORLAND
BUILDER, investieren. Für Ihre ersten Gehversuche reicht
auch einer der Freeware-Compiler BORLAND C++ 5.5.1
oder die WINDOWS Version MinGW des GNU C++
Compilers. Der MinGW Compiler befindet sich mit auf der
CD-ROM. Wie Sie diesen Compiler installieren, dass
erfahren Sie gleich noch im Kurs. Alle Beispiele und
Übungen wurden mit diesem Compiler übersetzt.
Und dann brauchen Sie noch Spaß und Ausdauer bei der
Arbeit an Ihrem Computer. Sie werden sehen, das Erlernen
der Sprache C++ geht nicht von heute auf morgen.
Sollten Sie als C++ Profi diesen Kurs aus Neugierde einmal überfliegen, denken Sie
daran, dass der Kurs für Einsteiger geschrieben wurde. Er behandelt absichtlich nicht
jedes Detail des C++ Standards da dies im Allgemeinen beim Einstieg in die C++
Programmierung leicht zu Verwirrung führen kann. Sollte jedoch ein Widerspruch zum
Standard im Kurs erwähnt werden, so ist der Autor über jeden Hinweis darüber
dankbar. Auch Teile des Standards die (bis jetzt noch) kein Compiler richtig beherrscht,
wie z.B. das exportieren von Templates, wurden in den Kurs noch nicht aufgenommen.
Die Symbole im Kurs
Im Kurs werden bestimmte Abschnitte durch ein Symbol gekennzeichnet. Welche Bedeutung
http://www.cpp-tutor.de/cpp/intro/hinweise.htm (1 von 3) [17.05.2005 11:37:38]
Hinweise
ein Symbol hat können Sie aus der nachfolgenden Tabelle entnehmen:
Symbol
Bedeutung
Allgemeiner Hinweis
Besonders wichtiger Textabschnitt oder Hinweis auf eine Fehlerfalle.
Verweis auf einen anderen Lernabschnitt. Durch anklicken des Symbols
gelangen Sie zu dem entsprechenden Lernabschnitt.
Beispiel zur Lektion
Übung zur Lektion
Weitergehende Erklärungen, die nicht unbedingt zur weiteren
Bearbeitung des Kurses benötigt werden.
Das sehen wir uns nicht ausführlich an
Dieser Kurs soll eine Einführung in die Programmiersprache C++ sein. Er
zeigt Ihnen die Möglichkeiten auf, die Ihnen C++ zur Lösung Ihrer
Programmieraufgabe zur Verfügung stellt.
Nicht Ziel des Kurses ist es, alle Bibliotheksfunktionen zu beschreiben, die
Ihnen C++ zur Verfügung stellt. Dies würde den Kursumfang erheblich
sprengen. Sehen Sie dazu bitte in der Online-Hilfe zu Ihrem Compiler nach.
Beim auf der CDROM befindlichen MinGW Compiler liegt die OnlineHilfe in MinGW-Verzeichnis im Unterverzeichnis doc in den Dateien libc2.3.2.chm bzw. libstdc++-3.3.2.chm. Werden im Kurs Bibliotheksfunktionen
verwendet, so werden diese natürlich auch erklärt.
Die Beispiele und Übungen
Da das Erlernen einer neuen Programmiersprache nur durch fleißiges
üben möglich ist werden im Kurs viele kleine Beispiele verwendet. Am
Ende einer (fast) jede Lektion ist dann ein etwas größeres Beispiel
aufgeführt, in dem der Lehrstoff der Lektion nochmals zusammengefasst
wird. Zusätzlich enthalten die meisten Lektionen noch eine Übung anhand
der Sie Ihr Wissen überprüfen können. Sie können den Code der Beispiele
über die Zwischenablage jederzeit in Ihren Compiler übernehmen.
http://www.cpp-tutor.de/cpp/intro/hinweise.htm (2 von 3) [17.05.2005 11:37:38]
Hinweise
So, wenn Sie jetzt soweit sind, dann kann's los gehen!
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/intro/hinweise.htm (3 von 3) [17.05.2005 11:37:38]
Homepage
C++ Home
Dieser Kurs sich an alle, die einen Einblick in die C++ Programmierung
erhalten wollen. Er setzt keine Vorkenntnisse in der Programmierung voraus,
obwohl dies den Einstieg natürlich erheblich erleichtert.
Was Sie lernen: Sie lernen die Sprachmittel kennen, die Ihnen C++ bietet um
Ihre Programmieraufgabe zu lösen, d.h. Sie lernen den Inhalt des
Werkzeugkoffers C++ kennen.
Was Sie nach Bearbeitung des Kurses noch nicht können: das effektive
Einsetzen der C++ Sprachmittel. Dies erreichen Sie nur durch stetige
Anwendung des im Kurs vermittelten Wissens.
Nach Beendigung des Kurses werden Sie aber in der Lage sein, eigene C++
Programme zu entwickeln und, genügend Neugierde vorausgesetzt, den
Anschlusskurs 'C++ in der Praxis' zu bearbeiten.
Bestell-Information
Wenn Ihnen der Kurs gefällt, können Sie Ihn auch auf CD-ROM beziehen. Auf der CDROM sind alle Beispiele und Lösungen zu den Übungen enthalten sowie eine komplette
C++ Entwicklungsumgebung. Die CD-ROM kostet 25.00 EUR, inkl. Versand innerhalb
Deutschlands. Bei gleichzeitiger Bestellung des C++ und MFC Kurses ermäßigt sich
der Preis für beide Kurse auf 40.00 EUR. Die Bezahlung erfolgt entweder per
Vorauskasse (Scheck) oder per Nachnahme. Bei Versand per Nachnahme erhöhen sich
die obigen Preise um die jeweilige Nachnahmegebühr. Bei Firmen ist eine Lieferung
auch gegen Rechnung möglich. Die Bestelladresse ist unten auf der Seite angegeben.
http://www.cpp-tutor.de/cpp/index.html (1 von 2) [17.05.2005 11:37:41]
Homepage
Und hier noch das Rechtliche:
Wenn Sie nun mit dem Kurs fortfahren, gilt folgender Haftungsausschluss als vereinbart:
Der Autor übernimmt keinerlei Gewähr für die Richtigkeit und Vollständigkeit der
bereitgestellten Informationen. Haftungsansprüche gegen den Autor für Schäden, die durch
Nutzung der Informationen verursacht wurden, sind ausgeschlossen.
Bei direkten und indirekten Verweisen auf fremde Internetseiten (Links), die außerhalb der
Verantwortung des Autors liegen, kann der Autor keine Gewährleistung für deren Inhalt
übernehmen. Die Inhalte der entsprechenden Seiten wurden zum Zeitpunkt der Einbringung der
Verweise soweit wie möglich überprüft. Der Autor distanziert sich deshalb ausdrücklich von allen
Inhalten der verwiesenen Seiten, soweit diese nach der Anbringung der Verweise verändert
wurden.
Ferner sind die Kurse urheberrechtlich geschützt. Markenzeichen und Produktnamen sind eventl.
Warenzeichen oder eingetragene Warenzeichen der jeweiligen Rechteinhaber und werden hier
nur zur redaktionellen Zwecken verwendet, ohne Absicht einer Warenzeichenverletzung. Eine
bloße Nennung lässt nicht den Schluss zu, dass Markenzeichen und Produktnamen frei von
Rechten Dritter sind.
Dieser Haftungsausschluss gilt als zur Kenntnis genommen, wenn Sie nun mit dem Kurs
fortfahren. Sollten Sie mit dem Haftungsausschluss nicht einverstanden sein, so beenden Sie jetzt
bitte den Kurs.
Um mit dem Kurs zu beginnen, klicken Sie den nachfolgenden Pfeil an.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/index.html (2 von 2) [17.05.2005 11:37:41]
Entwicklungsumgebung
Entwicklungsumgebung
Die Themen:
Einleitung
Editor
Compiler
Linker
MinGW Entwicklungsumgebung
Einleitung
Als Einstieg in den C++ Kurs wollen wir uns zunächst einmal ansehen, welche Komponenten benötigt
werden um ein C++ Programm zu erstellen. Bei dem meisten kommerziellen C++ Compilern, wie z.B.
dem MICROSOFT VC++ Compiler, sind die in dieser Lerneinheit vorgestellten Komponenten Editor,
Compiler und Linker zwar unter einer einheitlichen Oberfläche, der so genannten IDE (Integrated
Development Environment) zusammengefasst, jedoch sollten Ihnen Begriffe wie Compiler und Linker
bekannt sein. Der in diesem Kurs standardmäßig verwendete MinGW C++ Compiler ist ein 'reiner'
Kommandozeilen Compiler, d.h. zum Übersetzen eines C++ Programms müssen Sie normalerweise ein
Konsolen-Fenster (DOS-Fenster) öffnen um den Compiler zu starten. Wir werden später weiter unten
auch diese Compiler komfortabel in einen Editor einbinden.
Sollten Ihnen die Begriffe Compiler und Linker schon bekannt sein, so können Sie auch gleich zur
Beschreibung der MinGW Entwicklungsumgebung gehen ( ). Verwenden Sie eine andere
Entwicklungsumgebung, und Ihnen sind die Begriffe bekannt, so gehen Sie einfach zur nächsten Seite
über.
Editor
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (1 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Fangen wir mit der Eingabe des
Programms an. Für die Eingabe eines
C++ Programms können Sie jeden
beliebigen ASCII-Editor benutzen, wie
z.B. den guten alten DOS Edit oder
unter WINDOWS auch den
NOTEPAD. Was Sie aber auf keinen
Fall verwenden dürfen, sind Editoren
die Texte formatieren wie z.B.
WinWord. Diese Editoren legen außer
dem eigentlichen Text noch
Steuerzeichen mit ab, die dann später
den Compiler mit Sicherheit verwirren.
Haben Sie Ihr Programm im Editor
fertig eingegeben, so sollten Sie die
Datei vorzugsweise unter einen Namen
mit der Extension .cpp speichern. Auf
keinen Fall sollten Sie die Extension .c
verwenden, da einige C++ Compiler
sonst annehmen, Sie wollen ein C
Programm und nicht ein C++
Programm erstellen. Und C
Programme besitzen u.a. nur eine
Untermenge der Schlüsselwörter eines
C++ Programms.
Compiler
Nach dem Sie das Programm eingegeben haben, müssen
Sie es mit dem Compiler übersetzen. Beim Übersetzen
des Programms wird aus Ihrem ASCII Quelltext
zunächst nur einmal ein Zwischencode erzeugt, der in
entsprechenden Dateien abgelegt wird. Dabei wird für
jede Quelldatei auch eine Datei mit Zwischencode
erzeugt. Dateien mit diesem Zwischencode haben sehr
oft die Extension .obj oder .o. Der Grund für diesen
'Zwischenschritt' liegt unter anderem darin, dass Ihr
Programm alleine noch nicht lauffähig ist, sondern
zusätzliche Funktionen benötigt die vom CompilerHersteller zur Verfügung gestellt werden. Zu diesen
Funktionen gehören z.B. alle Funktionen für die Einund Ausgaben oder auch die trigonometrischen
Funktionen wie sin(...) usw. Diese Funktionen liegen in
der so genannten CRT (C Runtime Library) die Sie
Ihrem Programm noch hinzufügen müssen (siehe dazu
nächsten Abschnitt).
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (2 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Linker
Der letzte Schritt bei der Erstellung
eines C++ Programms ist das
Zusammenbinden der diversen
Zwischendateien (vom Compiler
aus den Quelldateien erzeugt) und
das Hinzufügen der Funktionen aus
der CRT. Dieser Vorgang wird vom
Linker durchgeführt. Der Linker
'tackert' sozusagen alle
Zwischendateien und die CRTFunktionen zusammen und erzeugt
daraus dann eine ausführbare
Datei, die unter DOS bzw.
WINDOWS die Extension .exe
besitzt. Die CRT-Funktionen holt
sich der Linker aus
Bibliotheksdateien. Solche
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (3 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Bibliotheksdateien haben sehr oft
die Extension .lib. Vielleicht fragen
Sie sich nun, warum die
Bibliotheksdateien nicht ebenfalls
die Extension .obj oder .o haben?
Wenn der Linker beim
Zusammenbinden der ausführbaren
Datei eine Datei mit der Extension
.lib benötigt, so bindet er nicht wie
bei .obj bzw. .o Dateien die
komplette Datei ein, sondern holt
sich aus dieser Lib-Datei im Prinzip
nur die Funktionen heraus, die vom
Programm auch benötigt werden.
Beim Einbinden einer Obj-Datei
hingegen wird immer die gesamte
Datei verwendet. Die Verwendung
von Lib-Dateien verringert die
Größe der ausführbaren Datei
erheblich. Und warum sollte der
Linker z.B. auch immer die
Funktion sin(...) dazu binden, wenn
Sie sie nur in manchen
Programmen verwenden?
Damit sind wir am Ende dieser kurzen Erklärung der Begriffe Compiler und Linker angelangt, und
wir können zur Beschreibung der MinGW Entwicklungsumgebung innerhalb des Kurses übergehen.
MinGW Entwicklungsumgebung
Installation des Compilers
Auf der CD-ROM befindet sich im Verzeichnis MinGW der Freeware C++ Compiler MinGW.
Installieren Sie zunächst den C++ Compiler indem Sie die Datei setup.exe auf der CD-ROM starten und
die Anweisungen des Setup-Programms befolgen. Anschließend müssen Sie jetzt noch das binVerzeichnis des Compilers zum aktuellen Pfad hinzufügen.
Unter WINDOWS 9x/ME:
Fügen Sie als erstes zur Datei AUTOEXEC.BAT den Compiler-Pfad wie folgt hinzu:
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (4 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
set PATH=%PATH%;c:\mingw\bin;
Die obige Angabe bezieht sich auf eine Installation des Compilers ins Verzeichnis c:\mingw. Passen Sie
die Pfadangaben entsprechend Ihrer Installation bitte an. Beachten Sie, dass Sie danach den Rechner
neu starten müssen damit die Einstellungen wirksam werden.
Unter WINDOWS 2000/XP:
Öffnen Sie die Systemsteuerung und führen Sie dann einen Doppelklick auf das Symbol System aus.
Öffnen Sie im eingeblendeten Dialog die Seite Erweitert und klicken dann den Button
Umgebungsvariablen an:
Die nachfolgenden Ausführungen gehen davon aus, dass Sie den MinGW Compiler im StandardVerzeichnis c:\mingw installiert haben. Haben Sie den Compiler in einem anderen Verzeichnis
installiert, so passen Sie die Angaben entsprechend an.
Erweitern Sie dann die Benutzervariable path wie folgt:
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (5 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Übernehmen Sie die Einstellung. Ein Neustart des Rechners ist unter WINDOWS 2000/XP nicht
notwendig.
Hier geht's wieder gemeinsam weiter:
Haben Sie alle Einstellung vorgenommen, so öffnen Sie ein Konsolen-Fenster (DOS-Box) und geben
folgenden Befehl ein:
E:\DevTools\MinGW1\bin>g++ -v
Sie sollten daraufhin etwa folgende Ausgabe erhalten:
Reading specs from ./../lib/gcc-lib/mingw32/3.2.2/specs
Configured with: ../gcc/configure --with-gcc --with-gnu-ld --with-gnu-as
--host=
mingw32 --target=mingw32 --prefix=/mingw --enable-threads --disable-nls -enable
-languages=c++,f77,objc --disable-win32-registry --disable-shared -enable-sjljexceptions
Thread model: win32
gcc version 3.2.2 (mingw special 20030208-1)
Die ausgegebene Versions-Nummer (gcc version ...) kann je nach installiertem MinGW Compiler
abweichen.
Wenn Sie diese Ausgabe erhalten, ist die Installation des C++ Compiler erfolgreich verlaufen. Erhalten
Sie die Ausgabe nicht, so sehen Sie bitte nach, ob das bin-Verzeichnis des Compilers wie angegeben im
Pfad eingetragen ist.
Installation des SciTE Editors
Für die Eingabe des Quelltextes können Sie prinzipiell jeden beliebigen ASCII-Editor verwenden. Ein
für die Eingabe von C++ Programmen sehr guter Editor ist der Freeware Editor SciTE, der sich
ebenfalls auf der CD-ROM befindet. Dieser Editor kennt unter anderem auch die C++
Schlüsselwörter und stellt diese farbig dar. Dieses so genannte Syntax-Highlightning verbessert den
Überblick über das Programm erheblich. Da ein Bild oft mehr aussagt als Worte, nachfolgend ein
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (6 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Screenshot des Editors:
Um den Editor zu installieren, führen Sie die Datei setup.exe im Verzeichnis SciTE auf der CD-ROM
aus und befolgen Sie die Anweisungen des Setup-Programms.
Nach der Installation des Editors brauchen Sie in der Regel keine weiteren Einstellungen vornehmen.
Wenn Sie wollen, können Sie den Editor jedoch Ihren persönlichen Wünschen entsprechend anpassen.
Der Editor wird fasst vollständig durch properties-Dateien im Editor-Verzeichnis konfiguriert. Nähere
Informationen dazu entnehmen Sie bitte der Datei SciTEDoc.html im Installationsverzeichnis. So lässt
sich durch editieren der Datei cpp.properties ohne weiteres auch ein beliebiger anderer Compiler
einbinden.
Testen wir nun einmal, ob sich der C++ Compiler vom Editor aus richtig aufrufen lässt. Starten Sie
jetzt den Editor und öffnen dann die Datei le01\prog\test\test.cpp im Installationsverzeichnis des C++
Kurses. Im Editorfenster sollten folgende Anweisungen stehen:
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (7 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Die Funktionsweise der Programms brauchen Sie zum jetzigen Zeitpunkt noch nicht verstehen, wir
wollen ja jetzt nur die Ausführung des Compilers testen.
Drücken Sie die <Strg>-0 Taste oder wählen aus dem Menü Extras den Menüpunkt CompLink aus
um den Compiler zu starten. Wurde der Compiler erfolgreich gestartet, so erhalten Sie im unteren
Ausgabefenster des Editors folgende Meldungen:
Das heißt, in unserem Programm ist noch ein kleiner Fehler in der Zeile 5. Gehen Sie einmal in die
Zeile 5. Die aktuelle Zeilennummer wird übrigens links neben dem Quellcode angezeigt. Bei kleineren
Programmen können Sie immer per Tastatur zur Fehlerzeile gehen. Schöner wäre es jedoch, wenn die
Fehlerzeile automatisch im Editor angezeigt werden würde, insbesondere bei 'richtigen' größeren
Programmen. Und auch dies kann der Editor. Führen Sie im Ausgabefenster einen Doppelklick auf die
Zeile aus, in der der Fehler spezifiziert ist. Anschließend sollte die entsprechende Zeile im Editor
farblich hervorgehoben anzeigt werden.
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (8 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Entfernen Sie nun die komplette Zeile mit x = 5; und übersetzen Sie das Programm erneut. Es sollte
dann keine Fehlermeldung mehr im Ausgabefenster erscheinen.
Nun gilt es nur noch das Programm zu starten. Da der MinGW C++ Compiler in unserer
Konfiguration ein Konsolenprogramm erstellt, müssten Sie zuerst ein entsprechendes Fenster (MSDOS Eingabeaufforderung) öffnen, dann in das Verzeichnis wechseln, in dem Ihr Programm steht, und
schließlich das Programm starten. Die ersten beiden Schritte können wir auch hier wieder unserem
Editor übertragen. Sie können durch drücken der Tasten <Strg>-3 oder Auswahl des Menüpunktes
Konsole im Extra Menü ein Konsolen-Fenster öffnen und befinden sich dann im gleichen Verzeichnis, in
dem aktuelle Quelldatei des Editors liegt. Drücken Sie nun die Tasten <Strg>-3. Es sollte dann ein
Konsolen-Fenster aufgehen und Sie sollten sich im gleichen Verzeichnis befinden wie Ihr Programm.
test.exe. Starten Sie das Programm einmal durch Eingabe von test. Sie werden die Ausgabe Hallo!
erhalten.
Es geht aber auch noch einfacher. Wählen Sie aus dem Menü Extras den Menüpunkt Start aus (oder
drücken Sie einfach <F5>)und das Programm wird gestartet und im Ausgabefenster erscheint nun die
Programmausgabe:
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (9 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Ist doch recht angenehm mit so einem Editor zu arbeiten, oder?
Die Übung:
Und hier Ihre erste Übung:
●
●
●
●
Gehen Sie wieder zum Editor zurück.
Ersetzen Sie den Text Hallo! durch einen beliebigen anderen Text. Lassen Sie dabei aber die
Anführungszeichen links und rechts des Textes bitte stehen!
Übersetzen Sie das Programm (Menüpunkt Extras-CompLink oder <Strg>-0).
Starten Sie das Programm danach (z.B. mittels <F5>). Ihr neu eingegebener Text sollte nun im
Ausgabefenster des Editors erscheinen.
Mehr zu Ausgaben nachher im Kurs noch.
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (10 von 11) [17.05.2005 11:37:50]
Entwicklungsumgebung
Die Übung:
Und gleich eine zweite Übung. Sie werden hierbei lernen, wie Sie selbst ein eigenes C++ Programm
schreiben.
●
●
●
●
●
●
Falls Sie den Editor und das Konsolen-Fenster noch geöffnet haben, schließen Sie bitte zuerst
das Konsolen-Fenster und dann den Editor.
Starten den Editor. Wenn Sie vorhin alles richtig konfiguriert haben, sollte keine Datei im
Editor geöffnet sein.
Wählen Sie aus dem Menü Datei den Eintrag Neu aus oder klicken Sie das Symbol
an.
Geben Sie dann folgenden Code ein. Beachten Sie genau die Groß-/Kleinschreibung.
Speichern Sie das Programm in einem beliebigen Verzeichnis unter einem beliebigen Namen
mit der Extension .cpp ab. Sie sollten für eigene C++ Programme immer die Extension .cpp
wählen, sonst kann der Editor die Datei dem MinGW-Compiler nicht zuordnen.
Übersetzen Sie das Programm und starten Sie es, so wie oben angegeben.
Auf diese Art und Weise werden eigene C++ Programme erstellt und übersetzt.
Und das war's auch auch schon. Und jetzt geht's dann los, mit der ersten richtigen Lektion!
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_01.htm (11 von 11) [17.05.2005 11:37:50]
Aufbau eines C++ Programms
Aufbau eines C++ Programms
Die Themen:
Wichtiger Hinweis
main() Funktion
Syntax einer Ausdrucks-Anweisungen
Source-Code Formatierung
Wichtiger Hinweis
Bevor wir in die C++ Programmierung so richtig einsteigen, eines gleich vorne weg:
Die Programmiersprache C++ unterscheidet, im Gegensatz zu so manch anderen
Programmiersprachen, streng zwischen Klein- und Großschreibung! So ist z.B. die
Funktion sin(..) zur Sinusberechnung etwas anderes als die Funktion Sin(...) (die es so
standardmäßig nicht gibt).. Beachten Sie dies bitte unbedingt, wenn Sie irgendwelche
Beispiele aus dem Kurs selbst eingeben.
main() Funktion
http://www.cpp-tutor.de/cpp/le01/le01_02.htm (1 von 5) [17.05.2005 11:37:51]
Aufbau eines C++ Programms
Fangen wir nun auch gleich int main()
mit der wichtigsten Frage
{
an, wenn es ums
...hier stehen nachher Anweisungen
Programmieren geht:
}
welches ist denn
letztendlich die erste, von
Ihnen geschriebene,
Anweisung, die in einem
Programm ausgeführt
wird? Nun, jedes C++
Programm muss eine
Funktion namens main(...)
enthalten.
Und diese Funktion ist die erste Anweisung die in Ihrem Programm ausgeführt wird. Oben
sehen Sie den allgemeinen Aufbau der Funktion main(...). Nach dem Funktionskopf int main(...)
folgt ein Paar geschweifter Klammern. Innerhalb dieses Klammerpaares stehen nachher die
Anweisungen, die das Programm ausführen soll Dieses 'kleine Programm' oben führt natürlich
noch nichts aus, da es außer der main(...)-Funktion noch keine weiteren Anweisungen enthält.
Es gibt noch eine zweite Form der main(...)-Funktion, die es erlaubt, dem Programm
beim Start zusätzliche Angaben (Parameter) mitzugeben. Diese können dann
innerhalb von main(...) ausgewertet werden. Wenn Sie mehr darüber erfahren
möchten, klicken Sie links das Symbol an.
Syntax einer Ausdrucks-Anweisung
Eine Ausdrucks-Anweisung
besteht aus einem
Ausdruck, dessen Ergebnis
dann in der Regel
irgendwie verarbeiten wird.
Im Beispiel rechts wird zum
Beispiel das Ergebnis des
Ausdrucks var1 * ...; einer
Variablen zugewiesen.
Jedes AusdrucksAnweisung wird mit einem
Semikolon abgeschlossen.
Erst das Semikolon teilt
dem Compiler das Ende
dieser Anweisung mit!
Daraus folgt, dass eine
Ausdrucks-Anweisung über mehrere Zeilen:
value = var1 * 3.1416 +
var2 * var2;
Mehrere Ausdruck-Anweisungen in einer Zeile:
var1 = 10;
var2 = 0;
http://www.cpp-tutor.de/cpp/le01/le01_02.htm (2 von 5) [17.05.2005 11:37:51]
Aufbau eines C++ Programms
Ausdrucks-Anweisung sich
auch über mehrere Zeilen
erstrecken kann, oder auch
dass mehrere AusdruckAnweisungen innerhalb
einer Zeile stehen dürfen.
Der Zeilenumbruch spielt
für den Compiler keine
Rolle, er dient einzig und
allein dazu, ein Programm
lesbar zu gestalten. Mehr
zur Formatierung eines
C++ Programms nachher
gleich noch.
Sie sollten allerdings mehrere Ausdrucks-Anweisungen nicht innerhalb einer Zeile stehen
haben, wenn Sie beabsichtigen, Ihr Programm einmal mit einem Debugger im EinzelschrittModus zu durchlaufen. Debugger durchlaufen in der Regel ein Programm immer zeilenweise.
Falls mehrere Ausdrucks-Anweisungen nun in einer Zeile stehen, so werden alle diese
Anweisungen innerhalb einer Zeile auf einmal ausgeführt. Für den Ablauf des übersetzten
Programms ist es unerheblich, ob Sie mehrere dieser Anweisungen innerhalb einer Zeile
eingeben oder jede Anweisung in einer getrennten Zeile steht.
Und nochmals weil's so wichtig ist:
Vergessen Sie nie am Ende einer Ausdrucks-Anweisung das Semikolon! Wenn Sie es
einmal vergessen haben, so meldet Ihnen der Compiler unter Umständen eine ganze
Reihe von Fehlern, da er das Ende der Anweisung nicht erkennen kann!
Source-Code Formatierung
Eigentlich könnten wir hier unsere erste Lektion abschließen. Da wir aber lesbare Programme
schreiben wollen, hier noch einige Worte zur Formatierung des Source-Codes.
http://www.cpp-tutor.de/cpp/le01/le01_02.htm (3 von 5) [17.05.2005 11:37:51]
Aufbau eines C++ Programms
Der C++ Compiler lässt eine
formatfreie Eingabe des SourceCode zu. So lässt sich das rechts
stehende Programm zwar
fehlerfrei übersetzen, ist jedoch
schwer lesbar.
Damit Sie Ihr Programm auch
nach einiger Zeit noch ohne
Schwierigkeiten lesen können,
sollten Sie sich an folgende 2
Regeln halten:
●
●
Beginnen Sie jede
Funktion in der ersten
Spalte. Danach rücken
Sie jeden weiteren Block
{...} mit einem TAB ein.
Setzen den TAB z.B. auf
4 Zeichen damit die
Einrückungen nicht zu
tief werden.
Schreiben Sie nicht mehr
als eine Anweisung pro
Zeile. Geht die Zeile über
die Spalte 80 hinaus (im
Regelfall rechter Rand
eines DIN A4 Blatts),
beginnen Sie eine neue
Zeile und rücken den
Rest der Anweisung
entsprechend ein.
Wenn Sie jetzt rechts den
Button Formatiert anklicken
wird das Programm
entsprechend formatiert. Und
dann sieht das ganze doch schon
viel übersichtlicher aus, oder?
Aber es nicht noch nicht gut genug! Was hier noch fehlt, das erfahren Sie in der nächsten
Lektion.
http://www.cpp-tutor.de/cpp/le01/le01_02.htm (4 von 5) [17.05.2005 11:37:51]
Aufbau eines C++ Programms
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_02.htm (5 von 5) [17.05.2005 11:37:51]
Kommentare
Kommentare
Die Themen:
Einleitung
Mehrzeilen-Kommentar
Einzeilen-Kommentar
Einleitung
Kommentare sind Passagen innerhalb des Programms die vom Compiler ignoriert werden. Sie
dienen hauptsächlich dazu, Hinweise zur Funktion eines Programms zu geben. Kommentare
können an beliebiger Stelle innerhalb eines Programms stehen und (fast) beliebige Zeichen
enthalten.
Auch wenn es oft lästig ist Kommentare zu schreiben, denken Sie immer an folgenden Satz:
Ein Programm ist meistens nur so gut, wie seine Kommentare sind. Dies gilt umso mehr,
je länger ein Programm ist bzw. je komplizierter der Programmablauf ist.
Doch damit die Sache nicht ganz so einfach wird, gibt es zwei Arten von Kommentaren.
Mehrzeilen-Kommentar
http://www.cpp-tutor.de/cpp/le01/le01_03.htm (1 von 3) [17.05.2005 11:37:53]
Kommentare
Der Mehrzeilen-Kommentar wird
eingeleitet durch die beiden
Zeichen /* und durch die Zeichen
*/ beendet. Zwischen den beiden
Zeichen darf kein Leerzeichen
stehen. Alles was zwischen diesen
Zeichenfolgen steht wird vom
Compiler ignoriert. Daraus folgt,
dass dieser Kommentar auch über
mehrere Zeilen gehen kann und
sogar auch innerhalb einer
Anweisung stehen darf. Vermeiden
Sie aber trotzdem Kommentare
dieses Typs in mitten von
Anweisungen zu platzieren, dies
macht das Programm nur
unübersichtlich. Wenn Sie nun
rechts den Button ein bzw. aus
anklicken, so werden im Programm
rechts einige Kommentare dieses
Typs ein- bzw. ausgeblendet.
Diese Kommentar-Art
eignen sich auch sehr gut
zum Auskommentieren
von Anweisungsfolgen.
Mehr darüber durch
anklicken des Symbols
links.
Befinden sich innerhalb eines Kommentars weitere Kommentare des Typs /*...*/,
so meldet der Compiler im Regelfall einen Fehler. Laut ANSI C++ sind geschachtelte
Kommentare dieses Typs nicht erlaubt. Viele Compiler besitzen jedoch eine Option
mit der Sie geschachtelte Kommentare freigegeben werden können.
Einzeilen-Kommentar
http://www.cpp-tutor.de/cpp/le01/le01_03.htm (2 von 3) [17.05.2005 11:37:53]
Kommentare
Außer dem Kommentartyp
/*...*/ bietet C++ noch den Typ
//. Alles was nach den beiden
Zeichen // bis zum Zeilenende
folgt, wird vom Compiler als
Kommentar betrachtet. Auch hier
darf zwischen den beiden Zeichen
// kein Leerzeichen stehen. Dieser
Kommentar bietet sich immer dann
an, wenn nur eine einzelne Zeile
auskommentiert werden soll oder
am Ende einer Anweisung eine
Erläuterung zur Anweisung folgt.
Die beiden Kommentartypen
/*...*/ und // können gemischt
im Programm verwendet werden.
Wenn Sie jetzt rechts wieder den
Button ein anklicken, wird unter
anderem die letzte
Ausgabeanweisung
auskommentiert.
So, war bisher mehr oder weniger alles graue Theorie, so kommt in der nächsten Lektion die
Praxis. Wir werden uns ansehen, wie Ausgaben auf die Standard-Ausgabe (in der Regel ist dies
der Bildschirm) vonstatten gehen und damit auch die in den bisherigen Beispielen immer
wieder auftauchende cout-Anweisung enträtseln.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_03.htm (3 von 3) [17.05.2005 11:37:53]
main() die Zweite
main() die Zweite
Die zweite Form der main(...) Funktion
Diese zweite Form der
int main(int argn, char *argv[])
main(...)-Funktion besitzt
{
innerhalb der runden
...hier stehen nachher Anweisungen
Klammern noch zusätzliche }
Angaben. Solche Angaben
innerhalb von
Funktionsklammern
werden auch als Parameter
bezeichnet.
Mithilfe dieser Parameter können Sie Ihren Programmen beim Start zusätzliche Angaben
mitgeben und diese dann innerhalb des Programms auswerten. Nehmen wir einmal an, Ihr
Programm heißt MYPROG.EXE und würde z.B. irgendwelche Konfigurationsdaten benötigen.
Diese Konfigurationsdaten könnten in einer Datei abgelegt sein, deren Namen Sie dem
Programm beim Starten übergeben wollen. Damit könnte ein Aufruf z.B. wie folgt aussehen:
myprog -c config.dat
Ihr Programm müsste nun irgendwie die zusätzlichen Angaben -c conf.dat erhalten. Und genau
dies ist Zweck der Parameter argn und argv innerhalb der Parameterklammer von main(...).
Die Namen der Parameter argn und argv sind 'rein willkürlich'. Sie könnten den
Parametern auch andere Namen geben wie z.B. option und value, jedoch haben sich argn
und argv quasi als Standardnamen hierfür eingebürgert. Was Sie aber unter keinen
Umständen ändern dürfen, sind die Datentypen der Parameter, also int und char*...[].
Was Datentypen sind wird später noch ausführlich erklärt.
Der Parameter argn enthält die Anzahl der eingegebenen Wörter einschließlich des
Programmnamens. Unter Wörter werden hier beliebige Eingaben verstanden, die durch ein
Leerzeichen voneinander getrennt sind. Auf die einzelnen Wörter selbst kann über den
Parameter argv zugegriffen werden. argv ist ein Feld von char-Zeigern, wobei der letzte Eintrag
im Feld immer eine '0' ist. Zu Zeigern und Felder kommen wir später noch. Nur soviel vorab:
um auf ein bestimmtes Element in einem Feld zuzugreifen, geben Sie zuerst den Feldnamen an
(hier also argv) und dann in einem eckigen Klammerpaar den Index des gewünschten Elements.
Der Inhalt der Parameter von main(...) könnte bei obigem Aufruf somit wie folgt aussehen:
http://www.cpp-tutor.de/cpp/le01/le01_02_d1.htm (1 von 3) [17.05.2005 11:37:53]
main() die Zweite
Parameter Inhalt
argn
3
argv[0]
myprog
argv[1]
-c
argv[2]
config.dat
argv[3]
0
Anzumerken sei an dieser Stelle noch, dass die Inhalte von argv[] lt. C++ Standard nicht
vorgeschrieben sind. In der Regel enthält das Element argv[0] aber den Namen des Programms
selbst. Der C++ Standard verlangt nur, dass hier der Name steht, mit dem das Programm
aufgerufen wurde, und dies ist fast immer der Programmname.
Die Übung:
Sie können jetzt einmal anhand eines vorgegebenen Programms le01\prog\mainpar zum einen
Ihren Compiler testen und zum anderen sich anschauen, wie innerhalb eines Programms die
Parameter ausgewertet werden.
●
●
Laden Sie das Programm mainpar.cpp aus dem Kursverzeichnis le01/prog/mainpar in
Ihren Editor und übersetzen Sie es.
Wenn Sie den SciTE Editor verwenden, so setzen Sie den Cursor in das
Ausgabefenster am unteren Rand und geben die nachfolgende, rot dargestellte, Zeile
ein. Das Programm wird daraufhin gestartet und gibt die einzelnen Parameter aus.
Haben Sie einen anderen Editor, so müssen Sie ein extra Konsolenfenster öffnen und
dann das Programm aufrufen.
mainpar para1 para2 "para blank"<RETURN>
Folgende Parameter wurden uebergeben:
1. Parameter: c:/cppkurs/le01/prog/mainpar/mainpar.exe
2. Parameter: para1
3. Parameter: para2
4. Parameter: para blank
Wie Sie dem Auszug entnehmen können, werden die einzelnen Parameter durch Leerzeichen
voneinander getrennt. Soll ein Parameter auch Leerzeichen enthalten, so ist der Parameter in
Anführungszeichen zu setzen.
Wenn Sie fertig sind, schließen Sie den Editor wieder und kehren durch schließen dieses
Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le01/le01_02_d1.htm (2 von 3) [17.05.2005 11:37:53]
main() die Zweite
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_02_d1.htm (3 von 3) [17.05.2005 11:37:53]
Streamausgabe
Streamausgabe cout
Die Themen:
Ausgabestream cout
Ausgabe von Text und Zeichen
Escape-Sequenzen
Ausgabe von Daten (Zahlen)
Dezimale, hexadezimale und oktale Ausgabe
Manipulatoren
Beispiel und Übung
Ausgabestream cout
In dieser Lektion erfahren Sie, wie Sie Ausgaben auf die Standard-Ausgabe durchführen. Da uns bis
jetzt noch die Behandlung der Datentypen fehlt, wir aber auch die Ausgabe von Daten an dieser Stelle
uns ansehen werden, werden wir nur ein paar einfache Daten für die Ausgabe verwenden, was aber für
die Erklärung der Standard-Ausgabe ausreichen sollte.
Wenn Sie über Streams im Allgemeinen eine kurze Übersicht erhalten wollen, klicken Sie das
Symbol links an.
Für die Ausgabe auf die Standard-Ausgabe (dies ist in der Regel der Bildschirm) kann das
Ausgabestream-Objekt cout verwendet werden. Damit der Compiler aber weiß, dass cout ein
Ausgabestream-Objekt ist, müssen Sie eine entsprechende Datei, die so genannte Include- oder HeaderDatei, in Ihr Programm einbinden.
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (1 von 14) [17.05.2005 11:37:56]
Streamausgabe
Die Datei, die Sie für cout einbinden
müssen, besitzt den Namen
iostream. Das Einbinden der Datei
erfolgt durch die PräprozessorDirektive #include, wobei der
Dateiname in spitzen Klammern
einzuschließen ist. Beachten Sie
auch, dass am Ende der
Präprozessor-Direktive kein
Semikolon steht! Mehr zu den
Präprozessor-Direktiven, die alle
mit dem Zeichen '#' beginnen,
später noch im Kurs. Die
eigentliche Ausgabe erfolgt dann
mit der Anweisung std::cout.
// Datei iostream einbinden
#include <iostream>
int main ( )
{
std::cout [<< ausgabe1...];
}
Die allgemeine Syntax für die Ausgabe mittels cout lautet:
std::cout << ausgabe1 [ << ausgabe2 [ << ausgabe3 ...]];
Der Präfix std:: teilt dem Compiler mit, dass das Ausgabestream-Objekt im Namensraum std zu finden
ist. Beachten Sie bitte, dass nach std zwei Doppelpunkte folgen. Auch zu Namensräumen später im Kurs
noch mehr. Vorläufig können Sie sich unter einem Namensraum einen getrennten Bereich vorstellen,
der zum Beispiel Ihre Daten von den Daten des Systems trennt.
Anstelle nun bei jeder Ausgabe den Präfix std:: für den Namensraum anzugeben, stehen Ihnen auch
noch folgende zwei Alternativen zur Verfügung:
Sie können einmal am Anfang des
Programms, nach der #includeAnweisung und vor main(...), die
using-Anweisung
using std::cout;
// Datei iostream einbinden
#include <iostream>
// using Anweisung
using std::cout;
int main ( )
{
cout [<< ausgabe1...];
}
angeben. Dadurch teilen Sie dem
Compiler mit, dass er das
Ausgabestream-Objekt coutAnweisung aus dem erwähnten
Namensraum std verwenden soll.
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (2 von 14) [17.05.2005 11:37:56]
Streamausgabe
Als zweite Alternative können Sie
die using-Directive
using namespace std;
verwenden. Diese Anweisung
bindet sozusagen den kompletten
Namensraum std ins Programm ein,
d.h. auf alles, was im Namensraum
std enthalten ist, können Sie dann
ohne Angabe von std:: zugreifen.
// Datei iostream einbinden
#include <iostream>
// using Directive
using namespace std;
int main ( )
{
cout [<< ausgabe1...];
}
Die using-Directive sollten Sie in 'realen' Programmen nur dann einsetzen, wenn Sie sich über
deren Auswirkung voll im Klaren sind. Ansonsten kann es unliebsamen Überraschungen durch
Doppeldeutigkeit von Symbolen kommen. In den kleinen Beispielen im Kurstext wird diese
Anweisung einfach aus Bequemlichkeitsgründen verwendet (oder ganz weggelassen). In den
Beispielen im Kurs wird die using-Directive nicht eingesetzt. Vermeiden Sie auch in Ihren
Übungen die using-Directive..
ACHTUNG! Die mittels #include einzubindende Datei iostream besitzt keine Extension .h, wie
manchmal in anderen (leider auch neueren) Quellen noch angegeben.
Ausgabe von Text und Zeichen
Sollen fixe Texte (bestehend aus
mehreren Zeichen, so genannte CStrings) ausgegeben werden, so
folgt nach dem Streamobjekt cout,
wie bereits erwähnt, zunächst der
Operator << . Im Anschluss an
diesen Operator folgt dann der
auszugebende Text, der in
Anführungszeichen einzuschließen
ist. Mehrere Texte können durch
entsprechende Wiederholungen
ausgegeben werden. Am Ende einer
cout-Anweisung wird kein
automatischer Zeilenvorschub
eingefügt. Wollen Sie die einzelnen
Textteile auf der Ausgabe in einer
neuen Zeile beginnen lassen, so
müssen Sie bestimmte EscapeSequenzen in den Text einfügen.
Aber dazu kommen wir gleich
noch. Anstatt einer bestimmten
Escape-Sequenze können Sie auch
den Manipulator endl in den
// IO-Stream Datei einbinden
#include <iostream>
// cout und endl aus std Namensraum
using std::cout;
using std::endl;
// Das Programm
int main ( )
{
// Ausgabe eines einzelnen Textes
// mit Zeilenvorschub
cout << "Aber Hallo!" << endl;
// Ausgabe mehrere Texte
cout << "Mein " << " erstes"
<< " Programm";
// Einzelne Zeichen ausgeben
cout << '!' << '?';
// und fertig!
}
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (3 von 14) [17.05.2005 11:37:56]
Streamausgabe
Ausgabestream einfügen um einen
Zeilenumbruch zu erhalten (siehe
erste Ausgabe rechts). Auch der
Manipulator endl liegt im
Namensraum std.
Die Programmausgabe:
Aber Hallo!
Mein erstes Programm!?
Beachten Sie im Beispiel rechts,
dass die zweite cout-Anweisung
über 2 Zeilen geht. Sie wissen ja,
erst das Semikolon schließt eine
Anweisung ab!
PS: Was es mit den Anführungszeichen, die den Text einschließen, genau auf sich hat, das erfahren Sie
in der Lektion über Konstanten noch genauer.
Außer C-Strings können Sie auch einzelne Zeichen ausgeben. Dazu ist das auszugebende Zeichen in
Hochkomma einzuschließen. Selbstverständlich könnten Sie auch ein einzelnes Zeichen in
Anführungszeichen einschließen, also z.B. anstatt 'A' auch "A" schreiben. Dies würde aber (unter
anderem) mehr Programmlaufzeit benötigen als die direkte Ausgabe des Zeichens.
Escape-Sequenzen
Sehen wir als Nächstes an, wie nicht-druckbare Zeichen (z.B. ein Zeilenvorschub oder auch ein
Tabulator) ausgegeben werden. Hierfür stehen verschiedene so genannte Escape-Sequenzen zur
Verfügung. Diese Escape-Sequenzen können an beliebiger Stelle in einem Text (eingeschlossen in
Anführungszeichen) oder aber als einzelnes Zeichen (eingeschlossen in Hochkomma) in einer coutAnweisung stehen. Sie beginnen immer mit dem Backslash-Zeichen \ und nach dem Backslash folgt ein
ASCII-Zeichen das die auszuführende 'Operation' beschreibt.
Die nachfolgende Tabelle enthält alle verfügbaren Escape-Sequenzen. Ein Programmbeispiel
einschließlich der dazugehörigen Ausgabe können Sie sich ansehen, wenn Sie das Symbol
entsprechenden Tabellenzeile anklicken.
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (4 von 14) [17.05.2005 11:37:56]
in der
Streamausgabe
Die Bedeutung und Wirkungsweise der meisten Escape-Sequenzen dürfte aus der Beschreibung und der
dazugehörigen Ausgabe ersichtlich werden. Beachten Sie bei den Escape-Sequenzen \" und \' im
Programmbeispiel wann diese benötigt werden. Innerhalb eines fixen Textes muss das Zeichen " als
Escape-Sequenz definiert werden da es standardmäßig den auszugebenden Text begrenzt. Das Zeichen '
benötigt hier keine Escape-Sequenz. Genau andersherum verhält es sich, wenn Sie ein einzelnes Zeichen
ausgeben, da einzelne Zeichen in ' eingeschlossen werden.
Vielleicht verwirren mag am Anfang die Escape-Sequenz \?, da Sie ja ein Fragezeichen auch
direkt innerhalb eines Textes oder als einzelnes Zeichen ausgeben können. Aber sehen Sie sich
einmal das Programm und die Ausgabe zu dieser Escape-Sequenz in der obigen Tabelle an.
Was hier vonstatten geht hängt mit den so genannten Trigraphen zusammen. Diese besitzen in
der täglichen Praxis keine Bedeutung mehr. Falls Sie aber über die Trigraphen mehr erfahren
wollen, so klicken Sie das Symbol links an.
Ausgabe von Daten (Zahlen)
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (5 von 14) [17.05.2005 11:37:56]
Streamausgabe
Um numerische Daten auszugeben, // IO-Stream Datei einbinden
werden diese, genauso wie die C#include <iostream>
Strings, einfach mit dem Operator // cout aus std Namensraum
<< an das Ausgabestream-Objekt
using std::cout;
cout übergeben. Dabei spielt der
Datentyp (wird in der nächsten
// Das Programm
Lektion erklärt) des auszugebenden int main ( )
Datums keine Rolle, d.h. Sie
{
können auf diese Art sowohl
// Auszugebende Daten
Ganzzahlen wie auch
int anyVal = 10;
Gleitkommazahlen ausgeben.
char byteVal = 66;
Ebenso können innerhalb einer coutfloat floatVal = 1.23f;
Anweisungen Texte und Daten
// Gemischte Ausgabe von Text und Daten
beliebig gemischt werden. Beachten
cout << "int-Datum: " << anyVal <<
Sie im Beispiel, dass die cout" float-Datum: " << floatVal <<
Anweisungen teilweise über
'\n';
mehrere Zeilen gehen!
// Ausgabe von char-Daten
cout << "char-Datum: " << byteVal << '\n';
Bei der Ausgabe eines Datums
cout << "char-Datum als Wert: " <<
werden so viele Stellen verwendet,
static_cast<int>(byteVal);
wie das Datum benötigt.
// und fertig!
Ganzzahlen werden standardmäßig
}
als Dezimalzahlen ausgegeben. Bei
der Ausgabe von
Programmausgabe:
Gleitkommazahlen werden diese je
nach Wert mit oder ohne
int-Datum: 10 float-Datum: 1.23
Exponenten dargestellt.
char-Datum: B
char-Datum als Wert: 66
Wie Sie die Ausgabe von
numerischen Daten steuern
können, das erfahren Sie gleich
noch.
Stören Sie sich bitte im Augenblick
nicht an den ersten drei
Anweisungen in der Funktion
main(...). Hier werden 3 Variablen
für die Ausgabe definiert und
initialisiert. Mehr zur Definition
von Variablen erfahren Sie gleich
in der nächsten Lektion. Anstelle
der Variablen hätten wir auch
direkt die Zahlenwerte, also die
Zahlen 10, 66 und 1.23, in der coutAnweisung ausgeben können.
Aber Achtung!
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (6 von 14) [17.05.2005 11:37:56]
Streamausgabe
Werden char, signed char oder unsigned char Daten ausgegeben, so erfolgt die Ausgabe
standardmäßig als ASCII-Zeichen, d.h. der Hex-Wert 0x41 wird als Buchstabe 'A'
ausgegeben. Wollen Sie char-Daten als numerischen Wert ausgeben, so müssen Sie vor dem
Datum eine Typkonvertierung z.B. in den Datentyp int angeben.
char var = 'A';
cout << var;
// Gibt A aus
cout << static_cast<int>(var); // Gibt 65 (=0x41) aus
Mehr zur Typkonvertierung später noch im Kurs.
Wenn Sie neugierig sind, können Sie sich nun auch einmal die Tabelle der ASCII-Code
ansehen.
Dezimale, hexadezimale und oktale Ausgabe
Aber damit sind die
Ausgabemöglichkeiten von cout
natürlich noch nicht ausgeschöpft.
In manchen Fällen kann es
durchaus sinnvoll sein, Ganzzahlen
in einem anderen Zahlensystem als
dem standardmäßigen
Dezimalsystem darzustellen. Um
Ganzzahlen als hexadezimal oder
oktal Zahl auszugeben, stellt C++
so genannte Manipulatoren zur
Verfügung, die einfach in den
Ausgabestream eingefügt werden.
Folgende Manipulatoren stellen die
Ausgabe von Ganzzahlen auf das
hexadezimale, oktale oder dezimale
Zahlensystem um:
// IO-Stream Datei einbinden
#include <iostream>
// cout aus std Namensraum
using std::cout;
// Das Programm
int main ( )
{
// Auszugebende Daten
int var = 10;
// Zahlenbasis mit ausgeben
cout << std::showbase;
cout << " Dez: " << var;
cout << " Hex: 0x" << std::hex << var;
cout << " Okt: " << std::oct << var;
cout << " Zahl: " << var;
// und fertig!
}
Programmausgabe:
Manipulator Ausgabebasis
std::dec
dezimal
(Basis 10)
std::hex
hexadezimal
(Basis 16)
std::oct
oktal (Basis
8)
Dez: 10 Hex: 0xa Okt: 012 Zahl: 012
Die Manipulatoren können
alternativ auch mit der usinghttp://www.cpp-tutor.de/cpp/le01/le01_04.htm (7 von 14) [17.05.2005 11:37:56]
Streamausgabe
Anweisung using std::xxx aus den
std-Namensraum eingebunden
werden. Sie brauchen dann z.B. nur
noch hex im Programm angeben.
Wenn Sie das hexadezimal und oktal Zahlensystem noch nicht kennen, dann können Sie jetzt
links das Symbol anklicken um eine kleine Einführung in die Zahlensysteme zu erhalten.
Eine einmal eingestellte Zahlenbasis für die Ausgabe bleibt solange aktiv, bis sie explizit umgestellt
wird.
Bei der Ausgabe sollten Sie allerdings beachten, dass standardmäßig keinerlei Kennung für die aktuelle
Zahlenbasis (wie z.B. 0x für Hex-Zahlen.) der Zahl automatisch vorangestellt wird. Die Darstellung der
Zahl 10 kann damit sowohl 10 (Ausgabe in dezimal), 16 (Ausgabe in hexadezimal) oder 8 (Ausgabe in
oktal) bedeuten. Damit bei der Ausgabe die Zahlenbasis ersichtlich wird, sollten Sie noch den
Manipulator showbase (ebenfalls aus dem std-Namensraum) mit in den Ausgabestream einfügen (siehe
Beispiel oben). Die ausgegebenen Werte erhalten dann folgende Kennungen:
Zahlensystem
Kennung
dezimal
keine
hexadezimal
0x oder 0X
oktal
0
Lesen Sie in der Ausgabe z.B: 0x10, so bedeutet dies eine hexadezimal 10 (gleich 16 dezimal). Besonders
beachten Sie müssen oktale Ausgaben! Die Ausgabe 0100 bedeutet eine oktale 100, und entspricht damit
64 dezimal.
Manipulatoren
Außer den vorhin vorgestellten Manipulatoren dec, hex und oct stehen unter anderem noch die
Manipulatoren setbase(...),. setw(...), setfill(...), setprecision(...), fixed und scientific zur Verfügung. Wenn
Sie einen dieser Manipulatoren einsetzen, müssen Sie die Datei iomanip mittels #include zusätzlich
einbinden.
setbase()
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (8 von 14) [17.05.2005 11:37:56]
Streamausgabe
Beginnen wir mit dem Manipulator
setbase(n). Er bietet prinzipiell
nichts Neues. setbase(n) dient,
genauso wie dec, hex und oct, zur
Einstellung des Zahlensystems für
die Ausgabe. Der Parameter n gibt
das entsprechende Zahlensystem an
und kann die Werte 8, 10 und 16
für das Oktal-, Dezimal- und
Hexadezimal-System annehmen.
Alle anderen Werte stellen die
Ausgabe wieder auf das
Dezimalsystem zurück.
// IO-Stream und Manipulatoren einbinden
#include <iostream>
#include <iomanip>
Beachten Sie auch, dass Sie für
setbase(...) ebenfalls eine usingAnweisung benötigen, außer Sie
schreiben statt setbase(...) lieber
std::setbase(...). Es stehen immer
beide Möglichkeiten offen.
a->12->10
using std::cout;
using std::setbase;
// Das Programm
int main ( )
{
// Auszugebendes Datum
int var = 10;
// Ausgabe als Hex-Zahl
cout << setbase(16) << var << "->";
// Ausgabe als Oktal-Zahl
cout << setbase(8) << var << "->";
Eine einmal eingestellte
// Ausgabe wieder als Dezimal-Zahl
Zahlenbasis bleibt solange aktiv, bis
cout << setbase(10) << var;
sie explizit umgestellt wird.
Außerdem wirkt setbase(n) nur auf }
Ganzzahlen.
Programmausgabe:
setw()
Mit dem Manipulator setw(n) kann
die minimale Breite des nächsten
Ausgabefeldes eingestellt werden..
Dieser Manipulator erhält als
Parameter n die Anzahl der
minimalen Ausgabestellen. Benötigt
die Ausgabe mehr Stellen als
angegeben wurden, so wird das
Ausgabefeld entsprechend
vergrößert, d.h. die Zahl wird
immer vollständig dargestellt. Die
Ausgabe erfolgt standardmäßig
rechtsbündig. Wie Sie eine
linksbündige Ausgabe erhalten, das
erfahren wenn Sie am Ende des
Abschnitts das Symbol
bei
// IO-Stream Datei und
// Manipulatoren einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::setw;
// Das Programm
int main ( )
{
// Auszugebendes Datum
int var = 10;
// Datum ohne min. Feldbreite ausgeben
cout << ":" << var << ":";
// Min. Feldbreite für ':' auf 4 setzen
cout << setw(4) << ":" << var << ":";
'setf() Memberfunktion' anklicken.
// Min. Feldbreite für var auf 4 setzen
cout << ":" << setw(4) << var << ":";
Und hierbei ist ganz wichtig:
// Ausgabe wieder ohne min. Feldbreite
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (9 von 14) [17.05.2005 11:37:56]
Streamausgabe
cout << ":" << var << ":";
Die Angabe von setw(n)
gilt nur für die
unmittelbar nachfolgende
Ausgabe (siehe Beispiel)!
}
Programmausgabe:
:10:~~~:10::~~10::10:
Sehen Sie sich dazu die zweite cout- Hinweis: Das Zeichen ~ steht hier für ein
Leerzeichen.
Anweisung einmal an. Hier wird
die Breite des Ausgabefeldes auf 4
Zeichen gesetzt und anschließend
der Doppelpunkt ausgegeben.
Damit wirkt sich die eingestellte
Feldbreite auch nur auf die
Ausgabe des Doppelpunktes aus.
Außerdem wirkt setw(n) wirkt alle
Datentypen.
setfill()
Ist die Breite des Ausgabefeldes
größer als tatsächlich Stellen
benötigt werden, so können nicht
belegte Stellen mit einem beliebigen
Zeichen ausgefüllt werden. Die
Festlegung des Füllzeichens erfolgt
mit dem Manipulator setfill(n). Er
erhält als Parameter n das zu
setzende Füllzeichen. Beachten Sie
bitte, dass Sie hier ein Zeichen
übergeben müssen das in einfache
Hochkomma eingeschlossen wird.
Wie Sie vielleicht schon erraten
haben, ist das Standard-Füllzeichen
das Leerzeichen. Ein einmal
eingestelltes Füllzeichen bleibt
solange aktiv, bis es wieder
umgesetzt wird. setfill(n) wirkt
ebenfalls auf alle Datentypen.
Im Beispiel rechts wurde der
setfill(...) Manipulator einmal mit
der vollen Qualifikation
std::setfill(...) verwendet.
// IO-Stream Datei und
// Manipulatoren einbinden
#include <iostream>
#include <iomanip>
using std::cout;
// Das Programm
int main ( )
{
// Auszugebendes Datum
int var = 10;
// Ausgaben mit Füllzeichen '#'
cout << ":" << std::setw(4) <<
std::setfill ('#') <<
var << ":";
cout << ":" << std::setw(3) <<
var << ":";
// Füllzeichen wieder zurückstellen
cout << ":" << std::setw(4) <<
std::setfill(' ') <<
var << ":";
}
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (10 von 14) [17.05.2005 11:37:56]
Streamausgabe
Programmausgabe:
:##10::#10::~~10:
Hinweis: Das Zeichen ~ steht hier für ein
Leerzeichen.
setprecision() und fixed/scientific
Für die Ausgabe von
Gleitkommazahlen können Sie über
den Manipulator setprecision(...) die
Anzahl der auszugebenden Stellen
(ohne einen eventl. Exponenten und
Vorzeichen) einstellen.
Überschreitet die Anzahl der
Vorkommastellen die mit
setprecision(...) eingestellte
Stellenanzahl, so wird automatisch
auf Exponentialdarstellung
umgestellt (erste Ausgabe rechts).
Wird über den Manipulator fixed
oder scientific (siehe nachfolgend)
die Darstellung mit oder ohne
Exponenten erzwungen, so legt
setprecision(...) die Anzahl der
Nachkommastellen fest.
Und auch die einmal eingestellte
Anzahl der auszugebenden Stellen
bleibt solange gültig, bis sie erneut
umgesetzt wird. Standardmäßig
beträgt die Genauigkeit für die
Ausgabe 6 Stellen.
// IO-Stream Datei und
// Manipulatoren einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::setprecision;
using std::fixed;
// Das Programm
int main ( )
{
// Auszugebende Daten
double var1 = 40000.0/3.0;
double var2 = 4.0/3.0;
// Ausgabe auf 4 Stellen begrenzen
cout << setprecision(4);
// Normale Ausgabe
cout << var1 << ' ' << var2 << "->";
// Ausgabe immer ohne Exponenten
cout << fixed;
cout << var1 << ' ' << var2;
}
Programmausgabe:
1.333e+004~1.333->13333.3333~1.3333
Um die Ausgabe einer
Gleitkommazahl im
Hinweis: Das Zeichen ~ steht hier für ein
Festkommaformat zu erzwingen,
Leerzeichen.
wird der Manipulator std::fixed
verwendet. Er wird genauso wie
z.B. der Manipulator hex einfach in
den Ausgabestream eingefügt. Alle
Gleitkommazahlen werden danach
solange im Festkommaformat
ausgegeben, bis die Ausgabe durch
den Manipulator std::scientific auf
Exponentialdarstellung umgestellt
wird.
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (11 von 14) [17.05.2005 11:37:56]
Streamausgabe
setf(...) Memberfunktion: wie Sie noch mehr Kontrolle über die Ausgabe erhalten können, das
erfahren Sie, wenn Sie links das Symbol anklicken.
cout-Buffer: ebenfalls als Zusatz-Thema zum Kurs, können Sie sich einmal ansehen, wie der
Ausgabestream cout intern arbeitet.
cerr und clog: außer dem Ausgabe-Stream cout gibt noch zwei weitere Streams für die
Ausgabe, die Streams cerr und clog.
Ansonsten folgt jetzt das erste 'größere' Beispiel im Kurs und auch Ihre erste Übung.
Beispiel und Übung
Das Beispiel:
Das Programm gibt zunächst die unten dargestellte Anschrift aus.
Anschließend werden zwei Zeilen mit je drei Strings in Tabellenform dargestellt. Als 'Spaltentrenner'
wird ein Tabulator verwendet.
Zum Schluss erfolgt die Ausgabe eines Alarmtons (Bing!).
Das unten stehende Beispiel finden Sie auch unter le01\prog\bcout.
Die Programmausgabe:
Karl Mueller
Sackgasse 23
12345 Astadt
============
Eins Zwei Drei
Vier Fuenf Sechs
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (12 von 14) [17.05.2005 11:37:56]
Streamausgabe
Das Programm:
// C++ Kurs
// Beispiel zu cout
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
// std Namensbereich benutzen fuer
using std::cout;
using std::endl;
// Hauptprogramm
int main ()
{
// Anschrift ausgeben
cout << "Karl Mueller\nSackgasse 23\n";
cout << "12345 Astadt" << endl;
cout << "============\n\n";
cout << "Eins\tZwei\tDrei\n";
cout << "Vier\tFuenf\tSechs\n";
cout << "\a";
// und fertig
}
So, und hier Ihre Übung!
Die Übung:
Verwenden Sie in dieser Übung keine using-Anweisung und auch keine using-Directive!
Definieren und initialisieren Sie das Datum dblVar wie folgt:
double dblVal = 100.0/3.0;
Geben Sie das Datum dblVar dann mit 10 Stellen aus, wobei die nicht belegten Stellen mit dem
Zeichen '*' aufzufüllen sind.
Definieren Sie ein zweites Datum number:
int number = 10;
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (13 von 14) [17.05.2005 11:37:56]
Streamausgabe
Geben Sie das Datum number als Hexadezimal-, Oktal- und Dezimalzahl aus. Bei der Ausgabe soll die
Kennung für die aktuelle Zahlenbasis mit ausgegeben werden.
Die Programmausgabe:
100/3 = ***33.3333
10 im 16er, 8er und 10er System:
Hex: 0xa, Okt: 012, Dez: 10
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le01\Lcout.
Sie haben nun das Ende der ersten Lerneinheit erreicht. Durch anklicken des rechten Pfeils gelangen
Sie zur 2. Lerneinheit.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04.htm (14 von 14) [17.05.2005 11:37:56]
Kommentare
Kommentare
Auskommentieren von Anweisungen
Mithilfe dieses Kommentars ist
aber ebenso möglich, mehrere
Anweisungen z.B. für Testzwecke
außer Kraft zu setzen. Dazu wird
einfach vor den entsprechenden
Anweisungen der Kommentar
geöffnet und am Ende der
Anweisungen der Kommentar
wieder geschlossen. Wenn jetzt
wieder die Buttons ein bzw. aus
anklicken, so werden rechts die
ersten beiden Anweisungen in einen
Kommentar umgewandelt.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_03_d1.htm [17.05.2005 11:37:57]
Variablen
Variablen
Die Themen:
Definition einer Variablen
Ganzzahl-Datentypen
Gleitkomma-Datentypen
Zeichen-Datentypen
Sonstige Datentyp
Sonstiges zu Variablendefinitionen
Kenndaten von Datentypen
sizeof() Operator
Die Übung
Definition einer Variablen
In dieser Lektion werden wir uns ansehen, wie Daten innerhalb eines Programms abgelegt werden.
Veränderbare Daten werden in so genannten Variablen abgelegt. Sie können einer Variable beliebig
oft Werte zuweisen (siehe dazu übernächste Lektion Zuweisungen), der alte Wert wird dann einfach
überschrieben.
Bevor ein Wert in einer Variable
abgelegt werden kann, muss die
Variable vorher definiert werden.
C++ unterstützt keine
automatischen
Variablendefinitionen wie z.B.
BASIC. Eine Variablendefinition
hat folgenden vereinfachten
allgemeinen Aufbau:
DATENTYP varName;
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (1 von 10) [17.05.2005 11:37:59]
Variablen
An erster Stelle steht der Datentyp
DATENTYP der Variable und
danach folgt der Variablenname
varName. Da eine
Variablendefinition auch eine
ausführbare Anweisung ist, muss
sie mit einem Semikolon
abgeschlossen werden.
Der Datentyp bestimmt den
Wertebereich der in der Variable
abzulegende Wert annehmen kann
und damit auch indirekt den
Speicherplatz, den eine Variable
belegt. Je mehr Speicher die
Variable belegt, umso größer kann
der in ihr abzulegende Wert sein.
Mehr zu den Wertebereichen gleich
noch.
Nach dem Datentyp folgt der Name der Variable. Über diesen Namen können Sie dann der Variable
Werte zuweisen oder auch den aktuellen Wert auslesen. Der Name muss eindeutig sein, d.h. es darf
keine zweite Variable (oder Funktion und Ähnliches) mit gleichem Namen geben. Obwohl die
Schreibweise von Variablennamen beliebig ist, hat es sich inzwischen eingebürgert, Variablennamen
immer mit einem Kleinbuchstaben beginnen zu lassen und an den Wortgrenzen Grossbuchstaben zu
verwenden.
Beispiel:
int numberOfRanges;
double sinusValue;
Beachten Sie bitte, dass C++ streng zwischen Groß- und Kleinschreibung unterscheidet. So
ist die Variable max nicht identisch mit der Variable Max. Allerdings sollten Sie solche
Konstruktionen alleine der guten Lesbarkeit wegen vermeiden!
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (2 von 10) [17.05.2005 11:37:59]
Variablen
Für den Augenblick spielt es
noch keine Rolle, ob Sie die
Definition einer Variable vor
main(...) vornehmen oder aber
innerhalb von main(...). Worin
sich die Definition einer Variable
vor main(...) und innerhalb von
main(...) unterscheidet, werden
wir uns später noch genauer
ansehen. Sie können Variablen
an beliebiger Stelle definieren.
Dies hat den Vorteil, dass Sie
eine Variable genau an der Stelle
definieren können, an der Sie sie
zum ersten Mal auch benötigen.
// Variable ausserhalb von main() definieren
int numberOfRanges;
// main() Funktion
int main()
{
// Variable innerhalb von main() definieren
int sinusValue;
...
// weitere Variable definieren
double result;
...
}
Für die Benennung von Variablen (und den nachher gleich eingeführten Konstanten)
verwenden manchen Programmierer die so genannte Ungarische Notation. Was dies ist
erfahren Sie, wenn Sie links das Symbol anklicken.
Ganzzahl-Datentypen
C++ kennt folgende Ganzzahl-Datentypen, auch als Integer-Datentypen bezeichnet:
signed char
short int
int
long int
unsigned char
unsigned short
int
unsigned int
unsigned long int
Variablen der Datentypen signed char, short int, int und long int können sowohl positive wie auch
negative Zahlen aufnehmen, während die Variablen der Datentypen unsigned xxx nur positive Zahlen
(einschließlich der 0) aufnehmen können.
Bei den short und long Datentypen kann die Angabe von int auch entfallen, d.h. anstatt short int kann
zum Beispiel auch nur short geschrieben werden oder anstatt unsigned long int nur unsigned long.
Wenn Sie wollen, können Sie bei den Datentypen short int, int und long int noch explizit das
Schlüsselwort signed voranstellen, also zum Beispiel signed short int (das ist nun aber wirklich die
Langfassung). In der Praxis haben sich die Schreibweisen short, long, unsigned short und unsigned
long eingebürgert. Programmierer schreiben nun einmal nicht gerne mehr als unbedingt notwendig
ist.
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (3 von 10) [17.05.2005 11:37:59]
Variablen
Bei den char-Datentypen ist signed bzw. unsigned zwingend vorgeschrieben. Es gibt auch noch
den Datentyp char, der im übernächsten Abschnitt behandelt wird. Dieser char Datentyp ist
aber aus Sicht des Compilers nicht identisch mit dem signed char Datentyp.
Der Unterschied zwischen den Datentypen liegt unter anderem in der Größe des belegten Speichers
pro Datum (Anzahl Bytes) und damit auch im Wertebereich, den eine Variable des entsprechenden
Datentyps annehmen kann. Die Sprache C++ schreibt den Wertebereich der Datentypen nicht vor, es
muss jedoch immer folgender Zusammenhang gelten:
char
=
1 Byte
signed char <= short
short
<= int
int
<= long
1 Byte ist hierbei die Speichergröße die benötigt wird um ein Zeichen abzulegen. Dies sind in der
Regel 8 Bit. Es mag aber durchaus einmal Implementationen geben, bei den 1 Byte aus 4 Bit oder gar
16 Bit besteht. Der C++ Standard sagt darüber ebenfalls nichts aus. Jedoch ist dem Autor i.A. kein
gängiger Compiler bekannt, bei dem 1 Byte nicht 8 Bit belegt.
Damit können Sie nun, wie
rechts angegeben, IntegerVariablen definieren. Welchen
Wertebereich die einzelnen
Variablen annehmen können,
hängt aber letztendlich vom
verwendeten Compiler und der
verwendeten Rechner-Plattform
ab. In der nachfolgenden
Tabelle sind die jeweiligen
Wertebereiche widergegeben,
wie Sie auf einem PC und beim
Einsatz der VC++ Compilers
bzw. MinGW/BORLAND C++
typisch sind.
// short Variable definieren
short myValue;
// Variable mit großem Wertebereich definieren
signed long counter;
// Variable mit kleinem Wertebereich
// zur Aufnahme von nur positiven Zahlen
unsigned char index;
Datentyp
typische Wertebereiche
signed char
-128...127
short
-32768...32767
long
-2147483648...2147483647
unsigned char
0...255
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (4 von 10) [17.05.2005 11:37:59]
Variablen
unsigned short 0...65535
unsigned long
0...4294967295
Mehr zu den auf Ihrem System gültigen Wertebereichen und sonstigen 'Kenndaten' der
verschiedenen Datentypen können Sie sich gleich noch ansehen.
Wenn Sie wollen, können Sie sich durch anklicken des Symbols links auch einmal ansehen,
wie negative Zahlen im 2er Komplement dargestellt werden.
Außerdem können Sie sich noch ansehen, wie auf einem System mit INTEL-kompatiblen
Prozessor (zum Beispiel ein PC) die Daten letztendlich im Speicher zu liegen kommen.
Gleitkomma-Datentypen
Für die Darstellung von Gleitkommazahlen stehen 3 verschiedene Datentypen zur Verfügung: float,
double und long double. Diese Datentypen unterscheiden sich zum einen natürlich im darstellbaren
Wertebereich und zum anderen, was fast noch wichtiger für Berechnungen ist, durch die Anzahl der
Stellen, mit den der Datentyp arbeitet.
Der 'kleinste' Gleitkomma-Datentyp ist der Datentyp float, gefolgt vom Datentyp double. Und am
genauesten rechnet der Datentyp long double.
Die nachfolgende Tabelle soll einmal demonstrieren, wie genau die Gleitkomma-Datentypen bei
verschiedenen Compilern arbeiten. Ausgangspunkt ist hierbei die Division 1.0/3.0, die als long double
Operation durchgeführt wurde. Das Ergebnis der Division wurde dann Variablen des
entsprechenden Datentyps zugewiesen. Die Tabelle enthält nur die Stellen, die mit dem tatsächliche
Ergebnis übereinstimmen. Eventuell weitere, vom Soll abweichende Stellen, wurden abgeschnitten.
Compiler/Datentyp float
double
long double
GNU 2.95
3.333333e- 3.333333333333333e3.333333333333333e-01
01
01
MinGW 3.1
3.333333e- 3.333333333333333e- 3.333333333333333333e01
01
01
VC++ 6.0
3.333333e- 3.333333333333333e3.333333333333333e-01
01
01
BCC 5.5.1
3.333333e- 3.333333333333333e- 3.33333333333333333e01
01
01
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (5 von 10) [17.05.2005 11:37:59]
Variablen
Zeichen-Datentypen
Zum Abspeichern von Zeichen
werden die Datentypen char und
wchar_t verwendet. Der
Datentyp char dient zur
Aufnahme eines Zeichens das 1
Byte belegt während der
Datentyp wchar_t ein Zeichen
eines erweiterten Zeichensatzes
aufnehmen kann. In der Regel
werden in wchar_t Zeichen
abgelegt die nicht in einem Byte
Platz haben. Denken Sie hierbei
z.B. an den chinesischen
Zeichensatz. Sowohl unter
MinGW wie auch unter VC++
belegt dieser Datentyp 2 Bytes.
Dieses ist aber lt. ANSI C++
nicht genormt.
// char Variable definieren
char anyCharacter;
// Variable für den erweiterten Zeichensatz
// definieren
wchar_t unicodeChar;
Der Datentyp char ist ein eigenständiger Datentyp und entspricht nicht dem Datentyp
signed char! Dieser Sachverhalt spielt später beim Überladen von
Funktionen/Memberfunktionen noch wichtige Rolle.
Sonstige Datentypen
Und gleich haben wir die Datentypen (wenigstens vorläufig) geschafft. Was uns jetzt noch fehlt sind
die Datentypen bool und void.
Der Datentyp bool kann nur die
beiden Zustände (Werte) true
und false annehmen. Er wird
immer dann eingesetzt, wenn
irgendwo im Programm ein
logischer Zustand 'berechnet'
werden muss.
// Eine bool Variable definieren
bool allDone;
Der Datentyp void ist ein unvollständiger Datentyp der niemals alleine auftreten kann. Er wird
hauptsächlich als Returntyp von Funktionen/Memberfunktionen oder im Zusammenspiel mit
Zeigern verwendet. Auf ihn werden wir später noch zurückkommen.
Es soll aber an dieser Stelle noch darauf hingewiesen werden, dass es noch weitere Datentypen gibt,
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (6 von 10) [17.05.2005 11:37:59]
Variablen
die erst späterer im Kurs behandelt werden. Dies sind im Einzelnen:
●
●
●
●
enumerated Daten
Zeiger
Strukturen / Klassen
Varianten
Sonstiges zu Variablendefinitionen
Variablen lassen sich bei ihrer
Definition auch gleich mit einem
bestimmten Wert initialisieren
(vorbelegen). Dies erfolgt in der
Art, dass nach der Definition
der Variable der
Zuweisungsoperator = folgt und
danach der Initialwert. Einer
auf diese Weise initialisierte
Variable kann dann aber im
Verlaufe des Programms
trotzdem noch ein anderer Wert
zugewiesen werden.
Selbstverständlich muss der
Datentyp des Initialwertes auch
mit dem Datentyp der Variable
übereinstimmen. Mehr zum
Datentyp des Initialwert gleich
bei der Behandlung von
Konstanten.
// int-Variable initialisieren
int startValue = 10;
// double-Variable initialisieren
double oneThird = 1.0/3.0;
// bool-Variable initialisieren
bool isDone = false;
Außerdem können Sie, um sich
Schreibarbeit zu sparen,
mehrere Variablen des gleichen
Datentyps innerhalb einer
Anweisung definieren. Die
Namen der zu definierenden
Variablen werden dann durch
Komma voneinander getrennt.
// zwei int Variablen definieren
int startValue, endValue;
// zwei double-Variable definieren
// und auch gleich initialisieren
double beginOfRange = -2.5, endOfRange = 2.5;
Und auch das ist noch möglich: Definition und Initialisierung von mehreren Variablen (des selben
Datentyps) innerhalb einer Anweisung. Dieser Fall ist in der letzten Anweisung oben dargestellt.
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (7 von 10) [17.05.2005 11:37:59]
Variablen
Kenndaten von Datentypen
Im Verzeichnis le02\prog\numlim befindet sich das Programm numlim.cpp, das einige Kenndaten für
die vorgestellten Datentypen ermittelt. Zur Ermittlung der Kenndaten brauchen Sie das Programm
(noch) nicht zu verstehen, da das Programm schon ziemlich 'ins Eingemachte' geht (Stichwort:
Templates). Geben Sie beim Aufruf der Funktion PrintNumLimits<DTYP>() lediglich in den spitzen
Klammern den Datentyp an, für den Sie die Kenndaten ermitteln wollen und übersetzen Sie das
Programm dann. Eventuelle Warnungen können Sie an dieser Stelle (ausnahmsweise) einmal
ignorieren.
Beispiel:
PrintNumLimits<unsigned long>();
PrintNumLimits<double>();
// Kenndaten für unsigned long
ermitteln
// Kenndaten für double ermitteln
Nachfolgend die Ausgaben für die beiden oben angegeben Datentypen, ermittelt mit dem MinGW
Compiler:
unsigned long Minimum: 0
Maximum: 4294967295
Anzahl Bits (ohne Vorzeichen): 32
Vorzeichenbehaftet: false
Ganzzahl (Integer): true
keine Rundungsfehler moeglich: true
kleinster von 1 abweichender Wert: 0
double
Minimum: 2.22507e-308
Maximum: 1.79769e+308
Anzahl Bits (ohne Vorzeichen): 53
Vorzeichenbehaftet: true
Ganzzahl (Integer): false
keine Rundungsfehler moeglich: false
kleinster von 1 abweichender Wert: 2.22045e-16
Wie Sie aus der Aufstellung u.a. entnehmen können, gibt es beim unsigned long Datentyp keine
Rundungsfehler (wie übrigens bei allen Integer-Datentypen) während es beim double-Datentyp (wie
bei allen Gleitkomma-Datentypen) Rundungsfehler geben kann. Dies liegt darin begründet, dass
wegen der internen Darstellung von Gleitkommazahlen nicht jede beliebige Zahl exakt darstellbar
ist.
Die im Beispiel verwendete Klasse numeric_limits wird später im Kurs nochmals ausführlicher
behandelt.
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (8 von 10) [17.05.2005 11:37:59]
Variablen
sizeof() Operator
Sie können den von einem
Datentyp belegten Speicherplatz
(in Anzahl Bytes) jederzeit
mittels des Operators
sizeof(DTYP) ermitteln. DTYP
gibt den Datentyp an, dessen
Speicherbedarf in Bytes
ermittelt werden soll. Wollen Sie
den von einer Variablen
benötigten Speicherplatz
ermitteln, so geben Sie beim
sizeof(...) Operator anstelle eines
Datentyps den Namen der
Variablen an (2. Aufruf von
sizeof(...) rechts). Beachten Sie,
dass sizeof(...) ein Operator ist
(wie z.B. +) und keine Funktion
(wie z.B. sin(...) für die
Sinusberechnung). Zudem ist
sizeof(...) ein CompilezeitAusdruck, d.h. der Compiler
wird beim Übersetzen des
Programms schon die Größe
berechnen. Daraus folgt, dass
sizeof(...) selbst keine
Programmlaufzeit benötigt!
short anyValue; // short-Variable def.
...
cout << sizeof(char) << endl;
cout << sizeof(anyValue) << endl;
cout << sizeof(long) << endl;
Beispielhafte Programmausgabe:
1
2
4
Die oben im Beispiel dargestellte Programmausgabe bezieht sich auf eine PC-Umgebung und einen
32-Bit Compiler.
Die Übung
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (9 von 10) [17.05.2005 11:37:59]
Variablen
Die Übung:
Schreiben Sie ein Programm, das den kleinsten und größten Wert einer unsigned short und einer
long double Zahl ausgibt. Verwenden Sie hierzu als Vorlage das Beispielprogramm
le02\prog\numlim.cpp.
Anschließend geben Sie die von einer short und float Variable belegte Anzahl von Bytes aus.
HINWEIS: Die untenstehende Ausgabe gilt für den MinGW. Andere Compiler können durchaus
auch abweichende Werte ausgeben.
Die Programmausgabe:
Min/Max unsigned short: 0/65535
Min/Max long double : 0/-1.#QNAN
Anzahl der Bytes fuer short und float: 2/4
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le02\Ldtypen.
Beenden wir die Lektion über Variablen und wenden uns in der nächsten Lektion den Konstanten
zu.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_01.htm (10 von 10) [17.05.2005 11:37:59]
Kurzübersicht über Streams
Kurzübersicht über Streams
Kurzübersicht über Streams
Zur Ausgabe von Texte und Daten auf die Standard-Ausgabe verwenden C++ Programme wie
bereits erwähnt vorzugsweise den Ausgabestream cout.
Aber sehen wir uns einmal an, was ein Stream eigentlich ist. Ein Stream ist ein allgemeiner
Datenfluss von einer Datenquelle zu einer Datensenke. Er enthält in seiner allgemeinen Form
noch keine Beschreibung der Datenquelle (woher kommen die Daten) und der Datensenke
(wohin gehen sie). Dies wird erst durch ein entsprechendes, vom Grundstream abgeleitetes,
spezielles Streamobjekt erreicht. Das nachfolgende Bild zeigt die 4 wichtigsten Streamobjekte:
Standard-Ausgabestream cout:
Programm
Standard-Ausgabe
Standard-Eingabestream cin:
Programm
Standard-Eingabe
Datei-Ausgabestream ofstream:
Programm
Massenspeicher
Datei-Eingabestream ifstream:
Programm
Massenspeicher
http://www.cpp-tutor.de/cpp/le01/le01_04_d1.htm (1 von 2) [17.05.2005 11:38:02]
Kurzübersicht über Streams
Außer diesen Streamobjekten gibt es noch eine Reihe weiterer, von denen in dieser Lektion
noch die Streamobjekte cerr und clog behandelt werden. Die restlichen drei oben aufgeführten
Streamobjekte cin, ofstream und ifstream werden dann später im Kurs noch erläutert.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d1.htm (2 von 2) [17.05.2005 11:38:02]
Trigraphen
Die Trigraphen
Trigraphen
Rechts zur Wiederholung
Anweisung:
noch einmal die Anweisung
mit der dazugehörigen
std::cout << "Da schau??!";
Ausgabe. D.h. die Zeichen
??! werden in das einzelne Die Programmausgabe:
Zeichen | umgesetzt.
Da schau|
Aber warum???
In der 'Steinzeit' der C Programmierung wurde teilweise für die Programmeingabe ein 7-Bit
Code verwendet, anstatt wie heute üblich ein 8-Bit Code. Der 7-Bit Code hatte aber einige
Zeichen nicht im Zeichenvorrat, die für die C Programmierung benötigt wurden. Damit
trotzdem alle C relevanten Zeichen eingegeben werden konnten, behalf man sich mit den so
genannten Trigraphen. Jeder Trigraph besteht dabei aus 3 Zeichen, bei dem die ersten beiden
Zeichen immer aus zwei Fragezeichen ?? bestehen. Und da Nachfolgeversionen im Allgemeinen
kompatible zu Vorgängerversionen bleiben sollten, bestehen diese Trigraphen auch heute noch
(obwohl sie wohl überhaupt nicht mehr benötigt werden).
Die nachfolgende Tabelle listet alle Trigraphen sowie daraus resultierenden 'C Zeichen' auf:
Trigraph
C-Zeichen
Trigraph
C-Zeichen
Trigraph
C-Zeichen
??=
#
??(
[
??<
{
??/
\
??)
]
??>
}
??'
^
??!
|
??-
~
Wie Sie aus der Tabelle entnehmen können, wird darin der Trigraph ??! in das Zeichen |
umgesetzt. Und genau das wurde im Beispiel oben auch auf dem Bildschirm ausgegeben!
Beim mitgelieferten MinGW Compiler sind die Trigraphen standardmäßig außer Kraft
gesetzt, d.h. die Zeichenfolge "??!" wird auch als solche ausgegeben. Mehr dazu können
Sie aber in der Doku gcc-3.3.2.chm im Verzeichnis doc nachlesen.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le01/le01_04_d2.htm (1 von 2) [17.05.2005 11:38:03]
Trigraphen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d2.htm (2 von 2) [17.05.2005 11:38:03]
ASCII-Tabelle
ASCII-Tabelle
ASCII-Tabelle
Die nachfolgende Tabelle enthält eine Übersicht der ASCII-Zeichen und ihrem dazugehörigen
dezimalen Wert. Wenn Sie zum Beispiel eine char-Variable mit dem Wert 65 belegen und diese
Variable dann ausgeben, so erhalten Sie das ASCII-Zeichen A. ASCII-Code kleiner 32 und das
Zeichen 127 sind nicht-druckbare Zeichen wie Zeilenvorschub oder der Tabulator. ASCIICodes größer 127 enthalten nicht standardisierte Sonderzeichen, die vom jeweiligen Zeichensatz
abhängen. Dazu gehören unter anderem auch die deutschen Umlaute wie ä, ü usw. Das Zeichen
mit dem Code 127 entspricht dem DELETE-Zeichen.
ASCII-Tabelle
32:
37:
42:
47:
52:
57:
62:
67:
72:
77:
82:
87:
92:
97:
102:
107:
112:
117:
122:
127:
%
*
/
4
9
>
C
H
M
R
W
\
a
f
k
p
u
z
DEL
33:
38:
43:
48:
53:
58:
63:
68:
73:
78:
83:
88:
93:
98:
103:
108:
113:
118:
123:
!
&
+
0
5
:
?
D
I
N
S
X
]
b
g
l
q
v
{
34:
39:
44:
49:
54:
59:
64:
69:
74:
79:
84:
89:
94:
99:
104:
109:
114:
119:
124:
"
'
,
1
6
;
@
E
J
O
T
Y
^
c
h
m
r
w
|
35:
40:
45:
50:
55:
60:
65:
70:
75:
80:
85:
90:
95:
100:
105:
110:
115:
120:
125:
#
(
2
7
<
A
F
K
P
U
Z
_
d
i
n
s
x
}
36:
41:
46:
51:
56:
61:
66:
71:
76:
81:
86:
91:
96:
101:
106:
111:
116:
121:
126:
$
)
.
3
8
=
B
G
L
Q
V
[
`
e
j
o
t
y
~
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le01/le01_04_d3.htm (1 von 2) [17.05.2005 11:38:04]
ASCII-Tabelle
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d3.htm (2 von 2) [17.05.2005 11:38:04]
Das Hexadezimal- und Oktal-System
Das Hexadezimal- und Oktal-System
Das Hexadezimal-System
Das Hexadezimal-System ist ein Zahlensystem zur Zahlenbasis 16 und kennt die Ziffern 0-9 und
A-F. Die Zahlen A-F entsprechenden den Zahlenwerten 10-15. Die einzelnen Stellenwerte des
Hexadezimalsystems werden durch entsprechende 16er-Potenzen gebildet:
Hex-Zahl:
X Y Z
entspricht Dezimalzahl: X * 162 + Y * 161 + Z * 160
Beispiel
Hex-Zahl:
3FC
Dezimalzahl: 3 * 162 + 15 * 161 + 12 * 160
3 * 256 + 15 * 16 + 12
1020
Das Oktal-System
Das Oktal-System ist ein Zahlensystem mit der Zahlenbasis 8 und kennt demzufolge nur die
Ziffern 0--7. Hier werden die einzelnen Stellenwerte durch entsprechende 8er-Potenzen
gebildet:
Oktal-Zahl: X Y Z
entspricht Dezimalzahl: X * 82 + Y * 81 + Z * 80
Beispiel
Oktal-Zahl: 477
Dezimalzahl: 4 * 82 + 7 * 81 + 7 * 80
4 * 64 + 7 * 8 + 7
319
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le01/le01_04_d4.htm (1 von 2) [17.05.2005 11:38:04]
Das Hexadezimal- und Oktal-System
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d4.htm (2 von 2) [17.05.2005 11:38:04]
Formatflags für die Ausgabe
Formatflags für die Ausgabe
Zusätzlich können noch
weitere Formatierungen
über so genannte Flags
(Flaggen, nehmen nur die
Zustände gesetzt und nicht
gesetzt an) eingestellt
werden. Um eine bestimmte
Formatierung der Ausgabe
zu erreichen, setzen Sie das
entsprechende Flag mit der
Memberfunktion setf(...)
des cout-Streams.
Entsprechend wird mittels
unsetf(...) das Flag wieder
gelöscht. Beide Funktionen
liefern die bisherigen Flags
als long Wert zurück. Wenn
Sie die bisherigen Flags
nicht interessieren, können
Sie den Rückgabewert auch
einfach ignorieren (so wie
rechts im Beispiel). Rechts
sehen Sie einige Beispiel für
Formatierungen über
Flags. Stören Sie sich im
Augenblick nicht an der
etwas seltsam wirkenden
Schreibweise ios::xxx. xxx
ist das zu beeinflussende
Flag und ios:: gibt die
Klasse an, zu der das Flag
gehört. Mehr zu Klassen
und dem Operator :: später
im Kurs. Beachten Sie
auch, dass es zwei
verschiedene
Memberfunktionen setf(...)
gibt. Bei den ersten beiden
Aufrufen erhält die
Memberfunktion nur einen
Parameter übergeben und
// IO-Stream Datei und
// Manipulatoren einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::ios;
using std::setw;
// Das Programm
int main ( )
{
// Auszugebende Daten
int iVar = 20;
double dblVar = 1./3.;
// Ausgabe mit Zahlenbsis
cout.setf(ios::showbase);
cout << "Zahlenbasis mit ausgeben:" <<
std::hex << setw(4) << iVar <<
std::oct << setw(4) << iVar << '\n';
// Zahlenbasis unterdrücken
cout.unsetf(ios::showbase);
// Ausgabe linksbuendig mit Exponenten
cout.setf(ios::left, ios::adjustfield);
cout.setf(ios::scientific,
ios::floatfield);
cout << "linksbuendig und Exponent:" <<
std::setprecision(2) <<
setw(13) << dblVar << ":\n";
}
Programmausgabe:
Zahlenbasis mit ausgeben:0x14~024
linksbuendig und Exponent:3.33e-001~~~:
Hinweis: Das Zeichen ~ steht hier für ein
Leerzeichen.
http://www.cpp-tutor.de/cpp/le01/le01_04_d5.htm (1 von 4) [17.05.2005 11:38:05]
Formatflags für die Ausgabe
bei den nachfolgenden
Aufrufen zwei Parameter.
Die Memberfunktion mit
zwei Parametern verändert
nur die Flags, die im
zweiten Parameter
angegeben sind, d.h. der
zweite Parameter ist eine
Art Maske. Zuerst werden
alle entsprechenden Flags
des zweiten Parameters
gelöscht und dann die
Flags, die im ersten
Parameter spezifiziert sind,
gesetzt. Beachten Sie dabei
aber bitte, dass auch
wirklich nur die Flags
gesetzt werden, die auch im
zweiten Parameter
enthalten sind. Wenn Sie
sich das Beispiel einmal
genauer ansehen haben,
wundern Sie sich nun
vielleicht, dass z.B. beim
Aufruf der
Memberfunktion
setf(ios::left, ios::adjustfield)
das Flag ios::left im zweiten
Parameter nicht auftaucht.
Dies liegt aber daran, dass
ios::adjustfield ist eine
Kombination aus den 3
Flags ios::left, ios::right und
ios::internal ist.
Beachten Sie im obigen Programm auch die Angaben des Namensbereichs std. Hier wird bunt
zwischen der using-Form und der std::-Form hin und her gesprungen. Dies soll nur nochmals
verdeutlichen, dass Sie beide Formen parallel benutzen können. Bei einem so einfachen
Programm wie oben würde sich die anfangs im Kurs eingeführte Form using namespace std;
jedoch besser eignen.
Die nachfolgende Tabelle enthält alle Flags und deren Auswirkungen:
Aufruf
http://www.cpp-tutor.de/cpp/le01/le01_04_d5.htm (2 von 4) [17.05.2005 11:38:05]
Auswirkung
Formatflags für die Ausgabe
setf (ios::uppercase);
unsetf (ios::uppercase);
Großbuchstaben für HexZahlen und Exponenten
setf (ios::showbase);
unsetf (ios::showbase);
Präfix für Zahlenbasis
anzeigen
0x für hex, 0 für oktal
setf (ios::showpoint);
unsetf (ios::showpoint);
Erzwingt die Ausgabe des
Dezimalpunktes und eventl.
Nullen bei Gleitkommazahlen
setf (ios::showpos);
unsetf (ios::showpos);
Vorzeichen bei positiven
Zahlen
setf (ios::skipws);
unsetf (ios::skipws);
Überliest bei der Eingabe
white-space characters
(Leerzeichen, Tabs und
Linefeeds)
setf (ios::boolalpha);
unsetf(ios::boolalpha);
Gibt bei bool-Variablen
true bzw false aus
anstelle von 1 und 0
setf (ios::unitbuf);
unsetf (ios::unitbuf);
Leert den Ausgabepuffer
nach jeder Ausgabe
(siehe Vertiefung coutBuffer)
setf (ios::left, ios::adjustfield);
setf (ios::right, ios::adjustfield);
setf (ios::internal,
ios::adjustfield);
Ausgabe linksbündig,
rechtsbündig oder der
Raum zwischen Vorzeichen
und Zahl wird mit
Leerzeichen ausgefüllt.
setf (ios::scientific,
ios::floatfield);
setf (ios::fixed, ios::floatfield);
Ausgabe mit oder ohne
Exponenten
(beeinflusst auch die
Wirkung von setprecision)
setf (ios::dec, ios::basefield);
setf (ios::hex, ios::basefield);
setf (ios::oct; ios::basefield);
Ausgabe in dezimal, hex
oder oktal
Alternative zu dec, hex und
oct im Stream
Die Angabe ios::xxx bei den setf(...) Aufrufen gilt nur, wenn Sie vorher mittels using std::ios;
die Flags explizit
aus dem std-Namensraum eingebunden haben. Tun Sie dies nicht, so müssen Sie den
Namenraum bei jedem
Aufruf mit angeben, z.B. setf(std::ios::showbase);
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le01/le01_04_d5.htm (3 von 4) [17.05.2005 11:38:05]
Formatflags für die Ausgabe
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d5.htm (4 von 4) [17.05.2005 11:38:05]
Der cout-Ausgabepuffer
Der cout-Ausgabepuffer
Fangen wir mit der Eingabe
des Programms an. Für die
Eingabe eines C++
Programms können Sie jeden
beliebigen ASCII-Editor
benutzen, wie z.B. den guten
alten DOS Edit oder unter
WINDOWS auch den
NOTEPAD. Was Sie aber auf
keinen Fall verwenden dürfen,
sind Editoren die Texte
formatieren wie z.B.
WinWord. Diese Editoren
legen außer dem eigentlichen
Text noch Steuerzeichen mit
ab, die dann später den
Compiler mit Sicherheit
verwirren.
Haben Sie Ihr Programm im
Editor fertig eingegeben, so
sollten Sie die Datei
vorzugsweise unter einen
Namen mit der Extension .cpp
speichern. Auf keinen Fall
sollten Sie die Extension .c
verwenden, da einige C++
Compiler sonst annehmen, Sie
wollen ein C Programm und
nicht ein C++ Programm
erstellen. Und C Programme
besitzen u.a. nur eine
Untermenge der
Schlüsselwörter eines C++
Programms.
Beachten Sie bitte, dass auch der Manipulator endl im std-Namensraum liegt und entsprechend
'eingebunden' werden muss.
http://www.cpp-tutor.de/cpp/le01/le01_04_d6.htm (1 von 2) [17.05.2005 11:38:06]
Der cout-Ausgabepuffer
Wenn Sie viele Ausgaben nacheinander vornehmen, so sollten Sie aus
Geschwindigkeitsgründen alle Zeilenvorschübe bis auf den Letzten durch die EscapeSequenz \n erzeugen. Bei der letzten Ausgabe verwenden Sie dann endl um den
Ausgabepuffer dann anschließend auch zu leeren.
Wollen Sie die cout-Ausgabe ungepuffert durchführen, so können Sie mit cout.setf(ios::unitbuf)
die Pufferung 'abschalten' und mit cout.unsetf(ios::unitbuf) sie bei Bedarf wieder aktivieren.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d6.htm (2 von 2) [17.05.2005 11:38:06]
cerr und clog
cerr und clog
cerr und clog sind genauso wie cout Streams die standardmäßig ihre Ausgaben auf die
Standardausgabe ausgeben. Und sie liegen ebenfalls im std-Namensbereich. Überall wo Sie den
Ausgabestream cout bisher verwendet haben, könnten Sie auch den Ausgabestream cerr bzw.
clog einsetzen.
Vielleicht fragen Sie sich nun, warum es dann diese zusätzlichen Streams überhaupt gibt? Nun,
wie aus den Namen der Streams ersichtlich ist, dienen diese für Ausgaben von besonderen
Meldungen. So ist der Stream cerr für die Ausgabe von Fehlermeldungen gedacht und clog für
die Ausgabe von irgend welchen Kontrollausgaben, wie z.B. Meldungen zur Verfolgung des
Programmablaufs. Der Stream cerr besitzt gegenüber cout und clog jedoch einen kleinen
Unterschied: die Ausgabe erfolgt hier ungepuffert, d.h. direkt ohne Umweg über einen
Ausgabepuffer.
Aber wie gesagt,
normalerweise landen alle
diese Ausgabe genauso auf
dem Bildschirm wie cout
Ausgaben. Im Beispiel
rechts (das Sie nicht
komplett verstehen müssen
um es anzuwenden) können
Sie sich nun ansehen, wie
z.B. die Ausgabe des cerr
Streams in die Datei test.err
umgeleitet wird. Das
Prinzip bei dieser
Ausgabeumleitung, die
übrigens genauso für den
Ausgabestream clog und
sogar für cout funktionieren
würde, besteht darin, für
die cerr Ausgabe den
Ausgabepuffer eines
Dateistreams einzusetzen.
Mehr zu den Dateistreams
aber später im Kurs noch.
#include <iostream>
#include <fstream>
int main ()
{
// cerr Ausgabe auf Datei umleiten
std::ofstream out("test.err");
std::streambuf* sbuf = std::cerr.rdbuf();
std::cerr.rdbuf(out.rdbuf());
std::cerr << "cerr Ausgabe" << std::endl;
std::clog << "clog Ausgabe" << std::endl;
// Umleitung unbedingt wieder aufheben!
std::cerr.rdbuf(sbuf);
}
Beachten Sie wieder wo
überall der Namensraum
std angegeben werden muss.
Das nebenstehende
http://www.cpp-tutor.de/cpp/le01/le01_04_d7.htm (1 von 2) [17.05.2005 11:38:06]
cerr und clog
Programm kommt ohne
using-Anweisung aus.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le01/le01_04_d7.htm (2 von 2) [17.05.2005 11:38:06]
Konstanten
Konstanten
Die Themen:
Einleitung
Ganzzahl-Literale
Gleitkomma-Literale
Zeichen- und String-Literale
Benannte Konstanten
Einleitung
Eine Konstante ist ein Datum, das sich nach seiner Initialisierung im aktuellen
Gültigkeitsbereich nicht mehr verändern lässt. Ein einfaches Beispiel hierzu ist z.B. die Zahl Pi.
Wie allgemein bekannt, entspricht die Zahl Pi etwa dem Wert 3.1416. Anstatt nun bei jeder
Verwendung von Pi den Zahlenwert 3.1416 zu schreiben, kann dafür auch ein Name vergeben
werden, z.B. PI. Dieser Name wird dann anstelle des Zahlenwertes an den entsprechenden
Stellen im Programm eingesetzt, an denen Pi verwendet wird. Wird nun während des
Programmtests z.B. festgestellt, dass die Angabe von Pi zu ungenau ist, so müssen nun nicht
mehr im gesamten Programm die Werte verändert werden, sondern es wird lediglich die einmal
definierte Konstante angepasst.
C++ kennt unter anderem folgende Arten von Konstanten, die wir uns in dieser Lektion
ansehen werden:
●
●
●
●
Ganzzahlkonstanten
Gleitkommakonstanten
String- und Zeichenkonstanten
Objektkonstanten (wird später noch behandelt)
Generell können aber von jedem beliebigen Datentyp Konstanten gebildet werden.
Außer nach der Konstantenart werden Konstanten noch nach unbenannten und benannten
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (1 von 6) [17.05.2005 11:38:07]
Konstanten
Konstanten unterschieden.
Das Literal
Der Begriff Literal ist im Prinzip nur ein anderes Wort für eine zahlenmäßige Konstante. In der
Praxis werden direkte Zahlenangaben wie z.B. die Zahl 5 oder auch 3.14 als Literale bezeichnet,
während der Begriff Konstante für benannte Konstanten verwendet wird.
Ganzzahl-Literale
Ganzzahl-Literale besitzen standardmäßig den kleinsten Datentyp, in dem ihr Wert
repräsentiert werden kann. Dabei wird folgende Reihenfolge durchlaufen: int, long und unsigned
long. Eine Ausnahme von dieser Reihenfolge bilden Ganzzahl-Literale die als oktaler bzw.
hexadezimaler Wert angegeben werden. Hier lautet die Reihenfolge: int, unsigned int, long und
unsigned long. Literale in oktaler Schreibweise erhalten den Präfix 0 und Literale in
hexadezimaler Schreibweise den Präfix 0x bzw. 0X.
Um einem Ganzzahl-Literal explizit einen bestimmten
Datentyp zuzuweisen, wird die Zahlenangabe mit einem
Suffix erweitert. Bei diesen Erweiterungen wird die Groß/Kleinschreibung ausnahmsweise ignoriert. Die Tabelle
rechts enthält eine Übersicht über die verschiedenen
Erweiterungen sowie einige Beispiele dazu.
Schreiben Sie niemals 0010 wenn Sie ein DezimalLiteral definieren wollen! Diese Zahl entspricht
der Oktalzahl 10 und das ist in dezimal 8!
Literale ohne Erweiterung
3
-5
+4
Oktal-Literal: Präfix 0
03
012
0665
Hex-Literal: Präfix 0x
0x40
0XFF00
-0xFF
unsigned Literal: Suffix U
10U
0xFF00u
012u
long Literal: Suffix L
-10L
0xFF00l
012l
unsigned long Literal: Suffix UL
10UL 0xFF00ul 012ul
Gleitkomma-Literale
Gleitkomma-Literale bestehen aus Ziffern und einem Dezimalpunkt. Optional kann ein
Gleitkomma-Literal noch einen Exponenten enthalten. Standardmäßig sind GleitkommaLiterale vom Datentyp double.
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (2 von 6) [17.05.2005 11:38:07]
Konstanten
Um einem Gleitkomma-Literal einen von double
abweichenden Datentyp zuzuweisen, wird das Literal mit
einem Suffix laut nebenstehender Tabelle erweitert.
Auch hier spielt die Groß-/Kleinschreibung bei der
Erweiterung keine Rolle.
double Literal: ohne Erweiterung
-1.0 1. +2.32 -3.1E+4
float Literal: Suffix F
-1.0F 1.f 2.32f -3.1E+4F
long double Literal: Suffix L
-1.0L 1.L 2.32l -3.1E+4l
Zeichen- und String-Literale
Zeichen-Literale sind einzelne Buchstaben oder auch Escape-Sequenzen ( ) die in einfache
Hochkomma eingeschlossen werden. Sie besitzen den Datentyp char. Um ein Zeichen-Literal als
wide-character Literal (Datentyp wchar_t) zu kennzeichnen, wird das Literal mit dem Präfix L
erweitert.
String-Literale werden dagegen in Anführungszeichen eingeschlossen. Auch hier erhalten widecharacter String-Literale wieder den Präfix L (siehe erstes String-Literal in nachfolgender
Tabelle).
String-Literale weisen drei Besonderheiten auf:
●
●
●
Ihr Datentyp ist 'Feld mit n konstanten
Zeichen' (const char[n] bzw. const
wchar_t[n]), wobei n die Anzahl der Zeichen
plus 1 ist. Das Verändern von Zeichen in
einem String-Literal führt zu undefiniertem
Verhalten!
Sie haben immer die Speicherklasse static.
Das letzte Zeichen eines String-Literals (und
auch -Konstante) ist immer eine binäre 0!
String-Literale:
L"Dies ist eine Konstante"
DoAny("I'm a parameter");
Zeichen-Literal:
'a'
ASCII-Zeichen A
'\n'
Zeilenvorschub
L'A'
wide-character A
Felder und Speicherklassen werden später noch
behandelt.
Die nebenstehende Tabelle enthält einige Beispiele für die verschiedenen Literale.
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (3 von 6) [17.05.2005 11:38:07]
Konstanten
Nebeneinander stehende
Zusammenfassen von String-Literalen
String-Literale werden
immer zusammenfasst.
cout << "Diese ist ein String-Literal"
Somit ist die
"obwohl es über mehrere"
nebenstehende Ausgabe
"Zeilen geht!\n";
korrekt, da die drei StringLiterale zu einem Literal
zusammengefasst werden.
Das Einzige worauf Sie
dabei achten müssen ist,
dass Sie keine char und
wchar_t String-Literale
nebeneinander stellen
dürfen.
Benannte Konstanten
Benannte Konstanten werden
prinzipiell wie Variablen definiert,
d.h. sie haben einen vorgegebenen
Datentyp und einen Namen
(Konstantenname). Zusätzlich erfolgt
jedoch vor dem Datentyp die Angabe
des Schlüsselwortes const. Und da
Konstanten während des
Programmlaufs ihren Wert nicht
ändern können, müssen Sie bei ihrer
Definition initialisiert werden.
Datentyp
Konstantenname Initialwert
const int NOOFLINES
= 24;
const char LINEFEED
= '\n';
const float PI
= 3.1416f;
const short NOTTHIS;
nicht erlaubt!
Beachten Sie die letzte Definition.
Diese Definition erzeugt einen
Übersetzungsfehler da die Konstante
nicht initialisiert wird.
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (4 von 6) [17.05.2005 11:38:07]
Konstanten
Benannte Konstanten sind
standardmäßig
modulglobal, d.h. sie gelten
nur in der Quellcode-Datei,
in der sie definiert
sind. Benötigen Sie eine
benannte Konstante in
mehreren Modulen, so
legen Sie die Konstante am
besten in einer getrennten
Header-Datei (.h Datei) ab,
die sie dann in den
entsprechenden Modulen
mit #include "xx.h";
einbinden. Im Beispiel
rechts wird die Konstante
PI in der Header-Datei
myinc.h definiert. Diese
Datei wird dann sowohl im
Modul main.cpp wie auch
im Modul mod1.cpp
eingebunden. Dadurch ist
in beiden Modulen die
Konstante PI bekannt.
Beachten Sie bei
dieser
Vorgehensweise,
dass die Konstante
PI zweimal definiert
wird, einmal im
Modul main.cpp
und einmal im
Modul mod1.cpp.
Ob dafür auch
zweimal Speicher
belegt wird ist vom
verwendeten
Compiler abhängig.
Je nach
Verwendung der
Konstanten im
Programm kann
der Compiler z.B.
beim Übersetzen
die Konstante
Include-Datei myinc.h
const float PI = 3.1416F;
Modul main.cpp
#include <iostream>
#include "myinc.h"
using namespace std;
int main ( )
{
cout << "Pi hat den Wert " << PI << endl;
}
Modul mod1.cpp
#include <iostream>
#include "myinc.h"
using namespace std;
void PrintIt ( )
{
cout << "Pi hat den Wert " << PI << endl;
}
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (5 von 6) [17.05.2005 11:38:07]
Konstanten
durch das
entsprechende
Literal ersetzen.
Wenn Sie wollen, können Sie auch für Konstanten die Ungarische Notation verwenden. Dazu
wird auch hier wieder vor dem Konstantenname der Präfix gestellt, der den Datentyp der
Konstanten repräsentiert.
Damit Konstanten von Variablen unterschieden werden können, werden die Konstantennamen
innerhalb des Kurses stets in Großbuchstaben geschrieben. Dies ist aber keine Vorschrift!
Beenden wir damit diese Lektion (ausnahmsweise einmal ohne Beispiel und Übung) und wenden
wir uns den Zuweisungen zu.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_02.htm (6 von 6) [17.05.2005 11:38:07]
Ungarische Notation
Ungarische Notation
Zur Benennung von Konstanten und Variablen wird manchmal auch die so genannten
Ungarische Notation eingesetzt. Sie soll u.a. helfen, Fehler durch Zuweisungen von Daten mit
unterschiedlichen Datentypen zu meiden. Hierbei setzt sich ein Variablenname wie folgt
zusammen:
●
●
einem Präfix, der den Datentyp der Variable/Konstante beschreibt.
dem eigentlichen Namen. Der Name sollte bei Variablen nur aus Buchstaben bestehen,
wobei an Wortgrenzen, der besseren Lesbarkeit wegen, Großbuchstaben einzusetzen
sind. Konstantennamen (siehe nächste Lektion) hingegen bestehen nur aus
Großbuchstaben. Hier kann, ebenfalls der besseren Lesbarkeit wegen, auch ein
Underscore '_' an den Wortgrenzen eingesetzt werden.
Folgende Präfixe sind für die einzelnen Datentypen vorzugsweise einzusetzen:
Datentyp
Präfix
Anmerkung
bool / unsigned
char
b
vorzeichenloser 8-Bit Wert, bool Wert
oder OEM-Zeichen
char
c oder ch
vorzeichenbehafteter 8-Bit Wert, ASCIIZeichen
unsigned short
w
vorzeichenloser 16-Bit Wert
short / integer
n
vorzeichenbehafteter 16-Bit Wert oder
Integer Wert
unsigned long
dw
vorzeichenloser 32-Bit Wert
long
l
vorzeichenbehafteter 32-Bit Wert
float
f
32-Bit Gleitkommazahl
double
d
64-Bit Gleitkommazahl
double float
df
Gleitkommazahl 64-Bit bei VC++, 96-Bit
bei MinGW
class
C
Klasse oder Instanz einer Klasse
union
u
Variante (union) oder Instanz einer
Variante
struct
str
Definition oder Instanz einer Struktur
enum
e
Enumeration oder Instanz einer
Enumeration
http://www.cpp-tutor.de/cpp/le02/le02_01_d1.htm (1 von 2) [17.05.2005 11:38:08]
Ungarische Notation
Zusatz-Präfixe, stehen immer in Kombination mit einem anderen
Präfix und an erster Stelle
sz
Zeichenkette, mit '0' abgeschlossen
p
Zeiger (pointer)
a
Feld (array)
m_
Member (einer Klasse)
Damit Sie sich unter der Ungarischen Notation auch etwas vorstellen können, nachfolgend ein
paar Beispiele dazu:
Variablenbezeichnung
Bedeutung
pszString
Zeiger auf eine mit '0' abgeschlossene Zeichenkette (ASCII-String)
nVar
short- oder integer-Variable
acTemp[5]
Feld mit vorzeichenbehafteten 8-Bit Zahlen oder ASCII-Zeichen
strPoint
Strukturdefinition oder -instanz
CMyWindow
Klassendefinition oder -instanz
pCMyWin->m_wHeight
Zugriff über einen Klassenzeiger auf eine Membervariable vom Typ
unsigned short
Die Ungarische Notation ist natürlich keine(!) ANSI C++ Vorschrift. Es gibt genauso viele
Argumente für sie wie auch dagegen. Über die Ungarische Notation wurden teilweise in den
Newsgroups schon regelrechte 'Religionskriege' geführt. Nicht verschwiegen werden soll, dass
diese Schreibweise bei größeren Projekten aber auch eine 'Gefahr' mit sich bringt. Wird
irgendwann im Laufe eines Projekts einmal der Datentyp einer Variable oder Konstante
verändert, so müsste dann auch der Variablenname überall nachgezogen werden damit der
Präfix den neuen Datentyp widerspiegelt. Dies kann dann je nach Projektgröße zu einer recht
umfangreichen Aufgabe führen.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_01_d1.htm (2 von 2) [17.05.2005 11:38:08]
Das 2er-Komplement
Das 2er-Komplement
Negative Ganzzahlen werden im so genannten 2er-Komplement dargestellt.
Die Regel zur Bildung des 2er-Komplements lautet:
●
●
Alle Bits der entsprechenden positiven Zahl werden zuerst invertiert, d.h. aus einem 1 Bit
wird ein 0 Bit und umgekehrt.
Zu der so erhaltenen 'Zahl' wird der Wert 1 hinzuaddiert. Ein eventuell auftretender
Überlauf wird verworfen.
Beispiel für die Berechnung des 2er-Komplements:
Die char Zahl 2 ist in eine negative Zahl (-2) umzuwandeln:
2 ist binär
alle Bits invertieren
und 1 hinzuaddieren
0000 0010
1111 1101
1111 1110
(entspricht -2)
Überprüfen der Konvertierung durch Addition von 2 und -2:
2 ist binär 0000
-2 ist binär 1111
Überträge
1]1111
Ergebnis
1]0000
0010
1110
1100
0000
Was zu beweisen war!
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le02/le02_01_d2.htm (1 von 2) [17.05.2005 11:38:09]
Das 2er-Komplement
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_01_d2.htm (2 von 2) [17.05.2005 11:38:09]
Das Speicherabbild von Integer-Daten
Das Speicherabbild von IntegerDaten
Wenn Sie nun einen der
Buttons rechts anklicken,
können Sie sich einmal
ansehen, wie die Daten einer
Variablen auf einem System
mit INTEL-kompatiblen
Prozessor im Speicher zu
liegen kommen. Beachten Sie
bitte im Beispiel rechts, dass
der niederwertige Teil des
Wertes (Low-Byte) auf der
niederen Adresse zu liegen
kommt und der
höherwertige (High-Byte)
auf der höheren Adresse.
Diese Reihenfolge gilt
zumindest auf allen
Systemen mit INTELkompatiblen Prozessoren.
Andere Prozessoren, z.B.
MOTOROLA, legen die
Daten in umgekehrter
Reihenfolge ab!
Außerdem gilt in der
Darstellung:
bool =
char =
short =
long =
1
1
2
4
Byte
Byte
Byte
Byte
Dies ist aber laut C++
Standard nicht
vorgeschrieben, wohl aber
auf den meistens PCs im
Augenblick die Regel.
http://www.cpp-tutor.de/cpp/le02/le02_01_d3.htm (1 von 2) [17.05.2005 11:38:10]
Das Speicherabbild von Integer-Daten
Ferner können Sie dem
Speicherabbild noch
entnehmen, wie negative
Werte dargestellt werden.
Mehr zur Darstellung von
negativen Werten bei der
Behandlung des 2erKomplements. Sie können
sich das 2er-Komplement
nun ansehen, wenn Sie
hierauf klicken ( ).
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_01_d3.htm (2 von 2) [17.05.2005 11:38:10]
Zuweisungen
Zuweisungen
Die Themen:
Zuweisungen von numerischen Daten
Zuweisungen von Zeichen- und String-Literalen
Mehrfach-Zuweisungen
Beispiel und Übung
Die Zuweisung einer Konstanten, einer Variablen oder eines anderen Ausdrucks an eine Variable erfolgt
mit dem Zuweisungsoperator =.
Beachten Sie, dass wir im weiteren Verlaufe dieser Lerneinheit davon ausgehen, dass eine short
Variablen 2 Bytes und eine long Variable 4 Bytes belegt. Wie bereits erwähnt, kann dies auf anderen
Systemen durchaus anders sein. Jedoch ändert sich dadurch nichts am grundsätzlichen Verhalten
der gleich beschriebenen Anpassung (Konvertierung) von unterschiedlichen Datentypen.
Zuweisungen von numerischen Daten
Bei einer Zuweisung von numerischen Daten müssen drei Fälle unterschieden werden:
Die Datentypen links und rechts des Operators = sind gleich. In diesem Fall müssen bei der
Zuweisung keine Anpassungen vorgenommen werden und die Zuweisung erfolgt ohne Verlust an
Informationen.
Zuweisung einer short Variable an eine long Variable:
Der Datentyp des linken
Operanden besitzt einen größeren
Wertebereich als der Datentyp des
rechten Operanden. Beide Datentypen
sind jedoch mit oder ohne Vorzeichen
(signed bzw. unsigned).
In diesem Fall wird der rechte
Operand durch den Compiler
automatisch vorzeichenrichtig
short sVal = -10;
long lVal = sVal;
// = 0xFFF6
// = 0xFFFFFFF6
Zuweisung einer unsigned char Variable an eine unsigned long
Variable:
unsigned char bVal = 128;
unsigned long dwVal = bVal;
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (1 von 6) [17.05.2005 11:38:11]
// = 0x80
// = 0x00000080
Zuweisungen
erweitert und es geht somit keine
Information verloren (automatische
Typkonvertierung). Dies wird
besonders deutlich bei der ersten
Zuweisung im Beispiel rechts, bei der
eine short Variable einer long Variable
zugewiesen wird
Zuweisung einer long Variable an eine short Variable:
Der Datentyp des linken
Operanden besitzt einen kleineren
Wertebereich als der Datentyp des
rechten Operanden oder die
Datentypen besitzen unterschiedliche
'Vorzeichen-Datentypen' (unsigned
nach signed oder umgekehrt).
Hier wird's gefährlich! In beiden
Fällen werden die Bytes des rechten
Operanden einfach dem linken
Operanden zugewiesen. Die Anzahl
der zugewiesenen Bytes richtet sich
dabei selbstverständlich nach der
Größe des linken Operanden. D.h.
wenn Sie einer char-Variable einen
long Wert zuweisen, so wird nur das
niederwertigste Byte des long Werts
übernommen. Ein guter Compiler gibt
aber in diesen Fällen eine Warnung
aus, die Sie selbstverständlich auch
ernst nehmen sollten. Sonst kann es,
wie im Beispiel rechts bei der letzten
Zuweisung, zu recht seltsamen
Ergebnissen führen.
long lVal = -186L; // = 0xFFFFFF46
short wVal = lVal; // = 0xFF46 = -186
Zuweisung einer unsigned long Variable an eine unsigned char
Variable:
unsigned long dwVal = 128UL; // = 0x00000080
unsigned char bVal = dwVal; // = 0x80
Aber Achtung! Zuweisung einer unsigned long Variable an eine
char Variable:
unsigned long dwVal = 128UL // 0x00000080;
char cVal = dwVal; // 0x80 = -128
// (Vorzeichen!)
Bei der Konvertierung einer Gleitkommazahl in eine Ganzzahl werden zusätzlich noch die
Nachkommastellen einfach abgeschnitten, d.h. es erfolgt keine automatische Rundung.
Zuweisungen von Zeichen- und String-Literalen
Die Zuweisung von Zeichen- und String-Literalen erfolgt prinzipiell gleich wie die Zuweisung von
numerischen Daten. Lediglich beim Datentyp der linken Operanden müssen Sie acht geben.
Die nachfolgenden Ausführungen folgen der Richtlinie: die Datentypen bei Zuweisungen sollten links und
rechts des Gleichheitszeichens identisch sein.
Wenn ein Zeichen-Literal einer Variable zugewiesen werden soll, so sollte die Variable den Datentyp char,
signed char oder unsigned char besitzen wenn Sie nachher noch damit als ASCII-Zeichen arbeiten wollen.
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (2 von 6) [17.05.2005 11:38:11]
Zuweisungen
Das zuzuweisende Zeichen wird in
einfache Hochkomma eingeschlossen, so
wie in der letzten Lektion Konstanten
dargestellt. Das Gleiche gilt auch für
wide-character. Der Datentyp der
Variable sollte hier wchar_t sein und
das zuzuweisende Zeichen wird
ebenfalls in Hochkomma eingeschlossen
und ist mit dem Präfix L zu versehen.
Weisen Sie ein Zeichen einem anderen
Datentyp als char zu, so erfolgt wie bei
numerischen Daten eine entsprechende
Typkonvertierung vom Datentyp char
aus.
Zuweisungen von Zeichen:
char cVal = 'A';
// = 0x41
unsigned char bVal = '\n'; // = 0x0A
Zuweisungen von wide character:
wchar_t wVal = L'A';
wchar_t wVal = L'\n';
// = 0x0041
// = 0x000A
Zuweisungen von Strings:
const char *pText; // const char-Zeiger
pText = "Dies ist ein String!";
Wollen Sie ein String-Literal einer
Variable zuweisen, so muss die Variable den Datentyp Zeiger auf const char besitzen. Um einen const charZeiger zu definieren, geben Sie nach dem Datentyp const char und vor dem Variablennamen ein *
(Sternchen) ein, so wie im Beispiel oben dargestellt. Anschließend können Sie dann das in
Anführungszeichen eingeschlossene String-Literal diesem Zeiger zuweisen. Sie können diesem Zeiger
später im Programmverlauf durchaus ein anderes String-Literal zuweisen, da hier nicht der Zeiger
konstant ist sondern das worauf er zeigt, also das String-Literal. Mehr zu Zeigern später im Kurs.
Aus Gründen der Rückwärtskompatiblität mit älteren Programmen ist es auch erlaubt, ein StringLiteral einem char-Zeiger zuzuweisen.
char *pText = "String-Literal";
Bei neueren Programmen ist diese Konstruktion aber unbedingt zu vermeiden. Sie ist nur ein
'Tribut' des C++ Standards an bereits bestehende (alte C) Programme.
Mehrfach-Zuweisungen
C++ lässt außer einfachen
Zuweisungen auch MehrfachZuweisungen zu. Hierbei kann dann
mehreren Variablen in einer
Anweisung der gleiche Wert
zugewiesen werden. Die einzelnen
Variablen werden mit dem
Zuweisungsoperator verkettet und
ganz rechts wird der zuzuweisende
Wert (Konstante oder weitere
Variable) angegeben. Die Zuweisungen
erfolgen dann in der Reihenfolge von
rechts nach links.
short sVar1, sVar2;
char cVar;
// Ergebnis: sVar2=255, sVar1=255
sVar1 = sVar2 = 0xFF;
// Ergebnis: cVar=-1, sVar1=-1
sVar1 = cVar = 0xFF;
// Ergebnis: sVar1=255, cVar=-1!
cVar = sVar1 = 0xFF;
Aber Achtung!
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (3 von 6) [17.05.2005 11:38:11]
Zuweisungen
Vermeiden Sie bei Mehrfach-Zuweisungen unbedingt die Verwendung von unterschiedlichen
Datentypen. Dies kann sonst, wie im 3. Beispiel oben dargestellt, zu unliebsamen Überraschungen
führen.
Damit sind wir am Ende dieser Lerneinheit angelangt. Jetzt folgt noch das Beispiel und anschließend
dürfen Sie sich wieder an einer Übung versuchen.
Beispiel und Übung
Das Beispiel:
Innerhalb von main(...) wird die Konstante PI definiert und mit dem Wert 3.1416 belegt.
Die Konstante PI wird dann der int-Variable intPi zugewiesen, um so den ganzzahligen Anteil von PI zu
erhalten. Anschließend wird intPi ausgegeben.
Danach wird eine char-Variable letterA definiert und mit dem Buchstaben 'A' initialisiert.
Der Inhalt von letterA wird dann einmal als char-Variable, einmal als Dezimalwert und einmal als
Hexadezimalwert ausgegeben. Beachten Sie, dass der Inhalt von char-Variablen standardmäßig als
ASCII-Zeichen interpretiert wird. Um eine char-Variable als numerischen Wert auszugeben, müssen Sie
eine entsprechende explizite Typkonvertierung vornehmen. Im Beispiel wird der char-Wert dazu in einen
int-Wert konvertiert.
Das nachfolgende Beispiel finden Sie auch unter le02\prog\Bzuweis.
Die Programmausgabe:
Der Integerteil von Pi ist: 3
Inhalt der Variable 'letterA' ist: A
Und 'letterA' als Wert: 65
In Hex-Darstellung: 0x41
Das Programm:
// C++ Kurs
// Beispiel zu Zuweisungen
#include <iostream>
using std::cout;
using std::endl;
int main()
{
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (4 von 6) [17.05.2005 11:38:11]
Zuweisungen
// Definition der Konstanten PI
const double PI = 3.1416;
// Definition einer int Variable und den
// ganzzahligen Teil von Pi zuweisen
// Die Warnung, dass hier ein const double einem
// int zugewiesen wird kann ausnahmsweise ignoriert werden
int intPi = PI;
cout << "Der Integerteil von Pi ist: " << intPi << endl;
// char-Variable definieren und gleich
// mit dem Buchstaben A initialisieren
char letterA = 'A';
cout << "Inhalt der Variable 'letterA' ist: " << letterA << endl;
// Inhalt der char-Variable als Wert ausgeben
cout << "Und 'letterA' als Wert: " << static_cast<int>(letterA) << endl;
cout << "In Hex-Darstellung: " << std::hex << std::showbase
<< static_cast<int>(letterA) << endl;
}
Die Übung:
Definieren Sie eine short Variable shortValue und initialisieren diese bei ihrer Definition mit dem HexWert 7fff.
Geben Sie die short Variable in dezimal aus.
Definieren Sie eine char Variable charValue und weisen Sie ihr die Variable shortValue zu.
Definieren Sie eine long Variable longValue und weisen Sie ihr die Variable shortValue zu.
Geben Sie die char Variable und long Variable in dezimal aus. Welche Werte erhalten Sie und warum?
Die Programmausgabe:
?? Die sollen Sie selber herausbekommen ??
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie
die Lösung unter loesungen\le02\Lzuweis.
Ich hoffe, die Übung war nicht allzu schwer. In der nächsten Lerneinheit geht's dann mit den
Rechenoperationen los.
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (5 von 6) [17.05.2005 11:38:11]
Zuweisungen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le02/le02_03.htm (6 von 6) [17.05.2005 11:38:11]
Grundrechenoperationen
Grundrechenoperationen
Die Themen:
Grundrechenoperationen
Kurzschreibweisen
Beispiel und Übung
Grundrechenoperationen
Bringen wir nun etwas Schwung in die bisher doch relativ einfachen Programme. In dieser Lektion
werden wir uns mit den Grundrechenoperationen befassen die C++ zur Verfügung stellt.
Für Berechnungen stehen die bekannten Rechenoperationen Addition +, Subtraktion -, Multiplikation *
und Division / zur Verfügung. Diese Operatoren gelten sowohl für Ganzzahlen wie auch für
Gleitkommazahlen.
Aber Achtung!
Wird eine Division mit
zwei Ganzzahlen
durchgeführt so ist das
Ergebnis ebenfalls eine
Ganzzahl. Es wird keine
Rundung des Ergebnisses
durchgeführt. Wollen Sie
das Ergebnis der Division
als Gleitkommazahl
erhalten, so müssen Sie
die beiden Operanden vor
der Division in
Gleitkommazahlen
konvertieren. Mehr dazu
später bei den
Typkonvertierungen.
#include <iostream>
using namespace std;
// Zwei Ganzzahl-Variablen definieren
short var1=10, var2=3;
int main ( )
{
short result;
// Division zweier Ganzzahlen
result = var1/var2;
cout << var1 << '/' << var2 << '=' <<
result << endl;
// Modulo zweier Ganzzahlen
result = var1%var2;
cout << var1 << '%' << var2 << '=' <<
result << endl;
}
Programmausgabe:
10/3=3
10%3=1
http://www.cpp-tutor.de/cpp/le03/le03_01.htm (1 von 5) [17.05.2005 11:38:12]
Grundrechenoperationen
Für Ganzzahlen steht ferner noch
der Modulo-Operator % zur
Verfügung. Er berechnet den Rest
einer Division von zwei Ganzzahlen
(siehe Beispiel rechts).
Bei allen Operationen gilt stets: Punkt- vor Strichrechnung, d.h. Multiplikation, Division und Modulo
vor Addition und Subtraktion. Mehr zur Reihenfolge der Operatoren später noch.
Kurzschreibweisen
C++ stellt noch eine Reihe von Kurzschreibweisen für häufig benötigte Rechenoperationen bereit, die in
der nachfolgenden Tabelle aufgeführt sind. Diese Kurzschreibweisen können sowohl auf Ganzzahlen wie
auch auf Gleitkommazahlen angewandt werden. Die einzige Ausnahme bildet hierbei der ModuloOperator %=, er ist nur für Ganzzahlen zugelassen.
X++
Erhöht X um 1
nach dessen
Auswertung
++X
Erhöht X um 1
vor dessen
Auswertung
X - -
Vermindert X um
1 nach dessen
Auswertung
- - X
Vermindert X um
1 vor dessen
Auswertung
Beispiele für Anwendung der Kurzschreibweisen:
X = 10;
Y = 2;
// Ausgangszustand der
// der Variablen
X++;
// X = X+1 = 11
Y = X++;
// Zuerst Y = X = 11
// und dann X = X+1 = 12
Y = ++X;
// Jetzt zuerst X = X+1 = 13
// und dann Y = X = 13
X += Y;
// X = X+Y = 13+13 = 26
// X = X%5 = 26%5 = 1
X += Y
Addiert Y zu X
(X=X+Y)
X %= 5;
X -= Y
Subtrahiert Y
von X (X=X-Y)
X += (--Y * 2); // Zuerst Y = Y-1 = 12
// dann X = X+12*2 = 1+24
X *= Y
Multipliziert X
mit Y (X=X*Y)
X /= Y
Dividiert X
durch Y (X=X/Y)
X %= Y
Weist X den
Rest der
Division X/Y zu
(X=X%Y)
http://www.cpp-tutor.de/cpp/le03/le03_01.htm (2 von 5) [17.05.2005 11:38:12]
Grundrechenoperationen
Beachten Sie kleinen aber feinen Unterschied zwischen X++ und ++X bzw. X - - und - - X (siehe
auch Beispiel).
Beispiel und Übung
Das Beispiel:
Das Beispiel berechnet zunächst den Zins, den eine fixe Einlage von 66,- EUR bei 3% nach 5 und 6
Jahren bringt nach folgender Formel (ohne Zinseszins):
Zins = Betrag*Zinssatz*Jahre/100
Zum Schluss wird der Zinssatz auf 3.25% erhöht und dann erneut der Zins für 6 Jahre berechnet.
Alle Ausgaben werden auf 2 Nachkommastellen begrenzt. Beachten Sie bitte, dass setprecision(n)
alleine die Anzahl der auszugebenden Stellen einer Gleitkommazahl festlegt. Wollen Sie die Anzahl
der Nachkommastellen festlegen, so müssen Sie vorher noch den Manipulator fixed in den
Ausgabestream einfügen. Sie können im Beispiel versuchshalber ja auch einmal den fixed Manipulator
entfernen und sich dann die Ausgabe ansehen.
Das unten stehende Beispiel finden Sie auch unter: le03\prog\Brechop
Die Programmausgabe:
66.00 EUR bei 3.00% nach 5.00 Jahren 9.90 EUR Zinsen
66.00 EUR bei 3.00% nach 6.00 Jahren 11.88 EUR Zinsen
66.00 EUR bei 3.25% nach 6.00 Jahren 12.87 EUR Zinsen
Das Programm:
// C++ Kurs
// Beispiel zu Grundrechenoperationen
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
// Konstante fuer Betrag definieren
const double amount = 66.0;
// Zinssatz (in %) definieren und
// Anfangswert initialisieren
http://www.cpp-tutor.de/cpp/le03/le03_01.htm (3 von 5) [17.05.2005 11:38:12]
Grundrechenoperationen
double interestLoan = 3.0;
// Variable fuer aktuelles Jahr definieren
// und mit Anfangswert initialisieren
double year = 5.0;
// Variable fuer Zinsen definieren
double interest;
// Hauptprogramm
int main ()
{
// Anzahl der Nachkommastellen auf 2 begrenzen
cout << std::setprecision(2) << std::fixed;
// Zinsen nach folgender Formel berechnen
// Zinsen = (Betrag*Zinssatz*Jahre)/100
// Zinsen nach 5 Jahren berechnen
interest = (amount*interestLoan*year)/100.0;
cout << amount << " EUR bei " << interestLoan <<
"% nach " << year <<
" Jahren " << interest << " EUR Zinsen\n";
// nun Zinsen nach 6 Jahren berechnen
year++;
interest = (amount*interestLoan*year)/100.0;
cout << amount << " EUR bei " << interestLoan <<
"% nach " << year <<
" Jahren " << interest << " EUR Zinsen\n";
// das gleiche bei 3.25% Zinsen
interestLoan += 0.25;
interest = (amount*interestLoan*year)/100.0;
cout << amount << " EUR bei " << interestLoan <<
"% nach " << year <<
" Jahren " << interest << " EUR Zinsen\n";
}
Die Übung:
Nun einmal etwas Technik:
Berechnen Sie den elektrischen Widerstand für folgende Kupferleitungen:
●
●
●
Länge 10m, Querschnitt 1qmm
Länge 10m, Querschnitt 2qmm
Länge 100m, Querschnitt 2qmm
Die Formel zur Berechnung des Leitungswiderstands lautet:
http://www.cpp-tutor.de/cpp/le03/le03_01.htm (4 von 5) [17.05.2005 11:38:12]
Grundrechenoperationen
Leitungswiderstand = Kupferwiderstand * Länge /Querschnitt
Für den Kupferwiderstand ist Konstante 0.0172 einzusetzen. Die Angabe der Länge innerhalb der
Formel erfolgt in m und des Querschnitts in qmm.
Wenn Sie die Berechnungen richtig durchgeführt haben, so sollten Sie die unten angegebenen Werte
erhalten.
Die Programmausgabe:
Widerstand bei 10.00m und 1.00qmm: 0.17 Ohm
Widerstand bei 10.00m und 2.00qmm: 0.09 Ohm
Widerstand bei 100.00m und 2.00qmm: 0.86 Ohm
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le03\Lrechop.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le03/le03_01.htm (5 von 5) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Bit- und Schiebeoperationen
Die Themen:
Einleitung
Bitoperationen
Schiebeoperationen
Kurzschreibweisen
Beispiel und Übung
Einleitung
Bisher wurden Operationen nur auf Bytes oder einem Vielfachen davon (char, short, int, long)
ausgeführt. C++ bietet jedoch auch die Möglichkeit, einzelne Bits von Ganzzahlen mithilfe von
Bitoperationen zu beeinflussen.
Da Bitoperationen nur auf Ganzzahlen zulässig sind, können mit ihnen sizeof(char), sizeof(short),
sizeof(int) oder sizeof(long) Bits gleichzeitig beeinflusst werden, abhängig davon, ob die
Bitoperation auf einen char, short, int oder long Datentyp angewandt wird. Bis auf den
Bitoperator 'Schiebe rechts' arbeiten alle Bitoperatoren vorzeichenunabhängig, d.h. sie wirken
auf signed und unsigned Daten gleich.
Bitoperationen sind binäre Operationen, d.h. die Ganzzahl wird hier als eine
Kombination von 0 und 1 Bits ausgewertet.
Bevor es jetzt mit den Operationen los geht, nochmals zur Wiederholung die binäre Darstellung
von Ganzzahlen anhand einer unsigned char Variablen (Annahme: sizeof(char) = 8 Bit).
Zahl: 10 => Hex: 0x0A => binär: 0000 1010
Zahl: 240 => Hex: 0xF0 => binär: 1111 0000
Zahl: 131 => Hex: 0x83 => binär: 1000 0011
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (1 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Sehen wir uns nun die Bitoperatoren der Reihe nach an.
Bitoperationen
Um einzelne Bits innerhalb einer Ganzzahl zu beeinflussen, stehen die folgenden Bitoperationen
zur Verfügung:
UND-Operation:
Zwei Ganzzahlen werden durch den
Operator & UND-verknüpft.
Ergebnis = Operand1 &
Operand2;
Die UND-Verknüpfung liefert als
Ergebnis nur an den Stellen ein 1-Bit, an
denen beiden Operanden ein 1-Bit
besitzen.
ODER-Operation:
Zwei Ganzzahlen werden durch den
Operator | ODER-verknüpft.
Ergebnis = Operand1 |
Operand2;
Die ODER-Verknüpfung liefert als
Ergebnis an den Stellen ein 1-Bit, an
denen mindestens einer der Operanden
ein 1-Bit besitzt.
EXCLUSIV-ODER-Operation:
Zwei Ganzzahlen werden durch den
Operator ^ EXCLUSIV-ODERverknüpft.
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (2 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Ergebnis = Operand1 ^
Operand2;
Die EXCLUSIV-ODER-Verknüpfung
liefert als Ergebnis an den Stellen ein 1Bit, an denen beide Operanden
unterschiedliche Bits besitzen
Negationsoperation (1erKomplement):
Zum Invertieren aller Bits eines
Operanden wird der Operator ~
verwendet.
Ergebnis = ~Operand;
Das Ergebnis enthält an den Stellen ein 1Bit, an denen der Operand ein 0-Bit
besitzt und umgekehrt. Man bezeichnet
diese Operation auch als Bildung des 1erKomplements.
Sollten Sie Schwierigkeiten haben das Symbol ^ für den Exlusive-Oder Operator zu
finden: es befindet sich auf der deutschen Tastatur neben der 1-Taste. Um das Symbol ^
zu erhalten, drücken Sie zuerst die Taste ^ und dann die Leertaste!
Sie können die Wirkungsweise der Operatoren nun testen, in dem Sie oben rechts zwei 2-stellige
Hex-Zahlen eingeben und dann den entsprechenden Button für die jeweilige Operation
drücken.
Schiebeoperationen
Außer den Bitoperationen stellt C++ auch noch Operatoren zur Verfügung, um die Bits einer
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (3 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Ganzzahl um eine bestimmte Anzahl von Positionen nach links oder rechts zu schieben.
SHIFT-LEFT Operation:
Eine Ganzzahl kann mithilfe des
Operators << um eine bestimmte
Anzahl von Bits nach links geschoben
werden.
Ergebnis = Ganzzahl <<
Bitanzahl;
Die rechts frei werdenden Bits werden
mit 0 aufgefüllt und Überläufe werden
verworfen.
SHIFT-RIGHT Operation:
Der Operator >> schiebt eine Ganzzahl
um eine bestimmte Bitanzahl nach
rechts.
Ergebnis = Ganzzahl >>
Bitanzahl;
Ist die Ganzzahl vorzeichenlos
(unsigned), werden die rechts frei
werdenden Bits mit 0 aufgefüllt. Bei
positiven vorzeichenbehafteten Zahlen
werden die frei werdenden Bits
ebenfalls mit 0 aufgefüllt.
Bei negativen Zahlen ist das Verhalten lt. ANSI C++ undefiniert. In der Regel werden
die frei werdenden Bits aber mit dem Vorzeichenbit (höchstwertiges Bit = 1)
aufgefüllt.
Sie können die Wirkungsweise der Operatoren nun wieder testen, in dem Sie im ersten Feld die
zu schiebende Zahl in hexadezimal und im Feld darunter die Anzahl der Schiebeposition
eingeben. Drücken dann den entsprechenden Button für die jeweilige Operation.
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (4 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Kurzschreibweisen
Genauso wie bei den Grundrechenoperationen stehen auch für die Bit- und Schiebeoperationen
Kurzschreibweisen zur Verfügung, die in der nachfolgenden Tabelle aufgeführt sind.
X &= Y
X = X&Y
X |= Y
X = X|Y
X ^= Y
X = X^Y
X <<= Y
X = X<<Y
X >>= Y
X = X>>Y
Damit sind wir am Ende dieser Lektion angelangt und es folgt das Beispiel und die Übung.
Beispiel und Übung
Das Beispiel:
Einer unsigned short Variable nVar wird der Hex-Wert 0xAA55 zugewiesen. Von dieser
Variable wird dann einmal das High-Byte und einmal das Low-Byte ausgegeben.
Hinweis: Eine unsigned short Variable besteht hier aus 2 Bytes. Diese Bytes werden durch
entsprechendes Schieben und Ausmaskieren (logisches Verundung mit einer Bitkombination)
extrahiert.
Danach werden durch Schiebeoperationen die 2er Potenzen im Bereich 20 bis 23 gebildet.
Zum Schluss wird die Zahl 244 mittels einer Schiebeoperation durch 8 dividiert.
Das unten stehende Beispiel finden Sie auch unter le03\prog\Bbitop.
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (5 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Die Programmausgabe:
High-Byte von 0xaa55: 0xaa
Low-Byte von 0xaa55: 0x55
2 hoch 0: 1
2 hoch 1: 2
2 hoch 2: 4
2 hoch 3: 8
244/8: 30
Das Programm:
// C++ Kurs
// Beispiel zu Bitoperationen
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
// Variable fuer Zerlegung definieren
unsigned short var = 0xAA55;
// Hauptprogramm
int main ()
{
// Ausgabe auf hex umstellen mit Angabe der Zahlenbasis
cout << std::hex << std::showbase;
// High-Byte der Variablen ausgeben
cout << "High-Byte von " << var << ": " << (var>>8) << endl;
// Low-Byte der Variablen ausgeben
cout << "Low-Byte von " << var << ": " << (var&0xFF) << endl;
// Ausgabe wieder auf dezimal zurueckstellen
cout << std::dec;
// 2er Potenzen ausgeben
cout << "2 hoch 0: " << (1<<0) << endl;
cout << "2 hoch 1: " << (1<<1) << endl;
cout << "2 hoch 2: " << (1<<2) << endl;
cout << "2 hoch 3: " << (1<<3) << endl;
// Zum Schluss eine Division durch 8
cout << "244/8: " << (244>>3) << endl;
}
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (6 von 7) [17.05.2005 11:38:12]
Bit- und Schiebeoperationen
Die Übung:
Es ist eine short Variable shortVar und eine char Variable charVar zu definieren. Die short
Variable ist mit 10 und die char Variable mit dem Hex-Wert 0x55 zu initialisieren.
Anschließend ist die char Variable charVar in binärer Darstellung (nur 0 und 1) auszugeben.
Verwenden Sie dazu nur Bitoperationen.
Von der short Variablen shortVar ist zunächst das 1er-Komplement zu bilden und
auszugeben. Addieren Sie zu dem so erhaltenen Ergebnis eins hinzu und geben das Ergebnis
erneut aus. Welche Zahl erhalten Sie?
Die Programmausgabe:
0x55 nach binaer: 01010101
Das 1er Komplement von 10: ??
Und eins dazuaddiert ergibt: ??
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le03\Lbitop.
In der nächsten Lektion wenden wir uns der letzten Operatorgruppe zu, den Vergleichs- und
Logikoperationen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le03/le03_02.htm (7 von 7) [17.05.2005 11:38:12]
Vergleichs- und Logikoperationen
Vergleichs- und Logikoperationen
Die Themen:
Einleitung
Vergleichsoperationen
Logikoperationen
Einleitung
Die Vergleichs- und Logikoperatoren werden hauptsächlich in Verzweigungen und
Programmschleifen eingesetzt, die später im Kurs noch behandelt werden. Beide
Operatorgruppen liefern als Ergebnis einer Operation die bool-Werte true oder false zurück.
In C++ wird außerdem bei Vergleichsoperationen jeder numerische Wert ungleich 0 als
true interpretiert und der Wert 0 als false.
Wir werden uns jetzt zunächst die Vergleichsoperatoren und danach die Logikoperatoren
ansehen.
Vergleichsoperationen
Zum Vergleichen von von Daten stehen folgende Vergleichsoperatoren zur Verfügung:
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (1 von 6) [17.05.2005 11:38:13]
Vergleichs- und Logikoperationen
KLEINER-ALS Operation:
Der Operator < vergleicht zwei
Ausdrücke auf kleiner als.
Ergebnis = Ausdruck1 <
Ausdruck2;
Er liefert als Ergebnis true, wenn die
Auswertung des Ausdrucks Ausdruck1
einen kleineren Wert liefert als die
Auswertung des Ausdrucks Ausdruck2.
GRÖSSER-ALS-Operation:
Der Operator > vergleicht zwei
Ausdrücke auf größer als.
Ergebnis = Ausdruck1 >
Ausdruck2;
Er liefert als Ergebnis true, wenn die
Auswertung des Ausdrucks Ausdruck1
einen größeren Wert liefert als die
Auswertung des Ausdrucks Ausdruck2.
GLEICH-Operation:
Der Operator == vergleicht zwei
Ausdrücke auf Gleichheit
Ergebnis = Ausdruck1 ==
Ausdruck2;
Er liefert als Ergebnis true, wenn die
Auswertung des Ausdrucks Ausdruck1
den gleichen Wert liefert wie die
Auswertung des Ausdrucks Ausdruck2.
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (2 von 6) [17.05.2005 11:38:13]
Vergleichs- und Logikoperationen
Beachten Sie unbedingt, dass der GLEICH-Operator aus zwei Zuweisungszeichen ==
besteht! Dies ist eine häufige Fehlerquelle. Mehr dazu später im Kurs bei der IFAbfrage. Außerdem werden beide Ausdrücke vor dem Vergleich wieder auf den
gleichen Datentyp konvertiert.
UNGLEICH-Operation:
Der Operator != vergleicht zwei Ausdrücke auf ungleich.
Ergebnis = Ausdruck1 != Ausdruck2;
Er liefert als Ergebnis true, wenn die Auswertung der beiden Ausdrücke unterschiedliche
Ergebnisse liefern.
KLEINER-GLEICH-Operation:
Der Operator <= vergleicht zwei Ausdrücke auf kleiner-gleich:
Ergebnis = Ausdruck1 <= Ausdruck2;
Er liefern als Ergebnis true, wenn Ausdruck1 zahlenmäßig kleiner oder gleich Ausdruck2 ist.
GRÖSSER-GLEICH-Operation:
Der Operator >= vergleicht zwei Ausdrücke auf größer-gleich:
Ergebnis = Ausdruck1 >= Ausdruck2;
Er liefern als Ergebnis true, wenn Ausdruck1 zahlenmäßig größer oder gleich Ausdruck2 ist.
Sie können die Wirkungsweise aller Vergleichsoperatoren nun einmal testen, in dem Sie in oben
rechts Werte für die beiden Variablen var1 und var2 eingegeben. Der Vergleich erfolgt dann mit
folgender Anweisung:
bool result = (var1+10) Operator (var2);
Die Klammern um die zu vergleichenden Ausdrücke sind nicht unbedingt erforderlich, wenn
Sie sich mit der Reihenfolge der Abarbeitung von Operatoren auskennen. In Zweifelsfall ist es
aber immer besser, die beiden Ausdrücke in Klammern zu setzen.
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (3 von 6) [17.05.2005 11:38:13]
Vergleichs- und Logikoperationen
Anmerkungen zu den Vergleichsoperationen:
Da es auch unter C++ nicht möglich ist, Äpfel mit Birnen zu vergleichen, werden vor einem
Vergleich die beiden Ausdrücke bei ungleichen Datentypen auf einen gemeinsamen Datentyp
konvertiert. Ohne auf die genaue Konvertierung näher einzugehen, wird in der Regel der
kleinere Datentyp an den größeren angepasst. Hierdurch kann es unter Umständen zu nicht
beabsichtigten 'Nebeneffekten' kommen.
Sehen Sie sich dazu einmal Anweisungen:
das Beispiel rechts an. Hier char var = 0x80;
wird versucht, eine char....
Variable mit dem Integer- bool res = (var == 0x80);
Literal 0x80 zu vergleichen.
Obwohl dieser Vergleich
Ergebnis:
auf den ersten Blick true als res ist false!! Die Variable var wird zuerst
Ergebnis liefern sollte, ist
vorzeichenrichtig auf den Typ int konvertiert
das Ergebnis des Vergleichs und damit ergibt sich folgender Vergleich:
false. Der Grund hierfür
0x00000080 < 0xFFFFFFB0
liegt darin, dass das Integer- oder in dezimal:
Literal standardmäßig den
128 < -80
Datentyp int hat. Und damit
wird der char-Wert der
Variable vor dem Vergleich
auf einen int-Wert
konvertiert. Und diese
Konvertierung erfolgt unter
Beachtung des Vorzeichens
des char-Werts. Da alle charWerte größer/gleich 0x80
negativ sind (7. Bit ist
Vorzeichenbit!), ergibt sich
dann der nebenan
dargestellte Vergleich.
Ferner sollten Sie beim Vergleichen von Gleitkommazahlen den folgenden Satz niemals
vergessen:
Vergleichen Sie Gleitkommazahlen niemals auf Gleichheit!
Beim Rechnen mit Gleitkommazahlen müssen Sie immer mit eventuellen Rundungsfehlern
rechnen. So kann die Auswertung des Ausdrucks (1./3.) ein anderes Ergebnis liefern wie
(20./3.*20.).
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (4 von 6) [17.05.2005 11:38:13]
Vergleichs- und Logikoperationen
Logikoperationen
Zum Verknüpfen von mehreren Bedingung (in der Regel sind dies Ergebnisse von
Vergleichsoperationen) stehen folgende Operatoren zur Verfügung:
UND-Operation:
Der logische UND-Operator besitzt das
Symbol && und hat folgende Syntax:
Ergebnis = Ausdruck1 &&
Ausdruck2;
Er liefert als Ergebnis true, wenn die
Auswertung beider Ausdrücke true
liefert.
ODER-Operation:
Der logische ODER-Operator besitzt das
Symbol || und hat folgende Syntax:
Ergebnis = Ausdruck1 ||
Ausdruck2;
Er liefert als Ergebnis true, wenn die
Auswertung mindestens eines Ausdrucks
true liefert.
NOT-Operation:
Der logische NOT-Operator besitzt das
Symbol ! (Ausrufezeichen) und hat
folgende Syntax:
Ergebnis = !Ausdruck;
Er liefert als Ergebnis true, wenn die
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (5 von 6) [17.05.2005 11:38:13]
Vergleichs- und Logikoperationen
Auswertung des Ausdrucks false liefert
und false wenn Ausdruck gleich true ist.
Und auch hier können Sie wieder die Wirkungsweise der Operatoren testen, indem Sie in den
Feldern rechts oben für die Variablen var1 und var2 verschiedene Werte eingeben und dann den
Button Berechnen anklicken.
Beachten Sie bitte, dass die Logikoperatoren UND und ODER zwei Symbole && bzw.
|| besitzen. Verwechseln die Logikoperatoren nicht mit den bereits erwähnten
Bitoperatoren UND und ODER, die jeweils nur ein Symbol & bzw. | besitzen. Die
Auswertung
(Ausdruck1) | (Ausdruck2);
liefert als Ergebnis eine Bitkombination bei der die beiden Ausdrücke bitweise
verodert werden, während die Auswertung des Ausdrucks
(Ausdruck1) || (Ausdruck2);
ein boolsches Ergebnis (true oder false) liefert, je nachdem ob mindestens einer der
Ausdrücke true als Ergebnis besitzt.
Und noch ein Hinweis: Die Anzahl der Ausdrücke, die Sie in einer Anweisung mit den
Logikoperatoren verknüpfen können, ist nicht begrenzt. Jedoch sollten Sie aus Gründen der
Übersichtlichkeit nicht all zu viele Ausdrücke in einer Anweisung verknüpfen.
Damit beenden wir diese Lektion, diesmal ohne Beispiel und Übung da diese Operatoren wie
erwähnt hauptsächlich bei Verzweigungen und Schleifen eingesetzt werden. Dort folgen dann
genügend weitere Beispiele und auch die Übungen.
Weiter geht's nun mit der Übersicht über die Reihenfolge von Operatoren.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le03/le03_03.htm (6 von 6) [17.05.2005 11:38:13]
Rangfolge der Operatoren
Rangfolge der Operatoren
Zum vorläufigen Abschluss der Behandlung der Operatoren folgt in tabellarischer Form die
Rangfolge aller Operatoren. In der Tabelle gilt: Operatoren in der Gruppe 1 werden vor den
Operatoren der Gruppe 2 ausgeführt usw. Manche der Operatoren in der Tabelle sind Ihnen im
Augenblick noch nicht bekannt, wie z.B. der Operator -> oder auch new. Sie werden
selbstverständlich im weiteren Verlaufe des Kurses noch behandelt.
Gruppe Operator
Bedeutung
Auswertung
1
::
Zugriffsoperator
links>rechts
2
(...)
Klammer
links>rechts
(...)
Funktionsaufruf
[...]
Indizierung
->
Indirekter Zugriff auf
Klassenelement
.
Direkter Zugriff auf
Klassenelement
++ und --
Post-Inkrement/Dekrement
3
! und
~
NOT und 1er-Komplement
+ und -
Vorzeichen
++ und --
Pre-Inkrement/Dekrement
&
Adressoperator
*
Dereferenzierungsoperator
sizeof(...)
Größenoperator
rechts>links
new und delete dynamische Speicherverwaltung
(...)
Typkonvertierung
4
.* und ->*
Indirekter Zugriff auf
Klassenelement
links>rechts
5
* und / und %
Arithmetische Operatoren
links>rechts
http://www.cpp-tutor.de/cpp/le03/le03_04.htm (1 von 3) [17.05.2005 11:38:14]
Rangfolge der Operatoren
6
+ und -
Arithmetische Operatoren
links>rechts
7
<< und >>
Schiebeoperatoren
links>rechts
8
< und <= und >
Vergleichsoperatoren
und >=
links>rechts
9
== und !=
Vergleichsoperatoren
links>rechts
10
&
UND Bitoperator
links>rechts
11
^
EXCLUSIV-ODER Bitoperator
links>rechts
12
|
ODER Bitoperator
links>rechts
13
&&
logischer UND Operator
links>rechts
14
||
logischer ODER Operator
links>rechts
15
?:
Bedingungsoperator
rechts>links
16
= und *= und
Zuweisungsoperatoren
/= und
%= und += und = und
&= und ^= und
|= und
<<= und >>=
17
throw
Exception auslösen
18
,
Komma-Operator
rechts>links
links>rechts
Die letzte Spalte 'Auswertung' gibt an, in welcher Reihenfolge die einzelnen Operatoren
abgearbeitet werden. So werden z.B. arithmetischen Ausdrücke mit gleichrangigen Operanden
immer von links nach rechts abgearbeitet. Zuweisungen hingegen werden immer von rechts
nach links abgearbeitet.
Denken Sie immer daran, dass Sie die Reihenfolge der Operationen durch entsprechende
Klammerung jederzeit abändern können.
http://www.cpp-tutor.de/cpp/le03/le03_04.htm (2 von 3) [17.05.2005 11:38:14]
Rangfolge der Operatoren
ACHTUNG! Schreiben Sie niemals Anweisungen in folgender Form:
nVar = ++nVar + 1;
oder
cout << *pcPtr++ << ',' << *pcPtr++ << endl;
Das Verhalten einer Anweisung ist in der Regel undefiniert, wenn eine Variable
innerhalb einer Anweisung mehrfach verändert wird! Teilen Sie dann die
Anweisung auf mehrere Anweisungen auf. Wenn Sie mehr zu diesem Thema
wissen wollen, schauen Sie einmal in Ihrer Online-Hilfe oder auf dem Internet
unter 'side-effects' oder 'sequence-points' nach.
Verlassen wir damit nun das Thema Operatoren und befassen uns in der nächsten Lektion mit
den allseits so beliebten Zeigern.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le03/le03_04.htm (3 von 3) [17.05.2005 11:38:14]
Zeiger
Zeiger
Die Themen:
Zeigerdefinition
Adressberechnung
Zeigerzugriffe
Datentyp des Zeigers
String-Literale und Zeiger
Operationen mit Zeigern
Zeiger in cout-Anweisungen
const und Zeiger
Beispiel und Übung
Zeigerdefinition
Sehen wir uns zuerst einmal an, was ein Zeiger eigentlich ist. Ein Zeiger ist eine Variable die
anstelle eines 'normalen Wertes' eine Speicheradresse, z.B. einer Variablen oder Funktion, enthält.
Ein Zeiger zeigt also immer auf eine bestimmte Stelle innerhalb des Speichers (genauer: innerhalb
des Adressraums des Prozessors). Ebenfalls als Speicher können auch Peripheriebausteine (wie z.B.
ein Baustein für die seriellen Datenübertragung) angesprochen werden, solange sie keine besondere
Adressierung benötigen.
Diese Lektion behandelt nur Zeiger auf Variablen oder feste Speicheradressen. Zeiger auf
Funktionen werden in der Lektion Funktionen behandelt.
Ein Zeiger wird wie folgt
definiert:
short *pMaxValue;
long *pCounter;
char *pKey;
<DATENTYP> *name;
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (1 von 14) [17.05.2005 11:38:15]
Zeiger
Der DATENTYP bei der Zeigerdefinition gibt an, wie die Daten zu interpretieren sind, die im
Speicher ab der im Zeiger abgelegten Adresse liegen (mehr dazu gleich). Das kleine 'Sternchen' vor
dem Zeigernamen definiert name als Zeiger und wird als Zeigeroperator bezeichnet. Ohne das
'Sternchen' würde nur eine 'normale' Variable definiert werden.
Aber Achtung bei der Mehrfach-Definition von Zeigern!
Für den Compiler spielt es keine Rolle, ob der Zeigeroperator unmittelbar hinter dem
Datentyp, zwischen Leerzeichen oder vor dem Zeigernamen steht. Setzen Sie aber
trotzdem den Zeigeroperator immer vor dem Zeigernamen. Sehen Sie sich folgendes
Beispiel dazu an, bei dem zwei Zeiger definiert werden sollen:
short* pVar1, pVar2;
Hier ist nur pVar1 ein Zeiger, da der Zeigeroperator nur vor der Variablen pVar1 steht,
während pVar2 eine gewöhnliche short Variable ist. Schreiben Sie deshalb MehrfachDefinitionen von Zeigern immer so (es hilft diese Fehlerquelle zu vermeiden):
short *pVar1, *pVar2;
Adressberechnung
Nach dem ein Zeiger definiert
ist, kann ihm z.B. die Adresse
einer Variablen zugewiesen
werden. Um die Adresse einer
Variable zu erhalten, stellen
Sie vor dem Variablennamen
den Adressoperator & (erste
Zuweisung rechts).
short var;
short *pData;
long *pCounter;
pData = &var;
pCounter = reinterpret_cast<long*>(0x0200);
Inhalt der Variablen nach der Zuweisung:
Name
Adresse
Soll ein Zeiger dagegen auf
var
eine bestimmte Adresse im
Speicher zeigen, so kann dem
Zeiger auch ein Literal oder
pData
eine benannte Konstante
zugewiesen werden. In diesem pCounter
Fall müssen Sie aber eine
entsprechende
Typkonvertierung mittels
reinterpret_cast<..>
durchführen. Innerhalb der
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (2 von 14) [17.05.2005 11:38:15]
Speicherinhalt
0x1F00
????
0x2208
0x420c
0x1F00
0x0200
Zeiger
spitzen Klammer ist der
Datentyp des Zeigers
anzugeben (einschließlich des
'Sternchen'), dem der Wert
zugewiesen werden soll. So ist
in der zweiten Zuweisung
rechts der Datentyp von
pCounter ein long*, also muss
in der spitzen Klammer
ebenfalls ein long* stehen. Den
unter C++ muss rechts vom
Zuweisungsoperator ebenfalls
ein Zeiger vom gleichen
Datentyp wie der Zeiger selbst
stehen. Ansonsten erhalten Sie
vom Compiler eine
Fehlermeldung!
Hinweis: Die Adressen im obigen Beispiel (so wie in den folgenden) sind rein willkürlich
und dienen nur zur Veranschaulichung des Zeigerinhalts.
Zeigerzugriffe
Um auf den Inhalt der
Speicherstelle zuzugreifen,
deren Adresse im Zeiger
abgelegt ist, ist vor dem
Zeigernamen der
Dereferenzierungsoperator *
anzugeben.
short var1, var2;
short *ptr1;
char *ptr2;
Steht der Zeiger links vom
Zuweisungsoperator, so wird
der rechte Ausdruck zunächst
berechnet und das Ergebnis
dann in die Speicherstelle
übertragen, deren Adresse im
Zeiger abgelegt ist. Steht
dagegen der Zeiger rechts vom
Zuweisungsoperator oder
innerhalb eines Ausdrucks, so
wird der Inhalt der
Speicherstelle ausgelesen,
deren Adresse im Zeiger
abgelegt ist. In jedem Fall
bestimmt aber der Datentyp
// Daten übertragen
var1 = 10;
var2 = *ptr1 * 2;
*ptr2 = 0xFF;
// Zeiger initialisieren
ptr1 = &var1;
ptr2 = reinterpret_cast<char*>(0x0180);
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (3 von 14) [17.05.2005 11:38:15]
Zeiger
des Zeigers die Anzahl der zu
übertragenden Bytes (siehe
nächsten Abschnitt).
Inhalt der Variablen nach der Zuweisung:
Name
Adresse
Im Beispiel rechts wird
zunächst der Zeiger ptr1 mit
der Adresse der Variablen
var1 (gleich 0x0100) geladen.
Der Zeiger ptr2 wird auf die
fixe Adresse 0x0180 gesetzt.
Beachten Sie hierbei die
Typkonvertierung!
Anschließend wird der
Variablen var1 der Wert 10
(gleich 0x0A) zugewiesen.
Danach wird aus der
Speicherstelle der Wert
ausgelesen, auf die der Zeiger
ptr1 zeigt, also aus der
Speicherstelle der Variablen
var1. Dieser ausgelesene Wert
wird dann mit 2 multipliziert
und der Variablen var2
zugewiesen. In der letzten
Zuweisung wird das Byte 0xFF
in die Speicherstelle
übertragen, deren Adresse im
Zeiger ptr2 abgelegt ist, und
das ist die Speicherstelle mit
der Adresse 0x0180. Also
enthält diese Speicherstelle
danach den Wert 0xFF.
var1
var2
0x0100
0x0154
0x0180
0x0A
0x14
0xFF
ptr1
ptr2
0x0208
0x020c
0x0100
0x0180
Datentyp des Zeigers
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (4 von 14) [17.05.2005 11:38:15]
Speicherinhalt
Zeiger
Wie bereits erwähnt spielt der
Datentyp des Zeigers eine
wichtige Rolle bei der
Übertragung der Daten in den
bzw. aus dem Speicher. Der
Datentyp bestimmt letztendlich,
wie viele Bytes übertragen
werden. Dies können Sie nun
rechts durch anklicken der
Buttons char*, short*, long* und
float* ausprobieren.
In den Beispielen wird zunächst
eine Variable des entsprechenden
Datentyps (char, short, long, float)
definiert und mit einem Wert
belegt. Anschließend wird ein
Zeiger mit der Adresse der
Variablen initialisiert. Dieser
Zeiger wird dann dereferenziert
um aus der Speicherstelle das
Datum wieder auszulesen und in
einer weiteren Variable
abzulegen. Die Anzahl der
übertragenen Bytes beträgt dabei
bei einem Zeiger vom Typ DTYP*
immer sizeof(DTYP), also bei
einem char* 1 Byte und bei einem
long* 4 Bytes.
Das Beispiel geht davon aus, dass ein short-Wert 2 Bytes belegt und ein long- bzw. floatWert 4 Bytes. Dies muss, wie schon so oft gesagt, nicht unbedingt auf allen Systemen so sein.
Außerdem werden im Beispiel die Daten so abgelegt, dass das niederwertigste Byte auch auf
der niedersten Adresse zu liegen kommt. Dies trifft z.B. auf alle Systeme mit INTELProzessoren zu. Bei anderen Prozessoren, wie z.B. von MOTOROLA, können die Bytes in
umgekehrter Reihenfolge abgelegt sein.
Beachten Sie auch, dass der Zeiger im Beispiel selbst immer 4 Bytes belegt. Er enthält unabhängig
vom Datentyp immer eine Adresse (im Beispiel 32-Bit Adresse). Bei anderen Systemen können
Adressen durchaus abweichende Längen besitzen. 'Alte DOS-Systeme' verwendeten als Adresse
einen 16-Bit Wert der um eine Segment-Information erweitert werden konnte.
Sollten Sie nun durch die all zu große Freizügigkeit der Sprache C++ (nicht exakt definierte Größe
einer Variable / eines Zeigers und Reihenfolge der Bytes im Speicher) etwas 'geschockt' sein,
machen Sie sich nichts draus. Wichtig ist im Grunde nur, dass die Daten auch so ausgelesen
werden wie Sie sie abgelegt haben. Und das beherrschen alle Compiler. Kompliziert wird's nur,
wenn Sie Daten zwischen verschiedenen Plattformen austauschen wollen.
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (5 von 14) [17.05.2005 11:38:15]
Zeiger
Der void-Zeiger
Eine Besonderheit im Zusammenhang mit Zeigern spielt der Datentyp void. Ein Zeiger auf den
Datentyp void (void*) ist ein Zeiger, der an keinen bestimmten Datentyp gebunden ist. Soll über
einen solchen void-Zeiger auf Daten zugegriffen werden, so muss dieser zuerst in einen
entsprechenden typisierten Zeiger (char*, short* usw. ) konvertiert werden, damit die Anzahl der
zu übertragenden Bytes vom Compiler berechnet werden kann. Wozu void-Zeiger letztendlich
nützlich sind werden Sie im weiteren Verlaufe des Kurses noch sehen.
Es gibt außer den hier vorgestellten Zeigertypen noch zwei weitere Arten: den
Funktionszeiger und den Memberzeiger. Diese werden in den entsprechenden Lektionen
später erklärt.
String-Literale und Zeiger
Wie Sie wissen, haben StringLiterale immer die folgende
Form:
// Definition des const char-Zeigers
const char *pText;
...
// Dem const char-Zeiger die Adresse
"Dies ist ein String- des String-Literals zuweisen
pText = "Mein String-Literal";
Literal"
...
// Weiteres String-Literal dem Zeiger zuw.
Der Datentyp des StringpText = "Ein anderes String-Literal";
Literals ist standardgemäß
const char[n] ( ), und damit
können Sie ein String-Literal
einem const char* zuweisen.
Aus Gründen der
Rückwärtskompatiblität mit
bestehendem C++ Code wurde
im ANSI-C++ jedoch auch die
Zuweisung eines StringLiterals an ein char* (also ohne
das const davor) zugelassen.
Da String-Literale automatisch durch den Compiler in einen char* bzw. const char* umgewandelt
werden können, kann überall wo ein char-Zeiger erwartet wird auch ein String-Literal angeben
werden (soweit sinnvoll).
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (6 von 14) [17.05.2005 11:38:15]
Zeiger
Der folgende Code mag auf einigen System funktionieren, erzeugt jedoch lt. ANSI-C++
ein undefiniertes Verhalten:
char *pText = "String-Literal";
*pText = 'A';
Hier wird dem char-Zeiger pText die Adresse des String-Literals (das ja eigentlich
konstante Zeichen besitzt) zugewiesen. Anschließend wird dann über diesen Zeiger
versucht, das konstante erste Zeichen zu verändern, was dann je nach System bis zum
Absturz des Rechners führen kann. Wenn Sie also in Zukunft String-Literale einem charZeiger zuweisen, dann sollten Sie dies besser wie folgt tun:
const char *pText = "String-Literal";
Hier wird schon vom Compiler verhindert dass Sie über den Zeiger pText den Inhalt des
String-Literals verändern können. Mehr über const und Zeiger gleich noch.
Operationen mit Zeigern
Die einfachste Operation mit
Zeigern ist die Zuweisung an
einen Zeiger. Hierbei müssen
Sie aber immer darauf achten,
dass an einen Zeiger nur ein
anderer Zeiger vom gleichen
Datentyp zugewiesen werden
kann. Soll eine Zuweisung mit
einem anderen Datentyp
erfolgen, sei es eine Konstante
oder ein Zeiger mit einem
anderen Datentyp, so müssen
Sie diesen zuzuweisenden Wert
erst mittels
reinterpret_cast<DTYP>(Wert)
entsprechend konvertieren.
DTYP ist der Datentyp des
Ziels, also z.B. ein char*.
// Zeigerdefinitionen
long *pLong;
char *pChar;
// Variablendefinition
long lVar;
// Zuweisung long* an long*
pLong = &lVar;
// Zuweisung long* an char*
pChar = reinterpret_cast<char*>(pLong);
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (7 von 14) [17.05.2005 11:38:15]
Zeiger
Für arithmetische
Operationen mit
Zeigervariablen gelten zwei
Besonderheiten:
Es sind nur die
Operationen Addition
und Subtraktion
(einschl. deren
Kurzschreibweisen)
zugelassen. Alle
anderen Operationen
führen zu einem
Übersetzungsfehler.
Eine Addition des
Wertes X auf einen
Zeiger vom Typ
DTYP* erhöht den
Inhalt des Zeigers
(d.h. die in ihm
abgelegte Adresse) um
X*sizeof(DTYP) (siehe
Beispiele rechts). Für
die Subtraktion gilt
entsprechendes. Einen
der Gründe für dieses
Verhalten erfahren Sie
in der Lektion über
Felder im zweiten
Kapitel.
Addition auf einen char-Zeiger
char *pAny =
reinterpret_cast<char*>(0x0100);
pAny++;
pAny enthält danach den Wert 0x0101 da eine
char-Variable 1 Byte belegt.
Subtraktion von einem short-Zeiger
short *pSome =
reinterpret_cast<short*>(0x0208);
pSome -= 2;
pSome enthält danach den Wert 0x0204 da eine
short-Variable 2 Byte belegt und 2*2Bytes
subtrahiert werden.
Aber Achtung!
(*pAnother)++;
Diese Anweisung erhöht den Inhalt der
Speicherstelle die durch pAnother adressiert
wird.
Außer Addition und Subtraktion sind nur noch Vergleichsoperationen mit Zeigern erlaubt, d.h. Sie
können z.B. mit dem GLEICH-Operator == abfragen, ob ein Zeiger eine bestimmte Adresse
enthält. Beachten Sie dabei aber, dass beide Operanden eines Operators immer vom gleichen
Datentyp sein müssen. Führen Sie vorher, wie bereits erwähnt, eine eventl. notwendige
Typkonvertierung durch.
Zeiger in cout-Anweisungen
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (8 von 14) [17.05.2005 11:38:15]
Zeiger
Steht in einer cout-Anweisung
als auszugebendes Datum ein
Zeiger, so wird in der Regel
der Inhalt des Zeigers, d.h. die
in ihm abgelegte Adresse,
ausgegeben. Soll der Inhalt der
Speicherstelle die durch den
Zeiger adressiert wird
ausgegeben werden, so muss
der Zeiger dereferenziert
werden (*-Operator). Eine
Ausnahme davon bilden alle
Typen von char-Zeigern (siehe
Beispiel rechts). Bei charZeigern innerhalb einer coutAnweisung wird davon
ausgegangen, dass der Zeiger
auf einen String zeigt. Sie
wissen doch noch: Strings sind
Zeichenketten die mit einer
binären 0 abgeschlossen sind.
Fehlt die abschließende binäre
0 im String oder zeigt der charZeiger nicht auf einen String,
so wird der Speicherinhalt
solange ausgegeben, bis
zufällig eine 0 im Speicher
gefunden wird. Wollen Sie den
Inhalt des char-Zeigers
ausgeben, d.h. die in ihm
enthaltene Adresse, so müssen
Sie den Zeiger in einen
anderen Datentyp
konvertieren. Dieser Datentyp
muss dann aber
selbstverständlich genügend
groß sein (in Bezug auf die
Anzahl der Bytes), um den
Zeiger vollständig aufnehmen
zu können. Im rechten Beispiel
wird der char-Zeiger dazu in
einen void-Zeiger konvertiert.
Um auf ein einzelnes Byte der
Speicherstelle zuzugreifen, die
über den char-Zeiger
adressiert wird, ist der Zeiger
zu dereferenzieren. Der Wert
#include <iostream>
using namespace std;
// char-Zeiger mit Adresse des
// Strings "ABCD" initialisieren
char *pText = "ABCD";
int main()
{
// Alle Ausgaben in Hex mit Zahlenbasis
cout.setf(ios::showbase);
cout << hex;
// Den String ausgeben, auf den
// der Zeiger zeigt
cout << "Textausgabe ueber Zeiger: " <<
pText << endl;
// Nun den Inhalt des Zeigers
// (Adresse) ausgeben
cout << "Inhalt des Zeigers: "
<< static_cast<void*>(pText)
<< endl;
// Den Hex-Wert der entsprechenden
// Speicherstelle ausgeben
cout << "Datenausgabe ueber Zeiger: "
<< static_cast<int>(*pText)
<< endl;
Programmausgabe:
Textausgabe ueber Zeiger: ABCD
Inhalt des Zeigers: 0046C01C
Datenausgabe ueber Zeiger: 0x41
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (9 von 14) [17.05.2005 11:38:15]
Zeiger
wird dann standardmäßig als
ASCII--Zeichen dargestellt.
Für eine numerische
Darstellung ist wiederum eine
Typkonvertierung (im Beispiel
wird dazu z.B. der Datentyp
int verwendet) notwendig.
Mehr zu der im Beispiel verwendeten Typkonvertierung static_cast<...> gleich in der
nächsten Lektion.
const und Zeiger
Wie Sie bereits im Kurs weiter vorne erfahren haben, werden für nicht veränderliche Werte in
einem Programm Konstanten verwendet ( ). Und das gilt auch für Zeiger. Nur ist die Sache hier
etwas komplizierter, oder scheint auf den ersten Blick jedenfalls so.
Bei Zeigern müssen 3 Fälle unterscheiden: entweder ist der Zeiger konstant oder das worauf er
zeigt ist konstant oder sowohl der Zeiger wie auch das worauf er zeigt ist konstant. Sehen wir uns
die entsprechenden Zeiger-Definitionen einmal an:
Zeigerdefinition
Bedeutung
const DTYP* Ptr;
Zeiger zeigt auf Konstante vom Typ DTYP; der Zeiger kann verändert
werden.
DTYP* const Ptr;
Zeiger zeigt auf Variable vom Typ DTYP; der Zeiger selbst ist konstant.
const DTYP* const Ptr;
Zeiger zeigt auf Konstante vom Typ DTYP; der Zeiger selbst ist
ebenfalls konstant.
Rechts sehen Sie jetzt zu jedem
Fall ein Beispiel. Sehen Sie sich
genau an, wie ein Zeiger auf
eine Konstante definiert wird
und wie ein konstanter Zeiger.
Sie können sich diesen
'komplizierten' Sachverhalt
am besten merken, wenn Sie
eine Zeigerdefinition von
rechts nach links lesen. So
bedeutet z.B. die Anweisung
// 'normale' char Variable
char nonConst = 'a';
// Zeichenkonstante
const char constChar = 'A';
// Zeiger auf char-Konstante
const char* pNcPtr1 = &constChar;
// Konst-Zeiger auf char-Variable
char* const pCPtr2 = &nonConst;
// Konst-Zeiger auf char-Konstante
const char* const pCCPtr3 = &constChar;
// Geht nicht da Zeiger auf Konstante
*pNcPtr1 = 'B';
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (10 von 14) [17.05.2005 11:38:15]
Zeiger
const char* pcPtr;
das pcPtr ein Zeiger auf ein
char ist das konstant ist. Oder
die Anweisung
char* const pcPtr;
dass pcPtr ein konstanter
Zeiger auf ein char ist.
// Ok da Zeiger nicht konstant
pNcPtr1++;
// Ok da Zeiger auf char-Variable
*pCPtr2 = 'B';
// Geht nicht da Zeiger konstant
pCPtr2++;
// Geht nicht da Zeiger auf Konstante
*pCCPtr3 = 'B';
// Geht nicht da auch Zeiger konstant
pCCPtr3++;
Ist doch ganz logisch, oder?
Zum Abschluss noch etwas zur totalen Verwirrung. Wie Sie ja bereits wissen, wird ein
Zeiger auf einen konstanten Wert wie folgt definiert:
const DTYP* Ptr;
Das Gleiche erreichen Sie aber auch durch folgende Definition:
DTYP const* Ptr;
Sie sollten in Ihren Programmen aber immer nur eine Schreibweise einsetzen damit die
Sache mit den Zeigern nicht noch unnötig komplizierter wird. Laut einer C++ Empfehlung
(keine Vorschrift!) sollten Sie die erste Schreibweise verwenden.
Und jetzt sind Sie wieder daran!
Beispiel und Übung
Das Beispiel:
Es werden zunächst ein short-Zeiger, ein char-Zeiger so wie zwei short Variablen definiert. Die
beiden short Variablen werden bei ihrer Definition gleich mit unterschiedlichen Werten
initialisiert.
Im Programm wird dann zur Kontrolle der Inhalt der beiden short Variablen ausgegeben.
Anschließend wird im short-Zeiger die Adresse der zweiten short Variable abgelegt. Über den
Zeiger wird dann der zweiten short Variable der Wert der ersten short Variablen zugewiesen. Im
Anschluss daran werden die Adressen der beiden short Variablen ausgegeben.
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (11 von 14) [17.05.2005 11:38:15]
Zeiger
Zum Schluss wird der char-Zeiger auf die Adresse eines String-Literals gesetzt und der String
über diesen Zeiger ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le03\prog\Bzeiger.
Die Programmausgabe:
nVar1: 10, nVar2: 20
nVar2 ueber pnPtr: 20
nVar2 nach Veraenderung: 10
nVar1 hat die Adresse: 00474DC0 (kann variieren!)
nVar2 hat die Adresse: 00474DC2 (kann variieren!)
pszText zeigt auf einen String
Das Programm:
// C++ Kurs
// Beispiel zu Zeigern
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
// short Zeiger definieren
short *pnPtr;
// char Zeiger definieren
const char *pszText;
// 2 short Variablen definieren
short nVar1=10, nVar2=20;
// Hauptprogramm
int main ()
{
// Inhalt der short Variablen ausgeben
cout << "nVar1: " << nVar1 << ", nVar2: " << nVar2 << endl;
// Nun Zeiger auf die zweite short Variable setzen
pnPtr = &nVar2;
// Und Inhalt der Speicherstelle ausgeben
cout << "nVar2 ueber pnPtr: " << *pnPtr << endl;
// Ueber den Zeiger der Variablen nVar2 en Wert von nVar1 zuweisen
*pnPtr = nVar1;
// nVar2 zur Kontrolle ausgeben
cout << "nVar2 nach Veraenderung: " << nVar2 << endl;
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (12 von 14) [17.05.2005 11:38:15]
Zeiger
// Und jetzt noch die Adressen der beiden short Variablen ausgeben
cout << "nVar1 hat die Adresse: " << &nVar1 << endl <<
"nVar2 hat die Adresse: " << pnPtr << endl;
// Zeiger auf einen String setzen und String dann ausgeben
pszText = "String";
cout << "pszText zeigt auf einen " << pszText << endl;
}
Die Übung:
Definieren Sie einen char-Zeiger und eine long-Variable. Die long-Variable ist mit dem Hex-Wert
0x12345678L zu initialisieren.
Stellen Sie dann die Ausgabe auf Hex um. Zusätzlich soll bei allen nachfolgenden Ausgaben die
eingestellte Zahlenbasis mit ausgegeben werden (cout-Flags!).
Geben Sie zunächst den Inhalt der long Variablen aus. Anschließend ist der Inhalt der long
Variable in Byte-Darstellung auszugeben, so wie unten angegeben.
Hinweis: Verwenden Sie dazu den char-Zeiger. Die unten stehende Ausgabe gilt nur bei
Prozessoren, bei denen das Low-Byte auch auf der niederen Adresse liegt. Bei anderen
Prozessoren erhalten Sie eine umgekehrte Ausgabe.
Zum Schluss ist der char-Zeiger auf die Adresse des String-Literal "ABCD" zu setzen. Der
String ist dann mithilfe des char-Zeigers in einzelne Buchstaben zu zerlegen und auszugeben
(siehe Programmausgabe unten).
Die Programmausgabe:
0x12345678 liegt wie folgt im Speicher:
0x78,0x56,0x34,0x12
String ist: ABCD
In Buchstaben: A,B,C,D
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le03\Lzeiger.
Haben Sie die Lösung? War schon etwas schwieriger, oder? Dafür sind Sie aber am Ende der
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (13 von 14) [17.05.2005 11:38:15]
Zeiger
dritten Lerneinheit angekommen. In der nächsten Lerneinheit sehen wir uns zunächst die
Konvertierung zwischen unterschiedlichen Datentypen an und danach geht's ans Einlesen von
Daten.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le03/le03_05.htm (14 von 14) [17.05.2005 11:38:15]
Typkonvertierungen
Typkonvertierungen
Die Themen:
Einleitung
C++ Typkonvertierungen
Automatische Typkonvertierung (Promotions)
Einleitung
Kommen wir nun zu einem recht heiklen Thema, der Typkonvertierung. Mittels
Typkonvertierung kann der ursprüngliche Datentyp z.B. einer Variable oder Konstante (im
Folgenden Datum genannt) in einen anderen Datentyp konvertiert werden. Wie Sie in der
Zwischenzeit bereits erfahren haben, wird bei Operationen mit ungleichen Datentypen soweit
wie möglich eine automatische Typkonvertierung durch den Compiler vorgenommen, da
Operatoren nur Daten mit gleichen Datentypen verarbeiten können. Wir wollen uns in dieser
Lektion ansehen wie Sie explizit solche Typkonvertierungen durchführen und wie die
automatische Typkonvertierung arbeitet.
Wie stand es irgendwo einmal geschrieben: Typkonvertierung ist etwas für Experten;
da müssen Sie dem Compiler sagen was er tun soll! Und dieser Satz trifft genau ins
Schwarze. Durch eine unüberlegte Typkonvertierung können Sie Ihr Programm
relativ leicht zum Absturz bringen.
Alte Typkonvertierung im C-Stil
Fangen mit der 'alten' Form der Typkonvertierung an. Diese Form sollten Sie aber in Zukunft
so gut wie nicht mehr benützen. Sie enthält keinerlei Sicherheitsabfragen (d.h. die Datentypen
können beliebig umgewandelt werden) und wird hier nur der Vollständigkeit halber aufgeführt.
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (1 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
Der Datentyp eines Datums
oder Ausdruck kann mit
folgenden Anweisungen
explizit in einen anderen
Datentyp überführt
werden:
(neuer_Datentyp)
Datum bzw.
Ausdruck
neuer_Datentyp
(Datum) bzw.
(Ausdruck)
Ausgangszustand:
short var1 = 10;
short var2 = 4;
double result1, result2;
Berechnungen:
result1 = (double)var1 / (double)var2;
result2 = (double)(var1 / var2);
Ergebnisse:
result1 = 10.0 / 4.0
= 2.5
result2 = (double)(10 / 4)
= (double)(2)
= 2.0
Bei der Konvertierung
einer Gleitkommazahl in
eine Ganzzahl werden die
Nachkommastellen
abgeschnitten, es erfolgt
keine Rundung. Beachten
Sie dies bitte bei Ihren
Berechnungen.
Rechts sehen Sie ein
Beispiel mit der 'alten'
Typkonvertierung. Wie Sie
dem Beispiel auch
entnehmen können, ist es
nicht immer gleichgültig,
wann Sie was konvertieren.
Im Beispiel rechts liefert die
Division von Ganzzahlen
ebenfalls eine Ganzzahl,
auch wenn Sie diese
anschließend in eine
Gleitkommazahl
umwandeln.
C++ Typkonvertierungen
C++ kennt folgende (neuen) Typkonvertierungen, die Sie in Zukunft einsetzen sollten wenn Sie
schon Daten konvertieren müssen:
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (2 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
const_cast<DTYP>(ARG)
static_cast<DTYP>(ARG)
dynamic_cast<DTYP>(ARG)
reinterpret_cast<DTYP>(ARG)
DTYP ist auch hier der neue Datentyp, in den der Ausdrucks ARG konvertiert werden soll. Jede
dieser Typkonvertierung wird für ganz spezielle Konvertierungen eingesetzt.
const-cast Konvertierung
Sehen wir uns die neuen
Typkonvertierung an und
beginnen mit dem
einfachsten Fall, der
const_cast Konvertierung.
Der const_cast Operator
dient dazu, den const oder
volatile Modifizierer von
einem Datentyp zu
entfernen. Dabei darf sich
der neue Datentyp vom
Ursprungsdatentyp nur
durch den Modifizierer
const oder volatile
unterscheiden. Alle anderen
Konvertierungen führen zu
einem Fehler beim
Übersetzen des Programms.
Als Ergebnis liefert
const_cast den auf DTYP
konvertierten Ausdruck.
Die erste Zuweisung links
liefert einen Fehler, da ein
Zeiger auf einen konstanten
Text nicht einem Zeiger auf
einen veränderlichen Text
direkt zugewiesen werden
kann. Wie's trotzdem geht
ist in der darunter
stehenden Anweisung
dargestellt, hier wird der
const-Modifizierer entfernt.
// Zeiger auf konstante Zeichen
const char *pConst = "Text";
// Zeiger auf veraenderbare Zeichen
char *pVar
// Das schlägt fehl!
pVar = pConst;
// Aber so geht's
pVar = const_cast<char*>(pConst);
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (3 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
ACHTUNG! Wenn Sie von einem Datentyp das const-Attribut entfernen sollten Sie
genau wissen was Sie tun! In der Regel führt das Entfernen des const-Attributs nicht
automatisch dazu, dass Sie danach die ursprüngliche Konstante verändern können.
Manche Compiler legen Konstanten in einem Read-Only Speicher (z.B. Flash oder
EEPROM) ab. Und Schreibzugriffe auf einen solchen Speicher können bis zum
Programmabsturz führen!
static_cast Konvertierung
Der static_cast Operator
wird eingesetzt um
zwischen GanzzahlDatentypen und
Gleitkomma-Datentypen zu
konvertieren. Die erste
Anweisung konvertiert ein
long-Datum in ein charDatum. Beachten Sie
hierbei bitte, dass nur das
niederwertigste Byte
übernommen wird; der
Rest wird verworfen.
Ebenfalls eingesetzt wird
der static_cast Operator um
einen void-Zeiger (datentyploser Zeiger) in einen
beliebigen anderen Zeiger
(datentyp-gebundenen
Zeiger) zu konvertieren. So
konvertiert die letzte
Anweisung rechts den voidZeiger pVal in den intZeiger pVar.
// Konvertierung von long nach char
long longVar = ..;
char charVar = static_cast<char>(longVar);
// void-Zeiger-Konvertierung
void *pVal = ..;
int *pVar = static_cast<int*>(pVal);
Und zu guter Letzt kann der static_cast Operator noch eine Ganzzahl in einen enum-Wert
konvertieren. Allerdings müssen Sie dabei selber darauf achten, dass die zu konvertierende
Ganzzahl innerhalb der in der enum-Anweisung definierten Konstanten liegt. Enum-Datentypen
werden später noch behandelt.
Hinweis: Der static_cast Operator kann auch dazu verwendet werden, Objektzeiger zu
konvertieren deren Klassen in einer Beziehung zueinander stehen (Stichwort:
Ableitung). Mehr dazu aber in einem späteren Kapitel noch.
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (4 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
reinterpret_cast Konvertierung
Sehen wir uns als Letztes in
dieser Lektion noch den
reinterpret_cast Operator
an. Typkonvertierungen
mittels reinterpret_cast sind
die 'gefährlichsten', da sie
in der Regel
plattformabhängige
Konvertierungen
durchführen. Der
reinterpret_cast Operator
wird zum einen für die
Konvertierung zwischen
verschiedenen Zeigertypen
und zum anderen für die
Konvertierung von
Ganzzahlen in Zeiger und
umgekehrt eingesetzt.
// Definitionen
char *pText = "Text";
void *pAny;
const int CONST = 10;
long lVal;
// Konvertierungen Zeiger nach long
lVal = reinterpret_cast<long>(pText);
// Konvertierung nach char-Zeiger
pText = reinterpret_cast<char*>(CONST);
// Konvertierung zwischen Zeigern
pText = reinterpret_cast<char*>(pAny);
dynamic_cast Konvertierung
Der letzte Konvertierungsoperator dynamic_cast wird erst nach der Behandlung von
abgeleiteten Klassen beschrieben, da er nur in diesem Zusammenhang von Bedeutung ist. Er
wird nur der Vollständigkeit wegen an dieser Stelle erwähnt.
Automatische Typkonvertierung (Promotions)
Viele 'alltägliche' Typanpassungen kann der Compiler selbst vornehmen, wenn dadurch keine
Information verloren geht. Beachten Sie bitte, dass vor dem Ausführen einer Operation (und
auch eine Zuweisung ist eine Operation) immer die Datentypen der Operanden angepasst
werden. Die Anpassung erfolgt in der Weise, dass der 'kleinere' Datentyp auf den 'größeren'
erweitert wird.
Ohne jetzt auf alle Einzelheiten einzugehen, die im Standard ca. 8 DIN-A4 Seiten belegen, gibt
es folgende wichtige Konvertierungen.
Integral Promotion
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (5 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
Werte vom Datentyp char,
unsigned char, signed char,
unsigned short und signed
short können vom Compiler
auf den Datentyp int
erweitert werden, wenn
dadurch der gesamte
Wertebereich des
Originaldatentyps
abgedeckt wird. Ist dies
nicht der Fall, so wird eine
Anpassung auf einen
unsigned int Datentyp
versucht. Ebenfalls können
enum und wchar_t
Datentypen in int bzw. long
(sowohl signed wie auch
unsigned) umgewandelt
werden. Und zum Schluss
kann ein bool Wert noch in
einen int Wert konvertiert
werden, wobei für true
gleich 1 und für false gleich
0 verwendet wird.
char var = 0x55;
if (var == 0x80)
// Vergleich von 2 ints!
bool bVar = true;
int nVar = bVar;
// Konvertierung auf int
Floating Promotion
Ein Wert vom Typ float kann bei Bedarf auf einen Wert vom Datentyp double konvertiert
werden.
Floating-Integral Konvertierung
Ein Gleitkommawert kann in einen Ganzzahlwert konvertiert werden, wobei die
Nachkommastellen einfach abgeschnitten werden. Kann der Ganzzahlwert nicht exakt als
Gleitkommawert dargestellt werden, so ist die entsprechende Rundung vom Compiler
abhängig.
Zeiger Konvertierungen
Ein Zeiger eines beliebigen Datentyps kann immer in einen void-Zeiger (datentyp-loser Zeiger)
konvertiert werden. Außerdem kann ein Zeiger auf eine abgeleitete Klasse stets in einen Zeiger
auf die entsprechende Basisklasse konvertiert werden. Dieser Sachverhalt spielt später im
Kurses noch eine wichtige Rolle.
Und damit sind wir am Ende dieser Lektion angelangt, ganz ohne weiteres Beispiel und Übung
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (6 von 7) [17.05.2005 11:38:16]
Typkonvertierungen
diesmal.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/le04_01.htm (7 von 7) [17.05.2005 11:38:16]
Eingabestream cin
Eingabestream cin
Die Themen:
Einleitung
Eingabestream cin
Eingabe von numerischen Daten und Texten
Eingabe von Zeilen
Fehlerfälle
Beispiel und Übung
Einleitung
Für die Standard-Eingabe wird in C++ Programme vorzugsweise den Eingabestream cin verwendet. Die StandardEingabe entspricht in der Regel der Eingabe über die Tastatur.
Die Standard-Eingabe
Tastatur cin Programm
Eingabestream cin
Genauso wie der Ausgabestream cout ist auch der Eingabestream cin eine Instanz einer Streamklasse. Wenn Sie diesen
Stream einsetzen, müssen Sie die gleiche Datei iostream einbinden die Sie auch für den Ausgabestream cout benötigen.
Die allgemeine Syntax für die formatierte Eingabe mittels cin lautet:
cin >> var1 [ >> var2 [ >> var3...]];
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (1 von 7) [17.05.2005 11:38:17]
Eingabestream cin
Nach dem Eingabestream cin folgt der Operator >>
und dann die einzulesende Variable. Mehrere
einzulesende Variablen werden einfach durch
entsprechende Wiederholungen aneinander
gehängt. Die Datentypen der Variablen können
dabei beliebig 'gemischt' sein. Sie müssen dann bei
der Eingabe 'nur' darauf achten, dass die
eingegebenen Werte sich in den dazugehörigen
Variablen abspeichern lassen. Was bei fehlerhaften
Eingaben passiert, dass erfahren Sie gleich noch.
// Datei einbinden
#include <iostream>
using namespace std;
// Variablen definieren
short var1;
long var2;
// Hauptprogramm
int main ( )
{
....
// 2 Daten einlesen
cin >> var1 >> var2;
....
}
Eingabe von numerischen Daten und Texten
Bei der Eingabe von mehreren Daten, werden die einzelnen Daten werden durch Leerzeichen voneinander getrennt
eingegeben. Die Zuordnung Eingabe zur Variable erfolgt von links nach rechts, d.h. die erste Eingabe wird auch der
ersten Variable zugewiesen (wenn kein Eingabefehler vorliegt).
Obwohl Feldern später noch in einer eigenen Lektion behandelt werden, benötigen wir in dieser Lektion schon
char-Felder zum Abspeichern von alphanumerischen Eingaben. char-Felder werden wie folgt definiert:
char myArray[SIZE];
myArray ist der Name des Feldes und SIZE die Feldgröße. In einem so definierten Feld können dann unter
anderem maximal SIZE-1 Zeichen abgelegt werden. Die -1 rührt daher, dass C-Strings immer mit einer binären
0 abgeschlossen werden, und die muss schließlich ja auch im Feld abgespeichert werden.
Standardmäßig werden alle numerischen Eingaben Einlesen eines Hex-Wertes:
im Dezimalformat erwartet. Sie können jedoch mit
short var;
den bekannten Manipulatoren dec, hex und oct eine
cin >> hex >> var;
andere Zahlenbasis für die Eingabe einstellen.
(siehe Beispiel rechts). Die eingestellten
Einlesen eines Oktal-Wertes:
Zahlenbasen für cout und cin arbeiten unabhängig
short var;
voneinander.
cin >> oct >> var;
Beim Einlesen von Wörtern (Text) werden die
Einlesen zweier Wörter (mit je max. 9 Buchstaben!):
Wörter (jedes Wort ist ein einzelner C-String!)
char array1[10], array2[10];
durch Leerzeichen von einander getrennt. Jedes
cin >> array1 >> array2;
Wort im Beispiel rechts wird dabei in einem eigenen
char-Feld abgelegt das, wie bei C-Strings üblich, mit
einer binären Null abgeschlossen wird.
ACHTUNG! Die oben dargestellte Eingabe ist zwar syntaktisch richtig und funktioniert auch. Jedoch werden
Sie Schwierigkeiten bekommen, wenn ein Wort einmal länger ist als Feldgröße-1 ist (-1 wegen der
abschließenden 0!). Wie's besser geht erfahren Sie gleich noch.
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (2 von 7) [17.05.2005 11:38:17]
Eingabestream cin
Eingabe von Zeilen
Da beim Eingabestream cin die einzelnen Eingaben durch Leerzeichen voneinander getrennt werden, können Eingaben
die selbst Leerzeichen enthalten nicht direkt eingelesen werden. Soll eine komplette Zeile eingelesen werden, so kann
hierfür die cin-Memberfunktion getline(...) verwendet werden. getline(...) besitzt prinzipiell folgende
Funktionsdeklaration:
istream& getline (char *pBuffer, int noOfChars);
getline(...) erhält im ersten Parameter die Adresse des char-Feldes übergeben, in dem die Eingabe abgelegt werden soll.
Der zweite Parameter definiert die maximale Anzahl der einzulesenden Zeichen plus 1. Wenn Sie z.B. maximal 10
Zeichen einlesen wollen, so müssen Sie hier den Wert 11 übergeben (und auch das char-Feld mit mindestens 11
Elementen definieren!). Der Grund für dieses Verhalten ist, dass getline(...) die Eingabe wiederum als C-String ablegt,
und der wird bekanntermaßen immer mit einer binären 0 abgeschlossen (was man nicht oft genug wiederholen kann!).
Und zu getline(...) noch ein Beispiel. Zunächst wird
ein char-Feld mit der Größe SIZE (gleich 81)
definiert, in das dann eine komplette Zeile
eingelesen wird. Sie können also 'korrekterweise'
SIZE-1 Zeichen einlesen, die dann als String im charFeld abgelegt werden. Werden mehr als SIZE-1
Zeichen eingegeben, so werden SIZE Zeichen im
char-Feld abgelegt. Die restlichen eingegebenen
Zeichen verbleiben zunächst im Eingabepuffer. Da
nun SIZE Zeichen übernommen wurden, fehlt dann
natürlich die abschließende binäre 0, die ja für den
Abschluss eines Strings erforderlich ist. Diese
binäre 0 wird im Beispiel zur Sicherheit immer als
letzte Zeichen im char-Feld eingetragen, um den
String auf jeden Fall abzuschließen. Anschließend
wird die restliche Eingabe im Eingabepuffer mittels
cin.seekg(...) 'verworfen'. Na ja, eigentlich wird sie
nicht verworfen, sondern es werden nur die im
Eingabepuffer sich noch befindlichen Zeichen
übersprungen. Mehr zur seekg(...) Memberfunktion
erfahren Sie dann noch beim Dateistream. Und zum
Schluss wird das char-Feld dann mittels cout einfach
ausgegeben.
#include <iostream>
using namespace std;
const int SIZE = 81;
int main ()
{
// Feld zur Aufnahme der Eingabe def.
char array[SIZE];
// Hinweis ausgeben
cout << "Bitte eine Zeile eingeben: ";
// Zeile einlesen, aber max. SIZE Zeichen
cin.getline(array, SIZE);
// Zur Sicherheit 0 anfuegen
array[SIZE-1] = 0;
// Restliche Eingabe ueberspringen
cin.seekg(0,ios::end);
// Eingabe wieder ausgeben
cout << "Eingabe war: " << array <<
endl;
}
Die Ein- und Ausgaben:
Bitte eine Zeile eingeben: Ein Satz. Und Ende
Die Eingabe war: Ein Satz. Und Ende
Beachten Sie beim Einlesen mittels getline(...), dass alle Eingaben als ASCII-Zeichen im char-Feld abgelegt werden.
Wenn Sie also die Werte
2 12<RETURN>
eingeben, so liegen die Werte als String "2 12" vor. Wie Sie diese 'String-Werte' in numerische Werte konvertieren
können, das erfahren Sie später noch.
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (3 von 7) [17.05.2005 11:38:17]
Eingabestream cin
Die genaue Funktionsdeklaration der cin-Memberfunktion getline(...) und was die Memberfunktion noch so
alles zu leisten vermag, das erfahren Sie wenn Sie links das Symbol anklicken.
Verwenden Sie innerhalb eines Programms die formatierte Eingabe (cin >> ...) und unformatierte Eingabe
(cin.getline(...)), so beachten Sie unbedingt die folgenden Ausführungen!
Sehen Sie sich einmal das Beispiel rechts an. Dort
wird zunächst formatiert ein Text in das Feld array
eingelesen. Anschließend soll nun eine komplette
Zeile (unformatiert) eingelesen werden. Wenn Sie
dieses Code-Stückchen laufen lassen würden, so
würde das Einlesen der Zeile fehlschlagen!
char array[80];
...
// Formatierte Eingabe
cin >> array;
...
// Unformatierte Eingabe
cin.getline(array,sizeof(array));
Was passiert hier? Überlegen Sie sich einmal, welche Tasten Sie bei der ersten, formatierten Eingabe gedrückt haben.
Nun?
Die letzte gedrückte Taste war natürlich die <RETURN>-Taste, d.h. im Eingabepuffer liegt der eingegebene Text und
das abschließende RETURN. Bei der formatierten Eingabe wird nun nur der Text aus dem Eingabepuffer geholt, das
RETURN bleibt noch darin liegen. Wird dann unformatiert mit getline(...) eingelesen, so findet es zunächst dieses
RETURN, und damit ist für getline(...) die Sache erledigt! Was wird bräuchten wäre eine Funktion, die uns den
Eingabepuffer vorher leert.
Und selbstverständlich gibt es dass. Die
aufzurufenden cin-Memberfunktion heißt seekg(...)
und wird wie rechts angegeben aufgerufen.
Zusätzlich müssen Sie dann nach seekg(...) noch die
cin-Memberfunktion clear() aufrufen, um den
Status des Eingabestreams zurückzusetzen.
// Eingabepuffer leeren
cin.seekg(0,std::ios::end);
cin.clear();
// Weiter mit nnformatierter Eingabe
cin.getline(array,sizeof(array));
Wenn Sie also formatiert und unformatiert einlesen, stellen Sie jeder unformatierten Eingabe die Memberfunktionen
seekg(...) und clear(...) voran und nichts sollte mehr schief gehen.
Mehr zu den seek-Memberfunktionen können Sie nachher noch bei den Dateizugriffen nachlesen.
Fehlerfälle
Soll mittels cin ein numerischer Wert eingelesen
werden und es wird stattdessen ein nichtnumerischer Ausdruck eingegeben, so behält die
einzulesende Variable ihren ursprünglichen Wert.
Die Eingabe wird dann der nächsten cin Anweisung
'zugewiesen', die alphanumerische Eingaben
verarbeitet (siehe erste Eingabe rechts). Diesen
Fehlerfall können Sie aber abfangen, indem Sie
nach dem Einlesen die Memberfunktion fail(...)
aufrufen. Liefert die Memberfunktion true zurück,
so war die Eingabe fehlerhaft. Beachten Sie aber,
dass Sie danach unbedingt den Fehlerstatus des
Eingabestreams cin mittels clear(...) löschen müssen
und dass die Eingabe noch im Eingabepuffer steht!
Auszuführende Anweisung:
short var;
cin >> var;
if (cin.fail())
{ // Fehler loeschen und
// Eingabe ueberspringen
cin.clear();
cin.ignore(std::numeric_limits<int>::max(),
'\n');
}
Eingabe ist:
MaxMaier
Ergebnis:
var behält ihren ursprünglichen Inhalt
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (4 von 7) [17.05.2005 11:38:17]
Eingabestream cin
fail() liefert hier true zurück
Besteht die Eingabe eines numerischen Wertes am
Anfang aus numerischen Zeichen gefolgt von nichtnumerischen Zeichen, so erhält die einzulesende
Eingabe ist:
Variable den numerischen Teil der Eingabe
12MaxMaier
zugewiesen. Der Rest der Eingabe wird dann
Ergebnis:
wiederum an die nächste cin Anweisung
var erhält den Wert 12.
weitergeleitet die alphanumerische Eingaben
fail() liefert hier false!
verarbeitet (siehe zweite Eingabe rechts).
Dummerweise liefert die Memberfunktion fail(...)
hier false zurück, da ja die Eingabe (wenigstens
teilweise) erfolgreich war.
Wollen Sie sicher sein, dass nach jeder Eingabe der Eingabepuffer vollständig leer ist, so müssen Sie die im
Eingabepuffer noch befindlichen Zeichen 'verwerfen'. Dazu steht Ihnen die allgemeine Eingabestream-Memberfunktion
ignore(...) zur Verfügung, die folgende Funktionsdeklaration besitzt:
basic_istream<charT,traits>& ignore(int n = 1, int_type delim = traits::eof());
Lassen Sie sich von dieser komplizierten Funktionsdeklaration aber nicht erschrecken! Die Memberfunktion ignore(...)
verwirft alle Zeichen bis entweder n Zeichen verworfen wurden (1. Parameter), das Stream-Ende erreicht ist oder aber
das Abschlusszeichen gefunden wurde (2. Parameter). Das Abschlusszeichen selbst wird ebenfalls verworfen. Wenn Sie
sicher gehen wollen, dass alle Zeichen verworfen werden, so geben für die Anzahl der zu verwerfenden Zeichen dem
maximalen int-Wert (numeric_limits<int>::max()) an.
Beenden wir hier diese Lektion, aber nicht ohne das obligatorische Beispiel und die anschließende Übung.
Beispiel und Übung
Das Beispiel:
Das nachfolgende Beispiel zeigt, dass Sie nun schon in der Lage sind auch komplizierter Berechnungen
durchzuführen.
Das Beispiel dient zur Lösung eines Gleichungssystems mit 2 Unbekannten:
A1x + B1y + C1 = 0
A2x + B2y + C2 = 0
Das Gleichungssystem wird durch Gleichsetzen der beiden Gleichungen gelöst. Dazu werden beiden Gleichungen
zuerst so umgeformt, dass auf der linken Seite der Gleichungen nur noch x übrig bleibt. Die beiden umgeformten
Gleichungen werden gleichgesetzt und daraus dann y berechnet. Der so erhaltene y-Wert wird dann in eine der
beiden Gleichungen eingesetzt und daraus der x-Wert berechnet (siehe auch Kommentar im Code).
Das unten stehende Beispiel finden Sie auch unter: le04\prog\Bcin.
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (5 von 7) [17.05.2005 11:38:17]
Eingabestream cin
Die Programmausgabe:
Dieses Programm loest ein Gleichungssystem
mit zwei Unbekannten:
A1x + B1y + C1 = 0
A2x + B2y + C2 = 0
Alle Koeffizienten muessen ungleich 0 sein!
Bitte Koeffizienten der Gleichungen als float-Werte!
(A1 B1 C1)? 1. -2. -4.
(A2 B2 C2)? 2. 5. -35.
X = 10, Y = 3
Das Programm:
// C++ Kurs
// Beispiel zu cin
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::cin;
using std::endl;
// Variablen fuer 1. Gleichung definieren
double a1, b1, c1;
// Variablen fuer 2. Gleichung definieren
double a2, b2, c2;
// Normierte Koeffizienten
double b1Norm, c1Norm;
double b2Norm, c2Norm;
// Loesung der Gleichung
double x, y;
// Hauptprogramm
int main ( )
{
// Hinweis auf Programmfunktion ausgeben
cout << "Dieses Programm loest ein Gleichungssystem\nmit zwei Unbekannten:\n" <<
"\tA1x + B1y + C1 = 0\n\tA2x + B2y + C2 = 0\n" <<
"Alle Koeffizienten muessen ungleich 0 sein!\n";
cout << "Bitte Koeffizienten der Gleichungen als double-Werte!\n" <<
"(A1 B1 C1)? ";
// Koeffizienten der 1. Gleichung einlesen
cin >> a1 >> b1 >> c1;
cout << "(A2 B2 C2)? ";
// Koeffizienten der 2. Gleichung einlesen
cin >> a2 >> b2 >> c2;
// Koeffizienten normieren und Gleichung nach x aufloesen
//
x = -B1Ny - C1N
//
x = -B2Ny - C2N
b1Norm = -b1 / a1;
c1Norm = -c1 / a1;
b2Norm = -b2 / a2;
c2Norm = -c2 / a2;
// Durch Gleichsetzen der Gleichungen nun Y ausrechnen
//
-B1Ny - C1N = -B2N - C2N
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (6 von 7) [17.05.2005 11:38:17]
Eingabestream cin
//
y = (C2N-C1N) / (B1N - B2N)
y = (c2Norm-c1Norm)/(b1Norm-b2Norm);
// X nun ausrechnen durch einsetzen des Y-Wertes in 1. Gleichung
x = (-b1*y - c1)/a1;
// Und Ergebnis ausgeben
cout << "X = " << x << ", Y = " << y << endl;
}
Die Übung:
Und jetzt mal etwas aus dem alltäglichen Leben.
Sie sollen in der Übung den Steueranteil und den Nettobetrag Ihres Einkaufs ausrechnen. Dazu werden der an der
Kasse bezahlte Betrag und der darin enthaltene Steuersatz in % eingegeben. Das Programm soll dann berechnen, wie
viel Steuer in dem gezahlten Betrag enthalten ist und wie viel Sie ohne Steuer hätten zahlen müssen.
Hinweis: Bei einem angenommenen Steuersatz von 7% zahlen Sie 107% an der Kasse, nämlich 100% für Ihre Ware
und 7% für das Finanzamt.
Die berechneten Beträge sind mit 2 Nachkommastellen auszugeben.
Die Programmausgabe:
Wie viel EUR haben Sie bezahlt? 233.45
Und wie hoch ist die Steuer (%)? 7.
Steueranteil: 15.27
Nettobetrag : 218.18
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die Lösung
unter loesungen\le04\Lcin.
In der nächsten Lektion sehen wir uns an, wie Sie Daten in Dateien ablegen und dann auch wieder einlesen können.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/le04_02.htm (7 von 7) [17.05.2005 11:38:17]
Dateizugriffe
Dateizugriffe
Die Themen:
Einleitung
Text- und Binärdatei
Übersicht Dateistreams
Ausgabestream-Objekt
Schreiben in Datei
Eingabestream-Objekt
Lesen aus Datei
Gleichzeitiges Schreiben und Lesen
Sonstige Dateioperationen
Beispiel und Übung
Einleitung
Genauso wie für die Standard Ein- und Ausgabe Streams verwendet werden, so werden auch
für Dateizugriffe unter C++ entsprechende Streams eingesetzt:
Ausgabe auf Datei
Datei ofstream Programm
Eingabe von Datei
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (1 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Datei ifstream Programm
Daten die in Dateien abgelegt werden, werden oft auch als persistente Daten bezeichnet,
da sie zwischen zwei Programmläufen ihre Werte beibehalten.
Bevor auf die Behandlung von Dateien eingegangen wird, müssen noch zwei Begriffe geklärt
werden: Textdatei und Binärdatei. Die Unterscheidung ist deshalb notwendig, da die beiden
Dateitypen unterschiedlich gehandhabt werden.
Text- und Binärdatei
Textdatei
Textdateien sind Dateien
die nur ASCII-Zeichen
enthalten. Diese Dateien
können mit jedem Editor
bearbeitet werden der
'reine' Textdateien
erzeugen kann (z.B. der
DOS-Editor EDIT oder
auch NOTEPAD).
Numerische Daten werden
in diesen Dateien ebenfalls
in ASCII-Form abgelegt.
Beachten Sie rechts bitte,
wie die Werte der
Variablen var1 und var2 in
der Datei abgelegt sind.
Programmanweisungen:
short var1 = 10, var2 = 0x100;
outFile << "Emil Maier" << endl;
outFile << var1 << ' ' << var2 << endl;
Datei-Inhalt nach dem Schreiben:
Hex-Darstellung ASCII-Darstellung
45 6D 69 6C 20 4D Emil M
61 69 65 72 0D 0A aier..
31 30 20 32 35 36 10 256
0D 0A
..
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (2 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Diese Dateiform
ist die einzige
Möglichkeit Daten
zwischen
unterschiedlichen
Plattformen
auszutauschen.
Das Dateiformat
der im Anschluss
erwähnten
Binärdatei ist
nicht
standardisiert.
Binärdatei
Im Gegensatz dazu sind
Binärdateien Dateien, in
denen Daten in binärer
Form, also in der Form, wie
sie im Speicher des
Rechners liegen, abgelegt
sind. Diese Daten können in
der Regel nicht mit einem
Editor bearbeitet werden
da Editoren nur Dateien
mit ASCII-Zeichen sinnvoll
darstellen können. Der
Aufbau einer Binärdatei ist,
wie bereits erwähnt, aber
nicht standardisiert, d.h.
unterschiedliche Systeme
können Daten
unterschiedlich ablegen.
Programmanweisungen:
short var1 = 10, var2 = 0x100;
outFile.write("Emil Maier",10);
outFile.write(reinterpret_cast<char*>(&var1),
sizeof(var1));
outFile.write(reinterpret_cast<char*>(&var2),
sizeof(var2));
Datei-Inhalt nach dem Schreiben:
Hex-Darstellung ASCII-Darstellung
45 6D 69 6C 20 4D Emil M
61 69 65 72 0A 00 aier..
00 01
..
Die Ausgabe von Text in
eine Binärdatei
unterscheidet sich nicht von
der Ausgabe in eine
Textdatei. Der Unterschied
wird erst bei der Ausgabe
von Daten, wie z.B. der
Variablen var1 und var2,
deutlich.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (3 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Wenn Sie viele numerische Daten verarbeiten müssen und nicht auf den Datenaustausch
mit anderen System angewiesen sind, so legen Sie die Daten am besten innerhalb einer
Binärdatei ab. Dies kann zu erheblichen Platzeinsparungen führen. Nehmen wir einmal
an, Sie müssen einen 4-stelligen short-Wert 1000-mal in einer Datei ablegen. In einer
Textdatei benötigen Sie dafür 1000*5 Bytes, gleich 5000 Bytes. Das zusätzliche 5. Byte
wird als Trennzeichen zwischen den einzelnen Werten benötigt damit die Daten später
auch wieder eingelesen werden können. Die gleiche Datenmenge innerhalb einer
Binärdatei belegt dagegen nur 1000*2 Bytes, gleich 2000 Bytes, da ein short-Wert 2 Bytes
benötigt und keine Trennzeichen notwendig sind (wie Sie nachher gleich sehen werden).
Übersicht Dateistreams
Um Dateien mit Streams zu bearbeiten müssen folgende Schritte durchgeführt werden:
●
●
●
●
●
ein Stream-Objekt definieren
die Datei mit dem Stream-Objekt verbinden
Datei bearbeiten (schreiben/lesen der Daten)
die Datei-Verbindung mit dem Stream-Objekt aufheben
das Stream-Objekt löschen
Wenn Sie mit Dateistreams arbeiten müssen Sie die Datei fstream mit #include einbinden.
Ausgabestream-Objekt
Ausgabestream-Objekt definieren
Ein Ausgabestream-Objekt wird mit folgender Anweisung definiert:
ofstream myFile;
ofstream ist der Stream, der für die Dateiausgabe zuständig ist und myFile ist der Name des
Stream-Objekts. Den Namen des Stream-Objekts können Sie selbst bestimmen. Objekte werden
zwar erst später behandelt, aber stellen Sie sich unter der obigen Anweisung im Prinzip die
Definition einer Variablen myFile mit dem Datentyp ofstream vor.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (4 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Der Stream ofstream sowie alle nachfolgenden Memberfunktionen, die mit dem Stream
arbeiten, sind ebenfalls im Namensraum std definiert. Sie müssen also entweder vor
jeder Verwendung dieses Streams den Namensraum std:: angegeben oder aber durch
eine using-Anweisung den Stream explizit einbinden.
Verbindung mit Datei herstellen
Das so definiert Stream-Objekt myFile besitzt aber noch keine Zuordnung zu irgend einer Datei.
Diese Zuordnung erfolgt erst durch den Aufruf der Memberfunktion
void ofstream::open(const char *pFName, ios::openmode
mode=ios::out);
Stören Sie sich bitte im Augenblick nicht an den seltsamen Doppelpunkten beim Dateimodus
openmode; wir bringen gleich noch Licht ins Dunkel.
Der erste Parameter pFName ist ein Zeiger auf den Namen der zu öffnenden Datei.
Und noch mal weil's so wichtig ist: denken Sie immer daran, dass Sie eventuelle
Backslash-Zeichen im Pfadnamen verdoppeln müssen, da das Backslash-Zeichen
innerhalb von Strings eine Escape-Sequenz einleitet!
Der zweite Parameter mode gibt an, in welchem Modus die Datei geöffnet werden soll. Er ist ein
enumerated Datentyp (wird später noch erklärt) und kann eine sinnvolle Kombination aus
folgenden Werten sein:
Modus
Bedeutung
ios::out
Öffnen einer Datei zum Schreiben (default).
ios::trunc
Öffnet eine Datei, wobei der ursprüngliche Inhalt wird verworfen (default).
ios::ate
Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende.
Wird der Schreibzeiger neu positioniert (seekp(...)), so werden die Daten an
der neuen Position eingefügt.
ios::app
Öffnen einer Datei und positionieren des Schreibzeigers auf das Dateiende.
Die neuen Daten werden immer an die Datei eingefügt, unabhängig davon
oder der Schreibzeiger inzwischen neu positioniert wurde.
ios::binary Öffnet einer Datei im Binärmodus.
Sie können beim Aufruf der Memberfunktion open(...) auch den zweiten Parameter ganz
weglassen; in diesem Fall wird die Datei für die Ausgabe im Textmodus geöffnet.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (5 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Wenn Sie eine Datei zum Schreiben öffnen und den Mode dabei explizit vorgeben, so
müssen Sie auch immer dem Mode ios::out mit angeben!
Es gibt auch noch eine zweite Möglichkeit einen Ausgabestream zu definieren und dabei
gleichzeitig mit einer Datei zu verbinden.
ofstream myFile(const char* pFName, ios::openmode mode =
ios::out);
Hier wird beim Definieren des Stream-Objekts die zu öffnende Datei und der Mode gleich mit
angegeben.
Sehen wir uns beide Möglichkeiten nun anhand eines Beispiels einmal an.
Im ersten Fall wird
zunächst das StreamObjekt definiert und dann
mit open(...) mit einer Datei
verbunden. Beachten Sie
bitte, dass vor dem Name
der Memberfunktion der
Name des Stream-Objekts
und ein Punkt stehen. Wie
Sie später noch sehen
werden, ist dies die
allgemeine Syntax zum
Aufruf einer
Memberfunktion für ein
bestimmtes Objekt. Ein
solchermaßen definiertes
Stream-Objekt können Sie
nacheinander mit mehreren
Dateien verbinden. Wie dies
geht, das erfahren Sie
gleich beim Aufheben der
Verbindung mit einer Datei
noch.
1. Möglichkeit Ausgabestream-Datei im Textmodus öffnen
#include <fstream>
using namespace std;
....
int main ( )
{
ofstream myFile;
myFile.open ("d:\\tmp\\test.dat");
...
}
2. Möglichkeit Ausgabestream-Datei im Binärmodus öffnen
#include <fstream>
using namespace std;
....
int main ( )
{
ofstream myFile ("d:\\tmp\\test.dat",
ios::out|ios::binary);
...
}
Im zweiten Fall wird das
Stream-Objekt bei seiner
Definition gleich mit einer
Datei verbunden. Im
Beispiel wird die Datei nun
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (6 von 20) [17.05.2005 11:38:19]
Dateizugriffe
im Binärmodus geöffnet.
Beachten Sie bitte, dass Sie
hier auch den Modus
ios::out explizit angeben
müssen.
Und nochmals:
Beachten Sie die
Verdopplung des
BackslashZeichens im
Dateinamen!
Da es nicht ausgeschlossen
ist, dass beim Öffnen einer
Datei auch einmal etwas
schief geht, sollten Sie nach
dem Öffnen auch eventuell
aufgetretene Fehler
abfangen. Wie Sie dies bei
Streams durchführen ist
rechts dargestellt. Diese ifAbfrage (die im Detail
später noch behandelt
wird) kann sowohl bei Textwie auch bei Binärdateien
eingesetzt werden, nachdem
eine Datei mit einem
Stream-Objekt verbunden
wurde. Beachten Sie das
kleine Ausrufzeichen vor
dem Objekt-Namen, dies ist
der NOT-Operator. Sie
können sich die
Funktionsweise des
Operators nochmals
ansehen ( ).
#include <fstream>
using namespace std;
....
int main ( )
{
ofstream myFile;
myFile.open ("d:\\tmp\\test.dat");
if (!myFile)
{
... // Fehlerbehandlung
}
}
Verbindung mit Datei lösen
Nach dem die (gleich noch beschriebene) Bearbeitung der Datei abgeschlossen ist, müssen Sie
die Verbindung des Streams zur Datei wieder aufheben. Hierbei gibt es ebenfalls zwei
verschiedene Verfahren.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (7 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Im ersten Fall heben Sie die
Verbindung zur Datei
einfach durch den Aufruf
der Memberfunktion
#include <fstream>
using namespace std;
....
int main ( )
{
void
ofstream myFile;
ofstream::close();
myFile.open ("d:\\tmp\\test.dat");
...
wieder auf. Das StreammyFile.close();
Objekt besteht danach
....
weiter und Sie können
// Stream erneut mit einer Datei verbinden
mittels open(...) eine weitere
myFile.open(...);
Datei mit dem gleichen
}
Stream-Objekt verbinden.
Die zweite Möglichkeit
#include <fstream>
besteht darin, das Stream- using namespace std;
Objekt einfach zu
....
zerstören. Wenn Sie ein
int main ( )
Stream-Objekt (wie im
{
Beispiel rechts) innerhalb
....
eines Blocks definieren, so
{ // Blockbeginn
kann zunächst die Datei bei
ofstream myFile("d:\\tmp\\test.dat");
der Definition des Stream....
Objekts mit diesem
} // Blockende, Stream-Objekt löschen!
verbunden werden. Wird
}
am Blockende das StreamObjekt dann ungültig
(zerstört), so wird auch
automatisch die
Verbindung zur Datei
aufgehoben.
Schreiben in Datei
Textdatei
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (8 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Das Schreiben in eine
Textdatei erfolgt prinzipiell
gleich wie die Ausgabe auf
die Standard-Ausgabe
mittels cout. Anstelle des
Stream-Objekts cout steht
hier lediglich der Name des
Dateistream-Objekts. Und
alle zu schreibenden Daten
werden dann durch den
Operator << in den Stream
übertragen. Beachten Sie
aber beim Schreiben von
Daten, dass Sie diese durch
ein Trennzeichen
(Leerzeichen,
Zeilenvorschub o.Ä.)
voneinander trennen
müssen, da sie sonst
nachher nicht mehr
eingelesen werden können.
#include <fstream>
using namespace std;
int var1 = 10, var2 = 20;
int main ( )
{
ofstream myFile;
myFile.open ("d:\\tmp\\test.dat");
// Hier Fehler abfangen!
// Daten schreiben
myFile << var1 << ' ';
myFile << setw(5) << var2 << endl;
// Datei schliessen
myFile.close();
}
Dadurch, dass sowohl cout
wie auch ofstream die
gleiche Basisklasse
basic_ostream besitzen,
stehen für die Dateiausgabe
auch die gleichen
Manipulatoren, wie z.B.
hex, setw(...) oder
setprecision(...), zur
Verfügung.
Binärdatei
Sollen numerische Daten binär in einer Datei abgelegt werden, so müssen Sie beim Verbinden
der Datei mit den Stream-Objekt den Mode ios::out | ios::binary angeben.
Beachten Sie, dass Sie hier sowohl ios::out wie auch ios::binary angeben müssen.
Ferner sollten Sie immer daran denken, dass Binärdateien nicht für den
Datenaustausch zwischen unterschiedlichen Plattformen benutzt werden dürfen. Dies
gilt sogar auch für Programme die auf der gleichen Plattform laufen, aber mit
unterschiedlichen Compilern erstellt wurden. So kann z.B. ein Programm das mit dem
MICROSOFT VC++ Compiler erzeugt wurde ein anderes Format für Binärdatei
besitzen als das gleiche Programm mit dem MinGW C++ Compiler.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (9 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Das Schreiben eines
einzelnen Bytes erfolgt mit
der Memberfunktion
#include <fstream>
using namespace std;
int var1 = 10, var2 = 20;
int main ( )
{
ostream& put(char
ofstream myFile;
data);
myFile.open ("d:\\tmp\\test.dat",
ios::out|ios::binary);
wobei data ist das zu
// Hier Fehler abfangen!
schreibende Byte ist.
// Daten schreiben
myFile.write(reinterpret_cast<char*>(&var1),
Sollen Daten die aus
sizeof(var1));
mehreren Bytes bestehen
myFile.write(reinterpret_cast<char*>(&var2),
(short, long usw.) in einer
sizeof(var2));
binären Datei abgelegt
// Datei schliessen
werden, so wird hierfür die
myFile.close();
Memberfunktion
}
ostream&
write(const char
*pBuffer,
streamsize bytes);
eingesetzt. pBuffer ist ein
const char-Zeiger auf den
Beginn des Datenblocks,
der in die Datei geschrieben
werden soll und bytes gibt
die Anzahl der zu
schreibenden Bytes an. Da
die Memberfunktion einen
const char-Zeiger erwartet,
müssen Sie in der Regel
eine Typkonvertierung
vornehmen wenn Sie
numerische Daten
abspeichern (siehe Beispiel
rechts). Vergessen Sie die
Typkonvertierung, so
erhalten Sie vom Compiler
eine Fehlermeldung.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (10 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Wenn Sie sich das Beispiel oben genauer ansehen werden Sie feststellen, dass dort nur
eine Typkonvertierung in char* erfolgt und nicht in const char*. Ein char* kann überall
anstelle eines const char* übergeben werden, aber nicht umgekehrt! Das const char* bei
der Signatur der Memberfunktion write(...) sagt nur aus, dass die Memberfunktion den
Inhalt übergebenen Puffer nicht verändert.
Hier können Sie sich den reinterpret_cast Operator nochmals anschauen (
).
Beim Schreiben von Daten in Binärdateien können die Daten unmittelbar hintereinander
geschrieben werden, d.h. es sind keine Trennzeichen wie bei einer Textdatei notwendig.
Beenden wir damit die Dateiausgabe und wenden uns dem Einlesen der Daten zu.
Die Angabe des Returnwertes ostream& bei den obigen Memberfunktionen ist kein
Schreibfehler. Nur soviel vorne weg: die Memberfunktionen put(...) und write(...) liefern
eine Referenz auf ein Objekt der Klasse ostream (oder einer davon abgeleiteten Klasse)
zurück. Referenzen werden später noch gesondert behandelt.
Eingabestream-Objekt
Eingabestream-Objekt definieren
Ein Eingabestream-Objekt wird mit folgender Anweisung definiert:
ifstream myFile;
ifstream ist wieder der Stream, der für die Dateieingabe zuständig ist und myFile ist der
(beliebige) Name des Stream-Objekts. Das so definierte Stream-Objekt myFile besitzt aber
ebenfalls noch keine Zuordnung zu irgend einer Datei.
Verbindung mit Datei herstellen
Nach dem das Stream-Objekt erstellt ist kann es mit einer Datei verbunden werden. Dies erfolgt
wieder durch den Aufruf der Memberfunktion
void istream::open(const char *pFName, ios::openmode
mode=ios::in);
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (11 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Der erste Parameter pFName ist der char-Zeiger auf den Namen der zu öffnenden Datei und der
zweite Parameter mode gibt an, wie die Datei zu öffnen ist. Er kann hier eine sinnvolle
Kombination aus folgenden Werten sein:
Modus
Bedeutung
ios::in
Öffnen einer Datei zum Lesen.
ios::binary
Öffnet einer binären Datei.
Sie können beim Aufruf der Memberfunktion auch diesen zweiten Parameter weglassen; in
diesem Fall wird die Datei für die Eingabe im Textmodus geöffnet.
Und auch hier besteht die Möglichkeit, einen Eingabestream gleich bei seiner Definition mit
einer Datei zu verbinden.
ifstream myFile(const char* pFName,
ios::in);
ios::openmode mode =
Hier wird beim Definieren des Stream-Objekts die zu öffnende Datei und der Mode gleich mit
angegeben.
Sehen wir uns beide Möglichkeiten nun anhand eines Beispiels einmal an.
Im ersten Fall wird
zunächst das StreamObjekt definiert und dann
mit open(...) mit einer Datei
verbunden. Beachten Sie
bitte, dass vor dem Name
der Memberfunktion der
Name des Stream-Objekts
und ein Punkt stehen.
1. Möglichkeit Eingabestream-Datei öffnen
#include <fstream>
using namespace std;
....
int main ( )
{
ifstream myFile;
myFile.open ("d:\\tmp\\test.dat");
if (!myFile)
.... // Fehlerbehandlung
...
}
Im zweiten Fall wird das
Stream-Objekt bei seiner
Definition gleich mit einer
Datei verbunden. Im
Beispiel wird die Datei im
2. Möglichkeit Eingabestream-Datei öffnen
Binärmodus geöffnet.
Beachten Sie bitte, dass Sie
#include <fstream>
hier auch den Modus ios::in
using namespace std;
explizit angeben müssen.
....
int main ( )
Und auch das Öffnen einer
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (12 von 20) [17.05.2005 11:38:19]
Dateizugriffe
{
Datei zum Lesen kann
natürlich schief gehen.
Prüfen Sie deshalb immer
ab, ob die Datei erfolgreich
mit dem Stream verbunden
werden konnte. Stellen Sie
sich einmal vor, Sie wollen }
von einer Datei einlesen die
es gar nicht gibt!
ifstream myFile ("d:\\tmp\\test.dat",
ios::in|ios::binary);
if (!myFile)
.... // Fehlerbehandlung
...
Verbindung mit Datei lösen
Für das Lösen der Dateiverbindung und das Löschen des Stream-Objekts gilt das Gleiche wie
beim Ausgabestream.
Lesen aus Datei
Textdatei
Das Lesen aus einer
Textdatei erfolgt auch hier
prinzipiell gleich wie das
Lesen von Daten von der
Standard-Eingabe mittels
cin. Anstelle des
Streamobjekts cin steht hier
lediglich der Name des
Eingabestream-Objekts
und alle zu lesenden Daten
werden ebenfalls durch den
Operator >> aus dem
Stream ausgelesen.
#include <fstream>
using namespace std;
int var1, var2;
int main ( )
{
ifstream myFile;
myFile.open ("d:\\tmp\\test.dat");
// Hier Fehler abfangen!
// Daten lesen
myFile >> var1 >> var2;
// Datei schliessen
myFile.close();
}
Dadurch, dass sowohl cin
wie auch ifstream die gleiche
Basisklasse basic_istream
besitzen, stehen für die
Dateieingabe auch die
gleichen Manipulatoren,
wie z.B. hex oder oct, zur
Verfügung.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (13 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Binärdatei
Sollen numerische Daten aus einer Binärdatei ausgelesen werden, so müssen Sie beim
Verbinden der Datei mit den Stream-Objekt den Mode ios::in | ios::binary angeben.
Das Lesen eines einzelnen
Bytes erfolgt mit der
Memberfunktion
istream& get(char&
data);
#include <fstream>
using namespace std;
int var1, var2;
int main ( )
{
ifstream myFile;
myFile.open ("d:\\tmp\\test.dat",
ios::in|ios::binary);
// Hier Fehler abfangen!
// Daten lesen
Das gelesene Byte wird in
der Variable data abgelegt.
Beachten Sie bitte, dass
nach dem Datentyp char
myFile.read(reinterpret_cast<char*>(&var1),
der Operator & steht. Dies
sizeof(var1));
ist hier nicht der
Adressoperator sondern
myFile.read(reinterpret_cast<char*>(&var2),
der später bei den
sizeof(var2));
Funktionen noch
// Datei schliessen
aufgeführte
myFile.close();
Referenzoperator. In ihrem }
Programm übergeben Sie
beim Aufruf dieser
Memberfunktion die
einzulesende Variable 'ganz
normal' an die
Memberfunktion, also z.B.
myFile.get(charVar);..
Sollen Daten die aus
mehreren Bytes bestehen
(short, long usw.) aus einer
Binärdatei ausgelesen
werden, so wird hierfür die
Memberfunktion
istream& read(char
*pBuffer,
streamsize bytes);
eingesetzt. pBuffer ist ein
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (14 von 20) [17.05.2005 11:38:19]
Dateizugriffe
char-Zeiger auf den Beginn
des Datenblocks, in dem die
aus der Datei ausgelesenen
Daten abgelegt werden und
bytes gibt die Anzahl der zu
lesenden Bytes an. Da die
Memberfunktion wiederum
einen char-Zeiger erwartet,
müssen Sie in der Regel
eine Typkonvertierung
verwenden wenn Sie
numerische Daten einlesen
(siehe Beispiel rechts).
Gleichzeitiges Schreiben und Lesen
Und auch dies ist möglich: Sie können einen Dateistreams zum gleichzeitigen Lesen und
Schreiben öffnen. Hierzu benötigen Sie ein Stream-Objekt vom Typ fstream das Sie wie folgt
definieren:
fstream( const char* pFName, int mode );
pFName ist der Zeiger auf den Dateinamen und mode gibt den Modus an, in dem die Datei
geöffnet werden soll. Für den Modus kann eine sinnvolle Kombination unter anderem folgender
Werte eingesetzt werden:
Mode
Bedeutung
ios::in
Öffnen zum Lesen
ios::out
Öffnen zum Schreiben
ios::binary Öffnen einer binären Datei
ios::app
Neue Daten werden immer an die Datei angehängt
ios::ate
Neue Daten werden an die Datei angehängt.
ios::trunc Bisheriger Dateiinhalt wird verworfen.
Beachten Sie bitte, dass Sie den mode-Parameter hier immer angegeben müssen. Wenn Sie Datei
mit alleinigem Lesezugriff öffnen, muss die Datei natürlich bereits existieren. Der Unterschied
zwischen ios::app und ios::ate wurde schon beim Ausgabestream erläutert ( ).
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (15 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Wollen Sie eine Datei zum Lesen und Schreiben öffnen die noch nicht existiert, so
müssen Sie den Mode ios::trunc mit angeben. Selbstverständlich können Sie dann zu
Beginn nur Daten schreiben.
Alle 3 Streams (ofstream, ifstream und fstream) besitzen eigentlich noch einen 3.
Parameter, der den gleichzeitigen Zugriff auf eine Datei von mehreren Streams aus
kontrolliert. Dieser Parameter ist aber mehr für fortgeschrittene Dateioperationen und
wird deshalb hier nicht weiter betrachtet.
Sonstige Dateioperationen
In der Praxis werden Sie in vielen Fällen nicht von vornherein wissen, wie viele Daten in einer
Datei abgelegt sind. Beim Lesen von Daten aus einer Datei müssen Sie also irgend wie feststellen
können, ob das Dateiende erreicht und damit alle Daten eingelesen wurden. Die Abfrage ob das
Dateiende erreicht wurde erfolgt mit der Memberfunktion
bool basic_ios::eof()
Diese Memberfunktion
liefert den Wert true zurück
wenn das Dateiende
erreicht ist. Beachten Sie
bitte, dass weitere
Einleseversuche danach
zwar in der Regel nicht zu
einem Programmabsturz
führen, Sie jedoch
ungültige Daten erhalten.
Damit eof(...) aber das
Erreichen des Dateiendes
signalisieren kann, muss
vorher ein Einleseversuch
stattgefunden haben.
ifstream myFile("d:\\test.dat");
// Einlesen aus Datei
myFile >> data1 >> ...;
// Auf Dateiende abpruefen
if (!myFile.eof())
{
... // Daten aus Datei auswerten
}
....
Die Dateistreams bieten noch die Möglichkeit, mittels seek(...)-Memberfunktionen die
'Dateizeiger' explizit zu beeinflussen. Wie dies geht können Sie sich ansehen, wenn Sie
links das Symbol anklicken.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (16 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Ansonsten kommt jetzt das Beispiel und die Übung zur Lektion.
Beispiel und Übung
Das Beispiel:
Für eine Kundendatenbank wird eine Textdatei erstellt die folgende Daten enthält:
●
●
●
Kundennummer als short-Wert
Umsatz in Tausend-EUR als double-Wert
Datum des letzten Umsatzes, bestehend aus Tag, Monat und Jahr, jeweils als shortWert
Die Daten werden mit beliebigen Werten initialisiert und dann in die Datei übertragen.
Danach werden die Daten aus der Datei eingelesen und zur Kontrolle ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le04\prog\Bsfile.
Die Programmausgabe:
Kundendaten:
-----------KD-Nummer
: 123
Umsatz in TSD
: 12.3
Letzter Umsatz am: 10.5.1999
Das Programm:
// C++ Kurs
// Beispiel zum Dateihandling mittels Streams
// Dateien einbinden
#include <iostream>
#include <fstream>
using namespace std;
// Dateiname
const char* const pszDATEI = "kunden.dat";
// Hauptprogramm
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (17 von 20) [17.05.2005 11:38:19]
Dateizugriffe
int main()
{
// Definition der Kundendaten mit Initialisierung
short
kdNummer = 123;
double umsatz = 12.3f;
short
tag=10,monat=5,jahr=1999;
// Das Ausgabestream-Objekt wird innerhalb eines Blocks
// definiert. Am Blockende wird es geloescht und damit
// die Datei geschlossen!
{
// Ausgabestream-Objekt erstellen und mit Datei verbinden
ofstream OutFile(pszDATEI);
// Fehler abfangen
if (!OutFile)
{
cout << "Fehler beim Erstellen der Datei!\n";
exit (1);
}
// Daten in Datei schreiben
// ACHTUNG! Nach jedem Datum entweder ein Leerzeichen
// oder ein Zeilenvorschub eingeben damit Daten nachher
// auch wieder eingelesen koennen
OutFile << kdNummer << " " << umsatz << endl;
OutFile << tag << " " << monat << " " << jahr << endl;
}
// Blockende, Datei ist wieder geschlossen!
// Variablen zur Kontrolle mit 0 belegen
kdNummer = tag = monat = jahr = 0;
umsatz = 0.0;
// Eingabestream-Objekt definieren und jetzt mit open() mit
// der Datei verbinden. Moeglich waere auch die Datei direkt
// wie vorhin mit dem Stream zu verbinden
ifstream InFile;
InFile.open(pszDATEI);
// Fehler abfangen!
if (!InFile)
{
cout << "Fehler beim Oeffnen der Datei!\n";
exit(2);
}
// Daten aus Datei auslesen
InFile >> kdNummer >> umsatz;
InFile >> tag >> monat >> jahr;
// Dateiverbindung aufheben
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (18 von 20) [17.05.2005 11:38:19]
Dateizugriffe
InFile.close();
// Eingelesene Daten formatiert auf Bildschirm ausgeben
cout << "Kundendaten:\n------------\n";
cout << "KD-Nummer
: " << kdNummer << endl;
cout << "Umsatz in TSD
: " << umsatz << endl;
cout << "Letzter Umsatz am: ";
cout << tag << "." << monat << "." << jahr << endl;
// Fertig!
}
Die Übung:
Wandeln Sie das obige Beispiel so ab, dass die Daten nun in einer binären(!) Datei abgelegt
werden. Verwenden Sie hierfür ein Stream-Objekt das 'gleichzeitiges' Lesen und Schreiben
zulässt.
Schreiben Sie zunächst wieder folgende (beliebig zu initialisierende) Kundendaten in die
Datei:
●
●
●
Kundennummer als short-Wert
Umsatz in Tausend-EUR als float-Wert
Datum des letzten Umsatzes, bestehend aus Tag, Monat und Jahr, jeweils als shortWert
Lesen Sie Daten nach dem Schreiben gleich wieder ein und geben Sie sie zur Kontrolle auf
dem Bildschirm aus.
Anschließend erhöhen Sie den eingelesenen Umsatz um 5 und den Tag um eins. Schreiben Sie
die neuen Daten in die Datei zurück.
Zum Schluss lesen Sie die neuen wieder ein und geben sie zur Kontrolle nochmals aus.
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (19 von 20) [17.05.2005 11:38:19]
Dateizugriffe
Die Programmausgabe:
Kundendaten:
-----------KD-Nummer : 33
Umsatz in TSD : 12.3
Letzter Umsatz am: 1.3.2001
Kundendaten:
-----------KD-Nummer : 33
Umsatz in TSD : 17.3
Letzter Umsatz am: 2.3.2001
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le04\Lsfile.
Na, das war wohl schon etwas schwieriger oder? Dafür sind Sie aber auch am Ende der 4.
Lerneinheit angelangt. In der nächsten Lerneinheit geht's dann mit Verzweigungen und
Schleifen weiter.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/le04_03.htm (20 von 20) [17.05.2005 11:38:19]
Die vollständige getline() Memberfunktion
Die vollständige getline()
Memberfunktion
Das vollständige Prototying der cin-Memberfunktion getline(...) sieht wie folgt aus:
istream& getline (char *pBuffer, int noOfChars, char del = '\n');
Der erste und zweite Parameter wurde ja bereits erläutert. Und der letzte Parameter spezifiziert
das Zeichen, das das Ende der Eingabe signalisiert. Standardmäßig ist dies der Zeilenvorschub.
Für die Arbeitsweise von getline(...) gilt nun folgendes: getline(...) liest solange Zeichen von der
Tastatur ein, bis die <RETURN>-Taste gedrückt wird. Anschließend wird die Eingabe
überprüft. Zuerst wird die Eingabe auf das Vorhandsein des Abschlusszeichens (3. Parameter
von getline(...)) hin untersucht. Stehen vor dem Abschlusszeichen maximal noOfChars Zeichen,
so wird die Eingabe bis zum Abschlusszeichen in das übergebene Feld umkopiert. Anschließend
wird das Abschlusszeichen aus der Eingabe entfernt. Befinden sich dann noch weitere Zeichen
in der Eingabe, so verbleiben diese dort und werden dem nächsten cin zugewiesen. Wie Sie
diesen 'Rest' der Eingabe aus dem Eingabepuffer entfernen können ist im nachfolgenden
Beispiel nochmals aufgeführt. Fehlt das Abschlusszeichen, so liest die Memberfunktion
getline(...) weitere Zeichen ein. Das heißt getline(...) wird erst dann beendet, wenn entweder die
gewünschte Anzahl von Zeichen oder das Abschlusszeichen eingegeben wurde. Sehen Sie sich
dazu einmal folgenden Fall an:
Anweisung:
cin.getline(feld,20,'$');
// $ ist Abschlusszeichen
Eingabe:
123<RETURN>
456<RETURN>
78$9<RETURN>
Ausgabe:
123<RETURN>
456<RETURN>
78
http://www.cpp-tutor.de/cpp/le04/le04_02_d1.htm (1 von 3) [17.05.2005 11:38:21]
Die vollständige getline() Memberfunktion
Wie Sie der Ausgabe entnehmen können, werden in diesem Fall sogar die 'RETURN's mit im
Feld abgespeichert.
Und noch ein weiteres
Beispiel zu getline(...). Im
Beispiel wird für die
Berechnung der maximal
einzulesenden Zeichen beim
Aufruf der
Memberfunktion getline(...)
der Operator sizeof(...)
verwendet. sizeof(...) ist ein
Standard C++-Operator
(keine Funktion!), der den
von einer Variable oder
eines Datentyps belegten
Speicherplatz in Bytes
zurückliefert ( ). Im
Beispiel liefert sizeof(array)
die Größe des
Eingabepuffers array, hier
also 40. Die erste Eingabe
wird nun beendet wenn
mind. 39 Zeichen oder aber
ein Punkt eingegeben
wurden. Diese Eingabe
wird dann zur Kontrolle
ausgegeben. Beachten Sie
bei der Ausgabe, dass nur
die Eingabe bis zum Punkt
übernommen wurde. Die
restlichen eingegebenen
Zeichen befinden sich aber
noch im Eingabepuffer. Um
diese Zeichen zu verwerfen,
d.h. eigentlich werden sie
nur übersprungen, wird die
Memberfunktion seekg(...)
des Eingabestreams
aufgerufen. Mehr zu
seekg(...) später bei der
Behandlung der Ein/Ausgabe von Dateien. Sie
können das Programm zum
Testen auch einmal in
#include <iostream>
#include <climits>
using namespace std;
int main ()
{
// Feld zur Aufnahme der Eingabe def.
char array[40];
// Hinweis ausgeben
cout << "Bitte 1. Text eingeben: ";
// Zeile einlesen. Der Operator sizeof()
// liefert die Größe des Feldes in Bytes
cin.getline(array, sizeof(array),'.');
// Eingabe wieder ausgeben
cout << "1. Eingabe war: " << array <<
endl;
// Restliche Eingabe ueberspringen
cin.seekg(0,ios::end);
// Hinweis ausgeben
cout << "Bitte 2. Text eingeben: ";
// Nächste Zeile einlesen
cin.getline(array, sizeof(array),'.');
// Eingabe wieder ausgeben
cout << "2. Eingabe war: " << array <<
endl;
}
Die Ein- und Ausgaben:
Bitte 1. Text eingeben: Ein Satz. Und Ende
1. Eingabe war: Ein Satz
Bitte 2. Text eingeben: Das wars.
2. Eingabe war: Das wars
http://www.cpp-tutor.de/cpp/le04/le04_02_d1.htm (2 von 3) [17.05.2005 11:38:21]
Die vollständige getline() Memberfunktion
Ihren Compiler
übernehmen und dann
ausprobieren was passiert,
wenn Sie den Aufruf von
seekg(...) entfernen.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/le04_02_d1.htm (3 von 3) [17.05.2005 11:38:21]
Ende Kapitel 1
Ende Kapitel 1
Herzlichen Glückwunsch!
Sie haben damit das erste Kapitel dieses
Kurses
fertig bearbeitet.
Das nächste Kapitel befasst sich mit Ablaufsteuerung und
erweiterte Datentypen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/k1_end.htm [17.05.2005 11:38:22]
Positionieren des Dateizeigers
Positionieren des Dateizeigers
Und auch bei Dateistreams lassen sich die internen Dateizeiger (oder genau genommen sind es die
Pufferzeiger) beeinflussen, um einen wahlfreien Zugriff auf die in der Datei abgelegten Daten zu
erhalten. Hierzu werden die beiden Memberfunktionen
basic_istream& seekg(off_type offset, ios_base::seek_dir dir);
basic_ostream& seekp(off_type offset, ios_base::seek_dir dir);
Die Memberfunktion seekg(...) dient zum Positionieren des Lese-Dateizeigers und die Memberfunktion
seekp(...) zum Positionieren des Schreib-Dateizeigers, d.h. beide Dateizeiger lassen sich unabhängig
voneinander positionieren. offset gibt die Anzahl der Bytes an, um die der Dateizeiger von der Position
dir aus bewegt werden soll. Für dir ist einer der folgenden Werte zulässig:
Wert
Bedeutung
ios::cur Dateizeiger von der akt. Position aus bewegen
ios::end Dateizeiger vom Ende der Datei aus bewegen
ios::beg Dateizeiger vom Anfang der Datei aus bewegen
Das nachfolgende Beispiel zeigt die Handhabung der Memberfunktionen sowie das Abfragen auf das
Dateiende beim Einlesen von Daten auf.
Das Beispiel:
Das Beispiel schreibt die Werte 1...5 in eine Binärdatei die zum Lesen und Schreiben geöffnet wird.
Nach dem die Werte geschrieben wurden, werden mittels den erwähnten Memberfunktionen
verschiedene Werte aus der Datei ausgelesen. Beachten Sie dazu die Kommentare im Listing.
Zusätzlich wird der vorletzte Wert nachträglich verändert und dann alle Daten zur Kontrolle
nochmals ausgegeben.
Beachten Sie bitte, welche Definitionen Sie aus dem std-Namensraum einbinden müssen!
Das unten stehende Beispiel finden Sie auch unter: le04\prog\Bspos
http://www.cpp-tutor.de/cpp/le04/le04_03_d1.htm (1 von 4) [17.05.2005 11:38:23]
Positionieren des Dateizeigers
Die Programmausgabe:
1. Wert: 1
Letzter Wert: 5
Vorletzter Wert: 4
Ueberschreibe jetzt vorletzten Wert!
Datei enthaelt jetzt:
1 2 3 -1 5
Das Programm:
// C++ Kurs
// Beispiel zum wahlfreien Dateizugriff
#include <fstream>
#include <iostream>
using
using
using
using
using
std::cout;
std::endl;
std::fstream;
std::ios;
std::streamoff;
// Dateiname
const char* const pFILENAME = "test.dat";
// Das zu schreibende und lesende Datum
short var;
int main()
{
// Ein/Ausgabe Stream-Objekt
fstream inOutFile(pFILENAME,ios::binary|ios::in|ios::out|ios::trunc);
if (!inOutFile)
{
cout << "Fehler beim Oeffnen der Datei " << pFILENAME << endl;
exit (1);
}
// Werte 1...5 in Datei schreiben
// Mit einer Schleife geht das spaeter einfacher!
var = 1;
inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
var++;
inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
var++;
inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
var++;
inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
var++;
http://www.cpp-tutor.de/cpp/le04/le04_03_d1.htm (2 von 4) [17.05.2005 11:38:23]
Positionieren des Dateizeigers
inOutFile.write (reinterpret_cast<char*>(&var),sizeof(var));
// Auf Dateianfang zurueck
inOutFile.seekg(static_cast<streamoff>(0),ios::beg);
// 1. Datum einlesen
inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
cout << "1. Wert: " << var << endl;
// Auf letzten Wert positionieren
inOutFile.seekg(static_cast<streamoff>(-sizeof(var)),ios::end);
inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
cout << "Letzter Wert: " << var << endl;
// und auf vorletzten Wert positionieren
inOutFile.seekg(static_cast<streamoff>(-(sizeof(var)*2)),ios::end);
inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
cout << "Vorletzter Wert: " << var << endl;
// Vorletzten Wert ueberschreiben
cout << "Ueberschreibe jetzt vorletzten Wert!" << endl;
var = -1;
inOutFile.seekp(static_cast<streamoff>(-(sizeof(var)*2)),ios::end);
inOutFile.write(reinterpret_cast<char*>(&var),sizeof(var));
// Und nun alle Werte auslesen
cout << "Datei enthaelt jetzt:\n";
// Lesezeiger zuerst auf Dateianfang
inOutFile.seekg(static_cast<streamoff>(0),ios::beg);
// Die nachfolgende for-Schleife sowie die
// break-Anweisung werden spaeter noch behandelt.
//
// Nun alles wieder einlesen bis zum Dateiende
for(;;)
{
inOutFile.read(reinterpret_cast<char*>(&var),sizeof(var));
if (inOutFile.eof())
break;
cout << var << " ";
}
cout << endl;
}
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le04/le04_03_d1.htm (3 von 4) [17.05.2005 11:38:23]
Positionieren des Dateizeigers
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le04/le04_03_d1.htm (4 von 4) [17.05.2005 11:38:23]
if-Verzweigung
if-Verzweigung
Die Themen:
Syntax
Auszuführende Anweisungen
Mehrere Bedingungen
Bedingungsoperator und goto
Beispiel und Übung
Syntax
Die IF-ELSE Verzweigung
wird immer dann eingesetzt,
wenn in Abhängigkeit von
Bedingungen verschiedene
Anweisungen auszuführen
sind. Sie hat folgende
allgemeine Syntax:
if (Ausdruck)
Anweisung1
IF-Zweig
else
Anweisung2
ELSE-Zweig
Ausdruck mit logischem Vergleich:
if (var == 10)
var = 0;
else
var++;
//
//
//
//
Falls var gleich 10
var auf 0 setzen
sonst
var inkrementieren
Ausdruck mit ganzzahligem Vergleich:
//
//
if (var)
// Falls var ungleich 0 (=true!)
cout << "ungleich NULL\n";
else
// sonst
cout << "gleich NULL\n";
Ausdruck kann ein beliebiger
C++ Ausdruck sein der
entweder ein ganzzahliges oder
ein boolsches Ergebnis liefert.
Der Ausdruck muss immer in
runden Klammern stehen. In
der Regel werden Sie hier
einen Vergleichs- oder
Logikausdruck stehen haben.
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (1 von 9) [17.05.2005 11:38:25]
if-Verzweigung
Ergibt die Auswertung des Ausdrucks true (oder einen Wert ungleich 0), so wird die Anweisung
Anweisung1 ausgeführt. In allen anderen Fällen wird die Anweisung Anweisung2 ausgeführt.
Nach dem Ausdruck der IF-Abfrage darf kein Semikolon stehen. Wird hier ein
Semikolon gesetzt so gilt dies als leere Anweisung, d.h. bei erfüllter Bedingung wird
einfach nichts ausgeführt!
Auszuführende Anweisungen
Standardmäßig folgt auf ein if
oder else nur eine Anweisung,
so wie im vorherigen Beispiel
dargestellt. Sollen mehrere
Anweisung in Abhängigkeit
von einer Bedingung
ausgeführt werden, so sind die
Anweisungen in einen Block
{...} einzuschließen. Achten Sie
dabei aber auf eine saubere
Formatierung damit Sie auch
später noch Ihr Programm
ohne größere Anstrengungen
lesen können.
Außerdem ist der else-Zweig
optional. Ist er nicht
vorhanden und die
Auswertung der Bedingung
ergibt false, so wird mit der
nächsten Anweisung nach dem
if-Zweig fortgefahren. Im
Beispiel rechts wird die
Variable var nur dann
dekrementiert, wenn ihr Inhalt
ungleich 0 (entspricht true) ist.
if (var == 10)
{
var = 0;
cout << "var resetiert!";
}
else
{
var++;
cout << "var jetzt:" << var;
}
cout << endl;
if (var)
{
var--;
cout << "var dekrementiert.";
}
cout << "var jetzt:" << var << endl;
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (2 von 9) [17.05.2005 11:38:25]
if-Verzweigung
Innerhalb eines if- oder elseif (var > 0)
Zweigs sind alle C++
{
Anweisungen zugelassen. So
short temp = var % 5
können in einem Zweig (wie
if (temp)
// falls temp ungleich 0
rechts dargestellt) weitere if{
Anweisungen folgen oder auch
...
lokale Variablen definiert
}
werden. Diese lokalen
}
Variablen sind dann nur
innerhalb des Zweigs gültig
und belegen auch nur für diese
Zeit Speicherplatz.
ABER ACHTUNG!
Sehen Sie sich einmal die beiden folgenden if-Anweisungen an:
if (var == 10)
....
if (var = 10)
....
Beide Anweisungen sind syntaktisch richtig! Im ersten Fall wird die Variable var mit dem
Wert 10 verglichen und in Abhängigkeit davon werden weitere Anweisungen ausgeführt.
Im zweiten Fall wird der Variablen var der Wert 10 zugewiesen und das Ergebnis der
Zuweisung ausgewertet. Und da var danach ungleich 0 (und damit true) ist, werden die
Anweisungen im if-Zweig immer ausgeführt.
Mehrere Bedingungen
Bisher hatten wir bei der ifHier müssen beide Bedingungen erfüllt sein:
Anweisung nur eine
if ((var == 10) && (count != 0))
Bedingung stehen. Jedoch
....
können auch mehrere
Bedingung mittels logischer
Hier reicht es aus, wenn eine Bedingung erfüllt ist:
Operatoren verknüpft werden.
if ((var == 10) || (count != 0))
Das Beispiel rechts zeigt auf,
....
wie Sie eine if-Anweisung
aufbauen die mehrere
Bedingungen besitzt. Beachten
Sie dabei, dass die einzelnen
Bedingungen in Klammern
stehen und der gesamte ifAusdruck nochmals
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (3 von 9) [17.05.2005 11:38:25]
if-Verzweigung
geklammert ist.
Die Klammern um die einzelnen Bedingungen sind zwar nicht vorgeschrieben, jedoch sollten Sie
sich an diese Vorgehensweise halten. Sie erleichtert sich den Überblick über die einzelnen
Bedingungen und außerdem erleben Sie keine bösen Überraschungen in Bezug auf die Reihenfolge
der Auswertung der einzelnen Operatoren. Oder wissen Sie noch, ob der GLEICH-Operator vor
dem UND-Operator ausgewertet wird oder umgekehrt?
Und noch ein Hinweis: die Anzahl der Bedingungen in einer if-Anweisung ist zwar nicht begrenzt,
jedoch leidet die Übersichtlichkeit erheblich wenn viele Bedingungen in einer if-Anweisung
verarbeitet werden.
Und auch hier nochmals ein dickes Achtung!
Die Anweisung
if ((Ausdruck1) & (Ausdruck2))
....
ist syntaktisch richtig und erzeugt somit auch keinen Fehler beim Übersetzen des
Programms. Hier werden die beiden Ausdrücke bitweise verundet und nicht logisch (was
vermutlich erfolgen sollte).
Bedingungsoperator und goto
Eine etwas abgewandelte Form var2 = (var1>0) ? var1 : -var1;
der IF-ELSE Anweisung stellt
der Bedingungsoperator dar, Alternative Schreibweise:
der folgende Syntax besitzt:
Erg = (Ausdruck1) ?
Ausdruck2 :
Ausdruck3;
Liefert die Auswertung des
Ausdrucks Ausdruck1 als
Ergebnis true, so wird
Ausdruck2 berechnet und das
Ergebnis der Variablen Erg
zugewiesen. Liefert Ausdruck1
dagegen false, wird das
Ergebnis von Ausdruck3 der
Variablen Erg zugewiesen.
if (var1>0)
var2 = var1;
else
var2 = -var1;
Weiteres Beispiel:
cout << "Das Licht ist " <<
((light)? "an" : "aus") << endl;
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (4 von 9) [17.05.2005 11:38:25]
if-Verzweigung
Rechts im Beispiel sehen Sie,
wie mithilfe dieser Anweisung
der Absolutbetrag einer
Variablen 'berechnet' werden
kann. Außerdem ist ein etwas
unkonventionelles Beispiel
aufgeführt, das je nach
Zustand der Variablen light
entweder den Text Das Licht ist
an oder den Text Das Licht ist
aus ausgibt.
Nur der Vollständigkeit halber sei an dieser Stelle noch erwähnt, dass C++ auch eine gotoAnweisung kennt. Sie hat folgende Syntax:
if (....)
goto label;
....
label:
....
// Falls Bedingung erfüllt
// Sprung nach label
// Das Sprungziel
Das Sprungziel kann einen beliebigen, eindeutigen Namen besitzen und muss mit einem
Doppelpunkt abgeschlossen werden. Für die Position des Sprungziels gibt es eine Reihe von
Einschränkungen, die wir uns hier aber nicht ansehen wollen. In einem 'sauberen' Programm
kommt man in der Regel ohne goto aus. Wie die goto-Anweisung eingesetzt wird, das können Sie
sich im nachfolgenden Beispiel noch ansehen.
Beispiel und Übung
Das Beispiel:
Das Programm erzeugt eine bestimmte Anzahl von 'Zufallszahlen' und zählt dabei, wie viele
dieser erzeugten Zahlen gerade und ungerade sind.
Für die Erzeugung der Zufallszahl werden 3 neue Funktionen eingesetzt. Die erste Funktion
int rand( void );
liefert eine 'Zufallszahl' zwischen 0 und RAND_MAX. RAND_MAX ist eine compilerabhängige
Konstante, deren Wert beim MICROSOFT VC++ und beim BORLAND Compiler 0x7FFF und
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (5 von 9) [17.05.2005 11:38:25]
if-Verzweigung
beim MinGW Compiler 0x7FFFFFFF beträgt. Da rand(...) natürlich keine 'Zufallszahl' liefern
kann, sondern nur nach einem bestimmten Algorithmus verteilte Zahlen, wird zuerst mittels
time_t time( time_t *timer );
die aktuelle Uhrzeit ausgelesen. time(...) liefert die seit dem 1.1.1970 00:00Uhr vergangene Anzahl
von Sekunden zurück. Dieser Wert wird in einen unsigned int Wert umgewandelt. Anschließend
wird die Funktion
void srand( unsigned int seed );
aufgerufen, die den Zufallszahlen-Algorithmus initialisiert. Wird srand(...) immer mit dem
gleichen Wert aufgerufen, so werden auch immer die gleichen 'Zufallszahlenreihen' durch
rand(...) zurückgeliefert.
Wenn Sie die Funktionen rand(...) und srand(...) verwenden müssen Sie die Header-Datei
cstdlib einbinden und für die Funktion time(...) die Header-Datei ctime. Alle Funktionen
liegen ebenfalls im Namensraum std!
Der Rest des Programms ist relativ einfach. Bei der erzeugten Zufallszahl wird in einer ifAbfrage das Bit 0 ausmaskiert. Bei ungeraden Zahlen ist das Bit 0 immer 1 (1=00000001,
3=00000011, 5= 00000101, ...) und bei geraden Zahlen 0. Je nach Ergebnis der Abfrage wird
dann ein entsprechender Zähler um eins erhöht. Anschließend wird der Zähler für die Anzahl
der erzeugten Zufallszahlen erhöht. Wurde die festgelegte Anzahl von Zahlen noch nicht
erzeugt, erfolgt ein Sprung nach vorne im Programm.
Das unten stehende Beispiel finden Sie auch unter: le05\prog\Bif.
Die Programmausgabe:
Anzahl der geraden Zahlen: 520
Anzahl der ungeraden Zahlen: 480
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (6 von 9) [17.05.2005 11:38:25]
if-Verzweigung
Das Programm:
//
//
//
//
C++ Kurs
Beispiel zur if-else Verzweigung
HINWEIS: Das Beispiel verwendet eine goto-Anweisung
da bis jetzt noch keine Schleifen eingefuehrt wurden.
// Zuerst Dateien einbinden
#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;
using std::endl;
// Hauptprogramm
int main ()
{
// Anzahl der erzeugten Zufallszahlen
const int MAXCOUNTER = 1000;
// aktuelle Uhrzeit auslesen
unsigned int actTime = static_cast<unsigned int>(std::time(NULL));
// Zufallszahlen initialisieren
std::srand (actTime);
// Anzahl der erzeugten Zufallszahlen
int counter = 0;
// Zaehler fuer gerade und gerade Zahlen
int even=0, odd=0;
// Zufallszahl
int number;
// Schleife
OnceAgain:
// einfache Zufallszahl erzeugen
number = std::rand();
// Falls Bit 0 gesetzt ist Zahl ungerade
if (number&0x01 != 0)
odd++;
// sonst ist Zahl gerade
else
even++;
// Anzahl der erzeugten Zufallszahlen erhoehen
counter++;
// Wenn weniger als MAXCOUNTER Zahlen erzeugt,
// dann weiter Zahl erzeugen
if (counter < MAXCOUNTER)
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (7 von 9) [17.05.2005 11:38:25]
if-Verzweigung
goto OnceAgain;
// Ergebnis ausgeben
cout << "Anzahl der geraden Zahlen: " << even << endl;
cout << "Anzahl der ungeraden Zahlen: " << odd << endl;
}
Die Übung:
Es soll ein kleines Taschenrechner-Programm erstellt werden das zwei Zahlen (Operanden) und
einen Operator nach folgender Syntax einliest:
Operand1 Operator Operand2
Als Operator sind nur +, -, * und / zulässig. Wird ein anderer Operator eingegeben, so soll eine
entsprechende Fehlermeldung ausgegeben werden. Die Operanden sind als short-Werte
einzulesen.
Bei korrekter Eingabe ist das Ergebnis der eingegebenen Formel als Ganzzahl zu berechnen und
auszugeben.
Die Programmausgabe:
Formel nach folgender Syntax eingeben:
Operand1 Operator Operand2
Operator kann sein: + - * /
Formel ? 3 * 5
Ergebnis: 15
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le05\Lif.
So, in der nächsten Lektion werden wir uns mit Mehrfach-Verzweigungen beschäftigen.
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (8 von 9) [17.05.2005 11:38:25]
if-Verzweigung
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le05/le05_01.htm (9 von 9) [17.05.2005 11:38:25]
switch-case Verzweigung
switch-case Verzweigung
Die Themen:
Syntax
Ablauf der Verzweigung
break Anweisung
Aktionen im case-Zweig
Beispiel und Übung
Syntax
Sollen je nach Inhalt einer
ganzzahligen Variable
verschiedene Anweisungen
durchlaufen werden, so
kann dies, wie
nebenstehend dargestellt,
durch geschachtelte IFELSE Anweisungen
erfolgen. Im Beispiel
werden je nachdem, ob die
Variable var den Wert 1, 2
oder 3 enthält, die
Anweisungen Aktion1,
Aktion2 oder Aktion3
ausgeführt. Enthält var
einen anderen Wert, so
werden die Anweisungen
Aktion4 ausgeführt. Aber
das kennen Sie ja schon von
der letzten Lektion her.
if (var == 1)
{Aktion1}
else if (var == 2)
{Aktion2}
else if (var == 3)
{Aktion3}
else
{Aktion4}
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (1 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
Im ersten Satz auf dieser Seite wurde absichtlich nur die ganzzahlige Variable erwähnt.
Wie Sie aus der Lektion über Vergleichsoperator noch wissen sollten, sollten Sie wegen
Rundungsfehler niemals Gleitkommavariablen auf Gleichheit abprüfen!
Anstelle der oben angegebenen geschachtelten IF-ELSE Anweisung kann aber auch die
Mehrfach-Verzweigung SWITCH-CASE eingesetzt werden. Sie hat folgende Syntax:
switch (Ausdruck)
{
case K1:
Aktion1
[break;]
case K2:
Aktion2
[break;]
....
case Kx:
AktionX
[break;]
[default:
AktionY]
}
Beachten Sie bitte genau die Syntax! Der Ausdruck in der switch-Anweisung muss in runden
Klammern stehen und nach dem Ausdruck folgt immer ein Block {....}.
Ablauf der Verzweigung
Der Ablauf der Mehrfach-Verzweigung ist nun Folgender:
Zuerst wird der Ausdruck in der switch-Anweisung berechnet. Dieser Ausdruck muss
hier ein ganzzahliges Ergebnis liefern. Gleitkomma-Ausdrücke sind nicht erlaubt!
Danach wird das Ergebnis des Ausdrucks mit den Literalen/Konstanten hinter den
Schlüsselwörtern case verglichen. Stimmt das Ergebnis des Ausdrucks mit einem der
angegebenen Werte überein, so wird mit den Anweisungen nach der entsprechenden
case-Anweisung fortgefahren.
Stimmt das Ergebnis mit keinem der aufgeführten Werte überein, so wird mit den
Anweisungen nach der default-Anweisung fortgefahren.
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (2 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
Das Beispiel rechts
entspricht damit der am
Anfang der Lektion
dargestellten
verschachtelten IF-ELSE
Anweisung.
switch (var)
{
case 1:
Aktion1
break;
case 2:
Aktion2
Der default-Zweig ist
break;
optional, d.h. er muss nicht case 3:
zwingend vorhanden sein.
Aktion3
Fehlt der default-Zweig und
break;
stimmt die Auswertung des default:
switch-Ausdrucks mit
Aktion4
keinem der aufgeführten
}
Werte überein, so wird
.... // Nächste Anweisung nach SWITCH-CASE
einfach nichts ausgeführt
und es geht mit der
nächsten Anweisung weiter
die nach der geschweiften
Klammer zu des switchBlocks folgt.
break Anweisung
Kommen wird nun zum Schlüsselwort break innerhalb der SWITCH-CASE Anweisung. Die
break-Anweisung bewirkt ein sofortiges Verlassen des switch-Blocks, d.h. nach dem break wird
als nächstes die Anweisung nach der geschweiften Klammer zu ausgeführt. Ohne breakAnweisung werden alle nach der Einsprungmarke (case x:) folgenden Anweisungen bis zum
Ende des switch-Blocks ausführt. Sehen Sie sich dazu die beiden nachfolgenden Beispiele sowie
ihre Ausgaben an:
SWITCH-CASE mit break-Anweisung
SWITCH-CASE ohne break-Anweisung
int var = 2;
....
switch (var)
{
case 1:
cout << "Variable gleich
1\n";
break;
case 2:
int var = 2;
....
switch (var)
{
case 1:
cout << "Variable gleich
1\n";
case 2:
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (3 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
cout << "Variable gleich
2\n";
break;
case 3:
cout << "Variable gleich
3\n";
break;
default:
cout << "Variable nicht
1,2,3\n";
}
cout << "Variable gleich
2\n";
Programmausgabe:
Programmausgabe:
Variable gleich 2
Variable gleich 2
Variable gleich 3
Variable nicht 1,2,3
case 3:
cout << "Variable gleich
3\n";
default:
cout << "Variable nicht
1,2,3\n";
}
Aktionen im case-Zweig
Die innerhalb eines case-Zweigs stehenden Aktionen können auch aus mehreren C++
Anweisungen bestehen. Eine Blockung {...} wie bei der IF-ELSE Anweisung ist hier nicht
notwendig. Innerhalb eines case-Zweigs können alle C++ Anweisung stehen mit einer
Ausnahme: Sie dürfen hier ohne besondere Vorkehrung keine Variablen definieren! Wollen Sie
innerhalb eines case-Zweigs Variablen definieren, so müssen Sie einen entsprechenden Block
{....} einfügen, der die Lebensdauer der Variablen festlegt.
Und auch das ist möglich:
sollen für verschiedene
Werte die gleichen
Aktionen durchgeführt
werden, so können auch
mehrere case-Zweige
hintereinander gesetzt
werden. Auch die
Reihenfolge der caseAnweisungen ist beliebig,
d.h. sie müssen nicht in
sortierter Reihenfolge
angegeben werden. Im
Beispiel rechts wird eine
char-Variable ausgewertet.
Und bei der Auswertung
char alpha;
....
switch (alpha)
{
case 'e':
case 'E':
Aktion1;
break;
case 'a':
case 'A':
Aktion2;
}
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (4 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
spielt es dann keine Rolle,
ob ein Groß- oder
Kleinbuchstabe in der
Variable abgelegt ist, es
wird immer die gleiche
Aktion ausgeführt.
Sie können übrigens mithilfe der Bibliotheksfunktionen
int std::toupper(int toConvert);
int std::tolower(int toConvert);
Kleinbuchstaben in Großbuchstaben umwandeln und umgekehrt. Der umzuwandelnde
Buchstabe ist als int-Wert zu übergeben. Beide Funktionen liefern als Returnwert den
entsprechenden gewandelten Buchstaben. Handelt es sich beim übergebenen Wert nicht
um einen Buchstaben sondern z.B. um die ASCII-Zahl '9', so wird der Originalwert
zurückgeliefert. Die Funktion ist in der Header-Datei cctype deklariert.
Ja und das war's auch schon. Jetzt kommt das obligatorische Beispiel und dann die Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel entspricht von der Funktion her der Übung aus der letzten Lektion.
Es simuliert ein kleines Taschenrechner-Programm, das zwei Zahlen (Operanden) und einen
Operator nach folgender Syntax einliest:
Operand1 Operator Operand2
Als Operator sind wieder nur +, -, * und / zugelassen. Wird ein anderer Operator eingegeben,
so wird eine entsprechende Fehlermeldung ausgegeben werden. Die Operanden werden als
short-Werte eingelesen.
Bei korrekter Eingabe wird das Ergebnis der eingegebenen Formel berechnet und
ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le05\prog\Bswitch
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (5 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
Die Programmausgabe:
Formel nach folgender Syntax eingeben:
Operand1 Operator Operand2
Operator kann sein: + - * /
Formel ? 3 * 5
Ergebnis: 15
Das Programm:
// C++ Kurs
// Beispiel zur switch-case Verzweigung
// HINWEIS: Das Beispiel verwendet eine goto-Anweisung
//
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
using std::cout;
using std::endl;
using std::cin;
// Hauptprogramm
int main ()
{
// Variablen fuer die Operanden
short operand1, operand2;
// Variable fuer den Operator
char oper;
// Einlesen der Formel
cout << "Formel nach folgender Syntax eingeben:\n";
cout << "Operand1 Operator Operand2\n";
cout << "Operator kann sein: + - * /" << endl;
cout << "Formel ? ";
cin >> operand1 >> oper >> operand2;
// Ergebnissausgabe vorbereiten
cout << "Ergebnis: ";
// Nun den Operator auswerten
switch (oper)
{
case '+':
cout << operand1+operand2;
break;
case '-':
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (6 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
cout << operand1-operand2;
break;
case '*':
cout << operand1*operand2;
break;
case '/':
cout << operand1/operand2;
break;
default:
cout << "Nur die Operatoren +, -, * und / zugelassen!";
}
// Zeilenvorschub ausgeben
cout << endl;
}
Die Übung:
Schreiben Sie ein Programm, das nach dem ohmschen Gesetz
U = R * I
d.h.
Spannung = Widerstand * Strom
entweder die Spannung, den Widerstand oder den Strom berechnet. Was berechnet werden
soll, ist einzulesen (siehe Programmausgabe). Je nach gesuchtem Wert sind dann die beiden
anderen Werte einzulesen. Für die einzulesenden Werte sind Gleitkommazahlen zu
verwenden.
Die Ausgabe des berechneten Werts soll mit max. 2 Dezimalstellen erfolgen.
Hinweis: Mit obiger Formel gilt dann:
Widerstand = Spannung/Strom
bzw.
Strom
= Spannung/Widerstand
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (7 von 8) [17.05.2005 11:38:27]
switch-case Verzweigung
Die Programmausgabe:
Sie koennen nun mit Hilfe des Ohmschen Gesetzes
1. Die Spannung ueber einen Widerstand berechnen
2. Den Strom durch einen Widerstand berechnen
3. Den Widerstand selbst berechnen
Bitte geben Sie die entsprechende Ziffer ein (1..3): 2
Spannung (in Volt) und Widerstand (in Ohm) eingeben: 235. 1000.
Der Strom durch den Widerstand betraegt 0.23 Ampere
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le05\Lswitch.
Ich hoffe die Übung war nicht all zu schwer. In der nächsten Lektion geht's dann mit den
Schleifen los.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le05/le05_02.htm (8 von 8) [17.05.2005 11:38:27]
for-Schleife
for-Schleife
Die Themen:
Syntax
Initialisierungsausdruck
Abbruchbedingung
Schleifenrumpf
Die Schleifenaktionen
Beispiel und Übung
Syntax
Sollen eine oder mehrere Anweisungen wiederholt ausgeführt werden, so werden hierfür Schleifen eingesetzt.
In dieser Lektion lernen Sie eine der Schleifen kennen, die FOR-Schleife.
Die FOR-Schleife wird hauptsächlich immer dann eingesetzt, wenn bereits vor dem Eintritt in die Schleife
bekannt ist, wie oft die in ihr enthaltenen Anweisungen ausgeführt werden sollen. Die FOR-Schleife hat
folgende Syntax:
for (Ausdruck1; Ausdruck2; Ausdruck3)
Aktion;
Beachten Sie hierbei bitte, dass die Ausdrücke in der Klammer jeweils durch ein Semikolon voneinander
getrennt werden und nach der Klammer der FOR-Schleife kein Semikolon steht. Sehen wir uns nun Schritt
für Schritt den Aufbau der Schleife an.
Initialisierungsausdruck
Der erste Ausdruck innerhalb der
for (int index=0; ... ; ...)
Klammer ist der Initialisierungsausdruck.
Aktion;
Er wird nur einmal ausgeführt und zwar
bevor die erste Schleifenaktion ausgeführt
wird.
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (1 von 7) [17.05.2005 11:38:29]
for-Schleife
Im Regelfall initialisiert er eine Schleifenvariable, d.h. er legt den Startwert fest, mit dem die Schleife
begonnen wird. Im Beispiel rechts wird innerhalb der FOR-Schleife die Variable index definiert und mit dem
Wert 0 initialisiert. Laut ANSI C++ gilt diese Variable dann aber nur innerhalb der FOR-Schleife.
Der auf der CD-ROM befindliche MinGW C++ Compiler hält sich an diese ANSI C++ Regel. Der
MICROSOFT VC++ 6.0 Compiler dagegen nicht, d.h. die innerhalb der FOR-Schleife definierte
Variable ist auch nach der FOR-Schleife noch gültig!
Der Initialisierungsausdruck kann auch
for (count=10, loop=0; ... ; ...)
aus mehreren Anweisungen bestehen. Die
Aktion;
Anweisungen werden dann durch Komma
(kein Semikolon!) getrennt, so wie rechts
dargestellt. Selbstverständlich müssen die
im Beispiel rechts verwendeten Variablen
count und loop vorher irgendwo definiert
worden sein.
Abbruchbedingung
Der nächste Ausdruck bestimmt das Ende for (int index=0; index<10; ...)
der Schleife, d.h. bei welcher Bedingung
Aktion;
die Schleife verlassen wird. Dieser
Ausdruck ist in der Regel ein
count = 10;
vergleichender Ausdruck. Die Schleife
for (loop=0; count<10; ...)
wird solange durchlaufen, wie die
Aktion;
Auswertung dieses Ausdrucks true ergibt.
Aber Achtung!
Ergibt die Auswertung des Ausdruck2 schon vor dem Eintritt in die Schleife false, so wird die
Schleifenaktion nicht ausgeführt! Dies ist z.B. in der zweiten for-Schleife oben der Fall.
Im obigen Beispiel bestand die
for (index=0; (index<10) && (!done); ...)
Abbruchbedingung nur aus einem
Aktion;
einfachen vergleichenden Ausdruck.
Genauso gut können hier aber auch
Aber nie so!
mehrere Bedingungen stehen, die dann
durch entsprechende logische Operatoren
for (index=0; (index<10) & (!done); ...)
verknüpft werden. Im ersten Beispiel
Aktion;
rechts wird die FOR-Schleife solange
durchlaufen, solange index kleiner 10 ist
und done gleich false (Negation
beachten!). Das zweite Beispiel enthält
eine 'hinterlistige' Falle: der Compiler
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (2 von 7) [17.05.2005 11:38:29]
for-Schleife
meldet Ihnen hier keinen Fehler
(höchstens eine Warnung). Hier werden
die Ergebnisse der Ausdrücke bitweise
verundet, und das ist nun einmal etwas
anderes wie eine logische Verundung.
Die Anzahl der Bedingungen die Sie in einer FOR-Schleife angegeben können ist nicht begrenzt. Jedoch wird
es bereits ab 3 Bedingungen schon etwas unübersichtlich, und damit später auch schwerer zu warten.
Versuchen Sie deshalb die Anzahl der Bedingung zu minimieren.
Und noch ein Hinweis bevor wir zum letzten Ausdruck übergehen: setzen Sie die einzelnen Bedingungen
immer in Klammern. Dies ist zwar keine Vorschrift, jedoch vermeiden die Klammern oft unliebsame
Überraschungen in Bezug auf die Rangfolge der Operatoren. Oder wissen Sie noch auswendig, ob der
KLEINER-Operator < vor dem UND-Operator && ausgewertet wird?
Aktion pro Schleifendurchlauf
So, jetzt zum letzten Ausdruck in der
for (int index=0; index<10; index++)
Klammer. Er legt die Aktion fest, die nach
Aktion;
dem Ausführen der Schleifenaktion und
vor dem erneuten Auswerten der
Abbruchbedingung ausgeführt wird.
In der Praxis wird hier die mit dem ersten Ausdruck initialisierte Schleifenvariable verändert. Auch dieser
Ausdruck kann durchaus aus mehreren Anweisungen bestehen, die wieder durch Kommas getrennt werden.
Der Lesbarkeit wegen sollten Sie jedoch hier wirklich nur die Schleifenvariable beeinflussen und alles andere
mit der im Anschluss beschriebenen Schleifenaktion durchführen. Im Beispiel oben wird die Schleifenvariable
um eins erhöht.
Mit dem bisherigen Wissen hat eine FOR-Schleife damit folgenden internen Ablauf:
Reale FOR-Schleife
Pseudo FOR-Schleife
for (Ausdruck1; Ausdruck2; Ausdruck3)
Aktion;
Berechnen von Ausdruck1
Sprung zur Marke 'Schleifenende'
Schleife:
Aktion;
Berechnen von Ausdruck3
Schleifenende:
if Ausdruck2 gleich true
Sprung zur Marke 'Schleife'
nächste Anweisung nach FOR-Schleife
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (3 von 7) [17.05.2005 11:38:29]
for-Schleife
Jeder der 3 Ausdrücke ist optional, jedoch for (...;;...)
müssen die beiden Semikolon innerhalb
Aktion;
der Klammer immer angegeben werden.
Eine FOR-Schleife ohne
Abbruchbedingung erzeugt z.B. eine
Endlos-Schleife.
// Ein Schleife ohne Abbruch
Schleifenrumpf
Kommen wir nun zum letzten Teil der
FOR-Schleife, der Schleifenaktion. Die
Schleifenaktion besteht standardmäßig
nur aus einer einzigen Anweisung, so wie
im ersten Beispiel rechts. Sollen mehrere
Anweisungen ausgeführt werden, was
auch fast immer der Fall ist, so sind die
Anweisungen in einen Block {...}
einzuschließen (zweites Beispiel rechts).
Auch die Angabe der Aktion ist
(zumindest in der Theorie) optional. Aus
diesem Grund liefert die dritte rechts
dargestellt FOR-Schleife beim Übersetzen
auch keinen Fehler. Hier wird einfach 10mal gar nichts getan. Gute Compiler (und
die meisten sind gut!) optimieren eine
solche Schleife weg. Und noch bessere
Compiler geben dann auch noch eine
Warnung aus.
for (int index=0; index<10; index++)
cout << "Schleifenzähler: " << index
<< endl;
for (int index=0; index<10; index++)
{
.... // Hier können jetzt mehrere
.... // Anweisungen stehen
}
Aber Achtung! Leerschleife
for (int index=0; index<10; index++);
cout << "Schleifenzähler: " << index
<< endl;
Tja, und das war's auch schon. Sehen Sie sich jetzt das nachfolgende Beispiel an und dann kommt wieder Ihre
Übung.
Beispiel und Übung
Das Beispiel:
Das Programm berechnet die Bahngleichung eines schrägen Wurfs nach folgender Formel:
Weite = (Abwurfgeschwindigkeit2 * sin(2*Abwurfwinkel)) / G
Höhe = (Abwurfgeschwindigkeit2 * sin(Abwurfwinkel)2) / (2*G)
Die Bibliotheksfunktion sin(...) zur Berechnung des Sinus hat folgende Funktionsdeklaration in cmath:
double erg = sin (double rad);
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (4 von 7) [17.05.2005 11:38:29]
for-Schleife
sin(...) erwartet den Winkel als Radiant und nicht in Grad! Die Abwurfgeschwindigkeit ist in beiden
Formeln in m/s und G ist die Erdbeschleunigung 9.81m/s2. Beide Formeln liefern das Ergebnis in m zurück.
Beachten Sie im Beispiel, dass als Schleifenvariable der for-Schleife eine Gleitkommazahl verwendet wird.
Die Abbruchbedingung der for-Schleife prüft auf kleiner als ab und nicht auf Gleichheit! Außerdem wird
die Schleifenvariable hier pro Durchgang um 5 erhöht.
Das unten stehende Beispiel finden Sie auch unter: le05\prog\Bfor
Die Programmausgabe:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
Winkel:
10.00
15.00
20.00
25.00
30.00
35.00
40.00
45.00
50.00
55.00
60.00
65.00
70.00
75.00
80.00
85.00
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
Grad
->
->
->
->
->
->
->
->
->
->
->
->
->
->
->
->
Wurfweite: 3.49 m, Wurfhoehe: 0.15 m
Wurfweite: 5.10 m, Wurfhoehe: 0.34 m
Wurfweite: 6.55 m, Wurfhoehe: 0.60 m
Wurfweite: 7.81 m, Wurfhoehe: 0.91 m
Wurfweite: 8.83 m, Wurfhoehe: 1.27 m
Wurfweite: 9.58 m, Wurfhoehe: 1.68 m
Wurfweite: 10.04 m, Wurfhoehe: 2.11 m
Wurfweite: 10.19 m, Wurfhoehe: 2.55 m
Wurfweite: 10.04 m, Wurfhoehe: 2.99 m
Wurfweite: 9.58 m, Wurfhoehe: 3.42 m
Wurfweite: 8.83 m, Wurfhoehe: 3.82 m
Wurfweite: 7.81 m, Wurfhoehe: 4.19 m
Wurfweite: 6.55 m, Wurfhoehe: 4.50 m
Wurfweite: 5.10 m, Wurfhoehe: 4.76 m
Wurfweite: 3.49 m, Wurfhoehe: 4.94 m
Wurfweite: 1.77 m, Wurfhoehe: 5.06 m
Das Programm:
// C++ Kurs
// Beispiel zur for-Schleife
//
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
#include <cmath>
using std::cout;
using std::endl;
// Hauptprogramm
int main ()
{
// Konstanten definieren
const double GRAVITATION = 9.81;
const double GRAD2RAD = 2.0 * 3.1416 / 360.0;
// Abwurfgeschwindigkeit
double geschw = 10.0;
// Ausgabe mit Festkomma und 2 Nachkommastellen
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (5 von 7) [17.05.2005 11:38:29]
for-Schleife
cout << std::fixed << std::setprecision(2);
// Abwurfwinkel im Bereich 10...85 variieren
for (double winkel=10.0; winkel<90.0; winkel += 5.0)
{
// Winkel von Grad in Radiant umrechnen
double wrad = winkel*GRAD2RAD;
// Wurfweite berechnen
double weite = (geschw*geschw*sin(2.0*wrad))/GRAVITATION;
// Wurfhoehe berechnen
double hoehe = (geschw*geschw)*(sin(wrad)*sin(wrad))/(2.0*GRAVITATION);
// Ergebnis ausgeben
cout << "Winkel: " << winkel << " Grad -> Wurfweite: "
<< std::setw(6) << weite << " m,"
<< " Wurfhoehe: " << std::setw(6) << hoehe << " m" << endl;
}
}
Die Übung:
Es ist eine Tabelle der ASCII-Zeichen mit den dezimalen Codes 32 bis 127 wie unten dargestellt auszugeben.
Zuerst ist der jeweilige ASCII-Code 3-stellig auszugeben und dann das dazugehörige ASCII-Zeichen. Die
Tabelle soll 5 Spalten besitzen.
Verwenden Sie für die Schleifenvariable eine unsigned char Variable. Wie Sie eine unsigned char
Variable als Wert (und nicht als ASCII-Zeichen) ausgeben, das erfahren Sie hier ( ).
Hinweis: Die ASCII-Codes kleiner 32 sind nicht-druckbare Zeichen wie z.B.
Tabulator oder Zeilenvorschub und deshalb nicht innerhalb einer Tabelle
darstellbar.
Die Programmausgabe:
32
37
42
47
52
57
62
67
72
77
82
87
92
97
102
%
*
/
4
9
>
C
H
M
R
W
\
a
f
33
38
43
48
53
58
63
68
73
78
83
88
93
98
103
!
&
+
0
5
:
?
D
I
N
S
X
]
b
g
34
39
44
49
54
59
64
69
74
79
84
89
94
99
104
"
'
,
1
6
;
@
E
J
O
T
Y
^
c
h
35
40
45
50
55
60
65
70
75
80
85
90
95
100
105
#
(
2
7
<
A
F
K
P
U
Z
_
d
i
36
41
46
51
56
61
66
71
76
81
86
91
96
101
106
$
)
.
3
8
=
B
G
L
Q
V
[
`
e
j
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (6 von 7) [17.05.2005 11:38:29]
for-Schleife
107
112
117
122
127
k
p
u
z
•
108
113
118
123
l
q
v
{
109
114
119
124
m
r
w
|
110
115
120
125
n
s
x
}
111
116
121
126
o
t
y
~
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie
die Lösung unter loesungen\le05\Lfor.
Eine relativ einfach erscheinende Übung, oder? Aber hier lag die Schwierigkeit im Detail. Haben Sie sich in
der Lösung einmal angesehen wie dort die Tabelle mit 5 Spalten erstellt wird?
So, weiter geht's in der nächsten Lektion mit einer anderen Schleifenart, der while-Schleife.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le05/le05_03.htm (7 von 7) [17.05.2005 11:38:29]
while-Schleifen
while-Schleifen
Die Themen:
Einleitung
while-Schleife
do-Schleife
Beispiel und Übung
Einleitung
Außer der in der vorherigen Lektion beschriebenen FOR-Schleife kennt C++ noch die WHILE-Schleife.
Während die FOR-Schleife immer dann eingesetzt wird, wenn schon vor dem Eintritt in die Schleife die
Abbruchbedingung bekannt ist, so wird die WHILE-Schleife dagegen hauptsächlich dann eingesetzt, wenn die
Abbruchbedingung erst innerhalb der Schleife selbst festgestellt werden kann.
Selbstverständlich können Sie jederzeit eine FOR-Schleife gegen eine WHILE-Schleife austauschen
und umgekehrt, wenn Sie etwas mehr Aufwand spendieren.
Und damit es mal wieder nicht gar zu einfach wird kennt C++ zwei Arten von WHILE-Schleifen:
●
●
die eigentliche WHILE-Schleife und
die DO-Schleife
while-Schleife
Bei der WHILE-Schleife wird vor dem Eintritt in die Schleife bereits abgeprüft, ob die Schleife überhaupt
durchlaufen werden muss. Sie hat folgende Syntax:
while (Ausdruck)
Aktion;
Ausdruck kann jeder C++ Ausdruck sein der entweder ein ganzzahliges oder boolsches Ergebnis liefert.
http://www.cpp-tutor.de/cpp/le05/le05_04.htm (1 von 5) [17.05.2005 11:38:30]
while-Schleifen
Sie erinnern sich doch noch: jeder Wert ungleich 0 wird auch als true interpretiert und der Wert 0 als
false!
Der Ausdruck selbst muss immer in Klammern stehen und nach der Klammer folgt (in der Regel) kein
Semikolon!
Die auf den Ausdruck folgende Aktion
wird nun sooft wiederholt, wie die
Auswertung des Ausdrucks true ergibt.
Und auch hier gilt, wie bei der FORSchleife, dass die Aktion standardmäßig
nur aus einer Anweisung besteht. Sollen
mehrere Anweisungen ausgeführt werden,
was der Regelfall wohl ist, so müssen die
Anweisungen in einen Block {...}
eingeschlossen werden.
....
bool done = false;
while (!done)
{
.... // Hier irgend wann done auf
.... // auf true setzen damit Schleife
... // beendet wird.
}
Eine häufige Fehlerquelle bei dieser Schleife ist das Vergessen der Initialisierung des
Abbruchkriteriums. Wird das Abbruchkriterium (im obigen Beispiel die Variable done) nicht
initialisiert, so wird die Schleife unter Umständen nicht durchlaufen!
do-Schleife
Diese Schleife wird immer mindestens einmal durchlaufen, da das Abbruchkriterium erst am Ende der
Schleife abgeprüft wird. Die Schleife besitzt folgende Syntax:
do
Aktion;
while (Ausdruck);
Für den Ausdruck und die Aktion gilt das
Gleiche wie bei der WHILE-Schleife. Die
Aktion besteht standardmäßig aus einer
Anweisung, mehrere Anweisungen sind
also wieder in einen Block {...}
einzuschließen. Und Ausdruck kann
wieder jeder beliebige ganzzahlige oder
boolsche C++ Ausdruck sein. Die Schleife
wird auch hier solange durchlaufen, wie
die Auswertung des Ausdrucks true
ergibt.
bool done = false;
do
{
.... // Hier irgend wann done auf
.... // auf true setzen damit Schleife
... // beendet wird.
} while (!done);
Und nochmals Achtung: Beachten Sie bitte, dass nach der Klammer des Ausdrucks hier ein
Semikolon steht!
http://www.cpp-tutor.de/cpp/le05/le05_04.htm (2 von 5) [17.05.2005 11:38:30]
while-Schleifen
Auch mit WHILE/DO-Schleifen lassen
sich Endlos-Schleifen erzeugen, wie die
nebenstehenden Beispiele aufzeigen. Als
Ausdruck wird hier ganz einfach der
Wert true oder eine Ganzzahl-Konstante
ungleich 0 eingesetzt.
while(true)
Aktion;
do
Aktion
while (1);
Ja und das war's auch schon. Viel mehr gibt's hier nicht zu sagen. Jetzt kommt das Beispiel und dann sind Sie
wieder dran.
Beispiel und Übung
Das Beispiel:
Das Programm liest aus der Textdatei while.dat verschiedene double-Werte in einer WHILE-Schleife ein
und berechnet daraus den Mittelwert.
Um das Ende der Datei festzustellen, wird nach dem Einlesen eines Wertes mit der ifstream
Memberfunktion eof(...) das Erreichen des Dateiendes abgeprüft. Wie in der Lektion über Dateistreams
bereits erläutert, liefert die Memberfunktion true wenn das Dateiende erreicht wurde.
Die eingelesenen Werte sowie der Mittelwert werden zur Kontrolle ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le05\prog\Bwhile
Die Programmausgabe:
1. Wert: 10.1
2. Wert: 20.2
3. Wert: 30.3
4. Wert: 40.4
5. Wert: 50.5
6. Wert: 60.6
Mittelwert der Zahlen: 35.35
Das Programm:
// C++ Kurs
// Beispiel zur while-Schleife
//
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <fstream>
using std::cout;
using std::endl;
http://www.cpp-tutor.de/cpp/le05/le05_04.htm (3 von 5) [17.05.2005 11:38:30]
while-Schleifen
// Konstante fuer Dateinamen definieren
const char* const pFILENAME = "while.dat";
// Variablen definieren
double summe;
short zaehler;
// Hauptprogramm
int main ()
{
// Eingabestream einmal anders oeffen
std::ifstream InFile;
InFile.open(pFILENAME);
// Fehler abfangen!
if (!InFile)
{
cout << "Datei " << pFILENAME << " konnte nicht geoeffnet werden!\n";
exit (1);
}
// Summe und Zaehler initialisieren
summe = 0.0;
zaehler = 0;
// Daten aus Datei einlesen bis Dateiende
do
{
// Blocklokale Variable
double wert;
// Wert einlesen
InFile >> wert;
// Falls nicht Dateiende erreicht
if (!InFile.eof())
{
// Wert zur Kontrolle ausgeben und
// gleichzeitig Zaehler erhoehen
cout << ++zaehler << ". Wert: " << wert << endl;
// Wert aufsummieren
summe += wert;
}
} while (!InFile.eof()); // Bei Dateiende Schleife beenden
// Datei wieder schliessen
InFile.close();
// Falls Werte eingelesen wurden
if (zaehler)
// Mittelwert ausgeben
cout << "Mittelwert der Zahlen: " << summe/zaehler << endl;
else
// sonst Meldung ausgeben
cout << "Keine Werte eingelesen!\n";
}
http://www.cpp-tutor.de/cpp/le05/le05_04.htm (4 von 5) [17.05.2005 11:38:30]
while-Schleifen
Die Übung:
Für eine einmalige Einlage auf einem Sparkonto ist der sich ergebende Kontostand nach jedem Jahr zu
berechnen und auszugeben. Der Kontostand ist mit 2 Nachkommastellen auszugeben.
Die Einlage so wie der Zinssatz (in Prozent) sind über die Tastatur einzulesen.
Danach ist der Kontostand solange auszugeben, bis sich entweder die Einlage mindestens verdoppelt hat
(siehe Ausgabe unten) oder aber 10 Jahre abgelaufen sind.
Die Programmausgabe:
Welcher Betrag
Und zu welchem
Am Ende des 1.
Am Ende des 2.
Am Ende des 3.
Am Ende des 4.
Am Ende des 5.
Am Ende des 6.
Am Ende des 7.
soll verzinst werden? 100
Zinssatz (in Prozent)? 12
Jahr: 112.00
Jahr: 125.44
Jahr: 140.49
Jahr: 157.35
Jahr: 176.23
Jahr: 197.38
Jahr: 221.07
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie
die Lösung unter loesungen\le05\Lwhile.
Jetzt kennen Sie alle Schleifentypen die C++ zur Verfügung stellt. In der nächsten Lektion lernen Sie noch
zwei weitere Anweisungen kennen, denen im Zusammenhang mit Schleifen eine besondere Bedeutung zu
kommt.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le05/le05_04.htm (5 von 5) [17.05.2005 11:38:30]
break und continue
break und continue
Die Themen:
break Anweisung
continue Anweisung
Beispiel
break Anweisung
Die Anweisungen break und continue dienen zur Ablaufsteuerung von Schleifen.
Die break-Anweisung darf nur innerhalb
int main ( )
einer FOR- oder WHILE-Schleife oder in
{
einem case-Zweig der SWITCH-CASE
char inp;
Verzweigung stehen. Sie bewirkt das
....
sofortige Verlassen der aktuellen Schleife
for (;;)
oder der SWITCH-CASE Verzweigung. Sind
{
mehrere Schleifen verschachtelt, so wird nur
....
die aktuelle Schleife abgebrochen, die
cout << "Auswahl?
äußeren Schleifen werden nicht beeinflusst.
cin >> inp;
if (inp == 'E')
Im Beispiel rechts wird die prinzipielle
break;
Bearbeitung eines Menüs innerhalb einer
....
Endlos-Schleife dargestellt. Nach der (nicht
}
dargestellten) Ausgabe des Menüs wird die
....
Auswahl eingelesen. Wird als Auswahl der
}
Buchstabe 'E' eingegeben, wird die EndlosSchleife zur Menü-Auswahl beendet und mit
den nachfolgenden Anweisungen
fortgefahren..
continue Anweisung
http://www.cpp-tutor.de/cpp/le05/le05_05.htm (1 von 4) [17.05.2005 11:38:32]
// Endlos-Schleife
//
";
//
//
//
Menü ausgeben
Auswahl einlesen
Falls Auswahl E
Schleife beenden
break und continue
Die continue-Anweisung ist nur innerhalb
....
einer FOR- oder WHILE-Schleife erlaubt.
int main ( )
Sie bewirkt dass die restlichen, der continue- {
Anweisung folgenden Anweisungen
....
übersprungen werden. Die Schleife selbst
for (int index=0; index<10; index++)
wird aber nicht verlassen. Bei einer FOR{
Schleife wird nach dem continue mit der
....
Auswertung des letzten Ausdrucks in der
if (index == 5)
// Falls index 5
FOR-Klammer (Aktion pro
continue;
// Rest überspringen
Schleifendurchlauf) fortgefahren.
....
// Hier nur wenn
....
// index ungleich 5
Im Beispiel rechts wird der Rest der Schleife
}
übersprungen wenn der Schleifenzähler
....
index gleich 5 ist.
}
Jetzt folgt noch das Beispiel und eine Übung schenken wir und dieses Mal.
Beispiel
Das Beispiel:
Das Programm berechnet aus einer unbestimmten Anzahl von einzugebenden Zahlen den Mittelwert. Das
Einlesen der Zahlen erfolgt in einer Endlos-Schleife.
Werden negative Werte eingegeben, so wird nur eine Meldung ausgegeben und die Eingabe ignoriert.
Bei Eingabe des Wertes 0 wird die Endlos-Schleife verlassen und der Mittelwert aller Eingaben berechnet.
Das unten stehende Beispiel finden Sie auch unter: le05\prog\Bbreak
Die Programmausgabe:
1. Zahl: 4
2. Zahl: 3
3. Zahl: -3
Keine negativen Zahlen erlaubt!
3. Zahl: 1
4. Zahl: 8
5. Zahl: 0
Mittelwert der Zahlen: 4
http://www.cpp-tutor.de/cpp/le05/le05_05.htm (2 von 4) [17.05.2005 11:38:32]
break und continue
Das Programm:
// C++ Kurs
// Beispiel zu break und continue
//
// Zuerst Dateien iostream und iomanip einbinden
#include <iostream>
#include <fstream>
// Nun Namensraum auf std setzen
using std::cout;
using std::endl;
using std::cin;
// Hauptprogramm
int main ()
{
// Variablen definieren und initialisieren
int eingabe;
int summe = 0;
int zaehler = 1;
// Endlos-Schleife fuer die Eingabe
while(true)
{
// Zahl einlesen
cout << zaehler << ". Zahl: ";
cin >> eingabe;
// Falls Eingabe 0 war, Schleife verlassen
if (eingabe == 0)
break;
// Falls Eingabe negativ, Eingabe ignorieren
if (eingabe < 0)
{
cout << "Keine negativen Zahlen erlaubt!\n";
continue;
}
// Sonst Summe und Zaehler erhoehen
summe += eingabe;
zaehler++;
}
// Zaehler korrigieren da um 1 zu hoch
zaehler--;
// Nun Mittelwert berechnen
if (zaehler != 0)
{
cout << "Mittelwert der Zahlen: ";
cout << static_cast<double>(summe)/static_cast<double>(zaehler) << endl;
}
else
cout << "Keine Werte eingegeben!\n";
http://www.cpp-tutor.de/cpp/le05/le05_05.htm (3 von 4) [17.05.2005 11:38:32]
break und continue
}
Hiermit haben Sie eine weitere Lerneinheit des Kurses (hoffentlich) erfolgreich beendet.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le05/le05_05.htm (4 von 4) [17.05.2005 11:38:32]
Felder und C-Strings
Felder und C-Strings
Die Themen:
Eindimensionale Felder
Mehrdimensionale Felder
Zugriff auf Feldelemente
Initialisierung eines Feldes
Felder und Zeiger
C-Strings und Felder
Beispiel und Übung
Eindimensionale Felder
Ein häufiges Problem in der Programmierung ist das Abspeichern von mehreren Daten des selben
Datentyps, wie z.B. eine Messwertreihe. Hierzu können u.a. Felder (auch Arrays genannt) eingesetzt
werden. Ein Feld wird wie folgt definiert:
DTYP Feldname[DIM];
DTYP kann jeder beliebige Datentyp, wie z.B. short sein. Der Feldname entspricht dem Variablennamen
einer normalen Variable. Innerhalb der eckigen Klammer steht die Feldgröße DIM. Sie gibt die
maximale Anzahl der Daten an, die in einem Feld abgespeichert werden können. Selbstverständlich
müssen die abzuspeichernden Daten aber den gleichen Datentyp besitzen wie das Feld, oder zumindest
in diesen konvertierbar sein.
Für die Angabe der Feldgröße DIM gelten folgende Regeln:
●
●
Die Feldgröße muss eine Ganzzahl sein.
Die Feldgröße muss ein konstanter Ausdruck sein.
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (1 von 14) [17.05.2005 11:38:34]
Felder und C-Strings
Rechts sehen Sie einige Beispiele für
Felddefinitionen. Das erste Feld
values[...] kann maximal 40 shortWerte aufnehmen. Hier wird die
Feldgröße über ein Literal definiert.
Beim zweiten Feld text[...] erfolgt die
Definition der Feldgröße über eine
benannte Konstante. Nicht erlaubt
dagegen ist die dargestellte
Definition des Feldes digits[...], und
das obwohl die Variable max bei
ihrer Definition initialisiert wird.
Feldgrößen müssen durch einen
konstanten Ausdruck definiert
werden. Denn welche Größe sollte
das Feld besitzen wenn später im
Programm die Variable max z.B. auf
30 gesetzt wird?
const int SIZE = 10;
short int max = 20;
// Felddefinitionen
short values[40];
char text[SIZE];
// Das ist nicht erlaubt!
long digits[max];
Mehrdimensionale Felder
Doch das ist noch längst nicht alles was Felder bieten. Außer den vorgestellten eindimensionalen Felder
können auch mehrdimensionale Felder wie folgt definiert werden:
Datentyp Feldname[DIM1][DIM2]...;
Die Anzahl der Dimensionen ist nur
vom verfügbaren Speicherplatz
abhängig. Das erste Feld table im
Beispiel rechts ist ein 2dimensionales Feld. Sie können sich
unter einem 2-dimensionalen Feld
eine Art Tabelle mit Zeilen und
Spalten vorstellen. Das 2. Felder big
ist ein 3-dimensionales Feld, also
eine Art Karteikasten mit Tabellen
auf den einzelnen Karteikarten. Das
nachfolgende Bild soll diesen
Sachverhalt nochmals
verdeutlichen.
const int XSIZE = 10;
const int YSIZE = 50;
short table[XSIZE][YSIZE];
char big[10][10][5];
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (2 von 14) [17.05.2005 11:38:34]
Felder und C-Strings
Zugriff auf Feldelemente
Soviel zur Definition eines Feldes.
Sehen wir uns jetzt an, wie auf
Feldelemente zugegriffen wird. Um
auf ein einzelnes Element im Feld
zuzugreifen, wird zuerst der
Feldname angegeben, gefolgt von
einem eckigen Klammerpaar. In
dieser Klammer steht dann der
Index des gewünschten
Feldelements, wobei das erste
Element den Index 0 besitzt. So
greifen die ersten beiden
Zuweisungen rechts einmal auf das
erste und einmal auf das letzte
Element des Feldes array zu. Bei
mehrdimensionalen Feldern sind
entsprechend der Anzahl der
Felddimensionen mehrere Indizes in
Klammern anzugeben. Die letzte
Zuweisung rechts greift damit auf
das letzte Element im Feld lines zu.
// Konstanten-Definitionen
const int XSIZE=20, YSIZE=20;
// Felddefinition
short array[40];
char lines[XSIZE][YSIZE];
// Feldzugriffe
array[0] = 10;
short var = array[39];
char actChar = lines[XSIZE-1][YSIZE-1];
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (3 von 14) [17.05.2005 11:38:34]
Felder und C-Strings
Und noch einmal weil's so wichtig ist:
Das erste Element im Feld hat immer den Index 0 und das letzte Element damit den Index
GROESSE-1! Beachten Sie dies beim Zugriff auf das Feld. Der Compiler überprüft nicht ob
der angegebene Index auch innerhalb de Feldgrenzen liegt! Und auch zur Laufzeit erfolgt
keine Überprüfung des Feldzugriffs.
Initialisierung eines Feldes
Wie Sie sicher noch wissen, können Variablen bei ihrer Definition auch gleichzeitig initialisiert werden.
Und dies ist auch bei Feldern möglich. Nur sieht die Syntax hier ein klein wenig anders aus:
DTYP Feldname[DIM] = {Wert1, Wert2, ....};
Die einzelnen Initialwerte werden
hier in einer geschweiften Klammer
eingeschlossen und durch Komma
getrennt aufgelistet.
short array[] = {10, 20, 30, 40};
const int SIZE =
sizeof(array)/sizeof(array[0]);
....
Bei eindimensionalen Feldern gibt's noch einen schönen 'Nebeneffekt'. Hier kann die Angabe der
Feldgröße bei initialisierten Feldern entfallen. Das Feld wird dann vom Compiler genau so groß
angelegt, dass die angegebenen Werte darin Platz finden.
Nur stellt sich hier jedoch gleich das nächste Problem (das wir natürlich auch lösen). Wie können Sie
herausfinden, welchen Index dann das letzte Element im Feld besitzt? Wenn Sie die Feldgröße über eine
Konstante festgelegt haben, so kann diese Konstante zur Berechnung des letzten Elements herangezogen
werden. Was aber wenn das Feld ohne explizite Größenangabe erstellt wurde? Hier hilft uns der
sizeof(...) Operator weiter. Wie Sie vielleicht noch wissen, liefert dieser Operator den von einer Variablen
oder einem Datentyp belegten Speicherplatz in Bytes zurück. Und damit kann mit folgender Formel,
sozusagen nachträglich, die Anzahl der Elemente in einem Feld berechnet werden:
SIZE = sizeof(array) / sizeof(array[0]);
Hier wird die Größe des Feldes durch die Größe des ersten Elements im Feld (=Größe des Datentyps des
Feldes) dividiert, was dann die Anzahl der Feldelemente ergibt. Diese Berechnung wird schon beim
Übersetzen des Programms durchgeführt und nicht etwas zur Programmlaufzeit. Beachten Sie auch,
dass die so berechnete Feldgröße als Konstante abgelegt wird.
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (4 von 14) [17.05.2005 11:38:34]
Felder und C-Strings
Anstelle der obigen Berechnung hätten Sie auch schreiben können:
SIZE = sizeof(array) / sizeof(Datentyp des Feldes);
Diese Berechnung ist aber nicht ganz so flexibel wie die vorherige Berechnung, da der Datentyp
des Feldes direkt mit in die Berechnung eingeht. Ändern Sie zu einem spätern Zeitpunkt einmal
den Datentyp des Feldes, so dürfen Sie dann nicht vergessen, auch bei der Berechnung der
Feldgröße den Datentyp mit anzupassen.
Sehen Sie sich nun einmal das
Beispiel rechts an. Dort wird das
Feld array zur Aufnahme von 10
short-Werten definiert.
short array[10] = {10, 20};
In der Initialisierungsliste stehen aber nur zwei Werte. In diesem Fall werden die ersten beiden Elemente
mit 10 bzw. 20 initialisiert und die restlichen Elemente mit 0. Der damit schnellste Weg, ein
eindimensionales Feld explizit mit 0 vorzubelegen, besteht in der Ausführung der Anweisung
DTYP Feldname[DIM] = {0};
Die 0 in der Klammer müssen Sie immer mit angeben da eine leere Klammer nicht zulässig ist.
So, sehen wir uns als nächstes an,
long array[][3] = {
wie mehrdimensionale Felder
{1,2,3},
initialisiert werden. Bei
{10,20,30}
mehrdimensionalen Feldern werden
};
die Initialwerte für die einzelnen
Dimensionen zunächst in
geschweiften Klammern
zusammengefasst.
Diese Klammern werden dann, durch Komma getrennt, aufgelistet. Zum Schluss wird die gesamte
Initialisierungsliste nochmals in eine übergeordnete geschweifte Klammer gepackt. Im Beispiel rechts
wird ein Feld mit der Dimension 2x3 definiert und entsprechend initialisiert. Beachten Sie bei
mehrdimensionalen Feldern, dass Sie hier nur die erste Dimension weglassen dürfen.
Felder und Zeiger
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (5 von 14) [17.05.2005 11:38:34]
Felder und C-Strings
Kommen wir jetzt wieder auf die so
beliebten Zeiger zu sprechen. Sicher
wissen Sie noch, dass Zeiger zur
Aufnahme von Adressen dienen.
Nun können in Zeigern aber nicht
nur Adressen von 'einfachen'
Variablen abgelegt werden, sondern
auch von Feldelementen und sogar
die Startadresse eines Feldes. Um
die Adresse eines bestimmten
Feldelements zu erhalten, wird wie
üblich vor das Feldelement der
Adressoperator & gestellt. Rechts
sehen Sie die Definitionen eines einund eines zweidimensionalen Feldes.
Im Anschluss daran werden die
entsprechenden Zeiger definiert, die
natürlich mit dem Datentyp des
Feldes übereinstimmen müssen.
Nach der Definition der Zeiger wird
den Zeigern die Adresse des jeweils
ersten Feldelements zugewiesen, d.h.
im Zeiger wird die Anfangsadresse
des Feldes abgelegt. Beachten Sie
bitte die zweite Zuweisung, die ohne
den Adressoperator! Sie entspricht
genau der ersten Zuweisung.
// Felddefinitionen
char single[40];
short multi[10][3];
// Zeigerdefinitionen
char *pSingle;
short *pMulti;
// Adresse des ersten Feldelements
pSingle = &single[0];
pSingle = single;
pMulti = &multi[0][0];
// Adresse des letzten Feldelements
pSingle = &single[39];
pMulti = &multi[9][2];
Merken Sie sich bitte folgenden Satz:
Der Name eines eindimensionalen Feldes entspricht immer der Anfangsadresse des Feldes. Dies
gilt aber nur für eindimensionale Felder!
Zugriff auf Felder über Zeiger
Doch was fangen wir mit einem Zeiger auf ein Feld an? Wissen Sie noch welche arithmetischen
Operationen auf Zeiger erlaubt sind? Nur die arithmetischen Operationen Addition und Subtraktion.
Und eine Addition des Wertes X auf einen Zeiger erhöht diesen nicht um X, sondern um
X*sizeof(Datentyp), d.h. zum Beispiel einen short-Zeiger um X*2 und einen long-Zeiger um X*4. Für die
Subtraktion gilt entsprechendes. Da eindimensionale Felder kontinuierlich im Speicher abgelegt werden,
kann nun wahlweise indiziert (über die eckige Klammer) oder mithilfe eines Zeigers auf die
Feldelemente zugegriffen werden.
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (6 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Sehen wir und dies an einem
einfachen Beispiel an. Im Beispiel
rechts wird ein Feld array definiert
und mit Werten initialisiert.
Beachten Sie, dass die Feldgröße
hier automatisch vom Compiler so
berechnet wird dass alle Werte
genau darin Platz finden. Da wir die
Werte nachher in einer FORSchleife ausgeben, benötigen wir
noch die Anzahl der Elemente im
Feld. Dies wird in der darunter
stehenden Anweisung mit der vorhin
angegebenen Formel berechnet. Im
Hauptprogramm wird dann der
Zeiger auf das Feld definiert, der
natürlich den gleichen Datentyp
haben muss wie das Feld, und auch
gleich mit der Anfangsadresse des
Feldes initialisiert. Anschließend
wird eine FOR-Schleife so oft
durchlaufen, wie Elemente im Feld
enthalten sind. Beachten Sie hier
bitte, dass die FOR-Schleife bei 0
beginnt und folglich bei SIZE-1
enden muss, deshalb die Abfrage auf
KLEINER_ALS. Innerhalb der
FOR-Schleife werden durch
Dereferenzierung des Zeigers die
einzelnen Feldelemente ausgegeben,
wobei nach jeder Ausgabe der
Zeiger um eins erhöht wird
(entspricht einer Erhöhung um 2
Bytes, da der Zeiger vom Typ short
ist).
Mithilfe eines kleinen 'Kniffs'
können Sie aber auch den Inhalt des
short-Feldes byteweise ausgeben.
Dazu definieren Sie zunächst den
Zeiger auf das Feld als char-Zeiger.
Bei der Zuweisung der
Anfangsadresse des Feldes an den
Zeiger müssen Sie jetzt aber eine
entsprechende Typkonvertierung
vornehmen, da in char-Zeigern
normalerweise auch nur Adressen
von char-Daten abgelegt werden
können. Zum Schluss muss noch die
Anzahl der Durchläufe der FORSchleife angepasst werden. Beachten
// Felddefinition und Initialisierung
short array[ ] = {10,20,30,40};
// Feldgröße berechnen
const int SIZE =
sizeof(array)/sizeof(array[0]);
// Hauptprogramm
int main ( )
{
// Zeiger definieren und auf
// Feldanfang setzen
short *pValues = array;
// Nun alle Werte ausgeben
for (int index=0; index<SIZE; index++)
{
cout << *pValues << " ";
// Zeiger auf nächstes Feldelement
pValues++;
}
}
Programmausgabe:
10 20 30 40
// Felddefinition und Initialisierung
short array[ ] = {10,20,30,40};
// Hauptprogramm
int main ( )
{
// Zeiger definieren und auf
// Feldanfang setzen
char *pCValues =
reinterpret_cast<char*>(array);
// Nun alle Werte ausgeben
for (int index=0; index<sizeof(array);
index++)
{
cout << static_cast<int>(*pCValues)
<< " ";
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (7 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Sie bei der Ausgabe auch die
Typkonvertierung des ausgelesenen
char-Wertes. char-Werte werden
standardmäßig als ASCII-Zeichen
dargestellt, wir wollen hier aber den
numerischen Wert ausgeben. Ferner
sollten Sie bei solchen Aktionen
immer daran denken, dass die
Reihenfolge und Anzahl der Bytes
bei den Datentypen nicht durch
ANSI-C++ vorgeschrieben ist. Die
nebenstehende Ausgabe erhalten
z.B. auf einem 32-Bit System mit
einem INTEL-Prozessor. Bei
Systemen mit MOTOROLAProzessoren würde die Ausgabe
anders aussehen.
Bei eindimensionalen Feldern ist die
Sache mit dem Zugriff über einen
Zeiger noch relativ einfach. Doch
wie verhält es sich nun bei
mehrdimensionalen Feldern. Sehen
Sie sich dazu das Beispiel rechts an.
Dort wird ein 2-dimensionales Feld
definiert und initialisiert. Im
Hauptprogramm wird dann die
Anfangsadresse des Feldes einem
entsprechenden Zeiger zugewiesen.
Beachten Sie bitte, dass Sie bei
mehrdimensionalen Feldern den
Adressoperator verwenden müssen.
Anschließend wird der Feldinhalt
wieder in einer FOR-Schleife
ausgegeben. Der Zeiger wird dabei
immer um eins erhöht, so dass alle
Feldelemente nacheinander
ausgegeben werden. Die Ausgabe
erfolgt nun in der Art, dass zuerst
die 1. Reihe, dann die 2. Reihe und
zum Schluss die 3. Reihe ausgegeben
wird. Die Reihenfolge, in der die
Elemente ausgegeben werden, lässt
direkt auf die Ablage der Daten im
Speicher schließen.
// Zeiger auf nächstes Byte
pCValues++;
}
}
Programmausgabe:
10 0 20 0 30 0 40 0
// Felddefinition und Initialisierung
const int ROWS = 3;
const int COLUMNS = 5;
short array[ROWS][COLUMNS] =
{ {0x10,0x11,0x12,0x13},
{0x20,0x21,0x22,0x23},
{0x30,0x31,0x32,0x33}
};
// Hauptprogramm
int main ( )
{
// Zeiger definieren und auf
// Feldanfang setzen
short *pValues = &array[0][0];
// Nun alle Werte ausgeben
for (int index=0;
index<ROWS*COLUMNS;
index++)
{
cout << hex << *pValues << " ";
// Zeiger auf nächstes Byte
pValues++;
}
}
Programmausgabe:
10 11 12 13 20 21 22 23 30 31 32 33
Im nachfolgenden Bild sehen Sie wie die Daten im Speicher letztendlich zu liegen kommen.
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (8 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
ACHTUNG! Ein mehrdimensionales Feld muss lt. dem C++ Standard nicht unbedingt wie
angegeben im Speicher abgelegt sein. Das Einzige was lt. Standard garantiert ist, ist dass die
Daten der Zeilen unmittelbar hintereinander im Speicher liegen. Dieses Beispiel läuft aber
sowohl auf dem VC++ 6.0, dem MinGW 3.2 und dem BORLAND 5.5.1 in der angegebenen
Weise, aber nur solange das mehrdimensionale Feld nicht dynamisch angelegt wird. Wie Sie
Felder dynamisch, d.h. erst zur Programmlaufzeit anlegen, das erfahren Sie in einer späterer
Lektion noch.
C-Strings und Felder
Die nachfolgenden Ausführungen dienen eigentlich nur zur Übung des Umgangs mit Feldern.
C++ besitzt auch eine Standard Bibliothek die den Umgang mit Strings erheblich vereinfacht.
Mehr dazu aber nachher noch. Viele ältere (und leider auch neuere) C++ Programme legen
Strings aber immer noch in char-Feldern ab.
Strings kennen Sie ja schon als Literale/Konstanten von der Ausgabe her. Dort haben Sie schon oft
String-Literale verwendet die in Anführungszeichen "..." eingeschlossen waren.
Zur Erinnerung und ganz wichtig: Das letzte Zeichen in einem String muss immer eine binäre
0 sein. Erst dadurch wird der String abgeschlossen! Damit eine Verwechselung mit den
nachher gleich behandelten 'echten' C++ Strings vermieden wird, werden alle Strings die mit
einer binären 0 abgeschlossen werden, in Zukunft als C-Strings bezeichnet.
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (9 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Ein C-String kann nun aber auch in einem Feld abgelegt werden. Da C-Strings in der Regel aus ASCIIZeichen bestehen werden sie in einem char-Feld abgelegt. Wenn Sie mit 16-Bit Zeichen arbeiten (weil Sie
z.B. den chinesischen Zeichensatz so mögen), müssen Sie anstelle eines char-Feldes ein wchar_t Feld
verwenden. Alle Funktionsdeklarationen der nachfolgend beschriebenen Stringfunktionen sind in der
Header-Datei cstring enthalten.
Sie können bei der Definition eines
char-Feldes dieses auch gleich mit
einem C-String initialisieren, so wie
rechts dargestellt.
char myText[] = "C-String";
Das char-Feld wird dann so groß dimensioniert, dass der C-String einschließlich der abschließenden 0
darin Platz findet. D.h. das nebenstehende Feld myText belegt 9 Bytes.
Wenn im Folgenden von char-Zeigern die Rede ist, so ist damit ein Verweis auf den Beginn eines
char-Feldes oder auch eines String-Literals gemeint. Um einen Verweis auf den Beginn eines
Feldes zu erhalten, geben Sie, wie bereits erwähnt, einfach den Feldnamen an.
Beispiel:
// Feld mit 9 chars belegten
char myText[] = "C-String";
// char-Zeiger auf den Beginn des Feldes
char *ptrFeld = myText;
// char-Zeiger auf Beginn des String-Literals
char *ptrLiteral = "any literal";
Um einen C-String in ein char-Feld
zu kopieren wird die Funktion
char* strcpy(char
*pDest, const char
*pSource);
#include <cstring>
// char-Felder definieren
char acFeld[40];
// String ins char-Feld kopieren
strcpy(acFeld,"The C-String");
aufgerufen.
Dabei ist pDest ein char-Zeiger auf den Speicherbereich, in den der durch pSource adressierte String
kopiert werden soll. pDest muss immer ein char-Zeiger auf ein Feld sein, während pSource entweder ein
String-Literal (-konstante) oder die Adresse eines char-Feldes sein kann. Wenn pDest und pSource sich
überlappen ist das Ergebnis des Kopiervorganges undefiniert. Die Funktionsdeklaration der Funktion
strcpy(...) ist in der Header-Datei cstring enthalten.
Achten Sie unbedingt darauf, dass das Ziel groß genug ist um den String einschließlich der
abschließenden binären 0 aufnehmen zu können. Der Compiler überprüft dies nicht!
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (10 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Wenn Sie noch wissen wollen, wie Sie C-Strings in Feldern verarbeiten können, klicken Sie
links das Symbol an. Wir werden im weiteren Verlaufe des Kurses aber nur noch die 'echten'
C++ Strings verwenden.
Damit sind wir am Ende dieser Lerneinheit angelangt und es folgt das Beispiel und dann die Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel zeigt die Anwendung eines Feldes anhand eines kleinen Statistikprogramms.
Eine Anzahl von Zufallszahlen in einem bestimmten Wertebereich soll klassifiziert werden. Hierbei
wird der gesamte Wertebereich der Zahlen in kleiner Bereiche (Klassen) unterteilt.
Beispiel: Wertebereich
: 0...99
Anzahl der Klassen: 20
daraus folgt: Wertebereich
Wertebereich
Wertebereich
...
Wertebereich
der 1. Klasse: 0...4
der 2. Klasse: 5...9
der 3. Klasse: 10...14
der 20. Klasse: 95...99
Die Zufallszahlen werden dann entsprechend ihrem Wert in die einzelnen Klassen 'einsortiert', d.h.
eine Klassen zählt wie oft eine Zahl in ihrem Bereich auftritt.
Das Programm gibt dann die Verteilung der Zufallszahlen auf einzelne Klassen aus. Zusätzlich wird
die Klasse mit den wenigsten und meisten Werten ausgegeben. Wenn der Algorithmus zur Erzeugung
von Zufallszahlen richtig funktioniert, so müssten alle Klassen etwa gleich viele Werte enthalten,
nämlich Anzahl der Werte dividiert durch Anzahl der Klassen (bei genügend großer Anzahl
untersuchter Werte).
Im Beispiel wird die Anzahl der Klassen, der Wertebereich der Zufallszahlen und die Anzahl der
Zufallszahlen durch Konstanten festgelegt. Dadurch können Sie diese Parameter relativ leicht
abändern. Im Programm ein Feld definiert dessen Feldelemente als Klassenzähler fungieren. Die
Zufallszahlen werden dann in einer FOR-Schleife erzeugt und gleich in die Klassen einsortiert.
Anschließend wird die Klasse mit den wenigsten und meisten Werte gesucht. Zum Schluss werden alle
Klassen sowie die Klasse mit den wenigsten und meisten Werten ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le06\prog\Bfelder
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (11 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Die Programmausgabe:
Statistik-Daten:
================
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
Klasse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
besetzt
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
mit
487
482
463
484
532
522
470
508
513
514
490
499
484
536
493
462
507
514
535
505
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Werten
Kleinster Klassenwert: 462
Groesster Klassenwert: 536
Das Programm:
//
//
//
//
//
//
C++ Kurs
Beispiel zu Felder
ACHTUNG! Beim VC++ 6.0 muessen die Schleifenvariablen
jeder for-Schleife unterschiedlichen Namen besitzen da
VC++ hier nicht standard-konform ist.
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
#include <cstdlib>
using std::cout;
using std::endl;
// Hauptprogramm
int main ()
{
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (12 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
// Anzahl der Klassen, Wertebereich und Anzahl der Werte
const unsigned short ANZKLASSEN = 20;
const unsigned short BEREICH = 100;
const unsigned short ANZWERTE = 10000;
// Klassenfeld definieren und mit 0 initialisieren
unsigned short klasse[ANZKLASSEN] = {0};
// Wertebereich pro Klasse
const unsigned short DELTA = BEREICH/ANZKLASSEN;
// Zahlen nun erzeugen
for (int index=0; index<ANZWERTE; index++)
{
// Zufallszahl im Wertebereich erzeugen
unsigned short wert = rand()%BEREICH;
// Klasse berechnen und Wert einsortieren
unsigned short klassenIndex = wert / DELTA;
klasse[klassenIndex]++;
}
// Statistik der erzeugten Zahlen ausgeben
unsigned short untereGrenze;
unsigned short obereGrenze;
// Min/Max Werte Werte bestimmen
untereGrenze = obereGrenze = klasse[0];
for (int index=1; index<ANZKLASSEN; index++)
{
if (klasse[index]<untereGrenze)
untereGrenze = klasse[index];
else
if (klasse[index]>obereGrenze)
obereGrenze = klasse[index];
}
// Statistikdaten ausgeben
cout << "Statistik-Daten:" << endl;
cout << "================" << endl << endl;
for (int index=0; index<ANZKLASSEN; index++)
cout << "Klasse " << std::setw(2)<< index+1 << " besetzt mit " <<
std::setw(5) << klasse[index] << " Werten\n";
cout << "\nKleinster Klassenwert: " << untereGrenze << endl;
cout << "Groesster Klassenwert: " << obereGrenze << endl;
}
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (13 von 14) [17.05.2005 11:38:35]
Felder und C-Strings
Die Übung:
Ihre Aufgabe ist es nun, eine kleine 'Tabellenkalkulation' zu schreiben.
Die Tabelle definieren Sie mittels eines Feldes mit z.B. 10x10 Einträgen. Füllen Sie dieses Feld dann
mit Zufallszahlen im Bereich 0...9 aus.
Geben Sie dann die Tabelle selbst sowie die Zeilen- und Spaltensummen aus (siehe nachfolgenden
Programmausgabe). Achten Sie bei der Ausgabe auf eine saubere Formatierung.
Die Programmausgabe:
1
7
4
0
9
4
8
8
2
4 :
5
5
1
7
1
1
5
2
7
6 :
1
4
2
3
2
2
1
6
8
5 :
7
6
1
8
9
2
7
9
5
4 :
3
1
2
3
3
4
1
1
3
8 :
7
4
2
7
7
9
3
1
9
8 :
6
5
0
2
8
6
0
2
4
8 :
6
5
0
9
0
0
6
1
3
8 :
9
3
4
4
6
0
6
6
1
8 :
4
9
6
3
7
8
8
2
9
1 :
---------------------------------------49 49 22 46 52 36 45 38 51 60
47
40
34
58
29
57
41
38
47
57
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le06\Lfelder.
So, sehen wir uns in der nächsten Lektion nun aber einmal an, wie in C++ Strings 'richtig' verarbeitet
werden.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le06/le06_01.htm (14 von 14) [17.05.2005 11:38:35]
string Datentyp
string Datentyp
Die Themen:
Die string-Klasse
Strings definieren
Ein- und Ausgabe von Strings
String-Operationen
Beispiel und Übung
Die string-Klasse
Zur Verarbeitung von Strings stellt die C++ Standard Bibliothek eine Klasse string zur Verfügung.
In den nachfolgenden Lektionen dieses Kurses werden zur Verarbeitung von Strings dann nur noch
diese string Objekte verwendet.
Sehen wir uns zunächst einmal an, was die string Klasse so interessant macht:
Es wird automatisch genügend Speicher reserviert um eine vorgegebene Zeichenfolge
abzulegen. Und allein dies ist schon ausreichend Grund genug die string Klasse einzusetzen.
Es werden diverse Operatoren überladen um Strings auf einfache Art und Weise
verarbeiten zu können.
So erfahren Sie nachher gleich noch, dass Sie u.a. mittels + zwei Strings zusammenfügen
können.
Es stehen Konvertierungen zur Verfügung um ein string Objekt in einen char* und
umgekehrt zu konvertieren.
Die Klasse string enthält noch wesentlich mehr als in dieser kurzen Einführung aufgezeigt werden
kann. In einer späteren Lektion werden wir uns die Klasse string nochmals etwas genauer ansehen.
Wenn Sie die Klasse string in ihrem Programm einsetzen, so müssen Sie die mittels #include <string>
die dazugehörige Header-Datei einbinden. Achten Sie aber unbedingt darauf dass hier kein .h steht!
Die Header-Datei string.h bzw. ihr C++ Gegenstück cstring ist eine gänzlich andere Header-Datei!
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (1 von 10) [17.05.2005 11:38:37]
string Datentyp
Aber sehen wir uns zunächst einmal an wie string Objekte definierte werden.
Sollten Sie mit dem Begriff Objekt im Augenblick noch etwas Schwierigkeiten haben so
stellen Sie sich unter einem Objekt in erster Näherung einfach eine Variable vor.
Noch einmal zur Erinnerung: Damit in Zukunft nicht die totale Verwirrung bezüglich des
Begriffs String ausbricht, wird im weiteren Verlaufe des Kurses unter einem String immer
ein string Objekt verstanden. Zeichenfolgen die in char-Feldern abgelegt sind (das sind
diejenigen, die mit einer binären 0 abgeschlossen sind) werden demgegenüber als C-String
bezeichnet, weil dies unter C die einzige Möglichkeit war veränderbare Zeichenfolgen
abzulegen. Daraus folgt, dass ein char-Zeiger immer auf einen C-String zeigt und niemals auf
einen String.
Strings definieren
Schauen wir uns jetzt an, wie Strings definiert werden.
Im Prinzip definieren Sie einen
String genauso wie z.B. eine
short-Variable, nur dass anstelle
des Datentyps short nun der
Datentyp string steht. Ein so
definierter String enthält noch
keine Zeichen, d.h. er ist leer.
Sie können einen String bei
seiner Definition aber auch
bereits mit einer Zeichenfolge
initialisieren. Diese
Initialisierung können Sie im
Prinzip genauso durchführen
wie das Initialisieren einer
beliebigen anderen Variable,
d.h. nach dem Stringnamen
folgt der Zuweisungsoperator
und danach die Zeichenfolge,
mit der der String initialisiert
werden soll. Diese Zeichenfolge
kann entweder ein C-String
Literal, eine C-String Konstante
oder aber auch ein Zeiger auf
ein char-Feld sein, in dem ein
entsprechender C-String
// Header-Datei fuer string einbinden
#include <string>
using std::string;
// Leeren String definieren
string empty;
// String mit C-String initialisieren
string text1 = "Ein String";
// String Feld definieren
string array[10];
// und ersten String initialisieren
array[0] = "String-Element 0";
// String mit einem anderen String init.
string text2(text1);
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (2 von 10) [17.05.2005 11:38:37]
string Datentyp
abgelegt ist.
Und selbstverständlich können Sie auch Stringfelder definieren. Beachten Sie dabei aber, dass das
Feld nun n Strings aufnimmt, die auch unterschiedliche Längen besitzen können. Verwechseln dies
nicht mit der Definition eines char-Feldes, das ja einen C-String mit maximal n-1 Zeichen
aufnehmen kann.
Eine weitere Möglichkeit besteht darin, einen String mit einem bereits bestehenden String bei seiner
Definition zu initialisieren, d.h. Sie erzeugen sich eine String-Kopie. Wie dies geht sehen in der
letzten Definition im obigen Beispiel.
Wie Sie dem obigen Beispiel auch entnehmen können, liegt die Klasse string genauso im
Namensraum std wie z.B. der Ausgabestream cout. Sie können ein string Objekt auch ohne
die using-Anweisung definieren wenn Sie die Definition wie folgt schreiben:
std::string name;
War doch bis nicht kompliziert, oder? Zugegeben, wir haben uns nur die einfachen Fälle angesehen
einen String zu erstellen. Die C++ Standard Bibliothek kennt acht verschiedene Arten ein string
Objekt zu erstellen. Für unsere Zwecke sollten diese drei Möglichkeiten aber ausreichen.
Sehen wir uns nun an, wie Sie Strings ausgeben und einlesen können.
Ein- und Ausgabe von Strings
Fangen wir wieder mit dem
einfachen Fall an, der Ausgabe
von Strings.
#include <iostream>
#include <string>
using namespace std;
int main()
Die Ausgabe eines Strings
{
unterscheidet sich nicht von der
string text = "Ein string-Objekt";
Ausgabe von anderen Daten,
cout << text << endl;
d.h. Sie können den String
}
direkt im Ausgabestream
angeben.
Beim Einlesen eines Strings muss unterschieden werden, ob die einzelnen Wörter einer Eingabe
(getrennt durch Leerzeichen) in mehreren Strings abgelegt werden sollen oder eine ganze Zeile in
einem String.
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (3 von 10) [17.05.2005 11:38:37]
string Datentyp
Um einzelne Wörter einer
Eingabe in Strings abzulegen,
werden die Strings genauso wie
die bisherigen sonstigen
Variablen im Eingabestream
angegeben. Und hier sehen Sie
auch einen der Vorteile der
Strings: egal ob ein Wort aus 4
Zeichen oder 80 Zeichen
besteht, es wird automatisch
genügend Speicher reserviert
um das Wort ablegen zu
können.
Wollen Sie eine ganze Zeile
einlesen so verwenden Sie
hierfür die Funktion getline(...).
Beachten Sie bitte, dass die hier
eingeführte Funktion getline(...)
eine andere Signatur besitzt als
die Memberfunktion getline(...)
des Eingabestreams cin. Die hier
eingeführte Funktion getline(...)
erhält im ersten Parameter eine
Referenz auf einen Stream von
dem die Zeile eingelesen werden
soll. Im Beispiel rechts ist dies
der Eingabestream cin. Aber
ebenso möglich wäre hier z.B.
die Angabe eines Dateistreams
vom Typ ifstream. Der zweite
Parameter ist dann die Referenz
auf das string Objekt, in dem die
Eingabe abgelegt wird. Und
nochmals: egal wie lange die
Eingabezeile ist, es wird immer
genügend Speicher reserviert
um die komplette Zeile im
String ablegen zu können.
#include <iostream>
#include <string>
using namespace std;
int main()
{
string word1, word2;
cin >> word1 >> word2;
}
#include <iostream>
#include <string>
using namespace std;
int main()
{
string line;
getline(cin,line);
}
String-Operationen
Zuweisungen
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (4 von 10) [17.05.2005 11:38:37]
string Datentyp
Die einfachste Operation ist die
Zuweisung. So können Sie
einem String ein C-String
Literal/-Konstante zuweisen
oder auch einen anderen String.
// Zwei Strings definieren
string s1, s2;
// C-String-Literal einem String zuweisen
s1 = "Dies ist String1";
// String einem anderen String zuweisen
s2 = s1;
Addition
Ebenso ist es jetzt möglich,
Strings einfach mithilfe des
Operators + zu 'addieren'.
Zusätzlich zum Operator + stellt
die Klasse string zur Addition
noch den Operator += zur
Verfügung.
// Zwei Strings definieren
string s1("Text1"), s2;
// C-String-Literal addieren
s1 += " erweitert";
// Strings addieren
s2 = s1 + "in s2"
Zeichen-Zugriff und Stringlänge
Um auf die einzelnen Zeichen
eines Strings zuzugreifen,
verwenden Sie, wie bei den CStrings, den Indexoperator[].
Auch hier hat das erste Zeichen
im String den Index 0. Wollen
Sie über den gesamten String
iterieren, so liefert die
Memberfunktion size(...) Ihnen
hierfür die Anzahl der Zeichen
im String.
// String definieren
string s1("Ein Text");
// Alle Zeichem im String einzeln ausgeben
for (int i=0; i<s1.size(); i++)
cout << s1[i] << ',';
Strings vergleichen
Und sogar dies geht jetzt: Sie
können mit den bekannten
Vergleichsoperatoren <, >, ==
usw. Strings einfach
vergleichen. Der Vergleich
erfolgt hierbei lexikalisch, d.h.
'Aaaa' ist kleiner als 'Bb'. Für
einen der beiden Operanden
kann anstelle eines string
Objekts auch ein const char
Zeiger stehen.
// Zwei String definieren
string s1("Aaaa"), s2("Bb");
// Strings vergleichen
if (s1 < s2)
cout << "s1 kommt vor s2" << endl;
else
cout << "s2 kommt vor s1" << endl;
char-Zeiger Konvertierungen
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (5 von 10) [17.05.2005 11:38:37]
string Datentyp
Um einen String in einen const
char-Zeiger zu konvertieren,
enthält die string Klasse die
Memberfunktion c_str(...).
Mithilfe dieser Konvertierung
können Sie also jetzt überall
einen String verwenden, wo Sie
ansonsten einen const charZeiger benötigen. Im Beispiel
rechts wird so z.B. der Inhalt
eines Strings in ein char-Feld
kopiert.
// String definieren
string s1("Ein Text");
// Zeichen aus String in char-Feld umkopieren
char charArray[40];
strcpy(charArray,s1.c_str());
Der umgekehrte Weg, also einen char-Zeiger in ein String zu konvertieren, erfolgt über den am
Anfang dieses Abschnitts aufgeführten Zuweisungsoperator. Beachten Sie bitte, dass die
Memberfunktion c_str(...) einen const char* liefert und Sie somit den Inhalt des Strings nicht über
diesen Zeiger direkt verändern können.
Damit wollen wir an dieser Stelle die Einführung des string Datentyps abschließen. Der string
Datentyp enthält noch zahlreiche weitere Memberfunktionen um z.B. Zeichen mitten im String
einzufügen oder zu löschen oder sogar Zeichenfolgen in einem String zu suchen, die wir uns später
in einer weiteren Lerneinheit noch ansehen werden.
Und jetzt kommt wieder das Beispiel und dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
Das Beispielprogramm zählt die Häufigkeit des Auftretens der einzelnen Buchstaben des
Alphabets in einem bestimmten Text, wobei nicht nach Groß-/Kleinschreibung unterschieden
wird. Der zu untersuchende Text liegt als ASCII-Datei test.txt vor.
Dazu wird ein einsprechendes Feld mit 26 Einträgen definiert, das als 'Buchstabenzähler'
verwendet wird. Die Häufigkeit des Buchstabens 'a' wird im ersten Feldelement gezählt, die des
Buchstabens 'b' im zweiten usw. Für Sonderzeichen, wie zum Beispiel Punkt oder Komma, wird
ein getrennter Zähler verwendet.
Der zu untersuchende Text wird zeilenweise mittels getline(...) aus der Datei eingelesen.
Zum Schluss wird die Häufigkeitsverteilung in Tabellenform ausgegeben.
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (6 von 10) [17.05.2005 11:38:37]
string Datentyp
Das unten stehende Beispiel finden Sie auch unter: le06\prog\bstring
Die Programmausgabe:
a: 32
b: 6
c: 8
f: 5
g: 12
h: 16
k: 7
l: 17
m: 5
p: 2
q: 0
r: 25
u: 11
v: 6
w: 10
z: 3
Sonderzeichen: 80
d:
i:
n:
s:
x:
19
28
50
19
0
e: 76
j: 3
o: 15
t: 21
y: 0
Das Programm:
// C++ Kurs
// Loesung zur string Klasse
#include
#include
#include
#include
<iostream>
<iomanip>
<fstream>
<string>
using std::cout;
using std::endl;
// Konstanten fuer Dateiname und
// Anzahl der Buchstaben im Alphabet
const char *const pFILENAME = "test.txt";
const int NUMOFCHARS = 26;
// Hauptprogramm
int main()
{
// Datei oeffnen
std::ifstream inFile(pFILENAME);
if (!inFile)
{
cout << "Kann Datei " << pFILENAME << " nicht oeffnen!" << endl;
exit(1);
}
// Zaehler fuer die Anzahl der Buchstaben definieren
int characters[NUMOFCHARS] = {0};
// Zaehler fuer Sonderzeichen wie z.B. '.' oder ',' usw.
int specialChar = 0;
// Schleife zum Einlesen der Datei
do
{
// String fuer eingelesene Zeile aus der Datei
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (7 von 10) [17.05.2005 11:38:37]
string Datentyp
std::string line;
// Eine Zeile aus der Datei einlesen
std::getline(inFile,line);
// Falls Dateiende nicht erreicht
if (!inFile.eof())
{
// Komplette Zeile durchlaufen
for (unsigned int index=0; index<line.size(); index++)
{
// Falls Buchstabe im String a...z
if ((line[index]>='a') && (line[index]<='z'))
characters[line[index]-'a']++;
else
// Falls Buchstabe im String A...Z
if ((line[index]>='A') && (line[index]<='Z'))
characters[line[index]-'A']++;
else
// Sonderzeichen erhoehen
specialChar++;
}
}
} while (!inFile.eof());
// Datei auch wieder schliessen!
inFile.close();
// Ergebnis ausgeben
cout << "Anzahl der Buchstaben in der Datei " << pFILENAME << endl;
// Alle Buchstabenzaehler ausgeben
for (int index=0; index<NUMOFCHARS; index++)
{
// Nach jedem 5. Buchstaben eine neue Zeile beginnen
if (index%5 == 0)
cout << endl;
cout << static_cast<char>(index+'a') << ": " << std::setw(3)
<< characters[index] << '\t';
}
// Noch Anzahl der Sonderzeichen ausgeben
cout << "\nSonderzeichen: " << specialChar << endl;
}
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (8 von 10) [17.05.2005 11:38:37]
string Datentyp
Die Übung:
So, jetzt dürfen Sie einmal Ihr ganzes Wissen einsetzen. Ziel der Übung ist es, ein kleines DateiKonvertierungsprogramm zu schreiben.
Das Programm soll die Textdatei 'gcc.mak' einlesen, entsprechend den nachfolgenden Regeln
konvertieren und das Ergebnis dann in der Datei 'conv.mak' ablegen.
Die Konvertierung soll wie folgt erfolgen:
●
●
Alle Zeilen die mit dem Kommentarzeichen '#' beginnen sind zu entfernen.
Alle Kleinbuchstaben sind in Großbuchstaben umzuwandeln.
Zur Konvertierung Kleinbuchstaben -> Großbuchstaben ist die Bibliotheksfunktion int toupper(int
nC); zu verwenden. Sie erhält das zu konvertierende Zeichen als Parameter übergeben und liefert
als Returnwert das konvertierte Zeichen.
Die Programmausgabe:
Ausgangsdatei gcc.mak:
#
# Note : this makefile is for gcc-2.95 and later !
#
#
# compiler
#
CC = gcc
CXX = gcc
#
# Basename for libraries
#
LIB_BASENAME = libstlport_gcc
...
Ergebnisdatei conv.mak:
CC = GCC
CXX = GCC
LIB_BASENAME = LIBSTLPORT_GCC
...
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (9 von 10) [17.05.2005 11:38:37]
string Datentyp
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le06\lstring.
In der nächsten Lektion werden wir uns nochmals mit der Konvertierung von Daten befassen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le06/le06_02.htm (10 von 10) [17.05.2005 11:38:37]
C-String Funktionen
C-String Funktionen
C-String Manipulationen
Ist der String erst einmal in
einem char-Feld abgelegt,
können Sie durch
Indizierung auf die
einzelnen Elemente (gleich
Buchstaben) des Strings
zugreifen. Sehen Sie sich
dazu wiederum das Beispiel
rechts an. Zuerst wird im
Programm das StringLiteral "Menüpunkt A:" in
ein char-Feld kopiert.
Dieses String-Literal
besteht aus 12 Zeichen plus
der abschließenden binären
0. Da das Feld 20 Zeichen
aufnehmen kann sind wir
hier auf der sicheren Seite.
Anschließend wird das Feld
zunächst ausgegeben. Wie
Sie bereits erfahren haben,
werden char-Daten immer
als ASCII-Zeichen
ausgegeben und wir
erhalten somit als erste
Ausgabe Menüpunkt A:.
Danach wird das 11.
Element, das ist im
Ausgangsstring der
Buchstabe 'A', mit dem
Buchstaben 'B' bzw 'C'
überschrieben und der
String dann jeweils erneut
ausgegeben.
#include <iostream>
// Funktionsdeklarationen
#include <cstring>
// char-Feld für String definieren
char text[20];
// Hauptprogramm
int main ( )
{
// String-Konst. in Feld kopieren
std::strcpy (text, "Menüpunkt A:");
//1. Menüpunkt ausgeben
std::cout << text << std::endl;
// Menübuchstabe überschreiben
text[10] = 'B';
//2. Menüpunkt ausgeben
std::cout << text << std::endl;
// Menübuchstabe überschreiben
text[10] = 'C';
//3. Menüpunkt ausgeben
std::cout << text << std::endl;
}
Programmausgabe:
Menüpunkt A:
Menüpunkt B:
Menüpunkt C:
Außer der vorhin erwähnten Funktion strcpy(...) stehen noch eine ganze Reihe weiterer
Funktionen zur Verfügung, von den wir uns hier noch die drei wichtigsten anschauen wollen.
http://www.cpp-tutor.de/cpp/le06/le06_01_d1.htm (1 von 5) [17.05.2005 11:38:38]
C-String Funktionen
C-Strings 'addieren'
Um zwei Strings
#include <iostream>
zusammenzufügen wird die #include <cstring>
Funktion
// Felder fuer die Strings
char text[50];
char append[11];
char
// Hauptprogramm
std::strcat(char
int main ()
*pText1, const
{
char *pText2);
// Strings initialisieren
std::strcpy (text, "Text1");
verwendet. Sie fügt an den
std::strcpy (append, " und Text2");
über pText1 spezifizierten
// Ersten String ausgeben
String den String pText2 an
std::cout << text << std::endl;
und legt den so erhaltenen
// Zweiten String an ersten anfuegen
String wieder unter pText1
std::strcat (text,append);
ab. Der String pText2 wird
std::cout << text << std::endl;
dabei nicht verändert.
// String-Konstante noch anfuegen
pText1 muss ein char-Feld
std::strcat (text," und Text3");
sein das so groß
std::cout << text << std::endl;
dimensioniert ist, dass
}
beide Ausgangs-Strings
Programmausgabe:
darin Platz finden. Ist das
Feld zu klein, so werden die
Text1
nach dem Feld pText1
Text1 und Text2
nachfolgenden Daten
Text1 und Text2 und Text3
einfach überschrieben, d.h.
es erfolgt keine
Überprüfung der
notwendigen Feldgröße
durch den Compiler. Solche
Fehler sind dann sehr
schwer zu finden da sich
das Überschreiben der
nachfolgenden Daten in den
meisten Fällen erst sehr viel
später auswirkt und damit
die Fehlerstelle nicht so
schnell lokalisiert werden
kann.
Im Beispiel rechts werden
zwei char-Felder definiert
in denen die Strings text
http://www.cpp-tutor.de/cpp/le06/le06_01_d1.htm (2 von 5) [17.05.2005 11:38:38]
C-String Funktionen
und append abgelegt
werden. An den ersten Text
wird dann mittels strcat(...)
der zweite String angefügt
und dann der 'neu
entstandene' Text
ausgegeben. Zum Schluss
wird noch das StringLiteral 'und Text3' zum Text
hinzugefügt.
C-Strings vergleichen
Die zweite wichtige
Funktion ist die Funktion
#include <iostream>
#include <cstring>
// Felder fuer Strings
char text1[50];
int
// Hauptprogramm
std::strcmp(const
int main ()
char *pText1,
{
// Strings initialisieren
const char
std::strcpy (text1, "Maier");
*pText2);
// und vergleichen
if (std::strcmp(text1,"Maier") == 0)
Sie vergleicht ob zwei
cout << "Strings sind gleich\n";
Strings identisch sind.
else
pText1 und pText2 sind hier
cout << "Strings sind ungleich\n";
die Zeiger auf die zu
}
vergleichenden Strings. Als
Returnwert liefert die
Funktion folgende Werte
zurück:
Wert Bedeutung
<0
pText1 kleiner
pText2
=0
pText1 ist gleich
pText2
>0
pText1 größer
pText2
Kleiner bzw. größer bezieht sich hierbei nicht auf die Länge des Strings sondern darauf, ob der
erste String im Alphabet vor dem zweiten steht bzw. umgekehrt (lexikalischer Vergleich).
http://www.cpp-tutor.de/cpp/le06/le06_01_d1.htm (3 von 5) [17.05.2005 11:38:38]
C-String Funktionen
Stringlänge ermitteln
Die letzte String-Funktion #include <iostream>
die wir uns hier ansehen ist #include <cstring>
die Funktion
// Stringfeld definieren
char text[] = "Ein beliebiger Text";
std::size_t
std::strlen (const // Hauptprogramm
int main ()
char* pText);
{
// Stringlaenge ermitteln
std::size_t slen = std::strlen(text);
Sie liefert die Anzahl der
// und ausgeben
Zeichen in einem String,
std::cout << "Stringlaenge: "
ohne die abschließende
<< slen << std::endl;
binäre 0, zurück. pText hier }
der Zeiger auf den String
dessen Länge ermittelt
werden soll. Der Datentyp
size_t des Rückgabewertes
der Funktion ist ein
Datentyp im stdNamensraum zur
Aufnahme der Größe von
Speicherbereichen. Dieser
Datentyp wird uns später
bei der dynamischen
Belegung von Speicher
wieder begegnen. Sie
können den Datentyp
std::size_t wie einen
vorzeichenlosen
ganzzahligen Datentyp
verwenden.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le06/le06_01_d1.htm (4 von 5) [17.05.2005 11:38:38]
C-String Funktionen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le06/le06_01_d1.htm (5 von 5) [17.05.2005 11:38:38]
String-Konvertierungen
String-Konvertierungen
Die Themen:
Einleitung
ASCII nach binär
Fehler bei der Binär-Konvertierung
Binär nach ASCII
String zerlegen
Beispiel und Übung
Einleitung
Wie schon erwähnt, ist das Einlesen von Daten von der Tastatur eine recht komplizierte Sache, wenn man alle
Fehleingaben abfangen möchte. Der sicherste Weg zum Einlesen von Daten führt meistens über das Einlesen
einer kompletten Zeile. Diese Zeile ist dann im Programm entsprechend zu verarbeiten. Da eine eingelesene Zeile
aber immer als String abgelegt wird, gibt es entsprechende Memberfunktionen um den Inhalt eines Strings in
seine 'Einzelteile' zu zerlegen und dabei u.a. in numerische Werte (Binärdaten) zu konvertieren. Ebenso ist der
umgekehrte Weg möglich, d.h. numerische Werte können in ASCII-Zeichen konvertiert werden. So wird dann
zum Beispiel aus dem binären Wert 0x0a der ASCII-String "10".
Zur Konvertierung von Strings in numerische Werte stehen auch noch entsprechende C-Funktionen zur
Verfügung (die wir aber im Verlaufe des Kurses nicht weiter verwenden). Wenn Sie darüber etwas
erfahren wollen, klicken Sie das Symbol links an.
ASCII nach binär
Den Eingabestream cin haben Sie ja nun schon zu genüge kennen gelernt. Er liest im Prinzip Daten von der
Tastatur ein und legt die Eingaben dann in den entsprechenden Variablen ab, d.h. auch cin führt intern eine
Umwandlung von 'ASCII-Zahlen' in binäre Zahlen (so wie sie der Rechner verarbeitet) durch. Um nun einen
String in seine 'Einzelteile' zu zerlegen, benötigen wir fast die gleiche Funktionalität, nur dass der String jetzt
nicht über die Tastatur eingegeben wird sondern halt irgend wo im Speicher liegt.
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (1 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Und genau diese Aufgabe erfüllt der Stream
istringstream. Wenn Sie den Stream
istringstream einsetzen müssen Sie die
Header-Datei sstream einbinden. Um nun
eine einen String in seine 'Einzelteile' zu
zerlegen definieren Sie sich zunächst ein
istringstream Objekt. Anschließend weisen
Sie dem Objekt mittels der Memberfunktion
str(...) den entsprechenden String zu. Und ab
diesem Zeitpunkt verhält sich der
istringstream genauso wie der Eingabestream
cin, d.h. Sie lesen mittels des Operators >>
die einzelnen Werte aus diesem Stream aus.
Im nebenstehenden Beispiel enthält 'nach
dem Auslesen' die Variable intVar1 den Wert
123, intVar2 den Wert 456 und dblVar den
Wert 3.4.
// Header-Datei fuer istringstream einbinden
#include <sstream>
....
// Stream-Objekt definieren
std::istringstream is;
// string definieren und initalisieren
std::string s1 = "123 456 3.4";
// String dem Stream-Objekt zuweisen
is.str(s1);
// String nach binaer konvertieren
int intVar1, intVar2;
double dblVar;
is >> intVar1 >> intVar2 >> dblVar;
Soweit der 'Trivialfall'. Wenn Sie im Programm nun mehrere Strings zu konvertieren haben, so wird Ihr erster
Versuch meistens so verlaufen, dass Sie dem bereits bestehenden Stream-Objekt mittels der Memberfunktion
str(...) einfach einen neuen String zuweisen und diesen dann in der gleichen Art und Weise dekodieren wollen.
Dies wird mit (an Sicherheit grenzender Wahrscheinlichkeit) fehlschlagen. Der Grund dafür liegt darin
begründet, dass nach dem kompletten Auslesen eines Strings der Stream istringstream den Status EOF (=End Of
File) besitzt, und damit sich quasi in einem Fehlerzustand befindet. Um diesen 'Fehlerzustand' aufzuheben, rufen
Sie nach der Auswertung des ursprünglichen Strings die Memberfunktion clear(...) des Stream-Objekts auf. Und
danach ist das Stream-Objekt 'wie neu' und kann weiter verwendet werden.
Da istringstream und cin außerdem vom
gleichen Grundstream abstammen, können
Sie beim istringstream auch die gleichen
Manipulatoren verwenden wie beim
Eingabestream cin. In letzter Konsequenz
heißt dies, dass Sie mit dem istringstream
genau das Gleiche erreichen können, was Sie
auch mit dem Eingabestream cin können,
nur dass die 'Daten' nun aus einem String
stammen und nicht von der Tastatur
kommen. Rechts sehen Sie ein Beispiel bei
dem Hex-Zahlen aus einem String ausgelesen
werden. Beachten Sie dabei, dass der HexPräfix 0x.. im String dabei wahlweise
vorhanden sein darf.
// Header-Datei fuer istringstream einbinden
#include <sstream>
....
// Stream-Objekt definieren
istringstream is;
// String dem Stream-Objekt zuweisen
is.str("0x50 10");
// String nach binaer konvertieren
int var1, var2
is >> hex >> var1 >> var2;
cout << var1 << ' ' << var2;
Programmausgabe:
80 16
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (2 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Wenn Sie wollen, können Sie dem
istringstream Objekt bei seiner Definition
auch gleich einen String zuweisen.
Selbstverständlich können diesem StreamObjekt jederzeit einen weiteren String
zuweisen wenn Sie, wie vorher erwähnt, den
EOF-Status des Streams mittels clear(...)
aufheben.
// Header-Datei fuer istringstream einbinden
#include <sstream>
....
// Stream-Objekt definieren und init.
istringstream is("12 13");
// String nach binaer konvertieren
int var1, var2;
is >> var1 >> var2;
// Status des Stream-Objekts zurücksetzen
is.clear();
// Neuen String dem Stream-Objekt zuweisen
is.str("22 33");
Fehler bei der Binär-Konvertierung
Der Stream istringstream besitzt u.a die zwei Memberfunktionen fail(...) und eof(...), mit deren Hilfe Fehler beim
Konvertieren festgestellt werden können.
Fangen wir mit dem einfachsten Fehlerfall
an, dass zu wenige Daten im String enthalten
sind. Im Beispiel rechts enthält der String
(mit dem der Stream initialisiert wird) nur
einen 'Wert', während anschließend versucht
wird, aus dem Stream zwei Werte
auszulesen. In diesem Fall liefert die
Memberfunktion fail(...) den Wert true
zurück, da dies als Fehler gewertet wird.
Dieser Rückgabewert wird im Beispiel rechts
mittels der IF-Anweisung ausgewertet. Für
den Fall dass fail(...) true zurückliefert, wird
dann die Anweisung zur Behandlung des
Fehlers ausgeführt.
// Stream-Objekt def. und init.
istringstream is("10");
// Konvertieren nach binaer
int var1, var2;
is >> var1 >> var2;
if (is.fail()==true)
// Fehlerbehandlung
cout << "Fehler: " << is.str() << endl;
Im obigen Beispiel finden Sie übrigens ebenfalls die bereits bekannte string Memberfunktion str(...), diesmal
jedoch ohne Parameter. Wird str(...) ohne Parameter aufgerufen, so liefert sie einen char-Zeiger auf den aktuellen
String (eigentlich auf den Streaminhalt) zurück.
Hinweis: Enthält der String zu viele 'Werte', so werden die überzähligen Werte einfach im String
belassen und mit der nächsten Anweisung verarbeitet, die aus dem String ausliest. Eine 'Fehlermeldung'
erfolgt hierbei also nicht. Genau gleich wie beim Eingabestream cin.
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (3 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Sehen wir uns den nächsten Fehlerfall an,
dass der Stream anstelle einer 'ASCII-Zahl'
einen Text enthält. Auch hier liefert die
Memberfunktion fail(...) den Wert true
zurück, der dann entsprechend ausgewertet
werden kann.
// Stream-Objekt def. und init.
istringstream is("Emil");
// Konvertieren nach binaer
int var;
is >> var;
if (is.fail()==true)
... // Fehlerbehandlung
Und weiter geht's mit den Fehlern. Wieder
soll versucht werden eine 'ASCII-Zahl' nach
binär zu konvertieren. Der String enthält
nun am Anfang zwar 'ASCII-Zahlen', jedoch
folgt unmittelbar darauf ein Text (so wie im
Beispiel rechts angegeben). In diesem Fall
hilft uns die Memberfunktion fail(...) nicht
weiter, da ja die Konvertierung zunächst
erfolgreich ist; es werden die 'ASCII-Zahlen'
am Anfang des Strings richtig nach binär
konvertiert. Um nun feststellen zu können,
ob der komplette String konvertiert wurde,
wird die Memberfunktion eof(...) eingesetzt.
Sie liefert nur dann true zurück, wenn alles
konvertiert werden konnte, d.h. keine Reste
im Stream 'stehen geblieben' sind.
Rechts sehen noch ein weiteres Beispiel (mal
ohne Fehler), das aus einem String eine Zahl
nach der anderen nach binär konvertiert.
Tritt während dieser Konvertierung ein
Fehler auf, so liefert die Memberfunktion
fail(...) true zurück und die Einleseschleife
wird verlassen. Ebenfalls wird die Schleife
beendet, wenn alle 'ASCII-Zahlen'
konvertiert wurden, da in diesem Fall eof(...)
true zurückliefert. Nach dem Verlassen der
Schleife kann durch erneutes abprüfen des
Streamstatus festgestellt werden, ob die
Schleife durch einen Fehler oder durch
erreichen des Stringendes verlassen wurde.
In allen Fällen müssen Sie aber den Stream
mittels clear(...) zurücksetzen wenn Sie ihn
weiter verwenden wollen.
// Stream-Objekt def. und init.
istringstream is("12Emil");
// Konvertieren nach binaer
int var;
is >> var;
if (is.eof()==false)
... // Fehlerbehandlung
// Stream zuruecksetzen
is.clear();
// Stream-Objekt def. und init.
istringstream is("11 22 33");
int var;
// Kompl String konvertieren nach binaer
while (is.eof() == false)
{
is >> var;
if (is.fail() == true)
break;
cout << var << ',';
}
// Falls Fehler aufgetreten
if (is.fail())
... // Fehlerbehandlung
// Stream zuruecksetzen
is.clear();
Binär nach ASCII
Sehen wir uns nun den umgekehrten Fall, die Konvertierung von binär nach ASCII. Auch diese Konvertierung
kennen Sie prinzipiell schon durch den Ausgabestream cout. Er konvertiert für die Ausgabe binäre Daten nach
ASCII. Wir müssen nun 'nur' noch einen Stream finden, der die Daten anstatt auf die Standard-Ausgabe
auszugeben diese wieder irgend wo im Speicher ablegt.
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (4 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Und genau diese Aufgabe erfüllt der Stream
ostringstream. Wenn Sie den Stream
ostringstream einsetzen müssen Sie wieder die
gleiche Header-Datei sstream einbinden wie
beim istringstream. Um Daten in einen String
zu konvertieren, definieren Sie sich zunächst
ein ostringstream Objekt. Anschließend
können Sie dann die Daten genauso wie beim
cout-Stream 'ausgeben', nur dass die Daten
jetzt nicht auf die Standard-Ausgabe
gelangen sondern im Stream verbleiben. Um
auf den Inhalt des Streams (das ist der
entsprechende String mit den konvertierten
Daten) zuzugreifen, verwenden Sie ebenfalls
die vom ostringstream bekannte
Memberfunktion str(...).
// Header-Datei fuer ostringstream einbinden
#include <sstream>
....
// Stream-Objekt definieren
ostringstream os;
// Daten in Stream uebertragen
int var;
os << "Wert" << var << '\n';
// Streaminhalt ausgeben
cout << os.str();
String zerlegen
Sehen wir und zunächst einmal an, wie eine
Eingabe, die aus mehreren Wörtern
bestehen kann, in einzelne Strings zerlegt
wird. Nach dem Einlesen der Zeile mittels
der Funktion getline(...) wird ein
istringstream Objekt definiert, das mit dem
eingelesenen String (=Zeile) initialisiert wird.
Wie Sie bereits weiter vorne im Kurs
erfahren haben, kann mithilfe eines solchen
istringstream Objekts ein String in seine
'Einzelteile' zerlegt werden. Der Unterschied
zu den vorherigen Beispielen ist, dass nun
der String nicht in binäre Daten konvertiert
wird sondern in einzelne string Objekte. Um
den kompletten eingelesenen String in
einzelne Wörter zu zerlegen, wird solange
aus dem istringstream ausgelesen, solange
dessen Memberfunktion eof(...) false
zurückliefert. Die einzelnen Wörter werden
dann mittels des Operators >> einem
weiteren string Objekt 'zugewiesen'. Wenn
Sie wollen, können Sie das nebenstehende
Beispiel auch einmal in Ihren Compiler
übernehmen und laufen lassen.
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
string line, word;
getline(cin,line);
istringstream is(line);
while (!is.eof())
{
is >> word;
cout << word << endl;
}
}
Mithilfe des istringstream können aber auch zum Beispiel Teile eines Strings, die durch frei definierbares
Trennzeichen voneinander getrennt sind, extrahiert werden. Der 'Trick' ist dabei Folgender:
Die Funktion getline(...) wurde bisher nur zum Einlesen einer kompletten Zeile verwenden. Aber getline(...) kann
eigentlich noch wesentlich mehr. getline(...) liest zunächst aus einem beliebigen Input-Stream eine Zeile aus, d.h.
die Funktion getline(...) kann sowohl vom Eingabestream cin wie auch vom Dateistream ifstream oder sogar vom
istringstream Stream eine Zeile auslesen.
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (5 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Aber das ist in unserem Fall nur die halbe
Miete; wir wollen ja nicht eine ganze Zeile in
einem String ablegen sondern vorerst nur
den Teil bis zum ersten Trennzeichen. Und
dazu kann an getline(...) als dritten
Parameter das Trennzeichen mitgegeben
werden, bis zu dem getline(...) einlesen soll.
Standardmäßig ist dies der Zeilenvorschub
'\n'. Wir brauchen nun anstelle des
Zeilenvorschubs nur noch das gewünschte
Trennzeichen angeben. Und das war's dann
auch schon. Zugegeben, das ist eigentlich
schon etwas für Fortgeschrittene. Aber Sie
sehen, wenn man sich etwas intensiver mit
Strings und Streams befasst kann einem das
eine Menge Arbeit sparen.
// Zu untersuchende Zeile
// Trennzeichen ist hier das Komma
const char* const pLINE =
"Agathe Mueller,Gansweg 23,12345 ADorf";
// 3 string Objekte definieren
string name, street, city;
// Zeile ein istringstream zuweisen
istringstream is(pLINE);
// Aus istringstream einlesen
// mit Komma als Trennzeichen
getline(is,name,',');
// Weiter einlesen bis zum naechsten Trennz.
getline(is,street,',');
// Nun den Rest einlesen
getline(is,city);
...
Damit wollen wir die Behandlung der stringstreams beenden. Es folgt jetzt wieder das obligatorische Beispiel und
dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
Vorgegeben ist eine 'Datenbank'-Datei adresse.csv mit folgendem Inhalt:
Xaver;Xelsbrot;[email protected];Marmeladen Allee 5; 12345; AStadt
Gustav;Gans;[email protected];Gluecksstrasse 123; 99999; BDorf
Daisy;Duck;[email protected];Duckweg 1; 99999; BDorf
Emilie;Lehmann;[email protected];Allerwelts Gasse 55; 76543; CWeiher
Aus dieser Datei wird der Name sowie die Email-Adresse ausgegeben.
Dazu werden die einzelnen 'Datensätze' zuerst zeilenweise eingelesen. Aus der eingelesenen Zeile werden dann
über den istringstream die durch Semikolon getrennten Datenfelder extrahiert.
Das unten stehende Beispiel finden Sie auch unter: le06\prog\Bconv
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (6 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Die Programmausgabe:
Name: Xaver Xelsbrot
EMail: [email protected]
=========================================
Name: Gustav Gans
EMail: [email protected]
=========================================
Name: Daisy Duck
EMail: [email protected]
=========================================
Name: Emilie Lehmann
EMail: [email protected]
=========================================
Das Programm:
// C++ Kurs
// Beispiel zur String-Konvertierungen
#include
#include
#include
#include
<iostream>
<fstream>
<string>
<sstream>
// Konstante fuer Dateiname
const char* const pFILENAME = "adresse.csv";
using
using
using
using
std::cout;
std::endl;
std::string;
std::getline;
int main()
{
// Eingabedatei oeffnen
std::ifstream inFile;
inFile.open(pFILENAME);
if (!inFile)
{
cout << "Datei " << pFILENAME << " kann nicht geoeffnet werden!" << endl;
exit(1);
}
// Komplette Datei bearbeiten
do
{
// Komplette Zeile aus der Datei
string line;
// Zu extrahierende 'Felder' aus der Dateizeile
string firstname, lastname;
string email;
// Konvertierungsstream
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (7 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
std::istringstream is;
// Zeile einlesen
getline(inFile,line);
// Bei erreichen des Dateiendes den Rest ueberspringen
if (inFile.eof())
continue;
// Eingelesene Zeile dem Konvertierungsstream zuweisen
is.str(line);
// Nun Name und Email-Adresse aus der Zeile extrahieren
getline(is,firstname,';');
getline(is,lastname,';');
getline(is,email,';');
// und ausgeben
cout << "Name: " << firstname << ' ' << lastname << '\n';
cout << "EMail: " << email << '\n';
cout << "=========================================" << endl;
} while (!inFile.eof());
// Datei wieder schliessen
inFile.close();
}
Die Übung:
Ihre Aufgabe ist es nun, von der Tastatur einen int-Wert und einen double-Wert einzulesen und dabei
eventuelle Fehleingaben abzufangen. Wurden die Werte richtig eingegeben, so sind diese dann um 10 erhöht
auszugeben. War die Eingabe fehlerhaft, so sind entsprechende Meldungen auszugeben.
Testen Sie Ihr Programm mit dem folgenden, rot dargestellten, Eingaben:
Die Programmausgabe:
E:\cpp_neu\loesungen\le06\lconv>lconv
Bitte int und double Wert eingeben: 12 1.2
Die Werte um 10 erhoeht sind: 22 und 11.2
E:\cpp_neu\loesungen\le06\lconv>lconv
Bitte int und double Wert eingeben: 1.2 12
Zuviele Eingaben! 1.2 12
E:\cpp_neu\loesungen\le06\lconv>lconv
Bitte int und double Wert eingeben: 1 emil
Falsche Eingabe! 1 emil
E:\cpp_neu\loesungen\le06\lconv>lconv
Bitte int und double Wert eingeben: 1a 1.2
Falsche Eingabe! 1a 1.2
E:\cpp_neu\loesungen\le06\lconv>lconv
Bitte int und double Wert eingeben: 1 1.2 emil
Zuviele Eingaben! 1 1.2 emil
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (8 von 9) [17.05.2005 11:38:40]
String-Konvertierungen
Hinweis: Die Eingaben sind rot dargestellt.
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die
Lösung unter loesungen\le06\Lconv.
Damit sind Sie am Ende dieser Lerneinheit angelangt.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le06/le06_03.htm (9 von 9) [17.05.2005 11:38:40]
Funktionen
Funktionen
Die Themen:
Einleitung und Syntax
Funktionsdeklaration (Signatur)
Funktionsdefinition
Funktionsaufruf
Funktionsparameter
Funktionsrückgabewert
Rekursive Funktionen
Sonstiges zu Funktionen
Beispiel und Übung
Einleitung und Syntax
Eine Funktion ist, vereinfacht ausgedrückt, eine Zusammenfassung von mehreren Anweisungen
unter einem bestimmten Namen. Sie können sich eine Funktion als eine Art Teilprogramm
(Unterprogramm) innerhalb eines beliebig komplexen Gesamtprogramms vorstellen.
Funktionen werden hauptsächlich aus zwei Gründen eingesetzt:
Öfters benötigte Sequenzen von Anweisungen müssen nur einmal geschrieben werden
und können dann auf relativ einfache Art von verschiedenen Stellen im Programm aus
ausgeführt werden.
Erst mithilfe von Funktionen ist es möglich, ein größeres Programm in kleine, logische
Teile zu unterteilen. Diese Unterteilung erleichtert den Überblick über das
Gesamtprogramm und damit (auch ganz wichtig!) die Wartbarkeit. Außerdem lassen
sich einzelne Funktionen leichter testen als ein großes Gesamtprogramm.
Eine Funktion hat immer folgenden Aufbau:
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (1 von 21) [17.05.2005 11:38:46]
Funktionen
RETURNTYP Funktionsname ([Parameter])
{
[Definition von lokalen Variablen]
Anweisungen der Funktion
[return RETURNWERT]
}
Die Angaben in den Klammern [...] sind optional.
Beachten Sie bitte, dass nach der geschweiften Klammer zu am Ende der Funktion kein
Semikolon steht.
Funktionsdeklaration (Signatur)
Bevor Sie eine Funktion in einem Programm verwenden (aufrufen) können, muss die Funktion
entweder vorher definiert oder zumindest deklariert sein. Der Unterschied zwischen einer
Funktionsdefinition und einer Funktionsdeklaration ist Folgender:
●
●
Die Funktionsdefinition ist die Codierung der Funktion, d.h. hier stehen die
Anweisungen, die die Funktion ausführen soll. Eine Funktionsdefinition erzeugt folglich
auch Code.
Die Funktionsdeklaration enthält keinen Code sondern nur die Aufrufsyntax der
Funktion.
Vielleicht fragen Sie sich nun, warum überhaupt eine Funktionsdeklaration vor einem
Funktionsaufruf notwendig ist? Nun, der C++ Compiler ist ein 'sehr strenger' Compiler. Trifft
er während des Übersetzungsvorgangs auf den Aufruf einer Funktion die noch nicht definiert
ist, so kann er ohne die notwendige Funktionsdeklaration nicht abprüfen, ob z.B. der Name der
Funktion richtig geschrieben ist und auch die (optionalen) Parameter der Funktion richtig
angegeben wurden. D.h. erst durch diese Funktionsdeklaration kann der Compiler den
Funktionsaufruf syntaktisch überprüfen und auch den notwendigen Aufruf-Code erstellen.
Doch wie sieht die Deklaration aus? Sie hat folgende Syntax:
RETURNTYP Funktionsname ([Parameter]);
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (2 von 21) [17.05.2005 11:38:46]
Funktionen
Wenn Sie nun die
Deklaration mit dem im
vorherigen Abschnitt
aufgeführten Aufbau einer
Funktion vergleichen, so
werden feststellen, dass sie
genau dem Funktionskopf
entspricht, jedoch mit
einem abschließenden
Semikolon.
Funktionsdeklarationen
haben Sie bisher auch
schon mehr oder weniger
bewusst angewandt. Das
Einbinden von HeaderDateien mittels #include
diente unter anderem
genau diesem Zweck.
Rechts sehen Sie wieder
einige Beispiele für
Funktionsdeklarationen.
// Funktionsdeklarationen
void PrintHeader( );
short MinVal(short, short);
// Hauptprogramm
int main( )
{
....
}
Und noch ein weiterer Begriff wird später im Zusammenhang mit Funktionen auftauchen, die
Funktions-Signatur. Die Signatur einer Funktion besteht aus dem Funktionsnamen und den
Parametern, also ohne den Returntyp der Funktion. Sie spielt später beim Überladen von
Funktionen noch eine wichtige Rolle.
Funktionsdefinition
Sehen wir uns jetzt die
Definition einer Funktion
an. Beginnen wir mit dem
Funktionsnamen. Der
Name einer Funktion muss
eindeutig sein, d.h. es darf
keine weitere Funktion,
Variable usw. mit dem
gleichen Namen geben.
Ausnahme: das Überladen
von Funktionen das in einer
späteren Lektion noch
behandelt wird.
// Funktionsdeklarationen
void PrintHeader(); // PrintHeader() und
void Printheader(); // Printheader() sind
// untersch. Funktionen
short Min(short,short);
// Variablendefinitionen
short Min;
// Nicht erlaubt, da
// Min() Fkt. def.
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (3 von 21) [17.05.2005 11:38:46]
Funktionen
Beachten Sie bitte, dass C++ streng zwischen Groß-/Kleinschreibung unterscheidet. So
deklarieren die ersten zwei Funktionsdeklarationen im Beispiel oben zwei unterschiedliche
Funktionen. Vermeiden Sie aber der besseren Lesbarkeit wegen solche Konstruktionen. Die
Definition der short-Variable Min im Beispiel würde einen Übersetzungsfehler erzeugen, da
bereits vorher eine Funktion Min(...) deklariert wurde.
Die letztendlich von einer
Funktion auszuführenden
Anweisungen werden in
einem Block {...}
zusammengefasst.
Innerhalb einer Funktion
können bis auf eine
Ausnahme alle C++
Anweisungen stehen. Nicht
erlaubt ist es, innerhalb
einer Funktion eine weitere
Funktion zu definieren, so
wie dies z.B. die
Programmiersprache
PASCAL zulässt.
Außer Anweisungen im
üblichen Sinne, können Sie
innerhalb von Funktionen
auch Variablen oder
Konstanten definieren.
Auch das Definieren von
Variablen/Konstanten ist ja
letztendlich eine
Anweisung. Diese
Variablen/Konstanten sind
dann aber auch nur
innerhalb der Funktion
gültig.
Nicht erlaubt: Funktion in einer Funktion
void Func1( )
{
....
void Func2( )
{
...
}
....
}
// Das geht nicht!
void Func1( )
{
int temp;
const float PI = 3.1416;
...
}
Doch gehen wir jetzt zur Praxis über und sehen uns den einfachsten Fall einer
Funktionsdefinition an: eine Funktion die keine Parameter (Daten) benötigt und auch kein
Ergebnis zurückliefern muss. Wir wollen jetzt eine einfache Funktion schreiben um einen
bestimmten Text, z.B. für die Überschrift auf einer Seite, auszugeben. Die Funktion soll den
Namen PrintHeader erhalten.
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (4 von 21) [17.05.2005 11:38:46]
Funktionen
Bevor im Programm
PrintHeader(...) aufgerufen
werden kann, muss die
Funktion ( wie erwähnt )
zumindest deklariert sein.
Diese Deklaration ist wie
rechts angegeben
vorzunehmen. Das
Schlüsselwort void vor dem
Funktionsnamen gibt an,
dass die Funktion keinen
Wert zurückliefert. Da die
Funktion auch keine
Parameter (Daten) benötigt
um den Text auszugeben,
bleibt die
Funktionsklammer einfach
leer.
Im Anschluss an die
Deklaration kann die
Funktion dann irgendwo
definiert werden. Dazu ist
zunächst der
Funktionskopf einzugeben;
er entspricht der
Deklaration, jedoch ohne
das abschließende
Semikolon. Nach dem
Funktionskopf folgt der
Funktionsblock {...}. Und
innerhalb dieses Blocks
stehen die entsprechenden
Anweisungen der Funktion.
In unserem Beispiel ist die
Funktion relativ klein und
gibt nur einen festen Text
aus. Beim Erreichen des
Endes des Funktionsblocks
wird automatisch zu der
nach dem Aufruf (wird
gleich behandelt) folgenden
Anweisung zurückgekehrt.
#include <iostream>
// Funktionsdeklaration
void PrintHeader( );
....
int main( )
{
....
}
#include <iostream>
using namespace std;
// Funktionsdeklaration
void PrintHeader( );
....
int main( )
{
....
}
// Definition der Funktion
void PrintHeader ( )
{
cout << "Tabellenüberschrift\n";
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (5 von 21) [17.05.2005 11:38:46]
Funktionen
Funktionsaufruf
Nach dem die Funktion
deklariert und definiert ist,
fehlt jetzt nur noch die
entsprechende Anweisung
um die Funktion auch
aufzurufen, d.h. den
Funktionscode
auszuführen. Auch dies ist
sehr einfach. Schreiben Sie
dazu an den Stellen im
Programm, an denen die
Funktion aufgerufen
werden soll, einfach den
Funktionsnamen, gefolgt
von einer leeren Klammer (
) und einem abschließenden
Semikolon.
#include <iostream>
using namespace std;
// Funktionsdeklaration
void PrintHeader( );
....
int main( )
{
....
// Funktion aufrufen
PrintHeader();
}
// Definition der Funktion
void PrintHeader ( )
{
cout << "Tabellenüberschrift\n";
}
Funktionsparameter
Parameter im Allgemeinen
Nachdem die generelle Handhabung einer Funktion bekannt ist, folgt nun der nächste Schritt:
die Parametrierung von Funktionen. War bisher die Klammer nach dem Funktionsnamen noch
leer, so werden wir jetzt mithilfe dieser Klammer Parameter (Daten) an die Funktion
übergeben.
Fangen wir auch hier
wieder der Deklaration der
Funktion an. Wenn an eine
Funktion Parameter
übergeben werden, so
müssen bei der Deklaration
der Funktion mindestens
die Datentypen der
Parameter angegeben sein.
Damit kann der Compiler
später beim Aufruf der
Funktion zum einen den
void CalcSqrt (double);
ist gleichbedeutend mit
void CalcSqrt (double val);
void PrintIt (unsigned short xPos,
unsigned short yPos);
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (6 von 21) [17.05.2005 11:38:46]
Funktionen
richtigen Aufrufcode
erzeugen und zum anderen
auch eine Überprüfung der
beim Aufruf angegebenen
Parameter vornehmen.
Zusätzlich zum Datentyp kann auch ein beschreibender Parametername bei der
Funktionsdeklaration mit angegeben werden. Benötigt eine Funktion mehrere Parameter, so
sind diese durch Komma voneinander zu trennen. Besonders bei mehreren Parametern hilft die
Angabe eines beschreibenden Parameternamens, wenn der Name die Bedeutung des
Parameters widerspiegelt. Im letzten Beispiel oben erhält die Funktion PrintIt(...) im ersten
Parameter wahrscheinlich die X-Position und im zweiten Parameter die Y-Position für die
Ausgabe. Die Anzahl der Parameter einer Funktion ist nicht begrenzt.
Bei der Definition der
Funktion geben Sie genauso
wie bei der Deklaration die
Datentypen der Parameter
an, nun jedoch zwingend
gefolgt von einem
Parameternamen. Wenn
Sie schon bei der
Deklaration die Parameter
mit Namen versehen haben,
so brauchen Sie bei der
Definition der Funktion die
Deklaration im Prinzip nur
kopieren und das
abschließende Semikolon
entfernen. In der Funktion
können Sie dann über diese
Parameternamen auf die an
die Funktion übergebenen
Werte zugreifen, d.h. die
Parameternamen wirken
sozusagen als Platzhalter
für die im Aufruf
tatsächlich angegebenen
Werte. Die
Parameternamen bei der
Deklaration und der
Definition der Funktion
müssen nicht zwingend
übereinstimmen, aber der
besseren Lesbarkeit wegen
sollten sie es. Wie Daten
// Funktionsdeklaration
void PrintHeader(int pageNum);
....
int main ( )
{
// gleich noch Header ausdrucken
....
//
....
}
// Definition der PrintHeader-Funktion
void PrintHeader(int pageNum)
{
cout << "Dies ist Seite "
<< pageNum << endl;
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (7 von 21) [17.05.2005 11:38:46]
Funktionen
beim Aufruf an die
Funktion übergeben
werden, das kommt gleich
noch. Im Beispiel rechts
sehen Sie eine Funktion
zum Ausdruck einer
Kopfzeile auf einer Seite.
Die aktuelle Seitenummer
wird dabei als Parameter
an PrintHeader(...)
übergeben.
Achten Sie immer darauf, dass die Reihenfolge und Anzahl der Datentypen der
Parameter bei der Funktionsdeklaration und -definition übereinstimmen. Tun Sie dies
nicht, so meldet der Compiler Ihnen einen Fehler!
called-by-value Parameter
Bei einem Parameter vom
Typ called-by-value erhält
die aufgerufene Funktion
nur eine Kopie des
übergebenen Wertes.
Daraus folgt, dass
Änderung des Parameters
zwar innerhalb der
Funktion erlaubt sind, diese
aber auch nur auf die
Kopie des Wertes wirken.
Wird die Funktion wieder
verlassen, so hat sich am
Wert des übergebenen
Parameters nichts
verändert. Die Definition
eines called-by-value
Parameters erfolgt in der
Art, dass innerhalb der
Parameterklammer der
Funktion lediglich der
Datentyp und Name des
entsprechenden Parameters
angegeben wird. Im
Beispiel rechts wird am
Ende der Funktion der
Wert des Parameters
...
// Funktionsdeklaration
void PrintHeader (int pageNum);
....
int main ( )
{
int pNum = 1;
// pNum initialisieren
PrintHeader(pNum); // Funktion aufrufen
...
// pNum hier immer noch 1
PrintHeader(10);
// fixe Seitennummer
}
// Funktionsdefinition
void PrintHeader(int pageNum)
{
cout << "Seiten-Nummer: " << pNum << endl;
pageNum = 99;
// Parameter verändern
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (8 von 21) [17.05.2005 11:38:46]
Funktionen
pageNum zwar immer auf
99 gesetzt, jedoch wirkt sich
dies nicht weiter in main(...)
aus.
Beim Aufruf von Funktion
können Sie für einen calledby-value Parameter
entweder eine Variable
(erster Aufruf rechts) oder
aber eine Konstante
(zweiter Aufruf rechts) an
die Funktion übergeben.
Da die Funktion eine Kopie des Wertes erhält, kann es bei dieser Parameterart unter
Umständen notwendig sein, sehr viele Daten zu kopieren. Dies gilt insbesondere dann,
wenn die Funktion Objekte als Parameter erhält. Sie sollten diese Parameterart nach
Möglichkeit auf die einfachen, bereits bekannten Datentypen wie int oder unsigned
long beschränken.
Referenzparameter
Referenzparameter
ermöglichen Funktionen,
an sie übergebene
Parameter dauerhaft zu
verändern, so dass die
innerhalb der Funktion
durchgeführte Änderung
an den Parametern auch
nach dem Verlassen der
Funktion noch gültig ist.
// Deklaration
void Swap (short& val1, short& val2);
// Hauptprogramm
int main ( )
{
....
Swap (var1, var2);
....
}
// Funktionsdefinition
// Vertauscht die Inhalte zweier Variablen
Die Referenz ist letztendlich void Swap (short& val1, short& val2)
im Prinzip nichts anderes
{
als ein anderer Bezeichner
short temp = val1;
für ein bereits bestehendes
val1 = val2;
Datum. Um einen
val2 = temp;
Funktionsparameter als
}
Referenzparameter zu
kennzeichnen, wird bei der
Deklaration und der
Definition der Funktion
nach dem Datentyp des
Parameters der Operator &
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (9 von 21) [17.05.2005 11:38:46]
Funktionen
angefügt. Beim Aufruf der
Funktion wird ein
Referenzparameter wie ein
Parameter vom Typ calledby-value übergeben.
Oben sehen Sie die Funktion Swap(...), die einfach die Werte der beiden übergebenen Parameter
vertauscht.
Das Einzige was Sie nicht so ohne weiteres als Referenzparameter übergeben können, sind
Konstanten. D.h. die obige Funktion Swap(...) kann zum Beispiel nicht mit Swap(10,var);
aufgerufen werden (was hier aber auch keinen Sinn machen würde).
Da bei Referenzparametern die Daten nicht kopiert werden müssen, ist der Aufruf von
Funktionen mit Referenzparameter mindestens gleich schnell (bei der Übergabe von einfachen
Datentypen als Referenzparameter) oder sogar schneller (bei der Übergabe von Objekten per
Referenzparameter) als der Aufruf von Funktionen mit called-by-value Parameter.
Konstante Referenzparameter
In manchen Fällen kann es
durchaus sinnvoll sein, dass
Parameter die per Referenz
übergeben wurden (keine
Kopiervorgang der Daten)
innerhalb einer Funktion
nicht veränderbar sind.
Denken Sie an die vorherige
Funktion PrintHeader(...),
die die aktuelle
Seitennummer als
Parameter erhalten hat.
Niemand würde hier
vermuten, dass die
Seitennummer innerhalb
der PrintHeader(...)
Funktion verändert wird.
Um nun
Referenzparameter als
nicht-veränderbar
innerhalb einer Funktion
zu kennzeichnen, wird dem
Datentyp des
Referenzparameters das
Schlüsselwort const
...
// Funktionsdeklaration
void PrintHeader (const int& pageNum);
....
int main ( )
{
int pNum = 1;
// pNum initialisieren
PrintHeader(pNum); // Funktion aufrufen
...
// pNum hier immer noch 1
PrintHeader(10);
// fixe Seitennummer
}
// Funktionsdefinition
void PrintHeader(const int& pageNum)
{
cout << "Seiten-Nummer: " << pNum << endl;
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (10 von 21) [17.05.2005 11:38:46]
Funktionen
vorangestellt. Jeder
Versuch, einen als const
definierten
Referenzparameter
innerhalb der Funktion zu
verändern, führt zu einem
Übersetzungsfehler.
Außerdem können bei einem konstanten Referenzparameter nun auch Konstanten übergeben
werden. Dies ist im obigen Beispiel beim zweiten Aufruf der Funktion PrintHeader(...)
dargestellt.
Kennzeichnen Sie grundsätzlich alle nicht veränderbaren Parameter, die nicht per calledby-value übergeben werden, immer als const. In der englisch-sprachigen Literatur wird
dies auch als const-correctness bezeichnet.
Übergabe von eindimensionalen Feldern
Oftmals ist es auch notwendig, Felder an Funktionen zu übergeben. Sehen wir uns zuerst wieder
den einfacheren Fall, dass ein eindimensionales Feld an eine Funktion übergeben werden soll.
Um ein eindimensionales
Feld an eine Funktion zu
übergeben, wird nur dessen
Anfangsadresse übergeben.
Sie wissen doch noch
hoffentlich aus der Lektion
über Felder, dass der Name
eines eindimensionalen
Feldes nichts anderes ist als
der Zeiger auf den Beginn
des Feldes ist. Und damit
erhält die Funktion
zunächst als Parameter
einen entsprechenden
Zeiger von Datentyp des
Feldes. Doch wie wird nun
innerhalb der Funktion auf
die einzelnen Feldelemente
zugegriffen? Nun, der erste
Ansatz hierzu könnte wie
folgt aussehen: man addiert
zum Zeiger den Index des
gewünschten Elements und
// Funktionsdeklaration
void DoSomething(const short* const);
// Hauptprogramm
int main ( )
{
// Felddefinition und Initialisierung
short array[] = {10,20,30,40};
....
DoSomething(array);
....
}
// Funktionsdefinition, Feld kann hier in
// der Funktion nicht veraendert werden
void DoSomething (const short* const ptr)
{
// Zugriff auf 1. Element
short var = *ptr;
// Zugriff auf 3. Element
var = *(ptr+2);
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (11 von 21) [17.05.2005 11:38:46]
Funktionen
dereferenziert diesen dann,
so wie im Beispiel rechts
aufgeführt. Sie müssen
dabei nur beachten, dass
das erste Element den
Index 0 besitzt!
Und noch ein gut gemeinter
Rat. Wenn Sie innerhalb
der Funktion den Inhalt des
Feldes nicht verändern,
definieren Sie dann den
Feldinhalt auch als const.
Im Beispiel rechts erhält die
Funktion DoSomething(...)
einen konstanten Zeiger auf
ein konstantes Feldes.
DoSomething(...) kann
dadurch weder den Zeiger
selbst noch den Inhalt des
durch den Zeiger
adressierten Feldes
verändern.
Aber es geht auch
eleganter, wenigsten für
den armen Programmierer
der mit Zeigern noch etwas
auf Kriegsfuß steht.
Anstelle nun irgendwelche
Zeigerarithmetik
durchführen zu müssen,
kann der an die Funktion
übergebene Zeiger auch als
Feldname 'missbraucht'
werden und damit indiziert
auf die Feldelemente
zugegriffen werden. Dass
dies möglich ist rührt von
der bereits erwähnten
Tatsache her, dass der
Name eines
eindimensionalen Feldes
gleichzusetzen ist mit der
Anfangsadresse des Feldes.
Damit kann das vorherige
Beispiel auch wie recht
// Funktionsdeklaration
void DoSomething(short*);
// Hauptprogramm
int main ( )
{
// Felddefinition und Initialisierung
short array[] = {10,20,30,40};
....
DoSomething(array);
....
}
// Funktionsdefinition, Feld kann hier in
// der Funktion veraendert werden
void DoSomething (short* const ptr)
{
// Zugriff auf 1. Element
short var = ptr[0];
// 3. Element veraendern
ptr[2] = 10;
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (12 von 21) [17.05.2005 11:38:46]
Funktionen
angegeben umgeschrieben
werden.
Wenn Sie wollen können Sie bei der Deklaration und der Definition der Funktion für
den Parameter anstelle eines Zeigers auch Folgendes angeben:
void DoSomething (short array[4]);
void DoSomething (short array[]);
Übergabe von mehrdimensionale Feldern
Ein klein wenig
komplizierter sieht die
Sache bei
mehrdimensionalen
Feldern aus. Hier müssen
Sie dem Compiler etwas
unter die Arme greifen
damit er die Feldelemente
im Speicher auch findet.
Bei der Deklaration und
der Definition der Funktion
müssen Sie die Feldgröße
des übergebenen Feldes mit
angegeben, damit der
Compiler innerhalb der
Funktion die Position der
einzelnen Elemente korrekt
berechnen kann. Lediglich
die Angabe der 'höchsten
Dimension' ist optional
(siehe Beispiel rechts). Die
Übergabe des Feldes beim
Aufruf der Funktion bleibt
gegenüber
eindimensionalen Feldern
unverändert, d.h. in der
Parameterklammer der
Funktion steht auch hier
lediglich der Name des zu
übergebenden Feldes.
// Feld definieren
const int ROWS=4;
const int COLUMNS=3;
short array[ROWS][COLUMNS];
// Funktionsdeklaration
void PrintVal(short arr[][COLUMNS]);
int main()
{
// Funktion aufrufen
PrintVal(array);
....
}
// Funktionsdefinition
void PrintVal(short arr[][COLUMNS])
{
// Zugriff auf Feldelement
short var = arr[0][2];
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (13 von 21) [17.05.2005 11:38:46]
Funktionen
Die oben angegebene Übergabe eines mehrdimensionalen Feldes an eine Funktion ist
kürzeste Schreibweise. Alternativ kann auch die vollständige Dimension eines Felder für
die Übergabe angegeben werden:
void PrintVal(short arr[ROWS][COLUMNS]);
Funktionsrückgabewert
Doch außer dass
Funktionen Werte als
Parameter erhalten
können, können sie auch
einen (und nur einen) Wert
zurückliefern. Dieser Wert
wird auch als Returnwert
bezeichnet. Dazu muss der
Compiler aber wissen,
welchen Datentyp der
Returnwert besitzt (für eine
eventuelle Typanpassung).
Dieser Datentyp wird vor
dem Funktionsnamen
angegeben, und zwar
sowohl bei der Deklaration
wie auch bei der Definition
der Funktion. Die
Rückgabe des eigentlichen
Wertes erfolgt dann mit
einer return-Anweisung.
Nach dem Schlüsselwort
return folgt der
zurückzugebende Wert, der
natürlich mit dem Datentyp
vor dem Funktionsnamen
übereinstimmen muss.
Innerhalb einer Funktion
können, wenig notwendig,
durchaus mehrere returnAnweisungen stehen.
// Funktionsdeklaration
float Deg2Rad (float deg);
// Hauptprogramm
int main ( )
{
....
float rad = Deg2Rad(90.0f);
....
}
// Funktionsdefinition
// Umrechnung von Grad in Bogenmass
// Liefert umgerechnetes Ergebnis
float Deg2Rad (float deg)
{
const float PI = 3.1416f;
float result;
result = deg/360.0f * 2.0f * PI;
return result;
}
Die Funktion Deg2Rad(...) oben erhält als Parameter eine Gradzahl als float-Wert übergeben
und rechnet die Gradzahl dann in Bogenmaß um (3600 = 2*Pi).
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (14 von 21) [17.05.2005 11:38:46]
Funktionen
Die obige Funktion Deg2Rad(...) erhält den Wert mittels called-by-value übergeben. Als
Alternative hätten Sie den Wert auch als const-Referenz übergeben können, also in
folgender Form:
float Deg2Rad (const float& deg);
Wichtig hier die Angabe von const, da Deg2Rad(...) ansonsten den übergebenen Wert
'dauerhaft' verändern könnte.
Wollen Sie mehr als einen Wert aus der Funktion zurückliefern, so müssen Sie dies bis jetzt
über entsprechende Referenzparameter tun.
Rekursive Funktionen
Sehen wir uns jetzt noch
einen 'Spezialfall' von
Funktionen an. Da
innerhalb einer Funktion
(bis auf eine Ausnahme)
alle Anweisungen erlaubt
sind, können Funktionen
selbstverständlich
wiederum Funktionen
aufrufen. Einen Sonderfall
stellen hierbei solche
Funktionen dar, die sich
wieder selbst aufrufen.
Solche Funktionen werden
auch als rekursive
Funktionen bezeichnet.
Diese Funktionen benötigen
aber immer ein
Abbruchkriterium in etwa
der folgenden Form um
eine Endlos-Schleife zu
vermeiden:
if (Bedingung)
return Wert;
#include <iostream>
using namespace std;
// Funktionsdeklaration
void PrintLine(const short);
// Hauptprogramm
int main ( )
{
// Funktionsaufruf
PrintLine(4);
}
// Funktion PrintLine ruft sich selbst auf!
void PrintLine(const short count)
{
// Sternchen und Zeilenvorschub ausgeben
for (int index=0; index < count; index++)
cout << " *";
cout << endl;
// Falls mehr als 1 Sternchen ausgegeben,
if (count != 1)
// Funktion erneut aufrufen, jetzt
// jedoch mit einem Sternchen weniger
PrintLine(count-1);
// Nach dem letzten Sternchen fertig!
return;
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (15 von 21) [17.05.2005 11:38:46]
Funktionen
Die Angabe von Wert
entfällt bei Funktionen mit
dem Returntyp void.
Programmausgabe:
* * * *
* * *
* *
*
Oben rechts sehen Sie ein Beispiel für eine solche rekursive Funktion. Die Funktion PrintLine(...)
erhält aus dem Hauptprogramm zunächst die Anzahl der in einer Reihe auszugebenden
Sternchen. Nach dem die entsprechende Anzahl von Sternchen in einer FOR-Schleife
ausgegeben wurden, wird in der darauf folgenden IF-Abfrage abgeprüft, ob mehr als 1
Sternchen ausgegeben wurde. Ist dies der Fall, so ruft sich die Funktion erneut selbst auf, jetzt
doch mit einem Sternchen weniger. Dieses Spiel wiederholt sich solange bis nur noch ein
Sternchen ausgegeben wird. In diesem Fall ist die Bedingung der IF-Abfrage nicht mehr erfüllt
und die Funktion wird ganz normal über die RETURN-Anweisung beendet. Die RETURNAnweisung könnte hier auch entfallen, da sie sowieso die letzte Anweisung der Funktion ist.
Damit ergibt sich folgende Aufrufsequenz der Funktion:
PrintLine(4);
PrintLine(3);
PrintLine(2);
PrintLine(1);
Beachten Sie, dass die Funktion PrintLine(...) einen const Parameter erhält. Trotzdem kann die
Funktion selbstverständlich mit diesem Parameter Operationen durchführen, etwa wie beim
rekursiven Aufruf der Funktion:
PrintLine(nCount-1);
Das Einzige was die Funktion nicht tun darf ist, zu versuchen den Parameter zu verändern!
Sonstiges zu Funktionen
Damit sind wir wieder fast am Ende dieser Lektion angelangt. Bevor wir zum Beispiel und der
anschließenden Übung kommen, noch weitere 'Eigenschaften' von Funktionen:
●
●
C++ bietet die Möglichkeit für Funktionsparameter Defaultwerte zu spezifizieren. Dies
wird im 3. Kapitel in einer gesonderten Lektion noch behandelt.
Außerdem kennt C++ noch so genannte inline-Funktionen. Was sich dahinter verbirgt
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (16 von 21) [17.05.2005 11:38:46]
Funktionen
●
und welchen Vorteil sie bieten wird ebenfalls im 3. Kapitel noch behandelt.
Und jeder C++ Compiler kennt eine Unmenge an Standard-Funktionen wie z.B. sin(...)
oder strcpy(...). Viele dieser Funktionen sind im ANSI-C bzw. ANSI-C++ definiert.
Zusätzlich beherrschen die meisten Compiler noch zusätzliche Funktionen die nicht
standardisiert sind. Hier hilft nur ein Blick in die Beschreibung zum Compiler.
Und zu guter letzt gibt es noch Zeiger auf Funktionen. Was es damit auf sich hat, das
erfahren Sie, wenn Sie das Symbol links anklicken. Aber Funktionszeiger sind für den
Anfang nur etwas für eingefleischte Programmierer.
So, und jetzt kommt endlich das Beispiel und dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
In diesem Beispiel geht's etwas mathematisch zu. Jeder kennt aus seiner Schulzeit noch die
Winkelfunktion Sinus. Zur Berechnung des Sinus gibt es zwar auch schon eine fertige
Bibliotheksfunktion, wir werden aber zur Berechnung des Sinus eine Annährungsformel
verwenden und dann auch einmal nachschauen, wie genau diese Annäherung an die
Bibliotheksfunktion ist.
Die Annäherungsformel zur Berechnung des Sinus lautet:
sin(x) = x - x^3/3! + x^5/5! - x^7/7! + ...
Hierbei entspricht x^3 dem Term x*x*x. Der Ausdruck 3! ist die Fakultät von 3, d.h. 1*2*3.
Die Berechnung des Annäherungswerts erfolgt in der Funktion BerechneSin(...). Die Übergabe
des Werts, aus dem der Sinus berechnet werden soll, erfolgt als called-by-value da die
Funktion den Wert nicht verändern muss.
Zur Überprüfung der Funktion BerechneSin(...) wird dann die Bibliotheksfunktion sin(...)
aufgerufen, die folgende Funktionsdeklaration besitzt:
double sin(double value);
Sie berechnet aus dem übergebenen double-Wert value den Sinus und liefert ihn ebenfalls als
double-Wert zurück. Die für den Aufruf der Funktion sin(...) notwendige
Funktionsdeklaration ist in der Headerdatei cmath enthalten.
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (17 von 21) [17.05.2005 11:38:46]
Funktionen
Wenn Sie in den Help-Dateien nach der Funktion sin(...) suchen, so werden Sie dort
anstelle der Header-Datei cmath teilweise die Heder-Datei math.h vorfinden. Der
Grund hierfür ist, dass sin(...) eigentlich eine C-Funktion ist und in C-Programme
Header-Dateien die Extension .h besitzen.
Im Hauptprogramm werden dann für drei verschiedene Wert die Sinuswerte berechnet, und
zwar einmal mit der Funktion BerechneSin(...) und dann mit der Bibliotheksfunktion sin(...).
Da die berechneten Werte nicht weiter benötigt werden, werden die Funktionsaufruf direkt
innerhalb der cout-Anweisungen durchgeführt.
Falls Sie den MinGW-Compiler verwenden, müssen Sie die Definition der
Konstanten PI auskommentieren, da dort die Konstante bereits intern definiert
wird. Dies entspricht aber nicht dem C++ Standard!
Das unten stehende Beispiel finden Sie auch unter: le07\prog\Bfunc
Die Programmausgabe:
sin(0.5) : Berechnet = 0.479426, Bibl-Funktion = 0.479426
sin(PI) : Berechnet = -0.0752295, Bibl-Funktion = -7.34641e-06
sin(PI/2): Berechnet = 0.999843, Bibl-Funktion = 1
Das Programm:
// C++ Kurs
// Beispiel zu Funktionen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <cmath>
using std::cout;
using std::endl;
// PrototyPIKng
double BerechneSin(double wert);
int main()
{
// Konstante PIK definieren
const double PI = 3.1416;
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (18 von 21) [17.05.2005 11:38:46]
Funktionen
// Berechne Sinus(0.5) mittels Reihe
cout << "sin(0.5) : Berechnet = " << BerechneSin(0.5);
// Und nun Sinus(0.5) mittels Bibliotheksfunktion
cout << ", Bibl-Funktion = " << sin(0.5) << endl;
// Das ganz nun fuer PI (3.1416);
cout << "sin(PI) : Berechnet = " << BerechneSin(PI);
cout << ", Bibl-Funktion = " << sin(PI) << endl;
// und zum Schluss fuer PI/2
cout << "sin(PI/2): Berechnet = " << BerechneSin(PI/2.0);
cout << ", Bibl-Funktion = " << sin(PI/2.0) << endl;
}
// Funktion: BerechneSin
//
Berechnet Sinus mittels Reihe
//
sin(x) = x - x^3/3! + x^5/5! - x^7/7!
// Parameter wert: Wert aus dem Sinus berechnet werden soll
double BerechneSin(double wert)
{
// Ergebnis mit dem Wert vorinitialisieren
// 1. Term in der Reihe
double erg = wert;
// Divident (Nenner) mit wert^3 vorbelegen
double divident = wert*wert*wert;
// Divisor (Teiler) 3! vorbelegen
double divisor = 1.0*2.0*3.0;
// 3!
// Zweiten Term vom Ergebnis subtrahieren
erg -= divident/divisor;
// Dritten Term zum Ergebnis addieren
divident *= wert*wert;
// wert^5
divisor *= 20.0;
// 3! * 4 * 5 = 5!
erg += divident/divisor;
// Vierten Term wieder vom Ergebnis subtrahieren
divident *= wert*wert;
// wert^7
divisor *= 42.0;
// 5! * 6 * 7 = 7!
erg -= divident/divisor;
// Ergebnis zurückliefern
return erg;
}
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (19 von 21) [17.05.2005 11:38:46]
Funktionen
Die Übung:
Nun zur Abwechslung mal etwas aus der Physik.
Für einen Wurf (schräger Wurf) sind die Wurfweite und die maximale Wurfhöhe zu
berechnen. Für die Wurfweite gilt folgende Formel:
Weite = (Abwurfgeschwindigkeit2 * sin(2*Abwurfwinkel)) / G
Und für die Wurfhöhe gilt:
Höhe = Abwurfgeschwindigkeit2 * sin(Abwurfwinkel)2 / (2*G)
Die Funktionsdeklaration der Funktion sin(...) ist oben beim Beispiel angegeben. Beachten
Sie, dass sin(...) den Winkel in Rad erwartet! Die Abwurfgeschwindigkeit ist in beiden
Formeln in m/s und G ist die Gravitationskonstante 9.81m/s2. Beide Formeln liefern das
Ergebnis in m zurück.
Schreiben Sie jetzt jeweils eine Funktion zur Berechnung der Wurfweite und eine Funktion
zur Berechnung der Wurfhöhe bei vorgegebener Abwurfgeschwindigkeit und Abwurfwinkel.
Im Hauptprogramm ist nun zuerst bei einem fest vorgegebenem Abwurfwinkel von 45
Grad(!) die jeweilige Wurfweite und -höhe zu berechnen. Die Abwurfgeschwindigkeit ist im
Bereich 10....20 m/s in 2er-Schritten zu durchlaufen. Die berechneten Daten sind dann in
Tabellenform (wie unten angegeben) auszugeben.
Anschließend ist das ganze Spiel mit einer festen Abwurfgeschwindigkeit von 28m/s
(entspricht ca. 100km/h) zu wiederholen, wobei jetzt der Abwurfwinkel im Bereich 30...60
Grad in 5er-Schritten zu variieren ist.
Beachten Sie bitte, dass die Winkelangaben zunächst in Grad erfolgen und die Funktion
sin(...) als Einheit Rad erwartet. Sie wissen doch noch aus Ihrem Mathematik-Unterricht,
dass 2*PI gleich 360 Grad sind? Damit lautet die Formel für die Umrechung von Grad in
Rad:
Rad = Grad / 360.0 * 2.0 * 3.1416
So, und nun viel Glück beim Lösen der Aufgabe. Bei richtiger Lösung sollten Sie die unten
dargestellten Daten erhalten.
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (20 von 21) [17.05.2005 11:38:46]
Funktionen
Die Programmausgabe:
Schraeger Wurf mit konst. Winkel
Abwurfgeschw. (m/s):10, Wurfweit
Abwurfgeschw. (m/s):12, Wurfweit
Abwurfgeschw. (m/s):14, Wurfweit
Abwurfgeschw. (m/s):16, Wurfweit
Abwurfgeschw. (m/s):18, Wurfweit
Abwurfgeschw. (m/s):20, Wurfweit
von 45 Grad
(m):10.19, Hoehe
(m):14.68, Hoehe
(m):19.98, Hoehe
(m):26.10, Hoehe
(m):33.03, Hoehe
(m):40.77, Hoehe
(m): 2.55
(m): 3.67
(m): 4.99
(m): 6.52
(m): 8.26
(m):10.19
Schraeger Wurf mit konst. Abwurfgeschw. 28 m/s
Abwurfwinkel (Grad):30, Wurfweit (m):69.21, Hoehe
Abwurfwinkel (Grad):35, Wurfweit (m):75.10, Hoehe
Abwurfwinkel (Grad):40, Wurfweit (m):78.70, Hoehe
Abwurfwinkel (Grad):45, Wurfweit (m):79.92, Hoehe
Abwurfwinkel (Grad):50, Wurfweit (m):78.70, Hoehe
Abwurfwinkel (Grad):55, Wurfweit (m):75.10, Hoehe
Abwurfwinkel (Grad):60, Wurfweit (m):69.21, Hoehe
(m): 9.99
(m):13.15
(m):16.51
(m):19.98
(m):23.45
(m):26.81
(m):29.97
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le07\lfunc.
In der nächsten Lektion befassen wir uns mit der Lebensdauer von Daten.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le07/le07_01.htm (21 von 21) [17.05.2005 11:38:46]
C-String Konvertierungen
C-String Konvertierungen
Nachfolgend finden Sie eine
Übersicht über die 'alten' CFunktionen zur
Umwandlung eines Strings
in einen binären Wert.
Beachten dabei bitte, dass
mithilfe der Funktionen
nur immer ein Wert
umgewandelt wird.
const char *pAInt = "123";
const char *pADouble = "3.1416";
// 'int'-String nach int-Wert
int iVar = atoi(pAInt);
// 'double'-String nach double-Wert
double dVar = atof(pADouble);
Funktion
Beschreibung
int atoi (const char *pString)
Konvertiert den String
pString in eine intZahl
long atol (const char *pString)
wie atoi(), jedoch
Konvertierung in eine
long-Zahl
double atof (const char *pString)
wie atoi(), jedoch
Konvertierung in eine
double-Zahl
double strtod (const char *pString,
char** ppEnd)
wie atof(), ppEnd zeigt
danach auf den Teil des
Strings, der nicht
konvertiert wurde.
long strtol (const char *pString,
char** pEnd,
int radix)
wie atol(), jedoch wird
der String als Eingabe
zur Zahlenbasis radix
betrachtet.
unsigend long strtoul (const char* pString, wie strol(), jedoch
char** ppEnd,
Konvertierung in einen
int radix)
unsigned long Wert
Wie Sie der obigen Tabelle entnehmen können, gibt es nur Funktionen um einen String in einen
numerische Werte zu konvertieren.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
http://www.cpp-tutor.de/cpp/le06/le06_03_d1.htm (1 von 2) [17.05.2005 11:38:47]
C-String Konvertierungen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le06/le06_03_d1.htm (2 von 2) [17.05.2005 11:38:47]
Gültigkeitsbereiche
Gültigkeitsbereiche
Die Themen:
Arten von Gültigkeitsbereichen
Modul- oder Dateigültigkeit
Funktionsgültigkeit
Blockgültigkeit
Zugriff auf globale Daten
Arten von Gültigkeitsbereichen
Nichts währt ewig, und so hat auch eine Variable oder Konstante nur eine begrenzte
Lebensdauer oder Gültigkeit. Im Nachfolgenden wird der Begriff Daten bzw. Datum verwendet,
wenn damit sowohl Variablen wie auch Konstanten gemeint sind.
Der Gültigkeitsbereich eines Datums gibt den Bereich an, innerhalb dessen das Datum sichtbar
ist und damit darauf zugegriffen werden kann. Bei Variablen gilt zusätzlich, dass sie nur
innerhalb ihres Gültigkeitsbereichs auch tatsächlich Speicherplatz belegen. Durch geschickte
Wahl des Gültigkeitsbereichs lässt sich somit unter Umständen erheblich Speicherplatz
einsparen.
Im Prinzip gibt es 4
unterschiedliche
Gültigkeitsbereiche:
short modul;
.... // weitere Definitionen
int main ( )
{
Modul oder Datei
short local;
.... // weitere Defintionen
Funktion
{
Block
short block;
.... // weitere Definitionen
}
http://www.cpp-tutor.de/cpp/le07/le07_02.htm (1 von 5) [17.05.2005 11:38:48]
Gültigkeitsbereiche
Klasse
....
}
Ein Beispiel für die Gültigkeitsbereiche 1 bis 3 ist rechts dargestellt. Der vierte Gültigkeitsbereich
Klasse wird später bei der Behandlung der Klassen erläutert.
Modul- oder Dateigültigkeit
Daten die nicht innerhalb
von Blöcken {...} definiert
sind werden als globale
Daten bezeichnet. Sie sind ab
der Stelle im Programm
gültig, an der sie definiert
sind. Ihre Gültigkeit endet
am Modulende (Dateiende).
Globale Variablen werden
vor dem Eintritt in die
Funktion main(...) mit 0
vorbelegt.
short modul1;
// modul1 ab hier gültig
int main ( )
{
modul1 = 10;
....
}
short modul2;
// modul1 und modul2 ab
// hier gültig
void Function ( )
{
modul2 = modul1;
....
}
Obwohl C++ (im Gegensatz zu C) es zulässt, dass globale Daten an beliebiger Stelle im
Modul definiert werden können, sollten Sie jedoch aufgrund der besseren Lesbarkeit (und
Wartbarkeit!) alle globalen Daten am Modulanfang definieren. D.h. die Definition der
Variable modul2 im obigen Beispiel fördert eigentlich nicht gerade die Lesbarkeit eines
Programms.
Funktionsgültigkeit
http://www.cpp-tutor.de/cpp/le07/le07_02.htm (2 von 5) [17.05.2005 11:38:48]
Gültigkeitsbereiche
Daten die innerhalb von
Funktionen (wie z.B.
innerhalb von main(...))
definiert sind, werden als
lokale Daten bezeichnet.
Lokalen Daten sind auch nur
in der Funktion gültig, in der
sie definiert sind. Aus diesem
Grund dürfen lokale Daten
in verschiedenen Funktionen
den gleichen Namen besitzen
ohne dass diese sich
gegenseitig beeinflussen.
Lokale Daten können an
beliebiger Stelle innerhalb
einer Funktion definiert
werden.
short var;
int main ( )
{
var = 1;
....
}
// globales var
void Func1 ( )
{
short var; // verdeckt globales var
var = 10; // lokales var setzen
....
}
void Func2 ( )
{
short lVar = 3.14;
Lokale Variablen werden
nicht automatisch initialisiert
und haben damit einen
}
zufälligen Startwert. In der
Regel werden sie auf dem
Stack abgelegt (Stack =
besonderer Bereich
innerhalb eines Programms
z.B. zur Ablage von
temporären Daten) und
belegen nur während der
Zeit Speicher, während der
die Funktion ausgeführt
wird. Beim Verlassen der
Funktion werden sie
automatisch entfernt.
// lokale Variable
// Ausgabe lokales lVar
// und globales var.
cout << var << lVar << endl;
....
Besitzt ein lokales Datum den
gleichen Namen wie ein
globales Datum, so verdeckt
das lokale Datum das globale
(siehe auch Beispiel rechts).
Blockgültigkeit
http://www.cpp-tutor.de/cpp/le07/le07_02.htm (3 von 5) [17.05.2005 11:38:48]
Gültigkeitsbereiche
Daten die innerhalb eines
Blocks {...} (hier ist nicht der
Funktionsblock gemeint)
definiert werden, verhalten
sich wie lokale Daten, d.h. sie
werden nicht automatisch
initialisiert und verlieren
beim Verlassen des Blocks
ihre Gültigkeit und geben
damit auch den belegten
Speicher wieder frei. Und
auch hier gilt wieder: wird
innerhalb eines Blocks ein
Datum mit dem gleichen
Namen definiert wie ein
lokales oder globales Datum,
so verdeckt das Blockdatum
diese.
short var1;
// globales var1
int main ( )
{
short var2; // lokales var2
....
{
short var1;
....
// Blockvar. verdeckt
// globales var1
{
short var2; // Blockvar. verdeckt
....
// lokales var2
}
}
}
Funktionslokale Daten und
blocklokale Daten verhalten
sich im Prinzip gleich. Der
Unterschied wurde an dieser
Stelle nur gemacht damit
Ihnen bewusst wird, dass Sie
innerhalb eines beliebigen
Blocks jederzeit neue Daten
definieren können. Und nur
innerhalb dieses Blocks
belegen die Daten
letztendlich auch Speicher.
Zugriff auf globale Daten (scope resolution operator,
Gültigkeitsbereichsoperator)
http://www.cpp-tutor.de/cpp/le07/le07_02.htm (4 von 5) [17.05.2005 11:38:48]
Gültigkeitsbereiche
Werden globale Daten durch
funktionslokale oder
blocklokale Daten verdeckt,
so kann mithilfe des
Gültigkeitsbereichsoperators
:: (scope resolution operator,
das sind zwei Doppelpunkte!)
vor dem entsprechenden
Variablennamen trotzdem
auf die globalen Daten
zugegriffen werden. Dieser
Zugriff funktioniert aber nur
auf globale Daten und nicht,
wie im Beispiel rechts rot
dargestellt, auf
'übergeordnete' lokale Daten.
short var1;
// globales var1
int main ( )
{
short var2; // lokales var2
....
{
short var1;
short var2;
var1 = 10;
::var1 = 0;
::var2 = 0;
// Blockvariablen
//
//
//
//
Blockvar. setzen
globale Var. setzen
kein Zugriff auf
lokales var2!
}
}
War doch im Prinzip nicht schwer mit den Gültigkeitsbereichen, oder? Machen wir also gleich
weiter und sehen uns als nächstes die Speicherklassen an. Und da dürfen Sie auch wieder üben!
Noch ein Hinweis (im Vorgriff) zum Schluss: wenn Sie später im Kurs lernen, Objekte
richtig einzusetzen, ist die Verwendung von globalen Daten (aber nicht die von globalen
Objekten!) fast ein Vergehen. Da wir aber zum jetzigen Zeitpunkt noch keine Objekte
erstellen können, soll dieser 'Ausrutscher' bis zur Einführung von Objekten toleriert
werden.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le07/le07_02.htm (5 von 5) [17.05.2005 11:38:48]
Funktionszeiger
Funktionszeiger
Und hier wird's leider (mal wieder) etwas komplizierter. In einem Funktionszeiger können, wie
unschwer zu erraten ist, Adressen von Funktionen abgelegt werden.
Definition eines Funktionszeigers
Aber sehen wir uns
zunächst die Definition
eines Funktionszeigers an.
Die Definition eines
Funktionszeigers gleicht im
Prinzip der Deklaration
einer Funktion, deren
Adresse später im Zeiger
abgelegt werden soll. Nur
dass jetzt anstelle des
Funktionsnamens
innerhalb einer Klammer
der Name des
Funktionszeigers steht.
Damit kann ein
Funktionszeiger nun auch
nur die Adressen von
Funktionen aufnehmen die
im Returntyp und in der
Anzahl und Art der
Parameter mit der
Zeigerdefinition
übereinstimmen.
// Zeigt auf Funktionen vom Typ
//
void Func();
void (*pfnFunc1)();
// Zeigt auf Funktionen vom Typ
//
short Func(short, char*);
short (*pfnFunc2)(short, char*);
Damit können Sie im Funktionszeiger pfnFunc1 nur Adressen von Funktionen ablegen, die keine
Parameter besitzen und auch keinen Returnwert liefern. Der Zeiger pfnFunc2 hingegen kann
nur Adressen von Funktionen aufnehmen, die als ersten Parameter einen short-Wert und als
zweiten Parameter einen char-Zeiger erwarten. Zusätzlich müssen die Funktionen dann auch
noch einen short-Wert als Returnwert besitzen.
Bilden einer Funktionsadresse
http://www.cpp-tutor.de/cpp/le07/le07_01_d1.htm (1 von 5) [17.05.2005 11:38:49]
Funktionszeiger
Nach dem der
Funktionszeiger definiert
ist, kann ihm die Adresse
einer entsprechenden
Funktion zugewiesen
werden. Um die Adresse
einer Funktion zu erhalten
wird einfach der Name der
entsprechenden Funktion
angegeben, d.h. es darf hier
kein Adressoperator &
stehen. Beachten Sie aber
bitte, dass Sie dem Zeiger
auch nur Adressen von
solchen Funktionen
zuweisen, die im Returntyp
und den Parametern mit
dem Zeigertyp
übereinstimmen! So würde
im Beispiel rechts die
Zuweisung der Adresse der
Funktion DoAnything(...)
zum Zeiger pfnFunc1 einen
Übersetzungsfehler
erzeugen.
// Funktionsdeklarationen
void DoSomething();
short DoAnything(short, char*);
// Zeigerdefinition
void (*pfnFunc1)();
short (*pfnFunc2)(short, char*);
// Adresse der Funktion zuweisen
pfnFunc1 = DoSomething;
pfnFunc2 = DoAnything;
Funktionsaufruf über Funktionszeiger
So, bleibt zum Schluss nur
noch die Frage offen, wie
man die Funktion aufruft
deren Adresse im Zeiger
abgelegt ist? Wie Sie
bestimmt noch wissen
erfolgt der Zugriff auf
Speicherstellen über Zeiger
durch den
Dereferenzierungsoperator
*. Und genauso geht's auch
beim Aufruf von
Funktionen über
Funktionszeiger. Nur muss
der dereferenzierte
Funktionszeiger hier
innerhalb einer Klammer
// Zeigerdefinition
void (*pfnFunc1)();
short (*pfnFunc2)(short, char*);
....
// Funktionsaufrufe
(*pfnFunc1)();
nErg = (*pfnFunc2)(var, text);
http://www.cpp-tutor.de/cpp/le07/le07_01_d1.htm (2 von 5) [17.05.2005 11:38:49]
Funktionszeiger
stehen. Erhält die
aufzurufende Funktion
noch Parameter, so sind
diese wie bei einem
'normalen' Funktionsaufruf
in einer weiteren Klammer
anzugeben.
Ein Beispiel:
Über den Zeiger pfnState werden die Funktionen State1(...), State2(...) und State3(...)
nacheinander aufgerufen. Dazu wird zunächst im Hauptprogramm der Zeiger auf die erste
Funktion State1(...) gesetzt. In der nachfolgenden WHILE-Schleife wird dann solange die im
Zeiger abgelegte Funktion aufgerufen, bis der Zeiger den Wert NULL enthält. Welche
Funktion als nächstes aufgerufen wird, wird durch die aktuell ausgeführte Funktion
festgelegt. So lädt z.B. State1(...) als nächste auszuführende Funktion die Funktion State2(...)
in den Funktionszeiger. Die Funktion State3(...) veranlasst schließlich den Abbruch der
WHILE-Schleife, in dem abhängig von einer Bedingung der Wert NULL in den Zeiger
geschrieben wird.
Dieses Beispiel finden Sie auch unter: le07\prog\Bfzeiger
Die Programmausgabe:
Zustand
Zustand
Zustand
Zustand
Zustand
Zustand
1:
2:
3:
1:
2:
3:
Zaehler=1
Zaehler=2
Zaehler=3
Zaehler=4
Zaehler=5
Zaehler=6
http://www.cpp-tutor.de/cpp/le07/le07_01_d1.htm (3 von 5) [17.05.2005 11:38:49]
Funktionszeiger
Das Programm:
// C++ Kurs
// Beispiel zur Funktionszeiger
//
// Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Prototyping der Zustandsfunktionen
void State1(short&);
void State2(short&);
void State3(short&);
// Funktionszeiger definieren
void (*pfnState)(short&);
// Hauptprogramm
int main ()
{
short counter = 1;
// Funktionszeiger auf 1. Zustand
pfnState = State1;
// Nun alle Zustaende durchlaufen
do
{
// Zustandsfunktion aufrufen
(*pfnState)(counter);
}
while (pfnState != NULL);
}
// Funktion fuer Zustand 1
void State1(short& var)
{
cout << "Zustand 1: Zaehler=" << var << endl;
var++;
pfnState = State2;
}
// Funktion fuer Zustand 2
void State2(short& var)
{
cout << "Zustand 2: Zaehler=" << var << endl;
http://www.cpp-tutor.de/cpp/le07/le07_01_d1.htm (4 von 5) [17.05.2005 11:38:49]
Funktionszeiger
var++;
pfnState = State3;
}
// Funktion fuer Zustand 3
void State3(short& var)
{
cout << "Zustand 3: Zaehler=" << var << endl;
// Falls uebergebener Parameter kleiner 4
// Zustand wieder auf 1 setzen
// sonst kein weiterer Zustand
if (var < 4)
pfnState = State1;
else
pfnState = NULL;
var++;
}
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le07/le07_01_d1.htm (5 von 5) [17.05.2005 11:38:49]
Speicherklassen und Qualifzierer
Speicherklassen und Qualifizierer
Die Themen:
Einleitung
auto Speicherklasse
register Speicherklasse
extern Speicherklasse
static Speicherklasse
mutable Speicherklasse
const Qualifizierer
volatile Qualifizierer
Beispiel und Übung
Einleitung
Wie lange eine Variable gültig ist und damit auf sie zugegriffen werden kann (Gültigkeitsbereich,
Sichtbarkeit), wird durch die Speicherklasse der Variable festgelegt. C++ kennt folgende
Speicherklassen:
auto, register, extern, static, mutable
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (1 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Um eine Variable einer
bestimmten Speicherklasser
zuzuordnen, wird einfach vor
dem Datentyp der Variablen
die entsprechende
Speicherklasse angegeben.
Zudem können Variablen,
bedingt durch die Stellung
ihrer Definition im
Programm, implizit einer
bestimmten Speicherklasse
angehören.
auto short var;
register int count;
extern char *pText;
static bool first;
Außer Speicherklassen können Variablen zusätzlich noch einen so genannten Qualifizierer
(Qualifier) besitzen. Folgende Qualifizierer stehen zur Verfügung:
const, volatile
Die Angabe des zusätzlichen const int NUMBERELEMENTS;
Qualifizierers erfolgt vor
volatile unsigned long ticks;
dem Datentyp der Variablen extern volatile portReady;
aber nach nach einer
eventuell vorhandenen
Speicherklasse.
auto Speicherklasse
Variablen der Speicherklasse
auto sind immer lokale
Variablen. Ihr
Gültigkeitsbereich wird
durch den sie
umschließenden Block {...}
begrenzt. Diese
Speicherklasse wird jedoch
so gut wie nie angegeben da
lokale Variablen
standardmäßig dieser
Speicherklasse angehören.
auto Variablen werden nicht
automatisch initialisiert, d.h.
sie haben nach ihrer
Definition einen zufälligen
Wert.
auto short var; // keine glob. auto-Var!
int main ()
{
auto char alpha;
// expl. auto-Def.
short count ;
// impl. auto-Def.
....
}
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (2 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Manche Systeme legen auto Variablen auf dem Stack ab. Dies muss aber nicht
unbedingt so sein, da der C++ Standard keine Vorgaben über das Speicher-Layout eines
Systems machen kann. Sollten aber auf Ihrem System auto Variablen auf dem Stack
abgelegt werden, so achten Sie darauf, dass Sie keine großen Felder als auto Variablen
definieren. Der Stackbereich hat in der Regel nur eine bestimmte Größe (meistens
einstellbar über Compiler-Optionen) und es kann es Ihnen ansonsten passieren, dass der
Stack 'überläuft', d.h. der Stack überschreibt anderweitige Daten- bzw. Codebereiche in
Ihrem Programm.
register Speicherklasse
Variablen der Speicherklasse
register können ebenfalls nur
lokale Variablen sein oder
aber Parameter von
Funktionen. Der Compiler
versucht Variablen dieser
Speicherklasse innerhalb
eines Prozessor-Registers
abzulegen anstatt im
Speicher. Dadurch erhöht
sich die
Zugriffsgeschwindigkeit auf
die Variable erheblich, und
damit auch die
Ausführungsgeschwindigkeit
des Programms. Aber wie
gesagt, der Compiler
versucht nur die Variable in
einem Prozessor-Register
unterzubringen. Sie können
durchaus mehrere Dutzend
Variablen dieses Typs
definieren ohne einen
Übersetzungsfehler erhalten,
obwohl der Prozessor selbst
z.B. nur 8 Register besitzt.
// Funktion mit register Parameter
void PrintIt(register char *pText);
// Nicht erlaubt, da register nur
// auf lokale Variablen
register short count;
int main( )
{
register int loop;
....
}
void PrintIt(register char *pText)
{
....
}
Wenn Sie versuchen von einer register Variable die Adresse zu bilden, legt der Compiler die
Variable allerdings automatisch im Speicher ab da C++ davon ausgeht, dass von Registern keine
Adresse gebildet werden kann..
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (3 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Moderne Compiler optimieren in der Regel so gut, dass Sie sich nicht um die Ablage von
Variablen kümmern müssen. Die Speicherklasse register hat in der heutigen Zeit mehr
historische Bedeutung.
extern Speicherklasse
Variablen oder Funktionen
der Speicherklasse extern
teilen dem Compiler mit,
dass ihre Definition in einem
anderen Modul (QuellcodeDatei) erfolgt als im
aktuellen. D.h. als externe
deklarierte Daten belegen
zunächst einmal keinen
Speicherplatz, sondern erst
bei ihrer Definition in einem
anderen Modul.
Datei FILE1.CPP
Vergessen Sie als
extern deklarierte
Variablen oder
Funktionen in einem
anderen Modul zu
definieren, so erhalten
Sie zunächst keinen
Übersetzungsfehler.
Erst beim Linken
(Zusammenbinden)
der Module erhalten
Sie vom Linker eine
entsprechende
Fehlermeldung da er
die Definition nicht
findet.
Datei FILE2.CPP
// Funktionsdeklaration
void PrintIt (const char* const pText);
// Definition einer globalen Variable
short counter;
....
// Definition der Funktion
void PrintIt (const char* const pText)
{
....
}
// Verweis auf Fkt. im Modul FILE1.CPP
extern void PrintIt (const char* const);
// Verweis auf Variable im Modul FILE1.CPP
extern short counter;
....
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (4 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Eine etwas andere Funktion
besitzt die Speicherklasse
extern bei benannten
Konstanten. Wie schon in der
Lektion über Konstanten
erwähnt, sind benannten
Konstanten standardmäßig
modulglobal, d.h. nur in der
Quellcode-Datei gültig, in der
sie definiert sind. Soll eine
benannte Konstante so
definiert werden, dass sie in
verschiedenen Modulen
gültig ist, so muss sie sowohl
bei ihrer Definition als auch
bei den Referenzen darauf als
externe Konstante definiert
werden. Beachten Sie dabei
aber bitte, dass die Konstante
nur einmal initialisiert
werden darf; ansonsten
erhalten Sie beim Linken des
Programms einen Fehler in
der Form, dass die Konstante
mehrfach definiert ist.
Datei FILE1.CPP
// Definition der Konstanten
extern const int MAX = 5;
Datei FILE2.CPP
// Referenz auf benannte Konstante
extern const int MAX;
Die Speicherklasse extern wird auch dazu verwendet, um C Funktionen in ein C++
Programm einzubinden. Wie dies geht erfahren Sie, wenn Sie links das Symbol
anklicken.
static Speicherklasse
Kommen wir jetzt zur Speicherklasse static. Globale Variablen und Funktionen dieser
Speicherklasse sind nur in dem Modul sichtbar (und damit gültig), in dem Sie definiert sind. Eine
extern-Referenz auf eine Variable oder Funktion dieses Typs führt zu einer Fehlermeldung beim
Linken des Programms.
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (5 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Lokale Variablen der
Speicherklasse static behalten
ihren letzten Wert auch dann
noch bei, wenn ihr
Gültigkeitsbereich verlassen
wird. Beim nächsten Eintritt
in den Gültigkeitsbereich
kann dann mit dem zuletzt
abgespeicherten Wert weiter
gearbeitet werden. Wohl
gemerkt, dies betrifft nur die
Erhaltung des Wertes, der
Gültigkeitsbereich der
Variable bleibt weiterhin auf
den Block begrenzt in dem
sie definiert ist. Im Beispiel
rechts wird die Variable
count dazu verwendet, die
Anzahl der Funktionsaufrufe
zu zählen. Da lokale
Variablen aber nicht
automatisch initialisiert
werden, sollten sie (wie im
Beispiel) bei ihrer Definition
mit einem Startwert versehen
werden. Diese Initialisierung
wird aber nur ein einziges
Mal ausgeführt, nämlich
beim Reservieren des
Speicherplatzes für die staticVariable.
// Funktionsdeklaration
static void PrintIt (const char *pText);
....
// Funktionsdefinition
void PrintIt(const char *pText)
{
static short count = 0;
....
count++;
}
mutable Speicherklasse
Die mutable Speicherklasse spielt nur im Zusammenhange mit Klassen eine Rolle und ist nur der
Vollständigkeit halber hier erwähnt. Mehr dazu später bei der Einführung von Klassen ( ).
const Qualifizierer
Den const Qualifizierer im Zusammenhang mit Variablen (einfachen Variablen, Zeiger und
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (6 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Felder) haben Sie im Verlaufe des Kurses schon mehrfach kennen gelernt. Später im Kurs
werden wir uns diesen Qualifizierer nochmals ansehen, und zwar ebenfalls im Zusammenhang
mit Klassen ( ) und den dazugehörigen Konstruktoren ( ).
volatile Qualifizierer
Kommen wir jetzt zum
Qualifizierer volatile. volatile
kann nur für Variablen
verwendet werden. Eine als
volatile definierte Variable
kann auch außerhalb des
normalen Programmablaufs,
und damit auf eine nicht vom
Compiler feststellbare Art
und Weise, ihren Zustand
(Wert) verändern. Ursachen
für solche asynchronen
Zustandsänderungen von
Variablen sind das
Betriebssystem, die
Hardware (Interrupts) oder
ein gleichzeitig laufender
Thread. Eine typische volatile
Variable ist z.B. die
Systemzeit. Die Systemzeit
wird in der Regel nicht durch
die Anwendung verändert,
sondern durch das
Betriebssystem wie z.B. MS
Windows.
// beliebige Funktion
void DoAnything(...)
{
volatile unsigned long ticks;
unsigned long var;
....
var = ticks;
....
var = ticks;
}
Da die heutigen Compiler in der Regel sehr gut optimieren, könnte ohne volatile im Beispiel oben
die zweite Zuweisung unter gewissen Bedingungen durch den Compiler entfernt werden, da sie
genau der ersten Anweisung entspricht und ticks zwischen diesen beiden Anweisung
augenscheinlich nicht verändert wird. Durch die Definition von ticks als volatile wird dem
Compiler mitgeteilt, dass diese Variable auch außerhalb des normalen Programmablaufs (z.B. in
einer Interrupt-Routine) verändert werden kann und damit keinerlei Optimierungen bezüglich
ticks vorgenommen werden dürfen. Bei jedem Lesezugriff auf ticks wird immer der aktuelle Wert
aus dem Speicher ausgelesen und jeder Schreibzugriff führt zur sofortigen Ablage des neuen
Werts im Speicher.
Und damit sind wir am Ende dieser Lektion angelangt kommen nun zum Beispiel und Ihrer
Übung.
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (7 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Beispiel und Übung
Das Beispiel:
Das Beispiel berechnet aus 10 Zufallszahlen den Mittelwert. Die Berechnung des Mittelwerts
erfolgt innerhalb einer Funktion, die die erzeugten Zufallszahlen nacheinander als Parameter
erhält und daraus den jeweils aktuellen Mittelwert (Gleitender Mittelwert) berechnet.
Dazu verwendet die Funktion zwei Variablen der Speicherklasse static: ein Variable zum
Ablegen der Summe über alle Werte und eine static Variable für die Anzahl der Werte.
Das unten stehende Beispiel finden Sie auch unter: le07\prog\Bstatic
Die Programmausgabe:
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Wert:
Wert:
Wert:
Wert:
Wert:
Wert:
Wert:
Wert:
Wert:
Wert:
1
7
4
0
9
4
8
8
2
4
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Neuer
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
Mittelwert:
1
4
4
3
4.2
4.16667
4.71429
5.125
4.77778
4.7
Das Programm:
// C++ Kurs
// Beispiel zu Speicherklassen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
#include <cstdlib>
using std::cout;
using std::endl;
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (8 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
// Prototyping
double Mittelwert (const double wert);
//
// Hauptprogramm
int main ()
{
// Schleifenindex im Register ablegen
register short index;
// 10 Zufallszahlen erzeugen
for (index=0; index<10; index++)
{
// Zufallszahl im Bereich 0..9 erzeugen
double zahl = rand() % 10;
// Zufallszahl ausgeben
cout << "Neuer Wert: " << zahl;
// Mittelwert berechnen und ausgeben
cout << "
Neuer Mittelwert: " << Mittelwert(zahl) << endl;
}
}
//
// Funktion zur Berechnung des Mittelwerts
// Eingabe : Neuer hinzuaddierender Wert
// Ausgabe : Neuer Mittelwert
double Mittelwert(const double wert)
{
// Summe ueber alle bisherigen Werte
static double summe = 0.0f;
// Zaehler fuer Anzahl der Werte
static double anzahl = 0.0f;
// Summe und Anzahl der Werte erhoehen
summe += wert;
anzahl++;
// Mittelwert zurueckgeben
return summe/anzahl;
}
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (9 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Die Übung:
Sie sollen eine Funktion schreiben, die fortlaufende Seriennummern für 3 verschiedene
'Gerätetypen' erzeugen kann. Der Einfachheit halber werden die Gerätetypen mit den
Nummern 0, 1 und 2 gekennzeichnet.
Die Seriennummern der Geräte des Typs '0' sollen ab 0 beginnen, die des Gerätetyps '1' ab 100
und die des Gerätetyps '2' ab 1000.
Erzeugen Sie im Hauptprogramm zufallsmäßig insgesamt 15 Geräte mit unterschiedlichen
Gerätetypen. Rufen Sie dann die Funktion zur Erzeugung der jeweiligen Seriennummer auf
und geben Sie diese einschließlich des Gerätetyps aus.
Wenn Sie die Aufgabe richtig gelöst haben, sollten Sie eine Ausgabe in etwa der nachfolgenden
Form erhalten.
Die Programmausgabe:
Erzeuge nun
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Geraetetyp:
Seriennummern:
1 erhaelt Seriennummer
1 erhaelt Seriennummer
1 erhaelt Seriennummer
2 erhaelt Seriennummer
2 erhaelt Seriennummer
1 erhaelt Seriennummer
1 erhaelt Seriennummer
0 erhaelt Seriennummer
1 erhaelt Seriennummer
1 erhaelt Seriennummer
1 erhaelt Seriennummer
0 erhaelt Seriennummer
1 erhaelt Seriennummer
1 erhaelt Seriennummer
0 erhaelt Seriennummer
101
102
103
1001
1002
104
105
1
106
107
108
2
109
110
3
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le07\lstatic.
So, und wieder eine Lerneinheit geschafft.
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (10 von 11) [17.05.2005 11:38:51]
Speicherklassen und Qualifzierer
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le07/le07_03.htm (11 von 11) [17.05.2005 11:38:51]
typedef Anweisung
typedef Anweisung
Die Themen:
Syntax
typedef und Funktionszeiger
Syntax
Mithilfe der typedefAnweisung können
Synonyme (gleich andere
Namen) für bestehende
Datentypen gebildet
werden. Die typedefAnweisung hat folgende
Syntax:
// Synonym WORD für unsigned short
// und DWORD für unsigned long
typedef unsigned short WORD;
typedef unsigned long DWORD;
// unsigned long Variable definieren
DWORD ulongVar;
// unsigned short Variable definieren
WORD ushortVar;
typedef DTYP
Synonym;
Das Synonym kann nach
seiner Definition überall im
Programm dort stehen, wo
Datentypen zugelassen sind.
Steht die typedef-Anweisung
innerhalb eines Blocks, so
gilt das definierte Synonym
auch nur innerhalb dieses
Blocks.
http://www.cpp-tutor.de/cpp/le08/le08_01.htm (1 von 4) [17.05.2005 11:38:51]
typedef Anweisung
Auch im Zusammenhang
mit der Portierung von
Programmen auf eine
andere Plattform kann die
typedef-Anweisung recht
nützlich sein. Da beim
Programmentwurf in der
Regel der Datentyp einer
Variablen nach deren
Wertebereich festgelegt
wird, können sich beim
Portieren einer Anwendung
(z.B. von einer 32-Bit
Umgebung auf eine 16-Bit
Umgebung) Probleme
ergeben, wenn die
Wertebereiche der
Standard-Datentypen
unterschiedlich sind.
16-Bit System (int gleich 16 Bit)
typedef int INT16;
typedef unsigned int UINT16;
....
32-Bit System (short gleich 16 Bit)
typedef short INT16;
typedef unsigned short UINT16;
...
Definitionen
INT16 nVar1;
UINT16 wVar2;
Um diesem Problem aus
dem Weg zu gehen,
definieren Sie über typedefAnweisungen
entsprechende Synonyme
für die einzelnen
Wertebereiche, z.B. das
Synonym INT16 für eine
vorzeichenbehaftete 16-Bit
Ganzzahl. Wenn Sie jetzt
bei den Definitionen der
Daten anstelle der StandardDatentypen diese Synonyme
einsetzen, so belegt eine
INT16-Variable unabhängig
von der verwendeten
Plattform immer 16-Bit
und besitzt damit auch
immer den gleichen
Wertebereich.
typedef und Funktionszeiger
http://www.cpp-tutor.de/cpp/le08/le08_01.htm (2 von 4) [17.05.2005 11:38:51]
typedef Anweisung
Und auch wenn typedefs in
99% aller Fälle Ihnen nur
Schreibarbeit ersparen
(einmal abgesehen von der
vorhin erwähnten
Portabilität), so gibt es
jedoch einen Fall, bei dem
Sie ohne typedef nicht
auskommen: nämlich wenn
eine Funktion einen
Funktionszeiger
zurückgibt. In diesem Fall
müssen Sie für den
Funktionszeiger ein typedef
verwenden, da ansonsten
Ihr Compiler mit der
richtigen Interpretation der
Anweisung Schwierigkeiten
hat. In Beispiel rechts soll
die Funktion GetFPtr(...)
einen Zeiger auf eine
void/void Funktion
zurückliefern. Dazu muss
ein entsprechendes
Synonym für einen solchen
Funktionszeiger definiert
werden. Im Beispiel ist dies
fp.
// typedef fuer Funktionszeiger
typedef void (*fp)();
// Funktionen deklarieren
void f1();
void f2();
// Funktion die den Funktionszeiger
// liefert deklarieren
fp GetFPtr();
int main()
{
// Weiteren Fkt-Zeiger definieren
void (*pFunc)();
// Funktion aufrufen
pFunc = GetFPtr();
(*pFunc)();
}
// Funktion mit Fkt-Zeiger Returnwert
fp GetFPtr()
{
...
return f1;
}
Zugegeben, dies ist schon
ein sehr komplizierter Fall.
Aber wenn Sie so etwas
einmal später benötigen,
dann wissen Sie ja jetzt, wo
Sie das 'Rezept' hierfür
finden.
Ja und das war's auch schon. Kurz und (fast) schmerzlos. In der nächsten Lektion lernen Sie
einen neuen Datentyp kennen, den enumerated Datentyp.
http://www.cpp-tutor.de/cpp/le08/le08_01.htm (3 von 4) [17.05.2005 11:38:51]
typedef Anweisung
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/le08_01.htm (4 von 4) [17.05.2005 11:38:51]
extern "C" Speicherklasse
extern "C" Speicherklasse
Und noch eine Anwendung
für die Speicherklasse
extern. Eine besondere
Form der extern-Anweisung
ist die Anweisung extern
"C". Wollen Sie von einem
C++ Programm aus auf
Funktionen zugreifen, die
mit einem C-Compiler
(nicht C++ Compiler!)
übersetzt worden sind, so
müssen Sie diese
Funktionen als extern "C"
Funktionen kennzeichnen.
// Deklaration einer C-Funktion die
// von einem C++ Programm aufgerufen wird
extern "C" void MyCFunc(....);
// Deklarationen fuer mehrere C-Funktionen
extern "C"
{
void MyCFunc1(...);
short MyCFunc2(...);
}
Der Grund hierfür ist, dass ein C++ Compiler als Symbolname für eine Funktion nicht nur (wie
ein C-Compiler) den Funktionsnamen alleine verwendet, sondern auch noch die
Funktionsparameter mit in den Symbolnamen einbaut. Dieses Verhalten des C++ Compilers
wird auch als name mangeling bezeichnet. Durch die Anweisung extern "C" weisen Sie den C++
Compiler nun an, als Symbolname für die Funktion auch nur den Funktionsnamen zu
verwenden, genauso wie es der C-Compiler tut. Warum der C++ Compiler auch die Parameter
noch mit verwendet, das erfahren Sie im 4. Kapitel beim Überladen von Funktionen.
Haben Sie eine Reihe von CFunktionen geschrieben die
Sie sowohl unter C wie
auch unter C++ einsetzen
wollen, so werden Sie in der
Regel die Deklarationen der
Funktionen in einer HeaderDatei zusammenfassen.
Damit die C-Funktionen
auch von C++ aus
aufrufbar sind, müssen Sie
in der Header-Datei auch
als extern "C" Funktionen
deklariert werden (wie
bereits vorhin erwähnt).
#ifdef __cplusplus
extern "C"
{
#endif
.... // Hier stehen die Funktionsdekl.
#ifdef __cplusplus
}
#endif
Wird diese Header-Datei nun aber von einem C-Compiler eingelesen, so gibt dieser eine
http://www.cpp-tutor.de/cpp/le07/le07_03_d1.htm (1 von 2) [17.05.2005 11:38:52]
extern "C" Speicherklasse
Fehlermeldung aus da er die extern "C" Anweisung nicht kennt. Sie könnten nun getrennte
Header-Dateien für den C und C++ Compiler erstellen, was aber irgendwann zu
Wartungsproblemen führen wird, da Sie immer beide Header-Dateien gleichzeitig bearbeiten
müssen. Im Beispiel rechts sehen Sie eine Lösung dieses Problems. Nur ein C++ Compiler
definiert ein internes Symbol mit dem Namen __cplusplus (2 Underscores am Anfang!). Und
dieses Symbol können Sie wie rechts dargestellt mittels #ifdef...#endif abfragen und so
entsprechend die extern "C" Anweisung einbinden (oder auch nicht). Die Anweisungen
#ifdef...#endif werden am Ende dieses Kapitels bei den Präprozessor-Anweisungen noch
ausführlich behandelt.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le07/le07_03_d1.htm (2 von 2) [17.05.2005 11:38:52]
Klassen
Klassen
Die Themen:
Objekte in der Theorie
Die Klasse
Objekte
OOP-Begriffe
Definition von Memberfunktionen
Zugriffsrechte in Klassen
Zugriff auf Klassenmember
const-Memberfunktionen und mutable-Member
Aufruf von Funktionen aus Memberfunktionen
Kopieren von Objekten
Objekte als Parameter
Objekte als Rückgabewert
Entwicklung einer Klasse
Die objektorientierte Programmierung (OOP)
Beispiel und Übung
Objekte in der Theorie
In dieser Lektion beginnen wir den Einstieg in die objekt-orientierte Programmierung (OOP).
Sehen wir uns zunächst einmal an, was ein Objekt eigentlich ist. Nun, ein Objekt ist eine Instanz
einer bestimmten Klasse. Aber ich vermute einmal, dass Sie jetzt noch nicht viel schlauer sind als
vorher. Denn was ist eine Instanz oder eine Klasse?
Fangen wir also mit der Beschreibung der Klasse an. Eine Klasse ist letztendlich nichts anderes als
ein neuer Datentyp, der (vereinfacht ausdrückt) Daten und Funktionen vereint. Bisher konnten
Daten und Funktionen unabhängig voneinander bestehen, d.h. eine Funktion war (bei sauberer
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (1 von 35) [17.05.2005 11:38:56]
Klassen
Programmierung) nicht auf das Vorhandensein eines bestimmten Datums angewiesen und auch
nicht umgekehrt. Eine Klasse bringt nun Daten und Funktionen zusammen. Sie besteht in der Regel
aus Daten und Funktionen, die aber auf irgend eine Weise voneinander abhängig sind. Die Daten
einer Klassen werden auch als deren Eigenschaften (Properties) bezeichnet. Die Funktionen in einer
Klasse arbeiten nun aber nicht mit beliebigen Daten, sondern wirken die auf diese Eigenschaften
und werden auch als deren Memberfunktionen bezeichnet. Und die Gesamtheit aller Eigenschaften
und Memberfunktionen einer Klasse werden Member genannt. Damit besteht also eine Klasse aus
Member, die wiederum aus Eigenschaften und Memberfunktionen bestehen. Später in dieser
Lerneinheit, wenn wir einmal eine richtige Klasse entworfen haben, werden wir uns die Begriffe
nochmals anhand eines praktischen Beispiels ansehen.
Doch zunächst einmal ein (theoretisches) Beispiel für eine Klasse. Fast alle Betriebssysteme bieten
heutzutage eine grafische Oberfläche (GUI = graphic user interface). Diese grafische Oberfläche
besteht fast immer aus irgend welchen Fenstern. Wenn wir nun versuchen wollen, ein solches
allgemeines Fenster zu beschreiben, dann zählen wir dessen Eigenschaften auf. So hat ein Fenster
zum Beispiel ein Größe und bestimmte Position. Aber diese Eigenschaften alleine machen ein
Fenster noch nicht voll funktionsfähig. Wir benötigen letztendlich noch Funktionen, um das Fenster
auch in seinen Eigenschaften (Größe und Position) verändern zu können. Haben Sie die letzten
Sätze aufmerksam gelesen? Dort tauchen wieder die Begriffe Eigenschaft und Funktion auf. Und
sowohl die Eigenschaften (Größe und Position) wie auch die dazugehörige Funktionen (zum
Verschieben und Vergrößern) bilden letztendlich die Klasse Fenster. Ziel der OOP ist es, mehr
oder weniger komplexe Gebilde als Ganzes abzubilden, d.h. die Eigenschaften und die darauf
wirkenden Funktionen zusammenzufassen.
Kommen wir nun zum Begriff der Instanz. Eine Instanz ist prinzipiell nichts anderes, als die
Realisierung einer Klasse. Dazu wird eine 'Variable' der entsprechenden Klasse definiert. Unter der
OOP sprechen wir dann aber nicht von Variablen sondern von Objekten. Nehmen wir einmal an,
Sie hätten eine Klasse Window irgend wo definiert und dann folgende Anweisung geschrieben:
Window myWindow;
Dann ist myWindow eine Instanz der Klasse Window und man sagt dann auch: myWindow ist ein
Objekt vom Typ Window. Sie können von einer Klasse beliebig viele Objekte definieren, die alle die
gleichen Eigenschaften besitzen. Die Werte der Eigenschaften können dann natürlich
unterschiedlich sein.
Aber sehen wir uns nun den prinzipiellen Aufbau einer Klasse näher an.
Die Klasse
Genauso wie Variablen einen bestimmten Datentyp besitzen (bool., short, double usw.), besitzen
auch Objekte einen bestimmten Datentyp. C++ stellt für Objekte die drei Datentypen struct, class
und union zur Verfügung. Auf den Unterschied zwischen den Datentypen struct und class kommen
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (2 von 35) [17.05.2005 11:38:56]
Klassen
wir nachher gleich zu sprechen. Der union ist dann eine eigene Lerneinheit gewidmet.
Bilden wir nun Schritt für Schritt das erwähnte Fenster in einer grafischen Oberfläche als Klasse
ab.
Zunächst benötigt jede Klasse class Window
einen 'Rahmen', in dem die
{
Eigenschaften und
... // Hier folgen gleich die
Memberfunktionen
... // Eigenschaften und Memberfunktionen
zusammengefasst werden
};
können. Dieser Rahmen besteht
zunächst aus dem
Schlüsselwort class (oder auch
struct), gefolgt von einem
Namen für die Klasse (im
Beispiel rechts Window).
Anschließend folgt ein Block
{...}, der mit einem Semikolon
abgeschlossen werden muss.
Vergessen Sie dieses Semikolon am Ende der Klassendefinition, so meldet Ihnen der
Compiler beim Übersetzen des Programms eine Reihe von Fehlern. Dieses Vergessen des
Semikolons ist eine häufige Fehlerursache am Anfang.
Der Name für die Klasse muss immer eindeutig sein. Wenn Sie später mehrere Klassen definieren,
so müssen diese unterschiedliche Namen besitzen. Auch dürfen Sie keine weiteren Variablen oder
Funktionen mit dem Klassennamen definiert haben.
Fügen wir nun zur Klasse ihre Member (Eigenschaften und Memberfunktionen) innerhalb des
Blocks {...} hinzu.
Fangen wir sinnvoller Weise mit den Eigenschaften an. Welchen Eigenschaften besitzt ein Fenster,
d.h. was beschreibt ein solches GUI-Fenster? In der Praxis besitzt ein Fenster sicher ein Menge
Eigenschaften. Wir werden es aber auf vier Eigenschaften reduzieren, damit es nicht zu
unübersichtlich wird. Unser GUI-Fenster soll eine bestimmte Position und Ausdehnung besitzen.
Und damit könnte zum Beispiel class Window
im nächsten Schritt die Klasse {
für Fensterobjekte wie rechts
int xPos, yPos;
// Position
dargestellt aussehen. Ein
unsigned int width, height; // Groesse
Fenster besitzt hier die
};
Eigenschaften, dass es eine
bestimmte X- und Y-Position
sowie ein definierte Breite und
Höhe hat.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (3 von 35) [17.05.2005 11:38:56]
Klassen
Für die Eigenschaft in einer Klasse können alle bisher bekannten Datentypen verwendet werden; so
ist es zum Beispiel auch erlaubt, innerhalb einer Klasse als Eigenschaft eine weitere Klasse zu
verwenden. Solche eingeschlossenen Klassen werden später noch gesondert behandelt, da sie
bestimmte Anforderungen erfüllen müssen.
Sie können bis jetzt aber noch keine Konstanten als Eigenschaften einer Klasse definieren.
Wie Konstanten als Eigenschaften definiert werden, erfahren Sie in der Lektion über
Konstruktoren und Destruktoren.
Aber die Eigenschaften alleine machen das Fenster uninteressant. Es würde sich weder verschieben
noch in der Größe verändern lassen. Um die Eigenschaften verändern zu können, werden in der
objekt-orientierten Programmierung Memberfunktionen verwendet. Und auch diese
Memberfunktionen werden, da sie vom Konzept her nur zur Manipulation der Eigenschaften
angedacht sind, innerhalb der Klasse deklariert bzw. definiert.
Sehen wir uns das auch gleich class Window
wieder anhand der Klasse für {
Fensterobjekte an. Welche
int xPos, yPos;
Memberfunktionen innerhalb
unsigned int width, height;
einer Klasse notwendig sind,
void Move(int x, int y);
ergibt sich hier fast von alleine
void Size(unsigned int w,
aufgrund der Eigenschaften.
unsigned int h);
Wir haben eine
};
Fensterposition, also benötigen
wir eine Memberfunktion zum
Verschieben des Fensters, die
wir im Beispiel mit Move(...)
bezeichnen. Ferner besitzt das
Fenster noch eine Größe, also
benötigen wir zum Verändern
der Fenstergröße eine weitere
Memberfunktion, im Beispiel
Size(...) genannt.
//
//
//
//
//
Position
Groesse
Verschieben
Groesse
veraendern
Die Parameter der Memberfunktionen ergeben sich hier automatisch aus den Datentypen der
Eigenschaften, die nachher mit der Memberfunktion verändert werden sollen.
Die Gesamtheit aller Memberfunktionen einer Klasse wird auch als deren Schnittstelle (Interface)
bezeichnet, da in einer 'sauberen' Implementierung einer Klasse nur über diese Memberfunktionen
die Eigenschaften verändert werden sollten.
Beachten Sie bitte, dass in der obigen Klassendefinition die Memberfunktionen nur deklariert sind.
Wo Memberfunktionen definiert (kodiert) werden können, das sehen wir uns nachher an.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (4 von 35) [17.05.2005 11:38:56]
Klassen
Nicht alle Klassen benötigen unbedingt Memberfunktionen für den Zugriff auf die
Eigenschaften. Wenn Sie das Symbol links anklicken können Sie sich einmal ein Beispiel
für eine Klasse ansehen, die keine Memberfunktionen enthält.
Folgt nun der nächste Schritt, die Definition von Objekten.
Objekte
Damit ist der Aufbau eines Fensters vollständig beschrieben und wir können an das eigentliche
'Erstellen' von Fenstern gehen. Bisher haben wir ja lediglich die Eigenschaften und Schnittstelle
festgelegt. Um ein Fenster anzulegen wird ein Objekt der Fensterklasse Window definiert. Dies
erfolgt analog zur bisherigen Definition einer Variablen, d.h. zuerst folgt der Datentyp des Objekts
und danach der Name.
Der vollständige Datentyp eines
Objekts besteht aus dem
Schlüsselwort class bzw struct
(je nach dem, welchen Typ die
Klasse hat), dann dem
Klassennamen und zum
Schluss folgt der Objektname.
C++ erlaubt es auch, bei
Eindeutigkeit das
Schlüsselwort class bzw. struct
einfach wegzulassen, so wie im
Beispiel rechts bei der zweiten
Objekt-Definition dargestellt.
Diese zweite Form der ObjektDefinition ist auch die in der
Praxis am gebräuchlichste.
class Window
{
... // Member-Definitionen
};
// Objekte vom Typ class Window definieren
class Window firstWin;
Window secondWin;
Beide Fensterobjekte enthalten zwar die gleichen Eigenschaften, diese können aber (und werden es
in der Regel auch) unterschiedliche Werte besitzen. So kann das Fenster firstWin zum Beispiel eine
andere Größe besitzen als das Fenster secondWin.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (5 von 35) [17.05.2005 11:38:56]
Klassen
Sie können aber nicht nur
einfache Objekte definieren,
sondern sogar auch
Objektfelder. Die Definition
eines Objektfeldes erfolgt
analog zur Definition eines
Feldes mit den StandardDatentypen.
class Window
{
... // Member-Definitionen
};
// Objektfeld definieren
Window winArray[10];
Wenn Sie noch etwas zur prinzipiellen Ablage von Objekten im Speicher erfahren wollen,
klicken Sie das Symbol links an.
OOP-Begriffe
Nach dem Sie nun wissen, wie
eine Klasse prinzipiell
aufgebaut ist, jetzt nochmals
die wichtigsten OOP-Begriffe
im Überblick:
Eine Klasse besteht aus
Member.
Die Member einer
Klasse sind deren
Eigenschaften und
Memberfunktionen.
Die Eigenschaften einer
Klasse sind die
Klassendaten.
Die Memberfunktionen
sind die Schnittstellen
(Interfaces) einer
Klasse nach außen hin
und wirken auf die
Eigenschaften.
Objekte sind Instanzen
von Klassen.
So weit, so gut. Sehen wir uns als nächstes an, wie die Memberfunktionen einer Klasse definiert
werden.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (6 von 35) [17.05.2005 11:38:56]
Klassen
Definition von Memberfunktionen
Definition von Memberfunktionen innerhalb der Klasse
Die Definition von
Memberfunktionen kann zum
einen innerhalb der Klasse
selbst erfolgen. Die Definition
erfolgt dann prinzipiell gleich
wie die Definition einer
normalen Funktion, nur eben
innerhalb der Klasse.
Beachten Sie, dass nach
der schließenden
Klammer der
Memberfunktionen
kein Semikolon steht.
class Window
{
... // Eigenschaften
void Move(int x, int y)
{
... // Fenster verschieben
}
void Size(unsigned int w, unsigned int h)
{
... // Groesse veraendern
}
};
Definition von Memberfunktionen außerhalb der Klasse
Hier sind jetzt zwei Schritte
notwendig:
●
●
Innerhalb der Klasse
wird die
Memberfunktion nur
deklariert. Die
Deklarationen erfolgt in
der gleichen Art und
Weise wie bei
Funktionen.
Bei der Definition
außerhalb der Klasse ist
es nun erforderlich, dass
die zu definierende
Memberfunktion einer
Klasse zugeordnet wird.
Dazu wird nach dem
Returntyp und vor dem
Name der
Memberfunktion der
entsprechende
Klassenname gefolgt
class Window
{
... // Eigenschaften
... // Deklaration der Memberfunktionen
void Move(int x, int y);
void Size(unsigned int w, unsigned int h);
};
// Definition der Move(...) Memberfunktion
void Window::Move(int x, int y)
{
...
// Fenster verschieben
}
// Definition der Size(...) Memberfunktion
void Window::Size(unsigned int w,
unsigned int h)
{
...
// Groesse veraendern
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (7 von 35) [17.05.2005 11:38:56]
Klassen
vom GültigkeitsbereichsOperator :: angegeben.
Da bei der Definition der Memberfunktionen außerhalb der Klasse der Klassenname mit
angegeben werden muss, können verschiedene Klassen ohne weiteres Memberfunktionen mit
gleichem Namen besitzen. Der Compiler kann die Definition der Memberfunktionen durch die
Angabe des Klassennamens immer eindeutig zuordnen.
Definition von Klassen/Memberfunktionen in der Praxis
Sehen wir uns einmal an, wie Klassen und deren Memberfunktionen in der Praxis definiert werden.
Wenn Sie eine Klasse in
verschiedenen Modulen
(Quellcode-Dateien)
verwenden, so müssen Sie die
Klassendefinition in einer
entsprechenden Header-Datei
ablegen. Es hat sich in der
Praxis als sinnvoll erwiesen,
der Header-Datei den gleichen
Namen wie der in ihr
definierten Klasse zu geben.
Innerhalb der
Klassendefinition werden die
Memberfunktionen nur
deklariert.
Die Definitionen der
Memberfunktionen erfolgen
dann in einer getrennten
Quellcode-Datei, die ebenfalls
sinnvoller Weise den Namen
den Namen der Klasse erhält,
deren Memberfunktionen in
ihr definiert werden; nur
diesmal selbstverständlich mit
der Extension .cpp anstelle von
.h. Am Anfang der Datei wird
dann zuerst die Header-Datei
mit der Klassendefinition
eingebunden und danach
folgen die entsprechenden
MemberfunktionenDefinitionen.
Klassendefinition
Datei: window.h
class Window
{
int xPos, yPos;
unsigned int width, height;
void Move(int x, int y);
void Size(unsigned int w,
unsigned int h);
};
//
//
//
//
//
Position
Groesse
Verschieben
Groesse
veraendern
Definition von Memberfunktionen
Datei window.cpp
// Einbinden der Header-Datei
#include "window.h"
// Definition der Move(...) Memberfunktion
void Window::Move(int x, int y)
{
...
// Fenster verschieben
}
// Definition der Size(...) Memberfunktion
void Window::Size(unsigned int w,
unsigned int h)
{
...
// Groesse veraendern
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (8 von 35) [17.05.2005 11:38:56]
Klassen
Diese Aufteilung hat noch einen weiteren Vorteil: wollen Sie eine Klasse weitergeben, so reicht es
aus, wenn Sie die Header-Datei und die übersetzte(!) Quellcode-Datei (also die .obj Datei)
weitergeben. Der Anwender Ihrer Klasse muss bei der Verwendung der Klasse lediglich die HeaderDatei einbinden und zu seinem gesamten Projekt noch die entsprechende .obj Datei dazulinken. Das
heißt, Ihr Quellcode bleibt so geschützt.
Zugriffsrechte in Klassen
Nachdem die Klasse und deren Memberfunktionen definiert sind, könnten wir versuchen, auf die
Elemente der Klasse zu zugreifen. Diese würde aber im Augenblick noch einen Fehler beim
Übersetzen des Programms verursachen. Der Grund hierfür liegt darin, dass alle Klassen
bestimmte Standard-Zugriffsrechte besitzen die den Zugriff auf Ihre Member einschränken
können. Das heißt, je nach Zugriffsrecht kann der Zugriff auf ein Member von außerhalb der
Klasse verboten oder erlaubt sein. Der Sinn und Zweck dieser Zugriffsbeschränkung liegt darin,
dass der Zugriff auf die Member nur noch kontrolliert erfolgen sollte. Stellen Sie sich dazu wieder
einmal unsere bisherige Klasse Window vor. Diese Klasse enthält unter anderem die Breite und
Höhe des Fensters. Ohne Zugriffskontrolle könnte der Anwender bis jetzt einfach hergehen und
zum Beispiel die Fensterhöhe auf den (falschen) Wert -1 setzen, was dann wahrscheinlich ziemliche
Probleme beim Darstellen des Fensters geben würde. Wird der Zugriff auf die Fenstergröße aber
kontrolliert, so kann beim Setzen der Fenstergröße eine Plausibilitätsprüfung durchgeführt werden
und falsche Werte abgewiesen werden.
Sehen wir uns jetzt an, welche Zugriffsrechte es gibt und wie sie vergeben werden.
Um den Zugriff auf Member
class Window
von außerhalb der Klasse zu
{
sperren, wird innerhalb der
private:
Klasse vor die zu sperrenden
int xPos, yPos;
Member die Anweisung
unsigned int width, height;
private: gestellt. Alle Member
void Move(int x, int y);
die nach dieser Anweisung
void Size(unsigned int w,
folgen sind für den Zugriff
unsigned int h);
zunächst einmal gesperrt. Diese };
Sperrung gilt aber nur für
Zugriffe von außerhalb der
Klasse. Memberfunktionen
einer Klasse haben generell
immer Zugriff auf alle anderen
Member der eigenen Klasse.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (9 von 35) [17.05.2005 11:38:56]
//
//
//
//
//
Position
Groesse
Verschieben
Groesse
veraendern
Klassen
Beachten Sie den Doppelpunkt nach der Angabe des Zugriffsrechts!
Da eine Klasse mit nur
class Window
gesperrten Member aber in der {
Regel nutzlos ist, muss diese
private:
Sperrung auch wieder
int xPos, yPos;
aufgehoben werden können.
unsigned int width, height;
Dies erfolgt durch die
public:
Anweisung public:. Auf alle
void Move(int x, int y);
Member die nach dieser
void Size(unsigned int w,
Anweisung folgen kann dann
unsigned int h);
auch von außerhalb der Klasse, };
also zum Beispiel von main(...)
heraus, zugegriffen werden. Im
Beispiel rechts sind damit die
Eigenschaften der Klasse
Window gegen den direkten
Zugriff geschützt, während auf
die Memberfunktionen
zugegriffen werden kann.
// Position
// Groesse
// Verschieben
// Groesse
// veraendern
Und damit haben wir unseren kontrollierten Zugriff auf die Fenstereigenschaften! Soll zum Beispiel
die Fenstergröße verändert werden, muss dazu die Memberfunktion Size(...) aufgerufen werden,
width und height sind ja gegen den direkten Zugriff geschützt. Und innerhalb von Size(...) kann
dann eine Überprüfung der übergebenen Parameter auf Zulässigkeit erfolgen.
Wie Sie aus den vorherigen Ausführungen entnehmen können, sind Zugriffsspezifizierer innerhalb
einer Klasse so lange gültig, bis sie durch einen anderen 'überschrieben' werden. Die Anzahl und
Reihenfolge der private: und public: Anweisungen innerhalb einer Klasse ist laut C++ Standard
beliebig.
In der Praxis hat es sich als sinnvoll erwiesen, die Eigenschaften einer Klasse soweit wie
möglich innerhalb eines private-Bereichs unterzubringen, um so den Zugriff darauf über die
Schnittstelle kontrollieren zu können. Versuchen Sie ferner eine gewisse Struktur in den
Klassenaufbau zu bringen. Geben Sie z.B. zuerst alle public Eigenschaften, dann alle public
Memberfunktionen, dann alle private Eigenschaften und zum Schluss die noch fehlenden
private Memberfunktionen an.
Standard-Zugriffsrechte
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (10 von 35) [17.05.2005 11:38:56]
Klassen
Alle Member eines Klassentyps class Window
(struct, union und class) haben {
voreingestellte Zugriffsrechte:
// standardmäßig ist alles private
int xPos, yPos;
// Position
● struct-Member:
unsigned int width, height; // Groesse
Voreingestellt ist das
public:
Zugriffsrecht public.
void Move(int x, int y);
// Verschieben
Dieses Zugriffsrecht
void Size(unsigned int w,
// Groesse
kann geändert werden.
unsigned int h); // veraendern
● class-Member:
};
Voreingestellt ist das
Zugriffsrecht private.
struct Window
Das Zugriffsrecht kann {
geändert werden.
// standardmäßig ist alle public
void Move(int x, int y);
// Verschieben
Die beiden rechts angegebenen
void Size(unsigned int w,
// Groesse
Klassen sind damit bis auf den
unsigned int h); // veraendern
Klassentyp identisch. Der
private:
Unterschied liegt nur in der
int xPos, yPos;
// Position
Reihenfolge der Member. Beim
unsigned int width, height; // Groesse
Klassentyp class müssen Sie die };
Memberfunktionen explizit für
den Zugriff freigeben während
Sie beim Klassentyp struct die
Eigenschaften explizit schützen
müssen. Und dieses
voreingestellte StandardZugriffsrecht ist auch der
einzige Unterschied zwischen
Klassen vom Typ struct und
Klassen vom Typ class!
Um den Klassentyp union
kümmern wir uns, wie bereits
erwähnt, später noch.
Zugriffsrechte zwischen Objekten der gleichen Klasse
War's bis jetzt mehr oder weniger leicht, so kommt nun der etwas komplizierter Teil beim
Zugriffsrecht. Sehen Sie sich dazu zunächst einmal folgende Aussage an:
Wird einer Memberfunktion ein Objekt der gleichen Klasse übergeben, so kann die
Memberfunktion auch auf die geschützten Eigenschaften des übergegebenen Objekts
zugreifen.
Sehen wir uns diesen Sachverhalt einmal an. Er spielt bei dem später beschriebenen
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (11 von 35) [17.05.2005 11:38:56]
Klassen
Kopierkonstruktor eine entscheidende Rolle.
Das Beispiel rechts zeigt die
bekannte Klasse Window.
Dieser Klasse wurde die
Memberfunktion CopyData(...)
hinzugefügt, die die
Eigenschaften eines
übergebenen Objekts in das
aktuelle Objekt übernehmen
soll. Hierzu erhält die
Memberfunktion eine Referenz
auf das zu kopierende Objekt,
das natürlich ebenfalls der
Klasse Window angehört.
Obwohl nun die Eigenschaften
des übergebenen Objekts gegen
den direkten Zugriff geschützt
sind (private), kann die
Memberfunktion CopyData(...)
auf die geschützten Daten des
übergebenen Objekts
zugreifen. Wie gesagt, dies
funktioniert aber nur, wenn
das übergebene Objekt der
gleichen Klasse angehört wie
die aufgerufene
Memberfunktion.
class Window
{
// Eigenschaften
private:
// Ab hier alles geschützt
short width, height;
string title;
// Memberfunktionen
public:
// Ab hier alles zugänglich
void Size(short w, short h);
void SetTitle(const char *const pT);
void CopyData(const Win& source);
.... // weitere Member der Klasse
};
// Definition der Memberfunktion CopyData()
void Window::CopyData(const Window& source)
{
width = source.width;
height = source.height;
title = source.title;
}
So, und das war's vorläufig auch schon zu den Zugriffsrechten. Später werden Sie noch ein weiteres
Zugriffsrecht kennen lernen, das aber nur im Zusammenhang mit abgeleiteten Klasse eine Rolle
spielt.
Objekte von Klassen mit nur public-Eigenschaften lassen sich bei der Definition eines
Objekts auch gleich iniitialisieren. Wir werden aber im weiteren Verlaufe des Kurses
später ein anderes Verfahren zur Initialisierung von Objekten kennen lernen. Wenn Sie
trotzdem mehr über diese Initialisierungsart erfahren wollen, klicken Sie links das Symbol
an.
Zugriff auf Klassenmember
Zugriff aus Memberfunktionen heraus
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (12 von 35) [17.05.2005 11:38:56]
Klassen
Innerhalb einer
Memberfunktion kann auf alle
Member (egal ob public oder
private) der eigenen Klasse
direkt zugegriffen werden.
Hierbei spielt es keine Rolle, ob
der Zugriff auf Eigenschaften
erfolgt oder aber weitere
Memberfunktionen der eigenen
Klasse aufgerufen werden
sollen. So könnte zum Beispiel
eine weitere Memberfunktion
WinPos(...) der Klasse Window
zum Verändern der Position
und Größe eines Fensters wie
folgt aussehen:
void
Window::WinPos(int
x, int y,
// Klassendefinition
class Window
{
private:
int xPos, yPos;
// Position
unsigned int width, height; // Groesse
public:
void Move(int x, int y);
// Verschieben
void Size(unsigned int w,
// Groesse
unsigned int h); // veraendern
};
// Definition von Memberfunktionen
void Window::Move(int x, int y)
{
xPos = x; yPos = y;
// Position setzen
}
void Window::Size(unsigned int w,
unsigned int h)
{
width = w; height = h; // Groesse setzen
}
unsigned int w,
unsigned int h)
{
Move(x, y);
Size(w, h);
}
Die Wiederverwendung von
bereits bestehenden Code, hier
der Memberfunktionen
Move(...) und Size(...), ist
übrigens auch eines der Ziele
der OOP. Im neu-deutschen
heißt dies auch Code-Reuse.
Sehen wir uns jetzt an, wie auf die Member einer Klasse außerhalb von Memberfunktionen
zugegriffen wird. Denken Sie aber immer daran, dass Sie außerhalb von Memberfunktionen nur
Zugriff auf die public-Member einer Klasse haben!
Direkter Zugriff von außerhalb
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (13 von 35) [17.05.2005 11:38:56]
Klassen
Auf Member einer Klasse kann
in der Regel nur über ein
Objekt der Klasse direkt
zugegriffen werden. Denn
letztendlich existieren die
Member einer Klasse nur in
Verbindung mit einem
bestimmten Objekt. Der
direkte Zugriff auf Member
erfolgt durch die Angabe des
entsprechenden Objekts,
gefolgt vom Punktoperator '.'
und dem Namen des
entsprechenden Members.
// Klassendefinition
class Window
{
private:
int xPos, yPos;
// Position
unsigned int width, height; // Groesse
public:
void Move(int x, int y);
// Verschieben
void Size(unsigned int w,
// Groesse
unsigned int h); // veraendern
};
// Objektdefinitionen
Window myWin, yourWin;
// Direkter Zugriff auf public-Member
myWin.Move(10,200);
Im Beispiel rechts werden zwei yourWin.Size(640,480);
Objekte myWin und yourWin
definiert. Anschließend werden
die Positions-Eigenschaften von
myWin durch durch den Aufruf
von Move(...) geändert. Diese
Änderung der PositionsEigenschaft von myWin hat
aber keine Auswirkung auf die
Positions-Eigenschaft von
yourWin. Beide Objekte
besitzen zwar die gleichen
Eigenschaften, die aber völlig
unabhängig voneinander sind.
Im Anschluss daran wird die
Größen-Eigenschaft von
yourWin durch den Aufruf von
Size(...) verändert.
Selbstverständlich könnte zum Beispiel auch die Eigenschaft xPos verändert werden, wenn
xPos als public-Eigenschaft definiert wäre. Die entsprechende Anweisung dazu könnte dann
wie folgt aussehen:
myWin.xPos = ANYVAL;
Da es in der OOP aber fast ein Vergehen ist, Eigenschaften direkt zu verändern, sollten Sie
zur Veränderung von Eigenschaften auch immer entsprechende Memberfunktionen zur
Verfügung stellen. Sie erhalten dadurch eine wesentlich bessere Kontrolle über die Werte
der Eigenschaften.
Indirekter Zugriff von außerhalb
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (14 von 35) [17.05.2005 11:38:56]
Klassen
Der indirekten Zugriff, das
heißt über einen Zeiger, auf ein
Member erfolgt durch Angabe
des Objektzeigers, dann folgt
der Zeigeroperator -> und
anschließend der Name des
Members. Ganz wichtig dabei
ist, dass dem Zeiger vor dem
Zugriff auch eine gültige
Adresse, zum Beispiel die eines
bestehenden Objekts,
zugewiesen wurde. Dieser
Punkt mag auf den ersten Blick
zwar trivial erscheinen, ist aber
in der Praxis ein häufiger
Fehlerfall.
// Klassendefinition
class Window
{
private:
int xPos, yPos;
// Position
unsigned int width, height; // Groesse
public:
void Move(int x, int y);
// Verschieben
void Size(unsigned int w,
// Groesse
unsigned int h); // veraendern
};
// Objektdefinitionen
Window myWin, yourWin;
// Zeigerdefinition und Initialisierung
Window *pWin = &myWin;
// Indirekter Zugriff auf myWin-Member
pWin->Move(10,200);
Einem einmal definierten
// Zeiger umsetzen auf weiteres Objekt
Objektzeiger können im
pWin = &yourWin
weiteren Verlaufe des
// Indirekter Zugriff auf yourWin-Member
Programms beliebig oft weitere pWin->Move(100,0);
Adressen zugewiesen werden.
Es wird dann immer das
Member des Objekts
aufgerufen, dessen Adresse
gerade im Objektzeiger
abgelegt ist. So wird im Beispiel
rechts zunächst das Objekt
myWin und dann das Objekt
yourWin indirekt über den
Objektzeiger pWin verschoben.
Zugriff auf enum-Eigenschaften
Im Zusammenhang mit dem
Zugriff auf Eigenschaften
müssen wir uns nochmals kurz
mit dem enum-Datentyp
befassen. Werden innerhalb
einer Klasse enumEigenschaften definiert, so
gehören diese natürlich zur
Klasse. Wollen Sie eine enumKonstante auch außerhalb
einer Memberfunktionen
verwenden (z.B. als Parameter
beim Aufruf von
// enum-Member definieren
class Window
{
public:
// enum-Datentyp definieren
enum Style {FRAME, CLOSEBOX, SYSMENU};
private:
// enum-Eigenschaft definieren
enum Style winStyle;
....
public:
void DoAnything(Style s);
} myWin;
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (15 von 35) [17.05.2005 11:38:56]
Klassen
Memberfunktionen), so müssen // Aufruf einer Memberfunktion
Sie vor dem Namen der enum- myWin.DoAnything(Window::FRAME);
Konstante noch den
Klassennamen und den
Zugriffsoperator :: angegeben.
Auch muss der enum-Datentyp
als public deklariert sein, da Sie
ansonsten keinen Zugriff
darauf haben. Beachten Sie
dabei aber bitte, dass nur der
enum-Datentyp und nicht auch
die enum-Eigenschaft als public
deklariert ist.
Ferner sollten Sie auch immer an die Fehlerfalle beim Aufruf von Funktionen/Memberfunktionen
denken, wenn mehrere enum-Konstanten z.B. verodert werden. Hier können Sie sich diese
Fehlerfalle nochmals ansehen ( ).
const-Memberfunktionen und mutable-Member
Im Zusammenhang mit Memberfunktionen wollen wir uns jetzt noch eine besondere Art von
Memberfunktionen ansehen. Vielfach werden Sie Deklarationen von Memberfunktionen in der
folgenden Art finden:
<Returntyp> MName ([Parameter]) const;
Worauf es hierbei ankommt ist
das Schlüsselwort const am
Ende der Deklaration.
Memberfunktionen mit dieser
Deklaration werden auch als
const-Memberfunktionen
bezeichnet. constMemberfunktionen können
keine Eigenschaften des
Objektes verändern. Rechts
sehen Sie ein kleines Beispiel
für die Anwendung einer
solchen const-Memberfunktion.
class Window
{
int xPos, yPos;
...
public:
int GetXPos() const;
... // weitere Member der Klasse
};
int Window::GetXPos() const
{
return xPos;
}
... // Hier stehen die Memberfunktionen-Def.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (16 von 35) [17.05.2005 11:38:56]
Klassen
Die Memberfunktion
GetXPos(...) liefert die XPosition des Fensters zurück
und verändert selbst keine
Eigenschaft.
Beachten Sie bitte,
dass sowohl bei der
Deklaration wie auch
bei der Definition der
Memberfunktion
jeweils const
angegeben werden
muss.
int main()
{
Window myWin;
...
int x = myWin.GetXPos();
...
}
Da laut obiger Definition der Aufruf von const-Memberfunktionen keine Eigenschaften
verändern darf, dürfen const-Memberfunktionen auch wiederum nur weitere constMemberfunktionen aufrufen. Der Aufruf von nicht-const-Memberfunktionen ist nicht
erlaubt!
Im Zusammenhang mit constMemberfunktionen muss noch
das Schlüsselwort mutable
erwähnt werden. Mit mutable
definierte Eigenschaften
können auch in constMemberfunktionen verändert
werden, d.h. mutable
überschreibt sozusagen das
const-Attribut der
Memberfunktion für
bestimmte Eigenschaften.
Außerdem erlaubt mutable
selbst dann noch die
Veränderung einer
Eigenschaft, wenn von der
Klasse ein const-Objekt
definiert wurde.
class Window
{
int xPos, yPos;
public:
mutable long any;
void DoSomething() const;
... // weitere Member der Klasse
};
void Window::DoSomething() const
{
xPos = 10; // nicht erlaubt wegen const
any = 10L; // das geht wegen mutable
}
... // Hier stehen die Memberfunktionen-Def.
int main()
{
...
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (17 von 35) [17.05.2005 11:38:56]
Klassen
mutable kann nicht mit den Speicherklassen const und static kombiniert werden, d.h. die
folgenden Eigenschaften sind nicht zulässig:
mutable const int MAX=10;
mutable static short statVar;
Auf die Speicherklasse static im Zusammenhang mit Klassen kommen wir später noch zu
sprechen.
Aufruf von Funktionen aus Memberfunktionen
Wollen Sie von Memberfunktionen heraus 'normale' Funktionen aufrufen, so können dabei drei
Fälle auftreten:
Innerhalb der Klasse
gibt es kein Member mit
dem gleichen Namen
wie die aufzurufende
Funktion. Dann erfolgt
der Aufruf der Funktion
wie gewohnt, d.h. es
reicht die alleinige
Angabe des
Funktionsnamens.
// Funktionsdeklaration
bool CheckIt();
class Any
{
... // Enthält keine Memberfunktion
CheckIt()
void DoAny()
{
...
bool result = CheckIt();
}
};
Innerhalb der Klasse
gibt es eine
Memberfunktion mit
dem gleichen Namen
wie eine Funktion aus
einer der C++ StandardBibliotheken.
Standardmäßig wird
hier beim Aufruf immer
die Memberfunktion der
eigenen Klasse
aufgerufen. Um die
Funktion aus der C++
Standard-Bibliothek
aufzurufen, stellen Sie
vor dem Aufruf einfach
den Namen des
#include <cmath> // Bindet sin(...) ein
class Any
{
double sin(double x)
{
...
}
void DoAny()
{
...
// Aufruf der eigenen sin(..) Memberfkt.
double res1 = sin(...);
// Aufruf der Fkt aus cmath
double res2 = std::sin(...);
}
};
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (18 von 35) [17.05.2005 11:38:56]
Klassen
Namensraums std,
gefolgt von zwei
Doppelpunkten.
Innerhalb der Klasse
gibt es eine
Memberfunktion mit
dem gleichen Namen wie
eine globale 'normale'
Funktion, die aber in
keinem eigenen
Namensraum liegt.
Auch hier wird
standardmäßig wieder
zuerst die
Memberfunktion der
eigenen Klasse
aufgerufen. Um die
globale Funktion
aufzurufen, stellen Sie
vor dem
Funktionsnamen den
globalen
Zugriffsoperator :: (das
sind zwei
Doppelpunkte).
// Deklaration der globalen Funktion
bool CheckIt();
class any
{
bool CheckIt()
{
...
}
void DoAny()
{
...
// Aufruf der CheckIt(...)
Memberfunktion
bool res1 = CheckIt();
// Aufruf der globalen Funktion
bool res2 = ::CheckIt();
}
};
Wie sich das ganze Spiel mit globalen Variablen anstelle von Funktionen verhält, das
können Sie sich ansehen wenn Sie das Symbol links anklicken.
Kopieren von Objekten
Verlassen wird jetzt die Definition von Objekten und Memberfunktionen und sehen uns einmal an,
wie Objekte kopiert werden. In der Praxis ist es oft notwendig die Daten von einem Objekt in ein
anderes zu übertragen, wobei beide Objekte der gleichen Klasse angehören. Im ersten Ansatz
könnten Sie jetzt hergehen und eine Memberfunktion, z.B. CopyObject(...), schreiben, die
Eigenschaft für Eigenschaft über Zuweisungen umkopiert.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (19 von 35) [17.05.2005 11:38:56]
Klassen
Doch es geht auch wesentlich
einfacher wenn beide Objekte
der gleichen Klasse angehören:
weisen Sie die Objekte einfach
einander zu. Durch die
Zuweisungen werden alle
Eigenschaften kopiert, egal wie
viele es sind. Ganz schön clever
vom Compiler, oder? Nur eines
müssen Sie dabei aber
unbedingt beachten:
class Window
{
...
};
int main()
{
Window myWin, yourWin;
...
yourWin = myWin;
...
}
Enthält eine Klasse dynamische Eigenschaften (das sind Eigenschaften die über Zeiger
realisiert sind), so kann dieses Standardverhalten unter Umständen zu fatalen Fehlern
führen. Mehr zu Zeigern und dynamischen Eigenschaften aber später im Kurs.
Objekte als Parameter
Parametertyp
Werden Objekte an
Funktionen oder
Memberfunktionen übergeben,
so sollte dies in der Regel über
Referenzparameter erfolgen.
Eine Übergabe per called-byvalue sollte soweit wie möglich
vermieden werden. Übergeben
Sie ein Objekt per called-byvalue, so erhält die Funktion,
wie auch bei den einfachen
Daten, nur eine entsprechende
Kopie des Objekts, d.h. alle
Eigenschaften des zu
übergebenden Objekts werden
in ein temporäres Objekt
umkopiert und dieses dann an
die Funktion übergeben.. Dies
kann je nach Objektgröße
erheblich Zeit und Platz auf
dem Stack benötigen. Bei einer
Übergabe per Referenz entfällt
dieser Kopiervorgang.
class Window
{
...
};
// Funktionsdeklaration
void DoAny (Window& obj);
// Objekt definieren
Window myWin;
int main()
{
...
// Aufruf der Funktion
DoAny(myWin);
...
}
// Funktionsdefinition
void DoAny (Window& obj)
{
...
obj.Size (640,480); // Uebergebenes
obj.Move (0,0);
// Fenster verändern
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (20 von 35) [17.05.2005 11:38:56]
Klassen
Innerhalb der aufgerufenen
Funktion bzw.
Memberfunktion haben Sie
dann über den
Parameternamen Zugriff auf
alle public-Member des
übergebenen Objekts. Gehört
aber die aufgerufene
Memberfunktion zur gleiche
Klasse wie das übergebene
Objekt, so hat die
Memberfunktion Zugriff auf
alle Member, auch auf die
private Member, des
übergebenen Objekts (siehe
nächsten Abschnitt).
Den Nachteil den eine
Übergabe eines Objekts als
Referenzparameter mit sich
bringt ist, dass die Funktion
oder Memberfunktion das
übergebene Objekt eventuell
unbeabsichtigt verändern
kann. In vielen Fällen kann es
aber durchaus sinnvoll sein,
dass eine Funktion bzw.
Memberfunktion das
übergebene Objekt nicht
verändern können soll. In
diesem Fall übergeben Sie das
Objekt als konstante Referenz,
so wie nebenstehend
dargestellt. Ein Versuch, das
Objekt dann innerhalb der
Funktion/Memberfunktion zu
verändern, würde zu einem
Fehler beim Übersetzen des
Programms führen. Dies gilt
auch, wenn die aufgerufene
Funktion/Memberfunktion eine
Memberfunktion des
übergebenen Objekts aufruft,
die nicht als constMemberfunktion definiert ist.
Der Aufruf einer nicht-constMemberfunktion würde ja
ansonsten zu einer eventuellen
Veränderung des übergebenen
class Window
{
...
public:
void NonConstMeth(...);
void ConstMeth(...) const;
};
// Normale Funktion
void DoAny (const Window& obj)
{
obj.NonConstMeth(...); // FEHLER!
obj.ConstMeth(...);
// OK!
}
// Objekt definieren
Window myWin;
int main()
{
...
// Aufruf der Funktion
DoAny(myWin);
...
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (21 von 35) [17.05.2005 11:38:56]
Klassen
Objekts führen (siehe weiter
oben ).
Objekte der eigenen Klasse als Parameter
Normalerweise haben Sie von class Window
Funktionen/Memberfunktionen {
aus nur Zugriff auf die publicint xPos, yPos;
Member des übergebenen
...
Objekts. Ein besonderer Fall
public:
ist jedoch der Aufruf einer
void CopyObj(const Window& obj)
Memberfunktion, die als
{
Parameter ein Objekt der
xPos = obj.xPos;
eigenen Klasse erhält. Da die
yPos = obj.yPos;
aufgerufene Memberfunktion
...
zur gleichen Klasse wie das
}
übergebene Objekt gehört,
...
kann innerhalb der
};
Memberfunktion auch auf die
private Eigenschaften des
übergebenen Objekts
zugegriffen werden. Rechts
sehen Sie hierzu ein Beispiel.
Die Memberfunktion
CopyObj(...) der Klasse Window
erhält als Parameter ein Objekt
der eigenen Klasse Window.
Innerhalb von CopyObj(...)
kann nun auch auf die
ansonsten geschützten
Eigenschaften (z.B. xPos und
yPos) des übergebenen Objekts
zugegriffen werden. Diese
Sachverhalt spielt später
nochmals eine wichtige Rolle.
Objekte als Rückgabewert
Wie Sie bereits erfahren haben, lassen sich innerhalb von Funktionen und Memberfunktionen
lokale Variablen definieren. Und selbstverständlich können Sie in Funktionen/Memberfunktionen
auch lokale Objekte definieren. Etwas aufpassen müssen Sie nur, wenn Sie ein Objekte aus einer
Funktion/Memberfunktion heraus zurückgeben. Sehen wir uns dazu einmal ein Beispiel an:
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (22 von 35) [17.05.2005 11:38:56]
Klassen
Die Funktion CreateWin(...) soll
zur Erstellung eines Fenster
dienen. Dazu wird innerhalb
der Funktion zunächst ein
Window-Objekt erzeugt, dass
dann an den Aufrufer
zurückgeliefert wird.
class Window
{
...
};
// Funktion liefert Referenz zurück
// was bis zum Programmabsturz führen kann
Window& CreateWin()
{
Window myWin;
// FALSCH!!!
Sie wissen doch noch,
... // irgend etwas mit Fenster tun
dass lokale Variablen
... // und dann Referenz zurückliefern
(und damit auch lokale
return myWin;
Objekte) nur innerhalb
}
der Funktion
...
existieren?
int main()
{
Am Ende der Funktion wird
...
nun das lokale Window-Objekt
// Fenster erstellen lassen
wieder zerstört. Wenn Sie jetzt
Window newWin = CreateWin();
(wie im nebenstehenden
...
Beispiel) eine Referenz auf
}
dieses lokale Window-Objekt
zurückliefern, so würden Sie
eine Referenz auf ein nicht
mehr existierendes Objekt
zurückgeben. Und das ist nicht
erlaubt! Je nach verwendetem
Compiler erhalten Sie bei einer
solchen Konstruktion entweder
eine Warnung oder (was
eigentlich richtiger ist) einen
Fehler.
Doch wie geht's richtig?
Geben Sie anstelle einer
Referenz einfach das Objekt
selbst zurück. In diesem Fall
führt der Compiler intern
Folgendes durch: Da das
erzeugte Window-Objekt nur
bis zum Ende der Funktion
existiert, aber trotzdem an den
Aufrufer zurückgeliefert
werden muss, wird am Ende
der Funktion zunächst ein
temporäres Window-Objekt
class Window
{
...
};
// Funktion liefert Window Objekt zurück
Window CreateWin()
{
Window myWin;
... // irgend etwas mit Fenster tun
... // und dann zurückliefern
return myWin;
}
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (23 von 35) [17.05.2005 11:38:56]
Klassen
erstellt. Dieses temporäre
Window-Objekt wird dann mit
dem lokale Window-Objekt
initialisiert. Anschließend wird
das lokale Window-Objekt
zerstört. Das zurückgelieferte
temporäre Window-Objekt
wird nach der Rückkehr aus
der Funktion/Memberfunktion
dem Ziel-Window-Objekt (im
Beispiel ist dies newWin)
zugewiesen. Und am Ende der
Anweisung, die die Funktion
aufgerufen hat, wird schließlich
noch das temporäre WindowObjekt zerstört. Siehe sehen
also, der Compiler hat hier
(hinter ihrem Rücken) sehr viel
zu tun.
...
int main()
{
...
// Fenster erstellen lassen
Window newWin = CreateWin();
...
}
Enthält das zu zurückzugebende Objekt dynamische Daten (Zeiger!), so muss die Klasse
des Objekts in der Regel den Kopierkonstruktor und einen überladenen
Zuweisungsoperator besitzen! Mehr dazu nachher gleich.
Da die Entwicklung von Klassen die Grundlage der OOP ist, wollen wir uns jetzt einmal die
Entwicklung einer komplette Klasse ansehen.
Entwicklung einer Klasse
In diesem Beispiel werden wir eine Klasse zur Darstellung und Manipulation eines Rechtecks
entwickeln.
Wie Sie vielleicht noch wissen, sollten bei der Entwicklung einer Klasse zuerst deren Eigenschaften
(Daten) definiert werden. Sind alle Eigenschaften bekannt, so ergibt sich daraus fast zwangsläufig
die Schnittstelle (Memberfunktionen) der Klasse. Fangen wir also mit den Eigenschaften an, die die
Klasse Rect zur Darstellung eines Rechtecks besitzt. Wir werden die Eigenschaften gleich in einer
eigenen Header-Datei definieren, so wie es in der Praxis üblich ist.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (24 von 35) [17.05.2005 11:38:56]
Klassen
Unser darzustellendes
Rechteck soll die die
Eigenschaften Position, Größe
und Farbe besitzen. Definieren
wir also zunächst die Klasse
und fügen ihr dann die
Eigenschaften hinzu. Da wir
vielleicht später noch andere
Klassen definieren die ebenfalls
eine Farbinformation
enthalten, wird die
Farbinformation in einer
getrennten einfachen Klasse
ohne eigene Memberfunktionen
abgelegt. Die Farb-Klasse
enthält die jeweiligen Rot-,
Grün- und Blauanteile der
Farbe als unsigned char Werte.
Für die Eigenschaften Position
und Größe werden shortDatentypen verwendet.
Sind die Eigenschaften
definiert, kann es an das
Definieren der Schnittstelle
gehen. Beachten Sie bitte, dass
die Eigenschaften private sind
und die nachfolgenden
Memberfunktionen public, so
wie es sich in der OOP gehört.
Definition der Klasse Rect
Datei: rect.h
// Für verkürzte Schreibweise
typedef unsigned char BYTE;
// Struktur für Farbwerte
struct Color
{
BYTE red;
BYTE green;
BYTE blue;
};
// Klassendefinition
class Rect
{
short xPos, yPos;
short width, height;
Color rectColor;
public:
void Init(short x, short y,
short w, short h);
void Move(short x, short y);
void Resize(short w, short h);
void SetColor(BYTE r, BYTE g, BYTE b);
void DrawIt();
};
Zuerst benötigen wir eine
Memberfunktion um ein
Rechteck-Objekt mit seinen
'Grundeigenschaften' zu
versorgen. Nennen wir diese
Memberfunktion Init(...). In
unserem Fall erhält die
Memberfunktion Init(...) als
Parameter nur die Position und
Größe des neuen Rechtecks.
Die Farbe soll standardmäßig
bei der Definition eines
Rechteck-Objekts auf schwarz
(RGB = 0,0,0) eingestellt
werden.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (25 von 35) [17.05.2005 11:38:56]
Klassen
In der nächsten Lektion werden Sie ein Verfahren kennen lernen, wie ein Objekt bei seiner
Definition automatisch initialisiert werden kann (Stichwort: Konstruktor).
Als nächstes deklarieren wir die restlichen Memberfunktionen um die Eigenschaften des Rechtecks
gezielt verändern zu können. Da unser Rechteck die drei Eigenschaften Position, Größe und Farbe
besitzt, definieren wir drei entsprechende Memberfunktionen Move(...), Resize(...) und SetColor(...).
Zum Schluss benötigen wir noch eine Memberfunktion um das Rechteck darzustellen. Die hierfür
verwendete Memberfunktion erhält den Namen DrawIt(...).
Nach dem der Aufbau der
Klasse geht's ans Definieren
der Memberfunktionen. Die
Memberfunktionen werden in
einer eigenen Datei rect.cpp
definiert. Damit der Compiler
beim Übersetzen dieser Datei
die Klasse Rect auch kennt,
müssen Sie am Anfang die
vorhin erstellte Header-Datei
rect.h einbinden.
Der Aufbau der
Memberfunktionen dürfte aus
ihrer Funktion hervorgehen.
Lediglich die Memberfunktion
DrawIt(...) wurde etwas
vereinfacht, da wir hier keine
Grafikprogrammierung
betreiben wollen. Sie gibt nur
die Eigenschaften des
Rechtecks aus.
Im Beispiel rechts wurden
eventuelle
Plausibilitätsabfragen der
Parameter weggelassen, damit
das Beispiel noch einigermaßen
überschaubar bleibt.
Anschließend ist diese Datei
noch so zu übersetzen, dass nur
eine obj-Datei oder eine libDatei erzeugt wird. Sie können
diese Datei nicht mit dem
Definition der Memberfunktionen von Rect
Datei: rect.cpp
#include <iostream>
using std::cout;
using std::endl;
// Klassendefinition einbinden
#include "rect.h"
void Rect::Init(short x, short y,
short w, short h)
{
xPos = x; yPos = y;
width = w; height = h;
rectColor.red = 0x00;
rectColor.green = 0x00;
rectColor.blue = 0x00;
}
void Rect::Move(short x, short y)
{
xPos = x; yPos = y;
}
void Rect::Resize(short w, short h)
{
width = w; height = h;
}
void Rect::SetColor(BYTE r, BYTE g,
BYTE b)
{
rectColor.red = r;
rectColor.green = g;
rectColor.blue = b;
}
void Rect::DrawIt()
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (26 von 35) [17.05.2005 11:38:56]
Klassen
'normalen' Compileraufruf
übersetzen, da ansonsten der
Linker nachher versuchen
würde, eine ablauffähige EXEDatei zu erstellen. Und dies
geht hier nicht, da die Datei
wegen des fehlenden main(...)
nicht alleine lauffähig ist. Wie
Sie eine Datei nur übersetzen
können, ohne den Linker
hinterher aufzurufen,
entnehmen Sie bitte der
Beschreibung zu Ihrem
Compiler.
Und damit sind die Arbeiten an
unserer Klasse im Prinzip
beendet. Selbstverständlich
sollte die Klasse dann auch
noch dokumentiert werden
damit Sie eingesetzt werden
kann.
Rechts sehen Sie eine kleine
Anwendung, die den Einsatz
der neuen Klasse Rect
demonstriert. Um Objekte der
neuen Klasse bilden zu können,
müssen Sie zum einen im
Programm die Header-Datei
rect.h einbinden. Und zum
anderen müssen Sie dem
Linker noch mitteilen, dass er
zu Ihrem Programm noch die
obj- bzw. lib-Datei mit dem
Code Rect-Memberfunktion
dazu binden soll.
{
cout << "Position: " << xPos << ','
<< yPos << endl;
cout << "Groesse : " << width << ','
<< height << endl;
cout << std::showbase << std::hex;
cout << "RGB-Wert: "
<< static_cast<int>(rectColor.red) << ','
<< static_cast<int>(rectColor.green) <<
','
<< static_cast<int>(rectColor.blue) <<
endl;
cout << std::dec;
}
Anwendung der Rect-Klasse
// Zuerst Dateien einbinden
#include <iostream>
#include "Rect.h"
// Nun Namensraum auf std setzen
using namespace std;
// Zwei Rechteck definieren
Rect rect1, rect2;
int main()
{
// Beide Rechtecke initialisieren
rect1.Init(10,10,640,480);
rect2.Init(100,50,800,600);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
rect1.DrawIt();
cout << "2. Rechteck:\n";
rect2.DrawIt();
// 1. Rechteck verschieben
rect1.Move(20,20);
// 2. Rechteck vergroessern und
// Farbe abaendern
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (27 von 35) [17.05.2005 11:38:56]
Klassen
rect2.Resize(1024,786);
rect2.SetColor(0xC0, 0xC0, 0xC0);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
rect1.DrawIt();
cout << "2. Rechteck:\n";
rect2.DrawIt();
}
Wenn Sie dieses Beispiel mit dem MinGW- oder BORLAND Compiler nachvollziehen wollen, so
können Sie dies nicht direkt aus dem Editor heraus. Sie müssen dazu ein DOS-Fenster (KonsolenFenster) öffnen und dann in das Verzeichnis wechseln, in dem die Dateien abgelegt sind.
Geben Sie dann bei Verwendung des BORLAND Compilers folgende Zeilen ein:
bcc32 -c rect.cpp
bcc32 test.cpp rect.obj
Bei Verwendung des MinGW-Compilers sind folgende Zeilen einzugeben:
c++ -c rect.cpp
c++ test.cpp rect.o -o test.exe
Die ersten Zeilen übersetzen nur die Datei mit den Definitionen der Memberfunktionen der Klasse
Rect, ohne den Linker zu starten. Als Ergebnis erhalten Sie dann die Datei rect.obj bzw. rect.o. Die
zweite Zeile übersetzt dann zunächst das Testprogramm test.cpp und weist den Linker an,
zusätzlich die Datei rect.obj bzw. rect.o mit zum Programm dazu zu binden.
Unter VC++ müssen Sie wie gewohnt ein Win32 Konsolenprogramm erstellen und einfach beide
cpp-Dateien mit ins Projekt aufnehmen.
Damit wollen wir die Einführung von Klassen und Objekte beenden. Sie werden im weiteren
Verlaufe des Kurses noch genügend Möglichkeiten haben, dies ausführlich zu üben.
Die objektorientierte Programmierung (OOP)
Sehen wir uns nun noch kurz an, was alles eine objekt-orientierte Programmiersprache (OOP) wie
z.B. C++ von einer prozeduralen Programmiersprache wie z.B. C unterscheidet.
Zum einen unterstützt OOP die Kapselung von Member (Encapsulation). Durch die Kapselung
kann ein Klasse bestimmte Eigenschaften und Memberfunktionen für den direkten Zugriff sperren.
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (28 von 35) [17.05.2005 11:38:56]
Klassen
Aber das sollten Sie in der Zwischenzeit ja schon wissen. Damit kann eine Klasse wie eine Art
Blackbox betrachtet werden, die eine genau definierte Schnittstelle (Interface) besitzt. Nur über
diese Schnittstelle kann der Anwender dann mit der Klasse (eigentlich dem Objekt) agieren. Im
vorherigen Beispiel waren z.B. nur die Memberfunktionen Size(...) und Move(...) zugänglich,
während die Eigenschaften width, height, xPos und yPos vor dem Anwender verborgen waren.
Durch die Kapselung der Eigenschaften kann dann der Anwender nicht mehr direkt durch
Manipulation der Eigenschaft width die Fensterbreite unzulässig verändern. Dies kann nur noch
über die Memberfunktion Size(...) erfolgen, die dann natürlich auch entsprechende
Plausibilitätsüberprüfungen durchführen sollte.
Als zweites Merkmal stellt die OOP die Vererbung (Inheritance) zur Verfügung. Durch Vererbung
werden die Schnittstelle und die Eigenschaften einer Klasse (Basisklasse) an eine andere Klasse
(abgeleitete Klasse) übertragen. Diese neue Klasse verfügt dann über alle Member der
ursprünglichen Klasse sowie ihre eigenen Member. Um wieder zum vorherigen Beispiel zurück zu
kommen, könnten Sie die Klasse Window als Basisklasse für eine neue Klasse Button verwenden, da
ein Button (Schaltfläche) auch eine definierte Ausdehnung und Beschriftung besitzt. Zusätzlich
würde die neue Klasse Button noch die Eigenschaft erhalten, dass sie z.B. einen bestimmten Zustand
wie gedrückt oder ausgewählt annehmen kann.
Und als letzte Eigenschaft stellt OOP die Polymorphie (Polymorphism) zur Verfügung.
Polymorphie kennzeichnet die Eigenschaft, dass Memberfunktionen in abgeleiteten Klassen mit
gleichem Namen aber unterschiedlichem Ablauf implementiert werden können. Um diesen etwas
abstrakten Sachverhalt zu veranschaulichen, kehren wir wieder zum Beispiel mit der neuen Klasse
Button zurück. Button besitzt ja unter anderem die Member seiner Basisklasse Window. Sowohl
Window wie auch Button benötigen irgend eine Memberfunktion um sich darstellen zu können.
Mithilfe der Polymorphie kann nun sowohl die Klasse Window wie auch die der Klasse Button eine
Memberfunktion mit dem Namen Draw(...) hierfür verwenden. Wann welche Memberfunktion
aufzurufen ist hängt nur vom Objekttyp ab.
Mehr zu den einzelnen OOP Eigenschaften erfahren Sie aber im Verlaufe des Kurses noch.
So, und nun kommt das Beispiel und dann sind Sie wieder dran.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (29 von 35) [17.05.2005 11:38:56]
Klassen
Das Beispiel:
Es wird eine Klasse zum Abspeichern von komplexen Zahlen entwickelt.
Eine komplexe Zahl ist eine Zahl, die aus zwei Teilen besteht: einem Realanteil und einem
Imaginäranteil. Für beiden Teile werden double-Werte verwendet.
Damit der Anwender die Eigenschaften (Daten = Real- und Imaginäranteil) nicht direkt
verändern kann, sind diese gegen den direkten Zugriff geschützt (private).
Die Klasse enthält u.a. eine Memberfunktion um die komplexe Zahl zu initialisieren ( Init(...) )und
eine Memberfunktion um sie auszugeben (PrintComplex(...), siehe Programmausgabe).
Anschließend werden zwei Memberfunktionen definiert, um komplexe Zahlen addieren und
subtrahieren zu können. Sollen zwei komplexe Zahlen addiert bzw. subtrahiert werden, so
werden jeweils deren Real- und Imaginäranteile getrennt addiert bzw. subtrahiert (siehe auch
Programmausgabe).
Im Hauptprogramm werden zwei Objekte dieser Klasse definiert und initialisiert. Danach wird
die zweite Zahl zur ersten addiert und das Ergebnis auszugeben. Das so erhaltene Ergebnis wird
dann von der zweiten Zahl subtrahiert.
Das unten stehende Beispiel finden Sie auch unter: le09\prog\Bclass
Die Programmausgabe:
1. Komplexe Zahl:
Realteil: 1.1 / Imaginaerteil: 2.2
2. Komplexe Zahl:
Realteil: 3.3 / Imaginaerteil: 4.4
Nach Addition 2. Zahl zur 1. Zahl
Neue 1. Zahl:
Realteil: 4.4 / Imaginaerteil: 6.6
Nach Subtraktion der 1. Zahl von der 2. Zahl
Neue 2. Zahl:
Realteil: -1.1 / Imaginaerteil: -2.2
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (30 von 35) [17.05.2005 11:38:56]
Klassen
Das Programm:
// C++ Kurs
// Beispiel zu Klassen
//
// Dateien einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
// Klassendefinition
class Complex
{
// Geschuetzte Eigenschaften
double
real;
// Real-Anteil
double
imag;
// Imaginaer-Anteil
public:
// Memberfunktionen
void Init(double r, double i);
void AddComplex(const Complex& op);
void SubComplex(const Complex& op);
void PrintComplex() const;
};
// Definition der Memberfunktionen
// Komplexe Zahl setzen
void Complex::Init(double r, double i)
{
real = r;
imag = i;
}
// 2 komplexe Zahlen addieren
void Complex::AddComplex(const Complex& op)
{
real += op.real;
imag += op.imag;
}
// 2 komplexe Zahlen subtrahieren
void Complex::SubComplex(const Complex& op)
{
real -= op.real;
imag -= op.imag;
}
// Komplexe Zahl ausgeben
void Complex::PrintComplex() const
{
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (31 von 35) [17.05.2005 11:38:56]
Klassen
cout << "Realteil: " << real;
cout << " / Imaginaerteil: " << imag << endl;
}
// HAUPTPROGRAMM
int main()
{
// 2 Komplexe Zahlen (Objekte) definieren
Complex Number1, Number2;
// Objekte initialisieren
Number1.Init(1.1, 2.2);
Number2.Init(3.3, 4.4);
// Objekte ausgeben
cout << "1. Komplexe Zahl:\n";
Number1.PrintComplex();
cout << "2. Komplexe Zahl:\n";
Number2.PrintComplex();
// 2. Objekt zum 1. Objekt addieren
Number1.AddComplex(Number2);
// und Ergebnis ausgeben
cout << "\nNach Addition 2. Zahl zur 1. Zahl\n";
cout << "Neue 1. Zahl:\n";
Number1.PrintComplex();
// 1. Objekt vom 2. Objekt subtrahieren
Number2.SubComplex(Number1);
// und Ergebnis ausgeben
cout << "\nNach Subtraktion der 1. Zahl von der 2. Zahl\n";
cout << "Neue 2. Zahl:\n";
Number2.PrintComplex();
}
Die Übung:
Realisieren Sie eine Klasse zur Klassifizierung von Daten. Dazu wird der Gesamtwertebereich
der Daten in kleinere Wertebereiche (Histogrammklassen) unterteilt und die Häufigkeit des
Auftretens der Daten innerhalb einer solchen Klasse gezählt.
Beispiel: Wertebereich
: 0...99
Anzahl der Klassen: 20
daraus folgt: Wertebereich
Wertebereich
Wertebereich
...
Wertebereich
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (32 von 35) [17.05.2005 11:38:56]
der 1. Klasse: 0...4
der 2. Klasse: 5...9
der 3. Klasse: 10...14
der 20. Klasse: 95...99
Klassen
Damit die Übung überschaubar bleibt, sollen maximal 20 Werte klassifiziert werden und die
Anzahl der möglichen Klassen auf 10 beschränkt sein.
Die zu klassifizierenden Daten werden über eine Memberfunktion an die Klasse übergeben, die
die Werte zunächst nur abspeichert. Beachten Sie bitte, dass maximal 20 Werte klassifiziert
werden können.
Des weiteren soll die Klasse eine Memberfunktion enthalten, die alle in ihr abgelegten Werte
wieder ausgibt. Dies dient später zur Kontrolle der Funktionsfähigkeit der Klasse.
Der Wertebereich einer Histogrammklasse soll über eine Memberfunktion einstellbar sein.
Standardmäßig soll jede Histogrammklasse einen Wertebereich von 10 besitzen, d.h. die erste
Histogrammklasse zählt die Werte 0...9, die zweite 10..19 usw.. Da maximal 10 Klassen möglich
sind, enthält die letzte Klasse standardmäßig alle Werte von 90...99. Über diesen Bereich
hinausgehende Werte sollen gesondert gezählt werden.
Die Ausgabe der Verteilung der Werte auf die einzelnen Klassen soll ebenfalls über eine
Memberfunktion erfolgen. Beachten Sie bitte, dass Sie erst hier die Verteilung der Werte auf die
einzelnen Klassen vornehmen dürfen, da sich der Wertebereich der einzelnen
Histogrammklassen zur Laufzeit beliebig einstellen lässt.
Fügen Sie im Hauptprogramm zuerst die max. Anzahl der möglichen Werte zum Histogramm
hinzu und geben Sie zur Kontrolle diese Werte aus.
Geben Sie dann die Aufteilung der Werte mit der standardmäßigen Klassenteilung 10 aus.
Anschließend stellen Sie den Wertebereich für die einzelnen Histogrammklassen auf 5 um und
geben erneut die Aufteilung aus.
PS: Diese Übung sollten Sie prinzipiell schon kennen, Sie entspricht von der Aufgabenstellung
dem Beispiel in der Lektion Felder und Strings.
Die Programmausgabe:
Werte:
72,9,62,49,28,55,91,1,17,10,76,47,63,32,75,38,75,79,59,72,
Klassenteilung: 10
0...9: 2
10...19: 2
20...29: 1
30...39: 2
40...49: 2
50...59: 2
60...69: 2
70...79: 6
80...89: 0
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (33 von 35) [17.05.2005 11:38:56]
Klassen
90...99: 1
>=100: 0
Klassenteilung: 5
0...4: 1
5...9: 1
10...14: 1
15...19: 1
20...24: 0
25...29: 1
30...34: 1
35...39: 1
40...44: 0
45...49: 2
>=50: 11
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le09\lclass.
Da die Handhabung der Klassen ein wichtiger Punkt ist, hier gleich noch eine Übung:
Die Übung:
Es ist eine Klasse für die Implementierung eines Stack zu schreiben. Ein Stack ist ein Bereich in
dem temporär Werte zwischengespeichert werden. Auf diesem Stack können Werte abgelegt und
dann später ausgelesen werden. Hierbei gilt, dass der zuletzt abgelegte Wert als erstes wieder
ausgelesen wird. Im Prinzip können Sie sich ein Stack als eine Art Ablage vorstellen, auf dem Sie
ein Blatt nach dem anderen ablegen. Wenn Sie die Ablage abarbeiten, holen Sie sich immer das
zuoberst liegende Blatt wieder.
Der zu realisierende Stack soll zur Ablage von short-Wert dienen. Damit die Übung zunächst
einfach bleibt, sollen maximal 10 short-Werte auf dem Stack zwischengespeichert werden
können. Verwenden Sie zur Definition der maximalen Anzahl von short-Werten eine globale
Konstante und kein Literal!
Für die Ablage eine short-Zahl soll eine Memberfunktion Push(...) verwendet werden. Diese
Memberfunktion liefert solange true zurück, wie der ihr übergebene Wert abgelegt werden
konnte. Ist der Stack komplett belegt, so muss Push(...) false zurückliefern.
Zum Auslesen der auf dem Stack abgelegten short-Werte soll die Memberfunktion Pop(...)
verwendet werden. Enthält der Stack noch Werte, so soll die Memberfunktion als Returnwert
true zurückliefern und über einen Parameter den abgeholten short-Wert. Ist der Stack leer, liefert
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (34 von 35) [17.05.2005 11:38:56]
Klassen
die Memberfunktion false.
Im Programm ist ein globales Stackobjekt zu definieren. Anschließend ist der Stack solange mit
'Zufallszahlen' zu füllen, bis er voll ist. Die abgelegten Werte sind zur Kontrolle auszugeben.
Nach dem der Stack vollständig gefüllt ist, sind alle Werte wieder vom Stack abzuholen und
erneut auszugeben.
Die Programmausgabe:
Schiebe Werte auf Stack:
72 9 62 49 28 55 91 1 17 10
Lese Werte vom Stack:
10 17 1 91 55 28 49 62 9 72
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le09\lclass1.
Die nächste Lektion beschäftigt sich mit Zeigern auf Member einer Klasse. Diese Lektion ist nur
etwas für diejenigen, die auch noch das Letzte aus C++ herausholen wollen. Memberzeiger werden
im weiteren Verlaufe des Kurses nicht weiter benötigt. Wenn Sie wollen, können Sie durch
anklicken des Symbols ( ) diese Lektion auch überspringen. Dann geht's weiter mit der noch
fehlenden Klasse union.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_01.htm (35 von 35) [17.05.2005 11:38:56]
Konstruktor und Destruktor
Konstruktor und Destruktor
Die Themen:
Konstruktoraufruf
Konstruktordefinition
Aufrufzeitpunkt des Konstruktors
Konstruktorparameter, Initialisiererliste und Klassenkonstanten
Expliziter Konstruktor
Destruktordefinition
Aufrufzeitpunkt des Destruktors
Beispiel und Übung
Konstruktoraufruf
Um die Eigenschaften einer Klasse zu initialisieren, musste bisher immer eine Memberfunktion
(meistens mit dem Name Init(...)) aufgerufen werden. Da die Initialisierung von Eigenschaften
aber eine sehr oft benötigte Funktion ist, stellt C++ hierfür eine besondere Memberfunktion
bereit, den Konstruktor. In der englischsprachigen Literatur (und auch im Kurs) finden Sie für
den Konstruktor auch die Abkürzung ctor. Der Konstruktor ist für alle Klassentypen (union,
struct, class) zugelassen und weist einige Besonderheit auf.
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (1 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Zum einen wird der
Konstruktor automatisch
aufgerufen wenn ein Objekt
definiert wird. Innerhalb
des Konstruktors können
dann die Eigenschaften
entweder per Zuweisung
oder per Initialisiererliste
(wird gleich noch erläutert)
initialisiert werden.
// Klassendefinition
class Win
{
.... // Member der Klasse
.... // hier folgt gleich der ctor
};
// Hauptprogramm
int main ()
{
Win myWin; // Aufruf des ctor!
....
}
Konstruktordefinition
Die zweite Besonderheit
betrifft den Namen des
Konstruktors. Damit der
Konstruktor von anderen
Memberfunktionen
unterschieden werden
kann, besitzt er immer den
gleichen Namen wie die
Klasse selbst. So hat der
Konstruktor für die Klasse
Window rechts ebenfalls den
Namen Window(...).
Innerhalb eines
Konstruktor sind alle C++
Anweisungen erlaubt bis
auf eine Ausnahme: ein
Konstruktor darf kein
neues Objekt seiner eignen
Klasse anlegen. Dies würde
ansonsten zu einer EndlosSchleife führen. Objekte
anderer Klassen dürfen im
Konstruktor jedoch
definiert werden.
// Klassendefinition
class Window
{
.... // Member der Klasse
public:
// Definition des ctor
Window(...)
{
.... // ctor Anweisungen
}
};
Wenn Sie sich rechts die Definition des Konstruktors einmal näher ansehen, so werden Sie eine
weitere Besonderheit bemerken. Der Konstruktor besitzt keinen Rückgabewert, auch nicht
void! Sollte also bei der Ausführung des Konstruktors etwa ein Fehler auftreten, so können Sie
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (2 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
nicht so ohne weiteres einen Fehlerstatus zurückgeben. Später im Kurs werden wir uns aber
zwei verschiedene Verfahren ansehen mit denen Sie feststellen können, ob der Konstruktor
richtig und vollständig ausgeführt werden konnte.
Und nun ein ganz wichtiger Hinweis:
Da der Konstruktor bei der Definition eines Objektes immer automatisch aufgerufen
wird, darf er in der Regel nicht im private-Bereich der Klasse stehen. Sie könnten
ansonsten kein Objekt der Klasse definieren da der Konstruktor nicht aufgerufen
werden kann.
Aufrufzeitpunkt des Konstruktors
Bisher haben Sie erfahren, dass der Konstruktor automatisch bei der Definition eines Objekts
aufgerufen wird. Sehen wir uns nun einmal an, wann genau der Konstruktor für lokale und
globale Objekte ausgeführt wird.
Für lokale Objekte wird
der Konstruktor genau zu
dem Zeitpunkt aufgerufen,
an dem das lokale Objekt
definiert wird. Wird ein
Objekt z.B. wie rechts
dargestellt zwischen zwei
Anweisungen definiert, so
wird der Konstruktor des
Objekts auch zwischen
diesen Anweisungen
ausgeführt. Die Definition
eines Objekts reserviert
nun nicht nur alleine
Speicher für die
Eigenschaften, sondern
kann je nach Umfang des
Konstruktors die
Ausführung von mehr oder
weniger Code zur Folge
haben.
// Klassendefinition
class Window
{
.... // Member der Klasse
public:
// Definition des ctor
Window()
{
.... // ctor Anweisungen
}
};
// Hauptprogramm
int main()
{
....
Window myWin; // ctor ausführen!
....
}
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (3 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Für globale Objekte wird
deren Konstruktor noch
vor dem Eintritt in die
main(...) Funktion
ausgeführt. Nur so ist
gewährleistet, dass alle
globalen Objekte beim
Eintritt in main(...) auch
bereits initialisiert sind.
Sehen Sie sich dazu einmal
die Ausgabe des Beispiels
rechts an.
Beim Testen Ihres
Programms müssen
Sie jedoch
Folgendes beachten:
enthält ein
Konstruktor einen
Fehler, so kann dies
dazu führen, dass
main(...) überhaupt
nicht mehr
aufgerufen wird!
// Klassendefinition
class Window
{
.... // Member der Klasse
public:
// Definition des ctor
Window()
{
cout << "ctor von Window\n";
....
}
};
// Objektdefinition
Window myWin;
// Hauptprogramm
int main()
{
cout << "Beginn main()\n";
....
}
Programmausgabe:
ctor von Window
Beginn main()
Konstruktorparameter, Initialisiererliste und
Klassenkonstanten
Konstruktorparameter
Da der Konstruktor vom
// Klassendefinition
Prinzip her eine normale
class Window
Memberfunktion ist, kann {
er auch Parameter besitzen.
// Eigenschaften
short xPos, yPos;
Benötigt ein Konstruktor
unsigned short width, height;
Parameter, so werden diese
std::string title;
bei der Definition des
public:
Objekts mit angegeben.
// Definition des ctor
Dazu folgt nach dem
Window (unsigned short w,
Objektnamen eine
unsigned short h,
Klammer und innerhalb
const std::string& t)
der Klammer die
{
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (4 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
entsprechenden
Parameterwerte. Im
Beispiel rechts erhält das
erste Fenster die Größe
640x480 und den Titel
Kleines Fenster. Das zweite
Fenster erhält die Größe
800x600 und den Titel
Grosses Fenster.
Eine Möglichkeit, die
Eigenschaften dann zu
initialisieren besteht darin,
den Eigenschaften per
Zuweisung entweder einen
Parameter zu zuweisen
oder aber aber einen fixen
Wert. Im Beispiel rechts
wird die Fensterposition
eines neuen Fenster zum
Beispiel immer auf (0,0)
gesetzt während die
Fenstergröße und der
Fenstertitel aus den
Parametern bestimmt wird.
xPos = yPos = 0;
width = w; height = h;
title = t;
}
};
// Objektdefinition
// Führt zum Aufruf des ctor
Window myWin(640,480,"Kleines Fenster");
Window yourWin(800,600,"Grosses Fenster");
// Hauptprogramm
int main()
{
....
}
Initialisiererliste
Diese Verfahren, die Eigenschaften per Zuweisung zu initialisieren, ist für einfache Datentypen
effizient genug. Enthält aber die Klasse auch Objekte, wie zum Beispiel ein string-Objekt, so
wird die Initialisierung per Zuweisung uneffizient. Der Grund hierfür liegt darin, dass beim
Definieren eines Objekts zunächst das string-Objekt mit einem leeren String initialisiert wird.
Diesem 'leerem' string-Objekt wird dann später, wenn der Konstruktor abgearbeitet wird, per
Zuweisung der endgültige String zugewiesen. Das heißt, es werden bei der Initialisierung von
Objekten im Konstruktor per Zuweisung immer zwei Schritte zur Initialisierung benötigt.
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (5 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Und (fast)
selbstverständlich lassen
sich diese zwei Schritte
unter bestimmten
Umständen
zusammenfassen. Enthält
die Klasse des
eingeschlossenen Objekts
(im Beispiel ist dies das
string-Objekt für den
Fenstertitel) einen
Konstruktor mit
Parametern, so kann das
eingeschlossenen Objekt
per Initialisiererliste gleich
bei seiner Definition
initialisiert werden. Die
Initialisiererliste wird
eingeleitet durch einen
Doppelpunkt nach der
Parameterklammer des
Konstruktors bei der
Konstruktordefinition.
Nach dem Doppelpunkt
werden die zu
initialisierenden
Eigenschaften aufgelistet,
wobei die Initialwerte einer
jeden Eigenschaft in
Klammern angegeben
werden. Diese
Initialisierung per
Initialisiererliste
beschränkt sich aber nicht
nur auf eingeschlossene
Objekte innerhalb einer
Klasse, sondern es können
auch einfache Datentypen
auf diese Weise initialisiert
werden. So wird im Beispiel
rechts zunächst das stringObjekt mit dem an den
Konstruktor übergebenen
Parameter t initialisiert und
anschließend die beiden
einfachen Datentypen width
// Klassendefinition
class Window
{
// Eigenschaften
short xPos, yPos;
unsigned short width, height;
std::string title;
public:
// Definition des ctor
Window (unsigned short w,
unsigned short h,
const std::string& t):
title(t), width(w), height(h)
{
xPos = yPos = 0;
}
};
// Objektdefinition
// Führt zum Aufruf des ctor
Window myWin(640,480,"Kleines Fenster");
Window yourWin(800,600,"Grosses Fenster");
// Hauptprogramm
int main()
{
....
}
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (6 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
und height mit den
Parametern w bzw. h. Der
Einsatz einer
Initialisiererliste schließt
die Initialisierung von
weiteren Eigenschaften per
Zuweisung nicht aus.
Rechts werden die beiden
Eigenschaften xPos uns
yPos weiterhin per
Zuweisung initialisiert.
Aber denken Sie immer daran: falls eine Klasse Objekte enthält, so sollten diese in der
Regel über die Initialisiererliste initialisiert werden.
Initialisiererliste und Klassenkonstanten
Enthält eine Klasse
Konstanten, so müssen
diese per Initialisiererliste
initialisiert werden, da eine
Zuweisung an Konstanten
ja nicht erlaubt ist. Sie
können Klassenkonstanten
entweder mit einem Literal
oder aber einem
Konstruktorparameter
initialisieren. Dadurch ist es
möglich, dass
Klassenkonstanten für
jedes Objekt verschiedene
Werte annehmen können.
So erhält die
Klassenkonstante
CLASS_CONST für das
Objekt myObj den Wert 5
und für das Objekt yourObj
den Wert 10.
class Any
{
const int CLASS_CONST;
public:
Any(int c): CLASS_CONST(c)
{}
};
Any myObj(5);
Any yourObj(10);
Reihenfolge der Initialisierungen bei Initialisiererlisten
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (7 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Wenn Sie eine
Initialisiererliste zur
Initialisierung von
Eigenschaften verwenden,
dann sollten Sie allerdings
die Reihenfolge kennen in
der die Eigenschaften
initialisiert werden. Die
Reihenfolge der
Initialisierung richtet sich
nach der Reihenfolge der
Eigenschaften in der
Definition der Klasse. Bei
der Klasse Window wird
immer zuerst die
Eigenschaft xPos, dann
yPos usw. initialisiert, egal
in welcher Reihenfolge die
Initialisierungen bei der
Definition des Konstruktors
stehen würde.
Zur Veranschaulichung
dieses Sachverhalts einmal
eine kleine Fehlerfalle. Laut
vorheriger Aussage werden
die Eigenschaften ja in der
Reihenfolge initialisiert, in
der sie in der Klasse
angegeben sind. Im Beispiel
rechts also in der
Reihenfolge len und dann
pText. Sehen Sie sich jetzt
aber einmal die Definition
des Konstruktors an. Dort
wird len mit der
Stringlänge des Textes
pText initialisiert. Da aber
pText erst nach len
initialisiert wird, zeigt pText
noch auf einen
undefinierten Bereich und
len erhält damit einen
zufälligen Wert. Durch
Tauschen der beiden
Definitionen würde das
Beispiel richtig arbeiten.
// Klassendefinition
class Window
{
// Eigenschaften
short xPos, yPos;
unsigned short width, height;
std::string title;
....
};
class Any
{
int len;
char *pText;
....
public:
Any(const char *pT);
};
// Konstruktordefinition
Any::Any(const char *pT):
pText(pT),
len(strlen(pText))
{....}
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (8 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Objektfelder
Besitzt eine Klasse einen
Konstruktor mit
Parametern und wollen Sie
von dieser Klasse ein
Objektfeld anlegen, so
müssen Sie die einzelnen
Elemente des Objektfelds
auf eine etwas andere Weise
initialisieren. Wie Sie solche
Objektfelder initialisieren
ist rechts dargestellt. Nach
dem Namen des
Objektfelds folgt der
Zuweisungsoperator und
dann innerhalb eines {...}
Blocks der expliziten
Aufruf des Konstruktors
für jedes einzelne
Feldelement. Beachten Sie
dabei aber, dass die Anzahl
der Konstruktoraufrufe
auch mit der Anzahl der
Feldelemente
übereinstimmt!
// Klassendefinition
class Window
{
// Eigenschaften
short xPos, yPos;
unsigned short width, height;
std::string title;
public:
// Definition des ctor
Window (unsigned short w,
unsigned short h,
const std::string& t)
{
xPos = yPos = 0;
width = w; height = h;
title = t;
}
};
// Objektfeld-Definition
Window winArray[3] =
{ Window(640,480,"Klein"),
Window(800,600,"Mittel"),
Window(1280,1024,"Gross") };
Expliziter Konstruktor
Sehen wir uns in diesem
Zusammenhang noch eine
besondere Form der
Initialisierung an. Besitzt
eine Klasse einen
Konstruktor mit genau
einem Parameter, so kann
die Initialisierung des
Objekts auch bei der
Definition über eine
Zuweisung erfolgen (erstes
Beispiel rechts). Soll diese
Zuweisung bei der
// Klassendefinition
class Any
{
....
public:
Any(int);
};
// Objektdefinitionen
Any first = 1;
// Das wäre erlaubt
Any second(1);
// und das so wie so
Ausschließen der Zuweisung
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (9 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Objektdefinition verhindert
werden, so ist bei der
Deklaration des
Konstruktors das
Schlüsselwort explict dem
Konstruktornamen
voranzustellen (zweites
Beispiel rechts).
Solche Konstruktore mit
nur einem Parameter
verhalten sich prinzipiell
gleich wie
Konvertierungen! Im ersten
Beispiel rechts wird durch
den Konstruktor Any(int)
die
Konvertierungsvorschrift
festgelegt, wie ein int-Wert
in ein Any-Objekt
umgewandelt werden kann.
// Klassendefinition
class Any
{
....
public:
explicit Any(int);
};
// Objektdefinitionen
Any first = 1;
// Das geht nicht mehr!
Any second(1);
// Aber das immer
Beenden wir nun die Betrachtung des Konstruktors und wenden uns seinem Gegenstück zu,
dem Destruktor.
Destruktordefinition
Der Destruktor wird in der englischsprachigen Literatur (und im Kurs) oft auch mit dtor
abgekürzt.
Auch der Destruktor wird,
genauso wie der
Konstruktor, automatisch
aufgerufen, jetzt doch nicht
bei der Definition eines
Objekts sondern bei dessen
Zerstörung.
Damit der Destruktor von
'normalen'
Memberfunktionen
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (10 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
unterschieden werden
kann, besitzt auch er einen
fest vorgegebenen Namen.
Der Destruktor besitzt
ebenfalls den gleichen
Namen wie die Klasse, nur
wird jetzt vor dem Namen
noch das Symbol ~ gesetzt
(Tilde-Symbol, befindet
sich auf der deutschen
Tastatur auf der PlusTaste).
// Klassendefinition
class Window
{
.... // Member der Klasse
Window(...);
// ctor
~Window();
// dtor
};
// Hauptprogramm
int main ()
{
Window myWin;
....
}
Auch er kann niemals einen
Wert zurückliefern und
besitzt im Gegensatz zum
Konstruktor niemals
Parameter.
Der Destruktor darf niemals in einem private-Bereich der Klasse stehen!
Aufrufzeitpunkt des Destruktors
Sehen wir uns auch hier
wieder den Zeitpunkt an,
an dem der Destruktor
aufgerufen wird.
Für lokale Objekte wird
der Destruktor zu dem
Zeitpunkt aufgerufen, an
dem das lokale Objekt
zerstört (gelöscht) wird.
Komplizierter wird die
Sache bei globalen
Objekten. Hier wird der
Destruktor erst nach dem
Verlassen von main(...)
aufgerufen. Das Beispiel
rechts demonstriert beide
Fälle. Sehen Sie sich die
Programmausgaben an!
// Klassendefinition
class Window
{
.... // Member der Klasse
// Definition des ctor
public:
Window(...)
{
cout << "ctor von Window\n";
....
}
~Window()
{
cout << "dtor von Window\n";
}
};
// Objekt-Definition
Window myWin;
// Hauptprogramm
int main()
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (11 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Der Konstruktor des rechts
blau markierten globalen
Window Objekts wird vor
dem Eintritt in main(...)
aufgerufen und dessen
Destruktor nach dem
Verlassen von main(...). Der
Konstruktor des grün
markierten lokalen Window
Objekts wird, wie Sie
bereits erfahren haben, erst
dann aufgerufen, wenn das
Objekt definiert wird. Da
die Objekt-Definition
innerhalb eines Blocks
erfolgt, beschränkt sich die
Gültigkeit des Objekts auch
auf diesen Block. Beim
Verlassen des Blocks wird
das Objekt zerstört und
damit dessen Destruktor
aufgerufen. Beachten Sie
also, dass beim Schließen
eines Blocks nun ebenfalls
Code ausgeführt werden
kann.
{
cout << "Beginn main()\n";
{
Window localWin;
....
}
cout << "Ende main()\n";
}
Programmausgabe:
ctor von Window
Beginn main()
ctor von Window
dtor von Window
Ende main()
dtor von Window
Auf zwei Besonderheiten des Konstruktors bzw. Destruktors soll noch hingewiesen werden:
Von einem Konstruktor und Destruktor kann niemals eine Adresse gebildet werden, im
Gegensatz zu normalen Memberfunktionen.
Ein Objekt das einen Konstruktor oder Destruktor enthält kann nicht als Element einer
Variante (Union) verwendet werden.
Damit sind wir am Ende dieser wichtigen Lektion angelangt und es folgt nun das Beispiel und
dann wieder Ihre Übung.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (12 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Das Beispiel:
Die in der Lektion über Klassen in der Übung entwickelte Klasse Stack erhält nun einen
Konstruktor und Destruktor.
Der Konstruktor initialisiert den so wichtigen Stackindex stackPtr, so dass die
Memberfunktion Init(...) nun entfällt. Dadurch, dass der Konstruktor automatisch aufgerufen
wird, kann es jetzt niemals ein Objekt der Klasse Stack geben, dessen Stackindex nicht
initialisiert ist!
Im Destruktor werden die beim Zerstören des Stackobjekts noch auf dem Stack liegenden
Daten ausgegeben, d.h. der Stack wird sozusagen geleert.
Innerhalb des Hauptprogramms wird ein lokales Stackobjekt definiert, dessen Stack dann
komplett mit der bekannten Memberfunktion Push(...) komplett gefüllt wird.
Danach werden über die Memberfunktion Pop(...) 5 Werte vom Stack ausgelesen.
Wird nun das Programm beendet, so wird das Stackobjekt zerstört und der restliche Inhalt
des Stacks ausgegeben.
Sollten Sie eine Entwicklungsumgebung verwenden, die nach der
Programmausführung das Konsolenfenster automatisch schließt, so kann es passieren,
dass Sie die letzten drei Zeilen der unten dargestellten Programmausgabe nicht (oder
sehr kurz) zu sehen bekommen. In diesem Fall sollten Sie das Programm aus einem
gesonderten Konsolenfenster heraus starten.
Das unten stehende Beispiel finden Sie auch unter: le10\prog\Bctor
Die Programmausgabe:
Stack initialisiert!
Schiebe Werte auf Stack:
72 9 62 49 28 55 91 1 17 10
Lese 5 Werte vom Stack:
10 17 1 91 55
Hole restliche Werte vom Stack:
28 49 62 9 72
Stack geleert!
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (13 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Das Programm:
// C++ Kurs
// Beispiel zu Konstruktor und Destruktor
//
// Zuerst Dateien einbinden
#include <iostream>
#include <cstdlib>
using std::cout;
using std::endl;
const int SSIZE = 10;
// Klassendefinition
class Stack
{
// Die Eigenschaften sind private und damit
// gegen den direkten Zugriff geschuetzt
short
stackPtr;
short
values[SSIZE];
public:
// Die Memberfunktionen muessen public sein sonst
// kann mit der Klasse nicht gearbeitet werden
Stack();
~Stack();
bool Push(short val);
bool Pop(short& val);
};
// Definition der Memberfunktionen
// Konstruktor
Stack::Stack()
{
stackPtr = 0;
cout << "Stack initialisiert!\n";
}
// Destruktor
Stack::~Stack()
{
cout << "Hole restliche Werte vom Stack:\n";
while (stackPtr > 0)
{
stackPtr--;
cout << values[stackPtr] << ' ';
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (14 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
}
cout << "\nStack geleert!" << endl;
}
// Wert auf Stack ablegen
bool Stack::Push(short val)
{
// Falls Stack noch nicht voll ist
if (stackPtr != SSIZE)
{
// Neuen Wert ablegen
values[stackPtr] = val;
stackPtr++;
return true;
}
return false;
}
// Wert vom Stack holen
bool Stack::Pop(short& val)
{
// Falls noch Werte auf dem Stack
if (stackPtr!=0)
{
// Wert vom Stack holen
stackPtr--;
val = values[stackPtr];
return true;
}
return false;
}
// Hauptprogamm
int main()
{
// Stackobjekt definieren
Stack myStack;
// Sonstige lokale Daten definieren
bool retVal;
short val;
// Stack solange fuellen bis er voll ist
cout << "Schiebe Werte auf Stack:\n";
do
{
val = std::rand() % 100;
retVal = myStack.Push(val);
if (retVal)
cout << val << ' ';
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (15 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
} while (retVal);
// 5 Werte vom Stack wieder holen
cout << "\nLese 5 Werte vom Stack:\n";
for (int iIndex=0; iIndex<5; iIndex++)
{
myStack.Pop(val);
cout << val << ' ';
}
cout << endl;
}
Die Übung:
Bei dieser Übung dürfen Sie sich mal so richtig ins Zeug legen.
Es soll ein Klasse zum Abspeichern von Fensterdaten entwickelt werden. Ein Fenster enthält
dabei die bisher bekannten Eigenschaften Größe, Position und Fenstertitel. Hinzu kommt
nun noch eine weitere Eigenschaft, der Fensterstil. Dieser Fensterstil soll durch einen enumDatentyp repräsentiert werden, wobei folgende Fensterstile erlaubt sein sollen:
SYSMENU, MINBOX, MAXBOX und CLOSEBOX
Damit sowohl der Aufruf des Konstruktors wie auch des Destruktors im Ausgabefenster
erscheinen, sollen diese entsprechende Meldungen ausgeben (siehe nachfolgende Ausgabe).
Außer den erwähnten Eigenschaften soll die Klasse noch die Memberfunktionen Paint(...),
MoveWin(...) und ResizeWin(...) erhalten. Die Funktion der Memberfunktionen MoveWin(...)
und ResizeWin(...) ergibt sich aus ihrem Namen. Die Memberfunktion Paint(...) gibt alle
Eigenschaften des Fensters wie nachfolgend dargestellt aus.
Im Hauptprogramm sind dann zwei Fenster zu definieren, wobei das erste Fenster den
Standard-Fensterstil SYSMENU erhalten soll und das zweite Fenster die Stile MINBOX und
SYSMENU. Geben Sie den Fenstern die Titel Fenster1 und Fenster2; die restlichen
Eigenschaften sind beliebig.
Beachten Sie hierbei unbedingt die Fehlerfalle beim Aufruf von Funktionen mit
enum-Parametern. Sie können Sie sich die Fehlerfalle nochmals ansehen ( ).
Nach dem die beiden Fenster definiert sind, sind deren Eigenschaften auszugeben.
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (16 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Anschließend verschieben Sie das erste Fenster und verändern die Größe des zweiten
Fensters. Geben Sie danach nochmals zur Kontrolle die neuen Fensterdaten aus.
Die Programmausgabe:
Fenster1 erstellt
Fenster2 erstellt
Fenster1
Position: 10, 10
Groesse :300,200
Stil :1
Fenster2
Position: 20, 20
Groesse :600,800
Stil :3
Fenster1
Position: 30, 30
Groesse :300,200
Stil :1
Fenster2
Position: 20, 20
Groesse :640,480
Stil :3
Fenster2 geloescht
Fenster1 geloescht
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le10\Lctor.
So, haben Sie die Übung geschafft?
In der nächsten Lektion erfahren Sie nun, wie Sie Ihr Programm etwas auf Laufzeit optimieren
können.
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (17 von 18) [17.05.2005 11:38:59]
Konstruktor und Destruktor
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_01.htm (18 von 18) [17.05.2005 11:38:59]
enum Datentyp
enum Datentyp
Die Themen:
Syntax
Beispiel
Werte der enum-Konstanten
Fehlerfallen
Beispiel
Syntax
Der enumerated Datentypen (im Folgenden einfach mit enum abgekürzt) wird immer dann
verwendet, wenn eine Menge logisch zusammengehöriger Konstanten definiert werden soll. Der
enum Datentyp wird oft auch als Aufzählungsdatentyp bezeichnet.
Die enum Anweisung hat die enum [Name]
rechts angegebene Syntax. {
Nach dem Schlüsselwort
KONST1[=VAL1],KONST2[=VAL2],...
enum folgt (optional) ein
} [enumVar1,...];
Name, der den enumDatentyp eindeutig
kennzeichnet. Über diesen
Namen können Sie dann zu
einem beliebigen Zeitpunkt
entsprechende enumVariablen oder auch
Funktionsparameter mit
diesem enum-Datentyp
definieren.
Innerhalb der geschweiften Klammern folgen dann die Bezeichner (Konstanten), die mit dem
enum-Datentyp verbunden sein sollen. Welche 'numerischen Werte' diese Bezeichner
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (1 von 8) [17.05.2005 11:39:00]
enum Datentyp
letztendlich besitzen, das erfahren Sie gleich noch.
Nach der geschweiften
Klammer zu können
optional gleich Variablen
des entsprechenden enumDatentyps definiert werden.
Sollen zu einem späteren
Zeitpunkt weitere
Variablen des enumDatentyps definiert werden,
so geben Sie als Datentyp
der Variablen nur den
Namen des enum-Datentyps
an, selbstverständlich
gefolgt vom einem oder
mehreren Variablennamen.
// enum-Datentyp definieren und gleichzeitig
// eine enum-Variable definieren
enum Colors
{
RED, YELLOW, GREEN, BLUE
} myColors;
// weitere enum-Variablen definieren
Colors color1, color2;
Im Beispiel oben wird der enum-Datentyp Colors definiert und dabei auch gleichzeitig die enumVariable myColors definiert. Im Anschluss daran werden zwei weitere Variablen color1 und
color2 des gleichen Datentyps angelegt.
Einer enum-Variable kann dann im Verlaufe des Programms auch nur eine der innerhalb des
enum-Blocks definierten Konstanten zugewiesen werden.
Beispiel
Auch hier wollen wir uns
die Wirkungsweise des
enum-Datentyps anhand
eines kleinen Beispiels
ansehen. Ziel der
nebenstehenden
Anweisungen ist es, die für
eine Schaltfläche (Button)
möglichen Zustände durch
Symbole (Konstanten)
darzustellen. Eine
Schaltfläche soll z.B.
folgende 3 Zustände
annehmen können:
gesperrt, ausgewählt und
gedrückt. Für jeden dieser
// Definition der Schaltflächen-Konstanten
const int DISABLED = 0;
const int FOCUS = 1;
const int PRESSED = 2;
// Definition der Schaltflächen-Variablen
int button;
// Zuweisen eines Zustands
button = DISABLED;
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (2 von 8) [17.05.2005 11:39:00]
enum Datentyp
3 Zustände werden
entsprechende benannte
Konstanten definiert. Die
Schaltfläche selbst wird
durch eine int-Variable
repräsentiert, der dann je
nach Zustand eine der
definierten Konstanten
zugewiesen wird.
Mithilfe einer enumAnweisung kann nun die
Definition der Konstanten
und die Definition der
Variable in einer einzigen
Anweisung
zusammengefasst werden.
Dazu werden innerhalb des
Blocks der enum-Anweisung
einfach die Konstanten
aufgelistet. Diese
Konstanten werden
automatisch fortlaufend
durchnummeriert, wobei
die erste Konstante den
Wert 0 besitzt, die zweite
den Wert 1 usw.
// Definition
enum Button
{DISABLED, FOCUS, PRESSED} button1;
// Weitere Definition einer Schaltfläche
Button button2;
int main()
{
int var;
....
button2 = FOCUS;
// enum-Konstante einem int zuweisen
var = DISABLED;
....
}
Die in einer enumAnweisung definierten
Konstanten können im
Programm auch wie
'normale' benannte
Konstanten verwendet
werden (siehe letzte
Anweisung rechts im
Beispiel). Daraus folgt, dass
alle in einem Programm
definierten enumKonstanten
unterschiedliche Namen
besitzen müssen.
Ausnahme: enumKonstanten die innerhalb
von Klassen definiert
werden, aber dazu mehr im
nächsten Kapitel.
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (3 von 8) [17.05.2005 11:39:00]
enum Datentyp
Werte der enum-Konstanten
Einer enum-Konstante kann // Definition
aber auch innerhalb des
enum Button
enum-Blocks explizit ein
{DISABLED=1, FOCUS, PRESSED=FOCUS+10}
bestimmter Wert
button1;
zugeordnet werden. Hierzu
wird nach dem
Konstantennamen der
Zuweisungsoperator
angegeben, gefolgt von dem
gewünschten Wert.
Alle danach folgenden Konstanten werden dann, wenn sie keinen erneuten expliziten Wert
erhalten, wieder fortlaufend um eins erhöht. Für die enum-Konstanten sind nur Ganzzahlwerte
zugelassen. Außerdem ist es erlaubt, eine bereits in der Liste definierte Konstante zur
Berechnung des Wertes einer weiteren Konstanten zu verwenden. Im Beispiel rechts besitzt die
Konstante DISABLED den Wert 1, FOCUS den Wert2 und PRESSED den Wert 12.
Und wie jede andere Variable kann auch eine enum-Variable bei ihrer Definition gleich
initialisiert werden.
Fehlerfallen
Zum Schluss dieser kleinen Lektion noch zwei Hinweise:
Wie am Anfang der Lektion
bereits kurz erwähnt,
können Sie einer enumVariable nicht ohne
weiteres einen int-Wert
zuweisen, selbst wenn
dieser int-Wert durch eine
entsprechende Konstante
definiert ist.
Zum anderen dürfen Sie
zwar enum-Variablen
innerhalb von Schleifen
// Definition
enum Button
{DISABLED=1, FOCUS, PRESSED=FOCUS+10}
button1;
// Keine int-Zuweisung erlaubt
button1 = 1;
// enum in einer FOR-Schleife
for (button1=FOCUS; button1!=DISABLED;)
....
// Aber das geht nicht!
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (4 von 8) [17.05.2005 11:39:00]
enum Datentyp
verwenden, jedoch können
Sie mit diesen Variablen
keine Rechenoperationen
durchführen, d.h. die rechts
in der zweiten FORSchleife angegebene
Anweisung button1++ führt
zu einem Fehler.
Eine weitere Fehlerfalle
lauert bei der Übergabe von
enum-Parameter an
Funktionen. Im Beispiel
rechts wird der enumDatentyp Style zur
Aufnahme von
verschiedenen Stilen
definiert. Eine Funktion
Func(...) soll jetzt als
Parameter einen Wert
dieses enum-Typs erhalten.
Damit sieht die Deklaration
der Funktion Func(...) wie
rechts angegeben aus. Wird
diese Funktion nun mit nur
einem enum-Wert
aufgerufen so ist alles in
Ordnung (erster
Funktionsaufruf). Doch
Vorsicht! Werden mehrere
enum-Werte beim Aufruf
der Funktion z.B. verodert
usw., so meldet der
Compiler beim Übersetzen
einen Fehler (zweiter
Funktionsaufruf). Der
Grund hierfür liegt darin,
dass z.B. die Veroderung
von zwei Werten einen
Integral-Datentyp
zurückliefert (hier int). Und
damit passt der Aufruf
nicht mehr zur
Funktionsdeklaration. Sie
müssen dann die enumWerte explizit wieder in
den entsprechenden enum-
for (button1=FOCUS; button1!=DISABLED;
button1++)
....
// Enum-Definition
enum Style {STYLE1, STYLE2, STYLE3};
// Funktionsdeklaration
void Func(Style newStyle);
// So geht's
Func(STYLE2);
// Das geht schief
Func(STYLE1|STYLE2);
// So geht's wieder
Func(Style(STYLE1|STYLE2));
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (5 von 8) [17.05.2005 11:39:00]
enum Datentyp
Datentyp konvertieren
(letzter Funktionsaufruf).
Und das war's auch schon zum Thema enumerated Datentyp. Jetzt folgt noch das Beispiel. Eine
Übung schenken wir uns an dieser Stelle.
Beispiel
Das Beispiel:
Das Beispiel simuliert den Schaltzyklus einer Verkehrsampel. Die einzelnen Ampelphasen
werden hierbei durch entsprechende enum-Konstanten definiert.
Im Hauptprogramm werden in einer WHILE-Schleife alle Ampelphase durchlaufen.
Beachten Sie bitte, dass die Ampelphasen explizit der enum-Variable zugewiesen werden
müssen, da keine Rechenoperationen mit enum-Variablen erlaubt sind.
Das unten stehende Beispiel finden Sie auch unter: le8\prog\Benum
Die Programmausgabe:
Die
Die
Die
Die
Die
Ampel
Ampel
Ampel
Ampel
Ampel
hat
hat
hat
hat
hat
rot!
rot-gelb
gruen
gelb
rot!
Das Programm:
// C++ Kurs
// Beispiel zum enum Datentyp
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Hauptprogramm
int main()
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (6 von 8) [17.05.2005 11:39:00]
enum Datentyp
{
// enum-Variable definieren und initialisieren
enum eAMPEL {eROT1, eROTGELB, eGRUEN, eGELB, eROT2};
eAMPEL eAmpel=eROT1;
// Alle Ampelphasen durchlaufen
do
{
// Aktuelle Ampelphase auswerten
// und den entsprechenden Text ausgeben und
// naechste Ampelphase aufschalten
switch (eAmpel)
{
case eROT1:
case eROT2:
cout << "Die Ampel hat rot!\n";
if (eAmpel == eROT1)
eAmpel = eROTGELB;
else
eAmpel = eROT1;
break;
case eROTGELB:
cout << "Die Ampel hat rot-gelb\n";
eAmpel = eGRUEN;
break;
case eGRUEN:
cout << "Die Ampel hat gruen\n";
eAmpel = eGELB;
break;
case eGELB:
cout << "Die Ampel hat gelb\n";
eAmpel = eROT2;
break;
}
} while (eAmpel != eROT1);
}
So, die nächste Lektion befasst sich mit Bitfeldern und ist hauptsächlich etwas für die
hardwarenahe Programmierung. Wenn Sie wollen, können Sie diese Lektion überspringen da
Bitfelder im weiteren Verlaufe des Kurses nicht mehr benötigt werden. Wenn Sie die Lektion
überspringen wollen, klicken Sie hier ( ) um mit den Präprozessor-Direktiven fortzufahren.
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (7 von 8) [17.05.2005 11:39:00]
enum Datentyp
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/le08_02.htm (8 von 8) [17.05.2005 11:39:00]
Bitfelder
Bitfelder
Die Themen:
Einleitung
Syntax und Datenkomprimierung
Zugriff auf Bitfeld-Elemente
Zugriff auf Peripherie über Bitfelder
Besonderheiten von Bitfeldern
Einleitung
Wurden bisher immer nur Daten verarbeitet die mindestens 1 Byte belegten, so erfahren Sie
jetzt, wie Sie auf einzelne Bits zugreifen können. Im ersten Kapitel des Kurses haben Sie zwar
schon die Bitoperationen kennen gelernt, die es Ihnen ebenfalls erlauben, einzelne Bits zu
manipulieren. Doch es geht noch eleganter. Bevor Sie jetzt weiter machen, können Sie sich die
Bitoperationen nochmals ansehen ( ).
Um auf einzelne Bits zuzugreifen, stehen die so genannten Bitfelder zur Verfügung. Bitfelder
werden hauptsächlich bei folgenden Anforderungen eingesetzt:
●
●
Der verfügbare Speicherplatz soll so effektiv wie möglich genutzt werden. Dieser Punkt
verliert im Zeitalter der Megabytes aber zunehmend an Bedeutung. Lediglich bei kleinen
Maschinensteuerungen ist er noch relevant da dort RAM-Speicher einen nicht
unerheblichen Kostenfaktor darstellt.
Es soll auf Peripherie zugegriffen werden, wie z.B. auf einen Baustein für die serielle
Datenübertragung (SIO-Baustein). In solchen Peripheriebausteinen sind die einzelnen
Funktionen in der Regel bitweise kodiert.
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (1 von 8) [17.05.2005 11:39:01]
Bitfelder
Syntax und Datenkomprimierung
Datenkomprimierung
Sehen wir uns zunächst den
ersten Fall an, der
Einsparung von
Speicherplatz. Als
Ausgangspunkt sollen 6
Variablen zum Abspeichern
eines Zeitpunkts definiert
werden. Diese Variablen
benötigt bei den rechts
angegebenen Definitionen
auf einem PC etwa 6 Bytes
(4 x 1 Byte und 1 x 2 Byte).
unsigned
unsigned
unsigned
unsigned
unsigned
char
char
char
char
short
minute;
hour;
day;
month;
year;
Und diesen benötigten Speicherplatz gilt es nun zu optimieren. Dazu ist zuerst der Wertebereich
der einzelnen Elemente zu bestimmen. Ist der Wertebereich bekannt, so kann daraus die
Anzahl der benötigten Bits pro Element berechnet werden. Die nachfolgende Tabelle zeigt den
Wertebereich pro Variable so wie die dafür mindestens benötigten Bits.
Variable
Wertebereich
Bitanzahl
minute
0...59 (<64 = 26)
6
hour
0...23 (<32 = 25)
5
day
1...31 (<32 = 25)
5
month
1...12 (<16 = 24)
4
..2047 (<2048 = 211)
11
year
Damit werden für die (nicht redundante) Speicherung der Informationen 31 Bits, gleich 4 Bytes
benötigt. Die restlichen 2 Bytes der bisherigen Variablen sind 'verschenkter' Speicherplatz.
Syntax
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (2 von 8) [17.05.2005 11:39:01]
Bitfelder
Um diese Daten jetzt
struct [Name]
komprimiert im Speicher
{
abzulegen, wird ein Bitfeld
DATEMTYP1 Element1: Bitzahl;
verwendet. Die Syntax einer
DATENTYP2 Element2: Bitzahl;
Bitfeldanweisung ist
....
nebenstehend dargestellt.
} [bitVar1,...];
Eingeleitet wird ein Bitfeld
durch das Schlüsselwort
struct. Die einzelnen
Elemente werden dann
innerhalb einer
geschweiften Klammer
aufgelistet.
Zusätzlich wird jedoch nach jedem Elementnamen ein Doppelpunkt angegeben und danach die
Anzahl der Bits, die das Element belegt. Als Datentypen für die Elemente dürfen jetzt doch
nur noch die Datentypen bool, char, short, int und long (alle sowohl signed wie auch unsigned)
verwendet werden. Fast selbstverständlich ist, dass die Anzahl der Bits nicht größer sein kann
als der Datentyp zur Verfügung stellt. So können in einem short-Element nur maximal
sizeof(short)*BitsProByte (in der Regel sind dies dann 16 Bits) abgelegt werden.
Der C++ Standard lässt zwar mehr Bits zu als in dem entsprechenden Datentyp abgelegt
werden kann, jedoch haben Sie darauf keinen Zugriff mehr. D.h. die überzähligen Bits
sind nur Füllbits.
Mit dem nun erworbenen
struct Time
Wissen kann das Bitfeld
{
Time wie rechts angegeben
unsigned
komprimiert im Speicher
unsigned
abgelegt werden. Das
unsigned
nachfolgende Bild zeigt wie
unsigned
die einzelnen Elemente im
unsigned
Speicher liegen könnten:
};
char
char
char
char
short
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (3 von 8) [17.05.2005 11:39:01]
minute:
hour :
day
:
month :
year :
6;
5;
5;
4;
11;
Bitfelder
Ein anderer (vom
struct Flags
Speicherverbrauch sehr
{
effizienter) Anwendungsfall
bool flag1: 1;
ist, so genannte Flags
...
(Flaggen) über ein Bitfeld
bool flag8: 1;
zu realisieren. Flags können } anyFlags
nur die beiden Zustände
gesetzt oder nicht gesetzt
annehmen. Hierfür werden
i.d.R. bool Daten verwendet
die ja die äquivalenten
Zustände true und false
annehmen können. Ein bool
Datum belegt unter den
meisten Compilern 1 Byte
(herauszufinden mit dem
sizeof(...) Operator).
Werden nun z.B. 8 solche
Flags benötigt, so belegen
diese dann auch 8 Bytes.
Fassen Sie aber die Flags
wie rechts angegeben in
einem Bitfeld zusammen, so
werden für die 8 Flags nur
noch 8 Bit (i.d.R. gleich 1
Byte) benötigt. Wie gesagt,
dies ist eine Optimierung
bezüglich des
Speicherverbrauchs, nicht
der
Programmgeschwindigkeit.
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (4 von 8) [17.05.2005 11:39:01]
Bitfelder
Die in der Lektion über den Datentyp string erwähnte Standard-Bibliothek enthält schon
eine hierauf spezialisierte Klasse vector<bool>. Sie sehen einmal wieder, es lohnt sich
unbedingt die Standard-Bibliothek später einmal anzuschauen.
Zugriff auf Bitfeld-Elemente
Der Zugriff auf die
Elemente in einem Bitfeld
erfolgt in der Art, dass
zuerst der Name des
Bitfeldvariablen, dann der
Punktoperator und zum
Schluss der Name des
Bitfeldelements angegeben
wird. Wird der Wert eines
Bitfeldelements ausgelesen,
so wird er immer
rechtsbündig (ab der
Bitposition 0) in der
Zielvariablen abgelegt.
Beim Schreiben eines Werts
in ein Bitfeldelement
müssen Sie beachten, dass
überzählige Bits einfach
abgeschnitten werden.
Weisen Sie z.B. einem
Bitfeldelement mit der
Länge 4 Bits den Wert 0x1F
(5 Bits!) zu, so wird im
Element nur der Wert 0x0F
(die niederwertigen 4 Bits)
abgelegt.
// Bitfeld-Definition
struct Time
{
unsigned char minute: 6;
unsigned char hour : 5;
unsigned char day
: 5;
unsigned char month : 4;
unsigned short year : 11;
} myTime;
// Zugriffe auf Bitfeldelemente
mytime.minute = 23;
unsigned short var = myTime.year;
Zugriff auf Peripherie über Bitfelder
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (5 von 8) [17.05.2005 11:39:01]
Bitfelder
So, genug mit Speicherplatz
gegeizt. Sehen wir uns jetzt
an, wie Bitfelder für den
Zugriff auf PeripherieBausteinen verwendet
werden. PeripherieBausteine besitzen in der
Regel mehrere 8 oder 16 Bit
breite Register. Innerhalb
dieser Register werden
bestimmte Funktionen des
Bausteins durch einzelne
Bits kontrolliert. Sehen Sie
sich dazu rechts einmal den
Aufbau eines fiktiven
Timer-Bausteins an. Er
enthält drei 8-Bit breite
Register, deren einzelne
Bits verschiedene
Funktionen steuern. Diesen
Baustein gilt es nun
softwaremäßig
nachzubilden.
Als erstes werden die
einzelnen Bits der einzelnen
Register durch
einsprechende Bitfelder
abgebildet. Nur das
Register 1 bildet hierbei
eine Ausnahme, da es volle
8 Bit belegt und deswegen
direkt als unsigned char
Element reg1 angelegt
werden kann.
Aufbau eines fiktiven Peripherie-Bausteins
Register
Bit
0
0
1
2
1
3
4..7
0..7
2
0
1..6
7
Funktion
0=Baustein gesperrt
1=Baustein freigegeben
0 = Interrupt gesperrt
1 = Interrupt freigegeben
0 = einmaliger Interrupt
1 = periodischer Interrupt
nicht belegt
Vorteiler
Zähler
0 = kein Interrupt anstehend
1 = Interrupt anstehend
nicht belegt
Bausteinfehler
Abbildung des Peripherie-Bausteins
#define UCHAR unsigned char
// Peripherie-Baustein nachbilden
struct Timer
{
struct
{
bool enable
: 1;
bool intEnable: 1;
bool intPeriod: 1;
bool
: 1;
UCHAR preScale: 4;
} reg0;
UCHAR reg1;
struct
{
bool pending : 1;
UCHAR
: 6;
bool error
: 1;
} reg2;
} *pTimer = (struct Timer*)0x0100;
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (6 von 8) [17.05.2005 11:39:01]
Bitfelder
Beachten Sie im
Beispiel, wie die
nicht belegten Bits
des PeripherieBausteins
'übersprungen'
werden. Um Füllbits
zu definieren, wird
nur der Datentyp
und die Anzahl der
Bits definiert, ein
Elementname muss
hier nicht vergeben
werden.
// Zugriff auf Register des Bausteins
pTimer->reg0.enable = true;
error = pTimer->reg2.error;
Anschließend werden die
beiden Bitfelder reg0 und
reg2 sowie der unsigned
short Wert reg1 in einer
übergeordneten Struktur
Timer zusammengefasst.
Strukturen werden später
noch behandelt und dienen
zum Zusammenfassen von
Daten die logisch
zusammengehören. Damit
ist die 'Definition' des
Peripherie-Bausteins
komplett. Da PeripherieBausteine sich in der Regel
auf fixen Adressen
befinden, wird ein
entsprechender
Strukturzeiger definiert
und diesem gleich die
Adresse des ersten
Registers des Bausteins
zugewiesen.
Im Beispiel rechts sind nach der Zeigerdefinition noch zwei Zugriffe auf den PeripherieBaustein angegeben. Beachten Sie hier bitte, dass der Zeigeroperator nur für den Zugriff auf
die Struktur über den Zeiger verwendet wird. Der Zugriff auf die Elemente reg0 und reg2
erfolgt direkt über den Punktoperator. Mehr über den Zugriff auf Strukturelemente später
dann noch.
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (7 von 8) [17.05.2005 11:39:01]
Bitfelder
Besonderheiten von Bitfeldern
Auf einige Besonderheiten bei Bitfelder muss zum Schluss noch hingewiesen werden:
●
●
●
●
●
Von einem Bitfeldelement kann aus nahe liegenden Gründen keine Adresse gebildet
werden (dies wäre die Adresse eines bestimmten Bits im Speicher!).
Wegen dieser Einschränkung sind auch keine Zeiger oder Referenzen auf einzelne
Bitfeldelement erlaubt. Wohl aber können Sie Zeiger auf das Bitfeld selbst definieren, so
wie im Beispiel über die Handhabung von Peripherie-Bausteinen. Da keine Zeiger auf
Bitfeldelemente gebildet werden können, können Sie Bitfeldelemente auch nicht direkt
einlesen da cin die Referenz auf das einzulesende Datum benötigen.
Von Bitfeldelementen kann kein Feld definiert werden, wohl aber wieder vom Bitfeld
selbst.
Der Zugriff auf Bitfelder ist langsamer als der Zugriff auf 'normale' Variablen, da intern
Schiebe- und Maskierungsoperationen durchgeführt werden müssen.
Und zum Schluss der wichtigste Punkt: die Reihenfolge der einzelnen Bits in einem
Bitfeld ist nicht im ANSI C++ festgeschrieben, d.h. es nicht definiert dass das 1. Bit im
Bitfeld auch das niederwertigste Bit im Speicher ist. Wenn Sie Bitfelder im Programm
einsetzen müssen Sie damit rechnen, dass andere Compiler die Reihenfolge der Bits
anders handhaben. Hier hilft nur ein Blick in die Beschreibung zum Compiler.
Soviel zu Bitfeldern. Weiter geht's nun mit den Präprozessor-Direktiven.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/le08_03.htm (8 von 8) [17.05.2005 11:39:01]
Präprozessor-Direktiven
Präprozessor-Direktiven
Die Themen:
Einleitung
include-Direktive
define-Direktive
undef-Direktive
ifdef-, ifndef- und endif-Direktive
if-, elif-, else und endif-Direktiveg
defined-Direktive und vordefinierte Symbole
Weitere Präprozessor-Direktiven
Einleitung
Zum Schluss dieses Kapitels befassen wir uns noch mit den so genannten PräprozessorDirektiven.
Präprozessor-Direktiven sind Direktiven die vor dem eigentlichen Übersetzen des Programms
durch den Präprozessor ausgeführt werden. Alle Zeilen die mit dem Zeichen '#' beginnen
werden als Präprozessor-Direktive interpretiert. Vor dem Symbol '#' dürfen beliebig viele
Leerzeichen und Tabulatoren stehen. Nach dem Symbol '#' folgt die eigentlichen PräprozessorDirektive. Und auch hier gilt: zwischen dem Symbol '#' und der Direktive dürfen wieder
beliebig viele Leerzeichen bzw. Tabulatoren stehen. Diese Direktiven werden nicht mit einem
Semikolon abgeschlossen.
include-Direktive
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (1 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Sehen wir uns die erste, Ihnen bereits bekannte, Präprozessor-Direktive #include einmal näher
an. Es gibt zwei Arten von Include-Direktiven:
#include <DATEI>
#include "DATEI"
Die erste Form der IncludeDirektive sucht die
angegebene Datei in einem
voreingestellten Pfad.
Dieser Pfad wird in der
Regel durch die
Entwicklungsumgebung
(IDE = Integrated
Development Environment)
und/oder einer
Environment-Variable (z.B.
INCLUDE_DIR) festgelegt.
Header-Dateien die mit
dem Compiler ausgeliefert
werden, wie z.B. die Datei
cmath oder auch iostream,
werden in der Regel durch
diese Include-Form
eingebunden.
Suche nur im Standard-Include-Pfad
#include <iostream>
Suche im akt. Verzeichnis und im dann im
Standard-Include-Pfad
#include "common.h"
Suche in einem relativen Pfad
#include "..\include\myfile.h"
Die zweite Form hingegen sucht die angegebene Datei zuerst in dem Verzeichnis, in dem sich die
Datei befindet, die die Include-Direktive enthält. Wird die einzubindende Datei dort nicht
gefunden, wird wieder im voreingestellten Include-Pfad nach der Datei gesucht. Diese Form
wird hauptsächlich für eigene einzubindende Dateien verwendet.
Beide Include-Formen lassen sowohl absolute oder relative Pfadangaben zu. Beachten Sie
hierbei aber bitte, dass in der Pfadangabe nur einfache Backslash-Zeichen stehen (wenn Ihr
System für die Pfadangabe Backslashs verwendet). Wir haben es hier mit PräprozessorDirektiven zu tun und nicht mit Strings!
define-Direktive
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (2 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Die nächste Präprozessor-Direktive #define definiert ein Symbol oder Makro und hat folgende
Syntax:
#define SYMBOL
#define SYMBOL Konstante
#define MAKRO Ausdruck
Für SYMBOL bzw. MAKRO können Sie fast beliebige Namen einsetzen, jedoch sollte (oder besser
darf) der Namen nicht mit einen oder zwei Underscore anfangen und als ersten Buchstaben
einen Großbuchstaben besitzen. Die folgende define-Direktive sollte also besser nicht verwendet
werden: #define _MySymbol. Schreiben Sie stattdessen #define MySymbol. Symbole mit
Underscore gefolgt von einem Großbuchstaben sind eigentlich für die Hersteller von
Bibliotheken oder Compiler reserviert. Außerdem sind die Namen für die Symbole bzw.
Makros case-sensitiv, d.h. es wird nach Groß-/Kleinschreibung unterschieden.
Die erste Form der defineDirektive definiert nur ein
Symbol. Mithilfe weiterer
Präprozessor-Direktiven,
die nachher gleich
beschrieben werden, kann
dann später abgefragt
werden, ob ein bestimmtes
Symbol definiert ist oder
nicht, um damit z.B.
bestimmte Anweisungen
von der Compilierung
auszuschließen. Dies wird
auch als bedingte
Compilierung bezeichnet.
Im Beispiel rechts werden
die beiden Symbole
COMMON_H und DEBUG
definiert. Damit Symbole
im Programm gleich
erkannt werden hat es sich
eingebürgert, diese in
Großbuchstaben zu
schreiben. Jedoch ist dies
keine Vorschrift.
//Definition des Symbols COMMON_H
#define COMMON_H
// Definition des Symbols DEBUG
#define DEBUG
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (3 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Die zweite Form definiert
ebenfalls ein Symbol, weist
diesem aber gleichzeitig
einen Wert zu. Diese Form
sollte aber nur noch in
Ausnahmefällen eingesetzt
werden, besser ist es hierfür
eine benannte Konstante zu
verwenden.
// Definition des Symbols MAXSIZE mit
// dem Wert 10
#define MAXSIZE 10
// Definition des Symbols ERRTEXT mit
// einem String
#define ERRTEXT "Fehler aufgetreten!\n"
Beachten Sie bitte,
dass die Direktive
nicht mit einem
Semikolon
abgeschlossen wird.
Trifft der Präprozessor
dann auf ein Symbol dem
mittels #define ein Wert
zugewiesen wurde, so
ersetzt der Präprozessor
vor dem Compiliervorgang
das Symbol durch den
entsprechenden Wert, mit
folgenden zwei Ausnahmen:
Symbole innerhalb von
Strings (zweite cout
Anweisung im Beispiel
rechts) und Kommentaren
werden niemals ersetzt.
// Definition eines Text-Symbols
#define GSTRING "Guten Tag\n"
// Definition zweier int-Symbole
#define MAXSIZE 10
#define LARGESIZE (MAXSIZE*2)
// Variablendefinition
// Die folgende Anweisung wird durch den
// Präprozessor folgendermaßen erweitert:
// char array[10];
char array[MAXSIZE];
// Hauptprogramm
int main ()
{
// Die folgende Anweisung wird erweitert zu:
// cout << "Guten Tag\n";
Bei der Angabe des Wertes
cout << GSTRING;
für ein Symbol dürfen auch // Aber keine Erweiterung dieser Anweisung da
bereits definierte Symbole // das Symbol innerhalb eines Strings steht!
verwendet werden, so wie
cout << "GSTRING";
im Beispiel rechts bei der
}
Definition des Symbols
LARGESIZE.
Mithilfe der #define-Direktive können auch Makros (Zusammenfassung von mehreren
Anweisungen) definiert werden. Dies ist aber im Zeitalter der C++ Programmierung
eigentlich überflüssig da C++ bessere Möglichkeiten bietet um wiederkehrende
Anweisungen einzubinden (Stichwort: inline-Funktionen, werden später noch
behandelt). Wenn Sie mehr über #define-Makros erfahren möchten, klicken Sie das
Symbol links an.
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (4 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
undef-Direktive
Ein definiertes Symbol
kann mit der Direktive
#undef auch wieder
'gelöscht' werden. Eine
Referenz auf das Symbol
oder Makro nach der
#undef-Direktive führt dann
selbstverständlich zu einem
Fehler.
// Symbol DEBUG definieren
#define DEBUG
...
// Symbol wieder löschen
#undef DEBUG
...
ifdef-, ifndef- und endif-Direktive
Kehren wir wieder zum
Anfang zurück, der
Definition eines Symbols
mittels define-Direktive.
Vielleicht haben Sie sich in
der Zwischenzeit gefragt,
was Sie mit einem
definierten Symbol denn
alles anfangen können. Oft
es so, dass während der
Entwicklung eines
Programms verschiedene
Meldungen zur Kontrolle
des Programmablaufs
ausgegeben werden. In der
fertigen, auslieferfähigen
Version sollen diese
Meldungen dann natürlich
nicht mehr erscheinen. Sie
könnten jetzt hergehen und
alle Meldungen entfernen,
müssten dann aber bei
einem erneuten
Programmtest die
Meldungen wieder
einbauen. Aber es geht
natürlich auch einfacher.
#define DEBUG
....
int main ()
{
....
#ifdef DEBUG
cout << "Programmpunkt1" << endl;
#endif
....
#ifdef DEBUG
cout << "Variablenwert" << ....
#endif
....
}
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (5 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Mit der Präprozessor-Direktive #ifdef SYMBOL können Sie abfragen, ob ein bestimmtes Symbol
definiert ist oder nicht. Ist das Symbol definiert, so übernimmt der Präprozessor alle
Anweisungen die zwischen der #ifdef Direktive und der zu ihr dazugehörigen #endif Direktive
stehen. Ist das Symbol dagegen nicht definiert, so werden diese Anweisungen vor dem
Compiliervorgang 'entfernt', d.h. der Compiler bekommt die Anweisungen erst gar nicht 'zu
Gesicht', und erzeugt somit auch keinen Code dafür. Im Beispiel rechts werden die beiden
Ausgaben nur dann in den Code übernommen, wenn das Symbol DEBUG definiert ist.
Außer der #ifdef SYMBOL
Direktive gibt es noch die
#ifndef SYMBOL Direktive.
Sie hat die entgegengesetzte
Wirkung wie #ifdef, d.h. der
zwischen #ifndef und #endif
stehende Code wird nur
dann übernommen, wenn
das Symbol nicht definiert
ist. Diese Direktive spielt
bei den Dateien eine
wichtige Rolle, die mittels
#include eingebunden
werden. Sehen Sie sich dazu
einmal das Beispiel rechts
an. Die Datei FILE1.H
enthält z.B. beliebige
Definitionen und
Deklarationen. Einige
dieser Definitionen und
Deklarationen werden jetzt
z.B. auch von der Datei
FILE2.H benötigt, weshalb
FILE2.H die Datei FILE1.H
einbindet. So weit ist alles
noch in Ordnung. Sehen
wir uns jetzt einmal eine
typische Anwendung dazu
an. Da die Quellcode-Datei
MAIN.CPP sowohl
Definitionen und
Deklarationen aus der
Datei FILE1.H wie auch aus
FILE2.H benötigt, wird
MAIN.CPP auch beide
Dateien einbinden.
Datei FILE1.H
#ifndef FILE1_H
#define FILE1_H
.... // Definition und Deklaration
#endif
Datei FILE2.H
#ifndef FILE2_H
#define FILE2_H
#include "FILE1.H"
.... // weitere Definitionen und Deklarationen
#endif
Quellcode-Datei MAIN.CPP
#include "FILE1.H"
#include "FILE2.H"
....
Und schon hätten wir ein 'kleines' Problem. Denn zuerst bindet MAIN.CPP die Datei FILE1.H
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (6 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
ein und fügt dadurch bestimmte Deklarationen bzw. Definitionen ein. Anschließend bindet
MAIN.CPP nun die Datei FILE2.H ein. Da FILE2.H selbst aber nochmals die Datei FILE1.H
einbindet, würden damit die Definitionen/Deklarationen aus FILE1.H doppelt in MAIN.CPP
vorkommen, was dann zu einem Übersetzungsfehler führt. Auch das Vertauschen der includeDirektiven in MAIN.CPP bringt keine Lösung. Vielleicht sagen Sie sich nun: dann binde ich in
der Datei FILE2.H die Datei FILE1.H einfach nicht mehr ein und schon funktioniert's. Im
Prinzip haben Sie damit auch recht, doch sollten Sie niemals das Einbinden einer Datei vom
vorherigen Einbinden einer anderen Datei abhängig machen. Im Beispiel wäre das erfolgreiche
Einbinden der Datei FILE2.H dann vom vorherigen Einbinden der Datei FILE1.H abhängig.
Und solche Abhängigkeiten führen früher oder später zu einer nicht mehr überschaubaren
Abhängigkeit. Aber keine Panik, denn jetzt kommt die Lösung dieses Problems, mithilfe der
beiden Direktiven #ifndef und #define. Schließen Sie in Zukunft jede einbindbare Datei in einen
#ifndef...#endif Zweig ein, so wie im Beispiel angegeben. Als abzufragenden Symbolnamen
sollten Sie irgendwie den Namen der entsprechenden einzubindenden Datei mit verwenden.
Wurde die Datei noch nicht eingebunden, d.h. das Symbol ist noch nicht definiert, so definieren
Sie das Symbol mit der #define-Direktive und führen anschließend wie gewohnt die Definitionen
und Deklarationen durch. Wird die Datei dann ein zweites Mal eingebunden, so ist das
entsprechende Symbol bereits definiert, und damit wird der Inhalt der Datei einfach
übersprungen. So einfach geht's, wenn man es weiß!
if-, elif-, else und endif-Direktive
Außer der einfache Abfrage
ob ein Symbol definiert ist
oder nicht, gibt es auch ein
IF-ELIF-ELSE-ENDIF
Konstrukt, das im Prinzip
genauso arbeitet wie die
verwandte C++
Verzweigung. Der
Unterschied zwischen der
Präprozessor-Verzweigung
und der C++ Anweisung ist
der, dass die PräprozessorVerzweigung zum einen vor
dem Compilerdurchlauf
ausgewertet wird und zum
anderen auch nur mit
Präprozessor-Symbolen
arbeitet, also nicht C++
Daten. Im Beispiel rechts
werden je nach Wert des
Symbols MAX verschiedene
#define MAX 10
char array[MAX];
int main()
{
#if MAX<10
cout << "kleines Feld";
#elif MAX == 10
cout << "10er-Feld";
#elif MAX < 50
cout << "mittleres Feld";
#else
cout << "grosses Feld";
#endif
cout << endl;
}
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (7 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Text ausgegeben. Neu ist
die #elif Direktive, sie ist
eine Kombination aus
ELSE und IF.
Die #if und #elif Direktiven
können auch mehrere
Ausdrücke als Bedingungen
auswerten. Die einzelnen
Bedingungen werden dann
entweder durch den
Operator || verodert oder
durch && verundet.
defined-Direktive und vordefinierte Symbole
Ebenfalls im
Zusammenhang mit der IFPräprozessor-Direktive
steht die Direktive
defined(...). defined(...) dient
zum Überprüfen ob ein
Symbol definiert ist. Das zu
überprüfende Symbol wird
dann innerhalb einer
Klammer angegeben.
defined(...) liefert 1 zurück,
wenn das Symbol definiert
ist und ansonsten 0. Vor
defined(...) kann noch der
Operator '!' stehen um das
Abfrageergebnis zu
negieren. Im Beispiel
werden je nach
verwendetem Compiler
verschiedene Ausgaben mit
ins Programm
übernommen. Die in den
defined(...) Direktiven
stehenden Symbole
(beginnend mit 2
Underscore und einem
nachfolgenden
Großbuchstaben!) sind
int main ()
{
#if defined(_BORLANDC_)
cout << "Mit Borland übersetzt!";
#elif definied (_MSVC_)
cout << "Mit VC++ übersetzt!";
#else
cout << "Den kenn ich nicht!";
#endif
....
}
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (8 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Symbole die vom Compiler
selbst definiert werden. So
definiert der BORLANDCompiler z.B. das Symbol
_BORLANDC_ während der
MICROSOFT Compiler
das Symbol _MSVC_
definiert. Welche Symbole
Ihr Compiler definiert,
entnehmen Sie bitte aus der
Dokumentation zum
Compiler.
Auch die Sprache C++ selbst definiert einige Symbole die Sie der folgenden Tabelle entnehmen
können:
Symbol
Bedeutung
__LINE__ Enthält aktuelle Zeilennummer im Quellcode
__FILE__
String mit dem Namen der akt. Datei
__DATE__ String mit dem aktuellen Datum in der Form Monat/Tag/Jahr
__TIME__ String mit der aktuellen Uhrzeit in der Form Stunde:Minute:Sekunde
__STDC__
Compilerspezifisch, in der Regel ist dieses Symbol definiert wenn nur ANSI
C/C++ Code vom Compiler akzeptiert wird.
ANSI C++ konforme Compiler definieren für dieses Symbol einen Wert mit
__cplusplus mind. 6 Ziffern, alle anderen C++ Compiler einen Wert mit bis zu 5 Ziffern. C
Compiler definieren dieses Symbol überhaupt nicht.
So, fehlen uns nur noch die letzten drei Präprozessor-Direktiven.
Weitere Präprozessor-Direktiven
Die Direktive #error bewirkt einen Abbruch des Compilerlaufs. Nach der PräprozessorDirektive kann noch ein Text stehen (muss nicht in Anführungszeichen stehen!) der beim
Abbruch mit ausgegeben wird. In der Regel steht die #error Direktive innerhalb einer #ifDirektive.
Die #line Nummer ["Datei"] Direktive dient zum Umdefinieren der beiden Symbole __LINE__
und __FILE__ (siehe vorherige Tabelle). Die Angabe von Datei ist optional.
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (9 von 10) [17.05.2005 11:39:04]
Präprozessor-Direktiven
Und die letzte Präprozessor-Direktive #pragma dient zum Definieren von compilerspezifischen
Präprozessor-Direktiven. Welche Direktive hier zulässig sind, das müssen Sie Ihrer CompilerDokumentation entnehmen. Kennt ein Präprozessor die hinter einem #pragma stehende
Direktive nicht, so wird sie einfach ignoriert, d.h. es erfolgt keine Fehlermeldung.
So das war's. Und nun viel Spaß mit der nächsten Seite!
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/le08_04.htm (10 von 10) [17.05.2005 11:39:04]
Ende Kapitel 2
Ende Kapitel 2
Herzlichen Glückwunsch!
Sie haben damit das zweite Kapitel dieses
Kurses
fertig bearbeitet.
Das nächste Kapitel befasst sich mit Klassen, Objekte
und dynamischer Speicherverwaltung
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/k2_end.htm [17.05.2005 11:39:07]
Makro-Definitionen
Makro-Definitionen
Die dritte und letzte Form
der define-Anweisung
definiert ein Makro. Ein
Makro ist im Prinzip nichts
anderes als eine
Kurzschreibweise für
beliebige C++
Anweisungen. Trifft der
Präprozessor auf ein
Makro, so wird dieses
durch die bei der
Makrodefinition
angegebenen Anweisungen
ersetzt. Erstrecken Sie sich
die Anweisungen eines
Makros über mehrere
Zeilen, so sind die Zeilen
mit einem BackslashZeichen abzuschließen.
// Makro-Definition
#define CHECK_ERROR \
if (error != 0) \
{ cout << "Fehler!"; exit(error); }
// Im Programm dann
....
CHECK_ERROR
// Wird ersetzt durch
//
if (error != 0) \
//
{ cout <<"Fehler"; exit(error); }
Innerhalb eines
Makros das über
mehrere Zeilen
geht darf niemals
ein Kommentar
stehen!
Außer dieser einfachen
Makro-Anweisung können
Makros auch Parameter
besitzen. Die Parameter
werden hier genauso wie
bei Funktionen in
Klammern eingeschlossen
und mehrere Parameter
werden durch Komma
voneinander getrennt.
Beachten Sie bitte, dass hier
nur die Parameternamen
stehen und keine
Datentypen wie bei
Funktionen, da die define-
// Makro-Definition
#define CAREA1(rad) \
3.14/4.0*(rad)*(rad)
// Das gleiche Makro, nur andere Schreibweise
#define CAREA2(rad) \
3.14/4.0*rad*rad
// Hauptprogramm
int main ()
{
double erg; // Var. für Ergebnis
double val; // 'Parameter'
// Folgende Anweisung
erg = CAREA1(val+2.);
// wird erweitert zu:
http://www.cpp-tutor.de/cpp/le08/le08_04_d1.htm (1 von 2) [17.05.2005 11:39:07]
Makro-Definitionen
Anweisung vom
Präprozessor verarbeitet
wird und nicht vom
Compiler!
Solche Makros
sind eigentlich ein
Relikt aus
vergangenen
Tagen und sehr
fehleranfällig.
Sehen Sie sich
dazu einmal die
Definition des
Makros CAREA2
rechts an und den
daraus
resultierenden
Code. C++ bietet
für solche kurzen
Funktionen inlineFunktionen an die
im nächsten
Kapitel behandelt
werden.
// erg = 3.14/4.0*(val+2.)*(val+2.);
// Folgende Anweisung
erg = CAREA2(val+2.);
// wird erweitert zu:
// erg = 3.14/4.0*val+2.*val+2.;
// (Punkt- vor Strichrechnung!)
}
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le08/le08_04_d1.htm (2 von 2) [17.05.2005 11:39:07]
Memberzeiger
Memberzeiger
Die Themen:
Schnittstellenzeiger (Zeiger auf Memberfunktionen)
Eigenschaftszeiger (Datenzeiger)
Schnittstellenzeiger (Zeiger auf
Memberfunktionen)
Definition
Während 'normale' Zeiger Adressen von Variablen oder Funktionen enthalten, enthalten
Zeiger auf Klassenmember nur einen Offset auf das entsprechende Klassenmember innerhalb
der Klasse. Diese Tatsache hat Auswirkungen auf die Zugriffe über den Zeiger auf
Klassenmember.
Beginnen wollen hier mit Zeigern auf Memberfunktionen, den so genannten
Schnittstellenzeigern. Hier können Sie sich die Funktionszeiger vorher nochmals anschauen
( ).
Rechts ist die
class State
Ausgangsklasse für unser
{
Beispiel dargestellt. Die
public:
Klasse enthält zum einen
void Init();
wieder die Memberfunktion
void State1();
Init(...) zur Initialisierung
void State2();
der Eigenschaften und zum
void State3();
anderen drei weitere
void Execute();
Memberfunktionen
};
StateX(...), die später über
einen Schnittstellenzeiger
aufgerufen werden sollen.
http://www.cpp-tutor.de/cpp/le09/le09_02.htm (1 von 5) [17.05.2005 11:39:08]
Memberzeiger
Definieren wir zunächst den Schnittstellenzeiger innerhalb der Klasse. Vielleicht erinnern Sie
sich noch an die Definition des Funktionszeigers. Die Definition eines Funktionszeigers auf eine
Funktion die keinen Wert zurückgibt und auch keine Parameter benötigt sah wie folgt aus:
void (*pfnFunc) ( );
wobei pfnFunc ist der Name des Funktionszeigers ist.
Schnittstellenzeiger werden class State
fast in der gleichen Art und {
Weise definiert, nur dass
public:
vor dem Zeigernamen noch
void (State::*pfnState)();
der Klassenname
void Init();
angegeben wird, gefolgt
void State1();
vom Operator ::. Der
void State2();
Grund hierfür ist, dass im
void State3();
Schnittstellenzeiger nicht
void Execute();
die Adresse einer
};
Memberfunktion abgelegt
wird, sondern wie erwähnt
nur deren Offset innerhalb
der Klassenstruktur.
Rechts wurde der Klasse
State der
Schnittstellenzeiger pfnState
hinzugefügt.
Ist der Schnittstellenzeiger definiert, so kann ihm die 'Adresse' einer Memberfunktion
zugewiesen werden. Sehen wir uns zunächst wieder an wie Funktionszeigern die Adresse einer
Funktion zugewiesen wird:
pfnFunc = Func;
Func ist der Name der Funktion, deren Adresse im Zeiger abgelegt werden soll.
http://www.cpp-tutor.de/cpp/le09/le09_02.htm (2 von 5) [17.05.2005 11:39:08]
Memberzeiger
Und auch hier sieht die
void State::Init()
Zuweisung einer Adresse
{
einer Memberfunktion zum
pfnState = &State::State1;
Schnittstellenzeiger etwas
}
anders aus. Im Gegensatz
zu den Funktionszeigern
müssen Sie zum einen den
Adressoperator &
voranstellen und zum
anderen wieder den
Klassennamen gefolgt vom
Operator :: angeben. Damit
sieht die Zuweisung an
einen Schnittstellenzeiger
wie rechts angegeben aus.
Aufruf von Memberfunktionen über Schnittstellenzeiger
Bleibt nur noch der Aufruf void State::Execute()
der Memberfunktion übrig, {
deren Offset im
(this->*pfnState)();
Schnittstellenzeiger
}
abgelegt ist. Im Beispiel
rechts wird in der
Memberfunktion
Execute(...) die im Zeiger
pfnState abgelegte
'Memberfunktion'
aufgerufen.
Dieser Aufruf enthält zwei Neuigkeiten. Zum einen wird hier der Zeiger this verwendet, der
später noch genauer beschrieben wird. Vorab nur soviel dazu: der this Zeiger enthält stets die
Adresse des Objekts, in dessen Kontext die Memberfunktion aufgerufen wurde. Zum anderen
wird hier der neue Operator ->* verwendet. Beachten Sie bitte, dass ->* ein Operator ist und
keine Kombination aus den beiden Operatoren -> und *. Der Operator ->* dient
ausschließlich zum Aufruf von Memberfunktionen über Schnittstellenzeiger.
Klassenlose Schnittstellenzeiger
http://www.cpp-tutor.de/cpp/le09/le09_02.htm (3 von 5) [17.05.2005 11:39:08]
Memberzeiger
Wollen Sie
Memberfunktionen nicht
über einen innerhalb der
Klasse definierten
Schnittstellenzeiger
aufrufen sondern über
einen außerhalb der Klasse
definierten, so wird für die
Definition die gleiche
Syntax verwendet wie bei
der Definition innerhalb
der Klasse, d.h. auch hier
muss vor dem Zeigernamen
wieder der Klassenname
und der Operator :: stehen.
Auch die Zuweisung einer
'Adresse' (in Wirklichkeit
ist es ja nur ein Offset) zum
Zeiger ändert sich nicht
gegenüber vorher.
Lediglich der Aufruf der
Memberfunktion sieht
etwas anders aus. Da im
Schnittstellenzeiger nur der
Offset der Memberfunktion
abgespeichert ist, muss vor
dem Zeigernamen das
Objekt angegeben werden,
dessen Memberfunktion
aufgerufen werden soll.
Nach dem Objektnamen
folgt der neue Operator .*.
Beachten Sie auch hier, das
.* ein Operator ist und
nicht die Kombination aus
dem Operator . und dem
Operator *.
// Klassendefinition
class State
{
....
};
// Definition des Schnittstellenzeigers
void (State::*pfnMember)();
....
// Definition eines Objekts
State myState;
// Hauptprogramm
int main ( )
{
pfnMember = &State::State2;
(myState.*pfnMember)( )
....
};
Eigenschaftszeiger (Datenzeiger)
http://www.cpp-tutor.de/cpp/le09/le09_02.htm (4 von 5) [17.05.2005 11:39:08]
Memberzeiger
Sehen wir uns zum Schluss
noch an, wie über Zeiger
auf die Eigenschaften einer
Klasse zugegriffen wird.
Die Definition des
Eigenschaftszeigers erfolgt
wiederum durch
voranstellen des
Klassennamens vor dem
Zeigernamen. Auch die
Initialisierung des Zeigers
erfolgt in der gleichen Art
wie beim
Schnittstellenzeiger, d.h.
zuerst kommt der
Adressoperator, dann der
Klassenname, dann der
Operator :: und zum
Schluss der Name der
Eigenschaft.
// Klassendefinition
class State
{
public:
....
int data;
};
// Definition des Memberzeigers
int State::*pData;
// Hauptprogramm
int main()
{
// CState Objekt und Zeiger definieren
State myState, *pAnyState;
// Objektzeiger initialisieren
pAnyState = &myState;
// Memberzeiger auf Datum
pData = &State::data;
// Datum im ersten Objekt setzen
myState.*pData = 10;
// Datum über Zeiger auslesen
Soll nun über diesen Zeiger
int var = pAnyState->*pData;
eine Eigenschaft der Klasse
....
verändert werden, so
}
werden wieder die
Operatoren .* bzw. ->*
verwendet.
Ein Beispiel und eine Übung schenken wir uns hier. Weiter geht's dann jetzt mit den Unions.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_02.htm (5 von 5) [17.05.2005 11:39:08]
Klassen ohne Memberfunktionen
Klassen ohne Memberfunktionen
Manchmal kann es
struct Color
durchaus sinnvoll sein,
{
logisch zusammengehörige
unsigned char red;
Daten in Klassen
unsigned char green;
zusammen zu fassen.
unsigned char blue;
Rechts sehen Sie ein
};
Beispiel für eine solche
Klasse, die zur Definition
von Farbwerten verwendet
werden kann. In der Regel
werden solche Klassen
immer dann verwendet,
wenn die in ihr enthaltenen
Eigenschaften (Daten) für
sich alleine keine
Funktionalität ergeben.
Klassen ohne eigene Memberfunktionen werden oft in andere Klassen eingebunden und dann
über deren Memberfunktionen angesprochen. So wäre es zum Beispiel denkbar, dass die obige
Klasse Color in eine Fensterklasse Window gebunden wird und dann Farbwert für das Fenster
enthält. Solchen eingebundenen Klassen ist später noch eine eigene Lektion gewidmet.
Beachten Sie, dass die obige Klasse nun den Klassentyp struct besitzt. Warum diese hier so sein
muss, das erfahren Sie gleich noch unter dem Stichwort Zugriffsrechte in Klassen.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_01_d1.htm [17.05.2005 11:39:11]
Objekte im Speicher
Objekte im Speicher
Noch eine Anmerkung zum
Speicherbedarf von Objekten: bei
der Definition von mehreren
Objekten einer Klasse liegen die
Eigenschaften (Objektdaten)
selbstverständlich für jedes Objekt
getrennt im Speicher. Ansonsten
hätte z.B. jedes unserer beiden
Fenster die gleiche Position und
Größe. Die Schnittstelle
(Memberfunktionen) ist jedoch für
alle Objekte nur einmal im Speicher
vorhanden (siehe Bild rechts). Wie
die Memberfunktionen dann
feststellen können, mit welchen
Eigenschaften sie letztendlich
arbeiten, dass erfahren Sie in der
Lektion Der Zeiger this noch.
Doch auch für die Ablage der
Eigenschaften gilt wie so oft: keine
Regel ohne Ausnahme. In der
Lektion über statische Eigenschaften
erfahren Sie, wie Sie Eigenschaften
so definieren können, dass sie für alle
Objekte eine Klasse gelten.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_01_d2.htm [17.05.2005 11:39:14]
Initialisierung von Klassen mit nur public
Initialisierung von Klassen mit nur
public
Objekt von Klassen, die nur
public-Eigenschaften
enthalten (wie es zum
Beispiel beim Klassentyp
struct standardmäßig der
Fall ist), können auf die
rechts angegeben Art
initialisiert werden. Die
Initialisierung erfolgt
hierbei fast auf die gleiche
Weise wie die
Initialisierung von
einfachen Variablen, nur
dass die Initialwerte für die
einzelnen Elemente nun
innerhalb eines Blocks {...}
eingeschlossen werden. Und
selbstverständlich müssen
die Datentypen der
Initialwerte auch mit den
Datentypen der
entsprechenden Elemente
übereinstimmen.
// Definition und Init. einer Struktur
struct Color
{
unsigned char red;
unsigned char green;
unsigned char blue;
};
...
Color myColor = {0x00, 0x80, 0xc0};
Wie Objekte im Allgemeinen initialisiert werden, das erfahren Sie in der später noch.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_01_d3.htm [17.05.2005 11:39:15]
Verdeckung von Namen
Verdeckung von Namen
Obwohl es in der OOP fast ein Vergehen ist, aus Memberfunktionen heraus auf globale Daten
zuzugreifen, so ist dieses jedoch generell nicht durch den C++ Standard verboten.
Hierbei können wieder zwei Fälle auftreten:
Gibt es innerhalb
int value;
// Globales Datum
der Klasse kein
struct anyClass
Member mit dem
{
gleichen Namen wie
... // kein Member mit dem Namen value
das globale Datum,
void DoAny()
so reicht die alleinige
{
Angabe des
value = 10;
// globales Datum setzen
Variablennamens.
....
}
};
Enthält dagegen die
Klasse eine
Eigenschaft mit dem
gleichen Namen wie
das globale Datum,
so verdeckt zunächst
die Eigenschaft das
globale Datum. Um
trotzdem auf das
globale Datum
zugreifen zu können,
wird vor dem
Namen wiederum
der globale
Zugriffsoperator ::
(zwei Doppelpunkte)
gestellt.
int value;
// Globales Datum
struct anyClass
{
int value; // Member
void DoAny()
{
value = 10;
// Member setzen
::value = 1; // globales Datum setzen
....
}
};
http://www.cpp-tutor.de/cpp/le09/le09_01_d4.htm (1 von 2) [17.05.2005 11:39:17]
Verdeckung von Namen
Die
Datentypen
spielen dabei
überhaupt
keine Rolle.
Entscheidend
ist allein der
Name.
Wenn Sie fertig sind, kehren durch schließen dieses Fensters wieder zum Kurs zurück.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_01_d4.htm (2 von 2) [17.05.2005 11:39:17]
Varianten
Varianten
Die Themen:
Syntax
Beispiel einer Variante
Anonyme Variante
Initialisierung einer Variante
Syntax
Mithilfe einer Variante, häufig auch als Union bezeichnet, lassen sich auf ein und dem selben Speicherplatz
unterschiedliche Daten ablegen. Und diese Daten müssen dann nicht einmal den selben Datentyp besitzen.
Die Variantenanweisung gleicht bis auf das union [Name]
Schlüsselwort union der bisherigen Definition {
einer Klasse und hat damit die rechts
DATENTYP1 Element1;
angegebene Syntax. Die rechts in Klammern
DATENTYP2 Element2;
stehenden Angaben sind optional. Genau
....
genommen ist eine Variante nur eine
} [variantVar1,...];
besondere Klasse, die eine Reihe von
Einschränkungen besitzt.
Der von der Variante letztendlich belegte Speicherplatz wird von dem Variantenelement mit dem größten
Speicherbedarf bestimmt.
Varianten können alle bisher bekannten Datentypen enthalten. Auch der Zugriff auf ein Variantenelemente
erfolgt analog dem auf ein Klassenelemente, d.h. es folgt zuerst der Name der Variantenvariable, dann der
Punktoperator bei direktem Zugriff bzw. der Zeigeroperator bei indirektem Zugriff über einen Zeiger und zum
Schluss der Name des Variantenelements.
Eine Variante kann nicht den Datentyp string aufnehmen. Warum das so ist, das erfahren Sie in der
nächsten Lektion.
Beispiel einer Variante
http://www.cpp-tutor.de/cpp/le09/le09_03.htm (1 von 5) [17.05.2005 11:39:18]
Varianten
Um eines der Einsatzgebiete einer Variante
zu verstehen, sehen Sie sich nun einmal das
Beispiel rechts an. Dort wird die Variante
Date definiert die zum Abspeichern eines
Datums dienen soll. Die Variante enthält die
beiden Elemente dwDate und acDate. Da wie
erwähnt beide Daten ab der gleichen
Speicheradresse beginnen, ergibt sich
folgender Speicheraufbau:
// Variante definieren
union Date
{
unsigned long dwDate;
unsigned char acDate[4];
} date1, date2;
// Hauptprogramm
int main ()
{
// 1. Datum ablegen
date1.dwDate = 0;
dwDate
date1.acDate[0] = 1;
-acDate[0] acDate[1] acDate[2]
date1.acDate[1] = 2;
date1.acDate[2] = 99;
// 2. Datum ablegen
Mithilfe dieser Variante kann jetzt ein
date2.dwDate = 0;
Datum sowohl als long-Zahl wie auch
date2.acDate[0] = 9;
byteweise ausgewertet werden. Und damit ist
date2.acDate[1] = 9;
es durch diesen kleinen 'Kniff' relativ leicht
date2.acDate[2] = 99;
möglich, zwei Datumsangaben miteinander
// Datum vergleichen
zu vergleichen. Der eigentliche Trick besteht
if (date1.dwDate < date2.dwDate)
darin, dass bei einer long-Zahl das
cout << "Datum1 liegt vor Datum2\n";
niederwertige Byte auch auf der
else
niederwertigen Adresse zu liegen kommt. Da
cout << "Datum2 liegt vor Datum1\n";
bei einer Variante beide Daten 'übereinander }
liegen', wird im ersten Byte des char-Feldes
der Tag, danach der Monat und zum Schluss
das Jahr abgelegt.
Dieser Trick funktioniert so natürlich nur bei den Prozessoren, bei denen das Low-Byte des longDatums auf der niederen Adresse zu liegen kommt und für die gilt: char = 1 Byte und long = 4 Byte. Um
bei anderen Prozessoren diesen Trick anwenden zu können, müssen Sie die Variante entsprechend
anpassen. Aber denken Sie auch daran: bei solchen 'Tricks' ist ein Programm nicht mehr portabel.
Das Einzige was Sie bei dieser Variante beachten müssen ist, dass auch der nicht durch das char-Feld belegt Platz
initialisiert werden muss wenn Sie das Datum als long-Zahl auswerten wollen.
Sehen wir uns nun noch an, wie die beiden oben im Beispiel angegebenen Daten in den Varianten zu liegen
kommen (99 = 0x63):
0x00630201 (als long Zahl!)
0x01
0x02
0x63
0x00
0x00630909 (als long Zahl!)
0x09
0x09
0x63
Sehen wir noch ein weiteres Beispiel für eine Variante an.
http://www.cpp-tutor.de/cpp/le09/le09_03.htm (2 von 5) [17.05.2005 11:39:18]
0x00
Varianten
Da Varianten, wie bereits erwähnt,
// Variante
prinzipiell auch Klassen sind, können Sie
union Convert
auch Memberfunktionen enthalten. Im
{
Beispiel rechts wird eine Variante definiert
unsigned char bytes[2];
die zwei char-Werte zu einem short-Wert
unsigned short word;
zusammenfasst. Zum Setzen der beiden char// Memberfunktionen zum Byte setzen
Werte werden entsprechende
void SetByte1(unsigned char byte)
Memberfunktionen SetByteX(...) verwendet.
{
Der aus den beiden char-Werten
bytes[0] = byte;
zusammengesetzte short-Wert wird über die
}
Memberfunktion GetWord(...)
void SetByte2(unsigned char byte)
zurückgegeben.
{
bytes[1] = byte;
Im Hauptprogramm wird dann das
}
Varianten-Objekt byte2Word definiert.
// Bytes als Word zurückgeben
Beachten Sie bei der Definition des Objekts,
unsigned short GetWord() const
dass auch hier das Schlüsselwort union
{
entfallen kann.
return word;
}
};
Anschließend werden über die
// Hauptprgramm
Memberfunktionen SetByte1(...) und
int main()
SetByte2(...) die beiden unsigned char
{
Elemente des Variantenfelds bytes gesetzt.
// Varianten-Objekt definieren
Diese beiden Bytes werden dann zusammen
Convert byte2Word;
als unsigned short Wert ausgegeben.
// Bytes setzen
byte2Word.SetByte1(0x11);
Das hier gezeigte Verfahren
byte2Word.SetByte2(0x22);
funktioniert natürlich nur dann,
// Word ausgeben
wenn sizeof(short) gleich
cout << "WORD-Wert: " << hex
2*sizeof(char) ist.
<< byte2Word.GetWord() << endl;
}
Anonyme Variante
Eine weitere Variantenart ist die so genannte anonyme Variante. Anonyme Varianten besitzen keinen
Variantennamen und gehören zu keinem Variantenobjekt. Solchermaßen definierte anonyme Varianten weisen
zwei Besonderheiten auf:
●
●
Sie müssen immer der Speicherklasse static angehören, wenn Sie im globalem Namenraum oder in einem
benannten Namensraum definiert sind. Lokale anonyme Varianten können jeder Speicherklasse
angehören, die an der entsprechende Stelle erlaubt ist.
Sie können keine Memberfunktionen enthalten
http://www.cpp-tutor.de/cpp/le09/le09_03.htm (3 von 5) [17.05.2005 11:39:18]
Varianten
Anonyme Varianten dienen 'nur' zur Ablage
von verschiedenen Daten auf der gleichen
Speicherstelle. Alle Member der anonymen
Variante werden direkt angesprochen, d.h.
ohne die sonst übliche Varianten-Syntax x.y
bzw. x->y. Das nebenstehende Beispiel
demonstriert den Einsatz einer anonymen
Variante zur Konvertierung eines unsigned
long Werts in zwei unsigned short bzw. vier
unsigned char Werte.
Wenn Sie nebenstehendes Beispiel laufen
lassen, so erhalten Sie folgende Ausgabe:
Die Ausgabe:
Long-Wert: 0x12345678
Als short: 0x5678 0x1234
Als char : 0x78 0x56 0x34 0x12
Und wieder: die obige Ausgabe
erhalten Sie natürlich nur bei
Rechnern bei denen das Low-Byte
auch auf niederen Adresse steht und
bei denen gilt: sizeof(long) =
2*sizeof(short) = 4*sizeof(char).
// Definition der anonymen Variante
static union
{
unsigned long lVal;
unsigned short sVal[2];
unsigned char cVal[4];
};
// Hauptprogramm
int main()
{
// ulong-Anteil der anonymen Variante
lVal = 0x12345678UL;
// Ausgabe mit Basis in Hex
cout << hex;
cout.setf(ios::showbase);
// Ausgabe als ulong
cout << "Long-Wert: " << lVal << endl;
// Ausgabe als ushort
cout << "Als short: " << sVal[0] <<
' ' << sVal[1] << endl;
// Ausgabe als uchar
cout << "Als char : ";
for (int index=0; index<4; index++)
cout << static_cast<int>(cVal[index])
<< ' ';
cout << endl;
}
Initialisierung einer Variante
Fehlt uns jetzt zum Abschluss nur noch die
am Anfang erwähnte Abweichung der
Variante von der Struktur. Sie betrifft die
Initialisierung der Variante. Soll eine
Variante bei ihrer Definition initialisiert
werden, so muss der Datentyp des
Initialwertes mit dem Datentyp des ersten
Elements in der Variante übereinstimmen
und in einem Block {...} eingeschlossen
werden. Sehen Sie sich dazu besonders die 2.
Initialisierung im Beispiel rechts an. Die
alleinige Angabe des Initialwertes 10 reicht
bei dieser Variante nicht aus, da das Literal
10 hier als int-Wert interpretiert wird. Sie
müssen also eine explizite Typkonvertierung
des Literals in einen char-Wert vornehmen.
// Variante definieren
union Month{
char cMonth
char *ptrMonth;
};
// Ungültige Initialisierungen da Datentyp
// des Initialwertes nicht übereinstimmt
union Month month1 = {"Januar"};
union Month month2 = {10};
// Gültige Initialisierung
union Month month3 = {static_cast<char>(10}};
Und damit sind wir am Ende dieser Lektion angelangt, das auch gleichzeitig das Ende der 9. Lerneinheit ist. Auf
http://www.cpp-tutor.de/cpp/le09/le09_03.htm (4 von 5) [17.05.2005 11:39:18]
Varianten
ein weiteres Beispiel und eine Übung verzichten wir hier.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le09/le09_03.htm (5 von 5) [17.05.2005 11:39:18]
Inlines
Inlines
Die Themen:
Einführung
inline-Funktionen
inline-Memberfunktionen
Einschränkungen bei inlines
Einführung
In dieser Lektion werden wir einmal etwas für die Laufzeit-Optimierung der Anwendung tun.
Um auf die geschützten
Eigenschaften einer
Klasse zuzugreifen, muss ja
eine entsprechende
Memberfunktion
aufgerufen werden. Bei
kurzen Memberfunktionen,
die z.B. nur eine
Eigenschaft setzen oder
zurückliefern, wird dabei
aber mehr Zeit für den
Aufruf benötigt als für das
Setzen oder Lesen der
Eigenschaft. Rechts sehen
Sie die Aktionen, die beim
Aufruf einer
Memberfunktion
durchgeführt werden.
Dabei haben die Aktionen
1,2,3,5 und 6 nichts direkt
Aktionen beim Aufruf einer Memberfunktion
1.)
2.)
3.)
4.)
5.)
6.)
7.)
Parameter auf dem Stack ablegen
Rücksprungadresse auf dem Stack ablegen
Sprung zur Memberfunktion
Memberfunktion ausführen
Rücksprung hinter den Aufruf
Parameter vom Stack nehmen
nächste Anweisung
Legende:
Overhead für Aufruf von Memberfunktionen
Durchzuführende Aktion
http://www.cpp-tutor.de/cpp/le10/le10_02.htm (1 von 5) [17.05.2005 11:39:19]
Inlines
mit der gewünschten
Funktionalität zu tun. Man
bezeichnet dieses auch als
Overhead. Nur die
Aktionen 4 und 7 sind
eigentlich notwendig um
das gewünschte Ergebnis zu
erhalten. Und um diesen
Overhead zur vermeiden,
wurden die so genannten
inline-Funktionen bzw.
inline-Memberfunktionen
eingeführt.
inline-Funktionen
Sehen wir uns zunächst die inline void CheckError(int err, char* pT)
inline-Funktionen an. Inline- {
Funktionen werden
if (err)
prinzipiell genauso
{
definiert wie normale
cout << "Fehler: " << err
Funktionen. Zusätzlich
<< pT << endl;
wird nun aber vor dem
exit (err);
Returntyp der Funktion
}
das Schlüsselwort inline
}
gestellt.
Triff der Compiler dann
// Inline-Funktion definieren
beim Übersetzen auf den
inline void CheckError(int err, char* pT)
Aufruf einer inline{
Funktion, so wird anstelle
.... // Code siehe oben
des Funktionsaufrufs direkt }
der Code der Funktion
int main()
eingefügt. Und damit
{
entfällt der komplette
....
Overhead für den Aufruf.
CheckError(status,"Ueberlauf");
Im Beispiel rechts ist als
// Wird durch den Compiler erweitert zu:
Kommentar dargestellt, wie
// if (status)
der Compiler den
// {
Funktionsaufruf der inline//
cout << "Fehler << status
Funktion CheckError(...)
//
<< "Ueberlauf" << endl;
ersetzt.
// }
Da nun statt des
Funktionsaufrufs direkt
http://www.cpp-tutor.de/cpp/le10/le10_02.htm (2 von 5) [17.05.2005 11:39:19]
Inlines
der Funktionscode ins
übersetzte Programm
eingefügt wird, empfiehlt es
sich, nur kurze Funktionen
als inline-Funktionen zu
definieren da ansonsten die
Größe des übersetzten
Programms unter
Umständen beträchtlich
anwachsen kann.
inline-Funktionen entsprechen von der Wirkungsweise her in etwa den PräprozessorMakros (#define-Direktive). Sie können sich jetzt die Präprozessor-Makros nochmals
ansehen( ). Der Hauptunterschied zu den Makros liegt darin, dass die Parameter bei
inline-Funktionen typisiert sind.
inline-Memberfunktionen
Werden
Memberfunktionen
innerhalb einer
Klasse definiert, so
sind sie unter
gewissen
Einschränkungen
(folgen gleich noch)
automatisch als
inlineMemberfunktion
definiert. Beachten
Sie dies bitte, wenn
Sie
Memberfunktionen
innerhalb einer
Klasse definieren.
Sie sollten dies
wirklich nur für
sehr kurze
Memberfunktionen
tun.
class Window
{
string title;
....
public:
// autom. inline-Memberfunktion!
const string& GetTitle() const
{
return title;
}
....
};
int main()
{
Window myWin;
....
cout << myWin.GetTitle() << endl;
// wird compiliert zu:
// cout << myWin.title << endl;
....
}
http://www.cpp-tutor.de/cpp/le10/le10_02.htm (3 von 5) [17.05.2005 11:39:19]
Inlines
Wird eine
Memberfunktion
innerhalb einer
Klasse nur
deklariert, so kann
sie bei ihrer
Definition außerhalb
der Klasse durch
voranstellen des
Schlüsselworts inline
vor dem Returntyp
ebenfalls als inlineMemberfunktion
definiert werden.
Und auch hier gelten
ebenfalls
Einschränkungen
die gleich noch
aufgeführt werden.
class Window
{
string title;
....
public:
const string& GetTitle() const;
....
};
// inline Definition
inline const string& Win::GetTitle() const
{
return title;
}
Aber Achtung! Sollten Sie Ihre Klasse auf zwei Dateien aufgeteilt haben (HeaderDatei mit der Klassendefinition und CPP-Datei mit den Definitionen der
Memberfunktionen), so müssen die inline-Memberfunktionen immer mit in die HeaderDatei aufgenommen werden, damit der Compiler den einzusetzenden Code kennt.
Einschränkungen bei inlines
Wie bereits mehrfach erwähnt, bestehen bei inline-Funktionen bzw. Memberfunktionen gewisse
Einschränkungen. So sind z.B. rekursive Funktionen (das sind Funktionen die sich selbst
aufrufen) in der Regel nicht als inline-Funktionen zugelassen. Auch schließen einige Compiler
bestimmte Anweisungen innerhalb von inline-Funktionen aus. Beim BORLAND-Compiler
waren dies fast alle Anweisungen die irgendwelche Sprünge verursachen wie z.B. Schleifen.
Enthält eine inline-Memberfunktion eine nicht zugelassene Anweisung, so wird die Funktion
bzw. Memberfunktion nicht als inline betrachtet. Womit wir auch schon beim nächsten und
wichtigsten Punkt wären. Die Definition einer Memberfunktion als inline ist nur eine Bitte an
den Compiler, die Funktion/Memberfunktion entsprechend einzubauen und keine zwingende
Vorschrift. Und zum Schluss ist noch anzumerken, dass moderne Compiler versuchen den Code
eines Moduls so gut wie möglich zu optimieren. Und dazu gehört teilweise die eigenständige
Definition einer Funktion als inline-Funktion wenn sie im gleichen Modul (CPP-Datei)
verwendet wird. Aber wie so oft lässt sich auch hierbei sehr viel über entsprechende
Einstellungen des Compilers einstellen. Da hilft nur ein Blick ins Handbuch bzw. in die OnlineHilfe.
http://www.cpp-tutor.de/cpp/le10/le10_02.htm (4 von 5) [17.05.2005 11:39:19]
Inlines
So, und das war's auch schon wieder. Ganz ohne weiteres Beispiel und Übung.
In der nächsten Lektion geht's dann nochmals um Parameter von Funktionen und
Memberfunktionen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_02.htm (5 von 5) [17.05.2005 11:39:19]
Default-Parameter
Default-Parameter
Die Themen:
Syntax
Aufruf der Funktion
Definition der Defaultparameter
Funktionserweiterung durch Default-Parameter
Beispiel und Übung
Syntax
Die nachfolgenden Ausführungen beziehen sich sowohl auf Memberfunktionen wie auch auf
Funktionen. Wenn im Folgenden von Funktionen gesprochen wird, so sind damit auch immer
Memberfunktionen gemeint.
C++ erlaubt es, Parameter mit Defaultwerten zu belegen. Parameter die mit einem Defaultwert
versehen sind, können beim Aufruf der Funktion weggelassen werden. In diesem Fall erhält dann die
Funktion den angegebenen Defaultwert.
Um einen Parameter mit einem
Defaultwert zu versehen, wird
nach dem Parameternamen der
Zuweisungsoperator angegeben,
gefolgt vom Defaultwert. Im
Beispiel rechts wird der letzte
Parameter symbol z.B. mit dem
Defaultzeichen '#' belegt. Zu den
dann möglichen Aufrufen der
Funktion kommen wir gleich
noch.
void DrawRect(short xPos, short yPos,
short width, short height,
char symbol = '#')
{
....
}
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (1 von 8) [17.05.2005 11:39:20]
Default-Parameter
Die Anzahl der Default-Parameter
ist nicht begrenzt, jedoch ist dabei
Folgendes zu beachten: erhält
einer der Parameter einen
Defaultwert, so müssen alle rechts
davon stehenden Parameter
ebenfalls Defaultwerte besitzen.
Dies müssen Sie bereits bei der
Spezifikation der Funktion
beachten. Als Defaultwerte sind
nur Literale oder Konstanten
zulässig. Und selbstverständlich
muss der Datentyp des
Defaultwertes mit dem Datentyp
des Parameters übereinstimmen.
Das erste Beispiel rechts ist in
Ordnung, hier erhalten die
Parameter height und symbol einen
Defaultwert. Das zweite Beispiel
hingegen ist fehlerhaft. Der
Parameter height erhält hier
ebenfalls einen Defaultwert, aber
der rechts davon befindliche
Parameter symbol keinen. Und das
ist laut obiger Aussage nicht
zulässig.
void DrawRect(short xPos, short yPos,
short width, short height = 1,
char symbol = '#')
{
....
}
Aber so geht's nicht!
void DrawRect(short xPos, short yPos,
short width, short height = 1,
char symbol)
{
....
}
Aufruf der Funktion
Sehen wir uns nun den Aufruf
einer Funktion mit fünf
Parametern an, wobei die letzten
zwei Parameter Defaultwerte
besitzen. Der erste Aufruf
übergibt an die Funktion nur 3
Parameter. Die beiden fehlenden
Parameter height und symbol
werden dann mit den
Defaultwerten belegt. Die
Funktion wird hier eine Linie mit
dem Standardzeichen '#' zeichnen.
Der zweite Aufruf übergibt nun
zusätzlich die Höhe height, und
damit zeichnet die Funktion ein
Rechteck mit dem
Standardzeichen. Der dritte
Aufruf übergibt, wie bisher
void DrawRect(short xPos, short yPos,
short width, short height = 1,
char symbol = '#')
{
....
}
Erlaubte Aufrufe:
DrawRect(10,10,100);
entspricht dem Aufruf
DrawRect(10,10,100,1,'#');
DrawRect(10,10,100,100);
entspricht dem Aufruf:
DrawRect(10,10,100,100,'#');
Aber so geht' nicht:
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (2 von 8) [17.05.2005 11:39:20]
Default-Parameter
üblich, alle Parameter an die
Funktion.
DrawRect(10,10,100,,'*');
Eines müssen Sie noch beim
Aufruf von Funktionen mit
Defaultparametern beachten.
Wird einer der Defaultwerte beim
Aufruf überschrieben, so müssen
alle links davon stehenden
Parameter ebenfalls angegeben
werden. Beim letzten Aufruf im
Beispiel rechts wird der letzte
Defaultwert überschrieben. Und
damit müssen Sie für den links
davon stehenden Parameter height
ebenfalls einen Wert angeben.
Definition der Defaultparameter
Kommen wir nochmals auf die
Spezifikation der DefaultParameter zurück. Die Angabe
der Default-Parameter kann wie
rechts dargestellt auf zweierlei
Arten erfolgen:
Default-Parameter bei Deklaration
// Deklaration
void DrawRect(short xPos, short yPos,
short width, short height = 1,
char symbol = '#');
bei der Deklaration der
// Definition
Funktion/Memberfunktion void DrawRect(short xPos, short yPos,
short width, short weight,
oder bei ihrer Definition
char symbol)
{
....
Sie dürfen den Default-Parameter
}
aber nur einmal angeben, d.h.
entweder bei der Deklaration oder
bei der Definition; ansonsten
Default-Parameter bei Definition
erhalten Sie eine Fehlermeldung
vom Compiler.
// Deklaration
Geben Sie die Default-Parameter void DrawRect(short xPos, short yPos,
short width, short height,
aber nach Möglichkeit bei der
char symbol);
Deklaration an. In der Regel
// Definition
werden Sie die Deklaration bei
void DrawRect(short xPos, short yPos,
größeren Projekten in einer
short width, short height = 1,
Header-Datei vornehmen, die
char symbol = '#')
dann von den entsprechenden
{
Modulen eingebunden wird, die
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (3 von 8) [17.05.2005 11:39:20]
Default-Parameter
die Funktion verwenden. Und nur
}
wenn die Default-Parameter in
dieser Header-Datei stehen, sind
sie auch in allen Modulen auch
bekannt.
....
Funktionserweiterung durch Default-Parameter
Zum Abschluss dieser Lektion (das Beispiel und die Übung kommen aber gleich noch) noch ein kleiner
Tipp.
Default-Parameter eignen sich
auch hervorragend dazu, eine
bereits bestehende Funktion zu
erweitern. Sehen Sie sich das
Ausgangsbeispiel rechts an. Das
Beispiel enthält eine Funktion
SortIt(...), die ein char-Feld in
aufsteigender Reihenfolge
sortieren soll.
In weiteren Verlaufe der
Programmentwicklung kommt
nun die Anforderung, dass das
char-Feld auch wahlweise in
fallender Reihenfolge sortiert
werden soll. Hierzu muss der
Funktion ein neuer Parameter
übergeben werden, der die
Sortierrichtung beschreibt. Damit
wären dann aber auch alle
bisherigen Funktionsaufrufe
entsprechend zu korrigieren.
Ausgangsbeispiel
// Funktionsdeklaration
void SortIt(char* array);
// Hauptprogramm
int main()
{
....
SortIt(myArray);
....
}
// Funktionsdefinition
void SortIt(char* array)
{
.... //aufsteigend sortieren
}
Mithilfe von Default-Parameter ist es jetzt jedoch möglich, lediglich die Funktion an die neue
Anforderung anzupassen und bereits bestehende Funktionsaufrufe unverändert zu lassen.
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (4 von 8) [17.05.2005 11:39:20]
Default-Parameter
Dazu wird der für die
Sortierrichtung notwendige
Parameter mit einem Defaultwert
versehen, der die Sortierrichtung
aufsteigend beschreibt. Wie Sie in
der Zwischenzeit ja wissen,
können beim Funktionsaufruf
Parameter mit Defaultwerten
weggelassen werden. D.h. wird die
Funktion, wie bisher gewohnt, mit
nur einem Parameter aufgerufen,
wird das char-Feld aufsteigend
sortiert. Soll das Feld dagegen
fallend sortiert werden, so ist beim
Aufruf der Funktion ein vom
Defaultwert abweichender Wert
anzugeben.
Die erweiterte Funktion
// Funktionsdeklaration
void SortIt(char* array, bool dir=true);
// Hauptprogramm
int main()
{
....
SortIt(myArray);
// aufsteigend
....
SortIt(myArray,false); // fallend
}
// Funktionsdefinition
void SortIt(char* array, bool dir)
{
if (dir)
.... //aufsteigend sortieren
else
.... //fallend sortieren
}
Ja und das war's auch schon.
Beispiel und Übung
Das Beispiel:
Im Beispiel wird eine Funktion zur Berechnung der Fläche eines Rechtecks und des Rauminhalts
eines Quaders verwendet. Der 'Trick' der hierbei verwendet wird besteht darin, dass die Fläche dem
Rauminhalt entspricht wenn für die Höhe der Wert 1 eingesetzt wird. Demzufolge wird für den
Höhen-Parameter als Defaultwert 1 vorgegeben.
Das unten stehende Beispiel finden Sie auch unter: le10\prog\Bdefpar
Die Programmausgabe:
Daten (L: 10 B: 11 H: 12)
Rauminhalt: 1320
Daten (L: 10 B: 11 H: 1)
Flaeche: 110
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (5 von 8) [17.05.2005 11:39:20]
Default-Parameter
Das Programm:
// C++ Kurs
// Beispiel zu Default-Parameter
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Prototyping der Funktion zur Berechnung des Rauminhalts
unsigned long CalcCube (unsigned short lenght, unsigned short width,
unsigned short height=1);
//
// HAUPTPROGRAMM
// =============
int main()
{
// Rauminhalt berechnen
cout << "Rauminhalt: " << CalcCube(10,11,12) << endl;
// Flaeche berechnen
cout << "Flaeche: " << CalcCube(10,11) << endl;
}
//
// Funktion: CalcCube
// Eingang : Laenge, Breite, Hoehe des Koerpers
// Ausgang : Rauminhalt der Koerpers
unsigned long CalcCube(unsigned short length, unsigned short width,
unsigned short height)
{
cout << "Daten (L: " << length;
cout << " B: " << width;
cout << " H: " << height << ")\n";
return length * width * height;
}
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (6 von 8) [17.05.2005 11:39:20]
Default-Parameter
Die Übung:
Schreiben Sie eine Klasse zur Darstellung eines Rechtecks. Ein Rechteck soll die Eigenschaften
Länge, Breite, Position und Farbe besitzen, wobei sich die Farbinformation wiederum aus einen Rot, Grün- und Blauanteil zusammensetzt.
Das Rechteck soll verschoben und in seiner Größe verändert werden können. Zusätzlich soll noch
die Farbe des Rechtecks einstellbar sein.
Ferner ist eine Memberfunktion zu schreiben, um das Rechteck darzustellen. Da wir hier aber keine
Grafik-Programmierung betreiben wollen, sind lediglich die Eigenschaften des Rechtecks (so wie
unten dargestellt) auszugeben.
Um die Eigenschaften des Rechtecks zu initialisieren ist eine entsprechende Memberfunktion zu
verwenden. Hierbei soll es möglich sein, auch ein Rechteck ohne Angabe einer Farbinformation
anzulegen.
Diese Übung sollte Ihnen eigentlich bekannt vorkommen. Sie wurde in ähnlicher Form in der
Lektion über einfache Klassen schon aufgeführt.
Die Programmausgabe:
1. Rechteck:
Position: 10,10
Groesse : 640,480
RGB-Wert: 0,0,0
2. Rechteck:
Position: 100,50
Groesse : 800,600
RGB-Wert: 0x80,0x80,0x80
1. Rechteck:
Position: 20,20
Groesse : 640,480
RGB-Wert: 0,0,0
2. Rechteck:
Position: 100,50
Groesse : 1024,786
RGB-Wert: 0xc0,0xc0,0xc0
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le10\Ldefpar.
In der nächsten Lektion erfahren Sie, wie Sie unter anderem Eigenschaften definieren können die für
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (7 von 8) [17.05.2005 11:39:20]
Default-Parameter
alle Objekte einer Klasse gemeinsam gültig sind.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_03.htm (8 von 8) [17.05.2005 11:39:20]
Statische Member
Statische Member
Die Themen:
Statische Eigenschaften
Statische Memberfunktionen
Beispiel und Übung
Statische Eigenschaften
Das Besondere an statischen Eigenschaften ist, dass diese Eigenschaften zunächst für alle Objekte einer Klasse nur
ein einziges Mal vorhanden sind. Des weiteren werden statische Eigenschaften standardmäßig immer mit 0
initialisiert und auf sie kann auch ohne Bezug auf ein bestimmtes Objekt zugegriffen werden (wenn sie public sind).
Um eine statische Eigenschaft zu definieren sind zwei Schritte erforderlich:
// Klassendefinition
class Window
Zuerst muss innerhalb der Klasse die
statische Eigenschaft deklariert werden. Bei der {
....
Deklaration ist vor dem Datentyp der
Eigenschaft das Schlüsselwort static anzugeben.
static short noOfWin;
....
};
Da die statische Eigenschaft für alle
Definition der statischen Eigenschaft
//
Objekte der Klasse nur einmal vorhanden ist,
muss sie außerhalb der Klasse definiert werden.
short Window::noOfWin = 0;
Die Definition erfolgt prinzipiell genauso wie
die Definition einer 'normalen' Variablen,
jedoch wird vor dem Eigenschaftsnamen noch
die zugehörige Klasse angegeben. Beachten Sie
bitte, dass hier nicht mehr das Schlüsselwort
static stehen darf!
Selbstverständlich können Sie eine statische Eigenschaft bei ihrer Definition auch mit einem Wert
initialisieren, so wie oben angegeben.
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (1 von 7) [17.05.2005 11:39:21]
Statische Member
Der Zugriff auf eine statische Eigenschaft aus
einer Memberfunktion seiner Klasse heraus
erfolgt gleich wie der Zugriff auf normale
Eigenschaften. Ist die statische Eigenschaft
public, so kann auch von außerhalb der Klasse
ohne Bezug auf ein Objekt auf diese
Eigenschaft zugegriffen werden. In diesem Fall
müssen Sie (wie rechts unten angegeben) den
Klassennamen vor den Eigenschaftsnamen
stellen. Denken Sie immer daran: eine statische
Eigenschaft besitzt keine Objektzugehörigkeit!
Im Beispiel rechts sehen Sie einen
Anwendungsfall für eine statische Eigenschaft.
Die Eigenschaft noOfWin wird hier als Zähler
für die Anzahl der erstellen Fensterobjekte
verwendet. Da statische Eigenschaften
standardmäßig mit 0 initialisiert werden,
braucht im Konstruktor dieser Zähler einfach
nur um eins erhöht und im Destruktor um eins
erniedrigt werden. Selbstverständlich sollte in
der Praxis dieser Zähler nicht wie rechts
angegeben public sein. Dies dient nur zur
Demonstration des direkten Zugriffs auf eine
statische Eigenschaft.
// Klassendefinition
class Window
{
....
public:
Window();
~Window();
static short noOfWin;
....
};
// Definition der statischen Eigenschaft
short Window::noOfWin = 0;
// Zugriff aus Memberfunktion heraus
Window::Window()
{
....
noOfWin++;
}
Window::~Window()
{
....
noOfWin--;
}
// Hauptprogramm, direkter Zugriff
int main()
{
....
if (Window::noOfWin)
....
}
Statische Memberfunktionen
Sehen wir uns nun die statischen Memberfunktionen einer Klasse an. Statische Memberfunktionen besitzen eine
Reihe von Einschränkungen:
●
●
●
●
●
Sie haben nur Zugriff auf statische Member (Eigenschaften und weitere Memberfunktionen) einer Klasse.
Statische Memberfunktion haben keinen this-Zeiger (wird in der nächsten Lektion gleich erklärt).
Es darf keine statische und nicht-statische Memberfunktion mit der gleichen Signatur (Name plus
Parameter) geben. Dies spielt bei dem später noch aufgeführten Überladen eine Rolle.
Statische Memberfunktionen können nicht virtuell sein (auch das wird später noch erklärt).
Und zum Schluss: statische Memberfunktionen können nicht const oder volatile sein.
Wozu statische Memberfunktionen trotz dieser vielen Einschränkungen dennoch nützlich sein können, das erfahren
Sie gleich noch. Doch sehen wir uns zunächst an, wie eine statische Memberfunktion definiert wird.
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (2 von 7) [17.05.2005 11:39:21]
Statische Member
Um eine Memberfunktion als statische
// Klassendefinition
Memberfunktion zu definieren, wird bei ihrer class Window
Deklaration/Definition innerhalb der Klasse das {
Schlüsselwort static vorangestellt. Wird die
....
Memberfunktion außerhalb der Klasse
static short noOfWin;
definiert, so darf auch hier das Schlüsselwort
public:
static nicht mehr angegeben werden (siehe
static short GetNoOfWin();
Beispiel rechts).
....
};
Beachten Sie aber, dass statische
// Definition der statischen Eigenschaft
Memberfunktionen nur Zugriff auf statische
short Window::noOfWin = 0;
Member haben!
// Definition der statischen Memberfunktion
void Window::GetNoOfWin()
{
return noOfWin;
}
Der Aufruf einer statischen Memberfunktion
kann auf drei verschiedene Arten erfolgen:
// Klassendefinition
class Window
{
● entweder durch sich selbst (hier nicht
.... // Definitionen wie oben
aufgeführt, rekursive Memberfunktion!) };
● ohne Bezug auf ein Objekt, in dem beim
// Definition der statischen Memberfunktion
Aufruf vor dem Name der
void Window::SetOverallColor()
Memberfunktion der Klassenname
{
gestellt wird.
return noOfWin;
● in Verbindung mit einem beliebigen
}
Objekt.
int main()
{
Window myWin;
Die letzten beiden Fälle sind rechts im Beispiel
short winCount;
dargestellt.
....
winCount = Window::GetNoOfWin();
winCount = myWin.GetNoOfWin();
....
}
Und das war's auch schon. Nun folgt das Beispiel für den Einsatz einer solchen statischen Memberfunktion und
dann Ihre Übung.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (3 von 7) [17.05.2005 11:39:21]
Statische Member
Das Beispiel:
Das Beispiel zeigt die Anwendung von statischen Eigenschaften anhand einer Fensterklasse auf.
Alle Fenster der Fensterklasse sollen immer eine gemeinsame Titel- und Fensterfarbe besitzen. Die Farbwerte
werden dazu als statische unsigned long Daten innerhalb der Fensterklasse definiert. Zum Setzen der Farben
werden zwei statische Memberfunktionen SetTitleColor(...) und SetBackColor(...) verwendet. Außer den
Farbwerten enthält die Fensterklasse noch ein nicht-statisches Datum für den Fenstertitel.
Innerhalb des Hauptprogramms werden zuerst die beiden Farben gesetzt. Das Setzen der Farbwerte erfolgt
hierbei direkt ohne Bezug auf ein Fensterobjekt. Anschließend werden zwei Fensterobjekte definiert und dann
deren Fenstereigenschaften ausgegeben. Danach wird die Fensterfarbe über ein Fensterobjekt umgesetzt. Da die
Fensterfarbe eine statische Eigenschaft ist, wirkt sich dieses aber auf alle Fensterobjekte aus.
Das unten stehende Beispiel finden Sie auch unter: le10\prog\Bstatmem
Die Programmausgabe:
Titelfarbe: 0xff, Titel: Mein Fenster
Fensterfarbe: 0x808080
Titelfarbe: 0xff, Titel: Dein Fenster
Fensterfarbe: 0x808080
Titelfarbe: 0xff, Titel: Mein Fenster
Fensterfarbe: 0x80ff
Titelfarbe: 0xff, Titel: Dein Fenster
Fensterfarbe: 0x80ff
Das Programm:
// C++ Kurs
// Beispiel zu Default-Parameter
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Fensterklasse
// Alle Fenster der Klassen haben eine gemeinsame
// Titel- und Fensterfarbe aber unterschiedliche Titel
class Window
{
static unsigned long titleColor;
// Titelfarbe
static unsigned long winColor;
// Fensterfarbe
string title;
// Fenstertitel
public:
// ctor
// Beachte: string ist ein Objekt und sollte deshalb
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (4 von 7) [17.05.2005 11:39:21]
Statische Member
// per Initialisiererliste initialisiert werden
Window(const char* t): title(t)
{}
void Draw();
// statische Memberfunktion zum Setzen der Farben
static void SetTitleColor(unsigned char r, unsigned char g, unsigned char b);
static void SetBackColor(unsigned char r, unsigned char g, unsigned char b);
};
// Statische Eigenschaften der Window-Klasse definieren
unsigned long Window::titleColor;
unsigned long Window::winColor;
// Fenstereigenschaften ausgeben
void Window::Draw()
{
cout << std::hex << std::showbase;
cout << "Titelfarbe: " << titleColor;
cout << ", Titel: " << title << endl;
cout << "Fensterfarbe: " << winColor << endl;
cout << std::dec << std::noshowbase;
}
// Titelfarbe setzen
void Window::SetTitleColor(unsigned char r, unsigned char g, unsigned char b)
{
titleColor = (r<<16) + (g<<8) + b;
}
// Fensterfarbe setzen
void Window::SetBackColor(unsigned char r, unsigned char g, unsigned char b)
{
winColor = (r<<16) + (g<<8) + b;
}
// Hauptprogramm
// =============
int main()
{
// Fenster- und Titelfarbe setzen
Window::SetBackColor(0x80,0x80,0x80);
Window::SetTitleColor(0x00,0x00,0xff);
// Zwei Fenster definieren
Window myWin("Mein Fenster");
Window yourWin("Dein Fenster");
// und deren Eigenschaften ausgebne
myWin.Draw();
yourWin.Draw();
cout << endl;
// Fensterfarbe umsetzen
// Beachte: Es wird die Fensterfarbe fuer alle Fenster
// umgesetzt da die Fensterfarbe eine statische Eigenschaft ist
myWin.SetBackColor(0x00,0x80,0xff);
// Fenstereigenschaften erneut ausgeben
myWin.Draw();
yourWin.Draw();
}
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (5 von 7) [17.05.2005 11:39:21]
Statische Member
Die Übung:
Entwickeln Sie eine Klasse zur fiktiven Ansteuerung einer Schnittstelle für Ausgaben. Es sollen beliebig viele
Objekte von dieser Klasse erstellt werden können.
Da wir uns hier aber nicht mit der Ansteuerung einer realen Schnittstelle befassen wollen, sollen die Ausgaben
auf die Schnittstelle als Bildschirmausgaben erscheinen. Für die Ausgabe auf die Schnittstelle ist eine
entsprechende Memberfunktion vorzusehen, die als Parameter den auszugebenden Text erhalten soll.
Hat ein Objekt die Schnittstelle für eine Ausgabe erst einmal belegt, so belegt es die Schnittstelle solange, bis es
diese über eine ebenfalls zu schreibende Memberfunktion explizit wieder freigibt. Versuchen andere Objekte in
der Zwischenzeit auf eine belegte Schnittstelle etwas auszugeben, so erfolgt die Ausgabe einer Fehlermeldung.
Erzeugen Sie zwei Schnittstellen-Objekte. Geben Sie dann über das erste Objekt zweimal hintereinander einen
beliebigen Text aus. Beide Ausgaben müssen erfolgreich durchgeführt werden.
Nun versuchen Sie eine Ausgabe über das zweite Objekt. Diese Ausgabe sollte blockiert sein, da das erste Objekt
noch die Schnittstelle belegt hat.
Anschließend gibt das erste Objekt die Schnittstelle frei und das zweite Objekt versucht nun erneut zwei
Ausgaben über die Schnittstelle vorzunehmen. Beide Ausgaben sollten nun erfolgreich durchgeführt werden, da
das erste Objekt die Schnittstelle wieder freigegeben hat.
Bei richtiger Lösung werden Sie eine Ausgabe in etwa der Form erhalten, wie sie unten dargestellt ist.
Die Programmausgabe:
1. Schreiben von Objekt A
2. Schreiben von Objekt A
FEHLER: COM-Port belegt!
Objekt A gibt COM wieder frei
2. Schreiben von Objekt B
3. Schreiben von Objekt B
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die
Lösung unter loesungen\le10\Lstatmem.
In der nächsten Lektion lernen Sie einen versteckten Parameter von Memberfunktionen kennen, den this-Zeiger.
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (6 von 7) [17.05.2005 11:39:21]
Statische Member
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_04.htm (7 von 7) [17.05.2005 11:39:21]
this-Zeiger
this-Zeiger
Die Themen:
this-Zeiger
Beispiel
this-Zeiger
Jede nicht-statische Memberfunktion erhält als versteckten Parameter einen Zeiger auf das
Objekt, in dessen Kontext sie aufgerufen wurde. Dieser Zeiger wird this-Zeiger genannt. Er ist
immer vom Typ 'Zeiger auf eigene Klasse'.
Sehen Sie sich einmal das
Beispiel rechts an. Dort wird
eine minimale Klasse
definiert, die nur aus einer
einzigen Memberfunktion
Anything(...) besteht.
Im Hauptprogramm werden
dann zwei Objekte dieser
Klasse definiert.
Anschließend werden die
Adressen beider Objekte
ausgegeben, was letztendlich
nichts anderes bedeutet, als
die Ausgabe der Zeiger auf
diese Objekte.
Danach rufen beide Objekte
ihre Memberfunktion
Anything(...) auf. In dieser
Memberfunktion wird dann
der Inhalt des this-Zeigers
// Klassendefinition
class Any
{
public:
void Anything();
};
void Any::Anything()
{
// Inhalt des this-Zeigers ausgeben
cout << "this=" << this << endl;
}
int main()
{
// 2 Objekte definieren
Any obj1, obj2;
cout << hex;
// Adr. des Objekte ausgeben
cout << "Adr. obj1=" << &obj1 << endl;
cout << "Adr. obj2=" << &obj2 << endl;
obj1.Anything();
obj2.Anything();
http://www.cpp-tutor.de/cpp/le10/le10_05.htm (1 von 5) [17.05.2005 11:39:22]
this-Zeiger
ebenfalls ausgegeben. Und da }
dieser Zeiger laut obiger
Aussage auf das aufrufende
Objekt zeigt, werden hier die
gleichen Adressen wie in
main(...) ausgegeben.
Adr. obj1=006AFDF4
Adr. obj2=006AFDF0
this=006AFDF4
this=006AFDF0
Vielleicht fragen Sie sich jetzt, für das dieser Zeiger eigentlich eingesetzt wird? Nun, dieser Zeiger
wird immer dann verwendet, wenn ein Zeiger oder eine Referenz auf das aktuelle Objekt
innerhalb einer Memberfunktion benötigt wird. Aber sehen wir uns dies am Besten anhand eines
Beispiels an.
Beispiel
Das Beispiel:
Es soll eine Funktion (keine Memberfunktion) zur Ausgabe einer Meldung auf dem Bildschirm
geschrieben werden. Die Ausgabe soll dabei mit einer bestimmten Vorder- und
Hintergrundfarbe erfolgen. Die Funktion erhält als Parameter zunächst den auszugebenden
Text. Die Bestimmung der Farbe für die Ausgabe erfolgt nach folgendem Verfahren:
●
●
Wird die Funktion aus einer Memberfunktion der ebenfalls im Beispiel definierten
Fensterklasse Window heraus aufgerufen, so wird die Meldung in den Fensterfarben
dargestellt.
Wird die Funktion anderweitig, z.B. aus main(...) heraus, aufgerufen, wird eine
Standardfarbe für die Ausgabe verwendet.
Damit die Funktion nun feststellen kann, von wo aus sie aufgerufen wurde, erhält sie als
zweiten Parameter einen Window-Zeiger. Wird die Funktion aus einer Window
Memberfunktion heraus aufgerufen, so übergibt die Memberfunktion an die Ausgabefunktion
einen Zeiger auf das aktuelle Objekt, das die Memberfunktion aufgerufen hat. Und dies ist der
this-Zeiger. Über diesen Zeiger kann die Ausgabefunktion dann die aktuelle Vorder- und
Hintergrundfarbe auslesen. Wird die Funktion nicht aus einer Window Memberfunktion
heraus aufgerufen, so wird für den Window-Zeiger ein NULL-Zeiger übergeben. In diesem Fall
wird eine Standardfarbe für die Ausgabe verwendet. Damit nun bei einem allgemeinen Aufruf
nicht immer der NULL-Zeiger angegeben werden muss, wird dieser Parameter standardmäßig
mit NULL vorbelegt (Default-Parameter).
http://www.cpp-tutor.de/cpp/le10/le10_05.htm (2 von 5) [17.05.2005 11:39:22]
this-Zeiger
Im Hauptprogramm wird ein Window Objekt definiert und dessen Memberfunktion
DoAnything(...) aufgerufen, die dann wiederum die Ausgabefunktion aufruft. Die
Ausgabefunktion erhält nun (außer dem auszugebenden Text) als Parameter den Zeiger auf
das aktuelle Objekt (this-Zeiger) damit die Funktion nachher dessen Fensterfarben auslesen
kann. Im Anschluss an die Ausgabe einer Meldung über das Fensterobjekt wird im
Hauptprogramm noch eine weitere Meldung ausgegeben, wobei der letzte Parameter (WindowZeiger) beim Aufruf entfällt (Default-Parameter).
Das unten stehende Beispiel finden Sie auch unter: le10\prog\Bthis
Die Programmausgabe:
Vordergrund: 0x112233, Hintergrund: 0x0000ff
Text ist : Fenstermeldung!
Vordergrund: 0x000000, Hintergrund: 0xffffff
Text ist : Meldung von main()!
Das Programm:
// C++ Kurs
// Beispiel zum this-Zeiger
//
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
// Vorwaertsdeklaration wegen nachfolgendem
// Prototyping der Funktion DisplayMessage(...)
class Window;
// Prototypings
void DisplayMessage(const char* const pMsg, Window *pParent = NULL);
// Klassendefinition
class Window
{
unsigned long foreColor, backColor;
public:
Window(unsigned long fc, unsigned long bc);
void GetColor(unsigned long &fc, unsigned long &bc);
void DoAnything();
};
// Definition der Memberfunktionen
// Konstruktor
http://www.cpp-tutor.de/cpp/le10/le10_05.htm (3 von 5) [17.05.2005 11:39:22]
this-Zeiger
Window::Window(unsigned long fc, unsigned long bc)
{
foreColor = fc; backColor = bc;
}
// Fensterfarben zurueckliefern
void Window::GetColor(unsigned long &fc, unsigned long &bc)
{
fc = foreColor; bc = backColor;
}
// Ausgabe einer Meldung
void Window::DoAnything()
{
DisplayMessage("Fenstermeldung!",this);
}
// Funktion zur Ausgabe der Meldung
void DisplayMessage(const char* const pMsg,Window *pParent)
{
unsigned long fc, bc;
// Falls Aufruf der Funktion nicht von einem Fenster aus
if (pParent == NULL)
{
// Standardfarben fuer Ausgabe verwenden
fc = 0UL;
bc = 0xFFFFFFUL;
}
else
// sonst Fensterfarben verwenden
pParent->GetColor(fc, bc);
// Meldung ausgeben
cout << std::hex << std::setfill('0');
cout << "Vordergrund: 0x" << std::setw(6) << fc;
cout << ", Hintergrund: 0x" << std::setw(6) << bc << endl;
cout << std::dec << std::setfill(' ');
cout << "Text ist
: " << pMsg << endl;
}
// Hauptprogramm
// =============
int main()
{
// Fensterobjekt erstellen
Window myWin(0x112233UL,0x0000FFUL);
// Memberfunktion des Fensters aufrufen
myWin.DoAnything();
// Meldung ausgaben
DisplayMessage("Meldung von main()!");
}
http://www.cpp-tutor.de/cpp/le10/le10_05.htm (4 von 5) [17.05.2005 11:39:22]
this-Zeiger
Auf eine Übung können wir hier getrost verzichten, da uns dieser Zeiger in den nachfolgenden
Lektionen noch öfters begegnen wird.
Und damit kommen wir zur letzten Lektion in diesem Kapitel, die sich mit dem Thema
Eingeschlossene Objekte befasst.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_05.htm (5 von 5) [17.05.2005 11:39:22]
Eingeschlossene Objekte
Eingeschlossene Objekte
Die Themen:
Definition des eingeschlossenen Objekts
Aufruf des Konstruktors des eingeschl. Objekts
Zugriff auf Member des eingeschl. Objekts
Definition des eingeschlossenen Objekts
Ein eingeschlossenes
Objekt ist ein Objekt das in
einer Klasse enthalten ist.
Es kann jedem beliebigem
Klassentyp angehören,
außer natürlich der sie
umschließenden
Klasse. Das Einschließen
von Objekten wird auch als
Layering, Containment
oder Embedding
bezeichnet. Es bietet sich
immer dann, wenn die
Klassen zueinander eine hateine (has a) Beziehung
haben. Im Beispiel rechts
hat das Fenster
(umschließende Klasse
Window) eine Farbe
(eingeschlossene Klasse
Color).
class Color
{
....
public:
Color(int col);
};
// Umschliessende Klasse
class Window
{
Color winCol; // Eingeschl. Objekt
....
};
Das Einschließen eines
Objektes haben Sie im
Verlaufe des Kurses schon
http://www.cpp-tutor.de/cpp/le10/le10_06.htm (1 von 4) [17.05.2005 11:39:23]
Eingeschlossene Objekte
mehrfach angewandt,
nämlich dann, wenn Sie
innerhalb einer Klasse ein
string Objekt definiert
haben. (Nu ja, im strengen
Sprachgebrauch ist string
eigentlich keine Klasse
sondern nur ein typedef für
das Template
basic_string<char>. Mehr
zu Templates später noch
im Kurs).
Aufruf des Konstruktors des eingeschl. Objekts
Benötigt der Konstruktor
der Klasse des
eingeschlossenen Objekts
Parameter, so müssen Sie
diesen in der
Initialisiererliste des
Konstruktors der
umschließenden Klasse
aufrufen.
Nicht erlaubt ist es laut
Standard, das
eingeschlossene Objekt bei
seiner Definition bereits zu
initialisieren, d.h. folgende
Anweisung würde damit
einen Übersetzungsfehler
verursachen:
// Einzuschliessende Klasse
class Color
{
....
public:
Color(int col);
};
// Umschliessende Klasse
class Window
{
Color winCol; // Eingeschl. Objekt
....
public:
Window(..,int col): winCol(col)
{...}
};
class Window
{
....
Color
winCol(10); //
Fehler!
...
};
http://www.cpp-tutor.de/cpp/le10/le10_06.htm (2 von 4) [17.05.2005 11:39:23]
Eingeschlossene Objekte
Zugriff auf Member des eingeschl. Objekts
Der Zugriff auf die public
Member der
eingeschlossenen Klasse aus
Memberfunktionen der
umschließenden Klasse
heraus erfolgt in der Art,
dass zuerst der Name des
eingeschlossenen Objekts
angegeben wird, dann folgt
der Punktoperator und
dann das entsprechende
Member. Also genau so, wie
Sie sonst auf die public
Member eines Objekts von
außen zugreifen.
class Color
{
....
public:
void SetColor(...);
....
};
// Umschliessende Klasse
class Window
{
public:
Color winCol;
Window();
....
};
// ctor der umschliessenden Klasse
Soll von außerhalb der
umschließenden Klasse auf Window::Window()
{
Member des
....
eingeschlossenen Objekts
winCol.SetColor(...);
zugegriffen werden, so
}
erfolgt der Zugriff
// Fensterobjekt definieren
'zweistufig'. Zuerst wird
Window winObj;
der Name des
// Zugriff ueber aeusseres Objekt
umschließenden Objekts
angegeben, dann der Name winObj.winCol.SetColor(...);
des eingeschlossenen
Objekts und zum Schluss
das gewünschte Member.
Dieser Zugriff funktioniert
allerdings nur dann, wenn
das eingeschlossene Objekt
und das Member public
sind. Ist das eingeschlossene
Objekt als private definiert,
haben Sie über das
umschließende Objekt
keinen Zugriff auf das
eingeschlossene Objekt.
Durch die private Definition
des eingeschlossenen
http://www.cpp-tutor.de/cpp/le10/le10_06.htm (3 von 4) [17.05.2005 11:39:23]
Eingeschlossene Objekte
Objekts können Sie so die
Schnittstelle des
eingeschlossenen Objekts
nach außen hin verbergen
und damit die Anwendung
von dessen Schnittstellen
'entkoppeln'. Soll von der
Anwendung aus das
eingeschlossene Objekt
manipuliert werden
können, so muss die
umschließende Klasse
hierfür eine eigene
Schnittstelle zur Verfügung
stellen.
Noch angemerkt werden soll an dieser Stelle, dass es auch möglich ist, eine Klasse in einer
anderen Klasse einzuschließen. Da dieser Fall aber in der Praxis eher selten auftritt, wird auf
hier auf eine weitere Erläuterung diesbezüglich verzichtet um damit nicht noch völlig
Verwirrung auszulösen.
Und damit beenden wir diese Lektion und Lerneinheit. Die nächste Lerneinheit ist eine relativ
kurze, dafür aber umso wichtigere, Lerneinheit. Sie befasst sich mit dem dynamischen Anlegen
von Variablen, Feldern und Objekten.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le10/le10_06.htm (4 von 4) [17.05.2005 11:39:23]
Operatoren new und delete
Operatoren new und delete
Die Themen:
new Operator
delete Operator
Beispiel und Übung
new Operator
Einführung
new und delete sind Operatoren (so wie + oder *), und daher werden auch keine zusätzlichen HeaderDateien für eventl. Funktionsdeklarationen benötigt. Sehen wir uns zuerst die Reservierung von
Speicher mittels new an, und dann wie dieser reservierte Speicher mit delete wieder freigegeben wird.
new für einfache Daten und Felder
new erhält als Operand den
// Speicher für einen long-Wert reservieren
Datentyp, für den der Speicher
long *pVar = new long;
reserviert werden soll. Als
Ergebnis liefert new einen Zeiger
auf den reservierten
Speicherbereich zurück. Dieser
zurückgelieferte Zeiger besitzt
immer den richtigen Datentyp, so
dass eine Typkonvertierung
entfällt.
Konnte new nicht den angeforderten Speicher reservieren, so wird eine Exception (Ausnahme) vom
Typ bad_alloc ausgelöst und das Programm (bis jetzt) beendet. Was Exceptions sind und wie Sie auf
eine solche Exception reagieren können, das erfahren Sie später noch ausführlich. Wichtig ist im
Augenblick nur, dass das Programm i.A. beendet wird wenn eine Speicheranforderung nicht
erfolgreich war.
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (1 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
Unter MICROSOFT VC++ (wenigstens bis zur Version 6.x) löst eine nicht erfolgreiche
Speicherreservierung keine Exception aus. Dies widerspricht ganz klar dem C++ Standard!
Stattdessen liefert der new Operator in diesem Fall einen NULL-Zeiger zurück. Um auch unter MS
VC++ standardkonformes Verhalten zu erhalten, müssen Sie die Datei
<cppkurs_verzeichis>\patches\newpatch.cpp zu Ihren Projekten hinzufügen. Bei allen Beispielen und
Lösungen in diesem Kurs wurde dies bereits durchgeführt. D.h. es erfolgt keine Abprüfung mehr,
ob new einen NULL-Zeiger zurückliefert. Beachten Sie, dass Sie damit dann zwar
standardkonform sind, jedoch der 'normale' VC++ Programmierer im Fehlerfall mit einem NULLZeiger rechnet.
Siehe auch Online-Hilfe zu MSVC unter: PRB: Operator New Doesn't Throw bad_alloc Exception on
Failure
Wie gesagt, tritt eine Exception vom Typ bad_alloc ein, so wird das Programm bis jetzt noch beendet
und eine der folgenden Meldungen ausgegeben:
Unter MinGW:
Abort!
Exiting due to signal SIGABRT
Raised at eip=00094ff6
eax=0015195c ebx=00000120 ecx=00000000 edx=00014ff0 esi=00151cb0
edi=00151c00
ebp=00151a08 esp=00151958 program=E:\TEST\XX\XX.EXE
...
Unter MSVC und BORLAND:
abnormal program termination!
Soweit zum Fehlerfall beim Reservieren des Speichers. Fahren wir mit der Behandlung des new
Operators fort.
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (2 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
new kann nicht nur Speicher für // Speicher für 10 short-Variablen reservieren
einfache Datentypen reservieren, short *pArray = new short[10];
sondern auch für Felder. Soll
Speicher für einfache Felder
(nicht Objektfelder, das kommt
gleich noch) reserviert werden, so
erfolgt die Angabe der Feldgröße
nach dem Datentyp des Feldes in
eckigen Klammern. Im Beispiel
rechts wird Speicher für ein shortFeld mit 10 Elementen reserviert.
// Es sollte Platz für ein short-Feld mit
Beachten Sie bitte, dass // Elementen reserviert werden
bei der Reservierung
short *pArray = new short(10);
von Felder die Feldgröße
in eckigen Klammern stehen muss! Wenn Sie die Feldgröße ausversehen in runden
Klammern angeben, so meldet Ihnen der Compiler keinen Fehler! Vielmehr wird folgender
Vorgang ablaufen: Anstelle eines Feldes wird nur der Platz für eine einfache Variable des
entsprechenden Datentyps reserviert. Und diese Variable wird dann mit dem Wert
initialisiert, der innerhalb der runden Klammer angegeben ist. Sie haben damit sozusagen
einen Konstruktor für eine einfache Variable aufgerufen! Im obigen Beispiel wird also Platz
für eine short-Variable reserviert und der reservierte Speicher mit 10 initialisiert.
new für mehrdimensionale Felder
Soll für mehrdimensionale Felder Platz reserviert werden, so muss dies in mehreren Schritten
erfolgen. Der 'Trick' dabei ist, dass zuerst ein Feld von Zeigern reserviert wird. Dieses Zeigerfeld
repräsentiert die 'Zeilen'. Anschließend werden dann in diesem Zeigerfeld die Speicheradressen für
die Spaltenfelder abgelegt. Sehen wir uns diesen Vorgang nun Schritt für Schritt an:
// Feld mit 3x2 Elementen für die Aufnahme von
Zuerst wird ein Zeiger auf // float-Zahlen reservieren
const int ROW=3;
einen Zeiger des gewünschten
const int COLUMN=2;
Datentyps des Feldes definiert.
Zeiger auf Zeiger definieren
float **ppArray;
Anschließend wird mittels
Zeigerfeld für 3 float-Zeiger res.
new ein Zeigerfeld mit so vielen
Elementen reserviert, wie das
ppArray = new float*[ROW];
Feld Zeilen haben soll (1.
Dimension).
Spaltenfelder reservieren
for (int i=0; i<ROW; i++)
ppArray[i] = new float[COLUMN];
Zum Schluss werden in
einer Schleife die 'Spaltenfelder'
// Zugriff dann wie gewohnt
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (3 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
reserviert. Die von new
ppArray[iRow][iCol] = ...;
zurückgelieferten Adressen
werden dann in dem im 2. Schritt
reservierten Zeigerfeld abgelegt.
Der Zugriff auf die einzelnen
Elemente des mehrdimensionalen
Feldes kann in der üblichen Art
und Weise erfolgen (mehrfache
Indizierung).
Aber Achtung! Der für das mehrdimensionale Feld reservierte Speicher muss nun nicht
mehr zwingend zusammenhängend im Speicher liegen.
Damit genug der Reservierung. Sehen wir uns jetzt an, wie Sie den reservierten Speicher wieder
freigeben können, wenn Sie ihn nicht mehr benötigen.
delete Operator
delete für einfache Daten und Felder
Die Freigabe des mittels new
reservierten Speicherbereichs
erfolgt mit dem delete Operator.
Wurde Speicher für 'einfache'
Daten (keine Felder) reserviert,
so erhält delete als Operanden
den von new zurückgelieferten
Zeiger. Wurde dagegen Speicher
für ein Feld reserviert, so muss
nach dem delete Operator
zunächst eine leere eckige
Klammer stehen und erst danach
der von new zurückgelieferte
Zeiger. Vergessen Sie diese leere
eckige Klammer, so meldet Ihnen
der Compiler keinen Fehler! Bei
Felder von einfachen Datentypen,
d.h. keine Objekte, wird der
reservierte Speicher auch richtig
freigegeben. Warum aber bei der
Freigabe des Speichers von
Objektfeldern diese eckigen
Klammern so wichtig sind, das
// Speicher für long reservieren
// und wieder freigeben
long *pData = new long;
....
delete pData;
// Speicher für char-Feld reservieren
// und wieder freigeben
char *pArray = new char[30];
....
delete [] pArray;
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (4 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
erfahren Sie in der nächsten
Lektion über dynamische
Objekte. Vergessen Sie daher
niemals die eckigen Klammer
beim delete von Feldern.
delete für mehrdimensionale Felder
Auch hier werden wir uns noch
kurz ansehen, wie Sie den auf der
letzten Seite reservierten Speicher
für ein 2-dimensionales Feld
wieder freigeben. Bei der
Freigabe des reservierten
Speichers müssen Sie quasi die
Reihenfolge umdrehen, d.h. Sie
müssen zuerst den Speicher für
die Spaltenfelder freigeben und
dann zum Schluss den Speicher
für das Zeigerfeld. Beachten Sie
beim delete Operator die leeren
eckigen Klammern. Wird haben
es hier mit Feldern zu tun!
// Zuerst Spaltenfelder freigeben
for (int i=0; i<ROW; i++)
delete [] ppArray[i];
// Und Zeigerfeld freigeben
delete [] ppArray;
delete gibt nur den Speicher frei, verändert aber den Inhalt des Zeigers nicht, d.h. er enthält
weiterhin die alte Adresse. Sie dürfen aber nach der Ausführung von delete keine weiteren
Zugriffe mehr über diesen Zeiger auf den freigegebenen Speicherbereich durchführen.
Und noch ein Hinweis:
Der C++ Standard erlaubt es, den delete Operator auch mit einem NULL-Zeiger aufzurufen,
d.h. der Zeiger enthält den Wert NULL. In diesem Fall führt der delete Operator schlicht und
ergriffen gar nichts aus.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (5 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
Das Beispiel:
Das Beispiel berechnet aus einer einzugebenden Anzahl von Zufallszahlen den Mittelwert, wobei
die Zufallszahlen in einem Feld abgelegt werden. Da die Anzahl der Zufallszahlen nicht fest
vorgegeben ist, wird hierfür ein entsprechend großes Feld reserviert. Beachten Sie bei der Freigabe
des Speichers für das Feld die eckigen Klammern beim Aufruf des delete Operators.
Das unten stehende Beispiel finden Sie auch unter: le11\prog\Bnewdel
Die Programmausgabe:
Aus wie vielen Zufallszahlen soll
der Mittelwert berechnet werden ?100
Der Mittelwert ist: 46.48
Das Programm:
// C++ Kurs
// Beispiel zu new und delete
//
// Zuerst Dateien einbinden
#include <iostream>
#include <cstdlib>
// Hauptprogramm
int main()
{
unsigned int
double
unsigned int
int
noOfValues;
sum;
loop;
*pValues;
//
//
//
//
Anzahl der Zufallszahlen
Summe aller Zufallszahlen
Schleifenzaehler
Zeiger auf Feld mit Zufallszahlen
// Anzahl der Zufallszahlen einlesen
std::cout << "Aus wie vielen Zufallszahlen soll\n";
std::cout << "der Mittelwert berechnet werden ?";
std::cin >> noOfValues;
// Feld fuer Zufallszahlen reservieren
// ACHTUNG!! MSVC benoetigt den new-Patch newpatch.cpp
// damit Fehler beim Reservieren des Speichers eine
// bad_alloc Ausnahme ausloest!
pValues = new int[noOfValues];
// Feld mit Zufallszahlen belegen
for (loop=0; loop<noOfValues; loop++)
pValues[loop] = rand() % 100;
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (6 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
// Nun alle Zahlen im Feld aufsummieren
sum = 0;
for (loop=0; loop<noOfValues; loop++)
sum += (double)pValues[loop];
// Mittelwert berechnen und ausgeben
std::cout << "Der Mittelwert ist: " << sum/noOfValues << std::endl;
// Feld auch wieder loeschen
delete [] pValues;
}
Die Übung:
Die ASCII-Datei le11\prog\Lnewdel\names.txt enthält eine Liste mit Namen (enthalten auch
Leerzeichen!) die einzulesen sind. Die Anzahl der in der Datei enthaltenen Namen ist als erster
Eintrag (short-Datentyp) in der Datei mit abgespeichert. Lesen Sie alle Namen in ein entsprechend
großes string-Feld ein und geben Sie die Namen zu Kontrolle aus.
Die Programmausgabe:
Baisch, Robert
Reusch, Bernhard
Werner, Jutta
Schroff, Rosina
Stauss, Guenther
Mueller, Andrea
Herrmann, Fritz
Baumann, Marion
Maier, Klaus
Rapp, Rose
Neuhaus, Ursula
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le11\Lnewdel.
Ich hoffe, Sie konnten die Übung lösen. Sie dürfte nicht so einfach gewesen sein, wie es am Anfang den
Anschein hatte.
In der nächsten Lektion geht es dann ums Anlegen von dynamischen Eigenschaften und dynamischen
Objekten.
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (7 von 8) [17.05.2005 11:39:25]
Operatoren new und delete
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le11/le11_01.htm (8 von 8) [17.05.2005 11:39:25]
Dynamische Eigenschaften und Objekte
Dynamische Eigenschaften und Objekte
Die Themen:
Dynamische Eigenschaften
Beispiel und Übung
Dynamische Objekte
Vorwärtsdeklaration
Dynamische Objektfelder
Beispiel und Übung
Dynamische Eigenschaften
Anfordern des Speichers
Enthalten Objekte z.B. Felder deren Größe von Objekt zu Objekt variieren kann, so werden diese Felder
erst zur Programmlaufzeit, d.h. dynamisch, angelegt. Ein Beispiel hierfür ist die in der Lektion über
Konstruktor/Destruktor erwähnte Stack-Klasse Stack. War dort die Stackgröße, d.h. die maximal
Anzahl der abzulegenden Werte, bisher fest vorgegeben, so kann sie durch eine entsprechende
dynamische Eigenschaft nun variable gestaltet werden.
Für ein solches dynamische Feld
wird innerhalb der Klasse anstelle
des Feldes nur ein entsprechender
Zeiger vom Typ des Feldes definiert.
Der für das Feld notwendige
Speicherplatz wird dann in der
Regel im Konstruktor der Klasse
reserviert. Die notwendige
Feldgröße kann entweder, wie
nebenstehende dargestellt, als
Parameter an der Konstruktor
übergeben werden oder aber, wie
z.B. im Falle eines Fenstertitels, aus
einem der Parameter (hier dann die
Länge des Fenstertitels) berechnet
werden.
class Stack
{
short *pArray;
....
public:
Stack (int size);
....
};
// Definition des Konstruktor
Stack::Stack(int size)
{
pArray = new short[size];
}
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (1 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Freigeben des Speichers
Wird das Objekt, und damit auch
class Stack
der dynamisch angeforderte
{
Speicher, nicht mehr benötigt, so
short *pArray;
muss der Speicher im Destruktor
....
des Objekts auch wieder freigegeben
public:
werden. Beachten Sie bei der
Stack (int size);
Freigabe von Feldern unbedingt die
~Stack();
Schreibweise des delete Operators!
....
};
....
Stack::~Stack()
{
delete [] pArray;
}
Enthalten Objekte dynamische Eigenschaften, so sollten Sie vorläufig folgende Anweisung
vermeiden:
Object2 = Object1;
Durch diese Zuweisung werden die Eigenschaften des Objekts Object1 dem Objekt Object2 zugewiesen.
Und dies gilt auch für die Zeiger auf die in Object1 angelegten dynamischen Objekte, d.h. beide Objekte
verwenden nach der Zuweisung die gleichen dynamischen Objekte bzw. die Zeiger in beiden Objekten
verweisen nun die gleichen Speicherbereiche. Das allein ist schon fehleranfällig genug. Wird dann noch
das Objekt Object1 zerstört, so wird dessen Destruktor aufgerufen, der selbstverständlich die
reservierten Speicherbereiche freigibt. Und damit enthält das Objekt Object2 jetzt Zeiger auf ungültige,
freigegebene Speicherbereiche, was bis zum Programmabsturz führen kann!
Da dynamische Eigenschaften eine wichtige Rolle spielen, folgt nun ausnahmsweise einmal mitten in
einer Lektion eine Beispiel und dann auch eine Übung.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (2 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Das Beispiel:
Das Beispiel enthält eine Klasse Student, die eine Verknüpfung zwischen einem Studenten und den von
ihm belegten Kurs herstellt. Zusätzlich wird noch die Information abgelegt, ob der Student den Kurs
bereits bezahlt hat.
Für den Namen und den Kurstitels werden entsprechende string Eigenschaften dynamisch angelegt.
Dazu werden dem Konstruktor der Name und der Kurstitel übergeben. Die Information, ob der Kurs
bereits bezahlt ist oder nicht, kann ebenfalls mit übergeben werden. Standardmäßig bezahlt der
Teilnehmer den Kurs nicht gleich (Default-Parameter).
Damit der Kurs auch zu einem späteren Zeitpunkt bezahlt werden kann, enthält die Klasse eine
Memberfunktion Pay(...). Da der Code der Memberfunktion relativ klein ist, bietet es sich hier an, eine
inline-Memberfunktion einzusetzen.
Zur Ausgabe der Daten wird eine weitere Memberfunktion PrintStudent(...) verwendet.
Im Hauptprogramm werden dann zwei Teilnehmer mit den jeweiligen Kursen definiert, wobei der
erste Teilnehmer den Kurs nicht gleich bezahlt und der zweite Teilnehmer gleich. Anschließend
werden die beiden Daten ausgeben.
Vor Kursbeginn bezahlt nun auch der erste Teilnehmer die Kursgebühr und die Daten werden
nochmals zu Kontrolle ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le11\prog\Bdyndat
Die Programmausgabe:
Studentenliste:
Name: Karl Maier, Kurs: C++, nicht bezahlt!
Name: Agnes Mueller, Kurs: MFC, bezahlt!
Karl Maier zahlt nun.
Neue Studentenliste:
Name: Karl Maier, Kurs: C++, bezahlt!
Name: Agnes Mueller, Kurs: MFC, bezahlt!
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (3 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Das Programm:
// C++ Kurs
// Loesung zu dynamischen Daten in Objekten
//
// Dateien einbinden
#include <iostream>
#include <iomanip>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Klassendefinition
// Eigenschaften sind alle private!
class Student
{
string *pName;
// Name
string *pCourse;
// Kurs
bool hasPaid;
// Kurs bezahlt
public:
Student(const char *const pN, const char *const pC, bool paid=false);
~Student();
void Pay();
void PrintStudent();
};
// Definition der Memberfunktionen
// Konstruktor
Student::Student(const char *const pN, const char *const pC, bool paid)
{
// Speicher fuer Namen reservieren
pName = new string(pN);
// Speicher fuer Kurstitel reservieren
pCourse = new string(pC);
// Merken ob Kurs bezahlt ist
hasPaid = paid;
}
// Destruktor
Student::~Student()
{
// Speicher fuer Name und Kurstitel freigeben
delete pName;
delete pCourse;
}
// Hier zahlt der Student
inline void Student::Pay()
{
cout << *pName << " zahlt nun.\n\n";
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (4 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
hasPaid = true;
}
// Ausgabe der Studentendaten
void Student::PrintStudent()
{
cout << "Name: " << *pName;
cout << ", Kurs: " << *pCourse << ", ";
if (!hasPaid)
cout << "nicht ";
cout << "bezahlt!\n";
}
// Hauptprogramm
// =============
int main()
{
// Student-Objekt definieren
// 1. Student belegt C++ Kurs und zahlt nicht!
Student Student1("Karl Maier", "C++");
// 2. Student belegt MFC Kurs und zahlt
Student Student2("Agnes Mueller", "MFC", true);
// Ausgabe der Studenten
cout << "Studentenliste:\n";
Student1.PrintStudent();
Student2.PrintStudent();
// 2. Student zahlt nun
Student1.Pay();
// Ausgabe der Studenten
cout << "Neue Studentenliste:\n";
Student1.PrintStudent();
Student2.PrintStudent();
}
Die Übung:
Diese Übung basiert auf dem Beispiel aus der Lektion 5 Konstruktor und Destruktor.
Es ist wiederum eine Klasse Stack zu schreiben, die es erlaubt, mittels Push(...) short-Werte auf einem
Stack abzulegen und mittels Pop(...) die abgelegten Werte wieder auszulesen.
Neu hinzu kommt nun, dass die Anzahl der abzulegenden Werte nicht fest vorgegeben wird (bisher
waren es immer max. 10 Werte), sondern erst bei der Definition des Stack-Objekts festgelegt wird
(Parameter des Konstruktors!). Da innerhalb des Konstruktors beim Reservieren des erforderlichen
Speicherplatzes auch einmal etwas schief gehen kann, fügen Sie der Klasse eine Memberfunktion
hinzu um den erfolgreichen Aufruf des Konstruktors abprüfen zu können.
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (5 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Standardmäßig soll ein Stack für 10 short-Werte angelegt werden.
Im Programm legen Sie einen Stack für 5 short-Werte an, den Sie anschließend solange mit Werten
füllen, bis er voll ist. Geben Sie die auf dem Stack abgelegten Daten aus.
Zum Schluss lesen Sie wieder alle auf dem Stack abgelegten Werte aus und geben diese zur Kontrolle
aus.
Die Programmausgabe:
Werte auf Stack : 41 67 34 0 69
Und jetzt wieder lesen: 69 0 34 67 41
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le11\Ldyndat.
Weiter geht's nun mit dynamischen Objekten.
Dynamische Objekte
Genauso wie vorher normale Daten und Felder dynamisch mittels new angelegt wurden, können auch
Objekte und Objektfelder dynamisch erstellt werden. Sehen wir uns zunächst die Erstellung von
dynamischen Objekten an.
Erstellen von dynamischen Objekten
Um ein Objekt dynamisch zu
erstellen, erhält der Operator new
als Operanden den Namen der
Klasse, für die ein entsprechendes
Objekt erstellt werden soll. Konnte
das Objekt erstellt werden, so liefert
new den Objektzeiger darauf
zurück. Und auch hier wird im
Fehlerfall eine Exception vom Typ
bad_alloc ausgelöst.
// Klassendefintion
class Stack
{
....
public:
Stack(int size);
bool Push(short val);
....
};
// Hauptprogramm
int main()
Besitzt die Klasse einen Konstruktor {
der Parameter enthält (wie im
Stack *pMyStack = new Stack(10);
Beispiel rechts), so werden die
bool ret = pMyStack->Push(5);
Parameter nach dem Klassennamen
....
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (6 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
in Klammern bei der
Objektdefinition mit angegeben.
}
Der Zugriff auf die Member des
erstellten Objekts erfolgt dann, wie
bei Zeigern üblich, über den
Zeigeroperator ->.
Löschen von dynamischen Objekten
Wird das Objekt nicht mehr
benötigt, so muss es wieder gelöscht
werden. Dies erfolgt mit dem
bekannten delete Operator, der den
von new zurückgelieferten Zeiger als
Operanden erhält. Beachten Sie,
dass das Löschen des Objekts zum
Aufruf des Destruktors führt.
// Klassendefinition
class Stack
{
....
public:
Stack(int size);
....
};
// Hauptprogramm
int main()
{
Stack *pMyStack = new Stack(10);
....
delete pMyStack;
}
Vorwärtsdeklaration
Im Zusammenhang mit
dynamischen Objekten soll hier
noch ein auf den ersten Blick nicht
ganz trivialer Fall betrachtet
werden. Sehen Sie sich zunächst
einmal das Beispiel rechts an. Dort
sind zwei Klassen definiert, die jede
einen Zeiger auf die andere Klasse
enthält. Da der C++ Compiler in
einem Durchlauf den Quellcode
übersetzt, wird er hier die Definition
des Zeigers pAnother in der Klasse
Any mit einem Fehler quittieren, da
die Klasse Another noch nicht
bekannt ist. Auch ein Vertauschen
der Klassendefinitionen würde hier
nicht zu einer Lösung führen, da
dann die Definition des Zeigers pAny
in der Klasse Another fehlschlagen
// Klassendefinitionen
class Any
{
Another *pAnother;
....
};
class Another
{
Any *pAny;
....
};
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (7 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
würde. Wir haben hier also das
berühmte 'Henne-Ei' Problem.
Wie löst man nun dieses Problem?
Hierzu wird die
Vorwärtsdeklaration eingesetzt. Bei
der Vorwärtsdeklaration wird nicht
die gesamte Klasse definiert sondern
nur der Klassenname angegeben.
Durch diese Klassendeklaration
weiß der Compiler dann, dass
Another eine Klasse ist und kann
damit die Klasse Any richtig
aufbauen. Selbstverständlich muss
die Definition der Klasse an anderer
Stelle noch erfolgen.
// Klassendeklaration
class Another;
// Klassendefinitionen
class Any
{
Another *pAnother;
....
};
class Another
{
Any *pAny;
....
};
Soweit zur Erzeugung von einzelnen dynamischen Objekten. Sehen wir uns jetzt an, was es beim
Anlegen von dynamischen Objektfeldern zu beachten gibt.
Dynamische Objektfelder
Erstellen von dynamischen Objektfeldern
Soll von einer Klasse dynamisch ein
Objektfeld erstellt werden können,
so darf die Klasse entweder keinen
Konstruktor besitzen oder es muss
der Standard-Konstruktor
(parameterloser Konstruktor)
definiert sein. Von einer Klasse die
nur Konstruktore mit Parametern
enthält kann kein Objektfeld
dynamisch erstellt werden.
Das Erstellen eines dynamischen
Objektfeldes erfolgt analog zum
dynamischen Erstellen von
normalen Feldern, nur erhält new
jetzt als Operanden den
Klassennamen. Nach dem
Klassennamen folgt innerhalb einer
eckigen Klammer die Feldgröße.
// Klassendefinition
class Window
{
....
public:
Window();
....
};
// Hauptprogramm
int main()
{
Window *pWinArray = new Window[5];
....
}
Zugriff auf Member in dynamischen Objektfeldern
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (8 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Der Zugriff auf die Member des Objektfeldes kann dann auf zweierlei Arten erfolgen.
Die erste Zugriffsart verwendet die
Zeigeraddition. Wie Sie bestimmt
noch wissen, wird bei einer Addition
eines Werts X auf einen Zeiger vom
Typ Y der Zeiger nicht um X erhöht
sondern um X*sizeof(Y). So wird im
Beispiel rechts die Memberfunktion
Draw(...) für das zweite Fenster im
Feld aufgerufen.
// Klassendefinition
class Window
{
....
public:
Draw();
....
};
// Hauptprogramm
int main()
{
Window *pWinArray = new Window[5];
....
(pWinArray+1)->Draw();
}
Alternativ kann anstelle des Zugriffs int main()
über eine Zeigeraddition auch der
{
indizierte Zugriff verwendet
cout << "text" << endl;
werden. Dieses Verhalten erklärt
}
sich aus der bekannten Tatsache,
dass der Name eines Feldes nichts
anderes ist als der Zeiger auf den
Beginn des Feldes. Beachten Sie
dabei aber bitte, dass der
Zeigeroperator -> dann durch den
Punktoperator . ersetzt wird.
Löschen von dynamischen Objektfeldern
Wird das Objektfeld nicht mehr benötigt, so muss es auch wieder gelöscht werden. Hierbei ist aber
unbedingt darauf zu achten, dass beim delete Operator die für Felder notwendigen eckigen Klammer
angegeben werden. Sehen wir uns zur Demonstration einmal an was passiert, wenn Sie diese eckigen
Klammern ausversehen vergessen.
Zunächst der korrekte Fall. Wie Sie // Klassendefinition
schon bei der Behandlung des
class Window
Konstruktors erfahren haben, wird {
für jedes Feldelement der
....
Konstruktor (soweit vorhanden) der
public:
Klasse aufgerufen. Und das Gleiche
Window()
gilt natürlich auch für den Aufruf
{ cout << "ctor von Window\n"; }
des Destruktors.
~Window()
{ cout << "dtor von Window\n"; }
Somit ergibt sich für das Beispiel
....
rechts folgende Ausgabe:
};
// Hauptprogramm
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (9 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Programmausgabe:
ctor
ctor
ctor
dtor
dtor
dtor
von
von
von
von
von
von
Window
Window
Window
Window
Window
Window
Nun der Fehlerfall. Das Beispiel
rechts entspricht bis auf den Aufruf
des delete Operators dem vorherigen
Beispiel. Lediglich der eckigen
Klammern wurden beim delete
Operator weggelassen. An der
Aufrufreihenfolge der Konstruktore
ändert sich nichts. Da der delete
Operator aber nun nicht mehr
wissen kann, ob ein einzelnes Objekt
oder ein Objektfeld gelöscht werden
soll, wird der Destruktor nur noch
für das erste Objekt im Feld
aufgerufen. Somit ergibt sich für das
Beispiel rechts folgende Ausgabe:
Programmausgabe:
ctor
ctor
ctor
dtor
con
con
con
von
Window
Window
Window
Window
int main()
{
Window *pWinArray = new Window[3];
....
delete [] pWinArray;
}
// Klassendefinition
class Window
{
....
public:
Window()
{ cout << "ctor von Window\n"; }
~Window()
{ cout << "dtor von Window\n"; }
....
};
// Hauptprogramm
int main()
{
Window *pWinArray = new Window[3];
....
delete pWinArray;
}
Wenn nun im Konstruktor
dynamisch Speicher reserviert
wurde der im Destruktor dann auch
wieder freigegeben werden sollte, so
erfolgt jetzt lediglich die Freigabe
des Speichers für das erste
Feldelement.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (10 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Das Beispiel:
Es wird eine Klasse zur Aufnahme einer Tabelle entwickelt. Da die Anzahl der Reihen und Spalten
nicht fest innerhalb der Klasse vorgegeben werden soll, werden diese Daten dem Konstruktor
übergeben. Im Konstruktor wird ein entsprechendes 2-dimensionales Feld dynamisch angelegt, das im
Destruktor entsprechend wieder freigegeben werden muss.
Über die Memberfunktion SetCell(...) kann eine bestimmte Zelle innerhalb der Tabelle mit einem Wert
belegt werden. Zur Ausgabe der gesamten Tabelle dient noch die letzte Memberfunktion PrintIt(...).
Im Hauptprogramm wird ein Tabellenobjekt dynamisch angelegt, wobei die Tabellengröße über
Konstanten festgelegt wird. Anschließend wird die Tabelle mit Zufallszahlen ausgefüllt und
ausgegeben.
Im nächsten Schritt wird die Tabellendiagonale mit dem fixen Wert -1 belegt und danach die gesamte
Tabelle nochmals ausgegeben.
Zum Schluss wird das Tabellenobjekt wieder gelöscht.
Das unten stehende Beispiel finden Sie auch unter: le11\prog\Bdynobj
Die Programmausgabe:
Tabelleninhalt
==============
41 67 34 0 69
24 78 58 62 64
5 45 81 27 61
91 95 42 27 36
91 4 2 53 92
Tabelleninhalt
==============
-1 67 34 0 69
24 -1 58 62 64
5 45 -1 27 61
91 95 42 -1 36
91 4 2 53 -1
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (11 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Das Programm:
// C++ Kurs
// Beispiel zu dynamischen Objekten und Objektfelder
//
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
#include <cstdlib>
using std::cout;
using std::endl;
// Konstantendefinitionen
const int ROWS = 5;
const int COLS = 5;
// Klassendefinition
class Grid
{
short **ppGrid;
// Zeiger fuer 2-dimensionales Feld
short rows;
// Anzahl der Reihen
short cols;
// Anzahl der Spalten
public:
Grid(short ro, short co);
~Grid();
bool SetCell(short ro, short co, short v);
void PrintIt();
};
// Definition der Memberfunktionen
// Konstruktor
Grid::Grid(short ro, short co)
{
// Anzahl Reihen/Spalten merken
rows = ro;
cols = co;
// Feld fuer Tabellenreihen-Zeiger reservieren
ppGrid = new short*[rows];
// Speicher fuer die Spalten reservieren
for (int index=0; index<rows; index++)
{
ppGrid[index] = new short[cols];
}
}
// Destruktor
Grid::~Grid()
{
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (12 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
// Alle Spaltenfelder freigeben
for (int index=0; index<rows; index++)
delete [] ppGrid[index];
// Feld mit Tabellenreihen-Zeiger freigeben
delete [] ppGrid;
}
// Zelle in der Tabelle setzen
bool Grid::SetCell(short ro, short co, short v)
{
// Reihen-/Spaltennummer abprüfen
if ((ro<0) || (ro>=rows))
return false;
if ((co<0) || (co>=cols))
return false;
// Zelle setzen
ppGrid[ro][co] = v;
return true;
}
// Tabelle ausgeben
void Grid::PrintIt()
{
cout << "Tabelleninhalt\n";
cout << "==============\n";
for (int rIndex=0; rIndex<rows; rIndex++)
{
for (int cIndex=0; cIndex<cols; cIndex++)
cout << std::setw(4) << ppGrid[rIndex][cIndex];
cout << endl;
}
}
// Hauptprogramm
// =============
int main()
{
// Tabelle erzeugen
Grid *pGrid = new Grid(ROWS, COLS);
// Zellen mit Zufallszahlen belegen
for (short rIndex=0; rIndex<ROWS; rIndex++)
for (short cIndex=0; cIndex<COLS; cIndex++)
pGrid->SetCell(rIndex,cIndex,rand()%100);
// Tabelle ausgeben
pGrid->PrintIt();
// Diagonale der Tabelle mit -1 belegen
int maxDim = (ROWS>COLS)?COLS:ROWS;
for (short index=0; index<maxDim; index++)
pGrid->SetCell(index,index,-1);
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (13 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
// Tabelle nochmals ausgeben
pGrid->PrintIt();
// Tabelle nun auch wieder loeschen!
delete pGrid;
}
Die Übung:
Aus der ASCII-Datei le11\prog\Ldynobj\winarray.dat sind die Daten für mehrere Fenster einzulesen.
Die Datei besitzt folgenden prinzipiellen Aufbau:
4
100 100 50 50 Fenster 1
....
Der erste Dateieintrag enthält die Anzahl der in der Datei abgelegten Datensätze. Danach folgen die
Fenster-Datensätze, die folgenden Aufbau besitzen: X/Y Position, Breite/Höhe und Fenstertitel.
Beachten Sie bitte, dass im Fenstertitel auch Leerzeichen enthalten sein können.
Erstellen Sie nun eine entsprechende Fensterklasse zum Abspeichern der Fensterdaten. Der für den
Fenstertitel benötigte Speicherplatz ist dynamisch als string Objekt anzulegen.
Im Hauptprogramm ist dynamisch ein entsprechend großes Objektfeld zur Aufnahme aller
Fensterdaten anzulegen. Anschließend sind die Daten der Fenster über eine Memberfunktion der
Fensterklasse einzulesen und zur Kontrolle auszugeben.
Die Programmausgabe:
Speicher fuer Fenster reserviert.
Speicher fuer Fenster reserviert.
Speicher fuer Fenster reserviert.
Speicher fuer Fenster reserviert.
Fenster : ' Fenster 1'
Position: 100,100
Groesse : 50,50
Fenster : ' Fenster 2'
Position: 50,50
Groesse : 300,400
Fenster : ' Fenster 3'
Position: 200,300
Groesse : 100,100
Fenster : ' Fenster 4'
Position: 100,150
Groesse : 400,300
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (14 von 15) [17.05.2005 11:39:32]
Dynamische Eigenschaften und Objekte
Speicher
Speicher
Speicher
Speicher
fuer
fuer
fuer
fuer
Fenster
Fenster
Fenster
Fenster
freigegeben
freigegeben
freigegeben
freigegeben
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le11\Ldynobj.
Damit haben wieder ein Kapitel beendet. Viel Spaß mit der nächsten Seite.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le11/le11_02.htm (15 von 15) [17.05.2005 11:39:32]
Ende Kapitel 3
Ende Kapitel 3
Herzlichen Glückwunsch!
Sie haben damit das dritte Kapitel dieses
Kurses
fertig bearbeitet.
Das nächste Kapitel befasst sich mit dem Überladen von
Memberfunktionen und Operatoren und der Standard
Bibliothek
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le11/k3_end.htm [17.05.2005 11:39:33]
Überladen von Funktionen/Memberfunktionen
Überladen von
Funktionen/Memberfunktionen
Die Themen:
Einführung
Beispiel zum Überladen
Beispiel und Übung
Einführung
Wurde bisher immer behauptet, dass der Name einer Funktion oder Memberfunktion eindeutig sein
muss, so werden wir diese Einschränkung nun etwas aufweichen. Unter C++ ist es nämlich möglich,
unter gewissen Randbedingungen mehrere Funktionen oder auch Memberfunktionen mit gleichem
Namen zu definieren. Diese 'Mehrfach-Definitionen' werden auch als Überladen bezeichnet.
Wenn im Folgenden von Memberfunktionen die Rede ist, so sind damit auch immer
Funktionen gemeint.
Wenn nun aber mehrere Memberfunktionen den gleichen Namen haben, so müssen diese sich
irgendwie unterscheiden damit beim Aufruf der Memberfunktion ersichtlich wird, welche
Memberfunktion gemeint ist. Dazu müssen sich die Signaturen der Memberfunktionen in
mindestens einem der folgenden Punkte unterscheiden:
Die Memberfunktionen
besitzen eine unterschiedliche
Anzahl von Parametern.
class Window
{
void SetData();
void SetData(char*);
void SetData(char*,int);
....
};
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (1 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
class Window
{
void SetData(char*);
Parameter sind unterschiedlich.
void SetData(ifstream&);
....
};
Die Datentypen der
Obacht geben müssen Sie, wenn class Window
Sie auf const correctness Wert
{
legen ( ). Der constvoid SetData(int);
Qualifizierer bei Parametern
void SetData(const int);
wird beim Überladen nicht
....
ausgewertet.
};
Sehen wir uns noch ein Beispiel
für das Überladen von
Funktionen an. Die beiden
Funktionen Swap(...) rechts
dienen zum Vertauschen von
zwei Werten. Die erste Funktion
vertauscht zwei short-Werte und
die zweite Funktion zwei doubleWerte.
Beim Aufruf von
überladenen
Memberfunktionen
findet keine
automatischen
Typkonvertierung von
Parametern statt. So
würde zum Beispiel der
nebenstehende Aufruf
der Swap(...) Funktion
einen
Übersetzungsfehler
liefern!
// FEHLER!
// Tauscht zwei short-Werte
void Swap (short& v1, short& v2);
// Tauscht zwei double-Werte
void Swap (double& v1, double& v2);
short sVar;
double dVar;
...
Swap (sVar, dVar);
Aber Achtung! Besitzen die überladenen Memberfunktionen Default-Parameter, so achten
Sie darauf, dass sich die Memberfunktionen untereinander mindestens in einem Parameter
vor dem Default-Parameter unterscheiden. Nur dann ist beim Aufruf der Memberfunktion
eindeutig welche Memberfunktion ausgeführt werden soll.
Beispiel zum Überladen
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (2 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
Sehen wir uns noch einige weitere Beispiele zum Überladen von Memberfunktionen an:
1. Fall rechts:
class Window
Dieser Fall ist nicht zulässig, da {
sich die Memberfunktion nur
....
im Returntyp von der Ausgangspublic:
Memberfunktion unterscheidet.
// Ausgangs-Memberfunktion
void SetData(char*);
2. Fall rechts:
// Fall 1
Auch dieser Fall ist unzulässig,
int SetData(char*);
da beim Aufruf der
// Fall 2
Memberfunktion der zweite
void SetData(char*, int nP=1);
Parameter (Default-Parameter)
// Fall 3
entfallen kann und sich beim
void SetData(char*, char*, int nP=1);
Aufruf damit eventuell nicht
};
von der AusgangsMemberfunktion unterscheidet.
3. Fall rechts:
Zulässiger Fall, da die
Memberfunktion mit
mindestens zwei Parametern
aufgerufen werden muss.
Ja und das war's auch schon mal wieder. Es folgt nun das Beispiel und dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
Im Beispiel wird eine Klasse für ein Rechteck-Objekt definiert. Das Rechteck besitzt die üblichen
Eigenschaften wie Position und Größe. Zum Setzen der Rechteckdaten werden zwei
Memberfunktionen SetData(...) definiert. Die erste Memberfunktion erhält die Rechteckdaten als
short-Werte übergeben, während die zweite Memberfunktion die Eigenschaften eines als
Parameter übergebenen Rechtecks verwendet, d.h. es wird eine Kopie des Rechtecks erstellt. Die
Ausgabe der Rechteckdaten erfolgt mithilfe der Memberfunktion PrintRect(...).
Im Hauptprogramm werden zwei Rechteck-Objekte definiert, die anschließend mit
unterschiedlichen Daten initialisiert werden. Zur Kontrolle werden die Daten der beiden Objekte
ausgegeben. Danach werden die Eigenschaften des ersten Objekts dem zweiten Objekt
'zugewiesen' und das zweite Objekt nochmals ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Bumeth.
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (3 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
Die Programmausgabe:
1. Rechteck:
Rechteck auf (10,20) Groesse: (100,200)
2. Rechteck:
Rechteck auf (11,22) Groesse: (110,220)
2. Rechteck gleich 1. Rechteck gesetzt:
Rechteck auf (10,20) Groesse: (100,200)
Das Programm:
// C++ Kurs
// Beispiel zu ueberladenen Memberfunktionen
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Klassendefinition
class Rect
{
short
xPos, yPos;
// Position
short
width, height;
// Breite und Hoehe
public:
// 1. Memberfunktion zum Setzen der Recheck Daten mit 4 Parametern
void SetData(short x, short y, short w, short h);
// 2. Memberfunktion zum Setzen der Rechteck Daten mit 1 Parameter
void SetData(const Rect& source);
// Ausgabe der Rechteck Daten
void PrintRect() const;
};
// Definition der Memberfunktionen
// 1. Memberfunktion SetData() der Klasse Rect
// Erhaelt als Parameter die Rechteck-Daten als 4 short Werte
void Rect::SetData(short x, short y, short w, short h)
{
xPos = x; yPos = y;
width = w; height = h;
}
// 2. Memberfunktion SetData() der Klasse Rect
void Rect::SetData(const Rect& source)
{
xPos = source.xPos; yPos = source.yPos;
width = source.width; height = source.height;
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (4 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
}
// Ausgabe der Rechteck Daten
void Rect::PrintRect() const
{
cout << "Rechteck auf (" << xPos << "," << yPos << ") ";
cout << "Groesse: (" << width << "," << height << ")\n";
}
//
// Hauptprogramm
// =============
int main()
{
// 2 Rechteck Objekte definieren
Rect
firstRect;
Rect
secondRect;
// Daten der Rechteck Objekte setzen
firstRect.SetData(10,20,100,200);
secondRect.SetData(11,22,110,220);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
firstRect.PrintRect();
cout << "2. Rechteck:\n";
secondRect.PrintRect();
// Zweites Rechteck Objekt gleich erstem Objekt setzen
secondRect.SetData(firstRect);
// Daten des zweiten Objekts ausgeben
cout << "2. Rechteck gleich 1. Rechteck gesetzt:\n";
secondRect.PrintRect();
}
Die Übung:
In dieser und den folgenden Übungen soll ein Klasse CString zur Verarbeitung von Strings
entwickelt werden. Diese CString-Klasse besitzt zum Schluss einen Teil der Eigenschaften, die
auch die in der C++ Bibliothek enthaltene string-Klasse besitzt. Sie bauen in den Übungen diese
Klasse quasi teilweise nach.
Der Speicherplatz für den in der Klasse abzulegenden String soll dynamisch als char-Feld angelegt
werden. Außerdem soll die Klasse ein statisches Datum besitzen, um jedem Stringobjekt eine
eindeutige, fortlaufende Nummer zuweisen zu können.
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (5 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
In dieser Übung sind außer dem notwendigen Standard-Konstruktor und Destruktor (zuständig
u.a. für die Verwaltung der fortlaufenden Objektnummer) folgende Memberfunktionen zu
schreiben:
●
●
●
●
●
SetString(const char* const) um einen als char-Zeiger übergebenen String abzulegen.
SetString(const CString&) um einen als CString-Referenz übergebenen String abzulegen, d.h.
es wird eine Kopie des Ausgangs-Strings erstellt.
AddString(const char* const) um einen als char-Zeiger übergebenen String an den aktuellen
String anzuhängen.
AddString(const CString&) um einen als CString-Referenz übergebenen String an den
aktuellen String anzuhängen.
Print( ) um die dem String zugeordnete fortlaufende Stringnummer und den String
auszugeben.
Im Hauptprogramm sind zunächst zwei CString-Objekte zu definieren. Dem ersten CStringObjekt ist dann der C-String "Es waren " zuzuweisen; das zweite CString-Objekt bleibt noch leer.
Beide CString-Objekte sind zur Kontrolle auszugeben.
Anschließend ist dem ersten CString-Objekt der C-String "zwei Ameisen" und dem zweiten
CString-Objekt der C-String "die wollten nach Amerika reisen" hinzuzufügen. Beide CStringObjekte sind zur Kontrolle erneut auszugeben.
Zum Schluss ist noch ein drittes CString-Objekt zu definieren, dem zuerst das erste CString-Objekt
zugewiesen wird, dann der C-String "\n" und zum Schluss das zweite CString-Objekt. Geben Sie
das dritte CString-Objekt ebenfalls aus.
Verwenden Sie zum Kopieren eines Strings in das char-Feld die Funktion strcpy(...) und
zum Anfügen eines Strings an einen bestehenden String die Funktion strcat(...). Für eine
Beschreibung dieser beiden Funktionen klicken Sie bitte hier ( ) oder sehen in der OnlineHilfe Ihres Compiler nach.
Um die Übung noch relativ einfach zu halten soll davon ausgegangen werden, dass bei der
Übergabe eines char-Zeigers an eine Memberfunktion der Zeiger auf einen gültigen CString zeigt.
Die Programmausgabe:
Ausgangs-Strings
1.String: Es waren
2.String:
Nach AddString(...)
1.String: Es waren zwei Ameisen
2.String: die wollten nach Amerika reisen
Neuer String
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (6 von 7) [17.05.2005 11:39:35]
Überladen von Funktionen/Memberfunktionen
3.String: Es waren zwei Ameisen
die wollten nach Amerika reisen
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le12\Lumeth.
In der nächsten Lektion geht's weiter mit dem Überladen des Konstruktors.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le12/le12_01.htm (7 von 7) [17.05.2005 11:39:35]
Überladen des Konstruktors
Überladen des Konstruktors
Die Themen:
Überladen des Konstruktors
Kopierkonstruktor
Beispiel und Übung
Überladen des Konstruktors
Nachdem Sie in der letzten Lektion das Überladen von Memberfunktionen bzw. Funktionen
kennen gelernt haben, sehen wir uns jetzt das Überladen des Konstruktors an.
Hatten unsere Klassen
bisher immer nur einen
Konstruktor, so ermöglicht
das Überladen des
Konstruktors, dass ein
Objekt auf verschiedene
Weisen definiert werden
kann. Und auch für das
Überladen des
Konstruktors gilt: die
Konstruktore müssen sich
in der Anzahl der
Parameter und/oder
Datentypen der Parameter
unterscheiden. Welcher
Konstruktor dann
ausgeführt wird hängt von
den Parametern bei der
Definition des Objekts ab.
class Window
{
....
public:
Window();
// Standard-ctor
Window(char*, short); // 2. ctor
....
};
int main()
{
Window myWin;
// Standard-ctor
Window yourWin("Emil",10); // 2. ctor
....
}
Beachten Sie in diesem
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (1 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Zusammenhang auch, dass
Sie den StandardKonstruktor immer
benötigen, wenn Sie
Objektfelder dynamisch
definieren wollen.
Jede Klasse kann aber nur einen Destruktor besitzen, da der Destruktor immer
parameterlos ist!
Kopierkonstruktor
Eine besondere Form des
Konstruktors stellt der
Kopierkonstruktor (copyctor) dar. Er wird immer
dann aufgerufen, wenn ein
Objekt bei seiner Definition
mit einem anderen Objekt
der gleichen Klasse
initialisiert wird, d.h. das
neue Objekt ist eine Kopie
eines bestehenden Objekts.
Der Kopierkonstruktor hat
folgende Syntax:
CAny::CAny(const
CAny& source);
CAny ist eine beliebige
Klasse und der
Referenzparameter source
eine Referenz auf das zu
kopierende Objekt. Rechts
ist ein Beispiel für einen
solchen Kopierkonstruktor
dargestellt.
// Klassendefinition
class Window
{
char *pTitle;
....
public:
Window(const Window& source);
....
};
// Definition des Kopierkonstruktors
Window::Window(const Window& source)
{
// Platz für Fenstertitel reservieren
pTitle =
new char[strlen(source.pTitle)+1];
// Fenstertitel umkopieren
strcpy(pTitel,source.pTitle);
....
}
// Beispiele für den Aufruf des copy-ctors.
Window myWin(firstWin);
Window yourWin(*pAnyWin);
Window *pWin1 = new Window(firstWin);
Window *pWin2 = new Window(*pAnyWin);
Zusätzlich sind im Beispiel
noch vier Anweisungen
aufgeführt, die alle zum
Aufruf des
Kopierkonstruktor führen.
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (2 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Beachten Sie bitte bei den
Zeigerparametern, dass die
Zeiger dereferenziert
werden.
Selbstverständlich sollten Sie in der Praxis für die Ablage des Fenstertitels ein Objekt
von Typ string verwenden. Das char-Feld dient hier nur zur Veranschaulichung wie ein
Kopierkonstruktor generell aufgebaut ist.
Der Kopierkonstruktor ist in der Regel immer dann erforderlich, wenn eine Klasse dynamische
Eigenschaften enthält da meistens nicht einfach die Zeiger auf die dynamische Eigenschaften
kopiert werden dürfen (doppelt belegter Speicherplatz!).
Wollen Sie aus irgend welchen Anforderungen heraus verhindern, dass von bestimmten
Objekten Kopien erzeugt werden können, so deklarieren Sie den Kopierkonstruktor als
private.
Und damit haben Sie schon wieder eine weitere Lektion fertig bearbeitet. Es folgt jetzt noch das
Beispiel und die Übung.
Beispiel und Übung
Das Beispiel:
Dieses Beispiel entspricht in seine Funktionalität dem Beispiel aus der vorherigen Übung zum
Abspeichern von Rechteckdaten.
Im Beispiel wird eine Klasse für ein Rechteck-Objekt definiert. Das Rechteck besitzt auch
hier die üblichen Eigenschaften wie Position und Größe. Das Setzen der Rechteckdaten
erfolgt nun aber im Gegensatz zum vorherigen Beispiel im Konstruktor. Der erste
Konstruktor erhält die Rechteckdaten als short-Werte übergeben, während der zweite
Konstruktor die Eigenschaften eines als Parameter übergebenen Rechtecks verwendet
(Kopierkonstruktor). Zur Ausgabe der Rechteckdaten dient wieder die Memberfunktion
PrintRect(...).
Im Hauptprogramm wird dann ein erstes Rechteck dynamisch erstellt, wobei die
Rechteckdaten als short-Werte übergeben werden. Danach wird ein zweites Rechteck
dynamisch erstellt, das nun mit den Eigenschaften des ersten Rechtecks initialisiert wird. Zur
Kontrolle werden die Daten der beiden Rechtecke ausgegeben.
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (3 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Zum Schluss nicht vergessen, beide Rechtecke auch wieder zu löschen!
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Buctor.
Die Programmausgabe:
1. Rechteck:
Rechteck auf (10,20) Groesse: (100,200)
2. Rechteck:
Rechteck auf (10,20) Groesse: (100,200)
Das Programm:
// C++ Kurs
// Beispiel zum ueberladenen Konstruktor
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Klassendefinition
class Rect
{
short
xPos, yPos;
// Position
short
width, height;
// Breite und Hoehe
public:
// 1. Konstruktor
Rect(short x, short y, short w, short h);
// 2. Konstruktor (Kopierkonstruktor)
Rect(const Rect& source);
// Ausgabe der Rechteck Daten
void PrintRect() const;
};
// Definition der Memberfunktionen
// 1. Konstruktor
// Erhaelt als Parameter die Rechteck-Daten als 4 short Werte
Rect::Rect(short x, short y, short w, short h)
{
xPos = x; yPos = y;
width = w; height = h;
}
// 2. Konstruktor (Kopierkonstruktor)
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (4 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Rect::Rect(const Rect& source)
{
xPos = source.xPos; yPos = source.yPos;
width = source.width; height = source.height;
}
// Ausgabe der Rechteck Daten
void Rect::PrintRect() const
{
cout << "Rechteck auf (" << xPos << "," << yPos << ") ";
cout << "Groesse: (" << width << "," << height << ")\n";
}
//
// Hauptprogramm
// =============
int main()
{
// Zeiger fuer erste Rect Objekt
Rect
*pFirstRect;
// Erstes Rect Objekt erstellen
pFirstRect = new Rect(10,20,100,200);
// Zweites Rect Objekt erstellen und mit den Eigenschaften
// der ersten Rect Objekts initialisieren
Rect *pSecondRect = new Rect(*pFirstRect);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
pFirstRect->PrintRect();
cout << "2. Rechteck:\n";
pSecondRect->PrintRect();
// Beide Objekte wieder loeschen
delete pFirstRect;
delete pSecondRect;
}
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (5 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Die Übung:
Schreiben Sie die in der letzten Übung erstellte Klasse CString nun so um, dass die beiden
Memberfunktionen SetString(...) durch entsprechende Konstruktore ersetzt werden. Die
Klasse sollte dann folgende Konstruktore enthalten:
●
●
●
Den Standard-Konstruktor für ein leeres CString Objekt
Einen Konstruktor der den zu setzenden String als char-Zeiger erhält
Einen Kopierkonstruktor
Die anderen Memberfunktionen AddString(...) und PrintString(...) können Sie unverändert
übernehmen.
Erstellen Sie im Hauptprogramm ein erstes CString-Objekt, das Sie mit dem C-String "Es
waren " initialisieren und ein zweites CString-Objekt, das einen leeren String enthält. Geben
Sie beide CString-Objekte aus.
Fügen Sie dann zum ersten CString-Objekt den C-String "zwei Ameisen" und zum zweiten
CString-Objekt den C-String "die wollten nach Amerika reisen" hinzu. Geben Sie beide CStringObjekte wieder aus.
Zum Schluss definieren Sie ein drittes CString-Objekt, das Sie mit dem Inhalt des ersten
CString-Objekts initialisieren. Diesem neuen CString-Objekt fügen Sie dann den C-String "\n"
sowie das zweite CString-Objekt hinzu. Geben Sie auch dieses Objekt wieder aus.
Die Programmausgabe:
Ausgangs-Strings
1.String: Es waren
2.String:
Nach AddString(...)
1.String: Es waren zwei Ameisen
2.String: die wollten nach Amerika reisen
Neuer String
3.String: Es waren zwei Ameisen
die wollten nach Amerika reisen
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (6 von 7) [17.05.2005 11:39:36]
Überladen des Konstruktors
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le12\Luctor.
Haben wir bisher nur Memberfunktionen und Funktionen überladen, so geht's ab der nächsten
Lektion los mit dem Überladen von Operatoren.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le12/le12_02.htm (7 von 7) [17.05.2005 11:39:36]
Überladen des Zuweisungsoperators
Überladen des Zuweisungsoperators
Die Themen:
Überladen des Operators =
Returnwert des Operators =
Regel der großen 3
Aufruf des überladenen Operators =
Mehrfaches Überladen
Verhindern von Zuweisungen bei Objekten
Beispiel und Übung
Überladen des Operators =
Für Objekte lassen sich fast alle Operatoren so umdefinieren, dass sie im Zusammenhang mit diesen eine frei
definierbare Funktion ausführen. Als Beispiel mag hier die inzwischen bekannte Klasse CString dienen. Im weiteren
Verlauf des Kurses werden wir diese Klasse so erweitern, dass z.B. zwei CString-Objekte mit dem Plus-Operator '+'
zusammengefügt werden können. Als Einstieg in das Überladen von Operatoren soll in dieser Lektion zunächst das
Überladen des Zuweisungsoperators '=' betrachtet werden.
Um den Zuweisungsoperator '=' für eine Klasse
zu überladen ist in der Regel folgende
Memberfunktion einzusetzen:
// Klassendefinition
class Complex
{
double real;
double imag;
CAny& CAny::operator = (const
public:
DTYP& Param)
Complex(...);
Complex& operator=(const Complex& src);
CAny ist die Klasse, für die der
....
Zuweisungsoperator überladen werden soll.
};
Danach folgt das Schlüsselwort operator und
// Überladenen Operator definieren
dann der zu überladende Operator, hier also '='. Complex& Complex::operator =
Sie können die Kombination operator =
(const Complex& src)
sozusagen als Name der Memberfunktion
{
betrachten. Innerhalb der Parameterklammer
real = src.real;
// Beide Anteile
folgt der Datentyp DTYP des rechten Operanden
imag = src.imag;
// zuweisen
des Operators. Beim Zuweisungsoperator
return *this;
// Referenz zurück
entspricht dies dem Datentyp des rechts vom
}
Operator '=' stehenden Ausdrucks. Damit ist
dann folgende Zuweisung definiert:
CAnyObject = DTYP(Ausdruck);
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (1 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Beachten Sie bitte, dass Sie Objekte in der Regel als Referenzparameter übergeben sollten. Hier können Sie
das 'Warum' nochmals nachlesen ( ).
Returnwert des Operators =
Die Memberfunktion des überladenen Zuweisungsoperators '=' sollte (ja fast muss) immer eine Referenz auf das
aktuelle Objekt zurückliefern. Dies erfolgt durch Dereferenzierung des Zeigers this. Warum das so ist, soll anhand
eines Beispiels demonstriert werden.
Angenommen comp1, comp2 und comp3 sind Objekte vom Typ Complex. Nach dem Sie den Zuweisungsoperator
überladen haben, können Sie dann u.a. folgende Anweisung schreiben (Mehrfachzuweisung!):
comp3 = comp2 = comp1;
Dieser Ausdruck wird vom Compiler in zwei Teilausdrücke aufgeteilt. Zuerst wird der Teilausdruck comp2=comp1
berechnet. Dies führt zum Aufruf des überladenen Zuweisungsoperators für das Objekt comp2 (linker Operand des
Operators), wobei comp1 (rechter Operand) als Parameter an die Operator-Memberfunktion übergeben wird. Das
Ergebnis dieses Ausdrucks wird nun als neuer rechter Operand für den zweiten Teilausdruck comp3=Ergebnis aus
(comp2=comp1) eingesetzt. Und dieses 'Ergebnis' ist der Inhalt des Objekts comp2 nach der Auswertung des ersten
Teilausdrucks.
Sie müssen im Regelfall immer den Zuweisungsoperator überladen wenn eine Klasse dynamische
Eigenschaften enthält (Zeiger auf Speicherbereiche!). Tun Sie dies nicht und es wird eine Zuweisung eines
Objekts an ein anderes durchgeführt, so enthalten danach beide Objekte Zeiger auf den gleichen
Speicherbereich. Und dies kann dann bis zum Programmabsturz führen.
Regel der großen 3
Im Zusammenhang mit dem überladenen Operator = soll auch die so genannte "Regel der großen 3" erwähnt
werden:
Ist eine der Memberfunktionen
●
●
●
Kopierkonstruktor
Destruktor
Überladener Zuweisungsoperator
notwendig, so sind meistens auch die beiden anderen Memberfunktionen erforderlich.
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (2 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Sehen Sie sich dazu das nebenstehende Beispiel
class Window
an. Die dort definierte Klasse Window enthält die {
dynamische Eigenschaft pTitle zur Ablage des
char *pTitle;
// dyn. Eigenschaft
Fenstertitels. Der für den Fenstertitel benötigte
....
Platz wird im Konstruktor reserviert. Damit
public:
wird automatisch der Destruktor notwendig, da
// ctor, reserviert Platz für Titel
der reservierte Speicherplatz beim Entfernen des
Window(const char* const pT)
Objekts auch wieder freigegeben werden muss.
{
Nach der obigen Regel sind nun aber auch
pTitle = new char[strlen(pT)+1];
sowohl der Kopierkonstruktor wie auch der
strcpy(pTitle,pT);
überladene Zuweisungsoperator notwendig! Dass
}
dem tatsächlich so ist, lässt sich an den beiden
// dtor, gibt Platz für Titel frei
folgenden Anweisungen demonstrieren:
~Window()
{
Window myWin(yourWin);
delete [] pTitle;
myWin = yourWin;
}
// Kopierkonstruktor
Window(const Window& src)
Im ersten Fall wird ein neues Objekt myWin
{
definiert. Dieses Objekt wird mit den
pTitle =
Eigenschaften des bereits bestehenden Objekts
new char[strlen(src.pTitle)+1];
yourWin initialisiert. Dazu muss der in yourWin
strcpy(pTitle,src.pTitle);
enthaltene Titel in einen neu anzulegenden
}
Speicherbereich durch den Kopierkonstruktor
// Überladener Zuweisungsoperator
umkopiert werden. Beide Fenstertitel sollen ja
Window& operator= (const Window& src)
unabhängig voneinander später einmal eventl.
{
verändert werden können.
// Zuweisung auf sich selbst!
if (this == &src)
Das Gleiche gilt auch für die darunter stehende
return *this;
Zuweisung. Auch hier muss myWin neuen
delete [] pTitle;
Speicher für den Titel reservieren. In diesem Fall
pTitle =
darf natürlich auch die Freigabe des Speichers
new char[strlen(src.pTitle)+1];
für den bisherigen Titel nicht vergessen werden
strcpy(pTitle,src.pTitle);
(siehe Beispiel rechts)!
return *this;
}
};
Enthält die Klasse, für die der Operator = überladen wird, dynamische Daten, so sollte eine Zuweisung auf
sich selbst immer abgefangen werden:
object = object;
Wird eine solche Zuweisung nicht abgefangen, so kann dies u.U. zu fehlerhaftem Verhalten des Operators
führen. Überlegen Sie sich einmal was passiert, wenn im obigen Beispiel die entsprechende Abfrage nicht
vorhanden wäre.
Aufruf des überladenen Operators =
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (3 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Die überladene Memberfunktion des
class Window
Zuweisungsoperators kann entweder direkt, d.h. {
durch Angabe des Namens der Memberfunktion
....
operator=, oder indirekt durch einfaches
public:
Anwenden des Zuweisungsoperators aufgerufen
Window& operator= (const Window& src)
werden. In der Regel wird der letzte Fall
{...}
verwendet da er eingängiger ist.
....
};
// Hauptprogramm
int main()
{
Window win1, win2;
....
// direkter Aufruf
win1.operator=(win2);
// indirekter Aufruf
win1 = win2;
....
}
Mehrfaches Überladen
Da das Überladen des Zuweisungsoperators
durch eine Memberfunktion erfolgt, können
auch mehrere Memberfunktionen zum
Überladen des Operators definiert werden. Sie
müssen dies immer dann tun, wenn rechts vom
Zuweisungsoperator verschiedene Datentypen
auftreten können. Wenn Sie z.B., wie rechts
angegeben, eine zweite Memberfunktion für den
überladenen Zuweisungsoperator definieren der
als Parameter einen double Wert erhält, so
können Sie danach folgende Zuweisung
durchführen:
// Klassendefinition
class Complex
{
double real;
double imag;
public:
Complex(...);
Complex& operator=(const Complex& src);
Complex& operator=(double val);
....
};
// Überladenen Operator definieren
Complex& Complex::operator =
Complex comp;
(const Complex& src)
comp = 1.0;
{
....
Hier wird einem Objekt der Klasse Complex eine }
Complex& Complex::operator = (double val)
double-Zahl zugewiesen. Dies führt dann zum
{
Aufruf des überladenen Operators operator=
real = val;
(double val).
imag = 0;
return *this;
}
Verhindern von Zuweisungen bei Objekten
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (4 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Und zum Schluss noch ein Hinweis:
Standardmäßig ist es immer erlaubt, einem
Objekt einer Klasse ein anderes Objekt der
gleichen Klasse zuzuweisen. Sie müssen hierfür
nicht explizit einen überladenen
Zuweisungsoperator definieren, dies macht der
Compiler automatisch. In diesem Fall werden die
Daten einfach Element für Element umkopiert.
Wollen Sie dieses Verhalten unterbinden, so
überladen Sie den Zuweisungsoperator durch
eine entsprechende leere Memberfunktion, die
Sie aber innerhalb der private Sektion des Klasse
definieren. Da private Member nicht von
außerhalb der Klasse zugänglich sind, können
jetzt auch keine direkten Zuweisungen mehr
durchgeführt werden.
// Klassendefinition
class Complex
{
double real;
double imag;
Complex& operator=(const Complex& src)
{ } // leere Memberfunktion!
public:
Complex(...);
....
};
Und jetzt folgt wieder das Beispiel und dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
Im Beispiel wird eine Klasse Student definiert, die zur Aufnahme von Studentendaten dienen soll. Die Daten
bestehen der Einfachheit halber nur aus dem Namen des Studenten und einem von ihm belegten Kurs.
Bei der Definition eines Studenten muss mindestens dessen Namen angegeben werden, die Angabe des belegten
Kurses ist optional. Die verfügbaren Kurse sind als enum-Konstanten innerhalb der Klasse definiert.
Außer der üblichen Memberfunktion PrintIt(...), zur Ausgabe der aktuellen Studentendaten dient, wird der
Zuweisungsoperator zweimal überladen. Zum einen soll es möglich sein, einen Studenten an einem anderen
'zuzuweisen'. Hierbei soll nur der Kurs übertragen werden, der Studentenname muss dabei selbstverständlich
erhalten bleiben. Der zweite Zuweisungsoperator erlaubt die direkte Zuweisung eines Kurses an einen Studenten,
wobei der Kurs als eine der definierten enum-Konstanten anzugeben ist.
Sehen Sie sich im Beispiel unbedingt einmal den Konstruktor genauer an. Da der Konstruktor als optionalen
Parameter eine enum-Konstante für den zu belegenden Kurs erhält, der Kurs selbst aber innerhalb der
Studentenklasse als String abgelegt wird, wird im Konstruktor direkt der zweite überladene Zuweisungsoperator
aufgerufen. Er wandelt die enum-Konstante in einen String um und legt diesen dann ab.
Im Hauptprogramm werden dann drei Studenten 'definiert', wobei der erste und dritte Student jeweils gleich
einen Kurs belegen (Parameter des Konstruktors). Sehen Sie sich auch an, wie die enum-Konstanten beim Aufruf
des Konstruktors angegeben werden. Zur Kontrolle werden die Studentendaten dann ausgegeben.
Danach belegt der zweite Student den Kurs C++, was zum Aufruf des zweiten überladenen Zuweisungsoperators
führt.
Zum Schluss belegen der ersten und dritte Student schließlich den gleichen Kurs wie der zweite Student. Dies führt
zum Aufruf des ersten überladenen Zuweisungsoperators.
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Buzuweis.
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (5 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Die Programmausgabe:
Student: Agnes
Belegt Kurs Physik
----------------------------Student: Emil
Belegt Kurs keinen
----------------------------Student: Gabi
Belegt Kurs Mathematik
----------------------------Student: Emil
Belegt Kurs C++
----------------------------Student: Agnes
Belegt Kurs C++
----------------------------Student: Gabi
Belegt Kurs C++
----------------------------Das Programm:
// C++ Kurs
// Beispiel zum ueberladenen Zuweisungsoperator
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Klassendefinition
class Student
{
string name;
// Name des Studenten
string course;
// belegter Kurs
public:
// Enums fuer angebotene Kurse
enum eCourses {KEINER, PHYSIK, MATHEMATIK, PROGRAMMIERUNG};
Student(const char* const pN, enum eCourses eC=KEINER);
void PrintIt() const;
Student& operator = (const Student& rval);
Student& operator = (enum eCourses eC);
};
// Definition der Memberfunktionen
// Konstruktor
Student::Student(const char* const pN, enum eCourses eC):
name(pN)
{
// Name wurde schon in Initialisiererliste uebernommen
// Ueberladenen = Operator aufrufen um den Kursname
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (6 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
// als String zu uebernehmen!
operator=(eC);
}
// Ausgabe der Eigenschaften
void Student::PrintIt() const
{
cout << "Student: " << name << endl;
// Dies hier ist fast 'Trickprogrammierung'
// Es wird die Laenge des Strings mit dem Kurstitel abgeprueft und wenn diese
// ungleich 0 ist, wird der Kursstring an cout uebergeben. Ist die Laenge 0
// so wird der ASCII-String 'keinen' an cout uebergeben
cout << "Belegt Kurs " << ((course.size() != 0) ? course: string("keinen")) <<
endl;
cout << "-----------------------------\n";
}
// Ueberladener Zuweisungsoperator fuer die Zuweisung
// eines Student Objekts an ein anderes. Es wird
// hierbei nur der Kurs uebernommen und nicht der Name!
Student& Student::operator=(const Student& rval)
{
// Zuweisung auf sich selbst abfangen!
if (this == &rval)
return *this;
// Kurs uebernehmen
course = rval.course;
return *this;
}
// Ueberladener Zuweisungsoperator fuer die Zuweisung
// eines Kurses an ein Student Objekt.
Student& Student::operator =(enum Student::eCourses eC)
{
// Enum-Typ in String konvertieren
switch(eC)
{
case PHYSIK:
course = "Physik";
break;
case MATHEMATIK:
course = "Mathematik";
break;
case PROGRAMMIERUNG:
course = "C++";
break;
}
return *this;
}
// Hauptprogramm
// =============
int main()
{
// Drei "Studenten" definieren
Student Student1("Agnes",Student::PHYSIK);
Student Student2("Emil");
Student Student3("Gabi",Student::MATHEMATIK);
// Ausgabe der Studentendaten
Student1.PrintIt();
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (7 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Student2.PrintIt();
Student3.PrintIt();
// Der 2. Student belegt den Kurs Programmierung
Student2 = Student::PROGRAMMIERUNG;
Student2.PrintIt();
// Der 1. und 3. Student belegen den gleichen Kurs
// wie der 2. Student
Student1 = Student3 = Student2;
Student1.PrintIt();
Student3.PrintIt();
}
Die Übung:
Erweitern Sie die in vorherigen Übung erstellte Klasse CString um zwei überladene Zuweisungsoperatoren. Zum
einen soll es nun möglich sein, zwei CString-Objekte einander zuzuweisen und zum anderen einem CString-Objekt
einen C-String.
Alle anderen Memberfunktionen bleiben vorerst unverändert.
Erstellen Sie im Hauptprogramm zwei CString-Objekte denen Sie bei ihrer Definition einen beliebigen Text
zuweisen. Geben Sie die CString-Objekte zur Kontrolle aus.
Weisen Sie dann dem ersten CString-Objekt einen beliebigen C-String zu. Dieses erste CString-Objekt ist im
Anschluss daran dem zweiten CString-Objekt zuzuweisen. Geben Sie beide CString-Objekte erneut aus.
Die Programmausgabe:
Ausgangs-String
1.String: bla bla bla...
2.String: und sonstiger Nonsens
Nach Zuweisung
1.String: Dieser Text wird dupliziert
2.String: Dieser Text wird dupliziert
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die
Lösung unter loesungen\le12\Luzuweis.
Sehen wir uns als nächstes an, wie Operatoren im Allgemeinen überladen werden.
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (8 von 9) [17.05.2005 11:39:38]
Überladen des Zuweisungsoperators
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le12/le12_03.htm (9 von 9) [17.05.2005 11:39:38]
Überladen weiterer Operatoren
Überladen weiterer Operatoren
Die Themen:
Einführung
Überladen von binären Operatoren
Überladen von logischen Operatoren
Überladen von unären Operatoren
Überladen der Operatoren ++, - - und des cast-Operators
const Objekte und überladene Operatoren
Sonstige Hinweise zum Überladen von Operatoren
Beispiel und Übung
Einführung
Genauso wie in der letzten Lektion der Zuweisungsoperator '=' überladen wurde, können für ein Objekte fast
alle anderen Operatoren überladen werden, mit folgenden Ausnahmen:
●
●
●
●
der Punktoperator '.'
der Dereferenzierungsoperator für Zeiger auf Memberfunktionen '.*'
der Gültigkeitsbereichsoperator '::'
der Bedingungsoperator ':?'
Ebenfalls nicht überladbar ist das Präprozessor-Symbol '#', aber das versteht sich ja fast von selbst, da es vor
dem Compilerlauf durch den Präprozessor schon ersetzt wird.
Um einen Operator zu überladen stehen zwei prinzipielle Vorgehensweisen zur Verfügung:
●
●
durch eine nicht-statische Memberfunktion der Klasse.
durch eine normale Funktion, die in der Regel als friend-Funktion definiert wird (friend-Funktionen
werden später noch ausführlicher erklärt).
Überladen von binären Operatoren
Überladen durch Memberfunktionen
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (1 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Wird ein binärer Operator (das sind Operatoren mit zwei Operanden wie z.B. '+' oder '*') überladen, so wird
in der Regel folgende Memberfunktion hierfür eingesetzt:
RVAL CANY::operator OSYMBOL (DTYP);
RVAL ist der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators (z.B. '+' für
die Addition) für die Klasse CANY. Da die Memberfunktion immer im Kontext des ersten, linken Operanden
aufgerufen wird, gibt DTYP den Datentyp des zweiten, rechten Operanden an.
Beispiel:
Für die Klasse Complex soll der PlusOperator definiert werden, so dass folgende
Operation möglich ist:
comp3 = comp1 + comp2;
Dazu wird als erstes der Returntyp des
überladenen Operators bestimmt. Da eine
Addition von zwei Complex-Objekten
ebenfalls wieder ein Complex-Objekt ergibt,
muss die Memberfunktion ein Complex
Objekt zurückgeben, das das Ergebnis
enthält. Damit besitzt die Memberfunktion
die rechts in der Klassendefinition
aufgeführte Deklaration.
Danach kann die Memberfunktion definiert
werden. Eine Addition von zwei ComplexObjekten addiert deren Real- und
Imaginäranteile. Doch hier lauert eine
Fehlerfalle, die auf den ersten Blick nicht
gleich ersichtlich ist. Sehen Sie sich einmal
die Definition der Memberfunktion rechts
genau an. Dort wird zuerst ein lokales
Objekt der Klasse Complex definiert, das bei
seiner Definition auch gleich mit dem
aktuellen Objekt (dies ist der linke Operand
des + Operators!) initialisiert wird (Aufruf
des Kopier-Konstruktors). Anschließend
wird zu diesem lokalen Objekt das zweite
Objekt (rechter Operand der + Operators)
hinzuaddiert. Sie müssen hier ein
temporäres Hilfsobjekt zu Hilfe nehmen,
denn eine Addition von zwei Objekten darf
niemals den Inhalt der beiden als Operanden
verändern!
class Complex
{
double real;
// Realanteil
double imag;
// Imaginäranteil
public:
....
Complex operator+
(const Complex& op2) const;
};
// Überladener Plus-Operator
Complex Complex::operator+
(const Complex& op2) const;
{
// temp. Hilfsobjekt mit akt. Objekt
// initialisieren (Aufruf copy-ctor)
Complex temp(*this);
// 2. Operanden hinzuaddieren
temp.real += op2.real;
temp.imag += op2.imag;
// temp. Objekt zurückgeben
return temp;
}
Warum hier ein Objekt und keine Objektreferenz zurückgegeben werden muss können Sie hier (
nochmals nachlesen.
Weiteres Beispiel zum Überladen des Operators +:
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (2 von 15) [17.05.2005 11:39:44]
)
Überladen weiterer Operatoren
Sehen wir uns ein weiteres Beispiel für den
überladenen Plus-Operator der Klasse
Complex an. Anstelle zwei Complex-Objekte
zu addieren, soll jetzt zu einem ComplexObjekt ein double-Wert addieren werden,
d.h. folgende Operation soll erlaubt sein:
class Complex
{
double real;
// Realanteil
double imag;
// Imaginäranteil
public:
....
Complex operator+ (double val) const;
comp2 = comp1 + 1.2;
};
// Überladener Plus-Operator
Am Returntyp der Memberfunktion ändert Complex Complex::operator+
(double val) const
sich gegenüber vorher nichts, da auch hier
{
das Ergebnis der Addition wiederum ein
// temp. Hilfsobjekt definieren und mit
Complex-Objekt ist. Lediglich der Datentyp
// akt. Objekt initialisieren
des Parameters ändert sich, da er den
Complex temp(*this);
Datentyp des zweiten (rechten) Operanden
// 2. Operanden hinzuaddieren
widerspiegelt. In unserem Fall hier ist der
temp.real += val;
Datentyp double. Im Beispiel rechts wird der
// temp. Objekt zurückgeben
double-Wert nur zum Realanteil der
return temp;
komplexen Zahl addiert.
}
Die umgekehrte Operation
comp2 = 1.2 + comp1;
können Sie nicht mit einer Memberfunktion realisieren! Wie bereits erwähnt, wird die OperatorMemberfunktion im Kontext des linken Operanden aufgerufen, und das ist hier im Kontext von double.
Da Sie für die Standard-Datentypen keine Operatoren überladen können, müssen Sie für diese Operation
auf eine normale Funktion zurückgreifen. Mehr dazu auf der nächsten Seite.
Überladen durch Funktionen
Betrachten wir nun den Fall, dass der Operator durch eine normale Funktion überladen wird und nicht durch
eine Memberfunktion. Wie bereits am Anfang der Lektion erwähnt, wird hierfür eine friend-Funktion
benötigt. Die Besonderheit einer friend-Funktion ist, dass sie Zugriff auf alle Member einer Klasse (auch die
private-Member!) besitzen. Aber wie gesagt, mehr zu friend-Funktionen später im Kurs noch.
Wird ein binärer Operator durch eine normale Funktion überladen, so wird in der Regel folgende Funktion
hierfür eingesetzt:
RVAL operator OSYMBOL (DTYP1 Op1, DTYP2 Op2);
RVAL ist der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators (z.B. '+' für
die Addition). Da die Funktion nun nicht mehr zur Klasse gehört, benötigt sie auch zwei Parameter. Der erste
Parameter gibt dabei den linken Operanden und dessen Datentyp an und der zweite Parameter den rechten
Operanden und dessen Datentyp.
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (3 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Sehen wir uns an, wie der Plus-Operator für class Complex
die Klasse Complex durch eine normale
{
Funktion überladen wird, um zu einem
double real;
// Realanteil
Complex-Objekt einen double-Wert zu
double imag;
// Imaginäranteil
addieren. Beachten Sie bitte die Deklaration
....
der friend-Funktion innerhalb der Klasse
// Funktion als friend-Funktion dekl.
Complex. Die friend-Deklaration gleich bis
friend Complex operator+
auf das vorangestellte Schlüsselwort friend
(const Complex& op1,
der Deklaration der Operatorfunktion.
double val);
};
Auch die Funktion muss als Returnwert ein // Überladener Plus-Operator
Complex-Objekt mit dem Ergebnis der
Complex operator+ (const Complex& op1,
Addition liefern. Als ersten Parameter erhält
double val)
die Funktion eine Referenz auf den linken
{
Operanden des Operators, in unserem Fall
// temp. Hilfsobjekt
also eine Referenz auf ein Complex-Objekt.
Complex temp(op1);
Da der zweite Parameter den rechten
// 2. Operanden hinzuaddieren
Operanden repräsentiert, muss hier ein
temp.real += val;
double-Parameter stehen. Der Ablauf der
// temp. Objekt zurückgeben
Funktion selbst entspricht dem vorherigen
return temp;
Beispiel.
}
Aber Achtung!
Haben Sie den Plus-Operator wie oben angegeben überladen, so können Sie damit nur Operationen
vom folgenden Typ durchführen:
comp2 = comp1 + 1.2;
Wollen Sie auch Operationen vom Typ
comp2 = 1.2 + comp1;
durchführen, so müssen Sie eine weitere friend-Funktion definieren, bei der die beiden Parameter
vertauscht sind, d.h.
Complex operator+ (double val, const Complex& op2);
Überladen von logischen Operatoren
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (4 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Sehen wir uns noch ein letztes Beispiel für
class Complex
das Überladen von binären Operatoren an. {
Oftmals ist es erforderlich, zwei Objekte
double real;
// Realanteil
miteinander vergleichen zu können. Da der
double imag;
// Imaginäranteil
Compiler von Haus aus nicht wissen kann,
public:
wann zwei Objekte gleich sind, müssen wir
....
auch hierfür die entsprechenden Operatoren
bool operator ==
überladen um folgende Anweisungen
(const Complex& op2) const;
schreiben zu können:
bool operator !=
(const Complex& op2) const;
if (comp1 == comp2)
};
....
// Überladener == Operator
if (comp1 != comp2)
bool Complex::operator ==
....
(const Complex& op2) const
Beide Operatoren müssen als Ergebnis einen {
return ((real == op2.real) &&
bool-Wert zurückliefern. Und damit ergeben
(imag == op2.imag));
sich die rechts dargestellten Vergleichs}
Memberfunktionen. Sehen Sie sich einmal an
// Überladener != Operator
wie der != Operator überladen wurde. Er
bool Complex::operator !=
ruft den überladenen == Operator auf und
(const Complex& op2) const
negiert lediglich dessen Ergebnis.
{
Verwenden Sie soweit wie möglich bereits
return !operator==(op2);
bestehenden Code.
}
Überladen von unären Operatoren
Kommen wir nun zum Überladen von unären Operatoren wie z.B. dem NOT-Operator '!'. Auch diese
Operatoren können entweder durch eine nicht-statische Memberfunktion der Klasse oder eine Funktion
überladen werden.
Überladen durch Memberfunktionen
Um einen unären Operator durch eine nicht-statische Memberfunktion zu überladen wird in der Regel folgende
Memberfunktion verwendet:
RVAL CANY::operator OSYMBOL ();
RVAL ist der Returntyp des Operators und OSYMBOL wieder das Symbol des zu überladenden Operators (z.B. '!'
für die NOT-Operation) der Klasse CANY. Da unäre Operatoren nur einen Operanden besitzen, benötigt die
Memberfunktion keine weiteren Parameter.
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (5 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Für die bekannte Klasse Complex soll der
Vorzeichenoperator überladen werden, so
dass folgende Operation definiert ist:
class Complex
{
double real;
// Realanteil
double imag;
// Imaginäranteil
comp2 = -comp1;
public:
....
Complex operator -() const;
Beachten Sie bitte, dass es sich hier um den
Vorzeichenoperator und nicht um den Minus- };
// Überladener Vorzeichenoperator
Operator handelt! Beide besitzen zwar das
Complex Complex::operator- () const
gleiche Symbol, der Minus-Operator
{
benötigt jedoch zwei Operanden und der
Complex temp;
Vorzeichenoperator nur einen. Da das
temp.real = -real;
Ergebnis des Vorzeichenoperators immer
temp.imag = -imag;
vom Typ des Operanden ist, muss die
return temp;
Operator-Memberfunktion auch ein Complex}
Objekt zurückliefern. Im Beispiel rechts
kehrt der Vorzeichenoperator die
Vorzeichen des Real- und Imaginäranteils
um. Achten Sie beim Überladen von
Operatoren auch immer darauf, wann Sie
Operanden verändern dürfen und wann
nicht!
Überladen durch Funktionen
Wird ein unärer Operator durch eine normale Funktion überladen, so wird in der Regel folgende Funktion
hierfür eingesetzt:
RVAL operator OSYMBOL (CAny& Op1);
RVAL ist wieder der Returntyp des Operators und OSYMBOL das Symbol des zu überladenden Operators. Da die
Funktion nun nicht mehr zur Klasse gehört, muss sie wiederum eine friend-Funktion sein und sie benötigt jetzt
einen Parameter. Dieser Parameter gibt den Operanden an und ist immer vom Typ der Klasse, für die der
Operator überladen wird.
Überladen wir einmal den NOT-Operator '!' class Complex
für die Klasse Complex durch eine normale
{
Funktion um folgende Abfrage zu
double real;
// Realanteil
ermöglichen:
double imag;
// Imaginäranteil
....
if (!comp1)
// Funktion als friend-Funktion dekl.
....
friend bool operator !
(const Complex& op);
};
Der NOT-Operator muss laut C++ Syntax
// Überladener NOT-Operator
einen bool-Wert zurückliefern, denn
entweder ist die Bedingung erfüllt oder nicht bool operator! (const Complex& op)
{
erfüllt. Als Parameter erhält die Funktion
return ((op.real == 0.0) &&
eine const Referenz auf den Operanden. Der
(op.imag == 0.0));
NOT-Operator liefert im Beispiel rechts
dann true zurück, wenn sowohl der Real- wie }
auch der Imaginäranteil 0.0 enthält.
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (6 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Überladen der Operatoren ++, - - und des cast-Operators
Nun wollen wir uns einmal das Überladen von einigen besonderen Operatoren ansehen. Mehr zu weiteren,
spezielleren Überladungen von Operatoren dann noch in der nächsten Lektion.
Überladen der Operatoren ++ und - Einen Sonderfall beim Überladen von unären Operatoren stellen die Operatoren ++ und -- dar. Da sie sowohl
als Präfix (++X) wie auch als Postfix (X++) auftreten können, müssen hier beim Überladen zwei verschiedene
Memberfunktionen verwendet werden.
Um den Präfixoperator ++ zu überladen,
class Complex
wird die rechts im Beispiel angegebene
{
Memberfunktion eingesetzt. Diese
double real;
// Realanteil
Memberfunktion muss eine Referenz auf das
double imag;
// Imaginäranteil
aktuelle Objekt zurückgeben damit z.B.
public:
folgende Operation möglich ist:
....
Complex& operator ++();
comp2 = ++comp1;
};
// Überladener Präfixoperator ++
Complex& Complex::operator++ ()
Beachten Sie beim Präfixoperator, dass
{
zuerst die Addition ausgeführt und das
++real;
Ergebnis daraus zurückgeliefert wird.
++imag;
Außerdem muss der Operator hier den
return *this;
Operanden (sprich das Objekt) verändern,
}
so dass kein temporäres Hilfsobjekt wie z.B.
beim + Operator benötigt wird.
Soll der Postfixoperator überladen werden, class Complex
so erhält die Memberfunktion einen Dummy- {
Parameter vom Typ int. Auch hier muss der
double real;
// Realanteil
überladene Operator wieder ein
double imag;
// Imaginäranteil
entsprechendes Complex-Objekt
public:
zurückliefern.
....
Complex operator ++(int);
comp1 = comp1++;
};
// Überladener Postfixoperator ++
Complex Complex::operator++ (int)
Beachten Sie beim Überladen dieses
{
Operators jedoch, dass der Ursprungswert
Complex temp(*this); //copy-ctor
des Objekts zurückgegeben werden muss da
++real;
der Postfixoperator die Addition erst nach
++imag;
der Auswertung des Objekts durchführen
return temp;
darf. Wenn Sie sich nicht mehr sicher über
}
den Unterschied zwischen X++ und ++X
sind, dann hier nachschauen ( ).
Beachten Sie bitte, dass jetzt die Memberfunktion nicht mehr const sein darf da sie den Operanden ja
verändern muss!
Überladener cast-Operator
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (7 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Mithilfe von überladenen Operatoren kann dem Compiler aber auch mitgeteilt werden, wie ein Objekt in einen
anderen Datentyp zu konvertieren ist. Die allgemeine Syntax für eine solche Typkonvertierung mittels einer nichtstatischen Memberfunktion lautet:
CANY::operator NEWDTYP ();
NEWDTYP gibt den Datentyp an, in den das Objekt der Klasse CANY konvertiert werden soll. Am Besten sehen
wir uns dies auch anhand eines Beispiels an.
Ausgangsbasis für dieses Beispiel ist wieder class Complex
die Klasse Complex. Ziel der
{
Typkonvertierung ist es, folgende Anweisung
double real, imag;
schreiben zu können:
....
public:
double val = comp;
....
operator double () const;
};
wobei comp ein Objekt der Klasse Complex
sein soll. Damit obige Anweisung durch den // Typkonvertierung Complex -> double
Complex::operator double () const
Compiler richtig aufgelöst werden kann,
{
müssen wir ihm sagen, wie er ein Complexreturn static_cast<double>
Objekt in einen double-Wert zu konvertieren
(sqrt(real*real + imag*imag));
hat. D.h. NEWDTYP ist hier vom Datentyp
}
double, und damit ergibt sich die rechts
dargestellte Operator-Memberfunktion. Bei
dieser Typkonvertierung wird die Wurzel
aus der Addition der quadrierten Anteile
gebildet und als Ergebnis zurückgeliefert.
Beachten Sie, dass die Operator-Memberfunktion keinen Returntyp besitzt und eine const-Memberfunktion ist.
Würden Sie hier keine const-Memberfunktion verwenden, so könnten Sie kein const Complex Objekt in einen
double-Wert konvertieren.
const Objekte und überladene Operatoren
Sehen wir uns nun das Überladen von Operatoren einmal genauer an, und zwar im Zusammenhang mit constObjekten. Wie Sie ja bereits wissen, lautet die allgemeine Memberfunktion zum Überladen von Operatoren:
RVAL CANY::operator OSYMBOL (DTYP);
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (8 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Der Aufruf des Operators erfolgt dabei
immer im Kontext des linken Operanden
und der rechte Operand wird als Parameter
an die Memberfunktion übergeben. Nehmen
wir jetzt einmal an, Sie wollen für eine
Klasse CAny den Plus-Operator überladen
und hätten zwei Objekte dieser Klasse
definiert um die rechts angegebenen
Operationen durchführen zu können:
Worauf es hier ankommt ist das constObjekt cObject2. Da der überladene
Operator im Kontext des ersten Operanden
aufgerufen wird, ergeben sich damit
folgende rechts dargestellten Aufrufe:
CAny ncObject1, CRes;
const CAny cObject2;
CRes
CRes
CRes
CRes
=
=
=
=
ncObject1 + ncObject1;
ncObject1 + cObject2;
cObject2 + ncObject1;
cObject2 + cObject2;
ncObject1.operator + (ncObject1);
ncObject1.operator + (cObject2);
cObject2.operator + (ncObject1);
cObject2.operator + (cObject2);
Welche Operator-Memberfunktionen müssen Sie den jetzt letztendlich definieren damit auch alle Operationen
definiert sind?
Rechts sehen Sie alle erforderlichen
class Complex
Operator-Memberfunktionen in der
{
Reihenfolge, in der Sie für die obigen
double real;
// Realanteil
Operationen aufgerufen werden. Ist der
double imag;
// Imaginäranteil
zweite Operand ein const-Objekt, so muss
public:
damit der Parameter der Operator....
Memberfunktion auch ein const sein. Ist
Complex operator+ (Complex& op2);
dagegen der erste Operand ein const, so muss
Complex operator+ (const Complex op2);
die Operator-Memberfunktion selbst const
Complex& operator +
sein. Da ein const-Objekt laut Definition
(Complex& op2) const;
nicht verändert werden, kann es also nur
Complex& operator +
Memberfunktionen verwenden die dies auch
(const Complex op2) const;
sicherstellen. Und das sind const};
Memberfunktionen.
Aber keine Panik! Sie müssen nicht immer
alle vier Operator-Memberfunktionen für
überladene Operatoren definieren. Ein nichtconst Objekt kann jederzeit durch den
Compiler in ein const-Objekt konvertiert
werden (aber nicht umgekehrt). Und damit
würde im Prinzip die letzte Memberfunktion
alleine ausreichen. Ob dies aber für alle
Operatoren ausreicht hängt aber vom
Anwendungsfall ab.
Vergessen Sie einmal eine Operator-Memberfunktion für ein const Objekt zu definieren und Sie rufen
den Operator mit einem const Objekt auf, so erhalten Sie eine Fehlermeldung in der Art, dass kein
Operator für const class CLASS definiert ist.
Sonstige Hinweise zum Überladen von Operatoren
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (9 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Zum Schluss dieser Lektion noch drei Hinweise:
Das Überladen eines Operators ändert niemals die Rangfolge der Operatoren, d.h. es gilt z.B. auch
weiterhin, dass eine Multiplikation immer vor einer Addition ausgeführt wird.
Auch die Anzahl der Operanden eines Operators ist fest vorgegeben. So benötigt dar Plus-Operator
immer zwei Operanden.
Haben Sie die Operatoren für = und * überladen, so können Sie trotzdem nicht die Kurzschreibweise *=
verwenden. Sie müssen hierzu den Operator *= explizit überladen.
Sollten Sie ferner noch Zweifel haben, wann ein überladener Operator eine Referenz zurückliefern muss und
wann ein Objekt, so können Sie sich an folgender Daumregel orientieren:
Ein überladener Operator liefert in der Regel dann eine Referenz zurück, wenn der Operand selbst
verändert wird. In allen anderen Fällen ist ein Objekt zurückzuliefern
Und damit beenden wir diese Lektion, nicht aber ohne das obligatorische Beispiel und Ihre Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel demonstriert verschiedene überladene Operatoren anhand einer Klasse Rect zur Abspeicherung
von Rechteckdaten.
Außer dem notwendigen Konstruktor (zur Initialisierung der Rechteckdaten) und einer AusgabeMemberfunktion PrintRect(...) besitzt die Klasse vier überladene Operatoren deren Funktion in der
nachfolgenden Tabelle aufgeführt sind:
Operator Funktion
++
Verschiebt ein Rechteck um eine Einheit in X- und Y-Richtung.
&
Verundet zwei Rechtecke. Es wird hierbei die gemeinsame Fläche der beiden Rechtecke berechnet
und als Rect-Objekt zurückgegeben.
==
Vergleicht ob zwei Rechtecke identisch sind, d.h. die gleiche Position und Ausdehnung besitzen.
!
Stellt fest ob das Rect-Objekt eine Fläche besitzt.
Im Hauptprogramm werden zwei nicht identische Rechtecke definiert. Diese Rechtecke werden mithilfe des
überladenen == Operators auf Gleichheit überprüft und das Ergebnis der Überprüfung dann ausgegeben.
Anschließend wird das erste Rechteck mit dem Operator ++ in X/Y-Richtung um eine Einheit verschoben. Aus
den beiden Rechtecken wird dann mittels des & Operators die Schnittfläche gebildet und diese einem weiteren
Rechteck Objekt zugewiesen. Zum Schluss wird noch durch Anwendung des ! Operators überprüft, ob die
ermittelte Schnittfläche leer ist.
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Buoper.
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (10 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Die Programmausgabe:
1. Rechteck:
Rechteck auf (10,20) Groesse: (100,200)
2. Rechteck:
Rechteck auf (50,50) Groesse: (100,200)
Rechtecke sind unterschiedlich
1. Rechteck um eins verschoben:
Rechteck auf (11,21) Groesse: (100,200)
Rechtecke ueberschneiden sich! Gemeinsame Flaeche:
Rechteck auf (50,50) Groesse: (61,171)
Das Programm:
// C++ Kurs
// Beispiel zu ueberladenen Operatoren
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Klassendefinition
class Rect
{
short
xPos, yPos;
// Position
short
width, height;
// Breite und Hoehe
public:
Rect();
// ctors
Rect(short x, short y, short w, short h);
Rect(const Rect& source);
void PrintRect()const;
// Ausgabe des Rechtecks
Rect operator ++(int);
// Verschiebt Rechteck um eine X/Y-Position
Rect operator & (const Rect& op2) const; // Verundet zwei Rechtecke
bool operator ==(const Rect& op2) const; // Vergleicht zwei Rechtecke
bool operator !() const;
// Prueft auf leeres Rechteck ab
};
// Definition der Memberfunktionen
// 1. Konstruktor (Standard Konstruktor)
Rect::Rect()
{
xPos = yPos = width = height = 0;
}
// 2. Konstruktor
// Erhaelt als Parameter die Rechteck-Daten als 4 short Werte
Rect::Rect(short x, short y, short w, short h)
{
xPos = x; yPos = y;
width = w; height = h;
}
// 3. Konstruktor
// copy-ctor
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (11 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
Rect::Rect(const Rect& source)
{
xPos = source.xPos; yPos = source.yPos;
width = source.width; height = source.height;
}
// Ausgabe der Rechteck Daten
void Rect::PrintRect() const
{
cout << "Rechteck auf (" << xPos << "," << yPos << ") ";
cout << "Groesse: (" << width << "," << height << ")\n";
}
// Verschiebt Rechteck um eine X/Y-Position
// Achtung Postfixoperator!
// Darf keine const-Memberfunktion sein da das Objekt
// ja veraendert wird!
Rect Rect::operator ++(int)
{
// Hilfsobjekt zur Aufnahme der akt. Werte
// Ruft copy-ctor auf
Rect orig(*this);
// Nun erst Position veraendern
xPos++;
yPos++;
// Ursprungswerte zurueckgeben
return orig;
}
// Verundet zwei Rechtecke
// Das daraus resultierende Rechtecke enthaelt die Flaeche,
// die beiden Rechtecken gemeinsam ist
Rect Rect::operator & (const Rect& op2) const
{
// Resultierendes Rechteck
Rect result;
// Hilfsvariablen
short end1, end2;
// Groesste X-Position ist resultierende X-Position
result.xPos = (op2.xPos>xPos) ? op2.xPos : xPos;
// X-Positionen der rechten Kante berechnen
end1 = xPos+width;
end2 = op2.xPos+op2.width;
// Kleinste X-Position bestimmte Breite des result. Rechtecks
result.width = (end1>end2) ? end2-result.xPos : end1-result.xPos;
// Nun das gleiche Spiel mit der Y-Position und der Hoehe
result.yPos = (op2.yPos>yPos) ? op2.yPos : yPos;
end1 = yPos+height;
end2 = op2.yPos+op2.height;
result.height = (end1>end2) ? end2-result.yPos : end1-result.yPos;
// Falls keine gemeinsame Flaeche vorhanden ist
// leeres Rechteck setzen
if ((result.width <= 0) || (result.height <= 0))
{
result.width = result.height = 0;
result.xPos = result.yPos = 0;
}
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (12 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
return result;
}
// Vergleicht zwei Rechecke
bool Rect::operator ==(const Rect& op2) const
{
// Rechtecke sind gleich wenn Koordinaten und Ausdehnung gleich sind
if ((xPos == op2.xPos) && (width == op2.width) &&
(yPos == op2.yPos) && (height == op2.height))
return true;
else
return false;
}
// Prueft ab, ob das Rechteck eine Flaeche besitzt
bool Rect::operator ! () const
{
if ((width == 0) || (height == 0))
return true;
else
return false;
}
//
// Hauptprogramm
// =============
int main()
{
// Zwei Rect Objekte erstellen
Rect *pFirstRect = new Rect(10,20,100,200);
Rect *pSecondRect = new Rect(50,50,100,200);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
pFirstRect->PrintRect();
cout << "2. Rechteck:\n";
pSecondRect->PrintRect();
// Rechtecke vergleichen
if (*pFirstRect == *pSecondRect)
cout << "Rechtecke sind gleich\n";
else
cout << "Rechtecke sind unterschiedlich\n";
// Erstes Objekt verschieben
// Klammerung unbedingt beachten!
(*pFirstRect)++;
cout << "1. Rechteck um eins verschoben:\n";
pFirstRect->PrintRect();
// Neues Rechteck aus der gemeinsamen Flaeche der beiden
// Rechtecke bilden
Rect mergedRect = *pFirstRect & *pSecondRect;
// Abpruefen, ob beide Rechtecke eine gemeinsame Flaeche hatten
if (!mergedRect)
cout << "Keine Ueberschneidung der beiden Rechtecke!\n";
else
{
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (13 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
cout << "Rechtecke ueberschneiden sich! Gemeinsame Flaeche:\n";
mergedRect.PrintRect();
}
delete pFirstRect;
delete pSecondRect;
}
Die Übung:
Erweitern Sie die in vorherigen Übung erstellte Klasse CString um den Operator ==, um zwei CString-Objekte
vergleichen zu können.
Ersetzen Sie dann die beiden AddString(...) Memberfunktionen der Klasse durch entsprechendes Überladen des
Operators +.
Erstellen Sie im Hauptprogramm zwei CString-Objekte, wobei das erste CString-Objekt bei seiner Definition
mit dem Text "Es waren" zu initialisieren ist. Geben Sie beide Objekte zur Kontrolle aus.
'Addieren' Sie zum ersten CString-Objekt den Text " einmal zwei Ameisen" und zum zweiten den Text "die
wollten nach Amerika reisen" hinzu. Geben Sie beide Objekte erneut aus.
Vergleichen Sie dann beide CString-Objekte mittels einer if-Abfrage auf Gleichheit und geben das Ergebnis der
Abfrage aus.
Zum Schluss weisen Sie dem ersten CString-Objekt das zweite CString-Objekt zu und vergleichen nun
nochmals die beiden Objekte.
Die Programmausgabe:
Ausgangs-Strings
1.String: Es waren
2.String:
Nach Addition
1.String: Es waren einmal zwei Ameisen
2.String: die wollten nach Amerika reisen
Die Strings sind ungleich
Strings einander zugewiesen!
1.String: die wollten nach Amerika reisen
2.String: die wollten nach Amerika reisen
Die Strings sind gleich
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die
Lösung unter loesungen\le12\Luoper.
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (14 von 15) [17.05.2005 11:39:44]
Überladen weiterer Operatoren
So, in der nächsten Lektion werden wir noch einige besondere Operatoren überladen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le12/le12_04.htm (15 von 15) [17.05.2005 11:39:44]
Überladen spezieller Operatoren
Überladen spezieller Operatoren
Die Themen:
Überladen des Operators >>
Überladen des Operators <<
Überladen der Operatoren << und >> für Objektzeiger
Funktionsoperator ( )
Überladen des Indexoperators [ ]
Überladen von new und delete
Beispiel und Übung
In dieser Lektion werden wir uns einige Operatoren ansehen, die entweder beim Überladen eine
Sonderbehandlung erfordern oder aber die im Zusammenhang mit einer Klasse eine bestimmte Aufgabe
durchführen sollten.
Allgemeines zum Überladen der Operatoren << und >>
Diese Operatoren sind standardmäßig für das bitweise Schieben eines Operanden nach links bzw. rechts
zuständig. Sie wurden bisher aber auch schon dazu verwendet, um zum Beispiel Streamausgaben mit dem
Stream cout durchzuführen. Durch Überladen dieser Operatoren kann nun erreicht werden, dass nicht nur die
Standard-Datentypen wie z.B. short oder double mit Streams eingelesen bzw. ausgegeben werden können,
sondern sogar beliebige Objekte.
Überladen des Operators >>
Beginnen wir mit dem Überladen des Operators >>. Der Operator >> soll nun so überladen werden, dass im
Zusammenspiel mit einem Eingabestream ein beliebiges Objekt z.B. wie folgt von der Tastatur eingelesen werden
kann:
cin >> anyObject;
Wie Sie bestimmt noch wissen, wird ein Operator immer für die Klasse überladen, deren Objekt links vom
Operator steht. In diesem Fall müssen wir also den Operator >> für die Klasse istream überladen. Hierbei ist ein
kleines Problem zu lösen. Da cin ein Objekt der Klasse istream ist hat es zunächst keinen Zugriff auf die nichtpublic Eigenschaften des einzulesenden Objekts (zweiter Operand von >>). Da beim Einlesen aber die
Eigenschaften des Objekts natürlich verändert werden, müssen wir dem überladenen Operator den Zugriff auf
nicht-public Daten gestattet. Dies wird durch folgende Funktionsdeklaration innerhalb der einzulesenden Klasse
erreicht:
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (1 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
friend istream& operator >> (istream& is, Any& anyObject);
Wir benötigen hier also auch eine friend-Funktion um diesen Operator zu überladen.
Rechts ist wieder die bekannte Klasse
class Complex
Complex dargestellt. Um deren Daten mit der {
Anweisung
double real;
double imag;
cin >> comp1:
public:
....
friend istream& operator >>
einlesen zu können, wird zuerst die
(istream& is, Complex& op);
entsprechende friend-Funktion innerhalb der
};
Klasse Complex deklariert.
Nach dem die Funktion deklariert ist, muss istream& operator >> (istream& is,
sie noch definiert werden. Die Definition
Complex& op)
gleicht bis auf Schlüsselwort friend der
{
Funktionsdeklaration innerhalb der Klasse.
is >> op.real; // Real- und ImaginärDie Funktion erhält im ersten Parameter
is >> op.omag; // anteil einlesen
eine Referenz auf den Eingabestream und im
return is;
// Ref. auf Stream
zweiten Parameter eine Referenz auf das
}
einzulesende Objekt.
Beachten Sie bitte, dass die Funktion vollen Zugriff auf alle Member des Objekts besitzt! Für die Klasse Complex
würde damit die Operatorfunktion wie oben angegeben aussehen. Da die Funktion eine Referenz auf das
Streamobjekt zurückliefert, können auch mehrere Objekte oder Daten innerhalb einer Eingabeweisung
eingelesen werden:
cin >> comp1 >> var >> comp2;
Der so überladene Operator >> lässt aber
nicht nur das Einlesen der Eigenschaften von
der Tastatur zu, sondern auch das Einlesen
aus einer Datei. Das Einzige was Sie hierfür
tun müssen ist, einen Datei-Eingabestream
(z.B. ifstream) mit einer Datei zu verbinden
und den Stream cin aus dem vorherigen
Beispiel durch den Dateistream zu ersetzen.
Dies ist deshalb möglich, da sowohl der
Tastatur-Eingabestream cin wie auch der
Datei-Eingabestream ifstream Instanzen mit
der gleichen Basisklasse istream sind. Und
genau für diese Streamklasse wurde der
Operator überladen.
Klassen- und Funktionsdefinition wie oben
angegeben
// Hauptprogramm
int main()
{
// Eingabestream mit Datei verbinden
ifstream inFile;
inFile.open("MyFile.dat");
// Objekt der Klasse Complex definieren
Complex myComp;
// Daten für Objekt aus Datei einlesen
inFile >> myComp;
....
}
Überladen des Operators <<
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (2 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Genauso wie sich der Operator >> für Eingaben überladen lässt, lässt sich auch der Operator << für die Ausgabe
überladen. Dazu wird die friend-Funktion wie folgt deklariert:
friend ostream& operator << (ostream& os, Any& anyObject);
Für die Klasse Complex ergibt sich die rechts class Complex
dargestellte Klassendefinition und Definition {
der friend-Funktion. Damit ist z.B. folgende
double real;
Anweisung nun erlaubt:
double imag;
public:
cout << comp1;
....
friend ostream& operator <<
(ostream& os, Complex& op);
Beachten Sie, dass in der Funktion jetzt
};
natürlich der Operator << für die Ausgabe
// Funktionsdefinition
stehen muss und nicht der Operator >>.
ostream& operator << (ostream& os,
Complex& op)
Für die Ausgabe von Daten in eine Datei gilt
{
das Gleiche wie beim Einlesen aus einer
os << op.real; // Real- und ImaginärDatei. Nur müssen Sie hier
os << op.imag; // anteil ausgeben
selbstverständlich ein Dateiobjekt vom Typ
return os;
// Ref. auf Stream
ofstream erstellen.
}
Überladen der Operatoren << und >> für Objektzeiger
In den bisherigen Beispielen wurde immer
das Objekt selbst eingelesen bzw.
ausgegeben. Sollen Objektzeiger für die Ein/Ausgabe verwendet werden, so müssen Sie
entweder den Zeiger vorher dereferenzieren
oder aber eine weitere Operatorfunktion zur
Verfügung stellen. Im Beispiel rechts ist
einmal exemplarisch dargestellt, wie dies für
einen Objektzeiger auf ein Complex-Objekt
aussehen würde.
class Complex
{
double real;
double imag;
public:
....
friend ostream& operator <<
(ostream& os, Complex *pOp);
};
// Funktionsdefinition
ostream& operator << (ostream& os,
Mithilfe der nebenstehenden Funktion
Complex *pOp)
könnten dann die Eigenschaften des Objekts {
ausgegeben, auf das der Zeiger pComp1
os << pOp->real;
verweist::
os << pOp->imag;
return os;
}
cout << pCompl1;
Ohne diese Funktion würde ansonsten der
Inhalt des Zeigers ausgegeben werden.
Noch ein Hinweis: Wie Sie vielleicht vermuten, ist die Funktionalität der in dieser Lektion behandelten
Operatoren << und >> nicht fest vorgegeben. Sie könnten also durch Überladen dieser Operatoren auch ganz
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (3 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
andere Dinge durchführen. Bedenken Sie dabei aber, dass der 'normale' Anwender in der Regel diese
Operatoren mit einer Ein-/Ausgabe verbindet.
Funktionsoperator ( )
Und auch der Funktionsoperator ( ) (ja, das sind die Parameterklammern!) lässt sich überladen. Objekte von
Klassen welche den Funktionsoperator überladen, werden als Funktionsobjekte bezeichnet (auf neudeutsch
function object oder functor). Hat eine Klasse den Funktionsoperator überladen, so kann ein Objekt dieser Klasse
wie eine Funktion verwendet werden. Funktionsobjekte sind quasi das C++ Gegenstück zu Funktionszeiger.
Im Beispiel rechts wird die Klasse Difference
definiert deren Zweck es ist, die Differenz zu
einem bestimmten Wert zu bilden. Dazu
erhält der Konstruktor der Klasse den Wert
übergeben, zu dem später Differenzen zu
bilden sind. Die Differenzbildung selbst
erfolgt dann in der Memberfunktion des
überladenen Funktionsoperators.
// Klasse mit überladenem Funktionsoperator
class Difference
{
short value;
public:
Difference(short val): value(val)
{ }
short operator() (short val)
{
Wird im Programm dann Objekt dieser
return val-value;
Klasse definiert, so erhält es bei seiner
}
Definition zunächst den Wert übergeben,
};
von dem später die Differenz gebildet
// weiter unten im Programm
werden soll (im Beispiel also 50). Um nun die // Definition der Funktionsobjekts
Differenz zu diesem Wert zu bilden, wird der Difference diff50(50);
Funktionsoperator aufgerufen, der als
short val = 30;
Parameter den Wert für die
...
Differenzbildung erhält. Der Aufruf des
// Aufruf des überladenen () Operators
überladenen Funktionsoperators kann
cout << diff50.operator()(val) << endl;
hierbei; wie bei überladenen Operatoren
val = 60;
üblich, auf zweierlei Arten erfolgen:
cout << diff50(val) << ',';
....
any.operator()(var); bzw.
Programmausgabe:
any(var);
Im letzten Aufruf wird also das Objekt wie
eine Funktion angewandt. Solche
Funktionsobjekte spielen in der C++
Bibliothek Standard Template Library (STL)
eine wichtige Rolle.
-20
10
Überladen des Indexoperators [ ]
Der Indexoperator [] wird standardmäßig dazu verwendet, um indiziert auf Feldelemente zuzugreifen. Durch
Überladen dieses Operators kann jedoch erreicht werden, dass mithilfe des Indexoperators auch indiziert auf
eine verkettete Liste zugegriffen werden kann. Aber sehen wir uns erst einmal die Funktionsweise einer
verketteten Liste an.
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (4 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Die verkettete Liste
Eine verkettete Liste ist eine Aneinanderreihung von mehreren Objekten, wobei jedes Objekt mindestens einen
Zeiger auf ein anderes Objekt besitzt. Im einfachsten Fall besitzt ein Objekt lediglich einen Zeiger auf seinen
Nachfolger in der Liste. Da die Elemente innerhalb einer verketteten Liste in der Regel dynamisch erzeugt
werden, wird für die Kennzeichnung des Listenendes als Zeiger auf den Nachfolger der Wert NULL eingetragen.
Das nachfolgende Bild zeigt den prinzipiellen Aufbau einer einfach verketteten Liste.
Welche Nutzdaten innerhalb der Liste abgelegt sind, spielt für die Arbeitsweise der Liste keine Rolle.
Wird ein neues Element zu einer bestehenden Liste hinzugefügt, so wird zunächst dieses Element erstellt. Soll das
neue Element ans Ende der Liste angefügt wird, wird dessen Zeiger auf den Nachfolger auf NULL gesetzt.
Danach wird die komplette Liste solange durchlaufen, bis das bisherige letzte Listenelement gefunden ist. Dieses
hat ja immer noch als Zeiger auf seinen Nachfolger ebenfalls den Wert NULL. In diesem bisherigen letzten
Listenelement wird dann der Zeiger auf das neu erstellte Objekt eingetragen, das damit dann zur Liste
hinzugefügt wurde.
Der Nachteil einer solchen verketteten Liste ist, dass die Liste nur in einer Richtung durchlaufen werden kann.
Wenn Sie z.B. auf dem 3. Element in der Liste stehen und danach das 2. Element verarbeiten wollen, so müssen
Sie die Liste erneut von vorne nach diesem Element durchsuchen. Dieser Nachteil lässt sich umgehen, in dem eine
doppelt verkettete Liste eingesetzt wird. Hierbei hat jedes Element außer einen Zeiger auf seinen Nachfolger
auch noch einen Zeiger auf seinen Vorgänger.
Solche verketteten Listen werden immer dann eingesetzt, wenn eine nicht von Anfang an definierte Anzahl von
Elementen verarbeitet werden muss. Die Anzahl der möglichen Elemente hängt dann nur noch vom verfügbaren
Speicher ab.
Das Entfernen von Elementen, und ebenso das Sortieren, kann bei einer solchen Liste relativ schnell erfolgen, da
hierzu 'nur' die Zeiger auf die Nachfolger (bei doppelt verketteten Listen zusätzlich auf den Vorgänger)
umgesetzt werden müssen.
Ein Beispiel für eine einfach verkettete Liste finden Sie bei auch unter le13\prog\Bvliste. Sehen Sie sich im
Beispiel einmal an wie die Liste erzeugt und entfernt wird. Um die verkettete Liste zu erstellen wird die statische
Memberfunktion CreateList(...) aufgerufen. In dieser Memberfunktion wird dann mittels new das erste
Listenelement erstellt, das aber noch kein Nutzdatum besitzt. Beachten Sie im Beispiel, dass CreateList(...) eine
Referenz auf das erste Listenelement zurückliefert und dieser Rückgabewert auch in einer Referenzvariablen
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (5 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
abgelegt werden muss.
Damit der Anwender nicht selbst eine solche verkettete Liste direkt erstellen kann, wurde der Konstruktor als
private deklariert. Um Nutzdaten zur Liste hinzuzufügen, wird die Memberfunktion Add2List(...) der Liste
aufgerufen. Diese erstellt dann ein weiteres Element der verketteten Liste und fügt zu diesem das Nutzdatum
hinzu.
Das Entfernen der Liste am Ende des Programms erfolgt ebenfalls über eine statische Memberfunktion, umso
eine Rekursion innerhalb des Destruktors zu vermeiden. Im ersten Ansatz kommt man unweigerlich in
Versuchung, im Destruktor des ersten Listenelements ein delete auf den Nachfolger durchzuführen. Diese würde
dann aber seinerseits wieder zum Aufruf des Destruktors führen, der dann wiederum seinen Nachfolger löschen
würde usw. D.h. Sie hätten hier innerhalb des Destruktors eine nicht gleich sichtbare Rekursion. Und nun stellen
Sie sich einmal vor, eine solche Liste würde 1000 oder mehr Elemente besitzen. Beachten Sie hier, dass beim
Löschen eines Listenelements auch automatisch das dazugehörige Nutzdatum gelöscht wird.
Soviel zur verketteten Liste. Die Standard Template Library (STL) enthält bereits eine fertige Klasse für eine
solche verkettete Liste. Aber dazu kommen wir später noch.
Zugriff auf Elemente der verketteten Liste
Das Beispiel rechts zeigt den Zugriff auf ein
beliebiges Element innerhalb einer
verketteten Liste ohne Überladen des
Operators [ ]. Es ist eine Erweiterung des
Beispiels aus der Beschreibung zur
verketteten Liste. Die verkettete Liste wird
durch die Klasse List repräsentiert und das
Nutzdatum der Liste durch die Klasse Data.
// Klassendefinition der verketteten Liste
class List
{
Data *pData;
....
public:
....
Data& GetElement(int index) const;
};
Als Parameter erhält die Memberfunktion
// Definition der Memberfunktion
GetElement(...) den Index des gesuchten
Data& List::GetElement(int index) const
Elements übergeben und liefert als Ergebnis {
eine Referenz auf das Nutzdatum. Liegt der
// Index-Zeiger auf 1. Element
Index außerhalb des erlaubten Bereichs, so
List *pElement = this;
wird entweder das Nutzdatum des ersten
// Liste durchlaufen bis Element gefunden
Elements (Index ist kleiner 0) oder das des
for (;;)
letzten Elements (Index größer als Anzahl
{
der Elemente) zurückgegeben. Um diese
// Falls Index jetzt 0 ist
Fehlerfälle praxisgerechter behandeln zu
if (index <= 0)
können, würde in einer realen Anwendung
// Element gefunden
das Exception-Handling
break;
(Ausnahmebehandlung) eingesetzt werden.
// Falls Listenende erreicht
Was das Exception-Handling ist und wie es
if (pElement->pNext == NULL)
funktioniert, das erfahren Sie im nächsten
// Letztes Element zurückgeben
Kapitel.
break;
// nächstes Element holen
pElement = pElement->pNext;
// Index dekrementieren
index--;
}
// Referenz auf Datum zurückgeben
return *(pElement->pData);
}
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (6 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Überladen des Indexoperators [ ]
Sehen wir uns jetzt an, wie zum Zugriff auf
ein gewünschtes Element in der verketteten
Liste der Indexoperator eingesetzt werden
kann. Um den Indexoperator zu überladen
kann nur eine nicht-statische
Memberfunktion verwendet werden und
keine friend-Funktion. Diese
Memberfunktion besitzt folgende
Deklaration:
RVAL CAny::operator [] (int
index);
RVAL ist der Returntyp der Memberfunktion
und CAny die Klasse, für die der Operator
überladen werden soll. Der überladene
Indexoperator sollte in der Regel immer eine
Referenz auf das gewünschte Element
zurückliefern und nicht das Element selbst.
Nur so ist gewährleistet, dass der indizierte
Zugriff auf das Element sowohl rechts wie
auch links vom Zuweisungsoperator stehen
kann. Im Parameter index erhält die
Memberfunktion den Index des gewünschten
Elements.
Für das vorherige Beispiel ergibt sich damit
die rechts dargestellte OperatorMemberfunktion.
Wollen Sie auch const Objekte in einer
solchen Liste verarbeiten, so müssten Sie den
Index-Operator nochmals durch die
Memberfunktion
// Klassendefinition der verketteten Liste
class List
{
Data *pData;
....
public:
....
Data& operator[](int index);
};
// Definition der Memberfunktion
Data& List::operator[](int index)
{
// Index-Zeiger auf 1. Element
List *pElement = this;
// Liste durchlaufen bis Element gefunden
for (;;)
{
// Falls Index jetzt 0 ist
if (index <= 0)
// Element gefunden
break;
// Falls Listenende erreicht
if (pElement->pNext == NULL)
// Letztes Element zurückgeben
break;
// nächstes Element holen
pElement = pElement->pNext;
// Index dekrementieren
index--;
}
// Referenz auf Datum zurückgeben
return *(pElement->pData);
}
CData operator[] (int iIndex)
const;
überladen. Indizierte const-Objekte können
selbstverständlich nur rechts vom
Zuweisungsoperator stehen. Beachten Sie
auch, dass nun das Objekt selbst und keine
Referenz mehr zurückgeliefert wird.
Würde anstelle einer Referenz auf die Liste ein Listenzeiger zum Einsatz kommen, so müsste der
Indexoperator dann wie folgt aufgerufen werden:
pList->operator[](index);
(*pList)[index];
bzw.
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (7 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Überladener Indexoperator und das Safe-Array
Mithilfe des überladenen Indexoperators können auch so genannte SafeArrays erstellt werden. Wie Sie bestimmt
noch wissen, nimmt der Compiler beim Zugriff auf Feldelemente keinerlei Überprüfung der zulässigen
Bereichsgrenzen vor, so dass auch Zugriffe über das Feld hinaus möglich sind. Bei einem SafeArray können diese
unzulässigen Zugriffe abgefangen werden. Sehen Sie sich dazu das folgende Beispiel an:
Beispiel für Safe-Array:
Das Safe-Array wird durch die (minimale) Klasse SaveArray realisiert. Diese Klasse überlädt den
Indexoperator [ ] um die indizierten Zugriffe auf die Elemente zu kontrollieren. Dazu erhält der Konstruktor
der Klasse die Anzahl der Elemente, merkt sich diese und erstellt dann das erforderliche Feld.
Wird nun indiziert auf dieses Safe-Array zugegriffen, so erfolgt innerhalb des überladenen Indexoperators
eine entsprechende Bereichsüberprüfung. Im Beispiel wird beim Über- bzw. Unterschreiten der Feldgrenzen
eine Meldung ausgegeben und entweder das erste oder letzte Feldelement zurückgegeben. In der Praxis ist dies
wiederum ein Fall für die Ausnahmebehandlung (Exception-Handling), die aber erst später erläutert wird.
Im Hauptprogramm wird ein solches Safe-Array erstellt und gefüllt. Beim Auslesen der Elemente wird jetzt
versucht, über die obere Feldgrenze hinaus zuzugreifen, was dann auch mit einer Fehlermeldung quittiert
wird. Aber nicht nur die Lesezugriffe werden kontrolliert sondern auch die Schreibzugriffe, was noch
wichtiger ist. Um dies zu demonstrieren wird im Programm versucht, das Element -1 zu beschreiben. Und
dieser Schreibzugriff wird ebenfalls mit einer Fehlermeldung quittiert. Im Beispiel wird beim Schreiben auf
ein unzulässiges Element entweder das erste bzw. das letzte Feldelement überschrieben. Aber wie schon
erwähnt, in einer realen Anwendung würden Sie in diesem Fall eine Ausnahmebehandlung durchführen.
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Bsarray.
Die Programmausgabe:
0. Wert: 0
1. Wert: 1
2. Wert: 2
3. Wert: 3
4. Wert: 4
5. Wert: 5
6. Wert: 6
7. Wert: 7
8. Wert: 8
9. Wert: 9
Index ueberschreitet obere Grenze!
10. Wert: 9
Versuch das Element -1 zu beschreiben!
Index kleiner 0!
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (8 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Das Programm:
// C++ Kurs
// Beispiel fuer ein Safe-Array
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Klassendefinition
class SafeArray
{
short
size;
// Groesse des Feldes
short
*pData;
// Zeiger auf Datenfeld
public:
SafeArray(short size);
~SafeArray();
short& operator[] (int index);
};
// Definition der Memberfunktionen
// Konstruktor
SafeArray::SafeArray(short s)
{
// Groesse des Feldes merken
size = s;
// Feld anlegen
pData = new short[size];
}
// Destruktor
SafeArray::~SafeArray()
{
// Feld freigeben
delete [] pData;
}
// Ueberladener Indexoperator
short& SafeArray::operator [](int index)
{
// Falls Index kleiner 0 ist
if (index<0)
{
cout << "Index kleiner 0!\n";
return pData[0];
}
// Falls Index ueber das Feld hinausgreift
if (index>=size)
{
cout << "Index ueberschreitet obere Grenze!\n";
return pData[size-1];
}
// Datum zurueckgeben
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (9 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
return pData[index];
}
// Hauptprogramm
// =============
int main()
{
const short ARRAYSIZE = 10;
short
loop;
// Feldgroesse
// Schleifenzaehler
// Feldobjekt mit 10 Eintraegen anlegen
SafeArray myArray(ARRAYSIZE);
// Feld fuellen
for (loop=0; loop<ARRAYSIZE; loop++)
myArray[loop] = loop;
// Feld wieder auslesen
// Es wird hier versucht hinter das Feld zu greifen
for (loop=0; loop<=ARRAYSIZE; loop++)
cout << loop << ". Wert: " << myArray[loop] << endl;
// Versuch das Element mit dem Index -1 zu lesen
cout << "Versuch das Element -1 zu beschreiben!\n";
myArray[-1] = 111;
}
Sehen wir uns zum Schluss dieser Vertiefung noch das Überladen der Operatoren new und delete an.
Überladen von new und delete
Beim Überladen der Operatoren new und delete müssen vier Fälle unterschieden werden:
Allgemein gültiges Überladen der Operatoren zur Reservierung eines einzelnen Datum.
Allgemein gültiges Überladen der Operatoren zur Reservierung von Felder.
Überladen der Operatoren für ein einzelnes Objekt.
Überladen der Operatoren für Objektfelder.
Der erste und zweite Fall wird in der Praxis selten eingesetzt, da hierbei verschiedene Probleme auftreten
können. Sehen wir uns deshalb diese Fälle nur in der Theorie an.
Allgemein gültiges Überladen von new und delete für einzelne Daten
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (10 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Soll der allgemein gültige new und delete Operator für einzelne Daten überladen werden, so werden die beiden
unten dargestellten Funktionen hierzu verwendet.
Der überladene new Operator erhält als
Parameter size die Anzahl der zu
reservierenden Bytes. Konnte entsprechend
Speicher reserviert werden, so muss der
Operator einen Zeiger auf den Anfang des
reservierten Speicherbereichs zurückliefern.
War nicht genügend Speicher vorhanden,
muss NULL zurückgegeben werden. Das
Problem beim Überladen des new Operators
besteht darin, den erforderlichen Speicher
zu reservieren. Sie dürfen dazu innerhalb
der Operator-Memberfunktion auf keinen
Fall selbst wieder new verwenden, da Sie
sonst die Funktion erneut aufrufen würden
(Endlos-Rekursion).
void* new (size_t size)
{
void *pMem;
pMem = .....
// hier Speicher reserv.
return pMem;
}
void delete (void *pMem)
{
....
// hier Speicher freigeben
}
Der überladene delete Operator erhält als Parameter einen Zeiger auf den freizugebenden Speicherbereich.
Allgemein gültiges Überladen new und delete für Felder
Soll der new und delete Operator für die
Reservierung von Speicher für Felder
überladen werden, müssen etwas
abgewandelte Funktionen definiert werden.
Nach dem Operatornamen folgt in beiden
Fällen der leere Indexoperator [ ]. Auch hier
erhält new wieder die Anzahl der zu
reservierenden Bytes für das gesamte Feld
und delete einen Zeiger auf den
freizugebenden Speicherbereich.
void* new [](size_t size)
{
void *pMem;
pMem = .....
// hier Speicher reserv.
return pMem;
}
void delete [](void *pMem)
{
....
// hier Speicher freigeben
}
Soviel zur Theorie. Gehen wir jetzt zur Praxis über und sehen und das Überladen der Operatoren für eine
bestimmte Klasse an.
Überladen von new und delete für einzelne Objekte
Die Operatoren new und delete können für eine Klasse nur durch eine statische Memberfunktion der Klasse
überladen werden. Auch wenn die Memberfunktion nicht explizit als statisch deklariert wird, wird sie durch den
Compiler immer als solche angelegt. Wenn Sie sich nicht mehr sicher sind was statische Memberfunktionen sind,
dann schauen Sie hier nach ( ).
Beginnen wir mit dem Überladen des new und delete Operators für einzelne Objekte. Hierzu sind folgende
Memberfunktionen zur Klasse hinzuzufügen:
static void operator new (size_t size); bzw.
static void operator delete (void *pMem);
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (11 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
Der Operator new erhält die Anzahl der für
das Objekt zu reservierenden Bytes als
Parameter übergeben. Der Datentyp size_t
ist ein vom Compiler vorgegebener Datentyp
zur Speicherreservierung und in der Regel
über ein typedef entweder ein unsigned int
oder unsigned long. Innerhalb der OperatorMemberfunktion muss dann der
erforderliche Speicher reserviert werden,
was im Beispiel rechts mit dem globalen new
Operator erfolgt. Als Ergebnis liefert die
Memberfunktion den Zeiger auf den
reservierten Speicher zurück.
// Klassendefinition
class Complex
{
....
public:
static void* operator new (size_t size);
static void operator delete(void *pMem);
....
};
// Überladener new Operator
void* Complex::operator new (size_t size)
{
// Speicher reservieren
void *pMem = new char[size];
Im Beispiel rechts wird bei erfolgreicher
// Speicher mit 0 initialisieren
Speicherreservierung zusätzlich der
memset(pMem,0,size);
komplette Speicherbereich mit 0 initialisiert.
// Zeiger auf Speicher zurückgeben
return pMem;
}
Der überladene delete Operator erhält als
// Überladener delete Operator
Parameter einen void-Zeiger auf den
freizugebenden Speicher und besitzt keinen void Complex::operator delete(void *pMem)
{
Returnwert. Die Freigabe des Speichers
// Speicher freigeben
erfolgt in der Regel wiederum mit dem
delete [] pMem;
globalen delete Operator. Beachten Sie dabei,
}
dass im new Operator ein char-Feld
reserviert wurde und deshalb beim Aufruf
des globalen delete Operators die eckigen
Klammern mit angegeben werden müssen!
Überladen von new und delete für Objektfelder
Soll der new und delete Operator für Objektfelder überladen werden, so sind hierfür folgende
Memberfunktionen einzusetzen:
static void operator new [] (size_t size); bzw.
static void operator delete [](void *pMem);
Diese Memberfunktionen unterscheiden sich nur durch die Angabe des Indexoperators nach dem
Operatornamen von den vorherigen Memberfunktionen für einzelne Objekte. Der new Operator erhält im
Parameter die Anzahl der zu reservierenden Bytes für das gesamte Objektfeld.
Vielleicht fragen Sie sich jetzt: wozu sollte ich eigentlich new und delete überladen? Die Antwort darauf ist: aus
Geschwindigkeits- und Speicherplatzgründen. Die Standard-Operatoren new und delete sind so ausgelegt, dass
Sie für alle erdenklichen Fälle die Speicherverwaltung übernehmen können. Dazu muss aber zwischen new und
delete eine gewisse 'Kommunikation' stattfinden. delete benötigt zur Freigabe des Speichers verschiedene
Informationen, so z.B. die Größe des freizugebenden Speichers. Dazu reserviert new etwas mehr Speicherplatz
als für das eigentliche Datum benötigt wird. In diesem zusätzlichen Speicherplatz wird dann u.a. die
Speichergröße abgelegt. Bei sehr kleinen Daten oder Objekten kann nun die Größe des zusätzlich benötigten
Verwaltungsspeichers die des Nutzdatenspeichers übertreffen. Für solche Fälle bietet es sich an, den new und
delete Operator zu überschreiben. Im überschriebenen new Operator wird dann z.B. Speicher für mehrere
Objekte auf einmal reserviert. Dieser Speicher wird intern verwaltet und bei jedem Aufruf des new Operators ein
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (12 von 18) [17.05.2005 11:39:48]
Überladen spezieller Operatoren
'Speicherstück' daraus an die Anwendung zurückgegeben. Der delete Operator stellt dann den freizugebenden
Speicher wieder in diesen großen Speicher zurück. Zugegeben, solche eine eigene Speicherverwaltung ist ein
nicht ganz triviales Unterfangen, aber kann auch sehr lohnend sein.
So, und damit sind Sie am Ende des Vertiefungsstoffs angekommen. Jetzt kommt wieder das Beispiel und dann
die Übung.
Beispiel und Übung
Das Beispiel:
Als Ausgangspunkt für dieses Beispiel dient die in der vorherigen Lektion vorgestellte Klasse Rect zur
Abspeicherung und Manipulation von Rechteckdaten. Hier können Sie sich dieses Beispiel nochmals
ansehen( ).
Zur Ausgabe der Rechteckdaten wird nun aber nicht mehr die Memberfunktion PrintRect(...) verwendet,
sondern der überladene Operator <<. Da im Hauptprogramm die Ausgabe der Rechteckdaten sowohl über
Objektzeiger wie auch direkt erfolgt, werden zwei überladene Funktionen verwendet.
Das unten stehende Beispiel finden Sie auch unter: le12\prog\Bumoper.
Die Programmausgabe:
1. Rechteck:
Rechteck auf (10,20) Groesse: (100,200)
2. Rechteck:
Rechteck auf (50,50) Groesse: (100,200)
1. Rechteck um eins verschoben:
Rechteck auf (11,21) Groesse: (100,200)
Rechtecke ueberschneiden sich! Gemeinsame Flaeche:
Rechteck auf (50,50) Groesse: (61,171)
Das Programm:
// C++ Kurs
// Beispiel zu ueberladenen Operatoren
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
using std::ostream;
// Klassendefinition
class Rect
{
short
xPos, yPos;
short
width, height;
// Position
// Breite und Hoehe
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (13 von 18) [17.05.2005 11:39:49]
Überladen spezieller Operatoren
public:
Rect();
// ctors
Rect(short x, short y, short w, short h);
Rect(const Rect& source);
Rect operator ++(int);
// Verschiebt Rechteck um eine X/Y-Position
Rect operator & (const Rect& op2) const; // Verundet zwei Rechtecke
bool operator ==(const Rect& op2) const; // Vergleicht zwei Rechtecke
bool operator !() const;
// Prueft auf leeres Rechteck ab
// Ueberladen des Ausgabeoperators
friend ostream& operator << (ostream& OS, const Rect& op2);
friend ostream& operator << (ostream& OS, const Rect *pOp2);
};
// Definition der Memberfunktionen
// 1. Konstruktor (Standard Konstruktor)
Rect::Rect()
{
xPos = yPos = width = height = 0;
}
// 2. Konstruktor
// Erhaelt als Parameter die Rechteck-Daten als 4 short Werte
Rect::Rect(short x, short y, short w, short h)
{
xPos = x; yPos = y;
width = w; height = h;
}
// 3. Konstruktor
// copy-ctor
Rect::Rect(const Rect& source)
{
xPos = source.xPos; yPos = source.yPos;
width = source.width; height = source.height;
}
// Verschiebt Rechteck um eine X/Y-Position
// Achtung Postfixoperator!
// Darf keine const-Memberfunktion sein da das Objekt
// ja veraendert wird!
Rect Rect::operator ++(int)
{
// Hilfsobjekt zur Aufnahme der akt. Werte
// Ruft copy-ctor auf
Rect orig(*this);
// Nun erst Position veraendern
xPos++;
yPos++;
// Ursprungswerte zurueckgeben
return orig;
}
// Verundet zwei Rechtecke
// Das daraus resultierende Rechtecke enthaelt die Flaeche,
// die beiden Rechtecken gemeinsam ist
Rect Rect::operator & (const Rect& op2) const
{
// Resultierendes Rechteck
Rect result;
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (14 von 18) [17.05.2005 11:39:49]
Überladen spezieller Operatoren
// Hilfsvariablen
short end1, end2;
// Groesste X-Position ist resultierende X-Position
result.xPos = (op2.xPos>xPos) ? op2.xPos : xPos;
// X-Positionen der rechten Kante berechnen
end1 = xPos+width;
end2 = op2.xPos+op2.width;
// Kleinste X-Position bestimmte Breite des result. Rechtecks
result.width = (end1>end2) ? end2-result.xPos : end1-result.xPos;
// Nun das gleiche Spiel mit der Y-Position und der Hoehe
result.yPos = (op2.yPos>yPos) ? op2.yPos : yPos;
end1 = yPos+height;
end2 = op2.yPos+op2.height;
result.height = (end1>end2) ? end2-result.yPos : end1-result.yPos;
// Falls keine gemeinsame Flaeche vorhanden ist
// leeres Rechteck setzen
if ((result.width <= 0) || (result.height <= 0))
{
result.width = result.height = 0;
result.xPos = result.yPos = 0;
}
return result;
}
// Vergleicht zwei Rechecke
bool Rect::operator ==(const Rect& op2) const
{
// Rechtecke sind gleich wenn Koordinaten und Ausdehnung gleich sind
if ((xPos == op2.xPos) && (width == op2.width) &&
(yPos == op2.yPos) && (height == op2.height))
return true;
else
return false;
}
// Prueft ab, ob das Rechteck eine Flaeche besitzt
bool Rect::operator ! () const
{
if ((width == 0) || (height == 0))
return true;
else
return false;
}
// Ueberladene Operatorfunktion zur Ausgabe der Rechteckdaten
// Diese Funktion wird aufgerufen, wenn ein Objekt ausgegeben wird
ostream& operator << (ostream& OS, const Rect& op2)
{
OS << "Rechteck auf (" << op2.xPos << "," << op2.yPos << ") ";
OS << "Groesse: (" << op2.width << "," << op2.height << ")\n";
return OS;
}
// Diese Funktion wird aufgerufen, wenn ein Objekt ueber einen
// Objektzeiger ausgegeben wird
ostream& operator << (ostream& OS, const Rect *pOp2)
{
OS << "Rechteck auf (" << pOp2->xPos << "," << pOp2->yPos << ") ";
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (15 von 18) [17.05.2005 11:39:49]
Überladen spezieller Operatoren
OS << "Groesse: (" << pOp2->width << "," << pOp2->height << ")\n";
return OS;
}
//
// Hauptprogramm
// =============
int main()
{
// Zwei Rect Objekte erstellen
Rect *pFirstRect = new Rect(10,20,100,200);
Rect *pSecondRect = new Rect(50,50,100,200);
// Rechteckdaten ausgeben
cout << "1. Rechteck:\n";
cout << pFirstRect;
cout << "2. Rechteck:\n";
cout << pSecondRect;
// Rechtecke vergleichen
if (*pFirstRect == *pSecondRect)
cout << "Rechtecke sind gleich\n";
else
cout << "Rechtecke sind unterschiedlich\n";
// Erstes Objekt verschieben
// Klammerung unbedingt beachten!
(*pFirstRect)++;
cout << "1. Rechteck um eins verschoben:\n";
cout << pFirstRect;
// Neues Rechteck aus der gemeinsamen Flaeche der beiden
// Rechtecke bilden
Rect mergedRect = *pFirstRect & *pSecondRect;
// Abpruefen, ob beide Rechtecke eine gemeinsame Flaeche hatten
if (!mergedRect)
cout << "Keine Ueberschneidung der beiden Rechtecke!\n";
else
{
cout << "Rechtecke ueberschneiden sich! Gemeinsame Flaeche:\n";
cout << mergedRect;
}
delete pFirstRect;
delete pSecondRect;
}
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (16 von 18) [17.05.2005 11:39:49]
Überladen spezieller Operatoren
Die Übung:
Erweitern Sie die in vorherigen Übungen erstellte Klasse CString um die Operatoren << und >> zur Ausgabe
bzw. zum Einlesen eines CString-Objekts. Da nachher nur Objekte ausgegeben bzw. eingelesen werden sollen,
brauchen Sie keine Funktion für entsprechende Objektzeiger hier definieren. Beachten Sie beim Einlesen eines
CString-Objekts, dass Sie im Voraus nicht wissen wie lange die Eingabe ist.
Da für die Ausgabe der überladene Operator << verwendet wird, können Sie die Memberfunktion Print(...) aus
der Klasse CString entfernen.
Erstellen Sie im Hauptprogramm zwei CString-Objekte, denen Sie einen beliebigen Text bei ihrer Definition
zuweisen.
Geben Sie beide CString-Objekte mithilfe des überladenen Operators << aus.
Anschließend lesen Sie von der Tastatur mittels des Eingabestreams cin einen neuen Text für das erste CStringObjekt ein (Operator >>) und geben den eingelesenen String zur Kontrolle gleich wieder aus.
Zusatz:
Falls Sie die Ausführungen zum Überladen des Indexoperators gelesen haben, können Sie sich noch an
Folgendem versuchen:
Fügen zur Klasse noch den überladenen Indexoperator [ ] hinzu, um einzelne Zeichen des Strings
manipulieren zu können. Folgende Anweisungen sollen mit dem Indexoperator erlaubt sein:
char cZeichen = CStringObj[2];
CStringObj[3] = cZeichen;
Im ersten Fall wird das dritte(!) Zeichen des Strings zurückgegeben und im zweiten Fall das vierte(!) Zeichen
neu gesetzt.
Wandeln Sie dann den eingelesenen Text in Grossbuchstaben um, wobei Sie auf die einzelnen Stringzeichen
über den Operator [ ] zugreifen müssen. Zur Umwandlung von Kleinbuchstaben in Grossbuchstaben kann die
Bibliotheksfunktion toupper(...) verwendet werden. Die Funktion erhält als Parameter das zu konvertierende
Zeichen als int-Wert und liefert das konvertierte Zeichen ebenfalls als int-Wert zurück. Geben Sie dann den
umgewandelten Text wieder aus.
Das Problem hierbei besteht darin, dass Sie das Stringende irgend wie erkennen müssen. Sie könnten
hierfür eine weitere Memberfunktion der Klasse CString hinzufügen, die die Länge des Strings
zurückliefert. Versuchen aber einmal ohne zusätzliche Memberfunktion auszukommen. Die Lösung
dieses Problems liegt im überladenen Indexoperator.
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (17 von 18) [17.05.2005 11:39:49]
Überladen spezieller Operatoren
Die Programmausgabe:
1. Ausgabe
1.String: Es waren einmal zwei Ameisen
2.String: die wollten nach Amerika reisen
Bitte neuen Text fuer erstes String Objekt eingeben:
Wandeln nach Grossbuchstaben!
Die Eingabe war:
1.String: Wandeln nach Grossbuchstaben!
Alles in Grossbuchstaben:
1.String: WANDELN NACH GROSSBUCHSTABEN!
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie die
Lösung unter loesungen\le12\Lumoper.
Haben Sie die Lösung zur Erkennung des Stringendes gefunden?
Damit ist wieder eine Lerneinheit beendet. In der nächsten Lerneinheit lernen Sie einen Teil der C++ StandardBibliothek kennen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le12/le12_05.htm (18 von 18) [17.05.2005 11:39:49]
C++ Standard-Bibliothek
C++ Standard-Bibliothek
Die Themen:
Einleitung
Bibliotheks-Komponenten
Einleitung
Die Standard-Bibliothek stellt unter anderem allgemein gültige (generische) Datenstrukturen
und Algorithmen auf Basis von Templates zur Verfügung. Diese allgemeinen Datenstrukturen
und Algorithmen sind in der STL (Standard Template Library) zusammengefasst. Obwohl
Templates erst Thema einer späteren Lerneinheit sind, so können Sie Templates mit Ihrem
bisherigen Wissen jedoch schon einsetzen. Was Sie zurzeit noch nicht können, sind Templates
selber schreiben. Aber das wird sich auch noch ändern. Prinzipiell sind Templates Funktion
oder Klassen die mit beliebigen Datentypen umgehen können. Ein oft zitiertes Beispiel für ein
Funktions-Template ist zum Beispiel die Funktion swap(...), die es gestattet, die Werte zweier
Variablen oder zwei Objekte gegeneinander auszutauschen. Dem Funktions-Template swap(...)
ist es prinzipiell gleichgültig, ob sie zwei float-Werte oder gar zwei Objekte (zum Beispiel der
Fensterklasse Window) vertauscht. Template-Funktionen werden wie normale Funktionen
aufgerufen. Der Datentyp mit dem die Funktion arbeitet ergibt sich dann aus dem Datentyp der
Parameter.
http://www.cpp-tutor.de/cpp/le13/le13_01.htm (1 von 5) [17.05.2005 11:39:56]
C++ Standard-Bibliothek
Zur Veranschaulichung
dazu zwei Beispiele. Die
erste swap(...) Funktion
dient zum Vertauschen von
zwei float-Werten, während
die zweite swap(...)
Funktion zum Vertauschen
von zwei Window-Objekten
dient. Allein der Datentyp
der Parameter bestimmt,
mit welchen Datentyp die
Funktion swap(...) arbeitet.
// Funktion swap fuer float-Werte
float var1, var2;
....
swap(var1, var2);
// Funktion swap fuer zwei Window-Objekte
Window myWin, yourWin;
...
swap(myWin, yourWin);
Wichtig hierbei ist, dass die Template-Funktion nur einmal geschrieben wird, d.h. Sie müssen
nicht wie beim Überladen von Funktionen mehrere Funktionen swap(...) definieren. Dies
übernimmt automatisch der Compiler beim Übersetzen des Programms.
Noch ein Hinweis: wenn Sie Objekte mittels swap(...) vertauschen wollen, so sollte die
entsprechende Klasse dazu den Operator = überladen. Dies gilt insbesondere dann, wenn die
Klasse dynamische Eigenschaften enthält. Sie können sich das Überladen des Operators = hier
nochmals ansehen( ).
Kommen wir nun zur Definition eines Objekts eines Klassen-Templates. Da Templates den
aktuellen Datentyp, mit dem sie arbeiten, aus Parametern bestimmen, müssen Sie bei der
Definition eines Klassen-Template Objekts einen zusätzlichen Parameter mit angegeben. Dieser
Parameter wird innerhalb einer spitzen Klammer nach dem Klassen-Templatenamen und vor
dem Objektnamen gestellt.
Rechts sehen Sie dazu
// Vektor mit int definieren
wieder zwei Beispiele. Im
vector<int> myInts;
ersten Falle wird ein Objekt // Vektor mit Window-Objekten definieren
myInts vom Typ vector<int> vector<Window> myWindows
definiert, d.h. die TemplateKlasse myInts verarbeitet intWerte. Im zweiten Fall
wird ein Objekt der
gleichen Template-Klasse
vector definiert, diesmal
verarbeitet die Klasse
jedoch Window-Objekte.
Wie Sie später noch sehen
werden, gibt es die Klasse
vector in der STL
tatsächlich.
http://www.cpp-tutor.de/cpp/le13/le13_01.htm (2 von 5) [17.05.2005 11:39:56]
C++ Standard-Bibliothek
Solche generischen Klassen zum Abspeichern von Daten werden in der STL auch als Container
bezeichnet. In dieser Lektion lernen Sie einige spezielle Container kennen. Die allgemein
gültigen Container werden dann in einem späteren Kapitel noch behandelt.
Der Umgang mit Templates mag sich am Anfang vielleicht etwas kompliziert anhören. Sie
werden aber sehen, so besonders schwierig ist die Sache im Endeffekt dann aber auch nicht.
Außer solchen generischen Funktion und Klassen enthält die STL auch noch jede Menge
Algorithmen. So gibt es zum Beispiel einen Algorithmus sort(...) der es erlaubt, die Elemente
eines Containers nach einem bestimmten Kriterium zu sortieren. Diese Algorithmen werden
aber ebenfalls erst später behandelt, wenn Sie die allgemeinen Container kennen gelernt haben.
Die STL ist fester Bestandteil der Sprache C++ und in der ANSI-Spezifikation genau definiert.
Der wichtigste Aspekt der STL ist die strikte Trennung zwischen dem Abspeichern von Daten in
Containern und deren Verarbeitung mittels der Algorithmen. Container können (fast) beliebige
Daten aufnehmen und die Algorithmen arbeiten dann mit den verschiedensten Containern
zusammen, um zum Beispiel deren Inhalt zu sortieren oder zu modifizieren.
Da die STL zur Sprache C++ gehört, erhalten Sie diese auch mit Ihrem C++ Compiler
ausgeliefert. Ein Problem bei der Anwendung der STL ist jedoch, dass je nach verwendetem
Compiler (und dessen Alter!) die STL mehr oder weniger vollständig implementiert ist. Falls
aber Abweichung bezüglich der STL zwischen dem VC++ 6.0 Compiler, BORLAND C++
Compiler 5.5.1 oder dem MinGW C++ 3.x bestehen, so wird im Kurs darauf natürlich
hingewiesen.
Da sich die Implementierung STL mit der Compiler-Version ändern kann, versuchen
Sie zuerst immer die allgemeine Form des Containers bzw. des Algorithmus zu
verwenden. Erst wenn dies zu Übersetzungsfehlern führt, sollten Sie die compilierspezifische Variante verwenden. Ihr Programm ist dann aber nicht mehr portabel!
Sehen wir uns jetzt im Überblick an, aus welchen Teilen die Standard-Bibliothek besteht.
Bibliotheks-Komponenten
Container
Ein Container dient zur Aufnahme von Elementen
beliebigen (Daten-)Typs. Einer der einfachen
Containertypen ist der vektor. Er entspricht prinzipiell
einem Feld dessen Größe sich dynamisch an die in ihm
abgelegte Anzahl von Elementen anpasst.
http://www.cpp-tutor.de/cpp/le13/le13_01.htm (3 von 5) [17.05.2005 11:39:56]
C++ Standard-Bibliothek
Algorithmen
Algorithmen verarbeiten die in einem Container abgelegten Elemente.
Einer der in der STL vorhandenen Algorithmen sort(...) z.B. sortiert die
im Container enthaltenen Elemente.
Iteratoren
Die bisher noch nicht erwähnten Iteratoren sind Objekte mit deren Hilfe
die in einem Container enthaltenen Elemente durchlaufen werden
können. Ein Iterator repräsentiert dabei eine bestimmte Position
innerhalb eines Containers, d.h. er entspricht vom Prinzip her einem
Zeiger auf ein bestimmtes Element im Container.
Funktionsobjekte und Adapter
Funktionsobjekte sind Objekte die den Operator (...) (also den
Aufrufoperator) überladen. Sie sind das C++ Gegenstück zu
Funktionszeiger, aber viel mächtiger.
Die ebenfalls bisher noch nicht erwähnten Adapter dienen
prinzipiell zur Anpassung von Funktionen/Memberfunktionen an
eine vorgegebene Signatur (Funktions-/Memberfunktions-Name
einschließlich Parameter).
Aber keine Angst wenn Ihnen jetzt noch nicht alles sofort verständlich ist. Dies wird sich mit
Sicherheit im Verlaufe des Kurses noch ändern.
Damit genug der Vorwort, sehen wir uns nun den ersten Teil der Standard-Bibliothek einmal
an. Der Standard-Bibliothek ist sind später im Kurs noch weitere Lerneinheiten gewidmet.
http://www.cpp-tutor.de/cpp/le13/le13_01.htm (4 von 5) [17.05.2005 11:39:56]
C++ Standard-Bibliothek
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/le13_01.htm (5 von 5) [17.05.2005 11:39:56]
pair Datentyp
pair Datentyp
Die Themen:
Einleitung
Datentyp des pairs
make_pair Hilfsfunktion
Zugriff auf pair-Elemente
pair als Returnwert
pair-Operatoren
Beispiel und Übung
Einleitung
Der in der Standard-Bibliothek definierte Datentyp pair dient zum Zusammenfassen von zwei Daten,
deren Datentypen beliebig sein können. Mithilfe von pair können Sie nun sogar zwei Werte aus einer
Funktion/Memberfunktion zurückgeben, indem Sie als Returntyp der Funktion/Memberfunktion pair
verwenden. Dazu folgt später noch ein Beispiel.
Wenn Sie den Datentyp pair einsetzen, müssen Sie die Header-Datei <utility> einbinden. Außerdem liegt
der Datentyp pair, wie übrigens alle Standard-Bibliothek-Komponenten, im Namensraum std. Beachten
Sie dies bitte beim Einsatz der Standard-Bibliothek-Komponenten! Sie erhalten ansonsten eine
Fehlermeldung, dass die entsprechende Komponente nicht bekannt ist.
Allen in einem pair abgelegten Elementen ist eines gemeinsam: sie sind immer eine Kopie des
ursprünglichen Elements. Werden also nach dem Ablegen eines Elements in einem pair noch Änderungen
an dem Element vorgenommen, so wirken sich diese nicht auf das im pair abgelegte Element aus.
Datentyp des pair
pair für einfache Datentypen
Um ein Objekt des Datentyps pair zu definieren, geben Sie nach dem Datentyp pair in spitzen Klammern
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (1 von 12) [17.05.2005 11:40:01]
pair Datentyp
die beiden Datentypen an, die im pair Objekt abgelegt werden sollen. Im Anschluss daran folgt dann wie
üblich der Objektname. Geben Sie nach dem Objektnamen nichts weiter an, so werden die beiden
Elemente (bei einfachen Datentypen wie char, int usw.) mit 0 initialisiert.
Zusätzlich stellt pair noch
Konstruktore bereit, um ein pair
Objekt bei seiner Definition auch
gleich mit Werten zu initialisieren
(zweite Anweisung rechts) oder eine
Kopie eines bestehenden pair Objekts
zu erstellen (dritte Anweisung). Sie
müssen bei der Initialisierung eines
pair Objekts natürlich auf die
Datentypen achten, mit denen Sie das
pair Objekt initialisieren.
// pair für einen int und einen long
std::pair<int, long> emptyPair;
// pair für char* und float
std::pair<char*, float>
cfPair("any text", 1.2f);
// Kopieren eines pair Objekts
std::pair<char*, float> copyPair(cfPair);
pair für Objekte
Außer einfache Datentypen können in einem pair Objekt auch Objekte abgespeichert werden. Diese
Objekte müssen aber die folgenden Eigenschaften besitzen:
Sie müssen kopierbar sein. Bei Objekten die dynamische Eigenschaften oder andere Objekte
enthalten, muss die Objektklasse den Kopierkonstruktor definieren.
Sie müssen zuweisbar sein, d.h. das Verhalten des Zuweisungsoperators muss definiert sein. Bei
Objekten die dynamische Eigenschaften oder andere Objekte enthalten, muss die Objektklasse
dazu den Zuweisungsoperator = überladen.
Sie müssen vergleichbar sein. Dazu muss die Objektklasse die Operatoren < und == durch friendFunktionen überladen. Die restlichen Operatoren, wie zum Beispiel <= oder != werden dann aus
diesen beiden überladenen Operatoren gebildet.
Rechts sehen Sie eine Klasse, die die // Klasse die die pair Anforderung erfuellt
oben genannten Anforderungen
class Any
erfüllt. Beachten Sie bitte, dass die
{
Vergleichsoperatoren < und == durch
....
eine friend-Funktion überladen sind.
public:
Wie der Vergleich von pair Daten
// Standard-ctor fuer Initialisierung
letztendlich durchgeführt wird, das
// von leerem pair
sehen wir uns nachher gleich an.
Any();
// copy-ctor
Any(const Any& CObj);
// Ueberladener Zuweisungsoperator
Any& operator = (const Any& COp2);
// Ueberladener < Operator
friend bool operator < (const Any& COp1,
const Any& COp2);
// Uberladener == Operator
friend bool operator == (const Any& COp1,
const Any& COp2);
....
};
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (2 von 12) [17.05.2005 11:40:01]
pair Datentyp
Um ein Objekt in einem pair
abzulegen, geben Sie innerhalb der
spitzen Klammer die Klasse des
Objekts an. Die im pair abgelegten
Daten können dabei beliebig aus
Objekten, Objektzeigern, StandardDatentypen und Zeigern
zusammengesetzt sein. Ja ist sogar
möglich, als Datentyp ein weiteres
pair zu verwenden.
// Leeres pair mit Any-Objekt und float
std::pair<Any, float> emptyPair;
// pair mit zwei Window-Objekten
std::pair<Window, Window>
winPair(Window(...), Window(...));
// pair mit string und Window-Zeiger
std::pair<string, Window*>
swPair("Mein Fenster", new Window(...));
Sehen wir uns jetzt aber einmal genauer an was vonstatten geht, wenn Sie ein pair definieren in dem
Objekte abgelegt werden.
Wenn ein leeres pair definiert
wird, so wird zuerst der
Standard-Konstruktor der
Klasse ausgeführt damit das
im pair abgelegte Objekt
initialisiert ist (Obj-Nr 1
rechts in der
Programmausgabe). Bei der
anschließenden Zuweisung
wird dann zunächst ein
temporäres Objekt erzeugt
(Obj-Nr 2). Dieses temporäre
Objekte wird per
Kopierkonstruktor in ein
neues, weiteres pair Objekt
übertragen (das ist das rechts
vom = Operator!) (Obj-Nr 3).
Erst dann wird dieses neue
pair per Zuweisung dem
bisherigen leerem pair
zugewiesen, was wiederum
zum Aufruf des
Zuweisungsoperator des
Objekts führt. Und zum
Schluss wird das Objekt im
pair rechts vom
Zuweisungsoperator (Obj-Nr.
3) sowie das temporäre
Objekte (Obj-Nr 2) gelöscht.
Sie sehen, dieses Verfahren ist
relativ aufwändig.
// Leeres pair mit anschl. Zuweisung
std::pair<Any, float> aPair;
aPair = std::pair<Any,float>(Any(...), 1.2f);
// Objekte getrennt erstellen und dann
// in pair aufnehmen
Any myObj(...);
std::pair<Any, float> bPair(myObj, 2.0f);
// Objekt bei pair Definition erzeugen
std::pair<Any, float> cPair(Any(...),3.0f);
// pair kopieren
std::pair<Any, float> dPair(initPair);
Programmausgabe:
std-ctor Obj-Nr:1
ctor Obj-Nr:2
copy-ctor Obj-Nr:3
operator= Obj-Nr:1
dtor Obj-Nr:3
dtor Obj-Nr:2
ctor Obj-Nr:4
copy-ctor Obj-Nr:5
ctor Obj-Nr:6
copy-ctor Obj-Nr:7
dtor Obj-Nr:6
copy-ctor Obj-Nr:8
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (3 von 12) [17.05.2005 11:40:01]
pair Datentyp
Es wird zuerst ein Objekt
erstellt (Obj-Nr. 4) und diese
dann einem pair bei seiner
Definition zugewiesen. Hier
wird bei Initialisierung des
pairs der Kopierkonstruktor
ausgeführt (Obj-Nr. 5).
Bei der Definition eines pair
wird auch gleichzeitig das
darin abzulegende Objekt
definiert. In diesem Fall wird
zuerst ein temporäres Objekt
erzeugt (Obj-Nr. 6), das dann
per Kopierkonstruktor in das
pair übernommen wird (ObjNr 7). Nach der Übernahme
wird dieses temporäre Objekt
wieder gelöscht.
Wird ein pair bei seiner
Definition mit einem anderen
pair initialisiert, so wird nur
der Kopierkonstruktor der
Objektklasse aufgerufen.
Die in einem pair abgelegten Objekte werden beim Zerstören des pair ebenfalls zerstört, d.h. es wird der
Destruktor des im pair abgelegten Objekts automatisch aufgerufen.
Obacht geben müssen Sie, wenn Sie
statt Daten oder Objekte Zeiger in
einem pair ablegen. Zum einen wird,
wenn Objektzeiger im pair abgelegt
sind, beim Zerstören des pair nicht
mehr automatisch das über den
Zeiger referenzierte Objekt gelöscht,
sondern nur der Objektzeiger. Sie
müssen das Objekt also explizit selbst
(vorher!) entfernen.
// pair mit Objektzeiger erzeugen
std::pair<Any*, float>
pPair(new Any(...), 4.0f);
...
// pair duplizieren, dupliziert Objektzeiger!
std::pair<Any*, float> cPair(pPair);
Und zum anderen wird beim Duplizieren eines solchen pair nur der Zeiger dupliziert. Damit verweisen
beide Zeiger des pair auf die gleiche Speicherstelle bzw. auf das gleiche Objekt! Das Gleiche trifft auch
dann zu, wenn Sie zwei solche pair einander zuweisen; auch hier werden nur die Zeiger zugewiesen. Seien
Sie also vorsichtig mit pair die Zeiger enthalten!
pair Felder
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (4 von 12) [17.05.2005 11:40:01]
pair Datentyp
Von pair Objekten lassen sich auch
std::pair<std::string, float> myPairs[] = {
entsprechende Felder bilden. Mithilfe
std::pair<std::string,float>("Toast",1.60f),
des Funktions-Template make_pair(...)
std::pair<std::string,float>("Butter",1.05f)
lässt sich solch ein Feld auch recht
};
einfach initialisieren (zweite
Initialisierung rechts).
make_pair Hilfsfunktion
Die Standard-Bibliothek enthält das Funktions-Template make_pair(...) die es erlaubt, ein pair ohne die
explizite Angabe der pair-Datentypen zu erstellen. Dazu erhält make_pair(...) lediglich die beiden Daten als
Parameter übergeben, die im pair-Objekt abgelegt werden sollen.
Im Beispiel rechts wird zunächst ein
leeres pair Objekt definiert. Diesem
pair Objekt wird dann einmal normal
'ein' Wert zugewiesen und einmal
mithilfe des make_pair(...) FunktionsTemplate.
// Definition des pair Objekts
std::pair<char*, float> myPair;
// Zuweisung ohne make_pair(...)
myPair = std::pair<char*,float>
("Toast",1.60f);
// Zuweisung mit make_pair
myPair = std::make_pair("Toast",1.60f);
Haben Sie ein pair definiert bei dem ein Element vom Typ std::string ist, so müssen Sie beim
Aufruf von make_pair(...) auch ein string-Objekt angegeben und keinen C-String. Die folgende
Anweisung erzeugt deshalb einen Fehler:
std::pair<std::string, int> myPair;
myPair = std::make_pair("any c-string",1);
Richtig funktionierts so:
std::pair<std::string, int> myPair;
myPair = std::make_pair(std::string("any string"),1);
Zugriff auf pair-Elemente
Da pair in der Standard-Bibliothek
als struct-Datentyp realisiert ist (alle
Elemente sind public!), kann direkt
auf die beiden im pair abgelegten
Werte zugegriffen werden. Das erste
Element ist über den Elementnamen
first erreichbar und das zwei Element
über den Name second.
// pair Definition
std::pair<std::string, float>
myPair("Toast",1.60f);
// Ausgabe der beiden pair-Elemente
std::cout << myPair.first << ','
<< myPair.second << std::endl;
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (5 von 12) [17.05.2005 11:40:01]
pair Datentyp
pair als Returnwert
Der Datentyp pair lässt sich auch sehr gut als Returntyp von Funktionen/Memberfunktionen einsetzen die
mehr als einen Wert zurückliefern. Sehen Sie sich dazu einmal das nachfolgende Beispiel an.
Die Funktion CalcSqrt(...) dient zur
Berechnung der Quadratwurzel aus
einer Zahl. Als Returnwert liefert sie
ein pair, in dessen ersten Element
first der Erfolg oder Misserfolg der
Wurzelberechnung in Form eines
bool-Werts abgelegt ist. Konnte die
Wurzel erfolgreich berechnet werden,
so enthält das zweite Element die
Wurzel aus dem übergebenem Wert.
// Funktion zur Berechnung einer Wurzel
inline std::pair<bool,double>
CalcSqrt(double val)
{
if (val < 0)
return make_pair(false,0.0f);
else
return make_pair(true,sqrt(val));
}
...
// pair fuer Returnwert
Beachten Sie auch, dass die Funktion std::pair<bool,double> result;
CalcSqrt(...) als inline-Funktion
...
definiert ist damit der Aufruf der
// Wurzel berechnen lassen
Funktion keinen unnötigen Overhead result = CalcSqrt(-1.44);
erzeugt. Außerdem wird zu
// Ergebnis abpruefen
Erzeugung des pair Rückgabewerts
if (!result.first)
die Funktion make_pair(...)
std::cout << "sqrt() Fehler!";
verwendet.
else
std::cout << "Wurzel: " << result.second;
pair-Operatoren
Der Datentyp pair überlädt u.a. den
class Any
Zuweisungsoperator. Der
{
Zuweisungsoperator weist die beiden
....
Elemente first und second einander
Any& operator = (const Any& rhs)
direkt zu. Wenn Sie Objekte in einem
{
pair ablegen, so sollte die Klasse des
... // Zuweisungen der Any-Elemente
Objekts den Zuweisungsoperator also
}
entsprechend überladen.
};
...
std::pair<int,Any> firstPair(1,Any(...));
std::pair<int,Any> secondPair;
...
// Ruft = Operator von Any auf
secondPair = firstPair;
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (6 von 12) [17.05.2005 11:40:01]
pair Datentyp
Außer dem Zuweisungsoperator
class Any
überlädt pair noch die
{
Vergleichsoperatoren < und ==. Die
....
restlichen Vergleichsoperatoren, wie
friend bool operator < (const Any& lhs,
z.B. >= und !=, werden aus den beiden
const Any& rhs);
überladenen Operatoren gebildet.
friend bool operator == (const Any& lhs,
const Any& rhs);
Wenn ein pair Objekte enthält und
Sie diese pair miteinander vergleichen };
wollen, so muss die Klasse der
...
Objekte die beiden Operatoren < und std::pair<int,Any> firstPair(1,Any(...));
== durch friend-Funktionen
std::pair<int,Any> secondPair(2,Any(...));
überladen.
std::pair<Any,int> myPair(Any(...),3);
std::pair<Any,int> yourPair(Any(...),1);
Beim Vergleichen von pair Objekten ...
// Vergleicht zuerst int und dann Any
hat das Element first Vorrang. D.h.
nur wenn der Vergleich von first true if (firstPair < secondPair)
....
ist, wird das Element second mit in
der Vergleich einbezogen. Achten Sie // Vergleicht zuerst Any und dann int
if (myPair != yourPair)
deshalb bei der Definition eines pair
...
Objekts darauf, dass die pair
Elemente in der richtigen Reihenfolge // Direkter Vergleich der Elemente immer
// möglich
definiert sind. Wollen Sie zum
if (myPair.first == yourPair.first)
Beispiel ein pair Objekt für eine
...
Gehaltsliste (bestehend aus einem
string für den Namen und einen float
für das Gehalt) nach Namen
vergleichen, so muss der Name als
erstes angegeben werden. Wenn Sie
dagegen die Gehälter vergleichen
wollen, so sollten Sie das Gehalt als
erstes angegeben. Die Reihenfolge ist
aber nur dann relevant, wenn Sie die
pair Objekte direkt miteinander
vergleichen. Es bleibt Ihnen aber
immer die Option offen, die Elemente
eines pair direkt miteinander zu
vergleichen.
Sie werden in Zukunft bei überladenen Operatoren des öfteren Parameter mit den Bezeichnern rhs
bzw lhs finden. rhs steht hier für right hand side und gibt den rechten Operanden des Operators an
und lhs (left hand side) den linken Operanden.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (7 von 12) [17.05.2005 11:40:01]
pair Datentyp
Das Beispiel:
Das Beispiel implementiert eine Gehaltsliste in Form eines pair-Feldes. Da später die Gehälter und nicht
die Namen verglichen werden sollen, enthält das pair als erstes Datum das Gehalt.
Damit die Handhabung von Objekten in pair Objekten ersichtlich wird, ist der Name innerhalb der
Klasse Demo als string abgelegt. Die Klasse Demo enthält alle notwendigen
Memberfunktionen/Operatoren um ein Objekt dieser Klasse in einem pair verarbeiten zu können.
Zusätzlich enthält die Klasse noch den überladenen << Operator für die Ausgabe des Namens. Beachten
Sie auch die Abfrage am Anfang der Operator-Memberfunktion =.
Im Hauptprogramm wird dann zunächst ein pair-Feld erstellt und initialisiert. Die Anzahl der
Feldelemente wird mithilfe des sizeof(...) Operators berechnet.
Anschließend werden alle pair-Elemente ausgegeben.
Zum Schluss wird noch das Einkommen von zwei Feld-Elementen verglichen. Beachten Sie auch die
'trickreiche' Ausgabe des Vergleichs. Für den Vergleich wird der Bedingungsoperator ?: verwendet.
Liefert die Auswertung des Vergleichsausdrucks true, so wird der String "weniger" an cout übergeben
und im anderen Fall der String "mehr".
Das unten stehende Beispiel finden Sie auch unter: le13\prog\Bpair.
Die Programmausgabe:
Pair-Array erstellen:
param-ctor
copy-ctor
dtor
param-ctor
copy-ctor
dtor
param-ctor
copy-ctor
dtor
Name: 5000.00 Einkommen: Gustav Gans Goldtaler
Name: 3000.00 Einkommen: Daisy Duck Goldtaler
Name: 100000.00 Einkommen: Dagobert Duck Goldtaler
Gustav Gans verdient mehr als Daisy Duck
dtor
dtor
dtor
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (8 von 12) [17.05.2005 11:40:01]
pair Datentyp
Das Programm:
// C++ Kurs
// Beispiel zu STL: pair
#include
#include
#include
#include
<iostream>
<iomanip>
<utility>
<string>
using std::cout;
using std::endl;
using std::string;
// Demo Klasse
class Demo
{
string text;
public:
Demo()
{
cout << "std-ctor" << endl;
}
Demo(const char* pText): text(pText)
{
cout << "param-ctor" << endl;
}
Demo(const Demo& obj): text(obj.text)
{
cout << "copy-ctor" << endl;
}
~Demo()
{
cout << "dtor" << endl;
}
const string& GetString() const
{
return text;
}
Demo& operator = (const Demo& lhs)
{
if (this == &lhs)
// Zuweisung auf sich selbst abfangen!
return *this;
text = lhs.text;
cout << "= operator" << endl;
return *this;
}
friend bool operator < (const Demo& rhs, const Demo& lhs);
friend bool operator == (const Demo& rhs, const Demo& lhs);
friend std::ostream& operator << (std::ostream& os, const Demo& obj);
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (9 von 12) [17.05.2005 11:40:01]
pair Datentyp
};
bool operator < (const Demo& lhs, const Demo& rhs)
{
return lhs.text < rhs.text;
// string ueberlaedt < Operator
}
bool operator == (const Demo& rhs, const Demo& lhs)
{
return rhs.text == lhs.text; // string ueberlaedt == Operator
}
std::ostream& operator << (std::ostream& os, const Demo& obj)
{
cout << obj.text;
return os;
}
// Hauptprogramm
// =============
int main()
{
size_t index;
// pair-Array mit double/Demo-Objekten initialisieren
cout << "Pair-Array erstellen:\n";
std::pair<double,Demo> income[] = {
std::make_pair(5000.00,Demo("Gustav Gans")),
std::make_pair(3000.00,Demo("Daisy Duck")),
std::make_pair(100000.00,Demo("Dagobert Duck"))};
// Anzahl der Eintraege im pair-Array
const size_t SIZE = sizeof(income)/sizeof(income[0]);
// Ausgabe auf 2 Dezimalstellen
cout << std::fixed<< std::setprecision(2);
// pair-Array ausgeben
for (index=0; index<SIZE; index++)
{
cout << "Name: " << income[index].first;
cout << "\tEinkommen: " << income[index].second << " Goldtaler\n";
}
// Vergleich zweier Einkommen
cout << income[0].second << " verdient ";
cout << ((income[0] < income[1])? "weniger" : "mehr");
cout << " als " << income[1].second << endl;
}
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (10 von 12) [17.05.2005 11:40:01]
pair Datentyp
Die Übung:
Schreiben Sie ein Programm zum Abspeichern von Aktienkursen, wobei jeder Kurs aus einem Namen
(als string-Datentyp) und dem Aktienwert (als float-Wert) besteht. Die Aktienkurse sind in einem
entsprechenden pair-Feld abzulegen. Initialisieren das pair-Feld mit folgenden Werten:
VW
111.11f
BMW
222.22f
Daimler Crysler 333.33f
General Motors 444.44f
Das pair-Feld ist so anzulegen, dass die Aktienkurse auf einfache Weise verglichen werden können.
Geben Sie alle Aktienkurs aus.
Erhöhen Sie danach den Kurs der VW-Aktie um 111.11 und sortieren Sie die Aktienkurse aufsteigend
nach ihrem Kurswert.
Geben Sie zur Kontrolle nochmals alle Aktienkurse aus.
Die Programmausgabe:
Aktienkurse:
-----------:
Hersteller: VW, Kurs: 111.11
Hersteller: BMW, Kurs: 222.22
Hersteller: Daimler Crysler, Kurs: 333.33
Hersteller: General Motors, Kurs: 444.44
Kurse nach Wert/Hersteller sortiert:
-----------------------------------Hersteller: BMW, Kurs: 222.22
Hersteller: VW, Kurs: 222.22
Hersteller: Daimler Crysler, Kurs: 333.33
Hersteller: General Motors, Kurs: 444.44
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden
Sie die Lösung unter loesungen\le13\Lpair.
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (11 von 12) [17.05.2005 11:40:01]
pair Datentyp
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/le13_02.htm (12 von 12) [17.05.2005 11:40:01]
STL Hilfstemplates
STL Hilfstemplates
Die Themen:
min<>, max<> und swap<>
numeric_limits<>
min<>, max<> und swap<>
min<>, max<>
Zum Ermitteln des kleinsten bzw. größten Wertes aus zwei Werten stellt die StandardBibliothek die beiden Funktions-Templates min(...) und max(...) zur Verfügung. Wenn Sie diese
Funktions-Templates verwenden, müssen Sie die Header-Datei <algorithm> einbinden. Wie alle
Standard-Bibliothek-Komponenten liegen min(...), max(...) und auch das gleich noch behandelte
Funktions-Template swap(...) im Namensraum std.
min(...) bzw. max(...) können
zum einen zur Ermittlung
des kleinsten bzw. größten
Werts aus StandardDatentypen verwendet
werden. Hierbei spielt es
dann keine Rolle, ob Sie
Variablen (1. Beispiel
rechts) oder Konstanten
vergleichen (2. Beispiel
rechts).
short sval1, sval2;
...
short smin = std::min(sval1, sval2);
...
double dval;
...
double dmax = std::max(dval1, 10.0);
Außer einfachen Datentypen können Sie aber auch das 'kleinste' bzw. 'größte' Objekt von zwei
Objekten ermitteln lassen. Es versteht sich von selbst, dass die beiden zu vergleichenden
Objekte und der Rückgabewert vom gleichen Typ sein müssen.
http://www.cpp-tutor.de/cpp/le13/le13_03.htm (1 von 5) [17.05.2005 11:40:03]
STL Hilfstemplates
Die Klasse der zu
vergleichenden Objekte
muss für den Vergleich die
beiden Operatoren < und
== durch friend-Funktionen
überladen sowie für die
Rückgabe des Ergebnisses
eventuell den
Kopierkonstruktor und den
überladenen Operator =
definieren.
#include <algorithm>
#include <utility>
using namespace std::rel_ops;
class Any
{
...
friend bool operator <
(const Any& lhs, const Any& rhs);
friend bool operator ==
(const Any& lhs, const Any& rhs);
};
... // friend-Funktionen definieren
Damit die Funktions...
Templates min(...)/max(...)
Any obj1(...), obj2(...);
den Vergleich durchführen ...
können, benötigen Sie
Any resObj = std::min(obj1, obj2);
außer den überladenen
Operatoren < und == noch
weitere
Vergleichsoperatoren, die
aber durch die StandardBibliothek intern aus den
beiden überladenen
Operatoren gebildet
werden. Diese internen
Vergleichsoperatoren liegen
im Namensraum
std::rel_ops und werden
durch die Header-Datei
<utility> zur Verfügung
gestellt.
Der VC++ Compiler (mindestens bis zur Version 6.0) definiert (für die WINDOWSProgrammierung) eigene min(...)/max(...) Funktionen, die aber mit den StandardBibliothek Funktions-Templates nicht identisch sind. Damit Sie unter VC++ auch die
Standard-Bibliothek Funktions-Templates nutzen können, müssen Sie folgende
Anweisungen zu Ihrem Programm hinzufügen:
http://www.cpp-tutor.de/cpp/le13/le13_03.htm (2 von 5) [17.05.2005 11:40:03]
STL Hilfstemplates
#if defined (_MSC_VER)
namespace std
{
template <typename T> T min(const T& O1, const T& O2)
{return _MIN(O1,O2);}
template <typename T> T max(const T& O1, const T& O2)
{return _MAX(O1,O2);}
}
#endif
swap<>
Zum Vertauschen von zwei
Werten oder Objekten
enthält die StandardBibliothek das FunktionsTemplate swap(...).
Beachten Sie beim
Vertauschen von Objekten,
dass hier Zuweisungen
stattfinden und je nach
Eigenschaften der zu
vertauschenden Objekte
der Zuweisungsoperator
überladen werden sollte
(Stichwort: dynamische
Eigenschaften und
eingeschlossene Objekte).
int val1, val2;
Window win1(...), win2(...);
...
std::swap(val1,val2);
std::swap(win1,win2);
numeric_limits<>
Einer der am meist genannten Kritikpunkte an der Sprache C++ betrifft die
plattformabhängige Darstellung von numerischen Werten. So kann es durch aus einmal
passieren, dass auf einem System ein int-Wert 16-Bit belegt und auf einem anderen 32-Bit.
Damit aber zur Programmlaufzeit die Kenndaten von numerischen Werten ermittelt werden
können, stellt die Standard-Bibliothek Template-Klassen vom Typ numeric_limits zur
Verfügung. Auch diese Klassen liegen im Namensraum std und für ihre Verwendung muss die
Headerdatei <limits> eingebunden werden.
http://www.cpp-tutor.de/cpp/le13/le13_03.htm (3 von 5) [17.05.2005 11:40:03]
STL Hilfstemplates
Um eine bestimmte
'Eigenschaft' eines
numerischen Wertes zu
bestimmen, wird zuerst der
Klassenname numeric_limits
angegeben und danach in
spitzen Klammern der
Datentyp, dessen
Eigenschaft ermittelt
werden soll. Im Anschluss
daran folgt der
Gültigkeitsbereichsoperator
:: und danach der Name
der zu ermittelnden
Eigenschaft.
#include <limits>
// max. Wert eines ints
std::numeric_limits<int>::max();
// Rundungsfehler eines float-Werts
std::numeric_limits<float>::round_error();
Beachten Sie in der nachfolgenden Aufstellung der Eigenschaften, dass einige Eigenschaften als
Zahlenwerte vorliegen während andere über entsprechenden Memberfunktionen ermittelt
werden. Die Tabelle enthält eines Übersicht über die wichtigsten, durch numeric_limits zur
Verfügung gestellten, Eigenschaften für numerische Datentypen.
bool is_specialized Für den Datentyp gibt es weitere Kenndaten in numeric_limits. Nur
wenn diese Eigenschaft true ist gelten auch die folgenden
Kenndaten.
bool is_modulo
Operationen auf den Datentyp können zum Überlauf führen. So
kann eine Addition von zwei positiven Zahlen als Ergebnis eine
negativ Zahl zur Folge haben.
int digits10
Anzahl der Dezimalzahlen die immer gültig sind. Bei digits10+1
Dezimalstellen kann bei weiteren Rechenoperationen ein
Rechenfehler auftreten. Beim Datentyp short ist digits10 z.B. 4, d.h.
erst wenn ein short-Wert 5 Stellen besitzt kann zum Beispiel ein
Überlauf auftreten. Mithilfe des digits10+1 Wertes können Sie
zum Beispiel mittels setprecision die max. Anzahl der
auszugebenden Stellen festlegen. Beim short-Datentyp wären dies
dann 5 Stellen.
min( )
Liefert den kleinsten Wert des Datentyps zurück.
max( )
Liefert den größten Wert des Datentyps zurück.
epsilon( )
Nur für Gleitkommazahlen; liefert den kleinsten von 1.0
abweichenden Wert und ist damit auch ein Maß für die
Genauigkeit der Gleitkommazahl.
Es gibt noch eine ganze Reihe weiterer Eigenschaften die in numeric_limits spezifiziert sind.
Sehen Sie dazu bitte in der Online-Hilfe zu Ihrem Compiler nach.
http://www.cpp-tutor.de/cpp/le13/le13_03.htm (4 von 5) [17.05.2005 11:40:03]
STL Hilfstemplates
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/le13_03.htm (5 von 5) [17.05.2005 11:40:03]
STL Container
STL Container
Die Themen:
stack Container
queue Container
priority_queue Container
bitset Container
Beispiel und Übung
stack Container
In dieser Lektion werden wir uns vier spezielle Container der STL ansehen. Die allgemeinen
Container, wie z.B. vector, folgen in einem späteren Kapitel.
Beginnen werden wir mit dem Container stack. Und wie alle STL-Komponenten liegt auch der stackContainer wieder im Namensraum std. Die explizite Angabe des Namensraums wurde in den
folgenden Beispiel der Übersichtlichkeit wegen weggelassen.
Das Einsatzgebiet einer Stack-Klasse sollten Sie aus dem bisherigen Kursverlauf schon kennen, zumal
Sie sich in den vorangegangen Lektionen auch schon selbst einmal an der Realisierung einer solchen
Klasse versuchen konnten. Trotzdem zur Erinnerung noch einmal: eine Stack-Klasse dient zum
temporären Abspeichern von Daten, wobei der zuletzt abgelegte Wert auch wieder als erstes
ausgelesen wird. Ein Stack ist also ein LIFO-Speicher (LIFO = last in, first out).
Da wir uns hier mit der STL befassen, weist der stack Container der STL gegenüber unseren
bisherigen Implementierungen einen wichtigen Unterschied auf: er ist als Template realisiert und
damit für alle Datentypen gültig, d.h. der stack Container kann für die Ablage von beliebigen
Standard-Datentypen (wie z.B. int oder double) und zur Ablage von Objekten eingesetzt werden. Die
einzige 'Einschränkung' besteht darin, dass alle in einem Stack abgelegten Daten den selben Datentyp
besitzen müssen.
Für die Verwendung des stack Containers müssen Sie die Datei <stack> einbinden.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (1 von 19) [17.05.2005 11:40:08]
STL Container
Wenn Sie Objekte in einem Stack ablegen wollen, so sollte die entsprechende Klasse den
Zuweisungsoperator, den Kopierkonstruktor und den Destruktor definieren wenn sie nicht
trivial ist. Eine nicht-triviale Klasse enthält entweder dynamische Eigenschaften oder
weitere eingeschlossene Objekte. Ferner sollten die Vergleichsoperatoren < und == für die
Klasse definiert sein. Diese Anforderungen an eine Klasse gelten übrigens für alle Objekte
die in STL Containern abgelegt werden sollen.
Definition
Für die Definition eines Stacks
geben Sie zuerst den STLContainer stack an, gefolgt von
einen <...> Klammerpaar.
Innerhalb dieses Klammerpaars
steht der Datentyp, für den der
Stack-Container definiert werden
soll. Im Anschluss daran folgt wie
üblich noch der Name des StackContainers.
// stack fuer ints definieren
stack<int> intStack;
// stack für Objekte der Klasse
// Window definieren
stack<Window> winStack;
Der auf diese Weise definiert Stack ist natürlich noch leer.
Ablage von Elementen
Für die Ablage von Elementen
auf dem Stack dient die
Memberfunktion push(...). Sie
erhält als Parameter das
abzulegende Element (Wert oder
Objekt). Achten Sie aber bitte
darauf, dass das abzulegende
Element auch mit dem Datentyp
übereinstimmt, für das der Stack
definiert wurde.
// int auf Stack ablegen
intStack.push(5);
// Window-Objekt auf Stack ablegen
Window myWin(...);
...
winStack.push(myWin);
Legen Sie Objekte auf dem Stack ab, so wird im Stack eine Kopie des übergebenen Objekts
abgelegt! Nehmen Sie nach dem Ablegen des Objekts noch Änderungen am Objekt vor, so
wirkt sich dies nicht auf das im Stack abgelegt Objekt aus. Durch diese Eigenschaft des
Stacks können Sie z.B. einen aktuellen 'Abzug' eines Objekts auf dem Stack ablegen, das
Objekt dann verändern und später wieder mit dem ursprünglich Objekt weiterarbeiten.
Zugriff auf Elemente
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (2 von 19) [17.05.2005 11:40:08]
STL Container
Um auf das zuletzt abgelegte
Element zuzugreifen, dient die
Memberfunktion top(). top()
liefert nur eine Referenz auf das
letzte Element, d.h. das Element
selbst verbleibt weiterhin im
Stack. Beachten Sie bitte, dass
eine Referenz zurückgeliefert
wird und nicht das Objekt selbst.
Dies hat Konsequenzen wenn Sie
das zurückgelieferte Element
verändern. Im Beispiel rechts hat
der Veränderung der Variablen
val keinen Einfluss auf den im
Stack abgelegten Wert. Wird
dagegen die Referenz selbst
abgelegt und darüber eine
Veränderung vorgenommen, so
verändert sich natürlich auch das
Element auf dem Stack. In wie
weit dies aber 'saubere'
Programmierung ist, das bleibt
jedem selbst überlassen.
Sehen wir uns einmal an was
passiert, wenn Sie ein Objekt aus
dem Stack auslesen. Wird die
zurückgelieferte Referenz einem
bereits bestehenden Objekt
zugewiesen, so wird der
Zuweisungsoperator der
entsprechenden Klasse
ausgeführt, d.h. es wird eine
Kopie des obersten Stackelements
erstellt. Wird dagegen die
Referenz einem neuen Objekt
zugewiesen, so wird anstelle des
Zuweisungsoperators der
Kopierkonstruktor ausgeführt.
Am effizientesten ist es, wenn Sie
die zurückgelieferte Referenz
direkt abspeichern. Beachten
müssen Sie dabei, dass
nachträgliche Änderungen an
dem zurückgelieferten Objekt
über die Referenz auf das
Stackelement wirken. Zur
Referenz gleich noch mehr.
// Wert direkt auslesen
int val = intStack.top();
// Wert veraendern
val++;
// val erhaelt wieder urspruenglichen Wert
// vom Stack
val = intStack.top();
// Nun Referenz abspeichern
int& refVal = intStack.top();
// Wert ueber Referenz veraendern
refVal++;
// Liefert jetzt veraenderten Wert zurueck
int newVal = intStack.top();
// Objekt direkt auslesen
// Fuehrt zum Aufruf des Zuweisungsoperators
// der Klasse Window
Window winOne(...);
winOne = winStack.top();
// Hier wird der copy-ctor aufgerufen
Window winTwo(winStack.top());
// Referenz ablegen
// Weitere Veraenderungen von winRef wirken
// sich auch auf das Stackelement aus
Window& winRef = winStack.top();
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (3 von 19) [17.05.2005 11:40:08]
STL Container
So, nun wird's wieder etwas
einfacher. Um das zuletzt
abgelegte Element vom Stack zu
entfernen (top() liefert ja nur eine
Referenz auf das letzte Element
zurück), dient die
Memberfunktion pop(). Der
Aufruf dieser Memberfunktion
führt bei Stacks mit Objekten
auch zum Aufruf des Destruktors
des entfernten Objekts.
Wenn Sie mit der
zurückgelieferten
Referenz arbeiten, dann
ist dies zwar von der
Laufzeit effizient, birgt
aber auch eine gewisse
Gefahr. Sehen Sie sich
dazu rechts das Beispiel
an. Zunächst wird die
von top()
zurückgelieferte
Referenz auf das
oberste Stackelement
abgespeichert.
Irgendwann im
Verlaufe des Programm
wird dieses Element
dann mittels pop() vom
Stack entfernt, was
wiederum dazu führt,
dass das entsprechende
Objekt zerstört wird.
// Obersten Wert vom Stack entfernen
intStack.pop();
// Oberstes Objekt vom Stack entfernen
// Fuehrt zum Aufruf des dtor von Window
winStack.pop();
...
// Oberstes Stackelement auslesen und als
// Referenz ablegen
Window& winRef = winStack.top();
...
// Oberstes Element nun entfernen
winStack.pop();
...
// Referenz verweist nun auf ein nicht mehr
// vorhandenes Element! CRASH!!
winRef.Size(...);
Die vorhin durch top() zurückgelieferte Referenz verweist damit auf ein Objekt das dann
nicht mehr existiert. Ein weiterer Zugriff über die Referenz führt dann im besten Fall dazu,
dass das Programm einfach beendet wird.
Sonstige Memberfunktionen
Damit kennen Sie die wichtigsten Memberfunktionen des stack-Templates. Außer den bisher
aufgeführten Memberfunktionen stehen noch die Memberfunktionen empty() und size() zur
Verfügung. empty() liefert true, wenn der Stack keine Elemente enthält und size() die Anzahl der
Elemente im Stack.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (4 von 19) [17.05.2005 11:40:08]
STL Container
Fernen können Stacks noch mit
den üblichen
Vergleichsoperatoren wie z.B. ==
oder < verglichen werden. Sind
auf einem Stack Objekte
abgelegt, dann führt der
Vergleich zum Aufruf der
entsprechenden VergleichsMemberfunktionen bzw. (friend)Vergleichsfunktionen. Beachten
Sie, dass die Klasse der Objekte
nur die Operatoren == und <
definieren muss; die restlichen
Vergleichsoperatoren werden
dann intern aus diesen beiden
gebildet. Der Vergleich erfolgt
immer Element für Element. Im
Beispiel rechts ist damit der
astack kleiner als der istack, da
das zweite Element von istack
größer ist als das zweite Element
von astack.
istack.push(1);
istack.push(6);
astack.push(1);
astack.push(3);
astack.push(5);
if (astack<istack)
cout << "astack < istack\n";
else
cout << "astack >= istack\n";
Außer den erwähnten Memberfunktionen enthält der stack Container noch diverse typedefs, wovon
wir uns den in der Praxis am wichtigsten jetzt noch kurz anschauen wollen.
In der Implementierung des stack Container steht eine typedef-Anweisung in etwa folgender Form :
typedef typename _Sequence::value_type value_type;
value_type repräsentiert hierbei den Datentyp des Elements, das im stack Containers abgelegt ist.
Sehen wir das Einsatzgebiet von value_type anhand eines kleinen Beispiels an.
Vorgegeben sind die rechts
stehenden Anweisungen. Dort
wird ein int-Stack definiert, von
dem zu einem beliebigen
Zeitpunkt ein Wert ausgelesen
wird. Da top() eine Referenz auf
das zuletzt abgelegte Element
zurückliefert, muss die
Zielvariable den Datentyp int
oder int& haben.
// Stack für int definieren
stack<int> myStack;
....
// Wert aus dem Stack auslesen
int val = myStack.top();
Wenn Sie zu einem späteren Zeitpunkt in der Programmentwicklung den Entschluss fassen, dass
anstelle von int-Werten nun long-Werte auf dem Stack abzulegen sind, so müssen Sie u.a. alle Stellen
korrigieren, an denen Werte vom Stack ausgelesen werden.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (5 von 19) [17.05.2005 11:40:08]
STL Container
Tun Sie dies nicht, so weisen Sie
den long-Wert vom Stack
weiterhin einer int-Variablen zu,
was ja bekanntlich zum Verlust
von Informationen führen kann.
// Stack für long definieren
stack<long> myStack;
....
// Huch! Zuweisung long an int
int val = myStack.top();
Und um solche Fehler zu vermeiden wurde der Typ value_type eingeführt. Da value_type immer dem
aktuellen Datentyp des stack Containers entspricht, kann damit das Auslesen von Elemente aus einem
stack Container wie folgt 'typsicher' durchgeführt werden:
Um sich Schreibarbeit zu sparen,
wird zunächst für den Stacktyp
mittels typedef ein entsprechendes
Symbol definiert (rechts ist dies
z.B. stackType). Anschließend
kann der Stack selbst definiert
werden. Beim Auslesen eines
Elements wird nun das mit
typedef definierte Symbol
angegeben, gefolgt von :: und
dem Typ value_type für den
Zieldatentyp.
// Stack für long definieren
typedef stack<long> stackType;
stackType myStack;
....
// Datentyp des Ziels entspricht immer dem
// Datentyp des Stack-Elements
stackType::value_type val = myStack.top();
Und egal für welchen Datentyp Sie den Stack irgendwann letztendlich einsetzen, durch die Festlegung
des Ziel-Datentyps über value_type haben Sie rechts und links vom Zuweisungsoperator immer die
gleichen Datentyp stehen. Wurde der Stack (wie im Beispiel oben) für die Ablage von long-Werten
definiert, dann steht stackType::value_type für den Datentyp long. Wurde der Stack für WindowObjekte definiert, dann entspricht stackType::value_type nun dem Datentyp Window. Ist doch eine
feine Sache, oder?
queue Container
Während beim vorherigen stack Container die zuletzt abgelegten Elemente zuerst ausgelesen werden
(LIFO = last in, first out), werden beim queue Container die zuerst abgelegten Elemente auch wieder
zuerst ausgelesen (FIFO = first in, first out). Der queue Container eignet sich daher sehr gut als
Zwischenpuffer, da er die Reihenfolge der Elemente nicht umdreht.
Für die Verwendung des queue Containers müssen Sie die Datei <queue> einbinden.
Definition
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (6 von 19) [17.05.2005 11:40:08]
STL Container
Die Definition einer Queue erfolgt
analog zur Definition eines
Stacks, nur dass hier anstelle des
stack Containers der queue
Container angegeben wird. Die so
definierte Queue ist natürlich
ebenfalls noch leer.
// queue fuer ints definieren
queue<int> intQueue;
// queue für Objekte der Klasse
// Window definieren
queue<Window> winQueue;
Ablage von Elementen
Zur Ablage von Elementen in die
Queue dient hier ebenfalls die
Memberfunktion push(...). Sie
erhält als Parameter das
abzulegende Element. Und auch
hier gilt: in der Queue wird eine
Kopie des übergebenen Elements
abgespeichert und nicht das
Original.
// int in Queue ablegen
intQueue.push(5);
// Window-Objekt in Queue ablegen
Window myWin(...);
...
winQueue.push(myWin);
Zugriff auf Elemente
Um die in einer Queue abgelegten
Elemente auszulesen wird die
Memberfunktion front()
verwendet. Sie liefert eine
Referenz auf das nächstes, d.h.
älteste, Element in der Queue.
front() entfernt das per Referenz
zurückgelieferte Element nicht
aus der Queue, dies muss mit
einer weiteren Memberfunktion
gesondert durchgeführt werden.
// Ältestes Queue-Element auslesen und als
// Referenz ablegen
Window& winRef = winQueue.front();
// Zuletzt abgelegten int Wert auslesen
int newest = intQueue.back();
Außer den Zugriff auf das ältestes Element gestattet der queue Container auch den Zugriff auf das
zuletzt abgelegte Element (neuestes Element) über die Memberfunktion back(). back() liefert ebenfalls
wie front() nur eine Referenz auf das Element.
Bezüglich der zurückgelieferten Referenz (und den Konsequenzen daraus) gilt das gleich wie beim
stack Container.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (7 von 19) [17.05.2005 11:40:08]
STL Container
Um das ältestes Element aus der
Queue entfernen, dient die
Memberfunktion pop(). Der
Aufruf dieser Memberfunktion
führt bei Queues mit Objekten
auch zum Aufruf des Destruktors
des entfernten Objekts. Es gibt
aber keine Memberfunktion um
das zuletzt abgelegte (neueste)
Element aus einer Queue zu
entfernen.
// Ältesten Wert aus der Queue entfernen
intQueue.pop();
// Ältestes Objekt aus der Queue entfernen
// Fuehrt zum Aufruf des dtor
winQueue.pop();
Sonstige Memberfunktionen
Die restlichen Schnittstellen wie empty(), size(), die Vergleichsoperatoren und der Typ value_type
entsprechen denen des stack Containers.
priority_queue Container
Der priority_queue Container dient zum Abspeichern von Elementen, wobei die Elemente beim
Auslesen nach ihrer Priorität sortiert ausgegeben werden. Die Priorität wird dabei durch einen
einfachen Vergleich bestimmt. Bei numerischen Werten hat zum Beispiel der Wert 10 standardmäßig
eine höhere Priorität als der Wert 5. Damit eignet sich der priority_queue Container z.B. zum
einfachen Sortieren. Bei Elementen mit gleicher Priorität ist die Reihenfolge der Ausgabe
implementierungsabhängig. Werden in einem priority_queue Container Objekte abgelegt, so muss die
Klasse dieser Objekte zumindest den Operator < überladen.
Das Sortierkriterium, das die Prioritäten-Reihenfolge festlegt, kann aber auch frei definiert werden.
Wie dies erfolgt, sehen wir uns nachher gleich an.
Beginnen wir aber mit den einfacheren Dingen. Zuerst müssen Sie für die Verwendung des
priority_queue Containers ebenfalls wieder die Datei <queue> einbinden.
Definition, Ablage von Elementen usw.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (8 von 19) [17.05.2005 11:40:08]
STL Container
Die Definition einer
priority_queue erfolgt im
einfachsten Fall analog zur
Definition einer 'normalen'
Queue, nur dass hier jetzt als
Datentyp priority_queue anstelle
von queue angegeben wird. Auch
die Ablage, das Auslesen und das
Entfernen erfolgt mit den
gleichen Memberfunktionen
push(...), top() und pop() wie bei
der gewöhnlichen Queue.
Lediglich die Memberfunktion
front() gibt es bei einer
Prioritätsqueue nicht. Im Beispiel
rechts werden zunächst drei intWerte in der Prioritätsqueue
abgelegt. Diese Werte dann aus
der Prioritätsqueue nacheinander
ausgelesen. Wie Sie der
Programmausgabe entnehmen
können, werden die Werte mit
fallender Wertigkeit ausgegeben.
Beachten Sie im Beispiel rechts
auch bitte den Einsatz des typedef
Symbols value_type. Durch den
Einsatz von value_type können Sie
so später einmal leicht den
Datentyp der Elemente in der
Prioritätsqueue wechseln.
// typedef für Priority-Queue
typedef priority_queue<int> queueType;
// Priority-Queue definieren
queueType myPrio;
// Werte ablegen
myPrio.push(10);
myPrio.push(5);
myPrio.push(20);
// Variable zum Auslesen definieren
queueType::value_type val;
// Werte der Priorität nach auslesen
while (!myPrio.empty())
{
cout << myPrio.top() << ',';
myPrio.pop();
}
Programmausgabe:
20,10,5
Ebenfalls implementiert im
priority_queue Container sind die
Memberfunktionen size() und
empty(). Sie entsprechenden den
gleichnamigen
Memberfunktionen der normalen
Queue.
Ablage von Objekten
Sehen wir uns noch ein weiteres Beispiel für den Einsatz des priority_queue Containers an. Da die
Ablage von einfachen Datentypen in einer Prioritätsqueue wohl eher seltener vorkommt, wollen wir
uns im folgenden Beispiel einmal die Ablage von Aktienkursen in einer solchen Queue ansehen. Die
Aktienkurse sollen dabei nach fallendem Wert ausgegeben werden, d.h. als 'Priorität' soll der aktuelle
Kurs dienen.
Ein Aktienkurs setzt sich zusammen aus dem Namen eines Unternehmens (der AG) und dem
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (9 von 19) [17.05.2005 11:40:08]
STL Container
aktuellen Wert der Aktien. Damit kann der Aktienkurs komfortabel in einem pair abgelegt werden.
Da die Sortierreihenfolge der Queue nach dem Aktienwert erfolgen soll, muss als erstes Element des
pair auch der Aktienwert stehen. Denken Sie beim Einsatz eines pair immer daran, dass das erste
Element eines pair immer Vorrang hat. Würde als erstes pair-Element der Name des Unternehmens
stehen, so würde die Prioritätsqueue die Unternehmen alphabetisch sortieren.
Für das pair bietet es sich hier an,
mittels typedef ein entsprechendes
Symbol zu definieren. Dieses
Symbol wird dann bei der
Definition der Prioritätqueue
angegeben.
Danach kann die Prioritätsqueue
mit beliebig vielen Aktienkursen
'gefüllt' werden.
// pair für Aktienkurs
typedef pair<double,string> stock;
// Prioritätsqueue für Aktienkurs
priority_queue<stock> stockQueue;
Das Auslesen der Aktienkurse
erfolgt dann mit den Befehlen
top() und pop(). Beachten Sie
bitte, dass die Prioritätsqueue
keine Operationen kennt, mit
denen Sie die Queue durchlaufen
können. Für solche
Anwendungsfälle gibt es andere
Container die wir uns später
ansehen werden.
// Ausgabe der Aktienkurse
while (!stockQueue.empty())
{
stock actStock = stockQueue.top();
cout << "Company: " << actStock.second
<< "\tvalue: " << actStock.first
<< endl;
stockQueue.pop();
}
// Prioritätsqueue mit Aktienkursen belegen
stockQueue.push(stock(11.11,"One"));
stockQueue.push(stock(44.44,"Four"));
stockQueue.push(stock(22.22,"Two"));
Sortierkriterium für priority_queue
Standardmäßig werden die Elemente in einer Prioritätsqueue fallend sortiert. Wollen Sie eine andere
Sortier-Reihenfolge, so können Sie das Sortierkriterium selbst festlegen.
Um das Sortierkriterium festzulegen, müssen Sie einen Funktionsoperator zur Verfügung stellen.
Funktionsoperatoren wurden ja schon in der Lektion Überladen von speziellen Operatoren eingeführt.
Sie können Sie sich die Funktionsoperatoren hier nochmals ansehen ( ).
Der für den Vergleich verwendete Funktionsoperator (im folgenden functor genannt) erhält die
beiden zu vergleichenden Elemente per const-Referenz übergeben und liefert als Returnwert einen
bool-Wert zurück. Ist der Rückgabewert des functors true, so werden die beiden übergebenen
Elemente umgekehrt in der Prioritätsqueue abgelegt. Aber sehen wir uns dies anhand des Beispiel zur
Ablage von Aktienkursen einmal an. Diesmal sollen die Aktienkurse in aufsteigender Reihenfolge
(also der umgekehrten normalen Priorität) abgelegt werden.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (10 von 19) [17.05.2005 11:40:08]
STL Container
Zum Überladen des functors
definieren wir uns die Klasse
CompareStocks. Da die
Aktienkurse als stock pair
abgelegt wurden, erhält der
functor die zu vergleichenden
Elemente auch als solche
übergeben. Wenn wir die
Aktienkurse in aufsteigender
Reihenfolge ablegen wollen, so
müssen die beiden übergebenen
Elemente dann vertauscht
werden, wenn der Kurs des
ersten übergebenen Elements
größer ist als der des zweiten
übergebenen Elements. Folglich
muss der functor dann true
zurückliefern wenn gilt, dass
s1.first > s2.first ist (first ist der
Kurs im pair!).
// Funktionsobjekt
struct CompareStocks
{
bool operator()(const stock& s1,
const stock& s2) const
{
return (s1.first > s2.first);
}
};
Möchten Sie bei der vorgegebenen pair-Struktur zum Beispiel nach dem Unternehmensnamen
sortieren, so müssten Sie im functor lediglich anstelle des pair Elements first einen Vergleich mit dem
pair Element second durchführen. Sie sehen, solche eigen definierten Sortierkriterien gestatten eine
relativ flexible Anwendung der Prioritätsqueue.
Beachten Sie bitte, dass hier der Klassentyp struct verwendet wurde! Der überladene
Funktionsoperator muss natürlich public sein damit der priority_queue Container darauf auch
Zugriff hat.
Ist der functor erst einmal definiert, so muss er 'nur noch' mit der priority_queue 'verknüpft' werden.
Dies geschieht folgendermaßen:
Zuerst ist ein entsprechendes
Objekt der functor-Klasse zu
definieren. Dieses functor-Objekt
ist dann dem Konstruktor des
Prioritätsqueue Objekts zu
übergeben. Beachten Sie bitte die
geänderte Definition des QueueObjekts.
#include <vector>
....
CompareStocks cmpFunc;
priority_queue<stock, std::vector<stock>,
CompareStocks>
stockQueue(cmpFunc);
Innerhalb der spitzen Klammer steht zunächst (wie gewohnt) der Datentyp der Elemente der
Prioritätsqueue. Danach folgt der Container-Typ, der intern für die Prioritätsqueue verwendet wird.
Sie sollten bis auf weiteres dort die Angabe std::vector<DTYP> stehen lassen. DTYP muss mit dem
Datentyp der Elemente der Prioritätsqueue übereinstimmen. Zum Schluss folgt noch die Klasse des
functors. Beachten Sie bitte, dass nun in jedem Fall auch die Header-Datei <vector> einzubinden ist.
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (11 von 19) [17.05.2005 11:40:08]
STL Container
Wenn Sie die Prioritätsqueue
jetzt wie nebenstehend angegeben
füllen, so erhalten Sie die
darunter stehende Ausgabe. Die
Ausgabe erfolgt nun in
aufsteigender Sortierung nach
Aktienkursen, d.h. die Prioritäten
wurden umgedreht.
stockQueue.push(stock(11.11,"One"));
stockQueue.push(stock(44.44,"Four"));
stockQueue.push(stock(22.22,"Two"));
Programmausgabe:
Company: One value: 11.11
Company: Two value: 22.22
Company: Four value: 44.44
Der priority_queue Container enthält noch weitere Konstruktore die es erlauben, eine
Prioritätsqueue bei ihrer Definition mit Elementen zu belegen. Da diese Konstruktore aber
Iteratoren verwenden und wir für deren Einsatz noch nicht die notwendigen Grundlagen
haben, sei an dieser Stelle auf die Online-Hilfe verwiesen. Iteratoren werden später im Kurs
noch erläutert.
bitset Container
Das Klassen-Template bitset dient zum Abspeichern von Bitfolgen. Die Anzahl der Bits in der Folge ist
nicht an irgendwelche Größen von Standard-Datentypen gebunden. So können in einem bitset zum
Beispiel 16 oder sogar 10000 Bits abgespeichert werden. Da ein Bit entweder gesetzt sein kann (1Wert) oder nicht (0-Wert), werden bitset-Objekte in der Regel zum Abspeichern von so genannten
Flags eingesetzt. Aber bitset-Objekte können auch noch für einen anderen Zweck angewandt werden,
den wir uns am Ende dieser Lektion ansehen werden.
Wenn Sie mit bitsets arbeiten wollen, so müssen Sie die Header-Datei <bitset> einbinden. Denken Sie
auch immer daran daran, dass sowohl bitset wie auch die bisherigen Container alle im Namensraum
std liegen. Die Angabe des Namenraums wurde bei den Beispielen immer weggelassen.
Definition
Um nun ein einfaches bitset zu
// Definition eines einfachen bitset-Objekts
definieren, wird nach dem Namen // mit 50 Bits, alle auf 0 gesetzt
des Klassen-Templates bitset in
bitset<50> myFlags;
spitzen Klammern die Anzahl der
Bits für das bitset-Objekt
angegeben. Bei einem
solchermaßen definierten bitset
sind alle Bits auf 0 gesetzt.
Sie können ein bitset bei seiner Definition auch gleich entsprechend initialisieren. Dies geschieht durch
Aufruf des entsprechenden Konstuktors. Dabei stehen Ihnen folgende Möglichkeiten zur Verfügung:
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (12 von 19) [17.05.2005 11:40:08]
STL Container
Durch Angabe eines unsigned long
Wertes bei der Definition eines
bitset-Objekts können Sie max. 32
Bits initialisieren (myFlags
rechts).
Oder aber Sie erstellen einen
string mit entsprechenden 0/1
Folgen und geben dieses stringObjekt dann bei der
Objektdefinition des bitsets an.
Dabei können Sie dann den
kompletten String in
entsprechende Bits umwandeln
(yourFlags) oder aber aus dem
String entsprechende Teilstrings
'herausschneiden'
(lowFlags/highFlags). Der erste
Wert nach dem Stringnamen gibt
dabei die Startposition innerhalb
des Strings an und der zweite
Wert die Anzahl der zu
übernehmenden Zeichen.
Beachten Sie dabei aber, dass das
'Bit 0' das im string am weitesten
rechts stehenden Zeichen ist
(siehe auch 'Inhalte der bitsets'
rechts).
// bitset mit 8 Bits mit dem Wert 7 init.
bitset<8> myFlags(7UL);
// String mit 0/1 Folge
string stringFlags("10010011");
// String in Bits 'umwandeln'
bitset<8> yourFlags(stringFlags);
// niederwertige Flags extrahieren (0011)
bitset<4> lowFlags(stringFlags,0,4);
// hoeherwertige Flags extrahieren (1001)
bitset<4> highFlags(stringFlags,4,4);
Inhalte der bitsets:
myFlags: 00000111
yourFlags: 10010011
lowFlags: 0011
highFlags: 1001
Beachten Sie ferner, dass Sie hier einen string angeben müssen und keinen char-Zeiger, d.h. die
Anweisung
bitset<8> myFlags("01010101");
erzeugt einen Übersetzungsfehler!
Ausgabe eines bitsets
Werden bitsets ausgegeben, z.B. mit den Stream cout, so wird der Inhalt des bitsets als 0/1 Folgen
ausgegeben. Es werden dabei aber immer alle Bits eines bitsets ausgegeben, d.h. bei einem bitset mit
z.B. 50 Bits werden auch alle 50 Bits ausgegeben. Und auch das ist möglich: Sie können ein bitset sogar
einlesen, z.B. mit den Stream cin. Die Eingabe darf dabei natürlich nur aus 0/1 Kombinationen
bestehen. Ein Beispiel hierzu folgt weiter unten.
bitset Operationen
Das Klassen-Template bitset definierte eine relativ große Anzahl von Operationen, um die Bits in
einem bitset zu verarbeiten. Wir werden uns an dieser Stelle nur die wichtigsten ansehen. Für die
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (13 von 19) [17.05.2005 11:40:08]
STL Container
restlichen Operationen sei auf die Online-Hilfe verwiesen.
Die Memberfunktion size() liefert
die Anzahl der Bits in einem bitset
als size_t Wert zurück. Wollen Sie
wissen, ob überhaupt Bits in
einen bitset gesetzt sind, so
können Sie dies mit der
Memberfunktion any() abprüfen.
Sie liefert true zurück wenn
irgendein Bit gesetzt ist. Wenn
Sie es noch genauer wissen
wollen, dann liefert die
Memberfunktion count() die
Anzahl der gesetzten Bits zurück
(siehe auch Programmausgabe
rechts).
Mithilfe der Memberfunktion
test(...) können Sie ein bestimmtes
Bit innerhalb des bitsets
abprüfen. test(...) erhält als
Parameter den Index des
abzuprüfenden Bits und liefert
true zurück, wenn das
entsprechende Bit gesetzt ist
(siehe auch Programmausgabe
rechts).
Zum Setzen, Löschen und
Invertieren von Bits dienen die
Memberfunktionen set(...),
reset(...) und flip(...). Alle
Memberfunktionen erhalten als
Parameter den Index des zu
manipulierenden Bits und alle
drei Memberfunktionen haben
auch noch diverse weitere
Formen um z.B. alle Bits in eine
bitset gleichzeitig manipulieren zu
können.
Als Alternative für den Zugriff
auf ein bestimmtes Bit (anstelle
von test(...), set(...) und reset(...))
steht auch der Indexoperator []
zur Verfügung.
bitset<8> myFlags(string("10100011"));
cout << "size:" << myFlags.size() << ','
<< "any:" << myFlags.any() << ','
<< "count:" << myFlags.count() << endl;
Programmausgabe:
size:8,any:1,count:4
bitset<8> myFlags(string("10100011"));
for (size_t i=0; i<myFlags.size(); i++)
cout << (myFlags.test(i) ? 'S' : '.');
Programmausgabe:
SS...S.S
bitset<50> manyBits(0x5FFul);
// höchstwertigstes Bit setzen
manyBits.set(49);
// niederwertigstes Bit löschen
manyBits.reset(0);
// Bit 30 invertieren
manyBits.flip(30);
bitset<8> myFlags(0x07ul);
// höchstwertiges Bit setzen
myFlags[7] = 1;
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (14 von 19) [17.05.2005 11:40:08]
STL Container
Rechts sehen Sie nochmals einen
etwas komplizierten Einsatz eines
bitsets. Das bitset besteht dabei
aus 4 Flags. Je nach Zustand der
Flags wird dann im Programm
der auszugebende Text gesteuert.
Dazu werden den Flags mittels
enum-Konstanten zuerst
symbolische Namen zugewiesen.
So soll zum Beispiel das erste
Flag ROT dazu dienen, die
Ausgabe einer Farbinformation
zu steuern. Nach der Definition
des bitset-Objekts flags werden in
diesem bitset zwei beliebig Bits
gesetzt, einmal durch die set(...)
Memberfunktion und einmal
über den Indexoperator. Im
Anschluss daran wird je nach
Zustand der vier Flags (Bits) ein
entsprechender Text ausgegeben.
// Flagnummer als enums
enum choices {ROT, FAHRRAD, SCHNELL, KURVE};
// bitset mit 4 Flags
bitset<4> flags;
// Zwei beliebige Flags setzen
flags.set(rand()%4);
flags[rand()%4] = 1;
// Je nach Flag entsprechenden Text ausgeben
cout << "Das "
<< ((flags[ROT])?"rote":"gelbe");
cout << ((flags[FAHRRAD])?" Fahrrad ":" Auto
");
cout << "faehrt "
<<
((flags[SCHNELL])?"schnell":"langsam");
cout << " um die "
<< ((flags[KURVE])?"Kurve":"Ecke")
<< endl;
Das Klassen-Template bitset löst bei diversen unerlaubten Operationen Exceptions
(Ausnahmen) aus. So führt zum Beispiel das Setzen des Bits 8 in einem bitset mit 8 Bits zur
Auslösung der Exception out_of_range. Da wird zum jetzigen Zeitpunkt noch keine Exceptions
verarbeiten können, führt dies dann zum Beenden des Programms.
Aber außer zum Abspeichern von einzelnen Bits können Sie ein bitset auch zum Konvertieren einer in
einem string abgelegten 0/1 Folge in einen numerischen Wert oder umgekehrt verwenden. Dazu stellt
bitset die beiden Memberfunktionen to_ulong() und to_string() zur Verfügung. Sehen wir uns beide
Memberfunktionen am Besten anhand eines Beispiels an.
Im Beispiel rechts wird zuerst ein
bitset mit 8 Bits definiert. In
dieses bitset wird dann mittels cin
eine 0/1 Folge eingelesen.
Anschließend wird die
eingelesene 0/1 Folge mit der
Memberfunktion to_long() in
einen unsigned long Wert
umgewandelt und dann
ausgegeben. Danach wird ein
weiteres bitset definiert, dass bei
seiner Definition mit dem Wert
0x55 initialisiert wird. Um die im
bitset abgelegten Bits in einen 0/1
String zu konvertieren, wird die
Memberfunktion to_string()
// bitset mit 8 Bits definieren
bitset<8> toNumeric;
// bitset einlesen, als 0/1 Folge
cin >> toNumeric;
// 0/1 Folge als numerischen Wert ausgeben
cout << "Eingelesener Wert: "
<< toNumeric.to_ulong() << endl;
// bitset mit 01010101 initialiseren
bitset<8> toString(0x55);
// 0/1 Folge in string umwandeln
string converted = convBitset.template
to_string<char,char_traits<char>,
allocator<char> >();
cout << "Als string: " << converted << endl;
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (15 von 19) [17.05.2005 11:40:08]
STL Container
aufgerufen. Beachten Sie bitte
den etwas 'kryptische' Aufruf der
Memberfunktion. to_string() ist
als Funktions-Template definiert
und erfordert deshalb die
angegebene aufwändige
Schreibweise.
Damit wollen wir an dieser Stelle die Behandlung des bitsets beenden. Wie bereits erwähnt, besitzt das
Klassen-Template noch eine Reihe weiterer Operatoren wie z.B. <<= um alle Bits eines bitsets um eine
bestimmte Anzahl von Stellen nach links zu schieben.
Beispiel und Übung
Das Beispiel:
Das Beispiel bestimmt alle Primzahlen im Bereich 0..MAX, wobei MAX eine im Programm
definierte Konstante ist. Zur 'Abspeicherung' der Primzahlen wird ein entsprechend großes bitset
mit MAX Bits verwendet. Ist eine Zahl eine Primzahl, so wird im bitset das Bit mit dem jeweiligen
Index gesetzt
Die Primzahlbestimmung erfolgt nach dem Sieb des Eratosthenes, das folgenden Algorithmus
verwendet. Beginnend mit der Zahl 2 werden alle Zahlen im Bereich ab 2 gestrichen, die ohne Rest
durch 2 teilbar sind. Die 'Streichung' erfolgt durch Löschen des entsprechenden Bits im bitset.
Danach wird die nächste höhere 'nicht gestrichene' Zahl gesucht (ergibt nun die Zahl 3), die dann
auch der nächsten Primzahl entspricht. Ab dieser so gefundenen neuen Primzahl werden dann alle
Zahlen gestrichen die durch diese teilbar sind. Sind alle Zahlen gestrichen, beginnt das Spiel wieder
von vorne: Suche der nächsten nicht gestrichenen Zahl (ergibt jetzt die Zahl 5), streichen der
Zahlen die durch diese Zahl teilbar sind usw.
Zum Schluss werden alle gefundenen Primzahlen ausgegeben, d.h es werden die Indizes
ausgegeben, deren Bit im bitset noch gesetzt ist.
Das unten stehende Beispiel finden Sie auch unter: le13\prog\Bcontain.
Die Programmausgabe:
2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (16 von 19) [17.05.2005 11:40:08]
STL Container
Das Programm:
// C++ Kurs
// Beispiel zu bitset
// Bestimmung der Primzahlen
#include <iostream>
#include <bitset>
using std::cout;
using std::endl;
using std::bitset;
// Bereich der Primzahlen
const int MAX = 100;
int main()
{
int index;
int prim;
bitset<MAX> primes;
primes.set();
// 0 und 1 sind keine Primzahlen!
primes.reset(0);
primes.reset(1);
index = 2;
do
{
// Durchsuche alle Zahlen ab der aktuellen Primzahl+1
// und entferne diejenigen, die durch die Primzahl teilbar sind
for (prim = index+1; prim<MAX; prim++)
{
// Wenn Zahl noch nicht entfernt wurde
if (primes.test(prim))
{
// pruefe, ob Zahl durch Primzahl teilbar
if (prim%index == 0)
// Wenn ja, dann ist Zahl keine Primzahl
primes.reset(prim);
}
}
// Suche nun nächste noch vorhandene Zahl
// Dies ist auch die nächste Primzahl
index++;
if (index<MAX)
{
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (17 von 19) [17.05.2005 11:40:08]
STL Container
do
{
if (primes.test(index))
// naechste Primzahl gefunden
break;
index++;
} while (index<MAX);
}
} while (index<MAX);
// Alle gefundenen Primzahlen ausgeben
cout << "Gefundene Primzahlen:\n";
for (index=0; index<MAX; index++)
{
if (primes.test(index))
cout << index << ',';
}
cout << endl;
return 0;
}
Die Übung:
Schreiben Sie ein Programm, dass die Ankunftszeiten von Flügen sortiert ausgibt. Verwenden Sie
zum Sortieren einen entsprechenden STL-Container.
Die Flugdaten sind in einer Klasse Flight abzulegen, die folgende Eigenschaften besitzt:
Flugnummer als string, Ankunftszeit als Stunde und Minute als unsigned char.
Legen Sie vier Flugdaten im STL-Container ab und geben Sie diese dann aus.
Zusatz: Wenn Sie wollen erweitern Sie das Programm so, dass die Flugdaten nach aufsteigender
Ankunftszeit ausgegeben werden.
Die Programmausgabe:
Flug:
Flug:
Flug:
Flug:
333,
444,
111,
222,
Ankunft:
Ankunft:
Ankunft:
Ankunft:
17h38m
13h38m
10h59m
09h00m
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (18 von 19) [17.05.2005 11:40:08]
STL Container
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le13\Lcontain.
In der nächsten Lektion werden wir uns nochmals mit der wichtigen Klasse string beschäftigen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/le13_04.htm (19 von 19) [17.05.2005 11:40:08]
string (Teil 2)
string (Teil 2)
Die Themen:
Einleitung
Speicherverwaltung
string Iteratoren
Vergleichen von strings
Suchen in strings
Kopieren von strings und Teil-strings
Anhängen und ersetzen in strings
Einfügen und entfernen in strings
Beispiel und Übung
Einleitung
Die Klasse string haben Sie ja bisher schon des öfteren eingesetzt. Wird werden uns in der
Lektion diese Klasse einmal genauer ansehen. Im Folgenden wird davon ausgegangen, dass Sie
wissen wie string Objekte definiert werden, wie strings eingelesen und ausgegeben werden und
dass Sie die string Operatoren =, + sowie die Vergleichsoperationen kennen. Hier können Sie sich
diese Grundvoraussetzungen nochmals ansehen ( ).
Bisher haben wir der
#include <string>
Einfachheit halber immer
std::string charString;
nur Objekte der Klasse string std::wstring wcharString;
verwendet. string verwendet
zur Speicherung der Zeichen
den Datentyp char. Um auch
Zeichen mit dem Datentyp
wchar_t ablegen zu können,
gibt es außerdem noch die
Klasse wstring.
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (1 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Da string und wstring die gleichen Eigenschaften und Schnittstellen besitzen, wird im Folgenden
nicht mehr zwischen diesen beiden string-Typen unterschieden.
Speicherverwaltung
Der von einem string Objekt letztendlich belegte Speicher wurde bei den bisherigen
Ausführungen nicht betrachtet. Da in manchen Fällen aber der Speicherplatz eine nicht zu
vernachlässigende Rolle spielt, wollen wir dies an dieser Stelle jetzt nachholen.
Zur Ermittlung des von einem String belegten Speicherplatzes stehen die Memberfunktionen
size(), length() und capacity() zur Verfügung.
size() und length() sind
identischen
Memberfunktionen; sie
liefern die Anzahl der in
einem String abgelegten
Zeichen. capacity() hingegen
liefert die Anzahl der Zeichen
zurück die in einem String
abgelegt werden können,
ohne dass erneut Speicher
angefordert werden muss.
Und dies muss nicht
zwingend mit der Anzahl der
Zeichen im String
übereinstimmen. Sehen Sie
dazu einmal das
nebenstehende Programm
sowie die dazugehörige
Ausgabe an. Wie Sie der
Ausgabe entnehmen können,
verändert sich nach der
zweiten Zuweisung an den
String nur die Länge, nicht
aber die Anzahl der Zeichen
im String ohne erneute
Speicheranforderung.
string myString = "123456789";
cout << myString.length() << ',';
cout << myString.capacity() << endl;
myString = "12";
cout << myString.length() << ',';
cout << myString.capacity() << endl;
Programmausgabe (Comeau Compiler):
9,9
2,9
Der Grund, warum sich der vom String einmal belegte Speicherplatz in der Regel nicht mehr
vermindert, ist einfach die Ausführungsgeschwindigkeit (Performance). Dies bedeutet in der
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (2 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Praxis, dass ohne weiteres Zutun der von einem String belegte Speicherplatz immer nur
anwächst.
Um die zeitintensive
Speicheranforderung für
einen String zu minimieren,
stellt die string Klasse die
Memberfunktion reserve(...)
zur Verfügung. reserve(...)
erhält als Parameter die
Anzahl der Zeichen, für die
minimal Speicher in einem
String zu reservieren sind.
Wenn Sie also die maximale
Länge eines String im Voraus
wissen, so können Sie mithilfe
dieser Memberfunktion
gleich von Anfang an
genügend Speicher
reservieren und somit die
zeitintensiven
Speicheranforderungen
umgehen. Wird reserve()
ohne Parameter aufgerufen,
so wird der der für den
String belegte Speicherplatz
so bemessen, dass der
aktuelle Stringinhalt darin
Platz findet. Je nach
Implementierung der string
Klasse kann damit eine
Verminderung des
Speicherplatzverbrauchs
erreicht werden.
string myString = "123456789";
cout << myString.length() << ',';
cout << myString.capacity() << endl;
myString = "12";
myString.reserve();
cout << myString.length() << ',';
cout << myString.capacity() << endl;
Programmausgabe (Comeau Compiler):
9,9
2,2
Wie viel Speicher nach dem Aufruf von reserve() tatsächlich belegt wird, ist
implementierungsabhängig. So optimiert die string Implementierung des Comeau
Compilers den Speicherverbrauch sehr gut, während beim BORLAND und MinGW
Compiler mittels reserve() nur eine Vergrößerung des Speicherplatzes möglich ist. Der
VC++ Compiler wählt eine Zwischenlösung zwischen diesen beiden Extremen, in dem er
den Speicherbedarf zwar eventl. minimiert, aber dies auch nur innerhalb eines
bestimmten Rasters (Granularität) das von der Länge des aktuellen Stringinhalts
abhängig ist.
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (3 von 19) [17.05.2005 11:40:13]
string (Teil 2)
string Iteratoren
Bei einigen string-Operationen werden so genannte Iteratoren eingesetzt. Iteratoren im
Allgemeinen werden erst später im Kurs behandelt. Für den Augenblick können Sie sich unter
einem Iterator einen Positionsindikator vorstellen, der auf eine beliebige Position innerhalb des
Strings verweist. Der String-Iterator ist quasi das C++ Gegenstück zu char-Zeigern in C-Strings.
Beachten Sie bitte, dass auch die String-Iteratoren, genauso wie ja der string selbst, im
Namensraum std liegen.
Fangen wir mit der
Definition eines Iterators an.
Rechts sehen Sie die
Definition der StringIterators iter.
Und was für 'normale' Zeiger
gilt, gilt auch für Iteratoren:
sie müssen nach ihrer
Definition initialisiert
werden. Für die
Initialisierung von Iteratoren
stehen die stringMemberfunktionen begin()
und end() zur Verfügung.
begin() liefert einen Iterator
auf den Anfang des Strings
und end() einen Iterator auf
die nächste Position hinter
dem String, d.h. begin() und
end() definieren einen offenen
Bereich [...).
Um auf das Zeichen
zuzugreifen, auf das der
Iterator verweist, wird der
Iterator (genauso so wie
Zeiger) einfach
dereferenziert. Und es
besteht noch eine
Gemeinsamkeit zwischen
Zeigern und Iteratoren: auch
auf Iteratoren sind nur die
Operationen Addition und
Subtraktion sowie
Vergleichsoperationen
// Definition eines String-Iterators
string::iterator iter;
string myString("12345");
// Iterator auf Stringanfang setzen
iter = myString.begin();
// Iterator auf Stringende+1 setzen
iter = myString.end();
// String zeichenweise ausgeben
for (iter=myString.begin();
iter!=myString.end();
++iter)
cout << *iter << ',';
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (4 von 19) [17.05.2005 11:40:13]
string (Teil 2)
erlaubt.
Das obige Beispiel gibt damit den Inhalt des String-Objekts myString zeichenweise aus. Beachten
in der for-Schleife das Abbruch-Kriterium! end() liefert einen Iterator der hinter das letzte
Zeichen im String 'zeigt'.
Außer diesem 'normalen'
Iterator gibt es noch einen
Reverse-Iterator. Er erlaubt
es, einen String von hinten
nach vorne zu durchlaufen.
Beachten Sie im Beispiel
rechts, dass nun anstelle von
begin() und end() die
Memberfunktionen rbegin()
und rend() verwendet
werden. Um den Iterator auf
das vorhergehende Zeichen
zu positionieren, müssen Sie
den Iterator weiterhin
inkrementieren!
// Reverse-Iterator definieren
string::reverse_iterator riter;
// String rückwärts ausgeben
for (riter=myString.rbegin();
riter!=myString.rend();
++riter)
cout << *riter << ',';
Sollten Sie ausversehen anstelle von rbegin() bzw. rend() einmal begin() bzw. end()
geschrieben haben, so erhalten Sie vom Compiler eine Fehlermeldung.
Verwenden Sie bei Iteratoren nach Möglichkeit immer die Präfix-Operatoren ++iter bzw. -iter nicht die Postfix-Operatoren iter++ bzw. iter--. Die Postfix-Operatoren benötigen
intern ein temporäres Iterator-Objekt, was sich negativ auf die Laufzeit auswirkt. Sie
können Sie das 'Warum' hier nochmals ansehen ( ).
Iteratoren können nach der Durchführung von Operationen die eine erneute
Speicheranforderung verursachen ungültig werden! Zu diesen Operationen gehören
zum Beispiel alle Operationen, die die Länge eines Strings erhöhen (wie z.B. der der
Operator +) oder auch die Memberfunktion reserve(...).
Vergleichen von strings
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (5 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Den 'üblichen' Vergleich von
Strings mithilfe von
Vergleichsoperatoren kennen
Sie ja schon aus einer der
vorherigen Lektionen. Als
Ergebnis des Vergleichs
erhalten Sie entweder true
oder false. Hier können Sie
sich dies nochmals ansehen
( ).
Zusätzlich bietet die string
Klasse für Vergleiche auch
noch die Memberfunktion
compare(...) an. compare(...)
erhält im einfachsten Fall als
Parameter das zu
vergleichende string Objekt
oder einen const char Zeiger
auf einen C-String. Als
Returnwert liefert
compare(...) einen int-Wert,
der '0' ist, wenn beide Strings
den gleichen Inhalt haben, <0
wenn der zu vergleichen
String größer ist und >0
wenn er kleiner ist.
// Strings definieren
string s1("abc"), s2("xyz");
// Vergleich mit Vergleichsoperatoren
bool ret = s1<s2;
// Strings definieren
string s1("abc"), s2("xyz");
// Vergleich mittels compare()
int ret = s1.compare(s2);
// Vergleich mit einem C-String
int cret = s1.compare("c-string");
Größer bzw. kleiner bezieht sich hier nicht auf die Länge des Strings sondern lexikalisch,
d.h. "A" ist größer als "aaa".
Aber das ist noch nicht alles, was die Memberfunktion compare(...) kann. compare(...) kann auch
dazu verwendet werden, Teile eines Strings zu vergleichen. Dazu stehen folgenden
Memberfunktionen zur Verfügung:
Vergleich den Teilstring mit der
Länge len ab der Position pos
mit den zweiten String s2.
int compare (size_type pos, size_type len,
Vergleich den Teilstring mit der
const string& s2, size_type pos2, Länge len ab der Position pos
size_type len2) const;
mit dem zweiten Teilstring s2
mit der Länge len2 ab der
Position pos2.
int compare (size_type pos, size_type len,
const string& s2) const;
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (6 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Vergleich den Teilstring mit der
Länge len ab der Position pos
mit dem C-String, auf den pstr
verweist.
int compare (size_type pos, size_type len,
Vergleich den Teilstring mit der
const char* pstr, size_type len2) Länge len ab der Position pos
const;
mit len2 Zeichen aus dem CString, auf den pstr verweist.
int compare (size_type pos, size_type len,
const char* pstr) const;
Das nebenstehende Beispiel
zeigt exemplarisch den
Einsatz der compare(...)
Memberfunktion auf.
Innerhalb eines in einem
string abgelegten Textes wird
auf das Vorkommen eines
bestimmten Textes ab einer
definierten Stelle abgetestet.
Beachten Sie bitte, dass der
Text an einer bestimmten
Stelle stehen muss,
compare(...) durchsucht nicht
den komplette String. Hierzu
dienen Memberfunktionen
die auf der nächsten Seite
vorgestellt werden.
// Zu testender Text
string test1("Sehr geehrte Frau Ypsilon");
// Prüfen ob Empfänger maennlich/weiblich
if (test2.compare(12,5," Frau")==0)
cout << "Empfaenger ist weiblich" << endl;
else
cout << "Empfänger ist maennlich" << endl;
size_type ist ein Synonym (typedef) innerhalb der Klasse string für size_t. Wollen Sie
Variablen von diesem Datentyp definieren, so müssen Sie vor size_type die Klasse string
angegeben, also string::size_type.
Suchen in strings
Zum Suchen von Zeichen und Strings innerhalb von strings stehen über 40 verschiedenen
Memberfunktionen zur Verfügung. Aber keine Angst, wir werden uns hier nicht alle
Memberfunktionen ansehen, sondern nur deren 'Grundformen'. So erlauben die meisten
Memberfunktionen die Angabe des Suchkriteriums entweder als string oder über einen C-String.
Alle Such-Memberfunktionen haben als Rückgabetyp den Datentyp string::size_type.
string::npos Wert
Bei der Suche innerhalb eines Strings ist es nicht ausgeschlossen, dass ein gesuchtes Zeichen bzw.
ein gesuchter String im zu untersuchenden String nicht vorhanden ist. In diesem Fall liefern alle
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (7 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Such-Memberfunktionen den Wert string::npos zurück (siehe auch nachfolgendes Beispiel).
Suchen von Zeichen
Zum Suchen von Zeichen
stehen die
Memberfunktionen find(...)
und rfind(...) zur Verfügung.
Beide Memberfunktionen
erhalten das zu suchende
Zeichen als Parameter
übergeben. find(...) sucht
nach dem ersten Auftreten
des Zeichens und rfind(...)
nach dem letzten. Als
Returnwert liefern die
Memberfunktionen die
Position des gesuchten
Zeichens im String. Die
Positionsangabe ist 0basierend. Ist das Zeichen
nicht im String vorhanden, so
wird wie bereits erwähnt
string::npos zurückgeliefert.
// Zu durchsuchender String
string path("c:\\corba\\doc\\readme.doc");
// Suche nach erstem Backslash
string::size_type first = path.find('\\');
// Suche nach letztem Backslash
string::size_type last = path.rfind('\\');
// Suche nach nicht vorhandenem Zeichen
if (path.find("X") == string::npos)
cout << "Zeichen X nicht gefunden".
Suchen von Teilstrings
Zum Suchen von Teilstrings
in Strings werden ebenfalls
die Memberfunktionen
find(...) und rfind(...)
verwendet. Beide
Memberfunktionen erhalten
den zu suchenden String
entweder als String,
Teilstring oder C-String
(const char*) als Parameter
übergeben. find(...) sucht
wieder nach dem ersten
Auftreten der Zeichenfolge
und rfind(...) nach dem
letzten. Als Returnwert
liefern die
Memberfunktionen die
Position des gesuchten
Strings im String.
// Zu durchsuchender String
string path("c:\\corba\\doc\\readme.doc");
// Suche nach erstem Backslash
string::size_type first = path.find("doc");
// Suche nach letztem Backslash
string toTest(":\\");
string::size_type last = path.rfind(toTest);
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (8 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Suchen nach dem ersten (nicht) Auftreten eines Zeichens aus einem Zeichenvorrat
Ein String kann auch auf das
Vorhandensein eines
Zeichens aus einem
Zeichenvorrat durchsucht
werden. Hierzu dienen die
Memberfunktionen
find_first_of(...) und
find_first_not_of(...). Beide
Memberfunktionen erhalten
als Parameter einen String
übergeben, der den
Zeichenvorrat enthält.
find_first_of(...) liefert die
Position zurück, an der ein
beliebiges Zeichen aus dem
Zeichenvorrat im String das
erste Mal auftritt.
find_first_not_of(...) hingegen
liefert als Returnwert die
Position, an der das erste
nicht im Zeichenvorrat
vorhandene Zeichen auftritt.
Im Beispiel rechts liefert
find_first_of(..) die Position
des ersten Kleinbuchstabens
(hier 1) und
find_first_not_of(...) die
Position des ersten 'NichtBuchstabens' (hier 11,
Position der 2 in 23).
// Zeichenmengen definieren
string
lowerChar("abcdefghijklmnopqrstuvwxyz");
string
upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
string anyChar(lowerChar+upperChar+' ');
// Zu durchsuchender String
string street("Lerchenweg 23");
// Suchen nach dem ersten Kleinbuchstaben
string::size_type pos =
street.find_first_of(lowerChar);
// Suche nach dem ersten Nicht-Buchstaben
pos = street.find_first_not_of(anyChar);
Suchen nach dem letzten (nicht) Auftreten eines Zeichens aus einem Zeichenvorrat
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (9 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Während die find_first_xx(...)
Memberfunktionen nach dem
ersten (nicht-) Auftreten
eines Zeichens aus einem
Zeichenvorrat suchen, so
suchen die
Memberfunktionen
find_last_of(...) bzw.
find_last_not_of(...) nach dem
letzten (nicht-) Auftreten
eines Zeichens aus dem
Zeichenvorrat. Im Beispiel
rechts liefert find_last_of(...)
die Position 9 zurück, da 'g'
der letzte Kleinbuchstabe im
String ist. find_last_not_of(...)
liefert die Position 12 zurück,
da die 3 (aus der der 23) der
letzte 'Nicht-Buchstabe' im
String ist.
// Zeichenmengen definieren
string
lowerChar("abcdefghijklmnopqrstuvwxyz");
string
upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
string anyChar(lowerChar+upperChar+' ');
// Zu durchsuchender String
string street("Lerchenweg 23");
// Suchen nach dem letzten Kleinbuchstaben
string::size_type pos =
street.find_last_of(lowerChar);
// Suche nach dem letzten Nicht-Buchstaben
pos = street.find_last_not_of(anyChar);
Damit wollen wir an dieser Stelle bewenden lassen. Alle hier vorgestellten Memberfunktionen
haben, wie bereits erwähnt, noch diverse Überladungen die es erlauben, zum Beispiel auch einen
C-String als Suchkriterium bzw. als Zeichenvorrat anzugeben.
Kopieren von strings und Teil-strings
Das Kopieren eines strings in einen anderen string erfolgt (wie üblich) mit dem
Zuweisungsoperator. Was wir uns an dieser Stelle näher ansehen werden ist, wie Sie den Inhalt
eines strings in ein char-Feld umkopieren können.
Benötigen Sie nur einen const char-Zeiger auf den String-Inhalt, so können Sie dafür die
Memberfunktion c_str() verwenden. Sie ist für diesen Fall wesentlich schneller als die
gleich vorgestellte Memberfunktion, da hierbei kein Kopiervorgang stattfindet. Sie
können sich diese Memberfunktion jetzt nochmals ansehen ( ). Eine Veränderung des
Strings ist dann aber nicht möglich.
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (10 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Wollen Sie den Inhalt eines
strings in ein char-Feld
umkopieren, um diesen dann
anschließend verändern zu
können, so verwenden Sie
hierzu die Memberfunktion
copy(...). copy(...) erhält als
ersten Parameter einen
Zeiger auf den
Speicherbereich, in dem der
String-Inhalt abgelegt
werden soll und als zweiten
Parameter die max. Anzahl
der zu kopierenden Zeichen
aus dem String. Als
Returnwert liefert die
Memberfunktion die Anzahl
der kopierten Zeichen.
// String definieren
string street("Lerchenweg 23");
// char-Feld definieren
char buffer[80];
// String in char-Feld umkopieren
string::size_type len =
street.copy(buffer,sizeof(buffer));
// C-String mit 0 abschliessen!
buffer[len] = 0;
Die kopierte Zeichenfolge wird nicht mit einer '0' abgeschlossen. Wollen Sie den Inhalt
des Feldes als C-String weiterverwenden, so müssen Sie explizit die '0' hinzufügen (siehe
auch Beispiel oben). Außerdem müssen Sie selbst darauf achten, dass das char-Feld
auch groß genug ist, um alle kopierten Zeichen aufnehmen zu können.
Es steht außerdem noch eine zweite copy(...) Memberfunktion zur Verfügung die es gestattet, nur
Teile eines Strings umzukopieren. Dazu erhält die Memberfunktion als dritten Parameter die
Anfangsposition innerhalb des Strings, ab der kopiert werden soll.
Die Memberfunktion copy(...) kopiert aber immer ab einer bestimmten Anfangsposition
innerhalb des Strings bis zum Stringende um. Um aus einem String Teilstrings zu extrahieren
wird die Memberfunktion substr(...) eingesetzt. Im Gegensatz zu copy(...) wird der extrahierte
Teilstring aber nicht in einem char-Feld abgelegt sondern in einem weiteren String, der als
Returnwert von substr(...) zurückgeliefert wird.
Wird substr() aufgerufen
(ohne Parameter!), so wird
der komplette String in einen
neuen String umkopiert. Um
einen Teilstring zu
extrahieren, rufen Sie
substr(...) auf und geben als
ersten Parameter die
Anfangsposition an, ab der
die Zeichen extrahiert
werden sollen und als zweiten
Parameter die Anzahl der zu
// Strings definieren
string street("Lerchenweg 23");
string
lowerChar("abcdefghijklmnopqrstuvwxyz");
string
upperChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
string anyChar(lowerChar+upperChar);
// Suche nach erstem 'Nicht-Buchstaben'
string::size_type pos =
street.find_first_not_of(anyCahr);
// Strassennamen extrahieren
string part1 = street.substr(0,pos);
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (11 von 19) [17.05.2005 11:40:13]
string (Teil 2)
// Hausnummer extrahieren
kopierenden Zeichen (1.
Aufruf von substr(...) rechts). string part2 = street.substr(pos+1);
Als Alternative können Sie
auch den zweiten Parameter
weglassen, in diesem Fall
wird aber der
Anfangsposition bis zum
Ende alles extrahiert (2.
Aufruf von substr(...) rechts).
Das Beispiel rechts extrahiert
aus dem String street zum
Beispiel den Straßennamen
in den String part1 und in
den String part2 die
Hausnummer. Das
Leerzeichen wird dabei
explizit übersprungen.
Anhängen und ersetzen in strings
Anhängen von Zeichen
Zum Anhängen von Zeichen an einen String stehen ebenfalls diverse Memberfunktionen zur
Verfügung, von denen wir uns an dieser Stelle nur die beiden wichtigsten ansehen werden. Mehr
dazu entnehmen Sie bitte der Online-Hilfe.
Soll ein einzelnes Zeichen an
einen String angefügt
werden, so können Sie dies
mithilfe des Operators +=
tun. Um mehrere gleiche
Zeichen an einen String
anzuhängen, wird die
Memberfunktion append(...)
verwendet. Der erste
Parameter gibt die Anzahl
der anzufügenden Zeichen an
und der zweite Parameter
das Zeichen.
string myString("bla bla bla");
// Ein Ausrufezeichen anhängen
myString += '!';
// 5 Fragezeichen anhängen
myString.append(5,'?');
Anhängen von Strings
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (12 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Zum Anfügen von
kompletten Strings kann u.a.
auch der Operator +=
verwendet werden. Ebenfalls
kann die Memberfunktion
append(...) hierfür verwendet
werden. append(...) erlaubt es
auch, einen Teilstring aus
einem anderen String oder CString anzuhängen. Dazu
erhält append(...) dann als
ersten Parameter den String,
aus dem die Zeichen angefügt
werden sollen, als zweiten
Parameter die Startposition
innerhalb dieses Strings und
danach die Anzahl der
anzuhängenden Zeichen.
string string1("eins zwei ");
string string2("drei vier fuenf");
// Komplette strings2 anfügen
string1 += string2;
// C-String anfügen
string1.append("...");
// Teilstring aus string2 anfügen
string1.append(string2,5,4);
Ersetzen von Zeichen
Um Zeichen oder
Zeichenfolgen innerhalb
eines Strings zu ersetzen wird
die Memberfunktion
replace(...) verwendet.
replace(...) kann dabei ein
oder mehrere Zeichen ab
einer bestimmten Position
durch ein oder mehrere
andere Zeichen ersetzen. Die
ersetzende Zeichenfolge kann
dabei entweder als String
oder als C-String angegeben
werden. Im Beispiel rechts
hat der String gruss am
Schluss den Inhalt 'Guten
Morgen Frau Maier'.
string gruss("Hallo #");
string anrede("Frau Maier");
const char* morgen = "Guten Morgen";
// Nach Zeichen # suchen
string::size_type pos = gruss.find('#');
// und durch den String anrede ersetzen
gruss.replace(pos,anrede.length(),anrede);
// 'Hallo' durch den C-String morgen ersetzen
gruss.replace(0,5,morgen,strlen(morgen));
Einfügen und entfernen in strings
Einfügen
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (13 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Zum Einfügen von
Zeichenfolgen dient die
Memberfunktion insert(...).
Insgesamt stehen auch hier
wieder 8 verschiedene
Formen dieser
Memberfunktion zur
Verfügung. Alle
Memberfunktionen erhalten
als ersten Parameter den
Index, ab der das
einzufügende Zeichen bzw.
die einzufügende
Zeichenfolge einzufügen ist.
Das Beispiel rechts fügt das
Wort drei zwischen den
Wörtern zwei und drei ein.
// String definieren
string s1("eins zwei vier");
// Position von 'vier' suchen
string::size_type pos = s1.find("vier");
// und davor 'drei ' einfügen
s1.insert(pos,"drei ");
Da man sich in der Regel sowieso nicht alle Formen der insert(...) Memberfunktion merken kann,
sei an dieser Stelle auf die Online-Hilfe verwiesen.
Sehen wir uns zum Abschluss noch an, wie Zeichen bzw. Zeichenfolgen aus einem String entfernt
werden können.
Entfernen
Um den kompletten Inhalt
eines Strings zu löschen, wird
die Memberfunktion clear()
verwendet. Ob dabei auch
der bisher durch den String
belegten Speicher freigegeben
wird ist
implementierungsabhängig.
Definiert ist einzig und allein,
dass der String danach leer
ist.
string s1("Guten Tag");
....
// String-Inhalt löschen
s1.clear();
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (14 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Sollen nur Teile eines Strings
entfernt werden, so ist
hierfür die Memberfunktion
erase(...) einzusetzen. Auch
hiervon gibt es mehrere
Formen. Allen gemeinsam ist,
dass sie als ersten Parameter
die Startposition erhalten, ab
der zu löschen ist. Wird ein
zweiter Parameter
angegeben, so legt er die
Anzahl der zu löschenden
Zeichen fest. Im Beispiel
rechts wird das Wort
wunderschoenen aus dem
String s1 entfernt.
string
s1("Einen wunderschoenen guten Morgen");
string toErase("wunderschoenen");
// Position des zu loeschenden Strings suchen
string::size_type pos1 = s1.find(toErase);
// String nun entfernen
s1.erase(pos1,toErase.length()+1);
Beenden wir damit die Übersicht über die Klasse string. Sie sollten sich aber auf jeden Fall auch
einmal die Online-Hilfe zu dieser Klasse ansehen. Sie finden die Beschreibungen der
Memberfunktionen unter der Klasse basic_string. string selbst ist nur eine Spezialisierung von
basic_string in der Form, dass string char Elemente verarbeitet. Eine andere Form ist wstring zur
Verarbeitung von Elementen vom Typ wchar_t.
Beispiel und Übung
Das Beispiel:
Das Beispiel demonstriert wie man mithilfe von string-Memberfunktionen einen Serienbrief
realisieren kann.
Dazu sind in der Datei für den Serienbrief anstelle der Anrede entsprechende Steuerzeichen
untergebracht. Diese Steuerzeichen werden dann per Programm durch die entsprechenden
Strings für das Geschlecht und den Namen ausgetauscht.
Zusätzlich wird nach jedem Punkt ein neuer Absatz begonnen und am Schluss noch eine
Grußformel angefügt.
Das unten stehende Beispiel finden Sie auch unter: le13\prog\Bstring2.
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (15 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Die Programmausgabe:
=== Orignal-Text war ===
Hallo # %
schoen dass Sie diese Uebung durchfuehren wollen. # %, wenn Sie
diese Uebung ric
htig durchgefuehrt haben, dann sollte jetzt Ihr Name, # %, im Text
erscheinen.
=== Korrgierter Text ist ===
Hallo Frau Maier
schoen dass Sie diese Uebung durchfuehren wollen.
Frau Maier, wenn Sie diese Uebung richtig durchgefuehrt haben, dann
sollte jetzt
Ihr Name, Frau Maier, im Text erscheinen.
Mit freundlichen Gruessen
Das Programm:
// C++ Kurs
#include <iostream>
#include <fstream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Zu verarbeitende Datei
const char *pFILE = "sbrief.txt";
int main()
{
// Datei oeffnen
string serienBrief;
std::ifstream inFile(pFILE);
if (!inFile)
{
cout << "Fehler beim Oeffnen der Datei " << pFILE << endl;
return 1;
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (16 von 19) [17.05.2005 11:40:13]
string (Teil 2)
}
// Komplette Datei in einen string einlesen
while (!inFile.eof())
{
string line;
// Zeile aus Datei einlesen
std::getline(inFile,line);
// und an String anhaengen
serienBrief += line;
// Zeilenvorschub anhaengen
serienBrief += '\n';
}
cout << "=== Orignal-Text war ===\n" << serienBrief << endl;
// Alle '#' im Text durch Anrede ersetzen
string::size_type pos = 0;
do
{
pos = serienBrief.find('#',pos);
if (pos != string::npos)
serienBrief.replace(pos,1,"Frau");
} while (pos != string::npos);
// Alle '%' im Text durch Namen ersetzen
pos = 0;
do
{
pos = serienBrief.find('%',pos);
if (pos != string::npos)
serienBrief.replace(pos,1,"Maier");
} while (pos != string::npos);
// Nach jedem Punkt nun eine neue Zeile beginnen
pos = 0;
do
{
pos = serienBrief.find('.',pos);
if (pos != string::npos)
{
serienBrief.insert(pos+1,"\n");
serienBrief.erase(pos+2,1);
pos++;
}
} while (pos != string::npos);
// Grussformel anhaengen
serienBrief.append("Mit freundlichen Gruessen\n");
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (17 von 19) [17.05.2005 11:40:13]
string (Teil 2)
cout << "=== Korrgierter Text ist ===\n" << serienBrief << endl;
return 0;
}
Die Übung:
Schreiben Sie eine Klasse SplitFullPath die es erlaubt, die vollständige Angabe einer Datei
(bestehend aus Laufwerk, Pfad, Dateiname) in seine Einzelteile Laufwerk, Pfad und Dateiname
zu zerlegen.
Die vollständige Angabe der Datei soll dabei als C-String wie auch als string-Objekt möglich
sein.
Schreiben Sie entsprechende Memberfunktionen die das Laufwerk, den Pfad sowie den
Dateinamen zurückliefern.
Ersetzen Sie im Dateiname dann die ursprüngliche Extension durch die Extension bak.
Ziehen Sie zur Lösung dieser Aufgabe die Online-Hilfe zu Rate. Sie haben später bei der
Lösung Ihrer Aufgaben auch nur noch diese Hilfe zur Verfügung. Sie finden die
Memberfunktionen der string Klasse in der Online-Hilfe unter dem Stichwort
basic_string. Und nicht gleich verzweifeln, wenn die Lösung nach 5 Minuten noch nicht
fertig ist.
Die Programmausgabe:
Der zu zerlegende Dateiname ist:
c:\userdata\temp\data.dat
Laufwerk : c:
Verzeichnis: \userdata\temp
Dateiname : data.dat
Tausche Extension gegen ".bak"
Neuer Name : data.bak
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le13\Lstring2.
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (18 von 19) [17.05.2005 11:40:13]
string (Teil 2)
Und nun wieder viel Spaß mit der nächsten Seite.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/le13_05.htm (19 von 19) [17.05.2005 11:40:13]
Ende Kapitel 4
Ende Kapitel 4
Herzlichen Glückwunsch!
Sie haben damit das vierte Kapitel dieses
Kurses
fertig bearbeitet.
Das nächste Kapitel befasst sich mit C++ für Fortgeschrittene
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le13/k4_end.htm [17.05.2005 11:40:16]
Abgeleitete Klassen
Abgeleitete Klassen
Die Themen:
Einleitung
Beispiel einer Ableitung
Zugriffsrechte
Ableitung
Konstruktor und Destruktor
Zugriff auf Basisklassen-Member
Erweitern von Klassen über Ableitung
Beispiel und Übung
Einleitung
Mit Hilfe der Ableitung (Vererbung, Inheritance) von Klassen lassen sich prinzipiell zwei Aufgaben
durchführen:
Bei der Entwicklung einer Klassenbibliothek bietet die Ableitung den Vorteil, dass Eigenschaften
und gleichartige Memberfunktionen, die in mehreren Klassen benötigt werden, in einer getrennten
Klasse zusammengefasst werden können. Diese zusammengefassten Eigenschaften und
Memberfunktionen können dann in weiteren Klassen 'eingebaut' werden. Der eigentliche Vorteil für
den Entwickler besteht darin, dass er den Code für die Klasse nur einmal zu schreiben und zu
testen(!) braucht.
Für den Anwender einer Klasse bietet die Ableitung ihm die Möglichkeit, eine vorgegebene Klasse in
ihren Eigenschaften und Memberfunktionen erweitern zu können, ohne dass er dazu den Quellcode
der Ausgangsklasse modifizieren muss.
Stellen Sie sich einmal vor, Sie sollen eine Klassenbibliothek zur Darstellung von grafischen
Elementen entwickeln. Da jedes Grafikelement eine Position und Ausdehnung besitzt, können diese
Eigenschaften in einer gesonderten Klasse zusammengefasst werden (was wir nachher auch gleich tun).
Kaufen Sie jetzt als Anwender diese Grafikbibliothek, so stehen Ihnen die vom Entwickler der
Klassenbibliothek implementierten Grafikelemente zur Verfügung. Nehmen wir einmal an, dass diese
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (1 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Bibliothek eine Klasse zur Darstellung eines ausgefüllten Rechtecks enthält. Diese Klasse lässt aber nur
einfach ausgefüllte Rechtecke zu. Sie benötigen aber ein Rechteck, das mit einem Muster ausgefüllt werden
kann. Dann können Sie als Anwender jetzt die vorgegebene Klasse für das ausgefüllte Rechteck einfach als
Basis verwenden und sie um die Eigenschaft 'Muster' erweitern. Die restlichen Eigenschaften und
Memberfunktionen wie z.B. die Position übernehmen Sie unverändert. Sie brauchen für diese Erweiterung
wie gesagt nicht einmal den Quellcode der Ausgangsklasse.
Weiter vorne im Kurs haben Sie ebenfalls eine Möglichkeit kennen gelernt, eine Klasse in ihrer
Funktionalität zu erweitern, nämlich durch einschließen von Klassen. Und damit stellt sich nun die
Frage, wann eine Klasse in eine andere eingeschlossen wird und wann eine Klasse abgeleitet wird. Als
Faustregel können Sie sich folgenden Satz merken: eine Klasse wird in einer anderen Klasse
eingeschlossen wenn zwischen ihnen eine hat-eine (has-a) Beziehung besteht und abgeleitet wird, wenn
eine ist-eine (is-a) Beziehung besteht.
Beispiel: Gegeben sei eine Klasse Color mit Farbinformationen und eine Klasse Win zur Darstellung
eines Fensters. Da ein Fenster eine Farbe hat (und keine Farbe ist), wird die Klasse Color in Win
eingeschlossen. Nun soll für eine besondere Darstellung eine neue Fensterklasse SpecWin geschrieben
werden. In diesem Fall wird SpecWin von Win abgeleitet, da SpecWin ein spezielles Fenster ist (und
kein Fenster 'hat').
Eigentlich nicht schwierig, oder?
Beispiel einer Ableitung
Sehen wir uns zunächst anhand eines Beispiels das prinzipielle Auslagern von gemeinsamen Member
(Eigenschaften/Memberfunktionen) in eine eigene Klasse an.
Ausgangsbeispiel
Rechts sehen Sie zwei Klassen für die
class Frame
Darstellung von einfachen
{
Grafikelementen. Die Klasse Frame soll
short xPos, yPos;
zur Darstellung eines Rahmens dienen
short width, height;
und die Klasse Bar zur Darstellung eines
....
ausgefüllten Rechtecks. Beide Klassen
public:
besitzen nun gewisse Gemeinsamkeiten,
Frame();
die rechts rot hervorgehoben sind. So
void Draw(...);
haben beide Klassen die Eigenschaft,
void SetPosition(...);
dass Sie eine Position und eine Größe
void SetSize(...);
besitzen. Außerdem stehen in beiden
};
Klassen Memberfunktionen zur
Verfügung, um diese Eigenschaften
verändern zu können.
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (2 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Warum die Memberfunktion Draw(...)
hier nicht zu den Gemeinsamkeiten
zählt, das erfahren Sie gleich noch.
class Bar
{
short xPos, yPos;
short width, height;
short fillColor;
....
public:
Bar(...);
void Draw();
void SetPosition(...);
void SetSize(...);
};
Basisklasse definieren
Laut vorheriger Aussage lassen sich nun
diese gemeinsamen Eigenschaften und
Memberfunktionen in eine eigene Klasse
auslagern. Rechts sind die
Gemeinsamkeiten nun in die Klasse
GBase ausgelagert. Um diese
ausgelagerten Eigenschaften und
Memberfunktionen wieder zu den
Klassen Frame und Bar hinzuzufügen,
werden die beiden Klassen Frame und
Bar von der Klasse GBase abgeleitet. Wie
dies genau geht, das erfahren Sie gleich
noch.
Wenn Sie sich die beiden Grafikklassen
Frame und Bar nochmals ansehen
werden Sie feststellen, dass die
Memberfunktion Draw(...) ebenfalls in
beiden Klassen vorhanden ist. Diese
Memberfunktion eignet sich aber nicht
zur Auslagerung in die Klasse GBase, da
sie nachher die Objekte zeichnen soll.
Und ein Rahmen wird sicherlich anders
gezeichnet werden wie ein ausgefülltes
Rechteck.
class GBase
{
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame // Ableitung von GBase einfügen
{
....
public:
Frame(...);
void Draw()
};
class Bar
// Ableitung von GBase einfügen
{
....
short fillColor;
public:
Bar(...);
void Draw();
};
Erweitern von bestehenden Klassen
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (3 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Sehen wir uns die Ableitung nun aus
class PBar
// Ableitung von CBar einfügen
Anwendersicht an, d.h. dem Erweitern
{
einer vorgegebenen Klasse.
....
Angenommen, Sie haben die vorherige
short pattern;
Klassenbibliothek mit den
public:
Grafikelementen käuflich erworben.
PBar(...);
Diese Bibliothek enthält u.a. die Klasse
void Draw();
Bar für ein ausgefülltes Rechteck. Sie
void SetPattern(...);
benötigen aber ein Rechteck, das mit
};
einem von Ihnen definiertem Muster
(Pattern) ausgefüllt werden kann. In
diesem Fall leiten Sie einfach eine neue
Klasse PBar von der vorgegebenen
Klasse Bar ab und fügen ihr dann die
Eigenschaft Muster (Pattern) sowie die
entsprechenden Memberfunktionen
hinzu, und schon haben Sie eine neue
Klasse für ein Rechteck das mit einem
Muster ausgefüllt werden kann. Und das
sogar ohne den Quellcode der Klasse Bar
besitzen zu müssen!
Im Folgenden tauchen im Zusammenhang mit
Ableitungen zwei neue Begriffe immer wieder auf:
●
●
Basisklasse oder Superklasse
abgeleitete Klasse oder Subklasse
Die Basisklasse ist diejenige Klasse, die ihre
Eigenschaften und Memberfunktionen an die abgeleitete
Klasse weitergibt (vererbt). Die abgeleitete Klasse erbt
(prinzipiell) alle Eigenschaften und Memberfunktion der
Basisklasse, d.h. die abgeleitete Klasse verhält sich so, als
wären die geerbten Eigenschaften und
Memberfunktionen innerhalb der abgeleiteten Klasse
selbst enthalten.
Eine Klasse kann sowohl Basisklasse als auch abgeleitete
Klasse gleichzeitig sein. So ist im Beispiel rechts die
Klasse CBar von CGBase abgeleitete und gleichzeitig
Basisklasse für die Klasse CPBar.
Zugriffsrechte
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (4 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Beim Ableiten spielen auch die
Zugriffsrechte (public, private) eine
wichtige Rolle. Eine abgeleitete Klasse
hat nur Zugriff auf die public (und die
gleich noch eingeführten protected)
Member der Basisklasse, nicht aber auf
ihre private Member. Im Beispiel rechts
kann die Klasse Frame z.B. nur auf die
Memberfunktionen SetPosition(...) und
SetSize(...) der Basisklasse zugreifen aber
nicht auf deren Eigenschaften, da diese
private sind.
class GBase
{
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame // Ableitung von GBase einfügen
{
....
public:
Frame(...);
void Draw()
};
Eine abgeleitete Klasse erbt aber nicht nur die Eigenschaften und 'normalen' Memberfunktionen von ihrer
Basisklasse, sondern auch deren überladene Operatoren, mit einer einzigen Ausnahme die Sie nie vergessen
sollten:
Überlädt eine Basisklasse den Zuweisungsoperator '=', so wird dieser überladene Operator niemals
an die abgeleitete Klasse vererbt!
Alle anderen, nicht-private in der Basisklasse überladenen Operatoren stehen in der abgeleiteten Klasse
ebenfalls zur Verfügung.
Resultierende abgeleitete Klasse
Wie stellt sich nun eine von einer
Basisklasse abgeleitete Klasse
(Subklasse) prinzipiell nach außen hin,
d.h. für den Anwender, dar? Da die
Subklasse alle Member ihrer Basisklasse
erbt, sieht es für den Anwender (und
auch für weitere von der Subklasse
abgeleitete Klassen) so aus, als ob die
nicht-private Member der Basisklasse
innerhalb der Subklasse
deklariert/definiert wären. private
Member der Basisklasse sind ja niemals
nach außen hin sichtbar. Dieses
Verhalten ist rechts unten durch die
fiktive Klasse FFrame einmal dargestellt.
class GBase
{
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame // Ableitung von GBase einfügen
{
....
public:
Frame(...);
void Draw()
};
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (5 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Die aus der Ableitung resultierende fiktive Klasse FFrame
(Anwendersicht)
class FFrame
{
....
// Nur Eigenschaften von Frame
public:
Frame(...);
void Draw();
void SetPosition(...);
void SetSize(...);
};
Zugriffsrecht protected
Bevor gleich näher auf das eigentliche
Ableiten eingegangen wird, soll noch das
bis jetzt fehlende Zugriffsrecht protected
erläutert werden. Das Zugriffsrecht
protected ist eine Mischung aus den
beiden bereits bekannten
Zugriffsrechten public und private und
kann überall dort stehen, wo
Zugriffsrechte erlaubt sind. Auf die
protected Member einer Klasse kann nur
von abgeleiteten Klassen aus direkt
zugegriffen werden, d.h. sie verhalten
aus der Sicht der abgeleiteten Klasse aus
wie public Member. Der Zugriff auf
protected Member aus anderen Klassen
oder Funktionen heraus ist jedoch nicht
möglich. Hier verhalten sich die
protected Member also wie private
Member. Im Beispiel rechts können
Memberfunktionen der Klasse Frame
direkt auf die protected Member der
Klasse GBase zugreifen während alle
anderen Klassen oder Funktionen
keinen Zugriff darauf haben.
class GBase
{
protected:
short xPos, yPos;
short width, height;
....
};
// Definition der abgeleiteten Klasse
class Frame // Ableitung GBase einfügen
{
....
};
Ableitung
Doch sehen wir uns jetzt an, wie eine Klasse von einer anderen Klasse abgeleitet wird.
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (6 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Das Ableiten eine Subklasse von einer
Basisklasse erfolgt nach folgender
Syntax:
class CSub: ACCESS CSuper
{
....
};
// Definition der Basisklasse
class GBase
{
....
};
// Definition der abgeleiteten Klasse
class Frame: public GBase
{
....
};
CSub ist der Name der abgeleiteten Klasse und CSuper der Name ihrer Basisklasse. ACCESS gibt an, mit
welchem Zugriffrecht aus der Sicht des Anwenders oder weiterer abgeleiteten Klassen die Member der
Basisklasse in die abgeleitete Klasse übernommen werden. Für ACCESS können folgende Schlüsselwörter
stehen:
public, protected, private
Wie gesagt, das Zugriffsrecht ACCESS spielt nur aus der Sicht des Anwenders oder weiteren abgeleiteten
Klassen eine Rolle. Die abgeleitete Klasse selbst hat immer Zugriff auf alle public und protected Elemente
ihrer Basisklasse.
Wie sich diese Zugriffsrechte im Einzelnen auswirken soll jetzt anhand eines Beispiels demonstriert werden.
Die public Ableitung
Vorgegeben sind die drei Klassen GBase,
Frame und MyFrame. GBase enthält die
drei Member pub, prot und priv mit den
entsprechenden Zugriffsrechten. Frame
ist nun public von GBase abgeleitet. Bei
einer public Ableitung werden die
Zugriffsrechte aus der Basisklasse 1:1 in
die abgeleitete Klasse übernommen.
Damit kann dann auch über ein Objekt
der abgeleiteten Klasse Frame auf das
public Member pub der Basisklasse
zugegriffen werden.
Bedenken Sie immer: von
Memberfunktionen einer direkt
abgeleiteten Klassen kann immer
auf die public und protected
Member der Basisklasse
zugegriffen werden, unabhängig
vom Zugriffsrecht bei der
Ableitung.
// Definition der Basisklasse
class GBase
{
public: short pub;
protected: short prot;
private: short priv;
};
// Von GBase abgeleitete Klasse
class Frame: public GBase
{
....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
....
void AnyMethode()
{
pub = ....;
// ist erlaubt
prot = ....; // ist erlaubt
priv = ....; // ist nicht erlaubt
}
}
Wird nun von der abgeleiteten Klasse
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (7 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Frame eine weitere Klasse MyFrame
abgeleitet, so kann auch von
Memberfunktionen dieser Klasse aus auf
die Member pub und prot der Basisklasse
zugegriffen werden. Auch der Zugriff
auf das Member pub über ein Objekt
vom Typ MyFrame ist immer noch
erlaubt.
// Objekt der Klassen Frame definieren
Frame
frameObj;
// Zugriffe
frameObj.pub = ...; // ist erlaubt
frameObj.prot = ...; // nicht erlaubt
frameObj.priv = ...; // nicht erlaubt
Die protected Ableitung
Leiten wir jetzt die Klasse Frame
protected von GBase ab. Die Ableitung
der Klasse MyFrame lassen wir
gegenüber vorhin unverändert auf
public. Durch die protected Ableitung
werden alle public-Member der
Basisklasse zu protected-Member der
abgeleiteten Klasse. Der Unterschied
zum vorherigen Beispiel liegt im
Zugriffsrecht des public Members der
Basisklasse. Und damit können zwar die
Memberfunktionen der Klassen Frame
und MyFrame immer noch auf das
Member pub zugreifen, aber der direkte
Zugriff darauf aus der Anwendung
heraus über Objekte von Typ Frame und
MyFrame ist gesperrt.
Selbstverständlich könnten Sie über ein
Objekt der Basisklasse GBase immer
noch auch das public Member pub
zugreifen.
// Definition der Basisklasse
class GBase
{
public: short pub;
protected: short prot;
private: short priv;
};
// Von GBase abgeleitete Klasse
class Frame: protected GBase
{
....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
....
void AnyMethode()
{
pub = ....;
// ist erlaubt
prot = ....; // ist erlaubt
priv = ....; // ist nicht erlaubt
}
}
// Objekt der Klassen Frame definieren
Frame
frameObj;
// Zugriffe
frameObj.pub = ...; // nicht erlaubt
frameObj.prot = ...; // nicht erlaubt
frameObj.priv = ...; // nicht erlaubt
Die private Ableitung
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (8 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Und nun zum letzten Fall, der private
Ableitung. Jetzt werden alle vererbten
Member der Basisklasse zur private
Member der abgeleiteten Klasse. Damit
besitzt nur noch die Klasse Frame
Zugriff auf die public und protected
Member der Basisklasse GBase. Selbst
die Memberfunktionen der von Frame
abgeleiteten Klasse MyFrame haben jetzt
keinen Zugriff mehr auf die Member der
Klasse GBase. Dies ist die 'schärfste'
Form der Ableitung. Objekte vom Typ
Frame haben auch hier ebenfalls keinen
Zugriff auf die Member der Basisklasse.
// Definition der Basisklasse
class GBase
{
public: short pub;
protected: short prot;
private: short priv;
};
// Von GBase abgeleitete Klasse
class Frame: private GBase
{
....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
....
void AnyMethode()
{
pub = ....;
// ist nicht erlaubt
prot = ....; // ist nicht erlaubt
priv = ....; // ist nicht erlaubt
}
}
// Objekt der Klassen Frame definieren
Frame
frameObj;
// Zugriffe
frameObj.pub = ...; // nicht erlaubt
frameObj.prot = ...; // nicht erlaubt
frameObj.priv = ...; // nicht erlaubt
Zusammenfassung
Die nachfolgende Tabelle zeigt nochmals in einer Übersicht, wie die Zugriffsrechte von Basisklassen-Member
beim Ableiten umgewandelt werden.
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (9 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Konstruktor und Destruktor
Damit haben wir das Prinzip des Ableitens abgehandelt und wir können zum nächsten Schritt übergehen,
der Abarbeitung der Konstruktore und Destruktore beim Ableiten von Klassen.
Als Ausgangspunkt dient wieder die
Klasse GBase sowie die von ihr
abgeleitete Klasse Frame. Beide Klassen
enthalten nun entsprechende
Konstruktore. Soll ein Objekt der Klasse
Frame definiert werden, so ist die
Position, Größe und Linienfarbe des
Rahmenobjekts anzugeben. Da Frame
selbst aber nur die Linienfarbe enthält,
müssen die anderen Daten irgend wie an
die Basisklasse GBase weitergegeben
werden. GBase enthält ja einen
entsprechenden Konstruktor, der die
erforderlichen Daten als Parameter
erhält. Wir müssen jetzt also beim
Definieren eines Frame Objekts 'nur'
noch den Konstruktor von GBase
aufrufen.
class GBase
{
....
public:
GBase (short x, short y,
short w, short h);
....
};
class Frame: public GBase
{
....
short lineColor;
public:
Frame(short x, short y,
short w, short h,
short lCol);
};
// Definition eines Frame Objekts
Frame
myFrame(10,20,640,480,0xC0C0CO);
Konstruktor der abgeleiteten Klasse
Der Aufruf des Konstruktors der Basisklasse erfolgt jetzt bei der Definition des Konstruktors der
abgeleiteten Klasse mit folgender Syntax:
CSub::CSub(P1, P2,...): CSuper(Pa, Pb,...)
{
....
}
CSub ist der Name der abgeleiteten Klasse und CSuper der Name der Basisklasse. D.h. nach der
Parameterklammer des Konstruktors der abgeleiteten Klasse folgt zunächst ein Doppelpunkt und danach
der Aufruf des Konstruktors der Basisklasse. An dessen Konstruktor können dann die erforderlichen Daten
als Parameter übergeben werden, wobei es keine Rolle spielt, ob die zu übergebenden Daten Parameter des
Konstruktors der abgeleiteten Klasse sind (P1, P2,...) oder anderweitig bestimmt werden (zum Beispiel als
Konstanten).
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (10 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Damit kann der Konstruktor der
class GBase
vorherigen Klasse Frame wie rechts
{
angegeben definiert werden. Wie Sie
....
sehen, werden die Parameter x, y, w und
public:
h einfach an den Konstruktor der
GBase (short x, short y,
Basisklasse GBase weitergegeben. Der
short w, short h);
Konstruktor von Frame 'merkt' sich nur
....
die Eigenschaften seiner Klasse, nämlich };
die Linienfarbe.
class Frame: public GBase
{
....
Beachten Sie, dass der Aufruf des
short lineColor;
Konstruktors der Basisklasse nur
public:
bei der Definition des
Frame(short x, short y,
Konstruktors der abgeleiteten
short w, short h,
Klasse steht. An der Deklaration
short lCol);
des Konstruktors innerhalb der
};
abgeleiteten Klasse ändert sich
// Definition des Konstruktors
nichts.
Frame::Frame (short x, short y, short w,
short h, short lCol):
GBase(x, y, w, h)
{
lineColor = lCol;
}
Konstruktor bei mehrstufigen Ableitungen
Doch wie sieht im Fall einer
mehrstufigen Ableitung die Definition
des Konstruktors von MyFrame aus?
Muss er sowohl den Konstruktor von
Frame wie auch den Konstruktor von
GBase aufrufen?
// Konstruktor der Klasse GBase
GBase::GBase(short x, short y,
short w, short h)
{
....
};
// Konstruktor der Klasse Frame
Nein, so schlimm wird's dann doch
Frame::Frame(short x, short y,
nicht! Dies würde sonst sehr schnell zu
short w, short h, short lCol):
recht unübersichtlichen Konstruktoren
GBase(x, y, w, h)
in abgeleiteten Klassen führen. Der
{
Konstruktor von MyFrame muss (und
....
darf) nur den Konstruktor seiner
}
Basisklasse aufrufen, also den von
// Konstruktor der Klasse MyFrame
Frame. Und der Konstruktor von Frame MyFrame::MyFrame(short x, short y,
ruft dann wiederum den seiner
short w, short h,
Basisklasse, also GBase, auf. Rechts
short any):
sehen Sie die prinzipiellen Definitionen
Frame(x, y, w, h, 0xFF)
aller drei Konstruktoren. Beim Aufruf
{
des Konstruktors der Klasse Frame wird
....
im Beispiel für den letzten Parameter
}
eine Konstante übergeben.
Dieser Aufrufmechanismus funktioniert
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (11 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
sogar auch dann, wenn eine Basisklasse
selbst keinen Konstruktor oder 'nur'
den Standard-Konstruktor besitzt. In
diesem Fall braucht der Konstruktor der
abgeleiteten Klasse gar nichts tun. Es
wird immer automatisch der 'nächst
höhere' Konstruktor aufgerufen.
Aufruf-Reihenfolge der Konstruktore
Wird nun ein Objekt einer abgeleiteten
Klasse definiert, so läuft folgender
Vorgang ab. Zuerst wird der
Konstruktor der Basisklasse ausgeführt
damit sichergestellt ist, dass beim
Eintritt in den Konstruktor der
abgeleiteten Klasse alle Eigenschaften
der Basisklasse bereits initialisiert sind.
class GBase
{
....
};
Ist die Basisklasse selbst wiederum von
einer anderen Klasse abgeleitet, so wird
zuerst deren Konstruktor ausgeführt. Im
Beispiel rechts sehen Sie die Klassen
GBase, Frame und MyFrame mit den
bereits bekannten Ableitungen. Da
GBase die 'oberste' Basisklasse ist, wird
auch deren Konstruktor als erstes
ausgeführt. Danach wird der
Konstruktor von Frame ausgeführt und
ganz zum Schluss der Konstruktor der
'untersten' Klasse MyFrame.
class MyFrame: public Frame
{
....
};
class Frame: public GBase
{
....
};
Aufruf-Reihenfolge der Konstruktoren:
1) Konstruktor von GBase
2) Konstruktor von Frame
3) Konstruktor von MyFrame
Aufruf-Reihenfolge der Destruktoren
Und was für die Konstruktore gilt, gilt
auch für die Destruktoren. Nur ist hier
die ganze Sache etwas einfacher, da
Destruktoren keine Parameter besitzen
und auch niemals direkt aufgerufen
werden. Beim Schreiben eines
Destruktors müssen Sie also (zunächst
einmal) keine Rücksicht darauf nehmen,
ob die Klasse von einer anderen Klasse
abgeleitet ist. Wird ein Objekt das in der
Ableitungshierarchie ganz unten steht
entfernt, so wird zuerst dessen
Destruktor ausgeführt. Anschließend
wird der Destruktor seiner Basisklasse
und dann der Destruktor der nächst
höheren Basisklasse ausgeführt. Die
Aufruf-Reihenfolge ist hier genau
class GBase
{
....
};
class Frame: public GBase
{
....
};
class MyFrame: public Frame
{
....
};
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (12 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
umgekehrt wie bei den Konstruktoren.
Aufruf-Reihenfolge der Destruktoren:
1) Destruktor von MyFrame
2) Destruktor von Frame
3) Destruktor von GBase
Zugriff auf Basisklassen-Member
Doch nun genug abgeleitet, sehen wir uns jetzt an wie auf Member innerhalb von abgeleiteten Klassen
zugegriffen wird.
Beginnen wir mit dem Zugriff auf die
Member einer Basisklasse aus
Memberfunktionen einer abgeleiteten
Klasse heraus. Wie Sie schon wissen,
können abgeleitete Klassen auf alle nichtprivate Member der Basisklasse
zugreifen. Hierzu reicht es im
Normalfall aus, wenn der entsprechende
Membername angegeben wird. Nichtprivate Member von Basisklassen
verhalten sich für abgeleitete Klassen ja
prinzipiell wie eigene Member. Im
Beispiel rechts wird von der
Memberfunktion DoAnything(...) der
abgeleiteten Klasse aus auf die
Eigenschaft xPos der Basisklasse
zugegriffen und deren Memberfunktion
SetSize(...) aufgerufen.
In wie weiter der direkte Zugriff auf
Basisklassen-Member einer sauberen
Programmierung entspricht soll hier
nicht zur Debatte stehen. Das Beispiel
dient nur zur Demonstration, wie auf die
Member einer Basisklasse zugegriffen
werden kann.
class GBase
{
protected:
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame: public GBase
{
....
public:
Frame(...);
void DoAnything()
};
// Zugriff auf Member der Basisklasse
void Frame::DoAnything()
{
xPos -= 10;
// Sind Member der
SetSize(100,100); // der Basisklasse!
....
}
Gleiche Member in Basis- und Sub-Klasse
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (13 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Dieser Automatismus funktioniert aber
nur solange, solange wie die abgeleitete
Klasse keine Member mit gleichem
Namen besitzt wie die Basisklasse.
Besitzt die abgeleitete Klasse dagegen
Member mit gleichen Namen wie die
Basisklasse, so wird standardmäßig
immer das Member der eigenen Klasse
angesprochen. Um das Member der
Basisklasse anzusprechen, ist vor dem
Membernamen noch der Name der
Basisklasse anzugeben, gefolgt vom
obligatorischen Zugriffsoperator ::.
class GBase
{
protected:
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame: public GBase
{
....
short width;
public:
Frame(...);
void DoAnything()
};
// Zugriff auf Member der Basisklasse
void Frame::DoAnything()
{
width++;
// eigenes Member
GBase::width++; // Basisklassen-Member
....
}
Zugriff auf Basisklassen-Member über Objekte
Auch beim Zugriff über Objekten der
abgeleiteten Klassen auf BasisklassenMember reicht im Regelfall die Angabe
des Membernamens. Der erste Zugriff
rechts über ein Objekt der Klasse Frame
ruft die Memberfunktion SetPosition(...)
der Klasse GBase auf. Ebenso wird die
Memberfunktion SetSize(...) der Klasse
GBase aufgerufen, wenn der Aufruf über
ein Objekt der Klasse MyFrame erfolgt.
Dieser Zugriffsmechanismus
funktioniert also auch über mehrere
Ableitungsebenen hinweg.
class GBase
{
protected:
short xPos, yPos;
short width, height;
public:
void SetPosition(...);
void SetSize(...);
};
class Frame: public GBase
{
....
};
class MyFrame: public Frame
{
....
};
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (14 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
// Objekt der abgeleiteten Klassen
Frame
frameObj;
MyFrame myFrameObj;
// Zugriff auf public Member von GBase
frameObj.SetPosition(...);
myFrameObj.SetSize(...);
Verdeckte Basisklassen-Member
Im Zusammenhang mit Aufrufen von
Basisklassen-Memberfunktionen soll
noch auf eine kleine Stolperfalle
hingewiesen werden. Sehen Sie zunächst
einmal die rechts dargestellten Klassen
an. Sowohl Frame wie auch MyFrame
besitzen jeweils eine Memberfunktion
Move(...). Die Memberfunktion der
Klasse Frame besitzt jedoch einen
Parameter und die der Klasse MyFrame
zwei Parameter.
Die Arbeitsweisen ersten beiden Aufrufe
im Beispiel unten rechts sollten Ihnen
jetzt keine mehr Schwierigkeiten
bereiten. Hier wird einfach die zum
Objekt gehörige Memberfunktion
aufgerufen.
class Frame: public GBase
{
public:
void Move(short x);
....
};
Einen Fehler verursacht dagegen der
dritte Aufruf. Was hier versucht wird,
ist die Memberfunktion Move(...) der
Klasse Frame über ein MyFrame Objekt
aufzurufen. Die Sache mit dem
Überladen von Memberfunktionen über
Klassengrenzen hinweg funktioniert
leider (oder meistens zum Glück!) nicht.
Eine Memberfunktion der abgeleiteten
Klasse verdeckt immer gleichnamige
Memberfunktionen (und Eigenschaften)
ihrer Basisklasse(n). Wollen Sie
trotzdem auf die Memberfunktion der
Move(...) der Klasse Frame über ein
MyFrame Objekt zugreifen, so müssen
Sie, wie rechts im vierten Aufruf
angegeben, nach dem Punktoperator
zunächst den Klassennamen, dann den
Zugriffsoperator :: und erst danach die
Memberfunktion angeben.
// Objekt der abgeleiteten Klassen
Frame
frameObj;
MyFrame myFrameObj;
class MyFrame: public Frame
{
public:
void Move(short x, short y);
....
};
// Aufrufe der Memberfunktion Move(...)
frameObj.Move(10);
myFrameObj.Move(10,20);
myFrameObj.Move(10); // Fehler!
myFrameObj.Frame::Move(10)
Basisklassenzeiger
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (15 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
So, jetzt wissen schon fast alles über
'einfache' Ableitungen. Im
Zusammenhang mit abgeleiteten Klassen
sei an dieser Stelle vorab noch auf
folgenden wichtigen Sachverhalt
hingewiesen:
Ein Zeiger vom Typ der
Basisklasse kann auch Zeiger
vom Typ einer abgeleiteten
Klasse aufnehmen.
Diese Eigenschaft ist rechts dargestellt.
Sie spielt später in der Lektion über
virtuelle Memberfunktionen eine
entscheidende Rolle.
class GBase
{
....
};
class Frame: public GBase
{
....
};
class MyFrame: public Frame
{
....
}
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase = new MyFrame(...);
}
Damit wäre diese doch recht umfangreiche Lektion fast beendet. Zum Schluss sehen wir uns nochmals an,
wie Sie eine vorgegebene Klasse mittels Ableitung anpassen können.
Erweitern von Klassen über Ableitung
Wenn Sie eine Klassenbibliothek
käuflich erwerben, erhalten Sie nur in
wenigen Fällen den Quellcode der
Bibliothek. In der Regel erhaltenen Sie
den übersetzen Quellcode in Form einer
Objektdatei xxx.obj oder
Bibliotheksdatei xxx.lib sowie die
dazugehörige Header-Datei xxx.h. Wenn
Sie nun eine Klasse aus der
Klassenbibliothek einsetzen, müssen Sie
zum einen die Header-Datei xxx.h in
Ihren Quellcode einbinden und zum
anderen den Objektcode xxx.obj bzw.
xxx.lib zu Ihrem Programm dazulinken.
Modul myprog.cpp das die Klasse verwendet
#include "oclass.h"
....
int main()
{
....
}
Projektdatei für den Linker
load myprog.obj
load oclass.lib
Um jetzt eine vorgegebene Klasse anzupassen gehen Sie am Besten wie folgt vor:
Erzeugen Sie eine neue HeaderDatei für die Klassendefinition Ihrer
neuen Klasse (hier MyClass.h).
#ifndef MYCLASS_H
#define MYCLASS_H
....
#endif
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (16 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
#ifndef MYCLASS_H
Binden Sie in diese neue Header- #define MYCLASS_H
#include "oclass.h"
Datei die Header-Datei der zu
erweiternden Klasse ein (hier oclass.h). ....
#endif
Leiten Sie dann in der HeaderDatei eine neue Klasse von der zu
erweiternden Klasse ab und erweitern
Sie die neue Klasse entsprechend den
Anforderungen.
#ifndef MYCLASS_H
#define MYCLASS_H
#include "oclass.h"
class MyClass: public OClass
{....};
#endif
#include "myclass.h"
.... // hier jetzt die Memberfunktionen der
Erstellen Sie eine Quellcode-Datei
.... // neuen Klasse definieren
in der Sie die Memberfunktionen Ihrer
neuen Klasse definieren. Am Besten
geben Sie dabei der Quellcode-Datei den
gleichen Namen wie der Header-Datei,
natürlich jetzt aber mit der Extension
cpp. Binden Sie die Header-Datei mit
ihrer neuen Klassendefinition ein.
#include "myclass.h"
....
Binden Sie dann anstelle der
int main()
ursprünglichen Header-Datei Ihre neue
{
Header-Datei im Programm ein und
MyClass myObj;
fügen Sie die Quellcode-Datei Ihrer
....
neuen Klasse (myclass.cpp) zum Projekt
}
hinzu.
So einfach geht's, wenn man es weiß. Und nun wieder das Beispiel zur Lektion und dann Ihre Übung.
Beispiel und Übung
Das Beispiel:
Als Basisklasse in diesem Beispiel fungiert die Klasse Window. Sie enthält die Grundeigenschaften eines
Fensters. Über Memberfunktionen der Klasse kann ein Fenster verschoben (MoveWindow(...)) und
dargestellt werden (Draw(....)). Zusätzlich wird der Zuweisungsoperator private überladen damit
Fensterobjekte nicht einander zugewiesen werden können.
Diese Grundeigenschaften der Klasse Window werden dann durch die Klasse FrameWin für die Darstellung
eines Fensters mit einem farbigen Rahmen erweitert. Für die Farbauswahl des Rahmens wird ein enumDatentyp verwendet. Beachten Sie bitte, dass der enum-Datentyp public sein muss und die dazugehörige
enum-Variable aber als private definiert ist. Dies ist deshalb notwendig, da bei der Definition des
Rahmenfensters die Rahmenfarbe als enum-Parameter mit angegeben wird und dadurch ein Zugriff auf
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (17 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
die enum-Konstanten erforderlich ist. Der Konstruktor von CFrameWin ruft, wie in der Lektion
beschrieben, den der Klasse Window auf. Hierbei werden die Parameter für die Fenstergröße als
Konstanten übergeben, d.h. ein Rahmenfenster besitzt im Beispiel immer die feste Größe 640x480. Sehen
Sie sich auch die Memberfunktion Draw(...) des Rahmenfensters genauer an. Dort wird die
Memberfunktion Draw(...) der Klasse Window aufgerufen. Sie müssen bei diesem Aufruf unbedingt die
Klasse mit angeben, da sich sonst die Memberfunktion Draw(...) der Klasse CFrameWin selbst wieder
aufrufen würde.
Ebenfalls erwähnenswert ist die Tatsache, dass nur die Klasse Window einen Destruktor enthält; er muss
den für den Fenstertitel reservierten Speicherplatz ja wieder freigeben. Dieser Destruktor wird auch
aufgerufen wenn ein CFrameWin Objekt zerstört wird, und das obwohl CFrameWin selbst keinen
Destruktor besitzt!
Im Programm werden dann Objekte von jedem Fenstertyp definiert und 'dargestellt'. Anschließend wird
das Rahmenfenster mittels MoveWindow(...) verschoben was zum Aufruf der entsprechenden
Memberfunktion in der Basisklasse Window führt.
Das unten stehende Beispiel finden Sie auch unter: le14\prog\Babgelei.
Die Programmausgabe:
Pos (10,10), Groesse (800,600)
Titel: Normales Fenster
Pos (40,40), Groesse (640,480)
Titel: Rahmenfenster
Rahmenfarbe: blau
Pos (0,0), Groesse (640,480)
Titel: Rahmenfenster
Rahmenfarbe: blau
Das Programm:
// C++ Kurs
// Beispiel zu abgeleiteten Klassen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
// Nun Namensraum auf std setzen
using std::cout;
using std::endl;
using std::string;
// Definition der allg. Fensterklasse
// ==================================
class Window
{
short xPos, yPos;
// Fensterposition
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (18 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
short width, height;
// Fenstergroesse
string title;
// Fenstertitel
// Zuweisungsoperator als private definieren damit
// Objekte nicht einander zugewiesen werden können
Window& operator=(const Window&)
{return *this;};
public:
Window(short x, short y, short w, short h,
const char* const pT);
void MoveWindow(short x, short y);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
Window::Window(short x, short y, short w, short h,
const char* const pT):
xPos(x), yPos(y), width(w),
height(h), title(pT)
{}
// Verschiebt Fenster
inline void Window::MoveWindow(short x, short y)
{
xPos = x; yPos = y;
}
// Zeichnet Fenster
void Window::Draw() const
{
cout << "Pos (" << xPos << ',' << yPos << "), Groesse (" <<
width << ',' << height << ")\n";
cout << "Titel: " << title << endl;
}
// Abgeleitete Klasse fuer Rahmenfenster
// =====================================
class FrameWin: public Window
{
public:
// enums fuer Rahmenfarbe, muessen public sein da
// sie im ctor benoetigt werden
enum eFColor{ROT=0xFF0000,GRUEN=0x00FF00,BLAU=0x0000FF};
private:
eFColor frameColor;
// Rahmenfarbe
public:
FrameWin(short x, short y,
const char* const pT, eFColor eFC);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
FrameWin::FrameWin(short x, short y,
const char* const pT, eFColor eFC)
:Window(x, y, 640, 480, pT), frameColor(eFC)
{}
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (19 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
// Zeichnet Fenster
void FrameWin::Draw() const
{
// 'Normales' Fenster zeichnen
Window::Draw();
// Rahmenfarbe darstellen
cout << "Rahmenfarbe: ";
switch (frameColor)
{
case ROT:
cout << "rot";
break;
case BLAU:
cout << "blau";
break;
case GRUEN:
cout << "gruen";
}
cout << endl << endl;
}
// Hauptprogramm
// =============
int main()
{
// Ein normales und ein Rahmenfenster erstellen
Window myWin(10,10,800,600,"Normales Fenster");
// Beachten Sie die Angabe des enum-Parameter (letzter Parameter)!
FrameWin myFrameWin(40,40,"Rahmenfenster",FrameWin::BLAU);
// Beide Fenster darstellen
myWin.Draw();
myFrameWin.Draw();
// Rahmenfenster verschieben
// MoveWindow(...) ist Memberfunktion von Window!
myFrameWin.MoveWindow(0,0);
// Rahmenfenster erneut darstellen
myFrameWin.Draw();
}
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (20 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Die Übung:
In dieser Übung sollen Sie eine bestehende Klasse erweitern.
Vorgegeben ist die unten aufgeführte Klasse Address zum Abspeichern von Adressdaten. Der Einfachheit
halber enthält die Klasse im Beispiel nur einen Namen. Zum Einlesen und Ausgeben von Adressdaten
stehen die beiden überladenen Operatoren << und >> zur Verfügung.
Im Hauptprogramm wird dann ein Objektfeld für drei Adresseinträge dynamisch angelegt. Anschließend
werden die Adressen (hier nur der Name) von der Tastatur mittels des überladenen Operators >>
eingelesen. Die eingelesenen Daten werden dann über den Operator << auf die Standardausgabe
ausgegeben. Danach werden die Adressdaten noch einer Datei abgelegt. Auch hierfür wird wieder der
überladene Operator << aufgerufen, wobei als Stream nun anstelle von cout der Dateistream verwendet
wird. Zum Schluss muss das Objektfeld mit den Adressdaten dann noch entfernt werden.
Ihre Aufgabe ist es nun eine weitere Klasse Student zu schreiben. Diese Klasse soll die Ausgangklasse
Address um eine Eigenschaft zur Aufnahme eines vom Studenten belegten Kurses erweitern. Der Kurs ist
als String abzulegen. Auch für diese Klasse ist für die Ein- und Ausgabe der überladene Operator >> bzw.
<< zu verwenden.
Sie müssen innerhalb der überladenen Operatoren << und >> für die Klasse Student die
überladenen Operatoren für die Basisklasse Address aufrufen. Wie Sie einen überladenen Operator
direkt aufrufen, das können Sie sich hier nochmals ansehen ( ).
Schreiben Sie dann das Hauptprogramm so um, dass anstelle eines Address Objektfeldes ein Student
Objektfeld dynamisch erstellt wird. Der Rest des Programms soll in seiner Funktionalität nicht verändert
werden, d.h. Sie sollen zunächst drei Studentendaten von der Tastatur einlesen, diese dann auf der
Standardausgabe ausgeben und zum Schluss noch in eine Datei ablegen.
In der nachfolgenden Programmausgabe sind die Eingaben rot und unterstrichen hervorgehoben.
Diese Ausgangslisting für die Übung finden Sie auch auf der CD-ROM unter le14\prog\LabgelX.
Die Programmausgabe:
Bitte Studenten-Daten eingeben.
Zuerst den Namen und dann den Kurs.
Beide Angaben bitte in getrennten Zeilen!
1. Datensatz:Gustav Gans<RETURN>
Mathematik<RETURN>
2. Datensatz:Daisy Duck<RETURN>
Physik<RETURN>
3. Datensatz:Duffy Duck<RETURN>
Informatik<RETURN>
Gespeicherte Daten:
Name: Gustav Gans
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (21 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Kurs: Mathematik
Name: Daisy Duck
Kurs: Physik
Name: Duffy Duck
Kurs: Informatik
Speicher Daten jetzt in Datei PERS.DAT
Das Programm:
// C++ Kurs
// Ausgangslisting fuer Uebung zu abgeleiteten Klassen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <fstream>
#include <string>
// Nun Namensraum auf std setzen
using std::cout;
using std::endl;
using std::string;
// Definition der Adressenklasse
// Enthaelt der Einfachheit halber nur einen Namen
class Address
{
string name;
public:
Address();
friend std::ostream& operator << (std::ostream& OS, const Address& obj);
friend std::istream& operator >> (std::istream& IS, Address& obj);
};
// Definition der Memberfunktionen
// Standard-ctor, wird fuer Objektfeld immer benoetigt
Address::Address()
{}
// Gibt Adresse aus
std::ostream& operator << (std::ostream& os, const Address& obj)
{
os << "Name: ";
// Falls kein Name vorhanden, irgendwas ausgeben
if (obj.name.length() == NULL)
os << "unbekannt!\n";
// sonst Namen ausgeben
else
os << obj.name << endl;
return os;
}
// Liest Adresse ein
std::istream& operator >> (std::istream& is, Address& obj)
{
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (22 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
// Ganze Zeile einlesen
getline(is,obj.name);
return is;
}
// Konstanten fuer Dateiname und Groesse des Objektfeldes
const char* const pFILENAME = "PERS.DAT";
const int ARRAYSIZE=3;
// Hauptprogramm
// =============
int main()
{
int index;
Address *pAddr;
// Schleifenindex
// Zeiger fuer Objektfeld
// Objektfeld erstellen
pAddr = new Address[ARRAYSIZE];
// Nun die Namen von der Tastatur einlesen
cout << "Bitte " << ARRAYSIZE << " Namen eingeben:\n";
for (index=0; index<ARRAYSIZE; index++)
{
cout << index+1 << ". Name:";
// Adresse einlesen
std::cin >> pAddr[index];
}
// Zur Kontrolle die eingelesenen Daten ausgeben
cout << "\nGespeicherte Daten:\n";
for (index=0; index<ARRAYSIZE; index++)
// Adresse ausgeben
cout << pAddr[index];
// Nun Daten in Datei ablegen
cout << "Speicher Daten jetzt in Datei " << pFILENAME << endl;
// Datei oeffnen
std::ofstream outFile;
outFile.open(pFILENAME);
// Falls Datei erfolgreich geoeffnet
if (outFile)
{
// Daten ablegen
for (index=0; index<ARRAYSIZE; index++)
outFile << pAddr[index];
outFile.close();
}
else
cout << "Fehler beim Oeffnen der Datei!\n";
// Objektfeld loeschen
delete [] pAddr;
}
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (23 von 24) [17.05.2005 11:40:20]
Abgeleitete Klassen
Die Erweiterung sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le13\Lstring2.
In dieser Lektion haben Sie die einfache Ableitung kennen gelernt, in der nächsten Lektion erfahren Sie was
mehrfache Ableitungen sind.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le14/le14_01.htm (24 von 24) [17.05.2005 11:40:20]
Mehrfach abgeleitete Klassen
Mehrfach abgeleitete Klassen
Die Themen:
Grundprinzip der Mehrfach-Ableitung
Konstruktor und Destruktor
Gleichnamige Member in den Basisklassen
Beispiel
Grundprinzip der Mehrfach-Ableitung
Bisher wurde eine Klasse immer nur von einer Klasse
direkt abgeleitet. C++ gibt Ihnen aber auch die
Möglichkeit, eine Klasse von mehreren Basisklassen
abzuleiten. Die Anzahl der Basisklassen ist nicht
begrenzt, jedoch sollten Sie immer versuchen ein Klasse
von maximal zwei oder drei Basisklassen abzuleiten.
Ansonsten geht leicht der Überblick verloren. Rechts ist
anhand eines kleinen Bildes das Prinzip der
mehrfachen Ableitung dargestellt. Die Klasse CMixed
vereint zunächst alle Eigenschaften/Memberfunktionen
der Klassen CBase1 und CBase2 und fügt eventuell
eigene hinzu.
Die Ableitung einer Klasse von
// 1. Basisklasse
mehreren Basisklassen erfolgt analog
class CBase1
dem Ableiten von einer Basisklasse, nur {....};
werden jetzt bei der Definition der
abgeleiteten Klasse mehrere
// 2. Basisklasse
Basisklassen, einschließlich deren
class CBase2
Zugriffsrechte(!), angegeben. Die
{....};
einzelnen Basisklassen werden dabei
durch Komma voneinander getrennt.
// Neue abgeleitete Klasse
class CMixed: public CBase1, protected CBase2
{....}
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (1 von 6) [17.05.2005 11:40:24]
Mehrfach abgeleitete Klassen
Konstruktor und Destruktor
Außer der Klassendefinition muss nun
selbstverständlich auch der Konstruktor
der abgeleiteten Klasse entsprechend
angepasst werden. Er muss nun alle
Konstruktore seiner Basisklassen
aufrufen. Dazu wird, wie bei der
einfachen Ableitung, nach der
Parameterklammer zunächst ein
Doppelpunkt angegeben und danach
werden alle Konstruktore der
Basisklassen aufgelistet. Einzige
Ausnahme: Eine Basisklasse besitzt
einen Standard-Konstruktor
(parameterloser Konstruktor). In diesem
Fall erfolgt der Aufruf automatisch.
Die Konstruktoren der Basisklassen
werden in der Reihenfolge abgearbeitet,
in der die Basisklassen bei der Definition
der abgeleiteten Klasse aufgeführt
wurden. Die Reihenfolge bei der
Definition des Konstruktors der
abgeleiteten Klasse spielt keine Rolle, sie
muss nicht einmal zwingend mit der
Reihenfolge bei der Deklaration
übereinstimmen. Im Beispiel rechts wird
also zuerst der Konstruktor der Klasse
CBase1 ausgeführt und danach der
Konstruktor der Klasse CBase2.
Allerdings sollte bei sauberer
Programmierung diese Reihenfolge
keine Rolle spielen dürfen, da beide
Basisklassen voneinander unabhängig
sind.
// ctor der abgeleiteten Klasse
CMixed::CMixed(P1,P2,...):
CBase1(a1,a2,...),
CBase2(b1,b2,...)
{
....
}
// Definition der abgeleiteten Klasse
class CMixed: public CBase1, protected CBase2
{....}
// Definition des ctors
CMixed::CMixed(P1,P2,...):
CBase2(a1,a2,...),
CBase1(b1,b2,...)
{
....
}
Und auch für die Abarbeitung der Destruktoren gilt das gleiche wie bei einfach abgeleiteten Klassen: die
Destruktoren werden in umgekehrter Reihenfolge ausgeführt wie die Konstruktoren.
Gleichnamige Member in den Basisklassen
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (2 von 6) [17.05.2005 11:40:24]
Mehrfach abgeleitete Klassen
Etwas acht geben müssen Sie, wenn die
Basisklassen Memberfunktionen mit
gleichen Namen enthalten oder sogar die
abgeleitete Klasse noch eine
Memberfunktion mit gleichem Namen
besitzt wie die Basisklassen. In diesem
Fall müssen Sie wieder den
Klassennamen vor dem Aufruf der
Memberfunktion stellen. Dies gilt sogar
auch dann, wenn die Memberfunktionen
unterschiedliche Parameter besitzen.
Wie Sie bestimmt noch aus der
vorherigen Lektion wissen, funktioniert
die Sache mit dem Überladen von
Memberfunktionen nur innerhalb einer
Klasse und nicht über Klassengrenzen
hinweg! Vergessen Sie einmal die Klasse
explizit anzugeben, so meldet Ihnen der
Compiler aber hier einen Fehler.
// 1. Basisklasse
class CBase1
{
....
void DoAnything(...);
};
// 2. Basisklasse
class CBase2
{
....
void DoAnything(...);
};
// Neue abgeleitete Klasse
class CMixed: public CBase1, protected CBase2
{
void AnyMeth(...)
{
CBase2::DoAnything();
}
....
}
Ja und das war's auch schon zu mehrfach abgeleiteten Klassen.
Beispiel
Das Beispiel:
Das Beispiel demonstriert das Zusammenfügen der Klassen string und GBase zu einer neuen Klasse
ColorString für die 'farbige' Darstellung eines Textes auf einer bestimmten Position.
Die Klasse string kennen Sie in der Zwischenzeit ja schon zu genüge. GBase soll eine Basisklasse für
beliebige Grafiken sein. Der Einfachheit wegen enthält sie hier nur eine Positionsangabe. Werden beide
Klassen nun zur Klasse ColorString zusammengefügt, so erhält diese neue Klasse alle Eigenschaften der
Basisklassen. Zusätzlich besitzt ColorString noch die Eigenschaft color für die Verwaltung der
Farbinformation.
Sehen Sie sich auch einmal an, wie in der Memberfunktion ChangeText(...) der String verändert wird. Da
ColorString ja selbst kein string-Objekt enthält sondern 'nur' von string abgeleitet ist, können Sie natürlich
auch keine Zuweisung durchführen (an was den auch?). Glücklicherweise enthält die string-Klasse eine
Memberfunktion assign(...) um den Text im String zu verändern.
Ferner sollten Sie sich noch den überladenen Operator << für die Ausgabe anschauen. Auch dort wird ja
der im String abgelegt Text benötigt. Da die Klasse string nun Basisklasse von ColorString ist, kann die
Memberfunktion c_str(...) der string-Klasse auch über ein ColorString Objekt aufgerufen werden. Das
gleiche gilt übrigens auch für die Memberfunktion GetPos(...) der GBase Klasse.
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (3 von 6) [17.05.2005 11:40:24]
Mehrfach abgeleitete Klassen
Das unten stehende Beispiel finden Sie auch unter: le14\prog\Bmabgel.
Die Programmausgabe:
Text : ColorString
Position: (10,20)
Farbe : 0xcc00cc
Text : ColorString modifiziert
Position: (200,200)
Farbe : 0xcc00cc
Das Programm:
// C++ Kurs
// Beispiel zu mehrfach abgeleiteten Klassen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <iomanip>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Basisklasse fuer die Aufnahme von Grafikpositionen
class GBase
{
int xPos, yPos;
// Grafikposition
public:
GBase(int x, int y): xPos(x), yPos(y)
{}
// Position umsetzen
void SetPos(int x, int y)
{
xPos = x; yPos = y;
}
// Position zurueckgeben
// Ist const-Memberfunktion!
void GetPos(int& x, int& y) const
{
x = xPos; y = yPos;
}
};
//
//
//
//
//
Zusammengesetzte Klasse fuer die Darstellung eines
Textes auf einer bestimmten Position 'in Farbe'
Text wird in string-Klasse abgelegt und die
Position in der GBase-Klasse
Hinweis: Eigentlich sollte die Klasse string als eingeschlossenene
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (4 von 6) [17.05.2005 11:40:24]
Mehrfach abgeleitete Klassen
// Klasse definiert werden da hier eine has-a Beziehung besteht.
class ColorString: public string, public GBase
{
unsigned long color;
// zusaetzliche Farb-Info
public:
// Konstruktor
ColorString(int x, int y, const char* const pT, unsigned long col):
string(pT), GBase(x, y), color(col)
{}
// Verschiebt Grafik
void MoveIt(int x, int y)
{
SetPos(x, y);
// GBase-Memberfunktion hierfuer aufrufen
}
// Aendert den Text
void ChangeText(const char* const pT)
{
assign(pT);
// Memberfunktion assign() der string-Klasse
}
// Ausgabeoperator ueberladen
friend std::ostream& operator << (std::ostream& os, const ColorString&
colString);
};
// Ueberladene Ausgabeoperator um ein ColorString auszugeben
std::ostream& operator << (std::ostream& os, const ColorString& colString)
{
// colString.c_str() holt const char* auf den Text im string
os << "Text
: " << colString.c_str() << '\n';
// Position holen von GBase
int x, y;
colString.GetPos(x, y);
os << "Position: (" << x << ',' << y << ")\n";
// Farb-Info ist eigene Eigenschaft
os << "Farbe
: 0x" << std::hex << colString.color << std::dec << endl;
return os;
}
// Hauptprogramm
// =============
int main()
{
// ColorString Objekt definieren
ColorString myString(10,20,"ColorString",0xcc00cc);
// und ausgeben
cout << myString << endl;
// ColorString mit neuem Text und neuer
// Position versehen
myString.ChangeText("ColorString modifiziert");
myString.MoveIt(200,200);
// und erneut ausgeben
cout << myString << endl;
}
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (5 von 6) [17.05.2005 11:40:24]
Mehrfach abgeleitete Klassen
Und die Übung entfällt an dieser Stelle.
So, und wieder eine Lerneinheit geschafft.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le14/le14_02.htm (6 von 6) [17.05.2005 11:40:24]
Funktions-Templates
Funktions-Templates
Die Themen:
Einleitung
Definition eines Funktions-Templates
Aufruf von Funktions-Templates
Spezialisierung und Überschreiben von Funktions-Templates
Lokale Daten mit formalen Datentyp
Mehrere Template-Parameter
Beispiel und Übung
Einleitung
In dieser und der nächsten Lektion werden wir uns mit Templates befassen. C++ kennt zwei Arten von
Templates: Funktions-Templates und Klassen-Templates. Sie können sich unter einem Template eine Art
Vorlage oder Vorschrift vorstellen die dem Compiler mitteilt, wie eine Funktion oder Klasse generiert werden
soll. In dieser Lektion werden wir uns zunächst einmal den einfacheren Fall ansehen, die FunktionsTemplates.
Funktions-Templates sind Vorlagen für gleichartige Funktionen, die sich in einem oder mehreren der
folgenden Punkte unterscheiden:
●
●
●
dem Returntyp der Funktion
den Datentypen der Parameter
den Datentypen von lokalen Variablen
Entscheidend bei dieser Aufzählung ist aber das, was nicht dort steht, nämlich was die aus einem FunktionsTemplates erzeugten Funktionen gemeinsam haben müssen. So müssen alle Funktionen die aus einem
Template generiert werden z.B. die gleiche Anzahl von Parametern besitzen, was sie von überladenen
Funktionen unterscheidet. Und außerdem besitzen alle Funktionen auch die gleichen Anweisungen!
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (1 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Vielleicht fragen Sie sich nun, was kann
ich denn mit diesem Funktions-Templates
anfangen wenn der Code sowieso immer
der gleiche ist? Sehen Sie sich dazu einmal
die drei Funktionen rechts an. Dort
werden drei Funktionen Max(...) definiert
die alle das gleiche tun: sie liefern von
zwei als Parametern übergebenen Werten
den größten Wert zurück. Der
Unterschied liegt hier einzig und allein in
den Datentypen der Parameter und damit
sind die Funktionen ein geradezu
klassisches Beispiel für ein FunktionsTemplate.
short Max(short p1, short p2)
{
return ((p1>p2)? p1 : p2);
}
long Max(long p1, long p2)
{
return ((p1>p2)? p1 : p2);
}
float Max(float p1, float p2)
{
return ((p1>p2)? p1 : p2);
}
Wenn Sie den Kurs bis hierher komplett
durchgearbeitet haben, so sollten Sie eine
solche Template-Funktion ja schon aus
der Standard-Bibliothek her kennen.
Mit Ihrem bisherigen Wissen könnten Sie hier vielleicht auf die Idee kommen, für eine solche Funktion
ein #define Makro einzusetzen (siehe ). Dies wäre hier prinzipiell auch möglich, doch wird damit die
Typüberprüfung der Parameter durch den Compiler umgangen. Wenn Sie anstelle einer Funktion
Max(...) ein entsprechendes Makro einsetzen, so können hierbei die beiden Parameter unterschiedliche
Datentypen besitzen ohne das es zu einer Fehlermeldung kommt.
Definition eines Funktions-Templates
Wie ein Funktions-Template prinzipiell definiert wird, soll jetzt anhand der vorhin aufgeführten Funktion
Max(...) demonstriert werden.
Ersetzen der verschiedenen Datentypen
Im ersten Schritt werden alle
Funktionen bis auf eine entfernt und die T Max(T p1, T p2)
{
Datentypen, die von Funktion zu
return ((p1>p2)? p1 : p2);
Funktion unterschiedlich sind, durch
}
einen beliebigen Namen ersetzt (der
natürlich aber kein Schlüsselwort sein
darf). Dieser beliebige Name wird auch
als formaler Datentyp bezeichnet. Im
Beispiel rechts wurden die Datentypen
durch den Namen (Buchstaben) T ersetzt.
T ist eine allgemein übliche Bezeichnung
für einen Template-Datentyp.
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (2 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Spezifikation des formalen Datentyps
Im zweiten Schritt müssen wir dem
Compiler nun etwas unter die Arme
greifen. Damit er weiß, dass im Beispiel T
nur ein Platzhalter für einen später noch
festzulegenden Datentyp ist, wird vor die
Funktion noch die Anweisung
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
template <typename T>
gesetzt. Wohlgemerkt, der Name des
formalen Datentyps ist (fast) beliebig.
Und fertig ist die Definition eines Funktions-Templates. Beachten Sie bitte, dass es sich hier nur um eine
'unvollständige' Definition handelt, d.h. der Compiler erzeugt noch keinen Code da er den tatsächlichen
Datentyp des formalen Datentyps natürlich zu diesem Zeitpunkt noch nicht kennt.
In vielen Programmen werden Sie Funktions-Templates noch wie folgt definiert finden:
template <class T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Hier wird anstelle des Schlüsselworts typename noch class verwendet. typename ist eines der jüngeren
C++ Schlüsselwörter. Beide Template-Definitionen sind aber gleichwertig.
Aufruf von Funktions-Templates
Ist das Funktions-Template definiert,
können Sie die damit prinzipiell
festgelegte Funktion wie jede normale
Funktion aufrufen (siehe Beispiel rechts).
Trifft der Compiler beim Übersetzen auf
den Aufruf einer Funktion, so führt er
folgende Schritte durch:
●
●
Templatedefinition
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Aufrufe der Funktionen
shortMax = Max(shortV1, shortV2);
Zuerst wird abgeprüft, ob es
longMax = Max(longV1, longV2);
bereits eine Funktion gibt die exakt floatMax = Max(floatV1, floatV2);
zu den angegebenen Datentypen
beim Funktionsaufruf passt. Ist
Compiler-generierte Funktionen
dies der Fall, wird diese Funktion short Max(short p1, short p2)
aufgerufen.
{....}
Gibt es keine entsprechende
long Max(long p1, long p2)
Funktion, so wird nach einem
{....}
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (3 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Funktions-Template gesucht. Gibt float Max(float p1, float p2)
es ein solches, so wird der formale {....}
Datentyp (im Beispiel T) durch den
tatsächlichen Datentyp der
Parameter beim Funktionsaufruf
ersetzt und eine entsprechende
Funktion automatisch durch den
Compiler generiert. Im Beispiel
werden also drei Funktionen durch
den Compiler erstellt, wobei der
formale Datentyp T nacheinander
durch die Datentypen short, long
und float ersetzt wird.
Arbeiten Sie mit getrennten Dateien für Deklarationen der Funktionen (Header-Dateien) und deren
Definitionen (Quellcode-Dateien), so müssen Sie das Funktions-Template immer mit in die HeaderDatei aufnehmen! Die endgültige Funktion wird ja erst beim Aufruf einer durch das FunktionsTemplate vorgegebenen Funktion erstellt, und dazu benötigt der Compiler den Code der Funktion.
Doch Achtung! Bei der
Auflösung eines Templates wird
niemals eine automatische
Typkonvertierung
vorgenommen. Im Beispiel
rechts wird versucht die
Funktion Max(...) mit einem shortund einem char-Parameter
aufzurufen. Damit müsste der
Compiler die in der Mitte
angegebene Funktion generieren.
Da aber der formale Datentyp T
nur für einen bestimmten
Datentyp stehen kann, meldet
der Compiler hier einen Fehler!
Wollen Sie trotzdem diesen
Vergleich durchführen (ohne ein
weiteres Funktions-Template zu
spezifizieren), so müssen Sie
beim Aufruf der Funktion eine
entsprechende Typkonvertierung
vornehmen.
Templatedefinition
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Aufruf der Funktion
char charVal, result;
short shortVal;
....
result = Max(shortVal, charVal);
Vom Compiler zu generierende Funktion
char Max(short p1, char p2)
{
return ((p1>p2)? p1 : p2);
}
Spezialisierung und Überschreiben von FunktionsTemplates
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (4 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Soweit, so gut. Doch was passiert nun,
wenn Sie die Funktion Max(...) mit zwei CStrings (char-Zeigern) aufrufen um die
Strings miteinander zu vergleichen? Da
der Compiler beim Aufruf der Funktion
die formalen Datentypen durch die
tatsächlichen ersetzt, generiert er Ihnen
die rechts dargestellte Funktion. Doch
diese vergleicht nicht die Strings sondern
nur deren Adressen im Speicher! Was
also tun?
Fehlerhafter Einsatz eines Funktions-Templates
Templatedefinition
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Aufruf der Funktion
char *pName1, *pName2, *pMax;
.....
pMax = Max(pName1, pName2);
Compiler-generierte Funktion
char* Max(char* p1, char* p2)
{
return ((p1>p2)? p1 : p2);
}
Zum einen können Templates für
bestimmte Datentypen spezialisiert
werden. Um für einen bestimmten
Datentyp ein spezielles FunktionsTemplate zu erstellen, wird zunächst die
template-Anweisung angegeben, jetzt
jedoch mit einer leeren spitzen Klammer.
Der Datentyp für den dieses FunktionsTemplates verwendet werden soll, wird
dann nach dem Funktionsnamen in
spitzen Klammer angegeben. Ergibt sich
dieser Datentyp quasi von alleine aus den
Parametern der Funktion, so kann die
Angabe des Datentyps auch entfallen.
Damit könnten Sie das FunktionsTemplate rechts auch wie folgt schreiben:
Templatedefinition
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Spezielles Funktions-Template
template<>
char* Max<char*>(char* p1, char* p2)
{
if (strcmp(p1,p2) > 0)
return p1;
else
return p2;
}
template<> char* Max(char* p1, Aufruf des speziellen Funktions-Template
char* p2)
pMax = Max(pName1, pName2);
Eine andere Möglichkeit besteht darin,
explizit eine Funktion vorzugeben. Wie
Sie bereits zuvor erfahren haben, schaut
der Compiler vor der Generierung einer
Funktion aus einem Funktions-Template
immer zuerst nach, ob es schon eine
Funktion mit den entsprechenden
Datentypen der Parameter gibt. Gibt es
eine solche, so ruft er auch diese auf. Für
unseren Fall müssen Sie dazu explizit eine
Funktion Max(...) schreiben die zwei
Parameter vom Typ char-Zeiger besitzt
(und dort natürlich wie angegeben die
Templatedefinition
template <typename T>
T Max(T p1, T p2)
{
return ((p1>p2)? p1 : p2);
}
Spezielles Funktions-Template
char* Max(char* p1, char* p2)
{
if (strcmp(p1,p2) > 0)
return p1;
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (5 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Strings richtig vergleichen).
else
return p2;
}
Aufruf des speziellen Funktions-Template
pMax = Max(pName1, pName2);
Haben Sie ein FunktionsTemplate für char* und const
char* spezialisiert, so wird beim
Aufruf mit einem C-Strings das
const char* Template verwendet
da es am genauesten mit dem
Aufruf übereinstimmt. Hier
entsprechen der BORLAND und
MSVC Compiler leider nicht
dem Standard. Beide rufen
immer die Template-Funktion
für den char* Datentyp auf.
template<>
char* Max<char*>(char* p1, char* p2)
{...}
template<>
const char* Max<const char*>(const char* p1,
const char* p2)
{...}
...
pMax = Max("Xaver","Agathe");
Lokale Daten mit formalen Datentyp
Formale Datentypen können nicht nur als
Parameter bei Funktions-Templates
eingesetzt werden. Benötigen Sie
innerhalb eines Funktions-Templates eine
Variable vom gleichen Datentyp wie einer
der Parameter, so können Sie auch hier
anstelle eines bestimmten Datentyps den
formalen Datentyp angeben. Bei der
Generierung der Funktion durch den
Compiler wird dann dieser formale
Datentyp wieder durch den tatsächlichen
Datentyp ersetzt. Die rechts aufgeführte
Funktion Swap(...) dient zum Vertauschen
von Werten. Hierzu muss zuerst ein Wert
in eine lokale Variable umkopiert werden,
die natürlich den gleichen Datentyp wie
der übergebene Wert besitzen muss.
Funktions-Template
template <typename T>
void Swap (T& p1, T& p2)
{
T temp = p1;
p1= p2;
p2 = temp;
}
Aufruf der Funktion
Swap(var1, var2);
Vom Compiler generierte Funktion
void Swap (short& p1, short& p2)
{
short temp = p1;
p1 = p2;
p2 = temp;
}
Mehrere Template-Parameter
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (6 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Und auch das ist möglich: FunktionsTemplates können auch mehrere formale
Datentypen besitzen. Die Anzahl der
formalen Datentypen ist nicht begrenzt.
Wenn Sie sich das Beispiel rechts einmal
ansehen, werden Sie feststellen, dass die
Parameter beim Aufruf des FunktionsTemplates Func(...) unterschiedliche
Datentypen besitzen. Und damit müssen
Sie auch bei der Spezifikation des
Funktions-Templates zwei formale
Datentypen angeben. Wie dies zu erfolgen
hat, ist rechts dargestellt. Beachten Sie
dabei bitte, dass auch beide formalen
Datentypen innerhalb der spitzen
Klammer der template-Anweisung
angegeben werden müssen.
Außer dass Funktions-Template formale
Datentypen als Parameter besitzen,
können selbstverständlich auch 'normale'
Parameter verwendet werden. Im Beispiel
erhält das Funktions-Template Func(...)
als zweiten Parameter p2 einen int-Wert,
der hier zusätzlich noch einen Defaultwert
besitzt.
Funktions-Template
template <typename T1, typename T2>
void Func(T1 p1, T2 p1)
{
T1 loc1 = p1;
T2 loc2 = p2;
....
}
Aufruf der Funktion
float fVar;
char *pChar;
....
Func (fVar, pChar);
Vom Compiler generierte Funktion
void Func(float p1, char* p2)
{
float loc1 = p1;
char* loc2 = p2;
....
}
template <typename T>
void Func(T p1, int p2=10)
{
....
}
Und damit beenden wir die Einführung der Funktions-Templates und kommen jetzt zum Beispiel und der
Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel zeigt die Anwendung von Funktions-Templates zum Sortieren von Feldern.
Das Funktions-Template Sort(...) zum Sortieren erhält als Parameter einen Zeiger auf den Beginn des zu
sortierenden Feldes sowie die Anzahl der Feldelemente. Innerhalb von Sort(...) wird ein weiteres FunktionsTemplate Swap(...) aufgerufen, das zwei beliebige Datenelemente vertauscht.
Im Hauptprogramm wird dann ein long- und ein double-Feld definiert und mit beliebigen Werten gefüllt.
Sehen Sie sich in diesem Zusammenhang auch noch einmal an, wie die Anzahl der Feldelemente berechnet
wird. Da die Feldgrößen durch die Anzahl der Elemente bei der Felddefinition festgelegt wird, muss hier die
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (7 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Anzahl der Elemente explizit berechnet werden.
Beide Felder werden dann zur Kontrolle zunächst im unsortierten Zustand ausgegeben, dann durch Aufruf
des Funktions-Template Sort(...) sortiert und zum Schluss im sortierten Zustand nochmals ausgegeben.
Das unten stehende Beispiel finden Sie auch unter: le15\prog\Bftempl.
Die Programmausgabe:
Unsortierte long:
1, -10, -2, 20,
Sortierte long:
-10, -2, 1, 20,
Unsortiert double:
1.1, 0.9, -1.2, 5.5,
Sortierte double:
-1.2, 0.9, 1.1, 5.5,
Das Programm:
// C++ Kurs
// Beispiel zu Funktionstemplates
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
// Funktionstemplate zum Tauschen von Werten beliebigen Datentyps
// ==============================================================
template <typename T>
void Swap(T& val1, T& val2)
{
T
temp(val1);
// temp mit val1 initialisieren!
val1 = val2;
// Damit koennten Sie sogar Objekt tauschen
val2 = temp;
// wenn diese den Operator = definieren
}
// Funktionstemplate zum Sortieren von
// beliebigen Datentypen innerhalb eines Feldes
// ===========================================
// pValues ist der Zeiger auf den Beginn des Datenfeldes
// und nNoOfValues enthaelt die Anzahl der Daten
template <typename T>
void Sort(T pValues, int const noOfValues)
{
bool changed;
// Tauschflag
// Tauschschleife
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (8 von 13) [17.05.2005 11:40:28]
Funktions-Templates
do
{
// Tauschflag loeschen
changed = false;
// Alle Elemente vergleichen
for (int index=0; index<noOfValues-1; index++)
{
// Falls getauscht werden muss
if (pValues[index]>pValues[index+1])
{
// Werte tauschen
Swap(pValues[index],pValues[index+1]);
// Tauschflag setzen
changed = true;
}
}
} while (changed);
// Schleife so lange durchlaufen, bis nicht mehr getauscht wurde
}
//
// HAUPTPROGRAMM
// =============
int main()
{
int index;
// Felder mit den zu sortierenden Daten
long
longArray[] = {1,-10,-2,20};
double doubleArray[] = {1.1f, 0.9f, -1.2f, 5.5f};
// Anzahl der Elemente in den Feldern berechnen!
const int
noOfLongs = sizeof(longArray)/sizeof(longArray[0]);
const int
noOfDouble = sizeof(doubleArray)/sizeof(doubleArray[0]);
// unsortiertes long-Feld ausgeben
cout << "Unsortierte long:\n";
for (index=0; index<noOfLongs; index++)
cout << longArray[index] << ", ";
cout << endl;
// -> erzeugt Funktion: void Sort(long*, int);
Sort(longArray,noOfLongs);
// sortiertes long-Feld ausgeben
cout << "Sortierte long:\n";
for (index=0; index<noOfLongs; index++)
cout << longArray[index] << ", ";
cout << endl << endl;
// unsortiertes double Feld ausgeben
cout << "Unsortiert double:\n";
for (index=0; index<noOfDoubl; index++)
cout << doubleArray[index] << ", ";
cout << endl;
// -> erzeugt Funktion: void Sort(float*, int);
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (9 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Sort(doubleArray,noOfDouble);
// sortiertes double-Feld ausgeben
cout << "Sortierte double:\n";
for (index=0; index<noOfDouble; index++)
cout << doubleArray[index] << ", ";
cout << endl << endl;
}
Die Übung:
Mit Hilfe der im Beispiel aufgeführten Funktions-Templates Swap(...) und Sort(...) soll nun eine
Adressenliste alphabethisch sortiert werden.
Die Adressdaten Name und Wohnort sind als string-Objekte innerhalb der Adresse abgelegt.
Die Adressenliste selbst wird durch ein Objektfeld vom Typ Address implementiert und ist im
Ausgangslisting ebenfalls bereits vorgegeben. Für die Ausgabe der Adressdaten wird der überladene
Operator << verwendet.
Im Hauptprogramm wird eine Adressenliste für vier Einträge dynamisch erstellt und mit Adressdaten
belegt. Zur Kontrolle werden die Adressdaten ausgeben.
Ihre Aufgabe ist es nun, diese Adressenliste mit Hilfe der beiden Funktions-Templates Sort(...) und Swap(...)
alphabethisch nach dem Namen zu sortieren.
So einfach diese Übung am Anfang auch scheinen mag, hier steckt die Schwierigkeit im Detail. Damit Sie
sich nicht all zu sehr verirren, hier ein paar Hinweise zur Lösung:
●
●
●
An den beiden Funktions-Templates sind keinerlei Änderungen notwendig.
Die Klasse Address benötigt zwei zusätzliche überladene Operatoren. Welche das sind, das sollen Sie
selbst herausfinden. Als kleiner Tipp: Sehen Sie sich die Funktions-Templates einmal genauer an,
welche Operatoren dort verwendet werden.
Zusätzlich müssen Sie der Klasse Address noch einen weiteren, ganz bestimmten Konstruktor
hinzufügen. Dieser wird vom Funktions-Template Swap(...) benötigt.
So und nun viel Spaß bei der Lösung der Aufgabe. Aber nicht gleich aufgeben wenn es innerhalb der ersten
15 Minuten nicht gleich funktionieren sollte.
Das Ausgangslisting für die Übung finden Sie auch der CD-ROM unter le15\prog\LftemplX.
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (10 von 13) [17.05.2005 11:40:28]
Funktions-Templates
Die Programmausgabe:
Unsortierte Adressen:
Name: Karl Maier Ort: AStadt
Name: Agathe Mueller Ort: XDorf
Name: Xaver Lehmann Ort: CHausen
Name: Berta Schmitt Ort: FStadt
Sortierte Adressen:
Name: Agathe Mueller Ort: XDorf
Name: Berta Schmitt Ort: FStadt
Name: Karl Maier Ort: AStadt
Name: Xaver Lehmann Ort: CHausen
Das Programm:
// C++ Kurs
// Ausgangslisten zur Uebung zu Funktionstemplates
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
// Funktionstemplate zum Tauschen von Werten beliebigen Datentyps
// ==============================================================
template <typename T>
void Swap(T& val1, T& val2)
{
T temp(val1);
val1 = val2;
val2 = temp;
}
// Funktionstemplate zum Sortieren von Zahlen
// beliebigen Datentyps innerhalb eines Feldes
// ===========================================
// pValues ist der Zeiger auf den Beginn des Datenfeldes
// und noOfValues enthaelt die Anzahl der Daten
template <typename T>
void Sort(T pValues, int noOfValues)
{
bool changed;
// Tauschflag
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (11 von 13) [17.05.2005 11:40:28]
Funktions-Templates
// Tauschschleife
do
{
// Tauschflag loeschen
changed = 0;
// Alle Elemente vergleichen
for (int index=0; index<noOfValues-1; index++)
{
// Falls getauscht werden muss
if (pValues[index]>pValues[index+1])
{
// Werte tauschen
Swap(pValues[index],pValues[index+1]);
// Tauschflag setzen
changed = 1;
}
}
} while (changed);
// Schleife so lange durchlaufen, bis nicht mehr getauscht wurde
}
// Definition der Klasse fuer die Adressdaten
class Address
{
std::string name;
// Name
std::string location;
// Ort
public:
Address()
// ctor, hat nichts zu tun
{}
void SetData(const char* const pN, const char* const pL);
friend std::ostream& operator << (std::ostream& os, const Address& obj2);
};
// Definition der Memberfunktionen
// Setzen der Objektdaten
void Address::SetData(const char* const pN, const char* const pL)
{
name = pN;
location = pL;
}
// Ueberladener Operator << fuer Ausgabe
std::ostream& operator << (std::ostream& os, const Address& obj2)
{
os << "Name: " << obj2.name;
os << " Ort: " << obj2.location << endl;
return os;
}
//
// HAUPTPROGRAMM
// =============
int main()
{
int
index;
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (12 von 13) [17.05.2005 11:40:28]
Funktions-Templates
// Objektfeld fuer Adressdaten anlegen
const int SIZE = 4;
Address *pAddress = new Address[SIZE];
// Objektfeld mit Daten belegen
pAddress[0].SetData("Karl Maier","AStadt");
pAddress[1].SetData("Agathe Mueller","XDorf");
pAddress[2].SetData("Xaver Lehmann","CHausen");
pAddress[3].SetData("Berta Schmitt","FStadt");
// unsortiertes Objektfeld ausgeben
cout << "Unsortierte Adressen:\n";
for (index=0; index<SIZE; index++)
cout << pAddress[index] << endl;
// Hier fuer die Uebung die Adressen sortieren und
// erneut ausgeben
delete [] pAddress;
}
Die Erweiterung sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so
finden Sie die Lösung unter loesungen\le15\Lftempl.
In der nächsten Lektion erfahren Sie dann wie Sie Klassen-Templates definieren.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le15/le15_01.htm (13 von 13) [17.05.2005 11:40:28]
Klassen-Templates
Klassen-Templates
Die Themen:
Einleitung
Definition des Klassen-Templates
Formaler Datentypen im Kopf von Memberfunktionen
Definition von Memberfunktionen
Überschreiben von Template-Memberfunktionen
Definition von Objekten
Mehrere formale Datentypen
Non-type Parameter
Default-Datentyp
Beispiel und Übung
Einleitung
In der letzten Lektion haben Sie etwas über Funktions-Templates erfahren. Aber Templates sind
nicht nur auf Funktionen beschränkt, sondern können auch für Klassen eingesetzt werden. In dieser
Lektion erfahren Sie, wie Sie solche Klassen-Templates definieren und anwenden.
Dazu gleich wieder ein Beispiel.
Rechts werden zwei Klassen
ShortStack und WinStack
definiert, die beide das
Verhalten eines Stacks
implementieren. Der einzige
Unterschied zwischen den
beiden Klassen liegt nur im
Datentyp des Zeigers auf die
Stackdaten. Im ersten Fall ist
dies ein short-Zeiger und im
zweiten Fall ein Objektzeiger
// Stack für short-Werte
class ShortStack
{
short *pData;
....
public:
ShortStack(int size)
{
pData = new short[size];
....
}
};
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (1 von 19) [17.05.2005 11:40:34]
Klassen-Templates
der Klasse Win. Im Konstruktor
der Klasse wird nun ein Feld
// Stack für Zeiger auf Win-Objekte
vom Datentyp der Stackdaten class WinStack
angelegt.
{
Win* *pData;
Da aber die Memberfunktionen
....
zum Ablegen von Daten auf
public:
dem Stack (Memberfunktion
WinStack(int size)
Push(...)) und zum Auslesen der
{
Daten (Memberfunktion
pData = new Win*[size];
Pop(...)) in ihrer Funktionalität
....
gleich sein werden, bietet es sich
}
hierfür an, ein Klassen};
Template zu entwickeln. Im
Folgenden werden wir deshalb
eine allgemein gültige Klasse
für einen Stack entwickeln.
Wenn Sie bis hierher
den Kurs
durchgearbeitet haben,
so sollten wissen, dass
die STL bereits eine
solche Klasse enthält.
Die hier entwickelte
Stack-Klasse dient
daher nur zur Übung.
Definition des Klassen-Templates
Auch hier verläuft die Entwicklung eines Klassen-Templates zunächst in den gleichen zwei
Schritten wie bei der Entwicklung eines Funktions-Templates.
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (2 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Ersetzen der verschiedenen Datentypen
Im ersten Schritt werden
alle Klassendefinitionen bis auf
eine entfernt und die
Datentypen, die von Klasse zu
Klasse unterschiedlich sind,
durch einen beliebigen Namen
ersetzt (der natürlich aber kein
Schlüsselwort sein darf). Dieser
beliebige Name wird, wie Sie in
der Zwischenzeit wissen, auch
als formaler Datentyp
bezeichnet. Im Beispiel rechts
wurden die Datentypen wieder
durch den Namen (Buchstaben)
T ersetzt. Fast von selbst
versteht es sich, dass dann auch
in den jeweiligen
Memberfunktionen die
unterschiedlichen Datentypen
durch den formalen Datentyp
ersetzt werden müssen. So
wurde beim Aufruf des new
Operators im Konstruktor
ebenfalls der formale Datentyp
T eingesetzt.
// Allgemeine Stackklasse
class Stack
{
T *pData;
....
public:
Stack(int size)
{
pData = new T[size];
....
}
};
Spezifikation des formalen Datentyps
Im zweiten
template <typename T>
Schritt müssen wir auch hier
class Stack
wieder dem Compiler weiter
helfen. Damit er weiß, dass im {
T *pData;
Beispiel T nur ein Platzhalter
....
für einen später noch
public:
festzulegenden Datentyp ist
CStack(int size)
(formaler Datentyp), wird vor
{
die Klassendefinition wieder die
pData = new T[size];
Anweisung
....
}
template <typename T> };
gesetzt. Auch hier kann wie bei
den Funktions-Templates das
Schlüsselwort typename durch
das Schlüsselwort class ersetzt
werden; bei älteren
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (3 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Programmen werden Sie
ausschließlich class bei der
Templatedefinition vorfinden.
Und damit ist die Definition des
Klassen-Templates im Prinzip
auch schon vollständig.
Formaler Datentypen im Kopf von
Memberfunktionen
Der formale Datentyp kann
natürlich, wie bei FunktionsTemplates, auch als Parameter
oder als Returntyp von
Memberfunktionen auftreten.
So erhalten die
Memberfunktionen Push(...)
und Pop(...) eine Referenz auf
den abzulegenden bzw.
auszulesenden Wert.
template <typename T>
class Stack
{
T *pData;
....
public:
CStack(int size)
{
pData = new T[size];
....
}
bool Push(const T& val);
bool Pop(T& val);
};
Beachten Sie besonders im Zusammenhang mit Klassen-Templates bei den Parametern
von Memberfunktionen, dass Sie entweder mit Zeigern oder Referenzen arbeiten sollten.
Vermeiden nach Möglichkeit die direkte Übergabe eines Werts. Bei der Übergabe eines
Objekts werden sonst unter Umständen relativ viel Daten auf den Stack kopiert.
Definition von Memberfunktionen
Womit wir schon beim nächsten Thema sind, der Deklaration von Memberfunktionen die formale
Parameter erhalten oder einen formalen Datentyp als Returntyp besitzen. Und hierbei sind die
beiden Fälle zu unterscheiden, ob eine Memberfunktion innerhalb oder außerhalb der Klasse
definiert wird.
Beachten Sie bitte, dass die Memberfunktionen bis zu diesem Zeitpunkt nur 'vorläufig'
definiert werden da sie ja noch formale Datentypen besitzen.
Definition von Memberfunktionen innerhalb der Klasse
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (4 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Werden Memberfunktionen
template <typename T>
innerhalb der Klasse definiert, class Stack
so kann die Definition der
{
Memberfunktion wie gewohnt
T *pData;
erfolgen. Wie bereits erwähnt
....
sind dann nur die
public:
veränderlichen Datentypen
Stack(int size)
durch den formalen Datentyp
{
zu ersetzen. Aber denken Sie
....
auch daran, dass in der Regel
}
innerhalb der Klasse definierte
bool Push(const T& val)
Memberfunktionen als inline{
Memberfunktionen betrachtet
pData[sIndex++] = val;
werden. Und dies kann unter
....
Umständen den Code Ihres
}
Programms beträchtlich
bool Pop(T& val)
vergrößern!
{
val = pData[--sIndex];
Und noch ein Hinweis: Soll die
....
rechts dargestellte Klasse Stack
}
auch Objekte verarbeiten
};
können, so sollten Sie für die
abzulegenden Klassen auch den
Zuweisungsoperator '='
definieren, da in den
Memberfunktion Push(...) und
Pop(...) Objektzuweisung statt
finden!
Definition von Memberfunktionen außerhalb der Klasse
Werden die Memberfunktionen
außerhalb der Klasse definiert,
so ist eine auf den ersten Blick
etwas verwirrende Definition
erforderlich. Die allgemeine
Syntax zur Definition einer
Memberfunktion eines KlassenTemplates außerhalb der
Klasse lautet:
template <typename T>
bool Stack<T>::Push(const T& val)
{....}
template <typename T>
bool Stack<T>::Pop(T& val)
{....}
template <typename T>
RTYP
CLASS<T>::MNAME(....)
T ist wieder der formale Datentyp, RTYP der Returntyp der Memberfunktion und CLASS der Name
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (5 von 19) [17.05.2005 11:40:34]
Klassen-Templates
des Klassen-Templates. MNAME ist schließlich noch der Name der Memberfunktion. Das obige
Bespiel zeigt die Definitionen der Memberfunktionen Push(...) und Pop(...) der vorherigen Klasse
Stack. Beachten Sie, dass zwischen dem Returntyp der Memberfunktion und dem Namen der
Memberfunktion der Klassenname steht, gefolgt vom formalen Datentyp in spitzen Klammern.
Überschreiben von Template-Memberfunktionen
Steigern wir das Ganze nun
langsam. Wie bei FunktionsTemplates können Sie auch für
bestimmte Datentypen die
Memberfunktionen eines
Klassen-Templates
überschreiben. Wie dies geht ist
rechts dargestellt. Dort werden
die Memberfunktionen Push(...)
und Pop(...) des KlassenTemplates Stack überschrieben
um auf dem Stack char-Zeiger
(wie z.B. für C-Strings)
abzulegen. Soll ein C-String
mittels Push(...) auf dem Stack
abgelegt werden, so wird eine
Kopie des C-Strings erzeugt
und letztendlich der Zeiger auf
diese Kopie auf dem Stack
abgelegt. Beim Auslesen des CStrings mittels Pop(...) erhält
die Anwendung dann den
Zeiger auf diese Kopie zurück.
Die Anwendung ist nun auch
dafür verantwortlich, dass der
Speicherplatz für den
zurückgelieferten String
irgendwann freigegeben wird.
Ein solches Verhalten müssen
Sie natürlich gut
dokumentieren damit keine
"Speicherlöcher" entstehen. So
darf z.B. an die
Memberfunktion Pop(...) nur
ein char-Zeiger übergeben
werden aber auf keinen Fall ein
Zeiger auf char-Feld. Dieser
Zeiger würde von Pop(...)
template<> // Zur Ablage von C-Strings
bool Stack<char*>::Push(char* &ptr)
{
pData[sIndex] = new char[strlen(ptr)+1];
strcpy(pData[sIndex],ptr);
sIndex++;
....
}
template<>
bool Stack<char*>::Pop(char* &ptr)
{
sIndex--;
ptr = pData[sIndex];
....
}
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (6 von 19) [17.05.2005 11:40:34]
Klassen-Templates
überschrieben werden.
Beachten Sie bitte die Datentypen der Parameter! Da Push(...) und Pop(...) laut
Deklaration innerhalb des Klassen-Templates Referenzen erwarten, müssen Sie an diese
Memberfunktionen Referenzen auf char-Zeiger übergeben. Ferner müssen Sie bei einer
solchen Lösung auch beachten, dass eventuell nicht alle Werte vom Stack geholt werden.
Sie müssen zusätzlich noch einen eigenen Destruktor Stack<char*>::~Stack()
erstellen um den Speicher für die nicht abgeholten C-Strings freizugeben.
Definition von Objekten
Wenn Sie aus der Einführung der Standard-Bibliothek noch wissen, wie Objekte von
Klassen-Templates definiert werden und dass ein Template auch mehrere formale
Datentypen besitzen kann, dann können Sie gleich zu den non-type Parameter übergehen
( ).
Nach dem Sie gesehen haben
wie Klassen-Templates definiert
werden, wollen wir jetzt an die
Definitionen von Objekten von
Klassen-Templates gehen.
Rechts sehen Sie 'zur
Auffrischung' die Definition der
Klasse Stack, die zum Ablegen
von short-Werten dient. Der
Konstruktor der Klasse erhält
als Parameter die Anzahl der
maximal auf dem Stack
abzulegenden Werte. Darunter
wird dann das Stack Objekt
myStack definiert, das maximal
10 short-Werte abspeichern
kann.
Sehen wir uns jetzt die
Definition eines Objekts eines
Klassen-Templates an. Da bei
der Definition des KlassenTemplates nur der formale
Datentyp spezifiziert wurde,
muss nun bei der Definition des
Objekts der tatsächlich zu
verwendende Datentyp
angegeben werden. Dieser wird
nach dem Klassennamen in
spitzen Klammern angegeben.
Im Beispiel rechts wird zuerst
Bisherige Objektdefinition:
// Klassendefinition
class Stack
{
short *pnData
public:
Stack(int size);
....
};
// Objektdefinition
Stack myStack(10);
Definition eines Objekts eines Klassen-Templates:
// Template-Definition
template <typename T> class Stack
{
T *pData;
public:
CStack(int size);
....
};
// Objektdefinition + Template-Instanziierung
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (7 von 19) [17.05.2005 11:40:34]
Klassen-Templates
ein Stack zur Aufnahme von 10 Stack<long> longStack(10);
Stack<char*> charStack(50);
long-Werten definiert und
danach ein Stack zur Aufnahme
von 50 char-Zeigern. Im
Beispiel zu dieser Lektion
finden Sie dann die vollständige
Implementation des KlassenTemplates Stack.
Erst bei der Definition des Objekts eines Klassen-Templates wird durch den Compiler auch
eine entsprechende Klasse erstellt, da er erst zu diesem Zeitpunkt den formalen Datentyp
durch den tatsächlichen Datentyp ersetzen kann.
Mehrere formale Datentypen
Machen wir jetzt den nächsten
Schritt. Genauso wie FunktionsTemplates nicht nur einen
formalen Datentyp besitzen
können, können auch KlassenTemplates mehrere formale
Datentypen haben. Die
formalen Datentypen werden
dann bei der Definition des
Klassen-Templates einfach
wieder aufgelistet. Beachten Sie
dabei bitte, dass das
Schlüsselwort typename (oder
class) vor jedem formalen
Datentyp anzugeben ist. Im
Beispiel rechts enthält das
Klassen-Template MyClass die
beiden formalen Datentypen T1
und T2. Und selbstverständlich
müssen dann bei der Definition
eines Objekts des KlassenTemplates auch entsprechend
viele Datentypen angeben. Auch
diese werden in spitzen
Klammern einfach aufgelistet.
// Template-Definition
template <typename T1, typename T2>
class MyClass
{....};
// Objektdefinition
MyClass<int, char*> myObj1;
MyClass<float, double> myObj2;
Non-type Parameter
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (8 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Erweitern wir das KlassenTemplate ein vorletztes Mal.
Bisher enthielt das KlassenTemplate als TemplateParameter nur formale
Datentypen. Klassen-Templates
können aber auch so genannte
non-type Parameter erhalten.
Sie können sich unter einem
non-type Parameter eine Art
Konstante vorstellen, die Sie
dem Klassen-Template mit auf
den Weg geben. Im Beispiel
rechts sehen Sie einen Auszug
aus einer erweiterten Version
der Klasse SArray die ein
SafeArray implementiert. Das
Safe-Array können Sie sich hier
nochmals ansehen ( ). Das
Klassen-Template erhält als
zweiten Parameter die Größe
des anzulegenden Feldes.
Innerhalb der Klasse selbst
wird dieser zweite Parameter
wie eine Konstante behandelt,
d.h. Sie können den Wert dieses
Parameters nicht mehr
verändern (siehe Definition des
Datenfeldes in der TemplateDefinition). Für non-type
Parameter sind nur die
folgenden Datentypen
zugelassen: ganzzahlige
Konstante/Literal, ein non-type
Template-Parameter, eine
Funktion, ein Objekt, die
Adresse einer Funktion oder
eines Objekts oder ein Zeiger
auf ein Member. D.h. Sie
können keine Gleitkommazahl
oder gar einen char-Zeiger als
non-type Parameter verwenden.
template <typename T, int SIZE>
class SArray
{
T pData[SIZE]; // Datenfeld
public:
T& operator[] (int index);
};
// Ueberladener Indexoperator
template <typename T, int SIZE>
T& SArray<T, SIZE>::operator [](int index)
{
....
// Falls Index ueber das Feld hinausgreift
if (index>=SIZE)
{
.....
}
....
}
// Objektdefintion
SArray<short,10> myArray;
Sehen Sie sich auch die Definition des überladenen Indexoperators [ ] an. Auch hier muss nun in der
Template-Anweisung der non-type Parameter mit angegeben werden. Innerhalb der OperatorMemberfunktion wird dieser non-type Parameter für die Abprüfung auf die obere Feldgrenze
verwendet.
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (9 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Ebenfalls ändert sich auch die Definition eines Objektes des Klassen-Templates. Sie müssen Sie hier
jetzt auch den non-type Parameter mit angeben.
Sie können für diesen non-type Parameter auch einen Defaultwert vorgeben. Dann sieht die
Definition des Klassen-Templates wie folgt aus:
template <typename T, int SIZE=5> class SArray
{....};
Damit können Sie dann ein Objekt wie folgt definieren:
SArray<float> myFloatArray;
In diesem Fall wird ein SafeArray für 5 float-Werte erstellt.
Default-Datentyp
Beenden wir (vorläufig) die
Behandlung von KlassenTemplates mit der Einführung
des Default-Datentyps. Dass
Funktionen und
Memberfunktionen Parameter
mit Defaultwerte besitzen
können sollte Ihnen in der
Zwischenzeit geläufig sein. Und
genau das gleiche gilt auch für
Klassen-Templates, nur dass
hier nun nicht ein Defaultwert
vorgegeben wird sondern ein
Default-Datentyp. Dazu wird
nach dem formalen Datentyp
der Zuweisungsoperator
angegeben und dann der
Default-Datentyp. Wird bei der
Definition eines Objekts des
Klassen-Templates dann
innerhalb der spitzen Klammer
kein Datentyp spezifiziert, so
wird der Default-Datentyp für
die zu erstellenden Klasse
verwendet. Im Beispiel wird
zuerst ein Stack für int-Daten
erzeugt und dann ein Stack für
// Definition des Klassen-Templates
template <typename T=int>
class Stack
{
....
};
// Definition von Objekten
Stack<> intStack;
Stack<float> floatStack;
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (10 von 19) [17.05.2005 11:40:34]
Klassen-Templates
float-Daten.
Damit haben Sie nun die Grundlagen von Klassen-Templates kennen gelernt. Nicht verschwiegen
werden soll an dieser Stelle, dass Sie mit Klassen-Templates noch wesentlich mehr anfangen
können. So können innerhalb eines Klassen-Templates weitere Klassen-Templates definiert werden
oder auch Klassen-Templates für bestimmte Datentypen explizit definiert werden. Doch für den
Anfang sollte Ihnen das im Kurs vermittelte Wissen über Templates ausreichen. Mehr zu Templates
erfahren Sie dann noch im nächsten Kapitel.
So, bleibt jetzt nur noch das abschließende Beispiel und die Übung übrig.
Beispiel und Übung
Das Beispiel:
Entwickelt wird ein Klassen-Templates für die Realisierung eines Stacks. Der Stack kann
standardmäßig fünf Werte/Objekte aufnehmen. Zum Ablegen von Werten/Objekten dient wie
üblich die Memberfunktion Push(...) und zum Auslesen die Memberfunktion Pop(...).
Zum Nachweis dass auch Objekte auf dem Stack abgelegt werden können, ist eine einfache Klasse
Window implementiert. Beachten Sie, dass für die Klasse Window der Standard-Konstruktor
definiert sein muss, da Stack ein Objektfeld anlegt. Außerdem ist, wie bei Klassen mit
dynamischen Daten in der Praxis üblich, der Zuweisungsopertor '=' zu überschreiben. Innerhalb
der Memberfunktionen Push(...) und Pop(...) der Stack Klasse werden ja schließlich
Objektzuweisungen durchgeführt.
Im Hauptprogramm wird zunächst ein Stack für die Aufnahme von fünf short-Werten definiert.
Dieser Stack wird dann komplett gefüllt und wieder geleert.
Anschließend wir ein Stack zum Abspeichern von Window-Objekten definiert. Sie können auch
einen Stack für die Aufnahme von Objektzeigern definieren, hätten dabei aber den Nachtteil, dass
dann Veränderungen an Objekten auch auf die auf dem Stack abgelegten Objekte wirken. Der
Stack würde in diesem Fall ja nur die Zeiger auf die Objekte enthalten und nicht die Objekte
selbst!
Das unten stehende Beispiel finden Sie auch unter: le15\prog\Bctempl.
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (11 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Die Programmausgabe:
1 abgelegt.
2 abgelegt.
3 abgelegt.
4 abgelegt.
5 abgelegt.
6 konnte nicht abgelegt werden!
5 wieder geholt.
4 wieder geholt.
3 wieder geholt.
2 wieder geholt.
1 wieder geholt.
short-Stack nun leer!
Fuelle CWin-Stack
Fenster 1 abgelegt
Fenster 2 abgelegt
Fenster 3 abgelegt
Hole CWin-Daten vom Stack
Fenster 3
Fenster 2
Fenster 1
Das Programm:
// C++ Kurs
// Beispiel zu Klassentemplates
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
// Definition der Template-Klasse Stack
// =====================================
// Defaultmaessig wird Stack fuer 5 Eintraege erstellt
template <typename T, int SIZE=5>
class Stack
{
private:
short stackPtr;
// Stackindex (Stackpointer)
T
data[SIZE];
// Zeiger auf Stackobjekte
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (12 von 19) [17.05.2005 11:40:34]
Klassen-Templates
public:
Stack()
{
stackPtr = 0;
// Stackindex resetieren
}
bool
Push(const T&);
bool
Pop(T&);
};
// Definition der Memberfunktionen
// Legt Datum auf dem Stack ab
template <typename T, int SIZE> bool Stack<T,SIZE>::Push(const T& nV)
{
// Falls Stack schon voll ist
if (stackPtr==SIZE)
return false;
// Datum ablegen
data[stackPtr++] = nV;
return true;
}
// Liest Datum vom Stack aus
template <typename T, int SIZE> bool Stack<T,SIZE>::Pop(T& nV)
{
// Falls Stack leer
if (stackPtr==0)
return false;
// Datum holen
nV = data[--stackPtr];
return true;
}
// Definition der Demo-Klasse Window
// ================================
// Window dient nur zum Beweis, dass auf dem Stack jetzt auch
// Objekte bzw. Zeiger auf Objekte abgelegt werden können
class Window
{
string title;
public:
Window(const char *const pT): title(pT)
{}
Window()
// Wird fuer Objektfeld in Stack benoetigt!!
{}
Window& operator = (const Window& obj2);
friend std::ostream& operator << (std::ostream&, const Window&);
};
// Definition der Memberfunktionen
// Ueberladener Zuweisungsoperator
Window& Window::operator =(const Window& obj2)
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (13 von 19) [17.05.2005 11:40:34]
Klassen-Templates
{
title = obj2.title;
return *this;
}
// Ueberladener Ausgabeoperator << fuer die Window-Klasse
std::ostream& operator << (std::ostream& os, const Window& obj)
{
os << obj.title;
return os;
}
//
// HAUPTPROGRAMM
// =============
int main()
{
short index;
// Short-Stack definieren für 5 short-Wert (Defaultgroesse)
Stack<short>
shortStack;
// Short-Stack komplett fuellen
for (index=1;;index++)
{
// Falls Wert auf dem Stack abgelegt
if (shortStack.Push(index))
cout << index << " abgelegt.\n";
// sonst Schleife verlassen
else
{
cout << index << " konnte nicht abgelegt werden!\n";
break;
}
}
// Short-Stack wieder auslesen
for (;;)
{
short val;
// Falls Wert ausgelesen wurde
if (shortStack.Pop(val))
cout << val << " wieder geholt.\n";
// sonst Schleife verlassen
else
{
cout << "short-Stack nun leer!\n";
break;
}
}
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (14 von 19) [17.05.2005 11:40:34]
Klassen-Templates
// Stack fuer 10 Window-Objekte definieren
Stack<Window,10>
windowStack;
// Window-Objekte auf Stack ablegen
// ACHTUNG! Hier wird ein temp. Window-Objekt erstellt
// welches dann an Push() uebergeben wird. Push() ruft
// den Zuweisungsoperator von Window auf um das temp.
// Objekt in den Stack zu uebernehmen. Nach der Rueckkehr
// von Push() wird das temp. Objekt wieder zerstoert!
cout << "Fuelle Window-Stack\n";
windowStack.Push(Window("Fenster 1"));
windowStack.Push(Window("Fenster 2"));
windowStack.Push(Window("Fenster 3"));
// Nun Window-Objekte wieder vom Stack holen
cout << "Hole Window-Daten vom Stack\n";
// Window-Objekt erstellen
// Wird von Pop() mit dem akt. Stack-Objekt
// 'ausgefuellt' (Aufruf des Zuweisungsoperators)
Window actWindow;
while (windowStack.Pop(actWindow))
{
// Window ausgeben
cout << actWindow << endl;
}
}
Die Übung:
Entwickeln Sie ein Klassen-Template für ein SafeArray, das standardmäßig zur Ablage von intWerten dient. Ein SafeArray verhält aus der Sicht des Anwenders wie ein normales Feld, d.h. es
speichert eine Anzahl von Werten/Objekten ab auf die indiziert zugegriffen wird. Beim
indizierten Zugriff auf ein Feldelement wird jedoch der Index auf Gültigkeit überprüft. Liegt der
Index außerhalb des erlaubten Bereichs (kleiner 0 oder über der oberen Feldgrenze), so wird eine
Fehlermeldung ausgegeben und das Programm beendet. Dazu überlädt das SafeArray den
Indexoperator [ ]. Dem Konstruktor des SafeArrays wird die benötigte Feldgröße als Parameter
übergeben.
Im Hauptprogramm wird dann, je nach dem ob das Symbol INT_TEST bzw. CLASS_TEST
definiert ist, ein SafeArray für int-Werte bzw. Demo-Objekte angelegt. Das SafeArray wird dann
mit Werten/Objekten belegt. Zur Kontrolle der richtigen Arbeitsweise des SafeArrays werden
mit gültigem Index verschiedene Werte/Objekte abgelegt und wieder ausgelesen. Anschließend
erfolgt dann ein Zugriff mit ungültigem Index, was zur Ausgabe einer Fehlermeldung und zum
Abbruch des Programms führt.
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (15 von 19) [17.05.2005 11:40:34]
Klassen-Templates
Ein Großteil der Übung ist bereits fertig vorgegeben, so auch die Klasse Demo. Beachten Sie bei
dieser Klasse dass sie dynamische Eigenschaften enthält und damit die Regel der großen 3 erfüllen
sollte (oder besser muss), d.h. die Klasse muss einen Destruktor, den Kopierkonstruktor und den
überladenen Zuweisungsoperator '=' besitzen.
Wenn Ihr Klassen-Template richtig arbeitet, müssen Sie nachfolgende Ausgabe erhalten. Um das
SafeArray mit den beiden verschiedenen Datentypen zu testen, definieren Sie eines der Symbole
INT_TEST oder CLASS_TEST am Programmanfang. Sie können nicht beide Datentypen in einem
Durchlauf testen, da bei fehlerhaftem Zugriff auf das SafeArray das Programm beendet werden
soll.
Diese Ausgangslisting für die Übung finden Sie auch der CD-ROM unter le15\prog\LctemplX.
Die Programmausgabe:
Ausgabe SafeArray mit int-Werten
0. Wert: 0
1. Wert: 1
2. Wert: 2
3. Wert: 3
4. Wert: 4
5. Wert: 5
6. Wert: 6
7. Wert: 7
8. Wert: 8
9. Wert: 9
Versuch das Element -1 zu beschreiben!
Index kleiner 0!
Ausgabe
Zugriff
Zugriff
Zugriff
Zugriff
Index 5
SafeArray mit CDemo-Objekten
auf 0 ok!
auf 2 ok!
auf 2 ok!
auf 0 ok!
ueberschreitet obere Grenze 3!
Das Programm:
// C++ Kurs
// Ausgangslisting fuer Uebung zu Klassentemplates
//
// Zuerst Dateien einbinden
#include <iostream>
using std::cout;
using std::endl;
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (16 von 19) [17.05.2005 11:40:34]
Klassen-Templates
// Auswahl des Datentyps fuer SafeArray
#define INT_TEST
//#define CLASS_TEST
//
//
//
//
Definition der Template-Klasse SArray
=====================================
Defaultmaessig wird SafeArray fuer int-Daten erstellt
... Hier jetzt Klassentemplate definieren
// Demo-Klasse
// ===========
class Demo
{
char *pText;
public:
// Standardkonstruktor, wird benoetigt fuer Felddefinition
// in der SafeArray Klasse
Demo()
{
pText = NULL;
}
// Konstruktor fuer Objekterstellung
Demo(const char* const pT)
{
pText=new char[strlen(pT)+1];
strcpy(pText,pT);
}
// Kopierkonstruktor, wird benoetigt wenn aus einem
// SafeArray Element ein neues Objekt erstellt wirde
Demo(const Demo& Orig)
{
pText = new char[strlen(Orig.pText)+1];
strcpy(pText,Orig.pText);
}
// Destruktor
~Demo()
{
delete [] pText;
}
// Ueberladener Zuweisungsoperator, wird benoetigt wenn einem
// SafeArray Element ein Demo Objekt zugewiesen wird
Demo& operator = (const Demo& rhs)
{
if (&rhs == this)
return *this;
delete [] pText;
pText = new char[strlen(rhs.pText)+1];
strcpy(pText,rhs.pText);
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (17 von 19) [17.05.2005 11:40:35]
Klassen-Templates
return *this;
}
// AusgabeMemberfunktion
void Print() const
{
cout << pText << endl;
}
};
// Hauptprogramm
// =============
int main()
{
#ifdef INT_TEST
const int ARRAYSIZE = 10;
int index;
// Feldgroesse
// Schleifenzaehler
// SafeArray mit Standard-Datentyp int anlegen
SArray<> shortArray(ARRAYSIZE);
// SafeArray fuellen
for (index=0; index<ARRAYSIZE; index++)
shortArray[index] = index;
// SafeArray wieder auslesen
for (index=0; index<ARRAYSIZE; index++)
cout << index << ". Wert: " << shortArray[index] << endl;
// Versuch das Element mit dem Index -1 zu lesen
cout << "Versuch das Element -1 zu beschreiben!\n";
shortArray[-1] = 111;
#endif
#ifdef CLASS_TEST
// SafeArray fuer Demo-Objekte erstellen
SArray<Demo> demoArray(3);
// Erstes und letztes Feldelement belegen
// Ruft ueberladenen Zuweisungsoperator von Demo auf!
demoArray[0] = Demo("Zugriff auf 0 ok!");
demoArray[2] = Demo("Zugriff auf 2 ok!");
// Demo-Objekte im SafeArray ausgeben
demoArray[0].Print();
demoArray[2].Print();
// Objekt aus SafeArray kopieren
// Ruft Kopierkonstruktor von Demo auf!
Demo newObject = demoArray[2];
newObject.Print();
// SafeArray Elemente zuweisen
// Ruft ueberladenen Zuweisungsoperator von Demo auf!
demoArray[2] = demoArray[0];
demoArray[2].Print();
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (18 von 19) [17.05.2005 11:40:35]
Klassen-Templates
// Nun ueber obere Grenze hinausgreifen
demoArray[5] = Demo("Fehler!");
#endif
}
Die Erweiterung sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses
besitzen, so finden Sie die Lösung unter loesungen\le15\Lctempl.
In der nächsten Lektion begeben wir uns ins Reich des Virtuellen. Dort erfahren Sie, was es mit den
so wichtigen virtuellen Memberfunktionen auf sich hat.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le15/le15_02.htm (19 von 19) [17.05.2005 11:40:35]
Virtuelle Memberfunktionen
Virtuelle Memberfunktionen
Die Themen:
Basisklassenzeiger
Deklaration von virtuellen Memberfunktionen
Pure virtual Memberfunktionen
Überschreiben von virtuellen Memberfunktionen
Virtueller Destruktor
Beispiel und Übung
Basisklassenzeiger
Bevor wir auf die virtuellen Memberfunktionen eingehen, sehen Sie sich nochmals folgenden Sachverhalt an
der in der Lektion über abgeleitete Klassen schon kurz erwähnt wurde. Er spielt bei virtuellen
Memberfunktionen die entscheidende Rolle.
Ein Zeiger vom Typ der Basisklasse kann auch Zeiger vom Typ einer abgeleiteten Klasse aufnehmen
Dieser Sachverhalt ist rechts nochmals
dargestellt. Dort wird von der Basisklasse
GBase zunächst die Klasse Frame und von
dieser wiederum die Klasse MyFrame
abgeleitet. Im Hauptprogramm wird dann
ein Zeiger auf die Basisklasse GBase
definiert. Diesem Zeiger können dann laut
obiger Aussage sowohl Zeiger auf die
Klasse Frame wie auch MyFrame
zugewiesen werden.
class GBase
{
....
};
class Frame: public GBase
{
....
};
class MyFrame: public Frame
{
....
In den weiteren Beispielen wird der
}
Übersichtlichkeit wegen nur noch eine
int main()
einstufige Ableitung verwendet. Die im
{
Folgenden gemachten Aussagen gelten
GBase *pBase;
aber ebenso für mehrstufige Ableitungen.
pBase = new Frame(...);
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (1 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
pBase = new MyFrame(...);
}
Soweit, so gut. Vielleicht fragen Sie sich
nun, für was das Ganze mit den
Basisklassenzeigern eigentlich gut sein
soll? Lassen Sie uns dazu das vorherige
Beispiel noch etwas erweitern. Fügen wir
der Basisklasse GBase sowie der davon
abgeleiteten Klasse Frame jeweils eine
Memberfunktion Draw(...) hinzu.
class GBase
{
....
public:
void Draw();
};
class Frame: public GBase
{
....
Was passiert aber nun, wenn dem
public:
Basisklassenzeiger ein Objekt der
void Draw();
abgeleiteten Klasse zugewiesen und dann };
über diesen Zeiger die Memberfunktion
int main()
Draw(...) aufgerufen wird? Da der Zeiger {
vom Typ Basisklasse ist, wird auch
GBase *pBase;
Draw(...) der Basisklasse aufgerufen. Aber
pBase = new Frame(...);
eigentlich sollte hier die Memberfunktion
pBase->Draw(); // Draw() von GBase!
Draw(...) der abgeleiteten Klasse
}
aufgerufen werden. Und dies ist auch
möglich! Die Lösung dazu heißt
dynamische Bindung (auch späte
Bindung, dynamic linking oder late
binding genannt) und erfolgt über so
genannte virtuelle Memberfunktionen.
Wie dies genau geht, das ist Thema dieser
Lektion.
Deklaration von virtuellen Memberfunktionen
Um die dynamische Bindung zu
ermöglichen, muss die über einen
Basisklassenzeiger aufzurufende
Memberfunktion 'nur' als virtuelle
Memberfunktion deklariert werden. Dies
erfolgt durch voranstellen des
Schlüsselwortes virtual vor dem Returntyp
der Memberfunktion. Wird dann zur
Programmlaufzeit über einen
Basisklassenzeiger eine Memberfunktion
aufgerufen die sowohl in der Basisklasse
wie auch in abgeleiteten Klasse als virtual
deklariert ist, so wird immer diejenige
Memberfunktion aufgerufen, die zu dem
im Basisklassenzeiger abgelegten Objekt
gehört. Im Beispiel rechts wird nun also
nicht mehr Draw(...) von GBase sondern
class GBase
{
....
public:
virtual void Draw();
};
class Frame: public GBase
{
....
public:
virtual void Draw();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase->Draw(); // Draw() von Frame!
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (2 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
von Frame aufgerufen. Sie müssen dazu
nicht einmal mehr in Frame die
Memberfunktion Draw(...) als virtual
deklarieren, denn eine einmal in einer
Basisklasse als virtuell deklarierte
Memberfunktion ist automatisch in allen
davon abgeleiteten Klasse ebenfalls
virtuell.
}
Eine Klasse mit mindestens einer virtuellen Memberfunktion wird auch als polymorphe Klasse
bezeichnet.
Wie Sie diese 'Zusammenarbeit' von Basisklassenzeiger und virtuellen Memberfunktionen einsetzen können,
soll nun anhand eines kleinen Beispiels demonstriert werden.
Das Beispiel:
Im Beispiel finden Sie wieder die inzwischen bekannte Basisklasse GBase. Sie enthält der Einfachheit halber
nur die Memberfunktion Draw(...), die jetzt als virtuelle Memberfunktion deklariert ist. Draw(...) gibt nur
einen kurzen Text aus.
Von dieser Basisklasse sind zwei weitere Klasse Frame und Bar abgeleitet. Auch diese Klassen enthalten
jeweils eine Memberfunktion Draw(...), die aber einen anders lautenden Text ausgeben. Beachten Sie, dass
Draw(...) in diesen Klassen nicht explizit als virtuelle Memberfunktion deklariert wird, aber wegen der
virtual Deklaration innerhalb der Basisklasse ebenfalls virtuell ist.
Nach den Klassendefinitionen erfolgt die Definition der Funktion DoAnything(...). Diese Funktion erhält als
Parameter einen Zeiger vom Typ Basisklasse GBase. Nachdem die Funktion einen Text ausgegeben hat, ruft
sie die Memberfunktion Draw(...) über den erhaltenen Basisklassenzeiger auf.
Im Hauptprogramm wird dann ein Basisklassenzeiger pBase definiert, in dem ein Zeiger auf ein Frame
Objekt abgelegt wird. Anschließend wird die Funktion DoAnything(...) aufgerufen, der der
Basisklassenzeiger pBase übergeben wird. Beachten Sie, dass der Basisklassenzeiger auf ein Objekt vom Typ
Frame zeigt.
Nach dem DoAnything(...) einen Text ausgegeben hat, wird über den Basisklassenzeiger die Memberfunktion
Draw(...) aufgerufen. Da Draw(...) in der Basisklasse als virtuell deklariert ist, wird jetzt diejenige Draw(...)
Memberfunktion aufgerufen die zu dem im Zeiger abgelegten Objekt gehört, in diesem Fall also Draw(...)
von Frame.
Dann wird im Hauptprogramm ein Objekt vom Typ Bar definiert und dessen Adresse an DoAnything(...)
übergeben. Da DoAnything(...) einen Basisklassenzeiger GBase als Parameter erwartet, kann an diese
Funktion auch ein Zeiger auf ein Objekt einer von der Basisklasse abgeleitete Klasse übergeben werden.
Und auch hier ruft die Funktion dann letztendlich die richtige Draw(...) Memberfunktion von Bar auf.
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (3 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
Die Programmausgabe:
doing anything
Frame Draw()
doing anything
Bar Draw()
Das Programm:
// Definition der Basisklasse mit der virtuellen
// Memberfunktion Draw(...)
class GBase
{
public:
virtual void Draw() const
{
cout << "GBase Draw()\n";
}
};
// Definition der von GBase abgeleiteten Klasse Frame
// Draw(...) ist virtuelle Memberfunktion!
class Frame: public GBase
{
public:
void Draw() const
{
cout << "Frame Draw()\n";
}
};
// Definition einer weiteren Klasse Bar, ebenfalls abgeleitet
// von GBase
class Bar: public GBase
{
public:
void Draw() const
{
cout << "Bar Draw()\n";
}
};
// Beliebige normale Funktion die als Parameter
// einen Basisklassenzeiger erhält
void DoAnything(const GBase *pObj)
{
cout << "doing anything\n";
// Aufruf der Draw(...) Memberfunktion, die zu dem im
// Basisklassenzeiger abgelegten Objekt gehört!
pObj->Draw();
}
// Hauptprogramm
int main()
{
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (4 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
// Definition des Basisklassenzeigers
GBase *pBase;
// Frame Objekt im Basisklassenzeiger ablegen
pBase = new Frame;
// Funktion mit Basisklassenzeiger aufrufen
DoAnything(pBase);
.... // hier muss das Frame-Objekt noch geloescht werden!
// Bar Objekt definieren
Bar myBar;
// Funktion mit Adresse des Bar Objekts aufrufen
// Beachten Sie, dass DoAnything(...) einen Basisklassen// zeiger als Parameter besitzt!
DoAnything(&myBar);
return 0;
}
Pure virtual Memberfunktionen
Gehen wir jetzt einen Schritt weiter. Stellen Sie sich einmal vor, Sie wollen für ein neues Grafikobjekt eine
neue, von GBase abgeleitete Klasse erstellen. Im Eifer des Gefechts vergessen Sie aber, der Klasse für Ihr
neues Grafikobjekt eine Memberfunktion Draw(...) zum Zeichnen des Objekts hinzuzufügen. In diesem Fall
würde über den Basisklassenzeiger die Memberfunktion Draw(...) der Basisklasse aufgerufen werden. Da die
Basisklasse aber selbstverständlich nicht wissen kann, wie Ihr neues Grafikobjekt zu zeichnen ist, würden Sie
keine oder eine völlig falsche Darstellung erhalten. Zweifelsohne würden Sie dies beim ersten Testlauf
bemerken. Schöner wäre es jedoch, wenn Sie schon beim Übersetzen des Programms einen Hinweis erhalten
würden, dass ein wesentlicher Teil in Ihrer Klasse fehlt.
Und auch diese Überprüfung kann Ihnen
der Compiler abnehmen. Um zu
erreichen, dass alle von einer Basisklasse
abgeleiteten Klassen eine bestimmte
Memberfunktion besitzen müssen, wird
innerhalb der Basisklasse die
entsprechende Memberfunktion als pure
virtual deklariert. Dies wird dadurch
erreicht, dass bei der Deklaration der
Memberfunktion der Zusatz = 0
angehängt wird. Eine pure virtual
Memberfunktion darf innerhalb der
Basisklasse nur deklariert werden, d.h.
Sie besitzt niemals einen
Memberfunktionsrumpf {....}. Im Beispiel
rechts wurde die Memberfunktion
Draw(...) der Klasse GBase als pure virtual
Memberfunktion deklariert, und damit
müssen alle von GBase abgeleiteten
Klassen diese Memberfunktion definieren.
class GBase
{
....
public:
virtual void Draw() = 0;
};
class Frame: public GBase
{
....
public:
void Draw();
};
void Frame::Draw()
{
....
}
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (5 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
Von einer Klasse, die mindestens eine pure
virtual Memberfunktion enthält, kann
kein Objekt definiert werden, da ja die
Definition der Memberfunktion fehlt.
Klasse mit pure virtual Memberfunktionen
werden auch als abstrakter Datentyp
(ADT) bezeichnet.
Überschreiben von virtuellen Memberfunktionen
Jetzt noch ein wichtiger
Hinweis. Die dynamische Bindung
über virtuelle Memberfunktionen
erfolgt nur dann, wenn die
Memberfunktionen in der Basisklasse
und in der abgeleiteten Klasse die
gleiche Signatur haben, d.h. sie
müssen im Namen und in den
Parameter übereinstimmen.
Unterscheidet sich eine
Memberfunktion in der abgeleiteten
Klasse in den Parametern von der
virtuellen Memberfunktion der
Basisklasse, so verdeckt sie die
virtuelle Memberfunktion der
Basisklasse. Ein Aufruf der
Memberfunktion über einen
Basisklassenzeiger ist dann nicht mehr
möglich, außer durch explizite
Typkonvertierung des Zeigers. Aber
das sollten Sie nach Möglichkeit
vermeiden! Im Beispiel rechts erhält
die Memberfunktion Draw(...) der
abgeleiteten Klasse Frame nun einen
int-Parameter. Wird dann wie im
Beispiel versucht Draw(...) über einen
Basisklassenzeiger aufzurufen, so
meldet Ihnen der Compiler jetzt einen
Fehler.
class GBase
{
....
public:
virtual void Draw();
};
class Frame: public GBase
{
....
public:
virtual void Draw(int val);
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase->Draw(2); // geht nicht mehr!
}
Virtueller Destruktor
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (6 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
Vielleicht ist Ihnen bei den bisherigen
Beispielen aufgefallen, dass bis jetzt zwar
Objekte erstellt aber noch nicht zerstört
wurden. Sehen Sie sich dazu wieder das
Beispiel rechts an. Dort wird zunächst wie
gewohnt einem Basisklassenzeiger ein
dynamisch erstelltes Objekt einer
abgeleiteten Klasse zugewiesen. Am Ende
des Programms wird dann das Objekt
wieder mittels delete gelöscht. Wenn Sie
dieses Programm nun laufen lassen
würden, würden Sie feststellen, dass der
delete Operator aber den Destruktor der
Basisklasse aufruft und nicht den der
abgeleiteten Klasse, wie es eigentlich sein
sollte.
class GBase
{
....
public:
~GBase();
};
class Frame: public GBase
{
....
public:
~Frame();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
....
delete pBase;
}
Und auch hier hilft uns das Schlüsselwort
virtual weiter. Deklarieren Sie den
Destruktor der Basisklasse einfach als
virtuell und schon werden die richtigen
Destruktoren (in der richtigen
Reihenfolge) aufgerufen. Sie wissen doch
hoffentlich noch, dass bei abgeleiteten
Klassen zuerst die Destruktoren der
abgeleiteten Klassen ausgeführt werden
und erst zum Schluss der Destruktor der
Basisklasse. Für das Beispiel bedeutet
dies, dass zuerst der Destruktor von
Frame und dann der von GBase
ausgeführt wird.
class GBase
{
....
public:
virtual ~GBase();
};
class Frame: public GBase
{
....
public:
virtual ~Frame();
};
int main()
{
GBase *pBase;
Die Angabe des Schlüsselworts virtual
pBase = new Frame(...);
beim Destruktor der Klasse Frame ist
....
übrigens optional, da ja alle
delete pBase;
Memberfunktionen die in der Basisklasse }
als virtuell deklariert wurden in den
abgeleiteten Klassen ebenfalls virtuell
sind. Ebenso können Destruktoren auch
als pure virtual deklariert werden. In
diesem Fall müssen Sie aber im Gegensatz
zu 'normalen' pure virtual
Memberfunktionen den Destruktor in der
Basisklasse noch explizit definieren.
Beachten Sie, dass es nicht erlaubt ist Konstruktore als virtuelle Memberfunktionen zu deklarieren und
virtuelle Memberfunktionen niemals static- oder friend-Memberfunktionen (wird nachher gleich noch
behandelt) sein können.
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (7 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
Zum Schluss dieser Lektion nochmals der Hinweis, dass virtuelle Memberfunktionen nur im
Zusammenspiel mit Basisklassenzeigern Sinn machen. In allen anderen Fällen gibt es keinen
Unterschied zwischen einer 'normalen' und einer virtuellen Memberfunktion. So können Sie z.B. auch
virtuelle Memberfunktionen direkt über das entsprechende Objekt aufrufen.
So, und damit wären wir wieder beim Beispiel und der Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel enthält die vollständigen Klassen der in dieser Lektion behandelten Grafikelemente. Die
Basisklasse für alle Grafikobjekte bildet die Klasse Graphic. Diese Klasse enthält u.a. einen virtuellen
Destruktor und die pure virtual Memberfunktion Draw(...). Dadurch dass Draw(...) als pure virtual deklariert
ist, müssen alle abgeleiteten Klassen diese Memberfunktion implementieren um ihre Grafik darzustellen.
Von Graphic werden dann die Klassen Circle, Bar und Text abgeleitet.
Im Hauptprogramm wird ein Zeigerfeld vom Typ der Basisklasse Graphic definiert in dem Zeiger auf die
drei verschiedenen Grafikobjekte abgelegt werden. Innerhalb einer Schleife werden dann alle Grafiken
ausgegeben und danach wieder gelöscht.
Das unten stehende Beispiel finden Sie auch unter: le15\prog\Bvmeth.
Die Programmausgabe:
Rechteck zeichnen:
Cursor auf (10,10) Breite=20 Hoehe=20
Kreis zeichnen:
Cursor auf (40,40) Radius=20
Text ausgeben:
Cursor auf (100,100) Text: "My Text"
Das Programm:
// C++ Kurs
// Beispiel zu virtuellen Memberfunktionen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (8 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
// Definition der Basisklasse Graphic
// ===================================
// Graphic enthaelt die fuer alle Grafikelemente notwendigen X/Y Koordinaten.
class Graphic
{
short
xPos, yPos;
// Koordinaten der Grafik
public:
Graphic(short x, short y);
virtual ~Graphic()
// virtueller Destruktor
{}
virtual void DrawIt() const = 0;
// pure virtual Memberfunktion
void GetCurPos () const;
};
// Definition der Memberfunktionen
// Konstruktor
Graphic::Graphic(short x, short y): xPos(x), yPos(y)
{
}
// Grafikcursor setzen
void Graphic::GetCurPos() const
{
cout << "Cursor auf (" << xPos << "," << yPos << ") ";
}
// Definition der Klasse Circle
// =============================
class Circle: public Graphic
{
short radius;
// Kreisradius
public:
Circle (short, short, short);
void DrawIt() const;
};
// Definition der Memberfunktionen
// Konstruktor der Klasse Circle
Circle::Circle(short x, short y, short r):
Graphic(x, y),
radius(r)
{
}
// Kreis zeichnen
void Circle::DrawIt() const
{
cout << "Kreis zeichnen:\n";
GetCurPos();
cout << "Radius=" << radius << endl;
}
// Definition der Klasse Bar
// ==========================
class Bar: public Graphic
{
short width;
// Breite und Hoehe
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (9 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
short height;
public:
Bar (short, short, short, short);
void DrawIt() const;
};
// Definition der Memberfunktionen
// Konstruktor der Klasse Bar
Bar::Bar(short x, short y, short w, short h):
Graphic(x, y),
width(w), height(h)
{
}
// Rechteck zeichnen
void Bar::DrawIt() const
{
cout << "Rechteck zeichnen:\n";
GetCurPos();
cout << "Breite=" << width;
cout << " Hoehe=" << height << endl;
}
// Definitionder Klasse Text
// ==========================
class Text: public Graphic
{
string sText;
// abzulegender Text
public:
Text (short, short, const char *const);
void DrawIt() const;
};
// Definition der Memberfunktionen
// Konstruktor der Klasse Text
Text::Text (short x, short y, const char *const pT):
Graphic(x, y),
sText(pT)
{
}
// Text ausgeben
void Text::DrawIt() const
{
cout << "Text ausgeben:\n";
GetCurPos();
cout << "Text: \"" << sText << "\"\n";
}
//
// HAUPTPROGRAMM
// =============
int main()
{
// Feld fuer 3 Grafikobjekt definieren
Graphic *pObjects[3];
// Nun nacheinander ein Rechteck, einen Kreis und
// einen Text im Feld ablegen
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (10 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
pObjects[0] = new Bar(10,10,20,20);
pObjects[1] = new Circle(40,40,20);
pObjects[2] = new Text(100,100,"My Text");
// Alle drei Objekte ausgeben
for (short index=0; index<3; index++)
{
// Objekte ausgeben
pObjects[index]->DrawIt();
// und gleich wieder loeschen
delete pObjects[index];
}
}
Die Übung:
Es ist sind die rechts dargestellten Klassen zu realisieren. Von einer
Klasse CWinBase ist zunächst Klasse CWindow und davon wiederum
CButton abzuleiten.
Die Klasse CWinBase soll die grundlegenden Eigenschaften aller
Fenster enthalten wie Größe und Position. Zur Ausgabe der
Eigenschaften des Fensters wird die Memberfunktion Draw(...)
verwendet.
Die Klasse CWindow repräsentiert einen bestimmten Fenstertyp. Diese
von CWinBase abgeleitete Klasse enthält zusätzlich noch die
Eigenschaft Beschriftung für den Fenstertitel. Zur Darstellung des
Fensters wird wieder die Memberfunktion Draw(...) verwendet.
Von CWindow abgeleitet ist schließlich die Klasse CButton zur
Darstellung eines Buttons. Ein Button ist im Prinzip ein Sonderfall
eines Fensters das beim Anklicken irgend welche Aktionen auslöst.
Damit Buttons unterschieden werden können, erhält jeder Button
seine eigene Nummer, die Button-ID. Und auch hier wird ebenfalls eine
Memberfunktion Draw(...) eingesetzt um den Button-Eigenschaften
auszugeben.
Beachten Sie bitte, dass im Bild rechts nicht alle notwendigen Memberfunktionen angegeben sind. So sind
eventuelle Konstruktore und Destruktoren noch selbstständig zu implementieren.
Definieren Sie dann im Hauptprogramm ein entsprechendes Feld um Zeiger auf Objekte vom Typ CWindow
und CButton ablegen zu können. Erstellen Sie dann von CWindow und CButton Objekte und legen deren
Zeiger im erwähnten Feld ab. Lassen Sie beide Objekte darstellen (Ausgabe der Eigenschaften).
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (11 von 12) [17.05.2005 11:40:37]
Virtuelle Memberfunktionen
Die Programmausgabe:
Daten des CWindow-Objekts:
Position: (10,10)
Groesse : (200,100)
Beschriftung: Fensterobjekt
Daten des CButton-Objekts:
Position: (50,50)
Groesse : (100,50)
Beschriftung: Buttonobjekt
Button-ID: 10
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie
die Lösung unter loesungen\le15\Lvmeth.
Und auch in der nächsten Lektion geht es noch virtuell weiter. Wir werden uns virtuelle Basisklassen ansehen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le15/le15_03.htm (12 von 12) [17.05.2005 11:40:37]
Virtuelle Basisklassen
Virtuelle Basisklassen
Die Themen:
Problem bei Mehrfach-Ableitung
Virtuelle Basisklassen
Virtuelle Basisklassen und Konstruktore
Beispiel
Problem bei Mehrfach-Ableitung
Um die Bedeutung von virtuellen
Basisklassen zu demonstrieren, sehen Sie
sich einmal die nebenstehende
Klassenhierarchie an. Oberste Basisklasse ist
die Klasse CWinBase. Davon abgeleitet
werden die beiden Klassen CFrame (für die
Implementation eines Fensters mit Rahmen)
und die Klasse CMenu (für die
Implementation eines Fenstermenüs). Aus
diesen beiden Klassen wird dann eine neue
Klasse CWindow gebildet, die ein
Rahmenfenster mit Menü darstellt.
Doch halt, irgend etwas stimmt hier nicht! Da sowohl CFrame wie auch CMenu von CWinBase
abgeleitet sind, erben diese beiden Klassen natürlich die Eigenschaften von CWinBase. Wird nun aus
CFrame und CMenu eine neue Klasse gebildet, so enthält die neue Klasse die Eigenschaften von
CWinBase in zweifacher Ausführung, einmal aus CFrame und einmal aus CMenu. Und das ist
bestimmt nicht das, was mit der Ableitung erreicht werden sollte. Das nachfolgende Bild
veranschaulicht diesen Sachenverhalt nochmals.
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (1 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
Virtuelle Basisklassen
Und auch in diesen Fall hilft uns das Schlüsselwort virtual weiter. Wird eine Klasse von mehreren
Basisklassen abgeleitet, die wiederum selbst von einer gemeinsamen Basisklasse abgeleitet sind, so
kann durch virtuelles Ableiten der Basisklasse vermieden werden, dass die Eigenschaften der
obersten Basisklasse mehrfach weitervererbt werden. In unserem Beispiel müssen also die Klassen
CFrame und CMenu virtuell von CWinBase abgeleitet werden.
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (2 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
Virtuelle Basisklassen und Konstruktore
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (3 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
Sehen wir uns nun dieses virtuelle
Ableiten in der Praxis an. Um
eine Klasse virtuelle abzuleiten,
wird vor dem Zugriffrecht der
Ableitung das Schlüsselwort
virtual gestellt. Beachten Sie, dass
nur CFrame und CMenu virtuell
abgeleitet sind und nicht mehr
die endgültige Klasse CWindow.
// Oberste Basisklasse
class CWinBase
{....};
// 1. virtuell abgeleitete Klasse
class CFrame: virtual public CWinBase
{....};
// 2. virtuell abgeleitete Klasse
class CMenu: virtual public CWinBase
{....};
// Endgültige Klasse
class CWindow: public CFrame, public CMenu
{....};
Eine Besonderheit tritt hierbei
CWindow::CWindow(...):
jedoch auf. Bei einer 'normalen'
CWinBase(...),
Ableitung ruft der Konstruktor
CFrame(...),
einer abgeleiteten Klassen nur die
CMenu(...)
Konstruktoren seiner
{
Basisklassen auf. Sie können Sie
....
sich diesen Sachverhalt jetzt
}
nochmals ansehen ( ). Dieser
Sachverhalt trifft bei virtuell
abgeleiteten Klassen aber nur
dann zu, wenn die Basisklassen
einen Standard-Konstruktor
besitzen. Benötigt der
Konstruktor der 'obersten'
Basisklasse jedoch Parameter, so
ist der Konstruktor der jeweils
'untersten' Klasse selbst dafür
verantwortlich, den Konstruktor
der 'obersten' Basisklasse
aufzurufen. Für unser Beispiel
ergibt sich damit der rechts
dargestellte Konstruktor der
Klasse CWindow.
Für die Aufruf-Reihenfolge der Konstruktoren gilt, dass zuerst der Konstruktor der 'obersten'
Basisklasse ausgeführt wird und danach die Konstruktoren der nachfolgenden Basisklassen in der
Reihenfolge, in der die Basisklassen bei der Klassendefinition angegeben wurden. Halten Sie diese
Reihenfolge auch bei der Initialisiererliste der 'untersten' Klasse ein, da Sie ansonsten je nach
Compiler eventuell eine Reihe Warnungen erhalten.
Damit haben wir das Ende dieser Lektion erreicht. Es folgt nun noch ein Beispiel. Auf eine Übung
wird ausnahmsweise verzichtet.
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (4 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
Beispiel
Das Beispiel:
Im Beispiel werden drei Klassen für Grafikobjekte definiert. Die Klassen CCircle und CBar dienen
zur Darstellung eines Kreises bzw. eines Rechtecks. Die beiden Klassen gemeinsamen Eigenschaften
sind in ihrer Basisklasse CGraphic abgelegt, im Beispiel der Einfachheit halber nur die Position.
Aus den Klassen CCircle und CBar wird eine neue Klasse CCBar gebildet, die ein abgerundetes
Rechteck mit einer Beschriftung realisiert. Damit CGraphic in der neuen Klasse CCBar aber nur
einmal vorhanden ist, müssen die Klassen CCircle und CBar virtuell von CGraphic abgeleitet
werden. Damit muss aber der Konstruktor von CCBar explizit den Konstruktor der obersten
Basisklasse CGraphic aufrufen.
Im Hauptprogramm wird dann Zeigerfeld vom Typ der Basisklasse CGraphic definiert, in dem
dann Zeiger auf die drei verschiedenen Grafikobjekte abgelegt werden. Innerhalb einer Schleife
werden dann alle Grafiken ausgegeben und danach wieder gelöscht.
Das unten stehende Beispiel finden Sie auch unter: le15\prog\Bvbase.
Die Programmausgabe:
--- Definition CBar --Konstruktor CGraphic
Konstruktor CBar
--- Definition CCircle --Konstruktor CGraphic
Konstruktor CCircle
--- Definition CCBar --Konstruktor CGraphic
Konstruktor CCircle
Konstruktor CBar
Konstruktor CCBar
Rechteck-Parameter
Position: 10,10
Groesse : 20,20
Kreis-Parameter
Position: 40,40
Radius : 20
CCBar besteht aus:
Kreis-Parameter
Position: 50,50
Radius : 50
Rechteck-Parameter
Position: 50,50
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (5 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
Groesse : 100,100
Beschriftung: Rechteck mit Text
Das Programm:
// C++ Kurs
// Beispiel zu virtuellen Basisklassen
//
// Zuerst Dateien einbinden
#include <iostream>
#include <string>
// Nun Namensraum auf std setzen
using namespace std;
// Definition der Klasse CGraphic
// ==============================
// CGraphic ist eine abstrakte Klasse da sie die 'pure virtual'
// Memberfunktion Draw() enthält.
// CGraphic speichert die allen Grafikelementen gemeinsamen
// X/Y Koordinaten.
class CGraphic
{
protected:
short
nXPos, nYPos;
// Koordinaten der Grafik
CGraphic(short nX, short nY);
// protected-Konstruktor
public:
virtual ~CGraphic()
{}
virtual void Draw() const = 0;
};
// Definition der Memberfunktionen
// Konstruktor
CGraphic::CGraphic(short nX, short nY)
{
// Koordinaten der Grafik ablegen
nXPos = nX;
nYPos = nY;
cout << "Konstruktor CGraphic\n";
}
// Definition der Klasse CCircle
// =============================
// CCircle ist abgeleitet von CGraphic und enthaelt als
// zusaetzliches Datum den Radius eines Kreises
class CCircle: virtual public CGraphic
{
short
nRadius;
// Kreisradius
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (6 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
public:
CCircle (short, short, short);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
CCircle::CCircle(short nX, short nY, short nR):
CGraphic(nX, nY)
// Konstruktor Basisklasse
{
// Radis ablegen
cout << "Konstruktor CCircle\n";
nRadius = nR;
}
// Gibt die Kreisdaten aus
void CCircle::Draw() const
{
cout << "Kreis-Parameter\n";
cout << "Position: " << nXPos << "," << nYPos << endl;
cout << "Radius : " << nRadius << endl;
}
// Definition der Klasse CBar
// ==========================
// CBar ist abgeleitet von CGraphic und enthält als
// zusätzliches Datum die Breite und Höhe des Rechtecks
class CBar: virtual public CGraphic
{
short
nWidth, nHeight;
// Groesse des Rechtecks
public:
CBar (short, short, short, short);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor der Klasse CBar
CBar::CBar(short nX, short nY, short nW, short nH):
CGraphic(nX, nY)
// Konstruktor Basisklasse
{
// Ausdehnung ablegen
cout << "Konstruktor CBar\n";
nWidth = nW;
nHeight = nH;
}
// Gibt die Daten des Rechtecks aus
void CBar::Draw() const
{
cout << "Rechteck-Parameter\n";
cout << "Position: " << nXPos << "," << nYPos << endl;
cout << "Groesse : " << nWidth << "," << nHeight << endl;
}
// Definition der Klasse CCBar
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (7 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
// ===========================
// CCBar ist abgeleitet von CCircle und CBar und damit auch
// von CGraphic
class CCBar: public CCircle, public CBar
{
string sText;
public:
CCBar(short, short, short, const char *const);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor von CCBar
// Beachten Sie die Aufrufe der Konstruktoren der Basisklassen.
// Obwohl CCBar nicht direkt von CGraphic abgeleitet ist muß der
// Konstruktor von CGraphic gesondert aufgerufen werden
CCBar::CCBar(short nX, short nY, short nW, const char *const pszT):
CGraphic(nX, nY),
// ctor virtuelle Basisklasse
CCircle(nX, nY, nW>>1),
// ctor Basisklasse
CBar(nX, nY, nW, nW),
// ctor Basisklase
sText(pszT)
// string initialisieren
{
cout << "Konstruktor CCBar\n";
}
// Gibt Daten des beschrifteten Rechtecks aus
// Ruft die Memberfunktionen Draw() der Klassen CBar und CCircle auf
// Beachten Sie, daß unbedingt die Angabe des Zugriffsspezifizieres
// beim Aufruf der Memberfunktionen notwendig ist
void CCBar::Draw() const
{
cout << "CCBar besteht aus:\n";
CCircle::Draw();
CBar::Draw();
cout << "Beschriftung: " << sText << endl;
}
//
// HAUPTPROGRAMM
// =============
int main()
{
// Feld fuer 3 Zeiger auf Grafikobjekte definieren
// Das Feld nimmt Basisklassenzeiger auf!
CGraphic *pCObject[3];
// Kreis erstellen und Zeiger ablegen
cout << "--- Definition CBar ---\n";
pCObject[0] = new CBar(10,10,20,20);
// Rechteck erstellen und Zeiger ablegen
cout << "--- Definition CCircle ---\n";
pCObject[1] = new CCircle(40,40,20);
// Beschriftetes Rechteck erstellen und Zeiger ablegen
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (8 von 9) [17.05.2005 11:40:42]
Virtuelle Basisklassen
cout << "--- Definition CCBar ---\n";
pCObject[2] = new CCBar(50,50,100,"Rechteck mit Text");
// Alle Grafiken ausgeben und dann wieder loeschen
for (short iIndex=0; iIndex<3; iIndex++)
{
pCObject[iIndex]->Draw();
delete pCObject[iIndex];
}
}
So, und wieder ist eine Lerneinheit geschafft.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le15/le15_04.htm (9 von 9) [17.05.2005 11:40:42]
Friend-Funktion und -Klassen
Friend-Funktion und -Klassen
Die Themen:
Einleitung
friend-Klassen
friend-Funktionen
Übung
Einleitung
In einigen Fällen kann es notwendig werden, dass andere Klassen oder Funktionen Zugriff auf
die geschützten Daten einer Klasse benötigen. Sehen wir uns hierzu wieder ein Beispiel an.
Angenommen Sie haben
// Jede Zeile des Feldes Matrix::aMatrix ist
eine Klasse Matrix die eine 2- // mit dem Faktor des Feldes Vektor::aVektor
dimensionale Matrix
// zu multiplizieren, also
(Tabelle) repräsentiert und //
aMatrix[i][0..2] *= aVektor[i]
eine Klasse Vektor die ein 1- // Definition von Matrix
dimensionales Feld enthält. class Matrix
Nun wollen Sie im
{
Programm die Daten der
short aMatrix[3][3];
Matrix so verändern, dass
....
alle Zeilen der Matrix
};
Matrix mit einem
// Definition von Vector
bestimmten Faktor
class Vektor
multipliziert werden. Die
{
Faktoren für die einzelnen
short aVektor[3];
Zeilen sind innerhalb der
....
Klasse Vektor abgelegt. Da };
die Eigenschaften (Daten)
in der Regel in der privateoder protected-Sektion einer
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (1 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Klasse abgelegt sind,
können Sie von Matrix nicht
auf die Faktoren in Vektor
zugreifen und auch nicht
umgekehrt. Sie könnten
jetzt zwar die
Eigenschaften als public
deklarieren, würden damit
aber den Zugriff auf die
Eigenschaften generell
freigeben was wiederum in
der Regel auch nicht
erwünscht ist. Der Ausweg
aus diesem Dilemma heißt
friend-Klasse.
friend-Klassen
Damit eine Klasse Class1
Zugriff auf alle Member
einer anderen Klasse Class2
erhält, wird die Klasse
Class1 als friend-Klasse der
Klasse Class2 deklariert.
Dazu wird innerhalb der
Klassendefinition der
Klasse, die ihren 'Schutz'
aufgibt, folgende
Anweisung eingefügt:
'
friend class
FClass;
// Definition von Matrix
class Matrix
{
short aMatrix[3][3];
....
};
// Definition von Vektor
// Erlaubt Matrix Zugriff auf alle Member
class Vektor
{
friend class Matrix;
short aVektor[3];
....
};
FClass ist hierbei die
Klasse, die
uneingeschränkten Zugriff
auf alle Member der Klasse
erhalten soll. Im Beispiel
rechts erhält also die Klasse
Matrix vollen Zugriff auf
alle Member der Klasse
Vektor.
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (2 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Da nun Matrix auf alle
Member von Vektor
zugreifen kann, könnte die
notwendige Routine zur
Multiplikation der
Matrixdaten wie rechts
angegeben aussehen.
// Vorwärtsdeklararation von Vektor
class Vektor;
// Definition von Matrix
class Matrix
{
short aMatrix[3][3];
void Multi(const Vektor& CV);
....
Beachten Sie dabei bitte,
};
dass Vektor vor Matrix
// Definition von Vektor
definiert werden muss, da // Erlaubt Matrix Zugriff auf alle Member
die Memberfunktion
class Vektor
Multi(...) der Klasse Matrix {
als Parameter eine
friend class Matrix;
Referenz auf die Klasse
short aVektor[3];
Vektor erhält. Eine
....
vollständige Definition der };
Klasse Vektor vor Matrix ist // Definition von Matrix::Multi
noch nicht möglich, da
void Matrix::Multi(const Vektor& vekt)
innerhalb von Vektor die
{
Klasse Matrix als friendfor (int row=0; row<3; row++)
Klasse deklariert wird.
for (int col=0; col<3; col++)
Außerdem kann die
aMatrix[row][col] *= vekt.aVektor[row];
endgültige Definition der
}
Memberfunktion
Matrix::Multi(...) erst nach
der Definition der Klasse
Vektor erfolgen, da
innerhalb der
Memberfunktion auf die
Eigenschaft aVektor von
Vektor zugegriffen wird. Sie
sehen, die Reihenfolge
wann was deklariert und
definiert wird ist nicht
immer ganz einfach.
friend und Vererbung
Die friend-Eigenschaft einer Klasse ist nicht vererbbar. Würde im vorherigen Beispiel von der
Klasse Matrix, die ja friend-Klasse von Vektor ist, eine weitere Klasse SMatrix abgeleitet werden,
so hätte SMatrix keinen Zugriff auf die geschützten Eigenschaften von Vektor. Das gleiche gilt
übrigens auch für den Fall, dass von Vektor eine weitere Klasse abgeleitet wird. Für diese neuen
Klasse wäre die Klasse Matrix ebenfalls nicht automatisch friend-Klasse.
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (3 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Außerdem gilt die friend-Eigenschaft nicht automatisch für die Umkehrfall, d.h. ist z.B. die
Klasse Matrix friend-Klasse von Vektor, so ist Vektor nicht von Haus aus auch friend-Klasse von
Matrix. Dieser Fall muss explizit deklariert werden.
friend-Funktionen
Außer den bisher
behandelten friend-Klassen
gibt es auch noch friendFunktionen. friendFunktionen gehören keiner
Klasse an und haben
ebenfalls vollen Zugriff auf
alle Member einer Klasse.
Um eine Funktion als friendFunktion einer Klasse zu
deklarieren, wird wieder
innerhalb der Klasse die
ihren 'Schutz' aufgibt
folgende Anweisung
eingefügt:
// Klassendefinition
class Base
{
....
friend void DoAnything();
};
// Definition der friend-Funktion
void DoAnything()
{
.... // Voller Zugriff auf Base
}
friend RTYP
FNAME(...);
RTYP ist der Returntyp der
Funktion und FNAME der
Funktionsname. Die
Definition der Funktion
erfolgt wie gewohnt, d.h.
ohne Angabe der friendBeziehung zu einer Klasse.
Aus diesem Grund kann
eine Funktion auch friendFunktion von mehreren
Klassen sein.
Bezüglich Ableitung von Klassen und friend-Funktionen gilt auch hier das gleiche wie bei friendKlassen. Ist eine Funktion Func(...) friend-Funktion von Klasse Base und ist von Base eine Klasse
Any abgeleitet, so ist Func(...) nicht automatisch friend-Funktion von Any. Auch dies muss
ebenfalls explizit deklariert werden.
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (4 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Einsatz von friend-Funktionen
Obwohl friend-Funktionen
soweit wie möglich
vermeiden sollten da sie die
Datenkapselung
durchbrechen, gibt es Fälle,
wo es ohne friend-Funktion
nicht geht. Erinnern Sie
sich noch an die Klasse
CString aus dem letzten
Kapitel? Dort haben wir
u.a. den Plus-Operator '+'
überladen um folgende
Anweisungen schreiben zu
können:
// Klassendefinition
class CString
{
....
CString& operator + (char* pT);
CString& operator + (CString& op2);
};
stringObj3 =
stringObj1 +
stringObj2;
stringObj3 =
stringObj1 +
"any text";
D.h. wir konnten zu einem CString Objekt entweder ein weiteres CString Objekt oder einen
ASCII-String hinzuaddieren. Was bisher nicht ging war folgende Operation:
stringObj3 = "any text" + stringObj2;
Der Grund dafür ist, dass
der überladene Operator
'+' als Memberfunktion des
ersten Operanden
aufgerufen wird. Damit
aber auch diese Operation
definiert, ist kann die rechts
angegebene friend-Funktion
eingesetzt werden. Beim
Überladen von binären
Operatoren durch
Funktionen erhält die
Funktion immer zwei
Parameter. Der erste
Parameter steht für den
linken Operanden und der
// Klassendefinition
class CString
{
....
CString operator + (const char *pT);
CString operator + (const CString& op2);
friend CString
operator +(const char* pT,
const CString& op2);
};
// Definition der friend-Funktion
CString operator + (const char* pT,
const CString& op2)
{
CString temp(pT);
temp = temp + op2;
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (5 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
zweite Parameter für den
}
rechten. Da für die obige
Anweisung der linke
Operand den Datentyp
const char-Zeiger besitzt
und der rechte Operand
wieder eine Referenz auf
ein CString Objekt ist,
ergibt sich damit die rechts
dargestellte friendFunktion.
return temp;
Hinweis: Das Beispiel ist
nur ein Auszug,
selbstverständlich müssen
Sie für die Klasse CString
noch andere Operatoren
wie z.B. den
Zuweisungsoperator '='
überladen.
Eine weitere Ausnahme wo
es ohne friend-Funktion
nicht geht haben Sie
ebenfalls in den letzten
Lektionen kennen gelernt,
nämlich beim Überladen
der Operatoren '<<' und
'>>' für die Ausgabe bzw.
Eingabe von Objekten.
Und damit sind wird schon wieder am Ende einer Lektion angelangt. Zum Schluss folgt jetzt
noch die Übung (ohne weiteres Beispiel).
Übung
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (6 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Die Übung:
Erstellen Sie eine Klasse Matrix zur Aufnahme einer Tabelle mit 3 Zeilen á 3 Spalten.
Initialisieren Sie die Tabelle im Konstruktor mit beliebigen Werten. Fügen Sie der Klasse eine
Memberfunktion zur Ausgabe der Tabellenwerte hinzu.
Erstellen Sie eine weitere Klasse Vector zur Aufnahme eines Vektors (Feldes) mit 3
Elementen. Initialisieren Sie den Vektor im Konstruktor ebenfalls mit beliebigen Daten.
Schreiben Sie dann eine Funktion Multi(...), die alle Elemente der Zeile n in Matrix mit dem
Feldelement n in Vector multipliziert.
Definieren Sie im Hauptprogramm jeweils ein Objekt vom Typ Matrix und eins vom Typ
Vector. Geben Sie die Daten des Matrix Objekts aus. Multiplizieren anschließend die Tabelle
mit dem Vektor über die Funktion Multi(...). Geben Sie die geänderten Daten des Matrix
Objekts aus.
Die Programmausgabe:
Ausgangsmatrix:
0 1 2
10 11 12
20 21 22
Nach der Multiplikation:
0 2 4
30 33 36
80 84 88
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le16\Lfriend.
In der nächsten Lektion erfahren Sie dann, wie Sie innerhalb eines C++ Programms
schwerwiegende Fehlerfälle abfangen können.
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (7 von 8) [17.05.2005 11:40:44]
Friend-Funktion und -Klassen
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/le16_01.htm (8 von 8) [17.05.2005 11:40:44]
Ausnahme-Behandlungen
Ausnahme-Behandlungen
Die Themen:
Bisherige Fehlerbehandlung
Einleiten und Auslösen einer Exception
Abfangen (Behandeln) einer Exception
Exceptions in Funktionen/Memberfunktionen
Exceptions in Konstruktoren
Weiterleiten von Exceptions
Parameter von throw
Exception-Klassen
new und Exception
Sonstige Exception-Funktionen
Beispiel und Übung
Bisherige Fehlerbehandlung
In dieser Lektion erfahren Sie welche Möglichkeiten Ihnen C++ bietet, schwerwiegende Fehler,
die zur Programmlaufzeit auftreten, abzufangen.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (1 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Beginnen wir mit einem
relativen einfachen Beispiel
das die bisherige
Behandlung eines
Laufzeitfehlers
demonstriert. Im Beispiel
rechts wird eine Funktion
CalcSqrt(...) aufgerufen um
die Quadratwurzel aus
einem als Parameter
übergebenen Wert zu
berechnen. Ist der
übergebene Wert negativ,
so kann daraus laut
Mathematik-Unterricht 6.
Klasse keine Wurzel
berechnet werden. In
diesem Fall gibt die
Funktion den Wert -1.0
zurück. War der Wert
positiv, so wird die
Bibliotheksfunktion sqrt(...)
aufgerufen um die
Quadratwurzel zu
berechnen. Das Ergebnis
daraus wird
zurückgegeben. Im
Hauptprogramm wird
anschließend der
Returnwert der Funktion
abgeprüft und dann eine
eventuell notwendige
Fehlerbehandlung
durchgeführt.
double CalcSqrt(double val)
{
if (val < 0.0)
return -1.0;
return sqrt(val);
}
int main()
{
....
double res = CalcSqrt(var);
if (res < 0.0)
.... // Fehler behandeln
....
}
Einleiten und Auslösen einer Exception
Aber das Ganze geht unter C++ natürlich wesentlich eleganter. Um solche Fehlerfälle
abzufangen, wird das Exception-Handling (Ausnahmebehandlung) eingesetzt. Hierzu werden
nun nacheinander drei neue Schlüsselwörter eingeführt: try, catch und throw.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (2 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Jedes Exception-Handling
wird durch das
Schlüsselwort try (gleich
'versuche') eingeleitet. Nach
try muss immer ein Block
{...} folgen. Innerhalb diese
Blocks stehen nun die
Anweisungen, für die ein
Exception-Handling im
Fehlerfall durchgeführt
werden soll. Dabei spielt es
dann aber keine Rolle, ob
der Fehler innerhalb des
Blocks oder gar in einer
darin aufgerufenen
Funktion auftritt. Vom
Beginn des Blocks bis zu
dessen Ende werden alle
Fehlerfälle abgefangen.
double CalcSqrt(double val)
{
if (val < 0.0)
....
return sqrt(val);
}
int main()
{
....
try
{
double res = CalcSqrt(var);
....
}
}
Im Beispiel wird die
Funktion CalcSqrt(...)
später eine Exception
auslösen, wenn der ihr
übergebene Wert negativ
ist. Wie dies geschieht, das
erfahren Sie gleich noch.
Damit die Exception aber
richtig bearbeitet werden
kann, muss der Aufruf von
CalcSqrt(...) zunächst
innerhalb eines try-Blocks
erfolgen.
Sehen wir uns nun an, wie
die Funktion CalcSqrt(...)
eine Exception auslösen
kann. Zum Auslösen einer
Exception dient die throwAnweisung (gleich 'werfe')
die folgende Syntax besitzt:
double CalcSqrt(double val)
{
if (val < 0.0)
throw 1;
return sqrt(val);
}
int main()
{
....
throw <PARAM>;
try
{
Welche Bedeutung PARAM
double res = CalcSqrt(var);
hat wird nachher noch
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (3 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
näher erläutert. Für den
Augenblick reicht es aus,
wenn für PARAM eine
beliebige int-Konstante
eingesetzt wird.
....
}
}
Mit diesem Wissen können
wir die Funktion
CalcSqrt(...) jetzt wie rechts
angegeben erweitern. Ist
der an die Funktion
übergebene Wert negativ,
so löst die Funktion mittels
throw 1; eine Exception aus.
Damit kennen wir nun die Anweisungen die ein Exception-Handling einleiten (try) und die eine
Exception auslösen (throw). Was jetzt nur noch fehlt ist die Anweisung zum Abfangen
(Behandeln) der Exception.
Abfangen (Behandeln) einer Exception
Um eine innerhalb eines tryBlocks ausgelöste Exception
abzufangen, folgt
unmittelbar nach dem tryBlock ein neuer Block, der
durch eine catch-Anweisung
(gleich 'fange') eingeleitet
wird. Er besitzt folgende
Syntax:
double CalcSqrt(double val)
{
if (val < 0.0)
throw 1;
return sqrt(val);
}
int main()
{
....
try
{
catch (PARAM)
double res = CalcSqrt(var);
{
....
.... //
}
Ausnahmebehandlung
catch(...)
}
{
cout << "Fehler CalcSqrt()\n";
Und auch auf die genaue
}
Bedeutung von PARAM
....
kommen wir gleich noch zu }
sprechen. Für PARAM
setzen wir vorerst die
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (4 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Zeichenfolge ... (jawohl, das
sind drei Punkte) ein. Und
innerhalb des catch-Blocks
stehen dann die
Anweisungen, die beim
Auftreten einer Exception,
und nur dann, ausgeführt
werden sollen. Damit
können wir unser Beispiel
jetzt wie rechts angegeben
vervollständigen. Und wie
gesagt, der catch-Block
muss unmittelbar nach dem
try-Block stehen. Andere
Anweisungen dazwischen
sind nicht erlaubt! Der
catch-Block wird auch als
Exception-Handler
bezeichnet.
Sehen wir uns jetzt an, was die eingefügten Anweisungen letztendlich bewirken.
Gehen wir als erstes vom Gut-Fall aus, d.h. es wird kein negativer Wert an die Funktion
CalcSqrt(...) übergeben. Damit wird in der Funktion auch keine Exception ausgelöst und die
Funktion berechnet die Quadratwurzel und gibt diese ans Hauptprogramm zurück. Im
Hauptprogramm werden dann alle weiteren Anweisungen innerhalb des try-Blocks ganz normal
ausgeführt. Der auf den try-Block folgende catch-Block wird anschließend übersprungen, da
dessen Anweisungen ja nur im Falle einer Exception ausgeführt werden.
Sehen wir uns jetzt zum Schlecht-Fall an, also der Übergabe eines negativen Wertes an
CalcSqrt(...). Dies führt zum Auslösen einer Exception innerhalb der Funktion mittels throw 1.
Wird die throw-Anweisung ausgeführt, so wird sofort zu dem auf den try-Block folgenden catchBlock gesprungen, d.h. die Funktion CalcSqrt(...) wird vorzeitig verlassen und die restlichen
Anweisungen innerhalb des try-Blocks werden ebenfalls übersprungen. In unserem Beispiel
wird also nach der throw-Anweisung sofort die cout-Anweisung im catch-Block ausgeführt. Ist
der catch-Block abgearbeitet wird ganz normal mit der Programmbearbeitung fortgefahren.
Exceptions in Funktionen/Memberfunktionen
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (5 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Das korrekte Aufräumen
des Stacks, der ja die
Rücksprungadressen,
eventuelle
Funktionsparameter und
die lokale Daten einer
Funktion enthält, ist Sache
des Laufzeitsystems.
Darum brauchen Sie sich
nicht kümmern. Dieses
Aufräumen des Stacks
funktioniert sogar dann
noch, wenn zwischen dem
try-Block und der throwAnweisung mehrere
Funktionsebenen liegen. Im
Beispiel rechts ruft das
Hauptprogramm innerhalb
eines try-Blocks zunächst
die Funktion Func1(...) auf
und diese wiederum ruft
irgendwann Func2(...) auf.
In der Funktion Func2(...)
wird bei einer bestimmten
Bedingung nun eine
Exception ausgelöst. Und
auch hier kehrt das
Programm beim Auslösen
der Exception sofort in den
catch-Block im
Hauptprogramm zurück
um eine entsprechende
Meldung auszugeben und
dann das Programm zu
beenden. D.h. es werden
keinerlei Anweisungen
mehr innerhalb von
Func2(...) und Func1(...)
sowie innerhalb des tryBlocks ausgeführt.
void Func2()
{
if (...)
throw 1;
....
}
void Func1()
{
....
Func2();
....
}
int main()
{
....
try
{
Func1();
....
}
catch(...)
{
cout << "Fehler aufgetreten\n";
exit(1);
}
....
}
Exceptions in Konstruktoren
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (6 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Ebenfalls möglich ist der prinzipielle Einsatz von Exceptions in Konstruktoren. Wie Sie ja
wissen, besitzen Konstruktore keinen Returnwert über den man z.B. abprüfen kann, ob die
Erstellung eines Objekts erfolgreich war. Beim Einsatz von Exceptions in Konstruktoren sind
jedoch einige Dinge zu beachten.
Fangen wir mit der
Window::Window(int width,...)
Erstellung und Zerstörung {
des Objekts an. Beim
....
Auslösen der Exception
if (width > 1600)
innerhalb des Konstruktors
throw 1;
gibt es zunächst keine
....
weiteren Besonderheiten zu }
beachten. Aber beim
Zerstören des Objekts muss
eines beachtet werden.
Merken Sie sich immer
folgenden wichtigen Satz:
Der Destruktor eines Objekts wird nur dann aufgerufen, wenn der Konstruktor
vollständig, und damit fehlerfrei, ausgeführt wurde!
Wird also im Konstruktor eine Exception ausgelöst, so wird der Destruktor nicht mehr
aufgerufen. Sie müssen in diesem Fall eventuelle Aufräumarbeiten im Konstruktor also selbst
durchführen.
Der nächste Punkt ist zwar try
schon fast trivial, jedoch
{
vergisst man ihn leicht.
Window myWin(...)
Wird innerhalb eines try....
Blocks ein Objekt definiert, }
so ist dieses auch nur im try- catch(...)
Block gültig. Das
{
nebenstehende Beispiel
....
erzeugt also beim
}
Übersetzen eine
myWin.MoveWin(...);
Fehlermeldung, da
außerhalb des Blocks
versucht wird auf das im tryBlock definierte Objekt
zuzugreifen. Wollen Sie ein
Objekt innerhalb eines tryBlocks kontrolliert erstellen
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (7 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
und danach auch außerhalb
des Blocks mit dem Objekt
arbeiten, so müssen Sie
einen entsprechenden
Objektzeiger (der natürlich
außerhalb des try-Blocks
definiert wird) verwenden.
Vergessens Sie dabei aber
nicht, dass im Fehlerfall das
Objekt nicht gültig ist!
Das Auslösen einer Exception im Destruktor sollte soweit wie möglich vermieden
werden, da ansonsten das entsprechende Objekt nicht korrekt zerstört wird.
Weiterleiten von Exceptions
Machen wir den nächsten
Schritt. In manchen Fällen
kann es erforderlich
sein, dass die rechts in
Func2(...) ausgelöste
Exception sowohl in
Func1(...) wie auch im
Hauptprogramm
verarbeitet werden muss,
d.h. die Exception muss
nach ihrer ersten
Verarbeitung irgendwie
weitergegeben werden. In
diesem Fall wird zunächst
der Aufruf der Funktion
Func2(...) innerhalb von
Func1(...) in einen try-Block
gelegt, auf den dann
natürlich der dazugehörige
catch-Block folgen muss.
Zusätzlich muss nun aber
auch der Aufruf von
Func1(...) aus main(...)
heraus in einen try-Block
gelegt werden. Löst nun die
Funktion Func2(...) eine
Exception aus, so wird diese
zunächst im catch-Block
void Func2()
{
if (...)
throw 1; // Exception auslösen
....
}
void Func1()
{
try
{
Func2();
}
catch(...) // Exception abfangen
{
cout << "Func2-Fehler!\n";
throw; // Exception weiterleiten
}
....
}
int main()
{
....
try
{
Func1();
....
}
catch(...)
// Exception abfangen
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (8 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
von Func1(...) abgefangen
und kann dort
entsprechende bearbeitet
werden. Jetzt gilt es 'nur'
noch, das Hauptprogramm
}
vom Auftreten der
Exception zu
benachrichtigen. Und dies
können Sie dadurch
erreichen, dass die im catchBlock in Func1(...)
aufgefangene Exception
durch die Anweisung throw
(jetzt ohne zusätzlichen
Parameter!) erneut
ausgelöst wird und damit
im Hauptprogramm
ebenfalls bearbeitet werden
kann. Wird im Beispiel also
in der Funktion Func2(...)
eine Exception ausgelöst, so
erhalten Sie folgende
Ausgaben:
{
cout << "Fehler aufgetreten\n";
exit(1);
}
....
Func2-Fehler!
Fehler
aufgetreten
Parameter von throw
Erweitern wir jetzt das Exception-Handling. Die bisherigen Beispiele gingen immer davon aus,
dass für alle Exception die innerhalb eines try-Blocks auftraten die auch gleiche Behandlung
durchgeführt wurde. Erweitern wir das Beispiel jetzt in der Art, dass innerhalb eines try-Blocks
Exceptions auftreten können die unterschiedliche Behandlungen erfordern. Hierzu gibt es
verschiedene Lösungswege.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (9 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Die einfachste Art
Exceptions unterscheiden
zu können besteht darin,
der throw-Anweisung
verschiedene Werte
mitzugeben, die aber alle
den gleichen Datentyp
besitzen müssen. Bleibt
noch das kleine Problem,
wie der Exception-Handler
nun an den mitgegebenen
Wert gelangt. Erinnern Sie
sich noch an den
allgemeinen Aufbau der
catch-Anweisung? Sie hat ja
folgende Syntax: catch
(PARAM). Für PARAM
hatten wir bisher immer
drei Punkte eingesetzt.
Diese drei Punkte bedeuten
nichts anderes als: fange
alle Exceptions ab für die
nicht explizit ein anderer
Exception-Handler
definiert wurde. Um nun
den Wert der throwAnweisung auszuwerten,
wird die catch-Anweisung
wie folgt umgeschrieben:
enum Error {ZDIV, SQRT};
void Func()
{
if (....)
throw ZDIV; // 1. Exception auslösen
....
if (....)
throw SQRT; // 2.
}
int main()
{
....
try
{
Func();
}
catch(const Error& e)
{
switch(e)
{
case ZDIV:
cout << "0 Division\n";
break;
case SQRT:
cout << "Wurzelberechnung\n";
break;
}
}
....
}
catch (DTYP PARAM)
DTYP ist der Datentyp des
Werts der throw-Anweisung
und PARAM ein beliebiger
Parametername. Durch
Auswerten des Parameters
innerhalb des ExceptionHandlers kann nun
unterschieden werden,
welche Exception ausgelöst
wurde. So werden im
Beispiel rechts beim
Auftreten der Exceptions
ZDIV bzw. SQRT
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (10 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
entsprechende
Fehlermeldungen
ausgegeben. Vermeiden Sie
aber soweit wie möglich die
Angabe von direkten
Zahlen sondern verwenden
Sie für die verschiedenen
Fehlerfälle besser enumKonstanten. Dies erhöht die
Lesbarkeit und
Wartbarkeit des
Programms erheblich und
vermeidet die mehrfache
Vergabe von gleichen
Fehlernummern.
Verwenden Sie für den Datentyp innerhalb der catch-Anweisung i.d.R eine constReferenz. Sie ersparen sich damit unter Umständen unnötige Kopieroperationen.
Sehen wir uns jetzt die
allgemeine Form an wie
verschiedene Exceptions
unterschieden werden. Wie
vorher bereits erwähnt,
hängt der Datentyp des
Wertes der throwAnweisung eng mit dem
Datentyp des Parameters
der dazugehörigen catchAnweisung zusammen.
Wenn nun bei
verschiedenen throwAnweisungen
unterschiedliche
Datentypen angegeben
werden, so können damit
unterschiedliche ExceptionHandler ausgeführt
werden. Im Beispiel rechts
löst die Funktion Func(...)
eine erste Exception mit
einem int-Wert aus und eine
zweite Exception mit einem
char-Zeiger.
Unterschiedliche
Datentypen in der throwAnweisung bedingen nun
void Func()
{
if (....)
throw 1;
// 1. Exception auslösen
....
if (....)
throw "A"; // 2. Exception auslösen
}
int main()
{
....
try
{
Func();
}
catch(int val)
{
cout << "Fehler " << val << endl;
}
catch(const char *const pT)
{
cout << "Fehler " << pT << endl;
}
....
}
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (11 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
aber auch mehrere
Exception-Handler. Für das
Beispiel benötigen wir jetzt
einen Exception-Handler
zum Auffangen der 'intException' und einen zum
Auffangen der 'char-ZeigerException'. Beachten Sie,
dass beide ExceptionHandler unmittelbar
hintereinander stehen.
Zwischen dem try-Block
und den ExceptionHandlern dürfen keine
anderen Anweisungen
stehen! Die Reihenfolge der
Exception-Handler spielt
(in diesem Fall) keine Rolle.
Die Anzahl der auf einen try- int
Block folgenden Exception- {
Handler ist nicht begrenzt,
d.h. Sie können beliebige
viele Datentypen zum
Auslösen der Exception
verwenden.
Dies entspricht aber
nicht der Praxis.
Dort werden in der
Regel zur
Verarbeitung von
Exceptions
entsprechende
Fehlerklassen
verwendet, was wir
im nächsten
Abschnitt auch
gleich tun werden.
main()
....
try
{
....
}
catch(int val)
{
....
}
catch(const char *const pT)
{
....
}
catch(...)
{
....
}
....
}
Zusätzlich zu den vorhin
aufgeführten typisierten
Exception-Handlern
können Sie immer auch den
Default-Exception-Handler
definieren; das ist der
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (12 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Exception-Handler mit den
drei Punkten innerhalb der
Parameterklammer der
catch-Anweisung. Alle nicht
durch typisierte ExceptionHandler abgefangenen
Exceptions werden dann
dort aufgesammelt. Sie
müssen hierbei nur darauf
achten, dass dieser als
letzter in der 'Reihe' der
Exception-Handler
definiert wird, sonst
erhalten Sie unter
Umständen vom Compiler
eine Fehlermeldung.
Exception-Klassen
Definition der Exception-Klasse
Erweitern wir das bisher
class Ex
doch schon recht
{
komfortable Exceptionshort errNum;
Handling nochmals. Bisher
static const char *pErrText[];
haben wir beim Auslösen
public:
von Exceptions nur
enum {ZDIV, SQRT};
Standard-Datentypen wie
Ex(int err)
int oder char-Zeiger
{
verwendet. Da das
errNum = err;
Exception-Handling aber
}
beliebigen Datentypen
Ex(const Ex& src);
zulässt, können auch
{
Klassen hierfür eingesetzt
errNum = src.errNum;
werden.
}
friend ostream& operator <<
Dazu muss zunächst eine
(ostream& os, const Ex& e);
Klasse für die Exception
};
erstellt werden. Rechts im // Feld mit Fehlermeldungen
Beispiel wird die Klasse Ex const char *Ex::pErrText[] =
für die Behandlung von
{
mathematischen Fehlern
"Division durch 0",
definiert. Die Klasse enthält
"Wurzel aus negativer Zahl"
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (13 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
zum einen die Nummer der
aufgetretenen Exception
(errNum) und zum anderen
die entsprechenden
Fehlermeldungen
(pErrText). Beachten Sie
bitte, dass die Fehlertexte
statisch sind da sie nur
einmal für alle Objekte
dieser Klasse vorhanden
sein müssen. Die
Fehlernummern selbst sind
wieder als enum-Werte
innerhalb der Klasse
definiert. Dadurch, dass
hier sowohl die
Fehlernummern (als enum)
wie auch die Fehlertexte in
einer Klasse
zusammengefasst sind, ist
das Anwenderprogramm
völlig unabhängig von der
internen
Durchnummerierung der
Fehler und dem zum Fehler
gehörigen Fehlertext. Zur
Ausgabe des Fehlertextes
wurde der Klasse noch der
überladene Operator <<
hinzugefügt.
};
ostream& operator <<(ostream& os,
const Ex& e)
{
os << "Fehler " <<
e.pErrText[e.errNum] <<
" aufgetreten!\n";
return os;
}
Sie sollten der Exception-Klassen auch immer den Kopierkonstruktor mitgeben. Beim
Auslösen einer Exception wird u.U. eine Kopie des Exception-Objekts erstellt und an
den Exception-Handler zurückgegeben. Dies gilt insbesondere dann, wenn Sie im
entsprechenden Exception-Handler (catch-Block) keine const-Referenz auf das
Exception-Objekt angegeben haben!
Auslösen und Auffangen von Exceptions mit Exception-Klassen
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (14 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Nach dem die ExceptionKlasse erstellt wurde, kann
es an das Auslösen der
entsprechenden Exception
gehen. Dazu erstellt die
throw-Anweisung ein
Objekt der ExceptionKlasse. Der Konstruktor
unsere Exception-Klasse
erhält als Parameter die
Bezeichnung des
aufgetretenen Fehlers (als
enum-Konstante). Beachten
Sie bitte, dass Sie bei der
Angabe der enumKonstante den
Klassennamen voranstellen
müssen.
Zum Schluss muss die
Exception noch abgefangen
werden. Da der ExceptionHandler jetzt Exception
vom Typ Ex bearbeiten
muss, erhält die catchAnweisung als Parameter
eine const-Referenz auf ein
Objekt dieser Klasse.
Innerhalb des ExceptionHandlers erfolgt dann die
entsprechende
Fehlermeldung durch
Ausgabe des Objekts, was
zum Aufruf des
überladenen
Ausgabeoperators der
Klasse Ex führt.
class Ex
{
....
};
void Func()
{
if (....)
throw Ex(Ex::ZDIV);
....
if (....)
throw Ex(Ex::SQRT);
}
int main()
{
try
{
Func();
}
catch(const Ex& e)
{
cout << e;
}
....
}
Beachten Sie, dass der Exception-Handler eine Objektreferenz erhält und nicht direkt
das Objekt. Würden Sie hier direkt das Objekt angeben, so würde zusätzlich noch der
Kopierkonstruktor der Ausnahmeklasse aufgerufen werden, was aber nur unnötig
Zeit kostet.
Das komplette Beispiel finden Sie auch unter le16\prog\Bexcept1. Dort werden in den
Konstruktoren und im Destruktor noch die Adressen der erstellten bzw. zu löschenden Objekte
ausgegeben (this-Zeiger). Modifizieren Sie den Exception-Handler auch einmal so, dass anstelle
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (15 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
einer Objektreferenz direkt das Objekt verarbeitet wird.
Ableiten von Exception-Klassen
Spielen wir noch ein klein
wenig mit den ExceptionKlassen. Wie Sie in der
Lektion über Ableiten von
Klassen erfahren haben,
lassen sich von einer
Basisklasse weitere Klassen
ableiten. Und dies gilt
natürlich auch für
Exception-Klassen. Im
Beispiel rechts wurde von
der Basisklasse Ex eine
spezielle Klassen ExDiv
abgeleitet um für eine
Division durch Null eine
gesonderte
Fehlerbehandlung
durchführen zu können.
Das Auslösen einer
Exception erfolgt wie
bisher. Beachten müssen
Sie lediglich, dass beim
Erstellen eines ExceptionObjekts, dessen
Konstruktor keine
Parameter besitzt (Aufruf
des StandardKonstruktors), eine leere
Klammer angegeben
werden muss.
// Basis-Exception-Klasse
class Ex
{...};
// Exception-Klasse fuer 0-Division
class ExDiv: public Ex
{...};
void Div(int cal)
{
if (val == 0)
throw ExDiv();
cout << "Division ok!\n";
}
Interessant wird das Ganze beim Abfangen der Exceptions. Exception-Handler vom Typ der
Basisklasse können nämlich auch Exception vom Typ einer abgeleiteten Klasse verarbeiten.
Die Basis-Exception-Klasse wird im Beispiel oben in der Funktion Div(...) nicht
verwendet. In einer realen Anwendung könnten Sie aber auch Exception-Objekte dieser
Klasse einsetzen.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (16 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Wenn Exception-Handler
sowohl für Exceptions vom
Typ einer Basisklasse wie
auch für davon abgeleiteten
Klassen definiert sind,
spielt die Reihenfolge der
Definitionen der ExceptionHandler eine wichtige
Rolle. Eine Exception wird
immer vom ersten
zutreffenden ExceptionHandler nach dem tryBlock bearbeitet. Im
Beispiel rechts wird zuerst
der Exception-Handler für
die abgeleitete Klasse ExDiv
definiert und danach der
für die Basisklasse Ex. Wird
nun in der Funktion Div(...)
eine Exception vom Typ
ExDiv ausgelöst, so wird
diese im Exception-Handler
catch(const ExDiv&)
bearbeitet. Anders würde
der Fall liegen, wenn die
beiden Exception-Handler
in umgekehrter Reihenfolge
definiert wären. Da der
Exception-Handler
catch(const Ex&) dann vor
dem Exception-Handler
catch(const ExDiv&)
definiert wäre, würde die
Exception nun im
Exception-Handler
catch(const Ex&) für die
Basisklasse bearbeitet
werden.
try
{
Div(2);
Div(0);
}
catch(const ExDiv& e) // abgel. Klasse
{
....
}
catch(const Ex& e)
// Basisklasse
{
....
}
Damit haben wir den prinzipiellen Ablauf des Exception-Handlings fertig behandelt. Sehen wir
uns jetzt noch kurz die restlichen Funktionen an die mit dem Exception-Handling zu tun haben.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (17 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
new und Exception
Wie schon bei der
Beschreibung des new
Operators erwähnt, löst
new bei nicht erfolgreicher
Speicherreservierung eine
Exception von Typ
bad_alloc aus. Um nun auf
einen solchen Fehler
reagieren zu können, ist
zunächst mindestens die
Speicherreservierung in
einen try-Block
einzuschließen. Auf den tryBlock folgt dann der
Exception-Handler für die
bad_alloc Exception. Was
im Falle einer nicht
erfolgreichen
Speicherreservierung dann
durchzuführen ist hängt
letztendlich nur von der
Anwendung ab. Wenn Sie
vor der
Speicherreservierung
zusätzlich noch den Zeiger
mit NULL initialisieren,
brauchen Sie beim Aufruf
des delete Operators nicht
einmal abprüfen ob auch
tatsächlich Speicher
reserviert werden konnte.
Ein Aufruf von delete mit
einem NULL-Zeiger ist
erlaubt und führt
bekanntlich keinerlei
Aktion durch.
try
{
short *pData = NULL; // Immer gut
pData = new short[1000];
.... // Arbeiten mit dem Feld
}
catch (const bad_alloc& e)
{
cout << "Out of memory!" << endl;
.... // weitere Fehlerbehandlung
}
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (18 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Unter MICROSOFT VC++ (wenigstens bis zur Version 6.x) löst eine nicht erfolgreiche
Speicherreservierung keine Exception aus sondern liefert einen NULL-Zeiger. Dies
widerspricht ganz klar dem C++ Standard! Stattdessen liefert der new Operator in diesem
Fall einen NULL-Zeiger zurück. Um auch unter MSVC++ standardkonformes Verhalten zu
erhalten, müssen Sie die Datei <cppkurs_verzeichis>\patches\newpatch.cpp zu Ihren Projekten
hinzufügen. Bei allen Beispielen und Lösungen in diesem Kurs wurde dies bereits
durchgeführt. D.h. es erfolgt keine Abprüfung mehr, ob new einen NULL-Zeiger
zurückliefert. Beachten Sie, dass Sie damit dann zwar standardkonform sind, jedoch der
'normale' MSVC++ Programmierer im Fehlerfall mit einem NULL-Zeiger rechnet (leider).
Siehe auch Online-Hilfe zu MSVC unter: PRB: Operator New Doesn't Throw bad_alloc
Exception on Failure
Sonstige Exception-Funktionen
Beginnen wir mit der Beantwortung der Frage: was passiert wenn eine Exception ausgelöst
wird und kein entsprechender Exception-Handler definiert ist? In diesem Fall wird
standardmäßig die Funktion terminate(...) aufgerufen, die innerhalb der Standard-Bibliothek
definiert ist. Diese Funktion beendet das Programm defaultmäßig. Sie können jedoch eine
eigene terminate(...) Funktion definieren, die Sie mit der Funktion
terminate_handler set_terminate(terminate_handler ph) throw();
einhängen. Die einzuhängende Funktion ph ist eine Funktion vom Typ terminate_handler.
terminate_handler ist intern als void(*pfnVFunc)( ) definiert. Innerhalb Ihrer eigenen Funktion
müssen Sie das Programm dann selbst beenden, entweder durch Aufruf der Funktion exit(...),
abort(...) oder der ursprünglichen terminate(...) Funktion.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (19 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Bisher ebenfalls noch nicht void Div(int val) throw(ExZDiv, char*)
behandelt wurde die
{
Möglichkeit, dass bei der
if (val == 0)
Definition einer Funktion
throw "Divisionsfehler";
die weiterzugebenden (also
cout << "Division ok!\n";
außerhalb der Funktion
}
aufzufangenden)
Exceptions eingeschränkt
werden kann. Um diese
Einschränkung zu
spezifizieren, wird die
Funktionsdefinition wie
folgt erweitert:
RTYP FNAME (P1,
P2,...) throw(Ex1,
Ex2,..)
{....}
Innerhalb der Klammer der throw-Anweisung stehen die Datentypen der Exceptions, die die
Funktion nach außen liefern kann. Im obigen Beispiel kann die Funktion Div(...) Exceptions
vom ExZDiv und char-Zeiger weiterleiten. Was aber passiert hier nun, wenn eine Exception
ausgelöst wird deren Typ nicht innerhalb dieser throw-Anweisung angegeben ist? In diesem Fall
wird die Funktion unexpected(...) aufgerufen, die wiederum ebenfalls terminate(...) aufruft (welch
Überraschung). Aber auch diese Standardfunktion können Sie durch Aufruf von
unexpected_handler set_unexpected(unexpected_handler ph) throw();
überschreiben. Die einzuhängende Funktion ph ist eine Funktion jetzt vom Typ
unexpected_handler, der intern ebenfall als void(*pfnVFunc)( ) definiert ist. Innerhalb Ihrer
eigenen Funktion müssen Sie das Programm dann wieder selbst beenden.
Der MICROSOFT Compiler VC++ bis zur Version 6.xx unterstützt das Einschränken
von weiterzugebenden Exceptions nicht!
Und damit wären wir wieder beim Beispiel und der Übung angelangt.
Beispiel und Übung
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (20 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Das Beispiel:
Dieses doch etwas größere Beispiel zeigt Ihnen nochmals die Verwendung von Templates und
dem Exception-Handling auf. Damit das Beispiel trotz seiner Größe noch halbwegs
übersichtlich bleibt, gehen wir hier schrittweise vor, d.h. es wird eine Klasse nach der
anderen entwickelt und ganz zum Schluss noch das dazugehörige Hauptprogramm
Im Beispiel wird eine allgemein gültige Klasse SafeArray entwickelt die eine SafeArray
realisiert. Wenn Sie bisher aufmerksam den Kurs durchgearbeitet haben wissen Sie, dass ein
SafeArray vom Prinzip her einem Feld entspricht, jedoch die indizierten Zugriffe darauf auf
Plausibilität abgeprüft werden. Erfolgt ein Zugriff außerhalb des erlaubten Bereichs
(Feldgröße), so soll nun eine entsprechende Exception ausgelöst werden.
In diesem SafeArray werden später Zeiger auf Fensterobjekte abgelegt werden.
Fangen wir das Beispiel also mit der Implementierung der Exception-Klasse für das
SafeArray an. Dazu gilt es zuerst zu überlegen, welche Fehlerfälle auftreten können. Zum
einen kann die untere oder obere Grenze des Feldes beim indizierten Zugriff unter- bzw.
überschritten werden. Zum anderen kann aber auch beim Anlegen des Speichers für das
SafeArray-Feld ein Fehler in der Form auftreten, dass nicht genügend Speicher vorhanden
ist. Und damit können wir die Klasse für die SafeArray-Exception wie folgt implementieren:
Die Exception-Klasse des SafeArray
// Ausnahmeklasse fuer SafeArray-Fehler definieren
// ===============================================
class ArrayEx
{
public:
enum eArrayEx{UNDERFL,OVERFL,NOMEM}; // Fehlernummern
private:
enum eArrayEx error;
// akt. Fehlernummer
static const char* const pText[];
// Fehlertexte
public:
ArrayEx(eArrayEx);
friend ostream& operator << (ostream& os, const ArrayEx& ex);
};
// Die Fehlertexte
const char *const ArrayEx::pText[] =
{
"Index kleiner 0",
"Zugriff ueber Feld hinaus",
"Nicht genuegend Speicher"
};
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (21 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
// Definition der Memberfunktionen
// Konstruktor, erhaelt Nummer des akt. Fehlers
inline ArrayEx::ArrayEx(eArrayEx err)
{
error = err;
}
// Ueberschriebener Ausgabeopertor zur Ausgabe
// des Fehlertextes
inline ostream& operator << (ostream& os, const ArrayEx& ex)
{
os << "Fehler " << ex.pText[ex.error] << endl;
return os;
}
So, als nächstes gilt es das Template CSafeArray für die SafeArray-Klasse zu implementieren.
Da unser SafeArray keine fixe Größe haben soll, wird der Speicher für das Datenfeld
dynamisch im Konstruktor angelegt. Damit nachher das Exception-Handling für das Beispiel
im Konstruktor getestet werden kann, begrenzen wir die max. Feldgröße auf 100 Elemente.
Wird versucht mehr Elemente anzulegen, lösen wir die Exception für Speichermangel aus.
Das Gleiche gilt auch wenn tatsächlich einmal nicht mehr genügend Speicher vorhanden ist.
In diesem Fall fangen wir im Konstruktor die bad_alloc Exception ab und lösen im ExceptionHandler unsere eigene Exception hierfür aus. Beachten Sie bitte, dass bei nicht vollständiger
Bearbeitung des Konstruktors niemals der Destruktor aufgerufen wird!
Für den indizierten Zugriff muss noch der Indexoperator [ ] überladen werden, der beim
Unter- bzw. Überschreiten der Feldgrenzen eine entsprechende Exception auslöst. Damit
ergibt sich folgende Implementation für das Template:
Das SafeArray-Template
// Vereinfachtes Template fuer SafeArray
// =====================================
template <typename T> class SafeArray
{
short size;
// Groesse des Feldes
T
*pData;
// Zeiger auf das Datenfeld
T& operator = (T&)
// Zuweisungen verbieten!
{return *this;}
SafeArray(const SafeArray&)
// Kopieren verbieten!
{}
public:
inline SafeArray(short s);
~SafeArray();
T& operator [] (int index);
};
// Definition der Memberfunktionen
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (22 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
// Konstruktor
template<typename T> inline SafeArray<T>::SafeArray(short s)
{
// Falls mehr als 100 Eintraege, Fehler ausloesen
if (s>100)
throw ArrayEx(ArrayEx::NOMEM);
// Speicher für Datenfeld reservieren
try
{
pData = new T[s];
}
// Im Fehlerfall SafeArray-Ausnahme ausloesen
catch (std::bad_alloc&)
{
throw ArrayEx(ArrayEx::NOMEM);
}
// Feldgroesse merken
size = s;
}
// Destruktor
template<typename T> inline SafeArray<T>::~SafeArray()
{
delete [] pData;
}
// Ueberladener Indexopertor fuer Index-Ueberwachung
template<typename T> T& SafeArray<T>::operator [] (int index)
{
// Falls Index kleiner 0, SafeArray-Ausnahme ausloesen
if (index<0)
throw ArrayEx(ArrayEx::UNDERFL);
// Falls Index über oberer Feldgrenze, SafeArray-Ausnahem
ausloesen
if (index>=size)
throw ArrayEx(ArrayEx::OVERFL);
// Referenz auf gewuenschtes Element zurueckliefern
return pData[index];
}
Kommen wir nun zu den Fensterobjekten. Da bei der Erstellung und Manipulation eines
Fensters die übergebenen Fensterdaten auf Plausibilität überprüft werden sollen, wird auch
hier eine entsprechende Exception-Klasse für die Fehlerfälle definiert. Sie entspricht bis auf
die Fehlernummern und -texten der Exception-Klasse für SafeArray-Fehler.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (23 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Die Exception-Klasse der Fensterklasse
// Ausnahmeklasse fuer Fensterfehler definieren
// ============================================
class WindowEx
{
public:
enum eWinEx{ILLPOS,ILLSIZE,NOMEM};
// Fehlernummern
private:
enum eWinEx error;
// akt. Fehlernummer
static const char* const pText[]; // Fehlertexte
public:
WindowEx(eWinEx);
friend ostream& operator << (ostream& os, const WindowEx& ex);
};
// Die Fehlertexte
const char *const WindowEx::pText[] =
{
"Position unzulaessig",
"Unzulaessige Groesse",
"Nicht genuegend Speicher"
};
// Definitionen der Memberfunktionen
// Konstruktor, erhaelt die Nummer des akt. Fehlers
inline WindowEx::WindowEx(eWinEx err)
{
error = err;
}
// Ueberschriebener Ausgabeoperator zur Ausgabe
// des Fehlertextes
inline ostream& operator << (ostream& os, const WindowEx& ex)
{
os << "Fehler " << ex.pText[ex.error] << endl;
return os;
}
Implementieren wir jetzt die Fensterklasse. Die Fensterklasse enthält nur die Position und
Größe des Fensters sowie einen Fenstertitel. Die Fensterposition soll über die
Memberfunktion MoveWin(...) und die Fenstergröße über ResizeWin(...) veränderbar sein.
Abgefangen werden sollen negative Fensterkoordinaten und Fenstergrößen, also wenn als
Fensterbreite z.B. der Wert -1 angegeben wird. In diesen Fällen werden entsprechende
Exceptions ausgelöst. Zusätzlich soll die Bedingung gelten, dass das Fenster nicht den Bereich
0...1024 in X-Richtung und 0...768 in Y-Richtung verlassen darf. Für die Ausgabe der
Fensterdaten wird wieder der Ausgabeoperator << überladen.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (24 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Die Fensterklasse
// Fensterklasse definieren
// ========================
class Window
{
short xPos, yPos;
// Position
short width, height;
// Groesse
std::string title;
// Fenstertitel
public:
Window(short x, short y, short w, short h, const char *pT =
"Defaultfenster");
void MoveWin(short x, short y);
void ResizeWin(short w, short h);
friend ostream& operator << (ostream& os, const Window& win);
};
// Definition der Memberfunktionen
// ===============================
// Konstruktor
Window::Window(short x, short y, short w, short h, const char
*pT):
title(pT)
{
// Falls Position negativ, Ausnahme ausloesen
if ((x<0) || (y<0))
throw WindowEx(WindowEx::ILLPOS);
// Falls Groessenwert negativ, Ausnahme ausloesen
if ((w<0) || (h<0))
throw WindowEx(WindowEx::ILLSIZE);
// Falls Fenster die Postion 1024/768 ueberschreitet
// Ausnahme ausloesen
if ((x+w>1024) || (y+h>768))
throw WindowEx(WindowEx::ILLSIZE);
// Fensterdaten merken
xPos = x; yPos = y;
width = w; height = h;
}
// Fenster verschieben
// Falls Position negativ oder Fenster die Position
// 1024x768 ueberschreitet, Ausnahme ausloesen
void Window::MoveWin(short x, short y)
{
if ((x<0) || (y<0))
throw WindowEx(WindowEx::ILLPOS);
if ((x+width>1024) || (y+height>768))
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (25 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
throw WindowEx(WindowEx::ILLSIZE);
xPos = x; yPos = y;
}
// Fenstergroesse verandern
// Falls Groessenangabe negativ oder Fenster die Position
// 1024x768 ueberschreitet, Ausnahme ausloesen
void Window::ResizeWin(short w, short h)
{
if ((w<0) || (h<0))
throw WindowEx(WindowEx::ILLSIZE);
if ((xPos+w>1024) || (yPos+h>768))
throw WindowEx(WindowEx::ILLSIZE);
width = w; height = h;
}
// Fensterdaten ausgeben
ostream& operator << (ostream& os, const Window& win)
{
os << "Fenstertitel: " << win.title << endl;
os << "Position: (" << win.xPos << ',' << win.yPos << ")\n";
os << "Groesse: (" << win.width << ',' << win.height << ")\n";
return os;
}
So, damit haben wir alles definiert was zu definieren ist und wir können zum
Hauptprogramm übergehen.
Im Hauptprogramm wird zunächst ein SafeArray für die Aufnahme von 10 Fensterzeigern
definiert. Anschließend werden in einem try-Block drei Fenster erstellt. Die ersten beiden
Fenster werden mit den Indizes 0 und 1 im SafeArray abgelegt. Bei der Ablage des dritten
Fensters wird dagegen versucht das 20. Element zu belegen, was zum Auslösen einer
Exception führt. Beachten Sie bitte, dass im Exception-Handler dieses dritte Fenster noch
explizit zerstört werden muss. Die Erzeugung des Fensters verlief ja fehlerfrei, nur die Ablage
des Fensterzeigers führte zu einer Exception.
Anschließend wird das erste Fenster zweimal hintereinander verschoben. Beim zweiten
Verschieben wird wieder eine Exception ausgelöst, da das Fenster dann den zulässigen
Bereich von 1024x768 verlassen würde.
Zum Schluss werden die beiden Fenster wieder gelöscht und danach versucht, ein SafeArray
mit 200 Einträgen zu erstellen. Dies schlägt ebenfalls fehl, da im Konstruktor des SafeArrays
CArray die maximale Anzahl der Einträge auf 100 begrenzt wurde.
Das unten stehende Beispiel finden Sie auch unter: le16\prog\Bexcept2.
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (26 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Die Programmausgabe:
Fenstertitel: Kleines Fenster
Position: (0,0)
Groesse: (640,480)
Fenstertitel: Grosses Fenster
Position: (100,100)
Groesse: (800,600)
Fehler Zugriff ueber Feld hinaus
Fenstertitel: Kleines Fenster
Position: (200,200)
Groesse: (640,480)
Fehler Unzulaessige Groesse
Fehler Nicht genuegend Speicher
Das Hauptprogramm:
// Hauptprogramm
// =============
int main()
{
// SafeArray fuer 10 Fensterzeiger erstellen
SafeArray<Window*> WindowArray(10);
// Hilfszeiger
Window* pWindow;
// Versuche nun Fenster im SafeArray abzulegen
try
{
// Kleines Fenster ablegen und Daten ausgeben
pWindow = new Window(0,0,640,480,"Kleines Fenster");
WindowArray[0] = pWindow;
cout << *WindowArray[0];
// Groesses Fenster ablegen und Daten ausgeben
pWindow = new Window(100,100,800,600,"Grosses Fenster");
WindowArray[1] = pWindow;
cout << *WindowArray[1];
// Riesiges Fenster anlegen
pWindow = new Window(0,0,1024,768,"Riesiges Fenster");
// Ueber SafeArray hinausgreifen -> Ausnahme!
WindowArray[20] = pWindow;
cout << *WindowArray[20];
}
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (27 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
// Exception-Handler fuer SafeArray-Fehler
catch(const ArrayEx& CAE)
{
// Fehlermeldung ausgeben
cout << CAE << endl;
// und letztes Fenster wieder loeschen!
delete pWindow;
}
// Exception-Handler fuer Fenster-Fehler
catch(const WindowEx& CWE)
{
// Fehlermeldung ausgeben
cout << CWE << endl;
}
// Versuche Fenster zu verschieben
try
{
// Endposition danach auf 840x680
WindowArray[0]->MoveWin(200,200);
cout << *WindowArray[0];
// Endposition danach auf 1240x1080 -> Ausnahme!
WindowArray[0]->MoveWin(400,400);
cout << *WindowArray[0];
}
// Fensterfehler auffangen
catch(const WindowEx& CWE)
{
cout << CWE << endl;
}
// Fenster nun wieder loeschen
delete WindowArray[0];
delete WindowArray[1];
// Versuche grosses SafeArray anzulegen
try
{
SafeArray<short> CMyShorts(200);
cout << "SafaArray fuer 100 shorts angelegt!\n";
}
// SafeArray-Fehler auffangen
catch(const ArrayEx& CAE)
{
cout << CAE << endl;
}
}
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (28 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Die Übung:
Schreiben Sie ein Template zur Realisierung eines Stacks. Wenn Sie bisher den Kurs
aufmerksam verfolgt haben so wissen Sie ja bereits zu genüge, dass ein Stack ein
Datenbereich zur Zwischenspeicherung von Werten/Objekten ist. Dabei gilt folgende Regel:
was zuletzt auf dem Stack abgelegt wird, wird auch als erstes wieder ausgelesen (LastIn FirstOut LIFO). Die Größe des Stacks soll bei der Stackdefinition angegeben werden können.
Wird aber versucht mehr als 100 Elemente auf dem Stack abzulegen (Stackgröße > 100), so
soll eine Exception ausgelöst werden. Den Typ der Exception können Sie beliebig festlegen.
Zum Ablegen von Werten wird die (bekannte) Memberfunktion Pop(...) verwendet. Wird
diese Memberfunktion aufgerufen obwohl der Stack belegt ist, so soll ebenfalls eine Exception
ausgelöst werden.
Die weitere Memberfunktion Push(...) dient zum Auslesen von Werten. Ist kein Wert mehr
auf dem Stack abgelegt und die Memberfunktion wird aufgerufen, so soll wiederum eine
Exception ausgelöst werden.
Legen Sie dann im Hauptprogramm einen Stack zur Ablage von short-Werten an und füllen
Sie diesen komplett, d.h. warten Sie auf das Auftreten einer Exception. Anschließend ist der
Stack vollständig zu leeren.
Erstellen Sie dann einen zweiten Stack (Typ beliebig) für 1000 Einträge. Dies müsste dann
zum Auslösen einer Exception führen.
Denken Sie auch daran, dass Sie die Exception für eine fehlerhafte Speicherreservierung in
Zukunft immer abfangen sollten.
Die Programmausgabe:
Fuelle Stack:
FEHLER:Stack belegt
Leere Stack:
14,13,12,11,10,
FEHLER:Stack leer
Lege riesigen Stack an:
FEHLER:Max. Stackgroesse ueberschritten
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (29 von 30) [17.05.2005 11:40:53]
Ausnahme-Behandlungen
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen,
so finden Sie die Lösung unter loesungen\le16\Lexcept.
So, damit hätten Sie diese doch umfangreiche Lektion geschafft. In der nächsten Lektion
erfahren Sie, wie Sie zur Laufzeit den Typ eines Objekts bestimmen können.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/le16_02.htm (30 von 30) [17.05.2005 11:40:53]
Laufzeit-Typinformationen
Laufzeit-Typinformationen
Die Themen:
Einleitung
RTTI
Beispiel und Übung
Einleitung
In manchen Fällen kann es sehr hilfreich sein, Informationen über den Datentyp eines Objekts (oder auch
einer einfachen Variablen) zu erhalten. Hierzu bietet C++ die Typinformation zur Laufzeit (Runtime type
information = RTTI) an. Wie und für was diese Typinformation eingesetzt werden kann, das soll nun
wieder anhand eines Beispiels erläutert werden.
Doch zuvor noch eine kleine Information für alle MICROSOFT VC++ Anwender.
Wenn Sie die im Folgenden beschriebene RTTI in eigenen Programmen einsetzen wollen, müssen
Sie diese beim MICROSOFT VC++ Compiler explizit freigeben. Sie erreichen den Dialog hierzu
unter dem Menüpunkt Projekt-Einstellung. Wählen Sie dann im Dialog den Tabulator C/C++ und
als Kategorie Programmiersprache C++ aus. Markieren Sie dann die Option Run-Time-TypeInformation (RTTI) aktivieren.
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (1 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
RTTI
Das Ausgangsbeispiel
Doch nun zum Beispiel. Rechts sehen
die bekannten Klassen GBase als
Basisklasse für alle Grafikobjekte
sowie die beiden von GBase
abgeleiteten Klassen Circle und Bar für
die eigentlichen Grafikobjekte. Im
Hauptprogramm wird dann ein Feld
vom Typ Zeiger auf Basisklasse
definiert in dem die Zeiger auf die
Grafikobjekte abgelegt werden.
// Klassendefinitionen
class GBase
{....};
class Circle: public GBase
{....};
class Bar: public GBase
{....};
// Hauptprogramm
int main()
{
// Basiszeigerfeld definieren
Ich hoffe Sie erinnern sich noch daran,
GBase *pGraphic[3];
dass in einem Basisklassenzeiger auch
// 3 Grafikobjekte im Feld ablegen
Zeiger auf abgeleitete Objekte abgelegt
pGraphic[0] = new Bar(...);
werden können?!
pGraphic[1] = new Circle(...);
pGraphic[2] = new Bar(...);
....
Soweit nicht Ungewöhnliches.
// Objekte hier wieder loeschen
}
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (2 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
Wir erweitern jetzt die Basisklasse um
eine Memberfunktion Align(...) die es
uns gestatten soll, zwei Grafikobjekte
auszurichten. Die Ausrichtung soll der
Einfachheit halber in der Art erfolgen,
dass die X-Positionen gleichgesetzt
werden.
void GBase::Align(GBase *pGObj)
{
pGObj->xPos = xPos; // xPos gleichsetzen
}
// Mögliche Aufrufe
pGraphic[0]->Align(pGraphic[1]);
pGraphic[0]->Align(pGraphic[2]);
Unterhalb der Definition der
Memberfunktionen rechts sehen Sie
zwei mögliche Aufrufe der neuen
Memberfunktion. Dort werden die
Grafikobjekte zwei und drei auf die
gleiche X-Position wie das erste
Grafikobjekt ausgerichtet.
Diese neue Memberfunktion Align(...) wollen wir gleich so abändern, dass nur noch gleiche Grafiken
ausgerichtet werden können, d.h. Sie können zwei Bar oder zwei Circle Objekte ausrichten aber nicht mehr
ein Bar Objekt an einem Circle Objekt. Und hier kommt die RTTI ins Spiel.
RTTI
Die RTTI
Um zur Laufzeit feststellen zu können, ob die auszurichtenden Objekte vom gleichen Typ sind, muss die
Memberfunktion Align(...) Informationen über die Typen (RTTI) der auszurichtenden Objekt erhalten.
Damit ein Objekt aber eine RTTI enthält, muss dessen Klasse polymorph sein, d.h. sie muss mindestens
eine virtuelle Memberfunktion besitzen. Im Zusammenhang mit virtuellen Memberfunktionen erinnern Sie
sich vielleicht noch daran, dass eine in einer Basisklasse als virtuelle deklarierte Memberfunktion in allen
abgeleiteten Klassen automatisch ebenfalls virtuell ist. Wenn Sie also für eine Klasse RTTI benötigen, muss
entweder die Klasse selbst oder aber eine ihrer Basisklassen mindestens eine virtuelle Memberfunktion
enthalten.
Aber sehen wir uns jetzt die RTTI an. Um RTTI einsetzen zu können muss die Datei <typeinfo> mittels
include-Anweisung zunächst eingebunden werden. Diese Datei enthält die Definition der für die RTTI
benötigten Struktur type_info.
Um die Typinformation eines Objekts letztendlich zu erhalten, rufen Sie den Operator
type_info& typeid(DTYP)
auf. DTYP ist die Variable oder das Objekt, dessen Typinformation bestimmt werden soll. Für DTYP kann
aber auch ein bestimmter Datentyp oder Klassenname eingesetzt werden. Als Ergebnis liefert der Operand
eine Referenz auf die Struktur type_info, die die Typinformation enthält. Sie müssen diese Struktur nicht
selbst auswerten, sondern es stehen dafür verschiedene überladene Operatoren bereit, die in der
nachfolgenden Tabelle aufgeführt sind.
Operator
Bedeutung
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (3 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
bool operator == (const type_info& COp2) Vergleicht ob zwei Datentypen identisch sind
bool operator != (const type_info& COp2) Vergleicht ob zwei Datentypen unterschiedlich sind
bool before(const type_info& COp2)
Funktion für interne Zwecke
const char* name()
Liefert einen Zeiger auf den Datentyp als ASCII String;
der String selbst ist aber nicht standardisiert, sodass
unterschiedliche
Compiler unterschiedlich Strings liefern können.
Damit können Sie z.B. mit folgender Anweisung vergleichen, ob das Objekt AnyGraph zur Klasse Bar
gehört:
if (typeid(AnyGraph) == typeid(Bar))
....
Vergleichen von Datentypen
Und damit lässt sich die
void GBase::Align(GBase* pGObj)
Memberfunktion Align(...) der
{
Basisklasse nun wie rechts angegeben
// Grafiktypen vergleichen
definieren. Die Memberfunktion
if (typeid(*this) != typeid(*pGObj))
Align(...) erhält als Parameter einen
{
Zeiger auf das auszurichtende Objekt.
// Ungleich, Meldung ausgeben und
Zuerst wird in der Memberfunktion
// Exception ausloesen
der Datentyp des aktuellen Objekts
cout << "Fehler beim Ausrichten " <<
typeid(*this) mit dem des
typeid(*this).name();
auszurichtenden Objekts
cout << '-' << typeid(*pGObj).name()
typeid(*pGObj) verglichen. Beachten
<< endl;
Sie hier bitte, dass die Zeiger
throw "Ungleiche Grafikelemente!";
dereferenziert werden müssen, da Sie
}
ja den Datentyp des Objekts und nicht
// Gleiche Grafiktypen, dann ausrichten
den des Zeigers ermitteln wollen. Der
pGObj->xPos = xPos;
Datentyp des Zeigers ist hier immer
pGObj->yPos = yPos;
GBase*! Sind die Datentypen
}
unterschiedlich, so werden die Namen
der beiden Datentypen ausgegeben
(Aufruf der Memberfunktion name( ) )
und eine Exception ausgelöst. Bei
gleichen Datentyp werden die Objekte
dann entsprechend der Vorgabe
ausgerichtet.
Nachfolgend sehen Sie ein Beispiel für die Ausgabe der Memberfunktion, wenn versucht wird, ein Circle
Objekt an einem Bar Objekt auszurichten.
Fehler beim Ausrichten class Bar-class Circle
Wie Sie der Ausgabe entnehmen können, liefert name(...) die Klassennamen als ASCII-Strings zurück.
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (4 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
Die obige Ausgabe triff nur zu, wenn Sie das Beispiel mit dem VC++ Compiler erstellt haben. Haben
Sie den MinGW-Compiler verwendet, so ergibt sich ein etwas anderer Klassenname.
Das Vergleichen von Datentypen funktioniert übrigens auch dann, wenn die Objekte über KlassenTemplates erstellt wurden.
Und damit sind wir auch schon beim Beispiel und der Übung.
Beispiel und Übung
Das Beispiel:
Das Beispiel zeigt das vollständig Listing zu den vorherigen Auszügen. Im Hauptprogramm werden drei
Grafikobjekte erstellt, zwei Rechtecke und ein Kreis. Anschließend wird versucht, das zweite Rechteck
und den Kreis am ersten Rechteck auszurichten. Da die Memberfunktion Align(...) der Basisklasse GBase
nur die Ausrichtung von gleichen Grafikobjekten zulässt, führt die Ausrichtung des Kreises zu einer
Exception.
Das unten stehende Beispiel finden Sie auch unter: le16\prog\Brtti.
Die Programmausgabe:
-- Zeichne Rechteck --Position: 10,10
Groesse: 100,100
-- Zeichne Rechteck --Position: 30,20
Groesse: 300,200
-- Zeichne Kreis --Position: 50,50
Radius: 100
Fehler beim Ausrichten class CBar-class CCircle
*** Exception: Ungleiche Grafikelemente!***
-- Zeichne Rechteck --Position: 10,10
Groesse: 100,100
-- Zeichne Rechteck --Position: 10,10
Groesse: 300,200
-- Zeichne Kreis --Position: 50,50
Radius: 100
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (5 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
Das Programm:
// C++ Kurs
// Beispiel zu RTTI
//
// Zuerst Dateien einbinden
#include <iostream>
#include <typeinfo>
using std::cout;
using std::endl;
// Definition der Klasse GBase
// ============================
// GBase dient als Basisklasse fuer die Grafikelemente
// Bar und Circle
// HINWEIS: Damit die Runtime-Typinformation eingebunden wird
// muss die Klasse polymorph sein, d.h. mindestens eine
// virtuelle Memberfunktion besitzen!
class GBase
{
short
xPos, yPos;
// Position der Grafik
public:
GBase(short x, short y);
virtual void Draw() const;
void Align(GBase&);
};
// Definition der Memberfunktionen
// Konstruktor
GBase::GBase(short x, short y)
{
xPos = x; yPos = y;
}
// Ausgabe der Koordinaten
void GBase::Draw() const
{
cout << "Position: " << xPos << ',' << yPos << endl;
}
// Memberfunktion Align() der Klasse GBase
// Richtet zwei gleiche Grafikelemente aus
// Wird versucht zwei verschiedene Grafikelemente auszurichten,
// so wird eine Ausnahme ausgeloest
void GBase::Align(GBase& pGraphic)
{
// Grafiktypen vergleichen
if (typeid(*this) != typeid(pGraphic))
{
// Ungleich, Meldung ausgeben und Ausnahme ausloesen
cout << "Fehler beim Ausrichten " << typeid(*this).name();
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (6 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
cout << '-' << typeid(pGraphic).name() << endl;
throw "Ungleiche Grafikelemente!";
}
// Gleiche Grafiktypen, dann ausrichten
pGraphic.xPos = xPos;
pGraphic.yPos = xPos;
}
// Definition der Klasse Bar
// ==========================
class Bar: public GBase
{
short
width, height;
public:
Bar(short, short, short, short);
void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
Bar::Bar(short x, short y, short w, short h):
GBase(x, y)
{
width = w; height = h;
}
// Gibt die Daten des Rechtecks aus
void Bar::Draw() const
{
cout << "-- Zeichne Rechteck ---\n";
GBase::Draw();
cout << "Groesse: " << width << "," << height << endl;
}
// Definition der Klasse Circle
// =============================
class Circle: public GBase
{
short
radius;
public:
Circle(short, short, short);
void Draw() const;
};
// Definition der Memberfunktionen
Circle::Circle(short x, short y, short r):
GBase(x, y)
{
radius = r;
}
// Gibt die Daten des Kreises aus
void Circle::Draw() const
{
cout << "-- Zeichne Kreis ---\n";
GBase::Draw();
cout << "Radius: " << radius << endl;
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (7 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
}
//
// HAUPTPROGRAMM
// =============
int main()
{
// 3 Grafikobjekte anlegen
GBase* pGBase1 = new Bar(10,10, 100, 100);
GBase* pGBase2 = new Bar(30,20, 300, 200);
GBase* pGBase3 = new Circle(50,50,100);
// Grafiken ausgeben
pGBase1->Draw();
pGBase2->Draw();
pGBase3->Draw();
// Versuche Grafiken auszurichten
try
{
pGBase1->Align(*pGBase2); // 2 Bar-Objekte ausrichten; zulaessig
pGBase2->Align(*pGBase3); // Bar und Circle ausrichten; unzulaessig
}
// Ausnahme vom Ausrichten auffangen
catch (const char *const pT)
{
cout << "*** Exception: " << pT << "***\n";
}
// Grafiken nochmals ausgeben
pGBase1->Draw();
pGBase2->Draw();
pGBase3->Draw();
// Grafiken loeschen
delete pGBase1;
delete pGBase2;
delete pGBase3;
}
Die Übung:
Erstellen Sie Klassen für die drei Grafikobjekte Kreis, Rechteck und Polygon (Vieleck). Alle drei Klassen
sind von einer Basisklasse für Grafiken abzuleiten. Alle Klassen sollen nur eine Memberfunktion
Draw(...) besitzen, die einen entsprechenden Text ausgibt (siehe nachfolgende Ausgabe).
Schreiben Sie dann eine Funktion die zufällig eines der drei Grafikobjekte erstellt und den Zeiger auf das
erstellte Objekt zurück gibt. Eine solche Funktion, die nur zur Erstellung eines Objekts dient, wird auch
als Klassenfabrik (class factory) bezeichnet. Da die Funktion sowohl einen Kreis wie auch ein Rechteck
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (8 von 9) [17.05.2005 11:40:57]
Laufzeit-Typinformationen
oder Polygon erzeugen kann muss Sie einen bestimmten Returntyp besitzen. Als kleiner Hinweis dazu:
das Ganze hat etwas mit abgeleiteten Klassen und Zeigern zu tun.
Definieren Sie dann im Hauptprogramm ein Feld zur Aufnahme von zehn Zeigern auf die so erzeugten
Grafikobjekte und füllen Sie dieses Feld mit Hilfe der oben beschriebenen Klassenfabrik. Geben Sie die
Grafikobjekte zur Kontrolle aus.
Anschließend bestimmen Sie, wie oft jedes der drei Grafikelemente erzeugt wurde und geben das
Ergebnis dann aus.
Die Programmausgabe:
Rechteck zeichnen
Polygon zeichnen
Polygon zeichnen
Kreis zeichnen
Polygon zeichnen
Polygon zeichnen
Rechteck zeichnen
Rechteck zeichnen
Polygon zeichnen
Polygon zeichnen
1 Objekte von Typ class CCircle
3 Objekte von Typ class CRect
6 Objekte von Typ class CPolygon
Das Programm:
Ja das sollen Sie jetzt selbst schreiben. Wenn Sie die CD-ROM Version des Kurses besitzen, so finden Sie
die Lösung unter loesungen\le16\Lrtti.
In der nächsten Lektion werden wir uns nochmals mit dem Thema Typkonvertierung befassen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/le16_03.htm (9 von 9) [17.05.2005 11:40:57]
Typkonvertierung auf Klassenzeiger
Typkonvertierung auf Klassenzeiger
Die Themen:
static_cast Operator
dynamic_cast Operator
static_cast Operator
Wie schon an Anfang des Kurses erfahren haben, können Sie mittels des static_cast Operators
u.a. einen void-Zeiger in einen anderen beliebigen Zeiger konvertieren.
Aber static_cast kann noch
mehr. Mithilfe des
static_cast Operators
können Sie BasisklassenZeiger auf Zeiger einer
davon abgeleiteten Klasse
konvertieren (erste
static_cast Konvertierung
rechts). Das Casting von
einer Basisklasse auf eine
abgeleitete Klasse wird
auch als downcasting
bezeichnet. Eine
Konvertierung eines
anderes Zeigers (außer der
vorhin erwähnten voidKonvertierung) führt schon
beim Übersetzen des
Programms zu einem
Fehler. Im Beispiel rechts
wird bei der zweiten
Konvertierung z.B.
// Basisklasse
class Base
{...} *pBase;
// Abgeleitete Klasse
class Der: public Base
{...} derObj;
// Beliebige andere Klasse
class Any
{...};
...
// Basis-Referenz auf abgeleitetes Objekt
Base baseRef& = derObj;
// Zeiger auf abgeleitete Klasse
Der *pDer = static_cast<Der*>(pBase);
// Das geht aber nicht!
Any *pAny = static_cast<Any*>(pBase);
// Auch mit Referenzen
Der& refDer = static_cast<Der&>(baseRef);
http://www.cpp-tutor.de/cpp/le16/le16_04.htm (1 von 3) [17.05.2005 11:40:58]
Typkonvertierung auf Klassenzeiger
versucht, einen Zeiger auf
die Basisklasse Base in
einen Zeiger auf die Klasse
Any zu konvertieren. Da
Any aber nicht von Base
abgeleitet ist führt dies zu
einem Übersetzungsfehler.
Außer der Konvertierung
von Zeigern kann
static_cast auch
entsprechende Referenzen
konvertieren (letzte
static_cast Konvertierung).
dynamic_cast Operator
Der dynamic_cast Operator
dient einzig und allein zum
Konvertieren von Zeigern
und Referenz zwischen
abgeleiteten Klassen. Er
wird hauptsächlich dann
eingesetzt, wenn ein
Basisklassenzeiger auf
einen Zeiger einer
abgeleiteten Klasse
konvertiert werden soll
(Downcasting). Diese
Konvertierung können Sie
ja schon mit dem static_cast
Operator durchführen.
Aber der dynamic_cast
Operator bietet natürlich
noch eine zusätzliche
Funktionalität. Die
Arbeitsweise des
dynamic_cast Operators
lässt sich am Besten wieder
anhand eines Beispiels
demonstrieren. Im Beispiel
rechts werden drei Klassen
definiert, wobei die Klasse
Win als Basisklasse für die
// Klassendefinitionen
class Win
{....};
class FrameWin: public Win
{....};
class AnotherWin: public Win
{....};
// Definitionen
Win *pWin1 = new FrameWin;
Win *pWin2 = new AnotherWin;
FrameWin *pConv;
// Konvertierungen ok
pConv = dynamic_cast<FrameWin*>(pWin1);
// Konvertierung schlägt fehl
// und liefert als Ergebnis NULL!
pConv = dynamic_cast<FrameWin*>(pWin2);
http://www.cpp-tutor.de/cpp/le16/le16_04.htm (2 von 3) [17.05.2005 11:40:58]
Typkonvertierung auf Klassenzeiger
beiden anderen Klassen
FrameWin und AnotherWin
fungiert.
Anschließend werden zwei Basisklassenzeiger definiert in denen jeweils ein Zeiger auf ein
Objekt von Typ FrameWin und AnotherWin abgelegt wird. Zusätzlich wird noch ein FrameWinZeiger definiert. Sehen wir uns nun die Konvertierungen an. Die erste Konvertierung
konvertiert den ersten Basisklassenzeiger pWin1 in einen FrameWin-Zeiger. Da in pWin1 auch
ein Objekt vom Typ FrameWin abgelegt ist liefert diese Konvertierung keinen Fehler zurück.
Die zweite Konvertierung hingegen schlägt fehl! Hier wird versucht den Basisklassenzeiger
pWin2 in einen FrameWin-Zeiger zu konvertieren. Da aber in pWin2 ein Zeiger auf ein
AnotherWin Objekt abgelegt ist, liefert der dynamic_cast Operator hier als Ergebnis einen NULLZeiger zurück. D.h. der dynamic_cast Operator überprüft zur Laufzeit, ob die Konvertierung
des Basisklassenzeigers typsicher durchgeführt werden kann.
Wird anstelle eines Zeigers eine Referenz mittels dynamic_cast konvertiert und die
Konvertierung ist nicht erlaubt, so löst der dynamic_cast Operator eine Exception vom Typ
bad_cast aus. Wie Sie solche Exceptions behandeln können haben Sie ja bereits erfahren.
Der dynamic_cast Operator arbeitet nur mit Zeigern auf polymorphe Klassen, d.h. die
Klasse muss mindestens eine virtuelle Memberfunktion enthalten.
Und damit beenden wir diese (sehr) kurze Lektion über die zusätzlichen Typkonvertierungen
und befassen uns in der nächsten Lektion mit den so genannten Namenräumen.
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/le16_04.htm (3 von 3) [17.05.2005 11:40:58]
Namensbereiche
Namensbereiche
Die Themen:
Einleitung
Definition eines Namensraums
Anwendung von Namensräumen
Zugriff auf Namensräume
Geschachtelte und anonyme Namensräume
Einleitung
In dieser Lektion wollen wir uns mit Namensbereichen befassen und damit nun auch endlich die
Anweisung using xxx; näher betrachten.
Namensräume wurden dazu geschaffen, Namenskonflikte von Variablen, Klassen usw.
aufzulösen. Auch diese Problematik lässt sich wiederum am Besten anhand eines Beispiels
demonstrieren.
Im Beispiel rechts wird eine
Variable var und eine
Klasse Any definiert. Auf
beide wird im
Hauptprogramm
zugegriffen. Bis hierher
noch nichts Unbekanntes.
Worauf es im Folgenden
ankommt sind die Namen.
// Definitionen
int var;
class Any
{....};
// Hauptprogramm
int main()
{
Any myObj;
var = 10;
....
}
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (1 von 8) [17.05.2005 11:41:00]
Namensbereiche
Jetzt wollen Sie in Ihrem
Programm z.B.
eine gekaufte
Klassenbibliothek
einsetzen. Zu dieser
Klassenbibliothek gehört
natürlich auch eine HeaderDatei (im Beispiel clib.h),
die auszugsweise rechts
dargestellt ist. In dieser
Header-Datei ist aber
unglücklicherweise
ebenfalls eine Klasse Any
und eine Variable var
definiert. Was nun kommt
ahnen Sie vielleicht schon.
Header-Datei clib.h der erworbenen Bibliothek
#ifndef CLIB_H
#define CLIB_H
int var;
class Any
{....};
#endif
Definition eines Namensraums
Um die Klassenbibliothek
in Ihrem Programm
verwenden zu können,
müssen Sie
selbstverständlich die
Header-Datei clib.h
einbinden. Und damit
können Sie das Programm
nicht mehr fehlerfrei
übersetzen, da sowohl in
der Header-Datei clib.h wie
auch in Ihrem Programm
die Variable var und die
Klasse Any definiert sind.
Ein erster Lösungsansatz
bestände nun darin, dass
Sie in Ihrem Programm die
Namenskonflikte durch
entsprechende
Umbennungen auflösen.
Dies kann aber je nach
Programmgröße sehr
umfangreich, und was noch
wichtiger ist, auch
fehleranfällig sein.
#include "clib.h"
// Definitionen
int var;
// Das erzeugt jetzt Fehler!
class Any
{....};
// Hauptprogramm
int main()
{
Any myObj;
var = 10;
....
}
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (2 von 8) [17.05.2005 11:41:00]
Namensbereiche
Vergessen Sie z.B. nur ein
einziges Vorkommen von
var in Ihrem Programm
umzubenennen, dann
rechnen Sie fast unbemerkt
mit einer 'falschen'
Variable weiter.
Und genau an dieser Stelle setzen die Namensbereiche ein. Die allgemeine Syntax zur Definition
eines Namensraums lautet:
namespace [NNAME]
{
.... // Definitionen und Deklarationen
}
NNAME gibt den Namen des Namensraums an. Und alle in einem Namensraum
vorgenommenen Definitionen und Deklarationen sind auch nur innerhalb dieses Namensraums
gültig! Gleichnamige Definitionen/Deklarationen in unterschiedlichen Namensräumen erzeugen
somit keinen Namenskonflikt mehr.
Anwendung von Namensräumen
Um den Namenskonflikt
mittels Namensräume in
unserem Beispiel
aufzulösen stehen zwei
Lösungswege zur
Verfügung. Im ersten Fall
legen wir die Header-Datei
clib.h in einen eigenen
Namensraum und im
zweiten Fall unsere eigenen
Definitionen und
Deklarationen. Beide
Lösungen sind gleichwertig.
Da nun in beiden Fällen die
Definitionen von var und
Any in verschiedenen
Namensräumen erfolgen,
gibt es keinen
Namenskonflikt mehr. Auf
Lösung 1:
namespace CLIB
{
#include "clib.h"
}
int var;
class Any
{....};
Lösung 2:
#include "clib.h"
namespace cppkurs
{
int var;
class Any
{....};
}
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (3 von 8) [17.05.2005 11:41:00]
Namensbereiche
den Zugriff auf die Klasse
Any bzw. auf die Variable
var kommen wir gleich zu
sprechen.
Die Anzahl der
Namensräume innerhalb
eines Programms ist nicht
begrenzt.
Und noch ein Hinweis:
Kommen Sie hier nicht auf
die Idee, in die käuflich
erworbene Header-Datei
clib.h einen Namensraum
einzubauen. Beim nächsten
Update der Bibliothek (mit
neuer clib.h-Datei) wäre der
Namensraum dann wieder
weg!
Wird ein bereits
bestehender
Namensraum erneut
definiert, so wird der
bisherige Namensraum um
die Deklarationen und
Definitionen im erneuten
Namensraum erweitert. Im
Beispiel rechts enthält der
Namensraum cppkurs alle
Deklarationen und
Definitionen aus den beiden
Header-Dateien mylib.h und
yourlib.h.
Header-Datei mylib.h
namespace cppkurs
{
...
}
Header-Datei yourlib.h
namespace cppkurs
{
...
}
Zugriff auf Namensräume
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (4 von 8) [17.05.2005 11:41:00]
Namensbereiche
So, sehen wir uns jetzt die
Zugriffe auf die
Namensräume an. Hierzu
gibt es ebenfalls
verschiedene Wege. Eine
Lösung besteht darin, dass
beim Zugriff auf die
Deklarationen und
Definitionen zuerst der
jeweilige Namensraum,
dann der Zugriffsoperator
:: und danach die
entsprechende
Deklaration/Definition
angegeben wird. Rechts im
Beispiel wurde die HeaderDatei clib.h in einen eigenen
Namensraum clib gelegt.
Wie Sie dann auf die darin
enthaltene Variable var
zugreifen und ein Objekt
der darin enthaltenen
Klasse Any definieren ist
ebenfalls rechts dargestellt.
Eine weitere Lösung für
den Zugriff besteht darin,
den Namensbereich einer
Variablen, Funktion,
Objekts usw. explizit für
die restlichen Anweisungen
des aktuellen Blocks
vorzugeben. Dies erfolgt
mittels des Schlüsselworts
using.
using
NNAME::MNAME;
namespace clib
{
#include "clib.h"
}
int var;
// Eigene Definitionen
class Any
// und Deklarationen
{....};
// Zugriff auf var aus Namensraum clib
clib::var = 10;
// Definition eines Objekts vom Typ Any
// aus dem Namensraum clib
clib::Any myObj;
// Zugriff auf eigenes var
var = 33;
// Für Any standardmäßig clib-Namensraum
// setzen
using clib::Any;
// Objekt aus clib-Namensraum definieren
Any myObj;
// Objekt aus eignem Namensraum definieren
::Any anotherObj;
// Standard var ist aus clib Namensraum
using clib::var;
var = 10;
// Nun var aus globalem Namensraum setzen
::var = -10;
NNAME ist der Name des
Namensraums und MNAME
der Name der jeweiligen
Deklaration/Definition. Die
using-Anweisung erlaubt
aber nur die Angabe eines
Members. Sollen mehrere
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (5 von 8) [17.05.2005 11:41:00]
Namensbereiche
Member aus einem
Namenraum explizit
vorgegeben werden, so
müssen entsprechend viele
using-Anweisungen hierfür
verwendet werden.
Dies ist auch der Grund, warum Sie bei den bisherigen Beispielen und Übungen immer using
std::cout; und using std::endl; schreiben mussten.
Und noch einmal weil's so wichtig ist: Die Gültigkeit einer using-Anweisung endet am Ende des
aktuellen Blocks {...}!
Sollen aber alle Member eines Namensraums eingeblendet werden, so wird dafür eine etwas
andere Form der using-Anweisung verwendet:
using namespace NNAME;
NNAME ist wiederum der
Name des einzublendenden
Namensraums. Schreiben
Sie im Programm z.B. using
namespace std; so wird der
komplette Namensraum std
eingeblendet, mit allen Vorund Nachteilen. Wollen Sie
einmal den Namensraum
std nicht komplett
einblenden, so müssen Sie
vor allen Zugriffen auf die
Bibliothek diesen
Namensraum explizit
angeben, oder die using
std::xxx; Anweisung
verwenden.
#include <iostream>
int main()
{
std::cout << "Hallo!"
}
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (6 von 8) [17.05.2005 11:41:00]
Namensbereiche
Sie können in einem
Programm auch mehrere
Namensräume mittels using
namespace einblenden.
Diese Namensräume sind
dann additiv, d.h. es sind
alle Member aus beiden
Namenräumen gültig. Soll
dieses additive Verhalten
vermieden werden, so
setzen Sie die usingAnweisung einfach in einen
Block {...}. Wie bereits
vorhin erwähnt, endet das
Einblenden eines
Namensraums immer an
der Grenze des aktuellen
Blocks.
using namespace std;
using namespace cppkurs;
// Ab hier können alle Member des Namensraums
// std und cppkurs direkt verwendet werden
....
Geschachtelte und anonyme Namensräume
Ebenfalls möglich, wenn
auch selten angewandt, ist
es, Namenräume zu
schachteln. Beim expliziten
Zugriff auf ein Member
müssen dann die Namen
beider Namenräume (von
außen nach innen)
angegeben werden.
namespace outer
{
namespace inner
{
int var;
}
}
// Expliziter Zugriff auf var
outer::inner::var = 10;
Mit Hilfe eines
namespace
Namensraums können Sie {
auch Variablen,
int var;
Funktionen usw. einer
....
'Übersetzungseinheit' (in
void Func1(...)
der Regel ist dies eine
{
Quelldatei) nach außen hin
....
verbergen. Innerhalb der
}
Übersetzungseinheit kann
....
dann auf alle Member des }
Namensraums wie gewohnt
zugegriffen werden, aber
außerhalb der
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (7 von 8) [17.05.2005 11:41:00]
Namensbereiche
Übersetzungseinheit haben
Sie keinen Zugriff auf den
Inhalt des Namensraums.
So verhält sich im Beispiel
rechts die Variable var
innerhalb der Quelldatei
wie eine globale Variable,
aber nach außen hin ist sie
nicht sichtbar. Würde in
einer anderen Quelldatei
z.B. die Anweisung extern
int var stehen, so würde dies
beim Linken des
Programms zu einem
Fehler führen.
Das obige Beispiel enthält noch eine kleine Besonderheit, den unbenannten oder anonymen
Namensraum. Da auch nur innerhalb des Namensraums auf die Member des Namensraums
zugegriffen werden soll, benötigen Sie hier keinen Namen für den Namensraum.
So, und damit hätten wir
fast diese Lektion beendet,
es fehlt nur noch ein
winziger Hinweis:
Namensräume dürfen nicht
innerhalb von Blöcken
definiert werden. Die
einzige Ausnahme dabei
bilden die vorhin
erwähnten geschachtelten
Namensräume.
Aber das geht nicht!
....
void Func(...)
{
namespace wrong
{
....
}
}
So und das war's auch schon. Ohne Beispiel und ohne Übung, damit's zum Schluss der Lektion
nicht zu stressig wird!
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/le16_05.htm (8 von 8) [17.05.2005 11:41:00]
Ende Kapitel 5
Ende Kapitel 5
Herzlichen Glückwunsch!
Sie haben damit das fünfte Kapitel dieses
Kurses
fertig bearbeitet.
Das nächste Kapitel befasst sich nochmals mit Templates
und der Standard C++ Bibliothek
Copyright © 2004
Senden Sie E-Mails mit Fragen oder Kommentaren zu dieser Website an: mailto:[email protected]
Wolfgang Schröder, Lerchenweg 23, D-72805 Lichtenstein, Tel: +49 7129 6470
http://www.cpp-tutor.de/cpp/le16/k5_end.htm [17.05.2005 11:41:02]
Template Vertiefung
Template Vertiefung
Die Themen:
Default-Datentypen für Templates
Non-type Template-Parameter
Explizite Typangabe von Template-Argumenten
Spezialisierung bei Funktions-Templates
Templates als Parameter
Klassen-Templates und Ableitungen
Member-Templates
Template Spezialisierungen
Partielle Template-Spezialisierung
Partielle Template-Spezialisierung und non-type Argumente
Template-Template-Parameter
Templates als Compile-Zeit Ausdrücke
WICHTIGER HINWEIS!
Die Ausführungen zum Thema Templates in dieser Lektion beziehen sich auf den C++ Standard.
Jedoch weisen die Compiler wie MS VC++ 6.0, BORLAND C++ 5.5.1 und MinGW 3.2 in Sachen
Template-Techniken teilweise noch einige 'Schwächen' auf, sodass nicht alle Beispiele mit allen
Compilern übersetzbar sind. Wenn einer dieser Compiler von Standard abweicht, so wird dieses
aber selbstverständlich erwähnt. Alle Beispiele wurden zumindest mit dem aktuellen ComeauCompiler auf ihre Standard-Konformität hin überprüft.
So, genug der Vorworte, steigen wir hinab in die die Wunderwelt der Template-Programmierung und beginnen
den Anfang recht gemütlich, mit einer kurzen Wiederholung von zwei Template-Themen.
Default-Datentypen für Templates
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (1 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Häufig werden Template-Objekte mit dem
gleichen Template-Datentyp definiert. So
werden im Beispiel rechts unter anderem zwei
Template-Objekte mit dem Datentyp int
definiert.
template <typename T>
class Any
{...};
Man kann sich in solchen Fällen etwas
Schreibarbeit sparen, wenn man bei der
Definition des Klassen-Templates diesen
Datentyp als Default-Datentyp vorgibt. Die
Vorgabe des Default-Datentyps erfolgt in der
Art, dass nach dem Template-Argument ein
Gleichheitszeichen folgt und dann der DefaultDatentyp. Bei der Definition eines TemplateObjekts mit diesem Default-Datentyp kann
dann die Angabe des Datentyps entfallen. Auf
jeden Fall aber müssen die beiden (nun leeren)
spitzen Klammern immer mit angegeben
werden.
template <typename T=int>
class Any
{...};
Any<int> objOne;
...
Any<double> objTwo;
...
Any<int> objThree;
Any<> objOne;
// definiert Any<int>
...
Any<double> objTwo; // definiert Any<double>
...
Any<> objThree;
// definiert Any<int>
Das Beispiel:
Das Beispiel realisiert ein Array, dessen Größe sich zur Programmlaufzeit an die im Array abzulegenden
Elemente anpasst. Das Array ist als Template realisiert um beliebige Datentypen aufnehmen zu können.
Wird beim Zugriff auf ein bestimmtes Element (Aufruf des überladenen Index-Operators []) über die obere
Feldgrenze hinaus zugegriffen, so wird das Array entsprechend erweitert. Da das erweiterte Array mithilfe des
new Operators allokiert wird, werden die neuen Elemente in Array zunächst mit dem Standard-Konstruktor des
Elements initialisiert (siehe auch Erstellen von dynamischen Objektfeldern ) Bei einem Zugriff auf Elemente mit
einem Index kleiner 0 wird eine std::range_error Exception ausgelöst.
Standardmäßig wird das Array für die Aufnahme von int-Daten angelegt.
Die im Beispiel verwendete Klasse Demo dient nur zur Kontrolle, ob das Array auch Objekte richtig
verarbeitet. Die Klasse Demo ist einem eigenen Verzeichnis Demo abgelegt und vollständig innerhalb der
Header-Datei demo.h definiert (siehe auch Hinweise in der Datei demo.h).
Das unten stehende Beispiel finden Sie auch unter: le17\prog\def_param.
Die Programmausgabe:
Das 10. Element ist: 0
Und das komplette SafeArray:
4316716, 4316716, 0, 52117, 44, 0, 66, 0, 0, 0,
, erstes Objekt, ,
, erstes Objekt, , , , zweites Objekt,
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (2 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Das Programm:
//
//
//
//
//
//
//
C++ Kurs
Beispiel fuer Default-Datentyp in Templates
Realisiert ein dynamische DynArray
ACHTUNG!! Werden in dem Feld Objektzeiger abgelegt
so müssen die dazugehörigen Objekte explizit wieder
zerstört werden. Der dtor gibt nur die Zeiger frei
und die durch den Zeiger referenzierten Objekt.
#include <iostream>
#include <stdexcept>
// Demo-Klasse einbinden
#include "..\demo\demo.h"
using std::cout;
using std::endl;
// Klasse implementiert einen gesicherten Zugriff auf
// ein Feld. Wird beim Schreiben ueber das Ende des Feldes
// hinausgegriffen, so wird das Feld entsprechend erweitert
// Standardmaessig wird das Feld fuer die Ablage von int// Daten angelegt
template <typename T = int>
class DynArray
{
T* data;
// Zeiger auf Datenfeld
int size;
// Groesse des Datenfelds
public:
// ctor, reserviert optional ein Feld mit einer
// bestimmten Anzahl von Elementen
DynArray(size_t reserveData = 0);
// dtor
~DynArray();
// Indexoperator fuer Array-Zugriff
T& operator[] (int index);
// Ausgabe des DynArrays
void PrintArray();
};
// ctor
// Standardmaessig wird kein Feld allokiert und der Datenzeiger
// mit NULL initialisiert
template <typename T>
DynArray<T>::DynArray(size_t reserveData): data(NULL), size(0)
{
// Falls Datenfeld angelegt werden soll
if (reserveData)
{
// Datenfeld reservieren
data = new T[reserveData];
size = reserveData;
}
}
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (3 von 45) [17.05.2005 11:41:11]
Template Vertiefung
// dtor, gibt reserviertes Feld frei
template <typename T>
DynArray<T>::~DynArray()
{
delete [] data;
}
// Ueberladener Indexoperator fuer den Zugriff
// auf die Elemente im DynArray
template <typename T>
T& DynArray<T>::operator[] (int index)
{
// Falls Index < 0 dann eine Standard-Exception ausloesen
if (index<0)
throw std::range_error("Index kleiner 0!");
// Falls Index groesser als die Anzahl der Feldelement
if (index>=size)
{
// Neues Feld mit der Groesse 'Index+1' reservieren
// +1 hier unbedingt notwendig, da der z.B. der
// Index 10 das 11. Element adressiert!
T* newData = new T[index+1];
// Bisherige Daten in neues Feld uebernehmen
for (int copyOld=0; copyOld<size; copyOld++)
newData[copyOld] = data[copyOld];
// Altes Feld nun entfernen
delete [] data;
// Und Zeige auf neues Feld abspeichern
data = newData;
// Nicht vergessen: die Groesse korrigieren
size = index+1;
}
// Gewuenschtes Element zurueckliefern
return data[index];
}
// Komplettes Feld ausgeben
template <typename T>
void DynArray<T>::PrintArray()
{
for (int index=0; index<size; index++)
cout << data[index] << ", ";
cout << endl;
}
// Hauptprogramm
// =============
int main()
{
// DynArray mit Default-Datentyp 'int' und 5 Elementen anlegen
DynArray<> intArray(5);
// 5. Element beschreiben
intArray[4] = 44;
// 7. Element beschreiben -> fuehrt zur Erweiterung des DynArrays
intArray[6] = 66;
// 10. Element ausgeben -> fuehrt zur Erweiterung des DynArrays
cout << "Das 10. Element ist: " << intArray[9] << endl;
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (4 von 45) [17.05.2005 11:41:11]
Template Vertiefung
// Komplettes Feld ausgeben (10 Elemente)
cout << "Und das komplette DynArray:\n";
intArray.PrintArray();
// DynArray mit 3 Demo-Objekten anlegen
DynArray<Demo> objArray(3);
// 2. Element beschreiben
objArray[1] = Demo("erstes Objekt");
// Komplettes Feld ausgeben (3 Elemente)
objArray.PrintArray();
// 6. Element beschreiben -> fuehrt zur Erweiterung des DynArrays
objArray[5] = Demo("zweites Objekt");
// Komplettes Feld ausgeben (6 Elemente)
objArray.PrintArray();
}
Non-type Template-Parameter
Non-type Template-Parameter sind TemplateParameter die zur Übersetzungszeit
berechenbar sein müssen. Sie können sich
unter einem non-type Parameter eine Art
Konstante vorstellen, die Sie der TemplateKlasse mit auf den Weg geben. Innerhalb
einer Template-Klasse werden non-type
Template-Parameter wie Konstanten
behandelt. Mithilfe von non-type TemplateParametern können Sie zum Beispiel die
Größe eines Feldes innerhalb einer TemplateKlasse festlegen.
Non-type Template-Parameter können, wie
auch alle anderen Template-Parameter, einen
Default-Wert besitzen. Beachten müssen Sie
bei non-type Template-Parametern nur, dass
sie, wie bereits erwähnt, zur Übersetzungszeit
berechenbar sein müssen (damit fallen z.B.
Adressen von lokalen Variablen/Objekten
weg) und dass keine Gleitkommatypen
zugelassen sind.
Werden Template-Memberfunktionen von
Klassen-Templates mit non-type Parametern
außerhalb des Klassen-Templates definiert, so
muss der non-type Template-Parameter bei
der Memberfunktionen-Definition ebenfalls
mit angegeben werden.
template <typename T, int SIZE>
class Any
{
T array[SIZE];
...
};
template <typename T, int SIZE=50>
class Any
{
T array[SIZE];
...
};
template <typename T, int SIZE=50>
class Any
{
...
void DoAny(short);
};
template <typename T, int SIZE>
void Any<T,SIZE>::DoAny(short v)
{...}
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (5 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Bei der Definition eines Objekts eines KlassenTemplates mit non-type Template-Parametern
muss für den non-type Parameter innerhalb
der spitzen Klammer ein Ausdruck angegeben
werden. Ausnahme: für den non-type
Template-Parameter gibt es einen
Defaultwert. Somit erhält rechts das Objekt
obj1 ein short-Feld mit 100 Elementen und das
Objekt obj2 ein double-Feld mit den
standardmäßigen 50 Elementen.
template <typename T, int SIZE=50>
class Any
{
T array[SIZE]
...
};
Any<short,100> obj1;
Any<double> obj2;
Das Beispiel:
Das Beispiel realisiert ein Klassen-Template für ein SafeArray. Die im SafeArray abzuspeichernden Daten werden
in einem Feld abgespeichert. Die maximale Anzahl der abzuspeichernden Daten wird bei der Definition eines
SafeArray-Objekts als non-type Template-Parameter spezifiziert. Standardmäßig können in einem SafeArray 50
Werte abgespeichert werden.
Beachten Sie im Beispiel auch, wie beim Auslesen der Daten aus dem SafeArray in main(...) der Datentyp der
Zielvariablen definiert wird. Dazu wird zum einen der Datentyp des SafeArrays über eine typedef-Anweisung
definiert und zum anderen innerhalb des Klassen-Template SafeArray des Datentyp der im SafeArray abgelegten
Daten durch eine weitere typedef-Anweisung definiert.
Durch die Verwendung von typedefs haben Sie den Vorteil, dass der Datentyp der im SafeArray abgelegten Daten
relativ einfach geändert werden kann. Sie müssen dazu nur die typedef-Anweisung für das SafeArray
entsprechend anpassen. Die Anpassung des Datentyps der Zielvariablen erfolgt dann automatisch über die
typdef-Anweisung innerhalb des Templates. Versuchen Sie einmal, das SafeArray für die Ablage der int-Daten so
abzuändern, dass double-Daten abgespeichert werden können.
Das unten stehende Beispiel finden Sie auch unter: le17\prog\NonTypeParam.
Die Programmausgabe:
Das 10. Element ist: 0
Und das komplette SafeArray:
4316716, 4316716, 0, 52117, 44, 0, 66, 0, 0, 0,
, erstes Objekt, ,
, erstes Objekt, , , , zweites Objekt,
Das Programm:
// C++ Kurs
// Beispiel fuer non-type Template-Parameter
#include
#include
#include
#include
<iostream>
<stdexcept>
<sstream>
"..\demo\demo.h"
using std::cout;
using std::endl;
// Template fuer SafeArray
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (6 von 45) [17.05.2005 11:41:11]
Template Vertiefung
// Defaultmaessig wird ein int-Array mit
// 50 Eintraegen realisiert
// HINWEIS: Der gleiche Mechanismus koennte auch dafuer verwendet
// werden, um nicht 0-basierende Arrays zu implementieren
template <typename T=int, int SIZE=50>
class SafeArray
{
T array[SIZE];
public:
typedef T value_type;
T& operator[](int index);
};
// Implementierung des ueberladenen Index-Operators
template <typename T, int SIZE>
T& SafeArray<T, SIZE>::operator[] (int index)
{
// Falls Zugriff ausserhalb der Grenzen, out_of_range werfen
if ((index<0) || (index>=SIZE))
{
std::ostringstream os;
os << "SafeArray: Erlaubt [0," << SIZE << "), Index war " << index;
throw std::range_error(os.str());
}
// Referenz auf Element zurueckgeben
return array[index];
}
// Voll-spezifiziertes SafeArray fuer Demo-Objekte und 10 Elemente
typedef SafeArray<Demo,10> arrayType1;
// Standard SafeArrey fuer 50 int-Elemente
typedef SafeArray<> arrayType2;
// Hauptprogramm
// =============
int main()
{
// Array mit Demo-Objekten definieren
arrayType1 myArray;
// Zugriff testen, die zweite Anweisung greift ueber
// das Array hinaus
try
{
myArray[0] = Demo("Element 0");
cout << "1. Element: " << myArray[0] << endl;
arrayType1::value_type anyVal = myArray[99];
}
catch(std::range_error& ex)
{
cout << ex.what() << endl;
}
// Default
arrayType2
// Zugriff
// vor das
Safe-Array fuer int-Daten mit 50 Elemente definieren
yourArray;
testen, die zweite Anweisung fuehrt einen Zugriff
erste Element durch
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (7 von 45) [17.05.2005 11:41:11]
Template Vertiefung
try
{
arrayType2::value_type anyVal = yourArray[49];
yourArray[-1] = 10;
}
catch(std::range_error& ex)
{
cout << ex.what() << endl;
}
}
Explizite Typangabe bei Template-Argumenten
Normalerweise kann der Datentyp von
Template-Argumenten eines FunktionsTemplates aus den Datentypen der
übergebenen Parameter abgeleitet werden.
template <typename T>
void DoAny(T val)
{...};
float var1;
DoAny(var1);
...
char var2;
DoAny(var2);
Ein Problem taucht hierbei jedoch immer
dann auf, wenn der Returntyp des FunktionsTemplates selbst ein Template-Argument ist.
In diesem Fall kann der Datentyp des
Rückgabewerts nicht aus dem
Funktionsaufruf hergeleitet werden. So kann
im Beispiel rechts das Funktions-Template
Calculate(...) einen beliebigen Datentyp
zurückliefern, der sich für die Zuweisung nur
irgendwie in einen double-Datentyp
konvertieren lassen muss.
In einen solchen Fall müssen Sie den Datentyp
des Template-Arguments explizit vorgeben.
Die Vorgabe von Datentypen erfolgt in der
Art, dass beim Aufruf der Funktion nach dem
Funktionsnamen innerhalb von spitzen
Klammern die entsprechenden Datentypen
angegeben werden. Im Beispiel rechts
entspricht damit das Argument T1 dem
Datentyp double und T2 dem Datentyp float.
// erzeugt DoAny(float val)
// erzeugt DoAny(char val)
template <typename T1, typename T2>
T1 Calulate(T2 val)
{...};
// Was wird hier zugewiesen?
float var1;
double var2 = Calculate(var1);
template <typename T1, typename T2>
T1 Calulate(T2 val)
{...};
// Explizite Typangabe fuer Template-Argumente
float var1;
double var2 = Calculate<double,float>(var1);
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (8 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Kann einer der Datentypen implizit beim
Aufruf durch den Compiler hergeleitet
werden (so wie im Beispiel rechts der
Datentyp des Parameters T2), so reicht die
expliziten Angabe der vorhergehenden
Datentypen aus. Der Datentyp des zweiten
Template-Arguments wird dann aus dem
Datentyp des Funktionsparameters hergeleitet
(hier float).
template <typename T1, typename T2>
T1 Calulate(T2 val)
{...};
// Explizite Typangabe des 1. Arguments
float var1;
double var2 = Calculate<double>(var1);
Das Beispiel:
Das Beispiel definiert ein Funktions-Template zur byteweisen 'Übertragung' eines beliebigen Datentyps. Die
einzelnen Bytes der Daten werden für die Übertragung nach ASCII konvertiert. Damit der Empfänger der
Daten diese auswerten kann, wird zusätzlich der Datentyp der übertragenen Daten noch mit übertragen.
Werden über diese Template-Funktion Variablen übertragen, so kann der Compiler aus dem Datentyp der
Variablen die zu erzeugende Funktion herleiten. Anders sieht es aus, wenn Literale (unbenannte Konstanten)
übertragen werden. Da Literale einen Standard-Datentyp besitzen ( ), muss zum Beispiel bei der Übertragung
eines char-Literals entweder das Literal beim Aufruf der SendTo(...) Funktion mittels Typkonvertierung explizit
in ein char konvertiert werden oder aber explizit die SendTo(...) Funktion für ein char-Datum aufgerufen
werden.
Das unten stehende Beispiel finden Sie auch unter: le17\prog\TempFunc.
Die Programmausgabe:
sending
sending
sending
sending
sending
sending
bytes:
bytes:
bytes:
bytes:
bytes:
bytes:
int 0a 00 00 00
char 41
int 61 00 00 00
long 61 00 00 00
char 61
short 34 12
Hinweis: Die Datentypkennung kann je nach Compiler abweichen.
Das Programm:
// C++ Kurs
// Beispiel zur expliziten Typ-Definition
// fuer Funktions-Templates
#include <iostream>
using std::cout;
using std::endl;
// Funktions-Template zur byteweisen 'Uebertragung' eines
// Wertes in ASCII-Form
template <typename T>
void SendTo(std::ostream& os, T value)
{
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (9 von 45) [17.05.2005 11:41:11]
Template Vertiefung
os << "sending bytes: ";
os << typeid(T).name() << ' ';
// char-Zeiger auf Datenbeginn aufsetzen
const char* p = reinterpret_cast<const char*>(&value);
// Alle Bytes durchlaufen
for (size_t i=0; i<sizeof(T); i++)
{
// Low-Byte nach ASCII konvertieren
char lowByte = (*p)&0x0f;
lowByte = (lowByte>9) ? lowByte+0x57 : lowByte|0x30;
// Heigh-Byte nach ASCII konvertieren
char highByte = ((*p)>>4)&0x0f;
highByte = (highByte>9) ? highByte+0x57 : highByte|0x30;
// Datum in ASCII-Form 'uebertragen'
os << highByte << lowByte << ' ';
// Zeiger auf naechstes Byte
p++;
}
cout << endl;
}
// Hauptprogramm
// =============
int main()
{
// Auszugebende Daten
int intVal = 10;
char charVal = 0x41;
// entspricht 'A'
// Aufruf der Template-Funktion
// Der Template-Datentyp ergibt sich aus dem Datentyp des Parameters
SendTo(cout,intVal);
SendTo(cout,charVal);
// Aufruf der Template-Funktion mit Konstanten
// Standardmaessig werden Konstanten als ints behandelt
// Soll SendTo(...) mit anderem Datentyp aufgerufen werden,
// so kann entweder der Datentyp explizit festgelegt werden
// (2. Aufruf) oder aber der Template-Datentyp explizit
// angegeben werden (3. + 4. Aufruf)
SendTo(cout,0x61);
SendTo(cout,0x61L);
SendTo<char>(cout,0x61);
SendTo<short>(cout,0x1234);
}
Spezialisierung von Funktions-Templates
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (10 von 45) [17.05.2005 11:41:11]
Template Vertiefung
In manchen Fällen kann es notwendig sein,
dass Sie ein Funktions-Template für
bestimmte Datentypen überschreiben
(spezialisieren). Das Funktions-Template
Any(...) rechts gibt zum Beispiel in Inhalt des
übergebenen Parameters auf die StandardAusgabe aus.
template <typename T>
void Any (const T& data)
{
cout << data << endl;
}
...
int val = 10;
Any(val);
// Gibt val aus
Was aber passiert, wenn an Any(...) ein Zeiger ...
übergeben wird? Nun, dann wird wie bisher
Any(&val);
// Gibt Adresse von val aus
auch der Inhalt des übergebenen Datums
ausgegeben, also die Adresse die im Zeiger
abgelegt ist.
Vermutlich aber sollte nicht der Inhalt des Zeigers ausgegeben werden sondern die Daten, auf die der Zeiger
verweist. Was ist also zu tun?
Die Antwort auf diese Frage laut:
Spezialisierung des Funktions-Templates. D.h.
man definiert ein zweites Funktions-Template
für den Datentyp, für den diese TemplateFunktion aufgerufen werden soll. Bei der
Auflösung des Funktionsaufrufs sucht der
Compiler immer nach einer Funktion oder
einem Funktions-Template, dessen
Parametertypen möglichst genau zu den
Parametertypen des Funktionsaufrufs passt
(best match). Dieser Vorgang wird auch als
template argument deduction bezeichnet. Soll
also für Zeiger ein eigenes FunktionsTemplate definiert werden, so ist innerhalb
der Parameterklammer der TemplateFunktion ein Zeiger anzugeben. Beachten Sie
genau, wo rechts der Zeiger (d.h. das '*')
steht!
template <typename T>
void Any (const T& data)
{...}
template <typename T>
void Any (T* data)
{
cout << *data <<endl;
}
...
int val = 10;
Any(val);
// Gibt val aus
...
Any(&val);
// Gibt deref. Zeiger aus
Vielleicht hätten Sie in einem ersten Schritt zur Lösung dieses Problems versucht, das Funktions-Template durch
eine entsprechende Any(...) Funktion zu überladen. Diese ist generell auch möglich, jedoch hätten Sie dann für
jeden Zeigertyp (d.h. für einen char*, einen short*, einen int* usw.) eine eigene Funktion schreiben müssen!
Wenn Sie Funktions-Templates spezialisieren, achten Sie immer auf die Datentypen! Sehen Sie sich dazu die
nachfolgenden Aufrufe von Template-Funktionen an:
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (11 von 45) [17.05.2005 11:41:11]
Template Vertiefung
// Funktions-Templates
template <typename T>
void Any(T& val)
{...}
// Hauptprogramm
int main()
{
int v1 = 10;
const int v2 = 20;
template <typename T>
void Any(const T& val)
{...}
Any(v1);
Any(v2);
Any(&v1);
Any(&v2);
Any(10);
// Any(T& val)
// Any(const T& val)
// Any(T* val)
// Any(const T* val)
// Any(const T& val)
// Any(const T& val)
Any<const char* const>("Text");
template <typename T>
void Any(T* val)
{...}
template <typename T>
void Any(const T* val)
{...}
}
Bei der Übergabe des C-Strings müssen Sie explizit die aufzurufende Template-Funktion angeben, da hier der
Compiler nicht das zu verwendende Funktions-Template herleiten kann.
Das Beispiel:
Das Funktions-Template SendTo(...) aus dem vorherigen Beispiel wird nun so erweitert, dass bei der Übergabe
eines Zeigers an die Template-Funktion die dereferenzierten Daten und nicht mehr der Zeiger selbst
'übertragen' werden.
Dieses Beispiel ist nicht mit dem VC++ 6.0 übersetzbar, da er die Spezialisierung von FunktionsTemplates nicht auflösen kann.
Das unten stehende Beispiel finden Sie auch unter: le17\prog\SpTempFunc.
Die Programmausgabe:
sending
sending
sending
sending
bytes:
bytes:
bytes:
bytes:
int 0a 00 00 00
char 41
int 0a 00 00 00
char 41
Hinweis: Die Datentypkennung kann je nach Compiler abweichen.
Das Programm:
//
//
//
//
C++ Kurs
Beispiel zur expliziten Template-Definition
fuer Template-Funktionen
Nicht mit VC++ uebersetzbar!
#include <iostream>
using std::cout;
using std::endl;
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (12 von 45) [17.05.2005 11:41:11]
Template Vertiefung
// Template-Funktion zur byteweisen 'Uebertragung' eines
// Wertes in ASCII-Form
template <typename T>
void SendTo(std::ostream& os, T value)
{
os << "sending bytes: ";
os << typeid(T).name() << ' ';
// char-Zeiger auf Datenbeginn aufsetzen
const char* p = reinterpret_cast<const char*>(&value);
// Alle Bytes durchlaufen
for (size_t i=0; i<sizeof(T); i++)
{
// Low-Byte nach ASCII konvertieren
char lowByte = (*p)&0x0f;
lowByte = (lowByte>9) ? lowByte+0x57 : lowByte|0x30;
// Heigh-Byte nach ASCII konvertieren
char highByte = ((*p)>>4)&0x0f;
highByte = (highByte>9) ? highByte+0x57 : highByte|0x30;
// Datum in ASCII-Form 'uebertragen'
os << highByte << lowByte << ' ';
// Zeiger auf naechstes Byte
p++;
}
cout << endl;
}
// Spezialisierung fuer 'Uebertragung' von Daten die
// ueber einen Zeiger referenziert werden
template <typename T>
void SendTo(std::ostream& os, T* value)
{
os << "sending bytes: ";
os << typeid(T).name() << ' ';
// char-Zeiger auf Datenbeginn aufsetzen
const char* p = reinterpret_cast<const char*>(value);
// Alle Bytes durchlaufen
for (size_t i=0; i<sizeof(T); i++)
{
// Low-Byte nach ASCII konvertieren
char lowByte = (*p)&0x0f;
lowByte = (lowByte>9) ? lowByte+0x57 : lowByte|0x30;
// Heigh-Byte nach ASCII konvertieren
char highByte = ((*p)>>4)&0x0f;
highByte = (highByte>9) ? highByte+0x57 : highByte|0x30;
// Datum in ASCII-Form 'uebertragen'
os << highByte << lowByte << ' ';
// Zeiger auf naechstes Byte
p++;
}
cout << endl;
}
// Hauptprogramm
// =============
int main()
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (13 von 45) [17.05.2005 11:41:11]
Template Vertiefung
{
// Auszugebende Daten
int intVal = 10;
char charVal = 0x41;
// entspricht 'A'
// Aufruf der Template-Funktion
SendTo(cout,intVal);
SendTo(cout,charVal);
// Aufruf der spezialisierten Template-Funktion
SendTo(cout,&intVal);
SendTo(cout,&charVal);
}
Templates als Parameter
Nach dem Sie nun einiges über Funktions-Templates erfahren haben, machen wir jetzt einen 'sanften' Übergang
zu den Klassen-Templates.
Bisher wurden Klassen-Templates 'nur' unter dem Gesichtspunkt betrachtet, dass entsprechende Objekte
definiert werden können. Wie aber übergibt man ein Objekt eines Klassen-Templates an eine Funktion oder
Memberfunktion? Hierbei müssen zwei Fälle unterschieden werden:
Es wird immer ein Objekt des gleichen Klassen-Templates an die Funktion übergeben.
Da der Datentyp des Funktionsparameters
indirekt über das Argument des KlassenTemplates variieren kann, muss die Funktion
als Funktions-Template definiert werden.
Innerhalb der Parameterklammer des
Funktions-Templates wird dann eine Referenz
(oder const-Referenz) von Typ des KlassenTemplates angegeben. Das TemplateArgument T des Funktions-Templates
entspricht dann dem Datentyp, mit dem das
übergebene Objekt des Klassen-Templates
instanziiert wurde. Damit können dann
innerhalb des Funktions-Templates auch
Daten vom Typ des Template-Arguments des
Klassen-Templates definiert werden (T val im
Beispiel rechts).
template <typename T>
class MyClass
{
T GetData();
...
};
template <typename T>
void DoAny(MyClass<T>& obj)
{
...
T val = obj.GetData();
}
MyClass<int> intClass;
DoAny(intClass);
// T in DoAny() int
MyClass<float> floatClass;
DoAny(floatClass); // T in DoAny() float
Es werden Objekte unterschiedlicher Klassen-Templates an die Funktion übergeben.
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (14 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Auch hier ist die Verwendung eines FunktionsTemplates erforderlich, da der ParameterDatentyp der Funktion variiert. Anstatt nun
jedoch das Klassen-Template in der
Parameterklammer des Funktions-Templates
anzugeben, wird nur noch das TemplateArgument T angegeben. Damit wird das
Template-Argument T der FunktionsTemplates innerhalb der Funktion durch das
Klassen-Template 'ersetzt'.
template <typename T> class MyClass;
template <typename T> class YourClass;
template <typename T>
void DoAny(T& obj)
{...}
MyClass<float> myObj;
DoAny(myObj);
// T in DoAny() MyClass<float>
YourClass<int> yourObj;
DoAny(yourObj); // T in DoAny() YourClass<int>
Ist doch gar nicht so schwierig, Template-Objekte an Funktionen zu übergeben, oder? Bleibt nur noch ein kleines
Problem bestehen, wenn unterschiedliche Klassen-Templates an eine Funktion übergeben werden sollen. Wie
kommt man in diesem Fall an den Datentyp, mit dem das Objekt des Klassen-Templates instantiiert wurde?
Die Lösung naht in Form eines typedefs
innerhalb des Klassen-Templates.. Innerhalb
des Klassen-Templates wird mittels typedef für
das Template-Argument T ein Synonym
definiert (zum Beispiel value_type). Dieses
Synonym kann dann für den Datentyp des
Template-Arguments des Klassen-Templates
innerhalb der Funktion eingesetzt werden.
Beachten müssen Sie hierbei aber
unbedingt, dass vor dem Synonym des
Schlüsselwort typename stehen muss.
Ansonsten kann der Compiler nicht
unterscheiden, ob hier eine Definition oder ein
Datentyp steht.
template <typename T> class MyClass
{
public:
typedef T value_type;
T GetData();
...
};
template <typename T>
void DoAny(T& obj)
{
typename T::value_type val = obj.GetData();
...
}
Aber eigentlich sollten Sie dieses Konstruktor MyClass<float> myObj;
DoAny(myObj);
// T in DoAny() MyClass<float>
schon von der Behandlung der Stack-Klasse
her kennen ( ).
Das Beispiel:
Das Funktions-Template SendTo(...) wurde so umgeschrieben, dass es nun Objekte der STL-Templates stack und
priority_queue verarbeiten kann.
Das unten stehende Beispiel finden Sie auch unter: le17\prog\FuncTempParam.
Die Programmausgabe:
sending bytes: 4,0,0,0,3,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,
sending bytes: 12,0,11,0,10,0,
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (15 von 45) [17.05.2005 11:41:11]
Template Vertiefung
Das Programm:
// C++ Kurs
// Templates als Funktionsparameter
#include <iostream>
#include <stack>
#include <queue>
using std::cout;
using std::endl;
using std::stack;
using std::priority_queue;
// Template-Funktion zur Uebertragung der STL Container
// stack und priority_queue
// Nach dem Verlassen der Funktion ist der uebergebene
// Container leer!!
template <typename T>
void SendTo(std::ostream& os, T& container)
{
os << "sending bytes: ";
// Kompletten Container leeren
while(!container.empty())
{
// Einen Wert aus dem Container holen. Der aktuelle Datentyp
// im Container ist als typedef value_type definiert
// Auch hier ist wieder typename erforderlich
typename T::value_type actVal = container.top();
// Wert nun byteweise ausgeben
char *ptr = reinterpret_cast<char*>(&actVal);
for (size_t i=0; i<sizeof(typename T::value_type); i++)
os << static_cast<int>(*(ptr++)) << ',';
// Element aus Container entferenen
container.pop();
}
os << endl;
}
// Hauptprogramm
// =============
int main()
{
// int-Stack erzeugen und fuellen
stack<int> intVector;
for (int i=0; i<5; i++)
intVector.push(i);
// Vektor jetzt byteweise an cout uebertragen
SendTo(cout,intVector);
// short-Priority-Queue erzeugen und fuellen
priority_queue<short> shortQueue;
for (int x=0; x<3; x++)
shortQueue.push(x+10);
// und ebenfalls an cout uebertragen
SendTo(cout, shortQueue);
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (16 von 45) [17.05.2005 11:41:11]
Template Vertiefung
}
Klassen-Templates und Ableitungen
Beim Ableiten von Klassen-Templates können drei Fälle auftreten:
Die abzuleitende Klasse wird von einem bestimmten Klassen-Template abgeleitet.
In diesem Fall erfolgt die Ableitung wie
gewohnt, d.h. nach dem Zugriffsrecht folgt
zunächst der Name der Basisklasse. Da die
Basisklasse nun aber eine Template-Klasse ist,
muss wie bei Templates üblich zusätzlich noch
in spitzen Klammern der Datentyp des
Template-Arguments der Basisklasse
angegeben werden. Ist der Datentyp fest
vorgegeben, so kann die abzuleitende Klasse
als 'normale' Klasse definiert werden (erste
Ableitung rechts MyClass). Variiert jedoch der
Datentyp des Template-Arguments, so muss
die abzuleitenden Klasse ebenfalls als KlassenTemplate definiert werden, wobei das
Template-Argument nun an die Basisklasse
'weitergegeben' wird (zweite Ableitung rechts
YourClass).
// Die Basisklasse
template <typename T>
class Any
{...};
class MyClass: public Any<int>
{...};
template <typename T>
class YourClass: public Any<T>
{...};
// Objekte definieren
Any<int> obj1;
// Basiskl.-Objekt
MyClass obj2;
// Basis: Any<int>
YourClass<short> obj3; // Basis: Any<short>
Die abzuleitende Klasse wird von verschiedenen Klassen abgeleitet.
Hierbei ist die abzuleitende Klasse immer als
Klassen-Template zu definieren, wobei die
Basisklasse dann als Template-Argument
spezifiziert wird.
// Zwei 'normale' Basisklassen
class Any
{...};
class Some
{...};
// Abgeleitete Klasse, Basisklasse wird ueber
// Template-Argument festgelegt
template <typename T>
class MyClass: public T
{...};
// Objekte definieren
MyClass<Any> anyBase;
// Basisklasse Any
MyClass<Some> someBase; // Basisklasse Some
Die Basisklasse kann ein beliebiges Klassen-Template sein.
http://www.cpp-tutor.de/cpp/le17/le17_01.htm (17 von 45) [17.05.2005 11:41:11]
Template Vertiefung
In diesem Fall ist die abzuleitende Klasse
ebenfalls als Klassen-Template zu definieren,
die als Template-Argument dann die
Template-Klasse der Basisklasse erhält.
Bei der Definition eines Objekts der
abgeleiteten Klasse muss dann das
Basisklassen-Template inklusive dessen
Template-Argument angegeben werden.
Beachten Sie unbedingt das
Leerzeichen zwischen den beiden
spitzen Klammern bei der Definition
eines solchen Objekts! Ohne dieses
Leerzeichen würde der Compiler die
beiden spitzen Klammern als ShiftRight Operator interpretieren.
// Klassen-Templates als Basisklassen
template <typename T>
class Base1
{...};
template <typename T>
class Base2
{...};
// Abgeleitete Klasse
template <typename T>
class Der: public T
{...};
// Objekte definieren
Der<Base1<short> > obj1;
Der<Base2<float> > obj2;
Leiten Sie aber niemals Klassen von STL-Containern (wie z.B. stack oder queue) ab! Die STL-Container
enthalten keinen virtuellen Destruktor und damit werden diese nicht ordnungsgemäß zerstört wenn das
abgeleitete Objekt zerstört wird.
Das Beispiel:
Vorgegeben ist ein Klassen-Template SpinButton für einen Oberflächen-Element Spin-Button (Drehschalter). Da
der Spin-Button beliebige Datentypen verarbeiten können
Herunterladen