Die Programmiersprache C++ Prof. Jürgen Sauer Programmieren in C++ Skriptum zur Vorlesung im SS 2002 1 Die Programmiersprache C++ Inhaltsverzeichnis 1. GRUNDLEGENDE KONZEPTE............................................................................................................. 7 1.1 ÜBERSICHT ZUR ENTWICKLUNG DER SPRACHE C++ ................................................................................. 7 1.2 EIN EINFÜHRENDES BEISPIEL .................................................................................................................. 8 1.2.1 Aufgabenstellung und Lösungsvorschlag zu einem Sortierverfahren .............................................. 8 1.2.2 Der Aufbau eines C++-Programms ............................................................................................... 9 1.2.2.1 1.2.2.2 1.2.2.3 1.2.2.4 1.2.2.5 1.2.2.6 1.2.2.7 Das Layout des Programms .................................................................................................................... 9 Übersetzung..........................................................................................................................................11 Entwicklungszyklus eines C++-Programms ...........................................................................................13 Kommentare und Präprozessor-Direktiven.............................................................................................14 Standardein-/ Standardausgabe..............................................................................................................17 Hauptprogramm ....................................................................................................................................21 Kommandozeilenversionen verschiedener Compiler ..............................................................................22 1.3 DEKLARATION UND DEFINITION VON BEZEICHNERN............................................................................... 29 1.4 SPEICHERKLASSEN, GÜLTIGKEITSSBEREICHE UND NAMENSBEREICHE ..................................................... 30 1.4.1 Speicherklassen ........................................................................................................................... 30 1.4.2 Gültigkeitsbereiche bzw. Geltungsbereiche.................................................................................. 31 1.4.3 Externe Variable und Funktionen ................................................................................................ 33 1.4.4 Namensbereiche .......................................................................................................................... 34 1.5 AUSDRÜCKE......................................................................................................................................... 37 1.5.1 Arithmetische Operatoren............................................................................................................ 37 1.5.2 Relationale und logische Operatoren........................................................................................... 38 1.5.3 Bitweise logische Operatoren ...................................................................................................... 39 1.5.4 Zuweisungsoperatoren................................................................................................................. 41 1.5.5 Inkrement- und Dekrementoperatoren ......................................................................................... 42 1.5.6 Bedingungsoperator .................................................................................................................... 43 1.5.7 Kommaoperator........................................................................................................................... 43 1.5.8 Vorrang und Assozitivität von Operatoren ................................................................................... 44 1.6 ANWEISUNGEN..................................................................................................................................... 45 1.6.1 Einfache Anweisung .................................................................................................................... 45 1.6.2 Kontrollanweisungen (-strukturen)............................................................................................... 45 1.7 FUNKTIONEN ....................................................................................................................................... 53 1.7.1 Definition und Deklaration von Funktionen................................................................................. 53 1.7.2 Parameterübergabe (Übergabe der Argumente) .......................................................................... 54 1.7.3 Funktionswerte (Ergebniswertrückgabe)...................................................................................... 60 1.7.4 Rekursion .................................................................................................................................... 61 1.7.4.1 1.7.4.2 1.7.4.3 1.7.4.4 Rekursive Funktionen ...........................................................................................................................61 Rekursion und Iteration .........................................................................................................................66 Türme von Hanoi ..................................................................................................................................67 Damen-Problem ....................................................................................................................................72 1.7.5 Überladen von Funktionsnamen (overloading) ............................................................................ 75 1.7.6 Operatorfunktionen ..................................................................................................................... 80 1.7.7 Inline-Funktionen ........................................................................................................................ 80 1.7.8 Funktionsschablonen ................................................................................................................... 81 1.7.9 Objekte als Funktionen................................................................................................................ 82 1.7.10 Spezifikation von Funktionen..................................................................................................... 83 2. DATENTYPEN ....................................................................................................................................... 84 2.1 EINFACHE, FUNDAMENTALE DATENTYPEN ............................................................................................ 84 2.2 ABGELEITETE DATENTYPEN ................................................................................................................. 94 2.2.1 Konstanten .................................................................................................................................. 94 2.2.2 Zeiger (pointer types) .................................................................................................................. 97 2.2.3 Vektoren (Arrays) ...................................................................................................................... 104 2.2.3.1 C-Arrays .............................................................................................................................................104 2.2.3.2 Der C++-Standardtyp vector ...........................................................................................................118 2.2.3.3 Die C++-Stringklasse..........................................................................................................................120 2.2.4 Strukturen.................................................................................................................................. 124 2.2.5 Variantenstruktur ...................................................................................................................... 129 2 Die Programmiersprache C++ 3. BENUTZERDEFINIERTE DATENTYPEN: KLASSEN.................................................................... 131 3.1 KONZEPTE FÜR BENUTZERDEFINIERTE DATENTYPEN ............................................................................ 131 3.1.1 Formale Definitionsmöglichkeiten............................................................................................. 131 3.1.2 Ein einführendes Beispiel: Stapelverarbeitung .......................................................................... 133 3.1.2 Konstruktoren, Klassenvariable und Destruktoren..................................................................... 135 3.1.4 Operatorfunktionen ................................................................................................................... 139 3.1.5 Konstante Komponentenfunktionen............................................................................................ 140 3.1.6 Statische Komponentenfunktionen ............................................................................................. 141 3.1.7 "friend"-Funktionen und "friend"-Klassen ................................................................................. 141 3.1.8 ADT Stapel................................................................................................................................ 142 3.2 ABGELEITETE KLASSEN ...................................................................................................................... 145 3.2.1 Basisklasse und Ableitung ......................................................................................................... 145 3.2.2 Einfache Vererbung................................................................................................................... 146 3.2.3 Klassenhierarchien.................................................................................................................... 149 3.2.4 Virtuelle Funktionen.................................................................................................................. 153 3.2.5 Abstrakte Klassen...................................................................................................................... 156 3.2.6 Mehrfachvererbung ................................................................................................................... 157 3.2.7 Virtuelle Basisklassen................................................................................................................ 159 3.2.8 Generische Datentypen.............................................................................................................. 160 3.3 SCHABLONEN..................................................................................................................................... 163 3.3.1 Klassenschablonen .................................................................................................................... 163 3.3.2 Methodenschablonen................................................................................................................. 164 3.4 EIN-, AUSGABE .................................................................................................................................. 165 3.4.1 Aufbau....................................................................................................................................... 165 3.4.2 Ausgabe..................................................................................................................................... 168 3.4.3 Eingabe..................................................................................................................................... 176 3.4.4 Formatierung in Zeichenketten.................................................................................................. 179 3.4.4.1 strstream für C++ im AT&T-Standard.................................................................................................179 3.4.4.2 stringstream für C++ im ANSI/ISO-Standard.......................................................................................179 3.4.5 Fehlerzustände .......................................................................................................................... 180 3.4.6 Positionieren in Dateien............................................................................................................ 181 3.5 AUSNAHMEBEHANDLUNG ................................................................................................................... 182 3.5.1 Übliche Fehlerbehandlungsroutinen.......................................................................................... 182 3.5.2 Schema zur Ausnahmebehandlung ............................................................................................. 182 3.5.3 Exception-Hierarchie ................................................................................................................ 184 3.5.4 Besondere Fehlerbehandlungsroutinen...................................................................................... 185 3.5.5 Unbehandelte Ausnahmen ......................................................................................................... 186 4. DATENSTRUKTUREN ........................................................................................................................ 188 4.1 DER BENUTZERDEFINIERTE DATENTYP „ARRAY“ BZW. „VEKTOR“ ....................................................... 188 4.1.1 Eindimensionale Felder............................................................................................................. 188 4.1.2 Mehrdimemensionale Felder ..................................................................................................... 188 4.1.3 Darstellung der Datenstruktur Stapel in einem Feld (C-Array).................................................. 188 4.1.4 Darstellung der Datenstruktur Schlange in einem Feld (C-Array).............................................. 190 4.2 LINEARE LISTEN ................................................................................................................................ 196 4.2.1 Einfach gekettete Listen............................................................................................................. 196 4.2.2 Klassenschablonen für verkettete Listen .................................................................................... 205 4.2.2.1 Doppelt gekettete Listen .....................................................................................................................205 4.2.2.2 Ringförmig geschlossene Listen ..........................................................................................................205 4.3 TABELLEN ......................................................................................................................................... 215 4.3.1 Einfache und Sortierte Tabellen ............................................................................................... 215 4.3.2 Hash-Tabellen ........................................................................................................................... 215 4.4 BINÄRBÄUME ..................................................................................................................................... 222 3 Die Programmiersprache C++ 5. DIE C++-STANDARDBIBLIOTHEK.................................................................................................. 241 5.1 DIE C++-STANDARDBIBLIOTHEK UND DIE STL.................................................................................... 241 5.1.1 Die C-Standard-Library ............................................................................................................ 242 5.1.2 Hilfsfunktionen und -klassen...................................................................................................... 246 5.1.2.1 Paare ..................................................................................................................................................246 5.1.2.2 Funktionsobjekte.................................................................................................................................246 5.2 CONTAINER ....................................................................................................................................... 250 5.2.1 Bitset ......................................................................................................................................... 255 5.2.2 Deque........................................................................................................................................ 255 5.2.3 List ............................................................................................................................................ 256 5.2.4 Map........................................................................................................................................... 258 5.2.5 Queue........................................................................................................................................ 261 5.2.6 Set ............................................................................................................................................. 263 5.2.7 Stack ......................................................................................................................................... 265 5.2.8 Vector........................................................................................................................................ 266 5.3 ITERATOREN ...................................................................................................................................... 268 5.3.1 Iteratorkategorien ..................................................................................................................... 270 5.3.2 distance(), advance() und iter_swap() ........................................................................................ 271 5.3.3 Iterator-Adapter ........................................................................................................................ 272 5.3.4 Stream-Iteratoren ...................................................................................................................... 273 5.3.5 Iterator-Traits ........................................................................................................................... 275 5.4 ALGORITHMEN................................................................................................................................... 276 5.5 NATIONALE BESONDERHEITEN ........................................................................................................... 287 5.6 DIE NUMERISCHE BIBLIOTHEK ............................................................................................................ 287 5.6.1 Komplexe Zahlen....................................................................................................................... 287 5.6.2 Grenzwerte von Zahlentypen...................................................................................................... 287 5.6.3 Numerische Algorithmen ........................................................................................................... 287 5.6.4 Optimierte numerische Arrays (valarray)................................................................................... 289 5.6.4.1 5.6.4.2 5.6.4.3 5.6.4.4 5.6.2.5 5.6.2.6 5.6.2.7 5.6.2.8 5.6.2.9 Konstruktoren und Elementfunktionen ................................................................................................289 Binäre Valarray-Operatoren ................................................................................................................289 Mathematische Funktionen .................................................................................................................289 slice ....................................................................................................................................................289 slice_array ..........................................................................................................................................289 gslice ..................................................................................................................................................289 gslice_array.........................................................................................................................................289 mask_array .........................................................................................................................................289 indirect_array......................................................................................................................................289 5.7 TYPERKENNUNG ZUR LAUFZEIT .......................................................................................................... 290 5.8 SPEICHERMANAGEMENT ..................................................................................................................... 290 5.8.1 <new>....................................................................................................................................... 290 5.8.2 <memory> ................................................................................................................................ 290 6. WINDOWS-PROGRAMMIERUNG UNTER VISUAL C++ .............................................................. 291 6.1 MERKMALE VON VISUAL C++............................................................................................................. 291 6.1.1 Visual C++ -Features................................................................................................................ 291 6.1.2 Werkzeuge für den Umgang mit Visual C++ -Strukturen ........................................................... 292 6.1.3 Erstellen einer Windows-Anwendung ......................................................................................... 293 6.2 DIE INTEGRIERTE ENTWICKLUNGSUMGEBUNG ..................................................................................... 307 6.2.1 Projekte und Arbeitsbereiche..................................................................................................... 307 6.2.2 Der Editor ................................................................................................................................. 307 6.2.3 Ressourcen ................................................................................................................................ 308 6.2.4 Dialogfelder .............................................................................................................................. 310 6.2.5 Steuerelemente .......................................................................................................................... 310 6.2.6 Mausereignisse.......................................................................................................................... 311 6.2.7 Menüs, Symbolleisten ................................................................................................................ 312 6.2.8 Texte und Dateien ..................................................................................................................... 316 6.2.8.1 Textverarbeitung.................................................................................................................................316 6.2.8.2 Dateien ...............................................................................................................................................318 6.2.8.3 Serialisierung......................................................................................................................................319 4 Die Programmiersprache C++ 6.3 APPLICATION PROGRAMMING INTERFACE............................................................................................ 320 6.3.1 Funktionsweise von Windows Programmen................................................................................ 320 6.3.2 Eintritt in die Nachrichtenverarbeitung ..................................................................................... 323 6.3.3 Vom API zur MFC ..................................................................................................................... 326 6.4 WINDOWS-PROGRAMMIERUNG MIT DER MFC...................................................................................... 330 6.4.1 Die Assistenten.......................................................................................................................... 330 6.4.1.1 MFC-Anwendungsassistent.................................................................................................................330 6.4.1.2 Der Klassen-Assistent .........................................................................................................................335 6.4.2 Das Doc/View-Modell................................................................................................................ 339 6.4.3 Das MFC-Anwendungsgerüst .................................................................................................... 344 6.4.3.1 6.4.3.2 6.4.3.3 6.4.3.4 6.4.3.5 6.4.3.6 Erzeugen der Fenster...........................................................................................................................344 Anpassen der Fenster ..........................................................................................................................344 Bearbeitung von Kommandozeilenargumenten ....................................................................................348 Die Nachricht WM_PAINT .................................................................................................................350 Zeitgeber (WM_TIMER) ....................................................................................................................353 Die Nachricht WM_COMMAND........................................................................................................353 6.4.4 Die Sammlungsklassen der MFC ............................................................................................... 355 6.5 BILDER, ZEICHNUNGEN UND BITMAPS ................................................................................................. 358 6.5.1 Die grafische Geräteschnittstelle (GDI)..................................................................................... 358 6.5.2 Die Zeichenwerkzeuge ............................................................................................................... 361 7. C# UND .NET ........................................................................................................................................ 363 8. DIE GRAFISCHEN BEDIENOBERFÄCHEN X UND OSF/MOTIF................................................. 364 8.1 XWINDOW BZW. X ............................................................................................................................ 364 8.1.1 Die Komponenten von X ............................................................................................................ 364 8.1.2 Architektur von X-Programmen ................................................................................................. 366 8.1.3 Ein X-Programm ....................................................................................................................... 367 8.2 OSF/MOTIF ....................................................................................................................................... 374 8.2.1 Einführung in OSF/Motif, Xt Intrinsics...................................................................................... 374 8.2.2 Struktur eines Intrinsics-Programmes........................................................................................ 375 8.2.3 Das OSF/Widget Set .................................................................................................................. 380 5 Die Programmiersprache C++ 6 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 7 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 8 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 9 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 10 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); 7 Hinweis: Falls ein Compiler #include <iostream> nicht versteht, entspricht er noch nicht dem C++Standard 11 Die Programmiersprache C++ { } markieren den Beginn und das Ende eines Anweisungsblocks. 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 12 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. 13 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. 14 Die Programmiersprache C++ 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 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 Header-Datei 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 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 10 15 Die Programmiersprache C++ In der dritten Form ist der dem Makro-Bezeichner zugeordnete Text parametrisiert und kann den individuellen Gegebenheiten angepaßte werden, z.B11.: #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 11 vgl. PR12203.CPP 16 Die Programmiersprache C++ 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. 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]; } 12 vgl. Borland C++ 4.0, Programmierhandbuch S. 206 17 Die Programmiersprache C++ 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. 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: - 13 14 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 Jeder Integer-Wert belegt einen Speicherbereich von 4 Bytes L-Wert bezeichnet eine Größe, die auf der linken Seite einer Zuweisung stehen darf. 18 Die Programmiersprache C++ 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 << ") <"; 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 Typ17, 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 15 vgl. 3.4.2 Linksshift zur Bitmanipulation 17 Das sind die grundlegenden Typen oder beliebige, überladene Typen 16 19 Die Programmiersprache C++ 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 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. 18 Behandlung als Zeichenkette vgl. 3.4.2 20 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); 19 20 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 21 Die Programmiersprache C++ 1.2.2.7 Kommandozeilenversionen verschiedener Compiler 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++ 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. 22 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++ 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ührnaren 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 EXEDatei aufgerufen wird). 23 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 24 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 25 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. Textbildschirm-Anwendung unter C++Builder27 Anlegen eines Projekts vom Typ Bildschirmanwendung Start des Dialogs OBJEKTGALERIE über das Menü DATEI/NEU... der IDE des C++Builder. Auswahl des Eintrags „Konsolen-Experte“. Abb.: Fenster zum Auswählen des zu erstellenen Projekts Nach Betätigen der Taste OK wird ein weiterer Dialog geöffnet,unter dem Konsolen-Anwendung festgelegt werden kann. Danach erscheint das Editierfenster des C++Builder: 27 Der C++Builder ist eine visuelle C++-Entwicklungsumgebung von Borland für C++-Programme unter den Betriebssystemen Windows 95 und Windows NT 26 Die Programmiersprache C++ Abb.: Das Quelltextfenster Dieser Text wird vom Anwender durch ein vollständiges Programm ergänzt. Danach wird in der Mauspalette auf START gedrückt. Das Programm wird dadurch kompiliert und ausgeführt. 27 Die Programmiersprache C++ Abb.: Programmcode für eine Konsolenanwendung Über START erfolgt die Kompilierung und Ausführung, die das folgende Konsolenfenster zeigt: Abb.: Konsolenfenster der Textbildschirm-Anwendung 28 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 lang28 sein. Auch Funktionsnamen unterliegen der angegebenen Konvention29. 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: - 28 29 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 29 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 werden30: /* 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 30 vgl. PR12101.CPP 30 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)31. 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 */ 31 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. 31 Die Programmiersprache C++ main() { 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 deklariert32. 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 auftreten33. „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. 32 33 vgl. Zeilen /* 14 */ und /* 15 */ in PR12101.CPP Der Compiler stellt den Fehler nicht fest, wohl aber der Linker mit „duplicate global“ 32 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 Aussehen34 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 Aussehen35: #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() 34 35 PR14203.CPP PR14205.CPP 33 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); } 34 Die Programmiersprache C++ 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;. 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. 35 Die Programmiersprache C++ 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() { // .. } } 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. 36 Die Programmiersprache C++ 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 +, -36. Läßt sich mit dieser 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); 36 Punktrechnung geht vor Strichrechnung, die modulo-Operation zählt zur Punktrechnung) 37 Die Programmiersprache C++ 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: 38 Die Programmiersprache C++ && Bedingung1 0 Bedingung2 1 0 0 0 1 0 1 ! Bedingung 0 1 1 0 Bedingung2 0 1 || 0 0 1 1 1 1 Bedingung1 Abb. 1.5-1: Wahrheitstafeln zur Beschreibung logischer Operatoren Aufgabe: Überprüfe, welches Wort das folgende Programm37 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; 37 PR15201.CPP 39 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. Zweierkomplement38. 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 Vorzeichenbit39 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 38 sog. „unechtes Komplement“; das Zweierkomplement wird gebildet, indem alle Bits invertiert werden und dann auf das Ergebnis 1 addiert wird 39 Das Bit an der am weitesten links stehenden Position 40 Die Programmiersprache C++ Bsp.: Bestimmen der int-Länge mit Bitoperatoren #include <iostream.h> 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 schreiben40 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 „*= “. 40 Hinweis: Diese Fassung läuft um Bruchteile von Sekunden schneller ab 41 Die Programmiersprache C++ Das Operationszeichen (+, -, *, /, usw.) steht immer vor dem Operator der Zuweisung. Die Anweisung x -= 1; ist grundsätzlich verschieden zu 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 42 Die Programmiersprache C++ 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 << " und " << j << " ist " << (i > j ? i : j) << endl; 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) 43 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 Bedeutung41 zugeordnet werden. Die syntaktischen Eigenschaften der Operatoren (Assoziativität, Präzedenz) können allerdings nicht verändert werden. 41 z.B. die spezielle Bedeutung der Operatoren << und >> im Zusammenhang mit Ein- und Ausgabe 44 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 Ausdrucksanweisung42 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.: 42 Jedem Ausdruck, dem ein Semikolon folgt, ist eine Ausdrucksanweisung 45 Die Programmiersprache C++ #include <iostream.h> main() { // 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.43: #include <iostream.h> void main() { int i, j; for (i = 0, j = 9; ((i <= 9) && (0 <= j)); i++, j--) cout << "\ni = " << i << " j = " << j; } 43 PR16202.CPP 46 Die Programmiersprache C++ 2. Die "while"-Anweisung44 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"-Anweisung45 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-Zahlen46 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; 44 kopfgesteuerte Schleife fußgesteuerte Schleife 46 PR16206.CPP 45 47 Die Programmiersprache C++ do { 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. 48 Die Programmiersprache C++ Bsp.: #include <iostream.h> int main() { int zaehler; cout << "Start einer Schleife mit continue " << endl; 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"; } 49 zu einem Block Die Programmiersprache C++ 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"; 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. 50 Die Programmiersprache C++ 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> 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. 51 Die Programmiersprache C++ goto Anweisung; goto marke: Anweisung; marke: Anweisung; if (Ausdruck) wahr Anweisung case Anweisung 1 if - else falsch 2 switch switch (Ausdruck) 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 52 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; } 53 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.47: #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; } 47 PR17201.CPP 54 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 darf den Wert des Arguments verändern. 55 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) 48 #include <iostream.h> #include <conio.h> void striche(int, int, char z = '-'); void main() { striche(0,40); striche(0,40,'$'); striche(0,40,'@'); striche(0,40,‘+‘); } void striche(int spalte, int zeile, int anz, int z) { for (int i = spalte; i < anz; i++) cout << z; cout << endl; } 2) 49 #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'; } 48 49 PR17202.CPP PR17204.CPP 56 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: 57 Die Programmiersprache C++ d: x: o: u: c: s: e: f: g: dezimal hexadezimal oktal 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-Tabelle50 // #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 50 PR17205.CPP 58 Die Programmiersprache C++ formale Parameter übergeben werden dürfen51. 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.52: #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 Funktionsprototyp53 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. 51 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. 52 PR17203.CPP 53 d.h. vor ihrem Aufruf deklariert bzw. definiert sein 59 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); 60 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); } 61 Die Programmiersprache C++ 3. Potenzieren 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. 0.52 = 0.25 . Ebenso sind Hochzahlen mit negativen Werten (Ganzzahlen) 1 1 = 4. möglich, z.B.: 0.5−2 = 2 = 0.5 0.25 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)54 auftreten: #include <iostream.h> void striche(short st) { for (int i = 1; i <= st; i++) cout << "- "; } 54 Rekursionstiefe: Anzahl der Aufrufe der Funktion seit Beginn der Programmausführung minus der Anzahl der Rückgaben an das ausführende Programm 62 Die Programmiersprache C++ unsigned long fib(short xN, short stufe) { unsigned long x, y; cout << "\n"; striche(stufe); 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 = 3 = N N N N = = = 3 2 = = = = 2 X 2 = = = = 2 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 63 Die Programmiersprache C++ - - Eingang: - - Ausgang: - Ausgang: N Ausgang: N = Ergebnis: 5 N N = 5 = = 3 X 1 1 X = 1 Y = 0 X = 1 Y = 1 = 3 Y = 2 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 2N-1 - 1 mal aufgerufen. Man sagt: Der Algorithmus verhält sich proportional zu 2N-1 55bzw. 2N. Allgemein ist ein Algorithmus von exponentieller Komplexität, falls es eine beliebige Basis M56 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; 55 56 Die 1 wird hier vernachlässigt, da sie bei großen Werten von N praktisch keinen Beitrag liefert. hier 2 64 Die Programmiersprache C++ vvx = vx; vx = x; } return(x); } 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 n− 2 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); } } 65 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.57: 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 recursion58“. 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; } 57 58 PR17307.CPP Endrekursion 66 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ückgabeadresse59 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 S1 = 1): S n = 2 ⋅ (S n−1 ) + 1 Sn = 2n − 1 59 d.h. die Stelle, an der nach der Ausführung der gerufenen Funktionsprozedur die Programmkontrolle übergeben wird 67 Die Programmiersprache C++ Die allgemeine Lösung der Aufgabe besteht in 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 so60, 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: 60 das ist der eigentliche Trick, den die Rekursion ermöglicht 68 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ß. 69 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 { private61: 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. 61 das Schlüsselwort private kann entfallen, weil die Voreinstellung private ist 70 Die Programmiersprache C++ 3. Implementierung in objektorientierter Darstellung62 /* 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"; 62 PR17305.CPP 71 Die Programmiersprache C++ cout << "\n\t\t\t-------------------\n\n"; 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 (2,_,_,_) (1,_,_,_) 2 3 4 1 S S (1,3,_,_) 2 S (1,4,_,_) 4 2 S 3 4 4 (2,4,_,_) 3 1 S (2,4,1,_) (1,4,2,_) (2,4,1,3) Abb. 1.8-4: Plan zur Lösung des 4-Damen-Problems 72 Die Programmiersprache C++ Von der Ausgangssituation (keine Dame ist positioniert, Wurzel) werden bestimmte Lösungsvariablen verfolgt. Im ungünstigsten Fall führt die bearbeitete Variante in 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> 73 Die Programmiersprache C++ class damen { private: 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() 74 Die Programmiersprache C++ Es gibt insgesamt 92 Lösungen63, 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 63 vgl. PR17308.CPP 75 Die Programmiersprache C++ Rückgabetyp wird nicht in den Namen codiert. Genausowenig wird zwischen einem 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 } 76 Die Programmiersprache C++ int main() { cout << "\n Die naechste Groesse << naechsteGroesse(5); cout << "\n Die naechste Groesse <<naechsteGroesse('F'); cout << "\n Die naechste Groesse << naechsteGroesse(5,4); cout << "\n Die naechste Groesse << naechsteGroesse('F',4); } von 5 ist " von F ist " von 5 ist nach 4 Einheiten " von F ist nach 4 Einheiten " 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 werden64, 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 Konvertierung65 und zu einer passenden Version. 64 65 vgl. 1.7.2, 4. der char-Wert ‘D’ wird in seinen entsprechenden int-Wert (68) umgewandelt. 77 Die Programmiersprache C++ Algorithmus zur Homonymenauflösung66 (1) Bestimme die Menge F jener Funktionen, die in Namen und Anzahl der Argumente67 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 pi die Menge Fi’ aller Funktionen aus F, die bzgl. des Datentyps für das Argument pi 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. 66 Vereinfachte Darstellung, im Detail beschrieben in Ellis, M.A. und Stroustrup, B.: The Annoted C++ Reference Manual, Addison Wesley, Reading MA, 1990 67 Arität 78 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. 79 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änkt68. In C++ können fast alle Operatoren (Ausnahmen sind: .,.*,::, ? : , sizeof) überladen werden. Man kann deshalb die Bedeutung69 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. Eingabestrom70, 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; } 68 d.h.: Defaultargumente sind nicht erlaubt Semantik 70 das wurde in der iostream-Klasse so festgelegt 69 80 Die Programmiersprache C++ main() { int n; cout << "Berechne die Fakultaet von "; cin >> n; cout << "Ergebnis: " << fak(n) << endl; 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. Ti kann Standard-Datentyp oder ein vom Benutzer definierter Typ sein71. 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: 71 Anstatt class kann auch typename geschrieben werden 81 Die Programmiersprache C++ template <class T> T min(T a, T b) { return (a < b ? a : b); } 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 Zeichenketten72 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. 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 werden73. 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. Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften von Objekten haben. 72 „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 73 Die Technik wird in den Algorithmen und Klassen der C++-Standardbibliothek häufig eingesetzt. 82 Die Programmiersprache C++ 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 precondition74 und postcondition75 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 10076 #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); } 74 Abkürzung pre Abkürzung post 76 PR17999.CPP 75 83 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-Datentypen77: a) folgende interne Darstellung s int 15 long int s 31 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-1: Ganzzahlige 16-Bit-Datentypen short int s 15 int, long int 0 s 31 0 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-2: Ganzzahlige 32-Bit-Datentypen float s Exponent 31 Mantisse 22 0 double s Exponent 63 Mantisse 51 0 s: Vorzeichenbit (0 = positiv, 1 = negativ) Abb.: 2.1-3: Gleitkomma-Datentypen 77 Für Borland C++ ist die IBM-PC-Familie Ausgangspunkt. Somit bestimmt die Architektur der Intel 8088und 80x86-Mikroprozessoren die Auswahl der internen Darstellung für verschiedene Datentypen 84 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 3.4 ⋅ 10 −38 bis 3.4 ⋅ 10 38 17 . ⋅ 10 −308 bis 17 . ⋅ 10 308 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 3.4 ⋅ 10 −38 bis 3.4 ⋅ 10 38 17 . ⋅ 10 −308 bis 17 . ⋅ 10 308 3.4 ⋅ 10 −4932 bis 11 . ⋅ 10 4932 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 limits78. C++ bietet die Möglichkeit, den Zahlenbereich mit einer Funktion abzufragen, z.B.: 78 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. 85 Die Programmiersprache C++ #include <iostream> #include "c:\cppbuch\include\limits" 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 79herangezogen werden, z.B.: unsigned int x; Insgesamt kann man 9 verschiedene "Integer"-Typen durch Kombination von "short", "long", "unsigned" mit "int" unterscheiden. + ++ -+ * / % = *= /= %= += -= < > <= >= == != << >> 79 +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 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 signed und unsigned sind sog. Modifizierer, da sie die Bedeutung des folgenden Begiffs ändern 86 Die Programmiersprache C++ & ^ | ~ <<= >>= &= ^= |= i & 7 i ^ 7 i | 7 ~i i <<= 3 i >>= 3 Bitweises UND Bitweises XOR (Exklusives Oder) 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]80; 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; 80 Eckige Klammern bedeuten: Typname, Variablenliste können weggelassen werden. Sinnvoll ist meistens nur das Weglassen der Variablenliste 87 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 Typ-Spezifizierer zur Deklaration von Bezeichnern verwendet werden, z.B.81 // 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: 81 PR21000.CPP 88 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 89 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 sizeof82 kann die Größe eines Datentyps bestimmt werden, z.B.83: #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; } 82 83 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 90 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 84 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 NaN84“ 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 NaN: „not a number“ 91 Die Programmiersprache C++ bool is_modulo true, falls bereichsüberschreitende Operationen wieder eine gültige Zahl ergeben. Art der Rundung Ganzzahlen: rounded_toward_zero (= 0) Gleitkommazahlen: round_to_nearest (= 1) round_style 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)85 wird durch (typ) ausdruck In C++ ist auch typ (ausdruck) möglich87, z.B.: 86veranlaßt. int i = 5; double d; 85 Die explizite Konvertierung bezeichnet man als cast Aus der Programmiersprache C übernommene Schreibweise 87 Funktionsschreibweise 86 92 Die Programmiersprache C++ d = (double) i; bzw. d = double (i) 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. 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 bspw. dann auch 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. 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. 93 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'88. 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 88 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. 94 Die Programmiersprache C++ 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. 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; 95 Die Programmiersprache C++ 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. Bsp.: "Demonstation symbolischer Konstanten89 #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.90: #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; } 89 90 PR22101.CPP PR22104.CPP 96 Die Programmiersprache C++ void benutzevolatile(void) { volatile int k; cout << "Zaehlen mit einer Volatilevariablen " << endl; for (k = 1; k <= 100; k++) cout << k; } 2.2.2 Zeiger (pointer types) Definition Zeigertypen sind Datentypen zur Manipulation von Objektadressen. Ein Zeigertyp entsteht, indem vor dem deklarierten Namen ein * (Stern)91 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 Zeigerderefenzierung92 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). 91 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 92 97 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, Dereferenzierung93 #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 93 j PR22304.CPP 98 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" 99 Die Programmiersprache C++ Zuweisung = 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 100 Die Programmiersprache C++ Objekt des Typs Typname durch Reservierung von sizeof(Type) Bytes im freien 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. Bsp94.: #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'; 94 vgl. PR22202.CPP 101 Die Programmiersprache C++ puffer[3] = '\0'; cout << "Adresse von Puffer = "; ausgabeZeiger(&puffer); 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: 102 Die Programmiersprache C++ const char *const kzk = "sepp"; // Auch erlaubt: const char* const kzk = "sepp"; kzk[0] = 'd'; // Fehler kzk = "depp"; // Fehler 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 103 Die Programmiersprache C++ Wie sortiert wird, bestimmt die Funktion fcompare(). Sie erwartet zwei Zeiger auf ein beliebiges Element als Argument und muß zurückgeben: 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.: 104 Die Programmiersprache C++ double x[3] = {1,0,0}; double y[3] = {0,1,0}; double z[] = {0,0,1}; 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 105 Die Programmiersprache C++ "\0"-Zeichen endet. Derartige Zeichenketten sind die einzige Form von C-Arrays, die auf einmal ausgegeben werden können, z.B.: cout << s; 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-Datei95 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) 95 Header <cstring> 106 Die Programmiersprache C++ -cmp()-Funktionen Sie vergleichen die beiden ersten Argumente. Sie liefern -1 zurück, falls a < b96 gilt, 0 für a = b und +1 für a > b. 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 96 Die Relationen < bzw > bezeichnen hier die lexikalische Reihenfolge 107 Die Programmiersprache C++ 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. 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. 108 Die Programmiersprache C++ 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' 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"}; 109 Die Programmiersprache C++ [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. 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 110 Die Programmiersprache C++ - Implementierung97 #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; // } } 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); } 97 PR22307.CPP 111 Die Programmiersprache C++ 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) 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 112 Die Programmiersprache C++ 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); } 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 113 Die Programmiersprache C++ Bsp.: Dynamische Speicherbelegung für eine Matrix98 // 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 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. 98 PR22310.CPP 114 Die Programmiersprache C++ 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"99 bestimmt die Anzahl der Argumente in "argv"100. "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 Programmen101 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 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. 99 argument counter argument value 101 PR22311.CPP 100 115 Die Programmiersprache C++ [0] a : \ p r [1] a : \ p r [2] a : \ p r 2 2 2 2 2 2 3 3 1 1 3 \0 P \0 1 1 . C P 1 1 . L S T \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 // Argument cout << "Datei: " << /*eingabedatei*/ argv[1] << "\t" << eingabe.tellg() << " Bytes " << endl; 116 Die Programmiersprache C++ /* 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-Klasse102. 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) cout << *arge++ << endl; } 102 vgl. 3.4 117 Die Programmiersprache C++ 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 Vektoren103 stellt einige Dienstleistungen zur Verfügung, z.B.: - - size() ermittelt die Größe des Vektors, im vorliegenden Fall zeigt „cout << v.size() << endl;“ die größe des Vektors an. 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() { int n; // int x[20]; // vector<int> x(20); vector<int> x; // anfaengliche Groesse ist 0 103 vgl. 5.2.8 118 Eingabe Ausgabe Bubble-Sort Sortieren durch Auswaehlen Sortieren durch Einfuegen Tauschen Die Programmiersprache C++ 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; tauschen(x,min,i); } } 119 Die Programmiersprache C++ 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 Ausschneiden von Teilzeichenketten geht, ohne daß man dazu ein spezielle Methode braucht. 120 Die Programmiersprache C++ 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). 121 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 122 Die Programmiersprache C++ // gibt string(*this,pos,n).compare(s) zurück. Es werden nur die Zeichen ab Position pos // 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-Operationen104. #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"); 104 PR22230.CPP 123 Die Programmiersprache C++ eineStringkopie = zuweisungsKopie; cout << eineStringkopie << endl; // Zuweisung einer Zeichenkette 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 Datenaggregat105, 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 } 105 s) << << << << << << "\n"; "\n"; "\n"; "\n"; "\n"; "\n"; Nicht objektorientierter Aspekt des struct-Datentyps 124 Die Programmiersprache C++ 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]; 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; } 125 Die Programmiersprache C++ void main() { struct koord position = {13.11,19.40}; cout << "\nVorher: " << position.xk << " " << position.yk; tauschen(&position); cout << "\nNachher: " << position.xk << " " << position.yk; } 2. Sortieren von Datensätzen im Arbeitsspeicher106 - - 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; 106 PR22403.CPP 126 Die Programmiersprache C++ void void void void void aufnehmenSaetze(eintrag**, int&, char*); ausgebenSaetze(eintrag**, int); sortierenSaetze(eintrag**, int); ausgebenDatei(eintrag**, int, char*); 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) { 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; 127 Die Programmiersprache C++ // 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; 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(); } 128 Die Programmiersprache C++ 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); 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; 129 Die Programmiersprache C++ 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. 130 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 gekapselt107 . 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: 107 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 131 Die Programmiersprache C++ klassenobjekt.komponentenname 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. 132 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 Elememte108 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 108 PR31201.CPP 133 Die Programmiersprache C++ cerr << "Fehler in push() : Stapel ist voll! \n"; return *this; /* 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; } 134 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> 135 Die Programmiersprache C++ int f(int i) { cout << i << " "; return i; } 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; 136 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. 137 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]109; // 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. 109 Das Feld stacks[100] wird hier durch sukzessives Aufrufen von intStapel mit 100 leeren Stapeln initialisiert 138 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; } 139 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); 140 } 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); }; 141 Die Programmiersprache C++ 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 << ">"; } „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); }; 142 Die Programmiersprache C++ const int intStapel :: stapelgroesse = 100; // Kopierkonstruktor 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“110 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: 110 PR31810.CPP 143 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; } } 144 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. 145 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 Darstellung111 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; } 111 PR22306.CPP 146 Die Programmiersprache C++ Zugriffsfunktionen: pop(), top() und push() // Zugriffsfunktionen pop() PruefeintStapel& PruefeintStapel :: pop() { if (!test(stapeltiefe() > 0, "pop")) // Vorbedingung intStapel::pop(); return *this; } // 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&)"); } 147 Die Programmiersprache C++ 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". 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: ......................... }; 148 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.112 #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 112 PR22312.CPP 149 Die Programmiersprache C++ Der Basisklassenkonstruktor muß explizit angegeben werden. Ausnahme: Es handelt sich um den Standardkonstruktor bzw. Defaultkonstruktor, z.B.113: #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.114: #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; } 113 114 PR22316.CPP PR22317.CPP 150 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.115: #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. 115 PR22318,CPP 151 Die Programmiersprache C++ Grundsätzlich werden Destruktoren immer in umgekehrter Reihenfolge wie die Konstruktoren aufgerufen, z.B.116: #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 116 PR22312.CPP 152 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.117: #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(); 117 PR22308.CPP 153 Die Programmiersprache C++ delete z; 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. 154 Die Programmiersprache C++ 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; 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; ... 155 Die Programmiersprache C++ 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; } 3.2.5 Abstrakte Klassen Es gibt Klassen mit rein virtuellen Funktionen (abstrakte Klassen)118. 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.119: #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; } 118 119 Eine Klasse, die mindestens eine rein virtuelle Funktion besitzt, heißt abstrakte Klasse PR22310.CPP 156 Die Programmiersprache C++ 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. 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.: 157 Die Programmiersprache C++ Bsp.120: Konstruktoren und Destruktoren bei Mehrfachvererbung #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 : 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; } 120 PR22313.CPP 158 Die Programmiersprache C++ 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 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; } }; 159 Die Programmiersprache C++ 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 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; 160 Die Programmiersprache C++ 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) { 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; } 161 Die Programmiersprache C++ // 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; } 162 Die Programmiersprache C++ 3.3 Schablonen Schablonen (Templates) können als "Meta-Funktionen" aufgefaßt werden, die zur Übersetzungszeit neue Klassen bzw. neue Funktionen121 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. 121 vgl. 1.7.7 163 Die Programmiersprache C++ 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) {} 164 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 Dateien122, 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. 122 Ein Stream ist gewissermaßen die objektorientierte Sichtweise einer Datei 165 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. 166 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. 167 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) sind123: 123 vgl. Borland C++ für Windows Version 4.0: Referenzhandbuch 168 Die Programmiersprache C++ Bezeichnung: skipws left, right, internal dec, oct, hex showbase showpoint uppercase showpos scientific fixed unitbuf stdio Bedeutung: Trennzeichen124 sind zu ignorieren Ausrichtung der Ausgabe 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. 124 Trennzeichen steht für den englischen Begriff „whitespace“ und bedeutet ein beliebiges Zeichen aus der Menge {‘ ‘,’\t’,’\n’} 169 Die Programmiersprache C++ 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"; } 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. 170 Die Programmiersprache C++ 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 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; 171 Die Programmiersprache C++ 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; } 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 fixed125 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() 125 Der Manipulator fixed fehlt beim aktuellen GNU C++ Compiler (Version 2.95), deshalb wird er hier zusätzlich definiert. 172 Die Programmiersprache C++ 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 Zeichen126, das zum Auffüllen des Ausgabefelds benützt wird, ist ebenfalls in einer ios-Variablen gespeichert. Es kann durch die Methode char ios::fill() 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. 126 normalerweise das Leerzeichen 173 Die Programmiersprache C++ Bsp.: „Fälschungssichere Ausgabe von Geldbeträgen“127 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); 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 Zeichen128 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. 127 PR34205.CPP putback(char z) erlaubt einem Programm, ein „ungewolltes“ Zeichen in den Strom zurückzuschreiben, damit es an anderer Stelle gelesen werden kann. 128 174 Die Programmiersprache C++ 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. 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 Öffnungsstrategien. Mögliche (durch "oder" kombinierbare) Werte sind: ios::out ios::ate ios::binary ios::app // // // // 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 existiert129 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)130 129 130 implizit, wenn ios::out angegeben ist oder weder ios::ate noch ios::app angegeben wird Konstruktor 175 Die Programmiersprache C++ 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. 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. Aus 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 176 Die Programmiersprache C++ 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 // dedeutet: unerlaubze 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. 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') 177 Die Programmiersprache C++ entspricht get(), überträgt jedoch auch noch das Zeichen „delim“, falls die 3. Bedingung auftritt131. 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. 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. 131 Das Trennzeichen wird gelesen, jedoch nicht in den Puffer übernommen. 178 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.: 179 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. 180 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 } 181 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 // ... 182 Die Programmiersprache C++ 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> 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 Stack132, 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“-Block133 einschließen. Direkt nach dem try-Block muß ein catch-Block folgen, der die eigentliche Ausnahmebehandlung 132 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. 133 throw, try, catch sind C++-Schlüsselworte 183 Die Programmiersprache C++ 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(...) { ........ } 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. Wude 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 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(); }; } 184 bad_exception Die Programmiersprache C++ 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.: 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 185 Die Programmiersprache C++ 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(); 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 terminate134 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 } 134 Die Funktionsprototypen und Definitionen dieser Routinen befinden sich in except.h 186 Die Programmiersprache C++ 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(...) { 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. 187 Die Programmiersprache C++ 4. Datenstrukturen 4.1 Der benutzerdefinierte Datentyp „Array“ bzw. „Vektor“ 4.1.1 Eindimensionale Felder vgl. 2.2.3 4.1.2 Mehrdimemensionale Felder 4.1.3 Darstellung der Datenstruktur Stapel in einem Feld (C-Array) Der ganzzahlige Stapel135 kann zu einer Klassenschablone136 verallgemeinert werden, der Datenwerte beliebiger Datentypen aufnehmen kann. #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>&); 135 136 vgl. 3.1.8: ADT-Stapel vgl. auch 5.2.7 188 Die Programmiersprache C++ 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) { 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; } // 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 << ">"; } Aufgaben zur Stapelverarbeitung Mit Hilfe der Klassenschablone „Stapel“ soll eine Anwendung zur Auswertung voll geklammerter arithmetischer Ausdrücke erstellt werden. 189 Die Programmiersprache C++ 4.1.4 Darstellung der Datenstruktur Schlange in einem Feld (C-Array) Grundlagen137 In einer Schlange werden Datenelemente grundsätzlich am Ende des aktuellen Datenbestands eingefügt. Die Verarbeitung von Datenelementen (Entfernen von Daten aus der Schlange) erfolgt grundsätzlich vorn, d.h. das am längsten im Datenbestand gehaltene Datenelement wird aus der Schlange entfernt und verarbeitet (FIFO). Start: vorn hinten Hinzufügen eines Elements: vorn hinten Hinzufügen von 5 Elementen: vorn hinten Entfernen von 2 Elementen: vorn hinten Hinzufügen von einem Element: vorn hinten 137 vgl. 5.2.2 190 Die Programmiersprache C++ Entfernen von einem Element: vorn hinten Hinzufügen von zwei Elementen: vorn hinten Abb.: Zur Vermeidung von „overhead“ wird die Datenstruktur „array“ zur Aufnahme der Schlange zirkular interpretiert. Berücksichtigt wird dies durch zwei Zeiger „vorn“ bzw. „hinten“. Falls ein Datenelement hinzugefügt wird, wird „hinten“ um eine Positionierungseinheit erhöht. Falls ein Element aus der Schlange entfernt wird, wird „vorn“ erhöht. Falls „hinten“ das Ende vom „Array“ erreicht hat, dann wird der Zeiger auf den Anfang zurückgesetzt. Erreicht „vorn“ das Ende des „Array“, dann wird dieser Zeiger ebenfalls auf den Beginn des „Array“ gesetzt. In diesem Sinne jagt „vorn“ „hinten“ durch den „Array“. Falls „vorn“ „hinten“ einholt, ist die Schlange voll. Klassenschablone für die Datenstruktur Schlange // Schnittstellenbeschreibung fuer den benutzterdefinierten Datentyp: // "schlange" template <class elt> class { private: elt *elemente; int vorn, hinten; int groessterIndex; schlange // "array" // Anfangs-, Endpositioen // Groesse: array - 1 public: // Initialisieren / Beenden schlange(int gr = 100); // Konstruktor ~schlange(); // Destruktor // Zugriff / Modifikation void hinzufuegen(elt); schlange<elt>& operator+=(elt); elt entfernen(); void bereinigen(); elt erstes(); // erstes Element elt letztes(); // letztes Element 191 Die Programmiersprache C++ // Bearbeitungsfunktionen private: int akt; // Bearbeitungs-Index public: void ruecksetzen(); // Durchlauf von "vorn" nach "hinten" bool beendet(); bool naechstes(); elt &aktuell(); int index(); // // Attribute bool leer(); bool voll(); int groesse(); // Vergleichen friend order vergleichen(schlange<elt>&, schlange<elt>&); friend bool gleich(schlange<elt>&, schlange<elt>&); // Elemente gleich? Pruefen auf Identitaet // Kopieren private: void kopiereElemente(schlange<elt>&); public: schlange(schlange<elt>&); schlange<elt>& operator=(schlange<elt>&); // Verarbeitung bool enthaelt(elt); bool enthaeltGleiches(elt); // Eingabe / Ausgabe friend ostream& operator<<(ostream&, schlange<elt>&); }; // Methoden fuer den benutzerdefinierten Datentyp: Schlange /* Initialisieren / Beenden */ template <class elt> schlange<elt>::schlange(int gr) : elemente(new elt[gr]), groessterIndex(gr-1) { assert(gr > 1); bereinigen(); } template <class elt> void schlange<elt>::bereinigen() { vorn = 0; hinten = 0; } template <class elt> schlange<elt>::~schlange() { delete [] elemente; } // Zugriff / Modifikation template <class elt> void schlange<elt>::hinzufuegen(elt mrk) { assert (!voll()); elemente[hinten++] = mrk; if (hinten > groessterIndex) hinten = 0; // } 192 Die Programmiersprache C++ template <class elt> schlange<elt>& schlange<elt>::operator+=(elt mrk) { hinzufuegen(mrk); return *this; } template <class elt> elt schlange<elt>::entfernen() { assert(!leer()); elt mrk = elemente[vorn++]; if (vorn > groessterIndex) vorn = 0; // return mrk; } template <class elt> elt schlange<elt>::erstes() { assert(!leer()); return elemente[vorn]; } template <class elt> elt schlange<elt>::letztes() { assert(!leer()); if (hinten == 0) return elemente[groessterIndex-1]; else return elemente[hinten-1]; } // Berbeitungsfunktionen template <class elt> void schlange<elt>::ruecksetzen() { akt = vorn-1; } template <class elt> bool schlange<elt>::naechstes() { if (++akt > groessterIndex) akt=0; return !beendet(); } template <class elt> elt& schlange<elt>::aktuell() { return elemente[akt]; } template <class elt> bool schlange<elt>::beendet() { return akt == hinten; } template <class elt> int schlange<elt>::index() { if (vorn <= akt) return ((akt - vorn) + 1); else // return ((groessterIndex - vorn) + akt + 1); } 193 Die Programmiersprache C++ // Attribute template <class elt> int schlange<elt>::groesse() { if (leer()) return 0; if (vorn < hinten) return hinten - vorn; else return (groessterIndex - vorn) + hinten + 1; } template <class elt> bool schlange<elt>::leer() { return vorn == hinten; } template <class elt> bool schlange<elt>::voll() { return (vorn == hinten+1) || ((vorn == 0) && (hinten == groessterIndex)); } // Vergleichen template <class elt> order vergleichen(schlange<elt>&, schlange<elt>&) { // Nicht implementiert: "vergleichen(schlange<elt>&, schlange<elt>&)" return OHNE; } // Elemente gleich? Vergleich auf Identitaet template <class elt> bool gleich(schlange<elt>& s1, schlange<elt>& s2) { if (&s1 == &s2) return TRUE; // Identitaet if (s1.groesse() != s2.groesse()) return FALSE; // s1.ruecksetzen(); s2.ruecksetzen(); while(s1.naechstes() && s2.naechstes()) if (s1.aktuell() != s2.aktuell()) return FALSE; if (!s1.beendet()) return FALSE; if (s2.naechstes()) return FALSE; // s1 ist beendet; weiter mit s2 return TRUE; } // Kopieren //private: template <class elt> void schlange<elt>::kopiereElemente(schlange<elt>& s) { vorn = s.vorn; hinten= s.hinten; memcpy(elemente, s.elemente, groessterIndex*sizeof(elt)); } template <class elt> schlange<elt>::schlange(schlange<elt>& s) : elemente(new elt[s.groessterIndex]), groessterIndex(s.groessterIndex) { kopiereElemente(s); } 194 Die Programmiersprache C++ template <class elt> schlange<elt>& schlange<elt>::operator=(schlange<elt>& s) { if (this == &s) return *this; // delete [] elemente; groessterIndex = s.groessterIndex; elemente = new elt[s.groessterIndex]; kopiereElemente(s); return *this; } // Verarbeitung template <class elt> bool schlange<elt>::enthaelt(elt mrk) { ruecksetzen(); while (naechstes()) if (mrk == aktuell()) return TRUE; // Identitaet return FALSE; } template <class elt> bool schlange<elt>::enthaeltGleiches(elt mrk) { ruecksetzen(); while (naechstes()) if (gleich(*mrk, *aktuell())) return TRUE; // Gleichheit return FALSE; } // Ausgabe template <class elt> ostream& operator<<(ostream& strm, schlange<elt>& s) { s.ruecksetzen(); while (s.naechstes()) strm << '\t' << *s.aktuell() << '\n'; return strm; } 195 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. 196 Die Programmiersprache C++ Durch 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) 197 Die Programmiersprache C++ zAnfang wird anschließend auf das neu erzeugte Element gesetzt: 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 sichern138. 138 vgl. PR43105.CPP 198 Die Programmiersprache C++ #include #include #include #include <iostream.h> <fstream.h> <string.h> <stdlib.h> 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(); } } 199 Die Programmiersprache C++ liste :: ~liste(void) { // Im Destruktor wird die Liste wieder in eine externe // Datei geschrieben eintrag* zLoe; 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; 200 Die Programmiersprache C++ // Weiterer Spezialfall: Das zu loeschende Element ist // das erste Element in der Liste if (strcmp(zElem->name,info) == 0) { zAnfang = zElem->zNachf; delete zElem; 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; } 201 Die Programmiersprache C++ void einfuegenElement(liste& adressen) { eintrag* zelem; if ((zelem = new eintrag) == NULL) cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden"; else { 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 Library139 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; } 139 vgl. 5.2.7 202 Die Programmiersprache C++ 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) { 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; } 203 Die Programmiersprache C++ } 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 "; 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:"; 204 Die Programmiersprache C++ cout << "\n\t" << *iter << endl; list<eintrag>::iterator liter; liter = iter; // iter = erase(liter); Fehler !!! } else cout << "\n\tNicht gefunden"; } 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 205 Die Programmiersprache C++ type Zeiger = record ............ ............ Nachf : ^Zeiger; end; Abb.: Eine ringförmige Datenstruktur kann durch die Anweisung ZGR1^.Nachf := ZGR; 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++140 // 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 140 vgl. ringkno.h 206 Die Programmiersprache C++ daten: nachf: Abb.: Liste mit Knoten // Schnittstellenfunktionen 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. 207 Die Programmiersprache C++ 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 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 208 Die Programmiersprache C++ 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ösung141: #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 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(); 141 PR22221.CPP 209 Die Programmiersprache C++ } } 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> 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 210 Die Programmiersprache C++ 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: links daten rechts ...... ..... 4 1 2 3 Abb.: Klassenschablone „doppelt verketteter RingKnoten“142 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; }; 142 dringkn.h 211 Die Programmiersprache C++ 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 // 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; } 212 Die Programmiersprache C++ 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 Listenknoten143 Falls der Aufbau einer geordneten Folge von doppelt verketteten Listenknoten im Rahmen einer ringförmig verketteten Liste gelingt, kann die Liste in Vorwärtsrichtung (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; } 143 PR22225.CPP 213 Die Programmiersprache C++ 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; } } 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; } 214 Die Programmiersprache C++ 4.3 Tabellen 4.3.1 Einfache und Sortierte Tabellen 144 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 Listenknoten145. Bsp.: Erstellen und Verwalten eines Telefon-Verzeichnisses mit einer Hash-Tabelle im Rahmen des „seperate chaining“. Das Verzeichnis ist bisher in einer 144 145 PR44205.CPP PR44310.CPP 215 Die Programmiersprache C++ Textdatei enthalten. Die in dieser Tabelle gespeicherten Sätze haben folgenden Aufbau: 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. // // // Hashing mit Kollisionsaufloesung durch chaining #include <fstream.h> #include <iomanip.h> #include <iostream.h> #include <stdlib.h> // Beschreibung der Listenknoten struct element { char *name; unsigned nummer; element *nachf; }; 216 Die Programmiersprache C++ // Beschreibung der Klasse einfach gekettete Liste class liste { private: element *zStart; public: liste(): zStart(NULL){} ~liste(); void entferneKnoten(const char *s); void einfuegenLiKnoten(const char *s, unsigned nummer); element *suchePosition(const char *s)const; void loeschen(element *p); }; #include <string.h> // Schnittstellenfunktionen zur Liste liste::~liste() { element *z1 = zStart, *z2; while (z1) { z2 = z1; z1 = z1->nachf; delete[] z2->name; delete z2; } } void liste::entferneKnoten(const char *s) { element *z1 = zStart, *z2; if (z1 == NULL) return; if (strcmp(z1->name, s) == 0) { zStart = z1->nachf; delete[] z1->name; delete z1; return; } for (;;) { z2 = z1->nachf; if (z2 == NULL) break; if (strcmp(z2->name, s) == 0) { z2->nachf = z2->nachf; delete[] z2->name; delete z2; return; } z1 = z2; } } void liste::einfuegenLiKnoten(const char *s, unsigned nummer) { element *z = new element; int len = strlen(s); z->name = new char[len + 1]; strcpy(z->name, s); z->nummer = nummer; z->nachf = zStart; zStart = z; } element *liste::suchePosition(const char *s)const { element *z = zStart; while (z && strcmp(z->name, s) != 0) z = z->nachf; return z; } 217 Die Programmiersprache C++ // Beschreibung der Hash-Tabelle fuer´das seperate chaining class hashTabelle { private: unsigned N; liste *a; public: // Konstruktor, Destruktor hashTabelle(unsigned laenge=1021): N(laenge){a = new liste[laenge];} ~hashTabelle(){delete[] a;} // Methoden zur Bearbeitung der Hash-Tabelle void einfuegen(const char *s, unsigned nummer) { a[hash(s)].einfuegenLiKnoten(s, nummer); } element *suche(const char *s)const { return a[hash(s)].suchePosition(s); } void loeschen(const char *s) { a[hash(s)].entferneKnoten(s); } // Hash-Funktion unsigned hash(const char *s)const; }; unsigned hashTabelle::hash(const char *s)const { unsigned sum = 0; for (int i=0; s[i]; i++) sum += (i + 1) * s[i]; return sum % N; } int main() { ifstream telefonDatei("telefon.txt", ios::in); if (!telefonDatei) { cout << "Kann die Datei telefon.txt nicht oeffnen\n"; exit(1); } char NamenPuffer[100], antwort; unsigned nummer, N; cout << "Tabellenlaenge N: "; cin >> N; element *z = 0; hashTabelle telefonBuch(N); cout << "Daten aus der Datei telefon.txt:\n"; while (telefonDatei >> setw(100) >> NamenPuffer >> nummer) { cout << setw(30) << setiosflags(ios::left) << NamenPuffer << nummer << endl; telefonBuch.einfuegen(NamenPuffer, nummer); } cout << endl; for (;;) { cout << "Gib einen Namen ein oder ! fuer das Ende "; cin >> NamenPuffer; if (*NamenPuffer == '!') break; z = telefonBuch.suche(NamenPuffer); if (z) { cout << "Nummer: " << z->nummer << endl; cout << "Soll dieser Eintrag geloescht werden? (J/N): " << flush; cin >> antwort; if (antwort == 'J' || antwort == 'j') telefonBuch.loeschen(NamenPuffer); } else cout << "Nichts gefunden" << endl; } } 218 Die Programmiersprache C++ 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) 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“146: // // // Hashing mit open adressing #include <fstream.h> #include <iomanip.h> #include <stdlib.h> #include <iostream.h> struct element { char name[30]; unsigned nummer; }; class hashTabelle { private: unsigned hash(const char *s); // Hash-Funktion unsigned HashIncr()const{return 1 + summe % (N - 2);} int h2(const char *t, unsigned &i)const; unsigned N, summe; element *a; // Hash-Tabelle 146 PR44315.CPP 219 Die Programmiersprache C++ public: hashTabelle(unsigned laenge=1021); // Konstruktor ~hashTabelle(){delete[] a;} // Destruktor void einfuegen(const char *s, unsigned nummer); element *suchen(const char *s); }; #include <string.h> #include <stdlib.h> // Konstruktor hashTabelle::hashTabelle(unsigned laenge) { N = (laenge > 3 ? laenge : 3); // N >= 3 a = new element[N]; // Hash-Tabelle for (unsigned i=0; i<N; i++) a[i].name[0] = '\0'; } // Hash-Funktion fuer das schrittweise Bestimmen eines freien // Platzes im Kollisionsfall int hashTabelle :: h2(const char *t, unsigned &i) const { unsigned zaehler = 0, incr; if (strcmp(a[i].name, t)) { incr = HashIncr(); do { if (++zaehler == N) return 0; // Fehlanzeige i = (i + incr) % N; } while (strcmp(a[i].name, t)); } return 1; // Erfolg } void hashTabelle :: einfuegen(const char *s, unsigned nummer) { unsigned i = hash(s); if (!h2("", i)){cout << "HashTabelle ist voll\n"; exit(1);} strcpy(a[i].name, s); a[i].nummer = nummer; } element *hashTabelle::suchen(const char *s) { unsigned i = hash(s); return h2(s, i) ? a + i : NULL; } unsigned hashTabelle::hash(const char *s) { summe = 0; for (unsigned i=0; s[i]; i++) summe += (i + 1) * s[i]; return summe % N; } void main() { ifstream telefonDatei("telefon.txt", ios::in); if (!telefonDatei) { cout << "Kann die Datei telefon.txt nicht oeffnen.\n"; exit(1); } char NamenPuffer[30]; unsigned nummer, N; cout << "Tabellenlaenge N (vorzugsweise eine Primzahl): "; cin >> N; element *z = NULL; hashTabelle telefonBuch(N); cout << "Daten aus der Datei phone.txt:\n"; while (telefonDatei >> setw(30) >> NamenPuffer >> nummer) { cout << setw(30) << setiosflags(ios::left) << NamenPuffer << nummer << endl; telefonBuch.einfuegen(NamenPuffer, nummer); 220 Die Programmiersprache C++ } cout << endl; for (;;) { cout << "Gib einen Namen ein, oder ! fuer das Ende: "; cin >> NamenPuffer; if (*NamenPuffer == '!') break; z = telefonBuch.suchen(NamenPuffer); if (z) cout << "Nummer: " << z->nummer << endl; else cout << "Nicht gefunden." << endl; } } 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. 221 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). 222 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) {} 223 Die Programmiersprache C++ // Die Methode holeLinks ermoeglicht den Zugriff auf den linken // 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. 224 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; } 225 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 + 226 Die Programmiersprache C++ (hoeheLinks > hoeheRechts ? hoeheLinks : hoeheRechts); } 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; } 227 Speicherplatz kann über folgende Die Programmiersprache C++ g) Löschen des Baums // Loeschen des Baums template <class T> void loescheBaum(baumKnoten<T>* b) { if (b != NULL) { 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; 228 Die Programmiersprache C++ // 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 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; 229 Die Programmiersprache C++ // baumspezifische Methoden void aktualisieren(const T& merkmal); baumKnoten<T> *holeWurzel(void) const; }; 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 Definition147: // 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++; } 147 vgl. bsbaum.h 230 Die Programmiersprache C++ 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.: 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. 231 Die Programmiersprache C++ Aufgaben 1) Gegeben ist ein binärer Baum folgender Gestalt: 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 232 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.: 233 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 linkten 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 234 Die Programmiersprache C++ // linken Nachfolgers zu "Eltern" geloescht EvonErsKnoZgr->rechts = ErsKnoZgr->links; // 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 12 LINKS RECHTS Ergebnis: Der Wurzelknoten wird gelöscht. 2) Vorgegeben ist Schlüssel 12 LINKS RECHTS 7 5 8 Der Wurzelknoten wird gelöscht. Ergebnis: 235 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: 236 Die Programmiersprache C++ Schlüssel 13 LINKS 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 237 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); } } 238 Die Programmiersprache C++ 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)". 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. 239 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 240 Die Programmiersprache C++ 5. Die C++-Standardbibliothek 5.1 Die C++-Standardbibliothek und die STL Die STL (Standard Template Library148) 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> 148 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. 241 Die Programmiersprache C++ <cstring> <ctime> <cwchar> <cwctype> Abb.: Aufbau der Standardbibliothek 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> Der Header enthält die in den folgenden Tabellen aufgeführten Funktionen zum Klassifizieren und Umwandeln von Zeichen: Schnittstelle tolower(z) toupper(z) Bedeutung Gibt z als Kleinbuchstaben zurück Gibt z als Großbuchstaben zurueck Abb.: Umwandlungsfunktionen aus <cctype> Schnittstelle isalnum(z) isalpha(z) iscntrl(z) isdigit(z) isgraph(z) islower(z) isprint(z) ispunct(z) Wahr, wenn z == Buchstabe oder Ziffer Buchstabe 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 Bereich A..Z, a..z, 0..9 A..Z, a..z 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. 242 Die Programmiersprache C++ Schnittstelle F149 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. 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>. 149 Die Abkürzung F bedeutet: Einer der Typen float, double oder long double 243 Die Programmiersprache C++ <cstdlib> Die folgenden mathematischen Funktionen gehören zum Header <cstdlib> Schnittstelle int abs(int x) long abs(long x) long labs(long x) div_t150 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> Schnittstelle void abort() void atexit(void (*f)()) Bedeutung 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. 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*)) Abb.: Ausgewählte Funktionen aus <cstdlib> <cstring> 150 Div_t ist eine vordefinierte Struktur, die das Divisionsergebnis und den Rest enthält: struct div_t { int quot; // Qotient int rem; // Rest } 244 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 CPUZeit 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 tm* gmtime(const time_t* z) Beide Funktionen wandeln die in *z vorliegende tm* localtime(const 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 gm_time() die UTC (Universal Time Coordinated, entspricht GMT (Greenwich mean Time)) zurückgibt. time_t mktime(const tm*) Wandelt eine Zeit im tm-Format in das time_tFormat um time_t time(time_z* z) Gibt die momentane Kalenderzeit zurück bzw. –1 bei Fehler. Falls z ungleich NULL ist, wird der Rückgabewert an der Stelle z hinterlegt. 245 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 beteiligeten 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. 246 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; 247 // x && y // x || y // !x Die Programmiersprache C++ Funktionsobjekte zum Negieren logischer Prädikate „not1()“ und „not2()“ sind Funktionen, die ein Funktionsobjekt zurückgeben, dessen Aufruf ein Prädikat negiert. 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 („Funktionsadapter151 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 = finf_if(v.begin(), v.end(),bind2nd(less<int>(),5)); Bsp.: Anwendung eines selbstdefinierten Funktionsobjekts Funktionsadaptern bind1st() und bind2nd(). hoch mit den #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(), // Quellbereich // Zielbereich // Operation // Quellbereich 151 Funktions-Adapter ermöglichen die vordefinierten Funtionsobjekte zu kombinieren oder mit bestimmten Werten zu versehen. Auch sie werden in der Header-Datei functional definiert 248 Die Programmiersprache C++ ostream_iterator<int>(cout," "), bind1st(hoch<int>(),3)); cout << endl; // Alle Elemente hoch 3 ausgeben transform(v.begin(), v.end(), ostream_iterator<int>(cout," "), bind2nd(hoch<int>(),3)); cout << endl; } // Zielbereich // Operation // Quellbereich // Zielbereich // 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.: 249 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. 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 Iteratyp 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 Vorhanden bei: Vektoren, Deques, Listen, sets, Multisets, Maps, Multimaps 250 Die Programmiersprache C++ container::reserve_iterator container:: const_reserve_iterator container::key_type container::key_ompare container::value_compare 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 Elementebei 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 251 Die Programmiersprache C++ Nicht-Modifizierender Zugriff Funktionen zur Größe 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) Bedeutung Weist dem Container die Elemente des Containers c zu Vorhanden bei: Vektoren, Deques, Listen, Sets, Multisets, Maps, Multimaps, Strings 252 Die Programmiersprache C++ Weist dem Container anz Elemente zu, die mit deren Default-Konstruktor erzeugt werden Vorhanden bei: Vektoren, Deques, Listen void container::assign(Size anz, Weist dem Container anz Kopien von wert zu const T& wert) Vorhanden bei: Vektoren, Deques, Listen, Strings void container::assign(InputIterator Weist dem Container alle Elemente im Bereich anf, InputIterator end) [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 void container::assign(Size anz) 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 253 Die Programmiersprache C++ reverse_iterator container::rbegin() const_reverse_iterator container:: rbegin() const reverse_iterator container::rend() const_reverse_iterator container::rend() const Liefert einen Reverse-Iterator für den Anfang eines umgekehrten Durchlaufs durch den Container Vorhanden bei: Vektoren, Deques, Listen, Sets, 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 wert) Elemente 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 254 Die Programmiersprache C++ 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 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 DefaultKonstruktor 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; 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 255 Die Programmiersprache C++ Die folgende Abbildung zeigt die Methoden einer Deque: Methode mit Rückgabetyp deque<T>() deque<T>(const deque<T>&) ~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>&) 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 <= 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 256 Die Programmiersprache C++ 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 liste& c) die 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 wird, vor das Element mit der Position pos. Falls c ein anderer Container ist, wird c dadurch ein Element kleiner. void liste::splice(iterator pos, Verschiebt alle Elemente des Bereichs liste& c, iterator cAnf, iterator [cAnf,cEnd) aus c in die Liste für die die cEnd) 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. void liste:.sort() Sortiert alle Elemente anhand < bzw. op. Op void liste::sort(CompFunc 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(). void liste::merge(liste& c) Verschiebt alle Elemente aus der vorsortierten void liste::merge(liste& c, CompFunc Liste c in den vorsortierten Container, für den die op) 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. void liste::reverse() 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 257 Die Programmiersprache C++ Bsp152.: 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; 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, 152 // Schluessel PR52301 258 Die Programmiersprache C++ class T, class Compare = less<Key> // 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 key_compare value_compare 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 Compare Klasse für Funktionsobjekte, vgl value_comp() bzw. Methoden153: Methode mit Rückgabetyp size_type container::count(const T& wert) const 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 iterator container::find(const T& Liefert die Position vom ersten Element, das wert) einen Wert gleich wert besitzt. Kann kein const_iterator container::find(const passendes Element gefunden werden, wird T& wert) const 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 iterator container::lower_bound(const Liefert die erste Position, an der wert eingefügt T& wert) werden kann, ohne die Sortierung zu zerstören. const_iterator Dies ist gleichbedeutend mit dem ersten Element, container::lower_bound(const T& wert) dessen Wert >= wert ist. Die Elementfunktion ist const 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 153 Elementfunktionen, die spezielle Implementierungen von Algorithmen für assoziative Container sind. 259 Die Programmiersprache C++ 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() key_compare container::key_comp() 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 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 Textdatei154 #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.,:;\"{}-+&^%$#@!~'\\<|()[]<>*="; 154 vgl. PR52410.CPP 260 Die Programmiersprache C++ 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; } 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 Implementierung155. 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 155 Falls nichts anderes angegeben wird, wird eine deque verwendet. 261 Die Programmiersprache C++ 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 = Compare(), const Container& = Container()) bool empty() const size_type size() const const value_type top()const void push(const value_type& x) void pop() Bedeutung Konstruktor. Eine Priority-Queue kann mit einem 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. 262 Die Programmiersprache C++ 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äume 156 implementiert. Ein „set“ besitzt zusätzlich folgende Datentypen Datentyp pointer const_pointer reverse_iterator const_reverse_iterator key_type value-type key_compare value_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 Compare bzw. Methoden: Methode mit Rückgabetyp set(const Compare& cmp = Compare()) pair<iterator, bool> insert(x) 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 156 Der Standard legt nicht direkt fest, wie assoziative Container implementiert sind. Vielfach werden diese Bäume als sog. „Red-Black-Trees“ implementiert. 263 Die Programmiersprache C++ size_type erase(k) void erase(p, q) void clear() value_compare value_comp() const 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 Textdatei157 #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")); } 157 PR52611.CPP 264 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); 265 Die Programmiersprache C++ Bsp.: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“158 #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 vektor-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: 158 PR52702.CPP 266 Die Programmiersprache C++ Datentyp pointer const_pointer reverse_iterator const reverse_iterator Bedeutung Zeiger auf Vektor-Element Zeiger auf konstantes Vektor-Element 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 267 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.: 268 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 Programm159 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 Beispiel160 zeigt, wie das Sortieren mit Random-Access-Iteratoren erledigt werden kann. #include #include #include #include #include 159 160 <string.h> <algorithm> <vector> <stdlib.h> <iostream.h> PR53010.CPP PR53011.CPP 269 Die Programmiersprache C++ int main() { vector<int> v; int ein; while (cin >> ein) 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 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 270 Die Programmiersprache C++ ++iter iter++ TYP() TYP(iter) iter1=iter2 Weitersetzen (liefert neue Position) Weitersetzen (liefert alte Position) Erzeugen (Default-Konstruktor) Kopieren (Copy-Konstruktor) 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) 271 Die Programmiersprache C++ // 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); 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 272 Die Programmiersprache C++ 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) Erzeugung back_inserter(container) front_inserter(container) inserter(container,pos) Abb. Arten von Insert-Iteratoren 1. front_insert_iterator 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. 273 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 274 Die Programmiersprache C++ Bsp.161: „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. 161 PR53510.CPP 275 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 276 Die Programmiersprache C++ adjacent_find Zwei gleiche direkt benachbarte Elemente werden mit der Funktion adjacent_find gefunden. template <class ForwardIterator> ForwardIterator adjacent_find(ForwardIterator first, ForwardIterator last); 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); 277 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); 278 Die Programmiersprache C++ template <class BidirectionalIterator1, class BidirectionalIterator2> BidirectionalIterator2 copy_backward(BidirectionalIterator1 first, BidirectionalIterator1 last, BidirectionalIterator2 result); 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 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); 279 Die Programmiersprache C++ replace ersetzt in einer Sequenz jeden vorkommenden wert old_value durch new_value. template <class ForwardIterator, class T> void replace(ForwardIterator first, 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 template <class ForwardIterator, class T> ForwardIterator remove(ForwardIterator first, ForwardIterator last, const T& value); 280 Die Programmiersprache C++ unique unique() löscht gleiche aufeinanderfolgende Elemente bis auf eins. 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 281 Die Programmiersprache C++ template <class RandomAccessIterator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last); template <class RandomAccessIterator, class RandomNumberGenerator> void random_shuffle(RandomAccessIterator first, RandomAccessIterator last RandomNumberGenerator& rand); 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 Zahlen162 #include <algorithm> #include <iostream> #include <vector> using namespace std; 162 vgl. PR52801.CPP 282 Die Programmiersprache C++ 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; 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; } } 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 template <class ForwardIterator, class T> ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& wert); 283 Die Programmiersprache C++ 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. 284 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 4 Heap-Algorithmen Sie sind auf alle Container, auf die mit Random-Access-Iteratoren zugegriffen werden kann, anwendbar. pop_heap() push_heap() make_heap() 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() template <class RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomaccessIterator last); template <class RandomAccessIterator> void sort_heap(RandomAccessIterator first, RandomaccessIterator last Compare comp); Die Sequenz ist aufsteigend sortiert. 285 Die Programmiersprache C++ 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); 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. 286 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 ⋅ v i i template <class InputIterator, class InputIterator , class T> T inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, T init); 287 Die Programmiersprache C++ template <class InputIterator, class InputIterator , class T, 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); 288 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.2.5 slice_array 5.6.2.6 gslice 5.6.2.7 gslice_array 5.6.2.8 mask_array 5.6.2.9 indirect_array 289 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> 290 Die Programmiersprache C++ 6. Windows-Programmierung unter Visual C++ 6.1 Merkmale von Visual C++ 6.1.1 Visual C++ -Features Compiler Debugger Ressorcen-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++ enthält eine integrierte Entwicklungsumgebung (MSDEV.EXE), 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++. 291 Die Programmiersprache C++ 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. Windows- Utilities Allgemeine Utilities 6.1.2 Werkzeuge für den Umgang mit Visual C++ -Strukturen Abb.: Das Fenster nach dem Auruf von Visual-C++ Symbolleisten Sie enthalten Schaltflächen, die angeglickt, verschiedene Aktionen ausführen. Die verschiedenen Kategorien für Symbolleisten werden nach Funktionen zusammengefaßt, z.B. Symbolleisten für Dateien, Ressourcen, Fenster. Mit einem Klick auf diese Symbolleisten erhält man Zugriff auf häufig gebrauchte Kommandos. 292 Die Programmiersprache C++ Hinweise Sie erscheinen in der Statuszeile. Sie liefern eine kurze Information darüber, was gerade getan wird. Wird bspw. die Maus über Menü-Kommandos bewegt, dann erkären die Hinweise, was die Menü-Einträge bewirken. Quickinfos oder Infofelder Sie tauchen in der Form von kleinen gelben Kästchen auf, falls der Mauszeiger für einige Sekunden auf der Schaltfäche einer Symbolleiste ruht. Hilfesystem (?) 6.1.3 Erstellen einer Windows-Anwendung Aufgabenstellung Erstelle eine Anwendung mit zwei Schaltflächen , wie es die folgende Abbildung zeigt: Die erste Schaltfläche („Hallo“) präsentiert dem Benutzer ein Begrüßungsmeldung, die zweite Schaltfläche schließt die Anwendung. Beim Klicken auf die Schaltfläche „Hallo“ erscheint eine einfache Begrüßungsformel: 293 Die Programmiersprache C++ Lösungsschritte163 1. Aufruf von Visual C++ 2. Anlegen eines Arbeitsbereichs und eines Projekts Rufe den Befehl DATEI/NEU auf; Im Dialogfeld NEU Anzeigen der Seite PROJEKTE. Eingabe eines Namens im Feld PROJETNAME bzw. eines Verzeichnisses im Feld PFAD. In dieses Verzeichnis werden die Dateien des Projekts abgespeichert. Standardmäßig wählt Visual C++ den Namen des Projekts als Name für das Projektverzeichnis. 3. Wahl eines Arbeitsbereichs für das Projekt Voreingestellt ist die Option NEUEN ARBEITSBEREICH erstellen. Die Option HINZUFÜGEN ZU AKT. ARBEITSBEREICH ist nur verfügbar, falls beim Aufruf des Befehls DATEI/NEU bereits ein Arbeitsbereich geöffnet war. 163 vgl. Pr61310 294 Die Programmiersprache C++ 4. Wähle im linken Teil des Dialogfelds einen Projekttyp aus der Liste aus. Hier soll der MFC-ANWENDUNGSASSISTENT (EXE) ausgewählt werden. (erstellen des Anwendungsrahmens mit dem Anwendungsassistenten) 5. Bestätigen mit OK Es erscheint das erste Dialogfeld des Anwendungsassistenten. Der Anwendungs-Assistent stellt eine Reihe von Fragen über den Typ der Anwendung, Merkmale und Funktionalität. Anhand dieser angaben erzeugt der Assistent ein Gerüst, das kompiliert und ausgeführt werden kann. 6. Bearbeiten des ersten Dialogfelds des Anwendungsassistenten Wähle die Option DIALOGFELDBASIEREND,d.h.: Das Hauptfenster der Anwendung wird ein Dialogfeld sein. Drücke den Schalter WEITER164. 7. Optionales Bearbeiten des zweiten Dialogfelds des Anwendungsassistenten Löschen der Option ACTIVEX-STEUERELEMTE Falls gewünscht, kann im letzten Eingabefeld des Dialogs ein Titel für die Anwendung angegeben werden. 164 Man könnte hier bereits auf FERTIGSTELLEN drücken. Es sollen aber noch einige untergeordnete Änderungen angebracht werden. 295 Die Programmiersprache C++ 8. Im 3. Schritt wird die Vorgabe zum Erzeugen von Kommentaren für die Quelldatei und die Verwendung der MFC-Bibliothek als gemeinsam genutzte DLL übernommen. 296 Die Programmiersprache C++ 9. Abschließendes Kontrollfenster. Das letzte Fenster des Anwendungs-Assistenten zeigt die C++-Klassen an, die der Anwendungs-Assistent für die Anwendung erstellt. Klick auf Fertigstellen bewirkt das Erstellen des Anwendungsgerüsts. 297 Die Programmiersprache C++ Bevor der Anwendungs-Assistent das Projektgerüst erstellt, zeigt er anhand der Angaben, die in den einzelnen Schritten des Assistenten festgelegt wurden, alle Komponenten an, die das Projektgerüst enthält. Bestätigen mit OK. 298 Die Programmiersprache C++ 10. Nachdem der Anwendungs-Assistent das Projektgerüst erstellt hat, kommt man in die Umgebung des Visual Studio zurück. Der Arbeitsbereich zeigt nun eine Baumansicht der Klassen im Projektgerüst: Es erscheint auch das Hauptfenster (ein Dialogfeld) im Editorbereich des Visual Studios. Abb.: Der Arbeitsbereich mit einer Baumansicht der Projektklassen 11. Kompilieren über den Befehl ERSTELLEN / PR61310.EXE ERSTELLEN. Beim Erstellen erscheinen Fortschrittsanzeigen und andere Compiler-Meldungen im Ausgabebereich. 12. Start der Anwendung mit dem Befehl ERSTELLEN / AUSFÜHREN VON PR61310.EXE Die Anwendung präsentiert ein Dialogfeld mit einer ZU ERLEDIGEN –Meldung und den beiden Schaltflächen. 299 Die Programmiersprache C++ 13. Gestaltung des Anwendungsfensters (Layout für das Anwendungsdialogfeld) 1. Aktivieren im Arbeitsbereich die Registerkarte Ressourcen 300 Die Programmiersprache C++ 2. Erweitern der Baumansicht der Ressourcen zur Anzeige der verfügbaren Dialogfelder. Jetzt kann ein Doppelklick auf das Dialogfeld IDD_PR61310_DIALOG erfolgen (zum Öffnen des Fensters im Editorbereich von Visual Studio). 3. Markieren des im Dialogfeld angezeigten Texts, Löschen des Texts durch Drücken der (Entf-) Taste. 4. Markieren der Schaltfläche Abbrechen, Ziehen an den unteren Rand des Dialogfelds, Verändern der Größe der Schaltfläche, so dass sie die gesamte untere Breite des Layoutbereichs des Fensters einnimmt. 5. Klick mit der rechten Maustaste über der Schaltfläche Abbrechen. Es erscheint (das in der folgenden Abbildung wiedergegebene) ein Kontextmenü, aus dem der Befehl EIGENSCHAFTEN ausgewählt wird. Daraufhin wird das Eigenschaftsdialogfeld geöffnet. 6. Ändere den Wert im Feld Titel in &Schießen. Schließe das Eigenschaftsdialogfeld. 7. Verschiebe die Schaltfläche OK in die Mitte des Fensters, ändere die Größe der Schaltfläche entsprechend. 8. Im Eigenschaftsdialogfeld der Schaltfläche OK ändere die ID in IDHallo und den Titel in &Hallo. 14. Code in die Anwendung aufnehmen Über den Klassen-Assistenten von Visual C++ kann das Dialogfeld mit Code verknüpft werden. Mit dem Klassen-Assistenten erstellt man die Tabelle der Nachrichten, die die Anwendung empfangen kann, einschl. der zu verarbeitenden Funktionen. Auf diese Angaben greifen die MFC-Makros zurück, um die Funktionalität mit Windows-Steuerelementen zu verbinden. Die Funktionalität für die erste Beispielanwendung wird über folgende Schritte zugewiesen: 1. Ordne der Schaltfläche Hallo eine Funktion zu, klicke über die rechte Maustaste über der Schaltfläche und wähle Klassen-Assistent aus dem Kontextmenü. 2. War die Schaltfläche Hallo bereits markiert, dann ist sie bereits in der Liste der verfügbaren Objekt-IDs ausgewählt, wie die folg. Abb. zeigt: 301 Die Programmiersprache C++ 3. Bei markiertem Eintrag IDHALLO in der Liste der Objekt-Ids wird BN_CLICKED in der Liste der Nachrichten ausgewählt, anschließend auf die Schaltfläche Funktion hinzufügen geklickt. Daraufhin öffnet sich das Dialogfeld Member-Funktion hinzufügen. Dieses Dialogfeld enthält einen Vorschlag für den Funktionsnamen. Klick auf OK, um die Funktion zu erzeugen und sie in die Nachrichtenzuordnungstabelle aufzunehmen. 302 Die Programmiersprache C++ 4. Markiere die Funktion OnHallo in der Liste der verfügbaren Funktionen Klick auf Code bearbeiten. Der Cursor steht dann im Quellcode der Funktion, und zwar genau an dem Punkt, an dem der Code für die gewünschte Funktion eingetragen werden soll. 303 Die Programmiersprache C++ Trage den Code aus folgendem Quellcode-Listing unmittelbar unter der TODO-Kommentarzeile ein: void CPr61310Dlg::OnHallo() { // TODO: Code für die Behandlungsroutine der Steuerelement// Benachrichtigung hier einfügen // Benutzer begruessen MessageBox("Hallo. Herzlich willkommen zum Programmieren in Visual C++!"); } Die Funktionalität der Anwendung ist damit vollständig realisiert. 15. Das Symbol des Dialogfelds erstellen Das Symol in der oberen linken Ecke des Anwendungsfensters umfasst drei Kästchen mit den Buchstaben MFC. Ein eigenes Anwendungssymbol, das die vorliegende Anwendung mit einem Bild repräsentiert, soll bereit gestellt werden: 1. In der Baumansicht der Ressourcen Erweitern des Zweigs Icon. Markieren des Symbols IDR_MAINFRAME. Daraufhin wird das Anwendungssymbol in den Editorbereich von Visual Studio gebracht. Mit den zur Verfügung stehenden Zeichenwerkzeugen ist das Symbol so umzugestalten, dass ein Bild entsteht, mit dem die vorliegenden Anwendung präsentiert werden kann. 304 Die Programmiersprache C++ 3. Wenn die Anwendung kompiliert und ausgeführt wird, steht das Symbol in der oberen linken Ecke des Anwendungsfensters. Klick auf das Symbol, Wahl des Befehls Infoüber Hallo aus dem Dropdown-Menü. 4. Im Info-Dialogfeld, das Visual C++ erstellt hat, ist eine große Version des Symbols zu sehen. Beim Öffnen eines Anwendungssysmbols im Symbol-Editor, wird die Größe des Symbols per Vorgabe auf 32 mal 32 Pixel eingestellt. 16. Hinzufügen der Schaltflächen Minimieren bzw. Maximieren Im Dialog-Editor können die Schaltflächen Minimieren und Maximieren in die Titelseite des Anwendungsfensters mit folgenden Schritten hinzugefügt werden: 1. Markiere das Dialogfenster selbst, als ob die Fenstergröße verändert werden soll. 2. Klick mit der rechten Maustaste, Wählen aus dem Kontextmenü den Befehl Eigenschaften. 3. Gehe auf die Registerkarte Formate, wie es die folgende Abb. zeigt. 305 Die Programmiersprache C++ 4. Schalte die Kontrollkästchen Minimieren-Schaltfläche und Maximieren-Schaltfläche ein. 17. Compiliere und Starte die Anwendung. Die Schaltfläche Minimieren und Maximieren erscheinen jetzt in der Titelleiste. 306 Die Programmiersprache C++ 6.2 Die integrierte Entwicklungsumgebung 6.2.1 Projekte und Arbeitsbereiche Projekt Ein Projekt besteht aus - den Quelldateien des Programms der Information, wie diese Quelldatei übersetzt und zu einer ausführbaren Datei (dem Programm) gelinkt werden können einem Verzeichnis auf der Festplatte, in dem die Dateien des Projekts abgelegt sind. Die IDE stellt eine Reihe von Menübefehlen und Dialogfeldern zur Verfügung, mit denen Projekte erzeugt, erweitert und überwacht werden können. Arbeitsbereich In Visual C++ werden Projekte immer in Arbeitsbereichen verwaltet. Ein Arbeitsbereich ist nichts Anderes als eine höhere Organisationsebene, die es erlaubt, mehrere Objekte gemeinsam zu verwalten. Der (Projekt-) Arbeitsbereich bildet den Ausgangspunkt für die Navigation zu den verschiedenen Teilen der Entwicklungsprojekte. Der Arbeitsbereich gestattet es, die die Anwendung in drei verschiedenen Modi zu betrachten: - In der Klassenansicht kann auf C++-Klassenebenen durch den Quellcode navigiert und Quellcode bearbeitet werden. Die Ressourcenansicht erlaubt das Aufsuchen der verschiedenen Ressourcen in der Anwendung und deren Bearbeitung. Dazu gehören die Entwürfe von Dialogfenstern, Symbolen und Menüs. Die Dateiansicht bietet eine Übersicht über die Dateien, aus denen eine Anwendung besteht. Man kann die Dateien anzeigen lassen und in ihnen navigieren. 6.2.2 Der Editor Den Editor gibt es in Visual C++ nicht. Es gibt verschiedene Editoren, mit denen die verschiedenen Arten von Quelldateien und Programmelementen bearbeitet werden können. Am wichtigsten sind: der Quelltexteditor und die Ressourceneditoren, 307 Die Programmiersprache C++ 6.2.3 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-Compilers165. 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-Datei166, denn diese Header-Datei muß in die Ressourcenskriptdatei und in die Quelldateien, die Ressourcen verwenden, über die #include-Direktive aufgenommen werden. 165 166 rc.exe Der MFC-Assiistent nennt diese Header-Datei standardmäßig resource.h 308 Die Programmiersprache C++ Die Ressourcenmethoden 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 Diaalogseite 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 309 Die Programmiersprache C++ 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 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 Klassenassistent167 ist das Werkzeug das den mit Hilfe des Ressouce Editor erstellten Steuerelementen die gewünschte Funktionalität verleiht. Der Klassenassistent kann vom Menü Ansicht (View) gestartet werden. 6.2.4 Dialogfelder Dialoge werden in der MFC durch Klassen repräsentiert, die sich von CDialog ableiten168. 6.2.5 Steuerelemente Steuerelemente sind die bereits aus den Dialogfeldern bekannten typischen Oberflächenelemente. Steuerelemente kann man aber nicht nur in Dialogfeldern verwenden, man kann sie auch in Rahmen- und Ansichtsfenster einbauen. Zur Aufnahme von Steuerelementen in Dialogfelder steht mit dem Dialog-Editor ein leistungsfähiges grafisches Design-Tool zur Verfügung. Zur Aufnahme von Steuerelementen 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 167 168 vgl. 6.4.1.2 vgl. 6.1.3 310 Die Programmiersprache C++ 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. 6.2.6 Mausereignisse Nachricht WM_LBUTTONDBLCLK WM_LBUTTONDOWN WM_LBUTTONUP WM_MBUTTONDBLCLK WM_MBUTTONDOWN WM_MBUTTONUP WM_MOUSEMOVE WM_MOUSEWHEEL WM_RBUTTONDBLCLK WM_RBUTTONDOWN WM_RBUTTONUP Beschreibung Linke Maustaste wurde doppelt betätigt, Fenster hat Stilattribut CS_DBLCLKS Linke Maustaste wurde gedrückt Linke Maustaste wurde losgelassen Mittlere Maustaste wurde doppelt betätigt, Fenster hat Stilattribut CS_DBLCLKS Mittlere Maustaste wurde gedrückt Mittlere Maustaste wurde losgelassen Maus wurde bewegt Wird an das aktive Fenster gesendet, wenn das Mausrad rotiert Rechte Maustaste wurde doppelt betätigt, Fenster hat Stilattribut CS_DBLCLKS Rechte Maustaste wurde gedrückt Rechte Maustaste wurde losgelassen Abb.: Wichtige Windows-Nachrichten für die Maus Bsp.: Behandlung von Mausklicks (im Client-Bereich des Hauptfensters) durch Anzeige der Mauskoordinaten169 1. 2. 3. Anlegen eines neuen Projekts mit dem Anwendungsassistenten (SDI-Anwendung mit Doc/View-Unterstützung). Aufruf des Klassenassistenten (Befehl: ANSICHT/KLASSENANSICHT), Anzeigen der Seite NACHRICHTENZUORDNUNGSTABELLEN Erweitern der Ansichtsklasse (Pr62410View) mit Hilfe des Klassenassistenten um eine Methode zur Bearbeitung des Ereignisses WM_LBUTTONDOWN. Wahl der Ansichtsklasse (Pr62410View) in den Feldern KLASSENNAME und OBJECT-IDs. Im Feld NACHRICHTEN scrollen bis zum Eintrag WM_LBUTTONDOWN; markieren dieses Eintrags. Drücken des Schalters FUNKTION HINZUFÜGEN. Die Methode wird im Feld MEMBER-FUNKTIONEN hervorgehoben angezeigt. Schalter CODE BEARBEITEN aktivieren. Im Editor wird die folgende Definition der Behandlungsroutine angezeigt: /////////////////////////////////////////////////////////////////////////// // CPr62410View Nachrichten-Handler void CPr62410View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Code für die Behandlungsroutine für Nachrichten hier //einfügen und/oder Standard aufrufen CView::OnLButtonDown(nFlags, point); } Nachrichten für Mausereignisse enthalten auch die Koordinaten der Maus und diverse Flags. Diese zusätzlichen Informationen werden den Behandlungsmethoden zu den Nachrichten als Parameter 169 vgl. Pr62410 311 Die Programmiersprache C++ übergeben. Mauskoordinaten werden für WM_LBUTTONDOWN OnLButtonDown() als CPoint-Parameter übergeben. der Behandlungsroutine 4. Einsetzen von Code in die Behandlungsroutine. Der Anwender wird über das Ereignis „Drücken der linken Maustaste“ über ein Meldungsfenster170 informiert. /////////////////////////////////////////////////////////////////////////// // CPr62410View Nachrichten-Handler void CPr62410View::OnLButtonDown(UINT nFlags, CPoint point) { char sKoord[100]; // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen // und/oder Standard aufrufen sprintf(sKoord,"Klick an Position: %d, %d",point.x,point.y); MessageBox(sKoord,"Linke Maustaste gedrückt",MB_OK | MB_ICONINFORMATION); CView::OnLButtonDown(nFlags, point); } 5. Ausführen des Programms Abb.: Mausklick und Meldungsfenster 6.2.7 Menüs, Symbolleisten Für Projekte, die mit dem MFC-Anwendungsassistenten beginnen, legt dieser automatisch eine Standardmenüleiste und optional eine Symbolleiste und Statusleiste an. Bsp.: Anwendungsgerüst mit kompletter Menüunterstützung 1. Anlegen eines neuen Projekts (Pr62710) Schritt 1: SDI-Anwendung mit Doc/View-Unterstützung Schritt 4: Ausschalten der Optionen für nicht gewünschte Menübefehle ( Drucken, Dateiliste); Aktivieren der Optionen ANDOCKBARE SYMBOLLEISTE und STATUSLEISTE. 2. Ausführen des Programms 170 vgl. 6.2.8.1 312 Die Programmiersprache C++ Abb.: Vom Assistenten erstelltes Anwendungsgerüst Erstellt wurde - eine voll funktionsfähige Menüleiste mit mehreren Popup-Menüs und etlichen Menübefühlen in den Popup-Menüs eine Symbolleiste mit Schaltflächen für die wichtigsten Befehle und Quickinfos. Statusleiste am unteren Rand des Hauptfensters in der Hilfetexte zu den einzelnen Menübefehlen angezeigt werden. Bearbeiten der zugehörigen Ressourcen Anpassungen, z.B. Erweitern um weitere Menübefehle, Löschen von Menübefehlen, etc werden in den Ressourcen vorgenommen. Das geschieht in - der Menü-Ressource der Symbolleisten-Ressource der Stringtabelle Bsp.: Anpassen der Symbolleiste für das Dropdown-Menü „Student“ im folgenden Hauptfenster einer SDI-Anwendung Den Menübefehlen Eintragen, Loeschen, Erster, Naechster, Vorheriger, Letzter sollen die Schaltflächen der Symbolleiste, die zwischen der Schaltfläche „Speichern“ und „Info“ liegen, zugeordnet werden. 313 Die Programmiersprache C++ Die Bearbeitung erfolgt im Symbolleisten-Editor nach Öffnen der Ressourcen-Ansicht, Expansion des Ordners TOOLBAR und Doppelklick auf den Eintrag IDR_MAINFRAME. Durch einen Klick auf eine Schaltfläche im oberen Fenster, das nach dem Doppelklick auf IDR_MAINFRAME angezeigt wird, kann das Schaltflächensymbol bearbeitet werden: Nachdem alle Schaltflächen mit den geforderten Schaltflächensymbolen versehen sind, können bzw. müssen die Schaltflächen mit des IDs der zugehörigen Menübefehle verknüpft werden. Das geschieht über ein Doppelklick in der Sybolleistenansicht auf die jeweilige Schaltfläche. Daraufhin erscheint das folgende Dialogfeld: Hier kann die ID eingetragen werden. 314 Die Programmiersprache C++ Abb.: Der Symbolleisten-Editor Aktion Neue Schaltfläche anlegen Schaltflächen bearbeiten Schaltflächen umordnen Schaltflächen löschen Leerräume einfügen Ressourcen-ID definieren Hilfetexte definieren Ausführung Klick in die leere Schablone, die im oberen Teilfenster der Symbolleiste angezeigt wird. Sobald mit der Bearbeitung in einem der unteren Fenster begonnen wird, wird im oberen Fenster eine neue leere Schablone eingefügt. Klick im oberen Fenster auf die zu bearbeitende Schaltfläche Verschiebe die Schaltflächen in der Symbolleiste im oberen Fenster mit der Maus Nimm die Schaltfläche im oberen Fenster mit der Maus auf und ziehe die Schaltfläche aus der Symbolleiste heraus. Zum Einfügen eines Leerraums schiebe die Schaltfläche einfach um die halbe Breite über die nachfolgende Schaltfläche. Für Schaltflächen, die zu Menübefehlen korrespondieren, wähle die ID des Menübefehls aus der Liste aus. Für Schaltflächen, die Aktionen auslösen sollen, zu denen es keine Entsprechung gibt, gib eine neue ID an. Soll ein beschreibender Hilfetext in die Statuszeile eingeblendet werden, wenn die betreffende Schaltfläche ausgewählt wird, dann rufe das Dialogfeld EIGENSCHAFTEN auf (über den Befehl ANSICHT/EIGENSCHAFTEN) und gib den Text in das Feld STATUSZEILENTEXT ein. Zur Einrichtung eines Quickinfo hänge an den Statuszeilentext „\n“ an und den Quickinfo-Text an. Besitzt die Schaltfläche eine ID, zu der ein Hilfetext bereits erzeugt wurde, dann wird dieser Hilfetext angezeigt. Abb.: Bearbeitung der Symbolleiste 315 Die Programmiersprache C++ 6.2.8 Texte und Dateien 6.2.8.1 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 IDRETRY IDYES 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 Die Wiederholen-Schaltfläche wurde gedrückt Die Ja-Schaltfläche wurde gedrückt. Abb.: 316 Die Programmiersprache C++ 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. CEditView CRichEditView 5. Weitere nützliche Klassen CString (MFC-Stringklasse) erleichtert die Handhabung von Zeichenketten, insbesondere die damit verbundene dynamische Speicherverwaltung. Sammlungen (Container) von Strings können in den MFC-Klassen CStringArray oder CStringList verwaltet werden. CFile unterstützt nur binäre, ungepufferte Operationen171. Die abgeleitete Klasse CStudioFile ermöglicht gepuffertes Lesen und Schreiben von Binär- und Textdateien. Alterantiv können die ANSI-C++-Datenstreamklassen verwendet werden. 171 vgl. 6.2.8.2 317 Die Programmiersprache C++ 6.2.8.2 Dateien 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: 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. 318 Die Programmiersprache C++ 6.2.8.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. 319 Die Programmiersprache C++ 6.3 Application Programming Interface 6.3.1 Funktionsweise von Windows Programmen 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" 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. 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 soll172. Erzeugen des Hauptfensters HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, 172 // // // // // // // // // 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 vgl. CWnd::ShowWindow() in OnLine-Hilfe 320 Die Programmiersprache C++ HANDLE hInstance, LPVOID lpParam ); // 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; // Fensterfunktion Für diese Struktur muß eine Variable erzeugt werden. Das ist dann die Fensterklasse. 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); 321 Die Programmiersprache C++ 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); 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; } 2. Ausführen des Programms (Strg + F5) 322 Die Programmiersprache C++ 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.3.2 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, die Windows für jede laufende Anwendung einrichtet. 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. // MessageLoop while (GetMessage(&Message, NULL, 0, 0)) { TransLateMessage(&Message); DispatchMessage(&Message); } GetMessage() liest die Nachrichten aus der Message Queue (ihres Thread) aus. TranslateMessage() übersetzt die Nachricht in ein besser lesbares (und verständliches) Format. Das eigentliche Ziel der Nachricht ist das Fenster, an das die Nachricht gerichtet ist. Die Verteilung der Nachrichten an die verschiednen Fenster der Anwendung übernimmt die API-Funktion DispatchMessage(). 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 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. Letzendlich werden Nachrichten an Fenster geschickt. Daher definiert jedes Fenster eine eigene Fensterfunktion, die einkommende Botschaften empfängt und einer passenden Bearbeitung zuführt. 323 Die Programmiersprache C++ 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. Eigene Nachrichtenbehandlung: Drücken der linken Maustaste WM_LBUTTONDOWN. 324 Die Programmiersprache C++ // Fensterfunktion WinProc 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. } } 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. 325 Die Programmiersprache C++ 6.3.3 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. 326 Die Programmiersprache C++ Fensterfunktion und Antworttabellen Nachrichten werden in API-Programmen in Fensterfunktionen behandelt. Fensterfunktionen enthalten eine switch-Anweisung, in der für alle zu 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 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 Wizards 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 namens "Anwendung" zum Beispiel, die vom AppWizard erzeugt wurde, kann 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 caseBlöcke der Fensterfunktion, z.B.: case WM_LBUTTONDOWN: ON_WM_LBUTTONDOWN() 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 327 Die Programmiersprache C++ aus dem Makronamen, indem man sämtliche Unterstriche und das Präfix WM herausstreicht und nur noch die Anfangsbuchstaben der Silben groß schreibt (OnLButtonDown()). Falls zur Einrichtung von Behandlungsmethoden der Klassen-Assistent genutzt wird, braucht man sich um die Namensgebung nicht zu kümmern. Man ruft den Befehl ANSICHT/KLASSEN-ASSISTENT auf und läßt sich die Seite NACHRICHTENZUORDNUNGSTABELLE (Antworttabelle) anzeigen: Abb.: Nachrichenbearbeitung mit dem Klassen-Anwendungsassistenten 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. Gerätekontexte Anstatt der API-Funktion GetDC() wird stets eine der MFC-Gerätekontextklassen instantiiert. 328 Die Programmiersprache C++ 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. 329 Die Programmiersprache C++ 6.4 Windows-Programmierung mit der MFC 6.4.1 Die Assistenten 6.4.1.1 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 Seiten, die über die Schalter ZURÜCK und WEITER aufgerufen werden. Dialogseiten des MFC-Anwendungsassistenten 1. Seite Auf der ersten Seite 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 bringt173. 173 Insbesonere dann, wenn daten auf unterschiedliche Weise angezeigt werden sollen. 330 Die Programmiersprache C++ 2. Seite Auf der zweiten Seite kann die Anwendung mit einer Datenbank verbunden werden. Im oberen Teil erfolgt die Wahl der Art der Datenbankunterstützung und ob Befehle zum Laden und Speichern von Dokumentdateien in das Menü DATEI aufgenommen werden sollen. Falls die Entscheidung für eine Datenbankanbindung gefallen ist, 331 Die Programmiersprache C++ dann kann im unteren Teil des Dialogfelds über den Schalter Datenquelle eine Datenbank ausgewählt werden. 3. Seite Die dritte Seite führt die OLE-Möglichkeiten und ActiveX-Features auf Hier 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. 4. Seite Hier werden alle Einstellungen zum Erscheinungsbild vorgenommen. Auf dieser Seite kann entschieden werden, - der Anwendung 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 Über den Schalte WEITERE OPTIONEN kann die Titelleiste des Rahmenfensters weiter konfiguriert werden. 332 Die Programmiersprache C++ 5. Seite Auf der fünften Seite wird festgelegt, ob - ein normales oder ein Explorer ähnliches Fenster gewünscht wird. ausfühliche Kommentare angelegt werden sollen die MFC statisch oder als DLL eingebunden werden soll.. 333 Die Programmiersprache C++ Auf der letzten Seite werden die zu generierenden Klassen angezeigt: Bevor der Anwendungs-Assistent an die Arbeit geht, zeigt er noch ein Kontrollfenster mit einer kurzen Beschreibung des zu erstellenden Projekts an. Falls dieses Kontrollfenster (durch OK) bestätigt wird, beginnt der Assistent das Projekt einzurichten und ein lauffähiges Programmgerüst zu implementieren. 334 Die Programmiersprache C++ Eine mit Hilfe des AppWizzard erstellte SDI-Anwendung erzeugt automatisch u.a. zwei Dateien mit dem Zusatz Doc und View. Gelagert werden die Daten im Dokument und gezeigt werden sie in der Ansicht. 6.4.1.2 Der Klassen-Assistent Der Klassen-Assistent dient der halbautomatischen Bearbeitung von MFC-Projekten, die über eine CLW-Datei174 verfügen. Mit dem Klassen-Assistenten kann man - neue Klassen erstellen virtuelle Methoden überschreiben Antwortmethoden zur Nachrichtenverarbeitung einrichten Dialogklassen erstellen Elementarvariable für Steuerelemente aus Dialogen anlegen Ereignisse für ActiveX-Steuerelemente definieren Klassen automatisieren Klassen aus Typbibliotheken erstellen Beim Aufruf des Klassen-Assistenten über den Befehl ANSICHT/KASSENASSISTENT erscheint ein fünfsetiges Dialogfeld. Abb.: Das Dialogfeld des Klassen-Assistenten 174 bspw. die vom MFC-Anwendungsassistenten generierten Projekte 335 Die Programmiersprache C++ Der ClassWizzard ist das wichtigste Werkzeug zum Umgang mit Nachrichten und Nachrichten-Funktionen. Eine über den Klassenassistent erzeugte Nachrichtenfunktion wird in einer Nachrichtentabelle (Message-Map) verwaltet. Nachrichtentabellen bestehen aus zwei Teilen: Der 1. Teil befindet sich in der Header-Datei, der zweite in der entsprechenden .cpp-Datei. Nachrichtentabellen sind in der Form von Makros realisiert. Erstellen einer neuen Klasse Mit einem Klick auf die Schaltfläche KLASSE HINZUFUEGEN wird eine neue Klasse für eine Anwendung erstellt. Grunsätzlich sollte eine eigene Klasse in der Lage sein, Daten zu serialisieren. Daher sollte die eigene Klasse immer von der MFC-Klasse CObject abgeleitet sein. CObject hat die Fähigkeit, daten zu serialiseren. class EigeneKlasse : public CObject { … } Die virtuelle Funktion Serialize() ist ein Member der Klasse CObject und kann in der eigenen Klasse dann überladen werden. Da die eigene Klasse nicht der MFCBibliothek angehört, weiß die MFC-Anwendung nicht, wie sie Daten ins „Archive“175 schreiben muß. Die neue Klasse muß selbst wissen, wie sie Daten ihres Typs ins „Archive“ schreibt. Innerhalb der eigenen Klasse muß zuerst das Makro DECLARE_SERIAL(EigeneKlasse) stehen. Innerhalb der Implementierungsdatei muß vor der eigenen Klasse ein zweites Makro mit dem Namen IMPLEMENT_SERIAL(EigeneKlasse,CObject,1) stehen. Bsp.: Erstellen einer eigenen Klasse „Student“, die Name, Matrikelnummer und Semestergruppe eines Studenten speichert. 1. Erstelle ein neues SDI-Projekt 2. Hinzufügen einer eigenen Klasse - Menübefehl EINFÜGEN/NEUE KLASSE 175 vgl. 6.2.8.3 336 Die Programmiersprache C++ - Markiere im Dialog für den Klassentyp „Allgemeine Klasse“ und trage den Klassennamen ein - Basisklasse für die eigene Klasse ist CObject. 4. Nach Erzeugen einer Header- (Student.h) und Implementierungsdatei (Student.cpp) schreibe in die Headerdatei der Klasse Student: class Student : public CObject { DECLARE_SERIAL(Student) protected: CString s_matrik; CString s_gruppe; CString s_name; class Student* std; public: Student(); virtual ~Student(); public: class Student* getstudent() { return std;} CString getMatrik() ; CString getGruppe(); CString getName() ; void setName(CString); void setMatrik(CString); void setGruppe(CString); virtual void Serialize(CArchive&); }; 337 Die Programmiersprache C++ 5. „Student.cpp“ umfaßt dann: IMPLEMENT_SERIAL(Student,CObject,1) ////////////////////////////////////////////////////////////////////// // Konstruktion/Destruktion ////////////////////////////////////////////////////////////////////// Student::Student() { } Student::~Student() { } ////////////////////////////////////////////////////////////////////// // Methoden ////////////////////////////////////////////////////////////////////// CString Student::getName() { return s_name; } CString Student::getMatrik() { return s_matrik; } CString Student::getGruppe() { return s_gruppe; } void Student::setName(CString str) { s_name = str; } void Student::setMatrik(CString mat) { s_matrik = mat; } void Student::setGruppe(CString gr) { s_gruppe = gr; } void Student::Serialize(CArchive& ar) { CObject::Serialize(ar); if(ar.IsStoring()) { ar << s_name; ar << s_matrik; ar << s_gruppe; } else { ar >> s_name; ar >> s_matrik; ar >> s_gruppe; } } Einrichten von Behandlungsmethoden mit dem Klassen-Assistenten In einem mit dem Anwendungsassistenten erstellten Programm bedient man sich üblicherweise des Klassen-Assistenten zur Einrichtung von Behandlungsmethoden für Nachrichten. Member-Variablen für den Datenaustausch Automatisierung Das Register AUTOMATISIERUNG im Dialog des Klassenassistenten ermöglicht das Hinzufügen und Modifizieren von Automatisierungs-Eigenschaften und – Methoden176. 176 nur interessant, wenn bestimmte Methoden und Eigenschaften der vorliegenden Klasse anderen Programmen zur Verfügung gestellt werden sollen. 338 Die Programmiersprache C++ ActiveX-Ereignisse Die Klasseninformationsdatei (.clw) Der Klassenassistent speichert Informationen, die nicht aus der Quelldatei ermittelt werden können, in einer besonderen Datei (der Klasseninformationsdatei) ab. Die Datei trägt die Bezeichnung des Projekts, die Dateiendung lautet .clw. 6.4.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. 339 Die Programmiersprache C++ Wichtige Funktionen aus den beiden Klassen sind: 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 Dialogseite angegebenen Satzes in der Ansicht einer SDI-Anwendung. Der Text im folgenden Fenster sagt dem Anwender, was zu tum ist. Nach Drücken der „OK“-Schaltfläche erscheint das Fenster der Ansichtsklasse. 340 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. 2. 3. 4. 5. Anlegen eines neuen SDI-Projekts 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. Erstelle mit Hilfe des Klassenassistenten - eine Klasse „CDialog1“ zur Präsentation der Dialogseite - eine Nachrichten-Funktion für die Nachricht WM_LBUTTONDOWN in der Ansicht Mache die Dialogseite in der Ansicht durch Eintragen der Header-Datei der Dialogseite (mit Hilfe von include) bekannt Deklariere in CPr64220Doc eine öffentliche Variable mit dem Namen „satz“ vom Typ CString. 341 Die Programmiersprache C++ void CPr64220View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen //und/oder Standard aufrufen CDialog1 dlg; int iresult=dlg.DoModal(); if(iresult == IDOK) { CPr64220Doc* 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); } 6. Für die Speicherung von “satz” muß in der Funktion Serialize() gesorgt werden void CPr64220Doc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // ZU ERLEDIGEN: Hier Code zum Speichern einfügen // Hier speichern: ar << satz; } else { // ZU ERLEDIGEN: Hier Code zum Laden einfügen // Hier laden: ar >> satz; } } 7. Die Funktion OnDraw() übernimmt die Darstellung des Satzes. void CPr64220View::OnDraw(CDC* pDC) { CPr64220Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten // hinzufügen pDC->TextOut(50,50,pDoc->satz); } 8. 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 CPr64220Doc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen // (SDI-Dokumente verwenden dieses Dokument) //Ab hier: satz = "Hier kann auch Ihr Satz stehen." ; return TRUE; } 342 Die Programmiersprache C++ 9. 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 CPr64220View::OnInitialUpdate() { CView::OnInitialUpdate(); // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen 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; } } 343 Die Programmiersprache C++ 6.4.3 Das MFC-Anwendungsgerüst 6.4.3.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.4.3.2 Anpassen der Fenster 1. Anpassen des Hauptfensters einer DOC/View-Anwendung über den Assistenten 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 in Schritt 4 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. Weiterhin kann die Titelleiste des Rahmenfensters konfiguriert werden (über den Schalter WEITERE OPTIONEN. 344 Die Programmiersprache C++ Abb.: Anpassung der Titelleiste 345 Die Programmiersprache C++ 2. Anpassen über die Methode PreCreateWindow() Diese Methode wird automatisch vor Anpassung des eigentlichen Windows-Fenster aufgerufen177. Dabei wird der Methode die Adresse auf eine Strukturvariable vom Typ CREATESTRUCT übergeben: typedef struct tagCREATESTRUCT { LPVOID lpCreateParams; // Fensterdaten HANDLE hInstance; // HMENU hMenu; // Menue 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. a) 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; } 5. Ausführen der Anwendung 177 Kurz bevor das Anwendungsgerüst intern die Methode Create() aufruft. 346 Die Programmiersprache C++ b) Anpassen des Fensterstils Der Fenterstil wird durch eine Kombination bestimmter Konstanten festgelegt. Stil WS_BORDER WS_CAPTION WS_CHILD WS_CHILDWINDOW WS_CLIPCHILDREN WS_DISABLED WS_DLGFRAME 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 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 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. Dicker 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 Die Stilkonstanten können mit Hilfe der Bit-Operatoren hinzugefügt, gelöscht und kombiniert werden, z.B. 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.style = WS_OVERLAPPEDWINDOW | WS_VSCROLL; cs.x = 100; cs.y = 100; cs.cx = 200; cs.cy = 200; 347 Die Programmiersprache C++ // Deaktivieren Minimieren-Schaltfläche // cs.style ^=WS_MINIMIZEBOX; // Deaktivieren Minimiere return TRUE; } 3. Anpassen über die Methode OnCreate() OnCreate() wird direkt nach Erzeugung der Windows Fenster, aber bevor das Fenster sichtbar wird, aufgerufen. 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. 6.4.3.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. 348 Die Programmiersprache C++ 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. 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. 349 Die Programmiersprache C++ Angabe von Kommnadozeileanargumenten über den Debugger 6.4.3.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()178 behandelt. Diese Methode muß in der abgeleiteten Klasse überschrieben werden. Der Anwendungsassistent tut dies automatisch beim Anlegen des Anwendungsgerüsts. 178 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. 350 Die Programmiersprache C++ 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) 2. Aufruf des Klassen-Assistenten, Einrichten einer behandlungsroutine für die WM_LBUTTONDOWN-Nachricht. 3. Einsetzen des Quellcodes in die Behandlungsroutine OnLButtonDown() /////////////////////////////////////////////////////////////////////////// // CPr64340View Nachrichten-Handler void CPr64340View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen //und/oder Standard aufrufen 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() void CPr64432View::OnDraw(CDC* pDC) { CPr64432Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen 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) 351 Die Programmiersprache C++ 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 in der Dokumenten-Klasse und eines Cpoint-array für 100 Kreismittelpunkte class CPr64342Doc : public CDocument { protected: // Nur aus Serialisierung erzeugen CPr64342Doc(); DECLARE_DYNCREATE(CPr64342Doc) // Attribute public: int m_nZaehler; CPoint m_Kreise[100]; // Operationen public: .......... 3. Initialisieren der Elementvariablen m_nZaehler im Konstruktor der Dokumentenklasse mit dem Wert 0. CPr64342Doc::CPr64342Doc() { // ZU ERLEDIGEN: Hier Code für One-Time-Konstruktion einfügen m_nZaehler = 0; } 4. Einrichten einer Nachrichtenbehandlungsmethode für WM_LBUTTONDOWN in der Klasse des Ansichtsfensters mit Hilfe des Klassenassistenten zum Speichern der Kreismittelpunkte void CPr64342View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen // und/oder Standard aufrufen 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 void CPr64342View::OnDraw(CDC* pDC) { CPr64342Doc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen 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, pDoc->m_Kreise[i].y + b); } } 352 Die Programmiersprache C++ 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 CPr64342View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen // und/oder Standard aufrufen ... 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 mit Hilfe des Klassenassistenten in der Dokumentenklasse die Methode DeleteContents() void CPr64342Doc::DeleteContents() { // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen m_nZaehler = 0; CDocument::DeleteContents(); } 6.4.3.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. 6.4.3.6 Die Nachricht WM_COMMAND Anwenderaktionen werden unter Windows vom Betriebssystem abgefangen und in Form spezieller Nachrichten an betroffene Programme geschickt. So löst bspw. das Drücken der linken Maustaste im Client-Bereich eines Fensters WM_LBUTTONDOWN aus. Windows kann aber nicht wissen, welche Menübefehle die Anwendung zur Verfügung stellt. Daher definiert es einfach eine WM_Nachricht für alle Menübefehle: WM_COMMAND. 353 Die Programmiersprache C++ Ruft der Anwender einen Menübefehl auf (oder drückt er auf eine Symbolfläche), dann wird eine WM_COMMAND-Nachricht an die Anwendung geschickt. Jeder Menübefehl verfügt über eine eindeutige Ressourcen-ID. Diese wird WM_COMMAND als Parameter mitgegeben und von der Anwendung ausgewertet. Dem Anwender bleibt die Aufgabe, die durch die Ressourcen-IDs identifizierten Menübefehle mit Behandlungsmethoden zu verbinden. Abb.: Behandlungsmethoden für Menübefehle einrichten Durch einen Klick mit der rechten Maustaste in das Fenster bei geöffnetem menüEditor kann der Klassen-Assistent aufgerufen werden. Beim Start zeigt der KlassenAssistent automatisch die Seite NACHRICHTENZUORDNUNGSTABELLEN an. Im Feld KLASSENNAME wurde die aktuelle Rahmenfensterklasse ausgewählt. Die Einrichtung einer Behandlungsmethode kann dann folgendermaßen erfolgen: 1. 2. 3. 4. Auswahl der Klasse in der die Behandlungsmethode definiert werden soll. Auswahl der Ressourcen-ID des Menübefehls im Feld OBJEKT_ID. Auswahl des Eintrags COMMAND im Feld NACHRICHTEN. Drücken des Schalters FUNKTION HINZUFÜGEN. Die Methode wird eingerichtet und im Feld MEMBER-FUNKTIONEN angezeigt. 5. Drücken auf den Schalter CODE BEARBEITEN. Im Texteditor wird der Code für die Behandlungsmethode aufgesetzt. 354 Die Programmiersprache C++ 6.4.4 Die Sammlungsklassen der MFC Es gibt vorlagenbasierte (Template Based) und nicht-volagenbasierte179 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. 179 Stammen aus früheren Versionen der MFC-Bibliothek 355 Die Programmiersprache C++ 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 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); } 356 Die Programmiersprache C++ 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; } 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); } } } 357 Die Programmiersprache C++ 6.5 Bilder, Zeichnungen und Bitmaps 6.5.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.180 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: 180 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. 358 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 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. 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); 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) 359 Die Programmiersprache C++ 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); 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 Zeichnet ein Polygon. Übergeben werden die BOOL Polygon(LPPoint lpPoints, int nCount); Punkte, die zu verbinden sind. Der letzte Punkt wird automatisch mit dem ersten Punkt verbunden Abb.: Auswahl an Zeichenmethoden 360 Die Programmiersprache C++ 6.5.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. 361 Die Programmiersprache C++ 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 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 RGBModell181 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ätekoordinaten182 arbeiten. Die MFC stellt Funktionen zur Umwandlung von Koordinaten bereit183. 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öße184. 181 Beruht auf dem Effekt, dass man durch Variation aus den drei Lichtfarben Rot, Grün und Blau sämtliche Farben mischen kann. 182 CDC-Menmber-Funktionen benutzen logische Koordinaten, CWnd-Funktionen benützen Gerätekoordinaten 183 in Gerätekontextklassen und der Klasse CWnd 184 Je nach Auflösung des Bildschirms und der Grafikkarte kann sich die Größe eines Pixels ändern. 362 Die Programmiersprache C++ 7. C# und .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. 363 Die Programmiersprache C++ 8. Die grafischen Bedienoberfächen X und OSF/Motif 8.1 XWindow bzw. X XWindow oder einfach X185 ist die grafische Benutzeroberfäche für UNIXSysteme186. X ist ein Multi Window-System, das Anwendung und Display auf verschiedenen Rechnern erlaubt187. 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 XServer 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 185 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 187 vgl. „Einführung in die XWindows-Programmierung“, Uni Regensburg / Physik (zusammengesetllt von F. Wünsch) 186 364 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ürfnissen188 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. 188 Typischerweise startet der Anwender den Window-Manager automatisch beim Login. Danach wird für die aktuelle „Session“ ein „Look-and-Feel“ festgelegt. 365 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. Widgets189 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-Sets190 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 OSF191) 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. 189 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 191 OSF = Open Software Foundation, inzwischen hat sich OSF/Motif als Standard implementiert 190 366 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. 367 Die Programmiersprache C++ 4. Hinweise an den Window-Manager 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*/ XSelectInput192( 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 192 Dieser befehl sollte vor XMapRaised im Programm stehen, da XMapRaised schon das Expose-Event sendet und damit anzeigt, daß das Fenster wirklich erzeugt ist. 368 Die Programmiersprache C++ 8. Eventbearbeitungsschleife 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 Mauszeiger193 zum Zeitpunkt der Aktion befand) - Tastatur-Events 193 in Pixel zum Window-Ursprung links oben 369 Die Programmiersprache C++ - Expose-Event So ein Event wird immer gesendet, falls die Anwendung den Fensterinhalt neu zeichnen soll. Dies ist der Fall, wenn -- 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 Hintergrund194 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 194 black, white; = BlackPixel(mydisplay, myscreen); = WhitePixel(mydisplay, myscreen); Es gibt weitere Attribute, z.B. das Muster für das Auffüllen von Flächen 370 Die Programmiersprache C++ Kennt man die Nummern der Farben, kann man sie im GC eintragen mit XSetForeGround bzw. XSetBackGround, z.B. /*92*/ /*93*/ XSetForeground(mydisplay, mygc, black); XSetBackground(mydisplay, mygc, white); 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 Funktionen195 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. 195 Die Versorgung der Funktionen mit Parametern, die Wirkungsweise dieser Funktionen sind umfassend in den man-Pages dokumentiert. 371 Die Programmiersprache C++ Ein vollständiges Demonstrationsprogramm: Hilbert-Kurve /* 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*/ /*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*/ #include #include #include #include #include Display Window GC <iostream.h> <X11/Xlib.h> // Definition von X-Datenstrukturen <X11/Xutil.h> // " " " <stdlib.h> <math.h> *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; 372 Die Programmiersprache C++ /*64*/ y_1 = -200; y_2 = -200; /*65*/ generiere(r, 0); /*66*/ } /*67*/ /*68*/ /*69*/ main() /*70*/ { /*71*/ int myscreen; /*72*/ XEvent myevent; /*73*/ int done; /*74*/ /*75*/ unsigned long black, white; /*76*/ int i; /*77*/ /*78*/ cout << "Stufen: "; cin >> stufe; /*79*/ // X-Sitzung eröffnen /*80*/ mydisplay = XOpenDisplay(""); /*81*/ myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr /*82*/ black = BlackPixel(mydisplay, myscreen); /*83*/ white = WhitePixel(mydisplay, myscreen); /*84*/ mywindow = XCreateSimpleWindow(mydisplay, /*85*/ DefaultRootWindow(mydisplay), /*86*/ 100, 300,640, 480, 17, /*87*/ black, white); // Definition Fenster /*88*/ XStoreName(mydisplay, mywindow, "Graphik"); /*89*/ XSetIconName(mydisplay, mywindow, "Draw"); /*90*/ // Erzeugen "graphical context" /*91*/ mygc = XCreateGC(mydisplay, mywindow, 0, 0); /*92*/ XSetForeground(mydisplay, mygc, black); /*93*/ XSetBackground(mydisplay, mygc, white); /*94*/ // Setzen der Event-Maske /*95*/ XSelectInput( mydisplay, mywindow, /*96*/ ButtonPressMask | ExposureMask); /*97*/ XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster /*98*/ // Eventbearbeitungsschleife /*99*/ done = 0; /*100*/ while (done == 0) /*101*/ { // Hole naechsten Eintrag aus dem Puffer /*102*/ XNextEvent(mydisplay, &myevent); /*103*/ switch(myevent.type) /*104*/ { /*105*/ case Expose: /*106*/ if (myevent.xexpose.count == 0) /*107*/ paint(); /*108*/ break; /*109*/ case ButtonPress: /*110*/ done = 1; /*111*/ break; /*112*/ } /*113*/ } /*114*/ // Schliessen der Fenster /*115*/ XFreeGC(mydisplay, mygc); /*116*/ XDestroyWindow(mydisplay, mywindow); /*117*/ XCloseDisplay(mydisplay); // Beenden des Server-Kontakts /*118*/ exit(0); /*119*/ } /*120*/ 373 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 Pixmap Window XPointer (seit X11R4) Xt Datentyp Boolean Bedeutung Display- Beschreibung Zeichensatz Umgebung für Zeichenoperationen Muster beim X-Server Fenster beim X-Server universeller Zeiger Art Struktur ID Zeiger Bedeutung Art char bis long WidgetClass XmString Wahrheitswert196 Laufindex Größenangabe Farbwert Koordinate Zeichenfolge Fensterbeschreibung beim XClient Fenstertypbeschreibung String mit Formatierung XtPointer universeller Zeiger Cardinal Dimension Pixel Position String Widget 196 ID ID char* unsigned int int unsigned long short / int char* Zeiger Zeiger CompoundStrin g void* Für den Datentyp Boolean sind die Konstanten „True“, „TRUE“ (!= 0) „False“, „FALSE“ (== 0) vordefiniert 374 Die Programmiersprache C++ Der hier interessanteste Datentyp ist der Xt Datentyp „Widget“ bzw. „WidgetClass“ . Die Instanzen des Datentyps Widget beschreiben immer einen Bereich des 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. 375 Die Programmiersprache C++ Beispiel eines Instrinics-Programmes // Allgemeine Header-Dateien einbinden #include<stdio.h> #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. 376 Die Programmiersprache C++ 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 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 377 Die Programmiersprache C++ 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 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. 378 Die Programmiersprache C++ 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. 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 wurde197. 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) 197 Eine Auflistung der verschiedenen Callbackressourcen der verschiedenen Widgets folgt im Abschnitt „OSF/Widget Set“. 379 Die Programmiersprache C++ 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. 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. 380 Die Programmiersprache C++ Überblick über alle Widgets Die folgende Aufstellung zeigt die wichtigsten Widgetklassen unterteilt in Display, Shell, Container Widgets: DisplayWidgets: WidgetKlassen-Name Beschreibung Motif-Version XmArrowButton XmScrollBar XmLabel XmList XmSeperator XmText XmTextField XmCsText 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 Zeile im 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 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 1.0 1.0 390 - 1.0 1.0 1.0 1.0 1.0 2.0 2.0 371 74 383 - 1.0 - XmPushButton XmToggleButton XmDrawButton XmCascadeButton ShellWidgets: OverrideShell XmMenuShell WmShell VendorShell TransientShell XmDialogShell TopLevelShell ApplicationShell ContainerWidgets: XmDrawingArea XmRowColumn XmForm XmMessageBox XmFileSelectionBox XmSelectionBox XmScrolledWindow XmSpinBox XmNotebook XmMainWindow 381 Die Programmiersprache C++ Die wichtigsten WidgetKlassen und ihre Ressourcen In diesem Abschnitt werden die wichtigsten Widgetarten genauer erläutert, insbesondere ihre speziellen Ressourcen aufgezeigt. XmLabel-Widget 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 Ressourcen198: 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 CallbackRessourcen 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 198 Neben diesen Ressourcen gibt es natürlich noch viele andere mehr, sie sind in der Fachliteratur dokumentiert. 382 Die Programmiersprache C++ 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. 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 XmCSText-Widgets. 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. 383 Die Programmiersprache C++ XmList-Widget In einem XmList-Widget können, untereinander in Zeilen aufgelistet werden, wobei mit der Maus eine oder mehrere werden können. Eine typische Verwendung ist dieses FileSelectionBox zur Auswahl der Datei. Diesem Widget Ressourcen zur Verfügung, hier die wichtigsten: angeordnet, Texte Zeilen ausgewählt Widget in einer stehen sehr viele 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 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-Widgets199 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. 199 Die Syntax und Funktionsweise kann den man-Pages entnommen werden. 384 Die Programmiersprache C++ 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. 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 385 Die Programmiersprache C++ 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 XmDIALOG_QUESTION XmDIALOG_INFORMATION XmDIALOG_ERROR XmDIALOG_WORKING für kein Symbol für ein Ausrufezeichen für einen Kopf mit Fragezeichen für ein großes I für ein Stop Symbol 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 386 Die Programmiersprache C++ 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. 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: 387 Die Programmiersprache C++ 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. 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); 388 Die Programmiersprache C++ //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) 389