Die Programmiersprache C++ Prof. Jürgen Sauer Programmieren in C++ Skriptum zur Vorlesung im SS 2003 1 Die Programmiersprache C++ Inhaltsverzeichnis 1. Grundlegende Konzepte 1.1 Übersicht zur Entwicklung der Programmierspache C++ 1.2 Ein einführendes Beispiel zur Präsentation grundlegender Konzepte 1.2.1 Aufgabenstellung und Lösungsvorschlag zu einem internen Sortierverfahren 1.2.2 Der Aufbau eines C++-Programms 1.2.2.1 Das Layout 1.2.2.2 Übersetzung 1.2.2.3 Entwicklungszklus eines C++-Programms 1.2.2.4 Kommentare und Präprozessor-Direktiven 1.2.2.5 Standardein-/Standardausgabe 1.2.2.6 Hauptprogramm 1.2.2.7 Kommandozeilenversionen bzw. Konsolenanwendungen von Visual C++ 6.0 bzw. Dev-C++ 1. Kommandozeilenversion von Visual C++ 2. Konsolenanwendung in IDE unter Visual C++ 3. Dev-C++ 1.2.2.8 Visual C++ .NET 1. Erstellen einer prozedurorientierten C/C++-Konsolenanwendung 2. Erstellen einer prozedurorientierten C/C++-Windows-Anwendung 3. Verwaltete C++-Anwendung 4. Verwaltete C++-Anwendung mit GDI+ 5. 1.3 Deklaration und Definition von Bezeichnern 1.4 1.4.1 1.4.2 1.4.3 1.4.4 Speicherklassen, Gültigkeitsbereiche und Namensbereiche Speicherklassen Gültigkeitsbereiche bzw. Geltungsbereiche Externe Variable und Funktionen Namensbereiche 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 Ausdrücke Arithmetische Operatoren Relationale und logische Operatoren Bitweise logische Operatoren Zuweisungsoperatoren Inkrement- und Dekrementoperatoren Bedungungsoperator Kommaoperator Vorrang und Assoziativität von Operatoren 1.6 1.6.1 1.6.2 Anweisungen Einfache Anweisung Kontrollanweisungen (-strukturen) 1. Die for-Anweisung 2 Die Programmiersprache C++ 2. die while-Anweisung 3. die do-Anweisung 4. continue und break 5. die if-Anweisung 6. die „switch“-Anweisung 1.7 Funktionen 1.7.1 Definition und Deklaration von Funktionen 1.7.2 Parameterübergabe 1. Call by value 2. Call by reference 3. Vektoren als Parameter 4. Default-Argumente 5. Beliebig viele Argumente (variable Parameterlisten) 6. Prototypen 1.7.3 Funktionswerte (Ergebniswertrückgabe) 1.7.4 Rekursion 1.7.4.1 Rekursive Funktionen 1.7.4.2 Rekursion und Iteration 1.7.4.3 Türme von Hanoi 1.7.4.4 Damen-Problem 1.7.5 Überladen von Funktionsnamen (overloading) 1.7.6 Operatorfunktionen 1.7.7 Inline-Funktionen 1.7.8 Funktionsschablonen 1.7.9 Objekte als Funktionen 1.7.10 Spezifikation von Funktionen 2. Datentypen 2.1 Einfache, fundamentale Datentypen 2.2 Abgeleitete Datentypen 2.2.1 Konstanten 2.2.2 Zeiger (pointer types) 2.2.3 Vektoren (Arrays) 2.2.3.1 C-Arrays 2.2.3.2 Der C++-Standardtyp vector 2.2.3.3 Der C++-Stringklasse string 2.2.4 Strukturen 2.2.5 Variantenstrukturen 3. Benutzerdefinierte Datentypen: Klassen 3.1 3.1.1 3.1.2 3.1.3 Konzepte für benutzerdefinierte Datentypen Formale Definitionsmöglichkeiten Ein einführendes Beispiel: Stapelverarbeitung Konstruktoren, Klassenvariable und Destruktoren 3 Die Programmiersprache C++ 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 Operatorfunktionen Konstante Komponentenfunktionen Statische Komponentenfunktionen „friend“-Funktionen und „friend“-Klassen ADT-Stapel 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 Abgeleitete Klassen Basisklasse und Ableitung Einfache Verarbeitung Klassenhierarchien 1. Konstruktoren in Klassenhierarchien 2. Destruktoren in Klassenhierarchien Virtuelle Funktionen Abstrakte Klassen Mehrfachvererbung Virtuelle Basisklassen Generische Datentypen 3.3 3.3.1 3.3.2 Schablonen (Templates) Klassenschablonen Methodenschablonen 3.4 3.4.1 3.4.2 Ein-, Ausgabe Aufbau Ausgabe 1. Formatierte Ausgabe 2. Unformatierte Ausgabe 3. Adressierung von Streampositionen 4. Ausgabe auf Dateien 3.4.3 Eingabe 1. Formatierte Eingabe 2. unformatierte Eingabe 3. Eingabe aus Dateien 4. Eingabe aus Zeichenketten 3.4.4 Formatierung in Zeichenketten 3.4.4.1 strstream für C++ im AT&T-Standard 3.4.4.2 stringstream für C++ im ANSI/ISO-Standard 3.4.5 3.4.6 3.5 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 Fehlerzustände Positionieren in Dateien Ausnahmebehandlung Übliche Fehlerbehandlungsroutinen Schema für Ausnahmenbehandlungen Exception-Hierarchie Besondere Fehlerbehandlungsroutinen Unbehandelte Ausnahmen 4 Die Programmiersprache C++ 4. Templates für Algorithmen und Datenstrukturen 4.1 Darstellung von Algorithmen und Datenstrukturen für Graphen 4.1.1 Die Datentruktur Graph 4.1.2 Die STl-Containerklasse vector zur Implementierung einer Knotenliste für 4.1.3 Mehrdimensionale Felder 4.1.4 Durchlaufen von Graphen mit Hilfe der Containerklassen stack und queue 4.1.4.1 Tiefensuche 4.1.4.2 Breitensuche 4.1.5 Ermittlung der kürzesten Wege mit Hilfe der STL-Containerklasse priority_queue 4.2 4.2.1 4.2.2 Verkettet gespeicherte Listen Doppelt gekettete Listen Ringförmig geschlossene Listen 4.3 4.4.1 4.4.2 4.4.3 Tabellen Einfache Tabellen Sortierte Tabellen Hash-Tabellen 4.4 Binäre Bäume 5. C und C++-Bibliotheken 5.1 Die C++-Standardbibliothek 5.1.1 Die C++-Standardbibliothek und die STL 5.1.2 Hilfsfunktionen und -klassen 5.1.2.1 Paare 5.1.2.2 Funktionsobjekte 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 Container Bitset Deque List Map Queue Set Stack Vector 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 Iteratoren Iteratorkategorien distance(), advance() und iter_swap() Iterator-Adapter Stream-Iteratoren Stream-Traits 5.4 Algorithmen 5 Die Programmiersprache C++ 5.5 Nationale Besonderheiten 5.6 5.6.1 5.6.2 5.6.3 5.6.2 5.6.2.1 5.6.2.2 5.6.2.3 5.6.2.4 5.6.2.5 5.6.2.6 5.6.2.7 5.6.2.8 5.6.2.9 Numerik Komplexe Zahlen Grenzwerte von Zahlensystemen Halbnumerische Algorithmen Optimierte numerische Algorithmen (valarry) Konstruktoren und Elementfunktionen Binäre Valarray-Operatoren Mathematische Funktionen slice slice_array gslice gslice_array mask_array indirect_array 5.7 Typerkennung zur Laufzeit 5.8 5.8.1 5.8.2 Speichermanagement <new> <memory> 6. Windows-Programmierung unter Visual C++ 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 Merkmale von Visual C++ Visual C++-Features Funktionsweise von Windows-Programmen Eintritt in die Nachrichtenverarbeitung Vom API zur MFC Erstellen einer Windows-Anwendung mit der MFC 6.2 6.2.1 6.2.2 6.2.3 6.2.3.1 6.2.3.2 6.2.3.3 Die zentralen MFC-Klassen (Zusammensetzung und Zusammenspiel) Übersicht zu den zentralen MFC-Klassen Allgemeine Ereignisbehandlung unter Windows mit der MFC Verschiedene spezielle MFC-Klassen Spezielle Klassen zur Textverarbeitung Dateiverarbeitung CTimer 6.3 6.3.1 6.3.2 Die Kernkomponenten Aufbau von Dialogen aus Steuerelementen Die wichtigsten Steuerelemente 6.4 6.4.1 6.4.2 6.4.3 6.4.4 Erstellen von Dialogen über Ressourcen Ressorcen Dialoge aus Ressourcen Visuelle Erstellung Interaktive Platzierung der Komponenten 6 Die Programmiersprache C++ 6.5 6.5.1 6.5.2 6.5.3 6.5.3.1 6.5.3.2 6.5.4 6.5.4.1 6.5.4.2 6.5.4.3 6.5.4.4 6.5.4.5 Assistenten und MFC-Anwendungen Assistenten Doc/View-Modell SDI- ubd MDI-Anwendungen SDI-Anwendungen MDI-Anwendungen Das MFC-Anwendungsgerüst Erzeugen der Fenster Anpassen der Fenster Bearbeitung von Kommandozeilenargumenten Die Nachricht WM_PAINT Zeitgeber 6.6 6.8.1 6.8.2 Bilder, Zeichnungen und Bitmaps Die grafische Geräteschnitsstelle Die Zeichenwerkzeuge 6.7 Sammlungsklassen der MFC 7. C# und .NET 8. Die grafischen Bedienoberflächen X und OSF/Motif 8.1 X-Window bzw. X 8.1.1 8.1.2 8.1.3 Die Komponenten von X Architektur von X-Programmen Ein X-Programm 8.2 OSF/Motif 8.2.1 8.2.2 8.2.3 Einführung in OSF/Motif, Xt-Intrinsics Struktur eines Intrinsics-Programms Das OSF/Widget Set 7 Die Programmiersprache C++ 1. Grundlegende Konzepte 1.1 Übersicht zur Entwicklung der Sprache C++ C++ ist eine relativ junge Sprache. Erste Versionen dieser Sprache wurden unter dem Namen „C with Classes“1 1980 erstmals benutzt. Diese Erweiterung der Programmiersprache C war zur Entwicklung von Simulationsprogrammen nötig. So entstand auf der Basis von C unter Berücksichtigung von Konzepten, die in der Simulationssprache Simula entwickelt wurden, eine objektorientierte Erweiterung zu C. Je länger „C with Classes“ und später C++2 verwendet wurde, desto größer wurde der Funktionsumfang. Die Aufwärtskompatibilität zu C wurde jedoch gewahrt. C++3 ist bis auf einige Ausnahmen eine Obermenge der Programmiersprache C. Selbst CBibliotheken sind weiterhin (noch) uneingeschränkt benutzbar. Viele Anzeichen sprechen dafür, daß C++ zur Programmiersprache der neunziger Jahre wird. Die Gründe für diese Entwicklung sind offensichtlich: - - - C++ besitzt die wesentlichen Merkmale einer objektorientierten Programmiersprache, zwingt der Anwendung dieses Paradigma jedoch nicht auf, sondern läßt sich auch als verbessertes C einsetzen. Übersetzer zu C++ sind praktisch unter allen bekannten Betriebssystemen verfügbar und erzeugen relativ effektiven Code. Die derzeit laufende Standardisierung durch das ANSI Komitee X3J16 verspricht außerdem für die Zukunft eine portable Sprachdefinition. C++-Programme sind mit den großen Mengen existierender C-Software kombinierbar. Darüberhinaus ist bereits das Angebot an kommerziell verfügbaren CKlassenbibliotheken unüberschaubar. C++ hat allerdings auch einen Nachteil: C++ ist nicht einfach. Grundlage für C++ (in diesem Skriptum) bildet die Sprachdefinition der Version 2.1 von Margaret Ellis und Bjarne Stroustrup4 und der im Sommer 1998 verabschiedete internationale C++-Standard (ISO/IEC 14882) 1 Beschrieben erstmals in: Stroustrup, Bjarne: "Classes: An Abstract Data Type Facility for the C Language", ACM SIGPLAN Notices, January 1982 2 Stroustrup, Bjarne: "Data Abstractions in C", AT&T Bell Labaratories Technical Journal, October 1984 3 dokumentiert in: Stroustrup, Bjarne: "The C++ Language", Addison-Wesley, 1986 bzw. Stroustrup, Bjarne: "Die C++ Programmiersprache", Addison-Wesley, 2. überarbeitete Auflage, 1992 Ellis, Margaret und Stroustrup, Bjarne: "The Annotated C++ Reference Manual", Addison-Wesley, Reading, MA, 1990 4 Ellis, Margaret und Stroustrup, Bjarne: „The Annotated C++ Reference Manual, Addison-Wesley, Reading, MA, 1990 8 Die Programmiersprache C++ 1.2 Ein einführendes Beispiel 1.2.1 Aufgabenstellung und Lösungsvorschlag zu einem Sortierverfahren Aufgabenstellung: Schreibe ein Programm, das die in einem Arbeitsspeicherfeld („array“) gespeicherten ganzen Zahlen nach einem einfachen Sortierverfahren („Sortieren durch Austauschen, Bubble-Sort“) in aufsteigende Sortierreihenfolge bringt. Lösungsverfahren (Algorithmus): Im Bubble-Sort werden Zahlen eines Arbeitsspeicherfelds nach folgendem Schema miteinanander verglichen: Zuerst wird der letzte mit dem vorletzten Schlüssel (Zahl) verglichen. Ist der letzte bspw. kleiner als der vorletzte Schlüssel, dann werden im Falle der aufsteigenden Sortierung die Zahlen miteinander vertauscht. Der gleiche Vorgang wiederholt sich dann mit dem vorletzten und drittletzten Schlüssel, dann mit dem drittletzten und viertletzten Schlüssel. Dies wird bis zum Vergleich des zweiten mit dem ersten Schlüssel fortgesetzt. Nach (N - 1) Vergleichen befindet sich der kleinste Schlüssel auf der ersten Position. Nach dem gleichen Schema kann danach mit (N - 2) Vergleichen der zweitkleinste Schlüssel auf die zweite Position gebracht werden. Dies wird fortgesetzt bis schließlich nur ein einziger Schlüssel vorhanden ist, der dann auf der richtigen Position stehen muß. Bsp.: 1 37 37 37 22 22 22 18 18 9 9 9 18 25 25 25 37 9 22 18 25 9 37 22 18 25 2 3 9 9 9 37 37 18 22 18 37 18 22 22 25 25 25 9 18 37 22 25 4 9 18 22 37 25 9 18 22 25 37 Abb. 1.1-1: Sortiertabelle zum Sortieren durch Austauschen Alternativ zu dem vorliegenden Lösungsverfahren kann man auch jeweils den ersten Schlüssel gegen alle weiteren Schlüssel im Arbeitsspeicherfeld vergleichen und so an der ersten Position den kleinsten Schlüssel nach (N-1) Vergleichen ermitteln. Danach vergleicht man jeweils den Schlüssel aus der zweiten Position mit allen nachfolgenden Schlüsslwerten. Nach (N-2) Vergleichen steht der zweitkleinste Sachlüssel an der zweiten Position. Bei Fortsetzung dieser Verfahrensweise ist schließlich nur noch ein einziger Schlüssel vorhanden, der dann auf der richtigen, der letzten Position steht. Bsp.: 1 37 22 18 9 9 22 37 37 37 37 18 18 22 22 22 9 9 9 18 18 25 25 25 25 25 2 9 22 37 18 25 3 9 18 37 22 25 9 18 37 22 25 9 18 22 37 25 4 9 18 22 37 25 9 9 18 22 25 37 Die Programmiersprache C++ Vorschlag zur Implementierung5: /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* /* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Das folgende Programm enthaelt 3 Funktionsaufrufe: 1. eingabe() Zahlen werden in ein Feld eingelesen 2. bsort() Die Zahlen werden sortiert 3. ausgabe() Feldelemente werden ausgegeben */ */ #include <iostream.h> */ #include "a:\eing.h" // eingabe() */ #include "a:\ausg.h" // ausgabe() */ #include "a:\sort.h" // bsort() */ */ int n; // Vereinbarung von Variablen */ int x[20]; */ Hauptprogramm */ */ main() */ { */ eingabe(x,n); */ cout << "Ausgabe unsortiert: " << endl; */ ausgabe(x,n); */ cout << "Sortieren durch Austauschen" << endl; */ bsort(x,n); */ ausgabe(x,n); */ return 0; */ } 1.2.2 Der Aufbau eines C++-Programms 1.2.2.1 Das Layout des Programms Der äußere Aufbau eines Programms wird von C++ nicht vorgeschrieben. Die Markierung, die ein Programm in einzelne Anweisungen zerlegt, ist das Semikolon („;“). Jede Anweisung im Quellcode muß mit einem Semikolon enden. Geschweifte Klammern kennzeichnen den Anfang und das Ende eines Blocks. C++ ist eine formatfreie Sprache. Das Aussehen, die Gestalt des Programms kann man in weiten Grenzen selbst bestimmen. C++ ist beim äußeren Aufbau des Programms sehr großzügig. Bei der Schreibweise von Funktionen oder Variablennamen ist es aber sehr genau. C++ ist case sensitive, d.h.: Es unterscheidet zwischen Groß- und Kleinschreibung. Ein Quellprogrammtext ist in C++ in einer Datei6 zusammengefaßt. Dateien sind die grundlegenden Programmeinheiten in C++ und durch die Extension .C bzw. .CPP bzw. .CC gekennzeichnet. Jedes C++-Programm ist eine Sammlung von Funktionen. Die Ausführung eines Programms startet immer mit einer Funktion, die den Namen main hat. 5 6 PR12101.CPP PR12101.CPP 10 Die Programmiersprache C++ Includes Einlesen von Quellcode durch den Präprozessor Defines Anweisungen an den Präprozessor, z.B. zur Definition von Konstanten und Makros bzw. zur bedingten Übersetzung Globale Variablen Definition von Variablen, die allgemein gelten sollen funktion_1() Funktionen, die direkt oder indirekt von main() aufgerufen werden ............... funktion_n() main() Funktion, in der die Ausführung des Programms startet Häufig wird auch folgende Struktur für ein C- bzw. C++-Programms benutzt: Includes Einlesen von Quellcode durch den Präprozessor Defines Anweisungen an den Präprozessor, z.B. zur Definition von Konstanten und Makros bzw. zur bedingten Übersetzung Globale Variablen Definition von Variablen, die allgemein gelten sollen Funktionsprototypen Unterprogrammschnittstellen, die direkt oder indirekt von main() aufgerufen werden, aber erst im Anschluss definiert sind. main() Funktion, in der die Ausführung des Programms startet Funktions-Definitionen der Funktionsprototypen Funktionen, die direkt oder indirekt von main() aufgerufen werden Abb. 1.1-2: Struktur eines C++-Programms 11 Die Programmiersprache C++ 1.2.2.2 Übersetzung Vor der Übersetzung durchsucht der Präprozessor das zu übersetzende Programm nach besonderen Direktiven. So fügt bspw. #include <iostream.h> bzw7. #include <iostream> den Inhalt der (Header-) Datei iostream.h in das Programm ein. In der nachfolgenden Analysephase der Compilierung wird die Quellcode-Datei in Symbole und Whitespace-Zeichen zerlegt. Symbole sind wortähnliche Einheiten. Sie ergeben sich als Folge von Operationen, die der Compiler und sein Präprozessor mit dem zu übersetzenden Programm durchführen. C++ kennt 6 Arten von Symbolen: „Schlüsselwort, Bezeichner, Konstante, String-Literal, Operator, Interpunktionszeichen (Trennzeichen, Seperatoren)“. Schlüsselworte sind für spezielle Zwecke reservierte Worte, die nicht als Bezeichner verwendet werden dürfen. asm class double friend long register struct unsigned auto const else goto new return switch virtual break continue enum if operator short template void case default extern inline private signed this volatile catch delete float int protected sizeof typedef while char do for interrupt public static union Abb. 1.1-3: Schlüsselworte in C++ Bezeichner sind beliebige Namen von beliebiger Länge für Klassen, Objekte, Funktionen, benutzerdefinierte Datentypen, usw. Bezeichner können die Buchstaben a bis z, A bis Z, den Unterstrich _ und die Ziffern 0 bis 9 enthalten. Das erste Zeichen muß ein Buchstabe oder ein Unterstrich sein. Bezeichner können beliebige Namen sein, die diesen Regeln entsprechen. Konstante sind Symbole, die für numerische Werte oder Zeichenwerte (Zeichenkonstanten, Stringkonstanten) stehen. Stringkonstante (Zeichenkettenkonstante) bestehen aus einer Reihe beliebig vieler Zeichen, die in Anführungszeichen eingeschlossen sind, z.B.: "Sortieren durch Austauschen" Interpunktionszeichen bestehen aus einem der folgenden Symbole: [ ] ( ) { } , ; : ... * = # [ ] beinhalten einfache und mehrdimensionale Array-Indizes, z.B. int x[20]; ( ) fassen Ausdrücke zusammen, isolieren konditionale Ausdrücke, präsentieren Funktionsaufrufe und Funktionsparameter, z.B.: main() eingabe(x,n); ausgabe(x,n); bsort(x,n); { } markieren den Beginn und das Ende eines Anweisungsblocks. 7 Hinweis: Falls ein Compiler #include <iostream> nicht versteht, entspricht er noch nicht dem C++Standard 12 Die Programmiersprache C++ Das Komma „,“ trennt Elemente in einer Funktions-Argumentenliste. Das Semikolon „;“ dient als Endekriterium einer Anweisung. Mit dem Doppelpunkt „:“ wird ein Label gekennzeichnet. Whitespace-Zeichen8 können sein: Leerzeichen, horizontale und vertikale Tabulatoren, Zeichenvorschübe, Kommentare. Sie sind für das Erkennen von Anfang und Ende eines Symbols geeignet. Die beiden folgenden Sequenzen int n; int x[20]; und int n; int x[20]; sind (lexikalisch) identisch und werden in diesselben Symbole zerlegt: int n ; int x [ 20 ] ; Falls Whitespace-Zeichen innerhalb alphanumerischer Zeichenketten stehen, sind sie vom normalen Bearbeitungsvorgang (Parsing) ausgenommen, z.B.: char name[] = "Fachhochschule Regensburg"; Ein Sonderfall ist: Das Auftreten eines Backslash (\) vor dem Zeilenvorschubzeichen. Der Backslash und der Zeilenvorschub werden ignoriert mit der Konsequenz, daß 2 physisch vorhandene Zeichen als Einheit betrachtet werden. Aus "Fachhochschule \ Regensburg" wird "Fachhochschule Regensburg"; 8 Diese Zeichen sind über die Funktion isspace in ctype.h definiert 13 Die Programmiersprache C++ 1.2.2.3 Entwicklungszyklus eines C++-Programms Am Anfang steht die Eingabe des Quellcodes (Programmtext) mit einem Editor. Integrierte Entwicklungsumgebungen (IDEs) haben einen speziell auf Programmierzwecke zugeschnittenen Editor, der auf Tastendruck oder Mausklick die Übersetzung anstößt. Alternativ besteht die Möglichkeit, Compiler und Linker im Shell- oder MS-DOS-Fenster in der Kommandozeile zu starten, z.B. für das Programm PR12101.CPP und dem GNU-Compiler: g++ -c pr12101.cpp g++ -o pr12101.exe pr12101.o // Übersetzen: pr12101.o wird erzeugt // Linken bzw. zusammengefaßt: g++ -o pr12101.exe pr12101.cpp Es wird vorausgesetzt, daß der Compiler weiß, wo die Header-Files zu finden sind. Mit dem GNUCompiler wird eine Batch-Datei mit dem Namen cygnus.bat ausgeliefert, die alle nötigen Einstellungen vornimmt. Es folgt die Übersetzung durch den Compiler und dann ein abschließender LinkerLauf. Im Fehlerfall ist wieder der Editor an der Reihe. C++-Programme können auch aus getrennt übersetzten Moduln bestehen, die durch den Linker zusammengebunden werden. Außerdem benutzt nahezu jedes Programm Routinen aus mitgelieferten Bibliotheken. Der „Linker“ bindet den Objectcode der übersetzten Einheiten mit dem Objectcode der Bibliotheken zusammen und erzeugt ein ausführbares Programm, das nun gestartet werden kann. Der Aufruf des Programms bewirkt, daß der Lader (- eine Funktion des Betriebssystems -) das Programm in den Arbeitsspeicher lädt und startet. Die aktuellen Versionen (gcc version 2.95.3-5, cygwin spezial) erstellen ein .exe-File nach dem Aufruf gcc pr12101.cpp –lstdc++ 14 Die Programmiersprache C++ Editor Source Header Präprozessor Compiler Librarien (Verwaltung und Pflege der Objektmoduln) Objekt Linker Übernahme eigener Objekte in die Bibliothek Bibliotheken Programm Abb. 1.1-4: Ablaufdiagramm C++-Programmentwicklung 1.2.2.4 Kommentare und Präprozessor-Direktiven Kommentare In C++ leiten doppelte Schrägstriche (//) einen Kommentar ein, d.h.: Alles, was in der Zeile hinter diesen Zeichen folgt, wird vom Compiler ignoriert. Sollen Kommentare über mehrere Zeilen gehen, benutzt man die aus C bekannte Kombination „/* .... */“. Das vorliegende Programm beginnt mit einem Kommentar (eingeschachtelt durch /* ... */). Kommentare können auch mit dem Zeichen // eingeleitet werden (vgl. Zeile 10 .. 13). Nach // ist der Rest der Zeile Kommentar. Der Präprozessor Er dient zur Bearbeitung des Quelltextes, d.h.: Er sucht und ersetzt bestimmte Begriffe im Programm-Quelltext oder sorgt dafür, daß bestimmte Programmteile 15 Die Programmiersprache C++ compiliert werden und andere nicht. Was der Präprozessor macht, kann man mit einem bestimmten Befehl steuern. Diese Befehle beginnen mit einem Nummernzeichen (#), dem unmittelbar anschließend das Schlüsselwort9 folgt: #define #endif #ifdef #line #elif #error #ifndef #pragma #else #if #include #undef #include In Zeile /* 9 */ steht die Präprozessor-Direktive #include, die die Datei "iostream.h" einbezieht. In der Regel handelt es sich dabei um sog. Header-Dateien. Sie enthalten Definitionen, die für bestimmte Funktionen oder Objekte benötigt werden. Es gibt eine Vielzahl dieser Header-Dateien10 zur Abdeckung spezieller Bereiche. So muß z.B. die Datei string.h mit einem include-Befehl eingebunden werden, falls Zeichenketten-Funktionen (strcpy, strcat) benötigt werden. Welche HeaderDatei benötigt wird, ist im jeweiligen Referenzhandbuch der Funktionen beschrieben. Hinter dem Schlüsselwort include folgt der Name der Datei, die in das Programm einzukopieren ist. Der Dateiname ist häufig in spitzen Klammern (< > ) eingeschlossen, d.h.: Diese Datei wird in den voreingestellten „include-Pfaden“ gesucht, z.B.: #include <iostream.h>. Ist der Dateiname in Anführungszeichen eingeschlossen, dann wird diese Datei zuerst im aktuellen Verzeichnis gesucht und dann erst im voreingestellten Pfad, z.B.: #include "a:\ausg.h" #include "a:\sort.h" #define Mit #define kann man einer Konstanten einen Namen geben, z.B.: #define MAXWERT 100 Üblicherweise schreibt man zur besseren Unterscheidung von Variablen die mit #define definierte Konstante in Großbuchstaben. C++ kennt aber bessere Methoden (z.B. const) zur Defintion von Konstanten. Umfassend interpretiert, definiert die Direktive #define ein sog. Makro. Im einfachen Fall (ohne Parameter) schreibt man #define Makro_Bezeichner <SymbolSequenz> Jedes Auftreten von Makro-Bezeichner im Quelltext nach dieser Steuerzeile wird durch die (möglicherweise leere) Symbolsequenz ersetzt. Eine Makrodefinition ist mit dem Zeilenende abgeschlossen (Verlängerung über "\" als unmittelbar letztes Zeichen), z.B.: #define FH #define PI 3.141592653897953 #define quadrat(a) (a * a) // leerer Text // expansionsfähig // parametrisiert In der dritten Form ist der dem Makro-Bezeichner zugeordnete Text parametrisiert und kann den individuellen Gegebenheiten angepaßte werden, z.B 11.: 9 Je nach Compiler können noch einige weitere Begriffe hinzukommen Zu jedem Compiler gehören zahlreiche Header-Dateien, die gewöhnlich in einem Verzeichnis mit dem Namen INCLUDE abgelegt sind 11 vgl. PR12203.CPP 10 16 Die Programmiersprache C++ #include <iostream.h> #define PI 3.14159265389793 #define quadrat(a) (a * a) int main() { int zahl; cout << "Zahl eingeben:\n"; cin >> zahl; cout << "\nDie Kreisflaeche betraegt: " << quadrat(zahl) * PI; } Unerwünschte Seiteneffekte können allerdings auftreten. So würde z.B. quadrat(zahl - zahl) * PI keineswegs das erwartete Ergebnis 0 anzeigen. Ein korrektes Ergebnis würde in diesem Fall allerdings die Makrodefinition #define quadrat(a) ((a) * (a)) erzwingen. #undef Mit dieser Direktive kann ein Makro außer Kraft gesetzt werden: #undef MakroBezeichner. Diese Zeile entfernt jede vorherige Symbol-Sequenz aus dem MakroBezeichner. Die Makro-Definition wird sofort ungültig, der Makro-Bezeichner ist von da ab undefiniert. #ifdef und #ifndef Mit diesen Bedingungsdirektiven kann geprüft werden, ob ein Bezeichner derzeit definiert ist oder nicht. Die Zeile #ifdef Bezeichner besitzt die gleiche Wirkung wie #if 1, falls Bezeichner derzeit definiert ist, und die Wirkung #if 0 ,falls Bezeichner nicht definiert ist. Mit der #ifdef-#endif - Anwisung kann überprüft werden, ob eine Konstante bereits ein anderer Stelle definiert wurde. Die Anweisung #ifndef-#endif leistet das Gegenteil, z.B.: #ifndef TRUE #define TRUE 1 #define FALSE 0 #endif Die ersten beiden höchstwahrscheinlich: Präprozessoranweisungen von iostream.h lauten #ifndef __IOSTREAM_H #define __IOSTREAM_H Der Zweck dieser Angaben ist: Verhinderung von Mehrfachdefinitionen derselben Datei. Sobald die Datei name.h zum 1. Mal vom Präprozessor bearbeitet wird, ist der Name __NAME_H definiert. Bei einer weiteren Inklusion überspringt dann der Präprozessor den gesamten Text bis zum passenden #endif. #if, #elif, #else, #endif Sie funktionieren wie normale Bedingungsanweisungen, z.B. überprüft die #ifDirektive, ob ein angegebener Ausdruck einen wahren (d.h. von Null verschiedenen) Wert zur Übersetzungszeit ergibt und aktiviert bzw. deaktiviert Teile des Programms. 17 Die Programmiersprache C++ Bei allen Präprozessoranweisungen muß am Ende ein Zeilenvorschub erfolgen. Ein #if und #else dürfen bspw. nicht in derselben Zeile stehen. #pragma Diese Direktive erlaubt die Definition implementierungsspezifischer PräprozessorDirektiven der Form #pragma Direktive-Name. Über #pragma kann bspw. Borland C++ beliebig eigene Dateien definieren12, ohne dabei mit anderen Compilern in Konflikt zu geraten, die #pragma ebenfalls unterstützen. Falls der Compiler „Direktive-Name“ nicht kennt, dann ignoriert er die #pragma-Direktive ohne Fehlermeldung oder Warnung. 1.2.2.5 Standardein-/ Standardausgabe Ein-/ Ausgabefunktionen im einführenden Bsp. In der Datei „iostream.h“ befinden sich u. a. die Deklarationen der Funktionen zur Ein-, Ausgabebehandlung. Die Standardeingabe (normalerweise die Tastatur) ist hier durch den Eingabestrom cin festgeglegt. Die Ausgabe zum Terminal ist vordefiniert über cout. Der Operator << ist für den Linksshift zur Bitmanipulation vorgesehen. Er wird in iostream.h für Ausgabezwecke überladen und bestimmt die Richtung zur Ausgabe, z. B.: #include main() { int x float y cout << } <iostream.h> = 124; = 1.5; "x = " << x << " y = " << y << "\n"; „<<“ ist hier sogar mehrfach überladen, für jeden Datentyp des rechten Operanden ist hier eine eigene Version vorgesehen. Mit „cin >> “ werden Eingaben über die Tastatur der angegebenen Variablen zugewiesen: void eingabe(int *x, int& n) { cout << "Anzahl Elemente? \n"; cin >> n; cout << "Elementweise Eingabe: \n"; for (int i = 0; i < n; i++) cin >> x[i]; } Diese Prozedur ist in der Datei „eing.h“ enthalten und wird über das Kommando #include "a:\eing.h" in das vorliegende Programm einbezogen (vgl. Zeile /* 10 */). Die Hochkommata geben an, daß die Datei nicht unter dem üblichen Verzeichnis, sondern unter dem angegebenen Pfadnamen zu finden ist. Auf das übliche Verzeichnis wird durch spitze Klammern, z.B. #include <iostream.h> hingewiesen. 12 vgl. Borland C++ 4.0, Programmierhandbuch S. 206 18 Die Programmiersprache C++ Der Prozedurkopf von „eingabe“ trägt den Spezifizierer void. Damit ist „eingabe“ als Funktion definiert, die keinen Rückgabewert liefert. Wird kein Ergebnistyp angegeben, so wird als Ergebnistyp int angenommen. Die Parameterliste von „eingabe“ zeigt zwei Einträge. Der erste Eintrag beschreibt einen Referenzparameter auf das Feld x (in Zeile /* 15 */ global definiert). In C++ kann ein formaler Parameter einer Funktion als Referenz durch Nachstellen des Zeichen & nach dem Typ definiert werden. Dadurch wird dem Compiler signalisiert, daß der Wert als Referenz (call by reference) zu übergeben ist. In konventionellem C erfolgt die Paramterübergabe grundsätzlich mit „call by value“. Zur Veränderung von Daten muß man Zeiger als Parameter übergeben. Der hier verwendete erste Referenzparameter bezieht sich auf das eindimensionale Feld (C-Array): int x[20]; Falls 5 Komponenten dieses C-Array mit ganzen Zahlen belegt sind, könnte der Speicherplatz dafür so aussehen: ....... x[0] 1 Startadresse x x[1] 2 Startadresse + 413 x[2] 3 Startadresse + 8 x[3] 4 Startadresse + 12 x[4] 5 Startadresse + 16 ....... Abb. 1.1-5: „int-Array“-Tabelle Der Name des C-Arrays „x“ zeigt auf die Startadresse. Der Zugriff auf ein Element ist durch den Indexoperator [] möglich. Die Numerierung der Indexpositionen beginnt bei 0. Zwischen den eckigen Klammern des Indexoperators wird für den Zugriff auf die Komponente die (relative) Tabellenposition eingetragen. Für C-Arrays gelten folgende Regeln: - Array-Indizes beginnen mit 0. Das erste Element im C-Array hat den Index 0, das zweite Element den Index 1, usw. Array-Größen müssen Kompilierzeitkonstanten sein, d.h. der Compiler muß zur Kompilierzeit wissen, wieviel Platz er für das Array allokieren kann. Ein C- Array kann kein sogenannter L-Wert14 sein Zur Ausgabe der Feldelemente wurde in das vorliegende Programm die Funktionsprozedur „ausgabe“ (vgl. Zeile /* 22 */) aufgenommen: const zeilenLaenge = 12; // Anzahl der Elemente je Zeile void ausgabe(int* x, int n) { cout << "(" << n << ") <"; 13 14 Jeder Integer-Wert belegt einen Speicherbereich von 4 Bytes L-Wert bezeichnet eine Größe, die auf der linken Seite einer Zuweisung stehen darf. 19 Die Programmiersprache C++ for (int i = 0; i < n; ++i) { if (i % zeilenLaenge == 0 && i) cout << "\n\t"; cout << x[i]; // Trennen durch , Ausnahme letztes Element if (i % zeilenLaenge != zeilenLaenge - 1 && i != n - 1) cout << ", "; } cout << ">\n"; } Die Funktionsprozedur „ausgabe“ bezieht sich auf eine global definierte Konstante. Mit dem Schlüsselwort const lassen sich Objekte definieren, die einen Bezeichner zu einer Konstanten erklären, dessen Wert nicht verändert werden darf. Stream-Ausgabe Die Stream-Ausgabe wird mit <<15 erzeugt. Der standardmäßige LinksschiebeOperator16 ist für Ausgabe-Operationen überladen. Sein linker Operand ist ein Objekt der Klasse ostream. Sein rechter Operand ist ein beliebiger Typ 17, für den die Stream-Ausgabe definiert wurde, z.B.: cout << "hallo!\n"; Der Operator << wirkt von links nach rechts und liefert eine Referenz auf das ostream-Objekt, für das er gerufen wurde. Dadurch wird es möglich, mehrere Ausgaben aneinanderzureihen, z.B.: #include <iostream.h> int main() { int x = 124; float y = 1.5; cout << "x = " << x << "y = " << y << "\n"; } Zeichenketten enthalten besondere Zeichen, die nicht ausgedruckt werden, z.B.: cout << "Elementweise Eingabe: \n"; cout << "\n\t" Die Zeichenfolge „\n“ nennt man Escape-Sequenz. Escape-Sequenzen werden durch einen „backslash“ eingeleitet, dem ein oder mehrere Zeichen folgen. Das „n“ bedeutet „new-Line“ (und bewirkt einen Zeilenvorschub). Andere Zeichen, z.B. ‘\t‘ bewirken einen Tabulatorsprung. Escape-Sequenzen können an beliebiger Stelle und beliebig oft in der Zeichenkette auftauchen. Sie können in Hochkomma ('\n') und in Anführungszeichen ("\n")stehen. Folgende grundlegende Datentypen werden direkt unterstützt: char, short, int, long, char*18, float, double, long double und void*. Der Zeiger-Übergabeoperator void* wird zur Anzeige von Zeigeradressen verwendet, z.B.: int i; cout << &i; // zeigt die Zeigeradresse in hexadezimaler Schreibweise an 15 vgl. 3.4.2 Linksshift zur Bitmanipulation 17 Das sind die grundlegenden Typen oder beliebige, überladene Typen 18 Behandlung als Zeichenkette 16 20 Die Programmiersprache C++ Die Formatierung der Ein-/ Ausgabe wird durch verschiedene Formatierungs-Flags19 bestimmt, die in der Klasse ios definiert sind. Diese Flags werden über die Elementfunktionen flags, setf und unsetf erkannt und ersetzt. Formatvariable können über einen speziellen, funktionsähnlichen Operator verändert werden, der auch als Manipulator bezeichnet wird. Manipulatoren20 nehmen eine Stream-Referenz als Argument entgegen und liefern eine Referenz auf denselben Stream zurück. Stream-Eingabe Sie verwendet den überladenen Rechtsschiebe-Operator21 >>. Der linke Operand von >> ist ein Objekt der Klasse istream. Der rechte Operand kann ein beliebiger Typ sein, der für die Stream-Eingabe definiert wurde. Der Operator >> sorgt bei der Eingabe dafür, daß automatisch die nötigen Umformatierungen vorgenommen werden, z.B.: int zahl; cin >> zahl; bewirkt: Ein Folge von Ziffernzeichen wird bis zu einem Nicht-Ziffernzeichen eingelesen und in die interne Darstellung einer int-Zahl umgewandelt. Die Auswertung durch den >>-Operator hat bestimmte Eigenschaften: - Führende Zwischenraumzeichen werden ignoriert22. Zwischenraumzeichen werden als Endekennung genutzt. Andere Zeichen werden entsprechend dem verlangten Datentyp interpretiert. Umlenken Ein-, Ausgabe cin und cout können auf Betriebssystemebene mit < bzw. > umgelenkt werden. So können mit dem Umlenkungszeichen < und > die zugehörigen Eigabedaten bspw. aus einer Datei anstelle von der Tastatur geholt werden, und anstelle der Bildschirmausgabe kann in eine Datei geschrieben werden. 19 vgl. 3.4.2 vgl. 3.4.2 21 vgl. 3.4.3 22 Sollen Zwichenraumzeichen nicht ignoriert werden, ist die Funktion get() zu verwenden, die zum Einlesen einzelner Zeichen (keine Zahlen) verwendet werden kann, z.B.: // einzelnes Zeichen einlesen char zch; cin.get(zch); 20 21 Die Programmiersprache C++ 1.2.2.6 Hauptprogramm In den Zeilen /* 18 */ bis /* 27 */ steht das durch das Schlüsselwort main gekennzeichnete Hauptprogramm. Jedes ausführbare C++-Programm muß genau eine Funktion "main" enthalten, mit deren Aktivierung das Betriebssystem die Kontrolle an das Programm übergibt. Das Programm endet üblicherweise mit der letzten Anweisung in main(), mit der ein Fehlercode an das BS übergeben werden sollte. Mit "return" wird die Ausführung eines Funktionsaufrufs beendet. Die geschweiften Klammern in Zeile /* 19 */ bzw. /* 27 */ bestimmen den Beginn ({) und das Ende (}) eines Blocks. Das sog. Hauptprogramm des einführenden Beispiels könnte auch so aussehen: /* /* /* /* /* /* /* /* /* /* 18 19 20 21 22 23 24 25 26 27 */ int main() // kann auch so geschrieben werden: int main(void) */ { */ eingabe(x,n); */ cout << "Ausgabe unsortiert: " << endl; */ ausgabe(x,n); */ cout << "Sortieren durch Austauschen" << endl; */ bsort(x,n); */ ausgabe(x,n); */ return 0; // kann entfallen */ } Der Aufruf der Funktion void exit(int) (deklariert in <stdlib.h>) terminiert das Programm. Eine return-Anweisung bewirkt das gleiche, wie der Aufruf von exit() mit dem return-Wert als Argument. Der Rückgabetyp von main() soll int sein23. Return kann auch weggelassen werden, dann wird automatisch 0 zurückgegeben. Die beiden folgenden Formen von main() sind mindestens gefordert und werden daher von jedem Compilerhersteller zur Verfügung gestellt: int main() { ... return 0; } // Exit-Code bzw. int main(int argc, char* argv[]) { ... return 0; // Exit-Code } Die zweite Variante verwendet Zeiger (char*) und C-Arrays. Es bleibt dem Hersteller eines Compilers überlassen, ob er weitere Versionen anbietet. 23 ist implementierungsabhängig 22 Die Programmiersprache C++ 1.2.2.7 Kommandozeilenversionen bzw. Konsolenanwendungen von Visual C++ Version 6.0 bzw. Dev-C++ Das vorliegende Programm des einführenden Beispiels benutzt die Kommandozeilenversion des GNU-Compilers und ist eine Konsolenanwendung, d.h. eine Anwendung ohne eine grafische Benutzeroberfläche. Eine Konsolenanwendung wird unter Windows im (MS-DOS-) Eingabefenster ausgeführt. Das weit verbreitete System Windows unterstützt im starken Maße das Arbeiten mit grafischen Benutzeroberflächen (GUI). So sind die C++-Compiler für Windows (z.B. C++Builder, Visual C++) mit einer integrierten Entwicklungsumgebung (IDE) ausgestattet, von der alle für die Programmerstellung wichtigen Werkzeuge und Informationen aufgerufen werden können. Die wichtigsten C++-Compiler unter Windows ermöglichen auch den Einsatz von Kommandozeilenversionen. 1. Kommandozeilenversion von Visual C++ Version 6 Dieser Compiler befindet sich im BIN-Verzeichnis von Visual C++. Seine EXE-Datei heißt: cl.exe. Dieser Compiler wird aufgerufen über die (MS-DOS-) Eingabeaufforderung24, die unter Windows über das Menü START/PROGRAMME aufgerufen wird. Zum Aufruf des Compiler muß bekannt sein, in welchem Verzeichnis die EXE-Datei des Compiler zu finden ist. Weiterhin muß der Compiler wissen, wo die „include“-Dateien und die „lib“-Dateien zu finden sind. Zu diesem Zweck muß die Systemumgebung durch Erweiterung des Pfads (PATH) und Setzen einiger Umgebungsvarianten angepaßt werden. Microsoft liefert mit dem Visual C++ - Compiler eine Batch-Datei mit dem Namen vcvars32.bat aus, die alle nötigen Einstellungen vornimmt. Folgende Vorgehensweise empfieht sich: 1. Aufruf des (MS-DOS)-Eingabefensters über START/PROGRAMME 2. Wechseln im Eingabefenster mit dem DOS-Befehl cd in das BIN-Verzeichnis von C++. In diesem Verzeichnis sollte sich auch vcvars.bat befinden. 3. Aufruf von vcvars32.bat Bsp.: Erstellen einer Konsolenanwendung mit cl.exe 1. Aufruf des Notepad-Editor oder irgendeines beliebigen anderen Editors, mit dem man ASCIITextdateien erstellen kann. Eingabe des Quelltexts, z.B.: #include <iostream.h> int main() { cout << "Herzlich Willkommen" << endl; return 0; } 2. Speichern des Quelltexts 3. Aufruf der Eingabeaufforderung; Ausführen von vcvars32.bat; Wechseln in das Verzeichnis, in dem die Quelldatei steht 4. Aufruf des Compiler zum Erstellen des Programms 5. Falls das Übersetzen fehlerfrei erfolgt ist, Ausführen vom fertigen Programm 24 Unter fensterbasierenden Betriebssystem sind Konsolenanwendungen darauf angewiesen, daß ihnen das Betriebssystem ein Standardfenster zuweist, über das Ein- und Ausgabe erfolgen können. Dieses Fenster nennt man üblicherweise Konsole oder Konsolenfenster; unter Windows heißt es Eingabeaufforderung. 23 Die Programmiersprache C++ Abb.: Aufruf des Compiler von der Kommandozeile Standarausgabe für Konsolenanwendungen ist das Fenster der Eingabeaufforderung, Standardeingabe ist die Tastatur. Nur wenn die Eingabeaufforderung den Fokus hat, werden Tastatureingaben an die im Fenster der Eingabeaufforderung ausgeführte Konsolenanwendung geschickt. 2. Konsolenanwendung in IDE unter Visual C++ Version 6 Visual C++ umfaßt zwei Compiler - eine Befehlszeilen-Compiler eine alles-in-einem integrierte Entwicklungsumgebung (IDE) Die IDE erlaubt nicht - einfach das Anlegen einer Datei die Eingabe des Quelltexts das Erstellen einer ausführbaren Datei aus dem Quelltext Statt dessen wird verlangt, daß alle Dateien, die zu einem Programm gehören, in einem Projekt25 zusammen gefaßt werden. Bsp.: Erstellen einer Konsolenanwendung in IDE 1. Aufruf von Visual C++ 2. Anlegen eines Arbeitsbereichs und eines Projekts Aufruf des Befehls DATEI/NEU; Anzeigen der Seite PROJEKTE; Eingabe eines Namens für das Projekt und Auswahl eines übergeordneten Verzeichnisses für das Projekt. Visual C++ legt unter diesem Verzeichnis ein Unterverzeichnis für das Projekt an, das den gleichen Namen trägt wie das Projekt. 25 Projekte werden ihrerseits wieder in Arbeitsbereiche organisiert. Das ist vor allen interessant, wenn mehrere ausführbare Dateien irgendwie zusammengehören (bspw. eine EXE-Datei und eine DLL, die von der EXE-Datei aufgerufen wird). 24 Die Programmiersprache C++ Abb.: Anlegen eines neuen Projekts 3. Drücken auf OK. Es erscheint ein Dialogfeld, in dem ausgewählt werden kann, wie weit das Projekt von Visual C++ schon vorab konfiguriert und mit Code ausgestattet werden soll. Wähle die Option26 EIN LEERES PROJEKT; Drücke auf FERTIGSTELLEN. 4. Aufnahme einer Quelltextdatei in das Projekt. Auf der Seite DATEIEN Eingabe eines Namens für die hinzuzufügende Datei; Klick auf OK zur Aufnahme der Datei in das Projekt. 26 Verzicht auf jegliche weitere Unterstützung durch die IDE 25 Die Programmiersprache C++ Abb.: Hinzufügen einer Quelltextdatei zu einem Projekt 5. Eingabe Quelltext Die Datei wird in den integrierten Quelltexteditor geladen, der Quellcode kann direkt eingegeben werden. 6. Erstellen des Programms Im Ausgabefenster, das in dem unteren Rand des IDE-Rahmenfensters integriert ist, wird der Fortgang des Erstellungsprozesses angezeigt. 7. Ausführen des Programms Aufruf des Befehls ERSTELLEN/AUSFÜHREN (von pr12272.exe) bzw. STRG+F5 Abb.: Ausführen einer Konsolenanwendung 26 Die Programmiersprache C++ Die Meldung „Press any key to continue“ wird von der Visual C++ - IDE hinzugefügt. Sie verhindert, daß das Konsolenfenster gleich verschwindet. Zum Schließen des Fensters drücke eine beliebige Taste. 3. Dev-C++ GCC-Compiler27, "Win32 native executables" für Konsole und grafische Benutzeroberflächen, auch DLLs und statische Bibliotheken werden durch das "GNU General Public License" – Produkt Dev-C++ mit einer IDE (Integrated Development Environment) unterstützt. Dev-C++ läuft erfolgreich unter Windows 2000 bzw. XP. Nach dem Aufruf erscheint folgendes Fenster: Abb.: Ausgangspunkt für Programmentwicklung ist der Menüpunkt FILE | OPEN PROJECT OR FILE. Danach können die Quellen für das Programm ausgewählt werden. Über die Kartenreiter werden die Quellen im Editor der IDE angezeigt. Das Kompilieren, Binden besteht aus dem - Präprozessor, z.B. zum Prüfen und Ersetzen der Makros und include-Dateien. - Compiler zur Transformation des Quellcodes in Assembler - Assembler zum Erzeugen von Maschinencode (binary object code) - Linker zum Zusammenstellen von Object-Code zu einer ausführbaren Datei. 27 Dev-C++ kann auch in Kombination mit cygwin oder anderen GCC-Übersetzern aufgerufen werden. 27 Die Programmiersprache C++ Abb. Das Kompilieren wird ausgelöst über den Menüpunkt EXECUTE (Einfügen) und Klick auf COMPILE. Daraufhin werden Compiler und Linker automatisch aufgerufen. Im unteren Bereich des von der IDE bereitgestellten Fensters wird der Übersetungsvorgang protokolliert bzw. eine Liste mit Fehlern angezeigt. Durch Klick auf RUN im Menü EXECUTE läuft das Programm im Rahmen eines Textbildschirms ab: Abb.: Textbildschirm zum Programm pr12282.cpp 28 Die Programmiersprache C++ In Dev-C++ kann ein Projekt zur Verwaltung mehrerer Quelldateien herangezogen werden. Es gibt unterschiedliche Projekttypen: - Windows Application (Windows-Programm, das WIN32 API verwendet - Console Application - Static Library (erzeugt ein leeres Projekt und die Option, die der Linker zum Aufbau einer statischen Bibliothek benötigt) - DLL (erzeugt eine WIN32 Dynamic Library) 29 Die Programmiersprache C++ 1.2.2.8 Visual C++ .NET Visual Studio .NET (für ASP28-Webanwendungen und Desktopanwendungen umfasst Entwicklungstools für Visual C++, Visual Basic und Visual C#, unterstützt das .NET Framework29 über Common Language Runtime (CLR) und stellt vereinheitlichte Programmierklassen bereit. Es enthält die MSDN (Microsoft Developer Network) – Library (Dokumentations-Sammlung für Entwicklungstools). Sowohl für Visual C++ .NET als auch Visual Basic .NET bzw. Visual C# .NET 30 kann die gleiche IDE (Integrated Development Environment, Integrierte Entwicklungsumgebung) verwendet werden. Projekte anlegen und verwalten Visual Studio ordnet den Quellcode in Projektmappen an. Das sind Ordner, in denen eine oder mehrere Projekte gespeichert werden. Beim Erzeugen eines neuen Projekts, wird automatisch eine neue Projektmappe erstellt. Die Projektmappe dient dazu, Einstellungen, die für mehrere Projekte gelten, zusammen zu verwalten. Einer Projektmappe können mehrere Projekte hinzugefügt werden. Am schnellsten lässt sich ein Projekt über die Startseite anlegen. 28 Durch ASP .NET (aufgebaut auf der Programmierklasse des .NET Framework) wird ein Webanwendungsmodell mit einem Satz von Steuerelementen sowie die Infrastruktur für die Erstellung von Webanwendungen bereitgestellt. 29 Das Framework stellt den Entwicklern einen vereinheitlichten, objektorientierten, hierarchischen und erweiterten Satz an Klassenbibliotheken zur Verfügung. Durch die Entwicklung eines gemeinsamen Satzes an APIs für alle Programmiersprachen wird über die Common Language Runtime sprachübergreifende Vererbung, Fehlerbehandlung und Debuggen möglich. 30 Visual C# .NET (steht für "C Sharp") ist eine neue objektorientierte Programmiersprache, eine Weiterentwicklung von C und C++. 30 Die Programmiersprache C++ Abb. Der Eröffnungsbildschirm von Visual Studio .NET Projektmappen-Explorer. Er bildet den Ausgangspunkt für die Navigation zu den verschiedenen Teilen der Entwicklungsprojekte. Diese Ansicht gestattet es, die Teile einer Anwendung in drei verschiedenen Modi zu betrachten: - die KLASSENANSICHT ermöglicht die Navigation und die Bearbeitung von Quellcode auf C++Klassenebene. - die RESSOURCENANSICHT erlaubt das Aufsuchen und Bearbeiten verschiedener Ressourcen in der Anwendung. Dazu gehören Entwürfe von Dialogfenstern, Symbolen und Menüs. Diese Ansicht ist nur verfügbar, wenn ein Projekt geöffnet ist. - Die PROJEKTMAPPEN-EXPLORER-ANSICHT (Beschriftung: Projektmappen-Explorer) bietet eine Übersicht über die Dateien, aus denen die Anwendung besteht. Man kann Dateien anlegen und in ihnen navigieren. Da auch mehrere Projekte gleichzeitig geöffnet sein können, kann innerhalb des Projektmappen-Explorer zwischen Projekten navigiert werden. Die anfängliche Sammlung von Ansichten nach der Installation von Visual .NET ist nur die Standard-Anordnung. Es ist möglich, die ursprünglichen drei Ansichten in jedem beliebigem Bereich der IDE zu ziehen und der Gruppierung selbst neue Ansichten hinzuzufügen. Der Ausgabebereich. Nach dem Kompilieren der ersten Anwendung erscheint der Ausgabebereich am unteren Rand der Visual-Umgebung und bleibt bis zum Schließen geöffnet. Im Ausgabebereich stellt die IDE von Visual Studio alle relevanten Informationen bereit. Hier kann der Fortschritt (Warnungen, Fehlermeldungen) beim Kompilieren verfolgt werden. Auch der Debugger von Visual C++ zeigt im Ausgabebereich alle Variablen und ihre aktuellen Werte an,, wenn man den Code schrittweise bearbeitet. 31 Die Programmiersprache C++ Der Editorbereich umfaßt die Beschreibung der Entwicklungsumgebung, der nicht anderweitig von Ansichten, Menüs oder Symbolleisten eingenommen wird. Hier erscheinen die Fenster des Quellcode-Editors, falls C++-Quellcode bearbeitet wird. Hier wird der Dialog-Designer angezeigt, falls ein Dialogfeld entworfen wird. Der Editorbereich dient auch dem Symbol-Editor zur Anzeige, wenn man Symbole für den Einsatz in den Anwendungen gestattet. Menüleisten. Beim ersten Start von Visual C++ erscheinen unterhalb der Menüleiste zwei Symbolleisten: - Die Standardsymbolleiste enthält die meisten Standardwerkzeuge für das Öffnen und Speichern von Dateien, Ausschneiden, Kopieren, Einfügen und verschiedene allgemeine, gebräuchliche Befehle. - Die andere Symbolleiste enthält plausible Funktionen für Aktivitäten im Editorbereich. Weitere Symbolleisten sind verfügbar. Programmentwicklung Programmentwicklung im Visual Studio ist organisiert in Lösungen, die ein oder mehrere Projekte umfassen. Zu Beginn der Programmentwicklung steht deshalb das Erstellen eines neuen Projekts. Auf der Startseite befinden sich neben Hyperlinks auf bereits erstellte Projekte auch eine Schaltfläche NEUES PROJEKT, über die ein neues Projekt angelegt werden kann. Ein andere Möglichkeit besteht in der Wahl des Menüpunkts FILE | NEW | PROJECT. 1. Erstellen einer prozedurorientierten C/C++-Konsolenanwendung Aufgabenstellung. In C++ enthält die Standard Template Library ein mit einem CArray vergleichbaren Container mit dem Namen "vector". STL-Container sind als Template-Klassen implementiert. Die folgende Konsolenanwendung soll die Vorteile der STL-Container-Klasse "vector" gegenüber einem C-Array zeigen. Zur Demonstration der Arbeitsweise von "vector", soll ein prozedurorientiertes C++Programm ganze Zahlen einlesen, sie sortieren und anschließend ausgeben. Lösung. Die Lösung soll im Rahmen einer Konsolenanwendung durch ein Programm in Visual C++.NET erfolgen. 1. Schritt: Start eines neuen Projekts. Ausgangspunkt ist der Eröffnungsbildschirm von Visual Studio .NET. Mit DATEI | NEU und dem Untermenü PROJEKT eröffne das Dialogfeld NEUES PROJEKT. In diesem Dialogfelkd links unter PROJEKTTYPEN die Option VISUAL C++-Projekte und rechts unter VORLAGEN die Option WIN32Projekt wählen. Nach Eingabe des Namens vom neuen Projekt und Wahl des Speicherorts für die Dateien des Projekts Klick auf OK. 32 Die Programmiersprache C++ Abb.: Das Dialogfeld Neues Projekt zum Erstellen einer prozedurorientierten Anwendung 2. Schritt. Das WIN32-Anwendungs-Assistent-Dialogfeld wird als Nächstes (nach dem OK im Dialogfeld Neues Projekt) geöffnet. Abb.: Win32-Anwendungs-Assistent-Dialogfeld 33 Die Programmiersprache C++ Öffnen der Seite ANWENDUNGSEINSTELLUNGEN, danach Aktivieren unter ANWENDUNGSTYP Konsolenanwendung mit der zusätzlichen Option Leeres Projekt: Abb.: Die Seite Anwendungseinstellungen des Win32 Anwendungs-Assistenten Nach dem Klick auf FERTIG STELLEN erstellt der WIN32-ANWENDUNGS-ASSISTENT automatisch eine Reihe von Dateien. Es fehlt aber eine Datei, die den C-Quelltext ("Source") aufnimmt. 3. Schritt. Über den Projektmappen-Explorer, Klick mit der rechten Maustaste auf Quelldateien. Über Hinzufügen->Neues Element hinzufügen wird eine Quelldatei mit Hilfe des Fensters "Neues Element hinzufügen" ausgewählt 34 Die Programmiersprache C++ Abb. Als Template für die neue Quelle wird C++-Datei gewählt. 4. Schritt. Nach dem Klick auf ÖFFNEN im Dialogfeld "Neues Element hinzufügen" öffnet sich die neue (leere) Datei im Editor-Fenster. 5. Schritt. Mit Hilfe des eingebauten Editors wird folgender Quelltext eingegeben: #include <stdio.h> int main(void) { printf("Hallo Welt\n"); return 0; } Der angegebene Quelltext hat mit der gewünschten Lösung nichts zu tun, kann aber erfolgreich übersetzt und zum Laufen gebracht werden. 6. Schritt: Übersetzen zu einem lauffähigen Programm (Build bzw. Erstellen) Mit ERSTELLEN | KOMPILIEREN wird das aktuelle Projekt in ein ausführbares Programm übersetzt. Bei dem Übersetzungsvorgang wird u.a. die syntaktische Korrektheit des Programms überprüft. Treten dabei Fehler auf, werden sie im Build-Fenster (links unten) angezeigt. Solange das Programm nicht korrekt übersetzbar ist, kann es auch nicht ausgeführt werden. 35 Die Programmiersprache C++ Abb. 7. Schritt: Starten (ausführen) des Programms Wenn das Programm erfolgreich übersetzt ist, kann es u.a. mit DEBUGGEN | STARTEN OHNE DEBUGGEN ausgeführt werden. Es wird das folgende Konsolen-Fenster erzeugt: Abb. 36 Die Programmiersprache C++ 8. Schritt. Lösung der eigentlich gestellten Aufgabe mit einem C-Array und der in stdlib vorhandenen Funktion qsort()31. Die vorliegende Quelle wird mit folgendem Quelltext überschrieben: #include <stdlib.h> #include <iostream.h> // a und b zeigen auf zwei Ganzzahlen. cmp gibt -1 zurueck, // wenn a kleiner als b ist, 0 bei Gleichheit und 1, wenn // a groesser als b ist inline int cmp(const void *a, const void *b) { int aa = *(int *) a; int bb = *(int *) b; return (aa < bb) ? -1 :(aa > bb) ? 1 : 0; } // Einlesen einer Liste ganzer Zehlen von der Standard-Eingabe // Sortieren mit qsort() aus der C-Bibliothek // Ausgabe der Liste int main(void) { const int size = 10; // Array mit max. 10 Elementen int array[size]; int n = 0; // Einlesen einer ganzen Zahl in das jeweilige n+1.te Element while (cin >> array[n++]); n--; // Einmal wurde zuviel gezaehlt qsort(array, n, sizeof(int), cmp); for (int i = 0; i < n; i++) cout << array[i] << " "; cout << endl; return 0; } Anstatt #include <stdio.h> wurde hier #include <iostream.h> geschrieben. Das C-Header stdio.h enthält C-spezifische Funktionen zur Ausführung standardmäßiger I/O-Operationen in C. Durch den Wechsel von stdio.h in iostream.h werden in C++-spezifische Definitionen eingebunden. Die Extension ".h" zeigt, dass die Anweisung die Programmsyntax alten Stils verwendet. Der vorliegende Quelltext führt nach Übersetzen und Ausführen zu einer korrekten Lösung. 9. Schritt. 2.Version mit STL-Komponenten. Die STL besteht aus drei separaten Komponenten: Container, Algorithmen, Iteratoren. Zur Lösung der Aufgabe sollen geeignete Komponenten ausgewählt werden. Ein geeigneter Container ist "vector"32. Ein Algorithmus definiert eine Verhaltensweise eines Containers oder wendet eine gewisse Funtion (sort())33 auf einen Container an, um dessen Inhalt zu be- oder zu verarbeiten. In der STL werden Algorithmen durch Template-Funktionen repräsentiert. Sie können nicht nur mit STL-Containern, sondern auch mit gewöhnlichen C-Arrays oder anderen anwendungsspezifischen Containern verwendet werden. Die Interaktion zwischen Containertyp und dem Verhaltem der im Container gespeicherten Daten steuern Iteratoren34. Sie ähneln Zeigern, mit denen auf Datenelemente zugegriffen wird. In der STL wird ein Iterator-Objekt durch eine Iterator-Klasse repräsentiert. Mit dem Operator "++" kann ein Iterator inkrementiert werden, mit dem Operator "*" kann auf ein einzelnes Member des ausgewählten Elements zugegriffen werden. Es gibt verschiedene Klassen von Iteratoren, die mit speziellen Containertypen verwendet werden. Sollen Elemente eines Containers durchlaufen werden, verwendet man einen Iterator. Die Quelle wird durch folgenden Quelltext überschrieben: 31 vgl. 5.1.1 vgl. 2.2.3.2 bzw. 5.2.8 33 vgl. 5.4 34 vgl. 5.3 32 37 Die Programmiersprache C++ #include <vector> #include <algorithm> #include <iostream> using namespace std; // Einlesen einer Liste ganzer Zahlen von der Standard-Eingabe // Sortieren mit dem STL-Algorithmus sort() aus der STL // Ausgabe der Liste int main(void) { vector<int> v; // erzeugt einen leeren vector /* Eingabe */ int eingabe; while (cin >> eingabe) // Solange nicht end of file v.push_back(eingabe); // Anhaengen an den vector /* Verarbeitung */ sort(v.begin(),v.end()); // Der STL-Algorithmus sort() nimmt 2 Random-Access-Iteratoren // und sortiert die dazwischen liegenden Elemente /* Ausgabe */ int n = v.size(); for (int i = 0; i < n; i++) cout << v[i] << " "; cout << endl; return 0; } Die neue Quelltextversion enthält die Anweisung using namespace std. Diese Anweisung ist eine ANSI/ISO-C++-Anforderung an den Code. Das C++-Schlüsselwort using legt den Gültigkeitsbereich von Bezeichnern fest. Zur Vermeidung von Namenskonflikten, kann der Programmierer Definitionen in Namespaces einkapseln. Alle Namen, die zu Standard-C++ gehören, werden durch den Namespace mit dem standardisierten Namen std geschützt. Die using-Anweisung funktioniert logischerweise wie eine #include-Anweisung, indem sie die Definitionen einbindet, so dass die Namen in diesem Programm gültig sind. Die #include-Dateinamen wurden ohne die Extension ".h" angegeben. Der Dateiname ohne die Dateierweiterung .h zeigt an, dass die neuen ANSI/ISO-C++-Anforderungen an den Code erfüllt sind. Von C übernommene Header werden auch ohne die Extension ".h", aber durch Voranstellen des Buchstabens "c" angegeben, z.B. "cstdio" ersetzt "stdio.h". 10. Schritt: Auf- und absteigende Sortierung Neben Containern, Algorithmen und Iteratoren liefert die STL u.a.a Funktionsobjekte. Im Header functional sind Klassen definiert35, die zum Erzeugen diverser Funktionsobjekte dienen. sort() kann als 3. Parameter ein derartiges Funktionsobjekt aufnehmen. Die Quelle wird zur auf- und absteigenden Sortierung mit folgendem Quelltext überschrieben. #include #include #include #include <vector> <algorithm> <functional> <iostream> using namespace std; // Einlesen einer Liste ganzer Zahlen von der Standard-Eingabe // Sortieren mit dem STL-Algorithmus sort() aus der STL // Ausgabe der Liste int main(void) { vector<int> v; 35 // erzeugt einen leeren vector vgl. 5.1.2.2 38 Die Programmiersprache C++ /* Eingabe */ int eingabe; cout << "Gib ganze Zahlen ein, "; cout << "Return nach jeder Zahl, <CTRL-Z> am Ende der Eingabe.\n"; while (cin >> eingabe) // Solange nicht end of file v.push_back(eingabe); // Anhaengen an den vector /* Verarbeitung */ cout << "Aufsteigende Sortierung.\n"; sort(v.begin(),v.end()); // Der STL-Algorithmus sort() nimmt 2 Random-Access-Iteratoren // und sortiert die dazwischen liegenden Elemente /* Ausgabe der aufsteigend sortierten Ganzzahlen */ for (vector<int>::const_iterator viter = v.begin(); viter != v.end(); viter++) cout << *viter << " "; cout << endl; /* Aufsteigend sortierte Elemente im vector absteigend sortieren */ sort(v.begin(),v.end(),greater<int>()); /* Ausgabe der absteigend sortierten ganzen Zahlen */ cout << "Absteigende Sortierung.\n"; for (vector<int>::const_iterator viter = v.begin(); viter != v.end(); viter++) cout << *viter << " "; cout << endl; return 0; } Nach erfolgreichem Kompilieren erzeugt das Programm die folgende Ausgabe in einem Konsolenfenster. Abb. 39 Die Programmiersprache C++ 2. Erstellen einer prozedurorientierten C/C++-Windows-Anwendung Grundlagen von Windows. Drei Eigenschaften der Windows-Umgebung sind für den Programmierer von Windows-Anwendungen von besonderem Interesse: - Windows arbeitet mit grafischen Benutzeroberflächen Windows-Anwendungen werden aus Sicht des Benutzers in Fenstern ausgeführt. Windows arbeitet ereignisorientiert Aufgabe: Erstellen einer prozedurorientierten Anwendung mit dem Win32 Anwendungs-Assistenten zur Bearbeitung von WM_PAINT-Meldungen. Lösung. Sie umfasst folgende Schritte: 1. Öffne das Dialogfeld NEUES PROJEKT (DATEI | NEU | PROJECT) Abb. Das Dialogfeld Neues Projekt zum Erstellen einer prozedurorientierten Windows-Anwendung 2. Im Dialogfeld NEUES PROJEKT links unter PROJEKTTYPEN die Option VISUAL C++-PROJEKTE und rechts unter VORLAGEN die Option WIN32-PROJEKT wählen. 3. Eingabe von Name und Speicherort des neuen Projekts. 4. Klick auf OK. Das WIN32 ANWENDUNGS-ASSISTENT-Dialogfeld wird geöffnet. 40 Die Programmiersprache C++ Abb. Das Win32 Anwendungs-Assistent-Dialogfeld 5. Öffne die Seite ANWENDUNGSEINSTELLUNGEN, danach unter ANWENDUNGSTYP die Option WINDOWS-ANWENDUNG. Nach dem Klick auf FERTIG STELLEN erstellt der WIN32ANWENDUNGS-ASSISTENT automatisch eine Reihe von Dateien, einschl. einer Quelldatei. Diese Quelldatei kann als Vorlage verwendet werden. 41 Die Programmiersprache C++ Abb.: Die Quelldatei des Projekts Falls das Programm kompiliert und ausgeführt wird, erscheint folgendes einfaches Fenster. 42 Die Programmiersprache C++ Abb.: Das Standardfenster des Projekts 6. Einfügen einfacher Grafiken in den Arbeitsbereich des Projekt-Standardfensters. 6.1 Aufsuchen des folgenden Code in der Quelldatei case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: Fügen Sie hier den Zeichnungscode hinzu... EndPaint(hWnd, &ps); break; Verändere das Code-Fragment folgendermaßen: /* Zeichnet 2 Linien und etwas Text in den Arbeitsbereich */ MoveToEx(hdc,0,0,NULL); LineTo(hdc,639,429); MoveToEx(hdc,300,0,NULL); LineTo(hdc,50,250); TextOut(hdc,120,30,"<- einige Linien ->",19); 6.2 Überesetzung und Start des ablauffähigen Programms Es werden in das Fenster zwei Linien gezeichnet. 6.3 Das Graphic Device Interface Für das Zeichnen von Grafik und Text unter Windows steht eine leistungsfähige Bibliothek bereit – das Graphic Device Interface (GDI). Die GDI ist hardwareunabhängig. Ein Kreis wird sowohl auf dem Bildschirm als auch auf dem Drucker mit gleichen Befehlen gezeichnet. Die Unterscheidung erfolgt durch den Gerätekontext (Device Context – definiert in der Datenstruktur HDC (Handle to Devive Context). Das Zeichnen auf den Bildschirm (oder einen Drucker) stellt einen wichtigen Aspekt von WindowsProgrammen dar. Windows stellt dafür einen Satz von geräteunabhängigen Funktionen zur Verfügung, die über entsprechende Treiber oder Bibliotheken die Hardware bedienen. Von den Gerätetreibern werden Information zu 5 logischen Zeichenobjekten umgesetzt: - Stift (zum Zeichnen von Geraden, Linien) - Pinsel (zum Füllen von Bereichen) - Schriftarten (zur Anzeige von Text) - logische Farben (zur Beschreibung von Farben) Der Gerätekontext ist die Verbindung zwischen einem Windows-Programm und dem Gerätetreiber. Bevor Grafiken ausgegeben werden, wird zunächst der entsprechende Kontext angefordert. Damit erhält man nicht nur rinr Freigabe zum Ausgeben auf einem Gerät, sondern auch eine Kennung (Handle), die den GDI-Funktionen mitteilt, wo die grafische Ausgabe zu erfolgen hat. Ein wichtiger datentyp ist daher der Handle auf einen Gerätekontext (Handle to Device Context). Er schafft die Verbindung zwischen einem grafischen Befehl unter Windows und dem Ausgabegerät. In jedem Gerätekontext sind etwa 20 Zeichenattribute gespeichert. Die Koordination der Ausgabe wird vom Windows-Manager übernommen. Die Clipping36Informationen werden dabei in einer Datenstruktur kombiniert, die man Clipping-Bereich nennt. Es gibt 3 Window-Manager-Routinen bzgl. Clipping, die zum Erhalt eines Gerätekontexts dienen: - BeginPaint, EndPaint:Clippen des ungültigen Teils des Client-Bereichs. Dies dient der "Reperatur" eines Fensters, dessen Notwendigkeit Windows mit der Meldung WM_PAINT anzeigt. GetDC, ReleaseDC: Clippen des ungültigen Teils des Client-Bereichs. Dies dient der "Reperatur" eines Fensters, deren Notwendigkeit Windows mit der Meldung WM_PAINT anzeigt. GetWindowDC, ReleaseDC: Clippen des gesamten Fensters, Client- und Nicht-Client-Bereiche. Dies wird jedoch normalerweise von der Standard-Fensterprozedur übernommen. 6.4 Funktionen für das Zeichnen von Grafik und Text unter Windows 36 Unter Clipping versteht man das Trennen der Bildschirm-Ausgaben verschiedener Programme. Es ähnelt dem Erstellen imaginärer Zäune um den Zeichenbereich eines Programms. Daten können dann nicht außerhalb dieses Zaunes ausgegeben werden. 43 Die Programmiersprache C++ Text. Für die Ausgabe von Text stehen 5 Routinen zur Verfügung: DrawText, ExtTextOut, GrayString, TabbedTextOut und TextOut TextOut(hDC,x,y,lpString,nCount) Linien. Jede Linie besitzt einen Start- und Endpunkt, es wird der Inklusiv-Exklusiv-Algorithmus verwendet, d.h.: Der Stratpunkt gehört zur Linie, der Endpunkt jedoch nicht. Das GDI verfügt über eine Reihe von Funtionen zur Drstellung von Linien und Kurven. MoveToEx(hdc,x,y,&pt) LineTo(hdc,x,y) Rectangle(hdc,x1,y1,x2,y2) Ellipse(hdc,x1,y1,x2,y2) Chord(hdc,x1,y2,x2,y2,x3,y3,x4,y4) Pie(hdc, x1,y2,x2,y2,x3,y3,x4,y4) TextOut(hdc,x,y,"text",count) setzt die Zeichenposition und speichert die alte in 'pt' malt eine Linie von der aktuellen Zeichenposition zu dem angegebenen Punkt zeichnet ein Rechteck, der durch die Koordinaten x1, y1, und x2,y2 beschrieben wird. Alle Parameter außer dem Handle hdc sind vom Typ int Das Handle für den Gerätekontext ist durch hdc gegeben. Alle anderen Parameter sind vom Typ int. Zurückgegeben wird ein Wert vom Typ bool. Damit kann eine Linie zwischen 2 Punkten x3, y3 und x4,y4 gezeichnet werden. Von jedem der Punkte (x3,y3) bzw. (x4,y4) werden zwei Linien zum Mittelpunkt des den Kreis bzw. die Ellipse umschreibenden Rechtecks gezeichnet. zeichnet einen Text auf den DC Abb.: Wichtige Zeichenfunktionen der GDI Ergänze den angegeben Quellcode zum Zeichnen von zwei Linien folgendermaßen /* Zeichnen einer Ellipse */ Ellipse(hdc,550,100,625,150); TextOut(hdc,440,115,"eine Ellipse ->",15); /* Zeichnen eines Bogensegments in den Arbeitsbereich */ Chord(hdc,550,20,630,80,555,25,625,70); TextOut(hdc,410,30,"ein Bogensegment ->",19); /* Zeichnen eines Kreissegments */ Pie(hdc,300,50,400,150,300,50,300,100); TextOut(hdc,350,70,"<- Ein Kreissegment ->",19); /* Zeichnen eines Rechtecks */ Rectangle(hdc,500,200,600,300); TextOut(hdc,610,250,"<- Ein Rechteck",15); Nach Übersetzen und Start zeigt das Fenster die Wirkungsweise der grafischen Grundfunktionen. 6.5 Farb- und Mustereinstellungen bei Zeichenfunktionen Die Zeichenfunktionen enthalten keine Parameter für Farbe und Stil. Aus Effizienzgründen sind diese Parameter global einzustellen. Die Füllfarbe wird definiert mit einem Pinsel (Brush), z.B.: HBrush hBrush = CreateSolidBrush(RGB(0,255,0); // oder Farbkonstanten Die Linieneigenschaften werden definiert mit einem Stift (Pen), z.B.: HPen hPen = CreatePen(Style,Width,cl); mit Style = PS_SOLID oder PS_DASH oder PS_DOT oder Kombinationen. Sowohl Pinsel als auch Stift müssen dem GDC bekannt gemacht werden und sollten nach Gebrauch wieder auf den alten Wert rückgesetzt werden, z.B.: HRBRUSH hOldBrush = (HBRUSH) SelectObject(hdc,hBrush); /* --- Zeichenoperationen ---*/ DeleteObject(SelectObject(hdc,hOldBrush)); hOldBrush = NULL; 44 Die Programmiersprache C++ 3. Verwaltete C++-Anwendung Mit verwaltetem C++ wird eine Anwendung für die Common Language Runtime (CLR) erstellt. Die CLR entspricht grundsätzlich der Virtual Machine von Java. Es handelt sich um eine binäre Maschinensprache für einen Prozessor, der nicht wirklich existiert. Der Zweck der CLR ist, Anwendungen für die CLR anstatt für einen bestimmten Computerprozessor zu kompilieren. Der Vorteil dabei ist, dass nun jede Anwendung auf jeder Hardware laufen kann, für die es eine Version der CLR gibt. C und C++ sind deshalb so beliebt, weil sie sich direkt in die Maschinensprache kompilieren lassen. Das widerspricht aber dem Konzept, das der CLR zugrunde liegt. Mit verwaltetem C++ wird eine Anwendung für die CLR kompiliert anstatt für native Maschinensprache. Ein Vorteil bei der Erstellung verwalteter C++-Anwendungen ist die eingebaute Objekt-Bibliothek in der .NET-Plattform. Die Präprozessor-Direktive #using kann DLLs und eigens für die CLR kompilierte Objekte importieren, künftig auch ausführbare Dateien, die für die CLR kompiliert wurden. Die wichtigste DLL ist mscorlib.dll, die viele Core-Objekte enthält, die Teil der .NET-Plattform sind. Die Direktive steht am Anfang des Quellcodes: #using <mscorlib.dll> Die Direktive using namespace ist Teil der Sprachspezifikation in C++. Sie ermöglicht die Festlegung eines Namespace und verschachtelter NamespaceBezeichner. Objekte aus dem Namespace können ohne vollständig qualifizierten Namen der Objekte verwendet werden. Arbeitsschritte für ein Projekt mit verwaltetem C++ 1. Schritt: Projektstart (Erzeugen eines neuen Projekts) 1. In der Visual Studio .NET –Umgebung wähle aus dem Menü FILE | NEW | PROJECT: 45 Die Programmiersprache C++ 2. In dem vorliegenden Fenster wird auf linken Seite Visual C++-Projekte und auf der rechten Seite "Verwaltete C++-Anwendung" gewählt. 3. Bestimme den Namen den Projekts und gib den Speicherort an, in dem das Projekt erstellt werden soll. Den Rest übernimmt Visual Studio. Visual Studio .NET erzeugt daraufhin eine Lösung mit einem einfachen Projekt. Das Projekt enthält verschiedene Dateien (u.a.a. assemblyinfo.cpp und pr12281.cpp) 2. Schritt: Sündenfall "Hallo Welt!" Quellcodemodifikationen. 1. Doppelklick auf die Datei pr12281.cpp im "Projektmappen-Explorer". Der "ProjektmappenExplorer" kann mit Hilfe des Menüpunkts "Ansicht | Projektmappen-Explorer" aufgerufen werden. 2. An einer für Veränderungen vorgesehenen Stelle der generierten Schablone trage ein: Console::WriteLine("Hallo C++ .NET Welt!"); 46 Die Programmiersprache C++ Kompilieren der Anwendung 1. Das Visual C++-Projekt kann kompiliert werden über den Menüpunkt Erstellen | pr12281 erstellen und gebunden werden. 2. Fehler und Nachrichten vom Compiler werden im Ausgabebereich der IDE angezeigt. Falls keine Fehler vorliegen, kann die Windows-Anwendung über den Menüpunkt Debuggen | Starten ohne Debugger ausgeführt werden. Programmausgabe 47 Die Programmiersprache C++ 3. Schritt: Die Programmstruktur. "using"-Direktive. Das .NET Framework versorgt den Entwickler mit zahlreichen nützlichen Klassen. Die Klasse Console ist für Ein-, Ausgaben des Konsolenfensters zuständig. Die Klassen sind in einer hierarchischen Baumstruktur organisiert. Der vollqualifizierte Name der Klasse Console ist System::Console. Andere Klassen derselben Gruppe sind bspw. System::IO::FileStream und System::Collections::Queue. Die Direktive using namespace erlaubt das Referenzieren von Klassen in dem Namensraum (namespace) ohne Angabe des vollqualifizierten Namens. Die Anweisung #using <mscorlib.dll> wird in allen Visual C++ .NET Dateien verlangt, die die Anwendung von verwaltetem Code fordern. Die Funktion _tmain() übernimmt die Steuerung, nachdem die Anwendung in den Speicher geladen ist. 4. Schritt: Eingabe über die Konsole. Nötig ist eine Aufforderung zur Eingabe von Namen der Eingabe- und Ausgabedatei. Quellcodemodifikationen: // TODO: Ersetzen Sie den Beispielcode durch Ihren eigenen Code. // Beschreibung der Programmfunktion Console::WriteLine("QuickSort C++ .NET Anwendungsbeispiel\n"); // Aufforderung zur Eingabe durch den Benutzer Console::Write("Quelle: "); String* szSrcFile = Console::ReadLine(); Console::Write("Ausgabe: "); String* szDestFile = Console::ReadLine(); // Erfolgreiche Rückkehr return 0; Einlesen von der Console: Die statische Funktion ReadLine der Klasse Console fordert den Anwender zur Eingabe auf. Den eingegebenen String nimmt die Funktion zur Weitergabe als Rückgabewert auf. Programmausgabe: Über Debuggen | Starten ohne Debuggen ergibt sich folgender Textbildschirm: 5. Schritt. Das Programm muß die eingelesenen Zeilen mit den Zeichenkettet in einen Array einlesen. Dazu ist eine .NET-Klasse nötig, die ein Array von Objekten verwalten kann. Quellcodemodifikationen: using namespace System::Collections; ArrayList* szContents = new ArrayList(); Verwendung der KLasse ArrayList. Importiert wird der Namensraum (namespace) System::Collections für den Direktbezug aus ArrayList. Diese Klasse implementiert einen 48 Die Programmiersprache C++ dynamisch erweiterbaren Array von Objekten. Zum Einfügen dient die Methode Add() der ArrayListKlasse, z.B.: String* szElement = new String("fuege mich ein"); ArrayList* szArray = new ArrayList(); szArray->Add(szElement); Die Wiedergewinnung eines gespeicherten Element erfolgt über die Angabe vom Index des gewünschten Elements durch die Methode get_Item()37, z.B.: Console::WriteLine(szArray->get_Item(2)) 6. Schritt: Dateieingabe / Dateiausgabe Jede Zeile aus der Eingabe wird in einem Zeichenketten-Array eingelesen. Danach wird der Zeichenketten-Array ausgegeben. Anschießend wird der Quicksort-Algorithmus zum Sortieren herangezogen. Quellcodemodifikationen. using namespace System::IO; ... // Einlesen der Eingabedatei String* szSrcLine; ArrayList* szContents = new ArrayList(); FileStream* fsInput = new FileStream(szSrcFile,FileMode::Open,FileAccess::Read); StreamReader* srInput = new StreamReader(fsInput); while (szSrcLine = srInput->ReadLine()) { // Anhaengen im Array szContents->Add(szSrcLine); } srInput->Close(); fsInput->Close(); //TODO: Aufruf des Quicksort // Ausgabe der sortierten Zeilen FileStream* fsOutput = new FileStream(szDestFile,FileMode::Create,FileAccess::Write); StreamWriter* srOutput = new StreamWriter(fsOutput); for (int nIndex = 0; nIndex < szContents->Count; nIndex++) { // Schreibe eine Zeile in die Ausgabedatei srOutput->WriteLine(dynamic_cast<String*> (szContents->get_Item(nIndex))); } srOutput->Close(); fsOutput->Close(); Lesen aus der Eingabedatei. Die Klasse FileStreamReader wird zum Öffnen der Eingabedatei benötigt. Der FileStreamReader wird ein StreamReader unterlegt, so dass die Methode ReadLine() von StreamReader benutzt werden kann. Falls ReadLine() NULL zurückgibt, ist das Ende der Datei erreicht. Schreiben in die Ausgabedatei. Ein StreamWriter-Objekt wird an ein FileStream-Objekt angehängt. Das ermöglicht die Anwendung der Methode WriteLine(). 7. Schritt: Erzeugen einer Funktion, die den Quicksort auf einem Array von Zeichenketten implementiert. Quellcodemodifikationen. 37 Dokumentation in der MSDN-Library 49 Die Programmiersprache C++ 4. Verwaltete C++-Anwendung mit grafischer Benutzeroberfläche und GDI+ (Dialoganwendung) .NET stellt mit "WindowsForms" System.Windows.Forms, für die Programmierung grafischer Benutzeroberflächen zur Verfügung. Zum Zeichnen gibt es GDI+, eine Weiterentwicklung der Windows-API GDI. Voraussetzung ist verwalteter Code. Windows Forms. Forms sind ein Standard-Window, ein MDI-Client, eine Dialogbox. Die Controls (Steuerelemente) einer solchen Form sind wieder Forms. Zeichnen mit .NET GDI+. Die Unterstützung zum Zeichnen erfolgt mit der "class library GDI+", bereitgestellt über "Gdiplus.dll". GDI+ besteht aus: 2D Vector Graphic (Linien, Figuren, ...), Imaging (Darstellung von Bildern), Typographie bzw. Textverarbeitung. Soll in eine Form etwas gezeichnet werden, wird die OnPaint-Methode überschrieben. GDI+ befindet sich in der Sammlung System.Drawing.dll. Alle Klassen befinden sich in den "namespaces": System.Drawing, System.Text, System.Printing, System.Internal, System.Drawing2D und System.Design. Die Klasse Graphics kapselt die Fläche zum Zeichnen. Zum Zeichnen von irgendeinem Objekt (z.B. Kreis, Rechteck) ist ein Handle zum Zeichnen auf der Zeichenfläche über die Klasse Graphics zu beschaffen (z.B. über den Parameter der Methode OnPaint). Mit einer "Graphics Reference" können folgende Klassen zum Zeichnen verschiedener Objekte herangezogen werden: DrawArc, DrawBeziew, DrawBeziers, DrawClosedCurve, DrawCurve, DrawEllipse, DrawImage, DrawLine, DrawPath, DrawPolygon, DrawPie, DrawRectangle, DrawString, FillEllipse, FillPath, FillPie, FillPolygon, FillRectangle, FillRegion. Programmerstellung. Für eine Dialoganwendung wird eine eigene Klasse von der Basis Form abgeleitet. Member dieser Klasse sind Textbox, Button und andere Controls (Steuerelemente). Die Member werden im Konstruktor erzeugt, mit den wesentlichen Eigenschaften ausgestattet und als Kinder der Hauptform zugeordnet. Den Events eines Controls, wie dem Klick eines Buttons, auf den reagiert werden soll, wird eine Event-Funktion zugeordnet. Die Event-Funktion enthält den Code, der abläuft, wenn der Button gedrückt wurde. 1. Schritt: Unter dem Projektnamen pr12283 wird eine verwaltete C++-Anwendung erstellt, die die GDI+-Komponenten aufnehmen soll. Visual Studio .NET erzeugt das Projekt mit verschiedenen Dateien (u.a.a. AssemblyInfo.cpp, pr12283.cpp). 2. Schritt: Der vorliegende Quellcode (pr12283.cpp) wird überschrieben durch #include "stdafx.h" #using <mscorlib.dll> #using <System.DLL> #using <System.Windows.Forms.DLL> #using <System.Drawing.DLL> using namespace System; using namespace System::Windows::Forms; using namespace System::Drawing; #include <tchar.h> public __gc { private: TextBox* Button* Button* Button* int public: Form1() { Size = class Form1 : public Form box; closeButton; testButton; test1Button; pensize; System::Drawing::Size(400,400); 50 Die Programmiersprache C++ box = new TextBox; box->Size = System::Drawing::Size(100,100); box->Location = System::Drawing::Point(10,10); box->Text = "- - - "; closeButton = new Button; closeButton->Location = System::Drawing::Point(10,120); closeButton->Text = "Close"; closeButton->Size = System::Drawing::Size(70,20); closeButton->Click += new EventHandler(this, CloseButtonClicked); testButton = new Button; testButton->Location = System::Drawing::Point(10,140); testButton->Text = "test Textbox"; testButton->Size = System::Drawing::Size(70,20); testButton->Click += new EventHandler(this, TestButtonClicked); test1Button = new Button; test1Button->Location = System::Drawing::Point(10,160); test1Button->Text = "test gdi+"; test1Button->Size = System::Drawing::Size(70,20); test1Button->Click += new EventHandler(this, Test1ButtonClicked); pensize = 1; this->Controls->Add(box); this->Controls->Add(closeButton); this->Controls->Add(testButton); this->Controls->Add(test1Button); } private: void CloseButtonClicked( Object* sender, EventArgs* e) { Close(); } void TestButtonClicked( Object* sender, EventArgs* e) { box->Text = "hallo welt"; Width += 10; Height -= 10; Left += 30; Top += 30; testButton->Enabled = false; } void Test1ButtonClicked( Object* sender, EventArgs* e) { int x = 200; int y = 10; int w = 100; int h = 90; pensize++; Rectangle r = System::Drawing::Rectangle(x, y, w, h); Invalidate(r); } protected: virtual void OnPaint(PaintEventArgs *e) { Pen *myPen; int x = 200; int y = 10; int w = 100; int h = 90; // 200, 10 - 300, 100 Random *rdm1 = new Random((int)DateTime::Now.Ticks); Color c = Color::FromArgb(rdm1->Next(255), 0, 0, 255); myPen = new Pen(c, pensize); Graphics *myGraphics = e->Graphics; myGraphics->DrawLine(myPen, x, y, x+w, y+h); // 200, 110 - 300, 200 y = 110; System::Drawing::Drawing2D::HatchBrush * myHatchBrush = new System::Drawing::Drawing2D::HatchBrush( 51 Die Programmiersprache C++ System::Drawing::Drawing2D::HatchStyle::Cross, Color::FromArgb(255, 0, 255, 0), Color::FromArgb(255, 0, 0, 255)); myPen->Dispose(); myPen = new Pen(Color::FromArgb(255, 255, 0, 0), 1); myGraphics->FillRectangle(myHatchBrush, x, y, w, h); myGraphics->DrawRectangle(myPen, x, y, w, h); // 200, 210 - 300, 300 y = 210; System::Drawing::Drawing2D::LinearGradientBrush * myGradBrush = new System::Drawing::Drawing2D::LinearGradientBrush( Point( x, y), Point(x+w, y+h), Color::FromArgb(255, 0, 255, 0), Color::FromArgb(255, 0, 0, 255)); myGraphics->FillRectangle(myGradBrush, x, y, w, h); } }; // Dies ist der Einstiegspunkt für die Anwendung int _tmain(void) { // TODO: Ersetzen Sie den Beispielcode durch Ihren eigenen Code. // Instantiate a new instance of Form1. Form1 *f1 = new Form1; // Display a messagebox. This shows the application // is running, yet there is nothing shown to the user. // This is the point at which you customize your form. System::Windows::Forms::MessageBox::Show("The application " "is running now, but no forms have been shown."); // Customize the form. f1->Text = "Running Form"; // Show the instance of the form modally. f1->ShowDialog(); return 0; } 3. Schritt: Nach dem Erstellen von pr12283 und Start ohne Debuggen erscheint nach dem Konsolenfenster und einer Message Box das folgende Fenster: 52 Die Programmiersprache C++ Nach einem Klick auf den Button "test TextBo" bzw. mehreren Klicks auf "test gdi+" verändert sich das Fenster: 53 Die Programmiersprache C++ 1.3 Deklaration und Definition von Bezeichnern Allgemeine Form: typname bezeichner = anfangswert; (Die Angabe des Anfangswerts ist optional) bezeichner: Folge von Buchstaben und Ziffern, das 1. Zeichen ist Buchstabe, Großund Kleinbuchstaben werden unterschieden. Das spezielle Zeichen "_" wird als Buchstabe gewertet. Selbstdefinierte Namen (ProgrammiererWörter) dürfen nicht mit den vordefinierten Schlüsselwörtern übereinstimmen. Ein Name kann beliebig lang38 sein. Auch Funktionsnamen unterliegen der angegebenen Konvention 39. Definitionen reservieren Speicherplatz. Keine Definitionen sondern Deklarationen sind bspw.: extern int a; typedef int ganzZ; int f(int); extern const a; // Vereinbarung eines anderen Typnamens In C++ muß eine Variable, bevor sie benutzt wird, definiert sein. Variable besitzen einen Namen und erhalten einen Datentyp zugeordnet, z.B.: char zeichen; „char“ ist der Typ-Spezifizierer, „zeichen“ bestimmt ein Objekt, dem Speicherplatz zugeordnet ist. Zwei Werte sind mit symbolischen Variablen verknüpft: - 38 39 Datenwert (r-value) Speicheradresse (l-value) unter der der Datenwert gespeichert ist. Manchmal ist die Länge begrenzt, z.B. 31 Zeichen. Die Konventionen zeigen Regeln für die Struktur eines Bezeichners (Namens), auch Syntax genannt 54 Die Programmiersprache C++ 1.4 Speicherklassen, Gültigkeitssbereiche und Namensbereiche 1.4.1 Speicherklassen Jedes Objekt in C++ hat 2 Attribute: Eines bestimmt den Typ des Objekts und das andere legt die Speicherklasse fest. Die Speicherklasse bestimmt die (zeitliche) Lebensdauer und den (räumlichen) Geltungsbereich des Objekts. Hinsichtlich der räumlichen Gültigkeit oder Sichtbarkeit unterscheidet man lokale und globale Objekte. So können die geschweiften Klammern in /* 19 */ bzw. /* 27 */ Anfang und Ende eines Blocks bestimmen. Ein Block umfaßt mehrere sequentielle Anweisungen zu einem Verbund zusammen und definiert einen Geltungsbereich für die in ihm enthaltenen Variablen. Generell sind Variable lokal, d.h. auf den Block (oder den lokalen Geltungsbereich) beschränkt, in dem sie definiert wurden. In Ausnahmefällen kann der Geltungsbereich von Variablen durch außerhalb von Blöcken befindliche Definitionen erweitert werden40: /* 14 */ int n; /* 15 */ int x[20]; // globale Vereinbarung von Variablen C++ kennt die Speicherklassen auto(matic) und static. Sie bestimmen die Lebensdauer eines Objekts. Die Lebensdauer von Objekten der Speicherklasse auto ist die Aktivitätszeit des Blocks, in dem sie definiert sind. Eine „auto“-Variable ist der Standardfall für lokale Variable. „auto“-Variable haben Priorität vor globalen Variablen. Sie verstellen dem Block praktisch die Sicht auf übergeordnete Objekte des gleichen Namens, z.B.: #include <iostream.h> int x = 2; int main() { cout << "\n" << x; // Ausgabe x = 2 { int x = 3; cout << "\n" << x; // Ausgabe x = 3 x = 9; } cout << "\n" << x; } Die Speicherklasse static gibt es in 2 unterschiedlichen Formen, je nachdem, ob es sich um lokale oder globale Variable handelt: Setzt man das Schlüsselwort static vor eine Deklaration, ändert man bei globalen Objekten die räumliche Gültigkeit, bei lokalen Variablen verleiht man ihnen eine zeitliche Permanenz. Man kann überall, wo „auto“-Variablen stehen können, durch das Schlüsselwort static die Lebensdauer von Objekten auf die Dauer des Programms ausdehnen, die räumliche Geltungsbereich bleibt allerdings lokal. Nur der Block, in dem sie definiert 40 vgl. PR12101.CPP 55 Die Programmiersprache C++ wurden, hat einen Zugriff auf diese Variablen. Statische lokale Größen behalten ihre Werte in einem Block bei, z.B.: void f() { static int aufrufe = 1; cout << "Aufruf zum " << aufrufe << " .Mal\n"; aufrufe = aufrufe + 1; // sukzessives Inkrementieren bei jedem Aufruf } Globale Variable haben eine unbeschränkte Lebensdauer. Ihr Geltungsbereich erstreckt sich über alle Programmteile. Die räumliche globale Gültigkeit läßt sich durch das Schlüsselwort static einschränken. Sie sind außerhalb von der definierten Quelldatei „unsichtbar“. Ein Zugriff auf diese Variablen führt zu einer Fehlermeldung des Compilers. „Objekte mit dynamischer Lebensdauer“ werden während eines Programms mit besonderen Funktionsaufrufen angelegt und wieder freigegeben. Speicherzuweisung erfolgt entweder über Standardbibliotheksfunktionen wie malloc oder mit dem C++Operator new. Der zugewiesene Speicherplatz liegt in einen bestimmten Speicherbereich, dem sog. Heap. Mit free oder delete wird der Speicherplatz wieder freigegeben. Häufig wird der Datentyp „register“ zu den Speicherklassen gezählt. „register“ ist jedoch keine eigene Speicherklasse. Das Schlüsselwort bewirkt, soweit es möglich ist, die so typisierte Variable im Prozessorregister aufzubewahren. Auf die Variable kann dann schneller zugegriffen werden. Die Anweisung „register“ ist für lokale Variable und für „auto“-Variable zulässig. 1.4.2 Gültigkeitsbereiche bzw. Geltungsbereiche In der Programmiersprache C gibt es zwei Arten von Gültigkeitsbereichen: dateiweit (file scope) und blockweit (block scope). In C++ gibt es noch einen dritten Güligkeitsbereich: klassenweit (class scope)41. In den neuen Versionen von C++ kommt noch ein vierter Gültigkeitsbereich hinzu: namensraumweit (namespace scope), Objekte von C++ besitzen in der Regel vier Geltungsbereiche: lokal, funktional, global, funktionslokal und klassenlokal. Lokale Namen werden innerhalb eines Blocks vereinbart und sind von der Stelle ihrer Vereinbarung bis zum Ende des Blocks bekannt (sichbar). Bsp.: int i = 1; /* i ist ausserhalb des Blocks definiert, ist innerhalb eines jeden anderen Blocks gueltig und heisst deshalb globale Variable */ main() { 41 Die in einer Klasse definierten Namen haben Gültigkeit nur innerhalb einer Klasse. Insbesondere gibt es keinen Konflikt – und kein Overloading – zwischen freien Funktionen und Methoden. Der Scope Operator :: kann verwendet werden, um auf Bezeichner zuzugreifen, die nur innerhalb einer Klasse definiert sind. 56 Die Programmiersprache C++ int i = 2; // überlagert äußeres i { double i = 3.14; //überlagert inneres int i cout << i << "\n"; } // Ende des Gültigkeitsbereichs von double i cout << i << "\n"; } // Ende des Gültigkeitsbereichs des inneren i Globale Namen sind außerhalb jedes Blocks und außerhalb jeder Klasse deklariert 42. Ihre Sichbarkeit reicht von der Stelle der Deklaration bis zum Ende der Quelldatei (, falls nicht zwischendurch eine Überlagerung erfolgt). C++ verfügt über den "::"-Operator (scope resolution operator). Er ermöglicht den Zugriff auf die Datei eines globalen Namens, z.B.: int i; int main() { int i = 2; { // Beginn des inneren Gueltigkeitsbereichs double i = 3.14; cout << ::i << "\n"; // bezeichnet das äußere i } cout << i << "\n"; // Ende des inneren Gültigkeitsbereichs } Globale Namen können außerdem von getrennt übersetzten Programmteilen referenziert werden, falls sie a) nicht als static definiert sind b) in jeder "Konsumentendatei" ordnungsgemäß deklariert sind (kann durch Angabe des Schlüsselworts extern bei der Deklaration des globalen Namens in der Konsumentendatei erreicht werden). Jedes Objekt, das außerhalb jedes Blocks vereinbart wird, gehört zu Speicherklasse extern. Das Objekt ist damit global und zeitlich permanent. Es hat eine Lebensdauer, die sich über die gesamte Programmlaufzeit erstreckt und ist in allen zum Programm zugehörigen Programmbausteinen (Moduln) verfügbar. Für Variable ist die Verfügbarkeit erst ab dem Deklarationszeitpunkt gegeben. Funktionen gehören immer der Speicherklasse extern an, da eine Funktion nicht innerhalb eines Blocks definiert werden kann. Der Compiler betrachtet den Funktionstyp als extern und unterstellt den Typ int. Ein externes Objekt darf in allen Programmbausteinen nur einmal auftreten43. „extern“-Variablen werden in einem anderen Quelltext definiert. Sie werden durch eine „extern“-Deklaration zugänglich gemacht. Eine weitere Möglichkeit zur Schaffung eines Sichbarkeitsbereiches sind Namensräume (namespaces). Wird bspw. Der zur C++-Standardbibliothek gehörende Namensraum „std“ benutzt, dann kann das über using namespace std; angegeben werden. Namensräume spielen bei der Benutzung verschiedener Bibliotheken eine Rolle. 42 43 vgl. Zeilen /* 14 */ und /* 15 */ in PR12101.CPP Der Compiler stellt den Fehler nicht fest, wohl aber der Linker mit „duplicate global“ 57 Die Programmiersprache C++ 1.4.3 Externe Variable und Funktionen Funktionen können in vielen unterschiedlichen Programmen benutzt werden. Es wäre umständlich, jedesmal den Quelltext einer Funktion in ein neu erstelltes Programm hineinzukopieren. C++ benutzt dafür den Mechanismus der getrennten Übersetzung. Bsp.: Eine Funktion zum Potenzieren ist in einem separaten Block abzulegen und immer, wenn man 2 Zahlen potenzieren möchte, hinzuzubinden. Die erste Datei hat folgendes Aussehen44 double Potenz(double dX, short n) { // Potenz() potenziert dX und n. double dPot = 1.0; for(short i = 1; i <= n; i++) dPot = dPot * dX; return(dPot); // Rueckgabewert! } // Ende von Potenz() Die zweite Datei hat folgendes Aussehen45: #include <iostream.h> int main() { short exponent; double dBasis, dPot; // Extern Deklaration extern double Potenz(double, short = 2); void eingabe(double &, short &), ausgabe(double, short, double); eingabe(dBasis, exponent); // Aufruf der externen Funktion! dPot = Potenz(dBasis,exponent); ausgabe(dBasis, exponent, dPot); } // Ende von main() void eingabe(double &dB, short &e) { // Hier werden Basis und Exponent eingelesen. cout << "\n\tBasis: "; cin >> dB; do{ } cout << "\tExponent (> 0): "; cin >> e; } while(e < 0); // Ende von Eingabe() void ausgabe(double dB, short e, double dErg) { // Und hier findet die Ausgabe statt. cout << "\n\tErgebnis: " << dB << " hoch "; cout << e << " = " << dErg << '\n'; } // Ende von Ausgabe() 44 45 PR14203.CPP PR14205.CPP 58 Die Programmiersprache C++ Die Compilierung der beiden separaten Dateien erfolgt getrennt, die Zusammenstellung zu einem ablauffähigen Programm erfolgt über ein sog. Projekt in Borland C++ bzw. über ein „Makefile“ in GNU C/C++. Ergänzen von Namen: Falls ein C++-Baustein kompiliert ist, erzeugt der Compiler Funktionsnamen, die die Typen der Funktionsargumente in verschlüsselter Form enthalten (Namensergänzung, name mangling). Es gibt Fälle, in denen die Namensergänzung nicht gewünscht ist. Das wird dem Compiler auf folgende Weise mitgeteilt: extern "C" void exit(int); Man kann die Deklaration extern "C" auch auf einen Namensblock anwenden: extern "C" { void Cfunktion_1(int) void Cfunktion_2(int); void Cfunktion_3(int) } 1.4.4 Namensbereiche Namensbereiche (name spaces), auch Namensräume genannt, dienen dazu, die Wahrscheinlichkeit von Namenskollisionen (name clashes) zu verringern, indem man neben dem Gültigkeitsbereichen global, lokal und klassenweit noch zusätzlich den Gültigkeitsbereich Namensbereich einführt. Deklaration eines Namensbereichs Ein Namensbereich beispielspace wird so deklariert: namespace beispielspace { double f(double) } Im Namensbereich ist jetzt eine Funktion f() deklariert, deren Namen nicht mit einer globalen Funktion f() kollidiert, denn sie heißt vollständig beispielspace::f(). So muß sie bei ihrer Definition auch genannt werden: double beispielspace::f(double x) { return sqrt(x); } Using-Deklaration und -Direktive Aus Namensbereichen kann man Bezeichner durch Voranstellen des ScopeOperators verwenden, man importiert bspw. einzelne Namen aus dem Namensbereich mit using beispielspace::f oder man inportiert den ganzen Namensbereich mit using namespace beispielspace;. 59 Die Programmiersprache C++ Der erste Fall ist praktisch, wenn man eine Funktion innerhalb eines lokalen Gültigkeitsbereichs öfters aufrufen muß und sich die Schreibarbeit des qualifizierten Aufrufs mit Namen und Scope-Operator sparen möchte. Außerhalb von Funktionen ist die Verwendung unsicher, denn es könnte Namenskollisionen geben. Allerdings werden sie zum Zeitpunkt der Übersetzung festgestellt, sind also nicht problematisch. Die zweite Version, das Importieren eines kompletten Namensbereichs, ist prizipiell eine Übergangslösung, weil Namensbereiche noch nicht von allen Compilern unterstützt werden. Man braucht den Namen einer Funktion nicht zu qualifizieren, wenn sie innerhalb einer anderen Funktion aufgerufen wird, dessen Paramter aus demselben Namensbereich stammen, z.B.: namespace Zeit { class datum { /* ... */ }; string format(const Datum&); // .... }; } void f(Zeit::Datum d) { string s = format(d); // ... } // f Auch wenn im Kontext von f() keine Funktion format() vorhanden ist, so wird sie dennoch gefunden, denn im Namensbereich des Paramters gibt es eine passende Funktion. Aliasnamen für Namensbereiche Bei häufigem Verwenden eines Namensbereichs ist es praktischer, wenn der Name kurz ist. Andererseits ist das Risiko von Namenskollisionen bei sehr kurzen Namen hoch. Beim Entwurf eines Namensbereichs sind lange, beschreibende Namen zweckmäßig. Der Anwender kann leicht einen kurzen Namen erzeugen, z.B.: namespace kurz = langer_und_eindeutiger_Name; Jetzt kann man mit kurz::meineFunktion() leichter qualifizieren. Unbenannte Namenbereiche Oft möchte man Namenskollisionen verhindern, aber nicht ständig neue, garantiert eindeutige Namen erfinden. Namen von Namensbereichen können schließlich immer noch kollidieren. Unbenannte Namensbereiche können hier eine Hilfestellung sein. namespace { void f() { // .. } } 60 Die Programmiersprache C++ Die Funktion f() ist im Namensbereich versteckt. Bei einen unbenannten Namensbereich ist ein implizites using namespace vorhanden. Da Namen des Namensbereichs nur in der aktuellen Übersetzungseinheit (also in der vorliegenden Quelldatei) bekannt sind, sind Namenskollisionen mit anderen Quelldateien ausgeschlossen. 1.5 Ausdrücke Ein Ausdruck ist eine Folge von Operatoren, Operanden und Interpunktionszeichen, die eine Berechnung definiert. Die einfachste Form eines Ausdrucks ist eine Konstante oder Variable, z.B. 3.14159 "Fachhochschule Regensburg" obereGrenze Die Auswertung von Ausdrücken richtet sich nach bestimmten Konvertierungs- und Gruppierungsregeln, nach der Abarbeitungsreihenfolge und der Richtung der Operatoren, dem Vorhandensein von Klammern und den Datentypen der Operanden. Ausdrücke werden allgemein durch Konstante, Bezeichner, indizierte bzw. nicht indizierte Variable, Verweise auf Strukturen, Funktionsaufrufe gebildet, die sowohl mit unären als auch binären Operatoren verknüpft sind. Die Reihenfolge bei der Auswertung der Ausdrücke ist bestimmt durch Vorrang und Assoziativität der Operatoren. Die meisten C++-Operationen sind linksassoziativ. Man unterscheidet arithmetische, logische Operatoren, Zuweisungsoperatoren, Operatoren zur Speicherverwaltung und eine Reihe sonstiger Operatoren. Einige der Operatoren können sowohl unäre als auch binäre Operationen bestimmen. 1.5.1 Arithmetische Operatoren Neben den vier Grundrechnungsarten Addition (+), Subtraktion (-), Multiplikation (*) und Division (/) steht eine fünfte (sog. Modulo-Operation) zur Verfügung- Sie berechnet den Rest einer Division zweier ganzer Zahlen. Operator * / % + - Funktion multipliziere dividiere Modulo-Operator für ganze Zahlen addiere subtrahiere Anwendung ausdruck1 * ausdurck2 ausdruck1 / ausdruck2 ausdruck1 % ausdruck2 ausdruck1 + ausdruck2 ausdruck1 - ausdruck2 Nur das negative Vorzeichen "-" ist zugelassen. Es hat den Vorrang vor den Operatoren *, / und %, diese wiederum haben Vorrang vor +, -46. Läßt sich mit dieser 46 Punktrechnung geht vor Strichrechnung, die modulo-Operation zählt zur Punktrechnung) 61 Die Programmiersprache C++ Regel keine eindeutige Auswertungsreihenfolge festlegen, dann wird von links nach rechts ausgewertet. Nach der Zuweisung A = 10 * 2 * 3 + 5 * 2; enthält die Variable A den Wert 70. Überschaubarer wäre folgende Form A = (10 * 2 * 3) + (5 * 2); Klammern verändern die Auswertungsreihenfolge, z.B.: A = 10 * 2 * (3 + 5) * 2; führt für A zum Wert 320. 1.5.2 Relationale und logische Operatoren In C++ ist jeder Ausdruck wahr, der nicht Null ist. if (xa = 20) cout << "\nSinnlose Bedingung"; ist für C++ immer wahr, da sich am Wert der Zuweisung nichts ändern wird. In der Regel wird aber im Innern der Klammer eine echte Bedingung stehen, z.B. if (z == '+') Das doppelte Gleichheitszeichen hat ausgewertet den Wert 1, wenn die Bedingung erfüllt ist, andernfalls 0. Die Kombination von Bedingungen übernehmen logische Operatoren. Operator ! > >= < <= == != && || Funktion logisches NICHT größer als größer gleich kleiner kleiner gleich gleich ungleich logisches UND logisches ODER Anwendung !ausdruck ausdruck1 > ausdruck2 ausdruck1 >= ausdruck2 ausdruck1 < ausdruck2 ausdruck1 <= ausdruck2 ausdruck1 == ausdruck2 ausdruck1 != ausdruck2 ausdruck1 && ausdruck2 ausdruck1 || ausdruck2 Der logische NOT-Operator ! ergibt wahr, falls sein Operand den Wert 0 hat. Im anderen Fall bewertet er mit falsch. Die relationalen Operatoren ergeben den Wert 0, falls der Vergleich bzw. das Ergebnis falsch ist, und 1, falls es wahr ist. && ergibt den Wert 1, falls die beiden Operatoren von 0 verschieden sind, andernfalls 0. Der rechte Operand wird nur ausgewertet, wenn der linke Operand nicht 0 ist Der logische Operator || gibt den Wert 1 zurück, falls jeder der Operatoren verschieden von 0 ist, andernfalls 0. Der 2. Operand wird nicht ausgewertet, falls der 1. verschieden von 0 ist. Anschaulich lassen sich die Ergebnisse logischer Operatoren durch sog. Wahrheitstafeln darstellen: 62 Die Programmiersprache C++ && Bedingung 1 0 Bedingung 2 1 0 0 0 1 0 1 ! Bedingung 0 1 1 0 Bedingung 2 0 1 || 0 0 1 1 1 1 Bedingung 1 Abb. 1.5-1: Wahrheitstafeln zur Beschreibung logischer Operatoren Aufgabe: Überprüfe, welches Wort das folgende Programm 47 ausgibt. #include <iostream.h> int main() { int a = 2, b = 5, c = 7; if (!(a < b && c <= a + b || a - b < c)) cout << "\nROT"; else cout << "\nGRUEN"; } 1.5.3 Bitweise logische Operatoren Sie können nur auf ganzzahlige Werte verwendet werden. Operator ~ << >> & | ^ Funktion Bit-Komplement Shiften nach links Shiften nach rechts bitweises UND bitweises inklusives ODER bitweises exklusives ODER Anwendung ~ausdruck ausdruck1 << ausdruck2 ausdruck1 >> ausdruck2 ausdruck1 & ausdruck2 ausdruck1 | ausdruck2 ausdruck1 ^ ausdruck2 Shift-Operatoren verschieben Bit-Muster um eine angegebene Anzahl von Bits nach links bzw. nach rechts. In Abhängigkeit vom Vorzeichen fallen Nullen oder Einsen in die freien Positionen. Der &-Operator wird gewöhnlich eingesetzt, um bestimmte Bits auf 0 zu setzen, der |-Operator um bestimmte Bits auf 1 zu setzen. Die beiden Variablen short xa = 5; short xb = 11; werden durch folgende Anweisung verknüpft: cout << "xa & xb = " << xa & xb; 47 PR15201.CPP 63 Die Programmiersprache C++ Die Ausgabe ist nach Ausführung der Anweisung: xa & xb = 1 Das Ergebnis kann man mit Hilfe der internen Zahlendarstellung des Rechners nachvollziehen: xa = 0000000000000101 & xb = 0000000000001011 ----------------------0000000000000001 Die „ODER-Verknüpfung“ ergibt xa = 0000000000000101 | xb = 0000000000001011 ----------------------0000000000001111 Beim „exklusiven ODER“ erhält man eine 1, falls die Operanden verschieden sind: xa = 0000000000000101 ^ xb = 0000000000001011 ----------------------0000000000001110 Der „Bit-Komplement-Operator“ kippt alle Bits seines Arguments. So liefert cout << "~xa = " << ~xa; die Ausgabe ~xa = -6 xa = 0000000000000101 ~xa = 1111111111111010 Die Darstellung ist das sog. Zweierkomplement48. Das vorderste Bit bestimmt das Vorzeichen. Eine 1 bedeutet negative Zahl, eine 0 positive Zahl. Die Zahl, die nur Einsen enthält, steht im Zweierkomplement für den Wert -1. Addiert man hierzu eine 1, erhält man den Wert 0. Zieht man eine 1 ab, dann wird die letzte Stelle zu 0, alle übrigen bleiben 1. So kommt man schließlich auf „-6“. Beim „Schieben nach Links (<<)“ werden an den freiwerdenden Stellen grundsätzlich Nullen nachgeschoben. Beim Shift nach rechts werden bei vorzeichenlosen Zahlen immer Nullen („logical shift“, bei vorzeichenbehafteten Zahlen das Vorzeichenbit 49 nachgeschoben („arithm. shift“), z.B. xa << 2; xa >> 2; // = 0000000000010100 // = 0000000000000001 Ein Shift um eine Position nach links entspricht einer Multiplikation mit 2, und ein Shift nach rechts verhält sich wie eine ganzzahlige Division durch 2. Generell kann man einen Shift um n Positionen nach links bzw. nach rechts einer Multiplikation bzw. einer ganzzahligen Division mit 2 n gleichsetzen Bsp.: Bestimmen der int-Länge mit Bitoperatoren #include <iostream.h> sog. „unechtes Komplement“; das Zweierkomplement wird gebildet, indem alle Bits invertiert werden und dann auf das Ergebnis 1 addiert wird 49 Das Bit an der am weitesten links stehenden Position 48 64 Die Programmiersprache C++ int main() { unsigned n, w = ~0; for (n = 0; (w & 01) != 0; w = w >> 1, n++) // for (n = 0; w & 01; w >>= 1, n++) ; cout << "\nDie Wortlaenge von int betraegt " << n << " Bits"; } 1.5.4 Zuweisungsoperatoren Eine Zuweisung hat die Form: L-Wert = R-Wert „L-Wert (lvalue)“ ist das Ziel der Zuweisung. Er ist ein Ausdruck, der ein Objekt bezeichnet, das in der Lage ist seinen Wert zu ändern. „R-Wert“ wird an der Stelle abgespeichert, die durch „L-Wert“ bezeichnet ist. Mit dem Zuweisungsoperator = wird einer Variablen ein Wert zugewiesen (bzw. eine Variable erhält durch den Zuweisungsoperator einen Wert), z.B.: x = 20; ..... x = x + 1; Nach der rechten Seite des Gleichheitszeichens steht die Variable x als R-Wert. Sie wird geladen. Nach dem Laden wird sie um 1 vermindert. Der neue Wert wird an die Stelle geschrieben, die die Variable auf der linken Seite angibt (L-Wert). Anstelle von x = x - 1; kann man auch schreiben50 x -= 1; Die abkürzende Schreibweise ist auch für andere Operatoren zugelassen, z.B.: fx *= 100; laenge += 1; derg /= 100.0; // fx = fx * 100 // laenge = laenge + 1; // derg = derg / 100.0; Operatoren dieser Form sind so definiert, daß der Ausdruck x op = y mit dem Ausdruck x = x op y gleichbedeutend ist. So ist bspw. „a *= b + c“ gleichwertig zu „a = a * (b + c) “, da die Addition eine höhere Priorität besitzt als der Operator „*= “. Das Operationszeichen (+, -, *, /, usw.) steht immer vor dem Operator der Zuweisung. Die Anweisung x -= 1; ist grundsätzlich verschieden zu 50 Hinweis: Diese Fassung läuft um Bruchteile von Sekunden schneller ab 65 Die Programmiersprache C++ x =-1; // ist dasselbe x = -1; 1.5.5 Inkrement- und Dekrementoperatoren Die Operatoren -- und ++ sind eine weitere Spezialität von C++. So ist n = (x++) * (y++) gleich n = x * y und n = (++x) * (++y) gleich n = (x + 1) * (y + 1) Stehen die Operatoren -- bzw. ++ vor der Variablen, dann spricht man von Prädekrement bzw. Präinkrement. Stehen sie dahinter, dann spricht man von Postdekrement bzw. Postinkrement. Interessant werden diese Operatoren insbesondere dann, wenn sie mit anderen Ausdrücken kombiniert werden, z.B.: b = a-- entspricht b = a; a = a - 1; b = --a; entspricht a = a - 1; b = a; Durch die abkürzende Schreibweise können aber auch Probleme auftreten, z.B.: a b c c = 10; = 20; = 30; += --b + a++ - 5; // c = 30 + 19 + 10 - 5 = 54 1.5.6 Bedingungsoperator Er besitzt die Form: ausdruck ? ausdruck1:ausdruck2 Falls „ausdruck“ einen Wert verschieden von 0 besitzt, ist das Ergebnis der Wert von „ausdruck1“ andernfalls von „ausdruck2“. Bsp.: #include <iostream.h> int main() { int i = 10, j = 20, k = 30; cout << "Der groessere Wert von " << << (i > j ? i : j) << endl; 66 i << " und " << j << " ist " Die Programmiersprache C++ cout << "Der Wert von " << i << " ist " << (i % 2 ? " " : " nicht ") << "ungerade" << endl; // Auch verschachtelte "arithmetische if" sind moeglich, z.B. wenn // die Variable "groesster" auf den groessten Wert der drei Variablen // gesetzt werden soll int groesster = ( ( i > j) ? ((i > k) ? i : k) : (j > k) ? j : k); cout << "Der groesste Wert von " << i << " , " << j << " und " << k << " ist " << groesster << endl; } 1.5.7 Kommaoperator Mehrere Ausdrücke lassen sich über den Kommaoperator sequentiell auswerten. Der Ausdruck „ausdruck1, ausdruck2, ... , ausdruckn “ wird von links nach rechts ausgewertet. Der Wert des gesamten Ausdrucks entspricht dem Wert von „ausdruckn“. Bsp.: #include <iostream.h> main() { int a = 0, b = 0, c; c = (a++, b++, b++, b++); cout << "a = " << a << "b = " << b << "c = " << c << endl; } Der Kommaoperator wird gelegentlich in den Bestandteilen einer for-Schleife benutzt, z.B. „Berechnung der Summe der Zahlen 1 bis 100“ for (int i = 1, sum = 0; i <= 100; sum += i, ++i) 67 Die Programmiersprache C++ 1.5.8 Vorrang und Assozitivität von Operatoren Die folgende Zusammenstellung beginnt mit Operatoren hoher Priorität und endet bei Operatoren mit niedrigerer Priorität. R L L L L L R R R R R R R R L L L L L L L L L L L L L L Operator :: :: -> [] () () sizeof ++, -~ ! +, *, & () new, delete ->*, .* *, /. % +, <<, >> <, <=, >, >= ==, != & ^ | && || ?: =, *=, /=, %=, += -=, <<=, >>=, |=, ^= , Bedeutung global scope class scope member->auswahl Vektoren Funktionsaufruf Wertkonstruktion Objekt-, Typgröße De-, Inkrementierung Komplement Negation unäres Minus, Plus Dereferenz, Referenz Typumwandlung Kreieren, Löschen Komponentenzeigerdereferenzierung multiplikative Operation Arthm. Operationen Links-, Rechtsshift relationale Operatoren gleich, ungleich bitweises UND bitweises exkl. ODER bitweises inkl. ODER logisches UND logisches ODER arithmetisches if Zuweisungsoperator Beispiel ::name ::klassenName :: member pointer->member pointer[ausdruck] ausdruck(ausdruckliste) type(ausdruckliste) sizeof(type) lvalue++; ++lvalue ~ausdruck !ausdruck -ausdruck; +ausdruck *ausdruck; &lvalue (type) ausdruck new type; delete p ausdruck1 * ausdruck2 ausdruck1 + ausdruck2 lvalue << ausdruck ausdruck1 < ausdruck2 ausdruck1 == ausdruck2 ausdruck1 & ausdruck2 ausdruck1 ^ ausdruck2 ausdruck1 | ausdruck2 ausdruck1 && ausdruck2 ausdruck1 || ausdruck2 Kommaoperator Vielen Operatoren für benutzerdefinierte Datentypen kann durch Überladen eine beliebige Bedeutung51 zugeordnet werden. Die syntaktischen Eigenschaften der Operatoren (Assoziativität, Präzedenz) können allerdings nicht verändert werden. 51 z.B. die spezielle Bedeutung der Operatoren << und >> im Zusammenhang mit Ein- und Ausgabe 68 Die Programmiersprache C++ 1.6 Anweisungen 1.6.1 Einfache Anweisung Anweisungen steuern die Ablaufkontrolle während der Ausführung eines Programms. Enthält das Programm keine Sprung- oder Auswahlanweisungen, dann werden Anweisungen nacheinander (sequentiell) ausgeführt. Die einfache Anweisung oder Ausdrucksanweisung52 wird durch ein Semikolon ; abgeschlossen. Durch Auswerten des Ausdrucks wird die Anweisung ausgeführt. Die häufigste Form der einfachen Anweisung ist die Zuweisung (Opertor: =). Die leere Anweisung besteht nur aus einem abschließenden Semikolon und hat keine Wirkung. Eine Anweisung kann auch aus mehreren anderen Anweisungen zusammengesetzt sein, der dazu gebildete Block wird durch geschweifte Klammern ({}) eingeschlossen. 1.6.2 Kontrollanweisungen (-strukturen) Alle Kontrollstrukturen testen logische Werte auf 0 bzw. ungleich 0. C++ interpretiert 0 als den logischen Wert falsch, einen von 0 verschiedenen Wert als wahr. 1. Die "for"-Anweisung for (ausdruck1; ausdruck2; ausdruck3) anweisung; ausdruck1 wird einmal bewertet und bestimmt die Anfangsanweisung (Initialisierung des Schleifenzählers). Demnach wird, solange ausdruck2 ungleich Null ist, der Anweisungsteil gefolgt von ausdruck3 (Manipultion des Schleifenzählers) ausgeführt. Bsp.: void bsort(int* x, int n) { // Sortieren durch Austauschen for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) if (x[i] > x[j]) tauschen(x,i,j); } Falls der 1. und 2. Ausdruck fehlen, so bedeutet dies, daß an entsprechender Stelle nichts ausgeführt werden soll. Ein fehlender 2. Ausdruck wird jedoch stets als wahr interpretiert (Endlos-Schleife), z.B.: #include <iostream.h> main() { 52 Jedem Ausdruck, dem ein Semikolon folgt, ist eine Ausdrucksanweisung 69 Die Programmiersprache C++ // Vor Ablauf des Programs Unterbrechungsmoeglichkeiten feststellen for (int i = 1;/* kein 2. Ausdruck */;i++) cout << i << endl; } Jeder Ausdruck der "for"-Schleife ist optional und kann wegfallen. Jeder Ausdruck hat aber eine bestimmte Funktion, so daß mindestens das Semikolon bleiben muß: for(;;) ; // leere Anweisung tut eigentlich nichts, das aber unendlich oft. Beim Verändern der Schleifenvariablen im ausdruck3 einer for()-Schleife spielt es keine Rolle, ob der Operator ++ bzw. -- vor oder hinter der Schleifenvariablen steht. Die Anweisung wird immer nach dem Durchlaufen des Schleifenrumpfs ausgeführt. ausdruck 1 ausdruck 2 Rumpf ausdruck 3 Abbruchbedingung Abb. 1.61: Schematischer Programmablauf in einer for()-Schleife Man kann auch mehr als 3 Anweisungen im Schleifenkopf realisieren. So ist bspw. beim Sortieren u.U. nötig, mehr als 2 Laufvariablen zu kontrollieren. Eine zählt von 1 aufwärts, die andere von einer Obergrenze abwärts. Treffen sich beide Variablen, dann soll die Schleife abbrechen: for (int i = 0, int j = Obergrenze; i <=j; i++, j--) ausdruck1 und ausdruck3 sind hier zusammengesetzt bzw. durch ein Komma getrennt. Sie werden syntaktisch wie eine Anweisung behandelt. Bsp.53: #include <iostream.h> void main() { int i, j; for (i = 0, j = 9; ((i <= 9) && (0 <= j)); i++, j--) cout << "\ni = " << i << " j = " << j; } 53 PR16202.CPP 70 Die Programmiersprache C++ 2. Die "while"-Anweisung54 while ( ausdruck ) anweisung; Der Anweisungsteil wird ausgeführt solange ausdruck ungleich Null ist bzw. bis ausdruck den Wert Null (falsch) ergibt.. Bsp.: „Sortieren durch Einfügen“ void einfuegesortieren(int *x, int n) { int schl, j; for (int i = 1; i < n; i++) { schl = x[i]; j = i - 1; while ((schl < x[j]) && (j >= 0)) { x[j + 1] = x[j]; j--; } x[j + 1] = schl; } } 3. Die "do"-Anweisung55 do anweisung while ( ausdruck ); Sie wird immer dann angewendet, falls die Schleife mindestens einmal durchlaufen werden soll. Eine do-Anweisung wird solange ausgeführt. bis ausdruck den Wert Null (falsch) annimmt. Bsp.: 1) Fibonacci-Zahlen56 Alle Glieder einer (Fibonacci-) Folge, deren Werte eine eingegebene Größe („maxwert“) nicht überschreitet, sollen berechnet werden. In der Fibonacci-Folge 1, 1, 2, 3, 5, 8, 13, ... kann ab dem 3. Glied jedes weitere Glied aus der Summe der beiden vorhergehenden Glieder berechnet werden: #include <iostream.h> void main() { int maxwert, c; int a = 1, b = 1; cout << "\nMaxwert? "; cin >> maxwert; cout << '*' << endl; do 54 kopfgesteuerte Schleife fußgesteuerte Schleife 56 PR16206.CPP 55 71 Die Programmiersprache C++ { for (int i = 1; i <= b; i++) cout << '*'; cout << endl; c = a + b; a = b; b = c; } while (c < maxwert); } 2) Sortieren durch Zerlegen „Quicksort" Der Algorithmus besteht aus folgenden Arbeitsschritten: (1) Auswahl eines Elements „test“ aus der zu sortierenden Liste (2) Zerlegen des zu sortierenden Bereichs in 2 Teilbereiche - Teilbereich 1 enthält alle Elemente, die kleiner sind als „test“ - Teilbereich 2 enthält alle Elemente, die größer sind als „test“ (3) Sortien der Teibereiche (rekursiv) void quicksort(int *x, int links, int rechts) { int i = links; int j = rechts; // Auswahl eines Elements aus dem zu sortierenden Teilbereich int test = x[(links + rechts) / 2]; do { // Zerlegen in 2 Teilbereiche while (x[i] < test) i++; while (test < x[j]) j--; if (i <= j) { tauschen(x,i,j); i++; j--; } } while (i <= j); if (links < j) quicksort(x,links,j); if (i < rechts) quicksort(x,i,rechts); } 4. continue und break continue bewirkt, daß der Rumpf einer Schleife nicht bis zum Ende ausgeführt wird, sondern sofort ein neuer Schleifendurchgang startet. break Verlassen der Schleife, d.h. Sprung hinter das Schleifenende. Alle 3 Schleifenarten lassen sich mit Hilfe von break vorzeitig verlassen. Tritt der Befehl im Schleifenrumpf auf, wird an das Ende gesprungen und die Schleife nicht wiederholt. Bsp.: #include <iostream.h> int main() { int zaehler; cout << "Start einer Schleife mit continue " << endl; 72 Die Programmiersprache C++ for (zaehler = 1; zaehler <= 10; zaehler++) { if (zaehler > 5) continue; cout << zaehler << endl; } cout << "Nach der for-Schleife, zaehler = " << zaehler << endl; cout << "Start einer for-Schleife mit break " << endl; for (zaehler = 1; zaehler <= 10; zaehler++) { if (zaehler > 5) break; cout << zaehler << endl; } cout << "Nach der for-Schleife, zaehler = " << zaehler << endl; } 5. "if"-Anweisung if (x[i] > x[j]) tauschen(x,i,j); ist eine bedingte Anweisung. Der Aufruf der Funktionsprozedur „tauschen“ erfolgt nur dann, falls die vorstehende Bedingung erfüllt ist. if (ausdruck) anweisung1; else anweisung2; Der "else" Anteil ist optional. Fallunterscheidungen hängen generell davon ab, ob die arithmetische Auswertung des ausdruck von 0 verschieden ist. „ausdruck“ muß einen "integer"-Wert liefern. Jeder Wert ungleich Null bewirkt die Durchführung von "anweisung1", das Ergebnis Null führt zur Aktivierung von "anweisung2". Bsp.: "Ausgabe der Zeichenfolge 12345789 " #include <iostream.h> int main() { int i; for (i = 1; i <= 10; i++) { if (i == 6) continue; else cout << i; } } Mehrere Anweisungen nach einer Bedingung müssen zusammengefaßt werden. So gibt bspw. die Bedingung if (x == 100) { cout << "Die Variable x "; cout << "enthaelt den Wert 100 \n"; } nur aus, falls x tatsächlich den Wert 100 hat. Wird angegeben if (x == 100) cout << "Die Variable x "; cout << "enthaelt den Wert 100\n"; 73 zu einem Block Die Programmiersprache C++ führt das zur Ausgabe (auch wenn x nicht den Wert 100 hat): enthaelt den Wert 100 Hinter der schließenden, runden Klammer einer if-Konstruktion steht in der Regel kein Semikolon. Allerdings wird if (i == 0); cout << "Untergrenze erreicht\n"; keine Warnung oder Fehlermeldung des Compiler hervorrufen. Das einzelne Semikolon wird als leere Anweisung betrachtet. Es wird hier deshalb Untergrenze erreicht ausgegeben. Bsp.: #include <iostream.h> main() { int i; for (i = 1; i <= 10; i++) { if (i == 6) ; else cout << i; } } Im „else-Teil“ sind weitere bedingte Anweisungen möglich. 6. Die "switch"-Anweisung switch ( ausdruck ) { case konstante1: /* Folge von Anweisungen */ break; case konstante2: ..... case konstanten: /* Folge von Anweisungen */ break; default: /* Folge von Anweisungen */ } /* Weitere Anweisungen */ Diese Anweisung bewirkt in Abhängigkeit vom Wert eines Ausdrucks eine mehrfach verzweigte Auswahl. Der Ausdruck wird bewertet und auf Gleichheit mit einer Anzahl von Konstanten verglichen. Die "default"-Marke ist optional und wird angesprungen, wenn keine der Konstanten zutrifft. Nach "case" steht eine Folge keiner oder mehrerer Anweisungen. Die „break“-Anweisung am Ende von „case“ ist optional. Sie sorgt dafür, daß der Programmablauf zum Ende des „switch“-Statement verzweigt. Die „switch“-Anweisung bewertet den Ausdruck und prüft auf eine Übereinstimmung mit einer Konstanten. Wird die Übereinstimmung gefunden, wird die Kontrollstruktur switch ab dieser Konstanten (Marke) bis zur schließenden geschweiften Klammer oder bis zum nächsten „break“ abgearbeitet, z.B. #include <iostream.h> 74 Die Programmiersprache C++ main() { char z; cin >> z; while (z != '#') { switch(z) { case '+': case '-': case '*': case '/': case '%': cout << "arihmetische Operator " << endl; break; case '&': case '^': case '!': cout << "Bit-Verknuepfung" << endl; break; default: cout << "Sonstiges" << endl; break; } cin >> z; } cout << "Schleife abgebrochen" << endl; } 7. „goto“-Anweisung „goto marke“ Die „goto“-Anweisung verzweigt die Programmsteuerung zu der Anweisung mit dem Sprungziel marke. marke: anweisung marke ist ein Bezeichner. Der Gültigkeitsbereich erstreckt sich auf die Funktion, d.h. das Sprungziel muß sich innerhalb derselben Funktion befinden. 75 Die Programmiersprache C++ goto Anweisung; goto marke: Anweisung; marke: Anweisung; if (Ausdruck) wahr Anweisung Anweisung 1 if - else falsch 2 switch switch (Ausdruck) case 1 case 2 default Folge Folge Folge Folge Ausdruck solange Ausdruck 2 Folge for 1 wahr ist Anweisung; Ausdruck 3 while solange Ausdruck wahr ist Anweisung; do - while Anweisung; solange Ausdruck wahr ist Abb. 1.6-2: Kontrollstrukturen in C++ als Nassi-Shneidermann-Diagramme 76 Die Programmiersprache C++ 1.7 Funktionen 1.7.1 Definition und Deklaration von Funktionen Eine Aufgabe wird in C++ normalerweise über den Aufruf einer Funktion gelöst, die die Aufgabe bearbeitet. In der Definition ist spezifiziert, wie die Funktion arbeitet. Eine Funktion kann erst aufgerufen werden, nachdem sie deklariert ist. Deklaration von Funtionen Sie enthält den Namen der Funktion, den Typ des Funktionswerts, Anzahl der Typen der Argumente, die der Funktion übergeben werden: ergebnistypname funktionsname(parameterliste); Defintion von Funktionen ergebnistypname funktionsname (typname1 parametername1, typname2 parametername2, ..... , typnameN parameternameN) { vereinbarungen anweisungen } ergebnistypname ist der Resultattyp der Funktion. Ohne Typangabe wird „integer" angenommen. funktionsname ist der Name der Funktion parameternamen sind eine Liste von Argumenten (formale Parameter), die durch Kommas getrennt sind. Die Parameter 1..N sind innerhalb des Funktionsrumpfs gültig. Die Liste der Parameter kann auch leer sein, auch die Anweisungsfolge im Funktionsrumpf darf ausgelassen werden, z.B.: dummy(){}. vereinbarungen sind Definitionen von lokalen Variablen und Deklarationen von Objekten, deren Gültigkeitsbereich auf die Funktion beschränkt sein soll. anweisungen sind Aktionen, die ausgeführt werden, falls die Funktion aufgerufen wird. Eine Funktions-Definition ist eine Funktionsdeklaration, die (zusätzlich) den Prozedurrumpf der Funktion enthält. Bsp.: void tauschen(int* x, int i, int j) { int zwischen = x[i]; x[i] = x[j]; x[j] = zwischen; } 77 Die Programmiersprache C++ 1.7.2 Parameterübergabe (Übergabe der Argumente) Beim Aufruf einer Funktion wird für jeden formalen Parameter Speicherplatz reserviert und mit den Werten der aktuellen Parameter belegt. Der Typ des formalen Parameters wird mit dem Typ des aktuellen Arguments abgeglichen, und es werden alle standardmäßigen und benutzerdefinierten Typumwandlungen durchgeführt. funktionsname(parameterliste_der_aktuellen_Parameter); Bsp.: #include <iostream.h> void f(int wert, int& ref) { wert++; ref++; } main() { int i = 1; int j = 1; f(i,j); // Aufruf der Funktion mit den aktuellen Parametern cout << "i = " << i << " j = " << j << endl; } Das erste Argument von f() wird "by value" übergeben, das 2. Argument "by reference" . 1. Call by value Die Werte aktueller Parameter ändern sich bei Abarbeitung der Funktion nicht. Im folgenden Programm wechselt die Funktion tausch() die lokalen Kopien ihrer Argumente. Bsp.57: #include <iostream.h> void austausch(int v1, int v2) { int hilf = v2; v2 = v1; v1 = hilf; } main() { int i = 10; int j = 20; cout << "vor dem austausch() \t i: " << i << "\t j: " << j << endl; austausch(i,j); cout << "nach dem austausch() \t i: " << i << "\t j: " << j << endl; return 0; } 57 PR17201.CPP 78 Die Programmiersprache C++ Die aktuellen Variablen, die "tausch()" zum Auswechseln übergeben bekommt, bleiben davon unberührt. Diese Problem des call by value kann man auf zwei alternativen Lösungswegen umgehen: 1. "tausch()" kann über die Deklaration der formalen Parameter zu Zeigern umgeschrieben werden: void zaustausch(int* v1, int* v2) { int hilf = *v2; *v2 = *v1; *v1 = hilf; } In main() erfogt der Aufruf von tausch() dann so: zaustausch(&i,&j); 2. Die formalen Argumente werden vom Typ "reference" vereinbart void raustausch(int &v1, int &v2) { int hilf = v2; v2 = v1; v1 = hilf; } In main() erfogt der Aufruf dann so: raustausch(i,j); Falls Zeiger getauscht werden sollen, wäre folgende Behandlung von tausch() möglich: #include <iostream.h> void zraustausch(int *&v1, int *&v2) { int *hilf = v2; v2 = v1; v1 = hilf; } main() { int i = 10; int j = 20; int *zi = &i; int *zj = &j; cout << "vor dem austausch() \t i: " << *zi << "\t j: " << *zj << endl; zraustausch(zi,zj); cout << "nach dem austausch() \t i: " << *zi << "\t j: " << *zj << endl; return 0; } 2. Call by reference Bei der Übergabe einer Referenz auf ein Objekt, kann die Funktion die Werte der aktuellen Parameter ändern. Außerdem kann ein formaler Parameter der Funktion als Referenz definiert werden. Der Name des formalen Paramters ist dann ein anderer Name für das Objekt, das durch den aktuellen Parameter bestimmt wird. Die Übergabe "by reference" kann speziell für große Objekte sehr effizient sein. Falls Argumente dann nicht geändert werden sollen, sollte das Argument als const deklariert sein. Damit wird angezeigt: Die Übergabe "by reference" geschieht aus Effizienzgründen, die Funktion kann den Wert des Arguments nicht verändern. 79 Die Programmiersprache C++ 3. Vektoren als Parameter Wird ein Vektor (C-Array) als Parameter einer Funktion verwendet, so wird nur die Adresse auf den Anfang des Vektors übergeben. Ein Argument vom Typ T[] wird in den Typ T* konvertiert, falls er Parameter eines Funktionsaufrufs ist. Die Funktion arbeitet auf den aktuellen Parameter und kann ihn verändern. Vektoren (C-Arrays) werden in C++ immer mit "call by reference" übergeben. 4. Default-Argumente (Standardwerte für Funktionsargumente) Sie dürfen am Ende der Parameterliste auftreten. Default-Argumente werden bei der Deklaration der Funktion typüberprüft und beim Aufruf der Funktion ausgewertet. Bsp.: 1) 58 #include <iostream.h> void striche(int, int, char z = '-'); int main() { striche(0,40); striche(0,40,'$'); striche(0,40,'@'); striche(0,40,'+'); char zeichen; cin >> zeichen; return 0; } void striche(int spalte, int anz, char z) { for (int i = spalte; i < anz; i++) cout << z; cout << endl; } 2) 59 #include <iostream.h> int addiere(int i, int j, int m = 0, int n = 0) { return (i + j + m + n); } void main() { int a = 1, b = 5, c = 10, d = 20; cout << addiere(a,b) << '\n'; cout << addiere(a,b,c) << '\n'; cout << addiere(a,b,c,d) << '\n'; } 58 59 PR17202.CPP PR17204.CPP 80 Die Programmiersprache C++ Für Parameter dürfen Standardwerte (default values) angegeben werden. Sie werden vom Übersetzer eingesetzt, falls das entsprechende Argument im Aufruf fehlt. 5. Beliebig viele Argumente (variable Parameterlisten) Für Funktionen, bei denen Anzahl und Typ der Argumente nicht exakt feststeht, endet die Argumentenliste mit einer Ellipse (...). Damit wird ausgedrückt: Es können weitere Argumente folgen. Ein gut entworfenes Programm wird höchstens einige wenige solcher Funktionen benötigen. Überladene Funktionen und Funktionen mit Default-Argumenten ermöglichen in den meisten Fällen ordnungsgemäße Typüberprüfungen. Nur wenn Anzahl und Typ der Argumente unbekannt sind, ist die Ellipse notwendig (wichtigster Anwendungsbereich: Schnittstellen-Spezifikation zu C-Bibliotheken die zu einer Zeit entworfen wurden, als andere Alternativen nicht verfügbar waren). Bsp.: Die C-Ausgabe-Funktion int printf(const char* format,...) erzeugt für eine beliebige Anzahl von Argumenten formatierte Ausgaben, deren Gestalt vom Format-String "format" kontrolliert wird. Ein vollständiger Aufruf von printf() ist nach dem folgenden Schema aufgebaut: printf(format, wert1, wert2, ....) Der Format-String besteht aus zwei Typen von Elementen: - einfache Textzeichen (die in den Ausgabestrom kopiert werden) Umwandlungs-Angaben (eingeleitet durch das Zeichen %) Die Textzeichen werden unmittelbar ausgegeben, für Umwandlungsangaben (Formatelemente) wird der Text ausgegeben, der durch die Umwandlung eines Wertarguments entsteht, z.B.: #include <stdio.h> main() { char* vorname = "juergen"; char* nachname = "sauer"; printf("Hallo Informatiker\n"); printf("Mein Name ist %s %s\n", vorname, nachname); // %s erwartet char* printf("%d + %d = %d\n",2,3,5); // %d erwartet int-Argument } Das 1. Argument von printf() ist ein Format-String mit speziellen Zeichenfolgen, die es printf() erlauben, die übrigen Elemente richtig zu verarbeiten (z.B. %s, %d). Der Compiler weiß das nicht und kann somit u.U. nicht sicherstellen, daß die erwarteten Argumente tatsächlich vorhanden sind. Das „%“-Zeichenmit dem folgenden Konvertierungszeichen (code) dient zur Formatierung, so daß printf() Zahlenvariable auch als Text ausgegeben kann. Einige der am häufigsten verwendeten Formatelemente sind: d: dezimal x: hexadezimal o: oktal 81 Die Programmiersprache C++ u: c: s: e: f: g: dezimal (ohne Vorzeichen) einzelnes Zeichen Zeichenkette dezimale Gleitkommadarstellung (scientific) dezimale Festkommadarstellung (fixed) kürzeste Version von e und f Mit den folgenden Zusätzen kann über printf() eine Ausrichtung bzw. eine Anpassung erreicht werden: -: linsbündige Ausrichtung +: rechtsbündige Ausrichtung l: langer Ausdruck Formatelemente bestehen aus dem %-Zeichen, einer optionalen Angaben zur minimale Feldbreite mit evtl. (durch Punkt getrennt) einer Angabe zur Anzahl der Stellen nach dem Dezimalpunkt und dem „Code“. Der Code definiert den Typ des betroffenen Werts und die Art der Umwandlung. Bsp.: Aufbau einer ASCII-Tabelle60 // #include <iostream.h> #include <stdio.h> void main() { /* int x = 32; // 0 .. 31 sind Steuercodes und nicht darstellbar while (x <= 256) { printf("\nZeichen: %c Dezimal: %d Hexadezimal: %x", x, x, x); x = x + 1; } /* fuehrt zur gleichen Ausgabe: for (int x = 32; x <= 256; x++) cout << "\nZeichen: " << (char) x << " Dezimal: " << dec << x << " Hexadezimal " << hex << x; */ } Kann eine Funktion ermitteln, wieviel aktuelle Parameter von welchem Datentyp bei einem bestimmten Aufruf tatsächlich angegeben werden, dann sind variable Parameterlisten möglich. Zu Beginn einer derartigen Parameterliste steht zweckmäßigerweise dann die Ausgabe des aktuellen Parameters (z.B. anzArg). Dann gilt bspw. für das bestimmen des kleinsten Parameterwerts für eine Reihe ganzzahliger Paramertypen: int min(int anzArg, int a, int b, ...) Die 3 Punkte ... am Ende der Liste der formalen Parameter bezeichnen das „Auslassungszeichen (Ellipse)“ und geben an, daß mehr aktuelle als deklarierte formale Parameter übergeben werden dürfen 61. Der Zugriff auf die durch ... vereinbarten formalen Parameter kann über sie durch stdarg.h vereinbarten Makros va_list, va_start, va_arg, va_end erreicht werden, z.B.62: 60 PR17205.CPP An Stelle der 3 Punkte sind beliebige Parameter erlaubt, ohne Rücksicht darauf, welche Art Argument die Funktion tatsächlich erwartet. Durch das Auslassungszeichen wird der Typkontrollmechanismus von C++ außer Kraft gesetzt. 62 PR17203.CPP 61 82 Die Programmiersprache C++ #include <iostream.h> #include <stdarg.h> int min(int anzArg, int a, int b, ...) { // Hilfsvariable für variable Parameter va_list argumente; va_start(argumente,b); // "b" letzter Parameter der Funktion, mit dessen // Hilfe der Makro die Anfangsadresse der variablen Para// meterliste ermittelt int m = a; if (b < m) m = b; for (int i = 3; i <= anzArg; i++) { int par = va_arg(argumente, int); // sukzessive Anwendung des Makro va_arg // Der Datentyp der Argumente ist int if (par < m) m = par; } va_end(argumente); // Schliessen der Bearbeitung "Parameterliste" return m; } int main() { int a = 20, b = 30, c = 40, d = 50; cout << min(4,a,b,c,d) << endl; } 6. Prototypen In C++ muß jede Funktion einen Funktionsprototyp63 haben. Ein Prototyp gibt an, welchen Rückgabewert die Funktion hat, wie sie heißt und welche Parametertypen sie bekommt. Abgeschlossen wird diese Angabe durch ein Semikolon. Der Funktionsprototyp muß immer vor der ersten Stelle auftreten, an der die Funktion genutzt wird. Sinnvollerweise sammelt man sie zu Beginn des Programms. Grundsätzlich ist es gleich, ob eine Funktion vor oder hinter main() steht. Die einzige Bedingung ist, daß sie vor dem ersten Aufruf durch ein Prototyp bekannt gemacht wurde. Es hat sich eingebürgert, die Funktion main() entweder als erste oder letzte Funktion in einem Programm einzusetzen. 63 d.h. vor ihrem Aufruf deklariert bzw. definiert sein 83 Die Programmiersprache C++ 1.7.3 Funktionswerte (Ergebniswertrückgabe) Funktionen, die nicht mit void deklariert sind, müssen einen Funktionswert zurückgeben. Der Funktionswert wird durch eine return-Anweisung festgelegt: return return_ausdruck Der Wert des return_ausdrucks entspricht dem Rückgabewert der Funktion und muß dem vereinbarten Typ entsprechen, z.B.: #include <iostream.h> int fak(int n) { return (n > 1) ? n * fak(n-1) : 1; } int main() { int n; cout << "Berechne die Fakultaet von "; cin >> n; cout << endl << "Ergebnis: " << fak(n) << endl; } Der Typ des "return"-Ausdrucks wird mit dem Typ des Funktionswerts abgeglichen und gegebenenfalls alle standardmäßigen und benutzerdefinierten Umwandlungen durchgeführt. Trifft das Programm auf eine return-Anweisung, dann wird die Ausführung des Funktionsaufrufs beendet. Ist kein return vorhanden, dann endet die Ausführung an der letzten schließenden Klammer des Funktionsausrumpfes. Falls der Rückgabewert vom Typ void ist, dann kann die return-Anweisung auch weggelassen werden bzw. nur „return“ angegeben werden. Auch Referenzen können als Funktionswert zurückgegeben werden. Da Referenzen L-Values sind (d.h.: Sie können verändert werden), kann ein Funktionsaufruf auf der linken Seite einer Zuweisung stehen oder als Referenzparameter an eine andere Funktion übergeben werden. Bsp.: Eine Minimum-Funktion bestimmt die kleinere von zwei Variablen. Dieser Variablen soll der Wert 0 zugewiesen werdenbzw. ein Wert von der Tastatur eingelesen werden. int &min(int &a, int &b) { if (a < b) return a; return b; } int x, y; // Zuweisen von 0 min(x,y) = 0; // Einlesen Wert von der Tastatur cin >> min(x,y); 84 Die Programmiersprache C++ 1.7.4 Rekursion 1.7.4.1 Rekursive Funktionen Eine Funktion ist rekursiv, falls die Ausführung des Rumpfs der Funktion wiederum zum Aufruf der Funktion führt. Man unterscheidet direkte Rekursion (Eine Funktion ruft sich selbst im Rumpf wieder auf) und indirekte Rekursion (Der rekursive Aufruf befindet sich nicht im Funktionsrumpf. So ruft bspw. eine Funktion A eine Funktion B auf und diese startet in ihrem Rumpf die Funktion A). Wie Wiederholungsanweisungen neigen auch rekursive Prozeduren zur Gefahr nicht abbrechbarer Berechnungen. Eine Terminierung ist unbedingt erforderlich. Das geschieht über eine Bedingung, von der der rekursive Aufruf abhängt. Falls x bspw. die Menge der Programmvariablen ist, dann muß es eine Funktion f(x) geben, die die Abbruchbedingung ( wie in den while-Schleifen) festlegt. Nachzuweisen ist , daß f(x) bei jeder Ausführung der Rekursion abnimmt. Besonders einfach ist der Nachweis der Terminierung dann, wenn der rekursiven Funktionsprozedur der ganzzahlige Parameter n bzw. N zugeordnet wurde. Der rekursive Aufruf mit dem Parameterwert n - 1 bzw. N - 1 garantiert das strikte Abnehmen des Wertes von f(x) . Bei jeder rekursiven Anwendung wird ein Satz lokaler, gebundener Variablen kreiert. Sie haben zwar diesselben Namen wie die Objekte des vorangegangenen Aufrufs der Prozedur, besitzen aber verschiedene Werte. Die Namen beziehen sich immer auf die zuletzt erzeugten Variablen. Bsp. für einfache rekursive Funktionen C++ ermöglicht die Definition rekursiver Funktionen. N 1. Berechnung von N ! n n 1 Man kann dies aber auch so schreiben: N! N ( N 1)! für N 1 und 0! 1 . Das führt unmittelbar zur Lösung: long fak(int n) { if (n == 0) return 1; else return n * fak(n-1); } 2. Zurückführung der Multiplikation auf die Addition long mult(int a, int b) { if (b == 1) return(a); else return(mult(a,b - 1) + a); } 3. Potenzieren 85 Die Programmiersprache C++ In C++ ist kein spezieller Operator dafür vorgesehen. Dieser Operator kann aber leicht über eine rekursive Funktionsprozedur simuliert werden: long potenz(int x, int n) { if (x == 0) return(0); else if (n == 0) return(1); else return(potenz(x,n-1)*x); } bzw. double potenz(float x, int n) { if (x == 0.0) return(0.0); else if (n == 0) return(1); else if (n < 0) return(potenz(x,n+1)/x); else return(potenz(x,n-1)*x); } Das Beispiel zeigt, daß man auch mit gebrochene Zahlen Potenzen bilden kann, z.B. 05 . 2 0.25 . Ebenso sind Hochzahlen mit negativen Werten (Ganzzahlen) möglich, 1 1 4. z.B.: 0.52 2 0.25 0.5 4. Fibonacci-Zahlen Die Berechnung dieser Zahlen erfolgt nach folgender Vorschrift: Fib(N) = Fib(N - 1) + Fib(N - 2) für N > 1. Fib(1) = 1, Fib(0) = 0 Dieser Vorschrift kann folgende Funktionsprozedur zugeordnet werden: unsigned long fib(short n) { if (n == 0) return 0; else if (n == 1) return 1; else return(fib(n-1) + fib(n-2)); } Das folgende Programm ermöglicht durch die Funktion „striche()“ und einen Zähler in der Parameterliste ein Protokoll. Es zeigt, welche Funktionsaufrufe in welchen Rekursionsstufen (-tiefen)64 auftreten: #include <iostream.h> void striche(short st) { for (int i = 1; i <= st; i++) cout << "- "; } unsigned long fib(short xN, short stufe) { unsigned long x, y; cout << "\n"; striche(stufe); 64 Rekursionstiefe: Anzahl der Aufrufe der Funktion seit Beginn der Programmausführung minus der Anzahl der Rückgaben an das ausführende Programm 86 Die Programmiersprache C++ cout << " Eingang N = " << xN; if (xN <= 1) { cout << "\n"; striche(stufe); cout << " Ausgang: N = " << xN << " X = " << x << " Y = " << y; return(xN); } else { x = fib(xN-1,++stufe); --stufe; y = fib(xN-2,++stufe); --stufe; cout << "\n"; striche(stufe); cout << " Ausgang: N = " << xN << " X = " << x << " Y = " << y; return(x + y); } } void main() { short n; unsigned long ergebnis; cout << "\nBerechnung von Fibonacci-Zahlen\n"; cout << "Argument: "; cin >> n; ergebnis = fib(n,0); cout << '\n'; cout << "\nFib(" << n << ")=" << ergebnis; } Das vorliegende Programm führt bei einem Aufruf bspw. zu dem folgenden Protokoll: Eingang: N = 5 - Eingang: N = 4 - - Eingang: N = - - - Eingang: N - - - - Eingang: - - - - Ausgang: - - - - Eingang: - - - - Ausgang: - - - Ausgang: N - - - Eingang: N - - - Ausgang: N - - Ausgang: N = - - Eingang: N = - - - Eingang: N - - - Ausgang: N - - - Eingang: N - - - Ausgang: N - - Ausgang: N = - Ausgang: N = 4 - Eingang: N = 3 - - Eingang: N = - - - Eingang: N - - - Ausgang: N - - - Eingang: N - - - Ausgang: N - - Ausgang: N = - - Eingang: N = - - Ausgang: N = - Ausgang: N = 3 Ausgang: N = 5 X Ergebnis: 5 3 = N N N N = = = 3 2 = = = = 2 X 2 = = = = 2 1 1 X = 2 = = = = 2 1 1 X 1 1 0 0 X = 1 1 X = 11721 Y = 1 0 0 X = 11721 Y = 1 X = 1 Y = 0 X = 1 Y = 0 = 1 Y = 1 X = 11721 Y = 1 X = 11721 Y = 1 = 1 Y = 0 2 Y = 1 1 1 X = 11721 Y = 1 0 0 X = 11721 Y = 1 X = 1 Y = 0 X = 1 Y = 0 = 1 Y = 1 3 Y = 2 87 Die Programmiersprache C++ Jeder Aufruf mit N > 1 führt zu zwei weiteren Aufrufen. Das zeigt die folgende grafische Darstellung der Aufrufhierarchie: Aufruf: Fib(5) 4 3 3 2 1 2 1 1 2 0 1 1 0 0 Abb. 1.8-1: Schematische Darstellung der Aufrufhierarchie von Fib(5) Insgesamt finden für Fib(5) 15 Aufrufe statt, für den Aufruf Fib(6) sind es bereits 31. Viele Aufrufe und damit Berechnungen werden wiederholt, obwohl sie bereits an anderer Stelle berechnet wurden. Allgemein wird die Funktion Fib() bei Eingabe einer beliebigen positiven, ganzen Zahl N genau 2 N-1 - 1 mal aufgerufen. Man sagt: Der Algorithmus verhält sich proportional zu 2N-1 65bzw. 2N. Allgemein ist ein Algorithmus von exponentieller Komplexität, falls es eine beliebige Basis M66 gibt, zu der er sich bei N Eingabewerten wie MN verhält. Für die Größenordnungen, in denen solche Komplexitätsbetrachtungen stattfinden, spielt es keine Rolle, welchen konkreten Wert M besitzt. Man spricht nur noch von exponentieller Komplexität. Der Aufwand eines Algorithmus steigt also exponentiell mit seinen Eingabewerten. Im vorliegenden Fall bedeutet dies: Falls die Eingabewerte für Fibonacci-Zahlen zu groß gewählt wurden, kann eine rekursive Berechnung dieser Zahlen nicht mehr erfolgen. Das Problem liegt darin, daß ein Aufruf von Fib() zwei weitere nach sich zieht. Allerdings könnte man hier auf die Rekursion zweckmäßigerweise verzichten. Es läßt sich ein iterativer Algorithmus angeben, der die Berechnung in einer Schleife realisiert, z.B.: unsigned long fibIt(short n) { unsigned long x, // aktuelle Fibonacci-Zahl vx = 1, // Vorgaenger fib(n-1) bzw. vvx = 0; // fib(n - 2) for (short i = 1; i < n; i++) { x = vx + vvx; vvx = vx; vx = x; } return(x); } 65 66 Die 1 wird hier vernachlässigt, da sie bei großen Werten von N praktisch keinen Beitrag liefert. hier 2 88 Die Programmiersprache C++ Ein Vergleich der rekursiven Lösung mit der iterativen Lösung zeigt: Die rekursive Lösung ist eigentlich unbrauchbar. Auf rekursive Lösungen sollte man verzichten, wenn es iterative Lösungsmöglichkeiten gibt. 5. Gegeben ist auf rekursiver Basis die mathematische Defintion einer Funktion 1 Fn ( x) x ((2 n 1) x F ( x) ( n 1) F ( x)) / n n 1 n2 Die obere Zeile nach der geschweiften Klammer gilt für n = 0, die folgende Zeile für n = 1 und die letzte Zeile , falls n > 1. Rekursiv läßt sich diese Aufgabe folgendermaßen lösen. double frek(int n, float x) { if (n == 0) return(1); else if (n == 1) return(x); else return (((2 * n - 1) * x * frek(n - 1,x) - (n - 1) * frek(n - 2,x)) / n); } Obwohl die Funktion rekursiv definiert ist, ist die iterative Lösung offensichtlich effiktiver: #include <iostream.h> double fit(int n, float x) { int f1 = 1; float fx = x, nwert; if (n == 0) return(1); else if (n == 1) return(x); else { for (int i = 2; i <= n; i++) { nwert = ((2 * i - 1) * x * fx - (i - 1) * f1) / i; f1 = fx; fx = nwert; } return(nwert); } } 89 Die Programmiersprache C++ 1.7.4.2 Rekursion und Iteration Die in den letzten Beispielen gezeigten Umwandlungen einer Rekursion in eine Iteration ist kein Zufall. Man kann zeigen, daß sich jeder rekursive Algorithmus in einen iterativen umwandeln läßt. Da die Iteration in den meisten Fällen wesentlich effizienter ist als die Rekursion, ist die Frage berechtigt, weshalb man überhaupt die Rekursion verwendet. Für die Rekursion spricht: 1. Es gibt bestimmte rekursiv formulierte Algorithmen, die schneller oder wenigstens gleich schnell arbeiten als vergleichbare iterative. 2. Es lassen sich viele Probleme rekursiv „sehr einfach“ lösen. Rekursiv formulierte Algorithmen bieten sich insbesondere an, wenn das zugrundeliegende Problem oder die zu behandelnden Daten rekursiv definiert sind. Das ist aber noch keine Garantie dafür, daß ein rekursiver Algorithmus auch der beste Weg zur Lösung des Problems ist. Bsp.67: Addition einer unbekannten Zahl von ganzzahligen Werten #include <iostream.h> int s = 0; void addierezahlen() { int x; if ((cin >> x) > 0) { // Terminierung: Eingabe eines Buchstaben bzw. Sonderzeichens s += x; addierezahlen(); } } int main() { addierezahlen(); cout << "Die Summe ist: " << s << endl; } Das vorliegende Programm ist ein Beispiel für eine „tail recursion68“. Es gibt nur einen rekursiven Aufruf, der am Ende der Funktion addierezahlen() erfolgt. Er unterscheidet sich nicht wesentlich von einem Sprung an den Anfang der Funktion. Daher kann addierezahlen() einfach durch die folgende iterative Version ersetzt werden: // Iterative Version void addierezahlen() { int x; while ((cin >> x) > 0) s += x; } 67 68 PR17307.CPP Endrekursion 90 Die Programmiersprache C++ Hinsichtlich der vorzuziehen, da Programmeffizienz ist die iterative, nichtrekursive Version - beim Einlesen einer große Zahlenmenge die rekursive Version einen Stapelüberlauf bewirken kann - auch die Rückgabeadresse69 gespeichert werden muß Nur im Stapel ist der einzige sichere Speicherplatz, da hier die letzten auf den Stapel geschobenen Daten zuerst wieder entnommen werden. Neben der Rückgabeadresse werden auch lokale Variable (z.B die Variable x) auf den Stapel gebracht. Die Stapelgröße ist natürlich begrenzt. Ein häufiges Aufrufen derselben rekursiven Funktion führt daher zum Überlaufen. Zu diesem Stapelüberlauf kommt es nur dann, wenn die Rekursion einen bestimmten Wert übersteigt. Unmittelbar nach dem Programmstart hat die main()-Funktion die Rekursionstiefe 1, und alle anderen Funktionen die Rekursionstiefe 0. Der zum Ablaufzeitpunkt benötigte Speicherplatz ist dann: S d 1 m1 d 2 m 2 ...d n m n d: Rekursionstiefe m: Menge an Speicherplatz 1..n: Indizes zur Bestimmung der vorliegenden Funktionen Generell sollte man daher auf die Verwendung von Rekursionen immer dann verzichten, falls es eine offensichtliche Lösung mit Iteration gibt. Solche Lösungen existieren bspw. nicht, wenn die rekursive Funktion mehr als einen rekursiven Aufruf enthält. 1.7.4.3 Türme von Hanoi 1. Aufgabenstellung und allgemeine Lösung Aufgabenstellung: Es soll ein Turm mit „n“ Scheiben, die der Größe nach geordnet liegen, scheibenweise von Quellplatz (K) nach Zielplatz (G) transportiert werden. Ein Ausweichplatz (S) steht zur Verfügung. Es darf aber nur eine kleinere auf eine größere Scheibe gelegt werden. Allgemeine Lösung der Ausgabe: Zum Transport von „n“ Scheiben sind Sn Schritte notwendig. Es gilt folgende rekursive Beziehung (mit S 1 = 1): S n 2 (S n 1 ) 1 Sn 2 n 1 Die allgemeine Lösung der Aufgabe besteht in 69 d.h. die Stelle, an der nach der Ausführung der gerufenen Funktionsprozedur die Programmkontrolle übergeben wird 91 Die Programmiersprache C++ 1) Transport von (n - 1) Scheiben nach dem Hilfsplatz Quellplatz Hilfsplatz 2) Verlagerung von Scheibe „n“ nach Zielplatz Quellplatz Zielplatz 3) Transport von (n - 1) Scheiben nach Zielplatz Zielplatz Hilfsplatz Abb. 1.8-2: Beschreibung der Problemlösung In der vorliegenden Abbildung werden zunächst 4 der 5 Scheiben vom Quellplatz auf einen Hilfsplatz bewegt. Danach wird die größte Scheibe vom Quellpaltz auf den den Zielplatz gebracht. Anschließend bewegt man die 4 Scheiben vom Hilfsplatz auf den Zielplatz und zwar so70, wie man zuvor die 5 Scheiben von Quellplatz auf Zielplatz bewegt hat. Wie der Algorithmus das im Detail bewältigt, zeigt die folgende Darstellung, die vollständig alle Schritte bei der Verlagerung von 3 Scheiben wiedergibt: 70 das ist der eigentliche Trick, den die Rekursion ermöglicht 92 Die Programmiersprache C++ Quellplatz Zielplatz Hilfsplatz Abb. 1.8-3: Beschreibung der Lösung für drei Scheiben In jedem Schritt wird ein um eine Scheibe kleinerer Stapel bewegt. Die Rekursion bricht offensichtlich ab, falls keine Scheibe mehr bewegt werden muß. 93 Die Programmiersprache C++ 2. Objektorientierte Darstellung Ein objektorientiertes Programm kann man sich als Abbildung von Objekten der realen Welt in Software vorstellen. Die Abbildungen selbst werden Objekte genannt. Klassen sind die Beschreibungen von Objekten. Eine Klasse ist ein „Abstrakter Datentyp“ (ADT), d.h. die Abstraktion von ähnlichen Eigenschaften und Verhaltensweisen ähnlicher Objekte. Ein Objekt hat einen inneren Zustand, der durch andere Objekte oder Elemente der in der Programmiersprache vorgegebenen Datentypen dargestellt wird. Der Zustand kann sich durch Aktivitäten (Operationen), die auf Objektdaten durchgeführt werden, ändern. Die von jedem benutzbaren Operationen bilden die „öffentliche Schnittstelle“, gekennzeichnet durch das Schlüsselwort „public“. Ein Objekt ist die konkrete Ausprägung einer Klasse, es belegt im Gegensatz zur Klasse Bereiche im Speicher, die Werte von Objekteigenschaften darstellen. Eine in C++ formulierte Klasse hat eine typische Gestalt: class Klassenname { private71: typ attribut1; typ attribut2; // ... public: typ elementfunktion1(); typ elementfunktion2(); // ... }; In einem Klassendiagramm wird das so beschrieben: Klassenname private: typ attribut1; typ attribut2; // ... public: typ elementfunktion1(); typ elementfunktion2() // ... Abb.: Das Klassendiagramm der UML Der Gültigkeitsbereich von Klassenelementen ist lokal zu der Klasse. Die Daten sind „private“, d.h.: Sie sind von außen nicht sichtbar. Alles nach dem Schlüsselwort „public“ ist öffentlich zugänglich. 71 das Schlüsselwort private kann entfallen, weil die Voreinstellung private ist 94 Die Programmiersprache C++ 3. Implementierung in objektorientierter Darstellung72 /* Die Tuerme von Hanoi ------------------In einer Tempelstadt sollen sich 3 Podeste aus Kupfer, Gold und Siber befunden haben. der Sage nach soll das Ende der Welt gekommen sein, falls es jemand gelingt: - 100 Scheiben, die auf dem Kunpferpodest liegen, abzutragen und in derselben Reihenfolge auf dem Golpodest aufzuschichten. - Scheiben duerfen auch auf dem Silberpodest abgelegt werden - Es ist nicht erlaubt, dass eine kleinere Scheibe auf einer groesseren abgelegt wird */ #include <iostream.h> // Beschreibung der Klasse hanoi class hanoi { private: char quellplatz, hilfsplatz, zielplatz; short n; public: void bewegeTurm(short,char,char,char); }; // Beschreibung der in der Klasse deklarierten Schnittstellen // (Methoden) void hanoi :: bewegeTurm(short n, char quellplatz, char zielplatz, char hilfsplatz) { if(n > 0) { bewegeTurm(n-1, quellplatz, hilfsplatz, zielplatz); cout << "\n\t\tBewege Scheibe von Turm " << quellplatz << " nach Turm " << zielplatz; bewegeTurm(n-1, hilfsplatz, zielplatz, quellplatz); } } // Klient (client) void main() { const short MAXSCHEIBEN = 16; // Mehr dauert ewig. short anzahl; hanoi turm; // Instanz, Objekt bzw. Server /* Wie einer Variablendefinition wird für ein Objekt Speicherplatz bereitgestellt. Zu diesem Zweck wird eine besondere Klassendefinition aufgerufen, die Konstruktor genannt wird. Der Konstruktor wird vom System automatisch bereitgestellt, kann aber auch selbst definiert werden. */ cout << "\n\n\t\t\tDie Tuerme von Hanoi"; cout << "\n\t\t\t-------------------\n\n"; 72 PR17305.CPP 95 Die Programmiersprache C++ do { cout << "\tWieviele Scheiben sollen bewegt werden (max. " << MAXSCHEIBEN << "): "; cin >> anzahl; } while ((anzahl < 1) || (anzahl > MAXSCHEIBEN)); /* Der Server "turm" erbringt fuer den Klient "main" eine Dienstleistung */ turm.bewegeTurm(anzahl, 'K', 'S', 'G'); } // Rekursionsanfang cout << endl; // Ende von main() Auch hier ist festzustellen: Die Bearbeitung einer großen Anzahl von Scheiben nimmt einige Rechenzeit in Anspruch. Auch hier die Komplexität dieses Algorithmus exponentiell (2N). genau wie bei der Berechnung der Fibonacci-Zahlen zieht ein Aufruf zwei weitere nach sich. 1.7.4.4 Damen-Problem Das Problem wird beschrieben am einfachen, übersichtlich darstellbaren 4-DamenProblem, einer Vereinfachung des bekannten 8-Damen-Problems. Hier sollen bekanntlich 8 Damen so auf einem Schachbrett positioniert werden, daß sie sich nicht schlagen können, d.h.: Zwei Damen stehen nie in derselben Zeile oder Spalte oder Diagonale. Das Verfahren kann grafisch als Baum dargestellt werden, dessen Knoten Lösungsvektoren sind und dessen Kanten den Weg des Algorithmus von einem Lösungsvektor der Länge (i - 1) zu einem der Länge i angeben. (_,_,_,_) 1 2 3 2 (2,_,_,_) (1,_,_,_) 4 1 S S (1,3,_,_) 2 S (1,4,_,_) 4 2 S 3 3 4 4 (2,4,_,_) 1 S (2,4,1,_) (1,4,2,_) (2,4,1,3) Abb. 1.8-4: Plan zur Lösung des 4-Damen-Problems Von der Ausgangssituation (keine Dame ist positioniert, Wurzel) werden bestimmte Lösungsvariablen verfolgt. Im ungünstigsten Fall führt die bearbeitete Variante in 96 Die Programmiersprache C++ eine Sackgasse. In diesem Fall muß der Variantenpfad so weit zurückverfolgt werden, bis man zu einem Knoten mit einem noch nicht bearbeiteten Nachfolger kommt. Man spricht bei dieser Vorgehensweise auch von Tiefensuche in vorliegenden Baumgraphen und nennt die Verfahrensweise Backtracking. Die Lösung läßt sich anschaulich darstellen: 0 -3 3 y x x x x x 2 8 5 Abb. 1.8-5: „Eindimensionale Beschreibung“ des 4-Damen-Problems Da Damen sich auch in der Diagonale bedrohen, sind auch Haupt- und Nebendiagonalplätze zu überwachen. Es sollen zur Lösung des 8-Damen-Problems acht Zahlen ermittelt werden. Die Zahlen stehen von links nach rechts für die Position der Dame in der entsprechenden Spalte. /* Das Problem der acht Damen -------------------------Kurzbeschreibung: Durch Backtracking werden alle Moeglichkeiten gefunden, acht Damen so auf einem Schachbrett zu positionieren, dass keine Dame eine andere bedroht. */ #include <iostream.h> class damen { private: 97 Die Programmiersprache C++ unsigned short pos[8]; bool Zeile[8]; bool HD[15]; // Hauptdiagonale "/" bool ND[15]; // Nebendiagonale "\" public: damen(); // Konstruktor void versuche(int); friend ostream& operator <<(ostream &, damen &); // Ausgabe }; // ------ Schnittstellendefinition (Methoden) ------------------damen :: damen() { int i; // Im Konstruktor werden die einzelnen Felder initialisiert. for(i = 0; i < 8; i++) Zeile[i] = true; for(i = 0; i < 15; i++) HD[i] = ND[i] = true; } void damen :: versuche(int j) { for(int i = 0; i < 8; i++) { if(Zeile[i] && HD[j+i] && ND[7+i-j]) { // Position ist moeglich, also wird Dame gesetzt: pos[i] = j; // Dadurch werden alle oben ueberpr•ften Zeilen und // Diagonalen bedroht: Zeile[i] = HD[j+i] = ND[7+i-j] = false; /* Falls noch nicht alle Damen gesetzt wurden, erfolgt ein weiterer, rekursiver Aufruf, der die naechste Spalte ueberprueft. */ if(j < 8) versuche(j+1); else // Sonst wurde moegliche Loesung gefunden cout << (*this); /* Nun muss der Versuch noch zurueckgenommen werden, da er am nach Verlassen dieser Spalte wieder moeglich wird */ Zeile[i] = HD[j+i] = ND[7+i-j] = true; } // Ende von if( ... } // Ende von for( ... } // Ende von Versuche() // --------- non-member-Funktionen --------------------ostream& operator<<(ostream &s, damen &d) { s << "\n\t"; for(short i = 0; i < 8; i++) s << d.pos[i] << " "; return(s); } // --------Klient: main-program -----------------------int main() { damen queens; // Instanz, Server queens.versuche(1); // Rekursionsanfang } // Ende von main() 98 Die Programmiersprache C++ Es gibt insgesamt 92 Lösungen73, wobei jedoch nur 12 tatsächlich verschieden sind. die übrigen entstehen durch Permutation der Zeilen, d.h.: Die Dame in der 1. Spalte wird in diesselbe Zeile der 2. Spalte, die Dame in der zweiten in dieselbe Zeile der der 3. Spalte gesetzt, bis schließlich die Dame in der 8 Spalte in diesselbe Zeile der 1. Spalte gesetzt wird. 1.7.5 Überladen von Funktionsnamen (overloading) Funktionen mit gleichen Funktionsnamen aber unterschiedlichen Parameterdatentypen Diese Technik wird in C++ für elementare Operationen bereits genutzt. Es gibt nur einen Namen für die Addition (, nämlich +,) der für die Addition von Integer-, Gleitpunkt- und Pointer-Typen verwendet werden kann. Normalerweise muß der Compiler entscheiden, wenn eine Funktion mit dem Namen f aufgerufen wird, welche Funktion mit dem Namen f gemeint ist. Dies geschieht über einen Vergleich der aktuellen Argumenttypen mit den deklarierten Argumenttypen. Die am besten passende Funktion wird aufgerufen. Gibt es keine "am besten passende Funktion" wird ein Compiler-Fehler erzeugt. Bsp. #include <iostream.h> int addiere(int a, int b) { return a + b; } float addiere(float a, float b){ return a + b; } int main() { int a = 3; int b = 2; float x = 3.14159; float y = 5.6782; cout << "a + b = " << addiere(a,b) << endl; cout << "x + y = " << addiere(x,y) << endl; cout << "a + y = " << addiere(float (a), y) << endl; // addiere(a,y); Fehler: zweideutig } Das Überladen von Funktionen (Bildung von Homonymen) ist ein Mittel, allgemeingültigere Programme zu schreiben. Es ist auch möglich, Operatoren zu überladen. Der Shift-Operator ist bspw. ein häufig vom Anwender überladener Operator, wenn es um die Ausgabe von Daten auf dem Bildschirm geht. Ein Aufruf überladener Funktionen ist in zwei Fällen möglich: 1. Die Typen der aktuellen Parameter stimmen genau mit denen der formalen Parameterliste einer überladenen Funktion überein. 2. Es existieren Typumwandlungen, mit denen Typen der aktuellen Parameter mit den Typen einer formalen Parameterliste zur Deckung gebracht werden können. Technisch funktioniert das dadurch, daß an den eigentlichen Funktionsnamen die Liste der Parameter codiert angefügt wird (sog. „name mangling“). Der Rückgabetyp wird nicht in den Namen codiert. Genausowenig wird zwischen einem 73 vgl. PR17308.CPP 99 Die Programmiersprache C++ Argument als „value“-Parameter und einem Argument gleichen Typs als ReferenzParameter unterschieden. Die große Familie der in Deklarationen möglichen Spezifizierer und Modifizierer erschweren Compiler und Programmierer die Entscheidung, ob Funktionen mit denselben Namen unterschiedliche Parameterlisten besitzen oder nicht. So können bspw. "reine" und ReferenzParameter nicht unterschieden werden, da ihre aktuellen Parameter identisch sind. Grundsätzlich gilt: Sind die aktuellen Argumente zweier formaler Parameter, die sich nur im Spezifizierer unterscheiden, identisch, dann ist Überladen nicht möglich, z.B.: f(int x); f(const int x); f(volatile int x); // Fehler // Fehler Auch int f() bzw. int f(void) sind in einem Gültigkeitsbereich nicht möglich. Funktionen können nur im selben Geltungsbereich (scope) überladen werden. Funktionen aus einem äußeren Gültigkeitsbereich werden durch die lokale Deklaration einer Funktion gleichen Namens verborgen. Falsch ist daher extern int f(char *); void illegal() { extern double f(double); // verbirgt äußeres f f("Leider falsch"); } Der Compiler arbeitet mit festen Regeln bei der Ermittlung der Definition einer Funktion, die für den jeweiligen Parameter am besten paßt. Bsp.: Hochzählen von Zahlen und Buchstaben durch eine überladenen Funktion #include <iostream.h> int naechsteGroesse(int a) { return(++a); } int naechsteGroesse(int a, int b) { return(a + b); } char naechsteGroesse(char a) { return(++a); } char naechsteGroesse(char a, int b) { return ((char)(a + b)); // cast sorgt für korrekte Rueckgabe } int main() { cout << "\n Die naechste Groesse von 5 ist " << naechsteGroesse(5); cout << "\n Die naechste Groesse von F ist " <<naechsteGroesse('F'); 100 Die Programmiersprache C++ cout << << cout << << "\n Die naechste Groesse von 5 ist nach 4 Einheiten " naechsteGroesse(5,4); "\n Die naechste Groesse von F ist nach 4 Einheiten " naechsteGroesse('F',4); } Der Compiler bildet für jeden Parameter eine Liste von passenden Funktionsversionen. So erzeugt der Compiler für den Aufruf naechsteGroesse(‘F’,4) folgende Listen: Nr. des Parameters: passende Funktion 1 naechsteGroesse(char,int) naechsteGroesse(char) 2 naechsteGroesse(char,int) Aus dieser Funktionsmenge bildet er dann die Schnittmenge. Ist diese leer oder enthält sie mehr als eine Funktionsversion, so führt das zu einem Fehler, z.B.: f1(char a, int b) { ....... } f1(int a, char b) { ....... } ...... f1('x','y'); Default-Argumente In C++ kann der Funktion eine variable Anzahl von Parametern übergeben werden 74, falls Standardwerte für fehlende Parameter festgelegt werden. Liegt bspw. anstatt char naechsteGroesse(char a, int b) folgende Darstellung der Funktion vor char naechsteGroesse(char a, int b = 1), dann findet der Compiler für den Aufruf naechsteGroesse('F') zwei passende Funktionen und erzeugt einen Fehler. Falls der Compiler Standardargumenten bei der Suche nach einer passenden Funktionsversion berücksichtigen muß, dann ist für ihn jede Aufrufmöglichkeit eine eigene Version. „n“ Standardargumente führen zu „n+1“ Versionen. Konvertierung (Konversion) Immer, wenn der Compiler keine genau passende Version findet, versucht er es mit Konvertierungen. Allerdings kann er dann auch natürlich mehrere passende Versionen finden. So führt ein Aufruf von naechsteGroesse('F','D') im vorliegenden Programm zu einer internen Konvertierung75 und zu einer passenden Version. 74 75 vgl. 1.7.2, 4. der char-Wert ‘D’ wird in seinen entsprechenden int-Wert (68) umgewandelt. 101 Die Programmiersprache C++ Algorithmus zur Homonymenauflösung76 (1) Bestimme die Menge F jener Funktionen, die in Namen und Anzahl der Argumente77 mit dem Aufruf übereinstimmen und deren formale Argumentdatentypen mit den aktuellen Parameterdatentypen des Aufrufs kompatibel (d.h. identisch oder konvertierbar) sind. Falls diese Menge höchstens ein Element besitzt, kann der Algorithmus abgebrochen werden, ansonsten ist mit Schritt (2) fortzufahren. (2) Bestimme für jeden aktuellen Parameter p i die Menge Fi’ aller Funktionen aus F, die bzgl. des Datentyps für das Argument p i am besten zum gegebenen Aufruf passen. Am „besten passen“ bedeutet: - Es werden keine Konversionssequenzen mit mehr als einer benutzerdefinierten Konversionsfunktion berücksichtigt. - Eine kürzere Trivialkonversion wird einer längeren vorgezogen - Die folgenden Trivialkonversionen haben im allg. keinen Einfluß auf die Beurteilung zweier Konversionssequenzen: T -> T&, T& -> T, T[] -> T*, T() -> (*T). T -> const T, T* -> const T*. Es läßt sich für Argumentkonversionen eine Wertskala angeben. Konversionen, die eine Konversion einer niederen Stufe beinhalten, sind besser als solche, die auch Konversionen höherer Stufen miteinbeziehen: 1. Exakte Entsprechung Es sind keine Konversionen (oder lediglich Trivialkonversionen) zur Anpassung des aktuellen Arguments an den Typ des formalen Parameters nötig. Unter Trivialkonversion sind jene, die „const“-Zeiger oder -Referenzen aus „nicht const-Zeigern“ oder „-Referenzen“ erzeugen, schlechter als andere. 2. Auswertungen (Integralauswertungen und float-double-Konversionen) Integralauswertungen sind: char, short, Aufzählungstypen, Bitfelder. Sie können immer anstelle vom int benutzt werden. Falls Werte des ursprünglichen Datentyps durch int dargestellt werden können, dann wird nach int umgewandelt, andernfalls nach unsigned int. Es wird von kleinen ganzzahligen Typen in Richtung int und von kleineren gebrochenzahligen Typen Richtung double promotet. 3. Standardkonversionen. Der Compiler versucht. Aktuelle und formale Parameter durch Anwendung von Standardkonversionen zur Deckung zu bringen. Alle numerischen typen oder zeichentypen können ineinander konvertiert werden, außerdem können alle Zeiger zu void* konvertiert werden. 4. Benutzerdefinierte Konversionen. Es werden die benutzerdefinierten Konversionen angewandt, die in benutzerdefinierten Klassen definiert werden. 5. Variable Parameterliste Die Zuordung eines Arguments zum Auslassungszeichen (...) ist schlechter als jede Konversion. (3) Bilde den Durchschnitt aller Fi’. Enthält dieses genau ein Element, nenne es f und fahre mit Schritt (4) fort. Andernfalls ist der Aufruf illegal. 76 Vereinfachte Darstellung, im Detail beschrieben in Ellis, M.A. und Stroustrup, B.: The Annoted C++ Reference Manual, Addison Wesley, Reading MA, 1990 77 Arität 102 Die Programmiersprache C++ (4) Überprüfe durch paarweises Vergleichen, ob f für mindestens ein Argument eine echt bessere Entsprechung darstellt als jede andere Funktion in F. Wenn dies der Fall ist, kann f aufgerufen werden. Andernfalls ist der Aufruf illegal. Codierung der Funktionsnamen für den Binder Der Compiler kann mit Hilfe der angegebenen Regeln Funktionen mit gleichen Namen anhand der Argumentdatentypen auseinander halten. Der Binder (Linker) hat keinen Zugriff auf syntaktische Informationen wie Datentypen und kann deshalb mit überladenen Funktionsnamen nichts anfangen. Argumentdatentypen werden deshalb vom Compiler nach einem bestimmten implemtierungsabhängigen Schlüssel kodiert und an den eigentlichen Namen angehängt. So kann int min(int anzArg, int a, int b, ...) im Objektcode bspw. min__Fiiie heißen. F: "globale Funktion" i: int-Argument e: Auslassungszeichen (ellipsis) Die Variante double min(int anzArg, double a, double b, ...); heißt dann min__Fidde. Einbinden fremdsprachiger Unterprogramme Die Namenskonvention zur Codierung von Funktionsnamen führt aber auch zu Problemen- So ist die Kompatobilität mit C-Unterprogrammen nicht mehr gesichert. Die C-Standardbibliotheksfunktion char* strcpy(char*, const char*) erhält in C++ bspw. die Objektcodebezeichnung strcpy__FPcCPc . P: Pointer c: char C: const Die Funktion wird in C-Bibliotheken nur strcpy genannt und kann daher vom Linker nicht gefunden werden. Für diese Fälle gibt es eine spezielle Art der internen Deklaration: extern "C" char* strcpy(char* ziel, char* quelle); “C“ gibt an: Es handelt sich um eine C-Funktion und der Funktionsname folgt nicht dem C++-Namensschema. 103 Die Programmiersprache C++ 1.7.6 Operatorfunktionen In Operatorfunktionen tritt an die Stelle des üblichen Funktionsnamens das Schlüsselwort operator, gefolgt von dem zu überladenden Operator. Die Parameterangaben sind auf die in C++ definierten Aritäten der Operatoren beschränkt78. In C++ können fast alle Operatoren (Ausnahmen sind: .,.*,::, ? : , sizeof) überladen werden. Man kann deshalb die Bedeutung79 der Operatoren im Zusammenhang mit benutzerdefinierten Datentypen (struct, class, union) frei bestimmen. Das Überladen der Operatoren << und >> in der iostream-Klasse ermöglicht so bspw. die Ausgabe und Eingabe von Werten zu elementaren Datentypen und char*, void*. Man kann generell diese Technik auf benutzerdefinierte Datentypen erweitern. Berücksichtigt man die Definition der Ausgabe- bzw. der Eingabeoperation in der iostream-Klasse, dann führt das zur folgenden allgemeinen Beschreibung für eine Funktion zur Ausgabe bzw. Eingabe eines beliebig definierten Datentyps: ostream& operator<<(ostream&, const T&); istream& operator>>(istream&, const T&); Das 1. Argument in diesen Operatorfunktionen ist ein Ausgabe- bzw. Eingabestrom80, das 2. Argument ist der benutzerdefinierte Datentyp. Der Rückgabewert ist eine Referenz auf den Ausgabe- bzw. Eingabestrom. 1.7.7 Inline-Funktionen In C++ kann eine Funktion als inline deklariert werden. Bei Aufrufen derartig deklarierter Funktionen setzt der Compiler den zugehörigen Programmcode direkt ein. Dadurch entfällt der beim Standard-Funktionsaufruf erforderliche Overhead der Parameterübergabe. Der Programmcode der Funktion kann jedoch mehrfach auftreten. Inline-Funktionen sind daher meistens nur für sehr kurze Routinen angebracht, z.B. #include <iostream.h> // inline-Funktionen inline fak(int n) { return n < 2 ? 1 : n * fak(n-1); } inline int max(int x, int y){ return x > y ? x : y; } inline int min(int x, int y){ return x <= y ? x : y; } inline int abs(int i){ return (i < 0 ? -i : i); } inline void setze(int &x, int bit){ x = (x | (1 << bit)); } // setzt das Bit mit der Nummer bit der Ganzzahl x inline void tausche(int& a, int& b){ int h = a; a = b; b = h; } main() { int n; cout << "Berechne die Fakultaet von "; cin cout << "Ergebnis: " << fak(n) << endl; 78 d.h.: Defaultargumente sind nicht erlaubt Semantik 80 das wurde in der iostream-Klasse so festgelegt 79 104 >> n; Die Programmiersprache C++ int i, j; cout << "Wert: "; cin >> i; cout << "Wert: "; cin >> j; cout << "Kleinerer Wert: " << min(i,j) << endl; cout << "Groesserer Wert: " << max(i,j) << endl; i = abs(i); j = abs(j); cout << "Wert von i: " << i << endl; cout << "Wert von j: " << j << endl; tausche(i,j); cout << "Vertauschter Wert i: " << i << endl; cout << "Vertauschter wert j: " << j << endl; setze(i,3); setze(j,5); cout << "Bit Nr. 3 von i gesetzt " << i << endl; cout << "Bit Nr. 5 von j gesetzt " << j << endl; } Wie die „register“-Angabe bei automatischen Variablen ist die „inline“-Spezifikation lediglich ein Hinweis für den Übersetzer. der Compiler kann, muß aber nicht den Funktionscode expandieren. Sobald die Komplexität der Funktion einen gewissen Schwellwert überschreitet (z.B. falls Schleifen vorkommen), wird ohne Warnung eine normale Funktion erzeugt. 1.7.8 Funktionsschablonen C++ erlaubt allgemeine Typ-Parameterangaben für Funktionen (und Klassen). Das ermöglicht in Funktionsparameterlisten die Angabe generischer Parameter und Funktionsaufrufe mit Parameterangaben (zur Laufzeit), die unterschiedliche Datentypen aufweisen. Deklarationen zu Funktionsschablonen (template functions) beginnen mit einer Parameterliste der Form template <class T1, class T2, .... , class Tn> ... funktionsdefinition Die Bezeichner Ti sind Namen, die für spezifische C++-Datentypen stehen und die bei der Anwendung einer Funktionsschablone übergeben werden. Das Schlüsselwort „class“ steht für den Datentyp bzw. Typ. T i kann Standard-Datentyp oder ein vom Benutzer definierter Typ sein81. Eine Funktionsschablone definiert eine Familie überladener Funktionen, deren Mitglieder im Bedarfsfall automatisch erzeugt werden. Bsp.: Die Funktion int min (int i, int j) { return (i < j) i ? : j; } kann für andere numerische Datentypen durch eine Funktionsschablone wesentlich allgemeingültiger angegeben werden: template <class T> T min(T a, T b) { return (a < b ? a : b); } 81 Anstatt class kann auch typename geschrieben werden 105 Die Programmiersprache C++ Diese Funktion wird wie eine gewöhnliche Funktion aktiviert: double m = min(13.11,11.13); int m = min(13,11); Natürlich ist diese Funktionsschablone auf den Vergleich solcher Elemente beschränkt, die in einer eindeutige Ordnungsbeziehung zueinander stehen. Ein derartiger unmittelbarer Vergleich ist bei Zeichenketten 82 nicht gegeben. Soll ein solcher Vergleich zur Ermittlung der kleineren Größe stattfinden, ist eine spezielle Funktion min() für den Typ „Zeichenkette“ anzugeben: #include <string.h> char* min(char* s1, char* s2) { return (strcmp(s1,s2) < 0 ? s1 : s2); } Der Compiler sucht bei der Übersetzung des vorstehenden Aufrufs zunächst eine exakt entsprechende (normale) Funktion. Falls eine derartige Funktion nicht vorhanden ist, werden alle verfügbaren Funktionsschablonen auf eine möglicherweise genau (ohne jegliche Argumentenkonversion) passende Variante untersucht. Wird eine solche Funktion gefunden, dann wird sie aus der Schablone generiert und aufgerufen. Für die Auswahl einer Funktion bei überladenen Funktions-Templates gelten folgende Regeln: 1. Der Compiler sucht nach einer existenten Funktion, die in den Parametern exakt übereinstimmt. Der sog. "exact match" ist nicht ganz trivial erklärt. Auf jeden Fall müssen die Parameter denselben Typ haben, Dabei sind triviale Umwandlungen wie z.B. T& oder T* nach T[] immer möglich. Existieren mehrere derartige Funktionen, wird eine Fehlermeldung wegen Mehrdeutigkeit ausgegeben. 2. Der Compiler sucht nach einem Funktions-Template, dass die entsprechende Funktion mit einer "exact match"-Argumentenliste erzeugen kann. Diese wird gegebenenfalls erzeugt. Bei Mehrdeutigkeit wird ein Fehler ausgegeben. 3. Nach den üblichen Regeln wird versucht überladene Funktionen zu finden. Sofern existent wird sie aufgerufen. Eine *.o- oder *.obj-Datei, die vom Compiler durch Übersetzen einer Datei nur mit Templates erzeugt wird, enthält keinen Programmcode und keine Daten. Ein Template ist keine Funktionsdefinition, sondern eine Schablone, nach der der Compiler erst bei Bedarf eine Funktion zu einem konkreten Datentyp erzeugt. 1.7.9 Objekte als Funktionen Die Aufgabe einer Funktion kann von einem Objekt übernommen werden 83. Dazu wird der Funktionsoperator () mit der Operatorfunktion operator()() überladen. Ein Objekt kann dann wie eine Funktion aufgerufen werden. Ein algorithmisches Objekt diesert Art wird Funktionsobjekt oder Funktor genannt. „kleiner“ und „groesser“ ist für Zeichenketten folgendermaßen zu interpretieren: „Kleiner“ ist eine Zeichenkette dann, wenn das 1. Element, an dem sich eine Zeichenkette von einer zweiten unterscheidet, im ASCII-Code weiter vorn liegt 83 Die Technik wird in den Algorithmen und Klassen der C++-Standardbibliothek häufig eingesetzt. 82 106 Die Programmiersprache C++ Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften von Objekten haben. 1.7.10 Spezifikation von Funktionen Eine Funktion erledigt eine Teilaufgabe und ändert dabei den Programmzustand. Die Spezifikation der Zustandänderungen durch die Bedingungen, die vor und nach dem Aufruf gelten, ist unbedingt sinnvoll. Dazu gehören - Annahmen über die Importschnittstelle (Eingabedaten) Fehlerbedingungen die Exportschnittstelle (Ausgabedaten) Für Vor- und Nachbedingungen werden die engl. Begriffe precondition84 und postcondition85 benutzt. Vor- und Nachbedingungen können zum Nachweis der Korrektheit eines Programms benutzt werden. Vor- und Nachbedingungen lassen sich über assert() verifizieren. „assert()“ ist abgeleitet vom engl. Assertion (Zusicherung). Zusicherungen werden mit dem Header <cassert> eingebunden. Bsp.: Erraten eine Zahl zwischen 1 und 10086 #include <iostream.h> #include <assert.h> void rateSpiel(int n) { // Precondtion: n > 0 // Postcondition: Der Anwender wurde gebeten sich eine Zahl // zwischen 0 und n zu merken. Die Funktion // stellt eine Reihe von Fragen, bis die // Zahl gefunden ist int rateAnz; char antwort; assert (n >= 1); cout << "Merken Sie sich eine Zahl zwischen 1 und " << n << "." << endl; antwort = 'N'; for (rateAnz = n;(rateAnz > 0) && (antwort != 'Y') && (antwort != 'y'); rateAnz--) { cout << "Ist die Zahl " << rateAnz << "?" << endl; cout << "Antworte Y oder N, druecke return: "; cin >> antwort; } if ((antwort == 'Y') || (antwort == 'y')) cout << "War mir bereits bekannt"; else cout << "Hier wurde geschwindelt" << endl; } int main() { rateSpiel(100); 84 Abkürzung pre Abkürzung post 86 PR17999.CPP 85 107 Die Programmiersprache C++ } 108 Die Programmiersprache C++ 2. Datentypen 2.1 Einfache, fundamentale Datentypen Größe und numerische Bereiche der grundlegenden Datentypen sind implementierungsspezifisch und leiten sich von der Architektur des Rechers ab. So gilt bspw. in Borland C++ für 16-Bit- bzw. 32-Bit-Datentypen87: a) folgende interne Darstellung s 15 int long int s 31 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-1: Ganzzahlige 16-Bit-Datentypen short int int, long int s 15 0 s 31 0 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-2: Ganzzahlige 32-Bit-Datentypen float s Exponent 31 22 Mantisse 0 double s Exponent 63 51 Mantisse 0 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-3: Gleitkomma-Datentypen 87 Für Borland C++ ist die IBM-PC-Familie Ausgangspunkt. Somit bestimmt die Architektur der Intel 8088- und 80x86-Mikroprozessoren die Auswahl der internen Darstellung für verschiedene Datentypen 109 Die Programmiersprache C++ b) folgende Größen und Bereiche Typ unsigned char Größe (Bits) 8 Bereich 0-255 char 8 -128 bis 127 enum 16 -32768 bis 32768 unsigned int 16 0 bis 65535 short int int 16 16 -32768 bis 32767 -32768 bis 32767 unsigned long 32 0 bis 4294967295 long 32 -2147483648 bis 2147483647 float 32 double 64 34 . 10 38 bis 3.4 1038 17 . 10 308 bis 17 . 10308 long double 80 3.4 10 4932 bis 11 . 10 4932 Anwendungen, z.B.: Kleine Zahlen, kompl. PC-Zeichens. Kleine Zahlen ASCII-Zeichen Geordnete Wertemengen Große Zahlen Schleifenzähler Zähler, kleine Zahlen Zähler, kleine Zahlen Astronomische Distanzen sehr große Zahlen 7-stellige Genauigkeit 15-stellige Genauigkeit 19-stellige Genauigkeit Abb. 2.1-4: 16-Bit-Datentypen, Größen und Bereiche Typ unsigned char char short int unsigned int int Größe (Bits) 8 8 16 32 32 unsigned long 32 enum 32 long 32 float 32 double 64 long double 80 Bereich 0 bis 255 -128 bis 127 -32768 bis 32768 0 bis 4294967295 -2147483648 bis 2147483647 0 bis 4294967295 -2147483648 bis 2147483647 -2147483648 bis 2147483647 34 . 10 38 bis 3.4 1038 17 . 10 308 bis 17 . 10308 3.4 10 4932 bis 11 . 104932 Anwendungen Kleine Zahlen ASCII-Zeichen Zähler Große Zahlen, Schleifenzähler Zähler, kleine Zahlen Astronomische Genauigkeit Geordnete Wertemengen sehr große Zahlen 7-stellige Genauigkeit 15-stellige Genauigkeit 19-stellige Genauigkeit Abb. 2.1-5: 32-Bit-Datentypen, Größen und Bereiche Die in einem C++-System zutreffenden Zahlenbereiche findet man in der Datei limits88. C++ bietet die Möglichkeit, den Zahlenbereich mit einer Funktion abzufragen, z.B.: #include <iostream> #include "c:\cppbuch\include\limits" 88 Im Header <limits> wird die Template-Klasse numeric_limits definiert. Sie hat Spezialisierungen für die ganzzahligen Grunddatentypen bool, char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long sowie für die Gleitkommazahltypen float, double und long_double. 110 Die Programmiersprache C++ using namespace std; int main() { cout << "Der Zahlenbereich fuer int geht von" << numeric_limits<int>::min() << " bis " << numeric_limits<int>::max() << endl; } Ganze Zahlen (int) verschiedener Größe werden beschrieben durch: char short int int long int // dient zur Darstellung einzelner Zeichen // int kann entfallen // int kann entfallen Die Typen char, short, int und long sind standardmäßig vorzeichenbehaftet, d.h.: Sie können postive und negative Zahlen darstellen. In C++ könnte man auch dafür signed int, signed short und signed long schreiben. Zur Definition positiver Bezeichner kann unsigned 89herangezogen werden, z.B.: unsigned int x; Insgesamt kann man 9 verschiedene "Integer"-Typen durch Kombination von "short", "long", "unsigned" mit "int" unterscheiden. + ++ -+ * / % = *= /= %= += -= < > <= >= == != << >> & ^ 89 Unäres Plus Unäres Minus Vorherige Inkrementierung um Eins Nachfolgende Inkrementierung um Eins Vorherige Dekrementierung um Eins Nachfolgende Dekrementierung um Eins Binäres Plus Binäres Minus Multiplikation Division Modulo (Rest mit Vorzeichen von i) Zuweisung i i i i i = = = = = i i i i i * / % + - 3 3 3 3 3 Kleiner als Größer als Kleiner gleich Größer gleich Gleich Ungleich Linksschieben Rechtsschieben Bitweises UND Bitweises XOR (Exklusives Oder) +i -i ++i i++ --i i-i + 2 i - 2 i * 2 i / 2 i % 13 i = 1 i *= 3 i /= 3 i %= 3 i += 3 i -= 3 i < j i > j i <= j i >= j i == j i != j i << 2 i >> 1 i & 7 i ^ 7 signed und unsigned sind sog. Modifizierer, da sie die Bedeutung des folgenden Begiffs ändern 111 Die Programmiersprache C++ | ~ <<= >>= &= ^= |= i | 7 ~i i <<= 3 i >>= 3 Bitweises ODER Bitweise Negation i = i << 3 i = i >> 3 i = i & 3 i = i ^ 3 i = i | 3 i &= 3 i ^= 3 i |= 3 Abb.: Operatoren für Ganzzahlen C++ unterschiedet zunächst einmal zwei "integer"-Typen: char und int. Zugehörige Werte sind ohne Umwandlung untereinander austauschbar. Zeichenliterale (z.B. '!') sind "integer"-Werte. Auch Wahrheitswerte werden in Ganzzahlen gefaßt. "0" entspricht dem logischen "false", alles andere ist "true". Da der Datentyp char intern als 1-Byte-Ganzzahl dargestellt wird, sind eigentlich alle Ganzzahl-Operatoren möglich. Bei der Interpretation von char als Zeichen sind nur folgende Operatoren sinnvoll: Operator = < > <= >= == != Bedeutung Zuweisung Kleiner als Groesser als Kleiner gleich Groesser gleich Gleich ungleich Beispiel x = ‘A‘ Der Datentyp size_t ist ein abgeleiteter Typ für Größenangeaben, die nicht negativ werden können. Der Typ entspricht entweder unsigned int oder unsigned long, je nach C++-System. Die Definition steht im Header <cstddef>. Ein weiterer integraler Datentyp ist der Aufzählungstyp (enumeration type). Die Syntax zur Deklaration ist enum [Typname] {Aufzaehlung} [Variablenliste]90; Bsp.: enum farbtyp {rot,gruen,blau,gelb}; enum wochentag {sonntag,montag,dienstag,mittwoch,donnerstag,freitag,samstag}; Falls der Datentyp bekannt ist, können Variable definiert werden, z.B.: farbtyp einheitlich; // Definition + Initialisierung wochentag feiertag, werktag, heute=dienstag; Wird ein Aufzählungstyp nur ein einziges Mal benötigt, kann der Typname weggelassen werden. Man erhält dann eine anonyme Typdefinition, z.B.: enum {fahrrad, mofa, lkw, pkw} fahrzeug; 90 Eckige Klammern bedeuten: Typname, Variablenliste können weggelassen werden. Sinnvoll ist meistens nur das Weglassen der Variablenliste 112 Die Programmiersprache C++ Den mit Hilfe eines Aufzählungstyps definierten Variablen können anschließend Werte aus der zugehörigen Liste zugewiesen werden. Aufzählungstypen sind eigene Datentypen, werden intern aber auf natürliche Zahlen abgebildet. Defaultmäßig erhält das erste Element in der Aufzählung den Wert 0 zugeordnet. Jedes folgende Element erhält einen Wert zugeordnet, der um eine Einheit größer ist als der Wert des unmittelbaren Vorgängers. Aufzählungen können mit einem Namen versehen werden. Jede benannte Aufzählung definiert einen Typ und kann als TypSpezifizierer zur Deklaration von Bezeichnern verwendet werden, z.B. 91 // Demonstration zu Aufzaehlungstypen #include <iostream.h> enum Sprachen {ASSEMBLER,COBOL,C,CPP,FORTRAN,LISP,PASCAL,PROLOG} sprache; int main() { bool allesistvergaenglich = true; sprache = CPP; // sprache ist vom Typ int cout << "Sprache = " << int (sprache) << endl; cout << "Alles ist vergaenglich = " << allesistvergaenglich << endl; } Umwandlungen von Enumerationen in int sind möglich, nicht möglich ist die Umwandlung einer int-Zahl in einen enum-Typ. Als Operation auf enum-Typen ist nur die Zuweisung erlaubt, bei allen anderen Operatoren wird vorher in int umgewandelt. Gleitpunktzahlen (Reelle Zahlen) verschiedener Größe werden beschrieben durch float, double und double long. Typ float double long double Bits 32 64 80 Zahlenbereich Stellen, Genauigkeit 7 15 19 Intern werden Mantisse und Exponent jeweils durch Binärzahlen einer bestimmten Bitbreite verkörpert. Bsp.: Repräsentation reeller Zahlen (Anzahl Bits) im Rechner Vorzeichen Mantisse Exp-Vorzeichen Exponent Summe float 1 23 1 7 32 double 1 52 1 10 64 long double 1 64 1 14 80 Der Zahlenbereich wird wesentlich durch die Anzahl Bits für den Exponenten bestimmt, der Einfluß der Mantisse ist minimal. Falls 32 Bits für die Darstellung einer reellen Zahl verwendet werden, existieren nur 4.294.967.296 verschiedene Möglichkeiten zur Bildung einer Zahl. Mit dem mathematischen Begriff eines reellen Zahlenkontinuums hat das nur näherungsweise zu tun. Folgen der nicht exakten Darstellung können sein: 91 PR21000.CPP 113 Die Programmiersprache C++ - - bei der Subtraktion zweier fast gleich großer Werte heben sich sie signifikanten Ziffern auf. Die Differenz wird somit ungenau. Dieser Effekt ist unter dem Namen numerische Auslöschung bekannt. Die Division zu kleiner Werte ergibt einen Überlauf (overflow). underflow tritt auf, wenn der Betrag des Ergebnisses zur Darstellung mit dem gegebenen Datentyp zu klein ist. Das Resultat wird dann gleich 0 gesetzt. Ergebnisse können von der Reihenfolge der Berechnung abhängen. Die Zahlenbereiche für reelle Zahlen sind im Header <limits> festgelegt. Auskunft über die Zahlenbereiche können über numeric_limits<float>- bzw. numeric_limits<double>-Funktionen erhalten werden: Operator + + * / = *= /= += -= < > >= <= == != Bedeutung Unäres Plus Unäres Minus Binäres Plus Binäres Minus Multiplikation Division Zuweisung d = d * 3 d = d / 3 d = d + 3 d = d - 3 Kleiner als Grösser als Grösser gleich Kleiner gleich Gleich ungleich Beispiel +a -a a + 2 d d d d *= /= += -= 3 3 3 3 a != b Abb.: Operatoren für float- und double-Zahlen Bsp.: Berechnung mathematischer Ausdrücke #include <iostream> #include <cmath> using namespace std; int main() { float x; cout << "x? "; cin cout << "fabs(x) = cout << "sqrt(x) = cout << "sin(x) = cout << "exp(x) = cout << "log(x) = } >> x; " << fabs(x) << endl; " << sqrt(x) << endl; " << sin(x) << endl; // x im Bogenmass " << exp(x) << endl; " << log(x) << endl; Für negatives x erhält man z.B.: x = 24 fabs(x) = 34 sqrt(x) = -NaN sin(x) = -0.529083 exp(x) = 1.7.1391e-15 log(x) = -Infinity 114 Die Programmiersprache C++ Einige mathematische Funktionen (z.B. sqrt(), exp()) sind vordefiniert. Die Deklarationen der meisten Funktionen befinden sich im Header <cmath>, die Funktion abs() für int-Zahlen ist jedoch im Header <cstdlib> definiert. Komplexe Zahlen bestehen aus dem Real- und Imaginärteil, die beide vom Typ float, double bzw. long double sein können. Sie werden im Header <complex> durch spezialisierte Templates realisiert. Mit komplexen Zahlen kann wie mit reellen Zahlen gerechnet werden, z.B.: #include <iostream> #include <complex> using namespace std; int main() { complex<float> c1; complex<float> c2(1.2, 3.4); cout << c2 << endl; c1 += c2; c1 = c2 * 0.5f; float re = c1.real(); cout << c1.imag() << endl; } // Komplexe Zahl 0.0 + 0.0i // (1.2 + 3.4i) // Standard-Ausgabeformat (1.2,3.4) // Realteil ermitteln // Imaginaerteil ausgeben Neben den arithmetischen Operatoren für reelle Zahlen kann auch auf Gleichheit (==) und Ungleichheit (!=) geprüft werden. Mit dem Operator sizeof92 kann die Größe eines Datentyps bestimmt werden, z.B.93: #include <iostream.h> int main() { cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << " Bytes" cout << "Groesse << " Bytes" cout << "Groesse << endl; cout << "Groesse << endl; cout << "Groesse << endl; } 92 93 von char .......... " << sizeof(char) << " Bytes" von short ......... " << sizeof(short) << " Bytes" von int ........... " << sizeof(int) << " Bytes" von long .......... " << sizeof(long) << " Bytes" von float ......... " << sizeof(float) << " Bytes" von double ........ " << sizeof(double) << " Bytes" von long double ... " << sizeof(long double) << " Bytes" von unsigned short. " << sizeof(unsigned short) << endl; von unsigned long.. " << sizeof(unsigned long) << endl; von char* ......... " << sizeof(char *) << " Bytes" von int* .......... " << sizeof(int *) << " Bytes" von double* ....... " << sizeof(double *) << " Bytes" Das Ergebnis von sizeof ist vom Typ unsigned int pr21001.CPP 115 Die Programmiersprache C++ Der logische Datentyp bool wurde 1993 in den Standard aufgenommen. Vorher wurde in C++ ein logischer Datentyp durch int simuliert. Dabei bedeutete ein Wert ungleich 0 wahr (true) und ein Wert gleich 0 falsch (false). Zur Wahrung der Kompatibilität wird der Datentyp bool an allen Stellen, die nicht ausdrücklich bool verlangen, in int umgewandelt. Die umgekehrte Wandlung von int nach bool ergibt false für 0 und true für alle anderen int-Werte. Die folgende Tabelle zeigt Operatoren für logische Dateitypen: Operator ! && || = Bedeutung Log. Negation Log. Und Log. Oder Zuweisung Bsp. !i a&&b a||b a = a && b; Im Header <limits> wird die Template-Klasse numeric_limits definiert. Sie umfaßt Spezialisierungen für Grunddatentypen: bool, char, signed char, unsigned char, short, int, unsigned int, long, unsigned long, float, double, long double. Für diese Grunddatentypen beschreiben Spezialisierungen verschieden implementationsabhängige Funktionen und Eigenschaften, die alle public sind. Schnittstelle bool is_specialised T min() T max() int radix int digits int digits10 bool is_signed bool is_integer bool_is exact T epsilon() T round_error() int min_exponent int min_exponent10 int max_exponent int max_exponent10 bool has_inifinity T infinity() T quiet_NaN() T signalling_NaN bool has_quiet_NaN bool has_signalling_NaN bool is_iec559 bool is bounded bool is_modulo 94 Bedeutung true nur für Grunddatentypen, für die eine Spezialisierung vorliegt, false für alle anderen. minimal möglicher Wert maximal möglicher Wert Zahlenbasis, normal 2 Ganzzahlen: Anzahl Bits (ohne Vorzeichen-Bit) Gleitkommazahlen: Anzahl der Bits in der Mantisse. Annahme: radix == 2 Anzahl signifikanter Dezimalziffern bei Gleitkommanzahlen (z.B 6 bei float, 10 bei double) true bei vorzeichenbehafteten Zahlen true bei Ganzzahlentypen true bei exakten Zahlen (z.B. ganzen Zahlen, rationale Zahlen) Kleinster positiver Wert, für den die Maschine die Differenz zwischen 1.0 und (1.0 + x) noch unterscheidet. Maximaler Rundungsfehler Kleinster negativer Exponent für Gleitpunktzahlen Kleinster negativer 10er-Exponent für Gleitkommazahlen Größtmöglicher Exponent für Gleitkommazahlen Größtmöglicher 10er-Exponent für Gleitkommazahlen (>= +37) true, falls der Zahltyp eine Repräsentation für „+Unendlich“ hat Repräsentation von „+Unendlich“, falls vorhanden Repräsentation einer „ruhigen NaN94“ Repräsentation einer signalisierenden NaN, falls vorhanden. true, falls der Zahltyp eine nicht signalisierende Repräsentation für NaN hat true, falls der Zahltyp eine signalisierende Repräsentation für NaN hat true, falls der Zahltyp dem IEC 559 (= IEEE 754)-Standard genügt true für alle Grunddatentypen, false wenn die Menge der darstellbaren Werte unbegrenzt ist, z.B. bei Typen mit beliebiger Genauigkeit true, falls bereichsüberschreitende Operationen wieder eine gültige Zahl ergeben. NaN: „not a number“ 116 Die Programmiersprache C++ round_style Art der Rundung Ganzzahlen: rounded_toward_zero (= 0) Gleitkommazahlen: round_to_nearest (= 1) Abb.: <limits> Attribute und Funktionen Mit typedef kann man einen anderen Namen für einen Typ vereinbaren, z.B.: typedef float real; int main() { real zahl = 1.7353; .... } // zahl ist vom Typ float typedef eignet sich besonders zur Vereinfachung komplexer Datentypen. Elementare Typen lassen sich beliebig mischen. Dabei werden Werte konvertiert. In arithmetischen Ausdrücken mit binären Operatoren wird folgende implizite Typkonvertierung vorgenommen: Der Operand mit dem niederwertigen Typ wird in den höherwertigen Typ konvertiert. Jeder Operand vom Typ char und short wird nach int und jeder Operand vom Typ float nach double konvertiert. Bei Anweisungen wird versucht den Wert der rechten Seite in die linke Seite zu konvertieren. Wird ein vorzeichenbehaftetes Objekt (z.B. eine Variable) einem integer-Objekt zugeordnet, erfolgt eine automatische Erweiterung des Vorzeichens. Objekte vom Typ signed char haben immer ein Vorzeichen. Objekte vom Typ unsigned char setzen das höherwetige Byte auf Null, wenn sie nach int konvertiert werden. Bei Konvertierung eines längeren „integer“-Typs in einen kürzeren werden die höherwertigen Bits abgeschnitten und die niederwertigen bleiben unverändert. Bei der Konvertierung eines kürzeren „integer“-Typs in einen längeren wird das Vorzeichen erweitert oder die zusätzlichen Bits des neuen Werts auf Null gesetzt. Das hängt davon ab, ob der kürzere Typ signed oder unsigned ist. Typ char unsigned char signed char short unsigned short enum Konvertierung in int int int int unsigned int int Verfahren Null- oder Vorzeichen erweitert Höherwetige Bits immer Null Vorzeichen erweitert unverändert, Vorzeichen erweitert unverändert, nullerweitert unverändert Abb.2.1-6: Verfahrensweise bei arithmetischen Standarkonvertierungen Eine explizite Typenkonvertierung (type cast)95 wird durch (typ) ausdruck 96veranlaßt. In C++ ist auch typ (ausdruck) möglich97, z.B.: int i = 5; double d; d = (double) i; bzw. d = double (i) 95 Die explizite Konvertierung bezeichnet man als cast Aus der Programmiersprache C übernommene Schreibweise 97 Funktionsschreibweise 96 117 Die Programmiersprache C++ Die Funktionsschreibweise kann nur für Typen verwendet werden, die einen Namen haben. Soll bspw. ein ganzzahliger Wert in eine Adresse konvertiert werden, dann muß die Konvertierung in "cast"-Schreibweise angegeben werden, z.B.: char *z = (char *) 0777; Die aus C übernommene Schreibweise ist auch für komplexe Datentypen möglich. Nachteile von "Casts" im C-Stil können mit Hilfe von neuen Typumwandlungsfunktionen vermieden werden. Sie arbeiten nach folgendem Schema: operatorname<T>(ausdruck) Das Ergebnis des Ausdrucks soll in den Typ T umgewandelt werden. Der static_cast-Operator ist zur Durchführung (bzw. zum Rückgängig-Machen) von Standardtypumwandlungen vorgesehen. 1. Bsp.: Umwandlung vom Enumerationstyp in einen ganzzahligen Zahlenwert enum wochentag {sonntag, montag, dienstag, mittwoch} heute = dienstag; int i = dienstag; // implizite Typumwandlung nach int heute = i; // Fehler, Datentyp inkompatibel heute = static_cast <wochentag>(i); // erlaubt! 2. Bsp.: Umwandlungen von Zeichen in ganze Zahlen Die Konvertierung geschieht über den static_cast-Operator (mit Angabe des gewünschten Datentyps in spitzen und Angabe der Variable in runden Klammern). Man kann die Umwandlung so beschreiben: char zch; int i; i = static_cast<int>(zch); // Umwandlung char in int Es gelten die Identitäten zch == static_cast<char>(static_cast<int>(zch)); i == static_cast<int>(static_cast<char>(i)); , falls –128 <= zch <= 127 ist (bzw. 0 <= i <= 255 bei unsigned char). Liegt i außerhalb dieses Bereichs, gibt es einen Datenverlust, weil die überflüssigen Bits bei der Umwandlung nicht berücksichtigt werden können. Der dynamic_cast-Operator dynamic_cast<T>(a) wirkt ähnlich, zeigt aber folgende Unterschiede: - Die Typüberprüfung findet zur Laufzeit statt, falls das Ergebnis nicht schon zur Compilier-Zeit bestimmt werden kann. Der Typ T muß ein Zeiger oder eine Referenz auf eine Klasse sein. Falls das Argument a ein Zeiger ist, der nicht auf ein Objekt vom Typ T (oder abgeleitet von T) zeigt, wird das Ergenis NULL zurückgegeben. Falls das Argument a eine Referenz ist, die nicht auf T (oder abgeleitet von T) verweist, wird eine Ausnahme oder Exception vom Typ bad_cast ausgeworfen. Ein reinterpret_cast reinterpret_cast<T>(a) kann einen Zeigertyp in einen anderen, Zahlen in Zeiger und umgekehrt Zeiger in Zahlen umwandeln. Es gibt nur einen sicheren Verwendungszweck für das Ergebnis eines reinterpret_cast: die Typumwandlung zurück in den ursprünglichen Typ. 118 Die Programmiersprache C++ Mit dem const_cast-Operator const_cast<T>(a) können die Attribute const, volatile und __unaligned einer Klasse entfernt werden. Bsp.: const int i = 100; const int *ip = &i; *ip = 0; // geht nicht int *iq = const_cast<int *>(&i); // explizite Typumwandlung *iq = 0; Initialisierer setzen den Anfangswert, der in einem Objekt (Variable, Array, Struktur) gespeichert wird. Ist das Objekt von automatischer Lebensdauer, dann ist sein Wert unbestimmt. Ein nicht initialisiertes Objekt statischer Lebensdauer wird durch die Voreinstellung folgendermaßen initialisiert: - Es wird auf Null gesetzt, falls es zu einem arithmetischen Typ gehört Es wird auf Null gesetzt, falls es ein Zeigertyp ist. 119 Die Programmiersprache C++ 2.2 Abgeleitete Datentypen Sie entstehen durch Anwendung bestimmter Konstruktionsvorschriften auf bekannte (fundamentale oder abgeleitete) Datentypen. Es entstehen dadurch Konstante, Zeiger, Referenzen, Felder (arrays), Strukturen, Variantenstrukturen, Funktionen und Klassen. 2.2.1 Konstanten C++ unterscheidet: a) Integer-Konstanten Die kommen in 4 verschiedenen Formen vor: dezimal, oktal (Kennzeichen: vorangestellte 0), hexadezimal (Kennzeichen durch 0x bzw. 0X am Anfang) und als "character"-Konstanten (z.B. char z = 10) Steht am Ende einer Konstante ein Buchstabe L (bzw. l), so handelt es sich um eine Zahl vom Typ long, bei U (bzw. u) ist die Zahl vom Typ unsigned, z.B. 1311u 1311l // unsigned // long b) Zeichen-Konstanten Eine "character"-Konstante ist in Hochkommata eingeschlossen, z.B. 'a', 'b', '1', '\n'98. Eine besondere Art von Zeichenkonstanten sind Escape-Sequenzen zur Darstellung von nicht abdruckbaren Zeichen. Sie werden durch „\“ eingeleitet, dem ein Buchstabe, ein Oktal- oder Hexadezimal-Wert folgt. Intern werden sie als Zeichen vom Typ char gespeichert. ‘\n’ ‘\r’ ‘\t’ ‘\v’ ‘\b’ ‘\f’ ‘\\’ ‘\’’ ‘\“’ ‘\a’ Neue Zeile Wagenrücklauf (Carriage Return) Horizontaler Tabulator Vertikaler Tabulator Backspace Neue Seite (Formfeed) Backslash Apostroph Anführungszeichen Alarm Zusätzlich gibt es „lange“ Zeichen (wide characters) vom Typ wchar_t. „Wide Characters“ sind für Zeichensätze gedacht, bei denen ein Byte nicht zur Darstellung eines Zeichens ausreicht. Neben diesen „Ein-Zeichen-Konstanten“ sind auch Konstanten mit 2 Zeichen möglich (‘ab’,’XY’). Diese stellen „integer“-Werte dar. Man sollte auf sie jedoch verzichten und in diesen Fällen eine hexadezimale Schreibweise bevorzugen. 98 120 Die Programmiersprache C++ c) Reelle Konstanten (Gleitpunkt-) Sie sind vom Typ double und können sowohl in üblicher Gleitpunktdarstellung als auch in exponentieller Form angegeben werden, z.B.: Darstellung 13. -.4 -13.0e-1 -04.E+2 13E-2 1.311E1 Wert 13,0 -0,4 -1,3 -400,0 0,13 13,11 Auch Gleitpunktzahlen können durch Anhängen eines Suffix zu einem bestimmten Typ gemacht werden. So bedeutet f (oder F) float und l (oder L) long double. Bsp.: Demonstration von Länge und Typ einzelner Gleitpunktkonstanten #include <iostream.h> void main() { cout << "\n" << sizeof(13.); cout << "\n" << sizeof(13.f); cout << "\n" << sizeof(13.l); } Zum Beschreiben einer Gleitpunktzahl ist der Dezimalpunkt unbedingt erforderlich, z.B. 1311. anstatt 1311 (wird als „int“ betrachtet). d) Zeichenketten (Strings) Eine Zeichenkette ist eine Zeichenfolge, die durch doppelte Hochkommata eingeschlossen ist, z.B.: "Hallo Semester I5T" Jede Zeichenkette enthält zusätzlich das Zeichen '\0', das das Ende der Zeichenkette bestimmt. Außerdem kann jedes C++-Zeichenkettenliteral Kontrollzeichen im Text enthalten, die jeweils durch einen Backslash (\) eingeleitet sind. Die Länge einer Zeichenkette kann nach ANSI-Norm bis zu 509 Zeichen betragen. Die meisten Compiler erlauben jedoch größere Werte (z.B. 2048 Bytes). e) const An diesem Schlüsselwort erkennt C++ Konstanten, z.B. const const const const int pufferGroesse = 512; int zaehler = 50; double pi; // Fehler, nicht initialisiert float pi = 3.14159; So definierte symbolische Konstante sind Variable, die nicht verändert werden dürfen. "zaehler = 50;" führt bspw. auf einen Fehler, da "zaehler" eine Konstante ist. 121 Die Programmiersprache C++ Bsp.: "Demonstation symbolischer Konstanten99 #include <iostream.h> // Zeichenkonstante const char zeichen = '&'; // Stringkonstante const char zeichenkette[] = "Hallo Informatik! "; // Integerkonstanten koennen dezimale, oktale oder hexadezimale Zahlen sein const int oktal = 0233; const int hexaD = 0x9b; const int dezimal = 155; // Gleitpunktkonstante const float gleitpunkt = 3.1459; void main() { cout << zeichen cout << zeichenkette cout << oktal cout << hexaD cout << dezimal cout << gleitpunkt } << << << << << << endl; endl; endl; endl; endl; endl; Einer mit const deklarierten Konstanten ordnet der Compiler die Speicherklasse static zu. Das Gegenstück zum Spezifizierer const ist volatile. Über dieses Schlüsselwort soll dem Compiler mitgeteilt werden, daß das Objekt von außen (z.B. über eine InterruptRoutine) verändert werden kann. Der Programmcode, der das über volatile spezifizierte Objekt enthält, sollte deshalb nicht vom Compiler optimiert werden. Die Deklaration eines Objekts als volatile warnt den Compiler davor, Annahmen über den Wert des betreffenden Objekts anzustellen, weill sich dieser (theoretisch) ständig ändern kann. Mit volatile deklarierte Variablen werden nicht als Registervariable behandelt, z.B.100: #include <iostream.h> void benutzeregister(void); void benutzevolatile(void); main() { benutzeregister(); cout << endl; benutzevolatile(); } void benutzeregister(void) { register int k; cout << "Zaehlen mit Registervariablen " << endl; for (k = 1; k <= 100; k++) cout << k; } void benutzevolatile(void) { volatile int k; cout << "Zaehlen mit einer Volatilevariablen " << endl; for (k = 1; k <= 100; k++) cout << k; 99 PR22101.CPP PR22104.CPP 100 122 Die Programmiersprache C++ } 2.2.2 Zeiger (pointer types) Definition Zeigertypen sind Datentypen zur Manipulation von Objektadressen. Ein Zeigertyp entsteht, indem vor dem deklarierten Namen ein * (Stern)101 angegeben wird, z.B.: int* zgr; // zgr ist ein Zeiger auf ein int-Objekt Für viele Typen T ist T* "pointer" auf T. Eine Variable vom Typ T* kann eine Adresse vom Typ T enthalten So teilt int *zgr bzw. int* zgr dem Compiler mit, daß die Variable zgr "Zeiger" aufnehmen kann. Zeiger sind typgebunden. Zur Zuordnung der Adresse eines Objekts kann der Adreßoperator & benutzt werden, z.B.: int* zgr; int i = 3; zgr = &i; 3 zgr i Abb. 2.2-1: Ein Zeiger auf das ganzzahlige Objekt i "zgr" hat als Wert die Adresse der Variablen i zugewiesen bekommen. Der Inhalt der Variablen i kann über zgr durch Zeigerderefenzierung102 angesprochen werden. *zgr = *zgr +1; // aequivalent zu i = i + 1; Das Zeichen * hat im Zusammenhang mit Zeigern 2 Bedeutungen: 1. Bei der Definition kennzeichnet es eine Variable als Zeigervariable 2. Innerhalb des Programmablaufs (in Anweisungen) zeigt es an, daß der Inhalt der Speicherstelle gemeint ist, auf die der Zeiger verweist (Dereferenzierung). 101 Der Stern vor dem Namen gehört aber nicht zum Namen sondern zum Typ Inhalts- bzw. indirection-Operator. Welche Bedeutung „*“ hat, ermittelt C++ aus dem Zusammenhang (Kontext), in dem „*“ auftritt 102 123 Die Programmiersprache C++ Referenz "T& name" bedeutet: Das so definierte Objekt ist eine Referenz auf das Objekt vom Typ T. Der Operator & liefert eine Adresse eines Objekts (Referenz). Mit dem Operator * erhält man wieder den Wert des Objekts (dereferenzieren). "*" wird aber auch bei der Deklaration bzw. Definition von Zeigervariablen benutzt. Referenztypen sind eng mit Zeigertypen verwandt. Bsp.: 1. Darstellung von Zeigern, Referenzen, Dereferenzierung103 #include <iostream.h> void main() { int i = 1024; int *zgri = &i; cout << "i: " << i << "\t\t&i:\t" << &i << endl; cout << "*zgri: " << *zgri << "\tzgri:\t" << zgri << endl << "\t\t&zgri:\t" << &zgri << endl; } Die Ausgabe dieses Programms zeigt: i: 1024 *zgri: 1024 &i: 0x2e972240 zgri: 0x2e972240 &zgri: 0x2e97223c 2. Veranschaulichung von Zeigerstrukturen int i = 2, j = 3; int *zi, **zzi; zi = &i 2 zi i 3 zzi j *zi = 3 3 zi i zzi = &zi; 3 zzi 103 j PR22304.CPP 124 Die Programmiersprache C++ 4 zi i 3 zzi j zi i *zzi = &j zzi j Abb. 2.2-2: Veranschaulichung von Zeigerstrukturen 3. Ein Demonstrationsprogramm , das das Kreieren, Initialisieren und Dereferenzieren einer "Pointer"-Variablen zeigt. #include <iostream.h> char z; int main() { char *zz; // Zeiger zu einer Zeichenvariablen zz = &z; for (z = 'A'; z <= 'Z'; z++) cout << *zz; } Null-Pointer Numerische Zeigerkonstanten sind unüblich. Eine Ausnahme davon ist der Nullzeiger (üblicherweise ist das eine Codierung für "Adresse noch undefiniert"). In vielen Programmen wird dafür eine Präprozessorkonstante NULL (definiert im Header <cstddef>) benutzt. Allerdings ordnet auch die Zuweisung der Konstanten 0 einem Zeiger den Null-Zeigerwert zu. In C++ wird NULL als Zahlenwert 0 oder 0L dargestellt, so daß in einem C++-Programm der Name NULL nicht notwendig ist. Zeigerarithmetik Für Zeigertypen ist eine besondere (Zeiger-)Arithmetik definiert. Zu einem Adreßausdruck darf ein integraler Ausdruck (vom Typ char, short, int, long, ...) addiert bzw. subtrahiert werden. Arithmetische Operationen berücksichtigen automatisch die Größe des Typs. Angenommen wird, daß "x" und "y" Zeigerausdrücke sind, "i" ein ganzzahliger Ausdruck ist und "var" eine Variable vom Typ T ist. Adresse & zgr = &var weist die Adressse von "var" an "zgr" Zuweisung 125 Die Programmiersprache C++ = zgr = x Dereferenzieren * var = *zgr weist den Zeigerwert "x" der Variablen "var" zu. weist das Datenelement vom Typ T, das durch "zgr" refernziert wird, der Variablen "var" zu Dynamische Speicherbelegung und Freigabe new zgr = new T erzeugt dynamisch Speicher für ein Datenelement vom Typ T und weist die Adresse dieses Speichers "zgr" zu delete delete zgr löscht den dynamischen Speicherbereich der durch die Adresse "zgr" lokalisiert wird. Arithmetik + x + i zeigt auf das Datenelement, das "i" Datenelemente rechts vom durch "x" adressierten Element liegt. x - i zeigt auf das Datenelement, das "i" Datenelemente links vom durch "x" adressierten Element liegt. x - y gibt die Anzahl der Datenelemente vom Basistyp zurück, die zwischen den beiden Zeigern liegen Vergleichen (relationale Operationen) Die 6 standardmäßig vorliegenden relationalen Operatoren können auf Zeiger angewendet werden. Vergleiche beziehen sich auf ganzzahlige Werte ohne Vorzeichen ("unsigned int") Abb. 2.2-10: Operationen zu Zeigerausdrücken Zeigerkonvertierungen Zeigertypen können im Rahmen der Typumwandlung in andere Zeiger konvertiert werden, z.B.: char* zeichenkette; int* iz; zeichenkette = (char*) iz; Allgemein kann man das so ausdrücken: (T*) konvertiert einen Zeiger in den Typ Zeiger auf T. Dynamische Speicherbelegung und Freigabe Da Zeiger selbst Objekte sind, können Zeiger auch auf Zeiger zeigen. Felder (arrays), Strukturen und Varianten, Konstante und Klassen sind Objekte, auf die gewöhnlich gezeigt wird. Speicherplatz für derartige Objekte mit dynamischer Dauer kann man mit dem C++-Operator new besorgen und mit delete wieder freigeben. Kann kein Speicherplatz bereitgestellt werden, dann wird NULL zurückgeliefert. new new_Argumente Typname Initialisierer new new_Argumente (Typname) Initialisierer new_Argumente läßt sich zur Übergabe weiterer Argumente an new verwenden. Dazu ist allerdings eine überladene Version von new erforderlich, die mit den optionalen Argumenten übereinstimmt. Initialisierer wird (falls angegeben) zur Initialisierung der Reservierung von Speicherplatz verwendet. new versucht ein Objekt des Typs Typname durch Reservierung von sizeof(Type) Bytes im freien 126 Die Programmiersprache C++ Speicher (dem Heap) zu erstellen. Der Operator new[] dient zur Erzeugung von CArrays. Der zurückgegebenen Zeiger hat immer den Typ „Zeiger auf Type“. Das neue Objekt wird von seiner Erzeugung bis zu dem Moment gespeichert, in dem der Operator delete es löscht. delete Typumwandlungsausdruck delete [] Typumwandlungsausdruck Bsp.: char* z = new char[100]; // Platz für 100 Zeichen int* i = new int; // Platz für eine Zahl /* Die Anforderung von Speicherplatz erfolgt durch Angabe des Objekttyps, das System liefert daraufhin die Startadresse des Speicherplatzes zurück */ int* dynfeld; int groesse; /* groesse wird belegt */ ......... dynfeld = new int[groesse]; /* "new int[groesse]" liefert einen Zeiger auf einen dem Heap entnommenen Speicherblock der Groesse "groesse * sizeof(int)" */ Mit delete wird Speicherplatz freigegeben, z.B. delete[100] z; delete i; delete [] dynfeld; /* Die Freigabe von Speicherplatz geschieht durch Angabe der Speicherplatz-adresse und evtl. der Anzahl der Objekte, die gelöscht werden sollen */ void-Pointer Mit dem speziellen Typ void* werden Zeiger definiert, die auf Objekte eines zur Compilierungszeit unbekannten Typs verweisen. C++ muß dann zur Ablaufzeit wissen, welchen Datentyp ein void-Zeiger adressiert. Das erfordert einen "pointer type cast"-Ausdruck, der temporär einen Zeiger zu einem Datentyp bindet. Bsp104.: #include <iostream.h> void ausgabeZeiger(void *zgr); int main() { char puffer[100]; void *zpuffer; // zpuffer = (void *) &puffer[0]; zpuffer = &puffer; * (char *) zpuffer = 'A'; // Einspeichern eines Zeichens ueber den Puffer puffer[1] = 'B'; // direktes Einspeichern puffer[2] = 'C'; puffer[3] = '\0'; cout << "Adresse von Puffer = "; ausgabeZeiger(&puffer); 104 vgl. PR22202.CPP 127 Die Programmiersprache C++ cout << "Daten im Puffer = "; cout << (char *) zpuffer; } void ausgabeZeiger(void *zgr) { cout << hex << zgr << endl; } Zeiger auf Konstante bzw. konstante Zeiger const-Objekte können über Zeiger nicht indirekt verändert werden, z.B.: const double pi = 3.14; double* zgr = &pi: /* ist verboten, da &pi den typ "Zeiger auf eine Konstante besitzt */ Der Fehler kann behoben werden, z.B.: const double* zgr = &pi; Allerdings ist dann *zgr = 3.2 verboten. Ein Zeiger auf eine Konstante darf, falls er dereferenziert wird, nicht auf der linken Seite einer Zuweisung stehen. Ein Zeiger auf eine Konstante, darf jedoch verändert werden, z.B. const double pi = 3.141; const double e = 2.718; const double* zgr = &pi; zgr = &e; cout << *zgr; // gibt 2.718 zurueck Zeiger können selbst als Konstante definiert werden, z.B.: double e = 2.718, pi = 3.141; double* const zgr = &e; // Initialisierung e = pi; cout << *zgr; // gibt 3.141 aus zgr = &pi; // verboten: zgr ist konstant In char* const kz = "sepp"; kz[0] = 'd'; kz = "depp"; // konstanter Zeiger // zulaessig // Fehler ist "kz" eine Konstante. In const char* zk = "sepp"; zk[0] = 'd'; zk = "depp"; // Zeiger auf eine Konstante // Fehler // zulaessig ist "zk" keine Konstante sondern ein Verweis auf eine Konstante. Das Objekt, auf das "zk" verweist, kann nicht verändert werden, aber "zk" darf verändert werden. Soll ein konstanter Zeiger auf ein konstantes Objekt definiert werden, gelten folgende Definitionen: const char *const kzk = "sepp"; // Auch erlaubt: const char* const kzk = "sepp"; kzk[0] = 'd'; // Fehler kzk = "depp"; // Fehler 128 Die Programmiersprache C++ Zeiger auf Funktionen Ist die Adresse einer Funktion bestimmt, dann kannn über diesen "Pointer" die Funktion aufgerufen werden. Der Typ „ist Zeiger auf eine Funktion“ besitzt folgende syntaktische Form: rückgabewertTyp (*funk) (argumentenTyp1, argumentenTyp2, ...) Rechnungen mit Zeigern auf Funktionen sind nicht zulässig. Bsp.: Sortieren der Elemente in Arbeitsspeicherfeldern 1. Zum Sortieren muß eine passende Vergleichsfunktion gefunden werden. Die Zuordnung der passenden Vergleichsfunktion erfolgt zweckmäßig über eine Funktions-Pointer. Dafür wird ein Typname (mit Argumentenliste) deklariert (zur Erleichterung der Beschreibung der etwas ungewöhnlichen Syntax). typedef int (*VRGL) (int,int); void tauschen(int* x, int i, int j) { int zwischen = x[i]; x[i] = x[j]; x[j] = zwischen; } int vrgl1(int z1, int z2) { return z1 - z2; } int vrgl2(int z1, int z2) { return z2 - z1; } void bsort(int* x, int n, VRGL vrgl) // void bsort(int*x, int n, int (*)(int,int) vrgl) { // Sortieren durch Austauschen for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) if ((*vrgl)(x[i],x[j]) < 0) tauschen(x,i,j); } 2. Zum Lieferumfang zahlreicher C++-Compiler gehört eine Bibliotheksfunktion void qsort(void* zgrVornF, size_t anz, size_t bytes, int (*fcompare)(const void* elem1, const void* elem2); zgrVornF: Zeiger auf das erste Element anz: Das Feld besteht aus anz Elementen bytes: Größe der Elemente Wie sortiert wird, bestimmt die Funktion fcompare(). Sie erwartet zwei Zeiger auf ein beliebiges Element als Argument und muß zurückgeben: 129 Die Programmiersprache C++ 0, falls *elem1 == *elem2 < 0, falls *elem1 < *elem2 > 0, falls *elem1 > *elem2 Der Typ size_t ist ein typedef für unsigned int. qsort() wird mit der Funktionsprozedur sortieren() aufgerufen, die ein Zeichenkettenfeld (char* [ ]) zum Sortieren der Zeichenkettenelemente übergibt. #include <stdlib.h> #include <string.h> void sortieren(char* x[], int n) { int vergleiche(const char*[], const char*[]); qsort((void*) x, (size_t) n, (size_t) sizeof(x[0]), (int (*) (const void*, const void*)) vergleiche); /* explizite Typumwandlung. In qsort() sind Zeiger auf void verlangt */ } int vergleiche(const char*elem1[], const char* elem2[]) { return(strcmp(*elem1,*elem2)); } 2.2.3 Vektoren (Arrays) 2.2.3.1 C-Arrays Definition und Initialisieren Vektoren bestehen aus einem zusammenhängenden Feld von einzelnen Variablen gleichen Namens und gleichen Typs, die über einen ganzzahligen Index angesprochen werden können. Bspw. ist int feld[10] eine Vektordefintion. Die von [ .. ] eingeschlossene Ganzzahl bestimmt die Anzahl der Vektorelemente. Hier sind 10 Variable feld[0], feld[1], ... , feld[9] vom Typ int verfügbar. Die Elemente werden vom Anfangsindex 0 aus durchgehend indiziert. Vektorelemente werden dadurch ausgewählt, daß dem Vektornamen, in [..] eingeschlossen, ein Ausdruck folgt, der eine ganze Zahl liefert. Vektorelemente können vordefiniert werden, z.B.: int feld[10] = {1,2,3,4,5,6,7,8,9,10}; Bei reinen Deklarationen kann die Ausdehnungsangabe entfallen, z.B.: extern double v[]; Dies gilt auch für den Fall der Definition mit Initialisierung durch Angabe einer Werteliste, z.B.: double x[3] = {1,0,0}; double y[3] = {0,1,0}; double z[] = {0,0,1}; 130 Die Programmiersprache C++ Parameterübergabe Vektoren (arrays) werden in C++ immer mit call by reference übergeben, z.B. void ausgabe(int[]) wird vom Compiler so behandelt: void ausgabe(int *) Die Größe des Felds spielt bei der Deklaration der formalen Parameter keine Rolle. Die folgenden drei Deklarationen sind äquivalent: void ausgabe(int *) void ausgabe(int []) void ausgabe(int[10]) C-Arrays und sizeof C_Arrays sind keine Objekte (Instanzen) und besitzen auch keine Methoden. Die Größe eines C-Array muß vorher bekannt sein oder mit sizeof ermittelt ermittelt werden, z.B.: const int anzahl = 5; int tablelle[10]; for (int i = 0; i < anzahl; i++) cout << i << ": " << tabelle[i] << endl; // bzw. for (int i = 0; i < sizeof(tabelle)/sizeof(tabelle[0]); i++) cout << i << ": " << tabelle[i] << endl; sizeof() gibt den Platzbedarf des in Klammern stehenden Objekts in Bytes zurück. Die Anzahl ergibt sich durch Division des Speicherbedarfs für das ganze Feld durch den Platzbedarf für ein einzelnes Element. Falls der Datentyp eindeutig bekannt ist, kann anstatt sizeof(tabelle[0]) auch sizeof(int) geschrieben werden. C-Zeichenketten Eine C-Zeichenkette (string) ist ein Spezialfall eines Arrays. C++ Zeichenketten kann man so char s[] = {'s','t','r','i','n','g','\0'} oder einfacher so char s[] = "string". definieren. Der Übersetzer berechnet die Wortlänge anhand der Zahl der Initialisierungen. Bei s wird sie hier auf 7 festgelegt, da ein "string" immer mit dem "\0"-Zeichen endet. Derartige Zeichenketten sind die einzige Form von C-Arrays, die auf einmal ausgegeben werden können, z.B.: cout << s; 131 Die Programmiersprache C++ Ebenso ist es möglich, eine Zeichenkette über >> und cin auf einmal zu füllen: cin >> s; Mit der Benutzung der binären Null als Endemarkierung kann man theoretisch beliebig lange Zeichenketten erzeugen. Mit s[0] = '\0'; ist die Länge der Zeichenkette auf Null gekürzt, also gelöscht. Zur Bearbeitung von Zeichenketten befinden sich in der Header-Datei105 string.h drei Funktionen strcpy (kopiert eine Zeichenkette, hängt automatisch binäre Null ans Ende der Kopie), strlen ( ermittelt die Länge einer Zeichenkette) und strcat (hängt eine Zeichenkette an eine andere an). Bei der Arbeit mit Zeichenketten ist, genau wie bei anderen Vektoren (arrays) darauf zu achten, daß das aufzunehmende Feld groß genug ist. C++ überprüft die Grenzen eines Felds nicht und schreibt daher über diesen Bereich hinaus. Funktionen zur Manipulation von Zeichenketten Sie sind aus der C-Standardbibliothek übernommen und in der Header-Datei string.h deklariert. Diese Routinen lassen sich in 3 Gruppen unterteilen. Die 1. Gruppe behandelt nullterminierte Zeichenketten. Die Namen dieser Funktionen beginnen mit str. Die 2. Gruppe sind die strn-Funktionen. Die beenden ihre Arbeit, sobald das erste Nullzeichen auftritt, bzw. nach spätestens n (zusätzliches Argument) Zeichen. Die Funktionen der 3. Gruppe (mem-Funktionen) operieren auf Bytefeldern bekannter Länge und beachten das Nullzeichen nicht. -cpy()-Funktionen Sie kopieren den Inhalt von s auf den Inhalt von t. „strcpy()“ und „strncpy()“ bis jeweils zum ersten Nullbyte von s (einschließlich), strncpy() jedoch maximal n Zeichen. „memcpy()“kopiert genau n Bytes. char* strcpy(char* t, const char* s); char* strncpy(char* t, const char* s, size_t n); void* memcpy(void* t, const void* s, size_t n); -cat()-Funktionen Sie ermitteln das Ende der Zeichenkette t und fügen dort die Zeichenkette s an. Sie geben t zurück. char* strcat(char* t, const char* s); char* strncat(char* t, const char* s, size_t n) -cmp()-Funktionen Sie vergleichen die beiden ersten Argumente. Sie liefern -1 zurück, falls a < b106 gilt, 0 für a = b und +1 für a > b. 105 106 Header <cstring> Die Relationen < bzw > bezeichnen hier die lexikalische Reihenfolge 132 Die Programmiersprache C++ int strcmp(const char* a, const char* b); int strncmp(const char* a, const char* b, size_t n); int memcmp(const void* a, const void* b, size_t n); strchr() und memchr() geben einen Zeiger auf das erste, strrchr() einen Zeiger auf das letzte Auftreten des Zeichen z zurück. Wird z in s nicht gefunden, wird 0 zurückgegeben. char* strchr(const char* s, int z); void* memchr(const void* s, int z, size_t n); char* strrchr(const char* s, int z); strpbrk() liefert einen Zeiger auf das erste Auftreten irgendeines Zeichen aus der Zeichenkette p in der Zeichenkette s. Bei Mißerfolg wird 0 zurückgegeben. char* strstr(const char* s, const char* p); strstr() liefert einen Zeiger auf das erste Auftreten der Zeichenkette p als Teilstring der Zeichenkette s. Enthält s den String p nicht, so ist das Ergebnis 0. char* strstr(const char* s, const char* p); char-Arrays Genau wie bei C-Arrays wird für char-Arrays Speicherplatz zur Kompilierungszeit reserviert. „char“-Arrays können auch keine L-Werte sein. Wie bei Strings (char *) nimmt der Ausgabeoperator << an, daß ein char-Array mit ‘\0‘ abschließt. Die Initialisierung kann wie bei Strings geschehen oder wie bei C-Arrays vorgenommen werden. Zeiger und Feldnamen Feldnamen sind konstante Zeiger auf das 1. Feldelement, z.B.: &s[0]. Eine Dereferenzierung ermöglicht den Zugriff. So kann *s *(s+1) geschrieben werden anstatt s[0] geschrieben werden anstatt s[1]. Bsp.: Gegeben ist ein Vektor, der Ganzzahlen aufnehmen kann. int x[5]; Der Zeiger int *zx; zeigt nach der Zuweisung zx = &x[0]; auf das 1. Element des Vektors. "zx + 1" zeigt dann auf x[1]. Das Inkrementieren eines Zeigers um Eins bewirkt, daß der Zeiger auf das nächste Objekt dieses Typs zeigt. Statt x[1] kann man auch *(zx + 1) schreiben. Da in C++ der Name eines Vektors gleichbedeutend mit einem Zeiger auf seinen Anfang ist, läßt sich zx = &x[0]; auch einfach durch zx = x;ersetzen. Allerdings besteht ein Unterschied: "zx" ist eine Zeigervariable, x eine Zeigerkonstante. So ist "zx + 1", "zx + 3" erlaubt. Dies ist aber nur für "x" nicht erlaubt, "(*x)++" ist zur Inkrementierung von x[0] um 1 erlaubt. 133 Die Programmiersprache C++ x[0] zx x[1] x[2] zx+2 x [3] x[4] zx+4 Abb. 2.2-3: Zeigerarithmetik Elementweises Kopieren von Vektoren Felder (C-Arrays) können nur elementweise in C++ kopiert werden. So ist char t[7]; t = s; falsch. Der Compiler kennt diesen Fehler jedoch nicht, denn er interpretiert diese Anweisung als „Kopieren einer Zeigervariablen“. Da es keine Zuweisungen eines Felds an ein anderes gibt, kann auch nicht über die return()-Anweisung ein Feld als Funktionsrückgabewert erscheinen. Mehrdimensionale Felder Sie werden folgendermaßen definiert: int matrix[10][10] Diese Definition erzeugt die Elemente matrix[i][j] für 0 <= i , j <= 9. Die Grenzen von Feldern sind konstant und werden zur Übersetzungszeit festgelegt. Bsp.: char v[2][5] = {'a','b','c','d','e', '1','2','3','4','5'} // erster Vektor // zweiter Vektor kann auch so angegeben werden char v[2][5] = {{'a','b','c','d','e'}, {'1','2','3','4','5'}} // erster Vektor // zweiter Vektor Der Zugriff auf die Komponenten erfolgt über Angabe der entsprechenden Indizes, z.B. v[0][0] v[1][2] // verweist auf den Wert 'a' // verweist auf den Wert '3' 134 Die Programmiersprache C++ Feld[0] Feld[0][0] Feld[0][1] Feld[1] Feld[1][0] Feld[1][1] Feld[1][2] Feld[2][0] Feld[2][1] Feld[2][2] Feld[2] Feld[0...2][0] Feld[0][2] Feld[0...2][1] Feld[0...2][2] Abb. 2.2-4: Ein zweidimensionales Feld Elemente eines zweidimensionalen Felds werden zeilenweise im Speicher abgelegt: Feld[0][0] Feld[0][1] Feld[0][2] Feld[1][0] ..... Allgemein kann festgehalten werden: Der letzte Index läuft immer am schnellsten. Auch mehrdimensionale Felder können direkt bei der Deklaration Werte zugewiesen bekommen. Mehrdimensionale Vektoren lassen sich dadurch simulieren, daß anstelle von Vektoren auf Vektoren ersatzweise "Vektoren von Zeigern auf Vektoren" definiert werden, z.B.: char* z[3] = {"1.Zeile","2.Zeile","3.Zeile"}; [0] 1 . Ze i l e \0 [1] 2 . Ze i l e \0 [2] 3 . Ze i l e \0 Abb. 2.2-5: Vektor mit Zeigern auf Zeichenketten Bsp.: „Sortieren durch Austauschen“ von Zeichenketten - Aufbau eines mehrdimensionalen Vektors zur Verwalltung von Zeichenketten. 135 Die Programmiersprache C++ Die Zeichenketten werden in einem Wortspeicher gebracht, der folgendermaßen definiert ist: const int groesseSp = 1000; char wortSp[groesseSp]; Über einen Vektor, dessen Komponenten Zeiger auf Zeichenketten enthalten, wird jeweils das erste Zeichen der im Wortspeicher befindlichen Zeichenketten adressiert: wortSp ~ u w e \0 h a n s \0 j u e r g e n \0 p e t e r \0 ~ zgrZk [0] [1] [2] [3] ~ ~ Abb. 2.2-6: Datenstruktur zum Sortieren von Zeichenketten Den Ausbau dieser Datenstruktur übernimmt die Funktionsprozedur „worteLesen()“. Diese Routine wird vom Hauptprogramm aufgerufen, das zusätzlich noch die Aufrufe für die Ausgabe der Feldkomponenten und den Aufruf an die Sortierroutine enthält - Implementierung107 #include <iostream.h> #include <string.h> const int groesseSp = 1000; char wortSp[groesseSp]; void worteLesen(char* z[], int& n) { char* zgrwortSp = wortSp; cout << "\nAnzahl Worte "; cin >> n; cout << "\nWorte eingeben: "; for (int i = 0; i < n; i++) { cin >> zgrwortSp; // Einlesen Zeichenkette z[i] = zgrwortSp; // Zuweisen Zeiger auf Zeichenkette zgrwortSp +=strlen(zgrwortSp) + 1; // } } 107 PR22307.CPP 136 Die Programmiersprache C++ void bubbleSort(char* z[], int n) { char* h; for (int i = 0; i < n - 1; i++) for (int j = n - 1; i < j; j--) if (strcmp(z[j-1], z[j]) > 0) { h = z[j- 1]; z[j - 1] = z[j]; z[j] = h; } } void ausgabe(char* z[], int n) { for (int i = 0; i < n; i++) cout << z[i] << " "; } int main() { const int anzWorte = 50; char* zgrZk[anzWorte]; int n; worteLesen(zgrZk,n); bubbleSort(zgrZk,n); ausgabe(zgrZk,n); } Dynamisch erzeugte mehrdimensionale Arrays Mehrdimensionale Felder werden durch Deklaration von Vektoren des Typs array aufgebaut. [0] [1] [2] [...] [n-1] [0] ... ..... ..... ... ..... ... ..... [m-1] ..... Abb. 2.2-7: Dynamische Speicherbelegung für ein zweidimensionales Objekt Für die dynamische Reservierung des Speicherplatzes wird der Heap, in dem Platz für dynamische Variablen reserviert und wieder freigegeben werden kann, benutzt. Die Reservierung von Hauptspeicher auf dem Heap übernimmt void* malloc(size_t groesse) 137 Die Programmiersprache C++ Bei fehlerfreier Ausführung liefert malloc einen Zeiger, der auf den reservierten Speicherbereich zeigt, Ist kein Speicherplatz ausreichender Größe zur Verfügung, dann ist der Rückgabewert Null. Mit void* calloc(size_t anzElemente, size_t groesse) wird der Heap verwaltet. Es wird Speicherbereich von anzElemente Bytes reserviert, die mit Null initialisiert werden. „calloc“ liefert einen Zeiger auf den reservierten Speicherbereich. Steht kein Speicherbereich ausreichender Größe zur Verfügung oder anzElemente bzw. groesse haben den Wert Null, dann liefert calloc Null zurück. Ein über calloc, malloc reservierter Speicherblock wird mit void free(void* block) wieder freigegeben. Bsp.: Dynamische Speicherbelegung für eine Matrix // Eingabe von Koeffizientenwerten void liesmatrix(float** matrix, int zeilen, int spalten) { float wert; for (int i = 0; i < zeilen; i++) for (int j = 0; j < spalten; j++) { cout << "Koeffizientenwert? "; cin >> wert; matrix[i][j] = wert; } } // Speicher freigeben void freispeicher(float** x, int zeilen) { unsigned i; for (i = 0; i < zeilen; i++) free(x[i]); // Spalten loeschen free(x); // Zeilen loeschen } int main() { int zeilen, spalten; float** koeffmatrix; // Koeffizientenmatrix cout << "\nAnzahl Zeilen / Spalten? "; cin >> zeilen; spalten = zeilen; // 1. Schritt: Setzen der Zeilen von der Koeffizientenmatix koeffmatrix = (float**) calloc (zeilen, sizeof (float *)); // 2. Schritt: Setzen der Spalten von der Koeffizientenmatrix for (i = 0; i < zeilen; i++) koeffmatrix[i] = (float*) calloc (spalten, sizeof (float )); // Eingabe der Koeffizientenmatrix cout << "Eingabe der Koeffizientenmatrix" << endl; liesmatrix(koeffmatrix,zeilen,spalten); // Freigabe des Speichers freispeicher(koeffmatrix,zeilen); } 138 Die Programmiersprache C++ Auch die Operatoren new und delete beschaffen dynamisch Speicherplatz für Vektoren. Ist der bei new angegebenen Typ ein array, dann gibt operator new[]() einen Zeiger auf das erste Element des Vektors (array) zurück. Sollten mehrdimensionale Felder angelegt werden, dann sind alle Dimensionen der Vektoren anzugeben, z.B matrixZgr = new int[3][20][12] Falls ein Vektor mit operator delete[]() gelöscht wird, muß das so beschrieben werden delete [] ausdruck z.B.: char* zgr; void funk() { zgr = new char[100]; delete [] zgr; } // reserviert Platz fuer 100 Zeichen Bsp.: Dynamische Speicherbelegung für eine Matrix108 // Ein-, Ausgabe von Matrizen void liesmatrix(float** matrix, int zeilen, int spalten) { float wert; for (int i = 0; i < zeilen; i++) for (int j = 0; j < spalten; j++) { cout << "Koeffizientenwert? "; cin >> wert; matrix[i][j] = wert; } } int main() { float** koeffmatrix; // Koeffizientenmatrix int zeilen, spalten; unsigned i; cin >> zeilen; spalten = zeilen; // 1. Schritt: Setzen der Zeilen von der Koeffizientenmatix koeffmatrix = new float*[zeilen]; // 2. Schritt: Setzen der Spalten von der Koeffizientenmatrix for (i = 0; i < zeilen; i++) koeffmatrix[i] = new float[spalten]; // Eingabe der Koeffizientenmatrix cout << "Eingabe der Koeffizientenmatrix" << endl; liesmatrix(koeffmatrix,zeilen,spalten); // Freigabe des Speichers delete [] koeffmatrix; } Zeiger und Vektoren Zwischen Zeigern und Vektoren besteht eine enge Beziehung. So zeigt bspw. "def" nach der folgenden Definition 108 PR22310.CPP 139 Die Programmiersprache C++ char *def = "#define"; genau auf die Anfangsadresse der Zeichenkette "#define". "def" ist damit eine Adreßvariable. Eine Zuweisung char z; z = *def bedeutet: z enthält das Zeichen "#". Mit ++def kann die Adresse, um die Länge von char (, 8 Bit ,) weitergesetzt werden. Danach wäre in *def "d" enthalten. Der Name einer Variablen bestimmt die Anfangsadresse des 1. Feldelements. Unterschiedlich zu Zeigern, kann diesem Vektornamen keine Adresse zugeordnet werden, da der Vektorname eine Adreßkonstante ist. Ein Zeiger ist dagegen eine Adreßvariable. Zeigervektoren zur Behandlung von Argumenten in Programmen Zeigervektoren spielen bei der Behandlung von Argumenten in C++- Programmen eine wesentliche Rolle. Ein C++-Programm beginnt seine Ausführungen bekanntlich mit einem Aufruf der Funktion main(). Diese Funktion ist so deklariert: int main(int argc, char *argv[], char *arge[]) "argc"109 bestimmt die Anzahl der Argumente in "argv"110. "argv" ist ein Adressverweis auf einen Vektor der Argumente. Enthält ein Kommando beim Aufruf 3 Argumente A1, A2, A3, dann hat bzw. ist bzw. enthält - - "argc" den Wert 4, "argv[0]" der Name des Kommandos, "argv[1]" ein Adressverweis auf die Zeichenkette der Argumente A1, "argv[2]", "argv[3]" Adressverweise auf die Zeichenketten der Argumente A2 bzw. A3, "argv[4]" enthält das Zeichen '\0'. die Variable "arge" Informationen über die Programmumgebung. Zu den Zeichenketten, die üblicherweise in der Programmungebung enthalten sind, gehören: -- der Suchpfad, der zur Lokalisierung von Programmen dient (PATH) -- das Login-Directory (HOME) -- der Name des Terminals Diese Daten werden beim Start des Programms übergeben: Bsp.: 1. Übergabe von Argumenten in der Kommandozeile von Programmen 111 In C++-Programmen können beim Aufruf einer ausführbaren Datei (z.B. a.out bzw. pr22311.EXE) Argumente in der Kommandozeile übergeben werden (z.B. a:\\22311.CPP und a:\\pr22311.LST). Diese Argumente werden in argv[1] bzw. argv[2] gespeichert. Damit stehen sie dem Programmierer zur Verfügung. Hier soll das 1. Argument auf eine Eingabedatei hinweisen, die 109 argument counter argument value 111 PR22311.CPP 110 140 Die Programmiersprache C++ eingelesen werden soll. Das 2. Argument verweist auf eine Ausgabedatei, die im wesentlichen den Inhalt der Eingabedatei aufnehmen soll. Zusätzlich soll jede Zeile durchnummeriert werden. [0] a : \ p r [1] a : \ p r [2] a : \ p r 2 2 2 2 2 2 1 1 1 1 . C P 1 . L S T \0 3 3 3 1 \0 P \0 argv[] Abb. 2.2-8: Kommandozeilen in einem C++-Programm #include <fstream.h> #include <iomanip.h> int main(int argc,char **argv) { int i = 1; char zeile[250]; // eingabedatei[50], ausgabedatei[50]; if (argc == 1) { cout << "\nDateinamen fuer Eingabe/Ausgabe erforderlich"; return(-1); } /* // Alternativ kann mit der Methode get() die Eingabe des Dateinamen // ueber die Tastatur ermoeglicht werden: cout << "Name der Eingabedatei: " << flush; cin.get(eingabedatei,50); */ // Erzeugen des Objekts „eingabe“ der Eingabeklasse ifstream ifstream eingabe(/* eingabedatei */ argv[1], ios::in|ios::nocreate); /* 1. Parameter beschreibt das aufzunehmende Feld 2. Paramter beschreibt die maximale Laenge der einzulesenden Zeichen 3. Parameter ist moeglich und gibt das Stopzeichen an. bei dem die Eingabe auf jedem Fall endet. Fehlt dieser Parameter gilt der Standardwert '\n' */ ofstream ausgabe; /* cout << "Name der Ausgabedatei: " << flush; cin.get(ausgabedatei,50); */ ausgabe.open(/* ausgabedatei */ argv[2],ios::out); if (!ausgabe) cout << "Kann Datei " << ausgabe << " nicht anlegen\n "; if (eingabe.good()) { eingabe.seekg(0L,ios::end); // Positionieren an das Ende der Datei // Das 1. Argument bestimmt den relativen Abstand vom zweiten 141 Die Programmiersprache C++ // Argument cout << "Datei: " << /*eingabedatei*/ argv[1] << eingabe.tellg() << " Bytes " << endl; << "\t" /* tellg() ermittelt die Dateizeigerposition in Bytes ab Dateianfang, d.h. die Dateigroesse, da der Zeiger ans Ende der Datei gesetzt wird */ for (int j = 0; j < 80; j++) cout << "-"; cout << endl; eingabe.seekg(0L, ios::beg); // Setzen des Dateizeigers auf den Dateianfang eingabe.getline(zeile,250); // funktioniert wie get(), entfernt aber das Stoppzeichen aus dem // Datenstrom while(eingabe.good()) // good() erkennt Lesefehler oder das Dateiende und liefert dann // FALSE { ausgabe << "/*" << setw(2) << i++ << "*/ " << zeile << endl; // ausgabe << zeile << endl; eingabe.getline(zeile,250); } } else cout << "Dateifehler oder Datei nicht gefunden" << endl; ausgabe.close(); return(0); } Das vorliegende Programm erzeugt eine Ausgabedatei, die folgende Gestalt besitzt: /* 1*/ #include <fstream.h> /* 2*/ #include <iomanip.h> /* 3*/ /* 4*/ int main(int argc,char **argv) /* 5*/ { /* 6*/ int i = 1; /* 7*/ char zeile[250]; // eingabedatei[50], ausgabedatei[50]; /* 8*/ if (argc == 1) /* 9*/ { /*10*/ cout << "\nDateinamen fuer Eingabe/Ausgabe erforderlich"; /*11*/ return(-1); /*12*/ } .................................................................. .................................................................. Das vorliegende Programm zeigt eine Reihe von Ein- Ausgabeanweisungen zur Bearbeitung von Dateien der iostream-Klasse112. Für grundlegende Dateioperationen gibt es in C++ drei Klassen: - Die ofstream-Klasse für die Ausgabe in eine Datei Die ifstream-Klasse für das Lesen aus einer Datei Die fstream-Klasse für die Ein- und Ausgabe 2. Informationen über die Programmumgebung #include <iostream.h> main(int argc, char *argv[], char *arge[]) { while (*arge) 112 vgl. 3.4 142 Die Programmiersprache C++ cout << *arge++ << endl; } Schwächen des Feldkonzepts sind: - keine Laufzeitüberwachung für Indexausdrücke keine Vereinbarung von beliebigen Indexbereichen keine Reduzierung einmal angelegter Felder keine bequemes Kopieren gleichartiger Felder 2.2.3.2 Der C++-Standardtyp vector Tabellen mit einer Spalte werden in C++ durch eine „Vektor“ genannte Konstruktion (vordefinierte Klasse) gebildet. Ein „Vektor“ ist eine Tabelle von Elementen desselben Datentyps, z.B. mit ganzen Zahlen oder mit double-Zahlen. In C++ ist ein Vektor eine vordefinierte Klasse. Die Anweisung vector<int> v(10) stellt einen Vektor v mit 10 Elementen vom Typ int bereit. Feldelemente sind in C/C++ von 0 bis (Anzahl der Elemente – 1) durchnumeriert (hier von 0 bis 9). Es gibt keine Überprüfung der Bereichsüber- oder –unterschreitung. Die Klasse für Vektoren113 stellt einige Dienstleistungen zur Verfügung, z.B.: - - size() ermittelt die Größe des Vektors, im vorliegenden Fall zeigt „cout << v.size() << endl;“ die tatsächliche Größe des Vektors an. capacity() bestimmt die maximal mögliche Anzahl der Vektorelemente, die in dem Vektor gespeichert werden können. Indexoperator [] realisiert den Zugriff auf ein spezielles Element des Vektors. „v[0]“ zeigt über „cout << v[0];“ das erste Element des Vektors an. Man kann auf die eckigen Klammern verzichten und einen Vektor über at nach einem Wert an einer Position fragen, z.B. cout << v.at(0) << endl;. Diese Art des Zugriffs wird geprüft.“cout << v.at(20) << endl“ führt zum Programmabbruch mit Fehlermeldung. push_back() Ein Vektor der C++-Standardbibliothek hat den Vorteil, daß er bei Bedarf Elemente hinten anhängt und dabei seine Größe ändert. Über push_back(wert) wird der anzuhängende „wert“ übergeben und ohne weiteres Zutun des Programmierers gespeichert. Bsp.: Einfache Sortierverfahren #include <iostream> #include <vector> // Standard-Vektor using namespace std; // Funktionsprototypen void eingabe(vector<int>&, int&); void ausgabe(vector<int>, int); void bsort(vector<int>&, int); void auswahlsortieren(vector<int>&, int); void einfuegesortieren(vector<int>&, int); void tauschen(vector<int>&, int, int); int main() 113 vgl. 5.2.8 143 // // // // // // Eingabe Ausgabe Bubble-Sort Sortieren durch Auswaehlen Sortieren durch Einfuegen Tauschen Die Programmiersprache C++ { int n; // int x[20]; // vector<int> x(20); vector<int> x; // anfaengliche Groesse ist 0 eingabe(x,n); cout << "Ausgabe unsortiert:\n"; ausgabe(x,n); cout << "Ausgabe sortiert:\n"; bsort(x,n); // einfuegesortieren(x,n); // auswahlsortieren(x,n); ausgabe(x,n); return 0; // kann weggelassen werden } // Eingabe void eingabe(vector<int>& x, int& n) { int wert; // cout << "Anzahl Elelemente?\n"; // cin >> n; cout << "Eingabe von Werten\n"; do { cout << "Wert (0 = Ende der Eingabe):"; cin >> wert; if (wert != 0) x.push_back(wert); // Wert anhaengen } while (wert != 0); n = x.size(); } // Ausgabe void ausgabe(vector<int> x, int n) { const int zeilenLaenge = 12; // Anzahl der Elemente je Zeile cout << "(" << n << ") <"; for (int i = 0; i < n; ++i) { if (i % zeilenLaenge == 0 && i) // ; cout << "\n\t"; cout << x[i]; // Trennen durch , Ausnahme letztes Element if (i % zeilenLaenge != zeilenLaenge - 1 && i != n - 1) cout << ", "; } cout << ">\n"; } // Sortieren void bsort(vector<int>& x, int n) { // Sortieren durch Austauschen for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) // if ((*vrgl) (x[i], x[j]) < 0) if (x[i] > x[j]) tauschen(x,i,j); } void auswahlsortieren(vector<int>& x, int n) { int min; for (int i = 0; i < n - 1; i++) { min = i; for (int j = i + 1; j < n; j++) if (x[j] < x[min]) min = j; 144 Die Programmiersprache C++ tauschen(x,min,i); } } void einfuegesortieren(vector<int>& x, int n) { int schl, j; for (int i = 1; i < n; i++) { schl = x[i]; j = i - 1; while ((schl < x[j]) && (j >= 0)) { x[j + 1] = x[j]; j--; } x[j + 1] = schl; } } void tauschen(/* int* x,*/ vector<int>& x, int i, int j) { int zwischen = x[i]; x[i] = x[j]; x[j] = zwischen; } 2.2.3.3 Die C++-Stringklasse Die C++-Stringklasse (Header: <string>) kann in den meisten Fällen C-Strings ersetzen. Der in der Standardbibliothek definierte Typ basic_string ist ein Template. Der Typ string ist eine Spezialisierung von basic_string für den Datentyp char: typedef basic_string<char> string;. Strings existieren nicht nur basierend auf dem Datentyp char, sondern auch noch einmal basierend auf dem Datentyp wchar_t. Dieser Datentyp dient zur Speicherung eines 16-Bit-Zeichens, d.h. eines Zeichens im Unicode-Zeichensatz. Strings, die auf wchar_t basieren, gehören zur Klasse wstring, während Strings, die auf char basieren, zur Klasse string gehören. Es handelt sich in beiden Fällen nur um verschiedene Instantiierungen derselben Basisklasse (basic_string). Alle Members sind gleich. Strings sind in C++ einfach zu handhaben und robust. Es gibt bspw. hier die üblichen Operatoren: << + == <= >= >> += != < > Ein- und Ausgabe Verketten, Anhängen Prüfung auf Gleichheit, Ungleichheit Alphabetische Vergleiche auf kleiner bzw. kleiner, gleich Alphabetische Vergleiche auf größer bzw. größer, gleich Erzeugen von String-Objekten. Beim Anlegen von String-Objekten braucht man keine maximale Länge anzugeben, weil Strings automatisch mitwachsen, wenn sich ihr Inhalt ändert. Beim Initialisieren können C-Zeichenketten und andere Strings verwendet werden. Sogar das 145 Die Programmiersprache C++ Ausschneiden von Teilzeichenketten geht, ohne daß man dazu ein spezielle Methode braucht. Arbeiten mit String-Objekten Zuweisen von String-Objekten: Strings können ohne Probleme zugewiesen werden. Lästige Aufrufe, z.B. strcpy, sind unnötig. Für das Zuweisen von Teilzeichenketten gibt es die Methode assign(): string zk1("Dies ist eine lange Zeichenkette"); string zk2; zk2.assign(zk1, 14, 4); // zugewiesen wird "lang" Längenbestimmung von String-Objekten: Sie erfolgt mit der Methode length() Zugriff auf einzelne Zeichen von String-Objekten: Man kann hier dieselbe Schreibweise wie bei den C-Zeichenketten verwenden, weil der []-Operator überladen wurde. Gleichbedeutend zu dieser Art des Zugriffs ist die Anwendung der Methode at(). Im Unterschied zu dem Operator [] enthält at() eine Bereichsüberprüfung, die ggf. eine Ausnahme erzeugt. Einfügen und Löschen von Zeichen in String-Objekten: Die Methode insert() erlaubt das Einfügen von Zeichen und Zeichenketten an beliebiger Stelle im String. Ein Überlaufen kann nicht passieren , da der String nötigenfalls vergrößert wird. Zum Anhängen an das Ende verwendet man einfacher den Operator +=. Suchen und Ersetzen von String-Objekten: Zum Suchen gibt es die Methoden find() zum Suchen vom Beginn der Zeichenkette und rfind(), die von rechts sucht. Zum Ersetzen gibt es die Methode replace(). Hier werden die Startposition, ab der ersetzt werden soll, die Anzahl der Zeichen, die ersetzt werden soll, und die Zeichenkette, die stattdessen eingesetzt werden soll, angegeben. Zeichen löschen. iterator erase(iterator p) löscht ein Zeichen an Stelle p. Zurückgegeben wird die Position direkt vorher, sofern sie existiert, andernfalls end(). iterator erase(iterator p, iterator q) löscht Zeichen im Bereich p bis ausschließlich q. string& erase(size_type pos = 0,size_type n = npos) löscht alle Zeichen ab der Stelle pos, aber nicht mehr als npos. Einlesen von String-Objekten: Strings können mit dem üblichen <<-Operator eingelesen werden. Es wird immer bis zu einem Whitespace gelesen. Möchte man mehr als nur ein Wort – üblicherweise eine Zeile – einlesen, dann verwendet man die Funktion :istream& getline(istream&, string&, char=‘\n‘); Speicherplatzreservierung für String-Objekte. Damit ein String nicht ständig reallokiert werden muß, weil er öfters mal wächst, kann man für ihn direkt eine bestimmte Größe reservieren. Das geht mit der Methode reserve(string::size_type). 146 Die Programmiersprache C++ Weitere Schnittstellenfunktionen der Klasse string Konstruktoren string() // Standarkonstruktor, erzeugt einen leeren String. string(const string& s, size_type pos = 0, size_type n = npos) // Der Kopierkonstruktor erzeugt einen String, wobei s ab Postion pos bis zum Ende kopiert wird. // Dabei gilt die Einschränkung, daß maximal n Zeichen kopiert werden. „string::npos“ ist eine // –1, konvertiert zum Typ size_type, in der Regel die größtmögliche unsigned Zahl. string(const char* s, size_type n) // n Zeichen werden aus dem bei s beginnenden Array kopiert string(const char* s) // erzeugt einen String aus dem C-String s string(size_type n, char z) // erzeugt einen String mit n Kopien von z. Destruktor ~string() // Anfang und Ende eines Strings const_iterator begin() const und iterator begin() // geben den Anfang des Strings zurück const_iterator end() const und iterator end() // geben die Position nach dem letzten Zeichen zurück Methoden size_type size() const // gibt die aktuelle Größe des Strings zurück (Anzahl der Zeichen) void clear() // löscht den Inhalt des String bool empty() const // gibt size() == 0 bzw. begin() == end zurück string& append(const string& s) string& append(const char* s) string& append(char z) string& operator+=(const string& s) string& operator+=(const char* s) string& operator+=(char z) // verlängern den String um den C-String bzw. String s bzw. das Zeichen z string append(const string& s, size_type pos, size_type n) // Von der Position pos des String s bis zum Ende wird alles an den String angehängt, aber nicht // mehr als n Zeichen. string& assign(const string& s) string& assign(const char* s) string& assign(char c) string& operator=(const string& s) string& operator=(const char* s) string& operator=(char z) // weisen dem String den C-String oder String s bzw. das zeichen z zu. string substr(size_type pos = 0, size_type n = npos) const // gibt den Substring zurück, der ab pos beginnt. Die Anzahl der Zeichen im Substring wird durch // das Ende des String bestimmt, kann aber nicht größer als n werden. size_type find(const string& s, size_type pos = 0) const // gibt die Position zurück, an der der Substring s gefunden wird, anderenfalls wird string::npos // zurückgegeben. int compare(const string& s) const // vergleicht zeichenweise die Strings *this und s. Es wird 0 zurückgegeben, wenn keinerlei // Unterschied festgestellt wird. Falls das erste unterschiedliche Zeichen von *this kleiner als das // entsprechende in s ist, wird eine negative Zahl zurückgegeben, andernfalls eine positive Zahl. // Falls bei unterschiedlicher Länge bis zum Ende eines der Strings keine verschiedenen Zeichen // gefunden werden, wird eine negative Zahl zurückgegeben, falls size() < s.size() ist, // andernfalls eine positive Zahl. int compare(size_type pos,size_type n,const string& s) const // gibt string(*this,pos,n).compare(s) zurück. Es werden nur die Zeichen ab Position pos 147 Die Programmiersprache C++ // in *this berücksichtigt, aber maximal n. Operatoren: string operator+(const string&, const string&) string operator+(const string&, const char*) string operator+(const char*, const string&) // Diese Operatoren verketten zwei Strings und geben das Ergebnis zurück. string operator+(const string&,char) string operator+(char,const string&) // Diese Operatoren verketten einen String mit einem Zeichen. bool operator==(x,y) bool operator!=(x,y) bool operator<=(x,y) bool operator>=(x,y) bool operator<(x,y) bool operator>(x,y) // sind relationale Operatoren zum Vergleichen von Strings. x und y stehen hier jeweils für einen // Type string& oder const char*. Es sind drei Kombinationen für x und y möglich: // const string&, const string& // const char*, const string& // const string&, const char* istream& operator>>(istream&,string&) // Dieser Operator erlaubt das Einlesen von Strings auf bequeme Weise. Die üblichen // Eigenschaften des „>>“-Operators werden beibehalten ostream operator<<(ostream&,string&) // Ausgabeopeartor für Strings istream& getLine(istream& is, string& s, char ende = '\n') // liest Zeichen für Zeichen aus der Eingabe is in den String s bis das Zeichen „ende“ eingelesen // wird. Bsp.: Typische String-Operationen114. #include <iostream.h> #include <string> using namespace std; int main() { // Anlegen eines String-Objekts string einString("Hallo"); // Ausgabe eines String cout << einString << endl; // Zeichenweise Ausgabe eines String mit ungeprueftem Zugriff for (size_t i = 0; i < einString.size(); i++) cout << einString[i]; cout << endl; // Zeichenweise Ausgabe eines String mit Indexpruefung for (size_t i = 0; i < einString.size(); i++) cout << einString.at(i); cout << endl; /* Der Versuch einString.at(i) mit i >= einString.size() abzufragen, fuehrt zum Programmabbruch und Fehlermeldung */ // Kopie eines String erzeugen string eineStringkopie(einString); cout << eineStringkopie << endl; // Kopie durch Zuweisung string zuweisungsKopie("Kopie durch Zuweisung"); eineStringkopie = zuweisungsKopie; cout << eineStringkopie << endl; // Zuweisung einer Zeichenkette 114 PR22230.CPP 148 Die Programmiersprache C++ eineStringkopie = "Informatik"; cout << eineStringkopie << endl; // Zuweisen eines einzelnen Zeichens einString = 'X'; cout << einString << endl; // Strings mit + verketten einString = "Hallo " + eineStringkopie + '!'; cout << einString << endl; } 2.2.4 Strukturen Eine Struktur ist ein Datenaggregat115, in dem heterogene Teilobjekte zu einer Einheit zusammengefaßt werden. In C++ erfolgt dies durch eine "struct"Spezifikation. struct studentenSatz { char name[20]; char vorname[20]; char strasse[20]; unsigned short hausnr; char plz[6]; char ort[20]; } student; Diese Definition vereinbart: - eine Strukturvariable "student" einen Strukturtyp "studentenSatz" Die Typdefinition erlaubt es, weitere Objekte von diesem Strukturtyp zu definieren, z.B.: studentenSatz s[100]; // äquivalent mit struct studentenSatz s[100] Der Zugriff auf die Komponenten (member) erfolgt über den Punktoperator: void ausgabestudent(studentenSatz& { cout << "Name: " << s.name cout << "Vorname: " << s.vorname cout << "Strasse: " << s.strasse cout << "Haus-Nr.: " << s.hausnr cout << "PLZ: " << s.hausnr cout << "Ort: " << s.hausnr } s) << << << << << << "\n"; "\n"; "\n"; "\n"; "\n"; "\n"; Strukturen können auch geschachtelt sein, z.B.: struct datum {int tag, monat, jahr}; struct studentenSatz { char name[20]; char vorname[20]; char strasse[20]; 115 Nicht objektorientierter Aspekt des struct-Datentyps 149 Die Programmiersprache C++ unsigned short hausnr; char plz[6]; char ort[20]; datum geburtstag; } student; void ausgabestudent(student_satz& s) { ..................... cout << "Geburtsdatum: "<< s.geburtstag.tag << "." << s.geburtstag.monat << "." << s.geburtstag.jahr << "\n"; } Wie Felder können Strukturen auch initialisiert sein, z.B.: datum geburtstagstudent = {10,4,1954}; Im Gegensatz zu Feldern sind Zuweisungen erlaubt: studentenSatz a, b; // "a" wird mit Werten belegt ........ b = a; Der Strukturname ist sofort nach Einführung gültiger Typname, z.B.: struct listenknoten { infotype info; listenknoten* nachf; } C++ stellt zur Übergabe von Zeigern auf Strukturen einen speziellen StrukturOperator (->) zur Verfügung. Bsp.: 1. Tauschen zweier Werte in einer Struktur #include <iostream.h> struct koord { double xk; double yk; }; void tauschen(struct koord* werte) { double h; h = werte->xk; werte->xk = werte->yk; werte->yk = h; } void main() { struct koord position = {13.11,19.40}; cout << "\nVorher: " << position.xk << " " << position.yk; tauschen(&position); cout << "\nNachher: " << position.xk << " " << position.yk; } 150 Die Programmiersprache C++ 2. Sortieren von Datensätzen im Arbeitsspeicher116 Eingabe von Datensätzen und Einlesen der Datensätze in ein Arbeitsspeicherfeld Sortieren der Datensätze Die Zeiger in den Komponenten des Arbeitsspeicherfelds werden zu diesem Zweck umgestellt. Nach dem Sortieren sind die Zeiger in den Komponeneten des Arbeitsspeicherfelds so bestimmt, daß die Namen in den Datensätzen (über aufsteigende Subskripte ermittelt) aufsteigend sortiert sind Ausgabe der sortierten Datensätze auf das Standardausgabegerät Ausgabe der sortierten Datensätze in eine Textdatei Einlesen der sortierten Datensätze aus der Textdatei und Ausgabe auf das Standardausgabegerät - - eintrag name vorname Abb. 2.2-9: Datenstruktur zur Adressenverwaltung #include <iostream.h> #include <fstream.h> #include <string.h> struct eintrag { char name[20]; char vorname[15]; }; void main() { char* dateiName = "a:ausgD.txt"; int belegung = 100; int n; // Anzahl der aktuellen Saetze eintrag** elemente; elemente = new eintrag*[belegung]; for (int i = 0; i < belegung; i++) elemente[i] = new eintrag; short menue(void), wahl; void aufnehmenSaetze(eintrag**, int&, char*); void ausgebenSaetze(eintrag**, int); void sortierenSaetze(eintrag**, int); void ausgebenDatei(eintrag**, int, char*); void eingebenDatei(char*); cout << "\n\t A D R E S S E N V E R W A L T U N G"; cout << "\n\t -----------------------------------"; do { wahl = menue(); switch(wahl) 116 PR22403.CPP 151 Die Programmiersprache C++ { case 1: aufnehmenSaetze(elemente,n,dateiName); break; case 2: sortierenSaetze(elemente,n); break; case 3: ausgebenSaetze(elemente,n); break; case 4: ausgebenDatei(elemente,n,dateiName); break; case 5: eingebenDatei(dateiName); break; case 6: break; } } while (wahl != 6); } short menue(void) { short izch; cout << "\n\tBitte eingeben: "; cout << "\n\t1: Eingabe von Datensaetzen "; cout << "\n\t2: Sortieren von Datensaetzen "; cout << "\n\t3: Ausgabe von Datensaetzen "; cout << "\n\t4: Speichern von Datensaetzen "; cout << "\n\t5: Einlesen gespeicherter Datensaetze "; cout << "\n\t6: Beendigung des Programms "; cout << "\n\tWahl: "; do { cin >> izch; } while ((izch < 1) || (izch > 6)); return izch; } void aufnehmenSaetze(eintrag** elemente, int& n, char* dateiName) { eintrag* zelem; int zaehler = 0; /* ifstream eingabe(dateiName, ios::in); if (eingabe.good()) { while (!eingabe.eof()) { zelem = new eintrag; if ((zelem = new eintrag) == 0) { cerr << "\n\t Nicht genuegend Speicherplatz vorhanden"; break; } eingabe >> zelem->name; // cout << zelem->name; eingabe >> zelem->vorname; // cout << zelem->vorname; elemente[zaehler++] = zelem; } eingabe.close(); } */ cout << "\n\tAnzahl der einzugebenden Saetze: "; cin >> n; cout << "\tEingabe von " << n << " Datensaetzen\n"; for (int i = 0; i < n; i++) { zelem = new eintrag; cout << "\tName: "; cin >> zelem->name; 152 Die Programmiersprache C++ cout << "\tVorname: "; cin >> zelem->vorname; elemente[i] = zelem; } n += zaehler; } void ausgebenSaetze(eintrag** elemente, int n) { for (int i = 0; i < n; i++) { cout << '\t' << elemente[i]->name << ", " << elemente[i]->vorname << endl; } } void sortierenSaetze(eintrag** elemente, int n) { eintrag* h; for (int i = 0; i < n - 1; i++) for (int j = n - 1; i < j; j--) if (strcmp(elemente[j-1]->name, elemente[j]->name) > 0) { h = elemente[j-1]; elemente[j-1] = elemente[j]; elemente[j] = h; } } void ausgebenDatei(eintrag** elemente, int n, char* dateiName) { ofstream ausgabe(dateiName,ios::out); for (int i = 0; i < n; i++) { ausgabe << elemente[i]->name << '\n'; ausgabe << elemente[i]->vorname << '\n'; } ausgabe.close(); } void eingebenDatei(char* dateiName) { struct eintrag ausgabeSatz; ifstream eingabe(dateiName, ios::in); while (!eingabe.eof()) { eingabe >> ausgabeSatz.name; cout << "\n\t" << ausgabeSatz.name << ", "; eingabe >> ausgabeSatz.vorname; cout << ausgabeSatz.vorname; } eingabe.close(); } Die Dateien, mit denen bisher gerbeitet wurde, waren Textdateien. Diese Dateien sind aus Datenzeilen aufgebaut, die mit den Zeichen '\n' abschließen. Beim Lesen wird dieses Zeichen in die Folge CR+LF (0xD+0xA) umgewandelt. Beim Speichern wird daraus wieder der Zeilenvorschub '\n'. Komplizierte Datenstrukturen werden binär (ios::binary) gespeichert. Im vorliegenden Fall ist dann die Ausgabe bzw. Eingabe in bzw. aus einer Datei so möglich: void ausgebenDatei(eintrag** elemente, int n, char* dateiName) { struct eintrag ausgabeSatz; ofstream ausgabe(dateiName,ios::out | ios::binary); 153 Die Programmiersprache C++ for (int i = 0; i < n; i++) { ausgabeSatz = *elemente[i]; ausgabe.write((char*) &ausgabeSatz,sizeof ausgabeSatz); /* Die Methode write() benötigt einen Zeiger auf den Speicherbereich, der die Daten enthält und eine Angabe über die Datenfgroesse. Hier werden Adresse und Groesse des Objekts angegeben. Der cast sorgt dafuer, dass ein korrekter Pointertyp beim Kompilieren erkannt wird */ } ausgabe.close(); } void eingebenDatei(char* dateiName) { struct eintrag eingabeSatz; ifstream eingabe(dateiName,ios::in | ios :: binary); if (eingabe.good()) cout << "\n\tEingabedatei erfolgreich eroeffnet" << endl; // eingabe.setmode(filebuf::binary); eingabe.read((char*) &eingabeSatz,sizeof eingabeSatz); // Die Methode read() liest die Daten in den Speicherbereich ein while (eingabe.good()) { cout << "\n\t" << eingabeSatz.name << ", " << eingabeSatz.vorname; eingabe.read((char*) &eingabeSatz,sizeof eingabeSatz); } eingabe.close(); } 2.2.5 Variantenstruktur Hier werden die Komponenten nicht hintereinander, sondern quasi übereinander angelegt. Zu einem bestimmten Zeitpunkt kann nur eine Komponente sinnvolle Werte enthalten, z.B.: union int_bzw_float { int i; float f; } v; Es kann nur auf "i" oder "f" zugegriffen werden, z.B.: v.i = 17; cout << "i = " << i << "\n"; v.f = 3.14; cout << "f = " << v.f << "\n"; cout << "i = " << v.i << "\n"; //Ausgabe: Mist Alle Komponenten beginnen an der gleichen Stelle. Die Länge einer union ist gleich der Länge der längsten Komponente. 154 Die Programmiersprache C++ 3. Benutzerdefinierte Datentypen: Klassen 3.1 Konzepte für benutzerdefinierte Datentypen Ziel des C++-Konzepts Klasse (definiert mit struct, union, class) ist: Bereitstellung eines Werkzeugs für die Erzeugung neuer Datentyypen (, die so bequem wie eingebaute Typen eingesetzt werden können). Zu einer Klasse gehören: Datenemente und Verarbeitungselemente (d.h. Funktionen). Daten und (Element-) Funktionen sind in der Klasse gekapselt117 . Bestandteile einer Klasse können dem allgemeinen Zugriff entzogen sein (information hiding). Der Programmentwickler bestimmt die Sichtbarkeit jedes einzelnen Elements. Einer Klasse ist ein Name (TypBezeichner) zugeordnet. Dieser Typ-Bezeichner kann zur Deklaration von Instanzen oder Objekten des Klassentyps verwendet werden. 3.1.1 Formale Definitionsmöglichkeiten Definition einer Klasse Sie besteht aus 2 Teilen 1. dem Kopf, der neben dem Schlüsselwort class (bzw. struct, union) den Bezeichner der Klasse enthält 2. den Rumpf, der umschlossen von geschweiften Klammern, abgeschlossen durch ein Semikolon, die Mitglieder (member, Komponenten, Elemente) der Klasse enthält. Der Zugriff auf Elemente von Klassen wird durch 3 Schlüsselworte gesteuert: private: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur Elementfunktionen zugreifen, die innerhalb derselben Klasse definiert sind. "class"-Komponenten sind standardmäßig "private". protected: Auf Elemente, die nach diesem Schlüsselwort stehen, können nur Elementfunktionen zugreifen, die in derselben Klasse stehen und Elementfunktionen, die von derselben Klasse abgeleitet sind public: Auf Elemente, die nach diesem Schlüsselwort stehen, können alle Funktionen in demselben Gültigkeitsbereich, den die Klassendefinition hat, zugreifen. Der Geltungsbereich von Namen der Klassenkomponenten ist klassenlokal, d.h. die Namen können innerhalb von Elementfunktionen und im folgenden Zusammenhang benutzt werden: klassenobjekt.komponentenname 117 Eine Klasse ist vereinfacht ausgedückt eine Struktur (struct), in der nicht nur Daten abgelegt sind, sondern auch gleichzeitig die Funktionen, die auf diese Daten zugreifen 155 Die Programmiersprache C++ zeiger_auf_klassenobjekt->komponentenname klassenname::komponentenname - - Der Punktoperator „.“ wird benutzt, falls der Programmierer Zugriff auf ein Datenelement oder eine Elementfunktion eines speziellen Klassenobjekts wünscht. Der Pfeil-Operator „->“ wird benutzt, falls der Programmierer Zugriff auf ein spezielles Klassenobjekt über einen Zeiger wünscht Klassen besitzen einen öffentlichen (public) und einen versteckten (private) Bereich. Versteckte Elemente (protected) sind nur den in der Klasse aufgeführten Funktionen zugänglich. Programmteile außerhalb der Klasse dürfen nur auf den mit public als öffentlich deklarierten Teil einer Klasse zugreifen. Eine mit struct definierte Klasse ist eine Klasse, in der alle Elemente gemäß Voreinstellung public sind Die Elemente einer mit union definierten Klasse sind public. Dies kann nicht geändert werden. Die Elemente einer mit class definierten Klasse sind gemäß Voreinstellung private. Die Zugriffsebenen können verändert werden. Datenelemente und Elementfunktionen Datenelemente einer Klasse werden genau wie die Elemente einer Struktur angegeben. Eine Initialisierung der Datenelemente ist aber nicht erlaubt. Deshalb dürfen Datenelemente auch nicht mit const deklariert sein. Funktionen einer Klasse sind innerhalb der Klassendefinition deklariert. Wird die Funktion auch innerhalb der Klassendefinition definiert, dann ist diese Funktion inline. Elementfunktionen von Klassen unterscheiden sich von den gewöhnlichen Funktionen: - Der Gültigkeitsbereich einer Klassenfunktion ist auf die Klasse beschränkt (class scope). Gewöhnliche Funktionen gelten in der ganzen Datei, in der sie definiert sind Eine Klassendefinition kann automatisch auf alle Datenelemente der Klasse zugreifen. Gewöhnliche Funktionen können nur auf die als "public" definierten Elemente einer Klasse zugreifen. Die Funktionen einer Klasse haben die Aufgabe, alle Manipulationen an den Daten dieser Klasse vorzunehmen. Der „this“-Zeiger Jeder (Element-) Funktion wird ein Zeiger auf das Element, für das die Funktion aufgerufen wurde, als zusätzlicher (verborgener) Parameter übergeben. Dieser Zeiger heißt this, ist automatisch vom Compiler) deklariert und mit der Adresse der jeweiligen Instanz (zum Zeitpunkt des Aufrufs der Elementfunktion) besetzt. "this" ist als *const deklariert, sein Wert kann nicht verändert werden. Der Wert des Objekts (*this), auf den this zeigt, kann allerdings verändert werden. 156 Die Programmiersprache C++ 3.1.2 Ein einführendes Beispiel: Stapelverarbeitung Ein Stapel ist eine Datenstruktur zur Aufnahme bzw. Abgabe von Daten (hier: ganze Zahlen). Die Aufnahme der Daten (Operation: push) bzw. die Abgabe der Daten ( (Operation: pop) kann nur von oben (Topelement, Zugriff über die Funktion top() ) erfolgen. Der Stapel soll Elememte118 vom Typ "int" aufnehmen. Schnittstellenbeschreibung #include <iostream.h> const int stapelgroesse = 100; class intStapel // ein neuer Typname wird eingefuehrt { private: // exklusiv für Methoden der Klasse int inhalt[stapelgroesse]; // Array-Implementation int nachf; // Index des nachfolgenden Elements public: // steht allen Benutzern zur Verfügung intStapel& push(int); intStapel& pop(); int top(); int stapeltiefe(); // Stapeltiefe int istleer(); }; Schnittstellenfunktionen (Elemenent-) Es gibt zwei Möglichkeiten zur Angabe von Elementfunktionen in Klassen: - Definition der Funktion innerhalb von Klassen Deklaration der Funktion innerhalb, Definition der Funktion außerhalb der Klasse. Mit dem Scope-Operator :: wird beider Defintion der Funktion außerhalb der Klasse dem Compiler mitgeteilt, wohin die Funktion gehört. Beim Aufruf von Elementfunktionen muß der Name des Zielobjekts angegeben werden: klassenobjektname.elementfunktionen(parameterliste) 1. Deklaration der Funktion innerhalb, Definition der Funktion außerhalb der Klasse. // Elementfunktionen intStapel& intStapel::push(int wert) /* push() soll einen "intStapel" zurueckgeben, aus Effizienzgruenden ist eine Referenz angebracht. push() steht ausserhalb der Klassendefinition, muss daher mit dem Klassennamen ueber den Operator :: verbunden werden: intStapel::push() bedeutet: Die Methode push() der Klassen "intStapel" */ { if (nachf < stapelgroesse) inhalt[nachf++] = wert; // unmittelbarer Zugriff else // primitive Fehlerbehandlung cerr << "Fehler in push() : Stapel ist voll! \n"; return *this; 118 pr31201.CPP 157 Die Programmiersprache C++ /* Das Schluesselwort this entspricht einem in Komponentenfunktionen immer vorhandenen Zeiger auf die Instanz, die Ausdruecke nachf und inhalt sind daher zu this->inhalt und this->nachf aequivalent. *this repraesentiert den vorliegenden Stapel, die Referenz darauf ermittelt der Uebersetzer automatisch */ } intStapel& intStapel::pop() { nachf--; return *this; } int intStapel::top() { return inhalt[nachf - 1]; } int intStapel::stapeltiefe() { return nachf; } int intStapel::istleer() { return nachf == 0; } // Ein kleines Testprogramm main() { intStapel s; // Instanz s.push(7); s.push(11); s.push(21); while ( !s.istleer() ) { cout << s.top() << " "; s.pop(); } return 0; } 2. Definition der Funktion innerhalb von Klassen Elementfunktionen können auch innerhalb der Klassendefinition definiert werden, womit sie automatisch als "inline" vereinbart gelten #include <iostream.h> const int stapelgroesse = 100; class intStapel { private: int inhalt[stapelgroesse]; int nachf; public: intStapel() : nachf(0) intStapel(int e) : nachf(0) intStapel& push(int w) intStapel& pop() int top() int stapeltiefe() int istleer() }; // konstante Klassenvariable? { } { push(e); } { inhalt[nachf++] = w; return *this; } { nachf--; return *this; } { return inhalt[nachf - 1]; } { return nachf; } {return nachf == 0; } 158 Die Programmiersprache C++ 3.1.2 Konstruktoren, Klassenvariable und Destruktoren Konstruktoren Das einführende Beispiel ist falsch. Es fehlt die Initialisierung. Die naive Lösung zur Initialisierung, z.B. intStapel s; s.nachf = 0; scheitert (auf "nachf" darf als private Komponente außerhalb einer Stapel-Methode nicht zugegriffen werden). Konstruktoren dienen zur Initialisierung von Klassen-Objekten, haben keinen Ergebnistyp und sind namensgleich mit ihrer Klasse, z.B.: intStapel::intStapel() {nachf = 0; } Über Argumente kann ein Konstruktor verfügen, z.B.: intStapel::intStapel(int erstesElement) { nachf = 0; push(erstesElement); /* Bei der Defimition des Stapel wird zugleich das erste Element gestapelt */ } Aktiviert wird der Konstruktor über intStapel s(7); bzw. intStapel s = 7; // Initialisierung, keine Zuweisung Konstruktoren können überladen werden und Vorgabewerte definieren. Falls mehrere Konstruktoren vorliegen, bestimmt der Compiler (anhand der Parameterlisten), welcher Konstruktor zu verwenden ist. Konstruktoren unterscheiden sich hinsichtlich Anzahl und/oder Typ der Parameter. Es ist nicht möglich, einen zweiten Konstruktor mit Hilfe eines ersten Konstruktors zu intialisieren: intStapel::intStapel(int erstesElement) { intStapel(); push(erstesElement); // Fehler } Für die Hauptaufgabe eines Konstruktors (Initialisierung einer Instanstanzvariablen) gibt es eine besondere Syntax, z.B.: intStapel :: intStapel() : nachf(0) { } intStapel :: intStapel(int e) : nachf(0) { push(e); } Werden mehrere Komponenten auf diese Weise initialisiert, entspricht die Reihenfolge der Initialisierung der Reihenfolge der Deklaration, z.B.: #include <iostream.h> int f(int i) { cout << i << " "; return i; } 159 Die Programmiersprache C++ class X { int a, b, c; public: X() : a(f(1)), c(f(2)), b(f(3)) { } }; main() { X x; return 0; } führt zur Ausgabe: 1 3 2 Klassenvariable C++ erlaubt die Definition und Deklaration von Klassenvariablen mit dem Schlüsselwort static. Allerdings können diese Elemente, z.B. "stapelgroesse", nicht innerhalb der Klassendefinition initialisiert werden. #include <iostream.h> class intStapel { private: static const int stapelgroesse = 100; ......... // hier verboten! "stapelgroesse" muß außerhalb der Klasse definiert und bei Bedarf initialisiert werden. class intStapel { private: static const int stapelgroesse; ................................. } // Deklaration const int intStapel :: stapelgroesse = 100; // Definition Allerdings ist dann "int inhalt[stapelgroesse]" falsch. Ein Ausweg ist: Verwendung einer Zeigerkomponente anstatt einer "array-"Komponenten. class intStapel { private: static const int stapelgroesse; int *inhalt; int nachf; public: intStapel() : nachf(0), inhalt(new int[stapelgroesse]) { } intStapel(int e) : nachf(0), inhalt(new int[stapelgroesse]) { push(e); } ............................................................... }; const int intStapel :: stapelgroesse = 100; 160 Die Programmiersprache C++ Destruktoren Sie werden aufgerufen, falls ein Objekt nicht mehr benötigt wird. Das ist in 4 verschiedenen Situationen der Fall: 1. 2. 3. 4. Der Block, in dem eine automatische Variable definiert wurde, wird verlassen Ein über "new" angelegtes Objekt wird mit "delete" zurüchgegeben Ein Programm, in dem eine statische Variable definiert ist, wird beendet. Ein vom Übersetzer erzeugtes temporäres Objekt wird nicht nehr benötigt. Destruktoren sind nicht immer nötig. So gibt es im einführenden Beispiel nichts wegzuräumen. Dynamisch angelegte Felder sind allerdings der Speicherverwaltung zurückzugeben. Destruktoren sind nach der Klasse benannt, ihre Namen beginnen jedoch mit einer Tilde (~). Sie sind immer parameterlos und verfügen über keinen Ergebnistyp (ebenso wie Konstruktoren). class intStapel { private: static const int stapelgroesse; int *inhalt; int nachf; void init() { inhalt = new int [stapelgroesse]; nachf = 0; } /* Hilfsfunktion zur Sicherstellung, dass die beiden Konstruktoren diesselben Initialisierungen ausführen */ public: intStapel() { init(); } intStapel(int e) { init(); } ~intStapel() { delete [] inhalt; } // Destruktor ..................................................... }; Kopierkonstruktoren Das folgende Programmstück funktioniert, falls die Implementierung eines Stapel nach der Vorlage des einführenden Beispiels erfolgt: intStapel s; s.push(1); // s s.push(2); // s intStapel t(s); // s.push(3); // t.push(0); // <- 1 <- 1,2 aequivalent zu t = s s <- 1,2,3 t <- 1,2,0 Die zuletzt eingeführte Variante der Stapelimplementierung führt allerdings auf einen Fehler: s, t erhalten am Ende diesselben Elemente. Keiner der beiden Konstruktoren wird hier aufgerufen, da keiner von ihnen ein Stapelargument erwartet. Es erfolgt (über den Standard-Kopierkonstruktor): - Anlegen eines leeren Stapelelements für t komponentenweises Kopieren, daraus folgt "s.inhalt" und t.inhalt" erhalten diesselbe Adresse. 161 Die Programmiersprache C++ t s 1 2 0 inhalt nachf 3 3 ~ ~ Abb. 3.1-1: Datenstrukturen zum Stapel Die Fehlerbehebung erfolgt durch einen passenden Kopierkonstruktor (copy constructor). Er erledigt die übliche Initialisierunsarbeit und kopiert anschließend den Inhalt seines Arguments auf die neu erzeugte Instanz. intStapel :: intStapel(const intStapel& s) // die Referenz auf eine Konstante zeigt, dass der Parameter durch // den Initialisierungsvorgang typischerweise nicht verändert wird { init(); // legt ein neues Feld an nachf = s.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i]; } Kopierkonstruktoren werden in folgenden Fällen aktiviert: - Explizite Initialisierung eines Objekts durch ein anderes Initialisierung eines formalen Parameters mit dem Wert des aktuellen Parameters bei Wertparameterübergaben Übergabe eines Funktionsergebnisses Hinweis: Bei der Initialisierung von Referenzen wird kein neues Objekt aufgerufen und daher auch kein Konstruktor aufgerufen. Defaultkonstruktoren Sie werden ohne Argumente aufgerufen. Er kann als einziger zur Initialisierung von Feldern (arrays) herangezogen werden, z.B.: intStapel stacks[100]119; // Initilaisieren aller Tabellenelemente in der Reihenfolge aufsteigender // Indexwerte // syntaktisch ist hier nicht moeglich: die Uebergabe von Argumenten // Die Aktivierung erfolgt nur ueber den Default-Konstruktur Für Klassen, deren Konstruktor mindestens einen Parameter erwarten, können keine "Arrays" definiert werden. Bei Klassen, für die überhaupt kein Konstruktor definiert ist, wird ein "public"-Defaultkonstruktor vom Compiler erzeugt. Wird stacks[100] eliminiert, wird der Destruktor für alle Feldelemente in absteigender Reihenfolge aufgerufen. 119 Das Feld stacks[100] wird hier durch sukzessives Aufrufen von intStapel mit 100 leeren Stapeln initialisiert 162 Die Programmiersprache C++ 3.1.4 Operatorfunktionen Bsp.: "Problematische Zuweisung" intStapel s, t; s.push(1); s.push(2); t = s; // Zuweisung s.push(3); // s <- 1,2,3 t.push(0); // s<- 1,2,0 s, t (eigentlich unterschiedliche Objekte) benutzen den gleichen Speicherbereich über die Komponente "inhalt". Die Lösung des Problems erfolgt über eine Komponentenoperatorfunktion: class intStapel { ........... intStapel& operator = (const intStapel&); }; // Operatorfunktion (Stapel mit eigenem Zuweisungsoperator) intStapel& intStapel :: operator = (const intStapel& r) { nachf = r.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i]; return *this; } Der am weitesten links stehende Operand wird durch *this (wird immer mitübergeben) repräsentiert, ein n-ärer Operator hat daher nur (n-1) Parameter. Da die Zuweisung in C++ üblicherweise den Wert der linken Seite liefert, ist es empfehlenswert, "operator=()" eine Referenz auf *this zurückgeben zu lassen. Überladen von Operatoren Damit kann die Bedeutung von Operatoren für die Anwendung auf Objekte spezifischer Klassen definiert werden. Die klassenspezifische Definition kann arithmetische, logische, relationale Operatoren, den Zuweisungsoperator, den Aufruf-Operator (), den Subskript-Operator [] und den "dereference"-Operator -> umfassen. Zu beachten ist: - Die Priorität der Operatoren kann nicht verändert werden Die Stelligkeit (unär, binär) kann nicht geändert werden Operatoren, die unär und binär sein können, gelten bei der Redefinition als unterschiedlich Bsp.: Test auf Gleichheit zweier Stapelobjekte int intStapel :: operator == (const intStapel& r) { if (r.nachf != nachf) return 0; // unterschiedliche Groessen if (r.inhalt == inhalt) return 1; // for (int i = 0; i < nachf; i++) if (inhalt[i] != r.inhalt[i]) return 0; // return 1; } 163 Die Programmiersprache C++ 3.1.5 Konstante Komponentenfunktionen Bei der folgenden Definition einer Nichtkomponentenfunktion groesseSt() kommt es zu einer Zugriffskollision: inline long groesseSt(const intStapel& s) { return sizeof(s) + intStapel::stapelgroesse * sizeof(int); } "stapelgroesse" ist private Klassenvariable und nur über Komponentenfunktionen heraus ansprechbar. Daher muß "groesseSt()" als Komponenetenfunktion definiert werden: inline long intStapel :: groesseSt() { return sizeof(intStapel) + stapelgroesse * sizeof(int); } , die dann mit inline long groesseSt(const intStapel& s) { return s.groesseSt(); // wird vom Compiler als Fehler markiert, // da die Konstanz von s nicht garantiert werden kann } überladen wird. Nötig ist die explizite Deklaration, daß die Methode "groesseSt()" ihr Objekt unangetastet läßt. Das geschieht unmittelbar durch Angabe des Schlüsselworts „const“ nach der Parameterliste inline long intStapel :: groesseSt() const { return sizeof(intStapel) + stapelgroesse * sizeof(int); 164 } Die Programmiersprache C++ 3.1.6 Statische Komponentenfunktionen In C++ können Komponentenfunktionen "static" vereinbart sein. Diese Klassenmethoden verlieren aber das implizite Argument "this" und dürfen daher nicht mehr auf Instanzen Bezug nehmen, können dafür aber objektunabhängig aufgerufen werden. Bsp.: Die Komponentenfunktion groesseSt() des vorstehenden Beispiels zeigt keinen Bezug zu einer Stapelinstanz (und ist damit offenbar so etwas wie eine Klassenmethode): class intStapel { ........................... public: .......................... static long groesseSt() }; inline long intStapel :: groesseSt() const { return sizeof(intStapel) + stapelgroesse * sizeof(int); } Der Aufruf dieser Funktion kann über ein beliebiges Objekt der Klasse intStapel oder lediglich unter Angabe des Klassennamens erfolgen. 3.1.7 "friend"-Funktionen und "friend"-Klassen Die Komponentenfunktion "groesseSt()" ermöglicht den Zugriff einer globalen Funtion gleichen Namens auf Klasseninterna. Alternativ dazu hätte der Funktion groessSt() auch explizit der Zugriff auf private Klassenkomponenten gewährt werden können, wenn sie als sogenannte "friend"-Funktion deklariert worden wäre: class intStapel { private: .................... public: ...................... friend long groesseSt(const intStapel& s); }; Auch Klassen können als "friend" anderer Klassen deklariert werden. Häufig wird "operator <<()" als friend vereinbart: class intStapel { private: .................... public: .................... friend ostream& operator << (ostream& o, const intStapel& s); }; ostream& operator << (ostream& o, const intStapel& s) { 165 Die Programmiersprache C++ o << "<"; for (int i = 0; i < s.nachf; i++) { if ((i <= s.nachf - 1) && (i > 0)) o << ", "; o << s.inhalt[i]; } return o << ">"; } „Friend“-Funktionen weichen das Prinzip des „information hiding“ auf und sollten deshalb sparsam verwendet werden. Am häufigsten werden sie zusammen mit dem Mechanismus des „Overloading“ benutzt. 3.1.8 ADT Stapel Abstrakte (Daten-) Typen werden über die Angabe der Datenelemente und der Methoden, die auf diesen Datenelementen operieren, beschrieben. Die Methoden können eingeteilt werden in: - Konstruktoren Sie liefern neue Elemente (Instanzen) des ADT. - Zugriffsfunktionen Sie bestimmen die Eigenschaften von existierenden Datenelementen des Typs - Umsetzungsfunktionen Sie bilden neue Elemente des ADT (durch Hinzufügen bzw. Entfernen) Bezogen auf den „ganzzahligen Stapel“ führt das zur folgenden Schnittstellenbeschreibung: #include <iostream.h> class intStapel { private: static const int stapelgroesse; int *inhalt; int nachf; void init() { inhalt = new int[stapelgroesse]; nachf = 0; } public: intStapel() { init(); } // Konstruktoren intStapel(int e) { init(); push(e); } intStapel(const intStapel&); ~intStapel() { delete [] inhalt; } // Destruktor intStapel& push(int w) { inhalt[nachf++] = w; return *this; } intStapel& pop() { nachf--; return *this; } int top() { return inhalt[nachf - 1]; } int stapeltiefe() { return nachf; } int istleer() { return nachf == 0; } long groesseSt() const { return sizeof(intStapel) + stapelgroesse * sizeof(int); } intStapel& operator = (const intStapel&); int operator == (const intStapel&); friend ostream& operator << (ostream& o, const intStapel& s); }; const int intStapel :: stapelgroesse = 100; // Kopierkonstruktor 166 Die Programmiersprache C++ intStapel :: intStapel(const intStapel& s) { init(); // Anlegen eines neuen Felds nachf = s.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i]; } // Operatorfunktion (Stapel mit eigenem Zuweisungsoperator) intStapel& intStapel :: operator = (const intStapel& r) { nachf = r.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i]; return *this; } // Operatorfunktion zur Ueberpruefung zweier Stapelobjekte auf Gleichheit int intStapel :: operator == (const intStapel& r) { if (r.nachf != nachf) return 0; // unterschiedliche Groessen if (r.inhalt == inhalt) return 1; // Fall 1 oder 2 for (int i = 0; i < nachf; i++) if (inhalt[i] != r.inhalt[i]) return 0; // Fall 3 return 1; } // Operator <<() ostream& operator << (ostream& o, const intStapel& s) { o << "<"; for (int i = 0; i < s.nachf; i++) { if ((i <= s.nachf - 1) && (i > 0)) o << ", "; o << s.inhalt[i]; } return o << ">"; } Anwendung: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“ 120 Aufgabenstellung: Defaultmäßig werden Zahlen dezimal ausgegeben. Ein Stapel, der Ganzzahlen aufnimmt, kann dazu verwendet werden, Zahlen bezogen auf eine andere Basis als 10 darzustellen. Die Funktionsweise der Umrechnung von Dezimalzahlen in eine Basis eines anderen Zahlensystem zeigen die folgenden Beispiele: 2810 3 8 4 34 8 72 10 1 64 0 16 2 4 0 1020 4 5310 1 32 1 16 0 8 1 4 0 2 1 1101012 Mit einem Stapel läßt sich die Umrechnung folgendermaßen unterstützen: 120 PR31810.CPP 167 Die Programmiersprache C++ 6 leerer Stapel n 355310 7 7 4 4 4 1 1 1 1 n%8=1 n/8=444 n 444 10 n%8=4 n/8=55 n 5510 n%8=7 n/8=6 n 610 n%8=6 n/6=0 n 010 Abb.3.1-2: Umrechnung von 355310 in 67418 mit Hilfe eines Stapel Algorithmus zur Lösung der Aufgabe: 1. 2. 3. 4. Die am weitesten rechts stehende Ziffer von n ist n%b. Sie ist auf dem Stapel abzulegen. Die restlichen Ziffern von n sind bestimmt durch n/b. Die Zahl n wird ersetzt durch n/b. Wiederhole die Arbeitsschritte 1. und 2. bis keine signifikanten Ziffern mehr übrig bleiben. Die Darstellung der Zahl in der neuen Basis ist aus dem Stapel abzulesen. Der Stapel ist zu diesem Zweck zu entleeren. Implementierung: Das folgende kleine Testprogramm realisiert den Algorithmus und benutzt dazu eine Instanz der vorliegenden Klasse intStapel. void ausgabe(long zahl, int b) { intStapel s; // Instanz // Extrahiere die Ziffern zur jeweiligen Basis (von rechts nach links) // und lege sie im Stapel ab do { s.push(zahl % b); zahl /= b; } while (zahl != 0); while (!s.istleer()) { cout << s.top(); s.pop(); } } int main(void) { long zahl; // int b; // Basis // lies 3 positive ganze Zahlen und die gewuenschte Basis for (int i = 0; i < 3; i++) { cout << "\nGib eine nicht negative ganze Dezimalzahl und " << "\ndanach die Basis (2 <= b <= 9) an" << endl; cin >> zahl >> b; cout << zahl << " basis " << b << " ist: "; ausgabe(zahl,b); cout << endl; } } 168 Die Programmiersprache C++ 3.2 Abgeleitete Klassen 3.2.1 Basisklasse und Ableitung Vorhandene Klassen können zur Definition neuer Klassen herangezogen werden. Die Klasse, die zur Definition verwendet wird, heißt Basisklasse. Die neue Klasse ist die abgeleitete Klasse oder Ableitung. Bsp.: class aKlasse { private: int A; protected: int B; public: int C; void f(); }; Die folgende Deklaration class abKlasse : public aKlasse { public: void g(); }; leitet eine neue Klasse (abKlasse) aus der alten Klasse (aKlasse) ab. "abKlasse" beerbt "aKlasse" und darf alle öffentlichen (public) und geschützten (protected) Bereiche benutzen. Private Bereiche dürfen nicht verwendet werden. So ist bspw. innerhalb der Elementfunktion g() die Anweisung "A = 10;" nicht erlaubt. "B = 10; C = 20;" sind dagegen gestattet. In einer privaten Ableitung erhalten alle geerbten Klassenmitglieder den Status private. "Friend"-Deklarationen werden nicht vererbt. Konstruktoren (und Destruktoren) nehmen bei abgeleiteten Klassen eine Sonderstellung ein: Sie werden ebenfalls nicht vererbt. 169 Die Programmiersprache C++ 3.2.2 Einfache Vererbung Bsp.: "Abgesicherter Stapeltyp" Die Methoden von intStapel sollen mit einer Fehlerbehandlung ausgestattet werden. Der abgesicherte Stapeltyp (mit Fehlerbehandlung) ist eine Spezialisierung des vorliegenden Stapeltyps. Die erweiterten Operationen sollen folgendermaßen gestaltet sein: 1. Überprüfung der Voraussetzung für die eigentliche Operation 2. Durchführung der eigentlichen Operation 3. Kontrolle, ob die Durchführung erfolgreich war Die folgende Darstellung121 beschreibt die vom bisher vorliegenden Stapeltyp abgeleitete Spezialisierung: class PruefeintStapel : public intStapel { static int abbruch; int fehler; // Fehlerstatus eines Objekts int test(int bed, char* ort); public: PruefeintStapel(); PruefeintStapel(int); PruefeintStapel (const PruefeintStapel&); PruefeintStapel& push(int w); PruefeintStapel& pop(); int top(); PruefeintStapel& operator = (const PruefeintStapel&); // long groesseSt() const; }; int PruefeintStapel::abbruch = 0; Die private Komponentenfunktion test() überprüft die Bedingung "bed" und gibt im Fehlerfall eine Meldung aus: // Programmabbruch extern "C" void exit(int); // Private Elementfunktion test() zur Ueberpruefung der Bedingungen int PruefeintStapel :: test( int bed, char* ort) { if (!fehler && !bed) { cerr << "Fehler in " << ort << "; this = " << long(this) << "\n"; if (abbruch) exit(1); // Beendigung des Programms else fehler = 1; // Markieren des fehlerhaften Objekts } return fehler; } Zugriffsfunktionen: pop(), top() und push() // Zugriffsfunktionen pop() PruefeintStapel& PruefeintStapel :: pop() 121 PR22306.CPP 170 Die Programmiersprache C++ { if (!test(stapeltiefe() > 0, "pop")) intStapel::pop(); return *this; // Vorbedingung } // Zugriffsfunktion top() int PruefeintStapel :: top() { if (!test(stapeltiefe() > 0, "top")) return intStapel::top(); // Eigentliche Operation return 0; // Fehlerfall: beliebiges Ergebnis } // Zugriffsfunktion push() PruefeintStapel& PruefeintStapel::push(int w) { if (!test(stapeltiefe() < stapelgroesse, "push")) // Der Zugriff auf "stapelgroesse" ist noch nicht erlaubt intStapel::push(w); return *this; } Es fehlt noch die Definition von operator=() PruefeintStapel& PruefeintStapel :: operator = (const PruefeintStapel& r) { intStapel::operator = (r); fehler = r.fehler; return *this; } Alle übrigen Methoden werden von den Neuerungen in "PruefeintStapel" nicht berührt und werden im Rahmen der Ableitung der Basisklasse geerbt. Konstruktoren sind allerdings nicht erblich. Allerdings wird häufig die Hauptarbeit vom Konstruktor (bzw. Destruktor) der Basisklasse übernommen, der vor der ersten (bzw. der letzten) Anweisung eines Konstruktors (Destruktors) der abgeleiteten Klasse vom Compiler automatisch aufgerufen wird. // Defaultkonstruktor PruefeintStapel::PruefeintStapel() : fehler(0) { test(inhalt != 0, "PruefeintStapel()"); // Nachbedingung } PruefeintStapel::PruefeintStapel(int e) : intStapel(e), fehler(0) { test (inhalt != 0, "PruefeintStapel(int)"); } PruefeintStapel::PruefeintStapel(const PruefeintStapel& s) : intStapel(s), fehler(s.fehler) { test(inhalt != 0, "PruefeintStapel(const PruefeintStapel&)"); } Ein neuer Destruktor wird nicht benötigt. Der vom Compiler ersatzweise zur Verfügung gestellte Destruktor ruft den Destruktor der Basisklasse auf und sorgt damit für die korrekte Rückgabe von "inhalt". 171 Die Programmiersprache C++ Noch nicht gelöst ist die Zugriffsmöglichkeit auf Komponenten der Basisklasse. Mögliche Fehlerbehebungsmaßnahmen sind: - - "stapelgroesse" und "inhalt" in "intStapel" public deklarieren (Verstoß gegen das Geheimnisprinzip) Deklaration von "PruefeintStapel" in "intStapel" als friend. Damit wären alle privaten Komponenten von "intStapel" an "PruefeintStapel" ausgeliefert. Das würde auch für die "friend"-Deklaration der Methoden "PruefeintStapel::push()" usw. gelten. Das Zugriffskontrollattribut protected erlaubt den Zugriff der Unterklassen. Die Definition von intStapel muß dazu folgendermaßen geändert werden: class intStapel { private: int nachf; void init() { inhalt = new int[stapelgroesse]; nachf = 0; } protected: static const int stapelgroesse; int* inhalt; public: ......................... }; 172 Die Programmiersprache C++ 3.2.3 Klassenhierarchien Von einer Klasse können andere Klassen abgeleitet werden. Eine Ableitung kann außerdem wieder für weitere Klassen verwendet werden. 1. Konstruktoren in Klassenhierarchien Konstruktoren werden nicht automatisch veerbt, z.B.122 #include <iostream.h> class aKlasse { int A; public: aKlasse(int Ain) { A = Ain; cout << "Aufruf Konstruktor aKlasse A = " << A << endl; } ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : public aKlasse { int B; public: abKlasse(int Ain, int Bin):aKlasse(Ain) { B = Bin; cout << "Aufruf Konstruktor abKlasse B = " << B << endl; } }; class abcKlasse : public abKlasse { int C; public: abcKlasse(int Ain, int Bin, int Cin):abKlasse(Ain, Bin) { C = Cin; cout << "Aufruf Konstruktor abcKlasse C = " << C << endl; } }; main() { abcKlasse b(10,20,30); return 0; } Das Ergebnis eines Aufrufs von main() ist: Aufruf Konstruktor aKlasse A = 10 Aufruf Konstruktor abKlasse B = 20 Aufruf Konstruktor abcKlasse C = 30 122 PR22312.CPP 173 Die Programmiersprache C++ Der Basisklassenkonstruktor muß explizit angegeben werden. Ausnahme: Es handelt sich um den Standardkonstruktor bzw. Defaultkonstruktor, z.B. 123: #include <iostream.h> class aKlasse { int A; public: aKlasse(void) { A = 0; cout << "Standardkonstruktor aKlasse A = " << A << endl; } }; class abKlasse : public aKlasse { int B; public: abKlasse(int Bin) { B = Bin; cout << "Konstruktor abKlasse B = " << B << endl; } }; main() { abKlasse b(20); return 0; } Das Ergebnis eines Aufrufs von main() ist: Standarkonstruktor aKlasse A = 0 Konstruktor abKlasse B = 20 Definiert eine Klasse überhaupt keinen Konstruktor, ergänzt der Compiler selbstständig einen Standardkonstruktor, z.B.124: #include <iostream.h> class aKlasse { int A; }; class abKlasse : public aKlasse { int B; public: abKlasse(int Bin) { B = Bin; cout << "Konstruktor abKlasse B = " << B << endl; } }; main() { abKlasse b(20); return 0; } 123 124 PR22316.CPP PR22317.CPP 174 Die Programmiersprache C++ Das Ergebnis eines Aufrufs von main() ist: Konstruktor abKlasse B = 20 2. Destruktoren in Klassenhierarchien In den meisten Fällen wird hier die Arbeit vom Destruktor der Basisklasse übernommen, die nach der letzten Anweisung eines Destruktors der abgeleiteten Klasse automatisch (vom Compiler) aufgerufen wird, z.B.125: #include <iostream.h> class aKlasse { int A; public: ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : public aKlasse { int B; }; main() { abKlasse b; return 0; } Beim Ende von main() wird b eliminiert. "abKlasse" übernimmt den Destruktor von "aKlasse" (Programmausdruck: "Destruktor aKlasse"). Der Unterschied zu normalen Elementfunktionen zeigt sich, falls "abKlasse" einen eigenen Destruktor definiert, z.B.: #include <iostream.h> class aKlasse { int A; public: ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : public aKlasse { int B; public: ~abKlasse(void) { cout << "Destruktor abKlasse" << endl; } }; main() { abKlasse b; return 0; } Am Ende von main() wird zuerst der Anweisungsteil des Destruktors von "abKlasse" ausgeführt. Danach wird der Destruktor der Basisklasse "aKlasse" aufgerufen. Grundsätzlich werden Destruktoren immer in umgekehrter Reihenfolge wie die Konstruktoren aufgerufen, z.B.126: 125 PR22318,CPP 175 Die Programmiersprache C++ #include <iostream.h> class aKlasse { int A; public: aKlasse(int Ain) { A = Ain; cout << "Aufruf Konstruktor aKlasse A = " << A << endl; } ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : public aKlasse { int B; public: abKlasse(int Ain, int Bin):aKlasse(Ain) { B = Bin; cout << "Aufruf Konstruktor abKlasse B = " << B << endl; } ~abKlasse(void) { cout << "Destruktor abKlasse" << endl; } }; class abcKlasse : public abKlasse { int C; public: abcKlasse(int Ain, int Bin, int Cin):abKlasse(Ain, Bin) { C = Cin; cout << "Aufruf Konstruktor abcKlasse C = " << C << endl; } ~abcKlasse(void) { cout << "Destruktor abcKlasse" << endl; } }; main() { abcKlasse b(10,20,30); return 0; } Der Aufruf von main() ergibt: Aufruf Konstruktor aKLasse A = 10 Aufruf Konstruktor abKlasse B = 20 Aufruf Konstruktor abcKlasse C = 30 Destruktor abcKlasse Destruktor abKlasse Destruktor aKlasse 126 PR22312.CPP 176 Die Programmiersprache C++ 3.2.4 Virtuelle Funktionen Sie ermöglichen in der Basisklasse die Deklaration von Funktionen (Schlüsselwort virtual), die in der abgeleiteten Klasse redefiniert werden. Compiler und Lader garantieren die exakte Übereinstimmung. Prinzipiell können alle Methoden außer Konstruktoren und statische Funktionen virtuell sein. Bsp.127: #include <iostream.h> class aKlasse { private: char* sx; public: virtual void f(void); /* virtual zeigt an: Diese Funktion kann verschiedene Versionen in verschiedenen abgeleiteten Klassen haben. Der Ergebnistyp (hier void) darf in abgeleiteten Klassen nicht umdefiniert werden */ }; class abKlasse : public aKlasse { private: char* sy; public: virtual void f(void); }; void aKlasse::f(void) { sx = "aKlasse"; cout << sx << " aufgerufen " << endl; } void abKlasse::f(void) { sy = "abKlasse"; cout << sy << " aufgerufen " << endl; }; main() { aKlasse *az1 = new aKlasse; abKlasse *az2 = new abKlasse; az1 = az2; az1->f(); return 0; } Das Programm gibt aus: "abKlasse aufgerufen" Der Typ des Objekts (Instanz) bestimmt, welche Funktion aufgerufen wird. Das gilt auch für "z", falls main() folgende Gestalt annimmt: int main() { aKlasse *z = new aKlasse; z->f(); delete z; z = new abKlasse; z->f(); delete z; 127 PR22308.CPP 177 Die Programmiersprache C++ return 0; } Wegen der erweiterten Zuweisungskompatibilität kann "z" auf alle Klassen der Hierarchie zeigen. Je nachdem, auf welches Objekt "z" gerade zeigt, wird die entsprechende Routine "f()" der Klasse aufgerufen. "z" zeigt auf etwas, das während des Programms viele verschiedene Gesichter annehmen kann. Man spricht von "Polymorphie" *) (vom griechischen "poly morphos"), d.h.: Vielgestaltigkeit. Eine virtuelle Funktion wird normal vererbt. Falls sie in einer Ableitung redefiniert werden soll, muß sie erst mit exakt identischer Parameterliste angegeben werden. Eine nicht virtuelle Funktion kann in einer Ableitung virtuell deklariert werden. Der umgekehrte Weg ist nicht möglich. Virtuelle Konstruktoren gibt es nicht, da zur ordnungsgemäßen Konstruktion Informationen über den genauen Typ benötigt werden. Auch Zeiger auf Konstruktoren sind nicht erlaubt. Ist eine Funktion virtuell definiert, dann wird die Zuordnung zwischen Prozeduraufruf und aufgerufener Prozedur tatsächlich erst zur Laufzeit des Programm hergestellt. Diesen Vorgang nennt man late binding. Im Gegensatz dazu bedeutet "early binding", daß die Zuordnung bereits zur Übersetzungszeit vorgenommen wird. Eine Klasse mit virtuellen Funktionen hat einen größeren Datenbereich, z.B. #include <iostream.h> class aKlasse { int A; void f() {} }; class bKlasse { int B; virtual void f(){} }; int main() { aKlasse a; bKlasse b; cout << "Groesse a: " << sizeof(a) << endl; cout << "Groesse b: " << sizeof(b) << endl; } Der Unterschied in der Ausgabe zum vorliegenden Programm ist durch eine zusätzliche Zeigervariable verursacht, die der Compiler zur Verwaltung der virtuellen Funktionen der Klasse anlegt. Der Zeiger zeigt auf die virtual function pointer table (vtbl)", die die für das "late binding" erforderliche Funktionsadresse enthält. Bsp.: "intStapel" mit virtuellen Methoden #include <iostream.h> class intStapel { private: int nachf; void init() { inhalt = new int[stapelgroesse]; nachf = 0; } protected: static const int stapelgroesse; 178 Die Programmiersprache C++ int* inhalt; public: intStapel() { init(); } intStapel(int e) { init(); push(e); } intStapel(const intStapel&); ~intStapel() { delete [] inhalt; } // Destruktor virtual intStapel& push(int w) { inhalt[nachf++] = w; return *this; } virtual intStapel& pop() { nachf--; return *this; } virtual int top() { return inhalt[nachf - 1]; } int stapeltiefe() const { return nachf; } int istleer() { return nachf == 0; } virtual intStapel& operator = (const intStapel&); int operator == (const intStapel&); friend ostream& operator << (ostream& o, const intStapel& s); }; // Unterklasse PruefeintStapel class PruefeintStapel : public intStapel { static int abbruch; int fehler; // Fehlerstatus eines Objekts int test(int bed, char* ort); public: PruefeintStapel(); PruefeintStapel(int); PruefeintStapel (const PruefeintStapel&); intStapel& push(int w); intStapel& pop(); int top(); intStapel& operator = (const intStapel&); }; Das Schlüsselwort virtual kann, muß aber nicht im Rahmen der Redefinition wiederholt werden. Alle Funktionen, die virtuelle Funktionen redefinieren sollen, müssen die gleiche Signatur besitzen. Allerdings müßte das Argument von "intStapel& operator = (const intStapel&)" sinnvollerweise "const PruefeintStapel&" sein. So ist "s" bspw. ein "PruefeintStapel" nach folgender Anweisungsfolge: ... intStapel s; PruefeintStapel ps; s = ps; ... Im Rumpf der virtuellen Funktion wird der Parameter der lt. Deklaration vom Basisdatentyp ist, in Wirklichkeit aber der abgeleiteten Klasse angehört, auf den eigentlichen Datentyp konvertiert. // operator =() intStapel& PruefeintStapel :: operator = (const intStapel& r) { const PruefeintStapel& rh = (PruefeintStapel&) r; intStapel::operator = (r); fehler = rh.fehler; return *this; 179 Die Programmiersprache C++ } 3.2.5 Abstrakte Klassen Es gibt Klassen mit rein virtuellen Funktionen (abstrakte Klassen)128. Eine derartige Funktion ist nur deklariert, nicht definiert. Klassen mit rein virtuellen Funktionen dienen nur zur Definition von Ableitungen. Eine abstrakte Klasse ist nur dann sinnvoll, falls in ihr mindestens eine Klasse abgeleitet wird, die die fehlende Funktion bzw. fehlenden Funktionen definiert, z.B.129: #include <iostream.h> class intStapel; class AbsintStapel { public: virtual intStapel& push(int) virtual intStapel& pop() virtual int top() virtual int stapeltiefe() virtual long groesseSt() const virtual intStapel& operator =(const intStapel&) virtual int operator ==(const intStapel&) }; = = = = = = = 0; 0; 0; 0; 0; 0; 0; class intStapel : public AbsintStapel { private: static const int stapelgroesse; int *inhalt; int nachf; void init() { inhalt = new int[stapelgroesse]; nachf = 0; } public: intStapel() { init(); } intStapel(int e) { init(); push(e); } intStapel(const intStapel&); ~intStapel() { delete [] inhalt; } // Destruktor intStapel& push(int w) { inhalt[nachf++] = w; return *this; } intStapel& pop() { nachf--; return *this; } int top() { return inhalt[nachf - 1]; } int stapeltiefe() { return nachf; } int istleer() { return nachf == 0; } long groesseSt() const { return sizeof(intStapel) + stapelgroesse * sizeof(int); } intStapel& operator = (const intStapel&); int operator == (const intStapel&); friend ostream& operator << (ostream& o, const intStapel& s); }; Da für eine abstrakte Klasse keine Objekte erzeugt werden können, darf sie nicht als Funktionsergebnis oder Funktionsargument auftreten, ebensowenig wie sie das Ergebnis einer expliziten Typkonversion sein darf. Referenzen oder Zeiger auf abstrakte Klassen sind jedoch zulässig. 128 129 Eine Klasse, die mindestens eine rein virtuelle Funktion besitzt, heißt abstrakte Klasse PR22310.CPP 180 Die Programmiersprache C++ 3.2.6 Mehrfachvererbung Eine Klasse kann von mehreren Basisklassen gleichzeitig abgeleitet werden (Mehrfachvererbung, multiple inheritance). Bsp.: "abKlasse" ist von aKlasse und "bKlasse" abgeleitet. "abKlasse" hat Funktionen und Daten von "aKlasse" und "bKlasse" geerbt. aKlasse bKlasse abKlasse Eine Klasse kann auch mehrfach als Basisklasse auftreten, z.B.: aKlasse abKlasse acKlasse abcKlasse Abb.: Bsp.130: Konstruktoren und Destruktoren bei Mehrfachvererbung #include <iostream.h> // Konstruktoren und Destruktoren bei Mehrfachvererbung class aKlasse { int A; public: aKlasse(int Ain) { A = Ain; 130 PR22313.CPP 181 Die Programmiersprache C++ cout << "Aufruf Konstruktor aKlasse A = " << A << endl; } ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : public aKlasse { int B; public: abKlasse(int Ain, int Bin):aKlasse(Ain) { B = Bin; cout << "Aufruf Konstruktor abKlasse B = " << B << endl; } ~abKlasse(void) { cout << "Destruktor abKlasse" << endl; } }; class acKlasse : public aKlasse { int C; public: acKlasse(int Ain, int Cin):aKlasse(Ain) { C = Cin; cout << "Aufruf Konstruktor acKlasse C = " << C << endl; } ~acKlasse(void) { cout << "Destruktor acKLasse" << endl; } }; class abcdKlasse : public abKlasse, public acKlasse { int D; public: abcdKlasse(int Ain, int Bin, int Cin, int Din) :abKlasse(Ain,Bin), acKlasse(Ain,Cin) { D = Din; cout << "Aufruf Konstruktor abcdKlasse D = " << D << endl; } ~abcdKlasse(void) { cout << "Destruktor abcdKlasse" << endl; } }; main() { abcdKlasse b(10,20,30,40); return 0; } Die Ausgabe des vorliegenden Programmbeispiels zeigt: Aufruf Aufruf Aufruf Aufruf Aufruf Konstruktor Konstruktor Konstruktor Konstruktor Konstruktor Destruktor Destruktor Destruktor Destruktor Destruktor aKLasse abKlasse aKlasse acKlasse abcdKlasse A B A C D = = = = = 10 20 10 30 40 abcdKlasse acKlasse aKlasse abKlasse aKlasse 182 Die Programmiersprache C++ 3.2.7 Virtuelle Basisklassen Im vorstehenden Beispiel wurden Datenelemente von "aKlasse" in der Ableitung "abcdKlasse" doppelt aufgenommen. Das kann durch sog. virtuelle Basisklassen (Ableitungen) verhindert werden, z.B.: #include <iostream.h> // Konstruktoren und Destruktoren bei Mehrfachvererbung class aKlasse { int A; public: aKlasse(int Ain) { A = Ain; cout << "Aufruf Konstruktor aKlasse A = " << A << endl; } ~aKlasse(void) { cout << "Destruktor aKlasse" << endl; } }; class abKlasse : virtual public aKlasse { int B; public: abKlasse(int Ain, int Bin):aKlasse(Ain) { B = Bin; cout << "Aufruf Konstruktor abKlasse B = " << B << endl; } ~abKlasse(void) { cout << "Destruktor abKlasse" << endl; } }; class acKlasse : virtual public aKlasse { int C; public: acKlasse(int Ain, int Cin):aKlasse(Ain) { C = Cin; cout << "Aufruf Konstruktor acKlasse C = " << C << endl; } ~acKlasse(void) { cout << "Destruktor acKLasse" << endl; } }; class abcdKlasse : public abKlasse, public acKlasse { int D; public: abcdKlasse(int Ain, int Bin, int Cin, int Din) : abKlasse(Ain,Bin), acKlasse(Ain,Cin) { D = Din; cout << "Aufruf Konstruktor abcdKlasse D = " << D << endl; } ~abcdKlasse(void) { cout << "Destruktor abcdKlasse" << endl; } }; main() { abcdKlasse b(10,20,30,40); return 0; } 183 Die Programmiersprache C++ Die Ausgabe des vorliegenden Programmbeispiels zeigt: Aufruf Aufruf Aufruf Aufruf Konstruktor Konstruktor Konstruktor Konstruktor Destruktor Destruktor Destruktor Destruktor aKLasse abKlasse acKlasse abcdKlasse A B C D = = = = 10 20 30 40 abcdKlasse acKlasse abKlasse aKlasse 3.2.8 Generische Datentypen Sie sind bei der Entwicklung wiederverwendbarer Softwarekomponenten von besonderer Bedeutung. So ist ein allgemeiner (generischer) Stapeldatentyp, der je nach Anwendung auf den einen oder anderen Elementdatentyp angepaßt werden könnte, das eigentliche Ziel. Diesselbe Problematik stellt sich bei allen Containerdatentypen, z.B.: Mengen, Liste, Warteschlangen usw., die für unterschiedliche Elementdatentypen definiert werden können. Bsp.: Eine generische Stapelklasse In einer abstrakten Basisklasse für Stapelelemente werden die Botschaften definiert, die ein zu stapelndes Element verstehen muß: #include <iostream.h> class stapelEl { virtual void ausgabe(ostream&) = 0; public: virtual ~stapelEl() {} // triviale Standardefinition des // Destruktor virtual stapelEl* clone() = 0; friend ostream& operator << (ostream& o, stapelEl& e) { e.ausgabe(o); return o; } }; // Generischer Stapel class Stapel { private: static const int stapelgroesse; // protected: stapelEl** inhalt; // dynamisches Feld von Stapelelement-Zeigern int nachf; public: Stapel() : inhalt(new stapelEl*[stapelgroesse]) , nachf(0) { } ~Stapel(); Stapel& push(stapelEl& w) 184 Die Programmiersprache C++ { inhalt[nachf++] = w.clone(); return *this; } Stapel& pop() { delete inhalt[--nachf]; return *this; } stapelEl& top() { return *inhalt[nachf - 1]; } int stapeltiefe() { return nachf; } int istleer() { return nachf == 0; } }; const int Stapel :: stapelgroesse = 100; // Zur Verwendung dieser Stapelklase ist mindestens eine // konkrete Klasse als Elementdatentyp notwendig, z.B.: class intStapel : public stapelEl { int w; void ausgabe(ostream& o) { o << w;} public: intStapel(int i = 0) : w(i) { } stapelEl* clone() { return new intStapel(w); } operator int() { return w; }; }; class doubleStapel : public stapelEl { double w; void ausgabe(ostream& o) { o << w; } public: doubleStapel(double d = 0) : w(d) { } stapelEl* clone() { return new doubleStapel(w); } operator double() { return w; } }; Stapel :: ~Stapel() { for (int i = 0; i < nachf; i++) delete inhalt[i]; delete [] inhalt; } // Ein kleines Testprogramm main() { Stapel s; doubleStapel x = 13, y = 11; intStapel a = 11, b = 13; s.push(x); s.push(y); cout << "top = " << s.top() << endl; s.push(a); s.push(b); cout << "top = " << s.top() << endl; return 0; } 185 Die Programmiersprache C++ 3.3 Schablonen Schablonen (Templates) können als "Meta-Funktionen" aufgefaßt werden, die zur Übersetzungszeit neue Klassen bzw. neue Funktionen 131 erzeugen. 3.3.1 Klassenschablonen Mit einem Klassen-Template (auch generische Klasse oder Klassengenerator) kann ein Muster für Klassendefinitionen angelegt werden. In einer Klassenschablone steht vor der eigentlichen Klassendefinition eine Parameterliste. Hier werden in allgemeiner Form „Datentypen“ bezeichnet, wie sie die Elemente einer Klasse (Daten und Funktionen) benötigen. #include <iostream.h> template <class elType, int stapelgroesse> // Parameter: ElType und stapelgroesse class Stapel { private: elType inhalt[stapelgroesse]; // Festdimensioniertes Feld int nachf; // Index des naechsten freien Elements public: Stapel() : nachf(0) { } Stapel<elType, stapelgroesse>& push(const elType& w) { inhalt[nachf++] = w; return *this; } Stapel<elType, stapelgroesse>& pop() { nachf--; return *this; } elType top() { return inhalt[nachf - 1]; } int stapeltiefe() { return nachf; } int istleer() { return nachf == 0; } }; // Ein kleines Testprogramm int main() { Stapel<int, 100> is; Stapel<double, 20> ds; is.push(13); is.push(11); is.push(40); ds.push(3.14); int i = is.top(); cout << i << endl; double d = ds.top(); cout << d << endl; is.push(ds.top()); return 0; } Jede Instanziierung einer Klassenschablone erzeugt neuen Code für die Methoden der Klasse. Soll der dafür nötige Speicherbedarf reduziert werden, dann empfiehlt es sich, Schablonen mit Ableitung von generischen Klassen zu kombinieren. Die folgende Darstellung zeigt eine Verallgemeinerung des ganzzahlige Stapels132 zu einer universell einsetzbaren Klassenschablone133. , die Datenwerte beliebiger Datentypen aufnehmen kann. 131 132 vgl. 1.7.7 vgl. 3.1.8: ADT-Stapel 186 Die Programmiersprache C++ #include <iostream.h> #include <stdlib.h> #include <ctype.h> // Zentrale Fehlerbehandlungsroutine static void fehler(char* nachricht) { cerr << nachricht << endl; exit(1); } template <class T> class Stapel { private: static const int stapelgroesse; T *inhalt; int nachf; void init() { inhalt = new T[stapelgroesse]; nachf = 0; } public: Stapel() { init(); } Stapel(T e) { init(); push(e); } Stapel(const Stapel&); ~Stapel() { delete [] inhalt; } // Destruktor Stapel& push(T w) { inhalt[nachf++] = w; return *this; } Stapel& pop() { nachf--; return *this; } T top() { return inhalt[nachf - 1]; } int stapeltiefe() { return nachf; } int istleer() { return nachf == 0; } long groesseSt() const { return sizeof(Stapel<T>) + stapelgroesse * sizeof(T); } Stapel& operator = (const Stapel<T>&); int operator == (const Stapel<T>&); friend ostream& operator << (ostream& o, const Stapel<T>& s); }; template <class T> const int Stapel<T> :: stapelgroesse = 100; // Kopierkonstruktor template <class T> Stapel<T> :: Stapel(const Stapel& s) { init(); // Anlegen eines neuen Felds nachf = s.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i]; } // Operatorfunktion (Stapel mit eigenem Zuweisungsoperator) template <class T> Stapel<T>& Stapel<T> :: operator = (const Stapel<T>& r) { nachf = r.nachf; for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i]; return *this; } // Operatorfunktion zur Ueberpruefung zweier Stapelobjekte auf Gleichheit template <class T> int Stapel<T> :: operator == (const Stapel<T>& r) { 133 vgl. auch 5.2.7 187 Die Programmiersprache C++ if (r.nachf != nachf) return 0; if (r.inhalt == inhalt) return 1; for (int i = 0; i < nachf; i++) if (inhalt[i] != r.inhalt[i]) return 0; return 1; // unterschiedliche Groessen // Fall 1 oder 2 // Fall 3 } // Platzbedarf fuer Struktur und gestapelte Elemente ermitteln template <class T> long groesse(const Stapel<T>& s) { return s.groesseSt(); } // Operator <<() template <class T> ostream& operator << (ostream& o, const Stapel<T>& s) { o << "<"; for (int i = 0; i < s.nachf; i++) { if ((i <= s.nachf - 1) && (i > 0)) o << ", "; o << s.inhalt[i]; } return o << ">"; } 3.3.2 Methodenschablonen Methoden zu Klassenschablonen können „inline“ oder außerhalb des Kassenkörpers definiert sein. Eine externe Definition muß als Funktionsschablone behandelt werden. Danach erfolgt der Name und der Scope-Operator. Eine Definition von push() außerhalb der Klassenschablone würde dann so aussehen: template <class ElType; int sg> Stapel <ElType,sg>& Stapel<ElType,sg>::push(const ElType& w) { inhalt[nachf++] = w; return *this; } Die aktuellen Schablonenargumente werden beim aufruf von push() aus dem Typ des Objekts, auf das die Funktion angewandt wird, abgeleitet: Stapel <char,20> s; s.push('X'); Konstanten ändern ihren Namen nicht, z.B.: template <class ElType, int sg> Stapel <ElType, sg> :: Stapel():nachf(0) {} 188 Die Programmiersprache C++ 3.4 Ein-, Ausgabe 3.4.1 Aufbau Streams Ein-, Ausgabe sind in C++ nicht unmittelbarer Sprachbestandteil. Sie werden durch Klassenbibliotheken (z.B. I/O-Stream-Bibliothek) abgedeckt. Zur Speicherung von Objekten werden besondere Objekte, sog. Streams, verwendet. Datenströme oder Streams bilden ein sehr leistungsfähiges Konzept zur Einbindung von Dateien134, externen Ein-/Ausgabegeräten, aber auch von internen Datenstrukturen in eine Programmierumgebung. Ein Datenstrom ist eine Folge von Objekten: ... Folge von Objekten ... Anfang Position Ende Abb. 3.4-1: Datenströme als Folge von Objekten Zum Lesen und Schreiben benutzt man einen Zeiger, den man in beiden Richtungen auf dem Datenstrom verschieben kann. Man muß nur die Position des Zeigers sowie Anfang und Ende des Datenstroms erkennen. Exemplare der Klasse Stream können Dateien, Eingaben von der Maus und der Tastatur sowie Strings, Arrays und andere Objekte sein. Auf der untersten Ebene wird ein Stream als eine Folge von Bytes aufgefaßt. Das Byte ist die Dateneinheit des Stroms, andere Datentypen wie int, struct, char* oder vector erhalten erst durch Bündelung und Interpretation von Bytesequenzen auf höherer Ebene ihre Bedeutung. Die Basisklasse heißt ios_base, aus ihr werden die anderen Klassen abgeleitet. 134 Ein Stream ist gewissermaßen die objektorientierte Sichtweise einer Datei 189 Die Programmiersprache C++ ios_base basic_ios basic_istream basic_ostrem basic_iostream basic_ostringstream basic_ofstream basic_istringstream basic_stringstream basic_ifstream basic_fstream Abb.: Hierarchie der Klassentemplates für Ein- und Ausgabe Die mit dem Wort „basic“ beginnenden Klassen sind Templates, die für beliebige Zeichentypen geeignet sind (z.B. Unicode-Zeichen). Für den am häufigsten vorkommenden Datentyp char wurden die Klassen durch Typdefinitionen wie typedef basic_ofstream<char> ofstream spezialisiert. ios_base ios istream istringstream ifstream iostream stringstream fstream ostream ostringstream ofstream Abb.: Spezialisierte Klassen für char Die I/O-Stream-Bibliothek Die I/O-Stream-Bibliothek (definiert in iostream.h) besteht aus 2 parallelen Klassenfamilien: Eine ist abgeleitet von streambuf, die andere von ios. Alle StreamKlassen stammen von diesen beiden Klassen ab. Der Zugriff von ios-Klassen auf streambuf-Klassen erfolgt über Zeiger. Die Klasse ios (und daher auch alle von ihr abgeleiteten Klassen) enthält einen Zeiger auf ein streambuf-Objekt. Damit führt sie formatierte Ausgaben und Eingaben bzw. eine Fehlerprüfung durch. streambuf stellt eine Schnittstelle zum Hauptspeicher und den Perepheriegeräten zur Verfügung und bietet allgemeine Methoden zum Aufbau und Bearbeiten von Streams an. Die Methoden der streambuf-Klassenhierarchie werden von den iosKlassen genutzt. Die Klassenhierarchie von streambuf umfaßt: filebuf (Klasse für Dateipuffer), strstreambuf (Klasse für Zeichenketten-Puffer), stdiobuf (Klasse für Standard-I/O-Dateipuffer) Die Klasse iostream kann über Mehrfachvererbung (multiple inheritance) sowohl zur Eingabe als auch zur Ausgabe verwendet werden. 190 Die Programmiersprache C++ ios istream ifstream ostream ofstream iostream istream_withassign ostream_withassign istrstream ostrstream fstream strstream stdiostream streambuf filebuf strstreambuf stdiobuf Abb. 3.4-3: Hierarchie der iostream-Klassen Die iostream-Bibliothek stellt drei Klassen bereit, die den drei Arten von Datenströmen entsprechen: - istream: Eingabeströme ostream: Ausgabeströme iostream: Ein- und Ausgabeströme Ein-, Ausgabe-Operationen können auf Objekten dieser drei Klassen ausgeführt werden. Alle Operationen, die für eine der beiden Klassen istream und ostream erlaubt sind, können auch auf Objekten der Klasse iostream ausgeführt werden. Bspw. ist << für ostream-Objekte definiert, >> für istream-Objekte. Beide sind erlaubt für iostream-Objekte. Vordefinierte Datenströme sind im Header <iostream> folgendermaßen deklariert namespace std { extern istream extern ostream extern ostream extern ostream } cin; cout; cerr; clog; // // // // Standardeingabe Standardausgabe Standardfehlerausgabe gepufferte Standardfehlerausgabe Sie entsprechen den FILE* Dateizeigern stdin, stdout und stderr in C. 191 Die Programmiersprache C++ Eingabe-Klassen Die allgemeine Eingabe-Klasse ist: istream. Spezielle Klassen existieren für Eingabedateien (ifstream), für cin (istream_withassign) und für den zu lesenden Arbeitsspeicher (istrstream). Ausgabe-Klassen Die allgemeine Ausgabe-Klasse ist: ostream. Spezielle Klassen existieren für Ausgabedateien (ofstream), für cout, cerr, clog (ostream_withassign) und für den zu lesenden Arbeitsspeicher (ostrstream). Initialisierungsklasse Sie heißt: Iostream_init und erzeugt cin, cout, cerr und clog. Beim Start eines C++-Programms sind diese Streams definiert, die folgendermaßen als Objekte von withassign-Klassen deklariert sind: extern extern extern extern istream_withassign ostream_withassign ostream_withassign ostream_withassign cin; cout; cerr; clog; // // // // entspricht stdin, Datei-Deskriptor 0 entspricht stdout, Datei-Deskriptor 1 entspricht stderr, Datei-Deskriptor 2 gepuffertes cerr, Datei-Deskriptor 3 3.4.2 Ausgabe 1. Formatierte Ausgabe erfolgt über ostream& ostream::operator<<(Argumenten-Typ) Für den Argumenten-Typ existieren entsprechende Varianten (einschl. char* zur Zeichenketten- und void* zur Adreßwertangabe). Der Rückgabetyp des Operator ist eine Referenz auf ostream. Das Ergebnis ist das ostream-Objekt selbst, so daß ein weiterer Operator darauf angewendet werden kann. Damit ist die Hintereinanderschaltung von Ausgabeoperatoren möglich. Die <<-Operatoren geben eine Referenz auf ihren linken Operanden zurück, so dass mehrfache Insertionen direkt hintereinander möglich sind. Bspw. bedeutet cout << i << j eigentlich (cout << i) << j. Das Ergebnis von cout << i ist cout im Zustand nach der Insertion. Dieser Datenstrom ist dann der linke Operand für die Insertion von j. Je nach auszugebendem Datentyp kann die Standardformatierung auf unterschiedliche Weise beeinflußt werden. Standardformatierung durch Formatstatus-Flags über die Methode flags() Eine ios-Instanzvariable vom Typ long, deren einzelne Bits Formatierungsinformationen tragen, beeinflußt die Darstellungsform. Diverse Formatbits (Formatstatus-Flags) sind135: Bezeichnung: skipws left, right, internal Bedeutung: Trennzeichen136 sind zu ignorieren Ausrichtung der Ausgabe 135 vgl. Borland C++ für Windows Version 4.0: Referenzhandbuch Trennzeichen steht für den englischen Begriff „whitespace“ und bedeutet ein beliebiges Zeichen aus der Menge {‘ ‘,’\t’,’\n’} 136 192 Die Programmiersprache C++ dec, oct, hex showbase showpoint uppercase showpos scientific fixed unitbuf stdio Benutztes Zahlensystem Ausgabe mit Zahlensystempräfixen (Anzeige der Basis) Dezimalpunkt wird erzwungen, nachfolgende Nullen werden ausgegeben Hexadezimalziffern in Großbuchstaben führende ‘+’ bei ganzen Zahlen > 0 Gleitpunktnotation (Exponential-Format) Fixpunktnotation Leeren der Ausgabepuffer nach jeder Einfügeoperation Leeren der Puffer von cout, cerr nach jeder Ausgabeoperation Das Lesen dieser Formatangaben erfolgt mit der Methode: long ios::flags() Verändert können sie u.a. durch long ios::flags(long) werden (mit Rückgabe des ursprünglichen Werts des Formatworts als Funktionswert). Für viele Anwendungsfälle gibt es spezifische Zugriffsfunktionen, z.B.: ios& ios::dec(ios&) ios& ios::oct(ios&) ios& ios::hex(ios&) // dezimale Ausgabe // oktale Ausgabe // hexidezimale Ausgabe Bsp.: Ausgabe ganzer Zahlen in verschiedenen Zahlensystemen #include <iostream.h> int main() { cout.flags(cout.flags() | ios :: showbase); /* setzt das normalerweise ausgeschaltete showbase-Bit, das die Ausgabe des Zahlensystem-Präfix bewirkt: 0 für oktale, x für hexadezimale Zahlen */ cout << "Dezimal " << 21 << "\n"; oct(cout); cout << "Oktal " << 21 << "\n"; hex(cout); cout << "Hexadezimal " << 21 << "\n"; } Standardformatierung durch einzelne Formatstatus-Flags über setf() setf() verwendet eine andere Methode für das Setzen der Formatbits long ios::setf(long setbits, long field) setzt die durch den Parameter field gesetzten Bits des Formatstatusworts auf das durch den ersten Parameter (setbits) angegebene Bitmuster. Alle in field nicht markierten Bits des Formatstatusworts bleiben unberührt. Soll das vorliegende (alte) Bitmuster nur über „bitweises Oder“ mit dem im ersten Parameter angegebenen Bitmuster verknüpft werden, dann kann der Parameter „field“ entfallen. Bsp.: Ausgabe ganzer Zahlen in verschiedenen Zahlensystemen #include <iostream.h> void main() { cout.setf(ios::showbase, ios::showbase); cout << "Dezimal " << 21 << "\n" << oct << "Oktal " << 21 << "\n" << hex << "Hexadezimal " << 21 << "\n"; } 193 Die Programmiersprache C++ Die folgenden drei Konstanten werden für den 2. Parameter der Funktion setf() benötigt: static const long adjustfield; static const long basefield; static const long floatfield; // left | right | internal // dec | oct | hex // scientific | fixed Diese Konstanten dienen im Zusammenhang mit setf()-Aufrufen zur Kontrolle: - für die Adjustierung von Zeichen cout.setf(ios::left,ios::adjustfield); cout.setf(ios::right,ios::adjustfield); cout.setf(ios::internal,ios::adjustfield); Die Aufrufe positionieren die auszugebenden Zeichen innerhalb des über ios::width() angegebenen Felds. - für die Ausgabe von Fließkomma-Werten cout.setf(ios::scientific,ios::floatfield); cout.setf(ios::scientific,ios::floatfield); Als Standard werden Ziffern ausgegeben, wobei n über cout.precision(n) gesetzt wird. „precision()“-Angaben gelten bis zum nächsten precision()Aufruf. - der Basis für die Bestimmung ganzer Zahlen cout.setf(ios::oct,ios::basefield); cout.setf(ios::dec,ios::basefield); cout.setf(ios::hex,ios::basefield); // oktal // dezimal // hexadezimal Falls explizite Ausgabe der Zahlenbasis gewünscht ist, setzt man setf(ios::showbase). Das Flag bleibt solange gesetzt, bis es wieder zurückgesetzt wird, z.B.: cout << 1234 << ' '; cout.setf(ios::oct,ios::basefield); cout << 1234 << ' '; cout.setf(ios::hex,ios::basefield); cout << 1234 << ' '; erzeugt als Ausgabe: 1234 2322 4d2. Mit cout.setf(ios::showbase) vor den angegebenen Anweisungen, ergibt sich die Ausgabe: 1234 02322 0x4d2. Manipulatoren Ein spezieller, funktionsähnlicher Operator (Manipulator) kann zur Änderung von Formatvariablen herangezogen werden. Nicht-parametrisierte Manipulatoren (z.B. dec, hex, und oct) nehmen keine Argumente entgegen und ändern die Konvertierungsbasis dauerhaft, z.B.: int i = 36; cout << dec << i << " " << hex << i << " " << oct << i << endl; cout << dec; // muß auf Dezimalbasis zurückgesetzt werden 194 Die Programmiersprache C++ Für Formatierungsangaben, die ein Argument benötigen, stehen parametrisierte Manipulatoren zur Verfügung. Manipulator boolalpha noboolalpha showbase noshowbase showpoint noshowpoint showpos noshowpos skipws noskipws dec hex oct ws endl ends flush setbase(int n) Aktion true/false alphabetisch ausgeben oder lesen true/false numerisch (1/0) ausgeben oder lesen Basis anzeigen Keine Basis anzeigen Nachfolgende Nullen ausgeben Keine nachfolgende Nullen ausgeben + bei positiven Zahlen ausgeben Kein + bei positiven Zahlen ausgeben Zwischenraumzeichen ignorieren Zwischenraumzeichen berücksichtigen setzt das Basisformat-Flag für Dezimaldarstellung setzt das Basisformat-Flag für Hexidezimaldarstellung setzt das Basisformat-Flag für Oktalkonvertierung entfernt Whitespace-Zeichen fügt einen Zeilenvorschub ein und leert den Stream fügt ein abschließendes Null-Endekennzeichen in einen Stream ein leert einen ostream setzt das Basisformat für die Konvertierung zur Basis n (0, 8, 10, 16). (0 bedeutet Standard: Dezimal für die Ausgabe, ANSI C-Regeln für literale Integer bei der Eingabe setzt die durch f angegebenen Format-Bits löscht die durch f angegebenen Format-Bits setzt das Füllzeichen auf z Festlegen der Gleitkomma-Genauigkeit auf n bestimmt die Feldbreite auf n setiosflags(long f) resetiosflags(long f) setfill(int z) setprecision(int n) setw(int n) Abb. 3.4-4: ios- bzw. iostream-Manipulatoren #include <iostream.h> #include <iomanip.h> void main() { int i = 1311, j cout << setw(6) cout << setw(6) setw(6) } = 1940, k = 10; << i << j << k << endl; << i << << j << setw(6) << k << endl; Eigene Manipulatoren. Die Operatoren << und >> akzeptieren Zeiger auf Funktionen. Falls die „ausgegebene“ bzw. „eingelesene“ Funktion einen der folgenden Prototypen hat, so wird nicht der Zeiger ausgegeben, sondern die Funktion aufgerufen: ios& f(ios& f); istream& f(istream& f); ostream& f(ostream&); Der Manipulator hex hat bspw. eine Funktion, die so aussieht: inline ios& hex(ios& i) { i.setf(ios::hex, ios::dec|ios::hex|ios::oct); return i; } 195 Die Programmiersprache C++ Es ist völlig egal, welche der drei folgenden Anweisungen man benutzt, denn es passiert immer genau dasselbe: cout << hex; cout.operator<< (hex); hex(cout); Eigene Manipulatoren müssen lediglich der Schnittstellenkonvention genügen. Bsp.: Manipulator DM, der dafür sorgt, daß eine nachfolgend ausgegebene Fließkommazahl in einem ordentlichen Währunssymbol erscheint. #include <iostream.h> #include <iomanip.h> ios &fixed(ios &stream) { stream.setf(ios::fixed, ios::floatfield); return stream; } // Manipulator fixed137 ostream &DM(ostream &os) { os << "DM "; os.width(10); os << fixed << setprecision(2); return os; } // Manipulator DM int main() { double x = 234.445; cout << DM << x << endl; } // main() Steuerung der Ausgabefeldbreite durch width() Eine Instanzvariable (vom Typ int) gibt an, wieviele Zeichen von den Ausgabeoperatoren mindestens übertragen werden. Ist diese Zahl 0, dann werden immer genau soviele Zeichen ausgegeben, wie notwendig sind, um den entsprechenden Wert darzustellen. Ist er positiv, werden zu kurze Ausgaben mit Füllzeichen ergänzt. Die Zahl wird durch die Methode int ios::width() gelesen und kann durch int ios::width(int) auf einen neuen Wert gesetzt werden, wobei der alte Wert zurückgegeben wird. Die width-Komponente wird nach jedem Datentransfer, der durch sie beeinflußt wurde, wieder auf Null gesetzt. Ändern des Füllzeichens Das Zeichen138, das zum Auffüllen des Ausgabefelds benützt wird, ist ebenfalls in einer ios-Variablen gespeichert. Es kann durch die Methode char ios::fill() 137 Der Manipulator fixed fehlt beim aktuellen GNU C++ Compiler (Version 2.95), deshalb wird er hier zusätzlich definiert. 138 normalerweise das Leerzeichen 196 Die Programmiersprache C++ abgefragt und durch char ios::fill(int z) auf den Wert ‘z’ gesetzt werden. Diese Funktion gibt das ursprüngliche Füllzeichen zurück. Bsp.: #include <iostream.h> void main() { int i = 64; cout.width(6); cout.fill('#'); cout.setf(ios::left,ios::adjustfield); // adjustfield bestimmt, welche Bits zu setzen sind // ios::left bestimmt, wie sie zu setzen sind cout << i << endl; } Festlegen der Ausgabegenauigkeit Zur Festlegung einer bestimmten Anzahl von Nullkommastellen kann eine weitere ios-Komponente durch die Methode precision() (Lesen einer Anzahl von Nachkommastellen) und precision(int) (Setzen einer Anzahl von Nachkommastellen) benutzt werden. Die Angabe bei precision(int) wird als Maximum interpretiert, d.h.: Es werden überflüssige Nachkommastellen abgeschnitten, jedoch rechts keine Nullen angehängt, falls der Nachkommaausdruck zu kurz ist. Das kann aber erzwungen werden, indem das Formatbit ios::showpoint gesetzt wird. Beschreibungsform für float- und double-Zahlen Bei Gleitpunktzahlen ist die Ausgabe im Gleitpunkt- bzw. Fixpunkt-Format möglich. Dementsprechend ist entweder das Formatbit ios::fixed oder ios::scientific anzugeben. Bsp.: „Fälschungssichere Ausgabe von Geldbeträgen“139 a) Ausgabe über setf() #include <iostream.h> void main() { double betrag = 1311.4; cout.width(10); // 10stelliges Betragsfeld cout.fill('='); // Fuellzeichen zur Faelschungssicherung cout.precision(2); // Genau 2 Nachkommastellen cout.setf(ios::showpoint, ios::showpoint); cout.setf(ios::fixed,ios::floatfield); cout << betrag << "DM"; } b) Ausgabe über parametrisierte Manipulatoren #include <iostream.h> #include <iomanip.h> int main() { double betrag = 1311.4; cout.setf(ios::showpoint, ios::showpoint); cout.setf(ios::fixed,ios::floatfield); 139 PR34205.CPP 197 Die Programmiersprache C++ cout << setw(10) << setprecision(2) << setfill('=') << betrag << "DM"; } 2. Unformatierte (binäre) Ausgabe Sie überträgt einen Speicherbereich 1:1. Das kann mit ostream& ostream::put(char z) für ein Zeichen140 und mit ostream& ostream::write(const char* s, int n) für eine beliebige Anzahl von Zeichen erfolgen. Mit dieser Methode können einfache Datentypen und Strukturen übertragen werden und durch iostream::read() wieder eingelesen werden. Prinzip der unformatierten Ausgabe: Es wird ein Zeiger auf den Beginn des Datenbereichs (Adresse) angegeben. Dabei wird der Zeiger in char* umgewandelt. Zusätzlich wird die Anzahl der zu übertragenden Bytes angegeben. Zur Wandlung des Zeigers wird der reinterpret_cast-Operator verwendet. Dieser Operator verzichtet im Gegensatz zum static_cast-Operator auf jegliche Verträglichkeitsprüfung, weil hier Zeiger auf beliebige (auch selbstgeschriebene) Datentypen in den Typ char* umgewandelt werden sollen. 3. Ausgabe auf Dateien Zur Arbeit mit Dateien werden die Streamklassen ofstream und ifstream verwendet. Gleichzeitige Ein- und Ausgabe mit einer Datei ist mit Hilfe der Klasse fstream möglich, die von iostream abgeleitet ist. ios istream iostream ostream fstreambase ifstream fstream ofstream Abb. 3.4.5: ios-Klassen Die Klassen ofstream, ifstream und fstream sind in fstream.h definiert. putback(char z) erlaubt einem Programm, ein „ungewolltes“ Zeichen in den Strom zurückzuschreiben, damit es an anderer Stelle gelesen werden kann. 140 198 Die Programmiersprache C++ Für die Ausgabe auf Dateien wurde die Klasse ofstream von ostream abgeleitet. Alle bisher behandelten Methoden sind auf ofstream-Objekte anwendbar. Zum Öffnen und Schließen der zum ofstream-Objekt gehörenden Dateien sind folgende spezifische Operationen vorgesehen: void ofstream::open(const char* dateiname, int mode); void ofstream::close(); "mode" bestimmt verschiedene kombinierbare) Werte sind: ios::out ios::ate ios::binary ios::app Öffnungsstrategien. Mögliche (durch "oder" // // // // Datei fuer Ausgabe oeffnen nach dem Oeffnen auf Dateiende positionieren Binaermodus Hängt Daten an- und schreibt immer an das Dateiende ios::trunc // verwirft den Inhalt der Datei, falls sie existiert141 ios::nocreate // Öffnen soll schief gehen, falls die Datei nicht bereits // existiert ios::noreplace // Öffen soll schief gehen, falls die Datei bereits // existiert, es sein denn, dass gleichzeitig ios::ate der // ios::app angegeben wird Mehrere dieser Modi können beim Öffnen über bitweises Oder verknüpft werden. Das Öffnen einer Datei kann auch unmittelbar beim Anlegen einer ofstreamVariablen erfolgen, z.B.: ofstream file("xxxxx.xxx",ios::out)142 Ob eine Datei offen ist oder nicht, kann mit der (booleschen) Methode is_open() festgestellt werden. Der Vorgabewert für ofstream-Dateien ist ios::out. Dateien werden nach Voreinstellung im Text-Modus geöffnet. Das bedeutet: Die Eingabe von "carriage return/linefeed" wird in '\n'-Zeichen umgewandelt bzw. die Ausgabe von '\n'-Zeichen in "carriage return/linefeed". Die Angabe ios::binary öffnet die Datei im binären Mode. 4. Schreiben in Zeichenketten Es gibt die Klasse ostrstream für das Schreiben in Zeichenketten. Der ostreamKonstruktor verlangt die Angabe einer Größe, damit nicht über das Ende der Zeichenkette hinaus geschrieben wird. Wenn Daten in den Strom hineingeschrieben werden, wird kein Begrenzungszeichen (ASCII-Null) hinzugefügt. Wenn man eines anhängen möchte, muß man das angeben. Formatierung in Zeichenketten im ANSI/ISO-Standard. Voraussetzung ist das Vorhandensein der Klasse string, definiert im Header <string>. String-Streams benötigen keine C-Zeichenkette mehr als Grundlage und können nicht überlaufen. 141 142 implizit, wenn ios::out angegeben ist oder weder ios::ate noch ios::app angegeben wird Konstruktor 199 Die Programmiersprache C++ 3.4.3 Eingabe 1. Formatierte Eingabe erfolgt über: istream& istream::operator>> (Argumenten-Typ&) istream& istream::operator>> (char*); Die Stream-Eingabe verwendet den überladenen Rechtsschiebe-Operator >> und funktioniert ähnlich wie die Stream-Ausgabe. Der linke Operand von >> ist ein Objekt der Klasse istream. Der rechte Operand kann ein beliebiger Typ sein, für den die Stream-Eingabe definiert wurde. Standardmäßig überspringt der Operator >> Whitespace-Zeichen und liest dann die Zeichen ein, die zum Typ des EingabeObjekts passen. Das Ignorieren von Whitespace-Zeichen wird durch das Flag ios::skipws in der Formatstatus-Auflistung gesteuert. Es gibt außerdem noch den speziellen „Ziel“-Manipulator ws, mit dem Whitespace-Zeichen entfernt werden können. Auf den Typ char besitzt der Opeartor >> die Wirkung, daß WhitespaceZeichen übersprungen und das nächste (Nicht-Whitespace-) Zeichen gespeichert wird. Auf den Typ char* (String) besitzt der Operator >> die Wirkung: Führende Whitespace-Zeichen werden ignoriert, dann werden (Nicht-Whitespace-) Zeichen eingelesen, bis wieder ein Whitespace-Zeichen folgt. Danach wird ein abschließendes Null-Endekrtrium angehängt. Ein Überlauf des „char-Arrays“ kann durch die ios-Methode width() bzw. durch den Manipulator setw auf die gewünschte Eingabefeldbreite eingeschränkt werden. Wird durch setw die Breite auf ‘b’ beschränkt, dann werden maximal (b-1) Zeichen übertragen, da das abschließende Null-Zeichen auch noch untergebracht werden muß. Jeder Eingabevorgang überspringt normalerweise erst evtl. vorliegende Trennzeichen, liest dann alle Zeichen vom Stream, die zur Darstellung eines Werts vom entsprechenden Datentyp gehören, wandelt die Darstellung in eine interne Repräsentation um und belegt den Referenzparameter, also den rechten Operanden des Eingabeoperators, mit diesem Wert. Sollte dabei ein Fehler auftreten, werden entsprechende Fehlerbits gesetzt: ios::failbit ios::badbit ios::eofbit // bedeutet allgemein: Der Einlesevorgang ist gescheitert // bedeutet: unerlaubte E/A-Operation versucht // bedeutet: Der Stream war vorzeitig erschöpft Der nach außen nicht sichtbare Ablauf einer Tastaturabfrage mit „cin >> “ besteht aus mehreren Schritten: 1. Aufforderung des (Betriebs-) Systems zur Zeichenübergabe 2. Eingabe der Zeichen auf der Tastatur (mit Korrekturmöglichkeit durch die Backspace-Taste). Die Zeichen werden vom System der Reihe nach in einem besonderen Speicherbereich abgelegt. 3. Abschluß der Eingabe mit der RETURN-Taste. Damit wird das '\n'-Zeichen als Zeilenendekennung im Tastaturpuffer abgelegt, der Puffer wird durch das System an C++ übergeben. 4. Auswertung des Tastatur-Puffers durch den Operator >> je nach Datentyp der gefragten Variable. 5. Daten, die nach der Auswertung übrig bleiben, weil sie nicht zu dem Datentyp passen, verbleiben im Tastaturpuffer und können mit dem nächsten „cin >> ...“ gelesen werden. 2. Für die unformatierte Eingabe stehen verschiedene Methoden zur Verfügung, die unabhängig vom skipws-Status keine Trennzeichen überlesen. 200 Die Programmiersprache C++ int istream::get() liest ein Zeichen, und gibt es als int-Wert entsprechend seiner Position in der ASCIITabelle zurück. Bei EOF wird „-1“ zurückgeliefert, z.B. char z; while ((c=cin.get()) != EOF) { // Verarbeitung von z } istream& istream::get(char& z) liest ein Zeichen und überträgt es in die Variable z. Der Rückgabewert ist dann der Datenstrom, aus dem gelesen wird. Dessen boolscher Wert kann dann wieder geprüft werden, z.B.: char z; while(cin.get(z)) { // Einlesen mit Prüfung auf Fehler bzw. Dateiende // Verarbeitung von z } istream& istream::get(char* s, int n, char delim = '\n') überträgt der Reihe nach Zeichen in den durch „s“ angegebenen Puffer, bis eine der folgenden Bedingungen auftritt, die in der angegebenen Reihenfolgegeprüft werden: a) n-1 Zeichen wurden übertragen b) ein Lesefehler ist aufgetreten c) das nächste zu lesende Zeichen ist „delim“ (Begrenzungs-, Trennzeichen) In jedem Fall wird die gelesene Zeichenkette durch ein Nullbyte abgeschlossen. istream& istream::getline(char*s, int n, char delim = '\n') entspricht get(), überträgt jedoch auch noch das Zeichen „delim“, falls die 3. Bedingung auftritt143. Wird der Lesevorgang aufgrund der 1. Bedingung abgebrochen, dann wird „ios::failbit“ gesetzt. dadurch wird angedeutet: Das gewünschte Trennzeichen wurde nicht gefunden. Der Aufruf von getline() liest, bis maximal n-1 Zeichen oder bis ein Zeilen-Endezeichen (oder das Dateiende) erreicht werden. Die Methode schreibt immer ein Null-Terminierungszeichen an das Ende der Zeichenkette. Nach einem Einlevorgang mit getline() kann man mit der Methode gcount() abfragen, wie viele Zeichen verarbeitet wurden. int peek() erlaubt einem Programm, das nächste zu lesende Zeichen zu untersuchen, ohne daß das Resultat der folgenden Operation beeinflußt wird. istream& istream::ignore(int n = 1, int delim = EOF) überliest n Zeichen, bricht jedoch auch ab, nachdem „delim“ gelesen wurde. Gilt „n == MAXINT“ wird nur auf „delim“ geachtet. istream& istream:: read(char* s, int n) (Gegenstück zu ostream::write()). Liest n Zeichen und überträgt diese in die Zeichenkette „s“. Es werden keine Zeichen überlesen, der Abschluß durch ein Nullbyte unterbleibt. Kann die Anforderung nicht erfüllt werden, wird das ios::failbit gesetzt. 143 Das Trennzeichen wird gelesen, jedoch nicht in den Puffer übernommen. 201 Die Programmiersprache C++ int istream::gcount() gibt die Anzahl der zuletzt übertragenen Zeichen zurück, falls es sich dabei um eine unformatierte Eingabe gehandelt hat 3. Eingabe aus Dateien Die Methoden open(), close() und is_open() (sowie die Konstuktoren und der Destruktor) entsprechen ihren ofstream-Gegenstücken. Öffnungsstrategien sind: ios::in ios::binary //Standard 4. Eingabe aus Zeichenketten Die Klasse istrstream dient zur Extraktion einzelner Teile aus einer Zeichenkette. Sie verfügt über 2 Konstuktoren: istrstream::istrstream(char* s, int n) Der Stream wird mit einer maximal n Zeichen langen Zeichenkette verknüpft. istrstream::istrstream(char* s) In diesem Fall wird angenommen, daß die dem Stream zugeordnete Zeichenkette s durch Nullzeichen abgeschlossen ist. Der Zugriff auf den Inhalt eines istrstream-Objekts kann durch die Methode str() erfolgen. 202 Die Programmiersprache C++ 3.4.4 Formatierung in Zeichenketten 3.4.4.1 strstream für C++ im AT&T-Standard Die Header-Datei strstream.h enthält die notwendigen Methoden zur formatierten Ausgabe von Zeichenketten, d.h. das, was man in C mit sprintf() macht. Man kann damit in binäre Dateien formatiert in Zeichenketten schreiben oder aus ihnen auch lesen. 3.4.4.2 stringstream für C++ im ANSI/ISO-Standard stringstream-Klassen sind im Header <sstream> deklariert. Die hier beschriebenen String-Streams benötigen keine C-Zeichenkette mehr als Grundlage. Voraussetzung für String-Streams der stringstream-Klassen sind C++-Strings der Klasse string. Bsp.: 203 Die Programmiersprache C++ 3.4.5 Fehlerzustände Eine Instanzvariable in jedem ios-Objekt gibt über evtl. Fehlerzustände Auskunft. Sie kann die im Aufzählungstyp iostate definierten booleschen Zustandsgrößen annehmen, die Zugriffsmethoden rdstate(() und clear() können den Zustandswert lesen bzw. schreiben. enum iostate { goodbit = 0x00, eofbit = 0x01, failbit = 0x02, badbit = 0x04 }; // // // // alles OK Ende des Streams letzte Ein- / Ausgabe war fehlerhaft ungueltige Operation, schwerer Fehler Ein Stream kann nicht mehr benutzt werden, falls badbit gesetzt ist. Folgende Elementfunktionen haben Zugriff auf die Statusbits: Funktion Ergebnis iostate rdstate() bool good() bool eof() bool fail() bool bad() void clear() void clear(iostate s) void setstate(iostate) Aktueller Status Wahr, falls gut Wahr, falls Dateiende Wahr, falls failbit oder badbit gesetzt ist Wahr, falls badbit gesetzt ist Statusbit auf goodbit setzen Statusbits auf s setzen Einzelne Statusbits setzen Zum Abfragen des Fehlerzustands gibt es außerdem die Operatoren bool ios :: operator !() ios :: operator void*() // überladener Negationsoperator // Typumwandlungsoperator Diese Operatoren werden vom Compiler zur Auswertung von Bedingungen genutzt. Sie verwenden die Funktion fail() zum Lesen des Dateistatus. Damit sind die folgenden Konstruktionen erlaubt: if (!cin) .... bzw. while (cin) //aequivalent zu if (cin.fail()) ... // entspricht while (!cin.fail()) ... Auf End-Of-File kann man zusätzlich mit der Methode eof() prüfen. 204 Die Programmiersprache C++ 3.4.6 Positionieren in Dateien ifstream- und ofstream-Ströme besitzen je einen (logischen) Zeiger, an dem die nächste Lese- und Schreiboperation stattfindet ("cp", current pointer). Dieser Zeiger kann vom Programmierer abgefragt und gesetzt werden. // Streampositionen zum Setzen und Lesen des current pointer // Lesen und Setzen des Lesezeigers streampos tellg(); // Lesen und Setzen des Schreibzeigers streampos tellp(); ostream& seekp(streampos); Der Zeiger ist vom Typ streampos. Der Typ ist in fstream.h in der Regel als long definiert. Arithmetik mit streampos-Werten ist aber als eine spezielle Form der seekp- bzw. seekg-Funktionen möglich. Falls ein zweiter Parameter für diese Funktion angegeben ist, wird der erste Parameter als (numerischer) Offset von seinem Ausgangspunkt gerechnet und der zweite Parameter gibt den Ausgangspunkt an. // Streamfunktionen zum relativen Setzen des current pointer // (cp) einer Datei istream& seekg(streamoff,seek_dir); // Lesezeiger ostream& seekp(streamoff,seek_dir); // Schreibzeiger Die für seek_dir möglichen Konstanten sind als Aufzählungstyp in der Klasse ios definiert: enum seek_dir { beg = 0, // Offset von Dateibeginn rechnen cw = 1, // Offset vom augenblicklichen cp rechnen end = 2 // Offset vom Dateiende rechnen } 205 Die Programmiersprache C++ 3.5 Ausnahmebehandlung 3.5.1 Übliche Fehlerbehandlungsroutinen Fehler treten häufig in Funktionen auf und können dort nicht behoben werden. Der Aufruf der Funktion muß in Kenntnis von dem Fehler gesetzt sein, damit der Fehler abgefangen oder weiter („nach oben“) gemeldet werden kann. Zu einer Fehlerbehandlung können eine Reihe verschiedener Strategien herangezogen werden: - - - Programmtechnisch am einfachsten ist der sofortige Programmabbruch innerhalb der Funktion, die einen Fehler feststellt. Falls keine Meldungen über den Abbruch ausgegeben werden, ist die Fehlerdiagnose erschwert. Üblich ist zur Fehlerbehandlung die Übergabe eines Parameters an den Aufrufer der Funktion, der Auskunft über Erfolg oder Mißerfolg der Funktion gibt. Der Parameter ist nach jedem Funktionsaufruf abzufragen. Eine Funktion kann im Fehlerfall die in der C-Welt übliche globale Variable errno setzen, die dann abgefragt wird. Globale Variable beeinträchtigen jedoch die Portabilität von Funktionen. Eine Funktion, die einen Fehler feststellt, kann eine andere Funktion zur Fehlerbehandlung aufrufen, die gegebenenfalls auch den Programmabbruch herbeiführt. 3.5.2 Schema zur Ausnahmebehandlung C++ bietet das folgende Schema für eine Ausnahmebehandlung an: try { funktion(); /* Falls funktion() einen Fehler entdeckt, wirft sie eine Ausnahme aus (throw), wobei ein Objekt fuer einen Anstoß einer geeigneten Fehlerbehandlungsroutine uebergeben werden kann */ } catch (datenTyp1) { // durch ausgeworfenes Objekt vom datenTyp1 ausgewaehlte Fehlerbehandlung // ... } catch (datenTyp2) { // durch ausgeworfenes Objekt vom datenTyp2 ausgewaehlte Fehlerbehandlung // ... } // gegebenenfalls weitere catch-Blöcke // ... // Fortsetzung des Programms nach Fehlerbehandlung an dieser Stelle // ... Bsp.: Die folgende Funktion liest „n“ ganze Zahlen in einen Array ein und berechnet den Durchschnitt der eingelesenen, ganzen Zahlen. #include <iostream.h> #include <except.h> 206 Die Programmiersprache C++ float liesGanzzahlenArray(int* x, int n) { float sum = 0; cout << "Eingabe von " << n << " ganzen Zahlen:\n"; for (int i = 0; i < n; i++) { cin >> x[i]; if (cin.fail()) throw i; sum += x[i]; } return sum / n; } Tritt beim Einlesen ein Fehler auf, dann löst die Funktion über einen Aufruf von throw eine Ausnahme aus. Die Laufzeitbibliothek des verwendeten Compilers durchsucht in diesem Fall die Aufrufkette auf dem Stack 144, bis ein zum aktuellen throw korrespondierender catch gefunden wurde. Der entsprechende catch-Block fängt ausgelöste Ausnahmen auf und ermöglicht eine Behandlung des aufgetretenen Fehlers. Bei der Ausführung der throw-Anweisung werden automatisch alle auf dem Stack angelegten Objekte durch den Aufruf ihres Destruktors gelöscht, und die Aufrufparameter werden vom Stack entfernt. Der zu der vorliegenden Funktion übergeordnete Programmteil könnte so aussehen: int main() { float durchschnitt; int a[10], b[12]; cout << "Ueberpruefe das Geschehen, wenn bei der Eingabe\n" << "nicht korrekte Zeichen (Ziffern) eingegeben wurden\n"; try { durchschnitt = liesGanzzahlenArray(a,10); cout << "10 ganze Zahle wurden eingelesen. Durchschnitt: " << durchschnitt << endl; durchschnitt = liesGanzzahlenArray(b,12); cout << "12 ganze Zahle wurden eingelesen. Durchschnitt: " << durchschnitt << endl; } catch(int k) { cout << "Nur " << k << " ganze Zahlen wurden gelesen\n"; } cout << "Ende des Programms"; } Zur Reaktion auf Ausnahmen muß ein Programmteil die Routinen, in denen diese Ausnahmen auftreten, in einem sog. „try“-Block145 einschließen. Direkt nach dem try-Block muß ein catch-Block folgen, der die eigentliche Ausnahmebehandlung vornimmt. Ein catch-Block darf nur direkt nach einem try-Block oder einem anderen catch-Block stehen. Zur Reaktion auf alle möglichen Ausnahmen könnte der catch-Block folgendermaßen gestaltet werden: catch(...) { ........ 144 Das geworfene Ausnahmeobjekt muß allerdings statisch oder global sein, damit es nicht durch die StackAbwicklung bis zum Fangen der Ausnahme, d.h. die Ausnahmebehandlung zerstört wird. 145 throw, try, catch sind C++-Schlüsselworte 207 Die Programmiersprache C++ } Durch die Ellipse (...) wird ein catch-Block spezifiziert, der generell alle Ausnahmen behandelt. Catch-Blöcke mit Ellipse müssen immer die letzten in einer Folge von catch-Blöcken sein. Wurde eine Ausnahme aufgefangen, ist sie abgeschlossen. Das bedeutet: kein weiterer catch-Block wird mit der Ausnahme aufgerufen. Ein catch-Block kann jedoch eine Ausnahme weiterreichen. Stellt ein derartiger Block fest, daß er den aufgetretenen Fehler nicht beheben kann, ruft er noch einmal throw auf. Durch die parameterlose Variante von throw wird die aktuelle Ausnahme erneut ausgelöst. Ist zur Zeit keine Ausnahme aktiv, wird unexspected aufgerufen. 3.5.3 Exception-Hierarchie C++ stellt eine Reihe vordefinierter Exception-Klassen zur Verfügung. Alle spezielle Exception-Klassen erben von der Basisklasse exception. exception logic error runtime_error bad_alloc invalid_argument range_error bad_typed length_error overflow_error bad_cast out_of_range underflow_error bad_exception domain_error Abb.: Exception-Hierarchien Die Klasse Exception besitzt die Schnittstelle namespace std { class exception { private: // ... public: exception() throw(); exception (const exception&) throw(); exception operator = (const exception&) throw(); virtual ~exception throw(); virtual const char* what() const throw(); }; } throw() nach den Deklarationen bedeutet: Die Klasse wirft selbst keine Exception aus, da es sonst eine unendliche Folge von Exceptions geben könnte. Die Methode what() gibt einen Zeiger auf char zurück, der auf eine Fehlermeldung verweist. Eigene Exception-Klassen können durch Vererbung die Schnittstelle übernehmen, z.B.: 208 Die Programmiersprache C++ namespace std; { class logischer_Fehler : public exception { public: explicit logic_error(const string& argument); } } 3.5.4 Besondere Fehlerbehandlungsroutinen Es kann vorkommen, daß mitten in einer Fehlerbehandlung selbst wieder Fehler auftreten, Dann werden folgende Funktionen aufgerufen: terminate() Die Funktion „void terminate()“ wird u.a. aufgerufen, falls - der Exception-Mechanismus keine Möglichkeit zur Bearbeitung einer geworfenen Exception findet der vorgegebene unexspected_handler() aufgerufen wird ein Destruktor während des Aufräumens eine Exception wirft ein statisches (nicht lokales) Objekt während der Konstruktion oder Zerstörung eine Exception wirft Die Standardimplementierung von terminate() beendet das Programm. unexspected() Die Funktion „void unexspected“ wird aufgerufen, falls eine Funktion ihre Versprechungen nicht einhält und eine Exception auswirft, die in der ExceptionSpezifikation nicht aufgeführt wird. Unexspected() kann nun selbst eine Exception auslösen, so daß die Suche nach dem geigneten Exception-Handler weitergeht. Falls die ausgelöste Exception nicht der Exception-Spezifikation entspricht, sind 2 Fälle zu unterscheiden: 1. Die Exception-Spezifikation führt bad_exception auf: Die geworfene Exception wird durch ein bad_exception-Objekt ersetzt. 2. Die Exception-Spezifikation führt bad_exception nicht auf: terminate() wird aufgerufen uncaught_exception() Die Funktion bool uncaught_exception() gibt nach Auswertung der Exception true zurück, bis die Initialisierung im Exception-Handler abgeschlossen ist oder unexspected() wegen der Exception aufgerufen wurde. „true“ wird auch zurückgegeben, wenn terminate() aus irgendeinem Grunde außer dem direkten Aufruf begonnen wurde. Benutzerdefinierte Fehlerbehandlungsfunktionen Die zuvor angegebenen Funktionen können bei Bedarf selbst definiert werden. Dazu sind standardmäßig zwei Typen für Funktionszeiger definiert: typedef void (*unexspected handler)(); typedef void (*terminate_handler)(); Es gibt zwei Funktionen, denen die Zeiger auf die selbstdefinierten Funktionen dieses Typs übergeben werden, um die vorgegebenen Funktionen zu ersetzen: unexspected_handler set_unexspected(unexpected_handler f) throw(); 209 Die Programmiersprache C++ terminate_handler set_terminate(terminate_handler f) throw(); Übergeben werden Zeiger auf selbstdefinierte Funktionen, an die bestimmte Anforderungen gestellt werden: - Ein unexspected_handler soll -- bad_exception oder eine der Exception_Spezifikation genügende Exception auswerfen oder -- terminate() rufen oder -- das Programm mit abort() oder exit() beenden. - Ein terminate_handler soll das Programm ohne Rückkehr an den Aufrufer beenden. 3.5.5 Unbehandelte Ausnahmen Wird im Laufe eines Programms eine Ausnahme ausgelöst, für die kein korrespondierender catch-Block verfügbar ist, ruft die Laufzeitbibliothek unexspected auf. Standardmäßig führt unexspected einen Aufruf der Routine terminate146 aus, die durch Aufruf von abort das Programm beendet. Das Standarverhalten von unexspected und terminate kann geändert werden. Das geschieht über einen Aufruf von set_unexspected. Dadurch wird eine Behandlungsroutine definiert, die prinzipiell beliebige Aktionen ausführen darf, wobei jedoch nicht zur originären unexspected-Funktion zurückgesprungen werden darf. Bsp.: #include <iostream.h> #include <except.h> typedef void (*funk)(); // Definition eines Funktionstyps void f() throw() // Verzicht auf Ausnahmen { // Soll keine Exceptions ausloesen, macht es aber doch throw "Die Ausnahme, die die Regel bestaetigt"; } void nasowas() // wird anstelle von terminate() benutzt { cout << "Damit haben Sie nicht gerechnet" << endl; throw; // reaktiviert die aktuelle Ausnahmebedingung } void main() { /* Installation einer Funktion ueber set_unexpected,die beim Auftreten einer Exception aufgerufen wird, obwohl sie in der Exception-Behandlung des auslösenden Objekts nicht aufgeführt ist. Bei dieser Funktion handelt es sich um eine Funktion ohne Argumente mit dem Rueckgabetyp void */ funk orig = set_unexpected(nasowas); /* set_unexpected liefert die zuvor installierte Funktion zurueck. terminate() wird demnach in orig gemerkt */ try { f(); } // Behandlung von Ausnahmen aller Art catch(...) 146 Die Funktionsprototypen und Definitionen dieser Routinen befinden sich in except.h 210 Die Programmiersprache C++ { cout << "Hoppla!, jetzt komm ich\n"; } set_unexpected(orig); // Wiederherstellung des ursprünglichen Status } Analog dazu läßt sich terminate() durch eine Aufruf von set_terminate() mit einem Zeiger auf eine parameterlose void-Funktion durch eine selbstdefinierte Funktion ersetzen. Die übergebene Funktion wird ab Ersetzungszeitpunkt von terminate() an Stelle von abort() aufgerufen. Auch hier darf nicht zur Originalroutine zurückgesprungen werden. Die Funktion terminate() wird nicht nur von unexspected() aufgerufen, sondern auch aktiviert, wenn - für eine Ausnahme keine passende catch()-Routine existiert der Ausnahmebehandlungsmechanismus den Laufzeit-Stack in einem nicht konsistenten Zustand vorfindet und ihn daher nicht ordnungsgemäß abbauen kann. ein beim Stack-Abbau aktivierter Destruktor seinerseits eine Ausnahme signalisiert. 211 Die Programmiersprache C++ 4. Templates für Algorithmen und Datenstrukturen 4.1 Darstellung von Algorithmen für Graphen mit sequentiell gespeicherte Listen 4.1.1 Die Datenstruktur Graph Grundlagen Graphen werden häufig zur Darstellung von Problemsituationen herangezogen. So zeigt der folgende (ungerichtete) Graph die Verkehrsverbindungen zwischen Städten: 752 604 763 648 504 432 355 Abb. Ein (ungerichteter) Graph Ein (ungerichteter) Graph besteht aus Knoten und Kanten. Knoten tragen in der Regel zur Identifikation eine Knoten-Identifizierung. Kanten können gewichtet sein (z.B. mit Entfernungsangaben). Beim gerichteten Graphen ersetzen Pfeile die Kanten. Daduch ist die ein Fluß in Richtung des Pfeils bestimmt. Im gerichteten Graphen wird die Kante durch ein Paar (Vi,Vj) beschrieben. Vi ist der Start- und Vj der Endknoten. Ein Pfad P(VS,VE) ist eine Folge von Knoten: VS ist der Startknoten, VE ist der Endknoten und jedes in dem Pfad aufgeführte Paar ist eine Kante. Darstellung Es gibt eine Reihe gebräuchlicher Darstellungen. Am häufigsten sind Adjazensmatrix und Adjazenstabelle. 212 Die Programmiersprache C++ 2 A B 1 B 4 3 A 5 C C E 7 D E D Adjazens-Matrizen: 0 0 0 0 0 2 0 4 0 3 1 5 0 7 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 1 1 0 0 1 1 0 0 0 0 0 0 0 1 0 Adjazentabellen (-listen) A B 2 B C C C 1 A B C 5 B A C B 4 C A D C 7 E B 3 D E Adjazensmatrizen und Adjazenstabellen 213 E C D Die Programmiersprache C++ 4.1.2 Die STL-Containerklasse vector zur Implemetierung einer Knotenliste für Graphen Anstelle des veralteten C-Array stellt die STL eine Klassenschablone (template class T> vector)147 zur Aufnahme und Verwaltung sequentiell gespeicherter Daten bereit. Diese STL-Containerklasse wird zur Darstellung eines Graphen (Knoten in einer sequentiellen Liste) benutzt. #ifndef GRAPH_CLASS #define GRAPH_CLASS #include #include #include #include <iostream> <fstream> <vector> <queue> using namespace std; const int maxGraphSize = 25; template <class T> class Graph { private: // Schluesseldaten mit einer Knotenliste, Adjazenzmatrix // und aktueller Groesse (Knotenanzahl) des Graphen vector<T> vertexList; int edge [maxGraphSize][maxGraphSize]; int graphsize; // Methoden zum Finden eines Knoten und Identifizieren // seiner Position in der sequentiellen Liste bool findVertex(vector<T> &L, const T& vertex); int getVertexPos(const T& vertex); public: // Konstruktor Graph(void); // Testmethoden int graphEmpty(void) const; int graphFull(void) const; // Zugriffsmethoden int numberOfVertices(void) const; int getWeight(const T& vertex1, const T& vertex2); vector<T>& getKnoten(); vector<T>& getNachbarn(const T& vertex); // Modifikationsmethoden void insertVertex(const T& vertex); void insertEdge(const T& vertex1, const T& vertex2, int weight); void DeleteVertex(const T& vertex); void DeleteEdge(const T& vertex1, const T& vertex2); // Anwendungsmethoden void readGraph(char *filename); int minimumPath(const T& sVertex, const T& eVertex); // SeqList<T>& DepthFirstSearch(const T& beginVertex); // SeqList<T>& BreadthFirstSearch(const T& beginVertex); }; Daten zum Graphen werden aufgenommen in: - einer sequentiellen Liste: vector<T> vertexList 147 vgl. 2.2.3 214 Die Programmiersprache C++ - in einer zweidimensionalen Matrix: int edge [maxGraphSize][maxGraphSize] Methoden im Graphen dienen - zum Einlesen der Knotenidentifikationen und Kantenbewertungen aus einer Datei: void readGraph(char *filename);. Der Parameter dieser Methode beschreibt einen Dateinamen. Die zugehörige Datei wird mit der Methode readGraph eingelesen. Die Datei hat bspw. folgenden Aufbau: 5 A B C D E 14 A B A C A D B C D C D E E C B A C A D A C B C D E D C E 604 604 648 648 752 752 432 432 763 763 504 504 355 355 Diese Datei ist folgendem Graphen zugeordnet: A 752 604 D B 648 504 432 763 E 355 C - zum Auffinden eines Knoten in der Knotenliste bool findVertex(vector<T> &L, const T& vertex) Implementierung // Konstruktor zum Initialisieren der Eintraege in der Adjazenzmatrix // mit 0, Setzen der Groesse des Graphen auf 0 template <class T> Graph<T>::Graph(void) { for (int i = 0; i < maxGraphSize; i++) for (int j = 0; j < maxGraphSize; j++) edge[i][j] = 0; 215 Die Programmiersprache C++ graphsize = 0; } // Zählen der Komponenten template <class T> int Graph<T>::numberOfVertices(void) const { return graphsize; } // Test, ob der Graph leer ist template <class T> int Graph<T>::graphEmpty(void) const { return graphsize == 0; } // Durchlaufen der Knotenliste und Ermitteln der Position des im Parameter der Funktion // angegebenen Knotens in der Knotenliste template <class T> int Graph<T>::getVertexPos(const T& vertex) { int pos = 0; int i = 0; for (i = 0; i < vertexList.size(); i++) { if (vertexList[i] == vertex) { pos = i; break; } } if (i == vertexList.size()) { cerr << "getVertex: Der Knoten ist nicht im Graph." << endl; pos = -1; } return pos; } // Ermitteln der Gewichte (Kantenbewertungen) der Kante zwischen den // in der Parameterliste der Funktion getWeight() angegebenen Knoten vertex1 und vertex2. template <class T> int Graph<T>::getWeight(const T& vertex1, const T& vertex2) { int pos1 = getVertexPos(vertex1), pos2 = getVertexPos(vertex2); if (pos1 == -1 || pos2 == -1) { cerr << "getWeight: Ein Knoten ist nicht im Graph." << endl; return -1; } return edge[pos1][pos2]; } // Rueckgabe einer Liste mit allen Knoten der Knotenliste template <class T> vector<T>& Graph<T>::getKnoten() { vector<T> *l; l = new vector<T>; for (int i = 0;i < vertexList.size();i++) l -> push_back(vertexList[i]); return *l; } 216 Die Programmiersprache C++ // Rueckgabe einer Liste mit allen benachbarten Knoten template <class T> vector<T>& Graph<T>::getNachbarn(const T& vertex) { vector<T> *l; // Zuweisen leere Liste l = new vector<T>; // Positionsbestimmung zur Zeilen-Identifikation in der // Adjazenzmatrix int pos = getVertexPos(vertex); // Terminiere, falls der Knoten nicht in der Knotemliste ist if (pos == -1) { cerr << "getNachbarn: Der Knoten ist nicht im Graph." << endl; return *l; // Rueckgabe leere Liste } // Durchlaufe die Zeilen der Adjazenzmatrix und erfasse // alle Knoten einer nicht mit Null bewerteten Kante for (int i = 0; i < graphsize; i++) { if (edge[pos][i] > 0) l->push_back(vertexList[i]); } return *l; } // Aufsuchen eines Knoten in der Knotenliste template <class T> bool Graph<T>::findVertex(vector<T> &L, const T& vertex) { vector<T>::iterator vecIter; bool ret = false; for (vecIter = L.begin(); vecIter != L.end(); vecIter++) { if (*vecIter == vertex) { ret = true; break; } } return ret; } // Einfügen eines Knoten template <class T> void Graph<T>::insertVertex(const T& vertex) { if (graphsize + 1 > maxGraphSize) { cerr << "Graph ist voll" << endl; exit (1); } vertexList.push_back(vertex); graphsize++; } // Einfügen einer Kante template <class T> void Graph<T>::insertEdge(const T& vertex1, const T& vertex2, int weight) { int pos1 = getVertexPos(vertex1), pos2 = getVertexPos(vertex2); if (pos1 == -1 || pos2 == -1) { 217 Die Programmiersprache C++ cerr << "insertEdge: ein Knoten ist nicht im Graph." << endl; return; } edge[pos1][pos2] = weight; } // Einlesen der Daten in den Graphen template <class T> void Graph<T>::readGraph(char *filename) { int i, nvertices, nedges; T s1, s2; int weight; ifstream f; f.open(filename, ios::in | ios::nocreate); if(!f) { cerr << "Nicht zu oeffnen " << filename << endl; exit(1); } f >> nvertices; for (i = 0; i < nvertices; i++) { f >> s1; insertVertex(s1); } f >> nedges; for (i = 0; i < nedges; i++) { f >> s1; f >> s2; f >> weight; insertEdge(s1, s2, weight); } f.close(); } 4.1.3 Mehrdimemensionale Felder Ein mehrdimensinales Feld wurde zur Speicherung der Bewertungen der Kanten im ADT Graph benutzt. 218 Die Programmiersprache C++ 4.1.4 Durchlaufen von Graphen mit Hilfe der STL-Containerklassen stack bzw. queue 4.1.4.1 Tiefensuche (First-Depth Search) Bei der Verarbeitung von Graphen treten häufig folgende Fragen auf: - Ist der Graph zusammenhängend? - Wenn nicht, was sind seine zusammenhängenden Komponenten? - Enthält der Graph einen Zyklus? Diese und viele andere Probleme können mit einer Methode gelöst werden, die Tiefensuche genannt wird und einen natürlichen Weg darstellt, wie im Graphen systematisch jeder Knoten "besucht" und jede Kante geprüft werden kann. Suchalgorithmus zur Tiefensuche: Er benutzt eine Liste für die Verwaltung aufgesuchter Knoten eine Liste und einen Stapel (stack der STL-Containerklasse). 4.1.4.2 Breitensuche (Breadth-First Search) Benutzt man zur Speicherung der Knoten, die bei der Suche im Graphen durchlaufen werden, anstatt eines Stapels eine Schlange (z.B. den STL-Container queue), dann führt das zu einem weiteren Algorithmus für die Traversierung in Graphen, die Breitensuche genannt wird. 4.1.5 Ermitteln der kürzesten Wege mit Hilfe der STL-Containerklasse priority_queue Erstellen einer neuen Klasse (struct) mit dem Namen PathInfo Objekte dieser Klasse spezifizieren Pfade, die zwei Knoten über eine Kante oder mehrere Kanten verbinden. Die Gewichte der zwischen den Knoten befindlichen Kanten werden aufsummiert. Die Pfad-Informationen werden in eine Priority-Queue der STL-Containerklasse priority_queue abgelegt. Dadurch ist Direktzugriff auf das Pfadobjekt mit den geringsten Kosten möglich: template <class T> { T startV, endV; int cost; bool operator < { return cost < bool operator > { return cost > }; struct PathInfo (const PathInfo<T> a) const a.cost; } (const PathInfo<T> a) const a.cost; } 219 Die Programmiersprache C++ Algorithmus Gegeben ist der folgende Graph 4 E 4 A B 2 8 6 10 4 12 6 6 12 F D C 20 14 Abb.: Startknoten (Ausgangspunkt startV) ist der Knoten A. Das Ziel ist der Endknoten D endV). Dazwischen soll der kürzeste Weg berechnet werden. Die Arbeitsweise des Algorithmus soll unter diesen Bedingungen beschrieben werden: Begonnen wired mit Knoten A. Dem Weg von A nach A wird der Kostenbetrag 0 zugeordnet. Inder Priority-Queue wird eingetragen: "A nach A 0". Es folgt ein iterativer Prozeß, der von A aus Nachfolgeknoten untersucht, bis der Endknoten endV erreicht ist. Durch das Einbringen aller der auf dem Weg zum Endknoten liegenden Knoten (einschl. der Pfadlängen) in die Priority-Queue, kann der kürzeste Pfad aus der Priority-Queue ausgelesen werden. Der Wert "A" für endV wird gelöscht, betrachtet werden die Nachbarn von A "B, C, E" als neue Endknoten. Das ergibt folgende Pfadobjekte: PfadInfo-Objekte OA,B OA,C OA,E startV A A A endV B C E Kosten 4 12 4 Diese Objekte werden in folgende Reihenfolge in die Priorirty-Queue eingeordnet: "A nach B 4", "A nach E" 4, "A nach C 12". Im nächsten Schritt wird das PfadInfo-Objekt OA,B aus der Priority-Queue gelöscht. Der zugehörige Enknoten ("B") wird in die Liste "l" der bereits berücksictigten Knoen aufgenommen, falls er sich nicht in "l" befindet. Die Nachbarn von B "A", "C" und "D" werden bestimmt. "A" befindet sich bereits in "l", PfadInfo-Objekte werden zu "C" und "D" ermittelt. PfadInfo-Objekte OB,C OB,D startV B B endV C D Kosten 10 = 6 + 4 12 = 4 + 8 Die Priority-Queue umfasst nun 4 Elemente: "A nach E 4", "A nach C 12", "B nach C 10", "B nach D 12". Der Eintrag, der mit der kleinste Pfadlänge verbunden ist, unfaßt: "A – B – C". Die Priority-Queue enthält folgende Elemente: "A nach E 4", "A nach C 12", "B nach C 10", "B nach D 12". Betrachtet wird das PfadInfo-Objekt OA,E. Es wird gelöscht, die zugehörigen 4 Kosteneinheiten werden in das erzeugende PfadInfoObjekt OE,D übernommen (4 + 10 = 14). Die Priority-Queue enthält die Einträge: "B nach C 10", "A nach 10" 12", "B nach D 12", "E nach D 14". Im nächsten Löschvorgang ist das Objekt OB,C der kleinste Wert. "C" kann nun "l" hinzugefügt werden, 10 Kosteneinheiten betragen die kleinsten Kosten von "A" nach "C". Die benachbarten Knoten von "C" sind "B" und "D". "B" wurde schon behandelt, die Priority-Queue besitzt noch 3 Elemente: "B nach D 12", "E nach D 14", "C nach D 24". Das Entfernen von OB,D aus der Priority-Queue führt auf den kleinsten Pfad von A nach D mit 12 Kosteneinheiten. 220 Die Programmiersprache C++ Implementierung template <class T> int Graph<T>::minimumPath(const T& sVertex, const T& eVertex) { // priority queue mit Informationen ueber die Kosten auf dem Pfad // vom Startknoten priority_queue< PathInfo<T>, vector<PathInfo<T> >, greater<PathInfo<T> > > pq; // wird benutzt, wenn Pfadinformationen in die // priority queue eingefuegt oder geloescht werden PathInfo<T> pathData; // l ist eine Liste aller Knoten , die von sVertex aus erreichbar sind // adjL ist die Liste aller Nachbarn, die besucht werden. // adjLiter wird zum Durchlaufen von adjL benutzt vector<T> l, adjL; vector<T>::iterator adjLiter; T sv, ev; int mincost; // Angabe der ersten Eintraege pathData.startV = sVertex; pathData.endV = sVertex; // Kosten von sVertex nach sVertex betragen 0 pathData.cost = 0; pq.push(pathData); // Bearbeite Knoten bis ein kuerzester Weg zum // Zielknoten gefunden ist oder die priority queue leer ist while (!pq.empty()) { // delete a priority queue entry, and record its // ending vertex and cost from sVertex. pathData = pq.top(); pq.pop(); ev = pathData.endV; mincost = pathData.cost; // Falls der Zielknoten erreicht wurde, wurde der // kuerzeste Weg vom Start- zum Zielknoten gefunden if (ev == eVertex) break; // Falls der Endknoten schon in l ist, soll er nicht // weiter betrachtet werden if (!findVertex(l,ev)) { // Einfuegen ev in l l.push_back(ev); // Bestimme alle Nachbarn des aktuellen Knoten ev, fuer // jeden Nachbarn der nicht in l ist, erzeuge einen // Eintrag und fuege ihn ein in die priority queue // mit Startknoten ev sv = ev; adjL = getNachbarn(sv); // adjLiter durchlaeuft die neue Liste adjL for(adjLiter = adjL.begin(); adjLiter != adjL.end(); adjLiter++) { ev = *adjLiter; if (!findVertex(l,ev)) { // Erzeuge neuen Eintrag fuer the priority queue pathData.startV = sv; pathData.endV = ev; // cost enthalt aktuelle minimale Kosten, hinzu kommen // die Kosten vom Start- zum Zielknoten pathData.cost = mincost + getWeight(sv,ev); pq.push(pathData); } 221 Die Programmiersprache C++ } } } // Ruechgabe: Erfolg bzw. kein Erfolg if (ev == eVertex) return mincost; else return -1; } Test Der Aufruf der vorliegenden Methode minimumPath()aus der folgenden main()Routine int main(int argc, char *argv[]) { // Knoten des Graphen werden über Grossbuchstaben bezeichnet Graph<char> g; char dName[50]; cout << "Eingabe-Datei: "; cin >> setw(50) >> dName; char s; // Eingabe der Knoten g.readGraph(dName); // Prompt fuer den Startknoten cout << "Berechne den kuerzesten Weg vom Startknoten "; cin >> s; vector<char> v = g.getKnoten(); // Kontrolle vector<char>::iterator vecIter; for (vecIter = v.begin();vecIter != v.end(); vecIter++) // cout << " " << *vecIter; cout << "Kuerzester Weg von " << s << " nach " << *vecIter << " ist " << g.minimumPath(s,*vecIter) << endl; system("PAUSE"); return 0; } , das Einlesen der Knoten und Kanten von folgenden Graphen A 752 604 D B 648 504 432 763 E 355 C führt zu der folgende Ausgabe: 222 Die Programmiersprache C++ Berechne den kuerzesten Weg Kuerzester Weg von A nach A Kuerzester Weg von A nach B Kuerzester Weg von A nach C Kuerzester Weg von A nach D Kuerzester Weg von A nach E vom ist ist ist ist ist Startknoten A 0 604 648 752 1003 223 Die Programmiersprache C++ 4.2 Lineare Listen 4.2.1 Einfach gekettete Listen Grundlagen Eine Liste ist verkettet gespeichert, falls jeder Knoten die Adresse der Arbeitsspeicherzelle enthält, in der sein Nachfolger gespeichert ist. Zeiger auf den Nachfolger-Knoten Knoten k Knoten k‘ Abb.: Aufbau Eine lineare Liste kann durch Kombination einer „structure“ mit einem „Zeiger“ erreicht werden. Die „structure“-Variable enthält beliebige Komponenten mit Daten und einer Komponente, die Zeiger auf die Nachfolger der Listenkomponenten aufnimmt, z.B. struct eintrag { char name[20]; char vorname[15]; eintrag* zNachf; }; // Zeiger auf den Nachfolger Die zusätzliche Komponente zNachf ist eine Zeigervariable, die auf eine Instanz vom Typ „eintrag“ verweist. Für eine Instanz vom Typ „eintrag“ steht noch kein Speicherplatz zur Verfügung. Dieser wird zur Programmlaufzeit erzeugt, z.B. mit eintrag* zelem; zelem = new eintrag; Der Inhalt des reservierten Speicherbereichs ist vollkommen undefiniert. Man kann ihn aber z.B. durch Eingabe über die Tastatur füllen: cout << "\tName: "; cin >> zelem->name; cout << "\tVorname: "; cin >> zelem->vorname; ............................................... So können alle Komponenten bis auf die letzte gefüllt werden. Zunächst sollte man immer dafür sorgen, daß der Zeiger nicht irgendwohin, sondern auf eine bestimmte Speicherstelle zeigt. Durch 224 Die Programmiersprache C++ zElem->zNachf = NULL; zeigt die „zNachf“-Komponente auf die symbolische Adresse mit dem Wert 0. zelem zAnfang char name[20]; char vorname[15]; ....... ....... ....... ........ eintrag* zelem; NULL Abb.: Einfügen des ersten Elements in die linear verkettete Liste (LIFO-Struktur) Zur Erzeugung eines weiteren Eintrags benötigt man eine weitere Zeigervariable: eintrag* zAnfang; Dieser Zeiger erhält den Inhalt von zElem durch zAnfang = zElem; Auf den zuvor mit new reservierten Speicherbereich zeigen dann zwei Variable. Mit zElem = new eintrag; wird ein weiterer Speicherbereich reserviert.. Wieder können die Komponenten durch Einlesen über die Tastatur mit Werten gefüllt werden. Die letzte Komponente erhält dieses Mal jedoch nicht Null, sondern die Adresse in zAnfang: zElem->zNachf = zAnfang; zelem zAnfang char name[20]; char vorname[15]; ....... ....... ....... ........ eintrag* zelem; NULL ........ Abb.: Einfügen des zweiten Elements in die linear verkettete Liste (LIFO-Struktur) zAnfang wird anschließend auf das neu erzeugte Element gesetzt: 225 Die Programmiersprache C++ zAnfang = zElem; Die so erzeugte Konstruktion ist eine lineare Liste. Das, was bisher in einzelnen Anweisungen durchgeführt wurde, läßt sich in einer Schleife zusammenfassen: do { if ((zelem = new eintrag) == NULL) { cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden"; break; } cout << "\tName: "; cin >> zelem->name; cout << "\tVorname: "; cin >> zelem->vorname; zElem->zNachf = zAnfang; zAnfang = zElem; do { cout << "\n\tWeiteres Element einfuegen?"; cin >> antwort; antwort = tolower(antwort); } while ((antwort != 'j') && (antwort != 'n')); } while (antwort == 'j'); Nach einigen Schleifendurchläufen ergibt sich folgende Situation: zelem zAnfang char name[20]; char vorname[15]; ....... ....... ....... ........ eintrag* zelem; NULL ........ ....., ....... Abb.: Linear verkettete Liste (LIFO-Struktur) Es handelt sich um eine LIFO-Struktur. Aufgabe zur Listenverarbeitung 1. Mit Hilfe der vorliegenden Darstellung über den Aufbau einer einfach geketteten Liste soll eine Anwendung „Adreßverwaltung“ entwickelt werden. Die Adreßverwaltung soll über einen benutzerdefinierten Datentyp „liste“, der eine einfach gekettete, lineare Liste beschreibt, erfolgen. Zu Beginn der Verarbeitung sind die Adreßverwaltungsdaten aus einer Textdatei, deren Datensätze (Zeilen) Adreßdaten enthalten, zu entnehmen bzw. zu sichern148. #include #include #include #include 148 <iostream.h> <fstream.h> <string.h> <stdlib.h> vgl. PR43105.CPP 226 Die Programmiersprache C++ typedef int bool; const int FALSE = 0; const int TRUE = 1; struct eintrag { char name[20]; char vorname[15]; eintrag* zNachf; }; class liste { private: eintrag eintr; eintrag* zAnfang; char* dateiName; // Hier werden die Daten abgelegt public: liste(char*); // Konstruktor ~liste(void); // Destruktor bool einfuegen(eintrag*); eintrag* holeEintr(char*); bool loescheEintr(char*); }; // Schnittstellenfunktionen // Konstruktor und Destruktor liste :: liste(char* dName) { // Im Konstruktor werden die Daten aus einer // externen Datei eingelesen eintrag* einTr; fstream datei; zAnfang = NULL; dateiName = dName; datei.open(dName, ios::in); // Keine Fehlerbehandlung, falls die Datei nicht existiert, // da dies beim ersten Start des Programms normal ist if (datei) { // Einlesen der Daten while (!datei.eof()) { if ((einTr = new eintrag) == NULL) { cerr << "\n\tNicht genuegend Speicher "; break; } datei >> einTr->name; datei >> einTr->vorname; // .................... if (datei.eof()) break; einTr->zNachf = zAnfang; zAnfang = einTr; } datei.close(); } } liste :: ~liste(void) { // Im Destruktor wird die Liste wieder in eine externe // Datei geschrieben eintrag* zLoe; 227 Die Programmiersprache C++ fstream datei; datei.open(dateiName, ios::out); if (!datei) { cerr << "\n\tDie Datei " << dateiName << " kann nicht zur Ausgabe geoeffnet werden \n"; exit(-1); } while (zAnfang != NULL) { datei << zAnfang->name << '\n'; datei << zAnfang->vorname << '\n'; // ...... zLoe = zAnfang; zAnfang = zAnfang->zNachf; delete zLoe; // Rueckgabe des Speicherplatzes } datei.close(); }; // Transferfunktionen bool liste :: einfuegen(eintrag* zEintr) { /* Ein neuer Listenknoten wird in die Liste eingefuegt Die Informationen werden an anderer Stelle eingetragen */ if (zEintr != NULL) { zEintr->zNachf = zAnfang; zAnfang = zEintr; return(TRUE); } return(FALSE); } eintrag* liste :: holeEintr(char* info) { // Suchen eines Eintrags mit der Bezeichnung "info" eintrag* zElem = zAnfang; while (zElem != NULL) { if (strcmp(info,zElem->name) == 0) // Eintrag gefunden return(zElem); // Kein Eintrag gefunden: Weitersuchen zElem = zElem->zNachf; } return(NULL); // Ende der Liste erreicht } bool liste :: loescheEintr(char* info) { // Der Eintrag mit dem Namen ifo wird aus der Liste // entfernt eintrag* zElem; eintrag* zLoe; if (zAnfang == NULL) // in einer leeren Liste gibt es nichts zu loeschen return(FALSE); zElem = zAnfang; // Weiterer Spezialfall: Das zu loeschende Element ist // das erste Element in der Liste if (strcmp(zElem->name,info) == 0) { zAnfang = zElem->zNachf; delete zElem; 228 Die Programmiersprache C++ return(TRUE); } while (zElem->zNachf != NULL) { if (strcmp(zElem->zNachf->name,info) == 0) { zLoe = zElem->zNachf; zElem->zNachf = zElem->zNachf->zNachf; delete zLoe; return(TRUE); } zElem = zElem->zNachf; } return(FALSE); } int main() { short menue(void), wahl; void einfuegenElement(liste&); void suchenElement(const liste&); void loeschenElement(liste&); liste adressen("a:adrlist.dat"); cout << "\n\t A D R E S S E N V E R W A L T U N G"; cout << "\n\t -----------------------------------"; do { wahl = menue(); switch(wahl) { case 1: einfuegenElement(adressen); break; case 2: suchenElement(adressen); break; case 3: loeschenElement(adressen); break; case 4: break; } } while (wahl != 4); } short menue(void) { short izch; cout << "\n\tBitte eingeben: "; cout << "\n\t1: Eingabe eines Datensatzes "; cout << "\n\t2: Suchen nach einem Datensatz "; cout << "\n\t3: Loeschen Datensatz aus Liste "; cout << "\n\t4: Ende Listenbearbeitung "; cout << "\n\tWahl: "; do { cin >> izch; } while ((izch < 1) || (izch > 4)); return izch; } void einfuegenElement(liste& adressen) { eintrag* zelem; if ((zelem = new eintrag) == NULL) cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden"; else { 229 Die Programmiersprache C++ cout << "\tName: "; cin >> zelem->name; cout << "\tVorname: "; cin >> zelem->vorname; if (adressen.einfuegen(zelem)) cout << "\n\tEintrag konnte eingefuegt werden" << endl; else cerr << "\n\tEintrag konnte nicht eingefuegt werden "; } } void suchenElement(const liste& adressen) { char suchName[20]; eintrag* zelem; cout << "\n\tName angeben: "; cin >> suchName; if ((zelem = adressen.holeEintr(suchName)) != NULL) { cout << "\n\tGefunden:"; cout << "\n\t" << zelem->name << ' ' << zelem->vorname; } else cout << "\n\tNicht gefunden"; } void loeschenElement(liste& adressen) { char suchName[20]; cout << "\n\tName angeben: "; cin >> suchName; if (adressen.loescheEintr(suchName)) cout << "\n\t" << suchName << " wurde geloescht"; else cout << "\n\t" << suchName << " konnte nicht geloescht werden"; } 2. Die vorstehende Aufgabe soll mit Hilfe der Klassenschablone „list“ der Standard Template Library149 gelöst werden. // ANSI C Headers #include <stdlib.h> // C++ STL Headers #include <algorithm> #include <iostream> #include <list> #include <string> struct eintrag { string name; string vorname; eintrag(string n, string v) { name = n; vorname = v; } bool operator==(const eintrag& ein) { if ((this->name.compare(ein.name) == 0) && (this->vorname.compare(ein.vorname) == 0)) return true; return false; } bool operator!=(const eintrag& ein) 149 vgl. 5.2.7 230 Die Programmiersprache C++ { if ((this->name.compare(ein.name) != 0) (this->vorname.compare(ein.vorname) return true; if ((this->name.compare(ein.name) == 0) (this->vorname.compare(ein.vorname) return true; if ((this->name.compare(ein.name) != 0) (this->vorname.compare(ein.vorname) return true; return false; && != 0)) && != 0)) && == 0)) } bool operator<(const eintrag& ein) { if ((this->name.compare(ein.name) < 0)) return true; else return false; } friend ostream& operator<<(ostream&, const eintrag&); }; ostream& operator << (ostream& strm, const eintrag& elem) { strm << elem.name << " " << elem.vorname; return strm; } list<eintrag> adressen; list<eintrag>::iterator iter; int main() { short menue(void), wahl; void einfuegenElement(); void auflisten(); void suchenElement(); void loeschenElement(); cout << "\n\t A D R E S S E N V E R W A L T U N G"; cout << "\n\t -----------------------------------"; do { wahl = menue(); switch(wahl) { case 1: einfuegenElement(); break; case 2: auflisten(); break; case 3: suchenElement(); break; case 4: loeschenElement(); break; case 5: break; } } while (wahl != 5); } short menue(void) { short izch; cout << "\n\tBitte eingeben: "; cout << "\n\t1: Eingabe eines Datensatzes "; cout << "\n\t2: Auflisten der Listenelemente "; cout << "\n\t3: Suchen nach einem Datensatz "; 231 Die Programmiersprache C++ cout << "\n\t4: Loeschen Datensatz aus Liste "; cout << "\n\t5: Ende Listenbearbeitung "; cout << "\n\tWahl: "; do { cin >> izch; } while ((izch < 1) || (izch > 5)); return izch; } void einfuegenElement() { string name; string vorname; cout << "\tName: "; cin >> name; cout << "\tVorname: "; cin >> vorname; eintrag elem(name,vorname); adressen.push_back(elem); } void auflisten() { adressen.sort(); cout << "\n\tAuflisten der sortierten Listenelemente:\n"; for ( iter = adressen.begin(); iter != adressen.end(); ++iter ) cout << "\t" << *iter << endl; } void suchenElement() { string suchName; string suchVorname; cout << "\n\tName angeben: "; cin >> suchName; cout << "\n\tVorname angeben: "; cin >> suchVorname; eintrag suchElem(suchName,suchVorname); iter = find(adressen.begin(),adressen.end(),suchElem); if (iter != adressen.end()) { cout << "\n\tGefunden:"; cout << "\n\t" << *iter << endl; } else cout << "\n\tNicht gefunden"; } void loeschenElement() { string suchName; string suchVorname; cout << "\n\tName angeben: "; cin >> suchName; cout << "\n\tVorname angeben: "; cin >> suchVorname; eintrag suchElem(suchName,suchVorname); iter = find(adressen.begin(),adressen.end(),suchElem); if (iter != adressen.end()) { cout << "\n\tGefunden mit abschliessendem Loeschen:"; cout << "\n\t" << *iter << endl; list<eintrag>::iterator liter; liter = iter; // iter = erase(liter); Fehler !!! } else cout << "\n\tNicht gefunden"; } 232 Die Programmiersprache C++ 4.2.2 Klassenschablonen für verkettete Listen 4.2.2.1 Doppelt gekettete Listen vgl. 5.2.3 4.2.2.2 Ringförmig geschlossene Listen 1. Einfach verkettete, ringförmig geschlossene Liste Aufbau einfach geketteter Ringstrukturen BASIS "Leerer Ring" BASIS Abb.: Eine leere ringförmig verkettete Liste enthält eine Listenknoten und ein nicht initialisiertes Datenfeld. Der Zeiger auf diesen Listenknoten zeigt auf sich selbst. Ein „Null“-Zeiger existiert in ringförmig verketteten Listen nicht. Gegeben ist folgende Listenstruktur ZGR ZGR1 type Zeiger = record ............ ............ Nachf : ^Zeiger; end; Abb.: Eine ringförmige Datenstruktur kann durch die Anweisung ZGR1^.Nachf := ZGR; 233 Die Programmiersprache C++ erreicht werden. In der Regel zeigt der letzte Knoten in der verkettet gespeicherten Liste auf den Listenanfang. Ringe können auch folgenden Aufbau besitzen: Abb.: Die Klasse „einfach verketteter Ringknoten“ in C++150 // Deklaration Listenknoten template <class T> class ringKnoten { private: // ringfoermige Verkettung auf den naechsten Knoten ringKnoten<T> *nachf; public: // "daten" im oeffentlichen Zugriffsbereich T daten; // Konstruktoren ringKnoten(void); ringKnoten (const T& merkmal); // Listen-Modifikationsmethoden void einfuegenDanach(ringKnoten<T> *z); ringKnoten<T> *loeschenDanach(void); // beschafft die Adresse des (im Ring) folgenden Knoten ringKnoten<T> *nachfKnoten(void) const; }; Die Struktur einer einfach veketteten, ringförmig geschlossenen Liste kann so dargestellt werden: daten: nachf: Abb.: leere Liste daten: nachf: Abb.: Liste mit Knoten // Schnittstellenfunktionen 150 vgl. ringkno.h 234 Die Programmiersprache C++ Der Konstruktor initialisiert einen Knoten, der einen Zeiger enthält, der auf diesen Knoten zurück verweist. So kann jeder Knoten den Anfang einer leeren Liste repräsentieren. Das Datenfeld des Knoten bleibt in diesem Fall unbesetzt. // Konstruktor der eine Liste deklariert und "daten" // uninitialisiert laesst. template <class T> ringKnoten<T>::ringKnoten(void) { // initialisiere den Knoten, so dass er auf sich selbst zeigt nachf = this; } // Konstruktor der eine leere Liste erzeugt und "daten" // initialisiert template <class T> ringKnoten<T>::ringKnoten(const T& merkmal) { // setze den Knoten so, dass er auf sich selbst zeigt // und initialisiere "daten" nachf = this; daten = merkmal; } Die Methode nachfKnoten() ermittelt einen Verweis auf den nächsten in der einfach verketteten, ringförmig geschlossenen Liste. Die Methode soll das Durchlaufen der Liste erleichtern. // Rueckgabe des Zeiger auf den naechsten Knoten template <class T> ringKnoten<T> *ringKnoten<T>::nachfKnoten(void) const { return nachf; } Die Methoden zur Modifikation der Liste einfuegenDanach(ringKnoten<T> *z); fügt die Listenknoten unmittelbar nach dem Anfangsknoten (der die leere Liste definiert) ein. vor dem Einfügen: nach dem Einfügen: daten: nachf: z Abb.: Einfügen des Knoten „z“ in eine leere Liste vor dem Einfügen: nach dem Einfügen: daten: nachf: Z Abb.: Einfügen des Knoten „z“ in ein einfach gekettete, ringförmig geschlossene Liste mit Listenknoten // Einfuegen eines Knoten z nach dem aktuellen Knoten 235 Die Programmiersprache C++ template <class T> void ringKnoten<T>::einfuegenDanach(ringKnoten<T> *z) { // z zeigt auf den Nachfolger des aktuellen Knoten, // der aktuellen Knoten zeigt auf z z->nachf = nachf; nachf = z; } Die Methode loeschenDanach() löscht den Listenknoten unmittelbar nach dem aktuellen Knoten. // Loesche den Knoten, der dem aktuellen Knoten folgt und gib seine // Adresse zurueck template <class T> ringKnoten<T> *ringKnoten<T>::loeschenDanach(void) { // Sichere die Adresse des Knoten, der geloescht werden soll ringKnoten<T> *tempZgr = nachf; // Falls "nachf" mit der Adresse des aktuellen Objekts (this) ueberein// stimmt, wird auf sich selbst gezeigt. Hier darf nicht geloescht werden // (Rueckgabewert NULL) if (nachf == this) return NULL; // Der aktuelle Knoten zeigt auf denNachfolger von tempZgr. nachf = tempZgr->nachf; // Gib den Zeiger auf den ausgeketteten Knoten zurueck return tempZgr; } Anwendung: Das „Josephus-Problem“ Aufgabenstellung: Ein Reisebüro verlost eine Weltreise unter „N“ Kunden. Dazu werden die Kunden von 1 bis N durchnummeriert, eine Bediensteter des Reisebüros hat in einem Hut N Lose untergebracht. Ein Los wird aus dem Hut gezogen, es hat die Nummer M (1 <= M <= N). Zur Auswahl des glücklichen Kunden stellt man sich dann folgendes vor: Die Kunden (identifiziert durch die Nummern 1 bis N) werden in einem Kreis angeordnet und mit Hilfe der gezogenen Losnummer aus diesem Kreis entfernt. Bei bspw. 8 Kunden und der gezogenen Losnummer 3 werden, da das Abzählen bzw. Entfernen im Uhrzeigersinn erfolgt, folgende Nummern aus dem Kreis entfernt: 3, 6, 1, 5, 2, 8, 4. Die Person 7 gewinnt die Reise. Lösung151: #include <iostream.h> #include <stdlib.h> #include "ringkno.h" // Erzeuge eine ringfoermig verkettete Liste mit gegebenem Anfang void erzeugeListe(ringKnoten<int> *anfang, int n) { // Beginn des Einfuegevorgangs ringKnoten<int> *aktZgr = anfang, *neuerKnotenWert; int i; // Erzeuge die n Elemente umfassende ringfoermige Liste for(i=1;i <= n;i++) { // Belege den Knoten mit Datenwert neuerKnotenWert = new ringKnoten<int>(i); // Einfuegen am Listenende 151 PR22221.CPP 236 Die Programmiersprache C++ aktZgr->einfuegenDanach(neuerKnotenWert); aktZgr = neuerKnotenWert; } } // Gegeben ist eine n Elemente umfassende, ringfoermige Liste; loese das // Josephus-Problem durch Loeschen jeder m. Person bis nur // eine Person uebrig bleibt void Josephus(ringKnoten<int> *liste, int n, int m) { // vorgZgr bewegt aktZgr durch die Liste ringKnoten<int> *vorgZgr = liste, *aktZgr = liste->nachfKnoten(); ringKnoten<int> *geloeschterKnotenZgr; // Loesche alle bis auf eine Person aus der Liste for(int i=0;i < n-1;i++) { // Zaehle die Personen jeweils an der aktuelle Stelle // Suche m Personen auf for(int j=0;j < m-1;j++) { // Ausrichten der Zeiger vorgZgr = aktZgr; aktZgr = aktZgr->nachfKnoten(); // Falls "aktZgr am Anfang steht, bewege die Zeiger weiter if (aktZgr == liste) { vorgZgr = liste; aktZgr = aktZgr->nachfKnoten(); } } cout << "Loesche Person " << aktZgr->daten << endl; // Ermittle den zu loeschenden Knoten und aktualisiere aktZgr geloeschterKnotenZgr = aktZgr; aktZgr = aktZgr->nachfKnoten(); // loesche den Knoten aus der Liste vorgZgr->loeschenDanach(); delete geloeschterKnotenZgr; // Falls aktZgr am Anfang steht, bewege Zeiger weiter if (aktZgr == liste) { vorgZgr = liste; aktZgr = aktZgr->nachfKnoten(); } } cout << endl << "Ausgezaehlt wurde " << aktZgr->daten << endl; // Loesche den uebrig gebliebenen Knoten geloeschterKnotenZgr = liste->loeschenDanach(); delete geloeschterKnotenZgr; } void main(void) { // Liste mit Personen ringKnoten<int> liste; // n ist die Anzahl der Personen, m ist die Abzaehlgroesse int n, m; cout << "Anzahl Bewerber? "; cin >> n; // Erzeuge eine ringfoermig gekettete Liste mit Personen 1, 2, ... n erzeugeListe(&liste,n); // Zufallswert: 1 <= m <= n randomize(); m = 1 + random(n); cout << "Erzeugte Zufallszahl " << m << endl; // loese das Josephus Problem und gib den Gewinner aus Josephus(&liste,n,m); } /* <Ablauf des Programms> 237 Die Programmiersprache C++ Anzahl der Bewerber? 10 Erzeugte Zufallszahl 5 Loesche Person 5 Loesche Person 10 Loesche Person 6 Loesche Person 2 Loesche Person 9 Loesche Person 8 Loesche Person 1 Loesche Person 4 Loesche Person 7 Person 3 gewinnt. */ 2. Doppelt verkettete, ringförmig geschlossene Liste Basis Abb.: Doppelt gekettete Ringstruktur Leerer Ring Basis Abb.: Der leere Ring in einer doppelt geketteten Ringstruktur Doppelt verkettete Listen erweitern den durch ringförmig verkettete Listen bereitgestellten Leistungsumfang beträchtlich. Sie erleichtern das Einfügen und das Löschen durch Zugriffsmöglichkeinten in zwei Richtungen: 238 Die Programmiersprache C++ links daten rechts ...... ..... 4 1 2 3 Abb.: Klassenschablone „doppelt verketteter RingKnoten“152 template <class T> class dkringKnoten { private: // ringfoermig angeornete Verweise nach links und rechts dkringKnoten<T> *links; dkringKnoten<T> *rechts; public: // daten steht unter oeffentlichem Zugriff T daten; // Konstruktoren: dkringKnoten(void); dkringKnoten (const T& merkmal); // Modifikation der Listen void einfuegenRechts(dkringKnoten<T> *z); void einfuegenLinks(dkringKnoten<T> *z); dkringKnoten<T> *loescheKnoten(void); // Beschaffen der Adressen der nachfolgenden Knoten auf der // linken und rechten Seite dkringKnoten<T> *nachfKnotenRechts(void) const; dkringKnoten<T> *nachfKnotenLinks(void) const; }; Methoden für doppelt verketteten Listenknoten einer ringförmig geschlossenen Liste Konstruktoren // Konstruktor: erzeugt eine leere Liste, das Datenfeld bleibt // ohne Initialisierung; wird zur Definition des Listenanfangs benutzt template <class T> dkringKnoten<T>::dkringKnoten(void) { // ínitialisiert den Knoten mit einem Zeiger, der auf den // Knoten zeigt links = rechts = this; } // Konstruktor: erzeugt eine leere Liste und intialisierte das Feld daten template <class T> dkringKnoten<T>::dkringKnoten(const T& merkmal) { // initialisiert den Knoten mit einem Zeiger der // auf den Knoten zeigt und initialisiert das Datenfeld links = rechts = this; daten = merkmal; } Einfügen eines Knoten 152 dringkn.h 239 Die Programmiersprache C++ // Fuege einen Knoten z rechts zum aktuellen Knoten ein template <class T> void dkringKnoten<T>::einfuegenRechts(dkringKnoten<T> *z) { // kette z zu seinem Nachfolger auf der rechten Seite ein z->rechts = rechts; rechts->links = z; // verkette z mit dem aktuellen Knoten auf seiner linkten Seite z->links = this; rechts = z; } // Fuege einen Knoten z links zum aktuellen Knoten ein template <class T> void dkringKnoten<T>::einfuegenLinks(dkringKnoten<T> *z) { // kette z zu seinem Nachfolger auf der linken Seite ein z->links = links; links->rechts = z; // verkette z mit dem aktuellen Knoten auf seiner rechten Seite z->rechts = this; links = z; } Löschen // Ausketten des aktuellen Knoten aus der Liste template <class T> dkringKnoten<T> *dkringKnoten<T>::loescheKnoten(void) { // Knotenverweis "links" muss verkettet werden mit dem // Verweis des aktuellen Knoten nach rechts links->rechts = rechts; // Knotenverweis "rechts" muss verkettetet werden mit dem // Verweis des aktuellen Knoten nach links rechts->links = links; // Rueckgabe der Adresse vom aktuellen Knoten return this; } Bestimmen der nachfolgenden Knoten // Rueckgabe Zeiger zum naechsten Knoten auf der rechten Seite template <class T> dkringKnoten<T> *dkringKnoten<T>::nachfKnotenRechts(void) const { return rechts; } // Rueckgabe Zeiger zum naechsten Knoten auf der linken Seite // return pointer to the next node on the left template <class T> dkringKnoten<T> *dkringKnoten<T>::nachfKnotenLinks(void) const { return links; } Anwendung: Einfügen eines doppelt verketteten Listenknoten in eine geordnete Fole von Listenknoten153 Falls der Aufbau einer geordneten Folge von doppelt verketteten Listenknoten im Rahmen einer ringförmig verketteten Liste gelingt, kann die Liste in Vorwärtsrichtung 153 PR22225.CPP 240 Die Programmiersprache C++ (links) durchlaufen bzgl. der in den Listenknoten gespeicherten Daten eine aufsteigende Sortierung zeigen und ,in Rückwärtsrichtung (rechts) durchwandert, eine absteigende Sortierung aufweisen. Mit zwei Funktionsschablonen einfuegenKleiner() und einfuegenGroesser() soll dies erreicht werden. Zum Aufbau der ringförmig, doppelt verketteten Liste wird die Funktionsschablone DverkSort() herangezogen, die zum geordneten Einfügen die Funktionsschablone einfuegenKleiner() und einfuegenGroesser() benutzt und den Anfangszeiger „dkAnfang“ verwaltet. template <class T> void einfuegenKleiner(dkringKnoten<T> *dkAnfang, dkringKnoten<T>* &aktZgr, T merkmal) { dkringKnoten<T> *neuerKnoten= new dkringKnoten<T>(merkmal), *z; // Bestimme den Einfuegepunkt z = aktZgr; while (z != dkAnfang && merkmal < z->daten) z = z->nachfKnotenLinks(); // Einfuegen des Knotens mit dem Datenelement z->einfuegenRechts(neuerKnoten); // Ruecksetzen aktZgr auf den neuen Knoten aktZgr = neuerKnoten; } template <class T> void einfuegenGroesser(dkringKnoten<T>* dkAnfang, dkringKnoten<T>* & aktZgr, T merkmal) { dkringKnoten<T> *neuerKnoten= new dkringKnoten<T>(merkmal), *z; // Bestimmen des Einfuegepunkts z = aktZgr; while (z != dkAnfang && z->daten < merkmal) z = z->nachfKnotenRechts(); // Einfuegen des Datenelements z->einfuegenLinks(neuerKnoten); // Ruecksetzen des aktuellen Zeigers auf neuerKnoten aktZgr = neuerKnoten; } template <class T> void DverkSort(T a[], int n) { // Die doppelt verkettete Liste soll Feld-Komponenten aufnehmen dkringKnoten<T> dkAnfang, *aktZgr; int i; // Einfuegen des ersten Elements in die doppelt verkettete Liste dkringKnoten<T> *neuerKnoten = new dkringKnoten<T>(a[0]); dkAnfang.einfuegenRechts(neuerKnoten); aktZgr = neuerKnoten; // Einbrigen weiterer Elemente in die doppelt verkettete Liste for (i = 1; i < n; i++) if (a[i] < aktZgr->daten) einfuegenKleiner(&dkAnfang,aktZgr,a[i]); else einfuegenGroesser(&dkAnfang,aktZgr,a[i]); // Durchlaufe die Liste und kopiere die Datenwerte zurueck in den "array" aktZgr = dkAnfang.nachfKnotenRechts(); i = 0; while(aktZgr != &dkAnfang) { a[i++] = aktZgr->daten; aktZgr = aktZgr->nachfKnotenRechts(); } // Loesche alle Knoten in der Liste while(dkAnfang.nachfKnotenRechts() != &dkAnfang) { aktZgr = (dkAnfang.nachfKnotenRechts())->loescheKnoten(); delete aktZgr; } 241 Die Programmiersprache C++ } Der folgende Hauptprogrammabschnitt ruft die vorliegende Funktionsschablone zum Sortieren eines Arbeitsspeicherfelds auf void main(void) { // Ein initialisierter "array" mit 10 Ganzzahlen int A[10] = {82,65,74,95,60,28,5,3,33,55}; DverkSort(A,10); // sortiere "array" cout << "Sortiertes Feld: "; for(int i=0;i < 10;i++) cout << A[i] << " "; cout << endl; } 242 Die Programmiersprache C++ 4.3 Tabellen 4.3.1 Einfache und Sortierte Tabellen 154 vgl. 5.2.4 4.3.2 Hash-Tabellen Grundlage: Hash-Funktion Eine grundliegende Idee, das Suchen nach bestimmten Tabelleneinträgen zu beschleunigen, ist: Aus dem Suchbegriff ist durch einen Umrechnungsalgorithmus der Index zu berechnen, die direkt auf den Tabelleneintrag führt, der den Suchbegriff enthält. Allgemein wird ein derartiger Algorithmus „Hashing“ genannt. Die „Hashing“Funktion ist (nach der Divisions-Rest-Methode) bestimmt durch: H(Schl) = SCHL mod M „M“ ist die Größe der Tabelle, der Suchbegriff wird Schlüssel (SCHL) genannt. Die gleiche Methode dient auch zum Speichern der Tabelleneinträge. Kollisionen sind dabei möglich, in solchen Fällen bestimmt die Hash-Funktion trotz unterschiedlicher Schlüssel jeweils den gleichen (Index-) Wert. Auflösen von Kollisionen Eine Hash-Funktion, die keine Kollisionen verursacht, heißt perfekt. Parktisch kann perfektes Hashing nur für eine fest vorgegebene Anzahl von Schlüsseln erreicht werden. Generell führen Hash-Funktionen zu Kollisionen. Zum Auflösen von Kollisionen gibt es zwei unterschiedliche Vorgehensweisen: - Die kollidierenden Schlüssel werden aneinander verkettet (chaining). - Von einer Anfangsadresse (Anfangsindex) wird eine Folge weiterer Adressen (Indexe) durchlaufen (open adressing) 1. Kettungstechniken: seperate chaining Alle Schlüssel, die sich auf denselben Ort abbilden, werden mit Hilfe einer geketteten Liste verwaltet. Die Hash-Tabelle enthält dann nur noch Zeiger auf Listenknoten155. Bsp.: Erstellen und Verwalten eines Telefon-Verzeichnisses mit einer Hash-Tabelle im Rahmen des „seperate chaining“. Das Verzeichnis ist bisher in einer Textdatei enthalten. Die in dieser Tabelle gespeicherten Sätze haben folgenden Aufbau: 154 155 PR44205.CPP PR44310.CPP 243 Die Programmiersprache C++ Name Juergen ...................... Telefon-Nr. 93622 ......... Die Datensätze dieser Datei sollen in eine Hash-Tabelle übernommen werden und dort verwaltet werden (Einfügen, Löschen, Wiederauffinden von Datensätzen). Der Hashtabellen-Zugriff soll über den Namen im Datensatz erfolgen. Der Aufbau der Hash-Tabelle und die dort eingetragenen Verweise sollen nach dem Prinzip des „seperate chaining“ organisiert werden. ~ ~ ~ ~ Hashtabelle Abb.: Hash-Tabelle für „seperate chaining“ Die von der Hash-Tabelle ausgehenden Verweise bilden eine lineare Liste. Eine solche Liste wird zweckmäßig über einen benutzerdefinierten Datentyp „liste“ zum Erzeugen und Verwalten einer einfachen, geketteten Liste realisiert. 2. Überlaufverfahren ohne Kettung: open adressing Diese Verfahren suchen bei Adreßkollisionen nach einem freien Tabellenplatz. Bei der Bestimmung solcher Folgen von Adressen (Indexfolgen) sollen möglichst wenig Überlappungen (Häufungen) entstehen. Zur Bildung von Folgen (für Adressen, Indexe) haben sich einige typische Methoden durchgesetzt: a) Lineare Fortschaltung Liefert die Namens-Adreßtransformation eine Adresse „as“ und der schon ein Name eingetragen ist, dan wird durch lineares Fortschalten as+1, as+2, as+3, ... der nächste freie Platz in der Hash-Tabelle gesucht. Dort wird dann das Schlüsselwort eingetragen. Ist as + i > M, dann wird die Tabelle zyklisch von vorn durchlaufen b) Zufälliges Suchen Im Kollisionsfall wird mit Hilfe einer Zufallszahl ein Ersatzplatz gesucht: 1) Berechnung einer Tabellenadresse as (Nach einer der beschriebenen Transformationen) 244 Die Programmiersprache C++ 2) Vergleich des vorgegebenen Schlüssels mit dem unter as eingetragenen Namen. Im Kollisionsfall weiter bei (3), andernfalls STOP. 3) Berechnung einer Zufallszahl xi und Bildung einer Zuordnung as = as + xi mod M c) Double Hashing Hier werden zwei voneinander unabhängige Hash-Funktionen benutzt. Die erste Funktion dient zur Ermittlung der Postion. Die zweite bestimmt, falls die Position belegt ist, die nächste freie Position im Rahmen des „open adressing“ 156: d) quadratisches Sondieren Zuerst wird im Kollisionsfall die unmittelbar folgende Position in der Hashtabelle untersucht (as + 1). Danach wird, falls die Position besetzt ist, der Index um 4 Einheiten heraufgesetzt (as + 4). Führt das auch nicht zum Erfolg, dann wird der Index um 9 Einheiten erhöht (as + 9). Die zur Erhöhung der Position dienenden Quadratzahlen kann man über eine einfache Addition bestimmen: 0 1 4 9 16 25 .. 1 3 5 7 11 .. Man braucht also nur die Zahl, die zur jeweils vorletzten Quadratzahl addiert wurde, eine 2 hinzuzufügen, und man erhält die neue Quadratzahl. Mit dem quadratischen Sondieren erreicht man mindestens die Hälfte aller Positionen, falls die Tabellengröße eine Primzahl ist. 156 PR44315.CPP 245 Die Programmiersprache C++ 4.4 Binärbäume Freie binäre IntervallBäume Aufbau Der Binärbaum ist eine Datenstruktur, in der jedes Element zwei Zeiger besitzt und daher maximal zwei Nachfolger besitzen kann. Der Einstiegpunkt in den binären Baum heißt Wurzel. Von der Wurzel verzweigt sich der binäre Baum nach unten. Die Elemente eines binären Suchbaums werden bei der Verzweigung nach einer ganz bestimmten Reihenfolge abgespeichert. Ein neues Element wird eingefügt, indem es nach einem, im Element gespeicherten Kriterium mit der Wurzel verglichen wird. Liefert der Vergleich „kleiner“ wird das neue Element dem linken Nachfolger zugeordnet, im anderen Fall wird zum rechten Nachfolger verzweigt. Der Vergleich wird solange vorgenommen, bis keine weiteren Nachfolger mehr vorhanden sind. wurzel Abb. 4.5-1: Ein ausgeglichener Binärbaum Ordnungsrelation und Darstellung Freie Bäume sind durch folgende Ordnungsrelation bestimmt: In jedem Knoten eines knotenorientierten, geordneten Binärbaums gilt: Alle Schlüssel im rechten (linken) Unterbaum sind größer (kleiner) als der Schlüssel im Knoten selbst. Mit Hilfe dieser Ordnungsrelation erstellte Bäume dienen zum Zugriff auf Datenbestände (Aufsuchen eines Datenelements). Die Daten sind die Knoten (Datensätze, -segmente, -elemente). Die Kanten des Zugriffsbaums sind Zeiger auf weitere Datenknoten (Nachfolger). 246 Die Programmiersprache C++ Dateninformation Knotenzeiger Schluessel Datenteil Links Rechts Zeiger Zeiger zum linken zum rechten Nachfolgeknoten Abb. 4.5-2: Knoten eines binären Suchbaums Die Klassenschablone BaumKnoten // Schnittstellenbeschreibung // Die Klasse binaerer Suchbaum binSBaum benutzt die Klasse baumKnoten template <class T> class binSBaum; // Deklaration eines Binaerbaumknotens fuer einen binaeren Baum template <class T> class baumKnoten { protected: // zeigt auf die linken und rechten Nachfolger des Knoten baumKnoten<T> *links; baumKnoten<T> *rechts; public: // Das oeffentlich zugaenglich Datenelement "daten" T daten; // Konstruktor baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL, baumKnoten<T> *rzgr = NULL); // virtueller Destruktor virtual ~baumKnoten(void); // Zugriffsmethoden auf Zeigerfelder baumKnoten<T>* holeLinks(void) const; baumKnoten<T>* holeRechts(void) const; // Die Klasse binSBaum benoetigt den Zugriff auf // "links" und "rechts" friend class binSBaum<T>; }; Die Dateninformation des Binärbaumknotens kann folgendermaßen beschrieben werden: struct info { // int schl; char* schl; char* vorname; }; // Schluessel // irgendwelche Daten // Schnittstellenfunktionen // Konstruktor: Initialisiert "daten" und die Zeigerfelder // Der Zeiger NULL verweist auf einen leeren Baum template <class T> baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr, baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr) {} // Die Methode holeLinks ermoeglicht den Zugriff auf den linken 247 Die Programmiersprache C++ // Nachfolger template <class T> baumKnoten<T>* baumKnoten<T>::holeLinks(void) const { // Rueckgabe des Werts vom privaten Datenelement links return links; } // Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den // rechten Nachfoger template <class T> baumKnoten<T>* baumKnoten<T>::holeRechts(void) const { // Rueckgabe des Werts vom privaten Datenelement rechts return rechts; } // Destruktor: tut eigentlich nichts template <class T> baumKnoten<T>::~baumKnoten(void) {} Aufsuchen von in Baumknoten gespeicherten Schlüsselwerten Ausganspunkt ist der Wurzelknoten: baumKnoten<T>* wurzel; // Zeiger auf den Wurzelknoten Das Aufsuchen eines Elements im Zugriffsbaum geht vom Wurzelknoten über einen Kantenzug (d.i. eine Reihe von Zwischenknoten) zum gesuchten Datenelement. Bei jedem Zwischenknoten auf diesem Kantenzug findet ein Entscheidungsprozeß über die folgenden Vergleiche statt: 1. Die beiden Schlüssel sind gleich: Das Element ist damit gefunden 2. Der gesuchte Schlüssel ist kleiner: Das gesuchte Element kann sich dann nur im linken Unterbaum befinden 3. Der gesuchte Schlüssel ist größer: Das gesuchte Element kann sich nur im rechten Unterbaum befinden. Das Verfahren wird solange wiederholt, bis das gesuchte (Schlüssel-) Element gefunden ist bzw. feststeht, daß es in dem vorliegenden Datenbestand nicht vorhanden ist. Struktur und Wachstum binärer Bäume sind durch die Ordnungrelation bestimmt. Aufgabe: Betrachte die 3 Schlüssel 1, 2, 3. Diese 3 Schlüssel können durch verschieden angeordnete Folgen bei der Eingabe unterschiedliche binäre Bäume erzeugen. Zeichne alle Bäume, die aus unterschiedlichen Eingaben der 3 Schlüssel resultieren, auf. 248 Die Programmiersprache C++ 1, 2, 3 1, 3, 2 2, 1, 3 1 2 1 2 3 3 1 1 3 2 2, 3, 1 3, 1, 2 3, 2, 1 2 3 3 3 1 2 2 1 Abb. 4.5-3: 6 unterschiedliche Eingabefolgen bewirken 6 unterschiedliche Bäume Es gibt also: 6 unterschiedliche Eingabefolgen und somit 6 unterschiedliche Bäume. Allgemein können n Elemente zu n! verschiedenen Anordnungen zusammengestellt werden. Zur Realisierung der Ordnungsrelation werden, je nach unterschiedlichem Datentyp der Schlüssel, Vergleichsfunktionen benötigt. Ohne Berücksichtigung der Ordnungsrelation kann ein binärer Baum von folgender Gestalt aus Elementen der Klassenschablone „baumknoten“ zusammengestellt werden: ‘A’ ‘B’ ‘C’ ‘D’ ‘E’ int main() { baumKnoten<char> *a, *b, *c, *d, *e; d = new baumKnoten<char>('D'); e = new baumKnoten<char>('E'); b = new baumKnoten<char>('B',NULL,d); c = new baumKnoten<char>('C',e); a = new baumKnoten<char>('A',b,c); wurzel = a; } 249 Die Programmiersprache C++ Funktionsschablonen zur Bearbeitung binärer Bäume a) Ausgabe eines binären Baums Die Ausgabe soll auf das Standardausgabegerät erfolgen // Ausgabefunktionen fuer die Darstellung des Binaerbaums // Zwischenraum zwischen den Stufen const int anzZwischenraum = 6; // Einfuegen einer anzahl Zwischenraumzeichen auf der // aktuellen Zeile void ausgAnzZwischenraum(int num) { for (int i = 0; i < num; i++) cout << " "; } template <class T> void ausgBaum(baumKnoten<T> *b, int stufe) { if (b != NULL) { // gib den rechten Teilbaum aus ausgBaum(b->holeRechts(), stufe + 1); ausgAnzZwischenraum(anzZwischenraum * stufe); cout << b->daten << endl; ausgBaum(b->holeLinks(),stufe + 1); } } b) Zählen der Blätter // Anzahl Blätter template <class T> void anzBlaetter(baumKnoten<T>* b, int& zaehler) { // benutze den Postorder-Durchlauf if (b != NULL) { anzBlaetter(b->holeLinks(), zaehler); anzBlaetter(b->holeRechts(), zaehler); // Pruefe, ob der erreichte Knoten ein Blatt ist if (b->holeLinks() == NULL && b->holeRechts() == NULL) zaehler++; } } c) Ermitteln der Tiefe bzw. der Höhe // Hoehe des Baums template <class T> int hoehe(baumKnoten<T>* b) { int hoeheLinks, hoeheRechts, hoeheWert; if (b == NULL) hoeheWert = -1; else { hoeheLinks = hoehe(b->holeLinks()); hoeheRechts = hoehe(b->holeRechts()); hoeheWert = 1 + (hoeheLinks > hoeheRechts ? hoeheLinks : hoeheRechts); } 250 Die Programmiersprache C++ return hoeheWert; } d)Kopieren des Baums // Kopieren eines Baums template <class T> baumKnoten<T>* kopiereBaum(baumKnoten<T>* b) { baumKnoten<T> *neuerLzgr, *neuerRzgr, *neuerKnoten; // Rekursionsendebedingung if (b == NULL) return NULL; if (b->holeLinks() != NULL) neuerLzgr = kopiereBaum(b->holeLinks()); else neuerLzgr = NULL; if (b->holeRechts() != NULL) neuerRzgr = kopiereBaum(b->holeRechts()); else neuerRzgr = NULL; // Der neue Baum wird von unten her aufgebaut, // zuerst werden die Nachfolger bearbeitet und // dann erst der Vaterknoten neuerKnoten = erzeugebaumKnoten(b->daten, neuerLzgr, neuerRzgr); // Rueckgabe des Zeigers auf den zuletzt erzeugten Baumknoten return neuerKnoten; } f) Erzeugen bzw. Freigabe eines Binärbaumknotens template <class T> baumKnoten<T>* erzeugebaumKnoten(T merkmal, baumKnoten<T>* lzgr = NULL, baumKnoten<T>* rzgr = NULL) { baumKnoten<T> *z; // Erzeugen eines neuen Knoten z = new baumKnoten<T>(merkmal,lzgr,rzgr); if (z == NULL) { cerr << "Kein Speicherplatz!\n"; exit(1); } return z; // Rueckgabe des Zeigers } Der durch den Baumknoten belegte Funktionsschablone freigegeben werden: template <class T> void gibKnotenFrei(baumKnoten<T>* z) { delete z; } g) Löschen des Baums // Loeschen des Baums template <class T> void loescheBaum(baumKnoten<T>* b) { if (b != NULL) { 251 Speicherplatz kann über folgende Die Programmiersprache C++ loescheBaum(b->holeLinks()); loescheBaum(b->holeRechts()); gibKnotenFrei(b); } } Die in den Funktionsschablonen angesprochenen Verarbeitungsfunktionen zum Bestimmen eines Baumknotens anhand des Schlüsselwerts (Merkmal), zum Löschen eines Baums, zum Kopieren eines binären Baums, zur Speicherbeschaffung bzw. Speicherfreigabe für Baumknoten bilden die privaten (internen) Verarbeitungsmethoden einer Klassenschablone für einen binären Suchbaum. Schnittstellenbeschreibung (Deklaration) einer Klassenschablone für den binären Suchbaum: #include "baumkno.h" template <class T> class binSBaum { protected: // Zeiger auf den Wurzelknoten und den Knoten, auf den am // haeufigsten zugegriffen wird baumKnoten<T> *wurzel; baumKnoten<T> *aktuell; // Anzahl Knoten im Baum int groesse; // Speicherzuweisung / Speicherfreigabe // Zuweisen eines neuen Baumknoten mit Rueckgabe // des zugehoerigen Zeigerwerts baumKnoten<T> *holeBaumKnoten(const T& merkmal, baumKnoten<T> *lzgr,baumKnoten<T> *rzgr) { baumKnoten<T> *z; // Datenfeld ubd die beiden Zeiger werden initialisiert z = new baumKnoten<T> (merkmal, lzgr, rzgr); if (z == NULL) { cerr << "Speicherbelegungsfehler!\n"; exit(1); } return z; } // gib den Speicherplatz frei, der von einem Baumknoten belegt wird void freigabeKnoten(baumKnoten<T> *z) // wird vom Kopierkonstruktor und Zuweisungsoperator benutzt { delete z; } // Kopiere Baum b und speichere ihn im aktuellen Objekt ab baumKnoten<T> *kopiereBaum(baumKnoten<T> *b) // wird vom Destruktor, Zuweisungsoperator und bereinigeListe benutzt { baumKnoten<T> *neulzgr, *neurzgr, *neuerKnoten; // Falls der Baum leer ist, Rueckgabe von NULL if (b == NULL) return NULL; // Kopiere den linken Zweig von der Baumwurzel b und weise seine // Wurzel neulzgr zu if (b->links != NULL) neulzgr = kopiereBaum(b->links); else neulzgr = NULL; // Kopiere den rechten Zweig von der Baumwurzel b und weise seine // Wurzel neurzgr zu if (b->rechts != NULL) neurzgr = kopiereBaum(b->rechts); else neurzgr = NULL; // Weise Speicherplatz fuer den aktuellen Knoten zu und weise seinen // Datenelementen Wert und Zeiger seiner Teilbaeume zu 252 Die Programmiersprache C++ neuerKnoten = holeBaumKnoten(b->daten, neulzgr, neurzgr); return neuerKnoten; } // Loesche den Baum, der durch im aktuellen Objekt gespeichert ist void loescheBaum(baumKnoten<T> *b) // Lokalisiere einen Knoten mit dem Datenelementwert von merkmal // und seinen Vorgaenger (eltern) im Baum { // falls der aktuelle Wurzelknoten nicht NULL ist, loesche seinen // linken Teilbaum, seinen rechten Teilbaum und dann den Knoten selbst if (b != NULL) { loescheBaum(b->links); loescheBaum(b->rechts); freigabeKnoten(b); } } // Suche nach dem Datum "merkmal" im Baum. Falls gefunden, Rueckgabe // der zugehoerigen Knotenadresse; andernfalls NULL baumKnoten<T> *findeKnoten(const T& merkmal, baumKnoten<T>* & eltern) const { // Durchlaufe b. Startpunkt ist die Wurzel baumKnoten<T> *b = wurzel; // Die "eltern" der Wurzel sind NULL eltern = NULL; // Terminiere bei einen leeren Teilbaum while(b != NULL) { // Halt, wenn es passt if (merkmal == b->daten) break; else { // aktualisiere den "eltern"-Zeiger und gehe nach rechts bzw. nach // links eltern = b; if (merkmal < b->daten) b = b->links; else b = b->rechts; } } // Rueckgabe des Zeigers auf den Knoten; NULL, falls nicht gefunden return b; } public: // Konstruktoren, Destruktoren binSBaum(void); binSBaum(const binSBaum<T>& baum); ~binSBaum(void); // Zuweisungsoperator binSBaum<T>& operator= (const binSBaum<T>& rs); // Bearbeitungsmethoden int finden(T& merkmal); void einfuegen(const T& merkmal); void loeschen(const T& merkmal); void bereinigeListe(void); int leererBaum(void) const; int baumGroesse(void) const; // baumspezifische Methoden void aktualisieren(const T& merkmal); baumKnoten<T> *holeWurzel(void) const; }; 253 Die Programmiersprache C++ Operationen 1. Einfügen eines Knoten Vorstellung zur Lösung: 1. Suche nach dem Schlüsselwert 2. Falls der Schlüsselwert im binären Suchbaum gespeichert ist, erfolgt kein Einfügen. 3. Bei erfolgloser Suche wird ein neuer Baumknoten mit dem Schlüsselwert als Sohn des erreichten Blatts eingefügt. Die Schnittstellenfunktion void einfuegen(const T& merkmal); besitzt folgende Definition157: // Einfuegen "merkmal" in den Suchbaum template <class T> void binSBaum<T>::einfuegen(const T& merkmal) { // b ist der aktuelle Knoten beim Durchlaufen des Baums baumKnoten<T> *b = wurzel, *eltern = NULL, *neuerKnoten; // Terminiere beim leeren Teilbaum while(b != NULL) { // Aktualisiere den zeiger "eltern", // dann verzweige nach links oder rechts eltern = b; if (merkmal < b->daten) b = b->links; else b = b->rechts; } // Erzeuge den neuen Blattknoten neuerKnoten = holeBaumKnoten(merkmal,NULL,NULL); // Falls "eltern" auf NULL zeigt, einfuegen eines Wurzelknoten if (eltern == NULL) wurzel = neuerKnoten; // Falls merkmal < eltern->daten, einfuegen als linker Nachfolger else if (merkmal < eltern->daten) eltern->links = neuerKnoten; else // Falls merkmal >= eltern->daten, einfuegen als rechter Nachf. eltern->rechts = neuerKnoten; // Zuweisen "aktuell": "aktuell" ist die Adresse des neuen Knoten aktuell = neuerKnoten; groesse++; } 2. Löschen eines Knoten Es soll ein Knoten mit einem bestimmten Schlüsselwert entfernt werden. Dabei sind folgende Fälle zu unterscheiden: a) Der zu löschende Knoten ist ein Blatt, z.B.: 157 vgl. bsbaum.h 254 Die Programmiersprache C++ vorher nachher Das Entfernen kann leicht durchgeführt werden b) Der zu löschende Knoten hat genau einen Sohn, z.B.: vorher nachher c) Der zu löschende Knoten hat zwei Söhne, z.B.: nachher vorher Der nach dem Löschen resultierende Teilbaum ist natürlich wieder ein Suchbaum, häufig allerdings mit erheblich vergrößerter Höhe. Aufgaben 1) Gegeben ist ein binärer Baum folgender Gestalt: 255 Die Programmiersprache C++ k k1 k2 k3 Die Wurzel wird gelöscht. Welche Gestalt nimmt der Baum dann an: k1 k3 k2 Es ergibt sich eine Höhendifferenz H , die durch folgende Beziehung eingegrenzt ist: 1 H H (TL ) ( H (TL ) ist die Höhe des linken Teilbaums). 2) Gegeben ist die folgende Gestalt eines binären Baums 12 7 5 2 15 13 6 14 Welche Gestalt nimmt dieser Baum nach dem Entfernen der Schlüssel mit den unter a) bis f) angegebenen Werten an? a) 2 b) 6 256 Die Programmiersprache C++ 12 7 15 5 13 14 c) 13 12 7 15 5 14 d) 15 12 7 14 5 e) 5 12 7 14 f) 12 7 14 Schlüsseltransfer: Der angegebene Algorithmus zum Löschen von Knoten kann zu einer beträchtlichen Vergrößerung der Baumhöhe führen. Das bedeutet auch eine beträchtliche Steigerung des mittleren Suchaufwands. Man ersetzt häufig die angegebene Verfahrensweise durch ein anderes Verfahren, das unter dem Namen Schlüsseltransfer bekannt ist. Der zu löschende Schlüssel (Knoten) wird ersetzt durch den kleinsten Schlüssel des rechten oder den größten Schlüssel des linken Teilbaums. Dieser ist dann nach Fall A) bzw. B) aus dem Baum herauszunehmen, z.B.: 257 Die Programmiersprache C++ Größter / Kleinster Schlüsselwert des linken / des rechten Teilbaums Abb.: Darstellung der Verfahrensweise „Schlüsseltransfer“ Implementierung der Verfahrensweise „Schlüsseltransfer“ zum Löschen von Baumknoten in einem binären Suchbaum: // Falls "merkmal" im baum vorkommt, dann loesche es template <class T> void binSBaum<T>::loeschen(const T& merkmal) { // LKnoZgr: Zeiger auf Knoten L, der geloescht werden soll // EKnoZgr: Zeiger auf die "eltern" E des Knoten L // ErsKnoZgr: Zeiger auf den rechten Knoten R, der L ersetzt baumKnoten<T> *LKnoZgr, *EKnoZgr, *ErsKnoZgr; // Suche nach einem Knoten, der einen Knoten enthaelt mit dem // Datenwert von "merkmal". Bestimme die aktuelle Adresse diese Knotens // und die seiner "eltern" if ((LKnoZgr = findeKnoten (merkmal, EKnoZgr)) == NULL) return; // Falls LKnoZgr einen NULL-Zeiger hat, ist der Ersatzknoten // auf der anderen Seite des Zweigs if (LKnoZgr->rechts == NULL) ErsKnoZgr = LKnoZgr->links; else if (LKnoZgr->links == NULL) ErsKnoZgr = LKnoZgr->rechts; // Beide Zeiger von LKnoZgr sind nicht NULL else { // Finde und kette den Ersatzknoten fuer LKnoZgr aus. // Beginne am linken Zweig des Knoten LKnoZgr, // bestimme den Knoten, dessen Datenwert am groessten // im linken Zweig von LKnoZgr ist. Kette diesen Knoten aus. // EvonErsKnoZgr: Zeiger auf die "eltern" des zu ersetzenden Knoten baumKnoten<T> *EvonErsKnoZgr = LKnoZgr; // erstes moegliches Ersatzstueck: linker Nachfolger von L ErsKnoZgr = LKnoZgr->links; // steige den rechten Teilbaum des linken Nachfolgers von LKnoZgr hinab, // sichere den Satz des aktuellen Knoten und den seiner "Eltern" // Beim Halt, wurde der zu ersetzende Knoten gefunden while(ErsKnoZgr->rechts != NULL) { EvonErsKnoZgr = ErsKnoZgr; ErsKnoZgr = ErsKnoZgr->rechts; } if (EvonErsKnoZgr == LKnoZgr) // Der linke Nachfolger des zu Beschreibenden Knoten ist das // Ersatzstueck // Zuweisung des rechten Teilbaums ErsKnoZgr->rechts = LKnoZgr->rechts; else { // es wurde sich um mindestens einen Knoten nach unten bewegt // der zu ersetzende Knoten wird durch Zuweisung seines // linken Nachfolgers zu "Eltern" geloescht EvonErsKnoZgr->rechts = ErsKnoZgr->links; 258 Die Programmiersprache C++ // plaziere den Ersatzknoten an die Stelle von LKnoZgr ErsKnoZgr->links = LKnoZgr->links; ErsKnoZgr->rechts = LKnoZgr->rechts; } } // Vervollstaendige die Verkettung mit den "Eltern"-Knoten // Loesche den Wurzelknoten, bestimme eine neue Wurzel if (EKnoZgr == NULL) wurzel = ErsKnoZgr; // Zuweisen Ers zum korrekten Zweig von E else if (LKnoZgr->daten < EKnoZgr->daten) EKnoZgr->links = ErsKnoZgr; Else EKnoZgr->rechts = ErsKnoZgr; // Loesche den Knoten aus dem Speicher und erniedrige "groesse" freigabeKnoten(LKnoZgr); groesse--; } Test: Die nun bekannte Prozedur zum Löschen vom Baumknoten nach der Verfahrensweise des "Schlüsseltransfer" ist folgenden Prüfungen zu unterwerfen: 1) Der zu löschende Baumknoten besteht nur aus einem Wurzelknoten, z.B.: Schlüssel LINKS 12 RECHTS Ergebnis: Der Wurzelknoten wird gelöscht. 2) Vorgegeben ist Schlüssel LINKS 12 RECHTS 7 5 8 Der Wurzelknoten wird gelöscht. Ergebnis: 259 Die Programmiersprache C++ 7 5 8 3) Vorgegeben ist Schlüssel 12 LINKS RECHTS 7 5 15 8 13 14 Abb.: Der Wurzelknoten wird gelöscht. Ergebnis: 260 Die Programmiersprache C++ Schlüssel LINKS 13 RECHTS 7 5 15 8 14 Abb.: Ordnungen und Durchlaufprinzipien Das Prinzip, wie ein geordneter Baum durchlaufen wird, legt eine Ordnung auf der Menge der Knoten fest. Es gibt 3 Möglichkeiten (Prinzipien), die Knoten eines binären Baums zu durchlaufen: 1. Inorder-Durchlauf LWR-Ordnung (1) Durchlaufen (Aufsuchen) des linken Teilbaums in INORDER (2) Aufsuchen der BAUMWURZEL (3) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER RWL-Ordnung (1) Durchlaufen (Aufsuchen) des rechten Teilbaums in INORDER (2) Aufsuchen der BAUMWURZEL (3) Durchlaufen (Aufsuchen) des Teilbaums in INORDER Der LWR-Ordnung und die RWL-Ordnung sind zueinander invers. Die LWR Ordnung heißt auch symmetrische Ordnung. 2. Präorder-Durchlauf WLR-Ordnung (1) Aufsuchen der BAUMWURZEL (2) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER (3) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER 261 Die Programmiersprache C++ WRL-Ordnung (1) Aufsuchen der BAUMWURZEL (2) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER (3) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER Es wird hier grundsätzlich die Wurzel vor den (beiden) Teilbäumen durchlaufen. 3. Postorder-Durchlsauf LRW-Ordnung (1) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER (2) Durchlaufen (Aufsuchen) des rechten Teilbaums in POSTORDER (3) Aufsuchen der BAUMWURZEL Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen. RLW-Ordnung (1) Durchlauden (Aufsuchen) des rechten Teilbaums in POSTORDER (2) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER (3) Aufsuchen der BAUMWURZEL Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen. Funktionsschablonen für das Durchlaufen binärer Bäume // Funktionsschablonen fuer Baumdurchlaeufe template <class T> void inorder(baumKnoten<T>* b, void aufsuchen(T& merkmal)) { if (b != NULL) { inorder(b->holeLinks(),aufsuchen); aufsuchen(b->daten); inorder(b->holeRechts(),aufsuchen); } } template <class T> void postorder(baumKnoten<T>* b, void aufsuchen(T& merkmal)) { if (b != NULL) { postorder(b->holeLinks(),aufsuchen); // linker Abstieg postorder(b->holeRechts(),aufsuchen); // rechter Abstieg aufsuchen(b->daten); } } Aufgaben: Gegeben sind eine Reihe binärer Bäume. Welche Folgen entstehen beim Durchlaufen der Knoten nach den Prinzipien "Inorder (LWR)", "Praeorder WLR" und "Postorder (LRW)". 262 Die Programmiersprache C++ 1) A B C E D F I G J H K L "Praeorder": A B C E I F J D G H K L "Inorder": EICFJBGDKHLA "Postorder": I E J F C G K L H D B A 2) + * A + B * C E D "Praeorder": + * A B + * C D E "Inorder": A*B+C*D+E "Postorder": A B * C D * E + + Diese Aufgabe zeigt einen Strukturbaum (Darstellung der hierarchischen Struktur eines arithmetischen Ausdrucks). Diese Baumdarstellung ist besonders günstig für die Übersetzung eines Ausdrucks in Maschinensprache. Aus der vorliegenden Struktur lassen sich leicht die unterschiedlichen Schreibweisen eines arithmetischen Ausdrucks herleiten. So liefert das Durchwandern des Baums in „Postorder" die Postfixnotation, in „Praeorder“ die Praefixnotation. 263 Die Programmiersprache C++ 3) + A * B C "Praeorder": + A * B C "Inorder": A+B*C "Postorder": A B C * + 4) + * A C B "Praeorder": * + A B C "Inorder": A+B*C "Postorder": A B + C * Balancierte Bäume Strukturbäume 264 Die Programmiersprache C++ 5. C- und C++-Bibliotheken 5.1 Die C++-Standardbibliothek und die STL Die STL (Standard Template Library158) unfaßt nicht die ganze C++Standardbibliothek und auch nicht alle ihre Templates, sie stellt aber den wichtigsten und interessantesten Teil dar. Hilfsfunktionen und -klassen Container Iteratoren Algorithmen Ein-/Ausgabe Nationale Besonderheiten Numerisches String Laufzeittyperkennung Fehlerbehandlung Speicher C-Header Header <utility> <functional> <bitset> <deque> <list> <map> <queue> <set> <stack> <vector> <iterator> <algorithm> <fstream> <iomanip> <ios> <istream> <ostream> <sstream> <streambuf> <complex> <limits> <numerics> <valarray> <string> <typeinfo> <exception> <stdexcept> <memory> <new> <cassert> <cctype> <cerrno> <cfloat> <ciso646> <clocale> <cmath> <csetjmp> <csignal> <cstdarg> <cstdef> <cstdio> <cstdlib> <cstring> 158 Die STL wurde bei Hewlett-Packard von Alexander Stepanow, Meng Lee und ihren Kollegen entwickelt. Sie wurde vom Ansi-/ISO-Komitee als Teil des C++-Standards (ISO/IEC 14882) akzeptiert. Ihr Schwerpunkt liegt auf Datenstrukturen für Container und Algorithmen, die damit arbeiten. 265 Die Programmiersprache C++ <ctime> <cwchar> <cwctype> Abb.: Aufbau der Standardbibliothek Die STL besteht aus drei Komponenten: Container (Auflistungen), Algorithmen und Iteratoren. Ein Container ist eine Datenstruktur zur Speicherung und Organisation von Daten. STL-Container sind als Template-Klassen implementiert. Ein Algorithmus aus der STL definiert eine Verhaltensweise eines Container und verwendet gewisse Funktionen auf einen Container an, um dessen Inhalt irgendwie zu be- oder zu verarbeiten, z.B. sortieren, kopieren. In der STL werden Algorithmen durch Template-Funktionen repräsentiert. Diese Funktionen sind keine MemberFunktionen der Container-Klassen sondern eigenständige Funktionen. Sie können nicht nur mit STL-Containern, sondern auch mit gewöhnlichen C-Arrays oder anwendungsspezifischen Containern arbeiten. Ein Iterator kann als ein verallgemeinerter Zeiger betrachtet werden, der auf Elemente in einem Container zeigt. Ein Iterator kann wie ein Zeiger inkrementiert werden, um auf das nächste Element in dem Container zu zeigen. 5.1.1 Die C-Standard-Library C-Header wurden von der Programmiersprache C übernommen. Ihr Inhalt entspricht der C-Standard-Library (ISO 90). Die Dateinamen ergeben sich aus dem HeaderNamen, so heißt bspw. die Datei zum Header <cmath> dementsprechend math.h. <cassert> Zusicherungen werden mit <cassert> eingebunden. <cctype> In den meisten Sprachen bestehen Zeichen aus einem einzigen Byte. Die Makros und Funktionen zur Manipulation von Zeichen sind komplett oder als Prototyp in cctype enthalten. Sie haben int-Argumente, verwenden aber nur das untere Byte der int-Werte. Wegen der automatischen Typumwandlung können normalerweise auch Zeichenargumente an diese Makros und Funktionen übergeben werden. Der Header enthält die in den folgenden Tabellen aufgeführten Funktionen zum Klassifizieren und Umwandeln von Zeichen: Schnittstelle tolower(z) toupper(z) toascii(z) Bedeutung Gibt z als Kleinbuchstaben zurück Gibt z als Großbuchstaben zurueck Gibt z als ASCII-Zeichen zurück Abb.: Umwandlungsfunktionen aus <cctype> Schnittstelle isalnum(z) isalpha(z) isascii(z) Wahr, wenn z == Buchstabe oder Ziffer Buchstabe ganzzahliger Wert Bereich A..Z, a..z, 0..9 A..Z, a..z ASCII-Werte, 0 .. 127 266 Die Programmiersprache C++ iscntrl(z) isdigit(z) isgraph(z) islower(z) isprint(z) ispunct(z) Steuerzeichen Ziffer Druckbares Zeichen (ohne ‘‘) Kleinbuchstabe Druckbares Zeichen (mit ‘‘) Druckbar, aber weder ‘‘ noch alphanumerisch isspace(z) isupper(z) isxdigit(z) Zwischenraumzeichen Großbuchstabe Hexadezimale Ziffer 0x00..0x1f,0x7f 0..9 0x21..0x7e a..z 0x20..0x7e 0x21..0x2f,0x3a..0x40, 0x5b..0x7e 0x09..0x0d A..Z 0..9, A..F, a..f Klassifizierungsfunktionen aus <cctype> <cerrno> Im Header <cerrno> wird eine globale Variable errno deklariert, deren Wert von vielen Systemfunktionen im Fehlerfall gesetzt wird. <cmath> Die mathematischen Funktionen der folgenden Tabelle sind im Header <cmath> für Grunddatentypen zu finden. Schnittstelle F159 abs(F x) F acos(F x) F asin(F x) F atan(F x) F atan2(F x, F y) F ceil(F x) F cos(F x) F cosh(F x) F exp(F x) F fabs(F x) F floor(F x) F fmod(F x, F y) F frexp(F x, int* pn) F ldexp(F x, int* pn) F log(F x) F log10(F x) F modf(F x, F* i) F pow(F x, Fy) F pow(F x, int y F sin(F x) F sinh(F x) F sqrt(F x) F tan(F x) F tanh(F x) Mathematische Entsprechung arctan(x/y) ln x sinh x tan x tanh x Abb.: Mathematische Funktionen <csignal> <cstddef> Der Header <cstddef> enthält Standarddefinitionen des jeweiligen Systems. 159 Die Abkürzung F bedeutet: Einer der Typen float, double oder long double 267 Die Programmiersprache C++ Name size_t ptrdiff_t wchar_t offsetof NULL Bedeutung Vorzeichenloser Ganzzahlentyp für das Ergebnis von sizeof Ganzzahliger Typ mit Vorzeichen zur Substraktion von Zeigern Typ für „wide characters“. Wide characters sind für Zeichensätze gedacht, bei denen ein Byte nicht ausreicht. Abstand eines Strukturelements vom Strukturanfang in Bytes Null-Zeiger (dasselbe wie 0 oder 0L) Abb.: Standarddefinitionen aus <cstddef> <cstdarg> Funktionen mit Argumentlisten variabler Länge enthalten eine Ellipse in der Parameterliste, z.B. (int ...). Die Funktionen benötigen die Datentypen und Makros des Header <cstdarg>. <cstdlib> Die erste wichtige Gruppe von Funktionen in cstdlib sind Funktionen zur Umwandlung von Daten. Ihre Hauptaufgabe besteht darin, Daten von einem Datentyp in einen anderen umzuwandeln. Funtionsprototyp double atof(const char *s) int atoi(const char *s) long atol(const char *s) char *ecvt(double wert,int n, int *dec, int *sign) char *fcvt(double wert,int n, int *dec, int *sign) char *gcvt(double value, int n, char *buf) char *iota(int value, char *s, int radix) char *ltoa(long value, char *s, int radix) double strtod(const char *s, char **endptr) long strtol(const char *s, char **endptr, int radix) unsigned long strtoul(const char *s, char **endptr, int radix) char *ultoa(unsigned long value, char *s, int radix) Beschreibung wandelt einen einen String in einen float-Wert wandelt einen einen String in einen int-Wert wandelt einen einen String in einen long-Wert wandelt einen String in einen double-Wert wandelt einen String in einen long-Wert wandelt einen String in einen unsigned long-Wert Abb.: Funktionsprototypen von Umwandlungsfunktionen Die folgenden mathematischen Funktionen gehören zum Header <cstdlib> Schnittstelle int abs(int x) long abs(long x) long labs(long x) div_t160 div(int z, int n) ldiv_t div(long z, long n) ldiv_t ldiv(long z, long n) int rand() Mathematische Entsprechung Betrag Betrag Betrag Pseudozufallszahl zwischen 0 und RAND_MAX. RAND_MAX ist die größtmögliche Pseudozufallszahl Initialisiert den Zufallszahlengenerator void srand(unsigned seed) Abb.: Mathematische Funktionen aus <cstdlib> 160 div_t ist eine vordefinierte Struktur, die das Divisionsergebnis und den Rest enthält: struct div_t { int quot; // Qotient int rem; // Rest } 268 Die Programmiersprache C++ Weitere Funktionen für diverse Oprationen Schnittstelle void abort(void) void atexit(void (*f)()) Bedeutung gibt den Exit-Code 3 zurück Trägt die Funktion f in eine Liste von Funktionen ein, die vor dem normalem Programmende aufgerufen werden. void exit(int status) Normales Programmende, der Status wird an das Betriebssystem gemeldet (Exit-Code 0) int system(const char* B) Befehl B an den Kommandointerpreter des Betriebssystems geben char* getenv(const char* E) Wert der Environmemt-Variablen E long atoi(const char* s) Interpretiert den C-String s als int-Zahl long int atol(const char* s) Interpretiert den C-String s als long-Zahl double atof(const char* s) Interpretiert den C-String s als double-Zahl void* bsearch(const void* key, const Binäre Suche, key zeigt auf den Schlüssel, der void* base, size_t size, int im Feld base mit n Elementen gesucht wird. Die (*cmp)(const void*, const void*)) Größe der Feldelemente ist size, die Vergleichsfunktion ist cmp. void qsort(void*, size_t, size_t, int Quicksort (*) const void*, const void*)) void lfind(const void *key, const Durchsuchen eines Array sequentieller void *base, size_t *, size_t width, Datensätze linear nach einem Schlüsselbegriff int (*fcmp)(const void *, const void *)) void lsearch(const void *key, const Durchsuchen einer sortierten oder unsortierten void *base, size_t *, size_t width, Tabelle linear nach einem Schlüsselbegriff int (*fcmp)(const void *, const void *)) Abb.: Ausgewählte Funktionen aus <cstdlib> <cstring> String-Funktionen, deren Prototypen in cstring stehen verwenden üblicherweise Zeigerargumente und geben Zeiger oder ganzzahlige Werte zurück Funktion int strcmp(const char *s1, const char *s2) size_t strcspn(const char *s1, const char *s2) char *strcpy(char *s1, char *s2) char *strerror(int errnum) size_t strlen(const char *s) char *strncat(char *s1, const char *s2, size_t n) char strncmp(const char *s1, char* s2, size_t n) char strnicmp(const char *s1, char* s2, size_t n) char *strncpy(char *s1, const char *s2, size_t n) char *strnset(char *s1, int ch, size_t n) char strpbrk(const char *s1, const char *s2) char *strrchr(const char *s, int ch) char *strrev(char *s) char *strset(char *s, int ch) size_t strspn(const char *s1, const char *s2) char *strstr(const char *s1, const char *s2) char *strtok(char *s1, char *s2) char *strupr(char *s) Beschreibung vergleicht zwei Strings findet einen Substring in einem String kopiert einen String ANSI-spez. Fehlernummer wandelt einen String in Kleinbuchstaben um hängt n in char s2 an s1 an vergleicht die ersten n Zeichen zweier Strings vergleicht die ersten n Zeichen zweier Strings, Groß- /Kleinschreibung spielt keine Rolle kopiert n Zeichen von s2 nach s1 setzt die ersten n Zeichen eines String auf "ch" findet Zeichen aus s2 in s1 findet das letzte Zeichen von "ch" im String kehrt die Zeichen eines String um setzt alle Zeichen eines Strings auf "ch" durchsucht s1 nach Zeichen in s2 durchsucht s1 nach s2 durchsucht s1 nach Token, s1 enthält das / die Token, s2 enthält die Begrenzer wandelt einen String in Großbuchstaben um Abb.: Funktionen zur Manipulation von Strings 269 Die Programmiersprache C++ <ctime> Der Header <ctime> enthält verschiedene Funktionen zur Bearbeitung und Auswertung der Systemzeitinformation. Datentypen: Neben bekannten Typen NULL und size_t sind definiert: clock_t time_t struct tm int int int int int int int int int // Datentyp für CPU-Zeitangaben // Datentyp für Datums- und Zeitangaben // Struktur mit mindestens folgenden Elementen tm_sec // Sekunden 0 .. 59 tm_min // Minuten 0 .. 59 tm_hour // Stunden 0 .. 23 tm_mday // Monatstag 1 .. 31 tm_mon // Monat 0.. 11 tm_year // Jahr seit 1900 tm_wday // Wochentag seit Sonntag 0 .. 6 tm_yday // Tag seit 1. Januar 0 .. 365 tm_isdst // is daylight saving time, Werte: // Sommerzeit (> 0), Winterzeit (0) // undefiniert (-1) Funktionen: char * asctime(const tm*) char * ctime(const time_t*) Die Funktionen wandeln die im tm-Format oder time_t Format vorliegende Zeit in einen formatierten C-String um. clock_t clock() Gibt die seit Programmstart verstrichene CPU-Zeit in „Ticks“ zurück. Die „Ticks“ können in Sekunden umgerechnet werden, wenn durch die vordefinierte Konstante CLOCKS_PER_SEC dividiert wird double difftime(time_t t1, time_t t2) Ermittelt die Differenz der Zeiten in Sekunden size_t strftime(char* puffer, size_t Wandelt die im tm-Format vorliegende Zeit in max, const char* format, const tm* z) einen formatierten C-String um, wobei das Ergebnis im Puffer puffer abgelegt wird und format einen C-String mit Formatangaben darstellt. Die Anzahl der in den Puffer geschriebenen Zeichen wird zurückgegeben, falls sie < max ist, ansonsten ist das Ergebnis der Funktion 0. Die in einem Unix-Sytem möglichen Formate können mit man strftime erfragt werden. Formatierungsfunktionen umfassen: %a Abgekürzter Name des Wochentags %A Kompletter Name des Wochentags %b Abgekürzter Name des Monats %B Kompletter Name des Monats %c Datums- und Zeitinformationen %d Tag des Monats (01 bis 31) %H Stunde (00 bis 23)) %I Stunde (00 bis 12) %j Tag des Jahres (001 bis 366) %m Monat (01 bis 12) %M Minuten (00 bis 59) %S Sekunden (00 bis 59) %w Wochentag (0 bis 6) %x Datum %X Zeit %Y Jahr mit Jahrhundert %Z Name der Zeitzone %% Zeichen % tm* gmtime(const time_t* z) Beide Funktionen wandeln die in *z vorliegende tm* localtime(const time_t* z) 270 Die Programmiersprache C++ time_t time(time_t* z) Zeit in die Struktur tm um. Dabei gibt localtime() die lokale Ortszeit unter Berücksichtigung von Sommer- und Winterzeit zurück, während gmtime() die UTC (Universal Time Coordinated, entspricht GMT (Greenwich mean Time)) zurückgibt. Gibt die momentane Kalenderzeit zurück (die Zeit in Sekunden seit dem 1. Jan. 2000, 00:00:00 GMT) bzw. –1 bei Fehler. Falls z ungleich NULL ist, wird der Rückgabewert an der Stelle z hinterlegt. Abb.: Parameter der Datums- und Zeitfunktionen Der Umgebungsstring TZ hat folgende Syntax: TZ = zzz[+/-]d[d]{lll} zzz String aus drei Zeichen, der die lokale Zeitzone, bspw. EST für Eastern Standard Time, angibt. [+/-]d[d] enthält eine Anpassung für den Unterschied der lokalen Zeitzone und der GMT (Grennwich Mean Time). Positive Zahlen bedeuten eine Anpassung in westliche, negative Zahlen eine Anpassung in östliche Richtung. {lll} Anpassung der lokalen Zeitzone an die Sommer-/Winterzeit 271 Die Programmiersprache C++ 5.1.2 Hilfsfunktionen und -klassen Der Header <utility> deklariert verschiedene Templates von Hilfsfunktionen und –klassen, die in der ganzen Bibliothek benutzt werden. 5.1.2.1 Paare Das Template pair erlaubt die Kombination heterogener Wertepaare: template <class T1, class T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(); pair(const T1& x, const T2& y); template<class U, class V> pair(const pair<U, V> &p); }; Der Standardkonstruktor initialisiert beide Bestandteile mit ihrem jeweiligen Standardkonstruktor. Gleichheit und Vergleich sind ebenfalls definiert: // gibt first == y.first und x.second == y.second zurück template <class T1, class T2> bool operator ==(const pair<T1, T2>& x, const pair<T1, T2>& y); template <class T1, class T2> bool operator <(const pair<T1, T2>& x, const pair<T1, T2>& y); Rückgabewert true, falls x.first < y.first bzw. false, falls y.first < x.first. Falls beide Bedingungen nicht zutreffen, wird x.second < y.second zurückgegeben. Die Hilfsfunktion template <class T1, class T2> pair<T1, T2> make_pair(const T1& x, const T2& y); erzeugt aus den Parametern ein Paar. Die beteiligten Typen werden aus den Parametern abgeleitet, z.B.: return make_pair(1,2.7); gibt ein pair<int, double>-Objekt zurück. 5.1.2.2 Funktionsobjekte In einem Ausdruck wird der Aufruf einer Funktion durch das von der Funktion zurückgegebene Ergebnis ersetzt. Die Ausgabe der Funktion kann von einem Objekt übernommen werden. Dazu wird der Funktionsoperator () mit der Operatorfunktion operator()() überladen. Ein Objekt kann dann wie eine Funktion aufgerufen werden. Ein algorithmisches Objekt dieser Art wird Funktionsobjekt oder Funktor genannt. 272 Die Programmiersprache C++ Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften von Objekten haben. Sie können erzeugt, als Parameter übergeben oder in ihrem Zustand verändert werden. In <functional> sind Klassen definiert, die zum Erzeugen verschiedener Funktionsobjekte dienen. Klassen dieses Header erben von einer der Klassen: template <class Arg, class Result> struct unary_function { typedef Arg argument_type; typedef Result result_type; } template <class Arg1, class Arg2, class Result> struct binary_function { typedef Arg1 first_argument_type; typedef Arg2 second_argument_type; typedef Result result_type; } in Abhängigkeit davon, ob es sich um unäre oder binäre Operationen handelt, z.B.: template <class T> struct plus : binary_function<T,T,T> { T opertor()(const T& x, const T& y) const; } Arithmetische, vergleichende und logische Operationen In der STL werden für alle möglichen Grundoperationen Funktionsobjekte vordefiniert. // arithmetische Operationen: template template template template template template <class <class <class <class <class <class T> T> T> T> T> T> struct struct struct struct struct struct plus; minus; multiplies; divides; modulus; negate; // // // // // // x + x x * x / x % -x y y y y y T> T> T> T> T> T> T> struct struct struct struct struct struct struct equal_to; // x == y not_equal_to; // x != y greater; // x > y less; // x < y greater_equal; // x >= y less_equal; // x <= y equal_to; // x == y // Vergleiche template template template template template template template <class <class <class <class <class <class <class // logische Operationen template <class T> struct logical_and; template <class T> struct logical_or; template <class T> struct logical_not; // x && y // x || y // !x Funktionsobjekte zum Negieren logischer Prädikate „not1()“ und „not2()“ sind Funktionen, die ein Funktionsobjekt zurückgeben, dessen Aufruf ein Prädikat negiert. 273 Die Programmiersprache C++ Binden von Argumentwerten Diese Funktionen wandeln binäre in unäre Funktionsobjekte um, indem eines der beiden Argumente an einen Wert gebunden wird. Sie akzeptieren ein Funktionsobjekt mit zwei Argumenten und einem Wert x. Sie liefern ein unäres Funktionsobjekt zurück, dessen erstes („Funktionsadapter bind1st()“) bzw. zweites Argument („Funktionsadapter161 bind2nd()“) an den Wert x gebunden ist. Bsp.: Alle Elemente einer Menge mit 3 multiplizieren // Elemente mit 3 mutiplizieren transform(v.begin(), v.end(), ostream_iterator<int>(cout," "), bind2nd(multiplies<int>(),3)); // Quellbereich // Zielbereich // Operation Der Ausdruck bind2nd(multiplies<int>(),3)) dient dazu die zweistellige Multiplikation, die durch multiplies<int>() definiert wird, mit dem Wert 7 zu einer einstelligen Operation, wie sie von transform() her erwartet wird, zu verknüpfen. Mit dem gleichen Prinzip wird dem Iterator pos das erste Element mit einem Wert, der kleiner ist 5, zugewiesen: // Erstes Element mit Wert kleiner als 5 finden vector<int>::iterator pos; pos = find_if(v.begin(), v.end(),bind2nd(less<int>(),5)); Bsp.: Anwendung eines selbstdefinierten Funktionsobjekts Funktionsadaptern bind1st() und bind2nd(). hoch mit #include <iostream> #include <vector> #include <algorithm> using namespace std; /* Selbstdefiniertes Funktionsobjekt hoch */ #include "fohoch.h" int main() { vector<int> v; // Elemente 1 bis 6 in v einfuegen for (int i = 1; i <= 6; i++) v.push_back(i); // Elemente mit 3 mutiplizieren transform(v.begin(), v.end(), ostream_iterator<int>(cout," "), bind2nd(multiplies<int>(),3)); cout << endl; // 3 hoch alle Elemente ausgeben transform(v.begin(), v.end(), ostream_iterator<int>(cout," "), bind1st(hoch<int>(),3)); cout << endl; // Alle Elemente hoch 3 ausgeben transform(v.begin(), v.end(), ostream_iterator<int>(cout," "), // Quellbereich // Zielbereich // Operation // Quellbereich // Zielbereich // Operation // Quellbereich // Zielbereich 161 Funktions-Adapter ermöglichen die vordefinierten Funtionsobjekte zu kombinieren oder mit bestimmten Werten zu versehen. Auch sie werden in der Header-Datei functional definiert 274 den Die Programmiersprache C++ bind2nd(hoch<int>(),3)); cout << endl; // Operation } Funktionsobjekte können auch selbst definiert werden. Damit auf sie auch Funktionsadapter angewendet werden können, müssen sie von den vordefinierten Strukturen unary_function bzw. binary_function abgeleitet werden. #include <functional> template <class T> struct hoch : public binary_function<T, int, T> { T operator() (const T& basis, int exp) const { T res = 1; while (exp > 0) { res = res * basis; --exp; } return res; } }; Zeiger auf Funktionen in Objekte umwandeln Die Funktion ptr_fun() wandelt einen Zeiger auf eine Funktion in ein Funktionsobjekt um. Die Funktion kann ein oder zwei Parameter haben. Bsp.: 275 Die Programmiersprache C++ 5.2 Container Die Container-Klassen dienen dazu, eine Menge von Elementen in einer bestimmten Art und Weise zu verwalten. Da an Mengen verschiedene Anforderungen gestellt werden, gibt es auch verschiedene Container-Klassen, die diese Anforderungen erfüllen. Sequentielle und assoziative Container Die Container werden in sequentielle und assoziative Container unterteilt: - - Sequentielle Container sind geordnete Mengen, in denen jedes Element eine bestimmte Position besitzt, die durch den Zeitpunkt und den Ort des des Einfügens bestimmt wird. Vordefinierte sequentielle Container sind Vektoren, Deques und Listen. Assoziative Container sind sortierte Mengen, bei denen die Position der Elemente durch ein Sortierkriterium bestimmt wird. Werden bspw. 6 Elemente nacheinander in eine Menge eingefügt, besitzen sie eine Reihenfolge, die durch ihren Wert definiert wird. Vordefinierte assoziative Container sind Sets, Multisets sowie Maps, Multimaps. Neben den fundamentalen Container-Klassen existieren noch spezielle ContainerAdapter, die die fundamentalen Container auf spezielle Anforderungen abbilden: Stack, Queue, Priority Queue. Die Container-Adapter sind mit der STL in den Standard aufgenommen, für die Arbeit mit Iteratoren und Algorithmen stehen sie aber nicht zur Verfügung. Sie bilden normale Container-Klassen. Soweit möglich besitzen alle Container die gleiche Schnittstelle. Die Schnittstelle wird (leider) nicht in einer abstrakten Klasse definiert sondern durch Tabellen, in der die gemeinsamen Eigenschaften aller Container festgelegt werden. Container-Typen Datentyp container::value_type container::reference container::const_reference container::iterator container::const_iterator container::difference-type container::size_type Bedeutung Liefert den Datentyp der Elemente im Container Vorhanden bei: Vektoren, Deques. Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp der Elemente im Container als Referenz Vorhanden bei: vektoren, Deques, Listen, Sets, Maps, Multimaps Referenz auf konstantes Container-Element Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp für Iteratoren des Containers Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Iteratortyp für Container mit konstanten Elementen Vorhanden bei Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp für Abstandsabgeben (vorzeichenbehafteter ganzzahliger Datentyp) des Containers Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp für Größenbagaben des Containers 276 Die Programmiersprache C++ container::reserve_iterator container:: const_reserve_iterator container::key_type container::key_ompare container::value_compare Vorhanden bei: Vektoren, Deques, Listen, sets, Multisets, Maps, Multimaps Liefert den Datentyp für Reserve-Iteratoren des Containers Vorhanden bei Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp für Reserve-Iteratoren eines konstanten Containers Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Liefert den Datentyp für das Sortierkriterium bei assoziativen Containern (bei Sets und Multisets ist gleich value_type) Vorhanden Sets, Multisets, Maps, Multimaps Liefert den Datentyp für für Vergleichsfunktionen der Schlüssel bei assoziativen Containers Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert den Datentyp für für Vergleichsfunktionen der Elemente bei assoziativen Containers, bei Sets und Multisets ist dies gleich key_compare, bei Maps und Multimaps ist es eine Hilfsklasse, die dafür sorgt, das von den Elementen nur der Schlüsselwert verglichen wird. Vorhanden bei: Sets, Multisets, Maps, Multimaps Abb.: Container-Datentypen, die von der C++-Standardbibliothek bereitgestellt werden Jeder Container stellt einen öffentlichen Satz von Methoden zur Verfügung. Methoden zum Erzeugen, Kopieren und Zerstören Methode container::container() Bedeutung Default-Konstruktor, erzeugt einen leeren Container. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps explicit Copy-Konstruktor container::container(const container& Erzeugt einen neuen Container als Kopie des c) existierenden Containers c, für jedes Element in c wird der Copy-Konstruktoe aufgerufen. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps explicit Erzeugt einen Container mit anz Elementen, die container::container(size_type anz) elemente werden jeweils mit deren DefaultKonstruktor erzeugt. Vorhanden bei: Vektoren, Deques Listen container::container(size_type anz, Erzeugt einen Container mit anz Elementen, die const T& wert) Elemente sind jeweils Kopien von Wert. T ist der Datentyp der Elemente. Vorhanden bei: Vektoren, deques, Listen. container::container(InputIterator Erzeugt einen Container mit einer Kopie aller anf, InputIterator end) Elemente im Bereich [anf,end). Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps container::~container() Destruktor, zerstört alle elemente und gibt den Speicherplatz frei. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Funktionen zur Größe 277 Die Programmiersprache C++ Methode size_type container::size() const Bedeutung Liefert die aktuelle Anzahl von Elementen im Container Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings bool container::empty() const Liefert, ob der Container leer ist Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings size_type container::max_size() const Liefert die maximale Anzahl von Elementen, die der Container enthalten kann Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps Kapazitäts-Funktionen Methode Bedeutung size_type container::capacity() const Liefert die Anzahl von Elementen, die ohne Reallokierung aufgenommen werden können Vorhanden bei: Vektoren, Strings void container::reserve(size_type Reserviert für einen Vektor Speicherplatz für anz) mindestens anz Elemente, sofern nicht schon genug Speicherplatz zur Verfügung steht Vorhanden bei: Vektoren, Strings Vergleichsoperatoren Für alle Container-Klassen der Standard-Template-Library werden die Vergleichsoperatoren == und < definiert. Andere Vergleiche werden auf diese Operatoren umgesetzt. Methode bool operator == (const c1, const container& c2) Bedeutung container& Prüft die Container c1 und c2 auf Gleichheit, liefert true, wenn die beiden Container die gleiche Anzahl von Elementen Besitzen und jedes Element in c1 == dem entsprechenden element in c2 ist. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings bool operator < (const container& c1, Prüft, ob der Container c1 „kleiner als“ c2 ist. const container& c2) Liefert true, wenn der erste Container „lexikalisch kleiner“ als der zweite ist. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings Zuweisungen Methode container& container::operator = (const container& c) void container::assign(Size anz) Bedeutung Weist dem Container die Elemente des Containers c zu Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings Weist dem Container anz Elemente zu, die mit deren Default-Konstruktor erzeugt werden Vorhanden bei: Vektoren, Deques, Listen 278 Die Programmiersprache C++ void container::assign(Size anz, const T& wert) void container::assign(InputIterator anf, InputIterator end) Weist dem Container anz Kopien von wert zu Vorhanden bei: Vektoren, Deques, Listen, Strings Weist dem Container alle Elemente im Bereich [anf,end) zu Vorhanden bei: Vektoren, Deques, Listen, Strings void container::swap(container& c) Vertauscht die Elemente bei dem Container c Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings void container::swap(container& c1, Vertauscht die Elemente des Containers c1 mit container& c2) den Elementen vom Container c2 Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings Direkter Element-Zugriff Methode reference container::at(size_type idx) const_reference container::at(size_type idx) const reference container::operator[](size_type idx) const_reference container::operator [](size_type idx) const T& map::operator[](const key_type key) const T& map::operator[](const key_type key) const reference container::front() const_reference container::front() const reference container::back() const_reference container::back() const Bedeutung Liefert das Element mit dem Inhalt idx Vorhanden bei: Vektoren, Deques, Strings Liefert das Element mit dem Inhalt idx, im Gegensatz zu at() finder keine Überprüfung von idx statt. Der Programmierer muß sicherstellen, daß idx einen erlaubten Wert besitzt Vorhanden bei: Vektoren, Deques, Strings Liefert das Element mit dem Schlüsselwert key zum Abfragen und Setzen. Existiert kein Element mit solchem Schlüsselwert, wird automatisch eines angelegt und mit dem Default-Konstruktor des Werte-Typs initialisiert. T ist der datentyp der zum Schlüssel gehörenden Werte Vorhanden bei: Maps Liefert das erste Element (mit dem Index 0). Der Aufrufer muß sicherstellen, daß ein solches Element existiert (size() > 0). Vorhanden bei: Vektoren, Deques, Listen Liefert das letzte Element (das Element mit dem Index size()-1) Vorhanden bei: Vektoren, Deques, Listen Iterator-Funktionen Methode iterator container::begin() const_iterator container::begin() const Bedeutung Liefert einen Iterator für den Anfang des Containers (die Position des ersten elements im Container). Ist der Container leer, entspricht dies container::end() Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings iterator container::end() Liefert einen Iterator für das Ende des Containers const_iterator container::end() const (die Position hinter dem letzten Element im Container) Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings reverse_iterator container::rbegin() Liefert einen Reverse-Iterator für den Anfang const_reverse_iterator container:: eines umgekehrten Durchlaufs durch den rbegin() const Container Vorhanden bei: Vektoren, Deques, Listen, Sets, 279 Die Programmiersprache C++ reverse_iterator container::rend() const_reverse_iterator container::rend() const Multisets, Maps, Multimaps, Strings Liefert einen Reverse-Iterator für das Ende eines umgekehrten Durchlaufs durch den Container Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings Einfügen und Löschen Methode iterator container::insert(iterator pos) pair<iterator,bool> container::insert(const T& wert) iterator container::insert(const T& wert) iterator container::insert(iterator pos, const T& wert) Bedeutung Fügt an die Position des Iterators pos ein Element ein, das mit dessen Default-Konstruktor erzeugt wird, liefert die Position des neuen Elements. Vorhanden bei: Vektoren, Deques, Listen, strings Fügt in einen assoziativen Container eine Kopie von wert als Element ein Vorhanden bei: Sets, Multisets, Maps, Multimaps Fügt an die Position des Iterators pos eine Kopie von wert als Element ein, liefert die Position des neuen Elements void container::insert(iterator pos, Fügt an die Position des iterators pos anz Kopien size_type anz, const T& wert) von wert als Element ein. T ist der Datentyp der Elemente, bei Vektoren und Deques können dadurch Verweise auf andere Elemente ungültig werden. Vorhanden bei: Sets, Multisets, Maps, Multimaps void container::insert(InputIterator Fügt in assoziative Container Kopien aller anf, InputIterator end) Elemente des Bereichs von anf bis end ein. Vorhanden bei: Sets, Multisets, Maps, Multimaps void container::insert(iterator pos, Fügt an die Position des Iterators pos Kopien InputIterator anf, InputIterator end) aller Elemente des Bereichs von anf bis end ein. Vorhanden bei: Vektoren, Deques, Listen, Strings void container::push_front(const T& Fügt als erstes Element eine Kopie von wert ein. wert) T ist der Datentyp der Elemente. Vorhanden bei: Deques, Listen void container::push_back(const T& Fügt als letztes Element eine Kopie von wert ein. wert) T ist der Datentyp der Elemente. Vorhanden bei: Vektoren, Deques, Listen size_type container::erase(const T& Entfernt bei assoziativen Containern die Elemente wert) mit dem Wert wert. Für das entfernte Element wird dessen Destruktor aufgerufen. Vorhanden bei: Sets, Multisets, Maps, Multimaps. iterator container::erase(iterator Entfernt das Element an der Position des Iterators pos) pos, liefert die Position des ursprünglichen Nachfolgers des entfernten Elements bzw. end() Für das entfernte Element wird dessen Destruktor aufgerufen. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings iterator container::erase(iterator Löscht die Elemente im bereich [anf,end), liefert anf, iterator end) die Position des ursprünglichen Nachfolgers des letzten Elements bzw. end(). Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings void container::pop_front() Entfernt das erste Element im Container. Vorhanden bei: Vektoren, Deques, Listen void container:: pop_back() Entfernt das letzte Element im Container. Vorhanden bei: Vektoren, Deques, Listen 280 Die Programmiersprache C++ void container::resize(size_type anz) Ändert die Anzahl der Elemente im Container auf anz, wächst dadurch die Größe werden zusätzliche Elemente mit dem Default-Konstruktor erzeugt und am Ende angefügt. Schrumpft dadurch die Größe, werden die hinteren Elemente zerstört (mit Aufruf von deren Destruktoren). Vorhanden bei: Vektoren, Deques, Listen void container::resize(size_type anz, Ändert die Anzahl der Elemente im Container auf T& wert) anz. Wächst dadurch die Größe, werden zusätzliche Elemente als Kopie von wert erzeugt und am Ende eingefügt. Vorhanden bei: Vektoren, Deques, Listen void container::clear() Leert den Container (löscht alle Elemente im Container), für die zerstörten elemente werden deren Konstruktoren aufgerufen, dadurch werden alle Verweise auf die Elemente ungültig. Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps 5.2.1 Bitset Der Header <bitset> definiert eine Template-Klasse und zugehörige Funktionen zur Darstellung und Bearbeitung von Bitfolgen fester Größe template <size_t N> class bitset; Die "bitset"-Klasse unterstützt Operationen mit Mengen vin Bits, z.B. reset(), set(), size(), to_string(). flip(), 5.2.2 Deque Der Name „Deque“ ist die Abkürung für double ended queue. Darunter versteht man eine Warteschlange, die das Hinzufügen und Entnehmen von Elementen sowohl am Anfang als auch am Ende erlaubt. template <class T> class deque; Datentyp pointer const_pointer reverse_iterator const reverse_iterator Bedeutung Zeiger auf Deque-Element Zeiger auf konstantes Deque-Element Iterator für Durchlauf vom Ende zum Anfang Iterator für Durchlauf vom Ende zum Anfang mit konstanten Elementen Abb.: Zusätzliche öffentliche Datentypen für deque Die folgende Abbildung zeigt die Methoden einer Deque: Methode mit Rückgabetyp deque<T>() deque<T>(const deque<T>&) Bedeutung Konstruktor Kopierkonstruktor 281 Die Programmiersprache C++ ~deque<T>() iterator begin() const_iterator begin() iterator end() const_iterator end() size_type max_size() size_type size() bool empty() void swap(deque<T>&) operator=(const deque<T>&) bool operator==(const deque<T>&) bool operator!=(const deque<T>&) bool operator<(const deque<T>&) bool operator>(const deque<T>&) bool operator<=(const deque<T>&) bool operator>=(const deque<T>&) Destruktor Anfang des Containers Anfang des Containers mit konstanten Elementen Position nach dem letzen Element end() für Container mit konstanten Elementen Maximal mögliche Größe des Containers Aktuelle Größe des Containers size==0 bzw. begin() == end() Vertauschen mit Argument-Container Zuweisungsoperator Operator == Operator != Operator < Operator > Operator <= Operator <= Abb.: Methoden der Klasse deque 5.2.3 List Die Liste dieses Abschnitts ist eine doppelt-verkettete Liste, die das Hinzufügen und Entnehmen von Elementen sowohl am Anfang als auch am Ende erlaubt. Jedes Element in der Liste zeigt auf seinen Vorgänger und seinen Nachfolger. Dadurch ist kein wahlfreier Zugriff mehr möglich. template <class T> class list; Datentyp pointer const_pointer reverse_iterator const reverse_iterator Bedeutung Zeiger auf Listen-Element Zeiger auf konstantes Listen-Element Iterator für Durchlauf vom Ende zum Anfang Iterator für Durchlauf vom Ende zum Anfang mit konstanten Elementen Abb. Zusätzliche öffentliche Datentypen für list Die Klasse list stellt die folgende, spezielle Methoden zur Verfügung: Methode mit Rückgabetyp Bedeutung void liste::unique() Kollabiert Folgen von gleichen Elementen zu void liste:.unique(BinaryBoolFunc op) einem Element, entfernt aus einer Liste jeweils alle Elemente, die einem Element mit gleichem Wert unmittelbar folgen bzw. bei denen op(elem,folgeElem) wahr ist. Für entfernte Elemente werden deren Destruktoren aufgerufen. Diese Funktion ist die für Listen optimierte Version des gleichnamigen Algorithmus. void liste::splice(iterator pos, Verschiebt alle Elemente aus c in die Liste, für die liste& c) die Funktion aufgerufen wird, vor das Element mit Position pos. C wird dadurch geleert. Der Programmierer muß sicherstellen, daß es sich bei c um einen anderen Container handelt. void liste::splice(iterator pos, Verschiebt das Element aus c an der Position liste& c, iterator cPos) cPos in die Liste, für die die Funktion aufgerufen 282 Die Programmiersprache C++ void liste::splice(iterator pos, liste& c, iterator cAnf, iterator cEnd) void liste:.sort() void liste::sort(CompFunc op) void liste::merge(liste& c) void liste::merge(liste& c, CompFunc op) void liste::reverse() wird, vor das Element mit der Position pos. Falls c ein anderer Container ist, wird c dadurch ein Element kleiner. Verschiebt alle Elemente des Bereichs [cAnf,cEnd) aus c in die Liste für die die Funktion aufgerufen wird, vor das Element mit Position pos. Bei c darf es sich um den gleichen Container handeln. In diesem Fall darf pos nicht im verschobenen Bereich liegen, und der Bereich wird innerhalb des Containers verschoben. Falls c ein anderer Container ist, wird c dadurch entsprechend kleiner. Der Programmierer muß sicherstellen, daß sich der verschobene Bereich wirklich in c befindet. ansonsten ist das Verhalten undefiniert. Sortiert alle Elemente anhand < bzw. op. Op kann optional zum Sortierem übergeben werden und dient dazu, jeweils zwei Elemente zu vergleichen: op(elem,elem). Diese Fuktion ist die für Listen optimierte Version der Algorithmen sort() und stable_sort(). Verschiebt alle Elemente aus der vorsortierten Liste c in den vorsortierten Container, für den die Funktion aufgerufen wird, so daß die gemeinsame Menge ebenfalls sortiert ist. Op kann optional übergeben werden und dient dazu, jeweils zwei Elemente zu vergleichen: op(elem,cElem), c wird dadurch geleert. Diese Funktion ist die für Listen optimierte Version des gleichnamigen Algorithmus. Kehrt die Reihenfolge der Elemente in einer Liste um. Diese Funktion ist die für Listen optimierte Funktion des Algorithmus reverse(). Abb.: Spezielle Methoden der Klasse list Bsp162.: Demonstration Listen-Container // ANSI C Headers #include <stdlib.h> // C++ STL Headers #include <algorithm> #include <iostream> #include <list> #include <string> int main(int argc, char *argv[]) { string namen[] = { "Juergen", "Robert", "Philomena", "Ernst", "Andreas", "Liesel", "Christian" }; const int N = sizeof(namen)/sizeof(namen[0]); list< string > yrl; list< string >::iterator iter; for ( int i = 0; i < N; ++i) yrl.push_back(namen[i]); for ( iter = yrl.begin(); iter != yrl.end(); ++iter ) cout << *iter << endl; // Find "Ernst" cout << "\nSiehe nach Ernst" << endl; 162 PR52301 283 Die Programmiersprache C++ iter = find( yrl.begin(), yrl.end(), "Ernst" ); // Maria soll vor Ernst stehen (Einfuegen davor) if ( iter != yrl.end() ) { cout << "\nFuege Maria vor Ernst ein" << endl; yrl.insert( iter, "Maria" ); } else { cout << "\nKann Ernst nicht bestimmen" << endl; } for ( iter = yrl.begin(); iter != yrl.end(); ++iter ) cout << *iter << endl; return( EXIT_SUCCESS ); } 5.2.4 Map Die Klasse map<Key, T>, eingebunden durch den Header <map>, speichert Paare von Schlüsseln und zugehörigen Daten. Der Schlüssel ist eindeutig, es kann also keine zwei Datensätze zu demselben Schlüssel geben. template <class Key, class T, class Compare = less<Key>> // Schluessel // Daten /* Standardvergleich für Sortierung */ class map; map ist ein assoziativer Container: die Daten werden durch direkte Angabe des Schlüssels gefunden. Die Schlüssel liegen sortiert vor. Default-Wert ist das Funktionsobjekt less, d.h. Elemente werden, soweit nicht anders vorgegeben, aufsteigend sortiert. Der Typ eines map-Elements ist pair<const Key,T>. Im Gegensatz zu Maps erlauben Multimaps keine Duplikate: template <class Key, class T, class Compare = less<Key> // Schluessel // Daten /* Standardvergleich für Sortierung */ class Allocator = allocator<T> > class map; Die Elemente einer Map oder Multimap können Wertepaare von zwei beliebigen Typen Key und T sein. Map und Multimap sind wie alle assoziativen Container-Klassen als balancierte Bäume implementiert. Wegen der automatischen Sortierung und der Dateiorganisation ergibt sich ein gutes Zeitverhalten beim Suchen und Finden von Elementen. Eine „map“ besitzt zusätzlich folgende Datentypen Datentyp pointer const_pointer reverse_iterator const_reverse_iterator key_type value-type mapped-type Bedeutung Zeiger auf Map-element Zeiger auf konstantes Met-Element Iterator für Durchlauf vom Ende zum Anfang Iterator für Durchlauf vom Ende zum Anfang mit konstanten Elementen Entspricht Key Entspricht pair<const key, T> Entspricht T 284 Die Programmiersprache C++ key_compare value_compare Compare Klasse für Funktionsobjekte, vgl value_comp() bzw. Methoden163: Methode mit Rückgabetyp size_type container::count(const T& wert) const iterator container::find(const T& wert) const_iterator container::find(const T& wert) const iterator container::lower_bound(const T& wert) const_iterator container::lower_bound(const T& wert) const iterator container::upper_bound(const T& wert) const_iterator container::upper_bound(const T& wert) const pair<iterator,iterator> container::equal_range(const T& wert) pair<const_iterator,const_iterator> container::equal_range(const T& wert) const value_compare container::value_comp() 163 Bedeutung Liefert die Anzahl der Elemente, die den Wert wert besitzen. T ist der Datentyp des sortierten Wertes: - bei Sets und Multisets der Typ der Elemente - bei Maps und Multimaps der Typ der Schlüssel Vorhanden bei: Sets, Multisets, maps, Multimaps Liefert die Position vom ersten Element, das einen Wert gleich wert besitzt. Kann kein passendes Element gefunden werden, wird end() zurückgeliefert. Die Elementfunktion ist eine Spezialversion des gleichnamigen Algorithmus. T ist der Datentyp des sortierten Wertes: - bei Sets und Multisets der Typ der Elemente - bei Maps und Multimaps der Typ der Schlüssel Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert die erste Position, an der wert eingefügt werden kann, ohne die Sortierung zu zerstören. Dies ist gleichbedeutend mit dem ersten Element, dessen Wert >= wert ist. Die Elementfunktion ist eine Spezialversion des gleichnamigen Algorithmus. T ist der Datentyp des sortierten Wertes: - bei Sets und Multisets der Typ der Elemente - bei Maps und Multimaps der Typ der Schlüssel Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert die letzte Position, an der wert eingefügt werden kann, ohne die Sortierung zu zerstören. Dies ist gleichbedeutend mit dem ersten Element, dessen Wert > wert ist. Die Elementfunktion ist eine Spezialversion des gleichnamigen Algorithmus. T ist der Datentyp des sortierten Wertes: - bei Sets und Multisets der Typ der Elemente - bei Maps und Multimaps der Typ der Schlüssel Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert ein Wertepaar mit der ersten und letzten Position, an der wert eingefügt werden kann, ohne die Sortierung zu zerstören. Dies ist gleichbedeutend mit dem Bereich der elemente, deren Wert == wert ist. Die Elementfunktion ist eine Spezialversion des gleichnamigen Algorithmus. T ist der Datentyp des sortierten Wertes: - bei Sets und Multisets der Typ der Elemente - bei Maps und Multimaps der Typ der Schlüssel Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert die Vergleichsfunktion bzw. das Elementfunktionen, die spezielle Implementierungen von Algorithmen für assoziative Container sind. 285 Die Programmiersprache C++ key_compare container::key_comp() Vergleichsobjekt, mit dem die Elemente verglichen werden. Vorhanden bei: Sets, Multisets, Maps, Multimaps Liefert die Vergleichsfunktion bzw. das Vergleichsobjekt, mit dem Schlüssel verglichen werden. Vorhanden bei: Sets, Multisets, Maps, Multimaps Bsp.: Zählen von Worten in einer Textdatei164 #include #include #include #include <string> <map> <fstream> <assert.h> using namespace std; class Zaehler { private: int i; public: Zaehler() : i(0) { } void operator++(int) { i++; } int wert() { return i; } }; typedef map<string, Zaehler, less<string> > wortmap; const char* begrenzer = "\t.,:;\"{}-+&^%$#@!~'\\<|()[]<>*="; int main(int argc, char* argv[]) { assert(argc == 2); ifstream ein(argv[1]); assert(ein); wortmap worte; const int gr = 255; char puffer[gr]; while (ein.getline(puffer,gr)) { char* wort = strtok(puffer,begrenzer); while (wort) { worte[string(wort)]++; wort = strtok(0,begrenzer); } } for (wortmap::iterator w = worte.begin(); w != worte.end(); w++) cout << (*w).first << ": " << (*w).second.wert() << endl; } 164 vgl. PR52410.CPP 286 Die Programmiersprache C++ 5.2.5 Queue Eine Queue oder Warteschlange erlaubt die Ablage von Objekten auf einer Seite und ihre Entnahme von der anderen Seite. Sowohl list als auch deque sind geeignete Elemente zur Implementierung165. queue<double> schlange_1; // mit deque realisierte Queue queue<double,list<double> schlange_2; // mit list realisierte Queue Die Deklaration der Klasse ist: template <class T, class Container = deque<T>> class queue; Eine Queue hat zusätzlich bestimmte, öffentliche Datentypen: Datentyp value_type size_type Bedeutung Typ der Element Integraler Typ ohne Vorzeichenangabe für Größenangaben container_type Typ des Containers Abb. Zusätzliche Datentypen für queue Eine Queue stellt folgende Methoden zur Verfügung: Methode mit Rückgabetyp Bedeutung queue(const Container& = Container()) Konstruktor. Eine Queue kann mit einem bereits vorhandenen Container initialisiert werden. Container ist Typ des Containers. bool empty() const Gibt an, ob der Queue leer ist size_type size() const Gibt die Anzahl der in der Queue befindlichen Elemente zurück const value_type& front() const und Gibt das erste Element zurück value_type& front() const value_type& back() const und Gibt das letzte Element zurück value_type& back() void push(const value_type& x) Legt das Element x am Ende der Queue ab. void pop() entfernt das erste Element der Queue. Eine Priority-Queue ist eine prioritätsgesteuerte Warteschlange. Jedem Element wird eine Priorität zugeordnet, die den Platz innerhalb der Priority-Queue schon beim Einfügen bestimmt. Die relative Priorität wird durch Vergleich jeweils zweier Elemente bestimmt, indem entweder der „<“-Operator oder wahlweise ein Funktionsobjekt herangezogen wird. Die Deklaration der Klasse ist: template <class T, class Container = deque<T>, class Compare = less<typname Container::value_type>> class priority_queue; Eine Priority-Queue stellt diesselben Datentypen wie die Queue und die Folgenden Methoden zur Verfügung: Methode mit Rückgabetyp priority_queue(const Compare& cmp = 165 Bedeutung Konstruktor. Eine Priority-Queue kann mit einem Falls nichts anderes angegeben wird, wird eine deque verwendet. 287 Die Programmiersprache C++ Compare(), const Container& = Container()) bool empty() const size_type size() const const value_type top()const void push(const value_type& x) void pop() bereits vorhandenen Container initialisiert werden. Container ist Typ des Containers. Cmp ist das Funktionsobjekt, mit dem verglichen wird. Falls kein Funktionsobjekt angegeben wird, wird less<container::value_type> angenommen. Gibt an, ob der Priority-Queue leer ist Gibt die Anzahl der in der Priority-Queue befindlichen Elemente zurück Gibt das erste Element zurück, d.h. das mit der größten Priorität. Fügt das Element x ein. entfernt das erste Element der Priority-Queue. 5.2.6 Set Die Klasse set<Key, T> für Mengen, eingebunden durch den Header <set>, entspricht der Klasse map. Es werden aber nur Schlüssel gespeichert. template <class Key, // Schluessel class Compare = less<Key> > /* Standardvergleich für Sortierung */ class set; set ist ein assoziativer Container, die Schlüssel liegen sortiert vor. Falls eine Sortierung nicht mit dem Operator „<“ (less<Key>) hergestellt werden soll, kann eine Klasse für Funktionsobjekte angegeben werden. Ein Multiset kennt im Gegensatz zum Set keine Duplikate template <class Key, // Schluessel class Compare = less<Key> // Standardvergleich für // Sortierung class Allocator = allocator<T> > class set; Die Elemente eines Set oder Multiset können von einem beliebigen Typ T sein. T muß sortierbar sein, d.h. der Operator < muß definiert sein. Der zweite optionale Parameter legt das Default-Sortierkriterium fest. Der Default-Wert, das Funktionsobjekt less bedeutet: Elemente, sofern nicht anders angegeben, werden aufsteigend sortiert. Die beiden Container-Klassen sind als balancierte Binärbäume166 implementiert. Ein „set“ besitzt zusätzlich folgende Datentypen Datentyp pointer const_pointer reverse_iterator const_reverse_iterator key_type value-type key_compare Bedeutung Zeiger auf Set-element Zeiger auf konstantes Set-Element Iterator für Durchlauf vom Ende zum Anfang Iterator für Durchlauf vom Ende zum Anfang mit konstanten Elementen Entspricht Key Entspricht Key Compare 166 Der Standard legt nicht direkt fest, wie assoziative Container implementiert sind. Vielfach werden diese Bäume als sog. „Red-Black-Trees“ implementiert. 288 Die Programmiersprache C++ value_compare Compare bzw. Methoden: Methode mit Rückgabetyp set(const Compare& cmp = Compare()) pair<iterator, bool> insert(x) size_type erase(k) void erase(p, q) void clear() value_compare value_comp() const Bedeutung Konstruktor, der ein Compare-Objekt akzeptieren kann. Falls keins angegeben wird, wird less<Key> angenommen. Fügt den Schlüssel x ein, sofern er noch nicht vorhanden ist. Der Iterator des zurückgegebenen Paares zeigt auf den eingefügten Schlüssel bzw. auf den schon vorhandenen Schlüssel mit demselben Wert wie x. Der Warheitswert zeigt an, ob überhaupt ein Einfügen stattgefunden hat Löscht das Element, auf das der Iterator zeigt Alle Elemente im Iteratorbereich p bis q löschen Löscht alle Elemente (dasselbe wie key_comp(). Bsp.: Sortieren der Wörter einer Textdatei167 #include #include #include #include <string> <set> <fstream> <assert.h> using namespace std; const char* begrenzer = " \t;()\"<>:{}[]+-=&*#.,/\\" "0123456789"; int main(int argc, char* argv[]) { assert(argc == 2); ifstream ein(argv[1]); assert(ein); set<string, less<string> > woerter; const int gr = 1024; char puffer[gr]; while (ein.getline(puffer,gr)) { char* s = strtok(puffer,begrenzer); while (s) { woerter.insert(s); s = strtok(0, begrenzer); } } copy(woerter.begin(), woerter.end(), ostream_iterator<string>(cout, "\n")); } 167 pr52611.CPP 289 Die Programmiersprache C++ 5.2.7 Stack Ein Stack erlaubt die Ablage und Entnahme nur auf einer Seite. template <class T, class Container = deque<T>> class stack; Ein Stack benutzt intern einen anderen Container, der die Operationen back(), push_back() und pop_back() unterstützt. Falls nichts anderes angegeben wird, wird eine Deque verwendet: stack <double> stapel_1; stack<double, vector<double>> stapel_2; // mit deque realisierter Stack // mit vector realisierter Stack Ein Stack hat zusätzlich bestimmte, öffentliche Datentypen: Datentyp value_type size_type container_type Bedeutung Typ der Elemente Integraler Typ ohne Vorzeichen für Größenangaben Typ des Containers Abb.: Stack-Datentypen Die Klasse stack stellt die folgenden Methoden zur Verfügung: Methode mit Rückgabetyp Bedeutung stack(const Container& = Container()) Konstruktor. Ein Stack kann mit einem bereits vorhandenen Container initialisiert werden. Container ist Typ des Containers. bool empty() const Gibt an, ob der Stack leer ist size_type size() const Gibt die Anzahl der im Stack befindlichen Elemente zurück size_type size() const Gibt die Anzahl der im Stack befindlichen Elemente zurück const value_type& top() const und Geben das oberste Element zurück value_type& top() void push(const value_type& x) Legt das Element x auf dem Stack ab. void pop() entfernt das oberste Element vom Stack. Für die Stack-Klasse gibt es außerdem die globalen relationalen Operatoren: template <class T, class Container> bool operator==(const stack<T, Container>& x, const stack<T, Container>& y); template <class T, class Container> bool operator<(const stack<T, Container>& x, const stack<T, Container>& y); template <class T, class Container> bool operator>(const stack<T, Container>& x, const stack<T, Container>& y); template <class T, class Container> bool operator!=(const stack<T, Container>& x, const stack<T, Container>& y); template <class T, class Container> bool operator>=(const stack<T, Container>& x, const stack<T, Container>& y); template <class T, class Container> bool operator<=(const stack<T, Container>& x, const stack<T, Container>& y); 290 Die Programmiersprache C++ Bsp.: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“168 #include <iostream> #include <vector> #include <stack> // Ein Testprogramm void ausgabe(long zahl, int b) { stack<int, vector<int> > s; // Extrahiere die Ziffern zur jeweiligen Basis (von rechts nach links) // und lege sie im Stapel ab do { s.push(zahl % b); zahl /= b; } while (zahl != 0); while (!s.empty()) { cout << s.top(); s.pop(); } } int main(void) { long zahl; // int b; // Basis // lies 3 positive ganze Zahlen und die gewuenschte Basis for (int i = 0; i < 3; i++) { cout << "\nGib eine nicht negative ganze Dezimalzahl und " << "\ndanach die Basis (2 <= b <= 9) an" << endl; cin >> zahl >> b; cout << zahl << " basis " << b << " ist: "; ausgabe(zahl,b); cout << endl; } } 5.2.8 Vector Die Deklaration der Klasse ist: template<class T> class vector Ein vector-Container verwaltet die Elemente in einem dynamischen Array. Er ermöglicht wahlfreien Zugriff (random access). Auf jedes Element kann mit einem entsprechenden Index direkt zugegriffen werden. Das Anhängen und Löschen von Elementen am Ende des Array geht optional schnell. Das Einfügen und Löschen von Elementen mitten im Array kostet Zeit, da dann alle Elemente entsprechend verschoben werden müßten. Ein Vektor hat zusätzlich bestimmte, öffentliche Datentypen: Datentyp pointer const_pointer 168 Bedeutung Zeiger auf Vektor-Element Zeiger auf konstantes Vektor-Element pr52702.CPP 291 Die Programmiersprache C++ reverse_iterator const reverse_iterator Iterator für Durchlauf vom Ende zum Anfang Iterator für Durchlauf vom Ende zum Anfang mit konstanten Elementen Abb. Zusätzliche Datentypen für vector Die Klasse vector stellt die folgenden Methoden zur Verfügung: Methode mit Rückgabetyp vector<T>() vector<T>(const vector<T>&) ~vector<T>() iterator begin() const_iterator begin() iterator end() const_iterator end() size_type max_size() size_type size() bool empty() void swap(vector<T>&) operator=(const vector<T>&) bool operator==(const vector<T>&) bool operator!=(const vector<T>&) bool operator<(const vector<T>&) bool operator>(const vector<T>&) bool operator<=(const vector<T>&) bool operator>=(const vector<T>&) vector(size_type n,const T& t = T()) template <class InputIterator> vector(InputIterator i, InputIterator j) void push_back(const T& t) void pop_back() iterator insert(iterator p, const T& t) void insert(iterator p,size_type n, const T& t) iterator erase(iterator q) iterator erase(iterator q1, iterator q2) void clear() void reserve(size_type n) size_type capacity() const Bedeutung Konstruktor Kopierkonstruktor Destruktor Anfang des Containers Anfang des Containers mit konstanten Elementen Position nach dem letzen Element end() für Container mit konstanten Elementen Maximal mögliche Größe des Containers Aktuelle Größe des Containers size==0 bzw. begin() == end() Vertauschen mit Argument-Container Zuweisungsoperator Operator == Operator != Operator < Operator > Operator <= Operator <= Erzeugt einen Vektor mit n Kopien von t. Erzeugt einen Vektor, wobei Elemente i .. j in den Vektor kopiert werden. Fügt t am Ende ein. Löscht das letzte Element Fügt eine Kopie von t vor die Stelle p ein. Der Rückgabetyp zeigt auf die eingefügte Kopie. Fügt n Kopien von t vor die Stelle t ein Löscht das Element, auf das q zeigt. Löscht die Elemente im Bereich q1 .. q2. Der zurückgegebene Iterator zeigt auf das Element, das q vor dem Löschvorgang unmittelbar folgt, sofern es existiert. Anderenfalls wird end() zurückgegeben. Löscht alle Elemente Speicherpaltz reservieren, so daß der verfügbare Platz größer als der aktuelle ist. Zweck: Vermeidung von Speicherplatzbeschaffungsoperatoren während der Benutzung des Vektors Gibt den Wert der Kapazitärt zurück. size() ist immer kleiner oder gleich als capacity() Abb.: Methoden der Klasse vector 292 Die Programmiersprache C++ 5.3 Iteratoren Iteratoren sind Objekte, die über Container (Iteratoren) wandern können. Jedes Iterator-Objekt repräsentiert eine Position in einem Container. Drei fundamentale Operatoren definieren das Verhalten eines Iterators: iterator::operator*() // liefert das Element, an dessen Position sich der Operator befindet. Sofern die Elemente Objekte // sind, ist auch der Operator -> definiert. Er ermöglicht den direkten Zugriff auf eine Komponente. iterator::operator++() // setzt den Iterator ein Element weiter. iterator::operator==() // zeigt, ob zwei Iteratoren das gleiche Objekt repräsentieren. Diese Schnittstelle entspricht genau der Art und Weise, wie in C/C++ Zeiger über Arrays wandern können. Iteratoren können aber mit beliebigen Containern umgehen: Die Container stellen die dazugehörigen Iteratorklassen zur Verfügung und implementieren damit auch deren Operatoren. Das bedeutet: Iteratoren von verschiedenen Containern können unterschiedliche Typen besitzen. In der C++-Standardbibliothek wird ein Standardtyp für Iteratoren angegeben, von dem jeder benutzerdefinierte Iterator erben kann: namespace std { template <class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T&> struct iterator { typedef Distance difference_type; typedef T value_type; typedef Pointer pointer; typedef Reference reference; typedef Category iterator_category; } }; Über public-Vererbung sind die Namen in allen abgeleiteten Klassen sichtbar und verwendbar. Iterator-spezifische Datentypen und Funktionen werden in der Header-Datei <iterator> definiert. Diese Datei muß aber nicht eingebunden werden, da sie von allen Containern und der Header-Datei für Algorithmen ohnehin eingebunden wird. Iteratoren spezifizieren eine Position in einem Container. Sie können inkrementiert, dekrementiert werden. Zwei Iteratoren können miteinander verglichen werden. Es gibt einen speziellen Iterator-Wert „past-the-end“. Über die Nachricht „begin()“ kann ein Iterator auf das erste Element eines Containers gestzt werden. Mit der Nachricht „end()“ wird ein „past-the-end“ Iterator erhalten, z.B.: 293 Die Programmiersprache C++ vector<int> v; vector::iterator i1 = v.begin(); vector::iterator i2 = v.end(); i1 i2 v <T> <T> <T> <T> <T> <T> <T> *i1 Operationen (z.B. Sortieren) benutzen die beiden Iteratoren als Spezifikation des (Quell-) Bereichs. Das (Quell-) Element wird durch Inkrementieren und Dereferenzieren des ersten Iterators beschafft. Es stimmt dann mit dem zweiten Iterator überein. Zwei Iteratoren sind gleich, wenn sie sich auf dasselbe Element desselben Vektors beziehen. Bsp.: 1. Das folgende Programm169 zeigt, wie in C/C++ eine Liste mit ganzen Zahlen erstellt, sortiert und anschließend ausgegeben werden kann. #include <stdlib.h> #include <iostream.h> inline int vrgl(const void* a, const void* b) { int aa = * (int*) a; int bb = * (int*) b; return (aa < bb) ? -1 : (aa > bb) ? 1 : 0; } int main() { int x[100]; int n = 0; // Lies eine ganze Zahl in das n + 1. Element des Array while (cin >> x[n++]); n--; qsort(x,n,sizeof(int),vrgl); for (int i = 0; i < n; i++) cout << x[i] << endl; } 2. Das folgende Beispiel170 zeigt, wie das Sortieren mit Random-Access-Iteratoren erledigt werden kann. #include #include #include #include #include <string.h> <algorithm> <vector> <stdlib.h> <iostream.h> int main() { vector<int> v; int ein; while (cin >> ein) 169 170 pr53010.CPP pr53011.CPP 294 Die Programmiersprache C++ v.push_back(ein); sort(v.begin(),v.end()); int n = v.size(); for (int i = 0; i < n; i++) cout << v[i] << endl; } 5.3.1 Iteratorkategorien Es gibt verschiedene Kategorien von Iteratoren in einer hierarchischen Anordnung: Input-Iterator Dient zum sequentiellen Lesen von Daten, z.B. aus einem Container oder einer Datei. Ein Zurück an eine gelesene Stelle ist nicht möglich („--“-Operator ist nicht definiert). Ausdruck iter1==iter2 Iter1!=iter2 *iter iter->komp ++iter iter++ TYP(iter) Bedeutung Test auf Gleichheit mit anderem Iterator Test auf Ungleichheit mit anderem Iterator Lese-Zugriff auf das aktuelle Element des Iterators Zugriff auf eine Komponente des aktuellen Iterators Weitersetzen (liefert neue Position) Weitersetzen (liefert alte Position) Kopieren (Copy-Konstruktor) Abb.: Operationen für Input-Iteratoren Output-Iterator Kann sequentiell in einen Container oder in eine Datei schreiben, wobei der Dereferenzierungsoperator verwendet wird. Ausdruck *iter = wert ++iter iter++ TYP(iter) Bedeutung Schreib-Zugriff auf das aktuelle Element eines Iterators Weitersetzen (liefert neue Position) Weitersetzen (liefert alte Position) Kopieren (Copy-Konstruktor) Abb. Operationen für Output-Iteratoren Forward-Iterator Kann sich vorwärts bewegen. Im Unterschied zu den vorgenannten Iteratoren können jedoch Werte des Iterators gespeichert werden, z. B. um ein Element des Containers wiederzufinden. Damit ist ein mehrfacher Durchlauf in eine Richtung möglich, z.B. durch eine einfach verkettete Liste, falls man sich den Anfang gemerkt hat. Ausdruck iter1==iter2 iter1!=iter2 *iter iter->komp ++iter iter++ TYP() TYP(iter) Bedeutung Test auf Gleichheit mit anderem Iterator Test auf Ungleichheit mit anderem Iterator Lese-Zugriff auf das aktuelle Element des Iterators Zugriff auf eine Komponente des aktuellen Iterators Weitersetzen (liefert neue Position) Weitersetzen (liefert alte Position) Erzeugen (Default-Konstruktor) Kopieren (Copy-Konstruktor) 295 Die Programmiersprache C++ iter1=iter2 Zuweisung Abb.: Operationen für Forward-Iteratoren Bidirectional-Iterator Kann alles, was ein Forward-Iterator kann. Zusätzlich kann er noch mit dem „--„Operator rückwärts gehen, so daß er bspw. für eine doppelt verkettete Liste geeignet ist. Ausdruck --iter1 iter-- Bedeutung Zurücksetzen (liefert neue Position) Zurücksetzen (liefert alte Position) Abb.: Zusätzliche Operationen für Bidirectional-Iteratoren Random-Access-Iterator Kann alles, was ein Bidirectional-Iterator kann. Zusätzlich ist wahlfreier zugriff möglich, wie er für einen Vektor benötigt wird. Der wahlfreie Zugriff wird durch den Indexoperator operator[]() realisiert. Ausdruck iter[n] iter += n iter -= n iter + n n + iter iter - n iter1 < iter2 iter1 > iter2 iter1 - iter2 iter1 <= iter2 iter1 >= iter2 Bedeutung Zugriff auf das nte Element Iterator n Elemente weitersetzen (bzw. bei negativem wert zurücksetzen) Iterator n Elemente zurücksetzen (bzw. bei negativem Wert weitersetzen) Iterator für das nte folgende Element liefern Iterator für das nte folgende Element liefern Iterator für das nte vorherige Element liefern Zeigt, ob iter1 vor iter2 liegt Zeigt, ob iter1 hinter iter2 liegt Abstand zwischen iter1 und iter2 liefern Zeigt, ob iter1 nicht hinter iter2 liegt Zeigt, ob iter1 nicht vor iter2 liegt Abb.: Zusätzliche Operationen für Random-Access-Iteratoren 5.3.2 distance(), advance() und iter_swap() Mit distance() kann der Abstand zwischen zwei Iteratoren ermittelt werden, mit advance() kann der Iterator eine bestimmte Anzahl von Elementen weitergesetzt werden. Mit iter_swap() können die Werte der Positionen von zwei Iteratoren vertauscht werden. // schaltet i um n Positionen vor bzw. zurück, falls n > 0 template <class InputIterator, class Distance> void advance(InputIterator& i, Distance n) // gibt den Abstand zwischen zwei Iteratoren zurueck // last muss von first aus erreichbar sein template <class InputIterator> typname iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last); 296 Die Programmiersprache C++ void iter_swap(Forward_Iterator1 pos1, Forward_Iterator2 pos2) Die beiden Iteratoren müssen nicht den gleichen Typ besitzen. Die beiden Elemente müssen gegenseitig zuweisbar sein. 5.3.3 Iterator-Adapter Reverse-Iteratoren Eine Reverse-Iterator ist bei einem bidirektionalen Iterator immer möglich. Ein Reverse-Iterator durchläuft einen Container rückwärts mit der ++-Operation. Beginn und Ende des Containers werden mit rbegin() und rend() markiert. Einige Container stellen in Abhängigkeit von ihrem Typ Reverse-Iteratoren zur Verfügung. Diese Iteratoren werden mit der vordefinierten Klasse template <class Iterator> class reverse_iterator; realisiert. Die Container-Funktionen rbegin() und rend() liefern jeweils einen Reverse-Iterator: - rbegin() liefert die Position des ersten elements eines umgekehrten Durchlaufs, also das letzte Element im Container. rend() liefert die Position hinter dem letzten Element eines umgekehrten Durchlaufs, also die Position vor dem ersten Element im Container Bsp.: Insert-Iteratoren Insert-Iteratoren sind Iterator-Adapter, die Zuweisungen an den Iterator bzw. an das Element des Iterators in ein Einfügen umsetzen. Ausdruck *iter iter = wert ++iter iter++ Bedeutung Nichts (liefert iter zurück) Fügt wert an iter-Position ein Nichts (liefert iter zurück) Nichts (liefert iter zurück) Abb.: Operationen von insert-Operatoren Es gibt drei Arten von Insert-Operatoren Name Back-Inserter Front-Inserter Inserter Klasse back-insert_iterator front_insert_iterator insert_iterator Elementfunktion push_back(wert) push_front(wert) insert(pos,wert) Abb. Arten von Insert-Iteratoren 1. front_insert_iterator 297 Erzeugung back_inserter(container) front_inserter(container) inserter(container,pos) Die Programmiersprache C++ Dieser Insert-Iterator fügt etwas am Anfang des Containers ein. Der Container muß die Methode push_front() zur Verfügung stellen. 2. back_insert_iterator Dieser Insert-Iterator fügt etwas am Ende des Containers ein. Der Container muß die Methode push_back() zur Verfügung stellen. 3. insert_iterator Dieser Insert-Iterator fügt etwas an einer ausgewählten Position in den Container ein. Der Container muß die Methode insert() zur Verfügung stellen. Dem InsertIterator muß die gewünschte Einfügeposition mitgegeben werden. 5.3.4 Stream-Iteratoren Sie dienen zum sequentiellen Lesen und Schreiben mit den Operatoren << und >>. Der Istream-Operator ist ein Input-Iterator, der Ostream-Iterator ist ein OutputIterator. istream-Iterator Ein Eingabestrom, z.B. cin, hat die richtige Funktionalität für einen Input-Iterator (Zugriff auf eine Folge von Elementen). Allerdings erwarten Operationen mit Iteratoren „Inkrementieren und Dereferenzieren“. Diese Fähigkeiten stellt ein „cin“ nicht bereit. Die STL sieht dafür Adaptoren vor: Typen die das Interface auf andere Typen transformieren. Ein sehr nützlicher Adapter ist der „istream-iterator“, der mit dem gewünschten Objekttyp, der vom Eingabestrom eingelesen werden soll, parametrisiert ist. Eingabestrom-Iteratoren werden mit einem Strom initialisiert. Danach kann über Dereferenzieren ein Element aus dem Strom gelesen werden. Ein Eingabestrom-Iterator, der mit einem Default-Konstruktor erzeugt wurde, bekommt den „past-the-end“-Wert zugewiesen, falls der zum Eingabestrom zugehörige Iterator das Ende erreicht hat. 298 Die Programmiersprache C++ iter++ Algorithmen cin >> v Iter cin *iter return v Abb.: Zum Einlesen der Elemente in einen Vektor dient der copy-Algorithmus der STL. Dieser Algorithmus benutzt drei Iteratoren. Die ersten beiden spezifizieren den Quellbereich, der dritte das Ziel, z.B.: typedef istream_iterator<int,ptrdiff_t> istream_iterator_int; // Kopieren von der Standard-Eingabe copy(istream_iterator_int(cin), istream_iterator_int(),v.begin()) Ausdruck istream_iterator<T>() istream_iterator<T,ptrdiff_t>() istream_iterator<T>(istream) istream_iterator<T,ptrdiff_t>(istream) *iter iter->komp ++iter iter++ iter == wert iter != wert Bedeutung Erzeugt einen End-Of-Stream-Iterator Erzeugt einen End-Of-Stream-Iterator Erzeugt einen Istream-Iterator Erzeugt einen Istream-Iterator Liefert den zuletzt gelesenen Wert Liefert eine Komponente vom zuletzt gelesenen wert Liefert nächsten Wert und liefert Iterator zurück Liefert nächsten Wert und liefert vorherigen Iterator zurück Test der beiden Iteratoren auf Gleichheit Test von beiden Iteratoren auf Ungleichheit Abb.: Operationen von istream-Iteratoren ostream_iterator Auf ähnliche Weise werden die Werte (nach dem Sortieren) in den Ausgabestrom (Standard-Ausgabe) geleitet. Ausdruck ostream_iterator<T>(ostream) ostream_iterator<T>(ostream, zf) *iter iter = wert ++iter iter++ Bedeutung Erzeugt einen Ostream-Iterator Erzeugt einen ostream-Iterator, der zf zwischen den Elementen ausgibt Nichts (liefert iter zurück) Gibt wert auf ostream aus (ostream << wert) Nichts (liefert iter zurück) Nichts (liefert iter zurück) Abb.: Operationen von ostream-Iteratoren 299 Die Programmiersprache C++ Bsp.171: „Sortieren der Standard-Eingabe mit anschließender Ausgabe auf die Standard-Ausgabe“ #include #include #include #include #include <string.h> <algorithm> <vector> <stdlib.h> <iostream.h> int main() { vector<int> v; istream_iterator<int,ptrdiff_t> start(cin); istream_iterator<int,ptrdiff_t> end; back_insert_iterator<vector<int> > ziel(v); copy(start, end, ziel); sort(v.begin(), v.end()); copy(v.begin(), v.end(), ostream_iterator<int>(cout,"\n")); } 5.3.5 Iterator-Traits Iterator-Tags sind Datentypen, die einfach für eine entsprechende Iterator-Kategorie stehen. Iterator-Traits sind Strukturen, die sicherstellen, daß alle Iteratoren die gleichen datentypKomponenten besitzen. 171 pr53510.CPP 300 Die Programmiersprache C++ 5.4 Algorithmen Alle im Header <algorithm> vorhandenen Algorithmen sind unabhängig von der speziellen Implementierung der Container, auf denen sie arbeiten. Sie kennen nur Iteratoren, über die auf Datenstrukturen in Containern zugegriffen werden kann. for_each Der Algorithmus for_each bewirkt, daß auf jedem Element eines Containers eine Funktion ausgeführt wird: template <class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f) „f“ kann sowohl eine Funktion als auch ein Funktionsobjekt sein und wird nach Gebrauch zurückgegeben. find und find_if find() tritt in 2 Arten auf: mit oder ohne erforderliches Prädikat (find_if()): template <class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& wert); template <class InputIterator, class Predicate> InputIterator find_if(InputIterator first, InputIterator last, Predicate pred); find_end Der Algorithmus findet eine Subsequenz innerhalb einer Sequenz find_first_of Der Algorithmus findet eine Subseqenz innerhalb einer Sequenz adjacent_find Zwei gleiche direkt benachbarte Elemente werden mit der Funktion adjacent_find gefunden. template <class ForwardIterator> ForwardIterator adjacent_find(ForwardIterator first, ForwardIterator last); 301 Die Programmiersprache C++ template <class ForwardIterator, class BinaryPredicate> ForwardIterator adjacent_find(ForwardIterator first, ForwardIterator last, BinaryPredicate binary_pred); count Der Algorithmus gibt die Anzahl zurück, wie viele Elemente gleich einem bestimmten wert sind bzw. wie viele Elemente ein bestimmtes Prädikat erfüllen. template <class InputIterator, class T> iterator_traits<inputIterator>::difference_type count(InputIterator first, InputIterator last, const T& wert); template <class InputIterator, class Predicate> iterator_traits<inputIterator>::difference_type count(InputIterator first, InputIterator last, Predicate pred); mismatch überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei eine Varianle ein binäres Prädikat benutzt. equal equal() überprüft zwei Container auf Übereinstimmung ihres Inhalts, wobei eine Variante ein binäres Prädikat benutzt. template <class InputIterator1, class InputIterator2> bool equal(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2); template <class InputIterator1, class InputIterator2,class BinaryPredicate> bool equal(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, Predicate binary_pred); 302 Die Programmiersprache C++ search Der Algorithmus durchsucht eine Sequenz, ob eine zweite Sequenz in ihr enthalten ist. Es wird ein Iterator auf die Position innerhalb der ersten Sequenz zurückgegeben, an der die zweite Sequenz beginnt, sofern sie in der ersten enthalten ist. Andernfalls wird ein Iterator auf die last1-Position der ersten Sequenz zurückgegeben template <class ForwardIterator1, class ForwardIterator2> ForwardIterator1 search(ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator1 first2, ForwardIterator2 last2); template <class ForwardIterator1, class ForwardIterator2, class BinaryPredicate> ForwardIterator1 search(ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator1 first2, ForwardIterator2 last2, BinaryPredicate binary_pred); search_n search_n() durchsucht eine Sequenz daraufhin, ob eine Folge von gleichen Werten in ihr enthalten ist. template <class ForwardIterator, class Size, class T> ForwardIterator search-n(ForwardIterator first, ForwardIterator last, Size count, const T& value); template <class ForwardIterator, class Size, class T, class BinaryPredicate> ForwardIterator search_n(ForwardIterator first, ForwardIterator last, Size count, const T& value, BinaryPredicate binary_pred); copy und copy_backward copy() kopiert die Elemente eines Quellbereichs in den Zielbereich, das Kopieren beginnt am Anfang bzw. am Ende (mit copy_backward()). Falls der Zielbereich nicht überschrieben, sondern in ihn eingefügt werden soll, ist als Output-Iterator ein Iterator zum Einfügen zu nehmen. template <class InputIterator, class OutputIterator> OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result); template <class BidirectionalIterator1, class BidirectionalIterator2> BidirectionalIterator2 copy_backward(BidirectionalIterator1 first, BidirectionalIterator1 last, BidirectionalIterator2 result); 303 Die Programmiersprache C++ copy_backward ist immer dann zu nehmen, wenn Ziel- und Quellbereich sich so überlappen, daß der Anfang des Zielbereichs im Quellbereich liegt. result muß anfangs auf das Ende des Zielbereichs zeigen. swap_iter, swap und swap_ranges swap() vertauscht zwei Elemente. Die beiden Elemente können in verschiedenen, in demselben oder in keinem Container sein. template <class T> void swap( T& a, T& b ); swap() ist spezialisiert für diejenigen Container, die eine Methode swap() zum Vertauschen bereitstellen (deque, list, vector, set, map, multiset und multimap). iter_swap() nimmt zwei Iteratoren und vertauscht die dazugehörenden Elemente. Die beiden Iteratoren können zu verschiedenen oder zu demselben Container gehören tempate <class ForwardIterator1, class ForwardIterator2> void iter_swap(ForwardIterator1 a, ForwardIterator2 b); swap_ranges() vertauscht zwei Bereiche transform Kopieren mit gleichzeitigem Umwandeln. template <class InputIterator, class OutputIterator, class UnaryOperation> OutputIterator transform(InputIterator first, InputIterator last, OutputIterator result, UnaryOperation op); Auf jedes Element des Bereiches von first bis ausschließlich last wird die Operation op angewendet und das Ergebnis in den mit result beginnenden Bereich kopiert. template <class InputIterator1, class InputIterator2 class OutputIterator, class BinaryOperation> OutputIterator transform(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, OutputIterator result, BinaryOperation bin_op); replace ersetzt in einer Sequenz jeden vorkommenden Wert old_value durch new_value. template <class ForwardIterator, class T> void replace(ForwardIterator first, 304 Die Programmiersprache C++ ForwardIterator last, const T& old_value, const T& new_value); fill und fill_in Falls eine Komponente ganz oder teilweise mit immer gleichen Werten, nämlich Kopien von value vorbesetzt werden soll, eignen sich fill() oder fill_n(). template <class ForwardIterator, class T> void fill(ForwardIterator first, ForwardIterator last, const T& value); template <class OutputIterator, class Size, class T> OutputIterator fill_n(OutputIterator first, Size n, const T& value); generate und generate_n Ein Generator in generate() ist ein Funktionsobjekt oder eine Funktion, die ohne Parameter aufgerufen und deren Ergebnis den Elementen der Sequenz der Reihe nach zugewiesen wird. Wie bei fill() gibt es eine Variante, die ein Iteratorpaar erwartet, und eine Variante, die den Anfangsiterator und eine Stückzahl benötigt. template <class ForwardIterator, class Generator> void generate(ForwardIterator first, ForwardIterator last, Generator gen); template <class OutputIterator, class Size, class Generator> OutputIterator generate_n(OutputIterator first, Size n, generator gen); remove und Varianten Der Algorithmus entfernt alle Elemente aus einer Sequenz, die gleich einem Wert value sind bzw. einem Prädikat pred genügen. template <class ForwardIterator, class T> ForwardIterator remove(ForwardIterator first, ForwardIterator last, const T& value) template <class ForwardIterator, class Predicate> ForwardIterator remove(ForwardIterator first, ForwardIterator last, Predicate pred); unique unique() löscht gleiche aufeinanderfolgende Elemente bis auf eins. 305 Die Programmiersprache C++ template <class ForwardIterator> ForwardIterator unique(ForwardIterator first, ForwardIterator last); template <class ForwardIterator, class BinaryPredicate> ForwardIterator unique(ForwardIterator first, ForwardIterator last, BinaryPredicate binary_pred); template <class InputIterator, class OutputIterator> OutputIterator unique(InputIterator first, InputIterator last, OutputIterator result); template <class InputIterator, class OutputIterator, class BinaryPredicate> OutputIterator unique_copy(InputIterator first, InputIterator last, OutputIterator result BinaryPredicate binary_pred); reverse reverse() dreht die Reihenfolge der Elemente einer Sequenz um. template <class BidirectionalIterator> void reverse(BidirectionalIterator first, BidirectionalIterator last); template <class BidirectionalIterator, class OutputIterator> OutputIterator reverse_copy(BidirectionalIterator first, BidirectionalIterator last, OutputIterator result); rotate rotate() verschiebt die Elemente einer Sequenz nach links, die vorne herausfallenden Elemente werden hinten wieder eingefügt. template <class ForwardIterator> void rotate(ForwardIterator first, ForwardIterator middle, ForwardIterator last); random_shuffle random_shuffle() dient zum Mischen der Elemente einer Sequenz, also zur zufälligen Änderung ihrer Reihenfolge. Die Sequenz muß Random-Access-Iteratoren zur Verfügung stellen, z.B. vector oder deque template <class RandomAccessIterator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last); template <class RandomAccessIterator, class RandomNumberGenerator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last RandomNumberGenerator& rand); 306 Die Programmiersprache C++ Die Mischung der Elemente soll gleichverteilt sein. Dies ist abhängig vom verwendeten Zufallszahlengenerator. Die erste Variante benutzt eine interne Zufallsfunktion. Vom Zufallszahlengenerator oder der Zufallsfunktion wird erwartet, daß ein positives Argument n vom Distanztyp des verwendeten Random-Access-Iterators genommen und ein Wert zwischen 0 und (n-1) zurückgegeben wird. partition Eine Sequenz kann mit partition() so zerlegt werden, dass alle Elemente, die einem bestimmten Kriterium pred genügen, anschließend vor allen anderen liegen. Es wird ein Iterator zurückgegeben, der auf den Anfang des zweiten Bereichs zeigt. Alle vor diesem Iterator liegenden Elemente genügen dem Prädikat. template <class BidirectionalIterator, class Predicate> BidirectionalIterator partition(BidirectionalIterator first, BidirectionalIterator last, Predicate pred); sort sort() sortiert zwischen den Iteratoren first und last. Der Algorithmus ist nur für Container mit Random-Access-Iteratoren geeignet (z.B. deque, vector). template <class RandomAccessIterator> void sort(RandomAccessIterator first, RandomAccesIterator last); template <class RandomAccessIterator, class Compare> void sort(RandomAccessIterator first, RandomAccesIterator last, Compare comp); Der Aufwand ist im Mittel O ( N log N ) mit N = last – first. Bsp.: Sortieren ganzer Zahlen172 #include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { int anzN = 0; int iwert; vector<int> v; cout << "Gib ganze Zahlen ein, "; cout << "<Return nach jeder Zahl, <CTRL-Z am Ende:" << endl; 172 vgl. pr52801.CPP 307 Die Programmiersprache C++ while (cin >> iwert, cin.good()) { v.push_back(iwert); cout.width(6); cout << anzN << ": " << v[anzN++] << endl; } if (anzN) { sort(v.begin(), v.end()); for (vector<int>:: const_iterator viter = v.begin(); viter != v.end(); viter++) cout << *viter << " "; cout << endl; } } partial_sort Teilweises Sortieren bringt die M kleinsten Elemente nach vorn. Der Rest bleibt unsortiert. Der Algorithmus verlangt jedoch nicht die Zahl M, sondern einen Iterator middle auf die entsprechende Position, so dass M = middle – first glilt. template <class RandomAccessIterator> void partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last); template <class RandomAccessIterator> void partial_sort(RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last Compare comp); nth-Element Das n. größte oder das n. kleinste Element einer Sequenz mit Random-AccessIteratoren kann mit nth_element() gefunden werden. Binäre Suche Die C++-Standardbibliothek stellt vier Algorithmen zum Suchen und Einfügen in sortierte Folgen bereit: binary_search template <class ForwardIterator, class T> bool binary_search(ForwardIterator first, ForwardIterator last, const T& wert); template <class ForwardIterator, class T, class Compare> bool binary_search(ForwardIterator first, ForwardIterator last, const T& wert, Compare comp); lower_bound 308 Die Programmiersprache C++ template <class ForwardIterator, class T> ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& wert); template <class ForwardIterator, class T, class Compare> ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& wert, Compare comp); upper_bound Der Algorithmus findet die letzte Stelle, an der ein Wert wert eingefügt werden kann, ohne die Sortierung zu zerstören. Der zurückgegebene Iterator zeigt auf diese Stelle. template <class ForwardIterator, class T> ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& wert); template <class ForwardIterator, class T, class Compare> ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& wert, Compare comp); equal_range Der Algorithmus ermiitelt den größtmöglichen Bereich, innerhalb dessen an jeder beliebigen Stelle ein Wert value eingefügt werden kann, ohne die Sortierung zu stören. Mischen Mischen, auch Verschmelzen genannt, ist ein Verfahren, zwei sortierte Folgen zu einer einzigen zu vereinigen. Es werden schrittweise die jeweils ersten Elemente beider Sequenzen verglichen, und es wird das kleinere (oder größere je nach Sortierkriterium) Elemente in die Ausgabesequenz gepackt. template <class InputIterator1, class InputIterator2, class OutputIterator> OutputIterator merge(InputIterator first1, InputIteratot last1, InputIteratot first2, InputIterator last2, OutputIterator result); template <class InputIterator1, class InputIterator2, class OutputIterator> OutputIterator merge(InputIterator first1, InputIteratot last1, InputIteratot first2, InputIterator last2, OutputIterator result, Compare comp); merge() setzt eine vorhandene Ausgabeseqenz voraus. Falls eine der beiden Eingabesequenzen erschöpft ist, wird der Rest der anderen in die Ausgabe kopiert. 309 Die Programmiersprache C++ Mengenoperationen auf sortierten Strukturen Die folgenden Algorithmen beschreiben die grundlegenden Mengenoperationen wie Vereinigung, Durchschnitt usw. auf sortierte Strukturen includes set_union set_intersection set_difference set_symmetric_difference Heap-Algorithmen Wichtige Eigenschaften eines Heap Die folgenden Heap-Eigenschaften bilden die Voraussetzung für die Anwendung der Heap-Algorithmen: - Die n Elemente eines Heap liegen in einem Array auf den Positionen 0 bis n – 1. - Die Art der Anordnung der Elemente im Array entspricht einem vollständigen binären Baum, bei dem alle Ebenen besetzt sind. Die einzige mögliche Ausnahme bildet die unterste Ebene, in der alle Elemente auf der linken Seite erscheinen. 99 [0] 33 56 [1] [2] 21 [3] 11 [7] h[0] 99 30 20 48 [4] [5] [6] 9 25 1 10 17 40 [8] [9] [10] [11] [12] [13] [1] 33 [2] 56 [3] 21 [4] 30 [5] 20 [6] 48 [7] 11 [8] 9 [9] 25 [10] 1 [11] 10 [12] 17 [13] 40 Abb.: Array-Repräsentation eines Heap Das Element h[0] ist die Wurzel, jedes Element h[j] , j > 0 hat einen Elternknoten h[(j-1)/2] 310 Die Programmiersprache C++ - Jedem Element h[j] ist eine Priorität zugeordnet, die größer oder gleich der Priorität der Kindknoten h[2j+1] und h[j+2] ist173. - Ein Array h mit n Elementen ist genau dann ein Heap, wenn h[(j-1)/2] >= h[j] für 1<=j<=n gilt. Daraus folgt automatisch, dass h[0] das größte Element ist. Eine Priorityqueue entnimmt einfach das oberste Element eines Heap. Anschließend wird er rekonstruiert, d.h. das nächstgrößte Element rückt an die Spitze. Die 4 Heap-Algorithmen Sie sind auf alle Container, auf die mit Random-Access-Iteratoren zugegriffen werden kann, anwendbar. pop_heap() Die Funktion pop_heap() entnimmt ein Element aus einem Heap. template <class RandomAccessIterator> void pop_heap(RandomAccessIterator first, RandomAccessIterator last); template <class RandomAccessIterator, class Compare> void pop_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp); Die Entnahme besteht darin, daß der Wert mit der höchsten Priorität der an der Stelle first steht, mit dem Wert an der Stelle last – 1 vertauscht wird. Anschließend wird der Bereich (first, last – 1) in einen Heap verwandelt. Die Komplexität von pop_heap() ist O (log( last first )) . push_heap() Diese Funktion fügt ein Element einem vorhandenen Heap hinzu template <class RandomAccessIterator> void push_heap(RandomAccessIterator first, RandomAccessIterator last); template <class RandomAccessIterator, class Compare> void pop_heap(RandomAccessIterator first, RandomAccessIterator last, Compare comp); make_heap() Diese Funktion sorgt dafür, daß die Heap-Bedingung für alle Elemente innerhalb eines Bereichs gilt. template <class RandomAccessIterator> void make_heap(RandomAccessIterator first, RandomaccessIterator last); template <class RandomAccessIterator, class Compare> void make_heap(RandomAccessIterator first, RandomaccessIterator last Compare comp); sort_heap() Diese Funktion wandelt einen Heap in eine sortierte Sequenz. Die Sortierung ist nicht stabil, die Komplexität ist O ( N log N ) , wenn N die Anzahl der zu sortierenden Elemente ist. template <class RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomaccessIterator last); 173 Große Zahlen bedeuten hohe Prioritäten 311 Die Programmiersprache C++ template <class RandomAccessIterator, class Compare> void sort_heap(RandomAccessIterator first, RandomaccessIterator last Compare comp); Die Sequenz ist aufsteigend sortiert. Minimum und Maximum Die inline-Templates min() und max() geben jeweils das kleinere (bzw. das größere) von zwei Elementen zurück. Bei Gleichheit wird das erste Element zurückgegeben. template <class T> const T& min(const T& a, const T& b); template <class T, class Compare> const T& min(const T& a, const T& b, Compare comp); template <class T> const T& max(const T& a, const T& b); template <class T, class Compare> const T& max(const T& a, const T& b, Compare comp); Lexikographischer Vergleich Er dient zum Vergleich zweier Sequenzen, die auch verschiedene Längen haben können. Permutationen Eine Permutation entsteht aus einer Sequenz durch Vertauschen zweier Elemente. So ist (0,2,1) bspw. eine Permutation, die aus (0,1,2) entstanden ist. Für eine Sequenz mit N Elementen gibt es N! Permutationen. Man kann sich die Menge aller N! Permutationen einer Sequenz geordnet vorstellen. Die Ordnung kann man mit dem <-Operator oder einem Vergleichsobjekt comp herstellen. template <class BidirectionalIterator> bool prev_permutation(BidirectionalIterator first, BidirectionalIterator last); template <class BidirectionalIterator, class Compare> bool prev_permutation(BidirectionalIterator first, BidirectionalIterator last, Compare comp); template <class BidirectionalIterator> bool next_permutation(BidirectionalIterator first, BidirectionalIterator last); template <class BidirectionalIterator, class Compare> bool next_permutation(BidirectionalIterator first, BidirectionalIterator last, Compare comp); 312 Die Programmiersprache C++ Wenn eine Permutation gefunden wird, ist der Rückgabewert true. Andernfalls handelt es sich um das Ende eines Zyklus. Dann wird false zurückgegeben und die Sequenz in die kleinstmögliche (bei next_permutation()) bzw. die größtmögliche (bei prev_permutation()) entsprechend dem Sortierkriterium verwandelt. 313 Die Programmiersprache C++ 5.5 Nationale Besonderheiten Die Klasse locale (Header <locale>) bestimmt die nationalen Besonderheiten von Zeichensätzen. 5.6 Die numerische Bibliothek 5.6.1 Komplexe Zahlen Komplexe Zahlen werden im Header <complex> durch spezialisierte Templates für float, double und long double realisiert. 5.6.2 Grenzwerte von Zahlentypen 5.6.3 Numerische Algorithmen accumulate Der Algorithmus addiert auf einen Startwert alle Werte *i eines Iterators i von first bis last. template <class InputIterator, class T> T accumulate(InputIterator first, InputIterator last, T init); template <class InputIterator, class T, class binaryOperation> T accumulate(InputIterator first, InputIterator last, T init, binaryOperation binOp); inner_product Der Algorithmus addiert das Skalarprodukt zweier Container u und v, die meistens Vektoren sein werden, auf den Anfangswert init: Ergebnis = init + u i vi i template <class InputIterator, class InputIterator , class T> T inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, T init); template <class InputIterator, class InputIterator , class T, 314 Die Programmiersprache C++ class binaryOperation1, class binaryOperation2> T inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, T init, binaryOperation1 binOp1, binaryOperation2 binOp2); partial_sum template <class InputIterator, class OutputOperator> OutputIterator partial_sum(InputIterator first, InputIterator last, OutputOperator result); template <class InputIterator, class OutputOperator, class binaryOperation> OutputIterator partial_sum(InputIterator first, InputIterator last, OutputOperator result BinaryOperation binOp); adjacent_difference Dieser Algorithmus berechnet die Differenz zweier aufeinanderfolgender Elemente eines Containers v und schreibt das Ergebnis in einen Ergebniscontainer e, auf den vom Iterator result verwiesen wird. Da es genau einen Differezwert weniger als Elemente gibt, bleibt das erste Element erhalten. Falls das erste Element den Index 0 trägt, gilt: e0 = v0 ei = vi – vi-1 Außer Differenzbildung sind andere Operatoren möglich. template <class InputIterator, class OutputOperator> OutputIterator adjacent_difference(InputIterator first, InputIterator last, OutputOperator result); template <class InputIterator, class OutputOperator, class binaryOperation> OutputIterator adjacent_difference(InputIterator first, InputIterator last, OutputOperator result BinaryOperation binOp); 315 Die Programmiersprache C++ 5.6.4 Optimierte numerische Arrays (valarray) Der Header <valarray> schließt die Template-Klasse valarray ein, die für mathematische Vektor-Operationen gedacht ist. Ein valarray-Objekt ist ein für numerische Berechnungen optimierte Vekor. 5.6.4.1 Konstruktoren und Elementfunktionen 5.6.4.2 Binäre Valarray-Operatoren 5.6.4.3 Mathematische Funktionen 5.6.4.4 slice 5.6.4.5 slice_array 5.6.4.6 gslice 5.6.4.7 gslice_array 5.6.4.8 mask_array 5.6.2.9 indirect_array 316 Die Programmiersprache C++ 5.7 Typerkennung zur Laufzeit 5.8 Speichermanagement 5.8.1 <new> Der Header <new> enthält die Operatoren new, new[] und delete[]. 5.8.2 <memory> 317 Die Programmiersprache C++ 6. Windows-Programmierung unter Visual C++ mit MFC 6.1 Merkmale von Visual C++ .NET und MFC 6.1.1 Visual C++ -Features und MFC -Grundlagen Compiler Der Microsoft Visual C++ -Compiler stellt eine 32-Bit-Foundation-Classes-Bibliothek zur Verfügung, die einen Satz objektorientierter Programmierwerkzeuge für die Entwicklung von 32-Bit-Anwendungen enthält Ressourcen-Editor Ressourcen bestimmen die Benutzerschnittstellen von Windows-Programmen. Ressourcen eines Programms sind bestimmte Elemente der Benutzeroberfläche: Dialoge Menüs Tastaturkürzel Bitmaps Anwendungsbereiche Zeigersymbole Stringtabellen HTML-Code Versionsinformationen Benutzerdefinierte Ressourcen Ressourcen werden nicht durch C++-Anweisungen aufgebaut. Statt dessen bedient man sich zum Erstellen von Ressourcen spezieller Editoren, speichert die Ressourcen in speziellen Dateien (mit der Extension .rc, Ressourcenskriptdateien) und verwendet im C++-Quelltext spezielle Methodenaufrufe zum Laden der fertigen Ressourcen. Integrierte Entwicklungsumgebung (IDE) Visual C++ .NET ist eingebettet in eine integrierte Entwicklungsumgebung, die verschiedene Entwicklungswerkzeuge in einer einzigen, leicht zu bedienenden Umgebung zusammenfaßt. Die Visual C++ -Umgebung enthält folgende Hauptkomponenten: Editoren Compiler Debugger Projekt-Manager Browser Hilfe zum Verständnis der Beziehungen verschiedener Objekte in objektorientierten Programmen. Programmassistenten Bei diesen Assistenten handelt es sich um Utilities, die als einzige Schnittstelle zum Anwender eines oder mehrere Dialogfelder anzeigen, über die auf die Arbeit des Assistenten eingewirkt werden kann. Nachdem der Anwender seine Angaben abgeschlossen hat, erstellt der Assistent ein passendes Projekt, legt erste Dateien an und setzt ein Codegerüst auf, d.h. der Anwender wird von den mehr formalen Programmierangaben (z.B. Doc/View-Gerüst für MFC-Angaben, Einrichten einer Datenbankanbindung, Registrierung für ActiveX-Steuerelementanbindung) befreit. Eigenschaftsfenster Sie steuern das Verhalten von Visual C++. 318 Die Programmiersprache C++ Die Windows-API Da Windows eine geschützte Entwicklung von Microsoft ist und der Sourcecode nicht offengelegt ist, benötigen die Anwender einen Zugriff auf Windows-interne Funktionen – ein Application Programming Interface. Das Windows-API fasst eine große Anzahl Windows-Funktionen in einer Schnittstelle zusammen. Ältere Windows-API-Funktionen basieren auf reiner CProgrammierung und boten nur reine C-Funktionen an. Später wurde eine objektorientierte Schicht darüber gesetzt – die Microsoft Foundation Classes (MFC). Anwendungsgerüst Anwendungsgerüste vereinfachen die GUI-Programmierung. Sie stellen einen Satz von C++-Klassen bereit, die nachbilden, wie GUI-Programme arbeiten. Das Application-Framework, das zu Visual C++ gehört, ist eine Klassenbibliothek mit dem Namen: Microft Foundation Classes (MFC). Visual C++ schließt noch eine andere Art von Anwendungsgerüst mit ein: Die ActiveX Template Library (ATL). Das ist eine Sammliung von C++-Libraries, die es einfach machen, Objekte für das Component Object Model (COM) zu erzeugen. MFC-Bibliothek Bibliotheken wie die MFC-Bibliothek beginnen mit wenigen Basisklassen. Von diesen Basisklassen werden weitere Klassen abgeleitet. CObject ist eine Basisklasse, die extensiv in der Entwicklung von Windows-Umgebungen verwendet wird. HeaderDateien der MFC-Bibliothek befinden sich im Unterverzeichnis atlmfc/include. 6.1.2 Funktionsweise von Windows-Programmen Komponenten prozedurorientierter Windows-Anwendungen Alle Windows-Anwendungen enthalten zwei Komponenten - die Funktion WinMain() dient als Einstiegspunkt einer Windows-Anwendung und verhält sich ähnlich wie die main()-Funktion in gewöhnlichen C++-Programmen die Fensterfunktion. Eine Windows-Anwendung greift nie direkt auf eine Fensterfunktion zu. Falls eine Windows-Anwendung eine Standard-Fensterfunktion ausführen will, muß sie Windows auffordern, die angegebene Aufgabe auszuführen. Windows-Anwendungen besitzen deshalb eine sog. Callback-Fensterfunktion. Sie wird in Windows registriert und aufgerufen, wenn Windows eine Fensteroperation ausführt. Anlegen eines Win-API-Projekts 1. Aufruf des Befehls DATEI/NEU, Wechseln zur Seite PROJEKTE des Dialogfelds NEU; Angabe des Namens und des Verzeichnisses vom Projekt, Anlegen eines neuen Arbeitsbereichs, Wahl des Projekttyps WIN32-ANWENDUNG. 2. Entscheidung im nachfolgenden Dialogfeld für EINE EINFACHE WIN32-ANWENDUNG zur Einrichtung einer Quelltextdatei für das Programm. 3. Aufruf der Quelltextdatei zur Betrachtung des Aufrufs der WinMain()-Funktion. #include "stdafx.h" 319 Die Programmiersprache C++ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { // ZU ERLEDIGEN: Fügen Sie hier den Code ein. return 0; } Die Eintrittsfunktion WinMain() WinMain() wird von Windows aufgerufen und über Parameter mit passenden Argumenten versehen: hInstance. Windows muß alle im System existierenden Objekte (Anwendungen, Fenster, Gerätekontexte, Dateien, etc.) verwalten. Dazu muß es diese Objekte natürlich auch identifizieren und ansprechen können. Zu diesem Zweck weist Windows den Objekten sog. Handles zu – so auch jeder neu aufgerufenen Anwendung. Windows übergibt der Anwendung ihr eigenen Handle als Argument an hInstance. Diese Zahl identifiziert das Programm, wenn es unter Windows läuft. hPrevInstance. Relikt aus Win16, unter Win32 wird stets NULL übergeben. lpCmdLine. Hier erfogt die Übergabe der Kommandozeile des Programms nCmdShow. Dieser Parameter legt das Erscheinungsbild des Anwendungsfensters auf dem Desktop fest, z.B. ob das Fenster in normaler Größe, als Vollbild oder als Symbol aufgerufen werden soll174. Erzeugen des Hauptfensters HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HANDLE hInstance, LPVOID lpParam ); // // // // // // // // // // // Zeiger auf registr. Fensterklasse Zeiger auf Fenstername Fensterstil Horizonatale Position Vertikale Position Breite des Fensters Hoehe des Fensters Übergeordnetes Fenster / Besitzer Handle des Menüs Handle der Anwendung Zeiger auf Fensterdaten Fensterklassen. Unter Windows ist eine Fensterklasse eine Struktur, denn Windows ist nicht objektorientiert. typedef struct _WNDCLASS { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; } WNDCLASS; 174 // Fensterfunktion vgl. CWnd::ShowWindow() in OnLine-Hilfe 320 Die Programmiersprache C++ Eine Anwendung kann ihre eigene Fensterklasse definieren, indem sie eine Struktur des entsprechenden Typs erstellt und danach die Felder mit Informationen über die Fensterklasse füllt.. Nach Anmelden der Fensterklasse unter Windows können auf der Basis dieser Fensterklasse Fenster erzeugt werden. Alle Fenster einer Fensterklasse teilen sich die Elemente, die in der Fensterklasse definiert sind. Einige Fensterklassen sind in Windows vordefiniert und registriert, z.B. die Klassen für die Standardsteuerelemente. So heißt die Klasse für Schaltflächen BUTTON. Auf ihrer Grundlage kann man eigene Fenster erstellen, z.B.: hwnd = CreateWindow("BUTTON","Hallo Welt", WS_VISIBLE | BS_CENTER, 100, 100, 100, 80, NULL, NULL, hInstance, NULL); Definition einer Fensterklasse. Der folgende Quelltextauszug definiert eine Fensterklasse für Hauptfenster: WNDCLASS WinClass; // Fensterklasse // Speicher reservieren und Variable einrichten memset(&WinClass, 0, sizeof(WNDCLASS)); WinClass.style = CS_HREDRAW | CS_VREDRAW WinClass.lpfnWndProc = WndProc; WinClass.hInstance = hInstance; WinClass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); WinClass.hCursor = LoadCursor(NULL,IDC_ARROW); WinClass.lpszClassName = "Windows-Programm"; Bis auf WndProc sind alle Werte vordefiniert. WndProc muß mit passenden Parametern implementiert werden: // Fensterfunktion WinProc LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage, WPARAM wParam, LPARAM lParam) { return 0; // Rückgabewert 0, da die Funktionsweise noch nicht bekannt } Registrierung der Fensterklasse. Zur Registrierung der Fensterklasse verwendet man die API-Funktion RegisterClass(): if (!RegisterClass(&WinClass)) return(FALSE); Erzeugen der Fensterklasse. Nachdem die Fensterklasse registriert ist, kann auf Grundlage der Fensterklasse ein Fenster erzeugt und angezeigt werden. HWND hWindow; // Fenster-Handle // Erstelle Hauptfenster der Anwendung hWindow = CreateWindow("Windows-Programm", "API-Programm", WS_OVERLAPPEDWINDOW, 10, 10, 400, 300, NULL, NULL, hInstance, NULL); ShowWindow(hWindow, nCmdShow); UpdateWindow(hWindow); Die Funktion ShowWindow() ist erforderlich, um ein Fenster tatsächlich anzuzeigen. Der Parameter hWindow ist das Handle des Fensters, das durch Aufruf von CreateWindow() erstellt wird. Der zweite Parameter nCmdShow bestimmt, wie das Fenster anfänglich angezeigt wird (Sichbarkeitsstatus des Fensters). Falls bspw. nCmdShow durch die Konstante SW_SHOWINACTIVE ersetzt wird, die in winuser.h definiert ist, wird das Fenster als Symbol angezeigt. Andere Anzeigemöglichkeiten sind 321 Die Programmiersprache C++ SW_SHOWMAXIMIZED, die das Fenster aktiviert und bildschirmfüllend anzeigt, und das Gegenstüch SW_SHOWMINIMIZED. Der letzte Schritt zur Anzeige des Fensters ist UpdateWindow(hWindow). Der Aufruf von UpdateWindow() generiertdie WM_PAINT-Meldung, die dafür sorgt, dass der Arbeitsbereich aufgebaut wird. Vordefinierte Fensterklassen. Auf der graphischen Benutzeroberfläche können beliebig konfigurierte Fenster platziert werden. Jedes Dialogfeld, jedes Eingabefeld, jede Schaltfläche und jedes Textfeld ist ein Fenster. Die Windows-API besitzt einige Fensterklassen mit bereits vordefinierten Verhalten, die wichtigsten davon sind: Eingabefelder (edit), Schaltflächen (button), Textfelder, Komboboxen (combobox), Listenfelder (listbox). Erzeugen des Hauptfensters für die Anwendung. 1. Übertrage den vorstehend aufgeführten Code zur Erzeugung eines Fensters in das vorliegende API-Programm // pr00010.cpp : Definiert den Einsprungpunkt für die Anwendung. // #include "stdafx.h" // Vorwaertsdeklaration LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { // ZU ERLEDIGEN: Fügen Sie hier den Code ein. HWND hWindow; // Fenster-Handle WNDCLASS WinClass; // Fensterklasse // Speicher reservieren und Variable einrichten memset(&WinClass, 0, sizeof(WNDCLASS)); WinClass.style = CS_HREDRAW | CS_VREDRAW; WinClass.lpfnWndProc = WndProc; WinClass.hInstance = hInstance; WinClass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); WinClass.lpszClassName = "Windows-Programm"; // Fensterklasse anmelden if (!RegisterClass(&WinClass)) return(FALSE); // Erstelle Hauptfenster der Anwendung hWindow = CreateWindow("Windows-Programm", "API-Programm", WS_OVERLAPPEDWINDOW, 10, 10, 400, 300, NULL, NULL, hInstance, NULL); ShowWindow(hWindow, nCmdShow); UpdateWindow(hWindow); return 0; } // Fensterfunktion WinProc LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage, WPARAM wParam, LPARAM lParam) { return 0; } 322 Die Programmiersprache C++ 2. Ausführen des Programms (Strg + F5) Das Hauptfenster des Programms flackert kurz auf und verschwindet gleich wieder, weil die Anwendung sofort nach dem Aufruf beendet wird. Es fehlt eine Warteschleife, in der die Anwendung auf Windows-Nachrichten wartet und diese verarbeitet. 6.1.3 Eintritt in die Nachrichtenverarbeitung Windows fängt alle Benutzerereignisse (Eingaben über Maus und Tastatur) ab und schickt sie in Form von Nachrichten an die betreffenden Anwendungen, genauer gesagt an eine spezielle Warteschlangenstruktur (Message-Queue), die Windows für jede laufende Anwendung einrichtet. Die Message-Queue kann Meldungen enthalten, die von Windows generiert wurden, oder Meldungen, die von anderen Anwendungen gesendet wurden. Die Message Loop. Zum Auslesen der eingetroffenen Nachrichten muß die Anwendung eine Schleife implementieren, in der sie ständig die Nachrichtenwarteschlange nach Nachrichten abfragt und diese gegebenenfalls ausliest. // main message loop while (GetMessage(&msg, NULL, 0, 0)) { if (!TranlateAccelerator(msg.hwnd, hAccelTable, &msg)) TransLateMessage(&msg); DispatchMessage(&msg); } GetMessage() kopiert die Meldung in die Meldungsstruktur, auf die der long-Zeiger &msg verweist und übergibt die Meldungsstruktur an die Hauptfunktion des Programms. Der NULL-Parameter weist die Funktion an, jede Meldung für jedes Fenster abzufragen, das zu der Anwendung gehört. Die letzten beiden Parameter weisen GetMessage() an, keine Meldungsfilter anzuwenden. Meldungsfilter können die abgefragten Meldungen auf spezielle Kategorien, z.B. bestimmte Tastatureingaben oder Mausbewegungen, beschränken175. Die Meldung WM_QUIT ist die einzige Meldung, mit der eine Anwendung die Meldungsschleife beeinflussen kann. Die Funktion TranslateMessage() wandelt VirtualKey-Meldungen in Zeichen-Meldungen. Sie wird nur bei Anwendungen benötigt, die Zeichen verarbeiten müssen, die über die Tastatur eingegeben werden. Die Funktion DispatchMessage() sendet Widows aktuelle Meldungen an die korrekten Fensterprozeduren. Mit dieser Funktion können leicht zusätzliche Fenster und Dialogfelder zu einer Anwendung hinzugefügt werden. DispatchMessage() leitet jede Meldung an die entsprechende Fensterprozedur weiter. Nachrichten, die die Anwendung über vom Anwender ausgelöste Ereignisse informieren, laufen stets über die MessageLoop: 1. Ereignis tritt auf, bspw. die Bewegung der Maus 175 Die Filter heißen wMsgFilterMin und wMsgFilterMax. Sie geben an, welche Grtenzwerte für die numerischen Filter gelten sollen. 323 Die Programmiersprache C++ 2. Ereignis wird in die Message Queue eingetragen. Die System Message Queue –Warteschleife fängt erst einmal alle Eingaben ab, die von Perepheriegeräten kommen (Maus, Tastatur, Drucker, etc.) 3. Die System Message Queue wird abgearbeitet (First in, First out). Die abgefangenen Ereignisse werden ausgelesen und auf die Message Queue der zugehörigen Threads verteilt. Klickt bspw. der Anwender in ein Fenster, wird der Thread ermittelt, der dieses Fenster erzeugt hat, und die Botschaft wird in seine Message Queue eingetragen. 4. Die Botschaft wird in der MessageLoop empfangen, übersetzt (in für den Programmierer leichter zu lesende Parameter) und an die bearbeitende Fensterfunktion weitergereicht. 5. Die Fensterfunktion des Fensters erhält die Botschaft. Letztendlich werden Nachrichten an Fenster geschickt. Daher definiert jedes Fenster eine eigene Fensterfunktion, die einkommende Botschaften empfängt und einer passenden Bearbeitung zuführt. Windows Mausbewegung Anwendung Eintrag in SystemQueue Anwendung wird in CPU geladen WM_MOUSEMOVE MessageLoop fordert Meldung an fordert Botschaft Eintrag in Application-Queue WM_MOUSEMOVE MessageLoop verarbeitet Meldung WM_MOUSEMOVE Windows wird angewiesen, Fensterfunktion aufzurufen Fensterfunktion (reagiert auf Mausbewegung) Abb.: Ereignisbehandlung von Windows Nachrichten, die die Anwendung über vom Anwender ausgelöste Ereignisse informieren, laufen stets über die Message Loop. Die Fensterfunktion Windows ruft die Fensterfunktion des Fensters auf, an das die Nachricht gerichtet ist, und die Nachricht als Argument an die Parameter der Funktion übergibt. Aufgabe des Programmierers ist es, in der Funktion abzufragen, welche Nachricht empfangen wurde und dann auf die jeweilige Nachricht passend zu reagieren. Zu diesem Zweck richtet man in der Fensterfunktion eine switch-Verzweigung ein und für jede zu behandelnde Funktion einen case-Block. Da der Programmierer nicht für alle der über 200 Nachrichten Code zur Beantwortung bereitstellen kann, ist in Windows die Funktion DefWindowProc() definiert, der man im default-Block alle Nachrichten, die man nicht selbst behandeln möchte, weiterreicht. Eine Nachricht muß man aber auf jedem Fall behandeln: WM_DESTROY. Damit die Anwendung beendet wird, muß die Message Loop der Anwendung beendet werden. Das geschieht, in dem man als Antwort auf die WM_DESTROY-Nachricht die API-Funktion PostQuitMessage() aufruft. Diese schickt ihrerseits eine WM_QUIT-Nachricht an die Anwendung, diese beendet die Message Loop. 324 Die Programmiersprache C++ Windows verfügt über mehrere hundert verschiedene Windows-Meldungen, die es an die Fensterfunktion senden kann. Die Meldungen haben Bezeichner, die mit WM_ beginnen. Bspw. werden WM_COMMAND, WM_DESTROY, WM_INITDIALOG und WM_PAINT recht häufig verwendet. Die Bezeichner werden auch als symbolische Konstanten bezeichnet. Im nachfolgenden Beispiel wird eine Meldung mit der symbolischen Konstanten WM_LBUTTONDOWN (Drücken der linken Maustaste) behandelt. Alle Mausbewegungen werden als Nachrichten kodiert: WM_MOUSEMOVE WM_LBUTTONDOWN WM_LBUTTONUP WM_RBUTTONDOWN WM_RBUTTONUP WM_MBUUTONDOWN WM_MBUTTONUP Maus wird bewegt Linke Maustaste wird gedrückt Linke Maustaste wird losgelassen Rechte Maustaste wird gedrückt Rechte Maustaste wird losgelassen Mittlere Maustaste wird gedrückt Mittlere Maustaste wird losgelassen Die Nachrichtenparameter enthalten die Koordinaten und weitere Informationen: signed int xPos = LOWORD(lParam); // X-Koordinate signed int yPos = HIWORD(lParam); // Y-Koordinate Die Koordinaten relativ zum Fensterursprung (linke obere Ecke). Bsp. : Eigene Nachrichtenbehandlung nach WM_LBUTTONDOWN). Drücken der linken Maustaste ( // Fensterfunktion WndProc LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage, WPARAM wParam, LPARAM lParam) { char str[30] = "Hier erfolgte ein Mausklick"; HDC dc; // Beantworte Nachrichten mit entspechenden Aktionen switch(uiMessage) { case WM_LBUTTONDOWN: dc = GetDC(hWnd); TextOut(dc,LOWORD(lParam), HIWORD(lParam), str, strlen(str)); // Gerätekontext freigeben ReleaseDC(hWnd, dc); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProc(hWnd, uiMessage, wParam, lParam); // Alle Nachrichten, die nicht mit eigenen Antwort-Funktionen // verbunden werden. } } Der erste Parameter von WndProc ist hWnd. Das Handle hWnd ist ein Zeiger auf das Fenster, an das Windows die Meldung sendet. Da eine Fensterfunktion Meldungen für mehrere Fenster verarbeiten kann, die mit derselben Fensterklasse erstellt werden, verwendet die Fensterfunktion dieses Handle, um festzustellen, welches Fenster die Meldung empfangen soll. Der zweite Parameter der Funktion, uiMessage, gibt die tatsächliche Meldung an, die verarbeitet werden soll. Die Meldung ist in winuser.h definiert. 325 Die Programmiersprache C++ Die beiden letzten Parameter, wParam und lParam, enthalten Informationen zur Verarbeitung der jeweiligen Meldung. Bei vielen Funktionen haben diese Parameter den Wert Null. Verarbeitung von WM_PAINT-Meldungen. WM_PAINT verarbeitet alle WindowsPAINT-Meldungen. Hier kann die Windows-Funktion BeginPaint() aufgerufen werden. Diese Funktion bereitet das angegebene Fenster auf die Darstellung ("Painting") vor und füllt eine PAINTSTRUCT(&ps) mit Daten über den Bereich, der dargestellt werden soll. Die Funktion BeginPaint() gibt ein Handle auf den Gerätekontext des gegebenen Femsters zurück. Nachrichtenübertragung ohne MessageLoop Eine Reihe von Nachrichten haben nur indirekt etwas mit Benutzeraktionen zu tun und werden intern von Windows verschickt (z.B. Windows informiert ein Fenster darüber, daß es gerade verschoben, neu dimensioniert oder geschlossen wird). Windows umgeht dabei auch die Message Queues der einzelnen Threads und schickt die Nachricht statt dessen direkt an die Fensterfunktion des betroffenen Fensters. 1. Ereignis tritt auf (z.B. der Anwender hat in der Titelleiste eines Fensters die Schaltfläche zum Schließen geglickt). 2. Windows schickt eine entsprechende Botschaft direkt an die Fensterfunktion(en) des oder der betroffenen Fenster (in diesem Bsp. WM_CLOSE). 3. Die Fensterfunktion empfängt die Botschaft und führt sie einer korrekten Verarbeitung zu. 4. Ereignis tritt auf (z.B. der Anwender hat in der Titelleiste eines Fensters die Schaltfläche zum Schließen geglickt). 5. Windows schickt eine entsprechende Botschaft direkt an die Fensterfunktion(en) des oder der betroffenen Fenster (in diesem Bsp. WM_CLOSE). 6. Die Fensterfunktion empfängt die Botschaft und führt sie einer korrekten Verarbeitung zu. 326 Die Programmiersprache C++ 6.1.4 Vom API zur MFC Konzepte der API-Programmierung sind in der MFC gekapselt. WinMain() und Anwendungsobjekt MFC-Programme definieren keine WinMain()-Eintrittsfunktion. Statt dessen wird eine globale Instanz der Anwendungsklasse definiert. Die WinMain()Eintrittsfunktion ist im Code der MFC versteckt (Modul AppModul.cpp) und wird bei Verwendung der MFC-Bibliotheken automatisch mit eingebunden. Die WinMain()Funktion ruft die globale MFC-Funktion AfxWinMain() auf. Diese greift auf das Anwendungsobjekt des Programms zu und ruft dessen InitInstance()-Methode auf, in der alle wichtigen Initialisierungsarbeiten von der Auswertung bis zur Erzeugung des Hauptfensters vorgenommen werden. Message Loop und Run() Die Message Loop der API-Programme ist in der Run()-Methode der MFC-Klasse CWinApp gekapselt. Aufgerufen wird die Methode in der MFC-Funktion AfxWinMain(). Erzeugung der Fenster In API-Programmen erstellt man Fenster durch Einrichtung und Registrierung einer Fensterklasse und der nachfolgenden Erzeugung eines Fensters von dieser Fensterklasse. In der MFC erzeugt man zuerst ein Objekt einer MFC-Fensterklasse und ruft dann die Methode Create() auf, die das eigentliche Fenster erzeugt und mit dem Objekt verbindet. Der Methode Create() kann man wie der API-Funktion CreateWindow() eine eigene Fensterklasse übergeben.. Übergibt man in der Create()-Methode als erstes Argument den Wert NULL, wird die Standardfensterklasse von CWnd verwendet. In vielen Fällen braucht man für das Hauptfenster der Anwendung die Methode Create() nicht selbst aufzurufen. Man kann die Methode LoadFrame() verwenden oder – im Fall von Doc/View-Anwendungen – die ganze Arbeit vom Konstruktor der Dokumentenklasse erledigen lassen. Zur Beibehaltung der Kontrolle über die Erzeugung und Konfiguration der Fenster, kann man die Methoden PreCreateWindow() und OnCreate() überschreiben. Der Parameter nCmdShow der WinMain()-Funktion entspricht der MFC-Variablen m_nCmdShow, die in der Methode InitInstance() an die Methode ShowWindow() übergeben werden kann. Fensterfunktion und Antworttabellen Nachrichten werden in API-Programmen in Fensterfunktionen behandelt. Fensterfunktionen enthalten eine switch-Anweisung, in der für alle zu 327 Die Programmiersprache C++ behandelnden Nachrichten case-Blöcke vorgesehen werden. In dem case-Block steht dann der Code, der als Antwort auf die Nachricht ausgeführt wird. Ist dieser Code umfangreicher, dann kann man ihn auch in eine eigene Funktion auslagern, die dann im case-Block aufgerufen wird. Würde man dies für alle behandelten Nachrichten machen, wäre die Fensterfunktion genauso aufgebaut wie eine Antworttabelle. In MFC-Programmen werden Nachrichten in Antworttabellen (MESSAGE_MAP) weitergeleitet. In der Antworttabelle (Meldungs-) ist festgehalten, für welche Nachricht welche Methode zur Beantwortung aufgerufen werden soll. Nachrichtentabellen sind ein fester Bestandteil der Windows-Programmierung mit MFC. Nachrichtentabellen werden in der Regel von den Assistenten erzeugt und bestehen aus zwei Teilen. Der erste Teil befindet sich in der Headerdatei und der zweite Teil in der entsprechenden .cpp Datei. Die Nachrichtentabellen sind in Form von Makros realisiert worden. Die Deklaration einer Nachrichtentabelle in der Headerdatei einer Anwendung, die vom Anwendungs-Assistenten erzeugt wurde, kann bspw. folgendermaßen aussehen: //{{AFX_MSG(CPr00011App) //}}AFX_MSG DECLARE_MESSAGE_MAP() Der zweite Teil der Nachrichtentabelle in der entsprechenden .cpp Datei sieht so aus: BEGIN_MESSAGE_MAP(CPr00011Dlg, CDialog) //{{AFX_MSG_MAP(CPr00011Dlg) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_WM_LBUTTONDOWN() //}}AFX_MSG_MAP END_MESSAGE_MAP() Die ON_-Makros der Antworttabelle erfüllen die gleiche Aufgabe wie die case-Blöcke der Fensterfunktion, z.B.: case WM_LBUTTONDOWN: ON_WM_LBUTTONDOWN() Makros der Meldungstabelle In Meldungstabellen werden eine Reihe von Makros verwendet, z.B.: DECLARE_MESSAGE_MAP BEGIN_MESSAGE_MAP END_MESSAGE_MAP ON_COMMAND ON_COMMAND_RANGE ON_CONTROL ON_CONTROL_RANGE Wird in der Headerdatei verwendet, um anzuzeigen, daß es eine Meldungstabelle in der Quelldatei gibt Markiert den Anfang der Meldungstabelle in der Quelldatei Markiert das Ende einer Meldungstabelle in der Quelldatei Delegiert die Behandlung eines speziellen Befehls an eine Memberfunktion der Klasse Delegiert die Behandlung einer speziellen Gruppe von Befehlen, repräsentiert durch eine Bereich von Befehls-IDs, an eine einzelne Memberfunktion der Klasse Delegiert die Behandlung einer speziellen Benachrichtigung für ein benutzerdefiniertes Steuerelement an eine Memberfunktion der Klasse Delegiert die Behandlung einer Benachrichtigung einer 328 Die Programmiersprache C++ Gruppe von benutzerdefinierten Steuerelementen, repräsentiert durch einen Bereich von Steuerelement-IDs, an eine Memberfunktion der Klasse ON_MESSAGE Delegiert die Behandlung einer benutzerdefinierten Meldung an eine Memberfunktion der Klasse ON_REGISTERED_MESSAGE Delegiert die Behandlung einer registrierten benutzerdefinierten Meldung an eine Memberfunktion der Klasse. ON_UPDATE_COMMAND_UI Delegiert die Aktualisierung eines speziellen Befehls an eine Memberfunktion der Klasse ON_COMMAND_UPDATE_UI_RANGE Delegiert die Aktualisierung einer Gruppe von Befehlen, repräsentiert durch einen Bereich von Befehls-IDs, an eine einzelne Memberfunktion der Klasse. ON_NOTIFY Delegiert die Behandlung einer Steuerelementbenachrichtigung mit Zusatzdaten an eine Memberfunktion der Klasse. ON_NOTIFY_RANGE Delegiert die Behandlung einer Gruppe von Steuerelementbenachrichtigungen mit Zusatzdaten, repräsentiert durch einen Bereich von Kindbezeichnern, an eine einzelne Memberfunktion der Klasse. Die Steuerelemente, die diese Benachrichtigung senden, sind Kindfenster des empfangenden Fensters. ON_NOTIFY_EX Delegiert die Behandlung einer Steuerelementbenachrichtigung mit Zusatzdaten an eine Memberfunktion der Klasse, die TRUE oder FALSE zurückliefert, um anzuzeigen, dass die Benachrichtigung an ein anderes Objekt zur weiteren Bearbeitung weitergeleitet werden soll. ON_NOTIFY_EX_RANGE Delegiert die Behandlung einer Gruppe von Steuerelementbenachrichtigungen mit Zusatzdaten, repräsentiert durch einen Bereich von Kindbezeichnern, an eine einzelne Memberfunktion der Klasse, Die TRUE oder FALSE zurückliefert, um anzuzeigen, dass die Benachrichtigung an ein anderes Objekt zur weiteren Bearbeitung weitergereicht werden soll. Die Steuerelemente, die diese Benachrichtigung senden, sind Kindfenster des empfangenden Fensters. Außer diesen Makros gibt es noch mehr als 100 weitere Makros, die eine einzelne, spezielle Meldung an eine Memberfunktion weiterleten. So delegiert z.B. ON_CREATE die WM_CREATE-Meldung zu einer Funktion mit dem Namen OnCreate(). Namen von Behandlungsfunktionen. In der Fensterfunktion bleibt es dem Programmierer überlassen, wie er seine Behandlungsmethode nennen will. In den Antworttabellen müssen spezielle Makros verwendet werden. Für jede Nachricht ist der Name des Makros in der zugehörigen Behandlungsmethode festgelegt. Zur Ableitung des Makro-Namens für die Behandlungsmethode aus dem Namen der Nachricht (z.B. WM_LBUTTONDOWN) stellt man dem Namen der Nachricht einfach das Präfix ON_ voran (ON_WM_LBUTTONDOWN()). Der Name ergibt sich dann aus dem Makronamen, indem man sämtliche Unterstriche und das Präfix WM herausstreicht und nur noch die Anfangsbuchstaben der Silben groß schreibt (OnLButtonDown()). Die in den Makros festgelegten Namen können nicht verändert werden. Um Makros in Meldungstabellen einzufügen, verwendet man am besten die Klassenansicht. 329 Die Programmiersprache C++ Empfang von Nachrichten. Auch in MFC-Programmen werden Nachrichten von Windows an Fensterfunktionen geschickt: Handelt es sich um eine WM-Nachricht, werden spezielle Methoden der MFC aufgerufen, die nachschauen, ob in dem Fenster, an das die Nachricht gerichtet ist, eine Antworttabelle mit einem passenden Eintrag definiert ist. Wenn ja, wird die zugehörige Behandlungsmethode aufgerufen. Für COMMAND-Nachrichten (Menübefehle, Tastaturkürzel) kann die Nachricht sogar innerhalb der Klassen des Anwendungsgerüsts weitergereicht werden. Meldungen, die vom MFC-Code abgefangen werden MFC-Klassen bearbeiten den größten Teil allgemeiner Meldungen, ohne dass dafür eine Zeile Code geschrieben werden muß. Bspw. muß die Meldung, dass der Anwender DATEI/SPEICHERN UNTER gewählt hat, nicht selbst abgefangen werden. Die MFC fängt diese Meldung ab, ruft das ÖFFNEN-Dialogfeld zum Einlesen des neuen Dateinamens auf, kümmert sich um alle im Hintergrund zu erledigenden Aufgaben und ruft zum Speichern eine vom Anwender definierte Funktion auf, die den Namen Serialize() tragen muß. Gerätekontexte Anstatt der API-Funktion GetDC() wird stets eine der MFC-Gerätekontextklassen instantiiert. Aufruf von API-Funktionen Die MFC kapselt die am häufigsten benötigten API-Funktionen. In ihren Methoden und Klassen. Für API-Funktionen, die auf Systemfunktionen zugreifen oder der Shell-Programmierung dienen, gibt es allerdings keine Entsprechung in der MFC. Darüber hinaus, gibt es zusätzlich zur Windows-API noch eine Reihe weiterer APIs für spezielle Gebiete der Windows-Programmierung: die TAPI für TelefonieAnwendungen, die MAPI für Nachrichtenanwendungen, die DDI zum Schreiben von Gerätetreibern. Bei der Verwendung dieser API-Funktionen, ist zu beachten: - Voranstellen des Gültigkeitsbereichsoperator ::, damit der Compiler weiß, dass es sich um eine globale Funktion und nicht eine Klassenmethode handelt. Alle API-Funktionen verlangen als erstes den Handle des Objekts, auf das der Bezug stattfindet API-Funktionen verwenden keine Defaultargumente. API-Funktionen sind nicht überladen. 330 Die Programmiersprache C++ 6.1.5 Erstellen einer Windows-Anwendung mit MFC Eine MFC-Anwendung ruft niemals eine Windows-API-Funktion direkt auf. Statt dessen konstruiert die Anwendung ein Objekt des entsprechenden Typs und nutzt dessen Elementfunktionen. Der Konstruktor und der Destruktor überwachen die Initialisierung sowie die Freigabe der Systemressourcen. Klassenhierachie und Notation Klassenhierachie In der MFC findet man verschiedene Katagorien von Klassen. 1. Die Basisklasse CObject Die überwiegende Anzahl der MFC-Klassen ist von der Basisklasse CObject abgeleitet, durch die die Eigenschaften der Serialisierung und der Laufzeitinformation implementiert werden. Durch die Serialisierung wird ein einfacher und sicherer Mechanismus zur Speicherung eines Objekts in eine Datei und zum Laden aus einer Datei über ein Objekt der CArchiv-Klasse geschaffen. Durch die Laufzeitinforamtion, die über die CRunTimeClass realisiert und durch Makros DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC, DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE oder DECLARE_SERIAL/IMPLEMENT_SERIAL implementiert werden, wird die Feststellung zur Klassenzugehörigkeit eines Objekts während der Laufzeit möglich. 2. Die Fensterunterstützungsklassen Sie stellen einen Sammelbehälter für allgemeine Fenstertypen wie Rahmen-, Ansichts-, Dialogfenster oder Steuerelemente zur Verfügung. Sie werden von der CWnd-Klasse abgeleitet, die eine große Menge an Basisfunktionalitäten wie Initialiserung, Fensterzugriff, Größe und Position, Fenstertext, Bildlauf, Menü, Zeitgeber, Nachrichten und OLE-Steuerelemente enthält. Rahmenfensterklassen umfassen die Funktionalität der Hauptfenster der Applikation und der Ansichten. Sie verwalten Menü- und Statusleisten sowie Werkzeugleistenschaltflächen. Dazu zählen die SDI (Einzeldokument) und MDI (Multidokument) –Anwendungsrahmen, die alle von der CFrameWnd-Klasse abgeleitet werden. In Ansichtsklassen (von CView abgeleitet) werden gewöhnlich die Inhalte eines Dokuments in verschiedenen Darstellungsarten sichtbar gemacht. Sie unterstützen den Bildlauf, Textbearbeitung, Listenfelder, Formulare, Dialoge u.ä. Dialogklassen (CDialog) enthalten die Funktionalität benutzerdefinierte Dialoge und Standarddialoge. Steuerelementklassen umfassen die Funktionalität der Windows-Steuerelemente wie Schaltflächen, Textfelder, Eingabefelder, Listenfelder, Kombinationsfelder, Sinnbilder, Kontrollkästchen, Optionsfelder, Drehfelder, Statusanzeigen u.ä. 3. Applikationsunterstützungsklassen Sie werden von der Basisklasse CCmdTarget abgeleitet. Sie können Nachrichen bearbeiten und verfügen deshalb über eine Nachrichtentabelle. Da Fenster ebenfalls Nachrichten empfangen können ist die Klasse CWnd von CCmdTarget abgeleitet. Applikationsobjekte (CWinApp) repräsentieren Prozesse und Threads. Jede MFCAnwendungdrahmenapplikation verfügt ein über CWinApp abgeleitetes Objekt, das die Hauptnachrichtenschleife zur Verfügung stellt. Dokumente (CDocument) repräsentieren in MFC-Applikationen Daten, die vom Anwender geöffnet, bearbeitet und gespeichert werden können. Dokumente kooperieren mit Ansichten, in denen die Datendargestellt und durch den Benutzer ninteraktiv bearbeitet werden. Dokumentenvorlagen (CDocTemplate) beschreiben das grundlegende Verhalten von benutzerdefinierten Dokumenten und Ansichten. Mit ihrer Hilfe werden Rahmenfenster, Dokument und Ansicht zu einer Einheit verknüpft. 4. Grafikunterstützungsklassen Sie umfassen die GDI-Objekte (CGdiObject) und die Gerätekontexte (CDC), die das Zeichnen auf Ausgabegeräte mit hardwareunabhängigen Systemfunktionen ermöglichen. Notation 331 Die Programmiersprache C++ - Klassennamen beginnen mit einem großen C (z.B. CObject). - Membervariablen (Attribute) verwenden den Präfix m_ (z.B. m_lpszName) - Funktions- und Methodennamen beginnen mit einem Großbuchstaben. Jedes Teilwort beginnt selbst wieder mit einem Großbuchstaben. Das erste Wort ist in der Regel ein Verb (z.B. GetWindowHandle). - Microsoft empfiehlt die ungarische Methode zur Benennung von Variablen. Dem Namen wird ein Typkürzel vorangestellt. Typkürzel können auch kombiniert werden (z.B. pszName, m_nWert) Kürzel b c oder ch dw h lpsz n p w wnd l cx, cy clr str m_ sz Datentyp BOOL char DWORD Handle int Pointer WORD CWnd LONG Horizontale bzw. vertikale Position COLORREF CString Member-Variable 0-terminierter String Abb.: Eine sehr einfache Windows-Anwendung mit MFC 1. Erstellen eines Projekts mit einer Win32-Anwendung 2. Aufnahme der zwei folgenden Dateien über das Dialogfenster "Neues Element hinzufügen" /* MyMFCAnw.h */ #include <afxwin.h> class CMyMFCAnw:public CWinApp { public: virtual BOOL InitInstance(); }; class CMainFrame:public CFrameWnd { public: CMainFrame(); }; /* MyMFCAnw.cpp */ #include "MyMFCAnw.h" CMyMFCAnw meineAnw; BOOL CMyMFCAnw::InitInstance() { m_pMainWnd = new CMainFrame; m_pMainWnd->ShowWindow(m_nCmdShow); return TRUE; } 332 Die Programmiersprache C++ CMainFrame::CMainFrame() { Create(NULL,"Programm MyMFCAnw"); } 3. Öffnen des Dialogfensters "Eigenschaftsseiten" (z.B. durch Klick mit der rechten Maustaste auf das Projektverzeichnis, dann Klick mit der linken Maustaste im resultierenden Menü auf den Menüpunkt "Eigenschaften"). 4. Einstellen der Projekteigenschaften nach der vorliegenden Vorgabe. 5. Anschließend Klick auf OK im Dialogfenster "Eigenschaftsseiten". 6. Das Projekt kann anschließend erstellt und zum Laufen gebracht werden. Es erzeugt ein leeres Rahmenfenster. MFC-Programme müssen die Header-Datei afxwin.h einbinden. Dies ist eine Datei mit mehreren tausend Zeilen, die selbst noch andere große Dateien inkludiert (z.B. windows.h). Ein MFC-Programm muß mindestens zwei Klassen deklarieren und je ein Objekt der folgenden Klassen erzeugen: - Ein Objekt der Applikationsklasse repräsentiert das eigentliche Programm, diese Klasse (CMyMFCAnw) muß von der Basisklasse CWinApp abgeleitet werden. Ein Objekt einer Fensterklasse für das Hauptfenster fungiert als Rahmenfenster, die Klasse (CMainFrame) kann z.B. von der Basisklasse CFrameWnd abgeleitet werden. Von der Applikationsklasse wird genau eine globale Instanz erzeugt (meineAnw ). Globale Instanzen werden vor der Abarbeitung des Hauptprogramms erzeugt, so dass der Konstruktor von CWinApp die ersten Aktionen des Programms ausführt, verschiedene Windows-Variablen werden initialisiert, einer globalen Variablen wird 333 Die Programmiersprache C++ der Zeiger auf die Instanz meineAnw zugewiesen. Auf diesen Zeiger kann bei Bedarf mit der ebenfalls globalen Funktion AfxGetApp() zugegriffen werden. Anschließend startet die (in den Basisklassen versteckte) Funktion WinMain. Sie greift auf die Methoden der Applikationsklasse CMiniMFCApp zu. Die wichtigsten von WinMain zu startenden Methoden der Anwendungsklasse übernehmen folgende Aufgaben: - - Die Methode InitInstance sollte grundsätzlich von der Applikationsklasse des Programms überladen werden, da in der CWinApp-Version kein Fenster erzeugt wird. InitInstance ist der geeignete Ort, um Parameter der Applikation zu initialiseren, um das Hauptfenster zu erzeugen und auf den Bildschirm zu bringen. Die CWinApp-Methode Run betreibt die Nachrichtenschleife. Die Nachrichtenschleife bricht beim Eintreffen der WM_QUIT-Botschaft ab und startet die Methode ExitInstance, die sich für Aufräumarbeiten anbietet und für einen solchen Fall überladen werden müsste. Im vorliegenden Beispiel erzeugt die Methode InitInstance ein Objekt der Fensterklasse CMainFrame, deren Adresse in der Variablen m_pMainWnd abgelegt wird. Mit dem Zeiger m_pMainWnd kann man auf alle Methoden der Fensterklasse zugreifen. Mit der Methode ShowWindow wird das Fenster auf den Bildschirm gebracht. Mit der Methode Create von CMainFrame wird der Name der Fensterklasse (hier NULL) und der Fenstertitel gesetzt. Erweiterung der einfachen Windows-Anwendung mit MFC durch Nachrichtenschleife und eine neue Nachrichtenbehandlungsmethode Die Erweiterungen betreffen die Nachrichtenschleife und die neue Nachrichtenbehandlungsmethode, mit der der Text "Hallo, MFC-Welt!" in den Clientbereich geschrieben wird. /* */ #include <afxwin.h> class CMyMFCAnw:public CWinApp { public: virtual BOOL InitInstance(); }; class CMainFrame:public CFrameWnd { public: CMainFrame(); protected: afx_msg void OnPaint(); // behandelt die WM_PAINT-Botschaft DECLARE_MESSAGE_MAP() // übernimmt die Deklaration von Daten und Methoden // der Message Map }; /* … */ #include "MyMFCAnw.h" CMyMFCAnw meineAnw; 334 Die Programmiersprache C++ BOOL CMyMFCAnw::InitInstance() { m_pMainWnd = new CMainFrame; m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); // Das Neuzeichnen des Fensters wird durch UpdateWindow // infolge OnPaint ausgelöst return TRUE; } // Von den drei folgenden Makros wird Code für die Zuordnung // der Botschaften (WM_PAINT) zu ihren Behandlungsroutinen // erzeugt BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_PAINT() END_MESSAGE_MAP() CMainFrame::CMainFrame() { Create(NULL,"Programm MyMFCAnw"); } void CMainFrame::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(&rect); dc.DrawText("Hallo, MFC-Welt!", &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); } 335 Die Programmiersprache C++ 6.2 Die zentralen MFC-Klassen (Zusammensetzung und Zusammenspiel) 6.2.1 Übersicht zu den zentralen MFC-Klassen Ein MFC-Programm besteht mindestens aus einer von der Anwendungsklasse CWinApp abgeleiteten Klasse. Sie besorgt die Programmabarbeitung jedoch nicht die Fenstererstellung. Im Programm-Code wird die CWinApp-Methode InitInstance() mit der zur Fenstererzeugung und –anzeige nötigen Befehle überladen. Es gibt daher auch keine WinMain()-Funktion mehr. Die Klasse CWnd ist die Basisklasse aller Fenster. Von ihr leiten sich alle weiteren wichtigen Fensterklassen ab: - CFrameWnd (Rahmenfenster) - CView (Unterschiedliche Ansichten) - CDialog (Common Dialog Boxes, eigene Dialoge) - CDocuments (Dokumente) - CDocTemplates (Dokumenten-Templates) - CMDIChildWnd(MDI-Kind) - CMDIFrameWnd (MDI-Parent) - Controls (CAnimateCtrl, CButton, CComboBox, CEdit, CHeaderCtrl, CHotKeyCtrl, CListBox, CListCtrl, CProgressCtrl, COleControl, CRichEditCtrl, CScrollBar, CSliderCtrl, CSpinButtonCtrl, CStatic, CStatusBarCtrl, CTabCtrl, CToolBarCtrl) In der Klasse CWnd setzt der Mechanismus zur Abarbeitung der Nachrichten in einer Fensterfunktion an. Er wird mit Hilfe sog. Message Maps und zugehöriger Makros realisiert. Die Member-Funktionen der Klasse CWnd unterteilen sich in folgende Funktionen: Fenster-Initialisierung und -Erzeugung Funktionen zur Abfrage des Fensterstatus Abfrage und Veränderung von Fenstergröße und -position Fenster-Identifikation Update- und Zeichenfunktionen Koordinaten-Mapping Manipulation des Systemmenüs Manipulation von Dialog Box Items Manipulation von Menüs Setzen und Löschen von Zeitgebern (Timer) Alarmfenster (messageBox) Window-Messages Manipulation des Clipboard-Inhalts Die Klasse CFrameWnd (abgeleitet von CWnd) besitzt alle Eigenschaften eines normalen Popup-Windows. Es kann somit als Hauptfenster der Anwendung dienen. Es sind darin keine weiteren Funktionalitäten realisiert. Weitere von CWnd abgeleitete Klassen machen es auf einfache Art möglich, weitere grafische Gestaltungselemente in ein Fenster einzuführen: CToolBar CStatusBar CDialogBar CSplitterWnd (geteiltes Fenster) 336 Die Programmiersprache C++ Zur Erzeugung und Verwaltung von Klassen wird die Klasse CDialog genutzt. Man unterscheidet auch bei diesen Dialogen zwischen modal und nicht modal. Von CDialog ausgehend sind weitere Sonderklassen definiert, die in der Datei COMMDLG.DLL für alle Windowsprogrammierer vorgegeben sind: - CFileDialog - CColorDialog - CFontDialog - CFindReplaceDialog Die Ausgabe von Text und Zeichnung erfolgt über den Gerätekontext. In der MFC stammen diese von der Klasse CDC ab. Abgeleitete Kontexte hiervon sind - CClientDC - CWindowDC - CPaintDC - CMetaFileDC Zur Ausgabe von Grafiken wurden Klassen mit den wichtigsten Werkzeugen definiert: - CPen - CBrush - CFont - CBitMap - CPalette - CRgn Die MFC stellt auch viele Funktionen und Klassen zur Verfügung, die die Handhabung von Documents und View vereinfachen. Damit können Dokumente durch mehrere von ihnen abhängige Ansichten dargestellt werden. Die beteiligten Klassen sind: - CDocument - CDocTemplate - CView 6.2.2 Allgemeine Ereignisbehandlung in Windows mit der MFC Prizipieller Ablauf 1. Das Betriebssystem erkennt ein Ereignis (Mausklick, Tastaturbetätigung, etc. ). 2. Ermittlung des Fensters, für das das Ereignis bestimmt ist. 3. Aufbau einer speziellen Funktion der zugehörigen Anwendung. Die Parameter der Funktion beschreiben exakt das Ereignis. 4. Arbeitsweise der Funktion - Ermittlung des Ereignistyps anhand der Parameter - Aufruf der zum Ereignistyp gehörigen Ereignisfunktion 5. Die Ereignisfunktion führt enwendungsspezifische Aktionen aus (z.B. das Zeichnen einer Linie) Bedingungen 337 Die Programmiersprache C++ - Das Betriebssystem muß die Position und Größe aller Fenster sowie die zugehörigen Programme kennen Die Anwendung muß wissen, welche Funktionen bei einem bestimmten Ereignis aufgerufen werden sollen. 6.2.3 Verschiedene spezielle MFC-Klassen 6.2.3.1 Spezielle Klassen für die Textverarbeitung Textverarbeitung gehört zu den wichtigsten und am häufigsten benötigten Aufgaben der Windows-Programmierung. Entsprechend vielseitig ist das Angebot an unterstützenden Klassen. 1. Meldungsfenster (Einfachster Weg zur Ausgabe eines kurzen Textes) Sie sind in Windows vordefiniert und können durch den Aufruf einer einzigen Methode erzeugt und angezeigt werden. int CWnd::Messagebox(LPCTSTR lpszText,LPCTSTR lpszCaption=Null, UINT nType=MB_OK); lpszText .... Text der ausgegeben werden soll lpszCaption . Text für den Titel des Meldungsfensters nType ....... Kombination aus Konstanten, die angeben, welche Schalter und welches Symbol im Meldungsfenster angezeigt werden soll. Schalter MB_OK MB_OKCANCEL MB_RETRYCANCEL MB_YES MB_YESNO MB_YESNOCANCEL Symbole MB_ICONEXCAMATION MB_ICONINFORMATION MB_ICONQUESTION MB_ICONSTOP Abb.: Konstanten für den nType-Parameter Die MessageBox() ist eine Methode der Fensterklasse CWnd und ist nur in Methoden von Fensterklassen verfügbar. Zur Anzeige von Meldungsfenstern aus den Methoden anderer Klassen verwendet man die globale Funktion AfxMessageBox(). int AfxMessageBox(LPCTSTR lpszText,UINT nZype = MB_OK,UINT nIDHelp=0) Der Rückgabewert der Methoden zeigt an, welcher Schalter zum Schließen des Meldungsfensters gedrückt wurden. Rückgabewert IDABORT IDCANCEL IDIGNORE IDNO IDOK Zeigt an: Die Abbrechen-Schaltfläche wurde gedrückt Die Abbrechen-Schaltfläche wurde gedrückt Die Ignorieren-Schaltfläche wurde gedrückt Die Nein-Schaltfläche wurde gedrückt Die OK-Schaltfläche wurde gedrückt 338 Die Programmiersprache C++ IDRETRY IDYES Die Wiederholen-Schaltfläche wurde gedrückt Die Ja-Schaltfläche wurde gedrückt. Abb.: Die Funktion AfxMessageBox() ist keiner MFC-Klasse zugeordnet. Dies kann man ausnutzen und ein Programm erstellen, das mit weniger als 10 Zeilen Quellcode ein Fenster (zählt man den OK-Button mit, sind es sogar zwei) ausgibt: #include <afxwin.h> class CMeineAnwendung : public CWinApp { virtual BOOL InitInstance() { AfxMessageBox("Hallo Welt !!!"); return TRUE; } }; CMeineAnwendung meineAnwendung; 2. Text zeichnen (in ein Ansichts- oder ein Rahmenfenster) Falls man einen passenden Gerätekontext zu einem Fenster beschafft hat, stehen zwei Gerätekontextmethoden für die Textausgabe zur Verfügung. CDC::TextOut() CDC::DrawText() 3. Steuerelemente Steuerelemente können für die Ein- und Ausgabe für Text verwendet werden. Die grundlegende Funktionalität des Steuerelements ist bereits in Windows implementiert. CStatic-Textfeld. CEdit-Eingabefeld CRichEdit-Eingabefeld 4. Spezielle Ansichtsklassen Die MFC stellt zwei abgeleitete View-Klassen bereit, die das Erscheinungsbild und Verhalten eines normalen Ansichtsfensters mit der Funktionalität eines Eingabesteuerelements verbinden. 339 Die Programmiersprache C++ CEditView CRichEditView 5. Die Klasse Klassen CString (MFC-Stringklasse) Sie erleichtert die Handhabung von Zeichenketten, insbesondere die damit verbundene dynamische Speicherverwaltung. Sie soll die alten C-Zeichenarrays ersetzen und bietet mächtige und flexible CString-Operationen. Sammlungen (Container) von Strings können in den MFC-Klassen CStringArray oder CStringList verwaltet werden. 6.2.3.2 Dateibearbeitung Die MFC-Bibliothek stellt die Klasse CFile für Dateizugriffe bereit. CFile unterstützt nur binäre, ungepufferte Operationen. Die abgeleitete Klasse CStudioFile ermöglicht gepuffertes Lesen und Schreiben von Binär- und Textdateien. Alterantiv können die ANSI-C++-Datenstreamklassen verwendet werden. In C/C++ benutzt man für das Speichern bzw. Laden aus Dateien die in den Standardbibliotheken definierten Behandlungsfunktionen (fopen(), fprintf(), usw.) oder Streams (fstream). Diese besitzen auch in Windows-Programmen ihre Gültigkeit. Windows stellt aber auch eigene Funktionen für die Arbeit mit Dateien zur Verfügung. In der MFC sind diese in der Klasse CFile gekapselt. CFile ist die Basisklasse für MFC-Dateidienste. CFile unterstützt das Lesen und Schreiben in (binäre) Dateien: Dateien öffnen. Das Öffnen kann mit Hilfe der folgenden Konstruktoren erfolgen: CFile(int hFile); CFile(LPCTSTR lpszFileName,UINT nOpenFlags); Konstante CFile::modeCreate Bedeutung Datei wird bei Bedarf neu angelegt. Der Inhalt bestehender Dateien wird beim Öffnen gelöscht. CFile::modeRead Die Datei kann nur gelesen werden CFile::modeReadWrite Die Datei wird zum Lesen und Schreiben geöffnet CFile::modeWrite Die Datei wird nur zum Schreiben geöffnet Abb.: Einige Konstanten für den nOpenFlags-Parameter Alternativ kann 1. der Konstruktor CFile() verwendetwerden, dem keine Parameter übergeben werden. 2. danach die Methode Open() aufgerufen werden. Schließen von Dateien. Zum Schließen von Dateien wird die Methode Close() aufgerufen. Lesen und Schreiben in ein CFile-Objekt. Es wird mit den Methoden Read() und Write() ausgefüht: 340 Die Programmiersprache C++ virtual UINT Read(void* lpBuf, UINT nCount); lpBuf … bezeichnet einen Speicherbereich (Puffer), in den die Daten eingelesen werden. nCount ... ist die Anzahl Bytes, die maximal eingelesen werden. virtual void Write(const void* lpBuf, UINT nCount); lpBuf … nCount ... ist ein Zeiger auf den Speicherbereich, in dem die auszugebenden Daten stehen. ist die Anzahl Bytes, die ausgegeben werden. Fehlerbehandlung. Einige CFile-Methoden (z.B. Open()) zeigen aufgetretene Fehler in ihren Rückgabewerten an. Andere Funktionen lösen eine Ausnahme vom Typ CFileException aus. 6.2.3.3 Serialisierung Als Serialisierung bezeichnet man das Lesen und Schreiben von Objekten (genauer: CObject-Objekten). CObject ist die oberste Basisklasse der MFC-Klassen. Alle Objekte von Klassen (MFC-Klassen und vom Programmierer selbst definierte Klassen) können mit Hilfe der Serialisierung in Dateien geschrieben oder aus Dateien gelesen werden. Zum Schreiben von Objekten in CFile-Dateien per Serialisierung braucht man einen Mittler: die CArchive-Klasse. Diese Klasse dient als Eingabe- bzw. AusgabeStream für ein CFile-Objekt. 6.2.3.3 CTime CTime-Klassenobjekte geben das absolute Datum und die Uhrzeit an. Hierbei kapselt sie den ANSI-Datentyp time_t und seine verwandten Laufzeitfunktionen, einschl. der Fähigkeit in die gregorianische Zeitrechnung sowie 24-StundenAnzeigen umzurechnen. CTime-Werte basieren auf UCT (Universal Coordinate Time oder Greenwich Mean Time), wobei die lokale Zeitzone durch die TZUmgebungsvariable identifiziert wird. 341 Die Programmiersprache C++ 6.3 Die Kernkomponenten 6.3.1 Aufbau von Dialogen aus Steuerelementen Zu Windows gehören verschiedene Standardsteuerelemente, z.B. Schaltflächen, Schieberegler, Kontrollkästchen (Check Box), Kombinationsfeld (Combo Box) auch als Drop-Down-Listenfeld bezeichnet Zur Aufnahme von Steuerelementen (Controls) in einem Rahmen oder Ansichtsfenster verweigert die IDE jegliche Unterstützung. Steuerelemente müssen manuell konfiguriert werden und durch Angaben von Koordinaten im Quelltext plaziert werden. Steuerelemente sind Windows-Fenster. Zahlreiche Steuerelemente sind in Windows vordefiniert und als Teil des Betriebssystems implementiert. Zu diesen StandardSteuerelementen zählen: Das statische Textfeld Das Eingabefeld Die Schaltfläche Die Kontrollkästchen und Optionsfelder Die Listen- und Kombinationsfelder Die MFC stellt Klassen bereit, die diese Steuerelemente kapseln. Für alle wichtigen Arbeiten im Umgang mit Steuerelementen sind in diesen Klassen passende Methoden definiert. Eigenschaften - - Steuerelemente (Controls) haben jeweils eigene Fenster Controls werden an einer angegebenen Position mit rechteckiger Bounding-Box erzeugt. Controls erhalten bei ihrer Erzeugung eine Reihe von Stilelementen, die ihr Aussehen und Verhalten bestimmen Controls haben eine eigene Ereignisbehandlungsmethode. - Nach einem Ereignis des Betriebssystems ändert das Control in der Regel seinen Zustand - Danach wird das Ereignis weitergemeldet. Controls sind Objekte von Klassen und haben dementsprechend eigene Methoden zum Lesen und Schreiben der Zustände. Fenster Die folgenden Stile gelten für fast alle Controls (, weil sie sich auf die Fensterklasse CWnd beziehen): WS_CHILD, WS_VISIBLE, WS_GROUP. Soll ein Steuerelement mehr als einen Stil erhalten, so werden alle Stile mit logischem Oder "|" verknüpft. Einige wichtige Methoden der Basisklasse CWnd sind: void SetWindowText(LPCSTR text) LPCSTR GetWindowText() const schreibt bzw. liest Beschriftung (z.B. für Buttons, TextFelder, Labels, ...) void setFont(CFont* font, BOOL hRedraw = TRUE) CFont* GetFont() const schreibt bzw. liest den Zeichensatz, mit dem die Beschriftung dargestellt wird. Manuelle Erzeugung von Steuerelementen - Ein Objekt wird als Attribut der entsprechenden Dialogklasse angelegt, z.B. CButton okButton 342 Die Programmiersprache C++ - Während der Konstruktion des Dialogs (z.B. im Konstruktor der Dialogklasse) wird die Methode Ctreate des Controls zur Konfigurierung aufgerufen: okButton.Create("OK",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(CPoint(10,10),CSize(125,5)),this,IDC_BUTTON_OK) - Die Controls werdem in absoluten Koordinaten innerhalb des Clientbereichs positioniert. Jedes Control erhält eine eindeutige Identifikationsnummer (ID). Die ID wird in der Regel in der Header-Datei als Präprozessor-Direktive festgelegt. Aufbau der ID-Namen (alle Teile sind mit einem Unterstrich _ verbunden. -- IDC -- Typ des Controls, z.B. BTN für Button, STC für Static -- Bezeichnung, die beim Entwickler eine gewisse Assoziation mit der Bedeutung herstellt, z.B. IDC_BTN_OK. Ereignisbehandlung - Da ein Control seine Ereignisse an sein Vaterobjekt weiterleitet, werden die für den Anwender interessanten Ereignisse dort behandelt. Deklaration eines entsprechenden Makros für den Ereignistyp in der Message-Map. -- Makro-Name: Ereignistyp -- 1. Parameter: ID des Controls -- 2. Parameter: Aufzurufende Methode Bsp.: Eine MFC-Anwendung mit manueller Erstellung von Steuerelementen 176. Die Anwendung soll folgendes Dialogfenster erzeugen und je nach Button-Aktivierung einen entsprechenen Strich in das Fenster zeichnen: 1. Erstellen leeres Projekt - Über Datei | Neu | Projekte "Win32-Anwendung" markieren Angabe des Projektnamens Erstellen eines leeren Projekts 2. Einfügen einer neuen Header-Datei "MeineAnwendung.h" // Header-Datei des Hauptfensters #include <afxwin.h> class CMeineAnwendung : public CWinApp { public: virtual BOOL InitInstance(); }; class CHauptfenster : public CFrameWnd { CButton btnBlack; CButton btnRed; CButton btnChkDotted; CPen penRed; CPen penRedDotted; 176 vgl. pr63210 343 Die Programmiersprache C++ CPen penBlackDotted; char aktFarbe; // R = rot, B = schwarz public: CHauptfenster(); // Ereignisse afx_msg void OnPaint(); // Control-Ereignisse afx_msg void OnClickBlack(); afx_msg void OnClickRed(); afx_msg void OnClickDotted(); DECLARE_MESSAGE_MAP() }; 3. Einfügen einer neuen Quellcode-Datei "MeineAnwendung.cpp" # include "MeineAnwendung.h" // Erzeuge Anwendungsobjekt static CMeineAnwendung meineAnwendung; // BOOL CMeineAnwendung :: InitInstance() { m_pMainWnd = new CHauptfenster; m_pMainWnd->ShowWindow(m_nCmdShow); return TRUE; } /* Implementierung des Hauptfensters */ // Control-IDs #define IDC_BTN_BLACK 199 #define IDC_BTN_RED 200 #define IDC_BTN_DOTTED 201 // Message-Map BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd) ON_WM_PAINT() // Beim Click auf den zugehoerigen Button // wird diese Funktion ausgefuehrt ON_BN_CLICKED(IDC_BTN_BLACK,OnClickBlack) ON_BN_CLICKED(IDC_BTN_RED,OnClickRed) ON_BN_CLICKED(IDC_BTN_DOTTED,OnClickDotted) END_MESSAGE_MAP() /* Erzeugen des Hauptfensters und der Controls */ CHauptfenster :: CHauptfenster() { // 1. Schritt: Erzeuge das Hauptfenster Create(NULL,"Push & Check Schaltflaechen", WS_OVERLAPPEDWINDOW,CRect(0,0,300,140)); // 2. Schritt: Erzeuge die Fenster zu den Steuerelementen btnBlack.Create("Schwarze Linie", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, CRect(CPoint(5,5),CSize(125,25)),this,IDC_BTN_BLACK); btnRed.Create("Rote Linie", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(CPoint(5,35),CSize(125,25)),this,IDC_BTN_RED); btnChkDotted.Create("Dotted Linie", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, CRect(CPoint(5,65),CSize(125,25)),this,IDC_BTN_DOTTED); // 3. Schritt: Erzeuge Pens penRed.CreatePen(PS_SOLID,1,RGB(255,0,0)); penRedDotted.CreatePen(PS_DOT,1,RGB(255,0,0)); penBlackDotted.CreatePen(PS_DOT,1,RGB(0,0,0)); // 4. Schritt: Dateninitialisierung aktFarbe = 'B'; } 344 Die Programmiersprache C++ // Einer der beiden Pushbuttons wurde gedrueckt void CHauptfenster :: OnClickBlack() { aktFarbe = 'B'; Invalidate(); } void CHauptfenster :: OnClickRed() { aktFarbe = 'R'; Invalidate(); } // Neuzeichnen nach Ereignis, gestartet durch Invalidate() void CHauptfenster :: OnPaint() { CPaintDC dc(this); if (aktFarbe == 'R') { if (btnChkDotted.GetCheck() == BST_UNCHECKED) dc.SelectObject(&penRed); else dc.SelectObject(&penRedDotted); } else { if (btnChkDotted.GetCheck() == BST_UNCHECKED) dc.SelectStockObject(BLACK_PEN); else dc.SelectObject(&penBlackDotted); } dc.MoveTo(CPoint(150,10)); dc.LineTo(CPoint(240,80)); } // Checkbox gedrueckt: Neuzeichnen void CHauptfenster :: OnClickDotted() { Invalidate(); } 6.3.2 Die wichtigsten Steuerelemente Schaltflächen (Buttons) Eine Schaltfläche (Button) kann verschiedene Ausprägungen haben: Checkbox, PushButton, Radio-Button, Groupbox. PushButton Über einen PushButton löst der Benutzer eine bestimmte Aktion aus. Die Beschriftung (Caption) der Schaltfläche sollte einen Hinweis auf die Aktion liefern, die beim Klicken auf die Schaltfläche ausgeführt wird. Die wichtigsten Eigenschaften, Stile und Nachrichten für den PushButton sind: Eigenschaft ID Beschriftung Sichtbar (Visible) Deaktiviert (Disabled) Default Button Beschreibung identifiziert das Steuerelement gibt den auf der Schaltfläche angezeigten Text an gibt an, ob das Steuerelement sichtbar ist, wenn die Anwendung läuft. gibt an, dass das Steuerelement deaktiviert ist. gibt an, dass dieses Steuerelement ausgelöst werden soll, wenn der Benutzer die Eingabetaste betätigt. 345 Die Programmiersprache C++ Tabstopp gibt an, ob Benutzer das Steuerelment beim Navigieren mit der tabulatortaste erreichen können. Abb. Wichtige Eigenschaften des Steuerelements PushButtom Stile BS_PUSHBUTTON BS_DEFPUSHBUTTON Nachrichten ON_BN_CLICKED ON_BN_DOUBLECLICKED Abb.: Stile und Nachrichten des Pushbutton CButton. Das Steuerelement PushButton ist in der MFC-KLasse CButton verkapselt. Die Klasse ist ein Abkömmling der Klasse CWnd und verkapselt nicht nur PushButton sondern auch das Kontrollkästchen und das Optionsfeld. Checkbox Stile BS_CHECKBOX BS_AUTOCHECKBOX BS_3STATE BS_AUTO3STATE BS_LEFTTEXT Nachrichten ON_BN_CLICKED ON_BN_DOUBLECLICKED Radio Button Stile BS_RADIOBUTTON BS_AUTORADIOBUTTON - Nachrichten ON_BN_CLICKED ON_BN_DOUBLECLICKED Radio Buttons werden (in der Regel) zu einer Gruppe zusammengefasst. Nur ein Button der Gruppe kann selektiert werden In der MFC müssen Buttons einer Gruppe direkt aufeinanderfolgende Identifikationsnummern besitzen Das erste Control der Gruppe muß den Stil WS_GROUP gesetzt haben Dabei darf es sich auch um eine Group-Box handeln Es gibt zwei wesentliche Methoden zum Umgang mit gruppierten Buttons: -- CheckRadioButton(UINT FirstID, UINT LastID,UINT ToBeChecked) Der Button mit der ID ToBeChecked wird ausgewählt, alle anderen in dem angegebenen Bereich deselektiert. -- UINT CtrlID = GetCheckRadioButton(UINT FirstCtrl, UINT LastCtrl) Es wird die ID des selektierten Buttons eines angegebenen Bereichs ausgelesen Groupbox Stile BS_GROUPBOX Nachrichten Statische Textfelder (Static) Static ist ein Label, das Text oder ein Icon beinhalten kann. Stile SS_SUNKEN SS_CENTER SS_LEFT Nachrichten ON_STN_CLICKED ON_STN_DBLCLICK ON_STN_ENABLE 346 Die Programmiersprache C++ SS_RIGHT SS_NOTIFY SS_ICON ON_STN_DISABLE Ein- oder mehrzeiliges Texteingabefeld Stile WS_BORDER ES_LEFT ES_CENTER ES_AUTOSCROLL ES_MULTILINE Nachrichten EN_SETFOCUS EB_KILLFOCUS EN_CHANGE Bsp.: Ereignisse in TextEingabefeldern Die Anwendung soll folgendes Dialogfenster erzeugen und nach Eingaben in den Eingabetextfeldern eine Berechnung ausführen. 1. Erstellen leeres Projekt - Über Datei | Neu | Projekte "Win32-Anwendung" markieren Angabe des Projektnamens Erstellen eines leeren Projekts 2. Einfügen einer neuen Header-Datei "MeineAnwendung.h" #include <afxwin.h> class CMeineAnwendung : public CWinApp { public: virtual BOOL InitInstance(); }; class CHauptfenster : public CFrameWnd { CStatic stcBreite; CEdit edtBreite; CStatic stcLaenge; CEdit edtLaenge; CStatic stcFlaeche;; CStatic stcResult; CButton btnCalc; void Recalculate(); public: CHauptfenster(); // Control-Ereignisse afx_msg void OnClickCalc(); afx_msg void OnLaengeFocus(); afx_msg void OnLaengeKillFocus(); afx_msg void OnBreiteChange(); DECLARE_MESSAGE_MAP() 347 Die Programmiersprache C++ }; 3. Einfügen einer neuen Quellcode-Datei "MeineAnwendung.cpp" # include "MeineAnwendung.h" // Erzeuge Anwendungsobjekt static CMeineAnwendung meineAnwendung; // BOOL CMeineAnwendung :: InitInstance() { m_pMainWnd = new CHauptfenster; m_pMainWnd->ShowWindow(m_nCmdShow); return TRUE; } /* Implementierung des Hauptfensters */ // Control-IDs #define IDC_BTN_CALC 300 #define IDC_STC_BREITE 310 #define IDC_EDT_BREITE 311 #define IDC_STC_LAENGE 320 #define IDC_EDT_LAENGE 321 #define IDC_STC_FLAECHE 330 #define IDC_STC_RESULT 331 // Message-Map BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd) ON_BN_CLICKED(IDC_BTN_CALC,OnClickCalc) ON_EN_SETFOCUS(IDC_EDT_LAENGE,OnLaengeFocus) ON_EN_KILLFOCUS(IDC_EDT_LAENGE,OnLaengeKillFocus) ON_EN_CHANGE(IDC_EDT_BREITE,OnBreiteChange) END_MESSAGE_MAP() /* Erzeugen des Hauptfensters und der Controls */ CHauptfenster :: CHauptfenster() { // 1. Schritt: Erzeuge das Hauptfenster Create(NULL,"Static & Edit Controls", WS_OVERLAPPEDWINDOW,CRect(0,0,300,140)); // 2. Schritt: Erzeuge static controls stcLaenge.Create("Laenge", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(CPoint(10,10),CSize(60,25)),this,IDC_STC_LAENGE); stcBreite.Create("Breite", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(CPoint(80,10),CSize(60,25)),this,IDC_STC_BREITE); stcFlaeche.Create("Flaeche", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(CPoint(150,10),CSize(120,25)),this,IDC_STC_FLAECHE); stcResult.Create("", WS_CHILD | WS_VISIBLE | SS_SUNKEN | SS_CENTER, CRect(CPoint(150,40),CSize(120,25)),this,IDC_STC_RESULT); // 3. Schritt: Erzeuge edit und andere controls edtLaenge.Create(WS_CHILD | WS_VISIBLE | WS_BORDER, CRect(CPoint(10,40),CSize(60,25)),this,IDC_EDT_LAENGE); edtBreite.Create(WS_CHILD | WS_VISIBLE | WS_BORDER, CRect(CPoint(80,40),CSize(60,25)),this,IDC_EDT_BREITE); btnCalc.Create("Calc Area", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 348 Die Programmiersprache C++ CRect(CPoint(60,70),CSize(100,25)),this,IDC_BTN_CALC); edtLaenge.SetFocus(); } // Interne Methode zum erneuten Rechnen und Ausgeben von Werten void CHauptfenster :: Recalculate() { CString str; edtLaenge.GetWindowText(str); double laenge = atof(str); edtBreite.GetWindowText(str); double breite = atof(str); double flaeche = laenge * breite; str.Format("%f",flaeche); stcResult.SetWindowText(str); } // Ereignisbehandlung void CHauptfenster :: OnLaengeFocus() { edtLaenge.SetWindowText(""); stcResult.SetWindowText(""); } void CHauptfenster :: OnLaengeKillFocus() { CString str; edtLaenge.GetWindowText(str); if (atof(str) < 0) { MessageBox("Unzulaessiger Eintrag"); edtLaenge.SetWindowText(""); edtLaenge.SetFocus(); } } void CHauptfenster :: OnClickCalc() { Recalculate(); } void CHauptfenster :: OnBreiteChange() { Recalculate(); } 349 Die Programmiersprache C++ 6.4 Erstellen von Dialogen über Ressourcen 6.4.1 Ressourcen Ressourcenkonzept Die Grundidee des Ressourcenkonzepts ist die Auslagerung bestimmter Elemente der grafischen Benutzeroberfläche (Dialoge, Menüs, Bitmaps, etc.). Auslagerung bedeutet: Die Elemente werden nicht im Quelltext, sondern in Dateien definiert. Die Verbindung von C++-Quelltextdatei zu einer externen Ressourcendatei ist die Ressourcen-ID. Ressourcenskriptdatei Ressourcen werden in sog. Ressourcenskriptdateien definiert (Extension .rc). bei diesen Dateien handelt es sich um einfache Textdateien, in denen die verschiedenen Ressourcen nach eigenen Syntayregeln definiert werden. RES-Datei Ressourcenskriptdateien können nicht vom C-Compiler übersetzt werden, sie bedürfen eines eigenen Ressourcen-Compilers177. Dieser erzeugt aus der Ressourcenskriptdatei eine binäre .res-Datei. Ressourcenskriptdateien in einem Projekt werden über die Befehle im Menü ERSTELLEN beim Kompilieren durch automatischen Aufruf des Ressourcen-Compilers in eine .res-Datei übersetzt. Bei der Projekterstellung wird diese binäre Ressourcendatei im letzten Schritt des Linkers mit den Objektdateien des C++-Quelltextes zur ausführbaren EXE-Datei zusammengebunden. Ressourcen-IDs Zu jeder Ressourcendefinition gehört die Angabe einer Ressourcen-ID. IDs sollten eindeutig sein, da mit ihrer Hilfe die einzelnen Ressourcen vom Programm aus angesprochen werden. Diese Ressourcen-IDs müssen dem C++-Compiler bekannt gemacht werden. Dazu müssen die einzelnen Ressourcen-IDs mit Hilfe von #define-Direktiven mit IntegerKonstanten verbunden werden. Am sinnvollsten geschieht das mit einer eigenen Header-Datei178, denn diese Header-Datei muß in die Ressourcenskriptdatei und in die Quelldateien, die Ressourcen verwenden, über die #include-Direktive aufgenommen werden. Die Ressourcenmethoden 177 178 rc.exe Der MFC-Assiistent nennt diese Header-Datei standardmäßig resource.h 350 Die Programmiersprache C++ Der letzte Schritt besteht darin, die Ressourcen zur Anzeige und zum Arbeiten mit der Ressource zu laden. Dazu stehen verschieden API-Funktionen und MFCMethoden bereit. Ressourcen anlegen Der grundlegende Ablauf zum Anlegen und Bearbeiten von Ressourcen ist 1. 2. 3. 4. Eine neue Ressource anlegen. Ressource bearbeiten Ressource im Programm verwenden Ressourcen kompilieren Die verschiedenen Ressourcen-Arten Dialogfelder Tastaturkürzel Bitmaps Sie können mit dem Grafik-Editor erstellt werden. Mauszeiger Symbole (Icons) Symbole sind Bitmaps, die mit dem Grafik-Editor bearbeitet werden. Symbolleisten Stringtabellen Hier werden einzelne Strings mit IDs in Verbindung gebracht. Ressourcen-Editor Mit dem Ressourcen-Editor können Dialogseiten gestaltet werden, Menüs erzeugt werden und Symbolflächen, Icons gezeichnet werden. Der Ressource-Editor kann aufgerufen werden, in dem im Arbeitsbereich (Workspace) die Symbolfläche Ressource View aktiviert wird. Zum Erzeugen und anschließenden Gestalten einer neuen Dialogseite geht man zur Ressource DIALOG und wählt mit der rechten Maustaste den Befehl DIALOG EINFÜGEN aus. Der Ressource-Editor erzeugt daraufhin einen neue Dialogseite mit den Windows-Steuerelementen. Durch Klick auf ein bestimmtes Steuerelement in der Werkzeugleiste kann anschließend das Steuerelement auf der Dialogseite positioniert werden. Falls neue Menüpunkte entworfen werden sollen, geht man im Ressource Editor zum Ressource Menü. Je nach Anwendungstyp stehen eine oder zwei Menüleisten zur Verfügung. Mit einem Doppelklick auf die geeignete Ressource ruft man den Menü-Editor auf. Auf der Menüleiste findet man ein leeres Menü in Form eines leeren Käschchens. Mit der linken Maustaste kann man das leere Menü an die 351 Die Programmiersprache C++ richtige Stelle positionieren. Außerdem befinden sich auf der Menüleiste eine Vielzahl weitere Menübefehle, die für Anwendungen genutzt werden können. Analog zum Menü-Editor kann auch mit dem Toolbar-Editor gearbeitet werden. Falls eigenene Schaltflächen entworfen werden sollen, ist der Toolbar-Editor aufzurufen. Auf der Leiste des Toolbar befinden sich die gemalten Schaltflächen, die der AppWizzard erzeugt hat. Die Symbolschaltflächen können übermalt werden. Der Code-Assistent179 ist das Werkzeug das den mit Hilfe des Ressouce Editor erstellten Steuerelementen die gewünschte Funktionalität verleiht. 6.4.2 Erstellen von Dialogen und Menüs aus Ressourcen Dialoge und / oder Menüs sollen aus Ressourcen generiert und mit der Anwendungslogik verknüpft werden. Eine Ressource ist eine textuelle Beschreibung eines Dialogs oder eine Menüs. 1. Dialoge Grundlagen Die zugehörige Ressourcen-Datei kann manuell oder interaktiv (mit einem grafischen Editor) erstellt werden. Erzeugen eines Ressource-Scripts. - Projekt mit der rechten Maustaste auswählen "Hinzufügen" und dann "Hinzufügen Ressource" auswählen In der Ressourcen-Ansicht eines Projekts kann dann mit einen Klick mit der rechten Maustaste über "Hinzufügen" die Resource für einen Dialog erstellt werden. Mit "Neu" wird danach der Dialogeditor mit einem zunächst leeren Dialog eröffnet. Neben dem Resssource-Script wird aud die Datei resource.h generiert. Diese Datei enthält für alle Bezeichner eine Integer-Zahl, da alle Symbolnamen (Dialognamen, Dialogfeldnamen, etc.) auf ganzzahligen Konstanten abgebildet werden. Dies wird durch Präprozessor-Direktiven realisiert. Es gilt das folgende Namensschema: - IDC_NAME: - IDD_NAME: - ID_NAME: - IDR_NAME: 'C' für Control-Namen 'D' für Dialog-Namen ohne dritten Buchstaben für Menü-Namen 'R' für die komplette Menü-Ressource Zur Überprüfung der Bezeichner gibt es zwei Möglichkeiten: - 179 Öffnen der Datei resource.h als Textdatei Anzeige der Symbolnamen zusammen mit ihren Werten in einem Dialog -- Auswahl des Ressorcen-Scripts in einer Resourcen-Ansicht mit der rechten Maustaste -- Auswahl des Menüpunkts "Resource Symbols ..." -- In dem dann erscheinenden Dialog können Symbole gelöscht und verändert werden. vgl. 6.4.1.2 352 Die Programmiersprache C++ Zum Datenaustausch zwischen Anwendung und Oberfläche dient als Basisklasse die Klasse CDialog, d.h.: Eine eigene Klasse erbt von CDialog (zur Übernahme der Grundfunktionalität). CDialog besitzt nämlich eine Tabelle zur Behandlung von Ereignissen. Die wichtigsten unterstützenden Ereignisse sind: - - OnInitDialog - Aufruf: Beim Initialisieren des Dialogs - Aufgabe: Eintragen von Anwendungsdaten in Dialogfelder OnOK - Aufruf: Falls der Dialog mit OK beendet wurde - Aufgabe: Auslesen der Dialogfelder und Übernahme der Daten in die Anwendung. Der Datenaustausch zwischen Anwendung und Dialog erfolgt folgendermaßen: - - Die Anwendung schreibt Daten in die Attribute der Dialog-Klasse Die Dialogklasse schreibt die Daten während des Ereignisses OnInitDialog in die Steuerelemente des Dialogs (Zugriff mit evtl. automatischer Typkonvertierung): SetDlgItemText: String-Wert im Steuerelement eintragen (Control) SetDlgItemInt: Integer-Wert in Control eintragen Falls der Dialog mit OK beendet wird, wird die Ereignis-Methode OnOK() aufgerufen. Die Dialogklasse liest die Steuerelemente aus und speichert die Resultate in den eigenen Attributen (Zugriffe wie beim Schreiben, Methodennamen fangen mit Get an). Die Anwendung liest die Attribute aus. Bsp.180: Es ist eine dialogbasierte Anwendung zu erstellen, die über eine Dialogbox Datenaustausch ermöglicht. Aufgabenstellung: Über das Menü (ShowDialog) soll eine Dialogbox aufgerufen werden Die Dialogbox hat folgendes Aussehen: 180 pr64230 353 Die Programmiersprache C++ Mit dem vertikal angeordneten Schieberegler (der Klasse CSliderCtrl) kann ein Wert eingestellt werden, der in dem daneben stehenden Eingabe-Textfeld angezeigt wird, z.B.: Beim Betätigen der OK-Schaltfläche wird im Hauptfenster die Einstellung protokolliert: Wird die Schaltfläche Abbrechen betätigt, dann nimmt das Hauptfenster folgende Gestalt an: Lösungsschritte: 1. Erstellen der zur Anwendung bzw. zum Hauptfenster gehörigen Klassen einschl. der zum Hauptfenster zugehörigen Menü-Ressource Dekaration der Klasse CMeineAnwendung: MeineAnwendung.h #include <afxwin.h> #include "resource.h" class CMeineAnwendung : public CWinApp { public: 354 Die Programmiersprache C++ virtual BOOL InitInstance(); }; Methoden der Klasse CMeineAnwendung: MeineAnwendung.cpp #include "MeineAnwendung.h" #include "Hauptfenster.h" #include "MeinDialog.h" static CMeineAnwendung meineAnwendung; BOOL CMeineAnwendung::InitInstance() { m_pMainWnd = new CHauptfenster; m_pMainWnd ->ShowWindow(m_nCmdShow); return TRUE; } Deklaration der Klasse CHauptfenster: Hauptfenster.h #include <afxwin.h> class CHauptfenster : public CFrameWnd { private: int mWert; public: CHauptfenster(); afx_msg void ShowDialog(); DECLARE_MESSAGE_MAP() }; Methoden der Klasse CHauptfenster: Hauptfenster.cpp #include "Hauptfenster.h" #include "MeinDialog.h" BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd) ON_COMMAND(ID_SHOWDIALOG,ShowDialog) END_MESSAGE_MAP() CHauptfenster::CHauptfenster() { Create(NULL,"Dialogkommunikation", WS_OVERLAPPEDWINDOW,CRect(0,0,360,200),NULL, MAKEINTRESOURCE(IDR_MENU1)); mWert = 0; } void CHauptfenster::ShowDialog() { CClientDC dc(this); CMeinDialog dlg(this); // Erzeuge Dialogobjekt (1. Schritt) dlg.wert = mWert; // Initialisiere Dialogvariable if (dlg.DoModal() == IDOK) { mWert = dlg.wert; CString s; s.Format("Eingestellter Wert: %d", mWert); dc.TextOut(10,10,s); } else { dc.TextOut(10,10,"Abbrechen wurde gedrueckt"); } } 355 Die Programmiersprache C++ Menüs sind in Visual C++-Anwendungen als Ressourcen definiert. Man kann sie daher im Editor von Visual C++ über RESSOURCENANSICHT im Arbeitsbereich entwerfen. 2. Erstellen der zur Dialog-Box zugehörigen Ressource mit dem Ressourcen-Editor. 356 Die Programmiersprache C++ In der Dialog-Box wird ein CSliderCtrl zur Einstellung eines Datenwerts verwendet. Der CSliderCtrl wird vertikal eingesetzt und sendet WM_VSCROLL-Nachrichten bei jeder Positionsänderung. Die Nachrichtentabelle der Klasse CMeinDialog sieht damit so aus: BEGIN_MESSAGE_MAP(CMeinDialog,CDialog) ON_WM_VSCROLL() END_MESSAGE_MAP() Die zugehörige Methode ist OnVScroll. Die Dialogklasse CMeinDialog bearbeitet dieses Ereignis. 3. Erstellen der Dialog-Box zugehörigen Klasse CMeinDialog Deklaration der Klasse CMeinDialog: MeinDialog.h #include <afxwin.h> #include "resource.h" class CMeinDialog : public CDialog { public: int wert; CMeinDialog(CWnd* pParentWnd); virtual ~CMeinDialog(); virtual BOOL OnInitDialog(); virtual void OnOK(); afx_msg void OnVScroll(UINT nSBCode,UINT nPos, CScrollBar* pScrollBar); DECLARE_MESSAGE_MAP() }; Methoden der Klasse CMeinDialog: MeinDialog.cpp #include <afxcmn.h> #include "MeinDialog.h" 357 Die Programmiersprache C++ BEGIN_MESSAGE_MAP(CMeinDialog,CDialog) ON_WM_VSCROLL() END_MESSAGE_MAP() CMeinDialog::CMeinDialog(CWnd* pParentWnd) : CDialog(IDD_DIALOG1,pParentWnd) { } CMeinDialog::~CMeinDialog() { } BOOL CMeinDialog::OnInitDialog() { SetDlgItemInt(IDC_WERT,wert); CSliderCtrl* pSlider; pSlider = (CSliderCtrl*) GetDlgItem(IDC_WERT_SLIDER); pSlider->SetRange(0,99); pSlider->SetPos(wert); pSlider->SetTicFreq(5); pSlider->SetPageSize(5); return TRUE; } void CMeinDialog::OnOK() { wert = GetDlgItemInt(IDC_WERT); CDialog::OnOK(); } void CMeinDialog::OnVScroll(UINT nSBCode,UINT nPos,CScrollBar* pScrollBar) { // Wenn der Slider ausgerichtet ist, // empfaengt diese Funktion die Nachricht // Zugriff auf das Steuerelement CSliderCtrl* pSlider; pSlider = (CSliderCtrl*) GetDlgItem(IDC_WERT_SLIDER); wert = pSlider->GetPos(); // Aendere den Wert SetDlgItemInt(IDC_WERT,wert); } 4. Arbeitsschritte nach dem Aktivieren des Menüpunkts ShowDialog Nach dem Aktivieren des Menüpunkts ShowDialog werden folgende Schritte (vgl. void CHauptfenster::ShowDialog() )durchlaufen: 1. CMeinDialog dlg(this); // Erzeuge Dialogobjekt (1. Schritt) - Ein Objekt der zum Dialog gehörenden Klasse wird erzeugt - this ist das Vaterobjekt 2. CDialog(IDD_RESSOURCE_ID,parent) - Der Dialog-Konstruktor wird mit der ID der Ressource sowie dem Vaterobjekt aufgerufen 3. dlg.wert = mWert; // Initialisiere Dialogvariable (2. Schritt) - Die Attribute der Dialogklasse werden mit Startwerten belegt 4. dlg.DoModal() - Der Dialog wird modal abgefragt 5. OnInitDialog() - DoModal() ruft diese Initialisierungsmethode der Dialogklasse auf - Dort wird mit SetDlgItemXXX der Startwert aus den Attributen in die Controls eingetragen 6. OnOK() - Aufruf dieser Methode, wenn der Benutzer den Dialog mit OK abgeschlossen hat. - Dort wird mit GetDlgItemXXX der Wert des Controls ausgelesen und in den Attributen gespeichert - Bei Auswahl des Abbruch-Buttons wird die Methode OnCancel aufgerufen. 7. if (dlg.DoModal() == IDOK) - Test, ob der Dialog mit OK abgeschlossen wurde 8. mWert = dlg.wert 358 Die Programmiersprache C++ - die gefüllten Attribute aus dem Objekt der Dialogklasse auslesen und in eigenen Attributwerten speichern. Reduktion des Datenautauschs bei Dialogen auf das Lesen und Schreiben von Variablen Es gibt ein Verfahren, das den Austausch auf das Lesen und Schreiben von Variablen reduziert. Zusätzlich können automatisch einfache Eingabeprüfungen vorgenommen werden. Der Austausch kann komplett interaktiv generiert werden. Funktionsweise im modalen Dialog - OnInitDialog ruft UpdateData(false) auf (-> Beschreiben des Dialogs) - Arbeit mit dem Dialog, bis OK oder Abbruch gewählt wurde - Vor Beendigung der Methode DoModal() ruft das Framework OnOK() auf, das seinerseits UpdateData(true) aufruft. (-> Auslesen des Dialogs) Funktionsweise im nicht-modalen Dialog - Die Anwendung oder OnInitDialog rufen UpdateData(false) auf (-> Beschreiben des Dialogs). - Arbeit mit dem Dialog - Die Anwendung ruft UpdateData(true) auf (-> Auslesen des Dialogs). UpdateData ruft die virtuelle Methode DoDataExchange auf. - Diese Methode muß implementiert werden - Die Methode kopiert die Daten (für beide Richtungen) - DoDataExchange kann die Gültigkeit der Daten prüfen und automatisch Fehlermeldungen ausgeben Bsp181. für den Aufbau der Methode DoDataExchange void CMeinDialog::DoDataExchange(CDataExchange* pDx) { CDialog::DoDataExchange(pDx); DDX_Text(pDx,IDC_MITTEILUNG,mitteilung); DDV_MaxChars(pDx,mitteilung,30); // Max. 30 Zeichen DDX_Text(pDx,IDC_TEMP,temp); DDV_MinMaxInt(pDx,temp,0,50); // Temperatur zwischen 0 und 50 } Die Funktionen DDX_???? kopieren Daten Die Funktionen DDV_???? überprüfen Daten. Datenaustausch. Für den Zugriff auf Controls existieren über 50 verschiedene (überladene Funktionen), z.B.: - DDX_Text - liest textuelle Inhalte aus Controls und trägt sie in verschiedene Variablentypen ein - der Zieldatentyp kann u.a. byte, short, int, UINT, long, DWORD, Cstring, float und double sein - der Text wird automatisch konvertiert - DDX_Check - greift auf den Wert einer Checkbox zu - erlaubte Werte sind: BST_CHECKED, BST_INDETERMINATE und BST_UNCHECKED - DDX_DataTimeCtrl - greift auf den Wert des Controls CDataTimeCtrl zu 181 pr64220 359 Die Programmiersprache C++ Datenüberprüfung. Im Fehlerfall wird automatisch ein Dialog angezeigt, und der Dialog nicht verlassen. Es gibt 12 verschiedene Funktionen zur Überprüfung der Eingabe, z.B.: - DDV_MinMaxInt Der Integer-Wert muß in einem bestimmten angegebenen Zahlenbereich liegen - DDV_MaxChars Die Eingabe im Feld darf eine Anzahl Zeichen nicht überschreiten - DDV_MinMaxDataTime Datum und Uhrzeit müssen in einem bestimmten Bereich liegen Bsp.182: Erstelle eine dialogbasierte Anwendung, die über eine Dialogbox Datenaustausch über DoDataExchange() ermöglicht Aufgabenstellung. Über das Menü (ShowDialog) des Hauptfensters soll eine Dialogbox aufgerufen werden. Die Dialogbox hat folgendes Aussehen: In das obere Editierfeld kann eine Mitteilung (z.B.: "Temperaturwert (Grad Celsius)") eingeschrieben werden, die Auskunft über den im zweiten Editierfeld angegebenen Wert gibt. Die Wertangabe darf nur Zahlenwerte von 0 bis 50 umfassen, der im ersten Editierfeld angegebene Text darf nicht mehr als 30 Zeichen umfassen. Wird der Zahlenbereich bei der Wertangabe verletzt, z.B. durch folgenden Eintrag in der Dialogbox 182 pr64220 360 Die Programmiersprache C++ , dann öffnet eine MessageBox folgendes Fenster: Mit einem Klick auf OK wird wieser zurück ins Hauptfenster verzweigt. Ist die Eingabe in der Dialogbox korrekt, dann erscheint nach Klick auf OK in der Dialogbox bspw. folgendes Hauptfenster: Wird in der Dialogbox Abbrechen gedrückt, dann wird folgende Mitteilung im Hauptfenster ausgegeben: Lösungsschritte 1. Erstellen der zur Anwendung bzw. zum Hauptfenster gehörigen Klassen einschl. der zum Hauptfenster zugehörigen Menü-Ressource. Es gibt keine wesentlichen Veränderungen zum letzten Beispiel. 361 Die Programmiersprache C++ 2. Erstellen der zur Dialog-Box zugehörigen Ressource mit dem Ressourcen-Editor. Es gibt keine wesentlichen Veränderungen zum letzten Beispiel. 3. Erstellen der Dialog-Box zugehörigen Klasse CMeinDialog Deklaration der Klasse CMeinDialog: MeinDialog.h #include <afxwin.h> #include "resource.h" class CMeinDialog : public CDialog { public: CString mitteilung; // fuer IDC_MITTEILUNG int temp; // fuer IDC_TEMP CMeinDialog(CWnd* pParentWnd); virtual ~CMeinDialog(); virtual void DoDataExchange(CDataExchange *pDx); }; Methoden der Klasse CMeinDialog: MeinDialog.cpp #include "MeinDialog.h" CMeinDialog::CMeinDialog(CWnd* pParentWnd) : CDialog(IDD_DIALOG1,pParentWnd) { mitteilung = _T(""); temp = 0; } CMeinDialog::~CMeinDialog() { } void CMeinDialog::DoDataExchange(CDataExchange* pDx) { CDialog::DoDataExchange(pDx); DDX_Text(pDx,IDC_MITTEILUNG,mitteilung); DDV_MaxChars(pDx,mitteilung,30); // Max. 30 Zeichen DDX_Text(pDx,IDC_TEMP,temp); DDV_MinMaxInt(pDx,temp,0,50); // Temperatur zwischen 0 und 50 } 4. Hinweis: Die Ausgabe der MessageBox erfordert folgende Einträge in der " .rc"-Datei: 3 TEXTINCLUDE BEGIN "\r\n" "#include ""l.deu\afxres.rc""" "\0" END und #ifndef APSTUDIO_INVOKED // #include "l.deu\afxres.rc" // #endif // not APSTUDIO_INVOKED 362 Die Programmiersprache C++ 2. Menüs Grundlagen Auch Menüs werden mit Hilfe des Resourcen Editor erstellt. Es gibt zwei Möglichkeiten ein Menü zu einem Fenster hinzuzufügen: 1. Ein Fenster wird durch die Klasse CFrameWnd implementiert. Im Aufruf des Fensters enthält der 6. Parameter eine Angabe zum Menü: Create(...,MAKEINTRESOURCE(IDR_MENU));. MAKEINTRESOURCE ist ein Makro, das eine Resource-ID in einen Resource-Typ für die Win32-API umwandelt. 2. Der 6. Parameter der Create-Methode ist NULL. Mit folgenden Schritten kann dann das Menü manuell erstellt werden: CMenu menu; menu.LoadMenu(IDR_MENU); SetMenu(&menu); menu.Detach(); Menüs werden mit Hilfe des Ressourcen-Editor erstellt, z.B. Intern wird eine Ressource-Datei erstellt, die das Menü bearbeitet: IDR_MENU1 MENU BEGIN POPUP "OptionA" BEGIN MENUITEM "A-1", MENUITEM "A-2", END POPUP "OptionB" BEGIN MENUITEM "B-1", ID_OPTIONA_A1 ID_OPTIONA_A2 ID_OPTIONB_B1 363 Die Programmiersprache C++ MENUITEM "B-2", MENUITEM "B-3", ID_OPTIONB_B2 ID_OPTIONB_B3 END MENUITEM "Clear", ID_CLEAR END Auch für Menüs werden Ereignis-Tabellen (Message-Maps) verwendet, um Ereignissen Methoden zuzuordnen: - Einfache Zuordnung eines Ereignisses zu einer Methode, z.B. ON_COMMAND(ID_OPTIONA_A1,DoOptionA1) -- Bei Auswahl des Menüpunkts mit der ID ID_Option_A1 wird der Methode void DoOptionA1() aufgerufen. - Zuordnung eines Bereichs von Menü-IDs zu einer methode für eine Auswahl von Menüs, z.B. ON_COMMAND_RANGE(ID_OPTIONB_B1,ID_OPTIONB_B3,OnOptionB) -- Die Menü-IDs müssen aufeinanderfolgende IDs besitzen. (Dies muß evtl. durch manuellen Eingriff sichergestellt werden). -- Bei Auswahl einer der Menüpunkte aus dem Bereich wird die Methode void DoOptionB(UINT nID) aufgerufen. Implementierung: Das erzeugte Menü soll zu der folgenden Anwendung erweitert werden: Wird bspw. OptionA aktiviert und Menüpunkt A2 aufgerufen, dann erscheint im Hautfenster ein Text. Zur Implementierung sind folgende Dateien aufzunehmen: MeineAnwendung.h #include <afxwin.h> class CMeineAnwendung : public CWinApp { public: virtual BOOL InitInstance(); }; MeineAnwendung.cpp #include "MeineAnwendung.h" #include "Hauptfenster.h" static CMeineAnwendung meineAnwendung; BOOL CMeineAnwendung::InitInstance() { m_pMainWnd = new CHauptfenster; m_pMainWnd ->ShowWindow(m_nCmdShow); return TRUE; 364 Die Programmiersprache C++ } Hauptfenster.h #include <afxwin.h> class CHauptfenster : public CFrameWnd { private: char* text; public: CHauptfenster(); afx_msg void OnPaint(); afx_msg void DoOptionA1(); afx_msg void DoOptionA2(); afx_msg void DoOptionB(UINT nID); afx_msg void DoClear(); DECLARE_MESSAGE_MAP() }; Hauptfenster.cpp #include "Hauptfenster.h" #include "resource.h" BEGIN_MESSAGE_MAP(CHauptfenster,CFrameWnd) ON_WM_PAINT() ON_COMMAND(ID_OPTIONA_A1,DoOptionA1) ON_COMMAND(ID_OPTIONA_A2,DoOptionA2) ON_COMMAND_RANGE(ID_OPTIONB_B1,ID_OPTIONB_B3,DoOptionB) ON_COMMAND(ID_CLEAR,DoClear) END_MESSAGE_MAP() CHauptfenster::CHauptfenster() { Create(NULL,"Erstes Menue", WS_OVERLAPPEDWINDOW,CRect(0,0,280,200),NULL, MAKEINTRESOURCE(IDR_MENU1)); text = NULL; } void CHauptfenster::DoOptionA1() { text = "Option A1"; Invalidate(); } void CHauptfenster::DoOptionA2() { text = "Option A2"; Invalidate(); } void CHauptfenster::DoOptionB(UINT nID) { switch(nID) { case ID_OPTIONB_B1: text = "Option B1"; break; case ID_OPTIONB_B2: text = "Option B2"; break; case ID_OPTIONB_B3: text = "Option B3"; break; } Invalidate(); } void CHauptfenster::DoClear() { text = NULL; Invalidate(); 365 Die Programmiersprache C++ } void CHauptfenster::OnPaint() { if (text != NULL) { CPaintDC dc(this); dc.TextOut(10,10,text); } } Zustandänderungen Menüs können Zustände darstellen. Es gibt zwei Ansätze dafür: Ein alter und neuer Ansatz. Ziel des neuen Ansatzes ist die Trennung der Darstellung durch 2 getrennte Ereignisse: 1. Das erste Ereignis wird ausgelöst, kurz bevor das Menü angezeigt werden soll. Hier wird der Zustand in das Menü eingetragen. 2. Das zweite Ereignis nach Auswahl des Menüs wird wie bisher ausgelöst. Vorgehensweise. - Der Entwickler verwaltet die Zustände der Menüpunkte selbst im Programm. - Trtt das neue zusätzliche Ereignis ON_WM_INITMENUPOPUP ein, dann ruft MFC eine Reihe von Methoden auf, falls diese in der Ereignistabelle eingetragen sind. - Diese Methoden tragen den jeweiligen Zustand in den Menüeintrag ein. Beispieleintrag in der Ereignistabelle: ON_UPDATE_COMMAND_UI(ID_OPTIONA_A1, DoUpdateOptionA2) Dadurch wird eine Methode mit folgender Signatur aufgerufen: void DoUpdateOptionA2(CCmdUI* pCmdUI); - Der Parameter CCmdUI ist ein Zeiger auf den Eintrag, der das Ereignis ausgelöst hat. - Der Eintrag kann ein Menü oder eine ähnliche Komponente sein (z.B. Toolbar-Button). - CCmdUI hat folgende Methoden zur Änderung des Zustands Enable(BOOL bFlag): Eintrag aktivieren oder deaktivieren SetCheck(UINT nState):BST_UNCHECKED(0), BST_CHECKED(1), BST_INTERMINATE(2) setRadion(BOOL): Setzt oder löscht die Marke. setText(char *): Ändert den Text des Eintrags. - Sollen mehrere Einträge mit benachbarten IDs gemeinsam behandelt werden, so können diese mit einem Ereignis bearbeitet werden: ON_UPDATE_COMMAND_UI_RANGE(ID_RANGE_START, ID_RANGE_END, DoUpdateOptionRange) 366 Die Programmiersprache C++ 6.4.3 Visuelle Erstellung Dialogfeldbasierende Anwendung 1. Erstelle ein neues MFC-Applikations-Visual-C++-Projekt 2. Erstelle den Anwendungsrahmen mit dem Anwendungs-Assistenten in folgenden Schritten: 1. Festelegen des Anwendungstyps "Auf Dialogfeldern basierend" Abb.: Festlegen des Anwendungstyps 3. Unter BENUTZEROBERFLÄCHENFEATURES (Mekmale für die Benutzerschnittstelle) Aussehen des Hauptfensters für die Anwendung festlegen: Ändern des Eintrags im Dialogfeldtitel in "1. MFC-Assistenten-Anwendung". 367 Die Programmiersprache C++ Abb. 3. Klick auf FERTIG STELLEN, damit der MFC-Anwendungs-Assistent den Anwendungsrahmen erstellen kann. 4. Der Arbeitsbereich zeigt nun: 368 Die Programmiersprache C++ Abb. Im Fensterbereich erscheint eine Standard-Dialog-Vorlage mit einem Textfeld, einem OK- und einem Abbrechen-Button, die verändert / ergänzt werden können. Hierzu steht eine Werkzeugkasten Toolbox mit Steuerelementen bereit (auf der linken Seite). 3. Was hat der Assistent erzeugt? Mit dem Windows-Explorer lassen sich über das Projektmappenverzeichnis folgende Dateien feststellen: 369 Die Programmiersprache C++ Abb. 1. In der Datei ReadMe.txt befindet sich u. a. in Auszügen pr64310.vcproj Dies ist die Hauptprojektdatei für VC++-Projekte, die vom Anwendungs-Assistenten erstellt wird. Sie enthält Informationen über die Version von Visual C++, mit der die Datei generiert wurde, über die Plattformen, Konfigurationen und Projektfeatures, die mit dem Anwendungs-Assistenten ausgewählt wurden. pr64310.h Hierbei handelt es sich um die Haupt-Headerdatei der Anwendung. Diese enthält andere projektspezifische Header (einschließlich Resource.h) und deklariert die Cpr64310App-Anwendungsklasse. pr64310.cpp Hierbei handelt es sich um die Haupt-Quellcodedatei der Anwendung. Diese enthält die Anwendungsklasse Cpr64310App. pr64310.rc Hierbei handelt es sich um eine Auflistung aller Ressourcen von Microsoft Windows, die vom Programm verwendet werden. Sie enthält die Symbole, Bitmaps und Cursors, die im Unterverzeichnis RES gespeichert sind. Diese Datei lässt sich direkt in Microsoft Visual C++ bearbeiten. res\pr64310.ico Dies ist eine Symboldatei, die als Symbol für die Anwendung verwendet wird. Dieses Symbol wird durch die Haupt-Ressourcendatei pr64310.rc eingebunden. res\pr64310.rc2 Diese Datei enthält Ressourcen, die nicht von Microsoft Visual C++ bearbeitet wurden. In dieser Datei werden alle Ressourcen gespeichert, die vom Ressourcen-Editor nicht bearbeitet werden können. 370 Die Programmiersprache C++ Der Anwendungs-Assistent erstellt eine Dialogklasse: pr64310Dlg.h, pr64310Dlg.cpp - das Dialogfeld Diese Dateien enthalten die Klasse Cpr64310Dlg. Diese Klasse legt das Verhalten des Haupt-Dialogfelds der Anwendung fest. Die Vorlage des Dialogfelds befindet sich in pr64310.rc, die mit Microsoft Visual C++ bearbeitet werden kann. StdAfx.h, StdAfx.cpp Mit diesen Dateien werden vorkompilierte Headerdateien (PCH) mit der Bezeichnung pr64310.pch und eine vorkompilierte Typdatei mit der Bezeichnung StdAfx.obj erstellt. Resource.h Dies ist die Standard-Headerdatei, die neue Ressourcen-IDs definiert. Microsoft Visual C++ liest und aktualisiert diese Datei. 2. Die folgenden Dateien sind in ReadMe.txt nicht ausgeführt: pr64310.aps Binäre Version der aktuellen Ressourcenskriptdatei. pr64310.ncb Zugang mit dem Texteditor nur bei geschlossenem Arbeitsbereich möglich. (Absolutes Sperrgebiet, da hier ständig Details vom Projekt dokumentiert werden). pr64310.sln 3. Im Unterverzeichnis ...\res findet man: pr64310.ico 32*32 Pixel-Icon mit MFC-Darstellung pr64310.rc2 Öffnet man diese Datei mit einem Texteditor, dann liest man folgendes: // // pr64310.RC2 - Ressourcen, die Microsoft Visual C++ nicht direkt bearbeitet // #ifdef APSTUDIO_INVOKED #error this file is not editable by Microsoft Visual C++ #endif //APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // Fügen Sie hier manuell bearbeitete Ressourcen hinzu... ///////////////////////////////////////////////////////////////////////////// 4. Ressourcen-Ansicht pr64310 IDD_PR64310_DIALOG ist der Name des Dialogfelds. Durch Doppelklick darauf gelangt man im Fensterbereich zu der Ressource, die man dort auch bearbeiten kann. IDR_MAINFRAME zeigt auf ein 32*32 Pixel-Icon, das die Buchstaben MFC grafisch verarbeitet. VS_VERSION_INFO beinhaltet Versions-Informationen, die vom Assistenten erzeugt wurden. 371 Die Programmiersprache C++ Abb. 4. Kompilieren und Linken Durch Kompilieren und Linken ist nun pr64310.exe entstanden. Nach dem Start zeigt sich das folgende Bild: Abb.: Das Fenster nach dem ersten Testlauf 372 Die Programmiersprache C++ Nach dem Start wird das Dialogfenster in der Bildschirmmitte platziert. Durch Festhalten der Titelleiste mit der linkten Maustaste kann man das Fenster beliebig verschieben (ziehen). Die Anwendung schließt direkt durch Anklicken des OK-Buttons, des Abbrechen-Buttons und des "X" rechts oben. Durch Anklicken des Icon links oben öffnet sich ein Popup-Menü mit zwei Menü-Punkten. Das Fenster kann nicht in der Größe verändert, nach unten geglickt (minimiert) oder auf volle Größe (maximiert) gebracht werden. Ein Rand Ist vorhanden, man kann ihn jedoch nicht mit der Maus zur Veränderung der Größe ziehen. 5: Untersuchen der Arbeitsumgebung nach dem Titel der Dialoganwendung Öffnen (ohne zu speichern) mit einem Texteditor (z.B. Notizblock, Wordpad aus dem WindowsZubehör) die Datei pr64310.rc. Im Abschnitt "Dialog" finden sich folgenden Eintragungen: IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200 STYLE DS_SHELLFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "1. MFC-Assistenten-Anwendung" FONT 8, "MS Shell Dlg" BEGIN DEFPUSHBUTTON "OK",IDOK,263,7,50,16 PUSHBUTTON "Abbrechen",IDCANCEL,263,25,50,16 CTEXT "TODO: Dialogfeld-Steuerelemente hier positionieren.",IDC_STATIC,10,96,300,8 END In der ersten Zeile IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200 findet sich der Name IDD_PR64310_DIALOG (nameID), der der Dialog-Ressource zugeordnet wurde. Die Ausdehnung ist 320*200 (Breite und Höhe in Pixel). Die nameID ist in der Datei resource.h definiert: #define IDR_MAINFRAME #define IDM_ABOUTBOX #define IDD_ABOUTBOX #define IDS_ABOUTBOX #define IDD_PR64310_DIALOG 128 0x0010 100 101 102 Windows verwendet zur Identifizierung die Dialogressource den Wert 102. IDR_MAINFRAME ist der Name für das MFC-Menü. Im Ressourcenskript findet man danach: STYLE DS_SHELLFONT | WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | WS_SYSMENU Hier werden die verschiedenen Window Styles (WS) abgelegt. WS_CAPTION bedeutet, dass das Fenster eine Titelzeile hat. WS_SYSMENU liefert das Menü links oben und das "X" rechts oben, mit dem man die Anwendung einfach beenden kann. Die senkrechten Striche sind die bitweise ODERVerknüpfungen der verschiedenen Window Styles. EXSTYLE WS_EX_APPWINDOW ist die erweiterte Version von STYLE und definiert WS_EX_APP_WINDOW. CAPTION "1. MFC-Assistenten-Anwendung" legt die Titelzeile des Fensters fest. FONT 8, "MS Shell Dlg" Die zu verwendende Schrift wird hier vergeben. BEGIN DEFPUSHBUTTON "OK",IDOK,263,7,50,16 PUSHBUTTON "Abbrechen",IDCANCEL,263,25,50,16 CTEXT "TODO: Dialogfeld-Steuerelemente hier positionieren.",IDC_STATIC,10,96,300,8 END Zwischen BEGIN und END steht - Button mit der Aufschrift "OK" mit den Dialogfeldkoordinaten 263,7,50,16 und der Kennung IDOK - Button mit der Aufschrift "Abbrechen" mit den Dilogfeldkoordinaten 263,25,50,16 und Kennung IDCANCEL 373 Die Programmiersprache C++ - Linksbündiges Textfeld. IDC_STATIC definiert ein statisches Textfeld. 6. Die diversen Dateien für Ressourcen Mit dem grafischen Editor wird eine Ressourcenscriptdatei (.rc) und die Datei resource.h erzeugt. Der Explorer zeigt im Projektmappen-Unterverzeichnis Debug die Datei pr64310.res, eine binäre Datei, die vom Ressourcen-Compiler aus der Ressourcen-Dateiscriptdatei (.rc) erzeugt wurde. 7. Experimente 1. Verändern des Dialogfensters mit dem grafischen Ressourcen-Editor - - Doppelklick auf IDD_PR64310_DIALOG. Im daraufhin erscheinenden Fenster Eigenschaften unter Darstellung | Beschriftung "Neuer Titel" angeben. Entfernen OK- und Abbrechen-Taste (durch Anklicken des jeweiligen Steuerelements und Drücken der Taste "Entf"). Ändern des Textfelds (rechte Maustaste drücken, unter Eigenschaften, Text-Eigenschaften | Formate bei "Text ausrichten" eingeben: zentriert(e) (Darstellung). Die Veränderung des Textfeldinhalts erfolgt unter "Text-Eigenschaften" im Eingabefeld Beschriftung ("Hallo Welt!!!"). Nach dem Übersetzen, Linken, Starten erscheint jetzt folgendes Fenster: pr64310.rc sieht jetzt so aus: IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "Neuer Titel" FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN CTEXT "Hallo Welt!!!",IDC_STATIC,10,96,300,8 END 2. "X" oben rechts soll jetzt verschwinden. Das erfolgt über den Eintrag "False" im Systemmenü unter Eigenschaften | Darstellung. Die Datei pr64310.rc sieht jetzt so aus: IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 200 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION EXSTYLE WS_EX_APPWINDOW 374 Die Programmiersprache C++ CAPTION "Neuer Titel" FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN CTEXT "Hallo Welt!!!",IDC_STATIC,10,96,300,8 END Das Fenster kann jetzt nur noch mit "Alt + F4" geschlossen werden. 3. Entfernen von WS_CAPTION Mit "Alt+F4" kann das Fenster geschlossen werden. 4. WS_VISIBLE durch Verändern des Eintrags zu IDD_PR64310_DIALOG in Eigenschaften | Verhalten unter Sichtbar von True nach False deaktivieren. Nach Übersetzen, Linken, Starten ergibt sich keine Veränderung. Das Fenster bleibt sichtbar. Die Datei pr64310.rc sieht jetzt so aus: IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP EXSTYLE WS_EX_APPWINDOW FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN CTEXT "Hallo Welt!!!",IDC_STATIC,10,96,300,8 END WS_VISIBLE legt fest, ob ein bestimmtes Fenster von Anfang an sichtbar ist oder nicht. Ist dies nicht der Fall, muß man das Fenster im Programm mit ShowWindow() bzw. bei Dialogen mit DoModal() explizit sichtbar machen. Da es sich hier um das Hauptfenster handelt, erledigt dies der Assistent. BOOL Cpr64310App::InitInstance() { .... Cpr64310Dlg dlg; m_pMainWnd = &dlg; INT_PTR nResponse = dlg.DoModal(); .... .... }; 5. Entferne aus dem Ressourcenscript alle Einträge in der mit STYLE beginnenden Zeile bis auf WS_POPUP mit einem Texteditor. 375 Die Programmiersprache C++ IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201 STYLE WS_POPUP EXSTYLE WS_EX_APPWINDOW FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN CTEXT "Hallo Welt!!!",IDC_STATIC,10,96,300,8 END Beim Abspeichern erscheint folgende Meldung: Es wird offensichtlich sorgfältig über die Dateien des Projekts gewacht. Da die manuellen Änderungen übernommen werden sollen, wird mit "Ja" geantwortet. Nach Starten, Linken, Ausführen erscheint das folgende Fenster: Auch das Entfernen von "Hallo Welt!!!" und das Herausstreichen von STYLE WS_POPUP EXSTYLE WS_EX_APPWINDOW mit einem Texteditor führt noch zur Ausgabe eines Fensters (graue Fläche) mit den angegebenen Abmessungen. pr64310.rc kann bis auf IDD_PR64310_DIALOG DIALOGEX 0, 0, 320, 201 BEGIN END abgemagert werden. Es erscheint immer noch ein Fenster nach Starten, Linken, Ausführen, das mit "Alt + F4" geschlossen werden kann. 6. Nachdem das Dialog-Fenster auf eine ganze Fläche ohne Rand reduziert ist, öffne pr64310.rc mit Hilfe des Texteditors und führe folgende Veränderungen aus: 376 Die Programmiersprache C++ IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 150 STYLE WS_POPUP FONT 10,"MS Sans Serif" BEGIN END Die Angabe STYLE WS_POPUP ist hier nötig, da sonst automatisch der Kombi-Typ WS_POPUPWINDOW (= WS_POPUP | WS_BORDER | WS_SYSMENU) erzeugt wird. Das Systemmenü wird nur dann nicht angezeigt, solange WS_CAPTION noch fehlt.WS_BORDER ergibt einen dünnen Rand. Werden BEGIN END weggelassen, gibt es zahlreiche Fehlermeldungen. Nach Starten, Linken, Ausführen erscheint jetzt eine quadratische (graue Fensterfläche) ohne weitere Dekoration. 7. Neuaufbau des Dialogs über die Resourcenansicht, Klick auf IDD_PR64310_DIALOG, verändern über EIGENSCHAFTEN Stil in "Überlappend" und Rahmen in "Dialogfeldrahmen" bzw. aktivieren Systemmenü, Minimieren-, Maximieren-Schaltfläche. pr64310.rc besitzt danach folgende Gestalt: IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 157 STYLE DS_SETFONT | DS_MODALFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CAPTION | WS_SYSMENU FONT 10, "MS Sans Serif", 0, 0, 0x1 BEGIN END Jetzt wird noch in Eigenschaften Beschriftung in "Neuaufgebauter eigener Dialog" und Clientkante von "False" nach "True" geändert. Die Datei pr64310.rc hat folgende Gestalt angenommen: IDD_PR64310_DIALOG DIALOGEX 0, 0, 150, 160 STYLE DS_SETFONT | DS_MODALFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_CLIENTEDGE CAPTION "Neuaufgebauter eigener Dialog" FONT 10, "MS Sans Serif", 0, 0, 0x1 BEGIN END Das zugehörige Fenster hat folgende Gestalt: 377 Die Programmiersprache C++ 6.4.4 Interaktive Platzierung der Komponenten 1. Ein einfaches Beispiel Aufgabenstellung. Zwei Zahlen in Eingabetextfeldern, die miteinander multipliziert und in ein Ausgabetextfeld gebracht werden sollen. Die Anwendung soll folgendes Fenster zur Ein- bzw. Ausgabe der Zahlen benutzen. Abb.: Lösung mit Hilfe des Anwendungsassistenten über interaktive Platzierung der Komponenten. 1. Erstelle ein neues MFC-Applikations-Visual-C++-Projekt 2. Erstelle den Anwendungsrahmen mit dem Anwendungs-Assistenten in folgenden Schritten: 1. Festlegen des Anwendungstyps "Auf Dialogfeldern basierend" 2. Unter BENUTZEROBERFLÄCHENFEATURES (Mekmale für die Benutzerschnittstelle) Aussehen des Hauptfensters für die Anwendung festlegen: "Ändern des Eintrags im Dialogfeldtitel in "2. Anwendungsassistenten-Anwendung". 3. Klick auf FERTIG STELLEN, damit der MFC-Anwendungs-Assistent den Anwendungsrahmen erstellen kann. 4. Der Arbeitsbereich zeigt nun: 378 Die Programmiersprache C++ Abb.: Arbeitsbereich mit einer Baumansicht der Projektklassen 5. Kompilieren der Anwendung über ERSTELLEN | PROJEKTMAPPE erstellen. 6. Die KLassenansicht zeigt drei Klassen: Abb.: Klassenansicht 7. Start der Anwendung mit START | STARTEN OHNE DEBUGGEN 379 Die Programmiersprache C++ Abb.: Ausgeführte Anwendung ohne Änderung des Projektgerüsts Wird auf das Icon (links oben in der Titelleiste geglickt) erscheint das System-Menü, das auch zur "AboutBox" führt Die Wahl des Menüpunkts, der zum Aufruf dieses Info-Felds führt, erzeugt eine Nachricht, die folgende Member-Funktion startet: void Cpr64410Dlg::OnSysCommand(UINT nID, LPARAM lParam) { if ((nID & 0xFFF0) == IDM_ABOUTBOX) { CAboutDlg dlgAbout; dlgAbout.DoModal(); } else { CDialog::OnSysCommand(nID, lParam); } } Hier wird das Objekt dlgAbout der von der MFC-Fenster-Klasse CDialog abgeleiteten Klasse CAboutDlg deklariert. Aus dem Ressourcen-Skript erhält man die Beschreibung der Ressourcen für das Dialogfeld: IDD_ABOUTBOX DIALOGEX 0, 0, 235, 55 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Info über pr64410" FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN 380 Die Programmiersprache C++ ICON LTEXT LTEXT DEFPUSHBUTTON IDR_MAINFRAME,IDC_STATIC,11,17,20,20 "pr64410 Version 1.0", IDC_STATIC,40,10,119,8,SS_NOPREFIX "Copyright (C) 2003",IDC_STATIC,40,25,119,8 "OK",IDOK,178,7,50,16,WS_GROUP END 3. Gestalten des Hauptdialogfelds nach folgender Vorlage: 4. Die Konfiguration der Eigenschaften der Steuerelemente zeigt der folgende Ausschnitt aus dem Ressourcen-Skript: IDD_PR64410_DIALOG DIALOGEX 0, 0, 320, 117 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "2. Anwendungsassisten-Anwendung" FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN DEFPUSHBUTTON "OK",IDOK,263,7,50,16 EDITTEXT IDC_EDIT1,112,19,77,15,ES_AUTOHSCROLL LTEXT "Eingabe 1",IDC_STATIC,31,20,61,11 EDITTEXT IDC_EDIT2,111,41,77,14,ES_AUTOHSCROLL LTEXT "Eingabe 2",IDC_STATIC,32,42,60,13 PUSHBUTTON "Ausgabe",IDC_BUTTON1,266,77,47,15 EDITTEXT IDC_EDIT4,113,79,76,15,ES_AUTOHSCROLL | ES_READONLY LTEXT "Ausgabe",IDC_STATIC,32,82,49,13 PUSHBUTTON "Abbrechen",IDCANCEL,263,25,50,16 END 5. Festlegen der Tabulator-Reihenfolge von Steuerelementen Dadurch wird sichergestellt, dass der Benutzer bei der Navigation mit Hilfe der TabTaste die Steuerelemnte in der gewünschten Reihenfolge ansprcht 1. Markiere im Berabeitungsbereich von Visual Studio entweder das Dialogfeld oder eines der Steuerelemente im Fenster 2. Wahl von FORMAT / TABULATOR_REIHENFOLGE. Daraufhin erscheinen im Fenster neben den Steuerelementen Nummern. Diese kennzeichnen die Reihenfolge, in der die Navigation durch das Dialogfeld verläuft. Verändern der Reihenfolge durch Anklicken der Nummernfelder in der Reihenfolge, in der der Benutzer durch das Dialogfeld navigieren soll. Die Steuerelemente nummerieren sich automatisch neu. 381 Die Programmiersprache C++ 6. Verbinden von Variablen mit Steuerelementen, z.B. 382 Die Programmiersprache C++ 6. Ausstatten der Steuerelemente mit Funktionalität Rechtsklick auf die Schaltfläche "Ausgabe" im Ressourcen-Editor für das Dialogfeld. Darauf erscheint ein Kontextmenü. Dort erfolgt die Auswahl des Menüpunkts Eigenschaften. Unter Eigenschaften wird "der glebe Blitz" gewählt. Der Eintrag BN_CLICKED zeigt die hinzuzufügende Funktion OnBnClickedButton1(). void Cpr64410Dlg::OnBnClickedButton1() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode für die // Benachrichtigung ein. UpdateData(TRUE); // Felder -> Variablen m_Ausgabe = m_Eingabe1 * m_Eingabe2; UpdateData(FALSE); // Variablen -> Felder } 7. Kompilieren und Test 383 Die Programmiersprache C++ 2. Kontrollkästchen, Listenfeld und Kombinationsfeld Aufgabenstellung: Mit Hilfe des Anwendungs-Assistenten soll eine dialogfeldbasierende Anwendung erzeugt werden. In dem Dialogfeld befinden sich zwei statische Textfelder, zwei Kontrollkästchen, ein Listenfeld und eine Schaltfläche. Nach Speichern, Kompilieren und Starten sollte das Dialogfeld ungefähr so aussehen: 1. Erstellen der dilogbasierenden Anwendung mit dem MFC-Assistenten. 2.. Erstellen der Ressourcen mit dem Ressourcen-Editor Nach dem Erstellen der Ressourcen mit dem Ressourcen-Editor und nach Speichern, Kompilieren, Starten sollte das Ressourcenskript für die dialogfeldbasierte Anwendung so aussehen: IDD_PR64440_DIALOG DIALOGEX 0, 0, 286, 199 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "Pr64440" 384 Die Programmiersprache C++ FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN COMBOBOX IDC_COMBO1,17,28,145,14,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP LISTBOX IDC_LIST1,167,28,95,154,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP LTEXT "Dateien",IDC_STATIC,197,15,51,10 LTEXT "Verzeichnis",IDC_STATIC,52,14,84,10 CONTROL "Check1",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,97,11,11 CONTROL "Check2",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,16,120,10,9 LTEXT "nur Hidden Files",IDC_STATIC,34,96,65,11 LTEXT "nur Exe-Files",IDC_STATIC,31,119,55,9 PUSHBUTTON "Dateien zeigen",IDC_BUTTON1,15,142,86,16 END Das Kombinationsfeld (Combobox) ist eine Kombination aus Eingabefeld und Listenfeld (Listbox). Das Kontrollkästchen gehört zur MFC-Klasse CButton. Über das Kombinationsfeld soll eine Verzeichnis-Auswahl erfolgen. Das geschieht durch Erzeugen folgender Member-Variablen: Steuerelement-IDs IDC_CHECK1 IDC_CHECK2 IDC_COMBO1 IDC_COMBO2 IDC_LIST1 IDC_LIST1 Typ BOOL BOOL CString CComboBox CString CListBox Element (Member-Variable) m_bCheck1 m_bCheck2 m_strCombo1 m_ctlCombo1 m_strList1 m_ctlList1 3. Ausstatten der Steuerelemente mit Funktionalität - Hinzufügen der Member-Funktion OnBnClickedButton1() (Klick auf die Schaltfläche) void Cpr64440Dlg::OnBnClickedButton1() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. m_ctlList1.ResetContent(); // loescht den gesamtem Listeninhalt UpdateData(TRUE); // Uebertraegt die aktuellen Zustaende (TRUE und FALSE) der // Kontrollkaestchen in die Variablen m_bCheck1 und m_bCheck2 // und die Eingabe der Kombinationsliste in m_strCombo1 if ((m_bCheck1 == TRUE) && (m_bCheck2 == TRUE)) m_ctlList1.Dir(DDL_HIDDEN | DDL_EXCLUSIVE,m_strCombo1+"\\*.exe"); if ((m_bCheck1 == TRUE) && (m_bCheck2 == FALSE)) m_ctlList1.Dir(DDL_HIDDEN | DDL_EXCLUSIVE,m_strCombo1+"\\*.*"); if ((m_bCheck1 == FALSE) && (m_bCheck2 == TRUE)) m_ctlList1.Dir(DDL_DIRECTORY,m_strCombo1+"\\*.exe"); if ((m_bCheck1 == FALSE) && (m_bCheck2 == FALSE)) m_ctlList1.Dir(DDL_DIRECTORY,m_strCombo1+"\\*.*"); long anzahl = m_ctlList1.GetCount(); CString str; str.Format("Anzahl %i",anzahl); CClientDC dc(this); dc.TextOut(20,120," "); dc.TextOut(20,120,str); } - Hinzufügen einer Funktion, die auf "Doppelklick"-Nachricht reagiert 385 Die Programmiersprache C++ void Cpr64440Dlg::OnLbnDblclkList1() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. UpdateData(TRUE); WinExec(m_strList1, SW_NORMAL); // Dateien, die nicht selbst starten koennen, koenne // innerhalb von WinEWxec mit dem Explorer aufgerufen werden /* WinExec("explorer " + m_strCombo1 + "\\" + m_strList1, SW_NORMAL); */ } 4. Nach dem Kompilieren und Test können .exe-Files geladen werden. 386 Die Programmiersprache C++ 387 Die Programmiersprache C++ 3. Bearbeitung von Dateien Aufgabenstellung: Entwerfe eine dialogfeldbasierende Anwendung nach folgender Vorlage: IDD_PR64450_DIALOG DIALOGEX 0, 0, 320, 230 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_APPWINDOW CAPTION "Datei Lesen und Schreiben" FONT 8, "MS Shell Dlg", 0, 0, 0x1 BEGIN PUSHBUTTON "Datei Lesen",IDC_BUTTON1,15,7,77,17 EDITTEXT IDC_EDIT1,15,28,286,70,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY EDITTEXT IDC_EDIT2,15,131,282,92,ES_MULTILINE | ES_AUTOHSCROLL | ES_WANTRETURN PUSHBUTTON "Datei Schreiben",IDC_BUTTON2,15,105,75,17 END Abb. Dialog mit zwei Eingabefeldern und zugehöriger Dialog-Ressource Die beiden Eingabefelder ermöglichen mehrzeilige Eingaben (ES_MULTILINE). Das obere Eingabefeld ist zur Ausgabe schreibgeschützt (ES_READONLY), das untere erlaubt bei Eingaben die Verwendung von (einfachen) Return (ES_WANTRETURN). Strg + Return bewirkt Zeilenumbruch. Lösungsschritte: 1. Erstelle die dialogfeldbasierende Anwendung, die das Dialogfeld mit dem MFC-Anwendungsassistenten erzeugt. 2. Erzeuge folgende Member-Variablen Steuerelement_ID Variablen-Typ Element (Member-Variable) 388 Die Programmiersprache C++ IDC_EDIT1 IDC_EDIT2 CString CString m_strEdit1 m_strEdit2 3. Durch Doppelklick auf die Schaltflächen erzeuge die Member-Funktionen OnBnClickedButton1() und OnBnClickedButton2(). 4. Fülle die beiden Member-Funktionen mit folgenden Programmquellcode-Zeilen auf: void Cpr64450Dlg::OnBnClickedButton1() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. TCHAR183 str[1000]; CFile datei("D:\\dok\\pgc\\ss02\\projekt\\pr64450\\pr64450Dlg.cpp", CFile::modeRead); datei.Read(str,sizeof(str)); datei.Close(); m_strEdit1 = str; UpdateData(FALSE); // Variablen --> Felder } void Cpr64450Dlg::OnBnClickedButton2() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. TCHAR str[1000]; UpdateData(TRUE); // Felder -> Variablen _tcscpy(str,m_strEdit2); // kopiert m_strEdit2 in str CFile datei("D:\\dok\\pgc\\ss02\\projekt\\pr64450\\demo.txt", CFile::modeCreate | CFile::modeWrite); datei.Write(str,sizeof(str)); datei.Close(); } 5. Die im Programm festgelegten Pfad- und Dateinamen sollen flexibel gehandhabt werden. Für solche immer wiederkehrende Aufgaben werden Standarddialoge zur Verfügung gestellt: Standardaufgabe Dateiauswahl Farbauswahl Schriftauswahl Suchen / Ersetzen Einrichten von Seiten zum Drucken Drucken MFC-Klasse CFileDialog CColorDialog CFontDialog CFindReplaceDialog CPageSetupDialog CPrintDialog Einbinden von CFileDialog in die Schreibroutine: void Cpr64450Dlg::OnBnClickedButton2() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. TCHAR str[1000]; UpdateData(TRUE); // Felder -> Variablen _tcscpy(str,m_strEdit2); // kopiert m_strEdit2 in str CFileDialog m_dlgFile(FALSE); // TRUE: Datei oeffnen FALSE: Datei speichern if (m_dlgFile.DoModal() == IDOK) { m_pathname = m_dlgFile.GetPathName(); } 183 TCHAR umfasst wchar_t* (Unicode) als auch char* (ANSI). Die Zuweisung von TCHAR zu CString kann man mit dem Zuweisungsoperator erledigen, für den umgekehrten Weg benötigt man die String-Kopierfunktion _tcscpy(Zieladresse,Quelladresse). Anstelle des Unicode-portablen _tcscpy kann man auch strcpy einsetzen. 389 Die Programmiersprache C++ CFile datei(m_pathname,CFile::modeCreate | CFile::modeWrite); datei.Write(str,sizeof(str)); datei.Close(); } Damit der vorliegende Programmcode funktioniert, muß noch die Member-Variable m_pathname vom Typ CString mit Zugriffstatus private deklariert werden. Mit Hilfe von CFileDialog m_dlgFile(FALSE); wird ein Objekt der Klasse CFileDialog erzeugt. Durch den Parameter FALSE wird der Dialog zum Speichern einer Datei erzeugt. m_dlgFile.DoModal() zeigt den Standarddialog in modaler Form an, d.h. der Dialog muß zunächst abgearbeitet werden, bevor man zur ursprünglichen Anwendung zurückkehren kann. Über if (m_dlgFile.DoModal() == IDOK) wird der Rückgabewert der Funktion DoModal() abgefragt. Ist dieser durch Drücken der Schaltfläche gleich IDOK, dann wird mit m_pathname = m_dlgFile.GetPathName(); der im Standarddialog ausgewählte Pfad- und Dateiname übernommen. Einbinden von CFileDialog zum Einlesen von Dateien: void Cpr64450Dlg::OnBnClickedButton1() { // TODO: Fügen Sie hier Ihren Kontrollbehandlungscode // für die Benachrichtigung ein. TCHAR str[1000]; CFileDialog m_dlgFile(TRUE); if (m_dlgFile.DoModal() == IDOK) { m_pathname = m_dlgFile.GetPathName(); } CFile datei(m_pathname,CFile::modeRead); datei.Read(str,sizeof(str)); datei.Close(); m_strEdit1 = str; UpdateData(FALSE); // Variablen --> Felder } 390 Die Programmiersprache C++ 6.5 Assistenten und MFC-Anwendungen 6.5.1 Die Assistenten MFC-Anwendungsassistent Der MFC-Anwendungsassistent legt ein direkt kompilierbares und ausführbares MFC-Projekt an und erlaubt es dem Programmierer über eine Reihe von Dialogfeldern, das zu erzeugende Projekt in vielfältiger Weise anzupassen und bspw. mit - Doc/View-Unterstützung Datenbankanbindung OLE- und ActiveX-Features verschiedene Fensterdekorationen und anderem auszustatten. Der Aufruf des Anwendungsassistenten erfolgt über den Befehl DATEI/NEU. Auf der Seite PROJEKTE kann der Anwendungsassistent dann aus der Liste der Projekttypen und Assistenten ausgewählt werden. Klickt man auf OK, erscheint das Dialogfeld des Assistenten. Das Dialogfeld verfügt über mehrere Optionen. Abb.: 391 Die Programmiersprache C++ Unter Anwendungstyp wird der grundlegende Typ der Anwendung festgelegt: EINZELNES DOKUMENT (SDI), eine Anwendung in einem Rahmenfenster, in dem nur ein Dokumentfenster zur Zeit angezeigt werden kann. MEHRERE DOKUMENTE (MDI), eine Anwendung mit einem Rahmenfenster, in dem mehrere Dokumentfenster verwaltet werden können. DIALOGBASIEREND, eine Anwendung mit einem Dialogfeld als Rahmenfenster. Als Sprache für die anzulegende Ressource fällt die Wahl auf DEUTSCH. Man kann auf die DOC/VIEW-Unterstützung verzichten. Doc/View ist ein Programmiermodell, das sich auf die Konzeption eines Programms bezieht und die Idee propagiert, daß für bestimmte Anwendungen die Trennung der Daten (Doc) und deren Darstellung(View) Vorteile bringt. Abb. Unter Unterstützung für Verbunddokumente wird angegeben, ob die Anwendung OLE-Verbundfunktionalität als SERVER, CONTAINER, MINISERVER oder CONTAINER/SERVER unterstützen soll. Nach der Entscheidung für eine Form der Unterstützung von Verbunddokumenten, kann danach die Unterstützung für Verbunddateien aktiviert werden. Außerdem kann dem Projekt Unterstützung für AUTOMATIONS-SERVER sowie für ACTIVEX-STEUERELEMENT-CONTAINER hinzugefügt werden. 392 Die Programmiersprache C++ Abb. Abb. 393 Die Programmiersprache C++ Unter Datenbankunterstützung kann die Anwendung mit einer Datenbank verbunden werden. Falls die Entscheidung für eine Datenbankanbindung gefallen ist, dann kann im unteren Teil des Dialogfelds über den Schalter Datenquelle eine Datenbank ausgewählt werden. Abb. Unter Benutzeroberflächenfeatures werden alle Einstellungen zum Erscheinungsbild der Anwendung vorgenommen. Auf dieser Seite kann entschieden werden, - ob eine SYMBOLLEISTE, passend zum Menü, gewünscht wird ob am unteren Rand des Fensters eine STATUSLEISTE angezeigt werden soll (in der automatisch kurze Hilfetexte zu den Befehlen der Menü- und Symbolleiste angezeigt werden). welches Aussehen die Fenster haben sollen 394 Die Programmiersprache C++ Abb. Abb. 395 Die Programmiersprache C++ Zuletzt werden die zu generierenden Klassen angezeigt: Abb. Abfangen von Meldungen mit Hilfe des MFC-Klassenassistenten: Meldungstabellen sind mit Unterstützung durch Klassenansicht und Eigenschaftsfenster zu erstellen. Klassenansicht Die Klassenansicht wird über ANSICHT/KLASSENANSICHT aufgerufen Die Meldungen-Schaltfläche des Eigenschaftsfensters. Zur Tabelle der Meldungen gelangt man durch Auswahl einer Klasse, Anzeigen des zur Klasse zugehörigen Eigenschaftsfensters (z. B. das ANSICHT-Menü) und Klick in der Symbolleiste des Eigenschaftsfensters auf die Schaltfläche MELDUNGEN (befindet sich zwischen dem gelben Blitz und der grünen Raute). 396 Die Programmiersprache C++ 6.5.2 Das Doc/View-Modell Beim Doc/View-Modell handelt es sich nicht um ein konkretes Element einer Windows-Anwendung, sondern lediglich um einen bestimmten Programmteil (, der allerdings in der MFC-Programmierung eine große Rolle spielt). Auf der einen Seite stehen die eigentlichen Rohdaten, repräsentiert durch die Dokumentenklasse, die von der Basisklasse CDocument abgeleitet ist. Auf der anderen Seite steht die Ansichtsklasse, die für die Anzeige der Daten verantwortlich ist und auf CView basiert. Für die gleichen Daten (also ein CDocument-Objekt) können mehrere Ansichten eingerichtet werden, in den Daten auf jeweils unterschiedliche Weise angezeigt werden. Dokument, Ansicht und Dokumentenvorlage Die Dokumentklasse eines Projektes ist von der MFC Klasse CDocument abgeleitet. CDocument stellt die grundlegende Funktionalität für eine benutzerdefinierte Dokumentklasse zur Verfügung. Eine der nützlichsten Funktionen, die sich in einem Dokument befindet, ist die MFC Funktion Serialize(CArchive& ar). Mit Hilfe dieser Funktion können die Daten in einem Dokument relativ einfach serialisiert werden. Ein Dokument stellt den Teil der Daten dar, der typischerweise mit dem Befehl Datei Öffnen geöffnet und dem Befehl Datei Speichern, gespeichert wird. CDocument unterstützt die Standard Operationen wie Erstellen, Laden und Speichern eines Dokuments. Das Anwendungsgerüst reagiert automatisch auf die Befehle Datei Öffnen und Datei Speichern und ruft die bekannten Windows Dialoge für diesen Zweck auf. Die Ansichtsklasse einer Anwendung ist von der MFC Klasse CView abgeleitet. Diese Klasse bietet die grundlegende Funktionalität für eine benutzerdefinierte Ansichtsklasse. Eine Ansicht ist einem bestimmten Dokument zugeordnet und dient als eine Art Vermittler zwischen dem Dokument und Benutzer. Die Ansicht gibt ein Abbild des Dokuments auf dem Bildschirm oder Drucker wieder und interpretiert die Eingaben des Benutzers als Operationen aufs Dokument. Ein Dokument kann unter Umständen mehrere Ansichten haben, eine Ansicht ist jedoch nur einem einzigen Dokument zugeordnet. Die Klasse CView hat mehrere abgeleitete Klassen, die man als Basis für die Ansichtsklasse verwenden kann: Klasse CEditView CFormView CHtmlView CListView CRichEditView CSrollView CTreeView Beschreibung liefert die Funktionalität eines Eingabefelds. Mit dieser Klasse lassen sich einfache Text-Editoren implementieren. Die Basisklasse für Ansichten, die Steuerelemente enthalten. Mit dieser Klasse lassen sich formularbasierte Dokumente in Anwendungen bereitstellen. liefert die Funktionalität eines Webbrowsers. Die Ansicht behandelt direkt die URLNavigation, Hyperlinks, usw. stellt die Funktionalität von Listen in der Dokument-/View-Architektur realisiert Zeichen- und Absatzformatierungen. Mit dieser Klausel lassen sich Textverarbeitungen implementieren. stellt Bildlauffähigkeiten für eine CView-Klasse dar stellt die Funktionalität von Bäumen in der Dokument-/View-Architektur dar Abb.: Wichtige Funktionen der Klassen CDocument und CView: 397 Die Programmiersprache C++ CView::GetDocument() // GetDocument() liefert einen Zeiger auf ein Dokumentobjekt, das mit der Ansicht assoziiert // ist. Diese Funktion ermöglicht, auf Member-Funktionen und öffentlichen Variablen des Dokuments // zuzugreifen. Falls der Wunsch besteht, auch auf die nicht öffentlichen Variablen des Dokuments // zuzugreifen, muß man die Ansichtsklasse als “friend“ von Dokumentklasse deklarieren CDocument::SetModifiedFlag() // Mit dem Aufruf dieser Funktion wird sichergestellt, daß das Anwendungsgerüst (Framework) den // Benutzer auffordert, die Änderungen zu speichern, bevor er das Dokument schließen kann. CDocument::OnNewDocument() // Diese Funktion wird vom AppWizard in die Dokumentklasse erstellt. OnNewDocument() wird vom // Anwendungsgerüst als Reaktion auf den Befehl Neu des Menübefehls aufgerufen. Hier sollten die // Daten des Dokuments initialisiert werden. CDocument::Serialize(CArcive& ar) // In der Funktion Serialize() kann man mit Hilfe des Parameters ar direkt ins Archive schreiben // oder davon lesen. Kommunikation zwischen Dokument und Ansicht Die Ansicht ist für die Anzeige und Modifikation von Daten verantwortlich, jedoch nicht für deren Speicherung. Damit überhaupt eine funktionierende Kommunikation zwischen den beiden Klassen zustande kommen kann, muß das Dokument geeignete Funktionen und Wege der Ansicht zur Verfügung stellen, womit die Ansicht auf die Daten des Dokuments zugreifen kann. Mit der Funktion GetDocument() erfolgt der Zugriff auf die Daten des Dokuments. Die Beziehung zwischen der Dokumentklasse, Ansichtsklasse und Hauptrahmenfensterklasse wird in der sogenannten Dokumentvorlage in der Funktion InitInstance() der Anwendungsklasse festgehalten. Die Anwendungsklasse befindet sich in der Datei, die den Namen Ihres Projektes trägt. Der AppWizard übernimmt die Initialisierungen für die Anwendung in dieser Funktion. Der Anwender hat normalerweise mit der Anwendungsklasse nichts zu tun. Bsp.: Anzeige eines über eine Dialogbox angegebenen Satzes in der Ansicht einer SDI-Anwendung184. Der Text im folgenden Fenster sagt dem Anwender, was zu tun ist. Nach Drücken der „OK“-Schaltfläche erscheint das Fenster der Ansichtsklasse. 184 pr65220 398 Die Programmiersprache C++ Ein Klick auf die linke Maustaste bewirkt die Anzeige der folgenden Dialogseite: Hier kann ein Satz eingegeben werden, der anschließend (nach Betätigen der Schaltfläcke „OK“) im Dialogfenster im Fenster der Ansicht wiedergegeben wird. 1. Anlegen eines neuen SDI-Projekts 2. Entwurf einer Dialogseite für die Eingabe eines Datensatzes. Auf der Dialogseite befindet sich neben „OK“ und „Abbrechen“ eine Editbox mit der Member-Variable m_satz. Erzeugen der Dialogressource - Aufruf des Befehls PROJEKT/RESOURCE HINZUFUEGEN 399 Die Programmiersprache C++ - Doppelklick im Feld RESSOURCENTYP auf "Dialog". Der Dialog-Editor erscheint im Fenster von Visual Studio. Öffne das Eigenschaftsfenster für das neue Dialogfeld (über ANSICHT/EIGENSCHAFTSFENSTER). Ändere den Titel in "Beispieldialog". 3. Erstelle mit Hilfe des Klassenassistenten - eine Klasse „CDialog1“ zur Präsentation der Dialogseite Klick mit der rechten Maustaste irgendwo in das Dialogfeld und Auswahl des Befehls KLASSE HINZUFUEGEN im Kontextmenü. Über den MFC-KLassenassistenten kann die Klasse mit dem unter 2. erzeugten Dialogfeld verknüpft werden. Zur Verbindung der Steuerelemente des Dialogfelds mit dem Code müssen in der Dialogklasse passende Membervariablen verknüpft werden: Aufruf ANSICHT/KLASSENANSICHT, Wahl der Klasse CDialog1. Danach entweder VARIABLE HINZUFÜGEN aufrufen oder mit der rechten Maustaste das Kontextmenü öffnen und dort HINZUFÜGEN/VARIABLE HINZUFÜGEN anwählen. Man kann auch, wenn bereits eine Klasse für das Dialogfeld erstellt ist, irgendwo in das Dialogfeld klicken und dann im Kontextmenü VARIABLE HINZUFÜGEN auswählen. Die Variable wird dem Eingabefeld IDC_EDIT1 unter dem Namen m_satz zugeordnet. - eine Nachrichten-Funktion für die Nachricht WM_LBUTTONDOWN in der Ansicht (OnLButtonDown) 4. Mache die Dialogseite in der Ansicht durch Eintragen der Header-Datei der Dialogseite (mit Hilfe von include) bekannt 5. Deklariere in Cpr65220Doc eine öffentliche Variable mit dem Namen „satz“ vom Typ CString. 6. Quellcode-Ergänzungen. // Cpr65220View Meldungshandler void Cpr65220View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, // und/oder benutzen Sie den Standard. CDialog1 dlg; int iresult=dlg.DoModal(); if(iresult == IDOK) { Cpr65220Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // Inhalt der Editbox uebergeben pDoc->satz = dlg.m_satz; // Sicherstellen, dass der Benutzer beim Verlassen der Anwendung // zum Speichern der Änderungen aufgefordert wird. pDoc->SetModifiedFlag(); // Einleiten Zeichenvorgang Invalidate(); } CView::OnLButtonDown(nFlags, point); } Für die Speicherung von “satz” muß in der Funktion Serialize() gesorgt werden 400 Die Programmiersprache C++ // Cpr65220Doc Serialisierung void Cpr65220Doc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: Hier Code zum Speichern einfügen ar << satz; } else { // TODO: Hier Code zum Laden einfügen ar >> satz; } } Die Funktion OnDraw() übernimmt die Darstellung des Satzes. void Cpr65220View::OnDraw(CDC* pDC) { Cpr65220Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen pDC->TextOut(50,50,pDoc->satz); } Das Fenster der Ansicht kann zu Beginn einen Hinweis auf den darzustellenden Satz enthalten. Platz für eine derartige Initialisierung stellt die Funktion OnNewDocument() im Dokument bereit. BOOL Cpr65220Doc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // TODO: Hier Code zur Reinitialisierung einfügen // (SDI-Dokumente verwenden dieses Dokument) satz = "Hier kann auch Ihr Satz stehen."; // Titel fuer den Dialog SetTitle("Neuer Titel fuer den Dialog"); return TRUE; } Ein Name für das Dokument kann in der Dokumentenklasse unter OnNewDocument() mit der MFCFunktion CDocument::SetTitle(LPCSTR lpszTitel) bestimmt werden. Initialisierungen für die Ansicht erfolgen in der Funktion OnInitialUpdate(). Immer, wenn der Befehl NEU oder ÖFFNEN vom Dateimenü aus, betätigt wird, dann wird OnInitialUpdate() mit Informationen über die Anwendung in einer Message Box aufgerufen. Damit diese Message Box nicht bei jedem Öffnen einer neuen Datei auf dem Bildschirm erscheint, wurde im Konstruktor der Ansicht die BOOL-Variabe „Meldung“ initialisiert. void Cpr65220View::OnInitialUpdate(void) { CView::OnInitialUpdate(); if (Meldung) { MessageBox( "Es handelt sich hier um ein einfaches Programm für" " Serialisierung. Wenn Sie im Programm" " auf die linke Maustaste drücken, erscheint der Dialog" " zum Eingabe eines Satzes. Diesen Satz können Sie" " speichern und nochmals laden." ); Meldung=FALSE; } 401 Die Programmiersprache C++ } Die Geometrie des Hauptfensters wird über CMainFrame::PreCreateWindow(...) beeiflußt: BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // TODO: Ändern Sie hier die Fensterklasse oder die Darstellung, // indem Sie CREATESTRUCT cs modifizieren. cs.x = 100; cs.y = 100; cs.cx = 400; cs.cy = 200; cs.style = WS_OVERLAPPED | WS_CAPTION | FWS_ADDTOTITLE | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU; return TRUE; } 402 Die Programmiersprache C++ Dokumente und Ansichten Der Begriff Ansichten (View) gibt einen Hinweis auf die bildhafte Darstellung von Daten. Der Begriff Dokumente (Doc) ist nicht selbsterklärend. "Doc" enthält die Daten (Zahlen, Texte, ...). "View" stellt die Daten bildlich dar. "Doc" stellt auch einen Mechnismus zur Verfügung, der für das Lesen und Schreiben der Daten in Dateien sorgt ("Serialisierung"). Dafür bietet View die Möglichkeit, die Ansicht der Daten nicht nur auf dem Bildschirm, sondern auch z.B. auf einem Drucker auszugeben. Außerdem stellt eine View den Kontakt mit den Benutzereingaben her (Maus-, Tastatureingaben, ...). Für den Rahmen um "Doc" und "View" sorgt eine Rahmenfensterklasse, z.B. CFrameWnd. "Doc" beruht auf der Klasse CDocument, "View" auf der von CWnd abgeleiteten Klasse CView. Der Wirkungsmechanismus der Dokumentvorlagen ist in der abstrakten MFC-Klasse CDocTemplate enthalten. Diese Klasse wird nicht direkt benutzt, sondern die davon abgeleitete Klassen CSingleDocTemplate für SDI und CMultiDocTemplate für MDI. Zusätzlich benötigt man eine von CWinApp abgeleitete Klasse. 6.5.3 SDI- und MDI-Anwendungen Eine SDI-Anwendung ist eine dokumentenbezogene Anwendung, die nur mit einem Dokument zu einem bestimmten Zeitpunkt und nur mit einem Typ von Dokument arbeiten kann. Eine MDI-Anwendung ist ebenfalls eine dokumentenbezogene Anwendung, bei der der Benutzer an mehreren Dokumenten gleichzeitig arbeiten und zwischen den Fenstern der Anwendung umschalten kann. 403 Die Programmiersprache C++ 6.5.3.1 SDI-Anwendungen 1. Zeichnen eines Würfels 1. Anlegen eines neuen MFC-Anwendung-Visual-C++-Projekts 2. Im Bereich ANWENDUNGSTYP wähle die Option EINFACHES DOKUMENT Abb. 3. Im Bereich ZEICHENFOLGEN FUER DOKUMENTENVORLAGEN gib, wie die folgende Abb. zeigt, eine Dateinamenerweiterung für die Dateien an, die die Anwendung erzeugen wird. 404 Die Programmiersprache C++ 4. Im Bereich ERSTELLTE KLASSEN kann eine Basisklasse ausgewählt werden, auf der die Ansichtsklasse basieren soll. Wird die Einstellung CView beibehalten, kann direkt auf FERTIG STELLEN geglickt werden. Der MFC-Anwendungsassistent erzeugt das Anwendungsgerüst. Man erhält nach dem Ausführen folgendes Fenster: 405 Die Programmiersprache C++ Unter dem Menü "Datei" wurden bereits die wesentlichen Möglichkeiten in Bezug auf die ".sav"Dateien berücksichtigt. Der Menüpunkt "Bearbeiten" ist im Moment nicht aktuell. Unter Hilfe ergibt sich die bekannte About-Dialogbox. 5. In das vorliegende Fenster kann jetzt der Würfel eingezeichnet werden. Man zeichnet dazu ein erstes Quadrat, dahinter schräg versetzt ein zweites Quadrat, und verbindet die Eckpunkte mit vier diagonalen Linien. Quadrate und Würfel haben gleiche Seitenlängen. Die beiden Quadrate haben beide einen linken oberen Eckpunkt, der jeweils eine x- und eine y-Koordinate besitzt. Die zugehörigen Daten müssen in die Dokumentenklasse eingefügt werden. double laenge; CPoint erstesQuadrat; CPoint zweitesQuadrat; Die Deklaration erfolgt in der Dokumentenklasse , d.h. in Cpr65310.h. // pr65310Doc.h : Schnittstelle der Klasse Cpr65310Doc // #pragma once class Cpr65310Doc : public CDocument { ... // Implementierung public: CPoint erstesQuadrat; CPoint zweitesQuadrat; int laenge; virtual ~Cpr65310Doc(); ... }; Diese Member-Variablen werden an zwei Stellen der Dokumentenklasse initialisiert: - im Konstruktur der Dokumentenklasse - in der Member-Funktion "OnNewDocument" der Dokumentenklasse // pr65310Doc.cpp : Implementierung der Klasse Cpr65310Doc ... // Cpr65310Doc Erstellung/Zerstörung Cpr65310Doc::Cpr65310Doc() { // TODO: Hier Code für One-Time-Konstruktion einfügen laenge = 200; erstesQuadrat.x = 100; erstesQuadrat.y = 200; zweitesQuadrat.x = 200; zweitesQuadrat.y = 100; } Cpr65310Doc::~Cpr65310Doc() { } BOOL Cpr65310Doc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // TODO: Hier Code zur Reinitialisierung einfügen // (SDI-Dokumente verwenden dieses Dokument) 406 Die Programmiersprache C++ laenge = 200; erstesQuadrat.x = 100; erstesQuadrat.y = 200; zweitesQuadrat.x = 200; zweitesQuadrat.y = 100; return TRUE; } 6. Daten anzeigen. Die Anzeige der Daten wird in der Ansichtsklasse realisiert. Die grafische Anzeige erfolgt in der Member-Funktion OnDraw(). Diese Funktion bietet bisher folgendes: void Cpr65310View::OnDraw(CDC* /*pDC*/) { Cpr65310Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen } Wichtig sind die beiden Zeiger pDC und pDoc. Der erste Zeiger zeigt auf einen Device Context, der beim Neuzeichnen des Fensters eingesetzt wird, und der zweite zeigt auf die Dokumentenklasse Cpr65310Doc. Für OnDraw() ergibt sich folgende Programmquellcode: void Cpr65310View::OnDraw(CDC* pDC) { Cpr65310Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen // Erstes Quadrat zeichnen pDC->MoveTo(pDoc->erstesQuadrat); pDC->LineTo(pDoc->erstesQuadrat.x + pDoc->laenge, pDoc->erstesQuadrat.y); pDC->LineTo(pDoc->erstesQuadrat.x + pDoc->laenge, pDoc->erstesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->erstesQuadrat.x , pDoc->erstesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->erstesQuadrat.x ,pDoc->erstesQuadrat.y); // Zweites Quadrat zeichnen pDC->MoveTo(pDoc->zweitesQuadrat); pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge, pDoc->zweitesQuadrat.y); pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge, pDoc->zweitesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->zweitesQuadrat.x , pDoc->zweitesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->zweitesQuadrat.x , pDoc->zweitesQuadrat.y); // Verbindungslinien zeichnen pDC->MoveTo(pDoc->erstesQuadrat); pDC->LineTo(pDoc->zweitesQuadrat); pDC->MoveTo(pDoc->erstesQuadrat.x + pDoc->laenge, pDoc->erstesQuadrat.y); pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge, pDoc->zweitesQuadrat.y); pDC->MoveTo(pDoc->erstesQuadrat.x + pDoc->laenge, pDoc->erstesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->zweitesQuadrat.x + pDoc->laenge, pDoc->zweitesQuadrat.y + pDoc->laenge); pDC->MoveTo(pDoc->erstesQuadrat.x, pDoc->erstesQuadrat.y + pDoc->laenge); pDC->LineTo(pDoc->zweitesQuadrat.x, pDoc->zweitesQuadrat.y + pDoc->laenge); } 407 Die Programmiersprache C++ Nach dem Ausführen des Programms zeigt das Fenster folgendes Aussehen: Abb.: View des perspektifischen Würfels 7. Schreiben / Lesen von Dokumentobjekten (sog. Serialisierung). Der Assistent hat bereits eine Funktion zur Serialisierung der Daten bereit gestellellt: // Cpr65310Doc Serialisierung void Cpr65310Doc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: Hier Code zum Speichern einfügen } else { // TODO: Hier Code zum Laden einfügen } } Mit Serialisierung ist hier das Speichern und Laden der Daten im Zusammenhang mit ".sav" –Dateien gemeint. Fünf Daten (laenge, erstesQuadrat, zweitesQuadrat) werden übergeben: // Cpr65310Doc Serialisierung void Cpr65310Doc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: Hier Code zum Speichern einfügen ar << erstesQuadrat; ar << zweitesQuadrat; } else 408 Die Programmiersprache C++ { // ar ar ar TODO: Hier Code zum Laden einfügen >> laenge; >> erstesQuadrat; >> zweitesQuadrat; } } Die Daten werdem seriell in einer Datei abgelegt. Daher müssen lögischerweise die Daten immer in der gleichen Variablen-Reihenfolge gelesen bzw. geschrieben werden. In der Klasse CArchive existieren die überladenen Operatoren << und >>, mit denen auch zusammengesetzte Daten (z.B. CPoint) einfach hin- und herschieben kann. 8. Basteln einer Routine, mit der man das erste Quadrat verschieben kann. Über die Klassenansicht zu Cpr65310View aus der Eigenschaftsliste WM_KEYDOWN die Funktion OnKeyDown() hinzufügen. Es wird eine neue Funktion OnKeyDown() mit folgendem Aussehen generiert: // Cpr65310View Meldungshandler void Cpr65310View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, und/oder benutzen Sie den Standard. CView::OnKeyDown(nChar, nRepCnt, nFlags); } Über den ersten Parameter wird eine switch-Anweisung konstruiert. Je Tastendruck soll das erste Quadrat um 10 Pixel in die entsprechende Richtung verschoben werden. 409 Die Programmiersprache C++ void Cpr65310View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, und/oder benutzen Sie den Standard. Cpr65310Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); switch(nChar) { case 37: (pDoc->erstesQuadrat.x) -= 10; break; case 38: (pDoc->erstesQuadrat.y) -= 10; break; case 39: (pDoc->erstesQuadrat.x) += 10; break; case 40: (pDoc->erstesQuadrat.y) += 10; break; } // Mitteilung ueber die veraenderten Daten an die // Dokumentenklasse pDoc->SetModifiedFlag(); // Aktualisierung der Ansichtsklasse Invalidate(); CView::OnKeyDown(nChar, nRepCnt, nFlags); } Das Setzen des "ModifiedFlag" führt dazu, dass die Anwendung bei Veränderungen (mit den Pfeiltasten) vor dem Beenden oder Neuladen nach dem Speichern der aktuellen Daten befragt. Abb.: Bewegung des Würfels (zwei der fünf Daten) 410 Die Programmiersprache C++ 6.5.3.2 MDI-Anwendungen MDI-Anwendungen unterscheiden sich von SDI-Anwendungen durch die beiden (MDI-) spezifischen Klassen CMDIFrameWnd und CMDIChildWnd: - - Die von der CMDIFrameWnd abgeleiteten Klasse CMainFrame bestimmt den Hauptrahmen der Anwendung. Die Klasse CMDIFrame ist das äußere Rahmenfenster einer MDI-Anwendung. MDIGetActive ist eine Member-Funktion dieser Klasse, die einen Zeiger auf das momentan aktive untergeordnete Fenster zurückgibt. Die meisten anderen Funktionen, die mit untergeordneten Fenstern arbeiten, sind in dem im MDI-Anwendungsrahmen vom Anwendungsassistenten erstellten Fenstermenü enthalten. Die von CMDIChildWnd abgeleitete Klasse CChildFrame bildet den Rahmen, der die CViewKlassen aufnimmt. Dieser Rahmen leitet die Nachrichten und Ereignisse an die Ansichtsklasse zur Verarbeitung oder Anzeige weiter. Die Klasse CMDIChildView ist das innere Rahmenfenster einer MDI-Anwendung. Die meisten Funktionen dieser Klasse haben mit dem Zustand des untergeordneten Fensters im übergeordneten Rahmen zu tun: MDIDestroy, MDIActivate, MDIMaximize, MDIRestore. Keine dieser Funtionen übernimmt Parameter oder gibt Ergebnisse zurück. GetMDIFrame gibt einen Zeiger auf das übergeordnete CMDIFrameWnd-Fenster zurück. 411 Die Programmiersprache C++ 6.5.4 Das MFC-Anwendungsgerüst 6.5.4.1 Erzeugen der Fenster Bis auf wenige Ausnahmen verfügt jede Windows-Anwendung über ein sichtbares Hauptfenster. Ein Hauptfenster kann ein Dialogfenster oder ein sog. Rahmenfenster sein. Rahmenfenster haben einen speziellen Rahmen, in dem bestimmte Dekorationselemente (Menü, Symbolleisten und Statusleiste) integriert werden können. Der innere Bereich des Fensters, der nach Abzug der Dekorationselemente frei bleibt, ist der sog. Clientbereich. Zum Erzeugen eines Rahmenfensters sind folgende Schritte nötig: 1. Zum Erzeugen eines Rahmenfensters wird ein Objekt der Klasse CMainFrame instantiiert: CMainFrame *pMainWnd = new CMainFrame 2. Zur Verbindung des Rahmenfensters mit der Anwendung wird der Zeiger auf das Fensterobjekt an die globale MFC-Variable m_pMainWnd übergeben: m_pMainWnd = pMainWnd; 3. Zur Anzeige der Rahmenfenster werden die Methoden ShowWindow() und UpdateWindow() aufgerufen: m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); Im Quelltext eines mit dem MFC-Anwendungsassistenten erzeugten Programms wird man diese Anweisungen so nicht wiederfinden. Das liegt an der Doc/ViewUnterstützung der MFC, die mit sogenammten Dokumentvorlagen arbeitet. Bei der Einrichtung der Dokumentenvorlage wird auch das Rahmenfenster eingerichtet. 6.5.4.2 Anpassen der Fenster 1. Rahmenfenster und Systemmenü ( in einer SDI-Anwendung) Am einfachsten ist die Anpassung an eigene Vorstellungen durch Aufruf der entsprechenden Optionen auf den Dialogseiten des Anwendungs-Assistenten. Alle Einstellungen zum Erscheinungsbild der Anwendung werden im wesentlichen unter Benutzeroberflächenfeatures vorgenommen. Hier kann entschieden werden, - ob eine Symbolleiste, passend zum Menü, bereitgestellt werden soll. ob am unteren Rand des Fensters eine Statusleiste angezeigt werden soll, in der automatisch kurze Hilfetexte zu den Befehlen der Menü- und Symbolleiste angezeigt werden. welches Aussehen die Menüs haben sollen. Zur Demonstration wird ein Anwendungsbeispiel erstellt: - Erstelle mit dem Anwendungsassistenten eine neue SDI-Anwendung - Beachte unter Benutzeroberflächenfeatures die folgenden Einstellungen zu verschiedenen Zutaten (Symbolleiste, Statusleiste, Systemmenü, Breiter Rahmen, Minimieren- und MaximierenSchaltfläche, etc) für Gestalt und Aussehen des Fensters 412 Die Programmiersprache C++ Mit diesen Einstellungen sieht die Funktion CMainFrame::PreCreateWindow(...) so aus: BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; return TRUE; } Falls die Minimieren- und Maximieren-Schaltfläche, Breiter Rahmen, andockbare Symbolleiste abgewählt wurde, sähe das Ergebnis folgendermaßen aus: BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; cs.style = WS_OVERLAPPED | WS_CAPTION | FWS_ADDTOTITLE | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU; return TRUE; } Der nach der Abwahl verbleibende Rest wird dann cs.style zugewiesen. Der Fenterstil wird durch eine Kombination bestimmter Konstanten festgelegt. Stil WS_BORDER WS_CAPTION WS_CHILD WS_CHILDWINDOW WS_CLIPCHILDREN WS_DISABLED WS_DLGFRAME Bedeutung Dünner Rand Titel und dünner Rand Kindfenster Entspricht WS_CHILD Beim Zeichnen der Elternfensters werden Bereiche des Kindfensters ausgenommen Fenster anfänglich deaktiviert Für Dialogfenster 413 Die Programmiersprache C++ WS_GROUP WS_HSCROLL WS_ICONIC WS_MAXIMIZE WS_MAXIMIZEBOX WS_MINIMIZE WS_MINIMIZEBOX WS_OVERLAPPED WS_OVERLAPPEDWINDOW WS_POPUP WS_POPUPWINDOW WS_SIZEBOX WS_SYSMENU WS_TABSTOP WS_THICKFRAME WS_TILED WS_TILEDWINDOW WS_VISIBLE WS_VSCROLL Zur Gruppierung von Steuerelementen Horizontale Bildlaufleiste Entspricht WS_MINIMIZED Fenster ist anfänglich maximiert Schalter zum Maximieren (erfordert WS_SYSMENU, nicht in Kombination mit WS_EX_CONTEXTHELP) Fenster ist anfänlich minimiert Schlter zum Minimieren (erfordert WS_SYSMENU, nicht in Kombination mit WS_EX_CONTEXTHELP) Tiielleiste und Rahmen Kombination der Stile WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX Popup-Fenster Kombination der Stile WS_BORDER, WS_POPUP, WS_SYSMENU Entspricht WS_THICKFRAME Systemmenü (erfordert WS_CAPTION) Steuerelement kann mit der Tabulator-Taste angesteuert werden. Breiter Rahmen. Nur dieser Rahmen erlaubt das verändern der Fenstergröße durch Ziehen des Rahmens Entspricht WS_OVERLAPPED Entspricht WS_OVERLAPPEDWINDOW Fenster anfangs sichtbar Vertiklae Bildlaufleiste Abb.: Fensterstile für cs.style Neu ist FWS_ADDTOTITLE, ein Beispiel eines Frame-Window Style. Frame-Window Style (FWS) FWS_ADDTOTITLE FWS_PREFIXTITLE FWS_SNAPTOBARS Bedeutung Der Document-Titel wird dem Namen der Anwendung hinzugefügt Funktioniert zusammen mit FWS_ADDTOTITLE. Document-Titel steht vor dem Namen der Anwendung. Dies ist die Standardeinstellung des MFC-Assistenten. Steuert die Größe des Rahmenfensters, das eine Steuerleiste ("control bar") als frei bewegliches ("floating") Fenster beherbergt. Das Rahmenfenster wird der Größe der Steuerleiste angepasst. Elemente der Rahmenfensters enthält die von der Klasse CFrameWnd abgeleitete Klasse CMainFrame class CMainFrame : public CFrameWnd { protected: // Nur aus Serialisierung erstellen CMainFrame(); DECLARE_DYNCREATE(CMainFrame) public: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); public: virtual ~CMainFrame(); protected: // Eingebundene Elemente der Steuerleiste CStatusBar m_wndStatusBar; CToolBar m_wndToolBar; protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); DECLARE_MESSAGE_MAP() }; 414 Die Programmiersprache C++ Der Konstruktor CMainFrame() erzeugt für die Erzeugung des Rahmenfensters, der Destruktor ~CMainFrame() zerstört es. Außerdem enthält die Klasse zwei wichtige Funktionen: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); PreCreateWindow(…) ermittelt einen Rückgabewert vom Typ BOOL. Ist dieser Wert FALSE, dann ist die Einstellung des Rahmenfensters gescheitert. BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // TODO: Ändern Sie hier die Fensterklasse oder die Darstellung, // indem Sie CREATESTRUCT cs modifizieren. return TRUE; } Diese Methode wird automatisch vor Anpassung des eigentlichen Windows-Fenster aufgerufen185. Dabei wird der Methode die Adresse auf eine Strukturvariable vom Typ CREATESTRUCT übergeben: typedef struct tagCREATESTRUCT { LPVOID lpCreateParams; // Fensterdaten HANDLE hInstance; // HMENU hMenu; // Menue des Rahmenfensters HWND hwndParent; // uebergeordnetes Fenster int cy; // Hoehe int cx; // Breite int y; // y-Koordinate des oberen Rands int x; // x-Koordinate des linken Rands LONG style; // Fensterstil LPCSTR plszName; // Fenstername LPCSTR lpszClass; // Fensterstruktur DWORD dwExStyle; // erw. Fensterstil } CREATESTRUCT; In PreCreateWindow() kann auf einzelne Elemente dieser Struktur zugegriffen werden, z.B. Anpassen von Position und Größe des Hauptfensters 1. Anlegen einer neuen SDI-Anwendung mit Unterstützung für Doc/View durch den MFCAnwendungsassistenten. 2. Ausführen des Programms; Beobachten von Position und Größe des Fensters 3. Expandsion des Knoten CMainFrame in der KLASSEN-Ansicht, Doppelklick auf die Methode CreateWindow() 4. Den Koordinaten x und y für die obere linke Ecke des Fensters sollen neue Werte zugewiesen werden. Den Elementen cx und cy, die Höhe und Breite des Fensters bestimmen, sollen ebenfalls neue Werte zugewiesen werden. BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; // ZU ERLEDIGEN: Ändern Sie hier die Fensterklasse oder das // Erscheinungsbild, indem Sie CREATESTRUCT cs modifizieren. cs.x = 100; cs.y = 100; cs.cx = 400; cs.cy = 100; return TRUE; } 185 Kurz bevor das Anwendungsgerüst intern die Methode Create() aufruft. 415 Die Programmiersprache C++ OnCreate() zeigt die Erstellung der Symbolleiste, der Statusleiste und den Anweisungsblock, der sich mit der Andockbarkeit der Symbolleiste beschäftigt: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Symbolleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Statusleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } // TODO: Löschen Sie diese drei Zeilen, wenn Sie nicht möchten, // dass die Systemleiste andockbar ist m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; } Hier ist offensichtlich der Platz zur Vornahme von Veränderungen, z.B.: 1. Keine andockbare Symbolleiste Wie durch den Kommentar angegeben, müssen die drei letzten Anweisungen (vor dem return) gestrichen werden. Die Symbolleiste hängt danach fest. 2. Keine Symbolleiste Die Erzeugung wird gestrichen if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Symbolleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } 3. Keine Statusleiste, jedoch mit Symbolleiste 416 Die Programmiersprache C++ if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Statusleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } 4. Keine Statusleiste, keine Symbolleiste und vor allem kein Menü. Das Handle auf das Menü war cs.hMenu, die richtige Funktion ist CMainFrame::PreCreateWindow(...) BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { if (cs.hMenu != NULL) { ::DestroyMenu(cs.hMenu); // Geladenes Menü entfernen cs.hMenu = NULL; // Rahmenfenster hat kein Menü } if( !CFrameWnd::PreCreateWindow(cs) ) return FALSE; return TRUE; } 5. Schließen per Mausklick unterbinden (nach dem Wiederherstellen des Fensters in ursprünglicher Gestalt). Der richtige Ort für das Deaktivieren von "X" im Systemmenü ist die Funktion CMainFrame::OnCreate(): CMenu* pSystemMenu = GetSystemMenu(FALSE); pSystemMenu->EnableMenuItem(SC_CLOSE, MF_GRAYED); Mit der Funktion CMenu* CWnd :: GetSystemMenu(BOOL bRevert) const wird ein Zeiger auf das System-Menü-Objekt beschafft. Der Parameter muß FALSE gesetzt sein, damit ein Zeiger auf eine manipulierbare Kopie von CMenu erhalten wird. Mit dem Zeiger können verschiedene Funktionen der Klasse CMenu angewendet werden. Im aktuallen Beispiel wurde UINT CMenu::EnableMenuItem(UINT nIDEnableItem, UINT nEnable) verwendet. Wichtige Parameter zu UINT nIDEnableItem: SC_CLOSE SC_MAXIMIZE SC_MINIMIZE "X" Maximiert das Fenster Minimiert das Fenster 417 Die Programmiersprache C++ Wichtige Parameter zu UINT nEnable: MF_BYCOMMAND MF_BYPOSITION MF_DISABLED MF_ENABLED MF_GRAYED zeigt an, daß der erste Parameter durch seine ID angezeigt wird zeigt an, daß der erste Parameter durch seine Position angezeigt wird Deaktiviert den Menüpunkt Aktiviert den Menüpunkt Deaktiviert den Menüpunkt (Farbe des Strings: hellgrau) 6. Entfernen der Minimize- und Maximize-Box über die Window-Styles in PreCreateWindow(...) cs.style &= ~WS_MINIMIZEBOX; cs.style &= ~WS_MAXIMIZEBOX; Damit werden die Menüeinträge "Minimieren" und "Maximieren" auf hellgrau und inaktiv gesetzt. 2. Symbolleiste Symbole der Symbolleiste stellen z.B. Werkzeuge und Hilfsmittel dar (Papier, Ordner, Diskette, Schere, Drucker, etc.). Das dritte Symbol "Diskette" steht bspw. für Speichern. Bilder, Icons und Symbole sind Ressourcen. Die Standard-Symbolleiste wird über die Ressource IDR_MAINFRAME angesprochen) unter dem Ordner Toolbar in der RessourcenAnsicht). 418 Die Programmiersprache C++ 3. Statusleiste In der vorliegenden Anwendung wird automatisch eine Statusleiste erzeugt. Die dazugehörigen Bausteine enthält die Klasse CMainFrame: CStatusBar m_wndStatusBar; static UINT indicators[] = { ID_SEPARATOR, ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, }; // Statusleistenanzeige if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Statusleiste konnte nicht erstellt werden\n"); return -1; // Fehler bei Erstellung } Das Objekt "Statusleiste" ist eine Instanz der Klasse CStatusBar. In der Klasse CMainFrame wird dieses Objekt als Member-Variable definiert. In der Datei MainFrm.cpp befindet sich das globale statische UINT-Array indicators. In der Klasse CMainFrame::OnCreate(...) wird die Status-Leiste mit CStatusBar::Create(...), und die Funktion CStatusBar::SetIndicators(...) ordnet das Array indicators der Statusleiste zu. Einen weiterer Beitrag zur Statusleiste befindet sich unten im Hauptfenster: 419 Die Programmiersprache C++ Dort befindet sich der String "Bereit". Dieser Text befindet sich in der String Table unter der Bezeichnung AFX_IDS_IDLEMESSAGE. Unmittelbar danach befinden sich in der String Table die Indikatorbereiche. Die Reihenfolge der Indikatorbereiche stimmt mit der Reihenfolge der Definition im Array überein. Für die Ausgabe der Befehlsinformation ist der Eintrag ID_SEPERATOR zuständig. Jede Statusleiste besteht aus sog. "panes". Das sind rechteckige Bereiche, in denen Man Informationen wie z.B. Texte ausgeben kann. Die Zählung der "panes" beginnt links und startet bei 0. Der Text "Bereit" steht also in "pane" 0. 4. Das Anwendungssymbol Unter Windows verfügt jedes Programm über ein Symbol (Icon) zur grafischen Präsentation. Windows verwendet dieses Symbol in verschiedenen Kontexten (Titel des Hauptfensters, Anzeige im Explorer, Task-Leiste). Aufgabe jeder Windows-Anwendung ist es daher, ein entsprechendes Symbol bereitzustellen. 5. Initialisierung der Anwendung Die Funktion InitInstance() startet das SDI-Programm. Dort befindet sich eine Sammlung diverser Vorgänge: 6.5.4.3 Bearbeitung von Kommandozeilenargumenten Die MFC sieht für bestimmte Kommandozeilen-Argumente eine Standardverarbeitung vor, die mit wenigen Methodenaufrufen implementiert werden kann. Die von der MFC standardmäßig unterstützten Kommandozeilenargumente werden automatisch in der ParseParam()-Methode der CCommandLineInfoKlasse ausgewertet und können über einen Aufruf der CWinApp-Methode ProcessShellCommand() ihrer Verarbeitung zugeführt werden. Zum Einlesen und zur Abfrage der Kommandozeilenargumente sind in der MFC die Klasse CCommandLineInfo und die Methode ParseCommandLine() vorgesehen. Letztere ruft zur Verarbeitung der Kommandozeile die CCommandLineInfoMethode ParseParam() auf, die man zur Verarbeitung eigener Kommandozeilenargumente überschreiben kann. Bsp.: Unterstützung Kommandozeilenargumente. Im folgenden Programm soll das Hauptfenster je nach Kommandozeilenargument in normaler Größe (kein Argument), minimiert (-min) oder maximiert (-max) angezeigt werden. 1. Anlegen einer neuen SDI_Anwendung mit Hilfe des anwendungsassistenten und Doc/ViewUnterstützung. 420 Die Programmiersprache C++ Zum Überschreiben des CCommandLineInfo-Methode ParseParam() ist eine eigene Klasse von CCommandLineInfo abzuleiten. Der Einfachheit halber wird dafür keine eigene Datei angelegt, sondern die Dateien des Anwendungsgerüsts herangezogen. 2. Öffnen der Header-Datei, in der die Anwendungsklasse deklariert ist 3. Deklaration der abgeleiteten Klasse über die CCommandLineInfo-Klasse, in der die Methode ParseParam() deklariert ist. // Pr64330.h : Haupt-Header-Datei für die Anwendung PR64330 // Meine_CCommandLineInfo class Meine_CCommandLineInfo:public CCommandLineInfo { public: virtual void ParseParam(const char* pszParam, BOOL bFlag, BOOL bLast); }; 4. Öffnen der zugehörigen Quelltextdatei, implementiere hier die überschriebene Methode. ///////////////////////////////////////////////////////////////////////// // void Meine_CCommandLineInfo::ParseParam(const char* pszParam, BOOL bFlag, BOOL bLast) { // Datenelement des Anwendungsobjekts setzen if (lstrcmp(pszParam,"min") == 0) AfxGetApp()->m_nCmdShow = SW_SHOWMINIMIZED; else if (lstrcmp(pszParam,"max") == 0) AfxGetApp()->m_nCmdShow = SW_SHOWMAXIMIZED; CCommandLineInfo::ParseParam(pszParam,bFlag,bLast); } 5. Scrollen in der Datei nach unten, bis in der InitInstance()-Methode die Zeile mit der Anweisung „CCommandLineInfo cmdInfo;“ erreicht ist. „CCommandLineInfo“ ist durch „Meine_CCommandLineInfo“ zu ersetzen. 6. Scrollen bis zum Ende der InitInstance()-Methode. Hier gibt es den Aufruf der Methode ShowWindow(), der standardmäßig das Argument SW_SHOW übergeben wird. Ersetze das Argument SW_SHOW durch die Elementvariable m_nCmdShow. 7. Ausführen des Programms. Test der verschiedenen Kommandozeilenargumente. Zur Übergabe von Kommandozeilenargumente im Debugger wechsle über PROJEKT/EINSTELLUNGEN zum Dialogfenster PROJEKTEINSTELLUNGEN und gib Kommandozeilenargumente in das Eingabefeld PROGRAMMARGUMENTE ein. 421 Die Programmiersprache C++ Angabe von Kommandozeileanargumenten über den Debugger 6.5.4.4 Die Nachricht WM_PAINT Die WM_PAINT-Nachricht wird von Windows an Fenster geschickt, um mitzuteilen, dass sich das Fenster neu zeichnen soll. Das ist bspw. der Fall, wenn ein Fenster vom Anwender aus dem Hintergrund wieder in der Vordergrund gehoben wird. Windows kann zwar den Fensterrahmen rekonstruieren, nicht aber den im ClientBereich des Fensters angezeigten Text oder etwaige Grafiken. Es schickt daher eine WM_PAINT-Nachricht an das betreffende Fenster. Aufgabe des Programmierers ist es, die Nachricht abzufangen und mit der Funktion zu verbinden, die den Fensterinhalt rekonstruiert: - in MFC-Anwendungen ohne Doc/View-Unterstützung behandelt man die WM_PAINT-Nachricht in einer OnPaint()-Behandlungsmethode. in MFC-Anwendungen mit Doc/View wird die Nachricht von der Ansichtsklasse der MFC automatisch abgefangen und in der Methode OnDraw()186 behandelt. Diese Methode muß in der abgeleiteten Klasse überschrieben werden. Der Anwendungsassistent tut dies automatisch beim Anlegen des Anwendungsgerüsts. a) Ausserhalb von OnDraw() zeichnen. Bsp.: Zeichnen von einem Kreis auf einen Mausklick 1. Anlegen eines neuen Projekts (SDI-Anwendung mit Doc/View-Unterstüzung) 186 Intern wird in den Ansichtsklassen des Doc/View-Gerüsts die WM_PAINT-Nachricht von einer OnPaint()Methode abgefangen und bearbeitet. In dieser OnPaint()-Methode werden einige für das Zeichnen benötigte Vorarbeiten erledigt, dann wird die Methode OnDraw() aufgerufen, in die der Code eingefügt wird. 422 Die Programmiersprache C++ 2. Aufruf des Klassen-Assistenten, Einrichten einer Behandlungsroutine für die WM_LBUTTONDOWN-Nachricht. 3. Einsetzen des Quellcodes in die Behandlungsroutine OnLButtonDown() // Cpr65440View Meldungshandler void Cpr65440View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, // und/oder benutzen Sie den Standard. CClientDC dc(this); // Besorgen Geraetekontext dc.Ellipse(point.x-20,point.y-20,point.x+20,point.y+20); CView::OnLButtonDown(nFlags, point); } 4. Ausführen des Programms b) Innerhalb von OnDraw() zeichnen. Bsp.: Zeichnen eines Kreises 1. Anlegen eines neuen Projekts mit dem MFC-Anwendungs-Assistenten (SDI-Anwendung mit Doc/View-Unterstützung) 2. Für die Ansichtsklasse wird die WM_PAINT Nachricht bereits vom Anwendungsgerüst abgefangen und mit dem Aufruf der Methode OnDraw() verbunden (Expansion des Knotens Ansichtsklasse, Doppelklick auf Eintrag OnDraw()) 3. Einsetzen des Quellcodes für die Behandlungsmethode OnDraw() // Cpr65442View-Zeichnung void Cpr65442View::OnDraw(CDC* pDC) { Cpr65442Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen pDC->Ellipse(50,50,70,70); } 4. Ausführen des Programms Da hinter dem Doc/View-Modell die Trennung von Datenverwaltung und Datenanzeige steht, ist die Ansichtsklasse üblicherweise gezwungen, sich die anzuzeigenden Daten vom zugehörigen Dokument zu besorgen (Zeiger pDoc mit Zugriff auf public-Methoden) Bsp.: Kreise an Mausklickposition zeichnen 1. Anlegen einer neuen SDI-Anwendung mit Unterstützung für Doc/View durch den MFCAnwendungsassistenten 2. Deklarieren eines int-Zählers m_nZaehler in der Dokumenten-Klasse und eines CPoint-Array für 100 Kreismittelpunkte class Cpr65442Doc : public CDocument { protected: // Nur aus Serialisierung erstellen Cpr65442Doc(); DECLARE_DYNCREATE(Cpr65442Doc) // Überschreibungen public: virtual BOOL OnNewDocument(); 423 Die Programmiersprache C++ virtual void Serialize(CArchive& ar); // Implementierung public: virtual ~Cpr65442Doc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generierte Funktionen für die Meldungstabellen protected: DECLARE_MESSAGE_MAP() public: int m_nZaehler; CPoint m_Kreise[100]; }; 3. Initialisieren der Elementvariablen m_nZaehler im Konstruktor der Dokumentenklasse mit dem Wert 0. // Cpr65442Doc Erstellung/Zerstörung Cpr65442Doc::Cpr65442Doc() : m_nZaehler(0) { // TODO: Hier Code für One-Time-Konstruktion einfügen } 4. Einrichten einer Nachrichtenbehandlungsmethode für WM_LBUTTONDOWN in der Klasse des Ansichtsfensters mit Hilfe des Klassenassistenten zum Speichern der Kreismittelpunkte // Cpr65442View Meldungshandler void Cpr65442View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, // und/oder benutzen Sie den Standard. CPr64342Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CClientDC dc(this); int b = (int) (100.0 * rand() / RAND_MAX); dc.Ellipse(point.x-b,point.y-b,point.x+b,point.y+b); if (pDoc->m_nZaehler < 100) pDoc->m_nZaehler++; pDoc->m_Kreise[pDoc->m_nZaehler - 1] = point; CView::OnLButtonDown(nFlags, point); } 5. Rekonstruktion der Kreise mit der OnDraw()-Methode // Cpr65442View-Zeichnung void Cpr65442View::OnDraw(CDC* pDC) { Cpr65442Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: Code zum Zeichnen der systemeigenen Daten hinzufügen for (int i = 0;i < pDoc->m_nZaehler;i++) { int b = (int)(100.0 * rand() / RAND_MAX); pDC->Ellipse(pDoc->m_Kreise[i].x - b, pDoc->m_Kreise[i].y - b, pDoc->m_Kreise[i].x + b, 424 Die Programmiersprache C++ pDoc->m_Kreise[i].y + b); } pDC->Ellipse(50,50,70,70); } Das Anstoßen des Neuzeichnens des Fensters kann auch durch Selbstauslösung einer WM_PAINTNachricht (Aufruf der CWnd-Methode UpdateWindow()) erfolgen. Zur Sicherheit sollte man zuvor den Fensterinhalt als ungültig erklären. void Cpr65442View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Fügen Sie hier Ihren Meldungsbehandlungscode ein, // und/oder benutzen Sie den Standard. Cpr65442Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CClientDC dc(this); int b = (int) (100.0 * rand() / RAND_MAX); dc.Ellipse(point.x-b,point.y-b,point.x+b,point.y+b); if (pDoc->m_nZaehler < 100) pDoc->m_nZaehler++; pDoc->m_Kreise[pDoc->m_nZaehler - 1] = point; Invalidate(); UpdateWindow(); CView::OnLButtonDown(nFlags, point); } Man kann dem Anwender auch die Möglichkeit geben, den Fensterinhalt zu löschen: 1. Laden der Menü-Ressource IDR_MAINFRAME in den Menü-Editor 2. Löschen aller Menübefehle bis auf DATEI/NEU und DATEI/BEENDEN 3. Überschreiben der Methode DeleteContents() in der Dokumentenklasse void Cpr65442Doc::DeleteContents(void) { // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen m_nZaehler = 0; CDocument::DeleteContents(); } 6.5.4.5 Zeitgeber (WM_TIMER) Mit Hilfe der Nachricht WM_TIMER kann eine Art Zeitgeber implementiert werden, der ein Programm in immer gleichen Zeitabständen benachrichtigt. Ein Timer ist eine interne Routine, die Windows dazu veranlasst in bestimmten Zeitintervallen die Nachricht WM_TIMER zu senden. Das Zeitintervall wird durch den time_out-Wert bestimmt. Ein time_out ist eine Zeitspanne, die vom Timer in Millisekunden gemessen wird. In Windows muß der Timer mit Hilfe der Member-Funktion SetTimer() gesetzt werden. Danach muß mit Hilfe des Klassenassistenten eine Nachrichtenfunktion für die Nachricht WM_TIMER erzeugt werden. In dieser Funktion liegt die Anweisung, die in bestimmten Zeitintervallen wiederholt werden sollte. 425 Die Programmiersprache C++ 6.6 Bilder, Zeichnungen und Bitmaps 6.6.1 Die grafische Geräteschnittstelle (GDI) Gerätekontexte Bevor man irgendeine Grafik erstellen kann, muß man über den Gerätekontext verfügen, in dem die Grafiken angezeigt werden. Der Gerätekontext enthält Informationen über das System, die Anwendung und das Fenster, in dem die Ausgabe der Zeichnungen und Bilder erfolgt. Das Betriebssystem entnimmt dem Gerätekontext, in welchem Kontext eine Grafik zu zeichnen ist, wie groß der sichtbare Bereich ist, und wo sich der Zeichenbereich momentan auf dem Bildschirm befindet. Eine Grafikausgabe erfolgt im Kontext eines Anwendungsfensters aus. Dieses Fenster kann jederzeit als Vollbild, minimiert, teilweise überdeckt oder vollständig unsichtbar sein. Windows verwaltet alle Gerätekontexte und ermittelt daraus, wieviel und welcher Teil der gezeichneten Grafiken tatsächlich für den Benutzer anzuzeigen ist. Die meisten Funktionen in bezug auf Zeichnungen und Bilder führt der Gerätekontext mit zwei Ressourcen aus: Stift und Pinsel. Der Gerätekontext verwendet Stifte, um Linien und Figuren zu zeichnen, während Pinsel die Flächen auf dem Bildschirm ausfüllen. Die Gerätekontextklassen In Visual C++ stellt die MFC-Gerätekontextklasse (CDC) zahlreiche Zeichenfunktionen für Kreise, Quadrate, Linien, Kurven usw. bereit. Die Instanz einer Gerätekontextklasse erstellt man mit einem Zeiger auf die Fensterklasse, die man mit dem Gerätekontext verbinden möchte. Damit läßt sich in den Konstruktoren und Destruktoren der Gerätekontextklasse der gesamte Code unterbringen, der sich mit der Zuweisung und Freigabe eines Gerätekontextes befaßt. Gerätekontextobjekte gehören genau wie alle Arten von Zeichenobjekten zu den Ressourcen im Betriebssystem Windows. Das Betriebssystem verfügt nur über eine begrenzte Zahl dieser Ressourcen.187 Es empfiehlt sich also, die Ressourcen in Funktionen zu erzeugen, wo sie zum Einsatz kommen, und sie sobald wie möglich wieder freizugeben, wenn die Arbeit mit den betreffenden Ressourcen abgeschlossen ist. Dementsprechend nutzt man Gerätekontexte und deren Zeichenressourcen fast ausschließlich als lokale Variablen innerhalb einer einzigen Funktion. Die einzige echte Ausnahme liegt vor, wenn das Gerätekontextobjekt von Windows erzeugt und in eine ereignisverarbeitende Funktion als Argument übergeben wird. Die MFC stellt für die verschiedenen Ausgabegeräte spezielle Klassen bereit: 187 Obwohl die Gesamtzahl der Ressourcen in den neueren Versionen von Windows recht groß ist, kann dennoch ein Mangel an Ressourcen entstehen, wenn eine Anwendung die Ressourcen zwar reserviert, aber nicht wieder korrekt freigibt. Diesen Verlust bezeichnet man als Ressourcenlücke, die analog zu einer Speicherlücke das System des Benutzer blockieren oder lahmlegen kann. 426 Die Programmiersprache C++ Klasse CDC CPaintDC CClientDC CWindowDC Ausgabeeinheit Basisklasse aller Gerätekontextklassen mit reicher Auswahl an Zeichen- und Ausgabeoperationen. Spezieller Fenster-DC zur Implementierung von Behandlungsmethoden zur WM_PAINT-Nachricht. Die Fensteklassen der MFC implementieren standardmäßig bereits OnPaint()-Methoden zur Behandlung der WM_PAINT-Nachricht. In dieser wird eine CPaintDC-Instanz gebildet und an die virtuelle Methode OnDraw() übergeben, die zur Ausgabe überschrieben werden soll. Gerätekontext für die Ausgabe in den Client-Bereich eines Fensters Gerätekontext für den gesamten Bereichs eines Fensters (einschl. Rahmen, Titel). Abb.: Gerätekontextklassen für Fenster Windows speichert keine Informationen über Fensterinhalte. Das bedeutet: Bei bestimmten Fensteroperationen (z.B. Verkleinern, Vergrößern, Minimieren, in den Vordergrund holen) kann der Fensterinhalt nicht in Windows rekonstruiert werden. Windows benutzt in solchen Fällen die Nachricht WM_PAINT zur Information an das Fenster, seine Inhalte selbst zu zeichnen. Das MFC-Anwendungsgerüst fängt diese Nachricht ab und ruft die virtuelle Methode OnDraw() auf. In OnDraw() gehören alle Anweisungen zur Rekonstruktion (üblicherweise der gesamte Text und Grafikausgabe) des Fensters. - Für Zeichenausgabe in OnDraw() übergibt die MFC einen passenden Gerätekontext als Argument an OnDraw(). Für Zeichenausgaben an andere Methoden muß ein Gerätekontext als Instanz einer passenden Gerätekontextklasse erzeugt werden. Die Zeichenwerkzeuge CBitMap CBrush CFont CPalette CPen CRgn für Pinsel-Objekte für Schriftarten für Paletten (von 256 Farben) für Stiftobjekte für Zeichenbereiche Jeder Gerätekontext ist standardmäßig mit einem Satz vordefinierter GDI-Objekte ausgestattet. Falls bspw. ohne Laden eines Pinsels oder Stfts in den Gerätekontext (eine Linie oder ein Rechteck) gezeichnet wird, dann wird für Linie und Rahmen des Rechtecks das Standard-Stiftobjekt verwendet, das dünne schwarze Linien zieht. Ausgemalt wird das Rechteck mit dem Standardpinsel, der weiß und ohne Muster ist. Anderenfalls muß man CDC-Methoden mit Farben und Pinsel als Argumente übernehmen, die GDI-Objekte des Gerätekontexts ersetzen. Die Zeichenmethoden Die eigentlichen Zeichenmethoden werden mit Hilfe der Methoden des Gerätekontexts ausgeführt. In der Basisklasse für die Gerätekontexte (CDC) sind zahlreiche Methoden zum Zeichnen definiert. Diese Methoden können auch in den abgeleiteten Gerätekontextklassen (CPaintDC und CClientDC) verwendet werden. 427 Die Programmiersprache C++ Methode Linien CPoint MoveTo(int x, int y); CPoint MoveTo(POINT point); Ausgabeeinheit Die Linienfunktionen nutzen das Konzept der „aktuellen Zeichenposition“. Mit dieser Methode kann die aktuelle Zeichenposition auf eine bestimmte Koordinate im Gerätekontext gesetzt werden. Die Methode legt den Anfangspunkt einer Linie fest und liefert die letzte aktuelle Zeichenposition zurück. Zeichnet eine Linie von der aktuellen zeichenposition bis zu der als Argument übergebenen Koordinate. Die übergebene Koordinate ist danach die neue aktuelle Zeichenposition. BOOL LineTo(int x, int y); BOOL LineTo(POINT point); Bögen BOOL Arc(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL Arc(lpRect, Point ptStart, POINT ptEnd); Rechtecke void Draw3dRect(LPCRect lpRect, COLORREF clrTopLeft, COLORREF clrBottomRight); void Draw3dRect(int x, int y, int cx, int cy, COLORREF clrTopLeft, COLORREF clrBottomRight); void FillRect(LPCRECT lpRect, CBrush* pBrush); Zeichnet einen elliptischen Bogen. Der Bogen wird spezifiziert durch ein umgebendes Rechteck, sowie einen Anfangs- und Endpunkt (die nicht unbedingt auf dem Bogen liegen müssen) Position und Abmessung des zu zeichnenden Rechtecks werden durch Angabe der oberen linken Ecke sowie Breite und Höhe oder durch Übergabe eines CRect- oder Rect-Objekts spezifiziert. Zeichnet ein dreidimensionales Rechteck. Übergeben wird ein umgebendes Rechteck, die Farbe für den oberen und linken Teil des Rahmens und die Farbe für den unteren und rechten Teil des Rahmens Zeichnet ein Rechteck und füllt es in Farbe und dem Muster des übergebenen Pinsel-Objekts. Der linke und obere Rahmen werden ebenfalls eingefärbt. Zeichnet ein Rechteck und füllt es in der angegebenen Farbe void FillSolidRect(LPCRECT lpRect, COLORREF clr); void FillSolidRect(int x, int y, int cx, int cy, COLORREF clr); Zeichnet eine Rahmen und das angegebene void FrameRect(LPCRECT lpRect, CBrush* pBrush); Rechteck. Das Pinselobjekt bestimmt farbe und Muster des Rahmens. Zeichnet ein Rechteck. Der Rahmen wird mit dem BOOL Rectangle(int x1, int y1, int x2, int y2); Stift-Objekt, der Inhalt des Rechtecks mit dem Pinsel-Objekt des Gerätekontexts gezeichnet. BOOL Rectangle(LPRECT lpRect); Zeichnet ein Rechteck mit abgerundeten Ecken. BOOL RoundRect(int x1, int y1, int x2, int y2, int x3, int y3); Die Abrundung wird durch Breite und Höhe einer Ellipse definiert. BOOL RoundRect(LPRECT lpRect, POINT point); Kreise Zeichnet ein Ellipsensegmet (Schnittfigur einer BOOL Chord(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); Ellipse mit einer Linie). Übergeben werden das umschließendeRechteck sowie Startund BOOL Chord(LPCRECT lpRect, POINT ptStart, endpunkt der schneidenden Linie POINT ptEnd); Zeichnet eine Ellipse, die das übergebene BOOL Ellipse(int x1, int y1, int x2, int y2); Rechteck ausfüllt. Ist das Rechteck ein Quadrat, BOOL Ellipse(LPRect lpRect); dann entsteht ein Kreis. Zeichnet ein Tortenstück aus einer Ellipse. BOOL Pie(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); Übergeben werden das umschließende Rechteck der Ellipse sowie Start- und Enpunkt des Bogens. BOOL Pie(LPRECT lpRect, POINT ptStart, POINT ptEnd); Polygone 428 Die Programmiersprache C++ BOOL Polygon(LPPoint lpPoints, int nCount); Zeichnet ein Polygon. Übergeben werden die Punkte, die zu verbinden sind. Der letzte Punkt wird automatisch mit dem ersten Punkt verbunden Abb.: Auswahl an Zeichenmethoden Das RGB-Modell Die meisten Methoden, denen man Farbe übergeben kann, erwarten einen COLORREF-Wert, z.B. der Konstruktor für das Pinselobjekt CBrush(COLORREF crColor). Hinter COLORREF verbirgt sich ein 32-Bit-Wert, der eine Farbe nach dem RGB-Modell spezifiziert. In einem COLORREF-Wert kodiert das unterste Byte den Rotanteil, das zweite Byte den Grünanteil und das dritte Byte den Blauanteil. 429 Die Programmiersprache C++ 6.7 Die Sammlungsklassen der MFC Es gibt vorlagenbasierte (Template Based) und nicht-volagenbasierte188 Sammlungsklassen. Die Sammlungsklassen bestehen aus Listenklassen (Lists), Datenfeldklassen (Arrays) und Zuordnungsklassen (Maps. Dictionaries). Vorlagenbasierte Sammlungsklassen können nach Einfügen von #include <afxtempl.h> in die Datei stdAfx.h benutzt werden. Die vorlagenbasierte Sammlungsklasse CArray template< class TYPE, class ARG_TYPE > class CArray : public CObject TYPE ist der von CObject abgeleitete Typ des Objektes, der in CArray gespeichert wird. ARG_TYPE ist der Typ, mit dem auf die Objekte zugegriffen wird. Ein praktisches Beispiel kann wie folgt aussehen: CArray<Student, Student&> StudArray; Student studi; StudArray->Add( studi ); Im obigen Beispiel werden Objekte des Typs Student in CArray gespeichert. Der Zugriffstyp ist eine Referenz auf die Klasse Student selbst. Add() ist eine MemberFunktion der Klasse CArray, mit deren Hilfe ein neues Objekt in die Sammlungsklasse gespeichert wird. Die vorlagenbasierte Sammlungsklasse CTypedPtrArray template<class BASE_CLASS, class TYPE> class CTypedPtrArray : public BASE_CLASS Der erste Parameter BASE_CLASS ist die Basiskasse für das Feld von Zeigern. Dies muß eine Datenfeldklasse sein. Da Sie diese Klasse bestimmt zum Serialisieren benutzen, kommt als Basisklasse für CTypedPtrArray nur die Datenfeldklasse CObArray in Frage. TYPE kennzeichnet den Typ der Elemente, die in CObArray gespeichert werden. Ein praktisches Beispiel könnte folgendermaßen aussehen: CTypedPtrArray<CObArray, Student*> studi; for(int i=0; i<studi.GetSize(); i++) { Student* st= studi.GetAt(i);} Mit der Member-Funktion GetSize() wird die Anzahl der Elemente ermittelt und mit der Member-Funktion GetAt() werden die Elemente gelesen. Ein gewichtiges Argument für die Benutzung dieser Klasse, oder der Klasse CTypedPtrList ist darin begründet, daß diese beiden Klassen typsicher sind. Bsp.: Objekte der im vorhergehenden Abschnitt angegebenen eigenen Klasse Student sollen in der vorlagenbasierten MFC-Klasse CArray gelagert werden. Es ist praktisch am Ende der Header-Datei der eigenen Klasse Student Student.h zu diesem Zweck, die folgende Anweisung anzugeben: typedef CArray <Student*, Student*> StudentList StudentList ist der neue Typ der Daten, der in einer SDI-Anwendung direkt für die Trennung von Dokument und Ansicht benutzt werden kann. Damit die Anwendung 188 Stammen aus früheren Versionen der MFC-Bibliothek 430 Die Programmiersprache C++ der vorlagenbasierten MFC-Sammlungsklasse erkennen kann, muß in stdAfx.h die Anweisung #include <afxtempl.h> stehen. In die Header-Datei der Ansicht können dann folgende Deklarationen aufgenommen werden: // Attribute public: CPr64420Doc* GetDocument(); Student* st; // fuer den Zugriff auf Member-Funktionen von Student int c; // Laufvariabble zur Navigation zwischen Datensaetzen BOOL info; BOOL loesch; // noetig zur Befehlsaktuaktualisierung BOOL naechst; // noetig zur Befehlsaktualisierung // Operationen public: void leseStudent(); // liest Eingaben und transportiert sie ins Dokument void zeigeStudent(Student *); // Ausgabe der Daten eines Student-Objekts auf den Bildschirm Die Implementierungen von leseStudent() und zeigeStudent() sind: void CPr64420View::leseStudent() { CPr64420Doc* pDoc = GetDocument(); if(st=!NULL) { UpdateData(TRUE); st=new Student; st->setName(m_name); st->setMatrik(m_matrik); st->setGruppe(m_gruppe); pDoc->insertStudent(st); m_name = ""; m_matrik = ""; m_gruppe = ""; UpdateData(FALSE); (((CEdit*)GetDlgItem(IDC_NAME)->SetFocus())); } } void CPr64420View::zeigeStudent(Student* aktStudent) { m_name = aktStudent->getName(); m_matrik = aktStudent->getMatrik(); m_gruppe = aktStudent->getGruppe(); UpdateData(FALSE); } Die Header-Datei des Dokuments kann folgende Deklaration aufnehmen: // Attribute public: int anzahl; // Index des Datenfelds StudentList* starray; // Zeiger auf (den Typ der) Datensaetze // Operationen public: // Einfuegen eines Datensatzes in die Auflistung StudentList void insertStudent(Student*); // Ansprache der Datensaetze in der Auflistung StudentList* getList() { return starray; } 431 Die Programmiersprache C++ Die Implementierung im der Funktion insertStudent() sieht im Dokument folgendermaßen aus: void CPr64420Doc::insertStudent(Student* s) { starray->Add(s); SetModifiedFlag(); } Die Serialisierung der Datensätze findet in der Funktion Serialize() des Dokuments statt: void CPr64420Doc::Serialize(CArchive& ar) { anzahl = 0; if (ar.IsStoring()) { // ZU ERLEDIGEN: Hier Code zum Speichern einfügen anzahl = starray->GetSize(); //Anzahl der Datensätze. ar << anzahl; for(int i=0;i<starray->GetSize();i++) { ar << starray->GetAt(i); } } else { // ZU ERLEDIGEN: Hier Code zum Laden einfügen Student* s; ar >> anzahl; for(int i = 0;i < anzahl; i++) { ar >> s; starray->Add(s); } } } 432 Die Programmiersprache C++ 6.6.2 Die Zeichenwerkzeuge GDI-Objekte Nachdem ein Gerätekontext vorliegt, wird ein Zeichenwerkzeug benötigt, z.B. ein Zeichenstift, ein Pinsel oder auch eine Schriftart für eine Textausgabe. Diese Zeichenwerkzeuge werden GDI-Objekte genannt. Jedem GDI-Objekt entspricht eine eigene Klasse, z.B. CPen, CBrush. GDI-Objekte werden bei der Instanzenbildung konfiguriert und danach in den Gerätekontext geladen, wobei von jeder Art GDIObjekt genau eine Instanz in einem Gerätekontext vorhanden ist. Wird in den Gerätekontext gezeichnet, dann wird, je nachdem welche Zeichenoperation (CDC-Methoden) aufgerufen werden, automatisch das entsprechende Zeichenwerkzeug benutzt. Die Stiftklasse CPen für Stiftobjekte Die Pinselklasse CBrush für Pinselobjekte. Die Klasse CFont für Schriftarten. Das aktuelle Font-Objekt des Gerätekontextes wird für Textausgaben (CDC-Methode TextOut()) benutzt. Die Klasse CPalette für Paletten mit 256 Farben. Die Klasse CRgn für Zeichenbereiche. Jeder Gerätekontext ist standardmäßig mit einem Satz vordefinierter GDI-Objekte ausgestattet (sog. „Stock-Objects“). Beim Zeichnen einer Linie oder eines Rechtecks wird (ohne spezifisch geladenen Stift oder Pinsel) ein Standard-Stiftobjekt verwendet, das dünne schwarze Linien zieht. Ausgemalt wird das Rechteck mit dem Standardpinsel, der weiß und ohne Muster ist. Falls diese Vorgaben nicht zusagen, dann müssen - CDC-Methoden verwendet werden, die Farben oder Pinsel-Objekte als Argumente übernehmen die GDI-Objekte des Gerätekontexts ersetzt werden. Die Klasse CBitmap Es gibt verschiedene Möglichkeiten, um Bilder in einer Anwendung anzuzeigen. Feststehende Bitmaps nimmt man als Ressourcen mit zugewiesenen Objekt-IDs in die Anwendung auf und zeigt sie mit statischen Bildsteuerelementen oder ActiveXSteuerelementen an. Man kann auch die Bitmap-Klasse CBitmap einsetzen, um die Anzeige der Bilder weitgehend beeinflussen zu können. Mit Hilfe der Bitmap-Klasse lassen sich Bitmaps dynamisch aus Dateien des Systemlaufwerks laden, die Bilder bei Bedarf in der Größe ändern und sie an den zugewiesenen Platz anpassen. Einrichten von GDI-Objekten Zum Laden eines GDI-Objekts in einen Gerätekontext, verwendet man die CDCMethode SelectObject(): CBrush roterPinsel(RGB(255,0,0)); pDC->SelectObject(&roterPinsel); Beim Laden eines GDI-Objekts in einen Gerätekontext (z.B. in ein Cpen-Objekt) wird das alte Stiftobjekt, das zuvor im Gerätekontext war, aus dem Gerätekontext entfernt. Handelt es sich um ein von Windows vordefiniertes Objekt, dann braucht 433 Die Programmiersprache C++ man sich um das Löschen dieses Objekts nicht zu kümmern. Handelt es sich um ein selbst definiertes Objekt, dann sollte es gelöscht werden. Das geschieht - entweder automatisch, wenn der Gerätekontext selbst aufgelöst wird oder indem man ein neues GDI-Objekt lädt, das alte GDI-Objekt überschreibt und damit löscht. Das RGB-Modell Die meisten Methoden, denen man Farben übergeben kann, erwarten einen COLORREF-Wert, bspw. auch der Konstruktor für das Pinselobjekt CBrush: CBrush(COLORREF crColor). Hinter COLORREF verbirgt sich ein 32 Bit-Wert, der eine Farbe nach dem RGBModell189 spezifiziert. In einem COLORREF-Wert kodiert das unterste Byte den Rotanteil, das zweite Byte den Grünanteil und das dritte Byte den Rotanteil. Abbildungsmodi und Koordinatensysteme Die Wahl der Koordinaten ist nicht immer beliebig. Es gibt eine Reihe von Funktionen, die jeweils nur mit logischen oder Gerätekoordinaten 190 arbeiten. Die MFC stellt Funktionen zur Umwandlung von Koordinaten bereit 191. Abbildungdmodi legen die Skalierung für die Größe der Grafik fest. Der voreingestellte Wert für die Koordinaten ist MM_TEXT. Hier entspricht die Einheit des Koordinatensystems einer Pixelgröße192. 189 Beruht auf dem Effekt, dass man durch Variation aus den drei Lichtfarben Rot, Grün und Blau sämtliche Farben mischen kann. 190 CDC-Menmber-Funktionen benutzen logische Koordinaten, CWnd-Funktionen benützen Gerätekoordinaten 191 in Gerätekontextklassen und der Klasse CWnd 192 Je nach Auflösung des Bildschirms und der Grafikkarte kann sich die Größe eines Pixels ändern. 434 Die Programmiersprache C++ 7. C# und .NET Bedeutung von C# für Visual Studio .NET C# („ßi scharp“) ist eine Neuentwicklung von Micosoft, ein Konkurrent zu Java und im Gegensatz zu C++ keine Erweiterung von C. C# wurde in .NET eingebettet und besitzt keine Standardbibliotheken. .NET enthält Klassenbibliotheken, die Programmierern in Visual Basic, JScript und Visual C++ bekannt vorkommen. C# kann als Mischung aus Java, C++ und Visual Basic bezeichnet werden. C# gehört zu den .NET-Programmiersprachen und ist objektorientiert. C# spielt im Visual Studio keine Sonderrolle. C# ist aber die erste Microsoft-Programmiersprache, die sich an dem ECMA-Standard hält. In der Praxis bedeutet dies, dass Fremdfirmen auf der Basis dieses Standards spezielle Tools und auch eigene Compiler für C# entwickeln können. Auch große Teile des .NET Frameworks sind in C# geschrieben. Daher kann wohl behauptet werden, dass C# zumimdest künftig die wichtigste der .NETProgrammiersprachen ist. Erstellen eines C#-Projekts Visual Studio ordnet den Quellcode in Projektmappen (Ordner, in denen ein oder mehrere Projekte gespeichert werden) an. Einstellungen der Projektmappe werden in Dateien mit der Endung .sln gespeichert. Beim Erzeugen eines neuen Projekts wird automatisch eine neue Projektmappe erstellt. 1. Schritt: Erzeugen eines neuen Projets "Quicksort-Anwendung". 1. Datei | Neu | Projekt 2. Setze C#-Projekte auf der rechten Seite und Konsolenanwendung auf der linken Seite 3. Spezifiziere unter Name: pr71200 und unter Speicherort das Verzeichnis, in dem das Projekt abgelegt werden soll. Das Projet wird nach einem Klick auf OK erzeugt und umfasst zwei Dateien: assemblyinfo.cs Class1.cs 435 Die Programmiersprache C++ Visual Studio .NET hat eine Lösung für ein einzelnes C# Sharp Projekt erzeugt. Es erscheint folgende Darstellung: 2. Schritt: Sündenfall "Hallo Welt". Quellcodemodifikation der vordefinierten Schablone Class1.cs 436 Die Programmiersprache C++ using System; namespace pr71200 { /// <summary> /// Zusammendfassende Beschreibung für Class1. /// </summary> class Class1 { /// <summary> /// Der Haupteinstiegspunkt für die Anwendung. /// </summary> [STAThread] static void Main(string[] args) { // // TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten Console.WriteLine("Hallo, C# .NET Welt!"); } } } Kompilieren 1. Auswahl pr72100 erstellen im Menü Erstellen. 2. Fehler und Nachrichten vom C# Compiler werden im Ausgabefenster ausgegeben. Falls keine Fehler vorliegen, kann die Anwendung gestartet werden nach Klick auf "Starten ohne Debuggen" im Menü Debuggen. Programm-Ausgabe Die Funktion WriteLine() der Klasse Console gibt den String "Hallo, C# .NET Welt!" aus, anschließend folgt ein Zeilenvorschub. Die Funktion kann auch Werte anderer Datentypen (z.B. ganze Zahlen, Gleitpunktzahlen) ausgeben. Die Main()-Funktion übernimmt die Steuerung, nachdem das Programm geladen wurde. 3. Schritt: Prgrammstruktur Die using-Direktive 437 Die Programmiersprache C++ Im .NET Framework sind einige nützliche Klassen für den Entwickler vorhanden. So übernimmt die Klasse Console die Ein-, Ausgabe des Konsolen-Fensters. Die Klassen sind hierarchisch organisiert. Der voll qualifizierte Name der Klasse Console ist System.Console. Andere Klassen sind bspw. System.IO.FileStream und System.Collections.Queue. Die Direktive using erlaubt eine Referenz auf die Klassen im zugehörigen Namensraum ohne voll qualifizierte Namensnennung. Klassen-Deklaration Jede Funktion ist in C# Bestandteil einer Klasse. Eine neue Klasse wird über die class-Anweisung deklariert. In der Klasse Class1 gibt es nur eine einzige Funktion Main(). Sie empfängt die Steuerung, wenn das Programm geladen wird. Es ist möglich über Main() Kommandozeilen-Argumente zu übergeben. 4. Schritt: Ein- und Ausgabe über die Console Quellcodemodifikationen using System; namespace pr71200 { /// <summary> /// Zusammendfassende Beschreibung für Class1. /// </summary> class Class1 { /// <summary> /// Der Haupteinstiegspunkt für die Anwendung. /// </summary> [STAThread] static void Main(string[] args) { // // TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten // Beschreibung der Programm-Funktion Console.WriteLine("Quicksort c# Beispiel-Applikation\n"); // Name der Eingabedatei ? Console.Write("Quelle: "); string szSrcFile = Console.ReadLine(); // Name der Ausgabedatei ? Console.Write("Ausgabe: "); string destFile = Console.ReadLine(); } } } Lesen von der Konsole Die Methode ReadLine() der Klasse Console verlangt Eingabe vom Benutzer und erfasst die eingebene Zeichenkette. Automatisch wird die Speicherzuweisung für die Zeichenkette veranlasst. der .NET Garbage Collector besorgt die Freigabe von belegtem Speicher. Programm-Ausgabe Mit Debuggen | Start ohne Debuggen ergibt sich das folgende Konsolen-Fenster: 438 Die Programmiersprache C++ 5. Schritt: Verwendung eines Array zur Zwischenspeicherung eingelesener Objekte Die Eingabe wird in der .NET Basisklasse ArrayList aufgenommen, die ein "Array of Objects" implementiert. Quellcodemodifikationen using System; using System.Collections; using System.IO; namespace pr71200 { /// <summary> /// Zusammendfassende Beschreibung für Class1. /// </summary> // Deklaration der Anwendungsklasse class Class1 { /// <summary> /// Der Haupteinstiegspunkt für die Anwendung. /// </summary> [STAThread] static void Main(string[] args) { // // TODO: Fügen Sie hier Code hinzu, um die Anwendung zu starten // Beschreibung der Programm-Funktion Console.WriteLine("Quicksort c# Beispiel-Applikation\n"); // Name der Eingabedatei ? Console.Write("Quelle: "); string szSrcFile = Console.ReadLine(); // Name der Ausgabedatei ? Console.Write("Ausgabe: "); string szDestFile = Console.ReadLine(); // TODO: Einlesen der Quelle string szSrcLine; ArrayList szContents = new ArrayList(); FileStream fsInput = new FileStream(szSrcFile, FileMode.Open, FileAccess.Read); StreamReader szInput = new StreamReader(fsInput); while ((szSrcLine = szInput.ReadLine()) != null) 439 Die Programmiersprache C++ { // Anhaengen an Array szContents.Add(szSrcLine); } szInput.Close(); fsInput.Close(); // TODO: Quicksort FileStream fsOutput = new FileStream(szDestFile, FileMode.Create, FileAccess.Write); StreamWriter szOutput = new StreamWriter(fsOutput); for (int nIndex = 0; nIndex < szContents.Count; nIndex++) { // Ausgabe einer Zeile in die Ausgabedatei szOutput.WriteLine(szContents[nIndex]); } szOutput.Close(); fsOutput.Close(); // Nachricht über den Programmerfolg Console.WriteLine("\nDie sortierten Zeilen wurden gesichert\n"); } } } 440 Die Programmiersprache C++ 8. Die grafischen Bedienoberfächen X und OSF/Motif 8.1 XWindow bzw. X XWindow oder einfach X193 ist die grafische Benutzeroberfäche für UNIXSysteme194. X ist ein Multi Window-System, das Anwendung und Display auf verschiedenen Rechnern erlaubt195. 8.1.1 Die Komponenten von X Das Client-Server-Konzept Der X-Server Der X-Server ist ein Programm, das auf dem Rechner laufen muß, auf dessen Bildschirm (Root-Window) zugegriffen werden soll. Der Serverprozeß heißt X und hat Kontakt zur Hardware (Bildschirm), aber auch zu Tastatur und Maus. X setzt alle graphischen Ausgabebefehle der einzelnen Anwendungen in entsprechende Hardwarebefehle um und verteilt die Eingaben des Benutzers an die entsprechenden Anwendungen. Der X-Server stellt den Anwendungen komplexe Datenstrukturen (Ressourcen) zur Verfügung (z.B. Windows, Pixmaps, GCs und Fonts). Seine Dienstleistungen stellt er über ein Anwendungsprotokoll, dem X-Protokoll, zur Verfügung. Dabei bedient er netzweit mehrere Anwendungen (X-Clients) gleichzeitig. X-Clients Anwendungen werden im X Window System X-Clients genannt. Sie sind hardwaremäßig implementiert und senden an den Server nur allgemein gehaltene Befehle, z.B. „Erzeuge ein Fenster“, „Male einen Kreis“. Die Kommunikation zwischen X-Server und X-Clients werden gesammelt und in gebündelten Paketen abgeschickt. Das gleiche gilt für Nachrichten, die vom X-Server an die Clients geschickt werden. Ein spezieller X-Client ist der Window-Manager. Er verwaltet die einzelnen Anwendungen auf dem Bildschirm. Der Window-Manager übernimmt die Fensterverwaltung und die Gestaltung der Benutzeroberfläche. So erlaubt er das Verschieben von beliebigen Fenstern (mit der Maus) oder das Vergrößern und Verkleinern und sorgt auch dafür, daß jedes Fenster einen Rahmen und Titelbalken bekommt. Über eine Parameter-Datei weiß bspw. der Window-Manager, wie die 193 entwickelt wurde es am MIT, inzwischen ist das Produkt in der Version X11 R6 verfügbar Die Basis-Software ist public-domain, nur Anwenderprogramme oder konfortablere Oberflächen werden kommerziell vertrieben, z.B. das Sun Produkt Open Windows 195 vgl. „Einführung in die XWindows-Programmierung“, Uni Regensburg / Physik (zusammengesetllt von F. Wünsch) 194 441 Die Programmiersprache C++ Bedienoberfläche (Farben, Schriften, ...) aussehen soll. Nach diesen Vorgaben initialisiert er den Bildschirm. Wichtige Window-Manager sind: twm, olwm, mwm Der Anwender sucht sich den Window-Manager aus, der seinen Bedürfnissen 196 am meisten entspricht. X-Clients arbeiten ereignisorientiert. Der Server teilt bspw. der Client-Anwendung die Bewegung des Maus-Zeigers im zur Anwendung zugehörigen Fenster mit. Die Mausbewegung ist ein Event. Es gibt zahlreiche Ereignisse, die der Server senden kann. Der Client definiert, welche Typen ihn interesssieren. Nur die bekommt er dann geliefert. Events werden beim Client automatisch in einer Warteschlange abgelegt. Ereignisse (Events) können durch Eingaben der Benutzer, durch Änderungen auf dem Bildschirm oder durch Seiteneffekte von Funktionen ausgelöst werden. Durch fensterspezifische Ereignis-Masken wird vom X-Client jeweils festgelegt, welches Fenster über welche Ereignisse informiert werden soll. Fenster Ein beliebig großer rechteckiger Bereich auf dem Bildschirm, der vom X-Server (als Server-Ressource) verwaltet wird, ist ein Fenster (Datentyp: Window). Alle Fenster eines Bildschirms bilden eine baumartige Hierarchie. Fenster können nur im Bereich ihres Eltern-Fensters sichtbar sein. Geschwister-Fenster bilden eine Stacking-Order und können sich gegenseitig überdecken. Jedes Fenster besitzt verschiedene Attrubute, die die Eigenschaften des Fensters festlegen. Neben Größe und Position sind dies bspw. auch Rand bzwHintergrundfarben, sowie die Ereignis-Maske, die festlegt, welche Ereignisse für das Fenster relevant sind. Fenster können Properties besitzen. Dieser Begriff bezeichnet beliebige Daten, die an einem Fenster hängen. Jede Property hat einen Namen (, über den sich die beteiligten Applikationen vorher einigen müssen), einen Typ (, über den sie erst zur Laufzeit entscheiden brauchen) und einen Wert. Windows-Manager benutzen Properties zum Austausch von Daten mit anderen Programmen nach den Vorschriften des Interprocess Communication Manual (ICCM). Dort steht u.a., daß das Property WM_NAME eines Toplevel-Fensters vom Window-Manager als Fenstertitel verwendet werden soll. Properties können mit dem X-Utility xprop ausgegeben werden. Pixmap und Bitmap Eine Pixmap ist ein Array mit Werten (Datenstruktur im Off-Screen Bereich des XServer), das üblicherweise als rechteckige Fläche von Farbwerten interpretiert wird. Cursor Das ist sichtbare Repräsentation der Mausposition auf dem Bildschirm. 196 Typischerweise startet der Anwender den Window-Manager automatisch beim Login. Danach wird für die aktuelle „Session“ ein „Look-and-Feel“ festgelegt. 442 Die Programmiersprache C++ Graphics-Context Jede Ausgabe von Graphik geschieht mit Hilfe eines Graphics-Context (GC). Es handelt sich dabei um eine vom X-Server angelegte Datenstruktur (Ressource), die zahlreiche Informationen für Zeichenoperationen enthält, z.B. Vordergrundfarbe, Linienattribute, Flächenattribute oder Zeichensatz für Textausgaben. 8.1.2 Architektur von X-Programmen X-Anwendungen verwenden Funktionen aus drei Bibliotheken: - Xlib (Benutzeroberfläche des X-Window-System) Sie stellt dem Programmierer verschiedene Lowlevel-Funktionen auf der Ebene des X-Protokolls zur Verfügung. - X Toolkit Intrinsics (Aufsatz auf die Xlib) X Toolkit (Xt) Intrinsics dienen als Werkzeug zum Erzeugen und Verwalten von grafischen Grundobjekten der Bedienoberfläche, den sog. Widgets. Widgets197 sind bspw. Buttons, Menüs, Scrollbars, Dialoganwendungen oder Fenster zum Eingeben und Editieren von Text. Die Xt Intrinsics sind ein Zusatz zur Xlib und vereinachen das Ereugen von typischen grafischen Grundobjekten - Widget-Sets Das ist eine Sammlung von Widget-Beschreibungen (sog. Widget-Klassen, die von Widget-Designern entworfen wurden). Für die Xt Intrinsics stehen verschiedene Widget-Sets198 zur Auswahl. Das M.I.T hatte selbst einen eigenen Widget-Set mit dem Namen „Athena Widget Set“ entwickelt. Weitere, bereits kommerzielle Widget Sets sind Motif (von der OSF199) und Open Look (von AT&T). Die Widgets sind in diesen Widget-Sets so implementiert, daß sie das „Look und Feel“ der entsprechenden Bedienoberfläche unterstützen. Sie harmonisieren z.B jeweils mit einem speziell für die Oberfläche entwickelten Window-Manager. Zusammenfassung der Begriffe „window“ und „gadget“ (Fenster und „Dingsbums“). z.B. Athena Widget Set vom MIT, Motif von der OSF, Open Look von AT&T 199 OSF = Open Software Foundation, inzwischen hat sich OSF/Motif als Standard implementiert 197 198 443 Die Programmiersprache C++ 8.1.3 Ein X-Programm Das Programm zeichnet eine Hilbert-Kurve in ein von X bereitgestelltes Fenster und zeigt Befehle, die praktisch in jedem X-Programm vorkommen. 1. Include-Dateien Die komplexen X-Datenstrukturen werden zu Beginn des Programms über „include“-Anweisungen bekannt gemacht /* 2*/ #include <X11/Xlib.h> // Definition von X-Datenstrukturen /* 3*/ #include <X11/Xutil.h> // " " " 2. Kontaktaufnahme zum Server der lokalen Maschine XOpenDisplay ist der allererste X-Befehl. Er bezieht sich im Programm auf eine Benutzervariable mydisplay (Zeiger auf eine X-Struktur vom Typ Display) /* 7*/ Display *mydisplay; // Basisstruktur fuer Serververbindung ............... /*80*/ mydisplay = XOpenDisplay(""); /*81*/ myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr XOpenDisplay stellt die Verbindung mit dem X-Server her. ““ (Leerstring) zeigt an, daß der Server auf dem gleichen (lokalen) Rechner benutzt werden soll, auf dem auch der Client läuft. Kommt es zu keinem Kontakt, wird NULL zurückgegeben. 3. Definition des Fenster Mit XCreateSimpleWindow wird ein (einfaches) Fenster erzeugt, aber noch nicht angezeigt. /* 7*/ /* 8*/ /*71*/ /*75*/ /*84*/ /*85*/ /*86*/ /*87*/ Display *mydisplay; // Basisstruktur fuer Serververbindung Window mywindow; // Window-ID int myscreen; unsigned long black, white; mywindow = XCreateSimpleWindow(mydisplay, DefaultRootWindow(mydisplay), 100, 300,640, 480, 17, black, white); // Definition Fenster DefaultRootWindow bestimmt die Position des neuen Fensters in der WindowHierarchie. Die darauf folgenden Argumente sind Integer-Größen und beschreiben die Ursprungs-Koordinaten des Fensters. (0,0) liegt am Bildschirm links oben.Die folgenden zwei Integer-Größen definieren die Größe des Fensters in Pixel. Es folgt die Randbreite in Pixel. Die beiden letzten Argumente enthalten Kennziffern für die Farbe des Rands und des Fenster-Hintergrunds. 4. Hinweise an den Window-Manager 444 Die Programmiersprache C++ Informationen über jedes Fenster kann der Window-Manager über eine Struktur im X-Server mit dem Namen Properties erhalten, die jeder Client abfragen kann. Zwei solche Properties sind: /*88*/ /*89*/ XStoreName(mydisplay, mywindow, "Graphik"); XSetIconName(mydisplay, mywindow, "Draw"); „Graphik“ ist der „window-name“, „Draw“ ist der „icon-name“. 5. Anzeigen des Fenster Das durch XCreateSimpleWindow definierte Fenster wird mit XMapRaised angezeigt. /*97*/ XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster 6. Ende der X-Sitzung // Schliessen des Fenster /*116*/ XDestroyWindow(mydisplay, mywindow); // Beenden des Server-Kontakts /*117*/ XCloseDisplay(mydisplay); 7. Setzen der Eventmaske Der Client muß (am besten vor dem Anzeigen des Fenster) eine Maske der Events dem Server übermitteln, auf die er reagieren möchte. Zum Setzen der Event-Maske dient der Befehl XSelectInput. Die verschiedenen Masken müssen bitweise in folgender Form verknüpft werden: /* 7*/ Display *mydisplay; // Basisstruktur fuer Serververbindung /* 8*/ Window mywindow; // Window-ID /*94*/ // Setzen der Event-Maske /*95*/ XSelectInput200( mydisplay, mywindow, /*96*/ ButtonPressMask | ExposureMask); Event-Typ Expose ButtonPress ButtonRelease EnterNotify KeyPress Masken-Name ExposureMask ButtonPressMask ButtonReleaseMask EnterWindowMask KexPressMask Anwendung soll zeichnen Maustaste gedrückt Maustaste wieder loslassen Maus hat Fenster erreicht Taste gedrueckt Abb. 6.1-1: Ausgewählte Events: Masken und Typen 8. Eventbearbeitungsschleife 200 Dieser befehl sollte vor XMapRaised im Programm stehen, da XMapRaised schon das Expose-Event sendet und damit anzeigt, daß das Fenster wirklich erzeugt ist. 445 Die Programmiersprache C++ Das ist das Programmstück, das nach Aufforderung durch den Server etwas in das Fenster zeichnet, auf Tastendruck oder Mausklick reagiert. X-Routinen sollten immer in einer Schleife mit folgendem Format angeordnet sein: /* 7*/ Display *mydisplay; // Basisstruktur fuer Serververbindung /*72*/ /*73*/ XEvent int myevent; done; /*98*/ /*99*/ /*100*/ /*101*/ /*102*/ /*103*/ /*104*/ /*105*/ /*106*/ /*107*/ /*108*/ /*109*/ /*110*/ /*111*/ /*112*/ /*113*/ // Eventbearbeitungsschleife done = 0; while (done == 0) { // Hole naechsten Eintrag aus dem Puffer XNextEvent(mydisplay, &myevent); switch(myevent.type) { case Expose: if (myevent.xexpose.count == 0) paint(); break; case ButtonPress: done = 1; break; } } Mit XNextEvent wird ein Event aus dem Puffer geholt bzw. gewartet bis einer ankommt. „myevent.type“ zeigt auf den ersten Eintrag der Event-Struktur. In „switch“ sollten für alle möglichen bzw. erlaubten Events Reaktionen vorgesehen sein. Mit XEventsQueued wird die Anzahl der Einträge in der Warteschlange ermittelt, z.B.: Display *mydisplay; // Basisstruktur fuer Serververbindung int zahl; zahl = XEventsQueued(mydisplay,0); Zur Bearbeitung der Events in einer anderen Reihenfolge, wie sie in der Warteschlange vorliegen, existieren eine ganze Reihe von Befehlen: - Mausknopf-Events Falls eine beliebige Maustaste gedrückt bzw. losgelassen wird und sich der Mauszeiger innerhalb des zugehörigen Fensters befindet, dann werden die Events ButtonPress bzw. BottonRelease erzeugt. Die Eventstruktur liefert (u.a): -- myevent.xbutton.button(int) (gibt an, welche Maustaste es war (links = 1 .. rechts = 3) -- myevent.xbutton.x bzw. .y(int) (gibt an, wo sich der Mauszeiger201 zum Zeitpunkt der Aktion befand) - Tastatur-Events - Expose-Event So ein Event wird immer gesendet, falls die Anwendung den Fensterinhalt neu zeichnen soll. Dies ist der Fall, wenn 201 in Pixel zum Window-Ursprung links oben 446 Die Programmiersprache C++ -- das Fenster gerade erzeugt wurde -- eine Ikonisierung vorlag -- das Fenster von einem fremden Fenster überdeckt wurde -- das Fenster verkleinert / vergrößert wurde. 9. Erstellen von Grafik Der grafische Kontext (graphical context, GC) Im GC werdem Zeichenattribute festgelegt, z.B.: - Farbe für Vorder- und Hintergrund202 XOR-Modus beim Pixel-Setzen ein/aus Linienbreite, Strichen ein/aus Der Befehl zum Erzeugen eines GC heißt XCreateGC. Im einfachsten Fall zeigt er folgenden Ausprägung: /* 7*/ Display /* 8*/ Window /* 9*/ GC /*91*/ - - *mydisplay; mywindow; mygc; // Basisstruktur fuer Serververbindung // Window-ID mygc = XCreateGC(mydisplay, mywindow, 0, 0); In einer Zeichnung können beliebig viele GCs benutzt werden Einzelattribute eines GC können beliebig gesetzt / geändert werden. Für alle existieren Default-Werte, d.h. nur die Atrribute müssen explizit angesprochen werden, die Veränderungen ausdrücken Jede Zeichenoperation übermittelt nur die Nummer des benutzten GC an den Server Ein GC braucht Speicherplatz. Das Freisetzen von Speicherplatz übernimmt XFreeGC(mydisplay, mygc); Ändern von Attributen zum Zeichnen 1. Farbe Zum Zeichnen wird als Default die Farb-Nummer 1, für den Fensterhintergrund die Farb-Nummer 0 benutzt. Explizit kann man sich die Farbnummern beschaffen über /*75*/ unsigned long /*82*/ /*83*/ black white black, white; = BlackPixel(mydisplay, myscreen); = WhitePixel(mydisplay, myscreen); Kennt man die Nummern der Farben, kann man sie im GC eintragen mit XSetForeGround bzw. XSetBackGround, z.B. /*92*/ /*93*/ 202 XSetForeground(mydisplay, mygc, black); XSetBackground(mydisplay, mygc, white); Es gibt weitere Attribute, z.B. das Muster für das Auffüllen von Flächen 447 Die Programmiersprache C++ 2. Linienattribute XSetLineAttributes bestimmt, wie Linien aussehen sollen: unsigned int line_width; int line_style; XSetLineAttributes(mydisplay, mygc, line_width, line_style, 1, 0) „line_width“ sollte für schnelles Zeichen immer 0 sein, dann wird die minimale Linienbreite benutzt. „Zahlen > 0“ definieren die Breite in Pixel. Für „line_style“ kann man die vordefinierten Konstanten LineSolid oder LineOnOffDash hernehmen. Übersicht zu den Grafik-Routinen XDrawPoint XDrawPoints XDrawLine XDrawSegments XDrawLines XDrawArc XDrawArcs XFillArc XFillArcs XDrawRectangle XDrawRectangles XFillPolygon XClearArea XClearWindow setzt einen Punkt setzt mehrere Punkte zeichnet eine Linie zeichnet mehrere getrennte Linien zeichnet eine verbundene Folge von Linien zeichnet einen Bogen zeichnet Bögen / Kreise (Umrisse) füllt einen Bogen füllt mehrere Bögen zeichnet ein Rechteck zeichnet mehrere Rechtecke zeichnet ausgefüllte Fläche löscht rechteckigen Bereich löscht das Fenster Abb. 6.1-2: Einfache Grafik-Routinen aus der XLib - Zu allen Funktionen203 muß ein GC angegeben werden, dessen Attribute dann auch zum Zeichnen benutzt werden. Die aktuelle Window-Größe wird insofern berücksichtigt, daß jede Zeichenaktion an den Grenzen des Fensters beendet wird. Eine Umskalierung bzw. Vergrößern/Verkleinern des Fensters findet nicht statt. Ein vollständiges Demonstrationsprogramm: Hilbert-Kurve /* /* /* /* /* /* 1*/ 2*/ 3*/ 4*/ 5*/ 6*/ #include #include #include #include #include <iostream.h> <X11/Xlib.h> // Definition von X-Datenstrukturen <X11/Xutil.h> // " " " <stdlib.h> <math.h> 203 Die Versorgung der Funktionen mit Parametern, die Wirkungsweise dieser Funktionen sind umfassend in den man-Pages dokumentiert. 448 Die Programmiersprache C++ /* 7*/ /* 8*/ /* 9*/ /*10*/ /*11*/ /*12*/ /*13*/ /*14*/ /*15*/ /*16*/ /*17*/ /*18*/ /*19*/ /*20*/ /*21*/ /*22*/ /*23*/ /*24*/ /*25*/ /*26*/ /*27*/ /*28*/ /*29*/ /*30*/ /*31*/ /*32*/ /*33*/ /*34*/ /*35*/ /*36*/ /*37*/ /*38*/ /*39*/ /*40*/ /*41*/ /*42*/ /*43*/ /*44*/ /*45*/ /*46*/ /*47*/ /*48*/ /*49*/ /*50*/ /*51*/ /*52*/ /*53*/ /*54*/ /*55*/ /*56*/ /*57*/ /*58*/ /*59*/ /*60*/ /*61*/ /*62*/ /*63*/ /*64*/ /*65*/ /*66*/ /*67*/ /*68*/ /*69*/ /*70*/ /*71*/ /*72*/ /*73*/ Display Window GC *mydisplay; mywindow; mygc; // Basisstruktur fuer Serververbindung // Window-ID #define WIDTH 640.0 #define HEIGHT 480.0 int i, stufe; // x1, y1 funktioniert nicht, // weil bereits in LIB. libm.a verwendet. */ float x_1, x_2, y_1, y_2, r, r_1, r_2, h; void generiere( float r_1, float r_2) { stufe--; if (stufe > 0) generiere(r_2,r_1); x_2 += r_1; y_2 += r_2; XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320), (int)(240-y_1), (int)(x_2+320), (int)(240-y_2) ); x_1 = x_2; y_1 = y_2; if (stufe > 0) generiere(r_1,r_2); x_2 += r_2; y_2 += r_1; XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320), (int)(240-y_1), (int)(x_2+320), (int)(240-y_2) ); x_1 = x_2; y_1 = y_2; if (stufe > 0) generiere(r_1,r_2); x_2 -= r_1; y_2 -= r_2; XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320), (int)(240-y_1), (int)(x_2+320), (int)(240-y_2) ); x_1 = x_2; y_1 = y_2; if (stufe > 0) generiere(-r_2, -r_1); stufe++; } void paint(void) { h = 2; i = stufe; while (i > 1) { h *= 2; i--; } r = 400 / h; x_1 = -200; x_2 = -200; y_1 = -200; y_2 = -200; generiere(r, 0); } main() { int XEvent int myscreen; myevent; done; 449 Die Programmiersprache C++ /*74*/ /*75*/ /*76*/ /*77*/ /*78*/ /*79*/ /*80*/ /*81*/ /*82*/ /*83*/ /*84*/ /*85*/ /*86*/ /*87*/ /*88*/ /*89*/ /*90*/ /*91*/ /*92*/ /*93*/ /*94*/ /*95*/ /*96*/ /*97*/ /*98*/ /*99*/ /*100*/ /*101*/ /*102*/ /*103*/ /*104*/ /*105*/ /*106*/ /*107*/ /*108*/ /*109*/ /*110*/ /*111*/ /*112*/ /*113*/ /*114*/ /*115*/ /*116*/ /*117*/ /*118*/ /*119*/ } /*120*/ unsigned long int black, white; i; cout << "Stufen: "; cin >> stufe; // X-Sitzung eröffnen mydisplay = XOpenDisplay(""); myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr black = BlackPixel(mydisplay, myscreen); white = WhitePixel(mydisplay, myscreen); mywindow = XCreateSimpleWindow(mydisplay, DefaultRootWindow(mydisplay), 100, 300,640, 480, 17, black, white); // Definition Fenster XStoreName(mydisplay, mywindow, "Graphik"); XSetIconName(mydisplay, mywindow, "Draw"); // Erzeugen "graphical context" mygc = XCreateGC(mydisplay, mywindow, 0, 0); XSetForeground(mydisplay, mygc, black); XSetBackground(mydisplay, mygc, white); // Setzen der Event-Maske XSelectInput( mydisplay, mywindow, ButtonPressMask | ExposureMask); XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster // Eventbearbeitungsschleife done = 0; while (done == 0) { // Hole naechsten Eintrag aus dem Puffer XNextEvent(mydisplay, &myevent); switch(myevent.type) { case Expose: if (myevent.xexpose.count == 0) paint(); break; case ButtonPress: done = 1; break; } } // Schliessen der Fenster XFreeGC(mydisplay, mygc); XDestroyWindow(mydisplay, mywindow); XCloseDisplay(mydisplay); // Beenden des Server-Kontakts exit(0); 450 Die Programmiersprache C++ 8.2 OSF/Motif 8.2.1 Einführung in OSF/Motif, Xt Intrinsics Im Mai 1988 schlossen sich sieben verschiedene Hardwarehersteller zur Open Software Foundation (OSF) zusammen. Ziel dieses Zusammenschlusses war es eine UNIX Bedienoberfläche zu erschaffen. Sie wurde Mitte 1989 unter dem Namen OSF/Motif bereitgestellt. Zu OSF/Motif gehören: - Das OSF/Motif Widget-Set Der Motif Window-Manager (mwm) Die User Interface Language (UIL) Ein Styl Guide mit Stil-Richtlinien für Motif-Oberflächen Da Motif auf dem X-Window System aufsetzt, werden die sowohl die Xlib als auch die Xt Intrinsics in Programmen verwendet. Zur Unterscheidung welche Bibliothek eine Funktion oder einen Datentyp definiert, einigte man sich auf folgende Namenskonvention: - Alle Xlib-Funktionen und Xlib-Strukturtypen beginnen (bis auf wenige Ausnahmen) mit einem großen X Alle Xt Intrinsics-Funktionen und Strukturtypen beginnen (bis auf wenige Ausnahmen) mit Xt Alle Motif Bezeichnungen beginnen mit Xm Durch das Zusammenspiel von Motif, Xlib und Xt Intrinsics stehen dem Programmierer mehrere fundamentale Datentypen zur Verfügung: Xlib Datentyp Display Font GC Bedeutung Display- Beschreibung Zeichensatz Umgebung für Zeichenoperationen Muster beim X-Server Fenster beim X-Server universeller Zeiger Art Struktur ID Zeiger Xt Datentyp Boolean Bedeutung Art char bis long Cardinal Dimension Pixel Position String Widget WidgetClass XmString Laufindex Größenangabe Farbwert Koordinate Zeichenfolge Fensterbeschreibung beim XClient Fenstertypbeschreibung String mit Formatierung XtPointer universeller Zeiger Pixmap Window XPointer (seit X11R4) Wahrheitswert204 ID ID char* unsigned int int unsigned long short / int char* Zeiger Zeiger CompoundStrin g void* Der hier interessanteste Datentyp ist der Xt Datentyp „Widget“ bzw. „WidgetClass“ . Die Instanzen des Datentyps Widget beschreiben immer einen Bereich des 204 Für den Datentyp Boolean sind die Konstanten „True“, „TRUE“ (!= 0) „False“, „FALSE“ (== 0) vordefiniert 451 Die Programmiersprache C++ Bildschirms. Ein Widget kann zur Darstellung von Text, zur Darstellung einer Menüleiste oder zur Einteilung des Bildschirms benutzt werden. Man unterscheidet verschiedene Arten von Widgets: - - - Display-Widgets (Primitive Widgets) stellen ein konkretes Bedienelement dar, das man auf dem Bildschirm sieht. Sie besitzen Ausgabefunktionalität, zu der im allg. auch eine Eingabefunktionalität gehört. Sie können keine „Kind“-Widgets besitzen. Typische Beispiele: Buttons, Menüpunkte, Scrollbars oder Widgets zum Editieren von Text. Container-Widgets verwalten das geometrische Layout (Größe und Position) der ihnen untergeordneten („Kind-“) Widgets. Shell-Widgets sind eine spezielle Form von Container-Widgets und übernehmen die Kommunikation mit der Außenwelt (speziell mit dem Window-Manager). Ihre Fenster sind die Toplevel-Fenster der XAnwendung. Eine typische X-Anwendung besteht also aus einer baumartigen Hierarchie von Widgets. das oberste Widget eines solchen Widget-Baums ist ein Shell-Widget. darunter liegen Container-Widgets, die das Layout einer Anwendung immer weiter aufteilen. Die Blätter des Widgetbaums sind schließlich Display-Widgets, die die eigentliche Schnittstelle zu den Funktionen der Anwendung bilden. 8.2.2 Struktur eines Intrinsics-Programmes Programmrumpf von Intrinsics-Programmen Im Prinzip sieht der Programmrumpf bei allen Intrinsics-Programmen gleich aus: - Einbinden der Headerdateien für Motif ( <Xm/XmAll.h> bindet alle erforderlichen Dateien ein) Einlesen der Fallback-Ressourcen Programminitialisierung (meist mit XtAppInitialize()) Erzeugen und ausgeben eines Widgetbaums auf der erhaltenen Applikations-Shell Eintritt in „Endlosschleife“ zur Ereignissbehandlung (mit XtAppMainLoop()) Das Aussehen der Oberfläche und deren Funktionalität legt allein der Widgetbaum fest. Die Wurzel dieses Baumes ist immer die Application-Shell. Sie ist bereits ein Widget, das durch die Funktion XtAppInitialize() Gestalt erhält. Die Hauptschleife zur Ereignisbehandlung sendet eintreffende X-Events an die zugehörigen Widgets, wo sie entsprechend der Funktionalität des Widgets weiterverarbeitet werden, z.B. unter anderem Callbacks auslösen. Beispiel eines Instrinics-Programmes // Allgemeine Header-Dateien einbinden #include<stdio.h> 452 Die Programmiersprache C++ #include<Xm/XmAll.h> // Beginn des Hauptprogrammes main(int argc, char* argv[]) { XtAppContext kontext; Widget applShell, text; // Programminitialisierung und ApplicationsShell erzeugen applShell = XtAppInitialize (&kontext, "Beispiel1", (XrmOptionDescRec*) NULL, 0, &argc, argv, (String) NULL, (Arg*) NULL, 0); // Widgetbaum erzeugen, hier nur ein Textwidget text = XtCreateManagedWidget ("Beispieltext", xmLabelWidgetClass, applShell,(Arg*) NULL, 0); // Widgetbaum realisieren XtRealizeWidget(applShell); // Hauptschleife mit Ereignisbehandlung XtAppMainLoop(kontext); } Widgets und Widgetbaum Widgets können erzeugt, verwaltet (wird im Layout des Elternwidgets berücksichtigt), realisiert (das zugehörige X-Fenster wird erzeugt) und ausgegeben (auf dem Bildschirm „sichtbar“) werden. Dazu gibt es entsprechende Grundfunktionen: Funktionsname XtCreateWidget XtManageChild XtRealizeWidget XtCreateManagedWidget XtMapWidget Auswirkung liefert als Returnwert ein Widget berücksichtigen des Kindes im Layout des Eltern Widgets erzeugt X-Fenster zum Widget erzeugt und managt ein Widget macht ein Widget sichtbar XtDestroyWidget XtUnrealizeWidget XtUnmanageChild XtUnmapWidget zerstört den Teilbaum hebt die Realisierung wieder auf hebt das Management des Widgets wieder auf macht ein Widget unsichtbar XtIsManaged XtIsRealized liefert zurück ob ein Widget gemanagt wird liefert zurück ob ein Widget realisiert ist Im Normalfall (falls keine anderen Ressourcen gesetzt wurden) wird ein Widget sichtbar sobald es erzeugt, verwaltet und realisiert wurde. Selbstverständlich muß natürlich das jeweilige Elternwidget auch sichtbar sein, und es darf nicht durch ein Geschwisterwidget überdeckt werden. Über Vererbungsmechanismen kann ein Widget verschiedene Eigenschaften (Ressourcen) vom jeweiligen Elternwidget erben. Ressourcen und Callbacks Jedes Widget besitzt Parameter, die dessen konkrete Eigenschaften definieren. Diese Parameter nennt man Ressourcen. Welche Ressourcen ein Widget besitzt richtet sich nach deren Widgetklasse, aber alle Ressourcen der darüberliegenden Widgetklassen werden geerbt. So erhalten 453 Die Programmiersprache C++ alle Widgets die Ressourcen der Widgetklasse Core die als Basisklasse aller Widgets dient. Die wichtigsten Ressourcen der Basisklasse Core sind: Ressource-Konstante XmNx XmNy XmNwidth XmNheight XmNbackground XmNborderWidth XmNscreen XmNmappedWhenManaged XmNcolormap ... Datentyp Position Position Dimension Dimension Pixel Dimension Screen* Boolean Colormap ... Defaultwerte 0 0 Label-Breite Label-Breite verschieden 0 XtCopyScreen True XtCopyFromParent ... Möglichkeiten zum Manipulieren von Ressourcen Es gibt zwei Arten die Ressourcen von Widgets zu manipulieren: - Innerhalb des Programmes, diese können von außen nicht mehr geändert werden Außerhalb des Programmes, diese können auch nach der Übersetzung des Programmes überschrieben werden. Sie werden in spezielle Ressource-Dateien festgelegt, und zur Startzeit des Programmes gelesen Ressourcen-Dateien Zum Setzen von Ressourcen außerhalb des Programmes dienen vor allem Ressource-Dateien. Der Aufbauen einer solchen Datei muß einer speziellen Syntax gehorchen: In jeweils einer Zeile einer Ressource-Datei wird festgelegt, in welchem Programm, in welchen Widget, welche Ressource welchen Wert bekommt. Ressource Zeilen haben folgenden Aufbau: Programmname.Widgetpfad.Widget.RessourceKonstante: Wert Unter dem Begriff „Widgetpfad“ ist der Pfad im Widgetbaum gemeint um zu dem speziellen Widget zu gelangen. Zu beachten ist auch, daß die Ressourcekonstante ohne die Präfix „XmN“ angegeben wird. Um zum Beispiel das Textwidget des Beispielprogrammes1 auf „Hello World“ zu setzen, genügt in der Ressource-Datei folgende Zeile: Beispiel1.text.labelString: Hello World Es besteht die Möglichkeit im „Widgetpfad“ Wildcards zu benutzen, um mehrere Widgets gleichzeitig anzusprechen. Zum einen das Fragezeichen, das im Pfad genau ein Widget ersetzt, zum anderen der Stern, der einen beliebigen Pfad repräsentiert. Hierbei kann selbst der Programmname entfallen. Die RessourceZeile: *text.labelString: Hello World 454 Die Programmiersprache C++ setzt zum Beispiel in allen Programmen in denen die Ressource-Datei verwendet wird, in jedem Widget mit dem Namen „text“ die Ressource „labelString“ auf den Wert: „Hello World“. Durch ein Ausrufezeichen in einer Ressource-Zeile kann Kommentar eingeleitet werden, der bis zum Zeilenende geht. Ein Ausrufezeichen innerhalb eines Ressource-Wertes ist allerdings Teil dieses Wertes. Um nun diese Ressource-Dateien im Programm verwenden zu können, wird intern eine Ressource-Datenbasis durch die Initialisierungsfunktion erzeugt. Nur wenn eine Ressource für ein Widget benötigt wird, wird ermittelt welche dieser RessourceZeilen für das Widget von Bedeutung sind. Im allgemeinen gibt man solchen Application-Defaults-Ressource-Dateien die Endung „ad“. Sie muß aber in ein spezielles Directory für Ressource-Dateien z.B.: „usr/lib/X11/app-defaults“ und unter dem Klassennamen des Programmes z.B.: „Beispiel1.ad“ installiert werden. Die Initialisierungsfunktion „XtAppInitialize“ füllt nun die Datenbasis mit den Ressource-Zeilen der Datei auf. Dies geschieht durch die Angabe des Klassennamens im zweiten Parameter. Setzen von Ressourcen im Programm Innerhalb eines Intrinsics-Programmes hat der Applicationsprogrammierer zwei Möglichkeiten Widget-Ressourcen zu setzen. Zum einen bei der Erzeugung eines Widgets, zum anderen durch eine spezielle Funktion. In allen Fällen muß als Parameter ein Array von speziellen Strukturen, eine sogenannte Argument-Liste, übergeben werden. Zum Auffüllen dieses Arrays empfiehlt sich ein vordefiniertes Makro „XtSetArg“. Die Anwendung dieses Makros führt zu folgendem typischen Aufbau beim Setzen von Widget-Ressourcen. Arg args[10]; Cardinal n; //Argument-Liste //Laufvariable n = 0; XtSetArg(args[n], RessourceKonstante1, Wert1); n++; XtSetArg(args[n], RessourceKonstante2, Wert2); n++; Somit wird durch das Makro die Argument-Liste aufgefüllt, wobei die Ressource „RessourceKonstante“ den Wert „Wert“ zugeordnet bekommt. Um diese Ressourcen für ein Widget zu setzen gibt es wie schon erwähnt zwei Möglichkeiten: Falls die Ressourcen beim Erzeugen des Widgets gesetzt werden sollen, werden die letzten zwei Parameter der Funktion „XtCreateWidget“ benutzt: widget = XtCreateWidget(....., args, n); wobei „args“ den Zeiger auf die Argument-Liste repräsentiert, und „n“ den Laufindex. Falls die Ressourcen eines existierenden Widgets geändert werden sollen, wird die Funktion „XtSetValues“ benutzt. Diese Funktion erwartet als Parameter auch die schon beschriebene Argument-Liste und den Laufindex erwartet. So setzt: XtSetValues(widget, args, n); die Ressourcen des Widgets „Widget“, so wie in der Argument-Liste „args“ beschrieben. 455 Die Programmiersprache C++ Abfragen von Ressourcen Für das Abfragen von Ressourcen gibt es ebenfalls eine spezielle Funktion „XtGetValues“. Sie liest die Werte von Ressourcen über die schon erläuterte Argument-Liste aus. Die einzelnen Argumente bestehen allerdings aus dem Ressource-Namen (RessourceKonstante) und der Adresse der Variablen in der Wert der Ressource eingetragen werden soll. Man kann also nicht mit der gleichen Argument-Liste Ressourcen setzten und abfragen. Zum Abfragen der Größe eines Widgets dienen z.B. folgende Zeilen: Arg args[10]; Cardinal n; Dimension b; Dimension h; //Argumentliste //Laufindex //auszulesende Breite //auszulesende Höhe n = 0; XtSetArg (args[n], XmNwidth, &b); n++; XtSetArg (args[n], XmNheight, &h); n++; XtGetValues (widget, args, n) Callback-Ressourcen Um in Intrinsics-Programmen eine Funktionalität implementieren zu können, gibt es die Möglichkeit, einem Widget als Ressource Adressen von Funktionen zu übergeben, die dann unter bestimmten Bedingungen aufgerufen werden. Man spricht hierbei von ereignisorientierter Programmierung. Da dem Widget die Adresse der Funktion übergeben wird nennt man solche Parameter auch Callbacks. Callbackressourcen sind einfache Ressource-Konstanten. Sie hängen aber sehr eng mit dem benutzen Widget zusammen. So hat ein Widget der Klasse „XmPushButton“ eine RessourceKonstante „XmNactivateCallback“, mit denen die Callbackfunkionen verbunden werden, die dann ausgeführt werden sollen, falls der Button gedrückt und wieder losgelassen wurde205. Eine Callbackfunktion darf kein Ergebnis zurückliefen (void) somit im Sinne von Paskal eine Prozedur sein. Da sie von internen Funktionen aufgerufen werden, sind auch die Parameter vordefiniert. Somit ergibt sich folgender Aufbau für eine Callbackfunktion: void callbackFunktion (Widget widget, XtPointer clientDaten, XtPointer aufrufDaten) Im ersten Parameter wird der Funktion das aufrufende Widget übergeben. Die beiden folgenden Parameter sind Zeiger beliebigen Typs, werden sie benötigt, ist eine Typumwandlung erforderlich (sogenanntes Typcasting). Um nun Callbacks bei einem Widget zu registrieren gibt es eine spezielle Funktion „XtAddCallback“, um diese Verbindung wieder aufzulösen die Funktion „XtRemoveCallback“. Neben diesen zwei wichtigsten gibt es noch viele andere, die aber über Callback-Listen arbeiten, auf die hier nicht näher eingegangen wird. 205 Eine Auflistung der verschiedenen Callbackressourcen der verschiedenen Widgets folgt im Abschnitt „OSF/Widget Set“. 456 Die Programmiersprache C++ Um in einem Programm einen Widget Exit-Button „exit“ mit einer ExitCallbackfunktion „exitCB“ zu verbinden benötigt man folgende Zeilen: XtAddCallback(exit, XmNactivateCallback, exitCB, (XtPointer) NULL); wobei im letzten Parameter die XtPointer Variable für die Übergabe an die exitCB Funktion steht. Da sie hier nicht benutzt wird, wird durch einen Typcast der NULL-Zeiger übergeben. In der Funktion „exitCB“ wird z.B. die exit() Funktion aufgerufen, was zu einer ordentlichen Programmbeendung führt. 8.2.3 Das OSF/Widget Set Kategorien Widgets lassen sich gemäß ihres typischen Verwendungszweckes in folgende Kategorien einteilen: Display-Widgets haben die Eigenschaft, daß man sie auf dem Bildschirm „sehen“ kann. Veränderbare und konstante Texte, Knöpfe zum „draufdrücken“, Grafiken, Listen usw. sind einige Vertreter dieser Gruppe. Jedes „Blatt“ des Widgetbaumes muß folglich ein Display-Widget sein. Container-Widgets dienen vor allem dem Zweck, das geometrische Layout ihrer Kinder zu kontrollieren. Sie ordnen ihre Kind-Widgets in Zeilen und Spalten oder untereinander an oder stellen Ressourcen zur Verfügung, durch die sich ein Kind Widget abhängig von seinen Geschwistern ausrichten kann. Shell-Widgets sind zwar ebenfalls Container, sie besitzen jedoch nur ein einziges gemanagtes Kind-Widget. Eine der wesentlichen Aufgaben ist es, die Verbindung mit der „Außenwelt“ herzustellen. Dies bedeutet, beispielsweise dem WindowManager Hinweise zu übermitteln, wie „wichtig“ die in der darunterliegenden Widgethierarchie dargestellte Information ist, oder ob sie beispielsweise nur kurz auf dem Bildschirm sichtbar sein wird. Dialog-Widgets werden häufig dazu benutzt, relativ kurze Dialoge mit dem Benutzer abzuwickeln, und werden deshalb nur bei Bedarf auf den Bildschirm ausgegeben. Informationen, Fehlermeldungen, Dateinamen usw. können somit ausgegeben bzw. erfragt werden. Dialog-Widgets sind Container und somit um Kind-Widgets erweiterbar. Überblick über alle Widgets Die folgende Aufstellung zeigt die wichtigsten Widgetklassen unterteilt in Display, Shell, Container Widgets: DisplayWidgets: WidgetKlassen-Name Beschreibung Motif-Version 457 Zeile im Die Programmiersprache C++ XmArrowButton XmScrollBar XmLabel XmList XmSeperator XmText XmTextField XmCsText XmPushButton XmToggleButton XmDrawButton XmCascadeButton Bedienknopf mit Pfeil (Datensatznavigation) Verschiebeleiste Text oder Muster Liste von auswählbaren Texten Trennlinie Text einzeiliger Texteditor ein oder mehrzeiliger Texteditor mit Formatierungsmöglichkeit Taster Schalter Button mit Bild Menüpunkt mit Untermenue 1.0 1.0 1.0 1.0 1.0 1.0 1.1 2.0 Beispiel 76 - 1.0 1.0 1.0 1.0 241 237 wird nicht vom Windowmanager verwaltet für Menüs legt fest wie der Windowmanager mit einem Widget umgehen soll legt Merkmale im Zusammenspiel mit Windowmanager fest temporäre Dialoge mit dem Benutzer temporäre Dialoge mit dem Benutzer zur Verwendung mehrerer Widgetbäume in ihr läuft der Hauptteil der Applikation ab 1.0 1.0 1.0 320 - 1.0 - 1.0 1.0 1.0 1.0 74 191 1.0 1.0 1.0 1.0 1.0 1.0 1.0 2.0 2.0 390 371 74 383 - 1.0 - ShellWidgets: OverrideShell XmMenuShell WmShell VendorShell TransientShell XmDialogShell TopLevelShell ApplicationShell ContainerWidgets: XmDrawingArea XmRowColumn XmForm XmMessageBox XmFileSelectionBox XmSelectionBox XmScrolledWindow XmSpinBox XmNotebook XmMainWindow X-Window Anordnung der Kinder in Zeilen und Spalten Anordung der Kinder durch „berühren“ Nachrichten Fenster mit Dateiauswahlmöglichkeit Fenster mit Auswahlmögichkeit Window mit Bildlaufleisten zyklisches Durchlaufen einer Wertemenge Simuliert eine Buch das seitenweise durchgeblättert werden kann typisches Hauptfenster, mit festen Plätzen für Kinder Die wichtigsten WidgetKlassen und ihre Ressourcen In diesem Abschnitt werden die wichtigsten Widgetarten genauer erläutert, insbesondere ihre speziellen Ressourcen aufgezeigt. XmLabel-Widget 458 Die Programmiersprache C++ Eines der wichtigsten Display-Widgets ist die Klasse „XmLabel“. Sie werden benutzt um Text darzustellen, ohne das hier eine Eingabe des Benutzers erwartet wird. Sie dient aber auch als Oberklasse für alle „Button - Widgets“. Der Text im Label selbst ist ein Compound-String, d.h. er kann formatiert werden. Neben den Ressourcen der Oberklassen besitzt das Label-Widget folgende spezielle Ressourcen206: XmNLabelString: Ist vom Datentyp XmString und enthält den darzustellenden Text XmNLabelTyp: Für diese Ressource gibt es zwei mögliche Werte, XmSTRING (Default) zur Darstellung von Text und XmPIXMAP zur Darstellung eines Bilds. XmNLabelPixmap:Ist vom Typ Pixmap, dort muß das darzustellende Bild angegeben werden XmNalingnment:Diese Ressource steuert die Ausrichtung des Textes, möglich sind XmNALLIGNMENT_CENTER zum Zentrieren des Texte XmNALLIGNMENT_BEGINNING für linksbündig und XmNALLIGNMENT_END für rechtsbündingen Text. XmNrecomputeSize:Diese Ressource ist vom Typ Boolean und steuert mit True und False ob sich der Text bei Größenänderung des Widgets automatisch mit anpaßt. XmPushButton-Widget Durch das dreidimensionale Erscheinungsbild dieses Knopfes entsteht beim Anwender die Vorstellung, das durch das Drücken des Knopfes ein Kommando ausgelöst werde. Da diese Klasse eine Unterklasse der Label-Widgets ist, besitzt sie alle zuvor beschriebenen Ressourcen, dazu erhält er spezielle Callback-Ressourcen die genau steuern, wann welche Callback-Funktion auszulösen ist: XmNactivateCallback:Diese Callbacks werden aufgerufen, wenn der Benutzer den Mausknopf überhalb des Button-Widgets drückt, und dort auch wieder losläßt. XmNarmCallback: werden aufgerufen wenn der Mausbutton innerhalb des Widgets gedrückt wird. XmNdisarmCallback:wird aufgerufen wenn der Mausbutton innerhalb des Widgets gedrückt , aber irgendwo anders wieder losgelassen wurde XmToggleButton-Widget Widgets dieser Klasse sind Ein/Aus-Schalter. Sie können zwei verschiedene Zustände anzeigen. Die Callback-Ressourcen sind dieselben wie beim PushButton, nur die Ressource XmNactivateCallback hat den Namen XmNvalueChangedCallback erhalten. Beim XmNToggleButton-Widget sind vor allem die Ressourcen zur Darstellung der Zustände interessant: XmNSet: ist vom Typ Boolean und gibt an ob gerade der Ein- oder der Auszustand gewählt ist. 206 Neben diesen Ressourcen gibt es natürlich noch viele andere mehr, sie sind in der Fachliteratur dokumentiert. 459 Die Programmiersprache C++ Es gibt auch hier noch viele weitere Ressourcen, unter anderem gibt es seit Motif 2.0 die Möglichkeit nicht nur zwei, sondern drei Zustände über einen Schalter zu steuern. Ebenfalls ist es möglich Kombinationen von ToggleButtons zu erstellen, von denen genau einer den Zustand „Ein“ haben kann. XmCascadeButton-Widget Ein XmCascadeButton-Widget hat normalerweise eine Menüleiste oder ein anderes Menü als ElternWidget. Eine Aktivierung dieses Knopfes führt im allgemeinen dazu, das eine Untermenü erscheint. XmNsubMenuId: Diese Ressource erwartet die Angabe eines Widgets zur Präsentation des Untermenüs, das bei Aktivierung aufgeklappt werden soll XmText-Widget Ein Widget dieser Klasse ist dazu gedacht Texte einzulesen, die der Anwender eingibt. Dem Anwender kann man hierzu einen kleinen Texteditor zur Verfügung stellen. Hier die wichtigsten Ressourcen eines XmText-Widgets: XmNvalue: enthält den vom Anwender eingegebenen Text, ist aber kein CompoundString, sondern nur ein einfacher String XmNeditMode: mit dieser Ressource kann man über die Werte XmSINGLE_LINE_EDIT und XmMULTI_LINE_EDIT steuern wieviele Zeilen der Text haben darf. XmNeditable:gibt über True und False an, ob der Text editierbar ist. Um weiterführende Editorfunktionen zu erhalten, gibt es seit Motif 2.0 die XmCSTextWidgets. Diese speichern den eingegebenen Text als Compound-String, deshalb sind dort Textformatierungen möglich. Nebenbei sei noch erwähnt das sich der kleine Texteditor zwischen Einfüge- und Überschreibmodus mit der Funktion „toggle-overstrike()“ umschalten läßt. XmList-Widget In einem XmList-Widget können, untereinander in Zeilen angeordnet, Texte aufgelistet werden, wobei mit der Maus eine oder mehrere Zeilen ausgewählt werden können. Eine typische Verwendung ist dieses Widget in einer FileSelectionBox zur Auswahl der Datei. Diesem Widget stehen sehr viele Ressourcen zur Verfügung, hier die wichtigsten: XmNitems: ist ein Zeiger auf eine Liste von Compound-Strings, die die Einträge der Liste repräsentiert. XmNitemCount: ist die Anzahl der Einträge in XmNitems 460 Die Programmiersprache C++ XmNvisibleItemCount: gibt an wieviele Einträge in der Liste sichtbar sein sollen, es beeinflußt somit die Höhe des Widgets. XmNselectionPolicy: über diese Ressource kann man steuern, ob Einzelauswahl (XmSINGLE_SELECT) oder Mehrauswahl (XmMULTIPLE_SELECT) möglich ist XmNselectedItems:ist ein XmStringTable der die ausgewählten Einträge enthält. Im Zusammenhang mit diesem Widget gibt es noch einige Funktionen: XmListAddItem: fügt einen Item in die Liste ein. XmListDeleteItem: löscht einen Eintrag aus der Liste. XmListSelectItem: selektiert einen Eintrag anhand seines Textes XmListDeselectItem: hebt Selektierung des Eintrages wieder auf. XmListItemExists: Liefert True falls der Text ein Eintrag in der Liste ist XmCreateScrolledList: erzeugt Instanzen eines XmList-Widgets als Kind eines ScrolledWindow-Widgets207 XmSeperator-Widget Mit XmSeperator-Widgets können als optische Trennung horizontale oder vertikale Linien dargestellt werden. Sie dienen oft dazu Einteilungen des Bildschirms, z.B. funktionale Gruppierungen in Menüs, vorzunehmen. Diese Widgetklasse besitzt selbst nur drei spezielle Ressourcen: XmNmargin: vom Typ Dimension gibt die Ausdehnung an XmNorientation: steuerbar XmNseperatorTyp: gibt den Linientyp an XmSINGLE_LINE eine einfache Linie XmDOUBLE_LINE eine doppelte Linie XmSINGLE_DASHED_LINE eine einfach gestrichelte Linie XmSHADOW_ETCHED_LINE_IN eine Linie „ins Fenster“ XmSHADOW_ETCHED_LINE_OUT eine Linie „aus dem Fenster“ Für XmSeperator-Widgets gibt es keine Callbacks oder Reaktionen auf irgendwelche Benutzereingaben. XmSelectionBox-Widget In einem XmSelectionBox-Widget kann dem Benutzter eine Liste von Einträgen angeboten werden, aus denen er einen Eintrag selektieren kann. Diese ausgewählte Eintrag wird in ein editierbares Text-Widget übernommen. Per Default stehen außerdem noch vier Buttons für OK, Apply, Cancel, Help zur Verfügung. Verschiedene Ressourcen steuern Layout und Funktion: XmNapplyCallback: Gibt den Callback für den Applybutton an. XmNlapplyLabelString: Gibt die Beschriftung für den Applybutton an. Analog lassen sich die Labels für den OK, Cancel, Help Button angeben. 207 Die Syntax und Funktionsweise kann den man-Pages entnommen werden. 461 Die Programmiersprache C++ XmNdialogType: damit läßt sich steuern welche Kinder vorhanden sein sollen und welche nicht. bei XmDIALOG_SELECTION werden alle Kinder erzeugt und verwaltet. Bei XmDIALOG_WORK_AREA werden alle erzeugt, aber nur der Applybutton verwaltet. Bei XmDIALOG_PROMT fehlen das List-Widget und dessen Überschrift. Wird häufig für einzeilige Benutzereingaben verwendet. Um andere Dialog-Typen zu erhalten, gibt es die Möglichkeit, über die Funktion „XmSelectionBoxGetChild“ einzelne Kinder zu entfernen, bzw. umzuformen. Die Kind-Komponente läßt sich über folgende Konstanten angeben: XmDIALOG_APPLY_BUTTON XmDIALOG_LIST XmDIALOG_LIST_LABEL XmDIALOG_TEXT XmDIALOG_SELECTION_LABEL für den Applybutton und analog für die anderen Buttons. für das XmList-Widget für die Überschrift des XmList-Widgets für das Text-Widget für die Überschrift des XmText-Widgets. XmFileSelectionBox-Widget Diese Widgetklasse erlaubt dem Benutzer das Navigieren durch Directories. Er kann auch einen Pfadnamen angeben, und einen Filter aktivieren. Dieses Widget enthält wiederum die Buttons für OK, Apply (Filtern), Cancel und Help. Die speziellen Ressourcen sind: XmNfileTypeMask: damit läßt sich steuern, ob in der Liste nur Dateien (XmFILE_REGULAR) oder nur Directories (XmFILE_DIRECTORY) oder beides (XmFILE_ANY_TYPE) zu sehen sein soll XmNnoMatchString:Text der angezeigt werden soll, wenn es keine Datei oder Directory gibt zu dem das angegebene Suchmuster paßt Selbstverständlich steht auch hier eine Funktion zur Verfügung die einzelne KindWidgets ansprechbar macht. Die Funktion „XmFileSelectionBoxGetChild“ erfüllt diese Aufgabe. Die Kind-Widgets können über folgende Konstanten angesprochen werden: XmDIALOG_APPLY_BUTTON XmDIALOG_FILTER_LABEL XmDIALOG_FILTER_TEXT XmDIALOG_DIR_LIST XmDIALOG_LIST XmDIALOG_TEXT ... für den Applybutton und analog für die anderen Buttons. für die Überschrift über dem Dateifilter für den Dateifilter ist das List-Widget für die Navigation ist das List-Widget für das Ergebnis der Filterung ist das Text-Widget, das die Benutzerauswahl aus den beiden Listen enthält XmMessageBox-Widget Instanzen der Widgetklasse XmMessageBox können dazu benutzt werden um kurze Dialoge mit dem Benutzer abzuwickeln. Eine MessageBox enthält die Buttons OK, Cancel und Hilfe einen String mit der eigentlichen Nachricht, und ein Symbol für Warnung, Information usw. Die neuen Ressourcen dieser Widgetklasse sind: XmNokCallback: Der Callback für den Ok-Button. Analog für die anderen Buttons. XmNokLabelSring: Die Beschriftung des Ok-Buttons. Analog für die anderen Buttons. XmNdialogType: Hiermit läßt sich ein Symbol auswählen das angezeigt werden soll: XmDIALOG_MESSAGE XmDIALOG_WARNING für kein Symbol für ein Ausrufezeichen 462 Die Programmiersprache C++ XmDIALOG_QUESTION für einen Kopf mit Fragezeichen XmDIALOG_INFORMATION für ein großes I XmDIALOG_ERROR für ein Stop Symbol XmDIALOG_WORKING für eine Sanduhr XmNmessageString: Der Text der erscheinen soll Durch die Angabe von XmDIALOG_TEMPLATE können Dialoganfragen selbst designt werden. Um auf die Ressourcen der Kind-Widgets zuzugreifen zu können müß auch hier eine Funktion benutzt werden, die das Kind-Widget zurückliefert. Die Kind-Widgets können über folgende Konstanten angesprochen werden: XmDIALOG_OK_BUTTON für den Ok-Button und analog für die anderen Buttons. XmDIALOG_MESSAGE_LABEL für das Text-Widget, das den Messagestring enthält. XmRowColumn-Widget Dieses Widget ist ein Container Widget. Es besitzt viele Ressourcen, die verschiedene Layout-Strategien für die Kind-Widgets steuern. Hier die wichtigsten LayoutRessourcen: XmNorientation: durch die Angabe von XmHORIZONTAL oder XmVERTICAL läßt sich steuern ob die Kind-Widgets untereinander oder nebeneinander angeordnet werden sollen XmNpacking: mit der Angabe von XmPACK_COLUMN erreicht man daß die Kinder in gleiche große Boxen gesetzt werden. Typisch für Menüs. Durch die Angabe von XmPACK_NONE behält jedes Kind seine Position wie in der Ressource XmNx und XmNy angegeben XmNnumColumns: gibt die Anzahl der Zeilen bei horizontaler Ausrichtung an. XmNspacing: gibt die Dimension des Zwischenraumes, zwischen den Kind-Widgets an XmForm-Widget Diese Container-Widgetklasse ermöglicht eine weitere Layoutstrategie. Diese Strategie ermöglicht es den Kind-Widgets einen Nachbarn anzugeben, nach dem sich das Widget zu orientieren hat. Dazu erhält das Kind-Widget folgende neue Ressourcen: XmNseiteAttachment: Bei Angabe von XmATTACH_FORM richtet es sich nach dem betreffenden Rand des XmForm-Widgets aus. XmATTACH_WIDGET stellt eine Verbindung zur entgegengesetzten Seite des in der folgenden Ressource angegebenen Widgets her. XmATTACH_NONE läßt es ohne Ausrichtung. XmNseiteWidget: Das Widget richtet sich nach dem hier angegebenen Geschwister-Widget, falls XmATTACH_WIDGET oben angegeben wurde. XmNrezisable: Diese Ressource vom Typ Boolean definiert, ob ein von ihm selbst kommender Wunsch nach Größenänderung ausgeführt wird, oder nicht. 463 Die Programmiersprache C++ Hierbei steht „Seite“ für top, bottom, left oder right, denn alle vier Seiten eines Widgets können von anderen Widget-Seiten abhängig gemacht werden. Die Widgetklasse XmForm selbst, besitzt folgende neue Ressourcen: XmNverticalSpacing: hier wird der Default-Abstand nach links und rechts zwischen dem Widget und seinem Attachment angegeben XmNhorizontalSpacing: hier wird der Default-Abstand nach oben und unten zwischen dem Widget und seinem Attachment angegeben. XmDrawingArea - Widget Mit einem Widget dieser Klasse soll dem Applikationsprogrammierer eine Widgetart bereitgestellt werden, die eigentlich nicht mehr Funktionalität besitzt als ein Window, aber in die Widgethierarchie eingebunden ist. Hauptsächlich werden XmDrawingAreas benutzt um Grafiken darzustellen. Die neuen Ressourcen dieser Widgetklasse sind: XmNexposeCallback: wird aufgerufen falls der Fensterinhalt neu ausgegeben werden muß. XmNresizeCallback: wird aufgerufen falls sich die Größe des Fensters ändert. XmNinputCallback: wird aufgerufen falls Tastatureingaben oder Mauklicks erfolgten. XmNresizePolicy: diese Ressource definiert inwiefern das Widget von sich aus seine Größe zu ändern versucht. XmRESIZE_NONE bedeutet, daß das Widget seine Größe nicht von sich aus ändert. XmRESIZE_ANY veranlaßt das Widget seine Größe so zu ändern, daß alle Kind-Widgets genau Platz haben. XmRESIZE_GROW führt zu einem automatischen Wunsch nach Vergrößerung Da für Zeichenoperationen und Textausgaben mit den Funktionen der Xlib als Parameter eine Window-ID, ein Grafikkontext und ein Display nötig sind, stellt Motif einige Funktionen zur Verfügung, durch die man diese Parameter erhalten kann: XtDisplay(Widget) liefert das Display XtWindow(Widget) liefert die Window-ID XtGetGC(Widget, werteMaske, Werte) liefert den Grafikkontext Für die Erzeugung des Grafikkontexts ist die Abfrage der Vorder- und Hintergrundfarbe nötig. Diese Werte sind dann in die Wertestruktur einzutragen. Diese Struktur ist vom Typ „XGCValues“ und enthält „foreground“ und „background“ vom Datentyp „Pixel“. Menüleisten und Pulldown-Menüs Das OSF/Motif Widget-Set besitzt verschiedene Widgetklassen, die die Verwendung von Menüs in Programmen unterstützen. Folgende Widgetklassen besitzen spezielle Funktionalitäten für Menüs. 464 Die Programmiersprache C++ XmRowColumn: Sie dienen grundsätzlich als Container-Widgets für Menüs. XmCascadeButton: Haben die Aufgabe Menüs automatisch auszugeben. XmMenuShell: Sind spezielle Shell-Widgets für Menüs, die als Popups ausgegeben werden Eine Standard-Anwendung von Menüs ist die Verwendung einer Menüleiste, in der die einzelnen Menüpunkte Pulldown-Menüs ausgeben. Bei der Implementierung eines solchen Menüpunktes muß nur angegeben werden, welcher Menüpunkt welches Menü aktiviert. Alles weitere geschieht automatisch durch die Verwendung des „XmCascadeButtons“. Dieses Widget besitzt dazu die Ressource XmNsubMenuId in die das dazugehörige Container-Widget des dazugehörigen Pulldown-Menüs eingetragen wird. Dieses Sub-Menu wird in Abhängigkeit von der Menüart, in dem sich der Button befindet, aktiviert: Befindet sich der CascadeButton in einer Menüleiste, wird das dazugehörige Pulldown-Menü durch Drücken des Buttons aktiviert. Befindet sich der CascadeButton in einem Pulldown-Menü, so wird das Sub-Menü durch drüberstreichen mit der Maus ausgegeben. Auf diese Weise kann eine Kaskade (geschachtelte Folge) von Menüs entstehen. Daher hat das Widget auch seinen Namen. Das Erzeugen einer Menüleiste mit Pulldown-Menüs geht denkbar einfach: //Menüleiste erzeugen: menueLeiste = XmCreateMenuBar(elternWidget, "Menüleiste",(Arg*) NULL,0); //Menüleiste managen: XtManageChild(menueLeiste) //Pulldown-Menü erzeugen: dateiMenue = XmCreatePulldownMenu (menueLeiste, "Datei", (Arg*) NULL,0); //Menüpunkt innerhalb des Untermenüs erzeugen hier "Öffnen": button = XtCreateManagedWidget ("Punkt", xmPushButtonWidgetClass, dateiMenue, (Arg*) NULL,0); //Pulldown-Untermenü dazu erzeugen: speicherMenue = XmCreatePulldownMenu(dateiMenue, "Speichern", (Arg*) NULL,0); //Untermenuepunkt "Speichern erzeugen": button1 = XtCreateManagedWidget("Speichern", xmPushButtonWidgetClass, speicherMenue, (Arg*) NULL,0); //Pulldow-Untermenü "speichern als ..." erzeugen: button2 = XtCreateManagedWidget("Speichern als ...", xmPushButtonWidgetClass, speicherMenue, (Arg*) NULL,0); //Menüpunkt "speichern" erzeugen der dieses Untermenü aufruft: n = 0; XtSetArg(args[n], XmNsubMenuId, speicherMenue); n++; XtCreateManagedWidget("Speichern", xmCascadeButtonWidgetClass, dateiMenue, args, n); //Menüpunkt "Datei" erzeugen, der dieses Pulldown-Menue aufruft: n = 0; XtSetArg(args[n], XmNsubMenuId, dateiMenue); n++; XtCreateManagedWidget ("Datei", xmCascadeButtonWidgetClass, menueLeistem, args, n) 465 Die Programmiersprache C++ 466