1. Grundlegende Konzepte - oth

Werbung
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 .......................................................................................................................... 42
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 ................................................................................................................. 117
2.2.3.3 Die C++-Stringklasse ................................................................................................................................ 119
2.2.4 Strukturen ........................................................................................................................................ 123
2.2.5 Variantenstruktur ............................................................................................................................ 128
3. BENUTZERDEFINIERTE DATENTYPEN: KLASSEN ........................................................................ 130
2
Die Programmiersprache C++
3.1 KONZEPTE FÜR BENUTZERDEFINIERTE DATENTYPEN ............................................................................... 130
3.1.1 Formale Definitionsmöglichkeiten .................................................................................................. 130
3.1.2 Ein einführendes Beispiel: Stapelverarbeitung ............................................................................... 132
3.1.2 Konstruktoren, Klassenvariable und Destruktoren ......................................................................... 134
3.1.4 Operatorfunktionen ......................................................................................................................... 138
3.1.5 Konstante Komponentenfunktionen ................................................................................................. 139
3.1.6 Statische Komponentenfunktionen ................................................................................................... 140
3.1.7 "friend"-Funktionen und "friend"-Klassen ...................................................................................... 140
3.1.8 ADT Stapel ...................................................................................................................................... 141
3.2 ABGELEITETE KLASSEN ............................................................................................................................ 144
3.2.1 Basisklasse und Ableitung ............................................................................................................... 144
3.2.2 Einfache Vererbung ......................................................................................................................... 145
3.2.3 Klassenhierarchien .......................................................................................................................... 148
3.2.4 Virtuelle Funktionen ........................................................................................................................ 151
3.2.5 Abstrakte Klassen ............................................................................................................................ 155
3.2.6 Mehrfachvererbung ......................................................................................................................... 156
3.2.7 Virtuelle Basisklassen ...................................................................................................................... 158
3.2.8 Generische Datentypen.................................................................................................................... 159
3.3 SCHABLONEN ........................................................................................................................................... 161
3.3.1 Klassenschablonen .......................................................................................................................... 161
3.3.2 Methodenschablonen ....................................................................................................................... 162
3.4 EIN-, AUSGABE ......................................................................................................................................... 163
3.4.1 Aufbau ............................................................................................................................................. 163
3.4.2 Ausgabe ........................................................................................................................................... 166
3.4.3 Eingabe ............................................................................................................................................ 174
3.4.4 Formatierung in Zeichenketten ....................................................................................................... 177
3.4.4.1 strstream für C++ im AT&T-Standard....................................................................................................... 177
3.4.4.2 stringstream für C++ im ANSI/ISO-Standard ........................................................................................... 177
3.4.5 Fehlerzustände ................................................................................................................................ 178
3.4.6 Positionieren in Dateien .................................................................................................................. 179
3.5 AUSNAHMEBEHANDLUNG ......................................................................................................................... 180
3.5.1 Übliche Fehlerbehandlungsroutinen ............................................................................................... 180
3.5.2 Schema zur Ausnahmebehandlung .................................................................................................. 180
3.5.3 Exception-Hierarchie ...................................................................................................................... 182
3.5.4 Besondere Fehlerbehandlungsroutinen ........................................................................................... 183
3.5.5 Unbehandelte Ausnahmen ............................................................................................................... 184
4. DATENSTRUKTUREN .............................................................................................................................. 186
4.1 DER BENUTZERDEFINIERTE DATENTYP „ARRAY“ BZW. „VEKTOR“ .......................................................... 186
4.1.1 Eindimensionale Felder ................................................................................................................... 186
4.1.2 Mehrdimemensionale Felder ........................................................................................................... 186
4.1.3 Darstellung der Datenstruktur Stapel in einem Feld (C-Array) ...................................................... 186
4.1.4 Darstellung der Datenstruktur Schlange in einem Feld (C-Array) ................................................. 188
4.2 LINEARE LISTEN ....................................................................................................................................... 194
4.2.1 Einfach gekettete Listen ................................................................................................................... 194
4.2.2 Klassenschablonen für verkettete Listen.......................................................................................... 203
4.2.2.1 Doppelt gekettete Listen ............................................................................................................................ 203
4.2.2.2 Ringförmig geschlossene Listen ................................................................................................................ 203
4.3 TABELLEN ................................................................................................................................................ 213
4.3.1 Einfache und Sortierte Tabellen ..................................................................................................... 213
4.3.2 Hash-Tabellen ................................................................................................................................. 213
4.4 BINÄRBÄUME ........................................................................................................................................... 220
5. DIE C++-STANDARDBIBLIOTHEK ....................................................................................................... 239
5.1 DIE C++-STANDARDBIBLIOTHEK UND DIE STL ........................................................................................ 239
3
Die Programmiersprache C++
5.1.1 Die C-Standard-Library .................................................................................................................. 240
5.1.2 Hilfsfunktionen und -klassen ........................................................................................................... 244
5.1.2.1 Paare .......................................................................................................................................................... 244
5.1.2.2 Funktionsobjekte ....................................................................................................................................... 244
5.2 CONTAINER .............................................................................................................................................. 248
5.2.1 Bitset ................................................................................................................................................ 253
5.2.2 Deque............................................................................................................................................... 253
5.2.3 List ................................................................................................................................................... 254
5.2.4 Map .................................................................................................................................................. 256
5.2.5 Queue............................................................................................................................................... 259
5.2.6 Set .................................................................................................................................................... 260
5.2.7 Stack ................................................................................................................................................ 263
5.2.8 Vector .............................................................................................................................................. 264
5.3 ITERATOREN ............................................................................................................................................. 266
5.3.1 Iteratorkategorien ............................................................................................................................ 268
5.3.2 distance(), advance() und iter_swap() ............................................................................................. 269
5.3.3 Iterator-Adapter .............................................................................................................................. 270
5.3.4 Stream-Iteratoren ............................................................................................................................ 271
5.3.5 Iterator-Traits .................................................................................................................................. 273
5.4 ALGORITHMEN ......................................................................................................................................... 274
5.5 NATIONALE BESONDERHEITEN ................................................................................................................. 285
5.6 DIE NUMERISCHE BIBLIOTHEK .................................................................................................................. 285
5.6.1 Komplexe Zahlen ............................................................................................................................. 285
5.6.2 Grenzwerte von Zahlentypen ........................................................................................................... 285
5.6.3 Numerische Algorithmen ................................................................................................................. 285
5.6.4 Optimierte numerische Arrays (valarray) ....................................................................................... 287
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...................................................................................................... 287
Binäre Valarray-Operatoren ...................................................................................................................... 287
Mathematische Funktionen........................................................................................................................ 287
slice ........................................................................................................................................................... 287
slice_array ................................................................................................................................................. 287
gslice.......................................................................................................................................................... 287
gslice_array................................................................................................................................................ 287
mask_array ................................................................................................................................................ 287
indirect_array............................................................................................................................................. 287
5.7 TYPERKENNUNG ZUR LAUFZEIT................................................................................................................ 288
5.8 SPEICHERMANAGEMENT ........................................................................................................................... 288
5.8.1 <new> ............................................................................................................................................. 288
5.8.2 <memory> ....................................................................................................................................... 288
6. WINDOWS-PROGRAMMIERUNG UNTER VISUAL C++ .................................................................. 289
6.1 MERKMALE VON VISUAL C++ .................................................................................................................. 289
6.1.1 Visual C++ -Features ..................................................................................................................... 289
6.1.2 Werkzeuge für den Umgang mit Visual C++ -Strukturen ............................................................... 290
6.1.3 Erstellen einer Windows-Anwendung .............................................................................................. 291
6.2 DIE INTEGRIERTE ENTWICKLUNGSUMGEBUNG.......................................................................................... 305
6.2.1 Projekte und Arbeitsbereiche .......................................................................................................... 305
6.2.2 Der Editor........................................................................................................................................ 305
6.2.3 Ressourcen ....................................................................................................................................... 306
6.2.4 Dialogfelder ..................................................................................................................................... 308
6.2.5 Steuerelemente ................................................................................................................................. 308
6.2.6 Mausereignisse ................................................................................................................................ 309
6.2.7 Menüs, Symbolleisten ...................................................................................................................... 310
6.2.8 Texte und Dateien ............................................................................................................................ 313
6.2.8.1 Textverarbeitung........................................................................................................................................ 313
6.2.8.2 Dateien ...................................................................................................................................................... 315
6.2.8.3 Serialisierung ............................................................................................................................................. 316
6.3 APPLICATION PROGRAMMING INTERFACE ................................................................................................ 317
6.3.1 Funktionsweise von Windows Programmen .................................................................................... 317
6.3.2 Eintritt in die Nachrichtenverarbeitung .......................................................................................... 320
6.3.3 Vom API zur MFC ........................................................................................................................... 323
4
Die Programmiersprache C++
6.4 WINDOWS-PROGRAMMIERUNG MIT DER MFC .......................................................................................... 327
6.4.1 Die Assistenten ................................................................................................................................ 327
6.4.1.1 MFC-Anwendungsassistent ....................................................................................................................... 327
6.4.1.2 Der Klassen-Assistent................................................................................................................................ 332
6.4.2 Das Doc/View-Modell ..................................................................................................................... 336
6.4.3 Das MFC-Anwendungsgerüst .......................................................................................................... 341
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 ................................................................................................................................. 341
Anpassen der Fenster ................................................................................................................................. 341
Bearbeitung von Kommandozeilenargumenten ......................................................................................... 345
Die Nachricht WM_PAINT ....................................................................................................................... 347
Zeitgeber (WM_TIMER) .......................................................................................................................... 350
Die Nachricht WM_COMMAND ............................................................................................................. 350
6.4.4 Die Sammlungsklassen der MFC..................................................................................................... 351
6.5 BILDER, ZEICHNUNGEN UND BITMAPS ...................................................................................................... 355
6.5.1 Die grafische Geräteschnittstelle (GDI) .......................................................................................... 355
6.5.2 Die Zeichenwerkzeuge ..................................................................................................................... 358
7. C# UND .NET ............................................................................................................................................... 360
8. DIE GRAFISCHEN BEDIENOBERFÄCHEN X UND OSF/MOTIF .................................................... 361
8.1 XWINDOW BZW. X ................................................................................................................................... 361
8.1.1 Die Komponenten von X .................................................................................................................. 361
8.1.2 Architektur von X-Programmen ...................................................................................................... 363
8.1.3 Ein X-Programm ............................................................................................................................. 364
8.2 OSF/MOTIF .............................................................................................................................................. 371
8.2.1 Einführung in OSF/Motif, Xt Intrinsics ........................................................................................... 371
8.2.2 Struktur eines Intrinsics-Programmes ............................................................................................. 372
8.2.3 Das OSF/Widget Set ........................................................................................................................ 377
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);
{ } markieren den Beginn und das Ende eines Anweisungsblocks.
7
Hinweis: Falls ein Compiler #include <iostream> nicht versteht, entspricht er noch nicht dem C++Standard
11
Die Programmiersprache C++
Das Komma „,“ trennt Elemente in einer Funktions-Argumentenliste.
Das Semikolon „;“ dient als Endekriterium einer Anweisung.
Mit dem Doppelpunkt „:“ wird ein Label gekennzeichnet.
Whitespace-Zeichen8 können sein: Leerzeichen, horizontale und vertikale
Tabulatoren, Zeichenvorschübe, Kommentare. Sie sind für das Erkennen von
Anfang und Ende eines Symbols geeignet. Die beiden folgenden Sequenzen
int n; int x[20];
und
int n;
int x[20];
sind (lexikalisch) identisch und werden in diesselben Symbole zerlegt:
int
n
;
int
x
[
20
]
;
Falls Whitespace-Zeichen innerhalb alphanumerischer Zeichenketten stehen, sind
sie vom normalen Bearbeitungsvorgang (Parsing) ausgenommen, z.B.:
char name[] = "Fachhochschule Regensburg";
Ein Sonderfall ist: Das Auftreten eines Backslash (\) vor dem Zeilenvorschubzeichen. Der Backslash und der Zeilenvorschub werden ignoriert mit der
Konsequenz, daß 2 physisch vorhandene Zeichen als Einheit betrachtet werden.
Aus
"Fachhochschule \
Regensburg"
wird "Fachhochschule Regensburg";
8
Diese Zeichen sind über die Funktion isspace in ctype.h definiert
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 HeaderDatei benötigt wird, ist im jeweiligen Referenzhandbuch der Funktionen beschrieben.
Hinter dem Schlüsselwort include folgt der Name der Datei, die in das Programm
einzukopieren ist. Der Dateiname ist häufig in spitzen Klammern (< > )
eingeschlossen, d.h.: Diese Datei wird in den voreingestellten „include-Pfaden“
gesucht, z.B.: #include <iostream.h>. Ist der Dateiname in Anführungszeichen
eingeschlossen, dann wird diese Datei zuerst im aktuellen Verzeichnis gesucht und
dann erst im voreingestellten Pfad, z.B.:
#include "a:\ausg.h"
#include "a:\sort.h"
#define
Mit #define kann man einer Konstanten einen Namen geben, z.B.: #define
MAXWERT 100
Üblicherweise schreibt man zur besseren Unterscheidung von Variablen die mit
#define definierte Konstante in Großbuchstaben.
C++ kennt aber bessere Methoden (z.B. const) zur Defintion von Konstanten.
Umfassend interpretiert, definiert die Direktive #define ein sog. Makro. Im
einfachen Fall (ohne Parameter) schreibt man #define Makro_Bezeichner <SymbolSequenz>
Jedes Auftreten von Makro-Bezeichner im Quelltext nach dieser Steuerzeile wird
durch die (möglicherweise leere) Symbolsequenz ersetzt.
Eine Makrodefinition ist mit dem Zeilenende abgeschlossen (Verlängerung über "\"
als unmittelbar letztes Zeichen), z.B.:
#define FH
#define PI 3.141592653897953
#define quadrat(a) (a * a)
// leerer Text
// expansionsfähig
// parametrisiert
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.B 11.:
#include <iostream.h>
#define PI 3.14159265389793
#define quadrat(a) (a * a)
int main()
{
int zahl;
cout << "Zahl eingeben:\n";
cin >> zahl;
cout << "\nDie Kreisflaeche betraegt: "
<< quadrat(zahl) * PI;
}
Unerwünschte Seiteneffekte können allerdings auftreten. So würde z.B.
quadrat(zahl - zahl) * PI keineswegs das erwartete Ergebnis 0 anzeigen.
Ein korrektes Ergebnis würde in diesem Fall allerdings die Makrodefinition #define
quadrat(a) ((a) * (a)) erzwingen.
#undef
Mit dieser Direktive kann ein Makro außer Kraft gesetzt werden: #undef MakroBezeichner. Diese Zeile entfernt jede vorherige Symbol-Sequenz aus dem MakroBezeichner. Die Makro-Definition wird sofort ungültig, der Makro-Bezeichner ist von
da ab undefiniert.
#ifdef und #ifndef
Mit diesen Bedingungsdirektiven kann geprüft werden, ob ein Bezeichner derzeit
definiert ist oder nicht. Die Zeile #ifdef Bezeichner besitzt die gleiche Wirkung
wie #if 1, falls Bezeichner derzeit definiert ist, und die Wirkung #if 0 ,falls
Bezeichner nicht definiert ist.
Mit der #ifdef-#endif - Anwisung kann überprüft werden, ob eine Konstante
bereits ein anderer Stelle definiert wurde. Die Anweisung #ifndef-#endif leistet
das Gegenteil, z.B.:
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
Die ersten beiden
höchstwahrscheinlich:
Präprozessoranweisungen
von
iostream.h
lauten
#ifndef __IOSTREAM_H
#define __IOSTREAM_H
Der Zweck dieser Angaben ist: Verhinderung von Mehrfachdefinitionen derselben
Datei. Sobald die Datei name.h zum 1. Mal vom Präprozessor bearbeitet wird, ist
der Name __NAME_H definiert. Bei einer weiteren Inklusion überspringt dann der
Präprozessor den gesamten Text bis zum passenden #endif.
11
vgl. PR12203.CPP
16
Die Programmiersprache C++
#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];
}
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
12
vgl. Borland C++ 4.0, Programmierhandbuch S. 206
17
Die Programmiersprache C++
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:
-
Array-Indizes beginnen mit 0. Das erste Element im C-Array hat den Index 0, das zweite Element
den Index 1, usw.
Array-Größen müssen Kompilierzeitkonstanten sein, d.h. der Compiler muß zur Kompilierzeit
wissen, wieviel Platz er für das Array allokieren kann.
Ein C- Array kann kein sogenannter L-Wert14 sein
Zur Ausgabe der Feldelemente wurde in
das vorliegende Programm die
Funktionsprozedur „ausgabe“ (vgl. Zeile /* 22 */) aufgenommen:
const zeilenLaenge = 12;
13
14
// Anzahl der Elemente je Zeile
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++
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 Typ 17, für den die
Stream-Ausgabe definiert wurde, z.B.: cout << "hallo!\n";
Der Operator << wirkt von links nach rechts und liefert eine Referenz auf das
ostream-Objekt, für das er gerufen wurde. Dadurch wird es möglich, mehrere
Ausgaben aneinanderzureihen, z.B.:
#include <iostream.h>
int main()
{
int x = 124;
float y = 1.5;
cout << "x = " << x << "y = " << y << "\n";
}
Zeichenketten enthalten besondere Zeichen, die nicht ausgedruckt werden, z.B.:
cout << "Elementweise Eingabe: \n";
cout << "\n\t"
Die Zeichenfolge „\n“ nennt man Escape-Sequenz. Escape-Sequenzen werden
durch einen „backslash“ eingeleitet, dem ein oder mehrere Zeichen folgen. Das „n“
bedeutet „new-Line“ (und bewirkt einen Zeilenvorschub). Andere Zeichen, z.B. ‘\t‘
bewirken einen Tabulatorsprung. Escape-Sequenzen können an beliebiger Stelle
und beliebig oft in der Zeichenkette auftauchen. Sie können in Hochkomma ('\n') und
in Anführungszeichen ("\n")stehen.
Folgende grundlegende Datentypen werden direkt unterstützt: char, short,
int, long, char*18, float, double, long double und void*.
15
vgl. 3.4.2
Linksshift zur Bitmanipulation
17 Das sind die grundlegenden Typen oder beliebige, überladene Typen
18 Behandlung als Zeichenkette
16
19
Die Programmiersprache C++
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.
19
vgl. 3.4.2
vgl. 3.4.2
21 vgl. 3.4.3
22 Sollen Zwichenraumzeichen nicht ignoriert werden, ist die Funktion get() zu verwenden, die zum Einlesen
einzelner Zeichen (keine Zahlen) verwendet werden kann, z.B.: // einzelnes Zeichen einlesen
char zch;
cin.get(zch);
20
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 EXE-Datei
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 Konvention 29.
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 */
main()
{
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++
int i = 2; // überlagert äußeres i
{
double i = 3.14; //überlagert inneres int i
cout << i << "\n";
} // Ende des Gültigkeitsbereichs von double i
cout << i << "\n";
} // Ende des Gültigkeitsbereichs des inneren i
Globale Namen sind außerhalb jedes Blocks und außerhalb jeder Klasse deklariert 32.
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);
}
Using-Deklaration und -Direktive
34
Die Programmiersprache C++
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.
Unbenannte Namenbereiche
35
Die Programmiersprache C++
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 Programm 37 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 Vorzeichenbit 39
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
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
38
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 „*= “.
Das Operationszeichen (+, -, *, /, usw.) steht immer vor dem Operator der
Zuweisung. Die Anweisung
40
Hinweis: Diese Fassung läuft um Bruchteile von Sekunden schneller ab
41
Die Programmiersprache C++
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
1.5.6 Bedingungsoperator
42
Die Programmiersprache C++
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.:
#include <iostream.h>
42
Jedem Ausdruck, dem ein Semikolon folgt, ist eine Ausdrucksanweisung
45
Die Programmiersprache C++
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;
do
44
kopfgesteuerte Schleife
fußgesteuerte Schleife
46 PR16206.CPP
45
47
Die Programmiersprache C++
{
for (int i = 1; i <= b; i++)
cout << '*';
cout << endl;
c = a + b;
a = b;
b = c;
} while (c < maxwert);
}
2) Sortieren durch Zerlegen „Quicksort"
Der Algorithmus besteht aus folgenden Arbeitsschritten:
(1) Auswahl eines Elements „test“ aus der zu sortierenden Liste
(2) Zerlegen des zu sortierenden Bereichs in 2 Teilbereiche
- Teilbereich 1 enthält alle Elemente, die kleiner sind als „test“
- Teilbereich 2 enthält alle Elemente, die größer sind als „test“
(3) Sortien der Teibereiche (rekursiv)
void quicksort(int *x, int links, int rechts)
{
int i = links;
int j = rechts;
// Auswahl eines Elements aus dem zu sortierenden Teilbereich
int test = x[(links + rechts) / 2];
do {
// Zerlegen in 2 Teilbereiche
while (x[i] < test) i++;
while (test < x[j]) j--;
if (i <= j)
{
tauschen(x,i,j);
i++;
j--;
}
} while (i <= j);
if (links < j) quicksort(x,links,j);
if (i < rechts) quicksort(x,i,rechts);
}
4. continue und break
continue
bewirkt, daß der Rumpf einer Schleife nicht bis zum Ende ausgeführt wird, sondern
sofort ein neuer Schleifendurchgang startet.
break
Verlassen der Schleife, d.h. Sprung hinter das Schleifenende.
Alle 3 Schleifenarten lassen sich mit Hilfe von break vorzeitig verlassen. Tritt der
Befehl im Schleifenrumpf auf, wird an das Ende gesprungen und die Schleife nicht
wiederholt.
Bsp.:
#include <iostream.h>
48
Die Programmiersprache C++
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";
}
nur aus, falls x tatsächlich den Wert 100 hat. Wird angegeben
if (x == 100)
49
zu
einem
Block
Die Programmiersprache C++
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.
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
50
Die Programmiersprache C++
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
Anweisung
1
if - else
falsch
2
switch
switch (Ausdruck)
case 1
case 2
default
Folge
Folge
Folge
Folge
Ausdruck
solange Ausdruck
2
Folge
for
1
wahr ist
Anweisung;
Ausdruck
3
while
solange Ausdruck w ahr ist
Anweisung;
do - while
Anweisung;
solange Ausdruck w ahr 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.
3. Vektoren als Parameter
55
Die Programmiersprache C++
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';
}
Für Parameter dürfen Standardwerte (default values) angegeben werden. Sie
werden vom Übersetzer eingesetzt, falls das entsprechende Argument im Aufruf
fehlt.
48
49
PR17202.CPP
PR17204.CPP
56
Die Programmiersprache C++
5. Beliebig viele Argumente (variable Parameterlisten)
Für Funktionen, bei denen Anzahl und Typ der Argumente nicht exakt feststeht,
endet die Argumentenliste mit einer Ellipse (...). Damit wird ausgedrückt: Es können
weitere Argumente folgen. Ein gut entworfenes Programm wird höchstens einige
wenige solcher Funktionen benötigen. Überladene Funktionen und Funktionen mit
Default-Argumenten ermöglichen in den meisten Fällen ordnungsgemäße
Typüberprüfungen. Nur wenn Anzahl und Typ der Argumente unbekannt sind, ist die
Ellipse notwendig (wichtigster Anwendungsbereich: Schnittstellen-Spezifikation zu
C-Bibliotheken die zu einer Zeit entworfen wurden, als andere Alternativen nicht
verfügbar waren).
Bsp.: Die C-Ausgabe-Funktion int printf(const char* format,...) erzeugt
für eine beliebige Anzahl von Argumenten formatierte Ausgaben, deren Gestalt vom
Format-String "format" kontrolliert wird. Ein vollständiger Aufruf von printf() ist
nach dem folgenden Schema aufgebaut:
printf(format, wert1, wert2, ....)
Der Format-String besteht aus zwei Typen von Elementen:
-
einfache Textzeichen (die in den Ausgabestrom kopiert werden)
Umwandlungs-Angaben (eingeleitet durch das Zeichen %)
Die Textzeichen werden unmittelbar ausgegeben, für Umwandlungsangaben
(Formatelemente) wird der Text ausgegeben, der durch die Umwandlung eines
Wertarguments entsteht, z.B.:
#include <stdio.h>
main()
{
char* vorname = "juergen";
char* nachname = "sauer";
printf("Hallo Informatiker\n");
printf("Mein Name ist %s %s\n", vorname, nachname);
// %s erwartet char*
printf("%d + %d = %d\n",2,3,5);
// %d erwartet int-Argument
}
Das 1. Argument von printf() ist ein Format-String mit speziellen Zeichenfolgen,
die es printf() erlauben, die übrigen Elemente richtig zu verarbeiten (z.B. %s,
%d). Der Compiler weiß das nicht und kann somit u.U. nicht sicherstellen, daß die
erwarteten Argumente tatsächlich vorhanden sind. Das „%“-Zeichenmit dem
folgenden Konvertierungszeichen (code) dient zur Formatierung, so daß printf()
Zahlenvariable auch als Text ausgegeben kann. Einige der am häufigsten
verwendeten Formatelemente sind:
d:
x:
o:
u:
c:
s:
dezimal
hexadezimal
oktal
dezimal (ohne Vorzeichen)
einzelnes Zeichen
Zeichenkette
57
Die Programmiersprache C++
e: dezimale Gleitkommadarstellung (scientific)
f: dezimale Festkommadarstellung (fixed)
g: 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
formale Parameter übergeben werden dürfen 51. 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>
50
PR17205.CPP
An Stelle der 3 Punkte sind beliebige Parameter erlaubt, ohne Rücksicht darauf, welche Art Argument die
Funktion tatsächlich erwartet. Durch das Auslassungszeichen wird der Typkontrollmechanismus von C++ außer
Kraft gesetzt.
52 PR17203.CPP
51
58
Die Programmiersprache C++
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 Funktionsprototyp 53 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.
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.
05
. 2  0.25 . Ebenso sind Hochzahlen mit negativen Werten (Ganzzahlen) möglich,
1
1
 4.
z.B.: 0.52  2 
0.25
0.5
4. Fibonacci-Zahlen
Die Berechnung dieser Zahlen erfolgt nach folgender Vorschrift:
Fib(N) = Fib(N - 1) + Fib(N - 2) für N > 1.
Fib(1) = 1, Fib(0) = 0
Dieser Vorschrift kann folgende Funktionsprozedur zugeordnet werden:
unsigned long fib(short n)
{
if (n == 0) return 0;
else if (n == 1) return 1;
else
return(fib(n-1) + fib(n-2));
}
Das folgende Programm ermöglicht durch die Funktion „striche()“ und einen Zähler in
der Parameterliste ein Protokoll. Es zeigt, welche Funktionsaufrufe in welchen
Rekursionsstufen (-tiefen)54 auftreten:
#include <iostream.h>
void striche(short st)
{
for (int i = 1; i <= st; i++)
cout << "- ";
}
unsigned long fib(short xN, short stufe)
{
unsigned long x, y;
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++
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 =
- - Eingang: N =
- - Ausgang: N =
- Ausgang: N = 3
Ausgang: N = 5 X
Ergebnis: 5
3
=
N
N
N
N
=
=
=
3
2
=
=
=
=
2
X
2
=
=
=
=
2
1
1
X
=
2
=
=
=
=
2
1
1
X
1
1
0
0
X
=
1
1 X = 11721 Y = 1
0
0 X = 11721 Y = 1
X = 1 Y = 0
X = 1 Y = 0
= 1 Y = 1
X = 11721 Y = 1
X = 11721 Y = 1
= 1 Y = 0
2 Y = 1
1
1 X = 11721 Y = 1
0
0 X = 11721 Y = 1
X = 1 Y = 0
X = 1 Y = 0
= 1 Y = 1
3 Y = 2
63
Die Programmiersprache C++
Jeder Aufruf mit N > 1 führt zu zwei weiteren Aufrufen. Das zeigt die folgende
grafische Darstellung der Aufrufhierarchie:
Aufruf: Fib(5)
4
3
3
2
1
2
1
1
2
0
1
1
0
0
Abb. 1.8-1: Schematische Darstellung der Aufrufhierarchie von Fib(5)
Insgesamt finden für Fib(5) 15 Aufrufe statt, für den Aufruf Fib(6) sind es bereits 31.
Viele Aufrufe und damit Berechnungen werden wiederholt, obwohl sie bereits an
anderer Stelle berechnet wurden. Allgemein wird die Funktion Fib() bei Eingabe einer
beliebigen positiven, ganzen Zahl N genau 2 N-1 - 1 mal aufgerufen. Man sagt: Der
Algorithmus verhält sich proportional zu 2N-1 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;
vvx = vx;
vx = x;
}
return(x);
}
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++
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 S 1 = 1):
S n  2  (S n 1 )  1
Sn  2 n  1
Die allgemeine Lösung der Aufgabe besteht in
59
d.h. die Stelle, an der nach der Ausführung der gerufenen Funktionsprozedur die Programmkontrolle
übergeben wird
67
Die Programmiersprache C++
1) Transport von (n - 1) Scheiben nach dem Hilfsplatz
Quellplatz
Hilfsplatz
2) Verlagerung von Scheibe „n“ nach Zielplatz
Quellplatz
Zielplatz
3) Transport von (n - 1) Scheiben nach Zielplatz
Zielplatz
Hilfsplatz
Abb. 1.8-2: Beschreibung der Problemlösung
In der vorliegenden Abbildung werden zunächst 4 der 5 Scheiben vom Quellplatz auf
einen Hilfsplatz bewegt. Danach wird die größte Scheibe vom Quellpaltz auf den den
Zielplatz gebracht. Anschließend bewegt man die 4 Scheiben vom Hilfsplatz auf den
Zielplatz und zwar 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";
cout << "\n\t\t\t-------------------\n\n";
62
PR17305.CPP
71
Die Programmiersprache C++
do
{
cout << "\tWieviele Scheiben sollen bewegt werden (max. "
<< MAXSCHEIBEN << "): ";
cin >> anzahl;
} while ((anzahl < 1) || (anzahl > MAXSCHEIBEN));
/* Der Server "turm" erbringt fuer den Klient "main" eine Dienstleistung */
turm.bewegeTurm(anzahl, 'K', 'S', 'G');
}
// Rekursionsanfang
cout << endl;
// Ende von main()
Auch hier ist festzustellen: Die Bearbeitung einer großen Anzahl von Scheiben
nimmt einige Rechenzeit in Anspruch. Auch hier die Komplexität dieses Algorithmus
exponentiell (2N). genau wie bei der Berechnung der Fibonacci-Zahlen zieht ein
Aufruf zwei weitere nach sich.
1.7.4.4 Damen-Problem
Das Problem wird beschrieben am einfachen, übersichtlich darstellbaren 4-DamenProblem, einer Vereinfachung des bekannten 8-Damen-Problems. Hier sollen
bekanntlich 8 Damen so auf einem Schachbrett positioniert werden, daß sie sich
nicht schlagen können, d.h.: Zwei Damen stehen nie in derselben Zeile oder Spalte
oder Diagonale. Das Verfahren kann grafisch als Baum dargestellt werden, dessen
Knoten Lösungsvektoren sind und dessen Kanten den Weg des Algorithmus von
einem Lösungsvektor der Länge (i - 1) zu einem der Länge i angeben.
(_,_,_,_)
1
2
(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
Von der Ausgangssituation (keine Dame ist positioniert, Wurzel) werden bestimmte
Lösungsvariablen verfolgt. Im ungünstigsten Fall führt die bearbeitete Variante in
72
Die Programmiersprache C++
eine Sackgasse. In diesem Fall muß der Variantenpfad so weit zurückverfolgt
werden, bis man zu einem Knoten mit einem noch nicht bearbeiteten Nachfolger
kommt. Man spricht bei dieser Vorgehensweise auch von Tiefensuche in
vorliegenden Baumgraphen und nennt die Verfahrensweise Backtracking.
Die Lösung läßt sich anschaulich darstellen:
0
-3
3
y
x
x
x
x
x
2
8
5
Abb. 1.8-5: „Eindimensionale Beschreibung“ des 4-Damen-Problems
Da Damen sich auch in der Diagonale bedrohen, sind auch Haupt- und Nebendiagonalplätze zu überwachen.
Es sollen zur Lösung des 8-Damen-Problems acht Zahlen ermittelt werden. Die
Zahlen stehen von links nach rechts für die Position der Dame in der entsprechenden Spalte.
/*
Das Problem der acht Damen
-------------------------Kurzbeschreibung: Durch Backtracking werden alle Moeglichkeiten
gefunden, acht Damen so auf einem Schachbrett
zu positionieren, dass keine Dame eine andere
bedroht.
*/
#include <iostream.h>
class damen
{
private:
73
Die Programmiersprache C++
unsigned short pos[8];
bool Zeile[8];
bool HD[15];
// Hauptdiagonale "/"
bool ND[15];
// Nebendiagonale "\"
public:
damen(); // Konstruktor
void versuche(int);
friend ostream& operator <<(ostream &, damen &); // Ausgabe
};
// ------ Schnittstellendefinition (Methoden) ------------------damen :: damen()
{
int i;
// Im Konstruktor werden die einzelnen Felder initialisiert.
for(i = 0; i < 8; i++) Zeile[i] = true;
for(i = 0; i < 15; i++) HD[i] = ND[i] = true;
}
void damen :: versuche(int j)
{
for(int i = 0; i < 8; i++)
{
if(Zeile[i] && HD[j+i] && ND[7+i-j])
{
// Position ist moeglich, also wird Dame gesetzt:
pos[i] = j;
// Dadurch werden alle oben ueberpr•ften Zeilen und
// Diagonalen bedroht:
Zeile[i] = HD[j+i] = ND[7+i-j] = false;
/* Falls noch nicht alle Damen gesetzt wurden,
erfolgt ein weiterer, rekursiver Aufruf, der die
naechste Spalte ueberprueft.
*/
if(j < 8) versuche(j+1);
else // Sonst wurde moegliche Loesung gefunden
cout << (*this);
/* Nun muss der Versuch noch zurueckgenommen werden,
da er am nach Verlassen dieser Spalte wieder
moeglich wird
*/
Zeile[i] = HD[j+i] = ND[7+i-j] = true;
} // Ende von if( ...
}
// Ende von for( ...
}
// Ende von Versuche()
// --------- non-member-Funktionen --------------------ostream& operator<<(ostream &s, damen &d)
{
s << "\n\t";
for(short i = 0; i < 8; i++)
s << d.pos[i] << "
";
return(s);
}
// --------Klient: main-program -----------------------int main()
{
damen queens;
// Instanz, Server
queens.versuche(1);
// Rekursionsanfang
}
// Ende von main()
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
Rückgabetyp wird nicht in den Namen codiert. Genausowenig wird zwischen einem
63
vgl. PR17308.CPP
75
Die Programmiersprache C++
Argument als „value“-Parameter und einem Argument gleichen Typs als ReferenzParameter unterschieden. Die große Familie der in Deklarationen möglichen
Spezifizierer und Modifizierer erschweren Compiler und Programmierer die
Entscheidung, ob Funktionen mit denselben Namen unterschiedliche
Parameterlisten besitzen oder nicht. So können bspw. "reine" und ReferenzParameter nicht unterschieden werden, da ihre aktuellen Parameter identisch sind.
Grundsätzlich gilt: Sind die aktuellen Argumente zweier formaler Parameter, die sich
nur im Spezifizierer unterscheiden, identisch, dann ist Überladen nicht möglich, z.B.:
f(int x);
f(const int x);
f(volatile int x);
// Fehler
// Fehler
Auch
int f() bzw. int f(void)
sind in einem Gültigkeitsbereich nicht möglich.
Funktionen können nur im selben Geltungsbereich (scope) überladen werden.
Funktionen aus einem äußeren Gültigkeitsbereich werden durch die lokale
Deklaration einer Funktion gleichen Namens verborgen. Falsch ist daher
extern int f(char *);
void illegal()
{
extern double f(double); // verbirgt äußeres f
f("Leider falsch");
}
Der Compiler arbeitet mit festen Regeln bei der Ermittlung der Definition einer
Funktion, die für den jeweiligen Parameter am besten paßt.
Bsp.: Hochzählen von Zahlen und Buchstaben durch eine überladenen Funktion
#include <iostream.h>
int naechsteGroesse(int a)
{
return(++a);
}
int naechsteGroesse(int a, int b)
{
return(a + b);
}
char naechsteGroesse(char a)
{
return(++a);
}
char naechsteGroesse(char a, int b)
{
return ((char)(a + b)); // cast sorgt für korrekte Rueckgabe
}
int main()
{
cout << "\n Die naechste Groesse von 5 ist "
<< naechsteGroesse(5);
76
Die Programmiersprache C++
cout << "\n Die naechste Groesse von F ist "
<<naechsteGroesse('F');
cout << "\n Die naechste Groesse von 5 ist nach 4 Einheiten "
<< naechsteGroesse(5,4);
cout << "\n Die naechste Groesse von F ist nach 4 Einheiten "
<< naechsteGroesse('F',4);
}
Der Compiler bildet für jeden Parameter eine Liste von passenden Funktionsversionen. So erzeugt der Compiler für den Aufruf naechsteGroesse(‘F’,4)
folgende Listen:
Nr. des Parameters:
passende Funktion
1
naechsteGroesse(char,int)
naechsteGroesse(char)
2
naechsteGroesse(char,int)
Aus dieser Funktionsmenge bildet er dann die Schnittmenge. Ist diese leer oder
enthält sie mehr als eine Funktionsversion, so führt das zu einem Fehler, z.B.:
f1(char a, int b) { ....... }
f1(int a, char b) { ....... }
......
f1('x','y');
Default-Argumente
In C++ kann der Funktion eine variable Anzahl von Parametern übergeben werden 64,
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 p i die Menge Fi’ aller Funktionen aus F,
die bzgl. des Datentyps für das Argument p i am besten zum gegebenen Aufruf
passen.
Am „besten passen“ bedeutet:
- Es werden keine Konversionssequenzen mit mehr als einer benutzerdefinierten
Konversionsfunktion berücksichtigt.
- Eine kürzere Trivialkonversion wird einer längeren vorgezogen
- Die folgenden Trivialkonversionen haben im allg. keinen Einfluß auf die
Beurteilung zweier Konversionssequenzen: T -> T&, T& -> T, T[] ->
T*, T() -> (*T). T -> const T, T* -> const T*.
Es läßt sich für Argumentkonversionen eine Wertskala angeben. Konversionen,
die eine Konversion einer niederen Stufe beinhalten, sind besser als solche, die
auch Konversionen höherer Stufen miteinbeziehen:
1. Exakte Entsprechung
Es sind keine Konversionen (oder lediglich Trivialkonversionen) zur Anpassung des aktuellen
Arguments an den Typ des formalen Parameters nötig. Unter Trivialkonversion sind jene, die
„const“-Zeiger oder -Referenzen aus „nicht const-Zeigern“ oder „-Referenzen“ erzeugen,
schlechter als andere.
2. Auswertungen (Integralauswertungen und float-double-Konversionen)
Integralauswertungen sind: char, short, Aufzählungstypen, Bitfelder. Sie können immer anstelle
vom int benutzt werden. Falls Werte des ursprünglichen Datentyps durch int dargestellt
werden können, dann wird nach int umgewandelt, andernfalls nach unsigned int. Es wird
von kleinen ganzzahligen Typen in Richtung int und von kleineren gebrochenzahligen Typen
Richtung double promotet.
3. Standardkonversionen.
Der Compiler versucht. Aktuelle und formale Parameter durch Anwendung von
Standardkonversionen zur Deckung zu bringen. Alle numerischen typen oder zeichentypen
können ineinander konvertiert werden, außerdem können alle Zeiger zu void* konvertiert
werden.
4. Benutzerdefinierte Konversionen.
Es werden die benutzerdefinierten Konversionen angewandt, die in benutzerdefinierten Klassen
definiert werden.
5. Variable Parameterliste
Die Zuordung eines Arguments zum Auslassungszeichen (...) ist schlechter als jede Konversion.
(3) Bilde den Durchschnitt aller Fi’. Enthält dieses genau ein Element, nenne es f
und fahre mit Schritt (4) fort. Andernfalls ist der Aufruf illegal.
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; }
main()
{
68
d.h.: Defaultargumente sind nicht erlaubt
Semantik
70 das wurde in der iostream-Klasse so festgelegt
69
80
Die Programmiersprache C++
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. T i 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:
template <class T> T min(T a, T b)
{
return (a < b ? a : b);
}
71
Anstatt class kann auch typename geschrieben werden
81
Die Programmiersprache C++
Diese Funktion wird wie eine gewöhnliche Funktion aktiviert:
double m = min(13.11,11.13);
int m = min(13,11);
Natürlich ist diese Funktionsschablone auf den Vergleich solcher Elemente
beschränkt, die in einer eindeutige Ordnungsbeziehung zueinander stehen. Ein
derartiger unmittelbarer Vergleich ist bei Zeichenketten 72 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.
„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.
72
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
15
int
long int
s
31
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-1: Ganzzahlige 16-Bit-Datentypen
short int
int,
long int
s
15
0
s
31
0
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-2: Ganzzahlige 32-Bit-Datentypen
float s Exponent
31
22
Mantisse
0
double s Exponent
63
51
Mantisse
0
s: Vorzeichenbit (0 = positiv, 1 = negativ)
Abb.: 2.1-3: Gleitkomma-Datentypen
77
Für Borland C++ ist die IBM-PC-Familie Ausgangspunkt. Somit bestimmt die Architektur der Intel 8088- und
80x86-Mikroprozessoren die Auswahl der internen Darstellung für verschiedene Datentypen
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
34
.  10 38 bis 3.4  1038
17
.  10 308 bis 17
.  10308
long double
80
3.4  10 4932 bis 11
.  10 4932
Anwendungen, z.B.:
Kleine Zahlen,
kompl. PC-Zeichens.
Kleine Zahlen
ASCII-Zeichen
Geordnete
Wertemengen
Große Zahlen
Schleifenzähler
Zähler, kleine Zahlen
Zähler,
kleine Zahlen
Astronomische
Distanzen
sehr große
Zahlen
7-stellige Genauigkeit
15-stellige
Genauigkeit
19-stellige
Genauigkeit
Abb. 2.1-4: 16-Bit-Datentypen, Größen und Bereiche
Typ
unsigned char
char
short int
unsigned int
int
Größe (Bits)
8
8
16
32
32
unsigned long
32
enum
32
long
32
float
32
double
64
long double
80
Bereich
0 bis 255
-128 bis 127
-32768 bis 32768
0 bis 4294967295
-2147483648 bis
2147483647
0 bis 4294967295
-2147483648 bis
2147483647
-2147483648 bis
2147483647
34
.  10 38 bis 3.4  1038
17
.  10 308 bis 17
.  10308
3.4  10 4932 bis 11
.  104932
Anwendungen
Kleine Zahlen
ASCII-Zeichen
Zähler
Große Zahlen, Schleifenzähler
Zähler, kleine Zahlen
Astronomische
Genauigkeit
Geordnete Wertemengen
sehr große Zahlen
7-stellige Genauigkeit
15-stellige Genauigkeit
19-stellige Genauigkeit
Abb. 2.1-5: 32-Bit-Datentypen, Größen und Bereiche
Die in einem C++-System zutreffenden Zahlenbereiche findet man in der Datei
limits78. C++ bietet die Möglichkeit, den Zahlenbereich mit einer Funktion
abzufragen, z.B.:
#include <iostream>
#include "c:\cppbuch\include\limits"
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++
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
i & 7
i ^ 7
Unäres Plus
Unäres Minus
Vorherige Inkrementierung um Eins
Nachfolgende Inkrementierung um Eins
Vorherige Dekrementierung um Eins
Nachfolgende Dekrementierung um Eins
Binäres Plus
Binäres Minus
Multiplikation
Division
Modulo (Rest mit Vorzeichen von i)
Zuweisung
i
i
i
i
i
=
=
=
=
=
i
i
i
i
i
*
/
%
+
-
3
3
3
3
3
Kleiner als
Größer als
Kleiner gleich
Größer gleich
Gleich
Ungleich
Linksschieben
Rechtsschieben
Bitweises UND
Bitweises XOR (Exklusives Oder)
signed und unsigned sind sog. Modifizierer, da sie die Bedeutung des folgenden Begiffs ändern
86
Die Programmiersprache C++
|
~
<<=
>>=
&=
^=
|=
i | 7
~i
i <<= 3
i >>= 3
Bitweises ODER
Bitweise Negation
i = i << 3
i = i >> 3
i = i & 3
i = i ^ 3
i = i | 3
i &= 3
i ^= 3
i |= 3
Abb.: Operatoren für Ganzzahlen
C++ unterschiedet zunächst einmal zwei "integer"-Typen: char und int.
Zugehörige Werte sind ohne Umwandlung untereinander austauschbar.
Zeichenliterale (z.B. '!') sind "integer"-Werte. Auch Wahrheitswerte werden in
Ganzzahlen gefaßt. "0" entspricht dem logischen "false", alles andere ist "true".
Da der Datentyp char intern als 1-Byte-Ganzzahl dargestellt wird, sind eigentlich alle
Ganzzahl-Operatoren möglich. Bei der Interpretation von char als Zeichen sind nur
folgende Operatoren sinnvoll:
Operator
=
<
>
<=
>=
==
!=
Bedeutung
Zuweisung
Kleiner als
Groesser als
Kleiner gleich
Groesser gleich
Gleich
ungleich
Beispiel
x = ‘A‘
Der Datentyp size_t ist ein abgeleiteter Typ für Größenangeaben, die nicht negativ
werden können. Der Typ entspricht entweder unsigned int oder unsigned
long, je nach C++-System. Die Definition steht im Header <cstddef>.
Ein weiterer integraler Datentyp ist der Aufzählungstyp (enumeration type). Die
Syntax zur Deklaration ist
enum [Typname] {Aufzaehlung} [Variablenliste]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 TypSpezifizierer 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
bool is_modulo
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
true, falls bereichsüberschreitende Operationen wieder eine gültige Zahl
ergeben.
NaN: „not a number“
91
Die Programmiersprache C++
round_style
Art der Rundung
Ganzzahlen: rounded_toward_zero (= 0)
Gleitkommazahlen: round_to_nearest (= 1)
Abb.: <limits> Attribute und Funktionen
Mit typedef kann man einen anderen Namen für einen Typ vereinbaren, z.B.:
typedef float real;
int main()
{
real zahl = 1.7353;
....
}
// zahl ist vom Typ float
typedef eignet sich besonders zur Vereinfachung komplexer Datentypen.
Elementare Typen lassen sich beliebig mischen. Dabei werden Werte konvertiert. In
arithmetischen Ausdrücken mit binären Operatoren wird folgende implizite
Typkonvertierung vorgenommen: Der Operand mit dem niederwertigen Typ wird in
den höherwertigen Typ konvertiert. Jeder Operand vom Typ char und short wird
nach int und jeder Operand vom Typ float nach double konvertiert. Bei
Anweisungen wird versucht den Wert der rechten Seite in die linke Seite zu
konvertieren.
Wird ein vorzeichenbehaftetes Objekt (z.B. eine Variable) einem integer-Objekt
zugeordnet, erfolgt eine automatische Erweiterung des Vorzeichens. Objekte vom
Typ signed char haben immer ein Vorzeichen. Objekte vom Typ unsigned char
setzen das höherwetige Byte auf Null, wenn sie nach int konvertiert werden.
Bei Konvertierung eines längeren „integer“-Typs in einen kürzeren werden die
höherwertigen Bits abgeschnitten und die niederwertigen bleiben unverändert. Bei
der Konvertierung eines kürzeren „integer“-Typs in einen längeren wird das
Vorzeichen erweitert oder die zusätzlichen Bits des neuen Werts auf Null gesetzt.
Das hängt davon ab, ob der kürzere Typ signed oder unsigned ist.
Typ
char
unsigned char
signed char
short
unsigned short
enum
Konvertierung in
int
int
int
int
unsigned int
int
Verfahren
Null- oder Vorzeichen erweitert
Höherwetige Bits immer Null
Vorzeichen erweitert
unverändert, Vorzeichen erweitert
unverändert, nullerweitert
unverändert
Abb.2.1-6: Verfahrensweise bei arithmetischen Standarkonvertierungen
Eine explizite Typenkonvertierung (type cast)85 wird durch (typ) ausdruck
86veranlaßt. In C++ ist auch typ (ausdruck) möglich87, z.B.:
int i = 5;
double d;
d = (double) i; bzw. d = double (i)
85
Die explizite Konvertierung bezeichnet man als cast
Aus der Programmiersprache C übernommene Schreibweise
87 Funktionsschreibweise
86
92
Die Programmiersprache C++
Die Funktionsschreibweise kann nur für Typen verwendet werden, die einen Namen
haben. Soll bspw. ein ganzzahliger Wert in eine Adresse konvertiert werden, dann
muß die Konvertierung in "cast"-Schreibweise angegeben werden, z.B.:
char *z = (char *) 0777;
Die aus C übernommene Schreibweise ist auch für komplexe Datentypen möglich.
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
Zusätzlich gibt es „lange“ Zeichen (wide characters) vom Typ wchar_t. „Wide
Characters“ sind für Zeichensätze gedacht, bei denen ein Byte nicht zur Darstellung
eines Zeichens ausreicht.
Neben diesen „Ein-Zeichen-Konstanten“ sind auch Konstanten mit 2 Zeichen möglich (‘ab’,’XY’). Diese
stellen „integer“-Werte dar. Man sollte auf sie jedoch verzichten und in diesen Fällen eine hexadezimale
Schreibweise bevorzugen.
88
94
Die Programmiersprache C++
c) Reelle Konstanten (Gleitpunkt-)
Sie sind vom Typ double und können sowohl in üblicher Gleitpunktdarstellung als
auch in exponentieller Form angegeben werden, z.B.:
Darstellung
13.
-.4
-13.0e-1
-04.E+2
13E-2
1.311E1
Wert
13,0
-0,4
-1,3
-400,0
0,13
13,11
Auch Gleitpunktzahlen können durch Anhängen eines Suffix zu einem bestimmten
Typ gemacht werden. So bedeutet f (oder F) float und l (oder L) long double.
Bsp.: Demonstration von Länge und Typ einzelner Gleitpunktkonstanten
#include <iostream.h>
void main()
{
cout << "\n" << sizeof(13.);
cout << "\n" << sizeof(13.f);
cout << "\n" << sizeof(13.l);
}
Zum Beschreiben einer Gleitpunktzahl ist der Dezimalpunkt unbedingt erforderlich,
z.B. 1311. anstatt 1311 (wird als „int“ betrachtet.
d) Zeichenketten (strings)
Eine Zeichenkette ist eine Zeichenfolge, die durch doppelte Hochkommata
eingeschlossen ist, z.B.:
"Hallo Semester I5T"
Jede Zeichenkette enthält zusätzlich das Zeichen '\0', das das Ende der
Zeichenkette
bestimmt.
Außerdem
kann
jedes C++-Zeichenkettenliteral
Kontrollzeichen im Text enthalten, die jeweils durch einen Backslash (\) eingeleitet
sind. Die Länge einer Zeichenkette kann nach ANSI-Norm bis zu 509 Zeichen
betragen. Die meisten Compiler erlauben jedoch größere Werte (z.B. 2048 Bytes).
e) const
An diesem Schlüsselwort erkennt C++ Konstanten, z.B.
const
const
const
const
int pufferGroesse = 512;
int zaehler
= 50;
double pi;
// Fehler, nicht initialisiert
float pi
= 3.14159;
So definierte symbolische Konstante sind Variable, die nicht verändert werden
dürfen. "zaehler = 50;" führt bspw. auf einen Fehler, da "zaehler" eine
Konstante ist.
95
Die Programmiersprache C++
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;
}
void benutzevolatile(void)
{
volatile int k;
cout << "Zaehlen mit einer Volatilevariablen " << endl;
for (k = 1; k <= 100; k++) cout << k;
89
90
PR22101.CPP
PR22104.CPP
96
Die Programmiersprache C++
}
2.2.2 Zeiger (pointer types)
Definition
Zeigertypen sind Datentypen zur Manipulation von Objektadressen. Ein Zeigertyp
entsteht, indem vor dem deklarierten Namen ein * (Stern)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"
Zuweisung
99
Die Programmiersprache C++
=
zgr = x
Dereferenzieren
*
var = *zgr
weist den Zeigerwert "x" der Variablen "var" zu.
weist das Datenelement vom Typ T, das durch "zgr"
refernziert wird, der Variablen "var" zu
Dynamische Speicherbelegung und Freigabe
new
zgr = new T erzeugt dynamisch Speicher für ein Datenelement vom
Typ T und weist die Adresse dieses Speichers "zgr"
zu
delete delete zgr löscht den dynamischen Speicherbereich der durch
die
Adresse "zgr" lokalisiert wird.
Arithmetik
+
x + i
zeigt auf das Datenelement, das "i" Datenelemente
rechts vom durch "x" adressierten Element liegt.
x - i
zeigt auf das Datenelement, das "i" Datenelemente
links vom durch "x" adressierten Element liegt.
x - y
gibt die Anzahl der Datenelemente vom Basistyp
zurück, die zwischen den beiden Zeigern liegen
Vergleichen (relationale Operationen)
Die 6 standardmäßig vorliegenden relationalen Operatoren können auf
Zeiger angewendet werden. Vergleiche beziehen sich auf ganzzahlige Werte
ohne Vorzeichen ("unsigned int")
Abb. 2.2-10: Operationen zu Zeigerausdrücken
Zeigerkonvertierungen
Zeigertypen können im Rahmen der Typumwandlung in andere Zeiger konvertiert
werden, z.B.:
char* zeichenkette;
int* iz;
zeichenkette = (char*) iz;
Allgemein kann man das so ausdrücken: (T*) konvertiert einen Zeiger in den Typ
Zeiger auf T.
Dynamische Speicherbelegung und Freigabe
Da Zeiger selbst Objekte sind, können Zeiger auch auf Zeiger zeigen. Felder
(arrays), Strukturen und Varianten, Konstante und Klassen sind Objekte, auf die
gewöhnlich gezeigt wird. Speicherplatz für derartige Objekte mit dynamischer Dauer
kann man mit dem C++-Operator new besorgen und mit delete wieder freigeben.
Kann kein Speicherplatz bereitgestellt werden, dann wird NULL zurückgeliefert.
new new_Argumente Typname Initialisierer
new new_Argumente (Typname) Initialisierer
new_Argumente läßt sich zur Übergabe weiterer Argumente an new verwenden.
Dazu ist allerdings eine überladene Version von new erforderlich, die mit den
optionalen Argumenten übereinstimmt. Initialisierer wird (falls angegeben) zur
Initialisierung der Reservierung von Speicherplatz verwendet. new versucht ein
Objekt des Typs Typname durch Reservierung von sizeof(Type) Bytes im freien
100
Die Programmiersprache C++
Speicher (dem Heap) zu erstellen. Der Operator new[] dient zur Erzeugung von CArrays.
Der zurückgegebenen Zeiger hat immer den Typ „Zeiger auf Type“. Das neue Objekt
wird von seiner Erzeugung bis zu dem Moment gespeichert, in dem der Operator
delete es löscht.
delete Typumwandlungsausdruck
delete [] Typumwandlungsausdruck
Bsp.:
char* z = new char[100];
// Platz für 100 Zeichen
int* i = new int;
// Platz für eine Zahl
/* Die Anforderung von Speicherplatz erfolgt durch Angabe
des Objekttyps, das System liefert daraufhin die Startadresse
des Speicherplatzes zurück */
int* dynfeld;
int groesse;
/* groesse wird belegt */
.........
dynfeld = new int[groesse];
/* "new int[groesse]" liefert einen Zeiger auf einen
dem Heap entnommenen Speicherblock der Groesse
"groesse * sizeof(int)"
*/
Mit delete wird Speicherplatz freigegeben, z.B.
delete[100] z;
delete i;
delete [] dynfeld;
/* Die Freigabe von Speicherplatz geschieht durch Angabe
der Speicherplatz-adresse und evtl. der Anzahl der Objekte,
die gelöscht werden sollen */
void-Pointer
Mit dem speziellen Typ void* werden Zeiger definiert, die auf Objekte eines zur
Compilierungszeit unbekannten Typs verweisen. C++ muß dann zur Ablaufzeit
wissen, welchen Datentyp ein void-Zeiger adressiert. Das erfordert einen "pointer
type cast"-Ausdruck, der temporär einen Zeiger zu einem Datentyp bindet.
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';
puffer[3] = '\0';
cout << "Adresse von Puffer = ";
ausgabeZeiger(&puffer);
94
vgl. PR22202.CPP
101
Die Programmiersprache C++
cout << "Daten im Puffer = ";
cout << (char *) zpuffer;
}
void ausgabeZeiger(void *zgr)
{
cout << hex << zgr << endl;
}
Zeiger auf Konstante bzw. konstante Zeiger
const-Objekte können über Zeiger nicht indirekt verändert werden, z.B.:
const double pi = 3.14;
double* zgr = &pi:
/* ist verboten, da &pi den typ "Zeiger auf eine Konstante besitzt */
Der Fehler kann behoben werden, z.B.:
const double* zgr = π
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 = π
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 = π // verboten: zgr ist konstant
In
char* const kz = "sepp";
kz[0]
= 'd';
kz
= "depp";
// konstanter Zeiger
// zulaessig
// Fehler
ist "kz" eine Konstante.
In
const char* zk = "sepp";
zk[0]
= 'd';
zk
= "depp";
// Zeiger auf eine Konstante
// Fehler
// zulaessig
ist "zk" keine Konstante sondern ein Verweis auf eine Konstante. Das Objekt, auf
das "zk" verweist, kann nicht verändert werden, aber "zk" darf verändert werden.
Soll ein konstanter Zeiger auf ein konstantes Objekt definiert werden, gelten
folgende Definitionen:
const char *const kzk = "sepp";
// Auch erlaubt: const char* const kzk = "sepp";
kzk[0]
= 'd';
// Fehler
kzk
= "depp";
// Fehler
102
Die Programmiersprache C++
Zeiger auf Funktionen
Ist die Adresse einer Funktion bestimmt, dann kannn über diesen "Pointer" die
Funktion aufgerufen werden. Der Typ „ist Zeiger auf eine Funktion“ besitzt folgende
syntaktische Form:
rückgabewertTyp (*funk) (argumentenTyp1, argumentenTyp2, ...)
Rechnungen mit Zeigern auf Funktionen sind nicht zulässig.
Bsp.: Sortieren der Elemente in Arbeitsspeicherfeldern
1. Zum Sortieren muß eine passende Vergleichsfunktion gefunden werden. Die
Zuordnung der passenden Vergleichsfunktion erfolgt zweckmäßig über eine
Funktions-Pointer. Dafür wird ein Typname (mit Argumentenliste) deklariert (zur
Erleichterung der Beschreibung der etwas ungewöhnlichen Syntax).
typedef int (*VRGL) (int,int);
void tauschen(int* x, int i, int j)
{
int zwischen = x[i];
x[i] = x[j];
x[j] = zwischen;
}
int vrgl1(int z1, int z2)
{
return z1 - z2;
}
int vrgl2(int z1, int z2)
{
return z2 - z1;
}
void bsort(int* x, int n, VRGL vrgl)
// void bsort(int*x, int n, int (*)(int,int) vrgl)
{
// Sortieren durch Austauschen
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if ((*vrgl)(x[i],x[j]) < 0)
tauschen(x,i,j);
}
2. Zum Lieferumfang zahlreicher C++-Compiler gehört eine Bibliotheksfunktion
void qsort(void* zgrVornF, size_t anz, size_t bytes,
int (*fcompare)(const void* elem1, const void* elem2);
zgrVornF: Zeiger auf das erste Element
anz: Das Feld besteht aus anz Elementen
bytes: Größe der Elemente
Wie sortiert wird, bestimmt die Funktion fcompare(). Sie erwartet zwei Zeiger auf
ein beliebiges Element als Argument und muß zurückgeben:
103
Die Programmiersprache C++
0,
falls *elem1 == *elem2
< 0, falls *elem1 < *elem2
> 0, falls *elem1 > *elem2
Der Typ size_t ist ein typedef für unsigned int.
qsort() wird mit der Funktionsprozedur sortieren() aufgerufen, die ein
Zeichenkettenfeld (char* [ ]) zum Sortieren der Zeichenkettenelemente übergibt.
#include <stdlib.h>
#include <string.h>
void sortieren(char* x[], int n)
{
int vergleiche(const char*[], const char*[]);
qsort((void*) x, (size_t) n, (size_t) sizeof(x[0]),
(int (*) (const void*, const void*)) vergleiche);
/* explizite Typumwandlung. In qsort() sind Zeiger auf void
verlangt */
}
int vergleiche(const char*elem1[], const char* elem2[])
{
return(strcmp(*elem1,*elem2));
}
2.2.3 Vektoren (Arrays)
2.2.3.1 C-Arrays
Definition und Initialisieren
Vektoren bestehen aus einem zusammenhängenden Feld von einzelnen Variablen
gleichen Namens und gleichen Typs, die über einen ganzzahligen
Index
angesprochen werden können. Bspw. ist int feld[10] eine Vektordefintion. Die
von [ .. ] eingeschlossene Ganzzahl bestimmt die Anzahl der Vektorelemente. Hier
sind 10 Variable feld[0], feld[1], ... , feld[9] vom Typ int verfügbar.
Die Elemente werden vom Anfangsindex 0 aus durchgehend indiziert.
Vektorelemente werden dadurch ausgewählt, daß dem Vektornamen, in [..]
eingeschlossen, ein Ausdruck folgt, der eine ganze Zahl liefert. Vektorelemente
können vordefiniert werden, z.B.:
int feld[10] = {1,2,3,4,5,6,7,8,9,10};
Bei reinen Deklarationen kann die Ausdehnungsangabe entfallen, z.B.:
extern double v[];
Dies gilt auch für den Fall der Definition mit Initialisierung durch Angabe einer
Werteliste, z.B.:
double x[3] = {1,0,0};
double y[3] = {0,1,0};
double z[] = {0,0,1};
104
Die Programmiersprache C++
Parameterübergabe
Vektoren (arrays) werden in C++ immer mit call by reference übergeben, z.B.
void ausgabe(int[])
wird vom Compiler so behandelt: void ausgabe(int *)
Die Größe des Felds spielt bei der Deklaration der formalen Parameter keine Rolle.
Die folgenden drei Deklarationen sind äquivalent:
void ausgabe(int *)
void ausgabe(int [])
void ausgabe(int[10])
C-Arrays und sizeof
C_Arrays sind keine Objekte (Instanzen) und besitzen auch keine Methoden. Die
Größe eines C-Array muß vorher bekannt sein oder mit sizeof ermittelt ermittelt
werden, z.B.:
const int anzahl = 5;
int tablelle[10];
for (int i = 0; i < anzahl; i++)
cout << i << ": " << tabelle[i] << endl;
// bzw.
for (int i = 0; i < sizeof(tabelle)/sizeof(tabelle[0]); i++)
cout << i << ": " << tabelle[i] << endl;
sizeof() gibt den Platzbedarf des in Klammern stehenden Objekts in Bytes
zurück. Die Anzahl ergibt sich durch Division des Speicherbedarfs für das ganze
Feld durch den Platzbedarf für ein einzelnes Element. Falls der Datentyp eindeutig
bekannt ist, kann anstatt sizeof(tabelle[0]) auch sizeof(int) geschrieben
werden.
C-Zeichenketten
Eine C-Zeichenkette (string) ist ein Spezialfall eines Arrays. C++ Zeichenketten kann
man so
char s[] = {'s','t','r','i','n','g','\0'}
oder einfacher so
char s[] = "string".
definieren. Der Übersetzer berechnet die
Wortlänge anhand der Zahl der
Initialisierungen. Bei s wird sie hier auf 7 festgelegt, da ein "string" immer mit dem
"\0"-Zeichen endet. Derartige Zeichenketten sind die einzige Form von C-Arrays, die
auf einmal ausgegeben werden können, z.B.:
cout << s;
105
Die Programmiersprache C++
Ebenso ist es möglich, eine Zeichenkette über >> und cin auf einmal zu füllen:
cin >> s;
Mit der Benutzung der binären Null als Endemarkierung kann man theoretisch
beliebig lange Zeichenketten erzeugen. Mit
s[0] = '\0';
ist die Länge der Zeichenkette auf Null gekürzt, also gelöscht. Zur Bearbeitung von
Zeichenketten befinden sich in der Header-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)
-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.
95
96
Header <cstring>
Die Relationen < bzw > bezeichnen hier die lexikalische Reihenfolge
106
Die Programmiersprache C++
int strcmp(const char* a, const char* b);
int strncmp(const char* a, const char* b, size_t n);
int memcmp(const void* a, const void* b, size_t n);
strchr() und memchr() geben einen Zeiger auf das erste, strrchr() einen
Zeiger auf das letzte Auftreten des Zeichen z zurück. Wird z in s nicht gefunden,
wird 0 zurückgegeben.
char* strchr(const char* s, int z);
void* memchr(const void* s, int z, size_t n);
char* strrchr(const char* s, int z);
strpbrk() liefert einen Zeiger auf das erste Auftreten irgendeines Zeichen aus der
Zeichenkette p in der Zeichenkette s. Bei Mißerfolg wird 0 zurückgegeben.
char* strstr(const char* s, const char* p);
strstr() liefert einen Zeiger auf das erste Auftreten der Zeichenkette p als
Teilstring der Zeichenkette s. Enthält s den String p nicht, so ist das Ergebnis 0.
char* strstr(const char* s, const char* p);
char-Arrays
Genau wie bei C-Arrays wird für char-Arrays Speicherplatz zur Kompilierungszeit
reserviert. „char“-Arrays können auch keine L-Werte sein. Wie bei Strings (char *)
nimmt der Ausgabeoperator << an, daß ein char-Array mit ‘\0‘ abschließt. Die
Initialisierung kann wie bei Strings geschehen oder wie bei C-Arrays vorgenommen
werden.
Zeiger und Feldnamen
Feldnamen sind konstante Zeiger auf das 1. Feldelement, z.B.: &s[0]. Eine
Dereferenzierung ermöglicht den Zugriff. So kann
*s
*(s+1)
geschrieben werden anstatt s[0]
geschrieben werden anstatt s[1].
Bsp.: Gegeben ist ein Vektor, der Ganzzahlen aufnehmen kann.
int x[5];
Der Zeiger int *zx; zeigt nach der Zuweisung zx = &x[0]; auf das 1. Element
des Vektors. "zx + 1" zeigt dann auf x[1]. Das Inkrementieren eines Zeigers um
Eins bewirkt, daß der Zeiger auf das nächste Objekt dieses Typs zeigt. Statt x[1]
kann man auch *(zx + 1) schreiben. Da in C++ der Name eines Vektors
gleichbedeutend mit einem Zeiger auf seinen Anfang ist, läßt sich zx = &x[0];
auch einfach durch zx = x;ersetzen. Allerdings besteht ein Unterschied: "zx" ist
eine Zeigervariable, x eine Zeigerkonstante. So ist "zx + 1", "zx + 3" erlaubt. Dies
ist aber nur für "x" nicht erlaubt, "(*x)++" ist zur Inkrementierung von x[0] um 1
erlaubt.
107
Die Programmiersprache C++
x[0]
zx
x[1]
x[2]
zx+2
x [3]
x[4]
zx+4
Abb. 2.2-3: Zeigerarithmetik
Elementweises Kopieren von Vektoren
Felder (C-Arrays) können nur elementweise in C++ kopiert werden. So ist
char t[7];
t = s;
falsch. Der Compiler kennt diesen Fehler jedoch nicht, denn er interpretiert diese
Anweisung als „Kopieren einer Zeigervariablen“. Da es keine Zuweisungen eines
Felds an ein anderes gibt, kann auch nicht über die return()-Anweisung ein Feld
als Funktionsrückgabewert erscheinen.
Mehrdimensionale Felder
Sie werden folgendermaßen definiert:
int matrix[10][10]
Diese Definition erzeugt die Elemente matrix[i][j] für 0 <= i , j <= 9. Die
Grenzen von Feldern sind konstant und werden zur Übersetzungszeit festgelegt.
Bsp.:
char v[2][5] = {'a','b','c','d','e',
'1','2','3','4','5'}
// erster Vektor
// zweiter Vektor
kann auch so angegeben werden
char v[2][5] = {{'a','b','c','d','e'},
{'1','2','3','4','5'}}
108
// erster Vektor
// zweiter Vektor
Die Programmiersprache C++
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"};
[0]
1
. Ze
i l e \0
[1]
2
. Ze
i l e \0
[2]
3
. Ze
i l
e \0
109
Die Programmiersprache C++
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
- 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++)
97
PR22307.CPP
110
Die Programmiersprache C++
{
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);
}
Dynamisch erzeugte mehrdimensionale Arrays
Mehrdimensionale Felder werden durch Deklaration von Vektoren des Typs array
aufgebaut.
[0]
[1]
[2]
[...]
[n-1]
[0]
...
.....
.....
...
.....
...
.....
111
Die Programmiersprache C++
[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
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
}
112
Die Programmiersprache C++
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
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;
98
PR22310.CPP
113
Die Programmiersprache C++
// 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.
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'.
-
99
argument counter
argument value
100
114
Die Programmiersprache C++
die Variable "arge" Informationen über die Programmumgebung. Zu den Zeichenketten, die
üblicherweise in der Programmungebung enthalten sind, gehören:
-- der Suchpfad, der zur Lokalisierung von Programmen dient (PATH)
-- das Login-Directory (HOME)
-- der Name des Terminals
-
Diese Daten werden beim Start des Programms übergeben:
Bsp.:
1. Übergabe von Argumenten in der Kommandozeile von Programmen 101
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.
[0]
a
:
\
p
r
[1]
a
:
\
p
r
[2]
a
:
\
p
r
2
2
2
2
2
2
1
1
1 1
.
C P
1
.
L S T \0
3
3
3
1
\0
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);
*/
101
PR22311.CPP
115
P \0
Die Programmiersprache C++
// 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;
/* 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*/
/* 2*/
/* 3*/
/* 4*/
/* 5*/
/* 6*/
/* 7*/
/* 8*/
/* 9*/
/*10*/
/*11*/
/*12*/
#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);
}
116
Die Programmiersprache C++
..................................................................
..................................................................
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;
}
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.:
-
-
102
103
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.
vgl. 3.4
vgl. 5.2.8
117
Die Programmiersprache C++
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);
//
//
//
//
//
//
Eingabe
Ausgabe
Bubble-Sort
Sortieren durch Auswaehlen
Sortieren durch Einfuegen
Tauschen
int main()
{
int n;
// int x[20];
// vector<int> x(20);
vector<int> x; // anfaengliche Groesse ist 0
eingabe(x,n);
cout << "Ausgabe unsortiert:\n";
ausgabe(x,n);
cout << "Ausgabe
sortiert:\n";
bsort(x,n);
// einfuegesortieren(x,n);
// auswahlsortieren(x,n);
ausgabe(x,n);
return 0;
// kann weggelassen werden
}
// Eingabe
void eingabe(vector<int>& x, int& n)
{
int wert;
// cout << "Anzahl Elelemente?\n";
// cin
>> n;
cout << "Eingabe von Werten\n";
do
{
cout << "Wert (0 = Ende der Eingabe):";
cin >> wert;
if (wert != 0)
x.push_back(wert);
// Wert anhaengen
} while (wert != 0);
n = x.size();
}
// Ausgabe
void ausgabe(vector<int> x, int n)
{
const int zeilenLaenge = 12; // Anzahl der Elemente je Zeile
cout << "(" << n << ") <";
for (int i = 0; i < n; ++i)
{
if (i % zeilenLaenge == 0 && i) // ;
cout << "\n\t";
cout << x[i];
// Trennen durch , Ausnahme letztes Element
if (i % zeilenLaenge != zeilenLaenge - 1 && i != n - 1)
cout << ", ";
}
cout << ">\n";
}
// Sortieren
118
Die Programmiersprache C++
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);
}
}
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:
119
Die Programmiersprache C++
<<
+
==
<=
>=
>>
+=
!=
<
>
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.
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.
120
Die Programmiersprache C++
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).
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.
121
Die Programmiersprache C++
string substr(size_type pos = 0, size_type n = npos) const
// gibt den Substring zurück, der ab pos beginnt. Die Anzahl der Zeichen im Substring wird durch
// das Ende des String bestimmt, kann aber nicht größer als n werden.
size_type find(const string& s, size_type pos = 0) const
// gibt die Position zurück, an der der Substring s gefunden wird, anderenfalls wird string::npos
// zurückgegeben.
int compare(const string& s) const
// vergleicht zeichenweise die Strings *this und s. Es wird 0 zurückgegeben, wenn keinerlei
// Unterschied festgestellt wird. Falls das erste unterschiedliche Zeichen von *this kleiner als das
// entsprechende in s ist, wird eine negative Zahl zurückgegeben, andernfalls eine positive Zahl.
// Falls bei unterschiedlicher Länge bis zum Ende eines der Strings keine verschiedenen Zeichen
// gefunden werden, wird eine negative Zahl zurückgegeben, falls size() < s.size() ist,
// andernfalls eine positive Zahl.
int compare(size_type pos,size_type n,const string& s) const
// gibt string(*this,pos,n).compare(s) zurück. Es werden nur die Zeichen ab Position pos
// 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++)
104
PR22230.CPP
122
Die Programmiersprache C++
cout << einString[i];
cout << endl;
// Zeichenweise Ausgabe eines String mit Indexpruefung
for (size_t i = 0; i < einString.size(); i++)
cout << einString.at(i);
cout << endl;
/* Der Versuch einString.at(i) mit i >= einString.size()
abzufragen, fuehrt zum Programmabbruch und Fehlermeldung
*/
// Kopie eines String erzeugen
string eineStringkopie(einString);
cout << eineStringkopie << endl;
// Kopie durch Zuweisung
string zuweisungsKopie("Kopie durch Zuweisung");
eineStringkopie = zuweisungsKopie;
cout << eineStringkopie << endl;
// Zuweisung einer Zeichenkette
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& s)
{
cout << "Name: "
<< s.name
<< "\n";
105
Nicht objektorientierter Aspekt des struct-Datentyps
123
Die Programmiersprache C++
cout
cout
cout
cout
cout
<<
<<
<<
<<
<<
"Vorname: "
"Strasse: "
"Haus-Nr.: "
"PLZ: "
"Ort: "
<<
<<
<<
<<
<<
s.vorname
s.strasse
s.hausnr
s.hausnr
s.hausnr
<<
<<
<<
<<
<<
"\n";
"\n";
"\n";
"\n";
"\n";
}
Strukturen können auch geschachtelt sein, z.B.:
struct datum {int tag, monat, jahr};
struct studentenSatz
{
char name[20];
char vorname[20];
char strasse[20];
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
{
124
Die Programmiersprache C++
double xk;
double yk;
};
void tauschen(struct koord* werte)
{
double h;
h = werte->xk;
werte->xk = werte->yk;
werte->yk = h;
}
void main()
{
struct koord position = {13.11,19.40};
cout << "\nVorher: " << position.xk << " " << position.yk;
tauschen(&position);
cout << "\nNachher: " << position.xk << " "
<< position.yk;
}
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];
};
106
PR22403.CPP
125
Die Programmiersprache C++
void main()
{
char* dateiName = "a:ausgD.txt";
int belegung = 100;
int n;
// Anzahl der aktuellen Saetze
eintrag** elemente;
elemente = new eintrag*[belegung];
for (int i = 0; i < belegung; i++)
elemente[i] = new eintrag;
short menue(void), wahl;
void aufnehmenSaetze(eintrag**, int&, char*);
void ausgebenSaetze(eintrag**, int);
void sortierenSaetze(eintrag**, int);
void ausgebenDatei(eintrag**, int, char*);
void eingebenDatei(char*);
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
{
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())
{
126
Die Programmiersprache C++
zelem = new eintrag;
if ((zelem = new eintrag) == 0)
{
cerr << "\n\t Nicht genuegend Speicherplatz vorhanden";
break;
}
eingabe >> zelem->name;
// cout << zelem->name;
eingabe >> zelem->vorname;
// cout << zelem->vorname;
elemente[zaehler++] = zelem;
}
eingabe.close();
}
*/
cout << "\n\tAnzahl der einzugebenden Saetze: ";
cin >> n;
cout << "\tEingabe von " << n << " Datensaetzen\n";
for (int i = 0; i < n; i++)
{
zelem = new eintrag;
cout << "\tName: ";
cin >> zelem->name;
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())
{
127
Die Programmiersprache C++
eingabe
cout <<
eingabe
cout <<
>> ausgabeSatz.name;
"\n\t" << ausgabeSatz.name << ", ";
>> ausgabeSatz.vorname;
ausgabeSatz.vorname;
}
eingabe.close();
}
Die Dateien, mit denen bisher gerbeitet wurde, waren Textdateien. Diese Dateien
sind aus Datenzeilen aufgebaut, die mit den Zeichen '\n' abschließen. Beim Lesen
wird dieses Zeichen in die Folge CR+LF (0xD+0xA) umgewandelt. Beim Speichern
wird daraus wieder der Zeilenvorschub '\n'.
Komplizierte Datenstrukturen werden binär (ios::binary) gespeichert. Im
vorliegenden Fall ist dann die Ausgabe bzw. Eingabe in bzw. aus einer Datei so
möglich:
void ausgebenDatei(eintrag** elemente, int n, char* dateiName)
{
struct eintrag ausgabeSatz;
ofstream ausgabe(dateiName,ios::out | ios::binary);
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 {
128
Die Programmiersprache C++
int i;
float f;
} v;
Es kann nur auf "i" oder "f" zugegriffen werden, z.B.:
v.i = 17;
cout << "i = " << i << "\n";
v.f = 3.14;
cout << "f = " << v.f << "\n";
cout << "i = " << v.i << "\n"; //Ausgabe: Mist
Alle Komponenten beginnen an der gleichen Stelle. Die Länge einer union ist gleich
der Länge der längsten Komponente.
129
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:
klassenobjekt.komponentenname
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
130
Die Programmiersprache C++
zeiger_auf_klassenobjekt->komponentenname
klassenname::komponentenname
-
-
Der Punktoperator „.“ wird benutzt, falls der Programmierer Zugriff auf ein
Datenelement oder eine Elementfunktion eines speziellen Klassenobjekts
wünscht.
Der Pfeil-Operator „->“ wird benutzt, falls der Programmierer Zugriff auf ein
spezielles Klassenobjekt über einen Zeiger wünscht
Klassen besitzen einen öffentlichen (public) und einen versteckten (private) Bereich.
Versteckte Elemente (protected) sind nur den in der Klasse aufgeführten
Funktionen zugänglich. Programmteile außerhalb der Klasse dürfen nur auf den mit
public als öffentlich deklarierten Teil einer Klasse zugreifen.
Eine mit struct definierte Klasse ist eine Klasse, in der alle Elemente gemäß
Voreinstellung public sind
Die Elemente einer mit union definierten Klasse sind public. Dies kann nicht
geändert werden.
Die Elemente einer mit class definierten Klasse sind gemäß Voreinstellung private.
Die Zugriffsebenen können verändert werden.
Datenelemente und Elementfunktionen
Datenelemente einer Klasse werden genau wie die Elemente einer Struktur
angegeben. Eine Initialisierung der Datenelemente ist aber nicht erlaubt. Deshalb
dürfen Datenelemente auch nicht mit const deklariert sein.
Funktionen einer Klasse sind innerhalb der Klassendefinition deklariert. Wird die
Funktion auch innerhalb der Klassendefinition definiert, dann ist diese Funktion
inline. Elementfunktionen von Klassen unterscheiden sich von den gewöhnlichen
Funktionen:
-
Der Gültigkeitsbereich einer Klassenfunktion ist auf die Klasse beschränkt (class scope).
Gewöhnliche Funktionen gelten in der ganzen Datei, in der sie definiert sind
Eine Klassendefinition kann automatisch auf alle Datenelemente der Klasse zugreifen.
Gewöhnliche Funktionen können nur auf die als "public" definierten Elemente einer Klasse
zugreifen.
Die Funktionen einer Klasse haben die Aufgabe, alle Manipulationen an den Daten
dieser Klasse vorzunehmen.
Der „this“-Zeiger
Jeder (Element-) Funktion wird ein Zeiger auf das Element, für das die Funktion
aufgerufen wurde, als zusätzlicher (verborgener) Parameter übergeben. Dieser
Zeiger heißt this, ist automatisch vom Compiler) deklariert und mit der Adresse der
jeweiligen Instanz (zum Zeitpunkt des Aufrufs der Elementfunktion) besetzt. "this" ist
als *const deklariert, sein Wert kann nicht verändert werden. Der Wert des Objekts
(*this), auf den this zeigt, kann allerdings verändert werden.
131
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
cerr << "Fehler in push() : Stapel ist voll! \n";
return *this;
108
PR31201.CPP
132
Die Programmiersprache C++
/* Das Schluesselwort this entspricht einem in Komponentenfunktionen
immer vorhandenen Zeiger auf die Instanz, die Ausdruecke nachf und
inhalt sind daher zu this->inhalt und this->nachf aequivalent.
*this repraesentiert den vorliegenden Stapel, die Referenz
darauf ermittelt der Uebersetzer automatisch */
}
intStapel& intStapel::pop() { nachf--; return *this; }
int intStapel::top() { return inhalt[nachf - 1]; }
int intStapel::stapeltiefe() { return nachf; }
int intStapel::istleer() { return nachf == 0; }
// Ein kleines Testprogramm
main()
{
intStapel s; // Instanz
s.push(7); s.push(11); s.push(21);
while ( !s.istleer() )
{
cout << s.top() << " "; s.pop();
}
return 0;
}
2. Definition der Funktion innerhalb von Klassen
Elementfunktionen können auch innerhalb der Klassendefinition definiert werden,
womit sie automatisch als "inline" vereinbart gelten
#include <iostream.h>
const int stapelgroesse = 100;
class intStapel
{
private:
int inhalt[stapelgroesse];
int nachf;
public:
intStapel() : nachf(0)
intStapel(int e) : nachf(0)
intStapel& push(int w)
intStapel& pop()
int top()
int stapeltiefe()
int istleer()
};
// konstante Klassenvariable?
{ }
{ push(e); }
{ inhalt[nachf++] = w; return *this; }
{ nachf--; return *this; }
{ return inhalt[nachf - 1]; }
{ return nachf; }
{return nachf == 0; }
133
Die Programmiersprache C++
3.1.2 Konstruktoren, Klassenvariable und Destruktoren
Konstruktoren
Das einführende Beispiel ist falsch. Es fehlt die Initialisierung. Die naive Lösung zur
Initialisierung, z.B.
intStapel s; s.nachf = 0;
scheitert (auf "nachf" darf als private Komponente außerhalb einer Stapel-Methode
nicht zugegriffen werden).
Konstruktoren dienen zur Initialisierung von Klassen-Objekten, haben keinen
Ergebnistyp und sind namensgleich mit ihrer Klasse, z.B.:
intStapel::intStapel() {nachf = 0; }
Über Argumente kann ein Konstruktor verfügen, z.B.:
intStapel::intStapel(int erstesElement)
{
nachf = 0;
push(erstesElement);
/* Bei der Defimition des Stapel wird zugleich das erste
Element gestapelt */
}
Aktiviert wird der Konstruktor über
intStapel s(7);
bzw.
intStapel s = 7;
// Initialisierung, keine Zuweisung
Konstruktoren können überladen werden und Vorgabewerte definieren. Falls
mehrere Konstruktoren vorliegen, bestimmt der Compiler (anhand der
Parameterlisten), welcher Konstruktor zu verwenden ist. Konstruktoren
unterscheiden sich hinsichtlich Anzahl und/oder Typ der Parameter.
Es ist nicht möglich, einen zweiten Konstruktor mit Hilfe eines ersten Konstruktors zu
intialisieren:
intStapel::intStapel(int erstesElement)
{
intStapel(); push(erstesElement); // Fehler
}
Für die Hauptaufgabe eines Konstruktors (Initialisierung einer Instanstanzvariablen)
gibt es eine besondere Syntax, z.B.:
intStapel :: intStapel() : nachf(0)
{ }
intStapel :: intStapel(int e) : nachf(0) { push(e); }
Werden mehrere Komponenten auf diese Weise initialisiert, entspricht die
Reihenfolge der Initialisierung der Reihenfolge der Deklaration, z.B.:
#include <iostream.h>
int f(int i) { cout << i << " "; return i; }
134
Die Programmiersprache C++
class X
{
int a, b, c;
public:
X() : a(f(1)), c(f(2)), b(f(3)) { }
};
main()
{
X x; return 0;
}
führt zur Ausgabe: 1 3 2
Klassenvariable
C++ erlaubt die Definition und Deklaration von Klassenvariablen mit dem
Schlüsselwort static. Allerdings können diese Elemente, z.B. "stapelgroesse",
nicht innerhalb der Klassendefinition initialisiert werden.
#include <iostream.h>
class intStapel
{
private:
static const int stapelgroesse = 100;
.........
// hier verboten!
"stapelgroesse" muß außerhalb der Klasse definiert und bei Bedarf initialisiert
werden.
class intStapel
{
private:
static const int stapelgroesse;
.................................
}
// Deklaration
const int intStapel :: stapelgroesse = 100;
// Definition
Allerdings ist dann "int inhalt[stapelgroesse]" falsch. Ein Ausweg ist:
Verwendung einer Zeigerkomponente anstatt einer "array-"Komponenten.
class intStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
public:
intStapel()
: nachf(0),
inhalt(new int[stapelgroesse]) { }
intStapel(int e) : nachf(0),
inhalt(new int[stapelgroesse]) { push(e); }
...............................................................
};
const int intStapel :: stapelgroesse = 100;
Destruktoren
135
Die Programmiersprache C++
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.
136
Die Programmiersprache C++
t
s
1
2
inhalt
nachf
3
0
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
137
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;
}
138
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);
139
}
Die Programmiersprache C++
3.1.6 Statische Komponentenfunktionen
In C++ können Komponentenfunktionen "static" vereinbart sein. Diese
Klassenmethoden verlieren aber das implizite Argument "this" und dürfen daher nicht
mehr auf Instanzen Bezug nehmen, können dafür aber objektunabhängig aufgerufen
werden.
Bsp.: Die Komponentenfunktion groesseSt() des vorstehenden Beispiels zeigt
keinen Bezug zu einer Stapelinstanz (und ist damit offenbar so etwas wie eine
Klassenmethode):
class intStapel
{
...........................
public:
..........................
static long groesseSt()
};
inline long intStapel :: groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int);
}
Der Aufruf dieser Funktion kann über ein beliebiges Objekt der Klasse intStapel oder
lediglich unter Angabe des Klassennamens erfolgen.
3.1.7 "friend"-Funktionen und "friend"-Klassen
Die Komponentenfunktion "groesseSt()" ermöglicht den Zugriff einer globalen
Funtion gleichen Namens auf Klasseninterna. Alternativ dazu hätte der Funktion
groessSt() auch explizit der Zugriff auf private Klassenkomponenten gewährt
werden können, wenn sie als sogenannte "friend"-Funktion deklariert worden wäre:
class intStapel
{
private:
....................
public:
......................
friend long groesseSt(const intStapel& s);
};
Auch Klassen können als "friend" anderer Klassen deklariert werden.
Häufig wird "operator <<()" als friend vereinbart:
class intStapel
{
private:
....................
public:
....................
friend ostream& operator << (ostream& o, const intStapel& s);
};
ostream& operator << (ostream& o, const intStapel& s)
{
140
Die Programmiersprache C++
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf - 1) && (i > 0)) o << ", ";
o << s.inhalt[i];
}
return o << ">";
}
„Friend“-Funktionen weichen das Prinzip des „information hiding“ auf und sollten
deshalb sparsam verwendet werden. Am häufigsten werden sie zusammen mit dem
Mechanismus des „Overloading“ benutzt.
3.1.8 ADT Stapel
Abstrakte (Daten-) Typen werden über die Angabe der Datenelemente und der
Methoden, die auf diesen Datenelementen operieren, beschrieben. Die Methoden
können eingeteilt werden in:
-
Konstruktoren
Sie liefern neue Elemente (Instanzen) des ADT.
-
Zugriffsfunktionen
Sie bestimmen die Eigenschaften von existierenden Datenelementen des Typs
-
Umsetzungsfunktionen
Sie bilden neue Elemente des ADT (durch Hinzufügen bzw. Entfernen)
Bezogen auf den „ganzzahligen Stapel“ führt das zur folgenden Schnittstellenbeschreibung:
#include <iostream.h>
class intStapel
{
private:
static const int stapelgroesse;
int *inhalt;
int nachf;
void init() { inhalt = new int[stapelgroesse]; nachf = 0; }
public:
intStapel()
{ init(); }
// Konstruktoren
intStapel(int e) { init(); push(e); }
intStapel(const intStapel&);
~intStapel() { delete [] inhalt; }
// Destruktor
intStapel& push(int w)
{ inhalt[nachf++] = w; return *this; }
intStapel& pop()
{ nachf--; return *this; }
int top()
{ return inhalt[nachf - 1]; }
int stapeltiefe()
{ return nachf; }
int istleer()
{ return nachf == 0; }
long groesseSt() const
{ return sizeof(intStapel) + stapelgroesse * sizeof(int); }
intStapel& operator = (const intStapel&);
int operator == (const intStapel&);
friend ostream& operator << (ostream& o, const intStapel& s);
};
const int intStapel :: stapelgroesse = 100;
// Kopierkonstruktor
141
Die Programmiersprache C++
intStapel :: intStapel(const intStapel& s)
{
init();
// Anlegen eines neuen Felds
nachf = s.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = s.inhalt[i];
}
// Operatorfunktion (Stapel mit eigenem Zuweisungsoperator)
intStapel& intStapel :: operator = (const intStapel& r)
{
nachf = r.nachf;
for (int i = 0; i < nachf; i++) inhalt[i] = r.inhalt[i];
return *this;
}
// Operatorfunktion zur Ueberpruefung zweier Stapelobjekte auf Gleichheit
int intStapel :: operator == (const intStapel& r)
{
if (r.nachf != nachf)
return 0;
// unterschiedliche Groessen
if (r.inhalt == inhalt) return 1;
// Fall 1 oder 2
for (int i = 0; i < nachf; i++)
if (inhalt[i] != r.inhalt[i]) return 0; // Fall 3
return 1;
}
// Operator <<()
ostream& operator << (ostream& o, const intStapel& s)
{
o << "<";
for (int i = 0; i < s.nachf; i++)
{
if ((i <= s.nachf - 1) && (i > 0)) o << ", "; o << s.inhalt[i];
}
return o << ">";
}
Anwendung: „Umrechnen von Dezimalzahlen in andere Basisdarstellungen“ 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
142
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;
}
}
143
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.
144
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;
}
Zugriffsfunktionen: pop(), top() und push()
111
PR22306.CPP
145
Die Programmiersprache C++
// 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&)");
}
146
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:
.........................
};
147
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
148
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
149
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.
Grundsätzlich werden Destruktoren immer in umgekehrter Reihenfolge wie die
Konstruktoren aufgerufen, z.B.116:
115
PR22318,CPP
150
Die Programmiersprache C++
#include <iostream.h>
class aKlasse
{
int A;
public:
aKlasse(int Ain)
{
A = Ain;
cout << "Aufruf Konstruktor aKlasse A = " << A << endl;
}
~aKlasse(void)
{ cout << "Destruktor aKlasse" << endl; }
};
class abKlasse : public aKlasse
{
int B;
public:
abKlasse(int Ain, int Bin):aKlasse(Ain)
{
B = Bin;
cout << "Aufruf Konstruktor abKlasse B = " << B << endl;
}
~abKlasse(void)
{ cout << "Destruktor abKlasse" << endl; }
};
class abcKlasse : public abKlasse
{
int C;
public:
abcKlasse(int Ain, int Bin, int Cin):abKlasse(Ain, Bin)
{
C = Cin;
cout << "Aufruf Konstruktor abcKlasse C = " << C << endl;
}
~abcKlasse(void)
{ cout << "Destruktor abcKlasse" << endl; }
};
main()
{
abcKlasse b(10,20,30);
return 0;
}
Der Aufruf von main() ergibt:
Aufruf Konstruktor aKLasse
A = 10
Aufruf Konstruktor abKlasse B = 20
Aufruf Konstruktor abcKlasse C = 30
Destruktor abcKlasse
Destruktor abKlasse
Destruktor aKlasse
3.2.4 Virtuelle Funktionen
116
PR22312.CPP
151
Die Programmiersprache C++
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();
delete z;
return 0;
}
117
PR22308.CPP
152
Die Programmiersprache C++
Wegen der erweiterten Zuweisungskompatibilität kann "z" auf alle Klassen der
Hierarchie zeigen. Je nachdem, auf welches Objekt "z" gerade zeigt, wird die
entsprechende Routine "f()" der Klasse aufgerufen. "z" zeigt auf etwas, das während
des Programms viele verschiedene Gesichter annehmen kann. Man spricht von
"Polymorphie" *) (vom griechischen "poly morphos"), d.h.: Vielgestaltigkeit.
Eine virtuelle Funktion wird normal vererbt. Falls sie in einer Ableitung redefiniert
werden soll, muß sie erst mit exakt identischer Parameterliste angegeben werden.
Eine nicht virtuelle Funktion kann in einer Ableitung virtuell deklariert werden. Der
umgekehrte Weg ist nicht möglich.
Virtuelle Konstruktoren gibt es nicht, da zur ordnungsgemäßen Konstruktion
Informationen über den genauen Typ benötigt werden. Auch Zeiger auf
Konstruktoren sind nicht erlaubt.
Ist eine Funktion virtuell definiert, dann wird die Zuordnung zwischen Prozeduraufruf
und aufgerufener Prozedur tatsächlich erst zur Laufzeit des Programm hergestellt.
Diesen Vorgang nennt man late binding. Im Gegensatz dazu bedeutet "early
binding", daß die Zuordnung bereits zur Übersetzungszeit vorgenommen wird.
Eine Klasse mit virtuellen Funktionen hat einen größeren Datenbereich, z.B.
#include <iostream.h>
class aKlasse
{
int A;
void f() {}
};
class bKlasse
{
int B;
virtual void f(){}
};
int main()
{
aKlasse a;
bKlasse b;
cout << "Groesse a: " << sizeof(a) << endl;
cout << "Groesse b: " << sizeof(b) << endl;
}
Der Unterschied in der Ausgabe zum vorliegenden Programm ist durch eine
zusätzliche Zeigervariable verursacht, die der Compiler zur Verwaltung der virtuellen
Funktionen der Klasse anlegt. Der Zeiger zeigt auf die virtual function pointer table
(vtbl)", die die für das "late binding" erforderliche Funktionsadresse enthält.
Bsp.: "intStapel" mit virtuellen Methoden
#include <iostream.h>
class intStapel
{
153
Die Programmiersprache C++
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;
...
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 =()
154
Die Programmiersprache C++
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; }
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);
};
118
119
Eine Klasse, die mindestens eine rein virtuelle Funktion besitzt, heißt abstrakte Klasse
PR22310.CPP
155
Die Programmiersprache C++
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.:
Bsp.120: Konstruktoren und Destruktoren bei Mehrfachvererbung
#include <iostream.h>
// Konstruktoren und Destruktoren bei Mehrfachvererbung
class aKlasse
{
120
PR22313.CPP
156
Die Programmiersprache C++
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;
}
Die Ausgabe des vorliegenden Programmbeispiels zeigt:
Aufruf
Aufruf
Aufruf
Aufruf
Aufruf
Konstruktor
Konstruktor
Konstruktor
Konstruktor
Konstruktor
Destruktor
Destruktor
Destruktor
Destruktor
aKLasse
abKlasse
aKlasse
acKlasse
abcdKlasse
A
B
A
C
D
=
=
=
=
=
10
20
10
30
40
abcdKlasse
acKlasse
aKlasse
abKlasse
157
Die Programmiersprache C++
Destruktor 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; }
};
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()
158
Die Programmiersprache C++
{
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;
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;
159
Die Programmiersprache C++
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;
}
// 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;
}
160
Die Programmiersprache C++
3.3 Schablonen
Schablonen (Templates) können als "Meta-Funktionen" aufgefaßt werden, die zur
Übersetzungszeit neue Klassen bzw. neue Funktionen 121 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
161
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) {}
162
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
163
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.
164
Die Programmiersprache C++
ios
istream
ostream
ifstream
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.
165
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:
Bezeichnung:
123
Bedeutung:
vgl. Borland C++ für Windows Version 4.0: Referenzhandbuch
166
Die Programmiersprache C++
skipws
left, right, internal
dec, oct, hex
showbase
showpoint
uppercase
showpos
scientific
fixed
unitbuf
stdio
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.
Bsp.: Ausgabe ganzer Zahlen in verschiedenen Zahlensystemen
Trennzeichen steht für den englischen Begriff „whitespace“ und bedeutet ein beliebiges Zeichen aus der
Menge {‘ ‘,’\t’,’\n’}
124
167
Die Programmiersprache C++
#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.
Manipulatoren
168
Die Programmiersprache C++
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;
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);
169
Die Programmiersprache C++
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()
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
125
Der Manipulator fixed fehlt beim aktuellen GNU C++ Compiler (Version 2.95), deshalb wird er hier
zusätzlich definiert.
170
Die Programmiersprache C++
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.
Bsp.: „Fälschungssichere Ausgabe von Geldbeträgen“127
a) Ausgabe über setf()
#include <iostream.h>
void main()
{
double betrag = 1311.4;
126
127
normalerweise das Leerzeichen
PR34205.CPP
171
Die Programmiersprache C++
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.
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.
putback(char z) erlaubt einem Programm, ein „ungewolltes“ Zeichen in den Strom zurückzuschreiben,
damit es an anderer Stelle gelesen werden kann.
128
172
Die Programmiersprache C++
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
kombinierbare) Werte sind:
ios::out
ios::ate
ios::binary
ios::app
Öffnungsstrategien.
Mögliche
(durch
"oder"
//
//
//
//
Datei fuer Ausgabe oeffnen
nach dem Oeffnen auf Dateiende positionieren
Binaermodus
Hängt Daten an- und schreibt immer an das Dateiende
ios::trunc
// verwirft den Inhalt der Datei, falls sie 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
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.
129
130
implizit, wenn ios::out angegeben ist oder weder ios::ate noch ios::app angegeben wird
Konstruktor
173
Die Programmiersprache C++
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
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:
174
Die Programmiersprache C++
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')
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.
131
Das Trennzeichen wird gelesen, jedoch nicht in den Puffer übernommen.
175
Die Programmiersprache C++
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.
176
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.:
177
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.
178
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
}
179
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
// ...
180
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 Stack 132, 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
vornimmt. Ein catch-Block darf nur direkt nach einem try-Block oder einem anderen
catch-Block stehen.
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
181
Die Programmiersprache C++
Zur Reaktion auf alle möglichen
folgendermaßen gestaltet werden:
Ausnahmen
könnte
der
catch-Block
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
bad_exception
domain_error
Abb.: Exception-Hierarchien
Die Klasse Exception besitzt die Schnittstelle
namespace std
{
class exception
{
private:
// ...
public:
exception() throw();
exception (const exception&) throw();
exception operator = (const exception&) throw();
virtual ~exception throw();
virtual const char* what() const throw();
};
}
throw() nach den Deklarationen bedeutet: Die Klasse wirft selbst keine Exception
aus, da es sonst eine unendliche Folge von Exceptions geben könnte.
182
Die Programmiersprache C++
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
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)();
183
Die Programmiersprache C++
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
}
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);
134
Die Funktionsprototypen und Definitionen dieser Routinen befinden sich in except.h
184
Die Programmiersprache C++
/* 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.
185
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>&);
int operator == (const Stapel<T>&);
135
136
vgl. 3.1.8: ADT-Stapel
vgl. auch 5.2.7
186
Die Programmiersprache C++
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.
187
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
188
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
// Bearbeitungsfunktionen
private:
189
Die Programmiersprache C++
int akt;
public:
void ruecksetzen();
bool beendet();
bool naechstes();
elt &aktuell();
int index();
// Bearbeitungs-Index
// Durchlauf von "vorn" nach "hinten"
//
// 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;
//
}
template <class elt>
schlange<elt>& schlange<elt>::operator+=(elt mrk)
190
Die Programmiersprache C++
{
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);
}
// Attribute
template <class elt> int schlange<elt>::groesse()
{
191
Die Programmiersprache C++
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);
}
template <class elt>
schlange<elt>& schlange<elt>::operator=(schlange<elt>& s)
{
if (this == &s) return *this; //
delete [] elemente;
192
Die Programmiersprache C++
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;
}
193
Die Programmiersprache C++
4.2 Lineare Listen
4.2.1 Einfach gekettete Listen
Grundlagen
Eine Liste ist verkettet gespeichert, falls jeder Knoten die Adresse der
Arbeitsspeicherzelle enthält, in der sein Nachfolger gespeichert ist.
Zeiger auf den Nachfolger-Knoten
Knoten k
Knoten k‘
Abb.:
Aufbau
Eine lineare Liste kann durch Kombination einer „structure“ mit einem „Zeiger“
erreicht werden. Die „structure“-Variable enthält beliebige Komponenten mit Daten
und einer Komponente, die Zeiger auf die Nachfolger der Listenkomponenten
aufnimmt, z.B.
struct eintrag
{
char name[20];
char vorname[15];
eintrag* zNachf;
};
// Zeiger auf den Nachfolger
Die zusätzliche Komponente zNachf ist eine Zeigervariable, die auf eine Instanz
vom Typ „eintrag“ verweist. Für eine Instanz vom Typ „eintrag“ steht noch kein
Speicherplatz zur Verfügung. Dieser wird zur Programmlaufzeit erzeugt, z.B. mit
eintrag* zelem;
zelem = new eintrag;
Der Inhalt des reservierten Speicherbereichs ist vollkommen undefiniert. Man kann
ihn aber z.B. durch Eingabe über die Tastatur füllen:
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
...............................................
So können alle Komponenten bis auf die letzte gefüllt werden. Zunächst sollte man
immer dafür sorgen, daß der Zeiger nicht irgendwohin, sondern auf eine bestimmte
Speicherstelle zeigt.
Durch
194
Die Programmiersprache C++
zElem->zNachf = NULL;
zeigt die „zNachf“-Komponente auf die symbolische Adresse mit dem Wert 0.
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
Abb.: Einfügen des ersten Elements in die linear verkettete Liste (LIFO-Struktur)
Zur Erzeugung eines weiteren Eintrags benötigt man eine weitere Zeigervariable:
eintrag* zAnfang;
Dieser Zeiger erhält den Inhalt von zElem durch zAnfang = zElem;
Auf den zuvor mit new reservierten Speicherbereich zeigen dann zwei Variable. Mit
zElem = new eintrag;
wird ein weiterer Speicherbereich reserviert.. Wieder können die Komponenten
durch Einlesen über die Tastatur mit Werten gefüllt werden. Die letzte Komponente
erhält dieses Mal jedoch nicht Null, sondern die Adresse in zAnfang:
zElem->zNachf = zAnfang;
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
........
Abb.: Einfügen des zweiten Elements in die linear verkettete Liste (LIFO-Struktur)
zAnfang wird anschließend auf das neu erzeugte Element gesetzt:
195
Die Programmiersprache C++
zAnfang = zElem;
Die so erzeugte Konstruktion ist eine lineare Liste. Das, was bisher in einzelnen
Anweisungen durchgeführt wurde, läßt sich in einer Schleife zusammenfassen:
do
{
if ((zelem = new eintrag) == NULL)
{
cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden"; break;
}
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
zElem->zNachf = zAnfang; zAnfang = zElem;
do
{
cout << "\n\tWeiteres Element einfuegen?"; cin >> antwort;
antwort = tolower(antwort);
} while ((antwort != 'j') && (antwort != 'n'));
} while (antwort == 'j');
Nach einigen Schleifendurchläufen ergibt sich folgende Situation:
zelem
zAnfang
char name[20];
char vorname[15];
.......
.......
.......
........
eintrag* zelem;
NULL
........
.....,
.......
Abb.: Linear verkettete Liste (LIFO-Struktur)
Es handelt sich um eine LIFO-Struktur.
Aufgabe zur Listenverarbeitung
1. Mit Hilfe der vorliegenden Darstellung über den Aufbau einer einfach geketteten
Liste soll eine Anwendung „Adreßverwaltung“ entwickelt werden. Die
Adreßverwaltung soll über einen benutzerdefinierten Datentyp „liste“, der eine
einfach gekettete, lineare Liste beschreibt, erfolgen. Zu Beginn der Verarbeitung sind
die Adreßverwaltungsdaten aus einer Textdatei, deren Datensätze (Zeilen)
Adreßdaten enthalten, zu entnehmen bzw. zu sichern 138.
#include
#include
#include
#include
138
<iostream.h>
<fstream.h>
<string.h>
<stdlib.h>
vgl. PR43105.CPP
196
Die Programmiersprache C++
typedef int bool;
const int FALSE = 0;
const int TRUE = 1;
struct eintrag
{
char name[20];
char vorname[15];
eintrag* zNachf;
};
class liste
{
private:
eintrag eintr;
eintrag* zAnfang;
char* dateiName;
// Hier werden die Daten abgelegt
public:
liste(char*);
// Konstruktor
~liste(void);
// Destruktor
bool einfuegen(eintrag*);
eintrag* holeEintr(char*);
bool loescheEintr(char*);
};
// Schnittstellenfunktionen
// Konstruktor und Destruktor
liste :: liste(char* dName)
{
// Im Konstruktor werden die Daten aus einer
// externen Datei eingelesen
eintrag* einTr;
fstream datei;
zAnfang
= NULL;
dateiName = dName;
datei.open(dName, ios::in);
// Keine Fehlerbehandlung, falls die Datei nicht existiert,
// da dies beim ersten Start des Programms normal ist
if (datei)
{
// Einlesen der Daten
while (!datei.eof())
{
if ((einTr = new eintrag) == NULL)
{
cerr << "\n\tNicht genuegend Speicher ";
break;
}
datei >> einTr->name;
datei >> einTr->vorname;
// ....................
if (datei.eof()) break;
einTr->zNachf = zAnfang;
zAnfang = einTr;
}
datei.close();
}
}
liste :: ~liste(void)
{
// Im Destruktor wird die Liste wieder in eine externe
// Datei geschrieben
eintrag* zLoe;
197
Die Programmiersprache C++
fstream datei;
datei.open(dateiName, ios::out);
if (!datei)
{
cerr << "\n\tDie Datei " << dateiName
<< " kann nicht zur Ausgabe geoeffnet werden \n";
exit(-1);
}
while (zAnfang != NULL)
{
datei << zAnfang->name << '\n';
datei << zAnfang->vorname << '\n';
// ......
zLoe = zAnfang;
zAnfang = zAnfang->zNachf;
delete zLoe;
// Rueckgabe des Speicherplatzes
}
datei.close();
};
// Transferfunktionen
bool liste :: einfuegen(eintrag* zEintr)
{
/* Ein neuer Listenknoten wird in die Liste eingefuegt
Die Informationen werden an anderer Stelle eingetragen */
if (zEintr != NULL)
{
zEintr->zNachf = zAnfang;
zAnfang = zEintr;
return(TRUE);
}
return(FALSE);
}
eintrag* liste :: holeEintr(char* info)
{
// Suchen eines Eintrags mit der Bezeichnung "info"
eintrag* zElem = zAnfang;
while (zElem != NULL)
{
if (strcmp(info,zElem->name) == 0)
// Eintrag gefunden
return(zElem);
// Kein Eintrag gefunden: Weitersuchen
zElem = zElem->zNachf;
}
return(NULL); // Ende der Liste erreicht
}
bool liste :: loescheEintr(char* info)
{
// Der Eintrag mit dem Namen ifo wird aus der Liste
// entfernt
eintrag* zElem;
eintrag* zLoe;
if (zAnfang == NULL)
// in einer leeren Liste gibt es nichts zu loeschen
return(FALSE);
zElem = zAnfang;
// Weiterer Spezialfall: Das zu loeschende Element ist
// das erste Element in der Liste
if (strcmp(zElem->name,info) == 0)
{
zAnfang = zElem->zNachf;
delete zElem;
198
Die Programmiersprache C++
return(TRUE);
}
while (zElem->zNachf != NULL)
{
if (strcmp(zElem->zNachf->name,info) == 0)
{
zLoe = zElem->zNachf;
zElem->zNachf = zElem->zNachf->zNachf;
delete zLoe;
return(TRUE);
}
zElem = zElem->zNachf;
}
return(FALSE);
}
int main()
{
short menue(void), wahl;
void einfuegenElement(liste&);
void suchenElement(const liste&);
void loeschenElement(liste&);
liste adressen("a:adrlist.dat");
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
{
case 1: einfuegenElement(adressen);
break;
case 2: suchenElement(adressen);
break;
case 3: loeschenElement(adressen);
break;
case 4: break;
}
} while (wahl != 4);
}
short menue(void)
{
short izch;
cout << "\n\tBitte eingeben: ";
cout << "\n\t1: Eingabe eines Datensatzes ";
cout << "\n\t2: Suchen nach einem Datensatz ";
cout << "\n\t3: Loeschen Datensatz aus Liste ";
cout << "\n\t4: Ende Listenbearbeitung ";
cout << "\n\tWahl: ";
do
{
cin >> izch;
} while ((izch < 1) || (izch > 4));
return izch;
}
void einfuegenElement(liste& adressen)
{
eintrag* zelem;
if ((zelem = new eintrag) == NULL)
cerr << "\n\tKein Speicherplatz fuer Eintrag vorhanden";
else
{
199
Die Programmiersprache C++
cout << "\tName: ";
cin >> zelem->name;
cout << "\tVorname: "; cin >> zelem->vorname;
if (adressen.einfuegen(zelem))
cout << "\n\tEintrag konnte eingefuegt werden" << endl;
else
cerr << "\n\tEintrag konnte nicht eingefuegt werden ";
}
}
void suchenElement(const liste& adressen)
{
char suchName[20];
eintrag* zelem;
cout << "\n\tName angeben: ";
cin >> suchName;
if ((zelem = adressen.holeEintr(suchName)) != NULL)
{
cout << "\n\tGefunden:";
cout << "\n\t" << zelem->name << ' ' << zelem->vorname;
}
else cout << "\n\tNicht gefunden";
}
void loeschenElement(liste& adressen)
{
char suchName[20];
cout << "\n\tName angeben: ";
cin >> suchName;
if (adressen.loescheEintr(suchName))
cout << "\n\t" << suchName << " wurde geloescht";
else
cout << "\n\t" << suchName << " konnte nicht geloescht werden";
}
2. Die vorstehende Aufgabe soll mit Hilfe der Klassenschablone „list“ der Standard
Template 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;
}
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)
139
vgl. 5.2.7
200
Die Programmiersprache C++
{
if ((this->name.compare(ein.name) != 0)
(this->vorname.compare(ein.vorname)
return true;
if ((this->name.compare(ein.name) == 0)
(this->vorname.compare(ein.vorname)
return true;
if ((this->name.compare(ein.name) != 0)
(this->vorname.compare(ein.vorname)
return true;
return false;
&&
!= 0))
&&
!= 0))
&&
== 0))
}
bool operator<(const eintrag& ein)
{
if ((this->name.compare(ein.name) < 0))
return true;
else
return false;
}
friend ostream& operator<<(ostream&, const eintrag&);
};
ostream& operator << (ostream& strm, const eintrag& elem)
{
strm << elem.name << " " << elem.vorname;
return strm;
}
list<eintrag> adressen;
list<eintrag>::iterator iter;
int main()
{
short menue(void), wahl;
void einfuegenElement();
void auflisten();
void suchenElement();
void loeschenElement();
cout << "\n\t A D R E S S E N V E R W A L T U N G";
cout << "\n\t -----------------------------------";
do
{
wahl = menue();
switch(wahl)
{
case 1: einfuegenElement();
break;
case 2: auflisten();
break;
case 3: suchenElement();
break;
case 4: loeschenElement();
break;
case 5: break;
}
} while (wahl != 5);
}
short menue(void)
{
short izch;
cout << "\n\tBitte eingeben: ";
cout << "\n\t1: Eingabe eines Datensatzes ";
cout << "\n\t2: Auflisten der Listenelemente ";
cout << "\n\t3: Suchen nach einem Datensatz ";
201
Die Programmiersprache C++
cout << "\n\t4: Loeschen Datensatz aus Liste ";
cout << "\n\t5: Ende Listenbearbeitung ";
cout << "\n\tWahl: ";
do
{
cin >> izch;
} while ((izch < 1) || (izch > 5));
return izch;
}
void einfuegenElement()
{
string name; string vorname;
cout << "\tName: ";
cin >> name;
cout << "\tVorname: "; cin >> vorname;
eintrag elem(name,vorname);
adressen.push_back(elem);
}
void auflisten()
{
adressen.sort();
cout << "\n\tAuflisten der sortierten Listenelemente:\n";
for ( iter = adressen.begin(); iter != adressen.end(); ++iter )
cout << "\t" << *iter << endl;
}
void suchenElement()
{
string suchName; string suchVorname;
cout << "\n\tName angeben: ";
cin >> suchName;
cout << "\n\tVorname angeben: ";
cin >> suchVorname;
eintrag suchElem(suchName,suchVorname);
iter = find(adressen.begin(),adressen.end(),suchElem);
if (iter != adressen.end())
{
cout << "\n\tGefunden:";
cout << "\n\t" << *iter << endl;
}
else cout << "\n\tNicht gefunden";
}
void loeschenElement()
{
string suchName; string suchVorname;
cout << "\n\tName angeben: ";
cin >> suchName;
cout << "\n\tVorname angeben: ";
cin >> suchVorname;
eintrag suchElem(suchName,suchVorname);
iter = find(adressen.begin(),adressen.end(),suchElem);
if (iter != adressen.end())
{
cout << "\n\tGefunden mit abschliessendem Loeschen:";
cout << "\n\t" << *iter << endl;
list<eintrag>::iterator liter;
liter = iter;
// iter = erase(liter); Fehler !!!
}
else cout << "\n\tNicht gefunden";
}
202
Die Programmiersprache C++
4.2.2 Klassenschablonen für verkettete Listen
4.2.2.1 Doppelt gekettete Listen
vgl. 5.2.3
4.2.2.2 Ringförmig geschlossene Listen
1. Einfach verkettete, ringförmig geschlossene Liste
Aufbau einfach geketteter Ringstrukturen
BASIS
"Leerer Ring"
BASIS
Abb.:
Eine leere ringförmig verkettete Liste enthält eine Listenknoten und ein nicht
initialisiertes Datenfeld. Der Zeiger auf diesen Listenknoten zeigt auf sich selbst. Ein
„Null“-Zeiger existiert in ringförmig verketteten Listen nicht.
Gegeben ist folgende Listenstruktur
ZGR
ZGR1
type Zeiger = record
............
............
Nachf : ^Zeiger;
end;
Abb.:
Eine ringförmige Datenstruktur kann durch die Anweisung
ZGR1^.Nachf := ZGR;
203
Die Programmiersprache C++
erreicht werden. In der Regel zeigt der letzte Knoten in der verkettet gespeicherten
Liste auf den Listenanfang. Ringe können auch folgenden Aufbau besitzen:
Abb.:
Die Klasse „einfach verketteter Ringknoten“ in C++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
daten:
nachf:
Abb.: Liste mit Knoten
// Schnittstellenfunktionen
140
vgl. ringkno.h
204
Die Programmiersprache C++
Der Konstruktor initialisiert einen Knoten, der einen Zeiger enthält, der auf diesen
Knoten zurück verweist. So kann jeder Knoten den Anfang einer leeren Liste
repräsentieren. Das Datenfeld des Knoten bleibt in diesem Fall unbesetzt.
// Konstruktor der eine Liste deklariert und "daten"
// uninitialisiert laesst.
template <class T>
ringKnoten<T>::ringKnoten(void)
{
// initialisiere den Knoten, so dass er auf sich selbst zeigt
nachf = this;
}
// Konstruktor der eine leere Liste erzeugt und "daten"
// initialisiert
template <class T>
ringKnoten<T>::ringKnoten(const T& merkmal)
{
// setze den Knoten so, dass er auf sich selbst zeigt
// und initialisiere "daten"
nachf = this;
daten = merkmal;
}
Die Methode nachfKnoten() ermittelt einen Verweis auf den nächsten in der
einfach verketteten, ringförmig geschlossenen Liste. Die Methode soll das
Durchlaufen der Liste erleichtern.
// Rueckgabe des Zeiger auf den naechsten Knoten
template <class T>
ringKnoten<T> *ringKnoten<T>::nachfKnoten(void) const
{
return nachf;
}
Die Methoden zur Modifikation der Liste einfuegenDanach(ringKnoten<T>
*z); fügt die Listenknoten unmittelbar nach dem Anfangsknoten (der die leere Liste
definiert) ein.
vor dem Einfügen:
nach dem Einfügen:
daten:
nachf:
z
Abb.: Einfügen des Knoten „z“ in eine leere Liste
vor dem Einfügen:
nach dem Einfügen:
205
Die Programmiersprache C++
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 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)
141
PR22221.CPP
206
Die Programmiersprache C++
{
// 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();
}
}
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);
207
Die Programmiersprache C++
// 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
Leerer Ring
Basis
Abb.: Der leere Ring in einer doppelt geketteten Ringstruktur
208
Die Programmiersprache C++
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;
};
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
142
dringkn.h
209
Die Programmiersprache C++
// 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;
}
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;
}
210
Die Programmiersprache C++
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;
}
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();
143
PR22225.CPP
211
Die Programmiersprache C++
}
// 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;
}
212
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 Listenknoten 145.
Bsp.: Erstellen und Verwalten eines Telefon-Verzeichnisses mit einer Hash-Tabelle
im Rahmen des „seperate chaining“. Das Verzeichnis ist bisher in einer
Textdatei enthalten. Die in dieser Tabelle gespeicherten Sätze haben folgenden
Aufbau:
144
145
PR44205.CPP
PR44310.CPP
213
Die Programmiersprache C++
Name
Juergen
......................
Telefon-Nr.
93622
.........
Die Datensätze dieser Datei sollen in eine Hash-Tabelle übernommen werden und
dort verwaltet werden (Einfügen, Löschen, Wiederauffinden von Datensätzen). Der
Hashtabellen-Zugriff soll über den Namen im Datensatz erfolgen. Der Aufbau der
Hash-Tabelle und die dort eingetragenen Verweise sollen nach dem Prinzip des
„seperate chaining“ organisiert werden.
~
~
~
~
Hashtabelle
Abb.: Hash-Tabelle für „seperate chaining“
Die von der Hash-Tabelle ausgehenden Verweise bilden eine lineare Liste. Eine
solche Liste wird zweckmäßig über einen benutzerdefinierten Datentyp „liste“ zum
Erzeugen und Verwalten einer einfachen, geketteten Liste realisiert.
//
//
//
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;
};
// Beschreibung der Klasse einfach gekettete Liste
class liste
{
private:
element *zStart;
public:
214
Die Programmiersprache C++
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;
}
// Beschreibung der Hash-Tabelle fuer´das seperate chaining
class hashTabelle
{
private:
unsigned N;
liste *a;
public:
215
Die Programmiersprache C++
// 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;
}
}
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:
216
Die Programmiersprache C++
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
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
146
PR44315.CPP
217
Die Programmiersprache C++
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);
}
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;
}
}
218
Die Programmiersprache C++
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.
219
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).
220
Die Programmiersprache C++
Dateninformation
Knotenzeiger
Schluessel
Datenteil
Links
Rechts
Zeiger
Zeiger
zum linken
zum rechten
Nachfolgeknoten
Abb. 4.5-2: Knoten eines binären Suchbaums
Die Klassenschablone BaumKnoten
// Schnittstellenbeschreibung
// Die Klasse binaerer Suchbaum binSBaum benutzt die Klasse baumKnoten
template <class T> class binSBaum;
// Deklaration eines Binaerbaumknotens fuer einen binaeren Baum
template <class T> class baumKnoten
{
protected:
// zeigt auf die linken und rechten Nachfolger des Knoten
baumKnoten<T> *links;
baumKnoten<T> *rechts;
public:
// Das oeffentlich zugaenglich Datenelement "daten"
T daten;
// Konstruktor
baumKnoten (const T& merkmal, baumKnoten<T> *lzgr = NULL,
baumKnoten<T> *rzgr = NULL);
// virtueller Destruktor
virtual ~baumKnoten(void);
// Zugriffsmethoden auf Zeigerfelder
baumKnoten<T>* holeLinks(void) const;
baumKnoten<T>* holeRechts(void) const;
// Die Klasse binSBaum benoetigt den Zugriff auf
// "links" und "rechts"
friend class binSBaum<T>;
};
Die Dateninformation des Binärbaumknotens kann folgendermaßen beschrieben
werden:
struct info
{
// int schl;
char* schl;
char* vorname;
};
// Schluessel
// irgendwelche Daten
// Schnittstellenfunktionen
// Konstruktor: Initialisiert "daten" und die Zeigerfelder
// Der Zeiger NULL verweist auf einen leeren Baum
template <class T>
baumKnoten<T>::baumKnoten (const T& merkmal, baumKnoten<T> *lzgr,
baumKnoten<T> *rzgr): daten(merkmal), links(lzgr), rechts(rzgr) {}
// Die Methode holeLinks ermoeglicht den Zugriff auf den linken
221
Die Programmiersprache C++
// Nachfolger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeLinks(void) const
{
// Rueckgabe des Werts vom privaten Datenelement links
return links;
}
// Die Methode "holeRechts" erlaubt dem Benutzer den Zugriff auf den
// rechten Nachfoger
template <class T>
baumKnoten<T>* baumKnoten<T>::holeRechts(void) const
{
// Rueckgabe des Werts vom privaten Datenelement rechts
return rechts;
}
// Destruktor: tut eigentlich nichts
template <class T>
baumKnoten<T>::~baumKnoten(void)
{}
Aufsuchen von in Baumknoten gespeicherten Schlüsselwerten
Ausganspunkt ist der Wurzelknoten:
baumKnoten<T>* wurzel;
// Zeiger auf den Wurzelknoten
Das Aufsuchen eines Elements im Zugriffsbaum geht vom Wurzelknoten über einen
Kantenzug (d.i. eine Reihe von Zwischenknoten) zum gesuchten Datenelement. Bei
jedem Zwischenknoten auf diesem Kantenzug findet ein Entscheidungsprozeß über
die folgenden Vergleiche statt:
1. Die beiden Schlüssel sind gleich: Das Element ist damit gefunden
2. Der gesuchte Schlüssel ist kleiner: Das gesuchte Element kann sich dann nur im linken
Unterbaum befinden
3. Der gesuchte Schlüssel ist größer: Das gesuchte Element kann sich nur im rechten Unterbaum
befinden.
Das Verfahren wird solange wiederholt, bis das gesuchte (Schlüssel-) Element
gefunden ist bzw. feststeht, daß es in dem vorliegenden Datenbestand nicht
vorhanden ist.
Struktur und Wachstum binärer Bäume sind durch die Ordnungrelation bestimmt.
Aufgabe: Betrachte die 3 Schlüssel 1, 2, 3. Diese 3 Schlüssel können durch
verschieden angeordnete Folgen bei der Eingabe unterschiedliche binäre
Bäume erzeugen. Zeichne alle Bäume, die aus unterschiedlichen
Eingaben der 3 Schlüssel resultieren, auf.
222
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;
}
223
Die Programmiersprache C++
Funktionsschablonen zur Bearbeitung binärer Bäume
a) Ausgabe eines binären Baums
Die Ausgabe soll auf das Standardausgabegerät erfolgen
// Ausgabefunktionen fuer die Darstellung des Binaerbaums
// Zwischenraum zwischen den Stufen
const int anzZwischenraum = 6;
// Einfuegen einer anzahl Zwischenraumzeichen auf der
// aktuellen Zeile
void ausgAnzZwischenraum(int num)
{
for (int i = 0; i < num; i++)
cout << " ";
}
template <class T>
void ausgBaum(baumKnoten<T> *b, int stufe)
{
if (b != NULL)
{
// gib den rechten Teilbaum aus
ausgBaum(b->holeRechts(), stufe + 1);
ausgAnzZwischenraum(anzZwischenraum * stufe);
cout << b->daten << endl;
ausgBaum(b->holeLinks(),stufe + 1);
}
}
b) Zählen der Blätter
// Anzahl Blätter
template <class T>
void anzBlaetter(baumKnoten<T>* b, int& zaehler)
{
// benutze den Postorder-Durchlauf
if (b != NULL)
{
anzBlaetter(b->holeLinks(), zaehler);
anzBlaetter(b->holeRechts(), zaehler);
// Pruefe, ob der erreichte Knoten ein Blatt ist
if (b->holeLinks() == NULL && b->holeRechts() == NULL)
zaehler++;
}
}
c) Ermitteln der Tiefe bzw. der Höhe
// Hoehe des Baums
template <class T>
int hoehe(baumKnoten<T>* b)
{
int hoeheLinks, hoeheRechts, hoeheWert;
if (b == NULL)
hoeheWert = -1;
else
{
hoeheLinks = hoehe(b->holeLinks());
hoeheRechts = hoehe(b->holeRechts());
hoeheWert = 1 +
(hoeheLinks > hoeheRechts ? hoeheLinks : hoeheRechts);
}
224
Die Programmiersprache C++
return hoeheWert;
}
d)Kopieren des Baums
// Kopieren eines Baums
template <class T>
baumKnoten<T>* kopiereBaum(baumKnoten<T>* b)
{
baumKnoten<T> *neuerLzgr, *neuerRzgr, *neuerKnoten;
// Rekursionsendebedingung
if (b == NULL)
return NULL;
if (b->holeLinks() != NULL)
neuerLzgr = kopiereBaum(b->holeLinks());
else
neuerLzgr = NULL;
if (b->holeRechts() != NULL)
neuerRzgr = kopiereBaum(b->holeRechts());
else neuerRzgr = NULL;
// Der neue Baum wird von unten her aufgebaut,
// zuerst werden die Nachfolger bearbeitet und
// dann erst der Vaterknoten
neuerKnoten = erzeugebaumKnoten(b->daten, neuerLzgr, neuerRzgr);
// Rueckgabe des Zeigers auf den zuletzt erzeugten Baumknoten
return neuerKnoten;
}
f) Erzeugen bzw. Freigabe eines Binärbaumknotens
template <class T>
baumKnoten<T>* erzeugebaumKnoten(T merkmal,
baumKnoten<T>* lzgr = NULL,
baumKnoten<T>* rzgr = NULL)
{
baumKnoten<T> *z;
// Erzeugen eines neuen Knoten
z = new baumKnoten<T>(merkmal,lzgr,rzgr);
if (z == NULL)
{
cerr << "Kein Speicherplatz!\n";
exit(1);
}
return z; // Rueckgabe des Zeigers
}
Der durch den Baumknoten belegte
Funktionsschablone freigegeben werden:
template <class T>
void gibKnotenFrei(baumKnoten<T>* z)
{
delete z;
}
g) Löschen des Baums
// Loeschen des Baums
225
Speicherplatz
kann
über
folgende
Die Programmiersprache C++
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;
// 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
226
Die Programmiersprache C++
// 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;
// baumspezifische Methoden
void aktualisieren(const T& merkmal);
baumKnoten<T> *holeWurzel(void) const;
};
227
Die Programmiersprache C++
Operationen
1. Einfügen eines Knoten
Vorstellung zur Lösung:
1. Suche nach dem Schlüsselwert
2. Falls der Schlüsselwert im binären Suchbaum gespeichert ist, erfolgt kein Einfügen.
3. Bei erfolgloser Suche wird ein neuer Baumknoten mit dem Schlüsselwert als Sohn des erreichten
Blatts eingefügt.
Die Schnittstellenfunktion void einfuegen(const T& merkmal); besitzt
folgende 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++;
}
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.:
147
vgl. bsbaum.h
228
Die Programmiersprache C++
vorher
nachher
Das Entfernen kann leicht durchgeführt werden
b) Der zu löschende Knoten hat genau einen Sohn, z.B.:
vorher
nachher
C) Der zu löschende Knoten hat zwei Söhne, z.B.:
nachher
vorher
Der nach dem Löschen resultierende Teilbaum ist natürlich wieder ein Suchbaum,
häufig allerdings mit erheblich vergrößerter Höhe.
Aufgaben
1) Gegeben ist ein binärer Baum folgender Gestalt:
k
k1
k2
229
Die Programmiersprache C++
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
230
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.:
231
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
// linken Nachfolgers zu "Eltern" geloescht
EvonErsKnoZgr->rechts = ErsKnoZgr->links;
232
Die Programmiersprache C++
// plaziere den Ersatzknoten an die Stelle von LKnoZgr
ErsKnoZgr->links = LKnoZgr->links;
ErsKnoZgr->rechts = LKnoZgr->rechts;
}
}
// Vervollstaendige die Verkettung mit den "Eltern"-Knoten
// Loesche den Wurzelknoten, bestimme eine neue Wurzel
if (EKnoZgr == NULL) wurzel = ErsKnoZgr;
// Zuweisen Ers zum korrekten Zweig von E
else if (LKnoZgr->daten < EKnoZgr->daten)
EKnoZgr->links = ErsKnoZgr;
Else EKnoZgr->rechts = ErsKnoZgr;
// Loesche den Knoten aus dem Speicher und erniedrige "groesse"
freigabeKnoten(LKnoZgr);
groesse--;
}
Test: Die nun bekannte Prozedur zum Löschen vom Baumknoten nach der
Verfahrensweise des "Schlüsseltransfer" ist folgenden Prüfungen zu unterwerfen:
1) Der zu löschende Baumknoten besteht nur aus einem Wurzelknoten, z.B.:
Schlüssel
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:
233
Die Programmiersprache C++
7
5
8
3) Vorgegeben ist
Schlüssel
LINKS
12
RECHTS
7
5
15
8
13
14
Abb.:
Der Wurzelknoten wird gelöscht.
Ergebnis:
234
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
235
Die Programmiersprache C++
WRL-Ordnung
(1) Aufsuchen der BAUMWURZEL
(2) Durchlaufen (Aufsuchen) des rechten Teilbaums in PREORDER
(3) Durchlaufen (Aufsuchen) des linken Teilbaums in PREORDER
Es wird hier grundsätzlich die Wurzel vor den (beiden) Teilbäumen durchlaufen.
3. Postorder-Durchlsauf
LRW-Ordnung
(1) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER
(2) Durchlaufen (Aufsuchen) des rechten Teilbaums in POSTORDER
(3) Aufsuchen der BAUMWURZEL
Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen.
RLW-Ordnung
(1) Durchlauden (Aufsuchen) des rechten Teilbaums in POSTORDER
(2) Durchlaufen (Aufsuchen) des linken Teilbaums in POSTORDER
(3) Aufsuchen der BAUMWURZEL
Zuerst werden die beiden Teilbäume und dann die Wurzel durchlaufen.
Funktionsschablonen für das Durchlaufen binärer Bäume
// Funktionsschablonen fuer Baumdurchlaeufe
template <class T> void inorder(baumKnoten<T>* b,
void aufsuchen(T& merkmal))
{
if (b != NULL)
{
inorder(b->holeLinks(),aufsuchen);
aufsuchen(b->daten);
inorder(b->holeRechts(),aufsuchen);
}
}
template <class T> void postorder(baumKnoten<T>* b,
void aufsuchen(T& merkmal))
{
if (b != NULL)
{
postorder(b->holeLinks(),aufsuchen);
// linker Abstieg
postorder(b->holeRechts(),aufsuchen);
// rechter Abstieg
aufsuchen(b->daten);
}
}
Aufgaben: Gegeben sind eine Reihe binärer Bäume. Welche Folgen entstehen beim
Durchlaufen der Knoten nach den Prinzipien "Inorder (LWR)", "Praeorder WLR" und
"Postorder (LRW)".
236
Die Programmiersprache C++
1)
A
B
C
E
D
F
I
G
J
H
K
L
"Praeorder": A B C E I F J D G H K L
"Inorder":
EICFJBGDKHLA
"Postorder": I E J F C G K L H D B A
2)
+
*
A
+
B
*
C
E
D
"Praeorder": + * A B + * C D E
"Inorder":
A*B+C*D+E
"Postorder": A B * C D * E + +
Diese Aufgabe zeigt einen Strukturbaum (Darstellung der hierarchischen Struktur
eines arithmetischen Ausdrucks). Diese Baumdarstellung ist besonders günstig für
die Übersetzung eines Ausdrucks in Maschinensprache. Aus der vorliegenden
Struktur lassen sich leicht die unterschiedlichen Schreibweisen eines arithmetischen
Ausdrucks herleiten. So liefert das Durchwandern des Baums in „Postorder" die
Postfixnotation, in „Praeorder“ die Praefixnotation.
3)
+
237
Die Programmiersprache C++
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
238
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>
<cstring>
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.
239
Die Programmiersprache C++
<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.
Schnittstelle
Mathematische Entsprechung
240
Die Programmiersprache C++
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)
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>.
<cstdlib>
149
Die Abkürzung F bedeutet: Einer der Typen float, double oder long double
241
Die Programmiersprache C++
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>
<ctime>
150
Div_t ist eine vordefinierte Struktur, die das Divisionsergebnis und den Rest enthält:
struct div_t
{
int quot; // Qotient
int rem; // Rest
}
242
Die Programmiersprache C++
Der Header <ctime> enthält verschiedene Funktionen zur Bearbeitung und
Auswertung der Systemzeitinformation.
Datentypen: neben bekannten Typen NULL und size_t sind definiert:
clock_t
time_t
struct tm
int
int
int
int
int
int
int
int
int
// Datentyp für CPU-Zeitangaben
// Datentyp für Datums- und Zeitangaben
// Struktur mit mindestens folgenden Elementen
tm_sec
// Sekunden 0 .. 59
tm_min
// Minuten 0 .. 59
tm_hour
// Stunden 0 .. 23
tm_mday
// Monatstag 1 .. 31
tm_mon
// Monat 0.. 11
tm_year
// Jahr seit 1900
tm_wday
// Wochentag seit Sonntag 0 .. 6
tm_yday
// Tag seit 1. Januar 0 .. 365
tm_isdst // is daylight saving time, Werte:
// Sommerzeit (> 0), Winterzeit (0)
// undefiniert (-1)
Funktionen:
char * asctime(const tm*)
char * ctime(const time_t*)
Die Funktionen wandeln die im tm-Format oder
time_t Format vorliegende Zeit in einen
formatierten C-String um.
clock_t clock()
Gibt die seit Programmstart verstrichene CPU-Zeit
in „Ticks“ zurück. Die „Ticks“ können in Sekunden
umgerechnet
werden,
wenn
durch
die
vordefinierte
Konstante
CLOCKS_PER_SEC
dividiert wird
double difftime(time_t t1, time_t t2) Ermittelt die Differenz der Zeiten in Sekunden
size_t strftime(char* puffer, size_t Wandelt die im tm-Format vorliegende Zeit in
max, const char* format, const tm* z) einen formatierten C-String um, wobei das
Ergebnis im Puffer puffer abgelegt wird und
format einen C-String mit Formatangaben
darstellt. Die Anzahl der in den Puffer
geschriebenen Zeichen wird zurückgegeben, falls
sie < max ist, ansonsten ist das Ergebnis der
Funktion 0. Die in einem Unix-Sytem möglichen
Formate können mit man strftime erfragt
werden
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.
243
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.
244
Die Programmiersprache C++
Funktoren sind Objekte, die sich wie Funktionen verhalten, aber alle Eigenschaften
von Objekten haben. Sie können erzeugt, als Parameter übergeben oder in ihrem
Zustand verändert werden.
In <functional> sind Klassen definiert, die zum Erzeugen verschiedener
Funktionsobjekte dienen.
Klassen dieses Header erben von einer der Klassen:
template <class Arg, class Result> struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
}
template <class Arg1, class Arg2, class Result> struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
}
in Abhängigkeit davon, ob es sich um unäre oder binäre Operationen handelt, z.B.:
template <class T> struct plus : binary_function<T,T,T>
{
T opertor()(const T& x, const T& y) const;
}
Arithmetische, vergleichende und logische Operationen
In der STL werden für alle möglichen Grundoperationen Funktionsobjekte
vordefiniert.
// arithmetische Operationen:
template
template
template
template
template
template
<class
<class
<class
<class
<class
<class
T>
T>
T>
T>
T>
T>
struct
struct
struct
struct
struct
struct
plus;
minus;
multiplies;
divides;
modulus;
negate;
//
//
//
//
//
//
x +
x x *
x /
x %
-x
y
y
y
y
y
T>
T>
T>
T>
T>
T>
T>
struct
struct
struct
struct
struct
struct
struct
equal_to;
// x == y
not_equal_to; // x != y
greater;
// x > y
less;
// x < y
greater_equal; // x >= y
less_equal;
// x <= y
equal_to; // x == y
// Vergleiche
template
template
template
template
template
template
template
<class
<class
<class
<class
<class
<class
<class
// logische Operationen
template <class T> struct logical_and;
template <class T> struct logical_or;
template <class T> struct logical_not;
// x && y
// x || y
// !x
Funktionsobjekte zum Negieren logischer Prädikate
245
Die Programmiersprache C++
„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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/* Selbstdefiniertes Funktionsobjekt hoch */
#include "fohoch.h"
int main()
{
vector<int> v;
// Elemente 1 bis 6 in v einfuegen
for (int i = 1; i <= 6; i++)
v.push_back(i);
// Elemente mit 3 mutiplizieren
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind2nd(multiplies<int>(),3));
cout << endl;
// 3 hoch alle Elemente ausgeben
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind1st(hoch<int>(),3));
// Quellbereich
// Zielbereich
// Operation
// Quellbereich
// Zielbereich
// Operation
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
246
den
Die Programmiersprache C++
cout << endl;
// Alle Elemente hoch 3 ausgeben
transform(v.begin(), v.end(),
ostream_iterator<int>(cout," "),
bind2nd(hoch<int>(),3));
cout << endl;
// 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.:
247
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
container::reserve_iterator
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
Liefert den Datentyp für Reserve-Iteratoren des Containers
Vorhanden bei Vektoren, Deques, Listen, Sets, Multisets,
248
Die Programmiersprache C++
container::
const_reserve_iterator
container::key_type
container::key_ompare
container::value_compare
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
Nicht-Modifizierender Zugriff
Funktionen zur Größe
249
Die Programmiersprache C++
Methode
size_type container::size() const
Bedeutung
Liefert die aktuelle Anzahl von Elementen im
Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
bool container::empty() const
Liefert, ob der Container leer ist
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
size_type container::max_size() const Liefert die maximale Anzahl von Elementen, die
der Container enthalten kann
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
Kapazitäts-Funktionen
Methode
Bedeutung
size_type container::capacity() const Liefert die Anzahl von Elementen, die ohne
Reallokierung aufgenommen werden können
Vorhanden bei: Vektoren, Strings
void container::reserve(size_type
Reserviert für einen Vektor Speicherplatz für
anz)
mindestens anz Elemente, sofern nicht schon
genug Speicherplatz zur Verfügung steht
Vorhanden bei: Vektoren, Strings
Vergleichsoperatoren
Für alle Container-Klassen der Standard-Template-Library werden die
Vergleichsoperatoren == und < definiert. Andere vergleiche werden auf diese
Operatoren umgesetzt.
Methode
bool operator == (const
c1, const container& c2)
Bedeutung
container& Prüft die Container c1 und c2 auf Gleichheit, liefert
true, wenn die beiden Container die gleiche
Anzahl von Elementen Besitzen und jedes
Element in c1 == dem entsprechenden element in
c2 ist.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
bool operator < (const container& c1, Prüft, ob der Container c1 „kleiner als“ c2 ist.
const container& c2)
Liefert true, wenn der erste Container „lexikalisch
kleiner“ als der zweite ist.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Zuweisungen
Methode
container& container::operator =
(const container& c)
Bedeutung
Weist dem Container die Elemente des
Containers c zu
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
void container::assign(Size anz)
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
250
Die Programmiersprache C++
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
Direkter Element-Zugriff
Methode
reference container::at(size_type
idx)
const_reference
container::at(size_type idx) const
reference
container::operator[](size_type idx)
const_reference container::operator
[](size_type idx) const
T& map::operator[](const key_type
key)
const T& map::operator[](const
key_type key) const
reference container::front()
const_reference container::front()
const
reference container::back()
const_reference container::back()
const
Bedeutung
Liefert das Element mit dem Inhalt idx
Vorhanden bei: Vektoren, Deques, Strings
Liefert das Element mit dem Inhalt idx, im
Gegensatz zu at() finder keine Überprüfung von
idx statt. Der Programmierer muß sicherstellen,
daß idx einen erlaubten Wert besitzt
Vorhanden bei: Vektoren, Deques, Strings
Liefert das Element mit dem Schlüsselwert key
zum Abfragen und Setzen. Existiert kein Element
mit solchem Schlüsselwert, wird automatisch
eines angelegt und mit dem Default-Konstruktor
des Werte-Typs initialisiert. T ist der datentyp der
zum Schlüssel gehörenden Werte
Vorhanden bei: Maps
Liefert das erste Element (mit dem Index 0).
Der Aufrufer muß sicherstellen, daß ein solches
Element existiert (size() > 0).
Vorhanden bei: Vektoren, Deques, Listen
Liefert das letzte Element (das Element mit dem
Index size()-1)
Vorhanden bei: Vektoren, Deques, Listen
Iterator-Funktionen
Methode
iterator container::begin()
const_iterator container::begin()
const
Bedeutung
Liefert einen Iterator für den Anfang des
Containers (die Position des ersten elements im
Container). Ist der Container leer, entspricht dies
container::end()
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
iterator container::end()
Liefert einen Iterator für das Ende des Containers
const_iterator container::end() const (die Position hinter dem letzten Element im
Container)
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
reverse_iterator container::rbegin() Liefert einen Reverse-Iterator für den Anfang
const_reverse_iterator container::
eines umgekehrten Durchlaufs durch den
rbegin() const
Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
reverse_iterator container::rend()
Liefert einen Reverse-Iterator für das Ende eines
251
Die Programmiersprache C++
const_reverse_iterator
container::rend() const
umgekehrten Durchlaufs durch den Container
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
Einfügen und Löschen
Methode
iterator container::insert(iterator
pos)
pair<iterator,bool>
container::insert(const T& wert)
iterator container::insert(const T&
wert)
iterator container::insert(iterator
pos, const T& wert)
Bedeutung
Fügt an die Position des Iterators pos ein Element
ein, das mit dessen Default-Konstruktor erzeugt
wird, liefert die Position des neuen Elements.
Vorhanden bei: Vektoren, Deques, Listen, strings
Fügt in einen assoziativen Container eine Kopie
von wert als Element ein
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Fügt an die Position des Iterators pos eine Kopie
von wert als Element ein, liefert die Position des
neuen Elements
void container::insert(iterator pos, Fügt an die Position des iterators pos anz Kopien
size_type anz, const T& wert)
von wert als Element ein.
T ist der Datentyp der Elemente, bei Vektoren und
Deques können dadurch Verweise auf andere
Elemente ungültig werden.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
void container::insert(InputIterator Fügt in assoziative Container Kopien aller
anf, InputIterator end)
Elemente des Bereichs von anf bis end ein.
Vorhanden
bei:
Sets,
Multisets,
Maps, Multimaps
void container::insert(iterator pos, Fügt an die Position des Iterators pos Kopien
InputIterator anf, InputIterator end) aller Elemente des Bereichs von anf bis end ein.
Vorhanden bei: Vektoren, Deques, Listen, Strings
void container::push_front(const T&
Fügt als erstes Element eine Kopie von wert ein.
wert)
T ist der Datentyp der Elemente.
Vorhanden bei: Deques, Listen
void container::push_back(const T&
Fügt als letztes Element eine Kopie von wert ein.
wert)
T ist der Datentyp der Elemente.
Vorhanden bei: Vektoren, Deques, Listen
size_type container::erase(const T&
Entfernt bei assoziativen Containern die Elemente
wert)
mit dem Wert wert. Für das entfernte Element
wird dessen Destruktor aufgerufen.
Vorhanden bei: Sets, Multisets, Maps, Multimaps.
iterator container::erase(iterator
Entfernt das Element an der Position des Iterators
pos)
pos, liefert die Position des ursprünglichen
Nachfolgers des entfernten Elements bzw. end()
Für das entfernte Element wird dessen Destruktor
aufgerufen.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
iterator container::erase(iterator
Löscht die Elemente im bereich [anf,end), liefert
anf, iterator end)
die Position des ursprünglichen Nachfolgers des
letzten Elements bzw. end().
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps, Strings
void container::pop_front()
Entfernt das erste Element im Container.
Vorhanden bei: Vektoren, Deques, Listen
void container:: pop_back()
Entfernt das letzte Element im Container.
Vorhanden bei: Vektoren, Deques, Listen
void container::resize(size_type anz) Ändert die Anzahl der Elemente im Container auf
anz, wächst dadurch die Größe werden
252
Die Programmiersprache C++
zusätzliche Elemente mit dem Default-Konstruktor
erzeugt und am Ende angefügt. Schrumpft
dadurch die Größe, werden die hinteren Elemente
zerstört (mit Aufruf von deren Destruktoren).
Vorhanden bei: Vektoren, Deques, Listen
void container::resize(size_type anz, Ändert die Anzahl der Elemente im Container auf
T& wert)
anz. Wächst dadurch die Größe, werden
zusätzliche Elemente als Kopie von wert erzeugt
und am Ende eingefügt.
Vorhanden bei: Vektoren, Deques, Listen
void container::clear()
Leert den Container (löscht alle Elemente im
Container), für die zerstörten elemente werden
deren Konstruktoren aufgerufen, dadurch werden
alle Verweise auf die Elemente ungültig.
Vorhanden bei: Vektoren, Deques, Listen, Sets,
Multisets, Maps, Multimaps
5.2.1 Bitset
Der Header <bitset> definiert eine Template-Klasse und zugehörige Funktionen
zur Darstellung und Bearbeitung von Bitfolgen fester Größe
template <size_t N> class bitset;
5.2.2 Deque
Der Name „Deque“ ist die Abkürung für double ended queue. Darunter versteht
man eine Warteschlange, die das Hinzufügen und Entnehmen von Elementen
sowohl am Anfang als auch am Ende erlaubt.
template <class T> class deque;
Datentyp
pointer
const_pointer
reverse_iterator
const
reverse_iterator
Bedeutung
Zeiger auf Deque-Element
Zeiger auf konstantes Deque-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Abb.: Zusätzliche öffentliche Datentypen für deque
Die folgende Abbildung zeigt die Methoden einer Deque:
Methode mit Rückgabetyp
deque<T>()
deque<T>(const deque<T>&)
~deque<T>()
iterator begin()
const_iterator begin()
iterator end()
const_iterator end()
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
253
Die Programmiersprache C++
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>&)
Maximal mögliche Größe des Containers
Aktuelle Größe des Containers
size==0 bzw. begin() == end()
Vertauschen mit Argument-Container
Zuweisungsoperator
Operator ==
Operator !=
Operator <
Operator >
Operator <=
Operator <=
Abb.: Methoden der Klasse deque
5.2.3 List
Die Liste dieses Abschnitts ist eine doppelt-verkettete Liste, die das Hinzufügen und
Entnehmen von Elementen sowohl am Anfang als auch am Ende erlaubt. Jedes
Element in der Liste zeigt auf seinen Vorgänger und seinen Nachfolger. Dadurch ist
kein wahlfreier Zugriff mehr möglich.
template <class T> class list;
Datentyp
pointer
const_pointer
reverse_iterator
const
reverse_iterator
Bedeutung
Zeiger auf Listen-Element
Zeiger auf konstantes Listen-Element
Iterator für Durchlauf vom Ende zum Anfang
Iterator für Durchlauf vom Ende zum Anfang mit konstanten
Elementen
Abb. Zusätzliche öffentliche Datentypen für list
Die Klasse list stellt die folgende, spezielle Methoden zur Verfügung:
Methode mit Rückgabetyp
Bedeutung
void liste::unique()
Kollabiert Folgen von gleichen Elementen zu
void liste:.unique(BinaryBoolFunc op) einem Element, entfernt aus einer Liste jeweils
alle Elemente, die einem Element mit gleichem
Wert unmittelbar folgen bzw. bei denen
op(elem,folgeElem) wahr ist. Für entfernte
Elemente werden deren Destruktoren aufgerufen.
Diese Funktion ist die für Listen optimierte Version
des gleichnamigen Algorithmus.
254
Die Programmiersprache C++
void liste::splice(iterator pos,
liste& c)
void liste::splice(iterator pos,
liste& c, iterator cPos)
void liste::splice(iterator pos,
liste& c, iterator cAnf, iterator
cEnd)
void liste:.sort()
void liste::sort(CompFunc op)
void liste::merge(liste& c)
void liste::merge(liste& c, CompFunc
op)
void liste::reverse()
Verschiebt alle Elemente aus c in die Liste, für 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.
Verschiebt das Element aus c an der Position
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.
Verschiebt
alle
Elemente
des
Bereichs
[cAnf,cEnd) aus c in die Liste für die die
Funktion aufgerufen wird, vor das Element mit
Position pos. Bei c darf es sich um den gleichen
Container handeln. In diesem Fall darf pos nicht
im verschobenen Bereich liegen, und der Bereich
wird innerhalb des Containers verschoben. Falls c
ein anderer Container ist, wird c dadurch
entsprechend kleiner. Der Programmierer muß
sicherstellen, daß sich der verschobene Bereich
wirklich in c befindet. ansonsten ist das Verhalten
undefiniert.
Sortiert alle Elemente anhand < bzw. op. Op kann
optional zum Sortierem übergeben werden und
dient dazu, jeweils zwei Elemente zu vergleichen:
op(elem,elem).
Diese Fuktion ist die für Listen optimierte Version
der Algorithmen sort() und stable_sort().
Verschiebt alle Elemente aus der vorsortierten
Liste c in den vorsortierten Container, für den die
Funktion aufgerufen wird, so daß die gemeinsame
Menge ebenfalls sortiert ist.
Op kann optional übergeben werden und dient
dazu, jeweils zwei Elemente zu vergleichen:
op(elem,cElem), c wird dadurch geleert.
Diese Funktion ist die für Listen optimierte Version
des gleichnamigen Algorithmus.
Kehrt die Reihenfolge der Elemente in einer Liste
um. Diese Funktion ist die für Listen optimierte
Funktion des Algorithmus reverse().
Abb.: Spezielle Methoden der Klasse list
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[])
152
PR52301
255
Die Programmiersprache C++
{
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,
class T,
class Compare = less<Key>
// Schluessel
// Daten
/* Standardvergleich für
Sortierung */
class Allocator = allocator<T> >
class map;
Die Elemente einer Map oder Multimap können Wertepaare von zwei beliebigen
Typen Key und T sein.
Map und Multimap sind wie alle assoziativen Container-Klassen als balancierte
Bäume implementiert. Wegen der automatischen Sortierung und der
Dateiorganisation ergibt sich ein gutes Zeitverhalten beim Suchen und Finden von
Elementen.
Eine „map“ besitzt zusätzlich folgende Datentypen
256
Die Programmiersprache C++
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
iterator container::find(const T&
wert)
const_iterator container::find(const
T& wert) const
iterator container::lower_bound(const
T& wert)
const_iterator
container::lower_bound(const T& wert)
const
iterator container::upper_bound(const
T& wert)
const_iterator
container::upper_bound(const T& wert)
const
pair<iterator,iterator>
container::equal_range(const T& wert)
pair<const_iterator,const_iterator>
153
Bedeutung
Liefert die Anzahl der Elemente, die den Wert
wert besitzen.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, maps, Multimaps
Liefert die Position vom ersten Element, das einen
Wert gleich wert besitzt. Kann kein passendes
Element
gefunden
werden,
wird
end()
zurückgeliefert. Die Elementfunktion ist eine
Spezialversion des gleichnamigen Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert die erste Position, an der wert eingefügt
werden kann, ohne die Sortierung zu zerstören.
Dies ist gleichbedeutend mit dem ersten Element,
dessen Wert >= wert ist. Die Elementfunktion ist
eine
Spezialversion
des
gleichnamigen
Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert die letzte Position, an der wert eingefügt
werden kann, ohne die Sortierung zu zerstören.
Dies ist gleichbedeutend mit dem ersten Element,
dessen Wert > wert ist. Die Elementfunktion ist
eine
Spezialversion
des
gleichnamigen
Algorithmus.
T ist der Datentyp des sortierten Wertes:
- bei Sets und Multisets der Typ der Elemente
- bei Maps und Multimaps der Typ der
Schlüssel
Vorhanden bei: Sets, Multisets, Maps, Multimaps
Liefert ein Wertepaar mit der ersten und letzten
Position, an der wert eingefügt werden kann,
ohne die Sortierung zu zerstören. Dies ist
Elementfunktionen, die spezielle Implementierungen von Algorithmen für assoziative Container sind.
257
Die Programmiersprache C++
container::equal_range(const T& wert) gleichbedeutend mit dem Bereich der elemente,
const
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
value_compare container::value_comp() Liefert
die
Vergleichsfunktion
bzw.
das
Vergleichsobjekt, mit dem die Elemente verglichen
werden.
Vorhanden bei: Sets, Multisets, Maps, Multimaps
key_compare container::key_comp()
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.,:;\"{}-+&^%$#@!~'\\<|()[]<>*=";
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();
154
vgl. PR52410.CPP
258
Die Programmiersprache C++
w != worte.end(); w++)
cout << (*w).first << ": " <<
<< endl;
(*w).second.wert()
}
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
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.
155
Falls nichts anderes angegeben wird, wird eine deque verwendet.
259
Die Programmiersprache C++
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.
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
260
Die Programmiersprache C++
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äume156 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)
size_type erase(k)
void erase(p, q)
void clear()
value_compare value_comp() const
Bedeutung
Konstruktor, der ein Compare-Objekt akzeptieren
kann. Falls keins angegeben wird, wird
less<Key> angenommen.
Fügt den Schlüssel x ein, sofern er noch nicht
vorhanden ist. Der Iterator des zurückgegebenen
Paares zeigt auf den eingefügten Schlüssel bzw.
auf den schon vorhandenen Schlüssel mit
demselben Wert wie x. Der Warheitswert zeigt an,
ob überhaupt ein Einfügen stattgefunden hat
Löscht das Element, auf das der Iterator zeigt
Alle Elemente im Iteratorbereich p bis q löschen
Löscht alle Elemente
(dasselbe wie key_comp().
Bsp.: Sortieren der Wörter einer 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);
156
Der Standard legt nicht direkt fest, wie assoziative Container implementiert sind. Vielfach werden diese
Bäume als sog. „Red-Black-Trees“ implementiert.
157 PR52611.CPP
261
Die Programmiersprache C++
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"));
}
262
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);
263
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:
Datentyp
158
Bedeutung
PR52702.CPP
264
Die Programmiersprache C++
pointer
const_pointer
reverse_iterator
const
reverse_iterator
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
265
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.:
266
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
<string.h>
<algorithm>
<vector>
<stdlib.h>
<iostream.h>
int main()
159
160
PR53010.CPP
PR53011.CPP
267
Die Programmiersprache C++
{
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
++iter
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)
268
Die Programmiersprache C++
TYP()
TYP(iter)
iter1=iter2
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)
// gibt den Abstand zwischen zwei Iteratoren zurueck
// last muss von first aus erreichbar sein
template <class InputIterator>
typname iterator_traits<InputIterator>::difference_type
269
Die Programmiersprache C++
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
Es gibt drei Arten von Insert-Operatoren
Name
Back-Inserter
Klasse
back-insert_iterator
Elementfunktion
push_back(wert)
270
Erzeugung
back_inserter(container)
Die Programmiersprache C++
Front-Inserter
Inserter
front_insert_iterator
insert_iterator
push_front(wert)
insert(pos,wert)
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.
iter++
Algorithmen
cin >> v
271
Die Programmiersprache C++
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
Bsp.161: „Sortieren der Standard-Eingabe mit anschließender Ausgabe auf die
Standard-Ausgabe“
#include <string.h>
161
PR53510.CPP
272
Die Programmiersprache C++
#include
#include
#include
#include
<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.
273
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
274
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);
275
Die Programmiersprache C++
search
Der Algorithmus durchsucht eine Sequenz, ob eine zweite Sequenz in ihr enthalten
ist. Es wird ein Iterator auf die Position innerhalb der ersten Sequenz
zurückgegeben, an der die zweite Sequenz beginnt, sofern sie in der ersten
enthalten ist. Andernfalls wird ein Iterator auf die last1-Position der ersten Sequenz
zurückgegeben
template <class ForwardIterator1, class ForwardIterator2>
ForwardIterator1 search(ForwardIterator1 first1,
ForwardIterator1 last1,
ForwardIterator1 first2,
ForwardIterator2 last2);
template <class ForwardIterator1, class ForwardIterator2,
class BinaryPredicate>
ForwardIterator1 search(ForwardIterator1 first1,
ForwardIterator1 last1,
ForwardIterator1 first2,
ForwardIterator2 last2,
BinaryPredicate binary_pred);
search_n
search_n() durchsucht eine Sequenz daraufhin, ob eine Folge von gleichen
Werten in ihr enthalten ist.
template <class ForwardIterator, class Size, class T>
ForwardIterator search-n(ForwardIterator first,
ForwardIterator last,
Size count,
const T& value);
template <class ForwardIterator, class Size, class T,
class BinaryPredicate>
ForwardIterator search_n(ForwardIterator first,
ForwardIterator last,
Size count,
const T& value,
BinaryPredicate binary_pred);
copy und copy_backward
copy() kopiert die Elemente eines Quellbereichs in den Zielbereich, das Kopieren
beginnt am Anfang bzw. am Ende (mit copy_backward()). Falls der Zielbereich
nicht überschrieben, sondern in ihn eingefügt werden soll, ist als Output-Iterator ein
Iterator zum Einfügen zu nehmen.
template <class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first,
InputIterator last,
OutputIterator result);
template <class BidirectionalIterator1, class BidirectionalIterator2>
276
Die Programmiersprache C++
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);
replace
277
Die Programmiersprache C++
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);
unique
unique() löscht gleiche aufeinanderfolgende Elemente bis auf eins.
278
Die Programmiersprache C++
template <class ForwardIterator>
ForwardIterator unique(ForwardIterator first,
ForwardIterator last);
template <class ForwardIterator, class BinaryPredicate>
ForwardIterator unique(ForwardIterator first,
ForwardIterator last,
BinaryPredicate binary_pred);
template <class InputIterator, class OutputIterator>
OutputIterator unique(InputIterator first,
InputIterator last,
OutputIterator result);
template <class InputIterator, class OutputIterator, class BinaryPredicate>
OutputIterator unique_copy(InputIterator first,
InputIterator last,
OutputIterator result
BinaryPredicate binary_pred);
reverse
reverse() dreht die Reihenfolge der Elemente einer Sequenz um.
template <class BidirectionalIterator>
void reverse(BidirectionalIterator first,
BidirectionalIterator last);
template <class BidirectionalIterator, class OutputIterator>
OutputIterator reverse_copy(BidirectionalIterator first,
BidirectionalIterator last,
OutputIterator result);
rotate
rotate() verschiebt die Elemente einer Sequenz nach links, die vorne
herausfallenden Elemente werden hinten wieder eingefügt.
template <class ForwardIterator>
void rotate(ForwardIterator first,
ForwardIterator middle,
ForwardIterator last);
random_shuffle
random_shuffle() dient zum Mischen der Elemente einer Sequenz, also zur
zufälligen Änderung ihrer Reihenfolge. Die Sequenz muß Random-Access-Iteratoren
zur Verfügung stellen, z.B. vector oder deque
template <class RandomAccessIterator>
void random_shuffle(RandomAccessIterator first,
RandomAccessIterator last);
template <class RandomAccessIterator, class RandomNumberGenerator>
void random_shuffle(RandomAccessIterator first,
RandomAccessIterator last
279
Die Programmiersprache C++
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;
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;
162
vgl. PR52801.CPP
280
Die Programmiersprache C++
while (cin >> iwert, cin.good())
{
v.push_back(iwert);
cout.width(6);
cout << anzN << ": " << v[anzN++] << endl;
}
if (anzN)
{
sort(v.begin(), v.end());
for (vector<int>:: const_iterator viter = v.begin();
viter != v.end(); viter++)
cout << *viter << " ";
cout << endl;
}
}
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);
template <class ForwardIterator, class T, class Compare>
ForwardIterator lower_bound(ForwardIterator first,
ForwardIterator last,
const T& wert,
Compare comp);
upper_bound
281
Die Programmiersprache C++
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.
Mengenoperationen auf sortierten Strukturen
Die folgenden Algorithmen beschreiben die grundlegenden Mengenoperationen wie
Vereinigung, Durchschnitt usw. auf sortierte Strukturen
includes
set_union
282
Die Programmiersprache C++
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.
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);
283
Die Programmiersprache C++
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.
284
Die Programmiersprache C++
5.5 Nationale Besonderheiten
Die Klasse locale (Header <locale>) bestimmt die nationalen Besonderheiten
von Zeichensätzen.
5.6 Die numerische Bibliothek
5.6.1 Komplexe Zahlen
Komplexe Zahlen werden im Header <complex> durch spezialisierte Templates für
float, double und long double realisiert.
5.6.2 Grenzwerte von Zahlentypen
5.6.3 Numerische Algorithmen
accumulate
Der Algorithmus addiert auf einen Startwert alle Werte *i eines Iterators i von
first bis last.
template <class InputIterator, class T>
T accumulate(InputIterator first,
InputIterator last,
T init);
template <class InputIterator, class T, class binaryOperation>
T accumulate(InputIterator first,
InputIterator last,
T init,
binaryOperation binOp);
inner_product
Der Algorithmus addiert das Skalarprodukt zweier Container u und v, die meistens
Vektoren sein werden, auf den Anfangswert init: Ergebnis = init +  u i  vi
i
template <class InputIterator, class InputIterator , class T>
T inner_product(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
T init);
285
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);
286
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
287
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>
288
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++.
Anwendungsgerüst
289
Die Programmiersprache C++
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.
Hinweise
290
Die Programmiersprache C++
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:
291
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.
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
163
vgl. Pr61310
292
Die Programmiersprache C++
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.
293
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.
294
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.
295
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.
296
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.
297
Die Programmiersprache C++
13. Gestaltung des Anwendungsfensters (Layout für das Anwendungsdialogfeld)
1. Aktivieren im Arbeitsbereich die Registerkarte Ressourcen
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).
298
Die Programmiersprache C++
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 ObjektIDs ausgewählt, wie die folg. Abb. zeigt:
299
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.
300
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.
301
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.
302
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.
303
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.
304
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,
305
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
306
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
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
307
Die Programmiersprache C++
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
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.
167
168
vgl. 6.4.1.2
vgl. 6.1.3
308
Die Programmiersprache C++
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
übergeben. Mauskoordinaten werden für WM_LBUTTONDOWN der Behandlungsroutine
OnLButtonDown() als CPoint-Parameter übergeben.
4. Einsetzen von Code in die Behandlungsroutine. Der Anwender wird über das Ereignis „Drücken der
linken Maustaste“ über ein Meldungsfenster170 informiert.
169
170
vgl. Pr62410
vgl. 6.2.8.1
309
Die Programmiersprache C++
///////////////////////////////////////////////////////////////////////////
// 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
Abb.: Vom Assistenten erstelltes Anwendungsgerüst
310
Die Programmiersprache C++
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.
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:
311
Die Programmiersprache C++
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.
Abb.: Der Symbolleisten-Editor
Aktion
Neue Schaltfläche anlegen
Schaltflächen bearbeiten
Schaltflächen umordnen
Schaltflächen löschen
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.
312
Die Programmiersprache C++
Leerräume einfügen
Ressourcen-ID definieren
Hilfetexte definieren
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
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().
313
Die Programmiersprache C++
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.:
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.
314
Die Programmiersprache C++
Sammlungen (Container) von Strings können in den MFC-Klassen CStringArray
oder CStringList verwaltet werden.
CFile
unterstützt nur binäre, ungepufferte Operationen 171. Die abgeleitete Klasse
CStudioFile ermöglicht gepuffertes Lesen und Schreiben von Binär- und
Textdateien. Alterantiv können die ANSI-C++-Datenstreamklassen verwendet
werden.
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.
171
vgl. 6.2.8.2
315
Die Programmiersprache C++
virtual void Write(const void* lpBuf, UINT nCount);
lpBuf …
nCount ...
ist ein Zeiger auf den Speicherbereich, in dem die auszugebenden Daten stehen.
ist die Anzahl Bytes, die ausgegeben werden.
Fehlerbehandlung. Einige CFile-Methoden (z.B. Open()) zeigen aufgetretene
Fehler in ihren Rückgabewerten an. Andere Funktionen lösen eine Ausnahme vom
Typ CFileException aus.
6.2.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.
316
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,
HANDLE hInstance,
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
Handle der Anwendung
vgl. CWnd::ShowWindow() in OnLine-Hilfe
317
Die Programmiersprache C++
LPVOID lpParam
);
// 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);
318
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)
319
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.
Windows
Anwendung
320
Die Programmiersprache C++
Mausbewegung
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.
// Fensterfunktion WinProc
LRESULT CALLBACK WndProc(HWND hWnd, UINT uiMessage,
WPARAM wParam, LPARAM lParam)
321
Die Programmiersprache C++
{
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.
322
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.
Fensterfunktion und Antworttabellen
323
Die Programmiersprache C++
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 case-Blö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
aus dem Makronamen, indem man sämtliche Unterstriche und das Präfix WM
324
Die Programmiersprache C++
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.
Aufruf von API-Funktionen
325
Die Programmiersprache C++
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.
326
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.
327
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,
328
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.
329
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..
330
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.
331
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
Der ClassWizzard ist das wichtigste Werkzeug zum Umgang mit Nachrichten und
Nachrichten-Funktionen.
Eine
über
den
Klassenassistent
erzeugte
174
bspw. die vom MFC-Anwendungsassistenten generierten Projekte
332
Die Programmiersprache C++
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
333
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&);
};
5. „Student.cpp“ umfaßt dann:
334
Die Programmiersprache C++
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.
ActiveX-Ereignisse
176
nur interessant, wenn bestimmte Methoden und Eigenschaften der vorliegenden Klasse anderen Programmen
zur Verfügung gestellt werden sollen.
335
Die Programmiersprache C++
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.
Wichtige Funktionen aus den beiden Klassen sind:
CView::GetDocument()
// GetDocument() liefert einen Zeiger auf ein Dokumentobjekt, das mit der Ansicht assoziiert
336
Die Programmiersprache C++
// 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.
337
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.
338
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;
}
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
339
Die Programmiersprache C++
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;
}
}
340
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.
341
Die Programmiersprache C++
Abb.: Anpassung der Titelleiste
342
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.
343
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;
// Deaktivieren Minimieren-Schaltfläche
// cs.style ^=WS_MINIMIZEBOX; // Deaktivieren Minimiere
return TRUE;
}
344
Die Programmiersprache C++
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.
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.
345
Die Programmiersprache C++
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.
346
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.
347
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)
Bsp.: Kreise an Mausklickposition zeichnen
348
Die Programmiersprache C++
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);
}
}
349
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.
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
350
Die Programmiersprache C++
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.
6.4.4 Die Sammlungsklassen der MFC
351
Die Programmiersprache C++
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.
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:
179
Stammen aus früheren Versionen der MFC-Bibliothek
352
Die Programmiersprache C++
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);
}
Die Header-Datei des Dokuments kann folgende Deklaration aufnehmen:
// Attribute
public:
int anzahl; // Index des Datenfelds
StudentList* starray; // Zeiger auf (den Typ der) Datensaetze
353
Die Programmiersprache C++
// 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);
}
}
}
354
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.
355
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);
BOOL LineTo(int x, int y);
BOOL LineTo(POINT point);
Bögen
BOOL Arc(int x1, int y1, int x2, int y2, int x3,
int y3, int x4, int y4);
BOOL Arc(lpRect, Point ptStart, POINT ptEnd);
Rechtecke
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.
Zeichnet einen elliptischen Bogen. Der Bogen wird
spezifiziert durch ein umgebendes Rechteck,
sowie einen Anfangs- und Endpunkt (die nicht
unbedingt auf dem Bogen liegen müssen)
Position und Abmessung des zu zeichnenden
Rechtecks werden durch Angabe der oberen
356
Die Programmiersprache C++
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);
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
BOOL Chord(int x1, int y1, int x2, int y2, int x3,
int y3, int x4, int y4);
BOOL Chord(LPCRECT lpRect, POINT ptStart,
POINT ptEnd);
BOOL Ellipse(int x1, int y1, int x2, int y2);
BOOL Ellipse(LPRect lpRect);
Zeichnet ein Ellipsensegmet (Schnittfigur einer
Ellipse mit einer Linie). Übergeben werden das
umschließendeRechteck
sowie
Startund
endpunkt der schneidenden Linie
Zeichnet eine Ellipse, die das übergebene
Rechteck ausfüllt. Ist das Rechteck ein Quadrat,
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
357
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.
Einrichten von GDI-Objekten
358
Die Programmiersprache C++
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ätekoordinaten 182 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.
359
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.
360
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 X-Server
an die Clients geschickt werden.
Ein spezieller X-Client ist der Window-Manager. Er verwaltet die einzelnen
Anwendungen auf dem Bildschirm. Der Window-Manager übernimmt die
Fensterverwaltung und die Gestaltung der Benutzeroberfläche. So erlaubt er das
Verschieben von beliebigen Fenstern (mit der Maus) oder das Vergrößern und
Verkleinern und sorgt auch dafür, daß jedes Fenster einen Rahmen und Titelbalken
bekommt. Über eine Parameter-Datei weiß bspw. der Window-Manager, wie die
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
361
Die Programmiersprache C++
Bedienoberfläche (Farben, Schriften, ...) aussehen soll. Nach diesen Vorgaben
initialisiert er den Bildschirm.
Wichtige Window-Manager sind: twm, olwm, mwm
Der Anwender sucht sich den Window-Manager aus, der seinen Bedürfnissen 188 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.
362
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.
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
189
190
363
Die Programmiersprache C++
8.1.3 Ein X-Programm
Das Programm zeichnet eine Hilbert-Kurve in ein von X bereitgestelltes Fenster und
zeigt Befehle, die praktisch in jedem X-Programm vorkommen.
1. Include-Dateien
Die komplexen X-Datenstrukturen werden zu Beginn des Programms über
„include“-Anweisungen bekannt gemacht
/* 2*/ #include <X11/Xlib.h> // Definition von X-Datenstrukturen
/* 3*/ #include <X11/Xutil.h> //
"
"
"
2. Kontaktaufnahme zum Server der lokalen Maschine
XOpenDisplay ist der allererste X-Befehl. Er bezieht sich im Programm auf eine
Benutzervariable mydisplay (Zeiger auf eine X-Struktur vom Typ Display)
/* 7*/ Display
*mydisplay; // Basisstruktur fuer Serververbindung
...............
/*80*/
mydisplay = XOpenDisplay("");
/*81*/
myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr
XOpenDisplay stellt die Verbindung mit dem X-Server her. ““ (Leerstring) zeigt an,
daß der Server auf dem gleichen (lokalen) Rechner benutzt werden soll, auf dem
auch der Client läuft. Kommt es zu keinem Kontakt, wird NULL zurückgegeben.
3. Definition des Fenster
Mit XCreateSimpleWindow wird ein (einfaches) Fenster erzeugt, aber noch nicht
angezeigt.
/* 7*/
/* 8*/
/*71*/
/*75*/
/*84*/
/*85*/
/*86*/
/*87*/
Display
*mydisplay; // Basisstruktur fuer Serververbindung
Window
mywindow;
// Window-ID
int
myscreen;
unsigned long
black, white;
mywindow
= XCreateSimpleWindow(mydisplay,
DefaultRootWindow(mydisplay),
100, 300,640, 480, 17,
black, white); // Definition Fenster
DefaultRootWindow bestimmt die Position des neuen Fensters in der WindowHierarchie. Die darauf folgenden Argumente sind Integer-Größen und beschreiben
die Ursprungs-Koordinaten des Fensters. (0,0) liegt am Bildschirm links oben.Die
folgenden zwei Integer-Größen definieren die Größe des Fensters in Pixel. Es folgt
die Randbreite in Pixel. Die beiden letzten Argumente enthalten Kennziffern für die
Farbe des Rands und des Fenster-Hintergrunds.
4. Hinweise an den Window-Manager
364
Die Programmiersprache C++
Informationen über jedes Fenster kann der Window-Manager über eine Struktur im
X-Server mit dem Namen Properties erhalten, die jeder Client abfragen kann. Zwei
solche Properties sind:
/*88*/
/*89*/
XStoreName(mydisplay, mywindow, "Graphik");
XSetIconName(mydisplay, mywindow, "Draw");
„Graphik“ ist der „window-name“, „Draw“ ist der „icon-name“.
5. Anzeigen des Fenster
Das durch XCreateSimpleWindow definierte Fenster wird mit XMapRaised
angezeigt.
/*97*/
XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster
6. Ende der X-Sitzung
// Schliessen des Fenster
/*116*/
XDestroyWindow(mydisplay, mywindow);
// Beenden des Server-Kontakts
/*117*/
XCloseDisplay(mydisplay);
7. Setzen der Eventmaske
Der Client muß (am besten vor dem Anzeigen des Fenster) eine Maske der Events
dem Server übermitteln, auf die er reagieren möchte. Zum Setzen der Event-Maske
dient der Befehl XSelectInput. Die verschiedenen Masken müssen bitweise in
folgender Form verknüpft werden:
/* 7*/ Display
*mydisplay; // Basisstruktur fuer Serververbindung
/* 8*/ Window
mywindow;
// Window-ID
/*94*/
// Setzen der Event-Maske
/*95*/
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
8. Eventbearbeitungsschleife
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.
365
Die Programmiersprache C++
Das ist das Programmstück, das nach Aufforderung durch den Server etwas in das
Fenster zeichnet, auf Tastendruck oder Mausklick reagiert. X-Routinen sollten immer
in einer Schleife mit folgendem Format angeordnet sein:
/* 7*/ Display
*mydisplay;
// Basisstruktur fuer Serververbindung
/*72*/
/*73*/
XEvent
int
myevent;
done;
/*98*/
/*99*/
/*100*/
/*101*/
/*102*/
/*103*/
/*104*/
/*105*/
/*106*/
/*107*/
/*108*/
/*109*/
/*110*/
/*111*/
/*112*/
/*113*/
// Eventbearbeitungsschleife
done = 0;
while (done == 0)
{ // Hole naechsten Eintrag aus dem Puffer
XNextEvent(mydisplay, &myevent);
switch(myevent.type)
{
case Expose:
if (myevent.xexpose.count == 0)
paint();
break;
case ButtonPress:
done = 1;
break;
}
}
Mit XNextEvent wird ein Event aus dem Puffer geholt bzw. gewartet bis einer
ankommt. „myevent.type“ zeigt auf den ersten Eintrag der Event-Struktur. In „switch“
sollten für alle möglichen bzw. erlaubten Events Reaktionen vorgesehen sein. Mit
XEventsQueued wird die Anzahl der Einträge in der Warteschlange ermittelt, z.B.:
Display
*mydisplay; // Basisstruktur fuer Serververbindung
int zahl;
zahl = XEventsQueued(mydisplay,0);
Zur Bearbeitung der Events in einer anderen Reihenfolge, wie sie in der
Warteschlange vorliegen, existieren eine ganze Reihe von Befehlen:
-
Mausknopf-Events
Falls eine beliebige Maustaste gedrückt bzw. losgelassen wird und sich der
Mauszeiger innerhalb des zugehörigen Fensters befindet, dann werden die
Events ButtonPress bzw. BottonRelease erzeugt. Die Eventstruktur liefert
(u.a):
-- myevent.xbutton.button(int)
(gibt an, welche Maustaste es war (links = 1 .. rechts = 3)
-- myevent.xbutton.x bzw. .y(int)
(gibt an, wo sich der Mauszeiger193 zum Zeitpunkt der Aktion befand)
-
Tastatur-Events
-
Expose-Event
So ein Event wird immer gesendet, falls die Anwendung den Fensterinhalt neu
zeichnen soll. Dies ist der Fall, wenn
193
in Pixel zum Window-Ursprung links oben
366
Die Programmiersprache C++
-- das Fenster gerade erzeugt wurde
-- eine Ikonisierung vorlag
-- das Fenster von einem fremden Fenster überdeckt wurde
-- das Fenster verkleinert / vergrößert wurde.
9. Erstellen von Grafik
Der grafische Kontext (graphical context, GC)
Im GC werdem Zeichenattribute festgelegt, z.B.:
-
Farbe für Vorder- und 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
black, white;
= BlackPixel(mydisplay, myscreen);
= WhitePixel(mydisplay, myscreen);
Kennt man die Nummern der Farben, kann man sie im GC eintragen mit
XSetForeGround bzw. XSetBackGround, z.B.
/*92*/
/*93*/
194
XSetForeground(mydisplay, mygc, black);
XSetBackground(mydisplay, mygc, white);
Es gibt weitere Attribute, z.B. das Muster für das Auffüllen von Flächen
367
Die Programmiersprache C++
2. Linienattribute
XSetLineAttributes bestimmt, wie Linien aussehen sollen:
unsigned int line_width;
int line_style;
XSetLineAttributes(mydisplay, mygc, line_width, line_style, 1, 0)
„line_width“ sollte für schnelles Zeichen immer 0 sein, dann wird die minimale
Linienbreite benutzt. „Zahlen > 0“ definieren die Breite in Pixel. Für „line_style“ kann
man die vordefinierten Konstanten LineSolid oder LineOnOffDash hernehmen.
Übersicht zu den Grafik-Routinen
XDrawPoint
XDrawPoints
XDrawLine
XDrawSegments
XDrawLines
XDrawArc
XDrawArcs
XFillArc
XFillArcs
XDrawRectangle
XDrawRectangles
XFillPolygon
XClearArea
XClearWindow
setzt einen Punkt
setzt mehrere Punkte
zeichnet eine Linie
zeichnet mehrere getrennte Linien
zeichnet eine verbundene Folge von Linien
zeichnet einen Bogen
zeichnet Bögen / Kreise (Umrisse)
füllt einen Bogen
füllt mehrere Bögen
zeichnet ein Rechteck
zeichnet mehrere Rechtecke
zeichnet ausgefüllte Fläche
löscht rechteckigen Bereich
löscht das Fenster
Abb. 6.1-2: Einfache Grafik-Routinen aus der XLib
-
Zu allen 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.
Ein vollständiges Demonstrationsprogramm: Hilbert-Kurve
/*
/*
/*
/*
/*
/*
1*/
2*/
3*/
4*/
5*/
6*/
#include
#include
#include
#include
#include
<iostream.h>
<X11/Xlib.h> // Definition von X-Datenstrukturen
<X11/Xutil.h> //
"
"
"
<stdlib.h>
<math.h>
195
Die Versorgung der Funktionen mit Parametern, die Wirkungsweise dieser Funktionen sind umfassend in den
man-Pages dokumentiert.
368
Die Programmiersprache C++
/* 7*/
/* 8*/
/* 9*/
/*10*/
/*11*/
/*12*/
/*13*/
/*14*/
/*15*/
/*16*/
/*17*/
/*18*/
/*19*/
/*20*/
/*21*/
/*22*/
/*23*/
/*24*/
/*25*/
/*26*/
/*27*/
/*28*/
/*29*/
/*30*/
/*31*/
/*32*/
/*33*/
/*34*/
/*35*/
/*36*/
/*37*/
/*38*/
/*39*/
/*40*/
/*41*/
/*42*/
/*43*/
/*44*/
/*45*/
/*46*/
/*47*/
/*48*/
/*49*/
/*50*/
/*51*/
/*52*/
/*53*/
/*54*/
/*55*/
/*56*/
/*57*/
/*58*/
/*59*/
/*60*/
/*61*/
/*62*/
/*63*/
/*64*/
/*65*/
/*66*/
/*67*/
/*68*/
/*69*/
/*70*/
/*71*/
/*72*/
/*73*/
Display
Window
GC
*mydisplay;
mywindow;
mygc;
// Basisstruktur fuer Serververbindung
// Window-ID
#define WIDTH 640.0
#define HEIGHT 480.0
int i, stufe;
// x1, y1 funktioniert nicht,
// weil bereits in LIB. libm.a verwendet. */
float x_1, x_2, y_1, y_2, r, r_1, r_2, h;
void generiere( float r_1, float r_2)
{
stufe--;
if (stufe > 0)
generiere(r_2,r_1);
x_2 += r_1; y_2 += r_2;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(r_1,r_2);
x_2 += r_2; y_2 += r_1;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(r_1,r_2);
x_2 -= r_1; y_2 -= r_2;
XDrawLine(mydisplay, mywindow, mygc, (int)(x_1+320),
(int)(240-y_1),
(int)(x_2+320),
(int)(240-y_2) );
x_1 = x_2; y_1 = y_2;
if (stufe > 0)
generiere(-r_2, -r_1);
stufe++;
}
void paint(void)
{
h = 2;
i = stufe;
while (i > 1)
{
h *= 2;
i--;
}
r = 400 / h;
x_1 = -200; x_2 = -200;
y_1 = -200; y_2 = -200;
generiere(r, 0);
}
main()
{
int
XEvent
int
myscreen;
myevent;
done;
369
Die Programmiersprache C++
/*74*/
/*75*/
/*76*/
/*77*/
/*78*/
/*79*/
/*80*/
/*81*/
/*82*/
/*83*/
/*84*/
/*85*/
/*86*/
/*87*/
/*88*/
/*89*/
/*90*/
/*91*/
/*92*/
/*93*/
/*94*/
/*95*/
/*96*/
/*97*/
/*98*/
/*99*/
/*100*/
/*101*/
/*102*/
/*103*/
/*104*/
/*105*/
/*106*/
/*107*/
/*108*/
/*109*/
/*110*/
/*111*/
/*112*/
/*113*/
/*114*/
/*115*/
/*116*/
/*117*/
/*118*/
/*119*/ }
/*120*/
unsigned long
int
black, white;
i;
cout << "Stufen: "; cin >> stufe;
// X-Sitzung eröffnen
mydisplay = XOpenDisplay("");
myscreen = DefaultScreen(mydisplay); // Bildschirm-Nr
black
= BlackPixel(mydisplay, myscreen);
white
= WhitePixel(mydisplay, myscreen);
mywindow
= XCreateSimpleWindow(mydisplay,
DefaultRootWindow(mydisplay),
100, 300,640, 480, 17,
black, white); // Definition Fenster
XStoreName(mydisplay, mywindow, "Graphik");
XSetIconName(mydisplay, mywindow, "Draw");
// Erzeugen "graphical context"
mygc = XCreateGC(mydisplay, mywindow, 0, 0);
XSetForeground(mydisplay, mygc, black);
XSetBackground(mydisplay, mygc, white);
// Setzen der Event-Maske
XSelectInput( mydisplay, mywindow,
ButtonPressMask | ExposureMask);
XMapRaised(mydisplay, mywindow); // Anzeigen des Fenster
// Eventbearbeitungsschleife
done = 0;
while (done == 0)
{ // Hole naechsten Eintrag aus dem Puffer
XNextEvent(mydisplay, &myevent);
switch(myevent.type)
{
case Expose:
if (myevent.xexpose.count == 0)
paint();
break;
case ButtonPress:
done = 1;
break;
}
}
// Schliessen der Fenster
XFreeGC(mydisplay, mygc);
XDestroyWindow(mydisplay, mywindow);
XCloseDisplay(mydisplay); // Beenden des Server-Kontakts
exit(0);
370
Die Programmiersprache C++
8.2 OSF/Motif
8.2.1 Einführung in OSF/Motif, Xt Intrinsics
Im Mai 1988 schlossen sich sieben verschiedene Hardwarehersteller zur Open
Software Foundation (OSF) zusammen. Ziel dieses Zusammenschlusses war es
eine UNIX Bedienoberfläche zu erschaffen. Sie wurde Mitte 1989 unter dem Namen
OSF/Motif bereitgestellt. Zu OSF/Motif gehören:
-
Das OSF/Motif Widget-Set
Der Motif Window-Manager (mwm)
Die User Interface Language (UIL)
Ein Styl Guide mit Stil-Richtlinien für Motif-Oberflächen
Da Motif auf dem X-Window System aufsetzt, werden die sowohl die Xlib als
auch die Xt Intrinsics in Programmen verwendet. Zur Unterscheidung welche
Bibliothek eine Funktion oder einen Datentyp definiert, einigte man sich auf folgende
Namenskonvention:
-
Alle Xlib-Funktionen und Xlib-Strukturtypen beginnen (bis auf wenige Ausnahmen) mit einem
großen X
Alle Xt Intrinsics-Funktionen und Strukturtypen beginnen (bis auf wenige Ausnahmen) mit Xt
Alle Motif Bezeichnungen beginnen mit Xm
Durch das Zusammenspiel von Motif, Xlib und Xt Intrinsics stehen dem Programmierer mehrere fundamentale Datentypen zur Verfügung:
Xlib Datentyp
Display
Font
GC
Bedeutung
Display- Beschreibung
Zeichensatz
Umgebung für
Zeichenoperationen
Muster beim X-Server
Fenster beim X-Server
universeller Zeiger
Art
Struktur
ID
Zeiger
Xt Datentyp
Boolean
Bedeutung
Art
char bis long
Cardinal
Dimension
Pixel
Position
String
Widget
WidgetClass
XmString
Laufindex
Größenangabe
Farbwert
Koordinate
Zeichenfolge
Fensterbeschreibung beim XClient
Fenstertypbeschreibung
String mit Formatierung
XtPointer
universeller Zeiger
Pixmap
Window
XPointer (seit X11R4)
Wahrheitswert196
ID
ID
char*
unsigned int
int
unsigned long
short / int
char*
Zeiger
Zeiger
CompoundStrin
g
void*
Der hier interessanteste Datentyp ist der Xt Datentyp „Widget“ bzw. „WidgetClass“ .
Die Instanzen des Datentyps Widget beschreiben immer einen Bereich des
196
Für den Datentyp Boolean sind die Konstanten „True“, „TRUE“ (!= 0) „False“, „FALSE“ (== 0) vordefiniert
371
Die Programmiersprache C++
Bildschirms. Ein Widget kann zur Darstellung von Text, zur Darstellung einer
Menüleiste oder zur Einteilung des Bildschirms benutzt werden. Man unterscheidet
verschiedene Arten von Widgets:
-
-
-
Display-Widgets (Primitive Widgets)
stellen ein konkretes Bedienelement dar, das man auf dem Bildschirm sieht. Sie besitzen
Ausgabefunktionalität, zu der im allg. auch eine Eingabefunktionalität gehört. Sie können keine
„Kind“-Widgets besitzen.
Typische Beispiele: Buttons, Menüpunkte, Scrollbars oder Widgets zum Editieren von Text.
Container-Widgets
verwalten das geometrische Layout (Größe und Position) der ihnen untergeordneten („Kind-“)
Widgets.
Shell-Widgets
sind eine spezielle Form von Container-Widgets und übernehmen die Kommunikation mit der
Außenwelt (speziell mit dem Window-Manager). Ihre Fenster sind die Toplevel-Fenster der XAnwendung.
Eine typische X-Anwendung besteht also aus einer baumartigen Hierarchie von
Widgets. das oberste Widget eines solchen Widget-Baums ist ein Shell-Widget.
darunter liegen Container-Widgets, die das Layout einer Anwendung immer weiter
aufteilen. Die Blätter des Widgetbaums sind schließlich Display-Widgets, die die
eigentliche Schnittstelle zu den Funktionen der Anwendung bilden.
8.2.2 Struktur eines Intrinsics-Programmes
Programmrumpf von Intrinsics-Programmen
Im Prinzip sieht der Programmrumpf bei allen Intrinsics-Programmen gleich aus:
-
Einbinden der Headerdateien für Motif ( <Xm/XmAll.h> bindet alle erforderlichen Dateien ein)
Einlesen der Fallback-Ressourcen
Programminitialisierung (meist mit XtAppInitialize())
Erzeugen und ausgeben eines Widgetbaums auf der erhaltenen Applikations-Shell
Eintritt in „Endlosschleife“ zur Ereignissbehandlung (mit XtAppMainLoop())
Das Aussehen der Oberfläche und deren Funktionalität legt allein der Widgetbaum
fest. Die Wurzel dieses Baumes ist immer die Application-Shell. Sie ist bereits ein
Widget, das durch die Funktion XtAppInitialize() Gestalt erhält.
Die Hauptschleife zur Ereignisbehandlung sendet eintreffende X-Events an die
zugehörigen Widgets, wo sie entsprechend der Funktionalität des Widgets
weiterverarbeitet werden, z.B. unter anderem Callbacks auslösen.
Beispiel eines Instrinics-Programmes
// Allgemeine Header-Dateien einbinden
#include<stdio.h>
372
Die Programmiersprache C++
#include<Xm/XmAll.h>
// Beginn des Hauptprogrammes
main(int argc, char* argv[])
{
XtAppContext
kontext;
Widget applShell, text;
// Programminitialisierung und ApplicationsShell erzeugen
applShell = XtAppInitialize (&kontext, "Beispiel1", (XrmOptionDescRec*)
NULL, 0, &argc, argv, (String) NULL, (Arg*) NULL, 0);
// Widgetbaum erzeugen, hier nur ein Textwidget
text = XtCreateManagedWidget ("Beispieltext", xmLabelWidgetClass,
applShell,(Arg*) NULL, 0);
// Widgetbaum realisieren
XtRealizeWidget(applShell);
// Hauptschleife mit Ereignisbehandlung
XtAppMainLoop(kontext);
}
Widgets und Widgetbaum
Widgets können erzeugt, verwaltet (wird im Layout des Elternwidgets berücksichtigt),
realisiert (das zugehörige X-Fenster wird erzeugt) und ausgegeben (auf dem
Bildschirm „sichtbar“) werden. Dazu gibt es entsprechende Grundfunktionen:
Funktionsname
XtCreateWidget
XtManageChild
XtRealizeWidget
XtCreateManagedWidget
XtMapWidget
Auswirkung
liefert als Returnwert ein Widget
berücksichtigen des Kindes im Layout des Eltern Widgets
erzeugt X-Fenster zum Widget
erzeugt und managt ein Widget
macht ein Widget sichtbar
XtDestroyWidget
XtUnrealizeWidget
XtUnmanageChild
XtUnmapWidget
zerstört den Teilbaum
hebt die Realisierung wieder auf
hebt das Management des Widgets wieder auf
macht ein Widget unsichtbar
XtIsManaged
XtIsRealized
liefert zurück ob ein Widget gemanagt wird
liefert zurück ob ein Widget realisiert ist
Im Normalfall (falls keine anderen Ressourcen gesetzt wurden) wird ein Widget
sichtbar sobald es erzeugt, verwaltet und realisiert wurde. Selbstverständlich muß
natürlich das jeweilige Elternwidget auch sichtbar sein, und es darf nicht durch ein
Geschwisterwidget überdeckt werden.
Über Vererbungsmechanismen kann ein Widget verschiedene Eigenschaften (Ressourcen) vom jeweiligen Elternwidget erben.
Ressourcen und Callbacks
Jedes Widget besitzt Parameter, die dessen konkrete Eigenschaften definieren.
Diese Parameter nennt man Ressourcen.
Welche Ressourcen ein Widget besitzt richtet sich nach deren Widgetklasse, aber
alle Ressourcen der darüberliegenden Widgetklassen werden geerbt. So erhalten
373
Die Programmiersprache C++
alle Widgets die Ressourcen der Widgetklasse Core die als Basisklasse aller Widgets
dient. Die wichtigsten Ressourcen der Basisklasse Core sind:
Ressource-Konstante
XmNx
XmNy
XmNwidth
XmNheight
XmNbackground
XmNborderWidth
XmNscreen
XmNmappedWhenManaged
XmNcolormap
...
Datentyp
Position
Position
Dimension
Dimension
Pixel
Dimension
Screen*
Boolean
Colormap
...
Defaultwerte
0
0
Label-Breite
Label-Breite
verschieden
0
XtCopyScreen
True
XtCopyFromParent
...
Möglichkeiten zum Manipulieren von Ressourcen
Es gibt zwei Arten die Ressourcen von Widgets zu manipulieren:
-
Innerhalb des Programmes, diese können von außen nicht mehr geändert werden
Außerhalb des Programmes, diese können auch nach der Übersetzung des Programmes
überschrieben werden. Sie werden in spezielle Ressource-Dateien festgelegt, und zur Startzeit des
Programmes gelesen
Ressourcen-Dateien
Zum Setzen von Ressourcen außerhalb des Programmes dienen vor allem
Ressource-Dateien. Der Aufbauen einer solchen Datei muß einer speziellen Syntax
gehorchen: In jeweils einer Zeile einer Ressource-Datei wird festgelegt, in welchem
Programm, in welchen Widget, welche Ressource welchen Wert bekommt.
Ressource Zeilen haben folgenden Aufbau:
Programmname.Widgetpfad.Widget.RessourceKonstante:
Wert
Unter dem Begriff „Widgetpfad“ ist der Pfad im Widgetbaum gemeint um zu dem
speziellen Widget zu gelangen. Zu beachten ist auch, daß die Ressourcekonstante
ohne die Präfix „XmN“ angegeben wird.
Um zum Beispiel das Textwidget des Beispielprogrammes1 auf „Hello World“ zu
setzen, genügt in der Ressource-Datei folgende Zeile:
Beispiel1.text.labelString:
Hello World
Es besteht die Möglichkeit im „Widgetpfad“ Wildcards zu benutzen, um mehrere
Widgets gleichzeitig anzusprechen. Zum einen das Fragezeichen, das im Pfad
genau ein Widget ersetzt, zum anderen der Stern, der einen beliebigen Pfad
repräsentiert. Hierbei kann selbst der Programmname entfallen. Die RessourceZeile:
*text.labelString:
Hello World
374
Die Programmiersprache C++
setzt zum Beispiel in allen Programmen in denen die Ressource-Datei verwendet
wird, in jedem Widget mit dem Namen „text“ die Ressource „labelString“ auf
den Wert: „Hello World“.
Durch ein Ausrufezeichen in einer Ressource-Zeile kann Kommentar eingeleitet
werden, der bis zum Zeilenende geht. Ein Ausrufezeichen innerhalb eines
Ressource-Wertes ist allerdings Teil dieses Wertes.
Um nun diese Ressource-Dateien im Programm verwenden zu können, wird intern
eine Ressource-Datenbasis durch die Initialisierungsfunktion erzeugt. Nur wenn eine
Ressource für ein Widget benötigt wird, wird ermittelt welche dieser RessourceZeilen für das Widget von Bedeutung sind. Im allgemeinen gibt man solchen
Application-Defaults-Ressource-Dateien die Endung „ad“. Sie muß aber in ein
spezielles Directory für Ressource-Dateien z.B.: „usr/lib/X11/app-defaults“
und unter dem Klassennamen des Programmes z.B.: „Beispiel1.ad“ installiert
werden. Die Initialisierungsfunktion „XtAppInitialize“ füllt nun die Datenbasis mit
den Ressource-Zeilen der Datei auf. Dies geschieht durch die Angabe des
Klassennamens im zweiten Parameter.
Setzen von Ressourcen im Programm
Innerhalb eines Intrinsics-Programmes hat der Applicationsprogrammierer zwei
Möglichkeiten Widget-Ressourcen zu setzen. Zum einen bei der Erzeugung eines
Widgets, zum anderen durch eine spezielle Funktion. In allen Fällen muß als
Parameter ein Array von speziellen Strukturen, eine sogenannte Argument-Liste,
übergeben werden. Zum Auffüllen dieses Arrays empfiehlt sich ein vordefiniertes
Makro „XtSetArg“. Die Anwendung dieses Makros führt zu folgendem typischen
Aufbau beim Setzen von Widget-Ressourcen.
Arg
args[10];
Cardinal
n;
//Argument-Liste
//Laufvariable
n = 0;
XtSetArg(args[n], RessourceKonstante1, Wert1); n++;
XtSetArg(args[n], RessourceKonstante2, Wert2); n++;
Somit wird durch das Makro die Argument-Liste aufgefüllt, wobei die Ressource
„RessourceKonstante“ den Wert „Wert“ zugeordnet bekommt. Um diese Ressourcen
für ein Widget zu setzen gibt es wie schon erwähnt zwei Möglichkeiten:
Falls die Ressourcen beim Erzeugen des Widgets gesetzt werden sollen, werden die
letzten zwei Parameter der Funktion „XtCreateWidget“ benutzt:
widget = XtCreateWidget(....., args, n);
wobei „args“ den Zeiger auf die Argument-Liste repräsentiert, und „n“ den
Laufindex.
Falls die Ressourcen eines existierenden Widgets geändert werden sollen, wird die
Funktion „XtSetValues“ benutzt. Diese Funktion erwartet als Parameter auch die
schon beschriebene Argument-Liste und den Laufindex erwartet. So setzt:
XtSetValues(widget, args, n);
die Ressourcen des Widgets „Widget“, so wie in der Argument-Liste „args“
beschrieben.
375
Die Programmiersprache C++
Abfragen von Ressourcen
Für das Abfragen von Ressourcen gibt es ebenfalls eine spezielle Funktion
„XtGetValues“. Sie liest die Werte von Ressourcen über die schon erläuterte
Argument-Liste aus. Die einzelnen Argumente bestehen allerdings aus dem
Ressource-Namen (RessourceKonstante) und der Adresse der Variablen in der Wert
der Ressource eingetragen werden soll. Man kann also nicht mit der gleichen
Argument-Liste Ressourcen setzten und abfragen.
Zum Abfragen der Größe eines Widgets dienen z.B. folgende Zeilen:
Arg args[10];
Cardinal
n;
Dimension b;
Dimension h;
//Argumentliste
//Laufindex
//auszulesende Breite
//auszulesende Höhe
n = 0;
XtSetArg (args[n], XmNwidth, &b); n++;
XtSetArg (args[n], XmNheight, &h); n++;
XtGetValues (widget, args, n)
Callback-Ressourcen
Um in Intrinsics-Programmen eine Funktionalität implementieren zu können, gibt es
die Möglichkeit, einem Widget als Ressource Adressen von Funktionen zu
übergeben, die dann unter bestimmten Bedingungen aufgerufen werden. Man
spricht hierbei von ereignisorientierter Programmierung. Da dem Widget die Adresse
der Funktion übergeben wird nennt man solche Parameter auch Callbacks.
Callbackressourcen sind einfache Ressource-Konstanten. Sie hängen aber sehr eng
mit dem benutzen Widget zusammen. So hat ein Widget der Klasse
„XmPushButton“ eine RessourceKonstante „XmNactivateCallback“, mit denen
die Callbackfunkionen verbunden werden, die dann ausgeführt werden sollen, falls
der Button gedrückt und wieder losgelassen 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)
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.
197
Eine Auflistung der verschiedenen Callbackressourcen der verschiedenen Widgets folgt im Abschnitt
„OSF/Widget Set“.
376
Die Programmiersprache C++
Um in einem Programm einen Widget Exit-Button „exit“ mit einer ExitCallbackfunktion „exitCB“ zu verbinden benötigt man folgende Zeilen:
XtAddCallback(exit, XmNactivateCallback, exitCB, (XtPointer) NULL);
wobei im letzten Parameter die XtPointer Variable für die Übergabe an die
exitCB Funktion steht. Da sie hier nicht benutzt wird, wird durch einen Typcast der
NULL-Zeiger übergeben. In der Funktion „exitCB“ wird z.B. die exit() Funktion
aufgerufen, was zu einer ordentlichen Programmbeendung führt.
8.2.3 Das OSF/Widget Set
Kategorien
Widgets lassen sich gemäß ihres typischen Verwendungszweckes in folgende
Kategorien einteilen:
Display-Widgets haben die Eigenschaft, daß man sie auf dem Bildschirm „sehen“
kann. Veränderbare und konstante Texte, Knöpfe zum „draufdrücken“, Grafiken,
Listen usw. sind einige Vertreter dieser Gruppe. Jedes „Blatt“ des Widgetbaumes
muß folglich ein Display-Widget sein.
Container-Widgets dienen vor allem dem Zweck, das geometrische Layout ihrer
Kinder zu kontrollieren. Sie ordnen ihre Kind-Widgets in Zeilen und Spalten oder
untereinander an oder stellen Ressourcen zur Verfügung, durch die sich ein Kind
Widget abhängig von seinen Geschwistern ausrichten kann.
Shell-Widgets sind zwar ebenfalls Container, sie besitzen jedoch nur ein einziges
gemanagtes Kind-Widget. Eine der wesentlichen Aufgaben ist es, die Verbindung
mit der „Außenwelt“ herzustellen. Dies bedeutet, beispielsweise dem WindowManager Hinweise zu übermitteln, wie „wichtig“ die in der darunterliegenden
Widgethierarchie dargestellte Information ist, oder ob sie beispielsweise nur kurz
auf dem Bildschirm sichtbar sein wird.
Dialog-Widgets werden häufig dazu benutzt, relativ kurze Dialoge mit dem
Benutzer abzuwickeln, und werden deshalb nur bei Bedarf auf den Bildschirm
ausgegeben. Informationen, Fehlermeldungen, Dateinamen usw. können somit
ausgegeben bzw. erfragt werden. Dialog-Widgets sind Container und somit um
Kind-Widgets erweiterbar.
Überblick über alle Widgets
Die folgende Aufstellung zeigt die wichtigsten Widgetklassen unterteilt in Display,
Shell, Container Widgets:
DisplayWidgets:
WidgetKlassen-Name
Beschreibung
Motif-Version
377
Zeile im
Die Programmiersprache C++
XmArrowButton
XmScrollBar
XmLabel
XmList
XmSeperator
XmText
XmTextField
XmCsText
XmPushButton
XmToggleButton
XmDrawButton
XmCascadeButton
Bedienknopf mit Pfeil (Datensatznavigation)
Verschiebeleiste
Text oder Muster
Liste von auswählbaren Texten
Trennlinie
Text
einzeiliger Texteditor
ein oder mehrzeiliger Texteditor mit
Formatierungsmöglichkeit
Taster
Schalter
Button mit Bild
Menüpunkt mit Untermenue
1.0
1.0
1.0
1.0
1.0
1.0
1.1
2.0
Beispiel
76
-
1.0
1.0
1.0
1.0
241
237
wird nicht vom Windowmanager verwaltet
für Menüs
legt fest wie der Windowmanager mit einem
Widget umgehen soll
legt Merkmale im Zusammenspiel mit
Windowmanager fest
temporäre Dialoge mit dem Benutzer
temporäre Dialoge mit dem Benutzer
zur Verwendung mehrerer Widgetbäume
in ihr läuft der Hauptteil der Applikation ab
1.0
1.0
1.0
320
-
1.0
-
1.0
1.0
1.0
1.0
74
191
1.0
1.0
1.0
1.0
1.0
1.0
1.0
2.0
2.0
390
371
74
383
-
1.0
-
ShellWidgets:
OverrideShell
XmMenuShell
WmShell
VendorShell
TransientShell
XmDialogShell
TopLevelShell
ApplicationShell
ContainerWidgets:
XmDrawingArea
XmRowColumn
XmForm
XmMessageBox
XmFileSelectionBox
XmSelectionBox
XmScrolledWindow
XmSpinBox
XmNotebook
XmMainWindow
X-Window
Anordnung der Kinder in Zeilen und Spalten
Anordung der Kinder durch „berühren“
Nachrichten
Fenster mit Dateiauswahlmöglichkeit
Fenster mit Auswahlmögichkeit
Window mit Bildlaufleisten
zyklisches Durchlaufen einer Wertemenge
Simuliert eine Buch das seitenweise
durchgeblättert werden kann
typisches Hauptfenster, mit festen Plätzen
für Kinder
Die wichtigsten WidgetKlassen und ihre Ressourcen
In diesem Abschnitt werden die wichtigsten Widgetarten genauer erläutert,
insbesondere ihre speziellen Ressourcen aufgezeigt.
XmLabel-Widget
378
Die Programmiersprache C++
Eines der wichtigsten Display-Widgets ist die Klasse „XmLabel“. Sie werden benutzt
um Text darzustellen, ohne das hier eine Eingabe des Benutzers erwartet wird. Sie
dient aber auch als Oberklasse für alle „Button - Widgets“.
Der Text im Label selbst ist ein Compound-String, d.h. er kann formatiert werden.
Neben den Ressourcen der Oberklassen besitzt das Label-Widget folgende spezielle
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 Callback-Ressourcen
die genau steuern, wann welche Callback-Funktion auszulösen ist:
XmNactivateCallback:Diese Callbacks werden aufgerufen, wenn der Benutzer den
Mausknopf überhalb des Button-Widgets drückt, und dort auch wieder losläßt.
XmNarmCallback: werden aufgerufen wenn der Mausbutton innerhalb des Widgets
gedrückt wird.
XmNdisarmCallback:wird aufgerufen wenn der Mausbutton innerhalb des Widgets
gedrückt , aber irgendwo anders wieder losgelassen wurde
XmToggleButton-Widget
Widgets dieser Klasse sind Ein/Aus-Schalter. Sie können zwei verschiedene
Zustände anzeigen. Die Callback-Ressourcen sind dieselben wie beim PushButton,
nur
die
Ressource
XmNactivateCallback
hat
den
Namen
XmNvalueChangedCallback erhalten. Beim XmNToggleButton-Widget sind vor
allem die Ressourcen zur Darstellung der Zustände interessant:
XmNSet: ist vom Typ Boolean und gibt an ob gerade der Ein- oder der Auszustand
gewählt ist.
198 Neben diesen Ressourcen gibt es natürlich noch viele andere mehr, sie sind in der Fachliteratur
dokumentiert.
379
Die Programmiersprache C++
Es gibt auch hier noch viele weitere Ressourcen, unter anderem gibt es seit Motif 2.0
die Möglichkeit nicht nur zwei, sondern drei Zustände über einen Schalter zu
steuern. Ebenfalls ist es möglich Kombinationen von ToggleButtons zu erstellen, von
denen genau einer den Zustand „Ein“ haben kann.
XmCascadeButton-Widget
Ein XmCascadeButton-Widget hat normalerweise eine Menüleiste oder ein
anderes Menü als ElternWidget. Eine Aktivierung dieses Knopfes führt im
allgemeinen dazu, das eine Untermenü erscheint.
XmNsubMenuId: Diese Ressource erwartet die Angabe eines Widgets zur
Präsentation des Untermenüs, das bei Aktivierung aufgeklappt werden soll
XmText-Widget
Ein Widget dieser Klasse ist dazu gedacht Texte einzulesen, die der Anwender
eingibt. Dem Anwender kann man hierzu einen kleinen Texteditor zur Verfügung
stellen. Hier die wichtigsten Ressourcen eines XmText-Widgets:
XmNvalue: enthält den vom Anwender eingegebenen Text, ist aber kein CompoundString, sondern nur ein einfacher String
XmNeditMode: mit dieser Ressource kann man über die Werte
XmSINGLE_LINE_EDIT und XmMULTI_LINE_EDIT steuern wieviele Zeilen der
Text haben darf.
XmNeditable:gibt über True und False an, ob der Text editierbar ist.
Um weiterführende Editorfunktionen zu erhalten, gibt es seit Motif 2.0 die XmCSTextWidgets. Diese speichern den eingegebenen Text als Compound-String, deshalb
sind dort Textformatierungen möglich.
Nebenbei sei noch erwähnt das sich der kleine Texteditor zwischen Einfüge- und
Überschreibmodus mit der Funktion „toggle-overstrike()“ umschalten läßt.
XmList-Widget
In einem XmList-Widget können, untereinander in Zeilen angeordnet, Texte
aufgelistet werden, wobei mit der Maus eine oder mehrere Zeilen ausgewählt werden
können. Eine typische Verwendung ist dieses Widget in einer FileSelectionBox zur
Auswahl der Datei. Diesem Widget stehen sehr viele Ressourcen zur Verfügung,
hier die wichtigsten:
XmNitems: ist ein Zeiger auf eine Liste von Compound-Strings, die die Einträge der
Liste repräsentiert.
XmNitemCount: ist die Anzahl der Einträge in XmNitems
380
Die Programmiersprache C++
XmNvisibleItemCount: gibt an wieviele Einträge in der Liste sichtbar sein sollen, es
beeinflußt somit die Höhe des Widgets.
XmNselectionPolicy: über diese Ressource kann man steuern, ob Einzelauswahl
(XmSINGLE_SELECT) oder Mehrauswahl (XmMULTIPLE_SELECT) möglich ist
XmNselectedItems:ist ein XmStringTable der die ausgewählten Einträge enthält.
Im Zusammenhang mit diesem Widget gibt es noch einige Funktionen:
XmListAddItem: fügt einen Item in die Liste ein.
XmListDeleteItem: löscht einen Eintrag aus der Liste.
XmListSelectItem: selektiert einen Eintrag anhand seines Textes
XmListDeselectItem: hebt Selektierung des Eintrages wieder auf.
XmListItemExists: Liefert True falls der Text ein Eintrag in der Liste ist
XmCreateScrolledList: erzeugt Instanzen eines XmList-Widgets als Kind eines
ScrolledWindow-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.
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.
199 Die Syntax und Funktionsweise kann den man-Pages entnommen werden.
381
Die Programmiersprache C++
XmNdialogType: damit läßt sich steuern welche Kinder vorhanden sein sollen und
welche nicht. bei XmDIALOG_SELECTION werden alle Kinder erzeugt und verwaltet.
Bei XmDIALOG_WORK_AREA werden alle erzeugt, aber nur der Applybutton verwaltet.
Bei XmDIALOG_PROMT fehlen das List-Widget und dessen Überschrift. Wird häufig
für einzeilige Benutzereingaben verwendet.
Um andere Dialog-Typen zu erhalten, gibt es die Möglichkeit, über die Funktion
„XmSelectionBoxGetChild“ einzelne Kinder zu entfernen, bzw. umzuformen. Die
Kind-Komponente läßt sich über folgende Konstanten angeben:
XmDIALOG_APPLY_BUTTON
XmDIALOG_LIST
XmDIALOG_LIST_LABEL
XmDIALOG_TEXT
XmDIALOG_SELECTION_LABEL
für den Applybutton und analog für die anderen Buttons.
für das XmList-Widget
für die Überschrift des XmList-Widgets
für das Text-Widget
für die Überschrift des XmText-Widgets.
XmFileSelectionBox-Widget
Diese Widgetklasse erlaubt dem Benutzer das Navigieren durch Directories. Er kann
auch einen Pfadnamen angeben, und einen Filter aktivieren. Dieses Widget enthält
wiederum die Buttons für OK, Apply (Filtern), Cancel und Help. Die speziellen
Ressourcen sind:
XmNfileTypeMask: damit läßt sich steuern, ob in der Liste nur Dateien
(XmFILE_REGULAR) oder nur Directories (XmFILE_DIRECTORY) oder beides
(XmFILE_ANY_TYPE) zu sehen sein soll
XmNnoMatchString:Text der angezeigt werden soll, wenn es keine Datei oder
Directory gibt zu dem das angegebene Suchmuster paßt
Selbstverständlich steht auch hier eine Funktion zur Verfügung die einzelne KindWidgets ansprechbar macht. Die Funktion „XmFileSelectionBoxGetChild“
erfüllt diese Aufgabe. Die Kind-Widgets können über folgende Konstanten
angesprochen werden:
XmDIALOG_APPLY_BUTTON
XmDIALOG_FILTER_LABEL
XmDIALOG_FILTER_TEXT
XmDIALOG_DIR_LIST
XmDIALOG_LIST
XmDIALOG_TEXT
...
für den Applybutton und analog für die anderen Buttons.
für die Überschrift über dem Dateifilter
für den Dateifilter
ist das List-Widget für die Navigation
ist das List-Widget für das Ergebnis der Filterung
ist das Text-Widget, das die Benutzerauswahl aus den beiden
Listen enthält
XmMessageBox-Widget
Instanzen der Widgetklasse XmMessageBox können dazu benutzt werden um kurze
Dialoge mit dem Benutzer abzuwickeln. Eine MessageBox enthält die Buttons OK,
Cancel und Hilfe einen String mit der eigentlichen Nachricht, und ein Symbol für
Warnung, Information usw. Die neuen Ressourcen dieser Widgetklasse sind:
XmNokCallback: Der Callback für den Ok-Button. Analog für die anderen Buttons.
XmNokLabelSring: Die Beschriftung des Ok-Buttons. Analog für die anderen
Buttons.
XmNdialogType: Hiermit läßt sich ein Symbol auswählen das angezeigt werden soll:
XmDIALOG_MESSAGE
XmDIALOG_WARNING
für kein Symbol
für ein Ausrufezeichen
382
Die Programmiersprache C++
XmDIALOG_QUESTION
für einen Kopf mit Fragezeichen
XmDIALOG_INFORMATION für ein großes I
XmDIALOG_ERROR
für ein Stop Symbol
XmDIALOG_WORKING
für eine Sanduhr
XmNmessageString: Der Text der erscheinen soll
Durch die Angabe von XmDIALOG_TEMPLATE können Dialoganfragen selbst designt
werden.
Um auf die Ressourcen der Kind-Widgets zuzugreifen zu können müß auch hier eine
Funktion benutzt werden, die das Kind-Widget zurückliefert. Die Kind-Widgets
können über folgende Konstanten angesprochen werden:
XmDIALOG_OK_BUTTON für den Ok-Button und analog für die anderen Buttons.
XmDIALOG_MESSAGE_LABEL für das Text-Widget, das den Messagestring enthält.
XmRowColumn-Widget
Dieses Widget ist ein Container Widget. Es besitzt viele Ressourcen, die verschiedene Layout-Strategien für die Kind-Widgets steuern. Hier die wichtigsten LayoutRessourcen:
XmNorientation: durch die Angabe von XmHORIZONTAL oder XmVERTICAL läßt
sich steuern ob die Kind-Widgets untereinander oder nebeneinander angeordnet
werden sollen
XmNpacking: mit der Angabe von XmPACK_COLUMN erreicht man daß die Kinder in
gleiche große Boxen gesetzt werden. Typisch für Menüs. Durch die Angabe von
XmPACK_NONE behält jedes Kind seine Position wie in der Ressource XmNx und
XmNy angegeben
XmNnumColumns: gibt die Anzahl der Zeilen bei horizontaler Ausrichtung an.
XmNspacing: gibt die Dimension des Zwischenraumes, zwischen den Kind-Widgets
an
XmForm-Widget
Diese Container-Widgetklasse ermöglicht eine weitere Layoutstrategie. Diese
Strategie ermöglicht es den Kind-Widgets einen Nachbarn anzugeben, nach dem
sich das Widget zu orientieren hat. Dazu erhält das Kind-Widget folgende neue
Ressourcen:
XmNseiteAttachment: Bei Angabe von XmATTACH_FORM richtet es sich nach dem
betreffenden Rand des XmForm-Widgets aus. XmATTACH_WIDGET stellt eine
Verbindung zur entgegengesetzten Seite des in der folgenden Ressource
angegebenen Widgets her. XmATTACH_NONE läßt es ohne Ausrichtung.
XmNseiteWidget: Das Widget richtet sich nach dem hier angegebenen
Geschwister-Widget, falls XmATTACH_WIDGET oben angegeben wurde.
XmNrezisable: Diese Ressource vom Typ Boolean definiert, ob ein von ihm selbst
kommender Wunsch nach Größenänderung ausgeführt wird, oder nicht.
383
Die Programmiersprache C++
Hierbei steht „Seite“ für top, bottom, left oder right, denn alle vier Seiten eines
Widgets können von anderen Widget-Seiten abhängig gemacht werden.
Die Widgetklasse XmForm selbst, besitzt folgende neue Ressourcen:
XmNverticalSpacing: hier wird der Default-Abstand nach links und rechts zwischen
dem Widget und seinem Attachment angegeben
XmNhorizontalSpacing: hier wird der Default-Abstand nach oben und unten
zwischen dem Widget und seinem Attachment angegeben.
XmDrawingArea - Widget
Mit einem Widget dieser Klasse soll dem Applikationsprogrammierer eine Widgetart
bereitgestellt werden, die eigentlich nicht mehr Funktionalität besitzt als ein Window,
aber in die Widgethierarchie eingebunden ist. Hauptsächlich werden
XmDrawingAreas benutzt um Grafiken darzustellen. Die neuen Ressourcen dieser
Widgetklasse sind:
XmNexposeCallback: wird aufgerufen falls der Fensterinhalt neu ausgegeben
werden muß.
XmNresizeCallback: wird aufgerufen falls sich die Größe des Fensters ändert.
XmNinputCallback: wird aufgerufen falls Tastatureingaben oder Mauklicks
erfolgten.
XmNresizePolicy: diese Ressource definiert inwiefern das Widget von sich aus
seine Größe zu ändern versucht. XmRESIZE_NONE bedeutet, daß das Widget seine
Größe nicht von sich aus ändert. XmRESIZE_ANY veranlaßt das Widget seine Größe
so zu ändern, daß alle Kind-Widgets genau Platz haben. XmRESIZE_GROW führt zu
einem automatischen Wunsch nach Vergrößerung
Da für Zeichenoperationen und Textausgaben mit den Funktionen der Xlib als
Parameter eine Window-ID, ein Grafikkontext und ein Display nötig sind, stellt Motif
einige Funktionen zur Verfügung, durch die man diese Parameter erhalten kann:
XtDisplay(Widget) liefert das Display
XtWindow(Widget) liefert die Window-ID
XtGetGC(Widget, werteMaske, Werte) liefert den Grafikkontext
Für die Erzeugung des Grafikkontexts ist die Abfrage der Vorder- und
Hintergrundfarbe nötig. Diese Werte sind dann in die Wertestruktur einzutragen.
Diese Struktur ist vom Typ „XGCValues“ und enthält „foreground“ und „background“
vom Datentyp „Pixel“.
Menüleisten und Pulldown-Menüs
Das OSF/Motif Widget-Set besitzt verschiedene Widgetklassen, die die Verwendung
von Menüs in Programmen unterstützen. Folgende Widgetklassen besitzen spezielle
Funktionalitäten für Menüs.
384
Die Programmiersprache C++
XmRowColumn: Sie dienen grundsätzlich als Container-Widgets für Menüs.
XmCascadeButton: Haben die Aufgabe Menüs automatisch auszugeben.
XmMenuShell: Sind spezielle Shell-Widgets für Menüs, die als Popups ausgegeben
werden
Eine Standard-Anwendung von Menüs ist die Verwendung einer Menüleiste, in der
die einzelnen Menüpunkte Pulldown-Menüs ausgeben. Bei der Implementierung
eines solchen Menüpunktes muß nur angegeben werden, welcher Menüpunkt
welches Menü aktiviert. Alles weitere geschieht automatisch durch die Verwendung
des „XmCascadeButtons“. Dieses Widget besitzt dazu die Ressource
XmNsubMenuId in die das dazugehörige Container-Widget des dazugehörigen
Pulldown-Menüs eingetragen wird. Dieses Sub-Menu wird in Abhängigkeit von der
Menüart, in dem sich der Button befindet, aktiviert:
Befindet sich der CascadeButton in einer Menüleiste, wird das dazugehörige
Pulldown-Menü durch Drücken des Buttons aktiviert.
Befindet sich der CascadeButton in einem Pulldown-Menü, so wird das Sub-Menü
durch drüberstreichen mit der Maus ausgegeben. Auf diese Weise kann eine
Kaskade (geschachtelte Folge) von Menüs entstehen. Daher hat das Widget auch
seinen Namen.
Das Erzeugen einer Menüleiste mit Pulldown-Menüs geht denkbar einfach:
//Menüleiste erzeugen:
menueLeiste = XmCreateMenuBar(elternWidget, "Menüleiste",(Arg*) NULL,0);
//Menüleiste managen:
XtManageChild(menueLeiste)
//Pulldown-Menü erzeugen:
dateiMenue = XmCreatePulldownMenu (menueLeiste, "Datei", (Arg*) NULL,0);
//Menüpunkt innerhalb des Untermenüs erzeugen hier "Öffnen":
button = XtCreateManagedWidget ("Punkt", xmPushButtonWidgetClass,
dateiMenue, (Arg*)
NULL,0);
//Pulldown-Untermenü dazu erzeugen:
speicherMenue = XmCreatePulldownMenu(dateiMenue, "Speichern", (Arg*)
NULL,0);
//Untermenuepunkt "Speichern erzeugen":
button1 = XtCreateManagedWidget("Speichern", xmPushButtonWidgetClass,
speicherMenue, (Arg*) NULL,0);
//Pulldow-Untermenü "speichern als ..." erzeugen:
button2 = XtCreateManagedWidget("Speichern als ...",
xmPushButtonWidgetClass, speicherMenue, (Arg*) NULL,0);
//Menüpunkt "speichern" erzeugen der dieses Untermenü aufruft:
n = 0;
XtSetArg(args[n], XmNsubMenuId, speicherMenue); n++;
XtCreateManagedWidget("Speichern", xmCascadeButtonWidgetClass, dateiMenue,
args, n);
//Menüpunkt "Datei" erzeugen, der dieses Pulldown-Menue aufruft:
n = 0;
XtSetArg(args[n], XmNsubMenuId, dateiMenue); n++;
XtCreateManagedWidget ("Datei", xmCascadeButtonWidgetClass, menueLeistem,
args, n)
385
Die Programmiersprache C++
386
Herunterladen