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